## 文档移动(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` 退回。)
111 KiB
后端接口文档(全量梳理版)
生成日期: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. 阅读指引与端点编号约定
- 1. 全局约定
- 2. 公开面:访客令牌与产品目录
- 3. 认证:登录
- 4. 会话 / 消息 / 反馈 / 转人工
- 5. Agent 运行(异步三段式)
- 6. 记忆与画像候选
- 7. 新人引导(风险测评)
- 8. 知识库管理
- 9. 场内基金交易
- 10. 风控
- 11. 平台管理(配置 / 审计 / RBAC)
- 12. 投顾(目标书 / 组合分析 / 资产配置 / 推荐)
- 13. 场外基金运营
- 14. 基金推介材料
- 15. 运维与健康检查
- 16. 前端约定与静态度量
- 17. 注意事项汇总(易错点清单)
- 18. 端点总表
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。
- 场外运营的 Agent 触发端点在
- 请求体默认
application/json;只有两处用multipart/form-data: 风控证据上传(§10POST /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 |
三个易错点:
- 访客跳过身份解析。
build_request_context对 visitor 角色不走IdentityService.resolve, 直接放行。所以访客令牌从不需要在sys_user里存在。 - 引导闸门。已登录客户访问非
/api/v1/onboarding/**的接口时,会被RiskQuestionnaireService.is_required判定;需要则抛ONBOARDING_REQUIRED(403)。 这是"客户一登录发现哪都调不通"的常见原因。 - 登录接口本身不依赖
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>"。不匹配 → 409RESOURCE_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 层。
⚠️ 本域的三条硬约束
limit上限极严格:/risk/alerts最大 5(默认也是 5);/risk/evidence/{source}、/risk/notifications最大 10。- 扫描锁加在入口层(
mysql_scan_lock()),拿不到锁 →RiskScanBusyError。 - 写操作的
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 |
⚠️ 三重前置校验:
version.status必须是pending_review,否则 422「当前材料版本不在待审核状态」;- 存在阻断级合规问题 → 422「存在阻断级合规问题」;
- 否则可审。
响应:{"task_no", "material_version_id", "status"}。
X008 发送(PromotionDeliveryRequest)
| 字段 | 约束 |
|---|---|
material_version_id |
gt=0 |
advisor_ids |
1–100 个 |
delivery_channel |
默认 "internal_record" |
⚠️ 两条前置校验:
version.status必须是approved,否则 422「只有审核通过的材料可以发送」;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. 注意事项汇总(易错点清单)
按"会不会让人踩坑"排序。
🔴 会导致功能不可用
- 必须有常驻 Worker:
python -m app.worker。否则agent_run永远queued, 前端只显示"客服响应超时"。排查第一步:查agent_run最新那行是不是queued。 - 行情有效期只有 15 分钟(
MAX_QUOTE_AGE)。超时后所有委托一律 503, 且无自动刷新。补刷:python tools/sync_market_prices.py(立即生效、无需重启)。 config_release是环境数据、不随代码合并。"白名单已发布"必须带环境限定, 换环境要重发。发布脚本必须覆盖同 key 的继承项,否则旧值会把整次发布 422 拦下; 继承范围必须覆盖全部三张受管表(曾把customer_service_chitchat提示词 静默漏在旧版本里,靠 Agent 侧代码默认值兜底,零告警)。- 知识类意图要同时发
search_knowledge与query_knowledge—— 前者给登录客户,后者给访客令牌(访客只有knowledge:query)。 缺哪一条,对应人群就一问即失败。 - RBAC 权限码的定义源是
tools/seed_test_rbac.py的PERMISSIONS(9001–9046)。 它是 DELETE 重建语义;没并进它的权限码重建一次就没了, 表现是"接口突然 403 而没有任何报错线索"。
🟠 环境相关(换机器必踩)
- Milvus 集合 schema 因环境而异(字段名不同)。
不要在任何地方硬编码字段名 —— 检索层已改为运行时探测
(
app/core/knowledge_schema.py)。 start.ps1必须存为 UTF-8 with BOM。缺 BOM 时 PowerShell 5.1 按 GBK 解析, 中文注释直接抛Unexpected token语法错误。- 解释器探测必须实测「能 import 依赖」,不能只看
--version成功。 门槛:≥ 3.11 +import fastapi, sqlalchemy, asyncmy, pydantic通过。 曾经因此选中 Python 3.10(项目用datetime.UTC,3.11+ 才有), 报错却发生在行情刷新那一步,看起来像"行情源坏了"。 启动金融Agent平台.bat不要手写 —— 必须由python tools/make_launcher_bat.py生成,且同时满足 GBK 编码 + CRLF 换行 + 无 BOM,缺任何一条 cmd 都会解析错乱。
🟡 API 契约细节
meta.trace_id≠data.trace_id(R001):前者是本次请求,后者是该 run 自己。today_profit_loss恒为 0(T001 / T006)—— 硬编码占位值 (app/service/trade_service.pyL678 持仓列表、L748-749 账户看板), 不要当真实当日盈亏展示。注意同一响应里的profit_loss(持有盈亏)是真算的。 是否本期实现待业务确认 → 见docs/软件需求文档-2026-09-14.mdQ22 (含两条口径的可行性实测:行情基准不可行、净值基准可行)。change_pct可能为null(P001)—— 必须显示"暂无",绝不可当0。- P002 净值表为空返回
count = 0,不是错误。 - T005 撤单几乎总是 409 —— 市价单下单即成交,只有
"待风控"可撤。 前端应友好提示"该委托已成交,无法撤销"。 - T008 成交不存在也报
ORDER_NOT_FOUND(不是独立的成家错误码)。 - 风控
limit上限极小:预警 5,证据/通知 10。 - 风控时间是裸 datetime,按北京时间解释;Agent 路径带时区。 两条路径口径必须一致,否则界面与对话查出的结果不同。
- 风控
levels的键是中文(高风险/中风险/低风险)。 RiskEvidenceSource的 8 个值必须与前端EVIDENCE_COLUMNS一致, 传其它值 404。RUN_CANCELLED不是 HTTP 错误码,它是request_idempotency的状态标记, 没有异常类。- 投顾目标书流转失败复用
RUN_NOT_CANCELLABLE(409)—— 已知文档缺口, 语义上应为INVALID_STATE。 - 场外全套用旧式信封
{code, message, data},不是统一信封。 成功判断是code === 0。 - 场外权限是 any-of 语义(交集非空),不是
require。 - 场外附件下载可能返回文件或 JSON,需按
Content-Type分支。 - 推介材料的
code在 body 里(422/503),HTTP 状态码仍是 200。 ReplyScene必须与数据库 CHECK 约束一致,不一致时以 500 暴露而非 422。ItemPayload.namespace必须包含fund_market,否则行情配置 422。RoutingPayload.max_attempts不得超过端点数量。EndpointPayload.secret_ref只接受env:XXX格式。- 凡接受
If-Match的资源,必须同时提供能返回该 etag 的读取路径 (A048/A049)。缺了会变成"首次编辑必然 409"的死锁。 /products、/knowledge-references/{token}等公开面端点要求"合法令牌但无权限码" —— 访客令牌可用,无令牌不行(401)。- 交易域没有限流(T 系列低频),其余业务域有。
/api/tasks/{task_id}/trigger-agent-nl2sql没有v1(前缀是/api)。
🟢 顺序与安全(设计如此,不要"优化"掉")
- Agent 事件订阅:先查可见性,后查
Accept—— 反过来会让 406-vs-404 变成存在性探针。 - 风控日报流:先鉴权,后 Accept —— async generator 的函数体到第一次迭代才执行,那时响应头已发出, 403/406 只能变成"200 + 半截流"。
- 游标校验在权限闸门之后 —— 未授权一律 403, 不能因参数格式先漏一个 400(响应差异是越权探测信号)。
- 登录失败永不区分原因,且用户不存在时做一次 dummy bcrypt 对比。
- 风控扫描锁加在入口层,不能放 Service 里(会把定时扫描自己挡死)。
- 风控幂等
scope用实际路径(含alert_no),不是模板 —— 否则同一键会在不同预警间互相回放。 - 会话/委托/成交查询都带
customer_id过滤, 别人的资源一律 404 而非 403。
⚙️ 运行平台
/internal/**三个端点全部无需鉴权,且每个都不走统一信封。live只返回{"status":"ok"},ready才做真实探测。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)是从源码注释中提炼的,
源码里那段注释往往就是一次事故的记录 —— 改代码前请先读它。