From aadbce73e584036701109304f3cae5c0e1f2effa Mon Sep 17 00:00:00 2001 From: ogt Date: Thu, 25 Jun 2026 15:18:15 +0800 Subject: [PATCH] fix: sanitize observability and review UI copy --- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 1 + routes/admin_observability_routes.py | 221 ++++++++++++++++---- routes/import_routes.py | 11 +- routes/sales_routes.py | 4 +- routes/system_public_routes.py | 54 ++++- scripts/check_observability_ui.py | 2 +- scripts/observability_contract.py | 22 +- templates/admin/_observability_labels.html | 8 +- templates/admin/agent_orchestration.html | 8 +- templates/admin/ai_calls_dashboard.html | 10 +- templates/admin/budget.html | 6 +- templates/admin/host_health.html | 24 +-- templates/admin/observability_overview.html | 24 +-- templates/admin/ppt_audit_history.html | 26 +-- templates/admin/promotion_review.html | 10 +- templates/admin/quality_trend.html | 8 +- templates/admin/rag_queries.html | 16 +- templates/ai_recommend.html | 4 +- templates/code_review.html | 34 +-- templates/components/_ewoooc_shell.html | 6 +- templates/components/_navbar.html | 6 +- web/static/js/observability-charts.js | 28 +-- web/static/js/page-ai-recommend.js | 4 +- web/static/js/page-logs.js | 17 +- 25 files changed, 387 insertions(+), 169 deletions(-) diff --git a/config.py b/config.py index 50ccfb6..fb729fd 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.681" +SYSTEM_VERSION = "V10.682" 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 445f9d3..06c80ac 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -758,3 +758,4 @@ POSTGRES_HOST=momo-db | 2026-06-25 | 部署後 Code Review 不得把模型 timeout 寫成部署錯誤 | V10.679 起本地掃描可收斂的 Code Review 報告不再顯示「最後錯誤 / all hosts failed / OpenClaw timeout」等模型內部訊息;歷史 API 讀舊紀錄時也即時轉為「AI 延伸分析暫時略過,已以本地掃描完成部署後檢查」。 | | 2026-06-25 | Code Review 歷史理由不得外露模型路由 | V10.680 起 Code Review history 的 `ea_decision.reasoning` 與舊報告讀取層會把 OpenClaw、GCP-A/GCP-B、111 重分析、fallback 等內部模型路由轉成「AI 延伸分析暫時不可用時,維持本地掃描收斂」。 | | 2026-06-25 | 舊 Code Review 報告需廣義清除模型路由殘留 | V10.681 起舊 `openclaw_report` 讀取層用廣義規則移除 GCP-A/GCP-B、111 與 fallback 相關整段文字,避免歷史報告仍殘留內部模型拓撲。 | +| 2026-06-25 | 全站前台不可再把 AI 模型路由、資料表、raw log 當使用者訊息 | V10.682 起 PPT 視覺 QA、Logs、Code Review、AI 助手與觀測台主要頁面改用「AI 模型、知識命中、工具編排、產出紀錄、修復流程」等營運語言;PPT 審核舊錯誤與 Logs API 會即時脫敏 IP、模型失敗、金鑰路徑與資料表名稱,成長分析空狀態不再顯示 `realtime_sales_monthly`。 | diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index 3ac54da..4c13752 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -45,6 +45,32 @@ _HEALTH_INDICATOR_CACHE = { 'payload': None, } _HEALTH_INDICATOR_CACHE_TTL_SECONDS = 30 +_PPT_PUBLIC_RUNTIME_ERROR = '視覺審核暫時無法完成;請先用線上預覽人工確認版面,稍後重新執行審核。' +_PPT_INTERNAL_ERROR_MARKERS = ( + 'all 3 hosts failed', + 'httpconnectionpool', + 'multimodal data provided', + 'model does not support', + '/api/generate', + 'connectionerror', + 'readtimeout', + 'traceback', + 'gcp-a', + 'gcp-b', +) +_PPT_PUBLIC_REPLACEMENTS = ( + ('AiderHeal', '修復流程'), + ('RAG', '知識建議'), + ('Ollama', 'AI 模型服務'), + ('minicpm-v', '視覺模型'), + ('LibreOffice', '轉檔服務'), + ('runtime', '執行條件'), + ('DB', '產出紀錄'), + ('database', '產出紀錄'), + ('filesystem', '檔案來源'), + ('ppt_audit_results', '審核紀錄'), + ('ppt_generation_runs', '產出紀錄'), +) _GEMINI_BACKUP_CALLER_DISPLAY = { @@ -67,7 +93,99 @@ _GEMINI_BACKUP_CALLERS = { def _list_ppt_aider_heal_active_jobs(): with _PPT_AIDER_HEAL_LOCK: - return [dict(job) for job in _PPT_AIDER_HEAL_ACTIVE.values()] + jobs = [dict(job) for job in _PPT_AIDER_HEAL_ACTIVE.values()] + for job in jobs: + job['diagnosis'] = _public_ppt_text(job.get('diagnosis'), max_chars=100) + return jobs + + +def _ppt_text_has_internal_detail(value) -> bool: + text = str(value or '') + if not text: + return False + lowered = text.lower() + if any(marker in lowered for marker in _PPT_INTERNAL_ERROR_MARKERS): + return True + return bool(re.search(r'\b(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?\b', text)) + + +def _public_ppt_text(value, *, empty='', max_chars=180): + """把 PPT 觀測台的內部錯誤轉成操作員可讀的處置文字。""" + text = str(value or '').strip() + if not text: + return empty + if _ppt_text_has_internal_detail(text): + return _PPT_PUBLIC_RUNTIME_ERROR + for raw, label in _PPT_PUBLIC_REPLACEMENTS: + text = text.replace(raw, label) + text = re.sub(r'\b(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?\b', '內部主機', text) + text = re.sub(r'\s+', ' ', text).strip() + if max_chars and len(text) > max_chars: + return text[:max_chars].rstrip() + '…' + return text + + +def _public_ppt_text_list(values, *, max_chars=120): + public_values = [] + for value in values or []: + text = _public_ppt_text(value, max_chars=max_chars) + if text and text not in public_values: + public_values.append(text) + return public_values + + +def _public_ppt_source_label(source): + return { + 'both': '檔案 + 產出紀錄', + 'database': '產出紀錄', + 'filesystem': '檔案來源', + }.get(str(source or '').strip(), '檔案來源') + + +def _public_ppt_vision_status(status): + status = dict(status or {}) + status['summary'] = _public_ppt_text( + status.get('summary'), + empty='視覺檢查狀態待確認。', + max_chars=140, + ) + status['status_label'] = _public_ppt_text( + status.get('status_label'), + empty='待確認', + max_chars=40, + ) + status['model_label'] = '視覺模型' if status.get('model') else '未啟用' + status['converter_label'] = '轉檔服務' if status.get('converter') else '轉檔條件待確認' + status['blockers'] = _public_ppt_text_list( + status.get('blockers'), + max_chars=80, + ) or ['視覺檢查條件待確認'] + status['next_actions'] = _public_ppt_text_list( + status.get('next_actions'), + max_chars=90, + ) or ['確認視覺檢查條件後重新整理此頁。'] + sanitized_checks = [] + for check in status.get('readiness_checks') or []: + if not isinstance(check, dict): + continue + item = dict(check) + item['label'] = _public_ppt_text(item.get('label'), empty='檢查項目', max_chars=50) + item['value'] = _public_ppt_text(item.get('value'), empty='待確認', max_chars=50) + item['detail'] = _public_ppt_text(item.get('detail'), empty='等待檢查結果', max_chars=90) + sanitized_checks.append(item) + status['readiness_checks'] = sanitized_checks + return status + + +def _public_ppt_vision_audit_status(status): + status = dict(status or {}) + status['status_label'] = _public_ppt_text(status.get('status_label'), empty='待確認', max_chars=60) + status['message'] = _public_ppt_text( + status.get('message'), + empty='視覺檢查狀態待確認。', + max_chars=140, + ) + return status def _build_ai_call_recent_row(row): @@ -1909,16 +2027,17 @@ def ppt_audit_trigger_aider_heal(): 'pptx_filename': pptx_filename, 'target_file': 'services/ppt_generator.py', 'queued_at': queued_at, - 'diagnosis': diagnosis[:160], + 'diagnosis': _public_ppt_text(diagnosis, max_chars=120), } with _PPT_AIDER_HEAL_LOCK: if heal_key in _PPT_AIDER_HEAL_ACTIVE: existing_job = dict(_PPT_AIDER_HEAL_ACTIVE.get(heal_key) or active_job) + existing_job['diagnosis'] = _public_ppt_text(existing_job.get('diagnosis'), max_chars=120) return jsonify({ 'ok': True, 'status': 'already_running', 'action': 'CODE_FIX', - 'message': '這份簡報的 AiderHeal 已在背景執行中,請等 Telegram/Gitea/CD 結果回報。', + 'message': '這份簡報的修復流程已在背景執行中,請等通知結果回報。', 'target_file': 'services/ppt_generator.py', 'active_count': len(_PPT_AIDER_HEAL_ACTIVE), 'job': existing_job, @@ -1959,7 +2078,7 @@ def ppt_audit_trigger_aider_heal(): 'ok': True, 'status': 'queued', 'action': 'CODE_FIX', - 'message': 'AiderHeal 已排入背景執行;完成後會由 Telegram/Gitea/CD 結果回報。', + 'message': '修復流程已排入背景執行;完成後會由通知結果回報。', 'target_file': 'services/ppt_generator.py', 'active_count': len(_list_ppt_aider_heal_active_jobs()), 'job': active_job, @@ -2572,7 +2691,7 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run if not vision_status.get('ready'): health_status = 'partial' health_title = '視覺審核環境待確認' - health_message = 'PPT 可產出與預覽,但 minicpm-v / LibreOffice 的 runtime 狀態仍需維持就緒。' + health_message = 'PPT 可產出與預覽,但視覺檢查與轉檔條件仍需維持就緒。' elif run_error_count or broken_file_count: health_status = 'error' health_title = '產線有異常待處理' @@ -2584,11 +2703,11 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run elif audit_total and pass_rate < 80: health_status = 'partial' health_title = '審核通過率偏低' - health_message = f'本月視覺 QA 通過率 {pass_rate:.1f}%,需優先檢查失敗熱點與 RAG 修法建議。' + health_message = f'本月視覺 QA 通過率 {pass_rate:.1f}%,需優先檢查失敗熱點與修復建議。' elif total_count: health_status = 'ready' health_title = '產線覆蓋完整' - health_message = '定義簡報、DB 紀錄、線上預覽與視覺 QA 都已具備可追蹤入口。' + health_message = '定義簡報、產出紀錄、線上預覽與視覺 QA 都已具備可追蹤入口。' else: health_status = 'planned' health_title = '產線等待資料' @@ -2623,12 +2742,12 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run 'status': 'ready' if total_count and missing_count == 0 else 'partial', }, { - 'key': 'database', + 'key': 'records', 'icon': 'database', - 'label': 'DB 寫入', + 'label': '產出紀錄', 'value': f'{len(generation_runs)} 筆', 'meta': f'{run_ready_count} 成功 / {run_error_count} 失敗', - 'detail': latest_run.get('started_at') or '尚無本月寫入紀錄', + 'detail': latest_run.get('started_at') or '尚無本月產出紀錄', 'status': 'error' if run_error_count else ('ready' if generation_runs else 'planned'), }, { @@ -2646,7 +2765,7 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run 'label': '視覺 QA', 'value': qa_value, 'meta': qa_meta, - 'detail': 'minicpm-v + RAG 修法 + AiderHeal', + 'detail': '視覺檢查 + 修復建議 + 派工', 'status': qa_status, }, ] @@ -2675,7 +2794,10 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run triage_entries.append({ 'title': item.get('report_label') or item.get('report_type') or '未知簡報', 'meta': item.get('started_at') or '時間未知', - 'detail': item.get('error_msg') or f"{item.get('schedule_label') or '手動'} · {item.get('target_label') or '最新資料'}", + 'detail': _public_ppt_text( + item.get('error_msg'), + empty=f"{item.get('schedule_label') or '手動'} · {item.get('target_label') or '最新資料'}", + ), 'status_label': '產出失敗', 'filename': item.get('file_name') or '', 'report_type': item.get('report_type') or '', @@ -2685,7 +2807,10 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run triage_entries.append({ 'title': item.get('name') or '未命名檔案', 'meta': item.get('mtime') or '時間未知', - 'detail': item.get('file_error') or 'PPTX 檔案不可預覽,建議重新產生。', + 'detail': _public_ppt_text( + item.get('file_error'), + empty='PPTX 檔案不可預覽,建議重新產生。', + ), 'status_label': '檔案異常', 'filename': item.get('name') or '', 'report_type': item.get('report_type') or '', @@ -2697,7 +2822,10 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run triage_entries.append({ 'title': filename or '未命名檔案', 'meta': item.get('audited_at') or '時間未知', - 'detail': item.get('issue_summary') or item.get('error_msg') or f"問題 {item.get('issues_count', 0)} 個", + 'detail': _public_ppt_text( + item.get('issue_summary') or item.get('error_msg'), + empty=f"問題 {item.get('issues_count', 0)} 個", + ), 'status_label': '視覺 QA', 'filename': filename, 'report_type': inferred_report_type, @@ -2743,7 +2871,7 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run 'meta': item.get('mtime') or '時間未知', 'detail': ( f"{item.get('size_kb') if item.get('size_kb') is not None else '—'} KB · " - f"{item.get('source') or 'filesystem'} · " + f"{item.get('source_label') or _public_ppt_source_label(item.get('source'))} · " f"{'PDF 已快取' if item.get('preview_cache_ready') else '開啟時轉檔'}" ), 'status_label': 'PDF 快取' if item.get('preview_cache_ready') else '線上預覽', @@ -2762,7 +2890,10 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run { 'title': item.get('pptx_filename') or '未命名檔案', 'meta': item.get('audited_at') or '時間未知', - 'detail': item.get('issue_summary') or item.get('error_msg') or f"問題 {item.get('issues_count', 0)} 個,信心 {item.get('confidence', 0):.2f}", + 'detail': _public_ppt_text( + item.get('issue_summary') or item.get('error_msg'), + empty=f"問題 {item.get('issues_count', 0)} 個,信心 {item.get('confidence', 0):.2f}", + ), 'status_label': '需修復' if item.get('audit_status') == 'failed' else '需排查', 'filename': item.get('pptx_filename'), 'report_type': item.get('report_type') or _guess_ppt_report_type_from_filename(item.get('pptx_filename') or ''), @@ -2772,11 +2903,11 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run ], }, { - 'key': 'database', - 'label': 'DB 寫入', + 'key': 'records', + 'label': '產出紀錄', 'status': 'error' if run_error_count else ('ready' if generation_runs else 'planned'), 'count': len(generation_runs), - 'empty_text': '本月尚未看到 ppt_generation_runs 寫入紀錄。', + 'empty_text': '本月尚未看到簡報產出紀錄。', 'entries': [ { 'title': item.get('report_label') or item.get('report_type') or '未知簡報', @@ -2847,13 +2978,13 @@ def _build_ppt_operator_summary(files, auto_generation, pipeline_view, vision_st elif missing_count: status = 'partial' headline = '定期簡報尚未全數補齊' - message = f'本期還有 {missing_count} 類定義簡報缺漏,可手動補齊或等待排程寫入 DB。' + message = f'本期還有 {missing_count} 類定義簡報缺漏,可手動補齊或等待排程寫入產出紀錄。' primary_action = '補齊缺漏' primary_anchor = '#ppt-production-center' elif not vision_status.get('ready'): status = 'partial' headline = '簡報可管理,視覺 QA 待啟用' - message = 'PPT 產出與預覽入口仍可用;視覺模型、LibreOffice 或模型檔需補齊後才會自動審核。' + message = 'PPT 產出與預覽入口仍可用;視覺檢查與轉檔條件補齊後才會自動審核。' primary_action = '查看就緒檢查' primary_anchor = '#ppt-runtime-diagnostic' elif issue_count: @@ -2865,7 +2996,7 @@ def _build_ppt_operator_summary(files, auto_generation, pipeline_view, vision_st else: status = 'ready' if valid_preview_count else 'planned' headline = '簡報工作台待命' - message = '最新簡報、PDF 預覽、DB 寫入與視覺 QA 都集中在同一頁追蹤。' + message = '最新簡報、PDF 預覽、產出紀錄與視覺 QA 都集中在同一頁追蹤。' primary_action = '查看簡報' primary_anchor = '#ppt-deck-workbench' @@ -2888,9 +3019,9 @@ def _build_ppt_operator_summary(files, auto_generation, pipeline_view, vision_st 'latest_deck': latest_preview or {}, 'latest_deck_label': latest_deck_label, 'latest_deck_meta': latest_deck_meta, - 'latest_run_label': latest_run.get('report_label') or latest_run.get('report_type') or '尚無 DB run', + 'latest_run_label': latest_run.get('report_label') or latest_run.get('report_type') or '尚無產出紀錄', 'latest_run_meta': latest_run.get('started_at') or '等待下一次排程寫入', - 'blocker_text': ';'.join(blockers[:2]) if blockers else '', + 'blocker_text': ';'.join(_public_ppt_text_list(blockers[:2])) if blockers else '', 'signals': [ { 'label': '可預覽簡報', @@ -2907,7 +3038,7 @@ def _build_ppt_operator_summary(files, auto_generation, pipeline_view, vision_st { 'label': '視覺 QA', 'value': audit_total if audit_total else '待跑', - 'meta': '已就緒' if vision_status.get('ready') else 'runtime 待確認', + 'meta': '已就緒' if vision_status.get('ready') else '執行條件待確認', 'status': 'ready' if vision_status.get('ready') and not issue_count else 'partial', }, { @@ -2975,11 +3106,11 @@ def _enrich_ppt_coverage_items(auto_generation_items, files, generation_runs, au run_status = latest_run.get('status') or '' if latest_run and run_status == 'error': - db_status, db_label = 'error', 'DB 失敗' + db_status, db_label = 'error', '紀錄失敗' elif db_backed: - db_status, db_label = 'ready', 'DB 已寫入' + db_status, db_label = 'ready', '已記錄' else: - db_status, db_label = 'planned', '待 DB' + db_status, db_label = 'planned', '待紀錄' if valid_ppt and preview_cached: preview_status, preview_label = 'ready', 'PDF 快取' @@ -3033,7 +3164,10 @@ def _enrich_ppt_coverage_items(auto_generation_items, files, generation_runs, au 'qa_label': qa_label, 'delivery_status': delivery_status, 'delivery_label': delivery_label, - 'audit_summary': audit.get('issue_summary') or audit.get('error_msg') or '', + 'audit_summary': _public_ppt_text( + audit.get('issue_summary') or audit.get('error_msg'), + max_chars=160, + ), 'can_preview': valid_ppt and bool(file_name), 'can_prewarm': valid_ppt and bool(file_name) and not preview_cached, 'can_regenerate': bool(report_type), @@ -3244,8 +3378,11 @@ def ppt_audit_history(): files = list(files_by_name.values()) files.sort(key=lambda x: x['mtime_ts'], reverse=True) + for item in files: + item['source_label'] = _public_ppt_source_label(item.get('source')) + item['file_error'] = _public_ppt_text(item.get('file_error'), max_chars=120) except Exception as e: - error = f'{type(e).__name__}: {str(e)[:200]}' + error = _public_ppt_text(f'{type(e).__name__}: {str(e)[:200]}', empty='簡報清單讀取異常') audit_filter_sql = "" audit_params = {'month_start': month_start, 'month_end': month_end} @@ -3270,7 +3407,7 @@ def ppt_audit_history(): continue slide = item.get('slide') for issue in item.get('issues') or []: - text = str(issue).strip() + text = _public_ppt_text(issue, max_chars=120) if not text: continue prefix = f"S{slide}: " if slide else "" @@ -3291,6 +3428,8 @@ def ppt_audit_history(): def _classify_ppt_issue(issue_text: str): text = issue_text or '' + if _ppt_text_has_internal_detail(text): + return '審核執行', 'warn' if any(k in text for k in ['圖表', '切掉', '截斷', '超出', '溢出']): return '版面越界', 'error' if any(k in text for k in ['空白', '未填', '缺少', '無資料']): @@ -3308,10 +3447,10 @@ def ppt_audit_history(): slide = slide_item.get('slide') slide_label = f"S{slide}" if slide else 'S?' for raw_issue in slide_item.get('issues') or []: - issue_text = str(raw_issue).strip() + category, status = _classify_ppt_issue(str(raw_issue or '')) + issue_text = _public_ppt_text(raw_issue, max_chars=140) if not issue_text: continue - category, status = _classify_ppt_issue(issue_text) issue_items.append({ 'pptx_filename': pptx_filename, 'report_type': report_type_for_file, @@ -3354,7 +3493,7 @@ def ppt_audit_history(): 'issues_count': int(r[3] or 0), 'confidence': float(r[4] or 0), 'duration_ms': int(r[5] or 0), - 'error_msg': r[6], + 'error_msg': _public_ppt_text(r[6], max_chars=160), 'issue_summary': _summarize_ppt_issues(raw_issues), 'issue_items': _extract_ppt_issue_items( raw_issues, @@ -3407,6 +3546,8 @@ def ppt_audit_history(): 'message': '最近視覺 QA 狀態讀取失敗。', 'last_run': None, } + vision_status = _public_ppt_vision_status(vision_status) + vision_audit_status = _public_ppt_vision_audit_status(vision_audit_status) # Phase 47 K-6: 月報表統計 + top failure files audit_30d_stats = {} @@ -3489,12 +3630,12 @@ def ppt_audit_history(): rag_fixes.append({ 'pptx_filename': fr.get('pptx_filename'), 'audited_at': fr.get('audited_at'), - 'error_msg': (err_text or '')[:160], + 'error_msg': _public_ppt_text(err_text, max_chars=160), 'hits': [ { 'id': h.get('id'), 'insight_type': h.get('insight_type'), - 'content': (h.get('content') or '')[:200], + 'content': _public_ppt_text(h.get('content'), max_chars=180), 'similarity': round(float(h.get('similarity', 0)), 3), } for h in rag_result.hits[:2] @@ -3545,6 +3686,14 @@ def ppt_audit_history(): month_end=month_end, limit=24, ) + generation_runs = [ + { + **dict(item), + 'error_msg': _public_ppt_text(item.get('error_msg'), max_chars=160), + 'status_label': _public_ppt_text(item.get('status_label'), max_chars=60) or item.get('status_label'), + } + for item in generation_runs + ] except Exception: logger.debug("PPT auto-generation coverage unavailable", exc_info=True) diff --git a/routes/import_routes.py b/routes/import_routes.py index 34269d0..65c8975 100644 --- a/routes/import_routes.py +++ b/routes/import_routes.py @@ -238,7 +238,13 @@ def import_excel(): sys_log.warning(f"[Web] [Cache] 成長分析快取清除失敗: {cache_error}") sys_log.info(f"[Web] [Cache] 已清除業績分析快取: {table_name}") - return jsonify({'status': 'success', 'message': message, 'rows': rows_imported, 'table': table_name}) + return jsonify({ + 'status': 'success', + 'message': message, + 'rows': rows_imported, + 'dataset_label': '月度業績資料', + 'table': table_name, # 相容既有前端判斷;不可直接顯示給使用者。 + }) except Exception as de: sys_log.error(f"[Web] [Import] 業績報表匯入去重或寫入時發生錯誤: {de}") @@ -261,7 +267,8 @@ def import_excel(): 'status': 'success', 'message': f'匯入成功!已整理 {len(df)} 筆營運資料。', 'rows': len(df), - 'table': table_name + 'dataset_label': '營運資料', + 'table': table_name, # 相容既有前端判斷;不可直接顯示給使用者。 }) except Exception as e: diff --git a/routes/sales_routes.py b/routes/sales_routes.py index 2e55590..65f9393 100644 --- a/routes/sales_routes.py +++ b/routes/sales_routes.py @@ -1981,7 +1981,7 @@ def growth_analysis(): table_name = 'realtime_sales_monthly' inspector = inspect(db.engine) if not inspector.has_table(table_name): - return _render_growth_empty(f"尚未匯入業績資料 ({table_name})") + return _render_growth_empty("尚未匯入業績資料,請先完成數據匯入後再查看成長分析。") source_fingerprint = _get_growth_source_fingerprint(db.engine, table_name) @@ -2018,7 +2018,7 @@ def growth_analysis(): payload = _fetch_growth_payload_pandas(db.engine, table_name) if not payload: - return _render_growth_empty(f"資料表 {table_name} 為空") + return _render_growth_empty("業績資料目前沒有可分析的紀錄,請確認匯入檔案是否包含有效銷售資料。") chart_data, kpi = payload # 儲存快取 diff --git a/routes/system_public_routes.py b/routes/system_public_routes.py index 1ea6518..cbdc700 100644 --- a/routes/system_public_routes.py +++ b/routes/system_public_routes.py @@ -9,6 +9,7 @@ import os import hmac import mimetypes import posixpath +import re import zipfile from datetime import datetime, timezone, timedelta from urllib.parse import quote @@ -71,6 +72,57 @@ WEBCRUMBS_FALLBACK_LOADER = """ })(); """.strip() +_PUBLIC_LOG_REPLACEMENTS = ( + ('Ollama', 'AI 模型服務'), + ('ollama', 'AI 模型服務'), + ('fallback', '備援'), + ('Fallback', '備援'), + ('MCP', '工具服務'), + ('RAG', '知識檢索'), + ('autoheal_id_ed25519', '部署金鑰(已隱藏)'), + ('id_ed25519', '部署金鑰(已隱藏)'), + ('google_token.json', '雲端授權檔'), +) +_PUBLIC_LOG_RAW_ERROR_MARKERS = ( + 'httpconnectionpool', + 'all 3 hosts failed', + 'multimodal data provided', + 'traceback', + 'secret', + 'api_key', +) + + +def _sanitize_public_log_line(line): + text = str(line or '').rstrip('\n') + if not text: + return '' + prefix = '' + match = re.match(r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:,\d+)?)\s+(\w+)\s*(.*)$', text) + if match: + prefix = f"{match.group(1)} {match.group(2)} " + body = match.group(3) + else: + body = text + lowered = body.lower() + if any(marker in lowered for marker in _PUBLIC_LOG_RAW_ERROR_MARKERS): + body = '內部服務暫時異常,已保留完整診斷於伺服器端。' + for raw, label in _PUBLIC_LOG_REPLACEMENTS: + body = body.replace(raw, label) + body = re.sub(r'\b(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?\b', '內部主機', body) + body = re.sub( + r'(?i)(?:/[\w.\-~]+)+/(?:[^/\s]*(?:token|secret|key|credential|id_ed25519)[^/\s]*)', + '敏感設定路徑已隱藏', + body, + ) + if len(body) > 260: + body = body[:260].rstrip() + '…' + return prefix + body + + +def _sanitize_public_logs(lines): + return '\n'.join(_sanitize_public_log_line(line) for line in lines if str(line or '').strip()) + def _has_sensitive_webcrumbs_access(): if session.get('logged_in'): @@ -494,7 +546,7 @@ def get_logs_api(): if os.path.exists(LOG_FILE_PATH): try: with open(LOG_FILE_PATH, 'r', encoding='utf-8') as f: - return jsonify({"logs": "".join(f.readlines()[-60:])}) + return jsonify({"logs": _sanitize_public_logs(f.readlines()[-60:])}) except Exception as e: sys_log.error(f"[Web] [Logs] ❌ 日誌 API 讀取異常 | Error: {e}") return jsonify({"logs": "讀取日誌異常"}) diff --git a/scripts/check_observability_ui.py b/scripts/check_observability_ui.py index 006feb6..6648a32 100644 --- a/scripts/check_observability_ui.py +++ b/scripts/check_observability_ui.py @@ -95,7 +95,7 @@ REQUIRED_SHELL_SNIPPETS = [ "AI 觀測台", "戰情室", "系統與成本", - "RAG 與品質", + "知識與品質", "momo-nav-tree", "momo-nav-subtree", "momo-nav-subtitle", diff --git a/scripts/observability_contract.py b/scripts/observability_contract.py index 0abb514..13b1987 100644 --- a/scripts/observability_contract.py +++ b/scripts/observability_contract.py @@ -39,9 +39,9 @@ OBSERVABILITY_PAGES = ( "templates/admin/agent_orchestration.html", "obs_agent_orchestration", "/observability/agent_orchestration", - "Agent 編排矩陣", - "Agent", - ("Agent 編排矩陣", "LLM", "MCP", "RAG"), + "AI 分工矩陣", + "分工", + ("AI 分工矩陣", "本地模型", "知識"), ), ObservabilityPage( "templates/admin/business_intel.html", @@ -57,7 +57,7 @@ OBSERVABILITY_PAGES = ( "/observability/host_health", "主機健康", "主機", - ("主機健康", "Ollama", "AutoHeal"), + ("主機健康", "AI 模型", "自癒"), ), ObservabilityPage( "templates/admin/ai_calls_dashboard.html", @@ -65,7 +65,7 @@ OBSERVABILITY_PAGES = ( "/observability/ai_calls", "AI 呼叫", "AI 呼叫", - ("AI 呼叫", "供應商", "RAG"), + ("AI 呼叫", "供應商", "知識命中"), ), ObservabilityPage( "templates/admin/budget.html", @@ -79,17 +79,17 @@ OBSERVABILITY_PAGES = ( "templates/admin/promotion_review.html", "obs_promotion_review", "/observability/promotion_review", - "RAG 晉升審核", + "知識晉升審核", "晉升", - ("RAG 晉升審核", "晉升", "ai_insights"), + ("知識晉升審核", "晉升", "ai_insights"), ), ObservabilityPage( "templates/admin/rag_queries.html", "obs_rag_queries", "/observability/rag_queries", - "RAG 召回詳情", - "RAG", - ("RAG 召回詳情", "最近 50", "命中"), + "知識召回詳情", + "知識", + ("知識召回雷達", "最近 50", "命中"), ), ObservabilityPage( "templates/admin/quality_trend.html", @@ -105,7 +105,7 @@ OBSERVABILITY_PAGES = ( "/observability/ppt_audit_history", "PPT 視覺審核", "PPT", - ("PPT 視覺審核", "AiderHeal", "審核"), + ("PPT 視覺審核", "修復流程", "審核"), ), ) diff --git a/templates/admin/_observability_labels.html b/templates/admin/_observability_labels.html index aea0a18..059f952 100644 --- a/templates/admin/_observability_labels.html +++ b/templates/admin/_observability_labels.html @@ -103,7 +103,7 @@ 'competitor_price': '競品價格', 'sales_anomaly': '業績異常', 'budget_strategy': '預算策略', - 'rag_feedback': 'RAG 反饋', + 'rag_feedback': '知識反饋', 'ppt_audit': 'PPT 審核', 'quality_issue': '品質問題', 'promotion': '活動促銷', @@ -116,9 +116,9 @@ {% macro provider(value, fallback='未分類供應商') -%} {%- set labels = { - 'gcp_ollama': '主力 Ollama', - 'ollama_secondary': '備援 Ollama', - 'ollama_111': '111 Ollama', + 'gcp_ollama': '主力 AI 模型', + 'ollama_secondary': '備援 AI 模型', + 'ollama_111': '第三 AI 模型', 'nim_via_elephant': 'NIM Elephant', 'gemini': 'Gemini', 'claude': 'Claude', diff --git a/templates/admin/agent_orchestration.html b/templates/admin/agent_orchestration.html index 7c6a559..a13bf6c 100644 --- a/templates/admin/agent_orchestration.html +++ b/templates/admin/agent_orchestration.html @@ -9,20 +9,20 @@
-
AI 分工指揮台 · {{ hours }} 小時視窗

AI 分工指揮台

確認 AI 分工、Ollama 占比、知識命中與 MCP 編排是否支撐業績決策。

{% if overall %}
呼叫總量
{{ "{:,}".format(overall.total_calls) }}{{ "{:,}".format(overall.total_tokens) }} 用量
Ollama 占比
{{ "%.0f"|format(overall.local_pct) }}%{{ "{:,}".format(overall.local_calls) }} 次本地呼叫
付費成本
${{ "%.2f"|format(overall.total_cost) }}{{ "{:,}".format(overall.paid_calls) }} 次付費呼叫
知識命中率
{{ "%.0f"|format(overall.rag_rate) }}%{{ "{:,}".format(overall.rag_hits) }} 次命中
{% endif %}
+
AI 分工指揮台 · {{ hours }} 小時視窗

AI 分工指揮台

確認 AI 分工、本地模型、知識命中與工具編排是否支撐業績決策。

{% if overall %}
呼叫總量
{{ "{:,}".format(overall.total_calls) }}{{ "{:,}".format(overall.total_tokens) }} 用量
本地模型占比
{{ "%.0f"|format(overall.local_pct) }}%{{ "{:,}".format(overall.local_calls) }} 次本地呼叫
付費成本
${{ "%.2f"|format(overall.total_cost) }}{{ "{:,}".format(overall.paid_calls) }} 次付費呼叫
知識命中率
{{ "%.0f"|format(overall.rag_rate) }}%{{ "{:,}".format(overall.rag_hits) }} 次命中
{% endif %}
{% if error %}
{{ error }}
{% endif %}
-
AI 分工矩陣

模型、工具與知識命中矩陣

{% for ag in agent_matrix %}{% endfor %}
分工呼叫成本Ollama付費MCPRAG錯誤耗時
{{ ag.label }}{{ ag.desc }}{% if ag.calls > 0 %}{{ "{:,}".format(ag.calls) }}{{ "{:,}".format(ag.tokens) }} 用量{% else %}{% endif %}{% if ag.calls > 0 %}${{ "%.2f"|format(ag.cost) }}{% else %}{% endif %}{% if ag.calls > 0 %}{{ "%.0f"|format(ag.ollama_pct) }}%A {{ ag.ollama_gcp_a }} · B {{ ag.ollama_gcp_b }} · 111 {{ ag.ollama_111 }}{% else %}{% endif %}{% if ag.calls > 0 %}{{ "%.0f"|format(ag.paid_pct) }}%Gemini {{ ag.gemini }}{% if ag.other_paid %} · 其他 {{ ag.other_paid }}{% endif %}{% else %}{% endif %}{% if ag.calls > 0 %}{{ "%.1f"|format(ag.mcp_rate) }}%{{ ag.mcp_calls }}{% else %}{% endif %}{% if ag.calls > 0 %}{{ "%.1f"|format(ag.rag_rate) }}%{{ ag.rag_hits }}{% else %}{% endif %}{% if ag.calls > 0 %}{{ "%.1f"|format(ag.error_rate) }}%{{ ag.errors }}{% else %}{% endif %}{% if ag.calls > 0 %}{{ ag.avg_ms }} ms{% else %}{% endif %}
+
AI 分工矩陣

模型、工具與知識命中矩陣

{% for ag in agent_matrix %}{% endfor %}
分工呼叫成本本地模型付費工具知識錯誤耗時
{{ ag.label }}{{ ag.desc }}{% if ag.calls > 0 %}{{ "{:,}".format(ag.calls) }}{{ "{:,}".format(ag.tokens) }} 用量{% else %}{% endif %}{% if ag.calls > 0 %}${{ "%.2f"|format(ag.cost) }}{% else %}{% endif %}{% if ag.calls > 0 %}{{ "%.0f"|format(ag.ollama_pct) }}%主力 {{ ag.ollama_gcp_a }} · 備援 {{ ag.ollama_gcp_b }} · 第三 {{ ag.ollama_111 }}{% else %}{% endif %}{% if ag.calls > 0 %}{{ "%.0f"|format(ag.paid_pct) }}%Gemini {{ ag.gemini }}{% if ag.other_paid %} · 其他 {{ ag.other_paid }}{% endif %}{% else %}{% endif %}{% if ag.calls > 0 %}{{ "%.1f"|format(ag.mcp_rate) }}%{{ ag.mcp_calls }}{% else %}{% endif %}{% if ag.calls > 0 %}{{ "%.1f"|format(ag.rag_rate) }}%{{ ag.rag_hits }}{% else %}{% endif %}{% if ag.calls > 0 %}{{ "%.1f"|format(ag.error_rate) }}%{{ ag.errors }}{% else %}{% endif %}{% if ag.calls > 0 %}{{ ag.avg_ms }} ms{% else %}{% endif %}
{% if recommendations %}
策略規則

編排策略自動建議

{% for r in recommendations %}
{{ r.severity|upper }}{{ r.agent }}
發現:{{ r.finding }}
建議:{{ r.suggestion }}
{% endfor %}
{% endif %} {% if mcp_matrix %}
工具服務明細

工具服務 × 呼叫端工作量

{% for m in mcp_matrix %}{% endfor %}
工具服務呼叫端工具呼叫快取快取率成本
{{ m.server }}{{ m.caller }}{{ "{:,}".format(m.calls) }}{{ m.cache_hits }}{{ "%.0f"|format(m.cache_rate) }}%${{ "%.4f"|format(m.cost) }}
{% endif %} -

Ollama 優先策略 — AI 分工指揮台

+

AI 分工指揮台

{% endblock %} diff --git a/templates/admin/ai_calls_dashboard.html b/templates/admin/ai_calls_dashboard.html index ce5f2b4..2f78338 100644 --- a/templates/admin/ai_calls_dashboard.html +++ b/templates/admin/ai_calls_dashboard.html @@ -53,7 +53,7 @@
AI 流量管制 · {{ hours }} 小時視窗

AI 流量控制塔

-

看成本、錯誤率與 RAG 命中,確保 AI 建議穩定支援業績判斷。

+

看成本、錯誤率與知識命中,確保 AI 建議穩定支援業績判斷。

@@ -72,7 +72,7 @@
用量
{{ "{:,}".format(summary.total_tokens or 0) }}
{{ avg_tokens }} 單位/次
成本
${{ "%.2f"|format(summary.total_cost or 0) }}{% if hourly_trend %}{% endif %}
延遲
{{ summary.avg_duration or 0 }}ms
{{ summary.cache_hits or 0 }} 次快取命中
-
RAG 命中
{{ "%.1f"|format(rag_rate) }}%
{{ summary.rag_hits or 0 }} 次命中
+
知識命中
{{ "%.1f"|format(rag_rate) }}%
{{ summary.rag_hits or 0 }} 次命中
錯誤
{{ errors }}{% if hourly_trend %}{% endif %}
@@ -88,7 +88,7 @@ {% if caller_richness %}
呼叫端編排

呼叫端 × 知識與工具編排矩陣

-
{% for c in caller_richness %}{% endfor %}
呼叫端總呼叫RAG 命中MCP 編排RAG 反饋筆數
{{ c.caller }}{{ "{:,}".format(c.total_calls) }}{{ "%.1f"|format(c.rag_hit_rate) }}% ({{ c.rag_hits }}){{ "%.1f"|format(c.mcp_rate) }}%{% if c.feedback_count > 0 %}{{ "%.2f"|format(c.avg_rag_feedback) }}/5{% else %}{% endif %}{{ c.feedback_count }}
+
{% for c in caller_richness %}{% endfor %}
呼叫端總呼叫知識命中工具編排知識反饋筆數
{{ c.caller }}{{ "{:,}".format(c.total_calls) }}{{ "%.1f"|format(c.rag_hit_rate) }}% ({{ c.rag_hits }}){{ "%.1f"|format(c.mcp_rate) }}%{% if c.feedback_count > 0 %}{{ "%.2f"|format(c.avg_rag_feedback) }}/5{% else %}{% endif %}{{ c.feedback_count }}
{% endif %} @@ -125,10 +125,10 @@
最近呼叫

最近呼叫 100 筆

-
{% for r in recent %}{% endfor %}
編號時間呼叫端供應商模型輸入輸出耗時狀態成本標記
{{ r.id }}{{ r.called_at }}{{ r.caller_display or r.caller }}{% if r.caller_display and r.caller_display != r.caller %}
原始:{{ r.caller }}{% endif %}
{{ obs_label.provider(r.provider) }}{{ r.model[:25] }}{{ r.in_tokens }}{{ r.out_tokens }}{{ r.duration_ms }}{{ obs_label.status(r.status, '-') }}${{ "%.4f"|format(r.cost) }}{% for badge in r.route_badges %}{{ badge }}{% endfor %}{% if r.cache_hit %}快取{% endif %}{% if r.rag_hit %}RAG{% endif %}
+
{% for r in recent %}{% endfor %}
編號時間呼叫端供應商模型輸入輸出耗時狀態成本標記
{{ r.id }}{{ r.called_at }}{{ r.caller_display or r.caller }}{% if r.caller_display and r.caller_display != r.caller %}
原始:{{ r.caller }}{% endif %}
{{ obs_label.provider(r.provider) }}{{ r.model[:25] }}{{ r.in_tokens }}{{ r.out_tokens }}{{ r.duration_ms }}{{ obs_label.status(r.status, '-') }}${{ "%.4f"|format(r.cost) }}{% for badge in r.route_badges %}{{ badge }}{% endfor %}{% if r.cache_hit %}快取{% endif %}{% if r.rag_hit %}知識命中{% endif %}
-

Ollama 優先策略 v5.0 — AI 流量控制塔

+

AI 流量控制塔

{% set ai_calls_payload = { diff --git a/templates/admin/budget.html b/templates/admin/budget.html index 1dc7b93..7ef70ae 100644 --- a/templates/admin/budget.html +++ b/templates/admin/budget.html @@ -38,7 +38,7 @@
-
AI 成本治理 · 預算 / 節流 / RAG 策略
+
AI 成本治理 · 預算 / 節流 / 知識策略

AI 成本治理艙

控制 AI 花費與節流狀態,把預算留給能推動業績的任務。

預算超線時立即重算節流。
@@ -76,14 +76,14 @@
{% if budget_strategies %} -
RAG 策略

RAG 自動策略建議

{% for s in budget_strategies %}
{{ obs_label.insight(s.insight_type) }}相似度 {{ "%.2f"|format(s.similarity) }}{{ s.content }}{% if s.content|length >= 240 %}…{% endif %}
{% endfor %}
+
知識策略

知識策略建議

{% for s in budget_strategies %}
{{ obs_label.insight(s.insight_type) }}相似度 {{ "%.2f"|format(s.similarity) }}{{ s.content }}{% if s.content|length >= 240 %}…{% endif %}
{% endfor %}
{% endif %} {% if price_rec_7d %}
商業產出

AI 價格決策 7 日

{% for p in price_rec_7d %}
{{ obs_label.strategy(p.strategy) }}{{ p.count }}信心 {{ "%.2f"|format(p.avg_confidence) }}
{% endfor %}
{% endif %} -

Ollama 優先策略 v5.0 — AI 成本治理艙

+

AI 成本治理艙

{% set budget_payload = { diff --git a/templates/admin/host_health.html b/templates/admin/host_health.html index 5fefeb3..4a489b1 100644 --- a/templates/admin/host_health.html +++ b/templates/admin/host_health.html @@ -59,13 +59,13 @@
-
基礎設施生命線 · Ollama / MCP / AIOps
+
基礎設施生命線 · AI 模型 / 工具服務 / 自癒

基礎設施生命線

-

先看 Ollama、MCP 與自癒是否正常,避免 AI 建議與比價流程中斷。

+

先看 AI 模型、工具服務與自癒是否正常,避免 AI 建議與比價流程中斷。

-
Ollama 離線
{{ down.count }}{{ ollama_hosts|length }} 台即時探測
+
AI 模型離線
{{ down.count }}{{ ollama_hosts|length }} 台即時探測
AIOps 未解
{{ aiops_summary.incidents_open if aiops_summary else '—' }}7 日事件未解決
-
自癒成功率
{{ "%.0f"|format(aiops_summary.heal_success_rate) if aiops_summary else '—' }}{% if aiops_summary %}%{% endif %}ADR-013 自癒成功率
+
自癒成功率
{{ "%.0f"|format(aiops_summary.heal_success_rate) if aiops_summary else '—' }}{% if aiops_summary %}%{% endif %}近 7 日自癒成功率
節流供應商
{{ throttled.count }}成本節流供應商
@@ -74,7 +74,7 @@
-
主機級聯

Ollama 三主機

+
模型服務

AI 模型主機

{{ '需要處理' if down.count > 0 else '全部在線' }}
@@ -128,11 +128,11 @@ {% endif %}
-
MCP / Budget

工具層與節流

+
工具服務 / 預算

工具層與節流

-
MCP 服務{{ mcp_status|length }}
-
MCP 24 小時呼叫{{ "{:,}".format(mcp_24h|sum(attribute='total_calls')) if mcp_24h else 0 }}
+
工具服務{{ mcp_status|length }}
+
工具 24 小時呼叫{{ "{:,}".format(mcp_24h|sum(attribute='total_calls')) if mcp_24h else 0 }}
自癒劇本{{ active_playbooks.count }}/{{ playbook_ranking|length }}
嵌入佇列{{ embed_queue_pending }}/{{ embed_queue_failed }}
@@ -150,25 +150,25 @@ {% if mcp_24h %}
-
MCP 工作量

MCP 服務 24h 工作量

+
工具工作量

工具服務 24h 工作量

{% for s in mcp_24h %}{% endfor %}
服務呼叫成功率快取工具平均成本
{{ s.server }}{{ "{:,}".format(s.total_calls) }}{{ "%.1f"|format(s.success_rate) }}%{{ "%.1f"|format(s.cache_rate) }}%{{ s.tools_used }}{{ s.avg_ms }} ms${{ "%.4f"|format(s.total_cost) }}
{% endif %}
-
事件紀錄

最近 10 筆事件

{% if recent_incidents %}{% set task_labels = {'unknown_task': '待確認任務', 'ElephantAlphaAutonomousEngine': 'AI 自癒監控', 'run_icaim_analysis_task': '市場分析任務'} %}{% set error_labels = {'ollama_unhealthy': 'AI 模型主機不穩', 'scheduler_task_failure': '排程任務異常', 'crawler_timeout': '資料擷取逾時', 'python_exception': '程式例外'} %}{% for i in recent_incidents %}{% set incident_msg = i.error_message or '' %}{% endfor %}
時間任務問題等級狀態處置提醒
{{ i.created_at }}{{ task_labels.get(i.task_name, '系統任務') }}{{ error_labels.get(i.error_type, '系統異常') }}{{ i.severity }}{{ i.status }}{% if 'Ollama' in incident_msg %}AI 模型主機暫時不穩,已進入自癒監控。{% elif 'not defined' in incident_msg or 'Traceback' in incident_msg %}資料流程發生程式例外,需由修復流程處理。{% elif 'scheduler_task_failure' in incident_msg %}排程任務異常,需確認下次執行是否恢復。{% else %}{{ incident_msg|replace('_', ' ')|truncate(90) }}{% endif %}
{% else %}
尚無事件紀錄
{% endif %}
+
事件紀錄

最近 10 筆事件

{% if recent_incidents %}{% set task_labels = {'ElephantAlphaAutonomousEngine': 'AI 自癒監控', 'run_icaim_analysis_task': '市場分析任務'} %}{% set error_labels = {'ollama_unhealthy': 'AI 模型主機不穩', 'scheduler_task_failure': '排程任務異常', 'crawler_timeout': '資料擷取逾時', 'python_exception': '程式例外'} %}{% for i in recent_incidents %}{% endfor %}
時間任務問題等級狀態處置提醒
{{ i.created_at }}{{ task_labels.get(i.task_name, '系統任務') }}{{ error_labels.get(i.error_type, '系統異常') }}{{ i.severity }}{{ i.status }}{% if i.error_type == 'ollama_unhealthy' %}AI 模型主機暫時不穩,已進入自癒監控。{% elif i.error_type == 'python_exception' %}資料流程發生程式例外,需由修復流程處理。{% elif i.error_type == 'scheduler_task_failure' %}排程任務異常,需確認下次執行是否恢復。{% else %}系統事件已記錄,請依狀態追蹤後續是否恢復。{% endif %}
{% else %}
尚無事件紀錄
{% endif %}
自癒紀錄

最近 10 筆自癒

{% if recent_heals %}{% for h in recent_heals %}{% endfor %}
時間動作結果耗時細節
{{ h.created_at }}{{ h.action_type or '—' }}{% if h.result == 'success' %}成功{% elif h.result == 'failed' %}失敗{% else %}{{ h.result }}{% endif %}{{ h.duration_ms }} ms{{ h.action_detail }}
{% else %}
尚無自癒紀錄
{% endif %}
{% if embed_queue_pending > 0 or embed_queue_failed > 0 %}
Embedding 重試佇列:待處理 {{ embed_queue_pending }} 筆 · 失敗 {{ embed_queue_failed }} 筆
{% endif %} -

Ollama 優先策略 v5.0 — 基礎設施生命線

+

AI 基礎設施生命線

{% set host_health_payload = { diff --git a/templates/admin/observability_overview.html b/templates/admin/observability_overview.html index 738a510..22108d9 100644 --- a/templates/admin/observability_overview.html +++ b/templates/admin/observability_overview.html @@ -422,7 +422,7 @@ 01 指揮總覽 · {{ today }}

AI 觀測戰情室

- 私有 AI 中樞的第一入口:三主機、AI 呼叫、RAG 學習、MCP、AIOps、預算與 PPT 視覺審核收斂到同一張工作台。所有數字只讀正式資料來源;缺資料時呈現可診斷空狀態。 + 私有 AI 中樞的第一入口:模型主機、AI 呼叫、知識學習、工具編排、自癒、預算與 PPT 視覺審核收斂到同一張工作台。所有數字只讀正式資料來源;缺資料時呈現可診斷空狀態。

@@ -442,7 +442,7 @@
當月累計 ${{ "%.2f"|format(summary.month_cost|default(0)) }}
-
RAG 命中率
+
知識命中率
{{ "%.1f"|format(ai.rag_rate) if ai else '—' }}{% if ai %}%{% endif %}
快取命中 {{ "%.0f"|format(ai.cache_rate) if ai else '—' }}{% if ai %}%{% endif %}
@@ -535,7 +535,7 @@
錯誤率{{ "%.1f"|format(ai.error_rate) }}%
失敗{{ ai.errors }}
-
RAG 命中{{ ai.rag_hits }}
+
知識命中{{ ai.rag_hits }}
快取命中{{ ai.cache_hits }}
{% else %} @@ -548,7 +548,7 @@
學習閉環
-

RAG 學習閘

+

知識學習閘

審核
@@ -577,7 +577,7 @@
AIOps 未解{{ summary.aiops.incidents_open if summary.aiops else '—' }}
自癒成功率{{ "%.0f"|format(summary.aiops.heal_rate) if summary.aiops else '—' }}{% if summary.aiops %}%{% endif %}
-
MCP 呼叫{{ "{:,}".format(summary.mcp.total) if summary.mcp else '—' }}
+
工具呼叫{{ "{:,}".format(summary.mcp.total) if summary.mcp else '—' }}
PPT 通過率{{ "%.0f"|format(summary.ppt.pass_rate) if summary.ppt and summary.ppt.total > 0 else '—' }}{% if summary.ppt and summary.ppt.total > 0 %}%{% endif %}
@@ -599,27 +599,27 @@
系統成本

系統與成本

- 資料來源:主機探測、AI 呼叫、預算、學習事件、RAG 查詢、MCP 呼叫、事件與自癒、PPT 審核。 + 資料來源:主機探測、AI 呼叫、預算、學習事件、知識查詢、工具呼叫、事件與自癒、PPT 審核。

diff --git a/templates/admin/ppt_audit_history.html b/templates/admin/ppt_audit_history.html index 85eb6e6..6231504 100644 --- a/templates/admin/ppt_audit_history.html +++ b/templates/admin/ppt_audit_history.html @@ -62,10 +62,10 @@
視覺 QA 尚未就緒
- 先補齊視覺 QA runtime,再判斷簡報品質。 + 先補齊視覺 QA 執行條件,再判斷簡報品質。 {{ vision_status.summary }}
-
+
{% for check in vision_status.readiness_checks %}
{{ check.label }} @@ -89,9 +89,9 @@
- AiderHeal + 修復流程
- {% if aider_heal_active_count %}AiderHeal 執行中 · {{ aider_heal_active_count }}{% else %}AiderHeal 待命{% endif %} + {% if aider_heal_active_count %}修復流程執行中 · {{ aider_heal_active_count }}{% else %}修復流程待命{% endif %} {% if aider_heal_active_count %}修復完成後回報處理結果。{% else %}有問題的審核紀錄可直接派工。{% endif %}
@@ -120,7 +120,7 @@
執行環境 {{ vision_status.status_label }} - {{ vision_status.model }} · {{ vision_status.converter or '無轉檔器' }} + {{ vision_status.model_label }} · {{ vision_status.converter_label }}
{% if vision_audit_status.last_run %}
@@ -282,7 +282,7 @@