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

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

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

小夜