# dsh-plugin-archive-manager [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)Web 端插件:让会话归档**可见、可恢复**。 - **原位显示已归档会话**:勾选后,已归档会话重新出现在工作区列表的**原位置**(DSH 底层保留工作区记账槽位),标题比未归档会话**更暗**。 - **视图选项新增"筛选条件"组**:工作区视图选项菜单新增 **筛选条件** 组,含两个可**同时勾选**的项:**已归档** 与 **未归档**(默认勾选未归档)。勾选"已归档"即可看到已归档会话;不勾选"未归档"则只显示已归档会话。 - **行菜单一键取消归档**:已归档会话的操作从"归档会话"变为"**取消归档**"(带专属图标),点击后持久化恢复会话。 - **工作区级归档**:工作区行菜单新增"**归档工作区 / 取消归档**"——归档后整个工作区分组(含其全部会话)隐藏/置灰,受筛选条件控制;取消归档后工作区恢复原样,**各会话自身的归档状态保持不变**(两者相互独立)。 English README: [README.en.md](./README.en.md)。 ## 截图 **视图选项中的"筛选条件"组** —— 工作区视图选项菜单新增 **筛选条件** 组,含两个可**同时勾选**的项:**已归档** 与 **未归档**(默认勾选未归档)。勾选"已归档"即可原位看到已归档会话;不勾选"未归档"则只显示已归档会话。 ![视图选项中的筛选条件——已归档 / 未归档](screenshots/archived_options.png) **行菜单一键取消归档** —— 已归档会话的操作从"归档会话"变为"**取消归档**"(带专属图标),点击后持久化恢复会话,回到原位置。 ![已归档会话行操作:取消归档](screenshots/unarchive.png) ## 为什么需要它 DSH 支持归档会话(归档后从所有分组视图消失),但官方界面**没有任何查看或取消归档的入口**——官方 `dsh-client-ui-workspace` README 原话:*"sessions can be archived, but archived sessions have no viewing or unarchive surface."* 底层数据模型其实为取消归档预留了位置(归档只把 session id 加入全局归档集合,工作区记账槽位不动,官方注释写明 *"unarchiving restores its position"*),只是缺 API 和 UI。本插件补上这一环。 ## 工作原理 一个 npm 包、两个半区: | 半区 | 文件 | 职责 | | --- | --- | --- | | Host(进程端) | `lib/index.js` | 给 `ctx.workspaceRegistry` 补上官方缺失的 `unarchiveSession(sessionId)`(与官方 `archiveSession` 完全对称:幂等、持久化、走同一条 `domain/changed` → `host/archived-sessions-changed` 广播链路);维护**工作区级归档状态**(`$DSH_HOME/storages/dsh-plugin-archive-manager.json`,DSH 本身没有工作区归档概念);注册 4 个 exact HTTP 路由供浏览器半区调用。 | | Browser(浏览器端) | `lib/client.js` | **遮蔽** `sidebar.workspaces` 槽位(优先级 -1,官方为 0;最低优先级渲染者胜出,这是框架官方支持的机制),用忠实复刻的工作区浏览区替换官方组件:头部 + 视图选项、搜索、分组/单列列表、拖拽排序、重命名/分叉/归档、工作区重命名/删除——并叠加筛选组、暗色标题、会话/工作区取消归档操作。 | ### HTTP 路由 | 路由 | 方法 | 请求体 | 说明 | | --- | --- | --- | --- | | `/api/dsh-archive-manager/state` | GET / POST | — | 返回 `{ ok, archivedWorkspaceIds }`(工作区归档集合) | | `/api/dsh-archive-manager/unarchive` | POST | `{ sessionId }` | 会话取消归档,返回最新 `archivedSessionIds` | | `/api/dsh-archive-manager/workspace/archive` | POST | `{ workspaceId }` | 归档整个工作区(幂等),返回最新 `archivedWorkspaceIds` | | `/api/dsh-archive-manager/workspace/unarchive` | POST | `{ workspaceId }` | 取消归档工作区(幂等),返回最新 `archivedWorkspaceIds` | 所有路由要求携带自定义头 `x-dsh-archive-manager: 1` 且 Host 为回环地址。 ### 取消归档的数据流 1. 在已归档会话行上点击 **取消归档**。 2. 浏览器半区同源 `fetch` `POST /api/dsh-archive-manager/unarchive`,携带 `{ sessionId }` 与必需的自定义头 `x-dsh-archive-manager: 1`。 3. Host 路由调用补上的 `unarchiveSession`,通过与官方归档**完全相同**的 `setState` 持久化路径,把该 id 从全局归档集合中移除。 4. 注册表写入触发 `domain/changed`;Host API 网关比对归档集合变化后向**所有已连接客户端**(含其他标签页)广播 `host/archived-sessions-changed`。 5. 客户端运行时更新 `archivedSessionIds`,浏览区自动重渲染——行恢复原色、菜单换回"归档会话"、筛选规则立即生效。 因为变更走的是官方持久化链路,跨重启持久、跨标签页一致,无需自定义 RPC 协议或额外事件管道。 ### 工作区级归档 - **归档工作区**:把该工作区 id 写入插件状态文件(幂等),客户端派生把整个分组(头 + 会话)隐藏;勾选"已归档"时该分组以暗色行出现。 - **取消归档工作区**:从状态文件移除该 id,工作区恢复原样。**各会话自身的归档状态不受影响**(工作区归档与会话归档相互独立)。 - 删除已归档的工作区:只删注册记录,状态文件里的 id 成为无害的孤儿(重新添加同一目录会得到新 id 的新工作区,不受影响)。 ### 查看已归档会话的对话内容 DSH 官方客户端有一条规则:当前会话一旦归档就强制清空对话选择(这是"归档会话无查看入口"设计的一部分),导致点击已归档行会立刻弹回空的新会话视图。本插件禁用了这条规则(对客户端 `WorkspaceRuntime.project()` 投影打了一个小补丁,卸载时自动还原),因此**点击已归档会话可以正常打开并阅读其对话内容**。随之而来的行为变化:归档"当前"会话时不再自动把对话区清回"新会话"——会话保持打开,并在列表中作为暗色行可见。 ### 筛选语义 会话可见当且仅当 `(已归档 && 勾选已归档) || (未归档 && 勾选未归档)`。默认 `未归档: 勾选`、`已归档: 不勾选`——与官方行为完全一致。两项可同时勾选(全部显示);都不勾选则列表为空。筛选状态持久化在插件自己的 `localStorage` 键中,刷新后保留;分组/排序偏好会从官方 `dsh.workspace.view.v5` 键一次性迁移。 ## 安装 插件面向 DSH `0.1.0-rc.x`(在 `0.1.0-rc.6` 上验证)。 ### 从 GitHub 安装(推荐) ```bash npx @deepseek-ai/dsh plugin --profile web add github:piaohua/dsh-plugin-archive-manager # or dsh plugin --profile web add github:piaohua/dsh-plugin-archive-manager ``` ### 从 npm 安装(发布后) ```bash npx @deepseek-ai/dsh plugin --profile web add dsh-plugin-archive-manager ``` 然后**重启 `dsh web`**。插件是一个 profile *bundle*(自带 `cordis.patch.yml` 插入自己的加载行),走 `dsh plugin` 的正常 reconcile 流程。 卸载:`dsh plugin --profile web remove dsh-plugin-archive-manager` 后重启,官方工作区浏览器原样恢复(遮蔽机制天然可逆)。 ## 本地开发 ```bash # 在本仓库内 npm run build # 从 src/ 组装 lib/client.js(内嵌 CSS) npm run verify # 发布前结构检查 node scripts/smoke.mjs # 在桩浏览器环境执行 bundle factory 冒烟测试 # 把本地目录装进 web profile(相对路径会锚定到当前目录) npx @deepseek-ai/dsh plugin --profile web add file:/绝对路径/dsh-plugin-archive-manager ``` ## 包结构 ``` dsh-plugin-archive-manager/ ├── package.json # dsh.bundle.patch + dsh.client 声明 ├── cordis.patch.yml # 组合补丁:插入本插件的加载行 ├── lib/ │ ├── index.js # Host 半区:注册表 unarchiveSession + exact HTTP 路由 │ └── client.js # Browser 半区:__ModuleLoader__ bundle(遮蔽浏览器 + 新 UI) ├── scripts/ │ ├── build.mjs # 从 src/ 组装 lib/client.js │ ├── verify.mjs # 发布前检查 │ └── smoke.mjs # bundle factory 冒烟测试 ├── src/ │ ├── client.src.js # 浏览器 bundle 源码(含 CSS 占位符) │ ├── rows.css # 官方行样式(重打标签,自包含) │ ├── browser.css # 官方浏览区样式(重打标签,自包含) │ ├── picker.css # 官方选择器样式(重打标签,自包含) │ └── extra.css # 插件私有样式(暗色标题) ├── README.en.md # 英文说明 ├── README.md # 本文件(中文,GitHub 默认显示) └── LICENSE # MIT ``` ## 发布 构建产物(`lib/`)已提交入库,因此仓库本身即可通过上文 GitHub 方式安装。如需同时发布到 npm: ```bash npm publish ``` 发布后按上文安装即可。 ## 已知取舍与兼容性 - **浏览区是忠实复刻的重实现,不是官方组件**。DSH 0.1.0-rc.x 的工作区浏览器内部没有可扩展槽位(只有两个目录选择洞),要改其行为,唯一受支持的方式就是遮蔽槽位。实现复刻了官方逻辑与样式(MIT 许可,CSS 重打标签独立注入),视觉与交互一致;但**官方后续对工作区浏览区的改动不会自动流入本插件**——需要跟随 DSH 版本更新插件。 - **"添加工作区"改用 Host 原生目录选择**(`pickDirectory`)+ 路径输入回退,替代官方目录选择洞(该洞由被遮蔽的 entry 声明,本插件无权渲染)。会话空状态(英雄区)的选择器不受影响。 - 已归档会话的行菜单保留 **重命名 / 分叉**(两者对已归档会话仍然有效)。 - 卸载插件后官方浏览器原样恢复;筛选状态存在插件自己的 `localStorage` 键,无残留。 ## License MIT