← Back to home@Gty2408

dsh-plugin-publisher

Publish a DSH plugin to GitHub, and develop one repository from several machines.

Stars
0
Language
JavaScript
Created
Oct 1, 2026
Updated
Oct 6, 2026
GitHub repo

Introduction

dsh-plugin-publisher

把插件一键上传到 GitHub,并且让多台电脑上的 DSH 协作开发同一个仓库 —— 给 DSH 用的工具,没有界面。

DSH 里注册七个工具,我(agent)直接调用:

工具作用
check_plugin检查插件目录是否符合要求(只读,不联网)
publish_plugin创建 GitHub 仓库 + 上传代码 + 设置 topic
share_project把本机目录变成"协作项目"(写 main,建本机分支)
join_project在另一台电脑加入已有项目(建本机分支 + 装 SSH key)
sync_project日常用的那条:拉 main → 合并 → 推本机分支
merge_project把别的机器分支合并进来(本地三方合并)
project_status报告本机在这个项目里的状态(只读)

用途:把插件推到 GitHub,然后在另一台电脑上直接安装:

dsh plugin --profile desktop add github:<owner>/<repo>

多机协作:怎么用

一句话模型

main              大家商量好的结果,只有 share_project 写它
machine/<机器名>   每台电脑自己的分支,只有它自己能写

一台电脑一个分支,谁也不写别人的分支。 合并发生在本地 git 里, 因为 GitHub 的合并 API 遇到冲突会整体失败、而且不给冲突标记—— 那样 agent 就没法自己修冲突了。

两个人(或两台电脑)各自要做什么

场景需要手动做的事
同一个 GitHub 账号的第二台电脑装插件 + 配 token,然后 join_project
另一个人装插件 + 配 token + 让仓库管理员把你加成 collaborator(share_project 的 collaborator 参数可以直接邀请)

SSH key 不用你管。 插件会为每台机器自动生成一把钥匙并自动装到仓库上。 (需要手动做一次的事只有一件:如果你愿意,可以自己 git clone 一次仓库—— 不 clone 也行,join_project 会直接在本机目录里 git init。)

完整流程

第一台电脑:
  share_project   directory="C:\...\my-plugin"        ← 建仓库、推 main、建本机分支

第二台电脑:
  join_project    directory="C:\...\my-plugin"        ← 自动装 SSH key、建本机分支、合并 main

之后每台电脑:
  改代码 …
  sync_project    directory="C:\...\my-plugin"        ← 拉 main、合并、推自己的分支

要合别人的活:
  merge_project   directory="C:\...\my-plugin"        ← 默认合进本机分支,检查无误再 into="main"

随时看状态:
  project_status  directory="C:\...\my-plugin"

冲突怎么办

sync_project / merge_project / join_project 遇到冲突会停下来并列出文件名, 不会推任何东西。然后我(agent)去读文件里的标记:

<<<<<<< HEAD
=======
>>>>>>> machine/other

改完、把标记删掉,再跑一次同一条命令就继续了。不需要人来解冲突。

为什么每台机器一把钥匙

原因说明
一把钥匙只能绑一个仓库实测同一个公钥装到第二个仓库会 422,所以钥匙文件按仓库名命名
账号级 key 管理做不了实测 GET /user/keys 返回 404(repo 权限不够),但仓库级 deploy key 可以
没有 ssh-agent实测 ssh-add -l 报 "No such file or directory",所以钥匙不带口令(否则会卡住等输入)
路径必须用正斜杠git 会把 core.sshCommand 里的反斜杠吃掉,报错却长得像认证问题

权限决定传输方式,不是偏好:仓库管理员 → 装 deploy key 走 SSH; 只是 collaborator → 走 HTTPS + token(只有管理员能管仓库的 deploy key)。

安全默认值

  • replace_existing 默认 false:share_project 遇到 main 已有提交会拒绝, 因为覆盖上传会删掉别的机器的工作。
  • 推自己的分支从不加 --force:并发推送会响亮地失败,不会被悄悄覆盖。
  • 只有 share_project 用 --force,而且受上面的覆盖保护拦截。
  • 推 main 之前先查 /compare/{base}...{head},落后就要求先 sync_project。 (这个查询只用来提前警告,合并本身永远是本地三方合并。)

为什么没有界面

这个插件是给 agent 用的。插件由 agent 写,所以也该由 agent 发布——而 工具就是 agent 行动的方式。

早期版本有个设置页,那意味着人要手动复制路径、点按钮——而这一步正是要 消灭的东西。所以设置页连同 lib/client.js 和它需要的一整套路由都删掉了。

结果:插件里没有一行浏览器代码。


安装

dsh plugin --profile desktop add <本目录路径>

需要重启 DSH(新增 bundle 只在启动时读取)。


配置 GitHub token

三选一,按顺序尝试:

方式说明
1. 插件配置profile 的 cordis.patch.yml 里写 token
2. DSH 凭据服务键名 DSH_GITHUB_TOKEN
3. 环境变量DSH_GITHUB_TOKEN 或 GITHUB_TOKEN

token 需要 repo 权限(建仓库 + 推代码)。

推荐用 2 或 3——配置文件里的密钥更容易泄露。

当前状态:token 已存在 DSH 凭据服务里,键名 DSH_GITHUB_TOKEN,不用再配。


用法

检查一个插件

check_plugin  directory="C:\...\my-plugin"

返回逐条检查结果。只读,不联网,不改任何东西。

发布

publish_plugin  directory="C:\...\my-plugin"

通常只需要一个参数。 其余自动推断:

参数默认值
owner从 token 自动查出(顺便验证 token)
repopackage.json 的 name
descriptionpackage.json 的 description
privatefalse(公开)
replace_existingfalse(不覆盖已有代码)
submitfalse(不投稿市场)

发布流程:

1. 检查    ← 本地预检,不改任何东西
2. 验证    ← 用 token 查出你的账号(同时验证 token 有效)
3. 建仓    ← git init + commit + 创建 GitHub 仓库
4. 保护    ← 仓库已有代码?默认拒绝覆盖
5. 上传    ← 整个工作树作为一个 commit
6. 主题    ← 自动设置 dsh-plugin topic

每一步都有反馈,失败会明确说卡在哪、已经完成了什么。


默认不覆盖你的代码

这是这个工具唯一有破坏性的操作,所以默认关掉。

仓库状态行为
全新仓库✅ 直接发布
只有 GitHub 自动生成的初始提交✅ 直接发布(那个提交是空的 README)
已有你的提交❌ 拒绝,什么都不改

被拒绝时会明确告诉你:

the branch "main" already has commits; refusing to replace them. Nothing was changed. Pass replace_existing to overwrite, or choose a different repository name to keep both.

为什么这么设计:上传会替换仓库内容。如果那个仓库里有你在另一台电脑上推的东西,默认覆盖就等于删掉它。

检查在任何文件上传之前,所以拒绝零代价——不会留下半个仓库。

要覆盖就显式传 replace_existing: true,这时会明确警告正在替换。


安装命令给两条

发布成功后会打印两条安装命令:

dsh plugin --profile desktop add github:你/插件名
    (resolved through git; that machine needs git installed)

dsh plugin --profile desktop add https://codeload.github.com/你/插件名/tar.gz/HEAD
    (fetched over HTTPS; needs no git)

为什么给两条:github: 那条需要目标机器装了 git。第二条走 HTTPS,不需要 git。

这台机器实测:github.com 20 次只通 6 次。另一台电脑如果更差,第一条会失败——有备选就不会卡住。

还会额外给一条固定到具体版本的(用 commit SHA),装"确定是这一版"而不是"跟着 HEAD 走"。


上传的两条路

先试 git push  →  网络不通就自动改用 GitHub REST API

为什么需要两条路:实测这台机器

api.github.com:443    稳定,~185ms
github.com:443        不稳定,20 次里只成功 6 次,耗时 1.2–8 秒

git push 走 github.com,所以经常失败。API 走另一个域名,稳定得多。

只有网络类错误才回退。认证失败不回退——那只会用第二个错误掩盖真正的错误。


检查什么

错误级(阻止发布)

检查为什么
dsh.bundle.patch 已声明没有它插件根本无法安装(市场最常见的被拒原因)
patch 文件真实存在
patch 里的 insert id == 包名不一致则模块加载不到
浏览器半注册的 id == 包名不一致则浏览器半永远不加载,且没有任何报错
没有疑似凭据的文件.env / *.pem / id_rsa / .npmrc …
包名全小写npm 拒绝大写
版本号是合法 semver
入口文件存在占位仓库不算
owner / repo 名合法

警告级(不阻止)

keywords 缺 dsh-plugin、没有 README、没有 .gitignore、根目录有超大文件……

最值钱的两条

① 三方标识一致性

插件的身份写在三个地方:

package.json 的 name
cordis.patch.yml 里 insert 的 id
lib/client.js 里 __ModuleLoader__.load({ id })   ← 有浏览器半的插件才有

必须完全一致。 对不上的后果是静默无效——插件看起来装好了,就是什么都不做, 没有任何地方报错。所以是错误级。(本插件没有浏览器半,第三条不适用。)

② 凭据文件扫描

推到公开仓库的密钥,在推送那一刻就已经泄露——事后删提交也来不及。

误报的代价(改个文件名)远低于漏报的代价(token 泄露),所以命中即错误。


已知限制

  • 必须重启 DSH:新增 bundle 只在启动时读取。
  • API 传输单文件上限 40 MB:超大会明确报错并建议用 release asset。
  • repo 权限不能删仓库:实测 DELETE /repos/... 返回 403 (Must have admin rights)。删仓库需要单独的 delete_repo 权限。
  • repo 权限管不了账号级 SSH key:实测 GET /user/keys 返回 404。 仓库级 deploy key 可以(这也是协作走 deploy key 的原因)。
  • API 传输会丢文件模式:符号链接和 100755 权限位在 API 路径上不保留。
  • 二进制文件没法文本合并:冲突时只能二选一,不会自动合。
  • 公开仓库里的草稿是公开可见的:推到公开仓库那一刻就可见了。
  • 投稿市场是可选的(submit: true),默认关闭。市场还要求仓库创建满 1 天。

架构

文件职责
lib/tool.js发布两个工具的定义:JSON Schema、参数校验、结果渲染
lib/preflight.js检查:收录要求 + 三方标识 + 凭据扫描
lib/github.jsGitHub REST:仓库、主题、deploy key、分支比较
lib/transfer.jsAPI 传输:blob → tree → commit → ref
lib/git.js通过 ctx.subprocess 跑 git
lib/publish.js编排发布流程(含双传输回退)
lib/collab.js协作的零件:机器名、分支名、SSH key、URL 解析
lib/collab-sync.js协作的五个流程:join / share / sync / merge / status
lib/collab-tools.js五个协作工具的定义
lib/index.js注册七个工具(只有这一件事)

技术约束(实测得出)

① 插件无法 import '@deepseek-ai/*'

实测 ERR_MODULE_NOT_FOUND(dsh-tools、dsh-credentials 等全部)。 所以一切都通过 ctx 服务访问。

② 工具 schema 是手写的 JSON Schema

一线工具的 defineTool 在 @deepseek-ai/dsh-tools 里,导入不了。 所以参数 schema 直接写成 JSON Schema——tools.register() 接受原始 schema, 只要求 output.render 是函数。

代价:registry 不再帮我们校验参数。所以 execute 自己校验(见 readArgs)。这在这里格外重要——参数写错意味着在用户账号下创建公开仓库。

③ 绝对不要导出 Config(踩过的坑)

Cordis 用 runtime.Config["~standard"].validate(config) 校验插件配置, 要求的是一个 Standard Schema(带 ~standard 属性)。 纯 JSON Schema 没有这个属性,所以导出一个会让 Config["~standard"] 变成 undefined,插件加载直接失败:

fiberPhase: "failed"
Cannot read properties of undefined (reading 'validate')

Standard Schema 通常来自 schema 库(schemastery / zod),而它们都导入不了。 所以不导出 Config(本 profile 里其他能正常工作的插件也都这样), 配置改从 apply 的第二个参数读,并且逐字段自己校验。

④ 品牌字符串运行时是恒等函数

credentialRef() 只是校验后调用 brandString(),而 brandString(v) { return v } ——品牌纯粹是编译期的。所以可以直接传普通字符串给 ctx.credentials。


测试

node tools/publisher-preflight.test.mjs   # 收录要求 + 三方标识 + 凭据扫描(49 项)
node tools/publisher-publish.test.mjs     # 发布流程:顺序、失败、分支、主题(77 项)
node tools/publisher-tool.test.mjs        # 工具契约:schema、参数校验、结果(53 项)
node tools/publisher-overwrite.test.mjs   # 覆盖保护:拒绝、放行、警告(22 项)
node tools/publisher-eval.test.mjs        # 宿主半求值 + 确认无浏览器代码(59 项)
node tools/publisher-collab.test.mjs      # 协作:分支、钥匙、合并、冲突(104 项)
node tools/hermetic-selftest.mjs          # 确认没有任何测试能碰真实账号(28 项)

392 项。 关键断言:

  • 覆盖保护真的会拦,且零代价(拒绝时一个文件都没上传)
  • 只有"自动生成的初始提交"不算已有代码(否则新仓库永远发不出去)
  • 没有导出 Config(导出就会让 Cordis 加载失败)
  • 三方标识不一致会被拦(浏览器半静默失效的那种)
  • 凭据文件会被拦
  • 工具 schema 满足 registry 契约(output.render 必须是函数)
  • 没有任何测试能碰真实账号(这条是扫描其他测试文件得出的,不是自称)
  • 参数自己校验:未知参数、类型错误、空值都被拒绝
  • 没有 token 时报错清楚,并列出所有提供方式
  • 网络失败才回退 API,认证失败不回退
  • 主题设置合并而非覆盖(不会丢掉用户自己的 topic)
  • 插件里没有浏览器代码(window.__ModuleLoader__ 一处都没有)
  • inject 只声明 tools——访问其他服务一律走 ctx.get()
  • 一台机器只写自己的分支(分支名带 machine/ 前缀,且按名字排序)
  • 钥匙文件按仓库命名(一把钥匙只能绑一个仓库,同名会互相覆盖)
  • Windows 路径转成正斜杠(反斜杠会被 git 吃掉,报错像认证问题)
  • 钥匙不带口令(没有 ssh-agent,带口令会卡住等输入)
  • 冲突时停下来列文件名、且一个字节都不推
  • owner 只给一半时宁可丢掉(半对会让流程忽略本来就在那儿的 remote)