# 移动工作区插件（dsh-portable-workspace）

让 DeepSeek Harness 在移动硬盘 / U 盘上创建**完全自包含的便携工作区**：会话、插件、工程文件、交付、日志、账号凭据全部落在盘上；拔盘显示脱机，插盘自动重新识别；换一台装了本插件的电脑即可继续工作。

## 安装

在每台需要读取移动工作区的电脑上，把本目录安装进当前 DSH profile：

```
plugin_manager  install_bundle  <本目录绝对路径>
```

安装后侧栏「全局面板」多出「移动工作区」入口。修改 JS 后需 `set_bundle false → true` 重载（或重启 DSH）。

> 本插件自身是**按电脑安装**的（这是需求明确允许的前提）；移动盘上不需要预装它——打开工作区时，便携实例由本机 DSH 的 CLI 启动。
> 安装用的是 `link:` 软链接（本机 profile 只记一条指向该目录的路径），**所以这个目录不要移动或删除**；换位置需重新 `install_bundle`。
> 完整的分步截图教程见 [docs/移动工作区-图文教程.html](docs/移动工作区-图文教程.html)（单文件、图片内嵌，双击即可打开）。

## 卸载

卸载是**按电脑**进行的，只移除本机 profile 里那条 `link:` 软链接，**不会删除移动盘上的任何数据**；换一台仍装了插件的电脑插上盘照常打开。

1. 先在「移动工作区」面板点「关闭全部便携实例」（或逐个「登出」），避免残留 detached 进程继续占用安装文件。
2. 停用并移除 bundle（顺序不要反）：
   ```
   plugin_manager  set_bundle    dsh-portable-workspace  enabled=false
   plugin_manager  remove_bundle  dsh-portable-workspace
   ```
   `enabled=false` 会触发插件的 dispose 清理钩子，`remove_bundle` 再从 dependencies / `dsh.profile.bundles` 删除该记录；也可在「插件」面板里停用 → 移除。
3. 刷新/重启 DSH，侧栏「移动工作区」入口消失即完成。
4. （可选）删除本机桥梁 `~/.dsh/portable-workspace/roots.json` 以忘掉记住的卷（只记根路径，无工作区数据）；再删除解压出来的插件目录。

> 盘上 `Protable-DSH\` 全部保留。卸载 ≠ 删除工作区；要销毁某个工作区请用面板「删除」（固定文字 + 密码双确认），或卸载后手动删盘上对应目录。用「市场 Discover / 按包名」安装的卸载方式相同，执行第 2 步即可。完整图文见[教程第九节](docs/移动工作区-图文教程.html)。

## 盘上目录约定

用户指定的主目录名保留原拼写 `Protable-DSH`：

```
<盘>:\Protable-DSH\
├─ .dsh-portable.json         根标记（kind/rootId/format），盘符无关识别的唯一桥梁
├─ Protable-Workspaces\      各工作区数据
│  └─ <ws-id>\
│     ├─ workspace.json       清单：名称、密码 scrypt、插件声明、profile、origin
│     ├─ sessions\            会话日志（.jsonl.zstd）
│     ├─ storages\            存储域 / 会话投影缓存 / workspace registry
│     ├─ logs\                launch.log、plugins.log
│     ├─ launch\              cordis.patch.yml、start-portable.cmd、runtime.json
│     └─ ...
├─ Projects\<ws-id>\         工程文件与交付（默认工作目录也指向这里）
└─ support\
   └─ home\                  便携 DSH_HOME：profiles/portable/、.credentials.yaml 等
```

面板里的「初始化这块盘」创建根标记与三个目录；「新建移动工作区」创建工作区，密码可留空。

## 工作原理（为什么 100% 落在盘上）

打开工作区时，插件生成一个 `--patch` 覆盖层并启动**独立的便携实例**：

```
DeepSeek Harness.exe --expose-internals <cli.js> portable \
    --patch <工作区>/launch/cordis.patch.yml --port 0 --no-open
```

- `DSH_HOME` 指向 `<root>/support/home` → profile、附件、storages 全在盘上；
- patch 覆盖 `session-persistence-jsonl.root` / `storage-json.root` → 会话与存储落在工作区目录，多个工作区互不混淆；
- patch 覆盖 `working-directory.defaultDirectory` 与 `workspace-controller.documentsDirectory` → 工程与交付落在 `Projects/<id>`；
- patch 覆盖 `agent-default-model` → `provider: deepseek-account, model: deepseek-flash`，使用「同步本机登录」复制上盘的 DeepSeek 账号 grant，无需另配 API Key；
- 便携实例是 CLI web host（`ELECTRON_RUN_AS_NODE=1`），**不启动 Electron 桌面壳**，因此没有 Electron `userData` 落本地的问题；
- 每个实例用随机端口 + token，在独立窗口打开，不污染本机桌面版。

## 使用流程

1. **初始化 / 扫描**：插盘后点「重新扫描」；盘符变化也能识别（靠根标记文件 + 本机 `roots.json` 里记住的非盘符路径）。拔盘后该卷进入「脱机的盘」区，显示「移动盘未连接」。
2. **打开**：先验证密码（scrypt + 恒定时间比较，错误密码拒绝启动）。创建时的密码框要求输入两次并带「小眼睛」明文查看。通过后检查工作区声明的插件清单：
   - 缺少 → 弹「缺少插件」对话框，区分必需 / 可选，给出「一键安装缺失插件」「连可选插件一起装」「跳过，直接打开」；
   - 安装 = 用随包 pnpm 在便携 profile 里 `add`，并把带 `dsh.bundle` 的包追加进 `dsh.profile.bundles`（否则装了也不会被加载）；
   - 兼容性预检（`engines.dsh` / `@deepseek-ai/*` peerDependencies 与本机 DSH 版本比对）：冲突时列出原因与处理方案，但**不阻断**安装/启动（最终由 Loader 裁决）；
   - 可选插件 / 冲突都允许跳过，装完重开不会二次弹密码框。
   - **解锁态只在 Host 进程内存里，不落盘**：登出、DSH 重启、插件重载、拔盘重扫、换盘符都会让它失效 → 再次打开重新验密码。
3. **同步本机登录 / 退出登录**（可选，单向）：把本机 `.credentials.yaml`、`.anonymous-user-id` 复制到盘上的便携 home，永不从盘回写本机；「退出登录」删掉盘上这两个文件。这样换电脑不必重新登录。
4. **工作区行按钮**：打开 / 登出 / 修改密码（旧密码 + 新密码 + 确认新密码）/ 添加插件 / 迁移 / 删除。删除须**手动输入固定文字**「我已知晓删除该工作区，无法恢复！」并**输入密码**（无密码留空），客户端与 Host 双重校验，防止误删或绕过接口。
5. **添加插件到这块盘**：从 npm 官方源下载安装到移动盘的便携 profile（**不是从本机复制**），成功后自动激活 bundle，重启便携实例生效。内置**短名解析**：市场里的 `dsh-web-all` / `modlens` 会自动补全为真实包名 `@linxin666/dsh-web-all` / `@liustack/modlens`，并在最新版不兼容本机 DSH 时自动选最新兼容版本（如 `@linxin666/dsh-web-all@0.4.14`）；404 时给出「可能需要 scope 前缀」提示。
6. **迁移**：「迁移」对话框可把工作区复制到另一块盘或本地硬盘路径，可勾选「迁移后删除源工作区」。迁移会：
   - 重写 `workspace.json` 的 `rootId` 并记 `origin`；
   - 重写每份会话日志 header 里的绝对 `cwd`，并把会话目录收拢到新 `projectKey(cwd)`（否则 jsonl 后端会判 corrupt）；
   - 同步修正 workspace registry 的默认工作目录；
   - 新路径写入本机索引，重新扫描即可识别（非固定盘符）。

## 跨电脑

在另一台电脑上：插盘 → 装本插件 → 侧栏打开「移动工作区」→ 扫描/识别 → 输入密码 → 打开。前提是该电脑装了 DeepSeek Harness（版本越接近越好）；缺插件会提示并一键安装到**盘上的便携 profile**。会话与工程文件与本机完全无关。

## 工作机制与依赖

每个工作区 = 移动盘上的一份独立 DSH 数据 + 本机 DSH 启动的一个独立 CLI 实例。打开时生成 `--patch` 覆盖层并启动便携实例（见上文「工作原理」）。依赖全部是本机已有之物，**无需额外安装**：

- **本机 DeepSeek Harness**：用它的 CLI（`dsh-desktop-host/lib/cli.js`）、随包 node、随包 pnpm、asar 内的基础 bundles。插件本身**零 DSH 包依赖**，服务都从 `ctx` 运行时取得。
- **npm 官方源可达**：仅「安装插件」时需要联网下载；已装过的插件全在盘上，离线也能开。
- **盘上标记文件** `.dsh-portable.json`：盘符无关识别的桥梁；本机仅残留一个索引 `~/.dsh/portable-workspace/roots.json`（只记路径，不含任何工作区数据）。

## DeepSeek Harness 升级时，插件如何同步更新

结论：**插件不会随 DSH 升级「自动更新」，但也不会被升级删掉，通常升级后照常可用**。分两类看：

| 对象 | 存在哪里 | DSH 升级时 |
| --- | --- | --- |
| 插件本体 `dsh-portable-workspace` | 本机目录，以 `link:` 挂在当前 profile 的 dependencies / bundles 里 | profile 配置持久，升级不会动它；代码在那个目录，**原地保留** |
| 便携实例的基础 bundles（`@deepseek-ai/dsh-base` 等） | 本机 DSH 安装目录的 `app.asar` 内 | 随 DSH 升级自动更新 —— 便携实例每次启动都读**本机当前版本**的 CLI 与 asar |
| 工作区数据（会话 / 工程 / 日志 / 便携 home） | 移动盘 | 完全不受影响，一直在盘上 |
| 工作区依赖的第三方插件 | 移动盘上的便携 profile | 不随 DSH 升级变，版本由你自己添加时决定 |

- **★ 升级前必须先释放文件锁（否则升级会失败）**：便携实例是以 `detached` 独立进程运行的 `DeepSeek Harness.exe`，会长期占用安装目录里的 `DeepSeek Harness.exe` 与 `resources/app.asar`；Windows 不允许覆盖正在运行的可执行文件。升级器把新版下载到 `%LOCALAPPDATA%\@deepseek-aidsh-desktop-updater\pending\` 后，若文件仍被锁住就会卡在安装阶段。正确顺序：
  1. 在「移动工作区」面板点 **「关闭全部便携实例」**（或对每个工作区点「登出」），插件会按命令行扫描并终止所有便携 CLI 及其 recovery 子进程、清空运行态；
  2. **完全退出 DeepSeek Harness 主程序**（含主窗口与托盘）—— 主 GUI / desktop host 本身也会锁住同样的文件，这是升级器的常规要求；
  3. 重新打开 DSH 让升级器安装，或在设置里重试升级；
  4. 升级完成、确认 `DeepSeek Harness.exe` 版本已变（如 `0.2.2-rc.1`）后，再重新打开移动工作区。
  面板会自动检测「待安装升级」与「残留便携实例」并弹出黄色警示条；工具 `stop-all` 可脚本化完成第 1 步。
- **正常升级**：只换掉安装目录里的程序与内置 bundles；插件是软链接，profile 里那条 `link:` 记录还在，重启后自动加载，无需操作。
- **便携实例会自动「跟上」新 DSH**：每次打开工作区都用本机**当前**的 `DeepSeek Harness.exe` + `cli.js` 启动，所以升级后下次打开，便携实例就跑在新版上。
- **要重新验一次密码**：升级/重启 DSH 会清掉进程内的解锁状态（安全设计），重新输一次即可。
- **兼容性**：插件不声明 `engines.dsh` 硬红线，能力都通过公开 Service / `ctx` 取得，一般跨小版本可用。若升级后内部契约变化导致面板报错，把插件目录代码更新到适配版，再 `set_bundle false→true` 重载即可 —— 数据仍在盘上，不丢。
- **版本差异导致的「隐藏插件」**：市场按新版 `engines.dsh` 重新过滤；升级到 ≥0.2.2-rc.1 后，原本被隐藏的 `dsh-web-all 0.4.15` 之类会重新出现。
- **想让插件本体也升级**：把新版文件覆盖到原目录（保持路径不变）→ `set_bundle false→true` 重载（或重启 DSH）；换位置则重新 `install_bundle <新路径>`。整个过程**不需要动移动盘**。

## 已知边界（实测结论，非缺陷）

- **本机 DSH 必须存在**：插件只负责组织数据，运行仍依赖本机安装的 DSH（用其 CLI、随包 node/pnpm）。这是"只要安装了这个移动工作区插件"之外的隐含前提。
- **本机唯一残留 = 一个索引文件** `~/.dsh/portable-workspace/roots.json`：只记录已知移动工作区根路径（用于拔盘后显示脱机、迁移后识别非盘符路径），不含会话/插件/工程/交付/日志。
- **凭据**以明文 YAML 形式随盘携带，请保管好 U 盘；不想带账号就不要点「同步本机登录」。
- **exFAT 无硬链接**：pnpm 只能复制文件，冷装一个带 170 个传递依赖的插件实测约 5 分钟；装过一次后 profile 的 node_modules 留在盘上，下次秒开。
- **兼容性冲突是建议性的**：真正的版本红线由 DSH Loader 在启动时裁决，本插件只提前告知并给处理方案。
- 物理拔插未做真实断电测试（用移除根标记模拟），跨电脑未做双机实测（只有一台机器）；其余 6 条需求均在 H: 真实 U 盘上全流程验证通过。

## 交付内容

```
index.js           Host 半：路由、Agent 工具、工作区 CRUD/加载/迁移/密码/插件清单
client.js          Client 半：侧栏图标 + 主页面 + 各对话框（React slot）
lib/
  asar.js          零依赖 asar 读取（Electron 内用 original-fs）
  compatibility.js 零依赖 semver 子集 + 兼容性预检
  constants.js     常量
  credentials.js   单向账号同步
  fsx.js           文件系统助手
  launcher.js      便携实例进程管理
  names.js         市场短名 → 完整包名解析、兼容版本挑选、404 提示
  process.js       便携实例进程枚举 / 精确过滤 / 树杀（升级前释放文件锁）
  profile.js       profile 脚手架 / 插件清单 / pnpm 安装 / 激活 bundles
  sessions.js      会话日志 cwd 重写（迁移用）
  volumes.js       盘符无关识别
  workspace.js     工作区清单、密码、根标记、迁移、launch patch
locale/{zh,en}.json、icon.svg、cordis.patch.yml、package.json
scripts/           smoke / verify2 / launch-test / set-plugins（零 DSH 依赖验证脚本）
docs/              移动工作区-图文教程.html（单文件、图片内嵌）+ images/（6 张实机截图）
```

`node --check` 全部通过；`plugin_manager list_plugins` 中 `include:portable-workspace` 为 `active`。
