Archived
- contracts 补建 activity-ops(schema + 11 条路由);L1/L2/L3 全真接口对接(清单/详情/实例/启动抽屉/沙箱) - 启动抽屉:参数表服务端下发、幂等键防重、400 逐格报错;combo「新增」提为候选第一项 - 终止/禁用/激活轻气泡反馈闭环:在途留场转忙碌、成功自闭合并刷新、失败原因留气泡可重试 - L3 实例详情 7 秒轮询(待执行/进行中),终态自动停 - 员工模块四页改版:目录行+首字圆块、权限三槽点位、组织树/空态、成员详情 hero+角色开关;卡内容区内边距补齐
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 活动模块(运营中心 · 活动区)的数据结构 —— 口径:`docs/API文档-活动模块.md` §1。
|
||||
* 本文档以「契约已落地」的语气写成,但文件当时并不在仓库里(2026-08-27 对接时发现)——
|
||||
* 这里按文档 §1/§9 把 schema 补齐,两者不一致时回来改文档。
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
/* ── 枚举与标签 ─────────────────────────────────────────────────────────── */
|
||||
|
||||
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: '已禁用' };
|
||||
|
||||
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 ACTIVITY_HUES = ['blue', 'teal', 'amber', 'violet'] as const;
|
||||
export const activityHueSchema = z.enum(ACTIVITY_HUES);
|
||||
export type ActivityHue = z.infer<typeof activityHueSchema>;
|
||||
|
||||
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>;
|
||||
|
||||
/** 沙箱视图向父页报内容高度的 postMessage type(视图 origin 是 'null',父页按 source 认) */
|
||||
export const ACTIVITY_VIEW_HEIGHT_MESSAGE = 'jcai:view-height';
|
||||
|
||||
/* ── 基础件 ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** 凡出人都是这两段:展示用 name,筛选 / 提交用 id(= 名册 staffId) */
|
||||
export const staffRefSchema = z.object({ id: z.string(), name: z.string() });
|
||||
export type StaffRef = z.infer<typeof staffRefSchema>;
|
||||
|
||||
/* ── 活动 ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** L1 卡片(§1.2) */
|
||||
export const activitySummarySchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
biz: z.string(),
|
||||
status: activityStatusSchema,
|
||||
version: z.string(),
|
||||
/** 没建档 = null,前端摆「—」 */
|
||||
owner: staffRefSchema.nullable(),
|
||||
llm: z.string(),
|
||||
instanceCount: z.number(),
|
||||
createdAt: z.string(),
|
||||
/** 展示元数据可缺:缺省前端兜底 inbox / blue */
|
||||
ico: z.string().optional(),
|
||||
hue: activityHueSchema.optional(),
|
||||
});
|
||||
export type ActivitySummary = z.infer<typeof activitySummarySchema>;
|
||||
|
||||
/** L2 头部(§1.3)= Summary 去掉 instanceCount,加 desc / view */
|
||||
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>;
|
||||
|
||||
/* ── 实例清单(§1.4 / §1.5)─────────────────────────────────────────────── */
|
||||
|
||||
export const instanceSummarySchema = z.object({
|
||||
/** 寻址键(任务 id,uuid):L3 路由、终止、文件都用它 */
|
||||
id: z.string(),
|
||||
/** 人读实例号 INS-<序号>:表格上摆它、模糊搜也认它 */
|
||||
no: z.string(),
|
||||
state: instanceStateSchema,
|
||||
owner: staffRefSchema,
|
||||
/** 输入一句话摘要(≤80 字,可为 '') */
|
||||
input: z.string(),
|
||||
/** 产出一句话摘要;只有 completed 才有值;null 摆 `-` */
|
||||
output: z.string().nullable(),
|
||||
startedAt: z.string(),
|
||||
});
|
||||
export type InstanceSummary = z.infer<typeof instanceSummarySchema>;
|
||||
|
||||
export const instanceStatsSchema = z.object({
|
||||
/** 活动整体总数,不随检索条件动(页头统计带用) */
|
||||
total: z.number(),
|
||||
/** 五个键都在,没有的是 0 */
|
||||
byState: z.record(instanceStateSchema, z.number()),
|
||||
});
|
||||
export type InstanceStats = z.infer<typeof instanceStatsSchema>;
|
||||
|
||||
export const instanceListSchema = z.object({
|
||||
stats: instanceStatsSchema,
|
||||
/** 筛后条数,分页器按它 */
|
||||
total: z.number(),
|
||||
page: z.number(),
|
||||
size: z.number(),
|
||||
items: z.array(instanceSummarySchema),
|
||||
});
|
||||
export type InstanceList = z.infer<typeof instanceListSchema>;
|
||||
|
||||
/* ── 启动(§1.6 / §1.7)─────────────────────────────────────────────────── */
|
||||
|
||||
export const launchParamOptionSchema = z.object({ value: z.string(), label: z.string() });
|
||||
|
||||
export const launchParamSchema = z.object({
|
||||
key: z.string(),
|
||||
label: z.string(),
|
||||
type: launchParamTypeSchema,
|
||||
required: z.boolean().optional(),
|
||||
desc: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
/** select / combo 的候选,两段式;combo 可能没有这个键(候选读不到 = 自由输入) */
|
||||
options: z.array(launchParamOptionSchema).optional(),
|
||||
/** 预填值:list 给字符串数组,其余给字符串 */
|
||||
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({
|
||||
/** 幂等键:每开一次抽屉生成一枚(crypto.randomUUID());重复提交、网络重试都带同一枚 */
|
||||
idempotencyKey: z.string().min(1).max(128),
|
||||
/** 责任人 staffId(固定字段,不在参数表里) */
|
||||
ownerId: z.string().min(1),
|
||||
/** 键 = LaunchParam.key;list 传 string[],其余传 string;选填没填可以不传或传 '' */
|
||||
params: z.record(z.string(), z.union([z.string(), z.array(z.string())])),
|
||||
});
|
||||
export type LaunchInput = z.infer<typeof launchInputSchema>;
|
||||
|
||||
/* ── 实例详情(§1.8 – §1.12)────────────────────────────────────────────── */
|
||||
|
||||
export const instanceFileSchema = z.object({
|
||||
/** 同一侧(input / output)文件里的序号,从 0 起;下载用 */
|
||||
index: z.number(),
|
||||
name: z.string(),
|
||||
contentType: z.string(),
|
||||
size: z.number(),
|
||||
});
|
||||
export type InstanceFile = z.infer<typeof instanceFileSchema>;
|
||||
|
||||
export const instanceReasonSchema = z.object({
|
||||
/** 驳回 / 自己跑挂 / 手动终止 */
|
||||
kind: z.enum(['rejected', 'failed', 'terminated']),
|
||||
/** 人话原因,直接摆 */
|
||||
text: z.string(),
|
||||
});
|
||||
export type InstanceReason = z.infer<typeof instanceReasonSchema>;
|
||||
|
||||
export const instanceSourceSchema = z.object({
|
||||
/** manual / upstream / mail 等通道名 / testbed / system —— 开放枚举,不认识的按 label 摆 */
|
||||
kind: z.string(),
|
||||
/** 人读形式,直接摆 */
|
||||
label: z.string(),
|
||||
});
|
||||
export type InstanceSource = z.infer<typeof instanceSourceSchema>;
|
||||
|
||||
export const instanceStepSchema = z.object({
|
||||
/** 从 1 起,按活动案例模型的步骤序 */
|
||||
seq: z.number(),
|
||||
name: z.string(),
|
||||
by: z.enum(['agent', 'human']),
|
||||
/** agent → 模型名;human → 责任人姓名 */
|
||||
byName: z.string(),
|
||||
/** 开始时刻;没到这一步 = null */
|
||||
at: z.string().nullable(),
|
||||
/** 用时(毫秒);未完成 = null */
|
||||
durationMs: z.number().nullable(),
|
||||
/** 已完成 / 出事(性质看 reason.kind)/ 正在办 / 未到 */
|
||||
res: z.enum(['done', 'fail', 'current', 'pending']),
|
||||
note: z.string(),
|
||||
});
|
||||
export type InstanceStep = z.infer<typeof instanceStepSchema>;
|
||||
|
||||
export const instanceDetailSchema = instanceSummarySchema.omit({ input: true, output: true }).extend({
|
||||
activityId: z.string(),
|
||||
llm: z.string(),
|
||||
/** 未结束 = null;终止时 = 终止时刻 */
|
||||
endedAt: z.string().nullable(),
|
||||
/** endedAt − startedAt;未结束 = null */
|
||||
durationMs: z.number().nullable(),
|
||||
/** null = 不需要(待执行 / 进行中 / 正常完成) */
|
||||
reason: instanceReasonSchema.nullable(),
|
||||
input: z.object({
|
||||
content: z.string(),
|
||||
source: instanceSourceSchema,
|
||||
/** = startedAt */
|
||||
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>;
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 活动模块(运营中心 · 活动区)的 11 条路由 —— 口径:`docs/API文档-活动模块.md`。
|
||||
* 全部 `access: 'ops'`;写路由(启动 / 禁用 / 激活 / 终止)成功后服务端广播 `{kind:'system', id:'gov'}`。
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import type { RouteContract } from './http.ts';
|
||||
import {
|
||||
activityDetailSchema,
|
||||
activityStatusSchema,
|
||||
activitySummarySchema,
|
||||
instanceDetailSchema,
|
||||
instanceListSchema,
|
||||
instanceStateSchema,
|
||||
launchInputSchema,
|
||||
launchParamsSchema,
|
||||
} from '../activity-ops.ts';
|
||||
|
||||
const activityIdParam = z.object({ activityId: z.string().min(1) });
|
||||
const instanceIdParams = z.object({ activityId: z.string().min(1), instanceId: z.string().min(1) });
|
||||
|
||||
/** L1 活动清单 —— 两页签(激活中 / 已禁用)各取一份;不带 status = 全量;不分页 */
|
||||
export const listOpsActivitiesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities',
|
||||
access: 'ops',
|
||||
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: activityIdParam,
|
||||
output: activityDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 活动视图(沙箱):整份 HTML,iframe 直渲;装载那一刻取票据拼 ?ticket=(60 秒过期,别预取) */
|
||||
export const opsActivityViewRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId/view',
|
||||
access: 'ops',
|
||||
params: activityIdParam,
|
||||
query: z.object({ ticket: z.string().optional() }),
|
||||
output: z.string(),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** L2 实例清单:检索四条件 + 分页全在服务端;stats 是全局五态统计,total 是筛后条数 */
|
||||
export const listOpsInstancesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId/instances',
|
||||
access: 'ops',
|
||||
params: activityIdParam,
|
||||
query: z.object({
|
||||
id: z.string().optional(),
|
||||
ownerId: z.string().optional(),
|
||||
state: instanceStateSchema.optional(),
|
||||
startedFrom: z.string().optional(),
|
||||
startedTo: z.string().optional(),
|
||||
page: z.string().optional(),
|
||||
size: z.string().optional(),
|
||||
}),
|
||||
output: instanceListSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 启动参数表:一活动一份,前端只按 params 渲染表单,不写死字段 */
|
||||
export const opsLaunchParamsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId/launch-params',
|
||||
access: 'ops',
|
||||
params: activityIdParam,
|
||||
output: launchParamsSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 启动活动(创建实例):同一 idempotencyKey 重放回 200 给第一单,不起第二个 */
|
||||
export const launchOpsInstanceRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/activities/:activityId/instances',
|
||||
access: 'ops',
|
||||
params: activityIdParam,
|
||||
body: launchInputSchema,
|
||||
output: instanceDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** L3 实例详情;instanceId = 清单行的 id(任务 uuid),不是 no */
|
||||
export const getOpsInstanceRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId/instances/:instanceId',
|
||||
access: 'ops',
|
||||
params: instanceIdParams,
|
||||
output: instanceDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 实例文件:不带 raw 回 JSON(name / contentType / dataBase64);带 raw=1 直出文件字节(<a download> 走票据) */
|
||||
export const opsInstanceFileRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities/:activityId/instances/:instanceId/files/:io/:index',
|
||||
access: 'ops',
|
||||
params: instanceIdParams.extend({ io: z.enum(['input', 'output']), index: z.string().regex(/^\d+$/) }),
|
||||
query: z.object({ raw: z.literal('1').optional(), ticket: 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: activityIdParam,
|
||||
output: activityDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 激活:幂等 */
|
||||
export const activateOpsActivityRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/activities/:activityId/activate',
|
||||
access: 'ops',
|
||||
params: activityIdParam,
|
||||
output: activityDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 终止实例:pending / running 都可终止;已终态的 412。终止后 state='failed' + reason.kind='terminated' */
|
||||
export const terminateOpsInstanceRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/activities/:activityId/instances/:instanceId/terminate',
|
||||
access: 'ops',
|
||||
params: instanceIdParams,
|
||||
output: instanceDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 这一屏的全部路由 —— 归属一处列全,测试拿它逐条断言(同 ORG_DIRECTORY_ROUTES 的考量)。
|
||||
*/
|
||||
export const ACTIVITY_OPS_ROUTES = [
|
||||
listOpsActivitiesRoute,
|
||||
getOpsActivityRoute,
|
||||
opsActivityViewRoute,
|
||||
listOpsInstancesRoute,
|
||||
opsLaunchParamsRoute,
|
||||
launchOpsInstanceRoute,
|
||||
getOpsInstanceRoute,
|
||||
opsInstanceFileRoute,
|
||||
disableOpsActivityRoute,
|
||||
activateOpsActivityRoute,
|
||||
terminateOpsInstanceRoute,
|
||||
] as const;
|
||||
@@ -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