feat(antigravity): 对齐 AM 客户端标识,前端 OAuth 增加 TOTP 支持

- User-Agent 改为 Electron 浏览器格式,新增 x-client-name/x-client-version/x-vscode-sessionid/x-goog-api-client header
- 移除 MODEL_ALIAS_MAP 别名映射,改为仅剥离 -online 后缀
- thinking 自动注入关键字新增 gemini-3.1-pro
- 新增定时 Antigravity UA 版本刷新任务(每 6 小时 + 启动时)
- 前端 OAuth 设备授权新增 TOTP 验证码生成(otpauth),Region 改为按钮选择
- SSO OIDC 轮询日志 authorization_pending/slow_down 降级为 debug
This commit is contained in:
fawney19
2026-02-24 01:52:53 +08:00
parent cd70c9148e
commit 7e447fe54e
11 changed files with 290 additions and 92 deletions

View File

@@ -2037,12 +2037,21 @@ async def _sso_oidc_post(
# 返回原始 JSON 让调用方处理错误码
try:
err_data = resp.json()
logger.warning(
"SSO OIDC request failed: {} returned {} | body={}",
url,
resp.status_code,
err_data,
)
err_code = err_data.get("error", "")
if err_code in ("authorization_pending", "slow_down"):
logger.debug(
"SSO OIDC polling: {} returned {} ({})",
url,
resp.status_code,
err_code,
)
else:
logger.warning(
"SSO OIDC request failed: {} returned {} | body={}",
url,
resp.status_code,
err_data,
)
return {"_error": True, "_status": resp.status_code, **err_data}
except Exception:
logger.warning(

View File

@@ -371,9 +371,7 @@ class HandlerAdapterBase(ApiAdapter):
elif is_antigravity:
from src.services.provider.adapters.antigravity.constants import (
V1INTERNAL_PATH_TEMPLATE,
)
from src.services.provider.adapters.antigravity.constants import (
get_http_user_agent as _get_antigravity_ua,
get_v1internal_extra_headers,
)
from src.services.provider.adapters.antigravity.url_availability import (
url_availability,
@@ -392,7 +390,7 @@ class HandlerAdapterBase(ApiAdapter):
merged_extra.update(cli_extra)
if is_antigravity:
merged_extra["User-Agent"] = _get_antigravity_ua()
merged_extra.update(get_v1internal_extra_headers())
if is_kiro:
from src.services.provider.adapters.kiro.headers import (

View File

@@ -297,9 +297,7 @@ class GeminiChatAdapter(ChatAdapterBase):
if is_antigravity:
from src.services.provider.adapters.antigravity.constants import (
V1INTERNAL_PATH_TEMPLATE,
)
from src.services.provider.adapters.antigravity.constants import (
get_http_user_agent as _get_antigravity_ua,
get_v1internal_extra_headers,
)
from src.services.provider.adapters.antigravity.url_availability import url_availability
@@ -316,7 +314,7 @@ class GeminiChatAdapter(ChatAdapterBase):
# Antigravity 需要特定的 User-Agent
merged_extra = dict(extra_headers) if extra_headers else {}
if is_antigravity:
merged_extra["User-Agent"] = _get_antigravity_ua()
merged_extra.update(get_v1internal_extra_headers())
headers = cls.build_headers_with_extra(api_key, merged_extra if merged_extra else None)
# OAuth 统一处理替换端点默认认证头x-goog-api-key为 Authorization: Bearer

View File

@@ -15,7 +15,7 @@ from src.services.provider.adapters.antigravity.constants import (
PROD_BASE_URL,
SANDBOX_BASE_URL,
VERSION_FETCH_URL,
get_http_user_agent,
get_v1internal_extra_headers,
parse_version_string,
update_user_agent_version,
)
@@ -242,8 +242,8 @@ async def load_code_assist(
headers = {
"Authorization": f"Bearer {access_token}",
"User-Agent": get_http_user_agent(),
"Content-Type": "application/json",
**get_v1internal_extra_headers(),
}
body = {"metadata": _CODE_ASSIST_METADATA}
@@ -339,8 +339,8 @@ async def onboard_user(
headers = {
"Authorization": f"Bearer {access_token}",
"User-Agent": get_http_user_agent(),
"Content-Type": "application/json",
**get_v1internal_extra_headers(),
}
body = {
"tierId": tier_id,
@@ -447,9 +447,9 @@ async def fetch_available_models(
headers = {
"Authorization": f"Bearer {access_token}",
"User-Agent": get_http_user_agent(),
"Content-Type": "application/json",
"Accept": "application/json",
**get_v1internal_extra_headers(),
}
body = {"project": project_id}

View File

@@ -9,6 +9,7 @@ from __future__ import annotations
import platform
import re
import threading
import uuid
# ============== API 端点 ==============
# 唯一定义在 core 层,此处 re-export 保持向后兼容
@@ -19,25 +20,31 @@ SANDBOX_BASE_URL = "https://daily-cloudcode-pa.sandbox.googleapis.com"
# ============== User-Agent ==============
VERSION_FETCH_URL = "https://antigravity-auto-updater-974169037036.us-central1.run.app"
_FALLBACK_VERSION = "1.15.8"
_FALLBACK_VERSION = "1.18.4"
_FALLBACK_CHROME = "132.0.6834.160"
_FALLBACK_ELECTRON = "39.2.3"
_VERSION_RE = re.compile(r"\d+\.\d+\.\d+")
def _detect_platform_tag() -> str:
"""检测当前运行平台,格式与 Go runtime 保持一致"""
os_name = platform.system().lower() # linux, darwin, windows
arch = platform.machine().lower()
if arch in ("x86_64", "amd64"):
arch = "amd64"
elif arch in ("aarch64", "arm64"):
arch = "arm64"
return f"{os_name}/{arch}"
def _detect_platform_info() -> str:
"""检测当前运行平台,格式对齐 AM constants.rs 的 Electron UA"""
os_name = platform.system().lower()
if os_name == "darwin":
return "Macintosh; Intel Mac OS X 10_15_7"
elif os_name == "windows":
return "Windows NT 10.0; Win64; x64"
else:
return "X11; Linux x86_64"
_PLATFORM_TAG = _detect_platform_tag()
_PLATFORM_INFO = _detect_platform_info()
# HTTP Header User-Agent向后兼容:模块级常量保留,新代码应使用 get_http_user_agent()
HTTP_USER_AGENT = f"antigravity/{_FALLBACK_VERSION} {_PLATFORM_TAG}"
# HTTP Header User-Agent对齐 AM constants.rs: 完整 Electron 浏览器格式
HTTP_USER_AGENT = (
f"Mozilla/5.0 ({_PLATFORM_INFO}) AppleWebKit/537.36 (KHTML, like Gecko) "
f"Antigravity/{_FALLBACK_VERSION} Chrome/{_FALLBACK_CHROME} "
f"Electron/{_FALLBACK_ELECTRON} Safari/537.36"
)
# V1InternalRequest.userAgent 字段(固定值)
REQUEST_USER_AGENT = "antigravity"
@@ -48,9 +55,13 @@ _ua_version: str = _FALLBACK_VERSION
def get_http_user_agent() -> str:
"""返回当前 HTTP User-Agent 字符串(支持动态版本号更新)。"""
"""返回当前 HTTP User-Agent 字符串(对齐 AM Electron UA 格式)。"""
with _ua_lock:
return f"antigravity/{_ua_version} {_PLATFORM_TAG}"
return (
f"Mozilla/5.0 ({_PLATFORM_INFO}) AppleWebKit/537.36 (KHTML, like Gecko) "
f"Antigravity/{_ua_version} Chrome/{_FALLBACK_CHROME} "
f"Electron/{_FALLBACK_ELECTRON} Safari/537.36"
)
def update_user_agent_version(version: str) -> None:
@@ -61,8 +72,11 @@ def update_user_agent_version(version: str) -> None:
return
with _ua_lock:
_ua_version = version
# Backward compat: keep module-level constant in sync.
HTTP_USER_AGENT = f"antigravity/{_ua_version} {_PLATFORM_TAG}"
HTTP_USER_AGENT = (
f"Mozilla/5.0 ({_PLATFORM_INFO}) AppleWebKit/537.36 (KHTML, like Gecko) "
f"Antigravity/{_ua_version} Chrome/{_FALLBACK_CHROME} "
f"Electron/{_FALLBACK_ELECTRON} Safari/537.36"
)
def parse_version_string(text: str) -> str | None:
@@ -74,6 +88,25 @@ def parse_version_string(text: str) -> str | None:
# ============== URL 可用性 ==============
URL_UNAVAILABLE_TTL_SECONDS = 300 # 5 分钟
# ============== AM Client Identity Headers ==============
# 对齐 AM upstream/client.rs: 伪装为官方 Antigravity 客户端
# 缺少这些 header 会导致新模型(如 gemini-3.1-pro-preview返回 404
_SESSION_ID = uuid.uuid4().hex # 每次进程启动生成一个固定 session ID
def get_v1internal_extra_headers() -> dict[str, str]:
"""构建 v1internal 请求需要的额外 header对齐 AM upstream/client.rs"""
with _ua_lock:
version = _ua_version
return {
"User-Agent": get_http_user_agent(),
"x-client-name": "antigravity",
"x-client-version": version,
"x-vscode-sessionid": _SESSION_ID,
"x-goog-api-client": "gl-node/18.18.2 fire/0.8.6 grpc/1.10.x",
}
# ============== Thinking Signature ==============
# 统一从 core 层导入,避免多处定义
from src.core.api_format.conversion.constants import DUMMY_THOUGHT_SIGNATURE # noqa: E402
@@ -88,15 +121,12 @@ OUTPUT_OVERHEAD_IMAGE = 2048
# 模型最大输出限制(防止超限)
MODEL_MAX_OUTPUT_LIMIT = 65536
# 包含这些关键字的模型会自动注入 thinkingConfig如果缺失
THINKING_MODELS_AUTO_INJECT_KEYWORDS = ("thinking", "gemini-2.0-pro", "gemini-3-pro")
# ============== Model Alias Mapping ==============
# 对齐 AM common_utils.rs: 将预览/别名映射回上游物理模型名
MODEL_ALIAS_MAP: dict[str, str] = {
"gemini-3-pro-preview": "gemini-3-pro-high",
"gemini-3-pro-image-preview": "gemini-3-pro-image",
"gemini-3-flash-preview": "gemini-3-flash",
}
THINKING_MODELS_AUTO_INJECT_KEYWORDS = (
"thinking",
"gemini-2.0-pro",
"gemini-3-pro",
"gemini-3.1-pro",
)
# ============== Google Search (Grounding) ==============
# 对齐 AM common_utils.rs: 仅 gemini-2.5-flash 支持 googleSearch tool
@@ -203,7 +233,6 @@ __all__ = [
"IMAGE_ASPECT_RATIO_SUFFIXES",
"IMAGE_GEN_UPSTREAM_MODEL",
"MIN_SIGNATURE_LENGTH",
"MODEL_ALIAS_MAP",
"MODEL_MAX_OUTPUT_LIMIT",
"NETWORKING_TOOL_KEYWORDS",
"OUTPUT_OVERHEAD",
@@ -225,6 +254,7 @@ __all__ = [
"VERSION_FETCH_URL",
"WEB_SEARCH_MODEL",
"get_http_user_agent",
"get_v1internal_extra_headers",
"parse_version_string",
"update_user_agent_version",
]

View File

@@ -29,7 +29,6 @@ from src.services.provider.adapters.antigravity.constants import (
ASPECT_RATIO_TABLE,
IMAGE_ASPECT_RATIO_SUFFIXES,
IMAGE_GEN_UPSTREAM_MODEL,
MODEL_ALIAS_MAP,
MODEL_MAX_OUTPUT_LIMIT,
NETWORKING_TOOL_KEYWORDS,
OUTPUT_OVERHEAD,
@@ -45,7 +44,6 @@ from src.services.provider.adapters.antigravity.constants import (
THINKING_BUDGET_DEFAULT_INJECT,
THINKING_MODELS_AUTO_INJECT_KEYWORDS,
WEB_SEARCH_MODEL,
get_http_user_agent,
)
from src.services.provider.adapters.antigravity.url_availability import url_availability
from src.services.provider.request_context import get_selected_base_url
@@ -396,26 +394,6 @@ def _clean_json_schema(schema: dict[str, Any]) -> None:
clean_gemini_schema(schema)
# ---------------------------------------------------------------------------
# Model alias mapping对齐 AM common_utils.rs
# ---------------------------------------------------------------------------
def _resolve_model_alias(model: str) -> str:
"""将预览/别名模型名映射回上游物理模型名。
对齐 AM common_utils.rs resolve_request_config
- gemini-3-pro-preview → gemini-3-pro-high
- gemini-3-pro-image-preview → gemini-3-pro-image
- gemini-3-flash-preview → gemini-3-flash
- 同时剥离 -online 后缀
"""
resolved = model.rstrip()
# 剥离 -online 后缀(联网意图由 tools 检测,不依赖后缀传递到上游)
resolved = resolved.removesuffix("-online")
return MODEL_ALIAS_MAP.get(resolved, resolved)
# ---------------------------------------------------------------------------
# Google Search (Grounding) 检测与注入(对齐 AM common_utils.rs
# ---------------------------------------------------------------------------
@@ -895,9 +873,9 @@ def wrap_v1internal_request(
# 1. 深度清理 [undefined]
_deep_clean_undefined(inner_request)
# 2. 模型别名映射(对齐 AM common_utils.rs
# 2. 剥离 -online 后缀(联网意图由 tools 检测,不依赖后缀传递到上游
has_online_suffix = _detect_online_suffix(model)
final_model = _resolve_model_alias(model)
final_model = model.rstrip().removesuffix("-online")
# 3. 联网检测(对齐 AM-online 后缀或客户端声明了联网工具)
has_networking = has_online_suffix or _detect_networking_tools(inner_request)
@@ -1044,7 +1022,11 @@ class AntigravityV1InternalEnvelope:
name = "antigravity:v1internal"
def extra_headers(self) -> dict[str, str] | None:
return {"User-Agent": get_http_user_agent()}
from src.services.provider.adapters.antigravity.constants import (
get_v1internal_extra_headers,
)
return get_v1internal_extra_headers()
def wrap_request(
self,

View File

@@ -251,6 +251,14 @@ class MaintenanceScheduler:
name="Gemini文件映射清理",
)
# Antigravity User-Agent 版本刷新 - 每 6 小时
scheduler.add_interval_job(
self._scheduled_antigravity_ua_refresh,
hours=6,
job_id="antigravity_ua_refresh",
name="Antigravity UA版本刷新",
)
# Provider 签到任务 - 根据配置时间执行
checkin_hour, checkin_minute = self._get_checkin_time()
scheduler.add_cron_job(
@@ -280,6 +288,14 @@ class MaintenanceScheduler:
# 增加延迟时间避免与 UsageQueueConsumer 等后台任务竞争数据库连接
await asyncio.sleep(10)
# 刷新 Antigravity User-Agent 版本号(不阻塞其他启动任务)
try:
from src.services.provider.adapters.antigravity.client import refresh_user_agent
await refresh_user_agent()
except Exception as e:
logger.debug("启动时刷新 Antigravity UA 版本失败(不影响运行): {}", e)
try:
logger.info("启动时执行首次清理任务...")
await self._perform_cleanup()
@@ -338,6 +354,15 @@ class MaintenanceScheduler:
"""Gemini 文件映射清理任务(定时调用)"""
await self._perform_gemini_file_mapping_cleanup()
async def _scheduled_antigravity_ua_refresh(self) -> None:
"""Antigravity User-Agent 版本刷新(定时调用)"""
try:
from src.services.provider.adapters.antigravity.client import refresh_user_agent
await refresh_user_agent()
except Exception as e:
logger.debug("定时刷新 Antigravity UA 版本失败: {}", e)
async def _scheduled_provider_checkin(self) -> None:
"""Provider 签到任务(定时调用)"""
await self._perform_provider_checkin()