fix(web): explain ai provider fallback state
All checks were successful
CD Pipeline / tests (push) Successful in 1m31s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 4m19s
CD Pipeline / post-deploy-checks (push) Successful in 1m30s

This commit is contained in:
Your Name
2026-06-04 09:46:35 +08:00
parent 017dba8b00
commit a56580fc11
4 changed files with 150 additions and 1 deletions

View File

@@ -238,6 +238,19 @@
"network": "網路不可達",
"refused": "拒絕"
},
"aiModelSummary": {
"healthyTitle": "目前由 {provider} 承接",
"healthyDetail": "路由順序維持 GCP-A → GCP-B → 111 → GeminiGemini 只在三層 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": "搜尋...",

View File

@@ -238,6 +238,19 @@
"network": "網路不可達",
"refused": "拒絕"
},
"aiModelSummary": {
"healthyTitle": "目前由 {provider} 承接",
"healthyDetail": "路由順序維持 GCP-A → GCP-B → 111 → GeminiGemini 只在三層 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": "搜尋...",

View File

@@ -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<string, ModelInfo['role']> = {
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<typeof useTranslations>) {
if (typeof model.latencyMs === 'number' && model.status === 'up') {
return `${Math.round(model.latencyMs)}ms`
@@ -81,6 +101,60 @@ function modelDetail(model: ModelInfo, t: ReturnType<typeof useTranslations>) {
return t(`aiModelRoles.${model.role}` as never)
}
function buildRouteSummary(
components: Record<string, HealthComponent>,
t: ReturnType<typeof useTranslations>,
): 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<ModelInfo[]>([
@@ -89,12 +163,18 @@ export function AIModelStatus() {
{ name: '111', role: 'local', status: 'unknown' },
{ name: 'OpenClaw', role: 'agent', status: 'unknown' },
])
const [summary, setSummary] = useState<RouteSummary>({
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 (
<div style={{
@@ -124,6 +206,20 @@ export function AIModelStatus() {
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413' }}>{t('aiModelStatus')}</span>
</div>
<div style={{
margin: '12px 14px 0',
border: `0.5px solid ${summaryStyle.border}`,
background: summaryStyle.background,
borderRadius: 6,
padding: '8px 10px',
}}>
<div style={{ fontSize: 12, fontWeight: 700, color: '#141413', lineHeight: 1.35 }}>
{summary.title}
</div>
<div style={{ marginTop: 3, fontSize: 11, color: summaryStyle.color, lineHeight: 1.45 }}>
{summary.detail}
</div>
</div>
<div style={{ padding: 14, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
{models.map(m => (
<div key={m.name} style={{

View File

@@ -181,6 +181,33 @@
- GitOps control-plane hygiene`55% -> 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-03AwoooP 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 回測串起來。