双盲复算前端(三):检查器字段、节点卡片摘要,以及检查器与节点卡片的钩子

- nodeDefs:照约定一字不差地加 FieldDef.render 与 FieldRenderProps;NodeDef 加可选的 summary(ctx) 钩子
  (ctx:id、config、画布上的 nodes 与 edges,只返回字符串);dual_check 换成真字段:要复算的口径卡、复算的指标、
  口径说明、启用、审核人不采用原流程的结果时(基本),盲算模型的接入与模型、允许的相对误差(高级)
- Inspector:照约定一字不差地在 FieldInput 的 switch 前交给 field.render
- canvas/DualCheckFields.tsx(新):口径卡下拉分上游、其余两组,不在上游或不在画布上照实提醒;指标逐个勾,
  全勾回到缺省不写、最后一个不让取消;口径说明逐个指标写,被取消的指标上已写的说明列出来可删;盲算模型照「结论句
  裁判」的写法(只填模型时写明按模型名用哪个接入);相对误差的占位写出设置里的值;启用三态,跟随设置不留 null。
  每个控件只写自己的键,留空写回 undefined
- NodeCard:用只返回字符串的 studio 选择器调摘要钩子,有就用、没有照旧 summarize;摘要写「复算「卡名」 · N 个指标」,
  关闭时标出;去掉 dual_check 的占位 case
- 预览用例加检查器(小画布用真的节点卡片 + 360px 检查器),check-verify 加检查器写配置、摘要跟着变、窄屏和对比度
- 去掉 nodeDefs 里已经不成立的「下面两种还是占位」注释
This commit is contained in:
ThearchyHelios
2026-10-05 14:23:17 +08:00
parent cac34c90dd
commit 7a9dc93dde
8 changed files with 887 additions and 11 deletions
+223 -3
View File
@@ -24,7 +24,9 @@
"original_rendered": "45,678.5元",
"blind_rendered": "51,388.3元",
"delta_pct": 12.5,
"diff": ["原流程按支付时间 paid_at 筛选时间范围,盲算按下单时间 created_at 筛选"],
"diff": [
"原流程按支付时间 paid_at 筛选时间范围,盲算按下单时间 created_at 筛选"
],
"fallback": false
},
{
@@ -79,7 +81,10 @@
"diff": {
"equivalent": false,
"items": [
{ "aspect": "time", "text": "原流程按支付时间 paid_at 筛选时间范围,盲算按下单时间 created_at 筛选" }
{
"aspect": "time",
"text": "原流程按支付时间 paid_at 筛选时间范围,盲算按下单时间 created_at 筛选"
}
],
"fallback": false,
"text_diff": [
@@ -140,7 +145,10 @@
"diff": {
"equivalent": false,
"items": [
{ "aspect": "other", "text": "两条 SQL 的写法不同,未能归纳成具体差异,请对照原文" }
{
"aspect": "other",
"text": "两条 SQL 的写法不同,未能归纳成具体差异,请对照原文"
}
],
"fallback": true,
"text_diff": [
@@ -150,5 +158,217 @@
}
}
]
},
"graph": {
"nodes": [
{
"id": "start",
"type": "input",
"position": {
"x": 0,
"y": 120
},
"data": {
"label": "输入",
"config": {
"fields": [
{
"name": "week",
"required": true
}
]
}
}
},
{
"id": "fetch",
"type": "agent",
"position": {
"x": 280,
"y": 120
},
"data": {
"label": "取数",
"config": {
"prompt": "查询本周各门店的订单",
"tools": [
"db_query__shop"
],
"assign_to": "kpi"
}
}
},
{
"id": "card",
"type": "metrics",
"position": {
"x": 560,
"y": 40
},
"data": {
"label": "周报口径卡",
"config": {
"caliber": "周报口径",
"caliber_version": "v1",
"metrics": [
{
"id": "gmv",
"name": "销售额",
"unit": "元",
"expression": "vars.kpi.gmv"
},
{
"id": "orders",
"name": "订单数",
"unit": "单",
"expression": "vars.kpi.orders"
},
{
"id": "aov",
"name": "客单价",
"unit": "元",
"expression": "vars.kpi.gmv / vars.kpi.orders"
}
]
}
}
},
{
"id": "card2",
"type": "metrics",
"position": {
"x": 560,
"y": 260
},
"data": {
"label": "月报口径卡",
"config": {
"caliber": "月报口径",
"caliber_version": "v2",
"metrics": [
{
"id": "m_gmv",
"name": "月销售额",
"unit": "元",
"expression": "vars.kpi.gmv"
}
]
}
}
},
{
"id": "dc",
"type": "dual_check",
"position": {
"x": 840,
"y": 40
},
"data": {
"label": "复核周报口径",
"config": {}
}
},
{
"id": "done",
"type": "output",
"position": {
"x": 1120,
"y": 40
},
"data": {
"label": "成果",
"config": {
"fields": [
{
"name": "结果",
"value": "{{ last_message }}"
}
]
}
}
}
],
"edges": [
{
"id": "e1",
"source": "start",
"target": "fetch"
},
{
"id": "e2",
"source": "fetch",
"target": "card"
},
{
"id": "e3",
"source": "fetch",
"target": "card2"
},
{
"id": "e4",
"source": "card",
"target": "dc"
},
{
"id": "e5",
"source": "dc",
"target": "done"
}
]
},
"providers": [
{
"id": "prov-a",
"name": "云端接入",
"kind": "anthropic",
"base_url": null,
"default_model": "model-a",
"models": [
{
"id": "model-a"
},
{
"id": "model-b"
}
],
"enabled": true,
"extra": {}
},
{
"id": "prov-b",
"name": "备用接入",
"kind": "openai",
"base_url": null,
"default_model": "model-c",
"models": [
{
"id": "model-c"
}
],
"enabled": false,
"extra": {}
}
],
"verify": {
"provider": null,
"model": null,
"dual_check": {
"enabled": true,
"tolerance": 1e-06,
"max_queries": 6,
"timeout_s": 120
},
"challenge": {
"enabled": true,
"max_claims": 5,
"max_queries": 4,
"timeout_s": 90
},
"inquiry": {
"enabled": true,
"max_queries": 6,
"timeout_s": 120
},
"daily_max_usd": 2.0
}
}
+41 -2
View File
@@ -1,6 +1,12 @@
import { useState } from 'react'
import type { JSX } from 'react'
import { ReactFlow, ReactFlowProvider } from '@xyflow/react'
import { ApprovalCard } from '../../run/RunPanel'
import type { Approval } from '../../types'
import { Inspector } from '../../canvas/Inspector'
import { NodeCard } from '../../canvas/NodeCard'
import { toFlow, useStudio } from '../../store/studio'
import { useCatalog } from '../../store/catalog'
import type { Approval, GraphSpec, Provider } from '../../types'
import data from './dual_check.json'
/**
@@ -8,8 +14,10 @@ import data from './dual_check.json'
* - ?syn=dual-check-approval:正式运行里两路对不上的审批卡,宽栏(两条 SQL 左右并排)
* - ?syn=dual-check-approval-rail:同一张卡摆在 360px 的画布右栏里(两条 SQL 上下排)
* - ?syn=dual-check-approval-exploratory:探索运行,后果里不提出具
* - ?syn=dual-check-inspector:一张小画布(真的节点卡片)加 360px 的检查器,选中双盲复算节点;检查器改的是
* studio store 里的节点配置,卡片上的摘要跟着变
*
* 完整对照的工件(GET /api/artifacts/{id})和审批回复接口由检查脚本用 page.route 伪造,直接打开时卡上照实说
* 完整对照的工件(GET /api/artifacts/{id})、审批回复、设置、校验都由检查脚本用 page.route 伪造,直接打开时卡上照实说
* 无法加载完整对照。提交后 onResolved 只把运行 id 记到 window.__dualResolved,不去接事件流
*/
const approval = (patch: Partial<Approval> = {}): Approval => ({
@@ -26,6 +34,36 @@ function Card({ value }: { value: Approval }) {
)
}
const nodeTypes = { card: NodeCard }
/**
* 检查器用例:第一次渲染前把夹具里的图放进 studio store、选中双盲复算节点,模型接入直接写进目录 store(不走接口)。
* 用例都在一个预览页里预先加载,store 只能在这里(用到时)改,不能在模块顶层改
*/
function InspectorCase() {
useState(() => {
const { nodes, edges } = toFlow(data.graph as unknown as GraphSpec)
useStudio.setState({ nodes, edges, selectedId: 'dc' })
useCatalog.setState({ providers: data.providers as unknown as Provider[] })
return true
})
const nodes = useStudio((s) => s.nodes)
const edges = useStudio((s) => s.edges)
return (
<div className="flex h-full min-h-0 max-w-full" data-fixture="dual-check-inspector">
<div className="min-w-0 flex-1 max-sm:hidden" data-dual-canvas="">
<ReactFlowProvider>
<ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView nodesDraggable={false} nodesConnectable={false}
proOptions={{ hideAttribution: true }} />
</ReactFlowProvider>
</div>
<div className="h-full w-[360px] max-w-full shrink-0 overflow-hidden border-l bg-panel" data-dual-inspector="">
<Inspector />
</div>
</div>
)
}
export const cases: Record<string, () => JSX.Element> = {
'dual-check-approval': () => (
<div className="mx-auto max-w-3xl p-4 text-sm" data-fixture="dual-check-approval"><Card value={approval()} /></div>
@@ -40,4 +78,5 @@ export const cases: Record<string, () => JSX.Element> = {
<Card value={approval({ run_class: 'exploratory' })} />
</div>
),
'dual-check-inspector': () => <InspectorCase />,
}