dsh-workspace-sync
Share DSH sessions between machines (Windows ⇄ macOS) through a private git repository. A conversation started on one machine shows up on the other, grouped under the same workspace, and can be continued there — while the repository itself never contains a machine-specific path.
- Sessions are the only thing that syncs. The repository holds session logs plus
three metadata files (
manifest.json,workspaces.json,devices/); a test asserts nothing else can appear in it. - Identity is explicit: a session's header carries
dsh-workspace-sync://<workspaceId>, and each machine keeps its ownworkspaceId → local pathmapping. When an incoming workspace has no counterpart here, the plugin asks (pick a local directory, place it in a default workspace, or ignore it) instead of writing to your disk on its own. - Adoption rewrites only the first frame's
cwd(byte-surgical); the rest of the log is copied byte-identically. - Deletion propagation is bounded twice over: it needs the committed state to be fully adopted locally, and a session this machine did not create is never deleted by it.
- Configured in a runtime-live settings page (native DSH UI components); the
/wssync status|pull|push|mapcommands do the same from the terminal.
跨设备共享 DSH 会话(Windows ⇄ macOS)。一台机器上的对话,在另一台上能看到、归到同一个工作区分组、并且能接着聊——同时同步仓库里不出现任何机器相关的路径。
内核经过实测验证:git 传输、append-only 三方合并、keep-both 冲突保留、绝不 force-push、确认门、错误文本脱敏。
它解决什么
| 问题 | 这一版的做法 |
|---|---|
仓库里带着 D:/code/Article、/Users/x/… 这类机器路径 |
仓库里的会话 header 写成可移植标记 dsh-workspace-sync://<workspaceId>;真实路径只留在各机本地的映射表里 |
| 对端拉过来的会话在本机"看得到但不在工作区分组里" | 采纳时只做帧级手术把 header 的 cwd 换成本机路径(只重压第一帧,正文逐字节不动),再挂进本机工作区 |
| 每台机器的路径不同,不知道怎么对应 | 自动映射:本机已有映射 → 对端路径在本机真实存在 → 根重映射 → 按名字在本机找到同名目录。都对不上时停下来问你:指定一个本机目录、放到默认工作区(~/.dsh/workspaces/<名字>),或者忽略这个工作区 |
| 一台机器删会话会把另一台的一起删掉 | 删除传播要求提交态已全部并入本机(水位 == HEAD),且本机自有会话绝不被对端删除 |
| 配置是一行 21 键的 YAML | 图形设置页,同步方式/时间/工作区映射改完立即生效 |
安装
dsh plugin --profile desktop add link:/path/to/dsh-workspace-sync
仓库里声明了 dsh.bundle(cordis.patch.yml),所以 dsh plugin add 会自己把插件行挂进
profile,不必手写 patch;从市场安装时还会直接用预构建的 tgz,不需要构建授权。
想手写或覆写那一行时(id 覆写会替换整行,所以要的键都要写全):
- id: workspace-sync
name: dsh-workspace-sync
config:
remote: git@github.com:you/your-sessions.git
branch: main
authMode: ssh
syncMode: manual
认证三种模式
authMode |
做法 | 适用 |
|---|---|---|
ssh(默认) |
沿用本机既有的 git/ssh 配置(密钥、~/.ssh/config、代理隧道) |
已经配好 SSH 的机器 |
token |
从 DSH 凭据库取令牌,经 GIT_ASKPASS 喂给 git。配置里只存引用名;令牌不进 YAML、不进 remote URL、不进 argv、也不进磁盘上的脚本(只经子进程环境传递) |
想省事的新机器(尤其 Windows) |
system |
完全交给系统 git 的凭据助手,插件不碰凭据 | 环境已经配好 |
token 模式先存令牌:
# 值存在 ~/.dsh/.credentials.yaml(0600),配置里只引用它的名字
# 名称与 config.tokenRef 一致,默认 DSH_GIT_TOKEN
命令与工具
/wssync status 镜像状态、工作区落位、领先/落后(只读)
/wssync map 列出工作区映射与待确认项
/wssync pull 拉取对端会话、合并、落到本机工作区
/wssync push 镜像本机会话、提交、推送(绝不强推)
模型侧工具同名:wssync_status / wssync_map / wssync_pull / wssync_push。
设置页
Settings → 工作区同步(运行期即时生效):
- 同步方式:仅手动 / 启动时拉取 / 每轮结束推送 / 定时拉取(间隔可调)
- 仓库:远端、分支、认证方式、凭据引用名
- 状态:本机 head、远端 head、领先/落后、是否已全部采纳、上次拉/推时间、问题
- 工作区映射表:共享名、本机路径、来源、会话数、对端路径;可移除映射
- 待确认:给每个共享工作区选定本机目录(可点候选,也可手填)
本机路径(会话库 / 同步工作树 / 默认工作区)可直接编辑——它们决定仓库位置,属于需要 重启的改动,请在 profile patch 里设置。
语义细节(值得知道的)
- 删除会传播,但要求"提交态已全部并入本机"。水位对不上时镜像只补不删—— 这条闸门挡掉的是最坏的一种失败:迁移完(或刚克隆完)本机库是空的,第一次镜像就 把仓库里的会话全删掉。
- 本机自有的会话绝不被对端的缺失删除(在 manifest 里出现过的才算"曾同步过")。
- 真分歧两边都保留:本机版本留在原路径,对端版本写成
<文件名>.remote-fork-<14位时间戳>-<短设备id>,永不自动清理(有专门的测试钉住 这条不变量:就算你后来把那个会话删了,fork 文件仍然留着)。 - 纯追加不算冲突:本机字节是提交态前缀时直接采纳对端,不会制造假冲突。
- flock 租约与迁移暂存文件(
session.lock/session.migration.*.tmp)不参与同步 ——它们是运行时状态,同步它们只会把某台机器的瞬时状态变成另一侧的噪音。
测试
系统里没有 node,用 Electron 的 node:
ELECTRON_RUN_AS_NODE=1 "/Applications/DSH Desktop.app/Contents/MacOS/DSH Desktop" \
--test --test-timeout=90000 test/*.test.mjs
323 个测试,其中十四组跑真实数据 / 完整插件入口 / 真实设置页 / 真实 git 冲突 / 并发推送 / 真实数据端到端彩排 / 真实 git 安全边界 / 无 node_modules 加载:
realdata.test.mjs—— 用本机会话库里的每个真实会话验证"只改第一帧、其余逐字节不变";hostkey.test.mjs—— 用真实会话库核对projectKey/encodeSegment的复刻与宿主一致;e2e-sync.test.mjs—— 两台设备经真 bare 远端跑通推/拉/落位/本地化/续写/删除传播;migrate.test.mjs—— 迁移的计划/执行/对账,以及迁移后的仓库能被引擎原地接上且第一次 push 不会删掉迁移来的会话;plugin-wiring.test.mjs/two-device-wiring.test.mjs—— 在保真宿主仿真里真的执行apply():两台机器各自装配插件、经真 bare 远端互通,并用一个照宿主真实约束实现的 workspace 注册表校验挂载(本地化写错了它会拒绝挂载);auto-modes.test.mjs—— 真的分发turn/end/ 配置重载事件,断言自动同步确实发生;settings-page.test.mjs—— 用一个小型 React 渲染器渲染真实客户端 bundle,点按钮, 把产生的 RPC 喂给宿主真实的处理器,断言仓库/映射真的变了;cross-platform.test.mjs—— Windows ⇄ macOS 双向同步:Windows 端用一份会翻译盘符的 探针,路径逻辑全跑真实的D:\...字符串(文件 I/O 重定向到沙箱)。验的是:双向同步、 cwd 按机器本地化、同一工作区在两台机器上共享同一个身份 id、每台机器每个会话只有一份 日志、采纳后能挂进工作区、仓库里不含任何机器路径;tools-contract.test.mjs—— 工具描述与真实ctx.tools的契约(5 项):形状静态锁 (input.schema、execute()、必填的output、对象级required必须是数组),以及把 四个描述注册到真实 tools 服务上必须全部被接受。这一组是为一次真实事故写的:工具描述 缺output会让register抛错,而apply()抛出会让 cordis 回滚整个插件—— 连设置页的 RPC 路由一起消失,前端只看到HTTP 405;rungit.test.mjs—— git runner 与ctx.subprocess的对接(9 项):文本走readFrom、 二进制走snapshot()、二进制用独立的大上限、截断响亮报错、超时真的终止进程、 外部中止转成可读错误、可执行文件解析失败抛错、认证环境每次现取。这一组是用真实宿主 服务的语义写的(收集模式没有handle.stdout、readFrom只给 utf8),此前那三个 装配测试的替身在这三处撒谎,正好掩盖了"二进制读取被截断"的真 bug;packaging.test.mjs—— 打包与依赖解析:每个外部 import 都已声明、声明的都真被用、files覆盖运行时全部路径、客户端 bundle 的exports形状与dsh.client.platform; 以及动态证明——把插件拷到不含node_modules的目录、用模拟宿主 profile 解析器的 loader hook 加载,全部模块仍然成功(所以 link 安装不需要pnpm install);git-safety.test.mjs—— 安全边界:动词白名单(15 个,含"就是这 15 个"的闸门)、 强推的各种写法被拒、commit --amend被拒;以及事实层面的证明——一次真实的非快进推送 被拒后远端引用一动不动,调和重推后远端历史只前进、两条历史都在;realdata-migration.test.mjs—— 用本机真实存在的旧仓库跑完整流程(数量按实际存在的算, 仓库被清空时该测试自动跳过):拷贝 → 迁移 → 推到 bare 远端 → 一台空会话库的机器全部拉取(每个都用保真注册表校验 cwd 真能挂进工作区) → 正文逐字节一致 → 再推一次不删任何东西。真实数据里带着合成 fixture 没有的东西:v3 与 v4 并存的会话、子会话、Windows 盘符路径、遗留的 fork 文件;push-retry.test.mjs—— 并发推送:push 被拒后自动调和并重推(这条分支在其余所有 测试里都不会走到,因为首次推送总是成功);同一会话两侧都改过时对端版本留成 fork 文件; 反复交替推拉多轮后会话集合收敛、一条不丢;merge.test.mjs/merge-git.test.mjs—— 冲突解决的语义与真实 git 冲突: 双边追加留 fork、对端删除+本机改过保留本机(上游 abort 缺陷的回归锁)、本机删除+ 对端改过采纳对端,并断言每次之后工作树干净、没有留下进行中的合并。
许可
Apache-2.0。传输与合并内核继承自上游 dsh-session-sync(作者 PerryLink)及其 fork
dsh-session-share;本仓库的数据模型(可移植工作区标识)、配置面与设置页是重写的部分。
保留这段署名是许可证(以及诚实)的要求,删掉它会让本仓库变成一份没有出处的衍生作品。
No comments yet. Be the first to write one.