Files
proxycast/docs/plugins/antigravity-provider.md
T

37 KiB
Raw Blame History

Antigravity Provider 插件文档

版本: 1.0.0 仓库: aiclientproxy/antigravity-provider 类型: OAuth Provider Plugin


一、概述

1.1 插件简介

Antigravity Provider 是 Lime 的 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

{
  "name": "antigravity-provider",
  "version": "1.0.0",
  "description": "Antigravity (Google Gemini CLI) OAuth Provider - 支持 Gemini 和 Claude 模型",
  "author": "Lime Team",
  "homepage": "https://github.com/aiclientproxy/antigravity-provider",
  "license": "MIT",

  "plugin_type": "oauth_provider",
  "entry": "antigravity-provider-cli",
  "min_lime_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

{
  "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 核心数据结构

凭证结构

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AntigravityCredentials {
    pub access_token: Option<String>,
    pub refresh_token: Option<String>,
    pub token_type: Option<String>,
    pub expiry_date: Option<i64>,         // 毫秒时间戳
    pub expire: Option<String>,           // RFC3339 格式
    pub scope: Option<String>,
    pub last_refresh: Option<String>,     // RFC3339 格式
    pub cred_type: String,                // 默认: "antigravity"
    pub expires_in: Option<i64>,          // 有效期(秒)
    pub timestamp: Option<i64>,           // 获取时间(毫秒)
    pub enable: Option<bool>,
    pub project_id: Option<String>,
    pub email: Option<String>,
}

模型别名映射

/// 用户友好模型名 → 内部 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 动态协议选择

/// 根据模型确定输出协议
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 配置

/// OAuth 2.0 配置(Google)
/// 必须通过环境变量配置:
/// - ANTIGRAVITY_OAUTH_CLIENT_ID
/// - ANTIGRAVITY_OAUTH_CLIENT_SECRET
///
/// 获取方式:
/// 1. 使用 Antigravity CLI 的默认凭据
/// 2. 从 Google Cloud Console 创建 OAuth 2.0 客户端
fn oauth_client_id() -> String {
    std::env::var("ANTIGRAVITY_OAUTH_CLIENT_ID")
        .expect("ANTIGRAVITY_OAUTH_CLIENT_ID environment variable must be set")
}

fn oauth_client_secret() -> String {
    std::env::var("ANTIGRAVITY_OAUTH_CLIENT_SECRET")
        .expect("ANTIGRAVITY_OAUTH_CLIENT_SECRET environment variable must be set")
}

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 刷新

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 client_id = oauth_client_id();
        let client_secret = oauth_client_secret();
        let response = self.http_client
            .post(OAUTH_TOKEN_URL)
            .form(&[
                ("client_id", client_id.as_str()),
                ("client_secret", client_secret.as_str()),
                ("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 安全设置

/// 默认安全设置(关闭内容过滤)
pub fn default_safety_settings() -> Vec<SafetySetting> {
    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

/// 将 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<Content> {
    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 模型)

/// 将 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<AnthropicSseEvent> {
        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 调用

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<impl Stream<Item = Result<GeminiStreamChunk>>> {
        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 插件入口

// src/index.tsx
import { LimePluginSDK } from '@lime/plugin-sdk';
import { CredentialList } from './components/CredentialList';
import { AntigravityForm } from './components/AntigravityForm';
import { SettingsPanel } from './components/SettingsPanel';

interface PluginProps {
  sdk: LimePluginSDK;
  pluginId: string;
}

export default function AntigravityProviderUI({ 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="antigravity-provider-ui">
      <Header>
        <Title>Antigravity Provider</Title>
        <Subtitle>支持 Gemini 3 Pro 和 Claude 模型</Subtitle>
        <Actions>
          <Button onClick={() => setView('add')}>添加凭证</Button>
          <Button onClick={() => setView('settings')}>设置</Button>
        </Actions>
      </Header>

      {/* 动态协议说明 */}
      <InfoBanner>
        <p>此 Provider 支持动态协议选择:</p>
        <ul>
          <li><strong>gemini-*</strong> 模型 → Gemini 协议输出</li>
          <li><strong>claude-*</strong> 模型 → Anthropic 协议输出</li>
        </ul>
      </InfoBanner>

      {view === 'list' && (
        <CredentialList
          credentials={credentials}
          onRefresh={loadCredentials}
          sdk={sdk}
        />
      )}

      {view === 'add' && (
        <AntigravityForm
          sdk={sdk}
          onSuccess={() => {
            loadCredentials();
            setView('list');
          }}
          onCancel={() => setView('list')}
        />
      )}

      {view === 'settings' && (
        <SettingsPanel
          sdk={sdk}
          pluginId={pluginId}
          onClose={() => setView('list')}
        />
      )}
    </div>
  );
}

4.2 凭证添加表单

// src/components/AntigravityForm.tsx

type AddMode = 'oauth' | 'file';

interface AntigravityFormProps {
  sdk: LimePluginSDK;
  onSuccess: () => void;
  onCancel: () => void;
}

export function AntigravityForm({ sdk, onSuccess, onCancel }: AntigravityFormProps) {
  const [mode, setMode] = useState<AddMode>('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 (
    <div className="antigravity-form">
      {/* 模式选择 */}
      <Tabs value={mode} onChange={setMode}>
        <Tab value="oauth">Google OAuth 登录</Tab>
        <Tab value="file">导入凭证文件</Tab>
      </Tabs>

      {/* 通用配置 */}
      <FormField>
        <Label>凭证名称(可选)</Label>
        <Input
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="我的 Antigravity 凭证"
        />
      </FormField>

      <FormField>
        <Label>Project ID(可选)</Label>
        <Input
          value={projectId}
          onChange={(e) => setProjectId(e.target.value)}
          placeholder="自动获取或手动输入"
        />
        <HelpText>留空将自动从 Google 账户获取</HelpText>
      </FormField>

      {mode === 'oauth' && (
        <OAuthMode
          loading={loading}
          onLogin={handleOAuthLogin}
        />
      )}

      {mode === 'file' && (
        <FileMode
          loading={loading}
          onImport={handleFileImport}
        />
      )}

      <FormActions>
        <Button variant="secondary" onClick={onCancel}>取消</Button>
      </FormActions>
    </div>
  );
}

4.3 OAuth 登录模式

// src/components/OAuthLogin.tsx

interface OAuthModeProps {
  loading: boolean;
  onLogin: () => void;
}

export function OAuthMode({ loading, onLogin }: OAuthModeProps) {
  return (
    <div className="oauth-mode">
      <Alert type="info">
        <p>点击下方按钮将打开浏览器进行 Google 账户授权。</p>
        <p>授权完成后,凭证将自动添加。</p>
      </Alert>

      <div className="oauth-scopes">
        <h4>请求的权限:</h4>
        <ul>
          <li>Cloud Platform 访问</li>
          <li>用户邮箱和个人资料</li>
          <li>Cloud Code 日志</li>
        </ul>
      </div>

      <Button
        onClick={onLogin}
        loading={loading}
        disabled={loading}
        icon={<GoogleIcon />}
      >
        {loading ? '等待授权...' : '使用 Google 账户登录'}
      </Button>
    </div>
  );
}

4.4 凭证卡片

// 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 (
    <Card className={`credential-card ${isHealthy ? 'healthy' : 'unhealthy'}`}>
      <CardHeader>
        <div className="status-indicator">
          <StatusDot status={isHealthy ? 'green' : 'red'} />
          <span>{isHealthy ? '健康' : '异常'}</span>
        </div>
        <Badge variant="primary">Google OAuth</Badge>
      </CardHeader>

      <CardBody>
        <div className="info-row">
          <label>名称</label>
          <span>{credential.name || '未命名'}</span>
        </div>
        <div className="info-row">
          <label>邮箱</label>
          <span>{data.email || '-'}</span>
        </div>
        <div className="info-row">
          <label>Project ID</label>
          <span>{data.project_id || '自动'}</span>
        </div>
        <div className="info-row">
          <label>过期时间</label>
          <span>
            {data.expiry_date
              ? formatDate(new Date(data.expiry_date))
              : '-'
            }
          </span>
        </div>
        <div className="info-row">
          <label>上次刷新</label>
          <span>{data.last_refresh || '-'}</span>
        </div>

        {/* 支持的模型 */}
        <div className="supported-models">
          <label>支持的模型</label>
          <div className="model-tags">
            <Tag>Gemini 3 Pro</Tag>
            <Tag>Gemini 2.5 Flash</Tag>
            <Tag>Claude Sonnet 4.5</Tag>
            <Tag>Claude Opus 4.5</Tag>
          </div>
        </div>
      </CardBody>

      <CardFooter>
        <Button size="small" onClick={onRefresh}>刷新 Token</Button>
        <Button size="small" variant="danger" onClick={onDelete}>删除</Button>
      </CardFooter>
    </Card>
  );
}

4.5 设置面板

// src/components/SettingsPanel.tsx

interface SettingsPanelProps {
  sdk: LimePluginSDK;
  pluginId: string;
  onClose: () => void;
}

export function SettingsPanel({ sdk, pluginId, onClose }: SettingsPanelProps) {
  const [config, setConfig] = useState<PluginConfig | null>(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 <Loading />;

  return (
    <div className="settings-panel">
      <h3>插件设置</h3>

      {/* API 环境 */}
      <FormField>
        <Label>API 环境</Label>
        <Select
          value={config?.settings?.api?.environment || 'daily'}
          onChange={(value) => setConfig({
            ...config,
            settings: {
              ...config?.settings,
              api: { ...config?.settings?.api, environment: value },
            },
          })}
        >
          <Option value="daily">Daily(推荐)</Option>
          <Option value="autopush">Autopush</Option>
        </Select>
      </FormField>

      {/* 思维链配置 */}
      <FormField>
        <Label>默认思维深度</Label>
        <Select
          value={config?.settings?.reasoning?.default_effort || 'medium'}
          onChange={(value) => setConfig({
            ...config,
            settings: {
              ...config?.settings,
              reasoning: { ...config?.settings?.reasoning, default_effort: value },
            },
          })}
        >
          <Option value="low">低(1024 tokens)</Option>
          <Option value="medium">中(4096 tokens)</Option>
          <Option value="high">高(16384 tokens)</Option>
        </Select>
        <HelpText>影响 thinking 模型的思考预算</HelpText>
      </FormField>

      {/* 安全设置 */}
      <FormField>
        <Label>安全设置</Label>
        <Checkbox
          checked={config?.settings?.safety_settings?.harassment === 'OFF'}
          onChange={(checked) => {
            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',
                },
              },
            });
          }}
        >
          关闭所有内容过滤
        </Checkbox>
      </FormField>

      <FormActions>
        <Button variant="secondary" onClick={onClose}>取消</Button>
        <Button onClick={saveConfig}>保存设置</Button>
      </FormActions>
    </div>
  );
}

五、凭证文件格式

5.1 单凭证格式

路径: ~/.antigravity/oauth_creds.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)

[
  {
    "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

请求格式:

{
  "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 刷新错误

pub enum TokenRefreshError {
    MissingRefreshToken,
    InvalidRefreshToken,
    NetworkError(String),
    AuthorizationRevoked,
}

八、开发指南

8.1 本地开发

# 克隆仓库
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 测试

# 单元测试
cargo test

# 协议转换测试
cargo test --test converter

# 前端测试
pnpm test

附录

A. 环境变量

变量 说明 默认值
ANTIGRAVITY_ENVIRONMENT API 环境 daily
ANTIGRAVITY_DEBUG 调试模式 false
ANTIGRAVITY_TIMEOUT_MS 请求超时 120000
ANTIGRAVITY_OAUTH_CLIENT_ID OAuth Client ID(必需) 无,必须设置
ANTIGRAVITY_OAUTH_CLIENT_SECRET OAuth Client Secret(必需) 无,必须设置

注意:OAuth 凭据可以从 Antigravity CLI 获取或从 Google Cloud Console 创建。

B. 参考链接