Files
Aether/_deprecated_py_src/services/provider_ops/actions/balance.py
fawney19 1d9c77522a 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)
2026-04-03 16:26:16 +08:00

308 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
余额查询操作抽象基类
"""
import time
from abc import abstractmethod
from typing import Any
import httpx
from src.core.cache_service import CacheService
from src.core.logger import logger as _logger
from src.services.provider_ops.actions.base import ProviderAction
from src.services.provider_ops.types import (
ActionResult,
ActionStatus,
BalanceInfo,
ProviderActionType,
)
# 签到缓存 TTL6 小时)
_CHECKIN_CACHE_TTL = 6 * 3600
class BalanceAction(ProviderAction):
"""
余额查询操作抽象基类
子类必须实现 _parse_balance() 方法来处理特定平台的余额解析逻辑。
子类可选重写 _do_query_balance() 或 _do_checkin() 进行自定义。
如果子类使用 Cookie 认证,设置 _cookie_auth = True 可让 401/403 错误
显示 "Cookie 已失效" 而非 "认证失败"
"""
action_type = ProviderActionType.QUERY_BALANCE
display_name = "查询余额"
description = "查询账户余额信息"
default_cache_ttl = 86400 # 24 小时
# 子类设为 True 即可在 401/403 时显示 "Cookie 已失效" 消息
_cookie_auth: bool = False
async def execute(self, client: httpx.AsyncClient) -> ActionResult:
"""
执行余额查询(模板方法)
1. 先尝试签到(如果子类实现了 _do_checkin带缓存冷却
2. 执行余额查询
Args:
client: 已认证的 HTTP 客户端
Returns:
ActionResult其中 data 字段为 BalanceInfo
"""
# 先尝试签到(带缓存冷却)
checkin_result = await self._get_or_do_checkin(client)
# 执行余额查询
result = await self._do_query_balance(client)
# 将签到结果附加到 extra 字段
if checkin_result and result.data and hasattr(result.data, "extra"):
if result.data.extra is None:
result.data.extra = {}
# 处理 cookie_expired 标记
if checkin_result.get("cookie_expired"):
result.data.extra["cookie_expired"] = True
result.data.extra["cookie_expired_message"] = checkin_result.get("message", "")
result.status = ActionStatus.AUTH_EXPIRED
_logger.warning("Cookie 已失效: {}", checkin_result)
else:
result.data.extra["checkin_success"] = checkin_result.get("success")
result.data.extra["checkin_message"] = checkin_result.get("message", "")
_logger.debug("签到结果已附加到 extra: {}", checkin_result)
return result
async def _do_query_balance(self, client: httpx.AsyncClient) -> ActionResult:
"""
执行余额查询
默认实现处理通用的请求/响应/错误处理流程。
子类只需实现 _parse_balance() 即可。
如果查询逻辑不同(如并发调用多个接口),子类可重写此方法。
Args:
client: 已认证的 HTTP 客户端
Returns:
ActionResult其中 data 字段为 BalanceInfo
"""
endpoint = self.config.get("endpoint", "/api/user/self")
method = self.config.get("method", "GET")
start_time = time.time()
try:
response = await client.request(method, endpoint)
response_time_ms = int((time.time() - start_time) * 1000)
try:
data = response.json()
except Exception:
return self._make_error_result(
ActionStatus.PARSE_ERROR,
"响应不是有效的 JSON",
)
if response.status_code != 200:
return self._handle_http_error(response, data)
if data.get("success") is False:
message = data.get("message", "业务状态码表示失败")
return self._make_error_result(
ActionStatus.UNKNOWN_ERROR,
message,
raw_response=data,
)
balance = self._parse_balance(data)
return self._make_success_result(
data=balance,
response_time_ms=response_time_ms,
raw_response=data,
)
except httpx.TimeoutException:
return self._make_error_result(
ActionStatus.NETWORK_ERROR,
"请求超时",
retry_after_seconds=30,
)
except httpx.RequestError as e:
return self._make_error_result(
ActionStatus.NETWORK_ERROR,
f"网络错误: {str(e)}",
retry_after_seconds=30,
)
except Exception as e:
return self._make_error_result(
ActionStatus.UNKNOWN_ERROR,
f"未知错误: {str(e)}",
)
@abstractmethod
def _parse_balance(self, data: Any) -> BalanceInfo:
"""
解析余额数据(子类必须实现)
Args:
data: API 响应 JSON 数据
Returns:
BalanceInfo 对象
"""
pass
def _handle_http_error(
self, response: httpx.Response, raw_data: dict[str, Any] | None = None
) -> ActionResult:
"""
处理 HTTP 错误响应
Cookie 认证的子类设置 _cookie_auth = True 即可获得友好的错误提示,
无需再逐个重写此方法。
"""
status_code = response.status_code
if status_code == 401:
msg = "Cookie 已失效,请重新配置" if self._cookie_auth else "认证失败"
return self._make_error_result(ActionStatus.AUTH_FAILED, msg, raw_response=raw_data)
elif status_code == 403:
msg = "Cookie 已失效或无权限" if self._cookie_auth else "无权限访问"
return self._make_error_result(ActionStatus.AUTH_FAILED, msg, raw_response=raw_data)
elif status_code == 404:
return self._make_error_result(
ActionStatus.NOT_SUPPORTED, "功能未开放", raw_response=raw_data
)
elif status_code == 429:
retry_after = response.headers.get("Retry-After")
return self._make_error_result(
ActionStatus.RATE_LIMITED,
"请求频率限制",
retry_after_seconds=int(retry_after) if retry_after else 60,
raw_response=raw_data,
)
else:
return self._make_error_result(
ActionStatus.UNKNOWN_ERROR,
f"HTTP {status_code}: {response.reason_phrase}",
raw_response=raw_data,
)
async def _get_or_do_checkin(self, client: httpx.AsyncClient) -> dict[str, Any] | None:
"""
带缓存冷却的签到:优先返回缓存结果,未命中才实际执行签到。
缓存 key 使用 host同一站点多个 provider 只需签到一次TTL 6 小时。
签到失败或 cookie_expired 不写入缓存,允许下次重试。
"""
host = str(client.base_url.host or client.base_url.netloc or client.base_url)
cache_key = f"provider_ops:checkin:{host}"
# 检查缓存
try:
cached = await CacheService.get(cache_key)
if cached is not None:
_logger.debug("[{}] 签到缓存命中,跳过签到: {}", host, cached)
return cached
except Exception:
pass
# 缓存未命中,执行签到
result = await self._do_checkin(client)
# 签到成功或"已签到"时写入缓存;失败/cookie_expired 不缓存
if result is not None and not result.get("cookie_expired"):
success = result.get("success")
if success is True or success is None:
try:
await CacheService.set(cache_key, result, _CHECKIN_CACHE_TTL)
except Exception:
pass
return result
async def _do_checkin(self, client: httpx.AsyncClient) -> dict[str, Any] | None:
"""
执行签到(子类可选实现)
默认实现返回 None不签到
子类可重写此方法实现平台特定的签到逻辑。
Args:
client: 已认证的 HTTP 客户端
Returns:
签到结果字典 {"success": bool, "message": str},或 None 表示不签到
"""
return None
def _create_balance_info(
self,
total_granted: float | None = None,
total_used: float | None = None,
total_available: float | None = None,
currency: str = "USD",
extra: dict[str, Any] | None = None,
) -> BalanceInfo:
"""
创建余额信息对象
辅助方法,用于创建统一格式的 BalanceInfo。
如果只有部分数据,会尝试计算缺失的值。
Args:
total_granted: 总额度
total_used: 已用额度
total_available: 可用余额
currency: 货币单位
extra: 额外信息
Returns:
BalanceInfo 对象
"""
# 如果只有部分数据,尝试计算
if total_available is None and total_granted is not None and total_used is not None:
total_available = total_granted - total_used
if total_used is None and total_granted is not None and total_available is not None:
total_used = total_granted - total_available
if total_granted is None and total_used is not None and total_available is not None:
total_granted = total_used + total_available
return BalanceInfo(
total_granted=total_granted,
total_used=total_used,
total_available=total_available,
currency=currency,
extra=extra if extra is not None else {},
)
def _to_float(self, value: Any) -> float | None:
"""转换为浮点数"""
if value is None:
return None
try:
return float(value)
except (TypeError, ValueError):
return None
@classmethod
def get_config_schema(cls) -> dict[str, Any]:
"""获取操作配置 schema子类可重写"""
return {
"type": "object",
"properties": {
"currency": {
"type": "string",
"title": "货币单位",
"default": "USD",
},
},
"required": [],
}