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

归档会话清理 haotian-lu-prog/dsh-archived

DeepSeek Harness
haotian-lu-prog32 天前有提交已提供中文文档扫描:含敏感能力

删除已归档会话并清空磁盘残留,误删可恢复

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

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

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add haotian-lu-prog/dsh-archived
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

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

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

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

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

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 52 处,展示前 45 条)
  • 读取本地文件
    lib/index.js:225
    const cache = existsSync(cacheFile);
  • 读取本地文件
    lib/index.js:256
    present: existsSync(legacyAggregateFile()),
  • 读取本地文件
    lib/index.js:298
    if (existsSync(cacheFile)) await rm(cacheFile, { force: true });
  • 删除本地文件
    lib/index.js:298
    if (existsSync(cacheFile)) await rm(cacheFile, { force: true });
  • 删除本地文件递归
    lib/index.js:415
    for (const dir of dirs) await rm(dir, { recursive: true, force: true });
  • 读取本地文件
    lib/index.js:417
    if (existsSync(cacheFile)) {
  • 删除本地文件
    lib/index.js:418
    await rm(cacheFile, { force: true });
  • 读取本地文件
    lib/index.js:422
    } else if (dirs.length === 0 && !existsSync(cacheFile)) {
  • 读取本地文件
    lib/index.js:428
    const document = existsSync(cacheFile) ? await readCacheDocument(rawId) : null;
  • 写入本地文件
    lib/index.js:505
    await mkdir(dirname(sweepSettingsFile()), { recursive: true });
  • 写入本地文件
    lib/index.js:506
    await writeFile(sweepSettingsFile(), JSON.stringify(next, null, 2) + "\n", "utf8");
  • 删除本地文件递归
    lib/index.js:569
    for (const dir of dirs) await rm(dir, { recursive: true, force: true });
  • 删除本地文件
    lib/index.js:572
    await rm(cacheFile, { force: true });
  • 执行外部命令来自配置/变量
    lib/index.js:666
    const child = spawn(command, [path], { detached: true, stdio: "ignore" });
  • 发起网络请求
    lib/client.js:115
    return fetch(path, init).then(function (response) {
  • 读取本地文件
    lib/host/paths.js:195
    workspaces = await readdir(sessionsRoot(), { withFileTypes: true });
  • 读取本地文件
    lib/host/paths.js:204
    const direct = candidates.map((id) => join(bucket, id)).find((path) => existsSync(path));
  • 读取本地文件
    lib/host/paths.js:211
    entries = await readdir(bucket, { withFileTypes: true });
  • 读取本地文件
    lib/host/paths.js:230
    entries = await readdir(path, { withFileTypes: true });
  • 读取本地文件
    lib/host/paths.js:238
    else if (entry.isFile()) total += (await stat(child)).size;
  • 读取本地文件
    lib/host/metadata.js:16
    const text = await readFile(projectionCacheFile(sessionId), "utf8");
  • 读取本地文件
    lib/host/metadata.js:45
    const info = await stat(file);
  • 读取本地文件
    lib/host/metadata.js:47
    const parsed = JSON.parse(await readFile(file, "utf8"));
  • 读取本地文件
    lib/host/quarantine.js:31
    await access(path, constants.F_OK);
  • 写入本地文件
    lib/host/quarantine.js:40
    await mkdir(dirname(to), { recursive: true });
  • 写入本地文件
    lib/host/quarantine.js:42
    await rename(from, to);
  • 删除本地文件递归
    lib/host/quarantine.js:47
    await rm(from, { recursive: true, force: true });
  • 读取本地文件
    lib/host/quarantine.js:60
    bytes += (await stat(cacheFile)).size;
  • 写入本地文件
    lib/host/quarantine.js:65
    await mkdir(root, { recursive: true });
  • 写入本地文件
    lib/host/quarantine.js:76
    await writeFile(
  • 读取本地文件
    lib/host/quarantine.js:92
    children = await readdir(quarantineRoot(), { withFileTypes: true });
  • 读取本地文件
    lib/host/quarantine.js:101
    meta = JSON.parse(await readFile(quarantineMetaFile(sessionId), "utf8"));
  • 写入本地文件
    lib/host/quarantine.js:148
    await mkdir(dirname(target), { recursive: true });
  • 删除本地文件递归
    lib/host/quarantine.js:156
    await rm(root, { recursive: true, force: true });
  • 删除本地文件递归
    lib/host/quarantine.js:174
    await rm(quarantineEntry(sessionId), { recursive: true, force: true });
  • 读取本地文件
    lib/host/quarantine.js:193
    return (await readdir(sessionsRoot(), { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
  • 读取本地文件
    lib/host/deferred.js:41
    const parsed = JSON.parse(await readFile(deferredFile(), "utf8"));
  • 写入本地文件
    lib/host/deferred.js:54
    await mkdir(dirname(deferredFile()), { recursive: true });
  • 写入本地文件
    lib/host/deferred.js:57
    await writeFile(deferredFile(), JSON.stringify({ version: 1, items }, null, 2) + "\n", "utf8");
  • 读取本地文件
    lib/host/sweep.js:45
    entries = await readdir(dir, { withFileTypes: true });
  • 读取本地文件
    lib/host/sweep.js:68
    return (await stat(log)).mtimeMs;
  • 读取本地文件
    lib/host/sweep.js:75
    entries = await readdir(dir, { withFileTypes: true });
  • 读取本地文件
    lib/host/sweep.js:82
    const info = await stat(join(dir, entry.name));
  • 读取本地文件
    lib/host/sweep.js:139
    buffer = await readFile(logPath);
  • 读取本地文件
    lib/host/sweep.js:165
    workspaces = await readdir(sessionsRoot(), { withFileTypes: true });
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

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

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-client-locale@deepseek-ai/dsh-client-ui-settings@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-client-ui-workspace@deepseek-ai/dsh-host-webserver@deepseek-ai/dsh-session@deepseek-ai/dsh-workspace
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 删除已归档会话
  • 清空磁盘残留
  • 误删可恢复

DSH 已归档会话管理器:插件 + RPC/端到端工具

README

由 DeepSeek 最新模型翻译生成
dsh-archived 给 DeepSeek Harness 的 设置 → 已归档 页补上删除:逐条删、勾选批量删、清空;删除默认先进回收站(30 天内可恢复),并显示每行的磁盘占用。 DSH 至今没有官方「删除会话」的接口——归档只能取消归档,日志和投影缓存会一直留在磁盘上。 这个插件就是补上那一块。 它做了什么 DSH 0.2.0-rc.2 里,一个已归档会话牵涉四处状态,少清一处列表就会骗人: | # | 状态 | 位置 | | 1 | 会话日志目录 | $DSH_HOME/sessions/<编码工作区>/session-<id>/(session.v4.jsonl.zstd + session.lock) | | 2 | 投影缓存文档 | $DSH_HOME/storages/session_projcache/sessions/<id>.json | | 3 | 归档索引 | workspace registry 的 archivedSessionIds | | 4 | 回收站(本插件自建) | $DSH_HOME/.archived-sessions-quarantine/<id>/ | 删除有两条路径,界面上是两个分开的按钮: - 删除(默认):把 1、2 移动进 4,再清 3。
展开完整文档(README 剩余 1 节)
dsh-archived

给 DeepSeek Harness 的 设置 → 已归档 页补上删除:逐条删、勾选批量删、清空;删除默认先进回收站(30 天内可恢复),并显示每行的磁盘占用。

DSH 至今没有官方「删除会话」的接口——归档只能取消归档,日志和投影缓存会一直留在磁盘上。
这个插件就是补上那一块。

它做了什么

DSH 0.2.0-rc.2 里,一个已归档会话牵涉四处状态,少清一处列表就会骗人:

| # | 状态 | 位置 |
| 1 | 会话日志目录 | $DSH_HOME/sessions/<编码工作区>/session-<id>/(session.v4.jsonl.zstd + session.lock) |
| 2 | 投影缓存文档 | $DSH_HOME/storages/session_projcache/sessions/<id>.json |
| 3 | 归档索引 | workspace registry 的 archivedSessionIds |
| 4 | 回收站(本插件自建) | $DSH_HOME/.archived-sessions-quarantine/<id>/ |

删除有两条路径,界面上是两个分开的按钮:

- 删除(默认):把 1、2 移动进 4,再清 3。面板顶部常显「回收站:N 个 · X MB」,可以逐条恢复或整体清空;30 天后自动清除。
- 永久删除:只出现在二次确认里,直接 unlink,不进回收站。

第 3 步走官方 ctx.workspaceRegistry.unarchiveSession()——它刻意不校验会话是否存在,正好能把「只剩索引」的残留一起清掉,并借官方广播刷新所有已连接的客户端。

安全边界

- 只接受已归档的 id(not-archived 直接拒绝),不碰未归档会话;
- agent.status === "running" 的会话拒绝删除(正在生成的回合不能丢日志),空闲但打开的会话可删;
- 有正在运行的 subagent 后代时拒绝删除父会话(否则子会话的父会话会在它脚下消失);
- 索引残留没有 payload 就不进回收站:一个只有索引、磁盘上什么都没有的 id,清掉即可,不该在回收站里留一个永远恢复不了的条目;
- 路由仅限回环地址 + 同源 + 自定义标头:Origin 存在时必须与 Host 匹配;浏览器对同源 GET 不发 Origin,该情形由 sec-fetch-site: same-origin 兜底,两个信号都没有则拒绝;
- 宿主若失去 workspaceRegistry.unarchiveSession,破坏性路由直接拒绝(no-registry),不会「删了文件但留下索引」这种假成功。

删除后的残留清理

会话被归档时如果它的 agent 还开着(空闲未运行),DSH 的投影服务可能在删除后几秒把 <id>.json 缓存重新写回(日志目录不会复活)。因此每次成功删除都会排三个定时清扫(+3s / +15s / +60s),再次清掉这类残留。
清扫只删缓存文档,且要求该会话既没回到归档集合、也没在跑、磁盘上也没有会话目录——目录回来了说明会话被恢复了,绝不碰。
定时器在插件卸载时统一取消。

界面

行的事实源是宿主(GET /state:归档集合 + 磁盘 + 投影缓存文档),浏览器的会话摘要只用来把标题和时间显示得更准。
这一点是有意的:索引里还留着、但浏览器没加载过的会话,必须照样成行显示,否则用户根本无从清理。

页面提供:搜索(标题 / 会话 ID / 工作区)、按工作区分组、逐行勾选与全选、每行展开详情(占用空间、创建时间、缓存有无、父/子会话)、「打开文件夹」、行内二次确认、结果就地反馈,以及顶部常显的回收站(恢复最近一个 / 清空)。

导航行与页面标题都是 Archived(英文)/ 已归档(中文)。

宿主路由

| 方法 | 路径 | 说明 |
| GET | /api/dsh-archived/state | 归档行 + 磁盘占用 + 血缘 + 回收站 + 旧聚合探测 |
| POST | /api/dsh-archived/delete | { sessionId, mode: "quarantine" \| "forever" } |
| POST | /api/dsh-archived/delete-all | { mode, ids? };ids 缺省表示清空全部 |
| POST | /api/dsh-archived/restore | { sessionId } 从回收站恢复并重新归档 |
| POST | /api/dsh-archived/empty-quarantine | 彻底清空回收站 |
| POST | /api/dsh-archived/reveal | { sessionId } 在系统文件管理器里打开该会话目录 |

拒绝码:invalid-session-id、not-archived、session-running、subagent-running、no-registry、 busy、not-quarantined、no-artifact、forbidden、internal。

安装

装进 profile(包自带的 bundle patch 会把它挂上)
dsh plugin --profile web add dsh-archived

重启 dsh web,然后 设置 → 已归档

不需要改 profile patch。 DSH 0.2.0-rc.2 已经没有官方的归档设置页,settings.section 的 archived-sessions id 是空的,本插件是它唯一的占用者。

从源码(开发用)

1) 让 profile 能解析到这个包(link: 指向本仓库根目录) ln -s /path/to/repo ~/.dsh/profiles/web/node_modules/dsh-archived ~/.dsh/profiles/web/package.json
"dependencies": { "dsh-archived": "link:/path/to/repo" }
"dsh": { "profile": { "bundles": [ ..., "dsh-archived" ] } }

2) 开发时热重载宿主代码(默认 root: [] 只监听 profile 清单与 patch 文件) ~/.dsh/profiles/web/cordis.patch.yml
- id: hmr
config:
root:
- "/path/to/repo"

dsh-hmr 只监听 profile 清单与 patch 文件;改了 lib/index.js 想立即生效,需要把本仓库根目录加进 hmr.config.root。
客户端半边改动刷新浏览器即可。

兼容性

- 目标宿主:DSH 0.2.0-rc.2(peerDependencies 收敛到 ^0.2.0-rc.1)。
- 不支持 0.1.x:那时官方还有一个归档设置页,需要手动 disabled: true;0.2.0-rc.2 已经没有它了。
代码里的 priority: -1 只是对「还带官方页的宿主」的防御。
- DSH 升级后先跑契约检查,它直接读本机 app.asar,逐条确认插件依赖的接口还在: node tools/compat-check.mjs。

测试

npm test # 宿主离线 + 客户端冒烟 + 宿主契约,都不需要 DSH
node tools/host-smoke.mjs # 宿主半边:临时 $DSH_HOME + 假 ctx,跑完删除/恢复/回收站全流程
node tools/client-smoke.mjs # 客户端半边:桩 React + 桩 fetch
node tools/compat-check.mjs # 宿主契约(读 app.asar)
python3 tools/e2e.py # 真宿主端到端;需要 dsh web 在跑且插件已加载
python3 tools/browser-acceptance.py # 真浏览器;先起无头 Chrome --remote-debugging-port=9333

tools/e2e.py 会建临时会话 → 归档 → 删除(进回收站)→ 恢复 → 永久删除,并核对「其他归档会话与磁盘上的其他会话数量都没变」。

tools/browser-acceptance.py 是纯标准库的 CDP 客户端(自己实现 WebSocket 握手与帧):用本机密钥签出浏览器会话 cookie 注入无头 Chrome,打开 设置 → 已归档,断言导航里只有一个「已归档」入口,然后真的点删除 → 确认 → 恢复最近一个。

tools/rpc.py 是本地 RPC 小工具:用 ~/.dsh/.credentials.yaml 里的浏览器会话密钥现场签 cookie,从终端调用官方 RPC(session/create、workspace/archiveSession 等),测试因此不需要浏览器,也不会碰真实会话。

回滚

1. 从 ~/.dsh/profiles/web/package.json 的 bundles 与 dependencies 里删掉 dsh-archived;或直接 dsh plugin --profile web remove dsh-archived;
2. 删掉 ~/.dsh/profiles/web/node_modules/dsh-archived 软链(源码安装时);
3. ~/.dsh/.archived-sessions-quarantine/ 里可能还有待恢复的会话——确认不需要后手动删除即可。

插件删掉的会话不会回来(forever),或者还躺在回收站里等 30 天过期(默认路径)。

故障排查

dsh plugin add 报 ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION(Lockfile failed supply-chain policy check)。

这不是本插件的问题:pnpm 11 默认给「24 小时内发布的版本」设了闸门,而 profile 的 pnpm-workspace.yaml 里 minimumReleaseAgeExclude 是按「包名@版本」逐条累加的——同一个包名出现两条时只有其中一条生效 (实测是列表里靠前的那条),于是新发布的那个版本永远过不了闸门。报错点名的包可能根本不是你要装的那个。

修法:让每个包名只留一条,把已接受的确切版本用 || 连起来:

minimumReleaseAgeExclude:
- dsh-notifications@1.0.0 || 2.0.0 # ✅ 一条顶两条

minimumReleaseAgeExclude:
- dsh-notifications@1.0.0 # ❌ 这两条同时存在时,
- dsh-notifications@2.0.0 # 2.0.0 那条不生效

⚠️ 只接受确切版本:写成 dsh-notifications@* 或 @^2.0.0 会被直接拒绝(ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE: Use exact versions only.)。

pnpm 每次成功安装后都会往这个列表追加一条「包名@版本」,所以升级几次之后又会出现同名两条——
发现同名时把它们合并成上面那种 || 形式即可。删掉重复行后重跑
dsh plugin --profile <name> add <包名> 就通了。

已知边界

- $DSH_HOME/storages/schedule.json 与 message_feedback.json 里可能残留指向已删会话的引用。
它们是宿主自己的 domain store,外部改写会被内存状态覆盖,本插件不动它们(见 docs/decisions.md)。
- $DSH_HOME/storages/session_projcache.json 是 per-record 布局之前的旧聚合,实测早已停止写入(mtime 远早于 session_projcache/sessions/)。插件只探测并在详情里标注,不写入。
- 无头宿主没有文件管理器:设 DSH_ARCHIVED_OPENER=none,「打开文件夹」只回路径不启动程序。
预览分享卡片长什么样 ↓haotian-lu-prog/dsh-archived
分享 · DeepSeek Harness Hub
haotian-lu-prog/dsh-archived

删除已归档会话并清空磁盘残留,误删可恢复

这是你的仓库?认领条目

回到顶部