dsh-content-search:给当前对话加关键词搜索和定位

前言

在 DSH 里跑长对话,回查一个关键词通常靠滚动加肉眼找。dsh-content-search 做的事很直接:把已经写入日志的对话变成可检索的,在对话区右上角放一个搜索按钮,右边栏出搜索框,命中的字标黄,点一条结果跳过去。维护者是 appthin,归类为「记忆」,插件目录页显示 2 星。它是社区插件,和 DeepSeek / 幻方没有官方从属关系。

这是什么

一句话定位:给当前这段对话加搜索,不产生任何新内容。

插件明确说自己不碰对话窗口、不写会话事件、不改补丁层,只做两件事——读,和定位。它把已有的日志变成可检索的,而不是在对话里新增一层结构。

核心功能

下面列出的能力都来自 README。

1、搜索按钮:放在对话区头部右侧(conversation.session.header.utilities,order 15)。面板已开着时点击是聚焦并展开右边栏,不会再开一个。
2、右边栏搜索框:作为右边栏的正式标签页(类型 contentSearch)打开,位置会随会话记住。
3、两种范围:「已加载」是当前持有的事件窗口,纯前端即时出结果;「整段历史」走宿主日志全量。
4、黄色高亮:对话区里所有命中的字加黄色底,用户消息和助手回复一视同仁。
5、命中定位:点结果 → 翻历史到那条 → 滚到该行 → 落淡黄底,2 秒后淡出。
6、可选中复制:每条结果是 div role="button" 而不是 <button>,文字可以拖选复制。
7、展开更多上下文:悬停时右下角浮出「展开更多」,点击前后各多 300 字左右。
8、键盘:Enter 跳到第一个结果,↑/↓ 移动,Esc 清空。
9、中英双语:命名空间 content-search,跟随界面语言。

两种范围,区别是真区别

「已加载」和「整段历史」不是同一个功能的两个开关,数据来源和代价都不同。

已加载 整段历史
数据来源 对话当前持有的事件窗口 宿主日志的全量事件
实现 浏览器内直接扫 GET /api/content-search.search → 宿主
延迟 即时 一次网络往返
上限 结果 300 条 每页 200 条,累计 1000 条
会不会改对话 不会 不会

默认停在「已加载」。README 的解释是:慢的那条不适合设成默认,否则每次搜索都要先付一次网络代价。需要全量时再切。

安装与启用

版本要求:DSH >= 0.1.6-alpha.1 且 < 0.3.0,README 注明已在 0.2.1-rc.5 上测试。DSH 自身依赖 Node.js >= 24.2.0 与 Git >= 2.31.0。

Desktop 版在应用内安装,因为 Desktop 的 profile 由 Electron 独占管理,dsh plugin --profile desktop 会被拒绝。两种方式:插件页「插件 → 添加」,来源填 https://github.com/appthin/dsh-content-search.git;或在会话里让助手调 plugin_manager 的 install_bundle,目标填本目录绝对路径。

web / tui 等 profile 用命令安装:

# 本地目录(link 方式,改动即时生效,适合开发调试)
dsh plugin --profile web add file:E:/dev/deepseek_harness/dsh-content-search

dsh plugin --profile web ls                        # 查看已装
dsh plugin --profile web remove dsh-content-search # 卸载

装完打开「插件 → dsh-content-search」,卡片上应有标题与描述,取自 locale/*.json 的 meta。

同路径下改动后,remove_bundle + install_bundle 即可热更新,不必重启。注意:改了插件目录路径必须重启桌面端,否则宿主缓存里仍是旧路径,表现为「插件装了、按钮没了」;解决办法是重启或改包名。

卸载:

dsh plugin --profile web remove dsh-content-search

插件不写 profile 文件、不装依赖、不改 cordis.patch.yml,移除即清除。

典型用法:装完按这个顺序核对

README 给了验证步骤。先做语法检查,再动界面:

node --check index.js
node --check client.js

界面里依次确认:

1、对话区右上角出现放大镜按钮;
2、点击 → 右边栏出现「搜索」标签页(已存在时会聚焦并展开);
3、输入关键词 → 对话区命中的字变黄,用户消息和助手回复都有;
4、点一条结果 → 对话滚到该位置并短暂泛黄;
5、悬停一条结果 → 右下角浮出「展开更多」→ 点击后前后各多 300 字左右,链接变「收起」;
6、切到「整段历史」→ 提示行显示命中来源,结果条数变为「条命中(整段历史)」。

它怎么实现的

浏览器半边负责按钮、面板、高亮和定位。高亮用 CSS Custom Highlight API(CSS.highlights),不碰对话的 DOM:

registry.set('dsh-content-search', new Highlight(...ranges))
::highlight(dsh-content-search) { background-color: rgba(255, 206, 0, .55); color: inherit }
::highlight(dsh-content-search-hit) { background-color: rgba(255, 206, 0, .18) }

不包 <mark>,是因为那要改别的插件的 DOM。高亮只覆盖已经渲染出来的行,折叠收起的内容不会凭空被点亮。

宿主半边只有一个职责:读会话的完整持久日志,通过一条只读路由暴露给浏览器:

GET /api/content-search.search?sessionId=…&query=…&limit=…&cursor=…

每页默认 100 条、最多 300 条,游标是十进制偏移。每条命中带回关键词前后各 480 字左右的原文。定位则走 session.loadThrough(hit.seq) 翻历史,再按 Chat 的渲染属性(data-chat-node-key 等)找行;这套属性是渲染约定而非公开 API,插件做了特性检测,找不到时提示「已翻到该位置,但这一条当前没有渲染出来」。

一个刻意的取舍是:整段历史不用宿主的全文索引 sessionQuery.searchEvents,因为它可以被部署关掉(openAt: "never" 时抛 SESSION_QUERY_SEARCH_DISABLED),插件改用不带开关的 sessionQuery.filterEvents 做字面扫描。代价是大会话第一页比索引慢,理由是「能搜到比搜得快重要」。

适用场景与注意

适合在长对话里按关键词回查、跳回某条消息。它只搜当前会话,跨会话不通。

注意事项:

  • 插件以当前 dsh 进程的权限运行。安装前先检查源码与许可证再装。
  • 整段历史是字面扫描,大会话第一页会慢;不做模糊匹配、不分词、不支持正则。
  • 每条命中上下文窗口是 ±480 字,展开上限 300 字是从这个窗口里切的。
  • 高亮只覆盖已渲染的行,折叠收起的内容不会被强制点亮。
  • 流式回复期间可能出现短暂的重复行。
  • 定位依赖 Chat 渲染契约,上游改了那几个 data-chat-* 属性会退化。
  • 宿主日志不可读时,面板会显示宿主原话,并退回「加载更早的内容」把窗口撑大(最多 2 次、每次 5 秒超时)。

许可证:MIT,不含第三方代码,不打包第三方二进制,不引入依赖。

结尾

它的价值在于轻:不往对话里加东西,只把已有的日志变得能搜、能跳。目录页在 https://www.skillhub.cn/plugins/appthin/dsh-content-search,源码在 https://github.com/appthin/dsh-content-search。

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜