shuiqian520/dsh-session-delete

dsh-session-delete

在侧栏会话行的「…」菜单里永久删除单个会话;也可从侧栏标题旁的「⋯」菜单进入批量删除:在列表里勾选会话(运行中的不能删),再在确认弹窗里一次删掉 —— 会话日志、工作区归属、宿主内存实例、客户端列表行一并清理。同一个「⋯」菜单还收着会话搜索与视图选项;另可清理日志已不存在的残留行,用户消息与 AI 回复的复制按钮右侧各有「重试」。

미분류cordisdeepseek-harnessdelete

dsh-session-delete

English · 中文

DeepSeek Harness(DSH)插件:在侧栏会话行的「…」菜单里永久删除会话,并在用户消息与 AI 回复的复制按钮右侧加「重试」。

侧栏会话行「…」                用户消息 / AI 回复
  ├ 置顶                        [时间] [复制] [重试] [反馈] [分叉]
  ├ 重命名                            └── 本插件(复制按钮右侧)
  ├ 分叉
  ├ 归档
  └ 删除此会话  ← 本插件
  • 删除是真正的删除:会话日志目录、工作区归属、宿主内存实例、客户端列表行, 四处一起清,不留幽灵行、不进「未分组」、不需要重启。
  • 重试是把这条输入重新提交一次:AI 回复重试 = 重发它之前那条用户输入;用户消息重试 = 重发这条输入本身。DSH 的事件日志是 append-only,所以旧回复会留在上方,新回复追加在 下方(这不是替换式重生成,详见重试语义)。

界面

会话行「…」菜单永久删除确认
会话行菜单里的删除入口永久删除确认弹窗

截图同时通过 screenshots.json 提供给插件市场详情页;换图只要替换 assets/ 里的文件(想调顺序或增删就改 screenshots.json,最多 8 张)。

环境要求

项要求
DSH0.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 消失)。

批量删除会话(侧栏勾选 + 同款确认弹窗)

侧栏「工作区」标题行右侧、「添加工作区」按钮右边有一个 「⋯」 菜单,里面收着 搜索会话 / 视图选项 / 批量删除会话。

  1. 「⋯」→「批量删除会话」→ 进入批量选择模式:每个会话行 hover 时出现勾选框 (官方 sidebar.workspaces.session.row.action,和「归档 / 置顶」同一排);
  2. 勾选任意几行;此时点「⋯」菜单变成:全选可删除的 / 清除选择 / 删除选中的 N 个… / 退出批量选择;
  3. 点「删除选中的 N 个…」→ 弹出和单会话删除完全同款的确认弹窗 (同一个 Modal 结构:标题 + 一行说明「已选中 N 个会话」+ 一句不可恢复的警告 + [取消] [永久删除]);
  4. 确认后逐条删除,轻提示「已删除 N 个会话(,M 个未删除)」,列表自动刷新并退出批量模式。
  • 运行中的会话勾选框禁用(悬停提示),host 也会二次拒绝并把该条记为失败项,不碰它的产物。
  • 单次上限 200 个,重复 id 自动去重;「退出批量选择」或重新进入都会清空选择。
  • 「搜索会话 / 视图选项」不重写官方逻辑:点菜单项时先关掉我们的菜单,下一帧现查官方按钮 再点击(缓存引用会在 React 重渲染后失效),搜索照旧展开输入框、视图选项照旧弹官方 「分组 / 排序 / 归档筛选」菜单;官方这两个按钮只是移出视口,仍可被快捷键与官方逻辑使用。
  • 菜单里的危险项(批量删除 / 删除选中的 N 个)图标与文字统一红色。
  • 确认弹窗与单会话删除共用同一结构与样式,所以观感完全一致。

支撑它的 host 路由(只读清单 + 批量执行,没有新增持久状态):

方法路径说明
GET/api/session-delete/sessions?limit=<n>只读会话清单(「全选可删除的」用):{ sessionId, title, createdAt, cwd, live, running, artifactExists, deletable, reason? }
POST/api/session-delete/delete-many请求体 { sessionIds: [...], confirm: true } → { ok, total, deleted, failed, results: [{ sessionId, ok, mode? , error? }] }

重试

位置按钮行为
AI 回复复制按钮右侧的刷新图标取该回复之前最近一条用户输入,重新提问
用户消息复制按钮右侧的刷新图标重新提交这条输入本身

只重发文本:图片/文件附件不重发。点击后按钮短暂禁用,成功后轻提示 「已重新提交这条输入,模型将重新回答」。

删除到底删了什么

POST /api/session-delete/delete { sessionId, confirm: true } 依次做四件事 (任何一步失败都不静默吞掉,响应里逐项汇报):

  1. 删产物:由 sessionPersistence.locate(header) 定位日志文件,回收其父目录 (避免残留空壳目录)。Windows 上日志可能仍被写句柄占用而失败 → 先摘运行时实例、 等 300 ms 再重试;仍失败才降级回收区(quarantine),全失败才拒绝并提示「产物被占用」。
  2. 摘宿主内存实例:按官方自身的 detach 路径(SessionStore / AgentRegistry 的 公开 store Map + detachEntered(entry)),先 agent 后 session。官方由此发 session/disposed,不再以 live 优先返回该会话。
  3. 解绑工作区:逐个工作区 detachSession(sessionId)(账本 sessionIds 命中优先, cwd 回退匹配并短暂重试),失败不中断,响应附 detached: false 与提示。
  4. 清归档集合残留:避免归档面板以空壳行复活。

最后补发官方 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 }
GET/api/session-delete/sessions?limit=<n>会话清单(批量删除页的数据源),默认 300 条、上限 1000
POST/api/session-delete/delete-many请求体 { sessionIds, confirm: true },单次上限 200;逐条汇报结果
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 投递。

配置(可选)

字段默认说明
trashModepurgepurge = 直接永久删除;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。

发布检查清单

  1. 改 package.json 的 version,在 CHANGELOG.md 加一节;
  2. node --test test/smoke.mjs 全绿;
  3. npm pack 校验产物只含 files 列出的内容;
  4. 在 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>。

许可

MIT