mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 16:41:59 +00:00
界面跟上点名接入的新规矩:设置页标出已停用的接入、选不了别的停用接入,接入表变了画布重新校验,问题面板和修复预览不露键名
点名停用的接入以前照常调用,现在运行时照实报错,界面要提前说: - 设置页门控、裁判、核查三格:存着的接入已停用和「不存在」一样在那一格下面写明后果;下拉里别的停用接入列着但选不了(只留已选着的那个);模型一格不再写「留空则使用」一个调不了的接入的默认模型。服务端按项拒收(422 带 field)时整句落到那一格下面、焦点移过去,保存条连同原因一起写,不再截掉「请在……中启用它」。 - 画布校验现在读接入表:编排页记下校验时的接入表签名(id、名称、启用与否),从设置里改完接入回来、或目录刷新后接入表变了,就重新校验一次,不再停着一句过时的「已停用」。 - 问题面板、修复预览里点名接入的字段写「模型接入」「成员「分析师」的模型接入」「工作流默认设置 · 模型」,不露 provider、model 键名;修复预览给列表项写名字。同一节点上自己的接入和成员的接入是两处问题,合并、找修复都连字段一起认,计数不再少算。 检查:check-publish 加点名接入的修复(一处一行、候选、预览、应用、390px 不溢出)、问题面板字段叫法、在设置里删了接入回到画布重新校验;check-manage 加设置页已停用接入的三格说明、选项、按项拒收;check-evidence 里全图默认的 model 改按「模型」核对(不借裁判叫法的原意不变)。
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
import { PublishPreflightPane } from './PublishDialog'
|
||||
import { humanizeError } from '../lib/errors'
|
||||
import { formatShortcut } from '../lib/keys'
|
||||
import { PUBLISH_FIX_TEXT, UPGRADE_TEXT } from '../lib/terms'
|
||||
import { PROVIDER_REF_TEXT, PUBLISH_FIX_TEXT, UPGRADE_TEXT } from '../lib/terms'
|
||||
import { useCatalog } from '../store/catalog'
|
||||
import { EDIT_LOCK_TEXT, upgradeUnsaved, useEditLock, useStudio, type UpgradeFlow } from '../store/studio'
|
||||
import { Spinner, useRadioGroup } from '../components/ui'
|
||||
@@ -232,7 +232,7 @@ function ProblemRow({ problem: p, active, onLocate, onDeleteEdge }: {
|
||||
aria-label={err ? '错误' : '提示'} />
|
||||
<span className={clsx('min-w-0 flex-1', err ? 'text-fg' : 'text-dim')}>{p.message}</span>
|
||||
{where && (
|
||||
<span className="flex shrink-0 items-center gap-0.5 text-faint">
|
||||
<span className="flex shrink-0 items-center gap-0.5 text-faint" data-problem-field="">
|
||||
<CornerDownRight size={9} aria-hidden />{where}
|
||||
</span>
|
||||
)}
|
||||
@@ -615,13 +615,17 @@ function UpgradeValue({ value, field, nameOf, className, ...data }: {
|
||||
)
|
||||
}
|
||||
|
||||
const SUB_LABEL: Record<string, string> = { condition: '的条件', key: '的标识', tools: '的工具', system: '的系统提示' }
|
||||
const SUB_LABEL: Record<string, string> = {
|
||||
condition: '的条件', key: '的标识', tools: '的工具', system: '的系统提示', provider: PROVIDER_REF_TEXT.memberField,
|
||||
}
|
||||
/** 节点定义里没有、问题却会落上去的字段:点名的模型接入(多数节点的检查器里没有这一栏)。不写的话露出键名 */
|
||||
const FIELD_FALLBACK: Record<string, string> = { provider: PROVIDER_REF_TEXT.field }
|
||||
|
||||
function fieldLabel(field: FieldRef | null | undefined, type?: string, config?: Record<string, any>): string {
|
||||
if (!field) return ''
|
||||
if (field.key === 'label') return '节点名称'
|
||||
const def = type ? NODE_DEFS[type as keyof typeof NODE_DEFS] : undefined
|
||||
const base = def?.fields.find((f) => f.key === field.key)?.label ?? field.key
|
||||
const base = def?.fields.find((f) => f.key === field.key)?.label ?? FIELD_FALLBACK[field.key] ?? field.key
|
||||
if (field.index == null) return base
|
||||
const sub = (field.sub && SUB_LABEL[field.sub]) || ''
|
||||
// 成员有名字就叫名字:「第 2 个成员」还得数,「成员 writer」一眼就知道是谁
|
||||
|
||||
@@ -272,12 +272,13 @@ const unsavedApply = (a: ApplyState) => a.status === 'failed' || (a.status === '
|
||||
const unsavedFix = (pf: PreflightState) => !!pf.apply && unsavedApply(pf.apply)
|
||||
|
||||
/**
|
||||
* 节点 id → 名字和类型。跟着 issues 换新(改名、改配置之后校验都会重跑),不订阅 nodes:
|
||||
* 问题面板开着时拖一下节点,这一整块不必跟着每帧重渲染
|
||||
* 节点 id → 名字、类型和配置。跟着 issues 换新(改名、改配置之后校验都会重跑),不订阅 nodes:
|
||||
* 问题面板开着时拖一下节点,这一整块不必跟着每帧重渲染。配置给修复预览认列表里的那一项(成员「分析师」)
|
||||
*/
|
||||
function useNodeLookup(): (id: string) => { label: string; type: string } | undefined {
|
||||
function useNodeLookup(): (id: string) => { label: string; type: string; config?: Record<string, any> } | undefined {
|
||||
const tick = useStudio((s) => s.issues)
|
||||
const map = useMemo(() => new Map(useStudio.getState().nodes.map((n) => [n.id, { label: n.data.label, type: n.data.nodeType }])),
|
||||
const map = useMemo(() => new Map(useStudio.getState().nodes.map((n) => [n.id,
|
||||
{ label: n.data.label, type: n.data.nodeType, config: n.data.config }])),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[tick])
|
||||
return useCallback((id: string) => map.get(id), [map])
|
||||
@@ -544,7 +545,8 @@ export function Preflight({ pf, gate, onLocate, stale }: {
|
||||
)}
|
||||
|
||||
{pf.preview && <FixPreview pf={pf} labelOf={labelOf} nameOf={nameOf}
|
||||
typeOf={(id) => (id ? nodeOf(id)?.type : undefined)} />}
|
||||
typeOf={(id) => (id ? nodeOf(id)?.type : undefined)}
|
||||
configOf={(id) => (id ? nodeOf(id)?.config : undefined)} />}
|
||||
{/* 修复落到画布上却没存上,预览又已经收起了(换过等级):「重试保存」照样得在 */}
|
||||
{pf.preview?.status !== 'ready' && unsavedFix(pf) && <SaveTrouble pf={pf} />}
|
||||
|
||||
@@ -656,9 +658,11 @@ function FixControl({ fix, pf }: { fix: PublishFix; pf: PreflightState }) {
|
||||
}
|
||||
|
||||
/** 修复预览:逐项「节点 · 字段:原值 → 新值」,没采用的写原因,Copilot 的问题原样摆出来 */
|
||||
function FixPreview({ pf, labelOf, nameOf, typeOf }: {
|
||||
function FixPreview({ pf, labelOf, nameOf, typeOf, configOf }: {
|
||||
pf: PreflightState; labelOf: (id?: string | null) => string; nameOf: NodeNameOf
|
||||
typeOf: (id?: string | null) => string | undefined
|
||||
/** 列表里的某一项(agents[0]、fields[1])写那一项的名字,不写「第 1 项」 */
|
||||
configOf?: (id?: string | null) => Record<string, any> | undefined
|
||||
}) {
|
||||
const dirty = useStudio((s) => s.dirty)
|
||||
const p = pf.preview!
|
||||
@@ -703,7 +707,7 @@ function FixPreview({ pf, labelOf, nameOf, typeOf }: {
|
||||
<ul className="space-y-1" data-fix-changes="">
|
||||
{result.changes.map((c, i) => {
|
||||
const where = c.node_title || labelOf(c.node_id) || T.whole
|
||||
const field = fixFieldLabel(c.field, typeOf(c.node_id))
|
||||
const field = fixFieldLabel(c.field, typeOf(c.node_id), configOf?.(c.node_id))
|
||||
return (
|
||||
<li key={i} className="leading-snug" data-fix-change={c.fix_id}>
|
||||
<span className="[overflow-wrap:anywhere]">
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
* 那一栏,定位落到工具上(见 toolBindingOf)。
|
||||
*/
|
||||
import { ApiError } from '../api/client'
|
||||
import { APPROVAL_POLICY_LABEL, JUDGE_FIELD_LABEL, JUDGE_ON_UNSUPPORTED_LABEL, UPGRADE_TEXT, nodeTypeLabel } from '../lib/terms'
|
||||
import {
|
||||
APPROVAL_POLICY_LABEL, JUDGE_FIELD_LABEL, JUDGE_ON_UNSUPPORTED_LABEL, PROVIDER_REF_TEXT, UPGRADE_TEXT, nodeTypeLabel,
|
||||
} from '../lib/terms'
|
||||
import { NODE_DEFS } from './nodeDefs'
|
||||
import type { FlowNode } from '../store/studio'
|
||||
import type {
|
||||
@@ -244,6 +246,12 @@ function normalizeIssue(raw: any): ValidationIssue | null {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 点名的模型接入不存在或已停用(后端 engine/provider_refs.py)。一处一条问题、一条修复:同一个协作节点上自己的接入和
|
||||
* 成员的接入是两处,合并、找修复都要连字段一起认
|
||||
*/
|
||||
export const isProviderRefCode = (code?: string | null): boolean => code === 'provider.missing' || code === 'provider.disabled'
|
||||
|
||||
function normalizeFix(raw: any): PublishFix | null {
|
||||
if (!raw || typeof raw !== 'object' || typeof raw.id !== 'string' || !raw.id) return null
|
||||
const kind = raw.kind === 'choice' || raw.kind === 'assist' ? raw.kind : raw.kind === 'auto' ? 'auto' : null
|
||||
@@ -259,6 +267,7 @@ function normalizeFix(raw: any): PublishFix | null {
|
||||
code: asText(raw.code),
|
||||
kind,
|
||||
node_id: raw.node_id ?? null,
|
||||
...(typeof raw.field === 'string' && raw.field ? { field: raw.field } : {}),
|
||||
label: asText(raw.label) || raw.id,
|
||||
preview: raw.preview && typeof raw.preview === 'object' ? raw.preview : null,
|
||||
options,
|
||||
@@ -318,7 +327,10 @@ export function fixFor(issue: ValidationIssue, fixes: PublishFix[]): PublishFix
|
||||
if (issue.fix) return fixes.find((f) => f.id === issue.fix)
|
||||
if (!issue.code) return undefined
|
||||
const same = fixes.filter((f) => f.code === issue.code)
|
||||
const exact = same.find((f) => (f.node_id ?? null) === (issue.node_id ?? null))
|
||||
// 点名的接入一处一条修复:同一节点上的几处按字段认
|
||||
const exact = (isProviderRefCode(issue.code)
|
||||
? same.find((f) => (f.node_id ?? null) === (issue.node_id ?? null) && (f.field ?? null) === (issue.field ?? null))
|
||||
: undefined) ?? same.find((f) => (f.node_id ?? null) === (issue.node_id ?? null))
|
||||
if (exact) return exact
|
||||
return issue.node_id == null ? same[0] : same.find((f) => f.node_id == null)
|
||||
}
|
||||
@@ -326,13 +338,16 @@ export function fixFor(issue: ValidationIssue, fixes: PublishFix[]): PublishFix
|
||||
/**
|
||||
* 同一处问题只列一行:validate 和门禁常对同一处各报一条(同 code、同节点,说法不同),
|
||||
* 列两行就是两组一样的修复控件、「会被拦下 2 处」。合成的那一行取最重的级别、第一条 error 的说法,
|
||||
* 其余说法收进 others(悬停可见)。没有 code 的认不出是不是同一处,照原样各列一行
|
||||
* 其余说法收进 others(悬停可见)。没有 code 的认不出是不是同一处,照原样各列一行。
|
||||
* 点名的模型接入连字段一起认:协作节点自己的接入和成员的接入是两处问题、各有各的修复,不合成一行
|
||||
*/
|
||||
export function mergeIssues(issues: ValidationIssue[]): { issue: ValidationIssue; others: string[] }[] {
|
||||
const out: { issue: ValidationIssue; others: string[] }[] = []
|
||||
const at = new Map<string, number>()
|
||||
for (const issue of issues) {
|
||||
const key = issue.code ? `${issue.code}\u0000${issue.node_id ?? ''}` : null
|
||||
const key = issue.code
|
||||
? `${issue.code}\u0000${issue.node_id ?? ''}${isProviderRefCode(issue.code) ? `\u0000${issue.field ?? ''}` : ''}`
|
||||
: null
|
||||
const i = key != null ? at.get(key) : undefined
|
||||
if (i == null) {
|
||||
if (key != null) at.set(key, out.length)
|
||||
@@ -382,11 +397,15 @@ const CONTRACT_KEYS: Record<string, string> = {
|
||||
/** 不在节点定义的字段表里、由检查器自己画的几项(子工作流钉的版本、升版处置),以及图级的全图默认 */
|
||||
const EXTRA_FIELDS: Record<string, string> = {
|
||||
workflow_version: '固定版本', upgrade_policy: '上游发布新版本时', defaults: '工作流默认设置',
|
||||
// 多数节点的检查器里没有接入这一栏(只有双盲复算、反方审稿有),修复点名接入时照样要写得出叫法
|
||||
provider: PROVIDER_REF_TEXT.field,
|
||||
}
|
||||
/** 点号后面那一段的叫法:契约里的键,全图默认里的审批策略,列表项里的名称、取值、表达式 */
|
||||
const SUB_KEYS: Record<string, string> = {
|
||||
...CONTRACT_KEYS, approval: '审批策略', claims: '未附依据的结论句', on_uncited: '未附依据时',
|
||||
name: '名称', value: '取值', expression: '表达式',
|
||||
// 点名的接入:成员的、全图默认里的(连同全图默认里结论句裁判的那一份)
|
||||
provider: PROVIDER_REF_TEXT.field, model: PROVIDER_REF_TEXT.model, judge: PROVIDER_REF_TEXT.judge,
|
||||
}
|
||||
/** 契约 claims 的 on_uncited:没挂依据的结论句怎么算 */
|
||||
const ON_UNCITED_LABEL: Record<string, string> = {
|
||||
|
||||
@@ -919,6 +919,26 @@ export const PUBLISH_FIX_TEXT = {
|
||||
whole: '整个工作流',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 点名的模型接入不存在或已停用(后端 providers/factory.named_provider、engine/provider_refs.py)。以前点名的接入不存在时
|
||||
* 运行悄悄换成演示模型、点名停用的照常调用;现在运行照实报错,设置页、画布校验、发布前检查都提前说出来
|
||||
*/
|
||||
export const PROVIDER_REF_TEXT = {
|
||||
/** 节点配置里 provider 这一项的叫法:多数节点的检查器里没有这一栏,问题面板、修复预览里照样要写得出来 */
|
||||
field: '模型接入',
|
||||
/** 协作成员、修复预览里点号后面那一段:「成员「分析师」的模型接入」「工作流默认设置 · 模型接入」 */
|
||||
memberField: '的模型接入',
|
||||
model: '模型',
|
||||
judge: '结论句裁判',
|
||||
/** 设置页那一格下面:存着的接入不存在、已停用时先说状态,再说这一项因此会怎样(各组自己的后果) */
|
||||
missing: (name: string) => `模型接入「${name}」不存在`,
|
||||
disabled: (name: string) => `模型接入「${name}」已停用`,
|
||||
gateBroken: '门控模型无法调用,所有工具调用将转为人工审批',
|
||||
judgeBroken: '裁判调用将失败,所有结论句记为未裁判',
|
||||
// 和核查一组「不存在」那句(VERIFY_SETTING_TEXT.section.providerGone)同一个后果
|
||||
verifyBroken: '核查调用将失败',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 一键升级为可追溯结构(问题面板的快速修复、记录页的横幅)。和发布前修复同一套规矩:
|
||||
* 只出预览,人点「应用」才走现有的保存存成草稿;不发布
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
AlertTriangle, Check, Cpu, Download, KeyRound, Monitor, Moon, Plug, Plus, Settings as SettingsIcon, Sun, X,
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { api } from '../api/client'
|
||||
import { ApiError, api } from '../api/client'
|
||||
import type { ProviderDraft } from '../api/client'
|
||||
import { modelOptions, providerOfModel, useCatalog, useOnReconnect } from '../store/catalog'
|
||||
import {
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
promptDialog, SectionBar, Skeleton, Spinner, TabPanel, Tabs, toast, useRadioGroup, useTabRoute,
|
||||
} from '../components/ui'
|
||||
import { setThemePref, useTheme } from '../components/CommandPalette'
|
||||
import { humanizeError } from '../lib/errors'
|
||||
import { errorMessage } from '../lib/errors'
|
||||
import { formatNumber, shortLabel } from '../lib/format'
|
||||
import { checkHealth, forgetHealth, healthFromServer, setHealth, useHealth } from '../lib/health'
|
||||
import type { HealthRecord } from '../lib/health'
|
||||
@@ -22,9 +22,13 @@ import type { Provider } from '../types'
|
||||
import { normalizeTheme } from '../lib/theme'
|
||||
import { useLeaveGuard } from '../lib/leave'
|
||||
import { localActor, setLocalActor } from '../lib/actor'
|
||||
import { AGENT_GUARD_TEXT, JUDGE_SETTING_TEXT, TOOL_GATE_TEXT, VERIFY_SETTING_TEXT, judgeByModelText, judgeUnlimitedText } from '../lib/terms'
|
||||
import {
|
||||
AGENT_GUARD_TEXT, JUDGE_SETTING_TEXT, PROVIDER_REF_TEXT, TOOL_GATE_TEXT, VERIFY_SETTING_TEXT, judgeByModelText, judgeUnlimitedText,
|
||||
} from '../lib/terms'
|
||||
import type { ThemePref } from '../lib/theme'
|
||||
import { VerifySection, verifyChanged, verifyDraftOf, verifyErrors, verifyLabels, verifyPayload, type VerifyDraft } from './VerifySettings'
|
||||
import {
|
||||
ProviderOptions, VerifySection, verifyChanged, verifyDraftOf, verifyErrors, verifyLabels, verifyPayload, type VerifyDraft,
|
||||
} from './VerifySettings'
|
||||
|
||||
// 提到模块级:tab 名同时是 URL 的最后一段,两处各写一份迟早对不上。
|
||||
// 数据源搬去了顶级的「数据」页(/data),/settings/datasources 跳过去
|
||||
@@ -622,6 +626,8 @@ function PrefsTab() {
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [savedFlash, setSavedFlash] = useState(0)
|
||||
const [saveError, setSaveError] = useState<unknown>(null)
|
||||
// 服务端按项拒收(点名的模型接入不存在、已停用:422 带 field):整句落到那一格下面,焦点移过去
|
||||
const [fieldError, setFieldError] = useState<{ field: string; message: string } | null>(null)
|
||||
|
||||
// 今天裁判花了多少:只读,放在每日上限旁边。老后端没有这个接口就不显示
|
||||
const loadSpend = () => api.settings.judgeSpend().then(setSpend, () => setSpend(null))
|
||||
@@ -709,10 +715,12 @@ function PrefsTab() {
|
||||
|
||||
const edit = (patch: Partial<typeof saved>) => {
|
||||
setSaveError(null)
|
||||
setFieldError(null)
|
||||
setDraft({ ...cur, ...patch })
|
||||
}
|
||||
const editVerify = (patch: Partial<VerifyDraft>) => {
|
||||
setSaveError(null)
|
||||
setFieldError(null)
|
||||
setVerifyDraft({ ...verifyCur, ...patch })
|
||||
}
|
||||
|
||||
@@ -724,6 +732,7 @@ function PrefsTab() {
|
||||
if (!values || !dirty || invalid) return
|
||||
setSaving(true)
|
||||
setSaveError(null)
|
||||
setFieldError(null)
|
||||
const actor = cur.actor.trim()
|
||||
if (actor !== savedActor.current) {
|
||||
// 写不进去(隐私模式)时署名只在这一次会话里有效;setLocalActor 会通知导航底部的首字
|
||||
@@ -775,6 +784,15 @@ function PrefsTab() {
|
||||
// 署名已经存上了:留在草稿里的只剩运行默认值,条上的计数跟着变少
|
||||
setDraft({ ...cur, actor })
|
||||
setSaveError(e)
|
||||
const at = e instanceof ApiError && e.field ? PROVIDER_FIELD_IDS[e.field] : undefined
|
||||
if (at) {
|
||||
setFieldError({ field: (e as ApiError).field!, message: (e as ApiError).message })
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById(at)
|
||||
el?.scrollIntoView({ block: 'center' })
|
||||
el?.focus()
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -783,16 +801,29 @@ function PrefsTab() {
|
||||
const scopeOptions = [...new Set(['default', ...scopes.map((s) => s.scope), cur.scope])]
|
||||
const collectionNames = collections.map((c) => c.collection)
|
||||
const collectionMissing = !collectionNames.includes(cur.collection)
|
||||
/**
|
||||
* 接入那一格下面的报错:服务端按这一项拒收的话照它的整句;否则存着(或选着)的接入不存在、已停用时说清这一项会怎样。
|
||||
* 只在接入表取到了时判「不存在」:表还没取回来,什么都找不到
|
||||
*/
|
||||
const providerError = (field: string, name: string, found: Provider | undefined, broken: string): string | undefined => {
|
||||
if (fieldError?.field === field) return fieldError.message
|
||||
if (!name || !providers.length) return undefined
|
||||
if (!found) return `${PROVIDER_REF_TEXT.missing(name)}:${broken}`
|
||||
return found.enabled ? undefined : `${PROVIDER_REF_TEXT.disabled(name)}:${broken}`
|
||||
}
|
||||
// 门控模型留空时后端的挑法(resolve_provider):第一个启用的真实接入,没有就第一个启用的
|
||||
const enabledProviders = providers.filter((p) => p.enabled)
|
||||
const defaultProvider = enabledProviders.find((p) => p.kind !== 'mock') ?? enabledProviders[0]
|
||||
const gateProvider = cur.gateProvider ? providers.find((p) => p.name === cur.gateProvider) : defaultProvider
|
||||
const gateProviderMissing = !!cur.gateProvider && providers.length > 0 && !gateProvider
|
||||
// 点名的接入不存在、已停用都调不了(停用的以前照常调用,现在运行时照实报错):两种都在这一格下面说
|
||||
const gateProviderError = providerError('run.tool_gate_provider', cur.gateProvider, gateProvider, PROVIDER_REF_TEXT.gateBroken)
|
||||
const gateModels = (gateProvider?.models ?? []).map((m) => m.id)
|
||||
// 裁判模型的接入:接入和模型都留空时后端依次用 Copilot 的模型、默认接入(judge_model_spec)。只填了模型时
|
||||
// 这一级写了就整组用这一级,按模型名找接入(resolve_provider)——不再跟随 Copilot,留空那一项不能还叫「跟随 Copilot」
|
||||
const judgeProvider = cur.judgeProvider ? providers.find((p) => p.name === cur.judgeProvider) : undefined
|
||||
const judgeProviderMissing = !!cur.judgeProvider && providers.length > 0 && !judgeProvider
|
||||
const judgeProviderError = providerError('judge.provider', cur.judgeProvider, judgeProvider, PROVIDER_REF_TEXT.judgeBroken)
|
||||
const judgeModelName = cur.judgeModel.trim()
|
||||
const judgeByModel = !cur.judgeProvider && !!judgeModelName
|
||||
// 裁判模型没有单独配置(接入、模型都留空):后端依次用助手的模型、默认接入
|
||||
@@ -895,22 +926,19 @@ function PrefsTab() {
|
||||
<div id="pref-gate-label" className="text-xs font-medium">{TOOL_GATE_TEXT.label}</div>
|
||||
<p id="pref-gate-hint" className="mb-2 mt-0.5 text-2xs leading-relaxed text-faint">{TOOL_GATE_TEXT.hint}</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label={TOOL_GATE_TEXT.provider} htmlFor="pref-gate-provider"
|
||||
error={gateProviderMissing ? `模型接入「${cur.gateProvider}」不存在:门控模型无法调用,所有工具调用将转为人工审批` : undefined}>
|
||||
<Field label={TOOL_GATE_TEXT.provider} htmlFor="pref-gate-provider" error={gateProviderError}>
|
||||
{(p) => (
|
||||
<select {...p} className="field" value={cur.gateProvider} onChange={(e) => pickGateProvider(e.target.value)}>
|
||||
<option value="">
|
||||
{TOOL_GATE_TEXT.providerDefault}{defaultProvider ? `(当前为 ${defaultProvider.name})` : ''}
|
||||
</option>
|
||||
{gateProviderMissing && <option value={cur.gateProvider}>{cur.gateProvider}(不存在)</option>}
|
||||
{providers.map((pv) => (
|
||||
<option key={pv.id} value={pv.name}>{pv.name}{pv.enabled ? '' : '(已停用)'}</option>
|
||||
))}
|
||||
<ProviderOptions providers={providers} current={cur.gateProvider} />
|
||||
</select>
|
||||
)}
|
||||
</Field>
|
||||
<Field label={TOOL_GATE_TEXT.model} htmlFor="pref-gate-model"
|
||||
hint={gateProvider?.default_model ? `留空则使用 ${gateProvider.default_model}` : undefined}>
|
||||
hint={gateProvider?.enabled && gateProvider.default_model ? `留空则使用 ${gateProvider.default_model}` : undefined}>
|
||||
{(p) => (
|
||||
<>
|
||||
<input {...p} className="field mono text-xs" value={cur.gateModel} list="pref-gate-models"
|
||||
@@ -976,8 +1004,7 @@ function PrefsTab() {
|
||||
<p id="pref-judge-model-hint" className="mb-2 mt-0.5 text-2xs leading-relaxed text-faint">{JUDGE_SETTING_TEXT.differ}</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label={JUDGE_SETTING_TEXT.provider} htmlFor="pref-judge-provider"
|
||||
error={judgeProviderMissing ? `模型接入「${cur.judgeProvider}」不存在:裁判调用将失败,所有结论句记为未裁判` : undefined}
|
||||
<Field label={JUDGE_SETTING_TEXT.provider} htmlFor="pref-judge-provider" error={judgeProviderError}
|
||||
hint={judgeByModelHint ? (
|
||||
<span data-judge-by-model={judgeOwner ? (judgeOwner.enabled ? 'found' : 'disabled') : 'default'}
|
||||
style={judgeOwner?.enabled ? undefined : { color: 'var(--st-waiting)' }}>{judgeByModelHint}</span>
|
||||
@@ -986,14 +1013,12 @@ function PrefsTab() {
|
||||
<select {...p} className="field" value={cur.judgeProvider} onChange={(e) => pickJudgeProvider(e.target.value)}>
|
||||
<option value="">{judgeByModel ? JUDGE_SETTING_TEXT.providerFromModel : JUDGE_SETTING_TEXT.providerDefault}</option>
|
||||
{judgeProviderMissing && <option value={cur.judgeProvider}>{cur.judgeProvider}(不存在)</option>}
|
||||
{providers.map((pv) => (
|
||||
<option key={pv.id} value={pv.name}>{pv.name}{pv.enabled ? '' : '(已停用)'}</option>
|
||||
))}
|
||||
<ProviderOptions providers={providers} current={cur.judgeProvider} />
|
||||
</select>
|
||||
)}
|
||||
</Field>
|
||||
<Field label={JUDGE_SETTING_TEXT.model} htmlFor="pref-judge-model"
|
||||
hint={judgeProvider?.default_model ? `留空则使用 ${judgeProvider.default_model}` : undefined}>
|
||||
hint={judgeProvider?.enabled && judgeProvider.default_model ? `留空则使用 ${judgeProvider.default_model}` : undefined}>
|
||||
{(p) => (
|
||||
<>
|
||||
<input {...p} className="field mono text-xs" value={cur.judgeModel} list="pref-judge-models"
|
||||
@@ -1048,7 +1073,8 @@ function PrefsTab() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{values.verify && <VerifySection cur={verifyCur} errors={verifyErrs} onEdit={editVerify} refresh={verifySaves} />}
|
||||
{values.verify && <VerifySection cur={verifyCur} onEdit={editVerify} refresh={verifySaves}
|
||||
errors={fieldError?.field === 'verify.provider' ? { ...verifyErrs, provider: fieldError.message } : verifyErrs} />}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1076,11 +1102,14 @@ function PrefsTab() {
|
||||
</span>
|
||||
</span>
|
||||
{saveError != null && (
|
||||
<span className="text-2xs text-[var(--err)]" role="alert">保存失败:{humanizeError(saveError).title}</span>
|
||||
<span className="text-2xs text-[var(--err)]" role="alert" data-prefs-save-error="">
|
||||
{/* 连同原因:服务端的说明常是两三句,只留标题会把「请在……中启用它」这类下一步截掉 */}
|
||||
保存失败:{errorMessage(saveError)}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-1" />
|
||||
<button className="btn btn-sm btn-ghost" disabled={saving}
|
||||
onClick={() => { setDraft(null); setVerifyDraft(null); setSaveError(null) }}>放弃</button>
|
||||
onClick={() => { setDraft(null); setVerifyDraft(null); setSaveError(null); setFieldError(null) }}>放弃</button>
|
||||
<button className="btn btn-sm btn-primary" disabled={saving || invalid}
|
||||
title={guardInvalid ? '护栏设置中有无效值,请修正后再保存'
|
||||
: judgeInvalid ? '证据裁判的上限有无效值,请修正后再保存'
|
||||
@@ -1100,6 +1129,11 @@ function PrefsTab() {
|
||||
)
|
||||
}
|
||||
|
||||
/** 服务端按项拒收时(422 带 field)那一项在页面上是哪个控件:点名的模型接入的三格 */
|
||||
const PROVIDER_FIELD_IDS: Record<string, string> = {
|
||||
'run.tool_gate_provider': 'pref-gate-provider', 'judge.provider': 'pref-judge-provider', 'verify.provider': 'pref-verify-provider',
|
||||
}
|
||||
|
||||
/** 裁判那一组在草稿里的键:它们改了才写 judge 一组 */
|
||||
const JUDGE_DRAFT = new Set<string>(['judgeProvider', 'judgeModel', 'jClaims', 'jCost', 'jTimeout', 'jClick', 'jDaily'])
|
||||
|
||||
|
||||
@@ -78,6 +78,15 @@ function matches(e: KeyboardEvent, combo: string): boolean {
|
||||
/** 改图的快捷键。正式运行期间按了要说为什么没反应,不能安静地什么都不做 */
|
||||
const EDIT_SHORTCUTS = new Set<StudioShortcutId>(['undo', 'redo', 'paste', 'duplicate', 'layout'])
|
||||
|
||||
/**
|
||||
* 接入表的签名:只看校验用得上的(id、名称、启用与否)。画布校验会查节点点名的接入是不是不存在、已停用(后端
|
||||
* engine/provider_refs.py),接入在设置里启用、停用、改名、删除之后,眼前的问题清单就过时了。记在模块里而不是
|
||||
* 组件里:去设置页改完再回来,编排页是重新挂上的,要和离开前校验时的那份比
|
||||
*/
|
||||
let checkedProviders: string | null = null
|
||||
const providerSig = (providers: { id: string; name: string; enabled: boolean }[]) =>
|
||||
providers.map((p) => `${p.id}\u0000${p.name}\u0000${p.enabled ? 1 : 0}`).join('\u0001')
|
||||
|
||||
export function StudioPage() {
|
||||
const { workflowId } = useParams()
|
||||
const [params, setParams] = useSearchParams()
|
||||
@@ -87,6 +96,8 @@ export function StudioPage() {
|
||||
const workflows = useCatalog((s) => s.workflows)
|
||||
const refresh = useCatalog((s) => s.refresh)
|
||||
const catalogLoaded = useCatalog((s) => s.loaded)
|
||||
// 选签名(字符串)而不是整张表:目录每刷新一次表都是新数组,内容没变就不该重新校验
|
||||
const providers = useCatalog((s) => providerSig(s.providers))
|
||||
const workflow = useStudio((s) => s.workflow)
|
||||
const dirty = useStudio((s) => s.dirty)
|
||||
// 只订阅数量,不订阅整个 nodes——后者每拖一下都会让整页重渲染
|
||||
@@ -241,6 +252,15 @@ export function StudioPage() {
|
||||
resolve(workflowId)
|
||||
}, [workflowId, workflow, workflows, catalogLoaded, dirty, load, navigate, resolve, location.state])
|
||||
|
||||
// 接入表变了(在设置里启用、停用、改名、删除了接入):画布上的问题清单可能已经过时,重新校验一次。
|
||||
// 第一次拿到接入表时只记下来——打开画布本身就会校验,后端校验时读的是库里的接入表,不靠这里
|
||||
useEffect(() => {
|
||||
if (!catalogLoaded) return
|
||||
const changed = checkedProviders !== null && checkedProviders !== providers
|
||||
checkedProviders = providers
|
||||
if (changed && useStudio.getState().nodes.length) void useStudio.getState().validate()
|
||||
}, [providers, catalogLoaded])
|
||||
|
||||
// 断开期间没取到的那张,连上之后再取一次。有未保存的改动就不动它
|
||||
useOnReconnect(() => {
|
||||
if (loadError && loadError.id === workflowId && !useStudio.getState().dirty) resolve(loadError.id)
|
||||
|
||||
@@ -4,8 +4,8 @@ import { AlertTriangle } from 'lucide-react'
|
||||
import { get } from '../api/http'
|
||||
import { modelOptions, providerOfModel, useCatalog } from '../store/catalog'
|
||||
import { Field, SectionBar } from '../components/ui'
|
||||
import { VERIFY_SETTING_TEXT } from '../lib/terms'
|
||||
import type { VerifySpend } from '../types'
|
||||
import { PROVIDER_REF_TEXT, VERIFY_SETTING_TEXT } from '../lib/terms'
|
||||
import type { Provider, VerifySpend } from '../types'
|
||||
|
||||
/*
|
||||
* 设置页「偏好设置」里「核查」一节:双盲复算、反方审稿、审批质询共用的核查模型、各自的开关和上限、每日金额上限
|
||||
@@ -163,6 +163,10 @@ export function VerifySection({ cur, errors, onEdit, refresh }: {
|
||||
const defaultProvider = enabledProviders.find((p) => p.kind !== 'mock') ?? enabledProviders[0]
|
||||
const provider = cur.provider ? providers.find((p) => p.name === cur.provider) : undefined
|
||||
const providerMissing = !!cur.provider && providers.length > 0 && !provider
|
||||
// 点名停用的接入以前照常调用,现在核查时照实报「已停用」、记缺口:和「不存在」一样在这一格下面说。服务端按这一项
|
||||
// 拒收时(errors.provider,设置页从 422 的 field 落过来)照它的整句
|
||||
const providerError = errors.provider ?? (providerMissing ? S.providerGone(cur.provider)
|
||||
: provider && !provider.enabled ? `${PROVIDER_REF_TEXT.disabled(cur.provider)}:${PROVIDER_REF_TEXT.verifyBroken}` : undefined)
|
||||
const modelName = cur.model.trim()
|
||||
const byModel = !cur.provider && !!modelName
|
||||
const notSet = !cur.provider && !modelName
|
||||
@@ -221,8 +225,7 @@ export function VerifySection({ cur, errors, onEdit, refresh }: {
|
||||
<p id="pref-verify-model-hint" className="mb-2 mt-0.5 text-2xs leading-relaxed text-faint">{S.differ}</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-3 max-sm:grid-cols-1">
|
||||
<Field label={T.provider} htmlFor={fieldId('provider')}
|
||||
error={providerMissing ? S.providerGone(cur.provider) : undefined}
|
||||
<Field label={T.provider} htmlFor={fieldId('provider')} error={providerError}
|
||||
hint={byModelHint ? (
|
||||
<span data-verify-by-model={owner ? (owner.enabled ? 'found' : 'disabled') : 'default'}
|
||||
style={owner?.enabled ? undefined : { color: 'var(--st-waiting)' }}>{byModelHint}</span>
|
||||
@@ -231,11 +234,12 @@ export function VerifySection({ cur, errors, onEdit, refresh }: {
|
||||
<select {...p} className="field" value={cur.provider} onChange={(e) => pickProvider(e.target.value)}>
|
||||
<option value="">{byModel ? S.providerFromModel : S.providerDefault}</option>
|
||||
{providerMissing && <option value={cur.provider}>{S.providerGoneOption(cur.provider)}</option>}
|
||||
{providers.map((pv) => <option key={pv.id} value={pv.name}>{pv.name}{pv.enabled ? '' : S.providerOff}</option>)}
|
||||
<ProviderOptions providers={providers} current={cur.provider} />
|
||||
</select>
|
||||
)}
|
||||
</Field>
|
||||
<Field label={T.model} htmlFor={fieldId('model')} hint={provider?.default_model ? S.modelOf(provider.default_model) : undefined}>
|
||||
<Field label={T.model} htmlFor={fieldId('model')}
|
||||
hint={provider?.enabled && provider.default_model ? S.modelOf(provider.default_model) : undefined}>
|
||||
{(p) => (
|
||||
<>
|
||||
<input {...p} className="field mono text-xs" value={cur.model} list="pref-verify-models" spellCheck={false}
|
||||
@@ -290,6 +294,22 @@ export function VerifySection({ cur, errors, onEdit, refresh }: {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 接入下拉里的各家。停用的照列(带「已停用」),但选不了:选了运行时也调不了,保存时服务端同样会拒收。
|
||||
* 已经选着的那一个(存着的值后来被停用了)照常显示,下面那一格写明它用不了
|
||||
*/
|
||||
export function ProviderOptions({ providers, current }: { providers: Provider[]; current: string }) {
|
||||
return (
|
||||
<>
|
||||
{providers.map((pv) => (
|
||||
<option key={pv.id} value={pv.name} disabled={!pv.enabled && pv.name !== current}>
|
||||
{pv.name}{pv.enabled ? '' : S.providerOff}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 三项各一组:标题是服务端叫法表里那一组的叫法,下面一句说它做什么 */
|
||||
function PartGroup({ part, children }: { part: Part; children: ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -1384,6 +1384,8 @@ export interface PublishFix {
|
||||
code: string
|
||||
kind: 'auto' | 'choice' | 'assist'
|
||||
node_id?: string | null
|
||||
/** 这条修复落在哪个字段:点名的模型接入一处一条(同一节点上的几处按它区分) */
|
||||
field?: string | null
|
||||
label: string
|
||||
preview?: { field?: string | null; before?: unknown; after?: unknown } | null
|
||||
/** handoff:选了这一项就是「交给 Copilot」(G4 的「它在做计算」)。只认这个标记,不认值——
|
||||
|
||||
Reference in New Issue
Block a user