下载破千:一个网关配套 CLI 的 20 天(20260929)
- 2026-10-02 15:43:36

主题:
llm-api-gateway-cli在 npm 上的累计下载量突破 1000,写一篇记录。本文不是发布稿,是账本:数字、日期、版本、坑,逐条可复核——复核方式写在第 1 节末尾。相关文档:20260911-项目介绍.md(项目是什么、能干什么)、20260929-迭代计划-技能页三分页.md(今天刚落地的那一版)。
一、先把数字摊开
npm install -g llm-api-gateway-cli——这个包在2026-09-23那天,累计下载量过了 1000。
| 1.0.0 首次发布到 npm | |||
| 09-23 | 1227 | 破千 | |
几条必须说清的口径,否则这张表就成了修辞:
- 它不等于一千个人
。npm 的下载计数包含 CI、镜像回源、同一台机器反复安装、以及发布当天的验证性安装。09-21 首日 546 次明显偏高,本项目的习惯是不把「装了几次」说成「有多少用户」; - 数据源是 npm 官方 API
:`https://api.npmjs.org/downloads/range/2026-09-14:2026-09-29/llm-api-gateway-cli`,任何人现在都可以再查一次(数字只会变大); - 「破千」的确切时刻算不出来
:npm 只按天聚合。09-22 结束是 987,09-23 结束是 1227,所以破千发生在 09-23 这一天之内 —— 只能说这么准,再多说一句就是编的。
一句话概括这 20 天:09-09 第一次提交,09-21 上 npm,09-23 破千,09-29 发到 1.0.9。
二、起点:它本来只是几个验证脚本
第一次提交在2026-09-09。当时的定位很小:你有一个本地跑的 LLM API Gateway(http://127.0.0.1:9000),它把上游模型统一成 OpenAI/Anthropic 两种标准方言——问题是你凭什么确信它真的能用。光有管理后台的「测试连接」不够,真实开发里模型是被这些东西调用的:一段openaiSDK 的代码、一个 Anthropic 生态的客户端、一个要在终端里读你代码的 Agent。
所以一开始是四个脚本,一个方言一个:cli-openai.js走/v1/chat/completions,cli-anthropic.js走/v1/messages,cli-claude-code.js把 Claude Code 接到自己的网关上,cli-agent.js是网关自带的原生 Agent。
转机是把 Agent 内核抽出来给 Web 和 CLI 共用:lib/agent.js管模型↔工具的循环,lib/tools.js管工具与沙箱,lib/runner.js管「建会话→跑一轮→遇写入挂起→等批准→断点续跑」这条与传输无关的流程。Web 用 SSE+HTTP,CLI 用readline问 y/N——同一套runAgent,两种传输。
抽出来之后,验证网关的成本和「顺手用起来」的成本被摊平了:验证完的同一个程序,就是你日常在终端里干活的那个程序。这是这个项目后面所有事情的地基。
还有一个小细节值得记一笔:docs/20260911-项目介绍.md的第八节「目前的边界(不藏着)」里,第 2 条写着「private: true还没去掉,许可证仍是UNLICENSED——尚未正式发布到 npm」。八天之后,它以 MIT 发布到了 npm。那份文档现在没有改,留着当对照。
三、20 天里长出来的东西
按 npm 发布时间排(1.0.1–1.0.6是同一天的连续修补,主题以仓库提交信息为准):
首次发布到 npmnpm link) | ||
MCP 自助登记/mcp add)+ 网页查看命令行会话(/sessions)+ CLI 侧英文(--lang en) | ||
| skill(技能包)初版 | ||
包本体的增长也说明了一些事:1.0.0是 50 个文件/解包 1.12 MB,1.0.9是77 个文件 / 解包 1.80 MB。
而截止发稿,仓库里的账是这些:
lib/ | |
public/ | |
lib/public/ 行数 | |
tests/ | |
docs/ | |
恰好 2 个openai、@anthropic-ai/sdk,且只被模式一 / 模式二用到) |
测试行数约等于主代码的 65%——这个比例不是刷出来的,见第 4 节。
一条写稿当天的实测注脚(不是缺陷,是环境):在受限沙箱里跑
npm test,聚合器会停在第一个文件tests/deps.test.mjs,报spawnSync … EPERM。这与docs/20260911-项目介绍.md里记过的是同一件事——受限环境下 Node 的child_process只要用管道式 stdio 就一律 EPERM,属宿主限制,跟仓库代码无关。本次会话能确证的只有「聚合器起得来、离线组清单是 41 个文件」;要拿"全绿"这句话,得在不受限的终端重跑。
四、比下载数更值得记的四个决定
1. 依赖面冻结:需要新能力就自己写
package.json的dependencies恰好是openai与@anthropic-ai/sdk,而这两个只被两个「验证方言」的脚本使用;主力(原生 Agent、Web 服务、Agent 内核)全部走 Node 原生能力。这条不是口号,是tests/deps.test.mjs真的拦着的:要加依赖,那条冻结断言会红,必须显式改断言并说明理由。
于是这些都得自研(纯函数+单测,可整体替换):
lib/skillpkg.js—— stored-zip 打包与读回(用 node:zlib,测试里用独立实现读回并核对公开 CRC 向量);lib/difftext.js—— 行级 diff(最小编辑脚本 + 上下文窗口 + 大文件快路径),审批预览与「本轮改动」清单共用同一份,所以预览说改了 3 行、汇总就不会说 5 行; lib/mdrender.js—— 终端 markdown 渲染,只在真 TTY 下开启,只加样式、一个字都不改; lib/i18n.js—— 约 80 行的中英切换( --lang en),缺英文自动回落中文,不会出现半中半英或undefined。
代价是没有花哨的界面;收益是用户装 CLI 时零手工步骤,不会撞上 node-gyp、postinstall、或「请再跑一条安装命令」。
2. 文档不许手抄
操作手册页(/manual)里的指令清单,不是人写的,是页面通过GET /api/commands从lib/commands.js与public/task-slash.js两张表直接生成的。tests/manual.test.mjs拿真实接口数据断言「表里每一条都出现在手册里」——改代码即改手册,不存在「文档里少几条」的可能;改脚本或改默认值漏改手册,测试直接红。
这条规则救过很多次场:安装脚本名、主命令、最低 Node 版本、密钥文件名、.env查找顺序、默认网关地址,全部由测试与package.json、两份安装脚本、lib/common.js、lib/settings.js、lib/secrets.js交叉钉住。
3. 安全边界靠机制,不靠提示
八个工具里,所有路径都相对工作目录解析,../、绝对路径、盘符一律以「路径越界」拒绝;除了字符串校验,还会对最近存在的父目录取 realpath 再校验一次,所以目录联接/符号链接也逃不出去(tests/tools.test.mjs里有专门的对抗用例)。越界不是崩溃,是把拒绝原因回填给模型,它自己就能纠正方向。
由此长出三种审批模式,切换跟任务走:
- / + diff) | |
只给只读工具<工作目录>/docs/YYYYMMDD-<概要>.md,确认后点「按计划执行」才动手 |
bash更是默认关闭:它把安全面从路径级升到命令级,所以必须由宿主显式--allow-bash才出现。
4. 同一件事只有一份实现
密钥只有一个落点(~/.llm-api-gateway-cli/credentials.json,POSIX0600,永远不进 config.json,界面填的和命令行写的都是它,config get key只回掩码);配置只有一个实现(lib/settings.js);计划落盘只有一个编排函数(lib/plandoc.js的savePlanDocFromOutcome,Web 任务页与终端 CLI 共用);斜杠命令只有一张表(两端同名同义)。
两处各写一遍,迟早互相矛盾——这句话在这个仓库里出现的频率,比「零依赖」还高。
五、几个真实踩到的坑(这一节最值钱)
破千的故事里,真正值得留档的不是数字,是这些:
「我在终端聊完,去网页任务列表里找,找不到。」 这是 2026-09-22 的真实反馈。存储布局本身没问题(网页列的是
tasks/里的任务,终端 REPL 的对话在<数据根>/sessions/,两套存储),缺的只是一个看的入口。于是加了/sessions—— 只读回放,没有输入框、没有写入接口,要接着聊回终端gateway-agent -i --resume <id>。用户说的不是「功能缺失」,是「我看不见」,这两件事的修法完全不同。MCP 被当成「写入类」,于是非交互模式下一次都用不了。 一次调研要查十几个地点,
-p模式下每次 MCP 调用都被拒;唯一绕法是--yes,那又同时放开了任意文件写入 —— 用一个更大的权限换一个更小的功能,这个交换不成立。后来把「是不是写入工具」和「是不是本机文件写入」拆成两个谓词,MCP 走「看得见但不逐个批准」。「计划模式不给 MCP 工具」这条边界没有变 —— 那是「给不给模型」的问题,和「要不要人工批准」是两件事。曾经有一版「只读批次并发」,撤掉了。 五个只读工具全是同步实现(
readFileSync/readdirSync),Promise.all式的重叠一次都没发生(实测 1 个grep342ms、同批 4 个 1413ms —— 4.13×,纯串行),却要多背一套并发语义。要做真并发得先把只读工具改成异步 I/O —— 不值得为没发生的重叠买单。「下载了就能用」是个误会。 技能页上线后,用户下到
.skill包却发现用不了。原因是下载 ≠ 能用:浏览器存进的是下载目录,要解压到~/.agents/skills/<skill_id>/才算装上,而那个目录是 DSH / Codex 这类 harness 加载 skill 的地方,本 CLI 自己不读它。于是页面顶上补了「上传 → 下载 → 安装 → 使用」四步流程,把每步的去处各写一句话。这类边界不写清楚,用户就会把它当 bug 报回来。沙箱里的
spawn EPERM曾经被写进文档当成缺陷。 后来在不受限的终端重跑,bash一节全绿 —— 那纯粹是宿主环境的假象,跟lib/tools.js没关系。当时那句话现在还在文档里,后面跟着一条「更正」。 写错就写错,改过来并留着痕迹,比悄悄抹掉强。
六、还没做完的(照旧不藏着)
npm 上那句 description还是「CLI 测试工具:验证 LLM API Gateway……」—— 它已经不只是一个测试工具了,这句话该重写;- 逐步执行
(按计划里的某个节点、带着 planNode+skillId单独跑一步)目前只有外部调用/api/task这个入口,页面上还没有按钮; - 计划上送网关规划树
只在任务页做(它需要服务端的连接与密钥),终端里落的计划文件在磁盘上、但不在网关那张表里 —— 这不是漏提交,是如实; bash默认关闭这条不打算改:它是刻意的取舍,不是没做完; 下载量的构成(谁在装、装去干什么)我们不知道,也没打算编。
七、结语
20 天,22 次提交,10 个 npm 版本,1551 次下载。
这个项目有意思的地方在于它的两条线是同一件事:验证网关能不能用,和顺手用它干活,共用同一个程序。所以它没法靠「写个 demo」交差——一个要被人放进真实仓库里改文件的工具,路径越界、符号链接、审批闸门、密钥去哪、上下文记不记得住,每一条都得真回答。
一千次下载不是一个成就,是一个提醒:现在有人在你机器之外跑这段代码了。上面第 5 节那些坑,就是这种提醒换来的。
装它仍然是一条命令:
npm install -g llm-api-gateway-cli
gateway-agent setup # 问一次网关地址与密钥,写进本机gateway-agent # 裸敲就是启动器:对话 / 任务,命令行 / 网页或者先看看它长什么样:gateway-web起服务,打开 http://127.0.0.1:3100/manual ——第一节就是「安装与启动」。
附:本文所有数字的复核方式
https://api.npmjs.org/downloads/range/2026-09-14:2026-09-29/llm-api-gateway-cli | |
https://registry.npmjs.org/llm-api-gateway-cli`(读与dist.fileCount/dist.unpackedSize`) | |
git log --reverse --date=short --pretty=format:"%ad %h %s"git rev-list --count HEAD | |
lib/public/、tests/ 下直接数(Get-ChildItem ... | Measure-Object) | |
package.jsondependencies(三个键都认得出来)与 tests/deps.test.mjs | |
lib/tools.jslib/runner.js、lib/secrets.js,以及 README.md 对应小节 |
本文写于 2026-09-29,数据为当日查询结果(npm 侧结算到 09-27)。