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 ./...` 通过。
172 lines
6.5 KiB
Go
172 lines
6.5 KiB
Go
package core
|
||
|
||
import (
|
||
"crypto/hmac"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// CodeArts 专用适配器是通过**插件自装**(__llmsproxy_adapters.install)
|
||
// 引入的,不是内置适配器 —— 因为 AK/SK 签名属厂商知识,内置里出现 codearts
|
||
// 就破了"内核零厂商名"纪律。
|
||
//
|
||
// 这组判据要证明三件事:
|
||
// 1. 插件加载时确实把适配器装上了
|
||
// 2. 装上之后,源能引用它(不是"装了但引用不到")
|
||
// 3. 它产出的签名是**真的能验**(而不是拼出个像签名的字符串)
|
||
func TestCodeArtsAdapterInstalledByPlugin(t *testing.T) {
|
||
c := kernelTestCore(t)
|
||
ps := c.Plugins()
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", src); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// ① 适配器必须在插件加载后就位
|
||
adapters := c.ListAdapters()
|
||
found := false
|
||
for _, a := range adapters {
|
||
if a.Name == "codearts" {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("插件未装 codearts 适配器;已装: %#v", adapters)
|
||
}
|
||
|
||
// ② 归属必须是 channels 插件(证明是它装的,不是内置)
|
||
if owner := ps.AdapterOwnerOf("codearts"); owner != "channels" {
|
||
t.Errorf("适配器归属应为 channels,实得 %q", owner)
|
||
}
|
||
|
||
// ③ 签名必须真的可验:用 Go 标准库按 SDK-HMAC-SHA256 独立复算
|
||
got, err := ps.Call("channels", "__codearts_adapter_sign_probe", map[string]interface{}{
|
||
"url": "https://snap-access.cn-north-4.myhuaweicloud.com/api/v2/chat/completions",
|
||
"method": "POST",
|
||
"body": `{"model":"CodeArts-Pangu"}`,
|
||
"api_key": `{"access_key_id":"AKTEST","secret_access_key":"SKTEST","security_token":"STTEST"}`,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("签名探针: %v", err)
|
||
}
|
||
w, _ := got.(map[string]interface{})
|
||
m, _ := w["json"].(map[string]interface{})
|
||
if m == nil {
|
||
t.Fatalf("探针无 json: %#v", got)
|
||
}
|
||
if m["error"] != nil {
|
||
t.Fatalf("探针报错: %v", m["error"])
|
||
}
|
||
auth, _ := m["authorization"].(string)
|
||
if !strings.HasPrefix(auth, "SDK-HMAC-SHA256 Access=AKTEST,SignedHeaders=") {
|
||
t.Fatalf("Authorization 形态不符(参考要求 Access= 不是 Credential=): %s", auth)
|
||
}
|
||
if !strings.Contains(auth, "Signature=") {
|
||
t.Fatalf("Authorization 缺 Signature: %s", auth)
|
||
}
|
||
// 签名后追加的头必须在,且不参与 canonical
|
||
for _, k := range []string{"Agent-Type", "X-Language"} {
|
||
if m["hdr_"+k] == nil || m["hdr_"+k] == "" {
|
||
t.Errorf("缺签名后追加的头 %s", k)
|
||
}
|
||
}
|
||
if m["hdr_Agent-Type"] != "PromptCenter" {
|
||
t.Errorf("Agent-Type = %v,应为 PromptCenter", m["hdr_Agent-Type"])
|
||
}
|
||
// canonical 不得包含签名后追加的头(参考注释点名:它们是签名后加的)
|
||
canon, _ := m["canonical"].(string)
|
||
if strings.Contains(canon, "agent-type") || strings.Contains(canon, "x-language") {
|
||
t.Errorf("canonical 不应含签名后追加的头:\n%s", canon)
|
||
}
|
||
// Authorization 不得进 canonical
|
||
if strings.Contains(canon, "authorization") {
|
||
t.Errorf("canonical 不应含 authorization:\n%s", canon)
|
||
}
|
||
// x-sdk-content-sha256 说明 payload hash 参与了签名
|
||
if !strings.Contains(canon, "x-sdk-content-sha256") {
|
||
t.Errorf("canonical 应含 x-sdk-content-sha256:\n%s", canon)
|
||
}
|
||
// URI 必须以 / 结尾(参考点名的易错点)
|
||
if !strings.Contains(canon, "/api/v2/chat/completions/\n") {
|
||
t.Errorf("canonical 的 URI 未补尾斜杠(参考点名的易错点):\n%s", canon)
|
||
}
|
||
// ④ **独立验签**:用 Go 标准库按 SDK-HMAC-SHA256 从 canonical 复算签名,
|
||
// 必须与适配器给出的 Signature 逐字节相同。
|
||
//
|
||
// 为什么必须有这一步:前面几条只验证"形态正确"(Access=、SignedHeaders、
|
||
// 尾斜杠)。签名算法本身写错(HMAC 输入少一段、canonical 分隔符用错)时
|
||
// 形态依然全对,但上游会 401。复算才是真正的判据。
|
||
sts, _ := m["string_to_sign"].(string)
|
||
if sts == "" {
|
||
t.Fatal("探针未带出 string_to_sign(判据必须比对真实函数算出的中间值)")
|
||
}
|
||
// string_to_sign = "SDK-HMAC-SHA256\n" + date + "\n" + sha256(canonical)
|
||
parts := strings.SplitN(sts, "\n", 3)
|
||
if len(parts) != 3 || parts[0] != "SDK-HMAC-SHA256" {
|
||
t.Fatalf("string_to_sign 头段不符: %q", sts)
|
||
}
|
||
canonSum := sha256.Sum256([]byte(canon))
|
||
if parts[2] != hex.EncodeToString(canonSum[:]) {
|
||
t.Errorf("string_to_sign 的 canonical 摘要不符\n算得: %s\n实得: %s",
|
||
hex.EncodeToString(canonSum[:]), parts[2])
|
||
}
|
||
mac := hmac.New(sha256.New, []byte("SKTEST"))
|
||
mac.Write([]byte(sts))
|
||
wantSig := hex.EncodeToString(mac.Sum(nil))
|
||
gotSig := auth[strings.Index(auth, "Signature=")+len("Signature="):]
|
||
if gotSig != wantSig {
|
||
t.Errorf("签名独立复算不符(算法写错?)\n复算: %s\n实得: %s", wantSig, gotSig)
|
||
}
|
||
|
||
b, _ := json.Marshal(m)
|
||
t.Logf("签名探针输出: %s", string(b))
|
||
}
|
||
|
||
// 源注册必须真的引用插件装的 codearts 适配器 —— "装了但源还指向 openai"
|
||
// 是这类改动最可能的漏配,且不会报错(只是签名不生效、上游 401)。
|
||
func TestCodeArtsSourceUsesPluginAdapter(t *testing.T) {
|
||
// ⚠️ 必须用 poolCore:它给 channels 授予了 add_source/list_sources 等
|
||
// 内核权限。用 kernelTestCore 裸加载插件时 add_source 会被拒
|
||
//("kernel action add_source is not granted to plugin channels"),
|
||
// 而这个拒绝**不报错、只是不出源** —— 我就在这里绕了一圈才看出根因。
|
||
c, ps := poolCore(t)
|
||
// 建一个 codearts 账号 → 触发源注册
|
||
m, err := ps.Call("channels", "account_add", map[string]interface{}{
|
||
"platform": "codearts",
|
||
"credential": `{"access_key_id":"AK","secret_access_key":"SK","security_token":"ST"}`,
|
||
})
|
||
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)
|
||
}
|
||
aid, _ := j["account_id"].(string)
|
||
srcs := listSourcesViaKernel(t, c)
|
||
sm, ok := srcHasAccount(srcs, aid)
|
||
if !ok {
|
||
names := []string{}
|
||
for _, x := range srcs {
|
||
if n, _ := x["name"].(string); n != "" {
|
||
names = append(names, n)
|
||
}
|
||
}
|
||
t.Fatalf("codearts 账号 %q 应已同步源;现有源: %v", aid, names)
|
||
}
|
||
if ad, _ := sm["adapter"].(string); ad != "codearts" {
|
||
t.Errorf("源应引用插件装的 codearts 适配器,实得 %q(签名不会生效)", ad)
|
||
}
|
||
bu, _ := sm["base_url"].(string)
|
||
if !strings.Contains(bu, "snap-access") {
|
||
t.Errorf("base_url 不符: %q", bu)
|
||
}
|
||
}
|