Merge pull request #70 from router-for-me/log

Fix for the bug causing configuration to fail, and avoidance of invalid scanning of auth files.
This commit is contained in:
Luis Pater
2025-09-27 13:59:13 +08:00
committed by GitHub
9 changed files with 257 additions and 91 deletions

View File

@@ -8,7 +8,6 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cmd"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
@@ -123,22 +122,10 @@ func main() {
// Set the log level based on the configuration.
util.SetLogLevel(cfg)
// Expand the tilde (~) in the auth directory path to the user's home directory.
if strings.HasPrefix(cfg.AuthDir, "~") {
home, errUserHomeDir := os.UserHomeDir()
if errUserHomeDir != nil {
log.Fatalf("failed to get home directory: %v", errUserHomeDir)
}
// Reconstruct the path by replacing the tilde with the user's home directory.
remainder := strings.TrimPrefix(cfg.AuthDir, "~")
remainder = strings.TrimLeft(remainder, "/\\")
if remainder == "" {
cfg.AuthDir = home
} else {
// Normalize any slash style in the remainder so Windows paths keep nested directories.
normalized := strings.ReplaceAll(remainder, "\\", "/")
cfg.AuthDir = filepath.Join(home, filepath.FromSlash(normalized))
}
if resolvedAuthDir, errResolveAuthDir := util.ResolveAuthDir(cfg.AuthDir); errResolveAuthDir != nil {
log.Fatalf("failed to resolve auth directory: %v", errResolveAuthDir)
} else {
cfg.AuthDir = resolvedAuthDir
}
// Create login options to be used in authentication flows.

View File

@@ -1,25 +1,27 @@
package access
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
sdkConfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
log "github.com/sirupsen/logrus"
)
// ReconcileProviders builds the desired provider list by reusing existing providers when possible
// and creating or removing providers only when their configuration changed. It returns the final
// ordered provider slice along with the identifiers of providers that were added, updated, or
// removed compared to the previous configuration.
func ReconcileProviders(oldCfg, newCfg *config.Config, existing []access.Provider) (result []access.Provider, added, updated, removed []string, err error) {
func ReconcileProviders(oldCfg, newCfg *config.Config, existing []sdkaccess.Provider) (result []sdkaccess.Provider, added, updated, removed []string, err error) {
if newCfg == nil {
return nil, nil, nil, nil, nil
}
existingMap := make(map[string]access.Provider, len(existing))
existingMap := make(map[string]sdkaccess.Provider, len(existing))
for _, provider := range existing {
if provider == nil {
continue
@@ -30,7 +32,7 @@ func ReconcileProviders(oldCfg, newCfg *config.Config, existing []access.Provide
oldCfgMap := accessProviderMap(oldCfg)
newEntries := collectProviderEntries(newCfg)
result = make([]access.Provider, 0, len(newEntries))
result = make([]sdkaccess.Provider, 0, len(newEntries))
finalIDs := make(map[string]struct{}, len(newEntries))
isInlineProvider := func(id string) bool {
@@ -60,7 +62,7 @@ func ReconcileProviders(oldCfg, newCfg *config.Config, existing []access.Provide
}
}
provider, buildErr := access.BuildProvider(providerCfg, &newCfg.SDKConfig)
provider, buildErr := sdkaccess.BuildProvider(providerCfg, &newCfg.SDKConfig)
if buildErr != nil {
return nil, nil, nil, nil, buildErr
}
@@ -88,7 +90,7 @@ func ReconcileProviders(oldCfg, newCfg *config.Config, existing []access.Provide
if existingProvider, okExisting := existingMap[key]; okExisting {
result = append(result, existingProvider)
} else {
provider, buildErr := access.BuildProvider(providerCfg, &newCfg.SDKConfig)
provider, buildErr := sdkaccess.BuildProvider(providerCfg, &newCfg.SDKConfig)
if buildErr != nil {
return nil, nil, nil, nil, buildErr
}
@@ -100,7 +102,7 @@ func ReconcileProviders(oldCfg, newCfg *config.Config, existing []access.Provide
result = append(result, provider)
}
} else {
provider, buildErr := access.BuildProvider(providerCfg, &newCfg.SDKConfig)
provider, buildErr := sdkaccess.BuildProvider(providerCfg, &newCfg.SDKConfig)
if buildErr != nil {
return nil, nil, nil, nil, buildErr
}
@@ -112,7 +114,7 @@ func ReconcileProviders(oldCfg, newCfg *config.Config, existing []access.Provide
result = append(result, provider)
}
} else {
provider, buildErr := access.BuildProvider(providerCfg, &newCfg.SDKConfig)
provider, buildErr := sdkaccess.BuildProvider(providerCfg, &newCfg.SDKConfig)
if buildErr != nil {
return nil, nil, nil, nil, buildErr
}
@@ -146,6 +148,33 @@ func ReconcileProviders(oldCfg, newCfg *config.Config, existing []access.Provide
return result, added, updated, removed, nil
}
// ApplyAccessProviders reconciles the configured access providers against the
// currently registered providers and updates the manager. It logs a concise
// summary of the detected changes and returns whether any provider changed.
func ApplyAccessProviders(manager *sdkaccess.Manager, oldCfg, newCfg *config.Config) (bool, error) {
if manager == nil || newCfg == nil {
return false, nil
}
existing := manager.Providers()
providers, added, updated, removed, err := ReconcileProviders(oldCfg, newCfg, existing)
if err != nil {
log.Errorf("failed to reconcile request auth providers: %v", err)
return false, fmt.Errorf("reconciling access providers: %w", err)
}
manager.SetProviders(providers)
if len(added)+len(updated)+len(removed) > 0 {
log.Debugf("auth providers reconciled (added=%d updated=%d removed=%d)", len(added), len(updated), len(removed))
log.Debugf("auth provider changes details - added=%v updated=%v removed=%v", added, updated, removed)
return true, nil
}
// log.Debug("auth providers unchanged after config update")
return false, nil
}
func accessProviderMap(cfg *config.Config) map[string]*sdkConfig.AccessProvider {
result := make(map[string]*sdkConfig.AccessProvider)
if cfg == nil {

View File

@@ -552,19 +552,9 @@ func (s *Server) applyAccessConfig(oldCfg, newCfg *config.Config) {
if s == nil || s.accessManager == nil || newCfg == nil {
return
}
existing := s.accessManager.Providers()
providers, added, updated, removed, err := access.ReconcileProviders(oldCfg, newCfg, existing)
if err != nil {
log.Errorf("failed to reconcile request auth providers: %v", err)
if _, err := access.ApplyAccessProviders(s.accessManager, oldCfg, newCfg); err != nil {
return
}
s.accessManager.SetProviders(providers)
if len(added)+len(updated)+len(removed) > 0 {
log.Debugf("auth providers reconciled (added=%d updated=%d removed=%d)", len(added), len(updated), len(removed))
log.Debugf("auth provider changes details - added=%v updated=%v removed=%v", added, updated, removed)
} else {
log.Debug("auth providers unchanged after config update")
}
}
// UpdateClients updates the server's client list and configuration.

View File

@@ -15,7 +15,7 @@ import (
// Config represents the application's configuration, loaded from a YAML file.
type Config struct {
config.SDKConfig
config.SDKConfig `yaml:",inline"`
// Port is the network port on which the API server will listen.
Port int `yaml:"port" json:"-"`

View File

@@ -88,6 +88,11 @@ func (s *GeminiWebState) Label() string {
if s == nil {
return ""
}
if s.token != nil {
if lbl := strings.TrimSpace(s.token.Label); lbl != "" {
return lbl
}
}
if s.storagePath != "" {
base := strings.TrimSuffix(filepath.Base(s.storagePath), filepath.Ext(s.storagePath))
if base != "" {
@@ -169,8 +174,12 @@ func (s *GeminiWebState) Refresh(ctx context.Context) error {
s.client.Cookies["__Secure-1PSIDTS"] = newTS
}
s.tokenMu.Unlock()
// Detailed debug log: provider and account.
log.Debugf("gemini web account %s rotated 1PSIDTS: %s", s.accountID, MaskToken28(newTS))
// Detailed debug log: provider and account label.
label := strings.TrimSpace(s.Label())
if label == "" {
label = s.accountID
}
log.Debugf("gemini web account %s rotated 1PSIDTS: %s", label, MaskToken28(newTS))
}
s.lastRefresh = time.Now()
return nil

View File

@@ -200,7 +200,8 @@ func parseGeminiWebToken(auth *cliproxyauth.Auth) (*gemini.GeminiWebTokenStorage
if psid == "" || psidts == "" {
return nil, fmt.Errorf("gemini-web executor: incomplete cookie metadata")
}
return &gemini.GeminiWebTokenStorage{Secure1PSID: psid, Secure1PSIDTS: psidts}, nil
label := strings.TrimSpace(stringFromMetadata(auth.Metadata, "label"))
return &gemini.GeminiWebTokenStorage{Secure1PSID: psid, Secure1PSIDTS: psidts, Label: label}, nil
}
func stringFromMetadata(meta map[string]any, keys ...string) string {

View File

@@ -4,6 +4,7 @@
package util
import (
"fmt"
"io/fs"
"os"
"path/filepath"
@@ -30,23 +31,42 @@ func SetLogLevel(cfg *config.Config) {
}
}
// CountAuthFiles returns the number of JSON auth files located under the provided directory.
// The function resolves leading tildes to the user's home directory and performs a case-insensitive
// match on the ".json" suffix so that files saved with uppercase extensions are also counted.
func CountAuthFiles(authDir string) int {
// ResolveAuthDir normalizes the auth directory path for consistent reuse throughout the app.
// It expands a leading tilde (~) to the user's home directory and returns a cleaned path.
func ResolveAuthDir(authDir string) (string, error) {
if authDir == "" {
return 0
return "", nil
}
if strings.HasPrefix(authDir, "~") {
home, err := os.UserHomeDir()
if err != nil {
log.Debugf("countAuthFiles: failed to resolve home directory: %v", err)
return 0
return "", fmt.Errorf("resolve auth dir: %w", err)
}
authDir = filepath.Join(home, authDir[1:])
remainder := strings.TrimPrefix(authDir, "~")
remainder = strings.TrimLeft(remainder, "/\\")
if remainder == "" {
return filepath.Clean(home), nil
}
normalized := strings.ReplaceAll(remainder, "\\", "/")
return filepath.Clean(filepath.Join(home, filepath.FromSlash(normalized))), nil
}
return filepath.Clean(authDir), nil
}
// CountAuthFiles returns the number of JSON auth files located under the provided directory.
// The function resolves leading tildes to the user's home directory and performs a case-insensitive
// match on the ".json" suffix so that files saved with uppercase extensions are also counted.
func CountAuthFiles(authDir string) int {
dir, err := ResolveAuthDir(authDir)
if err != nil {
log.Debugf("countAuthFiles: failed to resolve auth directory: %v", err)
return 0
}
if dir == "" {
return 0
}
count := 0
walkErr := filepath.WalkDir(authDir, func(path string, d fs.DirEntry, err error) error {
walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
log.Debugf("countAuthFiles: error accessing %s: %v", path, err)
return nil

View File

@@ -14,6 +14,7 @@ import (
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"sync"
"time"
@@ -145,7 +146,7 @@ func (w *Watcher) Start(ctx context.Context) error {
go w.processEvents(ctx)
// Perform an initial full reload based on current config and auth dir
w.reloadClients()
w.reloadClients(true)
return nil
}
@@ -463,6 +464,12 @@ func (w *Watcher) reloadConfig() bool {
return false
}
if resolvedAuthDir, errResolveAuthDir := util.ResolveAuthDir(newConfig.AuthDir); errResolveAuthDir != nil {
log.Errorf("failed to resolve auth directory from config: %v", errResolveAuthDir)
} else {
newConfig.AuthDir = resolvedAuthDir
}
w.clientsMutex.Lock()
oldConfig := w.config
w.config = newConfig
@@ -530,16 +537,24 @@ func (w *Watcher) reloadConfig() bool {
if oldConfig.UsageStatisticsEnabled != newConfig.UsageStatisticsEnabled {
log.Debugf(" usage-statistics-enabled: %t -> %t", oldConfig.UsageStatisticsEnabled, newConfig.UsageStatisticsEnabled)
}
if changes := diffOpenAICompatibility(oldConfig.OpenAICompatibility, newConfig.OpenAICompatibility); len(changes) > 0 {
log.Debugf(" openai-compatibility:")
for _, change := range changes {
log.Debugf(" %s", change)
}
}
}
authDirChanged := oldConfig == nil || oldConfig.AuthDir != newConfig.AuthDir
log.Infof("config successfully reloaded, triggering client reload")
// Reload clients with new config
w.reloadClients()
w.reloadClients(authDirChanged)
return true
}
// reloadClients performs a full scan and reload of all clients.
func (w *Watcher) reloadClients() {
func (w *Watcher) reloadClients(rescanAuth bool) {
log.Debugf("starting full client reload process")
w.clientsMutex.RLock()
@@ -556,33 +571,48 @@ func (w *Watcher) reloadClients() {
// Create new API key clients based on the new config
glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount := BuildAPIKeyClients(cfg)
log.Debugf("created %d new API key clients", 0)
totalAPIKeyClients := glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
log.Debugf("loaded %d API key clients", totalAPIKeyClients)
// Load file-based clients
authFileCount := w.loadFileClients(cfg)
log.Debugf("loaded %d new file-based clients", 0)
var authFileCount int
if rescanAuth {
// Load file-based clients when explicitly requested (startup or authDir change)
authFileCount = w.loadFileClients(cfg)
log.Debugf("loaded %d new file-based clients", authFileCount)
} else {
// Preserve existing auth hashes and only report current known count to avoid redundant scans.
w.clientsMutex.RLock()
authFileCount = len(w.lastAuthHashes)
w.clientsMutex.RUnlock()
log.Debugf("skipping auth directory rescan; retaining %d existing auth files", authFileCount)
}
// no legacy file-based clients to unregister
// Update client maps
w.clientsMutex.Lock()
if rescanAuth {
w.clientsMutex.Lock()
// Rebuild auth file hash cache for current clients
w.lastAuthHashes = make(map[string]string)
// Recompute hashes for current auth files
_ = filepath.Walk(cfg.AuthDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return nil
// Rebuild auth file hash cache for current clients
w.lastAuthHashes = make(map[string]string)
if resolvedAuthDir, errResolveAuthDir := util.ResolveAuthDir(cfg.AuthDir); errResolveAuthDir != nil {
log.Errorf("failed to resolve auth directory for hash cache: %v", errResolveAuthDir)
} else if resolvedAuthDir != "" {
_ = filepath.Walk(resolvedAuthDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return nil
}
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") {
if data, errReadFile := os.ReadFile(path); errReadFile == nil && len(data) > 0 {
sum := sha256.Sum256(data)
w.lastAuthHashes[path] = hex.EncodeToString(sum[:])
}
}
return nil
})
}
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") {
if data, errReadFile := os.ReadFile(path); errReadFile == nil && len(data) > 0 {
sum := sha256.Sum256(data)
w.lastAuthHashes[path] = hex.EncodeToString(sum[:])
}
}
return nil
})
w.clientsMutex.Unlock()
w.clientsMutex.Unlock()
}
totalNewClients := authFileCount + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
@@ -855,14 +885,13 @@ func (w *Watcher) loadFileClients(cfg *config.Config) int {
authFileCount := 0
successfulAuthCount := 0
authDir := cfg.AuthDir
if strings.HasPrefix(authDir, "~") {
home, err := os.UserHomeDir()
if err != nil {
log.Errorf("failed to get home directory: %v", err)
return 0
}
authDir = filepath.Join(home, authDir[1:])
authDir, errResolveAuthDir := util.ResolveAuthDir(cfg.AuthDir)
if errResolveAuthDir != nil {
log.Errorf("failed to resolve auth directory: %v", errResolveAuthDir)
return 0
}
if authDir == "" {
return 0
}
errWalk := filepath.Walk(authDir, func(path string, info fs.FileInfo, err error) error {
@@ -912,3 +941,114 @@ func BuildAPIKeyClients(cfg *config.Config) (int, int, int, int) {
}
return glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount
}
func diffOpenAICompatibility(oldList, newList []config.OpenAICompatibility) []string {
changes := make([]string, 0)
oldMap := make(map[string]config.OpenAICompatibility, len(oldList))
oldLabels := make(map[string]string, len(oldList))
for idx, entry := range oldList {
key, label := openAICompatKey(entry, idx)
oldMap[key] = entry
oldLabels[key] = label
}
newMap := make(map[string]config.OpenAICompatibility, len(newList))
newLabels := make(map[string]string, len(newList))
for idx, entry := range newList {
key, label := openAICompatKey(entry, idx)
newMap[key] = entry
newLabels[key] = label
}
keySet := make(map[string]struct{}, len(oldMap)+len(newMap))
for key := range oldMap {
keySet[key] = struct{}{}
}
for key := range newMap {
keySet[key] = struct{}{}
}
orderedKeys := make([]string, 0, len(keySet))
for key := range keySet {
orderedKeys = append(orderedKeys, key)
}
sort.Strings(orderedKeys)
for _, key := range orderedKeys {
oldEntry, oldOk := oldMap[key]
newEntry, newOk := newMap[key]
label := oldLabels[key]
if label == "" {
label = newLabels[key]
}
switch {
case !oldOk:
changes = append(changes, fmt.Sprintf("provider added: %s (api-keys=%d, models=%d)", label, countNonEmptyStrings(newEntry.APIKeys), countOpenAIModels(newEntry.Models)))
case !newOk:
changes = append(changes, fmt.Sprintf("provider removed: %s (api-keys=%d, models=%d)", label, countNonEmptyStrings(oldEntry.APIKeys), countOpenAIModels(oldEntry.Models)))
default:
if detail := describeOpenAICompatibilityUpdate(oldEntry, newEntry); detail != "" {
changes = append(changes, fmt.Sprintf("provider updated: %s %s", label, detail))
}
}
}
return changes
}
func describeOpenAICompatibilityUpdate(oldEntry, newEntry config.OpenAICompatibility) string {
oldKeyCount := countNonEmptyStrings(oldEntry.APIKeys)
newKeyCount := countNonEmptyStrings(newEntry.APIKeys)
oldModelCount := countOpenAIModels(oldEntry.Models)
newModelCount := countOpenAIModels(newEntry.Models)
details := make([]string, 0, 2)
if oldKeyCount != newKeyCount {
details = append(details, fmt.Sprintf("api-keys %d -> %d", oldKeyCount, newKeyCount))
}
if oldModelCount != newModelCount {
details = append(details, fmt.Sprintf("models %d -> %d", oldModelCount, newModelCount))
}
if len(details) == 0 {
return ""
}
return "(" + strings.Join(details, ", ") + ")"
}
func countNonEmptyStrings(values []string) int {
count := 0
for _, value := range values {
if strings.TrimSpace(value) != "" {
count++
}
}
return count
}
func countOpenAIModels(models []config.OpenAICompatibilityModel) int {
count := 0
for _, model := range models {
name := strings.TrimSpace(model.Name)
alias := strings.TrimSpace(model.Alias)
if name == "" && alias == "" {
continue
}
count++
}
return count
}
func openAICompatKey(entry config.OpenAICompatibility, index int) (string, string) {
name := strings.TrimSpace(entry.Name)
if name != "" {
return "name:" + name, name
}
base := strings.TrimSpace(entry.BaseURL)
if base != "" {
return "base:" + base, base
}
for _, model := range entry.Models {
alias := strings.TrimSpace(model.Alias)
if alias == "" {
alias = strings.TrimSpace(model.Name)
}
if alias != "" {
return "alias:" + alias, alias
}
}
return fmt.Sprintf("index:%d", index), fmt.Sprintf("entry-%d", index+1)
}

View File

@@ -115,19 +115,9 @@ func (s *Service) refreshAccessProviders(cfg *config.Config) {
oldCfg := s.cfg
s.cfgMu.RUnlock()
existing := s.accessManager.Providers()
providers, added, updated, removed, err := access.ReconcileProviders(oldCfg, cfg, existing)
if err != nil {
log.Errorf("failed to reconcile request auth providers: %v", err)
if _, err := access.ApplyAccessProviders(s.accessManager, oldCfg, cfg); err != nil {
return
}
s.accessManager.SetProviders(providers)
if len(added)+len(updated)+len(removed) > 0 {
log.Debugf("auth providers reconciled (added=%d updated=%d removed=%d)", len(added), len(updated), len(removed))
log.Debugf("auth provider changes details - added=%v updated=%v removed=%v", added, updated, removed)
} else {
log.Debug("auth providers unchanged after config reload")
}
}
func (s *Service) ensureAuthUpdateQueue(ctx context.Context) {