refactor: Antigravity/Codex 服务重构为插件化适配器架构

- 将 Antigravity 和 Codex 从独立模块迁移至 src/services/provider/adapters/ 插件体系
- 新增 provider_types 和 oauth_token 模块,移除 maintenance_scheduler 中的 OAuth 定时刷新
- 增强 admin API:扩展 keys 和 provider_query 端点,新增 dashboard 路由
- 大幅增强 ProviderDetailDrawer 组件,新增 AntigravityQuotaDialog
- 改进 handler 基类(chat/cli)和错误分类器
- 优化 fetch_scheduler 和 upstream_fetcher
- 前端 UI 组件清理和优化
- 更新测试以匹配新模块结构
This commit is contained in:
fawney19
2026-02-06 16:37:06 +08:00
parent e01dfee41d
commit b88fb6273b
112 changed files with 5193 additions and 1992 deletions

View File

@@ -311,7 +311,9 @@ class GeminiNormalizer(FormatNormalizer):
)
if not has_thought:
try:
from src.services.antigravity.constants import DUMMY_THOUGHT_SIGNATURE
from src.services.provider.adapters.antigravity.constants import (
DUMMY_THOUGHT_SIGNATURE,
)
dummy_sig = DUMMY_THOUGHT_SIGNATURE
except Exception:
@@ -1285,8 +1287,8 @@ class GeminiNormalizer(FormatNormalizer):
signature: str | None = None
try:
from src.services.antigravity.constants import DUMMY_THOUGHT_SIGNATURE
from src.services.antigravity.signature_cache import signature_cache
from src.services.provider.adapters.antigravity.constants import DUMMY_THOUGHT_SIGNATURE
from src.services.provider.adapters.antigravity.signature_cache import signature_cache
cached_or_dummy = signature_cache.get_or_dummy(model, text_val)

View File

@@ -336,6 +336,7 @@ def build_upstream_headers_for_endpoint(
endpoint_headers: dict[str, str] | None = None,
extra_headers: dict[str, str] | None = None,
drop_headers: frozenset[str] | None = None,
header_rules: list[dict[str, Any]] | None = None,
) -> dict[str, str]:
"""
新模式:构建发送给上游 Provider 的请求头(基于 endpoint signature
@@ -343,8 +344,9 @@ def build_upstream_headers_for_endpoint(
优先级(后者覆盖前者):
1. 原始头部(排除 drop_headers
2. endpoint 配置头部
3. extra_headers
4. 认证头(最高优先级,始终设置)
3. header_rules用户自定义的请求头规则支持 set/drop/rename
4. extra_headers
5. 认证头(最高优先级,始终设置)
"""
if drop_headers is None:
drop_headers = UPSTREAM_DROP_HEADERS
@@ -363,6 +365,10 @@ def build_upstream_headers_for_endpoint(
if endpoint_headers:
builder.add_protected(endpoint_headers, protected_keys)
# 应用用户自定义的请求头规则(认证头受保护)
if header_rules:
builder.apply_rules(header_rules, protected_keys)
if extra_headers:
builder.add_many(extra_headers)

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import asyncio
from typing import Any
from typing import Any, Awaitable, Callable
from urllib.parse import urlsplit, urlunsplit
import httpx
@@ -9,6 +9,7 @@ import jwt
from src.clients.http_client import HTTPClientPool, build_proxy_url
from src.core.logger import logger
from src.core.provider_types import ProviderType
_ANTHROPIC_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"
_GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json"
@@ -171,7 +172,7 @@ async def post_oauth_token(
IMPORTANT: Never log secrets (tokens, secrets). This function only logs generic errors.
"""
if provider_type == "claude_code" and token_url == _ANTHROPIC_TOKEN_URL:
if provider_type == ProviderType.CLAUDE_CODE and token_url == _ANTHROPIC_TOKEN_URL:
proxy_url = _coerce_proxy_url(proxy_config)
try:
status_code, resp_headers, text = await asyncio.to_thread(
@@ -292,6 +293,64 @@ def extract_claude_email_from_token_response(token: dict[str, Any]) -> str | Non
return None
# ---------------------------------------------------------------------------
# Auth Enricher Registry
# ---------------------------------------------------------------------------
AuthEnricherFn = Callable[
[dict[str, Any], dict[str, Any], str, dict[str, Any] | None],
Awaitable[dict[str, Any]],
]
_auth_enrichers: dict[str, AuthEnricherFn] = {}
def register_auth_enricher(provider_type: str, enricher: AuthEnricherFn) -> None:
"""注册 provider 特有的 auth_config enrichment hook。"""
from src.core.provider_types import normalize_provider_type
_auth_enrichers[normalize_provider_type(provider_type)] = enricher
async def _enrich_claude_code(
auth_config: dict[str, Any],
token_response: dict[str, Any],
access_token: str,
proxy_config: dict[str, Any] | None,
) -> dict[str, Any]:
email = extract_claude_email_from_token_response(token_response)
if email:
auth_config["email"] = email
return auth_config
async def _enrich_gemini_cli(
auth_config: dict[str, Any],
token_response: dict[str, Any],
access_token: str,
proxy_config: dict[str, Any] | None,
) -> dict[str, Any]:
if not auth_config.get("email"):
email = await fetch_google_email(
access_token,
proxy_config=proxy_config,
timeout_seconds=10.0,
)
if email:
auth_config["email"] = email
return auth_config
def _bootstrap_auth_enrichers() -> None:
# 简单的内置 enrichers 直接注册
register_auth_enricher("claude_code", _enrich_claude_code)
register_auth_enricher("gemini_cli", _enrich_gemini_cli)
# Provider-specific enrichers 通过 plugin.register_all() 注册
# (called from envelope.py bootstrap)
_bootstrap_auth_enrichers()
async def enrich_auth_config(
*,
provider_type: str,
@@ -302,71 +361,14 @@ async def enrich_auth_config(
) -> dict[str, Any]:
"""Enrich auth_config with non-secret metadata (email/account_id).
- Claude Code: email from token response (if present)
- Codex: parse id_token -> email/account_id
- Gemini/Antigravity: call Google userinfo -> email
id_token is not persisted.
各 provider 的 enrichment 逻辑通过 register_auth_enricher 注册。
"""
from src.core.provider_types import normalize_provider_type
from src.services.provider.envelope import ensure_providers_bootstrapped
# Claude
if provider_type == "claude_code":
email = extract_claude_email_from_token_response(token_response)
if email:
auth_config["email"] = email
return auth_config
# Codex
if provider_type == "codex":
id_token = token_response.get("id_token")
logger.debug(
"Codex enrich_auth_config: id_token_present={} token_keys={}",
bool(id_token),
list(token_response.keys()),
)
codex_info = parse_codex_id_token(str(id_token) if id_token else None)
if codex_info:
logger.debug("Codex parsed id_token fields: {}", list(codex_info.keys()))
if codex_info.get("email"):
auth_config["email"] = codex_info["email"]
if codex_info.get("account_id"):
auth_config["account_id"] = codex_info["account_id"]
if codex_info.get("plan_type"):
auth_config["plan_type"] = codex_info["plan_type"]
if codex_info.get("user_id"):
auth_config["user_id"] = codex_info["user_id"]
return auth_config
# Gemini family (gemini_cli / antigravity)
if provider_type in {"gemini_cli", "antigravity"}:
# Only fetch if missing to reduce overhead
if not auth_config.get("email"):
email = await fetch_google_email(
access_token,
proxy_config=proxy_config,
timeout_seconds=10.0,
)
if email:
auth_config["email"] = email
# Antigravity: project_id 需要通过 /v1internal:loadCodeAssist 获取
if provider_type == "antigravity":
if not auth_config.get("project_id"):
try:
from src.services.antigravity.client import load_code_assist
code_assist = await load_code_assist(access_token, proxy_config=proxy_config)
project_id = code_assist.get("cloudaicompanionProject")
if isinstance(project_id, str) and project_id:
auth_config["project_id"] = project_id
tier_obj = code_assist.get("currentTier")
if isinstance(tier_obj, dict):
tier_type = tier_obj.get("tierType")
if isinstance(tier_type, str) and tier_type:
auth_config["tier"] = tier_type
except Exception as e:
logger.warning(f"Failed to load code assist: {e}")
return auth_config
ensure_providers_bootstrapped()
pt = normalize_provider_type(provider_type)
enricher = _auth_enrichers.get(pt)
if enricher:
return await enricher(auth_config, token_response, access_token, proxy_config)
return auth_config

View File

@@ -15,7 +15,9 @@ from __future__ import annotations
from dataclasses import dataclass
from src.core.provider_templates.types import ProviderType
from src.services.antigravity.constants import PROD_BASE_URL as ANTIGRAVITY_PROD_URL
from src.services.provider.adapters.antigravity.constants import (
PROD_BASE_URL as ANTIGRAVITY_PROD_URL,
)
@dataclass(frozen=True, slots=True)

View File

@@ -1,14 +1,7 @@
"""Backward-compat re-export — canonical definition is in src.core.provider_types."""
from __future__ import annotations
from enum import Enum
class ProviderType(str, Enum):
CUSTOM = "custom"
CLAUDE_CODE = "claude_code"
CODEX = "codex"
GEMINI_CLI = "gemini_cli"
ANTIGRAVITY = "antigravity"
from src.core.provider_types import ProviderType
__all__ = ["ProviderType"]

View File

@@ -0,0 +1,44 @@
"""Provider type 枚举与工具函数。
所有 provider_type 相关的比较、判断应使用此模块中的 ProviderType 枚举,
避免到处散落字符串字面量。
由于 ProviderType 继承自 str``ProviderType.ANTIGRAVITY == "antigravity"``
始终为 True因此与已有数据库值、序列化格式完全兼容。
"""
from __future__ import annotations
from enum import Enum
class ProviderType(str, Enum):
"""已支持的 Provider 类型。"""
CUSTOM = "custom"
CLAUDE_CODE = "claude_code"
CODEX = "codex"
GEMINI_CLI = "gemini_cli"
ANTIGRAVITY = "antigravity"
# 所有有效 provider_type 值的集合(用于校验)
VALID_PROVIDER_TYPES: frozenset[str] = frozenset(pt.value for pt in ProviderType)
def normalize_provider_type(value: object) -> str:
"""将任意输入规范化为小写 provider_type 字符串。
统一处理 ``str(getattr(provider, "provider_type", "") or "").strip().lower()``
这类散落在各处的 normalize 逻辑。
"""
if isinstance(value, ProviderType):
return value.value
return str(value or "").strip().lower()
__all__ = [
"VALID_PROVIDER_TYPES",
"ProviderType",
"normalize_provider_type",
]