"use client"; import {useEffect, useRef, useState} from "react"; import {Activity, ArrowRight, CheckCircle2, Database, FlaskConical, Play, RefreshCw, ShieldCheck, SlidersHorizontal, Square, X} from "lucide-react"; import AgentNav from "@/components/xingbang/AgentNav"; import "./operations.css"; const ROOT = "/api/risk-agent/project-assistant/operations"; const names: Record = {queued:"排队中", running:"执行中", completed:"执行完成", partial:"部分完成", failed:"执行失败", cancelled:"已取消", pass:"检查通过", issue:"发现问题", unknown:"依据不足", not_applicable:"不适用", error:"错误", skipped:"未执行", ok:"正常", unverified:"未验证", unavailable:"不可用"}; const stamp = (value?:string) => value ? new Date(value).toLocaleString("zh-CN", {hour12:false}) : "—"; const fmt = (value:any) => value === null || value === undefined ? "—" : String(value); function Badge({value}:{value:string}) {return {names[value] || value};} async function api(path:string, body?:unknown, key?:string) { const token = sessionStorage.getItem("xingbang-access-token"); const response = await fetch(ROOT+path, {method:body===undefined?"GET":"POST", cache:"no-store", headers:{"Content-Type":"application/json", ...(token?{Authorization:`Bearer ${token}`} : {}), ...(key?{"Idempotency-Key":key}:{})}, body:body===undefined?undefined:JSON.stringify(body)}); const data = await response.json(); if (!response.ok) throw Error(typeof data.detail === "string" ? data.detail : "请求失败,请核对输入与权限"); return data; } export default function OperationsCenter() { const [domain,setDomain] = useState("lab"), [view,setView] = useState("monitor"), [ready,setReady] = useState(false); const [overview,setOverview] = useState(null), [source,setSource] = useState(null), [detail,setDetail] = useState(null); const [selected,setSelected] = useState(""), [status,setStatus] = useState("all"), [search,setSearch] = useState(""); const [busy,setBusy] = useState(false), [error,setError] = useState(""), [notice,setNotice] = useState(""), [health,setHealth] = useState(null); const [connectionError,setConnectionError] = useState(""); const [form,setForm] = useState({}), [dirty,setDirty] = useState(false), [model,setModel] = useState(false), [requirement,setRequirement] = useState(""); const [compareId,setCompareId] = useState(""), [comparison,setComparison] = useState(null); const epoch = useRef(0); const admin = overview?.identity?.preview || overview?.identity?.role === "admin"; const loadSource = async () => { const data = await api("/lab/source"); setSource(data); setForm({revision:data.project.revision, amount:data.project.amount, liabilities:data.finance.liabilities, dd_liabilities:data.finance.dd_liabilities, cashflow:data.finance.cashflow ?? "", duplicate:!!data.assets.find((a:any)=>a.row_id===3)?.active, enabled:!!data.project.enabled}); setDirty(false); }; useEffect(()=>{const q=new URLSearchParams(location.search); setDomain(q.get("domain")==="current" || (q.has("project") && !q.has("domain")) ? "current":"lab"); setView(["monitor","trace","lab","health"].includes(q.get("view")||"")?q.get("view")!:"monitor"); setReady(true);},[]); useEffect(()=>{ if(!ready)return; const version=++epoch.current; let disposed=false, loading=false; setOverview(null);setSelected("");setDetail(null);setSource(null);setError("");setCompareId("");setComparison(null); async function load(){if(loading)return;loading=true;try{const data=await api(`/overview?domain=${domain}`);if(!disposed&&epoch.current===version){setOverview(data);setConnectionError("");}}catch(e:any){if(!disposed)setConnectionError(e.message);}finally{loading=false;}} load(); const timer=setInterval(load,2500); if(domain==="lab")loadSource().catch(e=>{if(!disposed)setError(e.message);}); return()=>{disposed=true;clearInterval(timer);}; },[domain,ready]); useEffect(()=>{ if(!selected)return; let disposed=false, loading=false;setDetail(null);setCompareId("");setComparison(null); async function load(){if(loading)return;loading=true;try{const data=await api(`/runs/${encodeURIComponent(selected)}?domain=${domain}`);if(!disposed)setDetail(data);}catch(e:any){if(!disposed)setError(e.message);}finally{loading=false;}} load();const timer=setInterval(load,2500);return()=>{disposed=true;clearInterval(timer);}; },[selected,domain]); useEffect(()=>{if(view==="health"&&admin)api("/health").then(setHealth).catch(e=>setError(e.message));},[view,admin]); useEffect(()=>{let disposed=false;setComparison(null);if(compareId)api(`/runs/${compareId}?domain=${domain}`).then(data=>{if(!disposed)setComparison(data);}).catch(e=>{if(!disposed)setError(e.message);});return()=>{disposed=true;};},[compareId,domain]); async function act(fn:()=>Promise){setBusy(true);setError("");setNotice("");try{await fn();setOverview(await api(`/overview?domain=${domain}`));}catch(e:any){setError(e.message);}finally{setBusy(false);}} function edit(key:string,value:any){setForm((f:any)=>({...f,[key]:value}));setDirty(true);} function chooseView(next:string){setView(next);history.replaceState({},"",`?domain=${domain}&view=${next}`);} async function submit(){await act(async()=>{const run=await api("/lab/runs",{source_revision:source.project.revision,model,requirement:model?requirement:""},crypto.randomUUID());setSelected(run.id);setView("monitor");await loadSource();setNotice("真实任务已入队。执行记录会自动刷新;历史快照保持不变。");});} const runs=(overview?.runs||[]).filter((r:any)=>(status==="all"||r.status===status)&&(!search||`${r.project_id} ${r.id}`.toLowerCase().includes(search.toLowerCase()))); const run=detail?.run; const oldChecks=new Map((comparison?.run?.checks||[]).map((c:any)=>[c.code,c])); const newChecks=new Map((run?.checks||[]).map((c:any)=>[c.code,c])); const differences=Array.from(new Set([...Array.from(oldChecks.keys()),...Array.from(newChecks.keys())])).map(code=>({code,before:oldChecks.get(code),after:newChecks.get(code)})); return

XINGBANG / OPERATIONS

项目运行中心

看到真实执行,追溯每次变化。

每 2.5 秒更新 最后成功读取 {overview?stamp(overview.time):"等待连接"}

{domain==="lab"?"LAB-001 · 独立 SQLite · 所有数值及阈值均为演示数据":"仅展示当前身份获授权的项目;不会自动重跑或修改项目"}

{error&&
{error}
} {connectionError&&
监控连接异常:{connectionError}。下方可能是上次读取的数据。
} {notice&&
{notice}
} {(view==="monitor"||view==="trace")&&<>
{[["running","执行中"],["queued","排队中"],["failed","执行失败"],["partial","部分完成"]].map(([key,label])=>)}
项目待处理问题{overview?.todo_count??"—"}{overview?.stale_report_count??0} 份底稿待更新

任务记录

最近 100 条 / 共 {overview?.total_runs??0} 条
setSearch(e.target.value)}/>
{runs.map((r:any)=>)}
项目 / 任务执行状态耗时 / 等待业务结果
{r.step}{r.lease_expired&&工作租约已过期,等待恢复}{r.heartbeat_at&&心跳 {stamp(r.heartbeat_at)}}{fmt(r.elapsed_seconds)} s等待 {fmt(r.queue_seconds)} s{r.summary?.open??"—"} 项待处理{r.superseded&&数据已变更,此次结果未覆盖项目}
{!runs.length&&

{overview?"此筛选下暂无运行":"正在连接后端…"}

{domain==="lab"&&}
}
{detail&&

执行依据 · {run.project_id}

{run.id}
{domain==="lab"&&admin&&["queued","running"].includes(run.status)&&}

数据版本 {detail.data_revision} · SQL 源版本 {fmt(detail.source_revision)} · 快照 {run.input_snapshot_id}。执行完成 ≠ 无业务问题。

本次自然语言要求:{run.requirement||"无;按已冻结的方案执行"}

{run.error&&

{run.error}

}
{run.steps.map((s:any)=>
{s.name}
{s.skill.version} · {fmt(s.elapsed_seconds)} 秒{s.errors?.map((e:any,i:number)=>

{e.message}

)}{s.missing_inputs?.length>0&&

缺输入:{s.missing_inputs.join("、")}

}
)}

实际采用的 Skills 与规则

{detail.components.map((c:any)=>)}
组件本次冻结版本当前发布版本执行内容 / 哈希
{c.name}{c.id} · {c.engine||c.kind}{c.version}{c.current_version}{c.changed&&版本已变化,历史任务不变}{c.conditions?.map((rule:any,i:number)=>

{rule.field} {rule.operator} {rule.threshold}(触发问题)

)}{c.content_hash?.slice(0,20)}…

检查结果与数据来源

{(run.checks||[]).map((c:any,i:number)=>
{c.title}

{c.suggested_action}

规则依据:{c.basis_ids?.join("、")}

{(run.evidence||[]).filter((e:any)=>c.evidence_ids?.includes(e.id)).map((e:any)=>

{e.source} · {e.locator} · 值:{fmt(e.value)}来源版本 {fmt(e.source_revision)} · {e.id}

)}
)}

与同项目历史运行对比

{comparison&&<>

所选历史 → 当前选中任务。覆盖率:{fmt(comparison.run.summary?.coverage)} → {fmt(run.summary?.coverage)};未执行的检查不会被视作通过。

{differences.map(d=>)}
检查项所选历史当前任务
{d.after?.title||d.before?.title}
}
执行事件 · {detail.events.length} 条{detail.events.map((e:any)=>

{stamp(e.time)} · {e.action} · {e.detail.name||""}

)}
} {view==="trace"&&!detail&&

点击上方任务,查看当次冻结的配置版本、检查结果、SQL 行级来源及历史对比。

} } {view==="lab"&&(domain!=="lab"?

演示操作只在隔离库执行

不会修改项目工作区已有数据。

:<>

MINIMAL DATA / REAL EXECUTION

一个项目,演示完整的数据变化

SQLite 原始记录 → 人工确认同步 → 冻结快照 → Skills / 规则 → 问题与差异

01 / 编辑演示源

{source?.database} · 版本 {source?.project.revision??"—"}

1 条项目、1 条财务、3 条设备(其中 1 条为重复记录)。万元 / 人民币 / 2025 年 / 合并口径。

{source&&<>
{[["amount","融资金额(万元)"],["liabilities","财报负债(万元)"],["dd_liabilities","尽调负债(万元)"],["cashflow","经营现金流(空白表示缺失)"]].map(([key,label])=>)}
{source.assets.map((a:any)=>)}
源表行 ID设备编号设备价值参与读取
{a.row_id}{a.asset_key}{a.value}{a.active?"是":"否"}

源版本 {source.project.revision} / 已同步版本 {fmt(source.synced_revision)} {dirty?"· 有未保存修改":""}

}

02 / 配置与真实执行

固定 4 个确定性 Skills,模型步骤可选
{["财务一致性","现金流完整性","设备去重","价值覆盖率"].map(s=>{s})}
默认不调用模型、不生成模拟思考过程。

03 / 隔离回归验收

临时库中执行四组固定案例,比较真实检查码与覆盖率。不改本页数据。

{overview?.evaluations?.map((e:any)=>
{stamp(e.created_at)} · {e.duration_ms} ms

{e.scope}

{e.cases.map((c:any)=>)}
案例预期 / 实际问题项预期 / 实际覆盖率结果
{c.name}规则 {c.rule_version}{c.expected_codes.length} / {c.actual_codes.length}
检查码对照

预期:{c.expected_codes.join("、")||"无"}

实际:{c.actual_codes.join("、")||"无"}

{c.expected_coverage} / {fmt(c.actual_coverage)}
)}{!overview?.evaluations?.length&&

尚无验收记录,不能据此声明通过。

}
)} {view==="health"&&

系统自检

{admin&&}
{!admin?

仅管理员可查看系统诊断,项目资料权限另行校验。

:<>

本次检测 {stamp(health?.time)};结果是检查时点的状态,不是持续可用性承诺。

{health?.checks.map((c:any)=>

{c.name}

{c.detail}

)}}
}
; }