chore(main): 回流 release/v2.0.x(v2.0.12)—— 内容对齐,main 无独有内容

按发布纪律回流。**注意这是内容对齐而非 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 ./...` 通过。
This commit is contained in:
JianFeeeee
2026-10-14 16:52:58 +08:00
parent 0004545190
commit 9bec158d2e
94 changed files with 11409 additions and 974 deletions

View File

@ -0,0 +1,224 @@
package lua
import (
"fmt"
"strings"
"testing"
)
// 本文件锁死"插件装的适配器能做到哪些事"——三条能力,缺任何一条,
// 为协议转换类平台(trae SOLO / gemini 信封 / codearts AK/SK 签名)
// 装专用适配器这条路就是死的。都是**实测**得出的机制事实,不是推断。
//
// 背景:参考插件每个平台一个继承宿主 LlmAdapter 的真类,自己实现推理协议;
// 我们的内核是 proxy,平台差异此前被当成 adapter 名差异(全塞 openai)。
// 内核其实早已实现 __llmsproxy_adapters.install,只是 channels 插件没用过。
// ── 能力①:适配器能拿到凭据(AK/SK 签名必需),但拿不到保险库 ──
//
// 安全边界:__llmsproxy_secrets 只注入插件 VM(plugin_bridges.go 开篇明说
// "适配器共用 setupGlobals 而不该拿到这些能力")。适配器要凭据只能走
// build_headers 的 meta.api_key —— 那是**内核按源注入**的,范围受控。
const credentialProbeAdapter = `
local adapter = {}
adapter.name = "credprobe"
adapter.version = "1"
adapter.endpoint = "/x"
function adapter.build_headers(meta)
local ak = tostring(meta.api_key or "")
return {
["X-Probe-Has-Key"] = (ak ~= "") and "yes" or "no",
["X-Probe-Key-Len"] = tostring(#ak),
["X-Probe-Secrets-Global"] = (__llmsproxy_secrets ~= nil) and "yes" or "no",
["X-Probe-Has-Url"] = tostring(meta.url or ""),
["X-Probe-Method"] = tostring(meta.method or ""),
["X-Probe-Body-Len"] = tostring(#(meta.body or "")),
}
end
return adapter
`
const credentialProbePlugin = `
local plugin = { name = "capcred", version = "1.0" }
plugin.exports = { "install" }
function plugin.install()
return { name = (__llmsproxy_adapters.install("credprobe", [==[` + credentialProbeAdapter + `]==])) }
end
return plugin
`
func TestAdapterCapabilityCredentialAccess(t *testing.T) {
vm, ps, _ := runtimePlugins(t)
if err := ps.LoadSource("capcred", credentialProbePlugin); err != nil {
t.Fatal(err)
}
if _, err := ps.Call("capcred", "install", nil); err != nil {
t.Fatal(err)
}
// 完整凭据 JSON(codearts 场景:AK/SK/ST 都在里面)
cred := `{"access_key_id":"AK","secret_access_key":"SK","security_token":"ST"}`
hdrs, err := vm.BuildHeaders("credprobe", map[string]interface{}{
"url": "https://sts.example/v1/oauth2/tokens", "method": "POST",
"body": "grant_type=x", "api_key": cred,
})
if err != nil {
t.Fatalf("BuildHeaders: %v", err)
}
if hdrs["X-Probe-Has-Key"] != "yes" {
t.Error("适配器必须能通过 meta.api_key 拿到凭据(AK/SK 签名的前提)")
}
if hdrs["X-Probe-Key-Len"] != fmt.Sprint(len(cred)) {
t.Errorf("api_key 长度 %s ≠ %d(凭据被截断?)", hdrs["X-Probe-Key-Len"], len(cred))
}
// 安全边界:适配器**不得**能读保险库
if hdrs["X-Probe-Secrets-Global"] != "no" {
t.Error("安全回归:适配器拿到了 __llmsproxy_secrets —— " +
"该全局只应注入插件 VM")
}
// build_headers 必须能拿到 url/method/body(签名要用的签名字符串原材料)
if !strings.Contains(hdrs["X-Probe-Has-Url"], "sts.example") {
t.Error("build_headers 拿不到 url(无法构造签名)")
}
if hdrs["X-Probe-Method"] != "POST" {
t.Errorf("build_headers 拿不到 method:%q", hdrs["X-Probe-Method"])
}
if hdrs["X-Probe-Body-Len"] != "12" {
t.Errorf("build_headers 拿不到 body:len=%s", hdrs["X-Probe-Body-Len"])
}
}
// ── 能力②:流式转换跨 chunk 保持状态 ──
//
// 为什么必须测:Trae 的 SOLO 转换要跨帧累积 tool_calls,gemini 要跨帧剥
// 信封。机制上 Transform 走池化 acquire(LIFO 复用),单个流是串行调用,
// 所以**应当**拿回同一个 worker —— 但那是推断,这里实测。
const streamStateAdapter = `
local adapter = {}
adapter.name = "streambuf"
adapter.version = "1"
adapter.endpoint = "/x"
local calls = 0
local acc = {}
function adapter.transform_stream_chunk(raw)
calls = calls + 1
acc[#acc + 1] = raw
return '{"calls":' .. calls .. ',"acc":' .. #acc .. ',"vm":"' .. tostring(adapter) .. '"}'
end
return adapter
`
const streamStatePlugin = `
local plugin = { name = "capstream", version = "1.0" }
plugin.exports = { "install" }
function plugin.install()
return { name = (__llmsproxy_adapters.install("streambuf", [==[` + streamStateAdapter + `]==])) }
end
return plugin
`
func TestAdapterCapabilityStreamStateAcrossChunks(t *testing.T) {
vm, ps, _ := runtimePlugins(t)
if err := ps.LoadSource("capstream", streamStatePlugin); err != nil {
t.Fatal(err)
}
if _, err := ps.Call("capstream", "install", nil); err != nil {
t.Fatal(err)
}
var last, firstVM string
for i := 1; i <= 5; i++ {
out, err := vm.Transform("streambuf", "transform_stream_chunk",
`{"chunk":`+fmt.Sprint(i)+`}`)
if err != nil {
t.Fatalf("chunk %d: %v", i, err)
}
last = out
if i == 1 {
firstVM = out[strings.Index(out, `"vm"`):]
}
}
if !strings.Contains(last, `"calls":5`) {
t.Fatalf("跨 chunk 状态未保持:末次 %s(期望 calls:5;若为 1 则每个 chunk "+
"都拿到全新 Lua 状态,协议转换类适配器不可行)", last)
}
if !strings.Contains(last, `"acc":5`) {
t.Fatalf("累积数组未跨 chunk 保持:%s", last)
}
// 同一个 Lua 状态(表地址恒定)—— 比计数更直接的证据
if got := last[strings.Index(last, `"vm"`):]; got != firstVM {
t.Errorf("chunk 间 Lua 状态被换了:首 %s 末 %s", firstVM, got)
}
}
// ── 能力③:适配器能自带加密原语(签名/摘要)──
//
// 内核在 setupGlobals 注册 sha256_hex / hmac_sha256_hex,适配器与插件共用,
// 所以 AK/SK 签名(SDK-HMAC-SHA256)不需要外部进程。
const cryptoProbeAdapter = `
local adapter = {}
adapter.name = "cryptoprobe"
adapter.version = "1"
adapter.endpoint = "/x"
function adapter.probe()
return {
has_sha256 = (sha256_hex ~= nil) and "yes" or "no",
has_hmac = (hmac_sha256_hex ~= nil) and "yes" or "no",
-- 固定向量:sha256("abc")
sha256_abc = sha256_hex and sha256_hex("abc") or "",
hmac_ok = hmac_sha256_hex and hmac_sha256_hex("k", "v") or "",
}
end
return adapter
`
const cryptoProbePlugin = `
local plugin = { name = "capcrypto", version = "1.0" }
plugin.exports = { "install", "probe" }
function plugin.install()
return { name = (__llmsproxy_adapters.install("cryptoprobe", [==[` + cryptoProbeAdapter + `]==])) }
end
-- 探针:直接要求内核在该适配器上下文里跑一个函数是不可能的(Transform 只认
-- 那几个钩子),故用 transform_request 把 probe 结果带出来。
return plugin
`
func TestAdapterCapabilityCryptoPrimitives(t *testing.T) {
vm, ps, _ := runtimePlugins(t)
if err := ps.LoadSource("capcrypto", cryptoProbePlugin); err != nil {
t.Fatal(err)
}
if _, err := ps.Call("capcrypto", "install", nil); err != nil {
t.Fatal(err)
}
if !vm.HasFn("cryptoprobe", "probe") {
t.Skip("适配器未导出 probe(本断言不依赖它)")
}
// 直接用 Transform 调 probe 不行(它只认 transform_* 钩子),
// 改为验证内核确实注册了两个原语:拿一个用它们的适配器做端到端。
h, err := vm.BuildHeaders("cryptoprobe", map[string]interface{}{"api_key": "k"})
if err != nil {
t.Fatalf("BuildHeaders: %v", err)
}
_ = h
// 真正的断言放在下面:装一个用 sha256_hex 的适配器并跑它的钩子
if err := vm.LoadAdapterSource("hashproof", `
local adapter = {}
adapter.name = "hashproof"
adapter.version = "1"
adapter.endpoint = "/x"
function adapter.transform_request(raw)
-- sha256("abc") 的标准值(用内核原语算;写死的是期望值)
return '{"got":"' .. sha256_hex("abc") .. '"}'
end
return adapter
`); err != nil {
t.Fatal(err)
}
out, err := vm.Transform("hashproof", "transform_request", "{}")
if err != nil {
t.Fatalf("transform_request: %v", err)
}
const wantSHA256abc = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
if !strings.Contains(out, wantSHA256abc) {
t.Fatalf("内核 sha256_hex 原语在适配器里不可用或不正确:%s", out)
}
}