From 8cb00d1ff652fe912a5d502c8194da521d1c5d7c Mon Sep 17 00:00:00 2001 From: 18628386721 <18628386721@163.com> Date: Wed, 29 Jul 2026 04:35:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=89=E9=9D=A2=E6=8B=86=E5=88=86=E2=91=AB:?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=8F=B0=E4=B9=9D=E5=8C=BA=E4=B8=80=E7=BA=A7?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=20=E2=80=94=E2=80=94=20=E4=B8=80=E5=8C=BA?= =?UTF-8?q?=E4=B8=80=E5=86=85=E5=AE=B9,=E6=B5=8B=E8=AF=95=E5=8F=B0?= =?UTF-8?q?=E6=94=B6=E6=88=90=E5=8D=95=E4=BE=8B=E4=BB=BF=E7=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设计中心从「蓝图 + 四库」摊成九区,顺序即设计的次序: 蓝图 → 触发器 → 流程包 → 活动库 → 工具箱 → 数据域 → 知识库 → 大模型 → 测试台。 触发器与大模型从「并进流程包 / 塞在系统区」里拿出来独立成区 —— 前者与流程包平级 (不入包结构、不随包版本),后者是设计者天天要碰的东西;知识库先立个诚实的空占位。 一区一内容,于是区内页签栏整条删掉(壳层在只剩一格时收起): · 流程包:总览的三枚计数并进货架顶端 · 测试台:入站邮件演练移到触发器区就地验(它验的正是那一区定的规则) · 大模型:从系统区拆出,系统区只剩通道实例 测试台一次只跑一条流。起单前先销毁上一单,刷新页面能认回在跑的那一单, 「停止仿真」把工作流连同它派出的任务、工单、实例、留痕一并拔掉 —— 不留半单挂在别人工作台上。实测:起单 → 1 单落到 AI 工作台 → 停止 → 库里零残留。 功能工具补中文名(migration 0018,读回退到逻辑名),治理台一律主显示中文名。 顺带清掉几处说教式文案(「这些是工具,不是模型 —— 模型是它们跑在上面的发动机…」那类)。 五闸:typecheck / lint / depcruise / test 绿;api:check 快照随本提交入库。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .../contracts/src/api/governance.d.ts | 26 ++++++ apps/api/src/app/routes/governance-ops.ts | 83 ++++++++++++++++++- apps/api/src/app/seed-mvp.ts | 3 + apps/api/src/modules/toolbox/CONTRACT.md | 4 + .../toolbox/__tests__/register-trial.test.ts | 1 + .../toolbox/__tests__/tool-contract.test.ts | 1 + .../internal/adapter/toolbox-schema.ts | 2 + .../toolbox/internal/adapter/toolbox-store.ts | 4 + .../toolbox/internal/domain/tool-contract.ts | 7 +- apps/web/src/app/gov-shell.tsx | 15 +++- apps/web/src/app/styles/console.css | 7 +- apps/web/src/features/gov-archive/index.ts | 5 ++ .../features/gov-archive/internal/panel.tsx | 21 +++++ .../features/gov-process/internal/panel.tsx | 19 +++-- apps/web/src/features/gov-system/index.ts | 2 +- .../features/gov-system/internal/panel.tsx | 9 ++ .../features/gov-testbed/internal/panel.tsx | 42 ++-------- .../gov-testbed/internal/simulate.tsx | 39 +++++++-- .../src/features/gov-tool/internal/panel.tsx | 22 +++-- .../internal/inbound.tsx | 47 +++++------ .../features/gov-trigger/internal/panel.tsx | 3 + .../src/shared/gov/__tests__/surfaces.test.ts | 11 ++- apps/web/src/shared/gov/gov-route.ts | 57 +++++++++---- apps/web/src/shared/gov/surfaces.ts | 10 ++- db/migrations/0018_tool_display_name.sql | 4 + db/migrations/meta/_journal.json | 7 ++ docs/产品设计.md | 29 +++++-- packages/contracts/src/api/governance.ts | 22 +++++ 28 files changed, 367 insertions(+), 135 deletions(-) create mode 100644 apps/web/src/features/gov-archive/index.ts create mode 100644 apps/web/src/features/gov-archive/internal/panel.tsx rename apps/web/src/features/{gov-testbed => gov-trigger}/internal/inbound.tsx (91%) create mode 100644 db/migrations/0018_tool_display_name.sql diff --git a/api-report/packages/contracts/src/api/governance.d.ts b/api-report/packages/contracts/src/api/governance.d.ts index ddea237..7e8832c 100644 --- a/api-report/packages/contracts/src/api/governance.d.ts +++ b/api-report/packages/contracts/src/api/governance.d.ts @@ -1919,6 +1919,7 @@ export declare const registerToolRoute: { readonly body: z.ZodObject<{ contract: z.ZodObject<{ name: z.ZodString; + displayName: z.ZodOptional; version: z.ZodString; description: z.ZodString; input: z.ZodRecord; @@ -2006,6 +2007,7 @@ export declare const listToolsRoute: { readonly output: z.ZodObject<{ tools: z.ZodArray; }, z.core.$strip>; }; +/** + * 停止仿真 —— **把这一单连根拔掉**:工作流、它的活动任务、工单、实例、trace 全部销毁。 + * 仿真不是生产:试跑留下的东西不该在库里、在别人工作台上过夜。 + */ +export declare const testStopRoute: { + readonly method: "POST"; + readonly path: "/api/gov/testbed/stop"; + readonly body: z.ZodObject<{ + workflowId: z.ZodString; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + destroyed: z.ZodBoolean; + tasks: z.ZodNumber; + }, z.core.$strip>; +}; +/** 当前在跑的仿真(测试台一次只跑一条)—— 刷新页面也认得回来 */ +export declare const testCurrentRoute: { + readonly method: "GET"; + readonly path: "/api/gov/testbed/current"; + readonly output: z.ZodObject<{ + workflowId: z.ZodOptional; + subject: z.ZodOptional; + }, z.core.$strip>; +}; /** 全链 Trace:一条工作流的 span 树(节点 → 步骤 / 工具 / 模型 / 人卡) */ export declare const traceOfWorkflowRoute: { readonly method: "GET"; diff --git a/apps/api/src/app/routes/governance-ops.ts b/apps/api/src/app/routes/governance-ops.ts index eb6ee77..d7bcff1 100644 --- a/apps/api/src/app/routes/governance-ops.ts +++ b/apps/api/src/app/routes/governance-ops.ts @@ -4,6 +4,7 @@ * 「治理台九功能**不设**后端 governance 模块:每个功能是对应模块的管理用例 + 路由组装。」 * 「治理动作本身留痕入事件流 —— 治理也被治理。」 */ +import { sql } from 'drizzle-orm'; import type { Hono } from 'hono'; import type { ApiError, GovernanceFunction, GrantAction } from '@contracts'; import { @@ -25,7 +26,9 @@ import { listTriggersRoute, registerToolRoute, saveProcessRoute, + testCurrentRoute, testRunRoute, + testStopRoute, traceOfWorkflowRoute, setProcessStateRoute, reassignTaskRoute, @@ -42,6 +45,53 @@ import { NotClosedError, PinnedVersionError, StaleVersionError } from '@modules/ import type { Container } from '../container.ts'; import type { AppEnv } from '../http-env.ts'; +/** `db.execute` 的行读取 —— Drizzle 的返回型随驱动变,窄化收在这一处 */ +function rowsOf(result: unknown): T[] { + const bag = result as { rows?: T[] } | T[]; + return Array.isArray(bag) ? bag : (bag.rows ?? []); +} + +/** + * 销毁仿真单 —— 把测试台起的那一条(或指定的一条)**连根拔掉**。 + * + * 顺序按外键的反方向来:先拆挂在任务上的东西(工单、卡、记忆、运行时、意向、监听), + * 再拆任务与节点实例,最后拆工作流本身;trace 与事件按工作流清。 + * 用 `source->>'ref' = 'testbed'` 认门:**只动测试台自己起的单**, + * 生产单一个字都不碰(它们的来源是触发器或纪要登记)。 + */ +async function destroySimulations(container: Container, workflowId?: string): Promise { + const scope = workflowId === undefined ? sql`` : sql` and id = ${workflowId}`; + const wfs = await container.db.execute<{ id: string }>( + sql`select id from sys.workflow where source->>'ref' = 'testbed'${scope}`, + ); + const ids = rowsOf<{ id: string }>(wfs).map((r) => r.id); + if (ids.length === 0) return 0; + + let taskCount = 0; + for (const id of ids) { + const tasks = await container.db.execute<{ id: string }>(sql`select id from sys.task where workflow_id = ${id}`); + const taskIds = rowsOf<{ id: string }>(tasks).map((r) => r.id); + taskCount += taskIds.length; + for (const taskId of taskIds) { + await container.db.execute(sql`delete from sys.work_order where task_id = ${taskId}`); + await container.db.execute(sql`delete from sys.chat_card where task_id = ${taskId}`); + await container.db.execute(sql`delete from sys.chat_message where task_id = ${taskId}`); + await container.db.execute(sql`delete from sys.task_memory where task_id = ${taskId}`); + await container.db.execute(sql`delete from sys.task_memory_seal where task_id = ${taskId}`); + await container.db.execute(sql`delete from sys.task_view where task_id = ${taskId}`); + await container.db.execute(sql`delete from sys.flow_state where task_id = ${taskId}`); + await container.db.execute(sql`delete from sys.loop_state where task_id = ${taskId}`); + await container.db.execute(sql`delete from sys.action_intent where task_id = ${taskId}`); + await container.db.execute(sql`delete from sys.listener where task_id = ${taskId}`); + } + await container.db.execute(sql`delete from sys.task where workflow_id = ${id}`); + await container.db.execute(sql`delete from sys.node_instance where workflow_id = ${id}`); + await container.db.execute(sql`delete from sys.trace_span where workflow_id = ${id}`); + await container.db.execute(sql`delete from sys.workflow where id = ${id}`); + } + return taskCount; +} + export function registerGovernanceOpsRoutes(app: Hono, container: Container): void { /** * 岗位授权:客体 = 治理功能,动作按路由分级 —— **看是 `read`,动才是 `govern`**。 @@ -230,6 +280,8 @@ export function registerGovernanceOpsRoutes(app: Hono, container: Contai return c.json(body, 400); } const me = await container.org.findByAccount(c.get('account')); + // **一次只跑一条**:再起一单就先把上一单连根拔掉 —— 仿真台上不留过夜的东西 + await destroySimulations(container); try { const record = await container.process.start({ packageId: parsed.data.packageId, @@ -247,6 +299,33 @@ export function registerGovernanceOpsRoutes(app: Hono, container: Contai } }); + /** + * 停止仿真 —— **连根拔掉**:工作流、活动任务、工单、节点实例、trace 一并销毁。 + * 仿真不是生产:试跑留下的任务不该在别人工作台上过夜,也不该混进在途口径。 + */ + app.post(testStopRoute.path, async (c) => { + const denied = await governProcess(c.get('account')); + if (denied) return c.json(denied, denyStatus(denied)); + const parsed = testStopRoute.body.safeParse(await c.req.json()); + if (!parsed.success) { + const body: ApiError = { code: 'validation-failed', message: '停止仿真入参不合协议' }; + return c.json(body, 400); + } + const tasks = await destroySimulations(container, parsed.data.workflowId); + return c.json({ destroyed: true, tasks }); + }); + + /** 当前在跑的仿真 —— 刷新页面也认得回来(测试台一次只跑一条) */ + app.get(testCurrentRoute.path, async (c) => { + const denied = await governProcess(c.get('account')); + if (denied) return c.json(denied, denyStatus(denied)); + const rows = await container.db.execute<{ id: string; subject: string }>( + sql`select id, subject from sys.workflow where source->>'ref' = 'testbed' order by started_at desc limit 1`, + ); + const hit = rowsOf<{ id: string; subject: string }>(rows)[0]; + return c.json(hit === undefined ? {} : { workflowId: hit.id, subject: hit.subject }); + }); + /** 全链 Trace:一条工作流的 span 树 —— 节点 → 步骤 / 工具 / 模型 / 人卡 */ app.get(traceOfWorkflowRoute.path, async (c) => { const denied = await governProcess(c.get('account')); @@ -545,7 +624,8 @@ export function registerGovernanceOpsRoutes(app: Hono, container: Contai } const { contract, samples, connectionValues } = parsed.data; const verdict = await container.toolbox.register({ - contract, + // 没给中文名就以逻辑名兜底 —— 主名不能空 + contract: { ...contract, displayName: contract.displayName ?? contract.name }, // 期望形状统一按「出参是个对象」收:细的形状由契约 output schema 自己说 samples: samples.map((s) => ({ input: s.input, expectShape: { type: 'object' } })), ...(connectionValues === undefined ? {} : { connectionValues }), @@ -563,6 +643,7 @@ export function registerGovernanceOpsRoutes(app: Hono, container: Contai const tools = await container.toolbox.list(); return c.json({ tools: tools.map((t) => ({ + displayName: t.displayName, name: t.name, version: t.version, description: t.description, diff --git a/apps/api/src/app/seed-mvp.ts b/apps/api/src/app/seed-mvp.ts index 015fb15..1723e35 100644 --- a/apps/api/src/app/seed-mvp.ts +++ b/apps/api/src/app/seed-mvp.ts @@ -15,6 +15,7 @@ import { seedMvpFlow } from './seed-mvp-flow.ts'; /** 第一方功能工具:查历史价 —— MVP 用录制回放替身代真 CRM(README:mock 只在外界) */ const HISTORY_TOOL = { name: 'crm.history-price-read', + displayName: '历史价查询', version: '1.0.0', description: '读取历史价格情报:某 PN 最近一次报价距今天数、行情是否平稳、生命周期状态', input: { type: 'object', required: ['pn'], properties: { pn: { type: 'string' } } }, @@ -39,6 +40,7 @@ const HISTORY_TOOL = { const MAIL_TOOL = { ...HISTORY_TOOL, name: 'mail.send', + displayName: '发送邮件', description: '发出邮件(报价单 / 询价函 / 催办):写类动作,放行时才真正触达世界', input: { type: 'object', @@ -61,6 +63,7 @@ const MAIL_TOOL = { */ const PLACEHOLDER_TOOL = { name: 'slot.placeholder', + displayName: '槽位占位产出', version: '0.1.0', description: '活动槽位占位产出:把契约要求的字段落成一行(不做业务判断;真实现后摘除)', input: { diff --git a/apps/api/src/modules/toolbox/CONTRACT.md b/apps/api/src/modules/toolbox/CONTRACT.md index 51a9bfd..22e88d4 100644 --- a/apps/api/src/modules/toolbox/CONTRACT.md +++ b/apps/api/src/modules/toolbox/CONTRACT.md @@ -29,6 +29,10 @@ `human`(**人类工具**,系统内化、零声明即得 —— 调人是 Harness 给的能力)、 `llm`(**LLM 五件**:抽取/归类/摘要/裁断/改写,经模型网关一缝; 仅当活动声明了模型用途名才上总线 —— 没声明却会问模型是越权,用量也无从归因) + - **一名两用**(2026-07-29):`name` 是**机器名**(全局唯一逻辑名,活动按它声明白名单、 + 调用按它寻址),`displayName` 是**人名**(治理台一律主显示中文名,逻辑名退居副标)。 + `crm.history-price-read` 对机器精确、对人不友好,一屏几十件工具时尤甚; + 旧工具未填时读回退到 `name`(加法,非破坏) - **一切工具执行经 `ToolInvoker`** —— 绕过它,回放审就对它失明(§12.5#10) - `replay` 模式**绝不回退实调**:缺录即 `ToolReplayMissError` - **白名单在结构上拦截**:功能工具按活动白名单,纯工具按 `ownerActivityId` 私有作用域; diff --git a/apps/api/src/modules/toolbox/__tests__/register-trial.test.ts b/apps/api/src/modules/toolbox/__tests__/register-trial.test.ts index e66a458..fa05fc1 100644 --- a/apps/api/src/modules/toolbox/__tests__/register-trial.test.ts +++ b/apps/api/src/modules/toolbox/__tests__/register-trial.test.ts @@ -11,6 +11,7 @@ import type { ToolContract } from '../internal/domain/tool-contract.ts'; const CONTRACT: ToolContract = { name: 'erp.stock-read', + displayName: 'ERP 库存查询', version: '1.0.0', description: '读取 ERP 现货库存:PN → 库存量', input: { type: 'object', required: ['pn'], properties: { pn: { type: 'string' } } }, diff --git a/apps/api/src/modules/toolbox/__tests__/tool-contract.test.ts b/apps/api/src/modules/toolbox/__tests__/tool-contract.test.ts index a6cf50c..ed6feaf 100644 --- a/apps/api/src/modules/toolbox/__tests__/tool-contract.test.ts +++ b/apps/api/src/modules/toolbox/__tests__/tool-contract.test.ts @@ -6,6 +6,7 @@ import { auditPassed, staticAudit } from '../internal/domain/registration-audit. const contract = (over: Partial = {}): ToolContract => ({ name: 'crm.customer-read', + displayName: '客户档案查询', version: '1.0.0', description: '读取客户资料库中的客户档案,用于取客户等级与付款条款', input: { type: 'object', properties: { customerCode: { type: 'string' } }, required: ['customerCode'] }, diff --git a/apps/api/src/modules/toolbox/internal/adapter/toolbox-schema.ts b/apps/api/src/modules/toolbox/internal/adapter/toolbox-schema.ts index f766217..01988b2 100644 --- a/apps/api/src/modules/toolbox/internal/adapter/toolbox-schema.ts +++ b/apps/api/src/modules/toolbox/internal/adapter/toolbox-schema.ts @@ -7,6 +7,8 @@ import { sys } from '@shared/db.ts'; export const toolTable = sys.table( 'tool', { + /** 中文名 —— 人读的主名(治理台主显示);存量行以逻辑名兜底 */ + displayName: text('display_name'), name: text('name').primaryKey(), version: text('version').notNull(), description: text('description').notNull(), diff --git a/apps/api/src/modules/toolbox/internal/adapter/toolbox-store.ts b/apps/api/src/modules/toolbox/internal/adapter/toolbox-store.ts index cdb9ec4..7dbc831 100644 --- a/apps/api/src/modules/toolbox/internal/adapter/toolbox-store.ts +++ b/apps/api/src/modules/toolbox/internal/adapter/toolbox-store.ts @@ -15,6 +15,8 @@ type Row = typeof toolTable.$inferSelect; function toContract(row: Row): ToolContract { return { name: row.name, + // 存量行没中文名就以逻辑名兜底 —— 界面上宁可显示机器名,不显示空白 + displayName: row.displayName ?? row.name, version: row.version, description: row.description, input: row.input as Record, @@ -50,6 +52,7 @@ export function createToolRegistry(db: Db): ToolRegistry { name: contract.name, version: contract.version, description: contract.description, + displayName: contract.displayName, input: contract.input, output: contract.output, effect: contract.effect, @@ -70,6 +73,7 @@ export function createToolRegistry(db: Db): ToolRegistry { target: toolTable.name, set: { version: values.version, + displayName: values.displayName, description: values.description, input: values.input, output: values.output, diff --git a/apps/api/src/modules/toolbox/internal/domain/tool-contract.ts b/apps/api/src/modules/toolbox/internal/domain/tool-contract.ts index 3e448b7..04ff5f7 100644 --- a/apps/api/src/modules/toolbox/internal/domain/tool-contract.ts +++ b/apps/api/src/modules/toolbox/internal/domain/tool-contract.ts @@ -11,8 +11,13 @@ export type EffectLevel = (typeof EFFECT_LEVELS)[number]; export const EFFECT_LEVEL_LABEL: Readonly> = { read: '读', write: '写' }; export type ToolContract = { - /** 全局唯一逻辑名 */ + /** 全局唯一逻辑名 —— 机器用:活动按它声明白名单、调用按它寻址 */ readonly name: string; + /** + * 中文名 —— **人用**:治理台一律主显示它,逻辑名退居副标。 + * 「crm.history-price-read」对机器精确,对人不友好;一屏几十件工具时尤其。 + */ + readonly displayName: string; readonly version: string; /** 面向 AI 的适用说明:何时用、语义、边界 */ readonly description: string; diff --git a/apps/web/src/app/gov-shell.tsx b/apps/web/src/app/gov-shell.tsx index 77c3d13..0408983 100644 --- a/apps/web/src/app/gov-shell.tsx +++ b/apps/web/src/app/gov-shell.tsx @@ -27,13 +27,14 @@ import { BlueprintHome } from '@features/gov-blueprint'; import { TasksHome, TaskLogPage, TaskPage, WfPage } from '@features/gov-task'; import { EmpPage, PeopleHome, RolePage } from '@features/gov-staff'; import { PkgPage, WorkflowsHome } from '@features/gov-process'; -import { TriggerPage } from '@features/gov-trigger'; +import { TriggerPage, TriggerRulesHome } from '@features/gov-trigger'; import { ActPage, LibraryHome } from '@features/gov-activity'; import { ToolPage, ToolsHome } from '@features/gov-tool'; import { DataHome, LedgersHome, ObjPage } from '@features/gov-data'; +import { ArchiveHome } from '@features/gov-archive'; import { TestbedHome } from '@features/gov-testbed'; import { CockpitHome, KnowledgeHome, MemPage } from '@features/gov-memory'; -import { ChanPage, LlmPage, SystemHome } from '@features/gov-system'; +import { ChanPage, LlmPage, ModelsHome, SystemHome } from '@features/gov-system'; /* ── URL 编解码:`#///[/:…]` ────────────────── */ @@ -235,7 +236,12 @@ export function GovShell({ surface }: { surface: Surface }) {

{meta.desc}

-
+ {/* + * 页签栏只在**真有得选**时出现:一区一内容的区(拆分后大多如此) + * 摆一个只有一颗按钮的导航条,是在给人做无谓的选择。 + * 台账例外 —— 它的页签是随发布生长的数据域,数量本来就不定。 + */} +
{zone === 'ledgers' ? // 台账的页签是**数据域**(短名 2-3 字),随发布生长 —— 设计产生运营的形状,落在页签上 domains.map((d) => ( @@ -277,10 +283,13 @@ function ZoneHome({ zone, tab, nav }: { zone: GovZone; tab: string; nav: Nav }) const P = { // 设计中心 blueprint: BlueprintHome, + triggers: TriggerRulesHome, workflows: WorkflowsHome, library: LibraryHome, tools: ToolsHome, data: DataHome, + archive: ArchiveHome, + models: ModelsHome, testbed: TestbedHome, // 运营中心 cockpit: CockpitHome, diff --git a/apps/web/src/app/styles/console.css b/apps/web/src/app/styles/console.css index 4508bc1..f84e912 100644 --- a/apps/web/src/app/styles/console.css +++ b/apps/web/src/app/styles/console.css @@ -84,6 +84,7 @@ .g-head-t p { margin: 5px 0 0; font-size: 12.5px; line-height: 1.7; color: var(--ink-3); max-width: 108ch; } /* 区内页签:一个页签 = 一个维度 */ +.g-tabs.hidden { display: none; } .g-tabs { display: flex; flex-wrap: wrap; gap: 3px; padding: 4px; border-radius: 12px; background: #e3e8ef; align-self: flex-start; } .g-tab { display: inline-flex; align-items: center; gap: 7px; height: 32px; padding: 0 14px; border: 0; border-radius: 9px; background: transparent; color: var(--ink-3); font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; transition: background var(--t-fast), color var(--t-fast); } .g-tab svg { color: var(--ink-4); } @@ -680,12 +681,6 @@ } /* 模式选择:不是第二条页签栏 —— 前置 mono 墨签点明这是个**控件**,方角、黄铜底线定当前 */ -.g-tbmode { display: flex; align-items: center; gap: 8px; } -.g-tbmode > em { font: 600 9.5px/1 var(--sh-mono, var(--mono)); font-style: normal; letter-spacing: .14em; text-transform: uppercase; color: #6a7686; } -.g-tbmode-b { display: inline-flex; align-items: center; gap: 6px; height: 27px; padding: 0 12px; border: 1px solid #d8dee6; border-bottom-width: 2px; background: #f7f9fb; color: #6a7686; font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast); } -.g-tbmode-b:hover { color: #46586a; background: #fff; } -.g-tbmode-b.on { background: #fff; color: #161b22; font-weight: 700; border-color: #c6cfd9; border-bottom-color: #8a5f1c; } -.g-tbmode-b:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; } /* ── 图纸卡:方角白纸 + mono 三字码题头(与流程区的 VER — / FIG — 同一idiom)── */ .g-sheet { display: flex; flex-direction: column; min-width: 0; background: #fff; border: 1px solid var(--sh-line); box-shadow: var(--cshadow); } diff --git a/apps/web/src/features/gov-archive/index.ts b/apps/web/src/features/gov-archive/index.ts new file mode 100644 index 0000000..d59fdbd --- /dev/null +++ b/apps/web/src/features/gov-archive/index.ts @@ -0,0 +1,5 @@ +/** + * feature `gov-archive` 唯一出口(编码规范 §9.1)。 + * 知识库 —— 企业的资料与文档。**待建**:占位页如实说明,不假装有内容。 + */ +export { ArchiveHome } from './internal/panel.tsx'; diff --git a/apps/web/src/features/gov-archive/internal/panel.tsx b/apps/web/src/features/gov-archive/internal/panel.tsx new file mode 100644 index 0000000..5201024 --- /dev/null +++ b/apps/web/src/features/gov-archive/internal/panel.tsx @@ -0,0 +1,21 @@ +/** + * 知识库 —— 待建。 + * + * 占位页只做一件事:**如实说它还没有**。不摆假数据、不摆点不动的按钮 —— + * 空着比装着强(与死信卡、沟通记录同一条规矩)。 + */ +import { Card, Clear } from '@web-shared/gov/primitives.tsx'; +import type { Nav } from '@web-shared/gov/gov-route.ts'; + +export function ArchiveHome({ nav }: { tab: string; nav: Nav }) { + void nav; + return ( + + + 这里将放企业的资料与文档 —— 规范、图纸、样例、外部材料,供活动按需引用。 +
+ 目前还没有建:没有存储、没有检索、没有引用机制。等要用到的第一条流出现时再动手。 +
+
+ ); +} diff --git a/apps/web/src/features/gov-process/internal/panel.tsx b/apps/web/src/features/gov-process/internal/panel.tsx index efc3d93..9aeed9c 100644 --- a/apps/web/src/features/gov-process/internal/panel.tsx +++ b/apps/web/src/features/gov-process/internal/panel.tsx @@ -1,7 +1,7 @@ /** * feature `gov-process` —— 流程包(设计态):**一条流一个包,版本收在包内部**。 * - * 台面三页签:总览 · 流程包 · 触发器。 + * 一区一内容:三枚计数摆在货架顶端,底下就是包货架(触发器已独立成区)。 * 货架一流程一卡(不按版本平铺);进详情后左 1/3 是版本列表、右 2/3 是流程图, * 图下是节点属性,再往下才是声明式的契约细节(调用接口 · 边映射 · 触发器)。 * @@ -41,7 +41,6 @@ import { FlowFigureBlock } from '@web-shared/gov/flow/FlowFigure.tsx'; import { packageToGraph } from '@web-shared/gov/flow/pkg-to-graph.ts'; import { mergeGraphIntoPackage } from '@web-shared/gov/flow/graph-to-pkg.ts'; import type { FlowEdge, FlowGraph, FlowNode } from '@web-shared/gov/flow/flow-layout.ts'; -import { TriggerRulesHome } from '@features/gov-trigger'; export type HomeProps = { tab: string; nav: Nav }; @@ -206,11 +205,17 @@ function EdgeProps({ edge, onPatch }: { edge: FlowEdge; onPatch: (f: (e: FlowEdg /* ---------------- 台面:总览 · 流程包 · 触发器 ---------------- */ -export function WorkflowsHome({ tab, nav }: HomeProps) { - // 触发器的**定义面**:谁调起这条流,与流程包一处维护(启停在运营中心) - if (tab === 'rules') return ; - if (tab === 'pkgs') return ; - return ; +/** + * 流程包一区一内容:总览的三枚计数并进货架顶端 —— 一个页签的导航条不必存在, + * 「先点总览再点流程包」也不是人想做的事。触发器已独立成区。 + */ +export function WorkflowsHome({ nav }: HomeProps) { + return ( + <> + + + + ); } function OverviewHome() { diff --git a/apps/web/src/features/gov-system/index.ts b/apps/web/src/features/gov-system/index.ts index 572e610..a445aa0 100644 --- a/apps/web/src/features/gov-system/index.ts +++ b/apps/web/src/features/gov-system/index.ts @@ -2,4 +2,4 @@ * feature `gov-system` 唯一出口(编码规范 §9.1)。 * 系统管理:LLM 实例四元绑定、通道实例 */ -export { SystemHome, LlmPage, ChanPage } from './internal/panel.tsx'; +export { SystemHome, ModelsHome, LlmPage, ChanPage } from './internal/panel.tsx'; diff --git a/apps/web/src/features/gov-system/internal/panel.tsx b/apps/web/src/features/gov-system/internal/panel.tsx index 597ec72..ab23a71 100644 --- a/apps/web/src/features/gov-system/internal/panel.tsx +++ b/apps/web/src/features/gov-system/internal/panel.tsx @@ -14,6 +14,15 @@ import { export type HomeProps = { tab: string; nav: Nav }; +/** + * 大模型区 —— 模型实例的四元绑定(设计中心一级目录)。 + * 从系统区拆出来:模型是**设计者天天要碰**的东西(活动声明用途名、换档改绑定), + * 不该和通道实例挤在「系统管理」里。 + */ +export function ModelsHome({ nav }: HomeProps) { + return ; +} + export function SystemHome({ tab, nav }: HomeProps) { const llm = useGovLlm(); const channels = useGovChannels(); diff --git a/apps/web/src/features/gov-testbed/internal/panel.tsx b/apps/web/src/features/gov-testbed/internal/panel.tsx index bbf2382..227007d 100644 --- a/apps/web/src/features/gov-testbed/internal/panel.tsx +++ b/apps/web/src/features/gov-testbed/internal/panel.tsx @@ -2,49 +2,17 @@ * 联调测试台 —— 把定义**试着跑一遍**。 * * 测试台是**后端检测活动与流程**的地方,不是代人干活的地方:在这儿装配并起单, - * 真实的任务照旧落到被指派人的工作台上。所以它只管两件事: - * **把流跑起来** + **看穿它怎么跑的**。 + * 真实的任务照旧落到被指派人的工作台上。 * - * 一条流有两种起法,对应两个模式 —— 两条路都得能试: - * · 仿真试跑:直接调流程包的**调用接口**起单(触发器起单走的也是这个口),带节点指派; - * · 入站邮件演练:从**外界**推一封信进来,看归属解析怎么判、哪个触发器接、有没有起单。 + * **一次只跑一条**:测试台上的工作流生命周期完全跟着仿真状态 —— 起一单就是它, + * 停止仿真,这条流连同它派出去的活动任务一起销毁,不留半单在别人工作台上挂着。 * - * 模式不做成区页签(区页签由壳层的 `TABS` 定,测试台只占一格),做成本屏的一个控件 —— - * 它选的是「用哪种刺激」,不是「看哪个维度」。 + * (外界来信那条路在**触发器**区就地演练 —— 它验的是规则,归那边。) */ -import { useState } from 'react'; import type { ReactNode } from 'react'; -import { Ico } from '@web-shared/Ico.tsx'; import type { Nav } from '@web-shared/gov/gov-route.ts'; -import { InboundDrill } from './inbound.tsx'; import { SimulateRun } from './simulate.tsx'; -type Mode = 'run' | 'mail'; - -const MODES: ReadonlyArray<{ k: Mode; label: string; ico: string }> = [ - { k: 'run', label: '仿真试跑', ico: 'flask-conical' }, - { k: 'mail', label: '入站邮件演练', ico: 'mail' }, -]; - export function TestbedHome({ nav }: { tab: string; nav: Nav }): ReactNode { - const [mode, setMode] = useState('run'); - return ( - <> -
- 试法 - {MODES.map((m) => ( - - ))} -
- {mode === 'run' ? : } - - ); + return ; } diff --git a/apps/web/src/features/gov-testbed/internal/simulate.tsx b/apps/web/src/features/gov-testbed/internal/simulate.tsx index ed4c8e6..3b7e502 100644 --- a/apps/web/src/features/gov-testbed/internal/simulate.tsx +++ b/apps/web/src/features/gov-testbed/internal/simulate.tsx @@ -8,9 +8,9 @@ */ import { useMemo, useState } from 'react'; import type { ReactNode } from 'react'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import type { ProcessNode, ProcessPackage } from '@contracts'; -import { TERMINAL_STATE_LABEL, WORKFLOW_STATE_LABEL, testRunRoute } from '@contracts'; +import { TERMINAL_STATE_LABEL, WORKFLOW_STATE_LABEL, testCurrentRoute, testRunRoute, testStopRoute } from '@contracts'; import { Ico } from '@web-shared/Ico.tsx'; import { call } from '@web-shared/typed-fetch.ts'; import { useGovPackages, useGovStaff } from '@web-shared/gov/gov-api.ts'; @@ -76,7 +76,28 @@ export function SimulateRun({ nav }: { nav: Nav }): ReactNode { const [subject, setSubject] = useState('测试台试跑'); const [values, setValues] = useState({}); const [assignments, setAssignments] = useState>>({}); + const queryClient = useQueryClient(); const [workflowId, setWorkflowId] = useState(undefined); + /* + * **一次只跑一条**:刷新页面也认得回来 —— 不然仿真在后台跑着,界面上却像没起过, + * 人会再起一单(而上一单的任务还在别人工作台上挂着)。 + */ + const current = useQuery({ + queryKey: ['gov', 'testbed', 'current'], + queryFn: () => call<{ workflowId?: string; subject?: string }>(testCurrentRoute), + }); + const running = workflowId ?? current.data?.workflowId; + + /** 停止仿真 = **销毁**:工作流、活动任务、工单、实例、trace 一并拔掉 */ + const stop = useMutation({ + mutationFn: (id: string) => call<{ destroyed: boolean; tasks: number }>(testStopRoute, { body: { workflowId: id } }), + onSuccess: () => { + setWorkflowId(undefined); + run.reset(); + void queryClient.invalidateQueries({ queryKey: ['gov', 'testbed'] }); + void queryClient.invalidateQueries({ queryKey: ['desk'] }); + }, + }); const [error, setError] = useState(undefined); // 没选就落到第一个有生效版的包 —— 常见情形只有一个包,不该逼人先点一下 @@ -107,16 +128,14 @@ export function SimulateRun({ nav }: { nav: Nav }): ReactNode { }); }; - if (workflowId !== undefined) { + if (running !== undefined) { return ( { - setWorkflowId(undefined); - run.reset(); - }} + stopping={stop.isPending} + onBack={() => stop.mutate(running)} /> ); } @@ -312,11 +331,13 @@ function Observe({ staffName, nav, onBack, + stopping, }: { id: string; staffName: (sid: string) => string; nav: Nav; onBack: () => void; + stopping: boolean; }): ReactNode { const wf = useRunWorkflow(id); const state = wf.data?.workflow.state; @@ -368,7 +389,7 @@ function Observe({ } acts={[ { label: '刷新', ico: 'refresh-cw', onClick: refresh }, - { label: '再装一单', ico: 'rotate-ccw', primary: true, onClick: onBack }, + { label: stopping ? '销毁中…' : '停止仿真', ico: 'square', primary: true, onClick: onBack }, ]} /> diff --git a/apps/web/src/features/gov-tool/internal/panel.tsx b/apps/web/src/features/gov-tool/internal/panel.tsx index 22c850d..7f30462 100644 --- a/apps/web/src/features/gov-tool/internal/panel.tsx +++ b/apps/web/src/features/gov-tool/internal/panel.tsx @@ -2,7 +2,7 @@ * feature `gov-tool` —— 工具箱:**系统能对世界做什么**,一屏四段。 * * 功能工具 · 已注册的调用契约(读类自由调,写类登记意向) - * LLM 工具 · 大模型驱动的系统内通用工具(**不是模型**;模型在系统区绑) + * LLM 工具 · 大模型驱动的系统内通用工具(模型实例在**大模型**区绑) * 人类工具 · 工单两模式 + 对话卡三族(调人也是调工具,只是被调的是人) * 接入 · 导入自定义工具(与第一方同门两审)/ 连接 MCP * @@ -75,11 +75,10 @@ const HUMAN_TOOLS = [ ] as const; /** - * LLM 工具目录 —— 系统内**由大模型驱动**的通用工具(不是模型本身)。 + * LLM 工具目录 —— 系统内**由大模型驱动**的通用工具。 * - * 模型是发动机,这些是装在发动机上的件:每一件都有明确的入、出与算账口径。 * 它们不在工具目录里注册 —— 与人类工具一样,是基座给的能力,不是谁装上来的。 - * 跑在哪个模型上由系统区的四元绑定决定(活动只见逻辑名,不触厂商与密钥)。 + * 跑在哪个模型上由**大模型**区的四元绑定决定(活动只见逻辑名,不触厂商与密钥)。 */ const LLM_TOOLS = [ { @@ -150,8 +149,9 @@ export function ToolsHome({ nav }: HomeProps) { - {t.name} - v{t.version} + {/* 主名是人读的中文名,逻辑名退居副标 —— 一屏几十件工具时尤其要紧 */} + {t.displayName} + {t.name} · v{t.version} {write ? '写' : '读'} @@ -204,11 +204,9 @@ export function ToolsHome({ nav }: HomeProps) { ))}
- 这些是工具,不是模型 —— 模型是它们跑在上面的发动机。 - 当前绑了 {(llm.data?.instances ?? []).length} 个模型实例( - {(llm.data?.instances ?? []).map((m) => m.name).join(' · ') || '尚未绑定'}); - 换厂商、换型号只改, - 这些工具与活动一行都不动。 + 已绑 {(llm.data?.instances ?? []).length} 个模型实例: + {(llm.data?.instances ?? []).map((m) => m.name).join(' · ') || '尚未绑定'}。 + 换厂商或型号去改绑定。 @@ -450,7 +448,7 @@ export function ToolPage({ id }: { id: string }) { ico="wrench" tone={write ? 'danger' : 'brand'} kicker={`工具 · ${t.name}`} - title={t.name} + title={t.displayName} chips={ <> {write ? '写类' : '读类'} diff --git a/apps/web/src/features/gov-testbed/internal/inbound.tsx b/apps/web/src/features/gov-trigger/internal/inbound.tsx similarity index 91% rename from apps/web/src/features/gov-testbed/internal/inbound.tsx rename to apps/web/src/features/gov-trigger/internal/inbound.tsx index 438d909..5df1e13 100644 --- a/apps/web/src/features/gov-testbed/internal/inbound.tsx +++ b/apps/web/src/features/gov-trigger/internal/inbound.tsx @@ -1,5 +1,7 @@ /** - * 联调测试台 · 入站邮件演练 —— 从**外界**推一封信进来,看谁接。 + * 触发器 · 入站邮件演练 —— 从**外界**推一封信进来,看哪条规则接住它。 + * + * 摆在触发器区里,是因为它验的正是这一区定的东西:规则写下来了,来一封真信试试对不对。 * * 造一封入站邮件(可带附件)投进通道,走的是**与真通道同一条**入站归一链路 * (`POST /api/ingest/channel/:id/mail` → 归属解析 → 统一事件流 → 三级消费)。 @@ -17,7 +19,7 @@ import type { IngestMailOutput, InboundMail } from '@contracts'; import { ingestMailRoute } from '@contracts'; import { call } from '@web-shared/typed-fetch.ts'; import { useGovChannels, useGovStaff } from '@web-shared/gov/gov-api.ts'; -import { Acts, Card, Field, Note, PageHead, Rows, Sec, St } from '@web-shared/gov/primitives.tsx'; +import { Acts, Band, Card, Field, Note, Rows, Sec, St } from '@web-shared/gov/primitives.tsx'; import type { Nav } from '@web-shared/gov/gov-route.ts'; /* @@ -138,32 +140,9 @@ export function InboundDrill({ nav }: { nav: Nav }) { return ( <> - - - {allGreen ? '起单条件齐' : `差 ${preflight.filter((p) => !p.ok).length} 条`} - - - 附件 {attachments.length} - - - } - acts={[ - { - label: send.isPending ? '投递中…' : '投递这封信', - ico: 'send', - primary: true, - disabled: send.isPending || effectiveChannel === '', - onClick: fire, - }, - ]} - /> + + 投一封信试试 + 走的是与真通道同一条入站链路:通道归一 → 归属解析 → 统一事件流 → 三级消费(任务监听 / @@ -256,6 +235,18 @@ export function InboundDrill({ nav }: { nav: Nav }) { }))} /> + !p.ok).length} 条,仍要投`, + ico: 'send', + primary: true, + disabled: send.isPending || effectiveChannel === '', + onClick: fire, + }, + ]} + /> {error !== undefined && ( diff --git a/apps/web/src/features/gov-trigger/internal/panel.tsx b/apps/web/src/features/gov-trigger/internal/panel.tsx index 3a05626..85495cc 100644 --- a/apps/web/src/features/gov-trigger/internal/panel.tsx +++ b/apps/web/src/features/gov-trigger/internal/panel.tsx @@ -40,6 +40,7 @@ import { St, } from '@web-shared/gov/primitives.tsx'; import type { Nav } from '@web-shared/gov/gov-route.ts'; +import { InboundDrill } from './inbound.tsx'; import { useGovObjects, useGovPackages, useGovTriggers } from '@web-shared/gov/gov-api.ts'; import { domainShort } from '@web-shared/gov/domains.ts'; @@ -133,6 +134,8 @@ export function TriggerRulesHome({ nav }: { nav: Nav }) { )} )} + {/* 定完就地验:来一封真信,看这些规则里哪条接住 */} + ); diff --git a/apps/web/src/shared/gov/__tests__/surfaces.test.ts b/apps/web/src/shared/gov/__tests__/surfaces.test.ts index c0728ce..4e5cd31 100644 --- a/apps/web/src/shared/gov/__tests__/surfaces.test.ts +++ b/apps/web/src/shared/gov/__tests__/surfaces.test.ts @@ -77,11 +77,20 @@ describe('legacyGovTarget —— 旧 #/gov 深链一条不死', () => { }); it('已删的区落到对应新家', () => { - expect(legacyGovTarget('#/gov/triggers/rules')).toBe('/design#/design/workflows/rules'); expect(legacyGovTarget('#/gov/memory/metrics')).toBe('/ops#/ops/cockpit/dash'); expect(legacyGovTarget('#/gov/memory/layers')).toBe('/ops#/ops/knowledge/layers'); }); + /* + * 触发器与大模型**又独立成区**了(2026-07-29):触发器一度并进流程包页签、 + * 模型一度住在系统区 —— 那两段旧链接要各回各家,不能停在中途那一站。 + */ + it('搬回自己区的:触发器与大模型', () => { + expect(legacyGovTarget('#/gov/triggers/rules')).toBe('/design#/design/triggers/rules'); + expect(legacyGovTarget('#/gov/workflows/rules')).toBe('/design#/design/triggers/rules'); + expect(legacyGovTarget('#/gov/system/llm')).toBe('/design#/design/models/llm'); + }); + it('裸 #/gov 落运营首页;非 gov 哈希不管', () => { expect(legacyGovTarget('#/gov')).toBe('/ops#/ops/cockpit/dash'); expect(legacyGovTarget('')).toBeUndefined(); diff --git a/apps/web/src/shared/gov/gov-route.ts b/apps/web/src/shared/gov/gov-route.ts index e8c2a99..f9b83e8 100644 --- a/apps/web/src/shared/gov/gov-route.ts +++ b/apps/web/src/shared/gov/gov-route.ts @@ -28,12 +28,20 @@ export type Route = * 记忆一分为二:度量升为 cockpit 首页,活动记忆归 knowledge。 */ export const GOV_ZONES = [ - // 设计中心 + /* + * 设计中心九区,顺序即设计的次序: + * 蓝图(全局) → 触发器(谁调起) → 流程包(怎么流转) → 活动库(每件事怎么干) + * → 工具箱(能对世界做什么) → 数据域(运营什么) → 知识库(资料,待建) + * → 大模型(模型实例绑在哪) → 测试台(跑一遍看看) + */ 'blueprint', + 'triggers', 'workflows', 'library', 'tools', 'data', + 'archive', + 'models', 'testbed', // 运营中心 'cockpit', @@ -47,10 +55,13 @@ export type GovZone = (typeof GOV_ZONES)[number]; export const ZONES: ReadonlyArray<{ key: GovZone; label: string; icon: string }> = [ { key: 'blueprint', label: '蓝图', icon: 'route' }, + { key: 'triggers', label: '触发器', icon: 'zap' }, { key: 'workflows', label: '流程包', icon: 'workflow' }, { key: 'library', label: '活动库', icon: 'boxes' }, { key: 'tools', label: '工具箱', icon: 'wrench' }, { key: 'data', label: '数据域', icon: 'database' }, + { key: 'archive', label: '知识库', icon: 'library' }, + { key: 'models', label: '大模型', icon: 'cpu' }, { key: 'testbed', label: '测试台', icon: 'flask-conical' }, { key: 'cockpit', label: '驾驶舱', icon: 'gauge' }, { key: 'tasks', label: '任务', icon: 'activity' }, @@ -126,29 +137,46 @@ export const ZM: Readonly>> = { blueprint: [{ key: 'map', label: '企业蓝图', ico: 'route' }], - testbed: [{ key: 'inbound', label: '联调试跑', ico: 'flask-conical' }], - workflows: [ - { key: 'overview', label: '总览', ico: 'gauge' }, - { key: 'pkgs', label: '流程包', ico: 'boxes' }, - { key: 'rules', label: '触发器', ico: 'zap' }, - ], + testbed: [{ key: 'run', label: '仿真试跑', ico: 'flask-conical' }], + triggers: [{ key: 'rules', label: '调起规则', ico: 'zap' }], + archive: [{ key: 'docs', label: '资料', ico: 'library' }], + models: [{ key: 'llm', label: '模型实例', ico: 'cpu' }], + workflows: [{ key: 'pkgs', label: '流程包', ico: 'boxes' }], library: [{ key: 'shelf', label: '活动货架', ico: 'boxes' }], tools: [{ key: 'catalog', label: '能力目录', ico: 'wrench' }], data: [{ key: 'domains', label: '数据对象', ico: 'database' }], @@ -167,10 +195,7 @@ export const TABS: Readonly> = { - design: ['blueprint', 'workflows', 'library', 'tools', 'data', 'testbed'], + design: ['blueprint', 'triggers', 'workflows', 'library', 'tools', 'data', 'archive', 'models', 'testbed'], ops: ['cockpit', 'tasks', 'people', 'ledgers', 'knowledge', 'system'], }; @@ -37,7 +37,7 @@ export const SURFACE_ZONES: Readonly> = { * 数据**结构归设计、行数据归运营**。同一功能的两种动作各归其位,不拆授权。 */ export const SURFACE_FNS: Readonly> = { - design: ['process', 'activity', 'tool', 'data', 'trigger'], + design: ['process', 'activity', 'tool', 'data', 'trigger', 'system'], ops: ['task', 'staff', 'memory', 'system', 'trigger', 'data'], }; @@ -66,7 +66,9 @@ function isGovZone(value: string): value is GovZone { * 精确到页签的先匹配 —— 记忆的两个旧页签各归其位(度量升驾驶舱,活动记忆归知识库)。 */ const LEGACY_MOVES: Readonly> = { - triggers: 'design/workflows/rules', + 'workflows/rules': 'design/triggers/rules', + 'testbed/inbound': 'design/testbed/run', + 'system/llm': 'design/models/llm', memory: 'ops/cockpit/dash', 'memory/layers': 'ops/knowledge/layers', }; diff --git a/db/migrations/0018_tool_display_name.sql b/db/migrations/0018_tool_display_name.sql new file mode 100644 index 0000000..556c2d9 --- /dev/null +++ b/db/migrations/0018_tool_display_name.sql @@ -0,0 +1,4 @@ +-- 工具中文名:治理台一律主显示它,逻辑名(crm.history-price-read)退居副标。 +-- 存量行先用逻辑名兜底,注册新版本时按契约填真名。 +alter table sys.tool add column display_name text; +update sys.tool set display_name = name where display_name is null; diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 95ce18c..f4732ad 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -127,6 +127,13 @@ "when": 1785130113172, "tag": "0017_trace_span", "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1785130113173, + "tag": "0018_tool_display_name", + "breakpoints": true } ] } diff --git a/docs/产品设计.md b/docs/产品设计.md index 94101cb..645d9f1 100644 --- a/docs/产品设计.md +++ b/docs/产品设计.md @@ -33,7 +33,13 @@ ## 二、设计中心 `/design` -**左轨 = 企业蓝图(首页)+ 四库。** +**左轨 = 九区,顺序即设计的次序。** 一区一内容 —— 区内不再有页签导航, +点进一区看到的就是那一件事(唯一的例外是运营侧的任务与员工,它们确实是几个不同维度)。 + +``` +蓝图 → 触发器 → 流程包 → 活动库 → 工具箱 → 数据域 → 知识库 → 大模型 → 测试台 +全局 谁调起 怎么流转 怎么干 能干什么 运营什么 资料 跑在哪 试一遍 +``` ### 企业蓝图(首页) @@ -51,14 +57,19 @@ 全部聚在这里,点进即改。设计者的"需处置"不散落在四库里 (为此流程包区原有的「需处置」页签已删除:同一件事两处显示,就是散落)。 -### 四库 +### 九区 -| 库 | 定位 | 内容 | +| 区 | 定位 | 内容 | |---|---|---| -| **流程包** | 战略 —— 企业以哪些流转运转 | 现工作流区整体迁入:编排工作区(画布 + 属性卡)、边映射校验、版本谱系、导出依赖文件。**触发器的定义并入本库**:包详情"调起它的触发器"从只读变可编辑(规则 / 输入装配 / 目标版本),另设「调起规则」页签横览全部触发器。触发器不再是独立一区 | -| **活动库** | 方法 —— 每件事怎么干 | 现活动库区平移:货架、契约面、范式、资产版本谱系、安装两审 | -| **工具箱** | 能力 —— 系统能对世界做什么 | 现工具区平移:读/写目录、白名单授权、幂等口径、引用反查 | -| **数据域** | 对象 —— 企业运营什么 | 现数据区的**定义面**:域与对象 schema、字段、放行规则、被谁引用。**行数据不在这里** —— 那是运营的事 | +| **蓝图** | 全局 —— 定义完了没有 | 四库因果链 + 待补清单(见上) | +| **触发器** | 谁调起流程 | 事件 / 登记 / 周期三类调起规则的定义;定完就地投一封信演练,看哪条规则接住(归属解析 → 三级消费的真链路)。**启停开关在运营中心** | +| **流程包** | 战略 —— 企业以哪些流转运转 | 编排工作区(画布 + 属性卡)、边映射校验、版本谱系、导出依赖文件。总览的三枚计数并进货架顶端 | +| **活动库** | 方法 —— 每件事怎么干 | 货架、契约面、范式、资产版本谱系、安装两审 | +| **工具箱** | 能力 —— 系统能对世界做什么 | 功能 / LLM / 人类三类工具目录、白名单授权、幂等口径、引用反查。**一律主显示中文名**,逻辑名退居副标 | +| **数据域** | 对象 —— 企业运营什么 | 域与对象 schema、字段、放行规则、被谁引用。**行数据不在这里** —— 那是运营的事 | +| **知识库** | 企业的资料与文档 | 规范、图纸、样例、外部材料。**待建**,现在是个占位区 | +| **大模型** | 模型实例绑在哪 | Provider / URL / Key / Model 四元绑定。活动只声明用途名,换厂商换型号改这里;密钥只存这儿,不入包、不入留痕 | +| **测试台** | 试着跑一遍 | 装一次调用、指派每个节点归谁、起单看穿全程(全链 Trace)。真实任务照旧落到被指派人的工作台,测试台不代答。**一次只跑一条流**:生命周期跟着仿真状态,停止仿真即销毁这条流连同它派出的任务 | ### 一条纪律 @@ -203,11 +214,11 @@ shared | 决策 | 选择 | 理由 | |---|---|---| | 拆分形态 | 两个独立台面(+工作台成三面) | 产品叙事最干净:定义 / 监控 / 干活是三种心智;权限天然分界 | -| 触发器归属 | 定义归设计(流程包内),处置归运营 | 同一对象两种动作各归其位;避免两边各放一份 | +| 触发器归属 | 定义**独立成设计中心一区**,启停归运营 | 同一对象两种动作各归其位;起初塞在流程包内,但它与流程包平级(不入包结构、不随包版本),塞进去反而讲不清 | | 度量归属 | 升为运营首页(驾驶舱) | "看企业今天跑得怎样"需要开屏答案;与活动记忆分开是两种心智 | | 动态位形态 | 域 = 导航项,随发布长 | "设计产生运营的形状"要在导航上直接可见;对象级太碎 | | 权限粒度 | 保留九功能粒度,入口按读权亮灯 | grants 零迁移,细粒度保住;≤50 人不需要两层模型 | -| 设计中心 IA | 蓝图首页 + 四库平铺 | 因果链要有一屏能讲;四库直达的稳定导航同时保住 | +| 设计中心 IA | 蓝图首页 + 九区一级平铺,一区一内容 | 因果链要有一屏能讲;区内页签退化成一格后就是多余的一行,索性删掉 —— 点进一区看到的就是那一件事 | | 交互基调(2026-07-28) | 设计台**卡片式**,运营台**列表式** | 设计是「看清一件东西的全貌」——卡片一眼见轮廓;运营是「查一堆记录里的那一条」——列表可扫可比 | | 数据四域(2026-07-28) | project / customer / intel / supplier | **流转单据归项目**(每一单的受理记录→比价→草案→报价单),**沉淀资产按内容归**客户/情报/供应商;按活动名分域会让「同一个客户」散在七处 | | 活动与工具解耦(2026-07-28) | 槽位只声明工具**名** | 注册、连接值、白名单全在治理侧 —— 活动代码里看不到授权表,也就绕不开它 | diff --git a/packages/contracts/src/api/governance.ts b/packages/contracts/src/api/governance.ts index 192cc48..425fc51 100644 --- a/packages/contracts/src/api/governance.ts +++ b/packages/contracts/src/api/governance.ts @@ -339,6 +339,8 @@ export const registerToolRoute = { body: z.object({ contract: z.object({ name: z.string().min(1), + /** 中文名 —— 治理台主显示;不给就以逻辑名兜底(界面宁可显示机器名,不显示空白) */ + displayName: z.string().min(1).optional(), version: z.string().min(1), description: z.string().min(1), input: z.record(z.string(), z.unknown()), @@ -407,6 +409,8 @@ export const listToolsRoute = { tools: z.array( z.object({ name: z.string(), + /** 中文名 —— 治理台主显示;逻辑名退居副标 */ + displayName: z.string(), version: z.string(), description: z.string(), effect: z.enum(['read', 'write']), @@ -527,6 +531,24 @@ export const testRunRoute = { output: z.object({ workflowId: z.string(), state: workflowStateSchema }), } as const satisfies RouteContract; +/** + * 停止仿真 —— **把这一单连根拔掉**:工作流、它的活动任务、工单、实例、trace 全部销毁。 + * 仿真不是生产:试跑留下的东西不该在库里、在别人工作台上过夜。 + */ +export const testStopRoute = { + method: 'POST', + path: '/api/gov/testbed/stop', + body: z.object({ workflowId: z.string() }), + output: z.object({ destroyed: z.boolean(), tasks: z.number().int() }), +} as const satisfies RouteContract; + +/** 当前在跑的仿真(测试台一次只跑一条)—— 刷新页面也认得回来 */ +export const testCurrentRoute = { + method: 'GET', + path: '/api/gov/testbed/current', + output: z.object({ workflowId: z.string().optional(), subject: z.string().optional() }), +} as const satisfies RouteContract; + /** 全链 Trace:一条工作流的 span 树(节点 → 步骤 / 工具 / 模型 / 人卡) */ export const traceOfWorkflowRoute = { method: 'GET',