Files
group_fqcd_jr/docs/演示用/全功能流程-大白话版.md
T
lzf_0626 076d786bc6 补齐客服转人工工单流:从"只能看"到"能推进"(基线状态机,不自行发明)
## 问题
`svc_handover_ticket` 的 DDL 与状态机在 `docs/02` §7.2 早就定好了
(pending → assigned → processing → resolved → closed,未解决可 cancelled),
但平台**只有 handover:read(只读队列)**:没有任何入口能改状态、assigned_to /
accepted_at / resolved_at / closed_at / resolution 五列**全库 0 非空**,
于是 40 张工单永远停在 pending —— 用户看到的就是"工单全都长一样"。

## 改了什么
后端:
- 新增 `app/service/customer_service_handover_action_service.py`:五个动作
  (分配/接单/解决/关闭/取消),`SELECT ... FOR UPDATE` 锁单后判状态;
  接单允许从 pending 自助接管(同时记受理人);取消不写 closed_at(该列属 closed 状态);
  每次流转写一条 interaction_audit(handover.assigned/accepted/resolved/closed/cancelled);
  非法流转 409、坐席不存在 422、工单不存在 404;回包不含 customer_id/session_id。
- 只读服务保持只读(读侧与写侧是两条边界,单测守着"读侧不许长出写方法"),
  但列表支持 `?status=` 六态筛选、详情补上受理人与流转时间(坐席侧路由信息,非客户数据)。
- `app/api/controllers/admin.py`:五个 action 端点 A049–A053
  (assignments / acceptances / resolutions / closures / cancellations),
  走 `ApiTransactionService.execute_in` —— 幂等记录与业务写入同事务、重复键回放。
- 权限:新增 `handover:write`(9069,只授 admin),已并进种子
  `tools/seed_test_rbac.py`;配套幂等脚本 `tools/grant_handover_write_permission.py`。

前端(管理员工作台 · 转人工工单页):
- 按状态给按钮(待处理→分配/直接接单、已分配→接单、处理中→解决、已解决→关闭、
  未解决都可取消),加了状态筛选与"刷新";摘要弹窗补上受理人与四个时间点、处置结论。
- api-client 注册五个端点;workspace.js 的 api-client 引用与页面自身的 ?v= 一并升版,
  避免浏览器拿旧缓存(旧缓存里没有这些端点)。

冒烟与测试:
- `tools/e2e_smoke_test.py`:B 段建的测试工单由 F 段走完 分配→接单→解决→关闭 收尾
  —— 既不再把测试件堆在 pending 队列里(此前每次冒烟攒一张),又让每次冒烟都覆盖一遍状态机。
  总数 40 → 44 项,实测 44/44 全绿。
- 新增单测 24 条(状态机合法/非法路径、越权、坐席不存在、审计、视图不泄漏客户标识)
  与一条真机集成用例(HTTP 十步 + 数据库侧审计证据 + 自动清理)。
- 读侧那条"详情不得返回 assigned_to"的旧断言按新口径更新,并写清为什么。

## 验证
- `pytest tests/unit tests/contract` → 1489 passed, 2 skipped, 0 failed
- 新增集成用例通过;`tests/integration` 全量跑时
  `test_memory_extraction` / `test_run_cancellation_mysql` 两条偶发红 —— 单独跑都通过,
  是 AGENTS.md 已登记的"常驻 Worker 抢队列"(跑验收前须先停 Worker)
- `tools/portal_api_check.py` → 41 项通过 39、失败 0
- `tools/e2e_smoke_test.py` → 44/44 全通过
- `python tools/check_rbac_seed_consistency.py` → 通过(种子 63 条权限)
- 真机 HTTP 实测:分配→接单→解决→关闭四步 200 且时间戳齐全;取消路径 200 且 closed_at 为空;
  同键重发回放不二次推进;对已关闭工单再分配 409;风控账号处置 403

## 文档
`docs/44-演示流程.md`(场景 4/8 + 命令 + 44 项)、`docs/演示用/后端接口文档`(新增 §11.4b 与
A049–A053)、`docs/演示用/全功能流程-大白话版.md`(工单页签改"读写"+ 已知偏差)、
`AGENTS.md`(9066-9069 号段演进 + 冒烟 44 项)
2026-09-15 00:41:59 +08:00

50 KiB
Raw Blame History

全平台功能流程 · 大白话版(2026-09-14)

读者:第一次接触这个平台的人(演示听众 / 接手的人 / 业务方)。 写法:不用"链路""编排""收敛""双写"这类词,讲的是"点一下之后到底发生了什么"。 每一句说法都能在代码或数据库里查到,关键处标了文件路径,方便你反查。 配套:想照着演一遍看 docs/44-演示流程.md;只想看「今日盈亏」看 今日盈亏实现说明-2026-09-14.md。


0. 三十秒版:这平台到底是什么

  1. 它是一个基金业务的"通用 Agent 平台":把"会话、登录鉴权、记忆、模型调用、工具调用、 合规校验、审计留痕、配置发布"这些每个业务都要用的东西做成底座, 上面挂着 7 个业务 Agent 和一条不做 AI 的模拟交易服务。
  2. 不做真实交易:场内基金是模拟盘(钱是假钱),但行情是真行情(取自公开数据源)。 场外基金那条线只做"运营流程"(收邮件、认单据、发通知),同样不下真实单。
  3. 对外一共六个门(门户),每个角色一个:
门户 地址 谁能进 一句话作用
访客 /portal/guest/home/ 任何人(不用登录) 看产品、问客服
客户 /portal/customer/login/ 客户 看资产、下单、看流水、做风险测评
风控 /portal/employee-risk/dashboard/ 风控专员 处置预警、出日报
投顾 /portal/employee-advisor/dashboard/ 投顾 给自己服务的客户出方案
运营 /portal/employee-operations/offsite/ 运营 场外收件、推广材料、问数(NL2SQL)
管理员 /portal/employee-console/workspace/ 管理员 权限、配置发布、审计、工单

员工侧只有一个登录页:/portal/employee-console/login/。 登录后系统按你的角色把你送到上面四个工作台里的某一个(见 §3)。 客户侧有自己的登录页 /portal/customer/login/,员工账号在客户登录页会被拒绝,反之亦然。


1. 跑起来一共三个"人":浏览器、API、Worker

看懂这三者的分工,后面所有的"为什么没反应"都能自己判断。

角色 是什么 少了它会怎样
浏览器 六个门户的页面(原生 JS,没有前端框架)。它只做两件事:调接口、把结果画出来 没有界面(但接口还能用 curl 调)
API python -m uvicorn app.main:app --port 8000。所有接口 + 所有页面都从它出 什么都没有
Worker python -m app.worker。后台慢慢干的活都归它 客服对话永远"繁忙/超时";新知识搜不到;记忆不更新;而且它不跑通常一声不响

Worker 具体干哪些活(都在 app/worker/runtime.py 的 run_once() 和 app/worker/__main__.py 里,一轮一轮轮询):

  1. Agent 运行:把"排队中"的 Agent 请求领出来真正执行(这一条最关键,见下);
  2. 领域事件投递(domain_event_outbox):事件先落库、再由它一条条消费,失败就重试、重试到头进死信;
  3. 知识向量同步:把知识库内容写进 Milvus(不写进去,客服就检索不到);
  4. 记忆抽取与画像重建:从对话里提炼长期记忆 → 重建画像 → 投到 Neo4j(关系)与 Milvus(向量);
  5. 会话片段聚合(把若干轮对话合成一个"事件片段",供长期记忆用);
  6. 清理派生数据:记忆失效/销户时删掉图库与向量库里的副本,删不掉就如实记"跳过",不假装删了;
  7. 场外收件:OffsiteMailWorker 在同一个进程、同一个入口里跑(邮箱收信、认单据)。

⚠️ "客服一直转圈/超时" 十有八九是 Worker 没在跑。 因为 Agent 请求是三段式:API 只负责受理并返回 202("收到了")→ 请求进队列 (agent_run.status='queued')→ Worker 领单才真正执行,结果再落库。 没有 Worker,队列里那行就一直停着,而页面只会显示"客服繁忙/响应超时"—— 看起来像慢,其实是没人处理。一句话排查:查 agent_run 最新那行是不是 queued。

数据库和中间件(都在本机):

组件 端口 干什么
MySQL 3306 唯一权威数据源,90 张表
Redis 6379 配置缓存、客服会话短期上下文
Milvus 19530 向量检索(知识库片段、长期记忆向量)
Neo4j 7687 客户画像的关系(标签、产品、多跳),是派生视图,一律以 MySQL 为准

一键启动:双击桌面的 启动金融Agent平台.bat(或 powershell -File start.ps1)—— 它按顺序做六件事:找解释器 → 检查 MySQL/Redis/Milvus → 刷行情 → 起 API 与 Worker 两个窗口 → 等 API 真正应答 → 自动打开浏览器。重复双击是安全的,不会起第二份。 ⚠️ 那个 bat 必须同时满足"GBK 编码 + CRLF 换行 + 无 BOM",所以别手写, 改完 start.ps1 后跑 python tools/make_launcher_bat.py 重新生成。


2. 第一眼:访客门户(不用登录就能看)

打开 http://127.0.0.1:8000/portal/,它会跳到 /portal/guest/home/。三个页面:

页面 看到什么 数据从哪来
首页 平台的宣传页 + 推荐位(前 3 只基金) 推荐位来自 GET /api/v1/products,其余静态
产品列表 场内基金清单、最新价、"最近涨跌";还能切"基金排行"视图 GET /api/v1/products(真实接口,不是写死的假数据)
产品详情 产品要素 + 净值走势图 净值来自 fin_nav_history(当前库里 26 只产品、3253 个净值日)

"最近涨跌"有个刻意的设计:这个数只显示行情源直接给的涨跌幅。 源没给、或这只产品走的是净值降级路径时,页面显示"暂无"而不是 0 —— 因为 0 会被读成"今天平盘",那是编数据。

右下角客服浮窗(本平台最值得看的 AI 能力,因为它"会拒答"):

  1. 你发一句话 → 平台先判断意图(是问费率?问规则?还是问账户?);
  2. 命知识类意图 → 去已发布的知识里做向量检索;
  3. 有把握才答:命中分 ≥ 0.75 直接答;0.55~0.75 之间还要求"领先第二名 ≥ 0.07";否则不答;
  4. 答的时候带引用来源,你可以点开看出自哪份材料;
  5. 答不了 → 引导你转人工(点"转人工客服"才建工单); 问"我的账户里有多少钱"这类要登录才能查的问题 → 它会明确说"需要登录后查询",不编数字。

它是"异步 + 轮询",不是"一问一答的同步调用":前端把问题交上去,接口立刻返回 202(收到了), 然后每 0.5 秒问一次"好了吗",最多问 40 次(约 20 秒)。所以Worker 没在跑时, 你看到的就是那句"客服繁忙,暂时没能给出答复"——不是模型慢,是没人处理。 (平台也提供了更省事的 SSE 推送端点,但浮窗目前用的是轮询。)

访客没有账号也能用,是因为平台给了它一张范围极小的临时通行证 (POST /api/v1/visitor-tokens,只带 knowledge:query 等最小权限)。 客户/员工登录后走的是另一套令牌,权限大得多。

两个"知道就好"的前端细节:产品详情页如果 ?code= 写错了,页面不会报错, 而是静默展示第一只基金;「资金流水」页因为导航配置里 active 是空串, 顶部导航不会高亮它(功能正常,只是不好看)。


3. 登录:我该去哪个门、进去之后能看到什么

3.1 一次登录到底做了什么

  1. 你在登录页填用户名 + 密码,前端调 POST /api/v1/auth/tokens(端点代号 A034);
  2. 接口返回你的角色列表(roles);
  3. 前端做一次门禁检查:客户登录页只接受 customer;员工登录页只接受 risk_operator / operator / advisor / admin / super_admin。角色的门走错了会被拦下, 并明确提示"该账号不是客户账号,请使用后台登录入口"(app/static/portal/common/login-controller.js);
  4. 登录成功 → 令牌写进 cookie(auth_token)+ sessionStorage 缓存, 跨标签页以 cookie 为准;
  5. 按角色送到不同的工作台(下表);客户如果还没做风险测评,会被强制先跳到问卷页 (GET /api/v1/onboarding/risk-questionnaire 返回 required=true 时);
  6. 令牌失效/换账号 → 自动跳回登录页并带 ?reason=session-expired 之类的提示。

3.2 角色 → 落地页(app/static/portal/common/auth.js 的 staffHomeForRoles)

角色 登录后落到
customer(客户) /portal/customer/dashboard/(未测评则先到 /portal/customer/risk-questionnaire/)
risk_operator(风控专员) /portal/employee-risk/dashboard/
advisor(投顾) /portal/employee-advisor/dashboard/
operator(运营) /portal/employee-operations/offsite/
admin / super_admin(管理员) /portal/employee-console/workspace/

3.3 为什么"令牌里不带权限"

令牌里只有"你是谁",没有"你能干什么"。 每次请求到达服务端时, 平台重新查一遍你的角色、权限码和数据范围。这么做换来两件事:

  • 管理员改了权限,下一次请求立刻生效,不用等你重新登录;
  • 令牌被别人拿到,也不能靠它越权——服务端照样按当前权限判。

3.4 越权有三道防线,但只有后两道是真的在拦

防线 在哪 实际强度
第一道:前端隐藏/禁用入口 各门户页面(员工工作台会调 A038 拿服务端角色与权限后再刷按钮) 体验层,不是安全层。而且客户门户这一道目前是装饰性的:登录响应里不含权限清单,前端对客户角色直接按"全量权限"渲染,所以 data-permission-required 恒为"已授权"。别把它当防线。
第二道:权限码 服务端每个接口都要一个权限码,比如下单要 trade:order:create 真拦。没有就 403(AGENT_PERMISSION_DENIED 一类)
第三道:数据范围 data_scope + 客户归属关系(sys_customer_assignment) 真拦。有"看客户"的权限,也只能看分配给自己的客户

换句话说:能不能点,看前端;能不能成,看后端。 判断"某人到底能不能做某件事", 一律以服务端为准;前端只是提前把不该看到的藏起来。

第三道是最容易被忽略、也最关键的一道:员工号和客户号是同号段的 (演示数据里客户 9001-9020、员工 9002/9020 并存)。 如果只按"号"去查,风控/投顾就会读到陌生客户的长期记忆,而且不会报错。 所以平台把"能读哪些客户"收敛到一个文件(app/core/memory_scope.py): 客户只读自己;员工只读分配给自己的客户,且单次最多 10 个;归属没维护就失败关闭。


4. 客户门户:八个页面,一页一句

页面 地址 干什么 主要接口
登录 /portal/customer/login/ 登录 A034
资产总览 /portal/customer/dashboard/ 总资产、持仓市值、累计盈亏、今日盈亏 T001
我的持仓 /portal/customer/holdings/ 每只基金的份额、成本、最新价、市值、持有盈亏、今日盈亏 T006
委托 /portal/customer/orders/ 我下过的委托及状态(页面上没有撤单按钮:市价单下单即成交,能撤的极少) T003 / T004(撤单接口 T005 已就绪但页面未接)
成交 /portal/customer/transactions/ 已经成交的每一笔(价格、数量、费用) T007 / T008
资金流水 /portal/customer/cash-ledger/ 每一笔钱的进出与余额变化 T009
盈亏分析 /portal/customer/profit-loss/ 累计盈亏 / 今日盈亏 + 按持仓拆分 T001(复用看板数据)
风险测评 /portal/customer/risk-questionnaire/ 填问卷,决定你的风险承受等级(C1–C5) ONB001 / ONB002

4.1 下单一笔,中间发生了六件事(这是全平台最该讲清楚的一条)

  1. 你点了买入 → 前端调 POST /api/v1/users/me/orders(T002), 并且必须带一个幂等键(Idempotency-Key,16–128 个可打印 ASCII 字符)。 它的作用是:同一次请求被重发(超时重试、网络抖动)时只成交一笔。 ⚠️ 注意边界:它防的是"同一次请求的重发",不防"你又点了一次"—— 前端目前每提交一次就生成一个新键,所以手动连点两下确实会产生两笔委托。 要防这个得让前端为"一次下单意图"复用同一个键,属于已知的前端缺口。
  2. 服务端先看行情:取这只基金的最新行情快照,要求它在 15 分钟内。 超过 15 分钟直接拒绝(503 行情已过期)——这是演示最容易翻的一环, 因为系统没有任何自动刷新,补刷命令是 python tools/sync_market_prices.py(立即生效,不用重启)。
  3. 适当性校验:你的风险等级(C1–C5)和这只基金的风险等级(R1–R5)必须匹配, 不匹配直接拒单。这就是"风险测评不是形式"的落地处。
  4. 算钱:按 fin_fee_rule 里优先级最高且生效的费率规则算手续费, 成交时把费率固化进这笔成交记录(以后改费率不影响历史)。
  5. 落账(同一个数据库事务里同时写四处):委托、成交、资金流水、持仓。 任何一处失败,整笔回滚。所以这三页(资产/持仓/流水)看到的数字永远对得上。
  6. 成交价来自外部行情,不是随机数;订单状态直接是"已成交"—— 因为首版是市价委托立即全额成交,没有撮合排队。

顺带解释一个常被当 bug 的现象:撤单几乎总是失败。 因为市价单下单即成交,只有停在"待风控"的委托才能撤,所以页面点撤单大概率提示"该委托已成交"。

4.2 「今日盈亏」是怎么算的(2026-09-14 起是真实数据)

一句话:今天收盘时值多少钱 − 昨天收盘时值多少钱 − 今天买入花掉的钱 + 今天卖出收回的钱 (不含手续费,和旁边的"持有盈亏"同口径)。

两个值得在演示时说清的细节:

  • 当天买的份额只算"买入价 → 今收",不享受昨天到今天那段涨幅(昨天它还不是你的);
  • 行情只有一天的产品会回退用净值算,所以不会出现"某只永远是 0"。

完整口径、三个数据坑、逐只复算命令见 今日盈亏实现说明-2026-09-14.md。


5. 风控专员工作台(预警的"从哪来 → 怎么处置 → 怎么闭环")

进门:从员工登录页登录后自动落到 /portal/employee-risk/dashboard/; 页面加载时先调 A038 把服务端真正解析出来的角色、权限、数据范围拿回来, 再据此决定哪些按钮可点(这就是 §3.4 说的"第一道只是体验层")。

5.1 上半屏:概览四张卡

卡片 含义
未闭环预警 还没走完处置流程的预警总数
高风险 其中等级为"高风险"的数量
待处理 状态还是"待处理"的数量
已超时 超过处置时限还没动的(大于 0 会标红)

5.2 预警是从哪来的

两条路:页面上点"手动扫描"(RK006,同步执行、最长 60 秒), 或定时调度(app/worker/risk_scan_scheduler.py,是个独立的可选进程)。 扫描会按规则(RW-xxx)核对交易、资金、持仓、登录等数据,命中就落一条预警。

演示数据里那些"异常交易"记录是刻意的:客户 12001–12005 名下有 6 行 RISKDEMO-* 记录(大额申购/赎回),就是为了造出"大额赎回""频繁申购"这类预警场景 才写进去的演示触发材料,有意保留,不是脏数据、也不是漏删。 它们不代表真实场内成交(不改持仓表,份额也和持仓对不上)。 副作用只有一个:算「今日盈亏」时必须只认本平台的场内成交(买入/卖出), 否则这些虚拟份额会把"昨日持仓"算到几十万份 —— 详见 今日盈亏实现说明-2026-09-14.md。

5.3 预警队列与筛选

游标分页(不是页码),每页固定 10 条。筛选项:关键词、客户编号、产品代码、 风险等级(高/中/低)、规则编号(必须形如 RW-001)。 后端还支持"产品名 + 时间范围"两个筛选,页面暂未提供输入框(属可用性缺口,不是故障)。

5.4 预警详情:为什么说它"有据可查"

一条预警点开,能看到四层东西:

  1. 基本信息:预警编号、风险等级、客户、产品、处置状态、确认状态、命中规则、是否已升级;
  2. 证据摘要:触发这条预警的那几条关键事实;
  3. 七个关联业务区段:把这个客户在这条线索上的交易、资金、持仓等上下文摆在一起;
  4. 证据快照 / 证据归档:当时的字段原样留存,事后可复查。

另外"证据查询"标签页提供 8 类只读实体的直查:客户、产品、交易、资金流水、 持仓、登录记录、预警、通知。全部是只读的,改不了任何数据。

5.5 处置是"五步状态机",不是改个字段

动作 效果
确认接收 只记录"谁在什么时候看到了",状态不变
进入调查 状态:待处理 → 调查中
关闭误报 状态 → 已排除,必须填理由
完成结案 只有"调查中"的才能结案;必须填结论,并且按风险等级扣客户行为分:低 3 / 中 5 / 高 20(行为分 0–20 封顶)
升级处理 只打"已升级"标记,不改状态,可以和上面几步并行

每一步都写审计(谁、何时、做了什么)。所以"这条预警谁处理的、依据是什么"事后能查清。

5.6 日报与助手

  • 日报:点生成后逐段出现(SSE 推流),一共 9 段(当日数量、等级分布、重点事件、 未闭环、误报情况、类型分布、处置结果、规则效果、优化建议)。 末尾会标来源:模型可用时是"大模型生成",不可用时自动降级为规则模板并如实标注—— 降级不是报错。生成完可以填多个邮箱发送(逗号/分号/空格分隔)。
  • 风控助手:预设问法 6 个(通用的 3 个 + 绑定当前预警的 3 个)。 它只会调三个只读工具:get_risk_overview、search_risk_alerts、get_alert_evidence, 输出的是"研判与话术草案",并明确声明不构成任何处置结论,处置一律由人工做。
  • 通知记录:可以看到每条通知的渠道、发送状态、失败原因。顶部铃铛只统计"站内提醒"。 ⚠️ 邮件通道依赖 SMTP 配置,授权码失效时会显示"发送失败"(详见 §12)。

6. 投顾工作台(给自己服务的客户出方案)

进门:员工登录页登录 → 落到 /portal/employee-advisor/dashboard/。 投顾能操作的客户范围由服务端按归属关系限定(sys_customer_assignment:本人 + 分配给我的客户), 拿不到全量客户——这是数据范围,不是页面缺陷。(页面上那份"我的客户"名单目前是内置的演示数据, 真正的范围以服务端为准,见 §6.1。)

6.1 页面上有什么

区域 作用
概览四张卡 已发布交付物数量、名下客户数、风评熔断数、会话来源
客户区 客户列表 + 参数 + 7 个动作按钮 + 一句自然语言输入框。⚠️ 这份客户名单目前是页面内置的演示数据(张总 9101 / 李阿姨 9102 / 王工 9103 / 陈医生 9104);真正的客户范围在服务端按归属关系(sys_customer_assignment)判定,并且有一个灰度开关:没开放的账号直接 403「当前投顾功能尚未对该账号开放」
操作区 7 个动作:组合分析 / 资产配置 / 生成推荐方案 / 录入客户目标 / 目标确认与方案书 / 画像评分 / 调仓建议
结果区 动作结果 + 推荐流水线六步进度(① 鉴权 ② 画像 ③ 筛选 ④ 方案 ⑤ 合规 ⑥ 审核留痕)
已发布交付物 已发布给客户的方案书/推荐方案(只列"我自己 + 归属我的客户"的,且已发布、倒序 20 条)
助手对话框 用自然语言下指令。⚠️ 它走的是前端正则意图路由(识别"组合分析/配置/推荐"等词后复用同一套按钮动作),刻意没有走 Agent 异步链路——因为异步 run 需要常驻 Worker 与发布配置,投顾工作台要保证离线也能演示

6.2 三种动作,真实的只有三种(这点要讲清楚)

动作 走哪里
组合分析、资产配置、生成推荐方案 走真实后端(/api/v1/advisor/portfolio-analysis、/asset-allocation、/recommendations),并且会带上所选客户的 customer_id
画像评分、调仓建议 页面本地演示引擎算(后端暂无对应接口),数据取自页面内置的演示产品/客户表
录入客户目标、目标确认与方案书 走真实后端(投资目标与方案书接口)

这是刻意标注的边界:页面内置了一张演示产品与客户表,只服务"离线演示"和那两个后端没有的动作。 演示时如果被问"这个评分是怎么来的",照实说"这一步是前端演示引擎,后端暂未提供该接口"即可。

6.3 生成推荐方案:为什么可能"一个产品都没有"

这是最容易被误判成故障的一步。系统不是"随机挑几只",而是逐道闸门筛,全过了才进候选:

  1. 可交易:产品状态是"上市"、在沪深交易所、当前在开放窗口内;
  2. 适当性证据:该产品在治理表里有 verified(已核验)的适当性记录,机构、来源链接、文档摘要都要齐;
  3. 合同证据:同样要有已核验的合同快照;
  4. 不在治理待办里(有未决事项的产品先排除);
  5. 规模(如果设了门槛);
  6. 流动性:日均成交额要达到门槛(日频 1000 万、7 日内 100 万、30 日内 10 万);
  7. 硬性适当性过滤:按客户风险等级(C1–C5)挡掉不匹配的产品;
  8. 剩下的按规则排序取前列。

所以"候选池为空"时,页面会把原因讲清楚(证据缺失/流动性不够/适当性不匹配/没有客户目标), 并给出两条路(补证据材料,或走管理员审核的既有交付物)——不是一句"没有通过校验的产品"。 实测:客户 9101(C4)出的方案里有 3 只 R4 产品,9102(C1)只有 1 只 R1。

6.4 审核与发布:哪一步投顾自己能做

交付物 投顾能做什么 谁来做审核/发布
产品推荐方案 生成、自助审核 / 驳回 / 发布给客户 投顾自己(2026-09-14 起开放),管理员复核队列同样能做
投资目标方案书 录入目标 → 确认目标 → 查看方案书 仍归管理员(接口上是 admin=True 的复核环节,故意的)

方案书与推荐方案都有状态:目标 待确认 → 已确认;方案书 待审核 → 审核通过 → 已发布(或已退回)。

6.5 合规熔断(FM-03)

客户的风险测评超过 12 个月即视为失效:页面会冻结购买类动作 (生成推荐方案、资产配置、画像评分、调仓建议),只留赎回方向,并明确显示熔断原因。 ⚠️ 这个拦截目前发生在前端闸门——后端只判断"测评有没有有效期字段", 不比较是否已过期。所以不要绕过页面直接调接口来"演示"这件事,那里拦不住。

6.6 一个专门的"离线演示模式":网址后面加 ?demo=1

投顾工作台有一条唯一能绕过登录守卫的路径:/portal/employee-advisor/dashboard/?demo=1。 进去之后:

  • 不持有令牌(所有真实接口请求都不发),页面顶部明确标注「演示模式 · 本地引擎」;
  • 数据范围文案改成"本地演示引擎",指标卡的数据源也标成"本地引擎";
  • 生成的方案带「规则模拟,不构成真实推荐」字样;
  • 没有退出按钮(本来就是离线)。

它是给"没有后端 / 没有 Worker 也要讲一遍投顾流程"准备的,演示时务必主动说明这是演示模式, 别让人以为真实链路就是这么跑的。


7. 运营门户(三条线:场外收件 / 推广材料 / 问数)

进门:员工登录页登录后落到 /portal/employee-operations/offsite/。 顶部导航四个入口:概览、场外基金、推广材料、NL2SQL。

7.1 线一 · 场外基金受理:把"收到的邮件"变成"核对过的单据 + 发出的通知"

这是一条确定性流程(不靠模型自由发挥),完整走一遍是这样:

步 操作 背后发生了什么
1 看收件状态 ⚠️ 页面上没有"上传邮件/投递"入口:邮件是 Worker 通过 IMAP 从真实邮箱收进来的(Worker 不在跑 = 永远不收信),而且发件人必须在白名单里,否则回 403「发件人不在场外业务白名单」。状态显示"运行中/未启用";游标卡住时可以点"恢复收件游标"(⚠️ 它只把状态从"阻塞"重置为"空闲",不会跳过那封失败的邮件)
2 打开一封邮件 左侧列表 → 右边显示正文、附件原件(可预览/下载)、OCR 识别字段、关联单据
3 人工修正 OCR 字段并保存 识别不可能 100% 准,所以运营可以逐字段改。保存后系统会自动重新核对一遍(触发 NL2SQL → 拉回字段 → 重判规则),不用再手动点一次
4 (必要时)识别重试 附件识别失败可以提交重试
5 看单据要素 申请日期、申购金额 / 赎回份额、机构、基金代码
6 看规则结果表 每条规则一行:规则名 / 结果(满足·不满足·无法判断)/ 单据值 / 规则判断。可点"重新核对并判定规则"(这一步只重跑计算与判定,不会重新识别、也不会重新生成自然语言)
7 人工下结论 "确认正常" 或 "确认异常" —— 这是人的决策,系统只给依据。⚠️ 识别或核对没走完就点确认,会回 422「识别或数据核对尚未完成,不能进行业务确认」
8 创建并发送通知 按结论选通知类型(邮件回执 / 正常回执 / 异常回执 / 风险通知 / 清算通知),正文可编辑,然后发送
9 收尾 结清统计可重算(只统计"已确认正常且已成功回执"的单据);也可以彻底删除一封邮件(附件、OCR、规则、通知一并删除,删除审计保留)

三个"知道就好、别被问倒"的点:

  • 后端对通知类型与确认结论的一致性有硬校验:风险通知 / 异常回执只允许"已确认异常", 清算通知 / 正常回执只允许"已确认正常",邮件回执必须先完成人工确认——对不上直接 422。
  • ⚠️ 发送通知默认是"演练"(dry-run):本机配置里 SMTP 发送是关着的、dry-run 是开着的, 所以点"发送通知"会返回发送结果与状态,但不会真的把邮件发出去。要真发得先配 SMTP。
  • ⚠️「收件状态」显示的是配置,不是 Worker 心跳:Worker 假死时它仍然显示"运行中"。 判断 Worker 死活要看进程,别只看这一格。

⚠️ 这一步是演示最容易尴尬的地方:邮件列表可能是 0 条,因为收件依赖真实可用的邮箱配置; 发通知还额外依赖 SMTP 授权码。没有真实邮件源时,这页就是空的——是"已知空数据",不是坏了。

7.2 线二 · 推广材料:把产品事实编排成"可审核、可交付"的材料

步 操作 要点
1 创建任务 任务编号、产品名称、产品代码、材料标题、视觉风格(均衡配置 / 稳健专业 / 成长研究)。进页面不会发任何请求,任务号由后端按 PM-日期-序号 生成
2 填结构化资料 产品信息、管理人信息、团队与策略、费用/业绩/风险四组。⚠️ 页面上带 * 的必填其实不会触发浏览器校验(页面没有 <form>),靠后端兜底
3 上传附件 基金经理照片(jpg/png/webp)+ 业绩数据文件(csv/xlsx/xlsm)。上传时解析就是上传的一部分,没有单独的"解析"按钮
4 生成材料 产出 PPTX、宣传长图、PDF 和若干图表。这一步是同步的(前端单独给了 120 秒超时),不依赖 Worker;只有背景图会调外部图像服务,不可用时自动降级为程序化背景
5 看合规检查 每条命中显示规则名 + 建议,分"阻断"与"提示"两类
6 审核 审核通过 / 要求修改 / 审核驳回,都要写意见
7 交付 审核通过后填投顾编号(逗号分隔)。⚠️ 首版交付只写内部记录,还没接外部投顾端

合规检查是真会拦人的(以下都是"阻断"级,不是提示):

  • 勾了"展示排名"就必须同时给出评价期间 ≥3 年 + 评价机构 + 公开来源;
  • 勾了"展示业绩"就必须已上传业绩数据文件,且历史月份 > 6;
  • 出现收益 / 回撤 / 波动率 / 夏普比率里任意一项,三项风险指标必须齐;
  • 七项费用必须填全;
  • 材料里不许出现"保本、稳赚、无风险、保证收益、抢购"这类词;
  • 必须有法定风险声明;不许把"从业年限"和"投资管理经验"混为一谈。

两个"为什么这么设计"的点,被问到很值得讲:

  • 业绩数字不是手填的:数据截止日期、历史月份、产品收益、最大回撤这几个字段是只读的, 由后端从你上传的业绩数据文件里算出来;波动率与夏普比率才需要手工填。 换句话说,编不出来——要么有数据文件,要么填不上。
  • "排名"必须给出来源:要单独填"评价期间(年,需 ≥3)/ 评价机构 / 公开来源 / 排名文本"四项。 因为排名属于公开可核查表述,没有来源就不让写进去(这是合规要求,不是表单啰嗦)。
  • 上传的幂等键用"文件字节数 + 修改时间"生成,不用文件名: 中文文件名会让浏览器在构造请求头时直接抛错,表现为"网络连接失败"、而服务端一条记录都没有(已修)。

7.3 线三 · NL2SQL:用中文问数,但只能读、只能查白名单表

页面顶部自己写着三条边界:只读查询 / 白名单表 / 权限审计。 它有两个模式:

模式 怎么用
场外单据核对 输入单据 task_id → 点"生成自然语言"(生成后可人工修改)→ 勾选"我已人工确认原始文件内容" → "执行只读核对" → 返回每张表核对规则的状态、返回行数、错误信息。这一段是同步的,不排队
通用自然语言查询 直接输入业务问题(如"查询某基金最近一个交易日的净值")→ 提交后页面每 1.2 秒轮询一次运行状态(依赖常驻 Worker,否则会一直停在"排队中")→ 展示 AI 生成的 SQL + 最终查询结果(没有图表)

关键点:SQL 由后端的 Agent 链路生成,并且只能碰发布配置里白名单允许的表; 前端不拼 SQL,也不能绕过白名单。查询、权限、审计与最终状态都在后端。 (操作界面里连"生成 SQL"按钮都没有——这是刻意的。)

后端会拦下这些写法,报错文案就写在那里,可以直接念: 仅支持 SELECT 查询、检测到非只读数据库操作、禁止使用 SELECT *、 SQL 使用的数据表与查询计划不一致、查询字段不在白名单内、 查询涉及的表缺少合法关联路径、查询参数过多。 单次查询结果最多 50 行;表最多跨 3 个业务域。

演示时如果问"它会不会乱查":讲这条链路是"自然语言 → 意图识别 → 白名单工具 → 生成 SQL → 只读执行", 越界的表在工具层就被挡掉。这里也踩过一个真实缺陷:只识别 6 位数字代码, 而演示产品里有 5 位代码(如 15911),结果"查的是别的产品的数据、状态还显示成功"—— 已修正为 5–6 位并加了单位校验。


8. 管理员工作台(平台的"总控台")

进门:员工登录页登录 → staffHomeForRoles() 判为管理员 → 落到 /portal/employee-console/workspace/。 上半屏四张卡:有效角色数、生效配置版本数、可用模型端点、待人工复核(工单 + 画像候选 + 画像漂移之和)。

下面是八个标签页,逐个一句话说清"它管什么、能写还是只读":

标签页 能做什么 读写
角色与权限 看角色清单(角色名/代码/权限数/用户数/状态);输入一个用户 ID,看服务端实际解析出来的角色、权限、数据范围 只读
配置与模型 平台里唯一能改配置的地方:新建发布版本 → 校验 → 审核 → 激活;管平台配置项(命名空间:工具白名单 agent_tools、记忆 memory、关系 relationship、运行时 runtime、行情 fund_market)与模型路由规则 读写
审计记录 最近 20 条审计(动作、主体、入口、时间);没有"看敏感详情"的权限时,详情由服务端脱敏 只读
转人工工单 看客服转人工的工单与二次脱敏的会话摘要;并且能处置:分配 → 接单 → 解决 → 关闭(未解决可取消),可按状态筛,每一步都写审计 读写
画像候选 客户对话里提炼出的画像候选,批准后才进正式记忆(批准/驳回都要填意见) 读写
投顾复核 待审的投顾交付物(推荐方案 + 投资方案书)→ 审核通过 / 驳回 / 发布给客户 读写
知识库 上传文档(faq / product / policy)→ 自动切分入库并投向量同步;客服就是靠它作答;也能让某份知识失效 读写
画像漂移复核 客户重新测评导致画像变化时,人工确认是否采纳 读写

配置发布的状态机(config_release 表里能直接看到这些取值):

草稿 draft → 待审核 pending_review → 已审核 approved → 生效 active
                                    ↘ 驳回 rejected
生效版本被新版本取代 → superseded(当前库里绝大多数历史版本都是这个状态)

为什么管理员也不能"直接改权限":平台的立场是权限与提示词的变更必须留痕、可复核、可回滚, 所以后端根本没提供"直接改"的接口,只能走上面这条状态机。这不是没做,是刻意不做。

已知的三个小缺口(后端有接口、页面没接):页面没有"驳回 / 回滚"按钮(审核只有"通过")、 模型端点页是只读的(包含激活入口)、审计页没有筛选框(只能看最近 20 条)。 演示被问到时按"前端未接、后端已有"回答即可。


9. 底座里那几件"看不见、但决定一切"的事

这九件事是跨所有业务的,任何一个业务页面背后都会用到其中几件。

9.1 鉴权与数据范围

令牌只回答"你是谁";权限与数据范围每次请求重新算。 数据范围只有三个有效取值:self(只看自己)、own_customers(只看分配给我的客户)、all(全部); 写成别的值会被静默丢弃(等于没给范围)。员工能看到哪些客户由 sys_customer_assignment 决定。

9.2 幂等:为什么"重发"不会重复扣钱

所有会产生数据的操作(下单、建会话、转人工、配置发布、场外确认……)都要求带 Idempotency-Key, 服务端把"键 + 结果"记在 api_request_receipt 里。同一个键再来一次,直接把上次的结果还给你, 不会重复执行。键的规则是 16–128 个可打印 ASCII 字符—— 这条规则是真踩过坑:曾经前端把中文文件名当成键,浏览器在发请求前就抛 UnicodeEncodeError, 页面只显示"网络连接失败",服务端一条记录都没有,非常难查。

⚠️ 它的边界要说清:幂等键保护的是"同一次请求的重发",不是"同一个操作意图"。 前端目前每次提交都用新键,所以手动连点两下会真的下两笔单。 要彻底防重,需要前端把"一次下单意图"的键稳定下来(把键与表单实例绑定),这是已知缺口。

9.3 三个 Agent、七个业务 Agent

  • 所有业务 Agent 都必须继承公共的 BaseAgent,并且只能由 AgentFactory 创建 —— 为的是不许绕过鉴权、记忆、模型路由、工具、合规、审计这条公共流程。 (规矩写在 AGENTS.md,有测试守着。)
  • 目前 7 个:客服、风控、投顾、场外基金、推广材料、平台探针、基金查询示例。
  • 工具能不能用,是两个集合的交集:代码里注册的 ∩ 当前生效配置里发布的。 缺发布配置 → 失败关闭(宁可不能用,也不放开)。这就是为什么"代码明明写了这个工具, 调用却报没有权限"——去管理员工作台看当前生效的发布版本里有没有它。

9.4 记忆:值在 MySQL、关系在 Neo4j、向量在 Milvus

一条"记住客户说过什么"的完整过程:

  1. 对话发生 → 若干轮之后 Worker 把这几轮聚合成一个片段;
  2. 从片段里抽取长期记忆(只抽明确的业务事实;客服和访客不写长期记忆);
  3. 记忆入库(MySQL,唯一权威)→ 触发画像重建 → 生成画像快照;
  4. 画像与记忆再投影出去:关系进 Neo4j、向量进 Milvus(集合 user_long_term_memory_v1);
  5. 下次这个客户再来,就能"召回"到相关记忆,注入给模型。

为什么投影要绕一层"事件":抽取发生时那条记忆还在没提交的事务里, 另开一个连接去重建画像根本看不到它。走事件就一定是"提交之后"才消费,数据一定可见。

谁能读到谁的记忆只有一个判定口径(app/core/memory_scope.py): 客户只读自己;员工只读分配给自己的客户,单次上限 10 个;归属没维护 → 失败关闭。 不这么收敛的后果是越权陷阱:员工号和客户号同号段,按号查会读到陌生客户的记忆且不报错。

9.5 模型路由

所有模型调用都从统一的网关走,按"任务类型"选端点(意图分类 / 向量化 / 生成各不相同), 端点在数据库里配置、可切换。密钥放在 .env(不进仓库)。 演示环境当前用 deepseek-flash 做意图与生成、qwen 做向量化。

9.6 配置发布:为什么管理员也"不能直接改权限"

平台故意不提供"直接改权限/直接改提示词"的接口,所有变更必须走状态机:

草稿(draft) → 待审核(pending_review) → 生效(active) → 被新版本取代(superseded)

一次发布是整版本替换:生效版本里包含哪些工具白名单、提示词、模型路由规则,一目了然、可回滚。 受管的内容只有三张表:平台配置项、提示词版本、模型路由规则。 生效后平台会发一个事件去清缓存,让新配置立刻对所有请求生效。

9.7 事件与 Outbox:先落库,再干活

"要慢慢做的事"(投递记忆、同步向量、重建画像、发通知)都不在请求里硬等, 而是先往事件表写一行,再由 Worker 一条条消费:失败了记重试次数、退避重试, 重试到头进"死信",永远不丢、可查、可重放。

一个真实踩过的坑:事件里表示"投到哪个存储"的字段必须是小写英文 (milvus / neo4j)。文档里曾写成大写 MILVUS,消费端按小写分派, 结果事件谁都领不到、永久滞留、且不报错。

9.8 审计:每一次判断都留痕

权限判定、工具调用、预警处置、投影清理、转人工……都往 interaction_audit 写一条: 谁、什么身份、对哪个客户、在哪个门户、做了什么、结果如何。 管理面的工单接口不返回客户标识与原始对话(脱敏),这是刻意的。

9.9 降级:宁可说"没做成",也不假装成功

Milvus/Neo4j 没配或连不上时,平台的取向是显式降级 + 留痕: 事件保持"待处理"、审计里写"跳过 + 原因",绝不写一条"已同步"。 所以看到"待处理堆积"先怀疑依赖没起来,而不是怀疑业务代码。


10. 把上面全部串起来:一条主线的时间线(从登录到一笔成交)

# 你做的事 平台内部发生什么 落在哪
1 打开 http://127.0.0.1:8000/portal/ 307 跳到访客首页 —
2 打开客户登录页,填 cust_t / 123456 前端调 A034 换令牌;核对"这是不是客户账号";写 cookie sys_user + 令牌
3 自动跳资产总览 前端带令牌调 T001;服务端重新解析身份/权限/数据范围 → 查账户、持仓、最新行情、昨日基准 → 算市值、持有盈亏、今日盈亏 读 fin_sim_account / fin_holding / fin_market_price / fin_nav_history / fin_transaction
4 切到"我的持仓" 调 T006,同一套计算(保证两页口径完全一致) 同上
5 点买入 100 份 510300 前端生成幂等键 → 调 T002 —
6 — 服务端:查幂等表(重复键直接返回上次结果)→ 校验行情是否在 15 分钟内 → 适当性校验 → 按费率规则算钱 → 一个事务里写委托、成交、资金流水、持仓 api_request_receipt / fin_sim_order / fin_transaction / fin_cash_ledger / fin_holding
7 刷新页面 持仓、成交、流水三页同时出现这笔;今日盈亏把"今天买的这部分"按成交价计入 同上
8 顺手在浮窗问客服"场内基金管理费率是多少" 前端建会话 C001 → 发问 R001,立即返回 202(受理)→ 事件落库 → Worker 领单执行 → 检索已发布知识 → 有把握才答、附引用 → 结果落库 → 前端取回 conversation* / agent_run / domain_event_outbox / interaction_audit
9 对话结束、过一会儿再问同样的问题 Worker 聚合会话片段 → 抽取长期记忆 → 重建画像 → 投影到 Neo4j(关系)与 Milvus(向量)→ 下次召回用得到 memory_unit / profile_snapshots / memory_sync_outbox
10 (若触发风控)风控专员在队列里处置 扫描 → 生成预警 → 通知;确认/调查/结案每一步写审计,结案按等级扣客户行为分 fin_risk_alert / fin_risk_notification / interaction_audit

11. 常见疑问(大白话问答)

Q:下单报"行情已过期"(503)怎么办? A:行情只认 15 分钟内的快照,且不会自动刷新。跑一次 python tools/sync_market_prices.py 即可,立即生效、不用重启任何服务。

Q:客服一直"繁忙/响应超时"? A:先看 Worker 在不在跑(python -m app.worker)。Agent 请求是"受理 → Worker 执行 → 落结果" 三段式,没有 Worker,队列里那行会一直停在 queued,页面只会显示超时。

Q:整个平台突然都很卡(客服、看板、风控同时迟钝)? A:多半是 Milvus 抖动:检索层用的是同步客户端,一次卡住会阻塞整个 API 进程。 确认 Docker Desktop / Milvus 在跑即可,恢复后自动正常,不用改代码也不用重启。

Q:为什么"今日盈亏"显示的是最近一个交易日的数? A:因为它用的行情/净值都是日终数据,语义就是"最近一个交易日相对前一交易日的盈亏", 不是盘中实时。当天数据还没同步时,它会等于"昨日"。

Q:为什么投顾看不到某个客户? A:投顾只能看分配给自己的客户(sys_customer_assignment)。没分配就看不到,这是数据范围,不是故障。

Q:为什么运营那边的邮件是 0 条? A:场外收件需要真实可用的邮箱(收件依赖邮箱配置;发件还依赖 SMTP 授权码)。 没有真实邮箱源时,这一页就是空的——这是"已知空数据",不是坏了。

Q:为什么管理员不能直接改权限? A:金融场景要求权限变更留痕、可追溯、可回滚,所以统一走"草稿→审核→生效"的发布流程。

Q:为什么有些问题客服答不上来? A:知识是人工审核后入库的。检索不到、或把握不够(分数不够高/候选并列)时就转人工—— 宁可转人工也不硬答,这是刻意的取舍。

Q:为什么撤单总是失败? A:首版是市价委托立即全额成交,只有停在"待风控"的委托能撤,所以基本都会提示"该委托已成交"。

Q:点了"发送通知",为什么对方没收到邮件? A:本机 SMTP 默认是演练模式(发送开关关着、dry-run 开着),所以流程会走完、状态会显示, 但不会真的发出去。要真发得先配好 SMTP。另外发件还依赖邮箱的 SMTP 授权码,授权码失效会显示"发送失败"。

Q:场外的"收件状态"显示运行中,为什么就是收不到邮件? A:那一格反映的是配置,不是 Worker 心跳——Worker 卡死时它仍然显示"运行中"。 另外发件人必须在白名单里,不在白名单的邮件会被拒(403)。

Q:为什么有的接口报错却还是 HTTP 200? A:场外这条线的业务错误用的是 HTTP 200 + {code, message} 的信封(前端把 code ≥ 400 当错误处理), 参数格式错误才会是真正的 HTTP 422。看错误要看 code 与 message,别只看状态码。


12. 速查

六个门户

访客   http://127.0.0.1:8000/portal/guest/home/
客户   http://127.0.0.1:8000/portal/customer/login/
风控   http://127.0.0.1:8000/portal/employee-console/login/   → 自动进风控工作台
投顾   http://127.0.0.1:8000/portal/employee-console/login/   → 自动进投顾工作台
运营   http://127.0.0.1:8000/portal/employee-console/login/   → 自动进运营门户
管理员 http://127.0.0.1:8000/portal/employee-console/login/   → 自动进管理员工作台

演示账号

角色 用户名 密码
客户 cust_t 123456
风控专员 risk_t 666666
管理员 admin_t 88888888
投顾 advisor_t abc12345
运营 offsite_t offsite123
风控演示客户 12001–12005 risk12345
投顾演示客户 advc_zhang / advc_li / advc_chen (见投顾线脚本)

常用命令

powershell -ExecutionPolicy Bypass -File start.ps1   # 起 API + Worker(自动刷行情、开浏览器)
python -m app.worker                                # 只起 Worker
python tools/seed_demo_data.py                      # 准备演示数据(10 步,首次/换机器)
python tools/sync_market_prices.py                  # 刷行情(下单报 503 时补跑)
python tools/check_today_profit_loss.py             # 复算并核对「今日盈亏」
python tools/e2e_smoke_test.py --read-only          # 业务链路体检(不动数据)
python tools/portal_api_check.py                    # 接口契约体检(按前端的方式调)

端口:API 8000 / MySQL 3306 / Redis 6379 / Milvus 19530 / Neo4j 7687

已知空数据(演示时别点进去尴尬)

位置 现状 原因
运营 · 场外邮件 0 条 需要真实可用的收件邮箱
运营 · 通知发送 只"演练",不真发 SMTP 开关默认关闭 + dry-run 默认开启
管理员 · 画像候选 0 条 需要客户对话触发画像抽取
投顾 · 客户列表 页面内置 4 位演示客户 前端暂无"查我的客户"接口,真实范围在服务端按归属判定

已知小偏差(不影响主流程;被问到就照实说,别硬解释)

现象 实情
场外通知类型的下拉"切了没用" 那个下拉没接事件处理,页面实际只能创建"正常回执/异常回执";风险通知、清算通知、邮件回执在这一页点不出来(接口是有的)
NL2SQL 场外核对结果里"错误信息"列永远是 -- 后端返回的记录里没这个字段,前端取了个不存在的键
场外部分写操作偶尔报"请求超时" 这些接口用的是默认 8 秒超时,而同步核对与统计重算本身可能跑到 10 秒以上(接口能跑完,只是前端先放弃了)
识别重试"提交了"但状态没变 后端失败时也返回 code:0 + "已保留异常状态",前端一律提示"已提交识别重试"
推广材料页的必填校验不弹提示 页面没有 <form>,required 不生效,必填靠后端返回错误
投顾"已发布交付物"空态写"需管理员审核" 文案没跟进"投顾可自助审核发布"这次改动
管理员配置发布没有"驳回/回滚"按钮、模型端点页是只读的、审计页没有筛选框 后端接口都有,前端未接
客户「资金流水」页顶部导航不高亮 导航配置里该页的 active 是空串
转人工工单队列里一堆一模一样的"端到端冒烟"单子 冒烟脚本每跑一次建一张。2026-09-14 起它自己会走完处置闭环收尾(分配→接单→解决→关闭),不再堆积;历史遗留的那些可以手动处置掉