refactor: 平台改为代码内置 provider 架构,一次性迁移现有数据 - 新增 app/providers:BalanceProvider 基类 + DeepSeek 适配器 + 注册表 - platforms 表去除 method/url/headers/body/balance_path,新增 provider_id - 现有数据库已一次性迁移(备份 data/monitor.db.bak-v1),不保留迁移工具 - 平台 UI 改为选择内置提供方;fetcher/monitor 走 provider 构建请求与解析 - 表达式引擎(运算符/函数)随架构保留,供 provider 内部使用 - 测试更新至 provider 模式,共 92 个
This commit is contained in:
+21
-47
@@ -2,18 +2,18 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
import requests
|
||||
|
||||
from app.expr import extract_path, to_number
|
||||
from app.providers.base import BalanceProvider
|
||||
|
||||
logger = logging.getLogger("monitor.fetcher")
|
||||
|
||||
PLACEHOLDER = "{{apiKey}}"
|
||||
_TOKEN_RE = re.compile(r"([^.\[]+)|\[(\d+)\]")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -33,57 +33,31 @@ def render_template(text: str, api_key: str) -> str:
|
||||
|
||||
|
||||
def extract_balance(data: object, path: str) -> float:
|
||||
"""按点路径/数组索引提取余额,如 data.balance、data[0].balance、$.data[0].balance。
|
||||
"""按点路径/数组索引提取余额(兼容旧写法,返回 float)。
|
||||
|
||||
取到的值必须是数字或可转数字的字符串,否则抛 ValueError。
|
||||
新写法请用 evaluate_balance(支持运算符与函数)。
|
||||
"""
|
||||
p = path.strip()
|
||||
if p.startswith("$"):
|
||||
p = p[1:]
|
||||
cur: object = data
|
||||
for key, idx in _TOKEN_RE.findall(p):
|
||||
if key:
|
||||
if not isinstance(cur, dict) or key not in cur:
|
||||
raise ValueError(f"路径不存在: {path}(在 {key!r} 处)")
|
||||
cur = cur[key]
|
||||
if idx != "":
|
||||
n = int(idx)
|
||||
if not isinstance(cur, list) or n >= len(cur):
|
||||
raise ValueError(f"数组索引越界: {path}(索引 {n})")
|
||||
cur = cur[n]
|
||||
if isinstance(cur, bool) or not isinstance(cur, (int, float, str)):
|
||||
raise ValueError(f"余额不是数字: {path} -> {cur!r}")
|
||||
try:
|
||||
return float(cur)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(f"余额不是数字: {path} -> {cur!r}")
|
||||
return to_number(extract_path(data, path), path)
|
||||
|
||||
|
||||
def _build_request(platform: dict, api_key: str) -> tuple[str, dict, str | None]:
|
||||
url = render_template(platform["url"], api_key)
|
||||
headers_raw = platform.get("headers") or {}
|
||||
if isinstance(headers_raw, str):
|
||||
headers_raw = json.loads(headers_raw) if headers_raw.strip() else {}
|
||||
headers = json.loads(render_template(json.dumps(headers_raw), api_key))
|
||||
body = None
|
||||
if platform["method"] == "POST" and platform.get("body"):
|
||||
body = render_template(platform["body"], api_key)
|
||||
if not any(k.lower() == "content-type" for k in headers):
|
||||
headers["Content-Type"] = "application/json"
|
||||
return url, headers, body
|
||||
|
||||
|
||||
def fetch_balance(platform: dict, api_key: str, retry_count: int, timeout: int) -> FetchResult:
|
||||
def fetch_balance(
|
||||
provider: BalanceProvider,
|
||||
api_key: str,
|
||||
retry_count: int,
|
||||
timeout: int,
|
||||
) -> FetchResult:
|
||||
"""执行一次余额拉取。
|
||||
|
||||
策略:
|
||||
- 401/403 → auth_error(不重试,由调用方禁用账号并通知)
|
||||
- 其他 4xx → 直接失败(配置问题,不重试)
|
||||
- 401/403 → 先按重试次数确认(可能瞬时),仍失败 → auth_error(调用方禁用账号并通知)
|
||||
- 其他 4xx → 直接失败(不重试)
|
||||
- 网络异常 / 5xx → 重试 retry_count 次,间隔 2s
|
||||
- JSON 解析/路径提取失败 → 直接失败
|
||||
- JSON 解析/余额提取失败 → 直接失败
|
||||
"""
|
||||
url, headers, body = _build_request(platform, api_key)
|
||||
method = platform["method"]
|
||||
url, headers, body = provider.build_request(api_key)
|
||||
method = provider.method
|
||||
if method == "POST" and body and not any(k.lower() == "content-type" for k in headers):
|
||||
headers["Content-Type"] = "application/json"
|
||||
attempts = retry_count + 1
|
||||
for attempt in range(attempts):
|
||||
try:
|
||||
@@ -118,8 +92,8 @@ def fetch_balance(platform: dict, api_key: str, retry_count: int, timeout: int)
|
||||
return FetchResult(ok=False, status_code=resp.status_code,
|
||||
error="响应不是合法 JSON")
|
||||
try:
|
||||
balance = extract_balance(data, platform["balance_path"])
|
||||
except ValueError as exc:
|
||||
balance = provider.extract_balance(data)
|
||||
except Exception as exc:
|
||||
return FetchResult(ok=False, status_code=resp.status_code, error=str(exc))
|
||||
return FetchResult(ok=True, balance=balance, status_code=resp.status_code)
|
||||
return FetchResult(ok=False, error=f"网络错误,重试 {retry_count} 次后仍失败")
|
||||
Reference in New Issue
Block a user