This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/docs/API文档-活动模块.md
T

18 KiB

API 文档 —— 活动模块(L1 清单 / L2 活动详情 / L3 实例详情)

本文是活动模块前端(运营中心 · 活动区,2026-08-25 重构模版)与服务端之间的接口约定, 给后端编写接口、前端取数用。鉴权 / 票据 / 错误体 / 端归属通则:docs/API文档-鉴权与人员.md §0,本文不重复。 需求出处:docs/活动模块重构需求-2026-08-25.md;启动参数口径:ACT-WORK-05-一键启动入参说明.md(极简四参)。 契约落地后唯一真相在 packages/contracts/src/api/*.ts(zod schema,前后端共用);两者不一致时以契约为准并回来改本文。

0. 通则(本文特有)

项 值
前缀 /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 用户重定,废除「有实例不能禁用」):禁用只拦「新启动」—— 已完成的实例是历史留痕,进行中的实例继续跑完不受影响;激活即恢复启动能力

状态码怎么读(通则之外本文特有的):

状态 code 何时
400 validation-failed 入参不合 schema(detail 是 zod issues);启动参数缺必填 / 类型不对(detail 指明是哪个 key)
404 not-found 活动 / 实例不存在;实例不属于这个活动
412 precondition-failed 启动:活动已禁用;终止:实例不在 running

1. L1 活动清单

GET /api/gov/activities?status=active|disabled —— access: ops

L1 两个页签(激活中 / 已禁用)各取一份;不带 status = 全量。量小,不分页。

{
  "activities": [
    {
      "id": "quote-intake",
      "name": "询价接收与结构化",
      "biz": "报价业务",
      "status": "active",
      "version": "1.3.0",
      "owner": { "id": "8f3c…", "name": "柴浩" },
      "llm": "K3 · quote-intake",
      "instanceCount": 12,
      "createdAt": "2026-08-12T01:00:00.000Z",
      "ico": "inbox",
      "hue": "blue"
    }
  ]
}
  • ico / hue:展示元数据(语义图标名 / 业务色相 blue|teal|amber|violet),创建活动时可配;缺省时前端兜底(inbox / blue),后端可以暂时不存。
  • llm:这个活动绑定的专属 agent 的人读标签;要链到 LLM 实例时再加 llmId,本期没有。

2. L2 活动定义

GET /api/gov/activities/:activityId —— access: ops

{
  "id": "quote-intake",
  "name": "询价接收与结构化",
  "biz": "报价业务",
  "status": "active",
  "version": "1.3.0",
  "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" }
}
  • desc:定义说明,创建时填,只读。
  • view:沙箱要渲染的活动视图;为 null 时前端藏起整个沙箱区块(不再摆空 iframe)。

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:该活动没配视图。

3. L2 实例清单(历史活动)

GET /api/gov/activities/:activityId/instances —— access: ops

查询参数(可组合,AND;都不带 = 全部):

参数 形 说明
id 字符串 实例号模糊搜,如 INS-24
ownerId staffId 精确到人
state 五态之一 精确
startedFrom / startedTo YYYY-MM-DD 启动日期段,含两端整日;可只给一端
page 整数,从 1 默认 1
size 整数 默认 8(与前端单页一致),上限 50
{
  "stats": { "total": 13, "byState": { "pending": 1, "running": 1, "completed": 7, "rejected": 2, "failed": 2 } },
  "total": 13,
  "page": 1,
  "size": 8,
  "items": [
    {
      "id": "INS-2417",
      "state": "running",
      "owner": { "id": "8f3c…", "name": "柴浩" },
      "input": "Itelma 车身控制器询价 · 3 项物料",
      "output": null,
      "startedAt": "2026-08-25T02:12:00.000Z"
    },
    {
      "id": "INS-2415",
      "state": "completed",
      "owner": { "id": "8f3c…", "name": "柴浩" },
      "input": "GAZ 底盘线束询价 · 7 项物料",
      "output": "quote-20260824-07.json",
      "startedAt": "2026-08-24T09:03:00.000Z"
    }
  ]
}
  • stats 是活动整体的五态统计,不随检索条件动(页头统计带摆的是全局数);total 是筛后条数,分页器按它走。
  • input / output 都是一句话摘要;output 为 null 前端摆 -。排序:启动时间倒序(新的在前)。

4. 启动参数表

GET /api/gov/activities/:activityId/launch-params —— access: ops

点「启动活动」开抽屉时取。一活动一份,前端只按此表渲染表单,不写死字段。

{
  "activityId": "quote-intake",
  "version": "1.3.0",
  "params": [
    {
      "key": "customer_id",
      "label": "客户",
      "type": "combo",
      "required": true,
      "desc": "台账里有就直接选;是新客户就自己填,启动后派数据工单补档案。",
      "options": [
        { "value": "cust-itelma", "label": "Itelma" },
        { "value": "cust-gaz", "label": "GAZ" }
      ]
    },
    {
      "key": "request_description",
      "label": "客户需求",
      "type": "textarea",
      "required": true,
      "desc": "保留原始需求语义,据此新建需求主记录(V1)。",
      "placeholder": "如:车身控制器询价 · 3 项物料,期望 9 月交期"
    },
    {
      "key": "original_internal_pn",
      "label": "原内部料号",
      "type": "combo",
      "required": true,
      "desc": "差异评估基准;库里没有该 PN 也可启动,启动后派数据工单补参数。",
      "placeholder": "如 PN-880501",
      "options": [
        { "value": "PN-880101", "label": "PN-880101" }
      ]
    },
    {
      "key": "candidate_internal_pn",
      "label": "候选内部料号",
      "type": "list",
      "required": true,
      "desc": "被评估物料,一次可传多个、逐个评估 —— 至少 1 个。",
      "placeholder": "如 PN-880412"
    }
  ]
}
字段 说明
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 —— 先选,候选里没有就新增自填。提交候选之外的值是合法的,不拦启动:服务端按 value 在台账里查,查不到照样启动,启动后派数据工单到工作台补这条档案(客户进客户台账、PN 补参数,确认即入库,活动从断点续跑)。combo 提交的值统一回字符串:选中候选回它的 value,新增回用户填的原样字符串 —— 服务端不靠类型区分新旧,靠台账存在性检查。

候选量大到不宜内联时(如全量物料台账选 PN),后续加 optionsSource 引用检索接口,本期一律内联 options。

5. 启动活动(创建实例)

POST /api/gov/activities/:activityId/instances —— access: ops

{
  "idempotencyKey": "01J9…",
  "params": {
    "customer_id": "cust-itelma",
    "request_description": "车身控制器询价 · 3 项物料,期望 9 月交期",
    "original_internal_pn": "PN-880101",
    "candidate_internal_pn": ["PN-880412", "PN-880530"]
  }
}

→ 200 一条实例详情(同 §6)。同一 idempotencyKey 重放返回第一次创建的实例,不起第二个(前端每开一次抽屉生成一枚;重复点提交、网络重试都靠它挡住)。 params 的键值形状由 §4 参数表决定:list 回字符串数组,其余回字符串;select 回 options 里某个 value;combo 回候选的 value 或新增的原样字符串,两种都合法。 412:活动已禁用(先激活再启动)。400:缺必填 / 类型不符 / select 的值不在 options 里(combo 不适用这条 —— 候选之外的值是合法新增)。

正常落地初始状态 pending(排入队列,调度器领到即转 running);存在性检查(客户 / 料号在不在台账)在启动后做,查不到不拦启动 —— 按入参说明口径,缺口派数据工单到工作台逐项补(确认即入库,活动从断点续跑);响应里不单独回这件事,工单出现在工作台那一侧(工单接口不在本文范围)。

6. L3 实例详情

GET /api/gov/activities/:activityId/instances/:instanceId —— access: ops

{
  "id": "INS-2417",
  "activityId": "quote-intake",
  "state": "running",
  "owner": { "id": "8f3c…", "name": "柴浩" },
  "llm": "K3 · quote-intake",
  "startedAt": "2026-08-25T02:12:00.000Z",
  "endedAt": null,
  "durationMs": null,
  "reason": null,
  "input": {
    "content": "Itelma 车身控制器询价 · 3 项物料",
    "source": { "kind": "manual", "label": "手动启动 · 柴浩" },
    "submittedAt": "2026-08-25T02:12:00.000Z",
    "files": [
      { "index": 0, "name": "RFQ-2026-081.eml", "contentType": "message/rfc822", "size": 48210 }
    ]
  },
  "output": {
    "files": [
      { "index": 0, "name": "quote-20260825-07.json", "contentType": "application/json", "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": "生成报价文件并归档,产出回写台账。" }
  ]
}
  • 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 是空数组(前端摆「暂无产出」)。

7. 实例文件下载

GET /api/gov/activities/:activityId/instances/:instanceId/files/:io/:index —— access: ops(支持 ?ticket=)

:io ∈ input | output,:index 对应 §6 文件行的 index。 → { "name": "…", "contentType": "…", "dataBase64": "…" }(与 GET /api/gov/upload/:id 同形);可带 ?ticket= 用 <a href download>。 本期可以只回元数据、这条路先 404 —— 前端下载钮当前就是「随接口开放」的置灰态。

8. 禁用 / 激活 / 终止

POST /api/gov/activities/:activityId/disable —— access: ops

→ 200 活动视图(§2 同形)。任何时刻都可禁用,没有前置条件(2026-08-27 用户重定): 禁用后这个活动不再允许启动(§5 返回 412);已完成的实例是历史留痕,进行中的实例继续跑完,都不受影响。 已禁用再禁用 = 幂等 200。

POST /api/gov/activities/:activityId/activate —— access: ops

→ 200 活动视图。已激活再激活 = 幂等 200。

POST /api/gov/activities/:activityId/instances/:instanceId/terminate —— access: ops

body 空(终止理由本期不填,reason.text 由服务端生成)。 → 200 实例详情(§6 同形,state='failed' + reason.kind='terminated')。412:实例不在 running(已终态的不能再终止)。

9. Schema 附录(落地时进 packages/contracts,同名)

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; 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 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[] };
type LaunchParams = { activityId: string; version: string; params: LaunchParam[] };
type LaunchInput = { idempotencyKey: 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 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;
  reason: InstanceReason | null;
  input: { content: string; source: { kind: string; label: string }; submittedAt: string; files: InstanceFile[] };
  output: { files: InstanceFile[] };
  steps: InstanceStep[] };

10. 前端对接要点(现状 → 接口落地)

现状(占位) 接口落地后
DEMO_ACTIVITIES 静态数组 §1;L1 页签切换即换 status 重取
demoInstances() 每活动共用一份 §3;检索四条件与分页全部上移到查询参数,前端删掉本地过滤/分页
统计带由前端从全量数组数 §3 stats(服务端给,分页下才数得准)
LAUNCH_PARAMS 占位参数表 §4;options 从纯字符串升级为 {value,label} 两段(渲染器小改);控件种类已含 combo(选择+新增,前端已实现)
onLaunch 在清单头落假行 §5;抽屉每开一次生成 idempotencyKey,提交走 mutation,成功失效 ['gov']
禁用/激活/终止就地改 state §8;mutation 成功后由服务端重投影(广播 → 失效 → 重取)
demoSteps / terminatedSteps 五步占位 §6 steps;终止归态以 reason.kind='terminated' 为准,前端不再落「失败」占位
沙箱 src="/sandbox-demo/activity-view.html" 固定 480 高 §2 view.url + 票据;view 为 null 藏起沙箱区;高度 postMessage 随第一版视图定
输出下载钮置灰 §7(本期可继续 404,钮保持置灰)