dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
给 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 | 工作区记账位 | workspace registry 里那个工作区的 sessionIds |
| 5 | 回收站(本插件自建) | $DSH_HOME/.archived-sessions-quarantine/<id>/ |
删除有两条路径,界面上是两个分开的按钮:
Workspace.detachSession 摘掉 4。第 3 步走官方 ctx.workspaceRegistry.unarchiveSession()——它刻意不校验会话是否存在,正好能把「只剩索引」的残留一起清掉,
并借官方广播刷新所有已连接的客户端。
但它不是无条件立刻清。 归档只是把 id 放进 archivedSessionIds,而 DSH 的会话列表(session.list)是
「磁盘上的 header + 宿主持有的活会话」的并集——归档并不会让宿主里那个空闲 agent 忘记会话。所以只要宿主还记着它,
第 3 步就先留着(这正是唯一挡住幽灵行的那一处):插件把这条记进 $DSH_HOME/dsh-archived/deferred.json、面板不画它,
用户侧会话立刻消失;等宿主放手(重启必然放手)后自动清掉。永久删除还会额外广播 api-session/removed,
让已经打开的客户端当场收行,而不是等到下一次刷新。
not-archived 直接拒绝),不碰未归档会话;agent.status === "running" 的会话拒绝删除(正在生成的回合不能丢日志),空闲但打开的会话可删;Origin 存在时必须与 Host 匹配;浏览器对同源 GET 不发 Origin,
该情形由 sec-fetch-site: same-origin 兜底,两个信号都没有则拒绝;workspaceRegistry.unarchiveSession,破坏性路由直接拒绝(no-registry),不会「删了文件但留下索引」这种假成功。archivedSessionIds 一空,宿主内存里的活会话就会
重新出现在工作区里,直到重启才消失(0.4.0 的 bug)。永久删除另走官方 Workspace.detachSession 摘掉工作区记账位
(宿主没这个动词时只记日志、不拒绝删除——条目已经挡住幽灵行),并广播 api-session/removed;
回收站路径不摘记账位(恢复要回到原位),也不广播(客户端丢了摘要,恢复后要等下次刷新才画得出来)。Workspace.detachSession;宿主没有这个动词时拒绝删除(no-detach),不会删了文件却留下悬挂行;会话被归档时如果它的 agent 还开着(空闲未运行),DSH 的投影服务可能在删除后几秒把 <id>.json 缓存重新写回
(日志目录不会复活)。因此每次成功删除都会排三个定时清扫(+3s / +15s / +60s),再次清掉这类残留。
清扫只删缓存文档,且要求该会话既没回到归档集合、也没在跑、磁盘上也没有会话目录——目录回来了说明会话被恢复了,绝不碰。
(例外:延迟清理的 id 仍在归档集合里,那正是挡住幽灵行的那一条,所以它继续参与清扫。)
定时器在插件卸载时统一取消。
延迟条目在插件加载时、每次读 GET /state 时、以及宿主自己的每小时 tick 上复查:宿主一旦不再持有该会话
(ctx.sessions.get() 查不到),条目就走官方 unarchiveSession() 清掉,顺带再扫一次缓存残留。
重启后的第一次加载必然把它清干净——所以「重启才消失」的那部分,现在变成了「重启顺手收尾」。
行的事实源是宿主(GET /state:归档集合 + 磁盘 + 投影缓存文档),浏览器的会话摘要只用来把标题和时间显示得更准。
这一点是有意的:索引里还留着、但浏览器没加载过的会话,必须照样成行显示,否则用户根本无从清理。
页面提供:搜索(标题 / 会话 ID / 工作区)、按工作区分组、逐行勾选与全选、每行展开详情 (占用空间、创建时间、缓存有无、父/子会话)、「打开文件夹」、行内二次确认、结果就地反馈, 以及顶部常显的回收站(恢复最近一个 / 清空)。
导航行与页面标题都是 Archived(英文)/ 已归档(中文)。
页面顶部还有一节**「清理陈旧会话 / Clean up stale sessions」:陈旧标准下拉(7 / 14 / 30 天)+「扫描」按钮; 候选行显示标题、原因(陈旧 / 空会话 / 两者)和「N 天未动 · 占用」,勾选后二次确认移入回收站; 底部是「每周自动清理」开关与上次运行结果。这一节只列未归档**会话,扫描本身是只读的。
| 方法 | 路径 | 说明 |
|---|---|---|
| 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 } 在系统文件管理器里打开该会话目录 |
| POST | /api/dsh-archived/sweep/scan | { days? } 陈旧/空会话候选(只读,不写盘) |
| POST | /api/dsh-archived/sweep/delete | { ids, mode } 逐个进回收站 + 官方 detach |
| POST | /api/dsh-archived/sweep/settings | { enabled?, days? } 每周自动清理的开关与天数 |
| POST | /api/dsh-archived/sweep/run | 立刻跑一次每周清理(同一套保护) |
拒绝码:invalid-session-id、not-archived、session-running、subagent-running、no-registry、
archived-session、too-recent、no-detach、
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。
客户端半边改动刷新浏览器即可。
peerDependencies 收敛到 ^0.2.0-rc.1)。disabled: true;0.2.0-rc.2 已经没有它了。
代码里的 priority: -1 只是对「还带官方页的宿主」的防御。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 等),测试因此不需要浏览器,也不会碰真实会话。
~/.dsh/profiles/web/package.json 的 bundles 与 dependencies 里删掉 dsh-archived;
或直接 dsh plugin --profile web remove dsh-archived;~/.dsh/profiles/web/node_modules/dsh-archived 软链(源码安装时);~/.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,「打开文件夹」只回路径不启动程序。