dsh-github-sync
用 GitHub 私有仓库在多台机器之间同步 dsh 会话、插件清单与设置。
每个 dsh 实例在仓库里拥有自己的目录(instances/<实例ID>/…),互不重叠——所以两台机器永远不会争抢同一个文件,同步不需要合并、不会静默覆盖:
instances/
laptop-a-7f3c/
manifest.json 这台机器的清单(主机名/分组/统计/工作区)
sessions/<工作区目录名>/<会话ID>/session[.vN].jsonl.zstd
plugins/<profile>/{package.json,cordis.patch.yml,pnpm-lock.yaml,pnpm-workspace.yaml}
settings/settings.yaml 可选,默认关闭
desktop-2a91/
…
与参考实现(weibaohui/dsh-sync)的差异
参考实现以 GitCode 为目标、用 git 二进制 + 影子工作树 + 分支/PR/冲突处理。这个插件面向 GitHub,并做了几处不同的取舍:
| 本插件 | 说明 | |
|---|---|---|
| 传输 | 纯 GitHub REST API(全局 fetch) |
不需要 git 二进制;token 不会落进 .git/config;沙箱/无 git 的机器也能用;走 dsh 启动器已装好的代理策略 |
| 冲突 | 结构上不存在 | 每台机器只写自己的 instances/<ID>/,没有共同文件可争 |
| 增量 | 内容寻址 | 本地算 sha1("blob <len>\0" + bytes),与远端树里的 blob sha 比对,未变的会话不重复上传 |
| PR | 可选 | pullRequest 打开后走「分支 → PR → 可合并则 squash 合并」,冲突则把 PR 留在仓库里 |
| 清单时标 | 不含时间戳 | manifest 对相同输入是确定性的,否则每次定时同步都会产生一个空提交;「何时备份」由 GitHub 的提交时间承担 |
安装
dsh plugin --profile web add github:minghuo/dsh-github-sync
装完重启 dsh web 生效。发布到 npm 之后也可以直接用包名安装:
dsh plugin --profile web add dsh-github-sync
更新到最新提交:dsh plugin --profile web update dsh-github-sync,再重启 dsh web。
使用
- 在 GitHub 建一个私有仓库(插件不代建)。公共仓库会被直接拒绝:会话日志里含本机绝对路径,清单里可能有你的配置。
- 建一个 fine-grained PAT,只授权这个仓库,权限勾
Contents: Read and write(要用 PR 模式再加Pull requests: Read and write)。 - 打开 Web UI → 设置 → GitHub 同步,填仓库(
owner/repo或完整 URL)、分支、令牌,保存。 - 点「验证仓库」确认可写且为私有,再点「立即备份」。
- 在另一台机器上装同一个插件、填同一个仓库,就能在「会话备份」页看到别的机器推上来的内容并一键恢复。
定时自动备份在「概览 → 自动备份」里开启;autoSync 打开后按 intervalMinutes(最小 5 分钟)执行,可选启动时备份一次。
同步内容
三类内容各自独立开关,关掉的那一类既不上传、也不会删除云端已有内容:
| 分组 | 默认 | 内容 | 排除 |
|---|---|---|---|
sessions |
开 | $DSH_HOME/sessions/** 整目录 |
*.tmp、session.lock、.dsh-mkdir* |
plugins |
开 | 各 profile 的 package.json、cordis.patch.yml、pnpm-lock.yaml、pnpm-workspace.yaml |
node_modules/、cordis.yml(启动器每次覆写)、.dsh-market/、.dsh-module-fallback/ |
settings |
关 | $DSH_HOME/settings.yaml |
— |
.credentials.yaml、.anonymous-user-id 之类的机器本地机密从不进入计划,无论开关怎么设。
会话相关功能
- 整目录备份:会话是一批 generation 文件(
session.jsonl.zstd与更新的session.vN.jsonl.zstd可能同时存在,前者是迁移前身、别丢),按字节复制,不做解码/重编码。 - 本机浏览:按工作区分组列出会话数、体积、最近修改时间与最大的日志文件名。
- 远端浏览:列出每台机器备份的工作区与会话数/体积。
- 恢复:整台机器、单个工作区、或先
dryRun预览。恢复前自动把当前状态拍成本地快照(pre-restore-*)。 - 工作区重映射:两台机器上同一个项目路径不同时,可以在恢复时把远端的工作区目录名映射到本机已有工作区,文件就会落到 dsh 真正会扫描的目录里。
- 阅读日志:直接点会话名,把 zstd 日志解压成文本在页面里看(Node ≥ 22.15 提供 zstd;不支持时接口明确返回 501)。
- 本地快照:只存本机(
$DSH_HOME/dsh-github-sync/snapshots/),滚动保留snapshotKeep份,用于快速回滚。恢复前总会自动拍一份(pre-restore-*);推送前是否也拍由「高级 → 推送前先拍本地快照」控制。注意每份快照约等于会话总体积(40 MB 量级),默认「推送前也拍 + 保留 20 份」最坏情况约 900 MB —— 不想付这个代价就关掉推送前快照,或把保留份数调小。
安全
- 令牌只写不回读:接口返回的永远是
hasToken,不是令牌本身;令牌存在 settings 命名空间(role('secret'))或$DSH_HOME/dsh-github-sync/config.json。 - 强制私有仓库:
POST /api/sync每次都会先读仓库元数据,private !== true直接拒绝。 - 改状态的请求校验同源:路由由插件自己注册在回环 web server 上,没有内核
/api那层 cookie 栅栏,因此对PUT/POST做了 Origin/Host 比对。 - 恢复路径受限:工作区目录名必须是
--…--形状的单个路径段,会话日志读取被限制在$DSH_HOME/sessions之下。
HTTP API
路由前缀 /dsh-github-sync/api(同源 fetch,无需额外认证头):
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /status |
实例 ID、配置、上次结果、本地统计、快照数 |
| PUT | /settings |
局部更新设置;token: '' 清除令牌 |
| POST | /verify |
读仓库元数据,判断私有/可推送 |
| POST | /sync |
立即推送(关掉的组不会删远端) |
| GET | /remote |
远端各实例清单(分支提交时间作为「最近备份」) |
| GET | /sessions/local |
本机会话清单 |
| POST | /sessions/restore |
{instanceId, workspace?, map?, overwrite?, dryRun?} |
| GET | /sessions/text |
?local=1&workspace=&session=&file= 或 ?path=<仓库路径> |
| GET | /snapshots |
本地快照列表 |
| POST | /snapshots · /snapshots/restore · /snapshots/delete |
新建 / 恢复 / 删除快照 |
发布
三件事互相独立:代码托管(GitHub 仓库)、可安装(git spec 或 npm 包)、上架(插件市场条目)。
1. 推到 GitHub
# 需要 fine-grained PAT:Contents: write(首次建仓库再加 Administration: write)
GITHUB_TOKEN=github_pat_xxx node scripts/publish-github.mjs repo --owner <你的账号>
脚本走 GitHub REST(不需要 git 远端,也不读凭据管理器),会建仓库、推送工作树、写入 dsh-plugin 等 topics。--dry-run 只打印计划;status 汇报现状与上架条件;--description "<一句话>" 改写已存在仓库的描述。
⚠️ 脚本按工作树内容直接建提交,不读本地 git 历史。别在工作树有未提交改动时混用两条路,否则远端会多出一个本地没有的提交(
git fetch && git reset --hard origin/main可对齐)。日常用git push,脚本只负责 topics / 描述 / 上架。
环境里 git 可用时,普通 git push 也一样:
git remote add origin https://github.com/<你的账号>/dsh-github-sync.git
git push -u origin main
2. 让它可安装
两条路,不必都做:
- git spec(零发布成本):
dsh plugin --profile web add github:<账号>/dsh-github-sync - npm:
npm version patch && git push --follow-tags,.github/workflows/publish.yml会在v*tag 上发布(需要仓库 secretNPM_TOKEN),之后dsh plugin --profile web add dsh-github-sync即可。
3. 上架到插件市场
市场目录来自 awesome-dsh-plugin:一个插件一个文件 data/plugins/<owner>__<repo>.yml。本仓库的条目在 docs/registry-entry.yml:
node scripts/publish-github.mjs registry # fork → 分支 → 条目 → PR
CI 会检查「仓库创建满 1 天」,所以仓库刚建的当天提交会被自动拒绝——脚本会算好还差几小时并拒绝执行,避免白发一个 PR。另外要求仓库带 dsh-plugin topic、package.json 声明 dsh.bundle(本仓库已有)。
开发
npm test # node:test,含一个把 GitHub 换成内存实现的端到端集成测试
npm run build:client # 由 client/index.js 生成 client/bundle.js
npm run check # 两个入口的语法检查
client/bundle.js 是提交进仓库的构建产物(git 安装不会跑构建),CI 会重新构建并断言它与 client/index.js 一致。
- 宿主半边是 ESM:
export { name, inject, apply },apply(ctx, config)里用ctx.effect包住ctx.webServer.register。 - 客户端半边由
scripts/build-client.mjs包成window.__ModuleLoader__.load({ id, factory }),id 必须等于包名,exports["./client"]指向构建产物。 - 测试不需要网络:
test/fake-github.mjs是一个内存 GitHub,test/plugin.test.mjs用假的 req/res 驱动真实路由。
已知限制
- 跨机恢复不会改写会话 header 里的
cwd。dsh 的list()只看目录,所以会话会出现在映射后的工作区下;但日志体里的绝对路径仍是原机器的。要做到无损,恢复时得解压/改序化/重压缩每一帧——那会破坏「源文件字节不变」的格式契约,因此没做。 $DSH_HOME/storages/workspace.json(工作区注册表 / 归档会话 id)不参与同步。它是机器本地的,覆盖它会影响本机侧栏;需要时可手工处理。- 附件(
$DSH_HOME/attachments/)不在同步范围内。 - GitHub 单个 blob 上限 100 MB,默认跳过超过
maxFileMb(45 MB)的单个文件并在结果里报告。 - 会话目录树很大时,GitHub 的 tree 接口可能返回
truncated,此时只做增量比对(插件会在结果里标注)。
与遗留的 ~/.dsh git 仓库并存
如果这台机器的 $DSH_HOME 曾经被某个旧同步插件初始化成 git 仓库(.git + 自动生成的 .gitignore),本插件不会碰它:它只使用 GitHub REST API,所有自身状态都在 $DSH_HOME/dsh-github-sync/ 下。两条链路可以并存;想彻底退役旧链路,需要手工删除 $DSH_HOME/.git、$DSH_HOME/.gitignore、$DSH_HOME/dsh-sync.json 与 .dsh-sync.state.json。
License
MIT
No comments yet. Be the first to write one.