From a56580fc11d6ea5a6623c21e233d5751dc859898 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 4 Jun 2026 09:46:35 +0800 Subject: [PATCH] fix(web): explain ai provider fallback state --- apps/web/messages/en.json | 13 +++ apps/web/messages/zh-TW.json | 13 +++ .../src/components/shared/ai-model-status.tsx | 98 ++++++++++++++++++- docs/LOGBOOK.md | 27 +++++ 4 files changed, 150 insertions(+), 1 deletion(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 77d20789..24d24d04 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -238,6 +238,19 @@ "network": "網路不可達", "refused": "拒絕" }, + "aiModelSummary": { + "healthyTitle": "目前由 {provider} 承接", + "healthyDetail": "路由順序維持 GCP-A → GCP-B → 111 → Gemini;Gemini 只在三層 Ollama 都不可用後接手。", + "localFallbackDownTitle": "目前由 {provider} 承接;111 備援不可達", + "localFallbackDownDetail": "GCP-A/GCP-B 仍可服務。下一步是修復 111 主機或 LAN,不需要改路由或直接切 Gemini。", + "localFallbackCooldownDetail": "111 備援剛失敗,正在短暫冷卻;GCP-A/GCP-B 仍可服務,先修復 111 主機或 LAN。", + "degradedTitle": "Ollama lane 已降級至 {provider}", + "degradedDetail": "系統已依序嘗試可用節點;請檢查被跳過的 provider 與最近修復證據。", + "downTitle": "三層 Ollama 目前不可用", + "downDetail": "這才會進入 Gemini 最終備援;請優先檢查 GCP-A、GCP-B、111 的連線與服務。", + "unknownTitle": "等待 AI 路由健康資料", + "unknownDetail": "正在讀取 GCP-A、GCP-B、111 與 OpenClaw 狀態。" + }, "loading": "載入中...", "trendUp": "↑{pct}%", "searchPlaceholderShort": "搜尋...", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 77d20789..24d24d04 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -238,6 +238,19 @@ "network": "網路不可達", "refused": "拒絕" }, + "aiModelSummary": { + "healthyTitle": "目前由 {provider} 承接", + "healthyDetail": "路由順序維持 GCP-A → GCP-B → 111 → Gemini;Gemini 只在三層 Ollama 都不可用後接手。", + "localFallbackDownTitle": "目前由 {provider} 承接;111 備援不可達", + "localFallbackDownDetail": "GCP-A/GCP-B 仍可服務。下一步是修復 111 主機或 LAN,不需要改路由或直接切 Gemini。", + "localFallbackCooldownDetail": "111 備援剛失敗,正在短暫冷卻;GCP-A/GCP-B 仍可服務,先修復 111 主機或 LAN。", + "degradedTitle": "Ollama lane 已降級至 {provider}", + "degradedDetail": "系統已依序嘗試可用節點;請檢查被跳過的 provider 與最近修復證據。", + "downTitle": "三層 Ollama 目前不可用", + "downDetail": "這才會進入 Gemini 最終備援;請優先檢查 GCP-A、GCP-B、111 的連線與服務。", + "unknownTitle": "等待 AI 路由健康資料", + "unknownDetail": "正在讀取 GCP-A、GCP-B、111 與 OpenClaw 狀態。" + }, "loading": "載入中...", "trendUp": "↑{pct}%", "searchPlaceholderShort": "搜尋...", diff --git a/apps/web/src/components/shared/ai-model-status.tsx b/apps/web/src/components/shared/ai-model-status.tsx index ed96d7b9..0d907039 100644 --- a/apps/web/src/components/shared/ai-model-status.tsx +++ b/apps/web/src/components/shared/ai-model-status.tsx @@ -24,6 +24,7 @@ interface ModelInfo { interface HealthComponent { status?: 'up' | 'down' | 'degraded' latency_ms?: number | null + provider_name?: string | null diagnosis_code?: string | null retry_after_seconds?: number | null is_cooldown?: boolean @@ -49,6 +50,12 @@ const PROVIDER_ROLES: Record = { openclaw: 'agent', } +interface RouteSummary { + tone: 'healthy' | 'warning' | 'critical' | 'unknown' + title: string + detail: string +} + function statusColor(status: ModelInfo['status']) { if (status === 'up') return '#22C55E' if (status === 'degraded') return '#F59E0B' @@ -56,6 +63,19 @@ function statusColor(status: ModelInfo['status']) { return '#87867f' } +function summaryToneStyle(tone: RouteSummary['tone']) { + if (tone === 'healthy') { + return { border: '#b8d8bd', background: '#f0faf2', color: '#17602a' } + } + if (tone === 'warning') { + return { border: '#d9b36f', background: '#fff7e8', color: '#7a4d05' } + } + if (tone === 'critical') { + return { border: '#e1aaa2', background: '#fff3f1', color: '#9f2f25' } + } + return { border: '#d8d3c7', background: '#faf9f3', color: '#5f5b52' } +} + function modelDetail(model: ModelInfo, t: ReturnType) { if (typeof model.latencyMs === 'number' && model.status === 'up') { return `${Math.round(model.latencyMs)}ms` @@ -81,6 +101,60 @@ function modelDetail(model: ModelInfo, t: ReturnType) { return t(`aiModelRoles.${model.role}` as never) } +function buildRouteSummary( + components: Record, + t: ReturnType, +): RouteSummary { + const aggregate = components.ollama + const selected = aggregate?.provider_name + const selectedLabel = selected ? (PROVIDER_LABELS[selected] ?? selected) : '--' + const gcpAUp = components.ollama_gcp_a?.status === 'up' + const gcpBUp = components.ollama_gcp_b?.status === 'up' + const local = components.ollama_local + const localDown = local?.status === 'down' || local?.status === 'degraded' + const localCooling = Boolean(local?.is_cooldown) + + if (aggregate?.status === 'up' && localDown && (gcpAUp || gcpBUp)) { + return { + tone: 'warning', + title: t('aiModelSummary.localFallbackDownTitle', { provider: selectedLabel }), + detail: localCooling + ? t('aiModelSummary.localFallbackCooldownDetail') + : t('aiModelSummary.localFallbackDownDetail'), + } + } + + if (aggregate?.status === 'up') { + return { + tone: 'healthy', + title: t('aiModelSummary.healthyTitle', { provider: selectedLabel }), + detail: t('aiModelSummary.healthyDetail'), + } + } + + if (aggregate?.status === 'degraded') { + return { + tone: 'warning', + title: t('aiModelSummary.degradedTitle', { provider: selectedLabel }), + detail: t('aiModelSummary.degradedDetail'), + } + } + + if (aggregate?.status === 'down') { + return { + tone: 'critical', + title: t('aiModelSummary.downTitle'), + detail: t('aiModelSummary.downDetail'), + } + } + + return { + tone: 'unknown', + title: t('aiModelSummary.unknownTitle'), + detail: t('aiModelSummary.unknownDetail'), + } +} + export function AIModelStatus() { const t = useTranslations('dashboard') const [models, setModels] = useState([ @@ -89,12 +163,18 @@ export function AIModelStatus() { { name: '111', role: 'local', status: 'unknown' }, { name: 'OpenClaw', role: 'agent', status: 'unknown' }, ]) + const [summary, setSummary] = useState({ + tone: 'unknown', + title: t('aiModelSummary.unknownTitle'), + detail: t('aiModelSummary.unknownDetail'), + }) useEffect(() => { fetch(`${API_BASE}/api/v1/health`) .then(r => r.ok ? r.json() : null) .then((d: HealthResponse | null) => { if (!d?.components) return + setSummary(buildRouteSummary(d.components, t)) const routeOrder = d.ollama_route_order?.length ? d.ollama_route_order : ['ollama_gcp_a', 'ollama_gcp_b', 'ollama_local'] @@ -110,7 +190,9 @@ export function AIModelStatus() { }))) }) .catch(() => {}) - }, []) + }, [t]) + + const summaryStyle = summaryToneStyle(summary.tone) return (
{t('aiModelStatus')}
+
+
+ {summary.title} +
+
+ {summary.detail} +
+
{models.map(m => (
68%`;已補健康設定 source-of-truth,仍需盤點 Argo 安裝來源、RBAC 與 notification config。 - 完整 AI 自動化飛輪:維持 `67%`;本輪降低 GitOps 可觀測性退化風險,未新增 auto-repair execution。 +## 2026-06-04|首頁 AI provider 摘要補強 + +**背景**:111 local fallback 目前因 `192.168.0.111` 主機 / LAN 不可達而顯示 down,但 GCP-A / GCP-B 仍可服務。既有首頁 `AIModelStatus` 只顯示四個節點小卡,使用者容易把 `111` 紅燈誤判為整條 AI 自動化或 Gemini fallback 已經接管。 + +**本次調整**: +- `apps/web/src/components/shared/ai-model-status.tsx` 新增 route summary: + - `ollama` aggregate up 且 `ollama_local` down 時,顯示「目前由 GCP-A/GCP-B 承接;111 備援不可達」。 + - 明確說明下一步是修復 111 主機或 LAN,不需要改路由或直接切 Gemini。 + - 若三層 Ollama 都不可用,才顯示 Gemini 最終備援的語意。 +- `apps/web/messages/zh-TW.json` / `en.json` 補 `dashboard.aiModelSummary` 文案,維持雙語檔同步。 + +**驗證**: +- `python3 -m json.tool apps/web/messages/zh-TW.json` / `en.json` 通過。 +- `cmp -s apps/web/messages/zh-TW.json apps/web/messages/en.json` 通過。 +- `pnpm --dir apps/web exec tsc --noEmit --tsBuildInfoFile /tmp/ai-model-status-route-summary.tsbuildinfo` 通過。 +- `NEXT_PUBLIC_API_URL=https://awoooi.wooo.work NEXT_PRIVATE_BUILD_WORKER_COUNT=1 pnpm --dir apps/web run build` 通過。 +- Browser 本機 production server:`/zh-TW` 可垂直捲動,`horizontalOverflow=0`。 +- Playwright mock production health: + - 桌機 1280px:可見 `目前由 GCP-A 承接;111 備援不可達`、`GCP-A/GCP-B 仍可服務`、`不需要改路由或直接切 Gemini`,`overflow=0`。 + - 手機 390px:同樣可見摘要,`overflow=0`、`canScrollVertical=true`。 + - 截圖:`/tmp/awoooi-ai-route-summary-local.png`、`/tmp/awoooi-ai-route-summary-mobile.png`。 + +**進度更新**: +- 前端 AI provider health 可讀性:`70% -> 82%`;首頁已能解釋 111 fallback 紅燈與 Gemini 接手條件,尚待 Runs / Work Items 的同款摘要元件化。 +- 111 local fallback:維持診斷 `90%`、恢復 `0%`;本輪是可視化,不是實體主機修復。 +- 完整 AI 自動化飛輪:`67% -> 68%`;使用者現在更能判斷 AI lane 是否真的可用,但 auto-repair execution / KM writeback 仍未新增。 + ## 2026-06-03|AwoooP Work Items Owner Review Gate 與 Mobile Shell 可讀性 **背景**:統帥要求 AwoooP / AI 治理不能只在 Telegram 噴告警,前端必須看得出事件跑到哪個流程、誰要接手、AI 做了什麼、哪些步驟被 gate 擋住。本階段聚焦 `/zh-TW/awooop/work-items` 的 KM owner-review 接續處理與手機可讀性:把告警中的 `KM 需要更新` 往 Work Items 的單筆審核、乾跑預覽、Owner 確認、寫回保護與 stale ratio 回測串起來。