🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

会话删除 Baboo-ming/dsh-session-delete

DeepSeek Harnessv0.1.0
Baboo-ming16 天前有提交已提供中文文档扫描:含敏感能力

当前版本 0.2.20.2.1 → 0.2.2:工作区行菜单项去掉会话计数,固定显示「删除所有会话」;0.2.0 →…

31.5/100
综合分
2
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/18 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

自动兼容检查未通过:仓库 package.json 标记 private,未发布到 npm,需从源码安装。以下命令仍可尝试,但可能失败。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add Baboo-ming/dsh-session-delete
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · other
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 16 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装

仓库 package.json 标记 private,未发布到 npm,需从源码安装

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-session-delete @ 0.1.0
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/10 15:49:12

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 11 处,展示前 11 条)
  • 读取本地文件
    lib/host/runtime.js:109
    const text = await readFile(file, 'utf8')
  • 读取本地文件
    lib/host/runtime.js:120
    const info = await stat(file)
  • 写入本地文件
    lib/host/runtime.js:145
    await mkdir(dirname(file), { recursive: true })
  • 写入本地文件
    lib/host/runtime.js:147
    await writeFile(tmp, `${JSON.stringify(ledger, null, 2)}\n`, 'utf8')
  • 写入本地文件
    lib/host/runtime.js:148
    await rename(tmp, file)
  • 删除本地文件
    lib/host/runtime.js:152
    await rm(tmp, { force: true })
  • 读取本地文件
    lib/host/runtime.js:567
    if ((await lstat(dir)).isSymbolicLink()) {
  • 读取本地文件
    lib/host/runtime.js:569
    entry.target = await realpath(dir).catch(() => undefined)
  • 读取密钥环境变量
    submission-awesome-dsh-plugin/set-topic.mjs:7
    const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
  • 请求外部主机api.github.com
    submission-awesome-dsh-plugin/set-topic.mjs:16
    const res = await fetch(`https://api.github.com/repos/${repo}/topics`, {
  • 发起网络请求
    submission-awesome-dsh-plugin/set-topic.mjs:16
    const res = await fetch(`https://api.github.com/repos/${repo}/topics`, {
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 GitHub 仓库
涉及外部主机:api.github.com

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 侧栏会话删除
  • 穿透链接定位真实文件
  • 彻底删除磁盘数据

dsh 侧栏会话删除插件。普通用户能安全清理侧栏会话数据,可穿透 junction/符号链接定位真实文件并彻底删除,安装使用都简单。

README

由 DeepSeek 最新模型翻译生成
dsh-session-delete 当前版本 0.2.2(0.2.1 → 0.2.2:工作区行菜单项去掉会话计数,固定显示「删除所有会话」;0.2.0 → 0.2.1:工作区行菜单文案定为「删除所有会话」;0.1.2 → 0.2.0:新增「删除当前工作区的所有会话」——工作区行 … 菜单里的批量红色危险项、带计数的预览、后台任务与进度/失败明细,见 §1.1;0.1.1 → 0.1.2:会话数据搬到别的盘、原位置只留目录链接(junction/符号链接)时也能删干净,见 §8 第 19–20 条;0.1.0 → 0.1.1:修掉「宿主 dispose 写回投影缓存」的时序缺陷,见 §8 第 18 条)。 把「删除会话」做成 DSH 插件:侧栏会话行 … 菜单注入红色危险项 → 二次确认模态框 → 七阶段删除流水线 → 广播事件并刷新侧栏;同一套流水线也支持在工作区行 … 菜单里一次删掉该工作区的全部会话(逐个执行、后台任务、可关窗、失败逐条报告)。 删除是永久的:会话日志会从磁盘上消失,无法用「撤销」找回。 - 宿主半边(Node):lib/index.js + lib/host/ + lib/core/ —— 拥有文件系统与宿主服务,通过自建 HTTP 路由暴露能力。
展开完整文档(README 剩余 1 节)
dsh-session-delete

当前版本 0.2.2(0.2.1 → 0.2.2:工作区行菜单项去掉会话计数,固定显示「删除所有会话」;0.2.0 → 0.2.1:工作区行菜单文案定为「删除所有会话」;0.1.2 → 0.2.0:新增「删除当前工作区的所有会话」——工作区行 … 菜单里的批量红色危险项、带计数的预览、后台任务与进度/失败明细,见 §1.1;0.1.1 → 0.1.2:会话数据搬到别的盘、原位置只留目录链接(junction/符号链接)时也能删干净,见 §8 第 19–20 条;0.1.0 → 0.1.1:修掉「宿主 dispose 写回投影缓存」的时序缺陷,见 §8 第 18 条)。

把「删除会话」做成 DSH 插件:侧栏会话行 … 菜单注入红色危险项 → 二次确认模态框 → 七阶段删除流水线 → 广播事件并刷新侧栏;同一套流水线也支持在工作区行 … 菜单里一次删掉该工作区的全部会话(逐个执行、后台任务、可关窗、失败逐条报告)。
删除是永久的:会话日志会从磁盘上消失,无法用「撤销」找回。

- 宿主半边(Node):lib/index.js + lib/host/ + lib/core/ —— 拥有文件系统与宿主服务,通过自建 HTTP 路由暴露能力。
- 浏览器半边(预构建懒加载 CJS):lib/client.js —— 注入菜单项、确认模态、toast,并调用 sessions.refresh()。
- 状态:核心/宿主/客户端三层共 82 个测试全部通过(node --test "tests/.test.js",core 37 + host 33 + client 12;其中 5 个 junction/符号链接用例需要 Windows 符号链接权限,受限沙箱里会失败、与改动前 HEAD 表现一致,正常桌面环境全部通过);tools/doctor.mjs 在临时 DSH home 上跑通完整七阶段流水线,并由三次独立只读对抗式审计逐条复核需求、宿主 API 与绕过面(审计提出的 1 个 blocker + 3 个 major + 其余 minor/info 已全部处理,见 §8)。已在真实 DSH(desktop profile)里完成端到端验证:侧栏出现红色「删除会话」、确认后行当场消失、磁盘目录/挂账/投影缓存全部清掉(记录见 §7 末尾)。

1. 它做什么

1. 会话行的 … 菜单里出现 「删除会话」(danger: true,红色文字/图标/悬停底色,位置在归档之后,order = 500)。
2. 点击后立刻关闭菜单并打开确认模态框,模态框会先拉取一次预览(真实调用预览接口):
- 会话 cwd(取自持久化 header,不是当前工作目录);
- 将被删除的目录数与文件数、总占用;
- 命中的工作区挂账、是否在归档集合/固定集合中、是否会清理投影缓存;
- 会话是否运行中(命中 workspace/session-activity 时显示红色警告条,并在提供 force 时二次确认)。
- 「取消」按钮带 data-modal-autofocus,打开即聚焦,避免误按回车。
3. 确认后按顺序执行七阶段流水线,每个阶段独立 try/catch,任一阶段失败只降级继续,绝不让异常逃到宿主:

| # | key | 名称 | 做什么 | 拒绝/失败时 |
| 1 | validate | 校验 ID | 正则 ^[A-Za-z0-9][A-Za-z0-9._-]{0,190}$ 防目录穿越;会话必须“已知”(运行态存储或工作区挂账里存在) | gate:拒绝 ⇒ 整条流水线中止,不修改任何数据 |
| 2 | running-guard | 运行保护 | ctx.waterfall('workspace/session-activity', {sessionId}),非空数组 ⇒ 运行中 | gate:无 force 拒绝(HTTP 409);force: true 时先 registry.stopSessionActivity(id)(内部 ctx.parallel('workspace/session-stop', …)) |
| 3 | workspace-ledger | 工作区挂账移除 | 优先调用注册表实体原语 registry.get(workspaceId).detachSession(sessionId)(宿主自己会写 ISO updatedAt 并剪掉指向过期路径的成员);无实体句柄时退化为 table.update(id, …);服务整体缺失时退化为直接原子编辑 <storagesRoot>\workspace.json | 挂账存在但清理失败 ⇒ failed(否则刷新后行会“复活”);账本不存在 ⇒ skipped |
| 4 | archive-set | 归档集合清理 | 先调 registry.unarchiveSession(id) / registry.unpinSession(id)(幂等原语),再校验 requireState() 是否已不含该 id;注册表缺少原语时退回 setState() | 失败仅告警并降级 |
| 5 | disk-logs | 磁盘日志删除 | 扫描 <sessionsRoot> 定位该 id 的目录(-<编码后的 id>),整棵删除;只清理因此变空的 project 目录;投影缓存两条路都走:ctx.get('storageDomain').get('session_projcache').table('sessions').delete(id)(绝不 open(),否则抛 already-open)+ 直接删 <storagesRoot>\session_projcache\sessions\<id>.json(只删 domain 记录不够:domain 的下一次 flush 会把文件写回来) | 失败 ⇒ deleted=false(disk-logs 是唯一“磁盘关键”阶段);Windows 文件占用时记入 leftover,等第 6 阶段摘除句柄后重试 |
| 6 | memory | 内存摘除 | sessions.get(id) → sessions.flush(session)(释放 JSONL writer)→ liveEntryFor(session) → detachEntered(entry)(宿主没有按 id 删除的 API;detachEntered 会顺带触发 session/disposed,live 会话因此连带完成第 7 阶段);摘除后再清一次投影缓存——dispose 会给会话写最后一份 projection 快照,真实环境里这一步会把第 5 阶段刚删掉的缓存文件重新写出来(实测延迟 27 s,见 §8 第 18 条) | 非运行态/服务缺失 ⇒ skipped(靠第 5、7 阶段兜底);摘除后的缓存重清失败只记入 extra.cacheSweep,不把一次成功的摘除判成失败 |
| 7 | broadcast | 广播事件 | ctx.emit('api-session/removed', sessionId) —— 客户端 sessions store 收到后移除该行 | 广播失败 ⇒ failed(不再假装成功);第 5 阶段失败时本阶段主动 blocked:磁盘日志还在,隐藏行只会让用户以为删掉了,刷新/重启后又「复活」 |

4. 成功后 toast 提示,并调用 sessions.refresh() 刷新侧栏列表(该 API 在 dsh-client-ui-workspace 里是真实存在的:会重新拉取持久化会话列表)。
5. 若流水线在 gate 上被拒绝,前端展示「未删除任何数据」并保持会话不变。

返回的报告是结构化的(report.stages[] 含每阶段 status/ms/detail/error,以及 deleted/partial/aborted/refused/warnings),宿主日志里有逐阶段记录。

1.1 删除当前工作区的所有会话(0.2.0)

1. 工作区行的 … 菜单里出现 「删除所有会话」(复用菜单里删除工作区那行的危险样式)。
2. 点击后关闭菜单并打开确认模态框,先调用 POST /session-delete/workspace/preview 拉取预览:
- 工作区标题与路径、会话总数;
- 其中多少会话在运行中(有活动即视为运行中,确认按钮切换成 force 变体)、多少在归档集合中;
- 会被清理的日志文件总数与总占用(只统计磁盘上真实存在的会话目录)。
3. 确认后调用 POST /session-delete/workspace/delete 启动宿主端后台任务,立即拿到 {jobId, total};客户端不等待删除完成(单个会话的删除已可能超过 20 s 的请求超时,批量更不能同步等),而是每 500 ms 轮询 POST /session-delete/workspace/status:
- 运行中显示 已完成 / 总数 进度;关掉模态框不会中止任务,重新打开该工作区的对话框会接着显示进度;
- 完成后 toast 汇总(全部成功 / 部分失败),失败条目逐条列出 sessionId 与原因;
- 全部成功时关闭模态框并 sessions.refresh()。
4. 宿主按 record.sessionIds 顺序逐个执行同一套七阶段流水线(force 时先停掉运行中的活动);单个会话失败不会中断批次,任务状态最终为 done / partial / failed。
5. 实现说明:DSH 0.2.0-rc.2 的工作区行菜单是硬编码的 rename/delete 两项(dsh-client-ui-workspace/lib/client.js 的 workspaceMenuItems),没有插件槽位。本插件在点击工作区行按钮后短暂轮询新出现的 [role="menu"][data-menu-material],克隆其中最后一个危险项并改写标签/点击行为,不保留全局 MutationObserver;所有步骤都包在 try/catch 里,未来 DSH 改 DOM 时最多是这一项不再出现,不影响会话行菜单与其余功能。
6. 验证状态:批量删除的宿主半边(预览 / job / 轮询)与对话框全流程由自动化测试覆盖;菜单项本体是 DOM 增强,本机没有可操作的浏览器,尚未在真实 DSH GUI 里点过(会话行菜单与 0.1.x 全部功能已在真实 DSH 里验证)。升级后请按 §7 实测一次:没出现时先看 §9 排障表,也可用 openWorkspaceDelete(workspaceId) 或直接调 /session-delete/workspace/ 绕过 UI。

2. 目录结构

| 路径 | 角色 |
| package.json | dsh.client(platform: web、inject: [])+ dsh.bundle.patch + exports["./client"] |
| cordis.patch.yml | bundle patch:向 profile 插件名册插入 id: session-delete 行 |
| lib/index.js | Cordis 入口:name / inject = [] / apply(),装配 runtime + stages + HTTP handler + 批量删除 job 表(startWorkspaceDelete / workspaceJob) |
| lib/host/runtime.js | 宿主能力适配器:路径解析、会话头、运行态、挂账/归档集合读写、活动检查、内存摘除、广播、preview()、previewWorkspace()(工作区会话清单 + 磁盘统计) |
| lib/host/stages.js | 把 runtime 能力接成七个阶段函数(含 force 语义与磁盘重试) |
| lib/host/http.js | /session-delete/ 路由:{ok,value} / {ok,error} 信封、来源/令牌/JSON 准入、400/403/405/409/413/415 守卫,以及工作区预览 / 批量删除 / job 状态三个路由 |
| lib/core/paths.js | encodeSegment / session 目录名与 id 的互推、非法 id 判定 |
| lib/core/scan.js | 目录发现与统计(describeDir / removeSessionDirs / 投影缓存候选) |
| lib/core/ledger.js | workspace.json 的纯函数式读写改(含原子替换、校验 verdict、重复 id 全清) |
| lib/core/pipeline.js | 七阶段定义 STAGES、runDeletionPipeline、summarizeReport、describeError |
| lib/client.js | 浏览器半边(手写懒加载 CJS:window.__ModuleLoader__.load({id, factory})):会话行菜单项、两种确认模态、toast、工作区行菜单 DOM 增强、job 轮询器 |
| tests/core.test.js | 37 个纯逻辑用例(路径编码、扫描、账本、流水线语义) |
| tests/host.test.js | 宿主用例(真实临时 DSH home + stub 服务 + 伪 req/res + 阶段级 fake runtime,含「摘除后重扫投影缓存」的时序断言、工作区预览/批量删除/job 轮询) |
| tests/client-bundle.test.js | 12 个客户端契约/行为用例(自建 module loader 替身 + 自建 hook 运行时驱动真实渲染状态 + 参照 .ref/ 的槽位/样式/exports 交叉核对,含工作区批量删除的预览→确认→轮询→toast 全流程) |
| tests/helpers/client-contract.mjs | 槽位名/顺序常量,防契约漂移 |
| tools/doctor.mjs | 手工自检:构造临时 home 跑一次流水线(--live --registry --busy --force) |
| tools/asar.mjs | 解包/读取 DSH asar 安装体的辅助脚本 |
| docs/research/r1…r4 | 四份源码级研究报告:客户端注入点、宿主会话生命周期、客户端 store/refresh、打包与安装 |

3. 安装

dsh plugin add 会做两件事:用 pnpm 把包装进 desktop profile(<DSH_HOME>\profiles\desktop,本机 DSH_HOME = C:\Users\ming\.dsh),再把带 dsh.bundle.patch 的依赖名追加到 dsh.profile.bundles(包里的 cordis.patch.yml 已声明自己是 bundle 层)。

安装前退出 DSH 桌面端(CLI 写 profile 时会阻止 profile 启动);想留后路就先备份 profile 里的 package.json 与 pnpm-lock.yaml。

安装命令

dsh plugin --profile desktop add github:Baboo-ming/dsh-session-delete # 跟随 main 最新提交
dsh plugin --profile desktop add github:Baboo-ming/dsh-session-delete#v0.2.2 # 固定到 release tag(推荐)

- 走 GitHub 打好的 tarball(codeload.github.com),本机不需要装 git。本包没有运行期依赖(package.json 无 dependencies),装的就是这一个 tarball。已实测:github:Baboo-ming/dsh-session-delete 12 s 装完 0.1.2,pnpm-lock.yaml 把来源锁成提交 defac52。
- 不想开终端也可以:设置 → 插件 → 安装,粘贴 github:Baboo-ming/dsh-session-delete(同一套 spec 解析器)。
- 升级:换成新 tag 重跑一次(…#v0.2.2)。github: 依赖在 lockfile 里按提交锁定,不重跑、不换 tag 就一直是旧版本。
- 同一套 spec 还接受 git+https://github.com/…、完整仓库 URL、.tgz 地址、registry 名(dsh-session-delete@0.1.2,前提是发过 npm)。
- dsh plugin add 会自动把 dsh-session-delete 追加到 dsh.profile.bundles;不要再往 profile 自己的 cordis.patch.yml 里插一行同名插件:cordis.patch.yml(bundle patch)已经会插入,重复 id 会让加载器崩。

生效与验证

- 首次安装:本机实测宿主半边不需要重启——改 dsh.profile.bundles 会让 profile 实时重组并把插件挂上(/session-delete/health 立刻 200),客户端半边刷新页面(F5)即可看到菜单项。
- 要让运行中的进程加载改过的宿主代码(lib/):bundle 基础层把 hmr.root 设为 [](模块根是 opt-in,见 @deepseek-ai/dsh-base 的 patch:# Profile configuration reloads by default; module roots are opt-in.),所以默认必须完整重启 DSH 才会加载新代码。想改成热更新,就在 profile 的 cordis.patch.yml 里加一行(把 root 换成你的源码目录;本机已按此配置、备份见 cordis.patch.yml.bak-hmr-root-20261009):

- id: hmr
name: "@deepseek-ai/dsh-hmr"
config:
root:
- "D:/AI_Work/开发/删除会话插件/lib"

之后 dsh-hmr 会监视 lib/:改动任一模块(含 mtime 变化)都会清掉该模块图的 ESM/CJS 缓存、重新 import 入口,并把新插件热替换进同一个 entry(token 会轮换,客户端遇到 403 invalid-token 会自动重新取一次)。本机验证方式:把 lib/index.js 的 version 提到新值(如 0.2.0)并 touch lib/*/.js,约 3 秒后 /session-delete/health 就返回新版本号。改 lib/client.js 仍由 dsh-client-hmr(500 ms 轮询)热替换,或直接刷新页面。
- 验证宿主已挂载:

Invoke-RestMethod http://127.0.0.1:19387/session-delete/health
-> { ok: true, value: { name: 'dsh-session-delete', version: '0.2.2', token: '<uuid>', requireToken: true } }

version 同时是「运行中的进程加载的是哪一代代码」的判据。

- 然后在侧栏任意会话行点 … 找到「删除会话」。

发布与发版(维护者)

仓库:<https://github.com/Baboo-ming/dsh-session-delete>(MIT)。可公开的只有 package.json、cordis.patch.yml、lib/、tools/、tests/、docs/、README.md、LICENSE、.gitignore(共 24 个文件)。

不要提交 .ref/——那是从 resources/app.asar 解包出来的 DSH 应用源码(本机 11,470 个文件 / 112.5 MB,属第三方代码),.gitignore 已排除。docs/research/r1–r4.md 是本项目自己写的研究笔记,但内含 DSH 内部源码的 file:line 引用与片段,公开前请自行过一遍(不想公开就把它加进 .gitignore)。本目录也没有 node_modules——依赖装在 profile 的 node_modules 里。

发版流程(每次改完把 package.json 与 lib/index.js 的 version 一起升):

git add -A
git commit -m "dsh-session-delete 0.2.2"
git push origin main
git tag -a v0.2.2 -m "dsh-session-delete 0.2.2"
git push origin v0.2.2

推完 tag,用户就能用 github:Baboo-ming/dsh-session-delete#v0.2.2 安装或升级到这一版(不打 tag 也能装,但锁的是某个提交,不好核对版本)。

package.json 里的 "private": true 只挡 npm publish,不影响 github: / link: 安装;哪天想同时发 npm,把它改成 false,用户就能 dsh plugin --profile desktop add dsh-session-delete@0.2.2。

4. 卸载与回滚

dsh plugin --profile desktop remove dsh-session-delete

手动回滚:

1. 从 dsh.profile.bundles 里删掉 "dsh-session-delete";
2. & $NODE $PNPM remove dsh-session-delete(或直接恢复备份的 package.json / pnpm-lock.yaml 并 pnpm install);
3. 重启桌面端。卸载只删 profile 里那份副本,不动任何会话数据;本项目目录可整体删除。

5. 配置

在 cordis.patch.yml 的 insert 行加 config,或在 profile patch 里覆盖:

- insert:
- id: session-delete
name: 'dsh-session-delete'
config:
allowForce: false # 彻底禁止“强制删除运行中的会话” sessionsRoot: 'D:\dsh-data\sessions' storagesRoot: 'D:\dsh-data\storages' ledgerFile: 'D:\dsh-data\storages\workspace.json' dshHome: 'D:\dsh-data'

| 键 | 默认值 | 说明 |
| dshHome | config.dshHome → $env:DSH_HOME → ~\.dsh | 所有路径的基准 |
| sessionsRoot | <dshHome>\sessions | 会话日志根目录 |
| storagesRoot | <dshHome>\storages | 存储域根目录 |
| ledgerFile | <storagesRoot>\workspace.json | 工作区挂账账本 |
| allowForce | true | false 时 force 被忽略(HTTP 403),运行中的会话一律拒删 |
| requireToken | true | false 时不校验 X-Session-Delete-Token(仅本机脚本场景;来源校验仍然生效) |

6. HTTP API(宿主半边)

前缀 /session-delete。DSH 的 webServer 自身不做鉴权(官方 /api 走 connection.admit,且 dsh-host-webserver 里既无 origin 校验也可能绑 0.0.0.0),所以本插件自带两层准入,除 /health 外每一条路由都会经过:
1. 来源校验:sec-fetch-site trim 后为 cross-site ⇒ 403 cross-site-blocked;Origin: null(不透明源:沙箱 iframe、data: 页面)⇒ 403 cross-origin-blocked;Origin 的 host 与 Host 大小写不敏感比较不一致 ⇒ 403 cross-origin-blocked;带 X-Forwarded-Proto 时还会比较协议(https 页面不等于 http API)。局限(务必知道):这一层只约束浏览器——Origin/Host 都是请求自带的,能伪造请求头的非浏览器调用方可以两边一起伪造。唯一真正的凭证是下面的令牌;想在没有令牌的情况下防住简单请求注入,就该保持 requireToken: true(默认)。
2. 令牌:除 GET /session-delete/health 外的所有路由(含预览)都必须带随进程启动生成的一次性令牌 X-Session-Delete-Token(否则 403 invalid-token,比较用 crypto.timingSafeEqual)。令牌只能通过同源 GET /session-delete/health 读到(响应不带 CORS 头,跨站脚本读不到),因此跨站请求连预览都发不出去。
3. 状态变更请求(POST):Content-Type 必须严格是 application/json(可带参数,如 ; charset=utf-8),否则 415 unsupported-media-type——application/jsonp、text/plain 这类浏览器无需 CORS 预检就能发出的类型一律拒绝。

| 方法 | 路径 | 请求 | 响应 |
| GET | /session-delete/health | — | {ok:true,value:{name,version,token,requireToken}}(不回显任何路径) |
| POST | /session-delete/preview | {sessionId}(也支持 ?sessionId=) | {ok:true,value:{valid,known,live,cwd,activity,dirs,cwds,totalBytes,totalFiles,cache,workspaces,archived,pinned,registryAvailable,forceAllowed}} |
| POST | /session-delete/delete(别名 /session-delete,尾斜杠会先被规范化) | {sessionId, force?} | {ok:report.deleted, value:report};被 gate 拒绝 ⇒ 409 |
| POST | /session-delete/workspace/preview | {workspaceId} | {ok:true,value:{valid,workspaceId,title,path,source,count,runningCount,archivedCount,pinnedCount,totalBytes,totalFiles,forceAllowed,sessions:[…]}};不存在 ⇒ 404 |
| POST | /session-delete/workspace/delete | {workspaceId, force?} | {ok:true,value:{jobId,workspaceId,total}};工作区没有会话 ⇒ 409 |
| POST | /session-delete/workspace/status | {jobId} | {ok:true,value:{jobId,workspaceId,state,total,completed,deleted,failed:[{sessionId,refused,message}],current,error}};state 为 running/done/partial/failed,未知 job ⇒ 404 |

错误信封统一为 {ok:false,error:{code,message}}:

| code | 状态 | 触发 |
| cross-site-blocked / cross-origin-blocked | 403 | 跨站来源 / 跨源或 Origin: null / host 或协议不匹配 |
| invalid-token | 403 | 除 /health 外的路由缺少令牌或令牌不对 |
| invalid-session-id | 400 | id 缺失、非法或含穿越字符 |
| invalid-workspace | 400 | 工作区 id 缺失或非法 |
| unknown-workspace | 404 | 工作区已不存在(注册表与 workspace.json 都没有) |
| empty-workspace | 409 | 该工作区当前没有可删除的会话 |
| unknown-job | 404 | 批量删除 jobId 不存在或已过期回收 |
| force-not-allowed | 403 | force: true 但配置 allowForce: false |
| method-not-allowed | 405 | delete 收到非 POST |
| unsupported-media-type | 415 | POST 不是 application/json |
| body-too-large | 413 | 请求体 > 256 KB:先回 413 再排空(req.resume(),不 destroy)余量并 Connection: close;destroy 会让仍在发送的客户端只看到 ECONNRESET 而读不到 413 |
| bad-url / invalid-json | 400 | URL 无法解析 / 请求体不是合法 JSON(调用方的错,不是服务端故障) |
| internal-error | 500 | 其他异常(详情进宿主日志) |
想彻底关掉令牌检查(仅限本机自用脚本):配置 requireToken: false,此时 health 里 requireToken: false 且不带 token 字段。来源校验无法关闭。

7. 测试与自检

打包运行时的 node(本机实测路径;DSH 安装体内的 primary-runtime 同样可用) $NODE = "C:\Users\ming\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe" & $NODE --test "tests/.test.js" # 82 个用例(core 37 + host 33 + client 12);无符号链接权限时 5 个 junction 用例失败 & $NODE tools/doctor.mjs --live --busy --force # 临时 home 上跑一遍完整流水线并打印报告

tools/doctor.mjs 会在系统临时目录里造一个假 DSH home(会话日志 + 工作区账本),按参数勾选「运行态服务 / 真正 detach / 运行中会话 / force」,打印每阶段 status/ms/detail,用来在真实宿主之外复现宿主行为;它永不触碰真实 ~/.dsh。

tests/client-bundle.test.js 除了用自己的 window.__ModuleLoader__ 替身跑 lib/client.js,还会交叉核对 .ref/ 里的真实客户端源码:槽位 sidebar.workspaces.session.menu.item 仍以 kind: "list" / scope: "root" 声明、ownerProps 仍含 sessionId/displayTitle、仍注入 useMenuOpenState、MenuItemButton 的 danger 仍由 --dsw-alias-state-error-primary 上色、consumed primitives 仍是真实导出。.ref/ 不存在时该用例自动 skip(t.skip)。

客户端行为用例使用一个自建的最小 hook 运行时(createHookRuntime():跨渲染保留 state/ref、执行 effect、state 未收敛就继续重渲染),因此断言的是用户真实看到的按钮状态——预览进行中确认键 disabled、预览返回后启用、运行中会话变「强制停止并删除」并先出红色告警、删除进行中确认键锁死而取消键仍可点、成功后才 toast + sessions.refresh() + 关窗、宿主 409 拒绝时保留模态并渲染「失败详情」(失败阶段与残留路径),以及重试自动带上 force: true。

真实 DSH 端到端验证(本机 desktop profile,已完成)

安装形态(本机开发机):C:\Users\ming\.dsh\profiles\desktop\package.json 里以 link:D:/AI_Work/开发/删除会话插件 依赖本目录(node_modules\dsh-session-delete 是 junction),dsh.profile.bundles 末尾追加 dsh-session-delete;回滚备份为同目录下 package.json.bak-session-delete-20261009104827 与 pnpm-lock.yaml.bak-session-delete-20261009104827。

发布形态(普通用户):github:Baboo-ming/dsh-session-delete(及 …#v0.1.2)已用 profile 同一份 pnpm 在临时目录实测——12 s 装完 0.1.2,lockfile 把来源锁成 https://codeload.github.com/Baboo-ming/dsh-session-delete/tar.gz/defac52…;tarball 直取,本机无需 git。

| 验证项 | 实测结果 |
| 宿主半边是否需重启 | 不需要:改 bundles 触发 profile 实时重组,GET http://127.0.0.1:19387/session-delete/health → 200 {name:'dsh-session-delete',version:'0.1.0',token:'<uuid>',requireToken:true}(加 hmr.root 后热更新到 0.1.1,再 0.1.2,见 §3) |
| 客户端半边 | 刷新页面(F5)后,会话行 … 菜单里出现红色「删除会话」 |
| 预览接口读真实数据 | 对一个真实会话(1 个 session.v4.jsonl.zstd、27,561 B)返回 dirs(generation: v4、locked: false)、cwds、workspaces(workspaceId 38b23fb6-…、title「删除会话插件」)、registryAvailable/forceAllowed: true;对正在跑 turn + pwsh 任务的会话返回 activity: [{kind:'turn'},{kind:'job',items:[…]}] ⇒ 活动探测真实可用 |
| 界面删除(用户操作) | 对新建的测试会话 Greeting(session-92b15655-d3a4-46b4-8c89-9c58fb044938)点「删除会话」→ 确认:行当场消失,右下角出现绿色「会话已删除」 |
| 会话目录 | sessions\--D-AI_Work-~5F00~53D1-~5220~9664~4F1A~8BDD~63D2~4EF6--\session-92b15655-…(1 file / 27,561 B)→ 确认后 27.1 s 内实测消失,之后不再出现 |
| 工作区挂账 | workspace.json:tables.workspaces['38b23fb6-…'].sessionIds 由 ['session-92b15655-…','session-e5594e8a-…'] → ['session-e5594e8a-…'],整个文件已不含该 id;global.archivedSessionIds 未被波及;updatedAt 为 ISO 字符串 |
| 投影缓存 | 旧代码在此暴露一个真缺陷:摘除后宿主又写回了缓存文件(10:59:00,比删除晚约 27 s,里面还带着 Greeting 标题)⇒ 已修(第 6 阶段摘除后重扫,见 §8 第 18 条);遗留的孤儿文件已手工清除,其后 12 s 未被重写 |
| 会话数据搬到别的盘(目录链接) | 0.1.2 用 mklink /J 造出 6 种布局逐一实测:home 整体 junction、sessions 根 junction、项目目录 junction、会话目录 junction(目标名 = 会话 ID)以及会话目录 junction(目标名任意,走「只删会话文件」的保守路径)⇒ 六种全部 deleted: true,链接与其真实负载一并清除,工作区挂账与投影缓存清空;只有「链接指向一个不含任何会话文件的无关目录」时如实 failed(磁盘日志删除:1 项失败(首项:目录链接的目标不是会话存储…))且第 7 阶段 blocked——行不消失、无关目录分毫未动(见 §8 第 19–20 条) |

说明:DSH 自己的 /、/api/boot 走 connection.admit(未带凭据一律 401),所以「页面真的渲染出菜单、点确认真的删掉」只能由浏览器侧确认——上表第 2、4 行就是浏览器侧的人工确认结果。

8. 设计取舍与已知限制

1. 没有“按 id 删除会话”的宿主 API:sessions 只暴露 get / liveEntryFor / detachEntered。内存阶段因此走 detachEntered,对运行态会话有效;对非运行态(进程重启后)会话该阶段为 skipped,行消失依赖第 7 阶段广播 + 第 5 阶段删盘后 session-query-sqlite 的自愈对账。
2. Windows 文件锁:宿主可能仍持有 session.v4.jsonl 句柄,所以磁盘阶段先删、删不掉的记入 leftover,等第 6 阶段摘除内存后再重试一次,并把最终残留路径写进报告。
3. 目录定位靠扫描,不靠 cwd 反推:会话目录里的 slug 是创建时的 cwd 快照,之后切换工作目录会与当前 cwd 不一致(本项目会话自己就是这种情况)。扫描限定深度并逐个校验目录名后缀,避免误删。
4. 拒绝删除“未知会话”:会话来路不明时宁可拒绝(gate 中止),也不去赌一个可能拼错的 id。
5. 客户端不蹭 ui-workspace 的私有 toast/store:那两样都没导出,插件自建 shell.overlay 槽、自建快照 store、自建 CSS。
6. 红色确认按钮:UI primitives 的 Button 只有 primary/ghost/outline/toolbar,所以用 variant: 'outline' + 自带类名 .session-delete-confirm(颜色取 var(--dsw-alias-state-error-primary))。
7. 自建 HTTP 路由而不是 typert:typert 需要严格 codec 与生成器产物,成本远高于一次同源 fetch;插件只在浏览器半边调用自家路由。
8. 不修改宿主快照/索引表:SQLite 会话索引由宿主自愈对账,插件只删日志与挂账,避免和宿主抢写入。不删附件:dsh-attachment-local 是内容寻址且跨会话共享的,删了会连累别的会话。
9. inject = []:三个宿主能力(sessions / workspaceRegistry / webServer)都按需查找,缺失就降级为 skipped,插件永远不会拖垮宿主启动。
10. 自带请求准入(审计后加固):审计指出宿主 webServer 自身不做鉴权,而插件路由能永久删数据,因此加了「来源校验 + JSON 强制 + 一次性令牌」三层(见 §6);令牌只走同源 fetch,跨站脚本因无 CORS 头读不到,比较用 crypto.timingSafeEqual。客户端同时加了 20 s AbortController 超时(取令牌 8 s)与同步的在飞守卫(react.useRef),宿主无响应或在飞时不能重复提交;令牌轮换(DSH 重启)会触发一次自动重取重试;deleting 期间允许关闭模态(不会留下打不开的弹窗)。
11. 账本文件兜底做丢更新检测 + 进程内串行:服务不可用时直接改 workspace.json 有覆盖风险,所以走「stat → 读 → 改 → 再 stat → 一致才 rename」的乐观重试(最多 3 次),仍冲突就如实报 ledger-contended,不假装成功。同一进程内的并发删除会把这段 read-modify-write 串成一条 promise 链(第二次审计实测:不串行 + 固定临时文件名会让两个写入互相 rename 掉对方的 tmp,把 workspace.json 写成非法 JSON),临时文件名也改成“pid + 序号 + 随机后缀”并对失败路径做 rm 清理。
12. 失败详情可见:阶段级 detail/error/leftover 会渲染在模态的「失败详情」区(此前只显示通用 HTTP 错误),409 也会把 report 一并带回;返回值型失败(status: 'failed')同样写进 warnings,避免降级跑完却看起来一切正常。
13. 运行中会话默认仍需 force:UI 在预览里明确写出「该会话正在运行(N 项活动)」,确认按钮变成「强制停止并删除」才带 force: true;allowForce: false 时该按钮不可用。
14. 磁盘没清干净就不广播:api-session/removed 是客户端隐藏该行的唯一信号,若第 5 阶段失败(Windows 文件占用)仍广播,就会得到「侧栏行没了但日志还在、重启复活」的状态。因此第 7 阶段此时返回 blocked 并在模态里列出残留路径;只有第 6 阶段摘除句柄后重试成功,才继续广播。广播本身失败也会如实标 failed(ctx.emit 不可用时不会假装成功)。
15. 重复挂账一并清除:workspace.json 里同一 id 出现两次时,第 3 阶段会删掉全部出现(旧实现只 splice 一处,会留下幽灵挂账并在刷新后复活)。
16. 插槽注册的卸载幂等:slots.inject 返回的 disposer 与插件自身 scope 可能都会触发卸载,客户端因此用一次性标志 + 逐个 try/catch 调用,避免「已释放」异常逃出卸载路径。
17. 客户端测试的边界:tests/client-bundle.test.js 用自建 hook 运行时驱动真实渲染状态,但它仍不是浏览器——没有 DOM、没有真实 Modal 焦点管理/Escape、没有真实 fetch 与 React 调度,因此「菜单项真的出现在侧栏、弹窗外观、sessions.refresh() 真的刷新了列表」只能在真实 DSH 进程 + 浏览器里验证。已在真实 DSH 里做过一轮(见 §7 末尾),但每次改动客户端后仍建议复验一次。
18. 投影缓存必须在内存摘除之后重扫一次:session_projcache 的 domain 记录删掉不等于文件消失——宿主在会话 dispose 时会再写一份最终 projection 快照。真实环境实测:删除成功约 27 s 后,<storagesRoot>\session_projcache\sessions\<id>.json 又被写回(内容里还带着会话标题),而 workspaceRegistry 的 header 缓存因此继续报 sessionKnown(id) === true(dsh-workspace/lib/index.js:612-617 的缓存 headers 从不按 id 逐条清理)。所以第 5 阶段改成「domain 记录 + 文件」两条路都删,第 6 阶段摘除后再扫一遍,兜住这次写回。
19. 会话目录是目录链接时,必须连链接目标一起删:用户把会话数据搬到 D:/E: 盘、在原来的位置只留 mklink /J(junction)是很自然的做法,而 fs.rm(dir, { recursive: true }) 遇到 reparse point 只解开链接、不删目标——实测(Windows mklink /J)会留下全部日志文件,旧行为却报告成功:一次「删掉了」的假象。0.1.2 因此先 lstat 判链接、realpath 解析目标,再按目标名分两种策略执行:目标名 == 会话 ID(encodeSegment(sessionId),宿主自己的布局)→ 递归删目标;目标名任意 → 只删目标目录里的会话文件(session.jsonl / session.v(N).jsonl + 可选 .zstd / .tmp、session.lock),随后 rmdir 目标(只在变空时成功),其余文件原样保留并写进报告 leftover;链接指向的目录里一个会话文件都没有(= 无关目录或恶意链接)→ 直接拒绝,不递归。链接始终在负载之后才删:负载被占用时链接仍在,第 6 阶段的 leftover 重试才有路径可打。
20. session.lock 是真名,别猜成 <日志>.lock:dsh-session-persistence-jsonl/lib/index.js:643 的 LEASE_FILENAME = "session.lock"(:667 拼成 join(dir, LEASE_FILENAME)),所以预览里的 locked 只认 session.lock / session.v(N).lock 两种名字;session.v4.jsonl.lock 这种臆造名既不会被判锁、也不会被第 5 阶段顺手带走(它不在会话文件白名单里)。同时日志名补齐了第 0 代:sessionFormatLogFilename(0) 返回 session.jsonl(无 .vN),SESSION_LOG_PATTERN 已放宽成 /^session(\.v\d+)?\.jsonl(\.zstd)?$/。失败详情也不再打印 [object Object](removeSessionDirs 的失败项里 error 是字符串、服务失败是 Error 对象,展示层两种都要认)。
21. 工作区行菜单只能 DOM 增强,批量删除必须做成宿主 job(0.2.0):工作区行菜单由 dsh-client-ui-workspace/lib/client.js:1270-1340 的硬编码 workspaceMenuItems 渲染,DSH 0.2.0-rc.2 没有对应槽位(README 明确写工作区行菜单「visual-only except Rename/Delete」)。因此客户端在点击工作区行按钮后启动一个短生命周期 rAF 轮询,找到新挂载的 [role="menu"][data-menu-material],用 document.activeElement.closest('[data-row-key^="workspace:"]') + elementFromPoint 双重定位工作区,克隆菜单里最后一个危险项、改写标签与点击行为;不保留全局 MutationObserver,任一步失败只是少一个菜单项。批量删除同步 POST 会撞上客户端 20 s 超时,所以宿主用内存 job 表(startWorkspaceDelete/workspaceJob,完成后 30 min 回收、最多 50 个)逐会话跑七阶段流水线,客户端轮询进度;关窗不中止任务,重开对话框会续上进度。

9. 故障排查

| 现象 | 排查 |
| … 菜单里没有「删除会话」 | 客户端插件没加载:确认 dsh.profile.bundles 里有 dsh-session-delete、node_modules\dsh-session-delete 存在(GitHub 安装是普通目录,link: 安装是 junction)、已重启桌面端并刷新页面;再确认槽位 sidebar.workspaces.session.menu.item 仍是 kind: "list"(跑 tests/client-bundle.test.js 会交叉核对 .ref/ 真实契约) |
| 工作区行 … 菜单里没有「删除所有会话」 | 该项是 DOM 增强(DSH 0.2.0-rc.2 无槽位):确认客户端已加载、工作区行有 data-row-key="workspace:<id>"、菜单容器是 [role="menu"][data-menu-material];DSH 改版后该增强可能静默失效,但会话行菜单与批量接口不受影响(可用 openWorkspaceDelete 或直接调 /session-delete/workspace/) |
| 工作区批量删除一直显示「运行中」 | 任务在宿主端执行:看 /session-delete/workspace/status 的 state;某会话卡住通常是文件占用,任务仍会逐个继续,并在 failed 里列出;30 min 后 job 会被回收(此后 status 返回 404) |
| dsh plugin add 直接报 a local path must be absolute | 路径式 spec(link: / file: / 裸路径)必须是绝对路径;从 GitHub 装请写 github:<owner>/<repo>[#tag]。安装失败时 dsh plugin add 会把 profile 的 package.json / pnpm-lock.yaml 还原成原样(已下载的文件可能留下) |
| 确认框一直转圈/报网络错 | 宿主路由没挂上:/session-delete/health 是否 200;webServer 能力是否可用;客户端 20 s 超时会提示「宿主无响应」 |
| 提示 403 invalid-token | DSH 重启后令牌轮换:刷新页面即可(客户端会自动重取一次令牌并重试一次) |
| 提示 403 cross-site-blocked / cross-origin-blocked | 请求来源不是本页面(代理、反代改写 Host、或从别的站点触发) |
| 会话删掉又「复活」 | 第 3 阶段失败(挂账没清干净)——宿主日志搜 [session-delete];workspace.json 是否可写 |
| 提示部分降级 | 报告里 warnings / 各阶段 status:skipped = 能力缺失,failed = 真失败,blocked = gate 拒绝或「磁盘没清干净所以不广播」(见 §8 第 14 条) |
| 磁盘上还有残留目录 | 文件被占用(宿主仍开着句柄):关闭该会话后再删,或重启 DSH 后重试;预览里若提示锁文件,先关闭该会话 |
| 会话数据搬到 D:/E: 盘后删不掉 | 0.1.2 会跟随目录链接删除(见 §8 第 19 条)。若报「目录链接的目标不是会话存储」,说明该链接指向的目录里没有任何 session.jsonl / session.lock——插件拒绝递归删除无关目录;请自行确认该链接是否指错,或删掉链接后重试 |
| 运行中的会话删不掉 | 这是设计行为:默认必须显式 force;要彻底禁止就配 allowForce: false |

10. 研究依据

四份源码级报告在 docs/research/:r1-client-ui-injection.md(菜单/模态/槽位/本地化契约)、r2-host-session-lifecycle.md(宿主会话生命周期与可用的删除面)、r3-client-store-refresh.md(客户端 store、refresh()、api-session/removed 事件链)、r4-packaging-and-install.md(dsh.client / bundle patch / profile 安装与 HMR 行为)。结论均标注了 dsh-/lib/.js:行号 出处。
预览分享卡片长什么样 ↓Baboo-ming/dsh-session-delete
分享 · DeepSeek Harness Hub
Baboo-ming/dsh-session-delete

当前版本 0.2.20.2.1 → 0.2.2:工作区行菜单项去掉会话计数,固定显示「删除所有会话」;0.2.0 →…

这是你的仓库?认领条目

回到顶部