forked from ChaiHao/BPOS-MVP
编排层③:活动装入/换版入口 —— 开发完能装进来,不必跑种子
此前活动只能靠 seed 装入,治理台没有任何入口 —— 这是「逐个开发活动」的拦路石。 补 POST /api/gov/activities(无代码通道:交清单 + .js 产物,都是数据), 两审在服务端跑;不过审按发布门同样的式样回 412 + ApiError.detail 带审计结论 (直接回业务体的话类型化客户端拿不到详情)。活动库给「装入 / 换版」卡:选文件或粘贴两份。 实测:0.1.0 → 0.2.0 版本并存;声明未注册的工具即 412 并点名到工具。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -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<AppEnv>, 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<AppEnv>, 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));
|
||||
|
||||
Reference in New Issue
Block a user