fix(v2): 浏览器端到端验证——修掉模块页在浏览器里渲染不出内容的缺陷
首次用真实 Chrome 打开 /trajectory-center(新增可选脚本 scripts/browser-e2e.cjs),
暴露 5 个源码守卫与生产构建都看不出的缺陷,全部修复:
1. 三个模块页读 `list.items`,而 /projects 返回的是 {projects, identity}——
项目列表恒为空,prescreen-workbench / experience-center / trajectory-center
整页渲染不出任何面板,只显示"当前身份没有可访问的项目"这种看着正常的空态。
(第二/三批引入)改为 list.projects。
2. TrajectoryPanel 没被传 identity,canRecord 恒假 → 会议结果录入表单永不出现。
3. ImprovementPanel 草稿下拉以组件 id 作 key/value,同一组件多版本草稿时撞 React key,
且选不准要发布哪一版;改以 `组件id@版本` 为键值。
4. JSX 文本里的 `**强调**` 会原样显示星号(JSX 不渲染 Markdown),共 9 处;
改为 <b>(字符串上下文处直接去掉标记)。
5. 深色导航模块名被 flex 压缩折行("AI 预审"断成两行);
加 flex:0 0 auto + white-space:nowrap。
另修一个既有测试脆弱点:test_trajectory.py 的更正用例断言"哪条排前面",
两条记录同刻度时排序翻转,曾让 verify.py 偶发失败;改为断言"两条都在、更正指向旧记录"。
回归防线:第 5 项补进 test_module_shell_ui.cjs 源码断言;
test_ui_readability.cjs 的导航字号断言改为不把 min-height 钉在规则首位。
验证:scripts/verify.py 14/14 PASS(backend-unit 572 项),
证据 test-evidence/20261001-030127-ff59d7e/;scripts/browser-e2e.cjs 19/19 PASS
(截图在 test-evidence/e2e-batch4/)。该 E2E 脚本刻意不接入 verify.py。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -128,7 +128,7 @@ export default function ExperienceCenter({project, identity, send}: any) {
|
||||
|
||||
<section className="xb-panel xb-wide">
|
||||
<h2>① 来源:历史过程时间线</h2>
|
||||
<p>时间线由项目材料**确定性**恢复,不调用模型、不编造经过。只有最终报告时会标"过程材料不完整",不生成虚构的问询顺序。</p>
|
||||
<p>时间线由项目材料<b>确定性</b>恢复,不调用模型、不编造经过。只有最终报告时会标"过程材料不完整",不生成虚构的问询顺序。</p>
|
||||
<div className="xb-actions">
|
||||
<button className="ra-button" disabled={busy} onClick={buildTimeline}><RefreshCw size={15}/>从项目材料重建时间线</button>
|
||||
{timeline && <span className="ra-badge">{timeline.status === "partial" ? "过程材料不完整" : "已恢复"} · {events.length} 个事件</span>}
|
||||
@@ -153,7 +153,7 @@ export default function ExperienceCenter({project, identity, send}: any) {
|
||||
|
||||
<section className="xb-panel xb-wide">
|
||||
<h2>② 提炼:用真实模型生成候选</h2>
|
||||
<p>提炼会**实际调用**已配置的模型。模型不可用时明确记为未完成(E-28),并且**不产生任何候选草稿**——这里不会用一个固定返回值冒充提炼成功。</p>
|
||||
<p>提炼会<b>实际调用</b>已配置的模型。模型不可用时明确记为未完成(E-28),并且<b>不产生任何候选草稿</b>——这里不会用一个固定返回值冒充提炼成功。</p>
|
||||
<label className="ra-field"><span>提炼要求(可选,只约束本次加工,不成为正式制度)</span>
|
||||
<input value={requirement} disabled={busy} placeholder="例如:重点整理现金流与利润不一致的核查方法"
|
||||
onChange={(e) => setRequirement(e.target.value)}/>
|
||||
@@ -177,7 +177,7 @@ export default function ExperienceCenter({project, identity, send}: any) {
|
||||
|
||||
<section className="xb-panel xb-wide">
|
||||
<h2>③ 候选:专家修订、评测与发布</h2>
|
||||
<p>修订只产生草稿,不直接生效;发布需要先通过隔离评测,并由**另一位**有发布资格的人操作。已发布版本不可覆盖,修订会升版本并保留父版本。</p>
|
||||
<p>修订只产生草稿,不直接生效;发布需要先通过隔离评测,并由<b>另一位</b>有发布资格的人操作。已发布版本不可覆盖,修订会升版本并保留父版本。</p>
|
||||
|
||||
{!candidates.length && <div className="ra-empty"><h2>尚无知识候选</h2><p>先提炼或人工新增一张候选卡片。</p></div>}
|
||||
{candidates.map((card: any) => <div className="xb-finding" key={card.id}>
|
||||
@@ -237,7 +237,7 @@ export default function ExperienceCenter({project, identity, send}: any) {
|
||||
|
||||
<section className="xb-panel xb-wide">
|
||||
<h2>④ 方案:让卡片真正影响后续核查</h2>
|
||||
<p>卡片只有在被选入**已发布方案**后才会影响运行;运行结果会冻结并记录卡片版本,核查项可追溯到 <code>卡片id@版本</code>。</p>
|
||||
<p>卡片只有在被选入<b>已发布方案</b>后才会影响运行;运行结果会冻结并记录卡片版本,核查项可追溯到 <code>卡片id@版本</code>。</p>
|
||||
{!plans.length && <p className="ra-note">还没有项目自建方案。在上方已发布卡片上点"选入新方案"即可装配。</p>}
|
||||
{plans.map((plan: any) => <div className="xb-finding" key={`${plan.id}@${plan.version}`}>
|
||||
<div>
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function ImportBatchPanel({project, request, base, headers, busy,
|
||||
const open = batches.find((batch) => batch.id === openId) || null;
|
||||
return <section className="xb-panel xb-wide">
|
||||
<h2>材料集合导入</h2>
|
||||
<p>一次登记一批文件,再逐个上传。登记只生成清单;**未上传或未确认的文件不会进入项目事实**。多来源数值不一致时会保留冲突并要求人工裁定,不会按上传先后覆盖。</p>
|
||||
<p>一次登记一批文件,再逐个上传。登记只生成清单;<b>未上传或未确认的文件不会进入项目事实</b>。多来源数值不一致时会保留冲突并要求人工裁定,不会按上传先后覆盖。</p>
|
||||
|
||||
<label className="ra-field"><span>选择文件集合(PDF / XLSX / CSV / TXT / DOCX / 图片;单文件 ≤ 10 MB,单批 ≤ 20 个文件且合计 ≤ 20 MB)</span>
|
||||
<input aria-label="选择材料集合" type="file" multiple disabled={busy} onChange={(e) => {pickFiles(e.target.files); e.target.value = "";}}/>
|
||||
|
||||
@@ -95,8 +95,11 @@ export default function ImprovementPanel({project, identity, send}: any) {
|
||||
catch (e: any) {setError(e.message); return null;} finally {setBusy(false);}
|
||||
}
|
||||
|
||||
const draftCards = cards.filter((card: any) => card.status === "draft");
|
||||
const proposedVersion = draftCards.find((card: any) => card.id === draft.target_ref);
|
||||
// 同一个组件可以同时存在多个版本的草稿(如 1.0.0 与 1.0.1),
|
||||
// 因此下拉必须以 `组件id@版本` 为键值——只用组件 id 会撞 key,也选不准要发布哪一版。
|
||||
const draftCards = cards.filter((card: any) => card.status === "draft")
|
||||
.map((card: any) => ({...card, ref: `${card.id}@${card.version}`}));
|
||||
const proposedVersion = draftCards.find((card: any) => card.ref === draft.target_ref);
|
||||
// 被改进的是"当前已发布的版本",拟发布的是那张草稿——两者是不同版本,不能混为一谈
|
||||
const published = proposedVersion && cards.find(
|
||||
(card: any) => card.status === "published" && card.id === proposedVersion.id);
|
||||
@@ -148,7 +151,7 @@ export default function ImprovementPanel({project, identity, send}: any) {
|
||||
return <section className="xb-panel xb-wide">
|
||||
<h2>改进对照与发布</h2>
|
||||
<p>从可核验的偏差出发:选来源运行或会议结果 → 提出候选修订 → 固定旧版做对照 → 独立审核 →
|
||||
**由服务端调用真实知识发布入口**。发布只影响后续新运行;旧运行的冻结版本不变,仍可回放。</p>
|
||||
<b>由服务端调用真实知识发布入口</b>。发布只影响后续新运行;旧运行的冻结版本不变,仍可回放。</p>
|
||||
{error && <div role="alert" className="ra-alert error">{error}</div>}
|
||||
{notice && <div role="status" className="ra-alert">{notice}</div>}
|
||||
{!loaded && <p>正在读取改进候选…</p>}
|
||||
@@ -186,7 +189,7 @@ export default function ImprovementPanel({project, identity, send}: any) {
|
||||
<select aria-label="要发布的草稿版本" value={draft.target_ref} disabled={busy}
|
||||
onChange={(e) => setDraft({...draft, target_ref: e.target.value})}>
|
||||
<option value="">选择一张草稿卡片</option>
|
||||
{draftCards.map((card: any) => <option key={card.id} value={card.id}>
|
||||
{draftCards.map((card: any) => <option key={card.ref} value={card.ref}>
|
||||
{card.title} · v{card.version}
|
||||
</option>)}
|
||||
</select></label>
|
||||
@@ -285,7 +288,7 @@ export default function ImprovementPanel({project, identity, send}: any) {
|
||||
})}
|
||||
|
||||
<p className="ra-note">
|
||||
发布由服务端调用真实知识发布入口,**版本号不由界面提交**;发布失败会保留旧版本,
|
||||
发布由服务端调用真实知识发布入口,<b>版本号不由界面提交</b>;发布失败会保留旧版本,
|
||||
候选也不会被标成已发布。这里没有"自动上线",发布后也不会自动发起运行。
|
||||
</p>
|
||||
</section>;
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function TrajectoryPanel({project, identity, send}: any) {
|
||||
const saved = await send("POST", `${PRESCREEN}${path}/review-outcomes`, body);
|
||||
setNotice(saved.status === "matched"
|
||||
? `已保存并关联材料版本 ${saved.reviewed_snapshot_id};计入分母前仍需口径映射。`
|
||||
: "已保存为**未匹配**:找不到对应材料版本,单列且不计入通过率分母。");
|
||||
: "已保存为未匹配:找不到对应材料版本,单列且不计入通过率分母。");
|
||||
setForm(EMPTY_FORM);
|
||||
await loadOutcomes();
|
||||
} catch (e: any) {setError(e.message);} finally {setBusy(false);}
|
||||
@@ -107,7 +107,7 @@ export default function TrajectoryPanel({project, identity, send}: any) {
|
||||
|
||||
return <section className="xb-panel xb-wide">
|
||||
<h2>运行轨迹回放</h2>
|
||||
<p>一次运行是一条轨迹,多次工具与模型调用是其中的步骤。这里只记录**可观察的动作与版本**,不包含模型内部推理;回放是只读的,不会改动这次运行。</p>
|
||||
<p>一次运行是一条轨迹,多次工具与模型调用是其中的步骤。这里只记录<b>可观察的动作与版本</b>,不包含模型内部推理;回放是只读的,不会改动这次运行。</p>
|
||||
|
||||
{!loaded && <p>正在读取轨迹…</p>}
|
||||
{loaded && !runs.length && <div className="ra-empty">
|
||||
@@ -161,8 +161,8 @@ export default function TrajectoryPanel({project, identity, send}: any) {
|
||||
</>}
|
||||
|
||||
<h3>会议结果对照</h3>
|
||||
<p>AI 轨迹与人工会议记录并列查看。匹配只基于可核验的项目、材料版本:找不到材料版本的结果**单列**,
|
||||
既不当否决、也不进已观察通过率的分母。更正只能**追加**并指向旧记录,历史不被覆盖。</p>
|
||||
<p>AI 轨迹与人工会议记录并列查看。匹配只基于可核验的项目、材料版本:找不到材料版本的结果<b>单列</b>,
|
||||
既不当否决、也不进已观察通过率的分母。更正只能<b>追加</b>并指向旧记录,历史不被覆盖。</p>
|
||||
|
||||
{notice && <div role="status" className="ra-alert">{notice}</div>}
|
||||
{!outcomes && <p>正在读取会议结果…</p>}
|
||||
@@ -235,7 +235,9 @@ function OutcomeRow({item}: any) {
|
||||
<b>{item.raw_code} · {item.status === "matched" ? "已关联材料版本" : "未匹配(单列)"}</b>
|
||||
<p>{item.normalized ? `归一化结果 ${item.normalized}` : "口径未映射,保留原码"}
|
||||
{item.label === true ? " · 计入通过" : item.label === false ? " · 计入未通过" : " · 不计入分母"}</p>
|
||||
<p><small><Clock size={12} aria-hidden="true"/> {new Date(item.occurred_at).toLocaleString()} · 记录人 {item.recorded_by}
|
||||
<p><small><Clock size={12} aria-hidden="true"/> 会议 {item.meeting_id}
|
||||
{item.reviewed_snapshot_id ? ` · 材料版本 ${item.reviewed_snapshot_id}` : ""}
|
||||
{" · "}{new Date(item.occurred_at).toLocaleString()} · 记录人 {item.recorded_by}
|
||||
{item.supersedes_id ? ` · 更正自 ${item.supersedes_id}` : ""}</small></p>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
|
||||
/* Business UI readability: keep navigation and identity obvious at a glance. */
|
||||
.xb-agent-nav{min-height:64px;padding:11px 24px;gap:10px;font-family:"Microsoft YaHei","PingFang SC",sans-serif;background:#102B27}
|
||||
.xb-agent-nav a{min-height:42px;padding:10px 13px;border-radius:9px;font-size:14px;font-weight:600;color:#DCE9E3;transition:background-color .16s,color .16s,box-shadow .16s}
|
||||
/* flex:0 0 auto + nowrap:模块名不得被压缩折行("AI 预审" 会断成两行)。
|
||||
容器仍保留换行,窄屏时整项换行而不是把标签撕开。 */
|
||||
.xb-agent-nav a{flex:0 0 auto;white-space:nowrap;min-height:42px;padding:10px 13px;border-radius:9px;font-size:14px;font-weight:600;color:#DCE9E3;transition:background-color .16s,color .16s,box-shadow .16s}
|
||||
.xb-agent-nav a.selected{background:#244D41;color:#FFFFFF;box-shadow:inset 0 0 0 1px #4b7d6b,0 3px 10px #00000033}
|
||||
.xb-brand-mark{width:36px;height:36px;font-size:17px}
|
||||
.xb-brand b{font-size:15px}.xb-brand small{font-size:11px}
|
||||
|
||||
Reference in New Issue
Block a user