fix(v2): 修掉预审面板不刷新与候选行同 id 连开多行的缺陷
真实 Chrome 端到端验证第二篇(/prescreen-workbench、/experience-center)发现两个
源码守卫与生产构建都看不见的缺陷:
- 发起 AI 预审、运行结束后 PrescreenPanel 不重载:新运行进不了"查看哪一次运行",
面板继续显示上一次运行,而徽标写着"结果会自动刷新"。根因是 load() 只依赖
project.id;补修时又发现 worker 先写 run.status、再另写 prescreen_run 视图
(assistant_worker.process_next),两者不在同一事务,只读一次会读到视图还没
落库的中间态,因此改为按运行集合重载并在最新运行进入视图前短时重试。
- 候选区 key={card.id} 撞 React key,且「自然语言修改适用范围」「补充独立测试」
按 card.id 开合,点一行连开 4 行(同一 card.id 的草稿行与各版本行共享 id)。
改为行级 rowKey(组件id@版本@状态)。
同时新增可选脚本 scripts/browser-e2e-modules.cjs(42/42 通过),并给
test_prescreen_ui.cjs / test_experience_center_ui.cjs 补上对应源码断言。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,12 @@ const ORIGIN_ICONS: Record<string, any> = {
|
||||
const CARD_STATUS: Record<string, string> = {
|
||||
draft: "草稿", published: "已发布", rejected: "已拒绝", withdrawn: "已撤回",
|
||||
};
|
||||
/** 候选行的稳定标识。列表里**同一个 card.id 会出现多行**(草稿行与各版本行共享 id,
|
||||
* 见 knowledge_cards.list_cards),所以 React key 与"展开哪一行的面板"都不能只用 card.id:
|
||||
* 只用 id 会撞 key,点一行的「自然语言修改适用范围」还会连开同 id 的所有行——
|
||||
* 这两点都是真实浏览器里量到的(见 17 四批交付记录「浏览器验证发现的问题(第二篇)」)。
|
||||
* (id, version, status) 在注册表里唯一:同一 (id, version) 的草稿行与版本行 status 不同。 */
|
||||
const rowKey = (card: any) => `${card.id}@${card.version}@${card.status}`;
|
||||
// 组方案时的基座核查项:与种子方案一致,保证新方案能独立跑通(不是"只放卡片"的半成品方案)。
|
||||
const BASE_SKILLS = ["corporate.consistency", "corporate.cashflow", "corporate.assets", "corporate.coverage"];
|
||||
|
||||
@@ -180,7 +186,7 @@ export default function ExperienceCenter({project, identity, send}: any) {
|
||||
<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}>
|
||||
{candidates.map((card: any) => <div className="xb-finding" key={rowKey(card)}>
|
||||
<div>
|
||||
<b>{card.title}</b>
|
||||
<p>
|
||||
@@ -194,10 +200,10 @@ export default function ExperienceCenter({project, identity, send}: any) {
|
||||
<p><small>编辑人 {card.edited_by}{card.reviewer_id ? ` · 审核人 ${card.reviewer_id}` : ""}</small></p>
|
||||
|
||||
<div className="xb-actions">
|
||||
<button className="ra-button" disabled={busy} onClick={() => {setReviseFor(reviseFor === card.id ? "" : card.id); setReviseText(""); setReviseNote("");}}>
|
||||
<button className="ra-button" disabled={busy} onClick={() => {setReviseFor(reviseFor === rowKey(card) ? "" : rowKey(card)); setReviseText(""); setReviseNote("");}}>
|
||||
自然语言修改适用范围
|
||||
</button>
|
||||
<button className="ra-button" disabled={busy} onClick={() => {setCasesFor(casesFor === card.id ? "" : card.id); setCasesText("");}}>
|
||||
<button className="ra-button" disabled={busy} onClick={() => {setCasesFor(casesFor === rowKey(card) ? "" : rowKey(card)); setCasesText("");}}>
|
||||
补充独立测试样本
|
||||
</button>
|
||||
{canReview && <button className="ra-button" disabled={busy} onClick={() => transition(card, "evaluate")}>隔离评测</button>}
|
||||
@@ -212,7 +218,7 @@ export default function ExperienceCenter({project, identity, send}: any) {
|
||||
{canPublish && card.edited_by === identity?.id &&
|
||||
<p className="ra-note">本卡由你本人提议或编辑,因此不显示发布按钮:发布需要独立审核人(BR-42 / AC-43)。</p>}
|
||||
|
||||
{reviseFor === card.id && <div className="xb-task">
|
||||
{reviseFor === rowKey(card) && <div className="xb-task">
|
||||
<label className="ra-field"><span>用自然语言说明要改什么(必须真实调用模型解析)</span>
|
||||
<input value={reviseText} disabled={busy} placeholder="例如:只适用于设备类租赁,并增加关联交易例外"
|
||||
onChange={(e) => setReviseText(e.target.value)}/>
|
||||
@@ -223,7 +229,7 @@ export default function ExperienceCenter({project, identity, send}: any) {
|
||||
<button className="ra-button" disabled={busy || !reviseText.trim() || !reviseNote.trim()} onClick={() => revise(card)}>形成草稿修订</button>
|
||||
</div>}
|
||||
|
||||
{casesFor === card.id && <div className="xb-task">
|
||||
{casesFor === rowKey(card) && <div className="xb-task">
|
||||
<label className="ra-field"><span>独立测试样本(JSON 数组;自定义 Skill 的发布门禁要求)</span>
|
||||
<textarea className="ra-code" value={casesText} disabled={busy}
|
||||
placeholder='[{"name":"缺证样本","values":{"financial.operating_cashflow":null},"expected_outputs":{},"expected_checks":{"history.repayment.dependency":"unknown"}}]'
|
||||
|
||||
@@ -60,7 +60,31 @@ export default function PrescreenPanel({project, request, busy, setBusy, setErro
|
||||
setLoaded(true);
|
||||
}
|
||||
}
|
||||
useEffect(() => {load();}, [project.id]);
|
||||
// 宿主页在运行期间会轮询项目(见 prescreen-workbench 与 xingbang-workbench 的 running 轮询),
|
||||
// 但 `load()` 原先只依赖 project.id:**从本页发起的预审跑完后,面板仍停在旧运行上**,
|
||||
// 新运行也进不了"查看哪一次运行",而顶部徽标写着"结果会自动刷新"——真实浏览器验证发现
|
||||
// (见 17 四批交付记录「浏览器验证发现的问题(第二篇)」)。
|
||||
//
|
||||
// 只在运行集合变化时重载还不够:worker 是**先更新 run.status、再另写 prescreen_run 视图**
|
||||
// (assistant_worker.process_next 的 put(run) 与 save_prescreen_view),两者不在同一事务里,
|
||||
// 状态一变就读会读到"视图还没落库"的中间态,新运行照样漏掉。所以先探视图里有没有最新运行,
|
||||
// 没有就短时重试几次;次数用尽照常渲染已有结果,不做无限轮询。
|
||||
const newestRun = (project.runs || [])[0]?.id || "";
|
||||
const runKeys = (project.runs || []).map((item: any) => `${item.id}:${item.status}`).join(",");
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let rounds = 0;
|
||||
const read = async () => {
|
||||
const listed: any = await request(`${PRESCREEN}${projectPath}/runs`).catch(() => null);
|
||||
if (!live) return;
|
||||
const present = Boolean(listed && (listed.items || []).some((item: any) => item.id === newestRun));
|
||||
if (newestRun && !present && rounds < 5) {rounds += 1; timer = setTimeout(read, 700); return;}
|
||||
await load();
|
||||
};
|
||||
read();
|
||||
return () => {live = false; if (timer) clearTimeout(timer);};
|
||||
}, [project.id, runKeys]);
|
||||
|
||||
function selectRun(id: string) {
|
||||
setRunId(id);
|
||||
|
||||
Reference in New Issue
Block a user