Files
picoclaw/docs/security/ANTIGRAVITY_AUTH.zh.md
T
SiYue e304dce40e docs: sync all documentation to V3 config format
- Replace config/config.example.json with V3 format (version: 3, api_keys array, channel_list)
- Update config-versioning.md: version 2→3, ConfigV2→ConfigV3, CurrentVersion=3
- Update 7 project READMEs: api_key→api_keys, add version: 3 to quick-start examples
- Update 12 security docs (ANTIGRAVITY_AUTH + credential_encryption): api_key→api_keys
- Update provider-refactoring.md: api_key→api_keys in all config examples
- Update security_configuration.md: api_key→api_keys in Before example
- Update 3 channel docs: channels→channel_list in JSON examples
2026-05-06 18:02:46 +08:00

810 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> 返回 [README](../project/README.zh.md)
# Antigravity 认证与集成指南
## 概述
**Antigravity**Google Cloud Code Assist)是由 Google 支持的 AI 模型提供商,通过 Google 的云基础设施提供对 Claude Opus 4.6 和 Gemini 等模型的访问。本文档提供了关于认证工作原理、如何获取模型以及如何在 PicoClaw 中实现新提供商的完整指南。
---
## 目录
1. [认证流程](#认证流程)
2. [OAuth 实现细节](#oauth-实现细节)
3. [令牌管理](#令牌管理)
4. [模型列表获取](#模型列表获取)
5. [用量追踪](#用量追踪)
6. [提供商插件结构](#提供商插件结构)
7. [集成要求](#集成要求)
8. [API 端点](#api-端点)
9. [配置](#配置)
10. [在 PicoClaw 中创建新提供商](#在-picoclaw-中创建新提供商)
---
## 认证流程
### 1. 带 PKCE 的 OAuth 2.0
Antigravity 使用 **OAuth 2.0 with PKCEProof Key for Code Exchange** 进行安全认证:
```
┌─────────────┐ ┌─────────────────┐
│ Client │ ───(1) Generate PKCE Pair────────> │ │
│ │ ───(2) Open Auth URL─────────────> │ Google OAuth │
│ │ │ Server │
│ │ <──(3) Redirect with Code───────── │ │
│ │ └─────────────────┘
│ │ ───(4) Exchange Code for Tokens──> │ Token URL │
│ │ │ │
│ │ <──(5) Access + Refresh Tokens──── │ │
└─────────────┘ └─────────────────┘
```
### 2. 详细步骤
#### 步骤 1:生成 PKCE 参数
```typescript
function generatePkce(): { verifier: string; challenge: string } {
const verifier = randomBytes(32).toString("hex");
const challenge = createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
```
#### 步骤 2:构建授权 URL
```typescript
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
const REDIRECT_URI = "http://localhost:51121/oauth-callback";
function buildAuthUrl(params: { challenge: string; state: string }): string {
const url = new URL(AUTH_URL);
url.searchParams.set("client_id", CLIENT_ID);
url.searchParams.set("response_type", "code");
url.searchParams.set("redirect_uri", REDIRECT_URI);
url.searchParams.set("scope", SCOPES.join(" "));
url.searchParams.set("code_challenge", params.challenge);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("state", params.state);
url.searchParams.set("access_type", "offline");
url.searchParams.set("prompt", "consent");
return url.toString();
}
```
**所需权限范围:**
```typescript
const SCOPES = [
"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",
];
```
#### 步骤 3:处理 OAuth 回调
**自动模式(本地开发):**
- 在端口 51121 上启动本地 HTTP 服务器
- 等待来自 Google 的重定向
- 从查询参数中提取授权码
**手动模式(远程/无头环境):**
- 向用户显示授权 URL
- 用户在浏览器中完成认证
- 用户将完整的重定向 URL 粘贴回终端
- 从粘贴的 URL 中解析授权码
#### 步骤 4:用授权码交换令牌
```typescript
const TOKEN_URL = "https://oauth2.googleapis.com/token";
async function exchangeCode(params: {
code: string;
verifier: string;
}): Promise<{ access: string; refresh: string; expires: number }> {
const response = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: params.code,
grant_type: "authorization_code",
redirect_uri: REDIRECT_URI,
code_verifier: params.verifier,
}),
});
const data = await response.json();
return {
access: data.access_token,
refresh: data.refresh_token,
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000, // 5 min buffer
};
}
```
#### 步骤 5:获取额外的用户数据
**用户邮箱:**
```typescript
async function fetchUserEmail(accessToken: string): Promise<string | undefined> {
const response = await fetch(
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
const data = await response.json();
return data.email;
}
```
**项目 IDAPI 调用必需):**
```typescript
async function fetchProjectId(accessToken: string): Promise<string> {
const headers = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": "google-api-nodejs-client/9.15.1",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
"Client-Metadata": JSON.stringify({
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
}),
};
const response = await fetch(
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
{
method: "POST",
headers,
body: JSON.stringify({
metadata: {
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
},
}),
}
);
const data = await response.json();
return data.cloudaicompanionProject || "rising-fact-p41fc"; // 默认回退值
}
```
---
## OAuth 实现细节
### 客户端凭据
**重要:** 这些凭据在源代码中以 base64 编码存储,用于与 pi-ai 同步:
```typescript
const decode = (s: string) => Buffer.from(s, "base64").toString();
const CLIENT_ID = decode(
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ=="
);
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
```
### OAuth 流程模式
1. **自动流程**(有浏览器的本地机器):
- 自动打开浏览器
- 本地回调服务器捕获重定向
- 初始认证后无需用户交互
2. **手动流程**(远程/无头/WSL2 环境):
- 显示 URL 供手动复制粘贴
- 用户在外部浏览器中完成认证
- 用户将完整的重定向 URL 粘贴回来
```typescript
function shouldUseManualOAuthFlow(isRemote: boolean): boolean {
return isRemote || isWSL2Sync();
}
```
---
## 令牌管理
### 认证配置文件结构
```typescript
type OAuthCredential = {
type: "oauth";
provider: "google-antigravity";
access: string; // 访问令牌
refresh: string; // 刷新令牌
expires: number; // 过期时间戳(毫秒,自 epoch 起)
email?: string; // 用户邮箱
projectId?: string; // Google Cloud 项目 ID
};
```
### 令牌刷新
凭据包含一个刷新令牌,可在当前访问令牌过期时用于获取新的访问令牌。过期时间设置了 5 分钟的缓冲区以防止竞态条件。
---
## 模型列表获取
### 获取可用模型
```typescript
const BASE_URL = "https://cloudcode-pa.googleapis.com";
async function fetchAvailableModels(
accessToken: string,
projectId: string
): Promise<Model[]> {
const headers = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": "antigravity",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
};
const response = await fetch(
`${BASE_URL}/v1internal:fetchAvailableModels`,
{
method: "POST",
headers,
body: JSON.stringify({ project: projectId }),
}
);
const data = await response.json();
// 返回带有配额信息的模型
return Object.entries(data.models).map(([modelId, modelInfo]) => ({
id: modelId,
displayName: modelInfo.displayName,
quotaInfo: {
remainingFraction: modelInfo.quotaInfo?.remainingFraction,
resetTime: modelInfo.quotaInfo?.resetTime,
isExhausted: modelInfo.quotaInfo?.isExhausted,
},
}));
}
```
### 响应格式
```typescript
type FetchAvailableModelsResponse = {
models?: Record<string, {
displayName?: string;
quotaInfo?: {
remainingFraction?: number | string;
resetTime?: string; // ISO 8601 时间戳
isExhausted?: boolean;
};
}>;
};
```
---
## 用量追踪
### 获取用量数据
```typescript
export async function fetchAntigravityUsage(
token: string,
timeoutMs: number
): Promise<ProviderUsageSnapshot> {
// 1. 获取额度和计划信息
const loadCodeAssistRes = await fetch(
`${BASE_URL}/v1internal:loadCodeAssist`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
metadata: {
ideType: "ANTIGRAVITY",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
},
}),
}
);
// 提取额度信息
const { availablePromptCredits, planInfo, currentTier } = data;
// 2. 获取模型配额
const modelsRes = await fetch(
`${BASE_URL}/v1internal:fetchAvailableModels`,
{
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ project: projectId }),
}
);
// 构建用量窗口
return {
provider: "google-antigravity",
displayName: "Google Antigravity",
windows: [
{ label: "Credits", usedPercent: calculateUsedPercent(available, monthly) },
// 各模型配额...
],
plan: currentTier?.name || planType,
};
}
```
### 用量响应结构
```typescript
type ProviderUsageSnapshot = {
provider: "google-antigravity";
displayName: string;
windows: UsageWindow[];
plan?: string;
error?: string;
};
type UsageWindow = {
label: string; // "Credits" 或模型 ID
usedPercent: number; // 0-100
resetAt?: number; // 配额重置的时间戳
};
```
---
## 提供商插件结构
### 插件定义
```typescript
const antigravityPlugin = {
id: "google-antigravity-auth",
name: "Google Antigravity Auth",
description: "OAuth flow for Google Antigravity (Cloud Code Assist)",
configSchema: emptyPluginConfigSchema(),
register(api: PicoClawPluginApi) {
api.registerProvider({
id: "google-antigravity",
label: "Google Antigravity",
docsPath: "/providers/models",
aliases: ["antigravity"],
auth: [
{
id: "oauth",
label: "Google OAuth",
hint: "PKCE + localhost callback",
kind: "oauth",
run: async (ctx: ProviderAuthContext) => {
// OAuth 实现在此处
},
},
],
});
},
};
```
### ProviderAuthContext
```typescript
type ProviderAuthContext = {
config: PicoClawConfig;
agentDir?: string;
workspaceDir?: string;
prompter: WizardPrompter; // UI 提示/通知
runtime: RuntimeEnv; // 日志等
isRemote: boolean; // 是否在远程运行
openUrl: (url: string) => Promise<void>; // 浏览器打开器
oauth: {
createVpsAwareHandlers: Function;
};
};
```
### ProviderAuthResult
```typescript
type ProviderAuthResult = {
profiles: Array<{
profileId: string;
credential: AuthProfileCredential;
}>;
configPatch?: Partial<PicoClawConfig>;
defaultModel?: string;
notes?: string[];
};
```
---
## 集成要求
### 1. 所需环境/依赖
- Go ≥ 1.25
- PicoClaw 代码库(`pkg/providers/``pkg/auth/`
- `crypto``net/http` 标准库包
### 2. API 调用所需的请求头
```typescript
const REQUIRED_HEADERS = {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": "antigravity", // 或 "google-api-nodejs-client/9.15.1"
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
};
// 对于 loadCodeAssist 调用,还需包含:
const CLIENT_METADATA = {
ideType: "ANTIGRAVITY", // 或 "IDE_UNSPECIFIED"
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
};
```
### 3. 模型 Schema 清理
Antigravity 使用兼容 Gemini 的模型,因此工具 schema 必须进行清理:
```typescript
const GOOGLE_SCHEMA_UNSUPPORTED_KEYWORDS = new Set([
"patternProperties",
"additionalProperties",
"$schema",
"$id",
"$ref",
"$defs",
"definitions",
"examples",
"minLength",
"maxLength",
"minimum",
"maximum",
"multipleOf",
"pattern",
"format",
"minItems",
"maxItems",
"uniqueItems",
"minProperties",
"maxProperties",
]);
// 发送前清理 schema
function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
// 移除不支持的关键字
// 确保顶层有 type: "object"
// 展平 anyOf/oneOf 联合类型
}
```
### 4. 思维块处理(Claude 模型)
对于 Antigravity 的 Claude 模型,思维块需要特殊处理:
```typescript
const ANTIGRAVITY_SIGNATURE_RE = /^[A-Za-z0-9+/]+={0,2}$/;
export function sanitizeAntigravityThinkingBlocks(
messages: AgentMessage[]
): AgentMessage[] {
// 验证思维签名
// 规范化签名字段
// 丢弃未签名的思维块
}
```
---
## API 端点
### 认证端点
| 端点 | 方法 | 用途 |
|------|------|------|
| `https://accounts.google.com/o/oauth2/v2/auth` | GET | OAuth 授权 |
| `https://oauth2.googleapis.com/token` | POST | 令牌交换 |
| `https://www.googleapis.com/oauth2/v1/userinfo` | GET | 用户信息(邮箱) |
### Cloud Code Assist 端点
| 端点 | 方法 | 用途 |
|------|------|------|
| `https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist` | POST | 加载项目信息、额度、计划 |
| `https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels` | POST | 列出可用模型及配额 |
| `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse` | POST | 聊天流式端点 |
**API 请求格式(聊天):**
`v1internal:streamGenerateContent` 端点期望一个包装标准 Gemini 请求的信封格式:
```json
{
"project": "your-project-id",
"model": "model-id",
"request": {
"contents": [...],
"systemInstruction": {...},
"generationConfig": {...},
"tools": [...]
},
"requestType": "agent",
"userAgent": "antigravity",
"requestId": "agent-timestamp-random"
}
```
**API 响应格式(SSE):**
每条 SSE 消息(`data: {...}`)被包装在 `response` 字段中:
```json
{
"response": {
"candidates": [...],
"usageMetadata": {...},
"modelVersion": "...",
"responseId": "..."
},
"traceId": "...",
"metadata": {}
}
```
---
## 配置
### config.json 配置
```json
{
"model_list": [
{
"model_name": "gemini-flash",
"model": "antigravity/gemini-3-flash",
"auth_method": "oauth"
}
],
"agents": {
"defaults": {
"model_name": "gemini-flash"
}
}
}
```
### 认证配置文件存储
认证配置文件存储在 `~/.picoclaw/auth.json` 中:
```json
{
"credentials": {
"google-antigravity": {
"access_token": "ya29...",
"refresh_token": "1//...",
"expires_at": "2026-01-01T00:00:00Z",
"provider": "google-antigravity",
"auth_method": "oauth",
"email": "user@example.com",
"project_id": "my-project-id"
}
}
}
```
---
## 在 PicoClaw 中创建新提供商
PicoClaw 提供商以 Go 包的形式实现,位于 `pkg/providers/` 下。要添加新提供商:
### 分步实现
#### 1. 创建提供商文件
`pkg/providers/` 中创建新的 Go 文件:
```
pkg/providers/
└── your_provider.go
```
#### 2. 实现 Provider 接口
你的提供商必须实现 `pkg/providers/types.go` 中定义的 `Provider` 接口:
```go
package providers
type YourProvider struct {
apiKey string
apiBase string
}
func NewYourProvider(apiKey, apiBase, proxy string) *YourProvider {
if apiBase == "" {
apiBase = "https://api.your-provider.com/v1"
}
return &YourProvider{apiKey: apiKey, apiBase: apiBase}
}
func (p *YourProvider) Chat(ctx context.Context, messages []Message, tools []Tool, cb StreamCallback) error {
// 实现带流式传输的聊天补全
}
```
#### 3. 在工厂中注册
将你的提供商添加到 `pkg/providers/factory.go` 中的协议分支:
```go
case "your-provider":
return NewYourProvider(sel.apiKey, sel.apiBase, sel.proxy), nil
```
#### 4. 添加默认配置(可选)
`pkg/config/defaults.go` 中添加默认条目:
```go
{
ModelName: "your-model",
Model: "your-provider/model-name",
APIKey: "",
},
```
#### 5. 添加认证支持(可选)
如果你的提供商需要 OAuth 或特殊认证,在 `cmd/picoclaw/internal/auth/helpers.go` 中添加分支:
```go
case "your-provider":
authLoginYourProvider()
```
#### 6. 通过 `config.json` 配置
```json
{
"model_list": [
{
"model_name": "your-model",
"model": "your-provider/model-name",
"api_keys": ["your-api-key"],
"api_base": "https://api.your-provider.com/v1"
}
]
}
```
---
## 测试你的实现
### CLI 命令
```bash
# 使用提供商进行认证
picoclaw auth login --provider your-provider
# 列出模型(用于 Antigravity
picoclaw auth models
# 启动网关
picoclaw gateway
# 使用指定模型运行代理
picoclaw agent -m "Hello" --model your-model
```
### 测试用环境变量
```bash
# 覆盖默认模型
export PICOCLAW_AGENTS_DEFAULTS_MODEL=your-model
# 覆盖提供商设置
export PICOCLAW_MODEL_LIST='[{"model_name":"your-model","model":"your-provider/model-name","api_keys":["..."]}]'
```
---
## 参考资料
- **源文件:**
- `pkg/providers/antigravity_provider.go` - Antigravity 提供商实现
- `pkg/auth/oauth.go` - OAuth 流程实现
- `pkg/auth/store.go` - 认证凭据存储(`~/.picoclaw/auth.json`
- `pkg/providers/factory.go` - 提供商工厂和协议路由
- `pkg/providers/types.go` - 提供商接口定义
- `cmd/picoclaw/internal/auth/helpers.go` - 认证 CLI 命令
- **文档:**
- `docs/ANTIGRAVITY_USAGE.md` - Antigravity 使用指南
- `docs/migration/model-list-migration.md` - 迁移指南
---
## 注意事项
1. **Google Cloud 项目:** Antigravity 要求在你的 Google Cloud 项目上启用 Gemini for Google Cloud
2. **配额:** 使用 Google Cloud 项目配额(非独立计费)
3. **模型访问:** 可用模型取决于你的 Google Cloud 项目配置
4. **思维块:** 通过 Antigravity 使用的 Claude 模型需要对带签名的思维块进行特殊处理
5. **Schema 清理:** 工具 schema 必须清理以移除不支持的 JSON Schema 关键字
---
---
## 常见错误处理
### 1. 速率限制(HTTP 429
当项目/模型配额耗尽时,Antigravity 会返回 429 错误。错误响应通常在 `details` 字段中包含 `quotaResetDelay`
**429 错误示例:**
```json
{
"error": {
"code": 429,
"message": "You have exhausted your capacity on this model. Your quota will reset after 4h30m28s.",
"status": "RESOURCE_EXHAUSTED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"metadata": {
"quotaResetDelay": "4h30m28.060903746s"
}
}
]
}
}
```
### 2. 空响应(受限模型)
某些模型可能出现在可用模型列表中,但返回空响应(200 OK 但 SSE 流为空)。这通常发生在当前项目没有权限使用的预览版或受限模型上。
**处理方式:** 将空响应视为错误,通知用户该模型可能对其项目受限或无效。
---
## 故障排除
### "Token expired"(令牌已过期)
- 刷新 OAuth 令牌:`picoclaw auth login --provider antigravity`
### "Gemini for Google Cloud is not enabled"Gemini for Google Cloud 未启用)
- 在 Google Cloud Console 中启用该 API
### "Project not found"(项目未找到)
- 确保你的 Google Cloud 项目已启用必要的 API
- 检查认证过程中项目 ID 是否正确获取
### 模型未出现在列表中
- 验证 OAuth 认证是否成功完成
- 检查认证配置文件存储:`~/.picoclaw/auth.json`
- 重新运行 `picoclaw auth login --provider antigravity`