mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
352
_deprecated_py_src/services/provider/fingerprint.py
Normal file
352
_deprecated_py_src/services/provider/fingerprint.py
Normal file
@@ -0,0 +1,352 @@
|
||||
"""Per-key request fingerprint generation and lazy persistence helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import random
|
||||
import secrets
|
||||
import threading
|
||||
import uuid
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from src.core.logger import logger
|
||||
|
||||
# curl_cffi impersonate profile pool (Chrome family)
|
||||
CHROME_IMPERSONATE_PROFILES: tuple[str, ...] = (
|
||||
"chrome110",
|
||||
"chrome116",
|
||||
"chrome119",
|
||||
"chrome120",
|
||||
"chrome123",
|
||||
"chrome124",
|
||||
"chrome131",
|
||||
"chrome133",
|
||||
)
|
||||
|
||||
KNOWN_IMPERSONATE_PROFILES: frozenset[str] = frozenset(CHROME_IMPERSONATE_PROFILES)
|
||||
|
||||
_CHROME_VERSION_BY_PROFILE: dict[str, str] = {
|
||||
"chrome110": "110.0.5481.177",
|
||||
"chrome116": "116.0.5845.188",
|
||||
"chrome119": "119.0.6045.214",
|
||||
"chrome120": "120.0.6099.216",
|
||||
"chrome123": "123.0.6312.122",
|
||||
"chrome124": "124.0.6367.243",
|
||||
"chrome131": "131.0.6778.265",
|
||||
"chrome133": "133.0.6943.142",
|
||||
}
|
||||
|
||||
_PLATFORM_VARIANTS: tuple[tuple[str, str, str, str], ...] = (
|
||||
("Linux", "x64", "X11; Linux x86_64", "Linux x86_64"),
|
||||
("Linux", "arm64", "X11; Linux arm64", "Linux arm64"),
|
||||
("Windows", "x64", "Windows NT 10.0; Win64; x64", "Windows x64"),
|
||||
("MacOS", "x64", "Macintosh; Intel Mac OS X 10_15_7", "Darwin x64"),
|
||||
("MacOS", "arm64", "Macintosh; ARM Mac OS X 14_0_0", "Darwin arm64"),
|
||||
)
|
||||
|
||||
_STAINLESS_PACKAGE_VERSIONS: tuple[str, ...] = ("0.68.0", "0.69.0", "0.70.0", "0.71.0")
|
||||
_NODE_VERSIONS: tuple[str, ...] = ("v20.18.1", "v22.12.0", "v22.14.0", "v24.13.0")
|
||||
_ELECTRON_VERSIONS: tuple[str, ...] = ("35.5.1", "36.7.1", "37.3.0", "38.7.0", "39.2.3")
|
||||
_STAINLESS_TIMEOUTS: tuple[str, ...] = ("600", "900")
|
||||
|
||||
_PENDING_LAZY_PERSIST: set[str] = set()
|
||||
_PENDING_LAZY_PERSIST_LOCK = threading.Lock()
|
||||
_PENDING_LAZY_PERSIST_MAX = 2000
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FingerprintProfile:
|
||||
# TLS layer
|
||||
impersonate: str
|
||||
# Claude Code / Claude Chat feature dimensions
|
||||
stainless_package_version: str
|
||||
stainless_os: str
|
||||
stainless_arch: str
|
||||
stainless_runtime_version: str
|
||||
stainless_timeout: str
|
||||
# Generic dimensions
|
||||
user_agent: str
|
||||
node_version: str
|
||||
chrome_version: str
|
||||
electron_version: str
|
||||
vscode_session_id: str
|
||||
platform_info: str
|
||||
|
||||
|
||||
def _build_rng(seed: str | None) -> random.Random:
|
||||
if seed is None:
|
||||
return random.Random(secrets.randbits(64))
|
||||
digest = hashlib.sha256(seed.encode("utf-8")).digest()
|
||||
return random.Random(int.from_bytes(digest[:8], "big", signed=False))
|
||||
|
||||
|
||||
def _normalize_impersonate(raw: Any, fallback: str) -> str:
|
||||
value = str(raw or "").strip().lower()
|
||||
return value if value in KNOWN_IMPERSONATE_PROFILES else fallback
|
||||
|
||||
|
||||
def _build_user_agent(
|
||||
platform_token: str,
|
||||
chrome_version: str,
|
||||
electron_version: str,
|
||||
) -> str:
|
||||
return (
|
||||
f"Mozilla/5.0 ({platform_token}) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
f"Chrome/{chrome_version} Electron/{electron_version} Safari/537.36"
|
||||
)
|
||||
|
||||
|
||||
def resolve_platform_token(
|
||||
fp: dict[str, str] | None = None,
|
||||
*,
|
||||
platform_info: str | None = None,
|
||||
stainless_os: str | None = None,
|
||||
stainless_arch: str | None = None,
|
||||
) -> str:
|
||||
"""Resolve a platform token string from fingerprint dict or explicit kwargs."""
|
||||
if fp is not None:
|
||||
os_name = str(fp.get("stainless_os") or "").lower()
|
||||
arch = str(fp.get("stainless_arch") or "").lower()
|
||||
info = str(fp.get("platform_info") or "").lower()
|
||||
else:
|
||||
os_name = str(stainless_os or "").lower()
|
||||
arch = str(stainless_arch or "").lower()
|
||||
info = str(platform_info or "").lower()
|
||||
|
||||
if os_name.startswith("win") or "windows" in info:
|
||||
return "Windows NT 10.0; Win64; x64"
|
||||
if os_name in {"darwin", "mac", "macos"} or "darwin" in info:
|
||||
return (
|
||||
"Macintosh; ARM Mac OS X 14_0_0"
|
||||
if arch in {"arm64", "aarch64"}
|
||||
else "Macintosh; Intel Mac OS X 10_15_7"
|
||||
)
|
||||
return "X11; Linux arm64" if arch in {"arm64", "aarch64"} else "X11; Linux x86_64"
|
||||
|
||||
|
||||
def _normalize_text(value: Any, fallback: str) -> str:
|
||||
text = str(value or "").strip()
|
||||
return text or fallback
|
||||
|
||||
|
||||
def _sanitize_fingerprint_dict(raw: dict[str, Any], key_id: str) -> dict[str, str]:
|
||||
generated = generate_fingerprint(seed=key_id or None)
|
||||
fp: dict[str, str] = {k: str(v) for k, v in generated.items()}
|
||||
for key, value in raw.items():
|
||||
if isinstance(value, str) and value.strip():
|
||||
fp[key] = value.strip()
|
||||
|
||||
fp["impersonate"] = _normalize_impersonate(fp.get("impersonate"), generated["impersonate"])
|
||||
fp["chrome_version"] = _normalize_text(
|
||||
fp.get("chrome_version"),
|
||||
_CHROME_VERSION_BY_PROFILE.get(fp["impersonate"], generated["chrome_version"]),
|
||||
)
|
||||
fp["node_version"] = _normalize_text(fp.get("node_version"), generated["node_version"])
|
||||
fp["electron_version"] = _normalize_text(
|
||||
fp.get("electron_version"),
|
||||
generated["electron_version"],
|
||||
)
|
||||
fp["stainless_package_version"] = _normalize_text(
|
||||
fp.get("stainless_package_version"),
|
||||
generated["stainless_package_version"],
|
||||
)
|
||||
fp["stainless_os"] = _normalize_text(fp.get("stainless_os"), generated["stainless_os"])
|
||||
fp["stainless_arch"] = _normalize_text(fp.get("stainless_arch"), generated["stainless_arch"])
|
||||
fp["stainless_runtime_version"] = _normalize_text(
|
||||
fp.get("stainless_runtime_version"),
|
||||
generated["stainless_runtime_version"],
|
||||
)
|
||||
fp["stainless_timeout"] = _normalize_text(
|
||||
fp.get("stainless_timeout"),
|
||||
generated["stainless_timeout"],
|
||||
)
|
||||
fp["platform_info"] = _normalize_text(fp.get("platform_info"), generated["platform_info"])
|
||||
fp["vscode_session_id"] = _normalize_text(
|
||||
fp.get("vscode_session_id"),
|
||||
generated["vscode_session_id"],
|
||||
)
|
||||
|
||||
if not str(fp.get("user_agent") or "").strip():
|
||||
fp["user_agent"] = _build_user_agent(
|
||||
resolve_platform_token(fp),
|
||||
fp["chrome_version"],
|
||||
fp["electron_version"],
|
||||
)
|
||||
|
||||
return fp
|
||||
|
||||
|
||||
def generate_fingerprint(seed: str | None = None) -> dict[str, str]:
|
||||
"""Generate a serializable fingerprint profile dict."""
|
||||
rng = _build_rng(seed)
|
||||
|
||||
impersonate = rng.choice(CHROME_IMPERSONATE_PROFILES)
|
||||
chrome_version = _CHROME_VERSION_BY_PROFILE.get(impersonate, "120.0.6099.216")
|
||||
node_version = rng.choice(_NODE_VERSIONS)
|
||||
electron_version = rng.choice(_ELECTRON_VERSIONS)
|
||||
stainless_os, stainless_arch, platform_token, platform_info = rng.choice(_PLATFORM_VARIANTS)
|
||||
|
||||
if seed is None:
|
||||
vscode_session_id = uuid.uuid4().hex
|
||||
else:
|
||||
vscode_session_id = uuid.uuid5(uuid.NAMESPACE_URL, f"aether:fingerprint:{seed}").hex
|
||||
|
||||
return {
|
||||
"impersonate": impersonate,
|
||||
"stainless_package_version": rng.choice(_STAINLESS_PACKAGE_VERSIONS),
|
||||
"stainless_os": stainless_os,
|
||||
"stainless_arch": stainless_arch,
|
||||
"stainless_runtime_version": node_version,
|
||||
"stainless_timeout": rng.choice(_STAINLESS_TIMEOUTS),
|
||||
"node_version": node_version,
|
||||
"chrome_version": chrome_version,
|
||||
"electron_version": electron_version,
|
||||
"vscode_session_id": vscode_session_id,
|
||||
"platform_info": platform_info,
|
||||
"user_agent": _build_user_agent(platform_token, chrome_version, electron_version),
|
||||
}
|
||||
|
||||
|
||||
def _dict_to_profile(d: dict[str, str]) -> FingerprintProfile:
|
||||
return FingerprintProfile(
|
||||
impersonate=d["impersonate"],
|
||||
stainless_package_version=d["stainless_package_version"],
|
||||
stainless_os=d["stainless_os"],
|
||||
stainless_arch=d["stainless_arch"],
|
||||
stainless_runtime_version=d["stainless_runtime_version"],
|
||||
stainless_timeout=d["stainless_timeout"],
|
||||
user_agent=d["user_agent"],
|
||||
node_version=d["node_version"],
|
||||
chrome_version=d["chrome_version"],
|
||||
electron_version=d["electron_version"],
|
||||
vscode_session_id=d["vscode_session_id"],
|
||||
platform_info=d["platform_info"],
|
||||
)
|
||||
|
||||
|
||||
def load_fingerprint(raw: dict[str, Any] | None, key_id: str) -> FingerprintProfile:
|
||||
"""Load fingerprint from DB JSON with deterministic fallback by key_id."""
|
||||
if raw and isinstance(raw, dict):
|
||||
normalized = _sanitize_fingerprint_dict(raw, key_id)
|
||||
else:
|
||||
normalized = generate_fingerprint(seed=key_id or None)
|
||||
return _dict_to_profile(normalized)
|
||||
|
||||
|
||||
def serialize_fingerprint(fp: FingerprintProfile) -> dict[str, str]:
|
||||
return {k: str(v) for k, v in asdict(fp).items()}
|
||||
|
||||
|
||||
def normalize_fingerprint(raw: dict[str, Any], key_id: str) -> dict[str, str]:
|
||||
return serialize_fingerprint(load_fingerprint(raw, key_id))
|
||||
|
||||
|
||||
def _persist_fingerprint_if_missing_sync(key_id: str, fp: dict[str, str]) -> None:
|
||||
from src.database import create_session
|
||||
from src.models.database import ProviderAPIKey
|
||||
|
||||
db = create_session()
|
||||
try:
|
||||
updated = (
|
||||
db.query(ProviderAPIKey)
|
||||
.filter(
|
||||
ProviderAPIKey.id == key_id,
|
||||
ProviderAPIKey.fingerprint.is_(None),
|
||||
)
|
||||
.update(
|
||||
{
|
||||
ProviderAPIKey.fingerprint: fp,
|
||||
ProviderAPIKey.updated_at: datetime.now(timezone.utc),
|
||||
},
|
||||
synchronize_session=False,
|
||||
)
|
||||
)
|
||||
if updated > 0:
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
logger.debug("lazy fingerprint persist failed for key {}: {}", key_id[:8], str(exc))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _mark_pending_persist(key_id: str) -> bool:
|
||||
with _PENDING_LAZY_PERSIST_LOCK:
|
||||
if key_id in _PENDING_LAZY_PERSIST:
|
||||
return False
|
||||
if len(_PENDING_LAZY_PERSIST) >= _PENDING_LAZY_PERSIST_MAX:
|
||||
return False
|
||||
_PENDING_LAZY_PERSIST.add(key_id)
|
||||
return True
|
||||
|
||||
|
||||
def _clear_pending_persist(key_id: str) -> None:
|
||||
with _PENDING_LAZY_PERSIST_LOCK:
|
||||
_PENDING_LAZY_PERSIST.discard(key_id)
|
||||
|
||||
|
||||
def schedule_lazy_fingerprint_persist(key_id: str, fp: dict[str, str]) -> None:
|
||||
"""Persist generated fingerprint in background when key.fingerprint is missing."""
|
||||
key_id = str(key_id or "").strip()
|
||||
if not key_id:
|
||||
return
|
||||
if not _mark_pending_persist(key_id):
|
||||
return
|
||||
|
||||
payload = dict(fp)
|
||||
|
||||
async def _persist_async() -> None:
|
||||
try:
|
||||
await asyncio.to_thread(_persist_fingerprint_if_missing_sync, key_id, payload)
|
||||
finally:
|
||||
_clear_pending_persist(key_id)
|
||||
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
try:
|
||||
_persist_fingerprint_if_missing_sync(key_id, payload)
|
||||
finally:
|
||||
_clear_pending_persist(key_id)
|
||||
return
|
||||
|
||||
from src.utils.async_utils import safe_create_task
|
||||
|
||||
safe_create_task(_persist_async())
|
||||
|
||||
|
||||
def ensure_key_fingerprint(
|
||||
key: Any,
|
||||
*,
|
||||
persist_if_missing: bool = False,
|
||||
) -> FingerprintProfile:
|
||||
"""Get a key fingerprint, generating deterministic fallback if missing."""
|
||||
key_id = str(getattr(key, "id", "") or "").strip()
|
||||
raw = getattr(key, "fingerprint", None)
|
||||
|
||||
if isinstance(raw, dict) and raw:
|
||||
return load_fingerprint(raw, key_id)
|
||||
|
||||
generated = generate_fingerprint(seed=key_id or None)
|
||||
|
||||
if persist_if_missing and key_id:
|
||||
schedule_lazy_fingerprint_persist(key_id, generated)
|
||||
|
||||
return _dict_to_profile(generated)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CHROME_IMPERSONATE_PROFILES",
|
||||
"FingerprintProfile",
|
||||
"KNOWN_IMPERSONATE_PROFILES",
|
||||
"ensure_key_fingerprint",
|
||||
"generate_fingerprint",
|
||||
"load_fingerprint",
|
||||
"normalize_fingerprint",
|
||||
"resolve_platform_token",
|
||||
"schedule_lazy_fingerprint_persist",
|
||||
"serialize_fingerprint",
|
||||
]
|
||||
Reference in New Issue
Block a user