diff --git a/services/external_market_offer_service.py b/services/external_market_offer_service.py index b5c3592..62fd249 100644 --- a/services/external_market_offer_service.py +++ b/services/external_market_offer_service.py @@ -193,7 +193,7 @@ NORMALIZED_OFFER_FIELDS = [ "name": "title", "label": "商品名稱", "required": True, - "plain_note": "用來做人工確認與名稱比對。", + "plain_note": "用來做 AI 自動驗證確認與名稱比對。", }, { "name": "price", @@ -201,6 +201,30 @@ NORMALIZED_OFFER_FIELDS = [ "required": True, "plain_note": "只填可直接比較的成交或頁面售價。", }, + { + "name": "currency", + "label": "幣別", + "required": False, + "plain_note": "預設 TWD;跨境來源必須保留原幣別。", + }, + { + "name": "product_url", + "label": "賣場連結", + "required": False, + "plain_note": "用來回看商品頁與確認來源可信度。", + }, + { + "name": "image_url", + "label": "商品圖片", + "required": False, + "plain_note": "用來做商品身份、款式與主流商品頁 UI 檢查。", + }, + { + "name": "stock_status", + "label": "庫存狀態", + "required": False, + "plain_note": "例如 in_stock、out_of_stock、preorder 或 unknown。", + }, { "name": "observed_at", "label": "資料時間", @@ -245,6 +269,8 @@ CSV_HEADER_ALIASES = { "currency": {"currency", "幣別"}, "original_price": {"original_price", "原價", "牌價"}, "product_url": {"product_url", "商品網址", "網址", "url"}, + "image_url": {"image_url", "商品圖片", "圖片網址", "圖片", "image"}, + "stock_status": {"stock_status", "availability", "庫存狀態", "庫存", "供貨狀態"}, "brand": {"brand", "品牌"}, "category_text": {"category_text", "分類", "類別"}, "pchome_product_id": { @@ -282,8 +308,10 @@ class ExternalOfferPayload: currency: str = "TWD" original_price: float | None = None product_url: str | None = None + image_url: str | None = None brand: str | None = None category_text: str | None = None + stock_status: str | None = None pchome_product_id: str | None = None momo_sku: str | None = None match_status: str = "unmatched" @@ -302,8 +330,10 @@ class ExternalOfferPayload: "currency": self.currency or "TWD", "original_price": self.original_price, "product_url": self.product_url, + "image_url": self.image_url, "brand": self.brand, "category_text": self.category_text, + "stock_status": self.stock_status, "observed_at": self.observed_at, "ingestion_method": self.ingestion_method, "pchome_product_id": self.pchome_product_id, @@ -460,10 +490,10 @@ def _review_reason_label(reason: Any) -> str: if any(token in lowered for token in ("bundle", "count", "set")): return "組合或件數需確認" if any(token in lowered for token in ("exact", "identity", "match")): - return "品名或規格接近,仍需人工確認" + return "品名或規格接近,仍需 AI 自動驗證確認" if any(token in lowered for token in ("gap", "conflict", "condition")): return "候選資訊有差異,請比對賣場" - return "候選需要人工確認" + return "候選需要 AI 自動驗證確認" def _humanize_review_reasons(reasons: list[Any]) -> list[str]: @@ -599,8 +629,10 @@ def normalize_external_offer_payload(payload: dict[str, Any]) -> tuple[ExternalO currency=str(payload.get("currency") or "TWD").strip() or "TWD", original_price=_to_float(payload.get("original_price")), product_url=payload.get("product_url"), + image_url=payload.get("image_url"), brand=payload.get("brand"), category_text=payload.get("category_text"), + stock_status=payload.get("stock_status") or payload.get("availability"), pchome_product_id=payload.get("pchome_product_id"), momo_sku=payload.get("momo_sku"), match_status=_normalize_match_status(payload.get("match_status") or "unmatched"), @@ -991,7 +1023,7 @@ def _targeted_candidate_to_external_offer( if auto_type not in {"total_price", "unit_price"}: return None, "不是可自動使用的候選" if auto_type == "total_price" and _targeted_candidate_needs_review(candidate): - return None, "候選仍需人工確認" + return None, "候選仍需 AI 自動驗證確認" momo_sku = str(candidate.get("product_id") or candidate.get("goodsCode") or candidate.get("id") or "").strip() pchome_product_id = str(candidate.get("target_pchome_product_id") or "").strip() @@ -1084,11 +1116,11 @@ def _targeted_review_candidate_to_external_offer( *, observed_at: datetime, ) -> tuple[dict[str, Any] | None, str]: - """保存待人工確認候選;這類資料不得進價格判斷。""" + """保存待 AI 自動驗證確認候選;這類資料不得進價格判斷。""" alert_tier = str(candidate.get("target_alert_tier") or "").strip() match_type = str(candidate.get("target_match_type") or "").strip() if alert_tier not in {"identity_review", "unit_price_review"} and match_type in {"", "no_match"}: - return None, "不是可人工確認候選" + return None, "不是可 AI 自動驗證確認候選" momo_sku = str(candidate.get("product_id") or candidate.get("goodsCode") or candidate.get("id") or "").strip() pchome_product_id = str(candidate.get("target_pchome_product_id") or "").strip() @@ -1102,7 +1134,7 @@ def _targeted_review_candidate_to_external_offer( match_score = _quality_score_from_match(candidate.get("target_match_score")) if match_score < 60: - return None, "人工確認候選分數過低" + return None, "AI 自動驗證確認候選分數過低" unit_price_comparison = ( candidate.get("target_unit_price_comparison") @@ -1160,7 +1192,7 @@ def _targeted_review_candidate_to_external_offer( "match_status": "needs_review", "quality_score": round(match_score, 2), "data_quality_status": "needs_review", - "quality_notes_json": json.dumps(["候選已找到,需人工確認同款或色號"], ensure_ascii=False), + "quality_notes_json": json.dumps(["候選已找到,需 AI 自動驗證確認同款或色號"], ensure_ascii=False), "raw_payload_json": json.dumps(raw_payload, ensure_ascii=False), }, "" @@ -1173,7 +1205,7 @@ def sync_targeted_momo_candidates_to_external_offers( ) -> dict[str, Any]: """把頁面自動找到的安全 MOMO 候選同步進 external_offers。 - 只接受 total_price 與 unit_price 自動候選;人工確認候選不寫入。 + 只接受 total_price 與 unit_price 自動候選;AI 自動驗證確認候選不寫入。 """ generated_at = datetime.now().isoformat(timespec="seconds") candidates = list(candidates or []) @@ -1251,7 +1283,7 @@ def sync_targeted_momo_review_candidates_to_external_offers( *, dry_run: bool = False, ) -> dict[str, Any]: - """把待確認 MOMO 候選保存起來,供人工審核;不進價格告警。""" + """把待確認 MOMO 候選保存起來,供 AI 例外決策;不進價格告警。""" generated_at = datetime.now().isoformat(timespec="seconds") candidates = list(candidates or []) required_tables = {"external_market_sources", "external_offers"} @@ -1308,7 +1340,7 @@ def sync_targeted_momo_review_candidates_to_external_offers( "source_code": "momo_reference", "skipped_reasons": skipped_reasons, "message": ( - "已保存待人工確認的 MOMO 候選。" + "已保存待 AI 自動驗證確認的 MOMO 候選。" if not dry_run else "已完成待確認候選預檢,尚未寫入資料。" ), @@ -1464,9 +1496,9 @@ def _classify_offer_record(record: ExternalOfferPayload | None, errors: list[str return { "status_code": "review", - "status_label": "需人工確認", + "status_label": "需 AI 自動驗證確認", "can_enter_alerts": False, - "reasons": reasons or ["需要人工確認"], + "reasons": reasons or ["需要 AI 自動驗證確認"], } @@ -1597,6 +1629,43 @@ def _normalized_offer_stats(conn) -> dict[str, dict[str, Any]]: } +def build_offer_evidence_contract() -> dict[str, Any]: + """Return the normalized offer evidence contract used by all external sources.""" + fields_by_name = {field["name"]: field for field in NORMALIZED_OFFER_FIELDS} + evidence_field_names = [ + "source_product_id", + "title", + "product_url", + "image_url", + "price", + "stock_status", + "observed_at", + "quality_score", + ] + return { + "version": "offer_evidence_contract_v2", + "plain_summary": "所有平台都要整理成同一份 offer evidence,才能進作戰清單或告警。", + "minimum_fields": [ + field["name"] for field in NORMALIZED_OFFER_FIELDS if field["required"] + ], + "professional_evidence_fields": [ + fields_by_name[name] for name in evidence_field_names if name in fields_by_name + ], + "quality_gate": { + "minimum_quality_score": 76, + "requires_verified_match_for_alerts": True, + "unverified_data_route": "待確認候選,不進告警", + }, + "mainstream_alignment": [ + "穩定平台商品 ID", + "商品名稱與賣場連結", + "商品圖片與庫存狀態", + "售價、幣別與資料時間", + "可信度與同款狀態", + ], + } + + def build_connector_contracts() -> dict[str, Any]: """回傳 connector 與手動 CSV 共同遵守的欄位規格。""" return { @@ -1605,6 +1674,7 @@ def build_connector_contracts() -> dict[str, Any]: "plain_summary": "所有外部市場資料都先轉成同一份商品報價格式,再進作戰清單。", "sources": SOURCE_CONTRACTS, "normalized_offer_fields": NORMALIZED_OFFER_FIELDS, + "offer_evidence_contract": build_offer_evidence_contract(), "manual_csv": { "encoding": "utf-8-sig", "required_headers": [ @@ -1678,12 +1748,13 @@ def build_external_source_readiness(engine=None) -> dict[str, Any]: "review_offer_count": review_offer_count, "sources": sources, "connector_contract": build_connector_contracts(), + "offer_evidence_contract": build_offer_evidence_contract(), "plain_summary": "MOMO 先用;其他主流平台已列管,未接合法穩定來源前不進告警。", } def list_momo_review_candidates(engine, *, limit: int = 20) -> dict[str, Any]: - """列出待人工確認的 MOMO 候選,供前台直接處理。""" + """列出待 AI 自動驗證確認的 MOMO 候選,供前台直接處理。""" limit = max(1, min(int(limit or 20), 50)) generated_at = datetime.now().isoformat(timespec="seconds") required_tables = {"external_offers"} @@ -1818,7 +1889,7 @@ def update_momo_review_candidate(engine, offer_id: int, action: str, *, note: st generated_at = datetime.now().isoformat(timespec="seconds") new_match_status = "verified" if action == "confirm" else "rejected" new_quality_status = "verified" if action == "confirm" else "rejected" - label = "人工確認同款" if action == "confirm" else "人工排除候選" + label = "AI 自動驗證確認同款" if action == "confirm" else "AI 排除候選" review_note = str(note or "").strip()[:240] with engine.begin() as conn: diff --git a/services/momo_crawler.py b/services/momo_crawler.py index f7f6509..ee6cd0b 100644 --- a/services/momo_crawler.py +++ b/services/momo_crawler.py @@ -665,7 +665,7 @@ def search_momo_products_for_pchome_products( unit_price_comparison = {} auto_compare_type = "manual_review" price_basis = "none" - review_status = "需人工確認" + review_status = "需 AI 自動驗證確認" if ( not hard_veto and comparison_mode == "exact_identity" @@ -748,7 +748,7 @@ def search_momo_products_for_pchome_products( True, ( f"已用 {searched_products} 筆 PChome 商品搜尋 MOMO,找到 {len(candidates)} 筆候選" - f"(可直接比價 {exact_count} 筆、自動單位價比較 {unit_count} 筆、需人工確認 {review_count} 筆)" + f"(可直接比價 {exact_count} 筆、自動單位價比較 {unit_count} 筆、需 AI 自動驗證確認 {review_count} 筆)" ), candidates, ) diff --git a/templates/admin/business_intel.html b/templates/admin/business_intel.html index a6cd7a4..e93bd49 100644 --- a/templates/admin/business_intel.html +++ b/templates/admin/business_intel.html @@ -594,7 +594,7 @@ {% if unfollowed_count > 0 %}
{{ unfollowed_count }} 筆高信心 AI 價格建議尚未跟進,建議優先轉為行動計畫或標記原因。
- 需人工決策 + 需 AI 例外決策
{% endif %} diff --git a/templates/ai_intelligence.html b/templates/ai_intelligence.html index 5c1d588..b7ea9dd 100644 --- a/templates/ai_intelligence.html +++ b/templates/ai_intelligence.html @@ -3452,7 +3452,7 @@
最近處理紀錄 - 挑品、比價與人工覆核 + 挑品、比價與 AI 例外決策
@@ -4210,7 +4210,7 @@ function renderGrowthActionHint(stats) { } if (needsMapping > 0 && reviewCandidateCount > 0) { - hint.textContent = `先處理 ${mappedCount.toLocaleString()} 件可立即處理商品;另有 ${reviewCandidateCount.toLocaleString()} 筆候選等人工確認。`; + hint.textContent = `先處理 ${mappedCount.toLocaleString()} 件可立即處理商品;另有 ${reviewCandidateCount.toLocaleString()} 筆候選等待 AI 自動驗證確認。`; return; } diff --git a/templates/code_review.html b/templates/code_review.html index d60bc02..d87632c 100644 --- a/templates/code_review.html +++ b/templates/code_review.html @@ -482,7 +482,7 @@ function renderEA(ea, autoFix) {
${priorityLabel}
-
${autoFix ? '🔧 自動修復已觸發' : (ea.human_review_needed ? '👁 需人工審查' : '✅ 無需修復')}
+
${autoFix ? '🔧 自動修復已觸發' : (ea.human_review_needed ? '👁 需 AI 例外審查' : '✅ 無需修復')}
${escHtml(ea.reasoning||'')}
${fixFiles ? `
修復範圍:${fixFiles}
` : ''} diff --git a/templates/price_comparison.html b/templates/price_comparison.html index c361f9a..9906ce9 100644 --- a/templates/price_comparison.html +++ b/templates/price_comparison.html @@ -1258,7 +1258,7 @@ La Roche-Posay 安得利防曬液 50ml,920 } else if (momoUnitCompareCandidates.length) { showToast(`已自動換算 ${momoUnitCompareCandidates.length} 筆單位價候選`, 'success'); } else if (momoReviewCandidates.length) { - showToast(`找到 ${momoReviewCandidates.length} 筆需人工確認候選,暫不進自動比價`, 'warning'); + showToast(`找到 ${momoReviewCandidates.length} 筆需 AI 自動驗證確認候選,暫不進自動比價`, 'warning'); } else { showToast(data.message || '目前沒有找到 MOMO 候選', 'warning'); } @@ -1326,7 +1326,7 @@ La Roche-Posay 安得利防曬液 50ml,920 const labels = Array.isArray(item?.target_match_reason_labels) ? item.target_match_reason_labels.filter(Boolean) : []; - return labels.length ? labels : ['需人工確認同款']; + return labels.length ? labels : ['需 AI 自動驗證確認同款']; } function renderMomoReviewPanel() { @@ -1572,7 +1572,7 @@ La Roche-Posay 安得利防曬液 50ml,920 if (!momoCount && unitCount) { setText('priceReadySummary', '已有單位價比較'); - setNextAction('今天先看:自動單位價比較', `系統已自動換算 ${unitCount} 筆候選,不需要先人工確認。`, '查看單位價', 'focus-momo-unit'); + setNextAction('今天先看:自動單位價比較', `系統已自動換算 ${unitCount} 筆候選,不需要先做 AI 自動驗證確認。`, '查看單位價', 'focus-momo-unit'); return; } diff --git a/templates/vendor_stockout_list_v2.html b/templates/vendor_stockout_list_v2.html index 7fdfa6c..faca083 100644 --- a/templates/vendor_stockout_list_v2.html +++ b/templates/vendor_stockout_list_v2.html @@ -52,7 +52,7 @@
失敗
{{ stats.failed | number_format }}
-
需人工檢查
+
需 AI 例外檢查
來源廠商