chore(main): 回流 —— opencode Zen 免费层可用 + noVNC path 相对名修复

内容与 release/v2.0.x 完全一致(git checkout release/v2.0.x -- .):
- feat(opencode): Zen 免费层门禁四要素 + 两个 id 公式(详见 release 提交)
- fix(vnc): noVNC 的 path 必须用相对名,否则 WebSocket 路径重复拼接
This commit is contained in:
JianFeeeee
2026-10-15 20:31:45 +08:00
parent 482a65958b
commit ddbafe822c
5 changed files with 891 additions and 2 deletions

View File

@ -0,0 +1,213 @@
package lua
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strings"
"testing"
"time"
)
// 真上游 e2e:用适配器产出的头 + body 直打 opencode /inference/v1。
// 需要真实凭据(从生产配置解密),故用 LLMSPROXY_LIVE_ZEN=1 显式开启。
func TestOpencodeZenLiveE2E(t *testing.T) {
if os.Getenv("LLMSPROXY_LIVE_ZEN") != "1" {
t.Skip("设 LLMSPROXY_LIVE_ZEN=1 开启真上游 e2e")
}
src, err := bundledAdapters.ReadFile("adapters/opencodezen.lua")
if err != nil {
t.Fatal(err)
}
vm := NewVM(t.TempDir())
defer vm.Stop()
if err := vm.LoadAdapterSource("opencodezen", string(src)); err != nil {
t.Fatal(err)
}
// 取生产凭据(解密)
out, err := exec.Command("/usr/local/bin/llmsproxy", "-show-secrets",
"-config", "/etc/llmsproxy/config.yaml").Output()
if err != nil {
t.Fatal(err)
}
apiKey := ""
for _, l := range strings.Split(string(out), "\n") {
if strings.HasPrefix(l, "source localzen") {
if i := strings.Index(l, "api_key="); i >= 0 {
apiKey = strings.TrimSpace(l[i+len("api_key="):])
}
}
}
if apiKey == "" {
t.Fatal("未取到 localzen 凭据")
}
cases := []struct {
name string
body string
want string
}{
{"纯对话(无 tools,靠适配器兜底注入)",
`{"model":"deepseek-v4.1-flash-free","stream":true,"max_tokens":60,
"messages":[{"role":"user","content":"reply with exactly: ADAPTER-OK"}]}`, "ADAPTER-OK"},
{"带自定义 system",
`{"model":"deepseek-v4.1-flash-free","stream":true,"max_tokens":60,
"messages":[{"role":"system","content":"You are terse."},{"role":"user","content":"reply with exactly: SYS-OK"}]}`, "SYS-OK"},
{"多轮",
`{"model":"deepseek-v4.1-flash-free","stream":true,"max_tokens":60,
"messages":[{"role":"user","content":"a"},{"role":"assistant","content":"b"},{"role":"user","content":"reply with exactly: MULTI-OK"}]}`, "MULTI-OK"},
{"算术(验证真推理)",
`{"model":"deepseek-v4.1-flash-free","stream":true,"max_tokens":60,
"messages":[{"role":"user","content":"What is 17*23? Reply with just the number."}]}`, "391"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
body, err := vm.Transform("opencodezen", "transform_request", tc.body)
if err != nil {
t.Fatal(err)
}
h, err := vm.BuildHeaders("opencodezen", map[string]interface{}{
"timestamp": time.Now().Unix(), "body": body,
})
if err != nil {
t.Fatal(err)
}
req, _ := http.NewRequest("POST",
"https://opencode.ai/inference/v1/chat/completions", bytes.NewReader([]byte(body)))
for k, v := range h {
req.Header.Set(k, v)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
cl := &http.Client{Timeout: 90 * time.Second}
resp, err := cl.Do(req)
if err != nil {
t.Fatal(err)
}
raw, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatalf("HTTP %d: %s", resp.StatusCode, string(raw)[:min(300, len(raw))])
}
// 上游现在返回 SSE(适配器强制 stream=true);判据需自己拼 delta.content。
got := ""
for _, line := range strings.Split(string(raw), "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "data: ") {
continue
}
p := strings.TrimPrefix(line, "data: ")
if p == "[DONE]" {
continue
}
var d struct {
Choices []struct {
Delta struct{ Content string } `json:"delta"`
} `json:"choices"`
}
if json.Unmarshal([]byte(p), &d) != nil || len(d.Choices) == 0 {
continue
}
got += d.Choices[0].Delta.Content
}
if got == "" {
t.Fatalf("未从 SSE 解出内容;原始=%s", string(raw)[:min(300, len(raw))])
}
if !strings.Contains(got, tc.want) {
t.Fatalf("回复 %q 不含 %q", got, tc.want)
}
t.Logf("✓ %s → %q", tc.name, strings.TrimSpace(got))
})
}
fmt.Println("真上游 e2e 全部通过")
}
// 经适配器完整转换链:上游 SSE → transform_stream_chunk → 统一格式。
// 上一条判据直接读原始 SSE,只证明"上游放行";这条证明"适配器能解析"。
func TestOpencodeZenLiveStreamThroughAdapter(t *testing.T) {
if os.Getenv("LLMSPROXY_LIVE_ZEN") != "1" {
t.Skip("设 LLMSPROXY_LIVE_ZEN=1 开启")
}
src, err := bundledAdapters.ReadFile("adapters/opencodezen.lua")
if err != nil {
t.Fatal(err)
}
vm := NewVM(t.TempDir())
defer vm.Stop()
if err := vm.LoadAdapterSource("opencodezen", string(src)); err != nil {
t.Fatal(err)
}
out, _ := exec.Command("/usr/local/bin/llmsproxy", "-show-secrets",
"-config", "/etc/llmsproxy/config.yaml").Output()
apiKey := ""
for _, l := range strings.Split(string(out), "\n") {
if strings.HasPrefix(l, "source localzen") {
if i := strings.Index(l, "api_key="); i >= 0 {
apiKey = strings.TrimSpace(l[i+len("api_key="):])
}
}
}
body, _ := vm.Transform("opencodezen", "transform_request",
`{"model":"deepseek-v4.1-flash-free","stream":true,"max_tokens":60,
"messages":[{"role":"user","content":"reply with exactly: CHAIN-OK"}]}`)
h, _ := vm.BuildHeaders("opencodezen", map[string]interface{}{
"timestamp": time.Now().Unix(), "body": body})
req, _ := http.NewRequest("POST",
"https://opencode.ai/inference/v1/chat/completions", bytes.NewReader([]byte(body)))
for k, v := range h {
req.Header.Set(k, v)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := (&http.Client{Timeout: 90 * time.Second}).Do(req)
if err != nil {
t.Fatal(err)
}
raw, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatalf("HTTP %d: %s", resp.StatusCode, string(raw)[:min(300, len(raw))])
}
// 逐行喂给适配器的流解析器,累积统一格式的 content
joined := ""
n := 0
for _, line := range strings.Split(string(raw), "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "data: ") || line == "data: [DONE]" {
continue
}
// 内核喂给适配器的是**已剥掉 data: 前缀**的载荷(见 provider.go:
// strings.TrimPrefix(line, "data:")),判据必须对齐这个契约。
payload := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
unified, err := vm.Transform("opencodezen", "transform_stream_chunk", payload)
if err != nil {
t.Fatalf("transform_stream_chunk: %v (%s)", err, line[:min(120, len(line))])
}
if unified == "" || unified == "{}" {
continue
}
var u struct {
Content string `json:"content"`
}
if json.Unmarshal([]byte(unified), &u) == nil {
joined += u.Content
n++
}
}
if n == 0 {
t.Fatal("适配器未解出任何 chunk(流解析链断了)")
}
if !strings.Contains(joined, "CHAIN-OK") {
t.Fatalf("经适配器链的内容 %q 不含 CHAIN-OK(chunk 数=%d)", joined, n)
}
t.Logf("✓ 完整链:%d 个 chunk,内容=%q", n, strings.TrimSpace(joined))
}