← Back to home@YumenoSayuri

dsh-github-sync

把工作区里的 DSH 插件按「干净副本」推送到各自独立的 GitHub 仓库:/git 指令按需触发,平时不注入任何提示词,token 只存本地一次。

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

Introduction

GitHub 同步插件 (@local/dsh-github-sync)

专为 DSH 插件开发者设计:把本地工作区里的插件作为干净副本推送到各自独立的 GitHub 仓库,并给新仓库打上 dsh-plugin topic。


核心设计与特性

  1. 绝对防误触 / 零日常污染(按需触发):

    • 平时不注入任何系统提示词,完全不消耗日常聊天的 Context 和 token。
    • 插件自身绝不自动上传,也没有任何后台轮询/保存即上传的钩子。
    • 必须由人类在聊天框显式输入 /git(例如 /git 或 /git 推送 sticker 插件)才会激活当轮的说明与工具权限。
    • 强同步守卫(Tool Guard):即使 AI 产生了幻觉试图私自调用 github_sync_push,若该轮未收到人类 /git 授权,调用会被底层同步拦截拒绝。这条限制用实测验证过:未授权的会话里调用会直接收到拒绝理由。
  2. 凭据安全(Token 只存一次,且不可能被推上去):

    • GitHub Token 只放在机器级用户目录:%USERPROFILE%\.dsh\github-sync\config.json。
    • 该文件不在任何工作区/仓库里,绝不进 git,所有 DSH 会话和 profile 自动复用。
    • 不要把 token 写进插件目录里的 sync.config.json:那个文件在干净副本范围内,等于把密钥放到待上传目录。若真写了,插件会:① 在状态里告警;② 由密钥扫描把含密钥的文件从干净副本中剔除并列为阻断项,绝不推上去。
    • 推送时通过临时生成的**凭据助手(Credential Helper)**把 token 交给 git,命令行参数、remote URL、报错信息里都不会出现明文(报错统一脱敏)。
    • 密钥扫描覆盖:GitHub 经典/细粒度 token、Slack token、AWS Access Key、sk- 风格密钥、Google API key、私钥文件头。文档里的占位符(如 ghp_xxxx)不会误报。
  3. 干净副本(Clean Copy):

    • 自动排除运行时与非必要文件:.git/、node_modules/、cache/、dist/、build/、coverage/、*.status.json、*.local.json、.env*、_scratch/、_research/ 等。
    • 符号链接不跟随(避免把仓库外的内容复制进去)。
    • 插件若自带 .gitignore 或配置了 include 白名单,一律尊重(include 白名单也不会绕过密钥扫描)。
    • 每次提交的内容都精确等于干净副本:发布时会先把镜像工作树清空、再按干净副本写入,因此不只是"只加不删"——先前误提交过的缓存/状态文件,会在下一次提交里作为删除出现在历史中(可追溯,不是静默改写)。
    • 单文件大小沿用 GitHub 自己的数字,不自定:默认上限 maxFileBytes = 100 MiB,正是 GitHub 的硬上限(官方原文:GitHub blocks files larger than 100 MiB);超过 50 MiB 按 GitHub 的规则给警告但仍可推(官方原文:you will receive a warning from Git)。超过上限是阻断项而非静默漏掉——静默丢文件会发布出一个悄悄坏掉的插件。密钥扫描的范围与上限一致,大文本文件无法靠体积绕过扫描。
  4. 提交署名归你(不是工具的假身份):

    • 提交的作者署名默认按 token 所属账号派生,邮箱用 GitHub 的 noreply 形式 <ID>+<用户名>@users.noreply.github.com:既能关联到你的头像,又不公开真实邮箱。
    • 想用自己的邮箱就配 git.userEmail(git.userName 可选)。前提是该邮箱已在 GitHub 账号里验证过:GitHub 靠邮箱归属提交,没验证过的邮箱会让提交显示成一个没有头像、点不开的陌生人。
    • 注意 git 的性质:写进提交的邮箱是公开的。不想公开就用默认的 noreply 形式。
  5. 标准 git 流程:增量提交、真实历史、普通推送:

    • 每个插件一个常驻本地镜像仓库(clone),存放在 %USERPROFILE%\.dsh\github-sync\mirrors\<仓库名>,.git 保留 → 历史正常累积。
    • 每次发布 = 把干净副本同步进镜像 → 一个真实提交 → 普通 git push(不强制)。git log / git blame / git revert 全部可用。
    • 推送前先 fetch 并以远端 tip 为基准提交,所以别人在 GitHub 网页上的修改、或另一台克隆推上来的提交,都会被保留,你的提交落在它们上面(而不是把它们抹掉)。
    • 无变更不提交:干净副本与远端一致时不产生空提交,结果为 unchanged。
    • 推送被拒绝就如实报错,绝不偷偷强推。拒绝信息会区分「远端有本地没有的提交(竞态,重跑即可)」和「分支受保护(要去改仓库设置)」两种情况。
    • force 保留为显式选项(原本的快照语义):生成一个孤立根提交(干净副本)并强推,远端历史被替换。用途是"这个提交必须从历史里消失"(比如误提交了密钥),不是日常发布。
    • 镜像坏了/想重新对齐远端:删掉该镜像目录即可,下次会自动重建。
  6. 开源协议(按需生成,不替你选):

    • 插件目录里已有 LICENSE / COPYING 等文件时直接沿用。
    • 缺失且配置了 github.license 时,从 GitHub 官方接口 GET /licenses/{key} 取权威正文(不自己编),替换版权行后作为 LICENSE 写进同一个提交。正文会缓存到 licensesDir,之后不再依赖网络。
    • 未配置时只告警:选协议是法律决定,不该由工具替你定。
    • 占位符按各协议实际形态替换(MIT/ISC/BSD 用 [year]/[fullname],Apache-2.0 用 [yyyy],MPL-2.0 无占位符则原样保留)。
  7. 自适应:先判定"这是什么",再套对应规则:

    • 工具会先判断目标是不是 DSH 插件,依据是 package.json 的 dsh.bundle / dsh.client,或关键词含 dsh(cordis.patch.yml 同样算),并把这个判定和它的依据报出来(状态、试运行、/git 说明里都写),所以判断是可见、可反驳的,不靠猜。

    • 不是所有目录都套同一条规则:dsh- 前缀、dsh-plugin topic 只对判定为 DSH 插件的目标生效;普通项目不套用,也不会被打上 DSH 的标签。

    • 工作区扫描只认带 DSH 指纹的目录——这是刻意的:否则它会把你的笔记、图片堆、node_modules 一起当成待发布物。

    • 扫描结果 ≠ 可发布范围(重要)。扫描器只回答"工作区里有什么看起来像 DSH 插件",它不是权限的来源——权限来自你(或正在那个项目里工作的会话)的要求。所以 github_sync_plan / github_sync_push 的 plugins 参数也可以直接写目录路径(相对或绝对),扫描没认出来的目录照样能发布。详见下一条。

  8. 按路径发布:任何目录都能推:

    • plugins 参数接受三种写法,按顺序解析:插件名(仓库名 / 目录名 / 包名)→ 目录路径 → 都失败才报错。
    • 所以 /git 推送 my-app(工作区里同名目录)和 /git 推送 ../my-app(工作区外)都能用。适合自己的普通项目、别的宿主的插件,或任何你不想为了发布而改目录名/加 DSH 标记的东西。
    • 发布流程完全一样:干净副本(排除 node_modules、cache、.env、*.status.json…)、密钥扫描、单文件上限、真实提交与普通推送。唯一的差别是不套 DSH 的命名约定,也不打 dsh-plugin topic(除非你显式要求)。
    • 计划里会明确标注「显式给出的路径,不在扫描结果里」,并且如果这个目录正好是扫描根目录本身,会额外提醒"会把该目录下所有内容当成一个仓库"。
    • 每个目标的仓库名默认取目录名;想改就在配置里写 plugins(键可用请求时的原字符串 / 绝对路径 / 目录名任一):"plugins": { "my-app": { "repo": "my-app-gh", "visibility": "public" } }。
    • 唯一会拒绝的路径是与 DSH 自己的目录(~/.dsh,存着 token、会话记录、发布镜像)相重叠的目录——例如 ~、~/.dsh、~/.dsh/profiles。这是为了保护密钥,不是约定。
  9. 命名约定:dsh- 前缀(建议,不是硬墙):

    • DSH 插件的文件夹建议带 dsh- 前缀,仓库名默认就等于那个文件夹名(文件夹 = 插件在磁盘上的身份)。所以 dsh-sticker/ → 仓库 dsh-sticker。
    • 不合规时默认只警告(naming: "warn"):仓库名会照文件夹名走,在 dsh-plugin 汇总里不太像同类——这是外观问题,不是数据问题,所以不值得拒绝发布。警告里会给出确切的改名建议(例如 nova-preset → dsh-nova-preset)以及改名的代价。
    • 想恢复硬拦就设 naming: "block";naming: "off" 则完全不检查。
    • 任何情况下都不会静默改名——只报告,由你决定。
    • 包名(package.json 的 name)与文件夹名不一致不影响判定:规则只认文件夹。
    • 有意的例外:显式写 plugins.<目录名>.repo 即按你的值走,不再判定合规。
    • 注意:改文件夹名会打断 profile 里按路径建立的 link: 依赖,改完需要在插件管理里重新安装/链接。
  10. 联合投稿(GitHub topics):

  • 默认给判定为 DSH 插件的仓库打上 dsh-plugin topic,全部汇总到 https://github.com/topics/dsh-plugin —— 这就是"联合投稿"的落地方式,不需要任何中心化注册表。
  • 改 github.topics 可加自己的标签;写 [] 表示不打标签;单个插件可用 plugins.<名>.topics 覆盖。
  • topics 页只收录公开仓库:私有仓库打了标签也不会出现在那里。要真正"被联合投稿收录",仓库得是 public。
  • topics 在代码推送成功之后才设置,所以即使打标签失败也不会影响代码上传。

快速配置使用

1. 填写 GitHub 账号与 Token(只需一次)

编辑用户配置文件(不在任何仓库里): %USERPROFILE%\.dsh\github-sync\config.json (插件首次加载会自动生成带注释的模板,所以通常直接打开改就行。)

{
  "github": {
    "owner": "你的GitHub用户名",
    "token": "ghp_xxxx 或 github_pat_xxxx",
    "topics": ["dsh-plugin"],
    "license": "MIT",
    "copyright": "你的署名或组织名"
  }
}

owner 也可以写在插件目录的 sync.config.json 里;用户文件里的空字符串不会覆盖插件目录里已填的值(留空即"未指定",要清空请写 null)。 github.license 留空表示不自动生成(只在状态里告警);github.copyright 留空则用 github.owner。 想让提交署自己的名字/邮箱,再加一段 "git": { "userName": "你的名字", "userEmail": "你的邮箱" };该邮箱要先在 GitHub 账号里验证过才会关联到你的头像。留空则自动用账号的 noreply 邮箱(可关联、且不公开真实邮箱)。

Token 权限要求(依据 GitHub REST 官方文档核实)

经典 Token(Classic)— 只勾 repo 就够:

  • repo 已包含 public_repo,所以勾了它就同时覆盖公开仓库与私有仓库。
  • POST /user/repos(在自己账号下建仓)官方原文:need the public_repo or repo scope to create a public repository, and repo scope to create a private repository. → repo 一个就够。
  • POST /orgs/{org}/repos(在组织下建仓)官方原文同上,但额外要求:The authenticated user must be a member of the organization.
  • repo 同时给到代码读写,所以推送也用它。
  • 勾 repo 时 UI 会自动带上 repo:status、repo_deployment、public_repo、repo:invite,无害,不用管。security_events、delete_repo、write:packages 都不需要。
  • 唯一例外:若某插件目录含 .github/workflows/*.yml,需要额外勾 workflow(授予"添加和更新 Actions 工作流文件")。首次推送到空仓库时不存在"同名同内容的旧分支",那条豁免不成立。

细粒度 Token(Fine-grained):

  • 推送代码:Contents: Read and write。
  • 自动新建仓库:Administration: write 或 Repository creation: write(官方为"满足其一")。
  • 打 topics:Administration: write。
  • 组织启用了 SAML SSO 时,token 需单独授权给该组织,否则 403。

权限不足时不用猜:报错会直接打出 HTTP 状态码与对应提示(401 认证失败 / 403 权限不足 / 404 看不到)。分支保护导致推送被拒时会明确说是分支保护,而不是伪装成别的错误。

2. 在会话中触发(两阶段)

/git 本身只是"去看看",不等于"必须推送":

/git                           → 调查轮:只读。推送工具会被拒绝
/git 看看 sticker 能不能推      → 调查轮(没有推送意图)
/git 推送 dsh-sticker           → 推送轮:这一轮允许上传
/git 先别推送,只看看           → 调查轮(明说"别推"会覆盖掉"推送")

所以典型流程是:先 /git 让我把事实查清并报告,你决定后再 /git 推送 …。这样"看看会推什么"不会被迫变成一次上传,而上传依然只可能发生在你明确要求的当轮——插件永远不会自己上传。

3. 命令与工具说明

  • /git [要求]:注入当轮说明。带推送意图(推送/上传/发布/同步/push…)才是推送轮,否则是只读的调查轮。
  • github_sync_status:只读查看配置、token 来源、git 可用性、目标清单与判定结果(哪些是 DSH 插件、哪些不是)、协议与镜像位置(只读,不联网)。
  • github_sync_plan:试运行(Dry Run),列出将提交/排除的文件与原因、会打的 topic、协议是沿用还是生成。verbose 可看全部排除项(只读,不提交也不推送)。
  • github_sync_push:真正的提交与推送。默认增量提交 + 普通 push;force: true 才会用单次快照替换远端历史。仓库不存在时按 visibility 自动创建,随后打 topics。仅在被 /git 授权的那一轮可用。

常见的几件运维事

想做的事怎么做
只想看会发生什么github_sync_plan(或 github_sync_push 加 dryRun: true),都不提交、不推送
重跑一次被拒的推送直接再推一次:会先 fetch 远端再提交,通常就正常了
某个提交必须从历史里消失github_sync_push 带 force: true(远端历史被替换成单次快照)
镜像状态坏了删掉 %USERPROFILE%\.dsh\github-sync\mirrors\<仓库名>,下次自动重建
某个插件不想带协议该插件目录放自己的 LICENSE(会被沿用),或给该插件设 license: ""

改代码之后:模块缓存(实测结论)

DSH 的 Loader 按模块 URL 缓存,并且不看内容与修改时间:文件内容改掉之后,运行中的进程仍然复用旧模块。实测过两次,结论是:

  • 只改入口文件名不够。 单独把入口改名、模块目录原样不动,宿主依旧跑旧逻辑 —— 因为被 import 的模块 URL 没变。
  • 每个改过内容的文件都必须换 URL。 要么重启 DSH(最省心,推荐),要么把改动的文件/目录一起改名,再重新 enable 一次 bundle。
  • cordis.patch.yml 里的 name 指向入口文件,入口改名时同步更新;package.json 的 exports / files 也要跟着改。

实际操作最省事的流程:改完代码 → 重启 DSH。只有在不能重启时才用改名法。

目录结构

main.mjs                入口:系统提示词闸门 + /git 指令 + 三个工具 + 守卫
core/config.js          配置合并、凭据定位、命名与 topic 规范化、告警
core/discover.js        插件发现 + dsh- 前缀约定(文件夹即仓库名)
core/manifest.js        干净副本清单 + 密钥扫描 + glob/gitignore 匹配
core/license.js         协议检测与生成(正文取自 GitHub 官方接口并缓存)
core/git.js             常驻镜像、增量提交、普通/强制推送、凭据助手
core/github.js          REST:建仓、查仓库、打 topics
core/sync.js            计划 / 执行编排(命名阻断、协议、topics、结果汇总)
core/prompt.js          /git 闸门(按会话、按轮次授权)与工具守卫
sync.config.json        插件自带配置(只放通用默认值)
sync.status.json        运行期诊断(自动生成,永不推送)

(src/、lib/、internal/ 与 index.js、host.mjs、plugin.mjs 都是同一份代码的早期名字:因为上面的模块缓存机制,每次改动都得换 URL,所以历史名被留在了旧代次里。今后请优先重启 DSH,别靠改名。)

发布约定

这个插件自己也遵守它检查别人的那两条约定(/git 的说明里会声明一次):

  1. 有改动就升 package.json 的 version。文件变了而版本没变,插件会在推送结果里警告。
  2. README 末尾保留本节,按版本倒序,每条含版本号与年月日时分。升了版本但 README 里搜不到该版本号,插件同样会警告。

📌 版本历史

v0.4.0(2026-10-07 00:05)

  • 按路径发布:扫描结果不再是可发布范围的上限。github_sync_plan / github_sync_push 的 plugins 参数现在也接受目录路径(相对或绝对),解析顺序是「插件名 → 目录路径 → 报错」。原来只认扫描结果的写法把发现当成了授权,而那两件事毫无关系:扫描器只是便利设施(列一下工作区里有什么),发布权限来自人的要求。
  • 于是自己的普通项目、别的宿主的插件、任何不想为发布而改名或加 DSH 标记的目录都可以直接推;会话本身就在该项目开发环境里时,直接给出目录即可。
  • 显式目标不套 DSH 命名约定、不打 dsh-plugin topic;其余流程完全一致(干净副本、密钥扫描、单文件上限、真实提交与普通推送)。仓库名默认取目录名,可在 plugins 里用「请求原字符串 / 绝对路径 / 目录名」任一键覆盖。
  • 新增一条安全拒绝(不是约定):与 DSH 自己的目录(~/.dsh,含 token、会话记录、发布镜像)相重叠的路径一律拒绝,避免误把凭据推上去。目录恰好是扫描根目录本身时会额外提醒。
  • 修掉两个实现缺陷:①"没有匹配的插件"曾在解析显式路径之前就发出,导致同一个计划里既有正常条目又有假报错;②读取 package.json 复用了扫描器的函数(无 DSH 标记即返回 undefined),导致普通项目的名字与版本读不到(显示 0.0.0)。现在"读清单"与"判定是不是插件"是两件事。
  • /git 说明新增一节「范围不等于扫描结果」。

v0.3.0(2026-10-06 18:40)

  • /git 改成两阶段:/git 本身只进入调查轮(只读),推送工具会被拒绝并说明"要上传请再发一条带推送意图的 /git";只有带推送意图(推送 / 上传 / 发布 / 同步 / push…)才是推送轮。这样"看看会推什么"不再被迫变成一次上传,而上传依然只可能发生在人明确要求的当轮。明说"先别推送/只看"会覆盖掉句中的"推送"。
  • 判定代替一律套用:工具先判定目标是不是 DSH 插件(依据 package.json 的 dsh.bundle / dsh.client,或关键词含 dsh),并把判定结果与依据报在状态、试运行与 /git 说明里;dsh- 前缀与 dsh-plugin topic 只对 DSH 插件生效,普通项目不套用、不打 DSH 标签。
  • dsh- 前缀从硬拦改为默认警告:新增 naming: "warn" | "block" | "off",默认 warn(仍可发布,警告里给出确切改名建议与改名代价);要恢复原来的硬拦设 block。任何情况下都不会静默改名。
  • /git 的说明里新增一节讲清"先判定再套规则",并明确:硬性要求只有两条——不要静默改名,不要静默替人决定协议。
  • 顺带清理:core/discover.js 末尾多余的 export default(同一类 unwrapExports 坑)。

v0.2.2(2026-10-06 17:20)

  • 让"消息工厂"的解析变成可失败但不致命:v0.2.1 改成静态 import { createUserMessage } from '@deepseek-ai/dsh-llm',但实测该裸模块名从插件目录解析不到(宿主的包在 app.asar 内的 /dsh/node_modules/,插件在外部,Node 只会从插件目录往上找 node_modules)。静态 import 解析失败会让整个 bundle 加载失败——/git 闸门与三个工具会一起消失,比原 bug 更严重。
  • 现在是加载期后台尝试 + 失败容忍:能解析就用宿主官方工厂,解析不到就用语义等价的本地实现(deepFreeze(structuredClone({ ...input, role: 'user', id: randomUUID() })),与宿主 createMessage 逐字对应)。两条路径产出的消息都带非空 id,都不会破坏会话回放。
  • 新增测试 ghsync-steer-test.mjs:驱动真实 bundle 与真实 apply(),断言真正进入收件箱的消息具备 id/role/source/content 且被冻结、两次注入 id 不同。故意不桩掉 @deepseek-ai/dsh-llm——"这个包能否解析"正是必须不影响结果的那件事。
  • 顺带记录一个宿主自身的坑:dsh-agent/README.md 里 steer() 的官方示例只写了 content 与 source(没有 id),而 steer(input) 的实现是原样 inbox.splice(input)、不补 id,所以照文档写出来的插件会写出回放时校验失败的会话事件。宿主自己的调用方(dsh-api-session-controller)都是先用 createUserMessage 造消息再 steer,照它做才对。
  • 补齐自动生成的用户配置模板:模板原先只有 github.owner / token / topics,新装的人根本不知道还能配提交署名和开源协议。现在模板同时列出 git.userName / git.userEmail 与 github.license / github.copyright,并注明"留空就按账号派生 noreply 邮箱(可关联头像、不公开真实邮箱)""自填邮箱需先在 GitHub 账号里验证过"。默认值仍安全留白:协议留空 = 不替你选协议,只提醒。
  • 顺带清理:core/prompt.js、core/config.js 末尾多余的 export default(unwrapExports 的 exports.default ?? exports 会因此把具名导出整片遮掉,属同一类坑)。

v0.2.1(2026-10-06 12:36)

  • 修复 /git 注入的消息缺 id,导致会话历史加载失败:/git 之前用裸对象 { role, content, source } 调 agent.steer(),而会话日志按原样记录 user/message,缺少 id 的事件在重启回放时被校验器拒绝——报 session event at seq N lacks an identified message,整个会话的历史加载都会失败(不只是 /git 那一轮)。
  • 现在改用 harness 工厂 createUserMessage(来自 @deepseek-ai/dsh-llm)构造消息,由它生成并冻结 id,与其余用户消息一致。
  • 仅影响 /git 注入的轮次;已写坏的历史事件需要单独修补,插件本身无法回改。

v0.2.0(2026-10-06 11:31)

  • 改用标准 git 流程:每个插件一个常驻本地镜像(%USERPROFILE%\.dsh\github-sync\mirrors\<仓库名>),每次发布是增量提交并普通推送,历史累积、可用 git log/blame/revert;无变更不产生空提交。
  • 推送前先 fetch 并以远端 tip 为基准提交,所以别人在网页或别处推上来的提交会被保留,不会被覆盖。
  • 强推保留为显式选项 force:生成孤立根提交(干净副本)替换远端历史,用于"某个提交必须消失"(例如误提交密钥),不是日常发布。
  • 新增开源协议支持:插件已有 LICENSE 则沿用;缺失且配置了 github.license 时从 GitHub 官方接口取正文(替换版权行、缓存到本地)写进同一提交;未配置只告警——选协议是法律决定,不替用户定。
  • 提交署名归账号:默认按 token 所属账号派生,用 <ID>+<用户名>@users.noreply.github.com,可关联头像且不公开真实邮箱;可用 git.userName/git.userEmail 覆盖。
  • 单文件上限对齐 GitHub 官方数字:默认 maxFileBytes = 100 MiB(官方硬上限),超过 50 MiB 按官方规则警告但可推;超限是阻断项而非静默漏掉。密钥扫描范围与上限一致。
  • 推送被拒时区分竞态(重跑即可)与分支保护(需去仓库设置改),不再笼统报错。
  • 提交内容严格等于干净副本:干净副本里没有的文件会在下次提交中作为删除出现在历史里。
  • 新增上述两条发布约定检查(有改动没升版本 / 升了版本 README 没记)。

v0.1.0(2026-10-05 16:16)

  • 首次发布。把插件作为干净副本推送到独立 GitHub 仓库,单次快照强制推送。
  • /git 单轮授权闸门:平时零注入,插件绝不自动上传,未授权轮的推送工具调用会被硬拦截。
  • token 只存机器级用户目录,经临时凭据助手交给 git,不进命令行、URL 或报错文本;发布前做密钥扫描。
  • dsh- 前缀命名约定(文件夹即仓库名),不合规列为阻断项并给出确切改名建议。
  • 默认给仓库打 dsh-plugin topic(联合投稿汇总页),在代码推送成功后设置。