diff --git a/docs/sdk-access.md b/docs/sdk-access.md deleted file mode 100644 index e4e69629..00000000 --- a/docs/sdk-access.md +++ /dev/null @@ -1,176 +0,0 @@ -# @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. diff --git a/docs/sdk-access_CN.md b/docs/sdk-access_CN.md deleted file mode 100644 index b3f26497..00000000 --- a/docs/sdk-access_CN.md +++ /dev/null @@ -1,176 +0,0 @@ -# @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` 保持一致,避免为更新访问策略而重启进程。 diff --git a/docs/sdk-advanced.md b/docs/sdk-advanced.md deleted file mode 100644 index 3a9d3e50..00000000 --- a/docs/sdk-advanced.md +++ /dev/null @@ -1,138 +0,0 @@ -# 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 - diff --git a/docs/sdk-advanced_CN.md b/docs/sdk-advanced_CN.md deleted file mode 100644 index 25e6e83c..00000000 --- a/docs/sdk-advanced_CN.md +++ /dev/null @@ -1,131 +0,0 @@ -# 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/` 变化会自动被侦测并应用 - diff --git a/docs/sdk-usage.md b/docs/sdk-usage.md deleted file mode 100644 index 55e7d5f9..00000000 --- a/docs/sdk-usage.md +++ /dev/null @@ -1,163 +0,0 @@ -# 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. diff --git a/docs/sdk-usage_CN.md b/docs/sdk-usage_CN.md deleted file mode 100644 index b87f9aa1..00000000 --- a/docs/sdk-usage_CN.md +++ /dev/null @@ -1,164 +0,0 @@ -# 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.*` 相关配置在内嵌服务器中会被遵循。 - diff --git a/docs/sdk-watcher.md b/docs/sdk-watcher.md deleted file mode 100644 index c455448b..00000000 --- a/docs/sdk-watcher.md +++ /dev/null @@ -1,32 +0,0 @@ -# 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. diff --git a/docs/sdk-watcher_CN.md b/docs/sdk-watcher_CN.md deleted file mode 100644 index 0373a45d..00000000 --- a/docs/sdk-watcher_CN.md +++ /dev/null @@ -1,32 +0,0 @@ -# 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`自动应用。 - -遵循上述流程即可在避免全量重载的同时保持凭据变更的实时性。 diff --git a/internal/provider/gemini-web/state.go b/internal/provider/gemini-web/state.go index c0f4d11c..bb7cc58a 100644 --- a/internal/provider/gemini-web/state.go +++ b/internal/provider/gemini-web/state.go @@ -81,6 +81,22 @@ func NewGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage, return state } +// Label returns a stable account label for logging and persistence. +// If a storage file path is known, it uses the file base name (without extension). +// Otherwise, it falls back to the stable client ID (e.g., "gemini-web-"). +func (s *GeminiWebState) Label() string { + if s == nil { + return "" + } + if s.storagePath != "" { + base := strings.TrimSuffix(filepath.Base(s.storagePath), filepath.Ext(s.storagePath)) + if base != "" { + return base + } + } + return s.stableClientID +} + func (s *GeminiWebState) loadConversationCaches() { if path := s.convPath(); path != "" { if store, err := LoadConvStore(path); err == nil { diff --git a/internal/runtime/executor/gemini_web_executor.go b/internal/runtime/executor/gemini_web_executor.go index 5f2e09a6..78f31abb 100644 --- a/internal/runtime/executor/gemini_web_executor.go +++ b/internal/runtime/executor/gemini_web_executor.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "net/http" + "strings" "sync" "time" @@ -136,6 +137,11 @@ func (e *GeminiWebExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth auth.Metadata["secure_1psidts"] = ts.Secure1PSIDTS auth.Metadata["type"] = "gemini-web" auth.Metadata["last_refresh"] = time.Now().Format(time.RFC3339) + if v, ok := auth.Metadata["label"].(string); !ok || strings.TrimSpace(v) == "" { + if lbl := state.Label(); strings.TrimSpace(lbl) != "" { + auth.Metadata["label"] = strings.TrimSpace(lbl) + } + } return auth, nil }