/** * feature `gov-tool` —— 工具箱:**系统能对世界做什么**,一屏四段。 * * 功能工具 · 已注册的调用契约(读类自由调,写类登记意向) * LLM 工具 · 大模型驱动的系统内通用工具(**不是模型**;模型在系统区绑) * 人类工具 · 工单两模式 + 对话卡三族(调人也是调工具,只是被调的是人) * 接入 · 导入自定义工具(与第一方同门两审)/ 连接 MCP * * 三类工具并列不是编排:它是「一件事能交给谁办」的全集 —— * 交给系统(功能工具)、交给模型(LLM 工具)、交给人(人类工具)。少列一类,活动就以为自己没得选。 * * 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移: * 类名(`g-*` + 色调 `t-*`)与文案沿用;设计台以**卡片**为基调(运营台才以列表为主)。 */ import { useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { registerToolRoute } from '@contracts'; import { call } from '@web-shared/typed-fetch.ts'; import { Ico } from '@web-shared/Ico.tsx'; import { Acts, Card, Chip, Clear, Field, ListEdit, Note, PageHead, Prop, QueryGate, Sec, Seg, St, } from '@web-shared/gov/primitives.tsx'; import type { Nav } from '@web-shared/gov/gov-route.ts'; import { useGovLlm, useGovTools } from '@web-shared/gov/gov-api.ts'; export type HomeProps = { tab: string; nav: Nav }; /** * 人类工具目录 —— 系统内置的五种形态(语义出处 `packages/contracts/src/card.ts`)。 * 它们不在工具目录里注册:调人的能力是基座给的,不是谁装上来的。 */ const HUMAN_TOOLS = [ { name: '执行工单', mode: 'work-order · execution', ico: 'clipboard-check', desc: '作业指导书 + 空表单,请人把一步做完;落地失败卡是它的系统预置形态,不算第五种卡', }, { name: '验收工单', mode: 'work-order · acceptance', ico: 'shield-check', desc: '请人验收产出:通过 / 驳回重做 / 驳回迭代 —— 终局唯放行', }, { name: '决策卡', mode: 'chat-card · decision', ico: 'git-branch', desc: '分支条件选择表,含自定义收尾项 —— 决策是对话区的选择题,不进视图区', }, { name: '补全卡', mode: 'chat-card · elicitation', ico: 'pen-line', desc: '就地要几个小信息;「没法填」旁路与提交两条路必走其一,不许静默挂着', }, { name: '评价卡', mode: 'chat-card · rating', ico: 'star', desc: '任务评价:评分 1–5 + 改进建议,回流度量与蒸馏', }, ] as const; /** * LLM 工具目录 —— 系统内**由大模型驱动**的通用工具(不是模型本身)。 * * 模型是发动机,这些是装在发动机上的件:每一件都有明确的入、出与算账口径。 * 它们不在工具目录里注册 —— 与人类工具一样,是基座给的能力,不是谁装上来的。 * 跑在哪个模型上由系统区的四元绑定决定(活动只见逻辑名,不触厂商与密钥)。 */ const LLM_TOOLS = [ { name: '案例决断', mode: 'loop-turn', ico: 'rotate-ccw', desc: '案例活动每一拍:读「当下可为 ∩ 目标所需」,从 调工具 / 调人 / 立监听 里选一个行动', cost: '活动预算', }, { name: '步内生成', mode: 'flow-step', ico: 'list-checks', desc: '标准活动的 llm 步:模板 + 出参 schema → 结构化产出;出参必过 schema 校验,不合即不算数', cost: '活动预算', }, { name: '叙述性小结', mode: 'system-use', ico: 'file-text', desc: '任务日志 → 200 字阶段小结,只陈述日志里有的事实、不推测', cost: '系统账', }, { name: '蒸馏归纳', mode: 'system-use', ico: 'layers', desc: '终态封卷时从任务记忆归纳活动记忆条目:四层归位 + 证据链必挂', cost: '系统账', }, { name: '连通探针', mode: 'probe', ico: 'plug', desc: '试一次端点与鉴权,回延迟 —— 绑完模型先探一下,别等活动跑起来才发现调不通', cost: '系统账', }, ] as const; export function ToolsHome({ nav }: HomeProps) { const tools = useGovTools(); const llm = useGovLlm(); const rows = tools.data?.tools ?? []; return ( <> 一件事能交给谁办,全集在这里:交给系统(功能工具)、交给模型(LLM)、交给 人(人类工具)。工具承载权力,约束严于活动 —— 契约对活动、实现对系统、凭证只存治理台。 {rows.length === 0 ? ( 目录里还没有工具 —— 从下面「接入」导入一件,或由第一方随系统装入。 ) : (
{rows.map((t) => { const write = t.effect === 'write'; return ( ); })}
)}
{LLM_TOOLS.map((t) => (
{t.name} {t.mode} 内置 {t.desc} 计量入{t.cost}
))}
这些是工具,不是模型 —— 模型是它们跑在上面的发动机。 当前绑了 {(llm.data?.instances ?? []).length} 个模型实例( {(llm.data?.instances ?? []).map((m) => m.name).join(' · ') || '尚未绑定'}); 换厂商、换型号只改, 这些工具与活动一行都不动。
{HUMAN_TOOLS.map((h) => (
{h.name} {h.mode} 内置 {h.desc}
))}
); } /* ---------------- 接入 · 导入自定义工具 ---------------- */ /** * 导入 = 交一份**调用契约** + 一个能调通的端点 + 至少一条样例。 * 两审当场跑:静态审读契约,试运行审真调那个端点 —— 调不通就不入目录。 */ function ImportToolCard() { const queryClient = useQueryClient(); const [open, setOpen] = useState(false); const [name, setName] = useState(''); const [version, setVersion] = useState('1.0.0'); const [description, setDescription] = useState(''); const [effect, setEffect] = useState<'read' | 'write'>('read'); const [endpoint, setEndpoint] = useState(''); const [inputFields, setInputFields] = useState([]); const [sample, setSample] = useState('{}'); const importTool = useMutation({ mutationFn: () => call<{ accepted: boolean; findings: Array<{ message: string }>; trialRun: { failures: string[] } }>( registerToolRoute, { body: { contract: { name: name.trim(), version: version.trim(), description: description.trim(), effect, input: { type: 'object', required: inputFields.map((f) => f.trim()).filter((f) => f !== ''), properties: Object.fromEntries( inputFields.map((f) => f.trim()).filter((f) => f !== '').map((f) => [f, { type: 'string' }]), ), }, output: { type: 'object' }, errors: [], idempotent: true, timeoutMs: 10_000, retries: 1, connectionKeys: ['endpoint'], }, connectionValues: { endpoint: endpoint.trim() }, samples: [{ input: parseSample(sample) }], }, }, ), onSuccess: (out) => { if (out.accepted) { void queryClient.invalidateQueries({ queryKey: ['gov', 'tools'] }); setOpen(false); } }, }); const verdict = importTool.data; if (!open) { return ( setOpen(true)}> 开始导入 } > 交一份调用契约 + 一个能调通的端点,两审当场跑:静态审读契约,试运行审真调那个端点。 调不通就不入目录 —— 一件调不通的工具进了目录,活动用它时才失败,那才是真的坏。 ); } return ( setName(e.target.value)} placeholder="erp.stock-read" /> setVersion(e.target.value)} /> setDescription(e.target.value)} placeholder="读取 ERP 现货库存:PN → 库存量与库龄,寻源前先看自有呆滞" /> setEffect(k === 'write' ? 'write' : 'read')} /> setEndpoint(e.target.value)} placeholder="https://erp.internal/api/stock" /> setSample(e.target.value)} /> {verdict !== undefined && !verdict.accepted && ( 两审未过,没有入目录: {[...verdict.findings.map((f) => f.message), ...verdict.trialRun.failures].join(';') || '未知原因'} )} setOpen(false) }, { label: importTool.isPending ? '两审中…' : '导入并送审', ico: 'circle-check', primary: true, disabled: importTool.isPending || name.trim() === '' || endpoint.trim() === '', ask: { title: `导入工具:${name.trim()}`, note: 与第一方同一条门:静态审 + 试运行审。过了才入目录;入目录还不等于能用,要再给活动授白名单。, impact: { who: '仅工具目录;没进白名单的活动仍然调不到它', undo: '可停用;契约演进只增不破坏,破坏性变更要换新工具名', when: '确认后当场跑两审', }, okLabel: '确认导入', onOk: () => importTool.mutate(), }, }, ]} /> ); } /** 样例入参解析:写坏了就当空对象送审 —— 让试运行去报错,别在这里拦 */ function parseSample(text: string): Record { try { const parsed: unknown = JSON.parse(text); return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? (parsed as Record) : {}; } catch { return {}; } } /* ---------------- 接入 · MCP ---------------- */ function McpCard() { return ( 未连接 } > MCP 服务器上的工具,接进来之后与自定义工具同等待遇:经工具调用一缝去调、过两审入目录、 按白名单授给活动。当前系统还没有 MCP 客户端 —— 这里如实标着未连接,不摆一个点不动的按钮。 ); } /* ---------------- 工具详情 ---------------- */ export function ToolPage({ id }: { id: string }) { const q = useGovTools(); const t = (q.data?.tools ?? []).find((x) => x.name === id); if (t === undefined) return 工具不存在。; const write = t.effect === 'write'; return ( <> {write ? '写类' : '读类'} {t.pure && 纯工具} v{t.version} } desc={ write ? '写类工具在过程中只登记动作意向,不改世界;放行通过后由落地收敛统一执行。' : '读类工具过程中自由调用 —— 读不改世界,故不需放行。' } />

{t.description}

凭证隔离:治理台只回配置键,永不回值;密钥不入包、不入活动、不入留痕明文。
{t.connectionKeys.map((k) => (
{k} 已在治理台绑定
))} {t.connectionKeys.length === 0 && 此工具无需外部连接。}
); }