feat: 完成项目助手与监控演示并修复聊天及云部署配置

This commit is contained in:
NathanAlex7
2026-09-11 15:56:28 +08:00
parent c57d291f65
commit af1a1027d7
75 changed files with 5215 additions and 101 deletions
+79 -8
View File
@@ -1,12 +1,83 @@
"use client";
import {useEffect,useState} from "react";
import {ArrowRight,RefreshCw,ShieldCheck} from "lucide-react";
import AgentNav,{projectLink} from "@/components/xingbang/AgentNav";
import {useEffect, useRef, useState} from "react";
import {ArrowRight, FileText, RefreshCw, ShieldCheck} from "lucide-react";
import AgentNav from "@/components/xingbang/AgentNav";
import AssistantConfiguration from "@/components/xingbang/AssistantConfiguration";
import AssistantHistory from "@/components/xingbang/AssistantHistory";
import "../data-access/workspace.css";
export default function XingbangWorkspace(){
const [data,setData]=useState<any[]>([]),[search,setSearch]=useState(""),[error,setError]=useState(""),[loading,setLoading]=useState(true);
async function load(){setLoading(true);setError("");try{const token=sessionStorage.getItem("xingbang-access-token");const r=await fetch("/api/risk-agent/workspace/projects",{headers:token?{Authorization:`Bearer ${token}`}:{}});const d=await r.json();if(!r.ok)throw Error(d.detail);setData(d.projects);}catch(e:any){setError(e.message);}finally{setLoading(false);}}
useEffect(()=>{load();},[]);
return <div className="ra-app"><AgentNav active="overview"/><main className="xb-overview"><div className="xb-overview-heading"><div><h1>兴邦金租 · 项目工作台</h1><p>项目总览</p></div><a className="ra-button primary" href="/data-access?view=run"><ShieldCheck size={16}/>发起风险研判</a></div>{error&&<div role="alert" className="ra-alert error">{error}<a href="/data-access">身份设置</a></div>}<div className="xb-overview-stat"><span><strong>{data.length}</strong>项目</span><span><strong>{data.filter(p=>p.coordination&&p.coordination.stage!=="已完成").length}</strong>评审在办</span><span><strong>{data.filter(p=>p.collateral?.stale).length}</strong>核验待更新</span><span><strong>{data.filter(p=>p.report?.status==="final").length}</strong>报告已定稿</span></div><div className="xb-overview-tools"><input aria-label="搜索项目" placeholder="项目编号 / 客户名称" value={search} onChange={e=>setSearch(e.target.value)}/><button className="ra-button" title="刷新项目" disabled={loading} onClick={load}><RefreshCw size={15}/></button></div><div className="xb-table-wrap"><table className="xb-project-table"><thead><tr><th>项目 / 客户</th><th>01 风险研判</th><th>02 报告与经验</th><th>03 评审协同</th><th>04 租赁物与增信</th></tr></thead><tbody>{data.filter(p=>(p.project_id+" "+p.risk?.customer).toLowerCase().includes(search.toLowerCase())).map(p=><tr key={p.project_id}><td><b>{p.project_id}</b><small>{p.risk?.customer||"客户资料待接入"}</small></td><td><a href={projectLink("/data-access",p.project_id)}>{p.risk?`${p.risk.findings} 项关注`:"开始研判"}<ArrowRight size={12}/></a><small>{p.risk?.time?new Date(p.risk.time).toLocaleString():"暂无快照"}</small></td><td><a href={projectLink("/report-workbench",p.project_id)}>{p.report?(p.report.status==="final"?"已定稿":"待复核草稿"):"建立报告"}<ArrowRight size={12}/></a><small>{p.report?`版本 ${p.report.version}`:"尚未建立"}</small></td><td><a href={projectLink("/review-workbench",p.project_id)}>{p.coordination?.stage||"接入评审"}<ArrowRight size={12}/></a><small>{p.coordination?`${p.coordination.owner} · ${p.coordination.open_tasks} 项补件`:"尚未分配"}</small></td><td><a href={projectLink("/collateral-workbench",p.project_id)}>{!p.collateral?"开始核验":p.collateral.stale?"需重新核验":`${p.collateral.open_checks} 项待落实`}<ArrowRight size={12}/></a><small>{p.collateral?.has_result?"已保存核验快照":"尚无核验结果"}</small></td></tr>)}</tbody></table></div>{!data.length&&<div className="ra-empty">{loading?"正在读取项目…":"暂无项目研判记录"}</div>}</main></div>;
const tabs = [["overview", "概览"], ["findings", "材料与问题"], ["scenario", "方案比较"], ["report", "报告底稿"], ["experience", "反馈与经验"]];
const statuses: Record<string, string> = {queued: "已排队", running: "检查中", completed: "已完成", partial: "部分完成", failed: "未完成", cancelled: "已取消", skipped: "未执行", draft: "草稿", published: "已发布", rejected: "已拒绝", rolled_back: "已停用", stale: "待更新", finalized: "已定稿"};
const fields = [["operating_cashflow", "经营现金流(万元)"], ["due_diligence_liabilities", "尽调负债(万元)"], ["financial_liabilities", "财报负债(万元)"], ["duplicate_asset_count", "重复设备数量"], ["collection_evidence", "回款依据"], ["collection_dependency", "是否依赖特定回款"]];
const categories = [["rule_omission", "检查遗漏"], ["false_positive", "误报"], ["judgment", "专业判断"], ["process", "流程建议"], ["expression", "表达修改"], ["factual", "事实纠正"], ["data", "数据纠正"]];
export default function XingbangWorkspace() {
const [items, setItems] = useState<any[]>([]), [project, setProject] = useState<any>(null), [identity, setIdentity] = useState<any>(null);
const [tab, setTab] = useState("overview"), [error, setError] = useState(""), [notice, setNotice] = useState(""), [busy, setBusy] = useState(false), [search, setSearch] = useState("");
const [requirement, setRequirement] = useState(""), [plan, setPlan] = useState("corporate.default"), [runId, setRunId] = useState(""), [document, setDocument] = useState<any>(null);
const [field, setField] = useState(fields[0][0]), [value, setValue] = useState(""), [reason, setReason] = useState(""), [amount, setAmount] = useState("4000");
const [template, setTemplate] = useState("template.due-diligence"), [reportId, setReportId] = useState(""), [sectionEdits, setSectionEdits] = useState<Record<string, string>>({});
const [category, setCategory] = useState("rule_omission"), [feedbackNote, setFeedbackNote] = useState(""), [feedbackId, setFeedbackId] = useState("");
const [experience, setExperience] = useState({title: "", trigger: "", action: "", exceptions: "", scope: "project", team: "", industry: "", required_field: "collection.evidence", exception_field: "collection.evidence", expires_at: "2027-09-11", condition_field: "collection.dependency", condition_value: "true"});
const selected = useRef("");
const headers = (json = true) => {const token = sessionStorage.getItem("xingbang-access-token"); return {...(json ? {"Content-Type": "application/json"} : {}), ...(token ? {Authorization: `Bearer ${token}`} : {})};};
async function request(path: string, body?: any) {
const response = await fetch(`/api/risk-agent/project-assistant${path}`, {method: body === undefined ? "GET" : "POST", headers: {...headers(), ...(/\/(runs|reports)$/.test(path) && body ? {"Idempotency-Key": crypto.randomUUID()} : {})}, body: body === undefined ? undefined : JSON.stringify(body)});
const data = await response.json();
if (!response.ok) throw Error(typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || "操作失败"));
return data;
}
async function open(id: string) {
selected.current = id;
const data = await request(`/projects/${encodeURIComponent(id)}`);
if (selected.current !== id) return;
setProject(data); history.replaceState({}, "", `?project=${encodeURIComponent(id)}`);
}
async function load() {try {const data = await request("/projects"); setItems(data.projects); setIdentity(data.identity); const id = new URLSearchParams(location.search).get("project"); if (id) await open(id);} catch (e: any) {setError(e.message);}}
async function act(path: string, body: any, message = "操作已记录") {
setBusy(true); setError(""); setNotice("");
try {const result = await request(path, body); if (selected.current) await open(selected.current); setNotice(message); return result;}
catch (e: any) {setError(e.message); return null;} finally {setBusy(false);}
}
async function download(path: string, name: string) {
try {const r = await fetch(`/api/risk-agent/project-assistant${path}`, {headers: headers(false)}); if (!r.ok) throw Error((await r.json()).detail || "下载失败"); const url = URL.createObjectURL(await r.blob()); const a = window.document.createElement("a"); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 10000);} catch (e: any) {setError(e.message);}
}
useEffect(() => {load();}, []);
const running = project?.runs?.some((r: any) => ["queued", "running"].includes(r.status));
useEffect(() => {if (!running || !project?.id) return; const id = project.id; const timer = setInterval(() => {if (selected.current === id) open(id).catch((e: any) => setError(e.message));}, 1500); return () => clearInterval(timer);}, [project?.id, running]);
const canConfigure = identity?.preview || ["admin", "reviewer"].includes(identity?.role);
const canReview = identity?.preview || identity?.role === "reviewer";
function back() {selected.current = ""; setProject(null); setDocument(null); setRunId(""); setReportId(""); setFeedbackId(""); setSectionEdits({}); history.replaceState({}, "", "/xingbang-workbench"); load();}
const alerts = <>{error && <div role="alert" className="ra-alert error">{error}</div>}{notice && <div role="status" className="ra-alert">{notice}</div>}</>;
if (!project) return <div className="ra-app"><AgentNav active="overview"/><main className="xb-overview"><div className="xb-overview-heading"><div><h1>兴邦金租 · 对公项目助手</h1><p>项目检查、补件复核、融资比较、底稿与经验复用</p></div><button className="ra-button" onClick={load}><RefreshCw size={15}/>刷新</button></div>{alerts}<div className="xb-actions"><input aria-label="搜索项目" placeholder="项目编号 / 客户名称" value={search} onChange={e => setSearch(e.target.value)}/>{canConfigure && <button className="ra-button" onClick={() => setTab(tab === "configuration" ? "overview" : "configuration")}>组件配置与发布</button>}</div>{tab === "configuration" && canConfigure ? <AssistantConfiguration request={request} canEdit={identity?.preview || identity?.role === "admin"}/> : <div className="xb-project-grid">{items.filter(x => (x.id + x.customer).toLowerCase().includes(search.toLowerCase())).map(x => <button className="xb-project-card" key={x.id} onClick={() => {setTab("overview"); open(x.id).catch(e => setError(e.message));}}><span>{x.data_domain === "demo" ? "演示项目" : "正式项目"}</span><h2>{x.customer}</h2><p>{x.id} · {x.industry} · {x.stage}</p><footer><b>{x.open_findings} 项待处理</b><small>{statuses[x.latest_run?.status] || "尚未检查"}</small><ArrowRight size={16}/></footer></button>)}</div>}</main></div>;
const base = `/projects/${encodeURIComponent(project.id)}`;
const chosenRun = project.runs.find((r: any) => r.id === runId) || project.runs[0];
const chosenReport = project.reports.find((r: any) => r.id === reportId) || project.reports[0];
const finished = project.runs.filter((r: any) => ["completed", "partial"].includes(r.status));
const currentRun = finished[0];
const reportSourceRun = finished.find((r: any) => r.id === runId) || currentRun;
const openFindings = project.findings.filter((f: any) => !["已解决", "不适用"].includes(f.status));
const evidence = project.runs.flatMap((r: any) => r.evidence || []);
async function upload(file: File) {
setBusy(true); setError("");
try {const form = new FormData(); form.append("file", file); const response = await fetch(`/api/risk-agent/project-assistant${base}/documents`, {method: "POST", headers: headers(false), body: form}); const data = await response.json(); if (!response.ok) throw Error(data.detail || "上传失败"); setDocument(data); await open(project.id); setNotice("材料已解析;请核对候选字段后确认,尚未自动修改项目事实。");} catch (e: any) {setError(e.message);} finally {setBusy(false);}
}
async function reportAction(action: string, payload: any = {}) {if (["regenerate", "finalize"].includes(action) && Object.keys(sectionEdits).length) {setError("请先保存尚未提交的章节修改"); return;} const saved = await act(`${base}/reports/${chosenReport.id}/actions`, {action, revision: chosenReport.revision, payload}); if (saved && payload.section_id) setSectionEdits(previous => {const next = {...previous}; delete next[payload.section_id]; return next;});}
return <div className="ra-app"><AgentNav active="overview" project={project.id}/><main className="xb-overview">
<button className="xb-back" onClick={back}>← 返回项目列表</button><div className="xb-overview-heading"><div><h1>{project.customer}</h1><p>{project.id} · {project.industry} · {project.stage} · {project.data_domain === "demo" ? "演示数据" : "正式数据"}</p></div><button className="ra-button" onClick={() => open(project.id)}><RefreshCw size={15}/>刷新</button></div>{alerts}
<section className="xb-task"><label className="ra-field"><span>本次检查要求(可选,不改变正式规则)</span><textarea value={requirement} onChange={e => setRequirement(e.target.value)} maxLength={2000} placeholder="例如:重点核对现金流与回款依据,说明哪些材料还需要补充"/></label><div className="xb-actions"><select aria-label="业务检查方案" value={plan} onChange={e => setPlan(e.target.value)}>{project.plans.map((p: any) => <option value={p.id} key={p.id}>{p.name} · {p.version}</option>)}</select><button className="ra-button primary" disabled={busy || running} onClick={() => act(`${base}/runs`, {requirement, plan_id: plan, revision: project.revision}, "检查已进入队列;刷新或离开页面不会丢失任务。 ")}><ShieldCheck size={16}/>{running ? "检查进行中" : "检查项目"}</button></div><small>必查项始终执行;语义核查需要已配置的内网模型,失败会明确标出。</small></section>
<nav className="xb-tabs">{tabs.map(t => <button key={t[0]} className={tab === t[0] ? "active" : ""} onClick={() => setTab(t[0])}>{t[1]}</button>)}{canConfigure && <button className={tab === "configuration" ? "active" : ""} onClick={() => setTab("configuration")}>配置治理</button>}</nav>
{tab === "overview" && <><section className="xb-cards"><article><b>当前数据版本</b><strong>v{project.data_revision}</strong><small>{project.documents.length} 份材料</small></article><article><b>未处理问题</b><strong>{openFindings.length}</strong><small>人工状态与机器结论分别留痕</small></article><article><b>最近运行</b><strong>{statuses[project.runs[0]?.status] || "未检查"}</strong><small>{project.runs[0]?.created_at ? new Date(project.runs[0].created_at).toLocaleString() : "点击检查项目"}</small></article></section>
<section className="xb-panel xb-wide"><h2>检查过程与历史</h2>{project.runs.length ? <><select aria-label="选择检查历史" value={chosenRun?.id || ""} onChange={e => setRunId(e.target.value)}>{project.runs.map((r: any) => <option key={r.id} value={r.id}>{new Date(r.created_at).toLocaleString()} · {statuses[r.status]} · {r.id.slice(-8)}</option>)}</select><p>本次要求:{chosenRun.requirement || "无额外要求"}</p><small>快照:{chosenRun.snapshot_id || "尚未完成取数"}</small>{["queued", "running"].includes(chosenRun.status) && <button className="ra-button" onClick={() => act(`${base}/runs/${chosenRun.id}/cancel`, {}, "已取消;迟到结果不会发布。")}>取消本次运行</button>}{chosenRun.error && <p role="alert">{chosenRun.error}</p>}{chosenRun.steps.map((s: any) => <details className="xb-step" key={s.step_id}><summary><span className="xb-status">{statuses[s.status]}</span> {s.name} · {s.skill.version}</summary><p>{s.errors?.map((e: any) => e.message).join(";")}</p>{s.outputs?.requirement_response && <p>{s.outputs.requirement_response}</p>}<pre>{JSON.stringify({outputs: s.outputs, checks: s.findings, input_hash: s.input_hash, trace: s.trace}, null, 2)}</pre></details>)}{chosenRun.data_errors?.map((e: any, i: number) => <p key={i} className="xb-warning">{e.card_id}:{e.message}</p>)}<details><summary>冻结组件版本与变更摘要</summary><pre>{JSON.stringify({versions: chosenRun.component_versions, changes: chosenRun.change_summary, superseded: chosenRun.superseded}, null, 2)}</pre></details></> : <p>尚无运行记录。</p>}</section>
<details className="xb-panel xb-wide"><summary>项目事件与兼容历史</summary><AssistantHistory key={project.id} projectId={project.id} records={project.legacy_records} request={request}/>{project.events.map((e: any) => <p key={e.seq}>{new Date(e.time).toLocaleString()} · {e.action} · {e.actor}</p>)}{project.migration_note && <p>{project.migration_note};已保留 {project.legacy_records.length} 条旧记录。</p>}<p><a href={`/review-workbench?project=${encodeURIComponent(project.id)}`}>原评审工作台</a> · <a href={`/report-workbench?project=${encodeURIComponent(project.id)}`}>原报告工作台</a> · <a href={`/collateral-workbench?project=${encodeURIComponent(project.id)}`}>租赁物核验</a></p></details></>}
{tab === "findings" && <><section className="xb-panel xb-wide"><h2>材料档案</h2><p>上传后先查看解析结果,确认候选值才更新项目。旧版本与来源保留;扫描件无法识别时会提示补充。</p><label className="ra-field"><span>上传 TXT / CSV / DOCX / XLSX / PDF / 图片(最多 10 MB)</span><input aria-label="上传项目材料" type="file" accept=".txt,.csv,.docx,.xlsx,.pdf,.png,.jpg,.jpeg" disabled={busy} onChange={e => {if (e.target.files?.[0]) upload(e.target.files[0]); e.target.value = "";}}/></label>{project.documents.map((d: any) => <div className="xb-finding" key={d.id}><div><b>{d.name}</b><p>版本 {d.version} · {d.status} · {d.size} 字节</p></div><button className="ra-button" onClick={async () => {try {setDocument(await request(`${base}/documents/${d.id}`));} catch (e: any) {setError(e.message);}}}>查看解析</button><button className="ra-button" onClick={() => download(`${base}/documents/${d.id}/download`, d.name)}>下载原件</button></div>)}
{document && <div className="xb-document"><h3>{document.name}</h3>{document.warnings.map((w: string, i: number) => <p className="xb-warning" key={i}>{w}</p>)}<label className="ra-field"><span>来源确认或补件说明</span><input value={reason} onChange={e => setReason(e.target.value)} placeholder="说明已核对的期间、口径和依据"/></label>{document.candidates.map((c: any) => <div className="xb-finding" key={c.id}><div><b>{c.field} = {String(c.value ?? "缺失")}</b><p>{c.locator} · {c.period || "期间未明"} · {c.unit || "单位未明"} · {c.scope || "口径未明"}</p></div><button className="ra-button" disabled={busy || !reason.trim()} onClick={() => act(`${base}/supplements`, {candidate_ids: [c.id], revision: project.revision, reason}, "候选值已确认;请重新检查项目。")}>确认该来源</button></div>)}<details><summary>解析原文与定位</summary>{document.chunks.map((c: any, i: number) => <p key={i}><b>{c.locator}</b> {c.text}{c.formula && <small> 公式:{c.formula};缓存:{c.cached_value || "无"}</small>}</p>)}</details></div>}</section>
<section className="xb-panel xb-wide"><h2>问题与补件</h2>{!project.findings.length && <p>尚无问题记录,请先执行检查。</p>}{project.findings.map((f: any) => <article className="xb-finding" key={f.id}><span className={`xb-status ${f.status === "已解决" ? "ok" : ""}`}>{f.status}</span><div className="xb-grow"><b>{f.title}</b><p>{f.suggested_action}</p><small>机器:{f.result} · {f.stale ? "来源待复核" : "已核对本次快照"} · {f.code}</small><details><summary>查看证据、依据与处理历史</summary>{f.evidence_ids.map((id: string) => {const e = evidence.find((r: any) => r.id === id); return <p key={id}>{e ? `${e.source} · ${e.locator} · ${e.field} = ${String(e.value ?? "缺失")}` : id}</p>;})}<p>依据:{f.basis_ids.join("、")}</p>{f.history?.map((h: any, i: number) => <p key={i}>{h.time} · {h.from} → {h.to} · {h.note || h.result}</p>)}</details><div className="xb-actions">{["待补件", "待复核", "不适用"].map(status => <button key={status} className="ra-button" disabled={busy || !reason.trim()} onClick={() => act(`${base}/findings/${f.id}/actions`, {action: status, revision: f.revision, note: reason})}>{status}</button>)}</div></div></article>)}<div className="xb-form-grid"><label className="ra-field"><span>人工补充字段</span><select value={field} onChange={e => setField(e.target.value)}>{fields.map(f => <option key={f[0]} value={f[0]}>{f[1]}</option>)}</select></label><label className="ra-field"><span>确认值(留空表示资料仍缺失)</span><input value={value} onChange={e => setValue(e.target.value)} placeholder={field === "collection_dependency" ? "true 或 false" : "输入实际核对值"}/></label><label className="ra-field"><span>补件 / 问题处理说明</span><input value={reason} onChange={e => setReason(e.target.value)} placeholder="必须填写确认依据"/></label></div><button className="ra-button primary" disabled={busy || !reason.trim()} onClick={() => act(`${base}/supplements`, {changes: {[field]: value === "" ? null : field === "collection_dependency" ? value === "true" ? true : value === "false" ? false : value : value}, finding_ids: [], revision: project.revision, reason}, "补件已保存,原来源已保留;请重新检查确认问题变化。")}>保存补件</button></section></>}
{tab === "scenario" && <section className="xb-panel xb-wide"><h2>融资金额情景比较</h2><p>当前金额 {project.facts.facility_amount} 万元,租赁物价值 {project.facts.lease_asset_value} 万元。比较不会修改基线;现金流等其他问题继续保留。120% 仅是演示阈值。</p><div className="xb-actions"><input aria-label="情景融资金额" value={amount} onChange={e => setAmount(e.target.value)}/><span>万元</span><button className="ra-button primary" disabled={busy} onClick={() => act(`${base}/scenarios`, {facility_amount: amount}, "情景已计算,项目基线未改变。")}>计算比较</button></div>{project.scenarios.map((s: any) => <article key={s.id} className="xb-scenario"><div className="xb-compare"><strong>{(Number(s.baseline.coverage) * 100).toFixed(2)}%</strong><ArrowRight/><strong>{(Number(s.scenario.coverage) * 100).toFixed(2)}%</strong><small>融资额 {s.baseline.facility_amount} → {s.scenario.facility_amount} 万元;另有 {s.unresolved.length} 项问题未消除</small></div><p>{s.assumptions.join(";")}</p><label className="ra-field"><span>采纳理由(可选,采纳将改变项目融资金额)</span><input value={reason} onChange={e => setReason(e.target.value)}/></label><button className="ra-button" disabled={busy || !reason.trim() || s.project_revision !== project.revision} onClick={() => act(`${base}/scenarios/${s.id}/adopt`, {action: "adopt", revision: project.revision, note: reason}, "情景已采纳;原版本保留,请重新检查。")}>{s.project_revision === project.revision ? "确认采纳该情景" : "基线已变化,请重新比较"}</button></article>)}</section>}
{tab === "report" && <section className="xb-panel xb-wide"><h2>报告底稿</h2><div className="xb-actions"><select aria-label="报告模板" value={template} onChange={e => setTemplate(e.target.value)}><option value="template.due-diligence">尽调草稿</option><option value="template.review">评审底稿</option><option value="template.committee">上会摘要</option></select><select aria-label="报告来源运行" value={reportSourceRun?.id || ""} onChange={e => setRunId(e.target.value)}>{finished.map((r: any) => <option key={r.id} value={r.id}>{new Date(r.created_at).toLocaleString()} · {statuses[r.status]}</option>)}</select><button className="ra-button primary" disabled={busy || !currentRun} onClick={async () => {const r = await act(`${base}/reports`, {run_id: reportSourceRun.id, template_id: template}); if (r) setReportId(r.id);}}><FileText size={15}/>生成底稿</button></div>{chosenReport && <div className="xb-report"><select aria-label="选择底稿版本" value={chosenReport.id} onChange={e => {setReportId(e.target.value); setSectionEdits({});}}>{project.reports.map((r: any) => <option key={r.id} value={r.id}>{r.template.name} · {statuses[r.status]} · {r.id.slice(-8)}</option>)}</select><p>{chosenReport.disclaimer} · {statuses[chosenReport.status]}</p><small>来源快照:{chosenReport.snapshot_id}</small><div className="xb-actions"><button className="ra-button" onClick={() => download(`${base}/reports/${chosenReport.id}/export`, `${project.id}-${chosenReport.template.name}.docx`)}>导出 Word</button><button className="ra-button" disabled={busy || !currentRun || chosenReport.status === "finalized"} onClick={() => reportAction("regenerate", {run_id: currentRun.id})}>按最新运行更新候选</button>{canReview && <button className="ra-button" disabled={busy || chosenReport.status === "finalized"} onClick={() => reportAction("finalize")}>确认定稿</button>}</div>{chosenReport.sections.map((s: any) => <article key={s.id}><h3>{s.title} {s.stale ? "· 来源已过期" : ""} {s.locked ? "· 已锁定" : ""} {s.confirmed ? "· 已确认" : ""}</h3><textarea aria-label={`编辑${s.title}`} className="xb-section-text" disabled={s.locked || ["overview", "findings"].includes(s.id) || chosenReport.status === "finalized"} value={sectionEdits[s.id] ?? s.content} onChange={e => setSectionEdits({...sectionEdits, [s.id]: e.target.value})}/>{chosenReport.status !== "finalized" && <div className="xb-actions"><button className="ra-button" disabled={busy || s.locked || ["overview", "findings"].includes(s.id)} onClick={() => reportAction("section", {section_id: s.id, content: sectionEdits[s.id] ?? s.content, confirmed: false})}>保存修改</button><button className="ra-button" disabled={busy || s.stale} onClick={() => reportAction("section", {section_id: s.id, content: sectionEdits[s.id] ?? s.content, confirmed: true, locked: true})}>确认并锁定</button>{s.locked && <button className="ra-button" disabled={busy} onClick={() => reportAction("section", {section_id: s.id, locked: false, confirmed: false})}>解除锁定</button>}</div>}{s.candidate && <details open><summary>更新候选(未覆盖当前正文)</summary><pre>{s.candidate.content}</pre><button className="ra-button" disabled={busy} onClick={() => reportAction("section", {section_id: s.id, accept_candidate: true})}>采纳候选内容</button></details>}</article>)}</div>}</section>}
{tab === "experience" && <section className="xb-panel xb-wide"><h2>专业反馈与经验复用</h2><p>先记录具体纠正,再形成经验草稿。经验需明确适用和例外条件,评测后审核发布;不会自动变成正式制度。</p><div className="xb-form-grid"><label className="ra-field"><span>反馈类别</span><select value={category} onChange={e => setCategory(e.target.value)}>{categories.map(c => <option key={c[0]} value={c[0]}>{c[1]}</option>)}</select></label><label className="ra-field"><span>反馈内容(关联最近一次已结束运行)</span><textarea value={feedbackNote} onChange={e => setFeedbackNote(e.target.value)}/></label></div><button className="ra-button" disabled={busy || !currentRun || !feedbackNote.trim()} onClick={async () => {const f = await act(`${base}/feedback`, {category, note: feedbackNote, target_kind: "run", target_id: currentRun.id}); if (f) setFeedbackId(f.id);}}>记录反馈</button>
<div className="xb-actions"><button className="ra-button" disabled={busy || running || !feedbackId} onClick={() => act(`${base}/runs`, {feedback_id: feedbackId, requirement: "根据选定专业反馈提炼经验草稿,不改变正式制度", plan_id: plan, revision: project.revision}, "经验提炼任务已排队;结果仅为本项目草稿,未配置模型时会明确失败。")}>从选定反馈提炼经验草稿</button></div><h3>创建经验草稿</h3><label className="ra-field"><span>来源反馈</span><select value={feedbackId} onChange={e => setFeedbackId(e.target.value)}><option value="">请选择反馈</option>{project.feedback.map((f: any) => <option key={f.id} value={f.id}>{f.note}</option>)}</select></label><div className="xb-form-grid">{[["title", "经验标题"], ["trigger", "适用条件说明"], ["action", "建议核查动作"], ["exceptions", "不适用 / 例外说明"]].map(([key, label]) => <label className="ra-field" key={key}><span>{label}</span><input value={(experience as any)[key]} onChange={e => setExperience({...experience, [key]: e.target.value})}/></label>)}<label className="ra-field"><span>触发条件</span><select value={experience.condition_field} onChange={e => setExperience({...experience, condition_field: e.target.value})}><option value="collection.dependency">依赖特定回款</option></select><select aria-label="触发条件值" value={experience.condition_value} onChange={e => setExperience({...experience, condition_value: e.target.value})}><option value="true">是</option><option value="false">否</option></select></label><label className="ra-field"><span>执行动作与例外</span><p>检查回款依据是否缺失;已有到账依据时不再触发。</p></label><label className="ra-field"><span>范围</span><select value={experience.scope} onChange={e => setExperience({...experience, scope: e.target.value})}><option value="project">仅本项目</option><option value="team">指定团队</option></select></label>{experience.scope === "team" && <label className="ra-field"><span>团队</span><input value={experience.team} placeholder="本机演示可填 demo" onChange={e => setExperience({...experience, team: e.target.value})}/></label>}<label className="ra-field"><span>有效期</span><input type="date" value={experience.expires_at} onChange={e => setExperience({...experience, expires_at: e.target.value})}/></label></div><button className="ra-button primary" disabled={busy || !feedbackId || !experience.title || !experience.trigger || !experience.action || !experience.exceptions} onClick={() => {const {condition_field, condition_value, ...body} = experience; act(`${base}/experiences`, {...body, feedback_id: feedbackId, conditions: {[condition_field]: condition_value === "true"}, expires_at: experience.expires_at + "T23:59:59+08:00"}, "经验草稿已保存,尚未生效。");}}>保存经验草稿</button>
{project.experiences.map((x: any) => <article className="xb-panel" key={x.id}><span className="xb-status">{statuses[x.status]}</span><h3>{x.title}</h3><p>适用:{x.trigger};例外:{x.exceptions}</p><p>{x.action} · {x.scope === "team" ? `团队 ${x.team}` : "仅本项目"} · 有效至 {x.expires_at}</p><div className="xb-actions">{x.status === "draft" && <button className="ra-button" disabled={busy} onClick={() => act(`${base}/experiences/${x.id}/actions`, {action: "evaluate", revision: x.revision, note: "隔离留出集评测"})}>评测适用与反例</button>}{canReview && x.status === "draft" && <button className="ra-button" disabled={busy || !x.evaluation?.passed} onClick={() => act(`${base}/experiences/${x.id}/actions`, {action: "publish", revision: x.revision, note: "确认脱敏条件与检查动作后发布"})}>审核发布</button>}{canReview && x.status === "published" && <button className="ra-button" disabled={busy} onClick={() => act(`${base}/experiences/${x.id}/actions`, {action: "rollback", revision: x.revision, note: "停用经验,后续运行不再应用"})}>停用回滚</button>}</div>{x.evaluation && <details><summary>评测结果:{x.evaluation.passed ? "通过" : "未通过"}</summary><pre>{JSON.stringify(x.evaluation, null, 2)}</pre></details>}</article>)}<h3>本项目实际应用记录</h3>{project.experience_applications.length ? project.experience_applications.map((a: any) => <p key={a.id}>{a.experience_version_id} · {a.status} · {a.check_code} · {a.reason}</p>) : <p>尚未应用已发布经验。</p>}</section>}
{tab === "configuration" && canConfigure && <AssistantConfiguration request={request} canEdit={identity?.preview || identity?.role === "admin"}/>}
</main></div>;
}