diff --git a/config.py b/config.py index 10e1b19..cd7e520 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.677" +SYSTEM_VERSION = "V10.678" 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 e133d2e..2983203 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -754,3 +754,4 @@ POSTGRES_HOST=momo-db | 2026-06-25 | 匯入、缺貨、設定與通知模板頁不可外露 SQL / 資料表 / 模板代碼 | V10.675 起匯入 job API 對前台回傳白話處置訊息,保留 raw error 於 DB/log;自動匯入失敗通知不再顯示 `psycopg2`、`daily_sales_snapshot`、`snapshot_date` 等內部字串;缺貨首頁、系統設定與通知模板列表改用營運語言,並補上逾時匯入任務重置與取消 API。 | | 2026-06-25 | 觀測台入口與通知預覽不可用工程主語干擾營運判讀 | V10.676 起觀測台導覽統一使用「AI 分工矩陣」,通知模板列表會把 K8s/Pod/資料庫/CI Pipeline 等內部詞轉成服務健康、資料連線與部署流程;主機健康事件與自癒劇本改顯示任務/問題/處置提醒,不直接露 `unknown_task`、`scheduler_task_failure`、`CODE_FIX` 等 raw code。 | | 2026-06-25 | 部署監控不得用退役正式域名判定失敗 | V10.677 起 CI/CD 狀態 API 與 active blackbox 監控預設以 `PUBLIC_URL` / `PROD_BASE_URL` 對齊現行正式入口 `https://mo.wooo.work/health`,不再把 `momo.wooo.work` timeout 判成正式部署失敗;Webcrumbs loader fallback 也改為資訊級降級訊號,避免健康頁與 log 產生假紅燈。 | +| 2026-06-25 | 匯入任務公開摘要不得回傳資料表或本機檔案定位 | V10.678 起 `/api/import_jobs` / `/api/import_job/` 的 `import_summary` 只回傳營運摘要、日期範圍、匯入筆數與同步狀態,不再外露 `table_name`、`synced_to`、`daily_sales_snapshot`、`realtime_sales_monthly`、Google Drive file id 或本機暫存路徑。 | diff --git a/routes/openclaw_bot_routes.py b/routes/openclaw_bot_routes.py index c910e58..7f0f198 100644 --- a/routes/openclaw_bot_routes.py +++ b/routes/openclaw_bot_routes.py @@ -8124,7 +8124,7 @@ def handle_cmd(cmd, arg, chat_id, reply_to): date_min = summ.get('date_min', '') date_max = summ.get('date_max', '') - synced = summ.get('synced_to', '') + sync_status = summ.get('sync_status', '') date_range_str = ( f"`{date_min}` ~ `{date_max}`" if date_min and date_max and date_min != date_max @@ -8138,8 +8138,8 @@ def handle_cmd(cmd, arg, chat_id, reply_to): f"📦 *匯入筆數*:`{sr:,}` / `{tr:,}` 筆\n" f"📅 *涵蓋日期*:{date_range_str}\n" ) - if synced: - result_msg += f"🔄 *同步至*:`{synced}`\n" + if sync_status: + result_msg += f"🔄 *處理狀態*:`{sync_status}`\n" result_msg += f"\n_✨ 業績資料已更新,可立即查詢!_" # 取涵蓋日期的 latest date 顯示快速按鈕 diff --git a/services/import_service.py b/services/import_service.py index 9920c9c..f386e22 100644 --- a/services/import_service.py +++ b/services/import_service.py @@ -73,11 +73,50 @@ def humanize_import_error(message: Any) -> str: return compact[:180] + ("…" if len(compact) > 180 else "") +def _public_import_summary(raw_summary: Any) -> Optional[Dict[str, Any]]: + """Return an operator-facing import summary without table names or SQL sync internals.""" + if not raw_summary: + return None + + summary = raw_summary + if isinstance(raw_summary, str): + try: + summary = json.loads(raw_summary) + except Exception: + return {"message": "匯入摘要暫時無法解析,請以任務狀態為準。"} + + if not isinstance(summary, dict): + return None + + sync_success = summary.get("sync_success") + public = { + "message": summary.get("message") or "匯入任務已完成。", + "imported_count": summary.get("imported_count"), + "verified": bool(summary.get("verified")), + "date_min": summary.get("date_min"), + "date_max": summary.get("date_max"), + "source_sheet": summary.get("source_sheet"), + "source_header_row": summary.get("source_header_row"), + } + if sync_success is True: + public["sync_status"] = "已更新業績分析儀表板" + elif sync_success is False: + public["sync_status"] = "業績分析儀表板同步未完成" + public["sync_error_message"] = humanize_import_error(summary.get("sync_error")) + else: + public["sync_status"] = "" + + return {key: value for key, value in public.items() if value not in (None, "")} + + def _public_import_job_payload(job: ImportJob) -> Dict[str, Any]: payload = job.to_dict() display_error = humanize_import_error(payload.get("error_message")) payload["error_message"] = display_error payload["display_error_message"] = display_error + payload["import_summary"] = _public_import_summary(payload.get("import_summary")) + payload.pop("drive_file_id", None) + payload.pop("local_file_path", None) return payload diff --git a/tests/test_auto_import_failure_boundaries.py b/tests/test_auto_import_failure_boundaries.py index 22dbb8c..16028e2 100644 --- a/tests/test_auto_import_failure_boundaries.py +++ b/tests/test_auto_import_failure_boundaries.py @@ -202,6 +202,48 @@ def test_import_job_public_payload_hides_database_error(monkeypatch, tmp_path): session.close() +def test_import_job_public_payload_hides_import_summary_internals(monkeypatch, tmp_path): + import_service = _load_import_service(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}") + import_service.Base.metadata.create_all(import_service.engine) + + service = import_service.ImportService() + job_id = service.create_import_job("daily_sales", "drive-file-1", "daily.xlsx", 1024) + + session = import_service.Session() + try: + job = session.query(import_service.ImportJob).filter_by(id=job_id).one() + job.status = "completed" + job.import_summary = json.dumps({ + "imported_count": 42, + "table_name": "daily_sales_snapshot", + "synced_to": "realtime_sales_monthly", + "sync_success": True, + "sync_error": None, + "verified": True, + "date_min": "2026-06-24", + "date_max": "2026-06-25", + "source_sheet": "即時業績明細", + "source_header_row": 1, + "message": "成功匯入 42 筆資料,已同步至業績分析儀表板", + }, ensure_ascii=False) + job.local_file_path = "data/temp/daily.xlsx" + session.commit() + finally: + session.close() + + public_job = service.get_job_status(job_id) + public_text = json.dumps(public_job, ensure_ascii=False) + + assert public_job["import_summary"]["imported_count"] == 42 + assert public_job["import_summary"]["sync_status"] == "已更新業績分析儀表板" + assert "daily_sales_snapshot" not in public_text + assert "realtime_sales_monthly" not in public_text + assert "table_name" not in public_text + assert "synced_to" not in public_text + assert "drive-file-1" not in public_text + assert "local_file_path" not in public_text + + def test_google_drive_auth_refuses_browser_without_json_token(monkeypatch, tmp_path): import services.google_drive_service as google_drive_service