mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
Merge pull request #62 from router-for-me/dev
feat(auth, docs): add SDK guides and local password support for manag…
This commit is contained in:
@@ -64,7 +64,7 @@ func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
|
|||||||
func init() {
|
func init() {
|
||||||
logDir := "logs"
|
logDir := "logs"
|
||||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to create log directory: %v\n", err)
|
_, _ = fmt.Fprintf(os.Stderr, "failed to create log directory: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +122,7 @@ func main() {
|
|||||||
var noBrowser bool
|
var noBrowser bool
|
||||||
var projectID string
|
var projectID string
|
||||||
var configPath string
|
var configPath string
|
||||||
|
var password string
|
||||||
|
|
||||||
// Define command-line flags for different operation modes.
|
// Define command-line flags for different operation modes.
|
||||||
flag.BoolVar(&login, "login", false, "Login Google Account")
|
flag.BoolVar(&login, "login", false, "Login Google Account")
|
||||||
@@ -132,6 +133,34 @@ func main() {
|
|||||||
flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
|
flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
|
||||||
flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
|
flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
|
||||||
flag.StringVar(&configPath, "config", "", "Configure File Path")
|
flag.StringVar(&configPath, "config", "", "Configure File Path")
|
||||||
|
flag.StringVar(&password, "password", "", "")
|
||||||
|
|
||||||
|
flag.CommandLine.Usage = func() {
|
||||||
|
out := flag.CommandLine.Output()
|
||||||
|
_, _ = fmt.Fprintf(out, "Usage of %s\n", os.Args[0])
|
||||||
|
flag.CommandLine.VisitAll(func(f *flag.Flag) {
|
||||||
|
if f.Name == "password" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf(" -%s", f.Name)
|
||||||
|
name, usage := flag.UnquoteUsage(f)
|
||||||
|
if name != "" {
|
||||||
|
s += " " + name
|
||||||
|
}
|
||||||
|
if len(s) <= 4 {
|
||||||
|
s += " "
|
||||||
|
} else {
|
||||||
|
s += "\n "
|
||||||
|
}
|
||||||
|
if usage != "" {
|
||||||
|
s += usage
|
||||||
|
}
|
||||||
|
if f.DefValue != "" && f.DefValue != "false" && f.DefValue != "0" {
|
||||||
|
s += fmt.Sprintf(" (default %s)", f.DefValue)
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprint(out, s+"\n")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Parse the command-line flags.
|
// Parse the command-line flags.
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -206,6 +235,6 @@ func main() {
|
|||||||
cmd.DoGeminiWebAuth(cfg)
|
cmd.DoGeminiWebAuth(cfg)
|
||||||
} else {
|
} else {
|
||||||
// Start the main proxy service
|
// Start the main proxy service
|
||||||
cmd.StartService(cfg, configFilePath)
|
cmd.StartService(cfg, configFilePath, password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
176
docs/sdk-access.md
Normal file
176
docs/sdk-access.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# @sdk/access SDK Reference
|
||||||
|
|
||||||
|
The `github.com/router-for-me/CLIProxyAPI/v6/sdk/access` package centralizes inbound request authentication for the proxy. It offers a lightweight manager that chains credential providers, so servers can reuse the same access control logic inside or outside the CLI runtime.
|
||||||
|
|
||||||
|
## Importing
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the module with `go get github.com/router-for-me/CLIProxyAPI/v6/sdk/access`.
|
||||||
|
|
||||||
|
## Manager Lifecycle
|
||||||
|
|
||||||
|
```go
|
||||||
|
manager := sdkaccess.NewManager()
|
||||||
|
providers, err := sdkaccess.BuildProviders(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
manager.SetProviders(providers)
|
||||||
|
```
|
||||||
|
|
||||||
|
* `NewManager` constructs an empty manager.
|
||||||
|
* `SetProviders` replaces the provider slice using a defensive copy.
|
||||||
|
* `Providers` retrieves a snapshot that can be iterated safely from other goroutines.
|
||||||
|
* `BuildProviders` translates `config.Config` access declarations into runnable providers. When the config omits explicit providers but defines inline API keys, the helper auto-installs the built-in `config-api-key` provider.
|
||||||
|
|
||||||
|
## Authenticating Requests
|
||||||
|
|
||||||
|
```go
|
||||||
|
result, err := manager.Authenticate(ctx, req)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
// Authentication succeeded; result describes the provider and principal.
|
||||||
|
case errors.Is(err, sdkaccess.ErrNoCredentials):
|
||||||
|
// No recognizable credentials were supplied.
|
||||||
|
case errors.Is(err, sdkaccess.ErrInvalidCredential):
|
||||||
|
// Supplied credentials were present but rejected.
|
||||||
|
default:
|
||||||
|
// Transport-level failure was returned by a provider.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Manager.Authenticate` walks the configured providers in order. It returns on the first success, skips providers that surface `ErrNotHandled`, and tracks whether any provider reported `ErrNoCredentials` or `ErrInvalidCredential` for downstream error reporting.
|
||||||
|
|
||||||
|
If the manager itself is `nil` or no providers are registered, the call returns `nil, nil`, allowing callers to treat access control as disabled without branching on errors.
|
||||||
|
|
||||||
|
Each `Result` includes the provider identifier, the resolved principal, and optional metadata (for example, which header carried the credential).
|
||||||
|
|
||||||
|
## Configuration Layout
|
||||||
|
|
||||||
|
The manager expects access providers under the `auth.providers` key inside `config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
providers:
|
||||||
|
- name: inline-api
|
||||||
|
type: config-api-key
|
||||||
|
api-keys:
|
||||||
|
- sk-test-123
|
||||||
|
- sk-prod-456
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields map directly to `config.AccessProvider`: `name` labels the provider, `type` selects the registered factory, `sdk` can name an external module, `api-keys` seeds inline credentials, and `config` passes provider-specific options.
|
||||||
|
|
||||||
|
### Loading providers from external SDK modules
|
||||||
|
|
||||||
|
To consume a provider shipped in another Go module, point the `sdk` field at the module path and import it for its registration side effect:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
providers:
|
||||||
|
- name: partner-auth
|
||||||
|
type: partner-token
|
||||||
|
sdk: github.com/acme/xplatform/sdk/access/providers/partner
|
||||||
|
config:
|
||||||
|
region: us-west-2
|
||||||
|
audience: cli-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
_ "github.com/acme/xplatform/sdk/access/providers/partner" // registers partner-token
|
||||||
|
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The blank identifier import ensures `init` runs so `sdkaccess.RegisterProvider` executes before `BuildProviders` is called.
|
||||||
|
|
||||||
|
## Built-in Providers
|
||||||
|
|
||||||
|
The SDK ships with one provider out of the box:
|
||||||
|
|
||||||
|
- `config-api-key`: Validates API keys declared inline or under top-level `api-keys`. It accepts the key from `Authorization: Bearer`, `X-Goog-Api-Key`, `X-Api-Key`, or the `?key=` query string and reports `ErrInvalidCredential` when no match is found.
|
||||||
|
|
||||||
|
Additional providers can be delivered by third-party packages. When a provider package is imported, it registers itself with `sdkaccess.RegisterProvider`.
|
||||||
|
|
||||||
|
### Metadata and auditing
|
||||||
|
|
||||||
|
`Result.Metadata` carries provider-specific context. The built-in `config-api-key` provider, for example, stores the credential source (`authorization`, `x-goog-api-key`, `x-api-key`, or `query-key`). Populate this map in custom providers to enrich logs and downstream auditing.
|
||||||
|
|
||||||
|
## Writing Custom Providers
|
||||||
|
|
||||||
|
```go
|
||||||
|
type customProvider struct{}
|
||||||
|
|
||||||
|
func (p *customProvider) Identifier() string { return "my-provider" }
|
||||||
|
|
||||||
|
func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sdkaccess.Result, error) {
|
||||||
|
token := r.Header.Get("X-Custom")
|
||||||
|
if token == "" {
|
||||||
|
return nil, sdkaccess.ErrNoCredentials
|
||||||
|
}
|
||||||
|
if token != "expected" {
|
||||||
|
return nil, sdkaccess.ErrInvalidCredential
|
||||||
|
}
|
||||||
|
return &sdkaccess.Result{
|
||||||
|
Provider: p.Identifier(),
|
||||||
|
Principal: "service-user",
|
||||||
|
Metadata: map[string]string{"source": "x-custom"},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sdkaccess.RegisterProvider("custom", func(cfg *config.AccessProvider, root *config.Config) (sdkaccess.Provider, error) {
|
||||||
|
return &customProvider{}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A provider must implement `Identifier()` and `Authenticate()`. To expose it to configuration, call `RegisterProvider` inside `init`. Provider factories receive the specific `AccessProvider` block plus the full root configuration for contextual needs.
|
||||||
|
|
||||||
|
## Error Semantics
|
||||||
|
|
||||||
|
- `ErrNoCredentials`: no credentials were present or recognized by any provider.
|
||||||
|
- `ErrInvalidCredential`: at least one provider processed the credentials but rejected them.
|
||||||
|
- `ErrNotHandled`: instructs the manager to fall through to the next provider without affecting aggregate error reporting.
|
||||||
|
|
||||||
|
Return custom errors to surface transport failures; they propagate immediately to the caller instead of being masked.
|
||||||
|
|
||||||
|
## Integration with cliproxy Service
|
||||||
|
|
||||||
|
`sdk/cliproxy` wires `@sdk/access` automatically when you build a CLI service via `cliproxy.NewBuilder`. Supplying a preconfigured manager allows you to extend or override the default providers:
|
||||||
|
|
||||||
|
```go
|
||||||
|
coreCfg, _ := config.LoadConfig("config.yaml")
|
||||||
|
providers, _ := sdkaccess.BuildProviders(coreCfg)
|
||||||
|
manager := sdkaccess.NewManager()
|
||||||
|
manager.SetProviders(providers)
|
||||||
|
|
||||||
|
svc, _ := cliproxy.NewBuilder().
|
||||||
|
WithConfig(coreCfg).
|
||||||
|
WithAccessManager(manager).
|
||||||
|
Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
The service reuses the manager for every inbound request, ensuring consistent authentication across embedded deployments and the canonical CLI binary.
|
||||||
|
|
||||||
|
### Hot reloading providers
|
||||||
|
|
||||||
|
When configuration changes, rebuild providers and swap them into the manager:
|
||||||
|
|
||||||
|
```go
|
||||||
|
providers, err := sdkaccess.BuildProviders(newCfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("reload auth providers failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accessManager.SetProviders(providers)
|
||||||
|
```
|
||||||
|
|
||||||
|
This mirrors the behaviour in `cliproxy.Service.refreshAccessProviders` and `api.Server.applyAccessConfig`, enabling runtime updates without restarting the process.
|
||||||
176
docs/sdk-access_CN.md
Normal file
176
docs/sdk-access_CN.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# @sdk/access 开发指引
|
||||||
|
|
||||||
|
`github.com/router-for-me/CLIProxyAPI/v6/sdk/access` 包负责代理的入站访问认证。它提供一个轻量的管理器,用于按顺序链接多种凭证校验实现,让服务器在 CLI 运行时内外都能复用相同的访问控制逻辑。
|
||||||
|
|
||||||
|
## 引用方式
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
通过 `go get github.com/router-for-me/CLIProxyAPI/v6/sdk/access` 添加依赖。
|
||||||
|
|
||||||
|
## 管理器生命周期
|
||||||
|
|
||||||
|
```go
|
||||||
|
manager := sdkaccess.NewManager()
|
||||||
|
providers, err := sdkaccess.BuildProviders(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
manager.SetProviders(providers)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `NewManager` 创建空管理器。
|
||||||
|
- `SetProviders` 替换提供者切片并做防御性拷贝。
|
||||||
|
- `Providers` 返回适合并发读取的快照。
|
||||||
|
- `BuildProviders` 将 `config.Config` 中的访问配置转换成可运行的提供者。当配置没有显式声明但包含顶层 `api-keys` 时,会自动挂载内建的 `config-api-key` 提供者。
|
||||||
|
|
||||||
|
## 认证请求
|
||||||
|
|
||||||
|
```go
|
||||||
|
result, err := manager.Authenticate(ctx, req)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
// Authentication succeeded; result carries provider and principal.
|
||||||
|
case errors.Is(err, sdkaccess.ErrNoCredentials):
|
||||||
|
// No recognizable credentials were supplied.
|
||||||
|
case errors.Is(err, sdkaccess.ErrInvalidCredential):
|
||||||
|
// Credentials were present but rejected.
|
||||||
|
default:
|
||||||
|
// Provider surfaced a transport-level failure.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Manager.Authenticate` 按配置顺序遍历提供者。遇到成功立即返回,`ErrNotHandled` 会继续尝试下一个;若发现 `ErrNoCredentials` 或 `ErrInvalidCredential`,会在遍历结束后汇总给调用方。
|
||||||
|
|
||||||
|
若管理器本身为 `nil` 或尚未注册提供者,调用会返回 `nil, nil`,让调用方无需针对错误做额外分支即可关闭访问控制。
|
||||||
|
|
||||||
|
`Result` 提供认证提供者标识、解析出的主体以及可选元数据(例如凭证来源)。
|
||||||
|
|
||||||
|
## 配置结构
|
||||||
|
|
||||||
|
在 `config.yaml` 的 `auth.providers` 下定义访问提供者:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
providers:
|
||||||
|
- name: inline-api
|
||||||
|
type: config-api-key
|
||||||
|
api-keys:
|
||||||
|
- sk-test-123
|
||||||
|
- sk-prod-456
|
||||||
|
```
|
||||||
|
|
||||||
|
条目映射到 `config.AccessProvider`:`name` 指定实例名,`type` 选择注册的工厂,`sdk` 可引用第三方模块,`api-keys` 提供内联凭证,`config` 用于传递特定选项。
|
||||||
|
|
||||||
|
### 引入外部 SDK 提供者
|
||||||
|
|
||||||
|
若要消费其它 Go 模块输出的访问提供者,可在配置里填写 `sdk` 字段并在代码中引入该包,利用其 `init` 注册过程:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
providers:
|
||||||
|
- name: partner-auth
|
||||||
|
type: partner-token
|
||||||
|
sdk: github.com/acme/xplatform/sdk/access/providers/partner
|
||||||
|
config:
|
||||||
|
region: us-west-2
|
||||||
|
audience: cli-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
_ "github.com/acme/xplatform/sdk/access/providers/partner" // registers partner-token
|
||||||
|
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
通过空白标识符导入即可确保 `init` 调用,先于 `BuildProviders` 完成 `sdkaccess.RegisterProvider`。
|
||||||
|
|
||||||
|
## 内建提供者
|
||||||
|
|
||||||
|
当前 SDK 默认内置:
|
||||||
|
|
||||||
|
- `config-api-key`:校验配置中的 API Key。它从 `Authorization: Bearer`、`X-Goog-Api-Key`、`X-Api-Key` 以及查询参数 `?key=` 提取凭证,不匹配时抛出 `ErrInvalidCredential`。
|
||||||
|
|
||||||
|
导入第三方包即可通过 `sdkaccess.RegisterProvider` 注册更多类型。
|
||||||
|
|
||||||
|
### 元数据与审计
|
||||||
|
|
||||||
|
`Result.Metadata` 用于携带提供者特定的上下文信息。内建的 `config-api-key` 会记录凭证来源(`authorization`、`x-goog-api-key`、`x-api-key` 或 `query-key`)。自定义提供者同样可以填充该 Map,以便丰富日志与审计场景。
|
||||||
|
|
||||||
|
## 编写自定义提供者
|
||||||
|
|
||||||
|
```go
|
||||||
|
type customProvider struct{}
|
||||||
|
|
||||||
|
func (p *customProvider) Identifier() string { return "my-provider" }
|
||||||
|
|
||||||
|
func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sdkaccess.Result, error) {
|
||||||
|
token := r.Header.Get("X-Custom")
|
||||||
|
if token == "" {
|
||||||
|
return nil, sdkaccess.ErrNoCredentials
|
||||||
|
}
|
||||||
|
if token != "expected" {
|
||||||
|
return nil, sdkaccess.ErrInvalidCredential
|
||||||
|
}
|
||||||
|
return &sdkaccess.Result{
|
||||||
|
Provider: p.Identifier(),
|
||||||
|
Principal: "service-user",
|
||||||
|
Metadata: map[string]string{"source": "x-custom"},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sdkaccess.RegisterProvider("custom", func(cfg *config.AccessProvider, root *config.Config) (sdkaccess.Provider, error) {
|
||||||
|
return &customProvider{}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
自定义提供者需要实现 `Identifier()` 与 `Authenticate()`。在 `init` 中调用 `RegisterProvider` 暴露给配置层,工厂函数既能读取当前条目,也能访问完整根配置。
|
||||||
|
|
||||||
|
## 错误语义
|
||||||
|
|
||||||
|
- `ErrNoCredentials`:任何提供者都未识别到凭证。
|
||||||
|
- `ErrInvalidCredential`:至少一个提供者处理了凭证但判定无效。
|
||||||
|
- `ErrNotHandled`:告诉管理器跳到下一个提供者,不影响最终错误统计。
|
||||||
|
|
||||||
|
自定义错误(例如网络异常)会马上冒泡返回。
|
||||||
|
|
||||||
|
## 与 cliproxy 集成
|
||||||
|
|
||||||
|
使用 `sdk/cliproxy` 构建服务时会自动接入 `@sdk/access`。如果需要扩展内置行为,可传入自定义管理器:
|
||||||
|
|
||||||
|
```go
|
||||||
|
coreCfg, _ := config.LoadConfig("config.yaml")
|
||||||
|
providers, _ := sdkaccess.BuildProviders(coreCfg)
|
||||||
|
manager := sdkaccess.NewManager()
|
||||||
|
manager.SetProviders(providers)
|
||||||
|
|
||||||
|
svc, _ := cliproxy.NewBuilder().
|
||||||
|
WithConfig(coreCfg).
|
||||||
|
WithAccessManager(manager).
|
||||||
|
Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
服务会复用该管理器处理每一个入站请求,实现与 CLI 二进制一致的访问控制体验。
|
||||||
|
|
||||||
|
### 动态热更新提供者
|
||||||
|
|
||||||
|
当配置发生变化时,可以重新构建提供者并替换当前列表:
|
||||||
|
|
||||||
|
```go
|
||||||
|
providers, err := sdkaccess.BuildProviders(newCfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("reload auth providers failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accessManager.SetProviders(providers)
|
||||||
|
```
|
||||||
|
|
||||||
|
这一流程与 `cliproxy.Service.refreshAccessProviders` 和 `api.Server.applyAccessConfig` 保持一致,避免为更新访问策略而重启进程。
|
||||||
138
docs/sdk-advanced.md
Normal file
138
docs/sdk-advanced.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# SDK Advanced: Executors & Translators
|
||||||
|
|
||||||
|
This guide explains how to extend the embedded proxy with custom providers and schemas using the SDK. You will:
|
||||||
|
- Implement a provider executor that talks to your upstream API
|
||||||
|
- Register request/response translators for schema conversion
|
||||||
|
- Register models so they appear in `/v1/models`
|
||||||
|
|
||||||
|
The examples use Go 1.24+ and the v6 module path.
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
- Provider executor: a runtime component implementing `auth.ProviderExecutor` that performs outbound calls for a given provider key (e.g., `gemini`, `claude`, `codex`). Executors can also implement `RequestPreparer` to inject credentials on raw HTTP requests.
|
||||||
|
- Translator registry: schema conversion functions routed by `sdk/translator`. The built‑in handlers translate between OpenAI/Gemini/Claude/Codex formats; you can register new ones.
|
||||||
|
- Model registry: publishes the list of available models per client/provider to power `/v1/models` and routing hints.
|
||||||
|
|
||||||
|
## 1) Implement a Provider Executor
|
||||||
|
|
||||||
|
Create a type that satisfies `auth.ProviderExecutor`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package myprov
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Executor struct{}
|
||||||
|
|
||||||
|
func (Executor) Identifier() string { return "myprov" }
|
||||||
|
|
||||||
|
// Optional: mutate outbound HTTP requests with credentials
|
||||||
|
func (Executor) PrepareRequest(req *http.Request, a *coreauth.Auth) error {
|
||||||
|
// Example: req.Header.Set("Authorization", "Bearer "+a.APIKey)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Executor) Execute(ctx context.Context, a *coreauth.Auth, req clipexec.Request, opts clipexec.Options) (clipexec.Response, error) {
|
||||||
|
// Build HTTP request based on req.Payload (already translated into provider format)
|
||||||
|
// Use per‑auth transport if provided: transport := a.RoundTripper // via RoundTripperProvider
|
||||||
|
// Perform call and return provider JSON payload
|
||||||
|
return clipexec.Response{Payload: []byte(`{"ok":true}`)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Executor) ExecuteStream(ctx context.Context, a *coreauth.Auth, req clipexec.Request, opts clipexec.Options) (<-chan clipexec.StreamChunk, error) {
|
||||||
|
ch := make(chan clipexec.StreamChunk, 1)
|
||||||
|
go func() { defer close(ch); ch <- clipexec.StreamChunk{Payload: []byte("data: {\"done\":true}\n\n")} }()
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Executor) Refresh(ctx context.Context, a *coreauth.Auth) (*coreauth.Auth, error) {
|
||||||
|
// Optionally refresh tokens and return updated auth
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Register the executor with the core manager before starting the service:
|
||||||
|
|
||||||
|
```go
|
||||||
|
core := coreauth.NewManager(coreauth.NewFileStore(cfg.AuthDir), nil, nil)
|
||||||
|
core.RegisterExecutor(myprov.Executor{})
|
||||||
|
svc, _ := cliproxy.NewBuilder().WithConfig(cfg).WithConfigPath(cfgPath).WithCoreAuthManager(core).Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
If your auth entries use provider `"myprov"`, the manager routes requests to your executor.
|
||||||
|
|
||||||
|
## 2) Register Translators
|
||||||
|
|
||||||
|
The handlers accept OpenAI/Gemini/Claude/Codex inputs. To support a new provider format, register translation functions in `sdk/translator`’s default registry.
|
||||||
|
|
||||||
|
Direction matters:
|
||||||
|
- Request: register from inbound schema to provider schema
|
||||||
|
- Response: register from provider schema back to inbound schema
|
||||||
|
|
||||||
|
Example: Convert OpenAI Chat → MyProv Chat and back.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package myprov
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
sdktr "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FOpenAI = sdktr.Format("openai.chat")
|
||||||
|
FMyProv = sdktr.Format("myprov.chat")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sdktr.Register(FOpenAI, FMyProv,
|
||||||
|
// Request transform (model, rawJSON, stream)
|
||||||
|
func(model string, raw []byte, stream bool) []byte { return convertOpenAIToMyProv(model, raw, stream) },
|
||||||
|
// Response transform (stream & non‑stream)
|
||||||
|
sdktr.ResponseTransform{
|
||||||
|
Stream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) []string {
|
||||||
|
return convertStreamMyProvToOpenAI(model, originalReq, translatedReq, raw)
|
||||||
|
},
|
||||||
|
NonStream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) string {
|
||||||
|
return convertMyProvToOpenAI(model, originalReq, translatedReq, raw)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When the OpenAI handler receives a request that should route to `myprov`, the pipeline uses the registered transforms automatically.
|
||||||
|
|
||||||
|
## 3) Register Models
|
||||||
|
|
||||||
|
Expose models under `/v1/models` by registering them in the global model registry using the auth ID (client ID) and provider name.
|
||||||
|
|
||||||
|
```go
|
||||||
|
models := []*cliproxy.ModelInfo{
|
||||||
|
{ ID: "myprov-pro-1", Object: "model", Type: "myprov", DisplayName: "MyProv Pro 1" },
|
||||||
|
}
|
||||||
|
cliproxy.GlobalModelRegistry().RegisterClient(authID, "myprov", models)
|
||||||
|
```
|
||||||
|
|
||||||
|
The embedded server calls this automatically for built‑in providers; for custom providers, register during startup (e.g., after loading auths) or upon auth registration hooks.
|
||||||
|
|
||||||
|
## Credentials & Transports
|
||||||
|
|
||||||
|
- Use `Manager.SetRoundTripperProvider` to inject per‑auth `*http.Transport` (e.g., proxy):
|
||||||
|
```go
|
||||||
|
core.SetRoundTripperProvider(myProvider) // returns transport per auth
|
||||||
|
```
|
||||||
|
- For raw HTTP flows, implement `PrepareRequest` and/or call `Manager.InjectCredentials(req, authID)` to set headers.
|
||||||
|
|
||||||
|
## Testing Tips
|
||||||
|
|
||||||
|
- Enable request logging: Management API GET/PUT `/v0/management/request-log`
|
||||||
|
- Toggle debug logs: Management API GET/PUT `/v0/management/debug`
|
||||||
|
- Hot reload changes in `config.yaml` and `auths/` are picked up automatically by the watcher
|
||||||
|
|
||||||
131
docs/sdk-advanced_CN.md
Normal file
131
docs/sdk-advanced_CN.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# SDK 高级指南:执行器与翻译器
|
||||||
|
|
||||||
|
本文介绍如何使用 SDK 扩展内嵌代理:
|
||||||
|
- 实现自定义 Provider 执行器以调用你的上游 API
|
||||||
|
- 注册请求/响应翻译器进行协议转换
|
||||||
|
- 注册模型以出现在 `/v1/models`
|
||||||
|
|
||||||
|
示例基于 Go 1.24+ 与 v6 模块路径。
|
||||||
|
|
||||||
|
## 概念
|
||||||
|
|
||||||
|
- Provider 执行器:实现 `auth.ProviderExecutor` 的运行时组件,负责某个 provider key(如 `gemini`、`claude`、`codex`)的真正出站调用。若实现 `RequestPreparer` 接口,可在原始 HTTP 请求上注入凭据。
|
||||||
|
- 翻译器注册表:由 `sdk/translator` 驱动的协议转换函数。内置了 OpenAI/Gemini/Claude/Codex 的互转;你也可以注册新的格式转换。
|
||||||
|
- 模型注册表:对外发布可用模型列表,供 `/v1/models` 与路由参考。
|
||||||
|
|
||||||
|
## 1) 实现 Provider 执行器
|
||||||
|
|
||||||
|
创建类型满足 `auth.ProviderExecutor` 接口。
|
||||||
|
|
||||||
|
```go
|
||||||
|
package myprov
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
clipexec "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Executor struct{}
|
||||||
|
|
||||||
|
func (Executor) Identifier() string { return "myprov" }
|
||||||
|
|
||||||
|
// 可选:在原始 HTTP 请求上注入凭据
|
||||||
|
func (Executor) PrepareRequest(req *http.Request, a *coreauth.Auth) error {
|
||||||
|
// 例如:req.Header.Set("Authorization", "Bearer "+a.Attributes["api_key"])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Executor) Execute(ctx context.Context, a *coreauth.Auth, req clipexec.Request, opts clipexec.Options) (clipexec.Response, error) {
|
||||||
|
// 基于 req.Payload 构造上游请求,返回上游 JSON 负载
|
||||||
|
return clipexec.Response{Payload: []byte(`{"ok":true}`)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Executor) ExecuteStream(ctx context.Context, a *coreauth.Auth, req clipexec.Request, opts clipexec.Options) (<-chan clipexec.StreamChunk, error) {
|
||||||
|
ch := make(chan clipexec.StreamChunk, 1)
|
||||||
|
go func() { defer close(ch); ch <- clipexec.StreamChunk{Payload: []byte("data: {\\"done\\":true}\\n\\n")} }()
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Executor) Refresh(ctx context.Context, a *coreauth.Auth) (*coreauth.Auth, error) { return a, nil }
|
||||||
|
```
|
||||||
|
|
||||||
|
在启动服务前将执行器注册到核心管理器:
|
||||||
|
|
||||||
|
```go
|
||||||
|
core := coreauth.NewManager(coreauth.NewFileStore(cfg.AuthDir), nil, nil)
|
||||||
|
core.RegisterExecutor(myprov.Executor{})
|
||||||
|
svc, _ := cliproxy.NewBuilder().WithConfig(cfg).WithConfigPath(cfgPath).WithCoreAuthManager(core).Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
当凭据的 `Provider` 为 `"myprov"` 时,管理器会将请求路由到你的执行器。
|
||||||
|
|
||||||
|
## 2) 注册翻译器
|
||||||
|
|
||||||
|
内置处理器接受 OpenAI/Gemini/Claude/Codex 的入站格式。要支持新的 provider 协议,需要在 `sdk/translator` 的默认注册表中注册转换函数。
|
||||||
|
|
||||||
|
方向很重要:
|
||||||
|
- 请求:从“入站格式”转换为“provider 格式”
|
||||||
|
- 响应:从“provider 格式”转换回“入站格式”
|
||||||
|
|
||||||
|
示例:OpenAI Chat → MyProv Chat 及其反向。
|
||||||
|
|
||||||
|
```go
|
||||||
|
package myprov
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
sdktr "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FOpenAI = sdktr.Format("openai.chat")
|
||||||
|
FMyProv = sdktr.Format("myprov.chat")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sdktr.Register(FOpenAI, FMyProv,
|
||||||
|
func(model string, raw []byte, stream bool) []byte { return convertOpenAIToMyProv(model, raw, stream) },
|
||||||
|
sdktr.ResponseTransform{
|
||||||
|
Stream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) []string {
|
||||||
|
return convertStreamMyProvToOpenAI(model, originalReq, translatedReq, raw)
|
||||||
|
},
|
||||||
|
NonStream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) string {
|
||||||
|
return convertMyProvToOpenAI(model, originalReq, translatedReq, raw)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当 OpenAI 处理器接到需要路由到 `myprov` 的请求时,流水线会自动应用已注册的转换。
|
||||||
|
|
||||||
|
## 3) 注册模型
|
||||||
|
|
||||||
|
通过全局模型注册表将模型暴露到 `/v1/models`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
models := []*cliproxy.ModelInfo{
|
||||||
|
{ ID: "myprov-pro-1", Object: "model", Type: "myprov", DisplayName: "MyProv Pro 1" },
|
||||||
|
}
|
||||||
|
cliproxy.GlobalModelRegistry().RegisterClient(authID, "myprov", models)
|
||||||
|
```
|
||||||
|
|
||||||
|
内置 Provider 会自动注册;自定义 Provider 建议在启动时(例如加载到 Auth 后)或在 Auth 注册钩子中调用。
|
||||||
|
|
||||||
|
## 凭据与传输
|
||||||
|
|
||||||
|
- 使用 `Manager.SetRoundTripperProvider` 注入按账户的 `*http.Transport`(例如代理):
|
||||||
|
```go
|
||||||
|
core.SetRoundTripperProvider(myProvider) // 按账户返回 transport
|
||||||
|
```
|
||||||
|
- 对于原始 HTTP 请求,若实现了 `PrepareRequest`,或通过 `Manager.InjectCredentials(req, authID)` 进行头部注入。
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
- 启用请求日志:管理 API GET/PUT `/v0/management/request-log`
|
||||||
|
- 切换调试日志:管理 API GET/PUT `/v0/management/debug`
|
||||||
|
- 热更新:`config.yaml` 与 `auths/` 变化会自动被侦测并应用
|
||||||
|
|
||||||
163
docs/sdk-usage.md
Normal file
163
docs/sdk-usage.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# CLI Proxy SDK Guide
|
||||||
|
|
||||||
|
The `sdk/cliproxy` module exposes the proxy as a reusable Go library so external programs can embed the routing, authentication, hot‑reload, and translation layers without depending on the CLI binary.
|
||||||
|
|
||||||
|
## Install & Import
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the `/v6` module path.
|
||||||
|
|
||||||
|
## Minimal Embed
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg, err := config.LoadConfig("config.yaml")
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
|
||||||
|
svc, err := cliproxy.NewBuilder().
|
||||||
|
WithConfig(cfg).
|
||||||
|
WithConfigPath("config.yaml"). // absolute or working-dir relative
|
||||||
|
Build()
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := svc.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The service manages config/auth watching, background token refresh, and graceful shutdown. Cancel the context to stop it.
|
||||||
|
|
||||||
|
## Server Options (middleware, routes, logs)
|
||||||
|
|
||||||
|
The server accepts options via `WithServerOptions`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
svc, _ := cliproxy.NewBuilder().
|
||||||
|
WithConfig(cfg).
|
||||||
|
WithConfigPath("config.yaml").
|
||||||
|
WithServerOptions(
|
||||||
|
// Add global middleware
|
||||||
|
cliproxy.WithMiddleware(func(c *gin.Context) { c.Header("X-Embed", "1"); c.Next() }),
|
||||||
|
// Tweak gin engine early (CORS, trusted proxies, etc.)
|
||||||
|
cliproxy.WithEngineConfigurator(func(e *gin.Engine) { e.ForwardedByClientIP = true }),
|
||||||
|
// Add your own routes after defaults
|
||||||
|
cliproxy.WithRouterConfigurator(func(e *gin.Engine, _ *handlers.BaseAPIHandler, _ *config.Config) {
|
||||||
|
e.GET("/healthz", func(c *gin.Context) { c.String(200, "ok") })
|
||||||
|
}),
|
||||||
|
// Override request log writer/dir
|
||||||
|
cliproxy.WithRequestLoggerFactory(func(cfg *config.Config, cfgPath string) logging.RequestLogger {
|
||||||
|
return logging.NewFileRequestLogger(true, "logs", filepath.Dir(cfgPath))
|
||||||
|
}),
|
||||||
|
).
|
||||||
|
Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
These options mirror the internals used by the CLI server.
|
||||||
|
|
||||||
|
## Management API (when embedded)
|
||||||
|
|
||||||
|
- Management endpoints are mounted only when `remote-management.secret-key` is set in `config.yaml`.
|
||||||
|
- Remote access additionally requires `remote-management.allow-remote: true`.
|
||||||
|
- See MANAGEMENT_API.md for endpoints. Your embedded server exposes them under `/v0/management` on the configured port.
|
||||||
|
|
||||||
|
## Using the Core Auth Manager
|
||||||
|
|
||||||
|
The service uses a core `auth.Manager` for selection, execution, and auto‑refresh. When embedding, you can provide your own manager to customize transports or hooks:
|
||||||
|
|
||||||
|
```go
|
||||||
|
core := coreauth.NewManager(coreauth.NewFileStore(cfg.AuthDir), nil, nil)
|
||||||
|
core.SetRoundTripperProvider(myRTProvider) // per‑auth *http.Transport
|
||||||
|
|
||||||
|
svc, _ := cliproxy.NewBuilder().
|
||||||
|
WithConfig(cfg).
|
||||||
|
WithConfigPath("config.yaml").
|
||||||
|
WithCoreAuthManager(core).
|
||||||
|
Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement a custom per‑auth transport:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type myRTProvider struct{}
|
||||||
|
func (myRTProvider) RoundTripperFor(a *coreauth.Auth) http.RoundTripper {
|
||||||
|
if a == nil || a.ProxyURL == "" { return nil }
|
||||||
|
u, _ := url.Parse(a.ProxyURL)
|
||||||
|
return &http.Transport{ Proxy: http.ProxyURL(u) }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Programmatic execution is available on the manager:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Non‑streaming
|
||||||
|
resp, err := core.Execute(ctx, []string{"gemini"}, req, opts)
|
||||||
|
|
||||||
|
// Streaming
|
||||||
|
chunks, err := core.ExecuteStream(ctx, []string{"gemini"}, req, opts)
|
||||||
|
for ch := range chunks { /* ... */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Built‑in provider executors are wired automatically when you run the `Service`. If you want to use `Manager` stand‑alone without the HTTP server, you must register your own executors that implement `auth.ProviderExecutor`.
|
||||||
|
|
||||||
|
## Custom Client Sources
|
||||||
|
|
||||||
|
Replace the default loaders if your creds live outside the local filesystem:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type memoryTokenProvider struct{}
|
||||||
|
func (p *memoryTokenProvider) Load(ctx context.Context, cfg *config.Config) (*cliproxy.TokenClientResult, error) {
|
||||||
|
// Populate from memory/remote store and return counts
|
||||||
|
return &cliproxy.TokenClientResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, _ := cliproxy.NewBuilder().
|
||||||
|
WithConfig(cfg).
|
||||||
|
WithConfigPath("config.yaml").
|
||||||
|
WithTokenClientProvider(&memoryTokenProvider{}).
|
||||||
|
WithAPIKeyClientProvider(cliproxy.NewAPIKeyClientProvider()).
|
||||||
|
Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
Observe lifecycle without patching internals:
|
||||||
|
|
||||||
|
```go
|
||||||
|
hooks := cliproxy.Hooks{
|
||||||
|
OnBeforeStart: func(cfg *config.Config) { log.Infof("starting on :%d", cfg.Port) },
|
||||||
|
OnAfterStart: func(s *cliproxy.Service) { log.Info("ready") },
|
||||||
|
}
|
||||||
|
svc, _ := cliproxy.NewBuilder().WithConfig(cfg).WithConfigPath("config.yaml").WithHooks(hooks).Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shutdown
|
||||||
|
|
||||||
|
`Run` defers `Shutdown`, so cancelling the parent context is enough. To stop manually:
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = svc.Shutdown(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Hot reload: changes to `config.yaml` and `auths/` are picked up automatically.
|
||||||
|
- Request logging can be toggled at runtime via the Management API.
|
||||||
|
- Gemini Web features (`gemini-web.*`) are honored in the embedded server.
|
||||||
164
docs/sdk-usage_CN.md
Normal file
164
docs/sdk-usage_CN.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# CLI Proxy SDK 使用指南
|
||||||
|
|
||||||
|
`sdk/cliproxy` 模块将代理能力以 Go 库的形式对外暴露,方便在其它服务中内嵌路由、鉴权、热更新与翻译层,而无需依赖可执行的 CLI 程序。
|
||||||
|
|
||||||
|
## 安装与导入
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
注意模块路径包含 `/v6`。
|
||||||
|
|
||||||
|
## 最小可用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg, err := config.LoadConfig("config.yaml")
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
|
||||||
|
svc, err := cliproxy.NewBuilder().
|
||||||
|
WithConfig(cfg).
|
||||||
|
WithConfigPath("config.yaml"). // 绝对路径或工作目录相对路径
|
||||||
|
Build()
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := svc.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
服务内部会管理配置与认证文件的监听、后台令牌刷新与优雅关闭。取消上下文即可停止服务。
|
||||||
|
|
||||||
|
## 服务器可选项(中间件、路由、日志)
|
||||||
|
|
||||||
|
通过 `WithServerOptions` 自定义:
|
||||||
|
|
||||||
|
```go
|
||||||
|
svc, _ := cliproxy.NewBuilder().
|
||||||
|
WithConfig(cfg).
|
||||||
|
WithConfigPath("config.yaml").
|
||||||
|
WithServerOptions(
|
||||||
|
// 追加全局中间件
|
||||||
|
cliproxy.WithMiddleware(func(c *gin.Context) { c.Header("X-Embed", "1"); c.Next() }),
|
||||||
|
// 提前调整 gin 引擎(如 CORS、trusted proxies)
|
||||||
|
cliproxy.WithEngineConfigurator(func(e *gin.Engine) { e.ForwardedByClientIP = true }),
|
||||||
|
// 在默认路由之后追加自定义路由
|
||||||
|
cliproxy.WithRouterConfigurator(func(e *gin.Engine, _ *handlers.BaseAPIHandler, _ *config.Config) {
|
||||||
|
e.GET("/healthz", func(c *gin.Context) { c.String(200, "ok") })
|
||||||
|
}),
|
||||||
|
// 覆盖请求日志的创建(启用/目录)
|
||||||
|
cliproxy.WithRequestLoggerFactory(func(cfg *config.Config, cfgPath string) logging.RequestLogger {
|
||||||
|
return logging.NewFileRequestLogger(true, "logs", filepath.Dir(cfgPath))
|
||||||
|
}),
|
||||||
|
).
|
||||||
|
Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
这些选项与 CLI 服务器内部用法保持一致。
|
||||||
|
|
||||||
|
## 管理 API(内嵌时)
|
||||||
|
|
||||||
|
- 仅当 `config.yaml` 中设置了 `remote-management.secret-key` 时才会挂载管理端点。
|
||||||
|
- 远程访问还需要 `remote-management.allow-remote: true`。
|
||||||
|
- 具体端点见 MANAGEMENT_API_CN.md。内嵌服务器会在配置端口下暴露 `/v0/management`。
|
||||||
|
|
||||||
|
## 使用核心鉴权管理器
|
||||||
|
|
||||||
|
服务内部使用核心 `auth.Manager` 负责选择、执行、自动刷新。内嵌时可自定义其传输或钩子:
|
||||||
|
|
||||||
|
```go
|
||||||
|
core := coreauth.NewManager(coreauth.NewFileStore(cfg.AuthDir), nil, nil)
|
||||||
|
core.SetRoundTripperProvider(myRTProvider) // 按账户返回 *http.Transport
|
||||||
|
|
||||||
|
svc, _ := cliproxy.NewBuilder().
|
||||||
|
WithConfig(cfg).
|
||||||
|
WithConfigPath("config.yaml").
|
||||||
|
WithCoreAuthManager(core).
|
||||||
|
Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
实现每个账户的自定义传输:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type myRTProvider struct{}
|
||||||
|
func (myRTProvider) RoundTripperFor(a *coreauth.Auth) http.RoundTripper {
|
||||||
|
if a == nil || a.ProxyURL == "" { return nil }
|
||||||
|
u, _ := url.Parse(a.ProxyURL)
|
||||||
|
return &http.Transport{ Proxy: http.ProxyURL(u) }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
管理器提供编程式执行接口:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 非流式
|
||||||
|
resp, err := core.Execute(ctx, []string{"gemini"}, req, opts)
|
||||||
|
|
||||||
|
// 流式
|
||||||
|
chunks, err := core.ExecuteStream(ctx, []string{"gemini"}, req, opts)
|
||||||
|
for ch := range chunks { /* ... */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:运行 `Service` 时会自动注册内置的提供商执行器;若仅单独使用 `Manager` 而不启动 HTTP 服务器,则需要自行实现并注册满足 `auth.ProviderExecutor` 的执行器。
|
||||||
|
|
||||||
|
## 自定义凭据来源
|
||||||
|
|
||||||
|
当凭据不在本地文件系统时,替换默认加载器:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type memoryTokenProvider struct{}
|
||||||
|
func (p *memoryTokenProvider) Load(ctx context.Context, cfg *config.Config) (*cliproxy.TokenClientResult, error) {
|
||||||
|
// 从内存/远端加载并返回数量统计
|
||||||
|
return &cliproxy.TokenClientResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, _ := cliproxy.NewBuilder().
|
||||||
|
WithConfig(cfg).
|
||||||
|
WithConfigPath("config.yaml").
|
||||||
|
WithTokenClientProvider(&memoryTokenProvider{}).
|
||||||
|
WithAPIKeyClientProvider(cliproxy.NewAPIKeyClientProvider()).
|
||||||
|
Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动钩子
|
||||||
|
|
||||||
|
无需修改内部代码即可观察生命周期:
|
||||||
|
|
||||||
|
```go
|
||||||
|
hooks := cliproxy.Hooks{
|
||||||
|
OnBeforeStart: func(cfg *config.Config) { log.Infof("starting on :%d", cfg.Port) },
|
||||||
|
OnAfterStart: func(s *cliproxy.Service) { log.Info("ready") },
|
||||||
|
}
|
||||||
|
svc, _ := cliproxy.NewBuilder().WithConfig(cfg).WithConfigPath("config.yaml").WithHooks(hooks).Build()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关闭
|
||||||
|
|
||||||
|
`Run` 内部会延迟调用 `Shutdown`,因此只需取消父上下文即可。若需手动停止:
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = svc.Shutdown(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 热更新:`config.yaml` 与 `auths/` 变化会被自动侦测并应用。
|
||||||
|
- 请求日志可通过管理 API 在运行时开关。
|
||||||
|
- `gemini-web.*` 相关配置在内嵌服务器中会被遵循。
|
||||||
|
|
||||||
32
docs/sdk-watcher.md
Normal file
32
docs/sdk-watcher.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# SDK Watcher Integration
|
||||||
|
|
||||||
|
The SDK service exposes a watcher integration that surfaces granular auth updates without forcing a full reload. This document explains the queue contract, how the service consumes updates, and how high-frequency change bursts are handled.
|
||||||
|
|
||||||
|
## Update Queue Contract
|
||||||
|
|
||||||
|
- `watcher.AuthUpdate` represents a single credential change. `Action` may be `add`, `modify`, or `delete`, and `ID` carries the credential identifier. For `add`/`modify` the `Auth` payload contains a fully populated clone of the credential; `delete` may omit `Auth`.
|
||||||
|
- `WatcherWrapper.SetAuthUpdateQueue(chan<- watcher.AuthUpdate)` wires the queue produced by the SDK service into the watcher. The queue must be created before the watcher starts.
|
||||||
|
- The service builds the queue via `ensureAuthUpdateQueue`, using a buffered channel (`capacity=256`) and a dedicated consumer goroutine (`consumeAuthUpdates`). The consumer drains bursts by looping through the backlog before reacquiring the select loop.
|
||||||
|
|
||||||
|
## Watcher Behaviour
|
||||||
|
|
||||||
|
- `internal/watcher/watcher.go` keeps a shadow snapshot of auth state (`currentAuths`). Each filesystem or configuration event triggers a recomputation and a diff against the previous snapshot to produce minimal `AuthUpdate` entries that mirror adds, edits, and removals.
|
||||||
|
- Updates are coalesced per credential identifier. If multiple changes occur before dispatch (e.g., write followed by delete), only the final action is sent downstream.
|
||||||
|
- The watcher runs an internal dispatch loop that buffers pending updates in memory and forwards them asynchronously to the queue. Producers never block on channel capacity; they just enqueue into the in-memory buffer and signal the dispatcher. Dispatch cancellation happens when the watcher stops, guaranteeing goroutines exit cleanly.
|
||||||
|
|
||||||
|
## High-Frequency Change Handling
|
||||||
|
|
||||||
|
- The dispatch loop and service consumer run independently, preventing filesystem watchers from blocking even when many updates arrive at once.
|
||||||
|
- Back-pressure is absorbed in two places:
|
||||||
|
- The dispatch buffer (map + order slice) coalesces repeated updates for the same credential until the consumer catches up.
|
||||||
|
- The service channel capacity (256) combined with the consumer drain loop ensures several bursts can be processed without oscillation.
|
||||||
|
- If the queue is saturated for an extended period, updates continue to be merged, so the latest state is eventually applied without replaying redundant intermediate states.
|
||||||
|
|
||||||
|
## Usage Checklist
|
||||||
|
|
||||||
|
1. Instantiate the SDK service (builder or manual construction).
|
||||||
|
2. Call `ensureAuthUpdateQueue` before starting the watcher to allocate the shared channel.
|
||||||
|
3. When the `WatcherWrapper` is created, call `SetAuthUpdateQueue` with the service queue, then start the watcher.
|
||||||
|
4. Provide a reload callback that handles configuration updates; auth deltas will arrive via the queue and are applied by the service automatically through `handleAuthUpdate`.
|
||||||
|
|
||||||
|
Following this flow keeps auth changes responsive while avoiding full reloads for every edit.
|
||||||
32
docs/sdk-watcher_CN.md
Normal file
32
docs/sdk-watcher_CN.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# SDK Watcher集成说明
|
||||||
|
|
||||||
|
本文档介绍SDK服务与文件监控器之间的增量更新队列,包括接口契约、高频变更下的处理策略以及接入步骤。
|
||||||
|
|
||||||
|
## 更新队列契约
|
||||||
|
|
||||||
|
- `watcher.AuthUpdate`描述单条凭据变更,`Action`可能为`add`、`modify`或`delete`,`ID`是凭据标识。对于`add`/`modify`会携带完整的`Auth`克隆,`delete`可以省略`Auth`。
|
||||||
|
- `WatcherWrapper.SetAuthUpdateQueue(chan<- watcher.AuthUpdate)`用于将服务侧创建的队列注入watcher,必须在watcher启动前完成。
|
||||||
|
- 服务通过`ensureAuthUpdateQueue`创建容量为256的缓冲通道,并在`consumeAuthUpdates`中使用专职goroutine消费;消费侧会主动“抽干”积压事件,降低切换开销。
|
||||||
|
|
||||||
|
## Watcher行为
|
||||||
|
|
||||||
|
- `internal/watcher/watcher.go`维护`currentAuths`快照,文件或配置事件触发后会重建快照并与旧快照对比,生成最小化的`AuthUpdate`列表。
|
||||||
|
- 以凭据ID为维度对更新进行合并,同一凭据在短时间内的多次变更只会保留最新状态(例如先写后删只会下发`delete`)。
|
||||||
|
- watcher内部运行异步分发循环:生产者只向内存缓冲追加事件并唤醒分发协程,即使通道暂时写满也不会阻塞文件事件线程。watcher停止时会取消分发循环,确保协程正常退出。
|
||||||
|
|
||||||
|
## 高频变更处理
|
||||||
|
|
||||||
|
- 分发循环与服务消费协程相互独立,因此即便短时间内出现大量变更也不会阻塞watcher事件处理。
|
||||||
|
- 背压通过两级缓冲吸收:
|
||||||
|
- 分发缓冲(map + 顺序切片)会合并同一凭据的重复事件,直到消费者完成处理。
|
||||||
|
- 服务端通道的256容量加上消费侧的“抽干”逻辑,可平稳处理多个突发批次。
|
||||||
|
- 当通道长时间处于高压状态时,缓冲仍持续合并事件,从而在消费者恢复后一次性应用最新状态,避免重复处理无意义的中间状态。
|
||||||
|
|
||||||
|
## 接入步骤
|
||||||
|
|
||||||
|
1. 实例化SDK Service(构建器或手工创建)。
|
||||||
|
2. 在启动watcher之前调用`ensureAuthUpdateQueue`创建共享通道。
|
||||||
|
3. watcher通过工厂函数创建后立刻调用`SetAuthUpdateQueue`注入通道,然后再启动watcher。
|
||||||
|
4. Reload回调专注于配置更新;认证增量会通过队列送达,并由`handleAuthUpdate`自动应用。
|
||||||
|
|
||||||
|
遵循上述流程即可在避免全量重载的同时保持凭据变更的实时性。
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
package management
|
package management
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -33,6 +34,8 @@ type Handler struct {
|
|||||||
authManager *coreauth.Manager
|
authManager *coreauth.Manager
|
||||||
usageStats *usage.RequestStatistics
|
usageStats *usage.RequestStatistics
|
||||||
tokenStore sdkAuth.TokenStore
|
tokenStore sdkAuth.TokenStore
|
||||||
|
|
||||||
|
localPassword string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new management handler instance.
|
// NewHandler creates a new management handler instance.
|
||||||
@@ -56,6 +59,9 @@ func (h *Handler) SetAuthManager(manager *coreauth.Manager) { h.authManager = ma
|
|||||||
// SetUsageStatistics allows replacing the usage statistics reference.
|
// SetUsageStatistics allows replacing the usage statistics reference.
|
||||||
func (h *Handler) SetUsageStatistics(stats *usage.RequestStatistics) { h.usageStats = stats }
|
func (h *Handler) SetUsageStatistics(stats *usage.RequestStatistics) { h.usageStats = stats }
|
||||||
|
|
||||||
|
// SetLocalPassword configures the runtime-local password accepted for localhost requests.
|
||||||
|
func (h *Handler) SetLocalPassword(password string) { h.localPassword = password }
|
||||||
|
|
||||||
// Middleware enforces access control for management endpoints.
|
// Middleware enforces access control for management endpoints.
|
||||||
// All requests (local and remote) require a valid management key.
|
// All requests (local and remote) require a valid management key.
|
||||||
// Additionally, remote access requires allow-remote-management=true.
|
// Additionally, remote access requires allow-remote-management=true.
|
||||||
@@ -65,10 +71,10 @@ func (h *Handler) Middleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
|
localClient := clientIP == "127.0.0.1" || clientIP == "::1"
|
||||||
|
|
||||||
// For remote IPs, enforce allow-remote-management and ban checks
|
fail := func() {}
|
||||||
if !(clientIP == "127.0.0.1" || clientIP == "::1") {
|
if !localClient {
|
||||||
// Check if IP is currently blocked
|
|
||||||
h.attemptsMu.Lock()
|
h.attemptsMu.Lock()
|
||||||
ai := h.failedAttempts[clientIP]
|
ai := h.failedAttempts[clientIP]
|
||||||
if ai != nil {
|
if ai != nil {
|
||||||
@@ -86,11 +92,25 @@ func (h *Handler) Middleware() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
h.attemptsMu.Unlock()
|
h.attemptsMu.Unlock()
|
||||||
|
|
||||||
allowRemote := h.cfg.RemoteManagement.AllowRemote
|
if !h.cfg.RemoteManagement.AllowRemote {
|
||||||
if !allowRemote {
|
|
||||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "remote management disabled"})
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "remote management disabled"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fail = func() {
|
||||||
|
h.attemptsMu.Lock()
|
||||||
|
aip := h.failedAttempts[clientIP]
|
||||||
|
if aip == nil {
|
||||||
|
aip = &attemptInfo{}
|
||||||
|
h.failedAttempts[clientIP] = aip
|
||||||
|
}
|
||||||
|
aip.count++
|
||||||
|
if aip.count >= maxFailures {
|
||||||
|
aip.blockedUntil = time.Now().Add(banDuration)
|
||||||
|
aip.count = 0
|
||||||
|
}
|
||||||
|
h.attemptsMu.Unlock()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
secret := h.cfg.RemoteManagement.SecretKey
|
secret := h.cfg.RemoteManagement.SecretKey
|
||||||
if secret == "" {
|
if secret == "" {
|
||||||
@@ -112,36 +132,32 @@ func (h *Handler) Middleware() gin.HandlerFunc {
|
|||||||
provided = c.GetHeader("X-Management-Key")
|
provided = c.GetHeader("X-Management-Key")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(clientIP == "127.0.0.1" || clientIP == "::1") {
|
if provided == "" {
|
||||||
// For remote IPs, enforce key and track failures
|
if !localClient {
|
||||||
fail := func() {
|
|
||||||
h.attemptsMu.Lock()
|
|
||||||
ai := h.failedAttempts[clientIP]
|
|
||||||
if ai == nil {
|
|
||||||
ai = &attemptInfo{}
|
|
||||||
h.failedAttempts[clientIP] = ai
|
|
||||||
}
|
|
||||||
ai.count++
|
|
||||||
if ai.count >= maxFailures {
|
|
||||||
ai.blockedUntil = time.Now().Add(banDuration)
|
|
||||||
ai.count = 0
|
|
||||||
}
|
|
||||||
h.attemptsMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if provided == "" {
|
|
||||||
fail()
|
fail()
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing management key"})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing management key"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(secret), []byte(provided)); err != nil {
|
if localClient {
|
||||||
|
if lp := h.localPassword; lp != "" {
|
||||||
|
if subtle.ConstantTimeCompare([]byte(provided), []byte(lp)) == 1 {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(secret), []byte(provided)); err != nil {
|
||||||
|
if !localClient {
|
||||||
fail()
|
fail()
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid management key"})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid management key"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Success: reset failed count for this IP
|
if !localClient {
|
||||||
h.attemptsMu.Lock()
|
h.attemptsMu.Lock()
|
||||||
if ai := h.failedAttempts[clientIP]; ai != nil {
|
if ai := h.failedAttempts[clientIP]; ai != nil {
|
||||||
ai.count = 0
|
ai.count = 0
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type serverOptionConfig struct {
|
|||||||
engineConfigurator func(*gin.Engine)
|
engineConfigurator func(*gin.Engine)
|
||||||
routerConfigurator func(*gin.Engine, *handlers.BaseAPIHandler, *config.Config)
|
routerConfigurator func(*gin.Engine, *handlers.BaseAPIHandler, *config.Config)
|
||||||
requestLoggerFactory func(*config.Config, string) logging.RequestLogger
|
requestLoggerFactory func(*config.Config, string) logging.RequestLogger
|
||||||
|
localPassword string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerOption customises HTTP server construction.
|
// ServerOption customises HTTP server construction.
|
||||||
@@ -63,6 +64,13 @@ func WithRouterConfigurator(fn func(*gin.Engine, *handlers.BaseAPIHandler, *conf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithLocalManagementPassword stores a runtime-only management password accepted for localhost requests.
|
||||||
|
func WithLocalManagementPassword(password string) ServerOption {
|
||||||
|
return func(cfg *serverOptionConfig) {
|
||||||
|
cfg.localPassword = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithRequestLoggerFactory customises request logger creation.
|
// WithRequestLoggerFactory customises request logger creation.
|
||||||
func WithRequestLoggerFactory(factory func(*config.Config, string) logging.RequestLogger) ServerOption {
|
func WithRequestLoggerFactory(factory func(*config.Config, string) logging.RequestLogger) ServerOption {
|
||||||
return func(cfg *serverOptionConfig) {
|
return func(cfg *serverOptionConfig) {
|
||||||
@@ -163,6 +171,9 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
|
|||||||
s.applyAccessConfig(cfg)
|
s.applyAccessConfig(cfg)
|
||||||
// Initialize management handler
|
// Initialize management handler
|
||||||
s.mgmt = managementHandlers.NewHandler(cfg, configFilePath, authManager)
|
s.mgmt = managementHandlers.NewHandler(cfg, configFilePath, authManager)
|
||||||
|
if optionState.localPassword != "" {
|
||||||
|
s.mgmt.SetLocalPassword(optionState.localPassword)
|
||||||
|
}
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
s.setupRoutes()
|
s.setupRoutes()
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ import (
|
|||||||
// Parameters:
|
// Parameters:
|
||||||
// - cfg: The application configuration
|
// - cfg: The application configuration
|
||||||
// - configPath: The path to the configuration file
|
// - configPath: The path to the configuration file
|
||||||
func StartService(cfg *config.Config, configPath string) {
|
// - localPassword: Optional password accepted for local management requests
|
||||||
|
func StartService(cfg *config.Config, configPath string, localPassword string) {
|
||||||
service, err := cliproxy.NewBuilder().
|
service, err := cliproxy.NewBuilder().
|
||||||
WithConfig(cfg).
|
WithConfig(cfg).
|
||||||
WithConfigPath(configPath).
|
WithConfigPath(configPath).
|
||||||
|
WithLocalManagementPassword(localPassword).
|
||||||
Build()
|
Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to build proxy service: %v", err)
|
log.Fatalf("failed to build proxy service: %v", err)
|
||||||
|
|||||||
@@ -142,6 +142,15 @@ func (b *Builder) WithServerOptions(opts ...api.ServerOption) *Builder {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithLocalManagementPassword configures a password that is only accepted from localhost management requests.
|
||||||
|
func (b *Builder) WithLocalManagementPassword(password string) *Builder {
|
||||||
|
if password == "" {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
b.serverOptions = append(b.serverOptions, api.WithLocalManagementPassword(password))
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
// Build validates inputs, applies defaults, and returns a ready-to-run service.
|
// Build validates inputs, applies defaults, and returns a ready-to-run service.
|
||||||
func (b *Builder) Build() (*Service, error) {
|
func (b *Builder) Build() (*Service, error) {
|
||||||
if b.cfg == nil {
|
if b.cfg == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user