Update proxy to Zhanlu v1.4.2 provider flow

The 1.4.2 extension replaced the old signed/encrypted chat gateway with an
OpenAI-compatible aigateway. Align the proxy with the new flow:

- Use ecloud.10086.cn login/model base URLs, zhanlu_ide plugin headers and
  v1.4.2 plugin version
- Provision the model API key via SM2-signed get-or-create after v1/login
  profile fetch; store api_key/model_base_url/email in credentials
- Chat via Bearer apiKey against {modelBaseUrl}/chat/completions with plain
  OpenAI SSE passthrough; fetch /v1/models from the gateway model-info endpoint
- Force HTTP/1.1 upstream (gateway drops HTTP/2 ALPN negotiation with EOF)
- Drop obsolete AES body encryption, model name mapping and vscode headers
This commit is contained in:
2026-08-05 14:55:28 +08:00
parent c3af3caa5b
commit 7d8a5b6f74
18 changed files with 1122 additions and 232 deletions
+54 -38
View File
@@ -10,74 +10,90 @@ import (
"time"
)
type ExchangeResponse struct {
ErrorCode string `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
Message string `json:"message"`
Body map[string]any `json:"body"`
}
func ExchangeCode(client *http.Client, endpoint string, code string, decryptKey string) (Credentials, error) {
// ExchangeCode exchanges an SSO auth code for a user profile via the Zhanlu
// gateway authToken endpoint (POST /api/acepilot/zhanlu/authToken). The
// profile fields may be AES-ECB encrypted with the token decrypt key; each
// field falls back to the raw value when decryption fails.
func ExchangeCode(client *http.Client, endpoint string, code string, decryptKey string) (Profile, error) {
if strings.TrimSpace(endpoint) == "" {
return Credentials{}, errors.New("exchange endpoint is required")
return Profile{}, errors.New("exchange endpoint is required")
}
if strings.TrimSpace(code) == "" {
return Credentials{}, errors.New("code is required")
}
if strings.TrimSpace(decryptKey) == "" {
return Credentials{}, errors.New("decrypt key is required")
return Profile{}, errors.New("code is required")
}
if client == nil {
client = &http.Client{Timeout: 60 * time.Second}
// HTTP/1.1 only: the Zhanlu gateway drops HTTP/2 negotiation.
client = &http.Client{Timeout: 60 * time.Second, Transport: &http.Transport{ForceAttemptHTTP2: false}}
}
body, _ := json.Marshal(map[string]string{"code": code})
body, _ := json.Marshal(map[string]string{"deputyAccountNumber": code})
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return Credentials{}, err
return Profile{}, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return Credentials{}, err
return Profile{}, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b := make([]byte, 1024)
n, _ := resp.Body.Read(b)
return Profile{}, fmt.Errorf("exchange returned %d: %s", resp.StatusCode, strings.TrimSpace(string(b[:n])))
}
var exchange ExchangeResponse
if err := json.NewDecoder(resp.Body).Decode(&exchange); err != nil {
return Credentials{}, err
return Profile{}, err
}
if exchange.ErrorCode != "Success" {
if exchange.ErrorCode != "" && exchange.ErrorCode != "Success" {
msg := firstNonEmpty(exchange.ErrorMsg, exchange.Message, exchange.ErrorCode)
return Credentials{}, fmt.Errorf("exchange failed: %s", msg)
return Profile{}, fmt.Errorf("exchange failed: %s", msg)
}
if exchange.State != "" && exchange.State != "OK" {
msg := firstNonEmpty(exchange.ErrorMsg, exchange.Message, exchange.State)
return Profile{}, fmt.Errorf("exchange failed: %s", msg)
}
ak, err := decryptBodyField(exchange.Body, "ak", decryptKey)
if err != nil {
return Credentials{}, err
profile := Profile{}
for _, m := range []map[string]any{exchange.Body, exchange.Result, exchange.Data} {
if m == nil {
continue
}
profile.Email = decryptProfileField(m, "email", decryptKey)
profile.Organization = decryptProfileField(m, "organization", decryptKey)
profile.Team = decryptProfileField(m, "team", decryptKey)
profile.UserName = decryptProfileField(m, "name", decryptKey)
profile.Telephone = decryptProfileField(m, "telephone", decryptKey)
if profile.Email != "" || profile.Organization != "" || profile.Team != "" {
return profile, nil
}
}
sk, err := decryptBodyField(exchange.Body, "sk", decryptKey)
if err != nil {
return Credentials{}, err
}
token, err := decryptBodyField(exchange.Body, "token", decryptKey)
if err != nil {
return Credentials{}, err
}
return Credentials{AccessKey: ak, SecretKey: sk, Token: token, SavedAt: time.Now()}, nil
return Profile{}, errors.New("exchange response body missing profile fields")
}
func decryptBodyField(body map[string]any, key string, decryptKey string) (string, error) {
v, ok := body[key]
func decryptProfileField(m map[string]any, key, decryptKey string) string {
v, ok := m[key]
if !ok {
return "", fmt.Errorf("response body missing %s", key)
return ""
}
s, ok := v.(string)
if !ok || strings.TrimSpace(s) == "" {
return "", fmt.Errorf("response body %s is not a string", key)
if !ok {
return ""
}
return DecryptCredential(strings.TrimSpace(s), decryptKey)
return DecryptCredentialOrRaw(strings.TrimSpace(s), decryptKey)
}
type ExchangeResponse struct {
ErrorCode string `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
Message string `json:"message"`
State string `json:"state"`
Body map[string]any `json:"body"`
Result map[string]any `json:"result"`
Data map[string]any `json:"data"`
}
func firstNonEmpty(values ...string) string {