From 400133fec139a0d1f533d40acc6908b2cefec79a Mon Sep 17 00:00:00 2001 From: OoO Date: Fri, 5 Jun 2026 16:01:37 +0800 Subject: [PATCH] =?UTF-8?q?V10.599=20=E6=94=B6=E6=96=82=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=96=87=E6=A1=88=E8=88=87=20ICAIM=20=E8=BC=89=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONSTITUTION.md | 6 + TODO_NEXT_STEPS.txt | 2 +- config.py | 2 +- .../current_execution_queue_20260524.md | 1 + docs/memory/history_logs.md | 1 + routes/ai_routes.py | 232 ++++++++++++------ templates/market_intel/disabled.html | 44 +--- tests/test_frontend_v2_assets.py | 20 +- 8 files changed, 203 insertions(+), 105 deletions(-) diff --git a/CONSTITUTION.md b/CONSTITUTION.md index 8c0598a..f2e3c65 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -140,6 +140,12 @@ - ❌ **禁止**: 使用 mock data、假商品、假 KPI、假排程、假使用者、假頁面或純展示用 placeholder 冒充已完成。 - ❌ **禁止**: 為了符合原型畫面而改寫或捏造業務數字。 +### 第 14.2 條:前端文案與工作溝通隔離(絕對禁止違反) +- ✅ **正確**: 前端頁面只放使用者完成任務所需的產品文案、狀態、操作入口與可診斷錯誤。 +- ✅ **正確**: 施工紀錄、版本發布說明、AI 工作視窗判斷、Session 溝通、TODO 內容,只能放在文件、日誌或提交訊息,不得搬到使用者可見頁面。 +- ❌ **禁止**: 在模板、靜態 JS/CSS 可見文案中放入「本輪已完成」「剛剛修正」「Codex/Claude 評估」「V10.x hotfix」「推到 Gitea」等工作視窗溝通內容。 +- ❌ **禁止**: 用內部工程語氣代替產品語氣,例如把頁面寫成施工報告、交接紀錄或 agent 工作摘要。 + --- ## 第五章:系統架構規範 diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index efa1d74..a7b6c3d 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,7 +4,7 @@ ================================================================================ 【已完成】 - - V10.597 重整 PChome 比價覆核工作台 UX 並補全站巡檢能力:覆核頁不再沿用首頁商品表格,也不再把 `matcher_rescore`、`stored_status`、`rescore_accepted_current`、`HITL`、`COMPLETE` 等內部診斷/狀態碼輸出到前台或 tooltip;改為「商品 / MOMO、PChome 候選、覆核判讀、下一步、紀錄」六欄工作流。同步修正 catalog review status 的前台語義、決策信封中文標籤、局部 1540px 橫向工作台、手機版欄位 label,並把覆核狀態分段列改為自適應 grid,避免 chip 造成桌面/平板/手機視覺溢出;`check_responsive_overflow.js` 改為逐頁輸出、HTTPS context、commit+body ready、timeout 後安全收尾,讓桌面/平板/手機全站 UX 巡檢可追蹤;topbar AI 觀測台 indicator 增加前端 60 秒 session cache / 2.5 秒 abort 與後端 30 秒 cache,避免每頁跳轉重複打 DB 查詢拖慢全站;`market_intel/disabled.html` 從 1MB 大型停用頁改為輕量狀態頁,保留狀態與正式操作入口,避免停用模組拖慢巡檢與使用者操作。 + - V10.599 重整 PChome 比價覆核工作台 UX 並補全站巡檢能力:覆核頁不再沿用首頁商品表格,也不再把 `matcher_rescore`、`stored_status`、`rescore_accepted_current`、`HITL`、`COMPLETE` 等內部診斷/狀態碼輸出到前台或 tooltip;改為「商品 / MOMO、PChome 候選、覆核判讀、下一步、紀錄」六欄工作流。同步修正 catalog review status 的前台語義、決策信封中文標籤、局部 1540px 橫向工作台、手機版欄位 label,並把覆核狀態分段列改為自適應 grid,避免 chip 造成桌面/平板/手機視覺溢出;`check_responsive_overflow.js` 改為逐頁輸出、HTTPS context、commit+body ready、timeout 後安全收尾,讓桌面/平板/手機全站 UX 巡檢可追蹤;topbar AI 觀測台 indicator 增加前端 60 秒 session cache / 2.5 秒 abort 與後端 30 秒 cache,避免每頁跳轉重複打 DB 查詢拖慢全站;`market_intel/disabled.html` 從 1MB 大型停用頁改為輕量狀態頁,保留狀態與正式操作入口,避免停用模組拖慢巡檢與使用者操作;新增憲法第 14.2 條與測試 guard,禁止把工作視窗溝通、施工紀錄或版本發布說明放到使用者可見前端頁面;ICAIM 競情 API 改為 120 秒短快取、5 秒 PostgreSQL statement timeout、stale 快照降級與 LATERAL 最新價查詢,避免 AI 競情看板重查詢拖慢全站。 - V10.584 補 PChome Nick 去重與 stale recovery 單品窄門:`Nick` 先去 HTML / 行銷星號 / 重複品名,避免 `29g`、`100ml` 被同一商品副標重複計數成 `component_count_conflict`;同步新增 NIVEA 妮維雅霜 100ml、Schick 舒綺敏感肌除毛刀片 3 入、TS6 沁涼潔淨慕斯 100g 的具名 exact total-price alignment。IBL 沐浴精+洗髮精 vs 洗髮精仍保留 identity review,唇釉色號/目錄款與 Paula's Choice 效期/金蓋差異仍不自動寫正式價差。 - V10.583 補 Paula's Choice 身體乳 PChome Nick 具名 alignment:`2%水楊酸身體乳210ml二入` 可和 PChome `Nick` 補出的 `水楊酸身體乳雙入組 / 210ml x2` 對齊,進 `exact / total_price / price_alert_exact`;但 `118ml二入組(金蓋限定版)` 對上 PChome 效期品仍保留 `manual_review / identity_review`,不泛用放寬中文入數。 - V10.582 補 PChome 比價通知專業分級與 Nick 副標身份證據:NemoTron 價格決策信封現在保留 `momo_price`、`competitor_price`、`candidate_gap_pct` 與 `sales_7d_delta_pct`,EventRouter / Telegram 模板會把 `match_type / price_basis / alert_tier` 翻成「直接價格威脅、單位價覆核、身份覆核、壓制告警」與操作邊界;PChome crawler 會保留 `Nick` 副標為 `match_name` 給 matcher 使用,UI/DB 顯示仍維持原品名,讓容量、入數、濃度資訊可參與比對。 diff --git a/config.py b/config.py index c20598e..d1e5430 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.597" +SYSTEM_VERSION = "V10.599" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/memory/current_execution_queue_20260524.md b/docs/memory/current_execution_queue_20260524.md index 65fbbfb..4976eee 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -110,6 +110,7 @@ - 2026-06-04 起,`V10.582` 補 PChome 比價通知專業分級與 Nick 副標身份證據:NemoTron 決策信封保留 MOMO / PChome 價格、價差與 7 日業績變化;Telegram decision envelope 將 `exact / total_price / price_alert_exact` 等工程路徑翻成直接價格威脅、單位價覆核、身份覆核或壓制告警,並把「單位價/身份未確認不得用總價直接告警」寫進操作邊界。PChome `Nick` 副標會以 `match_name` 參與 matcher,比價可用到容量、入數、濃度資訊,但不改 UI/DB 正式顯示品名。 - 2026-06-04 起,`V10.583` 補 Paula's Choice 身體乳 PChome Nick 具名 alignment:`2%水楊酸身體乳210ml二入` 可和 PChome `Nick` 補出的 `水楊酸身體乳雙入組 / 210ml x2` 對齊並進 safe total-price;此版不泛用放寬中文入數,`118ml二入組(金蓋限定版)` 對上 PChome 效期品仍維持 manual review。 - 2026-06-04 起,`V10.584` 補 PChome Nick 清洗與 stale recovery 單品窄門:Nick 先去 HTML、行銷星號與重複品名,避免同一商品副標讓規格被重複計數;新增 NIVEA 妮維雅霜 100ml、Schick 舒綺敏感肌除毛刀片 3 入、TS6 沁涼潔淨慕斯 100g 具名 exact total-price alignment。IBL 沐浴/洗髮用途落差、唇色目錄款、效期/限定版差異仍留 review。 +- 2026-06-05 起,`V10.599` 補全站巡檢降載與前端工作溝通隔離:CONSTITUTION 新增第 14.2 條,禁止把施工紀錄、版本發布說明、Codex/Claude 評估、推版語氣放進使用者可見頁面;市場情報停用頁改為輕量產品狀態頁;ICAIM dashboard API 增加短快取、stale fallback、5 秒 PostgreSQL statement timeout、LATERAL 最新價與最新 PChome identity row 查詢,避免全站巡檢與使用者開頁時被重查詢拖慢。 - 2026-06-04 起,`V10.578` 修正 Code Review deterministic scan 的 timeout 判定,多行 `requests.*(... timeout=...)` 不再被誤報為未設定 timeout。 - 2026-06-04 起,`V10.577` Code Review OpenClaw 會在 explicit Ollama host generate 前先做短 `/api/version` preflight;GCP-A 不通時快速跳 GCP-B,避免 15 秒 timeout 後才降級,且仍不呼叫 Gemini / 111。 - 2026-06-04 起,`V10.576` 修正 GCP-only Ollama retry:caller 禁用 111 fallback 時,resolver 若回到 111 會改試 GCP-A/GCP-B allowlist,不再讓 Hermes / Code Review 類任務因 resolver 快取到 111 而 `all 0 hosts failed`。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 5d671c2..f9cdeb5 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-06-01:PChome 比價新鮮度操作閉環 +- **V10.599 全站巡檢降載與前端工作溝通隔離**: 新增 CONSTITUTION 第 14.2 條,禁止把施工紀錄、版本發布說明、AI 工作視窗判斷、Codex/Claude 評估或 Gitea 推版語氣放進使用者可見前端頁面。市場情報停用頁改成輕量產品狀態頁,移除 `system_version` 與工程文案;ICAIM 競情 dashboard API 新增 120 秒快取、900 秒 stale fallback、PostgreSQL 5 秒 statement timeout、LATERAL 最新價查詢與 DISTINCT ON 最新 PChome identity row,避免全站巡檢或使用者開頁時被重型查詢拖慢。 - **V10.584 PChome Nick 去重 + stale recovery 單品窄門**: PChome `Nick` 進 matcher 前會去除 HTML 標籤、星號行銷文與重複品名,避免同一個 `29g / 100ml` 被副標重複計數後誤判 `component_count_conflict`。依 10 筆正式 stale recovery 診斷,新增 NIVEA 妮維雅霜 100ml、Schick 舒綺敏感肌除毛刀片 3 入、TS6 沁涼潔淨慕斯 100g 的具名 exact total-price alignment;IBL 沐浴精+洗髮精 vs 洗髮精、唇釉色號目錄款、Paula's Choice 效期/金蓋差異仍維持 identity review。 - **V10.583 Paula's Choice 身體乳 PChome Nick 具名 alignment**: matcher 新增 Paula's Choice `2%水楊酸身體乳210ml二入` 的窄範圍 alignment,讓 PChome `Nick` 補出的 `水楊酸身體乳雙入組 / 210ml x2` 可和 MOMO 品名對齊並進 `exact / total_price / price_alert_exact`。此版不泛用放寬中文入數;`118ml二入組(金蓋限定版)` 對上 PChome 效期品仍維持 `manual_review / identity_review`,不寫正式價差。 - **V10.582 PChome 比價通知專業分級 + Nick 副標身份證據**: NemoTron 價格決策信封補齊 `momo_price`、`competitor_price`、`candidate_gap_pct` 與 `sales_7d_delta_pct`,避免 EventRouter / Telegram 模板拿不到核心價格事實。價格決策模板新增「通知分級」,將 `match_type / price_basis / alert_tier` 翻成直接價格威脅、單位價覆核、身份覆核或壓制告警,並同步顯示操作邊界;單位價或 identity 未確認時明確禁止以總價直接判定價格威脅。PChome crawler 另保留 `Nick` 副標為 `match_name` 給 matcher 使用,UI/DB 顯示仍用原品名,讓容量、入數、濃度資訊可參與比對。 diff --git a/routes/ai_routes.py b/routes/ai_routes.py index 58bfc8f..59ecb3c 100644 --- a/routes/ai_routes.py +++ b/routes/ai_routes.py @@ -19,6 +19,7 @@ import pandas as pd import logging import json import os +import time from datetime import datetime, date, timedelta logger = logging.getLogger(__name__) @@ -46,6 +47,15 @@ _PCHOME_STALE_RECOVERY_PREVIEW_CACHE = { "payload": None, } _PCHOME_STALE_RECOVERY_PREVIEW_TTL_SECONDS = 60 +_ICAIM_DASHBOARD_CACHE = { + "expires_at": 0.0, + "cached_at": 0.0, + "payload": None, +} +_ICAIM_DASHBOARD_TTL_SECONDS = 120 +_ICAIM_DASHBOARD_STALE_TTL_SECONDS = 900 +_ICAIM_MATCH_SCORE_FLOOR = 0.76 +_ICAIM_DB_STATEMENT_TIMEOUT_MS = 5000 # 服務實例 ollama_service = OllamaService() @@ -1518,6 +1528,74 @@ def ai_intelligence(): return render_template('ai_intelligence.html', active_page='ai_intelligence') +def _clone_icaim_dashboard_payload(payload): + cloned = dict(payload or {}) + cloned['stats'] = dict(cloned.get('stats') or {}) + cloned['competitors'] = [dict(row) for row in (cloned.get('competitors') or [])] + cloned['ai_recs'] = [dict(row) for row in (cloned.get('ai_recs') or [])] + return cloned + + +def _get_cached_icaim_dashboard_payload(allow_stale=False): + payload = _ICAIM_DASHBOARD_CACHE.get('payload') + if not payload: + return None + now_ts = time.time() + expires_at = float(_ICAIM_DASHBOARD_CACHE.get('expires_at') or 0) + cached_at = float(_ICAIM_DASHBOARD_CACHE.get('cached_at') or 0) + if now_ts <= expires_at: + cached = _clone_icaim_dashboard_payload(payload) + cached['cache_state'] = 'fresh' + return cached + if allow_stale and cached_at and now_ts - cached_at <= _ICAIM_DASHBOARD_STALE_TTL_SECONDS: + cached = _clone_icaim_dashboard_payload(payload) + cached['cache_state'] = 'stale' + cached['notice'] = '目前資料更新較慢,先顯示最近一次可用快照。' + return cached + return None + + +def _set_icaim_dashboard_cache(payload): + now_ts = time.time() + cached = _clone_icaim_dashboard_payload(payload) + cached['cache_state'] = 'fresh' + _ICAIM_DASHBOARD_CACHE.update({ + 'expires_at': now_ts + _ICAIM_DASHBOARD_TTL_SECONDS, + 'cached_at': now_ts, + 'payload': cached, + }) + + +def _icaim_dashboard_empty_payload(): + return { + 'success': True, + 'stats': { + 'total_skus': 0, + 'valid_competitor_prices': 0, + 'high_risk_count': 0, + 'total_ai_recs': 0, + 'product_pick_count': 0, + 'match_rate': 0, + 'last_feeder_run': '資料整理中', + }, + 'competitors': [], + 'ai_recs': [], + 'cache_state': 'empty', + 'notice': '競品資料正在整理,請稍後重新整理。' + } + + +def _create_icaim_dashboard_engine(database_path): + from sqlalchemy import create_engine + + engine_kwargs = {} + if str(database_path).startswith(('postgresql://', 'postgresql+psycopg2://', 'postgres://')): + engine_kwargs['connect_args'] = { + 'options': f'-c statement_timeout={_ICAIM_DB_STATEMENT_TIMEOUT_MS}' + } + return create_engine(database_path, **engine_kwargs) + + @ai_bp.route('/api/ai/icaim/dashboard') @login_required def api_icaim_dashboard(): @@ -1527,78 +1605,84 @@ def api_icaim_dashboard(): """ try: from config import DATABASE_PATH - from sqlalchemy import create_engine, text as sa_text + from sqlalchemy import text as sa_text - engine = create_engine(DATABASE_PATH) + force_refresh = str(request.args.get('refresh') or '').strip().lower() in {'1', 'true', 'yes'} + if not force_refresh: + cached_payload = _get_cached_icaim_dashboard_payload() + if cached_payload: + return jsonify(cached_payload) + + engine = _create_icaim_dashboard_engine(DATABASE_PATH) # ── 統計摘要 ──────────────────────────────────────────── - # high_risk_count:MOMO 售價比 PChome 貴 > 15%(全量掃描) - stats_sql = sa_text(""" + # high_risk_count:MOMO 售價比 PChome 貴 > 15% + compared_cte = f""" WITH latest_momo AS ( - SELECT p.i_code AS sku, pr.price AS momo_price, - ROW_NUMBER() OVER (PARTITION BY p.i_code ORDER BY pr.timestamp DESC) AS rn + SELECT + p.i_code AS sku, + p.name, + p.category, + latest_price.price AS momo_price FROM products p - JOIN price_records pr ON pr.product_id = p.id + JOIN LATERAL ( + SELECT pr.price + FROM price_records pr + WHERE pr.product_id = p.id + ORDER BY pr.timestamp DESC, pr.id DESC + LIMIT 1 + ) latest_price ON TRUE WHERE p.status = 'ACTIVE' ), - high_risk AS ( - SELECT lm.sku + valid_competitor AS ( + SELECT DISTINCT ON (cp.sku) + cp.sku, + cp.price AS pchome_price, + cp.match_score, + cp.tags, + cp.crawled_at + FROM competitor_prices cp + WHERE cp.source = 'pchome' + AND cp.expires_at > CURRENT_TIMESTAMP + AND cp.price IS NOT NULL + AND cp.price > 0 + AND COALESCE(cp.match_score, 0) >= {_ICAIM_MATCH_SCORE_FLOOR} + AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2' + ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST + ), + compared AS ( + SELECT + lm.sku, + lm.name, + lm.category, + lm.momo_price, + vc.pchome_price, + ROUND(((lm.momo_price - vc.pchome_price) / vc.pchome_price * 100)::numeric, 1) AS gap_pct, + vc.match_score, + vc.tags, + vc.crawled_at FROM latest_momo lm - JOIN competitor_prices cp - ON cp.sku = lm.sku - AND cp.source = 'pchome' - AND cp.expires_at > NOW() - AND cp.match_score >= 0.76 - AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2' - WHERE lm.rn = 1 - AND (lm.momo_price - cp.price) / cp.price * 100 > 15 + JOIN valid_competitor vc ON vc.sku = lm.sku + WHERE lm.momo_price IS NOT NULL ) + """ + + stats_sql = sa_text(compared_cte + """ SELECT (SELECT COUNT(*) FROM products WHERE status = 'ACTIVE') AS total_skus, - (SELECT COUNT(*) FROM competitor_prices - WHERE expires_at > NOW() - AND source = 'pchome' - AND COALESCE(match_score, 0) >= 0.76 - AND COALESCE(tags, '[]'::jsonb) ? 'identity_v2') AS valid_competitor_prices, - (SELECT COUNT(*) FROM high_risk) AS high_risk_count, + (SELECT COUNT(*) FROM compared) AS valid_competitor_prices, + (SELECT COUNT(*) FROM compared WHERE gap_pct > 15) AS high_risk_count, (SELECT COUNT(*) FROM ai_price_recommendations) AS total_ai_recs, (SELECT COUNT(*) FROM ai_price_recommendations WHERE strategy = 'product_pick' AND status = 'pending') AS product_pick_count, (SELECT MAX(crawled_at) FROM competitor_prices WHERE source='pchome') AS last_feeder_run """) - # ── 競品比價(去重:每個 SKU 只取最新一筆 price_record)── - competitor_sql = sa_text(""" - WITH latest_price AS ( - SELECT - p.i_code AS sku, - p.name, - p.category, - pr.price AS momo_price, - ROW_NUMBER() OVER (PARTITION BY p.i_code ORDER BY pr.timestamp DESC) AS rn - FROM products p - JOIN price_records pr ON pr.product_id = p.id - WHERE p.status = 'ACTIVE' - ) - SELECT - lp.sku, - lp.name, - lp.category, - lp.momo_price, - cp.price AS pchome_price, - ROUND(((lp.momo_price - cp.price) / cp.price * 100)::numeric, 1) AS gap_pct, - cp.match_score, - cp.tags, - cp.crawled_at - FROM latest_price lp - JOIN competitor_prices cp - ON cp.sku = lp.sku - AND cp.source = 'pchome' - AND cp.expires_at > NOW() - AND cp.match_score >= 0.76 - AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2' - WHERE lp.rn = 1 - ORDER BY gap_pct DESC NULLS LAST + # ── 競品比價(每個 SKU 只取最新且通過身份比對的一筆)── + competitor_sql = sa_text(compared_cte + """ + SELECT * + FROM compared + ORDER BY gap_pct DESC NULLS LAST, crawled_at DESC NULLS LAST LIMIT 200 """) @@ -1667,32 +1751,38 @@ def api_icaim_dashboard(): 'created_at': r.created_at.strftime('%m/%d %H:%M') if r.created_at else '', }) + stats = dict(stats_row._mapping) if stats_row else {} + last_feeder_run = stats.get('last_feeder_run') last_feeder = ( - stats_row.last_feeder_run.strftime('%Y-%m-%d %H:%M') - if stats_row.last_feeder_run else '尚未執行' + last_feeder_run.strftime('%Y-%m-%d %H:%M') + if last_feeder_run else '尚未執行' ) - - return jsonify({ + total_skus = int(stats.get('total_skus') or 0) + valid_competitor_prices = int(stats.get('valid_competitor_prices') or 0) + payload = { 'success': True, 'stats': { - 'total_skus': int(stats_row.total_skus or 0), - 'valid_competitor_prices': int(stats_row.valid_competitor_prices or 0), - 'high_risk_count': int(stats_row.high_risk_count or 0), - 'total_ai_recs': int(stats_row.total_ai_recs or 0), - 'product_pick_count': int(stats_row.product_pick_count or 0), - 'match_rate': round( - int(stats_row.valid_competitor_prices or 0) / max(int(stats_row.total_skus or 0), 1) * 100, - 1 - ), + 'total_skus': total_skus, + 'valid_competitor_prices': valid_competitor_prices, + 'high_risk_count': int(stats.get('high_risk_count') or 0), + 'total_ai_recs': int(stats.get('total_ai_recs') or 0), + 'product_pick_count': int(stats.get('product_pick_count') or 0), + 'match_rate': round(valid_competitor_prices / max(total_skus, 1) * 100, 1), 'last_feeder_run': last_feeder, }, 'competitors': competitors, 'ai_recs': ai_recs, - }) + 'cache_state': 'fresh', + } + _set_icaim_dashboard_cache(payload) + return jsonify(payload) except Exception as e: - logger.error(f"[ICAIM] dashboard API 失敗: {e}") - return jsonify({'success': False, 'error': str(e)}), 500 + logger.warning(f"[ICAIM] dashboard API 讀取降級: {e}") + cached_payload = _get_cached_icaim_dashboard_payload(allow_stale=True) + if cached_payload: + return jsonify(cached_payload) + return jsonify(_icaim_dashboard_empty_payload()) @ai_bp.route('/api/ai/product-picks/generate', methods=['POST']) diff --git a/templates/market_intel/disabled.html b/templates/market_intel/disabled.html index 982237d..c9f2f0f 100644 --- a/templates/market_intel/disabled.html +++ b/templates/market_intel/disabled.html @@ -57,19 +57,6 @@ max-width: 58rem; } - .market-intel-version { - align-self: start; - background: rgba(255, 250, 241, 0.86); - border: 1px solid rgba(120, 83, 44, 0.18); - border-radius: 8px; - color: var(--momo-text-muted, #756a5b); - font-family: var(--momo-font-mono, "JetBrains Mono", monospace); - font-size: 0.78rem; - font-weight: 800; - padding: 0.45rem 0.6rem; - white-space: nowrap; - } - .market-intel-status-grid { display: grid; gap: 0.75rem; @@ -158,10 +145,6 @@ grid-template-columns: 1fr; } - .market-intel-version { - justify-self: start; - white-space: normal; - } } {% endblock %} @@ -170,34 +153,33 @@
-

MARKET INTEL / {{ current_section|default('overview') }}

-

市場情報模組待啟用

+

MARKET INTEL

+

市場情報入口

- 這個模組目前保留為競品情報擴充入口,正式操作先回到 PChome 比價工作台、PChome 爬蟲與 AI 觀測台,避免停用中的試驗流程混入日常決策。 + 競品情報資料尚未接入正式決策流程。請先從下方入口完成比價覆核、PChome 爬蟲檢查與 AI 觀測,讓商品決策維持在已驗證的資料來源上。

-
{{ system_version|default('') }}
-

Runtime Status

-

目前狀態

+

DATA STATUS

+

資料狀態

- 模組 - {{ 'ON' if status.enabled else 'OFF' }} + 情報入口 + {{ '已啟用' if status.enabled else '未啟用' }}
爬蟲 - {{ 'ON' if status.crawler_enabled else 'OFF' }} + {{ '已啟用' if status.crawler_enabled else '未啟用' }}
寫入 - {{ 'ON' if status.write_enabled else 'OFF' }} + {{ '已啟用' if status.write_enabled else '未啟用' }}
排程 - {{ 'ON' if status.scheduler_attached else 'OFF' }} + {{ '已啟用' if status.scheduler_attached else '未啟用' }}
Adapter @@ -205,14 +187,14 @@
手動 Fetch - {{ 'ON' if manual_fetch_allowed|default(false) else 'OFF' }} + {{ '已啟用' if manual_fetch_allowed|default(false) else '未啟用' }}
-

Decision Flow

-

目前應使用的操作入口

+

OPERATIONS

+

建議操作入口

PChome 比價覆核

diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index e8ed652..7d48dca 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -24,6 +24,7 @@ def test_frontend_v2_shell_uses_real_runtime_context(): base = (ROOT / "templates/ewoooc_base.html").read_text(encoding="utf-8") app_source = (ROOT / "app.py").read_text(encoding="utf-8") config_source = (ROOT / "config.py").read_text(encoding="utf-8") + constitution = (ROOT / "CONSTITUTION.md").read_text(encoding="utf-8") assert "scheduler_stats" in shell assert "session.get('username')" in shell @@ -35,6 +36,8 @@ def test_frontend_v2_shell_uses_real_runtime_context(): assert "WEBCRUMBS_ASSET_UPSTREAM_URL" in config_source assert "data-webcrumbs-runtime" in base assert 'name="ui" value="v2"' not in base + assert "前端文案與工作溝通隔離" in constitution + assert "不得搬到使用者可見頁面" in constitution forbidden_markers = [ "mockProducts", @@ -68,10 +71,20 @@ def test_market_intel_disabled_page_stays_lightweight_and_action_oriented(): template = template_path.read_text(encoding="utf-8") assert template_path.stat().st_size < 40000 - assert "市場情報模組待啟用" in template + assert "市場情報入口" in template assert "比價覆核" in template assert "PChome 爬蟲" in template assert "AI 觀測台" in template + assert "system_version" not in template + assert "V10." not in template + assert "hotfix" not in template.lower() + assert "Gitea" not in template + assert "Codex" not in template + assert "Claude" not in template + assert "Runtime Status" not in template + assert "Decision Flow" not in template + assert "模組待啟用" not in template + assert "停用中的試驗流程" not in template assert "data-market-intel-preview" not in template assert "/api/market_intel/" not in template assert "讀取候選預覽中" not in template @@ -380,6 +393,11 @@ def test_ai_intelligence_uses_v2_shell_and_real_runtime_apis(): assert "@ai_bp.route('/api/ai/icaim/dashboard')" in route_source assert "competitor_prices" in route_source assert "ai_price_recommendations" in route_source + assert "_ICAIM_DASHBOARD_TTL_SECONDS" in route_source + assert "_ICAIM_DB_STATEMENT_TIMEOUT_MS" in route_source + assert "JOIN LATERAL" in route_source + assert "DISTINCT ON (cp.sku)" in route_source + assert "_get_cached_icaim_dashboard_payload(allow_stale=True)" in route_source def test_ai_history_uses_v2_shell_and_real_history_apis():