V10.432 tighten marketplace near-threshold matching

This commit is contained in:
OoO
2026-05-24 16:41:14 +08:00
parent d650b10797
commit 051c654713
7 changed files with 174 additions and 4 deletions

View File

@@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.431"
SYSTEM_VERSION = "V10.432"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -2,7 +2,7 @@
> **最後更新**: 2026-05-24 (台北時間)
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地LLM 路由紅線升級為 Ollama-first 三主機級聯Gemini 備援預設關閉
> **適用版本**: V10.431
> **適用版本**: V10.432
---
@@ -371,6 +371,7 @@ LEFT JOIN competitor_prices cp
- `services/competitor_identity_revalidator.py` 可對既有 `competitor_prices` legacy row 離線重跑 `identity_v2`:只有新版 matcher 分數 `>= 0.76` 且無 hard veto 才補 `identity_v2` / `legacy_revalidated` tags預設不刷新 `expires_at`,避免過期價格進入決策。
- `CompetitorPriceFeeder.run_expired_identity_refresh()` 會優先刷新已通過 `identity_v2` 但 TTL 過期的 PChome row直接用既有 `competitor_product_id` 批次呼叫 PChome 商品 API再用新版 matcher 重新驗證名稱/規格/價格 sanity通過後寫回 `competitor_prices``competitor_price_history`。這條路徑提升新鮮價格覆蓋率,但不降低 match threshold也不讓過期價格直接進入決策。
- `marketplace_product_matcher.py` 的擴充只能走「正向證據 + 反向 veto」品牌一致、商品線/型號訊號強、價格合理且無 hard veto 時才允許 `strong_product_line_match` 加分;補充瓶/補充包/refill 與一般正裝不互相配對,分享組/加量組/明星組等組合包不得誤配單品。
- 近門檻規則必須成對補「召回 + 防錯配」測試:可召回者需有品牌、商品線、規格或具名 identity anchor例如 MUJI 精油芬香護手霜、Mustela 慕之幼爽身潤膚乳、Herbacin 小甘菊護手霜;防錯配者需成為 hard veto例如 M·A·C Macximal 柔霧/緞光唇膏質地、ERBE 指甲清垢棒/指甲緣刨刀功能、Schick 舒芙/舒綺女用除毛刀品線。不得用單一同規格或同品牌放寬全域門檻。
- 套組/買送/件數不同但品牌、核心商品線與單一基礎規格一致時matcher 必須回傳 `comparison_mode='unit_comparable'``unit_comparable` reasonFeeder 只能寫入 `competitor_match_attempts.attempt_status='unit_comparable'``refresh_unit_comparable`,不得寫入 `competitor_prices`。Dashboard 與 `competitor_intel_repository` 必須用 `build_unit_price_comparison()` 產生每 ml / 每 g / 每入單位價證據,讓 PPT / AI 報表可說明「需單位價比較」而不是把總價當同款價差。商品看板在正式配對尚未成立時,仍必須顯示最佳候選 PChome 商品名稱、候選價與「候選價需單位換算」說明讓人工覆核可直接看見下一步daily/growth、PPT 與 OpenClaw 摘要不得自建查詢,需消費 `fetch_competitor_review_queue()` 與 coverage 的 `unit_comparable_count`。若任一側含多個不同容量/重量規格,視為多品項套組,不可進 `unit_comparable`
- PChome feeder 的外部 request timeout 由 `PCHOME_FEEDER_TIMEOUT` 控制,預設 12 秒;排程不得因單一 PChome 搜尋 API timeout 被拖到數分鐘。
- 商品看板的 PChome 狀態必須把 matcher 診斷原因翻成可行動語意:品牌不符已排除、規格不符已排除、補充包不相容、組合規格不相容、系列不符已排除、需單位價比較、低信心待補強等,不可只顯示籠統「待比對」或「身份否決」。

View File

@@ -13,6 +13,7 @@
| 檔案 | 用途 | 何時閱讀 |
|---|---|---|
| `history_logs.md` | 重大里程碑與歷史脈絡 | 要理解演進背景、排查「為何變成這樣」時 |
| `current_execution_queue_20260524.md` | 目前核心產品推進佇列涵蓋比價、圖表、PPT、觀測台、外部入口、效能與部署驗證 | 接續本輪「批准繼續」、避免漏掉工作項目、需要新 session 交接時 |
| `ai_automation_closure_20260429.md` | 四 AI Agent 自動化閉環、Smoke、metrics 與 Grafana 觀測實況 | 接續 AI 自動化、EventRouter、AutoHeal、OpenClaw memory、ElephantAlpha 編排、可觀測性時 |
| `credentials_passbook.md` | 伺服器、帳密、埠位對照 | 需要維運、部署、憑證核對時 |
| `feedback_db_metadata_import.md` | SQLAlchemy metadata / `create_all()` 漏表鐵律 | 新增 model、修 schema、排查 fresh env 漏表時 |

View File

@@ -0,0 +1,74 @@
# Current Execution Queue 2026-05-24
> 目的:把目前使用者要求的核心產品優化整理成單一執行佇列,避免 UI、比價、PPT、AI 觀測台與部署驗證漏項。
> 原則:準確率優先;不放寬 MOMO / PChome 全域比對門檻;正式部署不碰 `momo-db`;每個工作流都需有測試或正式 smoke。
## 0. 部署與驗證通道基準
- 確認 `main`、Gitea、正式環境版本一致。
- 修復或確認 SSH / Gitea / 188 hop 可用。
- 每次上線只 recreate `momo-app``scheduler``telegram-bot`,禁止使用 `--remove-orphans`,禁止影響 `momo-db`
## 1. MOMO / PChome 核心比價準確率
- 查正式 `competitor_match_attempts` 最新狀態分布與高量低信心 cohort。
- 以小批次 pilot 處理 `recoverable_low_score`,優先品線:
- DASHING DIVA
- aroma / diffuser / essential oil
- lip / cosmetic variant
- private-care / body-care
- 只新增窄範圍、可解釋 matcher 規則。
- 保留 `MIN_MATCH_SCORE``identity_veto`、既有正式候選覆寫保護。
- 驗收:`matched` 有增加、目標 `low_score` 下降、`needs_review` 不異常上升、無明顯跨色號/跨款式/跨劑型錯配。
## 2. 商品列表與人工覆核閉環
- 商品列表不得再大量顯示籠統「待對比」。
- 將狀態拆成:尚未搜尋、價格過期待刷新、近門檻可救回、證據不足、既有強配對保護、已排除、需單位價比較、找不到同款。
- 每筆覆核要顯示候選 PChome 商品、候選價、match score、診斷原因、下一步動作。
- 人工採用 / 否決 / 單位價 / 補搜尋必須能回寫 review queue並影響 feeder 後續行為。
## 3. 12 Agent 決策信封整合
- `decision_envelope` 已接到 NemoTron 價格告警與人工覆核,下一步要讓 OpenClaw、ElephantAlpha、PPT QA 與 review queue 共用同一份 evidence contract。
- 告警不得再輸出空泛「預期效益」必須帶資料品質、證據來源、HITL 邊界與 trace id。
- Agent 建議只能輔助排序與分析,不得繞過 matcher / feeder / review service 寫正式價格。
## 4. 業績分析資料與圖表修復
- 修正即時業績匯入 `snapshot_date text = date` 類型錯誤。
- `/daily_sales``/growth_analysis` 圖表不得空白;需保留原本圖表並升級成更專業的呈現。
- 圖表需通過 runtime nonblank canvas 檢查與手機版 responsive。
- daily/growth/PPT 必須共用 `competitor_intel_repository` 的比價資料出口,避免價差方向或統計口徑分裂。
## 5. PPT 視覺 QA 與自動簡報產線
- 每日、每週、每月、每季、半年、年度簡報需依排程產出。
- 每次產出與視覺 QA 結果必須完整寫入 DB。
- `/observability/ppt_audit_history` 必須清楚顯示 runtime 狀態、產出狀態、視覺 QA、問題追蹤與可預覽檔案。
- PPTX / PDF 預覽需可站內直接開啟,不能只下載。
## 6. 外部 BI / 協作入口
- `/metabase` 不可空白,需顯示可診斷 bridge 狀態或可用替代入口。
- `/grist` / 資料協作連結不得連到其他專案站。
- 側欄與 topbar 外部工具入口要統一走 momo-pro bridge route。
## 7. AI 觀測台與全站 UI/UX
- 10 個 AI 觀測台頁面必須符合新版字體、字級、暖墨色、焦糖 accent、點陣視覺與 responsive 規範。
- 全站主要頁面需通過 desktop / mobile overflow guard。
- 表格與圖表只允許在局部容器橫向滾動,不可造成整頁無限延伸。
## 8. 效能與可觀測性
- 持續降低 `/daily_sales``/growth_analysis`、商品看板、PChome queue、PPT audit 首屏 TTFB。
- 避免 worker cold start 重查重算;必要時使用共享快取與指紋失效。
- 111 fallback 只作最後救急;持續監控 GCP-A / GCP-B / 111 用量與 circuit breaker。
## 9. 每輪收尾
- Focused tests → full pytest 或合理範圍回歸 → production smoke。
- 更新 SOT / memory / TODO。
- 推 Gitea正式部署確認 `/health` 版本。
- 記錄未完成與下一輪入口。

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-24PChome 近門檻身份回收第二輪
- **V10.432 近門檻比價 hard-veto 補強**: marketplace matcher 不放寬 `MIN_MATCH_SCORE`,針對正式 `true_low_confidence` 前段新增窄範圍防錯配M.A.C `MACximal` 柔霧唇膏 vs 緞光唇膏標記 `makeup_finish_conflict`、ERBE 指甲清垢棒 vs 指甲緣刨刀標記 `nail_tool_function_conflict`、Schick 舒芙 vs 舒綺仕女除毛刀標記 `schick_razor_line_conflict`,三者皆進 hard veto同時把 `潤膚乳` / `身體乳` / `嬰兒乳液` / `寶寶乳液` 納入乳液型別,讓慕之幼爽身潤膚乳等真同款回刷更穩定。新增測試鎖住 MUJI 護手霜、Mustela 慕之幼潤膚乳、Herbacin 小甘菊護手霜可 exact並確保高 variant 錯配不被 focused rule 推進。
- **V10.431 Telegram callback byte-safe**: `triaged_alert()``momo:eig:*` HITL callback 改為依 UTF-8 byte 長度截斷,不再用字元數截斷;中文或過長 `event.id` / `decision_envelope.decision_id` 仍會保留可追蹤 payload且保證 `callback_data` 不超過 Telegram 64-byte 限制,避免專業排版告警因 callback 太長而整則送出失敗。
- **V10.430 NemoTron 決策 callback 追蹤 ID 修補**: `NemoTronDispatcher._send_telegram()` 會把 `decision_envelope.decision_id` 提升為 EventRouter `event.id``triaged_alert()` 也會在上游缺 `event.id` 時改用 `decision_id` 產生 `momo:eig:*` callback避免價格決策通知的「忽略此事件」audit 落成 `unknown` 而無法追查。
- **V10.429 111 / NemoTron 治理回歸補齊**: 補齊 `.env.example` 中 111 circuit breaker、111 allowlist proxy、部署 smoke、資料庫與 Redis runtime keys並同步大檔 inventory 行數,讓完整測試可覆蓋最新 `V10.425``V10.428` 變更;此版不放寬商品比對門檻、不修改 `competitor_prices` 寫入規則。

View File

@@ -677,7 +677,7 @@ PRODUCT_TYPES = {
"粉底棒": ("粉底棒", "foundation stick"),
"精華": ("精華", "精華液", "essence", "serum", "安瓶"),
"化妝水": ("化妝水", "機能水", "toner", "lotion"),
"乳液": ("乳液", "按摩乳", "emulsion", "milk"),
"乳液": ("乳液", "按摩乳", "潤膚乳", "身體乳", "嬰兒乳液", "寶寶乳液", "emulsion", "milk"),
"面霜": ("面霜", "乳霜", "", "cream"),
"防曬": ("防曬", "spf", "uv", "sunscreen"),
"洗面乳": ("洗面乳", "洗顏", "潔面", "cleanser", "foam"),
@@ -1900,6 +1900,15 @@ def score_marketplace_match(
makeup_usage_conflict = _has_makeup_usage_conflict(left, right)
if makeup_usage_conflict:
reasons.append("makeup_usage_conflict")
makeup_finish_conflict = _has_makeup_finish_conflict(left, right)
if makeup_finish_conflict:
reasons.append("makeup_finish_conflict")
nail_tool_function_conflict = _has_nail_tool_function_conflict(left, right)
if nail_tool_function_conflict:
reasons.append("nail_tool_function_conflict")
schick_razor_line_conflict = _has_schick_razor_line_conflict(left, right)
if schick_razor_line_conflict:
reasons.append("schick_razor_line_conflict")
lancome_line_conflict = _has_lancome_ultra_line_conflict(left, right)
if lancome_line_conflict:
reasons.append("lancome_line_conflict")
@@ -1948,6 +1957,12 @@ def score_marketplace_match(
hard_veto = True
if makeup_usage_conflict:
hard_veto = True
if makeup_finish_conflict:
hard_veto = True
if nail_tool_function_conflict:
hard_veto = True
if schick_razor_line_conflict:
hard_veto = True
if lancome_line_conflict:
hard_veto = True
if dr_hsieh_line_conflict:
@@ -2752,6 +2767,61 @@ def _has_makeup_usage_conflict(left: ProductIdentity, right: ProductIdentity) ->
return bool((left_cheek and right_eye) or (left_eye and right_cheek))
def _has_makeup_finish_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
left_text = left.searchable_name
right_text = right.searchable_name
if "mac" not in (left.brand_tokens & right.brand_tokens):
return False
if not (
"macximal" in left_text
and "macximal" in right_text
and "唇膏" in left_text
and "唇膏" in right_text
):
return False
matte_terms = ("柔霧", "霧面", "matte")
satin_terms = ("緞光", "satin")
left_matte = any(term in left_text for term in matte_terms)
right_matte = any(term in right_text for term in matte_terms)
left_satin = any(term in left_text for term in satin_terms)
right_satin = any(term in right_text for term in satin_terms)
return bool((left_matte and right_satin) or (left_satin and right_matte))
def _has_nail_tool_function_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
left_text = left.searchable_name
right_text = right.searchable_name
if "erbe" not in (left.brand_tokens & right.brand_tokens):
return False
if "指甲" not in left_text or "指甲" not in right_text:
return False
cleaning_terms = ("清垢棒", "清潔棒")
plane_terms = ("指甲緣刨刀", "刨刀")
left_cleaning = any(term in left_text for term in cleaning_terms)
right_cleaning = any(term in right_text for term in cleaning_terms)
left_plane = any(term in left_text for term in plane_terms)
right_plane = any(term in right_text for term in plane_terms)
return bool((left_cleaning and right_plane) or (left_plane and right_cleaning))
def _has_schick_razor_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
left_text = left.searchable_name
right_text = right.searchable_name
if not ({"schick", "舒適牌"} & (left.brand_tokens & right.brand_tokens)):
return False
pair_text = f"{left_text} {right_text}"
if "除毛刀" not in pair_text:
return False
women_razor_terms = ("仕女", "除毛刀")
if not all(term in pair_text for term in women_razor_terms):
return False
left_silk_effects = "舒芙" in left_text
right_silk_effects = "舒芙" in right_text
left_intuition = "舒綺" in left_text
right_intuition = "舒綺" in right_text
return bool((left_silk_effects and right_intuition) or (left_intuition and right_silk_effects))
def _has_lancome_ultra_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
left_text = left.searchable_name
right_text = right.searchable_name

View File

@@ -1330,6 +1330,10 @@ def test_marketplace_matcher_keeps_high_variant_low_score_lines_outside_focused_
"【Solone】持久眼線筆(眼線膠 超防暈推薦)",
"Solone 斜角眉筆 0.35g",
)
erbe_tool_gap = score_marketplace_match(
"ERBE 德國不鏽鋼指甲清垢棒",
"ERBE 德國雙頭指甲緣刨刀",
)
sunscreen_line_gap = score_marketplace_match(
"【我的心機】溫和寶貝兒童防曬乳35ml(SPF50+ PA+++)",
"我的心機 海洋友善保濕高效防曬乳35ml(SPF50+PA++++)",
@@ -1349,6 +1353,7 @@ def test_marketplace_matcher_keeps_high_variant_low_score_lines_outside_focused_
romand_line_gap,
summer_eve_variant_gap,
solone_type_gap,
erbe_tool_gap,
sunscreen_line_gap,
):
assert diagnostics.score < 0.76
@@ -1364,6 +1369,12 @@ def test_marketplace_matcher_keeps_high_variant_low_score_lines_outside_focused_
assert "cotton_swab_variant_conflict" in muji_swab.reasons
assert lancome_line_gap.hard_veto is True
assert "lancome_line_conflict" in lancome_line_gap.reasons
assert mac_finish_gap.hard_veto is True
assert "makeup_finish_conflict" in mac_finish_gap.reasons
assert schick_bundle_gap.hard_veto is True
assert "schick_razor_line_conflict" in schick_bundle_gap.reasons
assert erbe_tool_gap.hard_veto is True
assert "nail_tool_function_conflict" in erbe_tool_gap.reasons
def test_marketplace_matcher_rejects_saugella_private_wash_variant_gap():
@@ -1729,8 +1740,20 @@ def test_marketplace_matcher_promotes_safe_exact_spec_near_threshold():
momo_price=211,
competitor_price=319,
)
muji_hand_cream = score_marketplace_match(
"【MUJI 無印良品】精油芬香護手霜 50g",
"MUJI 無印良品 精油芬香護手霜/薰衣草&迷迭香/50g",
)
mustela_lotion_2pack = score_marketplace_match(
"【Mustela 慕之恬廊】慕之幼 爽身潤膚乳500mlX2",
"Mustela慕之恬廊 慕之幼爽身潤膚乳500mlX2",
)
herbacin_hand_cream = score_marketplace_match(
"【Herbacin 德國小甘菊】小甘菊1號護手霜20ml",
"Herbacin 小甘菊經典護手霜20ml",
)
for diagnostics in (opi, mustela, romand):
for diagnostics in (opi, mustela, romand, muji_hand_cream, mustela_lotion_2pack, herbacin_hand_cream):
assert diagnostics.score >= 0.76
assert diagnostics.hard_veto is False
assert "strong_exact_spec_match" in diagnostics.reasons