dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
把 DSH 插件仓库的持续构建与发布搬进应用:侧边栏面板逐仓库显示最近一次工作流运行、已发布与草稿 Release,以及本地检出相对上次发布的领先与未提交情况,并提供触发构建、发布、公开草稿、重跑、取消、查失败日志等按钮。通过本机已登录的 gh CLI 驱动工作流,插件自身不保存任何凭据,被监视的仓库列表放在配置文件里、不打进包。
把 **DSH 插件的持续构建与发布(CI/CD)**搬进 DeepSeek Harness:侧边栏一个图标,点开是一个占满主区域的独立面板。
名字:界面显示名为「DSH 插件发布台」(副标题「持续构建与发布(CI/CD)」;英文界面为
DSH Plugin CI/CD)。包名与仓库 id 保持dsh-plugin-cicd—— 它们只是技术标识,改名会破坏已有安装(profile 的link:路径、bundles 条目、patch 里的 row id、配置文件路径)。
它回答四个 GitHub 页面各自只说了一部分的问题:
package.json 版本、领先/落后提交数、未提交文件数。link: 的本地检出、还是 Release 上的 tgz;差几号版本。也就是说:一个包,两条分发渠道。GitHub Release 给人下载与审阅,npm 让 dsh plugin add <名字> 一条命令装完。
面板上能直接做的动作,行上的顺序就是工作的顺序:提交(git add -A + commit + push)、构建(触发 ci.yml)、发布(触发 release.yml,默认产出草稿)、升版本并发布、公开发布草稿、推送到 npm vX(+ 在面板里写 npm token)、装 Release vX / 更新到 vX、重跑、取消、看失败日志,以及更新之后的 立即重启 DSH。
提交 为什么排在最前面:构建 和 发布 都作用于 GitHub 上的那个提交——workflow 跑的是推送上去的 commit。所以只存在于工作区、或只存在于本机的改动,它们都看不见。面板一直在警告这件事(脏工作区那行、↑N 那个 chip),在这之前唯一的出路是开终端。它永远不是主按钮:它是步骤,不是目的。
提交 一个按钮两种含义,因为两者是同一个陷阱的两半:有改动 → 提交并推送;工作区干净但领先上游 → 只推送(按钮此时显示推送);两者皆无 → 按钮禁用并写明原因。git add -A 是刻意的(发布也需要新增文件),所以确认框会把待提交的文件名列出来——"7 个改动"可能是你要的 3 个加上你没见过的 4 个。提交信息为空时确认按钮保持禁用。
本机 profile 用 link: 指向检出目录,和其它插件一致:
dsh plugin --profile desktop add "link:F:\CodeProj\dsh-plugin-cicd"
装完需要重启 DSH 才会被组合进运行时(宿主半边与浏览器半边都要)。
面板的目标是只用鼠标就能完成日常使用。凡是下表打了勾的,都不需要打开终端:
| 要做的事 | 怎么做 | 需要命令行吗 |
|---|---|---|
| 登录 GitHub | 面板顶部的「用浏览器登录 GitHub」→ 打开授权页 → 输入面板显示的一次性代码 | 不需要(Host 替你跑 gh auth login --web,把码显示出来) |
| 补齐权限 | 同一处的「补齐权限」,走同样的浏览器流程 | 不需要 |
| 添加仓库 | 「管理仓库」→ 从你的仓库列表勾选(本地检出自动匹配) | 不需要 |
| 移除仓库 | 取消勾选 | 不需要 |
| 触发构建 | 仓库那一行的「构建」 | 不需要 |
| 发布(一键:代码推送 + Release + npm) | 仓库那一行的「一键发布」→ 确认框(脏工作区就填提交信息,2FA 就填一次性密码)→「确认发布」 | 不需要(Host 起一条流水线,跟着 CI 走完:升版本、推 npm、公开 Release、B 站播报都由它自己接着做) |
| 只出草稿(不要一键) | 配置里关掉 oneClickRelease,「发布」回到只触发构建 | 不需要 |
| 公开草稿(手动路径) | 展开行 →「公开草稿」→「确认公开?」(两次点击) | 不需要 |
| 给 npm 一个 token | 面板顶部(未登录时才出现)粘贴 Access Token →「写入并验证」 | 不需要(Host 写进 npm 自己读的 ~/.npmrc,插件不保存 token) |
| 单独推送到 npm(手动路径) | 展开行 →「推送到 npm vX」→「确认推送」(两次点击;开了 2FA 就在同一行填一次性密码) | 不需要(Host 用 DSH 自带的 pnpm 发布,复用同一个包管理器) |
| 更新已装的那份 | 行上的「更新到 vX」(只有 profile 装的是更旧的发布包时才有;link: 检出与"装它会降级"两种情况的按钮在展开处)→ 展开处「确认更新」 | 不需要(Host 把 tgz 交给 DSH 自己的插件管理器安装) |
| 让新版生效 | 更新后面板直接问「现在重启?」→「立即重启 DSH」 | 不需要(转发给 dsh-plugin-restart;没装它会明确说) |
| 看失败日志 / 重跑 / 取消 | 展开行里的对应按钮 | 不需要 |
| 账号、权限、仓库清单 | 设置 → 插件 → DSH 插件发布台 → GitHub | 不需要 |
仍然需要命令行的两件事,如实列出,不假装不需要:
git tag vX && git push origin vX 仍是发布之外的动作,面板不做。「更新」装完要重启才生效,这一步面板会主动问(见下文《更新已安装的插件》);重启由 dsh-plugin-restart 执行,发布台自己不碰进程。
「更新」装完要重启才生效,这一步面板会主动问(见下文《更新已安装的插件》);重启由 dsh-plugin-restart 执行,发布台自己不碰进程。
连 gh 都没装时,面板给下载页链接;而安装 gh 这一步在 Windows 上目前仍需一次终端(或用安装包)。
Release 里有两个资产,回答两个不同的问题:
| 资产 | 用途 |
|---|---|
dsh-plugin-cicd-<version>.tgz | 可安装的那份:一条命令装,不用解压、不需要本地检出 |
dsh-plugin-cicd-<version>.zip | 整棵仓库树,给人看/审/做 diff |
SHA256SUMS.txt | 上面两个的校验和(用途是"确认你下载的没坏",不是防篡改——见 RELEASING.md) |
# 1. 下载 tgz(或从 Release 页面直接下)
gh release download v0.1.0 -R SOH4C4759/dsh-plugin-cicd -p '*.tgz'
# 2. 装进你的 profile(把 desktop 换成你自己的)
dsh plugin --profile desktop add "file:$PWD\dsh-plugin-cicd-0.1.0.tgz"
# 3. 重启 DSH,侧边栏出现「发布台」图标
file: 指向本地的 tgz,pnpm 会解包进 profile 的 node_modules。这条路径已经在发布资产上实测过:解包后 scripts/verify-bundle.mjs 与 tests/host-checks.mjs 都能跑通,cordis.patch.yml 与 scripts/configure.mjs 都在包里(npm pack 只带 files 白名单,CI 每次都验这两点)。
上面那三行命令,现在就是面板上一个按钮:「装 Release vX」/「更新到 vX」。它做的正是那三行的前两步,第三步(重启)由面板接着问。
每一行都会先回答"这个 profile 里现在装的是什么",因为版本号相等不代表同一份代码:
| profile 里的依赖 | 面板的判断 | 行上有没有按钮 |
|---|---|---|
link:F:\...(本地检出) | 本地检出 —— 跑的不是发布出去的那份,即使版本号与 Release 完全相同 | 没有。展开处有次要按钮【装 Release vX】(+ 说明会替换掉什么),因为这台机器是开发这份插件的地方,行上的按钮只会把正在改的代码换成 tgz |
file:...\x-1.2.0.tgz,装的是 1.2.0,Release 是 1.3.0 | 可更新 —— 行上出现「可更新 v1.3.0」标记 | 有,【更新到 v1.3.0】——这是行上唯一会被摆出来的安装动作 |
| 装的是 2.0.0,Release 是 1.3.0 | 面板明说装它会往回退,不假装是"更新" | 没有(等于把降级当日常维护)。展开处仍可点,但只是次要按钮 |
为什么把行让出来:面板的行是"你现在要做的事"。对插件作者自己来说,link: 是常态,装 Release 不是要做的事,而是取消掉让开发能进行的那套设置;而 ahead(profile 里更新)那种情况,行上摆按钮等于把降级摆在最显眼的位置。这个能力保留在展开处——"发布出去的那份到底装不装得起来"仍然要能在这里回答——只是不再叫卖。
| 装的是 1.2.0-rc.1 | 两边无法比较,如实说不能比较,而不是猜一个先后 |
| 版本相同、且就是 Release 的 tgz | 已是最新,没有按钮 |
| 不在这个 profile 的依赖里 | 未安装 —— 没有"已装的那份"可以更新,并写出找的是哪个 profile |
| 还没有已发布的 Release(草稿不算) | 没有可装的 |
装的时候:
gh release download <tag> -p <tgz> 把资产下到插件自己的目录 <DSH_HOME>\dsh-plugin-cicd\downloads\(不是临时目录——profile 的依赖会指向这个路径,下完就删会让清单再也装不上)。ctx.get('pluginManager').installBundle),也就是 dsh plugin add 走的同一条 pnpm 路径:它持有 profile 锁,失败时把 package.json / pnpm-lock.yaml 还原。发布台不自己起一个 dsh plugin 子进程——那会是对同一个 profile 的第二个写入者。dsh-plugin-restart;没装这个插件时回一句明确的「没挂载」,而不是一个看不出所以然的失败。一次只跑一个更新(再来一个回 409 busy),因为插件管理器本来就会排队,而两个转圈比一个明确的拒绝更难懂。
没有装它就没有这部份:update 路由需要 Host 提供 pluginManager 服务,没有就回 501 plugin-manager-missing,而不是假装装过。
按下「发布」就是三件事一起做完,因为分开做留下了三种"看起来成功"的半成品:没人公开的草稿、npm 上有版本而 GitHub 上没有 Release、Release 构建的是一个 GitHub 根本没收到过的提交(工作区里的改动、或只提交在本机的改动,两个 workflow 都看不见)。
顺序不是实现细节,是这个功能的全部:
代码推送 → 版本号 → 触发构建 → 等待构建 → npm 推送 → 公开 Release → B 站播报
ci.yml 与 release.yml 跑的都是推上去的那个提交。有改动就 git add -A + commit + push(需要一条提交信息,确认框里填;确认框把改动文件名列出来,因为这是面板上唯一能扫进你没打算发布的东西的动作)。工作区干净但有本地提交就只 push,两者都没有就跳过这一格。release.yml 拒绝复用属于另一个提交的版本,所以流水线先问(releasePreflight),真被占用就自己升一个补丁号、提交、推送。-f draft=true)。草稿只有作者看得见,这正是后面两格能成立的原因。面板上看得见它走到哪一格:七格状态(待做/进行中/完成/跳过/停住/失败/已停止)随 30 秒轮询刷新,gh run 的链接就在旁边。停住不是失败——那一行会给出两条件路:【继续】(补一次性密码,或修好 token 再来)与【跳过 npm 并公开 Release】(npm 本来就不是这个仓库的渠道,或者它暂时连不上,而 Release 不该因此永远不出门)。
不需要 npm 的仓库照样能发:private: true、没有 package.json、名字没占——这些是"不适用"而不是"坏了",那一格记【跳过】然后继续公开 Release。版本已经在 npm 上了同理(目标状态已达成)。
关掉它:oneClickRelease: false 写在 profile 的 cordis.patch.yml 那一行(见《配置》),面板的「发布」就回到只触发构建,npm 与公开草稿又是单独的按钮——某个仓库的 Release 必须一直是草稿时,这是那条退路。
流水线的记录落在 <DSH_HOME>\dsh-plugin-cicd\pipelines.json,"跑到哪一格"就是这个文件回答的:DSH 在构建期间关掉,重启后它会接着查那次运行、继续推 npm、继续公开 Release。这个文件读不出来时一键发布会拒绝启动(而不是覆盖掉一条跑到一半的记录),理由和播报台账一样:那一格里可能有一条已经建好的草稿,丢了就再也没人去公开它。
它不是什么:不是 CI 的替代品(门禁仍然在 workflow 里),也不给 npm 带 provenance——需要可验证构建来源的包应该在 CI 里用 trusted publishing 发(见下节)。
Release 是给人下载的,npm 是给 dsh plugin add 装的。面板把它们并排放:一行上既有 GitHub 的 tag,也有 npm 上的版本。
没有一键发布时(oneClickRelease: false),npm 是独立按钮,不和「发布」绑在一起——推送不可逆,不该搭在另一个动作上顺带发生。一键发布之所以能把它带上,是因为顺序:npm 排在公开 Release 之前,所以拒一次推送不会留下任何公开的东西。
每一行先说清 npm 现在是什么状态:
| 面板看到 | 判定 | 按钮 |
|---|---|---|
| 这个版本已在 npm 上 | 已发布 | 没有按钮(npm 不允许同一版本推第二次) |
| 名字在 npm 上还没人占 | 未注册 | 【推送到 npm vX】——这次就是首次发布 |
| 名字已存在,但这个版本没推过 | 未发布 | 【推送到 npm vX】 |
| 问不到源(离线/被拦) | 未知 | 没有按钮——宁可不给,也不给一个注定失败的 |
推送前会逐条拒绝,且每一条都发生在任何字节上传之前:
private: true —— 作者的决定,不是这里能绕过的(本仓库集里的 dsh-knowledge-console 就是这种)。publish 打包的是工作目录;files 白名单挡不住"白名单目录里的新文件"。gh 同一个原则插件不保存任何 token。它用 DSH 自带的 pnpm(profileContext.packageManager,也就是插件管理器安装时用的那个),凭据来自 npm 自己读的用户级 .npmrc。
gh 那侧不需要教:它自己开浏览器设备码流程。npm 这侧不一样——token 得手动在网站上建,而且那条路的 UI 在 2025 年 11 月变过。所以未登录时面板给的是从零开始的四步,不是一句话:
| 步 | 面板说什么 |
|---|---|
| ① 没有账号 | 「打开 npmjs.com 注册」按钮 + 注册后必须到邮箱点确认链接(未验证邮箱的账号发不出去) |
| ② 建 token | 「打开 token 页面」→ npmjs.com/settings/~/tokens。类型只有 Granular Access Token 一种可选:Classic token 已于 2025-11-19 被 npm 全部撤销,现在也建不出来。Permissions 选 Read and write,Packages 选全部或只勾这个包 |
| ②a 有效期 | 最长 90 天,这是 npm 对可写 token 的硬限制。到期就静默失效,回到第 ② 步再建一个 |
| ②b 2FA | 只有"给 CI 用、没人能输一次性密码"时才勾 Bypass 2FA。在这里推不要勾 |
| ③ 粘贴 | 写进 %USERPROFILE%\.npmrc(就是 npm login 会写的那个文件;其他行原样保留,旧版本留一份 .bak),然后立刻用 whoami 验证 |
| ④ 2FA | 推送的确认行里会出现一次性密码输入框 |
token 不回显、不落插件、不进日志;页面在请求返回的那一刻就清空输入框。token 被拒时,面板直接把排查清单摊在同一块里(复制不全 / 权限不是 Read and write / 已撤销或超过 90 天 / 范围是别的包 / 邮箱未验证),而不是只回一句 ERR_PNPM_WHOAMI_UNAUTHORIZED。
如果 npmjs.com 在你这台机器上打不开——实测过:整站被 Cloudflare 挑战拦下(/、/signup、/settings/~/tokens 一起回 403 Just a moment...,走本地代理也一样)——那就换一台设备或换一个网络(手机流量最省事)生成 token,再粘到第 ③ 步。面板只认这个 token,不在乎它在哪里生成,这一步和你本机的浏览器无关。
同时别再指望命令行:POST registry.npmjs.org/-/user/org.couchdb.user:* 现在回 405 Method Not Allowed,也就是 npm login --auth-type=legacy / pnpm login 这条路已被 npm 移除;而建 token 的 POST /-/npm/v1/tokens 虽然还活着并真在认证(假凭据回 404 User not found),但 npm 公告写明新 classic token 不能再经 API 创建。所以网站(或在另一台设备上打开它)是唯一的路。发布本身不受影响:registry.npmjs.org 一直是通的。
凭据是三个状态,不是一个布尔值(npmAuthState)。whoami 是用户级端点,而 granular token 是包级范围的——它完全可能在 whoami 上被拒、却发布得好好的。把这种拒绝读成"没登录",就会正好挡住引导让用户去建的那种 token,还会对刚做完第 ③ 步的人说"你还没开始"。所以:
| 状态 | 面板 |
|---|---|
signed-in(whoami 答了) | 顶部显示 npm <账号>,不出引导 |
credential-present(.npmrc 里有 token,但 whoami 不确认) | 一句说明 + 原样显示 whoami 的拒绝,照着可以推;真伪由推送来判 |
none(两者都没有) | 四步引导 |
没有答案(/npm-status 未响应) | 什么都不显示——"没有答案"不等于"没有凭据" |
同理,推送的闸门是"有没有可用来认证的东西",不是"whoami 答没答":token 不对的话推送本来就会失败,而失败已经被归类成下一步。
推送失败也归类成下一步做什么,而不是把 npm 的原话丢给用户:otp-required(去填一次性密码)、email-unverified(去点确认链接)、not-logged-in(重建 token)、already-published(先升版本)、payment-required(私有包要付费)、forbidden(检查 token 的 Packages 范围)、not-found、rate-limited、registry-error、network、timeout、unknown。npm 的原话仍然原样显示在下面——归类是补充,不是替换。
开了 2FA 的账号,推送时 npm 要一次性密码:确认行里就有那个输入框。子进程的 stdin 是关闭的(ignore),所以它不会挂着等一个没人持有的 stdin——失败会明说是要 OTP,而不是转圈到超时。
如果你的目标是"CI 里自动发布",不该用上面这条路。npm 支持 trusted publishing(GitHub Actions OIDC):release.yml 加 id-token: write 权限并在 npm 上登记这个仓库,发布时用 OIDC 换一次性凭据——没有任何长期 token,还自带 provenance。这与本插件"不存凭据"的原则完全一致,也是本仓库更推荐的方向。
面板不做这件事:它是在这台机器上推一次的路径,不是 CI 的替代品。CI 发布需要 token 时,token 应放在仓库 secret 里(NPM_TOKEN),而不是这台机器上。
插件更新了,除了 Release 和 npm,还有一件观众真的会看的事:在介绍这个插件的视频下面留一句"更新了什么"。这一条也做进面板里——每个仓库绑一个 BV 号,Release 公开之后自动在评论区补一条更新说明。
触发条件只有一个:Release 已公开。 草稿不算——草稿只有作者能看见,为它发"上线了"是对着空气说话。所以:
bilibiliWatchSeconds(默认 90 秒)巡检一次每个已绑定的仓库;(仓库, tag),落在 <DSH_HOME>\dsh-plugin-cicd\bilibili-announcements.json 里。同一个版本永远只发一条;这份记录读不出来时什么也不发(否则会把已经发过的评论再刷一遍),面板会把原因写在最上面。绑定视频时会写一条基线:绑定那一刻已经公开的版本不算"这次更新"。所以把视频接上来不会追发历史版本,只有之后的新版本会播报。
"发送成功"不等于"读得到",所以发送后要复核。 x/v2/reply/add 会为一条公开列表里根本读不到的评论回 code: 0 加一个 rpid——实测有两条被记成"已播报",按 rpid 查回却都是 12006 没有该评论。所以:
x/v2/reply/reply?root=<rpid>),把结果写成台账里的 visible(true/false;null = 发送失败没问过;字段缺失 = 早于这个功能的旧记录)与 visibleCode;12006,约 4 秒后才可读(实测 +0s 失败 / +4s 成功),所以是"查 → 等 3 秒 → 再查",默认 3 次。一次性的即时检查会把一条好好的评论判成不可见,那比不检查更糟;12006),所以不做自动重发——评论可能还在审核,自动重发会变成重复评论。要重发就人工走【仍然发送】。x/v2/reply/reply 精确查找;x/v2/reply 与 x/v2/reply/wbi/main 这两个列表端点实测只回 1 条(all_count=6 却只给 1 条),拿列表判断会误判。发评论走的是 B 站 Web 接口,所以凭据必须是网页会话。这里踩过一回,记在文档里免得再踩:biliup login 写下的 cookies.json 是 BiliTV 登录(platform: BiliTV)——它能投稿(APP 接口),但所有 Web 会员接口一律回 -101 账号未登录。所以本插件不假设"文件里有 SESSDATA 就能用",而是拿凭据去问一次账号接口,并把答案写在面板上;认不出来时给出的原话是"这份凭据是 BiliTV 登录(APP/TV)……",不是"你没登录"。
凭据有两种给法,面板里都能做,不需要终端:
【登录 B 站】:走 passport 的网页接口拿一个二维码。码是插件自己画的——用手机 B 站 App 扫它,然后在手机上确认。面板轮询到确认后把 Cookie 存进插件自己的文件。
面板不提供"打开登录页面":那串 URL 是给手机看的 H5 页,在电脑浏览器上打开只会给你一个 B 站 App 的 APK 下载,不是登录。所以扫码是唯一的路,扫不动就用下面第 2 条的粘贴方式。
为什么要自带一个二维码编码器:登录接口交回来的是一串要被手机扫的 URL,而客户端半边只有 React 等五个内建、没有任何依赖。用外部二维码图片服务等于把一条活的登录 URL 交给第三方(泄凭据);加依赖则违背"装一个 tgz、不拉依赖树"。所以 client.js 里有一段 byte 模式、纠错等级 M、版本 1–10 的编码器,画成内联 SVG。它拿 profile 里现成的 qrcode 包(那是别的包的依赖,不是本插件的)逐模块验证过,并把基准矩阵冻结成测试断言——任何一个模块翻转都会让测试红。
粘贴一次:浏览器 F12 → Application → Cookies → bilibili.com,把 SESSDATA 与 bili_jct(即 csrf)复制进来。B 站当场接受才写入——存一份已经回 -101 的凭据,只会让面板显示一个假的"已登录"。
两者都落在 <DSH_HOME>\dsh-plugin-cicd\bilibili-cookies.json(bilibiliCookieFile 可以指向别处,比如 biliup 那份,作为兜底读取;面板自己写的永远优先)。插件不回显这个文件的内容,【退出 B 站登录】也只删自己写的这一份——外部那份属于别的工具,不动。
默认模板是 【更新 {tag}】{summary}:{summary} 取 Release 标题;标题就是版本号时改取 Release 正文第一段(去掉 Markdown、去掉 GitHub 自动生成的 "What's Changed / Full Changelog" 和 by @someone in https://… 尾巴)。可用占位符:{tag} {version} {label} {repo} {title} {summary} {url} {date};不认识的占位符留在原地并报出来,不会被悄悄删掉。
默认模板不带链接:带外链的评论更容易被 B 站过滤,而被过滤和发成功在这边看起来一模一样。想要链接就把 {url} 写进 bilibiliTemplate。
发之前一定先预览:面板上点【发更新评论】会先问 Host "这条会写成什么"(dryRun),把即将发送的原文和 Host 的判决一起显示出来,确认后才真的发。预览由 Host 组装,不是浏览器——否则会出现"看到的是一句、发出去的是另一句"。
B 站的拒绝会被归类,而不是原样抛给用户:-101 未登录、-400 被拒、-403 无权限、-412 风控拦截、-509 频率限制、12061 内容被过滤……每一类给的是下一步做法。失败会记进播报记录;同一个版本失败 3 次后就停下等人工判断——风控拦下来的东西,连续重试只会更糟。
包本身不带仓库列表——哪些仓库被监视是部署状态,不是包状态;把某个人的私有仓库名打进公开包,每个安装者都得先去拆它。
列表放在一个由脚本管理的 JSON 文件里(默认 %USERPROFILE%\.dsh\dsh-plugin-cicd\repos.json),profile patch 只留两行机器相关的配置:
- id: dsh-plugin-cicd
config:
owner: SOH4C4759
ghPath: 'C:\Program Files\GitHub CLI\gh.exe'
日常操作全走插件自带的脚本:
$cfg = "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-plugin-cicd\scripts\configure.mjs"
# 或直接用你的检出目录:F:\CodeProj\dsh-plugin-cicd\scripts\configure.mjs
node $cfg list # 现在登记了什么
node $cfg add dsh-plugin-restart --path F:\CodeProj\dsh-plugin-restart
node $cfg add someone-else/their-plugin # 别人的仓库,只读
node $cfg remove dsh-plugin-restart
node $cfg owner SOH4C4759
node $cfg check # 面板此刻会显示什么
add 在面板下一次轮询(默认 30 秒)就生效,不需要重启。.bak。name 或 owner/name,--path 必须是绝对路径——相对路径会按宿主进程的目录解析,那不是你的 shell 目录。repos。patch 里仍可用的选项(每个都有默认值):
| 键 | 默认 | 含义 |
|---|---|---|
owner | '' | 裸 repo 名用的 GitHub 账号;配置文件里的同名值优先 |
repos | [] | 手写兜底;配置文件存在时以文件为准 |
configFile | <DSH_HOME>\dsh-plugin-cicd\repos.json | 受管列表的位置 |
projectsRoot | '' | 本地检出所在根目录;面板添加仓库时据此自动填 localPath(按各目录 package.json 的 name 匹配,所以目录名与仓库名不同也能找到),克隆【克隆】的落点也是这里。这是行配置(cordis.patch.yml),不是 repos.json 的键——写进后者会被忽略 |
defaultBranch | main | 面板触发 workflow 用的 ref |
buildWorkflow | ci.yml | 构建 按钮触发的 workflow |
releaseWorkflow | release.yml | 一键发布触发的 workflow;关掉一键发布后是 发布 按钮触发的那个 |
oneClickRelease | true | 发布同台:发布 = 代码推送 + Release + npm 一条流水线。设 false 就回到"只触发构建(出草稿)",npm 与公开草稿各自是独立按钮 |
pipelineWatchSeconds | 20 | 一键发布流水线的推进间隔(每次最多一次 gh run list);0 停掉时钟,只保留按需推进(DSH 重启后的补跑也会停) |
ghPath | '' | 空 = 从 PATH 与 %ProgramFiles%\GitHub CLI\gh.exe 等位置解析 |
requestTimeoutMs | 20000 | 单次 gh 调用的硬超时 |
overviewTtlMs | 15000 | 概览缓存窗口;面板轮询会走缓存 |
logTailLines | 120 | 失败日志取最后多少行 |
pollSeconds | 30 | 面板自动刷新间隔 |
npmRegistry | https://registry.npmjs.org/ | 推送目标源。改成私有源即可(只能 http/https,且 URL 里带凭据会被拒绝);.npmrc 里的 token 行按这个源的 host 匹配 |
enabled | true | 关掉后路由只回「已停用」,不碰 GitHub |
所有值都做夹取而不是拒绝:手写的 patch 不该能让宿主起不来,最坏情况是某个路由报告「未配置」。
面板不自己存凭据,它用你机器上的 gh。所以第一次打开时它可能无事可做——这时面板不会只显示一句"读不到",而是按缺什么给什么:
| 状态 | 面板给出 |
|---|---|
找不到 gh | winget install --id GitHub.cli(可一键复制)+ cli.github.com 链接 |
| 装了但没登录 | gh auth login,并说明选 GitHub.com → HTTPS → 浏览器登录,完成后点「重新检测」 |
| 登录了但缺 scope | gh auth refresh -h github.com -s repo,workflow,缺哪个补哪个 |
| 没有任何仓库 | configure.mjs add ... 的实际命令 + 配置文件路径 |
scope 是从 gh auth status 真读出来的:缺 repo 读不到私有仓库,缺 workflow 无法触发构建——这两种情况在按钮按下去之前就会说明白。细粒度 token 不报 scope 行时按"未知"处理,不会误报成"全都缺"。
gh 而不是自带 token本机 gh 已经登录、已经带好了 token scope。复用它意味着:插件不存任何凭据,不需要你再走一次 OAuth/PAT,而面板执行的命令就是你会手敲的那条(gh run list / gh workflow run / gh release edit)。反过来,任何要求你再输一次 token 的方案,都是把同一条权限链复制了第二份。
npm 这一侧是同一条原则的两个面:包管理器复用 Host 自己的 pnpm(profileContext.packageManager——插件管理器安装插件时用的就是它,第二个答案早晚会和第一个不一致),凭据放在 npm 自己读的 .npmrc 里。面板提供了一个写 token 的入口,但写的是那个文件,不是插件的任何存储:这与"面板能登录 GitHub 但插件不存 token"是同一句话。
调用一律用 execFile 传 argv、不经 shell;仓库名、workflow 名、tag 在进入 gh 之前都按 slug 形状校验过。子进程带 GH_PROMPT_DISABLED=1,否则一个想提问的 gh 会挂在没人持有的 stdin 上,面板只会转圈到超时。
isTrustedRequest),与宿主设置桥对自家回环路由的信任策略一致:只有「来自本机」且「来自这个 Host 服务的文档」的请求能过。这些路由以本机 GitHub 凭据行事,所以不能只按端口放行。dispatch、run-action、release-action、version-bump、commit、clone、update、restart、npm-login、npm-publish。clone 是唯一会从网络拉一个新目录的路由,服务"仓库已登记但没有本地检出"这个状态——那种状态下本页每个动作都用不了(提交没东西可提交,构建与发布作用于 GitHub 上的提交而不是这份磁盘)。地址由仓库名推导(owner/repo 自带,裸名用配置里的 owner),面板把它预填好、可改(fork、私有远端都是真实需求);落点是 projectsRoot/<仓库名>,也就是 findLocalCheckout 之后会去找的地方。已有可用检出回 409 already-cloned,目标目录非空回 409 target-exists,没有 projectsRoot 回 409 no-projects-root(并点名这个配置键)。version-bump 只改 package.json 的版本行,然后 git commit 只提交这一个文件并推送当前分支。工作区不干净、分支没有上游、或落后于上游时它直接拒绝,不做任何写入。commit 是唯一会提交整个工作区的路由(git add -A + commit + push)。工作区干净且与上游同步时回 409 nothing-to-commit;message 为空且确实有改动时回 400 message-required(先拒绝,不写任何东西);分支没有上游时回 409 no-upstream——发布构建的是 GitHub 上的提交,推不上去就等于没提交。提交信息为空但工作区干净是合法用法,含义是「把已经提交的推上去」。update 是唯一会改 profile 依赖的路由:下载 Release 里的 tgz,再交给 Host 的插件管理器安装;失败时由管理器还原 package.json 与 lockfile。restart 自己不重启任何东西:它把请求转发到同一个 Host 上的 /api/dsh-restart/restart,由 dsh-plugin-restart 决定停哪个进程、用什么命令拉起来。没有那个插件就回 501 restart-unavailable。npm-login 是唯一会写用户级配置的路由:把 token 合并进 ~/.npmrc(其他行原样保留,旧版本留 .bak)。token 绝不出现在任何响应、日志或状态里——npm-status 只回答"有没有那一行",不回答"那行是什么"。npm-publish 是另一个不可逆动作:npm 不允许同一版本推两次,撤回也只在很短的时间内可行。所以它也要两次点击,且推送前逐条拒绝(private: true、脏工作区、版本已存在、未登录)。公开发布草稿 与 npm-publish 仍是仅有的两个不可逆动作,各自单独用时都要两次点击、中间那一步明说后果。一键发布把它们放进一条流水线,那一格确认框就是"中间那一步":它把七格顺序、要提交的文件、以及「npm 通了才公开 Release」写在同一屏上,再让人按下去。面板用的就是这些,脚本也可以直接用:
| 路由 | 作用 |
|---|---|
POST /api/dsh-cicd/status | gh 身份与解析后的仓库列表 |
POST /api/dsh-cicd/overview | 每个仓库的运行 + Release + 本地 git 状态({force:true} 绕过缓存) |
POST /api/dsh-cicd/runs | {repo, limit} 单仓库运行列表 |
POST /api/dsh-cicd/dispatch | {repo, workflow?, ref?, inputs?} 触发 workflow_dispatch;触发发布流程时会先做发布预检(见下节) |
POST /api/dsh-cicd/run-action | {repo, runId, action},action ∈ rerun / rerun-failed / cancel |
POST /api/dsh-cicd/release-action | {repo, tag, action},action ∈ publish / delete(delete 有意不给按钮) |
POST /api/dsh-cicd/version-bump | {repo, release?},release ∈ patch(默认)/ minor / major:升 package.json、提交、推送当前分支 |
POST /api/dsh-cicd/release-all | {repo, message?, otp?}:一键发布——代码推送 → 版本号 → 触发构建(草稿)→(流水线自己继续)等 CI → 推 npm → 公开 Release → B 站播报。开工前能本地判定的全部判定完(无检出 / 无上游 / 落后上游 / 脏工作区没给信息 → 400/409 且一个字节都没写);已经开跑的失败用 200+value.pipeline 如实回报,status 是 blocked 时 Release 仍是草稿 |
POST /api/dsh-cicd/release-all-resume | {repo, otp?, skipNpm?}:继续一条停在 npm 的流水线。skipNpm: true = 不管 npm 了,直接公开 Release。没有停住的流水线回 409 nothing-to-resume |
POST /api/dsh-cicd/release-all-cancel | {repo}:不再跟进这条流水线(GitHub 上的运行不会被它停掉) |
POST /api/dsh-cicd/logs | {repo, runId} 失败步骤日志的尾部 |
POST /api/dsh-cicd/update | {repo, tag?}:把该 Release 的 .tgz 下载到 <DSH_HOME>\dsh-plugin-cicd\downloads\ 并装进当前 profile。tag 缺省 = 最新一个非草稿且带 tgz 的 Release |
POST /api/dsh-cicd/restart | 无参数:转发到 dsh-plugin-restart 的重启路由;没挂载则 501 restart-unavailable |
POST /api/dsh-cicd/npm-status | 无参数({force:true} 绕缓存):whoami 的结果 + 每个包在源上的状态。按需拉取,不参与 30 秒轮询——每个仓库一次 HTTPS 加一次 whoami |
POST /api/dsh-cicd/npm-login | {token}:把 token 合并进用户级 .npmrc 再用 whoami 验证。token 不回显 |
POST /api/dsh-cicd/npm-publish | {repo, otp?}:用 Host 自己的 pnpm 发布当前版本。otp 是 2FA 一次性密码(6–8 位) |
POST /api/dsh-cicd/bilibili-status | 无参数({force:true} 绕缓存):凭据状态 + 每个仓库的绑定/已播报/失败 + 播报记录文件。不碰 GitHub |
POST /api/dsh-cicd/bilibili-login-start | 无参数:生成 B 站网页登录二维码,返回 { url, key, expiresAt } |
POST /api/dsh-cicd/bilibili-login-poll | 无参数:问一次是否已确认。state ∈ waiting / scanned / succeeded / expired;成功后 Cookie 落盘 |
POST /api/dsh-cicd/bilibili-login-cancel | 无参数:放弃这次登录 |
POST /api/dsh-cicd/bilibili-credential | {cookie} 或 {sessdata, bili_jct}:验证通过才写入插件自己的凭据文件 |
POST /api/dsh-cicd/bilibili-logout | 无参数:删掉插件自己写的那份凭据(不动 bilibiliCookieFile 指定的外部文件) |
POST /api/dsh-cicd/bilibili-bind | {repo, bvid, auto?}:绑定/解绑视频;绑定会写一条基线,bvid: '' 即解绑(播报记录保留) |
POST /api/dsh-cicd/bilibili-announce | {repo, tag?, text?, force?, dryRun?}:组装(dryRun)或发送这一条更新评论。tag 缺省 = 最新的已公开 Release |
一律返回 { ok: true, value } 或 { ok: false, code, message }。
npm-status 的每条仓库记录:{ repo, label, localPath, packageName, version, dirty, privatePackage, state, latest, blockers, canPublish, registryProblem, registry, pageUrl }。state ∈ unregistered / unpublished / published / unknown;blockers 是具名原因(private-package、dirty-tree、not-logged-in、already-published、registry-unreachable、no-checkout、no-version、no-package-name)——"没有按钮"是最没用的一句话,private: true 和"没登录"在屏幕上同样是空白,修法却完全不同。
overview 的每一行多一个 install 块:{ profile, profileDir, profileReadable, packageName, present, spec, kind, installedVersion, latestTag, latestVersion, latestAsset, state }。state ∈ not-installed / no-release / current / update / ahead / differs / checkout——判决在 Host 上做,所以面板和 update 路由不会各说一套。
overview 的每一行还多一个 pipeline 块(null = 这个仓库从没跑过一键发布):{ id, repo, slug, tag, version, headSha, startedAt, updatedAt, status, message, blockedCode, runId, runUrl, runConclusion, skipNpm, stages }。status ∈ running / blocked / done / failed / cancelled;stages 是固定七格 [{ id, status, detail, at }],id 依次是 push / version / dispatch / build / npm / release / announce,status ∈ pending / active / done / skipped / blocked / failed / cancelled。blockedCode 是停住的原因(otp-required、not-logged-in、registry-unreachable、timeout …),面板按它给出下一步。overview 顶层另有 pipelineProblem:记录文件读不出来时一键发布会拒绝启动,原因写在这里。
status 另外报出 profile: { name, dir, readable }:更新写的是哪个 profile,是报出来的而不是假设的——「没装」和「面板看错了 profile」是两句不同的话,只有路径能把它们分开。
根因:发布用的是 package.json 里的版本号,而 release.yml 有意拒绝覆盖属于另一个提交的同号 Release(否则公开草稿会创建旧提交上的 tag,tag 与资产从此不一致,而且整个过程静默)。于是只要树往前走了而版本没升,点「发布」触发的运行一定在第一步失败,约 10 秒后红掉;面板却提前宣告「已产出草稿」,真正的原因还藏在两层点击之后。实测有四个失败运行属于这一类(dsh-plugin-restart ×3、dsh-plugin-cicd ×1)。
现在:
releaseCheck)。宿主拿 releases[].target_commitish 与「这次发布会构建的提交」对比:只有能证明版本已被另一个提交占用时才判 blocked(脏工作区、分支名 target、读不到远端提交都算「证明不了」,一律放行)——预检拦下一个本来能成功的发布,比没有预检更糟。行上出现【版本被占用】标记,展开即写明 v1.0.0 已属于 48ce81c,而这次发布会构建 e6a1cc1。dispatch 会拒绝注定失败的发布,返回 409 version-taken,不再浪费一次运行。version-bump + 发布触发。version-bump 的拒绝清单(都保证一个字节都没写):工作区有未提交改动(发布构建的是已推送的提交,这些文件会静默缺席于发布包)、分支没有上