From 9de641459ea4c4356bb141b601f55dffb2755242 Mon Sep 17 00:00:00 2001 From: 18628386721 <18628386721@163.com> Date: Tue, 28 Jul 2026 03:02:44 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BC=96=E6=8E=92=E5=B1=82=E2=91=A0:=E6=94=B9?= =?UTF-8?q?=E5=9B=BE=E5=85=A5=E5=8F=A3=E4=B8=8B=E6=B2=89=E7=94=BB=E5=B8=83?= =?UTF-8?q?=20+=20=E6=B4=BB=E5=8A=A8=E5=A5=91=E7=BA=A6=E9=9D=A2=E5=85=A8?= =?UTF-8?q?=E9=93=BE=E6=89=93=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ① 已发布包就地给「起草新版本并去编辑」,不用去页头找; ② 修一个我自己引入的 bug:卡点还原把审批点的「退回调整」牌子当成边,凭空造出 e-A5-A4 —— 卡点只认亲、不新建(旧包有这条边才认);实测编辑器往返 16 边 14 节点无损; ③ 活动契约面接进治理台:列表出参带必填入参/输出口/声明依赖,活动详情摆成一屏, 导出依赖文件一并带上 —— 开发一个活动要照着的东西,现在有一处能看全。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .../contracts/src/api/governance.d.ts | 18 ++++++ apps/api/src/app/routes/governance-ops.ts | 45 ++++++++++++++- .../src/modules/activity-registry/CONTRACT.md | 6 ++ .../features/gov-activity/internal/panel.tsx | 56 ++++++++++++++++++- .../features/gov-process/internal/panel.tsx | 42 +++++++++++++- .../gov/flow/__tests__/graph-to-pkg.test.ts | 13 +++++ apps/web/src/shared/gov/flow/graph-to-pkg.ts | 12 +++- packages/contracts/src/api/governance.ts | 14 +++++ 8 files changed, 200 insertions(+), 6 deletions(-) diff --git a/api-report/packages/contracts/src/api/governance.d.ts b/api-report/packages/contracts/src/api/governance.d.ts index 84b7a61..f7faa7b 100644 --- a/api-report/packages/contracts/src/api/governance.d.ts +++ b/api-report/packages/contracts/src/api/governance.d.ts @@ -1603,6 +1603,24 @@ export declare const listActivitiesRoute: { version: z.ZodNumber; effective: z.ZodBoolean; }, z.core.$strip>>; + contract: z.ZodObject<{ + requiredInputs: z.ZodArray; + inputFields: z.ZodArray>; + outputPorts: z.ZodArray; + fields: z.ZodArray; + }, z.core.$strip>>; + tools: z.ZodArray; + models: z.ZodArray; + objects: z.ZodArray; + roles: z.ZodArray; + humanCallBudget: z.ZodNumber; + }, z.core.$strip>; }, z.core.$strip>>; }, z.core.$strip>; }; diff --git a/apps/api/src/app/routes/governance-ops.ts b/apps/api/src/app/routes/governance-ops.ts index 98d63f8..c7772bb 100644 --- a/apps/api/src/app/routes/governance-ops.ts +++ b/apps/api/src/app/routes/governance-ops.ts @@ -355,8 +355,50 @@ export function registerGovernanceOpsRoutes(app: Hono, container: Contai const denied = await readActivity(c.get('account')); if (denied) return c.json(denied, denyStatus(denied)); const cards = await container.registry.list(); + /* + * 契约面随卡一起给 —— 开发一个活动要照着的东西全在清单里: + * 七件契约的入参与出口、声明的工具/模型/域对象/岗位。 + * 编排的人也要看它:引用一个活动却不知道它吃什么,边映射就只能靠猜。 + */ + const faces = await Promise.all( + cards.map(async (card) => { + const manifest = await container.registry.manifestOf(card.activityId); + const data = manifest.contract.input.data as { + required?: string[]; + properties?: Record; + }; + const required = data.required ?? []; + const props = data.properties ?? {}; + const portFields = (payload: unknown) => { + const p = payload as { properties?: Record } | undefined; + return Object.keys(p?.properties ?? {}); + }; + return { + requiredInputs: required, + inputFields: Object.entries(props).map(([name, spec]) => ({ + name, + type: spec.type ?? 'unknown', + required: required.includes(name), + })), + // 必发口与选择组一并列出:调用方只凭口名与 schema 就能衔接下游 + outputPorts: [ + ...manifest.contract.output.mandatory, + ...manifest.contract.output.selective.flatMap((g) => g.ports), + ].map((port) => ({ + name: port.name, + ...(port.description === undefined ? {} : { description: port.description }), + fields: portFields(port.payload), + })), + tools: manifest.declarations.tools.map((t) => t.name), + models: manifest.declarations.models, + objects: manifest.declarations.ledger.map((o) => `${o.domain}.${o.object}`), + roles: [...manifest.contract.resources.roles], + humanCallBudget: manifest.contract.authorization.humanCallBudget, + }; + }), + ); return c.json({ - activities: cards.map((card) => ({ + activities: cards.map((card, i) => ({ activityId: card.activityId, name: card.name, category: card.category, @@ -372,6 +414,7 @@ export function registerGovernanceOpsRoutes(app: Hono, container: Contai version: a.version, effective: a.effective, })), + contract: faces[i]!, })), }); }); diff --git a/apps/api/src/modules/activity-registry/CONTRACT.md b/apps/api/src/modules/activity-registry/CONTRACT.md index a195589..cfd4207 100644 --- a/apps/api/src/modules/activity-registry/CONTRACT.md +++ b/apps/api/src/modules/activity-registry/CONTRACT.md @@ -35,3 +35,9 @@ 覆盖只能是显式的 `rollbackAsset` 治理动作 - **进化必过治理**:候选 → 回放 → 人放行,状态机不许跳过回放 - **安装恒为新增、版本并存**:卸载只置 state,在途任务按其启动时版本跑完 + +近变(2026-07-28):模块面未变。app 层的 `GET /api/gov/activities` 出参加 `contract` 面 + (必填入参 / 入参字段 / 输出口与载荷字段 / 声明的工具·模型·域对象·岗位 / 调人预算), + 由 `manifestOf` 逐个读清单组装。此前治理台看不到活动的契约: + 编排的人引用一个活动却不知道它吃什么,边映射只能靠猜; + 要开发一个活动的人也没有一处能照着看。 diff --git a/apps/web/src/features/gov-activity/internal/panel.tsx b/apps/web/src/features/gov-activity/internal/panel.tsx index 8a00cac..3cd2542 100644 --- a/apps/web/src/features/gov-activity/internal/panel.tsx +++ b/apps/web/src/features/gov-activity/internal/panel.tsx @@ -6,7 +6,7 @@ * 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。 */ import { Ico } from '@web-shared/Ico.tsx'; -import { Card, Chip, Clear, Entry, Note, PageHead, QueryGate, St } from '@web-shared/gov/primitives.tsx'; +import { Card, Chip, Clear, Entry, Note, PageHead, Prop, QueryGate, St } from '@web-shared/gov/primitives.tsx'; import { PARA } from '@web-shared/gov/tone.ts'; import type { Nav } from '@web-shared/gov/gov-route.ts'; import { @@ -98,6 +98,60 @@ export function ActPage({ id, nav }: { id: string; nav: Nav }) { } desc={PARA[a.paradigm].full + ' —— 活动是系统外开发、经此安装的外装件。'} /> + {/* + 契约面 —— 开发这个活动要照着的东西:吃什么、吐什么、用谁的工具、动哪些域对象。 + 它排在版本谱系前面:先看清契约,再看装了哪几版。 + */} + + + {a.contract.inputFields.length === 0 ? ( + 不吃结构化入参 —— 只凭委托消息启动。 + ) : ( +
+ {a.contract.inputFields.map((f) => ( +
+ + {f.name} + {f.required && 必填} + + {f.type} +
+ ))} +
+ )} +
+ +
+ {a.contract.outputPorts.map((port) => ( +
+ {port.name} + {port.fields.join(' · ') || '(无字段)'} +
+ ))} + {a.contract.outputPorts.length === 0 && 没有声明输出口 —— 下游无从衔接,发布会被拦。} +
+
+ +
+
+ 工具 + {a.contract.tools.join(' · ') || '(不调工具)'} +
+
+ 模型 + {a.contract.models.join(' · ') || '(不调模型)'} +
+
+ 岗位 + {a.contract.roles.join(' · ') || '(未声明)'} +
+
+ 域对象 + {a.contract.objects.join(' · ') || '(不读写台账)'} +
+
+
+
{a.packageVersions.map((p) => ( diff --git a/apps/web/src/features/gov-process/internal/panel.tsx b/apps/web/src/features/gov-process/internal/panel.tsx index 7088940..2c3e729 100644 --- a/apps/web/src/features/gov-process/internal/panel.tsx +++ b/apps/web/src/features/gov-process/internal/panel.tsx @@ -31,9 +31,11 @@ import { Sec, Seg, St, + Todo, } from '@web-shared/gov/primitives.tsx'; import type { Nav } from '@web-shared/gov/gov-route.ts'; import { useGovActivities, useGovPackages, useGovPkgReports, useGovStaff, useGovTriggers } from '@web-shared/gov/gov-api.ts'; +import type { GovActivities } from '@web-shared/gov/gov-api.ts'; 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'; @@ -183,12 +185,14 @@ export function PkgPage({ id, nav }: { id: string; nav: Nav }) { ); } +/** 本页要用的活动字段面 —— 与 `GovActivitiesOutput` 的一致子集(含契约面,导出依赖文件要它) */ type ActivityRow = { activityId: string; name: string; paradigm: string; packageVersions: ReadonlyArray<{ version: string }>; assetVersions: ReadonlyArray<{ version: number; effective: boolean }>; + contract: GovActivities['activities'][number]['contract']; }; function Orchestrator({ @@ -321,6 +325,7 @@ function Orchestrator({ * 一并成一份 JSON。**依赖文件即包清单骨架**,开发者对着声明在系统外开发活动。 */ const exportDeps = () => { + const actContract = new Map(activities.map((a) => [a.activityId, a.contract])); const doc = { package: { id: pkg.id, name: pkg.name, version: pkg.version }, callInterface: pkg.callInterface, @@ -331,8 +336,14 @@ function Orchestrator({ label: n.label, activityId: n.activityId, owner: n.owner, + // 编排上手写的注记(给人读) declaredIn: n.in ?? [], declaredOut: n.out ?? [], + /* + * 活动库里那份**真契约**:必填入参、字段、输出口、声明的工具/模型/域对象。 + * 开发者照着它写实现;只给注记的话,依赖文件说不清这个活动到底要什么。 + */ + contract: actContract.get(n.activityId) ?? null, upstreamAssembly: pkg.edges .filter((e) => e.to === n.id) .map((e) => ({ from: e.from, mapping: e.mapping })), @@ -480,8 +491,37 @@ function Orchestrator({ + {/* + 已发布不可回改是规矩,但「去哪儿改」不该让人自己找 —— + 就地把路指出来:一句为什么 + 一颗按钮,点了直接建稿并跳过去。 + */} + {published && ( +
+ draftNext.mutate(), + } + : { + label: `去草稿 v${draftSibling.version} 编辑`, + ico: 'square-pen', + onClick: () => nav.push({ k: 'pkg', id: `${draftSibling.id}@${draftSibling.version}` }), + }, + ]} + /> +
+ )}
diff --git a/apps/web/src/shared/gov/flow/__tests__/graph-to-pkg.test.ts b/apps/web/src/shared/gov/flow/__tests__/graph-to-pkg.test.ts index aa0cf27..91786fb 100644 --- a/apps/web/src/shared/gov/flow/__tests__/graph-to-pkg.test.ts +++ b/apps/web/src/shared/gov/flow/__tests__/graph-to-pkg.test.ts @@ -77,6 +77,19 @@ describe('mergeGraphIntoPackage', () => { expect(diagram.nodes.map((n) => n.id)).toContain('c1'); }); + it('卡点只认亲、不新建 —— 审批点的驳回牌子不该被当成一条边', () => { + // 「审批点 → 退回卡点(to: A)」在旧包里没有对应的边:它是 reject 声明的呈现 + const withReject: FlowGraph = { + ...graph, + nodes: [...graph.nodes, { id: 'AP', kind: 'activity', label: '审批', flavor: 'approval' }], + edges: [...graph.edges, { from: 'AP', to: 'c1', label: '不通过' }], + }; + const merged = mergeGraphIntoPackage(basePkg, withReject); + expect(merged.edges.some((e) => e.from === 'AP' && e.to === 'A')).toBe(false); + // 旧包里有的回边照旧认得出来 + expect(merged.edges.some((e) => e.from === 'G' && e.to === 'A')).toBe(true); + }); + it('画布装不下的声明原样保留:边映射与负责人策略', () => { const merged = mergeGraphIntoPackage(basePkg, graph); expect(merged.edges.find((e) => e.id === 'e-s-a')?.mapping).toHaveLength(1); diff --git a/apps/web/src/shared/gov/flow/graph-to-pkg.ts b/apps/web/src/shared/gov/flow/graph-to-pkg.ts index 64a497e..356ef0b 100644 --- a/apps/web/src/shared/gov/flow/graph-to-pkg.ts +++ b/apps/web/src/shared/gov/flow/graph-to-pkg.ts @@ -112,8 +112,13 @@ export function mergeGraphIntoPackage( /* * 卡点是**回跳的呈现**:图上不画长线,画一枚「返回调整→A4」的小牌子。 - * 所以「X → 卡点(to: Y)」就是语义边 X→Y —— 落包时把它还原回去, - * 否则一存盘回边就没了(卡点不入语义,这条边两端有一端不在 semanticIds 里,会被滤掉)。 + * 「X → 卡点(to: Y)」要还原成语义边 X→Y,否则一存盘回边就没了 + * (卡点不入语义,这条边有一端不在 semanticIds 里,会被滤掉)。 + * + * 但还原只用于**认亲**,不用于**新建**:只有旧包里确有 X→Y 这条边才认。 + * 不设这道闸,审批点的「退回调整→A4」卡点(它是 `reject` 声明的呈现、不是边) + * 会被当成 A5→A4 凭空造出一条边来 —— 实测就多出过一条 `e-A5-A4`。 + * 真要新连一条边,画一条线;卡点只是牌子。 */ const calloutTarget = new Map( graph.nodes @@ -122,7 +127,8 @@ export function mergeGraphIntoPackage( ); const resolved = graph.edges.map((e) => { const via = calloutTarget.get(e.to); - return via === undefined ? e : { ...e, to: via }; + if (via === undefined) return e; + return priorEdge.has(`${e.from}→${via}`) ? { ...e, to: via } : e; }); const edges: Edge[] = resolved diff --git a/packages/contracts/src/api/governance.ts b/packages/contracts/src/api/governance.ts index 7f4aac9..772b5c8 100644 --- a/packages/contracts/src/api/governance.ts +++ b/packages/contracts/src/api/governance.ts @@ -332,6 +332,20 @@ export const listActivitiesRoute = { enabled: z.boolean(), packageVersions: z.array(z.object({ version: z.string(), state: z.string(), installedAt: z.string() })), assetVersions: z.array(z.object({ kind: z.string(), version: z.number().int(), effective: z.boolean() })), + /** + * 契约面 —— 开发这个活动要照着的东西:吃什么、吐什么、用谁的工具、动哪些域对象。 + * 治理台不给这一层,「编排定契约」就只是句口号:编排的人看不到自己引用的活动要什么。 + */ + contract: z.object({ + requiredInputs: z.array(z.string()), + inputFields: z.array(z.object({ name: z.string(), type: z.string(), required: z.boolean() })), + outputPorts: z.array(z.object({ name: z.string(), description: z.string().optional(), fields: z.array(z.string()) })), + tools: z.array(z.string()), + models: z.array(z.string()), + objects: z.array(z.string()), + roles: z.array(z.string()), + humanCallBudget: z.number().int(), + }), }), ), }),