This commit is contained in:
qsc
2026-09-06 19:22:56 +08:00
parent a42d3146c5
commit 2cf54a5e6c
18 changed files with 2203 additions and 216 deletions
+96 -9
View File
@@ -1,8 +1,11 @@
import json
import html
import re
import hashlib
import asyncio
import time
from datetime import datetime
from typing import Dict, Any, Optional, List, Union
from typing import TYPE_CHECKING, Dict, Any, Optional, List, Union
from inspect import isawaitable
from typing import Any, Awaitable, Callable, Dict, Optional
@@ -14,6 +17,9 @@ from .request import APIClient, APIErrorResponse
from .sqlite import AsyncSQLiteDB
from .fun_basic import load_template,gold_to_parts,week_to_num,compare_date_str,format_time,format_remaining
if TYPE_CHECKING:
from .cache import CacheService
ROLE_RANK_NAMES = {
"名士五十强",
@@ -74,25 +80,33 @@ RANK_NAMES = frozenset().union(
class JX3APIService:
def __init__(self, config: AstrBotConfig, sqlite: AsyncSQLiteDB):
def __init__(
self,
config: AstrBotConfig,
sqlite: AsyncSQLiteDB,
cache: Optional["CacheService"] = None,
):
# 实例化 API Client
self._api: APIClient = APIClient()
# 引用插件配置文件
self._config = config
# 引用sqlite
self._sql_db = sqlite
self._cache = cache
self._token_stats_cache: tuple[float, Dict[str, Any]] | None = None
self._token_stats_lock = asyncio.Lock()
# 获取配置中的 Token
self.token = self._config.get("jx3api_token", "")
if self.token == "":
logger.warning("获取配置token失败,请正确填写token,否则部分功能无法正常使用")
else:
logger.debug(f"获取配置token成功。{self.token}")
logger.debug(f"获取配置token成功。")
# 获取配置中的 ticket
self.ticket = self._config.get("jx3api_ticket", "")
if self.ticket == "":
logger.warning("获取配置ticket失败,请正确填写ticket,否则部分功能无法正常使用")
else:
logger.debug(f"获取配置ticket成功。{self.ticket}")
logger.debug(f"获取配置ticket成功。")
async def close(self):
@@ -100,11 +114,13 @@ class JX3APIService:
if self._api:
await self._api.close()
async def server_list(self) -> list[str]:
async def server_list(self, force_refresh: bool = False) -> list[str]:
"""获取当前有效区服名称,供会话绑定和参数消歧使用。"""
data = await self._base_request(
data, _ = await self._cached_request(
"/server/status/check",
{"server": "", "type": "其他"},
force_refresh=force_refresh,
allow_stale=not force_refresh,
)
if not isinstance(data, list):
return []
@@ -117,18 +133,38 @@ class JX3APIService:
)
async def token_stats(self) -> Optional[Dict[str, Any]]:
"""读取令牌统计;短时内存复用,避免 WebUI 保存配置时重复请求。"""
if not str(self.token or "").strip():
return None
now = time.monotonic()
if self._token_stats_cache and self._token_stats_cache[0] > now:
return dict(self._token_stats_cache[1])
async with self._token_stats_lock:
now = time.monotonic()
if self._token_stats_cache and self._token_stats_cache[0] > now:
return dict(self._token_stats_cache[1])
result = await self._fetch_token_stats()
if result is not None:
self._token_stats_cache = (now + 30, dict(result))
return result
async def _fetch_token_stats(self) -> Optional[Dict[str, Any]]:
"""查询当前配置 JX3API Token 的等级、用量及有效状态。"""
if not str(self.token or "").strip():
return None
try:
data = await self._api.post(
async def requester():
return await self._api.post(
"https://www.jx3api.com/token/stats",
data={"token": self.token},
out_key="data",
success_codes=(200, "200"),
return_error=True,
)
try:
data = await requester()
except Exception as exc:
logger.warning(f"查询 JX3API Token 统计失败: {exc}")
return None
@@ -204,6 +240,56 @@ class JX3APIService:
logger.error(f"基础请求调用出错 ({api_path}): {e}")
return None
@staticmethod
def _is_cacheable_response(data: Any) -> bool:
return data is not None and not isinstance(data, APIErrorResponse)
async def _cached_request(
self,
api_path: str,
params: Optional[Dict[str, Any]] = None,
out: Optional[str] = "data",
force_refresh: bool = False,
allow_stale: bool = True,
) -> tuple[Any, dict[str, Any]]:
request_params = params or {}
if not self._cache:
return await self._base_request(api_path, request_params, out), {
"endpoint": api_path,
"hit": False,
"stale": False,
"ttl_seconds": 0,
}
cache_params = dict(request_params)
credential_values = [
str(value)
for key, value in request_params.items()
if str(key).lower() in {"token", "ticket"} and value
]
if credential_values:
cache_params["__credential_scope"] = hashlib.sha256(
"|".join(credential_values).encode("utf-8")
).hexdigest()
try:
return await self._cache.request_api(
api_path,
cache_params,
lambda: self._base_request(api_path, request_params, out),
self._is_cacheable_response,
force_refresh=force_refresh,
allow_stale=allow_stale,
)
except Exception as exc:
logger.warning(f"接口缓存不可用,直接请求 JX3API endpoint={api_path}: {exc}")
return await self._base_request(api_path, request_params, out), {
"endpoint": api_path,
"hit": False,
"stale": False,
"ttl_seconds": 0,
}
async def _request_api(
self,
@@ -217,7 +303,8 @@ class JX3APIService:
"""通用接口请求与模板处理。"""
return_data = self._init_return_data()
data = await self._base_request(path, params)
data, cache_metadata = await self._cached_request(path, params)
return_data["_cache"] = cache_metadata
if isinstance(data, APIErrorResponse):
return_data["msg"] = data.message or "获取接口信息失败"
return return_data