Archived
- contracts 补建 activity-ops(schema + 11 条路由);L1/L2/L3 全真接口对接(清单/详情/实例/启动抽屉/沙箱) - 启动抽屉:参数表服务端下发、幂等键防重、400 逐格报错;combo「新增」提为候选第一项 - 终止/禁用/激活轻气泡反馈闭环:在途留场转忙碌、成功自闭合并刷新、失败原因留气泡可重试 - L3 实例详情 7 秒轮询(待执行/进行中),终态自动停 - 员工模块四页改版:目录行+首字圆块、权限三槽点位、组织树/空态、成员详情 hero+角色开关;卡内容区内边距补齐
This commit is contained in:
+427
-160
@@ -1,102 +1,303 @@
|
||||
# API 文档 —— 活动模块(L1 清单 / L2 活动详情 / L3 实例详情)
|
||||
|
||||
> 本文是活动模块前端(运营中心 · 活动区,2026-08-25 重构模版)与服务端之间的接口约定,
|
||||
> 给后端编写接口、前端取数用。鉴权 / 票据 / 错误体 / 端归属通则:`docs/API文档-鉴权与人员.md` §0,本文不重复。
|
||||
> 本文是活动模块前端(运营中心 · 活动区,2026-08-25 重构模版)与服务端之间的接口约定,给前端取数、后端维护用。
|
||||
> 鉴权 / 票据 / 错误体 / 端归属的**通则**在 `docs/API文档-鉴权与人员.md` §0,本文 §0.1 只摘调用时必须知道的几条。
|
||||
> 需求出处:`docs/活动模块重构需求-2026-08-25.md`;启动参数口径:`ACT-WORK-05-一键启动入参说明.md`(极简四参)。
|
||||
> 契约落地后唯一真相在 `packages/contracts/src/api/*.ts`(zod schema,前后端共用);两者不一致时以契约为准并回来改本文。
|
||||
> **契约已落地(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. 通则
|
||||
|
||||
### 0.1 调用约定
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 前缀 | `/api/gov/activities/*`;**全部 `access: 'ops'`** —— 活动区落在运营中心,与工作流/任务看板同端 |
|
||||
| 时间 | 一律 ISO 8601 UTC 字符串(`2026-08-27T02:25:11.000Z`);**展示格式化是前端的事**(`YYYY-MM-DD HH:mm:ss` 本地时区) |
|
||||
| 人员 | 凡出人,都是 `{ "id": "<staffId>", "name": "柴浩" }` 两段 —— 展示用 name,筛选/提交用 id;人员候选名单用既有 `GET /api/gov/staff`,本文不另做 |
|
||||
| 广播 | 写路由(启动 / 禁用 / 激活 / 终止)成功后广播 `{kind:'system', id:'gov'}` —— 前端按既有套路失效 `['gov',*]` 重取 |
|
||||
| 实例状态 | 五态,英文码:`pending`(待执行)/ `running`(进行中)/ `completed`(已完成)/ `rejected`(驳回)/ `failed`(失败);**没有第六态** —— 手动终止归入 `failed`,用 `reason.kind='terminated'` 区分「自己跑挂」(见 §6) |
|
||||
| 禁用语义 | **任何时刻都可禁用**(2026-08-27 用户重定,废除「有实例不能禁用」):禁用只拦「新启动」—— 已完成的实例是历史留痕,进行中的实例**继续跑完不受影响**;激活即恢复启动能力 |
|
||||
| 前缀 | `/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 错误体
|
||||
|
||||
| 状态 | code | 何时 |
|
||||
所有非 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`)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| 400 | `validation-failed` | 入参不合 schema(`detail` 是 zod issues);启动参数缺必填 / 类型不对(`detail` 指明是哪个 `key`) |
|
||||
| 404 | `not-found` | 活动 / 实例不存在;实例不属于这个活动 |
|
||||
| 412 | `precondition-failed` | 启动:活动已禁用;终止:实例不在 `running` |
|
||||
| `id` | string | 名册 staffId |
|
||||
| `name` | string | 姓名 |
|
||||
|
||||
## 1. L1 活动清单
|
||||
### 1.2 `ActivitySummary`(`activitySummarySchema`)—— L1 卡片
|
||||
|
||||
### `GET /api/gov/activities?status=active|disabled` —— access: `ops`
|
||||
| 字段 | 类型 | 必有 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `id` | string | 是 | 活动 id,如 `part-substitution-assessment`;后面所有路径里的 `:activityId` |
|
||||
| `name` | string | 是 | 活动名 |
|
||||
| `biz` | string | 是 | 业务分类(卡片副题) |
|
||||
| `status` | `ActivityStatus` | 是 | `active` / `disabled` |
|
||||
| `version` | string | 是 | 当前生效的包版本,如 `1.5.1` |
|
||||
| `owner` | `StaffRef` \| **null** | 是 | 活动负责人;没建档 = `null`,摆「—」 |
|
||||
| `llm` | string | 是 | 绑定的模型(逻辑名,如 `deepseek-v4-flash`) |
|
||||
| `instanceCount` | number | 是 | 该活动的实例总数(含测试台试跑单) |
|
||||
| `createdAt` | string(ISO) | 是 | 首个版本装入的时刻 |
|
||||
| `ico` | string | 可缺 | 语义图标名;缺省前端兜底 `inbox` |
|
||||
| `hue` | `'blue' \| 'teal' \| 'amber' \| 'violet'` | 可缺 | 业务色相;缺省前端兜底 `blue` |
|
||||
|
||||
L1 两个页签(激活中 / 已禁用)各取一份;不带 `status` = 全量。量小,**不分页**。
|
||||
### 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 摆「暂不支持的控件」** |
|
||||
| `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` **或**新增的原样字符串(两种都合法);选填没填可以不传或传 `''` |
|
||||
|
||||
### 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[]` | 是 | 时间线 |
|
||||
|
||||
注意:详情里**没有** `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": "quote-intake",
|
||||
"name": "询价接收与结构化",
|
||||
"biz": "报价业务",
|
||||
"id": "part-substitution-assessment",
|
||||
"name": "ACT-WORK-05 · 差异评估",
|
||||
"biz": "需求分析与方案匹配",
|
||||
"status": "active",
|
||||
"version": "1.3.0",
|
||||
"owner": { "id": "8f3c…", "name": "柴浩" },
|
||||
"llm": "K3 · quote-intake",
|
||||
"version": "1.5.1",
|
||||
"owner": null,
|
||||
"llm": "deepseek-v4-flash",
|
||||
"instanceCount": 12,
|
||||
"createdAt": "2026-08-12T01:00:00.000Z",
|
||||
"ico": "inbox",
|
||||
"hue": "blue"
|
||||
"createdAt": "2026-08-26T01:00:00.000Z",
|
||||
"ico": "git-compare",
|
||||
"hue": "teal"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `ico` / `hue`:展示元数据(语义图标名 / 业务色相 `blue|teal|amber|violet`),创建活动时可配;**缺省时前端兜底**(`inbox` / `blue`),后端可以暂时不存。
|
||||
- `llm`:这个活动绑定的专属 agent 的人读标签;要链到 LLM 实例时再加 `llmId`,本期没有。
|
||||
### 2.2 活动详情 —— `GET /api/gov/activities/:activityId`(`getOpsActivityRoute`)
|
||||
|
||||
## 2. L2 活动定义
|
||||
**请求**:路径参数 `activityId`(string,必填)。
|
||||
|
||||
### `GET /api/gov/activities/:activityId` —— access: `ops`
|
||||
**响应 200**:`ActivityDetail`。**404**:活动不存在。
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "quote-intake",
|
||||
"name": "询价接收与结构化",
|
||||
"biz": "报价业务",
|
||||
"id": "part-substitution-assessment",
|
||||
"name": "ACT-WORK-05 · 差异评估",
|
||||
"biz": "需求分析与方案匹配",
|
||||
"status": "active",
|
||||
"version": "1.3.0",
|
||||
"version": "1.5.1",
|
||||
"owner": { "id": "8f3c…", "name": "柴浩" },
|
||||
"llm": "K3 · quote-intake",
|
||||
"createdAt": "2026-08-12T01:00:00.000Z",
|
||||
"ico": "inbox",
|
||||
"hue": "blue",
|
||||
"desc": "活动定义只读 —— 输入与输出在创建时定死,演进走版本迭代;验收在工作台,这里只看状态。",
|
||||
"view": { "url": "/api/gov/activities/quote-intake/view" }
|
||||
"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" }
|
||||
}
|
||||
```
|
||||
|
||||
- `desc`:定义说明,创建时填,只读。
|
||||
- `view`:沙箱要渲染的活动视图;**为 `null` 时前端藏起整个沙箱区块**(不再摆空 iframe)。
|
||||
### 2.3 活动视图(沙箱)—— `GET /api/gov/activities/:activityId/view`(`opsActivityViewRoute`)
|
||||
|
||||
### `GET /api/gov/activities/:activityId/view` —— access: `ops`(支持 `?ticket=`)
|
||||
**请求**
|
||||
|
||||
→ `content-type: text/html`,服务器托管的活动视图 HTML,前端 `<iframe sandbox="allow-scripts">` 直接渲染。
|
||||
iframe 带不了 Authorization 头,走票据(`GET /api/auth/ticket`,ADR-012 §6)—— 前端每次装载前取一张拼 `?ticket=`。
|
||||
**内容高度用 postMessage 通讯自适应(视图脚本向父页报高),协议随第一版视图实现一起定。**404:该活动没配视图。
|
||||
| 位置 | 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| path | `activityId` | string | 是 | |
|
||||
| query | `ticket` | string | 是(iframe 场景) | `<iframe src>` 带不了请求头:**装载那一刻** `call(authTicketRoute)` 取一张拼上,不要渲染时预取(60 秒过期) |
|
||||
|
||||
## 3. L2 实例清单(历史活动)
|
||||
**响应 200**:`content-type: text/html`(`cache-control: no-store`),整份 HTML,`<iframe sandbox="allow-scripts" src="...">` 直接渲染。
|
||||
**404**:活动不存在 / 该活动没配视图(`detail.view === null` 时压根别请求)。
|
||||
|
||||
### `GET /api/gov/activities/:activityId/instances` —— access: `ops`
|
||||
**高度协议**:页面自己会向父页 `postMessage({ type: 'jcai:view-height', height: <number> }, '*')`(load、尺寸变化、每秒兜底)。父页:
|
||||
|
||||
查询参数(可组合,AND;都不带 = 全部):
|
||||
```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));
|
||||
});
|
||||
```
|
||||
|
||||
| 参数 | 形 | 说明 |
|
||||
|---|---|---|
|
||||
| `id` | 字符串 | 实例号**模糊**搜,如 `INS-24` |
|
||||
| `ownerId` | staffId | 精确到人 |
|
||||
| `state` | 五态之一 | 精确 |
|
||||
| `startedFrom` / `startedTo` | `YYYY-MM-DD` | 启动日期段,**含两端整日**;可只给一端 |
|
||||
| `page` | 整数,从 1 | 默认 1 |
|
||||
| `size` | 整数 | 默认 8(与前端单页一致),上限 50 |
|
||||
## 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
|
||||
{
|
||||
@@ -106,195 +307,226 @@ iframe 带不了 Authorization 头,走票据(`GET /api/auth/ticket`,ADR-012 §6)
|
||||
"size": 8,
|
||||
"items": [
|
||||
{
|
||||
"id": "INS-2417",
|
||||
"id": "0c9d3a1e-…",
|
||||
"no": "INS-2417",
|
||||
"state": "running",
|
||||
"owner": { "id": "8f3c…", "name": "柴浩" },
|
||||
"input": "Itelma 车身控制器询价 · 3 项物料",
|
||||
"input": "工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND…",
|
||||
"output": null,
|
||||
"startedAt": "2026-08-25T02:12:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "INS-2415",
|
||||
"id": "7b21e6c0-…",
|
||||
"no": "INS-2415",
|
||||
"state": "completed",
|
||||
"owner": { "id": "8f3c…", "name": "柴浩" },
|
||||
"input": "GAZ 底盘线束询价 · 7 项物料",
|
||||
"output": "quote-20260824-07.json",
|
||||
"input": "…",
|
||||
"output": "已发口:assessment-released",
|
||||
"startedAt": "2026-08-24T09:03:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **`stats` 是活动整体的五态统计,不随检索条件动**(页头统计带摆的是全局数);`total` 是**筛后**条数,分页器按它走。
|
||||
- `input` / `output` 都是**一句话摘要**;`output` 为 `null` 前端摆 `-`。排序:启动时间倒序(新的在前)。
|
||||
- 页头统计带摆 `stats`(全局),分页器按 `total`(筛后)走;筛中提示「筛出 `total` / 共 `stats.total`」。
|
||||
- 负责人下拉的候选用 `GET /api/gov/staff`(在职的),提交 `ownerId`。
|
||||
- 前端**不再**本地过滤 / 分页 / 数统计,全部上移到这里。
|
||||
|
||||
## 4. 启动参数表
|
||||
## 4. 启动参数表 —— `GET /api/gov/activities/:activityId/launch-params`(`opsLaunchParamsRoute`)
|
||||
|
||||
### `GET /api/gov/activities/:activityId/launch-params` —— access: `ops`
|
||||
点「启动活动」开抽屉时取。**一活动一份,前端只按 `params` 渲染表单,不写死字段。**
|
||||
|
||||
点「启动活动」开抽屉时取。**一活动一份,前端只按此表渲染表单,不写死字段。**
|
||||
**请求**:路径参数 `activityId`。**响应 200**:`LaunchParams`。**404**:活动不存在。
|
||||
|
||||
```json
|
||||
{
|
||||
"activityId": "quote-intake",
|
||||
"version": "1.3.0",
|
||||
"activityId": "part-substitution-assessment",
|
||||
"version": "1.5.1",
|
||||
"params": [
|
||||
{
|
||||
"key": "customer_id",
|
||||
"key": "customerCode",
|
||||
"label": "客户",
|
||||
"type": "combo",
|
||||
"required": true,
|
||||
"desc": "台账里有就直接选;是新客户就自己填,启动后派数据工单补档案。",
|
||||
"desc": "台账里有就直接选;是新客户就自己填客户编码,启动后派数据录入卡建档。",
|
||||
"placeholder": "CU-0142",
|
||||
"options": [
|
||||
{ "value": "cust-itelma", "label": "Itelma" },
|
||||
{ "value": "cust-gaz", "label": "GAZ" }
|
||||
{ "value": "CU-0142", "label": "Itelma" },
|
||||
{ "value": "CU-2039", "label": "青岛新星电子有限公司" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "request_description",
|
||||
"key": "customerName",
|
||||
"label": "客户名称(仅客户不在库时填)",
|
||||
"type": "text",
|
||||
"desc": "新客户先把名字摆上,启动后建档时让人核。",
|
||||
"placeholder": "深圳恒星电源有限公司"
|
||||
},
|
||||
{
|
||||
"key": "requestDescription",
|
||||
"label": "客户需求",
|
||||
"type": "textarea",
|
||||
"required": true,
|
||||
"desc": "保留原始需求语义,据此新建需求主记录(V1)。",
|
||||
"placeholder": "如:车身控制器询价 · 3 项物料,期望 9 月交期"
|
||||
"desc": "保留原始需求语义,据此新建 P-01 需求主记录(版本 V1)。",
|
||||
"placeholder": "工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料"
|
||||
},
|
||||
{
|
||||
"key": "original_internal_pn",
|
||||
"key": "originalInternalPn",
|
||||
"label": "原内部料号",
|
||||
"type": "combo",
|
||||
"required": true,
|
||||
"desc": "差异评估基准;库里没有该 PN 也可启动,启动后派数据工单补参数。",
|
||||
"placeholder": "如 PN-880501",
|
||||
"options": [
|
||||
{ "value": "PN-880101", "label": "PN-880101" }
|
||||
]
|
||||
"placeholder": "LM2596S-5.0",
|
||||
"options": [{ "value": "LM2596S-5.0", "label": "LM2596S-5.0" }]
|
||||
},
|
||||
{
|
||||
"key": "candidate_internal_pn",
|
||||
"key": "candidateInternalPns",
|
||||
"label": "候选内部料号",
|
||||
"type": "list",
|
||||
"required": true,
|
||||
"desc": "被评估物料,一次可传多个、逐个评估 —— 至少 1 个。",
|
||||
"placeholder": "如 PN-880412"
|
||||
"desc": "被评估物料,一次可传多个、逐个评估 —— 至少 1 个;不在库的启动后经数据工单创建。",
|
||||
"placeholder": "XL2596S-5.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `key` | 提交时回传的字段名(接口契约,英文) |
|
||||
| `label` | 表单上的中文名 |
|
||||
| `type` | 控件种类:`text` 单行 / `textarea` 多行 / `select` 单选(只能选候选) / **`combo` 选择+新增**(候选末项是「新增」,选了换输入框自己填;见下方「combo 语义」) / `list` 字符串数组(一次可传多个,逐行自由填);**新增种类前要前后端一起升级**,前端遇到不认识的 type 摆「暂不支持的控件」 |
|
||||
| `required` | 必填;前端在没填完时禁提交,**服务端仍要再验一道**(400) |
|
||||
| `desc` | 参数描述,挂在控件下当辅助说明 |
|
||||
| `placeholder` | 选填,text / textarea / combo(新增态) / list 用 |
|
||||
| `options` | `select` / `combo` 必填;**两段式 `{ value, label }`** —— value 是提交的标识(如客户 ID),label 是显示名;标识即显示名时两段相同 |
|
||||
| `default` | 选填;预填值(`list` 给字符串数组,其余给字符串)—— 上游触发预填场景用 |
|
||||
**combo 语义(2026-08-27 用户定)**:启动表单大部分是自由输入;客户 / 料号这类「台账里可能有」的参数才用 combo —— 先选,候选里没有就新增自填。**提交候选之外的值是合法的,不拦启动**:服务端按值在台账里查,查不到照样启动,启动后派**数据工单**到工作台补这条档案(客户进客户台账、PN 补参数,确认即入库,活动从断点续跑)。combo 提交的值统一是字符串:选中候选传它的 `value`,新增传用户填的原样字符串。
|
||||
|
||||
**combo 语义(2026-08-27 用户定)**:启动表单大部分是自由输入;客户 / 料号这类「台账里可能有」的参数才用 combo —— 先选,候选里没有就新增自填。**提交候选之外的值是合法的,不拦启动**:服务端按 value 在台账里查,查不到照样启动,启动后派**数据工单**到工作台补这条档案(客户进客户台账、PN 补参数,确认即入库,活动从断点续跑)。combo 提交的值统一回字符串:选中候选回它的 `value`,新增回用户填的原样字符串 —— 服务端不靠类型区分新旧,靠台账存在性检查。
|
||||
候选量大到不宜内联时(如全量物料台账选 PN),后续加 `optionsSource` 引用检索接口;**本期一律内联 `options`**(封顶 500 行)。
|
||||
|
||||
候选量大到不宜内联时(如全量物料台账选 PN),后续加 `optionsSource` 引用检索接口,**本期一律内联 `options`**。
|
||||
> 活动清单侧怎么声明(前端不用管,给写活动包的人看):`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:'列' }` 让候选从正式台账现读;`type:'array'` 且元素是字符串→list;**`'x-order': <数字>` 定表单次序**(清单存在 jsonb 里,书写顺序到不了这里);`format:'file'` 的字段本期不上表单;select 没候选降成 text。
|
||||
|
||||
## 5. 启动活动(创建实例)
|
||||
## 5. 启动活动(创建实例)—— `POST /api/gov/activities/:activityId/instances`(`launchOpsInstanceRoute`)
|
||||
|
||||
### `POST /api/gov/activities/:activityId/instances` —— access: `ops`
|
||||
**请求**:路径参数 `activityId`;请求体 `LaunchInput`(§1.7)。
|
||||
|
||||
```json
|
||||
{
|
||||
"idempotencyKey": "01J9…",
|
||||
"idempotencyKey": "b5a2d6f0-3e1c-4a7b-9d2e-8f1c0a4b7e93",
|
||||
"ownerId": "8f3c1a2b-6c1e-4b3a-9f0e-2d5c7a1b9e44",
|
||||
"params": {
|
||||
"customer_id": "cust-itelma",
|
||||
"request_description": "车身控制器询价 · 3 项物料,期望 9 月交期",
|
||||
"original_internal_pn": "PN-880101",
|
||||
"candidate_internal_pn": ["PN-880412", "PN-880530"]
|
||||
"customerCode": "CU-0142",
|
||||
"requestDescription": "工业电源项目在用 LM2596S-5.0,原厂 PCN 转 NRND,客户要求验证替代料",
|
||||
"originalInternalPn": "LM2596S-5.0",
|
||||
"candidateInternalPns": ["XL2596S-5.0", "LM2596S-ADJ"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`ownerId`(必填,2026-08-27 用户定)是启动契约的固定字段,不进 §4 参数表** —— 每个活动启动都要指定这次实例的**责任人**:负责人验收、存在性检查派出的数据工单都落到责任人身上。候选名单 = 既有 `GET /api/gov/staff`(前端只列 `state='active'` 的);前端默认填当前登录人,允许改。服务端要验:ownerId 是名册里在职的人(400),不验「是不是启动人自己」(代别人启动是合法场景)。
|
||||
启动成功后,实例视图(§3/§6)里的 `owner` 就是它。
|
||||
**响应 200**:`InstanceDetail`(§1.12,与 §6 同形)——新建的实例,初始 `state='pending'`(推进器领到即转 `running`,靠 SSE 失效重取)。**同一 `idempotencyKey` 重放也回 200,给的是第一次创建的那一单,不起第二个。**
|
||||
|
||||
→ 200 一条实例详情(同 §6)。**同一 `idempotencyKey` 重放返回第一次创建的实例,不起第二个**(前端每开一次抽屉生成一枚;重复点提交、网络重试都靠它挡住)。
|
||||
`params` 的键值形状由 §4 参数表决定:`list` 回字符串数组,其余回字符串;`select` 回 `options` 里某个 `value`;**`combo` 回候选的 `value` 或新增的原样字符串,两种都合法**。
|
||||
412:活动已禁用(先激活再启动)。400:缺必填 / 类型不符 / select 的值不在 options 里(**combo 不适用这条** —— 候选之外的值是合法新增)。
|
||||
**错误**
|
||||
|
||||
正常落地初始状态 `pending`(排入队列,调度器领到即转 `running`);**存在性检查(客户 / 料号在不在台账)在启动后做,查不到不拦启动** —— 按入参说明口径,缺口派数据工单到工作台逐项补(确认即入库,活动从断点续跑);响应里不单独回这件事,工单出现在工作台那一侧(工单接口不在本文范围)。
|
||||
| 状态 | 何时 | `detail` |
|
||||
|---|---|---|
|
||||
| 400 | 请求体不合 schema | zod issues |
|
||||
| 400 | 启动参数缺必填 / `list` 没传数组 / 标量传了数组 / `select` 值不在候选 / 不认识的参数名 | `[{ key, message }]`,逐格点名 —— 前端把 `message` 摆到对应控件下 |
|
||||
| 400 | 责任人不在名册 / 已停用 | `[{ key: 'ownerId', message }]` |
|
||||
| 404 | 活动不存在 | — |
|
||||
| 409 | 同一 `idempotencyKey` 的实例正在创建(60 秒内);或这枚键已用在别的活动上 | — |
|
||||
| 412 | 活动已禁用(先激活再启动) | — |
|
||||
|
||||
## 6. L3 实例详情
|
||||
成功后:关抽屉、失效 `['gov']`、按返回的 `id` 跳 L3(路由 id = `${activityId}:${id}`)。存在性检查(客户 / 料号在不在台账)在启动后由活动做,查不到不拦启动 —— 响应里不单独回这件事,工单出现在工作台那一侧。
|
||||
|
||||
### `GET /api/gov/activities/:activityId/instances/:instanceId` —— access: `ops`
|
||||
## 6. L3 实例详情 —— `GET /api/gov/activities/:activityId/instances/:instanceId`(`getOpsInstanceRoute`)
|
||||
|
||||
**请求**:路径参数 `activityId`、`instanceId`(= 清单行的 `id`,任务 uuid,**不是** `no`)。
|
||||
|
||||
**响应 200**:`InstanceDetail`(§1.12)。**404**:实例不存在或不属于这个活动。
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "INS-2417",
|
||||
"activityId": "quote-intake",
|
||||
"id": "0c9d3a1e-…",
|
||||
"no": "INS-2417",
|
||||
"activityId": "part-substitution-assessment",
|
||||
"state": "running",
|
||||
"owner": { "id": "8f3c…", "name": "柴浩" },
|
||||
"llm": "K3 · quote-intake",
|
||||
"llm": "deepseek-v4-flash",
|
||||
"startedAt": "2026-08-25T02:12:00.000Z",
|
||||
"endedAt": null,
|
||||
"durationMs": null,
|
||||
"reason": null,
|
||||
"input": {
|
||||
"content": "Itelma 车身控制器询价 · 3 项物料",
|
||||
"content": "工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND…",
|
||||
"source": { "kind": "manual", "label": "手动启动 · 柴浩" },
|
||||
"submittedAt": "2026-08-25T02:12:00.000Z",
|
||||
"files": [
|
||||
{ "index": 0, "name": "RFQ-2026-081.eml", "contentType": "message/rfc822", "size": 48210 }
|
||||
{ "index": 0, "name": "XL2596S-5.0.pdf", "contentType": "application/pdf", "size": 48210 }
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"files": [
|
||||
{ "index": 0, "name": "quote-20260825-07.json", "contentType": "application/json", "size": 6102 }
|
||||
{ "index": 0, "name": "差异评估报告.docx", "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "size": 6102 }
|
||||
]
|
||||
},
|
||||
"steps": [
|
||||
{ "seq": 1, "name": "输入接收", "by": "agent", "byName": "K3 · quote-intake", "at": "2026-08-25T02:12:00.000Z", "durationMs": 60000, "res": "done", "note": "询价单已进入,要素识别完成,实例正式起步。" },
|
||||
{ "seq": 2, "name": "要素结构化", "by": "agent", "byName": "K3 · quote-intake", "at": "2026-08-25T02:15:00.000Z", "durationMs": 360000, "res": "done", "note": "抽取客户 / 物料 / 交期等字段,落入结构化模型。" },
|
||||
{ "seq": 3, "name": "定价测算", "by": "agent", "byName": "K3 · quote-intake", "at": "2026-08-25T02:21:00.000Z", "durationMs": null, "res": "current", "note": "按成本模型逐项给出建议价与毛利测算。" },
|
||||
{ "seq": 4, "name": "负责人验收", "by": "human", "byName": "柴浩", "at": null, "durationMs": null, "res": "pending", "note": "确认测算结果 —— 规划类产出必须由人验收担责。" },
|
||||
{ "seq": 5, "name": "输出归档", "by": "agent", "byName": "K3 · quote-intake", "at": null, "durationMs": null, "res": "pending", "note": "生成报价文件并归档,产出回写台账。" }
|
||||
{ "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": "" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `endedAt` / `durationMs`:未结束(null)。`durationMs` 毫秒,前端格式化(`27m`)。
|
||||
- **`reason`**:终态的原因说明,`null` = 不需要(进行中 / 待执行 / 正常完成)。
|
||||
`{ "kind": "rejected", "text": "验收驳回:测算毛利低于阈值,退回重新测算 —— 负责人 柴浩。" }`
|
||||
`{ "kind": "failed", "text": "要素结构化失败:询价单附件缺失,已通知发起人补齐后重试。" }`
|
||||
`{ "kind": "terminated", "text": "已于 08-27 10:41 由 柴浩 手动终止。" }` —— **手动终止的实例状态归 `failed`,靠 `reason.kind='terminated'` 与「自己跑挂」区分**(过程面当前步记「已终止」,不摆失败原因)。
|
||||
- `input.source.kind`:`manual`(运营端手动启动)/ `upstream`(上游活动触发)/ `mail`(邮件触发)…… 枚举随触发渠道增加;`label` 是人读形式,前端直接摆。
|
||||
- `steps[].res`:`done` / `fail` / `current` / `pending`;`fail` 那行的性质由 `reason.kind` 定(驳回 / 失败 / 已终止)。
|
||||
- `input.files` / `output.files`:只有元数据;**下载走 §7**。没有产出时 `output.files` 是空数组(前端摆「暂无产出」)。
|
||||
三种终局的 `reason` 示例:
|
||||
|
||||
## 7. 实例文件下载
|
||||
```json
|
||||
{ "kind": "rejected", "text": "验收驳回:测算毛利低于阈值,退回重新测算 —— 负责人 柴浩" }
|
||||
{ "kind": "failed", "text": "预算耗尽:步数 90/90、活跃时长 120/120 分钟" }
|
||||
{ "kind": "terminated", "text": "由 柴浩 手动终止" }
|
||||
```
|
||||
|
||||
### `GET /api/gov/activities/:activityId/instances/:instanceId/files/:io/:index` —— access: `ops`(支持 `?ticket=`)
|
||||
- 头部:状态丸(`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。
|
||||
|
||||
`:io` ∈ `input` | `output`,`:index` 对应 §6 文件行的 `index`。
|
||||
→ `{ "name": "…", "contentType": "…", "dataBase64": "…" }`(与 `GET /api/gov/upload/:id` 同形);可带 `?ticket=` 用 `<a href download>`。
|
||||
**本期可以只回元数据、这条路先 404** —— 前端下载钮当前就是「随接口开放」的置灰态。
|
||||
## 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. 禁用 / 激活 / 终止
|
||||
|
||||
### `POST /api/gov/activities/:activityId/disable` —— access: `ops`
|
||||
### 8.1 禁用 —— `POST /api/gov/activities/:activityId/disable`(`disableOpsActivityRoute`)
|
||||
|
||||
→ 200 活动视图(§2 同形)。**任何时刻都可禁用,没有前置条件**(2026-08-27 用户重定):
|
||||
禁用后这个活动不再允许启动(§5 返回 412);**已完成的实例是历史留痕,进行中的实例继续跑完,都不受影响**。
|
||||
已禁用再禁用 = 幂等 200。
|
||||
**请求**:路径参数 `activityId`;**body 空**。
|
||||
**响应 200**:`ActivityDetail`(`status='disabled'`)。**404**:活动不存在。已禁用再禁用 = 幂等 200。
|
||||
任何时刻都可禁用,没有前置条件:禁用后 §5 回 412;已完成的实例是历史留痕,进行中的继续跑完。
|
||||
|
||||
### `POST /api/gov/activities/:activityId/activate` —— access: `ops`
|
||||
### 8.2 激活 —— `POST /api/gov/activities/:activityId/activate`(`activateOpsActivityRoute`)
|
||||
|
||||
→ 200 活动视图。已激活再激活 = 幂等 200。
|
||||
**请求**:路径参数 `activityId`;**body 空**。
|
||||
**响应 200**:`ActivityDetail`(`status='active'`)。**404**:活动不存在。已激活再激活 = 幂等 200。
|
||||
|
||||
### `POST /api/gov/activities/:activityId/instances/:instanceId/terminate` —— access: `ops`
|
||||
### 8.3 终止实例 —— `POST /api/gov/activities/:activityId/instances/:instanceId/terminate`(`terminateOpsInstanceRoute`)
|
||||
|
||||
body 空(终止理由本期不填,`reason.text` 由服务端生成)。
|
||||
→ 200 实例详情(§6 同形,`state='failed'` + `reason.kind='terminated'`)。412:实例不在 `running`(已终态的不能再终止)。
|
||||
**请求**:路径参数 `activityId`、`instanceId`;**body 空**(终止理由本期不填,服务端生成「由 <操作人> 手动终止」)。
|
||||
**响应 200**:`InstanceDetail`,`state='failed'`、`reason.kind='terminated'`、`endedAt` = 终止时刻。
|
||||
**404**:实例不存在。**412**:实例已终态(已完成 / 已失败 / 已终止的不能再终止;`pending` 与 `running` 都可以)。
|
||||
前端:二次确认(轻气泡)后调用;成功后由服务端重投影(广播 → 失效 → 重取),不要就地改 state。
|
||||
|
||||
## 9. Schema 附录(落地时进 `packages/contracts`,同名)
|
||||
## 9. 类型附录(`packages/contracts/src/activity-ops.ts`,同名导出)
|
||||
|
||||
```ts
|
||||
type ActivityStatus = 'active' | 'disabled';
|
||||
@@ -302,41 +534,76 @@ 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; llm: string; instanceCount: number; createdAt: string; ico?: string; hue?: 'blue' | 'teal' | 'amber' | 'violet' };
|
||||
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; state: InstanceState; owner: StaffRef; input: string; output: string | null; startedAt: string };
|
||||
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 }[] /* select / combo 必填 */; default?: string | 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 /* 责任人,固定字段,名册 staffId */; params: Record<string, string | string[]> };
|
||||
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 = InstanceSummary & { activityId: string; llm: string; endedAt: string | null; durationMs: number | null;
|
||||
type InstanceDetail = Omit<InstanceSummary, 'input' | 'output'> & { activityId: string; llm: string; endedAt: string | null; durationMs: number | null;
|
||||
reason: InstanceReason | null;
|
||||
input: { content: string; source: { kind: string; label: string }; submittedAt: string; files: InstanceFile[] };
|
||||
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` 静态数组 | §1;L1 页签切换即换 `status` 重取 |
|
||||
| `demoInstances()` 每活动共用一份 | §3;检索四条件与分页全部上移到查询参数,前端删掉本地过滤/分页 |
|
||||
| `DEMO_ACTIVITIES` 静态数组 | §2.1;L1 页签切换即换 `status` 重取;`owner` 为 null 摆「—」,`ico`/`hue` 缺省兜底 |
|
||||
| `demoInstances()` 每活动共用一份 | §3;检索四条件与分页全部上移到查询参数,前端删掉本地过滤/分页;表格摆 `no`,寻址用 `id` |
|
||||
| 统计带由前端从全量数组数 | §3 `stats`(服务端给,分页下才数得准) |
|
||||
| `LAUNCH_PARAMS` 占位参数表 | §4;`options` 从纯字符串升级为 `{value,label}` 两段(渲染器小改);控件种类已含 `combo`(选择+新增,前端已实现) |
|
||||
| `onLaunch` 在清单头落假行 | §5;抽屉每开一次生成 `idempotencyKey`,提交走 mutation,成功失效 `['gov']` |
|
||||
| —(新增)责任人固定字段 | §5 `ownerId`;抽屉「固定参数」段已实现(名册下拉、默认当前登录人),接接口后直接随 body 走 |
|
||||
| 禁用/激活/终止就地改 state | §8;mutation 成功后由服务端重投影(广播 → 失效 → 重取) |
|
||||
| 中文状态字面量当键 | 五态英文码 + `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 `view.url` + 票据;`view` 为 null 藏起沙箱区;高度 postMessage 随第一版视图定 |
|
||||
| 输出下载钮置灰 | §7(本期可继续 404,钮保持置灰) |
|
||||
| 沙箱 `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 的是路由对象),无需改动 |
|
||||
|
||||
Reference in New Issue
Block a user