Archived
CI / ci (push) Failing after 10s
契约:launchOption 加 group/groupLabel(x-options.group 从字典表现读人读名)、 launchParam 加 linkGroup(x-link 结组);api-report 已随 --update 入库。 前端 launch-link.ts 纯函数管三条口径:锚摆全量、其余按锚的分组键收窄 (未标类型殿后一组,新增照走);锚粘着 —— 清空只解除收窄不换锚,人改得了主意。 服务端不据此立墙:combo 本就收候选之外的值,类型一致归 S1 收步判据 sub_eval_part_distinct_chip_type_id = 1(数得出来的门)。 W05 清单两格挂上 x-link: chip-type;文档同步。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
633 lines
37 KiB
Markdown
633 lines
37 KiB
Markdown
# API 文档 —— 活动模块(L1 清单 / L2 活动详情 / L3 实例详情)
|
||
|
||
> 本文是活动模块前端(运营中心 · 活动区,2026-08-25 重构模版)与服务端之间的接口约定,给前端取数、后端维护用。
|
||
> 鉴权 / 票据 / 错误体 / 端归属的**通则**在 `docs/API文档-鉴权与人员.md` §0,本文 §0.1 只摘调用时必须知道的几条。
|
||
> 需求出处:`docs/活动模块重构需求-2026-08-25.md`;启动参数口径:`ACT-WORK-05-一键启动入参说明.md`(极简四参)。
|
||
> **契约已落地(2026-08-27)**:`packages/contracts/src/activity-ops.ts`(数据结构 zod)+ `packages/contracts/src/api/activity-ops.ts`
|
||
> (11 条路由,花名册 `ACTIVITY_OPS_ROUTES`);前端一律 `import { xxxRoute } from '@contracts'` 经 `shared/typed-fetch` 的 `call(route, { params, query, body })` 调用,
|
||
> 类型直接用 `@contracts` 导出的(§1 每张表都标了导出名)。两者不一致时以契约为准并回来改本文。
|
||
> 设计端的活动库两条路由已改名让位:`GET/POST /api/gov/activities` → `/api/gov/activity-packages`(前端 import 路由对象,不用改)。
|
||
|
||
## 0. 通则
|
||
|
||
### 0.1 调用约定
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 前缀 | `/api/gov/activities/*`;**11 条全部 `access: 'ops'`** —— 登录人的 `ends` 里要有 `ops`,否则 403 |
|
||
| 请求头 | `Authorization: Bearer <Keycloak access_token>`;写请求 `content-type: application/json` |
|
||
| 票据 | 只有两条路带不了请求头(`<iframe src>` / `<a href download>`):§2.3 视图、§7 文件。GET 时改带 `?ticket=<票据>`(取自 `GET /api/auth/ticket`,**60 秒有效,用的那一刻再取**) |
|
||
| 响应 | 除 §2.3(`text/html`)与 §7 `?raw=1`(文件字节)外,全部 `application/json` |
|
||
| 时间 | 一律 ISO 8601 UTC 字符串(`2026-08-27T02:25:11.000Z`);**展示格式化是前端的事**(`YYYY-MM-DD HH:mm:ss` 本地时区,用 `shared/at.ts`) |
|
||
| 人员 | 凡出人,都是 `StaffRef = { id, name }` —— 展示用 `name`,筛选 / 提交用 `id`(= 名册 staffId);候选名单用既有 `GET /api/gov/staff` |
|
||
| 实时刷新 | 写路由(启动 / 禁用 / 激活 / 终止)成功后服务端广播 `{kind:'system', id:'gov'}`,前端既有 SSE 订阅会失效 `['gov', …]` 缓存 —— 查询键放在 `['gov', 'activities', …]` 下就自动重取 |
|
||
| 分页 | 只有 §3 实例清单分页(`page` 从 1 起,`size` 默认 8、上限 50);其余不分页 |
|
||
|
||
### 0.2 错误体
|
||
|
||
所有非 2xx 都是同一形状:
|
||
|
||
```json
|
||
{ "code": "validation-failed", "message": "启动参数不合要求:candidateInternalPns 必填,至少一项", "detail": [ { "key": "candidateInternalPns", "message": "必填,至少一项" } ] }
|
||
```
|
||
|
||
| 状态 | `code` | 何时 | `detail` |
|
||
|---|---|---|---|
|
||
| 400 | `validation-failed` | 查询参数 / 路径参数 / 请求体不合 schema;启动参数缺必填 / 类型不对 / select 值不在候选 / 不认识的参数名 / 责任人不在职 | schema 不合 → zod issues 数组;启动参数问题 → `[{ key, message }]`,`key` 是哪一格(责任人那一条 `key='ownerId'`) |
|
||
| 401 | `unauthenticated` | 没带 token / 过期 / 票据坏了 | — |
|
||
| 403 | `forbidden` | 登录人进不了运营中心 | `{ need, ends }` |
|
||
| 404 | `not-found` | 活动不存在;实例不存在或不属于这个活动;活动没配视图;文件序号不存在 | — |
|
||
| 409 | `conflict` | 启动:同一 `idempotencyKey` 的实例**正在创建**(60 秒内再来);或这枚键已用在别的活动上 | — |
|
||
| 412 | `precondition-failed` | 启动:活动已禁用;终止:实例已终态 | — |
|
||
| 500 | `internal` | 服务端出错 | — |
|
||
|
||
### 0.3 状态口径
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 活动状态 `ActivityStatus` | `active`(激活中)/ `disabled`(已禁用)。中文在 `@contracts` 的 `ACTIVITY_STATUS_LABEL` |
|
||
| 实例状态 `InstanceState` | 五态英文码:`pending`(待执行)/ `running`(进行中)/ `completed`(已完成)/ `rejected`(驳回)/ `failed`(失败);中文在 `INSTANCE_STATE_LABEL`。**没有第六态** —— 手动终止归 `failed`,靠 `reason.kind='terminated'` 区分「自己跑挂」。口径:`pending` = 建了单、推进器还没领到;`running` = 在途(含等人回传 / 监听中);`completed` = 放行完成;`rejected` = 验收被负责人打回、模型还没重新提交的那一段(重新提交后回 `running`);`failed` = 跑挂(受阻 / 停滞熔断 / 预算耗尽)与手动终止 |
|
||
| 禁用语义 | **任何时刻都可禁用**:只拦「新启动」(§5 回 412);已完成的实例是历史留痕,进行中的**继续跑完**;激活即恢复 |
|
||
| 活动档案 | 负责人 / 说明 / 图标 / 色相来自运营侧档案表,不在活动包里;**没建档时 `owner` 是 `null`、`desc` 是空串、`ico`/`hue` 缺席**,前端摆「—」并兜底图标色 |
|
||
|
||
## 1. 数据结构(`@contracts` 导出名在括号里)
|
||
|
||
字段列「必有」= 响应里一定带这个键(可能是 `null` 的会单独写);「可缺」= 键可能不出现。
|
||
|
||
### 1.1 `StaffRef`(`staffRefSchema`)
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `id` | string | 名册 staffId |
|
||
| `name` | string | 姓名 |
|
||
|
||
### 1.2 `ActivitySummary`(`activitySummarySchema`)—— L1 卡片
|
||
|
||
| 字段 | 类型 | 必有 | 说明 |
|
||
|---|---|---|---|
|
||
| `id` | string | 是 | 活动 id,如 `part-substitution-assessment`;后面所有路径里的 `:activityId` |
|
||
| `name` | string | 是 | 活动名 |
|
||
| `biz` | string | 是 | 业务分类(卡片副题) |
|
||
| `status` | `ActivityStatus` | 是 | `active` / `disabled` |
|
||
| `version` | string | 是 | 当前生效的包版本,如 `1.5.7` |
|
||
| `owner` | `StaffRef` \| **null** | 是 | 活动负责人;没建档 = `null`,摆「—」 |
|
||
| `llm` | string | 是 | 绑定的模型(逻辑名,如 `deepseek-v4-flash`) |
|
||
| `instanceCount` | number | 是 | 该活动的实例总数(含测试台试跑单) |
|
||
| `createdAt` | string(ISO) | 是 | 首个版本装入的时刻 |
|
||
| `ico` | string | 可缺 | 语义图标名;缺省前端兜底 `inbox` |
|
||
| `hue` | `'blue' \| 'teal' \| 'amber' \| 'violet'` | 可缺 | 业务色相;缺省前端兜底 `blue` |
|
||
|
||
### 1.3 `ActivityDetail`(`activityDetailSchema`)—— L2 头部
|
||
|
||
`ActivitySummary` **去掉 `instanceCount`**,加:
|
||
|
||
| 字段 | 类型 | 必有 | 说明 |
|
||
|---|---|---|---|
|
||
| `desc` | string | 是 | 定义说明;没写就是 `''` |
|
||
| `view` | `{ url: string }` \| **null** | 是 | 沙箱要渲染的活动视图地址(相对路径,见 §2.3);`null` = 藏起整个沙箱区块 |
|
||
|
||
### 1.4 `InstanceSummary`(`instanceSummarySchema`)—— L2 表格一行
|
||
|
||
| 字段 | 类型 | 必有 | 说明 |
|
||
|---|---|---|---|
|
||
| `id` | string | 是 | 实例的**寻址键**(任务 id,uuid):L3 路由、终止、文件都用它 |
|
||
| `no` | string | 是 | 人读实例号 `INS-<序号>`(表格上摆它、模糊搜也认它) |
|
||
| `state` | `InstanceState` | 是 | 五态 |
|
||
| `owner` | `StaffRef` | 是 | 责任人 |
|
||
| `input` | string | 是 | 输入一句话摘要(≤80 字,可为 `''`) |
|
||
| `output` | string \| **null** | 是 | 产出一句话摘要;只有 `completed` 才有值(文件名,或「已发口:<口名>」);`null` 摆 `-` |
|
||
| `startedAt` | string(ISO) | 是 | 启动时刻 |
|
||
|
||
### 1.5 `InstanceStats` / `InstanceList`(`instanceStatsSchema` / `instanceListSchema`)
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `stats.total` | number | 该活动实例总数(**不随检索条件动**,页头统计带用) |
|
||
| `stats.byState` | `Record<InstanceState, number>` | 五个键**都在**(没有的是 0) |
|
||
| `total` | number | **筛后**条数,分页器按它 |
|
||
| `page` | number | 当前页(回显请求值,默认 1) |
|
||
| `size` | number | 单页条数(回显请求值,默认 8) |
|
||
| `items` | `InstanceSummary[]` | 当前页,启动时间倒序 |
|
||
|
||
### 1.6 `LaunchParam` / `LaunchParams`(`launchParamSchema` / `launchParamsSchema`)—— 启动表单规格
|
||
|
||
`LaunchParams`:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `activityId` | string | |
|
||
| `version` | string | 参数表对应的包版本 |
|
||
| `params` | `LaunchParam[]` | **按这个顺序渲染表单** |
|
||
|
||
`LaunchParam`:
|
||
|
||
| 字段 | 类型 | 必有 | 说明 |
|
||
|---|---|---|---|
|
||
| `key` | string | 是 | 提交时 `params` 里的键名(英文) |
|
||
| `label` | string | 是 | 表单上的中文名 |
|
||
| `type` | `'text' \| 'textarea' \| 'select' \| 'combo' \| 'list'` | 是 | 控件种类:单行 / 多行 / 单选(只能选候选)/ **选择+新增**(候选末项「新增」,选了换输入框自填)/ 字符串数组(逐行自由填,可加多项)。**遇到不认识的 type 摆「暂不支持的控件」**。前端只按 `type` 渲染 —— 活动契约里是数组的字段也可能摆成 `combo`/`select`(只选一颗,提交单个字符串,后端包成数组),前端不必知道 |
|
||
| `required` | boolean | 可缺 | 缺席 = 选填。前端没填完禁提交;服务端仍再验一道(400) |
|
||
| `desc` | string | 可缺 | 控件下的辅助说明 |
|
||
| `placeholder` | string | 可缺 | text / textarea / combo(新增态)/ list 用 |
|
||
| `options` | `{ value: string; label: string }[]` | 可缺 | `select` / `combo` 的候选,两段式:`value` 是提交值,`label` 是显示名。**combo 可能没有这个键**(候选读不到),那时就是自由输入 |
|
||
| `default` | string \| string[] | 可缺 | 预填值:`list` 给字符串数组,其余给字符串 |
|
||
|
||
### 1.7 `LaunchInput`(`launchInputSchema`)—— 启动请求体
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `idempotencyKey` | string(1–128) | 是 | 幂等键:**每开一次抽屉生成一枚**(`crypto.randomUUID()`);重复点提交、网络重试都带同一枚 |
|
||
| `ownerId` | string | 是 | 责任人 staffId(固定字段,不在参数表里);默认当前登录人 `me.staff.id`,允许改;只列 `GET /api/gov/staff` 里 `state='active'` 的 |
|
||
| `params` | `Record<string, string \| string[]>` | 是 | 键 = `LaunchParam.key`;`list` 传 `string[]`,其余传 `string`;`select` 传某个 `options[].value`;`combo` 传候选的 `value` **或**新增的原样字符串(两种都合法);选填没填可以不传或传 `''`。服务端宽收:活动契约里是数组的字段(含 `list`)传 `string` 或 `string[]` 都行,统一归成数组交活动 |
|
||
|
||
### 1.8 `InstanceFile`(`instanceFileSchema`)
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `index` | number | 在同一侧(input / output)文件里的序号,从 0 起;§7 下载用 |
|
||
| `name` | string | 文件名(含扩展名) |
|
||
| `contentType` | string | MIME |
|
||
| `size` | number | 字节数 |
|
||
|
||
### 1.9 `InstanceReason`(`instanceReasonSchema`)
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `kind` | `'rejected' \| 'failed' \| 'terminated'` | 驳回 / 自己跑挂 / 手动终止 |
|
||
| `text` | string | 人话原因,直接摆。驳回:「验收驳回:<负责人写的意见> —— 负责人 <名>」(没写意见时「验收驳回:负责人未写明具体意见,已退回重做」);失败:引擎原话(预算耗尽已翻成「预算耗尽:步数 90/90、活跃时长 120/120 分钟」);终止:「由 <操作人> 手动终止」 |
|
||
|
||
### 1.10 `InstanceSource`(`instanceSourceSchema`)
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `kind` | string | `manual`(运营端手动启动)/ `upstream`(上游工作流节点)/ `mail` 等通道名(来件触发)/ `testbed`(测试台试跑)/ `system`(其余)。**开放枚举**,不认识的按 `label` 摆 |
|
||
| `label` | string | 人读形式,直接摆:`手动启动 · 柴浩` / `上游 · <工作流主题>` / `邮件 · <发件方>` / `测试台试跑` / `系统起单` |
|
||
|
||
### 1.11 `InstanceStep`(`instanceStepSchema`)—— L3 时间线一行
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `seq` | number | 从 1 起,按活动案例模型的步骤序 |
|
||
| `name` | string | 步骤名(活动定义的计划项名) |
|
||
| `by` | `'agent' \| 'human'` | 这一步谁办:AI / 人 |
|
||
| `byName` | string | `agent` → 模型名(同 `llm`);`human` → 责任人姓名 |
|
||
| `at` | string(ISO) \| null | 开始时刻;没到这一步 = `null` |
|
||
| `durationMs` | number \| null | 用时(毫秒);未完成 = `null`。格式化如 `6m` |
|
||
| `res` | `'done' \| 'fail' \| 'current' \| 'pending'` | 已完成 / 出事(性质看 `reason.kind`:驳回 · 失败 · 已终止)/ 正在办 / 未到 |
|
||
| `note` | string | 这一步交出什么的一句话;可为 `''` |
|
||
|
||
### 1.12 `InstanceDetail`(`instanceDetailSchema`)—— L3 整页
|
||
|
||
| 字段 | 类型 | 必有 | 说明 |
|
||
|---|---|---|---|
|
||
| `id` | string | 是 | 任务 id |
|
||
| `no` | string | 是 | 人读实例号 |
|
||
| `activityId` | string | 是 | |
|
||
| `state` | `InstanceState` | 是 | |
|
||
| `owner` | `StaffRef` | 是 | 责任人 |
|
||
| `llm` | string | 是 | 模型名 |
|
||
| `startedAt` | string(ISO) | 是 | |
|
||
| `endedAt` | string(ISO) \| **null** | 是 | 未结束 = `null`;终止时 = 终止时刻 |
|
||
| `durationMs` | number \| **null** | 是 | `endedAt − startedAt`;未结束 = `null` |
|
||
| `reason` | `InstanceReason` \| **null** | 是 | `null` = 不需要(待执行 / 进行中 / 正常完成) |
|
||
| `input.content` | string | 是 | 输入一句话摘要(= 清单上的 `input`) |
|
||
| `input.source` | `InstanceSource` | 是 | 来源 |
|
||
| `input.submittedAt` | string(ISO) | 是 | 提交时刻(= `startedAt`) |
|
||
| `input.files` | `InstanceFile[]` | 是 | 输入文件元数据(可为空数组) |
|
||
| `output.files` | `InstanceFile[]` | 是 | 产出文件元数据;没产出是 `[]`,摆「暂无产出」 |
|
||
| `steps` | `InstanceStep[]` | 是 | 时间线 |
|
||
| | | | **第 9 步的 `res` 与 `at` 跟第 8 步走**(服务端交出前改好,2026-08-28 定):放行那一步没有自己的收步事件,不这么做已放行的单第 9 步永远 `pending`;用时不编 |
|
||
|
||
注意:详情里**没有** `input`/`output` 那两句摘要字符串(它们是清单行的字段),`input`/`output` 在详情里是上面的结构体。
|
||
|
||
## 2. L1 / L2 活动
|
||
|
||
### 2.1 活动清单 —— `GET /api/gov/activities`(`listOpsActivitiesRoute`)
|
||
|
||
**请求**
|
||
|
||
| 位置 | 参数 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|---|
|
||
| query | `status` | `'active' \| 'disabled'` | 否 | L1 两个页签各取一份;不带 = 全量。别的值 400 |
|
||
|
||
**响应 200**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `activities` | `ActivitySummary[]` | 量小不分页;顺序 = 活动库顺序 |
|
||
|
||
```json
|
||
{
|
||
"activities": [
|
||
{
|
||
"id": "part-substitution-assessment",
|
||
"name": "ACT-WORK-05 · 差异评估",
|
||
"biz": "需求分析与方案匹配",
|
||
"status": "active",
|
||
"version": "1.5.7",
|
||
"owner": null,
|
||
"llm": "deepseek-v4-flash",
|
||
"instanceCount": 12,
|
||
"createdAt": "2026-08-26T01:00:00.000Z",
|
||
"ico": "git-compare",
|
||
"hue": "teal"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 2.2 活动详情 —— `GET /api/gov/activities/:activityId`(`getOpsActivityRoute`)
|
||
|
||
**请求**:路径参数 `activityId`(string,必填)。
|
||
|
||
**响应 200**:`ActivityDetail`。**404**:活动不存在。
|
||
|
||
```json
|
||
{
|
||
"id": "part-substitution-assessment",
|
||
"name": "ACT-WORK-05 · 差异评估",
|
||
"biz": "需求分析与方案匹配",
|
||
"status": "active",
|
||
"version": "1.5.7",
|
||
"owner": { "id": "8f3c…", "name": "柴浩" },
|
||
"llm": "deepseek-v4-flash",
|
||
"createdAt": "2026-08-26T01:00:00.000Z",
|
||
"ico": "git-compare",
|
||
"hue": "teal",
|
||
"desc": "对候选料号逐项比较参数、判四态、定推荐等级,审核后落 P-11 替代关系并回写 P-01。",
|
||
"view": { "url": "/api/gov/activities/part-substitution-assessment/view" }
|
||
}
|
||
```
|
||
|
||
### 2.3 活动视图(沙箱)—— `GET /api/gov/activities/:activityId/view`(`opsActivityViewRoute`)
|
||
|
||
**请求**
|
||
|
||
| 位置 | 参数 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|---|
|
||
| path | `activityId` | string | 是 | |
|
||
| query | `ticket` | string | 是(iframe 场景) | `<iframe src>` 带不了请求头:**装载那一刻** `call(authTicketRoute)` 取一张拼上,不要渲染时预取(60 秒过期) |
|
||
|
||
**响应 200**:`content-type: text/html`(`cache-control: no-store`),整份 HTML,`<iframe sandbox="allow-scripts" src="...">` 直接渲染。
|
||
**404**:活动不存在 / 该活动没配视图(`detail.view === null` 时压根别请求)。
|
||
|
||
**沙箱里摆什么**(2026-08-27 定):活动包里名为 `activity-ledger` 的视图 —— **关键台账数据看板**,
|
||
摆本活动**最近一次已放行实例**的落账快照(页头:实例号 / 开案与放行时刻 / 负责人 / 案号 / 关联需求 / 评估基准与候选;
|
||
正文:P-01 一条 · P-08 逐颗料 + 参数瓷贴 · P-11 主记录 + 差异明细 · P-10 · P-06 本单挂的 + 同料号历史)。
|
||
行由服务端按活动清单 `viewTemplates[].data` 的取数声明从**正式表**圈出(在办中的单不摆),渲染完再注入报高脚本。
|
||
三种局面页面自己说清、都回 200:有落账(`v-ready`)/ 本活动还没有已放行实例(`v-none`,只留页头与横幅)/
|
||
最近那一单已放行但案号在正式表里查无(`v-missing`,多半是起单时关了「落真库」)。前端不需要为这三态做任何事。
|
||
|
||
**高度协议**:页面自己会向父页 `postMessage({ type: 'jcai:view-height', height: <number> }, '*')`(load、尺寸变化、每秒兜底)。父页:
|
||
|
||
```ts
|
||
window.addEventListener('message', (e) => {
|
||
if (e.source !== iframe.contentWindow) return; // 沙箱帧 origin 是 'null',按 source 认,不按 origin
|
||
if (e.data?.type !== ACTIVITY_VIEW_HEIGHT_MESSAGE) return; // 常量在 @contracts,= 'jcai:view-height'
|
||
setHeight(Number(e.data.height));
|
||
});
|
||
```
|
||
|
||
## 3. L2 实例清单 —— `GET /api/gov/activities/:activityId/instances`(`listOpsInstancesRoute`)
|
||
|
||
**请求**
|
||
|
||
| 位置 | 参数 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|---|
|
||
| path | `activityId` | string | 是 | |
|
||
| query | `id` | string | 否 | 实例号**模糊**搜,如 `INS-24`:对 `no` 与 `id` 的子串,不分大小写 |
|
||
| query | `ownerId` | string(staffId) | 否 | 精确到人 |
|
||
| query | `state` | `InstanceState` | 否 | 精确;别的值 400 |
|
||
| query | `startedFrom` | `YYYY-MM-DD` | 否 | 启动日期段起点,**含整日**;格式不对 400 |
|
||
| query | `startedTo` | `YYYY-MM-DD` | 否 | 终点,**含整日**;可只给一端 |
|
||
| query | `page` | number ≥ 1 | 否 | 默认 1 |
|
||
| query | `size` | number 1–50 | 否 | 默认 8(与前端单页一致);>50 → 400 |
|
||
|
||
条件之间 AND;都不带 = 全部。
|
||
|
||
**响应 200**:`InstanceList`。**404**:活动不存在。
|
||
|
||
```json
|
||
{
|
||
"stats": { "total": 13, "byState": { "pending": 1, "running": 1, "completed": 7, "rejected": 2, "failed": 2 } },
|
||
"total": 13,
|
||
"page": 1,
|
||
"size": 8,
|
||
"items": [
|
||
{
|
||
"id": "0c9d3a1e-…",
|
||
"no": "INS-2417",
|
||
"state": "running",
|
||
"owner": { "id": "8f3c…", "name": "柴浩" },
|
||
"input": "工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND…",
|
||
"output": null,
|
||
"startedAt": "2026-08-25T02:12:00.000Z"
|
||
},
|
||
{
|
||
"id": "7b21e6c0-…",
|
||
"no": "INS-2415",
|
||
"state": "completed",
|
||
"owner": { "id": "8f3c…", "name": "柴浩" },
|
||
"input": "…",
|
||
"output": "已发口:assessment-released",
|
||
"startedAt": "2026-08-24T09:03:00.000Z"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
- 页头统计带摆 `stats`(全局),分页器按 `total`(筛后)走;筛中提示「筛出 `total` / 共 `stats.total`」。
|
||
- 负责人下拉的候选用 `GET /api/gov/staff`(在职的),提交 `ownerId`。
|
||
- 前端**不再**本地过滤 / 分页 / 数统计,全部上移到这里。
|
||
|
||
## 4. 启动参数表 —— `GET /api/gov/activities/:activityId/launch-params`(`opsLaunchParamsRoute`)
|
||
|
||
点「启动活动」开抽屉时取。**一活动一份,前端只按 `params` 渲染表单,不写死字段。**
|
||
|
||
**请求**:路径参数 `activityId`。**响应 200**:`LaunchParams`。**404**:活动不存在。
|
||
|
||
```json
|
||
{
|
||
"activityId": "part-substitution-assessment",
|
||
"version": "1.5.7",
|
||
"params": [
|
||
{
|
||
"key": "customerCode",
|
||
"label": "客户",
|
||
"type": "combo",
|
||
"required": true,
|
||
"desc": "台账里有就直接选;是新客户就自己填客户编码,启动后派数据录入卡建档。",
|
||
"placeholder": "CU-0142",
|
||
"options": [
|
||
{ "value": "CU-0142", "label": "Itelma" },
|
||
{ "value": "CU-2039", "label": "青岛新星电子有限公司" }
|
||
]
|
||
},
|
||
{
|
||
"key": "customerName",
|
||
"label": "客户名称(仅客户不在库时填)",
|
||
"type": "text",
|
||
"desc": "新客户先把名字摆上,启动后建档时让人核。",
|
||
"placeholder": "深圳恒星电源有限公司"
|
||
},
|
||
{
|
||
"key": "requestDescription",
|
||
"label": "客户需求",
|
||
"type": "textarea",
|
||
"required": true,
|
||
"desc": "保留原始需求语义,据此新建 P-01 需求主记录(版本 V1)。",
|
||
"placeholder": "工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料"
|
||
},
|
||
{
|
||
"key": "originalInternalPn",
|
||
"label": "原内部料号",
|
||
"type": "combo",
|
||
"required": true,
|
||
"desc": "差异评估基准;库里没有该 PN 也可启动,启动后派数据工单补参数。",
|
||
"placeholder": "LM2596S-5.0",
|
||
"linkGroup": "chip-type",
|
||
"options": [{ "value": "LM2596S-5.0", "label": "LM2596S-5.0", "group": "1", "groupLabel": "DC-DC 降压稳压器" }]
|
||
},
|
||
{
|
||
"key": "candidateInternalPns",
|
||
"label": "候选内部料号",
|
||
"type": "combo",
|
||
"required": true,
|
||
"desc": "被评估物料;库里有就直接选,没有就自己填料号,启动后经数据工单创建。",
|
||
"placeholder": "XL2596S-5.0",
|
||
"linkGroup": "chip-type",
|
||
"options": [
|
||
{ "value": "LM2596S-5.0", "label": "LM2596S-5.0", "group": "1", "groupLabel": "DC-DC 降压稳压器" },
|
||
{ "value": "AMS1117-3.3", "label": "AMS1117-3.3", "group": "2", "groupLabel": "LDO 线性稳压器" },
|
||
{ "value": "OLD-001", "label": "OLD-001", "group": "" }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
W05 的候选内部料号(2026-08-27 用户定)与原内部料号**同款下拉、只选一颗**:清单里它仍是数组字段,前端按 `type='combo'` 渲染、提交单个字符串,后端包成一元数组交给活动 —— 活动契约不变。
|
||
|
||
**combo 语义(2026-08-27 用户定)**:启动表单大部分是自由输入;客户 / 料号这类「台账里可能有」的参数才用 combo —— 先选,候选里没有就新增自填。**提交候选之外的值是合法的,不拦启动**:服务端按值在台账里查,查不到照样启动,启动后派**数据工单**到工作台补这条档案(客户进客户台账、PN 补参数,确认即入库,活动从断点续跑)。combo 提交的值统一是字符串:选中候选传它的 `value`,新增传用户填的原样字符串。
|
||
|
||
候选量大到不宜内联时(如全量物料台账选 PN),后续加 `optionsSource` 引用检索接口;**本期一律内联 `options`**(封顶 500 行)。
|
||
|
||
**按分组联动(2026-08-31 用户定)**:候选可带 `group`(分组键,如这颗料的芯片类型编号;`""` = 那一列没值,即「未标类型」)与 `groupLabel`(它的人读名,字典读不到就没有这个键);参数可带 `linkGroup`(联动组名)。**同一 `linkGroup` 的几格按 `group` 互相收窄**:组内**第一个填上值的那格是锚**,锚自己永远摆全量候选,其余格只列与锚同 `group` 的候选 + `group` 为空的那些(殿后单独一组)+「新增」这条路;锚清空则整组解除收窄(锚**不让位**给还填着值的另一格 —— 否则「清空锚、改个类型重填」当场被另一格锁住),整组都空了才重新认锚。锚没有 `group`、或锚填的是候选之外的新值 → 不收窄。锚换了类型之后,另一格里**在全量候选里、却不在收窄后候选里**的值由前端清空并说明(人自己新增的值不动 —— 它压根不在候选里)。
|
||
|
||
> **这是表单的引导,不是墙**:服务端不据 `linkGroup` 校验 —— 两格填不同 `group` 的值照样收得下(combo 本就允许候选之外的值)。类型一致性由活动自己的收步判据管(W05 是 S1 的 `sub_eval_part_distinct_chip_type_id = 1`)。
|
||
|
||
> 活动清单侧怎么声明(前端不用管,给写活动包的人看):`properties.<key>` 上 `title`→label、`description`→desc、`examples[0]`→placeholder、`default`、`required[]`;`enum`+`enumNames`→select;`'x-control'` 定标量控件(`text|textarea|select|combo`);`'x-options': { ledger:'域.对象', value:'列', label:'列' }` 让候选从正式台账现读;`'x-options'.group` 让候选带上分组键 —— 只写列名(`'chip_type_id'`)= 有键没名,整份写 `{ column, ledger, value, label }` 再从字典表换人读名;`'x-link': '<组名>'` 把几格结成联动组(同组按分组键互相收窄,见上);`type:'array'` 且元素是字符串→list,**数组字段标 `x-control: combo|select` 就摆成单选**(提交单个字符串,后端包成一元数组);**`'x-order': <数字>` 定表单次序**(清单存在 jsonb 里,书写顺序到不了这里);`format:'file'` 的字段本期不上表单;select 没候选降成 text。
|
||
|
||
## 5. 启动活动(创建实例)—— `POST /api/gov/activities/:activityId/instances`(`launchOpsInstanceRoute`)
|
||
|
||
**请求**:路径参数 `activityId`;请求体 `LaunchInput`(§1.7)。
|
||
|
||
```json
|
||
{
|
||
"idempotencyKey": "b5a2d6f0-3e1c-4a7b-9d2e-8f1c0a4b7e93",
|
||
"ownerId": "8f3c1a2b-6c1e-4b3a-9f0e-2d5c7a1b9e44",
|
||
"params": {
|
||
"customerCode": "CU-0142",
|
||
"requestDescription": "工业电源项目在用 LM2596S-5.0,原厂 PCN 转 NRND,客户要求验证替代料",
|
||
"originalInternalPn": "LM2596S-5.0",
|
||
"candidateInternalPns": "XL2596S-5.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
`candidateInternalPns` 在活动契约里是数组:表单单选传 `"XL2596S-5.0"`,服务端包成 `["XL2596S-5.0"]`;传 `["XL2596S-5.0", "LM2596S-ADJ"]` 也照收。
|
||
|
||
**响应 200**:`InstanceDetail`(§1.12,与 §6 同形)——新建的实例,初始 `state='pending'`(推进器领到即转 `running`,靠 SSE 失效重取)。**同一 `idempotencyKey` 重放也回 200,给的是第一次创建的那一单,不起第二个。**
|
||
|
||
**错误**
|
||
|
||
| 状态 | 何时 | `detail` |
|
||
|---|---|---|
|
||
| 400 | 请求体不合 schema | zod issues |
|
||
| 400 | 启动参数缺必填 / 标量格传了数组 / `select` 值不在候选 / 不认识的参数名 | `[{ key, message }]`,逐格点名 —— 前端把 `message` 摆到对应控件下 |
|
||
| 400 | 责任人不在名册 / 已停用 | `[{ key: 'ownerId', message }]` |
|
||
| 404 | 活动不存在 | — |
|
||
| 409 | 同一 `idempotencyKey` 的实例正在创建(60 秒内);或这枚键已用在别的活动上 | — |
|
||
| 412 | 活动已禁用(先激活再启动) | — |
|
||
|
||
成功后:关抽屉、失效 `['gov']`、按返回的 `id` 跳 L3(路由 id = `${activityId}:${id}`)。存在性检查(客户 / 料号在不在台账)在启动后由活动做,查不到不拦启动 —— 响应里不单独回这件事,工单出现在工作台那一侧。
|
||
|
||
## 6. L3 实例详情 —— `GET /api/gov/activities/:activityId/instances/:instanceId`(`getOpsInstanceRoute`)
|
||
|
||
**请求**:路径参数 `activityId`、`instanceId`(= 清单行的 `id`,任务 uuid,**不是** `no`)。
|
||
|
||
**响应 200**:`InstanceDetail`(§1.12)。**404**:实例不存在或不属于这个活动。
|
||
|
||
```json
|
||
{
|
||
"id": "0c9d3a1e-…",
|
||
"no": "INS-2417",
|
||
"activityId": "part-substitution-assessment",
|
||
"state": "running",
|
||
"owner": { "id": "8f3c…", "name": "柴浩" },
|
||
"llm": "deepseek-v4-flash",
|
||
"startedAt": "2026-08-25T02:12:00.000Z",
|
||
"endedAt": null,
|
||
"durationMs": null,
|
||
"reason": null,
|
||
"input": {
|
||
"content": "工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND…",
|
||
"source": { "kind": "manual", "label": "手动启动 · 柴浩" },
|
||
"submittedAt": "2026-08-25T02:12:00.000Z",
|
||
"files": [
|
||
{ "index": 0, "name": "XL2596S-5.0.pdf", "contentType": "application/pdf", "size": 48210 }
|
||
]
|
||
},
|
||
"output": {
|
||
"files": [
|
||
{ "index": 0, "name": "差异评估报告.docx", "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "size": 6102 }
|
||
]
|
||
},
|
||
"steps": [
|
||
{ "seq": 1, "name": "建立评估上下文:绑定四项、新建需求、存在性核对", "by": "agent", "byName": "deepseek-v4-flash", "at": "2026-08-25T02:12:00.000Z", "durationMs": 60000, "res": "done", "note": "铸案号、建需求 V1、锁规则版本、逐个料号查库、写存在性核对表" },
|
||
{ "seq": 2, "name": "数据工单:补建芯片主数据(P-08)", "by": "human", "byName": "柴浩", "at": "2026-08-25T02:13:00.000Z", "durationMs": null, "res": "current", "note": "" },
|
||
{ "seq": 3, "name": "评估输入与资料补齐确认", "by": "human", "byName": "柴浩", "at": null, "durationMs": null, "res": "pending", "note": "" }
|
||
]
|
||
}
|
||
```
|
||
|
||
三种终局的 `reason` 示例:
|
||
|
||
```json
|
||
{ "kind": "rejected", "text": "验收驳回:测算毛利低于阈值,退回重新测算 —— 负责人 柴浩" }
|
||
{ "kind": "failed", "text": "预算耗尽:步数 90/90、活跃时长 120/120 分钟" }
|
||
{ "kind": "terminated", "text": "由 柴浩 手动终止" }
|
||
```
|
||
|
||
- 头部:状态丸(`state`)+ 责任人 + 启动 / 结束 · 耗时(`startedAt` / `endedAt` / `durationMs`)+ 模型(`llm`)。
|
||
- 驳回 / 失败 / 已终止出原因块(`reason.text`);`reason === null` 不摆。
|
||
- 时间线按 `steps` 渲染;`res='fail'` 那一行的字眼按 `reason.kind` 选(驳回 / 失败 / 已终止)。
|
||
- 右栏输入(`input.content` / `input.source.label` / `input.submittedAt` / `input.files`)、输出(`output.files`;空数组摆「暂无产出」);文件行的下载走 §7。
|
||
|
||
## 7. 实例文件下载 —— `GET /api/gov/activities/:activityId/instances/:instanceId/files/:io/:index`(`opsInstanceFileRoute`)
|
||
|
||
**请求**
|
||
|
||
| 位置 | 参数 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|---|
|
||
| path | `activityId` | string | 是 | |
|
||
| path | `instanceId` | string | 是 | 任务 id |
|
||
| path | `io` | `'input' \| 'output'` | 是 | 哪一侧;别的值 400 |
|
||
| path | `index` | number ≥ 0 | 是 | 对应 §6 文件行的 `index`;不是整数 → 400 |
|
||
| query | `raw` | `'1'` | 否 | 带 `raw=1` 直出文件字节(`content-type` = 文件 MIME,`content-disposition: attachment; filename*=UTF-8''…`);不带回 JSON |
|
||
| query | `ticket` | string | `<a href download>` 场景必填 | 点击那一刻取票据拼上(`shared` 里的 `downloadWithTicket(url)`) |
|
||
|
||
**响应 200**(不带 `raw`):
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `name` | string | 文件名 |
|
||
| `contentType` | string | MIME |
|
||
| `dataBase64` | string | 文件字节的 base64(与 `GET /api/gov/upload/:id` 同形) |
|
||
|
||
**404**:实例不存在 / 该序号没有文件。
|
||
|
||
下载钮的写法:`downloadWithTicket(`/api/gov/activities/${activityId}/instances/${id}/files/${io}/${index}?raw=1`)`。
|
||
|
||
## 8. 禁用 / 激活 / 终止
|
||
|
||
### 8.1 禁用 —— `POST /api/gov/activities/:activityId/disable`(`disableOpsActivityRoute`)
|
||
|
||
**请求**:路径参数 `activityId`;**body 空**。
|
||
**响应 200**:`ActivityDetail`(`status='disabled'`)。**404**:活动不存在。已禁用再禁用 = 幂等 200。
|
||
任何时刻都可禁用,没有前置条件:禁用后 §5 回 412;已完成的实例是历史留痕,进行中的继续跑完。
|
||
|
||
### 8.2 激活 —— `POST /api/gov/activities/:activityId/activate`(`activateOpsActivityRoute`)
|
||
|
||
**请求**:路径参数 `activityId`;**body 空**。
|
||
**响应 200**:`ActivityDetail`(`status='active'`)。**404**:活动不存在。已激活再激活 = 幂等 200。
|
||
|
||
### 8.3 终止实例 —— `POST /api/gov/activities/:activityId/instances/:instanceId/terminate`(`terminateOpsInstanceRoute`)
|
||
|
||
**请求**:路径参数 `activityId`、`instanceId`;**body 空**(终止理由本期不填,服务端生成「由 <操作人> 手动终止」)。
|
||
**响应 200**:`InstanceDetail`,`state='failed'`、`reason.kind='terminated'`、`endedAt` = 终止时刻。
|
||
**404**:实例不存在。**412**:实例已终态(已完成 / 已失败 / 已终止的不能再终止;`pending` 与 `running` 都可以)。
|
||
前端:二次确认(轻气泡)后调用;成功后由服务端重投影(广播 → 失效 → 重取),不要就地改 state。
|
||
|
||
## 9. 类型附录(`packages/contracts/src/activity-ops.ts`,同名导出)
|
||
|
||
```ts
|
||
type ActivityStatus = 'active' | 'disabled';
|
||
type InstanceState = 'pending' | 'running' | 'completed' | 'rejected' | 'failed';
|
||
type StaffRef = { id: string; name: string };
|
||
|
||
type ActivitySummary = { id: string; name: string; biz: string; status: ActivityStatus; version: string;
|
||
owner: StaffRef | null; llm: string; instanceCount: number; createdAt: string; ico?: string; hue?: 'blue' | 'teal' | 'amber' | 'violet' };
|
||
type ActivityDetail = Omit<ActivitySummary, 'instanceCount'> & { desc: string; view: { url: string } | null };
|
||
|
||
type InstanceSummary = { id: string; no: string; state: InstanceState; owner: StaffRef; input: string; output: string | null; startedAt: string };
|
||
type InstanceStats = { total: number; byState: Record<InstanceState, number> };
|
||
type InstanceList = { stats: InstanceStats; total: number; page: number; size: number; items: InstanceSummary[] };
|
||
|
||
type LaunchParamType = 'text' | 'textarea' | 'select' | 'combo' | 'list';
|
||
type LaunchParam = { key: string; label: string; type: LaunchParamType; required?: boolean; desc?: string;
|
||
placeholder?: string; options?: { value: string; label: string }[]; default?: string | string[] };
|
||
type LaunchParams = { activityId: string; version: string; params: LaunchParam[] };
|
||
type LaunchInput = { idempotencyKey: string; ownerId: string; params: Record<string, string | string[]> };
|
||
|
||
type InstanceFile = { index: number; name: string; contentType: string; size: number };
|
||
type InstanceReason = { kind: 'rejected' | 'failed' | 'terminated'; text: string };
|
||
type InstanceSource = { kind: string; label: string };
|
||
type InstanceStep = { seq: number; name: string; by: 'agent' | 'human'; byName: string;
|
||
at: string | null; durationMs: number | null; res: 'done' | 'fail' | 'current' | 'pending'; note: string };
|
||
type InstanceDetail = Omit<InstanceSummary, 'input' | 'output'> & { activityId: string; llm: string; endedAt: string | null; durationMs: number | null;
|
||
reason: InstanceReason | null;
|
||
input: { content: string; source: InstanceSource; submittedAt: string; files: InstanceFile[] };
|
||
output: { files: InstanceFile[] };
|
||
steps: InstanceStep[] };
|
||
```
|
||
|
||
路由常量(`packages/contracts/src/api/activity-ops.ts`):
|
||
|
||
| 常量 | 方法 · 路径 |
|
||
|---|---|
|
||
| `listOpsActivitiesRoute` | GET `/api/gov/activities` |
|
||
| `getOpsActivityRoute` | GET `/api/gov/activities/:activityId` |
|
||
| `opsActivityViewRoute` | GET `/api/gov/activities/:activityId/view` |
|
||
| `listOpsInstancesRoute` | GET `/api/gov/activities/:activityId/instances` |
|
||
| `opsLaunchParamsRoute` | GET `/api/gov/activities/:activityId/launch-params` |
|
||
| `launchOpsInstanceRoute` | POST `/api/gov/activities/:activityId/instances` |
|
||
| `getOpsInstanceRoute` | GET `/api/gov/activities/:activityId/instances/:instanceId` |
|
||
| `opsInstanceFileRoute` | GET `/api/gov/activities/:activityId/instances/:instanceId/files/:io/:index` |
|
||
| `disableOpsActivityRoute` | POST `/api/gov/activities/:activityId/disable` |
|
||
| `activateOpsActivityRoute` | POST `/api/gov/activities/:activityId/activate` |
|
||
| `terminateOpsInstanceRoute` | POST `/api/gov/activities/:activityId/instances/:instanceId/terminate` |
|
||
|
||
其它导出:`INSTANCE_STATES` / `INSTANCE_STATE_LABEL`(五态中文)、`ACTIVITY_STATUSES` / `ACTIVITY_STATUS_LABEL`、`ACTIVITY_HUES`、`LAUNCH_PARAM_TYPES`、`ACTIVITY_VIEW_HEIGHT_MESSAGE`、`ACTIVITY_OPS_ROUTES`。
|
||
|
||
调用示例(前端唯一通道 `shared/typed-fetch`):
|
||
|
||
```ts
|
||
import { call } from '@web-shared/typed-fetch.ts';
|
||
import { launchOpsInstanceRoute, listOpsInstancesRoute } from '@contracts';
|
||
import type { InstanceDetail, InstanceList } from '@contracts';
|
||
|
||
const list = await call<InstanceList>(listOpsInstancesRoute, { params: { activityId }, query: { state: 'running', page: '2', size: '8' } });
|
||
const created = await call<InstanceDetail>(launchOpsInstanceRoute, { params: { activityId }, body: { idempotencyKey, ownerId, params: values } });
|
||
// 失败抛 ApiCallError { status, code, message, detail }
|
||
```
|
||
|
||
## 10. 前端对接要点(现状 → 接口落地)
|
||
|
||
接口已落地,前端仍是占位数据(对接另起一轮,2026-08-27 用户定)。届时照这张表换:
|
||
|
||
| 现状(占位) | 接口落地后 |
|
||
|---|---|
|
||
| `DEMO_ACTIVITIES` 静态数组 | §2.1;L1 页签切换即换 `status` 重取;`owner` 为 null 摆「—」,`ico`/`hue` 缺省兜底 |
|
||
| `demoInstances()` 每活动共用一份 | §3;检索四条件与分页全部上移到查询参数,前端删掉本地过滤/分页;表格摆 `no`,寻址用 `id` |
|
||
| 统计带由前端从全量数组数 | §3 `stats`(服务端给,分页下才数得准) |
|
||
| 中文状态字面量当键 | 五态英文码 + `INSTANCE_STATE_LABEL`,前端一张 `Record<InstanceState, {tone, ico}>` 映射 |
|
||
| `LAUNCH_PARAMS` 占位参数表 | §4;`options` 从纯字符串升级为 `{value,label}` 两段(渲染器小改);控件种类已含 `combo` |
|
||
| `onLaunch` 在清单头落假行 | §5;抽屉每开一次生成 `idempotencyKey`(`crypto.randomUUID()`),提交走 mutation,400 的 `detail[].key` 对回控件,成功失效 `['gov']` 并跳 L3 |
|
||
| 责任人固定字段 | §5 `ownerId`;抽屉加「固定参数」段(名册下拉取 `GET /api/gov/staff` 在职的人,默认当前登录人 `me.staff.id`) |
|
||
| 禁用/激活/终止就地改 state | §8;mutation 成功后由服务端重投影(广播 → 失效 → 重取);L2 行内「终止活动」也要二次确认 |
|
||
| `demoSteps` / `terminatedSteps` 五步占位 | §6 `steps`;终止归态以 `reason.kind='terminated'` 为准,前端不再落「失败」占位 |
|
||
| 沙箱 `src="/sandbox-demo/activity-view.html"` 固定 480 高 | §2.3 `view.url` + 票据(装载那一刻取);`view` 为 null 藏起沙箱区;高度听 `jcai:view-height` |
|
||
| 输出下载钮置灰 | §7 已落地:`downloadWithTicket(url + '?raw=1')`(从 `desk-task/internal/FileViewer.tsx` 抽到 shared) |
|
||
| 设计中心活动库 / 测试台的 `useGovActivities` | 路径改名对它们透明(import 的是路由对象),无需改动 |
|