Archived
feat(ops): 运营中心 · 活动模块接口落地 —— L1/L2/L3 十一条(全 ops)+ 设计端活动库两条改名让位
`apps/api/src/app/activity-ops/*`(service / store / instance-state / instance-steps / launch-params / summaries / view-html / view-scope)+ `routes/activities.ts`;契约 `activity-ops.ts` / `api/activity-ops.ts` (与队友按文档补的那版 43 个导出同名,多 INSTANCE_REASON_KINDS / STEP_RESULTS / launchOptionSchema); 迁移 0035_activity_ops;《API文档-活动模块》按契约回写。 设计端活动库 GET/POST `/api/gov/activities` 改名 `/api/gov/activity-packages`,`/api/gov/activities/*` 整个让给运营端(前端 import 路由对象,零改动);activity-registry CONTRACT.md 与路由总表同步。 2026-08-27 另一会话做的,一直留在工作树没入库;与同批另两条提交互相依赖,以合并提交为验证单位。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 运营中心 · 活动模块的契约:形状照 `docs/API文档-活动模块.md` §9,十一条路由全归 ops。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
ACTIVITY_OPS_ROUTES,
|
||||
INSTANCE_STATES,
|
||||
INSTANCE_STATE_LABEL,
|
||||
activityDetailSchema,
|
||||
activitySummarySchema,
|
||||
instanceDetailSchema,
|
||||
instanceListSchema,
|
||||
instanceStatsSchema,
|
||||
launchInputSchema,
|
||||
launchParamsSchema,
|
||||
listOpsInstancesRoute,
|
||||
opsInstanceFileRoute,
|
||||
} from '../index.ts';
|
||||
|
||||
const owner = { id: 'staff-1', name: '柴浩' };
|
||||
const summary = {
|
||||
id: 'part-substitution-assessment',
|
||||
name: 'ACT-WORK-05 · 差异评估',
|
||||
biz: '需求分析与方案匹配',
|
||||
status: 'active',
|
||||
version: '1.5.0',
|
||||
owner,
|
||||
llm: 'deepseek-v4-flash',
|
||||
instanceCount: 3,
|
||||
createdAt: '2026-08-27T01:00:00.000Z',
|
||||
};
|
||||
const instance = {
|
||||
id: 'task-1',
|
||||
no: 'INS-17',
|
||||
state: 'running',
|
||||
owner,
|
||||
input: '工业电源项目在用 LM2596S-5.0',
|
||||
output: null,
|
||||
startedAt: '2026-08-27T02:00:00.000Z',
|
||||
};
|
||||
|
||||
describe('活动 schema', () => {
|
||||
it('负责人可空、图标色相可缺;详情多说明与视图', () => {
|
||||
expect(activitySummarySchema.parse({ ...summary, owner: null }).owner).toBeNull();
|
||||
expect(activitySummarySchema.safeParse({ ...summary, hue: 'pink' }).success).toBe(false);
|
||||
const detail = activityDetailSchema.parse({ ...summary, instanceCount: undefined, desc: '', view: null });
|
||||
expect(detail.view).toBeNull();
|
||||
expect(activityDetailSchema.safeParse({ ...summary, desc: '' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('实例 schema', () => {
|
||||
it('五态统计的键必须齐;清单带页码', () => {
|
||||
const byState = Object.fromEntries(INSTANCE_STATES.map((s) => [s, 0]));
|
||||
expect(instanceStatsSchema.parse({ total: 0, byState }).byState.rejected).toBe(0);
|
||||
expect(instanceStatsSchema.safeParse({ total: 0, byState: { pending: 0 } }).success).toBe(false);
|
||||
expect(instanceListSchema.parse({ stats: { total: 1, byState: { ...byState, running: 1 } }, total: 1, page: 1, size: 8, items: [instance] }).items).toHaveLength(1);
|
||||
expect(Object.keys(INSTANCE_STATE_LABEL).sort()).toEqual([...INSTANCE_STATES].sort());
|
||||
});
|
||||
|
||||
it('详情:input 是结构体(摘要字段被 omit 掉),reason 三种 kind', () => {
|
||||
const detail = instanceDetailSchema.parse({
|
||||
...instance,
|
||||
input: { content: '…', source: { kind: 'manual', label: '手动启动 · 柴浩' }, submittedAt: instance.startedAt, files: [] },
|
||||
activityId: 'part-substitution-assessment',
|
||||
llm: 'deepseek-v4-flash',
|
||||
endedAt: null,
|
||||
durationMs: null,
|
||||
reason: { kind: 'terminated', text: '由 柴浩 手动终止' },
|
||||
output: { files: [] },
|
||||
steps: [{ seq: 1, name: '建立评估上下文', by: 'agent', byName: 'deepseek-v4-flash', at: null, durationMs: null, res: 'pending', note: '' }],
|
||||
});
|
||||
expect(detail.input.source.kind).toBe('manual');
|
||||
expect(instanceDetailSchema.safeParse({ ...detail, reason: { kind: 'oops', text: '' } }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('启动 schema', () => {
|
||||
it('参数表控件五种;入参 list 回数组、其余回字符串;责任人必填', () => {
|
||||
const table = launchParamsSchema.parse({
|
||||
activityId: 'a',
|
||||
version: '1.0.0',
|
||||
params: [
|
||||
{ key: 'customerCode', label: '客户', type: 'combo', required: true, options: [{ value: 'CU-1', label: 'Itelma' }] },
|
||||
{ key: 'pns', label: '候选料号', type: 'list', default: ['PN-1'] },
|
||||
],
|
||||
});
|
||||
expect(table.params[1]?.default).toEqual(['PN-1']);
|
||||
expect(launchParamsSchema.safeParse({ activityId: 'a', version: '1', params: [{ key: 'x', label: 'x', type: 'file' }] }).success).toBe(false);
|
||||
expect(launchInputSchema.parse({ idempotencyKey: 'k', ownerId: 's', params: { a: 'x', b: ['y'] } }).params).toEqual({ a: 'x', b: ['y'] });
|
||||
expect(launchInputSchema.safeParse({ idempotencyKey: 'k', params: {} }).success).toBe(false);
|
||||
expect(launchInputSchema.safeParse({ idempotencyKey: 'k', ownerId: 's', params: { n: 1 } }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('路由契约', () => {
|
||||
it('十一条全归运营中心', () => {
|
||||
expect(ACTIVITY_OPS_ROUTES).toHaveLength(11);
|
||||
for (const route of ACTIVITY_OPS_ROUTES) expect(route.access, route.path).toBe('ops');
|
||||
expect(new Set(ACTIVITY_OPS_ROUTES.map((r) => `${r.method} ${r.path}`)).size).toBe(11);
|
||||
});
|
||||
|
||||
it('实例清单查询:页码默认 1、单页默认 8 上限 50、日期要 YYYY-MM-DD', () => {
|
||||
expect(listOpsInstancesRoute.query.parse({})).toMatchObject({ page: 1, size: 8 });
|
||||
expect(listOpsInstancesRoute.query.parse({ page: '3', size: '20', startedFrom: '2026-08-01' })).toMatchObject({ page: 3, size: 20 });
|
||||
expect(listOpsInstancesRoute.query.safeParse({ size: '51' }).success).toBe(false);
|
||||
expect(listOpsInstancesRoute.query.safeParse({ startedTo: '08-27' }).success).toBe(false);
|
||||
expect(listOpsInstancesRoute.query.safeParse({ state: 'done' }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('文件路由:io 只认 input / output,index 从字符串转数', () => {
|
||||
expect(opsInstanceFileRoute.params.parse({ activityId: 'a', instanceId: 't', io: 'output', index: '2' }).index).toBe(2);
|
||||
expect(opsInstanceFileRoute.params.safeParse({ activityId: 'a', instanceId: 't', io: 'both', index: '0' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 运营中心 · 活动模块的契约(L1 活动清单 / L2 活动详情 / L3 实例详情;`docs/API文档-活动模块.md`)。
|
||||
*
|
||||
* 只有 zod schema 与推导类型。两个概念(2026-08-25 用户定):
|
||||
* · **活动** = 装进来的活动包在运营面上的投影 —— 名称 / 分类 / 版本 / 模型来自清单,
|
||||
* 负责人 / 说明 / 图标 / 色相来自运营侧档案(`sys.activity_profile`,活动包本身不载这些);
|
||||
* · **实例** = 这个活动的一次运行(一张 task)—— 五态是对 task 运行态 / 终态的**如实映射**,
|
||||
* 不是第六套状态机(见 `INSTANCE_STATE_LABEL` 旁的口径)。
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { staffIdSchema } from './id.ts';
|
||||
|
||||
/* ── 活动 ───────────────────────────────────────────────────────────── */
|
||||
|
||||
/** 运营态:激活中 / 已禁用 —— 禁用只拦「新启动」,已完成与在途实例都不受影响(2026-08-27 用户定) */
|
||||
export const ACTIVITY_STATUSES = ['active', 'disabled'] as const;
|
||||
export const activityStatusSchema = z.enum(ACTIVITY_STATUSES);
|
||||
export type ActivityStatus = z.infer<typeof activityStatusSchema>;
|
||||
export const ACTIVITY_STATUS_LABEL: Readonly<Record<ActivityStatus, string>> = {
|
||||
active: '激活中',
|
||||
disabled: '已禁用',
|
||||
};
|
||||
|
||||
/** 业务色相 —— 一卡一色;缺省由前端兜底(`blue`) */
|
||||
export const ACTIVITY_HUES = ['blue', 'teal', 'amber', 'violet'] as const;
|
||||
export const activityHueSchema = z.enum(ACTIVITY_HUES);
|
||||
export type ActivityHue = z.infer<typeof activityHueSchema>;
|
||||
|
||||
/** 凡出人都是两段:展示用 name,筛选 / 提交用 id */
|
||||
export const staffRefSchema = z.object({ id: staffIdSchema, name: z.string() });
|
||||
export type StaffRef = z.infer<typeof staffRefSchema>;
|
||||
|
||||
export const activitySummarySchema = z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string(),
|
||||
/** 业务分类 —— 清单 `identity.category` */
|
||||
biz: z.string(),
|
||||
status: activityStatusSchema,
|
||||
/** 当前生效的包版本(最新装入的那一版) */
|
||||
version: z.string(),
|
||||
/** 活动负责人 —— 来自运营侧档案;没建档就是 null,前端摆「—」,**不编一个** */
|
||||
owner: staffRefSchema.nullable(),
|
||||
/** 绑定的模型(清单声明的逻辑模型名,如 `deepseek-v4-flash`) */
|
||||
llm: z.string(),
|
||||
instanceCount: z.number().int().min(0),
|
||||
/** 首个版本装入的时刻 */
|
||||
createdAt: z.string(),
|
||||
ico: z.string().optional(),
|
||||
hue: activityHueSchema.optional(),
|
||||
});
|
||||
export type ActivitySummary = z.infer<typeof activitySummarySchema>;
|
||||
|
||||
export const activityDetailSchema = activitySummarySchema.omit({ instanceCount: true }).extend({
|
||||
/** 定义说明 —— 运营侧档案;没写就是空串 */
|
||||
desc: z.string(),
|
||||
/** 沙箱要渲染的活动视图;null = 这个活动没配视图,前端藏起整个沙箱区块 */
|
||||
view: z.object({ url: z.string() }).nullable(),
|
||||
});
|
||||
export type ActivityDetail = z.infer<typeof activityDetailSchema>;
|
||||
|
||||
/**
|
||||
* 沙箱视图向父页报高的消息类型(postMessage 载荷 `{ type, height }`)。
|
||||
* 视图模版本身**无脚本**(安装静态审拦着);报高脚本由服务端在出口那一刻注入。
|
||||
*/
|
||||
export const ACTIVITY_VIEW_HEIGHT_MESSAGE = 'jcai:view-height';
|
||||
|
||||
/* ── 实例 ───────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* 实例五态(2026-08-25 用户定),对 task 的如实映射:
|
||||
* pending 已建单、推进器还没领到(`used.steps = 0`)
|
||||
* running 在途(含等人回传 / 监听中 / 挂起 / 落地受阻)
|
||||
* completed 终态 released
|
||||
* rejected 验收被负责人打回、模型还没重新提交的那一段(重新提交后回 running)
|
||||
* failed 终态 blocked / stalled / exhausted,以及手动终止(靠 `reason.kind='terminated'` 区分)
|
||||
*/
|
||||
export const INSTANCE_STATES = ['pending', 'running', 'completed', 'rejected', 'failed'] as const;
|
||||
export const instanceStateSchema = z.enum(INSTANCE_STATES);
|
||||
export type InstanceState = z.infer<typeof instanceStateSchema>;
|
||||
export const INSTANCE_STATE_LABEL: Readonly<Record<InstanceState, string>> = {
|
||||
pending: '待执行',
|
||||
running: '进行中',
|
||||
completed: '已完成',
|
||||
rejected: '驳回',
|
||||
failed: '失败',
|
||||
};
|
||||
|
||||
export const instanceSummarySchema = z.object({
|
||||
/** 寻址键 = 任务 id */
|
||||
id: z.string().min(1),
|
||||
/** 人读实例号 `INS-<序号>` —— 清单上摆它、模糊搜也认它 */
|
||||
no: z.string(),
|
||||
state: instanceStateSchema,
|
||||
owner: staffRefSchema,
|
||||
/** 输入一句话摘要 */
|
||||
input: z.string(),
|
||||
/** 产出一句话摘要;null 前端摆 `-` */
|
||||
output: z.string().nullable(),
|
||||
startedAt: z.string(),
|
||||
});
|
||||
export type InstanceSummary = z.infer<typeof instanceSummarySchema>;
|
||||
|
||||
export const instanceStatsSchema = z.object({
|
||||
total: z.number().int().min(0),
|
||||
byState: z.record(instanceStateSchema, z.number().int().min(0)),
|
||||
});
|
||||
export type InstanceStats = z.infer<typeof instanceStatsSchema>;
|
||||
|
||||
export const instanceListSchema = z.object({
|
||||
/** 活动整体的五态统计,不随检索条件动 */
|
||||
stats: instanceStatsSchema,
|
||||
/** 筛后条数,分页器按它走 */
|
||||
total: z.number().int().min(0),
|
||||
page: z.number().int().min(1),
|
||||
size: z.number().int().min(1),
|
||||
items: z.array(instanceSummarySchema),
|
||||
});
|
||||
export type InstanceList = z.infer<typeof instanceListSchema>;
|
||||
|
||||
export const instanceFileSchema = z.object({
|
||||
index: z.number().int().min(0),
|
||||
name: z.string(),
|
||||
contentType: z.string(),
|
||||
size: z.number().int().min(0),
|
||||
});
|
||||
export type InstanceFile = z.infer<typeof instanceFileSchema>;
|
||||
|
||||
export const INSTANCE_REASON_KINDS = ['rejected', 'failed', 'terminated'] as const;
|
||||
export const instanceReasonSchema = z.object({ kind: z.enum(INSTANCE_REASON_KINDS), text: z.string() });
|
||||
export type InstanceReason = z.infer<typeof instanceReasonSchema>;
|
||||
|
||||
export const STEP_RESULTS = ['done', 'fail', 'current', 'pending'] as const;
|
||||
export const instanceStepSchema = z.object({
|
||||
seq: z.number().int().min(1),
|
||||
name: z.string(),
|
||||
by: z.enum(['agent', 'human']),
|
||||
byName: z.string(),
|
||||
at: z.string().nullable(),
|
||||
durationMs: z.number().int().min(0).nullable(),
|
||||
res: z.enum(STEP_RESULTS),
|
||||
note: z.string(),
|
||||
});
|
||||
export type InstanceStep = z.infer<typeof instanceStepSchema>;
|
||||
|
||||
/** 起单来源:manual(运营端手动启动)/ upstream(工作流节点)/ mail 等通道名 / testbed(测试台试跑)/ system */
|
||||
export const instanceSourceSchema = z.object({ kind: z.string(), label: z.string() });
|
||||
export type InstanceSource = z.infer<typeof instanceSourceSchema>;
|
||||
|
||||
export const instanceDetailSchema = instanceSummarySchema.omit({ input: true }).extend({
|
||||
activityId: z.string(),
|
||||
llm: z.string(),
|
||||
endedAt: z.string().nullable(),
|
||||
durationMs: z.number().int().min(0).nullable(),
|
||||
/** 终态 / 驳回的原因;null = 不需要(进行中 / 待执行 / 正常完成) */
|
||||
reason: instanceReasonSchema.nullable(),
|
||||
input: z.object({
|
||||
content: z.string(),
|
||||
source: instanceSourceSchema,
|
||||
submittedAt: z.string(),
|
||||
files: z.array(instanceFileSchema),
|
||||
}),
|
||||
output: z.object({ files: z.array(instanceFileSchema) }),
|
||||
steps: z.array(instanceStepSchema),
|
||||
});
|
||||
export type InstanceDetail = z.infer<typeof instanceDetailSchema>;
|
||||
|
||||
/* ── 启动 ───────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* 控件种类:text 单行 / textarea 多行 / select 单选(只能选候选)/ combo 选择 + 新增 / list 字符串数组。
|
||||
* 新增种类前后端一起升级;前端遇到不认识的 type 摆「暂不支持的控件」。
|
||||
*/
|
||||
export const LAUNCH_PARAM_TYPES = ['text', 'textarea', 'select', 'combo', 'list'] as const;
|
||||
export const launchParamTypeSchema = z.enum(LAUNCH_PARAM_TYPES);
|
||||
export type LaunchParamType = z.infer<typeof launchParamTypeSchema>;
|
||||
|
||||
export const launchOptionSchema = z.object({ value: z.string(), label: z.string() });
|
||||
export type LaunchOption = z.infer<typeof launchOptionSchema>;
|
||||
|
||||
export const launchParamSchema = z.object({
|
||||
/** 提交时回传的字段名(活动契约 `input.data.properties` 的键) */
|
||||
key: z.string().min(1),
|
||||
label: z.string(),
|
||||
type: launchParamTypeSchema,
|
||||
required: z.boolean().optional(),
|
||||
desc: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
/** select / combo 的候选;combo 提交候选之外的值也合法 */
|
||||
options: z.array(launchOptionSchema).optional(),
|
||||
default: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
});
|
||||
export type LaunchParam = z.infer<typeof launchParamSchema>;
|
||||
|
||||
export const launchParamsSchema = z.object({
|
||||
activityId: z.string(),
|
||||
version: z.string(),
|
||||
params: z.array(launchParamSchema),
|
||||
});
|
||||
export type LaunchParams = z.infer<typeof launchParamsSchema>;
|
||||
|
||||
export const launchInputSchema = z.object({
|
||||
/** 前端每开一次抽屉生成一枚;同键重放返回第一次创建的实例 */
|
||||
idempotencyKey: z.string().min(1).max(128),
|
||||
/** 责任人 —— 固定字段,不进参数表;须是名册里在职的人 */
|
||||
ownerId: staffIdSchema,
|
||||
/** 键值形状由参数表决定:list 回字符串数组,其余回字符串 */
|
||||
params: z.record(z.string(), z.union([z.string(), z.array(z.string())])),
|
||||
});
|
||||
export type LaunchInput = z.infer<typeof launchInputSchema>;
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* 运营中心 · 活动模块的路由契约(`docs/API文档-活动模块.md`)。
|
||||
*
|
||||
* 全部 `access: 'ops'` —— 活动区落在运营中心,与工作流 / 任务看板同端。
|
||||
* `/api/gov/*` 的写路由由 app 中间件自动广播 `{kind:'system', id:'gov'}`。
|
||||
* 设计端的活动库(包版本 / 资产版本 / 装入)在 `/api/gov/activity-packages`(governance.ts),
|
||||
* `/api/gov/activities/*` 整个归运营端(2026-08-27 用户定)。
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import type { RouteContract } from './http.ts';
|
||||
import {
|
||||
activityDetailSchema,
|
||||
activityStatusSchema,
|
||||
activitySummarySchema,
|
||||
instanceDetailSchema,
|
||||
instanceListSchema,
|
||||
instanceStateSchema,
|
||||
launchInputSchema,
|
||||
launchParamsSchema,
|
||||
} from '../activity-ops.ts';
|
||||
|
||||
const activityParams = z.object({ activityId: z.string().min(1) });
|
||||
const instanceParams = activityParams.extend({ instanceId: z.string().min(1) });
|
||||
/** 启动日期段,`YYYY-MM-DD`,含两端整日 */
|
||||
const day = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, '日期要写成 YYYY-MM-DD');
|
||||
|
||||
/* ── L1 ── */
|
||||
|
||||
export const listOpsActivitiesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities',
|
||||
access: 'ops',
|
||||
/** 不带 status = 全量;量小不分页 */
|
||||
query: z.object({ status: activityStatusSchema.optional() }),
|
||||
output: z.object({ activities: z.array(activitySummarySchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── L2 ── */
|
||||
|
||||
export const getOpsActivityRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId',
|
||||
access: 'ops',
|
||||
params: activityParams,
|
||||
output: activityDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 沙箱要渲染的活动视图 —— 处理器回 `text/html`(不是 JSON),`output` 只是形式上的字符串。
|
||||
* iframe 带不了 Authorization 头,走票据 `?ticket=`(GET,中间件已认)。404 = 该活动没配视图。
|
||||
*/
|
||||
export const opsActivityViewRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId/view',
|
||||
access: 'ops',
|
||||
params: activityParams,
|
||||
output: z.string(),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const listOpsInstancesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId/instances',
|
||||
access: 'ops',
|
||||
params: activityParams,
|
||||
query: z.object({
|
||||
/** 实例号模糊搜(对 `no` 与 `id` 的子串) */
|
||||
id: z.string().optional(),
|
||||
ownerId: z.string().optional(),
|
||||
state: instanceStateSchema.optional(),
|
||||
startedFrom: day.optional(),
|
||||
startedTo: day.optional(),
|
||||
page: z.coerce.number().int().min(1).default(1),
|
||||
/** 默认 8(与前端单页一致),上限 50 */
|
||||
size: z.coerce.number().int().min(1).max(50).default(8),
|
||||
}),
|
||||
output: instanceListSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── 启动 ── */
|
||||
|
||||
export const opsLaunchParamsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId/launch-params',
|
||||
access: 'ops',
|
||||
params: activityParams,
|
||||
output: launchParamsSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 启动活动 = 创建实例。同一 `idempotencyKey` 重放返回第一次创建的实例;
|
||||
* 412 活动已禁用;400 缺必填 / 类型不符 / select 值不在候选 / 责任人不在职;409 同键正在创建中。
|
||||
*/
|
||||
export const launchOpsInstanceRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/activities/:activityId/instances',
|
||||
access: 'ops',
|
||||
params: activityParams,
|
||||
body: launchInputSchema,
|
||||
output: instanceDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── L3 ── */
|
||||
|
||||
export const getOpsInstanceRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId/instances/:instanceId',
|
||||
access: 'ops',
|
||||
params: instanceParams,
|
||||
output: instanceDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 实例文件:默认 JSON + base64(与 `GET /api/gov/upload/:id` 同形);`?raw=1` 直出字节
|
||||
* (RFC 5987 文件名),配 `?ticket=` 给 `<a href download>` 用。
|
||||
*/
|
||||
export const opsInstanceFileRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId/instances/:instanceId/files/:io/:index',
|
||||
access: 'ops',
|
||||
params: instanceParams.extend({ io: z.enum(['input', 'output']), index: z.coerce.number().int().min(0) }),
|
||||
query: z.object({ raw: z.string().optional() }),
|
||||
output: z.object({ name: z.string(), contentType: z.string(), dataBase64: z.string() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── 禁用 / 激活 / 终止 ── */
|
||||
|
||||
/** 任何时刻都可禁用:只拦新启动,在途实例继续跑完;已禁用再禁用 = 幂等 200 */
|
||||
export const disableOpsActivityRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/activities/:activityId/disable',
|
||||
access: 'ops',
|
||||
params: activityParams,
|
||||
output: activityDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const activateOpsActivityRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/activities/:activityId/activate',
|
||||
access: 'ops',
|
||||
params: activityParams,
|
||||
output: activityDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** body 空;→ `state='failed'` + `reason.kind='terminated'`;412 实例已终态 */
|
||||
export const terminateOpsInstanceRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/activities/:activityId/instances/:instanceId/terminate',
|
||||
access: 'ops',
|
||||
params: instanceParams,
|
||||
output: instanceDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 全部 11 条 —— 文档总表与覆盖测试按它数 */
|
||||
export const ACTIVITY_OPS_ROUTES: readonly RouteContract[] = [
|
||||
listOpsActivitiesRoute,
|
||||
getOpsActivityRoute,
|
||||
opsActivityViewRoute,
|
||||
listOpsInstancesRoute,
|
||||
opsLaunchParamsRoute,
|
||||
launchOpsInstanceRoute,
|
||||
getOpsInstanceRoute,
|
||||
opsInstanceFileRoute,
|
||||
disableOpsActivityRoute,
|
||||
activateOpsActivityRoute,
|
||||
terminateOpsInstanceRoute,
|
||||
];
|
||||
@@ -537,10 +537,12 @@ export const saveWorkGroupRoute = {
|
||||
* 安装两审在服务端跑:静态审读清单(工具契约在否、模型名绑没绑、模版有没有脚本、
|
||||
* 声明与已物化的台账冲不冲),回放审重演历史(首装无历史即免,以样例集替代)。
|
||||
* **恒为新增、版本并存**:装入不覆盖旧版本,在途任务按其启动时版本跑完。
|
||||
* **路径(2026-08-27)**:`/api/gov/activity-packages` —— 从前叫 `/api/gov/activities`;那个前缀整个让给了运营端
|
||||
* 活动模块(L1/L2/L3,`api/activity-ops.ts`),这里管的是**包**(版本 / 资产 / 装入),名副其实。
|
||||
*/
|
||||
export const installActivityRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/activities',
|
||||
path: '/api/gov/activity-packages',
|
||||
access: 'design',
|
||||
body: z.object({
|
||||
/** 清单六区:身份 / 七件契约 / 声明 / 资产初始版 / 样例集 / 代码入口 */
|
||||
@@ -606,9 +608,10 @@ export const registerToolRoute = {
|
||||
|
||||
/* ── 活动库管理:两维分类 × 包版本 × 资产版本 ─────────────────────────── */
|
||||
|
||||
/** 路径同 `installActivityRoute` 的说明:`/api/gov/activity-packages`(2026-08-27 让位给运营端活动模块) */
|
||||
export const listActivitiesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities',
|
||||
path: '/api/gov/activity-packages',
|
||||
access: 'design',
|
||||
output: z.object({
|
||||
activities: z.array(
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './governance.ts';
|
||||
export * from './org-directory.ts';
|
||||
export * from './ingest.ts';
|
||||
export * from './mail.ts';
|
||||
export * from './activity-ops.ts';
|
||||
|
||||
@@ -15,4 +15,5 @@ export * from './process-package.ts';
|
||||
export * from './manifest.ts';
|
||||
export * from './org.ts';
|
||||
export * from './mail.ts';
|
||||
export * from './activity-ops.ts';
|
||||
export * from './api/index.ts';
|
||||
|
||||
Reference in New Issue
Block a user