Files
group_fqcd_jr/app/static/portal
张胜宇 2a55269e20 feat(W34-W36): 客服双通道口径分离收口 + 会签 20/21/22 落地 + 演示启动器修复
W34 · 会签 20/21/22 三项落地(先立单、经授权、后动手)
- 会签 20(白名单外):runtime_config_service 新增 load_collection_routes() /
  collection_routes() / _first_collection_name(),首次消费既有 JSON 列 collection_routes;
  消费方 customer_service 走「配置优先、缺失回落代码常量」。零 DDL;该列当前全为 None
  ⇒ 实际走回落路径,行为与改动前一致。
- 会签 21(类 3 + 融合层):retrieval_fusion 新增 fuse_rrf() + RRF_K(排名融合,只吃名次
  不吃分数 ⇒ 异质分数不可能污染判定分,best_vector_score 仍只取向量路原始 cosine);
  knowledge_search_service::search() 新增 literal_parallel: bool = False(默认值使行为
  逐字等同现状)。工具层透传未做 —— 那需改 KnowledgeSearchInput 契约(extra="forbid"),
  超出本单范围。
- 会签 22(发布配置 + bootstrap):customer_service 新增 INTENT_MARKET_QUOTE 常量 +
  TREND_WHITELIST_INTENT_CANDIDATES(按优先级回落)+ _trend_whitelist_intent()
  (运行时自检 + 自动回落,强于「仅报错」)。发布配置 customer_service:market_quote
  (release 260,allowed_tools=['query_fund_trend'])已写入并回读校验(9 → 10 行)。
  刻意未加入 supported_intents:授权维度与判定维度解耦,不动判定分布。
- 阶段 0:customer_service_rules 新增 normalize_query() + QUERY_SYNONYMS + 等级代号大写
  (纯函数;同义表只收纯书写差异,语义类同义留待金标 A/B 后逐条加;调用方默认不启用)。

W35 · 判定口径与融合口径分离(修 A-01 / C-04 / I-02 / E-04 四条)
- _dual_route_output 返回值新增 vector_order(向量路原始 doc_id 顺序、去重);
- 新增 _vector_decision_hits() 据此还原「判定序列」(带向量分的 basic 补位块回补首位;
  无 vector_order / 空 / id 全对不上 ⇒ 返回 None 回落原分支);
- _answer_from_knowledge 的 score / gap 与原文直返的 best 改从向量路原始序列取。
  语义边界(刻意):_evidence_pack / _exit_clarify / _answer_from_evidence 仍吃融合序列
  —— 融合的收益只留在「给哪些块、什么顺序」,符合三层分数分离约束。

W36 · 选块口径归一(收口最后一条 E-01)
- 新增 _pack_order():order 命中的块排前,其余按原相对顺序追加在后;
- _evidence_pack 新增 order= 参数,三处遍历 hits → ordered,top 由 hits[0] → ordered[0];
- _answer_from_knowledge 传入 order=[judge 的 doc_id 序列];judge is None ⇒ None
  (开关关闭时逐字零改动)。order 只当排序键、不当过滤器 ⇒ 证据包成员集合不变。

演示环境与文档
- start.ps1 / demo.ps1 默认端口 8000 → 8099(与 README / docs/06,07,09,14,15,32 /
  tools/smoke_check.py / login_console.py 的全仓口径对齐;字节级定长替换,保住
  UTF-8 BOM + CRLF,字节数不变);
- portal/README.md 更正 fin_nav_history 过期口径(「0 行」→ 实测 2494 行 / 20 个产品 /
  nav_date 覆盖 2026-03-18—2026-09-13)。

测试(新增 3 个文件、补强 2 个)
- 新增 tests/unit/service/test_decision_scope_w35.py(10 条)、
  tests/unit/service/test_evidence_pack_order_w36.py(13 条)、
  tests/unit/service/test_customer_service_trend_chart_inv8.py(INV-8 字面级判定,
  纳入 pytest 门禁,此前只在 jsdom 脚本里覆盖);
- 补强 tests/unit/core/test_customer_service_rules.py(normalize_query 7 条)与
  tests/integration/test_customer_service_trend_chart_persistence.py(图内每个数字
  都必须在答复正文出现过,判定口径与 INV-8 单测一致)。

验证
- 全量 pytest:2642 passed / 3 skipped / 0 failed(基线 2629 + 新增 13);
- 55 条金标真实链路 A/B 四组:off / norm / dual / on 均 55/55 = 100%
  (改前 dual 92.7%、on 90.9%);M-4 事实正确率恒 100%、M-7—M-10 全 0;
- 红线四条守住:融合/精排层仍不持 Milvus 客户端(INV-1)、阈值一字未动、零 DDL;
- 三个实验开关 CS_DUAL_ROUTE / CS_RERANK / CS_QUERY_NORM 仍默认关闭。
2026-09-22 18:05:59 +08:00
..

Portal 路由与角色映射

正式门户使用原生 JavaScript、原生 CSS 与同源 fetch,静态资源统一从 /static/portal/ 加载。

路由 角色 数据源
/portal/guest/home/ 访客 / 全部 P001(推荐位取前 3 只)
/portal/guest/products/ 访客 / 全部 P001
/portal/guest/product-detail/?code= 访客 / 全部 P001(不含历史净值曲线)
/portal/customer/login/ 未登录客户 A034
/portal/customer/dashboard/ customer / admin T001、T002
/portal/customer/holdings/ customer / admin T006
/portal/customer/profit-loss/ customer / admin T001
/portal/customer/orders/ customer / admin T003
/portal/customer/transactions/ customer / admin T007
/portal/customer/cash-ledger/ customer / admin T009
/portal/customer/risk-questionnaire/ customer ONB001、ONB002
/portal/employee-console/login/ 未登录员工 / 管理员 A034
/portal/employee-console/workspace/ admin / super_admin A002-A006、A012、A033、A035-A040、客服转人工管理接口
/portal/employee-risk/dashboard/ risk_operator / admin / super_admin /api/v1/risk/**、R001-R003
/portal/employee-advisor/dashboard/ advisor / admin / super_admin /api/v1/advisor/recommendations/published(本人 + 名下归属客户的已发布交付物)
运营账号默认进入 /portal/employee-operations/offsite/ operator / admin / super_admin /api/v1/offsite-fund/mails、/api/v1/offsite-fund/mailbox-status

投顾页的数据口径:接口按「本人 + sys_customer_assignment 里名下归属客户」过滤,且同时覆盖 investment_goal_book(方案书,发布后 review_status='published')与 advisor_recommendation_plan(推荐方案,审核后 review_status='approved')两类内容。 方案书的审核与发布都要求管理员(investment-goal:review / publish 都带 admin=True), 投顾自己发不出来 —— 这是有意设计的复核环节,不是缺陷。

客服浮窗(common/customer-service-widget/)由公开首页、基金产品页和客户工作台统一挂载, 挂载点是 common/layout/app-shell.js 的 mountShell() —— 按页面形态白名单 (public / customer)挂,员工四类工作台刻意不挂;样式也是同一个地方注入, 避免九个页面各写一份 ?v= 版本号。

访客使用 /api/v1/visitor-tokens 获取短期令牌(角色 visitor,权限只有 agent:run + knowledge:query),因此必须走 query_knowledge 这个工具名;登录客户走 search_knowledge。两者都要出现在发布配置的 agent_tools/customer_service:<intent> 白名单里,缺哪一条,对应人群就一问即失败。

公开产品数据来自 GET /api/v1/products(编号 P001,见 docs/05 §19), 产品与净值取自 fin_product(status='上市')、行情取自 fin_market_price 的最新一行。 访客页面通过 common/visitor-token.js 取短期访客令牌后调用;该文件是访客令牌的唯一实现 (客服浮窗也用它),不要在页面里另写一份,否则存储 key 与过期判断迟早不一致。 公开页一律用访客令牌,即使浏览器里还留着登录令牌:api-client.js 对调用方 显式传入的 Authorization 不再覆盖(此前会覆盖,于是同一个公开页对访客和已登录 用户显示不同内容)。客户工作台则相反,走登录令牌并建会话(C001), 这样多轮澄清与「转人工」才能挂到同一个 session_id 上。 公开产品数据不得与登录后的真实账户数据混用。

三点口径(改前端前先读):

  1. change_pct 可能是 null(行情只同步过一个交易日时算不出涨跌)。 调用方必须显示"暂无",不得当成 0 —— formatPercent 收到 null 会渲染成 +0.00%, 那等于告诉客户"今天平盘"。
  2. 历史净值曲线已有数据源(W31 起接入;本节此前写的"0 行"已过期): fin_nav_history 实测 2494 行 / 20 个产品,nav_date 覆盖 2026-03-18—2026-09-13, 由 tools/sync_nav_history.py 从东财净值接口同步。详情页 renderChart 取到点位就画折线, 仅当该产品没有历史时才退化为"历史净值数据尚未接入,暂不展示走势图"。 ⇒ 演示走势图请用真实在场内、已同步的产品(如 159382、511810)。 此前那条曲线曾是 mock 里 12 个编造点位 —— 走势图最容易被当成真业绩,故此处口径必须准确。
  3. 产品级披露在 common/product-notes.js(如 510300 的"同指数参考产品,非本公司发行")。 fin_product 没有这个字段,所以它留在前端;新增需要披露的产品时改那一份。 凡渲染公开产品的页面都要挂 data-source-notice 说明数据来源。

客户页面均由 common/auth.js 执行入口守卫,接口路径只在 common/api-client.js 的端点表登记。