Files
group_fqcd_jr/docs/05-接口文档.md
T
lzf_0626 36c7a9d8d2 文档:审查报告入库 + 全量校对补注
## 新入库(`docs/演示用/`)

- `代码库全面审查报告-2026-09-14.md`
- `代码修改方案-2026-09-14.md`
- `记忆系统排查报告-2026-09-14.md`
- `记忆系统修复文档-2026-09-14.md`
- `文档一致性审计报告-2026-09-14.md`
- `多Worker接入方案-2026-09-14.md`

## 全量校对(32 个既有文档 + `AGENTS.md`)

跨 39 个文件、**1125 insertions / 148 deletions**。

⚠️ **这批改动同样不是本次会话写的**。我抽样核对过性质:是**实质内容补充**而不是
格式/换行转换。例如 `docs/44-演示流程.md` 新增两条"2026-09-14 补注":

- `启动金融Agent平台.bat` 只在**桌面**上,仓库里只有 `启动平台.bat` 这一份
  (两份由同一个 `tools/make_launcher_bat.py` 产出,改完 `start.ps1` 重跑它一起更新);
- `advisor_t`(9020) 与 `offsite_t`(9006) **不在 `tools/seed_test_rbac.py` 的演示用户里**
  (那里只有 `cust_t`/`risk_t`/`admin_t`/`review_t` 四个),由 `grant_*.py` 系列创建,
  **重跑种子不会重建它们** —— 换机器时这两个账号登录失败,要先查 `sys_user` 有没有这两行,
  而不是查密码。

这两条都是对的地方,与我这一路踩到的现象一致(我确实用到了 `advisor_t`/`offsite_t`)。

**我没有逐字审阅全部 39 个文件**,只抽样确认了改动性质与规模。若其中有需要复核的段落,
请指明文件,我逐处核对。
2026-09-14 20:36:00 +08:00

68 KiB
Raw Blame History

MVC+S Agent 平台接口文档

版本:v1.0
状态:评审稿
修订日期:2026-09-09
适用对象:前端、后端、测试、运维、业务 Agent 开发人员和编码 Agent
数据库约束:允许新增表和字段,禁止修改已有表名及已有字段定义

1. 文档目的

本文是 Agent 平台 HTTP 接口的唯一权威规范,定义运行面、会话面、平台管理面、SSE 传输、JWT 鉴权、统一信封、错误映射、幂等、分页、审计及业务扩展规则。

内部 Python 类型、Service Protocol、领域事件字段和数据库 DDL 不在本文重复定义。本文通过权威源矩阵引用已有设计,避免同一契约出现两个定义。

当前业务能力只针对场内基金模拟交易。场外基金运营使用独立业务表和接口,不得写入场内模拟交易表。

2. 权威源与边界

2.1 权威源矩阵

契约项 契约编号 唯一权威源 本文职责
Agent 请求与上下文 AGENT-REQ-001 01 §5.2 定义 HTTP 投影和映射规则
Agent 结果 AGENT-RESULT-001 01 §5.3 定义对外字段和序列化
Service Protocol AGENT-SERVICE-001 01 §5.4 仅登记依赖方向
Agent 错误体系 AGENT-ERROR-001 01 §5.5 定义 HTTP 状态码映射
领域事件 AGENT-EVENT-001 01 §5.4、02 §8.2 定义触发接口和触发时机
SSE 事件载荷 AGENT-SSE-001 01 §12 定义 HTTP 传输方式
Agent 执行顺序 AGENT-FLOW-001 01 §6、03 §3-§5 保证接口不绕过执行顺序
数据库实体 DB-BASE-001 00、02 仅定义资源与实体映射
请求幂等 DB-IDEMPOTENCY-001 02 §8.3 定义 HTTP 幂等语义
Agent 运行 DB-RUN-001 02 中的 agent_run 定义运行资源语义
领域状态机 DOMAIN-FLOW-001 对应业务流程文档 仅登记入口和 Agent 边界

2.2 冲突裁决

  1. 本文的认证、路径、Header、HTTP 信封、分页、错误码映射和版本规则优先于业务接口文档。
  2. 业务字段、业务状态机和业务校验以对应业务文档为准,本文不得复制后形成第二权威源。
  3. 内部 DTO、Service Protocol、事件字段和数据库字段以矩阵指定文档为准。
  4. 同一契约出现两处定义时,非权威定义必须删除并改为引用,不允许两份定义同时进入实现。
  5. OpenAPI 是本文 HTTP 契约的机器可读投影。OpenAPI 与本文冲突时先停止发布并修正文档或生成逻辑。

2.3 MVC+S 依赖边界

Controller -> Application Service -> Domain Service/Repository -> Model
     |
     `-> View

AgentRunService -> RunRepository/IdempotencyService/RunDispatchPort
Run Worker -> AgentExecutor -> AgentFactory -> BaseAgent
Run Query Service -> RunRepository/ConversationRepository -> JSON/SSE View
  • Controller 不调用 Agent、ORM、Redis、Milvus、Neo4j 或消息中间件。
  • Worker 不依赖 HTTP、Controller 或 SSE View。
  • BaseAgent 返回传输无关的执行结果,不生成 HTTP 数据帧。
  • JSON 查询和 SSE 订阅使用同一个 RunQueryService 权威投影。
  • HTTP DTO 与内部 Agent DTO 分离,通过显式 Mapper 转换。
  • Redis 只承担缓存和实时通知;MySQL 保存权威运行状态和最终结果。

3. 通用 HTTP 约定

3.1 基础地址与命名

  • 业务接口前缀:/api/v1。
  • 平台管理接口前缀:/api/v1/admin。
  • 内部运维接口前缀:/internal。
  • HTTP 路径使用 kebab-case,资源名称使用复数。
  • agent_type 使用 snake_case,格式为 [a-z][a-z0-9_]{1,31}。
  • 请求和响应编码统一为 UTF-8。
  • JSON 请求使用 Content-Type: application/json。

3.2 通用请求 Header

Header 是否必填 规则
Authorization 是 Bearer <JWT>,运维接口除外
Idempotency-Key 写接口按本文要求 8-64 个可打印 ASCII 字符
Content-Type 有请求体时是 application/json
Accept 否 默认 application/json;SSE 为 text/event-stream
If-Match 管理面更新和状态转换是 使用服务端上次返回的 ETag
traceparent 否 符合 W3C Trace Context;非法值由服务端替换

所有响应返回 X-Trace-ID。客户端不能通过请求正文或 metadata 指定用户、角色、客户范围、模型、工具、合规策略或追踪标识。

3.3 成功信封

单资源:

{
  "data": {},
  "meta": {
    "trace_id": "trace-uuid"
  }
}

列表资源:

{
  "data": [],
  "meta": {
    "trace_id": "trace-uuid",
    "next_cursor": "opaque-cursor",
    "has_more": true
  }
}

业务接口不得增加其他顶层字段。SSE、文件下载和运维健康检查不使用业务 JSON 信封。

3.4 错误信封

{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "message": "幂等键已用于不同请求",
    "retryable": false,
    "field_errors": []
  },
  "meta": {
    "trace_id": "trace-uuid"
  }
}

field_errors 元素格式为:

{
  "field": "metadata.ui_entry",
  "reason": "格式不正确"
}

错误响应禁止包含异常堆栈、SQL、模型原文、供应商响应、内部网络地址和密钥引用。

3.5 HTTP 状态码

状态码 使用场景
200 查询、幂等更新或同步操作成功
201 同步创建资源成功
202 异步任务或取消请求已受理
204 操作成功且无响应体
304 条件查询内容未变化
400 JSON、Header 或查询参数无法解析
401 JWT 缺失、无效、过期或已吊销
403 角色、权限、适当性或数据范围拒绝
404 资源不存在,或为防止越权枚举而隐藏资源
409 幂等冲突、版本冲突或非法状态转换
413 请求体或上传文件超过大小限制
422 已解析请求不满足字段或业务输入约束
429 频率、并发或配额限制
500 未分类内部错误
502 上游模型或工具返回无效响应
503 必需依赖不可用
504 上游调用超时且无法降级

3.6 核心错误码

错误码 HTTP 是否可重试 说明
AGENT_INPUT_INVALID 422 否 请求内容不满足 Agent 输入约束
AGENT_PERMISSION_DENIED 403 否 角色、入口或数据范围不允许
AUTHENTICATION_REQUIRED 401 否 Token 缺失或无效
SESSION_NOT_FOUND 404 否 会话不存在或已被资源隐藏
SESSION_NOT_ACCESSIBLE 404 否 当前身份不能访问会话
AGENT_TYPE_NOT_FOUND 404 否 Agent 未注册
IDEMPOTENCY_CONFLICT 409 否 同一幂等键对应不同正文
RESOURCE_VERSION_CONFLICT 409 是 If-Match 版本过期
RUN_NOT_FOUND 404 否 运行不存在或不可见
RUN_NOT_CANCELLABLE 409 否 运行已进入不可取消阶段
RUN_CANCELLED 409 否 运行已取消
INVALID_CURSOR 400 否 游标非法、过期或与过滤条件不符
RATE_LIMITED 429 是 频率、并发或配额限制
DEPENDENCY_UNAVAILABLE 503 是 必需依赖不可用
UPSTREAM_TIMEOUT 504 是 上游超过时间预算
AGENT_INTERNAL_ERROR 500 视情况 未分类内部错误
ACCOUNT_NOT_FOUND 404 否 账户不存在或状态非"正常"
INSUFFICIENT_FUNDS 422 否 账户可用资金不足以扣减本次买入金额+费用
INSUFFICIENT_HOLDING 422 否 可用持仓不足以卖出本次数量
PRODUCT_NOT_TRADABLE 422 否 产品未上市或不在交易时段
FUND_QUOTE_UNAVAILABLE 503 是 行情快照缺失或过期(持仓比例上限校验依赖)
HOLDING_RATIO_EXCEEDED 422 否 买入后超过产品持仓比例上限
SUITABILITY_MISMATCH 422 否 客户适当性等级与产品风险等级不兼容
ORDER_NOT_CANCELLABLE 409 否 委托已进入不可撤单阶段(首版直接成交后不可撤)
ORDER_NOT_FOUND 404 否 委托不存在或不属于当前客户

3.7 数据格式

  • 时间统一使用 UTC 的 RFC 3339,例如 2026-09-09T08:30:00.000000Z。
  • 金额、价格、数量和置信度使用十进制字符串,禁止使用 JSON 浮点数。
  • 数据库 BIGINT 对外序列化为字符串,避免前端整数精度丢失。
  • 布尔值使用 true/false,不得使用 0/1。
  • 空集合返回 [],空对象返回 {},无值返回 null。
  • 未特别说明的文本字段去除首尾空白,禁止不可见控制字符。

3.8 游标分页

列表接口统一使用 cursor 和 limit。limit 默认 20、最小 1、最大 100。游标是不透明字符串,绑定用户、查询条件、排序字段和方向,客户端不得解析或修改。

默认排序为 created_at DESC, id DESC。默认不返回总条数;确需精确统计时使用对应领域的独立统计接口。

3.9 接口版本

SSE 实现补充:运行未完成时建立的连接,在观察到最终事务提交后分块输出 delta; 已完成运行的新连接使用 replace 返回完整结果。两者都是结果级恢复,不提供事件游标续传。 块长度通过 .env 的 SSE_CHUNK_CHARACTERS 控制,按 Unicode 字符切分,空结果也输出一个 delta。

  • 兼容变更:新增可选字段、新增接口、新增可识别的错误码。
  • 破坏性变更:删除或重命名字段、改变字段类型或含义、把可选字段改为必填、改变既有状态语义。
  • 破坏性变更必须升级主路径,例如 /api/v2。
  • 客户端必须对未知枚举值提供兜底行为。
  • 废弃接口返回 Deprecation、Sunset 和替代接口链接。
  • 废弃期不得少于两个发布周期且不得少于 90 天。

4. JWT 鉴权与授权

4.1 Token 要求

Authorization: Bearer <access-token>

JWT 至少包含 sub、jti、iss、aud、iat、nbf 和 exp。服务端必须验证签名算法白名单、签发方、受众、时间窗口和吊销状态。

JWT 只证明身份和基础授权范围。服务端每次请求重新加载有效用户状态、角色、权限、客户归属和数据范围;不得完全信任 Token 中的历史角色信息。

4.2 权限处理

实现补充(2026-09-09):统一 REST 入口在服务端设置 portal=api;Agent 声明须显式允许 此入口,metadata、Header 和 JWT 中的 portal/roles 均不作为授权依据。用户状态须为基线的 正常,角色状态为 active,角色分配在有效期内。每次请求直读 RBAC,撤权立即生效; 暂不缓存授权数据。运行同时要求 agent:run 权限及 AgentDefinition 的角色、入口交集。 权限范围按 permission_code 分别保存,禁止把另一权限的 all 扩散到全部资源。 管理面需 admin/super_admin 角色及第 19 节对应操作权限;拒绝追加审计。

  • 客户只能访问自己的运行、会话、消息、反馈、转人工请求和安全记忆投影。
  • 客服只能访问进入客服队列或分配给自己的会话和工单。
  • 投顾只能访问 sys_customer_assignment 中归属自己的客户。
  • 风控人员只能访问其角色和数据范围允许的风险记录。
  • 普通管理员默认无权查看未脱敏会话正文和敏感审计详情。
  • 越权资源统一返回 404,避免泄露资源是否存在;明确功能无权限时返回 403。

4.3 SSE 鉴权

SSE 订阅必须携带 Bearer Token。浏览器客户端使用支持自定义 Header 的 fetch 流式实现或合规 SSE 客户端,不使用无法设置 Authorization Header 的原生 EventSource。

连接建立时验证 JWT 和资源权限。Token 在连接期间过期不强制截断已经通过鉴权的短连接,但重连必须重新鉴权。

5. 幂等与并发

5.1 幂等范围

必须携带 Idempotency-Key 的接口包括:

  • 创建 Agent 运行。
  • 取消 Agent 运行。
  • 创建或关闭会话。
  • 创建转人工请求。
  • 业务写接口。
  • 配置审核、激活、回滚、停用和归档。

通用幂等范围为 user_id + HTTP method + normalized_path + idempotency_key。Agent 运行同时校验 agent_type。请求哈希使用规范化后的路径、查询参数和 JSON 正文计算,不包含 Authorization、trace Header 和传输时间。

5.2 重复请求

  • 同一范围、同一键、同一哈希:返回原资源和当前状态。
  • 同一范围、同一键、不同哈希:返回 409 IDEMPOTENCY_CONFLICT。
  • 原异步任务仍在执行:返回原 run_id,不得创建第二个任务。
  • Worker 租约超时:接管原 run_id,不得生成新的运行记录。
  • 已完成请求:返回原结果资源地址。

5.3 乐观并发

管理面草稿更新和状态转换必须携带 If-Match。ETag 是服务端根据资源版本生成的不透明值。版本不一致返回 409 RESOURCE_VERSION_CONFLICT。

已审核、已激活、已停用或已归档的不可变版本不得原地编辑。

6. Agent 运行接口

6.1 运行状态

queued -> running -> succeeded
                  -> failed
queued/running -> cancel_requested -> cancelled
  • queued:受理事务已提交,等待 Worker。
  • running:Worker 持有有效租约并正在执行。
  • cancel_requested:已收到取消请求,等待 Worker 到达安全停止点。
  • succeeded:最终消息、审计、幂等完成状态和 Outbox 已原子提交。
  • failed:不可恢复错误及审计已经提交。
  • cancelled:在最终事务开始前成功取消。

6.2 创建运行

POST /api/v1/agent-runs
Authorization: Bearer <token>
Idempotency-Key: <key>
Content-Type: application/json

请求:

{
  "agent_type": "customer_service",
  "session_id": "session-uuid",
  "message": "场内基金卖出后资金什么时候可用?",
  "end_session": false,
  "metadata": {
    "locale": "zh-CN",
    "client_version": "1.0.0",
    "ui_entry": "customer_chat"
  }
}

message 长度 1-8000,不能只包含空白。metadata 只允许 locale、client_version 和 ui_entry,当前 locale 只允许 zh-CN。会话必须属于当前用户或在员工数据范围内,Agent 必须已注册且允许当前角色和入口。

返回 202 Accepted:

{
  "data": {
    "run_id": "run-uuid",
    "trace_id": "trace-uuid",
    "status": "queued",
    "status_url": "/api/v1/agent-runs/run-uuid",
    "events_url": "/api/v1/agent-runs/run-uuid/events",
    "created_at": "2026-09-09T08:30:00.000000Z"
  },
  "meta": {
    "trace_id": "trace-uuid"
  }
}

受理事务必须原子保存用户消息、request_idempotency、agent_run 和 agent.run_requested Outbox。事务失败时不得返回 run_id。

主要错误:AGENT_INPUT_INVALID、AGENT_PERMISSION_DENIED、SESSION_NOT_FOUND、SESSION_NOT_ACCESSIBLE、AGENT_TYPE_NOT_FOUND、IDEMPOTENCY_CONFLICT、RATE_LIMITED。

6.3 查询运行

GET /api/v1/agent-runs/{run_id}
Authorization: Bearer <token>

返回 200 OK:

{
  "data": {
    "run_id": "run-uuid",
    "trace_id": "trace-uuid",
    "session_id": "session-uuid",
    "agent_type": "customer_service",
    "status": "succeeded",
    "result_version": 1,
    "result": {
      "message_id": "12345",
      "reply": "最终安全回复",
      "intent": "faq",
      "confidence": "0.9400",
      "source_references": [],
      "suggestions": [],
      "transfer_required": false,
      "transfer_reason": null,
      "degraded": false,
      "degradation_reason": null
    },
    "error": null,
    "created_at": "2026-09-09T08:30:00.000000Z",
    "started_at": "2026-09-09T08:30:01.000000Z",
    "completed_at": "2026-09-09T08:30:04.000000Z"
  },
  "meta": {
    "trace_id": "trace-uuid"
  }
}
  • 未完成时 result 和 error 均为 null。
  • 成功时 result 来自已持久化助手消息,不能从 Worker 内存拼装。
  • 失败时 result 为 null,error 只包含安全错误码、消息和可重试标志。
  • 取消时不创建助手成功消息。
  • result_version 用于客户端重连去重,MVP 成功结果固定为 1。

主要错误:RUN_NOT_FOUND、AGENT_PERMISSION_DENIED。

6.4 订阅运行结果

GET /api/v1/agent-runs/{run_id}/events
Authorization: Bearer <token>
Accept: text/event-stream

成功响应 Header:

Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache, no-transform
X-Accel-Buffering: no
X-Trace-ID: trace-uuid

传输顺序:

运行未完成:start -> 注释心跳 -> 等待
正常完成:start -> tools(可选) -> delta(一个或多个) -> done -> 关闭
安全替换:start -> replace -> done -> 关闭
不可恢复:start -> error -> 关闭

帧示例:

event: start
data: {"trace_id":"trace-uuid","session_id":"session-uuid"}

: keepalive

event: delta
data: {"trace_id":"trace-uuid","text":"安全文本片段"}

event: done
data: {"trace_id":"trace-uuid","intent":"faq","confidence":"0.9400","sources":[],"suggestions":[]}

事件名称和业务载荷以 AGENT-SSE-001 为准。注释心跳不是业务事件。tools、delta、replace 和 done 只能在最终持久化事务成功后发送。

MVP 采用结果级恢复,不实现事件级游标续传。Last-Event-ID 可以被忽略。重连时服务端根据 agent_run 和 conversation_message 重新发送完整最终结果,客户端按 run_id + result_version 去重。

若错误发生在响应 Header 发送前,返回统一 JSON 错误;发送后使用 error 事件并关闭连接。done 和 error 都是终止事件,之后不得继续发送数据。

主要错误:RUN_NOT_FOUND、AGENT_PERMISSION_DENIED、SSE_NOT_ACCEPTABLE。

6.5 取消运行

POST /api/v1/agent-runs/{run_id}/cancellations
Authorization: Bearer <token>
Idempotency-Key: <key>

请求:

{
  "reason": "user_cancelled"
}

返回 202 Accepted:

{
  "data": {
    "run_id": "run-uuid",
    "status": "cancel_requested",
    "cancel_requested_at": "2026-09-09T08:30:02.000000Z"
  },
  "meta": {
    "trace_id": "trace-uuid"
  }
}

重复取消返回同一状态。运行已成功、失败或进入最终提交事务时返回 409 RUN_NOT_CANCELLABLE。取消成功后,request_idempotency 使用 failed + RUN_CANCELLED 表示原请求已终止,不扩展其既有状态枚举。

7. 会话、消息、反馈与转人工接口

7.1 创建会话

POST /api/v1/conversations
Authorization: Bearer <token>
Idempotency-Key: <key>
Content-Type: application/json

请求:

{
  "agent_type": "customer_service"
}

返回 201 Created:

{
  "data": {
    "session_id": "session-uuid",
    "agent_type": "customer_service",
    "portal": "customer_chat",
    "status": "active",
    "clarification_round": 0,
    "created_at": "2026-09-09T08:20:00.000000Z"
  },
  "meta": {"trace_id": "trace-uuid"}
}

服务端根据请求入口确定 portal,通过 AgentFactory 校验 Agent、角色和入口。客户端不得提交 user_id、portal 或澄清轮次。

7.2 查询会话

GET /api/v1/conversations/{session_id}
Authorization: Bearer <token>

返回会话的 session_id、agent_type、portal、status、clarification_round、message_count、last_active_at、started_at 和 ended_at。权限不满足时按资源隐藏规则返回 404。

7.3 查询会话消息

GET /api/v1/conversations/{session_id}/messages?limit=20&cursor=<cursor>
Authorization: Bearer <token>

返回 conversation_message 的对客投影:message_id、role、content、intent、confidence、source_references、created_at 和脱敏后的 tool_calls。不返回内部 Prompt、模型原文、权限判断详情、未脱敏工具参数或其他客户数据。

用户消息的 intent、confidence 和 source_references 按现有流程可以为空;助手消息必须来自已经完成合规和持久化的结果。

7.4 结束会话

POST /api/v1/conversations/{session_id}/closures
Authorization: Bearer <token>
Idempotency-Key: <key>

请求体可为空。服务端条件更新会话状态为 ended,触发记忆提取事件的判定由公共底座完成。已结束会话重复调用返回当前会话状态;已转人工会话不得通过此接口绕过客服状态机。

7.5 创建用户反馈

POST /api/v1/conversation-messages/{message_id}/feedback
Authorization: Bearer <token>
Idempotency-Key: <key>
Content-Type: application/json

请求:

{
  "rating": -1,
  "feedback_type": "inaccurate",
  "feedback_content": "回答没有说明到账条件"
}

rating 只能为 1 或 -1,正文最多 1000 字符。消息必须属于当前用户;同一用户对同一消息只能有一条有效反馈,重复提交相同内容返回原反馈,内容不同返回 409 FEEDBACK_ALREADY_EXISTS。

7.6 用户申请转人工

POST /api/v1/conversations/{session_id}/handover-requests
Authorization: Bearer <token>
Idempotency-Key: <key>
Content-Type: application/json

请求:

{
  "reason_code": "user_requested",
  "reason_detail": "希望人工解释"
}

客户端只能提交 reason_code=user_requested 和有限长度的补充说明,不能提交 priority、assigned_to、置信度、会话摘要或来源引用。返回 202 Accepted:

{
  "data": {
    "handover_id": "ticket-uuid",
    "session_id": "session-uuid",
    "status": "pending",
    "created_at": "2026-09-09T08:35:00.000000Z"
  },
  "meta": {"trace_id": "trace-uuid"}
}

该接口只负责创建通用转人工请求。客服工单分配、接单、解决和关闭由客服领域 Service 处理,不允许 Controller 直接写 svc_handover_ticket。

7.7 查询转人工请求

GET /api/v1/handover-requests/{handover_id}
Authorization: Bearer <token>

客户只能查看自己的 status、创建时间和安全提示;客服坐席可按权限查看队列和分配信息。工单状态 pending -> assigned -> processing -> resolved -> closed,取消规则和字段以 02 §7.2 及客服业务文档为准。

8. 记忆与知识引用接口

8.1 查询记忆画像

GET /api/v1/users/me/memory-profile
GET /api/v1/customers/{customer_id}/memory-profile
Authorization: Bearer <token>

客户只能查询自己的画像;投顾、客服或授权员工必须通过客户归属和数据范围校验。返回经过字段策略过滤的当前画像、有效期、来源摘要和版本,不返回模型原始推理、Prompt、其他客户信息或未解决冲突的内部详情。

记忆提取没有客户端写接口。memory.extraction_requested 由 complete_run() 与最终结果在同一事务写入 Outbox,再由 Worker 调用内部 MemoryService。更正、遗忘和监管删除属于独立隐私流程,本接口不临时复用 memory_conflict。

8.2 解析知识引用

GET /api/v1/knowledge-references/{reference_token}
Authorization: Bearer <token>

reference_token 为服务端签发的短期不透明令牌,绑定用户、知识版本和过期时间。返回允许展示的标题、版本、生效日期、来源机构和摘要;客户端不能通过修改令牌枚举 doc_id 或读取草稿知识。

知识上传、审核、发布、失效和 Milvus 同步由知识业务文档定义。Agent 只能使用工具返回的 SourceReference,不得自行构造引用。

8.3 知识库文档管理

POST   /api/v1/knowledge/upload
GET    /api/v1/knowledge/list
DELETE /api/v1/knowledge/{knowledge_id}
Authorization: Bearer <token>

需求来源:老师《需求文档-修改版》Phase 1 验收第 7 条「知识库管理接口可正常上传/查询/删除文档」与 F1.2。与 §8.2 的 /knowledge-references/{reference_token}(引用解析,K001)是两个不同的资源面:本节是管理面文档生命周期,凭 knowledge:manage 操作,不解析令牌、不按客户归属过滤;K001 是读侧引用解析,凭 knowledge:reference:read,返回可用展示的引用片段。两者不复用同一 prefix 语义。

三个端点均需 knowledge:manage(现只授给管理员角色)。created_by 取自认证上下文,调用方不能指定。三个端点共用 router 级限流;底座没有匿名路径,认证先于限流。

上传

{
  "filename": "理财产品销售管理办法.md",
  "content_base64": "<文档字节的标准 base64>",
  "knowledge_type": "policy"
}
  • filename 只能是文件名本身(不得含路径与分隔符)、长度 ≤ 256。
  • knowledge_type 取 faq / product / policy 之一,由服务端映射集合,调用方不得指定集合名;非法取值返回 422。
  • content_base64 严格解码;非法 base64 返回 422 AGENT_INPUT_INVALID。
  • 老师原文写 multipart/form-data,一期实现为 JSON(理由见下)。

成功 201 返回本次产生的知识行(一份文档切多块即为多行):

{
  "knowledge_ids": [109, 110],
  "filename": "理财产品销售管理办法.md",
  "knowledge_type": "policy",
  "created_by": "9003",
  "chunk_count": 2
}

查询列表

GET /api/v1/knowledge/list?limit=20&offset=0&knowledge_type=policy

limit 默认 20、上限 100;offset ≥ 0;knowledge_type 可选。默认只列未过期行。响应只给 content_preview(前 200 字符)与 content_length,不返回整篇 content_text —— 正文是检索侧素材,管理面列表不得变成"直读知识正文"的旁路:

{
  "items": [
    {
      "knowledge_id": 109,
      "knowledge_type": "policy",
      "title": "### 第一条 目的…",
      "source_file": "理财产品销售管理办法.md",
      "collection": "fin_policy_collection",
      "version": "v1",
      "status": "active",
      "review_status": "published",
      "created_by": 9003,
      "tags": {"chunk_index": 2, "heading_path": ["理财产品销售管理办法"]},
      "content_preview": "### 第一条 目的…",
      "content_length": 457
    }
  ],
  "count": 1
}

删除

DELETE /api/v1/knowledge/{knowledge_id}

删除不改物理行,而是标记 fin_knowledge_meta.status='expired' 并在同一事务投递 knowledge.vector_delete_requested(aggregate_id 与 payload 均为 str(knowledge_id)), 由知识向量 Worker 消费后删除 Milvus 向量;本地文件随后尽力归档,归档失败不回滚已提交的删除(只记日志)。

成功 200:

{
  "knowledge_id": 109,
  "status": "expired",
  "vector_delete_event": "knowledge.vector_delete_requested"
}

不存在的 knowledge_id 返回 404,不静默成功。

两处口径说明(避免误读)

  1. 请求体形状:老师原文为 multipart/form-data,一期用 JSON + content_base64。 理由是底座其余写接口(admin 配置面、conversations、agent-runs)全是 JSON,错误信封(§3.4) 与 ValidationAgentError 的 422 口径都建立在 JSON body 上;一期单独引 multipart 等于新开一条 无测试覆盖的上传失败路径。后续接前端表单时新增一个 multipart 端点复用同一 Service 即可 (Service 只吃 filename + content: bytes,与传输形状无关)。
  2. 删除的 404 错误码:现实现复用 SESSION_NOT_FOUND(底座通用资源不存在码),语义上偏会话面; 若后续要求精确语义,应新增 KNOWLEDGE_NOT_FOUND。本文按现状登记,不做美化。

8.4 公共只读工具索引(Agent 面)

工具不是 HTTP 接口,但它们决定 Agent 能读什么,且必须由 AgentFactory 注入的 ToolExecutor 统一执行(鉴权、数据范围、审计不得绕过)。登记口径: 工具名 / 必需权限 / 只读 / 越权行为。

工具名 必需权限 只读 越权与失败行为
check_suitability suitability:read 是 除管理员外不得查他人风险测评;测评过期/缺失一律拒绝(失败关闭)
query_fund_quote fund:quote:read 是 只读行情,不得改写为成交、委托或持仓语义。超时 15s(EastmoneyAdapterFactory 单代码最坏预算 8.2s × 1.8)
search_knowledge knowledge:reference:read 是 知识检索的正式工具名。集合名由服务端按意图映射,调用方不得指定;维度不符失败关闭。超时 10s
query_knowledge knowledge:query 是 ⚠️ search_knowledge 的别名(bootstrap.py:321-328,同一 handler,为兼容一期发布配置与旧客户端保留),允许角色仅 visitor/customer
query_customer_profile memory:read:self(查他人为 memory:read:customer) 是 越范围按"不存在"处理且不泄露存在性;无当前画像抛错而不返回空画像

⚠️ 本表此前只列 4 个、且把 query_knowledge 当主名(2026-09-14 修正)。 两者都要发布:知识类意图缺 search_knowledge,登录客户与投顾那侧缺权限; 缺 query_knowledge,访客令牌只有 knowledge:query、一问即失败。

其余业务线工具按各自 Agent 白名单注册(不在本公共表内,权限码也不同): 风控 3 个(search_risk_alerts/get_risk_overview/get_alert_evidence,权限 risk:alert:read)、 投顾 6 个(investment-goal:* 等)、NL2SQL 1 个(query_financial_data,权限 financial:nl2sql:read)、 探针 1 个(权限 probe:read)。全部注册点在 app/service/agent/bootstrap.py::get_agent_factory()。

工具的白名单是两段式:代码里的 allowed_tools 是上限,实际可用范围还要与 当前 active 的 config_release 中 namespace=agent_tools、config_key=<agent_type>:<intent> 发布的工具白名单取交集;缺发布配置时交集为空、工具失败关闭。

query_customer_profile 的返回值遵循 §8.1 的字段策略:只投影画像属性白名单 (investor_type / investment_horizon / trading_frequency / preferred_asset_class / risk_tags / customer_tier / behavior_score / total_asset / assessment_valid_until / assessment_expired), 不返回 real_name、birth_date、mobile_masked、trade_account 等 PII; assessment_expired 按当前时间重算,不采信快照里的历史布尔值。

8.5 客服画像候选(Phase 2)

编号说明:本节为客服二期新增,不占用 §8.1–§8.4 既有号段,以避免破坏 AGENTS.md、docs/09、docs/14 对「§8.3 知识库管理三端点」「§8.4 公共只读工具索引」的既有引用。

客服 Agent 不读取或直接修改正式画像。已登录用户明确陈述长期偏好、约束或目标时,系统 异步生成 memory_unit.status='candidate' 候选;访客不会生成候选。候选不进入客服召回, 必须经过用户确认和管理员审核后才能晋升为 active。

GET  /api/v1/users/me/memory-candidates
POST /api/v1/users/me/memory-candidates/{candidate_id}/decisions
GET  /api/v1/admin/customer-profile-candidates
POST /api/v1/admin/customer-profile-candidates/{candidate_id}/reviews

用户确认请求体为 { "decision": "confirmed" | "rejected" },需要 memory:candidate:confirm;确认只将状态改为 verified。管理员审核请求体复用 ReviewPayload,需要管理员角色和 memory:candidate:review;approved 会在事务内 处理同键旧记忆冲突并将候选改为 active,rejected 将其改为 rejected。接口只返回 结构化候选值,不返回对话证据摘录、密码、验证码或其他原始敏感内容。

9. 平台管理面接口

管理面只操作草稿、审核、激活、停用、回滚和归档流程,不提供绕过版本控制的通用 CRUD。所有更新和状态转换都需要 If-Match;创建、审核、激活、回滚和停用需要 Idempotency-Key。

9.1 配置发布

POST /api/v1/admin/config-releases
GET  /api/v1/admin/config-releases
GET  /api/v1/admin/config-releases/{release_id}
POST /api/v1/admin/config-releases/{release_id}/validations
POST /api/v1/admin/config-releases/{release_id}/reviews
POST /api/v1/admin/config-releases/{release_id}/activations
POST /api/v1/admin/config-releases/{release_id}/rollbacks

创建草稿请求至少包含 release_no、title、change_summary。验证接口执行 Schema、权限上限、引用完整性、路由备用端点、Prompt、模板和意图配置校验;未通过验证不得审核或激活。

审核请求:

{
  "decision": "approved",
  "comment": "已完成审核"
}

创建人可以审核自己创建的版本(允许自审,不要求审核人不同于创建人);审核这一状态机节点不可跳过,未提交审核的草稿不能直接审核或激活。自审时 reviewer_id 留空——库约束 chk_config_release_separation(reviewer_id IS NULL OR reviewer_id <> created_by)属于数据库基线,不能把创建人写进 reviewer_id;自审人身份由 interaction_audit 的审核记录承担。他人复核仍照常写入 reviewer_id。激活必须是审核通过的完整批次,且原子切换当前生效版本。回滚不修改历史记录,而是创建新的发布记录并填写 rollback_of_release_id。

9.2 平台配置项

POST /api/v1/admin/config-releases/{release_id}/platform-config-items
GET  /api/v1/admin/config-releases/{release_id}/platform-config-items
PUT  /api/v1/admin/config-releases/{release_id}/platform-config-items/{item_id}

配置项必须按 namespace + item_key 唯一。value_json 必须通过对应 Pydantic Schema 校验,不接受任意权限扩大、模型端点、工具白名单或客户范围参数。

9.3 模型端点

POST /api/v1/admin/model-endpoints
GET  /api/v1/admin/model-endpoints
GET  /api/v1/admin/model-endpoints/{endpoint_id}
PUT  /api/v1/admin/model-endpoints/{endpoint_id}
POST /api/v1/admin/model-endpoints/{endpoint_id}/reviews
POST /api/v1/admin/model-endpoints/{endpoint_id}/activations
POST /api/v1/admin/model-endpoints/{endpoint_id}/disablements

请求只允许提交 endpoint_code、供应商标识、能力、敏感级别、超时、成本和 secret_ref。不得提交或返回 API 明文密钥。健康检查失败可以熔断流量,但不能自动把数据库配置改成 active。

9.4 模型路由

POST /api/v1/admin/config-releases/{release_id}/model-routing-rules
GET  /api/v1/admin/config-releases/{release_id}/model-routing-rules
PUT  /api/v1/admin/config-releases/{release_id}/model-routing-rules/{rule_id}

路由规则请求使用:

{
  "rule_code": "customer_service_answer",
  "agent_type": "customer_service",
  "task_type": "answer_generation",
  "model_policy": "balanced",
  "primary_endpoint_id": "1001",
  "fallbacks": [
    {"endpoint_id": "1002", "fallback_order": 1},
    {"endpoint_id": "1003", "fallback_order": 2}
  ],
  "max_attempts": 3,
  "latency_budget_ms": 15000
}

HTTP 不接受或返回旧的 fallback_endpoint_ids JSON 字段。备用链以 model_routing_fallback 为权威关系表,服务端校验主端点不在备用链、顺序不重复、端点存在且总尝试数不超过 3。

9.5 Prompt、意图和回复模板

POST /api/v1/admin/prompt-templates
GET  /api/v1/admin/prompt-templates
GET  /api/v1/admin/prompt-templates/{prompt_id}

POST /api/v1/admin/agent-intent-configs
GET  /api/v1/admin/agent-intent-configs
PUT  /api/v1/admin/agent-intent-configs/{config_id}

POST /api/v1/admin/reply-templates
GET  /api/v1/admin/reply-templates
PUT  /api/v1/admin/reply-templates/{template_id}

POST /api/v1/admin/negative-word-rules
GET  /api/v1/admin/negative-word-rules
PUT  /api/v1/admin/negative-word-rules/{rule_id}

这些资源使用各自的草稿、审核、激活和归档状态。由于现有表没有统一 release_id 外键,接口不得声称它们与 config_release 自动原子绑定;发布服务必须在激活前校验引用版本一致。

模板和意图配置的同一业务组合最多一个 active 版本,数据库生成列唯一约束是最终保障。已激活版本不可原地编辑。

9.6 审计查询

GET /api/v1/admin/audit-records
Authorization: Bearer <token>

支持 trace_id、run_id、用户、Agent 类型、事件类型、结果、时间范围和游标过滤。接口只读,不提供修改和删除。audit:read-sensitive 才能查看未脱敏详情。

9.7 客服转人工队列(只读)

GET /api/v1/admin/customer-service/handover-tickets?limit=20&cursor={ticket_id}
GET /api/v1/admin/customer-service/handover-tickets/{ticket_no}
Authorization: Bearer <token>

两个接口均要求 admin 或 super_admin 角色和 handover:read 权限。列表仅返回工单号、 会话标识、来源 Agent、优先级、转接原因、状态和时间;详情才追加二次脱敏后的转接原因、 会话摘要、意图置信度和受控知识来源。接口不得返回客户标识、原始会话正文、账户数据、 联系方式、工单分配信息或处理结论。当前仅支持查看,不支持接单、分配、处理、解决或关闭。

10. SSE 与领域事件映射

10.1 SSE 事件

事件名称和载荷以 AGENT-SSE-001 为准:start、tools、delta、replace、done、error。本文只定义传输和发送时机,不重新定义字段。

10.2 Outbox 事件

事件 产生位置 消费者
agent.run_requested 创建运行受理事务 Agent Worker
agent.run_cancel_requested 取消状态事务 Agent Worker
conversation.completed complete_run() 最终事务 会话投影、通知
conversation.transfer_requested complete_run() 或客户转人工申请事务 客服转人工消费者;写入 handover.queue_ready 审计,不改变工单 pending 状态
memory.extraction_requested complete_run() 最终事务 记忆提取 Worker
agent.run_failed 失败状态事务 监控和告警
config.release_activated 配置激活事务 缓存失效、实例刷新
config.release_rolled_back 回滚激活事务 缓存失效、监控
model.endpoint_disabled 端点停用事务 模型路由缓存

Outbox 消费者按 event_id 幂等。失败事件保留并重试,超过阈值进入死信状态并告警;不能删除未完成事件。

11. 审计分界

11.1 必须写 interaction_audit

  1. 委托、成交、资金、持仓和风险预警处置等受监管业务状态变化。
  2. 配置发布、审核、激活、停用和回滚。
  3. 越权、适当性、跨客户和其他权限决策拒绝。
  4. 工单全生命周期、方案审核和人工处置。
  5. Agent 运行的输入摘要、结果、模型/Prompt 版本、合规动作、降级原因和耗时。

request_idempotency 的完成状态随 complete_run() 与业务结果同一事务写审计。

11.2 只写运行日志或指标

心跳和租约续期、幂等抢占重试、缓存读写与失效、SSE 游标与断流、健康检查和普通限流拒绝只写运行日志或指标。若限流同时判定为攻击、越权或安全策略命中,则另写安全审计。

12. 业务接口索引与 Agent 边界

公共接口文档只登记入口、归属和 Agent 禁止边界,不预先定义领域载荷和状态机。

业务域 接口入口 归属文档 Agent 边界
客服工单 /api/v1/customer-service/handover-tickets/** 客服业务文档 可生成摘要和转人工请求,不分配、接单、解决或关闭工单
投顾方案 /api/v1/advisor/**(编号见 §19 的 AD 段与 A041–A046) 本文 §19 只生成分析草案,不代替投顾审核发布
场内模拟交易 /api/v1/sim-orders/** 交易业务文档 只读查询,不创建、确认或撤销委托
风控扫描 /api/v1/risk/** 风控业务文档 可解释规则结果,不启动人工处置
风险预警 /api/v1/risk/** 风控业务文档 只读分析,不确认、升级或关闭预警
场外基金运营 /api/v1/offsite-fund/**、/api(operation_router) docs/28、场外工作流文档 已落地(2026-09-14 更正:原写"不属于当前系统"已过期)。独立业务表,不读写场内交易表;沿用旧式信封 {code,message,data} 与 page/page_size 分页

风控模块落地时把扫描、预警、证据、通知和日报收在同一个 Controller 下,入口为 /api/v1/risk/**(早期规划写作 /risk-scans/**、/risk-alerts/**,以本节的实际入口为准)。 具体端点清单、权限与字段映射由风控业务文档登记,见第 19 节末尾。

B 类业务写接口必须复用 JWT、响应信封、错误码、幂等、统一 RequestContext、事务和审计规则。Controller 只路由、校验、映射和调用 Service;禁止直接访问 Model。

13. A 类和 B 类扩展

13.1 A 类:Agent 能力扩展

组员新增 Agent、意图或 handle() 分支时,继续使用 /api/v1/agent-runs,不新增 Controller。允许修改 Agent 实现、启动注册和测试数据;不得复制或覆盖 BaseAgent 的鉴权、记忆、模型、工具、合规、审计、SSE 和事件逻辑。

13.2 B 类:业务 API 扩展

新增模拟下单、方案审核或预警处置等领域接口时,新增该业务域的 Controller、Application Service、DTO、Repository 和测试。B 类接口必须登记:资源路径、权限码、幂等范围、事务边界、审计类别、归属文档和 Agent 只读边界。

13.3 组员 Agent 交付清单

  1. <agent_type>_agent.py:继承 BaseAgent,只实现 handle()。
  2. AgentDefinition:角色、入口、意图、工具、合规策略、模型策略和记忆视图。
  3. 启动注册新增一行,不修改工厂实现。
  4. 正常、低置信、工具失败、模型失败和越权用例。
  5. 每个来源引用必须来自工具或知识服务返回值。
  6. 不覆盖 run_stream()、执行模板、记忆沉淀或事件广播方法。

14. 内部契约索引

本文不复制以下签名。实现和测试必须引用指定权威源:

契约 权威源
AgentRequest、RequestContext、配置 DTO 01 §5.2,AGENT-REQ-001
IntentResult、SourceReference、AgentResult 01 §5.3,AGENT-RESULT-001
ModelGateway、MemoryService、ComplianceService、AuditService 01 §5.4,AGENT-SERVICE-001
AgentError 分类 01 §5.5,AGENT-ERROR-001
BaseAgent 执行顺序和 complete_run() 01 §6,AGENT-FLOW-001
DomainEvent 和 Outbox 01 §5.3/§6.2、02 §8.2,AGENT-EVENT-001
SSE 事件载荷 01 §12,AGENT-SSE-001
表和字段 00、02,DB-BASE-001

HTTP 层使用 DTO Mapper 投影内部对象。Controller 和 View 不得直接返回 ORM Model 或内部异常对象。

15. 运维与安全接口

GET /internal/health/live
GET /internal/health/ready
GET /internal/metrics

这些接口不使用业务 JSON 信封,不暴露数据库地址、模型密钥、Token、完整客户资料或异常堆栈,只允许内网和监控系统访问。JWT 签发、刷新、注销由统一身份认证模块负责,Agent 平台不重复实现。

实现现状(2026-09-11 更新):上面这句原本是"平台不做签发"的依据,实际落地时确认了 平台必须有一个登录入口 —— 否则客户 / 员工 / 管理员三种身份无法区分(各 Agent 的 allowed_roles 早就分开了,缺的只是"怎么证明你是谁")。因此平台现在提供 POST /api/v1/auth/tokens(账号密码换访问令牌,见 §19 的 A034), 这是本文档 §11 那句的唯一例外。

边界仍然守住:平台只做登录,刷新与注销仍归统一身份认证模块 (app/core/security.py 已留好 RevocationStore 协议,接上 Redis 即可)。 令牌里只放 sub,角色 / 权限 / 数据范围一律由 IdentityService 每次请求查库解析, 所以权限变更立即生效,不受令牌有效期影响。

16. 验收与契约测试

16.1 HTTP 通用测试

  • 所有 JSON 接口使用统一成功或错误信封。
  • JWT 缺失、过期、签发方错误、受众错误和吊销均被拒绝。
  • 同键同正文返回原资源,同键不同正文返回 409。
  • 跨客户查询返回 404,且响应不泄露资源是否存在。
  • 分页游标绑定过滤条件,非法游标返回 400 INVALID_CURSOR。
  • Controller 不直接导入 ORM、数据库客户端、Agent 实现或消息客户端。

16.2 Agent 运行测试

  • 受理事务同时创建用户消息、幂等记录、agent_run 和 agent.run_requested Outbox。
  • Worker 租约过期后使用同一 run_id 安全接管。
  • 最终消息、审计、幂等完成状态和领域事件在同一事务提交。
  • memory.extraction_requested 不得由请求线程在提交后直接调用。
  • SSE 仅在最终事务成功后发送 tools、delta、replace 和 done。
  • 断流重连可以根据 run_id 获取完整结果;MVP 不声称支持事件级续传。

16.3 管理面测试

  • 创建人可以审核自己创建的配置;审核状态机不可跳过。
  • 未通过校验、审核或版本检查的配置不能激活。
  • 已激活版本不可原地编辑。
  • 模板和意图组合最多一个 active 版本。
  • 备用端点必须存在、顺序唯一,且主端点不能出现在备用链。
  • 激活、回滚、停用和权限拒绝都写审计。

16.4 静态检查

检查 OpenAPI 与本文路径、方法和必填 Header 一致
检查每个接口都有权限码、幂等说明、错误码和审计分类
检查 Controller 不直接访问 Model
检查 Agent 子类未覆盖公共执行模板
检查契约编号唯一且引用存在

17. 实施顺序

  1. 创建 agent_run 表及 Alembic 迁移,核对 00 基线和 02 新表,不修改已有表名和已有字段定义。
  2. 实现 HTTP DTO、统一信封、JWT 依赖、错误映射、Trace 中间件和游标工具。
  3. 实现 AgentRunApplicationService、幂等、运行租约和 RunDispatchPort。
  4. 将 Agent 执行从 HTTP/SSE 中解耦为 Worker 可调用的 AgentExecutor。
  5. 实现 JSON 状态查询和结果级恢复 SSE。
  6. 实现会话、消息、反馈和公共转人工入口。
  7. 实现管理面配置、模型路由、Prompt、意图、模板和禁止表达接口。
  8. 生成 OpenAPI 并执行契约测试、安全测试、故障恢复和迁移演练。

18. 完成标准

  • 所有公共 HTTP 接口具有稳定路径、请求、响应、权限、错误、幂等和审计定义。
  • run_id 可在服务重启后查询、恢复和获取完整结果。
  • JSON 和 SSE 不共享传输实现,但共享运行查询投影和权限检查。
  • A 类 Agent 不需要修改公共 Controller、Factory、BaseAgent 或 SSE View。
  • B 类业务接口遵守统一 MVC+S 骨架和 Agent 只读边界。
  • 内部契约和数据库结构没有在本文形成第二权威定义。
  • 数据库迁移不修改已有表名和已有字段定义(当前现库 90 张表 = 89 张业务表 + alembic_version, 分域为场内 51 + 场外/推广 17 + 投顾 21。 本行原写"49 张"→ 2026-09-10 更正为"52 张"→ 2026-09-14 按 AGENTS.md 口径更正为 89 张业务表。 表的数量会随新增表变化,因此这里只保留"不修改已有表名与字段定义"这一硬约束; 数量口径以 python tools/audit_schema.py 的实测输出为准)。

19. 接口总目录

下表是 v1 接口的实现清单。除特别标注外,成功响应均使用第 3.3 节信封,错误响应均使用第 3.4 节信封。

编号 方法与路径 权限 幂等 成功状态 审计
R001 POST /api/v1/agent-runs agent:run 必须 202 Agent 运行
R002 GET /api/v1/agent-runs/{run_id} 资源所有者/数据范围 否 200 否
R003 GET /api/v1/agent-runs/{run_id}/events 资源所有者/数据范围 否 SSE 否
R004 POST /api/v1/agent-runs/{run_id}/cancellations agent:cancel 必须 202 取消操作
C001 POST /api/v1/conversations conversation:create 必须 201 会话创建
C002 GET /api/v1/conversations/{session_id} 会话所有者/数据范围 否 200 否
C003 GET /api/v1/conversations/{session_id}/messages 会话所有者/数据范围 否 200 否
C004 POST /api/v1/conversations/{session_id}/closures conversation:close 必须 200 会话结束
C005 POST /api/v1/conversations/{session_id}/handover-requests handover:create 必须 202 转人工请求
C006 GET /api/v1/handover-requests/{handover_id} 客户/客服数据范围 否 200 否
C007 POST /api/v1/conversation-messages/{message_id}/feedback conversation:feedback 必须 201 反馈创建
M001 GET /api/v1/users/me/memory-profile memory:read:self 否 200 敏感访问
M002 GET /api/v1/customers/{customer_id}/memory-profile memory:read:customer 否 200 敏感访问
M003 GET /api/v1/users/me/memory-candidates memory:read:self 否 200 候选查询
M004 POST /api/v1/users/me/memory-candidates/{candidate_id}/decisions memory:candidate:confirm 必须 200 用户确认/拒绝
A039 GET /api/v1/admin/customer-profile-candidates memory:candidate:review 否 200 候选审核列表
A040 POST /api/v1/admin/customer-profile-candidates/{candidate_id}/reviews memory:candidate:review 必须 200 候选审核
A041 POST /api/v1/admin/advisor/asset-allocation-backtests asset-allocation:backtest(+admin) 必须 201 配置回测
A042 GET /api/v1/admin/advisor/profile-tags profile-governance:read(+admin) 否 200 敏感访问
A043 GET /api/v1/admin/advisor/profile-drift-reviews profile-governance:read(+admin) 否 200 敏感访问
A044 POST /api/v1/admin/advisor/profile-drift-reviews/{review_id}/reviews profile-governance:review(+admin) 必须 200 画像漂移复核
A045 POST /api/v1/admin/advisor/recommendations/{content_id}/reviews product-recommendation:review(+admin) 必须 200 推荐方案审核
A046 POST /api/v1/admin/advisor/recommendations/{content_id}/publications product-recommendation:publish(+admin) 必须 200 推荐方案发布
A047 GET /api/v1/admin/advisor/pending-contents product-recommendation:review(+admin) 否 200 否
A048 GET /api/v1/admin/config-releases/{release_id}/platform-config-items/{item_id} config:read(+admin) 否 200 否
A049 GET /api/v1/admin/config-releases/{release_id}/model-routing-rules/{rule_id} config:read(+admin) 否 200 否
AD001 POST /api/v1/advisor/investment-goals investment-goal:write:self / :customer 必须 201 投资目标创建
AD002 GET /api/v1/advisor/investment-goals/current investment-goal:read:self 否 200 否
AD003 GET /api/v1/advisor/customers/{customer_id}/investment-goals/current investment-goal:read:self / :customer 否 200 否
AD004 POST /api/v1/advisor/investment-goals/{goal_no}/confirmations investment-goal:confirm:self / :customer 必须 200 目标确认
AD005 GET /api/v1/advisor/investment-goals/{goal_no}/goal-book investment-goal:read:self / :customer 否 200 否
AD006 POST /api/v1/advisor/investment-goals/{goal_no}/goal-book/reviews investment-goal:review(+admin) 必须 200 方案书审核
AD007 POST /api/v1/advisor/investment-goals/{goal_no}/goal-book/publications investment-goal:publish(+admin) 必须 200 方案书发布
AD008 POST /api/v1/advisor/portfolio-analysis portfolio-analysis:read:self 否 200 否
AD009 POST /api/v1/advisor/asset-allocation asset-allocation:generate:self 否 200 否
AD010 POST /api/v1/advisor/recommendations product-recommendation:generate:self 必须 200 否
AD011 GET /api/v1/advisor/recommendations/published product-recommendation:read:self 否 200 否
K001 GET /api/v1/knowledge-references/{reference_token} knowledge:reference:read 否 200 否
K002 POST /api/v1/knowledge/upload knowledge:manage 否 201 知识文档变更
K003 GET /api/v1/knowledge/list knowledge:manage 否 200 否
K004 DELETE /api/v1/knowledge/{knowledge_id} knowledge:manage 否 200 知识文档失效
A001 POST /api/v1/admin/config-releases config:write 必须 201 配置草稿
A002 GET /api/v1/admin/config-releases config:read 否 200 否
A003 GET /api/v1/admin/config-releases/{release_id} config:read 否 200 否
A004 POST /api/v1/admin/config-releases/{release_id}/validations config:write 必须 200 配置校验
A005 POST /api/v1/admin/config-releases/{release_id}/reviews config:review 必须 200 配置审核
A006 POST /api/v1/admin/config-releases/{release_id}/activations config:activate 必须 200 配置激活
A007 POST /api/v1/admin/config-releases/{release_id}/rollbacks config:activate 必须 201 配置回滚
A008 POST /api/v1/admin/config-releases/{release_id}/platform-config-items config:write 必须 201 配置变更
A009 GET /api/v1/admin/config-releases/{release_id}/platform-config-items config:read 否 200 否
A010 PUT /api/v1/admin/config-releases/{release_id}/platform-config-items/{item_id} config:write 必须 200 配置变更
A011 POST /api/v1/admin/model-endpoints model-endpoint:manage 必须 201 端点创建
A012 GET /api/v1/admin/model-endpoints config:read 否 200 否
A013 GET /api/v1/admin/model-endpoints/{endpoint_id} config:read 否 200 否
A014 PUT /api/v1/admin/model-endpoints/{endpoint_id} model-endpoint:manage 必须 200 端点变更
A015 POST /api/v1/admin/model-endpoints/{endpoint_id}/reviews config:review 必须 200 端点审核
A016 POST /api/v1/admin/model-endpoints/{endpoint_id}/activations model-endpoint:manage 必须 200 端点激活
A017 POST /api/v1/admin/model-endpoints/{endpoint_id}/disablements model-endpoint:manage 必须 200 端点停用
A018 POST /api/v1/admin/config-releases/{release_id}/model-routing-rules config:write 必须 201 路由变更
A019 GET /api/v1/admin/config-releases/{release_id}/model-routing-rules config:read 否 200 否
A020 PUT /api/v1/admin/config-releases/{release_id}/model-routing-rules/{rule_id} config:write 必须 200 路由变更
A021 POST /api/v1/admin/prompt-templates config:write 必须 201 Prompt 变更
A022 GET /api/v1/admin/prompt-templates config:read 否 200 否
A023 GET /api/v1/admin/prompt-templates/{prompt_id} config:read 否 200 否
A024 POST /api/v1/admin/agent-intent-configs config:write 必须 201 意图配置
A025 GET /api/v1/admin/agent-intent-configs config:read 否 200 否
A026 PUT /api/v1/admin/agent-intent-configs/{config_id} config:write 必须 200 意图配置
A027 POST /api/v1/admin/reply-templates config:write 必须 201 回复模板
A028 GET /api/v1/admin/reply-templates config:read 否 200 否
A029 PUT /api/v1/admin/reply-templates/{template_id} config:write 必须 200 回复模板
A030 POST /api/v1/admin/negative-word-rules config:write 必须 201 禁止表达
A031 GET /api/v1/admin/negative-word-rules config:read 否 200 否
A032 PUT /api/v1/admin/negative-word-rules/{rule_id} config:write 必须 200 禁止表达
A033 GET /api/v1/admin/audit-records audit:read 否 200 否
A034 POST /api/v1/auth/tokens 公开(登录前无身份) 否 200 登录成功/失败
A035 GET /api/v1/admin/roles audit:read 否 200 否
A036 GET /api/v1/admin/roles/{role_code} audit:read 否 200 否
A037 GET /api/v1/admin/roles/{role_code}/permissions audit:read 否 200 否
A038 GET /api/v1/admin/users/{user_id}/roles audit:read 否 200 否
O001 GET /internal/health/live 内网 否 200 否
O002 GET /internal/health/ready 内网 否 200/503 否
O003 GET /internal/metrics 监控系统 否 200 否
T001 GET /api/v1/users/me/account/dashboard account:read:self(已登录) 否 200 账户看板(汇总账户/资金/持仓/盈亏)
T002 POST /api/v1/users/me/orders trade:order:create(已登录) 必须 201 委托提交(市价立即全额成交)
T003 GET /api/v1/users/me/orders trade:order:read(已登录) 否 200 委托列表(按 id 倒序游标分页)
T004 GET /api/v1/users/me/orders/{order_no} trade:order:read(资源所有者) 否 200 委托详情
T005 POST /api/v1/users/me/orders/{order_no}/cancellations trade:order:cancel(资源所有者) 必须 200 撤单(首版仅"已接受/已部分成交"可撤)
T006 GET /api/v1/users/me/holdings holding:read:self(已登录) 否 200 持仓列表(含市值/盈亏/当日盈亏)
T007 GET /api/v1/users/me/transactions trade:txn:read(已登录) 否 200 成交记录列表
T008 GET /api/v1/users/me/transactions/{txn_no} trade:txn:read(资源所有者) 否 200 成交详情
T009 GET /api/v1/users/me/cash-ledger account:read:self(已登录) 否 200 资金账本(按 id 倒序游标分页)
P001 GET /api/v1/products 仅要求有效令牌(访客令牌即可,不校验权限码) 否 200 否
P002 GET /api/v1/products/{product_code}/nav-history 同上;days 取 1–365,默认 90 否 200 否

19.1 补充接口段(2026-09-14 补登)

⚠️ 以下几段此前完全未登记在 §19,但已由 app/main.py 实际挂载(include_router 共 21 个)。 使用者按本文档找场外/访客令牌/入驻接口时会找不到,故在此补登。各段的请求/响应字段级契约 以对应 Controller 与 Service 为准(本节只登记路由与鉴权口径):

段 前缀 挂载来源 权限口径
场外基金 /api/v1/offsite-fund offsite_fund.py 的 router **any-of(非空交集)**语义,不走 AuthorizationService.require:四种组合 ("offsite:read","offsite:write")、("offsite:write",)、("offsite:write","offsite:confirm")、("offsite:notify","offsite:write")
场外运营 /api(同一文件的 operation_router) offsite_fund.py 的 operation_router 同上
访客令牌 /api/v1/visitor-tokens visitor_tokens.py 签发访客令牌(访客页面用)
客户入驻 /api/v1/onboarding onboarding.py 注册/开户流程
推广材料 /api/v1/promotion-materials promotion_material.py 权限码 promotion:read/write/review/deliver(9047 段同批)
账户与交易 /api/v1/users/me trading.py 的 router 见上表 T001–T009

场外契约有两条与主站不同,容易踩(详见 docs/28 与 SRS):

  1. 场外全域沿用旧式信封 {code,message,data}(成功 = code === 0), 不是 §3.3 的新信封;
  2. 场外用 page/page_size 分页,不是 cursor 分页。

风控段的编号是 RK001–RK015(见 common/api-client.js), 其中 RK013–RK015 在 api-client.js 内已自述"未登记进本文 §19"——属已知缺口,待补。 §19 的 A 段编号当前到 A049(api-client.js 亦为 A001–A049),后续新增请顺延。

T001 – T009 的四点说明:

  • 首版只支持 price_type="market" 市价委托(docs/00 §6.6 定义),系统立即全额成交, 委托状态直接落到 已成交;因此 T005 撤单首版对任何在场委托都返回 ORDER_NOT_CANCELLABLE(409),保留接口作为后续限价/部分成交开启的入口。
  • 价格来源仅复用底座 FundQuoteService 的公共行情快照 (fin_market_price 最新交易日,quote_source = eastmoney_demo_seed); Service 层不做行情二次封装,从而满足 AGENTS.md 第 2 条 Agent/Service 不直接命中行情 API。
  • 数据库零变更:T 段所用的 10 张 fin_* 表均由 docs/00 定义;本批 PR 不重命名/删除/修改列类型 与可空性,与 AGENTS.md 第 1 条一致。
  • 持仓比例上限在 T002 买入路径强制校验 (当前持仓 + 本次拟成交)/ total_fund_shares * 100 <= single_investor_max_holding_ratio; fin_market_price 缺失或过期则拒绝买入(docs/00 §6.6 红线)。

A034 – A038 的两点说明:

  • POST /api/v1/auth/tokens 是平台内唯一的登录入口(§11 已注明这是"平台不重复实现 签发"的唯一例外;刷新与注销仍归统一身份认证模块)。
  • A035 – A038 是 RBAC 的只读查询,供管理员回答"谁能访问什么""这个人为什么 403"。 它们复用 audit:read 而不新增 rbac:read:这份清单本身就是审计材料,且复用是 零数据改动、立刻可用(新增权限码得先改 sys_permission,而它目前由 seed_test_rbac.py 以 DELETE 重建语义管理)。 权限变更(提权 / 降权)尚无接口 —— 那条写路径必须带三条红线 (审计留痕、禁止自我提权、保护内置角色),需要单独评审,不是遗漏。

业务域接口 /customer-service/handover-tickets/**、/advisory-plans/**、/sim-orders/**、/risk-scans/** 和 /risk-alerts/** 的具体方法、请求体、领域状态机和错误码分别由对应业务文档登记;它们仍必须遵守本文第 3-5、11 和 12 节。

A048 / A049 为什么必须存在:platform-config-items 与 model-routing-rules 的更新端点要求 If-Match,校验的是该行内容的 digest;而这两个资源此前没有详情端点, 列表的 meta 也不带 etag —— 客户端无从取得当前 digest,首次编辑必然 409 RESOURCE_VERSION_CONFLICT。乐观并发在"读不到版本"的前提下等于死锁。 补上详情端点后,客户端 GET 详情(响应头 ETag + meta.etag)再 PUT 即可。

⚠️ 判据:凡接受 If-Match 的资源,必须同时提供能返回该 etag 的读取路径 —— 这是本次由前端等价测试(照接口逐个调用核对)才暴露出来的,纯看代码不容易发现。

AD 段(投顾自用)与 A041–A047(投顾治理)的六点说明:

这批端点原先只存在于代码中,§19 一条都没登记(2026-09-13 补登)。当时 §12 写的 入口是 /api/v1/advisory-plans/**,与实际路径 /api/v1/advisor/** 不符, 也已一并修正。门禁脚本 check_docs_endpoint_ids.py 只校验 §19 内部编号唯一性, **查不出"代码里有端点、文档里没登记"**这类缺口 —— 新增端点时请按 §20 主动登记。

  • 为什么新开 AD 号段:这批端点落在 /api/v1/advisor/**,与 A 段的 /api/v1/admin/** 是两个不同的权限面(A 段是管理面,AD 段是投顾自用)。 混在一个号段里,"这条到底是投顾能调还是只有管理员能调"就得逐条去读权限列。
  • investment-goal 的两套权限码:investment-goal:<action>:self(本人)与 investment-goal:<action>:customer(名下客户),由 InvestmentGoalService._assert_customer_access 按 customer_id 是否等于 context.user_id 选择。:customer 那一支还要求数据范围是 all,或者 own_customers 且该客户确实在 sys_customer_assignment 里 —— 否则返回 404 客户不可访问(注意是 404 不是 403:不向调用方泄露"该客户存在但你没权看")。
  • AD006 / AD007 虽在投顾路径下,却要求 admin:方案书的审核与发布是管理员的 复核动作(review_book / publish_book 都带 admin=True),投顾本人发不出来。 这是有意的复核环节,不是遗漏。
  • enforce_advisor_rollout 是额外前置:AD 段每一条都挂了投顾灰度开关, 未放行时在鉴权之后、业务逻辑之前就被拦下。
  • 幂等:AD001 / AD004 / AD006 / AD007 / AD010 与 A041 / A044 / A045 / A046 接受 Idempotency-Key;AD008 / AD009(组合分析、资产配置)没有幂等头 —— 它们是纯分析入口,不落业务单据。
  • 失败口径:目标或方案书不存在 → 404;状态不允许(重复确认、未审核就发布) → 409。⚠️ 409 目前一律复用 RUN_NOT_CANCELLABLE 这个码(见 §3.6), 所以"投资目标不能确认"会报出字面像"运行不可取消"的码,属于已知的文档缺口。

P001(公开产品列表)的四点说明:

  • 鉴权口径:要求令牌但不校验权限码。 访客令牌的角色是 visitor、不带任何权限 (见 §4.2 与 app/core/security.py),因此这里不能用权限码把关,否则访客永远 401。 这与 /api/v1/agent-runs、/api/v1/conversations 面向访客的做法一致 —— 产品信息本身是公开信息,要求令牌只为复用统一入口、限流与追踪,不是为了授权。 数据面只暴露 fin_product(status='上市')与 fin_market_price 的最新一行, 不含任何账户、持仓或客户字段。
  • 载荷:data.products[] + data.count。产品字段与 fin_product 同名 (product_code/product_name/exchange_code/product_category/risk_level/ fund_manager/current_nav/current_nav_at/lot_size/price_tick/min_amount/ management_fee_rate/custodian_fee_rate/status), 另加 latest_close/latest_trade_date/quote_source(来自 fin_market_price) 与 change_pct。金额与费率一律为字符串(与既有接口口径一致)。
  • change_pct 可能为 null,调用方必须显示"暂无"而不得当成 0。 当日涨跌幅需要两个交易日的收盘价,而行情可能只同步过一天。 把 null 读成 0 等于对客户说"今天平盘",那是编出来的结论。
  • 首页推荐位、产品列表页、产品详情页共用本端点;产品详情页的净值走势图另走 P002。 此前这三页读的是前端手写的 app/static/portal/common/mock-data.js (8 只演示数据,其中 6 只不在 fin_product 里),该文件已随本次接入删除。

P002(历史净值序列)的三点说明:

  • 用途:产品详情页的净值走势图。数据来自 fin_nav_history, 由 tools/sync_nav_history.py 从东方财富历史净值接口 (api.fund.eastmoney.com/f10/lsjz)同步 —— 注意该域名与行情域名不同: 东财的 push2 / push2his 在本环境连接被拒,所以历史走势走净值这条路。
  • 表为空时返回 count=0 与空数组,不是错误:调用方据此显示"尚未接入", 而不得回退到编造的曲线。此前详情页那条线是演示数据里 12 个假点位, 走势图最容易被当成真实业绩。
  • 不校验基金归属:fin_product 里有非南方基金的产品(510300 是华泰柏瑞的), 所以这里不套 hq.py 的南方基金白名单,只按 product_code 取数。 产品不存在或未上市 → 404。

20. 变更流程

任何新增或修改接口必须同时更新:

  1. 本文对应章节和第 19 节目录。
  2. OpenAPI 机器可读定义。
  3. 权限码、审计类别、幂等说明和错误码清单。
  4. 请求、响应、越权、重复提交、故障恢复和安全测试。
  5. 对应业务文档的入口索引;业务载荷不得复制到公共文档。

涉及内部契约时,先修改权威源文档,再更新本文索引。禁止只修改 OpenAPI 或只修改某个业务文档造成第二权威源。