mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
44
src/core/provider_types.py
Normal file
44
src/core/provider_types.py
Normal 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",
|
||||
]
|
||||
Reference in New Issue
Block a user