dsh-session-delete
English · 中文
DeepSeek Harness(DSH)插件:在侧栏会话行的「…」菜单里永久删除会话,并在用户消息与 AI 回复的复制按钮右侧加「重试」。
侧栏会话行「…」 用户消息 / AI 回复
├ 置顶 [时间] [复制] [重试] [反馈] [分叉]
├ 重命名 └── 本插件(复制按钮右侧)
├ 分叉
├ 归档
└ 删除此会话 ← 本插件
- 删除是真正的删除:会话日志目录、工作区归属、宿主内存实例、客户端列表行, 四处一起清,不留幽灵行、不进「未分组」、不需要重启。
- 重试是把这条输入重新提交一次:AI 回复重试 = 重发它之前那条用户输入;用户消息重试 = 重发这条输入本身。DSH 的事件日志是 append-only,所以旧回复会留在上方,新回复追加在 下方(这不是替换式重生成,详见重试语义)。
环境要求
| 项 | 要求 |
|---|---|
| DSH | 0.2.x(Web UI / Desktop 均可;插件只依赖公开的 Host 服务与 Client 插槽) |
| Node | ≥ 22(仅开发/测试需要) |
| 依赖 | 无。不声明任何 @deepseek-ai/dsh-* peerDependencies |
安装
方式 A:插件管理器(推荐)
把本目录放到工作区(克隆仓库或解压产物包),让 DSH 执行一次
plugin_manager → install_bundle,target 为本目录的绝对路径:
install_bundle target: <本目录绝对路径>
它会把包装进当前 profile、把本插件自带的 cordis.patch.yml
(- insert: id: session-delete)注进 profile 根,并把 dsh-session-delete 加进
dsh.profile.bundles。启用 HMR 的 profile 当场生效;否则重启一次 DSH。
方式 B:从产物包安装(.tgz)
npm pack # 产出 dsh-session-delete-<版本>.tgz
把 .tgz 发给对方,解压后用方式 A 安装,或由 DSH 的插件页选择该文件。
方式 C:开发用 junction(改代码即生效)
$profile = "$env:USERPROFILE\.dsh\profiles\desktop"
New-Item -ItemType Junction -Path "$profile\node_modules\dsh-session-delete" -Target "<本目录绝对路径>"
# profile 的 package.json:dependencies 追加 "dsh-session-delete": "file:<本目录绝对路径>"
# dsh.profile.bundles 追加 "dsh-session-delete"
# 重启 DSH
host 半区的改动要重启 DSH 生效;client 半区(浏览器产物)改动要重启 + 刷新页面。
卸载
插件页停用/移除该组合包,重启即可;手动安装的删掉 junction、依赖行与 bundles 条目。 本插件不写任何持久状态(只读会话日志、删除时直接动产物目录),卸载后不留残余。
使用
删除会话
会话行右侧「…」→「删除此会话」→ 确认弹窗(红色确认按钮,文案说明不可恢复)。
- 只有非运行中的会话可删;运行中的菜单项置灰并标注原因,服务端也会二次拒绝。
- 产物已不存在的会话不再拒绝:走「清理列表」(
mode: ghost),把残留行彻底清掉。 - 失败/半成功都有顶部轻提示说明(约 3.2s 消失)。
重试
| 位置 | 按钮 | 行为 |
|---|---|---|
| AI 回复 | 复制按钮右侧的刷新图标 | 取该回复之前最近一条用户输入,重新提问 |
| 用户消息 | 复制按钮右侧的刷新图标 | 重新提交这条输入本身 |
只重发文本:图片/文件附件不重发。点击后按钮短暂禁用,成功后轻提示 「已重新提交这条输入,模型将重新回答」。
删除到底删了什么
POST /api/session-delete/delete { sessionId, confirm: true } 依次做四件事
(任何一步失败都不静默吞掉,响应里逐项汇报):
- 删产物:由
sessionPersistence.locate(header)定位日志文件,回收其父目录 (避免残留空壳目录)。Windows 上日志可能仍被写句柄占用而失败 → 先摘运行时实例、 等 300 ms 再重试;仍失败才降级回收区(quarantine),全失败才拒绝并提示「产物被占用」。 - 摘宿主内存实例:按官方自身的 detach 路径(
SessionStore/AgentRegistry的 公开storeMap +detachEntered(entry)),先 agent 后 session。官方由此发session/disposed,不再以 live 优先返回该会话。 - 解绑工作区:逐个工作区
detachSession(sessionId)(账本sessionIds命中优先, cwd 回退匹配并短暂重试),失败不中断,响应附detached: false与提示。 - 清归档集合残留:避免归档面板以空壳行复活。
最后补发官方 api-session/removed 事件——这是冷会话(没加载进内存、第 2 步没有实例
可摘)唯一能让客户端列表行立即消失的通知。响应里的 announced 字段即这一步的结果。
为什么必须补发那条事件
官方契约(见 docs/internals.md)是:
// Host:api-session-controller
ctx.on('session/disposed', (session) => { ctx.emit('api-session/removed', session.id); });
// Client:api-session-controller
ctx.remote.$on('api-session/removed', (sessionId) => sessions.handleSessionRemoved(sessionId));
detachEntered 会触发前者,但只对内存里活着的会话有效。冷会话删掉产物后没有任何
通知,客户端就会保留一行空壳:点开报 历史加载失败:session "…" not found(session/not-found)。
v0.3.0 起删除路径无论冷热都补发该事件。
失效行清理(sweep)
升级前遗留的空壳行、或手动删过日志目录的会话,可以用:
POST /api/session-delete/sweep # 无需请求体
→ { ok, scanned, cleaned: [{ sessionId, announced, disposed, detached }], kept: [...] }
它扫描「工作区账本(含归档集合)+ 宿主内存 store」里的全部会话 id,
只清理持久化里已经不存在的(判定唯一依据:产物目录不存在),任何日志还在的会话
一律进 kept,绝不误删。
路由
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/session-delete/status?sessionId=<id> |
删除资格:{ running, artifactExists, deletable, reason } |
| POST | /api/session-delete/delete |
请求体 { sessionId, confirm: true };成功 { ok, mode, detached, disposed, announced } |
| POST | /api/session-delete/sweep |
清理失效行(只清无产物的) |
| GET | /api/session-delete/retry-source?sessionId=<id>&messageId=<id> |
只读:解析该消息要重发的文本 { role, text, userMessageId, mode: 'queue' } |
mode 取值:purge(默认,直接永久删除)、recycle(系统回收站)、
quarantine(回收站不可用时的插件回收区降级)、ghost(产物已不存在,仅清理列表)。
重试没有 host 路由:sessionController.prompt 是 @Remote 方法(定义里
cancellation: { parameter: 'signal' }),插件直接调用会抛
Cannot read properties of undefined (reading 'throwIfAborted')。正确姿势是客户端会话绑定,
与输入框发送完全同一条路:
sessions.using(id, { source: 'workspaceOperation' },
(ref) => ref.binding.session.prompt([{ type: 'text', text }], 'queue'));
host 只提供只读的 retry-source 解析文本;retry-source 返回的 mode 字段用于告知客户端
按 queue 投递。
配置(可选)
| 字段 | 默认 | 说明 |
|---|---|---|
trashMode |
purge |
purge = 直接永久删除;recycle = 优先系统回收站,失败降级插件回收区 |
quarantineDir |
~/.dsh/trash/session-delete |
回收区降级时的暂存目录 |
- insert:
- id: session-delete
name: dsh-session-delete
config:
trashMode: purge # 或 recycle
quarantineDir: D:\dsh-trash
删掉或注释掉 config 即用默认值。本插件没有 Config 导出,配置不会被校验:
多写的字段被忽略,写错类型的字段退回默认值。
文案与本地化
- 界面文案走 Client
locale服务:插件在apply里用locale.register(NS, 'zh'|'en', dict)注册字典,再用locale.bind(NS)取翻译函数(调用时读当前语言)。语言切换后已渲染的 菜单项、弹窗、已注入的按钮title/aria-label都会跟着更新。 - locale 服务缺席、注册冲突或字典缺失时一律回退中文,本地化层不会成为功能故障点。
- 清单元数据(
meta.title/meta.description/ 图标)在locale/zh.json、locale/en.json、icon.svg,插件管理器读它们不需要激活插件。 - 服务端(host)返回的错误文案目前固定中文,属于已知取舍。
已知取舍
- 永久删除不可恢复:默认
trashMode: purge直接删会话产物目录,不进回收站。 需要可还原时设trashMode: recycle(还原后可用已装的@mzzsfy/dsh-session-manager面板「重新挂载」找回)。 - 为什么"删了还挂在未分组":官方
session.list以 live 优先——只要宿主内存里还有 该会话的Session/Agent实例(侧栏激活过的会话都是),列表仍会返回它;而工作区账本已被 摘掉,于是它落进「未分组」桶。所以删除必须连内存实例一起摘(见上文第 2 步)。 这些字段属于宿主内部结构:缺失时静默降级,绝不抛错。 - 运行中的会话由 host 权威拒绝;client 置灰只是提示(客户端的运行态可能略滞后)。
- 不做:批量删除、回收站还原台账、「下次不再提示」记忆(每次删除都要确认)。
- 与
@mzzsfy/dsh-session-manager的分工:对方只允许删除已归档会话,自带回收站 台账与重挂载;本插件面向「任意非运行中会话的一次性清理」。两者入口互不影响, 但指向同一份产物:先执行的那个生效。
开发
node --test test/smoke.mjs # 25 项测试
覆盖:运行中判定、status/delete/sweep 路由(方法闸、确认标记、未知会话、运行中拒绝、
purge 真删产物、摘内存实例、幽灵行清理、产物被占用后摘实例再重试、解绑失败半成功、
recycle/回收区降级与双失败拒绝、同 id 并发)、官方通知事件(api-session/removed)、
sweep 只清无产物行、client 半区加载面与三个注册项、locale 注册与回退。
测试用真实临时目录 + 注入的执行器与同形 store 桩,不触碰真实会话。
代码结构与官方契约依据见 docs/internals.md。
发布检查清单
- 改
package.json的version,在CHANGELOG.md加一节; node --test test/smoke.mjs全绿;npm pack校验产物只含files列出的内容;- 在 DSH 里重装/重启一次,确认菜单项与两个按钮仍在(见 docs/internals.md 的自检清单)。
分发须知
- 不声明任何
@deepseek-ai/dsh-*peerDependencies:插件的兼容判定只看这些 peer, 没有 peer 就意味着任何 DSH 版本都能装,不会被版本豁免机制拦下。 - 启动安全(重要):host 半区导出空
inject,webServer/sessionQuery/workspaceRegistry/agents全部在apply内用ctx.inject软注入。 任一宿主服务缺失或改名,本插件只是不注册路由(菜单项显示「无法读取会话状态」), 绝不会因服务未满足而变成 pending 把web boot拖垮——那正是硬注入插件的翻车方式。 - client 半区依赖面:只
require("react")与@deepseek-ai/dsh-client-ui-primitives(loader 的隐式 baseline external), 不额外依赖其它插件模块;共享状态用自带的极简快照 store,不引入dsh-client-store。 - 想上架 npm 集市:去掉
"private": true后npm publish --access public, 别人即可直接安装;或推 GitHub 后用install_bundle指向仓库目录 /dsh plugin add github:<user>/<repo>#<commit>。
No comments yet. Be the first to write one.