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:
fawney19
2026-04-03 16:26:16 +08:00
parent 8f26e1a31f
commit 1d9c77522a
868 changed files with 1735 additions and 2433 deletions

View File

@@ -0,0 +1,29 @@
"""
Provider 架构模块
"""
from src.services.provider_ops.architectures.anyrouter import AnyrouterArchitecture
from src.services.provider_ops.architectures.base import (
ProviderArchitecture,
ProviderConnector,
VerifyResult,
)
from src.services.provider_ops.architectures.cubence import CubenceArchitecture
from src.services.provider_ops.architectures.generic_api import GenericApiArchitecture
from src.services.provider_ops.architectures.nekocode import NekoCodeArchitecture
from src.services.provider_ops.architectures.new_api import NewApiArchitecture
from src.services.provider_ops.architectures.sub2api import Sub2ApiArchitecture
from src.services.provider_ops.architectures.yescode import YesCodeArchitecture
__all__ = [
"ProviderArchitecture",
"ProviderConnector",
"VerifyResult",
"AnyrouterArchitecture",
"CubenceArchitecture",
"GenericApiArchitecture",
"NekoCodeArchitecture",
"NewApiArchitecture",
"Sub2ApiArchitecture",
"YesCodeArchitecture",
]

View File

@@ -0,0 +1,469 @@
"""
Anyrouter 架构
针对 Anyrouter 中转站的预设配置,自动处理 acw_sc__v2 反爬 Cookie。
"""
import base64
import re
from typing import Any
import httpx
from src.core.logger import logger
from src.services.provider_ops.actions import (
AnyrouterBalanceAction,
ProviderAction,
)
from src.services.provider_ops.architectures.base import (
ProviderArchitecture,
ProviderConnector,
)
from src.services.provider_ops.types import ConnectorAuthType, ProviderActionType
from src.services.provider_ops.utils import extract_cookie_value
from src.utils.ssl_utils import get_ssl_context
# acw_sc__v2 算法常量
_XOR_KEY = "3000176000856006061501533003690027800375"
_UNSBOX_TABLE = [
0xF,
0x23,
0x1D,
0x18,
0x21,
0x10,
0x1,
0x26,
0xA,
0x9,
0x13,
0x1F,
0x28,
0x1B,
0x16,
0x17,
0x19,
0xD,
0x6,
0xB,
0x27,
0x12,
0x14,
0x8,
0xE,
0x15,
0x20,
0x1A,
0x2,
0x1E,
0x7,
0x4,
0x11,
0x5,
0x3,
0x1C,
0x22,
0x25,
0xC,
0x24,
]
def _compute_acw_sc_v2(arg1: str) -> str:
"""
计算 acw_sc__v2 Cookie 值
Args:
arg1: 从 HTML 中提取的 40 位十六进制字符串
Returns:
计算后的 Cookie 值
"""
# Step 1: unsbox - 根据置换表重排字符
unsboxed = "".join(arg1[i - 1] for i in _UNSBOX_TABLE)
# Step 2: hexXor - 与密钥逐字节异或
result = ""
for i in range(0, 40, 2):
a = int(unsboxed[i : i + 2], 16)
b = int(_XOR_KEY[i : i + 2], 16)
xored = format(a ^ b, "02x")
result += xored
return result
def _parse_session_user_id(cookie_input: str) -> tuple[str | None, str | None]:
"""
从 session cookie 中解析用户 ID 和用户名
Anyrouter 的 session cookie 结构:
base64(timestamp|gob_base64|signature)
gob 数据中包含:
- id: 内部数字 ID (gob 编码的整数)
- username: 用户名
- role, status, group 等
Args:
cookie_input: Cookie 字符串或 session 值
Returns:
(user_id, username) 元组,解析失败则返回 (None, None)
"""
try:
# 先提取 session 值
session_cookie = extract_cookie_value(cookie_input, "session")
# 1. URL-safe base64 解码外层
padding = 4 - len(session_cookie) % 4
if padding != 4:
session_cookie += "=" * padding
decoded = base64.urlsafe_b64decode(session_cookie)
text = decoded.decode("utf-8", errors="replace")
# 2. 分割: timestamp|gob_base64|signature
parts = text.split("|")
if len(parts) < 2:
return None, None
# 3. 解码 gob 数据 (第二层 base64)
gob_b64 = parts[1]
padding2 = 4 - len(gob_b64) % 4
if padding2 != 4:
gob_b64 += "=" * padding2
gob_data = base64.urlsafe_b64decode(gob_b64)
# 4. 从 gob 数据中解析 id 字段
# 查找 "\x02id\x03int" 模式,后面跟着 gob 编码的整数
id_pattern = b"\x02id\x03int"
id_idx = gob_data.find(id_pattern)
user_id = None
if id_idx != -1:
# 跳过 "\x02id\x03int" (7字节) 和类型标记 (2字节)
value_start = id_idx + 7 + 2
if value_start < len(gob_data):
# 读取第一个字节,检查是否是 00正数标记
first_byte = gob_data[value_start]
if first_byte == 0:
# 下一个字节是长度标记
marker = gob_data[value_start + 1]
if marker >= 0x80:
# 负的表示长度: 256 - marker = 字节数
length = 256 - marker
if value_start + 2 + length <= len(gob_data):
# 读取 length 字节,大端序转整数
val = int.from_bytes(
gob_data[value_start + 2 : value_start + 2 + length],
"big",
)
# gob zigzag 解码:正整数用 2*n 编码
user_id = str(val >> 1)
# 5. 从 gob 数据中提取用户名
username = None
# 查找 username 字段后的值
# 格式: \x08username\x06string\x0c\x10\x00\x0elinuxdo_129083
# 其中 \x0e (14) 是用户名的长度
username_pattern = b"\x08username\x06string"
username_idx = gob_data.find(username_pattern)
if username_idx != -1:
# 跳过模式本身 (17字节) 和 \x0c\x10\x00 (3字节)
# 第 4 个字节是长度
length_pos = username_idx + len(username_pattern) + 3
if length_pos < len(gob_data):
length_byte = gob_data[length_pos]
value_start = length_pos + 1
if length_byte < 128 and value_start + length_byte <= len(gob_data):
username = gob_data[value_start : value_start + length_byte].decode(
"utf-8", errors="ignore"
)
return user_id, username
except Exception as e:
logger.debug(f"解析 Anyrouter session cookie 失败: {e}")
return None, None
async def _get_acw_cookie(
base_url: str,
timeout: float = 10,
proxy: str | httpx.Proxy | None = None,
tunnel_node_id: str | None = None,
) -> str | None:
"""
获取 acw_sc__v2 Cookie
首先请求目标 URL如果返回包含 arg1 的反爬页面,则计算 Cookie 值。
Args:
base_url: 目标站点 URL
timeout: 请求超时时间
proxy: 代理地址
tunnel_node_id: tunnel 模式节点 ID优先于 proxy
Returns:
Cookie 字符串 (acw_sc__v2=xxx),如果不需要或获取失败则返回 None
"""
try:
# 构建 client 参数
client_kwargs: dict[str, Any] = {
"timeout": timeout,
"verify": get_ssl_context(),
}
if tunnel_node_id:
from src.services.proxy_node.tunnel_transport import create_tunnel_transport
client_kwargs["transport"] = create_tunnel_transport(tunnel_node_id, timeout=timeout)
elif proxy:
client_kwargs["proxy"] = proxy
logger.debug(f"获取 acw_sc__v2 Cookie 使用代理: {proxy}")
async with httpx.AsyncClient(**client_kwargs) as client:
resp = await client.get(
base_url,
headers={
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
},
follow_redirects=False,
)
# 尝试从响应中提取 arg1
match = re.search(r"var\s+arg1\s*=\s*'([0-9a-fA-F]{40})'", resp.text)
if not match:
# 没有反爬页面,不需要 Cookie
return None
cookie_value = _compute_acw_sc_v2(match.group(1))
return f"acw_sc__v2={cookie_value}"
except Exception as e:
logger.debug(f"获取 acw_sc__v2 Cookie 失败: {e}")
return None
class AnyrouterConnector(ProviderConnector):
"""
Anyrouter 专用连接器
特点:
- 使用 Cookie 认证session
- 自动补充 acw_sc__v2 反爬 Cookie
- 自动解析 user_id 用于 New-Api-User header
"""
auth_type = ConnectorAuthType.COOKIE
display_name = "Anyrouter Cookie"
def __init__(self, base_url: str, config: dict[str, Any] | None = None):
super().__init__(base_url, config)
self._session_cookie: str | None = None
self._acw_cookie: str | None = None
self._user_id: str | None = None
async def connect(self, credentials: dict[str, Any]) -> bool:
"""建立连接"""
session_cookie = credentials.get("session_cookie")
if not session_cookie:
self._set_error("Session Cookie 不能为空")
return False
# 提取纯 session 值(支持完整 Cookie 字符串或仅 session 值)
self._session_cookie = extract_cookie_value(session_cookie, "session")
# 解析 user_id
self._user_id, _ = _parse_session_user_id(session_cookie)
# 尝试获取反爬 Cookie使用配置中的代理
self._acw_cookie = await _get_acw_cookie(
self.base_url, proxy=self._proxy, tunnel_node_id=self._tunnel_node_id
)
self._set_connected()
return True
async def disconnect(self) -> None:
"""断开连接"""
self._session_cookie = None
self._acw_cookie = None
self._user_id = None
self._set_disconnected()
async def is_authenticated(self) -> bool:
"""检查是否已认证"""
return self._session_cookie is not None
def _apply_auth(self, request: httpx.Request) -> httpx.Request:
"""为请求应用认证信息"""
cookies = []
# 添加反爬 Cookie
if self._acw_cookie:
cookies.append(self._acw_cookie)
# 添加 session Cookie
if self._session_cookie:
cookies.append(f"session={self._session_cookie}")
if cookies:
request.headers["Cookie"] = "; ".join(cookies)
# 添加 New-Api-User header
if self._user_id:
request.headers["New-Api-User"] = self._user_id
return request
@classmethod
def get_credentials_schema(cls) -> dict[str, Any]:
"""获取凭据配置 schema"""
return {
"type": "object",
"properties": {
"base_url": {
"type": "string",
"title": "站点地址",
"description": "API 基础地址",
"x-default-value": "https://anyrouter.top",
},
"session_cookie": {
"type": "string",
"title": "Session Cookie",
"description": "从浏览器复制的 session Cookie 值",
"x-sensitive": True,
"x-input-type": "password",
},
},
"required": ["session_cookie"],
"x-field-groups": [
{"fields": ["base_url"]},
{"fields": ["session_cookie"]},
],
"x-auth-type": "cookie",
"x-default-base-url": "https://anyrouter.top",
"x-validation": [
{
"type": "required",
"fields": ["session_cookie"],
"message": "请填写 Session Cookie",
},
],
"x-quota-divisor": 500000,
"x-currency": "USD",
}
class AnyrouterArchitecture(ProviderArchitecture):
"""
Anyrouter 架构预设
针对 Anyrouter 中转站优化的预设配置。
特点:
- 使用 Cookie 认证session
- 自动处理 acw_sc__v2 反爬 Cookie
- 验证端点: /api/user/self
- quota 单位是 1/500000 美元
"""
architecture_id = "anyrouter"
display_name = "Anyrouter"
description = "Anyrouter 中转站预设配置,使用 Cookie 认证"
supported_connectors: list[type[ProviderConnector]] = [
AnyrouterConnector,
]
supported_actions: list[type[ProviderAction]] = [AnyrouterBalanceAction]
default_action_configs: dict[ProviderActionType, dict[str, Any]] = {
ProviderActionType.QUERY_BALANCE: {
"endpoint": "/api/user/self",
"method": "GET",
"quota_divisor": 500000, # 与 New API 相同
"checkin_endpoint": "/api/user/sign_in", # 自动签到端点
},
}
def get_credentials_schema(self) -> dict[str, Any]:
"""Anyrouter 使用 session_cookie 认证"""
return AnyrouterConnector.get_credentials_schema()
def get_verify_endpoint(self) -> str:
"""验证端点"""
return "/api/user/self"
async def prepare_verify_config(
self,
base_url: str,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, Any]:
"""
验证前获取 acw_sc__v2 Cookie
Args:
base_url: API 基础地址
config: 连接器配置
credentials: 凭据信息
Returns:
包含 acw_cookie 的配置
"""
# 从 config 获取代理配置(支持 proxy_node_id、tunnel 和旧的 proxy URL
from src.services.proxy_node.resolver import resolve_ops_proxy_config_async
proxy, tunnel_node_id = await resolve_ops_proxy_config_async(config)
acw_cookie = await _get_acw_cookie(base_url, proxy=proxy, tunnel_node_id=tunnel_node_id)
if acw_cookie:
return {"acw_cookie": acw_cookie}
return {}
def build_verify_headers(
self,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, str]:
"""
构建 Anyrouter 的验证请求 Headers
使用 Cookie 认证,不使用 Authorization。
同时添加 New-Api-User header。
"""
headers: dict[str, str] = {}
cookies = []
# 添加反爬 Cookie
acw_cookie = config.get("acw_cookie")
if acw_cookie:
cookies.append(acw_cookie)
# 添加 session Cookie
cookie_input = credentials.get("session_cookie")
if cookie_input:
# 提取 session 值(支持完整 Cookie 字符串或仅 session 值)
session_value = extract_cookie_value(cookie_input, "session")
cookies.append(f"session={session_value}")
# 从 session 解析 user_id 并添加 New-Api-User header
user_id, _ = _parse_session_user_id(cookie_input)
if user_id:
headers["New-Api-User"] = user_id
if cookies:
headers["Cookie"] = "; ".join(cookies)
return headers
def _auth_fail_message(self, status_code: int) -> str:
"""Cookie 认证的错误消息"""
if status_code == 401:
return "Cookie 已失效,请重新配置"
return "Cookie 已失效或无权限"

View File

@@ -0,0 +1,550 @@
"""
Provider 架构抽象基类
"""
from abc import ABC, abstractmethod
from collections.abc import AsyncIterator, Callable
from contextlib import asynccontextmanager
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
import httpx
from src.services.provider_ops.actions.base import ProviderAction
from src.services.provider_ops.types import (
ConnectorAuthType,
ConnectorState,
ConnectorStatus,
ProviderActionType,
)
from src.utils.ssl_utils import get_ssl_context
# ==================== 连接器基类 ====================
class ProviderConnector(ABC):
"""
提供商连接器基类
负责建立与提供商的认证连接,管理凭据状态。
每个架构应在自己的文件中实现对应的连接器子类。
"""
# 子类需要定义的类属性
auth_type: ConnectorAuthType = ConnectorAuthType.NONE
display_name: str = "Base Connector"
def __init__(self, base_url: str, config: dict[str, Any] | None = None):
"""
初始化连接器
Args:
base_url: 提供商 API 基础 URL
config: 连接器配置
"""
self.base_url = base_url.rstrip("/")
self.config = config or {}
self._status = ConnectorStatus.DISCONNECTED
self._connected_at: datetime | None = None
self._expires_at: datetime | None = None
self._last_error: str | None = None
# 代理配置(支持 proxy_node_id、tunnel 和旧的 proxy URL
from src.services.proxy_node.resolver import resolve_ops_proxy_config
self._proxy: str | httpx.Proxy | None
self._tunnel_node_id: str | None
self._proxy, self._tunnel_node_id = resolve_ops_proxy_config(self.config)
# HTTP 客户端配置
self._timeout = self.config.get("timeout", 30)
self._headers: dict[str, str] = {}
# 凭据更新回调Token Rotation 等场景需要持久化新凭据)
self._on_credentials_updated: Callable[[dict[str, Any]], None] | None = None
@abstractmethod
async def connect(self, credentials: dict[str, Any]) -> bool:
"""
建立认证连接
Args:
credentials: 凭据信息如用户名密码、API Key 等)
Returns:
是否连接成功
"""
pass
@abstractmethod
async def disconnect(self) -> None:
"""断开连接,清理状态"""
pass
@abstractmethod
async def is_authenticated(self) -> bool:
"""检查当前是否已认证"""
pass
@abstractmethod
def _apply_auth(self, request: httpx.Request) -> httpx.Request:
"""
为请求应用认证信息
Args:
request: 原始请求
Returns:
添加认证信息后的请求
"""
pass
async def refresh_auth(self, credentials: dict[str, Any]) -> bool:
"""
刷新认证(如 Token 过期)
默认实现:重新连接
Args:
credentials: 凭据信息
Returns:
是否刷新成功
"""
return await self.connect(credentials)
@asynccontextmanager
async def get_client(self) -> AsyncIterator[httpx.AsyncClient]:
"""
获取已认证的 HTTP 客户端
使用 context manager 确保资源正确释放。
tunnel 模式下使用 TunnelTransport 替代 proxy transport。
Yields:
已配置认证信息的 AsyncClient
"""
transport = None
if self._tunnel_node_id:
from src.services.proxy_node.tunnel_transport import create_tunnel_transport
transport = create_tunnel_transport(self._tunnel_node_id, timeout=self._timeout)
elif self._proxy:
transport = httpx.AsyncHTTPTransport(proxy=self._proxy)
async with httpx.AsyncClient(
base_url=self.base_url,
timeout=self._timeout,
transport=transport,
event_hooks={"request": [self._auth_hook]},
verify=get_ssl_context(),
) as client:
yield client
async def _auth_hook(self, request: httpx.Request) -> None:
"""请求钩子:应用认证信息"""
self._apply_auth(request)
def get_state(self) -> ConnectorState:
"""获取连接器当前状态"""
return ConnectorState(
status=self._status,
auth_type=self.auth_type,
connected_at=self._connected_at,
expires_at=self._expires_at,
last_error=self._last_error,
)
def _set_connected(self, expires_at: datetime | None = None) -> None:
"""设置为已连接状态"""
self._status = ConnectorStatus.CONNECTED
self._connected_at = datetime.now(timezone.utc)
self._expires_at = expires_at
self._last_error = None
def _set_error(self, error: str) -> None:
"""设置错误状态"""
self._status = ConnectorStatus.ERROR
self._last_error = error
def _set_disconnected(self) -> None:
"""设置为断开状态"""
self._status = ConnectorStatus.DISCONNECTED
self._connected_at = None
self._expires_at = None
@classmethod
def get_credentials_schema(cls) -> dict[str, Any]:
"""
获取凭据配置 JSON Schema用于前端表单生成
子类应重写此方法
"""
return {"type": "object", "properties": {}, "required": []}
# ==================== 验证结果 ====================
@dataclass
class VerifyResult:
"""认证验证结果"""
success: bool
message: str | None = None
username: str | None = None
display_name: str | None = None
email: str | None = None
quota: float | None = None
used_quota: float | None = None
request_count: int | None = None
extra: dict[str, Any] | None = None
def to_dict(self) -> dict[str, Any]:
"""转换为字典"""
if not self.success:
return {"success": False, "message": self.message}
return {
"success": True,
"data": {
"username": self.username,
"display_name": self.display_name or self.username,
"email": self.email,
"quota": self.quota,
"used_quota": self.used_quota,
"request_count": self.request_count,
"extra": self.extra or {},
},
}
# ==================== 架构基类 ====================
class ProviderArchitecture(ABC):
"""
提供商架构抽象基类
架构 = Connector鉴权方式 + Actions支持的操作
一个架构可以被多个 Provider 复用。
例如generic_api 架构可用于各种中转站。
## 添加新认证模板的步骤
1. 在 architectures/ 目录创建新文件
2. 继承 ProviderArchitecture 和 ProviderConnector
3. 定义类属性architecture_id, display_name, description
4. 实现连接器子类和架构类
5. 实现认证相关的抽象方法:
- get_credentials_schema(): 返回凭据字段定义
- get_verify_endpoint(): 返回验证端点
- build_verify_headers(): 构建验证请求 headers
- parse_verify_response(): 解析验证响应
6. 在 registry.py 的 _register_builtin_architectures() 中注册
"""
# 子类需要定义的类属性
architecture_id: str = ""
display_name: str = ""
description: str = ""
# 设为 True 时不在架构列表 API 中返回(内部使用的架构)
hidden: bool = False
# 支持的 Connector 类型列表(按优先级排序)
supported_connectors: list[type[ProviderConnector]] = []
# 支持的 Action 类型列表
supported_actions: list[type[ProviderAction]] = []
# 默认操作配置
default_action_configs: dict[ProviderActionType, dict[str, Any]] = {}
def __init__(self, config: dict[str, Any] | None = None):
"""
初始化架构
Args:
config: 架构配置
"""
self.config = config or {}
# ==================== 认证验证相关方法 ====================
@abstractmethod
def get_credentials_schema(self) -> dict[str, Any]:
"""
获取凭据字段定义JSON Schema 格式)
子类必须实现此方法定义需要的凭据字段。
Returns:
JSON Schema 格式的字段定义
"""
pass
@abstractmethod
def get_verify_endpoint(self) -> str:
"""
获取认证验证端点
子类必须实现此方法返回验证端点。
Returns:
验证端点路径(如 /api/user/self, /api/v1/auth/profile
"""
pass
@abstractmethod
def build_verify_headers(
self,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, str]:
"""
构建认证验证请求的 Headers
子类必须实现此方法构建认证 Headers。
Args:
config: 连接器配置(可能包含 prepare_verify_config 返回的额外配置)
credentials: 凭据信息
Returns:
Headers 字典
"""
pass
def parse_verify_response(
self,
status_code: int,
data: dict[str, Any],
) -> VerifyResult:
"""
解析认证验证响应
默认实现处理通用的 {"success": bool, "data": {...}} 格式。
子类可重写 _auth_fail_message() 和 _build_verify_result() 进行自定义。
Args:
status_code: HTTP 状态码
data: 响应 JSON 数据
Returns:
验证结果
"""
if status_code == 401:
return VerifyResult(success=False, message=self._auth_fail_message(401))
if status_code == 403:
return VerifyResult(success=False, message=self._auth_fail_message(403))
if status_code != 200:
return VerifyResult(success=False, message=f"验证失败HTTP {status_code}")
# 解析通用响应格式
if data.get("success") is True and "data" in data:
user_data = data["data"]
elif data.get("success") is False:
message = data.get("message", "验证失败")
return VerifyResult(success=False, message=message)
else:
user_data = data
return self._build_verify_result(user_data, data)
def _auth_fail_message(self, status_code: int) -> str:
"""
获取认证失败消息
子类可重写以提供自定义消息(如 Cookie 认证场景)。
"""
if status_code == 401:
return "认证失败:无效的凭据"
return "认证失败:权限不足"
def _build_verify_result(
self, user_data: dict[str, Any], raw_data: dict[str, Any] | None = None
) -> VerifyResult:
"""
从用户数据构建验证结果
默认实现提取 username, display_name, email, quota, used_quota, request_count。
子类可重写以自定义字段提取。
"""
known_fields = (
"username",
"display_name",
"email",
"quota",
"used_quota",
"request_count",
)
return VerifyResult(
success=True,
username=user_data.get("username"),
display_name=user_data.get("display_name") or user_data.get("username"),
email=user_data.get("email"),
quota=user_data.get("quota"),
used_quota=user_data.get("used_quota"),
request_count=user_data.get("request_count"),
extra={k: v for k, v in user_data.items() if k not in known_fields},
)
# ==================== 可选的钩子方法 ====================
async def prepare_verify_config(
self,
base_url: str,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, Any] | tuple[dict[str, Any], dict[str, Any]]:
"""
验证前的异步预处理(可选)
子类可重写以执行异步操作(如获取动态 Cookie、登录获取 Token
Args:
base_url: API 基础地址
config: 连接器配置
credentials: 凭据信息
Returns:
- dict: 额外配置(会与原 config 合并传递给 build_verify_headers
- tuple[dict, dict]: (额外配置, 需持久化的凭据更新)
当预处理过程中凭据发生变更时(如 Token Rotation
通过第二个 dict 显式返回需要持久化的字段。
"""
return {}
# ==================== 连接器和操作相关方法 ====================
def get_connector(
self,
base_url: str,
auth_type: ConnectorAuthType | None = None,
config: dict[str, Any] | None = None,
) -> ProviderConnector:
"""
获取连接器实例
Args:
base_url: 提供商 API 基础 URL
auth_type: 指定的认证类型None 则使用默认
config: 连接器配置
Returns:
连接器实例
Raises:
ValueError: 不支持的认证类型
"""
if not self.supported_connectors:
raise ValueError(f"架构 {self.architecture_id} 未配置支持的连接器")
# 查找匹配的连接器
connector_cls: type[ProviderConnector] | None = None
if auth_type:
for cls in self.supported_connectors:
if cls.auth_type == auth_type:
connector_cls = cls
break
if not connector_cls:
supported = [c.auth_type.value for c in self.supported_connectors]
raise ValueError(
f"架构 {self.architecture_id} 不支持 {auth_type.value} 认证,"
f"支持的类型: {supported}"
)
else:
# 使用第一个(默认)连接器
connector_cls = self.supported_connectors[0]
return connector_cls(base_url, config)
def get_action(
self,
action_type: ProviderActionType,
config: dict[str, Any] | None = None,
) -> ProviderAction:
"""
获取操作实例
Args:
action_type: 操作类型
config: 操作配置(会与默认配置合并)
Returns:
操作实例
Raises:
ValueError: 不支持的操作类型
"""
action_cls: type[ProviderAction] | None = None
for cls in self.supported_actions:
if cls.action_type == action_type:
action_cls = cls
break
if not action_cls:
supported = [a.action_type.value for a in self.supported_actions]
raise ValueError(
f"架构 {self.architecture_id} 不支持 {action_type.value} 操作,"
f"支持的操作: {supported}"
)
# 合并默认配置和用户配置
merged_config = dict(self.default_action_configs.get(action_type, {}))
if config:
merged_config.update(config)
return action_cls(merged_config)
def supports_action(self, action_type: ProviderActionType) -> bool:
"""检查是否支持指定操作"""
return any(a.action_type == action_type for a in self.supported_actions)
def supports_auth_type(self, auth_type: ConnectorAuthType) -> bool:
"""检查是否支持指定认证类型"""
return any(c.auth_type == auth_type for c in self.supported_connectors)
def get_supported_auth_types(self) -> list[ConnectorAuthType]:
"""获取支持的认证类型列表"""
return [c.auth_type for c in self.supported_connectors]
def get_supported_action_types(self) -> list[ProviderActionType]:
"""获取支持的操作类型列表"""
return [a.action_type for a in self.supported_actions]
def to_dict(self) -> dict[str, Any]:
"""转换为字典(用于 API 响应)"""
return {
"architecture_id": self.architecture_id,
"display_name": self.display_name,
"description": self.description,
"credentials_schema": self.get_credentials_schema(),
"verify_endpoint": self.get_verify_endpoint(),
"supported_auth_types": [
{
"type": c.auth_type.value,
"display_name": c.display_name,
"credentials_schema": c.get_credentials_schema(),
}
for c in self.supported_connectors
],
"supported_actions": [
{
"type": a.action_type.value,
"display_name": a.display_name,
"description": a.description,
"config_schema": a.get_config_schema(),
}
for a in self.supported_actions
],
"default_connector": (
self.supported_connectors[0].auth_type.value if self.supported_connectors else None
),
}

View File

@@ -0,0 +1,228 @@
"""
Cubence 架构
针对 Cubence 中转站的预设配置。
"""
from typing import Any
import httpx
from src.services.provider_ops.actions import ProviderAction
from src.services.provider_ops.actions.cubence_balance import CubenceBalanceAction
from src.services.provider_ops.architectures.base import (
ProviderArchitecture,
ProviderConnector,
VerifyResult,
)
from src.services.provider_ops.types import ConnectorAuthType, ProviderActionType
from src.services.provider_ops.utils import extract_cookie_value
class CubenceConnector(ProviderConnector):
"""
Cubence 专用连接器
特点:
- 使用 Cookie 认证token JWT
"""
auth_type = ConnectorAuthType.COOKIE
display_name = "Cubence Cookie"
def __init__(self, base_url: str, config: dict[str, Any] | None = None):
super().__init__(base_url, config)
self._token_cookie: str | None = None
async def connect(self, credentials: dict[str, Any]) -> bool:
"""建立连接"""
token_cookie = credentials.get("token_cookie")
if not token_cookie:
self._set_error("Token Cookie 不能为空")
return False
# 提取纯 token 值
self._token_cookie = extract_cookie_value(token_cookie, "token")
self._set_connected()
return True
async def disconnect(self) -> None:
"""断开连接"""
self._token_cookie = None
self._set_disconnected()
async def is_authenticated(self) -> bool:
"""检查是否已认证"""
return self._token_cookie is not None
def _apply_auth(self, request: httpx.Request) -> httpx.Request:
"""为请求应用认证信息"""
if self._token_cookie:
request.headers["Cookie"] = f"token={self._token_cookie}"
return request
@classmethod
def get_credentials_schema(cls) -> dict[str, Any]:
"""获取凭据配置 schema"""
return {
"type": "object",
"properties": {
"base_url": {
"type": "string",
"title": "站点地址",
"description": "API 基础地址",
"x-default-value": "https://cubence.com",
},
"token_cookie": {
"type": "string",
"title": "Token Cookie",
"description": "从浏览器复制的 token Cookie 值JWT 格式)",
"x-sensitive": True,
"x-input-type": "password",
},
},
"required": ["token_cookie"],
"x-field-groups": [
{"fields": ["base_url"]},
{"fields": ["token_cookie"]},
],
"x-auth-type": "cookie",
"x-default-base-url": "https://cubence.com",
"x-validation": [
{
"type": "required",
"fields": ["token_cookie"],
"message": "请填写 Token Cookie",
},
],
"x-quota-divisor": None,
"x-currency": "USD",
"x-balance-extra-format": [
{
"label": "5h",
"type": "window_limit",
"source": "five_hour_limit",
"unit_divisor": 1000000,
},
{
"label": "",
"type": "window_limit",
"source": "weekly_limit",
"unit_divisor": 1000000,
},
],
}
class CubenceArchitecture(ProviderArchitecture):
"""
Cubence 架构预设
针对 Cubence 中转站优化的预设配置。
特点:
- 使用 Cookie 认证token JWT
- 验证端点: /api/v1/dashboard/overview
- 余额单位直接是美元
- 支持窗口限额5小时/每周)
"""
architecture_id = "cubence"
display_name = "Cubence"
description = "Cubence 中转站预设配置,使用 Cookie 认证"
supported_connectors: list[type[ProviderConnector]] = [
CubenceConnector,
]
supported_actions: list[type[ProviderAction]] = [
CubenceBalanceAction,
]
default_action_configs: dict[ProviderActionType, dict[str, Any]] = {
ProviderActionType.QUERY_BALANCE: {
"endpoint": "/api/v1/dashboard/overview",
"method": "GET",
"response_mapping": {
"total_available": "data.balance.total_balance_dollar",
},
},
}
def get_credentials_schema(self) -> dict[str, Any]:
"""Cubence 使用 token_cookie 认证"""
return CubenceConnector.get_credentials_schema()
def get_verify_endpoint(self) -> str:
"""验证端点"""
return "/api/v1/dashboard/overview"
def build_verify_headers(
self,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, str]:
"""
构建 Cubence 的验证请求 Headers
使用 Cookie 认证,不使用 Authorization。
"""
headers: dict[str, str] = {}
# 添加 token Cookie
cookie_input = credentials.get("token_cookie")
if cookie_input:
token_value = extract_cookie_value(cookie_input, "token")
headers["Cookie"] = f"token={token_value}"
return headers
def _auth_fail_message(self, status_code: int) -> str:
"""Cookie 认证的错误消息"""
if status_code == 401:
return "Cookie 已失效,请重新配置"
return "Cookie 已失效或无权限"
def _build_verify_result(
self, user_data: dict[str, Any], raw_data: dict[str, Any] | None = None
) -> VerifyResult:
"""Cubence 自定义字段提取user/balance/subscription_limits"""
user_info = user_data.get("user", {})
balance_info = user_data.get("balance", {})
subscription_limits = user_data.get("subscription_limits", {})
# 构建 extra 信息,包含窗口限额
extra: dict[str, Any] = {
"role": user_info.get("role"),
"invite_code": user_info.get("invite_code"),
}
# 5小时窗口限额
five_hour = subscription_limits.get("five_hour", {})
if five_hour:
extra["five_hour_limit"] = {
"limit": five_hour.get("limit"),
"used": five_hour.get("used"),
"remaining": five_hour.get("remaining"),
"resets_at": five_hour.get("resets_at"),
}
# 每周窗口限额
weekly = subscription_limits.get("weekly", {})
if weekly:
extra["weekly_limit"] = {
"limit": weekly.get("limit"),
"used": weekly.get("used"),
"remaining": weekly.get("remaining"),
"resets_at": weekly.get("resets_at"),
}
return VerifyResult(
success=True,
username=user_info.get("username"),
display_name=user_info.get("username"),
quota=balance_info.get("total_balance_dollar"),
extra=extra,
)

View File

@@ -0,0 +1,204 @@
"""
通用 API 架构
支持各种中转站的可配置架构。
## 添加新认证模板示例
如需添加新的中转站模板(如 MyApi参考以下步骤
1. 在 architectures/ 目录创建新文件,如 my_api.py
from src.services.provider_ops.architectures.base import ProviderArchitecture
from src.services.provider_ops.connectors.base import ProviderConnector
class MyApiConnector(ProviderConnector):
# 实现自己的连接器
pass
class MyApiArchitecture(ProviderArchitecture):
architecture_id = "my_api"
display_name = "My API"
description = "My API 风格中转站"
supported_connectors = [MyApiConnector]
supported_actions = [BalanceAction]
# 如果需要特殊的认证 headers重写此方法
def build_verify_headers(self, config, credentials):
headers = super().build_verify_headers(config, credentials)
if "custom_field" in credentials:
headers["X-Custom-Header"] = credentials["custom_field"]
return headers
2. 在 registry.py 的 _register_builtin_architectures() 中注册:
from .my_api import MyApiArchitecture
builtin = [..., MyApiArchitecture]
3. 在前端 auth-templates/ 添加对应的模板定义
"""
from typing import Any
import httpx
from src.services.provider_ops.actions import (
NewApiBalanceAction,
ProviderAction,
)
from src.services.provider_ops.architectures.base import (
ProviderArchitecture,
ProviderConnector,
)
from src.services.provider_ops.types import ConnectorAuthType, ProviderActionType
class GenericApiKeyConnector(ProviderConnector):
"""
通用 API Key 连接器
支持多种 API Key 传递方式:
- Bearer Token (Authorization: Bearer xxx)
- Custom Header (X-API-Key: xxx)
"""
auth_type = ConnectorAuthType.API_KEY
display_name = "API Key"
def __init__(self, base_url: str, config: dict[str, Any] | None = None):
super().__init__(base_url, config)
self._api_key: str | None = None
# 支持配置认证方式
self._auth_method = self.config.get("auth_method", "bearer")
self._header_name = self.config.get("header_name", "Authorization")
async def connect(self, credentials: dict[str, Any]) -> bool:
"""建立连接"""
api_key = credentials.get("api_key")
if not api_key:
self._set_error("API Key 不能为空")
return False
self._api_key = api_key
self._set_connected()
return True
async def disconnect(self) -> None:
"""断开连接"""
self._api_key = None
self._set_disconnected()
async def is_authenticated(self) -> bool:
"""检查是否已认证"""
return self._api_key is not None
def _apply_auth(self, request: httpx.Request) -> httpx.Request:
"""为请求应用认证信息"""
if not self._api_key:
return request
if self._auth_method == "bearer":
request.headers["Authorization"] = f"Bearer {self._api_key}"
elif self._auth_method == "header":
request.headers[self._header_name] = self._api_key
return request
@classmethod
def get_credentials_schema(cls) -> dict[str, Any]:
"""获取凭据配置 schema"""
return {
"type": "object",
"properties": {
"base_url": {
"type": "string",
"title": "站点地址",
"description": "API 基础地址",
},
"api_key": {
"type": "string",
"title": "API Key",
"description": "提供商的 API Key",
"x-sensitive": True,
"x-input-type": "password",
},
},
"required": ["api_key"],
"x-field-groups": [
{"fields": ["base_url"]},
{"fields": ["api_key"]},
],
"x-auth-type": "api_key",
"x-auth-method": "bearer",
"x-validation": [
{
"type": "required",
"fields": ["api_key"],
"message": "请填写 API Key",
},
],
"x-quota-divisor": 500000,
"x-currency": "USD",
}
class GenericApiArchitecture(ProviderArchitecture):
"""
通用 API 架构
适用于各种中转站,支持所有认证方式和操作类型。
用户可以完全自定义 endpoint 和响应映射。
这是"自定义"模板对应的后端架构。
"""
architecture_id = "generic_api"
display_name = "通用 API"
description = "可配置的通用 API 架构,适用于各种中转站"
hidden = True
supported_connectors: list[type[ProviderConnector]] = [
GenericApiKeyConnector,
]
supported_actions: list[type[ProviderAction]] = [NewApiBalanceAction]
# 默认操作配置(可被用户配置覆盖)
default_action_configs: dict[ProviderActionType, dict[str, Any]] = {
ProviderActionType.QUERY_BALANCE: {
"endpoint": "/api/user/balance",
"method": "GET",
},
ProviderActionType.CHECKIN: {
"endpoint": "/api/user/checkin",
"method": "POST",
},
}
def get_credentials_schema(self) -> dict[str, Any]:
"""通用架构只需要 api_key"""
return GenericApiKeyConnector.get_credentials_schema()
def get_verify_endpoint(self) -> str:
"""通用架构验证端点"""
return "/api/user/self"
def build_verify_headers(
self,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, str]:
"""构建通用 API 的验证请求 Headers"""
headers: dict[str, str] = {}
api_key = credentials.get("api_key", "")
if api_key:
auth_method = config.get("auth_method", "bearer")
if auth_method == "bearer":
headers["Authorization"] = f"Bearer {api_key}"
elif auth_method == "header":
header_name = config.get("header_name", "X-API-Key")
headers[header_name] = api_key
return headers

View File

@@ -0,0 +1,289 @@
"""
NekoCode 架构
针对 NekoCode 中转站的预设配置,使用 Cookie 认证。
"""
from typing import Any
import httpx
from src.core.logger import logger
from src.services.provider_ops.actions import ProviderAction
from src.services.provider_ops.actions.nekocode_balance import NekoCodeBalanceAction
from src.services.provider_ops.architectures.base import (
ProviderArchitecture,
ProviderConnector,
VerifyResult,
)
from src.services.provider_ops.types import ConnectorAuthType, ProviderActionType
from src.services.provider_ops.utils import extract_cookie_value
from src.utils.ssl_utils import get_ssl_context
class NekoCodeConnector(ProviderConnector):
"""
NekoCode 专用连接器
特点:
- 使用 Cookie 认证session
"""
auth_type = ConnectorAuthType.COOKIE
display_name = "NekoCode Cookie"
def __init__(self, base_url: str, config: dict[str, Any] | None = None):
super().__init__(base_url, config)
self._session_cookie: str | None = None
async def connect(self, credentials: dict[str, Any]) -> bool:
"""建立连接"""
session_cookie = credentials.get("session_cookie")
if not session_cookie:
self._set_error("Session Cookie 不能为空")
return False
# 提取纯 session 值(支持完整 Cookie 字符串或仅 session 值)
self._session_cookie = extract_cookie_value(session_cookie, "session")
self._set_connected()
return True
async def disconnect(self) -> None:
"""断开连接"""
self._session_cookie = None
self._set_disconnected()
async def is_authenticated(self) -> bool:
"""检查是否已认证"""
return self._session_cookie is not None
def _apply_auth(self, request: httpx.Request) -> httpx.Request:
"""为请求应用认证信息"""
if self._session_cookie:
request.headers["Cookie"] = f"session={self._session_cookie}"
return request
@classmethod
def get_credentials_schema(cls) -> dict[str, Any]:
"""获取凭据配置 schema"""
return {
"type": "object",
"properties": {
"base_url": {
"type": "string",
"title": "站点地址",
"description": "API 基础地址",
"x-default-value": "https://nekocode.ai",
},
"session_cookie": {
"type": "string",
"title": "Session Cookie",
"description": "从浏览器复制的 session Cookie 值",
"x-sensitive": True,
"x-input-type": "password",
},
},
"required": ["session_cookie"],
"x-field-groups": [
{"fields": ["base_url"]},
{"fields": ["session_cookie"]},
],
"x-auth-type": "cookie",
"x-default-base-url": "https://nekocode.ai",
"x-validation": [
{
"type": "required",
"fields": ["session_cookie"],
"message": "请填写 Session Cookie",
},
],
"x-quota-divisor": None,
"x-currency": "USD",
"x-balance-extra-format": [
{
"label": "",
"type": "daily_quota",
"source_limit": "daily_quota_limit",
"source_remaining": "daily_remaining_quota",
"source_start_date": "effective_start_date",
},
{
"label": "",
"type": "monthly_expiry",
"source_end_date": "effective_end_date",
},
],
}
class NekoCodeArchitecture(ProviderArchitecture):
"""
NekoCode 架构预设
针对 NekoCode 中转站优化的预设配置。
特点:
- 使用 Cookie 认证session
- 验证端点: /api/usage/summary
- 显示余额、每日配额、订阅状态
"""
architecture_id = "nekocode"
display_name = "NekoCode"
description = "NekoCode 中转站预设配置,使用 Cookie 认证"
supported_connectors: list[type[ProviderConnector]] = [
NekoCodeConnector,
]
supported_actions: list[type[ProviderAction]] = [NekoCodeBalanceAction]
default_action_configs: dict[ProviderActionType, dict[str, Any]] = {
ProviderActionType.QUERY_BALANCE: {
"endpoint": "/api/usage/summary",
"method": "GET",
},
}
def get_credentials_schema(self) -> dict[str, Any]:
"""NekoCode 使用 session_cookie 认证"""
return NekoCodeConnector.get_credentials_schema()
def get_verify_endpoint(self) -> str:
"""验证端点"""
return "/api/user/self"
async def prepare_verify_config(
self,
base_url: str,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, Any]:
"""
验证前获取 /api/usage/summary 数据(用于显示天卡信息)
Args:
base_url: API 基础地址
config: 连接器配置
credentials: 凭据信息
Returns:
包含 _usage_summary 的配置(会被合并到验证响应中)
"""
try:
# 构建请求头
headers: dict[str, str] = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
}
# 添加 Cookie
cookie_input = credentials.get("session_cookie")
if cookie_input:
session_value = extract_cookie_value(cookie_input, "session")
headers["Cookie"] = f"session={session_value}"
# 构建 client 参数
client_kwargs: dict[str, Any] = {
"timeout": 10,
"verify": get_ssl_context(),
}
from src.services.proxy_node.resolver import resolve_ops_proxy_config_async
proxy, tunnel_node_id = await resolve_ops_proxy_config_async(config)
if tunnel_node_id:
from src.services.proxy_node.tunnel_transport import create_tunnel_transport
client_kwargs["transport"] = create_tunnel_transport(tunnel_node_id, timeout=10.0)
elif proxy:
client_kwargs["proxy"] = proxy
async with httpx.AsyncClient(**client_kwargs) as client:
resp = await client.get(
f"{base_url.rstrip('/')}/api/usage/summary",
headers=headers,
)
if resp.status_code == 200:
data = resp.json()
if data.get("success"):
return {"_usage_summary": data.get("data", {})}
except Exception as e:
logger.debug("获取 NekoCode usage summary 失败: {}", e)
return {}
def build_verify_headers(
self,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, str]:
"""
构建 NekoCode 的验证请求 Headers
使用 Cookie 认证,不使用 Authorization。
"""
headers: dict[str, str] = {}
# 添加 session Cookie
cookie_input = credentials.get("session_cookie")
if cookie_input:
# 提取 session 值(支持完整 Cookie 字符串或仅 session 值)
session_value = extract_cookie_value(cookie_input, "session")
headers["Cookie"] = f"session={session_value}"
return headers
def _auth_fail_message(self, status_code: int) -> str:
"""Cookie 认证的错误消息"""
if status_code == 401:
return "Cookie 已失效,请重新配置"
return "Cookie 已失效或无权限"
def _build_verify_result(
self, user_data: dict[str, Any], raw_data: dict[str, Any] | None = None
) -> VerifyResult:
"""NekoCode 自定义字段提取(合并 _usage_summary 天卡数据)"""
# 转换余额字符串为数字
balance = user_data.get("balance")
try:
quota = float(balance) if balance else None
except (TypeError, ValueError):
quota = None
# 从 prepare_verify_config 获取的 _usage_summary 数据(天卡信息)
extra: dict[str, Any] = {}
usage_summary = (raw_data or {}).get("_usage_summary", {})
subscription = usage_summary.get("subscription", {})
if subscription:
daily_limit = subscription.get("daily_quota_limit")
daily_remaining = subscription.get("daily_remaining_quota")
try:
extra["daily_quota_limit"] = float(daily_limit) if daily_limit else None
except (TypeError, ValueError):
pass
try:
extra["daily_remaining_quota"] = float(daily_remaining) if daily_remaining else None
except (TypeError, ValueError):
pass
extra["plan_name"] = subscription.get("plan_name")
extra["subscription_status"] = subscription.get("status")
extra["effective_start_date"] = subscription.get("effective_start_date")
extra["effective_end_date"] = subscription.get("effective_end_date")
return VerifyResult(
success=True,
username=user_data.get("username"),
display_name=user_data.get("display_name") or user_data.get("username"),
email=user_data.get("email"),
quota=quota,
extra=extra if extra else None,
)

View File

@@ -0,0 +1,232 @@
"""
New API 架构
针对 New API 风格的中转站优化的预设配置。
"""
from typing import Any
import httpx
from src.core.api_format.headers import BROWSER_FINGERPRINT_HEADERS
from src.services.provider_ops.actions import (
NewApiBalanceAction,
ProviderAction,
)
from src.services.provider_ops.architectures.base import (
ProviderArchitecture,
ProviderConnector,
)
from src.services.provider_ops.types import ConnectorAuthType, ProviderActionType
class NewApiConnector(ProviderConnector):
"""
New API 专用连接器
特点:
- 使用 Bearer Token 认证
- 需要 New-Api-User Header 传递用户 ID
"""
auth_type = ConnectorAuthType.API_KEY
display_name = "New API Key"
def __init__(self, base_url: str, config: dict[str, Any] | None = None):
super().__init__(base_url, config)
self._api_key: str | None = None
self._user_id: str | None = None
self._cookie: str | None = None
async def connect(self, credentials: dict[str, Any]) -> bool:
"""建立连接"""
api_key = credentials.get("api_key")
cookie = credentials.get("cookie")
user_id = credentials.get("user_id")
# api_key 和 cookie 至少需要一个
if not api_key and not cookie:
self._set_error("访问令牌和 Cookie 至少需要填写一个")
return False
# 使用 api_key 时必须提供 user_id使用 cookie 时 user_id 可选
if api_key and not cookie and not user_id:
self._set_error("使用访问令牌时,用户 ID 不能为空")
return False
self._api_key = api_key
self._user_id = str(user_id) if user_id else None
self._cookie = cookie
self._set_connected()
return True
async def disconnect(self) -> None:
"""断开连接"""
self._api_key = None
self._user_id = None
self._cookie = None
self._set_disconnected()
async def is_authenticated(self) -> bool:
"""检查是否已认证"""
# 有 cookie 就行,或者有 api_key + user_id
if self._cookie:
return True
return self._api_key is not None and self._user_id is not None
def _apply_auth(self, request: httpx.Request) -> httpx.Request:
"""为请求应用认证信息"""
# 添加浏览器指纹 Headers 以绕过 Cloudflare 等防护
for key, value in BROWSER_FINGERPRINT_HEADERS.items():
request.headers.setdefault(key, value)
if self._api_key:
request.headers["Authorization"] = f"Bearer {self._api_key}"
if self._user_id:
request.headers["New-Api-User"] = self._user_id
if self._cookie:
request.headers["Cookie"] = self._cookie
return request
@classmethod
def get_credentials_schema(cls) -> dict[str, Any]:
"""获取凭据配置 schema"""
return {
"type": "object",
"properties": {
"base_url": {
"type": "string",
"title": "站点地址",
"description": "API 基础地址",
},
"api_key": {
"type": "string",
"title": "访问令牌 (API Key)",
"description": "New API 的访问令牌,与 Cookie 二选一",
"x-sensitive": True,
"x-input-type": "password",
},
"user_id": {
"type": "string",
"title": "用户 ID",
"description": "使用访问令牌时必填,使用 Cookie 时可选",
},
"cookie": {
"type": "string",
"title": "Cookie",
"description": "用于 Cookie 认证,与访问令牌二选一",
"x-sensitive": True,
"x-input-type": "password",
},
},
"required": [],
"x-field-groups": [
{"fields": ["base_url"]},
{
"fields": ["cookie"],
"x-help": "从浏览器开发者工具复制完整 Cookie",
},
{
"layout": "inline",
"fields": ["api_key", "user_id"],
"x-flex": {"api_key": 3, "user_id": 1},
},
],
"x-auth-type": "api_key",
"x-auth-method": "bearer",
"x-validation": [
{
"type": "any_required",
"fields": ["api_key", "cookie"],
"message": "访问令牌和 Cookie 至少需要填写一个",
},
{
"type": "conditional_required",
"if": "api_key",
"unless": "cookie",
"then": ["user_id"],
"message": "使用访问令牌时,用户 ID 不能为空",
},
],
"x-quota-divisor": 500000,
"x-currency": "USD",
"x-field-hooks": {
"cookie": {
"action": "parse_new_api_user_id",
"target": "user_id",
},
},
}
class NewApiArchitecture(ProviderArchitecture):
"""
New API 架构预设
针对 New API 风格的中转站优化的预设配置。
特点:
- 使用 Bearer Token 认证
- 需要 New-Api-User Header 传递用户 ID
- 验证端点: /api/user/self
- quota 单位通常是 1/500000 美元
"""
architecture_id = "new_api"
display_name = "New API"
description = "New API 风格中转站的预设配置"
supported_connectors: list[type[ProviderConnector]] = [
NewApiConnector,
]
supported_actions: list[type[ProviderAction]] = [
NewApiBalanceAction,
]
default_action_configs: dict[ProviderActionType, dict[str, Any]] = {
ProviderActionType.QUERY_BALANCE: {
"endpoint": "/api/user/self",
"method": "GET",
"quota_divisor": 500000, # New API 的 quota 单位是 1/500000 美元
"checkin_endpoint": "/api/user/checkin", # 签到端点
},
}
def get_credentials_schema(self) -> dict[str, Any]:
"""New API 需要 api_key 和 user_id"""
return NewApiConnector.get_credentials_schema()
def get_verify_endpoint(self) -> str:
"""New API 验证端点"""
return "/api/user/self"
def build_verify_headers(
self,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, str]:
"""
构建 New API 的验证请求 Headers
New API 特有:需要 New-Api-User Header 传递用户 ID
"""
# 以浏览器指纹 Headers 为基础,绕过 Cloudflare 等防护
headers: dict[str, str] = {**BROWSER_FINGERPRINT_HEADERS}
# Bearer Token 认证
api_key = credentials.get("api_key", "")
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
# New API 特有的 header
user_id = credentials.get("user_id", "")
if user_id:
headers["New-Api-User"] = str(user_id)
# 可选的 Cookie
cookie = credentials.get("cookie", "")
if cookie:
headers["Cookie"] = cookie
return headers

View File

@@ -0,0 +1,565 @@
"""
Sub2API 架构
针对 Sub2API 风格的中转站优化的预设配置。
支持两种认证方式:
1. 账号密码登录(自动获取 JWT过期自动刷新refresh 失败自动重新登录)
2. Refresh Token从浏览器 localStorage 获取,自动续期,适合 OAuth 用户)
"""
import time
from collections.abc import AsyncIterator, Callable
from contextlib import asynccontextmanager
from typing import Any
import httpx
from src.core.logger import logger
from src.services.provider_ops.actions import ProviderAction
from src.services.provider_ops.actions.sub2api_balance import Sub2ApiBalanceAction
from src.services.provider_ops.architectures.base import (
ProviderArchitecture,
ProviderConnector,
VerifyResult,
)
from src.services.provider_ops.types import ConnectorAuthType, ProviderActionType
from src.utils.ssl_utils import get_ssl_context
def _calc_expires_at(token_data: dict[str, Any]) -> float:
"""从 token 响应数据计算过期时间(秒级时间戳,提前 60s"""
token_expires_at = token_data.get("token_expires_at")
if token_expires_at is not None:
# Sub2API 返回毫秒级绝对时间戳
return token_expires_at / 1000 - 60
expires_in = token_data.get("expires_in", 900)
return time.time() + expires_in - 60
async def _do_login(
client: httpx.AsyncClient,
email: str,
password: str,
) -> dict[str, Any]:
"""调用 Sub2API 登录接口,返回 token_data。失败抛 ValueError。"""
resp = await client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
data = resp.json()
if resp.status_code != 200 or data.get("code", -1) != 0:
raise ValueError(data.get("message", f"登录失败 (HTTP {resp.status_code})"))
return data.get("data", {})
async def _do_refresh(
client: httpx.AsyncClient,
refresh_token: str,
) -> dict[str, Any]:
"""调用 Sub2API refresh 接口,返回 token_data。失败抛 ValueError。"""
resp = await client.post(
"/api/v1/auth/refresh",
json={"refresh_token": refresh_token},
)
data = resp.json()
if resp.status_code != 200 or data.get("code", -1) != 0:
raise ValueError(data.get("message", "Refresh Token 无效或已过期"))
return data.get("data", {})
def _collect_updated_credentials(
token_data: dict[str, Any],
old_refresh_token: str | None = None,
) -> dict[str, Any]:
"""从 token 响应中提取需要持久化的凭据变更"""
updated: dict[str, Any] = {}
new_refresh_token = token_data.get("refresh_token")
if new_refresh_token and new_refresh_token != old_refresh_token:
updated["refresh_token"] = new_refresh_token
access_token = token_data.get("access_token")
if access_token:
updated["_cached_access_token"] = access_token
updated["_cached_token_expires_at"] = _calc_expires_at(token_data)
return updated
class _Sub2ApiTokenMixin:
"""Sub2API JWT token 管理公共逻辑
与 ProviderConnector 配合使用MRO 中由 ProviderConnector 提供实际属性初始化)。
以下类型注解声明 mixin 依赖的协议属性,不会创建新的实例属性。
"""
# Mixin 自身管理的 token 状态(提供默认值防止子类遗漏初始化)
_access_token: str | None = None
_refresh_token: str | None = None
_token_expires_at: float = 0
# 以下属性由 ProviderConnector.__init__ 初始化,仅作协议声明
_on_credentials_updated: Callable[[dict[str, Any]], None] | None
base_url: str
_timeout: int | float
_proxy: str | httpx.Proxy | None
_tunnel_node_id: str | None
@asynccontextmanager
async def _get_raw_client(self) -> AsyncIterator[httpx.AsyncClient]:
"""获取不带 auth hook 的裸 HTTP 客户端(用于登录/刷新 token"""
transport = None
if self._tunnel_node_id:
from src.services.proxy_node.tunnel_transport import create_tunnel_transport
transport = create_tunnel_transport(self._tunnel_node_id, timeout=self._timeout)
elif self._proxy:
transport = httpx.AsyncHTTPTransport(proxy=self._proxy)
async with httpx.AsyncClient(
base_url=self.base_url,
timeout=self._timeout,
transport=transport,
verify=get_ssl_context(),
) as client:
yield client
def _update_tokens(self, token_data: dict[str, Any]) -> None:
"""更新实例 token 状态并通过回调持久化变更"""
old_refresh_token = self._refresh_token
self._access_token = token_data.get("access_token")
self._refresh_token = token_data.get("refresh_token", self._refresh_token)
self._token_expires_at = _calc_expires_at(token_data)
if self._on_credentials_updated:
updated = _collect_updated_credentials(token_data, old_refresh_token)
if updated:
self._on_credentials_updated(updated)
async def _refresh(self) -> bool:
"""使用 refresh_token 续期"""
if not self._refresh_token:
return False
try:
async with self._get_raw_client() as client:
token_data = await _do_refresh(client, self._refresh_token)
self._update_tokens(token_data)
logger.debug("Sub2API token 续期成功")
return True
except ValueError as e:
logger.warning("Sub2API refresh_token 续期失败: {}", e)
return False
except Exception as e:
logger.warning("Sub2API refresh_token 续期异常: {}", e)
return False
class Sub2ApiConnector(_Sub2ApiTokenMixin, ProviderConnector):
"""
Sub2API 连接器(账号密码模式)
使用 email + password 登录获取 JWT Token支持自动刷新
- 登录后获取 access_token + refresh_token
- access_token 过期前自动使用 refresh_token 续期
- refresh_token 也过期时自动重新登录
"""
auth_type = ConnectorAuthType.SESSION_LOGIN
display_name = "账号密码"
def __init__(self, base_url: str, config: dict[str, Any] | None = None):
super().__init__(base_url, config)
self._access_token: str | None = None
self._refresh_token: str | None = None
self._token_expires_at: float = 0
self._email: str | None = None
self._password: str | None = None
async def connect(self, credentials: dict[str, Any]) -> bool:
email = credentials.get("email", "").strip()
password = credentials.get("password", "").strip()
if not email or not password:
self._set_error("邮箱和密码不能为空")
return False
self._email = email
self._password = password
# 如果有缓存的 access_token 且未过期,直接复用,避免不必要的登录
cached_access_token = credentials.get("_cached_access_token", "")
cached_expires_at = credentials.get("_cached_token_expires_at", 0)
if cached_access_token and time.time() < cached_expires_at:
self._access_token = cached_access_token
self._token_expires_at = cached_expires_at
# 恢复 refresh_token 以便 access_token 过期后可刷新而非重新登录
self._refresh_token = credentials.get("refresh_token", "").strip() or None
self._set_connected()
return True
return await self._login()
async def _login(self) -> bool:
"""使用 email + password 登录获取 token pair"""
try:
async with self._get_raw_client() as client:
token_data = await _do_login(client, self._email or "", self._password or "")
self._update_tokens(token_data)
self._set_connected()
logger.debug(
"Sub2API 登录成功: {}", self._email[:3] + "***" if self._email else "N/A"
)
return True
except ValueError as e:
self._set_error(str(e))
return False
except httpx.TimeoutException:
self._set_error("登录请求超时")
return False
except httpx.RequestError as e:
self._set_error(f"登录网络错误: {e}")
return False
except Exception as e:
self._set_error(f"登录失败: {e}")
return False
async def _ensure_token(self) -> None:
"""确保 access_token 有效,过期则自动刷新或重新登录"""
if self._access_token and time.time() < self._token_expires_at:
return
if self._refresh_token and await self._refresh():
return
logger.info("Sub2API token 已过期,尝试重新登录")
if not await self._login():
logger.error("Sub2API 重新登录失败: {}", self._last_error)
async def disconnect(self) -> None:
self._access_token = None
self._refresh_token = None
self._token_expires_at = 0
self._email = None
self._password = None
self._set_disconnected()
async def is_authenticated(self) -> bool:
if not self._access_token:
return False
return self._refresh_token is not None or self._email is not None
def _apply_auth(self, request: httpx.Request) -> httpx.Request:
if self._access_token:
request.headers["Authorization"] = f"Bearer {self._access_token}"
return request
async def _auth_hook(self, request: httpx.Request) -> None:
await self._ensure_token()
self._apply_auth(request)
async def refresh_auth(self, credentials: dict[str, Any]) -> bool:
if await self._refresh():
return True
self._email = credentials.get("email", self._email)
self._password = credentials.get("password", self._password)
return await self._login()
@classmethod
def get_credentials_schema(cls) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"base_url": {
"type": "string",
"title": "站点地址",
"description": "API 基础地址",
},
"email": {
"type": "string",
"title": "邮箱",
"description": "Sub2API 登录邮箱",
},
"password": {
"type": "string",
"title": "密码",
"description": "Sub2API 登录密码",
"x-sensitive": True,
"x-input-type": "password",
},
},
"required": ["email", "password"],
"x-field-groups": [
{"fields": ["base_url"]},
{"fields": ["email"]},
{"fields": ["password"]},
],
"x-auth-type": "session_login",
"x-auth-method": "jwt",
"x-validation": [
{
"type": "required",
"fields": ["email", "password"],
"message": "请填写邮箱和密码",
},
],
}
class Sub2ApiRefreshTokenConnector(_Sub2ApiTokenMixin, ProviderConnector):
"""
Sub2API 连接器Refresh Token 模式)
适合 OAuth 登录用户(如 LinuxDo从浏览器 localStorage 获取 refresh_token。
- 首次连接时用 refresh_token 换取 access_token
- access_token 过期前自动续期
- refresh_token 过期后需手动更新(无法自动重新登录)
"""
auth_type = ConnectorAuthType.API_KEY
display_name = "Refresh Token"
def __init__(self, base_url: str, config: dict[str, Any] | None = None):
super().__init__(base_url, config)
self._access_token: str | None = None
self._refresh_token: str | None = None
self._token_expires_at: float = 0
async def connect(self, credentials: dict[str, Any]) -> bool:
refresh_token = credentials.get("refresh_token", "").strip()
if not refresh_token:
self._set_error("请填写 Refresh Token")
return False
self._refresh_token = refresh_token
# 如果有缓存的 access_token 且未过期,直接复用,不消耗 refresh_token
cached_access_token = credentials.get("_cached_access_token", "")
cached_expires_at = credentials.get("_cached_token_expires_at", 0)
if cached_access_token and time.time() < cached_expires_at:
self._access_token = cached_access_token
self._token_expires_at = cached_expires_at
self._set_connected()
return True
# 首次连接或 access_token 已过期,用 refresh_token 换取
if not await self._refresh():
self._refresh_token = None # 清理,避免残留无效状态
self._set_error("Refresh Token 无效或已过期")
return False
self._set_connected()
return True
async def _ensure_token(self) -> None:
"""确保 access_token 有效,有 refresh_token 时自动续期"""
if self._access_token and time.time() < self._token_expires_at:
return
if self._refresh_token:
await self._refresh()
async def disconnect(self) -> None:
self._access_token = None
self._refresh_token = None
self._token_expires_at = 0
self._set_disconnected()
async def is_authenticated(self) -> bool:
return self._refresh_token is not None
def _apply_auth(self, request: httpx.Request) -> httpx.Request:
if self._access_token:
request.headers["Authorization"] = f"Bearer {self._access_token}"
return request
async def _auth_hook(self, request: httpx.Request) -> None:
await self._ensure_token()
self._apply_auth(request)
async def refresh_auth(self, credentials: dict[str, Any]) -> bool:
if self._refresh_token and await self._refresh():
return True
return await self.connect(credentials)
@classmethod
def get_credentials_schema(cls) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"base_url": {
"type": "string",
"title": "站点地址",
"description": "API 基础地址",
},
"refresh_token": {
"type": "string",
"title": "Refresh Token",
"description": ("从浏览器 F12 > Application > Local Storage 获取"),
"x-sensitive": True,
"x-input-type": "password",
"x-help": "浏览器控制台执行 localStorage.getItem('refresh_token') 获取",
},
},
"required": ["refresh_token"],
"x-field-groups": [
{"fields": ["base_url"]},
{"fields": ["refresh_token"]},
],
"x-auth-type": "api_key",
"x-auth-method": "bearer",
"x-validation": [
{
"type": "required",
"fields": ["refresh_token"],
"message": "请填写 Refresh Token",
},
],
}
class Sub2ApiArchitecture(ProviderArchitecture):
"""
Sub2API 架构
特点:
- 支持两种认证方式:账号密码 / Refresh Token
- 验证端点: /api/v1/auth/me
- balance 为充值余额points 为赠送余额
- 余额查询同时获取订阅概览信息
"""
architecture_id = "sub2api"
display_name = "Sub2API"
description = "Sub2API 风格中转站的预设配置"
supported_connectors: list[type[ProviderConnector]] = [
Sub2ApiConnector,
Sub2ApiRefreshTokenConnector,
]
supported_actions: list[type[ProviderAction]] = [Sub2ApiBalanceAction]
default_action_configs: dict[ProviderActionType, dict[str, Any]] = {
ProviderActionType.QUERY_BALANCE: {
"endpoint": "/api/v1/auth/me?timezone=Asia/Shanghai",
"subscription_endpoint": "/api/v1/subscriptions/summary",
"method": "GET",
},
}
def get_credentials_schema(self) -> dict[str, Any]:
return Sub2ApiConnector.get_credentials_schema()
def get_verify_endpoint(self) -> str:
return "/api/v1/auth/me?timezone=Asia/Shanghai"
def build_verify_headers(
self,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, str]:
headers: dict[str, str] = {}
access_token = credentials.get("_access_token", "")
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
return headers
async def prepare_verify_config(
self,
base_url: str,
config: dict[str, Any],
credentials: dict[str, Any],
) -> tuple[dict[str, Any], dict[str, Any]]:
"""
验证前预处理:根据凭据类型选择登录方式
- 有 email + password -> 账号密码登录
- 有 refresh_token -> 用 refresh_token 换 access_token
Returns:
(extra_config, updated_credentials):
extra_config 为空updated_credentials 包含需持久化的凭据变更
Token Rotation 后的新 refresh_token、缓存的 access_token 等)
"""
base_url = base_url.rstrip("/")
from src.services.proxy_node.resolver import resolve_ops_proxy_config_async
proxy, tunnel_node_id = await resolve_ops_proxy_config_async(config)
client_kwargs: dict[str, Any] = {
"base_url": base_url,
"timeout": 30.0,
"verify": get_ssl_context(),
}
if tunnel_node_id:
from src.services.proxy_node.tunnel_transport import create_tunnel_transport
client_kwargs["transport"] = create_tunnel_transport(tunnel_node_id, timeout=30.0)
elif proxy:
client_kwargs["proxy"] = proxy
email = credentials.get("email", "").strip()
password = credentials.get("password", "").strip()
refresh_token = credentials.get("refresh_token", "").strip()
try:
if email and password:
async with httpx.AsyncClient(**client_kwargs) as client:
token_data = await _do_login(client, email, password)
elif refresh_token:
async with httpx.AsyncClient(**client_kwargs) as client:
token_data = await _do_refresh(client, refresh_token)
else:
raise ValueError("请填写账号密码或 Refresh Token")
except ValueError:
raise
except Exception as e:
raise ValueError(f"验证失败: {e}") from e
access_token = token_data.get("access_token", "")
credentials["_access_token"] = access_token
updated_credentials = _collect_updated_credentials(
token_data, old_refresh_token=refresh_token or None
)
return {}, updated_credentials
def parse_verify_response(
self,
status_code: int,
data: dict[str, Any],
) -> VerifyResult:
if status_code == 401:
return VerifyResult(success=False, message=self._auth_fail_message(401))
if status_code == 403:
return VerifyResult(success=False, message=self._auth_fail_message(403))
if status_code != 200:
return VerifyResult(success=False, message=f"验证失败HTTP {status_code}")
code = data.get("code")
if code != 0:
message = data.get("message", "验证失败")
return VerifyResult(success=False, message=message)
user_data = data.get("data", {})
return self._build_verify_result(user_data, data)
def _build_verify_result(
self, user_data: dict[str, Any], raw_data: dict[str, Any] | None = None
) -> VerifyResult:
balance = float(user_data.get("balance") or 0)
points = float(user_data.get("points") or 0)
return VerifyResult(
success=True,
username=user_data.get("username") or user_data.get("email"),
display_name=user_data.get("username") or user_data.get("email"),
email=user_data.get("email"),
quota=balance + points,
extra={
"balance": balance,
"points": points,
"status": user_data.get("status"),
"concurrency": user_data.get("concurrency"),
},
)

View File

@@ -0,0 +1,344 @@
"""
YesCode 架构
针对 YesCode 中转站的预设配置。
"""
from typing import Any
import httpx
from src.services.provider_ops.actions import ProviderAction
from src.services.provider_ops.actions.yescode_balance import (
YesCodeBalanceAction,
fetch_yescode_combined_data,
parse_yescode_balance_extra,
)
from src.services.provider_ops.architectures.base import (
ProviderArchitecture,
ProviderConnector,
VerifyResult,
)
from src.services.provider_ops.types import ConnectorAuthType, ProviderActionType
from src.utils.ssl_utils import get_ssl_context
def _extract_cookies(cookie_string: str) -> dict[str, str]:
"""
从完整的 Cookie 字符串中提取 yescode_auth 和 yescode_csrf
Args:
cookie_string: Cookie 字符串
Returns:
包含 yescode_auth 和 yescode_csrf 的字典
"""
result: dict[str, str] = {}
for part in cookie_string.split(";"):
part = part.strip()
if "=" in part:
key, value = part.split("=", 1)
key = key.strip()
if key in ("yescode_auth", "yescode_csrf"):
result[key] = value.strip()
return result
def _build_cookie_header(cookie_string: str) -> str:
"""
从输入的 Cookie 字符串构建请求用的 Cookie header
支持两种输入格式:
1. 完整 Cookie: "yescode_auth=xxx; yescode_csrf=yyy"
2. 仅 auth 值: "eyJhbGciOiJI..."
Args:
cookie_string: Cookie 字符串或 auth 值
Returns:
Cookie header 值
"""
# 如果包含 "yescode_auth=",说明是完整 Cookie 字符串
if "yescode_auth=" in cookie_string:
cookies = _extract_cookies(cookie_string)
parts = []
if "yescode_auth" in cookies:
parts.append(f"yescode_auth={cookies['yescode_auth']}")
if "yescode_csrf" in cookies:
parts.append(f"yescode_csrf={cookies['yescode_csrf']}")
return "; ".join(parts)
# 否则认为直接是 auth 值
return f"yescode_auth={cookie_string.strip()}"
class YesCodeConnector(ProviderConnector):
"""
YesCode 专用连接器
特点:
- 使用 Cookie 认证yescode_auth JWT + yescode_csrf
"""
auth_type = ConnectorAuthType.COOKIE
display_name = "YesCode Cookie"
def __init__(self, base_url: str, config: dict[str, Any] | None = None):
super().__init__(base_url, config)
self._auth_cookie: str | None = None
async def connect(self, credentials: dict[str, Any]) -> bool:
"""建立连接"""
auth_cookie = credentials.get("auth_cookie")
if not auth_cookie:
self._set_error("Auth Cookie 不能为空")
return False
# 构建 Cookie header
self._auth_cookie = _build_cookie_header(auth_cookie)
self._set_connected()
return True
async def disconnect(self) -> None:
"""断开连接"""
self._auth_cookie = None
self._set_disconnected()
async def is_authenticated(self) -> bool:
"""检查是否已认证"""
return self._auth_cookie is not None
def _apply_auth(self, request: httpx.Request) -> httpx.Request:
"""为请求应用认证信息"""
if self._auth_cookie:
request.headers["Cookie"] = self._auth_cookie
return request
@classmethod
def get_credentials_schema(cls) -> dict[str, Any]:
"""获取凭据配置 schema"""
return {
"type": "object",
"properties": {
"base_url": {
"type": "string",
"title": "站点地址",
"description": "API 基础地址",
"x-default-value": "https://co.yes.vg",
},
"auth_cookie": {
"type": "string",
"title": "Auth Cookie",
"description": "从浏览器复制的 Cookie包含 yescode_auth 和 yescode_csrf",
"x-sensitive": True,
"x-input-type": "password",
},
},
"required": ["auth_cookie"],
"x-field-groups": [
{"fields": ["base_url"]},
{"fields": ["auth_cookie"]},
],
"x-auth-type": "cookie",
"x-default-base-url": "https://co.yes.vg",
"x-validation": [
{
"type": "required",
"fields": ["auth_cookie"],
"message": "请填写 Auth Cookie",
},
],
"x-quota-divisor": None,
"x-currency": "USD",
"x-balance-extra-format": [
{
"label": "",
"type": "weekly_spent",
"source_limit": "daily_limit",
"source_spent": "daily_spent",
"source_resets_at": "daily_resets_at",
},
{
"label": "",
"type": "weekly_spent",
"source_limit": "weekly_limit",
"source_spent": "weekly_spent",
"source_resets_at": "weekly_resets_at",
},
],
}
class YesCodeArchitecture(ProviderArchitecture):
"""
YesCode 架构预设
针对 YesCode 中转站优化的预设配置。
特点:
- 使用 Cookie 认证yescode_auth JWT + yescode_csrf
- 验证端点: /api/v1/user/balance
- 余额单位直接是美元
- 支持每周限额查询
"""
architecture_id = "yescode"
display_name = "YesCode"
description = "YesCode 中转站预设配置,使用 Cookie 认证"
supported_connectors: list[type[ProviderConnector]] = [
YesCodeConnector,
]
supported_actions: list[type[ProviderAction]] = [
YesCodeBalanceAction,
]
default_action_configs: dict[ProviderActionType, dict[str, Any]] = {
ProviderActionType.QUERY_BALANCE: {
"endpoint": "/api/v1/user/balance",
"method": "GET",
"response_mapping": {
"total_available": "total_balance",
},
},
}
def get_credentials_schema(self) -> dict[str, Any]:
"""YesCode 使用 auth_cookie 认证"""
return YesCodeConnector.get_credentials_schema()
def get_verify_endpoint(self) -> str:
"""验证端点 - 使用 profile 接口获取完整信息"""
return "/api/v1/auth/profile"
async def prepare_verify_config(
self,
base_url: str,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, Any]:
"""
预获取合并数据balance + profile
验证时并发调用两个接口获取完整数据。
"""
extra_config: dict[str, Any] = {}
cookie_input = credentials.get("auth_cookie")
if not cookie_input:
return extra_config
cookie_header = _build_cookie_header(cookie_input)
# 获取代理配置(支持 proxy_node_id、tunnel 和旧的 proxy URL
from src.services.proxy_node.resolver import resolve_ops_proxy_config_async
proxy, tunnel_node_id = await resolve_ops_proxy_config_async(config)
try:
# 构建 client 参数
client_kwargs: dict[str, Any] = {
"headers": {"Cookie": cookie_header},
"timeout": 10.0,
"verify": get_ssl_context(),
}
if tunnel_node_id:
from src.services.proxy_node.tunnel_transport import create_tunnel_transport
client_kwargs["transport"] = create_tunnel_transport(tunnel_node_id, timeout=10.0)
elif proxy:
client_kwargs["proxy"] = proxy
# 创建临时 client 获取合并数据
async with httpx.AsyncClient(**client_kwargs) as client:
combined_data = await fetch_yescode_combined_data(client, base_url)
extra_config["_combined_data"] = combined_data
except Exception:
# 如果调用失败,不影响验证流程(会回退到单独调用 profile
pass
return extra_config
def build_verify_headers(
self,
config: dict[str, Any],
credentials: dict[str, Any],
) -> dict[str, str]:
"""
构建 YesCode 的验证请求 Headers
使用 Cookie 认证,不使用 Authorization。
"""
headers: dict[str, str] = {}
# 添加 Cookie
cookie_input = credentials.get("auth_cookie")
if cookie_input:
headers["Cookie"] = _build_cookie_header(cookie_input)
return headers
def parse_verify_response(
self,
status_code: int,
data: dict[str, Any],
) -> VerifyResult:
"""解析 YesCode 验证响应(使用预获取的合并数据)"""
if status_code == 401:
return VerifyResult(success=False, message="Cookie 已失效,请重新配置")
if status_code == 403:
return VerifyResult(success=False, message="Cookie 已失效或无权限")
if status_code != 200:
return VerifyResult(success=False, message=f"验证失败HTTP {status_code}")
# 优先使用预获取的合并数据(包含 balance + profile
combined_data = data.get("_combined_data")
if combined_data:
# 检查是否有有效数据
if "_profile_data" not in combined_data and "_balance_data" not in combined_data:
return VerifyResult(success=False, message="Cookie 已失效,请重新配置")
# 使用公共函数解析余额
extra = parse_yescode_balance_extra(combined_data)
total_available = extra.pop("_total_available", 0)
extra.pop("_subscription_available", None)
return VerifyResult(
success=True,
username=combined_data.get("username"),
display_name=combined_data.get("username"),
email=combined_data.get("email"),
quota=total_available,
extra=extra if extra else None,
)
# 回退:仅使用 profile 数据(旧逻辑,当 prepare_verify_config 失败时)
if "username" not in data:
return VerifyResult(success=False, message="响应格式无效")
# 构造兼容格式供公共函数使用
compat_data = {
"pay_as_you_go_balance": data.get("pay_as_you_go_balance", 0),
"subscription_balance": data.get("subscription_balance", 0),
"weekly_spent_balance": data.get("current_week_spend", 0),
"subscription_plan": data.get("subscription_plan"),
"last_week_reset": data.get("last_week_reset"),
"last_daily_balance_add": data.get("last_daily_balance_add"),
}
extra = parse_yescode_balance_extra(compat_data)
total_available = extra.pop("_total_available", 0)
extra.pop("_subscription_available", None)
return VerifyResult(
success=True,
username=data.get("username"),
display_name=data.get("username"),
email=data.get("email"),
quota=total_available,
extra=extra if extra else None,
)