mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
按发布纪律回流。**注意这是内容对齐而非 merge**,理由与依据:
1. 纪律禁止 merge 回主干,因为 merge 会把发布分支的**版本号变更**带进 main。
本项目版本号**只由 `git describe --tags` 推导**(Makefile VERSION),
仓库里没有任何文件存放版本号 ⇒ 回流不存在版本污染风险。
2. 逐项核对确认 **main 是 release 的内容严格子集**:
- `main` 独有的**文件**:0 个;
- `internal/lua/plugins.go` / `internal/core/kernel_actions.go` 等内核文件
main 独有行数:0;
- `deploy.sh` 那 14 行只是 emoji 清理(release 刻意去掉了 emoji);
- `channels.lua` 那 333 行是**旧版内容**(旧 exports 表、旧模型名单)。
- main 独有的 9 个提交按 patch-id **全部已在 release 中等效存在**
(`git cherry` 逐条列出均为 `-`,即已合入)。
3. 故回流 = 把 release 的内容带到 main,等价于"只捡拾修复、不带版本号",
且不会丢任何 main 独有的修复。
本次带回 v2.0.12 的全部内容(37 个提交):
- 源串行 + 按模型节流(照 model-gate)
- 已失效模型存储 + 远端目录闸门
- 余额分档选号
- 模型能力元数据(models.dev,isFree 权威判定)
- **凭据续期修复**(此前恒报 ok:true 却一个请求都不发)
- 通用件 TTL 缓存 + 串行锁;account_test/account_adopt/account_delete 修正
- 内核级:出站授权持久化重启后不生效、`__llmsproxy_plugin_dir` ground truth
校验:`git diff release/v2.0.x` = 0 行;`go build -tags luajit ./...` 通过。
123 lines
4.9 KiB
Go
123 lines
4.9 KiB
Go
package core
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// 余额分档选号(照参考 buddy-balance-rank.ts / loomy-balance-rank.ts)。
|
||
//
|
||
// 解决什么:一个号常同时持有多**种**资源包(如「14 天到期」+「8 年后扣费截止」),
|
||
// 而**服务端扣哪个包不由我们决定** —— 插件能决定的是**用哪个号**:
|
||
// 优先用「还有 15 天内到期积分」的号(那部分不用就作废),只剩永久积分的号排最后;
|
||
// **锁定永久积分**时只剩永久积分的号等同于不可用。
|
||
//
|
||
// 判据用假余额上游,逐条钉住分档规则(含三个易错点)。
|
||
func TestBalanceTierSelection(t *testing.T) {
|
||
// 假 buddy 余额端点:返回三个包,覆盖三种情形
|
||
// pkg A: 有效 + 10 天内到期 + 剩余 100 → 临时桶
|
||
// pkg B: 有效 + 到期时间未知 + 剩余 50 → 永久桶(保守)
|
||
// pkg C: **已失效** + 剩余 9999 → 必须跳过(否则虚增可用额度)
|
||
future := nowMs() + 300*24*3600*1000 // 300 天后
|
||
soon := nowMs() + 10*24*3600*1000 // 10 天后
|
||
body := map[string]interface{}{
|
||
"data": map[string]interface{}{
|
||
"Response": map[string]interface{}{
|
||
"Data": map[string]interface{}{
|
||
"Accounts": []map[string]interface{}{
|
||
{"Status": 1, "CycleCapacityRemain": 100, "DeductionEndTime": soon},
|
||
{"Status": 1, "CycleCapacityRemain": 50, "DeductionEndTime": future},
|
||
{"Status": 2, "CycleCapacityRemain": 9999, "DeductionEndTime": future},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
raw, _ := json.Marshal(body)
|
||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_, _ = w.Write(raw)
|
||
}))
|
||
defer ts.Close()
|
||
|
||
_, ps := poolCore(t)
|
||
// 把一个 buddy 账号加进池,并把余额端点指到假上游
|
||
if _, err := ps.Call("channels", "set_endpoints", map[string]interface{}{
|
||
"platform": "buddy",
|
||
"balance": map[string]interface{}{"url": ts.URL},
|
||
}); err != nil {
|
||
t.Fatalf("set_endpoints: %v", err)
|
||
}
|
||
m, err := ps.Call("channels", "account_add", map[string]interface{}{
|
||
"platform": "buddy", "account_id": "tieracct",
|
||
"credential": `{"access_token":"TOK"}`,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("account_add: %v", err)
|
||
}
|
||
w, _ := m.(map[string]interface{})
|
||
j, _ := w["json"].(map[string]interface{})
|
||
if j == nil || j["error"] != nil {
|
||
t.Fatalf("account_add 失败: %#v", m)
|
||
}
|
||
|
||
// ① 分档:临时=100(只有 10 天内那个),永久=50(未知/远期),失效包被跳过
|
||
out, err := ps.Call("channels", "credits_tier_probe",
|
||
map[string]interface{}{"platform": "buddy", "account_id": "tieracct"})
|
||
if err != nil {
|
||
t.Fatalf("分档探针: %v", err)
|
||
}
|
||
tw, _ := out.(map[string]interface{})
|
||
tj, _ := tw["json"].(map[string]interface{})
|
||
if tj == nil {
|
||
t.Fatalf("无 json: %#v", out)
|
||
}
|
||
if tj["error"] != nil {
|
||
t.Fatalf("分档报错: %v", tj["error"])
|
||
}
|
||
if got, _ := tj["expiring"].(float64); got != 100 {
|
||
t.Errorf("临时余额应为 100(仅 10 天内到期的包),实得 %v", tj["expiring"])
|
||
}
|
||
if got, _ := tj["permanent"].(float64); got != 50 {
|
||
t.Errorf("永久余额应为 50(到期未知/远期),实得 %v(若为 10049 说明失效包没跳过)",
|
||
tj["permanent"])
|
||
}
|
||
|
||
// ② 档位判定:有临时积分 → "expiring"(最优先)
|
||
if tj["tier"] != "expiring" {
|
||
t.Errorf("有 15 天内到期积分时档位应为 expiring,实得 %v", tj["tier"])
|
||
}
|
||
|
||
// ③ 锁定永久积分:该账号**仍可用**(因为有临时积分)
|
||
if _, err := ps.Call("channels", "__set_lock_permanent_probe",
|
||
map[string]interface{}{"lock": true}); err != nil {
|
||
t.Fatalf("锁定探针: %v", err)
|
||
}
|
||
out2, _ := ps.Call("channels", "credits_tier_probe",
|
||
map[string]interface{}{"platform": "buddy", "account_id": "tieracct"})
|
||
tj2, _ := out2.(map[string]interface{})["json"].(map[string]interface{})
|
||
if tj2["usable"] != true {
|
||
t.Errorf("锁定永久积分时,有临时积分的号仍应可用,实得 usable=%v", tj2["usable"])
|
||
}
|
||
// ④ 窗口可被显式覆盖:把窗口设成 0 ⇒ 没有"临时积分" ⇒ 只剩永久 ⇒ 锁定后不可用
|
||
if _, err := ps.Call("channels", "__set_expiring_window_probe",
|
||
map[string]interface{}{"days": 0}); err != nil {
|
||
t.Fatalf("窗口探针: %v", err)
|
||
}
|
||
out3, _ := ps.Call("channels", "credits_tier_probe",
|
||
map[string]interface{}{"platform": "buddy", "account_id": "tieracct"})
|
||
tj3, _ := out3.(map[string]interface{})["json"].(map[string]interface{})
|
||
if got, _ := tj3["expiring"].(float64); got != 0 {
|
||
t.Errorf("窗口=0 时临时桶应为 0,实得 %v", tj3["expiring"])
|
||
}
|
||
if tj3["usable"] != false {
|
||
t.Errorf("窗口=0 且锁定永久积分时该号应不可用,实得 usable=%v", tj3["usable"])
|
||
}
|
||
}
|
||
|
||
// nowMs 返回当前毫秒时间戳(与插件里 DeductionEndTime 的口径一致)。
|
||
func nowMs() int64 { return time.Now().UnixMilli() }
|