mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +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:
@@ -21,13 +21,17 @@ from src.services.provider.adapters.claude_code.envelope import (
|
||||
claude_code_envelope,
|
||||
merge_anthropic_beta_tokens,
|
||||
)
|
||||
from src.services.provider.fingerprint import load_fingerprint
|
||||
from src.services.provider.request_context import set_current_fingerprint
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_claude_code_context():
|
||||
def _reset_claude_code_context() -> None: # type: ignore[misc]
|
||||
set_claude_code_request_context(None)
|
||||
set_current_fingerprint(None)
|
||||
yield
|
||||
set_claude_code_request_context(None)
|
||||
set_current_fingerprint(None)
|
||||
|
||||
|
||||
def test_merge_anthropic_beta_tokens_adds_required_and_deduplicates() -> None:
|
||||
@@ -45,7 +49,7 @@ def test_merge_anthropic_beta_tokens_adds_required_and_deduplicates() -> None:
|
||||
|
||||
|
||||
def test_claude_code_envelope_extra_headers_include_required_defaults(
|
||||
monkeypatch,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(config, "internal_user_agent_claude_cli", "claude-code/test")
|
||||
|
||||
@@ -61,6 +65,43 @@ def test_claude_code_envelope_extra_headers_include_required_defaults(
|
||||
assert "x-stainless-helper-method" not in headers
|
||||
|
||||
|
||||
def test_claude_code_envelope_extra_headers_use_current_fingerprint() -> None:
|
||||
fp = load_fingerprint(
|
||||
{
|
||||
"stainless_package_version": "1.0.5",
|
||||
"stainless_os": "Windows",
|
||||
"stainless_arch": "x64",
|
||||
"stainless_runtime_version": "v22.12.0",
|
||||
"stainless_timeout": "900",
|
||||
"user_agent": "Mozilla/5.0 test-fingerprint",
|
||||
},
|
||||
"key-fingerprint-1",
|
||||
)
|
||||
set_current_fingerprint(fp)
|
||||
|
||||
headers = claude_code_envelope.extra_headers() or {}
|
||||
assert headers.get("X-Stainless-Package-Version") == "1.0.5"
|
||||
assert headers.get("X-Stainless-OS") == "Windows"
|
||||
assert headers.get("X-Stainless-Arch") == "x64"
|
||||
assert headers.get("X-Stainless-Runtime-Version") == "v22.12.0"
|
||||
assert headers.get("X-Stainless-Timeout") == "900"
|
||||
assert headers.get("User-Agent") == "Mozilla/5.0 test-fingerprint"
|
||||
|
||||
|
||||
def test_claude_code_prepare_context_prefers_fingerprint_tls_profile() -> None:
|
||||
fp = load_fingerprint({"impersonate": "chrome124"}, "key-fingerprint-2")
|
||||
set_current_fingerprint(fp)
|
||||
|
||||
tls_profile = claude_code_envelope.prepare_context(
|
||||
provider_config={"claude_code_advanced": {"enable_tls_fingerprint": True}},
|
||||
key_id="key-fingerprint-2",
|
||||
is_stream=False,
|
||||
provider_id="provider-1",
|
||||
)
|
||||
|
||||
assert tls_profile == "chrome124"
|
||||
|
||||
|
||||
def test_claude_code_envelope_adds_stream_helper_header_for_stream_request() -> None:
|
||||
_, _ = claude_code_envelope.wrap_request(
|
||||
{"stream": True},
|
||||
|
||||
39
tests/services/test_provider_fingerprint.py
Normal file
39
tests/services/test_provider_fingerprint.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.services.provider.fingerprint import (
|
||||
CHROME_IMPERSONATE_PROFILES,
|
||||
ensure_key_fingerprint,
|
||||
generate_fingerprint,
|
||||
load_fingerprint,
|
||||
)
|
||||
|
||||
|
||||
def test_generate_fingerprint_is_deterministic_with_seed() -> None:
|
||||
first = generate_fingerprint(seed="key-123")
|
||||
second = generate_fingerprint(seed="key-123")
|
||||
assert first == second
|
||||
|
||||
|
||||
def test_load_fingerprint_falls_back_for_invalid_impersonate() -> None:
|
||||
profile = load_fingerprint({"impersonate": "invalid-profile"}, "key-abc")
|
||||
assert profile.impersonate in CHROME_IMPERSONATE_PROFILES
|
||||
|
||||
|
||||
def test_ensure_key_fingerprint_returns_profile_when_missing() -> None:
|
||||
key = SimpleNamespace(id="key-xyz", fingerprint=None)
|
||||
profile = ensure_key_fingerprint(key, persist_if_missing=False)
|
||||
|
||||
assert profile.impersonate in CHROME_IMPERSONATE_PROFILES
|
||||
# Deterministic: same key_id produces same profile
|
||||
profile2 = ensure_key_fingerprint(key, persist_if_missing=False)
|
||||
assert profile.impersonate == profile2.impersonate
|
||||
|
||||
|
||||
def test_ensure_key_fingerprint_uses_existing_fingerprint() -> None:
|
||||
existing = generate_fingerprint(seed="key-existing")
|
||||
key = SimpleNamespace(id="key-existing", fingerprint=existing)
|
||||
profile = ensure_key_fingerprint(key, persist_if_missing=False)
|
||||
|
||||
assert profile.impersonate == existing["impersonate"]
|
||||
Reference in New Issue
Block a user