From 06b9cbde04d401e4ae7f7854af463700a979c448 Mon Sep 17 00:00:00 2001 From: 18628386721 <18628386721@163.com> Date: Tue, 28 Jul 2026 02:02:00 +0800 Subject: [PATCH] =?UTF-8?q?MVP=E5=BA=95=E5=BA=A7=E2=91=A5:=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=AE=B1=E5=9B=9B=E6=AE=B5=E5=8D=A1=E7=89=87=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E5=8A=9F=E8=83=BD/LLM/=E4=BA=BA=E7=B1=BB/?= =?UTF-8?q?=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三类工具并列不是编排,是「一件事能交给谁办」的全集:交给系统、交给模型、交给人。 导入表单走两审同门(端点真调);MCP 如实标未连接,不摆点不动的按钮。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .../src/features/gov-tool/internal/panel.tsx | 427 ++++++++++++++++-- apps/web/src/shared/gov/gov-route.ts | 2 +- docs/debt.md | 13 + 3 files changed, 392 insertions(+), 50 deletions(-) diff --git a/apps/web/src/features/gov-tool/internal/panel.tsx b/apps/web/src/features/gov-tool/internal/panel.tsx index 217bf84..de3724e 100644 --- a/apps/web/src/features/gov-tool/internal/panel.tsx +++ b/apps/web/src/features/gov-tool/internal/panel.tsx @@ -1,65 +1,397 @@ /** - * feature `gov-tool` —— 工具管理:注册、白名单、凭证绑定(只回键,永不回值) + * feature `gov-tool` —— 工具箱:**系统能对世界做什么**,一屏四段。 + * + * 功能工具 · 已注册的调用契约(读类自由调,写类登记意向) + * LLM 工具 · 模型实例(活动只见逻辑名,不触厂商与密钥) + * 人类工具 · 工单两模式 + 对话卡三族(调人也是调工具,只是被调的是人) + * 接入 · 导入自定义工具(与第一方同门两审)/ 连接 MCP + * + * 三类工具并列不是编排:它是「一件事能交给谁办」的全集 —— + * 交给系统(功能工具)、交给模型(LLM)、交给人(人类工具)。少列一类,活动就以为自己没得选。 * * 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移: - * DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。 - * 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。 + * 类名(`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 { Card, Chip, Clear, NList, NRow, Note, PageHead, QueryGate } from '@web-shared/gov/primitives.tsx'; -import type { Nav } from '@web-shared/gov/gov-route.ts'; import { - useGovTools, -} from '@web-shared/gov/gov-api.ts'; + 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; + export function ToolsHome({ nav }: HomeProps) { - const q = useGovTools(); - const tools = q.data?.tools ?? []; - const reads = tools.filter((t) => t.effect === 'read'); - const writes = tools.filter((t) => t.effect === 'write'); + const tools = useGovTools(); + const llm = useGovLlm(); + const rows = tools.data?.tools ?? []; + return ( - - <> - - 按效应等级分列:读类过程中自由调用;写类只登记动作意向,放行后统一落地。密钥只存治理台,不入包、不入活动。 - - {[ - { label: '读类 · 过程中自由调用', items: reads, tone: 'brand', ico: 'search' }, - { label: '写类 · 登记意向,放行后落地', items: writes, tone: 'danger', ico: 'pen-line' }, - ].map((group) => ( -
-
- - {group.label} - {group.items.length} -
- - {group.items.map((t) => ( - {t.version} }, - { k: '连接配置键', v: {t.connectionKeys.join(', ') || '—'} }, - ]} - right={t.pure ? 纯工具 : {t.effect === 'read' ? '读' : '写'}} - onClick={() => nav.push({ k: 'tool', id: t.name })} - /> + + <> + + 一件事能交给谁办,全集在这里:交给系统(功能工具)、交给模型(LLM)、交给 + 人(人类工具)。工具承载权力,约束严于活动 —— 契约对活动、实现对系统、凭证只存治理台。 + + + + {rows.length === 0 ? ( + 目录里还没有工具 —— 从下面「接入」导入一件,或由第一方随系统装入。 + ) : ( +
+ {rows.map((t) => { + const write = t.effect === 'write'; + return ( + + ); + })} +
+ )} +
+ + +
+ {(llm.data?.instances ?? []).map((m) => ( + ))} - -
- ))} - + {(llm.data?.instances ?? []).length === 0 && ( + 还没有绑定模型 —— 去系统区加一个实例,活动才有模型可用。 + )} +
+ + + +
+ {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(); @@ -104,6 +436,3 @@ export function ToolPage({ id }: { id: string }) { ); } - -/* ---------------- 数据对象 ---------------- */ - diff --git a/apps/web/src/shared/gov/gov-route.ts b/apps/web/src/shared/gov/gov-route.ts index a9b1488..4eff566 100644 --- a/apps/web/src/shared/gov/gov-route.ts +++ b/apps/web/src/shared/gov/gov-route.ts @@ -113,7 +113,7 @@ export const ZM: Readonly