账本前端补两样:答案身上的召回标记写到问数据的轮次和证据面板「口径」一步;对账结果卡上问「说明有用 / 看不懂」

复核 W4R-03(a×c):召回在原答案的运行上追加 ledger.recalled、ledger.basis_changed,以前只在运行时间线(decode)里,问数据的
轮次底部、证据面板「口径」一步、账本接口都不带——使用者被标了「依据有变」、或者口径升版后「已按 v2 核对,不影响」,重开对话
什么也看不到,得点开「过程」才翻得出来,而使用者进不了记录页。W4-a 已在账本接口加上 recall(ledger/recall.answer_marks:
checked / basis / tasks,几种 state 都带),这边照它画:

- typesLedger:RunLedger.recall(LedgerRecallMarks)
- run/LedgerStep 新增 RecallMarks(问数据的轮次和「口径」一步共用):「已按口径「X」vN 核对,不影响」(同一份口径核对过
  几次只写最近那一版,数变了的不在这里写);依据有变按两类各一行——数据目录(catalog / values / catalog_edit)和表结构
  (structure),认不出的种类只说「答案的依据有变」不猜,项数相加,按新目录新出现错误级问题的写几条;看的人手上有说的是
  这个答案的召回待办时写一句(口径升到 vN、数变了 / 依据有变检查出新问题)并链到 /apps/inbox/{待办}
- LedgerTurn:比对过的卡里写在摘要下面;没比对、不比对的答案(state 不是 matched)只要带着标记就单独写一张小卡
- LedgerPart(证据面板「口径」一步):有标签的写在脚注前;没标签但答案带着标记的,这一步只写标记

复核 W4R-10(多包):细案 02 做法 10 的 E2「说明有用 / 看不懂」以前没有入口。W4-a 已加 POST /ledger/answers/{run}/reconcile/
{对账运行}/feedback({verdict: useful | unclear},记成对账运行上的封存后事件、统计 E2 数它),账本接口的 reconciled 每一项
带 feedback {mine, useful, unclear}。这边:

- api.caliberLedger.feedback;typesLedger 加 LedgerFeedback、LedgerFeedbackOut
- run/LedgerReconcile 的对账卡:做完的卡问一句「「差在哪」的说明有帮助吗?」,两个按钮,看的人选过的按下(aria-pressed),
  写各几个人(服务端给的数),可以改主意;交回来的反馈先画上,账本再取一次让别处挂着的同一张卡跟着变;回收站里只看不反馈;
  没做完的卡不问
- 问数据的轮次里卡片上方没有「差在哪」,卡里补写一份(showWhere),反馈问的就是它;证据面板里它本来就在卡片上方,不重复

检查(写请求都在浏览器层拦下):
- check-evidence 新一段 ledger-recall:比对过的答案「口径」一步写核对过(只写最近那一版)、依据有变(项数相加、新问题条数)、
  链到待办;不比对的答案这一步只写依据有变
- check-chat 新一段 recall:轮次底部同上,另核对做完的对账卡写「差在哪」和反馈人数;不比对的答案单独一张小卡写表结构有变
- check-ledger 对账一段:没做完不问;点「看不懂」请求体只有 verdict、按下、写各几个人;改点「说明有用」再记一条、按下的换过来
- 共用夹具 ledger-fixtures 加 RECALL
把这几个文件的改动拿掉回跑:check-chat recall 6 项、check-evidence ledger-recall 4 项、check-ledger 对账反馈 2 项都不过;
放回去全过
This commit is contained in:
ThearchyHelios
2026-10-16 22:21:11 +08:00
parent f72e1d4a65
commit 1195c4abcb
10 changed files with 370 additions and 23 deletions
+57 -2
View File
@@ -33,14 +33,17 @@
// 17. 口径账本(next-round 02):交付了的轮次底部写「正在比对口径」,按退避再取到比对出结果、比完不再取;一行摘要
// (几个数来自哪份口径)、按口径正式出具和对账按钮照服务端给的动作摆;出具、对账的写请求在浏览器层拦下并核对请求体,
// 出具过的正式运行挂在轮次下面(档位、打开运行)。
// 18. 过程里被驳回的那一步(node.failed 带 reason=rejected):停止的剪影、中性色、行尾写「被驳回」,不画成完成的绿勾。
// 18. 过程里被驳回的那一步(node.failed 带 reason=rejected):停止的剪影、中性色、行尾写「被驳回」,不画成完成的绿勾;
// 19. 答案身上的召回标记(账本接口的 recall)写在轮次底部:「已按口径「X」vN 核对,不影响」、依据有变(目录 / 表结构,几项、
// 按新目录新出现几条错误级问题),看的人手上有说的是这个答案的召回待办时链到待办;没比对的答案照样写依据有变;做完的
// 对账卡上写「差在哪」、问说明有没有帮助(说明有用 / 看不懂)。
//
// 所有写请求都被拦在浏览器里,不碰任何库。
// 跑之前前端得起着(./scripts/dev.sh),默认连 5273。对别的实例(比如一份沙箱拷贝)跑时
// 带上地址:AGENTLAB_WEB=http://localhost:<前端端口> node scripts/check-chat.mjs
import { chromium } from '../frontend/node_modules/playwright-core/index.mjs'
import { mkdirSync, readFileSync } from 'node:fs'
import { ISSUE, RECONCILE, WHOLE, WHOLE_LABELS, ledgerOf } from './ledger-fixtures.mjs'
import { ISSUE, RECALL, RECONCILE, WHOLE, WHOLE_LABELS, ledgerOf, metricDetail, reconciledOf } from './ledger-fixtures.mjs'
const WEB = process.env.AGENTLAB_WEB ?? 'http://localhost:5273'
const CHROME = process.env.CHROME_PATH
@@ -1653,6 +1656,58 @@ for (const theme of THEMES) {
}
})
await section('recall', '口径账本:答案身上的召回标记写在轮次底部(已按 vN 核对,不影响;依据有变;有待办时链到待办)', async () => {
// 召回在原答案的运行上追加 ledger.recalled、ledger.basis_changed(封存后),账本接口的 recall 把它们交出来(02 做法 7)。
// 以前只在「过程」里点开才看得到,使用者又进不了记录页。没比对的答案(state 不是 matched)照样可能被标依据有变
ctl.ledgerConvs = true
try {
// 比对过的那份另带一次做完的对账(另有两个人说过说明有用):轮次里卡片上方没有「差在哪」,卡里写一份,反馈问的就是它
const reconciled = [{ ...reconciledOf('succeeded'), feedback: { mine: null, useful: 2, unclear: 0 } }]
for (const [name, LED] of [['matched', { ...ledgerOf('run-led', { labels: WHOLE_LABELS, whole: WHOLE, actions: [], reconciled }),
recall: RECALL }],
['ineligible', { run_id: 'run-led', state: 'ineligible', reason: '这种来源的运行不比对', note: '封存后比对,不在封存范围内',
recall: { checked: [], basis: [{ kind: 'structure', items: 1, new_errors: 0, at: iso(5) }], tasks: [] } }]]) {
const { page, ctx, errors } = await open(theme)
await page.route(/\/api\/artifacts\//, (route) => route.fulfill({ json: { id: EVIDENCE.doc_artifact,
content: { ...EVIDENCE.doc, run_id: 'run-led' } } }))
await page.route(/\/api\/runs\/run-led\/evidence(\/.*)?(\?.*)?$/, (route) => {
const m = new URL(route.request().url()).pathname.match(/\/segments\/([^/]+)$/)
return m ? route.fulfill({ json: EVIDENCE.segments[decodeURIComponent(m[1])] ?? {} }) : route.fulfill({ json: EVIDENCE.graph })
})
await page.route(/\/api\/runs\/run-led\/ledger$/, (route) => route.fulfill({ json: LED }))
await page.route(/\/api\/runs\/run-rec-1\/evidence\/metrics\/reconcile\/[^/]+$/, (route) =>
route.fulfill({ json: metricDetail('run-rec-1', new URL(route.request().url()).pathname.split('/').at(-1)) }))
await goto(page, 'c0ledgr')
const marks = page.locator('[data-ledger-turn] [data-ledger-recall]').first()
const shown = await marks.waitFor({ timeout: 8000 }).then(() => true, () => false)
const text = shown ? (await marks.innerText()).replace(/\s+/g, ' ') : ''
if (name === 'matched') {
check('轮次底部写「已按口径「周报口径」v3 核对,不影响」(核对过两次只写最近那一版)', shown
&& text.includes('已按口径「周报口径」v3 核对,不影响') && !text.includes('v2 核对'), text)
check('……依据有变:项数相加,写按新目录新出现几条错误级问题', text.includes('依据的数据目录有变(3 项)')
&& text.includes('按新目录检查新出现 1 条错误级问题'), text)
const link = marks.locator('[data-ledger-recall-task="tk-recall-1"] a')
check('……有说的是这个答案的召回待办:写数变了、链到待办', text.includes('口径升到 v2,这个答案用到的数变了')
&& await link.getAttribute('href').catch(() => null) === '/apps/inbox/tk-recall-1', text)
check('……和摘要在同一张账本卡里', await page.locator('[data-ledger-turn="matched"] [data-ledger-recall]').count() === 1)
const card = page.locator('[data-ledger-turn] [data-ledger-reconciled="run-rec-1"]')
const where = (await card.locator('[data-ledger-reconciled-where]').innerText().catch(() => '')).replace(/\s+/g, ' ')
check('轮次里做完的对账卡:写「差在哪」,下面问说明有没有帮助、写各几个人', where.includes('口径只保留状态为已支付的行')
&& (await card.locator('[data-ledger-feedback]').innerText().catch(() => '')).includes('2 人觉得有用')
&& await card.locator('[data-ledger-feedback-give]').count() === 2, where)
await page.screenshot({ path: `${SHOTS}/ledger-recall-${theme}.png` })
} else {
check('不比对的答案:轮次底部只写依据有变(表结构)', shown && text.includes('依据的表结构有变(1 项)')
&& await page.locator('[data-ledger-turn="recall"]').count() === 1 && await page.locator('[data-ledger-summary]').count() === 0, text)
}
check(`没有运行时报错(${name})`, errors.length === 0, errors[0] ?? '')
await ctx.close()
}
} finally {
ctl.ledgerConvs = false
}
})
await section('fixfirst', '要改的东西在工具库里:先去改参数定义,「接着跑」退成次要', async () => {
ctl.fixConvs = true
try {
+35 -1
View File
@@ -17,7 +17,7 @@
// EVIDENCE_SHOTS=<目录> 时亮暗两套各截几张,供人眼复核。
import { readFileSync } from 'node:fs'
import { chromium } from '../frontend/node_modules/playwright-core/index.mjs'
import { ledgerOf } from './ledger-fixtures.mjs'
import { RECALL, ledgerOf } from './ledger-fixtures.mjs'
const WEB = process.env.AGENTLAB_WEB ?? 'http://localhost:5273'
const CHROME = process.env.CHROME_PATH
@@ -4207,6 +4207,40 @@ await section('ledger', '口径账本:来自口径的数字挂小标记(受
await r.ctx.close()
})
await section('ledger-recall', '口径账本:答案身上的召回标记写在「口径」一步里(已按 vN 核对,不影响;依据有变;有待办时链到待办)', async () => {
// 召回在原答案的运行上追加 ledger.recalled、ledger.basis_changed(封存后),账本接口的 recall 把它们交出来(02 做法 7)。
// 以前它们只在运行时间线里,证据面板、问数据的轮次都看不见。没比对的答案(state 不是 matched)照样可能被标依据有变
for (const [name, LED] of [['matched', { ...ledgerOf(fx.run_id), recall: RECALL }],
['ineligible', { run_id: fx.run_id, state: 'ineligible', reason: '这种来源的运行不比对', note: '封存后比对,不在封存范围内',
recall: { checked: [], basis: RECALL.basis, tasks: [] } }]]) {
const r = await open('/ui-harness.html?evidence=1', { before: (page) => page.route(
(u) => new URL(u).pathname === `/api/runs/${fx.run_id}/ledger`, (route) => route.fulfill({ json: LED })) })
const p = r.page
const part = p.locator('[data-evidence-panel] [data-ev-ledger]')
await openSeg(p, 's6', '[data-ev-ledger]')
const marks = p.locator('[data-evidence-panel] [data-ledger-recall]')
await marks.waitFor({ timeout: 4000 }).catch(() => {})
const text = (await marks.innerText().catch(() => '')).replace(/\s+/g, ' ')
if (name === 'matched') {
check('比对过的答案:「口径」一步写「已按口径「周报口径」v3 核对,不影响」(核对过两次只写最近那一版)',
text.includes('已按口径「周报口径」v3 核对,不影响') && !text.includes('v2 核对') && await p.locator('[data-evidence-panel] [data-ledger-recall-checked]').count() === 1,
text)
check('……依据有变:目录那一类两次的项数相加,写按新目录新出现几条错误级问题', text.includes('依据的数据目录有变(3 项)')
&& text.includes('按新目录检查新出现 1 条错误级问题'), text)
const link = p.locator('[data-evidence-panel] [data-ledger-recall-task="tk-recall-1"] a')
check('……看的人手上有说的是这个答案的召回待办:写数变了、链到待办', text.includes('口径升到 v2,这个答案用到的数变了')
&& await link.getAttribute('href').catch(() => null) === '/apps/inbox/tk-recall-1', text)
check('……标记写在「口径」一步里,标签照旧', await part.getAttribute('data-ev-ledger').catch(() => null) === 'caliber')
if (SHOTS) await p.locator('[data-evidence-panel]').screenshot({ path: `${SHOTS}/ledger-recall.png` }).catch(() => {})
} else {
check('不比对的答案:没有标签,「口径」一步只写依据有变', await part.getAttribute('data-ev-ledger').catch(() => null) === 'recall'
&& text.includes('依据的数据目录有变(3 项)') && !text.includes('核对,不影响'), text)
}
check(`没有运行时报错(${name})`, r.errors.length === 0, r.errors.join(' | '))
await r.ctx.close()
}
})
check('没有运行时报错(组件预览)', harness.errors.length === 0, harness.errors.join(' | '))
await harness.ctx.close()
await browser.close()
+29 -2
View File
@@ -6,7 +6,8 @@
// 之后才追加);等满了还没有结论照实说、给「再取一次」,点了重新取、取到就画;三种情况(来自口径 / 相关口径 / 这次的
// 推理)各写各的;
// - 口径对账:核对参数(推断的参数要先核对)→ 发起(认不出列怎么配时照 409 带回的选项选对照的指标,再发起)→ 结果卡片:
// 这次 / 口径 / 差值 / 相对差四个数各自取值、点开是出处链;请求体逐项核对;
// 这次 / 口径 / 差值 / 相对差四个数各自取值、点开是出处链;请求体逐项核对;做完的卡片上问「差在哪」的说明有没有帮助
// (说明有用 / 看不懂,统计 E2):请求体只有 verdict,选过的按下、写各几个人,能改主意;没做完的不问;
// - 可沉淀的问题:工作流列表弹窗里的抽屉,三类来源分开写;「固化成口径草稿」走 from-run 的 freeze(带同组的别的运行)、
// 打开画布;聚类按人挑的那一轮固化;「不是口径」要写理由;
// - 使用者看到的入口照服务端的动作:不能出具的写原因(请搭建者出具)、不摆出具按钮,能对账的照样对账;出具过的正式运行
@@ -180,8 +181,15 @@ await section('「口径」一步的三种情况:来自口径、相关口径
// ---------------------------------------------------------------- 对账
await section('口径对账:核对参数 → 发起 → 选对照的指标 → 三个数各自点得开', async () => {
const state = { posts: [], reconciled: [], polls: 0 }
const state = { posts: [], reconciled: [], polls: 0, feedback: [] }
const extra = (method, p, body) => {
// 「说明有用 / 看不懂」:服务端交回这次对账现在的反馈(看的人最后一次给的、各几个人;另有一个人之前说过有用)
if (method === 'POST' && p === `/ledger/answers/${RUN}/reconcile/run-rec-1/feedback`) {
state.feedback.push(body)
const fb = { mine: body?.verdict, useful: 1 + (body?.verdict === 'useful' ? 1 : 0), unclear: body?.verdict === 'unclear' ? 1 : 0 }
state.reconciled = state.reconciled.map((r) => ({ ...r, feedback: fb }))
return { json: { run_id: 'run-rec-1', feedback: fb } }
}
if (method === 'POST' && p === `/ledger/answers/${RUN}/reconcile`) {
state.posts.push(body)
if (!body?.pairs) {
@@ -236,6 +244,7 @@ await section('口径对账:核对参数 → 发起 → 选对照的指标 →
const card = page.locator('[data-evidence-panel] [data-ledger-reconciled="run-rec-1"]')
const running = await card.waitFor({ timeout: 6000 }).then(() => true, () => false)
check('结果卡片:对账运行还在进行时写「对账进行中」', running && (await card.innerText()).includes('对账进行中'))
check('……还没做完:不问说明有没有用', await card.locator('[data-ledger-feedback-give]').count() === 0)
const done = await page.locator('[data-evidence-panel] [data-ledger-reconciled-state="succeeded"]').waitFor({ timeout: 12000 }).then(() => true, () => false)
check('……再取一次(运行还没结束时接着等),做完了画出每组对照', done)
await until(async () => (await card.innerText()).includes(RECONCILE_VALUES.diff_0), 5000)
@@ -244,6 +253,24 @@ await section('口径对账:核对参数 → 发起 → 选对照的指标 →
&& cardText.includes(`按口径 ${RECONCILE_VALUES.cal_0}`) && cardText.includes(`差值 ${RECONCILE_VALUES.diff_0}`)
&& cardText.includes(`相对差 ${RECONCILE_VALUES.rel_0}`), cardText)
check('……写参数从哪来、原答案里写的是什么', cardText.includes('按核对过的参数') && cardText.includes('原答案里写的是 8.7%'), cardText)
// 「差在哪」的说明有没有帮人看懂(02 做法 10 的 E2):做完的卡片上问一句,记成对账运行上的封存后事件
const fbRow = card.locator('[data-ledger-feedback]')
check('做完的卡片问「差在哪」的说明有没有帮助:说明有用 / 看不懂', (await fbRow.innerText().catch(() => '')).includes('说明有用')
&& await card.locator('[data-ledger-feedback-give]').count() === 2, await fbRow.innerText().catch(() => ''))
await card.locator('[data-ledger-feedback-give="unclear"]').click()
await until(() => state.feedback.length === 1)
check('点「看不懂」:请求体只有 verdict,记在这次答案的这次对账上', JSON.stringify(state.feedback[0]) === JSON.stringify({ verdict: 'unclear' }),
JSON.stringify(state.feedback[0]))
const pressed = await until(async () => await card.locator('[data-ledger-feedback-give="unclear"]').getAttribute('aria-pressed') === 'true', 4000)
const fbText = (await fbRow.innerText().catch(() => '')).replace(/\s+/g, ' ')
check('……选过的按下,写各几个人(服务端给的数)', pressed && fbText.includes('1 人觉得有用') && fbText.includes('1 人看不懂'), fbText)
await card.locator('[data-ledger-feedback-give="useful"]').click()
await until(() => state.feedback.length === 2)
const changed = await until(async () => await card.locator('[data-ledger-feedback-give="useful"]').getAttribute('aria-pressed') === 'true', 4000)
check('改主意:再点「说明有用」照样记一条,按下的换过来', changed && JSON.stringify(state.feedback[1]) === JSON.stringify({ verdict: 'useful' })
&& await card.locator('[data-ledger-feedback-give="unclear"]').getAttribute('aria-pressed') === 'false')
await card.locator('[data-ledger-value="diff_0"]').click()
const drawer = page.locator('[data-evidence-panel="side"]').last()
const opened = await until(async () => (await drawer.innerText().catch(() => '')).includes('差值'), 5000)
+19
View File
@@ -51,6 +51,25 @@ export function ledgerOf(runId, { whole, actions, issued = [], reconciled = [],
}
}
/**
* 答案身上的召回标记(服务端 ledger/recall.answer_marks):周报口径升到 v3 重算过、数没变(同一份口径核对过两次,只写最近
* 那一版);目录里影响数字的两项被驳回、按新目录检查新出现 1 条错误级问题,另有一次出现了新码值(同归「数据目录」那一类,
* 项数相加);看的这个人手上有一条说的是这个答案的 recall 待办(订单口径升到 v2,数变了)
*/
export const RECALL = {
checked: [
{ workflow_id: 'wf-weekly', workflow_name: '周报口径', from_version: 1, to_version: 2, recall_run: 'run-rc0', changed: false,
at: '2026-10-14T01:00:00+00:00' },
{ workflow_id: 'wf-weekly', workflow_name: '周报口径', from_version: 2, to_version: 3, recall_run: 'run-rc1', changed: false,
at: '2026-10-15T01:00:00+00:00' },
],
basis: [
{ kind: 'catalog', items: 2, new_errors: 1, at: '2026-10-15T02:00:00+00:00' },
{ kind: 'values', items: 1, new_errors: 0, at: '2026-10-15T03:00:00+00:00' },
],
tasks: [{ id: 'tk-recall-1', reason: 'caliber', to_version: 2 }],
}
/** 看差多少(s8 那条查询、订单口径):参数是推断的,要先核对 */
export const RECONCILE = {
kind: 'reconcile', query: 'q-orders', caliber: ORDERS.caliber, workflow_id: ORDERS.workflow_id, version: 1, node_id: 'fetch',