## 对话内图表(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 改动, 两者无法按文件切分,故同批提交。
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 上。
公开产品数据不得与登录后的真实账户数据混用。
三点口径(改前端前先读):
change_pct可能是null(行情只同步过一个交易日时算不出涨跌)。 调用方必须显示"暂无",不得当成0——formatPercent收到null会渲染成+0.00%, 那等于告诉客户"今天平盘"。- 历史净值曲线没有数据源:
fin_nav_history目前 0 行,详情页因此不画走势图, 并显式说明"尚未接入"。此前那条曲线是 mock 里 12 个编造点位 —— 走势图最容易被当成真数据。 - 产品级披露在
common/product-notes.js(如 510300 的"同指数参考产品,非本公司发行")。fin_product没有这个字段,所以它留在前端;新增需要披露的产品时改那一份。 凡渲染公开产品的页面都要挂data-source-notice说明数据来源。
客户页面均由 common/auth.js 执行入口守卫,接口路径只在 common/api-client.js 的端点表登记。