第二十二轮 · 七项生产级完整优化

订阅留存与搜索 · SQL 快照复用 · 域外守卫默认放行 · 问句描述 · 弹窗统一 · 手动新建 · 关联理解

七件事里有两件是数据会丢(① 删订阅连带清空站内信)、两件是白花钱 (② 每轮重生成 SQL、③ 正常业务问题被域外守卫拦下),其余是体验与正确性质量。 本轮按生产级做法收口:可观测、可回退、不误伤,并把每一条都变成门禁里钉住的断言。

round22-wiring 102 / 0 item22 夹具 7 + 15(独立 runner) item16/18/19/20/21 防回退全绿 javac / SYMBOL / SCSS / SFC / i18n 全绿 DDL 未执行(沙箱无库,如实记录) 版本 v35 | 2026-09-22

0七项一览 — 用户报的话 → 真因 → 修法

#用户看到的现象真因(排查结论)本轮的修法
① 删掉一条订阅后,它以前推给我的站内信一起没了;站内信也没法按时间/问句找 inbox() 是"先查我当前还没删的订阅,再按这些订阅 id 取推送" —— 订阅一删,历史推送被连带排除 推送时把 owner_user / title_snapshot / question_snapshot 写成快照;站内信按属主+快照独立检索,与订阅行是否还在解耦;加关键词/时间/状态搜索(含 H5)
② "订阅每次执行都重新生成 SQL,慢、也费模型" 同一订阅每轮都走完整 NL2SQL;DiSqlCache 是分钟级 TTL 的共享缓存,按天/按小时跑的订阅必然 miss 订阅持久化 sql_snapshot,执行时带回请求 → 引擎直接复用并记策略 CACHE_HIT;成功执行后回写新快照
③ "系统用户数是多少""深圳总公司用户数量明细列表"被判成域外问题,问不出来 守卫停用词表里有裸的"用" → "用户数"被截成"系统"+"户数",词表里明明有"用户"也匹配不上(还有"人数"被当量词剥掉、滑窗只在 ≥4 字启用) 停用词修正 + "用+图表类型"正则等价补齐 + 滑窗放宽;forceQuery 默认 true(默认放行,严格拦截改为显式选择)
④ 订阅回执只写「订阅问句:系统用户数」——半截话,看不懂要查什么 回执/标题直接截用户原话,而原话常是碎片(无谓语、含指代与调度残留) 新增 SubscriptionQuestionDescriber:清噪声 → 去指代 → 按形状补谓语(指标类"统计"、清单类"查询"),回执展示「统计系统用户数」并括注执行原话
⑤ 「上报问题」和「对比版本」两个弹窗两种风格摆在同一页 上报弹窗用 Element 默认皮,且它的 traceId 行写在 Bubble.vue 的 scoped 块里 —— 弹窗 append-to-body 到 body,scoped 样式根本落不到那条 DOM(等于没样式) 抽入口级全局皮肤 dq-dialog.scss(与订阅表单同一套 token),两个弹窗都挂 .dq-dialog,元信息行/提示行共用类
⑥ "我的订阅"里没有新建入口,只能从某条回答下方订阅 表单入口只有一个(回答下方「订阅这条问数」),问句来自上下文、只读 新增 POST /chat/subscription/manual:手输问句 + 选数据源 + 频率/阈值,标题自动整理,走同一条幂等 upsert;Web 与 H5 都补入口
⑦ "给我列出研发部门的所有用户"生成的 SQL 错 join(sys_scope→sys_post),结果不对 提示词只要求"按登记关系 JOIN",没说清名称过滤要下推到键值、也没说关系表只是中转;生成侧没有确定性体检兜底 关联提示词补 3 条硬要求(过滤下推/关系表纪律/方向与基数);新增 JoinSemanticsCheck J1~J5 判据 + JoinSemanticsAdvisor 在两条引擎链路做只上报不拦截的体检

1① 删除订阅不再抹掉站内信 + 站内信可搜索 — 数据留存优先

修复前:消息跟着订阅一起消失
inbox(owner) → 先 mineOf(owner)(我还没删的订阅)
→ 再 run WHERE subscription_id IN (这些 id) AND notified=1
删除订阅(逻辑删)后,它推送过的每一条都取不到了 —— 用户要回看"上周那天到底是多少"时一片空白。
修复后:消息按属主 + 快照独立检索
推送时写入 owner_user / title_snapshot / question_snapshot
→ run WHERE notified=1 AND owner_user=? [AND 时间/关键词/状态]
订阅删了、改了名,历史消息都还在、仍显示当时的标题与问句;订阅信息只用于补"当前"的数据源等展示字段。
搜索条件关键词(标题 / 问句 / 回答 三列 OR,IFNULL 兜住 NULL —— SQL 三值逻辑下 NULL LIKE 会把整行丢掉)+ 时间范围 + 运行状态 未读角标同样按属主统计,订阅删了也能把未读清掉(否则红点永远消不掉) 上线兼容DDL 回填前写入的老行 owner_user 为空 → 走"旧数据支"(按我当前未删订阅归属)兜住,升级期站内信不会整片空白 H5保持 Vant 原生:van-search + van-dropdown-menu(今天 / 近 7 天 / 近 30 天 + 状态)
db/2026-09-22_subscription-inbox-retention-and-sql-snapshot.sql(沙箱无库,按惯例不执行)
-- 推送流水:属主与展示快照(站内信据此独立检索,删订阅不丢消息)
ALTER TABLE di_subscription_run ADD COLUMN owner_user VARCHAR(64) NULL;
ALTER TABLE di_subscription_run ADD COLUMN title_snapshot VARCHAR(255) NULL;
ALTER TABLE di_subscription_run ADD COLUMN question_snapshot VARCHAR(512) NULL;
CREATE INDEX idx_di_run_owner_time ON di_subscription_run(owner_user, notified, run_time);
-- 订阅:SQL 快照(② 复用)+ 单次/间隔既有列不变
ALTER TABLE di_query_subscription ADD COLUMN sql_snapshot TEXT NULL;
ALTER TABLE di_query_subscription ADD COLUMN sql_snapshot_time DATETIME NULL;

2② 订阅执行复用 SQL 快照 — 省模型、且同一条 SQL 口径稳定

修复前:每轮都重新生成
到点 → NL2SQL 全链路(Schema 筛选 → 提示词 → 模型 → 修复链)→ 执行
缓存救不了:DiSqlCache / 两层结果缓存都是分钟级 TTL 的共享缓存,按小时/按天跑的订阅必然 miss。更隐蔽的问题是SQL 漂移 —— 同一个订阅每天生成的口径可能不一样,历史值/环比不在同一基准上。
修复后:快照优先(CACHE_HIT)
到点 → 带 preferredSql → 引擎直接采用、策略记 CACHE_HIT → 执行
快照缺失(首跑 / 上次失败)→ 正常生成并在成功后回写新快照;追问(FOLLOW_UP)与"重新回答"不走快照(那两种语义要的就是重新生成)。
安全边界快照来自本系统自己生成并成功执行过的语句,仍走同一个 SqlGuard / 审计 / DryRun / 执行链路,不是"任意 SQL 通道" 门禁DTO/ChatState/Service/引擎四层落点断言 + 夹具验证 sqlStrategy="CACHE_HIT" 分支条件 为什么不做成缓存订阅间隔(小时/天)远长于所有内存缓存 TTL;只有随订阅持久化才谈得上复用

3③ 域外守卫:默认放行 + 误杀根因 — 误杀的代价远大于漏拦

用户报的两句都被判成"和数据源无关"。用真实守卫跑一遍就能看到根因不在判据松紧,而在分词:

问句修复前内容词判定修复后内容词判定
系统用户数是多少[系统, 户数]域外 ✗[系统用户数]域内 ✓
深圳总公司用户数量明细列表[深圳总公司, 户数量, 列表]域外 ✗[深圳总公司用户数量]域内 ✓
研发部门的用户[研发部门]域内 ✓[研发部门, 用户]域内 ✓
今天天气怎么样[天气]域外 ✓[天气]域外 ✓(不误放)
根因 1停用词表里有裸单字"用"(本意是剥"用饼图"这类说法)→ 任何含"用"的业务词都被截断 根因 2"人数"被当纯量词剥掉 → "用户人数 / 员工人数"同样匹配不上 根因 3滑窗兜底只在候选词长度 ≥4 时启用 → 被截断后的 3 字残留("户数量")拿不到兜底 修法把"用"从停用词拿掉,改用"用 + 图表类型"的正则等价剥离;"人数"保留为业务词;滑窗对所有 >2 字候选生效(方向仍是宁放过不误杀) 默认值forceQuery = true:默认跳过守卫,严格拦截变成前端显式传 false 的选择 —— 漏拦的代价是"多生成一条查不到数据的 SQL"(有结果体检兜底),误杀的代价是"用户问不出来" 口径变更如实记录:第十六轮定的是"库外问题不生成 SQL(默认拦)",本轮按用户要求改为默认放行

4④ 订阅问句的"问题描述" — 截图那句话的修法

修复前(截图复刻)
🔔 已为你更新问数订阅 ✅(没有新建重复订阅)
订阅问句:系统用户数
执行频率:每天 16:15
半截话:没有谓语、不知道要"数一数"还是"列出来";订阅中心列表一屏都是这种碎片。
修复后
🔔 已为你更新问数订阅 ✅(没有新建重复订阅)
订阅问句:统计系统用户数(执行时按「系统用户数」这句问数跑)
执行频率:每天 16:15
描述会补谓语、去掉订阅指令残留与指代;同时括注执行原话,用户不会以为系统改了他的问句(执行语义一点没动)。
三步整理① 清噪声(疑问语气 / 订阅指令残留 / 时刻片段 / 包裹引号)② 去指代("这个问数/这条")③ 按形状补谓语:指标类 → 统计(数/数量/多少/占比/趋势/排名…),清单类 → 查询(明细/列表/有哪些…) 兜底剥完只剩结构助词("把这个问数…"→"把")时返回「(未指定问句)」,不产出怪句 落位回执文案、订阅标题(列表 ≤18 字 + "…")、站内信 questionDesc;Web/H5 站内信优先展示描述,原话挂在 title 悬停可见 夹具15 条(含截图原句、"把系统用户数每天9点发我"、空输入兜底、长标题截断)

5⑤ 两个弹窗合成一套皮肤 — 先找到"样式为什么没生效"

真因弹窗 append-to-body(teleport 到 body)→ 组件 scoped 样式里的 .report-trace 永远匹配不上,等于没写过;相比之下"对比版本"弹窗是自绘灰底灰字 + 自绘表头 —— 两个弹窗摆一起就是两种风格 修法与 session-menu.scss 同一套路:抽入口级全局皮肤 assets/styles/dq-dialog.scss(在 main.ts 导入,避免路由懒加载导致"先访问哪个页面决定样式在不在"),两个弹窗都挂 .dq-dialog 统一项页头渐变 #fffaf5→#fff / 描边 #f3e4d6 / 标题 #4a382c 17px 700 / 关闭按钮 36×36 r10(hover #fff0e2)/ 控件 min-h 40、r10、描边 #f0dfcf(hover #ffc58f)/ 底栏按钮 min-h 38 / 主按钮渐变 #ff9a3d→#f97316 共享类.dq-dialog-meta(traceId 行 / 口径摘要行)、.dq-dialog-hint(辅助说明行)—— 两处同名同类,风格不可能再分叉 安全约束本文件所有选择器以 .dq-dialog* 起头,不外溢到别的弹窗;上报弹窗宽度 440→520 与订阅表单对齐

6⑥ 我的订阅:手动新建 — 一个入口到闭环

  1. 入口:我的订阅页签顶部「新建订阅」(空态里也放一个,避免"没有订阅时反而不知道从哪建")→ 同一弹窗进入 manual 模式。
  2. 模式差异:manual = 问句可输入(文本域)+ 数据源可选(默认当前数据源);回答下方的入口仍是 fromAnswer(问句与数据源由上下文带出、只读)。
  3. 服务端:POST /chat/subscription/manual → 校验(问句/数据源/登录用户)→ 标题走缺陷④ 的整理器 → 默认 DAILY 08:00 + notifyOn=ALWAYS → 汇入同一条 幂等 upsert(同用户+同数据源+同问句+同计划 = 更新已有那条,不产生重复订阅)。
  4. 审计:写入 SUBSCRIPTION_CREATE(动作名保持字面量,SEC-08 审计门禁继续可判)。
  5. H5:同一入口(openSubscribeForm('') → manual)走 /manual;表单保持 Vant 原生。

7⑦ 表关联理解 — 用户给的判据就是验收标准

现场错 SQL(串到 sys_post,且按中文名过滤)
SELECT su.user_name, sp.post_name
FROM sys_scope ss
JOIN sys_user su ON ss.user_id = su.id
JOIN sys_post sp ON ss.post_id = sp.id      -- 无关于本问
JOIN sys_dept sd ON ss.dept_id = sd.id
WHERE sd.dept_name = '研发部门'             -- 名称直接压维表
名称可能重名/写法不一致("研发部门" vs "研发部"),直接按名称过滤会漏行、错行;用户明确说他要的是"按部门唯一标识筛中间表"。
目标形态(用户给的口径)
SELECT su.id AS user_id, su.user_name AS 姓名
FROM sys_scope ss
JOIN sys_user su ON ss.user_id = su.id
JOIN sys_dept sd ON ss.dept_id = sd.id
WHERE ss.dept_id = '1664459372635472503'  -- 部门主键下沉到关系表
登记关联:sys_scope.post_id→sys_post.id、sys_scope.user_id→sys_user.id、sys_scope.dept_id→sys_dept.id(N:1)。
生成侧(提示词)关联段新增 3 条硬要求:过滤下推(先取主键,再把键值压在关系表/明细表的键列上,示例给 EXISTS 形态)、关系表纪律(中转表不当结果主体,去重用 DISTINCT 主体主键 或 EXISTS)、方向与基数(连接条件两侧同语义层级) 验收侧(判据)JoinSemanticsCheck:J1 臆造连接键 · J2 展示名过滤未下推 · J3 关系表当结果主体 · J4 跨关系 COUNT(*) 放大 · J5 主体表缺失 —— 夹具 7/7(含用户现场错 SQL 与目标 SQL) 运行时(顾问)JoinSemanticsAdvisor 在流式链路与同步链路(订阅执行)都做体检:命中即 WARN 日志 + 写入 ChatState.sqlRelationNotes;只上报不拦截 —— 拦截会把"样式不同但正确"的 SQL 也挡掉 为什么不全自动改写静态判据无法确证"该不该连这张表"(业务选择问题);本轮的定位是"生成侧说清 + 结果侧可判 + 运行时可观测",把口径交给治理侧沉淀进「表关联关系」配置
夹具已覆盖用户原句:目标 SQL 0 违规;现场错 SQL 恰好命中 1 条 J2;臆造键 → J1;COUNT(*) → J4;关系表当主体 → J3;合法多连不误报;问用户却不查用户表 → J5。

8门禁与证据 — 本轮新增 / 复跑

本轮接线门禁(新增)
102 / 0
tools/round22-wiring-check.sh:七项逐条落点
关联语义夹具(新增)
7 / 0
J1~J5 + 用户现场 SQL 与目标 SQL
问句描述夹具(新增)
15 / 0
含截图原句 / 引号 / 兜底 / 截断
防回退(19~21 轮)
45 / 28 / 55 / 83 / 44
item16 / 18 / 19 / 20 / 21 全 0 失败
接线(18~21 轮)
全绿
round18/19/20/21-wiring 复跑 0 失败
样式 / 结构 / 文案
87 / 297 / 1621
SCSS 真编译 / SFC 块结构 / i18n 双语
后端
145 / 7409 / 143
javac 145 文件 SYNTAX-FAIL=0|SYMBOL 823 个 .java 0 失败|P1 0 失败
前端类型(靶向)
22 / 22 exit=0
含本轮 tsconfig.round22.json(API+Bubble+VariantCompare+Web+H5;不含 main.ts)
沙箱边界(如实记录,不粉饰):① 本轮 DDL 只提交脚本、未执行(沙箱无实例库),真机需要按 db/2026-09-22_*.sql 建列建索引并回填,回填前站内信走"旧数据支"兜底;② 无 Maven / 依赖 jar,后端仍以"真实 javac 17 编生产源码 + 靶向夹具"验证,模块级 mvn 编译未覆盖;③ 无浏览器,前端以"靶向 vue-tsc + SCSS 真编译 + SFC 结构 + 结构不变量"验证,未做真机渲染回归。

9真机验收清单 — 建议按这个顺序点一遍

  1. ① 留存与搜索:建一条订阅 → 立即执行一次 → 删除该订阅 → 打开「订阅中心 → 站内信」:消息仍在,且带「该订阅已删除,历史推送仍保留」;用关键词(如"用户数")+ 时间范围 + 状态筛一遍,能命中。
  2. ② 快照复用:同一条订阅连点两次「立即执行」,看运行日志:第二轮起出现 【SQL快照】订阅复用已存 SQL,执行耗时明显低于首轮(SQL 与首轮一致)。
  3. ③ 守卫默认放行:直接问「系统用户数是多少」「深圳总公司用户数量明细列表」——不再出现"域外"卡片;问一句真的域外问题(如"今天天气怎么样")在默认放行下仍会尝试生成(若前端需要严格拦截,显式传 forceQuery=false)。
  4. ④ 问句描述:对一条碎片问句说「每天 16:15 发我」,回执应显示「订阅问句:统计<…>(执行时按「原话」这句问数跑)」;订阅中心标题不再是裸碎片。
  5. ⑤ 弹窗统一:任意一次失败回答点「上报问题」,再点一次「对比版本」:两个弹窗页头、圆角、按钮、输入框观感一致(同一套暖色皮肤),上报弹窗里 traceId 行有底卡样式(不再是一行裸文本)。
  6. ⑥ 手动新建:我的订阅 →「新建订阅」→ 手输问句、选数据源、设频率 → 提交:列表出现该条,标题为整理后的问句;重复提交同一组合不会多出一条(幂等更新)。
  7. ⑦ 关联理解:问「给我列出研发部门的所有用户」:SQL 应为部门主键下推到 sys_scope.dept_id(而非 sys_post、也不按 dept_name 过滤);服务端日志若有 【关联语义体检】 行,说明体检在跑(命中才输出)。
回退口径(不改即不破):R21 的 12 条 fe-invariant、R19 三项修复、R16-①/R18-⑥ 切图链路、R20/R21 订阅频率与残句口径、订阅中心 UI 同值规范,本轮全部复跑保持全绿;H5 仍为 Vant 原生。