fix(web): explain ai provider fallback state
This commit is contained in:
@@ -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": "搜尋...",
|
||||
|
||||
@@ -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": "搜尋...",
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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-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 回測串起來。
|
||||
|
||||
Reference in New Issue
Block a user