From f2fbe5f929bc81e8acadf93241221c5847a9924c Mon Sep 17 00:00:00 2001 From: OoO Date: Mon, 4 May 2026 13:48:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(p30):=20admin=20nav=20=E4=BA=92=E8=81=AF?= =?UTF-8?q?=20+=20deploy=20doctor=20v5.0=20=E8=85=B3=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (1) 6 個 admin 頁底部導覽全互聯(之前缺 Phase 29 兩頁的反向連結) - ai_calls / promotion_review / quality_trend / host_health 全部加 |Budget|PPT Audit| 連結 - 統帥從任一頁都可一鍵跳到其他 5 頁 (2) scripts/deploy_doctor_v5.py — 統帥手動待辦自助檢查 5 階段檢查:env vars / DB migrations / Ollama 三主機 / LibreOffice / MCP servers - 14 個 v5.0 env vars(含 criticality 分級 FAIL/WARN/INFO) - 5 張 v5.0 必備 table(ai_calls/mcp_calls/ai_call_budgets/ rag_query_log/learning_episodes) - ai_call_budgets seed ≥8 筆檢查 - 三主機 /api/tags HTTP probe + healthy 數判定 - 退出碼:0=全綠 1=WARN 2=FAIL(可進 CI) - SSH 188 / 本機都能跑:python3 scripts/deploy_doctor_v5.py 統帥之後想知道「v5.0 還有啥沒部署」直接跑 doctor 看清單, 不用再口頭追問哪些 env vars / 哪幾張 migration。 --- scripts/deploy_doctor_v5.py | 244 ++++++++++++++++++++++++ templates/admin/ai_calls_dashboard.html | 4 +- templates/admin/host_health.html | 4 +- templates/admin/promotion_review.html | 5 +- templates/admin/quality_trend.html | 4 +- 5 files changed, 257 insertions(+), 4 deletions(-) create mode 100755 scripts/deploy_doctor_v5.py diff --git a/scripts/deploy_doctor_v5.py b/scripts/deploy_doctor_v5.py new file mode 100755 index 0000000..7bd66e1 --- /dev/null +++ b/scripts/deploy_doctor_v5.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +"""Operation Ollama-First v5.0 deploy doctor. + +在 188 / 本機跑:python3 scripts/deploy_doctor_v5.py +檢查 v5.0 部署狀態,列出統帥手動還沒做的事。 +退出碼:0=全綠,1=有 WARN,2=有 FAIL。 +""" +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from typing import Tuple + +# ───────────────────────────────────────────────────────────────────────── +# v5.0 env vars: name → (criticality, expected_value_or_None, hint) +# ───────────────────────────────────────────────────────────────────────── +ENV_SPEC = { + # API keys (FAIL if missing — 沒這些 v5.0 直接斷) + 'ANTHROPIC_API_KEY': ('FAIL', None, 'Claude SDK 用;需 https://console.anthropic.com 申請'), + 'GOOGLE_API_KEY': ('FAIL', None, 'Gemini fallback 用;舊有'), + 'DEEPSEEK_API_KEY': ('WARN', None, '若 DEEPSEEK_DIRECT_ENABLED=true 需要'), + 'TAVILY_API_KEY': ('WARN', None, 'MCP omnisearch 用;可選'), + 'EXA_API_KEY': ('WARN', None, 'MCP omnisearch 用;可選'), + 'TELEGRAM_ADMIN_CHAT_ID': ('WARN', None, 'Phase 28 PromotionGate Telegram 推播 audience'), + 'TELEGRAM_BOT_TOKEN': ('FAIL', None, 'Telegram 推播必備'), + + # Feature flags (應 ON 才生效) + 'MODEL_ROUTER_ENABLED': ('WARN', 'true', 'caller × context 動態路由(預設 true)'), + 'COST_THROTTLE_ENABLED': ('WARN', 'true', '成本超 110% 自動 throttle(預設 true)'), + 'MCP_ROUTER_ENABLED': ('WARN', 'true', 'MCP 4-server 統一路由(預設 false)'), + 'PPT_VISION_ENABLED': ('WARN', 'true', 'PPT minicpm-v 視覺審核(預設 false)'), + 'DEEPSEEK_DIRECT_ENABLED': ('INFO', None, '直連 DeepSeek API;省 NIM 中間層'), + + # RAG / embedding + 'RAG_EMBED_MODEL': ('INFO', 'bge-m3', 'embedding 模型;ADR-026 鎖定 bge-m3'), + 'RAG_EMBED_DIM': ('INFO', '1024', 'bge-m3 維度;migration 027 已寫死 1024'), +} + +# 必跑 migrations +REQUIRED_TABLES = { + 'ai_calls': '024', + 'mcp_calls': '025', + 'ai_call_budgets': '025', + 'rag_query_log': '027', + 'learning_episodes': '028', +} + +# 三主機 +OLLAMA_HOSTS = [ + ('Primary GCP', '34.143.170.20:11434'), + ('Secondary GCP', '34.21.145.224:11434'), + ('111 Mac', '192.168.0.111:11434'), +] + + +def color(s: str, c: str) -> str: + if not sys.stdout.isatty(): + return s + codes = {'red': 31, 'green': 32, 'yellow': 33, 'cyan': 36, 'bold': 1} + return f'\x1b[{codes.get(c, 0)}m{s}\x1b[0m' + + +def status(level: str) -> str: + return { + 'OK': color(' OK ', 'green'), + 'WARN': color(' WARN ', 'yellow'), + 'FAIL': color(' FAIL ', 'red'), + 'INFO': color(' INFO ', 'cyan'), + }.get(level, level) + + +# ───────────────────────────────────────────────────────────────────────── +# Checks +# ───────────────────────────────────────────────────────────────────────── + +def check_env() -> Tuple[int, int]: + """Return (warn_count, fail_count).""" + print(color('\n[1/5] 環境變數', 'bold')) + warn = fail = 0 + for name, (crit, expected, hint) in ENV_SPEC.items(): + val = os.getenv(name, '').strip() + if not val: + print(f' {status(crit)} {name:30} <未設> — {hint}') + if crit == 'FAIL': + fail += 1 + elif crit == 'WARN': + warn += 1 + continue + if expected and val.lower() != expected.lower(): + print(f' {status("WARN")} {name:30} ={val!r} 期望={expected!r} — {hint}') + warn += 1 + else: + shown = val[:8] + '...' if 'KEY' in name or 'TOKEN' in name else val + print(f' {status("OK")} {name:30} ={shown}') + return warn, fail + + +def check_db() -> Tuple[int, int]: + print(color('\n[2/5] 資料庫 migrations', 'bold')) + warn = fail = 0 + db_url = os.getenv('DATABASE_URL', '').strip() + if not db_url: + print(f' {status("WARN")} DATABASE_URL 未設 — 跳過 DB 檢查') + return 1, 0 + try: + import psycopg2 # type: ignore + except ImportError: + print(f' {status("WARN")} psycopg2 not installed — 跳過 DB 檢查') + return 1, 0 + try: + conn = psycopg2.connect(db_url, connect_timeout=5) + except Exception as e: + print(f' {status("FAIL")} DB 無法連線: {type(e).__name__}: {str(e)[:100]}') + return 0, 1 + try: + cur = conn.cursor() + for table, mig in REQUIRED_TABLES.items(): + cur.execute( + 'SELECT 1 FROM information_schema.tables WHERE table_name = %s', + (table,), + ) + if cur.fetchone(): + print(f' {status("OK")} {table:25} (migration {mig})') + else: + print(f' {status("FAIL")} {table:25} 不存在 — 跑 migrations/{mig}_*.sql') + fail += 1 + # ai_call_budgets seed 檢查 + cur.execute('SELECT COUNT(*) FROM ai_call_budgets') + n = cur.fetchone()[0] + if n < 8: + print(f' {status("WARN")} ai_call_budgets 只有 {n} 筆 (期望 ≥8) — migration 025 seed 可能漏掉') + warn += 1 + else: + print(f' {status("OK")} ai_call_budgets {n} 筆 seed') + except Exception as e: + print(f' {status("FAIL")} 查詢失敗: {e}') + fail += 1 + finally: + conn.close() + return warn, fail + + +def check_ollama() -> Tuple[int, int]: + print(color('\n[3/5] Ollama 三主機', 'bold')) + warn = fail = 0 + try: + import urllib.request + except ImportError: + return 0, 0 + healthy = 0 + for label, host in OLLAMA_HOSTS: + try: + with urllib.request.urlopen(f'http://{host}/api/tags', timeout=3) as r: + if r.status == 200: + print(f' {status("OK")} {label:18} {host}') + healthy += 1 + continue + except Exception as e: + print(f' {status("WARN")} {label:18} {host} — {type(e).__name__}') + warn += 1 + if healthy == 0: + print(f' {status("FAIL")} 三主機全 DOWN — v5.0 無法運作') + fail += 1 + elif healthy < 3: + print(f' {status("INFO")} {healthy}/3 healthy — retry 鏈有 fallback,可運作') + return warn, fail + + +def check_libreoffice() -> Tuple[int, int]: + print(color('\n[4/5] LibreOffice (PPT vision)', 'bold')) + bin_path = shutil.which('libreoffice') or shutil.which('soffice') + if bin_path: + try: + ver = subprocess.check_output([bin_path, '--version'], timeout=5, text=True).strip() + print(f' {status("OK")} {bin_path} — {ver}') + return 0, 0 + except Exception: + print(f' {status("WARN")} {bin_path} 找到但跑不起來') + return 1, 0 + if os.getenv('PPT_VISION_ENABLED', '').lower() == 'true': + print(f' {status("FAIL")} 未安裝;PPT_VISION_ENABLED=true 但 .pptx→.png 會失敗') + print(f' apt install libreoffice # 188 上跑') + return 0, 1 + print(f' {status("INFO")} 未安裝;PPT_VISION_ENABLED 未開,可忽略') + return 0, 0 + + +def check_mcp() -> Tuple[int, int]: + print(color('\n[5/5] MCP servers', 'bold')) + if os.getenv('MCP_ROUTER_ENABLED', '').lower() != 'true': + print(f' {status("INFO")} MCP_ROUTER_ENABLED=false — 跳過') + return 0, 0 + warn = 0 + try: + import urllib.request + except ImportError: + return 0, 0 + for var in ('MCP_FIRECRAWL_URL', 'MCP_OMNISEARCH_URL', 'MCP_FILESYSTEM_URL', 'MCP_POSTGRES_URL'): + url = os.getenv(var, '').strip() + if not url: + print(f' {status("WARN")} {var} 未設') + warn += 1 + continue + try: + with urllib.request.urlopen(url.rstrip('/') + '/health', timeout=2) as r: + print(f' {status("OK")} {var:22} {url}') + except Exception as e: + print(f' {status("WARN")} {var:22} {url} — {type(e).__name__}') + warn += 1 + return warn, 0 + + +def main() -> int: + print(color('═══ Operation Ollama-First v5.0 Deploy Doctor ═══', 'bold')) + print(f' cwd: {os.getcwd()}') + print(f' host: {os.uname().nodename}') + + total_warn = total_fail = 0 + for fn in (check_env, check_db, check_ollama, check_libreoffice, check_mcp): + try: + w, f = fn() + total_warn += w + total_fail += f + except Exception as e: + print(f' {status("FAIL")} {fn.__name__} 自身爆炸: {type(e).__name__}: {e}') + total_fail += 1 + + print(color('\n═══ 總結 ═══', 'bold')) + print(f' WARN: {total_warn}') + print(f' FAIL: {total_fail}') + if total_fail: + print(color(' ❌ 有 FAIL — v5.0 部署未完成', 'red')) + return 2 + if total_warn: + print(color(' ⚠️ 有 WARN — 可運作但部分 feature 未啟用', 'yellow')) + return 1 + print(color(' ✅ 全綠 — v5.0 部署完整', 'green')) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/templates/admin/ai_calls_dashboard.html b/templates/admin/ai_calls_dashboard.html index 3ac5737..5fc609c 100644 --- a/templates/admin/ai_calls_dashboard.html +++ b/templates/admin/ai_calls_dashboard.html @@ -113,10 +113,12 @@

- 🤖 Operation Ollama-First v5.0 / Phase 27 — Admin Observability + 🤖 Operation Ollama-First v5.0 / Phase 29 — Admin Observability | Promotion Review | Quality Trend | Host Health + | Budget + | PPT Audit

{% endblock %} diff --git a/templates/admin/host_health.html b/templates/admin/host_health.html index b47285b..d5ce9bd 100644 --- a/templates/admin/host_health.html +++ b/templates/admin/host_health.html @@ -113,10 +113,12 @@

- 🤖 Operation Ollama-First v5.0 / Phase 27 — Host Health Dashboard + 🤖 Operation Ollama-First v5.0 / Phase 29 — Host Health Dashboard | AI Calls | Promotion Review | Quality Trend + | Budget + | PPT Audit

{% endblock %} diff --git a/templates/admin/promotion_review.html b/templates/admin/promotion_review.html index 282ef13..d4e04bd 100644 --- a/templates/admin/promotion_review.html +++ b/templates/admin/promotion_review.html @@ -53,9 +53,12 @@ {% endif %}

- 🤖 Operation Ollama-First v5.0 / Phase 28 — PromotionGate Web 審核頁 + 🤖 Operation Ollama-First v5.0 / Phase 29 — PromotionGate Web 審核頁 | AI Calls | Quality Trend + | Host Health + | Budget + | PPT Audit

diff --git a/templates/admin/quality_trend.html b/templates/admin/quality_trend.html index c7f6bc4..a6af835 100644 --- a/templates/admin/quality_trend.html +++ b/templates/admin/quality_trend.html @@ -93,10 +93,12 @@

- 🤖 Operation Ollama-First v5.0 / Phase 25+27 — Caller Quality Trend + 🤖 Operation Ollama-First v5.0 / Phase 29 — Caller Quality Trend | AI Calls | Promotion Review | Host Health + | Budget + | PPT Audit

{% endblock %}