Repository navigation
multi2api v1.9.14
v1.9.14
本版两条主线:API Key 粒度的模型对外名映射(新能力),以及把渠道凭据收进 auths/ 让整个目录可搬走。另修了三处交互 bug。
一、API Key 模型对外名映射(新能力)
解决什么问题
API Key 的模型白名单里填的是内部模型名 —— 例如 csg/deepseek-v4.1-flash-1017。直接暴露给调用方既难记,也把自己的渠道结构漏了出去。
现在可以给每把 key 配一张映射表:
内部模型名 对外显示名
csg/deepseek-v4.1-flash-1017 → deepseek-v4-pro
调用方在 /v1/models 里看到 deepseek-v4-pro,发请求也用这个名字;网关收到后换回内部名去上游。
两层映射是正交的
| 层 | 方向 | 解决什么 |
|---|---|---|
渠道 Channel.ModelMap |
公开名 → 上游名 | 怎么打到上游 |
key Key.ModelMap |
对外名 → 内部名 | 对外怎么称呼 |
换一把 key 可以换一套对外名,渠道配置不用动。发给 A 客户一套名字、B 客户另一套,后端是同一批模型。
三个关键设计
① 顺序不能反:先映射、再判白名单
客户端发 deepseek-v4-pro → 映射成 csg/... → 判白名单 ✓
若先判白名单,对外名不在白名单里会被 403 —— 而它恰恰是我们发布出去让人用的名字。
② 映射是增量能力,不是白名单
表里没有的名字原样保留。若把未命中的判成非法,配一张映射表就等于把所有老调用方式打死(表现为“以前能用的模型突然 404”)。
③ body 不用单独改
渠道分支由 forwardViaChannel(model, body) 自己重写 body 的 model 字段,legacy 分支同理。只改 peek.Model 一处,所有消费方自动一致 —— 分两处改必然有一天只改了一处。
界面:行式编辑器
模型对外名(可选)· 左边内部模型名 → 右边对外显示名 [+ 新增一行]
┌──────────────────────────────────────────────────────────────────┐
│ [csg/deepseek-v4.1-flash-1017 ▾] → [deepseek-v4-pro ] [×] │
└──────────────────────────────────────────────────────────────────┘
- 初始 0 行,点「+ 新增一行」才出现
- 左右两侧都能选也能手填(
input+datalist) - 左侧候选 = 已勾选的模型(不列全量目录:选一个没勾的等于配一条永不生效的映射)
二、渠道凭据收进 auths/,整个目录可搬走
之前是分裂的
渠道 API key → data/channels.json
账号凭证 → auths/<provider>/<uid>.json
只把 auths/ 拷到另一台机器,会得到「有账号但没渠道」—— 发请求时不知道该往哪个 base_url 发。
现在
auths/
├── loomy/ workbuddy/ ... 上游账号凭证(不变)
└── channels.json 渠道定义 + API Key + abilities
复制 auths/ 一个文件夹 = 完整迁移。 实测:只带 auths/ + config.json 启动新实例,/v1/models 返回 200 且模型齐全。
⚠ 为什么不只搬 key:base_url / models / model_mapping / priority 与 keys 是同一条路由决策的两半 —— 光有 key 没有 base_url,那个 key 是废的。而 abilities 是从 Channels 派生的索引,必须与它原子地在同一次写盘里。
迁移策略:只复制、不删除
目标已存在 ⇒ 不动(幂等,绝不覆盖你在新位置的修改)
源不存在 ⇒ 不报错(首次部署的正常状态)
父目录不存在 ⇒ 自动建
先写 .tmp 再 rename(中途崩溃不留半截文件)
不删源文件(旧版 exe 回退时仍能读自己的数据)
⚠ 边界:auths 只放上游凭据
控制台账号(accounts.json)、会话(sessions.json)、调用方 API key(apikeys.json)留在 data/ —— 它们不是"拿去换上游额度"的东西(一个是登录我们自己面板的用户,一个是发给下游的钥匙)。
三、三处交互修复
① API Key 启用/禁用只能单向生效
await busyRun(b, async () => {
const on = b.textContent === '启用'; // ← 此时文案已经是 '…'
...
}, { label: '切换', busyText: '…' });busyRun 一进入就把按钮文案改成 busyText,然后才调 fn。所以判断恒为 false ⇒ 永远发送 enabled: false ⇒ 只能禁用、永远启用不了。
改成从列表缓存取状态(const on = !cur.enabled),与「编辑」分支同一数据源。
② 点完 toggle 状态不刷新
toggle 分支漏了 writeInvalidate([EP_APIKEYS])。loadAPIKeys() 走 sharedGet(30s TTL),不丢缓存就拿旧快照重画 —— 表现为“提示说已启用,但那一行状态没变”。save / reset / delete 三处都有这一行,就它没有。
③ 模型映射「设了但重开是空的」/「csg 还是原名」
两个都是「存了但读不出来 / 改了但这个入口没生效」,数据一直在盘上:
3a Store.List() 构造 View 时漏了 ModelMap 字段 ⇒ 前端拿到 undefined ⇒ 回填 0 行。
(同一个坑这个文件踩过:OwnerID 那里就写着「漏掉这一行会让每个成员都看到空表」。)
3b /v1/models 的组装顺序:
data := h.filterModelsForRequest(r, h.modelList()) // ← 这里改名 ✓
data = h.appendChannelModels(r, data) // ← 追加渠道声明的内部名 ✗csg 是渠道不是 provider,不在上游目录里,是后加进来的、绕过了改名。新增 applyPublicNames() 在 append 之后再改一次名。
实测对比:
修复前: 001/DeepSeek-V4.1-Flash + csg/deepseek-v4.1-flash-1017 ← 一个改了一个没改
修复后: 001/DeepSeek-V4.1-Flash + 002/deepseek-v4.1-flash-1017 ✓ 两个都改了
四、删掉「对外模型」页
渠道页原来有两个 tab(渠道 / 对外模型)。「对外模型」回答的是「我配的渠道现在让外面看见什么」—— 那个问题现在由 API Key 的白名单 + 对外名映射回答,两处重复且后者才是对外真相。前端 tab 与后端 GET /admin/channels/public-models 一并删除。
⚠ 不是“把 tab 藏起来”:只删 DOM 却留着 document.getElementById('tab-ch').onclick = ... 会因元素不存在抛 TypeError,整个 script 停摆 ⇒ 渠道列表一条都不加载。所以切换逻辑整段删掉了。
五、安全:挡住凭据包进版本库
仓库根曾出现 auths.zip(98 个条目,含 loomy / codearts / workbuddy 的 token 与 session),只是碰巧没被 git add。
.gitignore 新增 auths.zip / auths-*.zip / auths.tmp / *.zip。
⚠ 为什么不能只靠已有的 auths/ 那条规则:它只挡目录。打包成 auths.zip 放在仓库根时不是“目录下的文件”,规则不匹配 —— 正是这次的形状。已确认仓库里没有已跟踪的 .zip/.tar/.gz,所以 *.zip 不会误伤。
六、验证
go build ./... exit=0
go test ./... ok=39 FAIL=0
go vet ./... exit=0
门禁① 语法 6/6
门禁② hover 读数 4/4
门禁③ toggle 状态 5/5
门禁④ 映射编辑器 10 项
CI verify + 5 平台构建 + 发布资产 全绿
新增守卫
apikey/model_map_test.go 7 条 变参三态 / 规整 / 副本语义
apikey/model_map_wiring_test.go 3 条 ★ List 必须带出 ModelMap;落盘后重开仍在
server/public_name_test.go 4 条 精确匹配 / 裸名兜底 / SplitModel 诊断
tests/frontend/verify_toggle_state_source.js 5 项 ★ 不用 DOM 文本判断业务状态
tests/frontend/verify_model_map_editor.js 10 项 ★ 转置方向 map[对外名] = 内部名
⚠ 本轮三次「假绿」,都记下来
① 检查脚本自己修好了被检查的东西
if (-not $joined.EndsWith("`n")) { $joined += "`n" } # ← 我的检查脚本里这句CI 报 channels.go 未通过 gofmt,真实原因是文件末尾缺换行(\ No newline at end of file)。而我的自检脚本正好补了那个换行 ⇒ 永远绿。
② 路径分隔符导致整轮跳过
用 git diff --name-only 返回的正斜杠路径去 Test-Path,Windows 上对不上就 continue ⇒ 全部跳过 ⇒ 报“全部通过”。
③ 门禁误报 / 误绿
- 门禁① 第一版扫到注释里引用的坏代码(作对照用的
// const on = b.textContent === '启用';)⇒ 改成先剥注释再匹配。 gofmt -l对「缺末尾换行」只报“未格式化”,gofmt -d不显示那是换行问题 —— 只有git diff会写出\ No newline at end of file。
七、已知未做
csg上游的total_tokens是累积值(实测 590 条里 573 次递增,真实总量 0.49M 而按行累加得 228.6M,虚高 463 倍)。这不是我们算错,是上游字段口径问题;要修需要一个累积值检测(三种方案各有代价),等明确方向。-race跑不了:本机无 gcc(CGO_ENABLED=0)。并发结论来自go vet(copylocks / loopclosure 无告警)与代码阅读,没有竞态检测器的证据。- hover 读数的真浏览器验收仍未跑通。CDP 脚本在
tests/frontend/cdp_verify_tooltip.js(改成函数级验证,不依赖登录),尚未实测。
升级方式
五个平台产物任选,SHA256SUMS.txt 校验:
wb2api-server-darwin-amd64.tar.gz
wb2api-server-darwin-arm64.tar.gz
wb2api-server-linux-amd64.tar.gz
wb2api-server-linux-arm64.tar.gz
wb2api-server-windows-amd64.zip
⚠ 前端内嵌在二进制里,升级后请 Ctrl+F5 硬刷新(渠道页是 iframe,必要时对它单独重载)。
⚠ 首次启动会自动把 data/channels.json 复制到 auths/channels.json(源文件保留,可回滚)。