dsh-plugin-syncup:用私有 Gist 在多主机之间同步 DSH 插件清单

前言

DSH 的理念是「一切皆插件」,插件装在哪台主机、装到哪个版本,基本靠每台机器自己维护。当你有桌面、服务器或几台开发机时,要么手动逐台对齐,要么维护一个 everyone 都能读的仓库包清单。前者费事,后者要比对版本、处理冲突,而且仓库的读写权限还要额外管理。

dsh-plugin-syncup 解决这个问题的思路是:清单本身不进版本库,而是存到一个私有的 GitHub Gist,由插件负责推送和拉取。它由 jijijitian 维护,源码在 GitHub 上,当前是 2 stars 的小项目。下面介绍它的定位、安装和典型用法。

这是什么

一句话定位:这是一个 DSH 插件,用来把本机的 profile bundle 清单写入一个 secret GitHub Gist,再从其他主机读回同一份清单,并通过插件页预览冲突后由你决定是否应用。

它做的是「同步清单」,不是「同步配置」或「同步代码」。同步的目标只是插件清单本身(哪个 bundle 启用、什么版本、什么来源),以及一次可选的插件版本刷新。

几个明确的边界:

  • 2.0.0 起只做手动同步,不再启动自动同步、定时同步或插件变化自动同步。
  • 2.2.0 起清单存储从 GitHub 仓库改为私有 Gist。
  • 2.4.0 起按 Gist 名称查找,默认名称 dsh-plugin-sync,不存在时首次推送自动创建。
  • 当前版本 4.0.0,面向 DSH 0.2.1-rc.3 的插件页配置槽位 plugins.bundle.config。
  • 插件本身没有 npm 运行时依赖,许可证是 MIT。

核心能力

从 README 可以核对到的能力:

  1. 在 DSH 的插件页直接修改本插件配置,不需要改代码。
  2. 第一次点击「创建 Gist 并推送」,插件会自动创建私有 Gist。
  3. 其他主机只需要填写同一个 Gist 名称,不用自己建仓库或建 Gist。
  4. 点击「拉取线上清单」只读取和预览,不安装任何东西。
  5. 需要上传本机清单时,手动点击「推送本机清单」。

除此之外还有一个独立的「插件更新」选项卡,绕过清单直接问 npm 和 GitHub 的真实最新版本,并逐项选择安装通道。这一部分后面单独展开。

安装与启用

安装命令来自 README:

plugin_manager action=install_bundle target=github:jijijitian/dsh-plugin-syncup

安装后打开 插件 → 已安装插件 → dsh-plugin-syncup,就能看到配置和手动同步面板。注意每台主机都要先装一次本插件,它不会自动安装自己。

如果从 1.x 升级,或者首次启用页面,需要重启 DSH。原因是 DSH 的 client-modules 会按 Loader 条目缓存包元数据直到进程重启;先装过没有 dsh.client 的 1.0,再装 2.x,页面不会立即出现配置面板,重启一次后才会扫描到新的客户端半。之后普通刷新页面即可。

GitHub Token

Gist 需要 GitHub token,这是插件唯一需要你准备的凭据。

  • 经典 token 需要 gist scope。
  • fine-grained token 对 Gist 的支持有限,README 建议使用 classic token。
  • 在插件页的「GitHub Token」输入框填写即可;token 会保存到 DSH 的凭据服务,不写入插件配置文件。
  • 首次填写时点击「如何获取 Token」,会先显示所需权限和操作步骤,再提供「打开 GitHub Token 页面」的链接,链接已预填描述和 gist 权限。
  • 如果已有环境变量 GITHUB_TOKEN,页面会显示「已配置」,可以留空不修改。

没有 token 时仍然可以读取可访问的 Gist 并预览,但不能创建或推送。

典型用法:第一台主机创建 Gist

配置好 token 之后,第一台主机的操作:

  1. 保持「Gist 名称」为默认的 dsh-plugin-sync。
  2. 点击「创建 Gist 并推送本机清单」。

插件会依次做这几件事:

  1. 创建一个 secret Gist,不是 public。
  2. 写入默认文件名 dsh-plugins.json。
  3. 把本机可移植的 profile bundle 清单写入 hosts.<hostId>。
  4. 把 Gist 名称和 ID 写入本机状态。
  5. 在页面上显示 Gist 名称、ID 和 URL。

之后把 Gist 名称告诉其他主机即可。同一 GitHub 账号下会按名称查找。

典型用法:其他主机拉取与冲突确认

其他主机在「Gist 名称」里填同一个名称,保存配置,然后点击「拉取线上清单」。插件用当前 token 搜索该账号下的同名 Gist;找不到时,首次推送会自动创建。不需要创建仓库,也不需要创建新的 Gist。

点击「拉取线上清单」只读,不装。页面会显示线上主机数、插件记录数,并逐条标出缺失、升级、降级、来源变更、启用、本机独有,以及冲突数量、来源主机和每个条目的推荐选择。

默认冲突规则摘录如下:

情况 默认处理
本机缺失,线上启用 安装线上版本
本机版本较低 升级到线上版本
本机版本较高 保留本机,不降级
版本相同但来源不同 保留本机,避免来源突换
本机已安装但停用,线上启用 保持停用
多台线上主机版本或来源不一致 使用最高版本
只有本机有 保留,可手动选择删除
线上已停用 不自动停用本机

只有选择「使用线上版本」或「删除本机插件」的条目,会在点击「应用同步」时执行。预览快照保留 10 分钟,过期后需要重新拉取。

删除是不可逆操作,会调用官方 ctx.pluginManager.removeBundle() 卸载包并移除 profile 中的选择,重启后生效。所以选中删除后,应用按钮会变成危险色并要求二次确认。由 DSH 安装提供、不可移除的插件只显示「保留」。

应用同步时,Host 对安装、升级、来源变更条目调用 ctx.pluginManager.installBundle(),对启用条目调用 ctx.pluginManager.setBundleEnabled(),对选择删除的本机独有条目调用 ctx.pluginManager.removeBundle()。没有选择的插件不会被自动禁用或删除。单次最多处理 maxInstallsPerRun 项,可能出现 restart-required,按 DSH 提示重启即可。

插件更新选项卡:问包源而不是问清单

Gist 清单只知道「某台主机上次推送的版本」,会落后于真实发布版。README 给的例子:清单里写 dshmarket@1.70.2,npm 上已经是 1.73.3。

插件更新选项卡绕过清单,直接问包源:

来源 查询方式
npm 包 GET registry.npmjs.org/<name> 的 dist-tags.latest,缺失时取 versions 里最大的版本号
GitHub 源码 GET api.github.com/repos/.../releases,取最新的正式 release
npm 包 + GitHub 仓库 从 npm 完整元数据的 repository 字段拿到仓库,再查 GitHub 并与 npm 版本比较

GitHub 侧有四条规则:

  1. 以 release 为准,而不是 tag。只有成功读到 release 列表且确实为空时才退回 tag,并在界面标注「GitHub tag(无 release)」。请求本身失败会明确报错,不会悄悄改用 tag。
  2. 按发布时间取最新,而不是版本号最大。默认按 release 的 published_at 取最新;拿不到时间戳时才退回版本号比较。
  3. 只取正式版,过滤平台构建。draft、prerelease: true、带预发布后缀的版本、带 + 构建元数据的平台版本都跳过。
  4. 版本相同则用 npm。只有 GitHub 版本严格更高时才选 GitHub。

npm 规格的包才会自动补查 GitHub;repository 不存在或不是 GitHub 时退回只查 npm。npm 查询使用 Accept: application/json,因为 repository 字段只出现在完整元数据里。

自动规则只是默认建议。列表里每个可更新项都带下拉框,可以逐项指定「自动」「npm」「GitHub」或「不更新」。某一通道没有可用版本时,对应选项置灰并显示「无」。流程是:

  1. 点「检查最新版本」,日志逐条实时输出,每查完一个插件立刻写一行,进度条同步前进。
  2. 勾选要更新的插件,并按需为每项选择通道,可用「选中全部」「清除选择」批量操作。
  3. 点「更新选中项」,只对选中的插件重新检测并安装,日志开头显示 checking N selected bundle(s)。

file:、link:、https://...tgz 这类本地或打包来源无法查询,会标记为「无法查询」,而不是猜一个版本。

升级已安装插件的保护机制

dsh 0.2.1 的插件管理器在存在 hmr 服务时,只要包已经在 profile 的 dependencies 里,installBundle 就一律返回 already-installed。也就是说没有原地升级这条路,插件页和 dshmarket 都是用「先卸载、再安装」绕过去的。

本插件采用同样的两步,但加了三层保护,因为 removeBundle() 一旦以 bundle-in-use 失败,插件已经被从 dsh.profile.bundles 里移除了,表现为装了但不生效,重启也回不来。

  1. 先探测,不盲目删除。升级前先调 setBundleEnabled(name, false) 探测;如果管理器返回 restart-required,或插件仍是 enabled 状态,就判定 HMR 无法卸载,直接放弃,不执行删除。
  2. 任何删除失败都立刻恢复选中。删除被拒后调 setBundleEnabled(name, true) 把插件放回 profile,不会留下「装但停用」的状态。
  3. 记住哪些插件只能手动更新。探测失败的插件写入 state.manualUpdates,预览里显示「需手动更新」徽标与原因,再次同步时直接跳过,不再重复尝试删除。

完整流程是:照常安装,遇到 already-installed 就记录当前版本;在 manualUpdates 里就直接跳过;探测确认能卸下后写 pendingUpgrades,再 removeBundle() 和 installBundle();删除失败就恢复选中;新版装不上就用旧 spec 装回并提示已回滚;成功或回滚后清除 pendingUpgrades。

两个例外:本插件不会升级或删除自己,因为删除会卸载正在运行的代码;由 DSH 安装提供、removable: false 的插件只显示「保留」。

手动更新步骤

探测失败的插件不会自动升级,而是直接给出一段可复制的终端命令。README 中的示例:

1. Quit DSH completely
2. cd "C:\Users\jht\.dsh\profiles\desktop"
3. "C:\Users\jht\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe" ^
   "C:\Users\jht\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\pnpm\bin\pnpm.cjs" ^
   add dsh-mnemon@0.5.36
4. Start DSH again

要点是:命令用的是 DSH 自带的 node 和 pnpm,不依赖系统 PATH,复制即可运行;用 pnpm add 一步完成,不需要先 remove,因为 DSH 关闭后没有 HMR 竞争。profile 目录由 DSH_HOME + DSH_PROFILE 推导,也可以用 DSH_PROFILE_DIR 覆盖。失败项里带 manual 字段,plugin_sync 工具的 apply 结果也会把这些命令打印出来。

错误信息

插件管理器的拒绝大多只带 code,没有 message。插件会把这些 code 翻译成可读句子,避免页面上出现 [object Object]。

code 页面显示
already-installed 该包已在此 profile 中;升级需要先卸载再安装
incompatible-version 该包要求的 DSH 版本与当前宿主不同
not-bundle 该包没有 bundle patch,无法启用
management-required 该 bundle 由 DSH 安装管理,无法从 profile 修改
bundle-in-use HMR 无法在运行中卸载该插件;已保持其启用状态

适用场景与注意

适合的场景:有两三台以上 DSH 主机,希望插件清单基本一致,又不想维护一个公开的仓库清单;或者希望顺带把各主机上的插件版本对齐到真实最新。

需要注意的点:

  • 插件以当前 DSH 进程的权限运行,同步和安装动作本质上与插件页手动操作相同。安装前三思,最好先检查一下插件源码。
  • 插件自身有许可证 MIT,清单文件 dsh-plugins.json 存的是插件名、版本和来源,不包含 token。
  • 它只做手动同步,指望它开机自动对齐是不现实的。
  • 同步安装走 DSH 官方插件管理器 ctx.pluginManager,底层由 DSH 自带的 pnpm 在 profile 目录执行,因此仍然可能遇到 restart-required。
  • 每台主机都要先装一次本插件,它不会自动安装自己。

另外说明一点生态背景:DSH 的社区目录是独立站点,与 DeepSeek、幻方无官方从属关系,本插件属于社区项目。

结尾

dsh-plugin-syncup 的价值在于把「对齐多主机插件清单」这件事收敛到插件页里的一次拉取和应用,冲突先预览再执行,安装失败有回滚和手动兜底。它不追求自动化,标准化程度也有限,但对手动同步这个场景覆盖得比较完整。

目录页:https://www.skillhub.cn/plugins/jijijitian/dsh-plugin-syncup
源码:https://github.com/jijijitian/dsh-plugin-syncup

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜