mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(fingerprint): 引入 per-key 请求指纹系统,替代全局 TLS 指纹开关
为每个 ProviderAPIKey 生成并持久化独立的请求指纹配置,涵盖 TLS impersonate profile、浏览器 UA、Stainless SDK 头部、Node/Chrome/Electron 版本等维度。 指纹基于 key ID 确定性生成,支持手动编辑和批量重新生成。 - 新增 fingerprint 模块:生成、加载、校验、懒持久化 - 数据库迁移:provider_api_keys 新增 fingerprint JSON 列 - 请求链路注入:handler 基类设置上下文指纹,request_builder 和 envelope 消费 - HTTP Client 支持动态 impersonate profile 选择 - Antigravity 适配器使用指纹覆盖 UA/session/Node 版本 - 前端移除手动 TLS 指纹开关,新增批量 regenerate_fingerprint 操作 - 号池管理 UI 优化:token 缩写格式、blocked 行样式、时间显示改为日期格式
This commit is contained in:
@@ -27,6 +27,7 @@ from src.core.exceptions import NotFoundException
|
||||
from src.core.logger import logger
|
||||
from src.database import get_db
|
||||
from src.models.database import Provider, ProviderAPIKey, Usage
|
||||
from src.services.provider.fingerprint import generate_fingerprint
|
||||
from src.services.provider.pool import redis_ops as pool_redis
|
||||
from src.services.provider.pool.account_state import resolve_pool_account_state
|
||||
from src.services.provider.pool.config import parse_pool_config
|
||||
@@ -143,7 +144,14 @@ async def batch_import_keys(
|
||||
# POST /api/admin/pool/{provider_id}/keys/batch-action
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ALLOWED_ACTIONS = {"enable", "disable", "delete", "clear_cooldown", "reset_cost"}
|
||||
ALLOWED_ACTIONS = {
|
||||
"enable",
|
||||
"disable",
|
||||
"delete",
|
||||
"clear_cooldown",
|
||||
"reset_cost",
|
||||
"regenerate_fingerprint",
|
||||
}
|
||||
|
||||
_COOLDOWN_REASON_LABELS: dict[str, str] = {
|
||||
"rate_limited_429": "429 限流",
|
||||
@@ -568,7 +576,7 @@ async def batch_action_keys(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> BatchActionResponse:
|
||||
"""Batch enable/disable/delete/clear_cooldown/reset_cost on pool keys."""
|
||||
"""Batch enable/disable/delete/clear_cooldown/reset_cost/regenerate_fingerprint on pool keys."""
|
||||
adapter = AdminBatchActionKeysAdapter(provider_id=provider_id, body=body)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
@@ -977,6 +985,11 @@ class AdminListPoolKeysAdapter(AdminApiAdapter):
|
||||
model_include_patterns=include_patterns,
|
||||
model_exclude_patterns=exclude_patterns,
|
||||
proxy=_mask_proxy_password(getattr(k, "proxy", None)),
|
||||
fingerprint=(
|
||||
getattr(k, "fingerprint", None)
|
||||
if isinstance(getattr(k, "fingerprint", None), dict)
|
||||
else None
|
||||
),
|
||||
account_quota=_build_account_quota(
|
||||
provider_type,
|
||||
getattr(k, "upstream_metadata", None),
|
||||
@@ -1035,13 +1048,15 @@ class AdminBatchImportKeysAdapter(AdminApiAdapter):
|
||||
|
||||
try:
|
||||
encrypted_key = crypto_service.encrypt(item.api_key)
|
||||
new_key_id = str(uuid.uuid4())
|
||||
new_key = ProviderAPIKey(
|
||||
id=str(uuid.uuid4()),
|
||||
id=new_key_id,
|
||||
provider_id=self.provider_id,
|
||||
name=item.name or f"imported-{idx}",
|
||||
api_key=encrypted_key,
|
||||
auth_type=item.auth_type or "api_key",
|
||||
proxy=key_proxy,
|
||||
fingerprint=generate_fingerprint(seed=new_key_id),
|
||||
is_active=True,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
@@ -1136,7 +1151,11 @@ class AdminBatchActionKeysAdapter(AdminApiAdapter):
|
||||
await pool_redis.clear_cost(pid, kid)
|
||||
affected += 1
|
||||
|
||||
if self.body.action in {"enable", "disable", "delete"}:
|
||||
elif self.body.action == "regenerate_fingerprint":
|
||||
key.fingerprint = generate_fingerprint(seed=None)
|
||||
affected += 1
|
||||
|
||||
if self.body.action in {"enable", "disable", "delete", "regenerate_fingerprint"}:
|
||||
try:
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
@@ -1150,6 +1169,7 @@ class AdminBatchActionKeysAdapter(AdminApiAdapter):
|
||||
"delete": "deleted",
|
||||
"clear_cooldown": "cooldown cleared",
|
||||
"reset_cost": "cost reset",
|
||||
"regenerate_fingerprint": "fingerprint regenerated",
|
||||
}
|
||||
|
||||
admin_name = context.user.username if context.user else "admin"
|
||||
|
||||
@@ -64,7 +64,6 @@ class PoolSchedulingReason(BaseModel):
|
||||
detail: str | None = None
|
||||
|
||||
|
||||
|
||||
class PoolKeyDetail(BaseModel):
|
||||
"""Detailed status of a single pool key."""
|
||||
|
||||
@@ -95,6 +94,7 @@ class PoolKeyDetail(BaseModel):
|
||||
model_include_patterns: list[str] | None = None
|
||||
model_exclude_patterns: list[str] | None = None
|
||||
proxy: dict[str, Any] | None = None
|
||||
fingerprint: dict[str, Any] | None = None
|
||||
account_quota: str | None = None
|
||||
cooldown_reason: str | None = None
|
||||
cooldown_ttl_seconds: int | None = None
|
||||
@@ -163,7 +163,7 @@ class BatchImportResponse(BaseModel):
|
||||
|
||||
class BatchActionRequest(BaseModel):
|
||||
key_ids: list[str] = Field(..., max_length=500)
|
||||
action: str # enable / disable / delete / clear_cooldown / reset_cost
|
||||
action: str # enable / disable / delete / clear_cooldown / reset_cost / regenerate_fingerprint
|
||||
|
||||
|
||||
class BatchActionResponse(BaseModel):
|
||||
|
||||
@@ -1491,8 +1491,11 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
||||
)
|
||||
encrypted_auth_config = crypto_service.encrypt(auth_config_str)
|
||||
|
||||
from src.services.provider.fingerprint import generate_fingerprint
|
||||
|
||||
new_key_id = str(uuid.uuid4())
|
||||
new_key = ProviderAPIKey(
|
||||
id=str(uuid.uuid4()),
|
||||
id=new_key_id,
|
||||
provider_id=provider_id,
|
||||
api_formats=normalized_formats,
|
||||
auth_type=key_data.get("auth_type", "api_key"),
|
||||
@@ -1514,6 +1517,7 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
||||
model_exclude_patterns=key_data.get("model_exclude_patterns"),
|
||||
is_active=key_data.get("is_active", True),
|
||||
proxy=key_data.get("proxy"),
|
||||
fingerprint=generate_fingerprint(seed=new_key_id),
|
||||
health_by_format={},
|
||||
circuit_breaker_by_format={},
|
||||
)
|
||||
|
||||
@@ -77,6 +77,8 @@ from src.models.database import (
|
||||
User,
|
||||
)
|
||||
from src.services.provider.behavior import get_provider_behavior
|
||||
from src.services.provider.fingerprint import ensure_key_fingerprint
|
||||
from src.services.provider.request_context import set_current_fingerprint
|
||||
from src.services.provider.stream_policy import (
|
||||
enforce_stream_mode_for_upstream,
|
||||
get_upstream_stream_policy,
|
||||
@@ -719,6 +721,8 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
else:
|
||||
request_body = dict(original_request_body)
|
||||
|
||||
set_current_fingerprint(ensure_key_fingerprint(key, persist_if_missing=True))
|
||||
|
||||
provider_type = str(getattr(provider, "provider_type", "") or "").lower()
|
||||
behavior = get_provider_behavior(
|
||||
provider_type=provider_type,
|
||||
|
||||
@@ -40,6 +40,8 @@ from src.core.exceptions import (
|
||||
)
|
||||
from src.core.logger import logger
|
||||
from src.services.provider.behavior import get_provider_behavior
|
||||
from src.services.provider.fingerprint import ensure_key_fingerprint
|
||||
from src.services.provider.request_context import set_current_fingerprint
|
||||
from src.services.provider.stream_policy import (
|
||||
enforce_stream_mode_for_upstream,
|
||||
get_upstream_stream_policy,
|
||||
@@ -325,6 +327,8 @@ class CliStreamMixin:
|
||||
)
|
||||
ctx.needs_conversion = needs_conversion
|
||||
|
||||
set_current_fingerprint(ensure_key_fingerprint(key, persist_if_missing=True))
|
||||
|
||||
provider_type = str(getattr(provider, "provider_type", "") or "").lower()
|
||||
behavior = get_provider_behavior(
|
||||
provider_type=provider_type,
|
||||
|
||||
@@ -33,6 +33,8 @@ from src.core.exceptions import (
|
||||
)
|
||||
from src.core.logger import logger
|
||||
from src.services.provider.behavior import get_provider_behavior
|
||||
from src.services.provider.fingerprint import ensure_key_fingerprint
|
||||
from src.services.provider.request_context import set_current_fingerprint
|
||||
from src.services.provider.stream_policy import (
|
||||
enforce_stream_mode_for_upstream,
|
||||
get_upstream_stream_policy,
|
||||
@@ -141,6 +143,8 @@ class CliSyncMixin:
|
||||
)
|
||||
needs_conversion = bool(getattr(candidate, "needs_conversion", False))
|
||||
|
||||
set_current_fingerprint(ensure_key_fingerprint(key, persist_if_missing=True))
|
||||
|
||||
provider_type = str(getattr(provider, "provider_type", "") or "").lower()
|
||||
behavior = get_provider_behavior(
|
||||
provider_type=provider_type,
|
||||
|
||||
@@ -22,6 +22,8 @@ from typing import Any
|
||||
from src.core.api_format import (
|
||||
UPSTREAM_DROP_HEADERS,
|
||||
HeaderBuilder,
|
||||
build_anthropic_extra_headers,
|
||||
build_browser_fingerprint_headers,
|
||||
get_auth_config_for_endpoint,
|
||||
make_signature_key,
|
||||
)
|
||||
@@ -29,6 +31,7 @@ from src.core.crypto import crypto_service
|
||||
from src.models.endpoint_models import _CONDITION_OPS, _TYPE_IS_VALUES, parse_re_flags
|
||||
from src.services.provider.auth import get_provider_auth # noqa: F401
|
||||
from src.services.provider.envelope import ProviderEnvelope
|
||||
from src.services.provider.request_context import get_current_fingerprint
|
||||
|
||||
# ==============================================================================
|
||||
# 统一的头部配置常量
|
||||
@@ -1202,6 +1205,17 @@ class PassthroughRequestBuilder(RequestBuilder):
|
||||
pre_computed_auth: 预先计算的认证信息 (auth_header, auth_value),
|
||||
用于 Service Account 等异步获取 token 的场景
|
||||
"""
|
||||
raw_family = getattr(endpoint, "api_family", None)
|
||||
raw_kind = getattr(endpoint, "endpoint_kind", None)
|
||||
endpoint_sig: str | None = None
|
||||
if isinstance(raw_family, str) and isinstance(raw_kind, str) and raw_family and raw_kind:
|
||||
endpoint_sig = make_signature_key(raw_family, raw_kind)
|
||||
else:
|
||||
# 兜底:允许 endpoint.api_format 已经是 signature key 的情况
|
||||
raw_format = getattr(endpoint, "api_format", None)
|
||||
if isinstance(raw_format, str) and ":" in raw_format:
|
||||
endpoint_sig = raw_format
|
||||
|
||||
# 1. 根据 API 格式自动设置认证头
|
||||
if pre_computed_auth:
|
||||
# 使用预先计算的认证信息(Service Account 等场景)
|
||||
@@ -1209,21 +1223,6 @@ class PassthroughRequestBuilder(RequestBuilder):
|
||||
else:
|
||||
# 标准 API Key 认证
|
||||
decrypted_key = crypto_service.decrypt(key.api_key)
|
||||
raw_family = getattr(endpoint, "api_family", None)
|
||||
raw_kind = getattr(endpoint, "endpoint_kind", None)
|
||||
endpoint_sig: str | None = None
|
||||
if (
|
||||
isinstance(raw_family, str)
|
||||
and isinstance(raw_kind, str)
|
||||
and raw_family
|
||||
and raw_kind
|
||||
):
|
||||
endpoint_sig = make_signature_key(raw_family, raw_kind)
|
||||
else:
|
||||
# 兜底:允许 endpoint.api_format 已经是 signature key 的情况
|
||||
raw_format = getattr(endpoint, "api_format", None)
|
||||
if isinstance(raw_format, str) and ":" in raw_format:
|
||||
endpoint_sig = raw_format
|
||||
|
||||
auth_header, auth_type = get_auth_config_for_endpoint(endpoint_sig or "openai:chat")
|
||||
auth_value = f"Bearer {decrypted_key}" if auth_type == "bearer" else decrypted_key
|
||||
@@ -1244,7 +1243,13 @@ class PassthroughRequestBuilder(RequestBuilder):
|
||||
if header_rules:
|
||||
builder.apply_rules(header_rules, protected_keys)
|
||||
|
||||
# 4. 添加额外头部
|
||||
# 4. 注入 per-key 指纹头(仅 Claude 格式需要浏览器指纹绕过 Cloudflare 检测)。
|
||||
if str(endpoint_sig or "").strip().lower().startswith("claude:"):
|
||||
fp = get_current_fingerprint()
|
||||
builder.add_many(build_browser_fingerprint_headers(fp))
|
||||
builder.add_many(build_anthropic_extra_headers(fp))
|
||||
|
||||
# 5. 添加额外头部
|
||||
effective_extra_headers = self._merge_extra_headers_with_original(
|
||||
original_headers,
|
||||
extra_headers,
|
||||
@@ -1253,10 +1258,10 @@ class PassthroughRequestBuilder(RequestBuilder):
|
||||
if effective_extra_headers:
|
||||
builder.add_many(effective_extra_headers)
|
||||
|
||||
# 5. 设置认证头(最高优先级,上游始终使用 header 认证)
|
||||
# 6. 设置认证头(最高优先级,上游始终使用 header 认证)
|
||||
builder.add(auth_header, auth_value)
|
||||
|
||||
# 6. 确保有 Content-Type
|
||||
# 7. 确保有 Content-Type
|
||||
headers = builder.build()
|
||||
if not any(k.lower() == "content-type" for k in headers):
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
Reference in New Issue
Block a user