diff --git a/config.py b/config.py index 6034712..685f0a9 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.719" +SYSTEM_VERSION = "V10.720" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 999ab19..ebaddfb 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -1,8 +1,8 @@ # PChome 業績成長自動化作戰系統 — AI 競價情報模組 Single Source of Truth -> **最後更新**: 2026-06-26 (台北時間) +> **最後更新**: 2026-06-27 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯;PChome 後台業績匯入韌性已補強;產品定位正名為「PChome 業績成長自動化作戰系統」;外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢、前台操作入口、高可見頁面繁中化守門、比價/作戰 UI 工作台化、跨平台來源治理與商品身份 UI 契約已建立,GCP embedding 熔斷延後處理、110 proxy rescue 與 direct host health skip 已建立 -> **適用版本**: V10.719 +> **適用版本**: V10.720 --- @@ -804,3 +804,4 @@ POSTGRES_HOST=momo-db | 2026-06-26 | 供貨風險頁不得使用資料表或英文模組名作為主語 | V10.717 起缺貨清單與補貨通知頁統一使用「供貨風險、缺貨處理清單、補貨通知紀錄」等營運語言,不再顯示「缺貨資料表、缺貨資料、Vendor Stockout」等資料庫或英文模組感文案。 | | 2026-06-26 | AI 觀測頁不得外露 caller key | V10.718 起 AI 品質診斷與知識召回頁使用「使用情境」作為可見主語,並透過 `obs_label.caller()` 顯示營運名稱;前台不得直接顯示 `{{ caller }}`、`top_k` 或「全部呼叫端」等工程語言。 | | 2026-06-26 | 商品來源頁不得提供 raw JSON 匯出 | V10.719 起 `/pchome_crawler` 改為「PChome 商品監控」營運清單,只提供表格與賣場清單 CSV;前台不得出現 `exportJson`、`JSON.stringify(currentProducts)`、`圖片URL`、`商品URL` 或 raw JSON 檔名。 | +| 2026-06-27 | 設定頁監控來源不得直接外露 crawler 命名 | V10.720 起 `/settings` 的商品監控來源由 API 邊界轉為營運名稱與說明,前端卡片再以 `escapeHtml(monitorText(...))` 顯示;啟停與頻率更新訊息統一使用「監控來源」,不得回傳「爬蟲 XXX 已啟用」這類工程主語。 | diff --git a/routes/crawler_management_routes.py b/routes/crawler_management_routes.py index 380ac0b..b3b28b6 100644 --- a/routes/crawler_management_routes.py +++ b/routes/crawler_management_routes.py @@ -4,22 +4,71 @@ 爬蟲管理 API 路由 提供網頁介面來管理爬蟲的啟用/停用狀態 """ -from flask import Blueprint, jsonify, request, render_template, redirect, url_for +from flask import Blueprint, jsonify, request, redirect, url_for from services.crawler_config_loader import ( load_crawler_config, update_crawler_status, get_crawler_info, get_enabled_crawlers, - get_paused_crawlers + get_paused_crawlers, + CONFIG_PATH as CRAWLER_CONFIG_PATH, ) import json -import os from datetime import datetime # 創建 Blueprint crawler_bp = Blueprint('crawler_management', __name__) -CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'data', 'crawler_config.json') +SOURCE_LABELS = { + 'momo_main': ('MOMO 主站商品監控', '追蹤 MOMO 主站商品與售價,支援 PChome 價差判斷。'), + 'edm_promo': ('MOMO 限時活動監控', '追蹤 MOMO 限時活動商品,補齊促銷壓力與主推機會。'), + 'festival_11': ('1.1 檔期活動監控', '檔期活動商品來源;活動結束時可暫停等待下次啟用。'), + 'mothers_day_2026': ('母親節檔期活動監控', '追蹤母親節檔期活動商品與促銷壓力。'), + 'valentine_520_2026': ('520 情人節活動監控', '追蹤 520 檔期活動商品與促銷壓力。'), + 'labor_day_2026': ('勞動節活動監控', '追蹤勞動節檔期活動商品與促銷壓力。'), +} + + +def _operator_text(value): + text = str(value or '').strip() + if not text: + return '' + replacements = { + '爬蟲任務': '資料擷取任務', + '商品爬蟲': '商品監控', + '促銷爬蟲': '促銷活動監控', + 'EDM 爬蟲': 'EDM 活動監控', + '爬蟲': '監控來源', + '保留程式碼和邏輯': '保留設定', + '同版型活動': '同類型活動', + } + for old, new in replacements.items(): + text = text.replace(old, new) + return text + + +def _sanitize_source_info(crawler_key, info): + if not info: + return None + sanitized = dict(info) + label, description = SOURCE_LABELS.get( + crawler_key, + (_operator_text(sanitized.get('name')) or '商品監控來源', _operator_text(sanitized.get('description'))), + ) + sanitized['name'] = label + sanitized['description'] = description or _operator_text(sanitized.get('description')) or '支援商品、價格與促銷監控。' + for field in ('pause_reason', 'notes', 'activity_name'): + if field in sanitized: + sanitized[field] = _operator_text(sanitized.get(field)) + sanitized.pop('function', None) + return sanitized + + +def _sanitize_sources(crawlers): + return { + key: _sanitize_source_info(key, info) + for key, info in (crawlers or {}).items() + } @crawler_bp.route('/crawler_management') def crawler_management_page(): @@ -33,7 +82,7 @@ def get_crawlers(): config = load_crawler_config() return jsonify({ "status": "success", - "data": config.get('crawlers', {}) + "data": _sanitize_sources(config.get('crawlers', {})) }) except Exception as e: return jsonify({ @@ -49,12 +98,12 @@ def get_crawler(crawler_key): if info is None: return jsonify({ "status": "error", - "message": f"爬蟲 {crawler_key} 不存在" + "message": "監控來源不存在" }), 404 return jsonify({ "status": "success", - "data": info + "data": _sanitize_source_info(crawler_key, info) }) except Exception as e: return jsonify({ @@ -87,11 +136,12 @@ def toggle_crawler(crawler_key): # 取得更新後的資訊 info = get_crawler_info(crawler_key) + display_info = _sanitize_source_info(crawler_key, info) or {'name': '監控來源'} return jsonify({ "status": "success", - "message": f"爬蟲 {info.get('name', crawler_key)} 已{'啟用' if enabled else '停用'}", - "data": info + "message": f"監控來源「{display_info.get('name', '商品監控來源')}」已{'啟用' if enabled else '停用'}", + "data": display_info }) except Exception as e: @@ -129,21 +179,22 @@ def update_crawler_schedule(crawler_key): if crawler_key not in config.get('crawlers', {}): return jsonify({ "status": "error", - "message": f"爬蟲 {crawler_key} 不存在" + "message": "監控來源不存在" }), 404 # 更新執行頻率 config['crawlers'][crawler_key]['schedule_hours'] = schedule_hours - config['metadata']['last_updated'] = datetime.now().isoformat() + config.setdefault('metadata', {})['last_updated'] = datetime.now().isoformat() # 寫回配置文件 - with open(CONFIG_PATH, 'w', encoding='utf-8') as f: + with open(CRAWLER_CONFIG_PATH, 'w', encoding='utf-8') as f: json.dump(config, f, ensure_ascii=False, indent=2) + updated_info = _sanitize_source_info(crawler_key, config['crawlers'][crawler_key]) return jsonify({ "status": "success", "message": f"執行頻率已更新為每 {schedule_hours} 小時", - "data": config['crawlers'][crawler_key] + "data": updated_info }) except Exception as e: diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index 35eed09..5befc1b 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -190,6 +190,8 @@ def test_growth_workflow_pages_hide_raw_export_and_fallback_content(): pchome_crawler = (ROOT / "templates/pchome_crawler.html").read_text(encoding="utf-8") market_intel = (ROOT / "templates/market_intel/disabled.html").read_text(encoding="utf-8") settings = (ROOT / "templates/settings.html").read_text(encoding="utf-8") + settings_js = (ROOT / "web/static/js/page-settings.js").read_text(encoding="utf-8") + crawler_routes = (ROOT / "routes/crawler_management_routes.py").read_text(encoding="utf-8") navbar = (ROOT / "templates/components/_navbar.html").read_text(encoding="utf-8") shell = (ROOT / "templates/components/_ewoooc_shell.html").read_text(encoding="utf-8") dashboard_js = (ROOT / "web/static/js/page-dashboard-v2.js").read_text(encoding="utf-8") @@ -222,6 +224,11 @@ def test_growth_workflow_pages_hide_raw_export_and_fallback_content(): assert "商品監控中心" in settings assert "監控來源設定" in settings + assert "escapeHtml(monitorText(info.name" in settings_js + assert "每 ${scheduleHours} 小時更新" in settings_js + assert "監控來源「" in crawler_routes + assert "CRAWLER_CONFIG_PATH" in crawler_routes + assert '"message": f"爬蟲' not in crawler_routes assert "商品監控" in navbar assert "商品監控狀態" in shell assert "全站商品監控" in dashboard_js diff --git a/web/static/js/page-settings.js b/web/static/js/page-settings.js index 50c2ab1..216f91c 100644 --- a/web/static/js/page-settings.js +++ b/web/static/js/page-settings.js @@ -38,6 +38,15 @@ function showToast(message, type = 'success') { function showLoading() { document.getElementById('loading-overlay').classList.add('active'); } function hideLoading() { document.getElementById('loading-overlay').classList.remove('active'); } function getCSRFToken() { return document.querySelector('meta[name="csrf-token"]').getAttribute('content'); } +function escapeHtml(value) { + const div = document.createElement('div'); + div.textContent = String(value ?? ''); + return div.innerHTML; +} + +function monitorText(value, fallback = '') { + return String(value || fallback).replaceAll('爬蟲', '監控來源'); +} // ───────── Crawler ───────── async function loadCrawlers() { @@ -68,12 +77,19 @@ function createCrawlerCard(key, info) { const card = document.createElement('div'); card.className = `crawler-card ${info.enabled ? 'active' : 'paused'}`; const statusClass = info.enabled ? 'active' : 'paused'; - const statusText = info.enabled ? '運行中' : '已暫停'; + const statusText = info.enabled ? '啟用中' : '已暫停'; + const sourceName = escapeHtml(monitorText(info.name, '商品監控來源')); + const sourceDescription = escapeHtml(monitorText(info.description, '支援商品、價格與促銷監控。')); + const scheduleHours = escapeHtml(info.schedule_hours || 'N/A'); + const lpnCode = escapeHtml(info.lpn_code || ''); + const lastActiveDate = escapeHtml(info.last_active_date || ''); + const pauseReason = escapeHtml(monitorText(info.pause_reason, '')); + const notes = escapeHtml(monitorText(info.notes, '')); card.innerHTML = `
-
${info.name}
+
${sourceName}
${statusText}
-
${info.description || 'N/A'}
-
每 ${info.schedule_hours || 'N/A'} 小時執行
- ${info.lpn_code ? `
活動代碼:${info.lpn_code}
` : ''} - ${info.last_active_date ? `
最後活動:${info.last_active_date}
` : ''} +
${sourceDescription}
+
每 ${scheduleHours} 小時更新
+ ${info.lpn_code ? `
活動代碼:${lpnCode}
` : ''} + ${info.last_active_date ? `
最後活動:${lastActiveDate}
` : ''}
${!info.enabled && info.pause_reason ? `
暫停原因 - ${info.pause_reason} - ${info.notes ? `
${info.notes}` : ''} + ${pauseReason} + ${info.notes ? `
${notes}` : ''}
` : ''} ${info.enabled ? `