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

会话清理 Winnie-0721/dsh-dialogue-cleanup

DeepSeek Harnessv1.2.0
Winnie-07216 天前有提交已提供中文文档扫描:含敏感能力

在运行中的 DeepSeek Harness Web 界面里删掉会话 —— 不用重启,也不用离开当前窗口。

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

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

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-dialogue-cleanup
npm 源 · dsh-dialogue-cleanup 的发布者账号已校验与本仓库一致

状态速览

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

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

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

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

✓npm 包dsh-dialogue-cleanup @ 1.2.0
✓Node 引擎要求 >=20.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/31 07:32:56

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

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

读写/删除本地文件发起外部网络请求
查看扫描证据(共 12 处,展示前 12 条)
  • 发起网络请求
    client.js:1367
    response = await fetch(`${BASE}/delete`, {
  • 发起网络请求
    client.js:1874
    const response = await fetch(`${PANEL_API.list}?${query.toString()}`, { method: "GET" });
  • 删除本地文件
    lib/audit.js:122
    await rm(`${filePath}.${keep}`, { force: true });
  • 写入本地文件
    lib/audit.js:129
    await rename(`${filePath}.${i}`, `${filePath}.${i + 1}`);
  • 写入本地文件
    lib/audit.js:135
    await rename(filePath, `${filePath}.1`);
  • 读取本地文件
    lib/audit.js:200
    const info = await stat(filePath);
  • 读取本地文件
    lib/artifacts.js:66
    realRoot = await realpath(root);
  • 读取本地文件
    lib/artifacts.js:71
    realTarget = await realpath(target);
  • 读取本地文件
    lib/artifacts.js:350
    info = await lstat(file);
  • 删除本地文件
    lib/artifacts.js:372
    await rm(file, { force: true });
  • 写入本地文件
    lib/artifacts.js:392
    await mkdir(dirname(filePath), { recursive: true });
  • 写入本地文件
    lib/artifacts.js:398
    await appendFile(filePath, `${JSON.stringify(value)}\n`, "utf8");
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

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

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

界面预览

2 张 · 点击查看大图

DeepSeek Harness 插件:从运行中的 Web 运行时删除会话——实时存储、工作区记录和磁盘产物一并清掉。宿主端加 Web 客户端,打包在一个包里。

v1.2.02026-10-28内容较长已截断
这一版把「列表里几处说不清的空状态」一次说清楚,并新增 5 个查询能力。全部改动向后兼容: 没有删除任何既有字段、参数或端点,旧客户端照常工作。 · 新功能:标题四态 / 会话分类三态 / 空会话筛选 | 新增 | 说明 | |---|---| | items[].titleState | 每行标题的四态:ready(有标题)/ blank(空会话,从未有消息)/ empty(宿主确认没有标题记录)/ unread(非空但本次没读到)。四态两两可区分,「本来没有标题」不再和「本次没读到」长得一样。 | | items[].classification | 会话身份三态 main / subagent / unknown,另带 classificationSource(corpus / summary / corpus+summary)。身份未知的会话不再被默认当成主会话。 | | kind=unknown | /list 的 kind 过滤放行第四个取值(GET 与 POST 共用同一份白名单);面板新增「会话类型」四选一与「未知」徽标。 | | blank 三选一 | /list?blank=hide\|show\|only,默认 hide —— 空会话默认不再占满首屏。 | | blank.hiddenInView | 本次视图实际藏掉的空会话条数(已扣除 project / kind / archived / running 等其它筛选),区别于全库口径的 blank.total。 | 另外:项目身份字段统一为「cwd 权威、workspace.path 只作兜底」,item.project、项目下拉选项、 项目筛选三处同源;workspace 多方认领时改为「首个声明者胜」。 · 修复:空会话提示条与下拉框重复 —— 同一件事在一屏里说了两遍 默认视图的列表上方曾有一条常驻横条「已隐藏 N 条空会话(从未有消息的会话)。」配一个 「仅显示空会话」按钮。但筛选行里的空会话下拉本身就在说「隐藏空会话」,横条只是把 同一个状态再说一遍;横条里的按钮又与下拉的第三项「仅显示空会话」完全等价。同一屏里 两处出口、两条相同的话,还额外占掉一行高度(用户 2026-10-25 指出「不是重复显示了?」)。 修法:删掉整条横条,把条数并进下拉框自己的选中文字: · 选中 hide 档且本视图确实藏了会话时,药丸显示「隐藏空会话(N 条)」; · 藏了 0 条时退回「隐藏空会话」,不显示一个「(0 条)」的空括号; · 另两档不带条数(条数只属于 hide 档)。 信息一点没少(仍然是 hiddenInView 那个「本视图藏了多少」的数字,不是全库口径), 但只剩一处出口。顺带删掉三条因此变成死键的 i18n 条目:panel.blank、panel.blankRow、 panel.blankHiddenNotice。 新增回归测试:test/render.test.mjs 的 「空会话条数并进下拉选中文字,不得再另起一条重复的提示横条」——只认渲染出的文字: 必须出现 7 条(hiddenInView)、不得出现 13 条(全库 total)、条数必须落在 空会话下拉字段内、整屏不得再出现「已隐藏」这个横条措辞。已验证该用例在修复前的 代码上失败、修复后通过。 · 修复:「已隐藏 N 条空会话」在带筛选的视图里多报 blank.total 是全库口径(与 archived.total 同构),回答的是「库里一共有多少条 空会话」。但界面那句提示说的是「你这个视图里刚才消失了多少条」。两者在 kind=all 下恰好相等,一加 kind / project 筛选就分叉: · 真机实测 kind=main&blank=hide → 返回 total=8、blank.total=13,而同一视图 在 blank=show 下只有 12 条空会话行 —— 提示条多报 1 条(第 13 条是子代理 空会话,本来就被 kind=main 滤掉了,从来没出现在这个视图里); · project=E:\AI\DeepSeek Harness\tmp 时同样:blank.total 仍是全库 13,实际本视图 只藏了 7 条。 数字偏大看着不严重,但它是在告诉用户「刚才消失了什么」——报多了等于谎报,而这一整 轮修复的起点正是「界面说的话和事实对不上」。 修法:在空会话筛选处(其它筛选全部生效之后、空会话筛选之前)统计本视图实际藏掉的 条数,返回体扩展为 blank: { filter, total, hiddenInView }: · total —— 全库口径,语义不变,仍供「库里一共多少条」这类判断; · h
v1.1.22026-10-20
· 性能:查询延迟归因与标题读取去重(PERF-03 / PERF-04) 没有改变任何默认配置,没有改变查询结果语义。 · 1. 新增阶段耗时指标(3642075,纯观测) /list 每请求采集 sessionsMs / runningStateMs / titlesMs / filterSortPageMs / serializeMs,配 perfSeq 与 reqId,每请求输出一条 debug 摘要。只记计数与耗时, 不记标题文本、正文或 cwd。默认日志级别下不可见,需要显式打开 debug。 · 2. 标题读取跨请求去重(e6853ef,唯一有行为变化的改动) 新增 titleInFlight 单飞表:同一 sessionId 正在读取时,后续请求复用那个 Promise, 读完 finally 立刻清表(失败也清,绝不把一次读失败永久缓存成「没有标题」)。 实测(8 并发、400 条标题需求):标题读取 RPC 400 → 50 条(−87.5%)。 单次冷查询没有提速(503 → 516 ms)—— 单飞只合并并发重复,不能并行化单条日志的解码。 结果、排序、分页逐字段不变(test/perf-regression.test.mjs 9 条护栏)。 真实宿主侧也验证了去重(【真机】,零请求静默冷态,两轮独立复现)。判据是「2 并发 wallMs 是否远低于 2× 单请求」:轮 A 1519 ms vs 2×1632=3264;轮 B 618.5 ms vs 2×535=1070,两轮都只有无去重期望值的 46%–58%。若无去重,两个请求会各自完整同步解码一遍, wall 必然接近单请求的 2 倍。 注:不能用「wall 是否低于单请求」作判据 —— 轮 A 的 1519 低于 1632 只是时序巧合(第二个 请求几乎完全搭上第一个的车),轮 B 的 618.5 高于单请求 535(+15%)同样成立。写成「低于」 会让结论依赖巧合。 判据不能用 fetched:它统计的是「本请求最终拿到标题的条数」,包含复用别人 Promise 的结果,多并发时每请求仍报满额是正常的。 · 3. 真实宿主归因结论(docs/PERF-04-REPORT.md) 在运行中的真实 DSH 宿主上测得(只读 GET /status 与 /list): · 「约 2000 ms」复现,但取值取决于 live 组成:零请求静默后同一份 16 条语料 (指纹恒为 c33620121c60)的冷读在 535 / 1180 / 1632 / 1647 ms 之间变化; 早期 23 条 / 108.4 MB 语料(含 58.33 MB 巨日志)为 2149 ms;重启后首查最坏 3761–5298 ms;热态 49–140 ms。 · 决定性发现:成本由「非 live 的持久化日志字节」决定,不是候选总字节。 两份总字节几乎相同的语料(52.38 vs 52.56 MB)因 live 组成不同,非 live 字节 8.48 vs 25.37 MB(2.99×),冷读 535 vs 1647 ms(3.08×),折算 63.1 vs 64.9 ms/MB 非 live,只差 3%。 · 机制:宿主 SessionCorpus.projectMany 先 this._ctx.sessions.get(id), 命中 live 就直接用内存 events 投影,只有未命中的才 readColdSessionLog 读整份日志。 实测 27 MB 的 live 会话单条冷读仅 145 ms(热 113 ms),而本机解码同一条需 900 ms 以上 —— 它根本没读盘。⇒ 真实使用场景下(当前对话的会话必然是 live)侧栏冷查询比 「全语料都冷」便宜约 3 倍;但重启后或访问大量历史会话时仍要承担完整解压成本。 · 单条成本未测出,且曾误判:原记录「单条 58.33 MB ≈ 1.9 s(1893 ms)」已作废 —— 那次实测走 query=,该路径折叠整个候选集的标题(同一次响应自报 fetched:23), 1893 ms 是全量成本而非单条成本。query= 不缩小标题读取范围。 · 交叉验证撤回:曾写「本机等价解码稳定偏慢 1.64–1.68 倍」,该比值建立在 「本机解全部 16 条 vs 宿主只解非 live 那部分」的口径不一致之上,已作废。 本机解码表只保留用作相对排序(最大的 d7d90226 一条占本机成本 53%)。 · 口径陷阱:磁盘 56 个日志 / 102.1 MB,但 /list 默认 kind=main 排除 subagent 与部分 归档会话,实际候选只有 1
v1.1.12026-10-19内容较长已截断
· 收尾整改:CI 补齐、审计治理、删除竞态与「未知状态」处理 上一版(1.1.0,2026-10-16)不含 10-18 的删除安全修复;本版把它们与后续 收尾整改一起发出。完整证据见 docs/整改报告-稳定性与安全性.md 与 docs/删除安全残留分析.md。 · P0 · 新增常规 CI(.github/workflows/ci.yml) 此前仓库只有标签/Release 触发的发布工作流,push 到 main 与 PR 都不会跑测试。 新增独立 ci.yml:push→main、PR→main、workflow_dispatch 三触发;矩阵 node 20/22 × ubuntu/windows;权限只有 contents: read,不接触任何发布凭据。 关键一步是断言测试条数:node --test-reporter=tap --test "test/**/*.test.mjs" 解析 # tests N 并要求 N ≥ 200。理由是那层 glob 由 node 自己展开,一旦某层吃掉引号 就会「跑了 0 条还退出 0」——绿着的假象。变异测试独立成 job,并要求 survived === 0(脚本在原文未命中时会打 SKIP 并计入 survived,只看退出码会漏掉 「断言退化成装饰品」)。 实测踩坑并已修正:写成 npm test -- --test-reporter=tap 时,参数落在文件实参之后 会被 node 静默忽略(输出仍是 ℹ tests N,没有 # tests N),CI 会永远匹配不到 而每次假红。必须把 reporter 旗标放在 --test 之前。 首次远端运行还抓到两个更严重的问题(本地 Windows/Node 24 都测不出来): 1. Node 20 的 node --test 不支持 glob:Could not find '.../test/**/*.test.mjs'。 glob 展开是 Node 22+ 才有的,而 engines.node 声明 >=20.0.0 —— 也就是 「文档里写的跑法」在最低支持版本上根本跑不起来。package.json、release.ps1、 ci.yml 三处都用了它,现已统一到新增的 scripts/run-tests.mjs (由 Node 自己枚举文件再传给 node --test),三个版本、两个平台行为一致。 2. isInside 测试硬编码 Windows 路径:not ok 236。POSIX 上 path.resolve() 把反斜杠当普通文件名字符,C:\tmp\sessions 变成无分隔符的单段路径,断言必失败。 这不是 isInside 的 bug,是测试的平台假设错误;已改为按 process.platform 取路径。 修复后 CI 四个组合(node 20/22 × ubuntu/windows)全绿, 日志确证每个组合都跑了 13 个文件 / 238 条测试并通过 ≥200 断言(不是空转)。 · P0 · 运行态检查与磁盘删除之间的竞态(公开 API 下的最保守策略) 调查结论(实测,非推测):DSH 确实有内核级会话写锁 —— SessionWriteLease.acquire,Windows 用命名信号量 Local\dsh-session-lock-,POSIX 用 fd 锁,占用时抛 SessionAlreadyOwnedError。但它不对插件暴露:SessionWriteLease 是内部模块 (无 export 块、无 ctx.* 挂载),唯一对外表现是 resume 被占用时返回 session/writer-held 错误 —— 那是错误码,不是可调用的锁。 因此不声称竞态已消除。采取能实现的最保守策略:在每个 rm 之前紧贴地再判一次 运行态(beforeDelete 回调),把「检查→删除」窗口从整个事务压到「单次探测→rm」; 二次确认失败或判为 running 时拒绝删除并如实进降级清单 (step: "preDeleteRecheck")。批量仍复用一次快照做早期拒绝,但删除前的最后一道 判定故意每次都取新鲜快照,不用过期快照做破坏性决策。 残余风险如实记录:窗口被压到单条语句级,但不为零;彻底消除需要宿主把 SessionWriteLease.acquire 挂到公开服务上(已作为上游建议写入残留分析文档)。 · P1 · 会话身份查询失败不再静默 sessions.list() 抛错 / 返回非数组 / 服务缺失,原来被同一个 catch 压成 liveIdsKnown=false,用户看到的是 complete:
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-dialogue-cleanup English · 简体中文 在运行中的 DeepSeek Harness Web 界面里删掉会话 —— 不用重启,也不用离开当前窗口。 会话行 ... 菜单里一项做单条删除,另有一页批量管理面板挂在设置页里。 安装 ·界面 ·实现 ·发布流程 · Releases 为什么会有它 一句话定位:删会话就该真的删掉。 DeepSeek Harness 把一条会话同时放在四个地方 ——内存 live 条目、工作区记录、注册表残留(pin / archive)、以及磁盘上的目录。只清掉其中一处,留下的就是一行幽灵会话、一条过期的工作区记录,或者一个还在悄悄占磁盘的目录。 下面每一条,都是这个定位的具体落实: - 一个不可逆操作,只实现一次。 删除逻辑在宿主(lib/service.js 的 deleteSession),由两个薄适配层通向它 —— HTTP 契约与 slot UI —— 因此单条删除与批量删除不可能各自漂移。 - 运行中的会话是「拒绝」,不是「猜」。 deletable 由宿主判定,UI 不去猜;宿主会拒的,界面根本不给选。 - 已归档默认仍然显示。 归档不等于删除:归档会话照样占磁盘,默认藏起来恰好会藏掉你想清理的那些。
展开完整文档(README 剩余 1 节)
dsh-dialogue-cleanup

English · 简体中文

在运行中的 DeepSeek Harness Web 界面里删掉会话 —— 不用重启,也不用离开当前窗口。
会话行 ... 菜单里一项做单条删除,另有一页批量管理面板挂在设置页里。

安装 ·界面 ·实现 ·发布流程 · Releases

为什么会有它

一句话定位:删会话就该真的删掉。 DeepSeek Harness 把一条会话同时放在四个地方 ——内存 live 条目、工作区记录、注册表残留(pin / archive)、以及磁盘上的目录。只清掉其中一处,留下的就是一行幽灵会话、一条过期的工作区记录,或者一个还在悄悄占磁盘的目录。
下面每一条,都是这个定位的具体落实:

- 一个不可逆操作,只实现一次。 删除逻辑在宿主(lib/service.js 的 deleteSession),由两个薄适配层通向它 —— HTTP 契约与 slot UI —— 因此单条删除与批量删除不可能各自漂移。
- 运行中的会话是「拒绝」,不是「猜」。 deletable 由宿主判定,UI 不去猜;宿主会拒的,界面根本不给选。
- 已归档默认仍然显示。 归档不等于删除:归档会话照样占磁盘,默认藏起来恰好会藏掉你想清理的那些。
(官方侧栏默认隐藏;这里是三选一 —— 隐藏已归档 / 全部对话 / 仅显示已归档,且默认显示已归档。)
- 空会话默认隐藏(照官方侧栏)。空会话 = 从未收到过任何用户消息的会话:日志只有五个初始化事件(约 588 字节),既没有 user/message 也没有 session/title,本来就无标题可显示。
它们数量往往远多于真会话(某宿主 254 条里 199 条),一行一条正是过去把面板刷成一片 (无标题) 的原因。官方侧栏 sessionVisible() 写的是
if (session.blank && session.id !== current) return false;。这里是三选一 ——
隐藏空会话 / 全部 / 仅显示空会话,默认隐藏;另给两个显式档位,是因为清理磁盘才是本插件的职责,空会话仍需能被批量删掉。列表返回 blank: { filter, total, hiddenInView } —— total 是全库口径(同 archived.total),hiddenInView 是本视图在其它筛选生效后实际藏掉的条数。条数直接写在下拉框自己的选中文字里(隐藏空会话(7 条)),所以数字永远描述用户正在看的这个列表,而且同一件事只在一处出现 —— 不再另起一条「已隐藏 N 条」的常驻横条把下拉已经说过的话再说一遍。一键即可切换档位,任何东西都不会「悄悄消失」。
- 子代理行明确标注。 官方侧栏把 origin: 'subagent' 的行整个藏起来,所以一旦这些行出现(kind = 子代理 / 全部),每一行都会显式打上徽标 —— 子代理会话绝不能看起来像普通对话。
- 它如实汇报做了什么。 结果行给出 成功 / 失败 / 降级 / 删掉的目录数,每条失败原因原样保留。
摘不掉 live store 条目时,插件报 detached: false,而不是假装成功。
- 错误码是语义正确的。 与上游包(永远回 200)不同,这里用真实状态码: 400 / 403 / 404 / 405 / 409 / 429 / 500。

界面速览

确认框:不可逆,并且明说。 只有两条关闭路径 —— 显式「取消」按钮,或 Esc。
点击遮罩故意不关闭:一次误点不该让你损失一次确认机会。

这个浮层是原生 + showModal() 驱动的,因此进入浏览器的 top layer,能盖在打开它的设置面板之上(普通定位元素做不到)。同时白送真正的焦点陷阱与正确的 Esc 语义,而且不需要把 DOM 挂到组件树之外。

批量面板(挂在设置页) —— 搜索、项目 / 状态 / 会话类型 / 归档四组筛选、排序、分页,以及逐行状态徽标。上面截图里的徽标(空闲 / 运行中 / 子代理 / 已归档)来自真实渲染输出: 运行中 的行不可勾选。

两张截图都由 scripts/render-shot.mjs 从真实组件树 + 真实 CSS 生成,再用无头 Edge 栅格化 —— 不是手画的示意图;但也不是在运行中的 GUI 里拍的照片,详见下面的已知限制。

快速开始

dsh plugin --profile web add dsh-dialogue-cleanup

1. 单条删除 —— 鼠标移到侧栏的会话行,打开 ... 菜单选 删除会话 (注册在 order: 500,紧跟官方 archive 项 = 400 之后)。确认框弹出后确认,即清掉 live 条目、工作区记录、注册表残留与磁盘工件。
2. 批量删除 —— 打开 设置 → 对话清理。勾选若干行(或「全选本页可删除项」),一次请求删完。
3. 宿主半改动需要重启 DSH 进程;客户端半不需要(见 docs/RELEASING.md 的表)。

兼容 DSH 0.2.0-rc.2。npm 包与 GitHub Release 一起发布;版本号规则与发布链路见 docs/RELEASING.md。

它怎么实现的

一个 npm 包同时装两半,按官方插件规范装进宿主:

- 宿主半(仓库进程) —— lib/index.js 是 Cordis 函数插件,只具名导出 name / inject / apply;它注册一条 ctx.webServer 前缀路由,删除实现全部留在 lib/service.js。磁盘删除与路径约束在 lib/artifacts.js,审计在 lib/audit.js。
- 客户端半(浏览器) —— client.js,单文件 bundle,经
window.__ModuleLoader__.load({ id, factory }) 注册,除 require("react") 外零依赖。
- 两个入口都是官方 slot,都不碰 DOM:
- sidebar.workspaces.session.menu.item,order: 500 → 单条删除那一项。
- settings.section(id: dialogue-cleanup,order: 25,排在官方
account(-10) / general(0) / models(10) / plugins(15) / agent-presets(20) 之后)
→ 入口和面板。一个注册项 = 设置里的一页,内容渲染在设置面板的内容列里。
- 面板因此不需要 layout 服务,也不往 main / sidebar 抢位置 ——少一个可选依赖、少一处降级分支。
- 样式表跟着组件走。 菜单项把 渲染在自己的树里 (这就是本项目自查发现的 H1:那一行原本完全没样式,因为样式表只在设置页路径上注入过)。

删除的六个步骤

1. 拒绝运行中的会话 —— 409 refused。
2. 摘掉 live store 条目 —— 否则删掉的会话会以幽灵行留在界面上。
3. 清理工作区记录 —— 只统计真正摘掉的那些工作区。
4. 清注册表残留 —— pin / archive 记录,走公开的幂等 API。
5. 删除磁盘工件 —— <DSH_HOME>/sessions/<project>/<sessionId>,外加派生的投影缓存。
每个目录真正 rm 之前还会再判一次运行态(见下)。
6. 写一行审计 —— <DSH_HOME>/logs/dialogue-cleanup.jsonl。

为什么第 5 步要「再判一次」

第 1 步判定到第 5 步真正删除之间存在时间窗。窗口内会话可能开始运行,那就删掉了一个正在用的会话 —— 不可逆。

DSH 本身有内核级会话写锁(SessionWriteLease,Windows 命名信号量 / POSIX fd 锁),但它没有暴露给插件(唯一外部表现是 resume 时的 session/writer-held 错误码)。
所以本插件拿不到原子互斥,只能采用公开 API 下最保守的策略:在每个 rm 之前紧贴地再判一次运行态,把窗口从「整个事务」压到「单次探测 → rm」。
二次确认判定 running 或状态不可知时拒绝删除该目录,并如实进 degradations (step: "preDeleteRecheck"),绝不静默当成 idle 删掉。

残余风险如实说明:窗口被压到单条语句级,但不为零。彻底消除需要宿主把写锁挂到公开服务上。详见 docs/删除安全残留分析.md。

删除是不可逆的

没有回收站、没有撤销。确认框里明说这一点,并且只有两条关闭路径(显式「取消」或 Esc)。
运行中 的行不可勾选 —— 宿主会拒的,界面根本不给选。

批量删除部分失败时怎么重试

批量结果是逐条的,不会因为一条失败就整批回滚。返回里有:

| 字段 | 含义 |
| requested / duplicates / total | 你提交了几条 / 其中重复几条 / 实际处理几条(重复 id 已去重) |
| ok / fail | 成功 / 失败条数 |
| complete / degraded / noop | 完整成功 / 成功但有降级 / 什么都没删到 |
| allComplete | 整批全绿才为 true;任一子项有降级即 false |
| outcomes[] | 逐条:outcome(deleted/noop/refused/blocked/failed)+ retryable |
| retry[] | 只含需要重试的 id(失败或降级的),已完整成功的不在内 |
| degradations[] | 逐条降级明细(sessionId + step + reason) |

重试方法:把 retry 数组原样作为下一次 sessionIds 提交即可 ——不要重跑整批,那样会把已完整成功的条目再做一次无意义的操作。
refused 的条目要先在界面上关掉那个会话(或等它跑完)再重试; noop 的条目不需要重试(它本来就不存在)。

HTTP 契约

| 方法 | 路径 | 用途 |
| POST | /api-ext/dialogue-cleanup/delete | { "sessionId": "..." } 删一条;{ "sessionIds": [...] } 批量删(宿主逐条走同一个 deleteSession,返回逐条结果) |
| POST/GET | /api-ext/dialogue-cleanup/list | 会话行,带 deletable / blockedReason / classification / titleState |
| GET | /api-ext/dialogue-cleanup/status | 能力、配置、运行时路径 |

信封:{ ok: true, value } / { ok: false, error: { code, message, hint? } }。

每行带 titleState,用来区分 title 为空的四种不同成因。把它们压成一句「(无标题)」正是一屏看起来像坏了的原因:

| titleState | 含义 | 重试有用吗 |
| ready | 有标题 | — |
| blank | blank: true —— 从未收到过用户消息(日志只有初始化事件) | 没用,它本来就不该有 |
| empty | 有内容,但宿主明确答复没有标题(例如只跑了 turn/start → turn/end 的子代理会话) | 没用 |
| unread | 有内容,本次没读到(超出 titleLimit 预算,或读取失败) | 有用 |

empty 靠 titleConfirmed 集合支撑,只收宿主成功答复过的 id,所以「超预算」或「这一批 RPC 失败」永远不会被伪装成「本来就没有标题」。客户端据此渲染三种不同占位文案,并对 empty 与 unread 都印出会话 id(两者都是真会话,而删除不可逆),blank 则不印(实测 254 条里 199 条是空会话,铺满 UUID 会把真会话淹掉)。

配置

所有可调项都在插件 Config(lib/config.js)里,因此用户的 patch 层能跨升级保留。
注意:匹配到的 override 会整体替换 config —— 你在意的字段要全写上。

| 键 | 默认 | 含义 |
| maxBodyBytes | 65536 | 请求体上限(上游:1 MiB) |
| readTimeoutMs | 10000 | 读体超时(上游:无) |
| audit / auditPath | true / null | 审计日志;null → <DSH_HOME>/logs/... |
| rateLimitPerMinute | 60 | 每分钟删除请求配额(一次批量算一次);0 关闭 |
| requireIdleSession | true | 运行中会话拒绝逻辑的安全阀 |
| stopIdleAgent | false | 见下方「已知限制」 |
| matchAlternateIdSpelling | true | 带守卫地探测另一种 session- 拼写 |
| removeProjectionCache | true | 同时删掉派生的投影缓存 |
| clearRegistryRecords | true | 解除 pin / archive 残留 |
| confirmInUi | true | UI 确认(宿主不信任它) |
| announceRemoval | true | 删除后广播 session.removed,让别的视图同步移除该行 |
| titleCacheTtlMs | 300000 | 标题折叠缓存 TTL;0 关闭 |
| titleFetchConcurrency | 8 | 折叠标题时的分片并发 |
| titleLimit | 100 | 每页最多为多少行折叠标题;0 完全跳过标题读取 |
| titleSearchLimit | 40 | 搜索时单独的标题预算(与 titleLimit 独立);预算外如实计入 titles.truncated |
| runningProbeTtlMs | 1000 | 运行态快照复用的时长。把批量删除从「O(N) 次全量清单调用」变成 O(1);0 则逐条重探。注意:删除前的最后一道判定不吃这个缓存(见「删除的六个步骤」) |
| auditMaxBytes | 4194304 | 审计文件轮转阈值(4 MiB)。超过就把当前文件改名为 .1,旧的依次后推;0 = 不轮转 |
| auditRotateKeep | 3 | 轮转保留的历史份数(不含当前文件)。超出该份数的最旧一份会被删除 |

审计日志在哪、留多久

默认写到 <DSH_HOME>/logs/dialogue-cleanup.jsonl(DSH_HOME 通常是 ~/.dsh),一行一条 JSONL。可用 auditPath 改位置,但路径会被规范化并校验必须落在 <DSH_HOME> 之内 —— 越界配置会被拒绝写入并如实上报(audit.ok=false),不会静默写去别处。

保留策略:当前文件超过 auditMaxBytes(默认 4 MiB)就整份改名为 .1, .1→.2、.2→.3,超出 auditRotateKeep(默认 3 份)的最旧一份删除。
所以默认最多占用约 4 份 × 4 MiB。轮转用 rename 搬走整份,不会截断正在写入的日志;轮转失败时不阻断写入(日志继续长总比丢审计好),只把 rotateError 作为降级返回。

字段最小化:一行只写诊断必需字段(时间 / 操作 / actor / phase / sessionId / 结果码 / 文件与缓存计数 / 耗时 / 降级步骤)。整份 runtime、会话标题、全量路径数组 都不写 —— 旧实现把那些都塞进一行,实测 910 B/行;现在实测 196 B/行。
需要明细时按 sessionId 去宿主日志里查。

审计失败与删除结果是两件不同的事实:audit.ok=false 只表示留痕失败,会进结果的 degradations(step: "audit"),但不会把已经执行的删除伪报成回滚。

已知限制

以下都是对着真实运行时测出来的,与上游包的假设不同。如实写出,而不是粉饰:

- 没有公开的「停掉空闲 agent」接口。 公开的 Agent 类型是 { readonly id: SessionId } ——没有 status、没有 ctx。因此上游的 agent.status === "running" 与
agent.ctx.fiber._unload() 都不可依赖(_unload 是 Cordis 的私有方法,这里刻意永不调用)。
「是否在跑」来自公开的 sessionController.list() → SessionSummary.running。
仍挂着 agent、又无法安全停掉的会话会被拒绝,且界面会说明原因。
- 摘 live 条目是尽力而为。 公开的 sessions 服务提供 create/prepare/enter/announce/flush/get/list/fork —— 没有公开的 detach。
插件先试公开方法,再试内部 store 条目;两者都拿不到时如实报 detached: false,而不是宣称成功。
- 磁盘上并存两种 id 拼写。 在同一个 <DSH_HOME> 里观察到: ...\<uuid>\session.v4.jsonl.zstd 与 ...\session-<uuid>\session.v4.jsonl.zstd。
按精确 id 拼接永远删不掉带前缀的那一半。本插件只在能确认候选不是另一个真实会话时才探测另一种拼写;没有这种证据就什么都不删,宁可漏删也不误删邻居。
- 截图是真实渲染输出,不是在跑着的 GUI 里拍的照片。 它们来自真实组件树 + 真实 CSS,无头栅格化;主题令牌固定为浅色,行数据是演示用的假会话。
面板在真实主题下的样子,本仓库没有拍过。

开发与验收

全套门禁:node --check + 契约/渲染/发布等回归套件
node scripts/run-tests.mjs

只跑发布门禁(不改版本、不打包、不发布) pwsh -File scripts/release.ps1 -LocalOnly

用真实组件树重新生成两张文档截图
node scripts/render-shot.mjs

不要写成 node --test "test//.test.mjs":glob 展开是 Node 22+ 才有的,而 engines.node 声明 >=20。在 Node 20 上这句会直接报 Could not find '.../test//.test.mjs'。scripts/run-tests.mjs 由 Node 自己枚举 test/.test.mjs 再交给 node --test,三个版本、两个平台行为一致(CI 已覆盖)。

当前共 302 条测试 / 18 个文件(其中 3 条是平台相关用例,每个平台实际跑 299 条):

| 套件 | 覆盖 |
| contract.test.mjs | 宿主 ↔ 客户端契约与 slot 声明(最大的一套) |
| service.test.mjs | deleteSession 端到端行为与否决路径 |
| fault-injection.test.mjs | 逐步骤故障注入:磁盘/内存/工作区/审计/通知分别失败时不得冒充完整成功;含 §4.1 删除前二次确认竞态、§4.2 身份查询失败可观测、§4.3 链接跳过可观测、对偶拼写红线、junction 越界(静态 + 竞态) |
| isolated-integration.test.mjs | 隔离环境破坏性集成:完整副作用清理、running 零改动、真实子进程 SIGKILL 中断后重启重跑、批量部分成功后重试 |
| batch-semantics.test.mjs | 批量语义:去重、deleted/noop/refused/blocked 可区分、精确 retry 清单、allComplete |
| audit.test.mjs | 审计:字段最小化、截断、轮转(含不截断正在写的日志)、并发、路径不可写/越界、禁用、配置边界 |
| perf-regression.test.mjs | 查询性能不退化的语义护栏:冷/热结果逐字段一致、单请求不重复读标题、N 并发只读一次、失败可恢复、指标不泄露标题与 cwd |
| client.test.mjs | 注册面、id、不跨包 require |
| render.test.mjs | 组件真的被渲染(React 替身);titleState 四种占位必须两两可区分,empty 不得复用 unread 的措辞;凡是有默认值的筛选维度(archived / kind / blank)都必须真的进到请求 URL —— 只在本地 state 里改的下拉框看着完全正常,却什么都筛不到;空会话条数必须写在下拉框自己的文字里(只此一处、用 hiddenInView、不得再有重复的提示横条) |
| artifacts.test.mjs | 路径约束、符号链接拒绝、另一种拼写的守卫、isRealPathInside 正反断言 |
| session-id.test.mjs | id 校验(不含分隔符与点号) |
| eol.test.mjs | 行尾,以及每个被跟踪文件都被 git 声明为 eol=lf |
| release-workflow.test.mjs | OIDC 发布结构(反向断言:pack-release.yml 不得自带发布步骤) |
| ps-script.test.mjs | Windows PowerShell 5.1 的 BOM / 编码坑 |
| project-identity.test.mjs | 一行的项目身份是同一个字段:显示、筛选、下拉选项全部由它派生(有 cwd 用 cwd,否则退回 workspace 路径);大小写/分隔符变体合并成一个选项 |
| transaction-boundary.test.mjs | 删除与并发的交界:标题读取在途、批量里一条被拒 + 一条非法 id、审计在途/失败、投影缓存三态(正常 / 符号链接 fail-closed / 根路径异常) |
| title-async-prototype.test.mjs | 原型,不是生产路径:docs/TITLE-ASYNC-DESIGN.md 的参考合并状态机,对「可控闸门」的假宿主验证(首屏不阻塞、不卡 pending、旧 token 被拒、已删行不复活、empty ≠ unreadable) |
| classification.test.mjs | 会话分类三态:只出现在摘要链路上的行必须回成 unknown,绝不能再算主会话;证据只累加不相减(字段缺失不是反证);另有一条 handler 级用例钉住 GET /list?kind=unknown 穿透 HTTP 白名单 |

另有 test/integration-real.mjs(只读真实 DSH 检查,需要活着的宿主,故不并入 .test.mjs 套件)与 test/mutate-tokens-official.mjs(变异测试,要求 survived=0)。

其中两条是因为对应的 bug 真的发出去过、之后才被发现才补上的:

- eol.test.mjs —— 第一次发版时,CI(pnpm pack)与本地(npm pack)打出的字节不一样: CI 里的 icon.svg 与 LICENSE 是 CRLF,因为 .gitattributes 只写了 * text=auto,而 GitHub 的 Windows runner 默认 core.autocrlf=true。
这条测试的第一版漏掉了它(它扫描的是硬编码扩展名清单,于是 *.svg 与没有扩展名的 LICENSE 完全不在视野里);现在改为扫所有被 git 跟踪的文件,并用 git check-attr 读实际生效的属性。修的过程中还顺带抓出了 .gitignore。
- release-workflow.test.mjs —— OIDC 信任绑定在工作流文件名上,而「把发布步骤再写一遍」正是让另一个仓库连续两次发版失败的那个 bug。
见 docs/RELEASING.md。

仓库结构

| 路径 | 内容 |
| lib/index.js | apply(ctx, config) —— 组装并注册宿主路由 |
| lib/service.js | deleteSession —— 唯一的删除实现 |
| lib/artifacts.js | 磁盘删除 + 路径约束 |
| lib/http.js | 同源检查、请求体上限、状态码 |
| lib/config.js | Config schema 与默认值 |
| lib/audit.js | JSONL 审计 |
| client.js | Web 客户端半:slot 注册(菜单项 + 确认框 + 设置页 section) |
| cordis.patch.yml | 插入宿主半,inject: [webServer] |
| .github/workflows/ | pack-release.yml(标签 → 门禁 → 打包 → GitHub Release)与 publish-npm.yml(唯一的 npm 发布实现,由同一次标签推送并列触发,不经 workflow_call 复用) |
| docs/RELEASING.md | 版本号规则与发布链路 |
| scripts/release.ps1 | 版本递增 + 门禁(+ CI 打包发布) |
| scripts/render-shot.mjs | 重新生成文档截图 |
| test/ | 302 条回归测试 |
| VERIFICATION.md | 完整验收报告 |

关于上游

这是对 npm 包 dsh-session-cleaner@1.0.2 能力边界的合规重写,不是照抄它的实现。
上文「已知限制」里的每一条,都是与它的假设不同的地方。

许可

MIT
预览分享卡片长什么样 ↓Winnie-0721/dsh-dialogue-cleanup
分享 · DeepSeek Harness Hub
Winnie-0721/dsh-dialogue-cleanup

在运行中的 DeepSeek Harness Web 界面里删掉会话 —— 不用重启,也不用离开当前窗口。

这是你的仓库?认领条目

回到顶部