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,164 @@
package lua
import (
"encoding/json"
"strings"
"testing"
)
// Gemini(Cloud Code)走**私有信封协议**,不是公开的 /v1beta/models/…。
// 参考 gemini.ts 点名三件必须保留:
// 1. 身份五头**逐字写死、不许随机**,且**不带** x-goog-api-key
// 2. 流式刻意**不带** Accept
// 3. 信封**逐层字母序**序列化(上游按字节比对)
//
// 此前我们的 gemini.lua 产的是公开的 contents/parts 裸体、且一个身份头都不发,
// 端点还指着公开路径 —— 凭据与协议都不匹配。
func TestChannelsGeminiPrivateProtocol(t *testing.T) {
vm, ps, _ := runtimePlugins(t)
src, err := bundledAdapters.ReadFile("plugins/channels.lua")
if err != nil {
t.Fatal(err)
}
if err := ps.LoadSource("channels", string(src)); err != nil {
t.Fatal(err)
}
names := map[string]bool{}
for _, a := range vm.ListAdapters() {
names[a.Name] = true
}
if !names["channels-gemini"] {
t.Fatalf("插件未装 channels-gemini;已装: %v", names)
}
// 内置 gemini(公开 API 用)不得被顶掉
if !names["gemini"] {
t.Error("内置 gemini 适配器被顶掉了")
}
if ep := vm.Endpoint("channels-gemini"); ep != "/v1internal:generateContent" {
t.Errorf("端点应为私有动词路径,实得 %q", ep)
}
if se := vm.StreamEndpoint("channels-gemini"); !strings.Contains(se, "streamGenerateContent") {
t.Errorf("流式端点不对: %q", se)
}
// 身份五头 + 不得带 google api key
hdrs, err := vm.BuildHeaders("channels-gemini", map[string]interface{}{"api_key": "tok"})
if err != nil {
t.Fatalf("BuildHeaders: %v", err)
}
for k, want := range map[string]string{
"User-Agent": "antigravity/4.3.0 (cmdc-pak)",
"x-client-name": "antigravity",
"x-client-version": "4.3.0",
"x-machine-id": "cmdc-pak",
"x-vscode-sessionid": "proxy",
} {
if hdrs[k] != want {
t.Errorf("身份头 %s 应为 %q,实得 %q", k, want, hdrs[k])
}
}
for _, bad := range []string{"x-goog-api-key", "x-goog-api-client", "Accept"} {
if _, ok := hdrs[bad]; ok {
t.Errorf("**不得**发 %s 头(参考点名)", bad)
}
}
// 信封:model/project/request/requestId/userAgent 五件 + request 内层
in := `{"model":"gemini-3.1-flash","messages":[` +
`{"role":"system","content":"sys"},` +
`{"role":"user","content":"hello"}],` +
`"max_tokens":100,"stream":true}`
out, err := vm.Transform("channels-gemini", "transform_request", in)
if err != nil {
t.Fatalf("Transform: %v", err)
}
t.Logf("信封: %s", out)
var env map[string]interface{}
if err := json.Unmarshal([]byte(out), &env); err != nil {
t.Fatalf("信封不是合法 JSON: %v\n%s", err, out)
}
for _, k := range []string{"model", "project", "request", "requestId", "userAgent"} {
if _, ok := env[k]; !ok {
t.Errorf("信封缺顶层字段 %s(私有端点要信封,不是裸 contents)", k)
}
}
if env["userAgent"] != "antigravity" {
t.Errorf("userAgent 应为 antigravity,实得 %v", env["userAgent"])
}
req, _ := env["request"].(map[string]interface{})
if req == nil {
t.Fatal("request 不是对象")
}
if _, ok := req["contents"]; !ok {
t.Error("request 缺 contents")
}
if _, ok := req["systemInstruction"]; !ok {
t.Error("system 消息应进 systemInstruction")
}
if _, ok := req["sessionId"]; !ok {
t.Error("request 缺 sessionId")
}
// ③ 字母序:顶层键与信封内层都必须有序(上游按字节比对)
if !isSortedJSONObject(out) {
t.Errorf("信封未按键字母序序列化(参考 marshalAlphabetical):\n%s", out)
}
// ④ 响应必须**剥一层**信封({response:{candidates}})—— 不剥则拿不到
// candidates,表现为"模型没回话"
respIn := `{"response":{"candidates":[{"content":{"parts":[{"text":"hi"}]},` +
`"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":3,` +
`"candidatesTokenCount":2,"totalTokenCount":5}}}`
rout, err := vm.Transform("channels-gemini", "transform_response", respIn)
if err != nil {
t.Fatalf("transform_response: %v", err)
}
var ru map[string]interface{}
if err := json.Unmarshal([]byte(rout), &ru); err != nil {
t.Fatalf("响应不是合法 JSON: %v\n%s", err, rout)
}
if ru["content"] != "hi" {
t.Errorf("响应未剥信封(content 应来自 response.candidates[0]):%s", rout)
}
if ru["finish_reason"] != "stop" {
t.Errorf("finishReason STOP 应映射为 stop,实得 %v", ru["finish_reason"])
}
tu, _ := ru["token_usage"].(map[string]interface{})
if tu == nil || tu["prompt"] != float64(3) {
t.Errorf("usageMetadata 未映射到 token_usage:%v", ru["token_usage"])
}
}
// isSortedJSONObject 检查 JSON 对象键是否字母序(只查顶层,够用)。
func isSortedJSONObject(s string) bool {
s = strings.TrimSpace(s)
if !strings.HasPrefix(s, "{") {
return false
}
dec := json.NewDecoder(strings.NewReader(s))
tok, err := dec.Token()
if err != nil {
return false
}
if d, ok := tok.(json.Delim); !ok || d != '{' {
return false
}
keys := []string{}
for dec.More() {
k, err := dec.Token()
if err != nil {
return false
}
keys = append(keys, k.(string))
var skip json.RawMessage
if err := dec.Decode(&skip); err != nil {
return false
}
}
for i := 1; i < len(keys); i++ {
if keys[i-1] > keys[i] {
return false
}
}
return true
}