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 %}
-
分工 呼叫 成本 Ollama 付費 MCP RAG 錯誤 耗時 {% for ag in agent_matrix %}{{ 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 %} {% endfor %}
+
分工 呼叫 成本 本地模型 付費 工具 知識 錯誤 耗時 {% for ag in agent_matrix %}{{ 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 %} {% endfor %}
- {% for ag in agent_matrix %}
{{ ag.label }} {{ ag.desc }}
{{ ag.calls }} 次呼叫
Ollama {{ "%.0f"|format(ag.ollama_pct) if ag.calls > 0 else 0 }}% · RAG {{ "%.0f"|format(ag.rag_rate) if ag.calls > 0 else 0 }}% · MCP {{ "%.0f"|format(ag.mcp_rate) if ag.calls > 0 else 0 }}% {% endfor %}
+ {% for ag in agent_matrix %}
{{ ag.label }} {{ ag.desc }}
{{ ag.calls }} 次呼叫
本地模型 {{ "%.0f"|format(ag.ollama_pct) if ag.calls > 0 else 0 }}% · 知識 {{ "%.0f"|format(ag.rag_rate) if ag.calls > 0 else 0 }}% · 工具 {{ "%.0f"|format(ag.mcp_rate) if ag.calls > 0 else 0 }}% {% endfor %}
{% 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 %}{{ m.server }}{{ m.caller }}{{ "{:,}".format(m.calls) }} {{ m.cache_hits }} {{ "%.0f"|format(m.cache_rate) }}% ${{ "%.4f"|format(m.cost) }} {% endfor %}
{% 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 建議穩定支援業績判斷。
@@ -125,10 +125,10 @@
- 編號 時間 呼叫端 供應商 模型 輸入 輸出 耗時 狀態 成本 標記 {% for r in recent %}{{ 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 %} {% endfor %}
+ 編號 時間 呼叫端 供應商 模型 輸入 輸出 耗時 狀態 成本 標記 {% for r in recent %}{{ 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 %} {% endfor %}
- 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 %}
-
{% 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 %}
{% 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 @@
-
+
{{ '需要處理' if down.count > 0 else '全部在線' }}
@@ -128,11 +128,11 @@
{% endif %}
-
+
-
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 %}
-
+
服務 呼叫 成功率 快取 工具 平均 成本 {% for s in mcp_24h %}{{ 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) }} {% endfor %}
{% endif %}
-
{% 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 '' %}{{ 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 %} {% endfor %}
{% else %}
尚無事件紀錄
{% endif %}
+
{% 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 %}{{ 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 %} {% endfor %}
{% else %}
尚無事件紀錄
{% endif %}
{% if recent_heals %}
時間 動作 結果 耗時 細節 {% for h in recent_heals %}{{ 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 }} {% endfor %}
{% else %}
尚無自癒紀錄
{% endif %}
- {% if playbook_ranking %}{% set playbook_error_labels = {'python_exception': '程式例外', 'scheduler_task_failure': '排程異常', 'ollama_unhealthy': 'AI 模型主機不穩'} %}{% set playbook_action_labels = {'CODE_FIX': '程式修復', 'SERVICE_RESTART': '服務重啟', 'ALERT_ONLY': '告警追蹤'} %}
名稱 成功率 狀態 切換 {% for p in playbook_ranking %}{{ p.name }} {{ playbook_error_labels.get(p.error_type, '系統異常') }} · {{ playbook_action_labels.get(p.action_type, '自癒處理') }} {% if (p.success + p.fail) > 0 %}{{ "%.0f"|format(p.success_rate) }}% {% else %}— {% endif %} {% if p.is_active %}啟用 {% else %}停用 {% endif %} 切換 {% endfor %}
{% else %}
尚無劇本資料
{% endif %}
+ {% if playbook_ranking %}{% set playbook_error_labels = {'python_exception': '程式例外', 'scheduler_task_failure': '排程異常', 'ollama_unhealthy': 'AI 模型主機不穩'} %}{% set playbook_action_labels = {'CODE_FIX': '程式修復', 'SERVICE_RESTART': '服務重啟', 'ALERT_ONLY': '告警追蹤'} %}
名稱 成功率 狀態 切換 {% for p in playbook_ranking %}{{ p.name }} {{ playbook_error_labels.get(p.error_type, '系統異常') }} · {{ playbook_action_labels.get(p.action_type, '自癒處理') }} {% if (p.success + p.fail) > 0 %}{{ "%.0f"|format(p.success_rate) }}% {% else %}— {% endif %} {% if p.is_active %}啟用 {% else %}停用 {% endif %} 切換 {% endfor %}
{% else %}
尚無劇本資料
{% endif %}
{% if backup_history %}
時間 狀態 MB {% for b in backup_history %}{{ b.created_at }} {% if b.status == 'success' %}成功 {% else %}{{ b.status }} {% endif %} {{ b.size_mb }} {% endfor %}
{% else %}
過去 7 日無備份紀錄
{% 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 @@
@@ -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 @@
系統成本
系統與成本
-
RAG 品質
-
RAG 與品質
+
知識品質
+
知識與品質
- 資料來源:主機探測、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 @@
diff --git a/templates/admin/promotion_review.html b/templates/admin/promotion_review.html
index 67f4bd8..7403a0e 100644
--- a/templates/admin/promotion_review.html
+++ b/templates/admin/promotion_review.html
@@ -1,6 +1,6 @@
{% extends "ewoooc_base.html" %}
-{% block title %}RAG 知識晉升閘{% endblock %}
+{% block title %}知識晉升審核{% endblock %}
{% block ewooo_content %}