Files
magpie/internal/provider/zcode_client.go
T
yetone 901fd87ec0 zcode: a Start Plan request goes as the OpenCode plugin ARNO sent sends it, the one zcode.z.ai still serves (ARNO on Discord: "0.1.633版本还是不行,我给的插件还是成功的") — 11be3f31 followed ZCode 3.14.3's CLI source and some Start Plan requests were still turned away with 405 / code 3012 "request has been blocked due to unusual activity", while ARNO's plugin ("Freeflow for OpenCode and magpie", provider zcode-start), which ARNO sent us, works on the same machine and account. Where the plugin and 11be3f31 differ, the plugin now wins: the stable system block is the identity section and then the "# ZCode Desktop Context" section joined by "\n\n" (11be3f31 had dropped the desktop section; it is back from 31568cb0's text as zcode_prompt.json's desktop); the powered-by line names zai-api / bigmodel-api (was account:zai-start-plan / account:bigmodel-start-plan); the environment's working directory is the one the agent's own prompt names (Claude Code's "Primary working directory:", Codex's <cwd>), else the home directory (was "unknown"), and its shell is the basename of $SHELL or %ComSpec% (cmd.exe on Windows, was "cmd"); the date reminder is a user turn of its own before the agent's turns, always, its tags hugging the text ("<system-reminder>As you answer…task.</system-reminder>", no newlines; was put into the first user turn with newlines, and left out when that turn already had a <system-reminder>, as Claude Code's always does); every cache mark in the turns goes and the last block of the last turn gets one (a string turn becomes a text block), where the agent's last mark used to be kept wherever it was; the agent's system goes as plain text blocks (non-text blocks and empty text dropped); metadata.user_id is {"device_id":<this machine's device id>,"account_uuid":"","session_id":""} as JSON, the rest of metadata kept; X-Os-Version is "<platform> <release> <arch>" (was the release alone); X-Client-Timezone is always sent (TZ, /etc/localtime, else a zone at the local offset, Asia/Shanghai at +8); X-Client-Language falls back to the Mac's AppleLocale or Windows' display language, then en-US as Node's Intl does, so magpie opened from Finder with no LANG no longer says en-US for a zh-CN user; HTTP-Referer is https://zcode.z.ai as written; anthropic-version is 2023-06-01 and the agent's anthropic-beta (claude-code-20250219…) is no longer passed on. Unchanged and already the plugin's: the opening line block, the three cached blocks, User-Agent ZCode/3.14.3 ai-sdk/anthropic/3.0.81, X-ZCode-App-Version, X-Title Z Code@cli, X-ZCode-Agent glm, X-Release-Channel production, X-Platform, X-Os-Category, fresh x-request-id and x-zcode-trace-id, x-zcode-session-type main, the JWT as a Bearer, no x-api-key or X-Device-Mid, tools without cache marks. The prompt texts' SHA-256 (first 8 bytes) all match the plugin's: cliPrefix 46dd360a22c87a92, stableSections[0] 3f21ff9a88a03a76, [1] 39a1e86c93452c01, joined 49bda31511fc9670, beforeEnvironment bbdc66b399d297ff, afterEnvironment 1732b7f098a925d7. The agent's thinking and other body fields go as sent. No request was made to zcode.z.ai. TestZCodeStartPromptIsThePlugins (the hashes) and TestZCodeStartSentAsTheApp (headers, the exact environment section, the context prefix turn, the cache marks, metadata.user_id, BigModel's provider, a string last turn) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 15:11:32 +08:00

319 lines
10 KiB
Go

package provider
// ZCode's Start Plan serves only what looks like ZCode's own request: one
// without ZCode's system prompt is turned away with 405 "request has been
// blocked due to unusual activity", code 3012 (#425), and one dressed as
// the desktop app, or as ZCode 3.14.3's CLI source has it, still was for
// some, while the OpenCode plugin ARNO sent ("Freeflow", provider
// zcode-start) got through on the same account. So a Start Plan request
// goes as that plugin sends it: three cached system blocks (its opening
// line; its identity and harness, then the desktop context; "\n\n" and its
// dynamic sections around the environment), the agent's own system text
// after them uncached; the day in a <system-reminder> as a user turn of
// its own before the agent's; one cache mark in the turns, on the last;
// metadata.user_id naming the device; and its headers
// (zcodeSourceHeaders). The text is zcode_prompt.json. The GLM Coding
// Plan, and every other provider, get the agent's request as it is.
import (
_ "embed"
"bytes"
"encoding/json"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
)
//go:embed zcode_prompt.json
var zcodePromptJSON []byte
var zcodePrompt = func() (p struct {
Prefix string `json:"prefix"`
Stable string `json:"stable"`
Desktop string `json:"desktop"`
BeforeEnvironment string `json:"beforeEnvironment"`
AfterEnvironment string `json:"afterEnvironment"`
Environment struct {
Heading string `json:"heading"`
InvokedLine string `json:"invokedLine"`
CwdLabel string `json:"cwdLabel"`
GitLabel string `json:"gitLabel"`
GitNo string `json:"gitNo"`
PlatformLabel string `json:"platformLabel"`
ShellLabel string `json:"shellLabel"`
OSVersionLabel string `json:"osVersionLabel"`
PoweredByLine string `json:"poweredByLine"`
} `json:"environment"`
Context struct {
Intro string `json:"intro"`
Outro string `json:"outro"`
CurrentDateHeading string `json:"currentDateHeading"`
CurrentDateLine string `json:"currentDateLine"`
} `json:"context"`
}) {
if err := json.Unmarshal(zcodePromptJSON, &p); err != nil {
panic("zcode_prompt.json: " + err.Error())
}
return p
}()
// zcodePlatform is the platform as ZCode (Node's process.platform) names it.
func zcodePlatform() string {
if runtime.GOOS == "windows" {
return "win32"
}
return runtime.GOOS
}
// zcodeArch is the architecture as ZCode (Node's os.arch()) names it.
func zcodeArch() string {
switch runtime.GOARCH {
case "amd64":
return "x64"
case "386":
return "ia32"
}
return runtime.GOARCH
}
// zcodeOSVersion is the prompt's OS Version as ZCode gives it: "darwin
// 25.2.0 arm64".
func zcodeOSVersion() string {
parts := []string{zcodePlatform()}
if r := zcodeOSRelease(); r != "" {
parts = append(parts, r)
}
return strings.Join(append(parts, zcodeArch()), " ")
}
// zcodeStartProvider is the provider the powered-by line names for the
// Start Plan of an account on base, as the plugin the Start Plan serves
// names it: zai-api, or bigmodel-api on BigModel.
func zcodeStartProvider(base string) string {
if base == ZCodeBigModelBase {
return "bigmodel-api"
}
return "zai-api"
}
// zcodeCwdRe finds the working directory an agent names in its own prompt
// (Claude Code's "Primary working directory: …", Codex's <cwd>…</cwd>).
var zcodeCwdRe = regexp.MustCompile(`(?m)(?:^\s*-?\s*(?:Primary working directory|Working directory):[ \t]*([^\r\n]+?)[ \t]*$|<cwd>([^<\r\n]+)</cwd>)`)
// zcodeCwd is the agent's working directory, as the environment section's
// process.cwd(): the one its request names, else the user's home.
func zcodeCwd(texts []string) string {
for _, t := range texts {
if m := zcodeCwdRe.FindStringSubmatch(t); m != nil {
return strings.TrimSpace(m[1] + m[2])
}
}
if h, err := os.UserHomeDir(); err == nil && h != "" {
return h
}
return "unknown"
}
// zcodeShell is the shell as the environment section names it: the
// basename of $SHELL, or of %ComSpec% on Windows (cmd.exe).
func zcodeShell() string {
for _, k := range []string{"SHELL", "ComSpec"} {
if s := os.Getenv(k); s != "" {
return filepath.Base(s)
}
}
return "unknown"
}
// zcodeEnvironment is the prompt's environment section for model on
// provider, cwd being where the agent runs.
func zcodeEnvironment(provider, model, cwd string) string {
e := zcodePrompt.Environment
lines := []string{
e.Heading,
e.InvokedLine,
"- " + e.CwdLabel + ": " + cwd,
"- " + e.GitLabel + ": " + e.GitNo,
"- " + e.PlatformLabel + ": " + zcodePlatform(),
"- " + e.ShellLabel + ": " + zcodeShell(),
"- " + e.OSVersionLabel + ": " + zcodeOSVersion(),
}
if model != "" {
lines = append(lines, strings.NewReplacer("{provider}", provider, "{model}", model).Replace(e.PoweredByLine))
}
return strings.Join(lines, "\n")
}
// zcodeStable is the stable system block: the identity and harness
// section, then the desktop context, as the plugin joins them.
func zcodeStable() string { return zcodePrompt.Stable + "\n\n" + zcodePrompt.Desktop }
// zcodeSystem is ZCode's three system blocks for model on provider.
func zcodeSystem(provider, model, cwd string) []any {
cached := func(text string) map[string]any {
return map[string]any{"type": "text", "text": text, "cache_control": map[string]any{"type": "ephemeral"}}
}
dynamic := strings.Join([]string{zcodePrompt.BeforeEnvironment, zcodeEnvironment(provider, model, cwd), zcodePrompt.AfterEnvironment}, "\n\n")
return []any{cached(zcodePrompt.Prefix), cached(zcodeStable()), cached("\n\n" + dynamic)}
}
// zcodeDateReminder is the context prefix, a user turn of its own before
// the agent's: the day in a <system-reminder>, the tags hugging the text.
func zcodeDateReminder(now time.Time) map[string]any {
c := zcodePrompt.Context
text := strings.Join([]string{c.Intro, c.CurrentDateHeading + "\n" + strings.ReplaceAll(c.CurrentDateLine, "{date}", now.Format("2006-01-02")), "", c.Outro}, "\n")
return map[string]any{"role": "user", "content": []any{map[string]any{"type": "text", "text": "<system-reminder>" + text + "</system-reminder>"}}}
}
// zcodeUserID is metadata.user_id as the plugin sends it: this machine's
// device id, no account or session, as JSON.
func zcodeUserID() string {
b, _ := json.Marshal(struct {
DeviceID string `json:"device_id"`
AccountUUID string `json:"account_uuid"`
SessionID string `json:"session_id"`
}{zcodeDeviceMid(), "", ""})
return string(b)
}
// zcodeStartBody is an Anthropic messages request as the plugin the Start
// Plan serves sends it, provider (zcodeStartProvider) serving it: ZCode's
// three cached system blocks, then the agent's own system text uncached;
// the context prefix as the first turn; no cache mark in the turns but one
// on the last block of the last; none on the tools; and metadata.user_id
// naming the device. A body that isn't one, or already starts with
// ZCode's prompt, is left as it is.
func zcodeStartBody(body []byte, provider string, now time.Time) []byte {
var m map[string]json.RawMessage
if json.Unmarshal(body, &m) != nil || m["messages"] == nil {
return body
}
var model string
json.Unmarshal(m["model"], &model)
var own []any
var texts []string
if raw := m["system"]; raw != nil {
var s string
var blocks []any
if json.Unmarshal(raw, &s) == nil {
if strings.TrimSpace(s) != "" {
own = []any{map[string]any{"type": "text", "text": s}}
texts = append(texts, s)
}
} else if zcodeDecode(raw, &blocks) == nil {
for _, b := range blocks {
b, _ := b.(map[string]any)
t, _ := b["text"].(string)
if b["type"] == "text" && t != "" {
if t == zcodePrompt.Prefix && len(own) == 0 {
return body
}
own = append(own, map[string]any{"type": "text", "text": t})
texts = append(texts, t)
}
}
} else if string(raw) != "null" {
return body
}
}
var msgs []map[string]any
if zcodeDecode(m["messages"], &msgs) != nil {
return body
}
if len(msgs) > 0 {
switch c := msgs[0]["content"].(type) {
case string:
texts = append(texts, c)
case []any:
for _, b := range c {
if b, ok := b.(map[string]any); ok {
if t, ok := b["text"].(string); ok {
texts = append(texts, t)
}
}
}
}
}
system, _ := zcodeEncode(append(zcodeSystem(provider, model, zcodeCwd(texts)), own...))
msgs = append([]map[string]any{zcodeDateReminder(now)}, msgs...)
lastAt := -1
for i, msg := range msgs {
if msg["role"] == "system" {
continue
}
lastAt = i
if blocks, ok := msg["content"].([]any); ok {
for _, b := range blocks {
if b, ok := b.(map[string]any); ok {
delete(b, "cache_control")
}
}
}
}
if lastAt >= 0 {
switch c := msgs[lastAt]["content"].(type) {
case string:
msgs[lastAt]["content"] = []any{map[string]any{"type": "text", "text": c, "cache_control": map[string]any{"type": "ephemeral"}}}
case []any:
if len(c) > 0 {
if b, ok := c[len(c)-1].(map[string]any); ok {
b["cache_control"] = map[string]any{"type": "ephemeral"}
}
}
}
}
messages, err := zcodeEncode(msgs)
if err != nil {
return body
}
if raw := m["tools"]; raw != nil {
var tools []map[string]any
if zcodeDecode(raw, &tools) == nil {
for _, t := range tools {
delete(t, "cache_control")
}
m["tools"], _ = zcodeEncode(tools)
}
}
meta := map[string]any{}
if raw := m["metadata"]; raw != nil {
zcodeDecode(raw, &meta)
if meta == nil {
meta = map[string]any{}
}
}
meta["user_id"] = zcodeUserID()
m["metadata"], _ = zcodeEncode(meta)
m["system"], m["messages"] = system, messages
out, err := zcodeEncode(m)
if err != nil {
return body
}
return out
}
// zcodeDecode reads JSON keeping its numbers as they are written.
func zcodeDecode(raw []byte, v any) error {
d := json.NewDecoder(bytes.NewReader(raw))
d.UseNumber()
return d.Decode(v)
}
// zcodeEncode writes JSON leaving <, > and & as they are.
func zcodeEncode(v any) ([]byte, error) {
var b bytes.Buffer
e := json.NewEncoder(&b)
e.SetEscapeHTML(false)
if err := e.Encode(v); err != nil {
return nil, err
}
return bytes.TrimSuffix(b.Bytes(), []byte("\n")), nil
}