From 373ff8d2b585637f736c6fb2c1f0ca2f6a99ff1e Mon Sep 17 00:00:00 2001 From: coso Date: Sun, 4 Jan 2026 19:14:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20API=20Key=20Provid?= =?UTF-8?q?er=20=E5=9C=A8=20/v1/chat/completions=20=E5=92=8C=20/v1/message?= =?UTF-8?q?s=20=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 get_enabled_api_keys_by_type 方法按 Provider 类型获取 API Keys - 添加 get_next_api_key_by_type 方法支持按类型轮询负载均衡 - 在 chat_completions 端点添加 API Key Provider 回退支持 - 在 anthropic_messages 端点添加 API Key Provider 回退支持 - 支持自定义 base_url 的 AnthropicKey 使用 OpenAI 兼容格式调用 - 添加 convert_openai_response_to_anthropic 响应转换函数 - 更新版本号到 v0.28.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/plugins/antigravity-provider.md | 1225 ++++++++ docs/plugins/claude-provider.md | 1255 +++++++++ docs/plugins/codex-provider.md | 1806 ++++++++++++ docs/plugins/droid-provider.md | 2029 ++++++++++++++ docs/plugins/gemini-provider.md | 1535 ++++++++++ docs/plugins/kiro-provider.md | 1255 +++++++++ ...credential-provider-plugin-architecture.md | 2009 +++++++++++++ package.json | 2 +- src-tauri/Cargo.lock | 3 +- src-tauri/Cargo.toml | 3 +- src-tauri/src/app/bootstrap.rs | 439 +++ src-tauri/src/app/commands/api_test.rs | 427 +++ src-tauri/src/app/commands/config.rs | 128 + .../src/app/commands/custom_providers.rs | 89 + src-tauri/src/app/commands/gemini.rs | 184 ++ src-tauri/src/app/commands/kiro.rs | 231 ++ src-tauri/src/app/commands/logs.rs | 19 + src-tauri/src/app/commands/mod.rs | 32 + src-tauri/src/app/commands/qwen.rs | 190 ++ src-tauri/src/app/commands/server.rs | 68 + src-tauri/src/app/mod.rs | 26 + src-tauri/src/app/runner.rs | 878 ++++++ src-tauri/src/app/setup.rs | 244 ++ src-tauri/src/app/state.rs | 319 +++ src-tauri/src/app/types.rs | 172 ++ src-tauri/src/app/utils.rs | 53 + .../src/commands/api_key_provider_cmd.rs | 171 ++ src-tauri/src/commands/mod.rs | 2 + src-tauri/src/commands/oauth_plugin_cmd.rs | 933 +++++++ src-tauri/src/commands/orchestrator_cmd.rs | 493 ++++ src-tauri/src/converter/protocol_selector.rs | 5 + src-tauri/src/credential/mod.rs | 44 + .../src/credential/oauth_plugin_loader.rs | 723 +++++ src-tauri/src/credential/plugin.rs | 568 ++++ src-tauri/src/credential/registry.rs | 802 ++++++ src-tauri/src/credential/risk.rs | 586 ++++ src-tauri/src/credential/sdk.rs | 778 ++++++ src-tauri/src/credential/sync.rs | 36 + src-tauri/src/credential/unified.rs | 328 +++ src-tauri/src/database/README.md | 55 + .../src/database/dao/api_key_provider.rs | 87 + src-tauri/src/database/dao/mod.rs | 2 + src-tauri/src/database/dao/orchestrator.rs | 647 +++++ .../src/database/dao/plugin_credential.rs | 530 ++++ src-tauri/src/database/migration.rs | 97 + src-tauri/src/database/mod.rs | 12 + src-tauri/src/database/schema.rs | 204 ++ src-tauri/src/lib.rs | 2476 +---------------- src-tauri/src/models/provider_pool_model.rs | 17 + src-tauri/src/orchestrator/fallback.rs | 322 +++ src-tauri/src/orchestrator/mod.rs | 43 + src-tauri/src/orchestrator/orchestrator.rs | 369 +++ src-tauri/src/orchestrator/pool_builder.rs | 643 +++++ src-tauri/src/orchestrator/selector.rs | 325 +++ .../orchestrator/strategies/cost_optimized.rs | 157 ++ .../orchestrator/strategies/load_balanced.rs | 158 ++ src-tauri/src/orchestrator/strategies/mod.rs | 34 + .../orchestrator/strategies/round_robin.rs | 172 ++ .../strategies/speed_optimized.rs | 157 ++ .../src/orchestrator/strategies/task_based.rs | 264 ++ src-tauri/src/orchestrator/strategy.rs | 383 +++ src-tauri/src/orchestrator/tier.rs | 363 +++ src-tauri/src/server/handlers/api.rs | 354 +++ .../src/server/handlers/credentials_api.rs | 471 +++- src-tauri/src/server/handlers/management.rs | 31 + .../src/server/handlers/provider_calls.rs | 485 ++++ src-tauri/src/server/mod.rs | 7 + .../src/services/api_key_provider_service.rs | 35 + .../src/services/provider_pool_service.rs | 74 +- src-tauri/src/services/token_cache_service.rs | 19 + src-tauri/tauri.conf.json | 2 +- .../agent/chat/components/ChatNavbar.tsx | 377 ++- .../agent/chat/components/EmptyState.tsx | 61 +- src/components/api-server/ApiServerPage.tsx | 369 ++- src/components/model-selector/ModeToggle.tsx | 59 + src/components/model-selector/ModelList.tsx | 162 ++ .../model-selector/ModelSelector.tsx | 201 ++ .../model-selector/TierSelector.tsx | 159 ++ src/components/model-selector/index.ts | 11 + .../plugins/OAuthPluginContainer.tsx | 581 ++++ src/components/plugins/index.ts | 1 + .../provider-pool/OAuthPluginTab.tsx | 781 ++++++ .../provider-pool/ProviderPoolPage.tsx | 23 +- .../AntigravityFormStandalone.tsx | 159 ++ .../credential-forms/KiroFormStandalone.tsx | 141 + src/components/provider-pool/index.ts | 1 + src/hooks/index.ts | 1 + src/hooks/useOAuthPlugins.ts | 276 ++ src/lib/api/apiKeyProvider.ts | 54 + src/lib/api/oauthPlugin.ts | 295 ++ src/lib/api/orchestrator.ts | 364 +++ src/lib/plugin-components/global.ts | 16 + src/lib/plugin-components/index.ts | 224 ++ src/lib/plugin-loader/PluginUIRenderer.tsx | 157 ++ src/lib/plugin-loader/index.ts | 166 ++ src/lib/plugin-sdk/index.ts | 67 + src/lib/plugin-sdk/sdk.ts | 560 ++++ src/lib/plugin-sdk/types.ts | 399 +++ src/lib/plugin-sdk/usePluginSDK.ts | 262 ++ src/main.tsx | 3 + 100 files changed, 32251 insertions(+), 2764 deletions(-) create mode 100644 docs/plugins/antigravity-provider.md create mode 100644 docs/plugins/claude-provider.md create mode 100644 docs/plugins/codex-provider.md create mode 100644 docs/plugins/droid-provider.md create mode 100644 docs/plugins/gemini-provider.md create mode 100644 docs/plugins/kiro-provider.md create mode 100644 docs/prd/credential-provider-plugin-architecture.md create mode 100644 src-tauri/src/app/bootstrap.rs create mode 100644 src-tauri/src/app/commands/api_test.rs create mode 100644 src-tauri/src/app/commands/config.rs create mode 100644 src-tauri/src/app/commands/custom_providers.rs create mode 100644 src-tauri/src/app/commands/gemini.rs create mode 100644 src-tauri/src/app/commands/kiro.rs create mode 100644 src-tauri/src/app/commands/logs.rs create mode 100644 src-tauri/src/app/commands/mod.rs create mode 100644 src-tauri/src/app/commands/qwen.rs create mode 100644 src-tauri/src/app/commands/server.rs create mode 100644 src-tauri/src/app/mod.rs create mode 100644 src-tauri/src/app/runner.rs create mode 100644 src-tauri/src/app/setup.rs create mode 100644 src-tauri/src/app/state.rs create mode 100644 src-tauri/src/app/types.rs create mode 100644 src-tauri/src/app/utils.rs create mode 100644 src-tauri/src/commands/oauth_plugin_cmd.rs create mode 100644 src-tauri/src/commands/orchestrator_cmd.rs create mode 100644 src-tauri/src/credential/oauth_plugin_loader.rs create mode 100644 src-tauri/src/credential/plugin.rs create mode 100644 src-tauri/src/credential/registry.rs create mode 100644 src-tauri/src/credential/risk.rs create mode 100644 src-tauri/src/credential/sdk.rs create mode 100644 src-tauri/src/credential/unified.rs create mode 100644 src-tauri/src/database/README.md create mode 100644 src-tauri/src/database/dao/orchestrator.rs create mode 100644 src-tauri/src/database/dao/plugin_credential.rs create mode 100644 src-tauri/src/orchestrator/fallback.rs create mode 100644 src-tauri/src/orchestrator/mod.rs create mode 100644 src-tauri/src/orchestrator/orchestrator.rs create mode 100644 src-tauri/src/orchestrator/pool_builder.rs create mode 100644 src-tauri/src/orchestrator/selector.rs create mode 100644 src-tauri/src/orchestrator/strategies/cost_optimized.rs create mode 100644 src-tauri/src/orchestrator/strategies/load_balanced.rs create mode 100644 src-tauri/src/orchestrator/strategies/mod.rs create mode 100644 src-tauri/src/orchestrator/strategies/round_robin.rs create mode 100644 src-tauri/src/orchestrator/strategies/speed_optimized.rs create mode 100644 src-tauri/src/orchestrator/strategies/task_based.rs create mode 100644 src-tauri/src/orchestrator/strategy.rs create mode 100644 src-tauri/src/orchestrator/tier.rs create mode 100644 src/components/model-selector/ModeToggle.tsx create mode 100644 src/components/model-selector/ModelList.tsx create mode 100644 src/components/model-selector/ModelSelector.tsx create mode 100644 src/components/model-selector/TierSelector.tsx create mode 100644 src/components/model-selector/index.ts create mode 100644 src/components/plugins/OAuthPluginContainer.tsx create mode 100644 src/components/provider-pool/OAuthPluginTab.tsx create mode 100644 src/components/provider-pool/credential-forms/AntigravityFormStandalone.tsx create mode 100644 src/components/provider-pool/credential-forms/KiroFormStandalone.tsx create mode 100644 src/hooks/useOAuthPlugins.ts create mode 100644 src/lib/api/oauthPlugin.ts create mode 100644 src/lib/api/orchestrator.ts create mode 100644 src/lib/plugin-components/global.ts create mode 100644 src/lib/plugin-components/index.ts create mode 100644 src/lib/plugin-loader/PluginUIRenderer.tsx create mode 100644 src/lib/plugin-loader/index.ts create mode 100644 src/lib/plugin-sdk/index.ts create mode 100644 src/lib/plugin-sdk/sdk.ts create mode 100644 src/lib/plugin-sdk/types.ts create mode 100644 src/lib/plugin-sdk/usePluginSDK.ts diff --git a/docs/plugins/antigravity-provider.md b/docs/plugins/antigravity-provider.md new file mode 100644 index 000000000..4f60e4fa6 --- /dev/null +++ b/docs/plugins/antigravity-provider.md @@ -0,0 +1,1225 @@ +# Antigravity Provider 插件文档 + +> 版本: 1.0.0 +> 仓库: `aiclientproxy/antigravity-provider` +> 类型: OAuth Provider Plugin + +--- + +## 一、概述 + +### 1.1 插件简介 + +Antigravity Provider 是 ProxyCast 的 OAuth Provider 插件,用于对接 **Google 内部 Gemini CLI** 服务(Antigravity)。它支持 **动态协议选择**:根据模型类型自动选择输出协议(Claude 模型 → Anthropic,Gemini 模型 → Gemini)。 + +### 1.2 核心能力 + +| 能力 | 说明 | +|------|------| +| 动态协议 | claude-* → Anthropic SSE,gemini-* → Gemini 协议 | +| 多模型支持 | Gemini 3 Pro、Gemini 2.5 Flash、Claude Sonnet/Opus | +| Google OAuth | 使用 Google 账户认证 | +| 安全设置 | 自动附加 Safety Settings(关闭内容过滤)| +| 思维链 | 支持 reasoning_effort 配置 | +| 流式响应 | 真实端到端流式传输 | + +### 1.3 支持的模型 + +| 用户模型名 | 内部 API 名称 | 底层模型 | 输出协议 | +|-----------|-------------|---------|---------| +| `gemini-3-pro-preview` | `gemini-3-pro-high` | Gemini | Gemini | +| `gemini-3-pro-image-preview` | `gemini-3-pro-image` | Gemini | Gemini | +| `gemini-3-flash-preview` | `gemini-3-flash` | Gemini | Gemini | +| `gemini-2.5-flash` | `gemini-2.5-flash` | Gemini | Gemini | +| `gemini-2.5-computer-use-preview-10-2025` | `rev19-uic3-1p` | Gemini | Gemini | +| `gemini-claude-sonnet-4-5` | `claude-sonnet-4-5` | Claude | **Anthropic** | +| `gemini-claude-sonnet-4-5-thinking` | `claude-sonnet-4-5-thinking` | Claude | **Anthropic** | +| `gemini-claude-opus-4-5-thinking` | `claude-opus-4-5-thinking` | Claude | **Anthropic** | + +--- + +## 二、插件架构 + +### 2.1 项目结构 + +``` +antigravity-provider/ +├── plugin/ +│ ├── plugin.json # 插件元数据 +│ └── config.json # 默认配置 +│ +├── src-tauri/src/ # 后端 Rust 代码 +│ ├── lib.rs # 插件入口 +│ ├── commands.rs # Tauri 命令 +│ ├── provider.rs # AntigravityProvider 核心实现 +│ ├── credentials.rs # 凭证管理 +│ ├── oauth.rs # Google OAuth 流程 +│ ├── token_refresh.rs # Token 刷新 +│ ├── models.rs # 模型定义和别名映射 +│ ├── safety_settings.rs # 安全设置 +│ └── converter/ # 协议转换 +│ ├── mod.rs +│ ├── openai_to_antigravity.rs # OpenAI → Antigravity +│ ├── anthropic_to_antigravity.rs # Anthropic → Antigravity +│ ├── antigravity_to_anthropic.rs # Antigravity → Anthropic SSE +│ └── antigravity_to_gemini.rs # Antigravity → Gemini +│ +├── src/ # 前端 React UI +│ ├── index.tsx # 插件 UI 入口 +│ ├── components/ +│ │ ├── CredentialList.tsx # 凭证列表 +│ │ ├── CredentialCard.tsx # 凭证卡片 +│ │ ├── AntigravityForm.tsx # 凭证添加表单 +│ │ ├── OAuthLogin.tsx # Google OAuth 登录 +│ │ └── SettingsPanel.tsx # 插件设置 +│ └── types/ +│ └── index.ts # 类型定义 +│ +└── .github/ + └── workflows/ + └── release.yml # 自动构建发布 +``` + +### 2.2 plugin.json + +```json +{ + "name": "antigravity-provider", + "version": "1.0.0", + "description": "Antigravity (Google Gemini CLI) OAuth Provider - 支持 Gemini 和 Claude 模型", + "author": "ProxyCast Team", + "homepage": "https://github.com/aiclientproxy/antigravity-provider", + "license": "MIT", + + "plugin_type": "oauth_provider", + "entry": "antigravity-provider-cli", + "min_proxycast_version": "1.0.0", + + "provider": { + "id": "antigravity", + "display_name": "Antigravity (Gemini CLI)", + "target_protocol": "dynamic", + "protocol_rules": { + "claude-*": "anthropic", + "gemini-*": "gemini" + }, + "supported_models": [ + "gemini-3-pro-*", + "gemini-2.5-*", + "gemini-claude-*" + ], + "auth_types": ["oauth"], + "credential_schema": { + "type": "object", + "properties": { + "access_token": { "type": "string", "title": "Access Token" }, + "refresh_token": { "type": "string", "title": "Refresh Token" }, + "expiry_date": { "type": "integer", "title": "过期时间戳" }, + "project_id": { "type": "string", "title": "Project ID" }, + "email": { "type": "string", "title": "Google 邮箱" } + }, + "required": ["access_token", "refresh_token"] + } + }, + + "binary": { + "binary_name": "antigravity-provider-cli", + "github_owner": "aiclientproxy", + "github_repo": "antigravity-provider", + "platform_binaries": { + "macos-arm64": "antigravity-provider-aarch64-apple-darwin", + "macos-x64": "antigravity-provider-x86_64-apple-darwin", + "linux-x64": "antigravity-provider-x86_64-unknown-linux-gnu", + "windows-x64": "antigravity-provider-x86_64-pc-windows-msvc.exe" + }, + "checksum_file": "checksums.txt" + }, + + "ui": { + "surfaces": ["oauth_providers"], + "icon": "Sparkles", + "title": "Antigravity Provider", + "entry": "dist/index.js", + "styles": "dist/styles.css", + "default_width": 900, + "default_height": 700, + "permissions": [ + "database:read", + "database:write", + "http:request", + "crypto:encrypt", + "shell:open" + ] + } +} +``` + +### 2.3 config.json + +```json +{ + "enabled": true, + "timeout_ms": 120000, + "settings": { + "api": { + "environment": "daily", + "base_url_daily": "https://daily-cloudcode-pa.sandbox.googleapis.com", + "base_url_autopush": "https://autopush-cloudcode-pa.sandbox.googleapis.com", + "api_version": "v1internal" + }, + "safety_settings": { + "harassment": "OFF", + "hate_speech": "OFF", + "sexually_explicit": "OFF", + "dangerous_content": "OFF", + "civic_integrity": "BLOCK_NONE" + }, + "token_refresh": { + "auto_refresh": true, + "refresh_skew_seconds": 3000, + "max_retry": 3 + }, + "reasoning": { + "default_effort": "medium", + "enable_thinking_models": true + } + } +} +``` + +--- + +## 三、后端实现 + +### 3.1 核心数据结构 + +#### 凭证结构 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AntigravityCredentials { + pub access_token: Option, + pub refresh_token: Option, + pub token_type: Option, + pub expiry_date: Option, // 毫秒时间戳 + pub expire: Option, // RFC3339 格式 + pub scope: Option, + pub last_refresh: Option, // RFC3339 格式 + pub cred_type: String, // 默认: "antigravity" + pub expires_in: Option, // 有效期(秒) + pub timestamp: Option, // 获取时间(毫秒) + pub enable: Option, + pub project_id: Option, + pub email: Option, +} +``` + +#### 模型别名映射 + +```rust +/// 用户友好模型名 → 内部 API 模型名 +pub fn map_model_name(model: &str) -> String { + let mappings = [ + ("gemini-2.5-computer-use-preview-10-2025", "rev19-uic3-1p"), + ("gemini-3-pro-image-preview", "gemini-3-pro-image"), + ("gemini-3-pro-preview", "gemini-3-pro-high"), + ("gemini-3-flash-preview", "gemini-3-flash"), + ("gemini-2.5-flash", "gemini-2.5-flash"), + ("gemini-claude-sonnet-4-5", "claude-sonnet-4-5"), + ("gemini-claude-sonnet-4-5-thinking", "claude-sonnet-4-5-thinking"), + ("gemini-claude-opus-4-5-thinking", "claude-opus-4-5-thinking"), + ]; + + for (from, to) in mappings { + if model.contains(from) { + return to.to_string(); + } + } + + model.to_string() +} +``` + +### 3.2 动态协议选择 + +```rust +/// 根据模型确定输出协议 +pub fn determine_output_protocol(model: &str) -> OutputProtocol { + // Claude 模型 → Anthropic 协议 + if model.contains("claude") { + return OutputProtocol::Anthropic; + } + + // Gemini 模型 → Gemini 协议 + OutputProtocol::Gemini +} + +#[derive(Debug, Clone)] +pub enum OutputProtocol { + Anthropic, // Claude Code 使用 + Gemini, // Gemini 客户端使用 +} +``` + +### 3.3 OAuth 配置 + +```rust +/// OAuth 2.0 配置(Google) +pub const OAUTH_CLIENT_ID: &str = + "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"; +pub const OAUTH_CLIENT_SECRET: &str = + "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"; + +pub const OAUTH_SCOPES: &[&str] = &[ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/cclog", + "https://www.googleapis.com/auth/experimentsandconfigs", +]; + +pub const OAUTH_AUTH_URL: &str = "https://accounts.google.com/o/oauth2/auth"; +pub const OAUTH_TOKEN_URL: &str = "https://oauth2.googleapis.com/token"; +``` + +### 3.4 Token 刷新 + +```rust +impl AntigravityProvider { + /// Token 刷新时间偏移(提前 50 分钟刷新) + const REFRESH_SKEW: i64 = 3000; // 秒 + + /// 检查 Token 是否需要刷新 + pub fn needs_refresh(&self, credential: &AntigravityCredentials) -> bool { + let now = Utc::now().timestamp(); + + // 优先使用 expiry_date(毫秒时间戳) + if let Some(expiry_date) = credential.expiry_date { + let expiry_secs = expiry_date / 1000; + return now >= expiry_secs - Self::REFRESH_SKEW; + } + + // 使用 expire(RFC3339 字符串) + if let Some(expire) = &credential.expire { + if let Ok(expiry) = DateTime::parse_from_rfc3339(expire) { + return now >= expiry.timestamp() - Self::REFRESH_SKEW; + } + } + + // 使用 expires_in + timestamp + if let (Some(expires_in), Some(timestamp)) = (credential.expires_in, credential.timestamp) { + let expiry_secs = timestamp / 1000 + expires_in; + return now >= expiry_secs - Self::REFRESH_SKEW; + } + + // 默认需要刷新 + true + } + + /// 刷新 Token + pub async fn refresh_token(&self, credential: &mut AntigravityCredentials) -> Result<()> { + let refresh_token = credential.refresh_token.as_ref() + .ok_or(Error::MissingRefreshToken)?; + + let response = self.http_client + .post(OAUTH_TOKEN_URL) + .form(&[ + ("client_id", OAUTH_CLIENT_ID), + ("client_secret", OAUTH_CLIENT_SECRET), + ("refresh_token", refresh_token.as_str()), + ("grant_type", "refresh_token"), + ]) + .send() + .await?; + + let token_response: TokenResponse = response.json().await?; + + // 更新凭证 + credential.access_token = Some(token_response.access_token); + credential.expiry_date = Some(Utc::now().timestamp_millis() + token_response.expires_in * 1000); + credential.last_refresh = Some(Utc::now().to_rfc3339()); + + Ok(()) + } +} +``` + +### 3.5 安全设置 + +```rust +/// 默认安全设置(关闭内容过滤) +pub fn default_safety_settings() -> Vec { + vec![ + SafetySetting { + category: "HARM_CATEGORY_HARASSMENT".to_string(), + threshold: "OFF".to_string(), + }, + SafetySetting { + category: "HARM_CATEGORY_HATE_SPEECH".to_string(), + threshold: "OFF".to_string(), + }, + SafetySetting { + category: "HARM_CATEGORY_SEXUALLY_EXPLICIT".to_string(), + threshold: "OFF".to_string(), + }, + SafetySetting { + category: "HARM_CATEGORY_DANGEROUS_CONTENT".to_string(), + threshold: "OFF".to_string(), + }, + SafetySetting { + category: "HARM_CATEGORY_CIVIC_INTEGRITY".to_string(), + threshold: "BLOCK_NONE".to_string(), + }, + ] +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SafetySetting { + pub category: String, + pub threshold: String, +} +``` + +### 3.6 协议转换 + +#### OpenAI → Antigravity + +```rust +/// 将 OpenAI ChatCompletion 请求转换为 Antigravity 格式 +pub fn convert_openai_to_antigravity( + request: &OpenAiRequest, + project_id: Option<&str>, +) -> AntigravityRequest { + // 1. 模型映射 + let model = map_model_name(&request.model); + + // 2. 消息转换 + let contents = convert_messages(&request.messages); + + // 3. 系统指令提取 + let system_instruction = extract_system_instruction(&request.messages); + + // 4. 工具转换 + let tools = request.tools.as_ref().map(|t| convert_tools(t)); + + // 5. 生成配置 + let generation_config = GenerationConfig { + max_output_tokens: request.max_tokens, + temperature: request.temperature, + top_p: request.top_p, + candidate_count: Some(1), + stop_sequences: request.stop.clone(), + // 思维链配置 + thinking_config: request.reasoning_effort.as_ref().map(|effort| { + ThinkingConfig { + thinking_budget: match effort.as_str() { + "low" => 1024, + "medium" => 4096, + "high" => 16384, + _ => 4096, + }, + } + }), + }; + + AntigravityRequest { + model, + contents, + system_instruction, + tools, + generation_config: Some(generation_config), + safety_settings: Some(default_safety_settings()), + } +} + +/// 消息格式转换 +fn convert_messages(messages: &[OpenAiMessage]) -> Vec { + messages + .iter() + .filter(|m| m.role != "system") // 系统消息单独处理 + .map(|msg| { + let role = match msg.role.as_str() { + "user" => "user", + "assistant" => "model", + "tool" => "function", + _ => "user", + }; + + Content { + role: role.to_string(), + parts: convert_content_parts(&msg.content, &msg.tool_calls), + } + }) + .collect() +} +``` + +#### Antigravity → Anthropic SSE(Claude 模型) + +```rust +/// 将 Antigravity 响应转换为 Anthropic SSE 格式 +pub struct AntigravityToAnthropicTranslator { + message_id: String, + model: String, + current_index: u32, + input_tokens: u32, + output_tokens: u32, +} + +impl AntigravityToAnthropicTranslator { + pub fn translate_chunk(&mut self, chunk: &GeminiStreamChunk) -> Vec { + let mut events = Vec::new(); + + // 候选内容处理 + if let Some(candidates) = &chunk.candidates { + for candidate in candidates { + if let Some(content) = &candidate.content { + for part in &content.parts { + // 文本内容 + if let Some(text) = &part.text { + events.push(AnthropicSseEvent::ContentBlockDelta { + index: self.current_index, + delta: Delta::TextDelta { text: text.clone() }, + }); + } + + // 思维内容(thinking models) + if let Some(thought) = &part.thought { + events.push(AnthropicSseEvent::ContentBlockDelta { + index: self.current_index, + delta: Delta::ThinkingDelta { thinking: thought.clone() }, + }); + } + + // 工具调用 + if let Some(function_call) = &part.function_call { + events.push(AnthropicSseEvent::ContentBlockStart { + index: self.current_index, + content_block: ContentBlock::ToolUse { + id: Uuid::new_v4().to_string(), + name: function_call.name.clone(), + }, + }); + } + } + } + + // 完成原因 + if let Some(finish_reason) = &candidate.finish_reason { + let stop_reason = match finish_reason.as_str() { + "STOP" => "end_turn", + "MAX_TOKENS" => "max_tokens", + "SAFETY" => "content_filter", + "TOOL_CALL" => "tool_use", + _ => "end_turn", + }; + + events.push(AnthropicSseEvent::MessageDelta { + delta: MessageDelta { + stop_reason: Some(stop_reason.to_string()), + }, + usage: Usage { + input_tokens: self.input_tokens, + output_tokens: self.output_tokens, + }, + }); + } + } + } + + // 使用统计 + if let Some(usage) = &chunk.usage_metadata { + self.input_tokens = usage.prompt_token_count.unwrap_or(0); + self.output_tokens = usage.candidates_token_count.unwrap_or(0); + } + + events + } +} +``` + +### 3.7 API 调用 + +```rust +impl AntigravityProvider { + /// API 基础 URL + fn get_base_url(&self) -> &str { + match self.config.environment.as_str() { + "autopush" => "https://autopush-cloudcode-pa.sandbox.googleapis.com", + _ => "https://daily-cloudcode-pa.sandbox.googleapis.com", + } + } + + /// 流式 API 调用 + pub async fn call_api_stream( + &self, + request: AntigravityRequest, + credential: &AntigravityCredentials, + ) -> Result>> { + let url = format!( + "{}/v1internal/models/{}:streamGenerateContent?alt=sse", + self.get_base_url(), + request.model + ); + + let response = self.http_client + .post(&url) + .header("Authorization", format!("Bearer {}", credential.access_token.as_ref().unwrap())) + .header("Content-Type", "application/json") + .header("X-Goog-Api-Client", "genai-js/0.21.0") + .json(&request) + .send() + .await?; + + // 解析 SSE 流 + Ok(parse_sse_stream(response.bytes_stream())) + } +} +``` + +--- + +## 四、前端 UI 实现 + +### 4.1 插件入口 + +```tsx +// src/index.tsx +import { ProxyCastPluginSDK } from '@proxycast/plugin-sdk'; +import { CredentialList } from './components/CredentialList'; +import { AntigravityForm } from './components/AntigravityForm'; +import { SettingsPanel } from './components/SettingsPanel'; + +interface PluginProps { + sdk: ProxyCastPluginSDK; + pluginId: string; +} + +export default function AntigravityProviderUI({ sdk, pluginId }: PluginProps) { + const [view, setView] = useState<'list' | 'add' | 'settings'>('list'); + const [credentials, setCredentials] = useState([]); + + useEffect(() => { + loadCredentials(); + }, []); + + const loadCredentials = async () => { + const result = await sdk.database.query( + 'SELECT * FROM plugin_credentials WHERE plugin_id = ? ORDER BY created_at DESC', + [pluginId] + ); + setCredentials(result); + }; + + return ( +
+
+ Antigravity Provider + 支持 Gemini 3 Pro 和 Claude 模型 + + + + +
+ + {/* 动态协议说明 */} + +

此 Provider 支持动态协议选择:

+
    +
  • gemini-* 模型 → Gemini 协议输出
  • +
  • claude-* 模型 → Anthropic 协议输出
  • +
+
+ + {view === 'list' && ( + + )} + + {view === 'add' && ( + { + loadCredentials(); + setView('list'); + }} + onCancel={() => setView('list')} + /> + )} + + {view === 'settings' && ( + setView('list')} + /> + )} +
+ ); +} +``` + +### 4.2 凭证添加表单 + +```tsx +// src/components/AntigravityForm.tsx + +type AddMode = 'oauth' | 'file'; + +interface AntigravityFormProps { + sdk: ProxyCastPluginSDK; + onSuccess: () => void; + onCancel: () => void; +} + +export function AntigravityForm({ sdk, onSuccess, onCancel }: AntigravityFormProps) { + const [mode, setMode] = useState('oauth'); + const [loading, setLoading] = useState(false); + const [projectId, setProjectId] = useState(''); + const [name, setName] = useState(''); + + const handleOAuthLogin = async () => { + setLoading(true); + + try { + // 启动 OAuth 流程 + const result = await sdk.http.request('/api/antigravity/oauth/start', { + method: 'POST', + body: JSON.stringify({ + name: name || undefined, + skipProjectIdFetch: !projectId, + }), + }); + + // 打开授权 URL + await sdk.shell.open(result.authUrl); + + // 等待回调 + const credential = await sdk.http.request('/api/antigravity/oauth/callback/wait', { + method: 'POST', + timeout: 120000, // 2 分钟超时 + }); + + sdk.notification.success('OAuth 登录成功'); + onSuccess(); + } catch (error) { + sdk.notification.error(`登录失败: ${error.message}`); + } finally { + setLoading(false); + } + }; + + const handleFileImport = async (filePath: string) => { + setLoading(true); + + try { + await sdk.http.request('/api/antigravity/credentials/import', { + method: 'POST', + body: JSON.stringify({ + credsFilePath: filePath, + projectId: projectId || undefined, + name: name || undefined, + }), + }); + + sdk.notification.success('凭证导入成功'); + onSuccess(); + } catch (error) { + sdk.notification.error(`导入失败: ${error.message}`); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* 模式选择 */} + + Google OAuth 登录 + 导入凭证文件 + + + {/* 通用配置 */} + + + setName(e.target.value)} + placeholder="我的 Antigravity 凭证" + /> + + + + + setProjectId(e.target.value)} + placeholder="自动获取或手动输入" + /> + 留空将自动从 Google 账户获取 + + + {mode === 'oauth' && ( + + )} + + {mode === 'file' && ( + + )} + + + + +
+ ); +} +``` + +### 4.3 OAuth 登录模式 + +```tsx +// src/components/OAuthLogin.tsx + +interface OAuthModeProps { + loading: boolean; + onLogin: () => void; +} + +export function OAuthMode({ loading, onLogin }: OAuthModeProps) { + return ( +
+ +

点击下方按钮将打开浏览器进行 Google 账户授权。

+

授权完成后,凭证将自动添加。

+
+ +
+

请求的权限:

+
    +
  • Cloud Platform 访问
  • +
  • 用户邮箱和个人资料
  • +
  • Cloud Code 日志
  • +
+
+ + +
+ ); +} +``` + +### 4.4 凭证卡片 + +```tsx +// src/components/CredentialCard.tsx + +interface CredentialCardProps { + credential: Credential; + onRefresh: () => void; + onDelete: () => void; +} + +export function CredentialCard({ credential, onRefresh, onDelete }: CredentialCardProps) { + const data = JSON.parse(credential.credential_data) as AntigravityCredentials; + const isHealthy = credential.status === 'active'; + + return ( + + +
+ + {isHealthy ? '健康' : '异常'} +
+ Google OAuth +
+ + +
+ + {credential.name || '未命名'} +
+
+ + {data.email || '-'} +
+
+ + {data.project_id || '自动'} +
+
+ + + {data.expiry_date + ? formatDate(new Date(data.expiry_date)) + : '-' + } + +
+
+ + {data.last_refresh || '-'} +
+ + {/* 支持的模型 */} +
+ +
+ Gemini 3 Pro + Gemini 2.5 Flash + Claude Sonnet 4.5 + Claude Opus 4.5 +
+
+
+ + + + + +
+ ); +} +``` + +### 4.5 设置面板 + +```tsx +// src/components/SettingsPanel.tsx + +interface SettingsPanelProps { + sdk: ProxyCastPluginSDK; + pluginId: string; + onClose: () => void; +} + +export function SettingsPanel({ sdk, pluginId, onClose }: SettingsPanelProps) { + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadConfig(); + }, []); + + const loadConfig = async () => { + const result = await sdk.database.query<{ config: string }>( + 'SELECT config FROM plugin_configs WHERE plugin_id = ?', + [pluginId] + ); + if (result.length > 0) { + setConfig(JSON.parse(result[0].config)); + } + setLoading(false); + }; + + const saveConfig = async () => { + await sdk.database.execute( + 'UPDATE plugin_configs SET config = ? WHERE plugin_id = ?', + [JSON.stringify(config), pluginId] + ); + sdk.notification.success('设置已保存'); + }; + + if (loading) return ; + + return ( +
+

插件设置

+ + {/* API 环境 */} + + + + + + {/* 思维链配置 */} + + + + 影响 thinking 模型的思考预算 + + + {/* 安全设置 */} + + + { + const value = checked ? 'OFF' : 'BLOCK_MEDIUM_AND_ABOVE'; + setConfig({ + ...config, + settings: { + ...config?.settings, + safety_settings: { + harassment: value, + hate_speech: value, + sexually_explicit: value, + dangerous_content: value, + civic_integrity: 'BLOCK_NONE', + }, + }, + }); + }} + > + 关闭所有内容过滤 + + + + + + + +
+ ); +} +``` + +--- + +## 五、凭证文件格式 + +### 5.1 单凭证格式 + +**路径**: `~/.antigravity/oauth_creds.json` + +```json +{ + "access_token": "ya29.a0AfH6SMC...", + "refresh_token": "1//0gXXXXXXXXXXXX...", + "token_type": "Bearer", + "expiry_date": 1704369600000, + "scope": "https://www.googleapis.com/auth/cloud-platform ...", + "cred_type": "antigravity", + "project_id": "my-project-123", + "email": "user@example.com" +} +``` + +### 5.2 多凭证数组格式 + +**路径**: `accounts.json`(兼容 antigravity2api-nodejs) + +```json +[ + { + "access_token": "ya29.a0AfH6SMC...", + "refresh_token": "1//0gXXXXXXXXXXXX...", + "expiry_date": 1704369600000, + "email": "user1@example.com", + "enable": true + }, + { + "access_token": "ya29.b1BgH7TNE...", + "refresh_token": "1//0hYYYYYYYYYYYY...", + "expiry_date": 1704456000000, + "email": "user2@example.com", + "enable": true + } +] +``` + +--- + +## 六、API 调用流程 + +### 6.1 完整调用链路 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Antigravity Provider 调用流程 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. 客户端请求 (Anthropic/OpenAI 格式) │ +│ │ │ +│ ▼ │ +│ 2. 模型识别 & 协议选择 │ +│ ├── gemini-* → 输出 Gemini 协议 │ +│ └── claude-* → 输出 Anthropic 协议 │ +│ │ │ +│ ▼ │ +│ 3. 凭证获取 │ +│ ├── 从凭证池选择健康凭证 │ +│ ├── 检查 Token 是否过期 │ +│ └── 必要时刷新 Token │ +│ │ │ +│ ▼ │ +│ 4. 请求转换 │ +│ ├── 模型名映射(用户名 → 内部 API 名) │ +│ ├── 消息格式转换 │ +│ ├── 附加 Safety Settings │ +│ └── 配置思维链(reasoning_effort) │ +│ │ │ +│ ▼ │ +│ 5. Antigravity API 调用 │ +│ └── POST {base_url}/v1internal/models/{model}:streamGenerateContent │ +│ │ │ +│ ▼ │ +│ 6. 响应转换(根据模型类型) │ +│ ├── Gemini 模型 → Gemini SSE 格式 │ +│ └── Claude 模型 → Anthropic SSE 格式 │ +│ │ │ +│ ▼ │ +│ 7. 返回客户端 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 6.2 Antigravity API + +**端点**: `POST {base_url}/v1internal/models/{model}:streamGenerateContent?alt=sse` + +**请求格式**: + +```json +{ + "model": "gemini-3-pro-high", + "contents": [ + { + "role": "user", + "parts": [ + { "text": "Hello, how are you?" } + ] + } + ], + "systemInstruction": { + "parts": [ + { "text": "You are a helpful assistant." } + ] + }, + "generationConfig": { + "maxOutputTokens": 4096, + "temperature": 0.7, + "topP": 0.9, + "candidateCount": 1, + "thinkingConfig": { + "thinkingBudget": 4096 + } + }, + "safetySettings": [ + { "category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF" }, + { "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF" }, + { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF" }, + { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF" }, + { "category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE" } + ] +} +``` + +**响应格式**: Gemini SSE Stream + +--- + +## 七、错误处理 + +### 7.1 错误类型 + +| 错误码 | 说明 | 处理方式 | +|--------|------|---------| +| 401 | Token 过期 | 自动刷新 Token 重试 | +| 403 | 权限不足 / 账户无访问权限 | 标记凭证异常 | +| 429 | 请求限流 | 冷却期后重试 | +| 500 | 服务器错误 | 切换凭证重试 | + +### 7.2 Token 刷新错误 + +```rust +pub enum TokenRefreshError { + MissingRefreshToken, + InvalidRefreshToken, + NetworkError(String), + AuthorizationRevoked, +} +``` + +--- + +## 八、开发指南 + +### 8.1 本地开发 + +```bash +# 克隆仓库 +git clone https://github.com/aiclientproxy/antigravity-provider.git +cd antigravity-provider + +# 安装依赖 +pnpm install +cd src-tauri && cargo build + +# 前端开发 +pnpm dev + +# 后端开发 +cargo watch -x run +``` + +### 8.2 测试 + +```bash +# 单元测试 +cargo test + +# 协议转换测试 +cargo test --test converter + +# 前端测试 +pnpm test +``` + +--- + +## 附录 + +### A. 环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `ANTIGRAVITY_ENVIRONMENT` | API 环境 | `daily` | +| `ANTIGRAVITY_DEBUG` | 调试模式 | `false` | +| `ANTIGRAVITY_TIMEOUT_MS` | 请求超时 | `120000` | + +### B. 参考链接 + +- [Google Cloud AI Platform](https://cloud.google.com/ai-platform) +- [Gemini API 文档](https://ai.google.dev/docs) +- [ProxyCast 插件开发指南](../prd/credential-provider-plugin-architecture.md) diff --git a/docs/plugins/claude-provider.md b/docs/plugins/claude-provider.md new file mode 100644 index 000000000..f2b9d9710 --- /dev/null +++ b/docs/plugins/claude-provider.md @@ -0,0 +1,1255 @@ +# Claude Provider 插件文档 + +> 版本: 1.0.0 +> 仓库: `aiclientproxy/claude-provider` +> 类型: OAuth Provider Plugin + +--- + +## 一、概述 + +### 1.1 插件简介 + +Claude Provider 是 ProxyCast 的综合性 Anthropic/Claude 插件,支持 **多种认证方式** 访问 Claude 模型。无论是官方 OAuth、Claude Code、Console、AWS Bedrock 还是第三方中转服务,都可以通过此插件统一管理。 + +### 1.2 支持的认证方式 + +| 认证方式 | 说明 | 适用场景 | +|---------|------|---------| +| **OAuth** | 标准 OAuth 2.0 + PKCE | Claude.ai 个人账户 | +| **Claude Code** | Claude Code CLI 认证 | 开发者工具 | +| **Console** | Anthropic Console OAuth | 企业/团队账户 | +| **Setup Token** | 只读推理 Token | 最小权限场景 | +| **Bedrock** | AWS Bedrock Claude | AWS 云服务 | +| **CCR** | 第三方中转服务 | 自定义 API 端点 | + +### 1.3 核心能力 + +| 能力 | 说明 | +|------|------| +| 多认证统一管理 | 一个插件管理所有 Claude 访问方式 | +| 自动 Token 刷新 | OAuth 类型自动刷新,提前 5 分钟 | +| PKCE 安全 | OAuth 使用 PKCE 流程确保安全 | +| Cookie 快速授权 | 使用 sessionKey 自动完成 OAuth | +| 凭证加密存储 | AES-256 加密敏感信息 | +| 健康检查 | 凭证池级别健康监控 | + +### 1.4 支持的模型 + +| 模型 | 说明 | +|------|------| +| `claude-opus-4-20250514` | Claude Opus 4 最新版 | +| `claude-opus-4-5-20251101` | Claude Opus 4.5 | +| `claude-sonnet-4-5-20250929` | Claude Sonnet 4.5 | +| `claude-sonnet-4-20250514` | Claude Sonnet 4 | +| `claude-haiku-3-5-20241022` | Claude Haiku 3.5 | + +--- + +## 二、插件架构 + +### 2.1 项目结构 + +``` +claude-provider/ +├── plugin/ +│ ├── plugin.json # 插件元数据 +│ └── config.json # 默认配置 +│ +├── src-tauri/src/ # 后端 Rust 代码 +│ ├── lib.rs # 插件入口 +│ ├── commands.rs # Tauri 命令 +│ ├── provider.rs # ClaudeProvider 核心实现 +│ ├── auth/ # 认证模块 +│ │ ├── mod.rs +│ │ ├── oauth.rs # OAuth 2.0 + PKCE +│ │ ├── claude_code.rs # Claude Code 认证 +│ │ ├── console.rs # Console OAuth +│ │ ├── setup_token.rs # Setup Token +│ │ ├── bedrock.rs # AWS Bedrock +│ │ └── ccr.rs # 第三方中转 +│ ├── credentials.rs # 凭证管理 +│ ├── token_refresh.rs # Token 刷新 +│ └── api/ # API 调用 +│ ├── mod.rs +│ ├── anthropic.rs # Anthropic API +│ └── bedrock.rs # Bedrock API +│ +├── src/ # 前端 React UI +│ ├── index.tsx # 插件 UI 入口 +│ ├── components/ +│ │ ├── CredentialList.tsx # 凭证列表 +│ │ ├── CredentialCard.tsx # 凭证卡片 +│ │ ├── AuthMethodTabs.tsx # 认证方式选择 +│ │ ├── OAuthForm.tsx # OAuth 表单 +│ │ ├── ClaudeCodeForm.tsx # Claude Code 表单 +│ │ ├── ConsoleForm.tsx # Console 表单 +│ │ ├── BedrockForm.tsx # Bedrock 表单 +│ │ ├── CCRForm.tsx # CCR 表单 +│ │ └── SettingsPanel.tsx # 插件设置 +│ └── types/ +│ └── index.ts # 类型定义 +│ +└── .github/ + └── workflows/ + └── release.yml # 自动构建发布 +``` + +### 2.2 plugin.json + +```json +{ + "name": "claude-provider", + "version": "1.0.0", + "description": "Claude Provider - 支持 OAuth、Claude Code、Console、Bedrock、CCR 多种认证方式", + "author": "ProxyCast Team", + "homepage": "https://github.com/aiclientproxy/claude-provider", + "license": "MIT", + + "plugin_type": "oauth_provider", + "entry": "claude-provider-cli", + "min_proxycast_version": "1.0.0", + + "provider": { + "id": "claude", + "display_name": "Claude (Anthropic)", + "target_protocol": "anthropic", + "supported_models": ["claude-*"], + "auth_types": ["oauth", "claude_code", "console", "setup_token", "bedrock", "ccr"], + "credential_schemas": { + "oauth": { + "type": "object", + "properties": { + "access_token": { "type": "string" }, + "refresh_token": { "type": "string" }, + "email": { "type": "string" }, + "expire": { "type": "string" } + }, + "required": ["access_token", "refresh_token"] + }, + "claude_code": { + "type": "object", + "properties": { + "access_token": { "type": "string" }, + "refresh_token": { "type": "string" }, + "session_key": { "type": "string" } + } + }, + "console": { + "type": "object", + "properties": { + "access_token": { "type": "string" }, + "refresh_token": { "type": "string" }, + "organization_id": { "type": "string" } + } + }, + "setup_token": { + "type": "object", + "properties": { + "access_token": { "type": "string" } + }, + "required": ["access_token"] + }, + "bedrock": { + "type": "object", + "properties": { + "access_key_id": { "type": "string" }, + "secret_access_key": { "type": "string" }, + "session_token": { "type": "string" }, + "region": { "type": "string", "default": "us-east-1" } + }, + "required": ["access_key_id", "secret_access_key", "region"] + }, + "ccr": { + "type": "object", + "properties": { + "api_key": { "type": "string" }, + "base_url": { "type": "string" } + }, + "required": ["api_key", "base_url"] + } + } + }, + + "binary": { + "binary_name": "claude-provider-cli", + "github_owner": "aiclientproxy", + "github_repo": "claude-provider", + "platform_binaries": { + "macos-arm64": "claude-provider-aarch64-apple-darwin", + "macos-x64": "claude-provider-x86_64-apple-darwin", + "linux-x64": "claude-provider-x86_64-unknown-linux-gnu", + "windows-x64": "claude-provider-x86_64-pc-windows-msvc.exe" + }, + "checksum_file": "checksums.txt" + }, + + "ui": { + "surfaces": ["oauth_providers"], + "icon": "MessageSquare", + "title": "Claude Provider", + "entry": "dist/index.js", + "styles": "dist/styles.css", + "default_width": 950, + "default_height": 750, + "permissions": [ + "database:read", + "database:write", + "http:request", + "crypto:encrypt", + "shell:open" + ] + } +} +``` + +### 2.3 config.json + +```json +{ + "enabled": true, + "timeout_ms": 60000, + "settings": { + "oauth": { + "client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e", + "auth_url": "https://claude.ai/oauth/authorize", + "token_url": "https://console.anthropic.com/v1/oauth/token", + "redirect_uri": "https://console.anthropic.com/oauth/code/callback", + "scopes": "org:create_api_key user:profile user:inference", + "scopes_setup": "user:inference" + }, + "api": { + "base_url": "https://api.anthropic.com", + "version": "2023-06-01" + }, + "bedrock": { + "default_region": "us-east-1", + "model_prefix": "us.anthropic." + }, + "token_refresh": { + "auto_refresh": true, + "refresh_threshold_minutes": 5, + "max_retry": 3, + "retry_delay_ms": 1000 + }, + "encryption": { + "algorithm": "aes-256-cbc", + "key_derivation": "pbkdf2" + } + } +} +``` + +--- + +## 三、认证方式详解 + +### 3.1 OAuth 认证(标准 Claude.ai) + +#### OAuth 配置 + +```rust +const CLAUDE_AUTH_URL: &str = "https://claude.ai/oauth/authorize"; +const CLAUDE_TOKEN_URL: &str = "https://console.anthropic.com/v1/oauth/token"; +const CLAUDE_CLIENT_ID: &str = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; +const CLAUDE_REDIRECT_URI: &str = "https://console.anthropic.com/oauth/code/callback"; +const CLAUDE_SCOPES: &str = "org:create_api_key user:profile user:inference"; +``` + +#### PKCE 流程实现 + +```rust +use sha2::{Sha256, Digest}; +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use rand::Rng; + +/// 生成 OAuth 参数(PKCE) +pub fn generate_oauth_params() -> OAuthParams { + // 1. 生成随机 state + let state: [u8; 32] = rand::thread_rng().gen(); + let state = URL_SAFE_NO_PAD.encode(&state); + + // 2. 生成 code_verifier + let code_verifier: [u8; 32] = rand::thread_rng().gen(); + let code_verifier = URL_SAFE_NO_PAD.encode(&code_verifier); + + // 3. 计算 code_challenge = SHA256(code_verifier) + let mut hasher = Sha256::new(); + hasher.update(code_verifier.as_bytes()); + let code_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize()); + + // 4. 构建授权 URL + let auth_url = format!( + "{}?response_type=code&client_id={}&redirect_uri={}&scope={}&state={}&code_challenge={}&code_challenge_method=S256", + CLAUDE_AUTH_URL, + CLAUDE_CLIENT_ID, + urlencoding::encode(CLAUDE_REDIRECT_URI), + urlencoding::encode(CLAUDE_SCOPES), + state, + code_challenge + ); + + OAuthParams { + auth_url, + code_verifier, + state, + code_challenge, + } +} + +/// 交换授权码获取 Token +pub async fn exchange_authorization_code( + authorization_code: &str, + code_verifier: &str, + state: &str, +) -> Result { + let response = reqwest::Client::new() + .post(CLAUDE_TOKEN_URL) + .json(&json!({ + "client_id": CLAUDE_CLIENT_ID, + "grant_type": "authorization_code", + "code": authorization_code, + "redirect_uri": CLAUDE_REDIRECT_URI, + "code_verifier": code_verifier, + "state": state + })) + .send() + .await?; + + let token_response: TokenResponse = response.json().await?; + + Ok(OAuthTokens { + access_token: token_response.access_token, + refresh_token: token_response.refresh_token, + expires_at: Utc::now() + Duration::seconds(token_response.expires_in), + email: token_response.account.map(|a| a.email_address), + }) +} +``` + +#### Cookie 快速授权 + +```rust +/// 使用 sessionKey 自动完成 OAuth 流程 +pub async fn oauth_with_cookie( + session_key: &str, + is_setup_token: bool, +) -> Result { + let client = reqwest::Client::new(); + + // 1. 获取组织信息 + let orgs_response = client + .get("https://claude.ai/api/organizations") + .header("Cookie", format!("sessionKey={}", session_key)) + .header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)") + .header("Origin", "https://claude.ai") + .header("Referer", "https://claude.ai/new") + .send() + .await?; + + let organizations: Vec = orgs_response.json().await?; + + // 2. 选择具有 chat 能力的组织 + let org = organizations + .iter() + .find(|o| o.capabilities.contains(&"chat".to_string())) + .ok_or(Error::NoValidOrganization)?; + + // 3. 生成 OAuth 参数 + let params = generate_oauth_params(); + let scopes = if is_setup_token { CLAUDE_SCOPES_SETUP } else { CLAUDE_SCOPES }; + + // 4. 使用 Cookie 请求授权码 + let auth_url = format!( + "{}?response_type=code&client_id={}&redirect_uri={}&scope={}&state={}&code_challenge={}&code_challenge_method=S256", + CLAUDE_AUTH_URL, CLAUDE_CLIENT_ID, + urlencoding::encode(CLAUDE_REDIRECT_URI), + urlencoding::encode(scopes), + params.state, params.code_challenge + ); + + let auth_response = client + .get(&auth_url) + .header("Cookie", format!("sessionKey={}", session_key)) + .send() + .await?; + + // 5. 解析回调中的授权码 + let callback_url = auth_response.url().to_string(); + let code = extract_code_from_url(&callback_url)?; + + // 6. 交换 Token + exchange_authorization_code(&code, ¶ms.code_verifier, ¶ms.state).await +} +``` + +### 3.2 Claude Code 认证 + +```rust +/// Claude Code 凭证结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClaudeCodeCredentials { + pub access_token: String, + pub refresh_token: Option, + pub session_key: Option, + pub expires_at: Option>, +} + +/// 默认凭证路径 +const CLAUDE_CODE_CREDS_PATH: &str = "~/.claude/oauth_creds.json"; + +/// 从 Claude Code 配置加载凭证 +pub fn load_claude_code_credentials() -> Result { + let path = expand_tilde(CLAUDE_CODE_CREDS_PATH); + let content = fs::read_to_string(&path)?; + let creds: ClaudeCodeCredentials = serde_json::from_str(&content)?; + Ok(creds) +} +``` + +### 3.3 Console OAuth(企业/团队) + +```rust +/// Console OAuth 配置 +const CONSOLE_AUTH_URL: &str = "https://console.anthropic.com/oauth/authorize"; +const CONSOLE_TOKEN_URL: &str = "https://console.anthropic.com/v1/oauth/token"; + +/// Console 凭证结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsoleCredentials { + pub access_token: String, + pub refresh_token: String, + pub organization_id: Option, + pub organization_name: Option, + pub expires_at: DateTime, +} + +/// Console OAuth 流程(与标准 OAuth 类似,但针对企业账户) +pub async fn console_oauth_flow( + authorization_code: &str, + code_verifier: &str, +) -> Result { + // 与标准 OAuth 类似,但返回组织信息 + let response = exchange_authorization_code(authorization_code, code_verifier).await?; + + Ok(ConsoleCredentials { + access_token: response.access_token, + refresh_token: response.refresh_token, + organization_id: response.organization.map(|o| o.id), + organization_name: response.organization.map(|o| o.name), + expires_at: response.expires_at, + }) +} +``` + +### 3.4 Setup Token(最小权限) + +```rust +/// Setup Token - 只有推理权限,无 refresh_token +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetupTokenCredentials { + pub access_token: String, + pub creds_type: String, // "claude_setup_token" +} + +/// 使用 Cookie 获取 Setup Token +pub async fn get_setup_token(session_key: &str) -> Result { + let tokens = oauth_with_cookie(session_key, true /* is_setup_token */).await?; + + Ok(SetupTokenCredentials { + access_token: tokens.access_token, + creds_type: "claude_setup_token".to_string(), + }) +} +``` + +### 3.5 AWS Bedrock + +```rust +/// AWS Bedrock 凭证结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BedrockCredentials { + pub access_key_id: String, + pub secret_access_key: String, + pub session_token: Option, + pub region: String, + pub default_model: Option, +} + +/// Bedrock 模型映射 +const BEDROCK_MODEL_MAP: &[(&str, &str)] = &[ + ("claude-opus-4-20250514", "us.anthropic.claude-opus-4-20250514-v1:0"), + ("claude-sonnet-4-20250514", "us.anthropic.claude-sonnet-4-20250514-v1:0"), + ("claude-sonnet-4-5-20250929", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"), + ("claude-haiku-3-5-20241022", "us.anthropic.claude-haiku-3-5-20241022-v1:0"), +]; + +/// Bedrock API 调用 +pub async fn call_bedrock_api( + credentials: &BedrockCredentials, + request: &AnthropicRequest, +) -> Result>> { + // 1. 模型名映射 + let model_id = map_to_bedrock_model(&request.model); + + // 2. 构建 AWS 签名 + let aws_credentials = AwsCredentials::new( + &credentials.access_key_id, + &credentials.secret_access_key, + credentials.session_token.as_deref(), + ); + + // 3. 调用 Bedrock API + let url = format!( + "https://bedrock-runtime.{}.amazonaws.com/model/{}/invoke-with-response-stream", + credentials.region, + model_id + ); + + let signed_request = sign_aws_request( + "POST", + &url, + &aws_credentials, + &credentials.region, + "bedrock", + &serde_json::to_vec(request)?, + )?; + + // 4. 发送请求并返回流 + let response = reqwest::Client::new() + .post(&url) + .headers(signed_request.headers) + .body(signed_request.body) + .send() + .await?; + + Ok(parse_bedrock_stream(response.bytes_stream())) +} +``` + +### 3.6 CCR(第三方中转) + +```rust +/// CCR(Custom Claude Relay)凭证结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CCRCredentials { + pub api_key: String, + pub base_url: String, + pub name: Option, +} + +/// CCR API 调用(直接转发) +pub async fn call_ccr_api( + credentials: &CCRCredentials, + request: &AnthropicRequest, +) -> Result>> { + let url = format!("{}/v1/messages", credentials.base_url); + + let response = reqwest::Client::new() + .post(&url) + .header("x-api-key", &credentials.api_key) + .header("anthropic-version", "2023-06-01") + .header("Content-Type", "application/json") + .json(request) + .send() + .await?; + + Ok(parse_anthropic_sse_stream(response.bytes_stream())) +} +``` + +--- + +## 四、Token 刷新机制 + +### 4.1 自动刷新逻辑 + +```rust +impl ClaudeProvider { + /// Token 刷新阈值(提前 5 分钟) + const REFRESH_THRESHOLD: Duration = Duration::minutes(5); + + /// 检查 Token 是否需要刷新 + pub fn needs_refresh(&self, credentials: &OAuthCredentials) -> bool { + if let Some(expires_at) = credentials.expires_at { + let now = Utc::now(); + return now >= expires_at - Self::REFRESH_THRESHOLD; + } + true // 无过期时间则默认需要刷新 + } + + /// 刷新 Token(带重试) + pub async fn refresh_token_with_retry( + &self, + credentials: &mut OAuthCredentials, + max_retries: u32, + ) -> Result<()> { + let mut last_error = None; + + for attempt in 0..max_retries { + match self.refresh_token(credentials).await { + Ok(_) => return Ok(()), + Err(e) => { + last_error = Some(e); + // 指数退避 + let delay = Duration::milliseconds(1000 * 2_i64.pow(attempt)); + tokio::time::sleep(delay.to_std().unwrap()).await; + } + } + } + + Err(last_error.unwrap()) + } + + /// 刷新 Token + async fn refresh_token(&self, credentials: &mut OAuthCredentials) -> Result<()> { + let refresh_token = credentials.refresh_token.as_ref() + .ok_or(Error::MissingRefreshToken)?; + + let response = self.http_client + .post(CLAUDE_TOKEN_URL) + .json(&json!({ + "client_id": CLAUDE_CLIENT_ID, + "grant_type": "refresh_token", + "refresh_token": refresh_token + })) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + return Err(Error::TokenRefreshFailed(error_text)); + } + + let token_response: TokenResponse = response.json().await?; + + // 更新凭证 + credentials.access_token = token_response.access_token; + if let Some(new_refresh) = token_response.refresh_token { + credentials.refresh_token = Some(new_refresh); + } + credentials.expires_at = Some(Utc::now() + Duration::seconds(token_response.expires_in)); + credentials.last_refresh = Some(Utc::now()); + + // 更新邮箱(如果有) + if let Some(account) = token_response.account { + credentials.email = Some(account.email_address); + } + + Ok(()) + } +} +``` + +--- + +## 五、前端 UI 实现 + +### 5.1 插件入口 + +```tsx +// src/index.tsx +import { ProxyCastPluginSDK } from '@proxycast/plugin-sdk'; +import { CredentialList } from './components/CredentialList'; +import { AuthMethodTabs } from './components/AuthMethodTabs'; +import { SettingsPanel } from './components/SettingsPanel'; + +interface PluginProps { + sdk: ProxyCastPluginSDK; + pluginId: string; +} + +export default function ClaudeProviderUI({ sdk, pluginId }: PluginProps) { + const [view, setView] = useState<'list' | 'add' | 'settings'>('list'); + const [credentials, setCredentials] = useState([]); + + useEffect(() => { + loadCredentials(); + }, []); + + const loadCredentials = async () => { + const result = await sdk.database.query( + 'SELECT * FROM plugin_credentials WHERE plugin_id = ? ORDER BY created_at DESC', + [pluginId] + ); + setCredentials(result); + }; + + return ( +
+
+ Claude Provider + 支持 OAuth、Claude Code、Console、Bedrock、CCR + + + + +
+ + {view === 'list' && ( + + )} + + {view === 'add' && ( + { + loadCredentials(); + setView('list'); + }} + onCancel={() => setView('list')} + /> + )} + + {view === 'settings' && ( + setView('list')} + /> + )} +
+ ); +} +``` + +### 5.2 认证方式选择 + +```tsx +// src/components/AuthMethodTabs.tsx + +type AuthMethod = 'oauth' | 'claude_code' | 'console' | 'setup_token' | 'bedrock' | 'ccr'; + +interface AuthMethodTabsProps { + sdk: ProxyCastPluginSDK; + onSuccess: () => void; + onCancel: () => void; +} + +export function AuthMethodTabs({ sdk, onSuccess, onCancel }: AuthMethodTabsProps) { + const [method, setMethod] = useState('oauth'); + + return ( +
+ + + + OAuth + + + + Claude Code + + + + Console + + + + Setup Token + + + + Bedrock + + + + CCR + + + +
+ {method === 'oauth' && } + {method === 'claude_code' && } + {method === 'console' && } + {method === 'setup_token' && } + {method === 'bedrock' && } + {method === 'ccr' && } +
+ + + + +
+ ); +} +``` + +### 5.3 OAuth 表单 + +```tsx +// src/components/OAuthForm.tsx + +type OAuthMode = 'browser' | 'cookie' | 'file'; + +export function OAuthForm({ sdk, onSuccess }: FormProps) { + const [mode, setMode] = useState('cookie'); + const [sessionKey, setSessionKey] = useState(''); + const [authUrl, setAuthUrl] = useState(''); + const [loading, setLoading] = useState(false); + + const handleCookieAuth = async () => { + setLoading(true); + try { + await sdk.http.request('/api/claude/oauth/cookie', { + method: 'POST', + body: JSON.stringify({ sessionKey, isSetupToken: false }), + }); + sdk.notification.success('OAuth 认证成功'); + onSuccess(); + } catch (error) { + sdk.notification.error(`认证失败: ${error.message}`); + } finally { + setLoading(false); + } + }; + + const handleBrowserAuth = async () => { + setLoading(true); + try { + const result = await sdk.http.request('/api/claude/oauth/start'); + setAuthUrl(result.authUrl); + await sdk.shell.open(result.authUrl); + + // 等待回调 + const credential = await sdk.http.request('/api/claude/oauth/callback/wait', { + timeout: 120000, + }); + sdk.notification.success('OAuth 认证成功'); + onSuccess(); + } catch (error) { + sdk.notification.error(`认证失败: ${error.message}`); + } finally { + setLoading(false); + } + }; + + return ( +
+ + Cookie 快速授权 + 浏览器授权 + 导入文件 + + + {mode === 'cookie' && ( +
+ + +