fix(web): surface ai route action summaries
Some checks failed
CD Pipeline / tests (push) Successful in 1m21s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 4m21s
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-04 10:26:59 +08:00
parent 623ff19b0f
commit 9116ff7bf6
5 changed files with 181 additions and 23 deletions

View File

@@ -3084,12 +3084,24 @@
"recurrenceSourceApplied": "來源配對已套用:{count}",
"recurrenceEmpty": "近期重複告警尚無待處理工作項",
"aiRouteRepairWorkItem": "AI route{lane};目前 {selected};目標 {target};阻塞 {blockers} 項",
"aiRouteActions": {
"monitor": "持續監控即可",
"repair_skipped_primary_lane": "修復被跳過的 Primary lane",
"restore_ollama_lanes": "恢復 Ollama lanes避免只剩雲端",
"inspect_ai_router": "檢查 AI Router / provider 狀態",
"unknown": "待確認下一步"
},
"aiRouteRepairWorkItemId": "Work item{id}",
"aiRouteRepairSkipped": "已跳過:{skipped}",
"aiRouteRepairOwner": "Owner{owner};主責 Agent{lead}",
"aiRouteRepairPlaybook": "PlayBook{playbook};步驟 {steps}",
"aiRouteRepairSafety": "可安全自動修復:{safe}",
"aiRouteRepairSummary": "AI route 目前由 {selected} 承接;下一步:{action};需人工介入:{human}",
"aiRouteRepairUnavailable": "AI route repair evidence 尚未回傳",
"humanRequired": {
"yes": "是",
"no": "否"
},
"driftFingerprint": "Config Drift{state}12h 內 {count} 次",
"driftFingerprintUnavailable": "Config Drift fingerprint state API 尚未回應",
"driftFingerprintId": "Fingerprint{fingerprint}Report{report}",
@@ -4263,6 +4275,12 @@
"inspect_ai_router": "需檢查 AI Router / provider 狀態",
"unknown": "待確認下一步"
},
"summary": {
"primaryTitle": "目前由 {provider} 承接AI lane 正常",
"primaryDetail": "後續備援順序:{standby}。Gemini 只在 Ollama lanes 都不可用後接手;目前下一步是持續監控與保留 fallback 證據。",
"fallbackTitle": "目前由 {provider} 接手AI lane 已降級",
"fallbackDetail": "已跳過:{skipped}。下一步:{action};需確認是否已有 Work Item、PlayBook 與人工 gate。"
},
"degradedSummary": "目前由 {active} 接手;已跳過 {skipped};下一步:{action}",
"repairEvidence": {
"title": "最新修復診斷證據",

View File

@@ -3084,12 +3084,24 @@
"recurrenceSourceApplied": "來源配對已套用:{count}",
"recurrenceEmpty": "近期重複告警尚無待處理工作項",
"aiRouteRepairWorkItem": "AI route{lane};目前 {selected};目標 {target};阻塞 {blockers} 項",
"aiRouteActions": {
"monitor": "持續監控即可",
"repair_skipped_primary_lane": "修復被跳過的 Primary lane",
"restore_ollama_lanes": "恢復 Ollama lanes避免只剩雲端",
"inspect_ai_router": "檢查 AI Router / provider 狀態",
"unknown": "待確認下一步"
},
"aiRouteRepairWorkItemId": "Work item{id}",
"aiRouteRepairSkipped": "已跳過:{skipped}",
"aiRouteRepairOwner": "Owner{owner};主責 Agent{lead}",
"aiRouteRepairPlaybook": "PlayBook{playbook};步驟 {steps}",
"aiRouteRepairSafety": "可安全自動修復:{safe}",
"aiRouteRepairSummary": "AI route 目前由 {selected} 承接;下一步:{action};需人工介入:{human}",
"aiRouteRepairUnavailable": "AI route repair evidence 尚未回傳",
"humanRequired": {
"yes": "是",
"no": "否"
},
"driftFingerprint": "Config Drift{state}12h 內 {count} 次",
"driftFingerprintUnavailable": "Config Drift fingerprint state API 尚未回應",
"driftFingerprintId": "Fingerprint{fingerprint}Report{report}",
@@ -4263,6 +4275,12 @@
"inspect_ai_router": "需檢查 AI Router / provider 狀態",
"unknown": "待確認下一步"
},
"summary": {
"primaryTitle": "目前由 {provider} 承接AI lane 正常",
"primaryDetail": "後續備援順序:{standby}。Gemini 只在 Ollama lanes 都不可用後接手;目前下一步是持續監控與保留 fallback 證據。",
"fallbackTitle": "目前由 {provider} 接手AI lane 已降級",
"fallbackDetail": "已跳過:{skipped}。下一步:{action};需確認是否已有 Work Item、PlayBook 與人工 gate。"
},
"degradedSummary": "目前由 {active} 接手;已跳過 {skipped};下一步:{action}",
"repairEvidence": {
"title": "最新修復診斷證據",

View File

@@ -3437,6 +3437,29 @@ function aiRouteOperatorActionLabelKey(action?: string | null) {
return "operatorActions.unknown";
}
function aiRouteSummaryTone(mode?: string | null) {
if (mode === "primary") {
return "border-[#b8d8bd] bg-[#f0faf2] text-[#17602a]";
}
if (mode === "cloud_fallback" || mode === "unavailable") {
return "border-[#e1aaa2] bg-[#fff3f1] text-[#9f2f25]";
}
return "border-[#d9b36f] bg-[#fff7e8] text-[#6d4707]";
}
function aiRouteStandbySummary(status: AiRouteStatusResponse) {
const standby = status.policy_order
.filter((item) => item.provider_name !== status.selected_provider)
.map((item) => item.provider_name)
.slice(0, 3)
.join(" -> ");
const skipped = (status.skipped_lanes ?? [])
.map((lane) => lane.provider_name)
.filter(Boolean)
.join(" -> ");
return { standby: standby || "--", skipped: skipped || "--" };
}
const AI_ROUTE_REPAIR_BLOCKER_KEYS = new Set([
"gcloud_compute_instances_get_missing",
"gcloud_compute_instances_list_missing",
@@ -3499,6 +3522,7 @@ function AiRouteStatusPanel({
const repairBlockers = repairEvidence?.access_blockers?.filter(Boolean).slice(0, 4) ?? [];
const repairProbes = Object.entries(repairEvidence?.live_probe ?? {}).slice(0, 6);
const skippedLanes = status?.skipped_lanes ?? [];
const routeSummary = status ? aiRouteStandbySummary(status) : { standby: "--", skipped: "--" };
const skippedProviderSet = new Set(
skippedLanes
.map((lane) => lane.provider_name)
@@ -3551,6 +3575,27 @@ function AiRouteStatusPanel({
</div>
) : (
<>
<div className={cn(
"border-b px-4 py-3 text-sm",
aiRouteSummaryTone(laneMode)
)}>
<p className="font-semibold text-[#141413]">
{laneMode === "primary"
? t("summary.primaryTitle", { provider: selectedProvider ?? "--" })
: t("summary.fallbackTitle", { provider: selectedProvider ?? "--" })}
</p>
<p className="mt-1 leading-5">
{laneMode === "primary"
? t("summary.primaryDetail", {
standby: routeSummary.standby,
})
: t("summary.fallbackDetail", {
skipped: routeSummary.skipped,
action: t(operatorActionKey as never),
})}
</p>
</div>
{laneMode && laneMode !== "primary" && (
<div className="flex items-start gap-3 border-b border-[#e0ddd4] bg-[#fff7e8] px-4 py-3 text-sm text-[#6d4707]">
<TriangleAlert className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
@@ -3852,6 +3897,27 @@ export default function RunsPage() {
.catch(() => {});
}, []);
const fetchAiRouteStatus = useCallback(async () => {
try {
const routeStatusRes = await fetch(
`${API_BASE}/api/v1/platform/ai-route-status?workload_type=deep_rca`
);
if (routeStatusRes.ok) {
const routeStatusData: AiRouteStatusResponse = await routeStatusRes.json();
setAiRouteStatus(routeStatusData);
setAiRouteStatusError(null);
} else {
setAiRouteStatus(null);
setAiRouteStatusError(`HTTP ${routeStatusRes.status}`);
}
} catch (routeStatusError) {
setAiRouteStatus(null);
setAiRouteStatusError(
routeStatusError instanceof Error ? routeStatusError.message : "route status failed"
);
}
}, []);
const fetchRuns = useCallback(async (options?: { refresh?: boolean }) => {
try {
setError(null);
@@ -3989,25 +4055,6 @@ export default function RunsPage() {
);
}
try {
const routeStatusRes = await fetch(
`${API_BASE}/api/v1/platform/ai-route-status?workload_type=deep_rca`
);
if (routeStatusRes.ok) {
const routeStatusData: AiRouteStatusResponse = await routeStatusRes.json();
setAiRouteStatus(routeStatusData);
setAiRouteStatusError(null);
} else {
setAiRouteStatus(null);
setAiRouteStatusError(`HTTP ${routeStatusRes.status}`);
}
} catch (routeStatusError) {
setAiRouteStatus(null);
setAiRouteStatusError(
routeStatusError instanceof Error ? routeStatusError.message : "route status failed"
);
}
setLastRefresh(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : "載入失敗");
@@ -4020,18 +4067,20 @@ export default function RunsPage() {
// 初次載入
useEffect(() => {
setLoading(true);
fetchAiRouteStatus();
fetchRuns();
}, [fetchRuns]);
}, [fetchAiRouteStatus, fetchRuns]);
// 30 秒自動刷新
useEffect(() => {
intervalRef.current = setInterval(() => {
fetchAiRouteStatus();
fetchRuns();
}, AUTO_REFRESH_INTERVAL);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [fetchRuns]);
}, [fetchAiRouteStatus, fetchRuns]);
const totalPages = Math.ceil(total / PER_PAGE);
const laneSummary = useMemo(() => {
@@ -4106,7 +4155,11 @@ export default function RunsPage() {
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground"> 30 </span>
<button
onClick={() => { setLoading(true); fetchRuns({ refresh: true }); }}
onClick={() => {
setLoading(true);
fetchAiRouteStatus();
fetchRuns({ refresh: true });
}}
disabled={loading}
className="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg transition disabled:opacity-50"
aria-label="立即重新整理"

View File

@@ -2060,6 +2060,21 @@ function callbackTraceRecoveryAction(
return { actionKey: "observeRecovery", humanRequired: false };
}
function aiRouteOperatorActionText(
action: string | null | undefined,
t: ReturnType<typeof useTranslations>
) {
if (
action === "monitor" ||
action === "repair_skipped_primary_lane" ||
action === "restore_ollama_lanes" ||
action === "inspect_ai_router"
) {
return t(`evidence.aiRouteActions.${action}` as never);
}
return t("evidence.aiRouteActions.unknown");
}
function buildWorkItems(
telemetry: Telemetry,
t: ReturnType<typeof useTranslations>
@@ -2105,6 +2120,14 @@ function buildWorkItems(
?.map((lane) => lane.provider_name)
.filter(Boolean)
.join(" -> ");
const aiRouteActionText = aiRouteOperatorActionText(
aiRoute?.operator_action?.action,
t
);
const aiRouteHumanRequired = aiRoute?.operator_action?.human_required === true;
const aiRouteHumanRequiredText = aiRouteHumanRequired
? t("evidence.humanRequired.yes")
: t("evidence.humanRequired.no");
const remediationQueue = telemetry.slo?.adr100?.verification_coverage?.remediation_queue;
const remediationTotal = remediationQueue?.total ?? 0;
const remediationReadyForAi = remediationQueue?.ready_for_ai ?? 0;
@@ -2233,6 +2256,11 @@ function buildWorkItems(
}),
evidenceDetails: aiRouteRepairEvidence
? [
t("evidence.aiRouteRepairSummary", {
selected: aiRoute?.selected_provider ?? "--",
action: aiRouteActionText,
human: aiRouteHumanRequiredText,
}),
t("evidence.aiRouteRepairWorkItemId", {
id: aiRouteWorkItem?.work_item_id ?? "--",
}),
@@ -2251,7 +2279,14 @@ function buildWorkItems(
safe: String(aiRouteOwnerAction?.safe_to_auto_repair ?? false),
}),
]
: [t("evidence.aiRouteRepairUnavailable")],
: [
t("evidence.aiRouteRepairSummary", {
selected: aiRoute?.selected_provider ?? "--",
action: aiRouteActionText,
human: aiRouteHumanRequiredText,
}),
t("evidence.aiRouteRepairUnavailable"),
],
href: aiRouteWorkItem?.target_href ?? "/awooop/runs",
},
{

View File

@@ -1,3 +1,37 @@
## 2026-06-04AwoooP Runs / Work Items AI Route 摘要前移
**背景**統帥指出前端頁面仍有大量文字operator 很難快速知道 AI provider 目前由誰承接、下一步要做什麼、是否需要人工介入。本輪延續首頁 AI route summary把同一份「GCP-A → GCP-B → 111 → Gemini」順序與卡點摘要延伸到 AwoooP Runs / Work Items。
**本輪完成**
- `/zh-TW/awooop/runs` 的 AI Provider 路由區塊新增可掃描摘要:
- Primary 正常時顯示「目前由 {provider} 承接AI lane 正常」。
- 同步顯示後續備援順序,明確標示 Gemini 只在 Ollama lanes 都不可用後接手。
- 降級時顯示已跳過 lanes、operator action、是否要看 Work Item / PlayBook / 人工 gate。
- 修正 Runs route status 空白問題:
- 原本 `ai-route-status` fetch 綁在 runs list 後段,容易被其他補充 API 或載入流程擋住,導致 panel 顯示「尚未取得」。
- 改成獨立 `fetchAiRouteStatus()`初次載入、30 秒自動刷新、手動立即刷新都會更新 provider route。
- `/zh-TW/awooop/work-items` 的 T178 加入白話 evidence
- 顯示目前 selected provider、下一步 action、是否需人工介入。
- 不再把 raw action 或 i18n key 暴露給 operator。
- 補 `zh-TW` / `en` i18n`awooop.aiRouteStatus.summary.*``awooop.workItems.evidence.aiRouteActions.*``aiRouteRepairSummary``humanRequired.*`
**本機驗證**
- JSON / i18n parity`json ok``i18n parity ok`
- `pnpm --dir apps/web exec tsc --noEmit --tsBuildInfoFile /tmp/ai-route-summary-runs-workitems.tsbuildinfo`:通過。
- `NEXT_PUBLIC_API_URL=https://awoooi.wooo.work NEXT_PRIVATE_BUILD_WORKER_COUNT=1 SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 pnpm --dir apps/web run build`:通過。
- Playwright local production preview `localhost:3113`
- Runs desktop/mobile`ai-route-status` request 已發出;顯示 `目前由 ollama_gcp_a 承接AI lane 正常`;顯示 `Gemini 只在 Ollama lanes 都不可用後接手``horizontalOverflow=0`
- Work Items desktop/mobileT178 顯示 `AI route 目前由 ollama_gcp_a 承接;下一步:持續監控即可;需人工介入:否`;無 raw key`horizontalOverflow=0`
- 截圖:`/tmp/awoooi-runs-desktop-route-summary-final.png``/tmp/awoooi-runs-mobile-route-summary-final.png``/tmp/awoooi-work-items-desktop-route-summary-final.png``/tmp/awoooi-work-items-mobile-route-summary-final.png`
**目前整體進度(本階段完成後)**
- Frontend AI provider health readability88%首頁、Runs、Work Items 都已能看到承接者與 fallback 摘要。
- AwoooP Runs route visibility86%route status 已獨立刷新,仍待 production deploy 後用 live API 再驗證。
- Work Items route action readability84%T178 已有白話摘要,仍待後續串 owner / PlayBook 候選 / repair evidence 完整表格。
- Ollama provider order / health truth82%GCP-A → GCP-B → 111 → Gemini 順序已同步到前端,但 111 local fallback 仍因 110 到 111 LAN/route 不通而未恢復。
- 111 local fallback診斷 90%、恢復 0%;目前仍需主機 / 網路層修復,不在本前端階段處理。
- 完整 AI Agent 自動化飛輪69%;可視化與 operator 可判讀性提升,但 verified auto-repair execution success、executor handoff、KM 學習閉環仍是主要缺口。
## 2026-06-04IwoooS Kali 112 只讀重驗證與維護闖關路徑
**背景**:統帥要求 IwoooS 不能只停留在文字說明,必須讓 192.168.0.112 Kali 資安主機是否納管、目前完成哪些安全框架與下一步卡在哪裡,都能在前端與文件中看見。本輪維持初期低摩擦原則:只讀觀測、顯示闖關條件、暫不開啟掃描、更新、重啟、`/execute` 或服務硬化自動套用。