diff --git a/scripts/test_prescreen_panel.cjs b/scripts/test_prescreen_panel.cjs new file mode 100644 index 0000000..b0a60bc --- /dev/null +++ b/scripts/test_prescreen_panel.cjs @@ -0,0 +1,58 @@ +/** + * S1d 界面源码守卫。 + * + * 这是**源码断言**,不是浏览器端到端测试(docs/v2/06-validation.md §2 已明确 + * 现有 UI 脚本的性质)。它锁住的是可机械核对的约束:不新建路由、复用既有样式、 + * 上传必须带批次标识、冲突裁定必须留痕、不得展示任何审批概率。 + */ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); + +const root = path.resolve(__dirname, ".."); +const read = (file) => fs.readFileSync(path.join(root, file), "utf8"); + +const panel = read("src/components/xingbang/ImportBatchPanel.tsx"); +const workbench = read("src/app/xingbang-workbench/page.tsx"); +const lib = read("src/lib/prescreen.ts"); + +// 1. 挂载在既有工作台内,不新建路由(docs/v2/04-ui-spec.md §1:不恢复已合并的旧页面) +assert.match(workbench, /import ImportBatchPanel from "@\/components\/xingbang\/ImportBatchPanel"/, "工作台应导入导入面板"); +assert.match(workbench, / int: ("identity-recovery-ui", [executable("node"), "scripts/test_identity_recovery.cjs"], ROOT), ("cloud-assistant-access-ui", [executable("node"), "scripts/test_cloud_assistant_access.cjs"], ROOT), ("ui-readability", [executable("node"), "scripts/test_ui_readability.cjs"], ROOT), + ("prescreen-panel-ui", [executable("node"), "scripts/test_prescreen_panel.cjs"], ROOT), ("frontend-build", [npm, "run", "build"], ROOT), ("compose-local-config", [docker, "compose", "-f", "docker-compose.yml", "config", "--quiet"], ROOT), ("compose-aliyun-config", [docker, "compose", "--env-file", "deploy/aliyun.env.example", "-f", "deploy/docker-compose.aliyun.yml", "config", "--quiet"], ROOT), diff --git a/src/app/xingbang-workbench/page.tsx b/src/app/xingbang-workbench/page.tsx index 2c353da..2c857b3 100644 --- a/src/app/xingbang-workbench/page.tsx +++ b/src/app/xingbang-workbench/page.tsx @@ -4,6 +4,8 @@ import {ArrowRight, CheckCircle2, FileText, RefreshCw, ShieldCheck} from "lucide import AgentNav from "@/components/xingbang/AgentNav"; import AssistantHistory from "@/components/xingbang/AssistantHistory"; import ReviewerWorkspace from "@/components/xingbang/ReviewerWorkspace"; +import ImportBatchPanel from "@/components/xingbang/ImportBatchPanel"; +import {messageOf} from "@/lib/prescreen"; import {newRequestId} from "@/lib/request-id"; import "../data-access/workspace.css"; @@ -26,7 +28,7 @@ export default function XingbangWorkspace() { 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": newRequestId()} : {})}, 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 || "操作失败")); + if (!response.ok) throw Error(messageOf(data)); return data; } async function open(id: string) { @@ -90,7 +92,7 @@ export default function XingbangWorkspace() { {tab === "overview" && <>
当前数据版本v{project.data_revision}{project.documents.length} 份材料
最近运行{statuses[project.runs[0]?.status] || "未检查"}{project.runs[0]?.created_at ? new Date(project.runs[0].created_at).toLocaleString() : "点击检查项目"}

AI检查结果

先看业务结论;数据来源和解决建议可直接展开,技术信息放在最后。

{project.runs.length ? <>

本次要求:{chosenRun.requirement || "无额外要求"}

{["queued", "running"].includes(chosenRun.status) && }{chosenRun.error &&

{chosenRun.error}

}{chosenRun.steps.map((step:any)=>)}{chosenRun.data_errors?.map((e:any,i:number)=>

数据读取未完成:{e.message}

)}
技术追溯信息
{JSON.stringify({snapshot:chosenRun.snapshot_id,versions:chosenRun.component_versions,changes:chosenRun.change_summary},null,2)}
:

尚无运行记录。

}
业务历史与操作留痕{project.migration_note &&

{project.migration_note};已保留 {project.legacy_records.length} 条历史记录。

}
} - {tab === "findings" && <>

材料档案

上传后先查看解析结果,确认候选值才更新项目。旧版本与来源保留;扫描件无法识别时会提示补充。

{project.documents.map((d: any) =>
{d.name}

版本 {d.version} · {d.status} · {d.size} 字节

)} + {tab === "findings" && <> open(project.id)}/>

材料档案

上传后先查看解析结果,确认候选值才更新项目。旧版本与来源保留;扫描件无法识别时会提示补充。

{project.documents.map((d: any) =>
{d.name}

版本 {d.version} · {d.status} · {d.size} 字节

)} {document &&

{document.name}

{document.warnings.map((w: string, i: number) =>

{w}

)}{document.candidates.map((c: any) =>
{c.field} = {String(c.value ?? "缺失")}

{c.locator} · {c.period || "期间未明"} · {c.unit || "单位未明"} · {c.scope || "口径未明"}

)}
解析原文与定位{document.chunks.map((c: any, i: number) =>

{c.locator} {c.text}{c.formula && 公式:{c.formula};缓存:{c.cached_value || "无"}}

)}
}

{isReviewer?"全链路评审":"问题与解决"}

{isReviewer?"逐项查看数据来源、AI判断和客户经理说明,再选择评审结论。":"每个问题都说明影响和下一步;处理完成后重新AI预审。"}

{isReviewer&&}{!project.findings.length&&

尚无问题记录,请先执行检查。

}{[...project.findings].sort((a:any,b:any)=>Number(["已解决","不适用"].includes(a.status))-Number(["已解决","不适用"].includes(b.status))).map((finding:any)=>act(`${base}/findings/${finding.id}/actions`,{action:status,revision:finding.revision,note:reason})} onReviewer={(decision:string)=>reviewDecision(finding.id,decision)} decision={project.review_decisions?.find((item:any)=>item.finding_id===finding.id)}/>)}{!isReviewer&&<>
}{isReviewer&&
完成全链路核查AI只提供建议,批准或退回由评审员确认。
}
} {tab === "report" &&

{isReviewer ? "独立评审报告" : "AI预审报告"}

模板、运行快照、Skills 与规则版本随报告冻结;每个历史版本都可以重新打开。

{chosenReport &&

{chosenReport.disclaimer} · {statuses[chosenReport.status]}

来源快照:{chosenReport.snapshot_id}
{isManager && }{canReview && }
{chosenReport.sections.map((s: any) =>

{s.title} {s.stale ? "· 来源已过期" : ""} {s.locked ? "· 已锁定" : ""} {s.confirmed ? "· 已确认" : ""}