Files
yetone 11be3f31b0 zcode: a Start Plan request goes as the ZCode CLI sends it, not the desktop app (miaopasi and ARNO on Discord: Start plan还是405 感觉不是IP的问题; an OpenCode plugin sending the CLI's request works on the same plan) — dressed as the desktop app, some Start Plan requests were still turned away with 405 "request has been blocked due to unusual activity", code 3012. Following ZCode 3.14.3's own source (github.com/zai-org/ZCode, apps/zcode-cli/packages/core/src/context/builder.ts and sections/, bootstrap/src/model-config.ts and runtime-platform-headers.ts, adapters/src/model/runner-attribution.ts), the request is now the CLI's: X-Title "Z Code@cli" (was "Z Code@electron", which only the desktop app's agent server sends); the stable system block is the identity section alone, the "ZCode Desktop Context" section being added only for presentationSurface zcode_desktop; X-Platform is <platform>-<arch> and the new X-Os-Version is os.release(), as runtime-platform-headers sets them (Darwin's uname release, Windows' Major.Minor.Build from RtlGetVersion); the environment block's OS Version is "<platform> <release> <arch>" as env-info.ts writes it (was "unknown") and its powered-by line is the provider id and model as ZCode names them (account:zai-start-plan/GLM-5.3-Flash, account:bigmodel-start-plan on BigModel; was lowercased without the provider); the currentDate reminder is wrapped as system-reminder/source.ts wraps a context prefix (a newline after the opening tag, one after the closing). Model requests no longer carry X-Device-Mid (ZCode sends it only to its own API, where the balance still needs it, #282) nor x-api-key: the JWT goes as a Bearer only, as the plugin that works sends it (the official client passes it as the SDK's apiKey too, so this follows the working plugin over the source). The prompt text itself was checked byte for byte against the 3.14.3 sections (rendered with node). The plugin's own source wasn't found (pi-freeflow has no zcode code), so where it and the source might differ (X-Os-Version as release alone, not "<platform> <release> <arch>") the source wins. No request was made to zcode.z.ai. TestZCodeStartSentAsTheApp, TestZCodeStartPlanOwnAccount and TestZCodeSignInStartPlanDeviceMid are updated; with the old prompt, X-Title and x-api-key they fail (X-Title "Z Code@electron", X-Api-Key sent, the desktop section in the system prompt). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gateway/ pass.
2026-10-02 13:17:26 +08:00

93 lines
3.3 KiB
Go

package provider
import (
"context"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// stripDeviceMid sends requests as magpie did before #282: with no
// X-Device-Mid.
type stripDeviceMid struct{}
func (stripDeviceMid) RoundTrip(r *http.Request) (*http.Response, error) {
r = r.Clone(r.Context())
r.Header.Del("X-Device-Mid")
return http.DefaultTransport.RoundTrip(r)
}
// #282: zcode.z.ai's Start Plan balance answers 400, code 3001 "parameter
// error", to a request with no X-Device-Mid, so an account with only the
// Start Plan could not be added: the refusal was taken for the account
// having no Start Plan. magpie now names the machine as ZCode does, and a
// balance it can't read is said as it is.
func TestZCodeSignInStartPlanDeviceMid(t *testing.T) {
home := signIn(t)
jwt := zcodeTestJWT(time.Now().Add(24 * time.Hour))
u := newZCodeStartUpstream(t, jwt)
u.balance = zcodeActiveStart(time.Now(), "active")
ctx := context.Background()
k, plan, err := zcodeSignedIn(ctx, "zai", "zai-tok", jwt)
if err != nil || plan != "Start Plan" || k.JWT != jwt {
t.Fatalf("start plan: %v %q %+v", err, plan, k)
}
if q := zcodeStartQuota(ctx, Login{User: "a@example.com"}, jwt); q.Error != "" || len(q.Windows) != 1 {
t.Fatalf("quota: %+v", q)
}
// the id is a UUID, kept in magpie's own directory, never ZCode's
id := zcodeDeviceMid()
if !zcodeUUIDRe.MatchString(id) || id[14] != '4' {
t.Fatalf("not a v4 UUID: %q", id)
}
if b, err := os.ReadFile(filepath.Join(home, ".config", "magpie", "zcode-device-mid")); err != nil || strings.TrimSpace(string(b)) != id {
t.Fatalf("kept: %v %q", err, b)
}
if _, err := os.Stat(filepath.Join(home, ".zcode")); !os.IsNotExist(err) {
t.Fatalf("touched ~/.zcode: %v", err)
}
// the same next call, and in the next run of magpie
if zcodeDeviceMid() != id {
t.Fatal("changed between calls")
}
zcodeDevices.Lock()
zcodeDevices.m = map[string]string{}
zcodeDevices.Unlock()
if got := zcodeDeviceMid(); got != id {
t.Fatalf("changed between runs: %q %q", got, id)
}
// but not on the Start Plan's model requests, which ZCode sends without it
req, _ := http.NewRequest("POST", u.srv.URL+"/api/v1/zcode-plan/anthropic/v1/messages", nil)
zcodeSourceHeaders(req)
if req.Header.Get("X-Device-Mid") != "" {
t.Fatalf("model request: %v", req.Header)
}
// not to Z.ai's business API
req, _ = http.NewRequest("GET", "https://api.z.ai/api/biz/subscription/list", nil)
zcodeDeviceHeader(req)
if req.Header.Get("X-Device-Mid") != "" {
t.Fatalf("business API: %v", req.Header)
}
// without it, as before: refused, and the refusal is what the sign-in says
old := http.DefaultClient
http.DefaultClient = &http.Client{Transport: stripDeviceMid{}}
t.Cleanup(func() { http.DefaultClient = old })
_, _, err = zcodeSignedIn(ctx, "zai", "zai-tok", jwt)
if err == nil || !strings.Contains(err.Error(), "parameter error (400, code 3001)") || strings.Contains(err.Error(), "has ended") {
t.Fatalf("without X-Device-Mid: %v", err)
}
http.DefaultClient = old
// no Start Plan at all still says so
u.balance = zcodeActiveStart(time.Now(), "expired")
if _, _, err := zcodeSignedIn(ctx, "zai", "zai-tok", jwt); err == nil || !strings.Contains(err.Error(), "has ended or was never started") {
t.Fatalf("no start plan: %v", err)
}
}