- Home
- Plugins
- Dev & Plugin Tools
- dsh-plugin-syncup
dsh-plugin-syncup
jijijitian/dsh-plugin-syncup
Keeps plugin inventories in sync across machines through a secret GitHub Gist, and updates installed plugins to their newest npm or GitHub release. Push shows a diff first: version conflicts default to the newer side, entries only the Gist has stay by default, and entries only the local machine has are pushed. Each update can pick the npm or the GitHub channel, and every install goes through the plugin manager with a live log.
Install
dsh plugin --profile web add github:jijijitian/dsh-plugin-syncupREADME
dsh-plugin-syncup
通过一个私有 GitHub Gist 在不同主机之间同步 DeepSeek Harness(DSH)的插件清单:
- 在 DSH 的 插件页 直接修改本插件配置;
- 第一次点击 创建 Gist 并推送,插件自动创建私有 Gist;
- 其他主机只需填写同一个 Gist 名称,不需要再建仓库;
- 点击 拉取线上清单,预览冲突和变更,再由你选择是否应用;
- 需要上传本机清单时,手动点击 推送本机清单。
2.0.0 起只做手动同步,不再启动自动同步、定时同步或插件变化自动同步。2.2.0 起清单存储从 GitHub 仓库改为私有 Gist。2.4.0 起按 Gist 名称 查找,默认名称
dsh-plugin-sync,不存在时首次推送自动创建。
当前版本与 DSH 兼容性
本版本面向 DSH 0.2.1-rc.3 的插件页配置槽位:
plugins.bundle.config— 在插件包详情页内嵌本插件的配置和同步面板。
插件本身没有 npm 运行时依赖。DSH 从 0.2.1-rc.2 升到 0.2.1-rc.3 后,旧版的 Host 工具和同步逻辑仍然可用,但旧版没有配置页面;2.x 补上了新页面的配置/预览/冲突处理。
安装
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 的支持有限,建议使用 classic token;
- 在插件页的 GitHub Token 输入框填写即可,token 会保存到 DSH 的凭据服务,不写入插件配置文件;
- 首次填写时点击 如何获取 Token,会先显示所需权限和操作步骤,再提供 打开 GitHub Token 页面 的链接;链接已预填描述和
gist权限; - 如果已有环境变量
GITHUB_TOKEN,页面会显示“已配置”,可以留空不修改。
没有 token 时可以读取可访问的 Gist 并预览,但不能创建或推送。
使用
1. 第一台主机:创建 Gist
在插件页填写 GitHub Token,保持 Gist 名称 为默认的 dsh-plugin-sync,点击 创建 Gist 并推送本机清单。
插件会:
- 创建一个 secret Gist(不是 public gist);
- 写入默认文件名
dsh-plugins.json; - 把本机可移植的 profile bundle 清单写入
hosts.<hostId>; - 把 Gist 名称和 ID 写入本机状态;
- 在页面上显示 Gist 名称、ID 和 URL。
把 Gist 名称告诉其他主机即可(同一 GitHub 账号下会按名称查找)。
2. 其他主机:填写 Gist 名称
在 Gist 名称 中填写同一个名称(默认 dsh-plugin-sync),保存配置,然后点击 拉取线上清单。插件会用当前 token 搜索该账号下同名 Gist;找不到时,首次推送会自动创建。
不需要创建仓库,也不需要创建新的 Gist。
插件更新(第二个选项卡)
Gist 清单只知道“某台主机上次推送的版本”,会落后于真实发布版:例如清单里写 dshmarket@1.70.2,npm 上其实已经是 1.73.3。
插件更新 选项卡绕过清单,直接问包源:
| 来源 | 查询方式 |
|---|---|
npm 包(name 或 name@ver) | GET registry.npmjs.org/<name> 的 dist-tags.latest;缺失时取 versions 里最大的版本号 |
GitHub 源码(github:owner/repo#ref) | GET api.github.com/repos/.../releases,取最新的正式 release;仓库从未发布 release 时才退回 tags |
| npm 包 + GitHub 仓库(自动) | 从 npm 完整元数据的 repository 字段拿到仓库,按上面的规则查 GitHub,再与 npm 版本比较 |
GitHub 侧的四条规则
- 以 Release 为准,而不是 Tag。 有人会先推 tag、过一阵才发布 release(甚至不发布)。直接按 tag 安装会得到
ERR_PNPM_NO_MATCHING_VERSION。因此有 release 时只看 release;只有成功读到 release 列表且确实为空时,才退回 tag,并在界面标注「GitHub tag(无 release)」。- 关键点:release 请求本身失败(限流 403、网络、鉴权)不会被当作“没有 release”。这种情况会明确报错,而不是悄悄改用 tag——否则一旦触发 API 限流就会退回按 tag 更新。
- 按发布时间取最新,而不是版本号最大。 仓库可能先发一个高版本号的构建,之后回退到低版本线继续维护,此时版本号最大的并不是最新代码。默认按 release 的
published_at取最新;无法获得时间戳(如 tag 列表)时才退回版本号比较。 - 只取正式版,并过滤平台构建。
draft跳过;prerelease: true跳过;版本号带预发布后缀(5.20.0-beta.1、0.5.0-alpha4)跳过;带+构建元数据的平台版本(v2.0.0+win、1.0.0+linux-x64)也跳过——它们在 semver 里与基础版本等价,且往往只是某个平台的构建,不代表项目最新。 - 版本相同则用 npm。 只有 GitHub 版本严格更高时才选 GitHub;两边相同则用 npm,因为 npm 安装更可靠(有预构建产物、不会缺
lib/)。
补充:npm 规格的包才会自动补查 GitHub(通过 repository 字段);repository 不存在或不是 GitHub 时退回只查 npm。该字段只出现在完整元数据里,install-v1 精简文档不含它,因此查询使用 Accept: application/json。
实例 1:
dsh-connect-workbuddy的 npm 最新为 5.17.0,仓库里有v5.18.0的 tag 但没有对应 release(最新 release 是 v5.17.0)。旧逻辑按 tag 判定可升到 5.18.0;现在正确判定为 5.17.0,且版本相同故使用 npm。实例 2:
dsh-ops的最新 release 是v0.5.0-alpha4(已标记 prerelease),正式版为v0.4.4。现在正确显示 0.4.4,而不是 alpha。实例 3:
dsh-whale-widget仓库的v2.0.0+win发布于 2026-09-20,而v0.4.2发布于 2026-10-11。按版本号会误选2.0.0,按发布时间并用平台过滤后正确得到 0.4.2。
每个插件单独选择通道
自动规则只是默认建议。列表里每个可更新项都带一个下拉框,可以逐项指定:
| 选项 | 含义 |
|---|---|
| 自动({version}) | 采用上面的自动判定结果 |
| npm({version}) | 强制从 npm 安装该版本 |
| GitHub({version}) | 强制从仓库 release 安装该版本 |
| 不更新 | 跳过该项 |
某一通道没有可用版本时,对应选项会置灰并显示「无」。复选框与下拉框联动:选择「不更新」会自动取消勾选。
流程:
- 点 检查最新版本:日志逐条实时输出,每查完一个插件立刻写一行,进度条同步前进;
- 勾选要更新的插件,并按需为每项选择通道(可用 选中全部 / 清除选择 批量操作);
- 点 更新选中项:只对选中的插件重新检测并安装,进度条与日志只反映这些条目;安装走与同步相同的「探测 → 卸载 → 安装 → 回滚」保护。
关于第 3 步的检测范围:更新是一次只针对选中项的操作,因此不会复用「检查最新版本」得到的全量结果,也不会重新扫描整个 profile。日志开头会显示 checking N selected bundle(s),N 即选中数量。
file:、link:、https://...tgz 这类本地或打包来源无法查询,会标记为「无法查询」而不是猜一个版本。
实测:
dshmarket当前 1.70.2,清单给的是 1.71.1,本选项卡查到的真实最新是 1.73.3。
进度与状态一致性
- 实时日志:检查与更新都按「每个插件一行」写入,不再等全部结束才一次性输出;
- 进度条:显示
已处理 N/总数与当前插件名,进度由 Host 的 job 真实进度驱动; - 离开页面再回来:页面重新挂载时会读取 Host 的
busy状态、当前 job 与上一次检查结果(?cached=1),恢复成实际状态;Host 仍在运行时会禁用按钮,不会因为误点而弹出「已有操作在运行」的报错。
手动更新步骤
HMR 无法卸载的插件(见上一节)不会自动升级,而是直接给出一段可复制的终端命令:
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结果也会把这些命令打印出来。
3. 预览、冲突与确认
点击 拉取线上清单 只读取 Gist,不安装任何东西。页面会显示:
- 线上主机数、插件记录数;
- 缺失、升级、降级、来源变更、启用、本机独有;
- 冲突数量、来源主机和每个条目的推荐选择。
默认冲突规则:
| 情况 | 默认处理 | 可选 |
|---|---|---|
| 本机缺失,线上启用 | 安装线上版本 | 使用线上 / 跳过 |
| 本机版本较低 | 升级到线上版本 | 使用线上 / 保留本机 |
| 本机版本较高 | 保留本机(不降级) | 保留本机 / 使用线上 / 跳过 |
| 版本相同但来源不同 | 保留本机(避免来源突换) | 保留本机 / 使用线上 / 跳过 |
| 本机已安装但停用,线上启用 | 保持停用 | 启用 / 保持停用 |
| 多台线上主机版本或来源不一致 | 使用最高版本 | 使用最高版本 / 保留本机 / 跳过 |
| 只有本机有 | 保留(默认) | 保留本机 / 删除本机插件 |
| 线上已停用 | 不自动停用本机 | — |
只有选择 使用线上版本 或 删除本机插件 的条目会在点击 应用同步 时执行。预览快照保留 10 分钟,过期后重新拉取。
删除是不可逆操作:会调用官方 ctx.pluginManager.removeBundle() 卸载包并移除 profile 中的选择,重启后生效。因此选中删除后,应用按钮会变成危险色并要求二次确认。由 DSH 安装提供、不可移除的插件只显示“保留”。
4. 应用同步
点击 应用同步 后,Host 会:
- 对安装/升级/来源变更条目调用官方
ctx.pluginManager.installBundle(); - 对启用条目调用
ctx.pluginManager.setBundleEnabled(); - 对选择删除的本机独有条目调用官方
ctx.pluginManager.removeBundle(); - 不自动禁用或删除你没选择的插件;
- 最多处理
maxInstallsPerRun项。
可能出现 restart-required,按 DSH 提示重启宿主。
升级已安装插件(重要)
dsh 0.2.1 的插件管理器在存在 hmr 服务时,只要包已经在 profile 的 dependencies 里,installBundle 就一律返回 already-installed(源码守卫在 dsh-plugin-manager 内部)。也就是说没有原地升级这条路,插件页和 dshmarket 都是用「先卸载、再安装」绕过去的。
本插件采用同样的两步,但加了三层保护(bundle-in-use 的教训在这里):
① 先探测,不盲目删除。 removeBundle() 的执行顺序是「先取消选择写盘 → 再尝试热重挂 → 最后检查是否仍在运行」,所以它一旦以 bundle-in-use 失败,插件就已经被从 dsh.profile.bundles 里移除了 —— 表现为装了但不生效,重启也回不来。因此升级前先调用 setBundleEnabled(name,false) 探测:
- 若管理器返回
restart-required,或插件仍是enabled状态 → 判定 HMR 无法卸载,直接放弃,不执行删除; - 只有确认已卸载,才继续走删除。
② 任何删除失败都立刻恢复选中。 删除被拒后调用 setBundleEnabled(name,true) 把插件放回 profile,保证失败不会留下“装但停用”的状态。
③ 记住哪些插件只能手动更新。 探测失败的插件写入 state.manualUpdates,之后:
- 预览里该行显示 需手动更新 徽标与原因;
- 再次同步时直接跳过,不再尝试删除(不会重复折腾)。
完整流程:
- 先照常安装;若返回
already-installed,记录当前已装版本; - 若在
manualUpdates里 → 直接跳过; - 探测:
setBundleEnabled(false),并回读确认是否真的卸下; - 探测失败 → 恢复选中 + 记入
manualUpdates,本次判失败并给出原因; - 探测成功 → 写
pendingUpgrades→removeBundle()→installBundle(); - 删除失败 → 恢复选中(
bundle-in-use同时记入manualUpdates); - 新版装不上 → 用旧 spec 装回,日志提示“已回滚到旧版本”;
- 成功或回滚后清除
pendingUpgrades;成功时清除manualUpdates。
如果进程在第 5 步中途被中断,页面顶部会显示 上次升级未完成,列出插件名与涉及版本。
两个例外:
- 本插件不会升级或删除自己(删除会卸载正在运行的代码)。遇到自己的升级项会直接标记失败并提示去插件页更新。
- 由 DSH 安装提供、
removable: false的插件只显示「保留」。
实测:
dsh-mnemon在这台机器上就属于“HMR 无法卸载”的类型。现在的表现是探测阶段即停手,插件保持启用,状态文件记入manualUpdates,再次同步直接跳过 —— 不会再出现被静默停用的情况。
错误信息
插件管理器的拒绝大多只带 code、没有 message。现在所有失败都会翻译成可读句子,例如:
| code | 页面显示 |
|---|---|
already-installed | 该包已在此 profile 中;升级需要先卸载再安装 |
incompatible-version | 该包要求的 DSH 版本与当前宿主不同 |
not-bundle | 该包没有 bundle patch,无法启用 |
management-required | 该 bundle 由 DSH 安装管理,无法从 profile 修改 |
bundle-in-use | HMR 无法在运行中卸载该插件;已保持其启用状态 |
不会再出现 [object Object]。
安装方式与日志
同步时的安装/删除都走 DSH 官方插件管理器(ctx.pluginManager),底层与插件页安装完全相同:由 DSH 自带的 pnpm 在 profile 目录执行,并写入 package.json / pnpm-lock.yaml。本插件不自己调用 pnpm,也不复制包。
页面底部新增 安装日志 面板:
- 显示当前任务类型(preview / apply / push)、状态与进度(
已完成/总数); - 逐条记录每个插件的动作与结果,例如
installing dsh-context@0.77.0、dsh-context: installed、dsh-orphan: removed、dsh-extra: already installed; replacing 1.0.0 with 2.0.0; - 失败与告警用不同颜色标出,便于定位是哪一项出错;
- 任务运行期间自动刷新,结束后保留最后一次结果。
同一份日志也可以通过 plugin_sync 工具的 log 动作读取。
5. 推送本机清单
点击 推送本机清单 后,插件会先读取 Gist、算出「线上 vs 线下」的差异,这一步不写任何东西;确认后才更新清单文件。
差异分三类,默认规则如下:
| 情况 | 默认处理 | 可选 |
|---|---|---|
| 线上、线下都有(版本或来源不同) | 保留最新版本(比较版本号,平手时保留本机来源) | 取较新版本 / 保留本机 / 保留线上 |
| 线上有、线下没有 | 保留在清单中 | 保留在清单 / 从清单移除 |
| 线下有、线上没有 | 直接推送,不需选择 | — |
- 完全一致的条目会折叠,不占篇幅;
- 选择「从清单移除」会把该插件从所有主机的记录里删掉,其他主机的同一插件也随之消失,请谨慎;
- 推送前后都会重新读取 Gist,若期间有别的机器推过,会基于最新内容重新生成差异再写入(最多重试 3 次);
- 同一主机重新推送只覆盖自己的
hosts.<hostId>记录。
内容没有变化时不会更新 Gist。GitHub 的 Gist 更新接口不接受 If-Match 等条件请求头,发送它会返回 400 Bad Request,所以并发保护靠「读版本 → 比较 → 重新合并重试」。
配置项
插件页只保留两个输入:
| 字段 | 默认 | 说明 |
|---|---|---|
Gist 名称 | dsh-plugin-sync | 默认读取该名称;可自行修改。首次推送找不到时自动创建。同一 GitHub 账号的其他主机填写相同名称 |
GitHub Token | — | 仅写入 DSH 凭据服务;已配置时可留空 |
其余参数使用固定默认值,不再要求填写:
- 文件名:
dsh-plugins.json - 描述:
DSH plugin inventory - 主机标识:主机名
- 单次最多安装:20
- GitHub API:
https://api.github.com
如果从旧版本保留了隐藏字段(文件名、主机标识等),保存配置时会继续保留;页面不再展示。
清单格式
仍然是原来的主机清单,只是存储在 Gist 文件里:
{
"schemaVersion": 1,
"generatedBy": "dsh-plugin-syncup",
"updatedAt": "2026-10-26T00:00:00.000Z",
"lastWriter": "work-pc",
"hosts": {
"work-pc": {
"hostId": "work-pc",
"updatedAt": "2026-10-26T00:00:00.000Z",
"dshVersion": "0.2.1-rc.3",
"profile": "desktop",
"platform": "win32",
"bundles": [
{
"name": "dsh-context",
"spec": "dsh-context@0.74.0",
"version": "0.74.0",
"enabled": true
}
]
}
}
}
从 2.1 的仓库配置迁移
2.2.0 不再读取 repo / branch / path。已有配置里的这些字段在保存配置时会被移除,界面改为 Gist。迁移步骤:
- 更新插件并重启 DSH;
- 在插件页填写 token 来源;
- 保持默认名称
dsh-plugin-sync,或填写已有 Gist 的名称,点击 创建 Gist 并推送; - 其他主机填写同一个 Gist 名称。
安全边界
- Gist 是 secret,但仍然是通过 URL 可访问的;不要把 Gist URL 和 token 一起公开。
- 远端清单是可信输入:它决定安装哪些 npm/GitHub 包,等同于执行第三方代码。请保护 Gist 和 token。
- 只接受可移植 spec,拒绝本机路径、
file:、link:、workspace:、盘符路径和包含..的路径。 - Token 不写入清单、Gist 或插件配置文件;页面输入会写入 DSH 凭据服务。
- 配置 API 只允许修改 Gist 名称,以及把 Token 写入凭据服务;不允许通过页面改
apiBaseUrl/token 本体。 - 只安装用户选择的条目;不自动卸载、禁用或降级本机已有插件。
- 不执行自动同步:没有启动同步、定时器或插件变化监听。
开发与验证
node --test
测试覆盖:
- Gist 读取、创建、更新、ETag 冲突、缺失 Gist、截断内容回退;
- 清单规范化、主机合并、版本选择、主机冲突变体;
- 安装/升级/降级/来源变更/启用/本机独有的预览分类;
- 配置读取、保存、token 隐藏、Gist 名称规范化;
- 预览后按选择安装、默认选择安装、无 token 只读同步;
- 推送、内容不变跳过、非法远端 JSON 不覆盖;
- 本机独有插件的删除流程、不可移除插件的选项收敛;
- 任务日志的进度、逐条消息与终态;
- 已安装插件的卸载后重装升级、失败回滚、self-update 拦截;
- HMR 无法卸载时的探测中止、选中恢复、手动更新记录与重复跳过;
- 只有 code 没有 message 的拒绝被翻译成可读文本;
- 工具结果渲染和客户端注册。
已知边界
- 只同步 bundle 的安装清单,不迁移插件配置、模型或凭据。
- 只处理有 bundle patch 的插件;纯依赖不会成为清单条目。
- 预览快照只保存在 Host 内存中,插件重载或 10 分钟后失效。
- Gist API 没有仓库 Contents API 的强原子更新;本插件用 ETag/版本检查降低并发覆盖窗口,但仍建议各主机避免同时推送。
- 安装依赖 DSH 的 pnpm 路径;缺失时结果会给出
pnpm-missing。 - 删除同样依赖 DSH 的 pnpm 路径;失败会记录在日志与结果里,插件保持原状。
- 日志只保存在 Host 内存(最近 10 个任务、每个最多 400 行),插件重载后清空。
- 只有本机有、且标记为可移除的插件才会出现“删除”选项。
Related plugins
deepseek-harness
deepseek-ai/deepseek-harness
dsh-market
dsh-market/dsh-market
dsh-web (dsh-plugin-manager)
zhu1090093659/dsh-web
dsh-web
zhu1090093659/dsh-web