334 lines
34 KiB
Markdown
334 lines
34 KiB
Markdown
# 字体与关键词高亮改进计划(2026-10-17)
|
||
|
||
基于 2026-10-17 的专项调研(字体两轮 + 高亮两轮,子代理实测探针交叉印证,主模型复核关键结论)。所有问题均给出证据位置与验收口径,按批次执行。
|
||
|
||
## 调研结论摘要
|
||
|
||
**字体**:能力面已全(family/字号/字重/粗体字重/字间距/行高 + 预览 + 热更 + 热键),WebGL 字形图集在字体变化时自动重建(无缺陷,不需要 `clearTextureAtlas`)。核心短板:默认字体栈无 CJK 字体、无 unicode11 宽度表、无 `rescaleOverlappingGlyphs`,中文环境列对齐无保障;字体下拉是系统全量字体且无等宽标记,与「系统等宽字体」文案矛盾。
|
||
|
||
**高亮**:引擎与规则体系在「用户自定义正则高亮」层领先 VS Code / Windows Terminal;性能实测 200 条规则 × 135KB 文本 ≈ 11ms,规则数量非瓶颈。核心短板:两个现成 bug(`perm` 预设锚定失效、注入的 `\x1b[0m` 清掉应用自身样式);「匹配后动作」能力为零(secret 只上色不设防);`m` flag 不可用;预设升级机制只覆盖 2 条旧 pattern。
|
||
|
||
---
|
||
|
||
## P0 正确性修复(bug,优先做)
|
||
|
||
### P0-1 `perm` 预设锚定失效(本轮实施:Qwen 任务 A)
|
||
|
||
- **问题**:`src/shared/settings.ts:299` 的 `^[-dlbcps][rwxstST-]{9}` 中 `^` 锚定的是「转义序列之间的 text run 起点」而非行首。`ls -l` 多行输出只有第一行权限位上色;带颜色的 `ls`(run 以 `\x1b[32m` 切开)完全不触发。`tests/hl-rules.mjs` 无 perm 用例。
|
||
- **方案**:pattern 改为 `(?<=^|\s)[-dlbcps][rwxstST-]{9}(?![rwxstST-])`(lookbehind 锚定行首/空白边界;trailing 用负向前瞻拒绝「再跟一个权限字符」——不能用 `(?=\s|$)`,会把 ACL/SELinux `+`、macOS `@` 后缀的真实 `ls -l` 输出挡掉,这是审核轮抓到的回退);旧 pattern 登记 `src/main/settingsStore.ts` 的 `LEGACY_BUILTIN_PATTERNS`,让存量安装升级;补 perm 测试用例(多行 `ls -l`、带色输出、单词内不误配、11 字符不截断、ACL `+`/macOS `@` 后缀仍染色)。
|
||
- **验收**:`npm test` 全绿;新增用例覆盖上述场景。
|
||
- **风险**:10 个连续连字符(markdown 分隔线 `----------`)会误配——触发面极窄,接受;已知带 ACL/SELinux 后缀的权限串不再染色的问题已由负向前瞻写法解决。
|
||
- **状态**:✅ 已完成(Qwen3.8-27B 实施 → flash 审核发现 ACL 回退 → 主模型定稿 pattern `(?![rwxstST-])` 并补 2 个后缀用例,`npm test` 全绿)。
|
||
|
||
### P0-2 注入的 `\x1b[0m` 清掉应用自身 SGR 样式
|
||
|
||
- **问题**:`src/renderer/src/terminal/highlightEngine.ts` 的 `wrap` 给每个命中 span 固定追加 `\x1b[0m`。实测 `\x1b[33mWARNING tail` → `tail` 丢失应用的黄色。
|
||
- **方案**:tokenize 时跟踪当前活跃 SGR 状态(进入 span 前记录自上次转义以来的 SGR 序列,span 结束后重放而非裸 reset)。只对 text→seq 边界内做,保持 `hl-split-smoke` 逐字节兼容。
|
||
- **验收**:新增引擎用例:`\x1b[33mWARNING tail` 染色后 `tail` 仍为黄色;`hl-split-smoke` 全绿。
|
||
- **⚠ 实施后的性能回归与修正(2026-10-17 实测)**:初版只设 `MAX_RESTORE_LEN=256`(累积器上限),结果恢复串在**每个 span 后**都重放,代价是 `串长 × span 数`。Kimi 这类全彩 TUI 一行 ~19 个 span,19 字节真彩色串使 9.5KB 输出膨胀到 40.8KB(**4.30x**,改动前 2.78x);叠加用户配置(背景图 + WebGL + 高亮 all 档,MCR=4.5 逐字形算对比度且真彩色令缓存全 miss),渲染线程卡 **18.4 秒**(日志 `[renderer] main thread stalled ~18384ms`,用户报「输入 kimi 要很久才加载出来」)。**修正**:新增独立成本闸门 `MAX_REPLAY_COST=11` 字节(按「收益能否覆盖长度×span 数」,而非串本身的绝对长度),≤11 字节(单色/单属性)仍回放、更长退回裸 reset 即改动前行为。实测膨胀回到 2.78x(与改动前一致),收益场景 `\x1b[33m` 仍正确保留。教训:**功能性用例全绿不代表热路径没回归,改这段必须跑 TUI 密集输出的膨胀基准**
|
||
|
||
### P0-3 多窗格统计「最后写入者赢」
|
||
|
||
- **问题**:`src/renderer/src/terminal/highlightStats.ts` 的模块级 `totals` 被每个 pane 的 1s 周期发布覆盖,设置页数据在窗格间跳动。
|
||
- **方案**:按 pane 汇总(key 为 session/pane id)或改为累加合并;设置页展示汇总值。
|
||
- **验收**:两个 pane 同时输出时统计稳定不跳动。
|
||
|
||
### P0-4 字体死代码:`automaticFontFallback`(本轮实施:Qwen 任务 B)
|
||
|
||
- **问题**:`src/renderer/src/terminal/TerminalView.tsx:878` 的 `automaticFontFallback: true` 在 @xterm/xterm 6.1.0-beta.304 中不存在(typings 与 lib 均无),是空操作,且靠 `as Terminal['options'] & object` 绕过了类型检查,误导维护者。
|
||
- **方案**:删除该行;若该 as-cast 仅为容纳此未知选项而存在则一并删除。`tsc -p tsconfig.web.json` 验证。
|
||
|
||
### P0-5 `disableQuoting: true` 去掉字体名引号保护
|
||
|
||
- **问题**:`src/main/ipc.ts:224` 显式关闭 font-list 的引号包裹,含 `()`/`+`/数字开头/等于 CSS 关键字的字体名在 CSS/canvas 解析中可能失败。
|
||
- **方案**:保持枚举原样,渲染层消费点(写入 settings 前或传给 xterm 前)对字体栈逐段 `JSON.stringify` 包裹非法标识符段(实现为纯函数,两端复用)。
|
||
|
||
### P0-6 `overviewRuler` 顶层写法无效 + `as` 断言可绕过任何伪造选项(审核轮新发现)
|
||
|
||
- **问题**:`TerminalView.tsx` 构造 Terminal 时顶层的 `overviewRuler: { width: 9, ... }` 不是 6.1.0-beta.304 的顶层选项——它属于 `IScrollbarOptions`,正确写法是 `scrollbar: { width: 9, overviewRuler: {...} }`。顶层键会被 OptionsService 构造**静默丢弃**(实跑验证 `new Terminal({overviewRuler}).options.overviewRuler === undefined`),当前滚动条宽度实为 xterm 默认值。另外 `as Terminal['options'] & object` 断言能让**任何**伪造键通过 tsc(审核实测 `totallyBogusOption` 也能过)。
|
||
- **方案**:决定产品意图——若想要 9px 滚动条 + overview ruler,改写为 `scrollbar: {...}` 并验证视觉效果;否则删除。删除后 `as` 断言可一并移除(审核实测:去掉 overviewRuler 后无断言也能过类型检查)。注意 `withChromeColors` 里的 `overviewRulerBorder` 是 `ITheme` 合法键,**有效,不要误删**。
|
||
- **验收**:tsc 零错误;若启用 `scrollbar.overviewRuler`,视觉验证滚动条与 ruler 表现。
|
||
|
||
---
|
||
|
||
## P1 高价值低成本
|
||
|
||
### P1-1 编辑器「按行匹配」开关(暴露 `m` flag)
|
||
|
||
- 引擎 `compileRules` 只拼 `g`/`gi`(`highlightEngine.ts:131`)。规则 schema 加 `lineAnchor?: boolean`,编译时追加 `m`;编辑器加开关。顺手化解 P0-1 类问题对用户自写 `^...$` 规则的困扰。
|
||
|
||
### P1-2 CJK 宽度与对齐三件套(字体侧最大用户价值)
|
||
|
||
1. 安装 `@xterm/addon-unicode11`(**先验证与 6.1.0-beta.304 兼容**;渲染层依赖 → devDependencies,遵守仓库依赖分类规则)+ `term.unicode.activeVersion = '11'`
|
||
2. 开 `term.options.rescaleOverlappingGlyphs = true`(仅 WebGL 有效,DOM 渲染器下无效需在文档/UI 标注)
|
||
3. `fontFamily` 支持独立 CJK 回退槽位(设置新增「中文字体」,拼接在字体栈尾)
|
||
|
||
### P1-3 字体下拉等宽徽标
|
||
|
||
- 渲染层 canvas `measureText` 检测字符步进一致性(采样 `i l 1 W m 0` 等字符宽度),列表加「等宽」徽标 + 默认等宽优先排序;同步修正「系统等宽字体」文案(现为误导)。
|
||
|
||
### P1-4 `MAX_LINE_LEN` 按行跳过(而非整段 run 跳过)
|
||
|
||
- 现状:一条 >4KB 的行让整个 text run(含其中的 `ERROR`)全部失去高亮,且无提示(`highlightEngine.ts:229-237`)。改为按行切分后只跳过超长行。`MAX_PER_RULE` 截断同理考虑提示(i18n 四语)。
|
||
|
||
### P1-5 字体预览补 CJK 混排样例(本轮实施:Qwen 任务 B 一并做)
|
||
|
||
- `TerminalPreview` 样例只有拉丁 `ls -la`。补中文文件名 + 权限位混排行,让用户选字体时即可暴露 CJK 错位。
|
||
|
||
---
|
||
|
||
## P2 功能演进(高亮的「匹配后动作」层)
|
||
|
||
### P2-1 引擎匹配事件出口(前置基建,性价比最高的新能力)
|
||
|
||
- 复用 `StatsSink` 注入模式:`applyHighlights` 接受可选事件 sink,命中时上报 `(ruleId, matchText, pane)`,默认路径零成本。这是解锁 P2-2/P2-3/P2-4 的钥匙。
|
||
- 难点:动作要跨到主进程/托盘(现有 `notifyUpdate` 只服务更新器);需按规则去重限流(忙碌日志每秒上千命中)。
|
||
|
||
### P2-2 secret 命中设防
|
||
|
||
- 依托 P2-1:secret 规则命中时可选「弹提醒 / 会话内打码 / 复制前警告」。现状 secret 与普通红字无区别,而它是 5 条 basic 规则之一。
|
||
|
||
### P2-3 用户自定义可点击链接规则(WezTerm hyperlink_rules 式)
|
||
|
||
- link provider 与「文本偏移→单元格坐标」映射已有(`TerminalView.tsx:1019-1047`,现仅 4 条写死 URL 正则)。接上「正则 + URL 模板(`$0/$1`)」即可,架构成本低。
|
||
|
||
### P2-4 关键词命中通知 / tab 标记(iTerm2 Triggers 子集)
|
||
|
||
- 依托 P2-1:匹配到指定模式时托盘通知(复用锁屏/托盘链路)、标签加红点。定位「跑长任务时人不在终端前」的场景。
|
||
|
||
---
|
||
|
||
## P3 打磨(按需排期)
|
||
|
||
| 项 | 内容 | 位置 |
|
||
|---|---|---|
|
||
| 归因诊断 | 编辑器显示草稿规则命中 N 处、被哪条规则抢走 | `HighlightTab.tsx` 预览扩展 |
|
||
| 规则管理 | 表格文本搜索、批量启停、命中 0 次提示 | `HighlightTab.tsx` |
|
||
| 主题色引用 | 规则颜色支持 `@green` 等主题 ANSI 引用 + per-rule「不跟随主题」 | `highlightColors.ts`、schema |
|
||
| 正则校验统一 | 主进程 `coerceRule` 补 `new RegExp` 校验(现编辑器/导入拦、主进程不拦、引擎静默跳过) | `settingsStore.ts:50-138` |
|
||
| 预设升级机制重建 | `LEGACY_BUILTIN_PATTERNS` 只覆盖 2 条旧 pattern,后续预设改动无法下发老用户 | `settingsStore.ts:145-163` |
|
||
| 16ms flush 建模 | `hl-split-smoke` 补应用层 16ms carry flush 的跨块场景(现为测试盲区) | `tests/hl-split-smoke.mjs` |
|
||
| 字体名双语 | 枚举优先 zh-cn 本地名,用户对不上 Consolas 等拉丁名;改双列显示 | `ipc.ts`、`SettingsTabs.tsx` |
|
||
| 主进程数字钳制 | fontSize/letterSpacing/lineHeight 仅 UI 钳制,主进程清洗放行任意值 | `settingsStore.ts:325-337` |
|
||
| SGR 跨转义匹配 | 被 SGR 打断的 token(如真彩色着色的 URL)匹配不到——低频,与 P0-2 同区域,可一并评估 | `highlightEngine.ts` tokenize |
|
||
|
||
---
|
||
|
||
## 执行批次
|
||
|
||
| 批次 | 内容 | 执行者 | 状态 |
|
||
|---|---|---|---|
|
||
| 本轮-1 | P0-1 perm 锚定修复(含审核轮 ACL 后缀修正) | Qwen3.8-27B 实施 + flash 审核 + 主模型定稿 | ✅ 完成 |
|
||
| 本轮-2 | P0-4 字体死代码删除 + P1-5 预览 CJK 样例 | Qwen3.8-27B 实施 + flash 审核 | ✅ 完成 |
|
||
| 本轮-审核 | 两个任务的代码审核(含独立探针复验) | flash(默认模型)子代理 | ✅ 完成(发现 1 处回退,已修;新增 P0-6) |
|
||
| 本轮-把关 | 测试全量回归 + 计划定稿 | 主模型 | ✅ 完成(`npm test` 29 测试全绿) |
|
||
| 第二批-波1 | P0-2 SGR 恢复 + P1-4 按行护栏(引擎)/ P0-3 统计聚合 + P0-6 死代码清理 | flash 实施 ×2 并行 + 主模型把关 | ✅ 完成(各 19/9/11 项新断言;pane key 用实例 UUID 规避 SSH 分屏同 session) |
|
||
| 第二批-波2 | P0-5 字体名引号包裹 / P1-1 按行匹配开关(m flag) | flash 实施 ×2 并行 + 主模型把关 | ✅ 完成(新增 tests/font-stack.mjs,测试数 29→30;i18n 四语 +2 key) |
|
||
| 第三批 | P1-2 CJK 三件套 + P1-3 等宽徽标 | flash 实施 ×2 并行(文件不相交、契约字段钉死)+ 主模型把关 | ✅ 完成(unicode11 装的是 0.10.0-beta.301,peer 精确接受 beta.304;`cjkFontFamily` 默认空=零行为变化;等宽检测 canvas 采样容差 0.01) |
|
||
| 远期 | P2-1 → P2-2/3/4 依次解锁 | flash 实施 ×2(串行两波)+ 主模型把关 | ✅ 完成(2026-10-17 第二轮) |
|
||
|
||
### P2 执行记录(2026-10-17 第二轮)
|
||
|
||
- **P2-1 引擎事件出口**:`applyHighlights` 第 5 参 `onMatch?: (ruleId, text) => void`,仅「真的画上屏」的 span 上报(被抢占/预算耗尽/超长行跳过都不报);`HighlightStream` 6 个 `applyHighlights` 调用点全透传;未传时零开销。测试 `[match events]` 段 7 断言(含 301 命中恰好 300 次上报的预算边界、跨切分只报一次)
|
||
- **P2-2 secret 命中提醒(产品取舍:应用内提醒,不打码)**:规则级 `notify?: boolean` 字段 + 编辑器「命中时提醒」开关;消费端 60s 冷却去重(per pane per rule);antd notification 提醒**只显示规则名、绝不渲染匹配原文**(secret 原文就是密钥);`notifyRules` 从 mode/profile 过滤后的实际规则集派生,被排除的规则不报警。**secret 预设默认 `notify: true`(仅新装;存量不静默升级**——把会产生弹窗的新行为塞进 refreshBuiltinRules 等于替用户做了「以后会被打断」的决定,用户可手动开)
|
||
- **P2-3 自定义可点击链接(WezTerm hyperlink_rules 式)**:`AppSettings.linkRules`(默认空)+ `src/shared/linkRules.ts` 纯模块(sanitize/formatLinkTemplate `$0`–`$9`/`compileLinkRules`/`mergeLinkSpans`)+ HighlightTab 底部编辑器(pattern/template/note/enabled,非法正则内联报错)+ link provider 叠加在内置 URL 之上(用户规则优先,热更走 ref 不重建终端)。**安全闸门**:`isSafeLinkTarget` 在生成 span 时就丢弃非 http/https 目标(`file://`/`javascript:` 连下划线都不出现),主进程 openExternal 协议白名单兜底。性能护栏:>4KB 行跳过用户规则、每规则每行 200 上限、零宽匹配强制前进。测试 `tests/link-rules.mjs` 86 断言(**测试数 30→31**)
|
||
- **P2-4 关键词命中通知/tab 标记**:通知子集已由 `notify` 字段覆盖(任何规则可开);**tab 红点标记搁置**——需 dockview 面板参数联动与面板生命周期管理,复杂度高且与通知重叠,待真实需求再评估
|
||
- 最终把关:`npm test` 31 个测试全绿、`verify-deps` 448 包一致、`npm run build` 通过、AGENTS.md 已同步(测试数 31、「可点击链接规则」新章)
|
||
- 未做 git 提交(用户要求)
|
||
|
||
### 进度记录(2026-10-17)
|
||
|
||
- P0 六项全部完成:P0-1 perm(含 ACL `+`/macOS `@` 后缀)、P0-2 SGR 恢复(`SgrState`,`MAX_RESTORE_LEN=256` 降级)、P0-3 统计按 pane 聚合、P0-4 死代码删除、P0-5 `quoteFontStack`(settings 存原样、消费点包裹)、P0-6 overviewRuler/as 断言清理(产品决定:不启用 scrollbar ruler,维持现状视觉)
|
||
- P1 五项全部完成:P1-1 `lineAnchor`(gm/gim + 编辑器开关 + `^$` 徽标)、P1-2 CJK 三件套(unicode11 0.10.0-beta.301 精确锁定 / `rescaleOverlappingGlyphs` / `cjkFontFamily` 槽位)、P1-3 等宽徽标 + 误导文案修正、P1-4 超长行按行降级、P1-5 预览中文混排样例
|
||
- 最终把关:`verify-deps` 448 包一致、`npm test` 30 个测试全绿、`npm run build` 通过、AGENTS.md 已同步(测试数、终端字体新章、高亮锚定/SGR/降级/统计四条铁律)
|
||
- 未做 git 提交(用户要求);工作区含全部改动
|
||
|
||
## 复查轮(2026-10-17 晚):完整 review 与修复
|
||
|
||
用户报「行高调到 0.95 后所有窗格崩溃」后做了一次全量复查,发现并修复以下问题(全部实测复现)。**行高只是冰山一角:核心缺陷是「UI 允许的值 / 配置能写的值 与 xterm 硬约束不一致,而主进程不做校验」。**
|
||
|
||
### 已修(P0)
|
||
|
||
| 问题 | 后果 | 修复 |
|
||
|---|---|---|
|
||
| `lineHeight < 1` | xterm 抛错 → 错误边界卸掉整个窗格;坏值落盘后每次启动都崩 | UI 下限 0.8→1 + 主进程钳制 + `healClampedFontSettings()` 回写自愈 |
|
||
| `cursorStyle` 非法值 | **更糟**:构造期静默吞错、热更期抛错,所以「重试」按钮永远无效,窗格死循环 | `CURSOR_STYLES` 白名单(shared 层单一来源) |
|
||
| `scrollback < 0` | 同上,热更抛错 | 纳入 `FONT_LIMITS` 钳制 100–100000 |
|
||
| `cursorInactiveStyle` / `rendererMode` 非法值 | 静默失效(失焦不画光标 / 误以为选了 DOM 却走 WebGL) | 同款白名单化 |
|
||
| **ReDoS** | pattern 无长度限制且预览每键跑一次:`(a+)+$` 配 30 字符实测卡 **8.2 秒**,25 字符翻倍。`MAX_LINE_LEN=4KB` 限的是长度不是回溯成本 | `MAX_PATTERN_LEN=500` + 编辑器 `maxLength` + `backtracksBadly()` 探测(4 组诱发输入实跑,超 50ms 即警告)。**根因未除**:正解需 Worker + 超时,列为待办 |
|
||
| **SGR reset 只认两种写法** | `ESC[00m`/`ESC[0;0m`/`ESC[0;32m`(Go/Java 日志库常写)被当作可累积序列**追加**,累积器逐行增长直到撞上限,之后所有 span 恢复永久失效 | 改为解析参数:全 0/空 = 全量复位;`0;…` 前缀先清空再记录 |
|
||
| **CSI 私有参数不识别** | `ESC[>4;1m`(modifyOtherKeys) / `ESC[<0;1;2M`(SGR 鼠标) / `ESC[=1;1$y`(DECRQM) 让整行成含裸 ESC 的文本,**该行之后所有关键词失去高亮**;vim/tmux 常发 | `ESCAPE_RE` 补 `<>=` 与中间字节区间 `[ -/]*` |
|
||
| `quoteFontStack` 不转义反斜杠 | 尾部反斜杠使 CSS 字符串不闭合,字体静默回落默认 | `replace(/[\\"]/g, '\\$&')` |
|
||
|
||
### 关键自我修正(两个判断失误,均已落成 AGENTS.md 铁律)
|
||
|
||
1. **回放闸门按字节数是错维度**:`MAX_REPLAY_COST=11` 会砍掉真彩色(13–19B)却放过索引色(11B)——同一个「单个前景色」语义待遇不同。改为按 **序列条数**(`MAX_REPLAY_SEQUENCES=3`)。
|
||
2. **恢复串不必每个 span 都重放**:只有「可见间隙」(≥3 个可见字符)和最后一个 span 之后才需要。改为按间隙插入后,密集 TUI 膨胀从 **4.30x 降到 3.05x**,长间隙着色仍正常(实测 12 处间隙正确恢复)。
|
||
|
||
### 待办(P1,本轮未做)
|
||
|
||
- **ReDoS 防护**:按下方「独立方案 A」实施主进程 Worker 异步校验;不采用 `re2`,不把 Worker 放进同步热路径
|
||
- **长期 renderer stall**:按下方「独立方案 B」先补齐时间线诊断,再按证据优化;fit 目前只是候选,不是既定根因
|
||
- `fontWeight`/`boldFontWeight` 越界被 xterm 静默改写(手改 json 才会遇到,纯体验问题)
|
||
- `resizePty` 的 `cols`/`rows` 完全信任渲染层(低危,靠自家 fit 逻辑兜底)
|
||
- 链接规则的导入/导出、profile 联动
|
||
|
||
|
||
## 约束提醒(对实施者)
|
||
|
||
- 高亮引擎改动必须过 `node tests/.hl-rules.cjs`(bundle 经 `node tests/build-bundles.cjs` 重建)与 `tests/hl-split-smoke`;改预设 pattern 必须同步 `LEGACY_BUILTIN_PATTERNS`,否则存量安装拿不到修复
|
||
- i18n 新文案四语齐备(zh-CN/zh-TW/en/ja,缺译回退简体)
|
||
- 渲染层新依赖一律 devDependencies;`@xterm/*` 版本必须与锁定的 6.1.0-beta.304 体系兼容,改依赖后必须真实 `npm install` 并跑 `node scripts/verify-deps.cjs`
|
||
- 改动若触碰 AGENTS.md 已记载的行为,同步更新 AGENTS.md
|
||
|
||
## 最终 review 结论(2026-10-17 收尾)
|
||
|
||
对 `c6aca15` 做了独立复查,**发现 ReDoS 静态检测不可靠,已从「拒绝闸门」降级为「编辑器提示」**。
|
||
|
||
### 实测漏判(危险规则仍会进热路径)
|
||
| pattern | 静态检测 | 实测耗时 |
|
||
|---|---|---|
|
||
| `[(](a+)+$` | 放行 | 30 字符 **4065ms** |
|
||
| `(?:a|a)+$` | 放行 | 26 字符 **7547ms** |
|
||
| `((a|a))+$` | 放行 | 24 字符 **16212ms** |
|
||
| `(\w|\d)+$` | 放行 | 26 字符 **14451ms** |
|
||
|
||
根因:外层扫描不跟踪字符类状态(`[(` 里的 `(` 被当组起始)、遇到不闭合组时整个函数提前返回、分支比较只做字面相等。
|
||
|
||
### 实测误判(合法规则被拒)
|
||
`(?:a{2,3})+`、`(?:\d{2})+` —— 把 `{n,m}` 范围重复当成 `{n}` 固定重复。
|
||
|
||
### 尝试修复但失败
|
||
补字符类状态后又误杀 `(?:\d{2})+` 与 `ipv4` 预设。**每修一处引入新问题**,证明手写正则语法扫描有本质缺陷。
|
||
|
||
### 否决的三个替代方案
|
||
1. **运行时熔断**(循环里查时钟):**原理上无效**。实测单次 `exec` 阻塞 **28426ms**,时钟检查根本执行不到
|
||
2. **re2**:不支持 lookbehind,`perm`/`okstate`/`http` 三条预设依赖它
|
||
3. **Worker 进入同步热路径**:往返 0.11ms/块 vs 直接 0.035ms,且 `WriteBatcher` 依赖同步染色结果;Worker 只用于规则异步校验
|
||
|
||
### 当前状态与遗留
|
||
`isReDoSProne` 保留为**编辑器提示**(漏判不比改动前差,误判只是多一个警告而非禁用规则)。热路径边界仍是 `MAX_LINE_LEN`(>4KB 行不进用户正则)+ `MAX_PER_RULE`。
|
||
|
||
---
|
||
|
||
## 独立方案 A:ReDoS 异步校验与执行闸门(已实施,待真实场景验收)
|
||
|
||
由 explore 子代理调查、主代理复核构建与终止前提后定稿。本节取代此前「可选 re2」「Worker 放进预览热路径」等粗略想法。
|
||
|
||
**执行状态**:A1–A6 已实施——共享探针/裁决模块、主进程 Worker 硬超时、设置回写、IPC、编辑器与导入 UI、四语状态与测试链均已落地;独立 review 后修复了默认安装误报服务不可用、测试未接入、Worker 产物无 smoke 等问题。剩余验收见 A6,且「通过当前探针 ≠ 对任意输入安全」的边界不变。
|
||
|
||
### A1. 目标与非目标
|
||
|
||
**目标**:自定义高亮规则、链接规则在进入 renderer 同步执行前,先经主进程 Worker 做版本化动态探测;超时规则被隔离,待校验规则不执行,Worker 卡死时主进程可硬终止。覆盖编辑器、实时预览、导入、存量配置和手改 `settings.json`。
|
||
|
||
**非目标**:固定探针不可能数学完备地识别所有病态正则,不得把「通过探针」宣传为「绝对安全」。若验收要求任意正则都绝不能在 renderer 内长时间阻塞,只能另立「线性时间/可限时正则引擎」或异步渲染架构方案;本方案不作此承诺。首版也不加入热路径计时和事后熔断,避免额外开销与复杂度。
|
||
|
||
### A2. 已独立验证的技术前提
|
||
|
||
- 临时最小工程实跑当前 `electron-vite 5.0.0 / Vite 7.3.6`:`import createWorker from './probeWorker?nodeWorker'` 可构建,产物为 `out/main/probeWorker-<hash>.js`,不是必须放进 `chunks/`。
|
||
- 构建后的 Worker 已用 Node 实跑;对执行 `((a|a))+$` 灾难回溯的 Worker,主控侧约 **313ms** 完成 `terminate()`,期间主线程持续 tick。超时计时必须放在主控侧,不能依赖 Worker 自己在正则里检查时钟。
|
||
- 当前 Electron 44 的最小 asar 应用已实跑,Worker 能从 asar 内加载并返回结果。**不需要为本方案修改 `asarUnpack`**;仍需构建/打包 smoke 防未来回归。
|
||
- renderer Web Worker 不作为首选:还需承担 `file://`、sandbox、CSP 与持久化裁决回写主进程等额外问题,而主进程路线已经验证。
|
||
|
||
### A3. 架构与规则信任边界
|
||
|
||
新增主进程按需校验服务:`settings 变更/编辑器或导入请求 → ruleValidationController → worker pool → 校验结果写回设置 → 现有 settings 广播 → renderer 重新编译`。`settingsStore` 通过注入钩子调用控制器,避免与 validator 循环依赖;同步 `loadSettings()` 内不得启动或等待 Worker。
|
||
|
||
规则执行状态:
|
||
|
||
- `builtin`:pattern + flags 与出厂预设精确匹配,走共享白名单,不启动 Worker。
|
||
- `passed`:Worker 在当前 `probeVersion` 与 V8 大版本下通过探针;UI 文案只能是「已通过当前探针」。
|
||
- `pending`:新规则、存量规则、pattern/flags 改变、裁决过期、Worker 暂不可用或校验尚未完成。
|
||
- `quarantined`:仅当某条规则实际超过硬预算、被主控终止;保留规则供用户编辑,但不执行。
|
||
|
||
**执行闸门是 fail-closed:`pending` 与 `quarantined` 都不进入 `compileRules` / `compileLinkRules`,也不进入编辑器预览。** 不采用「pending 限 512 字符继续跑」——指数回溯在 512 字符上仍可能冻结,这不是安全降级。Worker 不可用时只禁用自定义规则的执行,设置数据保留、内置规则不受影响、设置页给出明确警告;不提供「仍然强制运行」旁路。
|
||
|
||
裁决作为内部元数据随规则持久化,执行时同时核对 pattern、flags、probeVersion、runtime,而不是只信状态字段。导入/导出不得把内部裁决当作用户信任凭据;导入一律重新校验。主进程在首次广播前同步把过期裁决降级为 `pending`,避免应用更新后短暂执行旧裁决。`highlightProfiles` 只引用规则 id,不单独校验。
|
||
|
||
### A4. Worker、探针与预算
|
||
|
||
- `src/main/redosValidator.ts`:懒创建 Worker 池、in-flight 去重、队列、主控硬超时、崩溃重试一次、结果写回;默认安装不启动 Worker。
|
||
- `src/main/redosProbeWorker.ts`:只执行合成探针并返回耗时,不负责自我超时。
|
||
- `src/shared/redosProbes.ts`:零依赖、版本化的固定对抗语料;覆盖重复字符、失败终止符、交替输入、已知字面前缀和重叠字符类。**不再手写一套正则语法扫描器**来假装完整理解 pattern。
|
||
- `src/shared/ruleValidation.ts`:状态类型、flags/key 计算、出厂白名单与执行闸门,main/renderer/测试共用。
|
||
- 探针均为合成输入,长度不超过现有 4KB 行边界,不读取、不记录终端内容;每规则最多约 24 条。
|
||
- 单规则主控硬预算 **500ms**;超时即 `terminate()` 并重建该 Worker。池并发上限 **2**,空闲回收;同 pattern+flags 合并任务。批量导入显示异步进度,不用总预算把未测规则误判成危险。
|
||
- `isReDoSProne` 只保留为输入时提示,与执行裁决无关;同时修正编辑器里「无漏判」的过期注释。
|
||
|
||
### A5. UI、文件与兼容策略
|
||
|
||
- 编辑器按 300–500ms 去抖发起异步校验;pending 草稿不执行预览,保存按钮显示校验状态。每个草稿带递增序号,丢弃慢返回的旧请求,防止旧裁决覆盖新 pattern。超时规则可保存为隔离状态供修改,但不能执行。
|
||
- 导入后批量校验并报告「通过/待校验/隔离」数量;链接规则与高亮规则走同一服务。四语 i18n 增加状态与失败说明。
|
||
- 主进程 `coerceRule` 补上 `MAX_PATTERN_LEN=500`,避免手改配置在校验前先塞入超长 pattern;`sanitizeLinkRules` 已有相同上限。
|
||
- 主要改动:`highlightEngine.ts`、`linkRules.ts`、`settings.ts`、`settingsStore.ts`、`HighlightTab.tsx`、`HighlightImportExport.tsx`、IPC/preload/API 类型、四语字典、`tests/build-bundles.cjs`、`package.json` 测试链与 `AGENTS.md`。
|
||
- 不改 `electron-builder.yml`。新增构建产物 smoke,检查 main 包引用的 Worker 文件确实存在且可加载;发布前再用真实安装包做一次 asar smoke。
|
||
|
||
### A6. 测试、性能预算与实施顺序
|
||
|
||
测试必须包括:已知危险规则(含静态检测漏判的 4 条)全部能在硬预算内隔离;22 条预设与典型合法规则零误隔离;`[(](a+)+$` 等需要字面前缀的回归样本;pending/passed/quarantined/过期裁决的编译闸门;预览与链接 provider 不绕过;Worker 崩溃、缺失、超时、去重、队列和主线程持续响应;手改配置与旧版本迁移。
|
||
|
||
性能预算:纯预设路径不启动 Worker、不增加每 chunk 计时;执行裁决只在设置变化/编译时检查。单次编辑异步校验目标 ≤600ms,不阻塞 renderer;校验失败不得拖住设置页。批量校验不得无上限占用 CPU/内存。
|
||
|
||
实施顺序:①纯探针与裁决模块及测试 → ②主进程 Worker 服务和真实 terminate 测试 → ③编译/预览/链接统一闸门 → ④编辑器、导入、存量迁移与四语 UI → ⑤构建/asar smoke、全量回归与文档。每步可回退,但不把「只上一半闸门」的中间状态发布给用户。
|
||
|
||
最终验收:`npm test`、`npm run build`、`node scripts/verify-deps.cjs` 全过;构建产物 Worker smoke 通过;真实场景验证所有已知危险回归样本都不会冻结设置页或终端,合法预设与现有功能无回归。
|
||
|
||
---
|
||
|
||
## 独立方案 B:长期 renderer stall 时间线诊断与按需优化(第一阶段已实施,第二阶段待数据)
|
||
|
||
由 explore 子代理调查日志与代码、主代理复核后定稿。**当前无法把 stall 归因到 fit;第一阶段只增强观测,不改 reflow、不做背压、不关功能。**
|
||
|
||
**执行状态**:B2 第一阶段已实施——longtask/scheduler 分类、pane 指标聚合、fit trigger 与限频、xterm write callback 计时、WebGL 生命周期日志和表驱动测试均已落地;B3 复现矩阵与 B4 按证据优化尚未执行,不能在拿到时间线数据前宣称根因或性能收益。
|
||
|
||
### B1. 当前能与不能下的结论
|
||
|
||
- 历史日志以 renderer stall 为主,但现有看门狗只靠 2s `setInterval`:它混合了真实长任务、计时器调度延迟、可见性/系统休眠等状态,日志条数不能直接当成 288 次已证实长任务。
|
||
- fit 已有 >200ms 埋点,但无限频、无触发来源、缺少与 stall/write 的共同时间线;「没有 fit 日志」也不能反向排除 fit。
|
||
- `WriteBatcher` 只缓冲进入 `term.write` 前的字符串;当前没有 xterm `write` callback 消费指标,所以看不到 xterm 内部积压。第一阶段不得声称已有 PTY 背压。
|
||
- 高亮单块耗时低不等于下游成本低,SGR 注入膨胀、scrollback reflow、多 pane、WebGL 仍是对照变量,而不是已证实根因。
|
||
|
||
### B2. 第一阶段:低成本统一时间线(只加观测)
|
||
|
||
1. **修正看门狗分类**:`main.tsx` 增加 feature-detect 的 `PerformanceObserver('longtask')`,保留最近约 50 条 startTime/duration 的 ring buffer;`visibilitychange`、`pageshow`、恢复可见时重置 tick。watchdog lag 与长任务时间重叠才记 `renderer long task`,否则记 `scheduler delay/unclassified`,并写清区间起止,不再统称 stall。
|
||
2. **每 pane 轻量指标注册表**:新增纯诊断模块,TerminalView mount/unmount 注册,watchdog 只读聚合快照;不走 React setState,避免诊断本身触发重渲染。
|
||
3. **fit 埋点补全并限频**:记录 trigger(resize/font/scrollback/mount/renderer-switch)、开始时刻、耗时、cols/rows、实际 buffer 行数;每 pane **30s 最多一条**,下一条带 `suppressed=N`。全局 PERF 日志每分钟最多 5 条,watchdog 的严重长任务报告不被该上限吞掉。
|
||
4. **写入链路分两段计时**:WriteBatcher 暴露 `pendingChars`(JS 字符串长度,不冒充字节);每次 flush 记录 `term.write` 入队耗时,并用 `term.write(text, callback)` 记录 xterm 消费完成延迟、未完成 write 数与排队字符数。callback 只做计数和限频日志,**第一阶段不改变写入顺序或暂停 PTY**。
|
||
5. **WebGL 生命周期**:记录 renderer 初始化/切换、`onContextLoss`、dispose/重建结果;不记录图像或终端内容。
|
||
6. **stall 快照**:严重事件携带 pane 数、renderer 模式、最近一次 fit 元数据、Batcher/xterm 队列指标和最近 longtask;所有时间戳由 renderer 在恢复后补写,明确日志落盘时间不等于故障开始时间。
|
||
|
||
### B3. 对照复现矩阵
|
||
|
||
在 dev 独立用户目录下固定同一会话、同一输出样本,每个场景至少 3 次,每次只改一个变量:
|
||
|
||
| 组 | 对照 | 主要判定 |
|
||
|---|---|---|
|
||
| 窗口/布局 | 最大化/还原 20 次;单 pane vs 用户常用多 pane | longtask 是否与 fit 起止重叠,耗时是否随 pane/buffer 增长 |
|
||
| 输出负载 | 同一高输出 TUI 或固定 Node 输出样本,30–60s | 队列是否单调增长,write 消费延迟是否与长任务重叠 |
|
||
| scrollback | 500 / 5000 / 50000 | fit/reflow 是否随历史行数线性增长;仅为对照,不把缩小值当修复 |
|
||
| renderer | WebGL / DOM | 是否伴随 context loss 或仅单 renderer 出现 |
|
||
| 高亮 | all / basic / off | 字节膨胀、write 消费与 longtask 是否随档位变化;off 仅作诊断 |
|
||
| 字体与 CJK | 字号/行高变化、unicode11/rescale 开关 | 是否只在设置变更或图集重建时出现单次尖峰 |
|
||
|
||
同时保留「输入 Kimi 到首屏可交互」的真实场景,前后各测至少 5 次,比较中位数和最慢值,不能用一次感觉下结论。
|
||
|
||
### B4. 第二阶段:只对证实根因优化
|
||
|
||
| 证据 | 才允许进入的优化 | 功能与性能约束 |
|
||
|---|---|---|
|
||
| longtask 与 fit 明确重叠,且随 scrollback/pane 放大 | 去重/去抖非紧急 fit,合并字体滑块连续变化,队列积压时推迟非紧急 reflow | 必须保留最终 fit 和正确 PTY 尺寸;不重写 xterm reflow |
|
||
| xterm 队列持续增长、消费完成延迟与长任务重叠 | 设计端到端高低水位背压,renderer 与主进程/PTY/SSH 协同 | 不丢输出;多 pane 引用同 session 时不能互相误暂停;先补测试再实施 |
|
||
| 高亮档位显著改变消费耗时 | 优化 SGR 恢复与输出膨胀 | 保持逐字节兼容,跑 TUI 密集基准,不用关闭高亮冒充修复 |
|
||
| 日志出现 WebGL context loss | 重建或自动回退 DOM | 保留会话与 scrollback,不静默丢渲染器 |
|
||
| 只有 scheduler delay、无 longtask | 查可见性、系统休眠、OS/GPU/外部调度与看门狗误报 | 不改 fit/reflow |
|
||
|
||
「隐藏 pane 暂停解析并重放」属于高复杂度备选,只有在多 pane 被数据证实为主因后才单独评审;它涉及会话状态、重放和切回延迟,不进入首波优化。
|
||
|
||
### B5. 文件、测试与验收
|
||
|
||
第一阶段涉及 `src/renderer/src/main.tsx`、`TerminalView.tsx`、`writeBatcher.ts`,新增轻量诊断纯模块;扩展 `tests/write-batcher.mjs`、`tests/refit-throttle.mjs`,为限频、watchdog 分类和指标聚合新增表驱动纯函数测试。无需新依赖。
|
||
|
||
第一阶段验收:一次用户复现后,日志能区分「fit/reflow、xterm 消费积压、WebGL、其他 longtask、非长任务调度延迟」;正常路径无 React 额外渲染,PERF 落盘 ≤5 行/分钟,所有内容仅为元数据。`npm test` 与 `npm run build` 全过。
|
||
|
||
第二阶段验收:在相同压力场景下消除 >3s 的真实 longtask,队列不无限增长,输入到首屏可交互相对当前 HEAD 有统计上明确的改善;高亮、链接、scrollback、WebGL、会话重放与输出完整性均不得靠降级牺牲。优化未获数据支持前不实施。
|