mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
账本前端补两样:答案身上的召回标记写到问数据的轮次和证据面板「口径」一步;对账结果卡上问「说明有用 / 看不懂」
复核 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:
@@ -3,7 +3,8 @@
|
||||
*
|
||||
* - run:GET /api/runs/{id}/ledger,一次运行里每个数的口径标签、整题判断、能做的动作、出具过的正式运行、对过的账;
|
||||
* - issue:按口径正式出具(参数只由服务端从比对工件里取,这里只能选「按哪份口径」);
|
||||
* - reconcile:口径对账(点开才跑一次确定性派生的只读图);
|
||||
* - reconcile:口径对账(点开才跑一次确定性派生的只读图);feedback:对账结果上的「说明有用 / 看不懂」(记成对账运行上的
|
||||
* 封存后事件,统计 E2 数它);
|
||||
* - certifyQueue / dismiss:可沉淀的问题、「不是口径」(搭建者及以上);
|
||||
* - freeze:固化成口径草稿(/copilot/from-run 的 freeze 选项,报告引用的查询变成固定 SQL 的取数节点)。
|
||||
*
|
||||
@@ -11,7 +12,7 @@
|
||||
* 请求函数从 ./http 导入,不从 ./client 导入(client.ts 要导入这个文件,反过来就成了循环)。
|
||||
*/
|
||||
import type {
|
||||
CertifyQueue, FreezeOut, LedgerIssueOut, LedgerReconcileIn, LedgerReconcileOut, RunLedger,
|
||||
CertifyQueue, FreezeOut, LedgerFeedbackOut, LedgerFeedbackVerdict, LedgerIssueOut, LedgerReconcileIn, LedgerReconcileOut, RunLedger,
|
||||
} from '../typesLedger'
|
||||
import { get, post, type RequestOptions } from './http'
|
||||
|
||||
@@ -21,6 +22,8 @@ export const caliberLedger = {
|
||||
run: (runId: string, opts?: RequestOptions) => get<RunLedger>(`/runs/${encodeURIComponent(runId)}/ledger`, opts),
|
||||
issue: (runId: string, body: { workflow_id: string; version: number }) => post<LedgerIssueOut>(`${at(runId)}/issue`, body),
|
||||
reconcile: (runId: string, body: LedgerReconcileIn) => post<LedgerReconcileOut>(`${at(runId)}/reconcile`, body),
|
||||
feedback: (runId: string, reconcileId: string, verdict: LedgerFeedbackVerdict) =>
|
||||
post<LedgerFeedbackOut>(`${at(runId)}/reconcile/${encodeURIComponent(reconcileId)}/feedback`, { verdict }),
|
||||
certifyQueue: (opts?: RequestOptions) => get<CertifyQueue>('/ledger/certify-queue', opts),
|
||||
dismiss: (body: { item_id: string; kind?: string; note: string }) =>
|
||||
post<{ ok: boolean; item_id: string }>('/ledger/certify-queue/dismiss', body),
|
||||
|
||||
@@ -113,6 +113,34 @@ export const LEDGER_UI = {
|
||||
less: '收起',
|
||||
original: (text: string) => `原答案里写的是 ${text};对账时按同样的 SQL 重新取数,数据有变化时两者可能不同`,
|
||||
openRun: '打开对账运行',
|
||||
/** 「差在哪」的说明有没有帮人看懂(统计 E2 数它,衡量差异说明写得好不好) */
|
||||
feedback: {
|
||||
ask: '「差在哪」的说明有帮助吗?',
|
||||
useful: '说明有用',
|
||||
unclear: '看不懂',
|
||||
mine: (v: 'useful' | 'unclear') => `你选了「${v === 'useful' ? '说明有用' : '看不懂'}」,可以改`,
|
||||
counts: (useful: number, unclear: number) => [useful ? `${formatNumber(useful)} 人觉得有用` : '',
|
||||
unclear ? `${formatNumber(unclear)} 人看不懂` : ''].filter(Boolean).join(' · '),
|
||||
saved: '已记下你的反馈',
|
||||
readOnly: '对话在回收站中,恢复后才能反馈',
|
||||
},
|
||||
},
|
||||
|
||||
// ---- 答案身上的召回标记(问数据的轮次、证据面板「口径」一步) ----
|
||||
recall: {
|
||||
/** 口径升版后在同一份数据上重算过、数没变 */
|
||||
checked: (caliber: string) => `已按口径${caliber} 核对,不影响`,
|
||||
/** 依据有变:目录里影响数字的项被驳回或修改、出现新码值、处理变化时改了目录(目录类),表或列被删、类型变了(表结构) */
|
||||
basis: (what: 'catalog' | 'structure' | 'other', items: number) =>
|
||||
`${what === 'structure' ? '依据的表结构有变' : what === 'catalog' ? '依据的数据目录有变' : '答案的依据有变'}`
|
||||
+ `${items ? `(${formatNumber(items)} 项)` : ''}`,
|
||||
newErrors: (n: number) => `按新目录检查新出现 ${formatNumber(n)} 条错误级问题`,
|
||||
basisHint: '答案本身没有改:它依据的数据目录或表结构在答案之后有变化,请核对是否还能用',
|
||||
/** 看的这个人手上说的是这个答案的 recall 待办:口径升版后数变了的、依据有变检查出新问题的 */
|
||||
taskCaliber: (version: number | null | undefined) => `口径升到 ${version != null ? `v${version}` : '新版本'},这个答案用到的数变了`,
|
||||
taskBasis: '依据有变,按新目录检查出了新的问题',
|
||||
taskOther: '有一条召回待办',
|
||||
task: '去待办处理',
|
||||
},
|
||||
|
||||
// ---- 问数据:轮次底部的摘要 ----
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Scale } from 'lucide-react'
|
||||
import { ApiError, api } from '../api/client'
|
||||
@@ -9,8 +10,8 @@ import { LEDGER_UI } from '../lib/termsLedger'
|
||||
import { useAtLeast } from '../store/auth'
|
||||
import { useLedger } from '../store/ledger'
|
||||
import type {
|
||||
LedgerField, LedgerPairOptions, LedgerReconcileAction, LedgerReconcileIn, LedgerReconciled, LedgerReconcileGroup,
|
||||
LedgerRelated, RunLedger,
|
||||
LedgerFeedback, LedgerFeedbackVerdict, LedgerField, LedgerPairOptions, LedgerReconcileAction, LedgerReconcileIn, LedgerReconciled,
|
||||
LedgerReconcileGroup, LedgerRelated, RunLedger,
|
||||
} from '../typesLedger'
|
||||
import { BindingList, DiffList, caliberOf } from './LedgerStep'
|
||||
|
||||
@@ -26,6 +27,8 @@ import { BindingList, DiffList, caliberOf } from './LedgerStep'
|
||||
* - 结果(ReconcileResults):每一组对照写按你的问法、按口径、差值(另附相对差),每个数按
|
||||
* /runs/{对账运行}/evidence/metrics/{对账卡}/{指标} 取值,点一下打开它的出处链(apps/EvidenceDrawer,和应用页点数同一套)。
|
||||
* 哪几个指标是一组由服务端给(reconciled.groups),这里不按命名规则拼。
|
||||
* - 反馈(FeedbackRow):对账跑完之后问一句「差在哪」的说明有没有帮人看懂——「说明有用 / 看不懂」,记成对账运行上的封存后
|
||||
* 事件(服务端 ledger/feedback),统计 E2 数它。同一个人改主意只算最后一次;看的人选过哪个、各几个人都由服务端给。
|
||||
*/
|
||||
|
||||
const NEEDS_PAIRS = 'ledger_reconcile_needs_pairs'
|
||||
@@ -229,7 +232,55 @@ function GroupRow({ runId, card, group, onOpen }: {
|
||||
)
|
||||
}
|
||||
|
||||
function ReconcileCard({ item, data, original }: { item: LedgerReconciled; data?: RunLedger; original?: string }) {
|
||||
/**
|
||||
* 「差在哪」的说明有没有帮人看懂:说明有用 / 看不懂。按下的那个是看的人最后一次给的(服务端的 mine),可以改;回来的是这次
|
||||
* 对账现在的反馈,账本再取一次(别处挂着的同一张卡跟着变)
|
||||
*/
|
||||
function FeedbackRow({ runId, item, readOnly }: { runId: string; item: LedgerReconciled; readOnly: boolean }) {
|
||||
const refresh = useLedger((s) => s.refresh)
|
||||
const [fb, setFb] = useState<LedgerFeedback | undefined>(item.feedback)
|
||||
const [busy, setBusy] = useState<LedgerFeedbackVerdict | null>(null)
|
||||
useEffect(() => { setFb(item.feedback) }, [item.feedback])
|
||||
const F = LEDGER_UI.result.feedback
|
||||
const give = async (verdict: LedgerFeedbackVerdict) => {
|
||||
setBusy(verdict)
|
||||
try {
|
||||
const out = await api.caliberLedger.feedback(runId, item.run_id, verdict)
|
||||
setFb(out.feedback)
|
||||
toast.ok(F.saved)
|
||||
refresh(runId)
|
||||
} catch (e) {
|
||||
toast.error(e)
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
}
|
||||
const counts = fb ? F.counts(fb.useful, fb.unclear) : ''
|
||||
return (
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-1.5 text-2xs" data-ledger-feedback={fb?.mine ?? ''}>
|
||||
<span className="text-dim">{F.ask}</span>
|
||||
{(['useful', 'unclear'] as const).map((v) => (
|
||||
<button key={v} type="button" className={clsx('btn btn-xs', fb?.mine === v ? 'btn-primary' : 'btn-ghost')}
|
||||
aria-pressed={fb?.mine === v} disabled={readOnly || busy != null}
|
||||
title={readOnly ? F.readOnly : undefined} onClick={() => void give(v)} data-ledger-feedback-give={v}>
|
||||
{busy === v ? <Spinner size={11} /> : null}{F[v]}
|
||||
</button>
|
||||
))}
|
||||
{fb?.mine && <span className="text-faint" data-ledger-feedback-mine="">{F.mine(fb.mine)}</span>}
|
||||
{counts && <span className="text-faint" data-ledger-feedback-counts="">{counts}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 这张对账卡对照的那份相关口径「差在哪」(问数据的轮次里卡片上方没有这一段,反馈要有说的是什么) */
|
||||
function whereOf(data: RunLedger | undefined, item: LedgerReconciled): LedgerRelated | undefined {
|
||||
return (data?.queries ?? []).find((q) => q.artifact === item.query)?.candidates
|
||||
.find((c) => c.caliber === item.caliber && c.status === 'related')
|
||||
}
|
||||
|
||||
function ReconcileCard({ runId, item, data, original, readOnly, showWhere }: {
|
||||
runId: string; item: LedgerReconciled; data?: RunLedger; original?: string; readOnly: boolean; showWhere: boolean
|
||||
}) {
|
||||
const builder = useAtLeast('builder')
|
||||
const [all, setAll] = useState(false)
|
||||
const [target, setTarget] = useState<EvidenceTarget | null>(null)
|
||||
@@ -240,6 +291,7 @@ function ReconcileCard({ item, data, original }: { item: LedgerReconciled; data?
|
||||
const R = LEDGER_UI.result
|
||||
const state = item.status === 'succeeded' ? 'succeeded' : item.status === 'failed' ? 'failed'
|
||||
: item.status === 'cancelled' ? 'cancelled' : 'running'
|
||||
const where = showWhere ? whereOf(data, item) : undefined
|
||||
return (
|
||||
<div className="mt-1.5 rounded border px-2 py-1.5" data-ledger-reconciled={item.run_id} data-ledger-reconciled-state={state}>
|
||||
<div className="flex flex-wrap items-center gap-1.5 text-2xs">
|
||||
@@ -264,20 +316,33 @@ function ReconcileCard({ item, data, original }: { item: LedgerReconciled; data?
|
||||
{original && <p className="mt-1 text-2xs text-faint" data-ledger-original="">{R.original(original)}</p>}
|
||||
</>
|
||||
)}
|
||||
{state === 'succeeded' && showWhere && where && (
|
||||
<div className="mt-1 text-2xs" data-ledger-reconciled-where="">
|
||||
<div className="text-faint">{LEDGER_UI.step.where}</div>
|
||||
<DiffList related={where} />
|
||||
</div>
|
||||
)}
|
||||
{state === 'succeeded' && <FeedbackRow runId={runId} item={item} readOnly={readOnly} />}
|
||||
<EvidenceDrawer target={target} onClose={() => setTarget(null)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 对过的账(新的在前)。original:从证据面板打开时,正文里这个数写的是什么 */
|
||||
export function ReconcileResults({ items, data, original }: {
|
||||
runId: string; items: LedgerReconciled[]; data?: RunLedger; original?: string
|
||||
/**
|
||||
* 对过的账(新的在前)。runId 是答案的运行(反馈记在它名下的对账上);original:从证据面板打开时,正文里这个数写的是什么;
|
||||
* showWhere:卡片上方没有「差在哪」时(问数据的轮次)卡里写一份,反馈问的就是它;readOnly:对话在回收站里,只看不反馈
|
||||
*/
|
||||
export function ReconcileResults({ runId, items, data, original, showWhere = false, readOnly = false }: {
|
||||
runId: string; items: LedgerReconciled[]; data?: RunLedger; original?: string; showWhere?: boolean; readOnly?: boolean
|
||||
}) {
|
||||
const sorted = useMemo(() => items, [items])
|
||||
if (!sorted.length) return null
|
||||
return (
|
||||
<div data-ledger-reconciled-list="">
|
||||
{sorted.map((item) => <ReconcileCard key={item.run_id} item={item} data={data} original={original} />)}
|
||||
{sorted.map((item) => (
|
||||
<ReconcileCard key={item.run_id} runId={runId} item={item} data={data} original={original} readOnly={readOnly}
|
||||
showWhere={showWhere} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { BookMarked, RotateCw, Scale } from 'lucide-react'
|
||||
import { BookMarked, CircleCheck, Inbox, RotateCw, Scale, TriangleAlert } from 'lucide-react'
|
||||
import { ApiError, api } from '../api/client'
|
||||
import { Spinner, StatusBadge, toast } from '../components/ui'
|
||||
import { errorMessage } from '../lib/errors'
|
||||
@@ -9,7 +9,8 @@ import { LEDGER_UI, caliberName } from '../lib/termsLedger'
|
||||
import { useAtLeast } from '../store/auth'
|
||||
import { useLedger, useRunLedger, type LedgerSlot } from '../store/ledger'
|
||||
import type {
|
||||
LedgerBinding, LedgerIssueAction, LedgerIssued, LedgerLabel, LedgerReconcileAction, LedgerRelated, RunLedger, SegmentLedger,
|
||||
LedgerBinding, LedgerIssueAction, LedgerIssued, LedgerLabel, LedgerRecallMarks, LedgerReconcileAction, LedgerRelated, RunLedger,
|
||||
SegmentLedger,
|
||||
} from '../typesLedger'
|
||||
import { ReconcileDialog, ReconcileResults } from './LedgerReconcile'
|
||||
|
||||
@@ -19,7 +20,9 @@ import { ReconcileDialog, ReconcileResults } from './LedgerReconcile'
|
||||
* · 按口径正式出具;相关口径——差在哪 + 看差多少(口径对账:核对参数 → 发起 → 三个数各自点得开);这次的推理——
|
||||
* 写「没有相关的已发布口径」;没比对的写原因。比对还没出结果时写「正在比对口径」,store/ledger 按退避轮询,
|
||||
* 等满了还没结论照实说、给「再取一次」;
|
||||
* - 按口径正式出具那一项(IssueControl)和出具过的正式运行(IssuedList):问数据的轮次摘要用同一份。
|
||||
* - 按口径正式出具那一项(IssueControl)和出具过的正式运行(IssuedList):问数据的轮次摘要用同一份;
|
||||
* - 答案身上的召回标记(RecallMarks):「已按口径「X」vN 核对,不影响」、依据有变、说的是这个答案的召回待办(链到待办),
|
||||
* 「口径」一步和问数据的轮次底部用同一份。
|
||||
*
|
||||
* 界面层不放逻辑:标签、能不能出具、能不能自动、不能的原因、要不要先核对参数都是服务端给的(GET /runs/{id}/ledger),
|
||||
* 这里只照着画。比对在封存之后做,脚注写明「封存后比对,不在封存范围内」,事后补比的另写按哪种口径回看。
|
||||
@@ -93,6 +96,60 @@ export function LedgerWaiting({ slot, runId, compact = false }: { slot: LedgerSl
|
||||
)
|
||||
}
|
||||
|
||||
/** 依据有变的几种归成两类说:目录(目录里影响数字的项、新码值、处理变化时改了目录)和表结构;认不出的只说依据有变,不猜 */
|
||||
const BASIS_GROUP: Record<string, 'catalog' | 'structure'> = {
|
||||
catalog: 'catalog', values: 'catalog', catalog_edit: 'catalog', structure: 'structure',
|
||||
}
|
||||
|
||||
/** 有没有要画的召回标记 */
|
||||
export const hasRecallMarks = (recall: LedgerRecallMarks | undefined) =>
|
||||
!!(recall?.checked?.some((c) => !c.changed) || recall?.basis?.length || recall?.tasks?.length)
|
||||
|
||||
/**
|
||||
* 答案身上的召回标记(细案做法 7):「已按口径「X」v3 核对,不影响」、「依据的数据目录有变(2 项)」,看的这个人手上有说的
|
||||
* 是这个答案的 recall 待办时链到待办。标记是服务端从这次运行上封存后追加的 ledger.recalled、ledger.basis_changed 读出来的
|
||||
* (ledger/recall.answer_marks),这里只照着写;同一份口径核对过几次只写最近的那一版,依据有变按两类各写一行、项数相加
|
||||
*/
|
||||
export function RecallMarks({ recall }: { recall?: LedgerRecallMarks }) {
|
||||
if (!hasRecallMarks(recall)) return null
|
||||
const R = LEDGER_UI.recall
|
||||
const checked = new Map<string, NonNullable<LedgerRecallMarks['checked']>[number]>()
|
||||
for (const c of recall?.checked ?? []) if (!c.changed) checked.set(String(c.workflow_id ?? ''), c)
|
||||
const basis = new Map<'catalog' | 'structure' | 'other', { items: number; errors: number }>()
|
||||
for (const b of recall?.basis ?? []) {
|
||||
const group = BASIS_GROUP[String(b.kind ?? '')] ?? 'other'
|
||||
const cur = basis.get(group) ?? { items: 0, errors: 0 }
|
||||
basis.set(group, { items: cur.items + (b.items ?? 0), errors: cur.errors + (b.new_errors ?? 0) })
|
||||
}
|
||||
const tasks = recall?.tasks ?? []
|
||||
return (
|
||||
<ul className="space-y-0.5 text-2xs" data-ledger-recall="">
|
||||
{[...checked.values()].map((c) => (
|
||||
<li key={`c-${c.workflow_id ?? ''}`} className="flex items-start gap-1.5 text-dim" data-ledger-recall-checked={c.to_version ?? ''}>
|
||||
<CircleCheck size={11} className="mt-[2px] shrink-0 text-faint" aria-hidden />
|
||||
<span>{R.checked(caliberName(c.workflow_name, c.to_version))}</span>
|
||||
</li>
|
||||
))}
|
||||
{[...basis.entries()].map(([group, b]) => (
|
||||
<li key={`b-${group}`} className="flex items-start gap-1.5" data-ledger-recall-basis={group}>
|
||||
<TriangleAlert size={11} className="mt-[2px] shrink-0" style={{ color: 'var(--st-waiting)' }} aria-hidden />
|
||||
<span title={R.basisHint}>
|
||||
{R.basis(group, b.items)}
|
||||
{b.errors > 0 && <span className="text-dim"> · {R.newErrors(b.errors)}</span>}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
{tasks.map((t) => (
|
||||
<li key={`t-${t.id}`} className="flex flex-wrap items-start gap-1.5" data-ledger-recall-task={t.id}>
|
||||
<Inbox size={11} className="mt-[2px] shrink-0 text-faint" aria-hidden />
|
||||
<span>{t.reason === 'caliber' ? R.taskCaliber(t.to_version) : t.reason === 'catalog' ? R.taskBasis : R.taskOther}</span>
|
||||
<Link to={`/apps/inbox/${encodeURIComponent(t.id)}`} className="text-[var(--accent)] hover:underline">{R.task}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
/** 比对的来历:封存后比对,不在封存范围内;事后补比的写按哪种口径回看 */
|
||||
export function LedgerFootnote({ data }: { data: RunLedger }) {
|
||||
return (
|
||||
@@ -251,9 +308,14 @@ export function LedgerPart({ runId, runClass, report, segId, segText, segLedger,
|
||||
return (
|
||||
<Section title={LEDGER_UI.step.title} data-ev-ledger="unreadable">
|
||||
<p className="text-dim">{LEDGER_UI.step.unreadable(data.reason ?? '')}</p>
|
||||
<RecallMarks recall={data.recall} />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
// 没比对的答案照样可能被标依据有变:标记说的是整个答案,这一步只写它
|
||||
if (data && hasRecallMarks(data.recall)) {
|
||||
return <Section title={LEDGER_UI.step.title} data-ev-ledger="recall"><RecallMarks recall={data.recall} /></Section>
|
||||
}
|
||||
return null
|
||||
}
|
||||
const queries = label.queries ?? (label.query ? [label.query] : [])
|
||||
@@ -304,7 +366,7 @@ export function LedgerPart({ runId, runClass, report, segId, segText, segLedger,
|
||||
<Scale size={11} aria-hidden /> {LEDGER_UI.reconcile.button}
|
||||
</button>
|
||||
) : r.comparable === false ? <p className="mt-1 text-2xs text-faint">{r.reason || LEDGER_UI.step.notComparable}</p> : null}
|
||||
{!!done.length && <ReconcileResults runId={runId} items={done} data={data} original={segText} />}
|
||||
{!!done.length && <ReconcileResults runId={runId} items={done} data={data} original={segText} readOnly={readOnly} />}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -316,6 +378,7 @@ export function LedgerPart({ runId, runClass, report, segId, segText, segLedger,
|
||||
{label.kind === 'unknown' && (
|
||||
<p className="text-dim" data-ledger-unknown="">{LEDGER_TEXT.kind.unknown}{label.reason ? `:${label.reason}` : ''}</p>
|
||||
)}
|
||||
{hasRecallMarks(data?.recall) && <div className="mt-1.5"><RecallMarks recall={data?.recall} /></div>}
|
||||
{data && <LedgerFootnote data={data} />}
|
||||
{dialog && data && (
|
||||
<ReconcileDialog runId={runId} action={dialog.action} related={dialog.related} onClose={() => setDialog(null)} />
|
||||
|
||||
@@ -4,13 +4,17 @@ import { LEDGER_TEXT } from '../lib/terms'
|
||||
import { LEDGER_UI } from '../lib/termsLedger'
|
||||
import { useRunLedger } from '../store/ledger'
|
||||
import type { LedgerIssueAction, LedgerReconcileAction, LedgerRelated, RunLedger } from '../typesLedger'
|
||||
import { IssueControl, IssuedList, LedgerFootnote, LedgerWaiting, caliberOf, ledgerEnabled, ledgerErrorShown } from './LedgerStep'
|
||||
import {
|
||||
IssueControl, IssuedList, LedgerFootnote, LedgerWaiting, RecallMarks, caliberOf, hasRecallMarks, ledgerEnabled, ledgerErrorShown,
|
||||
} from './LedgerStep'
|
||||
import { ReconcileDialog, ReconcileResults } from './LedgerReconcile'
|
||||
|
||||
/**
|
||||
* 问数据轮次底部的口径账本(docs/design/next-round/02-口径账本.md 做法 11):一行摘要(「3 个数来自口径「周销售」v3,
|
||||
* 1 个数是这次的推理」)和对应的动作——整题等价时按口径正式出具(能自动的写正在自动出具),有相关口径时对账;
|
||||
* 正式出具的结果(自动的、一键的)挂在轮次下面、点开去运行;对过的账一张结果卡片,三个数各自点得开。
|
||||
* 正式出具的结果(自动的、一键的)挂在轮次下面、点开去运行;对过的账一张结果卡片,三个数各自点得开,卡里写「差在哪」、
|
||||
* 问说明有没有帮助(说明有用 / 看不懂)。答案身上的召回标记(「已按口径「X」vN 核对,不影响」、依据有变、说的是这个答案的
|
||||
* 召回待办)也写在这里:以前只在「过程」里点开才看得到,使用者又进不了记录页;没比对的答案照样可能带着,单独写一行。
|
||||
*
|
||||
* 比对在封存之后做:轮次刚交付时写「正在比对口径」,store/ledger 按退避再取,等满了还没结论照实说、给「再取一次」。
|
||||
* 不靠实时流(ledger.matched 追加时这次运行的事件总线已经关了)。界面层不放逻辑:每个数的标签、整题判断、能不能出具、
|
||||
@@ -62,12 +66,21 @@ export function LedgerTurn({ runId, runClass, done, readOnly = false }: {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!data || data.state !== 'matched' || !summary) return null
|
||||
if (!data) return null
|
||||
// 召回标记(依据有变、已按 vN 核对)说的是整个答案:没比对过、不比对的答案照样可能带着,单独写一行
|
||||
const marked = hasRecallMarks(data.recall)
|
||||
if (data.state !== 'matched' || !summary) {
|
||||
return marked ? (
|
||||
<div className="w-full basis-full rounded-md border px-2.5 py-2 text-xs" data-ledger-turn="recall">
|
||||
<RecallMarks recall={data.recall} />
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
const issue = (data.actions ?? []).find((a): a is LedgerIssueAction => a.kind === 'issue')
|
||||
const reconciles = reconcileEntries(data)
|
||||
const issued = data.issued ?? []
|
||||
const reconciled = data.reconciled ?? []
|
||||
if (!summary.total && !issued.length && !reconciled.length) return null
|
||||
if (!summary.total && !issued.length && !reconciled.length && !marked) return null
|
||||
const whole = data.whole
|
||||
return (
|
||||
<div className="w-full basis-full space-y-1.5 rounded-md border px-2.5 py-2 text-xs" data-ledger-turn="matched">
|
||||
@@ -83,6 +96,7 @@ export function LedgerTurn({ runId, runClass, done, readOnly = false }: {
|
||||
{LEDGER_UI.turn.whole(caliberOf(whole), LEDGER_TEXT.how[whole.how === 'composed' ? 'composed' : 'independent'])}
|
||||
</p>
|
||||
)}
|
||||
<RecallMarks recall={data.recall} />
|
||||
{(issue || reconciles.length > 0) && (
|
||||
<div className="flex flex-wrap items-start gap-1.5" data-ledger-actions="">
|
||||
{issue && <IssueControl runId={runId} action={issue} slot={slot} readOnly={readOnly} />}
|
||||
@@ -96,7 +110,7 @@ export function LedgerTurn({ runId, runClass, done, readOnly = false }: {
|
||||
</div>
|
||||
)}
|
||||
<IssuedList data={data} />
|
||||
{reconciled.length > 0 && <ReconcileResults runId={runId} items={reconciled} data={data} />}
|
||||
{reconciled.length > 0 && <ReconcileResults runId={runId} items={reconciled} data={data} showWhere readOnly={readOnly} />}
|
||||
<LedgerFootnote data={data} />
|
||||
{dialog && <ReconcileDialog runId={runId} action={dialog.action} related={dialog.related} onClose={() => setDialog(null)} />}
|
||||
</div>
|
||||
|
||||
@@ -182,6 +182,44 @@ export interface LedgerReconciled {
|
||||
card?: string
|
||||
groups?: LedgerReconcileGroup[]
|
||||
created_at?: string | null
|
||||
/** 「说明有用 / 看不懂」(服务端 ledger/feedback.feedback_of):看的这个人最后一次给的、各几个人 */
|
||||
feedback?: LedgerFeedback
|
||||
}
|
||||
|
||||
export type LedgerFeedbackVerdict = 'useful' | 'unclear'
|
||||
|
||||
/** 对账结果上的反馈:mine 是看的这个人最后一次给的(没给过为空),useful / unclear 是各几个人(每人只算最后一次) */
|
||||
export interface LedgerFeedback {
|
||||
mine?: LedgerFeedbackVerdict | null
|
||||
useful: number
|
||||
unclear: number
|
||||
}
|
||||
|
||||
/** POST /api/ledger/answers/{run}/reconcile/{对账运行}/feedback 的答复 */
|
||||
export interface LedgerFeedbackOut {
|
||||
run_id: string
|
||||
feedback: LedgerFeedback
|
||||
}
|
||||
|
||||
/**
|
||||
* 答案身上的召回标记(GET /runs/{id}/ledger 的 recall,服务端 ledger/recall.answer_marks;细案做法 7「依据有变」「已按 vN
|
||||
* 核对,不影响」)。几种 state 都可能带:没比对过、不比对的答案照样可能被标依据有变。不放数、表名、取值
|
||||
*/
|
||||
export interface LedgerRecallMarks {
|
||||
/** 口径升版后召回重算、数没变:在这个答案上记的「已按 vN 核对,不影响」 */
|
||||
checked?: {
|
||||
workflow_id?: string | null
|
||||
workflow_name?: string | null
|
||||
from_version?: number | null
|
||||
to_version?: number | null
|
||||
recall_run?: string | null
|
||||
changed?: boolean
|
||||
at?: string | null
|
||||
}[]
|
||||
/** 依据有变:kind 是 catalog(目录里影响数字的项被驳回或修改)/ structure(表结构)/ values(新码值)/ catalog_edit */
|
||||
basis?: { kind?: string | null; items?: number; new_errors?: number; at?: string | null }[]
|
||||
/** 看的这个人手上、说的是这个答案的 recall 待办(还没处理的) */
|
||||
tasks?: { id: string; reason?: string; to_version?: number | null }[]
|
||||
}
|
||||
|
||||
export interface LedgerCaliber {
|
||||
@@ -220,6 +258,7 @@ export interface RunLedger {
|
||||
actions?: LedgerAction[]
|
||||
issued?: LedgerIssued[]
|
||||
reconciled?: LedgerReconciled[]
|
||||
recall?: LedgerRecallMarks
|
||||
}
|
||||
|
||||
/** 证据片段接口(GET /runs/{id}/evidence/segments/{seg})的 ledger 键:这个片段的标签和比对的来历 */
|
||||
|
||||
+57
-2
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user