feat(provider): 增加 Claude Code 适配器、高级配置能力与 OAuth 账号类型统一解析

- 新增 Claude Code provider adapter (context, envelope, plugin, constants)
- 扩展 provider admin 路由,支持 Claude Code 高级配置 (CRUD)
- 统一 OAuth 账号类型解析逻辑,前后端对齐
- 重构 BatchAssignModelsDialog / ModelMappingDialog,简化组件逻辑
- handler 基类增强: request_builder 支持 Claude Code 信封格式
- CLI stream/sync mixin 适配 Claude Code 流式与同步模式
- 扩展 candidate builder / failover / scheduler 对 Claude Code 的支持
- 前端增加请求时间线可视化 (HorizontalRequestTimeline)
- 补充 Claude Code envelope / runtime controls / distributed sessions 等测试

Closes #183
Closes #185

Co-Authored-By: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
fawney19
2026-02-27 13:54:46 +08:00
parent f2f2a2dbc4
commit 579b5e4623
55 changed files with 3106 additions and 652 deletions

View File

@@ -0,0 +1,192 @@
from __future__ import annotations
import uuid
import pytest
from src.core.exceptions import ConcurrencyLimitError
from src.services.provider.adapters.claude_code.constants import TLS_PROFILE_CLAUDE_CODE
from src.services.provider.adapters.claude_code.context import (
ClaudeCodeRequestContext,
build_and_set_claude_code_request_context,
build_claude_code_request_context,
get_claude_code_request_context,
resolve_claude_code_tls_profile,
set_claude_code_request_context,
)
from src.services.provider.adapters.claude_code.envelope import claude_code_envelope
from src.utils.ssl_utils import get_ssl_context, get_ssl_context_for_profile
@pytest.fixture(autouse=True)
def _reset_claude_code_context():
set_claude_code_request_context(None)
yield
set_claude_code_request_context(None)
def _session_tail(user_id: str) -> str:
return user_id.split("_session_")[-1]
def test_build_context_reads_claude_code_advanced_from_provider_config() -> None:
ctx = build_claude_code_request_context(
provider_config={
"claude_code_advanced": {
"max_sessions": 3,
"session_idle_timeout_minutes": 7,
"enable_tls_fingerprint": True,
"session_id_masking_enabled": True,
}
},
key_id="key-123",
is_stream=False,
)
assert ctx.scope_key == "key:key-123"
assert ctx.key_id == "key-123"
assert ctx.max_sessions == 3
assert ctx.session_idle_timeout_minutes == 7
assert ctx.enable_tls_fingerprint is True
assert ctx.session_id_masking_enabled is True
def test_build_and_set_context_returns_tls_profile_when_enabled() -> None:
ctx, tls_profile = build_and_set_claude_code_request_context(
provider_config={"claude_code_advanced": {"enable_tls_fingerprint": True}},
key_id="key-tls",
is_stream=True,
)
assert ctx.key_id == "key-tls"
assert get_claude_code_request_context() == ctx
assert tls_profile == TLS_PROFILE_CLAUDE_CODE
assert resolve_claude_code_tls_profile(ctx) == TLS_PROFILE_CLAUDE_CODE
def test_get_ssl_context_for_claude_code_profile_is_cached() -> None:
first = get_ssl_context_for_profile(TLS_PROFILE_CLAUDE_CODE)
second = get_ssl_context_for_profile(TLS_PROFILE_CLAUDE_CODE)
assert first is second
def test_get_ssl_context_for_unknown_profile_falls_back_to_default() -> None:
assert get_ssl_context_for_profile("unknown_profile") is get_ssl_context()
def test_wrap_request_masks_session_id_when_enabled() -> None:
scope_key = f"key:test-mask-{uuid.uuid4()}"
set_claude_code_request_context(
ClaudeCodeRequestContext(
is_stream=False,
scope_key=scope_key,
key_id="key-mask",
session_id_masking_enabled=True,
)
)
body1 = {
"metadata": {
"user_id": "user_client_account_main_session_11111111-1111-1111-1111-111111111111"
}
}
body2 = {
"metadata": {
"user_id": "user_client_account_main_session_22222222-2222-2222-2222-222222222222"
}
}
wrapped1, _ = claude_code_envelope.wrap_request(
body1,
model="claude-sonnet-4-5-20250929",
url_model=None,
decrypted_auth_config=None,
)
wrapped2, _ = claude_code_envelope.wrap_request(
body2,
model="claude-sonnet-4-5-20250929",
url_model=None,
decrypted_auth_config=None,
)
tail1 = _session_tail(wrapped1["metadata"]["user_id"])
tail2 = _session_tail(wrapped2["metadata"]["user_id"])
assert tail1 != "11111111-1111-1111-1111-111111111111"
assert tail1 == tail2
def test_wrap_request_enforces_max_sessions() -> None:
scope_key = f"key:test-limit-{uuid.uuid4()}"
set_claude_code_request_context(
ClaudeCodeRequestContext(
is_stream=False,
scope_key=scope_key,
key_id="key-limit",
max_sessions=1,
session_idle_timeout_minutes=5,
)
)
first = {
"metadata": {"user_id": "user_a_account_b_session_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}
}
second = {
"metadata": {"user_id": "user_a_account_b_session_bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}
}
claude_code_envelope.wrap_request(
first,
model="claude-sonnet-4-5-20250929",
url_model=None,
decrypted_auth_config=None,
)
with pytest.raises(ConcurrencyLimitError):
claude_code_envelope.wrap_request(
second,
model="claude-sonnet-4-5-20250929",
url_model=None,
decrypted_auth_config=None,
)
def test_wrap_request_releases_expired_sessions(monkeypatch: pytest.MonkeyPatch) -> None:
scope_key = f"key:test-expire-{uuid.uuid4()}"
set_claude_code_request_context(
ClaudeCodeRequestContext(
is_stream=False,
scope_key=scope_key,
key_id="key-expire",
max_sessions=1,
session_idle_timeout_minutes=1,
)
)
ticks = iter([0.0, 61.0])
monkeypatch.setattr(
"src.services.provider.adapters.claude_code.envelope.time.monotonic",
lambda: next(ticks),
)
first = {
"metadata": {"user_id": "user_a_account_b_session_11111111-1111-1111-1111-111111111111"}
}
second = {
"metadata": {"user_id": "user_a_account_b_session_22222222-2222-2222-2222-222222222222"}
}
claude_code_envelope.wrap_request(
first,
model="claude-sonnet-4-5-20250929",
url_model=None,
decrypted_auth_config=None,
)
# 61s > 1min idle timeout旧会话应过期第二个会话可进入。
claude_code_envelope.wrap_request(
second,
model="claude-sonnet-4-5-20250929",
url_model=None,
decrypted_auth_config=None,
)