Conversation
架构审查后整改清单的批次 A。这一批的共同特征:都不是写错的代码,而是没人
验证过的代码路径。A5/A6 是有实际后果的行为变更,正文里单独标注。
1. onRetry 死路径 —— 撞额度时重试途中的换号从来没生效过(4.17.0「已知问题」
已记录)。这条路径上实际有三层缺陷,只补第一层仍然不会切号:
- sendToCC 从不 await opts.onRetry;
- headers 在重试循环之外构建一次,换号后仍带着旧 key;
- 路由里的回调给局部变量 apiKey 赋值,而 opts.apiKey 早在构造参数对象时
就把旧值快照进去了。
契约随之调整:onRetry 现在**返回**下一次要用的 apiKey(返回 undefined =
沿用当前 key)。
行为变更:撞额度时会在重试途中切到另一个账号,该请求的上游归属随之改变。
注:rotationMode !== 'auto-quota' 或 accounts.length <= 1 时回调必然返回
undefined(config.ts:479),单账号 manual 部署观测不到差别。
2. upstream.timeoutMs 从此真正生效 —— 此前它被加载、写入默认值、并在
/api/status 与仪表盘展示,但 src/ 里 0 个消费点;唯一起作用的 idleTimeoutMs
每收到一个字节就重置,因此一个持续 trickle 的上游可以无限期挂住连接。新增
跨「等响应头 + 读流」两阶段的挂钟上限,超时归类为既有的 REQUEST_TIMEOUT。
行为变更 / 需要关注:任何长度超过 timeoutMs(默认 600s)的请求现在会被切断。
此前它能活下来纯粹是因为这个配置不执行。
3. npm test 不再对推理路由零覆盖地报全绿 —— 本仓库所有集成用例都 spawn 编译
产物,缺 dist/ 时被 describe.skipIf 静默跳过(实测:未构建时 244 passed /
42 skipped 且退出码 0)。现在 pretest 自动构建,并在 beforeAll 首行加了明确
报错。注意 vitest 在一个文件没有任何可运行用例时不会执行文件级 beforeAll,
所以 tests/integration.test.ts 里那条不带 skipIf 的前置用例是这套防护的触发器。
4. 「发现新版本」提示恢复工作 —— 改为读 /tags 并按 semver 取最大。此前读的是
releases/latest,而本仓库只打 tag 不建 Release 对象:实测 releases/latest 停在
v4.12.0 而 tag 已到 v4.17.0,于是自 v4.13.0 起该提示永远不会触发。(另一条路线
是恢复创建 GitHub Release 并补齐缺的 5 个,未在本次改动内。)
5. /api/auth/manual-login 不再明文回传上游 apiKey,改为与 /api/accounts 一致的
apiKeyMasked。loginNewAccount 的返回类型仍带完整凭据(内部调用方需要),收口
在 HTTP 边界。SPA 只读 status/error 与 apiKeyMasked(index.html:471,493,523),
无消费方受影响。
6. 仪表盘 badge() 转义 text —— title 参数一直走 esc(),text 却是裸拼进 innerHTML,
而调用点把上游定价页抓来的 m.deal.discountPercent 直接传入。全站仍无 CSP,
故属纵深防御缺口。
7. pkg.assets 移除 models.json —— 它是运行时生成的缓存且已在 .gitignore 里,
全新克隆上打包会静默缺该资产。
附带修掉本次新增测试引入的一处自伤:admin-key-mask 走进程内 app.inject,而 .env
路径按 getProjectRootDir()(=cwd) 解析,于是账号保存的副作用把 .env(含
COMMANDCODE_API_BASE)写进了仓库根;env 的优先级高于 config.json(config.ts:264),
且 .env 命中 .gitignore 在 git status 里看不见。新增 COMMANDCODE_ENV_PATH 覆盖,
与 CONFIG_PATH / MODELS_CACHE_PATH 同一套约定,不设置时行为一字不变。
测试:新增 5 个文件共 16 项,全部先观察到失败再实现。全量 304 项通过(原 288+16),
tsc --noEmit 与 eslint . 无错误;语句覆盖率 48.36% → 57.57%。
覆盖率读数说明:先前记录的 57.8% 偏高,那多出的 12 条语句是仓库根存在 .env 时被
顺带跑到的 loadEnvFile() 解析路径 —— 也就是说该路径至今没有直接测试覆盖。
本次未改动但已记入 CHANGELOG「已知问题」:管理面默认零鉴权与 /v1、/api 共用一把
密钥(批次 B);syncEnvFile 把 apiBase/ccVersion 硬编码成默认值(config.ts:342-343),
用自建上游的人动一次账号就会在下次重启被静默改回公网默认。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
架构审查后整改清单的批次 A(7 项,全部 S 量级)。共同特征:不是写错的代码,而是没人验证过的代码路径——用户和 CI 都看不见。
onRetry死路径:撞额度时重试途中的换号从未生效。这条路径实际有三层缺陷,只补第一层仍然不会切号(sendToCC从不 await 回调 /headers在重试循环外构建一次 / 路由回调改的是局部变量而opts.apiKey已快照旧值)。契约调整:onRetry现在返回下一次要用的 apiKey。upstream.timeoutMs从此真正生效:此前被加载、写默认值、在/api/status与仪表盘展示,但src/里 0 个消费点;唯一生效的idleTimeoutMs每收到一个字节就重置,持续 trickle 的上游可无限挂住连接。新增跨「等响应头 + 读流」两阶段的挂钟上限。npm test不再零覆盖报全绿:集成用例全部 spawn 编译产物,缺dist/时被describe.skipIf静默跳过(实测未构建时 244 passed / 42 skipped 且退出码 0)。现在pretest自动构建 +beforeAll首行明确报错。/tags按 semver 取最大。本仓库只打 tag 不建 Release,实测releases/latest停在 v4.12.0 而 tag 已到 v4.17.0,于是自 v4.13.0 起该提示永不触发。/api/auth/manual-login不再明文回传上游 apiKey,改用与/api/accounts同一套apiKeyMasked;收口在 HTTP 边界。badge()转义text:title一直走esc(),text却裸拼进 innerHTML,而调用点传入上游定价页抓来的m.deal.discountPercent。全站无 CSP,属纵深防御。pkg.assets移除models.json:运行时生成物且已在.gitignore,全新克隆上打包会静默缺该资产。附带修掉本批新增测试引入的一处自伤:
admin-key-mask走进程内app.inject,.env路径按getProjectRootDir()(=cwd) 解析,于是账号保存的副作用把.env(含COMMANDCODE_API_BASE,而 env 优先级高于config.json)写进仓库根,且被.gitignore遮住。新增COMMANDCODE_ENV_PATH覆盖,与CONFIG_PATH/MODELS_CACHE_PATH同一套约定,不设置时行为一字不变。行为变更(需要关注的两条)
upstream.timeoutMs(默认 600s)的请求现在会被切断。此前它能活下来纯粹因为这个配置不执行。rotationMode !== 'auto-quota'或账号数 ≤ 1 时必然走return undefined分支(config.ts:479),即单账号 manual 部署观测不到差别。对日常使用的影响核查(判据来自运行中的实例与它自己的数据)
现行实例:
127.0.0.1:9090、v4.13.0、authRequired:false、1 个账号、rotationMode:manual。timeoutMs:600000本就是 config.json 里声明的值data.status/data.error(index.html:471),列表用apiKeyMasked(:493、:523),无消费方受影响/api/status实测latest:"v4.12.0"、available:false,即该 bug 的活证据Test plan
onretry-account-switch(断言上游实际收到的Authorization头变化,而非「回调被调用过」)、upstream-total-timeout(刻意把idleTimeoutMs设得大于总时限,使超时只能归因于挂钟上限)、admin-key-mask、spa-badge-escape(取出 index.html 里真实的badge/esc/BADGE_TONES源码执行)、update-check-tagsnpm test→ 304 项通过(原 288 + 16),Windows 本机跑过 3 轮tsc --noEmit、eslint .均 0 错误dist/直接vitest run tests/integration.test.ts→ 退出码 1 并打印「缺少构建产物」.env时顺带跑到的loadEnvFile()解析路径——反过来说该路径至今没有直接测试覆盖,归入后续批次。明确未做(已记 CHANGELOG「已知问题」)
管理面默认零鉴权与
/v1、/api共用一把密钥(批次 B);出站 fetch 未设redirect:'manual'(二跳可逃逸 SSRF 白名单);OAuth 回调缺state时放行;syncEnvFile把apiBase/ccVersion硬编码成默认值(config.ts:342-343),用自建上游的人动一次账号就会在下次重启被静默改回公网默认(默认部署两者取值相同故不触发);rewriteSafely的renameSync无 try/catch。