From 5619d86ce4f43c803428a82792bcd322417b0552 Mon Sep 17 00:00:00 2001 From: 18628386721 <18628386721@163.com> Date: Tue, 28 Jul 2026 10:01:25 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BC=96=E6=8E=92=E5=B1=82=E2=91=A2:=E6=B4=BB?= =?UTF-8?q?=E5=8A=A8=E8=A3=85=E5=85=A5/=E6=8D=A2=E7=89=88=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=20=E2=80=94=E2=80=94=20=E5=BC=80=E5=8F=91=E5=AE=8C?= =?UTF-8?q?=E8=83=BD=E8=A3=85=E8=BF=9B=E6=9D=A5,=E4=B8=8D=E5=BF=85?= =?UTF-8?q?=E8=B7=91=E7=A7=8D=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前活动只能靠 seed 装入,治理台没有任何入口 —— 这是「逐个开发活动」的拦路石。 补 POST /api/gov/activities(无代码通道:交清单 + .js 产物,都是数据), 两审在服务端跑;不过审按发布门同样的式样回 412 + ApiError.detail 带审计结论 (直接回业务体的话类型化客户端拿不到详情)。活动库给「装入 / 换版」卡:选文件或粘贴两份。 实测:0.1.0 → 0.2.0 版本并存;声明未注册的工具即 412 并点名到工具。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .../contracts/src/api/governance.d.ts | 392 ++++++++++++++++++ apps/api/src/app/routes/governance-ops.ts | 49 +++ .../features/gov-activity/internal/panel.tsx | 171 +++++++- packages/contracts/src/api/governance.ts | 34 +- 4 files changed, 644 insertions(+), 2 deletions(-) diff --git a/api-report/packages/contracts/src/api/governance.d.ts b/api-report/packages/contracts/src/api/governance.d.ts index f7faa7b..792e197 100644 --- a/api-report/packages/contracts/src/api/governance.d.ts +++ b/api-report/packages/contracts/src/api/governance.d.ts @@ -1531,6 +1531,398 @@ export declare const saveWorkGroupRoute: { }, z.core.$strip>; }, z.core.$strip>; }; +/** + * 经安装通道装入活动包 —— **无代码通道**(ADR-004):交的是清单 + .js 产物,都是数据。 + * + * 安装两审在服务端跑:静态审读清单(工具契约在否、模型名绑没绑、模版有没有脚本、 + * 声明与已物化的台账冲不冲),回放审重演历史(首装无历史即免,以样例集替代)。 + * **恒为新增、版本并存**:装入不覆盖旧版本,在途任务按其启动时版本跑完。 + */ +export declare const installActivityRoute: { + readonly method: "POST"; + readonly path: "/api/gov/activities"; + readonly body: z.ZodObject<{ + manifest: z.ZodObject<{ + identity: z.ZodObject<{ + activityId: z.core.$ZodBranded; + name: z.ZodString; + packageVersion: z.ZodString; + category: z.ZodString; + paradigm: z.ZodEnum<{ + loop: "loop"; + flow: "flow"; + }>; + sdkVersion: z.ZodString; + checksum: z.ZodString; + signature: z.ZodOptional; + }, z.core.$strip>; + contract: z.ZodObject<{ + activityId: z.core.$ZodBranded; + version: z.ZodString; + input: z.ZodObject<{ + data: z.ZodRecord; + messageAllowed: z.ZodDefault; + messageHint: z.ZodOptional; + }, z.core.$strip>; + output: z.ZodObject<{ + mandatory: z.ZodDefault; + description: z.ZodOptional; + }, z.core.$strip>>>; + selective: z.ZodDefault; + description: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>; + requiredDeliverables: z.ZodDefault>; + }, z.core.$strip>>>; + acceptanceCriteria: z.ZodDefault; + predicate: z.ZodOptional; + modelRef: z.ZodOptional; + }, z.core.$strip>>>; + prohibitions: z.ZodDefault>>; + authorization: z.ZodObject<{ + discretion: z.ZodDefault>>; + preauthorizedActions: z.ZodDefault; + }, z.core.$strip>>>; + humanCallBudget: z.ZodNumber; + }, z.core.$strip>; + resources: z.ZodObject<{ + tools: z.ZodDefault; + }, z.core.$strip>>>; + skills: z.ZodDefault>>; + models: z.ZodDefault>; + roles: z.ZodDefault>; + budget: z.ZodObject<{ + steps: z.ZodNumber; + tokens: z.ZodNumber; + activeMinutes: z.ZodNumber; + humanCalls: z.ZodNumber; + }, z.core.$strip>; + }, z.core.$strip>; + }, z.core.$strip>; + declarations: z.ZodObject<{ + models: z.ZodDefault>; + tools: z.ZodDefault; + }, z.core.$strip>>>; + ledger: z.ZodDefault; + fields: z.ZodArray; + label: z.ZodOptional; + primaryKey: z.ZodDefault; + masked: z.ZodDefault; + }, z.core.$strip>>; + }, z.core.$strip>>>; + listeners: z.ZodDefault; + eventType: z.ZodEnum<{ + human: "human"; + data: "data"; + time: "time"; + control: "control"; + }>; + keyFilter: z.ZodDefault>; + condition: z.ZodOptional; + semantic: z.ZodEnum<{ + observe: "observe"; + "complete-step": "complete-step"; + }>; + deadline: z.ZodString; + escalation: z.ZodDefault>>; + }, z.core.$strip>>>; + harness: z.ZodObject<{ + memoryWindow: z.ZodDefault; + stallAfter: z.ZodDefault; + nudge: z.ZodDefault; + }, z.core.$strip>>>; + retry: z.ZodDefault; + backoff: z.ZodDefault; + }, z.core.$strip>>; + }, z.core.$strip>; + }, z.core.$strip>; + assets: z.ZodObject<{ + businessModel: z.ZodDiscriminatedUnion<[z.ZodObject<{ + paradigm: z.ZodLiteral<"loop">; + cmmnXml: z.ZodString; + narrative: z.ZodString; + dmn: z.ZodObject<{ + drd: z.ZodArray>; + inputs: z.ZodDefault>; + }, z.core.$strip>>; + tables: z.ZodArray>; + inputs: z.ZodArray>; + outputs: z.ZodArray>; + rules: z.ZodArray; + then: z.ZodArray; + annotation: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + paradigm: z.ZodLiteral<"flow">; + steps: z.ZodArray; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional>; + fields: z.ZodRecord; + }, z.core.$strip>>; + when: z.ZodOptional; + view: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"llm">; + model: z.ZodString; + template: z.ZodString; + outputSchema: z.ZodRecord; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional>; + fields: z.ZodRecord; + }, z.core.$strip>>; + when: z.ZodOptional; + view: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + card: z.ZodRecord; + deadline: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional>; + fields: z.ZodRecord; + }, z.core.$strip>>; + when: z.ZodOptional; + view: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pure-tool">; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional>; + fields: z.ZodRecord; + }, z.core.$strip>>; + when: z.ZodOptional; + view: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>], "kind">>; + parallelGroups: z.ZodDefault; + }, z.core.$strip>>>; + }, z.core.$strip>], "paradigm">; + skills: z.ZodDefault; + label: z.ZodString; + applicability: z.ZodString; + method: z.ZodString; + crystallized: z.ZodDefault; + }, z.core.$strip>>>; + knowledgeRefs: z.ZodDefault>; + version: z.ZodString; + }, z.core.$strip>>>; + viewTemplates: z.ZodDefault>>; + cardSchemas: z.ZodDefault>>; + distillHooks: z.ZodDefault; + milestone: z.ZodOptional; + prompt: z.ZodString; + }, z.core.$strip>>>; + }, z.core.$strip>; + samples: z.ZodArray; + message: z.ZodOptional; + expect: z.ZodObject<{ + ports: z.ZodArray; + payloadShape: z.ZodOptional>; + }, z.core.$strip>; + }, z.core.$strip>>; + entry: z.ZodObject<{ + file: z.ZodString; + checksum: z.ZodString; + }, z.core.$strip>; + }, z.core.$strip>; + artifact: z.ZodString; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + accepted: z.ZodBoolean; + activityId: z.ZodString; + packageVersion: z.ZodString; + staticFindings: z.ZodArray>; + replay: z.ZodObject<{ + oldPassStillPass: z.ZodBoolean; + checked: z.ZodNumber; + skippedNoHistory: z.ZodBoolean; + regressions: z.ZodArray>; + }, 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 c7772bb..5011f09 100644 --- a/apps/api/src/app/routes/governance-ops.ts +++ b/apps/api/src/app/routes/governance-ops.ts @@ -19,6 +19,7 @@ import { listActivityMemoryRoute, listGovTasksRoute, listLedgerObjectsRoute, + installActivityRoute, listProcessPackagesRoute, listToolsRoute, listTriggersRoute, @@ -33,6 +34,8 @@ import { upsertTriggerRoute, validateProcessRoute, } from '@contracts'; +import { InstallRejectedError } from '@modules/activity-registry'; +import type { InstallVerdict } from '@modules/activity-registry'; import { NotPublishableError } from '@modules/process'; import type { Container } from '../container.ts'; import type { AppEnv } from '../http-env.ts'; @@ -63,6 +66,7 @@ export function registerGovernanceOpsRoutes(app: Hono, container: Contai const readTrigger = guard('trigger', 'read'); const governTrigger = guard('trigger', 'govern'); const readActivity = guard('activity', 'read'); + const governActivity = guard('activity', 'govern'); const readTool = guard('tool', 'read'); const governTool = guard('tool', 'govern'); const readData = guard('data', 'read'); @@ -351,6 +355,51 @@ export function registerGovernanceOpsRoutes(app: Hono, container: Contai /* ── 活动库管理:两维分类 × 两条版本序列 ─────────────────────────── */ + /** + * 装入 / 换版 —— 经**安装通道**,无代码通道(ADR-004):交清单 + .js 产物,都是数据。 + * 两审在这里跑;不过审即 412 并把审计结论原样回给人看,不静默失败。 + * 恒为新增、版本并存:装入不覆盖旧版本,在途任务按启动时版本跑完。 + */ + app.post(installActivityRoute.path, async (c) => { + const denied = await governActivity(c.get('account')); + if (denied) return c.json(denied, denyStatus(denied)); + const parsed = installActivityRoute.body.safeParse(await c.req.json()); + if (!parsed.success) { + const body: ApiError = { code: 'validation-failed', message: '活动清单不合 schema', detail: parsed.error.issues }; + return c.json(body, 400); + } + const { manifest, artifact } = parsed.data; + const reply = (verdict: InstallVerdict, accepted: boolean) => ({ + accepted, + activityId: manifest.identity.activityId, + packageVersion: manifest.identity.packageVersion, + staticFindings: verdict.staticFindings.map((f) => ({ code: f.code, severity: f.severity, message: f.message })), + replay: { + oldPassStillPass: verdict.replay.oldPassStillPass, + checked: verdict.replay.checked, + skippedNoHistory: verdict.replay.skippedNoHistory, + regressions: verdict.replay.regressions.map((r) => ({ taskId: r.taskId, detail: r.detail })), + }, + }); + try { + return c.json(reply(await container.registry.install(manifest, artifact), true)); + } catch (error) { + if (error instanceof InstallRejectedError) { + /* + * 两审未过:不上架。**按 ApiError 形状回**(与发布门一致)—— + * 类型化客户端只从 `detail` 取详情,直接回业务体的话前端拿不到审计结论。 + */ + const body: ApiError = { + code: 'precondition-failed', + message: error.message, + detail: reply(error.verdict, false), + }; + return c.json(body, 412); + } + throw error; + } + }); + app.get(listActivitiesRoute.path, async (c) => { const denied = await readActivity(c.get('account')); if (denied) return c.json(denied, denyStatus(denied)); diff --git a/apps/web/src/features/gov-activity/internal/panel.tsx b/apps/web/src/features/gov-activity/internal/panel.tsx index 3cd2542..8783dce 100644 --- a/apps/web/src/features/gov-activity/internal/panel.tsx +++ b/apps/web/src/features/gov-activity/internal/panel.tsx @@ -6,7 +6,37 @@ * 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。 */ import { Ico } from '@web-shared/Ico.tsx'; -import { Card, Chip, Clear, Entry, Note, PageHead, Prop, QueryGate, St } from '@web-shared/gov/primitives.tsx'; +import { useState } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { installActivityRoute } from '@contracts'; +import { ApiCallError, call } from '@web-shared/typed-fetch.ts'; +import { + Acts, + Card, + Chip, + Clear, + Entry, + Field, + Note, + PageHead, + Prop, + QueryGate, + St, +} from '@web-shared/gov/primitives.tsx'; + +/** 装入回参 —— 与 `installActivityRoute.output` 同形 */ +type InstallReply = { + accepted: boolean; + activityId: string; + packageVersion: string; + staticFindings: ReadonlyArray<{ code: string; severity: string; message: string }>; + replay: { + oldPassStillPass: boolean; + checked: number; + skippedNoHistory: boolean; + regressions: ReadonlyArray<{ taskId: string; detail: string }>; + }; +}; import { PARA } from '@web-shared/gov/tone.ts'; import type { Nav } from '@web-shared/gov/gov-route.ts'; import { @@ -27,6 +57,7 @@ export function LibraryHome({ nav }: HomeProps) { 每活动两条版本序列——包版本走安装两审,资产版本走进化管线。 审批点与网关不在这里:它们是流程层的系统内置要素,零开发、不入库。 + {activities.length === 0 ? ( 货架是空的 —— 装一个活动进来,编排里才引用得到它。 ) : ( @@ -187,3 +218,141 @@ export function ActPage({ id, nav }: { id: string; nav: Nav }) { ); } +/* ---------------- 装入 / 换版(安装通道) ---------------- */ + +/** + * 开发完一个活动,从这里装进来 —— **无代码通道**(ADR-004):交的是清单 + .js 产物,都是数据。 + * + * 两审在服务端跑:静态审读清单,回放审重演历史(首装无历史即免)。 + * 恒为新增、版本并存:装入不覆盖旧版本,在途任务按其启动时版本跑完。 + */ +function InstallCard() { + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [manifestText, setManifestText] = useState(''); + const [artifact, setArtifact] = useState(''); + + const install = useMutation({ + mutationFn: () => + call(installActivityRoute, { + body: { manifest: JSON.parse(manifestText) as unknown, artifact }, + }), + onSuccess: (out) => { + if (out.accepted) { + void queryClient.invalidateQueries({ queryKey: ['gov', 'activities'] }); + setOpen(false); + } + }, + }); + + const pick = (setter: (text: string) => void) => async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file !== undefined) setter(await file.text()); + }; + + /** 两审未过的结论:412 走的是 ApiCallError,详情在 detail 里 */ + const rejected = + install.error instanceof ApiCallError && install.error.status === 412 + ? (install.error.detail as InstallReply | undefined) + : install.data?.accepted === false + ? install.data + : undefined; + + if (!open) { + return ( + setOpen(true)}> + 装一个活动 + + } + > + + 交清单(manifest.json)+ 产物(index.js)两份文件,两审当场跑: + 静态审读清单(工具契约在否、模型名绑没绑、声明与已物化台账冲不冲), + 回放审重演历史成功单 —— 旧成不坏是生效硬门,首装无历史即免。 + 恒为新增、版本并存:装入不覆盖旧版本,在途任务按启动时版本跑完。 + + + ); + } + + return ( + + + + + + +