前言¶
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 可以核对到的能力:
- 在 DSH 的插件页直接修改本插件配置,不需要改代码。
- 第一次点击「创建 Gist 并推送」,插件会自动创建私有 Gist。
- 其他主机只需要填写同一个 Gist 名称,不用自己建仓库或建 Gist。
- 点击「拉取线上清单」只读取和预览,不安装任何东西。
- 需要上传本机清单时,手动点击「推送本机清单」。
除此之外还有一个独立的「插件更新」选项卡,绕过清单直接问 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 需要
gistscope。 - fine-grained token 对 Gist 的支持有限,README 建议使用 classic token。
- 在插件页的「GitHub Token」输入框填写即可;token 会保存到 DSH 的凭据服务,不写入插件配置文件。
- 首次填写时点击「如何获取 Token」,会先显示所需权限和操作步骤,再提供「打开 GitHub Token 页面」的链接,链接已预填描述和
gist权限。 - 如果已有环境变量
GITHUB_TOKEN,页面会显示「已配置」,可以留空不修改。
没有 token 时仍然可以读取可访问的 Gist 并预览,但不能创建或推送。
典型用法:第一台主机创建 Gist¶
配置好 token 之后,第一台主机的操作:
- 保持「Gist 名称」为默认的
dsh-plugin-sync。 - 点击「创建 Gist 并推送本机清单」。
插件会依次做这几件事:
- 创建一个 secret Gist,不是 public。
- 写入默认文件名
dsh-plugins.json。 - 把本机可移植的 profile bundle 清单写入
hosts.<hostId>。 - 把 Gist 名称和 ID 写入本机状态。
- 在页面上显示 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 侧有四条规则:
- 以 release 为准,而不是 tag。只有成功读到 release 列表且确实为空时才退回 tag,并在界面标注「GitHub tag(无 release)」。请求本身失败会明确报错,不会悄悄改用 tag。
- 按发布时间取最新,而不是版本号最大。默认按 release 的
published_at取最新;拿不到时间戳时才退回版本号比较。 - 只取正式版,过滤平台构建。draft、
prerelease: true、带预发布后缀的版本、带+构建元数据的平台版本都跳过。 - 版本相同则用 npm。只有 GitHub 版本严格更高时才选 GitHub。
npm 规格的包才会自动补查 GitHub;repository 不存在或不是 GitHub 时退回只查 npm。npm 查询使用 Accept: application/json,因为 repository 字段只出现在完整元数据里。
自动规则只是默认建议。列表里每个可更新项都带下拉框,可以逐项指定「自动」「npm」「GitHub」或「不更新」。某一通道没有可用版本时,对应选项置灰并显示「无」。流程是:
- 点「检查最新版本」,日志逐条实时输出,每查完一个插件立刻写一行,进度条同步前进。
- 勾选要更新的插件,并按需为每项选择通道,可用「选中全部」「清除选择」批量操作。
- 点「更新选中项」,只对选中的插件重新检测并安装,日志开头显示
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 里移除了,表现为装了但不生效,重启也回不来。
- 先探测,不盲目删除。升级前先调
setBundleEnabled(name, false)探测;如果管理器返回restart-required,或插件仍是enabled状态,就判定 HMR 无法卸载,直接放弃,不执行删除。 - 任何删除失败都立刻恢复选中。删除被拒后调
setBundleEnabled(name, true)把插件放回 profile,不会留下「装但停用」的状态。 - 记住哪些插件只能手动更新。探测失败的插件写入
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