mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
41 KiB
41 KiB
Gemini Provider 插件文档
版本: 1.0.0 仓库:
aiclientproxy/gemini-provider类型: OAuth Provider Plugin
一、概述
1.1 插件简介
Gemini Provider 是 Lime 的 Google Gemini 凭证提供者插件,支持 OAuth 2.0 + PKCE(Gemini CLI 兼容)和 API Key(Google AI Studio)两种认证方式。插件通过 Google Code Assist API 访问 Gemini 模型,支持个人 Google 账户和 Google Cloud/Workspace 企业账户。
1.2 支持的认证方式
| 认证方式 | 说明 | 适用场景 |
|---|---|---|
| OAuth | Google OAuth 2.0 + PKCE | 个人/企业 Google 账户 |
| API Key | Google AI Studio API Key | 标准 API 访问 |
1.3 核心能力
| 能力 | 说明 |
|---|---|
| 双重认证支持 | OAuth 和 API Key 两种模式 |
| 自动 Token 刷新 | OAuth 模式自动刷新,提前 5 分钟 |
| PKCE 安全 | OAuth 使用 S256 PKCE 流程 |
| Project ID 自动发现 | 通过 loadCodeAssist 获取临时 projectId |
| 请求格式转换 | OpenAI Chat Completion → Gemini Content |
| 代理支持 | HTTP/HTTPS/SOCKS5 代理 + TCP Keep-Alive |
| 凭证加密 | AES-256-CBC + scrypt + LRU 缓存 |
1.4 支持的模型
| 模型系列 | 示例模型 | 说明 |
|---|---|---|
| Gemini 2.5 | gemini-2.5-pro, gemini-2.5-flash |
最新旗舰/快速模型 |
| Gemini 2.0 | gemini-2.0-flash-exp |
实验性 Flash 模型 |
| Gemini 3 | gemini-3-pro-preview |
预览版 |
二、插件架构
2.1 项目结构
gemini-provider/
├── plugin/
│ ├── plugin.json # 插件元数据
│ └── config.json # 默认配置
│
├── src-tauri/src/ # 后端 Rust 代码
│ ├── lib.rs # 插件入口
│ ├── commands.rs # Tauri 命令
│ ├── provider.rs # GeminiProvider 核心实现
│ ├── auth/ # 认证模块
│ │ ├── mod.rs
│ │ ├── oauth.rs # Google OAuth 2.0 + PKCE
│ │ └── api_key.rs # API Key 模式
│ ├── credentials.rs # 凭证管理
│ ├── token_refresh.rs # Token 刷新
│ ├── code_assist.rs # Code Assist API 封装
│ ├── transform.rs # 请求格式转换
│ └── api/ # API 调用
│ ├── mod.rs
│ ├── code_assist.rs # Code Assist API
│ └── generative.rs # Generative Language API
│
├── src/ # 前端 React UI
│ ├── index.tsx # 插件 UI 入口
│ ├── components/
│ │ ├── CredentialList.tsx # 凭证列表
│ │ ├── CredentialCard.tsx # 凭证卡片
│ │ ├── AuthMethodTabs.tsx # 认证方式选择
│ │ ├── OAuthForm.tsx # OAuth 表单
│ │ ├── ApiKeyForm.tsx # API Key 表单
│ │ ├── ImportForm.tsx # 导入表单
│ │ └── SettingsPanel.tsx # 插件设置
│ └── types/
│ └── index.ts # 类型定义
│
└── .github/
└── workflows/
└── release.yml # 自动构建发布
2.2 plugin.json
{
"name": "gemini-provider",
"version": "1.0.0",
"description": "Gemini Provider - 支持 OAuth 和 API Key 两种认证方式访问 Google Gemini 模型",
"author": "Lime Team",
"homepage": "https://github.com/aiclientproxy/gemini-provider",
"license": "MIT",
"plugin_type": "oauth_provider",
"entry": "gemini-provider-cli",
"min_lime_version": "1.0.0",
"provider": {
"id": "gemini",
"display_name": "Gemini (Google)",
"target_protocol": "gemini",
"supported_models": ["gemini-*"],
"auth_types": ["oauth", "api_key"],
"credential_schemas": {
"oauth": {
"type": "object",
"properties": {
"access_token": { "type": "string" },
"refresh_token": { "type": "string" },
"expiry_date": { "type": "integer" },
"expire": { "type": "string" },
"scope": { "type": "string" },
"email": { "type": "string" },
"project_id": { "type": "string" },
"temp_project_id": { "type": "string" }
},
"required": ["access_token"]
},
"api_key": {
"type": "object",
"properties": {
"api_key": { "type": "string" },
"base_url": { "type": "string" }
},
"required": ["api_key"]
}
}
},
"binary": {
"binary_name": "gemini-provider-cli",
"github_owner": "aiclientproxy",
"github_repo": "gemini-provider",
"platform_binaries": {
"macos-arm64": "gemini-provider-aarch64-apple-darwin",
"macos-x64": "gemini-provider-x86_64-apple-darwin",
"linux-x64": "gemini-provider-x86_64-unknown-linux-gnu",
"windows-x64": "gemini-provider-x86_64-pc-windows-msvc.exe"
},
"checksum_file": "checksums.txt"
},
"ui": {
"surfaces": ["oauth_providers"],
"icon": "Sparkles",
"title": "Gemini 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
{
"enabled": true,
"timeout_ms": 600000,
"settings": {
"oauth": {
"client_id": "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
"client_secret": "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",
"scopes": ["https://www.googleapis.com/auth/cloud-platform"],
"redirect_uri": "https://codeassist.google.com/authcode",
"token_url": "https://oauth2.googleapis.com/token",
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth"
},
"api": {
"code_assist_endpoint": "https://cloudcode-pa.googleapis.com",
"code_assist_version": "v1internal",
"generative_endpoint": "https://generativelanguage.googleapis.com/v1beta"
},
"token_refresh": {
"auto_refresh": true,
"refresh_threshold_minutes": 5,
"max_retry": 3,
"retry_delay_ms": 1000
},
"network": {
"keep_alive": true,
"keep_alive_msecs": 30000,
"timeout": 120000,
"max_sockets": 100,
"max_free_sockets": 10
}
}
}
三、认证方式详解
3.1 OAuth 认证(Google OAuth 2.0 + PKCE)
OAuth 配置
// Gemini CLI OAuth 配置 - 公开的 Gemini CLI 凭据
const OAUTH_CLIENT_ID: &str =
"681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
const OAUTH_CLIENT_SECRET: &str = "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";
const OAUTH_SCOPES: &[&str] = &["https://www.googleapis.com/auth/cloud-platform"];
const OAUTH_REDIRECT_URI: &str = "https://codeassist.google.com/authcode";
const OAUTH_TOKEN_URL: &str = "https://oauth2.googleapis.com/token";
const OAUTH_AUTH_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth";
PKCE 流程实现
use sha2::{Sha256, Digest};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
/// 生成 PKCE code_verifier 和 code_challenge
fn generate_pkce() -> (String, String) {
// 生成 43-128 字符的随机字符串作为 code_verifier
let code_verifier: String = (0..64)
.map(|_| {
let idx = rand::random::<u8>() % 66;
let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
chars[idx as usize] as char
})
.collect();
// 计算 code_challenge = BASE64URL(SHA256(code_verifier))
let mut hasher = Sha256::new();
hasher.update(code_verifier.as_bytes());
let hash = hasher.finalize();
let code_challenge = URL_SAFE_NO_PAD.encode(hash);
(code_verifier, code_challenge)
}
/// 生成 OAuth 授权 URL(使用 PKCE)
pub fn generate_auth_url(state: &str, code_challenge: &str) -> String {
let scopes = OAUTH_SCOPES.join(" ");
let params = [
("access_type", "offline"),
("client_id", OAUTH_CLIENT_ID),
("code_challenge", code_challenge),
("code_challenge_method", "S256"),
("prompt", "select_account"),
("redirect_uri", OAUTH_REDIRECT_URI),
("response_type", "code"),
("scope", &scopes),
("state", state),
];
let query = params
.iter()
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&");
format!("{}?{}", OAUTH_AUTH_URL, query)
}
Token 交换
/// 交换授权码获取 tokens (支持 PKCE)
pub async fn exchange_code_for_tokens(
code: &str,
redirect_uri: &str,
code_verifier: &str,
) -> Result<TokenResponse> {
let params = [
("code", code),
("client_id", OAUTH_CLIENT_ID),
("client_secret", OAUTH_CLIENT_SECRET),
("code_verifier", code_verifier),
("redirect_uri", redirect_uri),
("grant_type", "authorization_code"),
];
let client = reqwest::Client::new();
let resp = client
.post(OAUTH_TOKEN_URL)
.form(¶ms)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(Error::TokenExchangeFailed(status.as_u16(), body));
}
let data: serde_json::Value = resp.json().await?;
Ok(TokenResponse {
access_token: data["access_token"].as_str().unwrap().to_string(),
refresh_token: data["refresh_token"].as_str().map(String::from),
scope: data["scope"].as_str().map(String::from),
token_type: data["token_type"].as_str().unwrap_or("Bearer").to_string(),
expiry_date: data["expires_in"]
.as_i64()
.map(|secs| Utc::now().timestamp_millis() + secs * 1000),
})
}
获取用户信息
/// 获取用户邮箱
pub async fn fetch_user_email(access_token: &str) -> Result<Option<String>> {
let resp = reqwest::Client::new()
.get("https://www.googleapis.com/oauth2/v2/userinfo")
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
if resp.status().is_success() {
let data: serde_json::Value = resp.json().await?;
Ok(data["email"].as_str().map(String::from))
} else {
Ok(None)
}
}
3.2 API Key 认证
/// Gemini API Key Provider
pub struct GeminiApiKeyCredential {
/// API Key
pub api_key: String,
/// Custom base URL (optional)
pub base_url: Option<String>,
/// Excluded models (supports wildcards)
pub excluded_models: Vec<String>,
/// Whether this credential is disabled
pub disabled: bool,
}
impl GeminiApiKeyCredential {
/// Get the effective base URL
pub fn get_base_url(&self) -> &str {
self.base_url.as_deref()
.unwrap_or("https://generativelanguage.googleapis.com")
}
/// Build the API URL for a given model and action
pub fn build_api_url(&self, model: &str, action: &str) -> String {
format!(
"{}/v1beta/models/{}:{}",
self.get_base_url(),
model,
action
)
}
/// Check if this credential supports the given model
pub fn supports_model(&self, model: &str) -> bool {
!self.excluded_models.iter().any(|pattern| {
if pattern.contains('*') {
let regex_pattern = pattern.replace('*', ".*");
regex::Regex::new(&format!("^{}$", regex_pattern))
.map(|re| re.is_match(model))
.unwrap_or(false)
} else {
pattern == model
}
})
}
}
四、Code Assist API
4.1 API 端点
const CODE_ASSIST_ENDPOINT: &str = "https://cloudcode-pa.googleapis.com";
const CODE_ASSIST_API_VERSION: &str = "v1internal";
4.2 loadCodeAssist - 获取 Project ID
/// 调用 loadCodeAssist 获取用户配置和 Project ID
pub async fn load_code_assist(
access_token: &str,
project_id: Option<&str>,
) -> Result<LoadCodeAssistResponse> {
// 对于个人账户(无 projectId),先调用 tokeninfo/userinfo
// 帮助 Google 获取临时 projectId
if project_id.is_none() {
// 验证 token
let _ = reqwest::Client::new()
.post("https://oauth2.googleapis.com/tokeninfo")
.header("Authorization", format!("Bearer {}", access_token))
.header("Content-Type", "application/x-www-form-urlencoded")
.form(&[("access_token", access_token)])
.send()
.await;
// 获取用户信息
let _ = reqwest::Client::new()
.get("https://www.googleapis.com/oauth2/v2/userinfo")
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await;
}
// 构建请求
let mut request = json!({
"metadata": {
"ideType": "IDE_UNSPECIFIED",
"platform": "PLATFORM_UNSPECIFIED",
"pluginType": "GEMINI"
}
});
if let Some(pid) = project_id {
request["cloudaicompanionProject"] = json!(pid);
request["metadata"]["duetProject"] = json!(pid);
}
let resp = reqwest::Client::new()
.post(format!(
"{}/{}:loadCodeAssist",
CODE_ASSIST_ENDPOINT, CODE_ASSIST_API_VERSION
))
.header("Authorization", format!("Bearer {}", access_token))
.header("Content-Type", "application/json")
.json(&request)
.send()
.await?;
let data: serde_json::Value = resp.json().await?;
Ok(LoadCodeAssistResponse {
cloud_ai_companion_project: data["cloudaicompanionProject"]
.as_str()
.map(String::from),
current_tier: data.get("currentTier").cloned(),
allowed_tiers: data.get("allowedTiers").cloned(),
})
}
4.3 onboardUser - 用户注册
/// 用户层级枚举
#[derive(Debug, Clone)]
pub enum UserTier {
Legacy,
Free,
Pro,
}
/// 获取 onboard 层级
fn get_onboard_tier(load_res: &LoadCodeAssistResponse) -> UserTier {
if let Some(current) = &load_res.current_tier {
return match current["id"].as_str() {
Some("PRO") => UserTier::Pro,
Some("FREE") => UserTier::Free,
_ => UserTier::Legacy,
};
}
if let Some(tiers) = &load_res.allowed_tiers {
if let Some(arr) = tiers.as_array() {
for tier in arr {
if tier["isDefault"].as_bool().unwrap_or(false) {
return match tier["id"].as_str() {
Some("PRO") => UserTier::Pro,
Some("FREE") => UserTier::Free,
_ => UserTier::Legacy,
};
}
}
}
}
UserTier::Legacy
}
/// 调用 onboardUser(包含轮询逻辑)
pub async fn onboard_user(
access_token: &str,
tier_id: &str,
project_id: Option<&str>,
) -> Result<OnboardResponse> {
let mut request = json!({
"tierId": tier_id,
"metadata": {
"ideType": "IDE_UNSPECIFIED",
"platform": "PLATFORM_UNSPECIFIED",
"pluginType": "GEMINI"
}
});
if let Some(pid) = project_id {
request["cloudaicompanionProject"] = json!(pid);
}
let client = reqwest::Client::new();
let url = format!(
"{}/{}:onboardUser",
CODE_ASSIST_ENDPOINT, CODE_ASSIST_API_VERSION
);
// 轮询直到长运行操作完成
let mut lro_resp: serde_json::Value;
let mut attempts = 0;
const MAX_ATTEMPTS: u32 = 12; // 最多等待 1 分钟
loop {
let resp = client
.post(&url)
.header("Authorization", format!("Bearer {}", access_token))
.header("Content-Type", "application/json")
.json(&request)
.send()
.await?;
lro_resp = resp.json().await?;
if lro_resp["done"].as_bool().unwrap_or(false) {
break;
}
attempts += 1;
if attempts >= MAX_ATTEMPTS {
return Err(Error::OnboardTimeout);
}
tracing::info!("Waiting for onboardUser... ({}/{})", attempts, MAX_ATTEMPTS);
tokio::time::sleep(Duration::from_secs(5)).await;
}
Ok(OnboardResponse {
project_id: lro_resp["response"]["cloudaicompanionProject"]["id"]
.as_str()
.map(String::from),
})
}
4.4 generateContent - 生成内容
/// 调用 generateContent(流式)
pub async fn stream_generate_content(
access_token: &str,
request: &GeminiRequest,
project_id: Option<&str>,
session_id: Option<&str>,
) -> Result<impl Stream<Item = Result<Bytes>>> {
let mut body = json!({
"model": request.model,
"request": request.request
});
if let Some(sid) = session_id {
body["request"]["session_id"] = json!(sid);
}
if let Some(pid) = project_id {
body["project"] = json!(pid);
}
let url = format!(
"{}/{}:streamGenerateContent?alt=sse",
CODE_ASSIST_ENDPOINT, CODE_ASSIST_API_VERSION
);
let resp = reqwest::Client::new()
.post(&url)
.header("Authorization", format!("Bearer {}", access_token))
.header("Content-Type", "application/json")
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(Error::ApiError(status.as_u16(), body));
}
Ok(resp.bytes_stream())
}
五、Token 刷新机制
5.1 Token 有效性检查
impl GeminiProvider {
/// 检查 Token 是否有效
pub fn is_token_valid(&self) -> bool {
if self.credentials.access_token.is_none() {
return false;
}
// 优先检查 RFC3339 格式的过期时间
if let Some(expire_str) = &self.credentials.expire {
if let Ok(expires) = DateTime::parse_from_rfc3339(expire_str) {
let now = Utc::now();
// Token 有效期需要超过 5 分钟
return expires > now + Duration::minutes(5);
}
}
// 兼容旧的毫秒时间戳格式
if let Some(expiry) = self.credentials.expiry_date {
let now = Utc::now().timestamp_millis();
return expiry > now + 300_000; // 5 分钟
}
true
}
}
5.2 Token 刷新
/// 刷新访问令牌
pub async fn refresh_access_token(
refresh_token: &str,
) -> Result<TokenResponse> {
let params = [
("client_id", OAUTH_CLIENT_ID),
("client_secret", OAUTH_CLIENT_SECRET),
("refresh_token", refresh_token),
("grant_type", "refresh_token"),
];
let resp = reqwest::Client::new()
.post(OAUTH_TOKEN_URL)
.form(¶ms)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(Error::TokenRefreshFailed(status.as_u16(), body));
}
let data: serde_json::Value = resp.json().await?;
Ok(TokenResponse {
access_token: data["access_token"]
.as_str()
.ok_or(Error::MissingAccessToken)?
.to_string(),
refresh_token: data["refresh_token"].as_str().map(String::from),
expiry_date: data["expires_in"]
.as_i64()
.map(|secs| Utc::now().timestamp_millis() + secs * 1000),
..Default::default()
})
}
5.3 分布式锁防止并发刷新
/// 获取刷新锁
pub async fn acquire_refresh_lock(
account_id: &str,
platform: &str,
) -> Result<bool> {
let lock_key = format!("token_refresh_lock:{}:{}", platform, account_id);
// 使用 Redis SETNX 实现分布式锁
let result: bool = redis_client
.set_nx(&lock_key, "1", Duration::from_secs(30))
.await?;
Ok(result)
}
/// 释放刷新锁
pub async fn release_refresh_lock(
account_id: &str,
platform: &str,
) -> Result<()> {
let lock_key = format!("token_refresh_lock:{}:{}", platform, account_id);
redis_client.del(&lock_key).await?;
Ok(())
}
/// 带分布式锁的 Token 刷新
pub async fn refresh_account_token(account_id: &str) -> Result<TokenResponse> {
let mut lock_acquired = false;
// 尝试获取锁
lock_acquired = acquire_refresh_lock(account_id, "gemini").await?;
if !lock_acquired {
tracing::info!(
"Token refresh already in progress for account: {}",
account_id
);
// 等待其他进程完成
tokio::time::sleep(Duration::from_secs(2)).await;
// 返回更新后的凭证
return get_account_credentials(account_id).await;
}
let result = async {
let account = get_account(account_id).await?;
let refresh_token = account.refresh_token
.ok_or(Error::NoRefreshToken)?;
let new_tokens = refresh_access_token(&refresh_token).await?;
// 更新账户
update_account(account_id, &new_tokens).await?;
Ok(new_tokens)
}.await;
// 释放锁
if lock_acquired {
let _ = release_refresh_lock(account_id, "gemini").await;
}
result
}
六、请求格式转换
6.1 OpenAI → Gemini 转换
/// 转换 OpenAI 消息格式到 Gemini 格式
pub fn convert_messages_to_gemini(
messages: &[Message],
) -> (Vec<GeminiContent>, Option<String>) {
let mut contents = Vec::new();
let mut system_instruction = String::new();
for message in messages {
match message.role.as_str() {
"system" => {
if !system_instruction.is_empty() {
system_instruction.push_str("\n\n");
}
system_instruction.push_str(&message.content);
}
"user" => {
contents.push(GeminiContent {
role: "user".to_string(),
parts: vec![GeminiPart {
text: Some(message.content.clone()),
}],
});
}
"assistant" => {
contents.push(GeminiContent {
role: "model".to_string(),
parts: vec![GeminiPart {
text: Some(message.content.clone()),
}],
});
}
_ => {}
}
}
let system = if system_instruction.is_empty() {
None
} else {
Some(system_instruction)
};
(contents, system)
}
6.2 Gemini → OpenAI 转换
/// 转换 Gemini 响应到 OpenAI 格式
pub fn convert_gemini_response(
gemini_response: &GeminiResponse,
model: &str,
stream: bool,
) -> Option<ChatCompletionResponse> {
let candidate = gemini_response.candidates.as_ref()?.first()?;
if stream {
// 流式响应
let content = candidate.content.as_ref()?
.parts.first()?
.text.clone()
.unwrap_or_default();
let finish_reason = candidate.finish_reason.as_ref()
.map(|r| r.to_lowercase())
.filter(|r| r == "stop")
.map(|_| "stop".to_string());
Some(ChatCompletionResponse {
id: format!("chatcmpl-{}", Utc::now().timestamp_millis()),
object: "chat.completion.chunk".to_string(),
created: Utc::now().timestamp(),
model: model.to_string(),
choices: vec![Choice {
index: 0,
delta: Some(Delta { content: Some(content) }),
message: None,
finish_reason,
}],
usage: None,
})
} else {
// 非流式响应
let content = candidate.content.as_ref()?
.parts.first()?
.text.clone()
.unwrap_or_default();
let usage = gemini_response.usage_metadata.as_ref().map(|u| Usage {
prompt_tokens: u.prompt_token_count.unwrap_or(0),
completion_tokens: u.candidates_token_count.unwrap_or(0),
total_tokens: u.total_token_count.unwrap_or(0),
});
Some(ChatCompletionResponse {
id: format!("chatcmpl-{}", Utc::now().timestamp_millis()),
object: "chat.completion".to_string(),
created: Utc::now().timestamp(),
model: model.to_string(),
choices: vec![Choice {
index: 0,
message: Some(Message {
role: "assistant".to_string(),
content,
}),
delta: None,
finish_reason: Some("stop".to_string()),
}],
usage,
})
}
}
七、前端 UI 实现
7.1 插件入口
// src/index.tsx
import { LimePluginSDK } from '@lime/plugin-sdk';
import { CredentialList } from './components/CredentialList';
import { AuthMethodTabs } from './components/AuthMethodTabs';
import { SettingsPanel } from './components/SettingsPanel';
interface PluginProps {
sdk: LimePluginSDK;
pluginId: string;
}
export default function GeminiProviderUI({ sdk, pluginId }: PluginProps) {
const [view, setView] = useState<'list' | 'add' | 'settings'>('list');
const [credentials, setCredentials] = useState<Credential[]>([]);
useEffect(() => {
loadCredentials();
}, []);
const loadCredentials = async () => {
const result = await sdk.database.query<Credential>(
'SELECT * FROM plugin_credentials WHERE plugin_id = ? ORDER BY created_at DESC',
[pluginId]
);
setCredentials(result);
};
return (
<div className="gemini-provider-ui">
<Header>
<Title>Gemini Provider</Title>
<Subtitle>支持 OAuth 和 API Key 两种认证方式</Subtitle>
<Actions>
<Button onClick={() => setView('add')}>添加凭证</Button>
<Button onClick={() => setView('settings')}>设置</Button>
</Actions>
</Header>
{view === 'list' && (
<CredentialList
credentials={credentials}
onRefresh={loadCredentials}
sdk={sdk}
/>
)}
{view === 'add' && (
<AuthMethodTabs
sdk={sdk}
onSuccess={() => {
loadCredentials();
setView('list');
}}
onCancel={() => setView('list')}
/>
)}
{view === 'settings' && (
<SettingsPanel
sdk={sdk}
pluginId={pluginId}
onClose={() => setView('list')}
/>
)}
</div>
);
}
7.2 OAuth 表单
// src/components/OAuthForm.tsx
export function OAuthForm({ sdk, onSuccess }: FormProps) {
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState<'idle' | 'waiting' | 'success' | 'error'>('idle');
const [authUrl, setAuthUrl] = useState('');
const handleOAuthLogin = async () => {
setLoading(true);
setStatus('waiting');
try {
// 1. 获取授权 URL
const result = await sdk.http.request('/api/gemini/oauth/start');
setAuthUrl(result.authUrl);
// 2. 打开浏览器
await sdk.shell.open(result.authUrl);
// 3. 等待回调
const credential = await sdk.http.request('/api/gemini/oauth/callback/wait', {
timeout: 300000, // 5 分钟超时
});
setStatus('success');
sdk.notification.success(
`OAuth 认证成功: ${credential.email || '未知邮箱'}`
);
onSuccess();
} catch (error) {
setStatus('error');
sdk.notification.error(`认证失败: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<div className="oauth-form">
<Alert type="info">
<p>使用 Google 账户进行 OAuth 授权</p>
<p className="text-sm text-gray-500">
支持个人 Google 账户和 Google Cloud/Workspace 企业账户
</p>
</Alert>
{status === 'waiting' && (
<div className="waiting-status">
<Spinner />
<p>正在等待浏览器授权...</p>
<p className="text-sm">请在浏览器中完成 Google 账号登录</p>
</div>
)}
{authUrl && (
<div className="auth-url">
<Label>授权 URL(如果浏览器未自动打开)</Label>
<div className="flex gap-2">
<Input value={authUrl} readOnly className="flex-1" />
<Button
size="small"
onClick={() => {
navigator.clipboard.writeText(authUrl);
sdk.notification.success('已复制到剪贴板');
}}
>
复制
</Button>
</div>
</div>
)}
<Button
onClick={handleOAuthLogin}
loading={loading}
disabled={status === 'waiting'}
className="google-signin-btn"
>
<GoogleIcon />
{status === 'waiting' ? '等待授权中...' : '使用 Google 账号登录'}
</Button>
</div>
);
}
7.3 API Key 表单
// src/components/ApiKeyForm.tsx
export function ApiKeyForm({ sdk, onSuccess }: FormProps) {
const [form, setForm] = useState({
apiKey: '',
baseUrl: '',
name: '',
});
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
setLoading(true);
try {
// 测试 API Key
const testResult = await sdk.http.request('/api/gemini/apikey/test', {
method: 'POST',
body: JSON.stringify({
apiKey: form.apiKey,
baseUrl: form.baseUrl || undefined,
}),
});
if (!testResult.success) {
throw new Error(testResult.error || 'API Key 验证失败');
}
// 保存凭证
await sdk.http.request('/api/gemini/apikey/add', {
method: 'POST',
body: JSON.stringify(form),
});
sdk.notification.success('API Key 添加成功');
onSuccess();
} catch (error) {
sdk.notification.error(`添加失败: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<div className="api-key-form">
<FormField>
<Label>凭证名称(可选)</Label>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="我的 Gemini API Key"
/>
</FormField>
<FormField>
<Label>API Key *</Label>
<Input
type="password"
value={form.apiKey}
onChange={(e) => setForm({ ...form, apiKey: e.target.value })}
placeholder="AIzaSy..."
/>
<HelpText>
从 <a href="https://aistudio.google.com/app/apikey" target="_blank">
Google AI Studio
</a> 获取 API Key
</HelpText>
</FormField>
<FormField>
<Label>Base URL(可选)</Label>
<Input
value={form.baseUrl}
onChange={(e) => setForm({ ...form, baseUrl: e.target.value })}
placeholder="https://generativelanguage.googleapis.com"
/>
<HelpText>留空使用官方 API</HelpText>
</FormField>
<Button onClick={handleSubmit} loading={loading} disabled={!form.apiKey}>
添加凭证
</Button>
</div>
);
}
八、凭证文件格式
8.1 OAuth 凭证
{
"access_token": "ya29.a0AfH6...",
"refresh_token": "1//0g...",
"token_type": "Bearer",
"expiry_date": 1704067200000,
"expire": "2025-01-01T12:00:00Z",
"scope": "https://www.googleapis.com/auth/cloud-platform",
"email": "user@gmail.com",
"last_refresh": "2025-01-01T10:00:00Z",
"type": "gemini",
"project_id": null,
"temp_project_id": "cloudai-companion-xxxx"
}
8.2 API Key 凭证
{
"api_key": "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"base_url": "https://generativelanguage.googleapis.com",
"type": "gemini_api_key"
}
8.3 嵌套 Token 格式(兼容)
{
"token": {
"access_token": "ya29.a0AfH6...",
"refresh_token": "1//0g...",
"token_uri": "https://oauth2.googleapis.com/token",
"client_id": "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
"client_secret": "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",
"scopes": ["https://www.googleapis.com/auth/cloud-platform"]
},
"type": "gemini"
}
九、凭证加密
9.1 加密实现
use aes::cipher::{BlockEncrypt, BlockDecrypt, KeyInit};
use aes::Aes256;
use crypto::scrypt::{scrypt, ScryptParams};
const ALGORITHM: &str = "aes-256-cbc";
const ENCRYPTION_SALT: &[u8] = b"gemini-account-salt";
const IV_LENGTH: usize = 16;
// 缓存派生的加密密钥,避免每次重复计算
static ENCRYPTION_KEY_CACHE: OnceCell<[u8; 32]> = OnceCell::new();
/// 生成加密密钥
fn generate_encryption_key(master_key: &str) -> &'static [u8; 32] {
ENCRYPTION_KEY_CACHE.get_or_init(|| {
let mut key = [0u8; 32];
let params = ScryptParams::new(14, 8, 1).unwrap();
scrypt(master_key.as_bytes(), ENCRYPTION_SALT, ¶ms, &mut key).unwrap();
tracing::info!("Gemini encryption key derived and cached");
key
})
}
/// 加密敏感数据
fn encrypt(text: &str, master_key: &str) -> String {
if text.is_empty() {
return String::new();
}
let key = generate_encryption_key(master_key);
let mut iv = [0u8; IV_LENGTH];
rand::thread_rng().fill_bytes(&mut iv);
let cipher = Aes256Cbc::new_from_slices(key, &iv).unwrap();
let encrypted = cipher.encrypt_vec(text.as_bytes());
format!("{}:{}", hex::encode(iv), hex::encode(encrypted))
}
/// 解密敏感数据(带 LRU 缓存)
fn decrypt(text: &str, master_key: &str, cache: &LruCache<String, String>) -> String {
if text.is_empty() {
return String::new();
}
// 检查缓存
let cache_key = sha256_hash(text);
if let Some(cached) = cache.get(&cache_key) {
return cached.clone();
}
let key = generate_encryption_key(master_key);
let parts: Vec<&str> = text.split(':').collect();
if parts.len() != 2 {
return String::new();
}
let iv = hex::decode(parts[0]).unwrap_or_default();
let encrypted = hex::decode(parts[1]).unwrap_or_default();
let cipher = Aes256Cbc::new_from_slices(key, &iv).unwrap();
let decrypted = cipher.decrypt_vec(&encrypted).unwrap_or_default();
let result = String::from_utf8(decrypted).unwrap_or_default();
// 存入缓存(5分钟过期)
cache.insert(cache_key, result.clone(), Duration::from_secs(300));
result
}
9.2 加密字段
| 字段 | 是否加密 | 说明 |
|---|---|---|
access_token |
✅ 是 | OAuth Access Token |
refresh_token |
✅ 是 | OAuth Refresh Token |
api_key |
✅ 是 | API Key |
email |
❌ 否 | 用户标识 |
project_id |
❌ 否 | 项目 ID |
十、限流处理
10.1 限流状态管理
/// 设置账户限流状态
pub async fn set_account_rate_limited(
account_id: &str,
is_limited: bool,
) -> Result<()> {
let updates = if is_limited {
json!({
"rateLimitStatus": "limited",
"rateLimitedAt": Utc::now().to_rfc3339()
})
} else {
json!({
"rateLimitStatus": "",
"rateLimitedAt": ""
})
};
update_account(account_id, updates).await
}
/// 获取账户限流信息
pub async fn get_account_rate_limit_info(
account_id: &str,
) -> Result<RateLimitInfo> {
let account = get_account(account_id).await?;
if account.rate_limit_status == "limited" {
if let Some(limited_at) = account.rate_limited_at {
let limited_at = DateTime::parse_from_rfc3339(&limited_at)?;
let now = Utc::now();
let minutes_since = (now - limited_at).num_minutes();
// Gemini 限流持续时间为 1 小时
let minutes_remaining = (60 - minutes_since).max(0);
let is_rate_limited = minutes_remaining > 0;
return Ok(RateLimitInfo {
is_rate_limited,
rate_limited_at: Some(limited_at.to_rfc3339()),
minutes_remaining,
rate_limit_end_at: Some(
(limited_at + Duration::hours(1)).to_rfc3339()
),
});
}
}
Ok(RateLimitInfo::default())
}
10.2 检查账户是否被限流
/// 检查账户是否被限流
fn is_rate_limited(account: &Account) -> bool {
if account.rate_limit_status != "limited" {
return false;
}
if let Some(limited_at) = &account.rate_limited_at {
if let Ok(limited_at) = DateTime::parse_from_rfc3339(limited_at) {
let now = Utc::now();
let limit_duration = Duration::hours(1);
return now < limited_at + limit_duration;
}
}
false
}
十一、订阅过期管理
11.1 订阅过期检查
/// 检查账户订阅是否过期
fn is_subscription_expired(account: &Account) -> bool {
if let Some(expires_at) = &account.subscription_expires_at {
if let Ok(expiry) = DateTime::parse_from_rfc3339(expires_at) {
return expiry <= Utc::now();
}
}
// 未设置视为永不过期
false
}
11.2 Token 过期 vs 订阅过期
// Token 过期时间(技术字段,自动刷新)
account.expires_at
// 订阅过期时间(业务字段,手动管理)
account.subscription_expires_at
// 前端显示订阅过期时间
{
"tokenExpiresAt": account.expires_at,
"subscriptionExpiresAt": account.subscription_expires_at,
"expiresAt": account.subscription_expires_at // 前端显示用
}
十二、代理支持
12.1 TCP Keep-Alive Agent
/// TCP Keep-Alive Agent 配置
/// 解决长时间流式请求中 NAT/防火墙空闲超时导致的连接中断问题
pub fn create_keep_alive_agent() -> reqwest::Client {
reqwest::Client::builder()
.tcp_keepalive(Duration::from_secs(30))
.pool_max_idle_per_host(10)
.pool_idle_timeout(Duration::from_secs(120))
.timeout(Duration::from_secs(600))
.build()
.unwrap()
}
12.2 代理配置
/// 创建代理 Agent
pub fn create_proxy_agent(proxy_config: &ProxyConfig) -> Option<reqwest::Proxy> {
match proxy_config.protocol.as_str() {
"http" | "https" => {
let url = format!(
"{}://{}:{}",
proxy_config.protocol,
proxy_config.host,
proxy_config.port
);
let mut proxy = reqwest::Proxy::all(&url).ok()?;
if let (Some(user), Some(pass)) = (&proxy_config.username, &proxy_config.password) {
proxy = proxy.basic_auth(user, pass);
}
Some(proxy)
}
"socks5" => {
let url = format!(
"socks5://{}:{}",
proxy_config.host,
proxy_config.port
);
reqwest::Proxy::all(&url).ok()
}
_ => None,
}
}
十三、错误处理
13.1 错误类型
| 错误类型 | 说明 | 处理方式 |
|---|---|---|
MissingCredentials |
无可用凭证 | 添加凭证 |
TokenRefreshFailed |
Token 刷新失败 | 重试或重新授权 |
InvalidApiKey |
API Key 无效 | 检查 Key 配置 |
ProjectIdRequired |
需要 Project ID | 设置 GOOGLE_CLOUD_PROJECT |
OnboardTimeout |
用户注册超时 | 重试 |
RateLimited |
被限流 | 等待 1 小时 |
SubscriptionExpired |
订阅已过期 | 续费或更换账户 |
13.2 Webhook 通知
/// 发送账户异常 Webhook 通知
async fn send_account_anomaly_notification(
account_id: &str,
account_name: &str,
status: &str,
error_code: &str,
reason: &str,
) {
let payload = json!({
"accountId": account_id,
"accountName": account_name,
"platform": "gemini",
"status": status,
"errorCode": error_code,
"reason": reason,
"timestamp": Utc::now().to_rfc3339()
});
// 发送到配置的 Webhook URL
if let Some(webhook_url) = config.webhook_url {
let _ = reqwest::Client::new()
.post(&webhook_url)
.json(&payload)
.send()
.await;
}
}
十四、开发指南
14.1 本地开发
# 克隆仓库
git clone https://github.com/aiclientproxy/gemini-provider.git
cd gemini-provider
# 安装依赖
pnpm install
cd src-tauri && cargo build
# 前端开发
pnpm dev
# 后端开发
cargo watch -x run
14.2 测试
# 单元测试
cargo test
# OAuth 流程测试
cargo test --test oauth
# API 调用测试
cargo test --test api
# 前端测试
pnpm test
附录
A. 环境变量
| 变量 | 说明 | 默认值 |
|---|---|---|
GOOGLE_CLOUD_PROJECT |
Google Cloud 项目 ID | - |
GEMINI_DEBUG |
调试模式 | false |
GEMINI_TIMEOUT_MS |
请求超时 | 600000 |