V10.432 tighten marketplace near-threshold matching
This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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` reason;Feeder 只能寫入 `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 診斷原因翻成可行動語意:品牌不符已排除、規格不符已排除、補充包不相容、組合規格不相容、系列不符已排除、需單位價比較、低信心待補強等,不可只顯示籠統「待比對」或「身份否決」。
|
||||
|
||||
@@ -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 漏表時 |
|
||||
|
||||
74
docs/memory/current_execution_queue_20260524.md
Normal file
74
docs/memory/current_execution_queue_20260524.md
Normal 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` 版本。
|
||||
- 記錄未完成與下一輪入口。
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-24:PChome 近門檻身份回收第二輪
|
||||
- **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` 寫入規則。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user