期 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:
ThearchyHelios
2026-10-05 14:10:17 +08:00
parent b58321f6b8
commit 504bb9ad93
5 changed files with 430 additions and 6 deletions
+3
View File
@@ -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') {
+1 -1
View File
@@ -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 ''
}
+53 -2
View File
@@ -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: {},
},
}
+276
View File
@@ -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
}
}
+97 -3
View File
@@ -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},跳到这次查询`,
}
/** 设置页里复核套件(双盲复算、反方审稿等)的默认值一栏 */