mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:21:58 +00:00
期 3 反方审稿(前端):检查器与节点卡片——可配置要审的报告、句数与查询上限、反方模型、被推翻时的处理、启用
- nodeDefs、Inspector 加约定里的字段渲染钩子(FieldDef.render、FieldRenderProps,FieldInput 里给了 render 就交给它画),
代码照约定一字不差,双盲复算的工作包会加同样的代码
- 新增 canvas/verifyFields.tsx,三样通用控件写成双盲复算也能直接用的样子,文案全由调用方传入:
- 选画布上的节点:只列本节点上游的某一类节点(下游的运行到这里还没有产出);留空写回 undefined,按缺省取上游的那一个;
上游没有、有好几个又没选、选中的被删掉或挪到下游,都在下面照实提醒,选中项照实列出,不像「未选择」
- 三态开关:跟随设置 / 开启 / 关闭,对应配置里没有这一项 / true / false;单选组,方向键在组内移动
- 核查模型:接入加模型,写法同结论句裁判的模型选择(只填模型时说明按模型名会用哪个接入、和被核查的模型相同时提醒)
- challenge 节点去掉「功能开发中」,检查器字段:要审的报告、最多审几句、反方模型(接入、模型)、结论被推翻时
(出具降档(默认)/ 只标注 / 报错并停止运行)、启用;每句最多查询次数、每句时长上限放在高级选项。上限都可以不写,
按「设置 → 核查」取,不往配置里落默认值
- 节点卡片摘要按配置写:审哪份报告(节点 id,没选时写审上游报告)、最多审几句(没写时写句数跟随设置),关掉的写已关闭
- terms.ts 的 CHALLENGE_TEXT:判断统一叫经受住、被削弱、被推翻、无法判断;检查器、卡片摘要和审批卡的文案都在这一块
This commit is contained in:
@@ -618,6 +618,9 @@ function FieldInput({ field, id, nodeId, syntax, value, config, invalid, describ
|
||||
const defaults = useRunDefaults(field.key === 'scope' || field.type === 'collection')
|
||||
const aria = { 'aria-invalid': invalid || undefined, 'aria-describedby': describedBy }
|
||||
|
||||
if (field.render) {
|
||||
return <>{field.render({ id, nodeId, value, config, invalid, describedBy, disabled, onChange })}</>
|
||||
}
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
if (syntax !== 'plain') {
|
||||
|
||||
@@ -98,7 +98,7 @@ function summarize(type: NodeType, config: Record<string, any>): string {
|
||||
case 'dual_check':
|
||||
return DUAL_CHECK_TEXT.pending
|
||||
case 'challenge':
|
||||
return CHALLENGE_TEXT.pending
|
||||
return CHALLENGE_TEXT.summary(config)
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
Bot, Braces, Brain, CheckCircle2, Code2, Combine, Database, FileInput, FileOutput, FileText,
|
||||
Gauge, GitBranch, GitCompareArrows, Hand, Repeat, Search, ShieldAlert, Shuffle, Users, Wrench,
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
APPROVAL_POLICY_LABEL, CHALLENGE_TEXT, DUAL_CHECK_TEXT, MERGE_TEXT, NODE_TYPE_LABEL, UPGRADE_POLICY_LABEL,
|
||||
} from '../lib/terms'
|
||||
import type { NodeType } from '../types'
|
||||
import { nodePickField, triStateField, upstreamModel, verifyModelFields } from './verifyFields'
|
||||
|
||||
export type FieldType =
|
||||
| 'text' | 'textarea' | 'prompt' | 'code' | 'number' | 'select' | 'switch'
|
||||
@@ -51,6 +53,23 @@ export interface FieldDef {
|
||||
*/
|
||||
unknownLabel?: (value: string) => string
|
||||
advanced?: boolean
|
||||
/**
|
||||
* 现有字段类型画不了的配置(选画布上某一类节点、三态开关、接入加模型)交给它画。
|
||||
* 给了 render 就不再按 type 渲染;type 仍用于默认值和校验
|
||||
*/
|
||||
render?: (p: FieldRenderProps) => ReactNode
|
||||
}
|
||||
|
||||
/** FieldDef.render 拿到的:当前值、写回、所在节点和配置,以及无障碍属性 */
|
||||
export interface FieldRenderProps {
|
||||
id: string
|
||||
nodeId: string
|
||||
value: any
|
||||
config: Record<string, any>
|
||||
invalid: boolean
|
||||
describedBy?: string
|
||||
disabled?: boolean
|
||||
onChange: (v: any) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -669,9 +688,41 @@ export const NODE_DEFS: Record<NodeType, NodeDef> = {
|
||||
},
|
||||
challenge: {
|
||||
type: 'challenge', label: NODE_TYPE_LABEL.challenge, category: '把关', icon: ShieldAlert,
|
||||
description: `${CHALLENGE_TEXT.description}。${CHALLENGE_TEXT.pending}`,
|
||||
description: CHALLENGE_TEXT.description,
|
||||
hasTarget: true, sources: [{ id: 'out', label: '' }],
|
||||
fields: [],
|
||||
// 上限、模型、开关都可以不写:服务端按「设置 → 核查」里反方审稿那一组取(verify_settings)。没写的项不往配置里落默认值,
|
||||
// 设置改了,没单独配过的节点跟着变
|
||||
fields: [
|
||||
nodePickField({ key: 'report', label: CHALLENGE_TEXT.field.report, nodeType: 'report', text: CHALLENGE_TEXT.reportPick }),
|
||||
{
|
||||
key: 'max_claims', label: CHALLENGE_TEXT.field.max_claims, type: 'number', min: 1,
|
||||
placeholder: CHALLENGE_TEXT.followSettings, help: CHALLENGE_TEXT.help.max_claims,
|
||||
},
|
||||
// 和撰写报告的模型相同时提醒:同一个模型审自己写的结论,较难发现错误
|
||||
...verifyModelFields({
|
||||
labels: { provider: CHALLENGE_TEXT.field.provider, model: CHALLENGE_TEXT.field.model },
|
||||
text: CHALLENGE_TEXT.model, subject: upstreamModel('report', 'report'),
|
||||
}),
|
||||
{
|
||||
// 没写等于 degrade:降档放第一项,没写时下拉框正好落在它上面
|
||||
key: 'on_refuted', label: CHALLENGE_TEXT.field.on_refuted, type: 'select',
|
||||
options: [
|
||||
{ value: 'degrade', label: CHALLENGE_TEXT.onRefuted.degrade },
|
||||
{ value: 'flag', label: CHALLENGE_TEXT.onRefuted.flag },
|
||||
{ value: 'fail', label: CHALLENGE_TEXT.onRefuted.fail },
|
||||
],
|
||||
help: CHALLENGE_TEXT.help.on_refuted, unknownLabel: CHALLENGE_TEXT.unknownValue,
|
||||
},
|
||||
triStateField({ key: 'enabled', label: CHALLENGE_TEXT.field.enabled, text: CHALLENGE_TEXT.enabled }),
|
||||
{
|
||||
key: 'max_queries', label: CHALLENGE_TEXT.field.max_queries, type: 'number', min: 1, advanced: true,
|
||||
placeholder: CHALLENGE_TEXT.followSettings, help: CHALLENGE_TEXT.help.max_queries,
|
||||
},
|
||||
{
|
||||
key: 'timeout_s', label: CHALLENGE_TEXT.field.timeout_s, type: 'number', min: 1, advanced: true,
|
||||
placeholder: CHALLENGE_TEXT.followSettings, help: CHALLENGE_TEXT.help.timeout_s,
|
||||
},
|
||||
],
|
||||
defaults: {},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { Edge } from '@xyflow/react'
|
||||
import clsx from 'clsx'
|
||||
import { useRadioGroup } from '../components/ui'
|
||||
import { modelOptions, providerOfModel, useCatalog } from '../store/catalog'
|
||||
import { useStudio, type FlowNode } from '../store/studio'
|
||||
import type { NodeType } from '../types'
|
||||
import type { FieldDef, FieldRenderProps } from './nodeDefs'
|
||||
|
||||
/**
|
||||
* 核查类节点(反方审稿、双盲复算)检查器里的几样通用控件,nodeDefs 用下面的工厂函数拼进字段表:
|
||||
* - nodePickField:从画布上挑本节点上游的某一类节点(要审的报告、要复算的口径卡),留空按缺省取上游的那一个;
|
||||
* - triStateField:跟随设置 / 开启 / 关闭三态(配置里没有这一项、true、false);
|
||||
* - verifyModelFields:核查模型的接入加模型,写法同结论句裁判的模型选择。
|
||||
*
|
||||
* 文案一律由调用方传进来(各节点自己那一块的 *_TEXT),控件里不写死任何一句话:两个节点说法不同(反方模型 / 盲算模型),
|
||||
* 控件是同一个。读画布节点用 studio store,不往 FieldRenderProps 里加字段。
|
||||
*/
|
||||
|
||||
/** 节点在下拉里的叫法:名字和 id 不同时把 id 带上(同 NodeRefsInput) */
|
||||
function nodeLabel(n: FlowNode): string {
|
||||
const label = String(n.data?.label || n.id)
|
||||
return label === n.id ? label : `${label}(${n.id})`
|
||||
}
|
||||
|
||||
/** 本节点的全部上游(沿连线往回找)。只认上游:下游的节点运行到这里时还没有产出 */
|
||||
function upstreamOf(nodeId: string, edges: Edge[]): Set<string> {
|
||||
const parents = new Map<string, string[]>()
|
||||
for (const e of edges) parents.set(e.target, [...(parents.get(e.target) ?? []), e.source])
|
||||
const seen = new Set<string>()
|
||||
const stack = [...(parents.get(nodeId) ?? [])]
|
||||
while (stack.length) {
|
||||
const id = stack.pop()!
|
||||
if (seen.has(id) || id === nodeId) continue
|
||||
seen.add(id)
|
||||
stack.push(...(parents.get(id) ?? []))
|
||||
}
|
||||
return seen
|
||||
}
|
||||
|
||||
/**
|
||||
* 这个节点实际要核查的是哪一个:配置里写了就是它(还在画布上的话),没写时上游恰好有一个就是那一个;
|
||||
* 上游有好几个、一个都没有时说不准,返回 undefined
|
||||
*/
|
||||
export function targetNode(nodeId: string, chosen: unknown, type: NodeType, nodes: FlowNode[], edges: Edge[]): FlowNode | undefined {
|
||||
if (typeof chosen === 'string' && chosen) return nodes.find((n) => n.id === chosen)
|
||||
const up = upstreamOf(nodeId, edges)
|
||||
const candidates = nodes.filter((n) => up.has(n.id) && n.data?.nodeType === type)
|
||||
return candidates.length === 1 ? candidates[0] : undefined
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 选画布上的节点
|
||||
|
||||
export interface NodePickText {
|
||||
/** 留空那一项:上游恰好一个时给出它的名字 */
|
||||
auto: (only: string | null) => string
|
||||
/** 上游一个都没有 */
|
||||
none: string
|
||||
/** 上游不止一个、又没有选 */
|
||||
many: (n: number) => string
|
||||
/** 选中的节点已不在画布上(下拉里那一项的叫法) */
|
||||
missing: (id: string) => string
|
||||
/** 选中的节点不在上游(下拉里那一项的叫法) */
|
||||
notUpstream: (label: string) => string
|
||||
missingHint: string
|
||||
notUpstreamHint: string
|
||||
}
|
||||
|
||||
function NodePick({ p, nodeType, text }: { p: FieldRenderProps; nodeType: NodeType; text: NodePickText }) {
|
||||
const nodes = useStudio((s) => s.nodes)
|
||||
const edges = useStudio((s) => s.edges)
|
||||
const options = useMemo(() => {
|
||||
const up = upstreamOf(p.nodeId, edges)
|
||||
return nodes.filter((n) => up.has(n.id) && n.data?.nodeType === nodeType)
|
||||
}, [nodes, edges, p.nodeId, nodeType])
|
||||
const value = typeof p.value === 'string' ? p.value : ''
|
||||
const chosen = value ? nodes.find((n) => n.id === value) : undefined
|
||||
// 选中的节点被删了、挪到了下游:下拉里照实多列一项(选不回去),不让它看着像「未选择」
|
||||
const stale = value && !options.some((n) => n.id === value) ? (chosen ? 'notUpstream' : 'missing') : null
|
||||
const hint = stale === 'missing' ? text.missingHint
|
||||
: stale === 'notUpstream' ? text.notUpstreamHint
|
||||
: !options.length ? text.none
|
||||
: !value && options.length > 1 ? text.many(options.length) : ''
|
||||
const hintId = `${p.id}-hint`
|
||||
const only = options.length === 1 ? String(options[0].data?.label || options[0].id) : null
|
||||
return (
|
||||
<>
|
||||
<select id={p.id} className="field" value={value} disabled={p.disabled} aria-invalid={p.invalid || undefined}
|
||||
aria-describedby={[p.describedBy, hint ? hintId : ''].filter(Boolean).join(' ') || undefined}
|
||||
style={p.invalid || stale ? { borderColor: p.invalid ? 'var(--err)' : 'var(--warn)' } : undefined}
|
||||
onChange={(e) => p.onChange(e.target.value || undefined)}>
|
||||
<option value="">{text.auto(only)}</option>
|
||||
{options.map((n) => <option key={n.id} value={n.id}>{nodeLabel(n)}</option>)}
|
||||
{stale && (
|
||||
<option value={value} disabled>{stale === 'missing' ? text.missing(value) : text.notUpstream(nodeLabel(chosen!))}</option>
|
||||
)}
|
||||
</select>
|
||||
{hint && (
|
||||
<div id={hintId} className="mt-1 text-2xs leading-snug" data-node-pick-hint={stale ?? (options.length ? 'many' : 'none')}
|
||||
style={{ color: 'var(--st-waiting)' }}>
|
||||
{hint}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从画布上挑本节点上游的某一类节点。配置里存节点 id;留空(写回 undefined,不留空字符串)时服务端按缺省取上游的那一个
|
||||
*/
|
||||
export function nodePickField(def: {
|
||||
key: string; label: string; nodeType: NodeType; text: NodePickText; help?: string; advanced?: boolean
|
||||
}): FieldDef {
|
||||
return {
|
||||
key: def.key, label: def.label, type: 'select', help: def.help, advanced: def.advanced,
|
||||
render: (p) => <NodePick p={p} nodeType={def.nodeType} text={def.text} />,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 三态开关
|
||||
|
||||
export interface TriStateText {
|
||||
follow: string
|
||||
on: string
|
||||
off: string
|
||||
/** 选中各态时下面那句说明:跟随的是哪一项设置、关掉之后会怎样 */
|
||||
hint: { follow: string; on: string; off: string }
|
||||
}
|
||||
|
||||
type Tri = 'follow' | 'on' | 'off'
|
||||
const TRI: readonly Tri[] = ['follow', 'on', 'off']
|
||||
|
||||
function TriState({ p, label, text }: { p: FieldRenderProps; label: string; text: TriStateText }) {
|
||||
// 只认真正的布尔:手写的 "false"、0 不当成关闭,按跟随设置显示(服务端校验会报出来)
|
||||
const state: Tri = p.value === true ? 'on' : p.value === false ? 'off' : 'follow'
|
||||
// 跟随设置写回 undefined:配置里不留这一项,和没配过的节点一样
|
||||
const set = (t: Tri) => p.onChange(t === 'on' ? true : t === 'off' ? false : undefined)
|
||||
const radio = useRadioGroup(TRI, state, set)
|
||||
const hintId = `${p.id}-hint`
|
||||
return (
|
||||
<>
|
||||
{/* 字段标签的 for 指不到一组按钮,组名另写在 aria-label 上 */}
|
||||
<div id={p.id} role="radiogroup" aria-label={label} data-tri-state={state}
|
||||
aria-describedby={[hintId, p.describedBy].filter(Boolean).join(' ')}
|
||||
className="inline-flex max-w-full flex-wrap rounded-md border p-px">
|
||||
{TRI.map((t) => (
|
||||
<button key={t} type="button" {...radio(t)} disabled={p.disabled} data-tri={t} onClick={() => set(t)}
|
||||
className={clsx('rounded px-2 py-0.5 text-2xs', state === t ? 'bg-accent-soft text-fg' : 'text-faint hover:text-dim')}>
|
||||
{text[t]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div id={hintId} className="mt-1 text-2xs leading-snug text-faint" data-tri-hint={state}>{text.hint[state]}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 跟随设置 / 开启 / 关闭:配置里没有这一项(或 null)/ true / false */
|
||||
export function triStateField(def: { key: string; label: string; text: TriStateText; advanced?: boolean }): FieldDef {
|
||||
return {
|
||||
key: def.key, label: def.label, type: 'select', advanced: def.advanced,
|
||||
render: (p) => <TriState p={p} label={def.label} text={def.text} />,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 核查模型
|
||||
|
||||
export interface VerifyModelText {
|
||||
/** 接入、模型都留空 */
|
||||
providerFollow: string
|
||||
/** 接入留空、模型填了:服务端按模型名找接入,不再跟随设置 */
|
||||
providerFromModel: string
|
||||
providerMissing: (name: string) => string
|
||||
/** 接在已停用的接入名后面 */
|
||||
providerDisabled: string
|
||||
/** 模型框的占位:接入也没选 / 选了接入 */
|
||||
modelFollow: string
|
||||
modelDefault: string
|
||||
/** 取值链的说明,常显在模型框下面 */
|
||||
help: string
|
||||
byModel: (model: string, found: { name: string; enabled: boolean } | null | undefined, fallback: string | null | undefined) => string
|
||||
/** 和被核查的模型是同一个 */
|
||||
same: (model: string) => string
|
||||
}
|
||||
|
||||
/** 被核查的那个模型:节点上明确写着的模型名;说不准(没写、跟随默认)时为 null,不提醒 */
|
||||
export type SubjectModel = (nodeId: string, config: Record<string, any>, graph: { nodes: FlowNode[]; edges: Edge[] }) => string | null
|
||||
|
||||
function ProviderPick({ p, text }: { p: FieldRenderProps; text: VerifyModelText }) {
|
||||
const providers = useCatalog((s) => s.providers)
|
||||
const provider = typeof p.value === 'string' ? p.value : ''
|
||||
const model = typeof p.config.model === 'string' ? p.config.model.trim() : ''
|
||||
const known = provider ? providers.find((x) => x.name === provider) : undefined
|
||||
return (
|
||||
<select id={p.id} className="field" value={provider} disabled={p.disabled} aria-invalid={p.invalid || undefined}
|
||||
aria-describedby={p.describedBy} style={p.invalid ? { borderColor: 'var(--err)' } : undefined}
|
||||
onChange={(e) => p.onChange(e.target.value || undefined)}>
|
||||
<option value="">{model ? text.providerFromModel : text.providerFollow}</option>
|
||||
{/* 接入列表还没取回来时照写名字,免得落到第一项、看着像跟随设置 */}
|
||||
{provider && !known && <option value={provider}>{providers.length ? text.providerMissing(provider) : provider}</option>}
|
||||
{providers.map((x) => (
|
||||
<option key={x.id} value={x.name}>{x.name}{x.enabled ? '' : text.providerDisabled}</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelInput({ p, text, subject }: { p: FieldRenderProps; text: VerifyModelText; subject?: SubjectModel }) {
|
||||
const providers = useCatalog((s) => s.providers)
|
||||
// 选择器只返回一个字符串:画布上别的节点改动不让这一栏重渲染
|
||||
const against = useStudio((s) => (subject ? subject(p.nodeId, p.config, s) : null))
|
||||
const provider = typeof p.config.provider === 'string' ? p.config.provider : ''
|
||||
const model = typeof p.value === 'string' ? p.value : ''
|
||||
const name = model.trim()
|
||||
const pv = provider ? providers.find((x) => x.name === provider) : undefined
|
||||
const models = pv ? (pv.models ?? []).map((m) => m.id) : [...new Set(modelOptions(providers).map((o) => o.value))]
|
||||
// 只填了模型:这一级写了就整组用节点上的(verify_model_spec 不跨级拼),服务端按模型名找接入
|
||||
const byModel = !provider && !!name
|
||||
const owner = byModel ? providerOfModel(providers, name) : undefined
|
||||
const enabled = providers.filter((x) => x.enabled)
|
||||
const fallback = enabled.find((x) => x.kind !== 'mock') ?? enabled[0]
|
||||
const byModelHint = byModel && providers.length > 0
|
||||
? text.byModel(name, owner && { name: owner.provider.name, enabled: owner.enabled }, fallback?.name) : ''
|
||||
const same = !!name && !!against && name === against
|
||||
return (
|
||||
<>
|
||||
<input id={p.id} className="field mono text-xs" list={`${p.id}-models`} value={model} spellCheck={false}
|
||||
disabled={p.disabled} aria-invalid={p.invalid || undefined} aria-describedby={p.describedBy}
|
||||
placeholder={provider ? text.modelDefault : text.modelFollow}
|
||||
style={p.invalid ? { borderColor: 'var(--err)' } : undefined}
|
||||
onChange={(e) => p.onChange(e.target.value.trim() ? e.target.value : undefined)} />
|
||||
<datalist id={`${p.id}-models`}>{models.map((m) => <option key={m} value={m} />)}</datalist>
|
||||
{byModelHint && (
|
||||
<div className="mt-1 text-2xs leading-snug" data-verify-by-model={owner ? (owner.enabled ? 'found' : 'disabled') : 'default'}
|
||||
style={owner?.enabled ? undefined : { color: 'var(--st-waiting)' }}>
|
||||
<span className={owner?.enabled ? 'text-faint' : undefined}>{byModelHint}</span>
|
||||
</div>
|
||||
)}
|
||||
{same && (
|
||||
<div className="mt-1 text-2xs leading-snug" style={{ color: 'var(--st-waiting)' }} data-verify-same-model="">
|
||||
{text.same(name)}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 text-2xs leading-snug text-faint">{text.help}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 核查模型:接入、模型两个字段(配置里的 provider、model)。都留空时用设置里的核查模型;写了任一项就整组以节点为准。
|
||||
* subject 给出被核查的那个模型,相同时提醒(等于自己审自己)
|
||||
*/
|
||||
export function verifyModelFields(def: {
|
||||
labels: { provider: string; model: string }; text: VerifyModelText; subject?: SubjectModel; advanced?: boolean
|
||||
}): FieldDef[] {
|
||||
return [
|
||||
{
|
||||
key: 'provider', label: def.labels.provider, type: 'select', advanced: def.advanced,
|
||||
render: (p) => <ProviderPick p={p} text={def.text} />,
|
||||
},
|
||||
{
|
||||
key: 'model', label: def.labels.model, type: 'model', advanced: def.advanced,
|
||||
render: (p) => <ModelInput p={p} text={def.text} subject={def.subject} />,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 被核查节点上写着的模型:refKey 是本节点配置里指向它的那一项(反方审稿的 report),没写时取上游唯一的那一个 nodeType
|
||||
*/
|
||||
export function upstreamModel(refKey: string, nodeType: NodeType): SubjectModel {
|
||||
return (nodeId, config, { nodes, edges }) => {
|
||||
const model = targetNode(nodeId, config[refKey], nodeType, nodes, edges)?.data?.config?.model
|
||||
return typeof model === 'string' && model.trim() ? model.trim() : null
|
||||
}
|
||||
}
|
||||
@@ -3340,10 +3340,104 @@ export const DUAL_CHECK_TEXT = {
|
||||
pending: `${PENDING_TEXT}:本节点暂未生效,运行时将跳过`,
|
||||
}
|
||||
|
||||
/** 反方审稿节点:节点库说明、检查器和运行结果 */
|
||||
/**
|
||||
* 反方审稿节点:节点库说明、检查器、节点卡片摘要,以及审批卡上的反方审稿(run/ChallengeSummary.tsx)。
|
||||
* 四种判断全站只有这一种叫法:经受住、被削弱、被推翻、无法判断。判断由反方模型给出,不是系统核对。
|
||||
* 「出具会降档」只在正式运行、按降档处理时才说;探索运行不出具,不说任何和出具有关的话
|
||||
*/
|
||||
export const CHALLENGE_TEXT = {
|
||||
description: '从反方立场审阅上游报告,逐条提出质疑',
|
||||
pending: `${PENDING_TEXT}:本节点暂未生效,运行时将跳过`,
|
||||
description: '从反方立场复核上游报告的关键结论:另一个模型只读查库,逐句判断经受住、被削弱、被推翻还是无法判断',
|
||||
verdict: { stands: '经受住', weakened: '被削弱', refuted: '被推翻', inconclusive: '无法判断' },
|
||||
/** 服务端给了一个界面不认识的判断:照写原值,不归到哪一类 */
|
||||
verdictUnknown: (v: string) => `未知判断「${v}」`,
|
||||
|
||||
// ---- 节点卡片的摘要。report 是节点 id:卡片摘要只看配置 ----
|
||||
summary: (c: { report?: unknown; max_claims?: unknown; enabled?: unknown }) => [
|
||||
c.enabled === false ? '已关闭' : '',
|
||||
typeof c.report === 'string' && c.report.trim() ? `审报告节点 ${c.report.trim()}` : '审上游报告',
|
||||
typeof c.max_claims === 'number' && c.max_claims > 0 ? `最多审 ${formatNumber(c.max_claims)} 句结论` : '句数跟随设置',
|
||||
].filter(Boolean).join(' · '),
|
||||
|
||||
// ---- 检查器 ----
|
||||
field: {
|
||||
report: '要审的报告',
|
||||
max_claims: '最多审几句',
|
||||
max_queries: '每句最多查询次数',
|
||||
timeout_s: '每句时长上限(秒)',
|
||||
provider: '反方模型 · 接入',
|
||||
model: '反方模型',
|
||||
on_refuted: '结论被推翻时',
|
||||
enabled: '启用',
|
||||
},
|
||||
followSettings: '跟随设置',
|
||||
help: {
|
||||
max_claims: '每份报告最多挑几句关键结论来审。留空则使用「设置 → 核查」中的值',
|
||||
max_queries: '复核每句结论时最多执行几次查询。留空则使用「设置 → 核查」中的值',
|
||||
timeout_s: '复核每句结论的时长上限,到时按已查到的结果给出判断。留空则使用「设置 → 核查」中的值',
|
||||
on_refuted: '降档:被推翻的结论计入出具缺口,正式运行降档出具;只标注:在审批卡和运行记录中标出,不影响出具;'
|
||||
+ '报错:本节点失败,运行停止',
|
||||
},
|
||||
onRefuted: { degrade: '出具降档(默认)', flag: '只标注', fail: '报错并停止运行' },
|
||||
unknownValue: (v: string) => `${v}:无法识别的值,请从上方选项中选择`,
|
||||
reportPick: {
|
||||
auto: (only: string | null) => (only ? `未选择:审上游的「${only}」` : '未选择:审上游的报告撰写节点'),
|
||||
none: '上游没有报告撰写节点,请先在本节点之前连接一个报告撰写节点',
|
||||
many: (n: number) => `上游有 ${formatNumber(n)} 个报告撰写节点,请选择要审的那一个`,
|
||||
missing: (id: string) => `${id}(已不在画布上)`,
|
||||
notUpstream: (label: string) => `${label}(不在上游)`,
|
||||
missingHint: '所选节点已不在画布上,请重新选择',
|
||||
notUpstreamHint: '所选节点不在本节点上游,运行到这里时它还没有写出报告,请重新选择或调整连线',
|
||||
},
|
||||
enabled: {
|
||||
follow: '跟随设置', on: '开启', off: '关闭',
|
||||
hint: {
|
||||
follow: '使用「设置 → 核查」中「反方审稿 · 启用」的值',
|
||||
on: '无论设置如何都执行反方审稿',
|
||||
off: '不执行反方审稿,上游的结果原样交给下游,运行记录中记一条说明',
|
||||
},
|
||||
},
|
||||
model: {
|
||||
providerFollow: '跟随设置',
|
||||
providerFromModel: '按模型名找接入',
|
||||
providerMissing: (name: string) => `${name}(不存在)`,
|
||||
providerDisabled: '(已停用)',
|
||||
modelFollow: '跟随设置',
|
||||
modelDefault: '留空则使用该接入的默认模型',
|
||||
help: '接入和模型均未填写时,使用「设置 → 核查」中的核查模型;填写任一项后,整组以节点配置为准,不与设置合并。'
|
||||
+ '建议反方模型与撰写报告的模型不同,同一模型较难发现自身的错误',
|
||||
byModel: (model: string, found: { name: string; enabled: boolean } | null | undefined, fallback: string | null | undefined) => (
|
||||
found && !found.enabled
|
||||
? `只填了模型:「${model}」属于已停用的接入「${found.name}」,调用将失败。请启用该接入或选择其他接入`
|
||||
: found
|
||||
? `只填了模型:将按模型名使用接入「${found.name}」。接入和模型都未填写时,才跟随设置中的核查模型`
|
||||
: `只填了模型:所有接入的模型列表中都没有「${model}」,将使用默认接入${fallback ? `「${fallback}」` : ''}调用;`
|
||||
+ '如该模型不属于默认接入,请选择对应的接入'),
|
||||
same: (model: string) => `反方模型与撰写报告的模型相同(「${model}」),审稿效果有限`,
|
||||
},
|
||||
|
||||
// ---- 审批卡 ----
|
||||
head: (label: string) => `报告「${label}」的反方审稿`,
|
||||
count: (verdict: string, n: number) => `${verdict} ${formatNumber(n)} 句`,
|
||||
none: '报告中没有需要反方复核的关键结论',
|
||||
degrade: '出具会降档',
|
||||
degradeHint: '被推翻的结论计入出具缺口,本次正式运行将降档出具',
|
||||
flagNote: '按节点设置,被推翻的结论只做标注,不影响出具档位',
|
||||
more: (n: number) => `另有 ${formatNumber(n)} 句未列出`,
|
||||
showAll: '显示全部',
|
||||
loading: '正在加载反方审稿…',
|
||||
failed: '无法获取反方审稿的详细内容',
|
||||
retry: '重试',
|
||||
claimMissing: '反方审稿记录中没有这一句的详细内容',
|
||||
judgedBy: (model: string | null) => `判断由反方模型${model ? `「${model}」` : ''}给出,并非系统核对`,
|
||||
rationale: '理由',
|
||||
rationaleEmpty: '反方没有给出理由',
|
||||
docLoading: '正在加载理由…',
|
||||
queries: (n: number) => `反方执行的查询(${formatNumber(n)} 次)`,
|
||||
noQueries: '反方没有执行查询',
|
||||
rows: (n: number) => `${formatNumber(n)} 行`,
|
||||
truncated: '结果已截断,数据库中还有更多数据',
|
||||
/** 理由里有出处的数字:读屏听到的是「数字,出处:查询 Q2 · 第 1 行 · amount」,点一下跳到那次查询 */
|
||||
cited: (text: string, source: string) => `${text},出处:${source},跳到这次查询`,
|
||||
}
|
||||
|
||||
/** 设置页里复核套件(双盲复算、反方审稿等)的默认值一栏 */
|
||||
|
||||
Reference in New Issue
Block a user