fix(awooop): clarify operator disposition lanes
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 順序",
|
||||
|
||||
@@ -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 (
|
||||
<span className="inline-flex items-center gap-1.5 border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 text-xs font-semibold text-[#8a5a08]">
|
||||
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
人工閘門
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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 || "--"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<DecisionPostureBadge />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-muted-foreground font-mono">
|
||||
{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 (
|
||||
<div className="space-y-6">
|
||||
@@ -230,6 +281,33 @@ export default function ApprovalsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="grid gap-px border border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-4">
|
||||
{queueSummary.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div key={item.label} className="bg-white px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
|
||||
<div className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-8 w-8 items-center justify-center border",
|
||||
item.className
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">{item.detail}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<div className="flex items-start gap-3 border border-[#e2a29b] bg-[#fff0ef] p-4">
|
||||
@@ -266,6 +344,9 @@ export default function ApprovalsPage() {
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Agent
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
處置 Lane
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
建立時間
|
||||
</th>
|
||||
@@ -278,7 +359,7 @@ export default function ApprovalsPage() {
|
||||
{loading ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<tr key={i} className="border-b border-border">
|
||||
{Array.from({ length: 5 }).map((_, j) => (
|
||||
{Array.from({ length: 6 }).map((_, j) => (
|
||||
<td key={j} className="px-4 py-3">
|
||||
<div className="h-5 bg-muted animate-pulse rounded w-20" />
|
||||
</td>
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-[154px] border border-[#e0ddd4] bg-white px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#77736a]">{signal}</p>
|
||||
<h3 className="mt-2 text-base font-semibold tracking-normal text-[#141413]">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-8 w-8 items-center justify-center border",
|
||||
tone === "diagnosis" && "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
|
||||
tone === "approval" && "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
|
||||
tone === "execute" && "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
|
||||
tone === "manual" && "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<dl className="mt-4 grid gap-2 text-xs">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<dt className="text-[#77736a]">{route}</dt>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<dt className="text-[#77736a]">{owner}</dt>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AwoooPPage() {
|
||||
const t = useTranslations("awooop.home");
|
||||
const locale = useLocale();
|
||||
@@ -295,6 +346,51 @@ export default function AwoooPPage() {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="border border-[#e0ddd4] bg-[#e0ddd4]">
|
||||
<div className="border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ListChecks className="h-4 w-4 text-[#d97757]" aria-hidden="true" />
|
||||
<h3 className="text-sm font-semibold text-[#141413]">
|
||||
{t("disposition.title")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-px md:grid-cols-2 xl:grid-cols-4">
|
||||
<DispositionCell
|
||||
title={t("disposition.diagnosis.title")}
|
||||
signal={t("disposition.diagnosis.signal")}
|
||||
owner={t("disposition.diagnosis.owner")}
|
||||
route={t("disposition.diagnosis.route")}
|
||||
tone="diagnosis"
|
||||
icon={SearchCheck}
|
||||
/>
|
||||
<DispositionCell
|
||||
title={t("disposition.approval.title")}
|
||||
signal={t("disposition.approval.signal")}
|
||||
owner={t("disposition.approval.owner")}
|
||||
route={t("disposition.approval.route")}
|
||||
tone="approval"
|
||||
icon={ShieldCheck}
|
||||
/>
|
||||
<DispositionCell
|
||||
title={t("disposition.execute.title")}
|
||||
signal={t("disposition.execute.signal")}
|
||||
owner={t("disposition.execute.owner")}
|
||||
route={t("disposition.execute.route")}
|
||||
tone="execute"
|
||||
icon={Activity}
|
||||
/>
|
||||
<DispositionCell
|
||||
title={t("disposition.manual.title")}
|
||||
signal={t("disposition.manual.signal")}
|
||||
owner={t("disposition.manual.owner")}
|
||||
route={t("disposition.manual.route")}
|
||||
tone="manual"
|
||||
icon={TriangleAlert}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-5 xl:grid-cols-[1.1fr_0.9fr]">
|
||||
<div className="border border-[#e0ddd4] bg-white">
|
||||
<div className="border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
|
||||
@@ -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 (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 border px-2 py-0.5 text-xs font-semibold",
|
||||
lane.className
|
||||
)}
|
||||
title={lane.detail}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{lane.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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 }) {
|
||||
<td className="px-4 py-3">
|
||||
<RunStateBadge state={run.state} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<RunLaneBadge state={run.state} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<ShadowBadge isShadow={run.is_shadow} />
|
||||
</td>
|
||||
@@ -279,6 +362,20 @@ export default function RunsPage() {
|
||||
}, [fetchRuns]);
|
||||
|
||||
const totalPages = Math.ceil(total / PER_PAGE);
|
||||
const laneSummary = useMemo(() => {
|
||||
const counts: Record<RunLane, number> = {
|
||||
intake: 0,
|
||||
diagnosis: 0,
|
||||
approval: 0,
|
||||
execution: 0,
|
||||
done: 0,
|
||||
manual: 0,
|
||||
};
|
||||
runs.forEach((run) => {
|
||||
counts[getRunLane(run.state)] += 1;
|
||||
});
|
||||
return counts;
|
||||
}, [runs]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -309,6 +406,27 @@ export default function RunsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="grid gap-px border border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-3 xl:grid-cols-6">
|
||||
{(Object.keys(LANE_CONFIG) as RunLane[]).map((lane) => {
|
||||
const config = LANE_CONFIG[lane];
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<div key={lane} className="bg-white px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#77736a]">{config.label}</p>
|
||||
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">{config.detail}</p>
|
||||
</div>
|
||||
<Icon className="h-4 w-4 text-[#87867f]" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 font-mono text-2xl font-semibold text-[#141413]">
|
||||
{laneSummary[lane]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap items-center gap-3 border border-[#e0ddd4] bg-white p-4">
|
||||
<Filter className="w-4 h-4 text-muted-foreground flex-shrink-0" aria-hidden="true" />
|
||||
@@ -380,6 +498,9 @@ export default function RunsPage() {
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
狀態
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
處置 Lane
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Shadow
|
||||
</th>
|
||||
@@ -395,7 +516,7 @@ export default function RunsPage() {
|
||||
{loading ? (
|
||||
Array.from({ length: 8 }).map((_, i) => (
|
||||
<tr key={i} className="border-b border-border">
|
||||
{Array.from({ length: 7 }).map((_, j) => (
|
||||
{Array.from({ length: 8 }).map((_, j) => (
|
||||
<td key={j} className="px-4 py-3">
|
||||
<div className="h-5 bg-muted animate-pulse rounded w-20" />
|
||||
</td>
|
||||
@@ -404,7 +525,7 @@ export default function RunsPage() {
|
||||
))
|
||||
) : runs.length === 0 && !error ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-4 py-16 text-center">
|
||||
<td colSpan={8} className="px-4 py-16 text-center">
|
||||
<Activity className="w-10 h-10 text-muted-foreground/30 mx-auto mb-3" aria-hidden="true" />
|
||||
<p className="text-sm text-muted-foreground">尚無 Run 資料</p>
|
||||
</td>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 複驗。
|
||||
|
||||
Reference in New Issue
Block a user