fix(web): surface ai route action summaries
This commit is contained in:
@@ -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": "最新修復診斷證據",
|
||||
|
||||
@@ -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": "最新修復診斷證據",
|
||||
|
||||
@@ -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="立即重新整理"
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,37 @@
|
||||
## 2026-06-04|AwoooP 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/mobile:T178 顯示 `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 readability:88%;首頁、Runs、Work Items 都已能看到承接者與 fallback 摘要。
|
||||
- AwoooP Runs route visibility:86%;route status 已獨立刷新,仍待 production deploy 後用 live API 再驗證。
|
||||
- Work Items route action readability:84%;T178 已有白話摘要,仍待後續串 owner / PlayBook 候選 / repair evidence 完整表格。
|
||||
- Ollama provider order / health truth:82%;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-04|IwoooS Kali 112 只讀重驗證與維護闖關路徑
|
||||
|
||||
**背景**:統帥要求 IwoooS 不能只停留在文字說明,必須讓 192.168.0.112 Kali 資安主機是否納管、目前完成哪些安全框架與下一步卡在哪裡,都能在前端與文件中看見。本輪維持初期低摩擦原則:只讀觀測、顯示闖關條件、暫不開啟掃描、更新、重啟、`/execute` 或服務硬化自動套用。
|
||||
|
||||
Reference in New Issue
Block a user