Files
group_fqcd_jr/docs/05-接口文档.md
wangjianlong_0626 4766e3bd98 feat(benefit): 客户权益功能(T010)+ 修投顾迁移契约里写死 head 的脆弱断言
## 1. 新增客户权益(用户端)

`GET /api/v1/users/me/entitlements`(T010,权限 `benefit:read:self`):

- **层级**由 `fin_customer_profile.total_asset` **实时判定**
  (门槛来自 `knowledge/product/高净值客户服务规范.md`:
  金卡 50 万 / 白金 200 万 / 钻石 600 万 / 私行 1000 万;低于 50 万为普通客户);
- **权益按层级累积展开**(文档原文"含全部下级权益,新增以下"):
  金卡 9 条 / 白金 20 / 钻石 33 / 私行 54,各档已逐档实测;
- 返回**升级提示**(`next_tier`:下一层级与门槛),前端可直接渲染"再投 X 元升级"。

### 新增表 `fin_customer_benefit`(1 张)

层级 → 权益目录,54 条种子数据(`tools/seed_customer_benefits.py`,按 `benefit_code` 幂等)。

**基线合规证明**(规则 1/3/4):只新增这一张表;**未**重命名/删除任何已有表;
**未**重命名/删除/复用任何已有字段,**未**改任何已有字段的类型、可空性或业务含义;
未改 `docs/00`。
复核:`tools/audit_schema.py` → `90 business tables, no missing or unexpected tables`。

### 两条设计取舍

1. **不落"某客户享有哪些权益"**:层级可算,权益由层级推出,两者都不落库。
   与 `docs/00` L159(不保留 `net_worth_flag`,因为可算)同一取向。
2. **权益只存各层新增条目**,累积由服务层 `tier_chain()` 展开 ——
   否则改一条权益要改四处,漏一处就出现"白金没有金卡权益"。

### 数据来源与一处刻意省略

逐条照抄知识文档,不新增文档里没有的权益。**私行那条
「7×24小时私人银行专线:400-XXX-XXXX 转 8」不写号码** ——
文档里是占位符,而对客号码的唯一来源是 `customer_service_rules.CONTACT_PHONE`
(本线此前修过"同一客服给客户两个不同号码"的缺陷)。把占位符抄进库等于再造一份假号码。

## 2. 修投顾迁移契约里写死的断言

`tests/unit/test_advisor_migration_contract.py` 原先断言

```python
assert script.get_heads()[0] == "20260911_merge_adv_risk_heads"
```

那是"投顾迁移刚加完那一刻"的快照 —— 本 PR 一新增迁移(`20260912_customer_benefit`)
它就变红,**而红的原因与投顾链的对错无关**:断言测到的是时间,不是契约。

原意是"投顾链接在这条主链上、没另起分支"。改为断言**投顾链尾是当前 head 的祖先**
(链尾从 `ADVISOR_FILES[-1]` 派生,不写死),既保住原意又不受后续迁移影响。
`len(script.get_heads()) == 1`(链不分叉)与"投顾文件首尾相接"两条原样保留。

## 3. 顺带发现的既有缺口(**不在本次改动范围**)

`app/api/controllers/trading.py` 的 **T001–T009 未调用 `AuthorizationService.require`**:
`docs/05` §19 为它们登记了权限码(`account:read:self` / `trade:order:*` / `holding:read:self`),
但代码只做认证 + 开户测评门槛,**没有执行 RBAC 权限检查**。
对照:仓库里 **26 个 service** 都调了 `require`,`trade_service` 不在其中。

本线的 T010 **按正确做法实现**:`CustomerBenefitService.entitlements_for` 先鉴权再读数据,
且**鉴权在读取客户资产之前**(有测试断言"拒绝时未查库")。
T001–T009 如何补,需架构师定口径后另行处理。

## 4. 文档

- 新增 `docs/41-客户权益功能说明.md`:表登记 + 基线合规证明 + 分层口径 + 累积规则 +
  数据来源 + 权限 + 与仪表盘的关系 + 上述缺口
- `docs/05` §19 登记 T010,并**单独注明它引入了新表**(避免被误读为
  "T 段数据库零变更"的一部分)
- `AGENTS.md` 表数 89 → **90** 张业务表

## 验证

- `pytest tests/unit/service/test_customer_benefit_service.py` → **20 passed**
  (含边界:499999.99 不是金卡、500000 整是金卡、1000 万整是私行;累积条数;升级提示;
  鉴权先于读数据)
- 全量 `pytest tests` → `2 failed, 1469 passed, 1 skipped`
  (2 个失败为既有环境项:httpx 把中文序列化成 `\uXXXX`,非本次引入)
- `ruff check app tests tools alembic` → `All checks passed`
- `mypy app` → **0 错 / 252 文件**
- 真机:`GET /users/me/entitlements` → `200`;各档分层与累积条数逐档实测通过
- `audit_schema.py` → 90 张业务表无缺失/意外;文档守卫 55 份无编号冲突;
  端点编号无重复;RBAC 种子一致性通过
2026-09-12 17:24:37 +08:00

1216 lines
57 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 依赖边界
```text
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 成功信封
单资源:
```json
{
"data": {},
"meta": {
"trace_id": "trace-uuid"
}
}
```
列表资源:
```json
{
"data": [],
"meta": {
"trace_id": "trace-uuid",
"next_cursor": "opaque-cursor",
"has_more": true
}
}
```
业务接口不得增加其他顶层字段。SSE、文件下载和运维健康检查不使用业务 JSON 信封。
### 3.4 错误信封
```json
{
"error": {
"code": "IDEMPOTENCY_CONFLICT",
"message": "幂等键已用于不同请求",
"retryable": false,
"field_errors": []
},
"meta": {
"trace_id": "trace-uuid"
}
}
```
`field_errors` 元素格式为:
```json
{
"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 要求
```http
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 运行状态
```text
queued -> running -> succeeded
-> failed
queued/running -> cancel_requested -> cancelled
```
- `queued`:受理事务已提交,等待 Worker。
- `running`:Worker 持有有效租约并正在执行。
- `cancel_requested`:已收到取消请求,等待 Worker 到达安全停止点。
- `succeeded`:最终消息、审计、幂等完成状态和 Outbox 已原子提交。
- `failed`:不可恢复错误及审计已经提交。
- `cancelled`:在最终事务开始前成功取消。
### 6.2 创建运行
```http
POST /api/v1/agent-runs
Authorization: Bearer <token>
Idempotency-Key: <key>
Content-Type: application/json
```
请求:
```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`:
```json
{
"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 查询运行
```http
GET /api/v1/agent-runs/{run_id}
Authorization: Bearer <token>
```
返回 `200 OK`:
```json
{
"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 订阅运行结果
```http
GET /api/v1/agent-runs/{run_id}/events
Authorization: Bearer <token>
Accept: text/event-stream
```
成功响应 Header:
```http
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache, no-transform
X-Accel-Buffering: no
X-Trace-ID: trace-uuid
```
传输顺序:
```text
运行未完成:start -> 注释心跳 -> 等待
正常完成:start -> tools(可选) -> delta(一个或多个) -> done -> 关闭
安全替换:start -> replace -> done -> 关闭
不可恢复:start -> error -> 关闭
```
帧示例:
```text
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 取消运行
```http
POST /api/v1/agent-runs/{run_id}/cancellations
Authorization: Bearer <token>
Idempotency-Key: <key>
```
请求:
```json
{
"reason": "user_cancelled"
}
```
返回 `202 Accepted`:
```json
{
"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 创建会话
```http
POST /api/v1/conversations
Authorization: Bearer <token>
Idempotency-Key: <key>
Content-Type: application/json
```
请求:
```json
{
"agent_type": "customer_service"
}
```
返回 `201 Created`:
```json
{
"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 查询会话
```http
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 查询会话消息
```http
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 结束会话
```http
POST /api/v1/conversations/{session_id}/closures
Authorization: Bearer <token>
Idempotency-Key: <key>
```
请求体可为空。服务端条件更新会话状态为 `ended`,触发记忆提取事件的判定由公共底座完成。已结束会话重复调用返回当前会话状态;已转人工会话不得通过此接口绕过客服状态机。
### 7.5 创建用户反馈
```http
POST /api/v1/conversation-messages/{message_id}/feedback
Authorization: Bearer <token>
Idempotency-Key: <key>
Content-Type: application/json
```
请求:
```json
{
"rating": -1,
"feedback_type": "inaccurate",
"feedback_content": "回答没有说明到账条件"
}
```
`rating` 只能为 `1` 或 `-1`,正文最多 1000 字符。消息必须属于当前用户;同一用户对同一消息只能有一条有效反馈,重复提交相同内容返回原反馈,内容不同返回 `409 FEEDBACK_ALREADY_EXISTS`。
### 7.6 用户申请转人工
```http
POST /api/v1/conversations/{session_id}/handover-requests
Authorization: Bearer <token>
Idempotency-Key: <key>
Content-Type: application/json
```
请求:
```json
{
"reason_code": "user_requested",
"reason_detail": "希望人工解释"
}
```
客户端只能提交 `reason_code=user_requested` 和有限长度的补充说明,不能提交 `priority`、`assigned_to`、置信度、会话摘要或来源引用。返回 `202 Accepted`:
```json
{
"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 查询转人工请求
```http
GET /api/v1/handover-requests/{handover_id}
Authorization: Bearer <token>
```
客户只能查看自己的 `status`、创建时间和安全提示;客服坐席可按权限查看队列和分配信息。工单状态 `pending -> assigned -> processing -> resolved -> closed`,取消规则和字段以 02 §7.2 及客服业务文档为准。
## 8. 记忆与知识引用接口
### 8.1 查询记忆画像
```http
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 解析知识引用
```http
GET /api/v1/knowledge-references/{reference_token}
Authorization: Bearer <token>
```
`reference_token` 为服务端签发的短期不透明令牌,绑定用户、知识版本和过期时间。返回允许展示的标题、版本、生效日期、来源机构和摘要;客户端不能通过修改令牌枚举 `doc_id` 或读取草稿知识。
知识上传、审核、发布、失效和 Milvus 同步由知识业务文档定义。Agent 只能使用工具返回的 `SourceReference`,不得自行构造引用。
### 8.3 知识库文档管理
```http
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 级限流;底座没有匿名路径,认证先于限流。
**上传**
```json
{
"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` 返回本次产生的知识行(一份文档切多块即为多行):
```json
{
"knowledge_ids": [109, 110],
"filename": "理财产品销售管理办法.md",
"knowledge_type": "policy",
"created_by": "9003",
"chunk_count": 2
}
```
**查询列表**
```http
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`** —— 正文是检索侧素材,管理面列表不得变成"直读知识正文"的旁路:
```json
{
"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
}
```
**删除**
```http
DELETE /api/v1/knowledge/{knowledge_id}
```
删除**不改物理行**,而是标记 `fin_knowledge_meta.status='expired'` 并**在同一事务**投递
`knowledge.vector_delete_requested`(`aggregate_id` 与 payload 均为 `str(knowledge_id)`),
由知识向量 Worker 消费后删除 Milvus 向量;本地文件随后尽力归档,**归档失败不回滚已提交的删除**(只记日志)。
成功 `200`:
```json
{
"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` | 是 | 只读行情,不得改写为成交、委托或持仓语义 |
| `query_knowledge` | `knowledge:query` | 是 | 集合名由服务端按意图映射,调用方不得指定;维度不符失败关闭 |
| `query_customer_profile` | `memory:read:self`(查他人为 `memory:read:customer`) | 是 | 越范围按"不存在"处理且不泄露存在性;无当前画像**抛错**而不返回空画像 |
**工具的白名单是两段式**:代码里的 `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`。
```text
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 配置发布
```text
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、模板和意图配置校验;未通过验证不得审核或激活。
审核请求:
```json
{
"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 平台配置项
```text
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 模型端点
```text
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 模型路由
```text
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}
```
路由规则请求使用:
```json
{
"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、意图和回复模板
```text
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 审计查询
```http
GET /api/v1/admin/audit-records
Authorization: Bearer <token>
```
支持 `trace_id`、`run_id`、用户、Agent 类型、事件类型、结果、时间范围和游标过滤。接口只读,不提供修改和删除。`audit:read-sensitive` 才能查看未脱敏详情。
### 9.7 客服转人工队列(只读)
```http
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/advisory-plans/**` | 投顾业务文档 | 只生成分析草案,不代替投顾审核发布 |
| 场内模拟交易 | `/api/v1/sim-orders/**` | 交易业务文档 | 只读查询,不创建、确认或撤销委托 |
| 风控扫描 | `/api/v1/risk/**` | 风控业务文档 | 可解释规则结果,不启动人工处置 |
| 风险预警 | `/api/v1/risk/**` | 风控业务文档 | 只读分析,不确认、升级或关闭预警 |
| 场外基金运营 | 不属于当前系统 | 独立运营系统 | 不读写场内交易表 |
风控模块落地时把扫描、预警、证据、通知和日报收在同一个 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. 运维与安全接口
```text
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 静态检查
```text
检查 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 只读边界。
- 内部契约和数据库结构没有在本文形成第二权威定义。
- 数据库迁移不修改已有表名和已有字段定义(当前现库 **52 张表**;本行原写"49 张"是早期快照,
已于 2026-09-10 按实测更正 —— 表的**数量**会随新增表变化,因此这里只保留"不修改已有表名与字段定义"这一硬约束)。
## 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` | 候选审核 |
| 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 倒序游标分页) |
| T010 | `GET /api/v1/users/me/entitlements` | `benefit:read:self`(已登录) | 否 | `200` | 我的客户层级与应享权益(含升级提示) |
> **T010 的两点说明**(与 T001–T009 **不同源**,避免混淆):
>
> - **它引入了新表**:`fin_customer_benefit`(层级 → 权益目录,54 条种子数据)。
> 下面的「T001 – T009 的四点说明」中"数据库零变更"**不覆盖 T010** ——
> 该表是**新增**的,未重命名/删除/修改任何基线表或既有字段(规则 1/3/4 均未触碰)。
> - **不落"某客户享有哪些权益"**:层级由 `fin_customer_profile.total_asset` **实时判定**
> (门槛见 `knowledge/product/高净值客户服务规范.md`:金卡 50 万 / 白金 200 万 /
> 钻石 600 万 / 私行 1000 万),权益按层级**累积**展开(白金含全部金卡条目,依此类推)。
> 与 `docs/00` L159 不保留 `net_worth_flag` 是同一取向:可算的不落库。
> `sys_user.customer_tier` 字段**本接口只读、不写**。
> **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 节。
## 20. 变更流程
任何新增或修改接口必须同时更新:
1. 本文对应章节和第 19 节目录。
2. OpenAPI 机器可读定义。
3. 权限码、审计类别、幂等说明和错误码清单。
4. 请求、响应、越权、重复提交、故障恢复和安全测试。
5. 对应业务文档的入口索引;业务载荷不得复制到公共文档。
涉及内部契约时,先修改权威源文档,再更新本文索引。禁止只修改 OpenAPI 或只修改某个业务文档造成第二权威源。