Files
2026-07-13 12:35:43 +08:00

317 lines
8.7 KiB
Go

package synthesizer
import (
"context"
"encoding/json"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
)
// FileSynthesizer generates Auth entries from OAuth JSON files.
// It handles file-based authentication.
type FileSynthesizer struct{}
// NewFileSynthesizer creates a new FileSynthesizer instance.
func NewFileSynthesizer() *FileSynthesizer {
return &FileSynthesizer{}
}
// Synthesize generates Auth entries from auth files in the auth directory.
func (s *FileSynthesizer) Synthesize(ctx *SynthesisContext) ([]*coreauth.Auth, error) {
out := make([]*coreauth.Auth, 0, 16)
if ctx == nil || ctx.AuthDir == "" {
return out, nil
}
entries, err := os.ReadDir(ctx.AuthDir)
if err != nil {
// Not an error if directory doesn't exist
return out, nil
}
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if !strings.HasSuffix(strings.ToLower(name), ".json") {
continue
}
full := filepath.Join(ctx.AuthDir, name)
data, errRead := os.ReadFile(full)
if errRead != nil || len(data) == 0 {
continue
}
auths := synthesizeFileAuths(ctx, full, data)
if len(auths) == 0 {
continue
}
out = append(out, auths...)
}
return out, nil
}
// SynthesizeAuthFile generates Auth entries for one auth JSON file payload.
// It shares exactly the same mapping behavior as FileSynthesizer.Synthesize.
func SynthesizeAuthFile(ctx *SynthesisContext, fullPath string, data []byte) []*coreauth.Auth {
return synthesizeFileAuths(ctx, fullPath, data)
}
func synthesizeFileAuths(ctx *SynthesisContext, fullPath string, data []byte) []*coreauth.Auth {
if ctx == nil || len(data) == 0 {
return nil
}
now := ctx.Now
cfg := ctx.Config
var metadata map[string]any
if errUnmarshal := json.Unmarshal(data, &metadata); errUnmarshal != nil {
return nil
}
t, _ := metadata["type"].(string)
provider := strings.ToLower(strings.TrimSpace(t))
if provider == "gemini" {
provider = "gemini-cli"
}
if ctx.PluginAuthParser != nil {
auths, handled, errParse := parsePluginFileAuths(ctx.PluginAuthParser, pluginapi.AuthParseRequest{
Provider: provider,
Path: fullPath,
FileName: filepath.Base(fullPath),
RawJSON: data,
})
if errParse == nil && handled {
auths = compactPluginAuths(auths)
if len(auths) == 0 {
return nil
}
perAccountExcluded := extractExcludedModelsFromMetadata(metadata)
perAccountModelAliases := extractOAuthModelAliasesFromMetadata(metadata)
disabled, _ := metadata["disabled"].(bool)
for index, auth := range auths {
if auth == nil {
continue
}
if len(auths) > 1 {
coreauth.MarkPluginVirtualAuth(auth, fullPath, index)
}
auth.CreatedAt = now
auth.UpdatedAt = now
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
auth.Attributes[coreauth.AttributePath] = fullPath
auth.Attributes[coreauth.AttributeSource] = fullPath
auth.Attributes[coreauth.AttributeSourceBackend] = coreauth.AuthSourceFile
if disabled {
auth.Disabled = true
auth.Status = coreauth.StatusDisabled
if auth.Metadata == nil {
auth.Metadata = make(map[string]any)
}
auth.Metadata["disabled"] = true
}
coreauth.SetOAuthModelAliasesAttribute(auth, perAccountModelAliases)
ApplyAuthExcludedModelsMeta(auth, cfg, perAccountExcluded, "oauth")
coreauth.ApplyCustomHeadersFromMetadata(auth)
}
return auths
}
}
if provider == "" || provider == "gemini-cli" {
return nil
}
label := provider
if email, _ := metadata["email"].(string); email != "" {
label = email
}
// Use relative path under authDir as ID to stay consistent with the file-based token store.
id := fullPath
if strings.TrimSpace(ctx.AuthDir) != "" {
if rel, errRel := filepath.Rel(ctx.AuthDir, fullPath); errRel == nil && rel != "" {
id = rel
}
}
if runtime.GOOS == "windows" {
id = strings.ToLower(id)
}
proxyURL := ""
if p, ok := metadata["proxy_url"].(string); ok {
proxyURL = p
}
prefix := ""
if rawPrefix, ok := metadata["prefix"].(string); ok {
trimmed := strings.TrimSpace(rawPrefix)
trimmed = strings.Trim(trimmed, "/")
if trimmed != "" && !strings.Contains(trimmed, "/") {
prefix = trimmed
}
}
disabled, _ := metadata["disabled"].(bool)
status := coreauth.StatusActive
if disabled {
status = coreauth.StatusDisabled
}
// Read per-account excluded models from the OAuth JSON file.
perAccountExcluded := extractExcludedModelsFromMetadata(metadata)
perAccountModelAliases := extractOAuthModelAliasesFromMetadata(metadata)
a := &coreauth.Auth{
ID: id,
Provider: provider,
Label: label,
Prefix: prefix,
Status: status,
Disabled: disabled,
Attributes: map[string]string{
coreauth.AttributeSource: fullPath,
coreauth.AttributePath: fullPath,
coreauth.AttributeSourceBackend: coreauth.AuthSourceFile,
},
ProxyURL: proxyURL,
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,
}
// Read priority from auth file.
if rawPriority, ok := metadata["priority"]; ok {
switch v := rawPriority.(type) {
case float64:
a.Attributes["priority"] = strconv.Itoa(int(v))
case string:
priority := strings.TrimSpace(v)
if _, errAtoi := strconv.Atoi(priority); errAtoi == nil {
a.Attributes["priority"] = priority
}
}
}
// Read note from auth file.
if rawNote, ok := metadata["note"]; ok {
if note, isStr := rawNote.(string); isStr {
if trimmed := strings.TrimSpace(note); trimmed != "" {
a.Attributes["note"] = trimmed
}
}
}
coreauth.ApplyCustomHeadersFromMetadata(a)
coreauth.SetOAuthModelAliasesAttribute(a, perAccountModelAliases)
ApplyAuthExcludedModelsMeta(a, cfg, perAccountExcluded, "oauth")
// For codex auth files, extract plan_type from the JWT id_token.
if provider == "codex" {
if idTokenRaw, ok := metadata["id_token"].(string); ok && strings.TrimSpace(idTokenRaw) != "" {
if claims, errParse := codex.ParseJWTToken(idTokenRaw); errParse == nil && claims != nil {
if pt := strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType); pt != "" {
a.Attributes["plan_type"] = pt
}
}
}
}
return []*coreauth.Auth{a}
}
func parsePluginFileAuths(parser PluginAuthParser, req pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
if parser == nil {
return nil, false, nil
}
if multiParser, ok := parser.(PluginMultiAuthParser); ok {
return multiParser.ParseAuths(context.Background(), req)
}
auth, handled, errParse := parser.ParseAuth(context.Background(), req)
if errParse != nil || !handled || auth == nil {
return nil, handled, errParse
}
return []*coreauth.Auth{auth}, true, nil
}
func compactPluginAuths(auths []*coreauth.Auth) []*coreauth.Auth {
if len(auths) == 0 {
return nil
}
out := auths[:0]
for _, auth := range auths {
if auth == nil {
continue
}
out = append(out, auth)
}
return out
}
// extractOAuthModelAliasesFromMetadata reads per-account model aliases from OAuth JSON metadata.
// Supports both "model_aliases" and "model-aliases" keys.
func extractOAuthModelAliasesFromMetadata(metadata map[string]any) []config.OAuthModelAlias {
if metadata == nil {
return nil
}
raw, ok := metadata["model_aliases"]
if !ok {
raw, ok = metadata["model-aliases"]
}
if !ok || raw == nil {
return nil
}
data, errMarshal := json.Marshal(raw)
if errMarshal != nil {
return nil
}
var aliases []config.OAuthModelAlias
if errUnmarshal := json.Unmarshal(data, &aliases); errUnmarshal != nil {
return nil
}
cfg := config.Config{
OAuthModelAlias: map[string][]config.OAuthModelAlias{
"auth": aliases,
},
}
cfg.SanitizeOAuthModelAlias()
return cfg.OAuthModelAlias["auth"]
}
// extractExcludedModelsFromMetadata reads per-account excluded models from the OAuth JSON metadata.
// Supports both "excluded_models" and "excluded-models" keys, and accepts both []string and []interface{}.
func extractExcludedModelsFromMetadata(metadata map[string]any) []string {
if metadata == nil {
return nil
}
// Try both key formats
raw, ok := metadata["excluded_models"]
if !ok {
raw, ok = metadata["excluded-models"]
}
if !ok || raw == nil {
return nil
}
var stringSlice []string
switch v := raw.(type) {
case []string:
stringSlice = v
case []interface{}:
stringSlice = make([]string, 0, len(v))
for _, item := range v {
if s, ok := item.(string); ok {
stringSlice = append(stringSlice, s)
}
}
default:
return nil
}
result := make([]string, 0, len(stringSlice))
for _, s := range stringSlice {
if trimmed := strings.TrimSpace(s); trimmed != "" {
result = append(result, trimmed)
}
}
return result
}