# PRD:DSH 正则模式搜索插件(`dsh-pattern-search`) | 项目 | 内容 | | --- | --- | | 文档状态 | 定稿 | | 版本 | 0.3 | | 目标平台 | DeepSeek Harness Web(`dsh` web-app,纯客户端插件) | | 命令入口 | `/pattern-search`(裸命令,呼出浮层窗口) | | 核心价值 | 用一条正则从**当前会话上下文**中检索匹配,量化并观察模型输出行为(尤其是 "Actually, wait, let me …" 这类口头禅) | > 范围约束: > 1. **浮层窗口**(`shell.overlay`),非会话视图 Tab。 > 2. **仅 `/pattern-search` 命令**(呼出窗口),不留命令节点。 > 3. **显式搜索按钮**(输入过程不自动搜索)。 > 4. **不做「定位到会话位置」**。 > 5. **partial 开关在窗口内**(搜索时快照)。 > 6. **不做「复制摘要」**。 > 7. **仅 GitHub 安装**(不发布 npm);包名 `dsh-pattern-search`。 --- ## 1. 背景与问题 DeepSeek 模型在思考/输出过程中,高频出现类似 **"Actually, wait, let me …"** 的自我修正式表述。这类行为目前**难以被系统地检测与观察**: - 无法快速回答「出现过多少次、分布在哪几轮、在 reasoning 还是正文、前后各是什么上下文」; - 靠肉眼翻聊天记录成本高、易遗漏,且无法量化趋势; - 需要一种**通用机制**:不只为这一条口头禅,而是任意可被正则表达的模式(如 `Hmm,`、`let me think`、`I realize`、`wait, no` 等)。 因此需要开发一个 DSH 客户端插件:通过 `/pattern-search` 呼出搜索窗口,输入正则表达式,对当前会话上下文做检索,并以「统计 + 分页明细 + 长片段抽屉」三层信息展示。 > 观测目标(本插件的立身之本): > 1. **频率**:模式出现次数、命中率(命中轮数/总轮数、命中次数/文本块数); > 2. **分布**:命中发生在哪些「对话轮(turn)」,以及 assistant 的 `text` 与 `reasoning` 中的占比; > 3. **上下文**:命中前后的固定窗口文本,用于归纳触发模式的语言环境; > 4. **趋势**:随对话推进,模式是否收敛/发散。 --- ## 2. 目标与非目标 ### 2.1 目标(Goals) 1. 用 `/pattern-search` 呼出一个**浮层搜索窗口**,用户在其中输入正则表达式。 2. 在**当前会话**的上下文中检索匹配,覆盖:assistant 正文(`text`)、assistant 思考(`reasoning`)、用户消息、工具结果等(数据源可配置)。 3. 展示**匹配统计**(总数、去重串数、命中轮数、来源分布、按轮分布等)。 4. 展示**分页明细列表**:序号、对话轮数、匹配结果 + 前后 N 字符上下文;高亮命中片段。 5. 点击明细行 → 弹出**侧面抽屉**,展示更长片段(±M 字符)与完整元信息。 7. 提供正则 flags(`i`/`s`/`m`)、**partial 流式检索开关**(窗口内切换)等运行时控件,以及非法正则的友好报错。 8. 全程**纯客户端**:无宿主命令、无宿主往返、**不写入会话日志(不留痕)**、不发起模型请求。 ### 2.2 非目标(Non-goals,v1 不做) - `/pattern-search ` 带参直传(需宿主命令、会留痕,故不支持)。 - 跨会话/全局搜索(v1 仅当前会话;`ctx.sessions.search()` 的 Host 全文索引可作为 v2 的跨会话入口)。 - 持久化搜索历史 / 收藏的模式库(v2)。 - 自动告警/实时流式命中提示(v2,可在 streaming `partial` 上扩展)。 - 对命中做修改/替换(本插件只读,不写会话内容)。 - **定位到会话位置**。 - **复制统计摘要**。 --- ## 3. 术语 | 术语 | 含义(对应 DSH 类型/概念) | | --- | --- | | 会话(session) | 当前打开的一段对话;`SessionId` | | 对话轮(turn) | 一次模型回合;`AssistantMessageNode.turn` / `TurnLocation.turn` | | 步骤(step) | 一轮内的步骤;`AssistantMessageNode.step` / `StepLocation` | | 节点(node) | 会话流中的一个已定型条目;`ConversationNode`(`kind` 判别:`user`/`assistant`/`tool-result`/`context`/`steering`…) | | 块(block) | assistant 消息的分类内容块;`AssistantBlock`(`kind`:`text`/`reasoning`/`tool-call`/`image`/`other`) | | partial | 进行中的 assistant 流式输出;`ConversationSnapshot.partial` | | 上下文窗口 | 命中点前后各取 N(列表)/ M(抽屉)个字符 | | 命中(match) | 一次正则匹配结果 | --- ## 4. 用户画像与场景 - **用户画像**:使用 DSH 与 DeepSeek 模型交互的开发者/研究者,关注模型行为质量、想要"证据化"地观察某些表述。 - **典型场景**: 1. 输入 `/pattern-search`,粘贴 "Actually,?\s*wait,?\s*let me\b"(含 i 默认开),点击「搜索」,查看该口头禅在本会话出现了多少次、分别在第几轮、是思考还是正文。 2. 用自定义正则 `\bHmm\b|let me think` 观察犹豫式表述。 3. 勾选「包含进行中的输出(partial)」后搜索,观察当前正在流式输出的内容是否命中。 4. 点击某条命中,在抽屉里查看完整段落并复制,用于撰写行为分析笔记。 --- ## 5. 功能需求(Functional Requirements) ### FR1 `/pattern-search` 呼出浮层窗口 - 采用**客户端自有 '/' 触发器源**(`ctx.get('inputTriggers').registerSource`,参考 dsh-context 的 `/context` 命令):纯客户端、不触发宿主命令、不写入会话日志、**不留痕**。 - 命令可用性:输入管线天然按会话作用域工作,仅在存在会话时可用。 - 触发方式:输入 `/pattern-search` + 回车 → **直接打开浮层窗口**;在 '/' 菜单中也可点选 `/pattern-search` 条目。 - 打开窗口时**立即消费** draft 中的 `/pattern-search` token(消费事件 `slash/input-consume-token`),避免再次回车重复触发。 - **无预设**;**搜索框与「搜索」按钮同一行**,Enter 等效。 - 窗口打开/关闭状态由插件自有 store 控制(`open: boolean`),窗口组件注册在 `shell.overlay`(根作用域、可叠加的浮层席位,组件自行接管 `pointer-events`)。 ### FR2 正则输入、flags 与搜索触发 - 窗口顶部**搜索框与「搜索」按钮同一行**;输入框单行,支持粘贴多行 pattern 以 `\n` 输入,Enter 等效点「搜索」。 - 提供运行时 flags 开关: - `i`(忽略大小写,默认开) - `s`(dotall,`.` 匹配换行,默认开) - `m`(multiline,`^`/`$` 匹配行首尾,默认开) - `g`(全局)**始终开启**(内部固定,用于遍历所有命中);`u` 由引擎按需开启。 - **搜索触发(显式,防 CPU 打爆)**: - 用户在窗口内**输入/修改正则、切换 flags、切换 partial 开关等,绝不自动搜索**; - 仅当点击「搜索」按钮、或在输入框内按 `Enter` 时执行一次搜索。 - 非法正则:点击「搜索」时编译失败 → **内联显示错误**(尽量给出错误位置),不崩溃、不清空输入;窗口保持可用。 - 命中高亮支持**捕获组**:若有 group,列表/抽屉中可区分整段命中与 group 内命中。 ### FR3 匹配结果统计 统计区在结果列表上方,展示(指标定义见 §7): 1. **匹配总数**(`totalMatches`) 2. **去重匹配串数**(`uniqueMatchedStrings`) 3. **命中轮数**(`hitTurns`)与「含 assistant 消息的总轮数」,以及**命中率**(`hitTurns / totalTurns`) 4. **来源分布**:按 `{node kind, block kind}` 分组的命中数(如 assistant/text vs assistant/reasoning vs user vs tool-result) 5. **按轮分布**:轮号 → 命中数(数字列表,如 `#9: 72`);标签旁带 **ⓘ hover 说明**("#9: 72 表示第 9 轮命中 72 处") > 统计口径明确:一次 `RegExp.exec` 命中计 1;同一字符串多次出现计多次(除非「去重」指标)。 ### FR4 分页明细列表 - 每页行数可配置(默认 `pageSize = 50`)。 - 每行展示: - **序号**:全局序号(跨页连续); - **轮数**:该命中所属 turn(`#turn` 展示;step 可选展示 `#turn.step`); - **来源**:role + block 类型(如 `assistant·reasoning`、`user`、`tool-result`); - **上下文**:`前 N 字符` + `命中片段(高亮)` + `后 N 字符`,中间以省略号/分隔符衔接; - 分页控件:首页/上一页/下一页/末页、跳页输入、「第 X / Y 页 · 共 Z 条」。 - 长列表使用虚拟化(`@tanstack/react-virtual`,参考 trajectory),保证大结果集流畅。 - 命中总数超上限(`maxMatches`,默认 10000)时截断并在列表顶部提示「已截断,仅显示前 N 条,请细化正则」。 ### FR5 侧面抽屉(长片段) - 点击明细行 → 在窗口内**右侧弹出抽屉**(窄屏时以覆盖层呈现)。 - 抽屉展示: - **元信息**:轮数、步骤、节点 `seq`、来源(role/block kind)、命中串、捕获组; - **长上下文**:命中点前后各 `M` 字符(`drawerContextChars`,默认 500,可配置),命中片段高亮;整块文本过长时提供「展开全文」。 - 抽屉操作: - 复制片段 / 复制整段文本; - **上一处 / 下一处**(在抽屉内翻页浏览相邻命中,无需关闭); - 关闭(Esc / 点击遮罩 / 关闭按钮)。 - 不做「定位到会话位置」。 ### FR6 搜索范围与数据源 - 数据源默认全部开启,可配置: - `assistant.text`(正文) - `assistant.reasoning`(思考,**默认开**——"Actually wait" 常出现在这里) - `user`(用户消息文本) - `tool-result`(工具结果文本,含 `argsRaw` 可选) - `context` / `steering`(上下文注入/驾驶消息,默认关,可开) - 搜索对象默认只含 `ConversationSnapshot` 的**已定型节点**(`nodes`)。 - **partial 流式检索开关(窗口内)**:窗口内提供「包含进行中的输出(partial)」复选框(默认关)。开启后,点击「搜索」时**额外扫描 `snapshot.partial`(以点击时刻的快照为准)**;关闭则不纳入。 - 注意:partial 是**点击搜索时的快照**,不做流式持续刷新(实时刷新为 v2,见 §2.2)。 - 文本提取规则:`text`/`reasoning` 块取 `text`;`user`/`context`/`steering` 取 `content` 中 `type === 'text'` 块的 `text`;`tool-result` 取 `content` 文本块;partial 按其 `blocks`(`text`/`reasoning`)提取。 ### FR7 配置与默认值 插件为纯客户端形态,配置以**代码内默认值 + 窗口内运行时控件**为主: - 代码内默认值(如客户端配置通道可用则可覆盖): ```yaml contextChars: 60 # FR4 列表前后 N 字符 drawerContextChars: 500 # FR5 抽屉前后 M 字符 pageSize: 50 maxMatches: 10000 defaultFlags: [i, s, m] # g 固定开启 sources: assistantText: true assistantReasoning: true user: true toolResult: true context: false steering: false presets: - { label: "Actually, wait, let me", pattern: "Actually,?\\s*wait,?\\s*let me\\b" } - { label: "Actually,", pattern: "\\bActually," } ``` - 窗口内运行时控件:flags(`i`/`s`/`m`)、**「包含进行中的输出(partial)」开关**(默认取配置值)。 ### FR8 国际化(i18n) - 通过 `@deepseek-ai/dsh-client-locale` 的 `LocaleNamespaceMap` 注册命名空间,提供 `zh` / `en` 文案。 - 所有 UI 文案(命令描述、统计标签、分页、抽屉、报错、按轮分布说明、partial 开关)可翻译。 ### FR9 可访问性(a11y) - 键盘:窗口内 `Esc` 关闭抽屉→关闭窗口;列表 `↑/↓` 移动、`Enter` 打开抽屉;焦点陷阱在窗口/抽屉内。 - 命中片段使用语义化高亮(``),并给出可读的 aria 描述。 --- ## 6. 交互设计(流程与状态) ### 6.1 主流程 ``` /pattern-search + 回车(或 '/' 菜单点选)→ 直接打开浮层窗口(token 立即消费) 窗口内: └─ 输入/修改正则、切换 flags、切换 partial 开关(均不自动搜索) └─ 点「搜索」/ Enter → 编译执行 ├─ 非法 → 内联报错 ├─ 无命中 → 空态「无匹配」 └─ 有命中 → 统计区 + 分页列表 └─ 点击行 → 侧面抽屉(长片段 + 上下处导航 + 复制) ``` ### 6.2 窗口状态机(store) - `closed → opening → idle | searching | error | results` - `open`、`pattern`、`flags`、`includePartial`、`page`、`selectedMatch`、`drawerOpen` 为 store 字段。 ### 6.3 空态/边界 | 情况 | 表现 | | --- | --- | | 无当前会话 | 命令不可用;窗口顶部提示「请先打开会话」 | | 空正则 | 点「搜索」不执行,显示引导文案 | | 非法正则 | 内联错误 + 保留输入 | | 无命中 | 空态提示 + 建议(如关闭 `i` 或细化正则) | | 命中超上限 | 截断提示 | | 会话切换/关闭 | 结果与所选会话解绑,重新以新会话计算 | --- ## 7. 数据模型与统计定义 ### 7.1 命中记录 `MatchRecord` ```ts interface MatchRecord { index: number; // 全局序号(稳定排序) nodeSeq: number; // 来源节点 seq nodeKind: string; // 'assistant' | 'user' | 'tool-result' | 'context' | ... blockKind: string | null; // 'text' | 'reasoning' | null(非 assistant 节点) turn: number | null; // 所属轮(见 7.3) step: number | null; start: number; // 命中在源文本中的起始偏移 end: number; // 结束偏移 matched: string; // match[0] groups: string[]; // 捕获组 before: string; // 前 N 字符 after: string; // 后 N 字符 sourceText: string; // 完整源文本(供抽屉/展开) isPartial: boolean; // 是否来自 partial(流式进行中) } ``` ### 7.2 搜索算法 1. 遍历 `snapshot.nodes`(按 `seq` 升序);若 `includePartial` 开启,追加扫描 `snapshot.partial`(`isPartial = true`); 2. 按 FR6 提取每节点/partial 的可检索文本段(一个节点可能多段:多个 `text`/`reasoning` 块); 3. 对每段用 `new RegExp(pattern, flags + 'g')` 执行 `exec` 循环收集 `start/end/groups`; 4. 命中点前后按**码点安全**切片(避免截断代理对),生成 `before`/`after`; 5. 组装 `MatchRecord[]`,按 `(isPartial, nodeSeq, start)` 稳定排序并赋 `index`。 > **触发时机(显式)**:搜索仅在「点『搜索』/ Enter」时执行一次;输入/开关切换过程不触发。 ### 7.3 「对话轮数」推导 - assistant 节点:直接取 `AssistantMessageNode.turn`(`.step` 同理)。 - 非 assistant 节点(user/tool/context):节点本身无 `turn` 字段,通过会话时间线推导——取 `chat.timeline.turns`(`Map`,含 `turn/start` 与 `turn/end` 的 event seq),找到**包含该节点 `seq` 的 turn**;不在任何轮内则 `turn = null`。 - partial 命中:`partial` 携带 `turn`/`step`(`PartialAssistant.turn/.step`),直接取用。 - 展示规则:`#turn`;有 step 且需要时展示 `#turn.step`。 ### 7.4 统计指标定义 | 指标 | 公式/定义 | | --- | --- | | `totalMatches` | `MatchRecord[]` 长度(截断后需标注) | | `uniqueMatchedStrings` | 去重 `matched` 的数量 | | `totalTurns` | **当前客户端窗口内**「含可检索文本」的轮数(长会话早期轮次不在窗口内,不计入;UI 标签为「窗口内轮数」,带 ⓘ 说明) | | `hitTurns` | `MatchRecord.turn` 去重数量 | | `hitMessages` | `MatchRecord.seq` 去重数量(命中消息数) | | `hitRate` | `hitTurns / totalTurns`(totalTurns=0 时显示 `—`;**仅反映当前窗口**,UI 带 ⓘ 说明公式与口径) | | `matchesBySource` | 按 `(nodeKind, blockKind)` 聚合计数 | | `matchesByTurn` | 按 `turn` 聚合计数(供按轮分布) | ### 7.5 性能与安全(正则) - **显式触发**:搜索只在点「搜索」/ Enter 时执行一次,绝不在输入或开关切换过程中自动触发(防 CPU 打爆)。 - **规模护栏**:匹配次数上限(客户端 10000 / 宿主 5000)只限制**收集量**,超限截断并提示。 - **执行预算**:客户端搜索在 **Web Worker** 中执行并**超时终止**(3s),灾难性回溯正则会中止扫描而非冻结主线程;Worker 不可用时同步回退。宿主工具在服务端同步执行,仅以上限兜底。 - **零宽防死循环**:零宽命中时手动推进 `lastIndex`。 - **上下文切片**:按码点安全切片,不劈开代理对。 --- ## 8. 技术方案(DSH 落地) ### 8.1 插件形态:混合插件(宿主会话工具 + 客户端 UI) - **客户端半区**(`lib/client.js`):`/pattern-search` 浮层窗口(FR1–FR9 的 UI)。 - **宿主半区**(`lib/index.js`):注册会话工具 **`pattern_search`**——模型可直接调用,对当前会话的持久化事件日志(`agent.session.events`)做同样的正则检索,返回统计 + 分页明细。用途:① 模型自检/观察自身行为(本插件的原始动机);② 作为检索逻辑的会话内测试通道(与客户端 UI 共享同一套提取/匹配/统计语义)。纯只读,不写日志、不触发模型请求。 - 客户端 `search.ts` 与 `src/host/search.ts` 语义一致(命中记录、上下文切片、ReDoS 防护)。 `package.json`(无 `dsh.bundle`,仅 `dsh.client`): ```jsonc { "name": "@deepseek-ai/dsh-pattern-search", "type": "module", "exports": { ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" } }, "dsh": { "client": { "inject": [ "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-ui-commands", "@deepseek-ai/dsh-client-ui-layout", "@deepseek-ai/dsh-client-ui-conversation" ], "platform": "web" } }, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2", "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2", "@deepseek-ai/dsh-client-ui-commands": "^0.1.1-rc.2", "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2", "@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2" } } ``` ### 8.2 模块划分(`src/`) ``` dsh-pattern-search/ ├── package.json └── src/ ├── host/ │ ├── index.ts # 注册 pattern_search 会话工具(defineTool) │ └── search.ts # 事件日志 → 分段提取(检索语义走共享核心) ├── shared/ │ ├── search-core.ts # 共享搜索核心:匹配/切片/零宽/上限/截断(宿主与客户端共用) │ └── stats.ts # 共享统计(命中率=消息口径、来源/按轮分布) └── client/ ├── index.ts # 客户端插件体:命令 + 窗口注册 ├── command.ts # /pattern-search inputTriggers 触发器源 ├── store.ts # 窗口/结果状态 + 每页条数 ├── extract.ts # ConversationSnapshot → 可检索文本段(对真实类型编译) ├── worker.ts # Web Worker 搜索执行:超时终止,Worker 不可用时同步回退 ├── worker-source.ts# 自包含 worker 源码(与共享核心做 parity 测试) ├── i18n.ts # zh/en 文案 └── ui/ # PatternWindow + ResultList(主题自适应) ``` ### 8.3 关键接入点(对齐现有 DSH API) | 需求 | 接入点 | | --- | --- | | `/pattern-search` 命令 | `ctx.get('inputTriggers').registerSource({ trigger:'/', name:'pattern-search', order, candidates, onPick, matchEnter })`(纯客户端;`onPick`/`matchEnter` 返回 `'handled'` 并打开窗口、立即消费 token) | | 消费 token | `ctx.get('sessions').scope(sessionId)?.bail(scope, 'slash/input-consume-token', { guard: {kind:'bare-token'|'span', ...} })` | | 窗口渲染 | `ctx.get('slots').inject('shell.overlay', () => slots.register({ name:'shell.overlay', id, order, locale }, render))`(根作用域 list 席位,可叠加浮层;组件自行接管 `pointer-events`) | | 会话数据 | `ctx.sessions.list` 取 `current`;`ctx.sessions.binding(current).session`(`SessionFace = ISession & ObservableSnapshot`)→ 用 uSES 订阅快照 | | 节点/轮/partial | `ConversationSnapshot.nodes`(legacy 兼容视图)、`ConversationSnapshot.partial`(`PartialAssistant.turn/.step/.blocks`)、`chat.timeline`、`AssistantMessageNode.turn/step/blocks` | | UI 原语 | `@deepseek-ai/dsh-client-ui-primitives`(抽屉/浮层/输入/按钮/复选框)+ `@tanstack/react-virtual` | | i18n | `ctx.get('locale').register(NS, {zh, en})` + `locale.bind(NS)`(`LocaleNamespaceMap` 机制) | **客户端 `apply` 骨架:** ```ts export const inject = ['commandUi', 'sessions', 'slots', 'layout', 'locale']; export function apply(ctx: ClientContext) { const store = createStore(); // '/' 触发器源:/pattern-search → 打开窗口(纯客户端,不留痕;token 立即消费) const inputTriggers = ctx.get('inputTriggers'); inputTriggers.registerSource({ trigger: '/', name: 'pattern', order: 1, candidates: (session, req) => /* 菜单候选 */ ['/pattern-search'], onPick: (pick) => { openWindow(store); consumeToken(pick.session.sessionId, pick.span); return 'handled'; }, matchEnter: (session, line) => { if (line !== '/pattern-search') return Promise.resolve(undefined); openWindow(store); consumeToken(session.sessionId, { kind: 'bare-token', token: '/pattern-search' }); return Promise.resolve('handled'); }, }); // 注册窗口到 shell.overlay(略) } ``` ### 8.4 加载方式(仅 GitHub 安装) `cordis.patch.yml` 提供 bundle 行(`insert`),使包进入 profile 组合(`dsh plugin add` 追加到 `dsh.profile.bundles`);**宿主半区为 no-op**,真正逻辑在客户端。由 web-app 的 client 模块加载器依据 `dsh.client` 装配 `lib/client.js`(`window.__ModuleLoader__.load({id, factory})` 格式,参考 dsh-context)。 **安装(GitHub,单条命令):** ```bash dsh plugin --profile web add github:yzhangjy/dsh-pattern-search ``` `dsh plugin add` 自动克隆仓库、安装依赖并经 `prepare` 脚本构建 bundle;随后重启 `dsh web` 并硬刷新浏览器。 - 重建插件后无需重新 `dsh plugin add`(profile 以 `link:` 指向插件目录,改 `lib/` 即生效,客户端改动仅需硬刷新,宿主改动需重启)。 - **不发布 npm**;安装仅通过上述 GitHub 方式。 ### 8.5 关键取舍 - **无痕**:客户端自有 '/' 触发器源不触发宿主命令、不记录 `command/run`/`command/done`,**会话流不产生任何命令节点**——这是放弃 `/pattern-search ` 的直接收益。 - **partial 为搜索时快照**:开启开关后,partial 命中反映的是**点击「搜索」那一刻**的进行中输出,不随流式持续刷新(避免回到"自动搜索"的 CPU 风险)。 - **配置通道**:未使用配置注入,默认值写死在代码 + 窗口内控件。 --- ## 9. 实现状态与验收标准 ### 实现状态 - 已实现:`/pattern-search` 浮层窗口、文本提取(nodes + partial)、匹配统计、分页明细表、主题统一、`pattern_search` 会话工具。 - 未实现:侧面抽屉(长片段/复制)、虚拟化列表、a11y 收尾。 ### 验收标准(Acceptance Criteria) 1. 存在会话时,输入 `/pattern-search` + 回车(或在 '/' 菜单点选)**直接打开浮层窗口**;无会话时不可用。 2. 全程无宿主命令节点:搜索、打开窗口不向会话流写入任何 `CommandNode`(不留痕)。 3. 窗口内**修改正则、切换 flags、切换 partial 开关均不自动搜索**;仅点击「搜索」或按 Enter 才触发一次搜索。 4. 输入 `Actually,?\s*wait,?\s*let me\b`(含 `i`)能检索到 "Actually, wait, let me"、"Actually wait let me" 等变体。 5. 非法正则在窗口内联报错,不崩溃、输入保留。 6. 统计区数字与实际命中一致:总数、去重串数、命中轮数、来源分布、按轮分布。 7. 列表分页正确:序号跨页连续、页码/跳页/首末页可用、每页条数符合 `pageSize`。 8. 列表行正确展示 `前 N 字符 + 高亮命中 + 后 N 字符`,轮数与来源标注正确。 9. 点击行弹出抽屉,展示 ±M 字符长片段、命中高亮、元信息;上一处/下一处可遍历;复制可用;**无「定位到会话位置」入口**。 10. **partial 开关**:窗口内可见可切换;关闭时结果不含 partial;开启后点「搜索」结果包含点击时刻的 partial 命中(标注 `isPartial`)。 11. 命中超 `maxMatches` 时截断并提示。 12. 会话切换后,结果以新会话重新计算;插件卸载后命令与窗口移除。 13. 长会话(如数千节点)下列表滚动流畅、搜索不卡死(ReDoS 防护生效)。 14. 中英文案均可用,键盘可完成「打开窗口→搜索→打开抽屉→关闭」全流程。 ---