Files
group_fqcd_jr/app/static/portal
张胜宇 2b408dc602 feat(W29): 对话内区间涨跌图(零新数字路径)+ 客服业务层事项轴
## 对话内图表(W29)

按「零新数字」路径实现:图只画答复正文里**已经写出来**的数字,不引入任何新数值
(新不变量 INV-8:图内每个数值必须能在同轮文字中找到)。

- E6 出口 data 增 trend_chart(纯增量,只搬运模板已写出的数字:最新净值 /
  四个区间涨跌 / 区间首末净值 / 区间高低 / 数据日期)
- 新增 trend-chart.js:**全 DOM API 构建**(widget.js 的 addMessage 一律 textContent,
  图表若拼 HTML 串等于把那层 XSS 防护重新打开),涨红跌绿(中国市场惯例)
- widget.js 从 result.tool_calls.data 取数 ⇒ **零后端读取改动**
  (result.data 只对 financial_nl2sql 暴露;走 tool_calls 这个已落库的 JSON 列免会签)

验证(真实净值数据 511810 / 160 个净值点、区间 -9.19/5.28/15.27/-6.53):
- jsdom 渲染 11/11 通过;颜色序列 [绿,红,红,绿]
- INV-8 独立断言:图上 18 个可见数字 **100% 命中答复正文**(extra_numbers=[])
- 空数据三态返回 null;label 里塞 <img onerror=...> 后 DOM 中 img 元素数 0

## 客服业务层(W28 遗留,本次一并提交)

- app/core/service_topic.py(新增):业务事项轴 —— 12 个 SVC-* 事项码 ×
  处理主体 × 留痕等级;只做留痕统计,不参与任何判定
- customer_service.py 的 E5b 双段话术(删掉「换个说法再问我一次」= 把问题推回客户)、
  kb_miss 显式声明(不再靠文本比对,话术一变化比对就静默失效)、
  意图漂移护栏(分类器判闲聊但含业务实质则不采信)
- customer_service_rules.py:substantive_business_request 等判据

注:customer_service.py 同时承载 W29 的 trend_chart 与上述 W28 改动,
两者无法按文件切分,故同批提交。
2026-09-22 10:12:14 +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. 历史净值曲线没有数据源:fin_nav_history 目前 0 行,详情页因此不画走势图, 并显式说明"尚未接入"。此前那条曲线是 mock 里 12 个编造点位 —— 走势图最容易被当成真数据。
  3. 产品级披露在 common/product-notes.js(如 510300 的"同指数参考产品,非本公司发行")。 fin_product 没有这个字段,所以它留在前端;新增需要披露的产品时改那一份。 凡渲染公开产品的页面都要挂 data-source-notice 说明数据来源。

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