diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 5f27ab30..6c3e6119 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -1501,11 +1501,38 @@
"runsDetail": "Run state is the single view into async work",
"approvals": "Pending Approvals",
"approvalsDetail": "Every high-risk action must stop at the human gate",
- "contracts": "Contracts",
- "contractsDetail": "Project / Agent / Policy contract publish state"
- },
- "lanes": {
- "title": "Flywheel Lanes",
+ "contracts": "Contracts",
+ "contractsDetail": "Project / Agent / Policy contract publish state"
+ },
+ "disposition": {
+ "title": "Disposition Semantics",
+ "diagnosis": {
+ "title": "Read-only Diagnosis",
+ "signal": "AI collected evidence",
+ "owner": "Owner: AI summarizes, SRE judges",
+ "route": "Route: Run monitor / incident detail"
+ },
+ "approval": {
+ "title": "Human Gate",
+ "signal": "High-risk approval pending",
+ "owner": "Owner: SRE approve / reject",
+ "route": "Route: Approval queue"
+ },
+ "execute": {
+ "title": "Auto Execution",
+ "signal": "Low-risk closure path",
+ "owner": "Owner: MCP Gateway executes and audits",
+ "route": "Route: Run State / Audit"
+ },
+ "manual": {
+ "title": "Manual Escalation",
+ "signal": "AI cannot safely repair",
+ "owner": "Owner: war room takes over",
+ "route": "Route: AwoooI SRE war room"
+ }
+ },
+ "lanes": {
+ "title": "Flywheel Lanes",
"live": "Live",
"mirror": "Mirror",
"providerName": "Provider Order",
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index aad2a159..34a59f88 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -1502,11 +1502,38 @@
"runsDetail": "Run state 是非同步任務的唯一觀測入口",
"approvals": "待審批",
"approvalsDetail": "所有高風險動作都必須停在人工閘門",
- "contracts": "合約",
- "contractsDetail": "Project / Agent / Policy contract 發布狀態"
- },
- "lanes": {
- "title": "飛輪鏈路",
+ "contracts": "合約",
+ "contractsDetail": "Project / Agent / Policy contract 發布狀態"
+ },
+ "disposition": {
+ "title": "處置語義",
+ "diagnosis": {
+ "title": "只讀診斷",
+ "signal": "AI 已收集證據",
+ "owner": "負責:AI 先整理,SRE 判讀",
+ "route": "流向:Run 監控 / 事件詳情"
+ },
+ "approval": {
+ "title": "人工閘門",
+ "signal": "高風險待批准",
+ "owner": "負責:SRE approve / reject",
+ "route": "流向:審批佇列"
+ },
+ "execute": {
+ "title": "自動執行",
+ "signal": "低風險可閉環",
+ "owner": "負責:MCP Gateway 執行並稽核",
+ "route": "流向:Run State / Audit"
+ },
+ "manual": {
+ "title": "人工升級",
+ "signal": "AI 無法安全修復",
+ "owner": "負責:戰情室接手",
+ "route": "流向:AwoooI SRE 戰情室"
+ }
+ },
+ "lanes": {
+ "title": "飛輪鏈路",
"live": "已接線",
"mirror": "Mirror",
"providerName": "Provider 順序",
diff --git a/apps/web/src/app/[locale]/awooop/approvals/page.tsx b/apps/web/src/app/[locale]/awooop/approvals/page.tsx
index 2508f7b8..5561f5e2 100644
--- a/apps/web/src/app/[locale]/awooop/approvals/page.tsx
+++ b/apps/web/src/app/[locale]/awooop/approvals/page.tsx
@@ -5,13 +5,15 @@
"use client";
-import { useState, useEffect, useCallback, useRef } from "react";
+import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import {
ShieldCheck,
RefreshCw,
AlertCircle,
Clock,
ArrowRight,
+ ListChecks,
+ TriangleAlert,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { Link } from "@/i18n/routing";
@@ -97,6 +99,15 @@ function TimeoutCell({ timeoutAt }: { timeoutAt: string | null }) {
);
}
+function DecisionPostureBadge() {
+ return (
+
+
+ 人工閘門
+
+ );
+}
+
function ApprovalRow({ approval }: { approval: Approval }) {
const formattedDate = approval.created_at
? new Date(approval.created_at).toLocaleDateString("zh-TW", {
@@ -137,6 +148,9 @@ function ApprovalRow({ approval }: { approval: Approval }) {
{approval.agent_id || "--"}
+
+
+ |
{formattedDate}
@@ -193,6 +207,43 @@ export default function ApprovalsPage() {
const ms = getRemainingMs(a.timeout_at);
return ms !== null && ms <= 5 * 60 * 1000;
}).length;
+ const expiredCount = approvals.filter((a) => {
+ const ms = getRemainingMs(a.timeout_at);
+ return ms !== null && ms <= 0;
+ }).length;
+ const queueSummary = useMemo(
+ () => [
+ {
+ label: "待人工決策",
+ value: approvals.length,
+ detail: "等待 approve / reject 的唯一佇列",
+ icon: ShieldCheck,
+ className: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
+ },
+ {
+ label: "即將逾時",
+ value: criticalCount,
+ detail: "5 分鐘內必須處置",
+ icon: Clock,
+ className: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]",
+ },
+ {
+ label: "已逾時",
+ value: expiredCount,
+ detail: "不得再自動 resume",
+ icon: TriangleAlert,
+ className: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]",
+ },
+ {
+ label: "操作來源",
+ value: "Run",
+ detail: "審批必須回到 Operator Run",
+ icon: ListChecks,
+ className: "border-[#d8d3c7] bg-white text-[#5f5b52]",
+ },
+ ],
+ [approvals.length, criticalCount, expiredCount]
+ );
return (
@@ -230,6 +281,33 @@ export default function ApprovalsPage() {
+
+ {queueSummary.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+
+ {item.label}
+
+ {item.value}
+
+
+
+
+
+
+ {item.detail}
+
+ );
+ })}
+
+
{/* Error State */}
{error && (
@@ -266,6 +344,9 @@ export default function ApprovalsPage() {
Agent
|
+
+ 處置 Lane
+ |
建立時間
|
@@ -278,7 +359,7 @@ export default function ApprovalsPage() {
{loading ? (
Array.from({ length: 5 }).map((_, i) => (
- {Array.from({ length: 5 }).map((_, j) => (
+ {Array.from({ length: 6 }).map((_, j) => (
|
|
diff --git a/apps/web/src/app/[locale]/awooop/page.tsx b/apps/web/src/app/[locale]/awooop/page.tsx
index 51994740..d7f784f1 100644
--- a/apps/web/src/app/[locale]/awooop/page.tsx
+++ b/apps/web/src/app/[locale]/awooop/page.tsx
@@ -14,8 +14,11 @@ import {
CheckCircle2,
FileText,
GitBranch,
+ ListChecks,
RefreshCw,
+ SearchCheck,
ShieldCheck,
+ TriangleAlert,
Waypoints,
} from "lucide-react";
import { Link } from "@/i18n/routing";
@@ -140,6 +143,54 @@ function LaneRow({
);
}
+function DispositionCell({
+ title,
+ signal,
+ owner,
+ route,
+ tone,
+ icon: Icon,
+}: {
+ title: string;
+ signal: string;
+ owner: string;
+ route: string;
+ tone: "diagnosis" | "approval" | "execute" | "manual";
+ icon: typeof SearchCheck;
+}) {
+ return (
+
+
+
+ {signal}
+
+ {title}
+
+
+
+
+
+
+
+
+ - {route}
+
+
+ - {owner}
+
+
+
+ );
+}
+
export default function AwoooPPage() {
const t = useTranslations("awooop.home");
const locale = useLocale();
@@ -295,6 +346,51 @@ export default function AwoooPPage() {
/>
+
+
+
+
+
+ {t("disposition.title")}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx
index 5ee8b5b1..3683ac2a 100644
--- a/apps/web/src/app/[locale]/awooop/runs/page.tsx
+++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx
@@ -5,7 +5,7 @@
"use client";
-import { useState, useEffect, useCallback, useRef } from "react";
+import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import {
Activity,
RefreshCw,
@@ -15,6 +15,10 @@ import {
ChevronLeft,
ChevronRight,
Cpu,
+ ListChecks,
+ SearchCheck,
+ ShieldCheck,
+ TriangleAlert,
} from "lucide-react";
import { cn } from "@/lib/utils";
@@ -32,6 +36,8 @@ type RunState =
| "cancelled"
| "timeout";
+type RunLane = "intake" | "diagnosis" | "approval" | "execution" | "done" | "manual";
+
interface Run {
run_id: string;
project_id: string;
@@ -119,6 +125,62 @@ const STATE_CONFIG: Record<
},
};
+const LANE_CONFIG: Record<
+ RunLane,
+ {
+ label: string;
+ detail: string;
+ icon: typeof Activity;
+ className: string;
+ }
+> = {
+ intake: {
+ label: "排程入口",
+ detail: "等待 worker 接手",
+ icon: ListChecks,
+ className: "border-[#d8d3c7] bg-white text-[#5f5b52]",
+ },
+ diagnosis: {
+ label: "只讀診斷",
+ detail: "MCP / SSH / RAG 收集證據",
+ icon: SearchCheck,
+ className: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
+ },
+ approval: {
+ label: "人工閘門",
+ detail: "等待 SRE 批准或拒絕",
+ icon: ShieldCheck,
+ className: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
+ },
+ execution: {
+ label: "AI 執行中",
+ detail: "模型推理或安全動作執行",
+ icon: Activity,
+ className: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
+ },
+ done: {
+ label: "已收斂",
+ detail: "完成並等待稽核回寫",
+ icon: Activity,
+ className: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
+ },
+ manual: {
+ label: "人工升級",
+ detail: "AI 無法閉環,需人工處置",
+ icon: TriangleAlert,
+ className: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]",
+ },
+};
+
+function getRunLane(state: RunState): RunLane {
+ if (state === "pending") return "intake";
+ if (state === "waiting_tool") return "diagnosis";
+ if (state === "waiting_approval") return "approval";
+ if (state === "running") return "execution";
+ if (state === "completed") return "done";
+ return "manual";
+}
+
// =============================================================================
// Sub Components
// =============================================================================
@@ -151,6 +213,24 @@ function ShadowBadge({ isShadow }: { isShadow: boolean }) {
);
}
+function RunLaneBadge({ state }: { state: RunState }) {
+ const lane = LANE_CONFIG[getRunLane(state)];
+ const Icon = lane.icon;
+
+ return (
+
+
+ {lane.label}
+
+ );
+}
+
function RunRow({ run }: { run: Run }) {
const formattedDate = run.created_at
? new Date(run.created_at).toLocaleDateString("zh-TW", {
@@ -183,6 +263,9 @@ function RunRow({ run }: { run: Run }) {
|
+
+
+ |
|
@@ -279,6 +362,20 @@ export default function RunsPage() {
}, [fetchRuns]);
const totalPages = Math.ceil(total / PER_PAGE);
+ const laneSummary = useMemo(() => {
+ const counts: Record = {
+ intake: 0,
+ diagnosis: 0,
+ approval: 0,
+ execution: 0,
+ done: 0,
+ manual: 0,
+ };
+ runs.forEach((run) => {
+ counts[getRunLane(run.state)] += 1;
+ });
+ return counts;
+ }, [runs]);
return (
@@ -309,6 +406,27 @@ export default function RunsPage() {
+
+ {(Object.keys(LANE_CONFIG) as RunLane[]).map((lane) => {
+ const config = LANE_CONFIG[lane];
+ const Icon = config.icon;
+ return (
+
+
+
+ {config.label}
+ {config.detail}
+
+
+
+
+ {laneSummary[lane]}
+
+
+ );
+ })}
+
+
{/* Filters */}
@@ -380,6 +498,9 @@ export default function RunsPage() {
狀態
|
+
+ 處置 Lane
+ |
Shadow
|
@@ -395,7 +516,7 @@ export default function RunsPage() {
{loading ? (
Array.from({ length: 8 }).map((_, i) => (
- {Array.from({ length: 7 }).map((_, j) => (
+ {Array.from({ length: 8 }).map((_, j) => (
|
|
@@ -404,7 +525,7 @@ export default function RunsPage() {
))
) : runs.length === 0 && !error ? (
- |
+ |
尚無 Run 資料
|
diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx
index 25e88aef..1e71cdc6 100644
--- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx
+++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx
@@ -59,6 +59,24 @@ const workItems: WorkItem[] = [
gate: "Gemini 僅能作為 fallback",
href: "/awooop/runs",
},
+ {
+ phase: "P0",
+ title: "Telegram 診斷 / 修復語義分離",
+ status: "live",
+ surface: "Run 監控 / 審批佇列",
+ source: "automation_state",
+ gate: "只讀診斷不得標成自動修復失敗",
+ href: "/awooop/runs",
+ },
+ {
+ phase: "P0",
+ title: "告警訊息彙整與戰情室分流",
+ status: "in_progress",
+ surface: "AwoooP / Telegram",
+ source: "outbound_message / conversation_event",
+ gate: "TG Bot 私訊不得承接告警洪流",
+ href: "/awooop/approvals",
+ },
{
phase: "P0",
title: "飛輪 KPI 改讀 auto_repair_executions",
diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md
index 6e39ee82..87845c84 100644
--- a/docs/LOGBOOK.md
+++ b/docs/LOGBOOK.md
@@ -4269,3 +4269,35 @@ Playwright:
- 目前公開環境已無法重現 client-side exception;頁面、次級導航、SSE hydration 都正常。
- 若使用者瀏覽器仍看到錯誤,優先判斷為舊 JS chunk / 瀏覽器快取 / 部署切換期間殘留狀態;下一步可要求硬重新整理,或補 Service Worker / cache-busting 檢查。
- 仍需另排前端產品化改善:目前 AwoooP 可用但視覺與資訊密度仍偏 MVP,尚未達到正式 Operator Console 品質。
+
+## 2026-05-06(台北)— AwoooP Operator Console 補上處置語義層
+
+**觸發**:統帥指出 Telegram 告警與 AI 自動化訊息混雜,難以快速判斷「AI 已完成診斷」、「AI 可自動修復」、「AI 無法修復需人工」。
+
+### 改動
+
+- `/zh-TW/awooop` 首頁新增「處置語義」四分流:只讀診斷、人工閘門、自動執行、人工升級。
+- `/zh-TW/awooop/runs` 依 Run state 推導處置 Lane,將 `waiting_tool` 明確標成只讀診斷,`waiting_approval` 標成人工閘門,`failed/cancelled/timeout` 標成人工升級。
+- `/zh-TW/awooop/approvals` 新增審批佇列摘要與「人工閘門」欄位,避免審批項目被誤讀成 AI 已在自動修復。
+- `/zh-TW/awooop/work-items` 補上 Telegram 診斷/修復語義分離與告警彙整分流的工作項目。
+
+### 驗證
+
+```text
+pnpm --dir apps/web typecheck
+# passed
+
+NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web build
+# passed;AwoooP routes 均完成 production build
+
+local next start:
+/zh-TW/awooop -> HTTP 200
+/zh-TW/awooop/runs -> HTTP 200
+/zh-TW/awooop/approvals -> HTTP 200
+/zh-TW/awooop/work-items -> HTTP 200
+```
+
+### 判讀
+
+- 這次只收斂 Operator Console 語義與資訊架構,不改 runtime 決策鏈。
+- 本機瀏覽器檢查時因 localhost 呼叫 `https://awoooi.wooo.work` 受到 CORS 限制,頁面本身無 pageerror;部署到同源正式環境後需再做 live pageerror 複驗。
|