Skip to content

Releases: DeliciousBuding/metapi-go

v0.23.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 10:30
ff48477

[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)。

开发者可见

  • 验收:恢复严格的完整管理生命周期矩阵与清理检查,纯逻辑前端测试不再加载浏览器模拟环境(#1421、#1417)。

文档

  • API:补充日志计时和模型来源口径、Octopus 预览确认与导入边界;数据库新增列在启动时自动迁移,升级前按部署说明备份(#1415、#1418、#1422)。

v0.22.1

Choose a tag to compare

@github-actions github-actions released this 08 Oct 05:32
707ed31

[v0.22.1] — 2026-10-08

修复站点分布图例溢出,优化 CI 并行。

修复

  • 站点分布:站点较多时图例移出图表视口,改为卡片内可滚动的多列列表,不再被卡片裁剪(#1411)。

开发者可见

  • CI:web-dist 独立构建并与前端检查并行,SQLite/PG/a11y 不再串行等待 frontend;发布助手支持在 HEAD 等于 origin/master 的任意干净 checkout/worktree 打 tag(#1410)。

v0.22.0

Choose a tag to compare

@github-actions github-actions released this 07 Oct 12:48
1b7851c

[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

Choose a tag to compare

@github-actions github-actions released this 13 Sep 20:51
a0bef61

[v0.21.3] — 2026-09-14

代理日志展示下游客户端身份。

变更

  • 代理日志:新增「客户端」列,以胶囊展示检测到的下游客户端(Codex CLI / Claude Code / Gemini CLI / SDK family),详情抽屉会话区同步升级;未识别的客户端显示纯文本(#1374)。

v0.21.2

Choose a tag to compare

@github-actions github-actions released this 13 Sep 18:48
6132183

[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)。

开发者可见

  • 尺寸任意值收敛到 Tailwind 动态间距刻度(49 处);设置区文本空态统一为 EmptyText 配方(#1368 → #1370)。

v0.21.1

Choose a tag to compare

@github-actions github-actions released this 13 Sep 11:49
1dd137d

[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)。

开发者可见

  • gofmt 全仓统一并纳入 CI 门禁(#1350);CI 冒烟链覆盖 chat / messages / responses 三协议对账(#1360 → #1362);截图证据管道采集前注入确定性演示数据(#1364)。

v0.21.0

Choose a tag to compare

@github-actions github-actions released this 13 Sep 00:54
eaacca7

[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)。

开发者可见

  • Web 源码树按 feature 目录重组,跨 feature 引用一律经各 feature 的公开入口;设计文档与公开说明已逐句对树核校(#1330–#1348)。

已知遗留

  • admin_audit_logs 与 checkin_logs 仍无保留期;本版不自动清理已有审计或签到历史。

v0.20.0

Choose a tag to compare

@github-actions github-actions released this 09 Sep 07:26
eda14c4

[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

Choose a tag to compare

@github-actions github-actions released this 04 Sep 00:59
acdd4f5

[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,漏了服务端),免得后人按对称性「修」出一次多余写入。CI test-e2e 新增 aging 门:先老化存量凭据、再用同一枚 token 复跑同一条链并要求绿;老化 PUT 先自检 HTTP 200,以免「什么都没老化」的探针静默通过。
  • PostgreSQL 备份导入后重置 id 序列(#1217,经 #1218 闭合):备份导出带的是行当年写入时的显式 id,导入 PostgreSQL 后所有 serial 序列仍停在原位,于是恢复完成后的第一次普通写入就撞 duplicate key value violates unique constraint "sites_pkey"——错误只提约束、不提序列,看不出与导入有任何关系;恢复出来的部署看着健康,却在第一次写入时倒下。migrator 早就为此有自己的 setval 循环,导入路径从来没做。现在两者共用唯一 owner store.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。
  • 架构边界门不再可能空转(#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.go 1315 · downstream_keys.go 1040 · token_routes.go 1034 · sites.go 1028):重构阶梯(删除 → 折叠重复 → 同包拆文件 → 明确单一 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

Choose a tag to compare

@github-actions github-actions released this 03 Sep 16:02
cfd850c

[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-build target、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...

Read more