DSH-chat-keeper
仓库名:
DSH-chat-keeper(原DSH-conversation-manager,旧地址会自动跳转)。 包名:仍是dsh-conversation-manager—— npm 包名必须全小写,而且已安装的用户是靠包名关联的, 改包名会让他们失联,所以仓库名与包名在这里故意不一致。
一个真正接入 DeepSeek Harness(DSH) 插件体系的对话管家。它补齐了 DSH 界面缺失的那部分: 跨工作区盘点、正文搜索、批量处理,以及 DSH 本身没有的 —— 删除对话(回收站式,可恢复)。
其中搜索是最常被问到的:DSH 的正文索引默认是关的(组合包给的是 openAt: never),
打开后中文又只认"整段"。本插件替你打开索引,并额外提供一条读磁盘的通道 ——
中文子串能搜、没打开过的旧对话也能搜、还能定位到命中那句并把前后文读出来。
第三方社区插件,与 DeepSeek 官方无关。代码由作者与 AI 助手协作完成, 详见文末「关于作者与 AI 协作」一节。
它解决什么问题
DSH 自带的侧栏已经能搜索、置顶、改名、分叉、归档 —— 这些本插件刻意都不重复。 它只做四件侧栏给不了(或默认给不了)的事:
| 能力 | 为什么只有它能做 |
|---|---|
| 跨工作区单一总表 | 侧栏一次只看一个工作区;"我到底有多少对话、哪些是垃圾"需要一个扁平视角 |
| 批量处理 + 按时间筛选 | 侧栏只能一条条点;这里可勾选任意多行批量归档,或用「7/30/90 天前的」筛出陈旧对话一次处理;再用「显示已归档」开关把已归档的对话从总表里收起来 |
| 真删除(可恢复) | DSH 只有归档,没有删除。 本插件在磁盘层面删除,并保留回收站与恢复 |
| 正文搜索(可用 + 加强) | DSH 的正文搜索是默认关闭的 opt-in 能力(组合包给的是 openAt: never),此时侧栏只匹配标题与工作区名。本插件的组合包 patch 把它打开,并在面板里补上关键词高亮、工作区列与"复制结果清单" —— 详见下文「搜索对话正文」 |
组成
| 半 | 文件 | 作用 |
|---|---|---|
| 宿主半 | index.js + lib/ |
12 个模型可调用工具、会话索引、conversationManager 服务(纯 JS,无需构建) |
| 浏览器半 | src/client/ → dist/client.js |
Web GUI 的「对话管理器」页签 + 会话行菜单项(TypeScript + React) |
安装
方式一:从 GitHub 装(推荐给"我只是想用")
在 DSH 的插件页面里安装本仓库地址:
https://github.com/Sky-lll27/DSH-chat-keeper
或在命令行:
dsh plugin --profile <你的 profile> add https://github.com/Sky-lll27/DSH-chat-keeper
装好后重启 DSH(宿主插件树在启动时装配)。
不需要构建:浏览器半的产物(
dist/client.js)随仓库一起提供。 这一点是刻意的——pnpm 11 会拦住 git 依赖的安装期构建脚本 (ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED),要么让使用者去批准构建,要么干脆不构建。 所以本项目把产物入库,构建只在prepack(打包发布)与手动pnpm run bundle时发生; CI 会校验"提交的产物 == 重新构建的产物",防止两边漂移。
方式二:从本地目录装
在插件页面里安装本目录的绝对路径(例如 <本仓库目录>)。
同样是 link:,同样不需要构建(产物已在仓库里)。
本包声明了 dsh.bundle,因此会被当作组合包(而不是普通依赖)追加进
dsh.profile.bundles,插件页随即能看到它的开关与卸载按钮。
方式三:开发期用 --patch 直接挂载
cd /path/to/this/repo && pnpm install && pnpm run bundle
pnpm dsh web --patch /path/to/this/repo/patch.local.yml
适合边改边验:不需要安装成组合包。改了浏览器半的代码记得 pnpm run bundle。
卸载
在插件页点卸载;或从 profile 的 package.json 里移除 dependencies 与
dsh.profile.bundles 中的本包,再 pnpm install。
使用
页签正文是一张跨工作区的会话总表:
- 顶部:正文搜索框、
共 N 个 · M 运行中 · K 已归档、时间筛选下拉(全部 / 7 / 30 / 90 天前的)、「显示已归档」开关、全选。 - 「显示已归档」开关(默认打开,与历史行为一致):关掉后总表里不再列出已归档的对话,
顶部统计仍如实报告归档数,旁边补一个
符合条件 N 个说明当前可见几条。 选择写进localStorage,面板重开后保持。 两条边界:只管总表——搜索结果不受它限制(搜到什么就显示什么,否则会被当成"搜索坏了"); 关掉期间无法从总表批量「取消归档」,需要时把开关再打开即可。 - 表格:勾选框、状态点(运行中 / 等待交互 / 已完成未读 / 空闲)、标题、工作区、最近活动。 点击标题可打开该对话。
- 勾选后出现批量条:
归档/取消归档/删除所选/清除。 归档自己就能完成;若某行仍有工作在进行,会先弹出「停止并归档?」确认。 - 搜索时:表格被命中的对话替换(每条带工作区、状态与高亮摘要),可一键复制结果清单。
- 底部:回收站一览(批次数、批次名、条数、大小)与「刷新」。
搜索对话正文
搜索有两条路,能力不一样——先说清楚各自的边界(都经真机实测):
| DSH 原生侧栏搜索 | 本插件面板搜索 | 本插件工具搜索(conversation_search) |
|
|---|---|---|---|
| 匹配标题 / 工作区名 | ✅ 立即 | 表格里直接看 | ✅ |
| 匹配对话正文 | ✅ 但默认是关的(见下) | ✅ 走同一个索引 | ✅ 而且读磁盘,冷对话也能搜 |
| 中文子串(如在一整句里搜「检索」) | ✅ 装了本插件后(宿主侧字面兜底,见②);没装插件时按词元匹配 | ✅ 同一条兜底 | ✅ 字面子串,中文无障碍 |
| 结果条数 | 最多 20 个会话(宿主服务端就截到 20) | 同一上限,不承诺更多 | 默认 10(可调到 50),扫描范围另受时间预算约束 |
| 摘要片段 / 高亮 | 有片段、无高亮 | ✅ 片段 + 关键词高亮 | ✅ 片段 + 命中次数 |
| 复制结果清单 | ❌ | ✅ | ❌(结果直接回给模型) |
① 为什么"正文搜索默认是关的":dsh-base 与 dsh-web-app 两个组合包都把内容索引
session-query-sqlite 设成 openAt: never。此时 ctx.sessionQuery 虽然挂着,但一调用搜索就
失败,界面静默退化成只匹配标题与工作区名(官方文档原话:"内容搜索失败时,元数据匹配项
仍会显示,不另给警告")—— 所以"搜不到正文"的体验,真实原因是索引没打开,而不是缺功能。
本插件的组合包 patch 替你把它打开(openAt: first-search,默认内存索引不落文件)。
② 中文子串原来为什么搜不到,现在怎么补上的:这个索引建在 SQLite FTS5 上,
分词器 unicode61 写死在建表语句里,查询又被包成一个字面短语送进 MATCH。
unicode61 把连续字母串当一个词元:英文单词天然被空格隔开,所以英文搜得准;
中文整句连成一片,只有查询词正好等于被标点或英文切出来的那一段时才命中。
实测(同一份索引,未装本插件时):
| 查询 | 结果 |
|---|---|
session(英文) |
✅ 命中 |
"内容检索示例"(整段中文) |
✅ 命中 |
"检索"(中文子串,原文是「内容检索示例」) |
❌ 0 条 |
0.5.1 起:本插件在宿主侧把网关的搜索入口 sessionController.search 包了一层
(ctx.sessions 是会话存储,不是同一个对象——0.5.0 包错了对象、补丁从未被调用,
工程笔记里记了这次返工);查询里含中日韩文字时,
额外读一遍磁盘日志做字面子串扫描,把命中的会话合并进结果。因为官方放大镜与
本插件面板走的是同一个入口,所以两处一起变好;纯英文查询连扫描都不触发,
错误语义(比如"索引没打开")也原样保留。不改 DSH 的任何文件。
边界照实说:
- 兜底是尽力而为:默认时间预算 2 秒、最多读 120 个对话(按最近活动倒序), 超预算只返回已经扫到的,不假装"搜完了";
- 官方结果始终排在前面,兜底只往后面补,不改变官方的排序与授权过滤; 两边都有的会话保留官方片段;
- 扫描是同步读日志(与工具同一实现),中文查询时界面可能有轻微停顿;
调小
cjkSearchBudgetMs或设cjkSearch: false可以关掉; - 这是运行时包一层宿主方法。DSH 升级若改了
sessionController.search的返回形状, 兜底会自动退化成官方行为(形状不认识就原样返回 + 记一条 warn),不会把搜索搞坏。
工具那条路仍然不同(也仍是能力最全的):它直接读会话日志(多帧 zstd 逐帧解压)
做字面子串匹配,返回命中 seq / 事件类型 / 前后原文与命中次数,
扫描范围与预算可调,结果直接给模型用。用法就是跟模型说:
「帮我搜含『某个关键词』的对话」
工具自带三道刹车:最多读 max_conversations 个对话(默认 60,按最近活动倒序)、
时间预算 budget_ms(默认 5 秒)、结果上限 limit。超出预算时结果里会如实标注
diskTimedOut,而不是假装"搜完了没有"。
想要持久化索引(重启后不必重建)就把组合包 patch 里那行的
path从:memory:改成真实路径, 例如$DSH_HOME/storages/session-search.db;它只是派生数据,随时可删、删了会自动重建。
关于删除:为什么是"复制指令"
删除与恢复是宿主的磁盘操作,而浏览器半没有文件访问权。DSH 只允许浏览器半通过
生成的 Remote 命名空间与宿主通信;外部插件要声明自己的 Remote 需依赖内部包,代价不小。
因此面板负责选择,「删除所选」把一条带确切会话 id 的指令复制到剪贴板,
粘贴到输入框发送即可,由宿主的 conversation_delete 执行。
这样做的两个好处:不会偷偷往你的对话里发消息;指令带确切 id,不会误删。
刷新:让原生侧栏立刻少掉已删的行
删除发生在磁盘上,宿主不会为此推事件,客户端只在重连时重拉会话列表 ——
所以删完之后原生侧栏里的行会一直留着,直到重连或重启。
面板的「刷新」调用 sessions.refresh() 重新拉取列表基线,顺手也重读回收站;
打开面板时也会自动重拉一次。所以:删完点一下「刷新」就够了,不必重启。
模型可调用的工具
宿主半注册 12 个工具,随对话直接被模型调用:
| 工具 | 作用 |
|---|---|
conversation_list |
列出对话(最近活动在前),支持条数 / 关键字 / 是否含冷会话 |
conversation_get |
单个对话的元数据 + 最近消息 + 事件类型计数 |
conversation_history |
把对话历史投影为 user/assistant/tool 消息,可含工具调用与结果 |
conversation_search |
字面子串检索标题、工作目录与所有对话的正文(含冷对话,直接读磁盘日志),中文子串也能命中;返回摘录、命中次数与扫描诊断 |
conversation_stats |
汇总对话数、存活/运行数、轮次、消息数、工具调用、事件数、token 用量 |
conversation_label |
设置管理器侧标题;宿主有持久化改名服务时一并写入并如实报告走了哪条路 |
conversation_fork |
在指定事件序号处派生新会话 |
conversation_selftest |
只读自检:把本插件对宿主的所有假设与宿主真实形态逐条对账 |
conversation_list_all |
从磁盘列出全部对话(含未装载的),带标题、首句话、工作区、大小、磁盘路径 |
conversation_delete |
删除:默认进回收站(可恢复),permanent:true 才真删;必须 confirm:true |
conversation_restore |
从回收站原样恢复(默认最新一批);原位置被占用时跳过而不覆盖 |
conversation_purge |
清空回收站:永久删除批次(all:true 清全部,或 batch:"<批次名>" 删一批),必须 confirm:true。不可恢复 |
删除的三条安全底线
| 底线 | 行为 |
|---|---|
| 默认是移动而不是粉碎 | 进 $DSH_HOME/session-trash/<批次>/,MANIFEST.json 记着每条对话的原始位置,conversation_restore 可原样搬回 |
| 必须显式确认 | 少了 confirm:true 一律拒绝,原目录纹丝不动 |
| 拒绝删除正在运行的对话 | 本进程存活的会话会被列进 refusedLive 而不是删掉(也意味着不可能删掉你正在说话的那个) |
| 清空回收站是独立的、不可恢复的一步 | conversation_purge 同样要求 confirm:true,只接受回收站里实际存在的批次名(并对最终路径做包含性校验,不会被 .. 带出回收站) |
回收站与 sessions 同级、同卷,所以移动是同卷 rename:要么完整、要么没动。
回收站怎么清空:面板底部有「清空回收站」(同样以"复制指令"方式交付),
或直接对模型说「清空回收站」。conversation_purge 清完之后,连空的回收站目录也会一并收掉;
恢复完一个批次后,那个只剩清单的空壳批次也会自动消失,不会在面板里显示成一个"空批次"。
被删对话涉及的两处磁盘位置:
| 位置 | 内容 |
|---|---|
$DSH_HOME/sessions/<工作区>/<会话id>/session.v4.jsonl.zstd |
对话正文 |
$DSH_HOME/storages/session_projcache/sessions/<会话id>.json |
标题、cwd、创建时间 |
配置
config 全部有默认值,不传也能用:
| 键 | 默认 | 说明 |
|---|---|---|
maxTracked |
500 |
索引最多保留多少条记录 |
titleMax |
80 |
自动标题的最大字符数 |
persistPath |
''(关闭) |
非空时把索引快照写入该文件,重启后仍能看到历史对话元数据 |
saveDebounceMs |
1000 |
写盘防抖间隔 |
trashMirror |
true |
是否把回收站概览写进各存活会话的工作区(<工作区>/.dsh-conversation-manager/trash.json),供面板显示。镜像只含批次名/时间/条数/大小,不含正文或标题;设为 false 则不往你的工程目录写任何文件(连下面的状态快照一起停) |
statusFile |
true |
是否把宿主状态快照(<工作区>/.dsh-conversation-manager/status.json)写进各存活会话的工作区:只有插件版本、补丁装在哪些对象上、扫描计数,不含任何对话内容或你的输入。想知道"中文兜底到底装上没有"就看它 |
cjkSearch |
true |
中文(中日韩)查询的字面兜底:查询含 CJK 文字时额外扫一遍磁盘,把命中的会话合并进官方结果——官方放大镜与本插件面板同时受益。设为 false 则完全交还给官方索引(英文查询本来就不触发) |
cjkSearchBudgetMs |
2000 |
兜底扫描的时间预算(毫秒)。扫描是同步读日志,预算越大、中文查询时可能顿得越久 |
cjkSearchMaxConversations |
120 |
兜底最多读几个对话(按最近活动倒序,先搜最可能相关的) |
- insert:
- id: conversation-manager
name: 'dsh-conversation-manager'
config:
trashMirror: true
目录结构
index.js 宿主插件入口(会话索引 + 事件追踪 + 工具注册 + 服务)
lib/
conversations.js 对话索引:登记、增量计数、检索、统计、快照
events.js 会话事件 → 消息投影(含多帧 zstd 日志读取)
tools.js 12 个 ToolDefinition
cleanup.js 磁盘层面的列出 / 回收站 / 恢复(纯函数)
trash-mirror.js 把回收站概览写进会话工作区(供 Web 面板显示)
host-status.js 把"补丁装在哪些对象上"写进会话工作区(验收凭证)
log-read.js 按文件读会话日志(多帧 zstd 逐帧解压)
search-disk.js 磁盘字面子串扫描(工具那条路)
search-cjk.js 中文查询兜底:包在宿主 `sessionController.search` 上(0.5.1)
src/client/ 浏览器半(TypeScript + React)
index.tsx 页签类型、三个 slot 注册、inject face
manager.tsx 总表面板
locale.ts 中英字典
styles.ts 主题自适应样式
dist/client.js 浏览器半产物(**提交进仓库**,所以安装时无需构建)
scripts/ 产物契约校验与冒烟(verify-client / smoke-client)
test/smoke.mjs 宿主半冒烟(伪造 Cordis 上下文,无需 DSH)
docs/ENGINEERING-NOTES.md 工程笔记:契约核对、实测数据、踩过的坑
开发与验证
不需要 DSH 也能跑测试:
pnpm install
pnpm run check # 语法检查
pnpm test # 宿主半冒烟:87 项
pnpm run bundle # 构建浏览器半(改了 src/client/ 才需要)
pnpm run check:client # 产物契约 + 纯度门 + 冒烟:66 项
pnpm run check:dist # 重新构建后检查产物与提交的一致(CI 会跑)
其中三条值得一提的断言:
- 纯度门:浏览器半的产物只能依赖平台模块表里的东西(
react、react/jsx-runtime、@deepseek-ai/dsh-client-ui-*等);任何其它@deepseek-ai/*的运行时值导入都会 让构建失败(类型导入请用import type)。 - 跨两半一致性:回收站镜像的路径常量在宿主半与浏览器半各有一份(两半各自打包, 无法共享模块),测试会断言宿主声明的路径确实出现在客户端产物里。
- 产物不许漂移:
dist/client.js是入库的构建物,pnpm run check:dist会重新构建一次 并要求工作区无差异——改了源码忘了重新构建,CI 就会失败。
已知限制
- 面板里没有"一步到位"的删除按钮(见上文"关于删除")。要做成一步按钮,需要给插件加一个 自己的 Typert Remote 命名空间,代价大于收益 —— 这属于可选的后续升级。
- 回收站一览依赖宿主写在工作区里的镜像文件;把
trashMirror设为false后,面板会显示 "读不到镜像",此时用conversation_list_all/conversation_restore工具照常工作。 - 删除后原生列表需要一次「刷新」/重连/重启才会少掉那些行(原因见上文)。
- 中文搜索兜底是尽力而为的:受
cjkSearchBudgetMs(默认 2 秒)与cjkSearchMaxConversations(默认 120 个)限制,超预算只返回已扫到的; 它补的是"搜得到",不承诺"排序最相关"(官方结果仍在最前面)。
常见问题
怎么确认「中文搜索兜底」真的装上了(而不是又一版空转)?
看任意存活会话工作区里的 .dsh-conversation-manager/status.json:
{ "version": "0.5.1", "cjkSearch": { "installed": true,
"targets": { "sessionController": "installed", "sessions": "installed" }, ... } }
targets.sessionController 必须是 "installed" —— 那才是网关真正调用的入口
(0.5.0 包在 sessions 上,targets 会如实写 search-unavailable,功能等于没有)。
同一份数据也能在对话里让模型调 conversation_selftest 看到。
文件里只有版本与计数,没有你的对话内容。
Windows 上安装报"没有写入权限" / EPERM: operation not permitted, symlink ...?
这不是文件权限坏了,而是 Windows 默认不允许普通用户创建符号链接(只有管理员、或开启了 「开发者模式」才行)。pnpm 在导入包时偶尔需要建一个真正的符号链接(跨盘时不能用目录联接 代替),就会被系统拒绝,DSH 把它归类为"permission"失败。
两种解法:
- 开启开发者模式:Windows 设置 → 系统 → 开发者选项 → 打开「开发人员模式」。 之后重新安装即可(这也是 Node/pnpm 在 Windows 上这类 EPERM 的标准解法)。
- 改用本地目录安装(不经过包导入):把仓库克隆到本地,在插件页里填目录的绝对路径
(
link:形式,用目录联接实现,不需要符号链接权限)。本项目就是这么在自己机器上装的。
顺带一提:如果链接目标所在仓库里存在 node_modules(例如开发者刚跑过 pnpm install),
这个导入步骤更容易踩到它——消费者从 git 安装时不存在这个问题。
另外两个实测踩到的坑(Windows 上从 git 安装时)
- 不要用
git+https://…这种带前缀的写法:pnpm 会把 GitHub 的git+https地址规范化成 ssh (git+ssh://git@github.com/…),没配 SSH 密钥就会失败(git ls-remote ... exit 128)。 用不带前缀的普通 https 地址即可:https://github.com/Sky-lll27/DSH-chat-keeper.git - 已经用本地目录装过、想改成 git 版时,先卸载再装:本地目录安装是
link:(Windows 上是目录联接 junction),pnpm 试图"原地改名替换"会被系统拒绝 (EPERM ... rename ... _tmp_… -> dsh-conversation-manager)。 先在插件页卸载(或pnpm remove掉那条依赖),再装 git 版。 顺手提示:拆目录联接要用rmdir(只删联接本身),不要用递归删除,否则会连带删掉目标目录里的文件。
关于作者与 AI 协作
本项目的需求、设计取舍、验收与发布由作者(Sky-lll27)决定;代码由作者与 AI 助手 (DSH 内的编码代理)协作写成:宿主半与浏览器半的实现、测试与文档均在 AI 协助下完成, 作者负责提出需求、在真机上逐项验证(实测数据见 docs/ENGINEERING-NOTES.md)并对外发布。
这一点在提交记录里用 Assisted-by: trailer 标出——这是开源界对 AI 参与的通行做法
(比 Co-authored-by: 更准确:AI 不是合作作者)。可参考
Flux 的 AI 贡献政策 与
ASF 生成式工具指引的落地讨论。
需要说明的是:GitHub 不会为普通仓库自动添加"AI 生成"标记(那是它自家 Copilot 流程里的 机制),所以这类声明只能由作者自行写明。
No comments yet. Be the first to write one.