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,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';
|
||||
|
||||
Reference in New Issue
Block a user