Repository navigation
Releases: DeliciousBuding/metapi-go
Release list
v0.23.0
[v0.23.0] — 2026-10-09
完善代理日志的计时、用量与模型来源,加入 Octopus v5 渠道图导入,修复停用状态与原生协议续轮。
安全
- 运行时:升级 Go 标准库安全补丁;使用本版镜像或二进制即可,无需修改配置(#1414)。
修复
- 站点与账号停用:关联渠道立即显示继承不可用并退出选路,恢复后重新可用;不改变渠道自身启用配置或共享模型路由(#1416 → #1419)。
- 直连 Responses:保留原生 reasoning、工具结果与续轮字段,不再套用旧站点请求清洗;模型映射和显式参数覆盖仍生效(#1424)。
- 代理日志:列表和详情的
isStream统一返回布尔值,SQLite 非流式记录不再显示多余的数字 0(#1420、#1423)。 - 代理日志:流式耗时颜色跟随首输出而非响应头;CSV 保留原有列序并追加计时、模型来源、重试和缓存用量,未知值保持空白。
- 日志 CSV:按后端分页读取筛选记录,修复超过 100 行静默漏导出;超过 10,000 行时明确提示截断。
变更
- 代理日志:区分总耗时、上游首字节与 SSE 首输出;
proxy_logs新增可空first_output_latency_ms,API 返回firstOutputLatencyMs,旧记录显示未知(#1415)。 - 代理日志:分别展示请求模型、选路模型与上游自报模型;
proxy_logs新增可空upstream_reported_model,API 返回upstreamReportedModel,不将其作为模型身份验证(#1422)。 - 代理日志:展示缓存读取/写入、未知用量与计费明细;无上游用量或费用时不再伪装成 0,详情显示下游密钥名称与 ID,不返回密钥值(#1407、#1415、#1420)。
POST /api/settings/backup/import:支持 Octopus v5 渠道、凭据、模型、授权和分组事务导入,预览与提交须带稳定的X-External-Origin-Key(#1376 → #1418)。- Octopus 重导入:预览来源快照删除项,移除前须确认
X-Octopus-Replace-Origin: true;不删除其他来源或原生记录(#1418)。 - Octopus 导入:API keys、定价和非代理设置不启用,须确认
X-Octopus-Import-Mode: channels-only;不能表达的自定义策略仍拒绝导入(#1418)。 - 控制台:整理桌面与移动端信息层级、日志筛选及详情,演示模型更新为现代系列;演示用量与价格不是实时模型报价(#1388 → #1407、#1413)。
开发者可见
文档
v0.22.1
v0.22.0
[v0.22.0] — 2026-10-07
统一管理概览的统计时间范围,兼容 New API scoped PAT,整理桌面管理与模型测试动线。
变更
GET /api/stats/overview:新增period=24h|7d|30d|all(默认7d),返回同窗请求、模型与上游聚合;无效值返回 400(#1408)。- 管理概览:请求、成功率、成本与模型排行共用统计范围;当前余额和通知仍是实时状态,不随历史范围变化(#1408)。
- New API 账号登录:使用所需权限范围的 scoped PAT;重绑时复用已验证的有效 PAT,权限不足须在上游更新或导入合适 PAT(#1399 → #1403)。
- 模型测试器:按供应商分组搜索模型;模板和采样选项收于高级设置,响应阅读暂停跟随用户向上滚动(#1408)。
- 桌面列表:少量行时表格随内容收拢;模型名称改用 14px 中等字重界面字体,详情和辅助信息保留独立层级(#1408)。
修复
- 站点与账号导航:站点状态筛选与账号钻取链接保持选中条件和数值 ID,避免筛选丢失或跳转错误(#1408)。
v0.21.3
[v0.21.3] — 2026-09-14
代理日志展示下游客户端身份。
变更
- 代理日志:新增「客户端」列,以胶囊展示检测到的下游客户端(Codex CLI / Claude Code / Gemini CLI / SDK family),详情抽屉会话区同步升级;未识别的客户端显示纯文本(#1374)。
v0.21.2
[v0.21.2] — 2026-09-14
带数据设计评审修复批次:attention 空白行、图表轴刻度、表格横滚提示、设置页保存按钮、OAuth 计数;代理日志模型列改品牌胶囊。
修复
- 仪表盘与顶栏「需要关注」:空标题事件不再渲染空白行,回退显示事件内容(#1369)。
- 收支 / 趋势图 Y 轴:刻度精度按轴最大值统一($160 / $8.00 / $1.2K),长标签不再被裁切(#1369)。
- 设置-数据库:「保存配置」按钮不再被卡片边缘裁切(#1369)。
- 数据表:横向溢出时边缘渐隐提示可滚动;空态在可视区居中,641–1024px 不再隐形(#1369)。
- OAuth 连接:列表计数排除 session 导入账号,不再出现「共计 N」但表格为空(#1369)。
- 签到记录 / 代理日志筛选:新增 今天 / 近 24 小时 / 近 7 天 / 近 30 天 快捷范围(#1369)。
变更
- 代理日志「模型」列:渲染为品牌胶囊(品牌图标 + 型号名),未知模型保持纯文本(#1372)。
开发者可见
v0.21.1
[v0.21.1] — 2026-09-13
管理界面打磨批次:空态与零值呈现收敛、移动端顶栏瘦身、可观察性导航单套化、品牌 logo 随主题;截图证据管道注入演示数据。
修复
- 签到记录 / 代理日志日期范围筛选:移动端竖排堆叠,输入框不再被裁切(#1351 → #1363)。
- 空数据表:空态高度收拢,0 行时首页不再显示分页条,列表页空态补齐实体图标(#1355 → #1365)。
- 汇总指标卡:0 值不再按成功/失败语义着色(代理日志汇总、仪表盘今日快照)(#1352 → #1365)。
- 账号管理:「添加账号」禁用态补充原因提示(#1356 → #1365)。
- 移动端顶栏:操作图标由 7 个收敛为 5 个(语言 / 调色板 / 明暗移入抽屉),触控目标加大(#1357 → #1365)。
变更
- 可观察性:移除侧栏钻取导航,页内 tab 为唯一导航(#1353 → #1366)。
- 品牌 logo:改为内联 SVG,渐变跟随主题主色;静态 favicon 同步为默认靛蓝(#1354 → #1366)。
开发者可见
v0.21.0
[v0.21.0] — 2026-09-13
站点表单自定义请求头示例;主题调色板与预设集重新设计;Web 源码树完成 feature 化重组;管理文档逐句核校。
变更
- 主题:调色板重新设计为靛蓝主色(浅色白字、深色墨字),预设改为 graphite(单色高对比)与 cobalt、lagoon、kelp、moss、ochre、ember、berry、plum 八个色板;已保存的旧预设名回退为默认(#1346)。
- 站点表单「自定义请求头」:新增只读示例插入入口,提供常见 CLI 客户端的 header JSON;字段已有内容时不覆盖(#1132)。
- 管理界面:下游密钥独立为自有 feature 目录;表格工具栏、行列渲染与列模块重写,行为不变(#1336、#1337、#1320–#1327)。
- PostgreSQL:升级时调和 legacy 序列,避免旧库 id 冲突(#1294)。
修复
- 站点公告:通知链接指向上游站点,不再指向 /api/notice(#1302)。
- 复合表单控件:补齐屏幕阅读器可访问名称(#1300)。
- 下游密钥页:错误文案与实际失败原因一致(#1335 → #1337)。
开发者可见
已知遗留
admin_audit_logs与checkin_logs仍无保留期;本版不自动清理已有审计或签到历史。
v0.20.0
[v0.20.0] — 2026-09-09
自动路由生命周期、管理界面与 CLI 协议兼容。原生端点优先;未实现完整转换的协议组合不再裸转发。
安全
- 站点代理:HTTP(S) 环境代理同样拒绝 metadata/link-local 等禁止目标,保留直连地址校验(#1284)。
ACCOUNT_CREDENTIAL_SECRET:示例改为替换提示;生产须自行生成密钥,已有加密凭据换密钥后的恢复步骤见部署说明(#1237)。
修复
/v1/messages:文本及客户端工具可回退到 Chat,并转换 JSON/SSE 返回及工具续轮;不支持的扩展保留原生路径(#1288)。- SSE:显式上游错误记为失败;工具终止原因与完整参数一致;Messages 代理错误使用原生错误事件,不追加成功终止标记(#1278、#1279、#1288)。
- NewAPI 模型发现:禁用
/v1/models时可使用账号已有 PAT;无效 relay key 不被管理凭据掩盖,管理模型列表不冒充推理健康(#1232)。 - NewAPI 令牌:同步与删除覆盖全分页,掩码原值按最多 100 条分批获取;每次最多 1,000 条、10 页、8 MiB 令牌元数据。不完整或失败列表不驱动默认凭据切换,legacy 列表无法证明目标不存在时删除明确失败(#1289)。
/api/accounts/login:上游轮换默认 relay key 后,重新登录同步可用凭据,不再保留失效默认项(#1266)。- 管理接口:部分上游登录、余额、模型和令牌写入失败改为分类错误,不再伪装成空列表、零余额或成功;API-key-only 边界保持明确(#1227、#1244、#1246、#1247、#1248、#1251)。
- 签到:无签到能力的上游返回 skipped;成功事件正常进入通知流,重复签到不重复计算奖励(#1267、#1268、#1269)。
- 下游密钥:已删除的路由不再阻止编辑已有密钥策略(#1229)。
变更
autoCreateModelRoutes:默认 false;开启后随模型同步创建缺失路由,保留手工绑定、禁用策略和密钥授权,模型退订移除自动通道(#1285)。POST /api/routes/rebuild:wait:false返回 202 与 taskId;经GET /api/tasks/{taskId}查询,页面关闭不取消任务,服务器重启不续跑(#1285)。PROXY_FIRST_BYTE_TIMEOUT_SEC:流式请求的 0 使用 90 秒默认响应头预算;正数覆盖默认值,不再受隐藏 30 秒上限截断;响应开始后由流空闲超时控制(#1288)。- 管理界面:整理导航层级、路由编辑与批量通道选择;内嵌 Noto Sans SC,中文字体不再依赖操作系统,超时和协议回退说明与行为一致(#1260、#1286、#1288)。
移除
CLAUDE_CLIENT_SECRET:移除未消费的变量;Claude OAuth 覆盖仅需CLAUDE_CLIENT_ID,使用 PKCE(#1257)。CODEX_HEADER_DEFAULTS_USER_AGENT、CODEX_HEADER_DEFAULTS_BETA_FEATURES:移除从未生效的配置项(#1257)。PROXY_STICKY_SESSION_ENABLED、PROXY_SESSION_CHANNEL_LEASE_TTL_MS、PROXY_SESSION_CHANNEL_LEASE_KEEPALIVE_MS:移除未接线的开关与租约配置;现役 TTL/并发限制不变(#1257)。
已知遗留
- Messages 隐藏 reasoning 回放使用进程内缓存与自有工具 ID,经当前授权策略绑定原通道;上限 256 组 / 8 MiB / 单组 512 KiB / 30 分钟。过期、淘汰、重启或换实例会明确拒绝续轮,不承诺集群粘性。转换范围见客户端接入说明。
admin_audit_logs与checkin_logs仍无保留期;本版不自动清理已有审计或签到历史。
v0.19.0
[v0.19.0] — 2026-09-04
本波只做一件事:把「添加上游 → 登录账号 → 拿到模型 → 建路由 → 发下游 key → 真调一次」这条链做成可重复、可解释、可恢复、不可假绿的产品契约,并修掉四处会让它「看起来可用」的地方。没有新增产品面:无新 adapter、无新协议面、无新 dashboard、无新计费子系统,#1132(请求头模板)继续排队。四态旅程 Fresh / Restart / Aged / Restore 现在全部是 CI 里能红的门(公开承诺表 #1215),且每个门都先用变异探针证明过「坏掉时我真的会红」。
修复
- balance 刷新失败现在说清为什么(#1210,经 #1211 闭合):按需刷新失败此前只回
502 {"message":"balance refresh failed"},而同一刻服务端日志里写着sub2api /api/v1/auth/me: HTTP 401: Token has expired——「凭据过期」「上游拒绝」「上游不可达」「上游没返回余额」是修法完全不同的四个问题,却共用一句文案,运营只能翻日志。现在两个 balance 出口(单个与批量)都保留稳定前缀balance refresh failed并追加一行分类后的原因,例如balance refresh failed: upstream rejected the credential (HTTP 401)。分类而非回显:platform.ExplainUpstreamFailure只在既有UpstreamErrorClass枚举上渲染,不外泄上游 URL、token、query 或原文,WARN 日志另存上游原文与reason=;落不到已知类时回落到「状态本身能证明的部分」,绝不渲染空原因(issue 里那条真实报文恰好落在ClassUnknown,照抄枚举等于什么都没修)。顺带修掉service/balance一处return nil, nil:DB 读失败曾被当成「账号不存在」回 404——与 #1186 治的「无可用通道」同一族的[] + nil假语义。 - e2e 脚本的「可重跑」承诺兑现:存量资源收敛,不再跳过(#1209,经 #1212 闭合):
verify-token-import.sh自称可重跑,对 site/route/key 确实先查后建,唯独账号只在不存在的时创建;上游凭据短命的平台(sub2api 会话 JWT)因此每过一条寿命就必红一次——verify-token用的是环境里那枚新 token 所以照样 PASS,链在两步之外的balance上死掉,报的还是那句没有原因的 502。现在账号存在时就用本轮已持有且刚验证过的同一枚 token 执行PUT /api/accounts/{id}收敛(绝不二次签发:sub2api 每签发一次就顶掉上一枚),下游 key 在「本脚本拥有该记录」时重申其通配策略、不拥有时响亮 WARN 而不是假装重申过(CI 两条 token-import 链共用同一枚固定 key 值、TOKEN_NAME不同,而列表接口不回显 key 值,照搬smoke.sh的严格分支会让第二条链必红);site/account/key/route 四者都打印created/reused/refreshed,让「只是没动它」无法冒充「验过了」。smoke.sh行为不变,只补注释说明POST /api/accounts/login本身即 upsert(issue 的 grep 只看脚本里的 PUT,漏了服务端),免得后人按对称性「修」出一次多余写入。CItest-e2e新增 aging 门:先老化存量凭据、再用同一枚 token 复跑同一条链并要求绿;老化 PUT 先自检 HTTP 200,以免「什么都没老化」的探针静默通过。 - PostgreSQL 备份导入后重置 id 序列(#1217,经 #1218 闭合):备份导出带的是行当年写入时的显式 id,导入 PostgreSQL 后所有 serial 序列仍停在原位,于是恢复完成后的第一次普通写入就撞
duplicate key value violates unique constraint "sites_pkey"——错误只提约束、不提序列,看不出与导入有任何关系;恢复出来的部署看着健康,却在第一次写入时倒下。migrator 早就为此有自己的 setval 循环,导入路径从来没做。现在两者共用唯一 ownerstore.ResyncPGIDSequences(下次加表也不会漂),并且失败语义分开:migrator 警告续跑(目标可能没跑完全部迁移),导入判致命(schema 刚迁移完,缺表解释不了失败)。SQLite 不需要也什么都没加:AUTOINCREMENT自己在显式插入时维护sqlite_sequence。证明用的是真 PostgreSQL 16 而非 mock:导入三条带 id 1–3 的站点后用普通路径(不带 id)插入,没有 resync 时该插入与恢复行相撞,有 resync 时新行拿到 id 4 且序列last_value ≥ MAX(id);删掉 resync 调用的变异探针复现出运营真会看到的那条 23505 后逐字节还原。 - UI 不再把能用的通道说成坏的,也不再指向一个不存在的按钮(#1219,经 #1220 闭合):两处都是「每一步都报成功、产品却不能用」的同族故障,都由活体验收跑出来而不是读代码读出来。①路由详情里
token_id为空的通道被标成Unbound(中文「未绑定」),运营于是去找一个并不存在的绑定表单:routing.resolveChannelTokenValue对这类通道用账号自身凭据中继(OAuth 账号用 access token,其余用 API key),而collectDesiredChannels的两个来源(token_model_availability与model_availability)故意为同一账号同一模型各造一条通道——活体就是一条路由下tokenId=3与tokenId=null两条通道并存、都启用、中继真的走通。现在按 wire 真相显示Account credential/「账号凭据」,只有账号两种凭据都不存在(真的无法服务)才显示No credential/「无可用凭据」并指向账号侧修法;账号令牌面板的masked_pending徽章同样从只写「待补全」改成说明「拿到的只是上游掩码显示值,执行『同步站点令牌』取回真实值」。②空路由状态曾承诺「点『自动重建』让系统按账号模型生成路由」,而 fresh 实例上点它只留routes rebuild completed routesConsidered=0:RebuildTokenRoutesFromAvailability只重组已存在路由的通道,全仓唯一INSERT INTO token_routes在建路由处理器里。文案改为分别说清两个动作真正做什么(Add route建第一条路由、通道按账号模型可用性自动绑定;Auto-rebuild刷新模型并重组已有路由的通道)。
变更
-
model_probe_results有了保留期:7 天,且路由要读的那一行永不删(#1221,经 #1222 闭合):这张表是全仓写入量最大的一张(探针开启时每个被探测的(channel, model)每轮一行,繁忙实例一天数万行),此前没有任何 DELETE 路径,而且它被排除在备份之外——唯一那份副本就是不断变大的那份。两个读者都只要最新行:service.loadLatestProbeFailures(#625 的重建探针过滤)只读MAX(id) GROUP BY account_id, model_name,handler/admin.queryProbeHistory只取每 channel/account 的最新 N 行且不带时间过滤(整表窗口扫描),所以多留的日子永远不会出现在界面上,却要按每次历史请求和三个索引的每次维护付费。现在登记进既有的RetentionScheduler(与proxy_logs/proxy_video_tasks/admin_background_tasks同一 owner,不造新框架、不加新配置项),7 天窗口、每小时一次,并带一条让整个窗口安全成立的豁免:DELETE FROM model_probe_results WHERE created_at < ? AND id NOT IN (SELECT MAX(id) FROM model_probe_results GROUP BY account_id, model_name)
每个
(account_id, model_name)的最新一行无论多旧都保留,因为那正是路由重建要读的行集;纯按年龄删会在「探针暂停 / 通道停用 / 账号闲置」超过窗口后把过滤器的输入删掉,等于让一个清理任务悄悄改变路由行为。双方言真跑(SQLite 内存 +PG_TEST_DSN下的真 PostgreSQL 16),断言用的是消费者自己那条查询而不是它的转述;活体在真实进程、出厂配置下证明(scheduler started interval_min=60 retention_days=7→cleanup complete deleted=1,豁免行与窗口内行都还在)。
开发者可见
- 四态旅程补齐最后两态,都落成 CI 里能红的门:
- Restart(#1213):
test-e2e此前只启动一次 metapi,所有链都跑在一个从未重启过的进程上,而「设置保存成功、重启后消失」是本项目已经发过的缺陷(由运行中的部署发现,不是被任何测试发现)。现在链跑完后 kill 掉服务、对同一个 data dir 重启,中间不重新登录、不重新绑定、不重建路由、不重发 key,再重新证明核心链:kill 前写入的辨识性设置重启后仍读得回、/v1/models非空、completion 带精确 marker 且确定性 mock 上游的事件计数递增(防缓存或重放冒充中继)、存量短命凭据仍可刷新、同一枚 admin token 仍有效。实现上改为go build后直接跑二进制,使$server_pid就是服务本体(go run包装会让「kill 后重启」这个前提含糊,而含糊的重启照样绿),并且 kill 后必须先证明端口不再应答否则exit 1。 - Restore(#1216):「导入回了 200」与「恢复出来的部署能服务一次请求」是两个断言,此前只证了第一个——
e2e_backup_test.go全是数据往返,全仓没有一处对恢复后的库发起过中继。这个缺口不是假设:删掉四张死表曾让删表之前写出的每一份备份都无法恢复,而两个删表分支都靠修改历史 fixture(改掉「旧版本当年写了什么」的记录)把门弄绿,于是回归随版本发布。现在test-e2e从刚建好链的实例导出备份、在全新 data dir 上起第二个实例、导入,然后不重建任何东西直接断言/v1/models非空 + completion 精确 marker + mock 收到新事件;并含两条前提自检(导出必须带中继所需的 6 张表;导入前该实例必须 0 模型),以免门「出生即假」。导出属 #1034 敏感操作、额外需要X-Admin-Confirm-Token——漏了会先 403、再在导入侧报出误导性的expected a JSON object with a tables field。
- Restart(#1213):
- 架构边界门不再可能空转(#1214):
scanBoundaryViolations遍历仓库报告禁止的 import,一旦遍历覆盖不到任何生产 Go 文件(域目录被移动/改名,或该文件不再匹配布局)就返回空违规列表并判通过——扫不到东西的门不是宽松的门,是不存在的门,而这正是让约三十个 release tag 在必选分片跑 0 个测试的情况下持续全绿的那个形状(#1175)。现在TestPackageBoundaries统计每个域实际扫到的生产文件数,十二个域中任何一个为 0 就t.Fatalf;denylist 规则一字未改。同时修 skip 列表少了两项(agent worktree 目录与 gitignored 的私有开发目录,两者都已写在.gitignore里):两个 checkout 在场时旧门禁会解析 732 个生产 .go(仓库本体 366),等于把后端多读两份副本,WIP 分支的违规还会以无人能映射回契约的 worktree 相对路径报出。三个变异探针全红——清单名写错、走不进某个域目录(真实失效模式)、skip 列表吞掉真域(专门防「加个跳过项」这种把门悄悄关掉的手法)。 - 前端验收门补上旅程尾段,并先证明「回答我的是刚起的那个 build」(#1220):
web/scripts/acceptance-e2e.mjs此前停在签到——它证明的是运营能配置,而这恰好就是「部署完基本用不了」那类报障的形状:每步都绿、链是死的。新增四步:④绑定后的账号在详情抽屉里必须显示非空模型列表且与GET /api/accounts/{id}/models条数一致;⑤Add route建出第一条路由、通道自动挂上,且抽屉必须按 wire 逐个报出每条通道的凭据来源;⑥在 UI 里签发下游 key 并授权 ⑤ 验过的那个模型(空 model policy 是设计上的 deny-all,不授权=发一把看着健康、什么都调不动的 key);⑦用独立 HTTP 客户端(无页面、无会话、无 admin token)调/v1/models与/v1/chat/completions,要求真 2xx + content 非空,结构化 error 体判失败,配了确定性上游日志时还要核对预期 model 真的到达上游;无中继能力的链只报 SKIP 不报 PASS。另加/api/about身份 preflight(EXPECT_SERVER_COMMIT不符即拒跑):这条不是装饰——本轮有三次「绿跑」验的其实是旧内嵌 SPA,因为上一轮 build 起的孤儿实例一直占着端口、每轮新起的服务绑定失败即退出,而旅程照旧跟旧进程说话。cleanupState一并清 routes 与下游 key(残留路由会以「零通道行」回来、残留 key 值会把 ⑥ 变成 409,两笔都会算到本轮头上),脚本头部改为就地陈述契约与两条运营危害(它会删掉BASE_URL上的一切;登录旅程会顶掉该上游用户的 dashboard 凭据),两处指向 #1194 已删文档的悬空注释清掉。活体 8 passed / 0 failed / 0 skipped,7 个变异探针(3 单测级 + 4 活体级)全红且报对原因。
已知遗留(本版未做)
- #1132 请求头模板:属新产品面,冻结期排队;不因未做而计入欠债。
admin_audit_logs(参考测试床上最大的表,382 行)与checkin_logs(68 行)仍无保留期:删审计历史是需要运营明确点头的产品决定,不顺手塞在一次遥测清理旁边。已记录、本版未动。handler/admin仍有 4 个千行单文件(stats.go1315 ·downstream_keys.go1040 ·token_routes.go1034 ·sites.go1028):重构阶梯(删除 → 折叠重复 → 同包拆文件 → 明确单一 owner → 移动 package → 新增抽象)上「同包拆文件」这一档在accounts_*(7 文件)/settings_*(7)/stats_*(6) 上已经落地(#1192 起)。再往下要么把单一资源的 CRUD 按动词再切、要么搬包,而没有任何已观察到的用户故障可归因于此,按本波 Law 1 默认不做。- 第二轮测试消融未做:上一轮派出的只读审计没有返回结论,重做审计的成本高于本波剩余预算。KPI 是「故障场景守恒 + 认知成本下降」而不是 LOC,因此宁可不做,也不做没有变异探针支撑的折叠。
- 12 个顶层 Go 包整体搬进
internal/判 NO-GO 并已丢弃:取证显示 805/805 个 .go 文件在只归一化搬迁根后与 master 逐字节相同、844=844 文件守恒、8224 个 func 签名只差 1 个(即为了让架构门在搬迁后存活而被迫改写的scanBoundaryViolations),而pkg.go.dev上该 module 404 未索引、只出一个二进制 ⇒internal/的语言层收益对本仓为 0,代价却是数百文件路径 churn 与 4 道发布门被语义改动。全量 diff 已存档可 100% 复原;这次消融唯一的收获转成了上面那条边界门空转防护(#1214)。
v0.18.0
[v0.18.0] — 2026-09-03
修复
- New API 中继链从「看起来绑上」变成持久且可证明(#1179,经 #1187 闭合):New API v1 登录返回的是短命 dashboard JWT,Metapi 却把它当长期凭据持久化,后续模型刷新因此塌成空列表;现代 New API 的令牌列表把 relay key 显示成掩码值,Metapi 又把掩码显示值当可用路由凭据落成
masked_pending;模型列表此前要等后台调度先跑过一遍才出现,登录时并不可得。现在登录时把 fresh v1 JWT 提升为 New API 的长期 dashboard PAT(发不出持久凭据就拒绝绑定),并用 live JWT 撤销临时上游登录会话——即使代理剥掉 refresh cookie 也照撤;steady state 用不过期的 PAT,不再重复登录,只有显式 login/recovery 才轮换 New API 那唯一一个 dashboard PAT。掩码 key 用一次所有权校验过的/api/token/batch/keys批量换回真实 key,掩码显示值绝不作为路由凭据返回,水合失败沿单 token 与 verify-token 路径向上抛而不是装作没有 token。端到端门禁同时收紧:EXPECT_RELAY=1成为默认,账号模型为空、/v1/models为空、结构化错误、非 2xx 完成、缺失或空内容、错误完成标记全部判失败;显式无通道链用EXPECT_RELAY=0报 SKIP 而不是 PASS;CI 把 New API 接到确定性的本地 OpenAI mock 上核对真实完成标记与上游收到的具体模型,且不再绕过被测 adapter 注入完整 relay key;复用下游 key 时重新断言其通配策略(空模型列表就是刻意 deny-all)。升级后需要重新登录一次;令牌不需要手工绑定。 - 没有可用通道的 503 现在说清为什么(#1179,经 #1186 闭合):通道选择把「无可选」报成
(nil, nil),失败路径因此记err=<nil>并回503 {"error":{"message":"No available channels"}}——「没有启用的路由匹配这个模型」「路由匹配但每个通道令牌未绑定/停用」「所有通道都在冷却,或下游 key 策略排除了站点」三种完全不同的运维问题输出一字不差。现在proxy.ExplainNoChannel(...)渲染一行紧凑结论:解释器的 verdict,加上路由匹配但无人合格时的主候选拒绝原因(没有可用通道(…):令牌不可用);它经可选SelectionExplainer接口触达路由器(与AvailableModelsSource同款 idiom),不能解释的单测桩降级回旧文案,解释出错只给出「无原因」而不是二次失败。原因同时进 503 body(稳定前缀No available channels保留)、WARN 日志reason=与运营面向的 all-failed 事件。 - 重建路由不再随发出它的 HTTP 请求一起死,清缓存也不再删掉重建要用的输入(#1174,经 #1185 闭合):
POST /api/routes/rebuild曾把整趟 pass 跑在r.Context()上,refreshModels: true(UI 就是发这个)时每个活跃账号一次上游往返、单次上限 30s,真实车队要几分钟;web client 只给 30s 超时,于是客户端先挂断,日志里就是context canceled,重建永久失败。现在 handler 用context.WithoutCancel(r.Context())分离请求上下文并配自己的 30 分钟预算,浏览器或反向代理放弃不再取消它要求的路由状态;SyncAllAccountModels的收尾重建跑在局部有界上下文上,被打断的 pass 仍用已拿到的可用性重组路由(这一步不碰上游);收尾重建总是执行(无变化时短路不写),不再被success > 0卡住。web client 的重建超时从共享 30s 改为 5 分钟,且不再绑定服务器忽略的wait标志。第二个缺陷:POST /api/settings/maintenance/clear-cache(UI:清除缓存并重建路由)曾删掉token_routes、model_availability和route_channels,再排一个从这两张表重组route_channels的重建——要重建的东西已经被它删光:每条路由定义消失,每个已发现模型都要重新上游拉取(每账号 30s,凭据过期就直接失败),手工通道挂载也被抹掉。现在该端点只失效进程内缓存并排同一个真实后台重建,不删任何行;清业务行是factory-reset的职责,那个端点仍然会做。 - 账号编辑保存的凭据模式不再蒸发,换不过去的模式被 400 拒绝(#1176,经 #1184 闭合):编辑对话框每次保存都发
credentialMode,但AccountUpdatePayload没有这个字段,handler 解码时把它丢掉——建账号时选 API key、编辑改成 session、拿到成功 toast,重开对话框又变回 API key。现在credentialMode进入更新 payload 并合并进extraConfig(创建路径写的同一个存储,也是ResolveStoredCredentialMode第一个读的地方);请求的模式在碰任何凭据字段之前解析,因为shouldMirrorAPIKeyToken看的是已存模式——此前显式切到 session 也会把 session 凭据抄进api_token,而 session 凭据不是 API key,代理的凭据回落会把它当 API key 发出去。存储凭据撑不起的模式现在 400 而不是落库:没有accessToken的session、没有apiToken的apikey,此前都会产出「行上声称可认证、实际永远不能认证」的账号。 - AnyRouter 校验失败终于告诉运营它要什么凭据(#1133,经 #1195 闭合):运营配好 AnyRouter 站点、粘贴一段对上证明可用的 cookie,得到的只有
token verification failed,平台到底要什么没有任何提示。adapter 并不缺——AnyRouterAdapter内嵌NewApiAdapter,cookie/session 校验本来就会尝试,但它刻意关掉 token 管理(GetAPIToken返回nil, nil),因为 AnyRouter 的 API-key 流程不是 New API 的/api/token/契约;校验因此返回nil/空/unknown时,bind 路径与 verify 路径都把信号扔掉、对所有平台回同一句泛泛的话。现在两处共用一个credentialVerificationFailureMessage(platform):AnyRouter 明确说 access-token/API-key 绑定不支持、必须用 session 模式、字段接受session=<value>或完整Cookie:头、Platform User ID 仅在上游强制New-Api-User/User-id时才需要、绑定前必须用tokenType=session通过校验;其它平台保留原指引并补同一形状提示(session 平台 →session=<value>或完整 cookie 头,API-key 平台 → API Key 模式)。 - 「同步站点令牌」现在幂等,掩码 key 不再每点一次多一行(#1193,经 #1196 闭合):
SyncTokensFromUpstream此前只对 ready 行去重;上游在令牌列表里掩码 relay key(New API 系返回sk-abc***xyz)时,行解析成masked_pending,永远匹配不上,于是每次同步都走 INSERT、再插一份同一条 token 的副本——正是运营每点一次多一行的来源。现在按精确 key 值匹配已有行、优先 ready 行(已水合的 key 继续对账进同一可用行)、回落 masked-pending 行;UPDATE 保留匹配行解析出的value_status而不是强制写成ready——掩码行诚实地保持掩码,不声称持有没有的 key。已知残余(文档化、不在本版修):#1187 之前绑定的账号可能留一条masked_pending旧行,之后同步取回真实 key 时会再插一条可用行、旧的掩码行留在原地——一次性、可见标记、不可路由的重复项,运营可删;对账它需要平台信息,而该 service 函数没有,为一条一次性外观行不值得加这堆机器。 - 删除一张表不再让删除之前写的每个备份都恢复不了(#1201):删表曾让
validateBackupImportTableKeys拒绝它不认识的任何 payload key,于是升级过本版之后,一个本来有效的旧备份恢复直接变成400 {"error":"import failed: unknown table proxy_debug_traces"}(被现有的向后兼容测试抓到)。现在按来源区分而不是维护一张「退役表」清单:策略排除表(admin_sessions、admin_audit_logs…)无论来源都响亮 400;手写/粘贴 JSON 里的未知 key 仍 400——它最可能是拼写错误,其行会静默永远不落地;真正导出文件里的未知 key 说明是本 build 已删的表,跳过并在 import 与 preview 两个响应里报告为ignoredTables(WebDAV 恢复路径同样)。跳过是安全的:import/preview 循环遍历注册表再拿 payload key 查注册表,不认识的 key 从不被读。legacyBackupTables因此保留它的 28 个历史条目(含这四张已删表):它记录的是旧 build 写过什么、而不是本 schema——这正是它能当升级门的原因。
移除
-
未发布的 Electron 桌面外壳连同公开的
/api/desktop/health一起删除(#1197):electron/(main.js 600 行、preload.js、README、package.json、托盘图标)、scripts/build-electron.sh/.ps1、Makefile 的electron-buildtarget、web/scripts/desktop/generate-icons.mjs、两张 desktop 图标、npm 的desktop:icons,以及GET /api/desktop/health、它在isPublicAPIRoute白名单里的条目和对两个图标的根文件服务全部移除。删除证据:发布流水线从未产出过桌面产物——release job 只从./cmd/server与./cmd/migrate构建metapi-<os>-<arch>和metapi-migrate-<os>-<arch>,workflow 里唯一的 "desktop" 字符串是 visual-regression 的 viewport 注释;外壳只能由维护者手工make electron-build跑到,且web/src里没有对应ipcRenderer/contextBridge/window.electron消费者,它只是把一个 SPA 装进窗口。删除顺带拿掉一个公开、未认证、为托盘而设的端点;scripts/regen-ts-fixture.sh的就绪探针从/api/desktop/health改探/ready。 -
update-center 的五个操作臂与调度器桩删除,状态卡保留(#1199):
POST /api/update-center/check、PUT /config、POST /deploy、POST /rollback、GET /tasks/{id}/stream加scheduler/update_center.go与METAPI_ENABLE_UPDATE_CENTER全部移除;这个调度器本来就是 log-only no-op(环境变量自己的文档也这么说)。GET /api/update-center/status与它渲染的 UI 版本卡保留——UpdateCenterSection仍用api.getUpdateCenterStatus()。 -
model-tester 的流式/任务臂删除,同步探针保留(#1199):
POST /api/test/proxy(连同/stream、/jobs、GET/DELETE /jobs/{jobId})与POST /api/test/chat/stream、POST /api/test/chat/jobs、GET/DELETE /api/test/chat/jobs/{jobId}删除;这些 stream/job 臂从未实现成 SSE,model-tester/api.ts在 master 的注释里自己承认。POST /api/test/chat(同步探针实际调用的那个)保留。证据:每个被删路由在web/src/lib/api/都有客户端包装,master 上这些包装在lib/api/之外零引用——没有组件、hook 或测试调过它们。 -
proxy debug-trace 子系统整个删除:两张表、五个索引、两个读路由、九个
PROXY_DEBUG_*开关、设置 UI 与 web client(#1201):九个开关会被解析、持久化、热生效并渲染进设置 UI,两个路由也会把表读回来——但从没有任何东西写过一行:master 全树里仅有的INSERT INTO proxy_debug_*在e2e/e2e_backup_test.go与handler/admin/stats_detail_test.go测试种子里。读路由因此只能返回空列表,动任何开关都不改变行为;一个永久为空、却带九个用户可见控件的表面比没有更糟。 -
proxy_files、PROXY_FILE_RETENTION_*与scheduler/file_retention.go删除(#1201):master 上每个生产引用都是管线而非使用——auth/context.go里的注释、修剪它的 retention 调度器、备份包含、DDL 与注册表;没有 writer,没有 reader。这张表被创建、备份、修剪,却从未被写入。 -
admin_snapshots与admin-snapshot调度器删除(#1201):warmTarget是显式 stub(自己的注释写着「Stub: regenerate snapshot…」);调度器里唯一碰这张表的语句是DELETE FROM admin_snapshots WHERE expires_at < ?——周期性清理没有任何代码插入过的行。而每趟 pass 仍然扇出 4 个 goroutine 加一个wg.Wait只为了打 debug 日志。该调度器有用的那一半(usage aggregator 的RunProjectionPass)没有丢:usage-aggregation 调度器仍在自己的 interval 上跑它。 -
RESPONSES_COMPACT_FALLBACK_TO_RESPONSES_ENABLED与ShouldFallbackCompactResponsesToResponses删除(#1201):git grep ShouldFallbackCompactResponsesToResponses master -- '*.go' | grep -v _test.go只剩定义本身;它的两个测试(一个 table test 与一份固定期望值矩阵)随之一并删除——只练不可达函数的测试不把守任何东西。 -
RouteRefreshWorkflow家族删除(#1187):生产代码没有它的实现者,连其配置字段与 test-only mock 一起移除;路由器去掉这个没人实现的 refresh 接口后照常构建与选择。 -
六个无调用者的 helper 与四个不可能失败的测试删除(#1198、#1190):
stripModelPrefix、normalizeURLForDetection、normalizeURLProtocol(platform 内git grep只有定义 + 各自测试)、setReadinessDrainingForTest(一行转发markReadinessDraining的 pass-through,测试改直接调后者)、cronRunner.removeJob、toISOTime全部移除。同时删掉四个 vacuous 测试:TestOneApiAdapter_BalanceQuotaMinusUsed(零断言)、TestOneApiAdapter_BalanceParseLogic(只断言本地字面量算术、不调产品代码)、TestOpenAiAdapter_GetModels(接受任何结果)、TestListAdapters_Copy(唯一的if是空 body,注释承认「只保证不 panic」)——只练不可达函数或不可能失败的测试只会制造维护重量、报告假信心。scheduler 里的TestClampInt也随之删除(重测config.ClampInt,config 自己已把守),TestFormatTimeToSQL/TestCountResults折进表格。 -
14 份维护者过程史从公开仓移出(#1191):
docs/internal/STATE.md(点状产品状态)、docs/internal/log.md(按日收口日志)、docs/internal/progress/MASTER.md(会话任务台账)、docs/internal/benchmark.md(一次性测量)与docs/internal/analysis/下 9 份(竞品研究、凭据 IA、db pool 预算、P0-585 验证、包边界、Redis 共享状态、TS→Go gap、UI/UX 审计、wave12 需求真相)删除;它们的角色改由 GitHub issues/releases(现状与开放项)、CHANGELOG.md(版本叙事)、docs/architecture.md§"Ownership and boundary map"(包归属 + 例外边,已由docs/package_boundary_test.go机器强制)与scripts/verify-cascade-prod.sh+e2e/e2e_p0585_production_test.go(运营门级 cascade 证据流程)承接。 -
再清掉两份已无读者的维护者笔记(#1194):
docs/internal/analysis/e2e-acceptance-platform.md(运营门级 live-credential 验收制度 + wave 历史)与docs/internal/design/events-structured.md(冻结工作项的「实施中」设计笔记)删除;引用它们的两处注释改为就地陈述契约,web/knip.config.ts里引用已删web/scripts/oneoff/**的 ignore 条目一并移除。 -
docs/api.md从 456 行删到 28 行(#1187):原来是重复 deep-link 桩的堆积,现在只剩领域索引;各领域页仍是详细出处。 -
无破坏性迁移(#1201):已升级的安装保留这四张孤儿表;没有代码读它们,也没有代码 drop 它们。
cmd/migrate只拷贝注册表里的表,所以方言迁移不会搬走它们(永远为空的)行。已写进docs/deployment.md。 -
routing里整条死的 pricing override 链与一个fmt.Errorf别名删除(#1204):NormalizePricingRatio、EstimateProxyCostFromModel、BuildPricingOverrideModel全仓零调用者——非测试引用只剩它们自己的定义与文档注释。只经这三个函数可达的私有符号一并删除:asFiniteNumber(唯一调用者是NormalizePricingRatio)、jsonNumber接口(唯一用例是asFiniteNumber里一个 type-switch 分支)、ProxyBillingPricingOverride(唯一用例是BuildPricingOverrideModel的参数)。jsonNumber的注释自称是为了「避免在asFiniteNumber的每个调用点导入 encoding/json」——导入只落在一个文件里、不落在调用点,所以它什么也没买到;何况全仓只有一处UseNumber()(handler/admin/site_announcements.go),json.Number本来就永远到不了那个 type switch。三个只练这些函数的测试随之删除,其中TestEstimateProxyCostFromModel_MatchesCalculateModelUsageCost断言的是「别名返回被别名者返回的东西」,它自己的注释写着 "thin alias over CalculateModelUsageCost"。scheduler的formatErr是fmt.Errorf的一行转发(func formatErr(f string, args ...any) error { return fmt.Errorf(f, args...) }),六个调用点改为直接调用fmt.Errorf,它自己的测试一并删除;helpers.go保留stringsTrimLower(真实逻辑:trim + 小写 + 空值默认"active",两个生产调用者)。存活的 pricing API(`Fallbac...