模块 1 · 路由地图

v0.1 路由:
customers / products / advisors / compliance

代销平台 API 是「读 Core 模拟库」的 REST 门面。路由按业务域切分,不用 /api/platform/* 前缀。 和 Agent 对话线(/api/chat)是两条平行轨道——指挥 AI 加「查持仓」时,先确认走哪条。

四块 canonical 域(以本文为准)

/api/customers/*

客户 L0 档案、持仓、流水。平台客户 REST 薄路由(customers.py) → platform/customer_service → core_ro。

/api/products/*

产品列表、净值曲线。理财师/客户看产品详情都走这里,不另开 Agent Tool HTTP。

/api/advisors/*

理财师名下客户归属。advisor 只能查本人 roster,风控/分析员可全量只读。

/api/compliance/*

适当性判定等合规读接口。canonical:Agent 合并期改调同一 Service,不保留第二套路径。

重复能力以谁为准? 以本平台 API 为准。Agent 侧已有同能力 Tool 时,合并期改调 app/service/platform/,旧路径仅过渡。

三层分工(别让 AI 在路由里写 SQL)

🌐api/*.py
🔑deps 鉴权
📦platform Service
🗄core_ro

点击「下一步」看一层层往下走

平台客户 REST 薄路由(customers.py)
"""代销平台 · 客户 L0 与资产读 API(canonical · 不要求 X-Agent-Type)。"""
@router.get("/{customer_id}/holdings")
def list_holdings(
    customer_id: str,
    auth: AuthContext = Depends(get_platform_auth_context),
):
    assert_platform_customer_access(auth, customer_id, ...)
    return ok(platform_service.list_holdings(customer_id))
白话

文件头就写明:这是平台 canonical 读接口,不要 X-Agent-Type。

URL 用复数 customers + 嵌套 holdings,参数名和域 ID 一致。

鉴权用平台专用函数,不是 chat 那条 get_auth_context。

先断言「你有没有权看这个 customer_id」,再调 Service,路由里不出现 SQL。

理财师 Agent 也要查客户持仓,应该新建 /api/agent/holdings 吗?

模块 2 · 平台鉴权

get_platform_auth_context:
只要 JWT,不要 X-Agent-Type

对话线(/api/chat)必须带 X-Agent-Type; 平台读 API 故意不要——App 和仪表盘只证明「你是谁」,不声明「你在敲哪扇 Agent 窗」。

和 chat 鉴权的对照表

场景函数X-Agent-Type归属断言
平台 RESTget_platform_auth_context不要assert_platform_customer_access
对话 / 风控 RESTget_auth_context必须assert_customer_access + 准入矩阵
指挥 AI 接前端时: apiFetch 读持仓只带 Bearer;若误加 X-Agent-Type: customer,平台路由会忽略,但混进 chat 路由就会多一层校验甚至 401。

群聊:前端问「张三持仓多少」

理财师工作台发 GET 持仓——只走平台三层,不进 LangGraph。

deps.py
def get_platform_auth_context(request: Request) -> AuthContext:
    """平台 API 鉴权:JWT 通道 **不要求** X-Agent-Type。"""
    auth_header = request.headers.get("Authorization", "")
    if auth_header[:7].lower() == "bearer ":
        claims = verify_token(token)
        return _bind_state(request, _claims_to_auth(claims))
    # dev:X-Debug-Role / X-Debug-Actor 兜底
白话

函数名就写死:平台鉴权,不读 X-Agent-Type。

有 Bearer 就验 JWT,解析出 actor_id 和 roles。

本地开发没 JWT 时,可用 debug 头冒充身份(仅 development)。

和 get_auth_context 不同:后者在 JWT 通道会强制校验 X-Agent-Type + 准入矩阵。

分析员「问数工作台」调 /api/analyst/chat,要带 X-Agent-Type: analyst 吗?

模块 3 · 出参脱敏

PLATFORM_RESPONSE_DESENSITIZE:
模拟库原样,真库再遮

环境变量 PLATFORM_RESPONSE_DESENSITIZE 控制平台 API 出参是否打码姓名/手机/证件。 v0.1 默认关——本地 Core 模拟库联调看原值;接真 Core 再开,在 Platform Service 统一出口处理。

开关行为一览

开关模拟库联调生产 / 真 Core
false(默认)L0 字段原样返回—
true—姓名/手机/证件/银行卡打码;customer_id 不脱敏
实现位置: 响应脱敏(app/service/platform/common.py)的 maybe_desensitize_row → prepare_row。REST 与 Agent 适配层都应走这里,不要在路由里手写 mask。
响应脱敏(platform/common.py)
def maybe_desensitize_row(row: dict | None) -> dict | None:
    if row is None or not settings.platform_response_desensitize:
        return row
    for key, masker in (
        ("display_name", d.mask_name),
        ("mobile_phone", d.mask_phone),
        ("id_card_no", d.mask_id_card),
        ...
    ): ...
def prepare_row(row):
    return to_jsonable(maybe_desensitize_row(row) or row)
白话

开关关着 → 数据库行原样返回,方便本地对账。

开关开着 → 按字段名找 mask 函数,逐个打码。

prepare_row 是统一出口:先脱敏,再把 Decimal/日期转成 JSON 能序列化的类型。

指挥 AI 加新字段时,若含 PII,记得在这里登记 masker。

自检:你能否指挥 AI 正确切换?

准备接真 Core,要让出参打码手机号,最省事的做法是?

脱敏开启后,customer_id 会被打码吗?

模块 4 · 接缝与草案

适当性只准一条 canonical
还有空壳与行情草案

平台 API 是 v0.1 的「对外标准」,但仓库里已有重复能力、空壳路由、Phase B 草案。 指挥 AI 加第三条适当性 URL 或前端直连行情源,都会踩接缝。

路由补全:staff 与空壳

已实现 · v0.1 四域 + staff

/api/customers · products · advisors · compliance · staff

空壳 · 一期不做

知识库空壳 API(knowledge.py) · 管理空壳 API(admin.py) — T-21 拍板:脚本入库,无上传/重建 HTTP。

适当性:两条 URL,别再开第三条

canonical(平台)
POST /api/compliance/suitability-check
→ get_platform_auth_context
→ app/service/platform/
风控专用(对话/演示)

POST /api/risk/suitability/check — 风控 REST + X-Agent-Type: risk。

客服 suitability_check Tool 走 core_ro.check_suitability,是第三条对话内路径,但底层同一判定内核。

新增 HTTP 路由前先查契约迁移表,避免四个入口四个口径。

行情 Phase B(草案 · 未落地)

v0.2 草案:nav-snapshot · sync_market_nav。C-05 硬规则:禁止爬虫、禁止前端直连第三方行情——净值须走后端同步管道。

联调时响应是明文 ID,AI 说「脱敏坏了」,你怎么回?