Merge pull request #50 from router-for-me/auth-dev
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package geminiwebapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
@@ -137,5 +138,41 @@ func ConvertOutputToGemini(output *ModelOutput, modelName string, promptText str
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal gemini response: %w", err)
|
||||
}
|
||||
return b, nil
|
||||
return ensureColonSpacing(b), nil
|
||||
}
|
||||
|
||||
// ensureColonSpacing inserts a single space after JSON key-value colons while
|
||||
// leaving string content untouched. This matches the relaxed formatting used by
|
||||
// Gemini responses and keeps downstream text-processing tools compatible with
|
||||
// the proxy output.
|
||||
func ensureColonSpacing(b []byte) []byte {
|
||||
if len(b) == 0 {
|
||||
return b
|
||||
}
|
||||
var out bytes.Buffer
|
||||
out.Grow(len(b) + len(b)/8)
|
||||
inString := false
|
||||
escaped := false
|
||||
for i := 0; i < len(b); i++ {
|
||||
ch := b[i]
|
||||
out.WriteByte(ch)
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
switch ch {
|
||||
case '\\':
|
||||
escaped = true
|
||||
case '"':
|
||||
inString = !inString
|
||||
case ':':
|
||||
if !inString && i+1 < len(b) {
|
||||
next := b[i+1]
|
||||
if next != ' ' && next != '\n' && next != '\r' && next != '\t' {
|
||||
out.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/luispater/CLIProxyAPI/v5/internal/config"
|
||||
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
|
||||
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
|
||||
"github.com/luispater/CLIProxyAPI/v5/internal/util"
|
||||
"github.com/luispater/CLIProxyAPI/v5/internal/watcher"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -76,7 +77,7 @@ func StartService(cfg *config.Config, configPath string) {
|
||||
if !info.IsDir() && strings.HasSuffix(info.Name(), ".json") {
|
||||
misc.LogCredentialSeparator()
|
||||
log.Debugf("Loading token from: %s", path)
|
||||
data, errReadFile := os.ReadFile(path)
|
||||
data, errReadFile := util.ReadAuthFilePreferSnapshot(path)
|
||||
if errReadFile != nil {
|
||||
return errReadFile
|
||||
}
|
||||
@@ -344,6 +345,7 @@ func StartService(cfg *config.Config, configPath string) {
|
||||
}
|
||||
activeClientsMu.RUnlock()
|
||||
for _, c := range snapshot {
|
||||
misc.LogCredentialSeparator()
|
||||
// Persist tokens/cookies then unregister/cleanup per client.
|
||||
_ = c.SaveTokenToFile()
|
||||
switch u := any(c).(type) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
|
||||
)
|
||||
@@ -93,6 +94,52 @@ func WriteCookieSnapshot(mainPath string, v any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadAuthFilePreferSnapshot returns the first non-empty auth payload preferring snapshots.
|
||||
func ReadAuthFilePreferSnapshot(path string) ([]byte, error) {
|
||||
return ReadAuthFileWithRetry(path, 1, 0)
|
||||
}
|
||||
|
||||
// ReadAuthFileWithRetry attempts to read an auth file multiple times and prefers cookie snapshots.
|
||||
func ReadAuthFileWithRetry(path string, attempts int, delay time.Duration) ([]byte, error) {
|
||||
if attempts < 1 {
|
||||
attempts = 1
|
||||
}
|
||||
read := func(target string) ([]byte, error) {
|
||||
var lastErr error
|
||||
for i := 0; i < attempts; i++ {
|
||||
data, err := os.ReadFile(target)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
lastErr = err
|
||||
if i < attempts-1 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
CookieSnapshotPath(path),
|
||||
path,
|
||||
}
|
||||
|
||||
for idx, candidate := range candidates {
|
||||
data, err := read(candidate)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if idx < len(candidates)-1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
// RemoveCookieSnapshots removes the snapshot file if it exists.
|
||||
func RemoveCookieSnapshots(mainPath string) {
|
||||
_ = RemoveFile(CookieSnapshotPath(mainPath))
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -324,7 +323,7 @@ func (w *Watcher) reloadClients() {
|
||||
// Rebuild auth file hash cache for current clients
|
||||
w.lastAuthHashes = make(map[string]string, len(newFileClients))
|
||||
for path := range newFileClients {
|
||||
if data, err := readAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay); err == nil && len(data) > 0 {
|
||||
if data, err := util.ReadAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay); err == nil && len(data) > 0 {
|
||||
sum := sha256.Sum256(data)
|
||||
w.lastAuthHashes[path] = hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -353,7 +352,7 @@ func (w *Watcher) reloadClients() {
|
||||
|
||||
// createClientFromFile creates a single client instance from a given token file path.
|
||||
func (w *Watcher) createClientFromFile(path string, cfg *config.Config) (interfaces.Client, error) {
|
||||
data, errReadFile := readAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay)
|
||||
data, errReadFile := util.ReadAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay)
|
||||
if errReadFile != nil {
|
||||
return nil, errReadFile
|
||||
}
|
||||
@@ -416,48 +415,9 @@ func (w *Watcher) clientsToSlice(clientMap map[string]interfaces.Client) []inter
|
||||
return s
|
||||
}
|
||||
|
||||
// readAuthFileWithRetry attempts to read the auth file multiple times to work around
|
||||
// short-lived locks on Windows while token files are being written.
|
||||
func readAuthFileWithRetry(path string, attempts int, delay time.Duration) ([]byte, error) {
|
||||
read := func(target string) ([]byte, error) {
|
||||
var lastErr error
|
||||
for i := 0; i < attempts; i++ {
|
||||
data, err := os.ReadFile(target)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
lastErr = err
|
||||
if i < attempts-1 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
util.CookieSnapshotPath(path),
|
||||
path,
|
||||
}
|
||||
|
||||
for idx, candidate := range candidates {
|
||||
data, err := read(candidate)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if idx < len(candidates)-1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
// addOrUpdateClient handles the addition or update of a single client.
|
||||
func (w *Watcher) addOrUpdateClient(path string) {
|
||||
data, errRead := readAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay)
|
||||
data, errRead := util.ReadAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay)
|
||||
if errRead != nil {
|
||||
log.Errorf("failed to read auth file %s: %v", filepath.Base(path), errRead)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user