Loading... # LLMux 从 TypeScript 重写 Rust 后,我是如何把一个 CLI 工具发布到 npm 的 > 记录时间:2026-09-08 > 相关版本:`llmux-cli@0.5.24` > > **项目仓库**:[github.com/zhMoody/llmux-cli-rs](https://github.com/zhMoody/llmux-cli-rs) > **npm 包**:[`llmux-cli`](https://www.npmjs.com/package/llmux-cli) ## 背景:一个 Rust 重写项目要"上线" LLMux 是我做的一个**本地优先的 AI API 网关**。客户端工具(Claude Code / Codex / Gemini CLI)只要指向 `http://localhost:25975/v1`,它就能把请求路由到多个厂商账户,支持多协议透传、粘滞会话自动 failover、模型 alias 映射、API key 白名单。 最初它是 TypeScript 写的,但为了更快的启动速度和更低的内存占用,我用 Rust 完整重写了一遍。重构完成后面临一个很实际的问题:**怎么把它分发出去?** 我已经用 [cargo-dist](https://opensource.axo.dev/cargo-dist/) 接好了 Homebrew(`brew install zhmoody/tap/llmux`)和 Linux/macOS 的一键脚本,但一直还缺一个渠道:**npm**。 这篇文章记录的就是我为了给这个 Rust 二进制工具接 npm 发布,踩过的一串坑,以及最终的自动化方案。 ## 为什么不用 `npm` 直接发?因为这是个 Rust 项目 首先明确一个前提:`llmux` 是个 Rust 二进制,**不是 JS 包**。npm 上没法直接发布一个 `.cargo` 构建产物——npm 用户期望的是 `npm install -g xxx` 之后能直接用。 有两种主流做法: 1. **手写一个 npm wrapper 包**:package.json 里写 postinstall,下载 GitHub Release 里的预编译二进制 2. **用 cargo-dist 原生支持**:cargo-dist 0.25+ 自带 npm installer,自动生成包装项目并在 CI 里发布 我最初走了弯路,手写了一个 wrapper(`install.js` + `cli.js`),后来才迁移到 cargo-dist 原生方案。**能用工具原生支持就别手写**——cargo-dist 会帮你管理好版本同步、平台矩阵、CI 编排,全部自动化。 最后我选的是 cargo-dist 的 **reusable workflow** 方案,这背后的原因后面说。 ## 第一大坑:包名直接被 npm 拒绝 先查包名是否可用——`npm view llmux` 返回 404,看起来没问题。但**真发布时报错**: ```text 403 Forbidden - Package name too similar to existing package flux; try renaming your package to '@aluomanga/llmux' ``` **npm 有一个防抢注(anti-typosquatting)机制**:`llmux` 和已存在的 `flux` 太相似(都以 `lux` 结尾),所以即使包名不存在也会被硬性拦截。404 只代表"不存在",不代表"可注册"。 解决办法:用我另一个项目已经在用的名字 `llmux-cli`(`npm view llmux-cli` 显示 maintainer 就是我)。 > **经验**:npm 包名有相似性检查,`npm view` 查不到 ≠ 可以注册。测试包名时最好直接查已存在的相似包。 ## 第二大坑:cargo-dist 生成的 `release.yml` 不能手改 cargo-dist 是**配置驱动**的:你改 `dist-workspace.toml`,然后用 `cargo dist generate` 生成 CI workflow。它生成的 `release.yml` 顶部就写着: ```yaml # This file was autogenerated by dist ``` 我一度想直接在 `release.yml` 里手加一个 npm 发布 job,结果 CI 跑起来后在 `plan` 阶段就失败——**cargo-dist 会 diff 校验生成的 workflow,发现你手改过就直接拒绝**。 所以 npm 发布**不能**塞进 cargo-dist 生成的 `release.yml`。正确做法是用它官方的 **custom publish job** 机制:在 `dist-workspace.toml` 里配置 `publish-jobs = ["./publish-npm"]`,它会自动生成一个调用你仓库里 reusable workflow 的 job。 ## 第三大坑:Trusted Publishing 报 404 这是最耗时的一个。npm 的 Trusted Publishing(OIDC)能让我**不用在 CI 里存 token、也不用管 2FA 的 OTP**——这是官方推荐的更安全方案。 配置好 trusted publisher 后发布,**OIDC 身份验证通过了**(日志里有 provenance 签名成功),但最后一步报: ```text npm error 404 Not Found - PUT https://registry.npmjs.org/llmux-cli - Not found ``` Provenance 签名成功 ≠ trusted publisher 命中!**sigstore 的 OIDC 和 npm 的 OIDC 是两条独立通道**,前者成功不代表后者匹配。 真正的根因是 **npm 版本太旧**: > **Trusted Publishing 要求 npm ≥ 11.5.1。** 如果 npm 低于 11.5.1,即使 OIDC 权限配置正确,发布也会失败。 我在 workflow 里写的是 `node-version: "22"`,它自带 npm 10.x。`npm` 的 OIDC token exchange 逻辑在 10.x 里整个不存在,于是**静默跳过 OIDC**,回退用注册表 token(空的)去 PUT,最终 404。 修复只要一行: ```yaml - uses: actions/setup-node@v4 with: node-version: "24" # 关键:npm ≥ 11.5.1(Trusted Publishing 最低要求) registry-url: https://registry.npmjs.org ``` > **经验**:npm 的 OIDC/Trusted Publishing 对 npm CLI 版本有硬性要求(≥11.5.1,随 Node 22.14+ 附带),低于会静默失败而不是报错。node 22 默认带 npm 10 是坑。 ## 第四大坑:reusable workflow 的权限传递 把 npm 发布做成 reusable workflow 还有个权限坑。GitHub 规定:**callee 不能请求 caller 未提供的权限**。 我的 `publish-npm.yml`(被调方)顶层只写了 `id-token: write`,但一旦加上 `contents: read`,CI 就报: ```text Invalid workflow file... The workflow is requesting 'contents: read', but is only allowed 'contents: none' ``` 因为 caller(cargo-dist 的 `custom-publish-npm` job)只有 `id-token` 和 `packages` 权限。解法是 callee **只声明 `id-token: write`**,别请求额外权限。 ## 第五大坑:npm 网页不显示 README 发布成功后我检查 npm 页面,却显示 **"This package does not have a README"**。明明仓库根有 `README.md` 啊? 下载 tarball 一看,里面只有: ``` package/README.zh-CN.md # 没有 README.md! ``` 原因:cargo-dist 扫描仓库 README 时用 `starts_with("README")` + **先到先得**,目录遍历顺序恰好让它选中了 `README.zh-CN.md`(中文版)。而 **npm 网页只认标准名 `README.md`**,`README.zh-CN.md` 不算标准 README。 修复:显式指定 readme,不再依赖扫描顺序: ```toml # crates/llmux-bin/Cargo.toml [package] readme = "../../README.md" ``` 这样 cargo-dist 会把英文 `README.md` 以标准名打进包,npm 网页就能正常显示了。 > **经验**:cargo-dist 自动收集 README 时用的 `starts_with` + 先到先得,多个 README 变体时顺序不可控。多个语言版本时务必显式指定 `readme`。 ## 最终方案:一条命令全自动发布 解决了所有坑后,现在的发布流程是: **`dist-workspace.toml` 配置:** ```toml installers = ["shell", "powershell", "homebrew", "npm"] npm-package = "llmux-cli" # npm 包名 publish-jobs = ["homebrew", "./publish-npm"] # 自定义 reusable publish job ``` **`.github/workflows/publish-npm.yml`(reusable workflow):** ```yaml name: publish-npm on: workflow_call: inputs: plan: { required: true, type: string } permissions: id-token: write # OIDC,免 token 免 2FA jobs: publish: runs-on: ubuntu-latest env: PLAN: ${{ inputs.plan }} steps: - uses: actions/setup-node@v4 with: node-version: "24" # npm ≥ 11.5.1 registry-url: https://registry.npmjs.org - uses: actions/download-artifact@v4 with: pattern: artifacts-* path: npm/ merge-multiple: true - run: | for release in $(echo "$PLAN" | jq -c '.releases[] | select([.artifacts[] | endswith("-npm-package.tar.gz")] | any)'); do pkg=$(echo "$release" | jq -r '.artifacts[] | select(endswith("-npm-package.tar.gz"))') npm publish --provenance --access public "./npm/$pkg" done ``` **npm 侧一次性配置**([npmjs.com](https://www.npmjs.com/) → 包 → Settings → Trusted publishing): - Organization: `zhMoody` - Repository: `llmux-cli-rs` - Workflow filename: `release.yml`(reusable 场景 npm 校验 caller 名) **以后发布只需一条命令:** ```bash ./scripts/release.sh # 升版本 + 提交 + tag + 推送 ``` 打 tag 后 cargo-dist 自动完成:构建 5 个平台二进制 → GitHub Release → Homebrew tap → npm(OIDC + provenance,免 2FA)。 ## 总结 给一个 Rust CLI 工具接 npm 发布,最顺的路是 **cargo-dist 原生 npm 支持 + reusable workflow + OIDC Trusted Publishing**。但有几个坑值得记: 1. **npm 包名相似性检查**——`npm view` 查不到不代表能注册 2. **cargo-dist 自动生成的 workflow 不能手改**——用它的 custom publish job 3. **Trusted Publishing 需要 npm ≥ 11.5.1**——node 22 默认 npm 10 会静默失败,务必用 node 24 4. **reusable workflow 权限传递**——callee 只能声明 caller 有的权限 5. **npm 只认标准名 `README.md`**——多语言 README 时显式指定 `readme` 现在 `llmux-cli@0.5.24` 已经在 npm 上线,全程 OIDC 免 2FA,还带了可验证的 provenance 签名。发布一次,GitHub Release、Homebrew、npm 三个渠道全自动。 --- **相关链接** - ? 项目仓库:[LLMux (github.com/zhMoody/llmux-cli-rs)](https://github.com/zhMoody/llmux-cli-rs) - ? npm 包:[`llmux-cli`](https://www.npmjs.com/package/llmux-cli) - ? Homebrew:`brew install zhmoody/tap/llmux` - ? 安装即用:`npm install -g llmux-cli` END 最后修改:2026 年 09 月 08 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏 下一篇 发表评论 取消回复 使用cookie技术保留您的个人信息以便您下次快速评论,继续评论表示您已同意该条款 评论 * 私密评论 名称 * 🎲 邮箱 * 地址 发表评论 提交中...