Compare commits

..

5 Commits

Author SHA1 Message Date
Luis Pater
02e28bbbe9 feat(watcher): add support for proxy_url in auth metadata
Some checks failed
docker-image / docker (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled
- Extracted and assigned `proxy_url` from metadata to `Auth.ProxyURL`.
2025-10-10 10:20:33 +08:00
Luis Pater
b9c7b9eea5 docs: add Homebrew installation instructions to README and README_CN
- Updated both English and Chinese documentation with steps to install and start `cliproxyapi` via Homebrew.
2025-10-10 04:38:01 +08:00
Luis Pater
57195fa0f5 feat(managementasset): enforce 3-hour rate limit on management asset update checks
Some checks failed
docker-image / docker (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled
- Introduced synchronization with `sync.Mutex` to ensure thread safety.
- Added logic to skip update checks if the last check was performed within the 3-hour interval.
2025-10-10 04:23:58 +08:00
Luis Pater
11f090c223 Fixed #102
Some checks failed
docker-image / docker (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled
feat(translator): add support for removing `strict` in Gemini request transformation

- Updated API and CLI translators to remove the `strict` path during request transformation, in addition to existing predefined JSON paths.
2025-10-10 02:59:21 +08:00
Luis Pater
829dd06b42 feat(cliproxy/auth): restructure auth candidate selection and ensure synchronization
- Refactored candidate selection logic in `auth/manager.go`.
- Ensured proper synchronization around `mu.RUnlock` to prevent racing conditions.
2025-10-10 02:35:15 +08:00
7 changed files with 53 additions and 8 deletions

View File

@@ -62,6 +62,12 @@ Chinese providers have now been added: [Qwen Code](https://github.com/QwenLM/qwe
go build -o cli-proxy-api.exe ./cmd/server
```
### Installation via Homebrew
```bash
brew install cliproxyapi
brew services start cliproxyapi
```
## Usage

View File

@@ -76,6 +76,13 @@
go build -o cli-proxy-api ./cmd/server
```
### 通过 Homebrew 安装
```bash
brew install cliproxyapi
brew services start cliproxyapi
```
## 使用方法
### 图形客户端与官方 WebUI

View File

@@ -12,6 +12,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
@@ -23,11 +24,17 @@ const (
managementReleaseURL = "https://api.github.com/repos/router-for-me/Cli-Proxy-API-Management-Center/releases/latest"
managementAssetName = "management.html"
httpUserAgent = "CLIProxyAPI-management-updater"
updateCheckInterval = 3 * time.Hour
)
// ManagementFileName exposes the control panel asset filename.
const ManagementFileName = managementAssetName
var (
lastUpdateCheckMu sync.Mutex
lastUpdateCheckTime time.Time
)
func newHTTPClient(proxyURL string) *http.Client {
client := &http.Client{Timeout: 15 * time.Second}
@@ -68,6 +75,7 @@ func FilePath(configFilePath string) string {
// EnsureLatestManagementHTML checks the latest management.html asset and updates the local copy when needed.
// The function is designed to run in a background goroutine and will never panic.
// It enforces a 3-hour rate limit to avoid frequent checks on config/auth file changes.
func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL string) {
if ctx == nil {
ctx = context.Background()
@@ -79,6 +87,18 @@ func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL
return
}
// Rate limiting: check only once every 3 hours
lastUpdateCheckMu.Lock()
now := time.Now()
timeSinceLastCheck := now.Sub(lastUpdateCheckTime)
if timeSinceLastCheck < updateCheckInterval {
lastUpdateCheckMu.Unlock()
log.Debugf("management asset update check skipped: last check was %v ago (interval: %v)", timeSinceLastCheck.Round(time.Second), updateCheckInterval)
return
}
lastUpdateCheckTime = now
lastUpdateCheckMu.Unlock()
if err := os.MkdirAll(staticDir, 0o755); err != nil {
log.WithError(err).Warn("failed to prepare static directory for management asset")
return

View File

@@ -31,6 +31,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
util.Walk(root, "", "additionalProperties", &pathsToDelete)
util.Walk(root, "", "$schema", &pathsToDelete)
util.Walk(root, "", "ref", &pathsToDelete)
util.Walk(root, "", "strict", &pathsToDelete)
var err error
for _, p := range pathsToDelete {

View File

@@ -31,6 +31,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
util.Walk(root, "", "additionalProperties", &pathsToDelete)
util.Walk(root, "", "$schema", &pathsToDelete)
util.Walk(root, "", "ref", &pathsToDelete)
util.Walk(root, "", "strict", &pathsToDelete)
var err error
for _, p := range pathsToDelete {

View File

@@ -951,6 +951,11 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
id = rel
}
proxyURL := ""
if p, ok := metadata["proxy_url"].(string); ok {
proxyURL = p
}
a := &coreauth.Auth{
ID: id,
Provider: provider,
@@ -960,6 +965,7 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth {
"source": full,
"path": full,
},
ProxyURL: proxyURL,
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,

View File

@@ -787,27 +787,31 @@ func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cli
return nil, nil, &Error{Code: "executor_not_found", Message: "executor not registered"}
}
candidates := make([]*Auth, 0, len(m.auths))
for _, auth := range m.auths {
if auth.Provider != provider || auth.Disabled {
for _, candidate := range m.auths {
if candidate.Provider != provider || candidate.Disabled {
continue
}
if _, used := tried[auth.ID]; used {
if _, used := tried[candidate.ID]; used {
continue
}
candidates = append(candidates, auth.Clone())
candidates = append(candidates, candidate)
}
m.mu.RUnlock()
if len(candidates) == 0 {
m.mu.RUnlock()
return nil, nil, &Error{Code: "auth_not_found", Message: "no auth available"}
}
auth, errPick := m.selector.Pick(ctx, provider, model, opts, candidates)
selected, errPick := m.selector.Pick(ctx, provider, model, opts, candidates)
if errPick != nil {
m.mu.RUnlock()
return nil, nil, errPick
}
if auth == nil {
if selected == nil {
m.mu.RUnlock()
return nil, nil, &Error{Code: "auth_not_found", Message: "selector returned no auth"}
}
return auth, executor, nil
authCopy := selected.Clone()
m.mu.RUnlock()
return authCopy, executor, nil
}
func (m *Manager) persist(ctx context.Context, auth *Auth) error {