🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

发布台 SOH4C4759/dsh-plugin-cicd

DeepSeek Harnessv0.5.1
SOH4C475920 天前有提交已提供中文文档扫描:含敏感能力

DeepSeek Harness 的发布台:侧边栏入口加主面板页面,展示每个仓库最新的 CI 运行、发布记录,以及本地检出是否领先于发布版本,支持一键构建、发布、发布草稿。复用机器上已认证的 gh 命令行,不保存任何凭据。

30.8/100
综合分
1
GitHub 星标
299
npm 周下载
—/5
用户评分

数据截至 2026/10/12 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-plugin-cicd
npm 源 · dsh-plugin-cicd 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · market
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 20 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-plugin-cicd @ 0.5.1
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/12 04:19:54

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 57 处,展示前 28 条)
  • 执行外部命令固定命令
    index.js:327
    const out = execFileSync(
  • 读取本地文件
    index.js:702
    if (isAbsolute(candidate) && existsSync(candidate)) return candidate
  • 读取本地文件
    index.js:1096
    if (localPath === '' || !existsSync(localPath)) return null
  • 读取本地文件
    index.js:1098
    const parsed = JSON.parse(readFileSync(join(localPath, 'package.json'), 'utf8'))
  • 读取本地文件
    index.js:1229
    if (!existsSync(localPath)) return { available: false, reason: 'localPath does not exist' }
  • 读取本地文件
    index.js:1230
    if (!existsSync(join(localPath, '.git'))) return { available: false, reason: 'not a git checkout' }
  • 读取本地文件
    index.js:1311
    const parsed = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'))
  • 执行外部命令来自配置/变量
    index.js:2255
    const child = spawn(ghPath, args, {
  • 写入本地文件
    index.js:2831
    writeFileSync(manifest, source, 'utf8')
  • 写入本地文件
    index.js:2838
    writeFileSync(manifest, rewritten.text, 'utf8')
  • 写入本地文件
    index.js:3147
    mkdirSync(directory, { recursive: true })
  • 请求外部主机api
    index.js:3261
    response = await fetch(`http://${authority}/api/dsh-restart/restart`, {
  • 发起网络请求
    index.js:3261
    response = await fetch(`http://${authority}/api/dsh-restart/restart`, {
  • 发起网络请求
    index.js:3335
    const response = await fetch(url, {
  • 写入本地文件
    index.js:3498
    if (existsSync(npmrcPath)) writeFileSync(`${npmrcPath}.bak`, existing, 'utf8')
  • 写入本地文件
    index.js:3499
    writeFileSync(npmrcPath, merged.text, 'utf8')
  • 执行外部命令来自配置/变量
    index.js:3810
    const child = spawn(ghPath, ['auth', 'logout', '--hostname', 'github.com'], {
  • 写入本地文件
    index.js:4101
    mkdirSync(dirname(file), { recursive: true })
  • 删除本地文件
    index.js:5275
    unlinkSync(own)
  • 发起网络请求
    client.js:1109
    const response = await fetch(`${BASE}${path}`, {
  • 读取本地文件
    lib/config-store.mjs:119
    if (!existsSync(file)) return { exists: false, owner: '', repos: [], dropped: 0, problem: null }
  • 读取本地文件
    lib/config-store.mjs:120
    const parsed = parseConfig(readFileSync(file, 'utf8'))
  • 写入本地文件
    lib/config-store.mjs:138
    mkdirSync(dirname(file), { recursive: true })
  • 读取本地文件
    lib/config-store.mjs:139
    if (existsSync(file)) {
  • 读取本地文件
    lib/config-store.mjs:141
    writeFileSync(`${file}.bak`, readFileSync(file))
  • 写入本地文件
    lib/config-store.mjs:141
    writeFileSync(`${file}.bak`, readFileSync(file))
  • 写入本地文件
    lib/config-store.mjs:147
    writeFileSync(temporary, body, 'utf8')
  • 写入本地文件
    lib/config-store.mjs:148
    renameSync(temporary, file)
扫描于 2026/10/31(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包
涉及外部主机:api

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 查看最新CI运行
  • 查看仓库发布版本
  • 一键构建发布

README

由 DeepSeek 最新模型翻译生成
dsh-plugin-cicd(DSH 插件发布台) 把 DSH 插件的持续构建与发布(CI/CD)搬进 DeepSeek Harness:侧边栏一个图标,点开是一个占满主区域的独立面板。 名字:界面显示名为「DSH 插件发布台」(副标题「持续构建与发布(CI/CD)」;英文界面为 DSH Plugin CI/CD)。包名与仓库 id 保持 dsh-plugin-cicd —— 它们只是技术标识,改名会破坏已有安装(profile 的 link: 路径、bundles 条目、patch 里的 row id、配置文件路径)。 它回答四个 GitHub 页面各自只说了一部分的问题: - 构建过了吗 —— 每个仓库最近一次运行的状态、耗时、触发事件。 - 发布了吗 —— 已发布的 Release、还在草稿箱里的 Release、资产清单。 - 本地领先于发布吗 —— 本地 package.json 版本、领先/落后提交数、未提交文件数。 - 装的是发布出去的那份吗 —— 本机 profile 里这份是 link: 的本地检出、还是 Release 上的 tgz;差几号版本。 - npm 上是哪一版 —— 这个版本在不在公开源上;不在的话,一次点击就能推上去。 也就是说:一个包,两条分发渠道。
展开完整文档(README 剩余 1 节)
dsh-plugin-cicd(DSH 插件发布台)

把 DSH 插件的持续构建与发布(CI/CD)搬进 DeepSeek Harness:侧边栏一个图标,点开是一个占满主区域的独立面板。

名字:界面显示名为「DSH 插件发布台」(副标题「持续构建与发布(CI/CD)」;英文界面为 DSH Plugin CI/CD)。包名与仓库 id 保持 dsh-plugin-cicd —— 它们只是技术标识,改名会破坏已有安装(profile 的 link: 路径、bundles 条目、patch 里的 row id、配置文件路径)。

它回答四个 GitHub 页面各自只说了一部分的问题:

- 构建过了吗 —— 每个仓库最近一次运行的状态、耗时、触发事件。
- 发布了吗 —— 已发布的 Release、还在草稿箱里的 Release、资产清单。
- 本地领先于发布吗 —— 本地 package.json 版本、领先/落后提交数、未提交文件数。
- 装的是发布出去的那份吗 —— 本机 profile 里这份是 link: 的本地检出、还是 Release 上的 tgz;差几号版本。
- npm 上是哪一版 —— 这个版本在不在公开源上;不在的话,一次点击就能推上去。

也就是说:一个包,两条分发渠道。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 | 不需要 |

仍然需要命令行的两件事,如实列出,不假装不需要:

1. 安装这个插件本身(一次性)——见下一节。
2. 打 tag(只有想走 tag 触发那条路时)——版本号由【一键发布】自己升(版本已被别的提交占用时),但 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(草稿不算) | 没有可装的 |

装的时候:

1. gh release download <tag> -p <tgz> 把资产下到插件自己的目录 <DSH_HOME>\dsh-plugin-cicd\downloads\(不是临时目录——profile 的依赖会指向这个路径,下完就删会让清单再也装不上)。
2. 交给 DSH 自己的插件管理器(ctx.get('pluginManager').installBundle),也就是 dsh plugin add 走的同一条 pnpm 路径:它持有 profile 锁,失败时把 package.json / pnpm-lock.yaml 还原。发布台不自己起一个 dsh plugin 子进程——那会是对同一个 profile 的第二个写入者。
3. 面板接着问要不要重启(不假设答案)。重启转发给 dsh-plugin-restart;没装这个插件时回一句明确的「没挂载」,而不是一个看不出所以然的失败。

一次只跑一个更新(再来一个回 409 busy),因为插件管理器本来就会排队,而两个转圈比一个明确的拒绝更难懂。

没有装它就没有这部份:update 路由需要 Host 提供 pluginManager 服务,没有就回 501 plugin-manager-missing,而不是假装装过。

发布同台:一次点击 = 代码推送 + Release + npm

按下「发布」就是三件事一起做完,因为分开做留下了三种"看起来成功"的半成品:没人公开的草稿、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)。草稿只有作者看得见,这正是后面两格能成立的原因。
- npm 在公开 Release 之前,这是关键:npm 推送不可逆(同一版本不能推第二次),而草稿可以。所以 npm 失败时——缺一次性密码、token 失效、源连不上——Release 仍然是草稿,对外全有或全无,补上原因(或选择「跳过 npm 并公开 Release」)就能继续。反过来先公开就会留下「Release 出来了但 npm 上没有」,那是重试也收拾不干净的中间态。
- B 站播报最后,因为「这个版本上线了」只有在它真的上线之后才成立。

面板上看得见它走到哪一格:七格状态(待做/进行中/完成/跳过/停住/失败/已停止)随 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 发(见下节)。

推送到 npm(第二条分发渠道)

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 白名单挡不住「白名单目录里的新文件」。
- 这个版本已经在 npm 上 —— npm 不接受同一版本推两次,撤回也只在很短的时间内可行。
- 没有登录 npm —— 面板直接给出解决路径(见下)。
- 没有本地检出 / 没有版本号 / 读不到源。

凭据:和复用 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,而不是转圈到超时。

另一条路:完全不用 token(trusted publishing)

如果你的目标是「CI 里自动发布」,不该用上面这条路。npm 支持 trusted publishing(GitHub Actions OIDC):release.yml 加 id-token: write 权限并在 npm 上登记这个仓库,发布时用 OIDC 换一次性凭据——没有任何长期 token,还自带 provenance。这与本插件「不存凭据」的原则完全一致,也是本仓库更推荐的方向。

面板不做这件事:它是在这台机器上推一次的路径,不是 CI 的替代品。CI 发布需要 token 时,token 应放在仓库 secret 里(NPM_TOKEN),而不是这台机器上。

B 站更新播报(第三条渠道)

插件更新了,除了 Release 和 npm,还有一件观众真的会看的事:在介绍这个插件的视频下面留一句「更新了什么」。这一条也做进面板里——每个仓库绑一个 BV 号,Release 公开之后自动在评论区补一条更新说明。

触发条件只有一个:Release 已公开。 草稿不算——草稿只有作者能看见,为它发「上线了」是对着空气说话。所以:

- 面板每 bilibiliWatchSeconds(默认 90 秒)巡检一次每个已绑定的仓库;
- 点【公开草稿】成功后立刻巡检一次(这一下就是「发布成功」的那一刻);
- DSH 启动约 15 秒后巡检一次——关机期间发布的那些版本,只有这一次能补上;
- 幂等键是 (仓库, tag),落在 <DSH_HOME>\dsh-plugin-cicd\bilibili-announcements.json 里。同一个版本永远只发一条;这份记录读不出来时什么也不发(否则会把已经发过的评论再刷一遍),面板会把原因写在最上面。

绑定视频时会写一条基线:绑定那一刻已经公开的版本不算「这次更新」。所以把视频接上来不会追发历史版本,只有之后的新版本会播报。

「发送成功」不等于「读得到」,所以发送后要复核。 x/v2/reply/add 会为一条公开列表里根本读不到的评论回 code: 0 加一个 rpid——实测有两条被记成「已播报」,按 rpid 查回却都是 12006 没有该评论。所以:

- 发送后 Host 按 rpid 查回(x/v2/reply/reply?root=<rpid>),把结果写成台账里的 visible(true/false;null = 发送失败没问过;字段缺失 = 早于这个功能的旧记录)与 visibleCode;
- 必须重试,不能只查一次:刚发出的评论头几秒查回的是 12006,约 4 秒后才可读(实测 +0s 失败 / +4s 成功),所以是「查 → 等 3 秒 → 再查」,默认 3 次。一次性的即时检查会把一条好好的评论判成不可见,那比不检查更糟;
- 面板据此在「已播报」旁给出 warn chip【已发送但读不到】,发送后的提示也照实说;
- 「审核中」与「已被删除」在接口上无法区分(都是 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)……」,不是「你没登录」。

凭据有两种给法,面板里都能做,不需要终端:

1. 【登录 B 站】:走 passport 的网页接口拿一个二维码。码是插件自己画的——用手机 B 站 App 扫它,然后在手机上确认。面板轮询到确认后把 Cookie 存进插件自己的文件。

面板不提供「打开登录页面」:那串 URL 是给手机看的 H5 页,在电脑浏览器上打开只会给你一个 B 站 App 的 APK 下载,不是登录。所以扫码是唯一的路,扫不动就用下面第 2 条的粘贴方式。

为什么要自带一个二维码编码器:登录接口交回来的是一串要被手机扫的 URL,而客户端半边只有 React 等五个内建、没有任何依赖。用外部二维码图片服务等于把一条活的登录 URL 交给第三方(泄凭据);加依赖则违背「装一个 tgz、不拉依赖树」。所以 client.js 里有一段 byte 模式、纠错等级 M、版本 1–10 的编码器,画成内联 SVG。它拿 profile 里现成的 qrcode 包(那是别的包的依赖,不是本插件的)逐模块验证过,并把基准矩阵冻结成测试断言——任何一个模块翻转都会让测试红。
2. 粘贴一次:浏览器 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 次后就停下等人工判断——风控拦下来的东西,连续重试只会更糟。

配置:用脚本,不要手改 YAML

包本身不带仓库列表——哪些仓库被监视是部署状态,不是包状态;把某个人的私有仓库名打进公开包,每个安装者都得先去拆它。

列表放在一个由脚本管理的 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 秒)就生效,不需要重启。
- 写入是原子的(临时文件 + rename),旧版本留一份 .bak。
- 校验与宿主同规则:仓库名必须是 name 或 owner/name,--path 必须是绝对路径——相对路径会按宿主进程的目录解析,那不是你的 shell 目录。
文件读不了或格式错时,面板会明确报错,而不是假装「没有仓库」,并回退到 patch 里的 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 上,面板只会转圈到超时。

安全边界

- 所有路由都是 POST + 仅回环 + 同源(isTrustedRequest),与宿主设置桥对自家回环路由的信任策略一致:只有「来自本机」且「来自这个 Host 服务的文档」的请求能过。这些路由以本机 GitHub 凭据行事,所以不能只按端口放行。
- 只读部分:状态、概览、运行、日志、npm 状态。有副作用的是十条: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」写在同一屏上,再让人按下去。

HTTP 接口

面板用的就是这些,脚本也可以直接用:

| 路由 | 作用 |
| 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)。

现在:

1. 概览行先给出判决(releaseCheck)。宿主拿 releases[].target_commitish 与「这次发布会构建的提交」对比:只有能证明版本已被另一个提交占用时才判 blocked(脏工作区、分支名 target、读不到远端提交都算「证明不了」,一律放行)——预检拦下一个本来能成功的发布,比没有预检更糟。行上出现【版本被占用】标记,展开即写明 v1.0.0 已属于 48ce81c,而这次发布会构建 e6a1cc1。
2. dispatch 会拒绝注定失败的发布,返回 409 version-taken,不再浪费一次运行。
3. 面板给出解法:被占用时按钮变成【升版本并发布】,确认框里写明「从 1.0.1 升到 1.0.2、提交并推送到 main」——一次点击完成 version-bump + 发布触发。
4. 文案不再替运行结果打包票:触发后说的是「是否真的产出草稿要看这次运行的结果,失败时展开点【日志】」。

version-bump 的拒绝清单(都保证一个字节都没写):工作区有未提交改动(发布构建的是已推送的提交,这些文件会静默缺席于发布包)、分支没有上游、落后于上游、版本号不是 major.minor.patch、清单里出现第二个 version 键。推送失败不回滚:提交是真的,失败通常是 github.com:443 的老问题,所以如实回「本地已提交 X,推送失败」并让你重试推送。

设计取舍

- UI 落在 sidebar.panellist + main,不是浮层:侧边栏拿按钮,主区域拿页面,点开顶开内容而不是遮住它。
- 颜色只取 --dsw-alias- 主题 token,没有自带调色板——面板跟随亮/暗主题,而不是自带一套深色。
- 后台轮询失败不清空、不弹错:合盖的笔记本不会产生一个不是用户造成的错误横幅,只是停止更新。
- HTML/CSS/JS 三处都没有 position: absolute 的浮窗:main 面板是布局里的一列。

本仓库自身的 CI/CD

它自己也用同一套:ci.yml 每次 push 跑 tests/host-checks.mjs(167 条离线检查,覆盖入口校验/配置夹取/降级路径/发布预检与版本号运算/安装态与更新判决/npm 源与 token 与每条拒绝)、tests/mount-check.mjs(35 条:真挂载、真起 HTTP、真走路由,含更新、转发重启与 npm 三条拒绝路径)、tests/bump-e2e.mjs(29 条:真 git 仓库、真提交、真推送,以及每条拒绝都不留半截改动)与 tests/client-render.mjs(71 条:用桩 React 真渲染面板,证明「发布」与【升版本并发布】确实按判决切换、被拦的原因真的到了屏幕上、一次更新真的 POST 了 /update 并接着问要不要重启、以及一次 npm 推送真的 POST 了 /npm-publish 且未登录时真的能只靠粘贴 token 完成),并真造一个发布包;release.yml 在 v tag 上构建并上传 Release,对解包后的资产再跑一遍这几套。发布流程见 RELEASING.md。

tests/host-checks.mjs 里另有一半检查需要真实的 gh 与特定的仓库状态,用 DSH_CICD_LIVE=1 打开:

node tests/host-checks.mjs # 离线,CI 跑的就是这个
$env:DSH_CICD_LIVE = 1; node tests/host-checks.mjs

推送通道被阻断时

本机实测:github.com:443 会被间歇阻断(git push 报 Connection was reset 或直接连不上),而 api.github.com:443 一直可达——所以 git 挂了但 gh 一切正常。

scripts/push-via-api.ps1 走 API 造出逐字节相同 SHA 的提交对象再移动 ref,因此本地与远程不会分叉,不需要事后 git reset --hard:

pwsh -File scripts/push-via-api.ps1 -RepoPath 'F:\CodeProj\dsh-plugin-cicd'

它在每一步比对重建出的 blob / tree / commit SHA 与本地提交,任何一处不一致就中止并不移动 ref;成功后连同 refs/remotes/origin/main 一起更新,否则 git status 会显示一个并不存在的「领先」。仓库已经同步时它什么都不做。

已知边界

- 路径按 Windows 书写(F:\...),ghPath 的默认候选也是 Windows 安装位置;其它平台请显式配 ghPath。
- 私有仓库要求 gh 已登录且 token 有 repo scope。
- 只观察 GitHub 上的仓库;本地没有远程的仓库(例如还在本地开发的)不在范围内。
- 更新需要 Host 提供 pluginManager 服务(DSH 自带)。没有它时 update 回 501,不会退化成自己起一个 dsh 子进程去写 profile。
- 重启需要装了 dsh-plugin-restart。发布台不自己杀进程——停哪个、怎么拉起来是那个插件的契约。
- 判定更新时,读的是 node_modules/<name>/package.json 里的版本,不是依赖字符串。用 link: 时,这个版本等于本地检出的版本,所以「版本一样」绝不能当成「装的就是发布的那份」——这正是 checkout 这个状态存在的理由。
- npm 推送复用 Host 自己的包管理器:优先用 profileContext.packageManager(DSH 自带的 pnpm),其次 DSH_PNPM,再其次打包应用自带的 resources/runtime/pnpm,最后才轮到 PATH 上的 pnpm。走到最后一步而机器上没有 pnpm 时,报错会明说 ENOENT 以及它找的是什么。源码里 npm-status 只做 whoami,网络那一侧是 HTTPS fetch(不走 HTTP_PROXY,本机实测直连可达)。
- npm 推送没有 provenance,而且用的是长期 token(可写 token 最长 90 天)。真需要 --provenance(构建来源可验证),或者不想在机器上放任何 token 的包,应当走 trusted publishing(GitHub Actions OIDC)——那才是 CI 的正路,面板这条路只是「在这台机器上推一次」。
- npm 在 2025-11 之后只有 Granular Access Token:Classic token 已全部撤销,也不能再创建。可写 token 默认强制 2FA,最长 90 天。任何还写着「建一个 Classic Automation token」的文档都已经过期了。
- private: true 的包(例如 dsh-knowledge-console)面板不会提供推送按钮,这是有意的:那行字段是作者的决定。

License

MIT,见 LICENSE。
预览分享卡片长什么样 ↓SOH4C4759/dsh-plugin-cicd
分享 · DeepSeek Harness Hub
SOH4C4759/dsh-plugin-cicd

DeepSeek Harness 的发布台:侧边栏入口加主面板页面,展示每个仓库最新的 CI 运行、发布记录,以及本地检出是否领先于发布版本,支持一键构建、发布、发布草稿。复用机器上已认证的 gh 命令行,不保存任何凭据。

回到顶部