mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:31:58 +00:00
双盲复算前端(一):时间线解读双盲复算和反方审稿的全部日志代码,补类型和文案
- decode.ts:约定里的 12 个日志代码(dual_check_* 7 个、challenge_* 5 个)按 code 和结构化字段说成标题、副标题和下一步, 不从原话里认;后端发成 info 的(一致、裁决、记入数据目录、审稿完成)也上时间线,否则这两种节点在时间线上什么都不说; 有缺口、有推翻时用提醒色,完整对照和审稿记录的工件可下钻;老日志缺字段、类型不对时不崩、不瞎填数 - check-decode:每个代码一条断言,另有换一种取值和缺字段两组 - types.ts 末尾加双盲复算的审批 payload、对照工件、节点配置和设置里核查一组的类型 - terms.ts:DUAL_CHECK_TEXT 写全检查器、节点摘要、审批卡、时间线的文案(反方审稿日志的说法暂放 challengeLog); VERIFY_SETTING_TEXT 顶层和服务端 VERIFY_SETTING_LABEL 的键和叫法一字不差,其余放 section - 去掉双盲复算节点说明里的「功能开发中」和节点卡片上的占位摘要
This commit is contained in:
@@ -95,8 +95,6 @@ function summarize(type: NodeType, config: Record<string, any>): string {
|
||||
case 'subgraph':
|
||||
return config.workflow_id ? '嵌套工作流' : '未选择工作流'
|
||||
// 还是占位的两种:卡片上直接说尚未生效,免得以为放上去就会执行
|
||||
case 'dual_check':
|
||||
return DUAL_CHECK_TEXT.pending
|
||||
case 'challenge':
|
||||
return CHALLENGE_TEXT.pending
|
||||
default:
|
||||
|
||||
@@ -662,7 +662,7 @@ export const NODE_DEFS: Record<NodeType, NodeDef> = {
|
||||
// 说明里写明尚未生效,检查器顶上就是这一句;各自的工作包补上字段时把这句去掉
|
||||
dual_check: {
|
||||
type: 'dual_check', label: NODE_TYPE_LABEL.dual_check, category: '把关', icon: GitCompareArrows,
|
||||
description: `${DUAL_CHECK_TEXT.description}。${DUAL_CHECK_TEXT.pending}`,
|
||||
description: DUAL_CHECK_TEXT.description,
|
||||
hasTarget: true, sources: [{ id: 'out', label: '' }],
|
||||
fields: [],
|
||||
defaults: {},
|
||||
|
||||
+249
-4
@@ -3334,10 +3334,182 @@ export const WHATIF_TEXT = {
|
||||
empty: '这个指标的计算式没有引用任何输入,无可推演',
|
||||
}
|
||||
|
||||
/** 双盲复算节点:节点库说明、检查器和运行结果 */
|
||||
/**
|
||||
* 双盲复算:节点库说明、检查器、节点卡片摘要、两边对不上时的审批卡,以及时间线上双盲复算和反方审稿的日志。
|
||||
*
|
||||
* 两边的叫法固定为「原流程」「盲算」(服务端 explain_sql_diff 的 labels 也是这两个词),审核人的三个选择固定为
|
||||
* 「采用原流程的结果」「采用盲算的结果」「两边都不对」。选「采用盲算的结果」不会替换报告里的数,只记缺口,
|
||||
* 卡上要说清;探索运行不出具,出具相关的话只对正式运行说
|
||||
*/
|
||||
export const DUAL_CHECK_TEXT = {
|
||||
description: '另起一路独立计算,与原结果逐项比对',
|
||||
pending: `${PENDING_TEXT}:本节点暂未生效,运行时将跳过`,
|
||||
original: '原流程',
|
||||
blind: '盲算',
|
||||
|
||||
// ---- 检查器(nodeDefs 的 dual_check、canvas/DualCheckFields.tsx)
|
||||
field: {
|
||||
card: '要复算的口径卡',
|
||||
metrics: '复算的指标',
|
||||
definitions: '口径说明',
|
||||
provider: '盲算模型 · 接入',
|
||||
model: '盲算模型 · 模型',
|
||||
tolerance: '允许的相对误差',
|
||||
onReject: '审核人不采用原流程的结果时',
|
||||
enabled: '启用',
|
||||
},
|
||||
help: {
|
||||
card: '留空则复算上游的口径卡。只能复算本节点上游的口径卡:运行到本节点时,它的结果已经算出',
|
||||
metrics: '默认复算口径卡中输入来自查询结果的全部指标,取消勾选可以只复算其中一部分。输入不是来自查询的指标无法盲算',
|
||||
definitions: '给盲算模型看的业务定义,例如「销售额按支付时间统计,不含退款」。盲算模型看不到原流程的 SQL,'
|
||||
+ '只能依据指标名称、这里的说明和数据目录编写查询,说明越具体,比对越有意义',
|
||||
model: '接入和模型均未填写时,使用「设置 → 偏好设置 → 核查」中的核查模型;填写任一项后,整组以节点配置为准,'
|
||||
+ '不与设置合并。建议与原流程使用的模型不同,同一模型不易发现自身的错误',
|
||||
tolerance: '两边的值相差不超过这个比例时算一致,例如 0.001 表示千分之一,0 表示必须完全相同。留空则使用设置中的值',
|
||||
onReject: '盲算与原流程不一致时运行暂停,由审核人逐个指标选择采用哪一边。选择「采用盲算的结果」或「两边都不对」时按这里处理',
|
||||
enabled: '关闭时本节点不复算,直接放行,并在时间线上记录一条说明',
|
||||
},
|
||||
/** 口径卡下拉留空那一项:上游没有、只有一张、有好几张 */
|
||||
cardDefault: (name: string | null, upstream: number) => (upstream === 0 ? '默认:上游的口径卡(上游没有口径卡)'
|
||||
: upstream === 1 && name ? `默认:上游的口径卡「${name}」` : `默认:上游的口径卡(共 ${upstream} 张,建议指定)`),
|
||||
cardUpstream: '上游的口径卡',
|
||||
cardOther: '其他口径卡(需连接到本节点上游)',
|
||||
/** 画布上一张口径卡的叫法:节点名,口径名不同时附在后面 */
|
||||
cardOption: (label: string, caliber?: string | null) => (caliber && caliber !== label ? `${label}(${caliber})` : label),
|
||||
cardGone: (id: string) => `${id}(不在画布上)`,
|
||||
cardNotUpstream: '所选口径卡不在本节点上游,运行到本节点时还没有它的结果。请把它连接到本节点之前',
|
||||
cardMissing: (id: string) => `口径卡「${id}」不在画布上,请重新选择`,
|
||||
metricsAll: '全部指标',
|
||||
metricsDefault: '未指定:复算全部输入来自查询的指标',
|
||||
metricsPicked: (n: number, total: number) => `已选 ${n} / ${total} 个指标`,
|
||||
metricsNoCard: '先选择要复算的口径卡',
|
||||
metricsNone: '这张口径卡还没有定义指标',
|
||||
metricsPinned: '这张口径卡引用了其他工作流发布的口径,指标在运行时按引用的版本确定:将复算其中输入来自查询的全部指标',
|
||||
metricsStale: (ids: string[]) => `已选的指标不在这张口径卡中:${ids.join('、')}`,
|
||||
metricsClear: '清除已选',
|
||||
metricOption: (name: string | null | undefined, id: string) => (name && name !== id ? `${name}(${id})` : id),
|
||||
definitionPlaceholder: (name: string) => `「${name}」的口径说明`,
|
||||
definitionsNone: '选择要复算的指标后,可以为每个指标写一段口径说明',
|
||||
providerFollow: '跟随设置',
|
||||
providerFromModel: '按模型名找接入',
|
||||
providerGone: (name: string) => `${name}(不存在)`,
|
||||
providerOff: '(已停用)',
|
||||
modelFollow: '跟随设置',
|
||||
modelOfProvider: '留空则使用该接入的默认模型',
|
||||
/**
|
||||
* 节点上只填了盲算模型、接入留空时实际用哪个接入(verify_model_spec:哪一级写了就整组用哪一级,不跨级拼;
|
||||
* 再按模型名找接入)。found:按模型名找到的接入;fallback:认不出时去调的默认接入
|
||||
*/
|
||||
byModel: (model: string, found: { name: string; enabled: boolean } | null | undefined, fallback: string | null | undefined) => {
|
||||
if (found && !found.enabled) return `只填了模型:「${model}」属于已停用的接入「${found.name}」,盲算调用将失败。请启用该接入或选择其他接入`
|
||||
if (found) return `只填了模型:将按模型名使用接入「${found.name}」。接入和模型都未填写时,才跟随设置中的核查模型`
|
||||
return `只填了模型:所有接入的模型列表中都没有「${model}」,将使用默认接入${fallback ? `「${fallback}」` : ''}调用。`
|
||||
+ '如该模型不属于默认接入,请选择对应的接入'
|
||||
},
|
||||
tolerancePlaceholder: (v: number | null | undefined) => (v == null ? '跟随设置' : `跟随设置(${v})`),
|
||||
toleranceInvalid: '请填写不小于 0 的数',
|
||||
onReject: { degrade: '记为出具缺口,出具降档(默认)', fail: '判为失败,运行终止' } as Record<string, string>,
|
||||
enabled: { follow: '跟随设置', on: '开启', off: '关闭' },
|
||||
/** 「跟随设置」那一项附上设置里眼下是开是关(设置还没取回来就不附) */
|
||||
enabledFollow: (on: boolean | null | undefined) => (on == null ? '跟随设置' : `跟随设置(当前${on ? '开启' : '关闭'})`),
|
||||
|
||||
// ---- 节点卡片的摘要:复算哪张卡、几个指标
|
||||
summary: {
|
||||
card: (name: string, picked: number | null, total: number | null) => `复算「${name}」 · ${
|
||||
picked != null ? `${picked} 个指标` : total != null ? `全部 ${total} 个指标` : '全部指标'}`,
|
||||
noCard: '复算上游的口径卡 · 尚未连接口径卡',
|
||||
off: '已关闭',
|
||||
},
|
||||
|
||||
// ---- 审批卡(run/DualCheckApproval.tsx)
|
||||
approval: {
|
||||
intro: (n: number) => `盲算与原流程有 ${n} 个指标不一致,请逐个选择采用哪一边的结果`,
|
||||
model: (model: string) => `盲算模型:${model}`,
|
||||
sameModel: '盲算模型与原流程相同,复算缺乏独立性',
|
||||
/** delta_pct 已经乘过 100、带正负号写好(「+12.5%」) */
|
||||
delta: (text: string) => `相差 ${text}`,
|
||||
deltaZero: '原流程值为 0,无法计算相差比例',
|
||||
deltaHint: '相差比例 =(盲算值 − 原流程值)÷ |原流程值|',
|
||||
diffHead: '差异说明',
|
||||
fallbackOnly: '两条 SQL 的写法不同,未能归纳成具体差异',
|
||||
equivalent: '两条 SQL 的写法等价,差异出在取用的结果上,请对照两边取用的值',
|
||||
loading: '正在加载完整对照…',
|
||||
loadFailed: '无法加载完整对照,以下仅为摘要',
|
||||
compare: '对照两条 SQL',
|
||||
compareClose: '收起对照',
|
||||
sqlOriginal: '原流程的 SQL',
|
||||
sqlBlind: '盲算的 SQL',
|
||||
sqlMissing: '没有记录 SQL',
|
||||
source: (s: string) => `数据源 ${s}`,
|
||||
inputs: '输入取值',
|
||||
textDiff: '规范化后不同的行',
|
||||
rationale: '盲算的说明',
|
||||
catalog: '两边只差一个方面:选择任一边的结果后,这项口径会作为已确认的事实记入数据目录',
|
||||
choices: { a: '采用原流程的结果', b: '采用盲算的结果', neither: '两边都不对' } as Record<'a' | 'b' | 'neither', string>,
|
||||
choiceGroup: (name: string) => `「${name}」采用哪一边的结果`,
|
||||
/** 三个选择的后果,按运行类别说:探索运行不出具,不提出具;老服务端没给类别的照实说「正式运行」 */
|
||||
consequence: (runClass: string | null | undefined) => (runClass === 'formal'
|
||||
? '采用原流程的结果:沿用原流程的值,不影响出具。采用盲算的结果或两边都不对:报告中的数仍取自原流程,这个指标记为出具缺口,出具降档'
|
||||
: runClass === 'exploratory'
|
||||
? '采用原流程的结果:沿用原流程的值。采用盲算的结果或两边都不对:报告中的数仍取自原流程,这个指标记为缺口'
|
||||
: '采用原流程的结果:沿用原流程的值。采用盲算的结果或两边都不对:报告中的数仍取自原流程,这个指标记为缺口,正式运行的出具随之降档'),
|
||||
consequenceFail: '节点设为「判为失败」时,选择后两项会终止运行',
|
||||
note: '备注(可选)',
|
||||
notePlaceholder: '说明裁决的依据,会随裁决记入运行记录',
|
||||
submit: '提交裁决',
|
||||
pending: (n: number) => `还有 ${n} 个指标未选择`,
|
||||
done: '已提交裁决,运行继续',
|
||||
empty: '审批内容不完整,没有可以裁决的指标',
|
||||
},
|
||||
|
||||
// ---- 时间线(run/decode.ts explainLog):按日志的 code 和结构化字段说,不从原话里认
|
||||
/** 日志里的 limit 字段(达到了哪个上限) */
|
||||
limitLabel: {
|
||||
max_queries: '查询次数上限', timeout_s: '时长上限', max_cost_usd: '金额上限', daily_max_usd: '每日金额上限', max_claims: '句数上限',
|
||||
} as Record<string, string>,
|
||||
limitAny: '上限',
|
||||
limitDailyNext: '今日核查费用已达上限。请在「设置 → 偏好设置 → 核查」中调高每日金额上限,或等到次日再运行',
|
||||
log: {
|
||||
skippedDisabled: '双盲复算未启用,已跳过',
|
||||
skippedDisabledNext: '如需复算,请在节点的「启用」中选择「开启」,或在「设置 → 偏好设置 → 核查」中启用双盲复算',
|
||||
skippedNoInputs: '没有输入来自查询的指标,已跳过复算',
|
||||
skippedNoInputsNext: '双盲复算只复算输入来自查询结果的指标。请检查节点的「复算的指标」,或确认口径卡的输入取自查询',
|
||||
skipped: '双盲复算已跳过',
|
||||
sameModel: (model?: string) => (model ? `盲算模型与原流程同为「${model}」,复算缺乏独立性` : '盲算模型与原流程相同,复算缺乏独立性'),
|
||||
sameModelNext: '请在节点的「盲算模型」中指定其他模型,或在「设置 → 偏好设置 → 核查」中修改核查模型',
|
||||
agreed: (n?: number) => (n ? `双盲复算一致:${formatNumber(n)} 个指标与盲算结果相符` : '双盲复算一致:与盲算结果相符'),
|
||||
disagreed: (n?: number) => (n ? `双盲复算有 ${formatNumber(n)} 个指标与盲算结果不一致,等待审核人裁决`
|
||||
: '双盲复算有指标与盲算结果不一致,等待审核人裁决'),
|
||||
disagreedNext: '请在审批卡上对照两边的 SQL,逐个指标选择采用哪一边的结果',
|
||||
resolved: (parts: string[]) => (parts.length ? `审核人已裁决:${parts.join(',')}` : '审核人已裁决'),
|
||||
resolvedA: (n: number) => `采用原流程 ${formatNumber(n)} 个`,
|
||||
resolvedB: (n: number) => `采用盲算 ${formatNumber(n)} 个`,
|
||||
resolvedNeither: (n: number) => `两边都不对 ${formatNumber(n)} 个`,
|
||||
gaps: (n: number) => (n ? `${formatNumber(n)} 项记为缺口` : '没有缺口'),
|
||||
catalog: (n: number) => `已将 ${formatNumber(n)} 项裁决记入数据目录`,
|
||||
catalogNone: '裁决未记入数据目录',
|
||||
catalogSkipped: (n: number) => `${formatNumber(n)} 项未写入`,
|
||||
catalogNext: '可在「数据 → 数据目录」中查看或修改',
|
||||
limit: (label: string, metric?: string) => (metric ? `复算指标「${metric}」时已达${label},该指标未完成盲算` : `双盲复算已达${label}`),
|
||||
limitNext: '请在「设置 → 偏好设置 → 核查」中调高双盲复算的上限',
|
||||
},
|
||||
/** 反方审稿日志的时间线说法:时间线解读统一在这里做,反方审稿的工作包不另写一份 */
|
||||
challengeLog: {
|
||||
skippedDisabled: '反方审稿未启用,已跳过',
|
||||
skippedDisabledNext: '如需审稿,请在节点中开启,或在「设置 → 偏好设置 → 核查」中启用反方审稿',
|
||||
skippedNoReport: '没有找到要审的报告,已跳过反方审稿',
|
||||
skippedNoReportNext: '请在反方审稿节点的上游连接报告撰写节点,或在节点中指定要审的报告',
|
||||
skippedNoClaims: '报告中没有需要审的关键结论,已跳过反方审稿',
|
||||
skipped: '反方审稿已跳过',
|
||||
sameModel: (model?: string) => (model ? `反方审稿模型与报告撰写模型同为「${model}」,审稿缺乏独立性`
|
||||
: '反方审稿模型与报告撰写模型相同,审稿缺乏独立性'),
|
||||
sameModelNext: '请在节点中指定其他审稿模型,或在「设置 → 偏好设置 → 核查」中修改核查模型',
|
||||
done: (n?: number) => (n ? `反方审稿完成:审阅了 ${formatNumber(n)} 句结论` : '反方审稿完成'),
|
||||
verdict: { stands: '经受住', weakened: '被削弱', refuted: '被推翻', inconclusive: '无法判断' } as Record<string, string>,
|
||||
refuted: (n?: number) => (n ? `反方审稿推翻了 ${formatNumber(n)} 句结论` : '反方审稿推翻了报告中的结论'),
|
||||
refutedNext: '请在证据中对照反方审稿的查询和理由,核实这些结论',
|
||||
limit: (label: string, skipped?: number) => (skipped ? `反方审稿已达${label},${formatNumber(skipped)} 句结论未审` : `反方审稿已达${label}`),
|
||||
limitNext: '请在节点或「设置 → 偏好设置 → 核查」中调高反方审稿的上限',
|
||||
},
|
||||
}
|
||||
|
||||
/** 反方审稿节点:节点库说明、检查器和运行结果 */
|
||||
@@ -3346,5 +3518,78 @@ export const CHALLENGE_TEXT = {
|
||||
pending: `${PENDING_TEXT}:本节点暂未生效,运行时将跳过`,
|
||||
}
|
||||
|
||||
/** 设置页里复核套件(双盲复算、反方审稿等)的默认值一栏 */
|
||||
export const VERIFY_SETTING_TEXT = {}
|
||||
/**
|
||||
* 设置页「核查」一节(双盲复算、反方审稿、审批质询共用的核查模型、各自的开关和上限、每日金额上限)。
|
||||
*
|
||||
* 顶层是各项的叫法,和服务端 api/settings.py 的 VERIFY_SETTING_LABEL 一字不差(保存时 422 的报错写的就是这些,
|
||||
* 界面上的字段名得对得上),嵌套的项写成「组.项」;check-verify 从 settings.py 读出来逐一比对。
|
||||
* 这一节其余的标题、说明、今日花费放在 section 里
|
||||
*/
|
||||
export const VERIFY_SETTING_TEXT = {
|
||||
provider: '核查模型 · 接入',
|
||||
model: '核查模型 · 模型',
|
||||
daily_max_usd: '核查每日金额上限(美元)',
|
||||
dual_check: '双盲复算',
|
||||
'dual_check.enabled': '双盲复算 · 启用',
|
||||
'dual_check.tolerance': '双盲复算 · 允许的相对误差',
|
||||
'dual_check.max_queries': '双盲复算 · 每项最多查询次数',
|
||||
'dual_check.timeout_s': '双盲复算 · 每项时长上限(秒)',
|
||||
challenge: '反方审稿',
|
||||
'challenge.enabled': '反方审稿 · 启用',
|
||||
'challenge.max_claims': '反方审稿 · 每份报告最多审几句',
|
||||
'challenge.max_queries': '反方审稿 · 每句最多查询次数',
|
||||
'challenge.timeout_s': '反方审稿 · 每句时长上限(秒)',
|
||||
inquiry: '审批质询',
|
||||
'inquiry.enabled': '审批质询 · 启用',
|
||||
'inquiry.max_queries': '审批质询 · 每次最多查询次数',
|
||||
'inquiry.timeout_s': '审批质询 · 每次时长上限(秒)',
|
||||
section: {
|
||||
title: '核查',
|
||||
hint: '双盲复算、反方审稿和审批质询都由只读的 Agent 查询数据来核对:三项共用核查模型和每日金额上限,'
|
||||
+ '各自可以单独开关、设置上限。节点中已设置的项以节点为准。',
|
||||
model: '核查模型',
|
||||
/** 接入和模型都没填:服务端沿用证据裁判模型的取值链(verify_model_spec)。醒目地说一句,并给出建议 */
|
||||
notSet: '未单独配置,将使用证据裁判的模型',
|
||||
notSetAdvice: '建议配置一个与撰写报告、查询数据的模型都不同的模型:同一模型核对自身的输出时,不易发现其中的错误',
|
||||
differ: '建议与撰写报告、查询数据的模型不同:同一模型不易发现自身的错误',
|
||||
providerDefault: '跟随证据裁判模型',
|
||||
providerFromModel: '按模型名找接入',
|
||||
providerGone: (name: string) => `模型接入「${name}」不存在:核查调用将失败`,
|
||||
providerGoneOption: (name: string) => `${name}(不存在)`,
|
||||
providerOff: '(已停用)',
|
||||
modelDefault: '留空:用接入的默认模型',
|
||||
modelFollow: '留空:跟随证据裁判模型',
|
||||
modelOf: (model: string) => `留空则使用 ${model}`,
|
||||
/** 只填了核查模型、接入留空时实际用哪个接入(同裁判的说法,只是上一级是证据裁判模型) */
|
||||
byModel: (model: string, found: { name: string; enabled: boolean } | null | undefined, fallback: string | null | undefined) => {
|
||||
if (found && !found.enabled) return `只填了模型:「${model}」属于已停用的接入「${found.name}」,核查调用将失败。请启用该接入或选择其他接入`
|
||||
if (found) return `只填了模型:将按模型名使用接入「${found.name}」。「跟随证据裁判模型」仅在接入和模型都留空时生效`
|
||||
return `只填了模型:所有接入的模型列表中都没有「${model}」,将使用默认接入${fallback ? `「${fallback}」` : ''}调用。`
|
||||
+ '如该模型不属于默认接入,请选择对应的接入。「跟随证据裁判模型」仅在接入和模型都留空时生效'
|
||||
},
|
||||
parts: {
|
||||
dual_check: '另起一路,由盲算模型在看不到原流程 SQL 的情况下独立查询,再与原流程的结果逐项比对',
|
||||
challenge: '从报告中挑出关键结论,由反方模型查询数据尝试推翻',
|
||||
inquiry: '审批人在批准前就报告提问,由核查模型查询数据作答,问答随运行一并封存',
|
||||
} as Record<string, string>,
|
||||
toleranceHint: '两边的值相差不超过这个比例时算一致,例如 0.000001 表示百万分之一,0 表示必须完全相同',
|
||||
unlimited: '不限',
|
||||
/** 某项设成不限时写明它还受什么约束,不要悄悄生效。by:眼下还设着上限的相关项 */
|
||||
unlimitedNote: (kind: 'queries' | 'claims' | 'timeout' | 'daily', by: string[]) => {
|
||||
if (kind === 'timeout') return '不设上限,时长仅受模型接口自身的超时约束'
|
||||
const what = kind === 'queries' ? '查询次数' : kind === 'claims' ? '句数' : '每日核查费用'
|
||||
return by.length ? `不设上限,${what}仅受${by.join('、')}约束` : `不设上限,${what}不受其他上限约束`
|
||||
},
|
||||
bound: { timeout: '时长上限', daily: '每日金额上限', claims: '报告中关键结论的数量', queries: '各项的查询次数上限', timeouts: '各项的时长上限' },
|
||||
invalid: {
|
||||
count: '请填写正整数,或勾选「不限」',
|
||||
seconds: '请填写大于 0 的秒数,或勾选「不限」',
|
||||
usd: '请填写大于 0 的金额,或勾选「不限」',
|
||||
tolerance: '请填写不小于 0 的数',
|
||||
},
|
||||
spend: (usd: string, calls: number) => `今日已花费 $${usd}(${formatNumber(calls)} 次调用)`,
|
||||
spendNone: '今日尚无核查调用',
|
||||
unpriced: (n: number) => `另有 ${formatNumber(n)} 次调用无法估算金额(模型不在价格目录中),未计入`,
|
||||
saveInvalid: '核查设置中有无效值,请修正后再保存',
|
||||
},
|
||||
}
|
||||
|
||||
+148
-2
@@ -3,6 +3,7 @@ import type { NodeState, RunPhase } from './trace'
|
||||
import { formatDuration, formatNumber } from '../lib/format'
|
||||
import {
|
||||
ABSENCE_CLAIM_TEXT, CATALOG_DRIFT_TEXT, CATALOG_PATCH_TEXT, COPILOT_CONTEXT_TEXT, JUDGE_TEXT, MERGE_TEXT, RUN_SQL_CHECK_TEXT, SCHEMA_PARTIAL_TEXT, SQL_CHECK_TEXT, TYPE_LABEL,
|
||||
DUAL_CHECK_TEXT,
|
||||
claimTally,
|
||||
evidenceTally, issuanceLabel, nodeTypeLabel,
|
||||
} from '../lib/terms'
|
||||
@@ -714,13 +715,34 @@ function gateTitle(tool: string, verdict: 'allow' | 'escalate' | 'team', reason:
|
||||
return reason ? `${head}:${reason}` : head
|
||||
}
|
||||
|
||||
/**
|
||||
* 双盲复算、反方审稿的日志代码(期 3 前后端共用的约定)。这几条按 code 和结构化字段解读,不看原话;后端发成 info 的
|
||||
* (一致、裁决、记入数据目录、审稿完成)也上时间线,不然这两种节点就是一个跑了几十秒、什么都没说的黑盒
|
||||
*/
|
||||
const VERIFY_LOG_CODES = new Set([
|
||||
'dual_check_skipped', 'dual_check_same_model', 'dual_check_agreed', 'dual_check_disagreed', 'dual_check_resolved',
|
||||
'dual_check_catalog', 'dual_check_limit',
|
||||
'challenge_skipped', 'challenge_same_model', 'challenge_done', 'challenge_refuted', 'challenge_limit',
|
||||
])
|
||||
|
||||
const DC = DUAL_CHECK_TEXT
|
||||
const CL = DUAL_CHECK_TEXT.challengeLog
|
||||
const isRecord = (v: unknown): v is Record<string, any> => !!v && typeof v === 'object' && !Array.isArray(v)
|
||||
/** 老日志、别的服务端版本可能给的是一句中文原因而不是代码:中文的照写,认不得的代码不往界面上放 */
|
||||
const cjkText = (v: unknown): string | undefined => (typeof v === 'string' && /[㐀-鿿]/.test(v) ? v : undefined)
|
||||
/** 个数:给的是数就取整,给的是清单就数一下,别的(缺了、写坏了)当不知道 */
|
||||
const countOf = (v: unknown): number | undefined => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.round(v)
|
||||
: Array.isArray(v) ? v.length : undefined)
|
||||
|
||||
/**
|
||||
* 几种「看着跑完了、其实没做成」的状况(NI-3/4/5)。后端这几条日志是写给排查的,原样
|
||||
* 贴出来是一串标记和术语,而且不说该去哪儿改——这里说成人话,并给出下一步。
|
||||
* 认不得的 code 返回 null,照原话显示
|
||||
* 认不得的 code 返回 null,照原话显示。
|
||||
*
|
||||
* 双盲复算、反方审稿那几条(VERIFY_LOG_CODES)另带 level(该不该用提醒色)和 artifact(完整对照、审稿记录可下钻)
|
||||
*/
|
||||
function explainLog(code: string | undefined, message: string, d: Record<string, any> = {},
|
||||
): Pick<Step, 'title' | 'sub' | 'next' | 'fix' | 'fixNode' | 'fixField' | 'checks'> | null {
|
||||
): Pick<Step, 'title' | 'sub' | 'next' | 'fix' | 'fixNode' | 'fixField' | 'checks' | 'level' | 'artifact'> | null {
|
||||
const str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v : undefined)
|
||||
const strs = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string' && !!x) : [])
|
||||
switch (code) {
|
||||
@@ -971,6 +993,117 @@ function explainLog(code: string | undefined, message: string, d: Record<string,
|
||||
fix: 'canvas',
|
||||
}
|
||||
}
|
||||
// ---- 双盲复算:只读结构化字段。老日志缺了字段就不点名、不报数,标题照样说得通
|
||||
case 'dual_check_skipped': {
|
||||
const reason = str(d.reason)
|
||||
if (reason === 'disabled') {
|
||||
return { title: DC.log.skippedDisabled, next: DC.log.skippedDisabledNext, fix: 'canvas', fixField: 'enabled', level: 'info' }
|
||||
}
|
||||
// 放了这个节点却一个指标都复算不了:多半是配错了,用提醒色
|
||||
if (reason === 'no_inputs') {
|
||||
return { title: DC.log.skippedNoInputs, next: DC.log.skippedNoInputsNext, fix: 'canvas', fixField: 'metrics', level: 'warn' }
|
||||
}
|
||||
return { title: DC.log.skipped, sub: cjkText(d.reason), level: 'info' }
|
||||
}
|
||||
case 'dual_check_same_model':
|
||||
return { title: DC.log.sameModel(str(d.model)), next: DC.log.sameModelNext, fix: 'canvas', fixField: 'model', level: 'warn' }
|
||||
case 'dual_check_agreed':
|
||||
return { title: DC.log.agreed(countOf(d.metrics) || undefined), level: 'info' }
|
||||
case 'dual_check_disagreed': {
|
||||
// metrics 是 [{id, name}]:副标题点名,没起名字的指标写 id
|
||||
const list = Array.isArray(d.metrics) ? d.metrics.filter(isRecord) : []
|
||||
const names = list.map((m) => str(m.name) ?? str(m.id)).filter((n): n is string => !!n)
|
||||
return {
|
||||
title: DC.log.disagreed(list.length || undefined),
|
||||
sub: names.length ? names.map((n) => `「${n}」`).join('') : undefined,
|
||||
next: DC.log.disagreedNext,
|
||||
level: 'warn',
|
||||
...(str(d.card_artifact) ? { artifact: str(d.card_artifact) } : {}),
|
||||
}
|
||||
}
|
||||
case 'dual_check_resolved': {
|
||||
// choices 是 {指标 id: a | b | neither};gaps 是记成缺口的个数(给成清单的也数得出来)
|
||||
const picked = isRecord(d.choices) ? Object.values(d.choices) : []
|
||||
const n = (c: string) => picked.filter((v) => v === c).length
|
||||
const parts = [
|
||||
n('a') ? DC.log.resolvedA(n('a')) : '', n('b') ? DC.log.resolvedB(n('b')) : '', n('neither') ? DC.log.resolvedNeither(n('neither')) : '',
|
||||
].filter(Boolean)
|
||||
const gaps = countOf(d.gaps)
|
||||
// 没给缺口数时按约定推:采用盲算、两边都不对都会记缺口
|
||||
const warn = gaps != null ? gaps > 0 : n('b') + n('neither') > 0
|
||||
return { title: DC.log.resolved(parts), sub: gaps != null ? DC.log.gaps(gaps) : undefined, level: warn ? 'warn' : 'info' }
|
||||
}
|
||||
case 'dual_check_catalog': {
|
||||
// written:[{source, table, item}] 写进数据目录的已确认事实;skipped:[{reason}] 没写的(已有已确认的不覆盖……)
|
||||
const written = Array.isArray(d.written) ? d.written.filter(isRecord) : []
|
||||
const skipped = Array.isArray(d.skipped) ? d.skipped.filter(isRecord) : []
|
||||
const what = written.map((w) => {
|
||||
const table = str(w.table)
|
||||
const where = table ? `${table}${str(w.source) ? `(${str(w.source)})` : ''}` : ''
|
||||
return [where, str(w.item)].filter(Boolean).join(' · ')
|
||||
}).filter(Boolean)
|
||||
const why = skipped.map((s) => str(s.reason)).filter((r): r is string => !!r)
|
||||
const rest = skipped.length ? `${DC.log.catalogSkipped(skipped.length)}${why.length ? `:${why.join(';')}` : ''}` : ''
|
||||
return {
|
||||
title: written.length ? DC.log.catalog(written.length) : DC.log.catalogNone,
|
||||
sub: [what.join(';'), rest].filter(Boolean).join(';') || undefined,
|
||||
next: written.length ? DC.log.catalogNext : undefined,
|
||||
level: 'info',
|
||||
}
|
||||
}
|
||||
case 'dual_check_limit': {
|
||||
const which = str(d.limit)
|
||||
return {
|
||||
title: DC.log.limit((which && DC.limitLabel[which]) || DC.limitAny, str(d.metric)),
|
||||
next: which === 'daily_max_usd' ? DC.limitDailyNext : DC.log.limitNext,
|
||||
level: 'warn',
|
||||
}
|
||||
}
|
||||
// ---- 反方审稿:同上,文案在 DUAL_CHECK_TEXT.challengeLog(时间线的解读统一在这里做)
|
||||
case 'challenge_skipped': {
|
||||
const reason = str(d.reason)
|
||||
if (reason === 'disabled') {
|
||||
return { title: CL.skippedDisabled, next: CL.skippedDisabledNext, fix: 'canvas', fixField: 'enabled', level: 'info' }
|
||||
}
|
||||
if (reason === 'no_report') return { title: CL.skippedNoReport, next: CL.skippedNoReportNext, fix: 'canvas', level: 'warn' }
|
||||
if (reason === 'no_claims') return { title: CL.skippedNoClaims, level: 'info' }
|
||||
return { title: CL.skipped, sub: cjkText(d.reason), level: 'info' }
|
||||
}
|
||||
case 'challenge_same_model':
|
||||
return { title: CL.sameModel(str(d.model)), next: CL.sameModelNext, fix: 'canvas', fixField: 'model', level: 'warn' }
|
||||
case 'challenge_done': {
|
||||
// counts:{stands, weakened, refuted, inconclusive},四种结论各几句;为 0 的不写
|
||||
const c = isRecord(d.counts) ? d.counts : {}
|
||||
const keys = ['stands', 'weakened', 'refuted', 'inconclusive']
|
||||
const nums = keys.map((k) => countOf(c[k]) ?? 0)
|
||||
const total = nums.reduce((a, b) => a + b, 0)
|
||||
const parts = keys.map((k, i) => (nums[i] ? `${CL.verdict[k]} ${formatNumber(nums[i])}` : '')).filter(Boolean)
|
||||
return {
|
||||
title: CL.done(total || undefined),
|
||||
sub: parts.length ? parts.join(' · ') : undefined,
|
||||
level: nums[2] > 0 ? 'warn' : 'info',
|
||||
...(str(d.artifact) ? { artifact: str(d.artifact) } : {}),
|
||||
}
|
||||
}
|
||||
case 'challenge_refuted': {
|
||||
const items = Array.isArray(d.items) ? d.items.filter(isRecord) : []
|
||||
const texts = items.map((i) => str(i.text)).filter((t): t is string => !!t)
|
||||
return {
|
||||
title: CL.refuted(items.length || undefined),
|
||||
sub: texts.length ? texts.map((t) => `「${t}」`).join('') : undefined,
|
||||
next: CL.refutedNext,
|
||||
level: 'warn',
|
||||
}
|
||||
}
|
||||
case 'challenge_limit': {
|
||||
const which = str(d.limit)
|
||||
const skipped = countOf(d.claims_skipped)
|
||||
return {
|
||||
title: CL.limit((which && DC.limitLabel[which]) || DC.limitAny, skipped || undefined),
|
||||
next: which === 'daily_max_usd' ? DC.limitDailyNext : CL.limitNext,
|
||||
level: 'warn',
|
||||
}
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -2146,6 +2279,19 @@ export function decodeRun(events: RunEvent[], final?: RunFinal, opts?: DecodeOpt
|
||||
break
|
||||
}
|
||||
}
|
||||
// 双盲复算、反方审稿的日志按 code 和结构化字段解读:info 的也上时间线,没有原话的老日志照样说得出来。
|
||||
// 后端标了 warn / error 的照它的来,explainLog 认为该提醒的(有缺口、有推翻)也用提醒色
|
||||
if (typeof d.code === 'string' && VERIFY_LOG_CODES.has(d.code)) {
|
||||
const said = explainLog(d.code, message, d)
|
||||
if (said) {
|
||||
const tone = level === 'error' ? 'error' : level === 'warn' || said.level === 'warn' ? 'warn' : 'info'
|
||||
push({
|
||||
id: `lg-${seq}`, seq, kind: 'note', nodeId, status: 'done', code: d.code, ...said, level: tone,
|
||||
...(message ? { detail: message } : {}),
|
||||
}, nodeId)
|
||||
break
|
||||
}
|
||||
}
|
||||
// 其余 info 是给排查用的,不进主流程——但 warn/error 用户必须看到
|
||||
if (level === 'info' || !message) break
|
||||
const code = typeof d.code === 'string' && d.code ? d.code : undefined
|
||||
|
||||
@@ -3480,3 +3480,124 @@ export interface RunComparison {
|
||||
|
||||
/** 运行接口回的运行带着发起它的定时任务(RunOut.schedule_id);Run 上没写这一项,对比页的运行卡要用 */
|
||||
export type RunWithSchedule = Run & { schedule_id?: string | null }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 双盲复算:审批 payload、完整对照工件、节点配置,以及设置里「核查」一组(期 3 前后端共用的约定)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** 审核人对一个指标的裁决:a 采用原流程的结果,b 采用盲算的结果,neither 两边都不对 */
|
||||
export type DualCheckChoice = 'a' | 'b' | 'neither'
|
||||
|
||||
/** 双盲复算挂起时 payload.summary 的一项:审批卡先用它画,完整对照另从 card_artifact 取 */
|
||||
export interface DualCheckSummaryItem {
|
||||
metric_id: string
|
||||
name: string
|
||||
unit?: string | null
|
||||
original_rendered: string | null
|
||||
blind_rendered: string | null
|
||||
/** (盲算值 − 原流程值) ÷ |原流程值| × 100,保留两位小数(12.5 表示相差 12.5%);原流程值为 0 时为 null */
|
||||
delta_pct: number | null
|
||||
/** 具体差异,一条一句中文,最多 3 条 */
|
||||
diff: string[]
|
||||
/** 只有兜底说明:两条 SQL 的写法不同,未能归纳成具体差异 */
|
||||
fallback: boolean
|
||||
}
|
||||
|
||||
/** payload.kind 为 dual_check 的审批 payload(审批的 mode 是 input) */
|
||||
export interface DualCheckPayload {
|
||||
kind: 'dual_check'
|
||||
node_id: string
|
||||
title: string
|
||||
/** 完整对照放在这个工件里(GET /api/artifacts/{id}),内容见 DualCheckCard */
|
||||
card_artifact: string
|
||||
summary: DualCheckSummaryItem[]
|
||||
}
|
||||
|
||||
/** 审核人的回复:POST /approvals/{id}/decide 的 value */
|
||||
export interface DualCheckReply {
|
||||
choices: Record<string, DualCheckChoice>
|
||||
note: string
|
||||
}
|
||||
|
||||
/** 指标的一个输入:取自哪条查询的哪一格(alias 是查询快照的编号) */
|
||||
export interface DualCheckInput {
|
||||
path: string
|
||||
value: unknown
|
||||
sql?: string | null
|
||||
source?: string | null
|
||||
alias?: string | null
|
||||
}
|
||||
|
||||
/** 盲算那一边的输入多一段盲算模型自己的说明 */
|
||||
export interface DualCheckBlindInput extends DualCheckInput {
|
||||
rationale?: string | null
|
||||
}
|
||||
|
||||
export interface DualCheckSide<I extends DualCheckInput = DualCheckInput> {
|
||||
value: number | null
|
||||
rendered: string | null
|
||||
inputs: I[]
|
||||
}
|
||||
|
||||
/** 两条 SQL 差在哪(服务端 sql_semantics.explain_sql_diff)。aspect 为 other 的是兜底说明,parse 是解析不了 */
|
||||
export interface DualCheckDiff {
|
||||
equivalent: boolean
|
||||
items: { aspect: string; text: string }[]
|
||||
/** 用了兜底说明(一条具体说法都没有,或者具体说法只覆盖了一部分) */
|
||||
fallback: boolean
|
||||
/** 规范化之后两条 SQL 不同的行:「- 」开头是原流程的,「+ 」开头是盲算的 */
|
||||
text_diff: string[]
|
||||
}
|
||||
|
||||
export interface DualCheckMetric {
|
||||
metric_id: string
|
||||
name: string
|
||||
unit?: string | null
|
||||
decimals?: number | null
|
||||
original: DualCheckSide
|
||||
blind: DualCheckSide<DualCheckBlindInput>
|
||||
diff: DualCheckDiff
|
||||
/** 相差比例,口径同 DualCheckSummaryItem.delta_pct(有的话) */
|
||||
delta_pct?: number | null
|
||||
}
|
||||
|
||||
/** card_artifact 工件的内容 */
|
||||
export interface DualCheckCard {
|
||||
metrics: DualCheckMetric[]
|
||||
/** 盲算用的模型 */
|
||||
model?: string | null
|
||||
/** 盲算模型和原流程的模型相同 */
|
||||
same_model?: boolean
|
||||
}
|
||||
|
||||
/** 双盲复算节点的配置。留空的项:card 取上游的口径卡,metrics 取输入来自查询的全部指标,其余跟随设置 */
|
||||
export interface DualCheckConfig {
|
||||
card?: string
|
||||
metrics?: string[]
|
||||
definitions?: Record<string, string>
|
||||
provider?: string
|
||||
model?: string
|
||||
tolerance?: number
|
||||
on_reject?: 'degrade' | 'fail'
|
||||
/** null 或不写:跟随设置 */
|
||||
enabled?: boolean | null
|
||||
}
|
||||
|
||||
/** 设置里的 verify 一组(GET /settings 给的是补齐默认之后的完整一组;上限为 null 表示不限) */
|
||||
export interface VerifySettings {
|
||||
provider: string | null
|
||||
model: string | null
|
||||
dual_check: { enabled: boolean; tolerance: number; max_queries: number | null; timeout_s: number | null }
|
||||
challenge: { enabled: boolean; max_claims: number | null; max_queries: number | null; timeout_s: number | null }
|
||||
inquiry: { enabled: boolean; max_queries: number | null; timeout_s: number | null }
|
||||
daily_max_usd: number | null
|
||||
}
|
||||
|
||||
/** GET /settings/verify/spend:今天(本地日期)核查花了多少。unpriced_calls 是估不出金额、没算进 usd 的调用次数 */
|
||||
export interface VerifySpend {
|
||||
date?: string
|
||||
usd?: number
|
||||
calls?: number
|
||||
unpriced_calls?: number
|
||||
daily_max_usd?: number | null
|
||||
}
|
||||
|
||||
@@ -2018,5 +2018,154 @@ await section('并行查询配对(A1)、查询的 SQL 检查(tool.end.chec
|
||||
check('……下一步说改来源查询的 SQL', !!m?.next?.includes('来源查询的 SQL'), m?.next)
|
||||
})
|
||||
|
||||
await section('双盲复算与反方审稿的日志(期 3):按 code 和结构化字段说人话,info 也上时间线,老日志缺字段不崩', async () => {
|
||||
// 期 3 前后端共用的约定:后端发 log 时带 code 和结构化字段,时间线按 code 解读、不从原话里认。原话照旧留在展开区。
|
||||
// 一致、裁决、记入数据目录这几条后端发的是 info:不上时间线的话,双盲复算节点就是一个跑了几十秒、什么都没说的黑盒
|
||||
const E = (seq, type, node_id, data = {}) => ({ seq, type, node_id, ts: 1790000000 + seq, data })
|
||||
const CARD = 'c0ffee'.repeat(10) + 'beef'
|
||||
const DONE = 'facade'.repeat(10) + 'f00d'
|
||||
const events = [
|
||||
E(1, 'run.started', null, { total: 5 }),
|
||||
E(2, 'node.started', 'dc', { node_type: 'dual_check', label: '复核销售额' }),
|
||||
E(3, 'log', 'dc', { level: 'warn', code: 'dual_check_same_model', model: 'mx-1', message: '盲算模型与原流程相同(mx-1)' }),
|
||||
E(4, 'log', 'dc', { level: 'info', code: 'dual_check_agreed', metrics: 2, message: '2 个指标与盲算一致' }),
|
||||
E(5, 'log', 'dc', { level: 'warn', code: 'dual_check_disagreed', metrics: [{ id: 'gmv', name: '销售额' }, { id: 'aov', name: '' }],
|
||||
card_artifact: CARD, message: '2 个指标与盲算不一致,已暂停等待审核' }),
|
||||
E(6, 'log', 'dc', { level: 'warn', code: 'dual_check_limit', limit: 'max_queries', metric: '客单价', message: '已达查询次数上限' }),
|
||||
E(7, 'log', 'dc', { level: 'info', code: 'dual_check_resolved', choices: { gmv: 'a', aov: 'neither' }, gaps: 1, message: '审核人已裁决' }),
|
||||
E(8, 'log', 'dc', { level: 'info', code: 'dual_check_catalog', message: '裁决已记入数据目录',
|
||||
written: [{ source: 'shop', table: 'orders', item: '业务日期:支付时间 paid_at' }],
|
||||
skipped: [{ reason: '已有已确认的有效行过滤,未覆盖' }] }),
|
||||
E(9, 'node.finished', 'dc', { duration_ms: 3000 }),
|
||||
// 没有原话的老日志(只有 code 和结构化字段)照样解读
|
||||
E(10, 'node.started', 'dc2', { node_type: 'dual_check', label: '复核订单数' }),
|
||||
E(11, 'log', 'dc2', { level: 'info', code: 'dual_check_skipped', reason: 'disabled' }),
|
||||
E(12, 'node.finished', 'dc2', { duration_ms: 5 }),
|
||||
E(13, 'node.started', 'dc3', { node_type: 'dual_check', label: '复核退款' }),
|
||||
E(14, 'log', 'dc3', { level: 'info', code: 'dual_check_skipped', reason: 'no_inputs', message: '没有可复算的指标' }),
|
||||
E(15, 'node.finished', 'dc3', { duration_ms: 5 }),
|
||||
E(16, 'node.started', 'ch', { node_type: 'challenge', label: '反方审稿' }),
|
||||
E(17, 'log', 'ch', { level: 'warn', code: 'challenge_same_model', model: 'wm-2', message: '审稿模型与撰写模型相同' }),
|
||||
E(18, 'log', 'ch', { level: 'warn', code: 'challenge_limit', limit: 'timeout_s', claims_skipped: 2, message: '已达时长上限' }),
|
||||
E(19, 'log', 'ch', { level: 'warn', code: 'challenge_refuted', message: '2 句结论被推翻',
|
||||
items: [{ unit: 'u3', text: '增长主要来自新客' }, { unit: 'u5', text: '退款率下降' }] }),
|
||||
E(20, 'log', 'ch', { level: 'info', code: 'challenge_done', counts: { stands: 3, weakened: 1, refuted: 2, inconclusive: 0 },
|
||||
artifact: DONE, message: '反方审稿完成' }),
|
||||
E(21, 'node.finished', 'ch', { duration_ms: 9000 }),
|
||||
E(22, 'node.started', 'ch2', { node_type: 'challenge', label: '反方审稿二' }),
|
||||
E(23, 'log', 'ch2', { level: 'info', code: 'challenge_skipped', reason: 'disabled', message: '反方审稿已关闭' }),
|
||||
E(24, 'node.finished', 'ch2', { duration_ms: 5 }),
|
||||
E(25, 'run.finished', null, { status: 'succeeded' }),
|
||||
]
|
||||
const all = flatten(mod.decodeRun(events))
|
||||
const by = (code, node) => all.find((s) => s.code === code && (!node || s.nodeId === node))
|
||||
const CODES = ['dual_check_skipped', 'dual_check_same_model', 'dual_check_agreed', 'dual_check_disagreed', 'dual_check_resolved',
|
||||
'dual_check_catalog', 'dual_check_limit', 'challenge_skipped', 'challenge_same_model', 'challenge_done', 'challenge_refuted', 'challenge_limit']
|
||||
check('约定里的 12 个日志代码各有一行(info 的也在)', CODES.every((c) => by(c)), CODES.filter((c) => !by(c)).join('、'))
|
||||
check('……都挂在自己的节点下,不是平铺', CODES.every((c) => all.some((s) => s.kind === 'node' && s.children?.some((x) => x.code === c))))
|
||||
check('……标题里没有内部代码', !all.some((s) => /dual_check_|challenge_/.test(`${s.title}${s.sub ?? ''}${s.next ?? ''}`)))
|
||||
check('……有原话的,原话留在展开区', by('dual_check_agreed')?.detail === '2 个指标与盲算一致'
|
||||
&& by('challenge_done')?.detail === '反方审稿完成', `${by('dual_check_agreed')?.detail} / ${by('challenge_done')?.detail}`)
|
||||
|
||||
const same = by('dual_check_same_model')
|
||||
check('dual_check_same_model:点名模型、缺乏独立性、提醒色,入口落到节点的盲算模型', same?.title === '盲算模型与原流程同为「mx-1」,复算缺乏独立性'
|
||||
&& same.level === 'warn' && !!same.next?.includes('盲算模型') && same.fix === 'canvas' && same.fixField === 'model',
|
||||
`${same?.title} / ${same?.level} / ${same?.fixField}`)
|
||||
const agreed = by('dual_check_agreed')
|
||||
check('dual_check_agreed:几个指标相符,不用提醒色', agreed?.title === '双盲复算一致:2 个指标与盲算结果相符' && agreed.level === 'info',
|
||||
`${agreed?.title} / ${agreed?.level}`)
|
||||
const dis = by('dual_check_disagreed')
|
||||
check('dual_check_disagreed:几个指标不一致、等待裁决,副标题点名(没名字的写 id)', dis?.title === '双盲复算有 2 个指标与盲算结果不一致,等待审核人裁决'
|
||||
&& dis.sub === '「销售额」「aov」' && dis.level === 'warn', `${dis?.title} / ${dis?.sub}`)
|
||||
check('……下一步说去审批卡上逐个选,完整对照的工件可下钻', !!dis?.next?.includes('审批卡') && dis.artifact === CARD, `${dis?.next} / ${dis?.artifact}`)
|
||||
const lim = by('dual_check_limit')
|
||||
check('dual_check_limit:哪个指标、哪个上限、去哪儿调', lim?.title === '复算指标「客单价」时已达查询次数上限,该指标未完成盲算'
|
||||
&& lim.level === 'warn' && !!lim.next?.includes('核查'), `${lim?.title} / ${lim?.next}`)
|
||||
const res = by('dual_check_resolved')
|
||||
check('dual_check_resolved:按选择计数(为 0 的不写),缺口放副标题', res?.title === '审核人已裁决:采用原流程 1 个,两边都不对 1 个'
|
||||
&& res.sub === '1 项记为缺口' && res.level === 'warn', `${res?.title} / ${res?.sub} / ${res?.level}`)
|
||||
const cat = by('dual_check_catalog')
|
||||
check('dual_check_catalog:记入几项、写的是什么、没写入的照实说', cat?.title === '已将 1 项裁决记入数据目录'
|
||||
&& !!cat.sub?.includes('orders') && cat.sub.includes('业务日期:支付时间 paid_at') && cat.sub.includes('1 项未写入')
|
||||
&& cat.sub.includes('已有已确认的有效行过滤,未覆盖') && !!cat.next?.includes('数据目录') && cat.level === 'info', `${cat?.title} / ${cat?.sub}`)
|
||||
const off = by('dual_check_skipped', 'dc2')
|
||||
check('dual_check_skipped(disabled,没有原话):照样上时间线,说怎么开启', off?.title === '双盲复算未启用,已跳过'
|
||||
&& !!off.next?.includes('启用') && off.level === 'info' && off.detail === undefined, `${off?.title} / ${off?.next} / ${off?.detail}`)
|
||||
const none = by('dual_check_skipped', 'dc3')
|
||||
check('dual_check_skipped(no_inputs):没有可复算的指标是提醒,入口落到「复算的指标」', none?.title === '没有输入来自查询的指标,已跳过复算'
|
||||
&& none.level === 'warn' && none.fix === 'canvas' && none.fixField === 'metrics', `${none?.title} / ${none?.level} / ${none?.fixField}`)
|
||||
const csame = by('challenge_same_model')
|
||||
check('challenge_same_model:点名模型、缺乏独立性', csame?.title === '反方审稿模型与报告撰写模型同为「wm-2」,审稿缺乏独立性'
|
||||
&& csame.level === 'warn' && !!csame.next, csame?.title)
|
||||
const climit = by('challenge_limit')
|
||||
check('challenge_limit:哪个上限、几句没审、去哪儿调', climit?.title === '反方审稿已达时长上限,2 句结论未审' && climit.level === 'warn'
|
||||
&& !!climit.next?.includes('反方审稿'), `${climit?.title} / ${climit?.next}`)
|
||||
const ref = by('challenge_refuted')
|
||||
check('challenge_refuted:推翻了几句,句子放副标题', ref?.title === '反方审稿推翻了 2 句结论' && ref.sub === '「增长主要来自新客」「退款率下降」'
|
||||
&& ref.level === 'warn' && !!ref.next, `${ref?.title} / ${ref?.sub}`)
|
||||
const done = by('challenge_done')
|
||||
check('challenge_done:审了几句、四种结论各几句(为 0 的不写),有推翻时是提醒、工件可下钻', done?.title === '反方审稿完成:审阅了 6 句结论'
|
||||
&& done.sub === '经受住 3 · 被削弱 1 · 被推翻 2' && done.level === 'warn' && done.artifact === DONE, `${done?.title} / ${done?.sub} / ${done?.level}`)
|
||||
const coff = by('challenge_skipped')
|
||||
check('challenge_skipped(disabled):未启用、怎么开启', coff?.title === '反方审稿未启用,已跳过' && coff.level === 'info' && !!coff.next,
|
||||
`${coff?.title} / ${coff?.level}`)
|
||||
check('……节点照常完成', all.filter((s) => s.kind === 'node').every((s) => s.status === 'done'))
|
||||
|
||||
// 换一种取值:每日金额上限、选择全是原流程、缺口是一份清单、推翻 0 句、跳过原因是一句中文
|
||||
const alt = flatten(mod.decodeRun([
|
||||
E(1, 'node.started', 'dc', { node_type: 'dual_check' }),
|
||||
E(2, 'log', 'dc', { level: 'warn', code: 'dual_check_limit', limit: 'daily_max_usd', message: '今日核查费用已达上限' }),
|
||||
E(3, 'log', 'dc', { level: 'info', code: 'dual_check_resolved', choices: { gmv: 'a', aov: 'a' }, gaps: [], message: '审核人已裁决' }),
|
||||
E(4, 'log', 'dc', { level: 'info', code: 'dual_check_resolved', choices: { gmv: 'b' }, gaps: ['指标「销售额」采用了盲算的结果'], message: '审核人已裁决' }),
|
||||
E(5, 'log', 'dc', { level: 'info', code: 'dual_check_catalog', written: [], skipped: [{ reason: '差异不止一个方面' }], message: '未写入' }),
|
||||
E(6, 'node.finished', 'dc', {}),
|
||||
E(7, 'node.started', 'ch', { node_type: 'challenge' }),
|
||||
E(8, 'log', 'ch', { level: 'info', code: 'challenge_done', counts: { stands: 4, weakened: 0, refuted: 0, inconclusive: 1 }, message: '完成' }),
|
||||
E(9, 'log', 'ch', { level: 'info', code: 'challenge_skipped', reason: '报告节点没有产出文档', message: '跳过' }),
|
||||
E(10, 'log', 'ch', { level: 'info', code: 'challenge_skipped', reason: 'no_claims', message: '跳过' }),
|
||||
E(11, 'node.finished', 'ch', {}),
|
||||
]))
|
||||
const daily = alt.find((s) => s.code === 'dual_check_limit')
|
||||
check('每日金额上限:不点名指标,下一步说今日费用已达上限', daily?.title === '双盲复算已达每日金额上限' && !!daily.next?.includes('今日核查费用'),
|
||||
`${daily?.title} / ${daily?.next}`)
|
||||
const [allA, oneB] = alt.filter((s) => s.code === 'dual_check_resolved')
|
||||
check('全部采用原流程:没有缺口,不用提醒色', allA?.title === '审核人已裁决:采用原流程 2 个' && allA.sub === '没有缺口' && allA.level === 'info',
|
||||
`${allA?.title} / ${allA?.sub} / ${allA?.level}`)
|
||||
check('缺口给的是一份清单:照样数出几项', oneB?.title === '审核人已裁决:采用盲算 1 个' && oneB.sub === '1 项记为缺口' && oneB.level === 'warn',
|
||||
`${oneB?.title} / ${oneB?.sub}`)
|
||||
const cat0 = alt.find((s) => s.code === 'dual_check_catalog')
|
||||
check('一项都没写入数据目录:标题照实说,原因放副标题', cat0?.title === '裁决未记入数据目录' && !!cat0.sub?.includes('差异不止一个方面'),
|
||||
`${cat0?.title} / ${cat0?.sub}`)
|
||||
const calm = alt.find((s) => s.code === 'challenge_done')
|
||||
check('没有推翻:不用提醒色,0 句的不写', calm?.title === '反方审稿完成:审阅了 5 句结论' && calm.sub === '经受住 4 · 无法判断 1'
|
||||
&& calm.level === 'info', `${calm?.title} / ${calm?.sub} / ${calm?.level}`)
|
||||
const [said, noClaims] = alt.filter((s) => s.code === 'challenge_skipped')
|
||||
check('跳过原因是一句中文:照写在副标题;认得的原因说人话', said?.title === '反方审稿已跳过' && said.sub === '报告节点没有产出文档'
|
||||
&& noClaims?.title === '报告中没有需要审的关键结论,已跳过反方审稿', `${said?.title} / ${said?.sub} / ${noClaims?.title}`)
|
||||
|
||||
// 老日志缺字段、字段类型不对:不崩,标题照样说人话,不出 undefined、NaN、[object Object],不瞎填数
|
||||
const odd = [
|
||||
['dual_check_skipped', {}], ['dual_check_same_model', { model: 7 }], ['dual_check_agreed', { metrics: 'x' }],
|
||||
['dual_check_disagreed', { metrics: 'x', card_artifact: 3 }], ['dual_check_resolved', { choices: null, gaps: 'x' }],
|
||||
['dual_check_catalog', { written: 'x', skipped: { reason: 1 } }], ['dual_check_limit', { limit: 9 }],
|
||||
['challenge_skipped', { reason: ['x'] }], ['challenge_same_model', {}], ['challenge_done', { counts: [1, 2] }],
|
||||
['challenge_refuted', { items: {} }], ['challenge_limit', { claims_skipped: 'x' }],
|
||||
]
|
||||
let broken = null
|
||||
let steps = []
|
||||
try {
|
||||
steps = flatten(mod.decodeRun([E(1, 'node.started', 'x', { node_type: 'dual_check' }),
|
||||
...odd.map(([code, data], i) => E(i + 2, 'log', 'x', { level: 'warn', code, ...data })), E(99, 'node.finished', 'x', {})]))
|
||||
} catch (e) {
|
||||
broken = String(e?.message ?? e)
|
||||
}
|
||||
check('缺字段的老日志:不抛异常', broken === null, broken ?? '')
|
||||
const rows = odd.map(([code]) => steps.find((s) => s.code === code))
|
||||
check('……每个代码照样一行,标题不空', rows.every((s) => s && s.title), odd.filter((_, i) => !rows[i]?.title).map(([c]) => c).join('、'))
|
||||
check('……不出 undefined、NaN、[object Object],也不瞎填数', !rows.some((s) => /undefined|NaN|object Object|\d/.test(`${s?.title}${s?.sub ?? ''}`)),
|
||||
rows.filter((s) => /undefined|NaN|object Object|\d/.test(`${s?.title}${s?.sub ?? ''}`)).map((s) => `${s?.title}|${s?.sub}`).join(' ; '))
|
||||
check('……缺字段的工件 id 不当成可下钻的工件', !rows.some((s) => s?.artifact != null))
|
||||
})
|
||||
|
||||
console.log(failed ? `\n✗ ${failed} 项未通过` : '\n✓ 解码器全部通过')
|
||||
process.exit(failed ? 1 : 0)
|
||||
|
||||
Reference in New Issue
Block a user