Files
group_fqcd_jr/docs/演示用/后端接口文档-2026-09-14.md
T
lzf_0626 484cccc145 记录三个演示文档的移动,并把 .workbuddy/ 加进 .gitignore
## 文档移动(docs/ -> docs/演示用/)

`代码结构与关键逻辑梳理-2026-09-14.md`、`后端接口文档-2026-09-14.md`、
`软件需求文档-2026-09-14.md` 移入 `docs/演示用/`。
git 识别为 **rename**(状态 `R`),三份文档的历史完整保留,不是"删掉再新建"。

## .workbuddy/ 加入 .gitignore

它与文件里已有的 `.agents/`、`skills-lock.json` 属同一类 —— AI 编码助手的工具产物,
不是项目内容。放在同一节下,避免以后 `git add -A` 时误提交。

## 未包含(有意留下)

`docs/23-记忆分层与画像设计.md` 的工作区改动**没有**一并提交:那不是我改的,
也不在这次确认的范围内,留在工作区由作者决定何时提交。
(暂存时被 `git add docs` 一并带上,已 `git restore --staged` 退回。)
2026-09-14 12:09:23 +08:00

111 KiB
Raw Blame History

后端接口文档(全量梳理版)

生成日期:2026-09-14 生成方式:逐份通读 app/api/controllers/ 下 22 个路由模块 + 全部 Pydantic Schema + app/api/views/envelope.py + app/core/errors.py + app/api/dependencies/auth.py + app/main.py(异常处理器与 22 个 include_router)+ 各业务 Service 的真实返回体, 并与 docs/05-接口文档.md §19 端点索引逐条对照。 性质:派生文档。当本文件与 docs/05 冲突时,以 docs/05 为准;当两者都与代码冲突时, 以代码为准(本文件已尽量标注代码实际行为)。 本次未修改任何业务代码。


目录


0. 阅读指引与端点编号约定

本文档沿用 docs/05 §19 的编号体系,便于双向检索:

前缀 域
R00x Agent 运行
C00x 会话 / 消息 / 反馈 / 转人工
M00x 记忆与画像
A0xx 平台管理(配置、审计、RBAC、画像治理、投顾审核)
AD0xx 投顾业务(目标书、组合分析、资产配置、推荐)
K00x 知识库
O00x 运维
T00x 场内基金交易
P00x 公开产品面
V001 访客令牌
X0xx 本文件新增:docs/05 §19 未登记的接口(场外运营、推介材料),
编号仅用于本文档内互引

⚠️ X0xx 是本文件自己编的序号,不在 docs/05 §19 里。若后续把这两条业务线补进 docs/05, 应以那时的正式编号为准。


1. 全局约定

1.1 基础地址与内容类型

  • 本地默认地址:http://127.0.0.1:8000
  • API 前缀:绝大多数业务接口在 /api/v1/**;两处例外:
    • 场外运营的 Agent 触发端点在 /api/**(无 v1),见 §13;
    • 运维探针在 /internal/**,见 §15。
  • 请求体默认 application/json;只有两处用 multipart/form-data: 风控证据上传(§10 POST /risk/alerts/{alert_no}/evidence)与推介材料附件上传(§14)。
  • 知识库上传故意不用 multipart,走 JSON + content_base64,理由见 §8.1。
  • 响应 application/json,UTF-8;SSE 端点返回 text/event-stream。

1.2 统一成功信封

实现:app/api/views/envelope.py。

单对象:

{
  "data": { "...": "业务负载" },
  "meta": { "trace_id": "客户端带来的 X-Trace-ID,可能为空串" }
}

列表:

{
  "data": [ { "...": "第 1 条" } ],
  "meta": {
    "trace_id": "...",
    "next_cursor": "无下一页时为 null",
    "has_more": false
  }
}

铁律(docs/05 §3.3):业务接口不得在顶层增加其它字段。 app/api/views/envelope.py 的 docstring 记录了这个偏离发生过两次(Service 直接把 {items, next_cursor, has_more} 返回出去),所以实现被收敛成同一份共用函数。新增接口请直接调用它。

三类已知的"非标准"响应体(都是刻意为之,不是遗漏):

端点 形态 原因
POST /api/v1/visitor-tokens(V001) 裸体:{access_token, token_type, expires_in} 前端 api-client.js 对该端点标 raw: true
GET /api/v1/knowledge/list(K003) data 内是 {items, count},不是顶层 data 数组 见 §8.3
场外运营全套(§13) {code, message, data} 旧式信封 Service 直接返回该结构,未走统一 envelope()

风控列表的 meta 扩展:风控的列表端点(/risk/alerts、/risk/evidence/{source}、 /risk/notifications)在标准 meta 上额外加了 total 与 page_size(由 _risk_list_envelope 注入)。这是唯一使用该扩展的域。

1.3 统一错误信封

实现:app/main.py 的 agent_error_handler + request_validation_error_handler。

{
  "error": {
    "code": "AGENT_INPUT_INVALID",
    "message": "人类可读的说明",
    "retryable": false,
    "field_errors": [ { "field": "body.quantity", "message": "..." } ]
  },
  "meta": { "trace_id": "..." }
}

要点:

  • trace_id 绝不凭空生成(app/main.py:39-51):取值顺序为 request.state.request_context.trace_id → request.state.trace_id → X-Trace-ID 请求头 → 空字符串。 凭空生成会让客户端拿到的 id 与服务端日志里的不是同一个,反而失去定位价值。
  • retryable 按码逐个标注,不是简单按 status_code >= 500 推导 —— 典型反例是 RESOURCE_VERSION_CONFLICT:409 但可重试(app/core/errors.py)。
  • RequestValidationError(FastAPI 自身的参数校验失败)被接住并重写成同一信封, 保留 422 状态码,字段级原因放进 error.field_errors。 否则客户端要为"参数错误"单独兼容一套 {"detail": [...]} 解析逻辑。
  • 429 RATE_LIMITED 会额外带 Retry-After 响应头(app/main.py:82-87)。 只给 retryable: true 不给退避时长,客户端只能猜或立刻重试再被拒。

1.4 全量错误码表

来源:app/core/errors.py(唯一权威)。tests/unit/core/test_errors.py 会用 AST 检查 "404 必须用语义化子类",禁止抛基类 ResourceNotFoundError。

错误码 HTTP retryable 触发条件
AGENT_INPUT_INVALID 422 否 入参不满足字段/业务约束;ValidationAgentError 与所有 Pydantic 校验失败
AUTHENTICATION_REQUIRED 401 否 令牌缺失、无效或已吊销(统一文案,不区分原因)
AGENT_PERMISSION_DENIED 403 否 已认证但缺少所需权限码
ONBOARDING_REQUIRED 403 否 客户未完成风险测评,且访问的不是 /api/v1/onboarding/**
INVALID_CURSOR 400 否 cursor 非法(在任何数据访问之前抛出)
RATE_LIMITED 429 是 触发限流;响应带 Retry-After
SSE_NOT_ACCEPTABLE 406 否 Accept 不接受 text/event-stream
DEPENDENCY_UNAVAILABLE 503 是 RecoverableAgentError,外部依赖暂不可用
UPSTREAM_TIMEOUT 504 是 上游超时
SESSION_NOT_FOUND 404 否 默认 404 兜底文案(新代码不应直接使用)
SESSION_NOT_ACCESSIBLE 404 否 会话存在但不属于当前用户
AGENT_TYPE_NOT_FOUND 404 否 agent_type 未注册
RUN_NOT_FOUND 404 否 run_id 不存在
RESOURCE_NOT_FOUND 404 否 通用 404(GenericResourceNotFoundError)
REFERENCE_NOT_FOUND 404 否 知识引用令牌失效
CONFLICT 409 否 通用 409 兜底文案
IDEMPOTENCY_CONFLICT 409 否 同一 Idempotency-Key + 不同请求体
RESOURCE_VERSION_CONFLICT 409 是 If-Match 与当前内容摘要不一致
RUN_NOT_CANCELLABLE 409 否 运行已终态或状态不允许取消
RUN_LEASE_LOST 409 否 Worker 租约丢失
INVALID_STATE 409 否 状态机不允许该流转
RESOURCE_ALREADY_EXISTS 409 否 唯一性/引用/状态约束冲突
FEEDBACK_ALREADY_EXISTS 409 否 同一消息重复提交反馈
PRODUCT_NOT_TRADABLE 422 否 标的不在可交易范围
INSUFFICIENT_FUNDS 422 否 可用余额不足
INSUFFICIENT_HOLDING 422 否 可用持仓不足以卖出
SUITABILITY_MISMATCH 422 否 风险等级不匹配(适当性)
HOLDING_RATIO_EXCEEDED 422 否 买入后单一投资者持仓占比超限
FUND_QUOTE_UNAVAILABLE 503 是 行情过期/不可用(15 分钟新鲜度,见 §9)
ORDER_NOT_CANCELLABLE 409 否 委托状态不允许撤单
ORDER_NOT_FOUND 404 否 委托或成交记录不存在
ACCOUNT_NOT_FOUND 404 否 模拟账户不存在

⚠️ 注意 RUN_CANCELLED 不是 HTTP 错误类。它是 request_idempotency 表上的一个 status='failed' + error_code='RUN_CANCELLED' 业务标记,没有对应的异常类。 试图 import 它或捕获它都会失败。

⚠️ 已知文档缺口:投顾目标书、目标书审核与发布的状态流转失败返回 409, 但复用了字面量 RUN_NOT_CANCELLABLE(见 §12.1)。语义上应是 INVALID_STATE。 调用方若按码分支,需要同时接受这两个码。

1.5 鉴权模型

实现:app/api/dependencies/auth.py 的 build_request_context,用 HTTPBearer(auto_error=False) 取 Authorization: Bearer <token>。

核心事实:JWT 里只有 sub。角色、权限码、data_scope 全部每次请求实时解析。

Authorization: Bearer <access_token>
  → IdentityService.resolve(sub)  → roles / permissions / data_scope

这意味着吊销立即生效(无需等 token 过期)。ACCESS_TOKEN_TTL_SECONDS = 1800(30 分钟)。

三种身份:

身份 令牌来源 特点
客户 POST /api/v1/auth/tokens 登录换取 完整权限集;未完成风险测评时被引导闸门拦截
访客 POST /api/v1/visitor-tokens roles=("visitor",),无任何权限码;只有 knowledge:query 这类专门授予的权限
管理员 同客户端登录,但角色含管理权限 权限码如 config:*、audit:read

三个易错点:

  1. 访客跳过身份解析。build_request_context 对 visitor 角色不走 IdentityService.resolve, 直接放行。所以访客令牌从不需要在 sys_user 里存在。
  2. 引导闸门。已登录客户访问非 /api/v1/onboarding/** 的接口时,会被 RiskQuestionnaireService.is_required 判定;需要则抛 ONBOARDING_REQUIRED(403)。 这是"客户一登录发现哪都调不通"的常见原因。
  3. 登录接口本身不依赖 build_request_context(否则"要登录先登录")。 它单独使用 enforce_login_rate_limit。

权限码来源:定义源是 tools/seed_test_rbac.py 的 PERMISSIONS(9001–9046 号段)。 该脚本是 DELETE 重建语义(DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099)。 没并进它的权限码,重建一次就没了,表现为"接口突然 403 而没有任何报错线索"。 一致性由 python tools/check_rbac_seed_consistency.py 守着。

工具可用范围 = 代码上限 ∩ 当前 active config_release 的发布白名单; 缺发布配置则失败关闭。config_release 是环境数据,不随代码合并。

1.6 幂等

请求头:Idempotency-Key。

  • 幂等范围(docs/05 §5.1):user_id + method + normalized_path + idempotency_key。
  • 幂等记录与业务写入同事务(ApiTransactionService.execute_in)。重复请求直接回放 response_json,不会二次驱动状态机。
  • 同一键 + 不同请求体 → 409 IDEMPOTENCY_CONFLICT。

⚠️ 风控写接口的 scope 用「实际路径」(含 alert_no),不是路径模板 (app/api/controllers/risk.py:48-65 有详细理由)。把路径参数折成模板,会让同一个键在不同 预警之间互相回放 —— 那是把两次不同资源的操作当成一次。

哪些接口要求幂等键:风控全部写操作、交易下单、场外全套写操作、推介材料写操作、 投顾目标书系列、POST /agent-runs、POST /conversations 等。 明确不要求的:AD008 组合分析、AD009 资产配置(纯分析,无副作用)。

1.7 乐观并发(If-Match / ETag)

  • 需要版本控制的资源在响应头带 ETag: "<digest>"。
  • 更新时用 If-Match: "<digest>"。不匹配 → 409 RESOURCE_VERSION_CONFLICT(retryable = true)。
  • 校验在 Service 的权限闸门之后进行(admin_service.py:96-99):未授权调用一律 403, 不能因为参数格式先漏出一个 400 —— 响应差异本身就是一条越权探测信号。

⚠️ 关键约束(曾被踩成真实 bug):

凡接受 If-Match 的资源,必须同时提供能返回该 etag 的读取路径。

A048(GET /admin/config-releases/{release_id}/platform-config-items/{item_id})与 A049(GET /admin/config-releases/{release_id}/model-routing-rules/{rule_id}) 存在的唯一目的就是暴露这个 etag —— 列表的 meta 里只有 trace_id,不带 etag。

register_resource 里 platform-config-items 与 model-routing-rules 必须是 detail=True。此前它们被设成 detail=False,导致没有任何端点能返回 digest, 于是首次编辑必然 409 —— 乐观并发成了死锁,编辑功能实际不可用 (2026-09-13 前端等价测试发现)。源码里这段注释就是这次事故的记录。

1.8 游标分页

docs/05 §3.8。

  • cursor 语义是"取更旧的一页",不是页码。
  • limit 上限因端点而异(见各域章节)。风控最严格:预警列表 limit 最大 5, 证据/通知列表最大 10。
  • 游标非法 → 400 INVALID_CURSOR,且发生在任何数据访问之前。 这既防止非法游标退化成一次静默全量查询,也避免"参数错误"先于"权限错误"泄漏信息。

两种游标实现:

域 实现 备注
交易 / 会话 / 会话消息 / 管理员配置 id < cursor 的整数游标(int(cursor)) 控制器里直接 int(cursor);非法值会抛 ValueError
风控 偏移量编码游标 + binding 签名 binding 把 user_id 与筛选条件绑进游标,换筛选条件后旧游标失效

1.9 限流

enforce_rate_limit 是路由级依赖,且自身依赖 build_request_context —— 所以鉴权永远发生在限流之前。

  • 带限流的域:/api/v1/agent-runs、/api/v1/conversations、/api/v1/risk/**、 /api/v1/admin/**、/api/v1/advisor/**(经 enforce_advisor_rollout)、登录。
  • 不带限流:交易 /api/v1/users/me/**(T 系列是低频接口)。
  • 登录用独立的 enforce_login_rate_limit。

⚠️ 风控扫描还有一把数据库锁:mysql_scan_lock() 取 GET_LOCK('jr_risk_scan_schedule', 0),加在入口层而不是 RiskScanService.scan() 内部。 原因:GET_LOCK 是连接级的,调度器已在它自己的 session 上持锁; 被两个入口共用的服务方法若再取同一把锁,取锁的连接不是持锁的那一个、必然失败, 会把定时扫描自己挡死。拿不到锁 → RiskScanBusyError(503 语义)。

1.10 审计边界

  • 审计写入与业务写入同事务,但审计失败不阻断业务(典型:登录成功/失败都记, 审计写失败从不让登录失败)。
  • 风控端点的授权一律落在 Service 层(risk_query_service / risk_action_service / risk_scan_service),Controller 只负责取 context。
  • RBAC 四个只读端点(A035–A038)不写审计(app/api/controllers/rbac.py)。
  • 管理员配置变更的审计带前后摘要:before_hash / after_hash (admin_service.py:151-165),动作类型形如 platform.<resource>.<action>。
  • GET /admin/audit-records 对没有 audit:read-sensitive 权限的调用方, 会把 detail 替换成 {"redacted": True}(admin_service.py:112-113)。

2. 公开面:访客令牌与产品目录

V001 — POST /api/v1/visitor-tokens(发访客令牌)

项 内容
用途 未登录访客换取短期令牌,用于调用访客浮窗的客服问答
鉴权 无(这是唯一无需任何令牌的业务入口之一)
幂等 否
成功 201

请求参数:无请求体。

响应(裸体,不是统一信封):

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600
}

expires_in 取值范围 60–3600 秒(VisitorTokenResponse 的约束)。

示例:

curl -X POST http://127.0.0.1:8000/api/v1/visitor-tokens

错误码:422(参数约束,实际无参数,罕见)。

注意事项:前端 common/api-client.js 对该端点标了 raw: true,因为它没有信封。 访客令牌没有任何权限码,只能调专门为访客放开的接口。


P001 — GET /api/v1/products(产品列表)

项 内容
用途 公开产品目录,供访客/客户浏览"平台有哪些基金"
鉴权 需要合法令牌,但不要求权限码(访客令牌可用)
成功 200

请求参数:无(docs/05 §19 标注为无参数)。

响应:标准列表信封。

字段含义:每条含产品基础信息。change_pct(当日涨跌幅)可能为 null —— 调用方必须显示"暂无",绝不可当成 0 展示(那会把"没数据"说成"平盘")。

注意事项:

  • ⚠️ change_pct 为 null 时必须显示"暂无"。这是 docs/05 §19 明确写下的要求。
  • 该端点不受南方基金白名单限制(对比 P002 的说明)。

P002 — GET /api/v1/products/{product_code}/nav-history(净值走势)

项 内容
用途 单个产品的历史净值序列,供前端画走势图
鉴权 需要合法令牌,不要求权限码
成功 200

路径参数

参数 类型 约束
product_code string 产品代码

查询参数

参数 类型 默认 约束
days int 90 1–365

响应:标准单对象信封,data 内含净值序列,另带 count。

示例:

curl -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:8000/api/v1/products/159915/nav-history?days=30"

注意事项:

  • ⚠️ 净值表为空时返回 count = 0,这是正常结果不是错误。 不要把它当 404 处理,也不要报"数据加载失败"。
  • 该端点不套用南方基金白名单(与 P001 一致)。

3. 认证:登录

A034 — POST /api/v1/auth/tokens(登录)

项 内容
用途 平台唯一的登录入口
鉴权 不依赖 build_request_context(否则"要登录先登录");单独用 enforce_login_rate_limit
幂等 否
成功 200

请求体

字段 类型 必填 说明
username string 是 用户名
password string 是 密码

响应

{
  "data": {
    "access_token": "eyJhbGciOi...",
    "token_type": "Bearer",
    "expires_in": 1800,
    "user_id": "9001",
    "roles": ["customer"],
    "data_scope": "self"
  },
  "meta": { "trace_id": "..." }
}
字段 类型 含义
access_token string Bearer 令牌,JWT 内只含 sub
token_type string 固定 "Bearer"
expires_in int ACCESS_TOKEN_TTL_SECONDS = 1800(30 分钟)
user_id string 用户 ID(注意是字符串,不是数字)
roles string[] 角色列表
data_scope string 数据范围,如 self / all

示例:

curl -X POST http://127.0.0.1:8000/api/v1/auth/tokens \
  -H "Content-Type: application/json" \
  -d '{"username":"customer01","password":"<pwd>"}'

错误码:401 AUTHENTICATION_REQUIRED、429 RATE_LIMITED(带 Retry-After)、422。

注意事项:

  • 失败永不区分原因:用户不存在与密码错误返回同一条消息 "用户名或密码不正确"(INVALID_CREDENTIALS_MESSAGE),防用户名枚举。
  • 用户不存在时仍执行一次 bcrypt 对比(_DUMMY_HASH),抹平时间差。
  • 成功与失败都写审计(auth.login_succeeded / auth.login_failed); 审计写失败从不阻断登录。
  • user_id 是字符串。下游 int(context.user_id) 就是为它做的转换。
  • 刷新与吊销令牌不在本接口范围,仍属身份模块。
  • ⚠️ 重跑 tools/seed_test_rbac.py 不会再弄丢演示密码 —— sys_user 已改为 "存在则更新、不存在才插入"。

4. 会话 / 消息 / 反馈 / 转人工

全部在 /api/v1 下,带 enforce_rate_limit。

C001 — POST /api/v1/conversations(创建会话)

项 内容
用途 新建一次对话会话
鉴权 权限 conversation:create
成功 201

请求体

字段 类型 约束
agent_type string pattern ^[a-z][a-z0-9_]{1,31}$

响应:标准信封,data = 会话视图(含 session_id、agent_type、status、created_at)。

注意事项:创建时会话行的时间戳字段被显式赋 now,而不是依赖 server_default=CURRENT_TIMESTAMP(6) —— 异步读回 server_default 会触发 MissingGreenlet(session 已关但懒加载未完成)。


C002 — GET /api/v1/conversations/{session_id}(查询会话)

项 内容
用途 查询会话当前状态
成功 200

错误码:404 SESSION_NOT_FOUND / SESSION_NOT_ACCESSIBLE —— 会话存在但不属于当前用户时返回 404 而非 403,避免泄漏"这个 session 存在"。


C003 — GET /api/v1/conversations/{session_id}/messages(消息列表)

项 内容
用途 拉取会话历史消息
成功 200(标准列表信封)

查询参数

参数 类型 默认 约束
limit int 20 1–100
cursor string — 游标,取更旧一页

注意事项:Controller 在调 Service 之前就 parse_cursor(cursor), 非法游标立即 400,不会变成一次静默全量查询。


C004 — POST /api/v1/conversations/{session_id}/closures(关闭会话)

项 内容
用途 主动结束会话
鉴权 权限 conversation:close
成功 200

请求体

字段 类型 默认 约束
reason string "user_cancelled" max 128

响应:data = 会话视图(self._session_view(row))。

注意事项:reason_detail 会经过 sanitize_customer_service_message 清洗。


C005 — POST /api/v1/conversations/{session_id}/handover-requests(申请转人工)

项 内容
用途 会话转人工 —— 单一入口
鉴权 权限 handover:create
成功 202

响应

{
  "data": {
    "handover_id": "HT20260914001",
    "status": "...",
    "session_id": "...",
    "created_at": "2026-09-14T02:44:20"
  },
  "meta": { "trace_id": "..." }
}

注意事项:这是唯一入口,在同一事务里写入三样:工单行 + Outbox 事件 conversation.transfer_requested + 审计。不要在别处另写工单。


C006 — GET /api/v1/handover-requests/{handover_id}(查询转人工)

项 内容
用途 查询转人工请求状态
成功 200

错误码:404。


C007 — POST /api/v1/conversation-messages/{message_id}/feedback(消息反馈)

项 内容
用途 对某条消息点赞/点踩
鉴权 权限 conversation:feedback
成功 201

响应

{ "data": { "feedback_no": "FB...", "status": "..." }, "meta": { "trace_id": "..." } }

错误码:409 FEEDBACK_ALREADY_EXISTS(同一消息重复提交)。


5. Agent 运行(异步三段式)

这是本项目最核心的调用模式。 全在 /api/v1/agent-runs,带 enforce_rate_limit。

① POST /api/v1/agent-runs           → 202 受理,拿 run_id
② GET  /api/v1/agent-runs/{run_id}  → 轮询状态与结果        (或 ③)
③ GET  /api/v1/agent-runs/{run_id}/events → SSE 实时推送

⚠️ 必须有常驻 Worker:python -m app.worker。没有 Worker 时 agent_run 会一直停在 status='queued'、worker_id 为空,而前端只显示"客服响应超时 / 客服繁忙" —— 看起来像链路慢,实际是没人处理。排查第一步:查 agent_run 最新那行是不是 queued。

R001 — POST /api/v1/agent-runs(创建运行)

项 内容
用途 受理一次 Agent 请求
鉴权 按 agent_type 走各域权限
幂等 是(Idempotency-Key)
成功 202 Accepted

请求体(AgentRunCreateRequest,extra="forbid" —— 多字段会 422)

字段 类型 必填 约束
agent_type string 是 已注册的 Agent 类型
message string 是 min_length = 1
session_id string 是 会话 ID
idempotency_key string 是 min_length = 16,max_length = 128

响应

{
  "data": {
    "run_id": "run_...",
    "trace_id": "...",
    "status": "queued",
    "status_url": "/api/v1/agent-runs/run_...",
    "events_url": "/api/v1/agent-runs/run_.../events"
  },
  "meta": { "trace_id": "..." }
}

⚠️ 两个 trace_id 不是一回事:

  • meta.trace_id = 本次 HTTP 请求的追踪号;
  • data.trace_id = 这个 run 自己的追踪号。

排障时要看哪个,取决于你在查"请求没进来"还是"这次运行出没出结果"。

已注册的 7 个业务 Agent(app/service/agent/implementations/): FundQueryDemoAgent、CustomerServiceAgent、RiskAgent、PlatformProbeAgent、 AdvisorAgent、OffsiteFundAgent、PromotionMaterialAgent。

错误码:404 AGENT_TYPE_NOT_FOUND、422、429、503。

注意事项:

  • AGENTS.md 规则 7:业务 Agent 必须继承公共 BaseAgent 并由 AgentFactory 创建, 不得绕过公共鉴权、记忆、模型路由、工具、合规、审计和事件流程。
  • Agent 属于 Service 层(规则 6)。

R002 — GET /api/v1/agent-runs/{run_id}(查询运行)

项 内容
用途 轮询运行状态与结果
成功 200

响应(AgentRunStatusResponse)

字段 类型 含义
run_id string 运行 ID
trace_id string 该 run 自己的 trace
status string queued / running / succeeded / failed / ...
agent_type string Agent 类型
session_id string 所属会话
result object | null 成功时的结果负载
error_code string | null 失败时的错误码
created_at string 受理时间
completed_at string | null 完成时间;未完成时为 null

错误码:404 RUN_NOT_FOUND。


R003 — GET /api/v1/agent-runs/{run_id}/events(SSE 订阅)

项 内容
用途 实时接收运行过程与结果
请求头 Accept: text/event-stream 必需
成功 200,text/event-stream,Cache-Control: no-cache

事件类型(app/api/views/agent_run_sse.py)

事件 含义
start 运行开始
tools 工具调用过程
replace 整体替换当前文本
delta 增量追加(按 sse_chunk_characters 分块,默认 256 字符)
done 完成
error 出错

编码格式(严格):

event: <event_name>
data: <json>
id: {run_id}:{event_name}:{index}

id 用于断线续传。心跳是注释行:: heartbeat\n\n。

若运行已终态(terminal),直接走回放模式,把历史事件重放一遍。

注意事项 —— 顺序是刻意的:

先查可见性(query.get),后查 Accept。

app/api/controllers/agent_runs.py 的注释写明了理由:反过来会让 406-vs-404 变成存在性探针(攻击者靠"是 406 还是 404"判断某 run_id 是否存在)。 风控日报流(§10)遵循同样顺序:先鉴权,后 Accept。


R004 — POST /api/v1/agent-runs/{run_id}/cancellations(取消运行)

项 内容
用途 取消进行中的运行
鉴权 权限 agent:cancel
成功 202

请求体

字段 类型 默认 约束
reason string "user_cancelled" max 128

错误码:409 RUN_NOT_CANCELLABLE(已终态)、404 RUN_NOT_FOUND。

⚠️ 注意:RUN_CANCELLED(被取消这件事本身)不是 HTTP 错误码, 它是 request_idempotency 表上的状态标记。见 §1.4。

业务域 Agent 的运行权限速查

域 权限码
会话 conversation:create / conversation:close / conversation:feedback / agent:cancel / handover:create
记忆 memory:read:self / memory:read:customer / memory:candidate:confirm / memory:candidate:review
知识 knowledge:manage / knowledge:reference:read
风控 risk:alert:read / risk:alert:write / risk:alert:scan / risk:report:mail
交易 account:read:self / trade:order:create / trade:order:read / trade:order:cancel / holding:read:self / trade:txn:read
投顾 investment-goal:* / portfolio-analysis:read:self / asset-allocation:generate:self / asset-allocation:backtest / product-recommendation:* / profile-governance:read / profile-governance:review
场外 offsite:read / offsite:write / offsite:confirm / offsite:notify
推介 promotion:read / promotion:write / promotion:review / promotion:deliver
管理 config:read / config:write / config:review / config:activate / model-endpoint:manage / audit:read / audit:read-sensitive

6. 记忆与画像候选

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

响应形状特殊:data 直接是数组({"data": [...], "meta": {"trace_id": ...}}), Service 层自己拼了信封(customer_profile_candidate_service.py:52)。

过滤条件:status ∈ ("candidate", "verified"),按 updated_at 降序,limit 上限 100。

候选字段(_view):

字段 类型 含义
candidate_id int 候选 ID
customer_id string 客户 ID(字符串)
memory_key string 记忆键
value string 候选值(content)
memory_type string 记忆类型
confidence float 置信度
status string 状态
version int 版本号
created_at / updated_at string ISO 时间

⚠️ 不返回对话证据摘录 —— _view 的 docstring 明确写了"只返回结构化候选值, 不返回对话证据摘录"。


M004 — POST /api/v1/users/me/memory-candidates/{candidate_id}/decisions(确认/拒绝候选)

项 内容
用途 用户对自己的候选记忆做确认或拒绝
鉴权 权限 memory:candidate:confirm
成功 200

请求体

字段 类型 取值
decision string "confirmed" | "rejected"

响应:data = 候选视图(同 M003 的字段)。

错误码:404、409(状态不允许,候选不是 candidate)。

注意事项:确认不会直接激活正式记忆(decide_by_customer 的 docstring)。 需管理员在 A039/A040 再走一次审核。


7. 新人引导(风险测评)

/api/v1/onboarding,唯一不受引导闸门拦截的路径(其余客户接口都受)。

O001 — GET /api/v1/onboarding/risk-questionnaire(获取问卷)

项 内容
用途 取风险测评问卷
成功 200

查询参数

参数 类型 默认 含义
retake bool False 是否重新测评

O002 — POST /api/v1/onboarding/risk-questionnaire/submissions(提交答卷)

项 内容
用途 提交风险测评
幂等 是(Idempotency-Key)
成功 201

请求体(RiskQuestionnaireSubmission,extra="forbid",frozen)

字段 类型 约束
answers dict[str, int] 必须恰好包含每一道题各一次;每个选项下标必须在 QUESTION_OPTION_COUNTS[question_id] 范围内
declaration_accepted true 必须是字面量 true

错误码:422(缺题、多题、选项越界、未接受声明)。

注意事项:extra="forbid" + frozen 意味着传多余字段或尝试重复提交同一对象都会失败。 这道闸门的存在,是"客户登录后哪都调不通"的最常见原因 —— 见 §1.5。


O003 — 相关:引导闸门

客户访问 /api/v1/onboarding/** 之外的接口时:

已登录客户 → RiskQuestionnaireService.is_required() ?
   是 → 403 ONBOARDING_REQUIRED
   否 → 放行

8. 知识库管理

K001 — GET /api/v1/knowledge-references/{reference_token}(知识引用解析)

项 内容
用途 用引用令牌换回知识原文片段
鉴权 权限 knowledge:reference:read
成功 200

路径参数

参数 类型 约束
reference_token string min_length = 20,max_length = 300

错误码:404 REFERENCE_NOT_FOUND。


K002 — POST /api/v1/knowledge/upload(上传知识文档)

项 内容
用途 上传文档入库并向量化
鉴权 权限 knowledge:manage
成功 201

请求体(JSON,不是 multipart)

字段 类型 说明
content_base64 string base64 编码的文件内容
filename string 文件名
knowledge_type string 知识类型

响应

{
  "data": {
    "knowledge_ids": ["..."],
    "filename": "...",
    "knowledge_type": "...",
    "created_by": "...",
    "chunk_count": 12
  },
  "meta": { "trace_id": "..." }
}

⚠️ 为什么不用 multipart:Controller 的 docstring 解释了这一设计选择。

⚠️ Milvus 集合 schema 因环境而异: 本机是 knowledge_id/snippet(无 visibility), 架构师环境是 doc_id/content/visibility/chapter… 检索层已改为运行时探测字段名(app/core/knowledge_schema.py)。 不要在任何地方硬编码字段名 —— 那会把另一套环境打挂。


K003 — GET /api/v1/knowledge/list(文档列表)

项 内容
用途 列出已入库文档
鉴权 权限 knowledge:manage
成功 200

查询参数

参数 类型 默认 约束
limit int 20 1–100
offset int 0 ≥ 0(注意是 offset 不是 cursor)
knowledge_type string — 可选

响应:data = {"items": [...], "count": n}。

⚠️ 每个 item 只带 content_preview(前 200 字符)+ content_length,不是全文。 需要全文走 K001 用引用令牌换。


K004 — DELETE /api/v1/knowledge/{knowledge_id}(删除文档)

项 内容
用途 软删除文档(置 expired + 发向量删除事件)
鉴权 权限 knowledge:manage
成功 200

路径参数:knowledge_id,约束 gt=0。

响应

{
  "data": {
    "knowledge_id": "...",
    "status": "expired",
    "vector_delete_event": "..."
  },
  "meta": { "trace_id": "..." }
}

错误码:404 RESOURCE_NOT_FOUND(文档不存在或已删除)。

⚠️ 不存在的 id 与已删除的 id 都返回 404,绝不静默成功 —— 静默成功会让调用方以为删除生效了。


9. 场内基金交易

前缀 /api/v1/users/me,不带 enforce_rate_limit(T 系列是低频接口)。 全部以 context.user_id 为归属,不接受客户 ID 参数 —— 无法越权查别人的账户。

⚠️ 本域最重要的两条约束

① 行情有效期只有 15 分钟。

app/service/trade_service.py 的 MAX_QUOTE_AGE。超时后所有委托一律 503 FUND_QUOTE_UNAVAILABLE「行情已过期」,且没有自动刷新。这是演示最容易翻的一环。 补刷:python tools/sync_market_prices.py(立即生效、无需重启服务)。

② 第一版只支持 price_type="market"。

立即全额成交。limit_price 恒为 null,filled_quantity = quantity。 T005 撤单对任何在途委托都返回 409 ORDER_NOT_CANCELLABLE —— 因为市价单下单即成交,status 直接是 "已成交",只有 "待风控" 状态可撤。

只读持仓允许展示最近一笔可用行情(enforce_freshness=False), 15 分钟新鲜度只约束真实下单,避免行情暂时未更新时把客户已有数据整页隐藏。


T001 — GET /api/v1/users/me/account/dashboard(账户总览)

项 内容
用途 首页资产总览
鉴权 account:read:self
成功 200

响应(AccountDashboardResponse)

{
  "data": {
    "account": {
      "account_no": "SIM...",
      "status": "...",
      "currency": "CNY",
      "initial_balance": "1000000.00",
      "cash_balance": "...",
      "available_cash": "...",
      "frozen_cash": "..."
    },
    "summary": {
      "total_asset": "...",
      "total_market_value": "...",
      "total_cost": "...",
      "total_profit_loss": "...",
      "total_profit_loss_ratio": "...",
      "today_profit_loss": "0",
      "today_profit_loss_ratio": "0"
    },
    "holdings": [ { "...": "HoldingItem" } ],
    "as_of": "2026-09-14T02:44:20"
  },
  "meta": { "trace_id": "..." }
}
字段 含义
account.initial_balance 模拟初始资金
account.cash_balance 资金余额(含冻结)
account.available_cash 可用余额
account.frozen_cash 冻结资金
summary.total_asset cash_balance + total_market_value
summary.total_profit_loss total_market_value - total_cost
summary.today_profit_loss 恒为 "0" —— 见注意事项
summary.today_profit_loss_ratio 恒为 "0"
as_of 快照时间(UTC 无时区)

⚠️ today_profit_loss / today_profit_loss_ratio 目前恒为 0 (trade_service.py:748-749 直接写 ZERO)。前端若把它当真实当日盈亏展示会误导。 需要真实当日盈亏时,请以"当日最后一笔成交价 vs 当前价"自行计算,或推动后端补齐。

所有金额字段都是字符串化的 Decimal("1234.56"),不是 JSON number —— 避免浮点精度问题。前端需 parseFloat。


T002 — POST /api/v1/users/me/orders(下单)

项 内容
用途 场内基金模拟委托(市价、立即全额成交)
鉴权 trade:order:create
幂等 是(Idempotency-Key)
成功 201

请求体(OrderCreateRequest)

字段 类型 必填 约束
product_code string 是 1–32
order_side string 是 "buy" | "sell"
quantity Decimal 是 > 0
price_type string 否 默认 "market";目前只支持 market

响应(OrderCreateResponse)

{
  "data": {
    "order_no": "SO20260914024420A1B2C3D4",
    "status": "已成交",
    "executed_quantity": "...",
    "executed_price": "...",
    "gross_amount": "...",
    "fee_amount": "...",
    "net_amount": "...",
    "executed_at": "2026-09-14T02:44:20"
  },
  "meta": { "trace_id": "..." }
}

下单的完整校验链(顺序即报错优先级):

步 校验 失败错误码
1 产品可交易 422 PRODUCT_NOT_TRADABLE
2 适当性匹配 422 SUITABILITY_MISMATCH
3 行情新鲜度 ≤ 15 分钟 503 FUND_QUOTE_UNAVAILABLE
4 账户存在 404 ACCOUNT_NOT_FOUND
5 quantity > 0 422 AGENT_INPUT_INVALID
6 quantity % product.lot_size == 0 422 AGENT_INPUT_INVALID(最小交易单位整数倍)
7 买入:available_cash >= gross + fee 422 INSUFFICIENT_FUNDS
8 买入:持仓占比 ≤ 上限 422 HOLDING_RATIO_EXCEEDED
9 卖出:available_quantity >= quantity 422 INSUFFICIENT_HOLDING

买卖金额公式(方向不同,注意):

  • 买入:net_amount = gross_amount + fee_amount
  • 卖出:net_amount = gross_amount - fee_amount

示例:

curl -X POST http://127.0.0.1:8000/api/v1/users/me/orders \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0123456789abcdef" \
  -d '{"product_code":"159915","order_side":"buy","quantity":"1000","price_type":"market"}'

注意事项:

  • quantity 必须是 product.lot_size 的整数倍,否则 422。
  • 卖出按 available_quantity 校验,不是 total_quantity (冻结份额不可卖)。
  • 下单会写入委托行 + 成交行 + 资金账三张表,并更新持仓,全部在一个事务里。
  • 委托号格式:SO{UTC时间戳}{8位随机大写hex};成交号 TX...;资金账 L...。

T003 — GET /api/v1/users/me/orders(委托列表)

项 内容
用途 历史委托列表
鉴权 trade:order:read
成功 200(标准列表信封)

查询参数

参数 类型 默认 约束
limit int 20 1–100
cursor string — 整数游标(int(cursor))

排序:id DESC(最新在前)。取 limit + 1 条判断是否有下一页。

响应字段(OrderSummary)

字段 含义
order_no 委托号
product_id / product_code / product_name 标的
order_side buy / sell
price_type 目前恒 market
quantity 委托数量
limit_price 恒 null(市价单)
quote_price 成交所用行情价
quote_at 行情时间
filled_quantity 已成数量(市价单 = quantity)
average_executed_price 成交均价
status 如 已成交 / 已撤单
submitted_at / cancelled_at 时间
reject_reason 拒单原因

T004 — GET /api/v1/users/me/orders/{order_no}(委托详情)

项 内容
用途 单笔委托详情
鉴权 trade:order:read
成功 200

响应:data = OrderSummary(字段同 T003)。

错误码:404 ORDER_NOT_FOUND。

⚠️ 查询是带 customer_id == int(context.user_id) 条件的 —— 别人的委托号一律 404,不区分"不存在"和"不属于你"。


T005 — POST /api/v1/users/me/orders/{order_no}/cancellations(撤单)

项 内容
用途 撤销委托
鉴权 trade:order:cancel
成功 200

响应:data = 撤单后的 OrderSummary(status = "已撤单",带 cancelled_at)。

错误码

码 条件
404 ORDER_NOT_FOUND 委托不存在或不属于当前用户
409 ORDER_NOT_CANCELLABLE status != "待风控" —— 即任何已成交/在途的委托

⚠️ 第一版实践上几乎总是 409:因为只支持市价单,下单即成交(status = "已成交"), "待风控" 是唯一可撤状态,而它只在风控挂起场景下出现。前端应把 409 处理成 "该委托已成交,无法撤销",而不是报错弹窗。


T006 — GET /api/v1/users/me/holdings(持仓列表)

项 内容
用途 当前持仓
鉴权 holding:read:self
成功 200

响应:data = {"holdings": [HoldingItem]}。

过滤:status == "持有中"。

字段(HoldingItem)

字段 含义
product_id / product_code / product_name 标的
total_quantity 总份额
available_quantity 可用份额
frozen_quantity 冻结份额
average_cost 平均成本价
cost_amount 成本金额
latest_price 最新价(不强制 15 分钟新鲜度)
market_value 市值 = total_quantity * latest_price
profit_loss 浮动盈亏 = market_value - cost_amount
profit_loss_ratio 盈亏比例(cost_amount > 0 时计算,否则 0)
today_profit_loss 当日盈亏

⚠️ latest_price 允许是最近一笔行情(可能超过 15 分钟)。 这是刻意的:只读展示不应因为行情未更新而整页空白。


T007 — GET /api/v1/users/me/transactions(成交记录)

项 内容
用途 成交流水
鉴权 trade:txn:read
成功 200

查询参数:limit(1–100,默认 20)、cursor。

响应:data = {"transactions": [...], "next_cursor": "..."} —— 注意这里 data 是对象、内层才是数组(走 envelope() 而非 list_envelope())。

字段(TransactionItem):transaction_no、order_no、product_id、 product_code、product_name、order_side、executed_price、executed_quantity、 gross_amount、fee_amount、net_amount、quote_at、executed_at。


T008 — GET /api/v1/users/me/transactions/{txn_no}(成交详情)

项 内容
用途 单笔成交详情
鉴权 trade:txn:read
成功 200

错误码:404 ORDER_NOT_FOUND(注意:成交不存在也复用 ORDER_NOT_FOUND, 消息是"成交记录 {txn_no} 不存在")。


T009 — GET /api/v1/users/me/cash-ledger(资金流水)

项 内容
用途 资金变动明细
鉴权 trade:txn:read
成功 200

查询参数:limit(1–100,默认 20)、cursor。

���应:data = {"entries": [...], "next_cursor": "..."}。

字段(CashLedgerItem)

字段 含义
ledger_no 流水号(L...)
entry_type 记账类型
amount 变动金额
balance_after 变动后余额
available_cash_after 变动后可用
frozen_cash_after 变动后冻结
transaction_no 关联成交号(可能为 null)
occurred_at 发生时间

10. 风控

前缀 /api/v1/risk,带 enforce_rate_limit。所有授权在 Service 层。

⚠️ 本域的三条硬约束

  1. limit 上限极严格:/risk/alerts 最大 5(默认也是 5); /risk/evidence/{source}、/risk/notifications 最大 10。
  2. 扫描锁加在入口层(mysql_scan_lock()),拿不到锁 → RiskScanBusyError。
  3. 写操作的 scope 用实际路径(含 alert_no),不是模板。

GET /api/v1/risk/overview(风控总览)

项 内容
鉴权 risk:alert:read
成功 200(标准单对象信封,非列表信封)

响应

{
  "data": {
    "total": 12,
    "levels": { "高风险": 3, "中风险": 5, "低风险": 4 },
    "pending": 7,
    "overdue": 2,
    "high_priority": [ { "...": "预警记录" } ]
  },
  "meta": { "trace_id": "..." }
}

⚠️ levels 的键是中文(高风险/中风险/低风险),由 Service 把库里的 高/中/低 映射过来。前端按中文键取。

统计口径:只统计 未闭环 预警(status IN OPEN_STATUSES),受 data_scope 约束。


GET /api/v1/risk/alerts(预警列表)

项 内容
鉴权 risk:alert:read
成功 200(列表信封 + meta.total / meta.page_size)

查询参数(RiskAlertPageQuery)

参数 类型 默认 约束
keyword string — 关键词
customer_no string — 客户号
product_code string — 产品代码
product_name string — 产品名称
risk_level string — 低 / 中 / 高
rule_code string — pattern ^RW-[0-9]{3}$
start_time / end_time datetime — 裸 datetime(不带时区),按北京时间解释
limit int 5 最大 5
cursor string — 偏移量编码游标(含筛选条件签名)

⚠️ 时间参数口径:REST 的时间参数是裸 datetime(不带时区),按北京时间 解释后再换算成库内 UTC。Agent 路径本来就带时区 (risk_natural_language.py:117)。两条路径口径必须一致,否则同一个筛选条件 在界面与对话里查出不同结果。

⚠️ 有校验器:空白文本会被拒;end_time < start_time 会被拒。

响应:标准的 data 数组 + meta(含 next_cursor / has_more / total / page_size)。


POST /api/v1/risk/alerts/scan(触发规则扫描)

项 内容
用途 手工触发风控规则扫描
鉴权 risk:alert:scan
幂等 是(Idempotency-Key)
成功 200

请求体:无({})。

响应

{
  "data": {
    "message": "规则扫描完成",
    "created_count": 3,
    "high_risk_count": 1,
    "notification_count": 1
  },
  "meta": { "trace_id": "..." }
}

通知创建失败时(不回滚预警,但必须让调用方看见):

{
  "message": "规则扫描完成,但高风险通知创建失败",
  "created_count": 3,
  "high_risk_count": 1,
  "notification_count": 0,
  "notification_failure": "错误原因"
}

⚠️ 为什么要有 notification_failure:原先这里无处可查 —— 外面只拿到 notification_count=0,分不清"这批预警本来就不用通知"和"高风险通知创建失败了", 而后者意味着处置链路的第一环断了,扫描却报"完成"。

错误码:RiskScanBusyError(扫描进行中,503 语义)。


预警处置五件套

POST /api/v1/risk/alerts/{alert_no}/{动作},全部幂等,全部权限 risk:alert:write, 全部返回标准信封,data 形状统一:

{
  "alert_no": "...",
  "status": "...",
  "ack_status": "...",
  "alert_level": "...",
  "handle_result": "...",
  "closed_at": "ISO 时间或 null"
}
端点 请求体 前置状态要求
POST .../acknowledgements 无 status == "待处理";且未确认过(ack_at is None)
POST .../investigations 无 必须已确认;status == "待处理"
POST .../exclusions {"reason": "1–500 字"} 必须已确认;未闭环
POST .../resolutions {"resolution": "1–500 字"} 必须已确认;未闭环
POST .../escalations {"reason": "1–500 字"} 必须已确认

路径参数 alert_no:min_length 1,max_length 64,pattern ^[A-Za-z0-9_-]+$。

状态流转:

待处理 --acknowledge--> 待处理(ack_status=已确认)
      --investigate--> 调查中
      --exclude--> 已排除 (写入 close_reason / handle_result)
      --resolve--> 已解决 (同时算行为分扣减)
      --escalate--> 升级 (is_escalated=true)

resolutions 额外返回行为分变化:

{
  "alert_no": "...", "status": "...", ...,
  "behavior_score_before": 80,
  "behavior_score_deduction": 10,
  "behavior_score_after": 70
}

escalations 额外返回:is_escalated、escalated_at、escalation_reason。

错误码:404(预警不存在 / 不属于 data_scope)、409(RiskActionError: 状态不允许、重复确认)。


POST /api/v1/risk/alerts/{alert_no}/evidence(上传证据)

项 内容
用途 为预警归档证据文件
鉴权 risk:alert:write
成功 200
内容类型 multipart/form-data,字段名 evidence_file

响应

{
  "data": {
    "alert_no": "...",
    "evidence_archived": true,
    "stored_name": "...",
    "file_size": 12345
  },
  "meta": { "trace_id": "..." }
}

注意事项:Controller 用 try/finally 确保 evidence_file.close() 一定执行。


GET /api/v1/risk/alerts/{alert_no}(预警详情)

鉴权 risk:alert:read;成功 200;404 预警不存在。 响应:data = 预警记录全字段(_record 已把 Decimal 串化、datetime ISO 化)。


GET /api/v1/risk/evidence/{source}(证据分源查询)

项 内容
鉴权 risk:alert:read
成功 200(列表信封 + total / page_size)

路径参数 source(必须是这 8 个之一,RiskEvidenceSource Literal):

值 数据源
customers 客户(支持 behavior_level 筛选)
products 产品
transactions 交易
capital_flows 资金流
holdings 持仓
login_records 登录记录
alerts 预警(open_only=False,含已闭环)
notifications 通知(支持 send_status 筛选)

⚠️ 这 8 个值必须与前端 EVIDENCE_COLUMNS 一致。 传其它值 → 404 RESOURCE_NOT_FOUND("证据类型不存在")。

查询参数:keyword、带时间的源支持 start_time/end_time、 customers 支持 behavior_level、notifications 支持 send_status; limit 最大 10(默认 10)、cursor。


GET /api/v1/risk/notifications(通知列表)

鉴权 risk:alert:read;limit 最大 10(默认 10);列表信封 + total/page_size。


POST /api/v1/risk/daily-report(生成日报)

项 内容
鉴权 risk:alert:read
成功 200(标准单对象信封)

请求体

字段 类型 默认
report_date date | null 今天

响应(data 结构)

{
  "type": "风控日报",
  "report_date": "2026-09-14",
  "generated_at": "2026-09-14 10:44:20",
  "data_truncated": false,
  "daily_alert_count": 5,
  "level_distribution": { "...": 0 },
  "key_risk_events": [
    {
      "alert_id": "...", "alert_level": "高风险", "alert_type": "...",
      "triggered_rules": ["RW-001"],
      "evidence_summary": "...", "status": "...", "ack_status": "...",
      "handler_id": "...", "created_at": "...", "due_time": "...",
      "is_overdue": false, "is_escalated": false, "close_reason": null
    }
  ],
  "unresolved_items": {
    "total": 7, "new_today": 3, "historical": 4, "overdue": 2,
    "items": [ "同 key_risk_events 的结构" ]
  },
  "false_positive_statistics": {
    "total": 2,
    "reasons": [ { "alert_id": "...", "reason": "..." } ]
  },
  "type_distribution": { "...": 0 },
  "disposition_results": {
    "acknowledged": 3, "false_positive_closed": 2,
    "escalated": 1, "investigating": 1
  },
  "rule_effectiveness": { "...": "..." },
  "optimization_suggestions": "",
  "source": "",
  "prompt_version": "..."
}

⚠️ optimization_suggestions / source 在同步接口里是空串 —— 建议内容由流式接口(下一条)生成并回填。


POST /api/v1/risk/daily-report/stream(流式生成日报)

项 内容
请求头 Accept: text/event-stream 必需
成功 200,text/event-stream,Cache-Control: no-cache,X-Accel-Buffering: no

事件格式(注意与 §5 的 run_id 事件格式不同,这里没有 id: 行):

event: <type>
data: <json>

type 说明
start 带 generated_at
progress 带 stage(statistics / suggestions)与 message
replace 带 content(当前完整文本)
done 带 report(完整报告对象)

⚠️ 顺序是刻意的:service.authorize(context) → accepts_event_stream(Accept) → 才返回 StreamingResponse。

理由是 stream() 是 async generator,函数体到第一次迭代才执行, 而那时响应头已经发出去了 —— 403/406 只能变成"200 + 半截流"(docs/25 P3 #24)。 顺序与 §5 的 R003 一致。


POST /api/v1/risk/daily-report/mail(邮件发送日报)

项 内容
鉴权 risk:report:mail(校验在 Service 层)
成功 200

请求体(RiskDailyReportMailRequest)

字段 类型 约束
recipients string[] 1–10 个;RFC 风格校验(parseaddr);大小写不敏感去重
subject string 1–128
content string 1–20000

响应(data)

status 含义
"disabled" 环境变量未开启(带 recipient_count)
"dry_run" 演练模式(默认就是 dry_run)
"configuration_error" SMTP 配置错误(带 recipient_count)
其它 真实发送结果

⚠️ 这条 Service 的 docstring 值得读:「这个端点是此前唯一没有校验的 —— 收件人、标题、正文全由客户端决定,一旦运维开启 SMTP,它就是一个未授权的邮件发送器。」 权限校验因此被放在 Service 层(与风控其它端点一致),而不是 Controller。


11. 平台管理(配置 / 审计 / RBAC)

前缀 /api/v1/admin,带 enforce_rate_limit。两个文件共用该前缀: app/api/controllers/admin.py 与 app/api/controllers/rbac.py。

11.1 表驱动 CRUD(register_resource)

app/api/controllers/admin.py 用工厂函数动态生成端点。每个资源自动得到:

方法 路径 状态码 说明
POST {prefix} 201 创建;响应头设 ETag
GET {prefix} 200 列表(limit 1–100 默认 20 + cursor)
GET {prefix}/{id} 200 详情(仅在 detail=True 时生成);设 ETag
PUT {prefix}/{id} 200 更新(仅在 update=True 时生成);要求 If-Match

prefix 对 scoped=True 的资源是 /config-releases/{release_id}/{resource},否则是 /{resource}。

已注册的 8 个资源:

资源 Schema 路径参数 选项
config-releases ReleasePayload release_id update=False
platform-config-items ItemPayload item_id scoped=True, **detail=True**
model-endpoints EndpointPayload endpoint_id —
model-routing-rules RoutingPayload rule_id scoped=True, **detail=True**
prompt-templates PromptPayload prompt_id update=False
agent-intent-configs IntentPayload config_id —
reply-templates ReplyPayload template_id detail=False
negative-word-rules NegativePayload rule_id detail=False

⚠️ platform-config-items 与 model-routing-rules 必须是 detail=True —— 理由见 §1.7。这是修过的真 bug。

权限映射(admin_service.py):

操作 权限
query(除 audit-records) config:read
query on audit-records audit:read
mutate(默认) config:write
mutate on model-endpoints model-endpoint:manage
mutate with action="reviews" config:review
mutate with activations / rollbacks 且资源是 config-releases config:activate

全部带 admin=True 参数。

11.2 状态流转(register_transition)

生成 POST /{resource}/{id}/{action},状态码 201(仅 rollbacks)否则 200, 设 ETag,要求 If-Match,幂等。

资源 动作
config-releases validations、reviews、activations、rollbacks
model-endpoints reviews、activations、disablements
agent-intent-configs reviews、activations、archivals

请求体:action == "reviews" 时用 ReviewPayload(decision + comment), 其余用 EmptyPayload。

⚠️ agent-intent-configs 用 archivals 而不是 disablements: 该表 CHECK 约束只允许 draft/approved/active/archived,没有 disabled。

⚠️ 只能编辑草稿或停用版本(_write): existing.status 不在 {draft, disabled} 中 → 409 INVALID_STATE。 带 release_id 时,父批次状态必须是 draft,否则同样 409。

⚠️ 数据库完整性冲突被翻译成 409:IntegrityError → RESOURCE_ALREADY_EXISTS ("资源唯一性、引用或状态约束冲突"),不是 500。

11.3 关键 Payload 约束

ItemPayload(平台配置项)

字段 约束
namespace 必须是 agent_tools / memory / relationship / runtime / fund_market
item_key 1–128
value_json dict
schema_version 字面量 "1"

⚠️ fund_market 是必需的命名空间 —— 否则行情配置会 422。 每个命名空间有白名单字段(_write 里硬编码):

namespace 允许的 value_json 键
memory recall_limit、decay_days
relationship max_hops、limit
runtime timeout_seconds
agent_tools allowed_tools
fund_market FUND_MARKET_FIELDS

含未声明字段 → 422 AGENT_INPUT_INVALID("配置包含未声明字段")。

agent_tools 额外校验:allowed_tools 必须是字符串列表; item_key 格式为 agent_type:intent; intent 必须在该 Agent 声明的 supported_intents 内; 工具集必须是该 Agent allowed_tools 的子集。

RoutingPayload(模型路由)

字段 约束
max_attempts 1–3,默认 2
fallbacks 最多 2 个
latency_budget_ms 100–120000

⚠️ max_attempts 不得超过端点数量,否则 422。

EndpointPayload(模型端点)

字段 约束
endpoint_code / provider / model_name 必填
base_url HttpUrl
secret_ref pattern ^env:[A-Z][A-Z0-9_]{0,100}$ —— 只允许环境变量引用
capabilities / allowed_data_levels 列表
context_window > 0
timeout_ms 100–120000,默认 15000

⚠️ secret_ref 只接受 env:XXX —— 密钥不落库。

ReplyPayload(话术模板)

⚠️ scene 必须与数据库 CHECK 约束 chk_template_scene 完全一致:

disclaimer | low_confidence | compliance_block | transfer |
model_failure | system_busy | clarification

不一致时会以 500 暴露,而不是 422 —— 这是历史踩过的坑。

IntentPayload:confidence_threshold 是字符串形式的数字(用 pattern 校验), max_clarification_rounds 范围 0–10。

NegativePayload:match_type ∈ {contains, exact};severity ∈ {block, replace, warn}。

ReviewPayload:decision ∈ {approved, rejected};comment 默认 "",max 1000。

11.4 A030–A033、A039–A044 具体端点

编号 端点 用途 权限
A033 GET /api/v1/admin/audit-records 审计查询(limit 1–100 默认 20 + cursor) audit:read(无 audit:read-sensitive 则 detail 被涂成 {"redacted": true})
— GET /api/v1/admin/customer-service/handover-tickets 客服待转人工队列(只读) 管理员
— GET .../handover-tickets/{ticket_no} 单工单脱敏摘要 管理员
A039 GET /api/v1/admin/customer-profile-candidates 待处理画像候选(limit 默认 20) memory:candidate:review(admin=True)
A040 POST /api/v1/admin/customer-profile-candidates/{candidate_id}/reviews 批准/驳回候选 同上;成功 200
A041 POST /api/v1/admin/advisor/asset-allocation-backtests 资产配置回测 asset-allocation:backtest;201;幂等
A042 GET /api/v1/admin/advisor/profile-tags?customer_id=N 客户画像标签 profile-governance:read;customer_id gt=0
A043 GET /api/v1/admin/advisor/profile-drift-reviews 待处理画像漂移复核 profile-governance:read
A044 POST /api/v1/admin/advisor/profile-drift-reviews/{review_id}/reviews 复核漂移 profile-governance:review;review_id gt=0;幂等

A039/A040 的候选字段(同 §6 的 _view):candidate_id、customer_id(字符串)、 memory_key、value、memory_type、confidence、status、version、时间戳。 同样不返回证据原文。

A040 的排序不同:list_for_admin 按 updated_at 升序(先处理最旧的), 而 M003 的 list_for_customer 是降序。

A040 批准时会处理同键旧正式记忆(conflict_type="candidate_promoted")。

11.5 RBAC 只读查询(A035–A038)

app/api/controllers/rbac.py,前缀 /api/v1/admin。四个端点全部只读,且不写审计。

编号 端点 用途
A035 GET /api/v1/admin/roles 角色列表
A036 GET /api/v1/admin/roles/{role_code} 角色详情
A037 GET /api/v1/admin/roles/{role_code}/permissions 角色的权限码
A038 GET /api/v1/admin/users/{user_id}/roles 用户的角色与权限

权限:复用 audit:read,没有新增 rbac:read(docs/05 §19 的明确说明)。

A038 的响应

{
  "data": {
    "username": "...",
    "user_no": "...",
    "status": "...",
    "data_scope": "...",
    "roles": [...],
    "permissions": [...]
  },
  "meta": { "trace_id": "..." }
}

⚠️ A038 的 user_id 路径参数 pattern 是 ^[0-9]{1,20}$(字符串形式的数字), 与 A039 等处用 int + gt=0 的风格不同。前端传数字字符串。

⚠️ 权限的「修改」目前没有端点 —— 这是刻意的(docs/05 §19 说明"需要三条红线")。


12. 投顾(目标书 / 组合分析 / 资产配置 / 推荐)

前缀 /api/v1/advisor,带 enforce_rate_limit + enforce_advisor_rollout。

⚠️ enforce_advisor_rollout 是投顾域特有的额外闸门: 在鉴权之后、业务逻辑之前拦截。

12.1 投资目标书(AD001–AD007)

编号 端点 权限 成功 幂等
AD001 POST /api/v1/advisor/investment-goals investment-goal:write 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 200 —
AD004 POST /api/v1/advisor/investment-goals/{goal_no}/confirmations investment-goal:confirm 200 是
AD005 GET /api/v1/advisor/investment-goals/{goal_no}/goal-book investment-goal:read 200 —
AD006 POST .../{goal_no}/goal-book/reviews admin 200 是
AD007 POST .../{goal_no}/goal-book/publications admin 200 是

⚠️ AD006/AD007 要求 admin,尽管它们在 /advisor/** 下(docs/05 §19 特别标注)。

AD001 请求体(InvestmentGoalCreate)

字段 类型 约束
customer_id int | null gt=0;为空表示自己
annualized_return_lower_pct Decimal 0–100,max_digits 7,小数 4 位
annualized_return_upper_pct Decimal 同上;必须 ≥ lower
max_drawdown_pct Decimal 0–100
liquidity_requirement string 枚举(daily 等)
investment_horizon_months int 1–600
benchmark_name string 1–128
notes string max 1000

⚠️ 有控制字符拒绝校验。

AD001 响应(_view)

{
  "data": {
    "goal_no": "...", "customer_id": "9001", "status": "...",
    "goal_gap_status": "awaiting_confirmation | none",
    "annualized_return_lower_pct": "5.0000",
    "annualized_return_upper_pct": "8.0000",
    "max_drawdown_pct": "15.0000",
    "liquidity_requirement": "daily",
    "investment_horizon_months": 36,
    "benchmark_name": "...", "notes": "...", "source": "...",
    "goal_book": { "content_id": "...", "review_status": "...", "published_at": null },
    "confirmed_at": null, "created_at": "...", "updated_at": "..."
  },
  "meta": { "trace_id": "..." }
}

⚠️ customer_id 是字符串;所有百分比是字符串化的 Decimal。

AD004 请求体:{"confirmed": true} —— 必须是字面量 true。

AD005 响应(与其他 AD 端点形状不同,注意)

{
  "data": {
    "goal_no": "...",
    "goal_status": "...",
    "review_status": "...",
    "content": { "...": "目标书草稿内容(structured sections)" },
    "published_at": null
  },
  "meta": { "trace_id": "..." }
}

目标书 content 的结构(_build_draft):

{
  "document_type": "investment_goal_book",
  "document_version": "1.0",
  "goal_no": "...",
  "sections": {
    "investment_objective": {
      "annualized_return_expectation_pct": { "lower": "5.0000", "upper": "8.0000" },
      "benchmark_name": "..."
    },
    "risk_boundary": { "maximum_drawdown_pct": "15.0000" },
    "liquidity": { "requirement": "daily", "description": "可随时使用" },
    "investment_horizon": { "months": 36 },
    "notes": "..."
  },
  "disclosures": [
    "本目标书为待审核草稿,仅作为后续场内基金模拟交易分析的输入,不构成交易指令。"
  ]
}

AD006 请求体(InvestmentGoalBookReview):decision ∈ {approved, rejected}, comment max 1000。

AD007 请求体(InvestmentGoalBookPublish):{"publish": true}。

状态机

AD001 → pending_confirmation
AD004 → confirmed
AD006 → goal_book.review_status: approved / rejected
AD007 → published_at 有值

⚠️ 已知文档缺口:流转失败返回 409,但复用了字面量 RUN_NOT_CANCELLABLE (docs/05 §19 自述这是"已知文档缺口")。语义上应是 INVALID_STATE。

12.2 组合分析(AD008)

项 内容
端点 POST /api/v1/advisor/portfolio-analysis
权限 portfolio-analysis:read:self
额外闸门 enforce_profile_governance=True
幂等 否(纯分析)
成功 200

响应 status 三态:

status 含义 附带字段
no_positions 无持仓 position_count: 0
valuation_required 持仓缺可用市值,暂不能算集中度 position_count、warnings
ready 正常 summary、product_concentration、industry_concentration、warnings、disclaimer

ready 时的完整结构

{
  "status": "ready",
  "summary": {
    "position_count": 5, "valued_position_count": 4,
    "total_market_value": "123456.78",
    "industry_coverage_pct": "80.00",
    "metrics_coverage_pct": "75.00"
  },
  "product_concentration": {
    "hhi": 0.32,
    "rows": [ { "product_id": "...", "product_code": "...", "product_name": "...",
                "market_value": "...", "share_pct": "35.00" } ]
  },
  "industry_concentration": {
    "hhi": 0.28, "rows": [ { "industry_name": "...", "market_value": "...",
                             "share_pct": "..." } ],
    "conclusion_available": true
  },
  "warnings": [ { "code": "...", "severity": "high|medium", "message": "..." } ],
  "disclaimer": "分析结果仅供参考,不生成交易指令。"
}

warnings 的 7 种 code(前端可按码上图标):

code severity 含义
MARKET_VALUE_UNAVAILABLE high 持仓缺可用市值,暂不能算集中度
SINGLE_PRODUCT_CONCENTRATION high 单一产品占比高
SINGLE_INDUSTRY_CONCENTRATION high 单一行业穿透占比高
INDUSTRY_COVERAGE_INCOMPLETE medium 行业穿透数据覆盖不足,不输出确定性行业结论
INDUSTRY_EXPOSURE_INVALID medium 部分产品行业暴露数据异常,未纳入计算
HISTORICAL_METRICS_INCOMPLETE medium 部分持仓缺历史指标
MARKET_VALUE_PARTIAL medium 部分持仓缺市值,本次基于已估值持仓计算

⚠️ 行业穿透依赖 Neo4j。图不可用时 _graph_context 返回 {"degraded": true, "reason": "..."},不抛异常,行业结论降级为不可用。 reason 可能是 neo4j_unavailable:XXXError / neo4j_invalid_result / graph_not_configured。

⚠️ disclaimer 恒定:"分析结果仅供参考,不生成交易指令。"

12.3 资产配置(AD009)

项 内容
端点 POST /api/v1/advisor/asset-allocation
权限 asset-allocation:generate:self
额外闸门 enforce_profile_governance=True
幂等 否
成功 200

响应 status 四态

status 含义
profile_required 缺风险测评
investment_goal_required 缺投资目标书
investment_goal_invalid 目标书无效
ready 正常

ready 时

{
  "status": "ready",
  "allocation": [
    { "asset_class": "cash_management_etf", "label": "现金管理类场内基金", "target_pct": 15 },
    { "asset_class": "bond_etf", "label": "债券类场内基金", "target_pct": 45 },
    { "asset_class": "equity_etf", "label": "权益类场内基金", "target_pct": 40 }
  ],
  "optimization": {
    "method": "constrained_historical_multi_factor_v1",
    "dynamic": true,
    "metric_coverage_pct": "75.00",
    "strategic_allocation": { "...": "..." },
    "factor_evidence": { "...": "..." }
  },
  "constraints": {
    "annualized_return_lower_pct": "...",
    "max_drawdown_pct": "...",
    "liquidity_requirement": "...",
    "investment_horizon_months": 36
  },
  "analysis_only": true
}

⚠️ 战略基准配置按风险等级 C1–C5 硬编码(asset_allocation_service.py:26-30):

等级 现金管理 债券 权益
C1 50% 40% 10%
C2 30% 50% 20%
C3 15% 45% 40%
C4 10% 25% 65%
C5 5% 15% 80%

⚠️ analysis_only: true 恒定 —— 不生成交易指令。

12.4 产品推荐(AD010–AD011、A045–A047)

投顾侧

编号 端点 权限 成功 备注
AD010 POST /api/v1/advisor/recommendations product-recommendation:generate:self 201 enforce_profile_governance=True;幂等
AD011 GET /api/v1/advisor/recommendations/published product-recommendation:read:self 200 —

管理侧

编号 端点 权限 成功
A047 GET /api/v1/admin/advisor/pending-contents product-recommendation:review 200
A045 POST /api/v1/admin/advisor/recommendations/{content_id}/reviews product-recommendation:review 200
A046 POST /api/v1/admin/advisor/recommendations/{content_id}/publications product-recommendation:publish 200

AD010 响应 status 四态:profile_required / investment_goal_required / recommendation_input_invalid / ready。ready 时:

{
  "data": {
    "content_id": "...",
    "status": "pending_review",
    "plan": {
      "status": "ready",
      "document_type": "advisor_recommendation_plan",
      "document_version": "1.0",
      "products": [
        {
          "rank": 1, "product_code": "...", "product_name": "...",
          "product_category": "...", "reason": "...", "score": 0.8231,
          "recommendation_evidence_card": {
            "card_version": "1.0",
            "hard_constraints": [ "..." ],
            "suitability": { "risk_level": "...", "source_url": "...", "document_title": "..." },
            "contract": { "fund_type": "...", "source_url": "...", "document_title": "..." },
            "liquidity": { "status": "...", "average_daily_turnover_amount": "..." },
            "goal_constraints": { "liquidity_requirement": "...", "investment_horizon_months": 36 },
            "graph_status": "available | degraded"
          }
        }
      ],
      "excluded_candidates": [
        { "product_code": "...", "product_name": "...", "stage": "ranking",
          "reason_code": "RANKED_BELOW_SELECTION_LIMIT", "reason": "...",
          "ranking_score": 0.4512, "selection_limit": 5 }
      ],
      "selection_summary": {
        "candidate_count": 12, "selected_count": 5, "excluded_count": 7
      },
      "graph_context": { "...": "..." },
      "disclosures": [ "..." ],
      "analysis_only": true
    }
  },
  "meta": { "trace_id": "..." }
}

⚠️ 产品必须通过三重校验才会入选:场内可交易 + 权威适当性 + 合同证据 (reason 字段明写)。

A045 的入参校验在 Controller 手工做:decision 必须是 approved/rejected (否则 ValidationAgentError → 422),comment 必须是 str。

A045 响应:{"content_id": "...", "status": "approved|rejected"}。


13. 场外基金运营

⚠️ 本域用「旧式信封」{code, message, data},不是统一信封。 app/api/controllers/offsite_fund.py 直接用 OffsiteFundService 的返回值, 没有经过 envelope()。前端需按 code === 0 判断成功。

⚠️ 本域的权限检查风格与其它域不同:用 _permission_error(context, (...)) 的 any-of 语义(交集非空即通过),而不是 AuthorizationService.require。 观察到四种权限组合:

组合 用于
("offsite:read", "offsite:write") 读操作
("offsite:write",) 写操作
("offsite:write", "offsite:confirm") 确认操作
("offsite:notify", "offsite:write") 通知操作

13.1 两个 Router

Router 前缀 说明
router /api/v1/offsite-fund 主业务
operation_router /api 仅一个端点:Agent 触发

13.2 端点清单

端点 方法 权限组合 说明
/api/v1/offsite-fund/mails GET read/write 邮件列表
/api/v1/offsite-fund/mails/{mail_id} GET read/write 邮件详情
/api/v1/offsite-fund/mails/{mail_id}/deletions POST write 删除邮件
/api/v1/offsite-fund/mails/{mail_id}/recognition-fields GET / PUT read/write、write 识别字段读取/纠正
/api/v1/offsite-fund/documents/{task_id}/nl2sql-fields GET / PUT read/write、write NL2SQL 字段读取/纠正
/api/v1/offsite-fund/documents/{task_id}/rule-results GET read/write 规则结果
/api/v1/offsite-fund/documents/{task_id}/rule-results/recalculations POST write 重算规则
/api/v1/offsite-fund/mailbox-status GET read/write 邮箱状态
/api/v1/offsite-fund/mailbox-status/recoveries POST write 恢复邮箱
/api/v1/offsite-fund/attachments/{attachment_id}/file GET read/write 下载附件(FileResponse)
/api/v1/offsite-fund/documents/{task_id}/confirmations POST write+confirm 确认单据
/api/v1/offsite-fund/documents/{task_id}/recognition-retries POST write 重试识别
/api/v1/offsite-fund/documents/{task_id}/notifications POST notify/write 创建通知
/api/v1/offsite-fund/notifications/{notification_id}/send POST notify/write 发送通知
/api/v1/offsite-fund/settlement-statistics/recalculate POST write 重算结算统计
/api/tasks/{task_id}/trigger-agent-nl2sql POST write 触发 Agent NL2SQL

13.3 邮件列表参数与响应

查询参数

参数 类型 默认 约束
page int 1 ≥ 1
page_size int 20 1–100
sender string — 3–255
status string — 1–32

响应

{
  "code": 0,
  "message": "ok",
  "data": {
    "items": [ "..." ],
    "page": 1,
    "page_size": 20,
    "total": 57
  }
}

⚠️ 这是分页式(page/page_size),不是游标式。

13.4 邮件详情与识别字段

GET /mails/{mail_id} 返回 {"code": 0, "message": "ok", "data": detail} (调用了 _add_audit(context, "offsite.mail_viewed", ...));不存在 → {"code": 404, "message": "邮件不存在", "data": {}}。

recognition-fields 的每个 attachment 字段:

字段 含义
attachment_id / filename / document_type / media_type / size_bytes / status 基础信息
ocr_text OCR 原文
extracted_fields LLM 抽取字段
effective_fields 应用纠正后的有效字段(normalize_recognition_fields)
corrections 纠正记录
has_correction 是否有纠正
default_state 默认状态
field_confidence 字段级置信度
page_evidence 页码证据
missing_fields 缺失字段
low_confidence_fields 低置信字段
ocr_status / llm_status 各阶段状态
latest_attempt 最近一次尝试(attempt_no / source / status / started_at / finished_at / error_message)

纠正记录结构(_correction_payload):operator_id、fields、changed_fields、 corrected_at。

13.5 附件下载(⚠️ 特殊)

GET /api/v1/offsite-fund/attachments/{attachment_id}/file?disposition=inline|attachment
参数 约束
disposition pattern ^(inline|attachment)$
  • 返回 FileResponse(二进制流),不是 JSON。
  • 响应头带 X-Content-Type-Options: nosniff。
  • 若 Service 返回的是 dict(如错误),则原样返回该 dict —— 所以该端点可能同时返回文件或 JSON,调用方需按 Content-Type 分支。

13.6 业务含义速查

认购/赎回的份额口径(offsite_fund_service.py:107-108):

操作 需要的字段
subscription(认购) 最新净值、基金最新总份额、申请前持有份额
redemption(赎回) 基金最新总份额、当前最新可用份额

13.7 请求体要点

Schema 关键字段
OffsiteConfirmRequest decision、operator_id(1–64)
OffsiteRecalculateRequest fund_code(≤32,可选)、application_date(8–32)
OffsiteNotificationRequest notification_type、operator_id
OffsiteNotificationSendRequest operator_id、operator_confirmed(bool)、final_content(≤10000,可选)
OffsiteTriggerNl2SqlRequest operator_id、manual_confirmed(bool)
OffsiteRecognitionCorrectionRequest operator_id、attachments(min_length=1)
OffsiteRecognitionCorrectionItem attachment_id(1–24)、fields(dict[str,str])
OffsiteNl2SqlCorrectionRequest operator_id、fields(dict[str,str])

⚠️ 每个写操作都要 operator_id —— 场外是运营流程,操作人必须显式声明。

⚠️ 删除是幂等的:delete_mail 对已删除邮件返回 {"code": 0, "message": "邮件已删除", "data": {"mail_id": ..., "status": "deleted"}} —— 不报错。

⚠️ 场外独立于场内:AGENTS.md 规则 8 —— 场外基金运营流程独立, 不得写入场内交易表。


14. 基金推介材料

前缀 /api/v1/fund-promotion-materials。同样用旧式信封 {"code": 0, "message": "ok", "data": {...}}。

端点清单

# 端点 权限 成功 幂等
X001 POST /api/v1/fund-promotion-materials promotion:write 201 是
X002 PUT /api/v1/fund-promotion-materials/{task_no}/inputs promotion:write 200 是
X003 POST /api/v1/fund-promotion-materials/{task_no}/attachments promotion:write 200 是
X004 POST /api/v1/fund-promotion-materials/{task_no}/generations promotion:write 200 是
X005 GET /api/v1/fund-promotion-materials/{task_no}/compliance-checks promotion:read 200 —
X006 GET /api/v1/fund-promotion-materials/{task_no} promotion:read 200 —
X007 POST /api/v1/fund-promotion-materials/{task_no}/reviews promotion:review 200 是
X008 POST /api/v1/fund-promotion-materials/{task_no}/deliveries promotion:deliver 200 是

X001 请求体(PromotionTaskCreate)

字段 类型 约束
product_name string 1–128
product_code string ≤ 32,可选
material_title string 1–200
style_code string 样式码
output_formats string[] 1–2 个,默认 ["pptx"]

响应:{"code": 0, "message": "ok", "data": {"task_no": "...", "status": "draft"}}。

X002 请求体(PromotionInputsUpdate)

7 个结构化资料组 + 备注:

product_info(ProductInfo)、manager_info(ManagerInfo)、team_info(TeamInfo)、 strategy_info(StrategyInfo)、fee_structure(FeeStructure)、 ranking_info(RankingInfo)、performance_info(PerformanceInfo)、 risk_disclosure(RiskDisclosure)、source_notes。

响应:{"task_no": ..., "input_version": N, "status": ...}。

X003 附件上传(⚠️ multipart)

  • multipart/form-data,文件字段 file,另有 attachment_type 查询参数。
  • 按类型限制大小与扩展名(promotion_material_service.py:713-716):
attachment_type 大小上限 允许扩展名
manager_photo max_photo_size .jpg .jpeg .png .webp
performance_data max_performance_size .csv .xlsx .xlsm
source_evidence 30 MB .pdf .docx .xlsx .csv
template_file 50 MB .pptx

响应:{"attachment_id", "attachment_type", "sha256", "size_bytes"}。 重复上传同一文件 → 返回已有 id 并带 "duplicate": true(按 sha256 去重)。

X004 生成(PromotionGenerationRequest)

字段 类型 约束
output_formats string[] 1–2 个,可选

四种返回 code(注意这不是 HTTP 状态码,是 body 里的 code):

code message 含义
0 ok 成功
422 请先补充结构化资料 未先调 X002
422 输入资料未通过合规校验 带 findings
422 生成内容未通过合规校验 带 findings
503 异常消息 生成服务不可用

成功时 data:task_no、material_version_id、status、pptx_path、pdf_path、 poster_path、chart_paths、findings。

⚠️ 合规校验双重:输入资料校验 + 生成内容校验。阻断级问题直接拦截生成。

X005 合规检查

data = {"task_no": ..., "findings": [...]}。

finding 字段:id、material_version_id、rule_code、rule_name、scope、 severity、hit_text、suggestion。

X006 任务详情

data:task_no、product_name、status,以及 material_version (null 或 {id, version_no, style_code, status, pptx_path, pdf_path, poster_path, chart_paths})。

⚠️ material_version 可能是 null(尚未生成)。

X007 审核(PromotionReviewRequest)

字段 约束
material_version_id gt=0
decision pattern ^(approved|rejected|revision_requested)$
comment ≤ 1000

⚠️ 三重前置校验:

  1. version.status 必须是 pending_review,否则 422「当前材料版本不在待审核状态」;
  2. 存在阻断级合规问题 → 422「存在阻断级合规问题」;
  3. 否则可审。

响应:{"task_no", "material_version_id", "status"}。

X008 发送(PromotionDeliveryRequest)

字段 约束
material_version_id gt=0
advisor_ids 1–100 个
delivery_channel 默认 "internal_record"

⚠️ 两条前置校验:

  1. version.status 必须是 approved,否则 422「只有审核通过的材料可以发送」;
  2. delivery_channel 必须是 internal_record,否则 422「首版只支持内部发送记录,未接入外部投顾端」。

响应:{"task_no", "material_version_id", "advisor_ids", "status": "sent"}。


15. 运维与健康检查

前缀 /internal,全部无需鉴权(供探针/监控调用)。

GET /internal/health/live(存活探针)

响应 200:

{ "status": "ok" }

⚠️ 这是裸体响应,不带信封。

GET /internal/health/ready(就绪探针)

响应:

{
  "status": "ready",
  "checks": { "mysql": true, "redis": true, "milvus": true },
  "probes": { "milvus": "ready" }
}

状态码:status == "ready" → 200;否则 → 503。

status 取值:ready(全部 checks 为 true)| degraded(任一为 false)。

⚠️ 三项探测都是真实探测(health_service.py 的 docstring 记录了 B3 修复):

  • 原先 checks["milvus"] = True 是硬编码,健康检查永远报告 Milvus 正常, 属于误导性探针。现在改为真实探测(连接 + get_server_version)。
  • 探测有超时(默认 2 秒),超时返回 False + timeout 状态。
  • 外部依赖不可用时不抛异常,只返回 False + 明确状态,整体降级为 degraded。
  • pymilvus 是可选依赖(且无类型存根),缺失时返回 False + client_missing。

probes.milvus 可能的取值:ready / timeout / client_missing / unavailable 等。

probes.mysql:连接失败时是 "unavailable"。

GET /internal/metrics(Prometheus 指标)

响应 200,text/plain:

# TYPE jr_agent_up gauge
jr_agent_up 1

⚠️ 这是占位实现,只暴露一个 jr_agent_up gauge。


16. 前端约定与静态度量

虽不属于后端接口,但影响调用方式,列在这里备查。

四套页面(app/static/portal/,由 app/main.py 挂载):

目录 角色
guest/ 访客(公开产品页暂时用 common/mock-data.js,并在页面上标注)
customer/ 客户
employee-console/ 管理员
employee-risk/ 风控

⚠️ 端点表集中在 common/api-client.js —— 改接口调用请改那一份。

访问路径:

  • /portal/ 和 /(/ 会 307 跳到 /portal/guest/home/)
  • /static/**(静态资源)
  • /customer-service-test/**(客服测试页)

⚠️ 启动命令:python -m uvicorn app.main:app --port 8000 (注意模块级变量是 app,不是 application)。

⚠️ tools/portal.py(端口 8101)只是跨角色联调工具,不是产品前端, 不要再往它加功能。

已知前端 P0(本次梳理发现,供参考)

app/static/portal/employee-console/workspace/workspace.js 存在真实语法错误: submitRule 函数(约 L500–539)缺一个闭合的 }。

  • node --check 会给出假通过(exit 0);
  • 只有真实 ESM 解析(import() / vm.SourceTextModule)才暴露 SyntaxError: Unexpected end of input;
  • 已用 vm.SourceTextModule 扫全部 44 个前端模块,只有这一个失败。

结论:管理员工作台的 workspace.js 会整份加载失败。


17. 注意事项汇总(易错点清单)

按"会不会让人踩坑"排序。

🔴 会导致功能不可用

  1. 必须有常驻 Worker:python -m app.worker。否则 agent_run 永远 queued, 前端只显示"客服响应超时"。排查第一步:查 agent_run 最新那行是不是 queued。
  2. 行情有效期只有 15 分钟(MAX_QUOTE_AGE)。超时后所有委托一律 503, 且无自动刷新。补刷:python tools/sync_market_prices.py(立即生效、无需重启)。
  3. config_release 是环境数据、不随代码合并。"白名单已发布"必须带环境限定, 换环境要重发。发布脚本必须覆盖同 key 的继承项,否则旧值会把整次发布 422 拦下; 继承范围必须覆盖全部三张受管表(曾把 customer_service_chitchat 提示词 静默漏在旧版本里,靠 Agent 侧代码默认值兜底,零告警)。
  4. 知识类意图要同时发 search_knowledge 与 query_knowledge —— 前者给登录客户,后者给访客令牌(访客只有 knowledge:query)。 缺哪一条,对应人群就一问即失败。
  5. RBAC 权限码的定义源是 tools/seed_test_rbac.py 的 PERMISSIONS(9001–9046)。 它是 DELETE 重建语义;没并进它的权限码重建一次就没了, 表现是"接口突然 403 而没有任何报错线索"。

🟠 环境相关(换机器必踩)

  1. Milvus 集合 schema 因环境而异(字段名不同)。 不要在任何地方硬编码字段名 —— 检索层已改为运行时探测 (app/core/knowledge_schema.py)。
  2. start.ps1 必须存为 UTF-8 with BOM。缺 BOM 时 PowerShell 5.1 按 GBK 解析, 中文注释直接抛 Unexpected token 语法错误。
  3. 解释器探测必须实测「能 import 依赖」,不能只看 --version 成功。 门槛:≥ 3.11 + import fastapi, sqlalchemy, asyncmy, pydantic 通过。 曾经因此选中 Python 3.10(项目用 datetime.UTC,3.11+ 才有), 报错却发生在行情刷新那一步,看起来像"行情源坏了"。
  4. 启动金融Agent平台.bat 不要手写 —— 必须由 python tools/make_launcher_bat.py 生成,且同时满足 GBK 编码 + CRLF 换行 + 无 BOM,缺任何一条 cmd 都会解析错乱。

🟡 API 契约细节

  1. meta.trace_id ≠ data.trace_id(R001):前者是本次请求,后者是该 run 自己。
  2. today_profit_loss 恒为 0(T001 / T006)—— 硬编码占位值 (app/service/trade_service.py L678 持仓列表、L748-749 账户看板), 不要当真实当日盈亏展示。注意同一响应里的 profit_loss(持有盈亏)是真算的。 是否本期实现待业务确认 → 见 docs/软件需求文档-2026-09-14.md Q22 (含两条口径的可行性实测:行情基准不可行、净值基准可行)。
  3. change_pct 可能为 null(P001)—— 必须显示"暂无",绝不可当 0。
  4. P002 净值表为空返回 count = 0,不是错误。
  5. T005 撤单几乎总是 409 —— 市价单下单即成交,只有 "待风控" 可撤。 前端应友好提示"该委托已成交,无法撤销"。
  6. T008 成交不存在也报 ORDER_NOT_FOUND(不是独立的成家错误码)。
  7. 风控 limit 上限极小:预警 5,证据/通知 10。
  8. 风控时间是裸 datetime,按北京时间解释;Agent 路径带时区。 两条路径口径必须一致,否则界面与对话查出的结果不同。
  9. 风控 levels 的键是中文(高风险/中风险/低风险)。
  10. RiskEvidenceSource 的 8 个值必须与前端 EVIDENCE_COLUMNS 一致, 传其它值 404。
  11. RUN_CANCELLED 不是 HTTP 错误码,它是 request_idempotency 的状态标记, 没有异常类。
  12. 投顾目标书流转失败复用 RUN_NOT_CANCELLABLE(409)—— 已知文档缺口, 语义上应为 INVALID_STATE。
  13. 场外全套用旧式信封 {code, message, data},不是统一信封。 成功判断是 code === 0。
  14. 场外权限是 any-of 语义(交集非空),不是 require。
  15. 场外附件下载可能返回文件或 JSON,需按 Content-Type 分支。
  16. 推介材料的 code 在 body 里(422/503),HTTP 状态码仍是 200。
  17. ReplyScene 必须与数据库 CHECK 约束一致,不一致时以 500 暴露而非 422。
  18. ItemPayload.namespace 必须包含 fund_market,否则行情配置 422。
  19. RoutingPayload.max_attempts 不得超过端点数量。
  20. EndpointPayload.secret_ref 只接受 env:XXX 格式。
  21. 凡接受 If-Match 的资源,必须同时提供能返回该 etag 的读取路径 (A048/A049)。缺了会变成"首次编辑必然 409"的死锁。
  22. /products、/knowledge-references/{token} 等公开面端点要求"合法令牌但无权限码" —— 访客令牌可用,无令牌不行(401)。
  23. 交易域没有限流(T 系列低频),其余业务域有。
  24. /api/tasks/{task_id}/trigger-agent-nl2sql 没有 v1(前缀是 /api)。

🟢 顺序与安全(设计如此,不要"优化"掉")

  1. Agent 事件订阅:先查可见性,后查 Accept —— 反过来会让 406-vs-404 变成存在性探针。
  2. 风控日报流:先鉴权,后 Accept —— async generator 的函数体到第一次迭代才执行,那时响应头已发出, 403/406 只能变成"200 + 半截流"。
  3. 游标校验在权限闸门之后 —— 未授权一律 403, 不能因参数格式先漏一个 400(响应差异是越权探测信号)。
  4. 登录失败永不区分原因,且用户不存在时做一次 dummy bcrypt 对比。
  5. 风控扫描锁加在入口层,不能放 Service 里(会把定时扫描自己挡死)。
  6. 风控幂等 scope 用实际路径(含 alert_no),不是模板 —— 否则同一键会在不同预警间互相回放。
  7. 会话/委托/成交查询都带 customer_id 过滤, 别人的资源一律 404 而非 403。

⚙️ 运行平台

  1. /internal/** 三个端点全部无需鉴权,且每个都不走统一信封。
  2. live 只返回 {"status":"ok"},ready 才做真实探测。
  3. ready 的 Milvus 探测是真实的(B3 修复前是硬编码 True, 属于误导性探针);pymilvus 缺失返回 client_missing。

18. 端点总表

按 docs/05 §19 编号排列。X0xx 为本文件新增(见 §0 说明)。

Agent 运行(R)

编号 方法与路径 权限 幂等 成功
R001 POST /api/v1/agent-runs 按 agent_type 是 202
R002 GET /api/v1/agent-runs/{run_id} 按域 — 200
R003 GET /api/v1/agent-runs/{run_id}/events 按域 — 200 (SSE)
R004 POST /api/v1/agent-runs/{run_id}/cancellations agent:cancel 是 202

会话(C)

编号 方法与路径 权限 幂等 成功
C001 POST /api/v1/conversations conversation:create 是 201
C002 GET /api/v1/conversations/{session_id} conversation:read — 200
C003 GET /api/v1/conversations/{session_id}/messages conversation:read — 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} handover:create — 200
C007 POST /api/v1/conversation-messages/{message_id}/feedback conversation:feedback 是 201

记忆(M)

编号 方法与路径 权限 成功
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

新人引导(O)

编号 方法与路径 权限 成功
O001 GET /api/v1/onboarding/risk-questionnaire 登录即可 200
O002 POST /api/v1/onboarding/risk-questionnaire/submissions 登录即可 201
O003 (闸门,非端点) — 403 ONBOARDING_REQUIRED

知识库(K)

编号 方法与路径 权限 成功
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

场内交易(T)

编号 方法与路径 权限 成功
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
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 trade:txn:read 200

公开面(P)

编号 方法与路径 权限 成功
P001 GET /api/v1/products 令牌即可(无权限码) 200
P002 GET /api/v1/products/{product_code}/nav-history 令牌即可(无权限码) 200
V001 POST /api/v1/visitor-tokens 无 201

投顾(AD)

编号 方法与路径 权限 成功
AD001 POST /api/v1/advisor/investment-goals investment-goal:write 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 200
AD004 POST /api/v1/advisor/investment-goals/{goal_no}/confirmations investment-goal:confirm 200
AD005 GET /api/v1/advisor/investment-goals/{goal_no}/goal-book investment-goal:read 200
AD006 POST /api/v1/advisor/investment-goals/{goal_no}/goal-book/reviews admin 200
AD007 POST /api/v1/advisor/investment-goals/{goal_no}/goal-book/publications 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 201
AD011 GET /api/v1/advisor/recommendations/published product-recommendation:read:self 200

风控(R 前缀之外,/api/v1/risk)

方法与路径 权限
GET /api/v1/risk/overview risk:alert:read
GET /api/v1/risk/alerts risk:alert:read
POST /api/v1/risk/alerts/scan risk:alert:scan
POST /api/v1/risk/alerts/{alert_no}/acknowledgements risk:alert:write
POST /api/v1/risk/alerts/{alert_no}/investigations risk:alert:write
POST /api/v1/risk/alerts/{alert_no}/exclusions risk:alert:write
POST /api/v1/risk/alerts/{alert_no}/resolutions risk:alert:write
POST /api/v1/risk/alerts/{alert_no}/escalations risk:alert:write
POST /api/v1/risk/alerts/{alert_no}/evidence risk:alert:write
GET /api/v1/risk/alerts/{alert_no} risk:alert:read
GET /api/v1/risk/evidence/{source} risk:alert:read
GET /api/v1/risk/notifications risk:alert:read
POST /api/v1/risk/daily-report risk:alert:read
POST /api/v1/risk/daily-report/stream risk:alert:read
POST /api/v1/risk/daily-report/mail risk:report:mail

平台管理(A)

编号 方法与路径 权限 成功
A001–A0xx 每个资源的 POST/GET/GET-detail/PUT(表驱动,见 §11.1) config:read / config:write / model-endpoint:manage 201 / 200
A015–A0xx 状态流转 POST /{resource}/{id}/{action}(见 §11.2) config:review / config:activate 200 / 201(rollbacks)
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
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 201
A042 GET /api/v1/admin/advisor/profile-tags profile-governance:read 200
A043 GET /api/v1/admin/advisor/profile-drift-reviews profile-governance:read 200
A044 POST /api/v1/admin/advisor/profile-drift-reviews/{review_id}/reviews profile-governance:review 200
A045 POST /api/v1/admin/advisor/recommendations/{content_id}/reviews product-recommendation:review 200
A046 POST /api/v1/admin/advisor/recommendations/{content_id}/publications product-recommendation:publish 200
A047 GET /api/v1/admin/advisor/pending-contents product-recommendation:review 200
A048 GET /api/v1/admin/config-releases/{release_id}/platform-config-items/{item_id} config:read 200
A049 GET /api/v1/admin/config-releases/{release_id}/model-routing-rules/{rule_id} config:read 200
— GET /api/v1/admin/customer-service/handover-tickets 管理员 200
— GET /api/v1/admin/customer-service/handover-tickets/{ticket_no} 管理员 200

场外运营(X,本文件编号)

编号 方法与路径 权限组合
X001 GET /api/v1/offsite-fund/mails offsite:read / offsite:write
X002 GET /api/v1/offsite-fund/mails/{mail_id} 同上
X003 POST /api/v1/offsite-fund/mails/{mail_id}/deletions offsite:write
X004 GET/PUT /api/v1/offsite-fund/mails/{mail_id}/recognition-fields read/write、write
X005 GET/PUT /api/v1/offsite-fund/documents/{task_id}/nl2sql-fields read/write、write
X006 GET /api/v1/offsite-fund/documents/{task_id}/rule-results read/write
X007 POST /api/v1/offsite-fund/documents/{task_id}/rule-results/recalculations offsite:write
X008 GET /api/v1/offsite-fund/mailbox-status read/write
X009 POST /api/v1/offsite-fund/mailbox-status/recoveries offsite:write
X010 GET /api/v1/offsite-fund/attachments/{attachment_id}/file read/write
X011 POST /api/v1/offsite-fund/documents/{task_id}/confirmations write + confirm
X012 POST /api/v1/offsite-fund/documents/{task_id}/recognition-retries offsite:write
X013 POST /api/v1/offsite-fund/documents/{task_id}/notifications notify / write
X014 POST /api/v1/offsite-fund/notifications/{notification_id}/send notify / write
X015 POST /api/v1/offsite-fund/settlement-statistics/recalculate offsite:write
X016 POST /api/tasks/{task_id}/trigger-agent-nl2sql offsite:write

推介材料(X,本文件编号)

编号 方法与路径 权限
X017 POST /api/v1/fund-promotion-materials promotion:write
X018 PUT /api/v1/fund-promotion-materials/{task_no}/inputs promotion:write
X019 POST /api/v1/fund-promotion-materials/{task_no}/attachments promotion:write
X020 POST /api/v1/fund-promotion-materials/{task_no}/generations promotion:write
X021 GET /api/v1/fund-promotion-materials/{task_no}/compliance-checks promotion:read
X022 GET /api/v1/fund-promotion-materials/{task_no} promotion:read
X023 POST /api/v1/fund-promotion-materials/{task_no}/reviews promotion:review
X024 POST /api/v1/fund-promotion-materials/{task_no}/deliveries promotion:deliver

运维

方法与路径 鉴权 成功
GET /internal/health/live 无 200
GET /internal/health/ready 无 200 / 503
GET /internal/metrics 无 200(text/plain)

附:统计口径

项 数量
路由模块 22 个(app/main.py 的 include_router)
显式声明的端点函数 约 110 个
表驱动生成的端点 register_resource 8 个资源 × 2–4 个方法 ≈ 22 个;register_transition 11 个
端点总数(估算) 约 140 个
错误码 36 个(app/core/errors.py)
业务 Agent 7 个

端点总数是估算:表驱动端点由循环动态注册,精确计数需在运行时遍历 app.routes。可用如下方式核对:

python -c "from app.main import app; print(len([r for r in app.routes if hasattr(r,'methods')]))"

附:与 docs/05 的关系

本文档不替代 docs/05-接口文档.md。分工建议:

想知道 看哪份
某个接口为什么这样设计、背后的约束与事故 docs/05(权威)
接口的请求/响应字段逐个含义、示例、注意事项 本文档
端点编号与权限/幂等/审计的规范表 docs/05 §19
场外运营与推介材料(docs/05 §19 未登记) 本文档 §13、§14

⚠️ 维护提示:若代码变更涉及端点增删、字段改名或错误码调整, 应同步更新本文档与 docs/05。本文档的"注意事项汇总"(§17)是从源码注释中提炼的, 源码里那段注释往往就是一次事故的记录 —— 改代码前请先读它。