- Extract callUpstream helper to deduplicate ~30 lines between chatCompletions and responses - Move HTML templates to internal/server/templates/ via go:embed (server.go 1749→1192 lines) - Consolidate 4 copies of firstNonEmpty into util.FirstNonEmpty - Extract chatStreamChunk named type shared by aggregateStream and aggregateResponsesStream - Fix tool-call ordering: iterate sorted map keys instead of sequential 0..N - Map upstream finish_reason to Responses API status (length/content_filter → incomplete) - Surface /v1/models errors as 401/502 instead of silently returning empty 200 - Add Secure cookie flag via isTLSRequest helper - forEachSSEChunk: use sseDataPayload parser, drop redundant json.Valid, distinguish bufio.ErrTooLong - Fix StreamIdleTimout typo → StreamIdleTimeout - sso.go: single Read → io.ReadAll(io.LimitReader), explicit unknown error fallback
106 lines
3.3 KiB
Go
106 lines
3.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.misaka.ren/M1saka/zhanlu_proxy/internal/util"
|
|
)
|
|
|
|
// 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 Profile{}, errors.New("exchange endpoint is required")
|
|
}
|
|
if strings.TrimSpace(code) == "" {
|
|
return Profile{}, errors.New("code is required")
|
|
}
|
|
if client == nil {
|
|
// 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{"deputyAccountNumber": code})
|
|
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return Profile{}, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return Profile{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
return Profile{}, fmt.Errorf("exchange returned %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
|
|
}
|
|
|
|
var exchange ExchangeResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&exchange); err != nil {
|
|
return Profile{}, err
|
|
}
|
|
if exchange.ErrorCode != "" && exchange.ErrorCode != "Success" {
|
|
msg := util.FirstNonEmpty(exchange.ErrorMsg, exchange.Message, exchange.ErrorCode)
|
|
if msg == "" {
|
|
msg = "unknown error"
|
|
}
|
|
return Profile{}, fmt.Errorf("exchange failed: %s", msg)
|
|
}
|
|
if exchange.State != "" && exchange.State != "OK" {
|
|
msg := util.FirstNonEmpty(exchange.ErrorMsg, exchange.Message, exchange.State)
|
|
if msg == "" {
|
|
msg = "unknown error"
|
|
}
|
|
return Profile{}, fmt.Errorf("exchange failed: %s", msg)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
return Profile{}, errors.New("exchange response body missing profile fields")
|
|
}
|
|
|
|
func decryptProfileField(m map[string]any, key, decryptKey string) string {
|
|
v, ok := m[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
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"`
|
|
}
|