Files
group_fqcd_jr/docs/40-前端验收清单.md
T
lzf_0626 2fcc4939e7 feat(tools): 新增全流程端到端冒烟脚本 e2e_smoke_test.py
把验收清单里"能用接口判定"的部分自动化:6 条线 40 项,打真实 HTTP、逐条打印 PASS/FAIL。
不是 pytest 的替代 —— 单测看代码正确性,它看"部署后底座是不是活的"。

覆盖:
  A 访客  令牌 → 提问 → 收到回答 → 未转人工
  B 客户  登录 → 看板/持仓/流水 → 真实下单成交 → 委托详情 → 会话与转人工
  C 风控  概览/队列/详情/八类证据/通知/扫描 → 处置闭环(确认→调查→结案)
  D 投顾  已发布方案     E 运营  场外邮件与邮箱状态
  F 管理员 角色/权限/身份/配置发布/模型端点/审计/转人工工单/画像候选

两个实现细节值得留意:
- **各端点 data 形状不统一**(角色列表裸数组、持仓在 holdings、成交明细在 transactions、
  资金流水在 entries、知识列表不套 data),所以这里用 listed(payload, *keys) 依次尝试,
  并在注释里写明"别猜字段名"—— 我自己写联调脚本时连续猜错三次。
- 库里没有"待处理"预警时**自动造一条**再验证处置闭环,否则该环节只能跳过
  (实测库里的预警常常都已被处置过,第一次跑就跳过了)。

新增 --read-only:跳过所有写操作,用于生产或不想动数据时。

实测:40/40 通过(含真实下单 201 已成交、风控处置闭环三段全 PASS)。

docs/40 补上"一键冒烟"一节。
2026-09-13 21:39:37 +08:00

25 KiB
Raw Blame History

前端验收清单(正式门户)

被测对象:正式前端 app/static/portal/,由 app/main.py 挂载在 /portal/ 入口:http://127.0.0.1:8000/portal/(/ 与 /portal/ 都会 307 跳到 /portal/guest/home/) 时点:2026-09-13(第三次更新:补入投顾/运营两套页面、访客客服浮窗、Worker 前置)

证据口径 ✅实测 = 我用真实 HTTP 请求打过,数字是那次响应的真实值; ⚠️待点验 = 只能从代码与接口推断的页面交互,需要你在浏览器里点一遍。

交付方自述的路由与数据源见 app/static/portal/README.md。


0. 启动与前置 ⚠️ 这一节最容易踩

# 项 命令 / 检查 预期
0-1 依赖服务 MySQL、Redis 可用;Milvus 需要 Docker Desktop 在运行 平台能起,无 500
0-2 RBAC 种子 python tools/seed_test_rbac.py 五个演示账号可登录 ✅实测
0-3 演示口令 python tools/set_user_password.py 口令生效(非幂等,重跑等于改密)✅实测
0-4 虚拟资金账户 python -m tools.seed_sim_account_demo 客户 9001 开 10 万初始资金 + 2 只持仓。
不跑这步,/customer/dashboard/ 与 /customer/cash-ledger/ 必然打不开(404「客户未开户」)✅实测
0-5 风控 Agent 配置 python tools/publish_risk_agent_config.py risk_overview/risk_search/risk_evidence 三个工具生效 ✅实测
0-6 Agent Worker(必开!) python -m app.worker 不启动它,所有客服对话都会"超时" —— 见下方说明
0-7 场内行情同步 python tools/sync_market_prices.py 下单的硬前置。不跑它:只有 2 只产品有行情,其余下单直接 503;行情过期后同理(见下方说明)

⚠️ 0-6 为什么是"必开"而不是"必停"

Agent 请求是三段式:API 受理(返回 202)→ Worker 领单执行 → 落结果。

没有 Worker 时,agent_run 会一直停在 status='queued'、worker_id 为空, 前端轮询到底只会显示"客服繁忙/超时" —— 看起来像链路慢,实际是没人处理。 (2026-09-13 的访客浮窗"回答超时"就是这么来的。)

docs/20 里写的是"跑验收脚本前先停 Worker"(免得抢队列)—— 两件事不矛盾: 要跟 Agent 对话就必须开着;要跑验收脚本就关掉。

排查第一步永远是:查 agent_run 最新那行的 status 是不是 queued。

⚠️ 0-7 为什么下单前必须跑行情同步

fin_market_price 是下单的硬前置:TradeService 要求该产品在这张表里有 close_price > 0、total_fund_shares > 0,且 source_updated_at 落在 MAX_QUOTE_AGE 内。 行情过期后没有任何自动刷新机制 —— 表现是下单全线 503 FUND_QUOTE_UNAVAILABLE, 而错误信息只说"某产品行情已过期",看不出根因。

跑一次 python tools/sync_market_prices.py 同时解决两件事: 补齐行情(原本只有 2 只有)+ 把时间戳刷到当前(让新鲜度校验通过)。

数据源是腾讯行情(qt.gtimg.cn):东财的 push2 / push2his 两个行情域名在本环境 实测连不上,而它的净值/概况域名正常 —— 即东财只有行情类接口不可达。 总份额按「已有值 → 腾讯总市值推算 → 季度规模兜底」确定,都拿不到就跳过该产品。

# 一次性把前置跑齐(两个终端)
python tools/seed_test_rbac.py
python tools/set_user_password.py
python -m tools.seed_sim_account_demo
python tools/publish_risk_agent_config.py
python tools/sync_market_prices.py        # ← 下单前必跑

# 终端 1:API
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
# 终端 2:Worker(客服对话依赖它)
python -m app.worker

一键冒烟(推荐先跑这个)

前置跑完后,用一条命令确认 6 条线都是活的:

python tools/e2e_smoke_test.py              # 含写操作(下单、风控处置)
python tools/e2e_smoke_test.py --read-only  # 只看读链路,不动数据

它打真实 HTTP、逐条打印 PASS/FAIL(40 项,覆盖访客/客户/风控/投顾/运营/管理员六条线), 并在库里没有"待处理"预警时自动造一条来验证风控处置闭环。 它不是 pytest 的替代 —— 单测看代码正确性,它看"部署后底座是不是活的"。

模块级变量是 app,不是 application。


1. 页面路由与静态资源 ✅实测

18 个路径全部 200(/ 与 /portal/ 记的是跟随重定向后的最终 200):

分组 路径 实测
跳转 / → /portal/guest/home/
/portal/ → 同上
✅
访客 /portal/guest/home/(5740 B)、products/(2153 B)、product-detail/(2317 B) ✅
客户 customer/login/、dashboard/、holdings/、orders/、transactions/、profit-loss/、cash-ledger/、risk-questionnaire/ ✅ 8/8
员工 employee-console/login/、employee-console/workspace/ ✅
风控 employee-risk/dashboard/(11872 B) ✅
投顾 employee-advisor/dashboard/(2449 B) ✅ 本轮新增
运营 employee-operations/dashboard/(1968 B) ✅ 本轮新增

12 个关键静态资源全部 200:api-client.js / auth.js / login-controller.js / permission-guard.js / layout/app-shell.js / mock-data.js / customer-service-widget/widget.js / widget.css / base.css / tokens.css / operations.css / auth-layout.css ✅


2. 访客(未登录)

# 操作 预期 证据
2-1 打开 /portal/guest/home/ 首屏含品牌与产品摘要 ⚠️待点验
2-2 看页面的数据来源提示条 产品列表页与详情页顶部都有浅色提示条,写明"公开产品接口尚未提供…演示数据,非真实行情" ✅ 页面 HTML 与三个 JS 资源实测都带 data-source-notice / MOCK_SOURCE_NOTICE
⚠️ 渲染位置待点验
2-3 进产品列表 / 详情 能筛选、能按排名排序(?view=ranking)、详情有净值走势 ⚠️待点验(数据来自 common/mock-data.js,非真实接口)
2-4 点登录入口 进 /portal/customer/login/ ⚠️待点验

关于 mock:访客三页用 common/mock-data.js,因为公开产品 HTTP 接口尚未实现。 页面显著标注了来源 —— 这个标注不能删:删掉不会让数据变真,只会让客户以为看到的是真实净值。

2.5 访客智能客服浮窗 ⭐ 本轮重点

公开首页、产品列表页、产品详情页右下角都有客服浮窗。

# 操作 预期 证据
2-5 点浮窗,问一个知识库里有的问题 约 4 秒内给出答案 ✅实测端到端 4.11s
问「基金定投是什么」→ score 0.8203 → 直接答
2-6 问一个知识库里没有的问题 引导拨打客服热线(这是正确行为,见第 9 节) ✅实测 4.09s / 4.82s
2-7 问个人账户相关问题(如"我的风险等级是多少") 提示"该服务需要登录后才能查询您的个人信息" ⚠️待点验(代码路径已确认:_guide_to_login)
2-8 浮窗里点"登录后查询账户" 跳客户登录页 ⚠️待点验

访客令牌机制:POST /api/v1/visitor-tokens(无鉴权) → 返回短期令牌,身份是 roles=("visitor",)、permissions=("agent:run","knowledge:query") ✅实测

⚠️ 访客与客户走的是不同的检索工具名:访客走 query_knowledge(要 knowledge:query), 登录客户走 search_knowledge(要 knowledge:reference:read)。两者必须同时在发布配置 agent_tools/customer_service:<intent> 的白名单里 —— 缺哪一条,对应人群就一问即失败。 2026-09-13 修过一次:当时白名单只有 search_knowledge,访客一问就抛 ForbiddenAgentError。


3. 客户(cust_t / 123456)

3.1 登录与账户域

# 操作 预期 证据
3-1 登录(A034) 200,拿到令牌,跳 dashboard ✅ 200,user_id=9001,expires_in=1800
3-2 dashboard(T001) 总资产 / 可用资金 / 持仓市值 ✅ 200(修前置前是 404「客户未开户」)
3-3 持仓(T006) 持仓表 ✅ 200
3-4 交易流水(T007) 流水表 ✅ 200
3-5 资金流水(T009) 资金明细 ✅ 200
3-6 订单列表(T003) 空列表不报错 ✅ 200
3-7 下单(T002) 报文 {product_code, order_side, quantity},order_side ∈ {buy, sell} ⚠️待点验
需真实 product_code(7002 是 product_id 不是 code)
3-8 盈亏(T001) 收益曲线 ⚠️待点验
3-9 风险测评(ONB001) 返回问卷定义 ✅ 200
3-10 提交问卷(ONB002) 提交后状态变更 ⚠️待点验

3.2 客户客服浮窗

# 操作 预期 证据
3-11 在客户工作台用浮窗提问 与访客同一浮窗,但身份是登录客户 ⚠️待点验(需 Worker 在跑)
3-12 点"转人工客服" POST /conversations/{sid}/handover-requests → 202,工单进管理员队列 ✅ 实测 202(前提:先由 C001 真实创建会话)

转人工的实现口径:按你的要求,只回话术引导拨客服热线,不创建工单用于"答不上来"的场景; 上面 3-12 是客户主动点"转人工"按钮才建工单,两者不是一回事。


4. 员工 / 管理员(admin_t / 88888888)

# 操作 预期 证据
4-1 员工入口登录 进 workspace ✅ 200
4-2 配置发布列表(A002) 版本列表 ✅ 200
4-3 模型端点(A012) 端点列表 ✅ 200(deepseek-flash、qwen-embedding)
4-4 审计查询(A033) 时间/动作/操作者/结果 ✅ 200
4-5 角色 / 权限 / 用户身份(A035–A038) 角色与权限明细 ✅ 200(customer 26 项、advisor 28 项、admin 59 项、operator 2 项、risk_operator 10 项)
4-6 画像候选审核(A039) 候选列表 ✅ 200
4-7 客服转人工工单(ADMIN_HANDOVERS) 工单队列,不返回客户标识与原始正文 ✅ 200
4-8 配置发布四态(A004/A005/A006) 校验→审核→激活,必须带 Idempotency-Key ⚠️待点验(写操作)

平台只提供 RBAC 只读查询:没有改权限的写接口,权限变更走 config_release 发布。这是设计。


5. 风控(risk_t / 666666)—— 对齐 docs/风控业务演示文档/17-*.md

# 操作 预期 证据
5-1 风险概览(RK001) 总量/等级/待处理/超时/重点预警 ✅ 200(total=2 pending=1 overdue=2 levels{高风险:2})
5-2 预警队列(RK002) 每页 5 条、筛选、分页 ✅ 200(2 条:ALDEMO0002、ALDEMO0001)
⚠️ limit 上限就是 5,传 20 会 422 且表格渲染不出来
5-3 队列筛选 关键词/客户号/风险等级/规则码/产品/时间 ✅ rule_code=RW-015 → ALDEMO0002、customer_no=T-CUST → 2 条
⚠️ 风险等级填「高/中/低」:预警对象用「高」,概览 levels 用「高风险」
5-4 预警详情(RK003) 编号/状态/规则/证据/回执/客户 ✅ 200(alert 23 字段 + customer 13 字段)
5-5 八类证据(RK004) customers/products/transactions/capital_flows/holdings/login_records/alerts/notifications ✅ 8/8 全 200
⚠️ 是 holdings,写 positions 会被 422
5-6 通知记录(RK005) 预警编号/类型/发送状态 ✅ 200
5-7 手动扫描(RK006) 200 code=0,要带 Idempotency-Key ✅ 200
5-8 确认接收(RK007) 二次确认;状态不符返回 409 ✅ 409「只有待处理的预警才能确认解决」
5-9 进入调查(RK008)/ 关闭误报(RK009) 误报必须填理由 ⚠️待点验(写操作)
5-10 结案(RK010)/ 升级(RK011) 结案填 resolution、升级填 reason;需先确认接收 ⚠️待点验(顺序:确认 → 调查/误报/升级/结案)
5-11 证据上传(RK012) multipart/form-data ⚠️待点验
5-12 日报(RK013/RK014/RK015) 生成 / SSE 流式 / 多邮箱发送 ✅ RK013 200;SSE 事件类型实测 start/progress/replace

6. 投顾(advisor_t / abc12345)⭐ 本轮新增页面

登录入口同样是员工登录页 /portal/employee-console/login/,登录后自动落到投顾工作台。

# 操作 预期 证据
6-1 用 advisor_t 登录 跳 /portal/employee-advisor/dashboard/ ✅ 实测守卫生效(此前会静默弹回登录页,已修)
6-2 工作台列表 显示"投资目标方案书 · 客户 9001"卡片 + 发布时间 + 方案内容 ✅ 接口实测返回 1 条(content_id=1、customer_id=9001、type=investment_goal_book、published_at=2026-09-13T10:50:17)
6-3 点"刷新" 重新拉取 ADVISOR_PUBLISHED ⚠️待点验
6-4 用别的角色访问该页 risk_operator 会被弹回风控页;operator 弹回运营页 ✅ 实测守卫矩阵

数据口径(ProductRecommendationService.published): 按「本人 + sys_customer_assignment 里名下归属客户」过滤,且覆盖两类内容 —— investment_goal_book(方案书,发布后 review_status='published')与 advisor_recommendation_plan(推荐方案,审核后 'approved')。

不用 data_scope 的原因:投顾持有 promotion:* 这类 all 级权限, IdentityService 会把整个身份的 scope 抬到 all,按它判定会放开到全部客户。 归属关系来自 sys_customer_assignment(逐条授权 + 时间窗校验),比 scope 更窄。

本机归属数据:sys_customer_assignment 只有一行 (customer_id=9001, employee_id=9020, advisor)。 所以投顾现在能看到客户 9001 的交付物;换环境这条数据不跟着走,页面会是空态。

6.5 方案书的审核与发布必须由管理员做

客户确认目标 → 管理员审核方案书 → 管理员发布方案书

review_book 与 publish_book 都带 admin=True(investment_goal_service.py:174/216)—— 投顾虽然有 investment-goal:review 权限码也做不了。这是有意的复核环节,不是缺陷。 实测:投顾调用审核 → 403;管理员调用 → 200 ✅


7. 运营(offsite_t / offsite123)⭐ 本轮新增页面

# 操作 预期 证据
7-1 用 offsite_t 登录 跳 /portal/employee-operations/dashboard/ ✅ 实测守卫生效(此前同样静默弹回)
7-2 场外邮件列表(OFFSITE_MAILS) 分页列表:主题/发件人/时间/状态 ✅ 200 data=dict{items,page,page_size,total}
7-3 收件箱状态(OFFSITE_MAILBOX) 邮箱 / 游标状态 / 阻塞标记 ✅ 200 data=dict{mailbox,status,blocked,last_uid,…}
7-4 用客户/风控访问这两个接口 被拦 ✅ 客户 code=403 当前角色不能操作场外基金流程;风控 code=403 缺少场外基金操作权限

场外线按角色收口(operator/risk_operator/admin/super_admin),不是按权限码。


8. 通用预期(跨页面)

# 情形 预期
8-1 业务失败也返回 HTTP 200 平台把业务错误放在 body.code(如生成失败 HTTP 200 + code=422),前端按 error-codes.js 解析 ✅实测
8-2 403 "没有当前操作权限";按钮可见性应与服务端权限一致 ✅实测
8-3 404 SESSION_NOT_FOUND 被三个异常类共用(会话不存在 / 客户不可访问 / 知识文档不存在),只能看 message ✅实测
8-4 幂等 写请求必须带唯一 Idempotency-Key(api-client.js 用 idempotent: true 标记)✅实测
8-5 多标签 各页面独立。注意:auth.js 现在以 cookie 为权威(跨标签同步),所以同一浏览器两个账号不能并存 ✅实测代码逻辑
8-6 排版 文字不重叠、内容不溢出、表格行高稳定 ⚠️待点验
8-7 缓存 产品列表/详情页的 CSS/JS 已带 ?v=20260913-4;若改了资源仍看不到效果,请硬刷新 ⚠️待点验
8-8 GET /api/v1/knowledge/list 不套 data 信封 它直接返回 {"items": [...], "count": N},而多数端点返回 {"code":…, "data": {…}}。按 data.items 解包会得到 0 条、看着像"库里没数据" ✅实测(联调时踩过一次,误判成接口故障)
8-9 知识治理端点 上传是 POST /api/v1/knowledge/upload(不是 /documents,用后者会 405);删除是 DELETE /api/v1/knowledge/{id},语义是标记 expired + 投向量删除事件,不是物理删除
8-10 各端点的 data 形状不统一(写联调脚本时别猜) 实测:账户看板 data.account + data.summary、持仓 data.holdings、成交明细 data.transactions、资金流水 data.entries、角色列表 data(裸数组)、知识列表 items(不套 data)。我写全流程脚本时连续猜错三个字段名 ✅实测
8-11 创建类接口返回 201 而非 200 下单 T002、创建会话 C001、访客令牌 V001 都是 201。判"成功"要用 status in (200,201) ✅实测

9. ⚠️ 看起来像 bug、其实是设计(测试时最容易被误判的三件事)

9-1 "客服答不上来,总是引导转人工" —— 这是置信度防线在工作

客服命中知识后要过两道阈值(customer_service.py:148-150):

HIGH_SCORE = 0.75   # ≥ 0.75 直接答
MID_SCORE  = 0.55   # 0.55~0.75 之间,必须同时满足 MIN_GAP
MIN_GAP    = 0.07   # top1 领先次优的最小间隙

实测三条,逐条对上:

提问 top1 分数 次优 间隙 判定
基金定投是什么 0.8203 0.5886 0.2317 ≥0.75 → 直接答 ✅
南方沪深300ETF的起投金额是多少 0.6818 0.6784 0.0034 间隙 < 0.07 → 转人工
购买基金需要什么条件 0.5221 0.5162 0.0059 分数 < 0.55 → 转人工

第二问如果硬答,会答成"南方结构性存款(挂钩型):起投金额 20 万元" —— 那是另一个产品, 而且客户问的是场内 ETF。转人工在这里是正确行为,符合你定的"不会的就转人工,首先要保证稳定性"。

真正要改的是知识库内容,不是代码:检索命中的是一套"南方科技有限公司 个人理财产品手册" 的素材(PROD-*/FAQ-*/POL-*),里面没有本项目的场内 ETF 条目,所以问 ETF 注定分数上不去。

9-2 "客户能读投顾的接口" —— 不是越权

GET /api/v1/advisor/recommendations/published 客户调用返回 200,但 SQL 里按 customer_id IN (本人 + 归属客户) 过滤,读的是自己的。风控返回 403 是因为没有 product-recommendation:read:self 权限码。用"能访问"判断越权会误判。

9-3 "投顾发布不了方案书" —— 是有意的复核环节

见 6.5。审核与发布都要求 admin=True。


10. 我做了什么验证(可复现)

项 方式 结果
18 个页面路径 + 12 个静态资源 逐个 GET 全部 200
五个角色登录 POST /api/v1/auth/tokens × 5 全部 200
各角色登录后落点与越权矩阵 真实执行 auth.js 的守卫函数(node + DOM 桩) advisor→投顾页、operator→运营页、risk→风控页、admin→工作台;交叉误入能弹回自己家
访客客服问答端到端 与浮窗同链路(V001 → R001 → 轮询 R002) 4.11s / 4.09s / 4.82s,三条全部 succeeded
投顾页数据 走完整业务流(客户确认→管理员审核→管理员发布)后复查 投顾读到归属客户 9001 的方案书;客户只读到自己的;风控 403
平台门禁 ruff / mypy / pytest / 表审计 / 文档检查 ruff 干净 · mypy 249 文件 0 错 · 单元+契约 1376 passed 2 skipped · 集成 104 passed · 89 张业务表 · 文档 55 份无编号冲突
前端 JS 语法 7 个文件按 ESM 解析检查 全部通过

联调中发现并已修的问题:

  1. T001/T009 恒 404「客户未开户」 —— tools/seed_test_rbac.py 把 fund_account_status 硬编码成 'closed',与 create_test_user.py 的 {customer: 已开户, employee: closed} 不一致。 已改为按 user_type 取;配合 seed_sim_account_demo 后 T001/T006/T007/T009 全部 200。
  2. rule_code 筛选恒为空 —— risk_repository.py 用 .contains([code]),SQLAlchemy 编译成 LIKE, 而 trigger_rule_codes 是 JSON 数组。已改为 func.json_contains(...),扫描去重处同一写法一并修。
  3. 误提交的 .agents/skills(15 个文件) —— AI 助手配置,与本项目无关,已移除并加入 .gitignore。
  4. 投顾/运营登录后静默弹回登录页 —— staffHomeForRoles 缺这两个角色的落点, 进 workspace 后被 requireAdmin 弹回且不带任何提示,看起来像"登录没反应"。 已补两个落点与 requireAdvisor/requireOperator 守卫。
  5. 访客客服浮窗一问即失败 —— 客服代码让访客走 query_knowledge,但发布配置的三个知识意图 只发了 search_knowledge,工具白名单直接抛 ForbiddenAgentError。已补发 query_knowledge (保留 search_knowledge 给登录客户)。
  6. 访客页的"演示数据"声明被删掉、但假数据还在渲染 —— 已恢复 MOCK_SOURCE_NOTICE、 两个页面的提示条与样式,并给 link/script 加版本参数(此前会被浏览器缓存)。
  7. 发布脚本会静默丢提示词 —— publish_customer_service_config.py 只继承 platform_config_item,把 customer_service_chitchat 提示词漏在了旧版本里。 已改用 effective_snapshot() 读全三张受管表、补提示词搬运与条数硬校验;丢失的提示词已按原文恢复。

11. 已知问题(未修)

项 说明
知识库没有场内 ETF 内容 检索命中的是"南方科技理财产品手册"素材,问 ETF 类问题必然分数不足而转人工。属知识内容工作,需业务提供素材后灌库
fin_knowledge_meta 表为空 Milvus 里有知识(FAQ-*/PROD-*/POL-* 都能检索到),但 MySQL 元数据表 0 行 ⇒ 管理端列表(GET /api/v1/knowledge/list)看不到内容、也无从删除。元数据与向量不一致
品牌名三处不一致 已处理(2026-09-13) 已按项目方决定统一为「南方财富」:customer_service_rules.py 的防诈骗/转人工话术、risk_agent.py 与 risk_analysis_service.py 的风控助手名、customer_service.COMPANY 常量、app/static/index.html 旧联调页、风控页助手标题,以及闲聊提示词(已重发为 release 308 / v5)。
⚠️ 仍是旧品牌的地方:Milvus 里的知识素材(RAG-*/NF-* 写"奶龙基金"、PROD-* 写"南方科技")—— 属知识数据,需重灌才能统一。
⚠️ 不能改:fin_product.fund_manager = "南方基金",它被 market_quote_sync_service / product_history_sync_service 当过滤条件用
知识库缺场内基金条目 已产出草稿 docs/42-场内基金知识条目草稿.md(含 20 只产品,数据取自 fin_product),待审核后入库。草稿里费率等缺失字段一律标"以交易页面为准",未编造数字
ADVISOR_GOAL / ADVISOR_ANALYSIS 声明未用 投顾页只调了 ADVISOR_PUBLISHED;且 portfolio-analysis 的 body 必须是 {}(空模型 + extra="forbid")
widget.js 里的 portal:auth-changed 监听 是死代码 —— auth.js 从不派发该事件(它用 BroadcastChannel)
同一浏览器不能并存两个账号 见 8-5,是跨标签同步的代价,若不符合预期需要改回

附:演示账号

角色 用户名 密码 user_id 入口
客户 cust_t 123456 9001 /portal/customer/login/
风控专员 risk_t 666666 9002 /portal/employee-console/login/
管理员 admin_t 88888888 9003 同上
运营 offsite_t offsite123 9006 同上(→ 运营工作台)
投顾 advisor_t abc12345 9020 同上(→ 投顾工作台)

⚠️ tools/set_user_password.py 的内置演示规则只写了 9001/9002/9003; 9006 与 9020 的口令是建号时单独设的,换环境重建时会没有密码,需要用 python tools/set_user_password.py --user 9006 --password <口令> 补设。 另:9004 review_t、9005 offsite_worker 是占位符,登不了。