给 DeepSeek Harness 装内容搜索插件,解决搜得到标题,搜不到内容(源码挂载的静默失败与一行修复)
发布:2026-10-24 | 环境:DeepSeek Harness 0.2.1-rc.2(Windows / Electron 44.7.0 / Node 24)
插件:dsh-session-searchv0.3.2(MIT © Victue),本地源码安装
结论先行:官方搜索只能搜会话标题;社区插件能在全部会话的正文里做中文子串搜索、按内容时间倒序。源码手动挂载时,insert 行的name若指向"包目录"而不是"入口文件",Node ESM 无法导入目录,整行配置被静默跳过——不报错、不崩溃、什么都不出现。修复只要把路径指到入口 JS 文件。
本文包含:为什么需要这个插件、插件原理、源码挂载遇到的问题与修复、可直接交给 agent 的安装步骤、10 秒客观验证法。
一、痛点:会话攒了几百个,想找一句说过的话,找不到
DeepSeek Harness 用得越久,会话越是只进不出。我这台机器上已经落了 475 个会话日志文件,正文加起来几十 MB。日常场景很具体:
- 上个月明明解决过一个端口占用的问题,想翻出当时的命令;
- 某个方案在某个会话里讨论过一半,想找回那段上下文;
- 想搜自己两周前对某个概念的原始提问。
然后你点开侧边栏搜索,输入关键词——只能命中会话标题。正文里写过什么,一概搜不到。
会话内容检索这个需求,官方产品里目前是缺席的。
二、官方搜索为什么不行:不是 bug,是三件事叠出来的
这是我从 dsh-session-search 的 README 里读到的分析(作者做过扎实的逆向),转述如下:
| # | 事实 | 后果 |
|---|---|---|
| 1 | 内容检索后端在原生配置里被显式关闭(openAt: never),官方注释自己写着"侧边栏搜索只匹配标题和工作区名" |
侧边栏那个框,接到的就只有标题索引 |
| 2 | 即使打开后端,它用 FTS5 unicode61 分词——一整段中文被当成一个 token。实测「光速太慢,星际文明无法成为整体」只切出两个大 token,搜「光速」搜「星际」都返回空;换 trigram 分词器也不行,它不接受 2 字查询 |
对中文内容,这条路的工具本身就是错的 |
| 3 | 就算前两条都解决,跨进程返回的结果丢掉了每条命中的时间戳,排序是相关度优先 | "按内容时间倒序"这个最基本的需求,公开接口做不到 |
三条同时成立。所以这不是"打开一个开关"能解决的事,需要一个自己的索引——这就是选择 dsh-session-search 的原因。
三、插件做了什么:绕开官方后端,主机端自建索引
dsh-session-search 的思路(作者 README 的说法,装完后的体验与描述一致):
- 主机端增量解析全部会话日志(
session.v*.jsonl.zstd,多帧 Zstandard 逐帧解压——Node 自带的解压只解第一帧,这是个真坑),把每条记录的文本连同该记录自己的时间入库; - 查询用子串匹配而不是分词——中文 2 字查询能命中,大小写不敏感;
- 结果按命中记录自身时间倒序——这正是官方接口给不了的;
- UI 上是"一等公民":侧边栏 Plugins 和 Schedules 之间一个放大镜图标 + 全宽结果页 +
Ctrl+Shift+F,点命中可以看前后文再跳进会话;另送一个会话内Ctrl+F查找条(在已渲染的聊天流上高亮,不改 DOM); - 默认排除子代理会话(子代理会话数量常远超本体),可筛选"我的提问 / 助手回复 / 工具输出";
- 插件自身零依赖,只用
node:*内置模块。
作者在同版本(0.2.1-rc.2)上做过验证:178 个会话 / 21.4 MB 文本,冷建索引约 1.2 秒,单次查询 3–26 毫秒。我这边 475 个文件,实际体验也是秒级。
四、源码挂载遇到的问题:装完重启,整行没生效
安装方式是手动挂载:往 profile 的 cordis.patch.yml 加一行 insert,name 直接给源码的绝对路径。重启之后:
- ❌ 侧边栏没有图标
- ❌ 插件管理页看不到它
- ❌ 宿主端 HTTP 通道不存在(探测返回 405)
- ❌ 没有任何报错——应用启动正常、patch 解析正常、无崩溃日志
这类故障的特点是不给你任何抓手,只能靠主动探测把"它没活"变成可观测的信号。排查动作与结论:
| 检查 | 结果 | 说明 |
|---|---|---|
| patch 文件语法 | ✅ js-yaml 实际解析通过,10 个条目 | 排除 YAML 写坏 |
| patch 文件被加载了吗 | ✅ 同文件里更靠前的 MCP 条目在生效 | 排除"改错文件"(层级问题见第七节) |
| 崩溃 / 恢复痕迹 | 无 crash 日志、patch 文件没被崩溃恢复改名成 .bak |
排除"启动失败后自我修复" |
| 进程启动时间 vs 文件写入时间 | 启动晚于写入 | 排除"改完没生效就测" |
| 路由探测 | 405,与不存在的对照路由一致 | 宿主行没激活(正常应为 401,见第六节) |
| 浏览器启动图(SSE 图流) | 有已安装的 dsh-free-search,没有 dsh-session-search |
整行没进组合 |
两项红旗指向同一个结论:这一行没有进入插件组合。
五、根因:ESM 不能导入一个目录
定位靠一个 30 秒的最小复现:
# 指向包目录(出问题的写法)
node --input-type=module -e "import('file:///.../dsh-session-search')"
# → FAILED: ERR_UNSUPPORTED_DIR_IMPORT
# 指向入口文件
node --input-type=module -e "import('file:///.../dsh-session-search/lib/entry.js')"
# → OK:导出 name / inject / apply 全部正确
原因链条:loader 对 insert 行的绝对路径,处理方式是转成 file: URL 后直接做 ESM 导入。Node ESM 的导入目标必须是文件(或裸包名经 node_modules 解析),目录会直接抛 ERR_UNSUPPORTED_DIR_IMPORT。而这一行导入失败的后果是整行被跳过——不注册、不报错、不落盘(报错只进宿主 stderr,桌面启动时没人看得到)。
插件作者 README 的手动挂载示例用的是裸包名(从 profile 的 node_modules 解析),从未测过绝对路径写法;文档说"支持绝对路径",但没说必须精确到 JS 文件。坑就坑在这句没说清的半句话上。
修复——把目录换成入口文件,一行:
- insert:
- id: session-search
- name: '<你的插件检出路径>/dsh-session-search'
+ name: '<你的插件检出路径>/dsh-session-search/lib/entry.js'
重启后一切正常:图标出现、搜索可用。
六、10 秒客观验证法:两个探测,不用翻界面
插件装没装活,与其肉眼翻界面,不如直接问宿主。两个探测都不需要登录态:
探测 1:路由状态码。 插件的宿主行激活后会注册 /session-search 前缀路由,且被鉴权栅栏保护——未认证请求得到 401;没注册则是 405(随便拿一个不存在的路由当对照组,也是 405):
await fetch('http://127.0.0.1:<端口>/session-search/search', { method: 'POST' }) // → 401 = 活了
await fetch('http://127.0.0.1:<端口>/no-such-channel/foo', { method: 'POST' }) // → 405 = 对照组
探测 2:启动图。 GET /plugins/events 是一条 SSE 图流,推送浏览器要加载的插件条目——它是"哪些包进了组合"的权威信号,直接在里面找包名:
const res = await fetch('http://127.0.0.1:<端口>/plugins/events') // text/event-stream
// 读几秒事件流,在 data JSON 里搜 "dsh-session-search"
两个探测组合出四象限,覆盖所有故障形态:
| 路由 | 图里有包 | 结论 |
|---|---|---|
| 401 | 有 | ✅ 正常 |
| 405 | 无 | ❌ insert 行没激活——先查 name 是不是指向了目录 |
| 401 | 无 | ⚠️ 宿主活了但客户端声明没被扫到——查包里 package.json 的 dsh.client 与 exports['./client'] |
| 405 | 有 | ⚠️ 宿主 apply 抛错——查宿主 stderr |
端口重启会变。查当前端口:
Get-NetTCPConnection对一下DeepSeek Harness进程的监听端口即可。
七、避坑要点
name指向包目录 = 整行静默跳过——必须精确到入口 JS 文件(package.json的main/exports['.']指向的那个)。- 写到 profile 级 patch(
~/.dsh/profiles/<profile>/cordis.patch.yml)。home 级(~/.dsh/cordis.patch.yml)对所有 profile 生效且优先级更高,适合全局项,插件行放这里会让 headless 等无webServer的 profile 挂起等待。 - 没有日志是这类故障的正常表现——没有 crash、没有
.bak改名、控制台无输出,别在"找日志"上耗时间,直接做第六节的主动探测。 - 判断"文件被加载"要看同文件的其他条目是否工作,你这条没生效不代表文件没读。
- 鉴权机制:根路径 401 提示 “dsh web authentication required”,浏览器 cookie 按 Host 权限签名,重启后端口变化会使旧 cookie 失效——验证要用无认证探测端点(状态码、SSE),别试图直接打开页面。
- 配置改动只在启动时应用(desktop profile 无热加载),探测脚本在重启之前写好,珍惜每轮重启。
八、源码安装步骤(可直接交给 agent 执行)
如果你也是从本地源码/clone 安装,把下面的步骤交给 agent 即可,无需自己翻配置:
① 定位入口文件——读插件根目录 package.json 的 main 或 exports["."],得到入口路径(本插件是 lib/entry.js)。
② 写 insert 行——编辑 profile 级 ~/.dsh/profiles/<profile>/cordis.patch.yml(不是 home 级),末尾追加:
- insert:
- id: <插件名,如 session-search>
name: '<源码绝对路径>/<入口文件,如 lib/entry.js>' # ⚠️ 必须是文件,不能是目录
③ 校验 YAML——用 js-yaml(或任意解析器)实际解析一遍,确认是数组、末条是本次 insert。
④ 重启 DSH——patch 只在启动时应用。
⑤ 验证——重启后用当前端口跑第六节的两个探测:路由 401 + 启动图含包名 = 通过;界面上再看一眼侧边栏图标(作者 README 坦承未做视觉验证,界面确认是验收的最后一环)。
⑥ 失败对照——按第六节四象限表定位:405 + 图里没有 十有八九是 name 指向了目录。
给 agent 的三条红线:
name不能是包目录——ESM 导入目录 = 整行静默跳过- 写到 profile 级 patch,别写 home 级
- 改完重启才生效;探测脚本重启前写好
回滚:删掉 patch 末尾的 insert 块,重启即净。
兼容性:0.2.1-rc.2 实测通过。注意插件 v0.1.0 曾有一个 Web 端快捷键撞车导致启动崩溃的事故,v0.1.1 已修(快捷键仅桌面端注册 + 失败降级),现在装的是 v0.3.2。
首选方式其实是 bundle 安装(
plugin_manager action=install_bundle,或 profilepackage.json加link:依赖)——它走 node_modules 裸包名解析,天然避开本文的坑;手动挂载适合"只想指个本地目录"的场景,按上面步骤走即可。
九、结语
完整过程:发现官方搜不到会话内容 → 选定社区插件 → 源码挂载整行静默失败 → 六项排查定位"未进组合" → 最小复现锁定根因(ESM 不能导入目录)→ 改一行路径修复 → 沉淀出 10 秒验证法与可复用的安装步骤。
装插件这种事的讽刺之处在于:功能本身 1.2 秒就能建好索引,而让它跑起来的关键只是配置里的一行。把这个坑写下来,后来者照第八节的步骤走,十分钟就能避掉。
- 插件仓库:https://github.com/Victue/dsh-session-search (MIT © Victue,感谢作者的扎实逆向和文档)
- 排查判定表与回滚要点已并入本文第四、七、八节
- 环境:DeepSeek Harness 0.2.1-rc.2 / Windows
作者在 README 里也坦承:他构建插件的环境没有浏览器控制能力,没做视觉验证。所以装完请自己点开搜一下——按第六节的探测过了,再看一眼界面,就是完整的验收。