前端跟上口径账本内核:时间线认得 ledger.matched;问数据的运行带上属于哪段对话、哪一轮;「在画布里打开」带来源

为什么:这一波合入就进 main,后端新加的封存后事件、来源链要在界面上接得住,不能有订阅了收不到、点了没用的地方
(next-round 02 做法 2、9)。账本的标签、证据面板的「口径」一步、对账在第 4 波接界面,这一波只有接口。

- run/decode.ts:ledger.matched 解读成时间线上一行「比对已发布口径:1 条查询等价已证明,2 条有相关口径」,副标题写明「封存后比对,
  不在封存范围内」,事后补比、按需比的另写「按现在的口径回看」;有没比完的标成提醒;不改相位。文案进 lib/terms.ts 的 LEDGER_TEXT
  (连同 X10 的叫法:来自口径 / 相关口径 / 这次的推理 / 没有比对,等价已证明 / 助手选用,受管 / 已发布)。
- store/chat.ts:发起运行时带 origin_ref {conversation_id, turn_id}(等开轮请求拿到服务端的轮次 id);服务端核对不过
  (origin_ref_invalid:对话刚被删)时不带来源照常发起——来源只是留痕,不挡这一轮的回答。
- pages/ChatPage.tsx:「在画布里打开」带 origin {run_id, conversation_id, turn_id}(这一轮跑过才有);核对不过时不带来源照常新建。
  run/AssistantStream.tsx 的 onOpenGraph 多给一个轮次 id(只加参数,别处不受影响)。
- api/client.ts:runs.start 的 origin_ref、workflows.create 的 origin。
- 检查脚本:check-decode 加「口径账本」一节;check-chat 断言发起时带上 origin_ref、「在画布里打开」带上来源;check-report-export
  加「口径一步」:没比对过的运行没有这一步,数据块里放一份编的标签断网打开,面板最上面是「口径」、写明封存后比对、遮罩的参数写
  「已遮罩」(只打印这一节,不打印真数据)。

验证:tsc、check-copy;check-all --stacks 1 decode chat report-export 全过(1485 项)。
This commit is contained in:
ThearchyHelios
2026-10-16 14:15:22 +08:00
parent 5b4bd91476
commit 7576fa6ce9
10 changed files with 192 additions and 13 deletions
+7 -1
View File
@@ -628,7 +628,7 @@ async function fakeApi(route) {
: conversation.includes('步数用满') ? `run-steps-${++runSeq}`
: conversation.includes('照服务端的决定') ? `run-decide-${++runSeq}`
: `run-busy-${++runSeq}`
log.runStarts.push({ id, graph: b.graph })
log.runStarts.push({ id, graph: b.graph, origin_ref: b.origin_ref })
return json({ id, status: 'queued', run_class: 'exploratory', output: {}, usage: {} })
}
m = path.match(/^\/runs\/([^/]+)\/(cancel|continue)$/)
@@ -1455,6 +1455,10 @@ for (const theme of THEMES) {
const started = log.runStarts.slice(startsBefore).at(-1)
check('发起的图里 agent 步数是 24', started?.graph?.nodes?.find((n) => n.type === 'agent')?.data?.config?.max_steps === 24,
JSON.stringify(started?.graph?.nodes?.map((n) => n.data?.config)))
// 口径账本的来源链(next-round 02 做法 9):运行记下属于哪段对话、哪一轮(服务端核对后封存进运行来源)
const server = await page.evaluate(() => window.__chat.getState().byConversation.c0histf.at(-1).serverId)
check('发起时带上这一轮属于哪段对话、哪一轮', started?.origin_ref?.conversation_id === 'c0histf'
&& !!server && started?.origin_ref?.turn_id === server, JSON.stringify(started?.origin_ref))
check('上一次留档:第 1 次尝试 · 结论不可用', await shows(page, '第 1 次尝试'))
await page.screenshot({ path: `${SHOTS}/retried-${theme}.png` })
check('没有运行时报错', errors.length === 0, errors[0] ?? '')
@@ -2442,6 +2446,8 @@ for (const theme of THEMES) {
const asks = await page.evaluate(() => window.__asks)
check('说了放弃:建一个、跳过去,整条路只问这一遍', creates.length === 1 && asks === 2
&& new URL(page.url()).pathname === '/studio/wf-chat-open', `建了 ${creates.length} 个 · 一共问了 ${asks} 遍(取消那次算一遍)`)
check('建的工作流带上来源:哪次运行、哪段对话、哪一轮(服务端核对)', JSON.stringify(creates[0]?.origin)
=== JSON.stringify({ run_id: 'run-b1', conversation_id: 'c0idleb', turn_id: 'tb1' }), JSON.stringify(creates[0]?.origin))
check('没有运行时报错', errors.length === 0, errors[0] ?? '')
await ctx.close()
})
+25
View File
@@ -1823,6 +1823,31 @@ await section('结论句裁判(可点击证据第四期):裁判调用单
odd.find((s) => s.code === 'report_checked')?.title)
})
await section('口径账本:封存后比对已发布口径(ledger.matched)', async () => {
// next-round 02 做法 2:探索运行封存之后,服务端在后台比对已发布口径,追加一条 ledger.matched(只有各种状态几条)
const E = (seq, type, node_id, data = {}) => ({ seq, type, node_id, ts: 1790000000 + seq, data })
const run = (data) => flatten(mod.decodeRun([
E(1, 'run.started', null, {}), E(2, 'node.started', 'q', { node_type: 'tool' }), E(3, 'node.finished', 'q', {}),
E(4, 'run.finished', null, { status: 'succeeded' }),
E(5, 'ledger.matched', null, { artifact: 'cm-1', manifest_hash: 'h', algo: '1', post_seal: true, ...data }),
]))
const all = run({ counts: { proven: 1, composed: 0, related: 2, none: 0, skipped: 0 }, trigger: 'sealed' })
check('认得它:不是「未识别的事件」', !all.some((s) => s.title === '未识别的事件'))
const lm = all.find((s) => s.code === 'ledger_matched')
check('一行:几条等价已证明、几条有相关口径,写明封存后比对、不在封存范围内',
lm?.title === '比对已发布口径:1 条查询等价已证明,2 条有相关口径' && lm?.sub === '封存后比对,不在封存范围内'
&& lm?.level === 'info' && lm?.kind === 'note', `${lm?.title} / ${lm?.sub} / ${lm?.level}`)
const late = run({ counts: { proven: 0, related: 0, none: 1, skipped: 1 }, trigger: 'backfill' }).find((s) => s.code === 'ledger_matched')
check('事后补比的写明按现在的口径回看;没比完的是提醒', late?.title === '比对已发布口径:1 条没有相关的已发布口径,1 条没有比完'
&& late?.sub === '封存后比对,不在封存范围内 · 按现在的口径回看' && late?.level === 'warn', `${late?.title} / ${late?.sub} / ${late?.level}`)
const odd = run({ counts: 'x' }).find((s) => s.code === 'ledger_matched')
check('缺字段:只说比对过,不出 undefined、不瞎填数', odd?.title === '比对已发布口径' && !/undefined|NaN/.test(`${odd?.title}${odd?.sub}`),
odd?.title)
const phase = [E(4, 'run.finished', null, {}), E(5, 'ledger.matched', null, {})]
.reduce((s, ev) => mod.nextPhase(s, ev), { phase: 'running', awaiting: false })
check('不改相位:运行照旧是结束了', phase.phase === 'succeeded', phase.phase)
})
await section('术语', async () => {
// 没起名的节点退到类型名,类型名跟全站同一张表
const h = mod.decodeRun([{ seq: 1, type: 'node.started', node_id: 'h', ts: 1, data: { node_type: 'human', label: 'h' } }])
+68
View File
@@ -238,6 +238,74 @@ await section('断网打开', async () => {
}
})
// ---------------------------------------------------------------- 口径一步(口径账本,next-round 02 做法 3)
await section('口径一步', async () => {
if (skipped()) return
const data = dataOf(html)
// 没比对过的运行:数据块里没有 ledger,面板里也没有「口径」一步
if (!data.ledger) {
const plain = await openFile(writePage('report-plain.html', html))
try {
const cited = plain.page.locator('#report button[data-seg][data-state="deterministic"]').first()
if (await cited.count()) {
await cited.click()
await plain.page.locator('#detail').waitFor({ state: 'visible', timeout: 5000 })
check('没比对过的运行:面板里没有「口径」一步', await plain.page.locator('#detail [data-step="ledger"]').count() === 0)
}
} finally {
await plain.ctx.close()
}
}
// 服务端比对过的运行面板里是什么样:数据块里放一份口径账本给的标签(来自口径、相关口径各一个),断网打开点开看。
// 只改数据块(脚本的哈希不变,内容安全策略照样放行)
const keys = Object.keys(data.refs ?? {})
if (!keys.length) return note('报告里没有可点的片段,口径一步不查')
const labels = {
[keys[0]]: { kind: 'caliber', how: 'independent', caliber: 'wf-x@3', workflow_id: 'wf-x', version: 3, level: 'governed',
workflow_name: '周销售', level_text: '受管', column: 'gmv',
binding: [{ field: 'start', value: '2026-09-01' }, { field: 'phone', masked: true, value: '已遮罩' }] },
...(keys[1] ? { [keys[1]]: { kind: 'related', related: [{ caliber: 'wf-x@3', workflow_id: 'wf-x', version: 3,
workflow_name: '周销售', level_text: '受管', diff: { items: [{ aspect: 'filter', text: '口径只保留已支付的行,这次没有这个条件' }] } }] } } : {}),
}
const ledger = { labels, note: '封存后比对,不在封存范围内', retro: true, retro_note: '按现在的口径回看' }
const patched = html.replace(DATA_RE, (_, a, body, c) => a + JSON.stringify({ ...JSON.parse(body), ledger })
.replace(/</g, '\\u003c') + c)
const { ctx, page, errors } = await openFile(writePage('report-ledger.html', patched))
try {
const first = page.locator(`#report button[data-seg="${keys[0]}"]`).first()
if (!(await first.count())) return note('数据块里的片段在正文里找不到按钮,口径一步不查')
// 只打印「口径」这一节(数据块里放的是编的标签):面板别处是真数据,不打印
const ledgerText = async (seg) => {
await seg.click()
await page.locator('#detail').waitFor({ state: 'visible', timeout: 5000 })
const part = page.locator('#detail [data-step="ledger"]')
const text = (await part.count()) ? await part.innerText() : ''
const order = await page.evaluate(() => [...document.querySelectorAll('#detail-body > section')].map((s) => s.dataset.step ?? ''))
await page.keyboard.press('Escape')
return { text, order }
}
const { text, order } = await ledgerText(first)
check('面板最上面多「口径」一步:来自口径「周销售」v3(受管)、等价已证明', /口径/.test(text)
&& text.includes('来自口径「周销售」v3(受管)') && text.includes('等价已证明'), text.replace(/\n/g, ' '))
check('参数照给,遮罩的写「已遮罩」', text.includes('start = 「2026-09-01」') && text.includes('phone = 已遮罩'))
check('写明封存后比对、不在封存范围内,事后补比的写按现在的口径回看',
text.includes('封存后比对,不在封存范围内') && text.includes('按现在的口径回看'))
check('「口径」一步排在出处链前面', order[1] === 'ledger', order.slice(0, 2).join(','))
if (keys[1]) {
const second = page.locator(`#report button[data-seg="${keys[1]}"]`).first()
if (await second.count()) {
const other = (await ledgerText(second)).text
check('相关口径:写明是哪一份、差在哪', other.includes('相关口径') && other.includes('口径只保留已支付的行'),
other.replace(/\n/g, ' '))
}
}
check('页面没有报错', errors.length === 0, errors.slice(0, 2).join(' | '))
} finally {
await ctx.close()
}
})
// ---------------------------------------------------------------- 不执行脚本
await section('不执行脚本', async () => {