diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c3f135 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +tests/ diff --git a/README.md b/README.md index e51648a..8f38a83 100644 --- a/README.md +++ b/README.md @@ -189,12 +189,30 @@ astrbot_plugin_jx3/ ├─ _conf_schema.json # AstrBot 后台配置 schema ├─ requirements.txt # Python 依赖 ├─ data/ -│ ├─ api_config.json # 外部接口配置,按功能 key 维护 URL、method、默认参数 +│ ├─ api_config.json # 服务基地址、Adapter、端点相对路径和默认参数 │ └─ plugin_data.db # 插件随包数据,主要用于内置数据 ├─ core/ -│ ├─ jx3_data.py # 业务数据层,调用接口、整理数据、选择模板 +│ ├─ jx3_data.py # 领域 Service 组合与 HTTP 生命周期 +│ ├─ api_config.py # API 服务/端点配置校验与 URL 解析 +│ ├─ adapters/ +│ │ ├─ base.py # 通用端点请求与结构化 Adapter 异常 +│ │ ├─ jx3api.py # JX3API 凭证注入、业务码校验和 data 提取 +│ │ ├─ jx3box.py # JX3BOX 响应与分页规则 +│ │ └─ router.py # 根据服务配置选择 Adapter +│ ├─ services/ +│ │ ├─ base.py # ServiceResult、请求、模板和公共领域能力 +│ │ ├─ cache.py # 通用 JSON 缓存仓储 +│ │ ├─ calendar.py # 日常、活动和事件 +│ │ ├─ character.py # 角色、名片、奇穴和技能 +│ │ ├─ adventure.py # 奇遇查询、统计和攻略 +│ │ ├─ faction.py # 阵营、排行、沙盘和帮战 +│ │ ├─ arena.py # 战绩、名剑和招募 +│ │ ├─ achievement.py # 资历数据与进度计算 +│ │ ├─ trade.py # 金价、物价、交易行和掉落 +│ │ ├─ information.py # 新闻、区服和通用资料 +│ │ └─ content.py # 帮助、八卦、宏和配装 │ ├─ message.py # 消息构建层,负责文本、图片、富媒体回复 -│ ├─ request.py # aiohttp 请求封装,统一处理 GET/POST 和接口返回 +│ ├─ request.py # 通用 HTTP 客户端,不包含任何上游业务规则 │ ├─ async_task.py # APScheduler 后台推送任务 │ ├─ bilei_data.py # 本地避雷数据增删改查 │ ├─ sqlite.py # SQLite 异步封装 @@ -247,23 +265,35 @@ astrbot_plugin_jx3/ ### 4. 数据请求与业务处理 -`core/jx3_data.py` 是主要业务层。每个功能通常对应一个异步方法,例如: +`core/jx3_data.py` 只组合领域 Service 并管理共享 HTTP Client。业务方法按 +功能归入 `core/services/`,例如: ```python -async def jinqiqiyu(self, server: str) -> Dict[str, Any]: - ... +await services.calendar.richang(server, num) +await services.character.jueshe(name, server) +await services.adventure.jinqiqiyu(server) +await services.trade.jiaoyihang(name, server) ``` -业务方法通常遵循统一结构: +所有领域 Service 共享 `BaseDomainService`,新方法推荐直接返回统一结果: -1. 调用 `_init_return_data()` 创建标准返回对象。 -2. 构造接口参数,填入 `server`、`name`、`token`、`ticket` 等。 -3. 通过 `_base_request(config_key, method, params)` 读取 `data/api_config.json` 中的接口配置并发起请求。 -4. 判断接口返回是否为空或结构异常。 -5. 整理时间戳、字段名、分组、列表、统计值等模板需要的数据。 -6. 文本功能直接写入 `return_data["data"]`。 -7. 图片功能额外加载对应 HTML 模板并写入 `return_data["temp"]`。 -8. 成功时设置 `return_data["code"] = 200`。 +```python +if not isinstance(data, dict): + return self.failure("接口响应格式异常") + +return self.success(view_data) + +# 需要模板时 +return await self.template_result("example.html", view_data) +``` + +领域方法通常遵循统一结构: + +1. 构造游戏业务参数,例如 `server`、`name`;Token/Ticket 由 Adapter 注入。 +2. 通过基类请求入口读取端点配置并发起请求。 +3. 明确区分请求失败、合法空结果和响应结构异常。 +4. 整理时间戳、字段名、分组、列表、统计值等模板需要的数据。 +5. 使用 `success()`、`failure()` 或 `template_result()` 返回统一结果。 标准返回结构大致为: @@ -279,22 +309,50 @@ async def jinqiqiyu(self, server: str) -> Dict[str, Any]: ### 5. 接口请求封装 -`core/request.py` 中的 `APIClient` 负责实际 HTTP 请求。 +请求链路按职责拆分为: + +```text +MessageBuilder + ↓ +JX3Service(组合领域服务) + ↓ +Calendar / Character / Adventure / Trade 等 Domain Service + ↓ +BaseDomainService(统一请求、结果、模板) + ↓ +APIAdapterRouter(按服务选择 Adapter) + ├─ JX3ApiAdapter(业务码、Token/Ticket、data) + ├─ JX3BoxAdapter(响应结构、分页) + └─ EndpointAdapter(其他通用上游) + ↓ +APIClient(Session、TLS、连接池、超时、HTTP、重试) +``` + +`core/request.py` 中的 `APIClient` 只负责通用 HTTP 能力。 实现特点: - 复用 `aiohttp.ClientSession`。 -- 支持 GET 和 POST。 +- 默认校验 HTTPS 证书,并限制连接池和响应体大小。 +- 分别设置总超时、连接超时和读取超时。 +- 支持 GET、JSON POST、表单 POST、自定义 Header。 +- 对超时、连接异常、HTTP 429 和部分 5xx 做有限退避重试。 - 自动识别 JSON、图片或二进制响应。 -- 对 JX3API 常见返回结构做基础校验。 -- 支持通过 `out_key` 抽取返回对象中的指定字段,默认通常取 `data`。 +- 日志默认只记录参数字段名;需要记录参数值时也会清理 Token、Ticket 等敏感字段。 +- 使用结构化异常区分网络、HTTP、响应大小和响应格式错误。 -`JX3Service._base_request()` 会在业务层进一步统一: +Adapter 层负责: - 根据 `data/api_config.json` 查找接口。 +- 组合服务基地址、公共路径前缀和端点相对路径。 - 合并接口默认参数和运行时参数。 - 根据配置 method 调用 GET 或 POST。 -- 捕获异常并输出日志。 +- `JX3ApiAdapter` 集中注入 Token/Ticket、校验业务码并提取 `data`。 +- `JX3BoxAdapter` 集中处理 JX3BOX 响应与分页停止规则。 + +`BaseDomainService.request()` 将端点请求交给 Router,并把结构化请求异常 +转换为业务层约定的 `None`。领域服务统一使用 `new_result()`、`success()`、 +`failure()`、`attach_template()` 和 `template_result()` 构造结果。 ### 6. 图片渲染 @@ -345,7 +403,39 @@ html_renderer.render_custom_template() `core/sqlite.py` 封装了异步增删改查;`core/bilei_data.py` 基于该封装实现避雷数据管理。 -### 8. 资历查询 +### 8. API 地址配置 + +`data/api_config.json` 将服务地址和端点路径分开维护: + +```json +{ + "_services": { + "jx3api": { + "base_url": "https://www.jx3api.com", + "path_prefix": "", + "adapter": "jx3api" + } + }, + "jx3_richang": { + "service": "jx3api", + "path": "active/calendar", + "method": "GET" + } +} +``` + +- `base_url`:服务协议和域名。 +- `path_prefix`:该服务所有接口共享的路径前缀。 +- `adapter`:该服务的响应与鉴权适配器,可用值为 `jx3api`、`jx3box`、`generic`。 +- `path`:单个端点的相对路径。 + +服务公共路径变化时只需修改 `path_prefix`,域名变化时只修改 `base_url`。 + +动态端点可以在 `path` 中使用占位符,例如 +`serendipity/{dw_id}/achievement`。每个端点必须显式声明 `service` 和 +相对 `path`,不接受端点级绝对 `url`。 + +### 9. 资历查询 `资历 角色名称 [服务器]` 是 2.6 新增的两轮对话功能。 @@ -362,7 +452,7 @@ html_renderer.render_custom_template() 4. 展开菜单中单个 ID 和数组 ID,按资历点数计算 `已完成点数 / 总点数` 与百分比。 5. 使用 `templates/zili.html` 渲染进度条图片。 -### 9. 交易行查询 +### 10. 交易行查询 `交易行 物品名称 [服务器]` 在 2.7 中完成重构,属于交易功能,免令牌,输出为图片。 @@ -377,7 +467,7 @@ html_renderer.render_custom_template() 价格单位按铜钱换算为砖、金、银、铜,并使用 `templates/img` 下的 `zhuang.png`、`jin.png`、`yin.png`、`tong.png` 图标展示;0 砖不会显示砖图标。 -### 10. 后台推送 +### 11. 后台推送 `core/async_task.py` 使用 APScheduler 实现轮询推送。 @@ -401,7 +491,7 @@ html_renderer.render_custom_template() 新增一个查询功能通常需要改动 4 到 5 个位置: 1. 在 `data/api_config.json` 添加接口配置。 -2. 在 `core/jx3_data.py` 添加业务方法,完成请求、数据整理和错误处理。 +2. 在 `core/services/` 对应领域文件添加业务方法,完成请求和数据整理。 3. 在 `core/message.py` 添加消息包装方法,选择文本、图片或富媒体输出。 4. 在 `main.py` 的 `command_map` 注册中文触发指令。 5. 如果是图片输出,新增 `templates/xxx.html`。 diff --git a/core/adapters/__init__.py b/core/adapters/__init__.py new file mode 100644 index 0000000..4de473a --- /dev/null +++ b/core/adapters/__init__.py @@ -0,0 +1,13 @@ +from .base import APIAdapterError, APIBusinessError, EndpointAdapter +from .jx3api import JX3ApiAdapter +from .jx3box import JX3BoxAdapter +from .router import APIAdapterRouter + +__all__ = [ + "APIAdapterError", + "APIBusinessError", + "EndpointAdapter", + "JX3ApiAdapter", + "JX3BoxAdapter", + "APIAdapterRouter", +] diff --git a/core/adapters/base.py b/core/adapters/base.py new file mode 100644 index 0000000..07fb083 --- /dev/null +++ b/core/adapters/base.py @@ -0,0 +1,85 @@ +from collections.abc import Mapping +from typing import Any + +from ..api_config import APIEndpointRegistry +from ..request import APIClient, APIClientError, APIResponseFormatError + + +class APIAdapterError(APIClientError): + """Adapter 层的结构化异常。""" + + +class APIBusinessError(APIAdapterError): + """HTTP 请求成功,但上游接口返回业务错误。""" + + def __init__( + self, + message: str, + *, + endpoint: str, + code: Any = None, + ): + super().__init__(message) + self.endpoint = endpoint + self.code = code + + +class EndpointAdapter: + """把端点配置转换为 HTTP 请求;子类只处理各上游的协议差异。""" + + def __init__(self, client: APIClient, registry: APIEndpointRegistry): + self._client = client + self._registry = registry + + async def request( + self, + endpoint_name: str, + *, + params: Mapping[str, Any] | None = None, + out_key: str | None = "data", + path_params: Mapping[str, Any] | None = None, + ) -> Any: + endpoint = self._registry.get(endpoint_name) + request_params = self.prepare_params(endpoint, params) + url = self._registry.resolve(endpoint_name, path_params) + configured_method = str(endpoint.get("method", "GET")).upper() + + if configured_method == "POST": + payload = await self._client.post( + url, + data=request_params, + retry=bool(endpoint.get("retry", False)), + ) + else: + payload = await self._client.get( + url, + params=request_params, + retry=bool(endpoint.get("retry", True)), + ) + + payload = self.validate_payload(endpoint_name, payload) + return self.extract_payload(payload, out_key) + + def prepare_params( + self, + endpoint: Mapping[str, Any], + params: Mapping[str, Any] | None, + ) -> dict[str, Any]: + request_params = dict(endpoint.get("params", {})) + if params: + request_params.update(params) + return request_params + + def validate_payload(self, endpoint_name: str, payload: Any) -> Any: + return payload + + def extract_payload(self, payload: Any, out_key: str | None) -> Any: + if out_key in (None, ""): + return payload + if isinstance(payload, bytes): + raise APIResponseFormatError("二进制响应无法提取 JSON 字段") + if not isinstance(payload, Mapping): + raise APIResponseFormatError(f"响应不是对象,无法提取字段: {out_key}") + if out_key not in payload: + raise APIResponseFormatError(f"响应缺少字段: {out_key}") + return payload[out_key] diff --git a/core/adapters/jx3api.py b/core/adapters/jx3api.py new file mode 100644 index 0000000..e030b1f --- /dev/null +++ b/core/adapters/jx3api.py @@ -0,0 +1,73 @@ +from collections.abc import Mapping +from typing import Any + +from ..api_config import APIEndpointRegistry +from ..request import APIClient, APIResponseFormatError +from .base import APIBusinessError, EndpointAdapter + + +class JX3ApiAdapter(EndpointAdapter): + """适配 JX3API 的鉴权参数和业务响应协议。""" + + SUCCESS_CODE = 200 + + def __init__( + self, + client: APIClient, + registry: APIEndpointRegistry, + *, + token: str = "", + ticket: str = "", + ): + super().__init__(client, registry) + self._credentials = { + "token": token.strip(), + "ticket": ticket.strip(), + } + + def prepare_params( + self, + endpoint: Mapping[str, Any], + params: Mapping[str, Any] | None, + ) -> dict[str, Any]: + request_params = super().prepare_params(endpoint, params) + declared_params = endpoint.get("params", {}) + + # api_config.json 中出现 token/ticket 即表示此端点需要对应凭证。 + # 业务 Service 不再重复拼装,也不会把空凭证发送给上游。 + for name, value in self._credentials.items(): + if name not in declared_params: + continue + if value: + request_params[name] = value + else: + request_params.pop(name, None) + + return request_params + + def validate_payload(self, endpoint_name: str, payload: Any) -> Any: + if not isinstance(payload, Mapping): + raise APIResponseFormatError("JX3API 响应必须是对象") + if "code" not in payload: + raise APIResponseFormatError("JX3API 响应缺少业务码: code") + + code = payload.get("code") + if code != self.SUCCESS_CODE: + message = payload.get("msg") or "未知错误" + raise APIBusinessError( + f"JX3API 业务错误: code={code}, message={message}", + endpoint=endpoint_name, + code=code, + ) + return payload + + def extract_payload(self, payload: Any, out_key: str | None) -> Any: + if out_key in (None, ""): + return payload + if not isinstance(payload, Mapping): + raise APIResponseFormatError( + f"JX3API 响应不是对象,无法提取字段: {out_key}" + ) + if out_key not in payload: + raise APIResponseFormatError(f"JX3API 响应缺少字段: {out_key}") + return payload[out_key] diff --git a/core/adapters/jx3box.py b/core/adapters/jx3box.py new file mode 100644 index 0000000..044fd6e --- /dev/null +++ b/core/adapters/jx3box.py @@ -0,0 +1,92 @@ +import hashlib +import json +from collections.abc import Mapping +from typing import Any + +from astrbot.api import logger + +from ..request import APIResponseFormatError +from .base import EndpointAdapter + + +class JX3BoxAdapter(EndpointAdapter): + """适配 JX3BOX 响应结构,并集中实现其分页规则。""" + + def extract_payload(self, payload: Any, out_key: str | None) -> Any: + if out_key in (None, ""): + return payload + if not isinstance(payload, Mapping): + raise APIResponseFormatError( + f"JX3BOX 响应不是对象,无法提取字段: {out_key}" + ) + if out_key not in payload: + raise APIResponseFormatError(f"JX3BOX 响应缺少字段: {out_key}") + return payload[out_key] + + async def request_all_pages( + self, + endpoint_name: str, + *, + params: Mapping[str, Any] | None = None, + out_key: str | None = None, + path_params: Mapping[str, Any] | None = None, + page_key: str = "page", + list_key: str = "list", + max_pages: int = 10, + ) -> list[Any]: + if max_pages < 1: + raise ValueError("max_pages 必须大于等于 1") + + all_items: list[Any] = [] + request_params = dict(params or {}) + previous_page_hash: str | None = None + + for current_page in range(1, max_pages + 1): + request_params[page_key] = str(current_page) + data = await self.request( + endpoint_name, + params=request_params, + out_key=out_key, + path_params=path_params, + ) + page_items = self._extract_page_items(data, list_key) + if not page_items: + break + + page_hash = hashlib.sha256( + json.dumps( + page_items, + ensure_ascii=False, + sort_keys=True, + default=str, + ).encode("utf-8") + ).hexdigest() + if page_hash == previous_page_hash: + logger.warning( + "JX3BOX 分页接口连续返回重复页面,已停止: " + f"key={endpoint_name}, page={current_page}" + ) + break + + all_items.extend(page_items) + previous_page_hash = page_hash + + return all_items + + @staticmethod + def _extract_page_items(data: Any, list_key: str) -> list[Any]: + if data is None: + return [] + if isinstance(data, bytes): + raise APIResponseFormatError("JX3BOX 分页接口返回了二进制内容") + if isinstance(data, list): + return data + if not isinstance(data, Mapping): + raise APIResponseFormatError("JX3BOX 分页响应必须是列表或对象") + if list_key not in data: + raise APIResponseFormatError(f"JX3BOX 分页响应缺少字段: {list_key}") + + items = data[list_key] + if not isinstance(items, list): + raise APIResponseFormatError(f"JX3BOX 分页字段 {list_key} 不是列表") + return items diff --git a/core/adapters/router.py b/core/adapters/router.py new file mode 100644 index 0000000..5902091 --- /dev/null +++ b/core/adapters/router.py @@ -0,0 +1,74 @@ +from collections.abc import Mapping +from typing import Any + +from ..api_config import APIConfigError, APIEndpointRegistry +from ..request import APIClient +from .base import EndpointAdapter +from .jx3api import JX3ApiAdapter +from .jx3box import JX3BoxAdapter + + +class APIAdapterRouter: + """依据服务配置把端点请求路由到对应 Adapter。""" + + def __init__( + self, + client: APIClient, + registry: APIEndpointRegistry, + *, + jx3api_token: str = "", + jx3api_ticket: str = "", + ): + self._registry = registry + self._adapters: dict[str, EndpointAdapter] = { + "generic": EndpointAdapter(client, registry), + "jx3api": JX3ApiAdapter( + client, + registry, + token=jx3api_token, + ticket=jx3api_ticket, + ), + "jx3box": JX3BoxAdapter(client, registry), + } + + def get_adapter(self, endpoint_name: str) -> EndpointAdapter: + adapter_name = self._resolve_adapter_name(endpoint_name) + try: + return self._adapters[adapter_name] + except KeyError as exc: + raise APIConfigError( + f"端点引用了不支持的 Adapter: {endpoint_name} -> {adapter_name}" + ) from exc + + async def request( + self, + endpoint_name: str, + *, + params: Mapping[str, Any] | None = None, + out_key: str | None = "data", + path_params: Mapping[str, Any] | None = None, + ) -> Any: + adapter = self.get_adapter(endpoint_name) + return await adapter.request( + endpoint_name, + params=params, + out_key=out_key, + path_params=path_params, + ) + + async def request_all_pages( + self, + endpoint_name: str, + **kwargs, + ) -> list[Any]: + adapter = self.get_adapter(endpoint_name) + if not isinstance(adapter, JX3BoxAdapter): + raise APIConfigError( + f"端点不属于 JX3BOX,不能使用 JX3BOX 分页规则: {endpoint_name}" + ) + return await adapter.request_all_pages(endpoint_name, **kwargs) + + def _resolve_adapter_name(self, endpoint_name: str) -> str: + endpoint = self._registry.get(endpoint_name) + service = self._registry.get_service(str(endpoint["service"])) + return str(service.get("adapter", "generic")).lower() diff --git a/core/api_config.py b/core/api_config.py new file mode 100644 index 0000000..2a1d67a --- /dev/null +++ b/core/api_config.py @@ -0,0 +1,115 @@ +import json +from collections.abc import Mapping +from pathlib import Path +from typing import Any +from urllib.parse import quote, urlsplit + + +class APIConfigError(ValueError): + """API 配置缺失或格式不合法。""" + + +class APIEndpointRegistry: + """将服务地址与端点相对路径分离。""" + + def __init__(self, config: Mapping[str, Any]): + self._services = config.get("_services", {}) + self._endpoints = { + key: value for key, value in config.items() if not str(key).startswith("_") + } + self._validate() + + @classmethod + def from_file(cls, config_path: str | Path) -> "APIEndpointRegistry": + path = Path(config_path) + try: + with path.open("r", encoding="utf-8") as file: + config = json.load(file) + except FileNotFoundError as exc: + raise APIConfigError(f"API 配置文件不存在: {path}") from exc + except json.JSONDecodeError as exc: + raise APIConfigError( + f"API 配置 JSON 不合法: {path}, line={exc.lineno}" + ) from exc + + if not isinstance(config, Mapping): + raise APIConfigError("API 配置根节点必须是对象") + return cls(config) + + def _validate(self) -> None: + if not isinstance(self._services, Mapping): + raise APIConfigError("_services 必须是对象") + + for service_name, service in self._services.items(): + if not isinstance(service, Mapping): + raise APIConfigError(f"服务配置必须是对象: {service_name}") + base_url = str(service.get("base_url", "")).strip() + parsed = urlsplit(base_url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise APIConfigError(f"服务 base_url 不合法: {service_name}={base_url}") + adapter = str(service.get("adapter", "generic")).lower() + if adapter not in {"generic", "jx3api", "jx3box"}: + raise APIConfigError(f"服务 adapter 不受支持: {service_name}={adapter}") + + for endpoint_name, endpoint in self._endpoints.items(): + if not isinstance(endpoint, Mapping): + raise APIConfigError(f"端点配置必须是对象: {endpoint_name}") + + method = str(endpoint.get("method", "GET")).upper() + if method not in {"GET", "POST"}: + raise APIConfigError( + f"端点 method 仅支持 GET/POST: {endpoint_name}={method}" + ) + params = endpoint.get("params", {}) + if not isinstance(params, Mapping): + raise APIConfigError(f"端点 params 必须是对象: {endpoint_name}") + if "retry" in endpoint and not isinstance(endpoint["retry"], bool): + raise APIConfigError(f"端点 retry 必须是布尔值: {endpoint_name}") + + service_name = endpoint.get("service") + path = endpoint.get("path") + if service_name not in self._services: + raise APIConfigError( + f"端点引用了不存在的服务: {endpoint_name} -> {service_name}" + ) + if not isinstance(path, str) or not path.strip(): + raise APIConfigError(f"端点缺少相对路径: {endpoint_name}") + + def get(self, endpoint_name: str) -> Mapping[str, Any]: + endpoint = self._endpoints.get(endpoint_name) + if endpoint is None: + raise APIConfigError(f"未找到 API 端点: {endpoint_name}") + return endpoint + + def get_service(self, service_name: str) -> Mapping[str, Any]: + service = self._services.get(service_name) + if service is None: + raise APIConfigError(f"未找到 API 服务: {service_name}") + return service + + def resolve( + self, + endpoint_name: str, + path_params: Mapping[str, Any] | None = None, + ) -> str: + endpoint = self.get(endpoint_name) + + service_name = str(endpoint["service"]) + service = self._services[service_name] + base_url = str(service["base_url"]).rstrip("/") + path_prefix = str(service.get("path_prefix", "")).strip("/") + endpoint_path = str(endpoint["path"]).lstrip("/") + + encoded_params = { + key: quote(str(value), safe="") + for key, value in (path_params or {}).items() + } + try: + endpoint_path = endpoint_path.format_map(encoded_params) + except KeyError as exc: + raise APIConfigError( + f"端点 {endpoint_name} 缺少路径参数: {exc.args[0]}" + ) from exc + + path_parts = [part for part in (path_prefix, endpoint_path) if part] + return f"{base_url}/{'/'.join(path_parts)}" diff --git a/core/async_task.py b/core/async_task.py index c4105a7..96c9104 100644 --- a/core/async_task.py +++ b/core/async_task.py @@ -107,10 +107,10 @@ class AsyncTask: async def init_tasks(self): settings = [ - ("kfts", "开服监控", lambda: self.jx3fun.kaifu(self.server)), - ("xwts", "新闻资讯", lambda: self.jx3fun.xinwen(1)), - ("smts", "刷马消息", lambda: self.jx3fun.shuamamsg(self.server,type="horse",subtype="foreshow")), - ("ctts", "赤兔消息", lambda: self.jx3fun.shuamamsg(self.server,type="chitu-horse",subtype="share_msg")), + ("kfts", "开服监控", lambda: self.jx3fun.information.kaifu(self.server)), + ("xwts", "新闻资讯", lambda: self.jx3fun.information.xinwen(1)), + ("smts", "刷马消息", lambda: self.jx3fun.calendar.shuamamsg(self.server,type="horse",subtype="foreshow")), + ("ctts", "赤兔消息", lambda: self.jx3fun.calendar.shuamamsg(self.server,type="chitu-horse",subtype="share_msg")), ] for key, name, fetch in settings: diff --git a/core/jx3_data.py b/core/jx3_data.py index 644eb0c..9a3771d 100644 --- a/core/jx3_data.py +++ b/core/jx3_data.py @@ -1,3538 +1,92 @@ -import json -from datetime import datetime, timedelta -from typing import Dict, Any, Optional, List, Union -from collections import defaultdict +from typing import Final -from astrbot.api import logger -from astrbot.api import AstrBotConfig -import astrbot.api.message_components as Comp +from astrbot.api import AstrBotConfig, logger +from .adapters import APIAdapterRouter +from .api_config import APIEndpointRegistry from .request import APIClient +from .services import ( + AchievementService, + AdventureService, + ArenaService, + CalendarService, + CharacterService, + ContentService, + FactionService, + InformationService, + TradeService, +) +from .services.base import ServiceContext +from .services.cache import JSONCacheRepository from .sqlite import AsyncSQLiteDB -from .fun_basic import load_template,gold_to_string,gold_to_parts,week_to_num,compare_date_str -ROLE_RANK_NAMES = { - "名士五十强", - "老江湖五十强", - "兵甲藏家五十强", - "名师五十强", - "阵营英雄五十强", - "薪火相传五十强", - "庐园广记一百强", -} - -TONG_RANK_NAMES = { - "浩气神兵宝甲五十强", - "恶人神兵宝甲五十强", - "浩气爱心帮会五十强", - "恶人爱心帮会五十强", - "赛季恶人五十强", - "赛季浩气五十强", - "上周恶人五十强", - "上周浩气五十强", - "本周恶人五十强", - "本周浩气五十强", -} - -ACHIEVEMENT_CHOICES = [ - (0, None, "资历总览"), - (1, "1", "杂闻总览"), - (2, "2", "武学总览"), - (3, "3", "修为总览"), - (4, "4", "装备总览"), - (5, "5", "技艺总览"), - (6, "6", "阅读总览"), - (7, "7", "任务总览"), - (8, "8", "足迹总览"), - (9, "9", "战斗总览"), - (10, "10", "声望总览"), - (11, "11", "秘境总览"), - (12, "12", "帮会总览"), - (13, "13", "阵营总览"), - (14, "15", "节日总览"), - (15, "16", "活动总览"), - (16, "17", "风雨江湖路总览"), - (17, "40", "家园总览"), - (18, "41", "剑侠录总览"), -] - -ACHIEVEMENT_CHOICE_MAP = {index: (menu_id, title) for index, menu_id, title in ACHIEVEMENT_CHOICES} class JX3Service: - def __init__(self, api_data_path: str, config: AstrBotConfig, sqlite: AsyncSQLiteDB, cache_sqlite: Optional[AsyncSQLiteDB] = None): - # 加载API配置文件 - try: - with open(api_data_path, 'r', encoding='utf-8') as f: - self._api_config = json.load(f) - except FileNotFoundError as e: - logger.error(f"加载API配置文件失败: {e}") - self._api_config = {} - # 实例化 API Client - self._api: APIClient = APIClient() - # 引用插件配置文件 - self._config = config - # 引用sqlite - self._sql_db = sqlite - self._cache_db = cache_sqlite or sqlite - - # 获取配置中的 Token - self.token = self._config.get("jx3api_token", "") - if self.token == "": - logger.warning("获取配置token失败,请正确填写token,否则部分功能无法正常使用") - else: - logger.debug(f"获取配置token成功。{self.token}") - # 获取配置中的 ticket - self.ticket = self._config.get("jx3api_ticket", "") - if self.ticket == "": - logger.warning("获取配置ticket失败,请正确填写ticket,否则部分功能无法正常使用") - else: - logger.debug(f"获取配置ticket成功。{self.ticket}") - - - async def close(self): - """释放底层 APIClient 资源""" - if self._api: - await self._api.close() - - - def _init_return_data(self) -> Dict[str, Any]: - """初始化标准的返回数据结构""" - return { - "code": 0, - "msg": "功能函数未执行", - "data": {}, - "temp": "", - "icons": {} - } - - - async def _base_request( - self, - config_key: str, - method: str, - params: Optional[Dict[str, Any]] = None, - out_key: Optional[str] = "data" - ) -> Optional[Any]: - """ - 基础请求封装,处理配置获取和API调用。 - - :param config_key: 配置字典中对应 API 的键名。 - :param method: HTTP方法 ('GET' 或 'POST')。 - :param params: 请求参数或 Body 数据。 - :param out_key: 响应数据中需要提取的字段。 - :return: 成功时返回提取后的数据,失败时返回 None。 - """ - try: - if not self._api: - logger.error("API client is not initialized") - return None - - api_config = self._api_config.get(config_key) - if not api_config: - logger.error(f"配置文件中未找到 key: {config_key}") - return None - - request_params = api_config.get("params", {}).copy() - if params: - request_params.update(params) - - url = api_config.get("url", "") - if not url: - logger.error(f"API配置缺少 URL: {config_key}") - return None - - if method.upper() == 'POST': - data = await self._api.post(url, data=request_params, out_key=out_key) - else: # 默认为 GET - data = await self._api.get(url, params=request_params, out_key=out_key) - - if not data: - logger.warning(f"获取接口信息失败或返回空数据: {config_key}") - - return data - - except Exception as e: - logger.error(f"基础请求调用出错 ({config_key}): {e}") - return None - - - # --- 业务功能函数 --- - async def helps(self) -> Dict[str, Any]: - """帮助""" - return_data = self._init_return_data() - - # 加载模板 - try: - return_data["temp"] = await load_template("helps.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def richang(self,server: str, num: int = 0) -> Dict[str, Any]: - """日常活动""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "num": num} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_richang", "GET", params=params - ) - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - # 格式化字符串,利用字典的 get 方法提供默认值 - result_msg = ( - f"{server}\n{data.get('date', '未知日期')} 星期{data.get('week', '未知')}\n" - f"大战:{data.get('war', '无')}\n" - f"战场:{data.get('battle', '无')}\n" - f"阵营:{data.get('orecar', '无')}\n" - f"宗门:{data.get('school', '无')}\n" - f"驰援:{data.get('rescue', '无')}\n" - f"画像:{data.get('draw', '无')}\n\n" - ) - - # 安全地处理列表索引 - luck = data.get('luck', []) - luck_msg = f"【宠物福缘】\n{', '.join(luck)}\n" - card = data.get('card', []) - card_msg = f"【家园声望·加倍道具】\n{', '.join(card)}\n" - team = data.get('team', [None, None, None]) - team_msg = f"【武林通鉴·公共任务】\n{team[0] or '无'}\n【武林通鉴·团队秘境】\n{team[1] or '无'}\n" - - return_data["data"] = result_msg + luck_msg + card_msg + team_msg - return_data["code"] = 200 - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - return_data["code"] = 200 - - return return_data - - async def richangyuche(self) -> Dict[str, Any]: - """日常预测""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = { "num": 30} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_richangyuche", "GET", params=params - ) - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - items = [] - num_richang = week_to_num(data["data"][0]["week"]) or 0 - # 空白数据 - for _ in range(num_richang): - items.append({ - "en": False, - "compare": "", - "date": "", - "war": "", - "battle": "" - }) - - # 真实数据 - for m in data["data"]: - items.append({ - "en": True, - "compare": compare_date_str(m.get("date", "")), - "date": m.get("date", ""), - "war": m.get("war", ""), - "battle": m.get("battle", "") - }) - - return_data["data"]["items"] = items - return_data["data"]["today"] = data["today"] - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 加载模板 - try: - return_data["temp"] = await load_template("richangyuche.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def xingxiashijian(self,name: str) -> Dict[str, Any]: - """行侠事件""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = { "name": name} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_xingxiashijian", "GET", params=params - ) - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - return_data["data"]["items"] = data - return_data["data"]["name"] = name - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 加载模板 - try: - return_data["temp"] = await load_template("xingxiashijian.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def keju(self,subject: str, limit: int) -> Dict[str, Any]: - """科举""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"subject": subject, "limit": limit} - - # 2. 调用基础请求 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_keju", "GET", params=params - ) - if not data: - return_data["msg"] = "未查询到相关题目" - return return_data - - # 3. 处理返回数据 - try: - # 格式化字符串,利用字典的 get 方法提供默认值 - result_msg = "" - for m in data: - result_msg += f"{m['id']}.{m['question']}\n" - result_msg += f"答案:{m['answer']}\n\n" - - return_data["data"] = result_msg - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def huajia(self,server: str, name: str, map: str) -> Dict[str, Any]: - """花价""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "name": name, "map": map} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_huajia", "GET", params=params - ) - if not data: - return_data["msg"] = "未查询到相关内容" - return return_data - - # 3. 处理返回数据 - try: - return_data["data"]["data"] = data - return_data["data"]["server"] = server - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 加载模板 - try: - return_data["temp"] = await load_template("huajia.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def zhuangshi(self,name: str) -> Dict[str, Any]: - """装饰""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = { "name": name} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_zhuangshi", "GET", params=params - ) - if not data: - return_data["msg"] = "未查询到相关内容" - return return_data - - # 3. 处理返回数据 - try: - return_data["data"]["data"] = data - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 加载模板 - try: - return_data["temp"] = await load_template("zhuangshi.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def qiwu(self,name: str) -> Dict[str, Any]: - """器物""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = { "name": name} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_qiwu", "GET", params=params - ) - if not data: - return_data["msg"] = "未查询到相关内容" - return return_data - - # 3. 处理返回数据 - try: - return_data["data"]["data"] = data - return_data["data"]["name"] = name - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 加载模板 - try: - return_data["temp"] = await load_template("qiwu.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def xinwen(self, num:int) -> Dict[str, Any]: - """新闻资讯""" - return_data = self._init_return_data() - - params = {"limit": num} - # 提取字段可能返回列表 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_xinweng", "GET", params=params) - - if not data or not isinstance(data, list): - return_data["msg"] = "获取接口信息失败或数据格式错误" - return return_data - - try: - # - result = data[0] - return_data["status"] = result.get('id') - - result_msg = "新闻资讯推送\n" - # 仅展示前1条,避免消息过长 - for i, item in enumerate(data[:num], 1): - result_msg += f"{i}. 【{item.get('type', '无类型')}】\n" - result_msg += f"标题:{item.get('title', '未知时间')}\n" - result_msg += f"时间:{item.get('date', '未知时间')}\n" - result_msg += f"链接:{item.get('url', '无链接')}\n" - - return_data["data"] = result_msg - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - - return_data["code"] = 200 - - return return_data - - - async def weihu(self, num:int) -> Dict[str, Any]: - """维护""" - return_data = self._init_return_data() - - params = {"limit": num} - # 提取字段可能返回列表 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_weihu", "GET", params=params) - - if not data or not isinstance(data, list): - return_data["msg"] = "获取接口信息失败或数据格式错误" - return return_data - - try: - # - result = data[0] - return_data["status"] = result.get('id') - - result_msg = "维护推送\n" - # 仅展示前1条,避免消息过长 - for i, item in enumerate(data[:num], 1): - result_msg += f"{i}. 【{item.get('type', '无类型')}】\n" - result_msg += f"标题:{item.get('title', '未知时间')}\n" - result_msg += f"时间:{item.get('date', '未知时间')}\n" - result_msg += f"链接:{item.get('url', '无链接')}\n" - - return_data["data"] = result_msg - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - - return_data["code"] = 200 - - return return_data - - async def qufu(self,name: str) -> Dict[str, Any]: - """区服信息""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"name": name} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_qufu", "GET", params=params - ) - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - # 格式化字符串,利用字典的 get 方法提供默认值 - result_msg = f"主服:{data.get('zone', '无')}-{data.get('name', '无')}\n" - - slave = data.get('slave', []) - slave_msg = f"区服:{', '.join(slave)}\n" - - alias = data.get('alias', []) - alias_msg = f"别名:{', '.join(alias)}\n" - - return_data["data"] = result_msg + slave_msg + alias_msg - return_data["code"] = 200 - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def kaifu(self, server: str) -> Dict[str, Any]: - """开服状态查询""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server,"type": "1"} - - # 2. 调用基础请求 - data: Optional[Dict[str, Union[int, str]]] = await self._base_request( - "jx3_zhuangtai", "GET", params=params - ) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - status = data.get("status", 0) - lasttime = data.get("lasttime", 0) - shuttime = data.get("shuttime", 0) - - lasttime_t = datetime.fromtimestamp(float(lasttime)).strftime("%Y-%m-%d %H:%M:%S") - shuttime_t = datetime.fromtimestamp(float(shuttime)).strftime("%Y-%m-%d %H:%M:%S") - - if status == 1: - status_str = f"{server}服务器已开服,快冲,快冲!\n开服时间:{lasttime_t}" - status_bool = True - else: - status_str = f"{server}服务器当前维护中,等会再来吧!\n维护时间:{shuttime_t}" - status_bool = False - - return_data["status"] = status_bool - return_data["data"] = status_str - - except Exception as e: - logger.error(f"kaifu 数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def zhuangtai(self) -> Dict[str, Any]: - """区服状态""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": "","type": "2"} - - data: Optional[List[Dict[str, Any]]] = await self._base_request("jx3_zhuangtai", "GET", params=params) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - # 处理返回数据 - try: - server_wj = [] - server_dx = [] - server_sx = [] - - for itme in data: - if itme['zone'] == "无界区": - server_wj.append(itme) - elif itme['zone'] == "电信区": - server_dx.append(itme) - elif itme['zone'] == "双线区": - server_sx.append(itme) - - return_data["data"]["server_wj"] = server_wj - return_data["data"]["server_dx"] = server_dx - return_data["data"]["server_sx"] = server_sx - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - # 加载模板 - try: - return_data["temp"] = await load_template("qufuzhuangtai.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def jigai(self) -> Dict[str, Any]: - """技改记录""" - return_data = self._init_return_data() - - # 提取字段可能返回列表 - data: Optional[List[Dict[str, Any]]] = await self._base_request("jx3_jigai", "GET") - - if not data or not isinstance(data, list): - return_data["msg"] = "获取接口信息失败或数据格式错误" - return return_data - - try: - result_msg = "最近技改\n" - - for i, item in enumerate(data[:3], 1): - result_msg += f"{i}. {item.get('title', '无标题')}\n" - result_msg += f"时间:{item.get('time', '未知时间')}\n" - result_msg += f"链接:{item.get('url', '无链接')}\n\n" - - return_data["data"] = result_msg - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def xiaoyao(self) -> Dict[str, Any]: - """小药""" - return_data = self._init_return_data() - - data: Optional[List[Dict[str, Any]]] = await self._base_request("jx3_xiaoyao", "GET") - logger.debug(data) - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 处理返回数据 - try: - result = {} - - for item in data: - k = item["kungfu"] - color = item["color"] - cls = item["class"] - name = item["name"] - - if k not in result: - result[k] = { - "kungfu": k, - "purple": {}, - "blue": {} - } - - if color == "紫": - result[k]["purple"][cls] = name - else: - result[k]["blue"][cls] = name - - return_data["data"]["items"] = list(result.values()) - logger.debug(return_data["data"]) - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - - # 加载模板 - try: - return_data["temp"] = await load_template("xiaoyao.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def zhenyan(self, name: str) -> Dict[str, Any]: - """阵眼""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"name": name, "ticket": self.ticket, "token": self.token} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_zhenyan", "GET", params=params - ) - - if not data or not isinstance(data, dict): - return_data["msg"] = "未查询到该心法阵眼信息" - return return_data - - # 3. 处理返回数据 - try: - items = data.get("data", []) - if not isinstance(items, list) or not items: - return_data["msg"] = "未查询到该心法阵眼信息" - return return_data - - result_msg = f"{data.get('name', name)}-{data.get('skillName', '')}\n" - for item in items: - if not isinstance(item, dict): - continue - result_msg += f"{item.get('name', '')}:{item.get('desc', '')}\n" - - return_data["data"] = result_msg.rstrip() - except Exception as e: - logger.error(f"处理阵眼数据失败: {e}") - return_data["msg"] = "处理阵眼数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def qixue(self, name: str) -> Dict[str, Any]: - """奇穴""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"name": name, "ticket": self.ticket, "token": self.token} - - # 2. 调用基础请求 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_qixue", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到该心法奇穴信息" - return return_data - - # 3. 处理返回数据 - try: - level_titles = { - 1: "主奇穴", - 2: "第一重", - 3: "第二重", - 4: "第三重", - 5: "第四重", - 6: "第五重", - 7: "第六重", - 8: "混池", - } - - groups = [] - for group in data: - if not isinstance(group, dict): - continue - - level = group.get("level") - try: - level_key = int(level) - except (TypeError, ValueError): - level_key = 0 - - items = group.get("data", []) - if not isinstance(items, list) or not items: - continue - - parsed_items = [] - for item in items: - if not isinstance(item, dict): - continue - - class_value = item.get("class") - is_active = class_value == 1 or str(class_value) == "1" - parsed_items.append({ - "name": item.get("name", ""), - "icon": item.get("icon", ""), - "desc": item.get("desc", ""), - "class_text": "主动" if is_active else "被动", - "interval": item.get("interval", "") if is_active else "", - }) - - if parsed_items: - groups.append({ - "level": level_key, - "title": level_titles.get(level_key, f"第{level_key}组"), - "talents": parsed_items, - }) - - if not groups: - return_data["msg"] = "未查询到该心法奇穴信息" - return return_data - - groups.sort(key=lambda item: item["level"]) - return_data["data"] = { - "name": name, - "groups": groups, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - except Exception as e: - logger.error(f"处理奇穴数据失败: {e}") - return_data["msg"] = "处理奇穴数据失败" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("qixue.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - return return_data - - - async def jineng(self, name: str) -> Dict[str, Any]: - """技能""" - return_data = self._init_return_data() - - params = {"name": name, "ticket": self.ticket, "token": self.token} - - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_jineng", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到该心法技能信息" - return return_data - - try: - groups = [] - for group in data: - if not isinstance(group, dict): - continue - - skills = group.get("data", []) - if not isinstance(skills, list) or not skills: - continue - - parsed_skills = [] - for skill in skills: - if not isinstance(skill, dict): - continue - - parsed_skills.append({ - "name": skill.get("name", ""), - "icon": skill.get("icon", ""), - "desc": skill.get("desc", ""), - "interval": skill.get("interval", ""), - "distance": skill.get("distance", ""), - "release_type": skill.get("releaseType", ""), - "weapon": skill.get("weapon", ""), - }) - - if parsed_skills: - groups.append({ - "title": group.get("class", "其他技能"), - "skills": parsed_skills, - }) - - if not groups: - return_data["msg"] = "未查询到该心法技能信息" - return return_data - - return_data["data"] = { - "name": name, - "groups": groups, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - except Exception as e: - logger.error(f"处理技能数据失败: {e}") - return_data["msg"] = "处理技能数据失败" - return return_data - - try: - return_data["temp"] = await load_template("jineng.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def zilipaixing(self, school: str, server: str) -> Dict[str, Any]: - """资历排行""" - return_data = self._init_return_data() - - params = { - "server": server, - "school": school, - "ticket": self.ticket, - "token": self.token, - } - - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_zilipaixing", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到资历排行信息" - return return_data - - try: - return_data["data"] = { - "items": data, - "school": school, - "server": server, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - except Exception as e: - logger.error(f"处理资历排行数据失败: {e}") - return_data["msg"] = "处理资历排行数据失败" - return return_data - - try: - return_data["temp"] = await load_template("zilipaixing.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def shilianpaixing(self, name: str, server: str) -> Dict[str, Any]: - """试炼排行""" - return_data = self._init_return_data() - - params = { - "server": server, - "name": name, - "token": self.token, - } - - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_shilianpaixing", "GET", params=params - ) - - if not data or not isinstance(data, dict): - return_data["msg"] = "未查询到试炼排行信息" - return return_data - - try: - items = data.get("data", []) - if not isinstance(items, list) or not items: - return_data["msg"] = "未查询到试炼排行信息" - return return_data - - update_time = data.get("time") - if update_time: - try: - update_time = datetime.fromtimestamp(int(update_time)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - update_time = str(update_time) - else: - update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - return_data["data"] = { - "items": items, - "name": data.get("name", name), - "server": data.get("server", server), - "update_time": update_time - } - except Exception as e: - logger.error(f"处理试炼排行数据失败: {e}") - return_data["msg"] = "处理试炼排行数据失败" - return return_data - - try: - return_data["temp"] = await load_template("shilianpaixing.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def shaohua(self) -> Dict[str, Any]: - """骚话""" - return_data = self._init_return_data() - - # 因为没有参数,所以 params=None - data: Optional[Dict[str, Any]] = await self._base_request("jx3_shaohua", "GET") - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - text = data.get("text") - if text: - return_data["data"] = text - else: - return_data["msg"] = "接口未返回文本" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def jiemi(self) -> Dict[str, Any]: - """解密""" - return_data = self._init_return_data() - - params = {"token": self.token} - - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_jiemi", "GET", params=params - ) - - if not data or not isinstance(data, dict): - return_data["msg"] = "未查询到解密信息" - return return_data - - try: - curr = data.get("curr", {}) - next_data = data.get("next", {}) - if not isinstance(curr, dict): - curr = {} - if not isinstance(next_data, dict): - next_data = {} - - return_data["data"] = "\n".join([ - "解密", - f"当前时间:{data.get('time', '')}", - f"当前节点:{curr.get('node', '')}", - f"当前结果:{curr.get('data', '')}", - f"下轮节点:{next_data.get('node', '')}", - f"下轮结果:{next_data.get('data', '')}", - f"剩余时间:{data.get('cdtn', '')}", - ]) - except Exception as e: - logger.error(f"处理解密数据失败: {e}") - return_data["msg"] = "处理解密数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def shapan(self, server: str ) -> Dict[str, Any]: - """区服沙盘""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"serverName": server} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "aijx3_shapan", "POST", params=params - ) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 (直接提取图片 URL) - pic_url = data.get("picUrl") - if pic_url: - return_data["data"] = pic_url - else: - return_data["msg"] = "接口未返回图片URL" - return return_data - - return_data["code"] = 200 - - return return_data - - - - async def baizhan(self) -> Dict[str, Any]: - """百战首领""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = { "token": self.token} - - data: Optional[Dict[str, Any]] = await self._base_request("jx3_baizhan", "GET", params=params) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - # 处理返回数据 - - try: - return_data["data"] = data - return_data["data"]["start"] = datetime.fromtimestamp(float(data["start"])).strftime("%Y-%m-%d %H:%M:%S") - return_data["data"]["end"] = datetime.fromtimestamp(float(data["end"])).strftime("%Y-%m-%d %H:%M:%S") - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - # 加载模板 - try: - return_data["temp"] = await load_template("baizhan.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def fuyaojjiutian(self, server: str) -> Dict[str, Any]: - """扶摇九天""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "token": self.token} - - # 2. 调用基础请求 - data: Optional[list[Dict[str, Any]]] = await self._base_request( - "jx3_fuyaojiutian", "GET", params=params - ) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - data_new = data[0] - data_time = datetime.fromtimestamp(data_new['time']).strftime('%Y-%m-%d %H:%M:%S') - result_msg = f"{server}\n" - if data_new["status"] == 0: - result_msg += f"本轮扶摇九天尚未开启\n" - if data_new["status"] == 1: - result_msg += f"本轮扶摇九天正在进行\n" - if data_new["status"] == 2: - result_msg += f"本轮扶摇九天已经结束\n" - result_msg += f"开启时间:{data_time}\n" - return_data["data"] = result_msg - except Exception as e: - logger.error(f"处理返回数据失败: {e}") - return_data["msg"] = "处理返回数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def zhueevent(self) -> Dict[str, Any]: - """诛恶事件""" - return_data = self._init_return_data() - - params = {"token": self.token} - - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_zhueevent", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到诛恶事件信息" - return return_data - - try: - items = [] - for item in data: - if not isinstance(item, dict): - continue - - event_time = item.get("time") - if event_time: - try: - item["time"] = datetime.fromtimestamp(int(event_time)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - item["time"] = str(event_time) - else: - item["time"] = "" - - items.append(item) - - if not items: - return_data["msg"] = "未查询到诛恶事件信息" - return return_data - - return_data["data"] = { - "items": items, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - except Exception as e: - logger.error(f"处理诛恶事件数据失败: {e}") - return_data["msg"] = "处理诛恶事件数据失败" - return return_data - - try: - return_data["temp"] = await load_template("zhueevent.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def benrichitu(self) -> Dict[str, Any]: - """本日赤兔""" - return_data = self._init_return_data() - - params = {"token": self.token} - - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_benrichitu", "GET", params=params - ) - - def no_data() -> Dict[str, Any]: - return_data["code"] = 200 - return_data["data"] = "本日赤兔 暂无数据" - return return_data - - if not data or not isinstance(data, list): - return no_data() - - try: - items = [] - for item in data: - if not isinstance(item, dict): - continue - - date_value = item.get("date", "") - if date_value: - try: - date_text = datetime.fromtimestamp(int(date_value)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - date_text = str(date_value) - else: - date_text = "" - - items.append({ - "date": date_text, - "server": item.get("server", ""), - "map_name": item.get("map_name", ""), - }) - - if not items: - return no_data() - - result_lines = ["本日赤兔"] - for item in items: - result_lines.extend([ - f"时间:{item['date']}", - f"区服:{item['server']}", - f"地图:{item['map_name']}", - "", - ]) - - return_data["code"] = 200 - return_data["data"] = "\n".join(result_lines).rstrip() - except Exception as e: - logger.error(f"处理本日赤兔数据失败: {e}") - return no_data() - - return return_data - - - async def benzhouchitu(self) -> Dict[str, Any]: - """本周赤兔""" - return_data = self._init_return_data() - - params = {"token": self.token} - - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_benzhouchitu", "GET", params=params - ) - - def no_data() -> Dict[str, Any]: - return_data["code"] = 200 - return_data["data"] = "本周赤兔 暂无数据" - return return_data - - if not data or not isinstance(data, list): - return no_data() - - try: - items = [] - for item in data: - if not isinstance(item, dict): - continue - - date_value = item.get("date", "") - if date_value: - try: - date_text = datetime.fromtimestamp(int(date_value)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - date_text = str(date_value) - else: - date_text = "" - - items.append({ - "date": date_text, - "server": item.get("server", ""), - "map_name": item.get("map_name", ""), - }) - - if not items: - return no_data() - - result_lines = ["本周赤兔"] - for item in items: - result_lines.extend([ - f"时间:{item['date']}", - f"区服:{item['server']}", - f"地图:{item['map_name']}", - "", - ]) - - return_data["code"] = 200 - return_data["data"] = "\n".join(result_lines).rstrip() - except Exception as e: - logger.error(f"处理本周赤兔数据失败: {e}") - return no_data() - - return return_data - - - async def zhengyingpaimai(self, server: str, name: str) -> Dict[str, Any]: - """阵营拍卖""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "name": name, "token": self.token} - - # 2. 调用基础请求 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_zhengyingpaimai", "GET", params=params - ) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - for item in data: - item["time"] = datetime.fromtimestamp(item["time"]).strftime("%Y-%m-%d %H:%M:%S") - return_data["data"]["list"] = data - except Exception as e: - logger.error(f"处理返回数据失败: {e}") - return_data["msg"] = "处理返回数据失败" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("zhengyingpaimai.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - - return_data["code"] = 200 - - return return_data - - - async def zhenyingevent(self) -> Dict[str, Any]: - """阵营事件""" - return_data = self._init_return_data() - - params = {"token": self.token} - - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_zhenyingevent", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到阵营事件信息" - return return_data - - try: - items = [] - for item in data: - if not isinstance(item, dict): - continue - - seize_time = item.get("seize_time") - if seize_time: - try: - item["seize_time"] = datetime.fromtimestamp(int(seize_time)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - item["seize_time"] = str(seize_time) - else: - item["seize_time"] = "" - - items.append(item) - - if not items: - return_data["msg"] = "未查询到阵营事件信息" - return return_data - - return_data["data"] = { - "items": items, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - except Exception as e: - logger.error(f"处理阵营事件数据失败: {e}") - return_data["msg"] = "处理阵营事件数据失败" - return return_data - - try: - return_data["temp"] = await load_template("zhenyingevent.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def guanaishouling(self) -> Dict[str, Any]: - """关隘首领""" - return_data = self._init_return_data() - - params = {"token": self.token} - - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_guanaishouling", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到关隘首领信息" - return return_data - - try: - now_ts = int(datetime.now().timestamp()) - - def format_time(value: Any) -> str: - if not value: - return "" - try: - return datetime.fromtimestamp(int(value)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - return str(value) - - def format_remaining(end_time: Any) -> str: - if not end_time: - return "" - try: - seconds = max(0, int(end_time) - now_ts) - except (TypeError, ValueError): - return str(end_time) - - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - secs = seconds % 60 - return f"{hours}时{minutes:02d}分{secs:02d}秒" - - groups = [] - for group in data: - if not isinstance(group, dict): - continue - - records = group.get("data", []) - if not isinstance(records, list) or not records: - continue - - parsed_records = [] - for item in records: - if not isinstance(item, dict): - continue - - end_time = item.get("end_time") - parsed_records.append({ - "camp_name": item.get("camp_name", ""), - "castle": item.get("castle", ""), - "str_status": item.get("str_status", ""), - "start_time": format_time(item.get("start_time")), - "end_time": format_time(end_time), - "remaining_time": format_remaining(end_time), - }) - - if parsed_records: - groups.append({ - "server": group.get("server", ""), - "records": parsed_records, - }) - - if not groups: - return_data["msg"] = "未查询到关隘首领信息" - return return_data - - return_data["data"] = { - "groups": groups, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - except Exception as e: - logger.error(f"处理关隘首领数据失败: {e}") - return_data["msg"] = "处理关隘首领数据失败" - return return_data - - try: - return_data["temp"] = await load_template("guanaishouling.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def bangzhanjilu(self, server: str) -> Dict[str, Any]: - """帮战记录""" - return_data = self._init_return_data() - - params = { - "server": server, - "token": self.token, - } - - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_bangzhanjilu", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到帮战记录" - return return_data - - try: - def format_time(value: Any) -> str: - if not value: - return "" - try: - return datetime.fromtimestamp(int(value)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - return str(value) - - def format_duration(value: Any) -> str: - if value in (None, ""): - return "" - try: - seconds = max(0, int(value)) - except (TypeError, ValueError): - return str(value) - - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - secs = seconds % 60 - return f"{hours}时{minutes:02d}分{secs:02d}秒" - - items = [] - for item in data: - if not isinstance(item, dict): - continue - - items.append({ - "zoneName": item.get("zoneName", ""), - "serverName": item.get("serverName", ""), - "declaringTongName": item.get("declaringTongName", ""), - "acceptingTongName": item.get("acceptingTongName", ""), - "startTime": format_time(item.get("startTime")), - "matchDuration": format_duration(item.get("matchDuration")), - "endTime": format_time(item.get("endTime")), - }) - - if not items: - return_data["msg"] = "未查询到帮战记录" - return return_data - - return_data["data"] = { - "items": items, - "server": server, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - } - except Exception as e: - logger.error(f"处理帮战记录数据失败: {e}") - return_data["msg"] = "处理帮战记录数据失败" - return return_data - - try: - return_data["temp"] = await load_template("bangzhanjilu.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def tongzhanyy(self, server: str) -> Dict[str, Any]: - """统战歪歪""" - return_data = self._init_return_data() - - params = { - "server": server, - "token": self.token, - } - - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_tongzhanyy", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到统战歪歪信息" - return return_data - - try: - lines = ["统战歪歪"] - has_channel = False - - for group in data: - if not isinstance(group, dict): - continue - - group_server = group.get("server", "") - channels = group.get("data", []) - if not isinstance(channels, list) or not channels: - continue - - if len(lines) > 1: - lines.append("") - lines.append(f"服务器:{group_server}") - - for item in channels: - if not isinstance(item, dict): - continue - - has_channel = True - short_id = item.get("esid") or item.get("asid", "") - lines.extend([ - f"阵营:{item.get('campName', '')}", - f"频道ID:{item.get('sid', '')}", - f"短位ID:{short_id}", - f"在线人数:{item.get('users', '')}", - f"频道名:{item.get('snick', '')}", - "", - ]) - - if not has_channel: - return_data["msg"] = "未查询到统战歪歪信息" - return return_data - - return_data["data"] = "\n".join(lines).rstrip() - except Exception as e: - logger.error(f"处理统战歪歪数据失败: {e}") - return_data["msg"] = "处理统战歪歪数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def dilujilu(self, server: str) -> Dict[str, Any]: - """的卢记录""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "token": self.token} - - # 2. 调用基础请求 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_dilujilu", "GET", params=params - ) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - for item in data: - item["refresh_time"] = datetime.fromtimestamp(item["refresh_time"]).strftime("%Y-%m-%d %H:%M:%S") - item["capture_time"] = datetime.fromtimestamp(item["capture_time"]).strftime("%Y-%m-%d %H:%M:%S") - item["auction_time"] = datetime.fromtimestamp(item["auction_time"]).strftime("%Y-%m-%d %H:%M:%S") - return_data["data"]["list"] = data - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("dilujilu.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def yanhuachaxun(self, server: str, name:str ) -> Dict[str, Any]: - """烟花查询""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "name": name,"token": self.token} - - # 2. 调用基础请求 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_yanhuachaxun", "GET", params=params - ) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - for item in data: - if isinstance(item, dict): - timestamp = item.get("time") - if timestamp and isinstance(timestamp, (int, float)): - item["time"] = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") - else: - item["time"] = "未知时间" - - return_data["data"]["list"] = data - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("yanhuan.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def pianzhi(self, uid: str) -> Dict[str, Any]: - """骗子查询""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"uid": uid, "token": self.token} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_pianzhi", "GET", params=params - ) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - records = data["records"] - - if not records: - result_msg = "未找到该用户行骗记录,很棒!继续保持!" - else: - result_msg = "" - - for record in records: - result_msg += f"区服:{record['server']} 标签:{record['tieba']}\n\n" - - for item in record["data"]: - result_msg += f"标题:{item['title']}\n" - result_msg += f"地址:{item['url']}\n" - result_msg += f"ID:{item['tid']}\n" - result_msg += f"内容:{item['text']}\n" - result_msg += ( - f"时间:{datetime.fromtimestamp(item['time']).strftime('%Y-%m-%d %H:%M:%S')}\n\n" - ) - - result_msg += "\n\n" - return_data["data"] = result_msg - except Exception as e: - logger.error(f"处理返回数据失败: {e}") - return_data["msg"] = "处理返回数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def juesheqiyu(self, name: str, server: str) -> Dict[str, Any]: - """角色奇遇""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "name": name, "token": self.token} - - # 2. 调用基础请求 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_juesheqiyu", "GET", params=params - ) - - if not data: - return_data["msg"] = "未找到改角色奇遇信息" - return return_data - - # 3. 处理返回数据 - try: - return_data["data"]["ptqy"] = [] - return_data["data"]["jsqy"] = [] - return_data["data"]["cwqy"] = [] - - for item in data: - item["time"] = datetime.fromtimestamp(item["time"]).strftime("%Y-%m-%d %H:%M:%S") - if item["level"] == 1: - return_data["data"]["ptqy"].append(item) - if item["level"] == 2: - return_data["data"]["jsqy"].append(item) - if item["level"] == 3: - return_data["data"]["cwqy"].append(item) - except Exception as e: - logger.error(f"处理返回数据失败: {e}") - return_data["msg"] = "处理返回数据失败" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("juesheqiyu.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def weizuoqiyu(self, name: str, server: str) -> Dict[str, Any]: - """未做奇遇""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "name": name, "token": self.token} - - # 2. 调用基础请求 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_weizuoqiyu", "GET", params=params - ) - - if not data: - return_data["msg"] = "未查询到未做奇遇信息" - return return_data - - if not isinstance(data, list): - return_data["msg"] = "获取接口信息失败或数据格式错误" - return return_data - - # 3. 处理返回数据 - try: - return_data["data"]["ptqy"] = [] - return_data["data"]["jsqy"] = [] - return_data["data"]["name"] = name - return_data["data"]["server"] = server - - for item in data: - event_name = item.get("name") or item.get("event") - if not event_name: - continue - - try: - level = int(item.get("level", 0)) - except (TypeError, ValueError): - continue - - event_item = { - "event": event_name, - "time": "未触发" - } - - if level == 1: - return_data["data"]["ptqy"].append(event_item) - elif level == 2: - return_data["data"]["jsqy"].append(event_item) - - if not return_data["data"]["ptqy"] and not return_data["data"]["jsqy"]: - return_data["msg"] = "未查询到未做普通或绝世奇遇" - return return_data - except Exception as e: - logger.error(f"处理返回数据失败: {e}") - return_data["msg"] = "处理返回数据失败" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("weizuoqiyu.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def qiyutongji(self, name: str, server: str) -> Dict[str, Any]: - """奇遇统计""" - return_data = self._init_return_data() - - params = {"server": server, "name": name, "token": self.token} - - data_list: Optional[List[Dict[str, Any]]] = await self._base_request("jx3_qiyutongji", "GET", params=params) - - if not data_list or not isinstance(data_list, list): - return_data["msg"] = "获取接口信息失败或数据格式错误" - return return_data - - # 格式化时间 - try: - for item in data_list: - item["time"] = datetime.fromtimestamp(item["time"]).strftime("%Y-%m-%d %H:%M:%S") - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 准备模板渲染数据 - try: - return_data["data"] = { - "items": data_list, - "server": server, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "qiyuname": name - } - - except Exception as e: - logger.error(f"模板数据准备失败: {e}") - return_data["msg"] = "系统错误:模板渲染数据准备失败" - return return_data - - # 加载模板 - try: - return_data["temp"] = await load_template("qiyuliebiao.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def jinqiqiyu(self, server: str) -> Dict[str, Any]: - """近期奇遇""" - return_data = self._init_return_data() - - params = {"server": server, "token": self.token} - - data_list: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_jinqiqiyu", "GET", params=params - ) - - if not data_list or not isinstance(data_list, list): - return_data["msg"] = "未查询到近期奇遇信息" - return return_data - - # 格式化时间 - try: - for item in data_list: - event_time = item.get("time") - if event_time: - try: - item["time"] = datetime.fromtimestamp(int(event_time)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - item["time"] = str(event_time) - else: - item["time"] = "" - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 准备模板渲染数据 - try: - return_data["data"] = { - "items": data_list, - "server": server, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - except Exception as e: - logger.error(f"模板数据准备失败: {e}") - return_data["msg"] = "系统错误:模板渲染数据准备失败" - return return_data - - # 加载模板 - try: - return_data["temp"] = await load_template("jinqiqiyu.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def qiyuhuizong(self, server: str, num: int = 7) -> Dict[str, Any]: - """奇遇汇总""" - return_data = self._init_return_data() - - if num < 1 or num > 30: - return_data["msg"] = "汇总天数需在 1-30 天之间" - return return_data - - params = {"server": server, "num": num, "token": self.token} - - data_list: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_qiyuhuizong", "GET", params=params - ) - - if not data_list or not isinstance(data_list, list): - return_data["msg"] = "未查询到奇遇汇总信息" - return return_data - - # 整理最近一次触发信息 - try: - for item in data_list: - latest = item.get("data") - if not isinstance(latest, dict): - latest = {} - - item["latest_name"] = latest.get("name", "") - - latest_time = latest.get("time") - if latest_time: - try: - item["latest_time"] = datetime.fromtimestamp(int(latest_time)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - item["latest_time"] = str(latest_time) - else: - item["latest_time"] = "" - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 准备模板渲染数据 - try: - return_data["data"] = { - "items": data_list, - "server": server, - "num": num, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - except Exception as e: - logger.error(f"模板数据准备失败: {e}") - return_data["msg"] = "系统错误:模板渲染数据准备失败" - return return_data - - # 加载模板 - try: - return_data["temp"] = await load_template("qiyuhuizong.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def zhanji(self, name: str, server:str, mode:str) -> Dict[str, Any]: - """战绩""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "name":name, "mode":mode, "token": self.token, "ticket": self.ticket} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_zhanji", "GET", params=params - ) - - if not data: - return_data["msg"] = "查询角色战绩失败" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("zhanji.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["data"] = data - return_data["code"] = 200 - - return return_data - - - async def mingjianpaihang(self, limit: str, mode:str) -> Dict[str, Any]: - """名剑排行""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"limit": limit, "mode":mode, "token": self.token, "ticket": self.ticket} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_mingjianpaihang", "GET", params=params - ) - - if not data: - return_data["msg"] = "获取接口数据失败" - return return_data - - return_data["data"]["lists"] = data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("mingjianpaihang.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - - return_data["code"] = 200 - - return return_data - - - async def mingjiantongji(self, mode: str = "33") -> Dict[str, Any]: - """名剑统计""" - return_data = self._init_return_data() - - mode = str(mode) - if mode not in {"22", "33", "55"}: - return_data["msg"] = "模式错误,可选值:22、33、55" - return return_data - - # 1. 构造请求参数 - params = {"mode": mode, "token": self.token, "ticket": self.ticket} - - # 2. 调用基础请求 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_mingjiantongji", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到名剑统计信息" - return return_data - - # 3. 准备模板渲染数据 - try: - return_data["data"] = { - "items": data, - "mode": mode, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - except Exception as e: - logger.error(f"模板数据准备失败: {e}") - return_data["msg"] = "系统错误:模板渲染数据准备失败" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("mingjiantongji.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def tuanduizhaomu(self, server: str, keyword: str) -> Dict[str, Any]: - """团队招募""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "keyword": keyword, "token": self.token} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_tuanduizhaomu", "GET", params=params - ) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - for item in data["data"]: - item["createTime"] = datetime.fromtimestamp(item["createTime"]).strftime("%Y-%m-%d %H:%M:%S") - item["number"] = f"{item['number']}/{item['maxNumber']}" - return_data["data"]["list"] = data["data"] - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("tuanduizhaomu.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def shitu(self, type_value: int, keyword: str, server: str) -> Dict[str, Any]: - """师徒招募""" - return_data = self._init_return_data() - - if type_value not in {1, 2}: - return_data["msg"] = "师徒查询类型错误" - return return_data - - # 1. 构造请求参数 - params = {"type": type_value, "server": server, "keyword": keyword, "token": self.token} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_shitu", "GET", params=params - ) - - if not data or not isinstance(data, dict): - return_data["msg"] = "未查询到师徒招募信息" - return return_data - - # 3. 处理返回数据 - try: - items = data.get("data", []) - if not items: - return_data["msg"] = "未查询到师徒招募信息" - return return_data - - title = "收徒信息" if type_value == 1 else "拜师信息" - return_data["data"] = { - "items": items, - "server": data.get("server", server), - "keyword": keyword, - "type_value": type_value, - "title": title, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("shitu.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def rank_statistical(self, name: str, server: str) -> Dict[str, Any]: - """各类风云录榜单""" - return_data = self._init_return_data() - - if name in ROLE_RANK_NAMES: - template_name = "rank_role.html" - elif name in TONG_RANK_NAMES: - template_name = "rank_tong.html" - else: - return_data["msg"] = "榜单名称错误" - return return_data - - # 1. 构造请求参数 - params = {"server": server, "name": name, "token": self.token} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_rank_statistical", "GET", params=params - ) - - if not data or not isinstance(data, dict): - return_data["msg"] = "未查询到榜单信息" - return return_data - - # 3. 处理返回数据 - try: - items = data.get("data", []) - if not isinstance(items, list) or not items: - return_data["msg"] = "未查询到榜单信息" - return return_data - - return_data["data"] = { - "items": items, - "server": data.get("server", server), - "rank_name": data.get("name", name), - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template(template_name) - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def _load_achievement_cache(self, key: str) -> tuple[Optional[Any], bool]: - """读取资历基础数据缓存,返回数据和是否已过期""" - try: - row = await self._cache_db.select_one("achievement_cache", "key=?", (key,)) - except Exception as e: - logger.error(f"读取资历缓存失败: {e}") - return None, True - - if not row: - return None, True - - try: - payload = json.loads(row.get("content", "{}")) - updated_at = datetime.strptime(row.get("updated_at", ""), "%Y-%m-%d %H:%M:%S") - expired = datetime.now() - updated_at > timedelta(days=30) - return payload, expired - except Exception as e: - logger.error(f"解析资历缓存失败: {e}") - return None, True - - - async def _save_achievement_cache(self, key: str, payload: Any): - """写入资历基础数据缓存""" - try: - await self._cache_db.execute( - """ - INSERT INTO achievement_cache (key, content, updated_at) - VALUES (?, ?, ?) - ON CONFLICT(key) DO UPDATE SET - content=excluded.content, - updated_at=excluded.updated_at - """, - ( - key, - json.dumps(payload, ensure_ascii=False), - datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - ), - ) - except Exception as e: - logger.error(f"写入资历缓存失败: {e}") - - - async def _get_achievement_base_data(self, cache_key: str, config_key: str) -> Optional[Dict[str, Any]]: - """获取资历菜单或点数数据,优先使用未过期缓存""" - cached, expired = await self._load_achievement_cache(cache_key) - if cached and not expired: - return cached - - data: Optional[Dict[str, Any]] = await self._base_request(config_key, "GET") - if data and isinstance(data, dict): - await self._save_achievement_cache(cache_key, data) - return data - - if cached: - logger.warning(f"资历基础数据接口失败,使用旧缓存: {cache_key}") - return cached - - return None - - - async def _get_trade_item_groups(self) -> Optional[List[Dict[str, Any]]]: - """获取交易行物品库,优先使用未过期缓存""" - cache_key = "trade_item_groups" - cached, expired = await self._load_achievement_cache(cache_key) - if isinstance(cached, list) and not expired: - return cached - - data = await self._base_request("jx3box_trade_items", "GET") - if isinstance(data, list) and data: - await self._save_achievement_cache(cache_key, data) - return data - - if isinstance(cached, list) and cached: - logger.warning("交易行物品库接口失败,使用旧缓存") - return cached - - return None - - - def _flatten_trade_items(self, groups: List[Dict[str, Any]]) -> list[Dict[str, Any]]: - """从交易行物品分组中提取可查询物品""" - items = [] - for group in groups: - if not isinstance(group, dict): - continue - for item in group.get("items", []) or []: - if not isinstance(item, dict): - continue - item_id = item.get("item_id") - label = item.get("label") - if not item_id or not label: - continue - items.append( - { - "item_id": str(item_id), - "label": str(label), - "icon": str(item.get("icon") or ""), - } - ) - return items - - - def _match_trade_items(self, items: list[Dict[str, Any]], keyword: str, limit: int = 50) -> list[Dict[str, Any]]: - """按物品名模糊匹配交易行物品""" - keyword = (keyword or "").strip() - if not keyword: - return [] - - matched = [] - seen = set() - for item in items: - label = item.get("label", "") - item_id = item.get("item_id", "") - if keyword not in label or item_id in seen: - continue - seen.add(item_id) - if label == keyword: - rank = 0 - elif label.startswith(keyword): - rank = 1 - else: - rank = 2 - matched.append((rank, len(label), label, item)) - - matched.sort(key=lambda row: (row[0], row[1], row[2])) - return [row[3] for row in matched[:limit]] - - - def _flatten_achievement_ids(self, values: Any) -> list[int]: - """展开菜单中的单个资历 ID 和数组资历 ID""" - result = [] - - if isinstance(values, list): - for item in values: - result.extend(self._flatten_achievement_ids(item)) - return result - - try: - result.append(int(values)) - except (TypeError, ValueError): - pass - - return result - - - def _category_achievement_ids(self, category: Dict[str, Any]) -> list[int]: - """提取分类及其子分类包含的资历 ID""" - ids = self._flatten_achievement_ids(category.get("achievements", [])) - for child in category.get("children", []) or []: - if isinstance(child, dict): - ids.extend(self._flatten_achievement_ids(child.get("achievements", []))) - return ids - - - def _build_achievement_progress( + """组合各游戏领域 Service,并统一管理 HTTP 生命周期。""" + + DOMAIN_NAMES: Final = ( + "calendar", + "information", + "character", + "faction", + "adventure", + "arena", + "achievement", + "trade", + "content", + ) + + def __init__( self, - name: str, - achievement_ids: list[int], - point_map: Dict[str, Any], - completed_ids: set[int], - ) -> Dict[str, Any]: - """按资历点数计算完成进度""" - unique_ids = set(achievement_ids) - total_points = 0 - completed_points = 0 - completed_count = 0 + api_data_path: str, + config: AstrBotConfig, + sqlite: AsyncSQLiteDB, + cache_sqlite: AsyncSQLiteDB | None = None, + ): + registry = APIEndpointRegistry.from_file(api_data_path) + self._api = APIClient() - for achievement_id in unique_ids: - try: - point = int(point_map.get(str(achievement_id), 0) or 0) - except (TypeError, ValueError): - point = 0 - - total_points += point - if achievement_id in completed_ids: - completed_count += 1 - completed_points += point - - percent = round(completed_points / total_points * 100, 2) if total_points else 0 - - return { - "name": name, - "total_points": total_points, - "completed_points": completed_points, - "percent": percent, - "percent_text": f"{percent:.2f}%", - "achievement_count": len(unique_ids), - "completed_count": completed_count, - } - - - async def zili(self, name: str, server: str, choice: int) -> Dict[str, Any]: - """角色资历""" - return_data = self._init_return_data() - - if choice not in ACHIEVEMENT_CHOICE_MAP: - return_data["msg"] = "无效序号,结束会话" - return return_data - - role_params = {"server": server, "name": name, "token": self.token} - role_data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_jueshexinxi", "GET", params=role_params + token = str(config.get("jx3api_token", "")) + ticket = str(config.get("jx3api_ticket", "")) + router = APIAdapterRouter( + self._api, + registry, + jx3api_token=token, + jx3api_ticket=ticket, ) - if not role_data or not isinstance(role_data, dict): - return_data["msg"] = "未查询到角色" - return return_data - global_id = role_data.get("globalId") - if not global_id: - return_data["msg"] = "无法获取角色全区 ID" - return return_data - - achievement_data = await self._api.get( - "https://next2.jx3box.com/api/next2/user-achievements", - params={"jx3id": global_id}, - out_key="data", + cache_db = cache_sqlite or sqlite + context = ServiceContext( + router=router, + database=sqlite, + cache=JSONCacheRepository(cache_db), ) - if not achievement_data or not isinstance(achievement_data, dict): - return_data["msg"] = "未查询到资历数据" - return return_data - completed_ids = set() - achievements = achievement_data.get("achievements") or "" - if isinstance(achievements, str): - for item in achievements.split(","): - try: - completed_ids.add(int(item.strip())) - except ValueError: - continue - elif isinstance(achievements, list): - completed_ids = set(self._flatten_achievement_ids(achievements)) + self.calendar = CalendarService(context) + self.information = InformationService(context) + self.character = CharacterService(context) + self.faction = FactionService(context) + self.adventure = AdventureService(context) + self.arena = ArenaService(context) + self.achievement = AchievementService(context) + self.trade = TradeService(context) + self.content = ContentService(context) - menu_payload = await self._get_achievement_base_data( - "achievement_menus", - "jx3box_achievement_menus", - ) - point_payload = await self._get_achievement_base_data( - "achievement_points", - "jx3box_achievement_points", - ) - if not menu_payload or not point_payload: - return_data["msg"] = "基础资历数据获取失败" - return return_data - - menus = menu_payload.get("menus", {}) - points = point_payload.get("points", {}) - if not isinstance(menus, dict) or not isinstance(points, dict): - return_data["msg"] = "基础资历数据格式异常" - return return_data - - selected_menu_id, selected_title = ACHIEVEMENT_CHOICE_MAP[choice] - items = [] - - if choice == 0: - total_ids = [] - for _, menu_id, title in ACHIEVEMENT_CHOICES[1:]: - category = menus.get(str(menu_id), {}) - if not isinstance(category, dict): - continue - category_ids = self._category_achievement_ids(category) - total_ids.extend(category_ids) - items.append( - self._build_achievement_progress(title, category_ids, points, completed_ids) - ) - summary = self._build_achievement_progress(selected_title, total_ids, points, completed_ids) + if not token: + logger.warning( + "获取配置 token 失败,请正确填写 token,否则部分功能无法正常使用" + ) else: - category = menus.get(str(selected_menu_id), {}) - if not isinstance(category, dict) or not category: - return_data["msg"] = "未找到该资历分类" - return return_data - - category_ids = self._category_achievement_ids(category) - summary = self._build_achievement_progress(selected_title, category_ids, points, completed_ids) - for child in category.get("children", []) or []: - if not isinstance(child, dict): - continue - child_ids = self._flatten_achievement_ids(child.get("achievements", [])) - items.append( - self._build_achievement_progress(child.get("name", "未命名"), child_ids, points, completed_ids) - ) - - try: - return_data["temp"] = await load_template("zili.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["data"] = { - "title": selected_title, - "summary": summary, - "items": items, - "role_name": role_data.get("roleName", name), - "server": role_data.get("serverName", server), - "zone": role_data.get("zoneName", ""), - "force_name": role_data.get("forceName", "无"), - "camp_name": role_data.get("campName", "无"), - "tong_name": role_data.get("tongName", "无"), - "updated_at": achievement_data.get("updated_at", ""), - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - } - return_data["code"] = 200 - - return return_data - - - async def jueshe(self,name: str, server: str) -> Dict[str, Any]: - """角色""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "name": name, "token": self.token} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_jueshexinxi", "GET", params=params - ) - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - # 格式化字符串,利用字典的 get 方法提供默认值 - result_msg = ( - f"服务器:{data.get('zoneName', '无')}·{data.get('serverName', '无')}\n" - f"名称:{data.get('roleName', '无')}\n" - f"角色ID:{data.get('roleId', '无')}\n" - f"全区ID:{data.get('globalId', '无')}\n" - f"职业:{data.get('forceName', '无')}·{data.get('bodyName', '无')}\n" - f"帮会:{data.get('tongName', '无')}\n" - f"阵营:{data.get('campName', '无')}\n" + logger.debug("JX3API Token 已配置") + if not ticket: + logger.warning( + "获取配置 ticket 失败,请正确填写 ticket,否则部分功能无法正常使用" ) + else: + logger.debug("推栏 Ticket 已配置") - return_data["data"] = result_msg - return_data["code"] = 200 - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - return_data["code"] = 200 + async def start(self) -> None: + await self._api.start() - return return_data - - - async def jueshemingpian(self, server: str, name: str) -> Dict[str, Any]: - """单张名片""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "name": name, "token": self.token} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_jieshemingpian", "GET", params=params - ) - - if not data or not isinstance(data, dict): - return_data["msg"] = "未找到该角色名片" - return return_data - - # 3. 处理返回数据 - try: - url = data.get("showAvatar") - if not url: - return_data["msg"] = "未获取到名片图片" - return return_data - - server_name = data.get("serverName", server) - role_name = data.get("roleName", name) - show_like = data.get("showLike", 0) - msg = f"{server_name}-{role_name} 点赞:{show_like}" - - return_data["data"] = [ - Comp.Plain(msg), - Comp.Image.fromURL(url), - ] - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def jingnai(self, name: str, server: str) -> Dict[str, Any]: - """百战精耐""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "name": name, "token": self.token} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_jingnai", "GET", params=params - ) - - if not data or not isinstance(data, dict): - return_data["msg"] = "未查询到角色精耐信息" - return return_data - - # 3. 处理返回数据 - try: - items = data.get("skill_list", []) - if not isinstance(items, list) or not items: - return_data["msg"] = "未查询到技能信息" - return return_data - - color_map = { - 7: "黑色", - 6: "紫色", - 5: "红色", - 4: "绿色", - 3: "蓝色", - 2: "黄色", - 0: "无", - } - - for item in items: - if not isinstance(item, dict): - continue - - color = item.get("skill_color") - try: - color_key = int(color) - except (TypeError, ValueError): - color_key = None - item["skill_color_text"] = color_map.get(color_key, str(color) if color is not None else "") - - update_time = data.get("update_time") - if update_time: - try: - update_time = datetime.fromtimestamp(int(update_time)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - update_time = str(update_time) - else: - update_time = "" - - return_data["data"] = { - "server": data.get("server", server), - "role_name": data.get("role_name", name), - "skill_energy": data.get("skill_energy", ""), - "skill_stamina": data.get("skill_stamina", ""), - "skill_count": data.get("skill_count", len(items)), - "items": items, - "update_time": update_time - } - except Exception as e: - logger.error(f"处理精耐数据失败: {e}") - return_data["msg"] = "处理精耐数据失败" - return return_data - - # 4. 加载模板 - try: - return_data["temp"] = await load_template("jingnai.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def shuoyoumingpian(self, server: str, name: str) -> Dict[str, Any]: - """所有名片""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "name": name, "token": self.token} - - # 2. 调用基础请求 - data: Optional[list[Dict[str, Any]]] = await self._base_request( - "jx3_allmingpian", "GET", params=params - ) - - if not data: - return_data["msg"] = "未找到该角色" - return return_data - - # 3. 处理返回数据 - try: - chain = [] - for m in data: - status = "已启用" if m.get("showActive") else "未启用" - msg = f"第{m.get('showIndex')}张 {status}" - url = m.get("showAvatar") - - if not url: - logger.warning(f"第{m.get('showIndex')}张名片缺少图片URL,已跳过") - continue - - chain.extend([ - Comp.Plain(msg), - Comp.Image.fromURL(url), - ]) - - if not chain: - return_data["msg"] = "未获取到有效的名片数据" - return return_data - - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - return_data["data"] = chain - return_data["code"] = 200 - - return return_data - - - async def shuijimingpian(self, force: str, body:str, server:str) -> Dict[str, Any]: - """随机名片""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "body": body, "force":force, "token": self.token} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_shuijimingpian", "GET", params=params - ) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 (直接提取图片 URL) - return_data["data"] = data['showAvatar'] - - return_data["code"] = 200 - - return return_data - - - async def shuamamsg(self,server:str,type:str,subtype:str) -> Dict[str, Any]: - """刷马消息""" - return_data = self._init_return_data() - - params = {"server": server, "type": type, "subtype": subtype} - # 提取字段可能返回字典 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3box_shuamamsg", "GET", params=params) - - if not data: - return_data["msg"] = "获取接口信息失败或数据格式错误" - return return_data - - try: - # - new_msg = data.get("list",[])[0] - return_data["status"] = new_msg.get('id') - - result_msg = f"{server}\n" - result_msg += f"{new_msg.get('content')}\n" - result_msg += f"{new_msg.get('created_at')}\n" - - return_data["data"] = result_msg - except Exception as e: - logger.error(f"数据处理时出错: {e}") - return_data["msg"] = "处理接口返回信息时出错" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def shuma(self, server: str) -> Dict[str, Any]: - """刷马""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"server": server, "token": self.token} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3_shuama", "GET", params=params, out_key="" - ) - - if not data: - return_data["msg"] = "获取接口信息失败" - return return_data - - # 3. 处理返回数据 - try: - _data = data["data"]["data"] - result_msg = f"{server}\n" - result_msg += f"黑戈壁:\n{_data['黑戈壁'][0]}\n" - result_msg += f"阴山大草原:\n{_data['阴山大草原'][0]}\n" - result_msg += f"鲲鹏岛:\n{_data['鲲鹏岛'][0]}\n" - result_msg += f"\n{data['data']['note']}" - return_data["data"] = result_msg - except Exception as e: - logger.error(f"处理返回数据失败: {e}") - return_data["msg"] = "处理返回数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def jinjia(self, server: str, limit:str) -> Dict[str, Any]: - """区服金价""" - return_data = self._init_return_data() - - - params = {"server": server, "limit": limit, "token": self.token} - data_list: Optional[List[Dict[str, Any]]] = await self._base_request("jx3_jinjia", "GET", params=params) - - if not data_list or not isinstance(data_list, list): - return_data["msg"] = "获取接口信息失败或数据格式错误" - return return_data - - # 加载模板 - try: - return_data["temp"] = await load_template("jinjia.html") - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - # 准备模板渲染数据 - try: - return_data["data"]["items"] = data_list - - except Exception as e: - logger.error(f"模板数据准备失败: {e}") - return_data["msg"] = "系统错误:模板渲染数据准备失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def wujia(self, Name: str, server:str) -> Dict[str, Any]: - """物价查询""" - return_data = self._init_return_data() - - # 2. 确定外观名称和 ID - - params_search = {"name": Name,"token": self.token, "server": server} - search_data: Optional[Dict[str, Any]] = await self._base_request("jx3_wujia", "GET", params=params_search) - - if not search_data: - return_data["msg"] = "未找到该外观" - return return_data - - return_data["data"] = search_data - - # 5. 加载模板 - try: - return_data["temp"] = await load_template("wujia.html") - return_data["code"] = 200 - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return return_data - - - async def jiaoyihang(self, name: str , server: str) -> Dict[str, Any]: - """区服交易行""" - return_data = self._init_return_data() - - item_groups = await self._get_trade_item_groups() - if not item_groups: - return_data["msg"] = "交易行基础物品数据获取失败" - return return_data - - trade_items = self._flatten_trade_items(item_groups) - matched_items = self._match_trade_items(trade_items, name, 50) - if not matched_items: - return_data["msg"] = "未找到匹配的交易行物品" - return return_data - - item_map = {item["item_id"]: item for item in matched_items} - params = { - "item_ids": list(item_map.keys()), - "server": server, - "aggregate_type": "hourly", - } - price_data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_jiaoyihang", "POST", params=params, out_key="" - ) - - if not price_data or not isinstance(price_data, list): - return_data["msg"] = "未查询到交易行价格数据" - return return_data - - try: - result = [] - for price_item in price_data: - if not isinstance(price_item, dict): - continue - - item_id = str(price_item.get("item_id") or "") - base_item = item_map.get(item_id) - if not base_item: - continue - - timestamp = price_item.get("timestamp") - try: - created = datetime.fromtimestamp(int(timestamp)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - created = "" - - result.append( - { - "item_id": item_id, - "name": base_item.get("label", ""), - "icon": f"https://icon.jx3box.com/icon/{base_item.get('icon', '')}.png", - "server": price_item.get("server", server), - "price": price_item.get("price", 0), - "price_parts": gold_to_parts(price_item.get("price", 0)), - "sample": price_item.get("sample", 0), - "created": created, - } - ) - - if not result: - return_data["msg"] = "未查询到交易行价格数据" - return return_data - - return_data["data"] = { - "search_name": name, - "server": server, - "matched_count": len(matched_items), - "result_count": len(result), - "list": result, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - } - except Exception as e: - logger.error(f"处理交易行数据失败: {e}") - return_data["msg"] = "处理交易行数据失败" - return return_data - - # 5. 模板渲染 - try: - return_data["temp"] = await load_template("jiaoyihang.html") - return_data["code"] = 200 - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return return_data - - - async def tiebawujia(self, name: str, limit: int = 5, server: str = "") -> Dict[str, Any]: - """贴吧物价""" - return_data = self._init_return_data() - - try: - limit = int(limit) - except (TypeError, ValueError): - return_data["msg"] = "记录数量必须是数字" - return return_data - - if limit < 1 or limit > 50: - return_data["msg"] = "贴吧物价记录数量必须在 1-50 之间" - return return_data - - params = { - "server": server, - "name": name, - "limit": limit, - "token": self.token, - } - - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_tiebawujia", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到贴吧物价记录" - return return_data - - try: - lines = [ - f"贴吧物价:{name}", - f"服务器:{server}", - f"记录数:{len(data)}", - "", - ] - - for index, item in enumerate(data, start=1): - if not isinstance(item, dict): - continue - - item_time = item.get("time", "") - if item_time: - try: - item_time = datetime.fromtimestamp(int(item_time)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - item_time = str(item_time) - - lines.extend([ - f"{index}. {item.get('name', '')}", - f"区服:{item.get('zone', '')} 服务器:{item.get('server', '')}", - f"内容:{item.get('context', '')}", - f"回复:{item.get('reply', '')} 楼层:{item.get('floor', '')}", - f"时间:{item_time}", - f"链接:https://tieba.baidu.com/p/{item.get('url', '')}", - "", - ]) - - if len(lines) <= 4: - return_data["msg"] = "未查询到贴吧物价记录" - return return_data - - return_data["data"] = "\n".join(lines).rstrip() - except Exception as e: - logger.error(f"处理贴吧物价数据失败: {e}") - return_data["msg"] = "处理贴吧物价数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def diaoluo(self, name: str, limit: int = 20, server: str = "") -> Dict[str, Any]: - """物品掉落记录""" - return_data = self._init_return_data() - - try: - limit = int(limit) - except (TypeError, ValueError): - return_data["msg"] = "数量必须是数字" - return return_data - - if limit < 1 or limit > 100: - return_data["msg"] = "掉落记录数量需在 1-100 之间" - return return_data - - # 1. 构造请求参数 - params = {"server": server, "name": name, "limit": limit, "token": self.token} - - # 2. 调用基础请求 - data: Optional[List[Dict[str, Any]]] = await self._base_request( - "jx3_diaoluo", "GET", params=params - ) - - if not data or not isinstance(data, list): - return_data["msg"] = "未查询到掉落记录" - return return_data - - # 3. 处理返回数据 - try: - for item in data: - if not isinstance(item, dict): - continue - - drop_time = item.get("time") - if drop_time: - try: - item["time"] = datetime.fromtimestamp(int(drop_time)).strftime("%Y-%m-%d %H:%M:%S") - except (TypeError, ValueError, OSError): - item["time"] = str(drop_time) - else: - item["time"] = "" - - return_data["data"] = { - "items": data, - "name": name, - "limit": limit, - "server": server, - "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - except Exception as e: - logger.error(f"处理掉落数据失败: {e}") - return_data["msg"] = "处理掉落数据失败" - return return_data - - # 4. 模板渲染 - try: - return_data["temp"] = await load_template("diaoluo.html") - return_data["code"] = 200 - except FileNotFoundError as e: - logger.error(f"加载模板失败: {e}") - return_data["msg"] = "系统错误:模板文件不存在" - return return_data - - return return_data - - - async def bagua(self, type: str) -> Dict[str, Any]: - """八卦""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"class": type, "limit": "5", "token": self.token} - - # 2. 调用基础请求 - data: Optional[list[Dict[str, Any]]] = await self._base_request( - "jx3_bagua", "GET", params=params - ) - - # 3. 处理返回数据 - try: - if not data: - result_msg = f"未找到相关 {type} 记录。\n" - result_msg += f"可选范围:818 616 鬼网三 鬼网3 树洞 记录 教程 街拍 故事 避雷 吐槽 提问" - else: - result_msg = f"类型:【{type}】\n\n" - - for item in data: - result_msg += f"{item['title']}\n" - result_msg += f"分区:{item['zone']} 服务器:{item['server']}\n" - result_msg += f"所属吧:{item['name']}\n" - result_msg += f"链接:https://tieba.baidu.com/p/{item['url']}\n" - result_msg += f"日期:{item['date']}\n\n" - return_data["data"] = result_msg - except Exception as e: - logger.exception("处理返回数据失败") - return_data["msg"] = "处理返回数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def qiyugonglue(self, name: str) -> Dict[str, Any]: - """奇遇攻略""" - return_data = self._init_return_data() - - # 1. 构造请求参数 - params = {"name": name} - - # 2. 调用基础请求 - data: Optional[list[Dict[str, Any]]] = await self._base_request( - "jx3box_qiyugonglue", "GET", params=params, out_key="list" - ) - - if not data: - return_data["msg"] = "未找到该奇遇" - return return_data - - # 提取dwID - dwID = data[0]["dwID"] - url = f"https://node.jx3box.com/serendipity/{dwID}/achievement" - logger.debug(f"获取ID接口地址:{url}") - - data1 = await self._api.get(url) - url1 = f"https://cms.jx3box.com/api/cms/wiki/post/type/achievement/source/{data1['achievement_id']}" - logger.debug(f"获取攻略接口地址:{url1}") - - # 获取奇遇攻略 - data2 = await self._api.get(url1, out_key="data") - if not data2: - return_data["msg"] = "获取攻略数据异常" - return return_data - - # 4. 处理数据 - try: - return_data["data"] = {} - content = data2["post"]["content"] - return_data["temp"] = content - except Exception as e: - logger.exception("处理返回数据失败") - return_data["msg"] = "处理返回数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def hong1(self, name: str) -> Dict[str, Any]: - """宏 心法""" - return_data = self._init_return_data() - - # 数据库查询数据 - result = await self._sql_db.select_one( - "kungfu", - "name=? OR name1=? OR name2=? OR name3=? OR name4=? OR name5=?", - (name, name, name, name, name, name) - ) - - if result is None: - return_data["msg"] = "未找到该心法" - return return_data - - kungfu = result.get("name", None) - if kungfu is None: - return_data["msg"] = "未找到该心法" - return return_data - - logger.debug(f"查询到数据:{kungfu}") - - # 1. 构造请求参数 - params = {"subtype": kungfu} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3box_hong", "GET", params=params - ) - logger.debug(data) - if not data: - return_data["msg"] = "未找到该心法一键宏" - return return_data - - # 提取ID - pid_list = [0] - msg = "按照热度排列\n" - n = 1 - try: - for m in data.get('list',[]): - msg += f"{n}、{m['author']}\t{m['post_title']}\n" - pid_list.append(m["ID"]) - n += 1 - - return_data["msg"] = msg - return_data["data"]["list"] = pid_list - return_data["data"]["num"] = n - - except Exception as e: - logger.exception("处理返回数据失败") - return_data["msg"] = "处理返回数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def hong2(self, pid: str) -> Dict[str, Any]: - """宏 心法""" - return_data = self._init_return_data() - - # 发起请求 - url = f"https://cms.jx3box.com/api/cms/post/{pid}" - logger.debug(f"获取宏接口地址:{url}") - - data = await self._api.get(url, out_key="data") - # 验证数据 - if not data: - return_data["msg"] = "获取宏数据异常" - return return_data - - # 4. 处理数据 - try: - return_data["temp"] = data["post_content"] - msg = "" - for m in data["post_meta"]["data"]: - msg += f"【宏名称】\n{m['name']}\n" - msg += f"【使用说明】\n{m['desc']}\n" - msg += f"【宏脚本】\n{m['macro']}\n\n" - - return_data["data"] = msg - - except Exception as e: - logger.exception("处理返回数据失败") - return_data["msg"] = "处理返回数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - - async def peizhuang(self, name: str, tags: str) -> Dict[str, Any]: - """配装""" - return_data = self._init_return_data() - - # 数据库查询数据 - result = await self._sql_db.select_one( - "kungfu", - "name=? OR name1=? OR name2=? OR name3=? OR name4=? OR name5=?", - (name, name, name, name, name, name) - ) - logger.debug(result) - if result is None: - return_data["msg"] = "未找到该心法" - return return_data - - mount = result.get("pzid", None) - if not mount: - return_data["msg"] = "未找到该心法" - return return_data - - logger.debug(f"查询到数据:{mount}") - - # 1. 构造请求参数 - params = {"mount": mount, "tags": tags} - - # 2. 调用基础请求 - data: Optional[Dict[str, Any]] = await self._base_request( - "jx3box_peizhuang", "GET", params=params - ) - logger.debug(data) - # 验证数据 - if not data: - return_data["msg"] = "配装数据获取异常" - return return_data - - # 3. 处理返回数据 - try: - result_msg = f"{name}--配装\n" - for item in data["list"]: - result_msg += f"【{item['zlp']}】--{item['title']}\n" - result_msg += f"链接:https://www.jx3box.com/pz/view/{item['id']}\n\n" - - return_data["data"] = result_msg - - except Exception as e: - logger.exception("处理返回数据失败:",e) - return_data["msg"] = "处理返回数据失败" - return return_data - - return_data["code"] = 200 - - return return_data - - + async def close(self) -> None: + await self._api.close() diff --git a/core/message.py b/core/message.py index e12069a..732fbb2 100644 --- a/core/message.py +++ b/core/message.py @@ -234,7 +234,7 @@ class MessageBuilder: return try: - data = await self.jx3api.zili(name, server, choice) + data = await self.jx3api.achievement.zili(name, server, choice) if data["code"] != 200: await new_event.send(MessageChain().message(data.get("msg", "获取资历数据失败"))) controller.stop() @@ -270,110 +270,110 @@ class MessageBuilder: async def jx3_helps(self, event: AstrMessageEvent): """剑三 功能""" - return await self.T2I_image_msg(event, self.jx3api.helps) + return await self.T2I_image_msg(event, self.jx3api.content.helps) async def jx3_richang(self, event: AstrMessageEvent,server: str = "" ,num: int = 0): """剑三 日常 服务器 天数""" - return await self.plain_msg(event, lambda: self.jx3api.richang(self.serverdefault(server),num)) + return await self.plain_msg(event, lambda: self.jx3api.calendar.richang(self.serverdefault(server),num)) async def jx3_richangyuche(self, event: AstrMessageEvent): """剑三 日常预测""" - return await self.T2I_image_msg(event, self.jx3api.richangyuche) + return await self.T2I_image_msg(event, self.jx3api.calendar.richangyuche) async def jx3_xingxiashijian(self, event: AstrMessageEvent,name: str = "穹野卫"): """剑三 名望""" - return await self.T2I_image_msg(event, lambda: self.jx3api.xingxiashijian(name)) + return await self.T2I_image_msg(event, lambda: self.jx3api.calendar.xingxiashijian(name)) async def jx3_keju(self, event: AstrMessageEvent,subject: str, limit: int = 5): """剑三 科举""" - return await self.plain_msg(event, lambda: self.jx3api.keju(subject,limit)) + return await self.plain_msg(event, lambda: self.jx3api.information.keju(subject,limit)) async def jx3_huajia(self, event: AstrMessageEvent, name: str= "", server: str = "", map: str= ""): """剑三 花价 名称 服务器 地图""" - return await self.T2I_image_msg(event, lambda: self.jx3api.huajia(self.serverdefault(server),name,map)) + return await self.T2I_image_msg(event, lambda: self.jx3api.information.huajia(self.serverdefault(server),name,map)) async def jx3_zhuangshi(self, event: AstrMessageEvent, name: str): """剑三 装饰 名称""" - return await self.T2I_image_msg(event, lambda: self.jx3api.zhuangshi(name)) + return await self.T2I_image_msg(event, lambda: self.jx3api.information.zhuangshi(name)) async def jx3_qiwu(self, event: AstrMessageEvent, name: str): """剑三 器物 地图名称""" - return await self.T2I_image_msg(event, lambda: self.jx3api.qiwu(name)) + return await self.T2I_image_msg(event, lambda: self.jx3api.information.qiwu(name)) async def jx3_xinwen(self, event: AstrMessageEvent,num:int = 5): """剑三 新闻""" - return await self.plain_msg(event, lambda: self.jx3api.xinwen(num)) + return await self.plain_msg(event, lambda: self.jx3api.information.xinwen(num)) async def jx3_weihu(self, event: AstrMessageEvent,num:int = 5): """剑三 维护""" - return await self.plain_msg(event, lambda: self.jx3api.weihu(num)) + return await self.plain_msg(event, lambda: self.jx3api.information.weihu(num)) async def jx3_qufu(self, event: AstrMessageEvent,name: str = "双梦镇"): """剑三 区服""" - return await self.plain_msg(event, lambda: self.jx3api.qufu(name)) + return await self.plain_msg(event, lambda: self.jx3api.information.qufu(name)) async def jx3_kaifu(self, event: AstrMessageEvent,server: str = ""): """剑三 开服 服务器""" - return await self.plain_msg(event, lambda: self.jx3api.kaifu(self.serverdefault(server))) + return await self.plain_msg(event, lambda: self.jx3api.information.kaifu(self.serverdefault(server))) async def jx3_zhuangtai(self, event: AstrMessageEvent): """剑三 状态""" - return await self.T2I_image_msg(event, self.jx3api.zhuangtai) + return await self.T2I_image_msg(event, self.jx3api.information.zhuangtai) async def jx3_jigai(self, event: AstrMessageEvent,): """剑三 技改""" - return await self.plain_msg(event, self.jx3api.jigai) + return await self.plain_msg(event, self.jx3api.information.jigai) async def jx3_xiaoyao(self, event: AstrMessageEvent): """剑三 小药""" - return await self.T2I_image_msg(event, self.jx3api.xiaoyao) + return await self.T2I_image_msg(event, self.jx3api.information.xiaoyao) async def jx3_zhenyan(self, event: AstrMessageEvent, name: str): """剑三 阵眼 心法""" - return await self.plain_msg(event, lambda: self.jx3api.zhenyan(name)) + return await self.plain_msg(event, lambda: self.jx3api.character.zhenyan(name)) async def jx3_qixue(self, event: AstrMessageEvent, name: str): """剑三 奇穴 心法""" - return await self.T2I_image_msg(event, lambda: self.jx3api.qixue(name)) + return await self.T2I_image_msg(event, lambda: self.jx3api.character.qixue(name)) async def jx3_jineng(self, event: AstrMessageEvent, name: str): """剑三 技能 心法""" - return await self.T2I_image_msg(event, lambda: self.jx3api.jineng(name)) + return await self.T2I_image_msg(event, lambda: self.jx3api.character.jineng(name)) async def jx3_zilipaixing(self, event: AstrMessageEvent, school: str, server: str = ""): """剑三 资历排行 职业 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.zilipaixing(school, self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.faction.zilipaixing(school, self.serverdefault(server))) async def jx3_zilipaihang(self, event: AstrMessageEvent): """剑三 资历排行 """ - return await self.T2I_image_msg(event, lambda: self.jx3api.zilipaixing("", "")) + return await self.T2I_image_msg(event, lambda: self.jx3api.faction.zilipaixing("", "")) async def jx3_shilianpaixing(self, event: AstrMessageEvent, name: str, server: str = ""): """剑三 试炼排行 心法 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.shilianpaixing(name, self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.faction.shilianpaixing(name, self.serverdefault(server))) async def jx3_shaohua(self, event: AstrMessageEvent,): """剑三 骚话""" - return await self.plain_msg(event, self.jx3api.shaohua) + return await self.plain_msg(event, self.jx3api.calendar.shaohua) async def jx3_zili(self, event: AstrMessageEvent, name: str, server: str = ""): @@ -383,97 +383,103 @@ class MessageBuilder: async def jx3_jiemi(self, event: AstrMessageEvent): """剑三 解密""" - return await self.plain_msg(event, self.jx3api.jiemi) + return await self.plain_msg(event, self.jx3api.calendar.jiemi) async def jx3_shapan(self, event: AstrMessageEvent,server: str = ""): """剑三 沙盘 服务器""" - return await self.image_msg(event, lambda: self.jx3api.shapan(self.serverdefault(server))) + return await self.image_msg(event, lambda: self.jx3api.faction.shapan(self.serverdefault(server))) async def jx3_baizhan(self, event: AstrMessageEvent): """剑三 百战""" - return await self.T2I_image_msg(event, self.jx3api.baizhan) + return await self.T2I_image_msg(event, self.jx3api.calendar.baizhan) async def jx3_fuyaojjiutian(self, event: AstrMessageEvent,server: str = ""): """剑三 扶摇九天 服务器""" - return await self.plain_msg(event, lambda: self.jx3api.fuyaojjiutian( self.serverdefault(server))) + return await self.plain_msg(event, lambda: self.jx3api.calendar.fuyaojjiutian( self.serverdefault(server))) async def jx3_zhueevent(self, event: AstrMessageEvent): """剑三 诛恶事件""" - return await self.T2I_image_msg(event, self.jx3api.zhueevent) + return await self.T2I_image_msg(event, self.jx3api.calendar.zhueevent) async def jx3_benrichitu(self, event: AstrMessageEvent): """剑三 本日赤兔""" - return await self.plain_msg(event, self.jx3api.benrichitu) + return await self.plain_msg(event, self.jx3api.calendar.benrichitu) async def jx3_benzhouchitu(self, event: AstrMessageEvent): """剑三 本周赤兔""" - return await self.plain_msg(event, self.jx3api.benzhouchitu) + return await self.plain_msg(event, self.jx3api.calendar.benzhouchitu) async def jx3_zhengyingpaimai(self, event: AstrMessageEvent,name: str = "玄晶", server: str = ""): """剑三 阵营拍卖 物品名称 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.zhengyingpaimai( self.serverdefault(server), name)) + return await self.T2I_image_msg(event, lambda: self.jx3api.faction.zhengyingpaimai( self.serverdefault(server), name)) async def jx3_zhenyingevent(self, event: AstrMessageEvent): """剑三 阵营事件""" - return await self.T2I_image_msg(event, self.jx3api.zhenyingevent) + return await self.T2I_image_msg(event, self.jx3api.calendar.zhenyingevent) async def jx3_guanaishouling(self, event: AstrMessageEvent): """剑三 关隘首领""" - return await self.T2I_image_msg(event, self.jx3api.guanaishouling) + return await self.T2I_image_msg(event, self.jx3api.calendar.guanaishouling) async def jx3_bangzhanjilu(self, event: AstrMessageEvent, server: str = ""): """剑三 帮战记录 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.bangzhanjilu(self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.faction.bangzhanjilu(self.serverdefault(server))) async def jx3_tongzhanyy(self, event: AstrMessageEvent, server: str = ""): """剑三 统战歪歪 服务器""" - return await self.plain_msg(event, lambda: self.jx3api.tongzhanyy(self.serverdefault(server))) + return await self.plain_msg(event, lambda: self.jx3api.faction.tongzhanyy(self.serverdefault(server))) async def jx3_dilujilu(self, event: AstrMessageEvent,server: str = ""): """剑三 的卢 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.dilujilu( self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.faction.dilujilu( self.serverdefault(server))) async def jx3_yanhuachaxun(self, event: AstrMessageEvent,name: str = "飞翔大野猪", server: str = ""): """剑三 烟花 角色 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.yanhuachaxun( self.serverdefault(server),name)) + return await self.T2I_image_msg(event, lambda: self.jx3api.adventure.yanhuachaxun( self.serverdefault(server),name)) async def jx3_pianzhi(self, event: AstrMessageEvent,qq: str): """剑三 骗子 QQ""" - return await self.plain_msg(event, lambda: self.jx3api.pianzhi(qq)) + return await self.plain_msg(event, lambda: self.jx3api.adventure.pianzhi(qq)) async def jx3_juesheqiyu(self, event: AstrMessageEvent,name: str = "飞翔大野猪", server: str = ""): """剑三 奇遇 角色名称 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.juesheqiyu(name, self.serverdefault(server))) + return await self.T2I_image_msg( + event, + lambda: self.jx3api.adventure.juesheqiyu( + name, + self.serverdefault(server), + ), + ) async def jx3_weizuoqiyu(self, event: AstrMessageEvent,name: str = "飞翔大野猪", server: str = ""): """剑三 未做奇遇 角色名称 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.weizuoqiyu(name, self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.adventure.weizuoqiyu(name, self.serverdefault(server))) async def jx3_qiyutongji(self, event: AstrMessageEvent,adventureName: str = "阴阳两界", server: str = ""): """剑三 奇遇统计 奇遇名称 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.qiyutongji(adventureName,self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.adventure.qiyutongji(adventureName,self.serverdefault(server))) async def jx3_jinqiqiyu(self, event: AstrMessageEvent,server: str = ""): """剑三 近期奇遇 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.jinqiqiyu(self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.adventure.jinqiqiyu(self.serverdefault(server))) async def jx3_qiyuhuizong(self, event: AstrMessageEvent,num_or_server: str = "7", server: str = ""): @@ -486,42 +492,42 @@ class MessageBuilder: else: target_server = num_or_server - return await self.T2I_image_msg(event, lambda: self.jx3api.qiyuhuizong(self.serverdefault(target_server), num)) + return await self.T2I_image_msg(event, lambda: self.jx3api.adventure.qiyuhuizong(self.serverdefault(target_server), num)) async def jx3_zhanji(self, event: AstrMessageEvent,name: str = "飞翔大野猪", server: str = "", mode:str = "33"): """剑三 战绩 角色 服务器 类型""" - return await self.T2I_image_msg(event, lambda: self.jx3api.zhanji(name, self.serverdefault(server),mode)) + return await self.T2I_image_msg(event, lambda: self.jx3api.arena.zhanji(name, self.serverdefault(server),mode)) async def jx3_mingjianpaihang(self, event: AstrMessageEvent,limit: str = "50", mode:str = "33"): """剑三 排行 数量 类型""" - return await self.T2I_image_msg(event, lambda: self.jx3api.mingjianpaihang(limit,mode)) + return await self.T2I_image_msg(event, lambda: self.jx3api.arena.mingjianpaihang(limit,mode)) async def jx3_mingjiantongji(self, event: AstrMessageEvent,mode: str = "33"): """剑三 名剑统计 模式""" - return await self.T2I_image_msg(event, lambda: self.jx3api.mingjiantongji(mode)) + return await self.T2I_image_msg(event, lambda: self.jx3api.arena.mingjiantongji(mode)) async def jx3_tuanduizhaomu(self, event: AstrMessageEvent,keyword: str = "25人普通会战弓月城", server: str = ""): """剑三 招募 副本 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.tuanduizhaomu( self.serverdefault(server),keyword)) + return await self.T2I_image_msg(event, lambda: self.jx3api.arena.tuanduizhaomu( self.serverdefault(server),keyword)) async def jx3_baishi(self, event: AstrMessageEvent,keyword: str = "", server: str = ""): """剑三 拜师 搜索关键词 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.shitu(2, keyword, self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.arena.shitu(2, keyword, self.serverdefault(server))) async def jx3_shoutu(self, event: AstrMessageEvent,keyword: str = "", server: str = ""): """剑三 收徒 搜索关键词 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.shitu(1, keyword, self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.arena.shitu(1, keyword, self.serverdefault(server))) async def jx3_rank_statistical(self, event: AstrMessageEvent, rank_name: str, server: str = ""): """剑三 各类榜单 榜单名称 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical(rank_name, self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.faction.rank_statistical(rank_name, self.serverdefault(server))) async def jx3_mingshiwushiqiang(self, event: AstrMessageEvent, server: str = ""): @@ -611,47 +617,52 @@ class MessageBuilder: async def jx3_jueshe(self, event: AstrMessageEvent,name: str, server: str = ""): """剑三 角色 名称 服务器""" - return await self.plain_msg(event, lambda: self.jx3api.jueshe(name, self.serverdefault(server))) + return await self.plain_msg(event, lambda: self.jx3api.character.jueshe(name, self.serverdefault(server))) async def jx3_jueshemingpian(self, event: AstrMessageEvent, name: str = "飞翔大野猪", server: str = ""): """剑三 名片 角色 服务器""" - return await self.plain_chain(event, lambda: self.jx3api.jueshemingpian(self.serverdefault(server), name)) + return await self.plain_chain(event, lambda: self.jx3api.character.jueshemingpian(self.serverdefault(server), name)) async def jx3_jingnai(self, event: AstrMessageEvent, name: str = "飞翔大野猪", server: str = ""): """剑三 精耐 角色 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.jingnai(name, self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.character.jingnai(name, self.serverdefault(server))) async def jx3_shuoyoumingpian(self, event: AstrMessageEvent, name: str = "飞翔大野猪", server: str = ""): """剑三 所有名片 角色 服务器""" - return await self.plain_chain(event, lambda: self.jx3api.shuoyoumingpian( self.serverdefault(server),name)) + return await self.plain_chain( + event, + lambda: self.jx3api.character.shuoyoumingpian(self.serverdefault(server), name), + ) async def jx3_shuijimingpian(self, event: AstrMessageEvent,force: str = "万花", body: str = "萝莉", server: str = ""): """剑三 随机名片 职业 体型 服务器""" - return await self.image_msg(event, lambda: self.jx3api.shuijimingpian(force,body, self.serverdefault(server))) + return await self.image_msg(event, lambda: self.jx3api.character.shuijimingpian(force,body, self.serverdefault(server))) async def jx3_shuma(self, event: AstrMessageEvent,server: str = ""): """剑三 刷马 服务器""" - return await self.plain_msg(event, lambda: self.jx3api.shuma( self.serverdefault(server))) + return await self.plain_msg(event, lambda: self.jx3api.calendar.shuma( self.serverdefault(server))) async def jx3_jinjia(self, event: AstrMessageEvent,server: str = "", limit:str = "15"): """剑三 金价 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.jinjia( self.serverdefault(server),limit)) + return await self.T2I_image_msg(event, lambda: self.jx3api.trade.jinjia( self.serverdefault(server),limit)) async def jx3_wujia(self, event: AstrMessageEvent,Name: str = "秃盒", server: str = ""): """剑三 物价 外观名称""" - return await self.T2I_image_msg(event, lambda: self.jx3api.wujia(Name, self.serverdefault(server))) + return await self.T2I_image_msg( + event, lambda: self.jx3api.trade.wujia(Name, self.serverdefault(server)) + ) async def jx3_jiaoyihang(self, event: AstrMessageEvent,Name: str = "守缺式",server: str = ""): """剑三 交易行 物品名称 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.jiaoyihang(Name, self.serverdefault(server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.trade.jiaoyihang(Name, self.serverdefault(server))) async def jx3_tiebawujia(self, event: AstrMessageEvent, name: str = "狐金", limit_or_server: str = "5", server: str = ""): @@ -664,7 +675,7 @@ class MessageBuilder: else: target_server = limit_or_server - return await self.plain_msg(event, lambda: self.jx3api.tiebawujia(name, limit, self.serverdefault(target_server))) + return await self.plain_msg(event, lambda: self.jx3api.trade.tiebawujia(name, limit, self.serverdefault(target_server))) async def jx3_diaoluo(self, event: AstrMessageEvent, name: str = "玄晶", limit_or_server: str = "20", server: str = ""): @@ -677,27 +688,27 @@ class MessageBuilder: else: target_server = limit_or_server - return await self.T2I_image_msg(event, lambda: self.jx3api.diaoluo(name, limit, self.serverdefault(target_server))) + return await self.T2I_image_msg(event, lambda: self.jx3api.trade.diaoluo(name, limit, self.serverdefault(target_server))) async def jx3_bagua(self, event: AstrMessageEvent,name: str = "818"): """剑三 八卦 类型""" - return await self.plain_msg(event, lambda: self.jx3api.bagua(name)) + return await self.plain_msg(event, lambda: self.jx3api.content.bagua(name)) async def jx3_qiyugonglue(self, event: AstrMessageEvent,name: str): """剑三 奇遇攻略 奇遇名称""" - return await self.T2I_image_msg(event, lambda: self.jx3api.qiyugonglue(name)) + return await self.T2I_image_msg(event, lambda: self.jx3api.adventure.qiyugonglue(name)) async def jx3_hong(self, event: AstrMessageEvent,name: str = "易筋经"): """剑三 宏 心法""" - return await self.handler_plain_image_msg(event, lambda: self.jx3api.hong1(name), self.jx3api.hong2) + return await self.handler_plain_image_msg(event, lambda: self.jx3api.content.hong1(name), self.jx3api.content.hong2) async def jx3_peizhuang(self, event: AstrMessageEvent,name: str = "易筋经", tags: str = ""): """剑三 配装 心法""" - return await self.plain_msg(event, lambda: self.jx3api.peizhuang( name,tags)) + return await self.plain_msg(event, lambda: self.jx3api.content.peizhuang( name,tags)) async def bilei_add(self, event: AstrMessageEvent,name: str, text: str): diff --git a/core/request.py b/core/request.py index e204057..158455c 100644 --- a/core/request.py +++ b/core/request.py @@ -1,191 +1,430 @@ -# core/request.py -import json -import aiohttp import asyncio -from typing import Optional, Dict, Any, Union, List -from aiohttp import ClientTimeout, ClientSession +import json +import random +from collections.abc import Mapping +from email.utils import parsedate_to_datetime +from time import monotonic, time +from typing import Any +from urllib.parse import urlsplit, urlunsplit + +import aiohttp +from aiohttp import ClientSession, ClientTimeout from astrbot.api import logger -class APIClient: - """ - API客户端类 - - 优化说明: - 1. 复用 aiohttp.ClientSession 以提高性能。 - 2. 增加类型提示 (Type Hints)。 - 3. 支持异步上下文管理器 (Async Context Manager)。 - """ +SENSITIVE_KEYS = { + "access_token", + "api_key", + "authorization", + "cookie", + "jx3api_ticket", + "jx3api_token", + "password", + "refresh_token", + "secret", + "ticket", + "token", +} - def __init__(self, base_timeout: int = 10, ssl_verify: bool = False): - self.base_timeout = base_timeout +RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504} + + +class APIClientError(Exception): + """HTTP 客户端的结构化异常。""" + + def __init__( + self, + message: str, + *, + method: str = "", + url: str = "", + status: int | None = None, + retryable: bool = False, + ): + super().__init__(message) + self.method = method + self.url = url + self.status = status + self.retryable = retryable + + +class APIResponseError(APIClientError): + """服务端返回了错误状态或无法解析的响应。""" + + +class APIResponseTooLargeError(APIResponseError): + """响应体超过客户端允许的大小。""" + + +class APIResponseFormatError(APIResponseError): + """响应结构与调用方约定不一致。""" + + +def redact_mapping(data: Mapping[str, Any] | None) -> dict[str, Any] | None: + """递归清理日志中的敏感字段。""" + if data is None: + return None + + redacted: dict[str, Any] = {} + for key, value in data.items(): + key_text = str(key) + if key_text.lower() in SENSITIVE_KEYS: + redacted[key_text] = "***" + elif isinstance(value, Mapping): + redacted[key_text] = redact_mapping(value) + elif isinstance(value, list): + redacted[key_text] = [ + redact_mapping(item) if isinstance(item, Mapping) else item + for item in value + ] + else: + redacted[key_text] = value + return redacted + + +def safe_url(url: str) -> str: + """移除 URL 中的用户名、密码、查询参数和片段,仅用于日志。""" + parsed = urlsplit(url) + host = parsed.hostname or "" + if parsed.port: + host = f"{host}:{parsed.port}" + return urlunsplit((parsed.scheme, host, parsed.path, "", "")) + + +class APIClient: + """可复用、带安全与可靠性保护的 aiohttp 客户端。""" + + def __init__( + self, + base_timeout: float = 15, + *, + connect_timeout: float = 5, + read_timeout: float = 10, + ssl_verify: bool = True, + max_retries: int = 2, + max_response_bytes: int = 10 * 1024 * 1024, + max_binary_response_bytes: int = 20 * 1024 * 1024, + connection_limit: int = 50, + connection_limit_per_host: int = 10, + log_payloads: bool = False, + ): + if base_timeout <= 0 or connect_timeout <= 0 or read_timeout <= 0: + raise ValueError("请求超时时间必须大于 0") + if max_retries < 0: + raise ValueError("max_retries 不能小于 0") + if max_response_bytes <= 0 or max_binary_response_bytes <= 0: + raise ValueError("响应大小限制必须大于 0") + + self.timeout = ClientTimeout( + total=base_timeout, + connect=connect_timeout, + sock_connect=connect_timeout, + sock_read=read_timeout, + ) self.ssl_verify = ssl_verify - self._session: Optional[ClientSession] = None + self.max_retries = max_retries + self.max_response_bytes = max_response_bytes + self.max_binary_response_bytes = max_binary_response_bytes + self.connection_limit = connection_limit + self.connection_limit_per_host = connection_limit_per_host + self.log_payloads = log_payloads + + self._session: ClientSession | None = None + self._session_lock = asyncio.Lock() + + async def start(self) -> None: + """显式初始化连接池;重复调用是安全的。""" + await self.get_session() async def get_session(self) -> ClientSession: - """获取或创建单例 Session""" - if self._session is None or self._session.closed: - timeout = ClientTimeout(total=self.base_timeout) - self._session = ClientSession(timeout=timeout) - return self._session + """获取或并发安全地创建共享 Session。""" + if self._session is not None and not self._session.closed: + return self._session - async def close(self): - """关闭 Session""" - if self._session and not self._session.closed: - await self._session.close() + async with self._session_lock: + if self._session is None or self._session.closed: + connector = aiohttp.TCPConnector( + ssl=self.ssl_verify, + limit=self.connection_limit, + limit_per_host=self.connection_limit_per_host, + ttl_dns_cache=300, + ) + self._session = ClientSession( + timeout=self.timeout, + connector=connector, + trust_env=True, + ) + return self._session + + async def close(self) -> None: + """关闭连接池;重复调用是安全的。""" + async with self._session_lock: + session = self._session self._session = None + if session is not None and not session.closed: + await session.close() + async def __aenter__(self): - await self.get_session() + await self.start() return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self.close() - async def _request(self, method: str, url: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Any: - """ - 统一的内部请求处理方法 - """ - session = await self.get_session() + async def _request( + self, + method: str, + url: str, + *, + params: Mapping[str, Any] | None = None, + json_data: Any = None, + form_data: Any = None, + headers: Mapping[str, str] | None = None, + timeout: ClientTimeout | None = None, + retry: bool | None = None, + ) -> Any: method = method.upper() - - # 记录日志 - logger.debug(f"发起 {method} 请求: {url}") - if params: logger.debug(f"Query参数: {params}") - if json_data: logger.debug(f"Body数据: {json_data}") + request_url = safe_url(url) + started_at = monotonic() + retry_enabled = method in {"GET", "HEAD", "OPTIONS"} if retry is None else retry + retry_count = self.max_retries if retry_enabled else 0 - try: - # aiohttp 会自动处理 json=json_data 时的 Content-Type - async with session.request( - method=method, - url=url, - params=params, - json=json_data, - ssl=self.ssl_verify - ) as response: - return await self._handle_response(response) - - except aiohttp.ClientError as e: - logger.error(f"网络请求出错 ({method} {url}): {e}") - return None - except Exception as e: - logger.error(f"未知错误 ({method} {url}): {e}") - return None + if self.log_payloads: + params_log: Any = redact_mapping(params) + json_log: Any = ( + redact_mapping(json_data) + if isinstance(json_data, Mapping) + else "" + if json_data is not None + else None + ) + else: + params_log = sorted(str(key) for key in (params or {})) + json_log = ( + sorted(str(key) for key in json_data) + if isinstance(json_data, Mapping) + else "" + if json_data is not None + else None + ) + logger.debug( + f"发起 {method} 请求: {request_url}, " + f"param_keys={params_log}, json_keys={json_log}" + ) - async def _handle_response(self, response: aiohttp.ClientResponse) -> Any: - """处理响应:自动识别二进制或JSON""" - try: - logger.debug(f"响应状态: {response.status}") - response.raise_for_status() - - content_type = response.headers.get('Content-Type', '').lower() - - if 'image' in content_type or 'octet-stream' in content_type: - return await response.read() + retryable_exceptions = ( + aiohttp.ClientConnectionError, + aiohttp.ServerTimeoutError, + asyncio.TimeoutError, + ) + for attempt in range(retry_count + 1): + session = await self.get_session() try: - data = await response.json() - except Exception: - text = await response.text() + async with session.request( + method=method, + url=url, + params=params, + json=json_data, + data=form_data, + headers=headers, + timeout=timeout, + ) as response: + if ( + response.status in RETRYABLE_STATUS_CODES + and attempt < retry_count + ): + delay = self._retry_delay(attempt, response) + logger.warning( + f"{method} {request_url} 返回 HTTP {response.status}," + f"{delay:.2f}s 后重试 ({attempt + 1}/{retry_count})" + ) + response.release() + await asyncio.sleep(delay) + continue + + result = await self._handle_response(response, method, request_url) + elapsed_ms = (monotonic() - started_at) * 1000 + logger.debug( + f"{method} {request_url} 请求完成: " + f"status={response.status}, elapsed={elapsed_ms:.0f}ms" + ) + return result + + except asyncio.CancelledError: + raise + except retryable_exceptions as exc: + if attempt < retry_count: + delay = self._retry_delay(attempt) + logger.warning( + f"{method} {request_url} 网络异常,{delay:.2f}s 后重试 " + f"({attempt + 1}/{retry_count}): {type(exc).__name__}" + ) + await asyncio.sleep(delay) + continue + raise APIClientError( + f"网络请求失败: {type(exc).__name__}", + method=method, + url=request_url, + retryable=True, + ) from exc + except APIClientError: + raise + except aiohttp.ClientError as exc: + raise APIClientError( + f"HTTP 客户端错误: {type(exc).__name__}", + method=method, + url=request_url, + retryable=False, + ) from exc + + raise APIClientError( + "请求在重试后仍然失败", + method=method, + url=request_url, + retryable=True, + ) + + def _retry_delay( + self, + attempt: int, + response: aiohttp.ClientResponse | None = None, + ) -> float: + if response is not None: + retry_after = response.headers.get("Retry-After") + if retry_after: try: - loop = asyncio.get_running_loop() - data = await loop.run_in_executor(None, json.loads, text) - except json.JSONDecodeError: - logger.error(f"无法解析响应为 JSON。原始内容: {text[:100]}...") - return None + return min(max(float(retry_after), 0.0), 60.0) + except ValueError: + try: + retry_at = parsedate_to_datetime(retry_after) + seconds = retry_at.timestamp() - time() + return min(max(seconds, 0.0), 60.0) + except (TypeError, ValueError, OverflowError): + pass - logger.debug(f"响应数据: {data}") - return self._validate_api_payload(data) + base = min(0.5 * (2**attempt), 8.0) + return base + random.uniform(0, base * 0.25) - except aiohttp.ClientError as e: - logger.error(f"HTTP响应错误: {e}") + async def _handle_response( + self, + response: aiohttp.ClientResponse, + method: str, + request_url: str, + ) -> Any: + content_type = response.headers.get("Content-Type", "").lower() + is_binary = "image/" in content_type or "octet-stream" in content_type + size_limit = ( + self.max_binary_response_bytes if is_binary else self.max_response_bytes + ) + + if response.status >= 400: + await self._read_limited(response, self.max_response_bytes) + raise APIResponseError( + f"HTTP {response.status}", + method=method, + url=request_url, + status=response.status, + retryable=response.status in RETRYABLE_STATUS_CODES, + ) + + body = await self._read_limited(response, size_limit) + if is_binary: + return body + + if not body: return None - def _validate_api_payload(self, data: Any) -> Any: - """校验业务层面的 JSON 数据结构""" - if not data: - logger.error("API返回空数据") - return None + encoding = response.charset or "utf-8" + try: + text = body.decode(encoding) + except (LookupError, UnicodeDecodeError) as exc: + raise APIResponseFormatError( + f"响应字符编码无效: {encoding}", + method=method, + url=request_url, + status=response.status, + ) from exc - # 如果返回的是 JSON 字符串而非对象,再次解析 - if isinstance(data, str): - try: - data = json.loads(data) - except json.JSONDecodeError: - return None - - if isinstance(data, dict) and 'code' in data: - # 兼容多种成功状态码:200, "0", 0, 1 - code = data.get('code') - if code not in [200, "0", 0, 1]: - msg = data.get('msg') or data.get('message', '未知错误') - logger.error(f"API业务报错: code={code}, msg={msg}") - return None - + try: + return json.loads(text) + except json.JSONDecodeError as exc: + raise APIResponseFormatError( + "响应不是合法 JSON", + method=method, + url=request_url, + status=response.status, + ) from exc + + async def _read_limited( + self, + response: aiohttp.ClientResponse, + limit: int, + ) -> bytes: + content_length = response.content_length + if content_length is not None and content_length > limit: + raise APIResponseTooLargeError( + f"响应体过大: {content_length} bytes,限制为 {limit} bytes", + method=response.method, + url=safe_url(str(response.url)), + status=response.status, + ) + + chunks: list[bytes] = [] + received = 0 + async for chunk in response.content.iter_chunked(64 * 1024): + received += len(chunk) + if received > limit: + raise APIResponseTooLargeError( + f"响应体超过限制: {limit} bytes", + method=response.method, + url=safe_url(str(response.url)), + status=response.status, + ) + chunks.append(chunk) + return b"".join(chunks) + + async def get( + self, + url: str, + params: Mapping[str, Any] | None = None, + *, + headers: Mapping[str, str] | None = None, + timeout: ClientTimeout | None = None, + retry: bool = True, + ) -> Any: + data = await self._request( + "GET", + url, + params=params, + headers=headers, + timeout=timeout, + retry=retry, + ) return data - async def get(self, url: str, params: Optional[Dict] = None, out_key: Optional[str] = None) -> Any: - """GET 请求封装""" - data = await self._request('GET', url, params=params) - return self._extract_data(data, out_key) - - async def post(self, url: str, data: Optional[Dict] = None, out_key: Optional[str] = None) -> Any: - """POST 请求封装 (默认发送 JSON)""" - data = await self._request('POST', url, json_data=data) - return self._extract_data(data, out_key) - - def _extract_data(self, data: Any, key: Optional[str]) -> Any: - """辅助方法:从结果中提取指定字段""" - if data is None: - return None - if isinstance(data, bytes): - return data - if key and isinstance(data, dict): - return data.get(key, {}) - return data - - async def all_pages( - self, - method: str, - url: str, - params_data: Optional[Dict] = None, - out_key: str = "", - list_key: str = "list", - max_pages: int = 10 - ) -> List[Any]: - """ - 分页获取所有数据 - :param method: GET 或 POST - :param list_key: 列表数据在 JSON 中的字段名,如 'data' 或 'list' - """ - all_data = [] - current_page = 1 - params = params_data.copy() if params_data else {} - - while True: - params["page"] = str(current_page) - - if method.upper() == "POST": - data = await self.post(url, data=params, out_key=out_key) - else: - data = await self.get(url, params=params, out_key=out_key) - - # 终止条件判断 - if not data or isinstance(data, bytes): - break - - # 如果 data 是列表本身(有些API直接返回列表) - page_items = data if isinstance(data, list) else data.get(list_key) - - if not page_items: - break - - all_data.extend(page_items) - - if current_page >= max_pages: - break - - current_page += 1 - logger.info(f"已获取第 {current_page} 页数据") - - return all_data \ No newline at end of file + async def post( + self, + url: str, + data: Any = None, + *, + params: Mapping[str, Any] | None = None, + form_data: Any = None, + headers: Mapping[str, str] | None = None, + timeout: ClientTimeout | None = None, + retry: bool = False, + ) -> Any: + if data is not None and form_data is not None: + raise ValueError("JSON data 和 form_data 不能同时传入") + response_data = await self._request( + "POST", + url, + params=params, + json_data=data, + form_data=form_data, + headers=headers, + timeout=timeout, + retry=retry, + ) + return response_data diff --git a/core/services/__init__.py b/core/services/__init__.py new file mode 100644 index 0000000..5a9bddc --- /dev/null +++ b/core/services/__init__.py @@ -0,0 +1,21 @@ +from .achievement import AchievementService +from .adventure import AdventureService +from .arena import ArenaService +from .calendar import CalendarService +from .character import CharacterService +from .content import ContentService +from .faction import FactionService +from .information import InformationService +from .trade import TradeService + +__all__ = [ + "AchievementService", + "AdventureService", + "ArenaService", + "CalendarService", + "CharacterService", + "ContentService", + "FactionService", + "InformationService", + "TradeService", +] diff --git a/core/services/achievement.py b/core/services/achievement.py new file mode 100644 index 0000000..26724a5 --- /dev/null +++ b/core/services/achievement.py @@ -0,0 +1,240 @@ +from datetime import datetime +from typing import Any, Dict, Optional + +from astrbot.api import logger + +from .base import BaseDomainService + +ACHIEVEMENT_CHOICES = [ + (0, None, "资历总览"), + (1, "1", "杂闻总览"), + (2, "2", "武学总览"), + (3, "3", "修为总览"), + (4, "4", "装备总览"), + (5, "5", "技艺总览"), + (6, "6", "阅读总览"), + (7, "7", "任务总览"), + (8, "8", "足迹总览"), + (9, "9", "战斗总览"), + (10, "10", "声望总览"), + (11, "11", "秘境总览"), + (12, "12", "帮会总览"), + (13, "13", "阵营总览"), + (14, "15", "节日总览"), + (15, "16", "活动总览"), + (16, "17", "风雨江湖路总览"), + (17, "40", "家园总览"), + (18, "41", "剑侠录总览"), +] + + +ACHIEVEMENT_CHOICE_MAP = { + index: (menu_id, title) for index, menu_id, title in ACHIEVEMENT_CHOICES +} + + +class AchievementService(BaseDomainService): + async def _load_achievement_cache(self, key: str) -> tuple[Optional[Any], bool]: + """读取资历基础数据缓存,返回数据和是否已过期。""" + return await self._cache.get(key) + + async def _save_achievement_cache(self, key: str, payload: Any): + """写入资历基础数据缓存。""" + await self._cache.set(key, payload) + + async def _get_achievement_base_data( + self, cache_key: str, config_key: str + ) -> Optional[Dict[str, Any]]: + """获取资历菜单或点数数据,优先使用未过期缓存""" + cached, expired = await self._load_achievement_cache(cache_key) + if cached and not expired: + return cached + + data: Optional[Dict[str, Any]] = await self.request(config_key) + if data and isinstance(data, dict): + await self._save_achievement_cache(cache_key, data) + return data + + if cached: + logger.warning(f"资历基础数据接口失败,使用旧缓存: {cache_key}") + return cached + + return None + + def _flatten_achievement_ids(self, values: Any) -> list[int]: + """展开菜单中的单个资历 ID 和数组资历 ID""" + result = [] + + if isinstance(values, list): + for item in values: + result.extend(self._flatten_achievement_ids(item)) + return result + + try: + result.append(int(values)) + except (TypeError, ValueError): + pass + + return result + + def _category_achievement_ids(self, category: Dict[str, Any]) -> list[int]: + """提取分类及其子分类包含的资历 ID""" + ids = self._flatten_achievement_ids(category.get("achievements", [])) + for child in category.get("children", []) or []: + if isinstance(child, dict): + ids.extend(self._flatten_achievement_ids(child.get("achievements", []))) + return ids + + def _build_achievement_progress( + self, + name: str, + achievement_ids: list[int], + point_map: Dict[str, Any], + completed_ids: set[int], + ) -> Dict[str, Any]: + """按资历点数计算完成进度""" + unique_ids = set(achievement_ids) + total_points = 0 + completed_points = 0 + completed_count = 0 + + for achievement_id in unique_ids: + try: + point = int(point_map.get(str(achievement_id), 0) or 0) + except (TypeError, ValueError): + point = 0 + + total_points += point + if achievement_id in completed_ids: + completed_count += 1 + completed_points += point + + percent = round(completed_points / total_points * 100, 2) if total_points else 0 + + return { + "name": name, + "total_points": total_points, + "completed_points": completed_points, + "percent": percent, + "percent_text": f"{percent:.2f}%", + "achievement_count": len(unique_ids), + "completed_count": completed_count, + } + + async def zili(self, name: str, server: str, choice: int) -> Dict[str, Any]: + """角色资历""" + return_data = self.new_result() + + if choice not in ACHIEVEMENT_CHOICE_MAP: + return_data["msg"] = "无效序号,结束会话" + return return_data + + role_params = {"server": server, "name": name} + role_data: Optional[Dict[str, Any]] = await self.request( + "jx3_jueshexinxi", params=role_params + ) + if not role_data or not isinstance(role_data, dict): + return_data["msg"] = "未查询到角色" + return return_data + + global_id = role_data.get("globalId") + if not global_id: + return_data["msg"] = "无法获取角色全区 ID" + return return_data + + achievement_data = await self.request( + "jx3box_user_achievements", + params={"jx3id": global_id}, + out_key="data", + ) + if not achievement_data or not isinstance(achievement_data, dict): + return_data["msg"] = "未查询到资历数据" + return return_data + + completed_ids = set() + achievements = achievement_data.get("achievements") or "" + if isinstance(achievements, str): + for item in achievements.split(","): + try: + completed_ids.add(int(item.strip())) + except ValueError: + continue + elif isinstance(achievements, list): + completed_ids = set(self._flatten_achievement_ids(achievements)) + + menu_payload = await self._get_achievement_base_data( + "achievement_menus", + "jx3box_achievement_menus", + ) + point_payload = await self._get_achievement_base_data( + "achievement_points", + "jx3box_achievement_points", + ) + if not menu_payload or not point_payload: + return_data["msg"] = "基础资历数据获取失败" + return return_data + + menus = menu_payload.get("menus", {}) + points = point_payload.get("points", {}) + if not isinstance(menus, dict) or not isinstance(points, dict): + return_data["msg"] = "基础资历数据格式异常" + return return_data + + selected_menu_id, selected_title = ACHIEVEMENT_CHOICE_MAP[choice] + items = [] + + if choice == 0: + total_ids = [] + for _, menu_id, title in ACHIEVEMENT_CHOICES[1:]: + category = menus.get(str(menu_id), {}) + if not isinstance(category, dict): + continue + category_ids = self._category_achievement_ids(category) + total_ids.extend(category_ids) + items.append( + self._build_achievement_progress( + title, category_ids, points, completed_ids + ) + ) + summary = self._build_achievement_progress( + selected_title, total_ids, points, completed_ids + ) + else: + category = menus.get(str(selected_menu_id), {}) + if not isinstance(category, dict) or not category: + return_data["msg"] = "未找到该资历分类" + return return_data + + category_ids = self._category_achievement_ids(category) + summary = self._build_achievement_progress( + selected_title, category_ids, points, completed_ids + ) + for child in category.get("children", []) or []: + if not isinstance(child, dict): + continue + child_ids = self._flatten_achievement_ids(child.get("achievements", [])) + items.append( + self._build_achievement_progress( + child.get("name", "未命名"), child_ids, points, completed_ids + ) + ) + + if not await self.attach_template(return_data, "zili.html"): + return return_data + + return_data["data"] = { + "title": selected_title, + "summary": summary, + "items": items, + "role_name": role_data.get("roleName", name), + "server": role_data.get("serverName", server), + "zone": role_data.get("zoneName", ""), + "force_name": role_data.get("forceName", "无"), + "camp_name": role_data.get("campName", "无"), + "tong_name": role_data.get("tongName", "无"), + "updated_at": achievement_data.get("updated_at", ""), + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + return_data["code"] = 200 + + return return_data diff --git a/core/services/adventure.py b/core/services/adventure.py new file mode 100644 index 0000000..e837109 --- /dev/null +++ b/core/services/adventure.py @@ -0,0 +1,424 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + +from astrbot.api import logger + +from .base import DATA_PROCESSING_ERRORS, BaseDomainService + + +class AdventureService(BaseDomainService): + async def yanhuachaxun(self, server: str, name: str) -> Dict[str, Any]: + """烟花查询""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "name": name} + + # 2. 调用基础请求 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_yanhuachaxun", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + for item in data: + if isinstance(item, dict): + timestamp = item.get("time") + if timestamp and isinstance(timestamp, (int, float)): + item["time"] = datetime.fromtimestamp(timestamp).strftime( + "%Y-%m-%d %H:%M:%S" + ) + else: + item["time"] = "未知时间" + + return_data["data"]["list"] = data + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "yanhuan.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def pianzhi(self, uid: str) -> Dict[str, Any]: + """骗子查询""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"uid": uid} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_pianzhi", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + records = data["records"] + + if not records: + result_msg = "未找到该用户行骗记录,很棒!继续保持!" + else: + result_msg = "" + + for record in records: + result_msg += ( + f"区服:{record['server']} 标签:{record['tieba']}\n\n" + ) + + for item in record["data"]: + result_msg += f"标题:{item['title']}\n" + result_msg += f"地址:{item['url']}\n" + result_msg += f"ID:{item['tid']}\n" + result_msg += f"内容:{item['text']}\n" + item_time = datetime.fromtimestamp(item["time"]).strftime( + "%Y-%m-%d %H:%M:%S" + ) + result_msg += f"时间:{item_time}\n\n" + + result_msg += "\n\n" + return_data["data"] = result_msg + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理返回数据失败: {e}") + return_data["msg"] = "处理返回数据失败" + return return_data + + return_data["code"] = 200 + + return return_data + + async def juesheqiyu(self, name: str, server: str) -> Dict[str, Any]: + """角色奇遇""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "name": name} + + # 2. 调用基础请求 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_juesheqiyu", params=params + ) + + if not data: + return_data["msg"] = "未找到改角色奇遇信息" + return return_data + + # 3. 处理返回数据 + try: + return_data["data"]["ptqy"] = [] + return_data["data"]["jsqy"] = [] + return_data["data"]["cwqy"] = [] + + for item in data: + item["time"] = datetime.fromtimestamp(item["time"]).strftime( + "%Y-%m-%d %H:%M:%S" + ) + if item["level"] == 1: + return_data["data"]["ptqy"].append(item) + if item["level"] == 2: + return_data["data"]["jsqy"].append(item) + if item["level"] == 3: + return_data["data"]["cwqy"].append(item) + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理返回数据失败: {e}") + return_data["msg"] = "处理返回数据失败" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "juesheqiyu.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def weizuoqiyu(self, name: str, server: str) -> Dict[str, Any]: + """未做奇遇""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "name": name} + + # 2. 调用基础请求 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_weizuoqiyu", params=params + ) + + if not data: + return_data["msg"] = "未查询到未做奇遇信息" + return return_data + + if not isinstance(data, list): + return_data["msg"] = "获取接口信息失败或数据格式错误" + return return_data + + # 3. 处理返回数据 + try: + return_data["data"]["ptqy"] = [] + return_data["data"]["jsqy"] = [] + return_data["data"]["name"] = name + return_data["data"]["server"] = server + + for item in data: + event_name = item.get("name") + if not event_name: + continue + + try: + level = int(item.get("level", 0)) + except (TypeError, ValueError): + continue + + event_item = {"event": event_name, "time": "未触发"} + + if level == 1: + return_data["data"]["ptqy"].append(event_item) + elif level == 2: + return_data["data"]["jsqy"].append(event_item) + + if not return_data["data"]["ptqy"] and not return_data["data"]["jsqy"]: + return_data["msg"] = "未查询到未做普通或绝世奇遇" + return return_data + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理返回数据失败: {e}") + return_data["msg"] = "处理返回数据失败" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "weizuoqiyu.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def qiyutongji(self, name: str, server: str) -> Dict[str, Any]: + """奇遇统计""" + return_data = self.new_result() + + params = {"server": server, "name": name} + + data_list: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_qiyutongji", params=params + ) + + if not data_list or not isinstance(data_list, list): + return_data["msg"] = "获取接口信息失败或数据格式错误" + return return_data + + # 格式化时间 + try: + for item in data_list: + item["time"] = datetime.fromtimestamp(item["time"]).strftime( + "%Y-%m-%d %H:%M:%S" + ) + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 准备模板渲染数据 + try: + return_data["data"] = { + "items": data_list, + "server": server, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "qiyuname": name, + } + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"模板数据准备失败: {e}") + return_data["msg"] = "系统错误:模板渲染数据准备失败" + return return_data + + # 加载模板 + if not await self.attach_template(return_data, "qiyuliebiao.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def jinqiqiyu(self, server: str) -> Dict[str, Any]: + """近期奇遇""" + return_data = self.new_result() + + params = {"server": server} + + data_list: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_jinqiqiyu", params=params + ) + + if not data_list or not isinstance(data_list, list): + return_data["msg"] = "未查询到近期奇遇信息" + return return_data + + # 格式化时间 + try: + for item in data_list: + event_time = item.get("time") + if event_time: + try: + item["time"] = datetime.fromtimestamp(int(event_time)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + item["time"] = str(event_time) + else: + item["time"] = "" + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 准备模板渲染数据 + try: + return_data["data"] = { + "items": data_list, + "server": server, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"模板数据准备失败: {e}") + return_data["msg"] = "系统错误:模板渲染数据准备失败" + return return_data + + # 加载模板 + if not await self.attach_template(return_data, "jinqiqiyu.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def qiyuhuizong(self, server: str, num: int = 7) -> Dict[str, Any]: + """奇遇汇总""" + return_data = self.new_result() + + if num < 1 or num > 30: + return_data["msg"] = "汇总天数需在 1-30 天之间" + return return_data + + params = {"server": server, "num": num} + + data_list: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_qiyuhuizong", params=params + ) + + if not data_list or not isinstance(data_list, list): + return_data["msg"] = "未查询到奇遇汇总信息" + return return_data + + # 整理最近一次触发信息 + try: + for item in data_list: + latest = item.get("data") + if not isinstance(latest, dict): + latest = {} + + item["latest_name"] = latest.get("name", "") + + latest_time = latest.get("time") + if latest_time: + try: + item["latest_time"] = datetime.fromtimestamp( + int(latest_time) + ).strftime("%Y-%m-%d %H:%M:%S") + except (TypeError, ValueError, OSError): + item["latest_time"] = str(latest_time) + else: + item["latest_time"] = "" + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 准备模板渲染数据 + try: + return_data["data"] = { + "items": data_list, + "server": server, + "num": num, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"模板数据准备失败: {e}") + return_data["msg"] = "系统错误:模板渲染数据准备失败" + return return_data + + # 加载模板 + if not await self.attach_template(return_data, "qiyuhuizong.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def qiyugonglue(self, name: str) -> Dict[str, Any]: + """奇遇攻略""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"name": name} + + # 2. 调用基础请求 + data: Optional[list[Dict[str, Any]]] = await self.request( + "jx3box_qiyugonglue", params=params, out_key="list" + ) + + if not data: + return_data["msg"] = "未找到该奇遇" + return return_data + + # 提取dwID + dwID = data[0]["dwID"] + data1 = await self.request( + "jx3box_serendipity_achievement", + out_key="", + path_params={"dw_id": dwID}, + ) + if not data1 or not data1.get("achievement_id"): + return_data["msg"] = "获取奇遇成就 ID 失败" + return return_data + + # 获取奇遇攻略 + data2 = await self.request( + "jx3box_achievement_wiki", + out_key="data", + path_params={"achievement_id": data1["achievement_id"]}, + ) + if not data2: + return_data["msg"] = "获取攻略数据异常" + return return_data + + # 4. 处理数据 + try: + return_data["data"] = {} + content = data2["post"]["content"] + return_data["temp"] = content + except DATA_PROCESSING_ERRORS: + logger.exception("处理返回数据失败") + return_data["msg"] = "处理返回数据失败" + return return_data + + return_data["code"] = 200 + + return return_data diff --git a/core/services/arena.py b/core/services/arena.py new file mode 100644 index 0000000..9e6d3d1 --- /dev/null +++ b/core/services/arena.py @@ -0,0 +1,182 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + +from astrbot.api import logger + +from .base import DATA_PROCESSING_ERRORS, BaseDomainService + + +class ArenaService(BaseDomainService): + async def zhanji(self, name: str, server: str, mode: str) -> Dict[str, Any]: + """战绩""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "name": name, "mode": mode} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request("jx3_zhanji", params=params) + + if not data: + return_data["msg"] = "查询角色战绩失败" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "zhanji.html"): + return return_data + + return_data["data"] = data + return_data["code"] = 200 + + return return_data + + async def mingjianpaihang(self, limit: str, mode: str) -> Dict[str, Any]: + """名剑排行""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"limit": limit, "mode": mode} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_mingjianpaihang", params=params + ) + + if not data: + return_data["msg"] = "获取接口数据失败" + return return_data + + return_data["data"]["lists"] = data + + # 4. 加载模板 + if not await self.attach_template(return_data, "mingjianpaihang.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def mingjiantongji(self, mode: str = "33") -> Dict[str, Any]: + """名剑统计""" + return_data = self.new_result() + + mode = str(mode) + if mode not in {"22", "33", "55"}: + return_data["msg"] = "模式错误,可选值:22、33、55" + return return_data + + # 1. 构造请求参数 + params = {"mode": mode} + + # 2. 调用基础请求 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_mingjiantongji", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到名剑统计信息" + return return_data + + # 3. 准备模板渲染数据 + try: + return_data["data"] = { + "items": data, + "mode": mode, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"模板数据准备失败: {e}") + return_data["msg"] = "系统错误:模板渲染数据准备失败" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "mingjiantongji.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def tuanduizhaomu(self, server: str, keyword: str) -> Dict[str, Any]: + """团队招募""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "keyword": keyword} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_tuanduizhaomu", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + for item in data["data"]: + item["createTime"] = datetime.fromtimestamp( + item["createTime"] + ).strftime("%Y-%m-%d %H:%M:%S") + item["number"] = f"{item['number']}/{item['maxNumber']}" + return_data["data"]["list"] = data["data"] + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "tuanduizhaomu.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def shitu(self, type_value: int, keyword: str, server: str) -> Dict[str, Any]: + """师徒招募""" + return_data = self.new_result() + + if type_value not in {1, 2}: + return_data["msg"] = "师徒查询类型错误" + return return_data + + # 1. 构造请求参数 + params = {"type": type_value, "server": server, "keyword": keyword} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request("jx3_shitu", params=params) + + if not data or not isinstance(data, dict): + return_data["msg"] = "未查询到师徒招募信息" + return return_data + + # 3. 处理返回数据 + try: + items = data.get("data", []) + if not items: + return_data["msg"] = "未查询到师徒招募信息" + return return_data + + title = "收徒信息" if type_value == 1 else "拜师信息" + return_data["data"] = { + "items": items, + "server": data.get("server", server), + "keyword": keyword, + "type_value": type_value, + "title": title, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "shitu.html"): + return return_data + + return_data["code"] = 200 + + return return_data diff --git a/core/services/base.py b/core/services/base.py new file mode 100644 index 0000000..8d0d549 --- /dev/null +++ b/core/services/base.py @@ -0,0 +1,133 @@ +from dataclasses import dataclass +from typing import Any, TypedDict + +from astrbot.api import logger + +from ..adapters import APIAdapterRouter +from ..api_config import APIConfigError +from ..fun_basic import load_template +from ..request import APIClientError +from ..sqlite import AsyncSQLiteDB +from .cache import JSONCacheRepository + +DATA_PROCESSING_ERRORS = ( + AttributeError, + IndexError, + KeyError, + OSError, + OverflowError, + TypeError, + ValueError, +) + + +class ServiceResult(TypedDict): + code: int + msg: str + data: Any + temp: str + icons: dict[str, Any] + + +def success(data: Any, *, template: str = "") -> ServiceResult: + return { + "code": 200, + "msg": "", + "data": data, + "temp": template, + "icons": {}, + } + + +def failure(message: str) -> ServiceResult: + return { + "code": 0, + "msg": message, + "data": {}, + "temp": "", + "icons": {}, + } + + +@dataclass(frozen=True, slots=True) +class ServiceContext: + router: APIAdapterRouter + database: AsyncSQLiteDB + cache: JSONCacheRepository + + +class BaseDomainService: + """领域 Service 共享的请求、缓存和结果构建能力。""" + + def __init__(self, context: ServiceContext): + self._router = context.router + self._database = context.database + self._cache = context.cache + + @staticmethod + def new_result() -> ServiceResult: + return { + "code": 0, + "msg": "", + "data": {}, + "temp": "", + "icons": {}, + } + + @staticmethod + def success(data: Any, *, template: str = "") -> ServiceResult: + return success(data, template=template) + + @staticmethod + def failure(message: str) -> ServiceResult: + return failure(message) + + async def template_result( + self, + template_name: str, + data: Any, + ) -> ServiceResult: + try: + template = await load_template(template_name) + except FileNotFoundError: + logger.exception(f"模板文件不存在: {template_name}") + return failure("系统错误:模板文件不存在") + return success(data, template=template) + + async def attach_template( + self, + result: ServiceResult, + template_name: str, + ) -> bool: + try: + result["temp"] = await load_template(template_name) + except FileNotFoundError: + logger.exception(f"模板文件不存在: {template_name}") + result["msg"] = "系统错误:模板文件不存在" + return False + return True + + async def request( + self, + config_key: str, + params: dict[str, Any] | None = None, + out_key: str | None = "data", + path_params: dict[str, Any] | None = None, + ) -> Any | None: + try: + data = await self._router.request( + config_key, + params=params, + out_key=out_key, + path_params=path_params, + ) + if data is None: + logger.warning(f"获取接口信息失败或返回空数据: {config_key}") + return data + except (APIConfigError, APIClientError) as exc: + logger.error( + f"API 请求失败: key={config_key}, " + f"status={getattr(exc, 'status', None)}, " + f"error={type(exc).__name__}: {exc}" + ) + return None diff --git a/core/services/cache.py b/core/services/cache.py new file mode 100644 index 0000000..4942918 --- /dev/null +++ b/core/services/cache.py @@ -0,0 +1,70 @@ +import json +from datetime import datetime, timedelta +from typing import Any + +from astrbot.api import logger + +from ..sqlite import AsyncSQLiteDB + + +class JSONCacheRepository: + """基于 achievement_cache 表的通用 JSON 缓存。""" + + def __init__(self, database: AsyncSQLiteDB): + self._database = database + + async def get( + self, + key: str, + *, + max_age: timedelta = timedelta(days=30), + ) -> tuple[Any | None, bool]: + try: + row = await self._database.select_one( + "achievement_cache", + "key=?", + (key,), + ) + except Exception as exc: + logger.error(f"读取 JSON 缓存失败: key={key}, error={exc}") + return None, True + + if not row: + return None, True + + try: + payload = json.loads(row.get("content", "{}")) + updated_at = datetime.strptime( + row.get("updated_at", ""), + "%Y-%m-%d %H:%M:%S", + ) + except (json.JSONDecodeError, TypeError, ValueError) as exc: + logger.error(f"解析 JSON 缓存失败: key={key}, error={exc}") + return None, True + + return payload, datetime.now() - updated_at > max_age + + async def set(self, key: str, payload: Any) -> None: + try: + content = json.dumps(payload, ensure_ascii=False) + except (TypeError, ValueError) as exc: + logger.error(f"序列化 JSON 缓存失败: key={key}, error={exc}") + return + + try: + await self._database.execute( + """ + INSERT INTO achievement_cache (key, content, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + content=excluded.content, + updated_at=excluded.updated_at + """, + ( + key, + content, + datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + ), + ) + except Exception as exc: + logger.error(f"写入 JSON 缓存失败: key={key}, error={exc}") diff --git a/core/services/calendar.py b/core/services/calendar.py new file mode 100644 index 0000000..d5ba0c4 --- /dev/null +++ b/core/services/calendar.py @@ -0,0 +1,696 @@ +from collections.abc import Mapping, Sequence +from datetime import datetime +from typing import Any, Dict, List, Optional + +from astrbot.api import logger + +from ..fun_basic import compare_date_str, week_to_num +from .base import DATA_PROCESSING_ERRORS, BaseDomainService + + +def _text(value: Any, default: str = "无") -> str: + if value is None: + return default + result = str(value).strip() + return result or default + + +def _format_items(value: Any) -> str: + if value is None: + return "无" + if isinstance(value, str): + items = [value.strip()] if value.strip() else [] + elif isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray)): + items = [text for item in value if (text := _text(item, ""))] + else: + items = [] + return "\n".join(f"• {item}" for item in items) if items else "无" + + +class CalendarService(BaseDomainService): + async def richang(self, server: str, num: int = 0) -> Dict[str, Any]: + """日常活动""" + params = {"server": server, "num": num} + data: Optional[Dict[str, Any]] = await self.request( + "jx3_richang", params=params + ) + if not isinstance(data, dict) or not data: + return self.failure("获取接口信息失败") + + week = _text(data.get("week"), "未知") + week_label = week if week.startswith("星期") else f"星期{week}" + + weekly = data.get("weekly") + if isinstance(weekly, Mapping): + weekly_conn = weekly.get("conn") + weekly_raid = weekly.get("raid") + else: + weekly_conn = None + weekly_raid = None + + lines = [ + f"【{_text(server, '未知服务器')}】", + f"{_text(data.get('date'), '未知日期')} {week_label}", + "", + "【今日活动】", + f"大战:{_text(data.get('war'))}", + f"战场:{_text(data.get('battle'))}", + f"阵营:{_text(data.get('orecar'))}", + f"宗门:{_text(data.get('school'))}", + f"驰援:{_text(data.get('rescue'))}", + "", + "【画像】", + _format_items(data.get("draw")), + "", + "【宠物福缘】", + _format_items(data.get("lucky")), + "", + "【家园声望·加倍道具】", + _format_items(data.get("card")), + "", + "【武林通鉴·公共任务】", + _format_items(weekly_conn), + "", + "【武林通鉴·团队秘境】", + _format_items(weekly_raid), + ] + return self.success("\n".join(lines)) + + async def richangyuche(self) -> Dict[str, Any]: + """日常预测""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"num": 30} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_richangyuche", params=params + ) + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + items = [] + num_richang = week_to_num(data["data"][0]["week"]) or 0 + # 空白数据 + for _ in range(num_richang): + items.append( + {"en": False, "compare": "", "date": "", "war": "", "battle": ""} + ) + + # 真实数据 + for m in data["data"]: + items.append( + { + "en": True, + "compare": compare_date_str(m.get("date", "")), + "date": m.get("date", ""), + "war": m.get("war", ""), + "battle": m.get("battle", ""), + } + ) + + return_data["data"]["items"] = items + return_data["data"]["today"] = data["today"] + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 加载模板 + if not await self.attach_template(return_data, "richangyuche.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def xingxiashijian(self, name: str) -> Dict[str, Any]: + """行侠事件""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"name": name} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_xingxiashijian", params=params + ) + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + return_data["data"]["items"] = data + return_data["data"]["name"] = name + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 加载模板 + if not await self.attach_template(return_data, "xingxiashijian.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def shaohua(self) -> Dict[str, Any]: + """骚话""" + return_data = self.new_result() + + # 因为没有参数,所以 params=None + data: Optional[Dict[str, Any]] = await self.request("jx3_shaohua") + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + text = data.get("text") + if text: + return_data["data"] = text + else: + return_data["msg"] = "接口未返回文本" + return return_data + + return_data["code"] = 200 + + return return_data + + async def jiemi(self) -> Dict[str, Any]: + """解密""" + return_data = self.new_result() + + params = {} + + data: Optional[Dict[str, Any]] = await self.request("jx3_jiemi", params=params) + + if not data or not isinstance(data, dict): + return_data["msg"] = "未查询到解密信息" + return return_data + + try: + curr = data.get("curr", {}) + next_data = data.get("next", {}) + if not isinstance(curr, dict): + curr = {} + if not isinstance(next_data, dict): + next_data = {} + + return_data["data"] = "\n".join( + [ + "解密", + f"当前时间:{data.get('time', '')}", + f"当前节点:{curr.get('node', '')}", + f"当前结果:{curr.get('data', '')}", + f"下轮节点:{next_data.get('node', '')}", + f"下轮结果:{next_data.get('data', '')}", + f"剩余时间:{data.get('cdtn', '')}", + ] + ) + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理解密数据失败: {e}") + return_data["msg"] = "处理解密数据失败" + return return_data + + return_data["code"] = 200 + + return return_data + + async def baizhan(self) -> Dict[str, Any]: + """百战首领""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {} + + data: Optional[Dict[str, Any]] = await self.request( + "jx3_baizhan", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 处理返回数据 + + try: + return_data["data"] = data + return_data["data"]["start"] = datetime.fromtimestamp( + float(data["start"]) + ).strftime("%Y-%m-%d %H:%M:%S") + return_data["data"]["end"] = datetime.fromtimestamp( + float(data["end"]) + ).strftime("%Y-%m-%d %H:%M:%S") + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + # 加载模板 + if not await self.attach_template(return_data, "baizhan.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def fuyaojjiutian(self, server: str) -> Dict[str, Any]: + """扶摇九天""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server} + + # 2. 调用基础请求 + data: Optional[list[Dict[str, Any]]] = await self.request( + "jx3_fuyaojiutian", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + data_new = data[0] + data_time = datetime.fromtimestamp(data_new["time"]).strftime( + "%Y-%m-%d %H:%M:%S" + ) + result_msg = f"{server}\n" + if data_new["status"] == 0: + result_msg += "本轮扶摇九天尚未开启\n" + if data_new["status"] == 1: + result_msg += "本轮扶摇九天正在进行\n" + if data_new["status"] == 2: + result_msg += "本轮扶摇九天已经结束\n" + result_msg += f"开启时间:{data_time}\n" + return_data["data"] = result_msg + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理返回数据失败: {e}") + return_data["msg"] = "处理返回数据失败" + return return_data + + return_data["code"] = 200 + + return return_data + + async def zhueevent(self) -> Dict[str, Any]: + """诛恶事件""" + return_data = self.new_result() + + params = {} + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_zhueevent", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到诛恶事件信息" + return return_data + + try: + items = [] + for item in data: + if not isinstance(item, dict): + continue + + event_time = item.get("time") + if event_time: + try: + item["time"] = datetime.fromtimestamp(int(event_time)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + item["time"] = str(event_time) + else: + item["time"] = "" + + items.append(item) + + if not items: + return_data["msg"] = "未查询到诛恶事件信息" + return return_data + + return_data["data"] = { + "items": items, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理诛恶事件数据失败: {e}") + return_data["msg"] = "处理诛恶事件数据失败" + return return_data + + if not await self.attach_template(return_data, "zhueevent.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def benrichitu(self) -> Dict[str, Any]: + """本日赤兔""" + return_data = self.new_result() + + params = {} + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_benrichitu", params=params + ) + + def no_data() -> Dict[str, Any]: + return_data["code"] = 200 + return_data["data"] = "本日赤兔 暂无数据" + return return_data + + if not data or not isinstance(data, list): + return no_data() + + try: + items = [] + for item in data: + if not isinstance(item, dict): + continue + + date_value = item.get("date", "") + if date_value: + try: + date_text = datetime.fromtimestamp(int(date_value)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + date_text = str(date_value) + else: + date_text = "" + + items.append( + { + "date": date_text, + "server": item.get("server", ""), + "map_name": item.get("map_name", ""), + } + ) + + if not items: + return no_data() + + result_lines = ["本日赤兔"] + for item in items: + result_lines.extend( + [ + f"时间:{item['date']}", + f"区服:{item['server']}", + f"地图:{item['map_name']}", + "", + ] + ) + + return_data["code"] = 200 + return_data["data"] = "\n".join(result_lines).rstrip() + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理本日赤兔数据失败: {e}") + return no_data() + + return return_data + + async def benzhouchitu(self) -> Dict[str, Any]: + """本周赤兔""" + return_data = self.new_result() + + params = {} + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_benzhouchitu", params=params + ) + + def no_data() -> Dict[str, Any]: + return_data["code"] = 200 + return_data["data"] = "本周赤兔 暂无数据" + return return_data + + if not data or not isinstance(data, list): + return no_data() + + try: + items = [] + for item in data: + if not isinstance(item, dict): + continue + + date_value = item.get("date", "") + if date_value: + try: + date_text = datetime.fromtimestamp(int(date_value)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + date_text = str(date_value) + else: + date_text = "" + + items.append( + { + "date": date_text, + "server": item.get("server", ""), + "map_name": item.get("map_name", ""), + } + ) + + if not items: + return no_data() + + result_lines = ["本周赤兔"] + for item in items: + result_lines.extend( + [ + f"时间:{item['date']}", + f"区服:{item['server']}", + f"地图:{item['map_name']}", + "", + ] + ) + + return_data["code"] = 200 + return_data["data"] = "\n".join(result_lines).rstrip() + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理本周赤兔数据失败: {e}") + return no_data() + + return return_data + + async def zhenyingevent(self) -> Dict[str, Any]: + """阵营事件""" + return_data = self.new_result() + + params = {} + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_zhenyingevent", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到阵营事件信息" + return return_data + + try: + items = [] + for item in data: + if not isinstance(item, dict): + continue + + seize_time = item.get("seize_time") + if seize_time: + try: + item["seize_time"] = datetime.fromtimestamp( + int(seize_time) + ).strftime("%Y-%m-%d %H:%M:%S") + except (TypeError, ValueError, OSError): + item["seize_time"] = str(seize_time) + else: + item["seize_time"] = "" + + items.append(item) + + if not items: + return_data["msg"] = "未查询到阵营事件信息" + return return_data + + return_data["data"] = { + "items": items, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理阵营事件数据失败: {e}") + return_data["msg"] = "处理阵营事件数据失败" + return return_data + + if not await self.attach_template(return_data, "zhenyingevent.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def guanaishouling(self) -> Dict[str, Any]: + """关隘首领""" + return_data = self.new_result() + + params = {} + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_guanaishouling", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到关隘首领信息" + return return_data + + try: + now_ts = int(datetime.now().timestamp()) + + def format_time(value: Any) -> str: + if not value: + return "" + try: + return datetime.fromtimestamp(int(value)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + return str(value) + + def format_remaining(end_time: Any) -> str: + if not end_time: + return "" + try: + seconds = max(0, int(end_time) - now_ts) + except (TypeError, ValueError): + return str(end_time) + + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + secs = seconds % 60 + return f"{hours}时{minutes:02d}分{secs:02d}秒" + + groups = [] + for group in data: + if not isinstance(group, dict): + continue + + records = group.get("data", []) + if not isinstance(records, list) or not records: + continue + + parsed_records = [] + for item in records: + if not isinstance(item, dict): + continue + + end_time = item.get("end_time") + parsed_records.append( + { + "camp_name": item.get("camp_name", ""), + "castle": item.get("castle", ""), + "str_status": item.get("str_status", ""), + "start_time": format_time(item.get("start_time")), + "end_time": format_time(end_time), + "remaining_time": format_remaining(end_time), + } + ) + + if parsed_records: + groups.append( + { + "server": group.get("server", ""), + "records": parsed_records, + } + ) + + if not groups: + return_data["msg"] = "未查询到关隘首领信息" + return return_data + + return_data["data"] = { + "groups": groups, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理关隘首领数据失败: {e}") + return_data["msg"] = "处理关隘首领数据失败" + return return_data + + if not await self.attach_template(return_data, "guanaishouling.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def shuamamsg(self, server: str, type: str, subtype: str) -> Dict[str, Any]: + """刷马消息""" + return_data = self.new_result() + + params = {"server": server, "type": type, "subtype": subtype} + # 提取字段可能返回字典 + data: Optional[Dict[str, Any]] = await self.request( + "jx3box_shuamamsg", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败或数据格式错误" + return return_data + + try: + # + new_msg = data.get("list", [])[0] + return_data["status"] = new_msg.get("id") + + result_msg = f"{server}\n" + result_msg += f"{new_msg.get('content')}\n" + result_msg += f"{new_msg.get('created_at')}\n" + + return_data["data"] = result_msg + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + return_data["code"] = 200 + + return return_data + + async def shuma(self, server: str) -> Dict[str, Any]: + """刷马""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_shuama", params=params, out_key="" + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + _data = data["data"]["data"] + result_msg = f"{server}\n" + result_msg += f"黑戈壁:\n{_data['黑戈壁'][0]}\n" + result_msg += f"阴山大草原:\n{_data['阴山大草原'][0]}\n" + result_msg += f"鲲鹏岛:\n{_data['鲲鹏岛'][0]}\n" + result_msg += f"\n{data['data']['note']}" + return_data["data"] = result_msg + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理返回数据失败: {e}") + return_data["msg"] = "处理返回数据失败" + return return_data + + return_data["code"] = 200 + + return return_data diff --git a/core/services/character.py b/core/services/character.py new file mode 100644 index 0000000..e1720b0 --- /dev/null +++ b/core/services/character.py @@ -0,0 +1,424 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + +import astrbot.api.message_components as Comp +from astrbot.api import logger + +from .base import DATA_PROCESSING_ERRORS, BaseDomainService + + +class CharacterService(BaseDomainService): + async def zhenyan(self, name: str) -> Dict[str, Any]: + """阵眼""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"name": name} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_zhenyan", params=params + ) + + if not data or not isinstance(data, dict): + return_data["msg"] = "未查询到该心法阵眼信息" + return return_data + + # 3. 处理返回数据 + try: + items = data.get("data", []) + if not isinstance(items, list) or not items: + return_data["msg"] = "未查询到该心法阵眼信息" + return return_data + + result_msg = f"{data.get('name', name)}-{data.get('skillName', '')}\n" + for item in items: + if not isinstance(item, dict): + continue + result_msg += f"{item.get('name', '')}:{item.get('desc', '')}\n" + + return_data["data"] = result_msg.rstrip() + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理阵眼数据失败: {e}") + return_data["msg"] = "处理阵眼数据失败" + return return_data + + return_data["code"] = 200 + + return return_data + + async def qixue(self, name: str) -> Dict[str, Any]: + """奇穴""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"name": name} + + # 2. 调用基础请求 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_qixue", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到该心法奇穴信息" + return return_data + + # 3. 处理返回数据 + try: + level_titles = { + 1: "主奇穴", + 2: "第一重", + 3: "第二重", + 4: "第三重", + 5: "第四重", + 6: "第五重", + 7: "第六重", + 8: "混池", + } + + groups = [] + for group in data: + if not isinstance(group, dict): + continue + + level = group.get("level") + try: + level_key = int(level) + except (TypeError, ValueError): + level_key = 0 + + items = group.get("data", []) + if not isinstance(items, list) or not items: + continue + + parsed_items = [] + for item in items: + if not isinstance(item, dict): + continue + + class_value = item.get("class") + is_active = class_value == 1 or str(class_value) == "1" + parsed_items.append( + { + "name": item.get("name", ""), + "icon": item.get("icon", ""), + "desc": item.get("desc", ""), + "class_text": "主动" if is_active else "被动", + "interval": item.get("interval", "") if is_active else "", + } + ) + + if parsed_items: + groups.append( + { + "level": level_key, + "title": level_titles.get(level_key, f"第{level_key}组"), + "talents": parsed_items, + } + ) + + if not groups: + return_data["msg"] = "未查询到该心法奇穴信息" + return return_data + + groups.sort(key=lambda item: item["level"]) + return_data["data"] = { + "name": name, + "groups": groups, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理奇穴数据失败: {e}") + return_data["msg"] = "处理奇穴数据失败" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "qixue.html"): + return return_data + + return_data["code"] = 200 + return return_data + + async def jineng(self, name: str) -> Dict[str, Any]: + """技能""" + return_data = self.new_result() + + params = {"name": name} + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_jineng", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到该心法技能信息" + return return_data + + try: + groups = [] + for group in data: + if not isinstance(group, dict): + continue + + skills = group.get("data", []) + if not isinstance(skills, list) or not skills: + continue + + parsed_skills = [] + for skill in skills: + if not isinstance(skill, dict): + continue + + parsed_skills.append( + { + "name": skill.get("name", ""), + "icon": skill.get("icon", ""), + "desc": skill.get("desc", ""), + "interval": skill.get("interval", ""), + "distance": skill.get("distance", ""), + "release_type": skill.get("releaseType", ""), + "weapon": skill.get("weapon", ""), + } + ) + + if parsed_skills: + groups.append( + { + "title": group.get("class", "其他技能"), + "skills": parsed_skills, + } + ) + + if not groups: + return_data["msg"] = "未查询到该心法技能信息" + return return_data + + return_data["data"] = { + "name": name, + "groups": groups, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理技能数据失败: {e}") + return_data["msg"] = "处理技能数据失败" + return return_data + + if not await self.attach_template(return_data, "jineng.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def jueshe(self, name: str, server: str) -> Dict[str, Any]: + """角色""" + params = {"server": server, "name": name} + data: Optional[Dict[str, Any]] = await self.request( + "jx3_jueshexinxi", params=params + ) + if not isinstance(data, dict) or not data: + return self.failure("获取接口信息失败") + + result_msg = ( + f"服务器:{data.get('zoneName', '无')}·{data.get('serverName', '无')}\n" + f"名称:{data.get('roleName', '无')}\n" + f"角色ID:{data.get('roleId', '无')}\n" + f"全区ID:{data.get('globalId', '无')}\n" + f"职业:{data.get('forceName', '无')}·{data.get('bodyName', '无')}\n" + f"帮会:{data.get('tongName', '无')}\n" + f"阵营:{data.get('campName', '无')}\n" + ) + return self.success(result_msg) + + async def jueshemingpian(self, server: str, name: str) -> Dict[str, Any]: + """单张名片""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "name": name} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_jieshemingpian", params=params + ) + + if not data or not isinstance(data, dict): + return_data["msg"] = "未找到该角色名片" + return return_data + + # 3. 处理返回数据 + try: + url = data.get("showAvatar") + if not url: + return_data["msg"] = "未获取到名片图片" + return return_data + + server_name = data.get("serverName", server) + role_name = data.get("roleName", name) + show_like = data.get("showLike", 0) + msg = f"{server_name}-{role_name} 点赞:{show_like}" + + return_data["data"] = [ + Comp.Plain(msg), + Comp.Image.fromURL(url), + ] + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + return_data["code"] = 200 + + return return_data + + async def jingnai(self, name: str, server: str) -> Dict[str, Any]: + """百战精耐""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "name": name} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_jingnai", params=params + ) + + if not data or not isinstance(data, dict): + return_data["msg"] = "未查询到角色精耐信息" + return return_data + + # 3. 处理返回数据 + try: + items = data.get("skill_list", []) + if not isinstance(items, list) or not items: + return_data["msg"] = "未查询到技能信息" + return return_data + + color_map = { + 7: "黑色", + 6: "紫色", + 5: "红色", + 4: "绿色", + 3: "蓝色", + 2: "黄色", + 0: "无", + } + + for item in items: + if not isinstance(item, dict): + continue + + color = item.get("skill_color") + try: + color_key = int(color) + except (TypeError, ValueError): + color_key = None + item["skill_color_text"] = color_map.get( + color_key, str(color) if color is not None else "" + ) + + update_time = data.get("update_time") + if update_time: + try: + update_time = datetime.fromtimestamp(int(update_time)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + update_time = str(update_time) + else: + update_time = "" + + return_data["data"] = { + "server": data.get("server", server), + "role_name": data.get("role_name", name), + "skill_energy": data.get("skill_energy", ""), + "skill_stamina": data.get("skill_stamina", ""), + "skill_count": data.get("skill_count", len(items)), + "items": items, + "update_time": update_time, + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理精耐数据失败: {e}") + return_data["msg"] = "处理精耐数据失败" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "jingnai.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def shuoyoumingpian(self, server: str, name: str) -> Dict[str, Any]: + """所有名片""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "name": name} + + # 2. 调用基础请求 + data: Optional[list[Dict[str, Any]]] = await self.request( + "jx3_allmingpian", params=params + ) + + if not data: + return_data["msg"] = "未找到该角色" + return return_data + + # 3. 处理返回数据 + try: + chain = [] + for m in data: + status = "已启用" if m.get("showActive") else "未启用" + msg = f"第{m.get('showIndex')}张 {status}" + url = m.get("showAvatar") + + if not url: + logger.warning(f"第{m.get('showIndex')}张名片缺少图片URL,已跳过") + continue + + chain.extend( + [ + Comp.Plain(msg), + Comp.Image.fromURL(url), + ] + ) + + if not chain: + return_data["msg"] = "未获取到有效的名片数据" + return return_data + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + return_data["data"] = chain + return_data["code"] = 200 + + return return_data + + async def shuijimingpian( + self, force: str, body: str, server: str + ) -> Dict[str, Any]: + """随机名片""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "body": body, "force": force} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_shuijimingpian", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 (直接提取图片 URL) + return_data["data"] = data["showAvatar"] + + return_data["code"] = 200 + + return return_data diff --git a/core/services/content.py b/core/services/content.py new file mode 100644 index 0000000..c38cb14 --- /dev/null +++ b/core/services/content.py @@ -0,0 +1,195 @@ +from typing import Any, Dict, Optional + +from astrbot.api import logger + +from .base import DATA_PROCESSING_ERRORS, BaseDomainService + + +class ContentService(BaseDomainService): + async def helps(self) -> Dict[str, Any]: + """帮助""" + return await self.template_result("helps.html", {}) + + async def bagua(self, type: str) -> Dict[str, Any]: + """八卦""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"class": type, "limit": "5"} + + # 2. 调用基础请求 + data: Optional[list[Dict[str, Any]]] = await self.request( + "jx3_bagua", params=params + ) + + # 3. 处理返回数据 + try: + if not data: + result_msg = f"未找到相关 {type} 记录。\n" + result_msg += ( + "可选范围:818 616 鬼网三 鬼网3 树洞 记录 " + "教程 街拍 故事 避雷 吐槽 提问" + ) + else: + result_msg = f"类型:【{type}】\n\n" + + for item in data: + result_msg += f"{item['title']}\n" + result_msg += f"分区:{item['zone']} 服务器:{item['server']}\n" + result_msg += f"所属吧:{item['name']}\n" + result_msg += f"链接:https://tieba.baidu.com/p/{item['url']}\n" + result_msg += f"日期:{item['date']}\n\n" + return_data["data"] = result_msg + except DATA_PROCESSING_ERRORS: + logger.exception("处理返回数据失败") + return_data["msg"] = "处理返回数据失败" + return return_data + + return_data["code"] = 200 + + return return_data + + async def hong1(self, name: str) -> Dict[str, Any]: + """宏 心法""" + return_data = self.new_result() + + # 数据库查询数据 + result = await self._database.select_one( + "kungfu", + "name=? OR name1=? OR name2=? OR name3=? OR name4=? OR name5=?", + (name, name, name, name, name, name), + ) + + if result is None: + return_data["msg"] = "未找到该心法" + return return_data + + kungfu = result.get("name", None) + if kungfu is None: + return_data["msg"] = "未找到该心法" + return return_data + + logger.debug(f"查询到数据:{kungfu}") + + # 1. 构造请求参数 + params = {"subtype": kungfu} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3box_hong", params=params + ) + logger.debug(data) + if not data: + return_data["msg"] = "未找到该心法一键宏" + return return_data + + # 提取ID + pid_list = [0] + msg = "按照热度排列\n" + n = 1 + try: + for m in data.get("list", []): + msg += f"{n}、{m['author']}\t{m['post_title']}\n" + pid_list.append(m["ID"]) + n += 1 + + return_data["msg"] = msg + return_data["data"]["list"] = pid_list + return_data["data"]["num"] = n + + except DATA_PROCESSING_ERRORS: + logger.exception("处理返回数据失败") + return_data["msg"] = "处理返回数据失败" + return return_data + + return_data["code"] = 200 + + return return_data + + async def hong2(self, pid: str) -> Dict[str, Any]: + """宏 心法""" + return_data = self.new_result() + + # 发起请求 + data = await self.request( + "jx3box_hong_detail", + out_key="data", + path_params={"pid": pid}, + ) + # 验证数据 + if not data: + return_data["msg"] = "获取宏数据异常" + return return_data + + # 4. 处理数据 + try: + return_data["temp"] = data["post_content"] + msg = "" + for m in data["post_meta"]["data"]: + msg += f"【宏名称】\n{m['name']}\n" + msg += f"【使用说明】\n{m['desc']}\n" + msg += f"【宏脚本】\n{m['macro']}\n\n" + + return_data["data"] = msg + + except DATA_PROCESSING_ERRORS: + logger.exception("处理返回数据失败") + return_data["msg"] = "处理返回数据失败" + return return_data + + return_data["code"] = 200 + + return return_data + + async def peizhuang(self, name: str, tags: str) -> Dict[str, Any]: + """配装""" + return_data = self.new_result() + + # 数据库查询数据 + result = await self._database.select_one( + "kungfu", + "name=? OR name1=? OR name2=? OR name3=? OR name4=? OR name5=?", + (name, name, name, name, name, name), + ) + logger.debug(result) + if result is None: + return_data["msg"] = "未找到该心法" + return return_data + + mount = result.get("pzid", None) + if not mount: + return_data["msg"] = "未找到该心法" + return return_data + + logger.debug(f"查询到数据:{mount}") + + # 1. 构造请求参数 + params = {"mount": mount, "tags": tags} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3box_peizhuang", params=params + ) + logger.debug(data) + # 验证数据 + if not data: + return_data["msg"] = "配装数据获取异常" + return return_data + + # 3. 处理返回数据 + try: + result_msg = f"{name}--配装\n" + for item in data["list"]: + result_msg += f"【{item['zlp']}】--{item['title']}\n" + result_msg += f"链接:https://www.jx3box.com/pz/view/{item['id']}\n\n" + + return_data["data"] = result_msg + + except DATA_PROCESSING_ERRORS as e: + logger.exception("处理返回数据失败:", e) + return_data["msg"] = "处理返回数据失败" + return return_data + + return_data["code"] = 200 + + return return_data diff --git a/core/services/faction.py b/core/services/faction.py new file mode 100644 index 0000000..960977a --- /dev/null +++ b/core/services/faction.py @@ -0,0 +1,419 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + +from astrbot.api import logger + +from .base import DATA_PROCESSING_ERRORS, BaseDomainService + +ROLE_RANK_NAMES = { + "名士五十强", + "老江湖五十强", + "兵甲藏家五十强", + "名师五十强", + "阵营英雄五十强", + "薪火相传五十强", + "庐园广记一百强", +} + + +TONG_RANK_NAMES = { + "浩气神兵宝甲五十强", + "恶人神兵宝甲五十强", + "浩气爱心帮会五十强", + "恶人爱心帮会五十强", + "赛季恶人五十强", + "赛季浩气五十强", + "上周恶人五十强", + "上周浩气五十强", + "本周恶人五十强", + "本周浩气五十强", +} + + +class FactionService(BaseDomainService): + async def zilipaixing(self, school: str, server: str) -> Dict[str, Any]: + """资历排行""" + return_data = self.new_result() + + params = { + "server": server, + "school": school, + } + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_zilipaixing", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到资历排行信息" + return return_data + + try: + return_data["data"] = { + "items": data, + "school": school, + "server": server, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理资历排行数据失败: {e}") + return_data["msg"] = "处理资历排行数据失败" + return return_data + + if not await self.attach_template(return_data, "zilipaixing.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def shilianpaixing(self, name: str, server: str) -> Dict[str, Any]: + """试炼排行""" + return_data = self.new_result() + + params = { + "server": server, + "name": name, + } + + data: Optional[Dict[str, Any]] = await self.request( + "jx3_shilianpaixing", params=params + ) + + if not data or not isinstance(data, dict): + return_data["msg"] = "未查询到试炼排行信息" + return return_data + + try: + items = data.get("data", []) + if not isinstance(items, list) or not items: + return_data["msg"] = "未查询到试炼排行信息" + return return_data + + update_time = data.get("time") + if update_time: + try: + update_time = datetime.fromtimestamp(int(update_time)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + update_time = str(update_time) + else: + update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + return_data["data"] = { + "items": items, + "name": data.get("name", name), + "server": data.get("server", server), + "update_time": update_time, + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理试炼排行数据失败: {e}") + return_data["msg"] = "处理试炼排行数据失败" + return return_data + + if not await self.attach_template(return_data, "shilianpaixing.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def shapan(self, server: str) -> Dict[str, Any]: + """区服沙盘""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"serverName": server} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "aijx3_shapan", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 (直接提取图片 URL) + pic_url = data.get("picUrl") + if pic_url: + return_data["data"] = pic_url + else: + return_data["msg"] = "接口未返回图片URL" + return return_data + + return_data["code"] = 200 + + return return_data + + async def zhengyingpaimai(self, server: str, name: str) -> Dict[str, Any]: + """阵营拍卖""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "name": name} + + # 2. 调用基础请求 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_zhengyingpaimai", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + for item in data: + item["time"] = datetime.fromtimestamp(item["time"]).strftime( + "%Y-%m-%d %H:%M:%S" + ) + return_data["data"]["list"] = data + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理返回数据失败: {e}") + return_data["msg"] = "处理返回数据失败" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "zhengyingpaimai.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def bangzhanjilu(self, server: str) -> Dict[str, Any]: + """帮战记录""" + return_data = self.new_result() + + params = { + "server": server, + } + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_bangzhanjilu", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到帮战记录" + return return_data + + try: + + def format_time(value: Any) -> str: + if not value: + return "" + try: + return datetime.fromtimestamp(int(value)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + return str(value) + + def format_duration(value: Any) -> str: + if value in (None, ""): + return "" + try: + seconds = max(0, int(value)) + except (TypeError, ValueError): + return str(value) + + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + secs = seconds % 60 + return f"{hours}时{minutes:02d}分{secs:02d}秒" + + items = [] + for item in data: + if not isinstance(item, dict): + continue + + items.append( + { + "zoneName": item.get("zoneName", ""), + "serverName": item.get("serverName", ""), + "declaringTongName": item.get("declaringTongName", ""), + "acceptingTongName": item.get("acceptingTongName", ""), + "startTime": format_time(item.get("startTime")), + "matchDuration": format_duration(item.get("matchDuration")), + "endTime": format_time(item.get("endTime")), + } + ) + + if not items: + return_data["msg"] = "未查询到帮战记录" + return return_data + + return_data["data"] = { + "items": items, + "server": server, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理帮战记录数据失败: {e}") + return_data["msg"] = "处理帮战记录数据失败" + return return_data + + if not await self.attach_template(return_data, "bangzhanjilu.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def tongzhanyy(self, server: str) -> Dict[str, Any]: + """统战歪歪""" + return_data = self.new_result() + + params = { + "server": server, + } + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_tongzhanyy", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到统战歪歪信息" + return return_data + + try: + lines = ["统战歪歪"] + has_channel = False + + for group in data: + if not isinstance(group, dict): + continue + + group_server = group.get("server", "") + channels = group.get("data", []) + if not isinstance(channels, list) or not channels: + continue + + if len(lines) > 1: + lines.append("") + lines.append(f"服务器:{group_server}") + + for item in channels: + if not isinstance(item, dict): + continue + + has_channel = True + short_id = item.get("esid") or item.get("asid", "") + lines.extend( + [ + f"阵营:{item.get('campName', '')}", + f"频道ID:{item.get('sid', '')}", + f"短位ID:{short_id}", + f"在线人数:{item.get('users', '')}", + f"频道名:{item.get('snick', '')}", + "", + ] + ) + + if not has_channel: + return_data["msg"] = "未查询到统战歪歪信息" + return return_data + + return_data["data"] = "\n".join(lines).rstrip() + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理统战歪歪数据失败: {e}") + return_data["msg"] = "处理统战歪歪数据失败" + return return_data + + return_data["code"] = 200 + + return return_data + + async def dilujilu(self, server: str) -> Dict[str, Any]: + """的卢记录""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server} + + # 2. 调用基础请求 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_dilujilu", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + for item in data: + item["refresh_time"] = datetime.fromtimestamp( + item["refresh_time"] + ).strftime("%Y-%m-%d %H:%M:%S") + item["capture_time"] = datetime.fromtimestamp( + item["capture_time"] + ).strftime("%Y-%m-%d %H:%M:%S") + item["auction_time"] = datetime.fromtimestamp( + item["auction_time"] + ).strftime("%Y-%m-%d %H:%M:%S") + return_data["data"]["list"] = data + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, "dilujilu.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def rank_statistical(self, name: str, server: str) -> Dict[str, Any]: + """各类风云录榜单""" + return_data = self.new_result() + + if name in ROLE_RANK_NAMES: + template_name = "rank_role.html" + elif name in TONG_RANK_NAMES: + template_name = "rank_tong.html" + else: + return_data["msg"] = "榜单名称错误" + return return_data + + # 1. 构造请求参数 + params = {"server": server, "name": name} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_rank_statistical", params=params + ) + + if not data or not isinstance(data, dict): + return_data["msg"] = "未查询到榜单信息" + return return_data + + # 3. 处理返回数据 + try: + items = data.get("data", []) + if not isinstance(items, list) or not items: + return_data["msg"] = "未查询到榜单信息" + return return_data + + return_data["data"] = { + "items": items, + "server": data.get("server", server), + "rank_name": data.get("name", name), + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 4. 加载模板 + if not await self.attach_template(return_data, template_name): + return return_data + + return_data["code"] = 200 + + return return_data diff --git a/core/services/information.py b/core/services/information.py new file mode 100644 index 0000000..eeeca2f --- /dev/null +++ b/core/services/information.py @@ -0,0 +1,412 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional, Union + +from astrbot.api import logger + +from .base import DATA_PROCESSING_ERRORS, BaseDomainService + + +class InformationService(BaseDomainService): + async def keju(self, subject: str, limit: int) -> Dict[str, Any]: + """科举""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"subject": subject, "limit": limit} + + # 2. 调用基础请求 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_keju", params=params + ) + if not data: + return_data["msg"] = "未查询到相关题目" + return return_data + + # 3. 处理返回数据 + try: + # 格式化字符串,利用字典的 get 方法提供默认值 + result_msg = "" + for m in data: + result_msg += f"{m['id']}.{m['question']}\n" + result_msg += f"答案:{m['answer']}\n\n" + + return_data["data"] = result_msg + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + return_data["code"] = 200 + + return return_data + + async def huajia(self, server: str, name: str, map: str) -> Dict[str, Any]: + """花价""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "name": name, "map": map} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request("jx3_huajia", params=params) + if not data: + return_data["msg"] = "未查询到相关内容" + return return_data + + # 3. 处理返回数据 + try: + return_data["data"]["data"] = data + return_data["data"]["server"] = server + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 加载模板 + if not await self.attach_template(return_data, "huajia.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def zhuangshi(self, name: str) -> Dict[str, Any]: + """装饰""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"name": name} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request( + "jx3_zhuangshi", params=params + ) + if not data: + return_data["msg"] = "未查询到相关内容" + return return_data + + # 3. 处理返回数据 + try: + return_data["data"]["data"] = data + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 加载模板 + if not await self.attach_template(return_data, "zhuangshi.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def qiwu(self, name: str) -> Dict[str, Any]: + """器物""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"name": name} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request("jx3_qiwu", params=params) + if not data: + return_data["msg"] = "未查询到相关内容" + return return_data + + # 3. 处理返回数据 + try: + return_data["data"]["data"] = data + return_data["data"]["name"] = name + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + # 加载模板 + if not await self.attach_template(return_data, "qiwu.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def xinwen(self, num: int) -> Dict[str, Any]: + """新闻资讯""" + return_data = self.new_result() + + params = {"limit": num} + # 提取字段可能返回列表 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_xinweng", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "获取接口信息失败或数据格式错误" + return return_data + + try: + # + result = data[0] + return_data["status"] = result.get("id") + + result_msg = "新闻资讯推送\n" + # 仅展示前1条,避免消息过长 + for i, item in enumerate(data[:num], 1): + result_msg += f"{i}. 【{item.get('type', '无类型')}】\n" + result_msg += f"标题:{item.get('title', '未知时间')}\n" + result_msg += f"时间:{item.get('date', '未知时间')}\n" + result_msg += f"链接:{item.get('url', '无链接')}\n" + + return_data["data"] = result_msg + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + + return_data["code"] = 200 + + return return_data + + async def weihu(self, num: int) -> Dict[str, Any]: + """维护""" + return_data = self.new_result() + + params = {"limit": num} + # 提取字段可能返回列表 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_weihu", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "获取接口信息失败或数据格式错误" + return return_data + + try: + # + result = data[0] + return_data["status"] = result.get("id") + + result_msg = "维护推送\n" + # 仅展示前1条,避免消息过长 + for i, item in enumerate(data[:num], 1): + result_msg += f"{i}. 【{item.get('type', '无类型')}】\n" + result_msg += f"标题:{item.get('title', '未知时间')}\n" + result_msg += f"时间:{item.get('date', '未知时间')}\n" + result_msg += f"链接:{item.get('url', '无链接')}\n" + + return_data["data"] = result_msg + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + + return_data["code"] = 200 + + return return_data + + async def qufu(self, name: str) -> Dict[str, Any]: + """区服信息""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"name": name} + + # 2. 调用基础请求 + data: Optional[Dict[str, Any]] = await self.request("jx3_qufu", params=params) + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + # 格式化字符串,利用字典的 get 方法提供默认值 + result_msg = f"主服:{data.get('zone', '无')}-{data.get('name', '无')}\n" + + slave = data.get("slave", []) + slave_msg = f"区服:{', '.join(slave)}\n" + + alias = data.get("alias", []) + alias_msg = f"别名:{', '.join(alias)}\n" + + return_data["data"] = result_msg + slave_msg + alias_msg + return_data["code"] = 200 + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + return_data["code"] = 200 + + return return_data + + async def kaifu(self, server: str) -> Dict[str, Any]: + """开服状态查询""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": server, "type": "1"} + + # 2. 调用基础请求 + data: Optional[Dict[str, Union[int, str]]] = await self.request( + "jx3_zhuangtai", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 3. 处理返回数据 + try: + status = data.get("status", 0) + lasttime = data.get("lasttime", 0) + shuttime = data.get("shuttime", 0) + + lasttime_t = datetime.fromtimestamp(float(lasttime)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + shuttime_t = datetime.fromtimestamp(float(shuttime)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + + if status == 1: + status_str = ( + f"{server}服务器已开服,快冲,快冲!\n开服时间:{lasttime_t}" + ) + status_bool = True + else: + status_str = ( + f"{server}服务器当前维护中,等会再来吧!\n维护时间:{shuttime_t}" + ) + status_bool = False + + return_data["status"] = status_bool + return_data["data"] = status_str + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"kaifu 数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + return_data["code"] = 200 + + return return_data + + async def zhuangtai(self) -> Dict[str, Any]: + """区服状态""" + return_data = self.new_result() + + # 1. 构造请求参数 + params = {"server": "", "type": "2"} + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_zhuangtai", params=params + ) + + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 处理返回数据 + try: + server_wj = [] + server_dx = [] + server_sx = [] + + for itme in data: + if itme["zone"] == "无界区": + server_wj.append(itme) + elif itme["zone"] == "电信区": + server_dx.append(itme) + elif itme["zone"] == "双线区": + server_sx.append(itme) + + return_data["data"]["server_wj"] = server_wj + return_data["data"]["server_dx"] = server_dx + return_data["data"]["server_sx"] = server_sx + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + # 加载模板 + if not await self.attach_template(return_data, "qufuzhuangtai.html"): + return return_data + + return_data["code"] = 200 + + return return_data + + async def jigai(self) -> Dict[str, Any]: + """技改记录""" + return_data = self.new_result() + + # 提取字段可能返回列表 + data: Optional[List[Dict[str, Any]]] = await self.request("jx3_jigai") + + if not data or not isinstance(data, list): + return_data["msg"] = "获取接口信息失败或数据格式错误" + return return_data + + try: + result_msg = "最近技改\n" + + for i, item in enumerate(data[:3], 1): + result_msg += f"{i}. {item.get('title', '无标题')}\n" + result_msg += f"时间:{item.get('time', '未知时间')}\n" + result_msg += f"链接:{item.get('url', '无链接')}\n\n" + + return_data["data"] = result_msg + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + return return_data + + return_data["code"] = 200 + + return return_data + + async def xiaoyao(self) -> Dict[str, Any]: + """小药""" + return_data = self.new_result() + + data: Optional[List[Dict[str, Any]]] = await self.request("jx3_xiaoyao") + logger.debug(data) + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + + # 处理返回数据 + try: + result = {} + + for item in data: + k = item["kungfu"] + color = item["color"] + cls = item["class"] + name = item["name"] + + if k not in result: + result[k] = {"kungfu": k, "purple": {}, "blue": {}} + + if color == "紫": + result[k]["purple"][cls] = name + else: + result[k]["blue"][cls] = name + + return_data["data"]["items"] = list(result.values()) + logger.debug(return_data["data"]) + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"数据处理时出错: {e}") + return_data["msg"] = "处理接口返回信息时出错" + + # 加载模板 + if not await self.attach_template(return_data, "xiaoyao.html"): + return return_data + + return_data["code"] = 200 + + return return_data diff --git a/core/services/trade.py b/core/services/trade.py new file mode 100644 index 0000000..fb8478b --- /dev/null +++ b/core/services/trade.py @@ -0,0 +1,352 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + +from astrbot.api import logger + +from ..fun_basic import gold_to_parts +from .base import DATA_PROCESSING_ERRORS, BaseDomainService + + +class TradeService(BaseDomainService): + async def _get_trade_item_groups(self) -> Optional[List[Dict[str, Any]]]: + """获取交易行物品库,优先使用未过期缓存""" + cache_key = "trade_item_groups" + cached, expired = await self._cache.get(cache_key) + if isinstance(cached, list) and not expired: + return cached + + data = await self.request("jx3box_trade_items") + if isinstance(data, list) and data: + await self._cache.set(cache_key, data) + return data + + if isinstance(cached, list) and cached: + logger.warning("交易行物品库接口失败,使用旧缓存") + return cached + + return None + + def _flatten_trade_items( + self, groups: List[Dict[str, Any]] + ) -> list[Dict[str, Any]]: + """从交易行物品分组中提取可查询物品""" + items = [] + for group in groups: + if not isinstance(group, dict): + continue + for item in group.get("items", []) or []: + if not isinstance(item, dict): + continue + item_id = item.get("item_id") + label = item.get("label") + if not item_id or not label: + continue + items.append( + { + "item_id": str(item_id), + "label": str(label), + "icon": str(item.get("icon") or ""), + } + ) + return items + + def _match_trade_items( + self, items: list[Dict[str, Any]], keyword: str, limit: int = 50 + ) -> list[Dict[str, Any]]: + """按物品名模糊匹配交易行物品""" + keyword = (keyword or "").strip() + if not keyword: + return [] + + matched = [] + seen = set() + for item in items: + label = item.get("label", "") + item_id = item.get("item_id", "") + if keyword not in label or item_id in seen: + continue + seen.add(item_id) + if label == keyword: + rank = 0 + elif label.startswith(keyword): + rank = 1 + else: + rank = 2 + matched.append((rank, len(label), label, item)) + + matched.sort(key=lambda row: (row[0], row[1], row[2])) + return [row[3] for row in matched[:limit]] + + async def jinjia(self, server: str, limit: str) -> Dict[str, Any]: + """区服金价""" + return_data = self.new_result() + + params = {"server": server, "limit": limit} + data_list: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_jinjia", params=params + ) + + if not data_list or not isinstance(data_list, list): + return_data["msg"] = "获取接口信息失败或数据格式错误" + return return_data + + # 加载模板 + if not await self.attach_template(return_data, "jinjia.html"): + return return_data + + # 准备模板渲染数据 + try: + return_data["data"]["items"] = data_list + + except DATA_PROCESSING_ERRORS as e: + logger.error(f"模板数据准备失败: {e}") + return_data["msg"] = "系统错误:模板渲染数据准备失败" + return return_data + + return_data["code"] = 200 + + return return_data + + async def wujia(self, Name: str, server: str) -> Dict[str, Any]: + """物价查询""" + params_search = {"name": Name, "server": server} + search_data: Optional[Dict[str, Any]] = await self.request( + "jx3_wujia", params=params_search + ) + + if not isinstance(search_data, dict) or not search_data: + return self.failure("未找到该外观") + + return await self.template_result("wujia.html", search_data) + + async def jiaoyihang(self, name: str, server: str) -> Dict[str, Any]: + """区服交易行""" + return_data = self.new_result() + + item_groups = await self._get_trade_item_groups() + if not item_groups: + return_data["msg"] = "交易行基础物品数据获取失败" + return return_data + + trade_items = self._flatten_trade_items(item_groups) + matched_items = self._match_trade_items(trade_items, name, 50) + if not matched_items: + return_data["msg"] = "未找到匹配的交易行物品" + return return_data + + item_map = {item["item_id"]: item for item in matched_items} + params = { + "item_ids": list(item_map.keys()), + "server": server, + "aggregate_type": "hourly", + } + price_data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_jiaoyihang", params=params, out_key="" + ) + + if not price_data or not isinstance(price_data, list): + return_data["msg"] = "未查询到交易行价格数据" + return return_data + + try: + result = [] + for price_item in price_data: + if not isinstance(price_item, dict): + continue + + item_id = str(price_item.get("item_id") or "") + base_item = item_map.get(item_id) + if not base_item: + continue + + timestamp = price_item.get("timestamp") + try: + created = datetime.fromtimestamp(int(timestamp)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + created = "" + + result.append( + { + "item_id": item_id, + "name": base_item.get("label", ""), + "icon": ( + "https://icon.jx3box.com/icon/" + f"{base_item.get('icon', '')}.png" + ), + "server": price_item.get("server", server), + "price": price_item.get("price", 0), + "price_parts": gold_to_parts(price_item.get("price", 0)), + "sample": price_item.get("sample", 0), + "created": created, + } + ) + + if not result: + return_data["msg"] = "未查询到交易行价格数据" + return return_data + + return_data["data"] = { + "search_name": name, + "server": server, + "matched_count": len(matched_items), + "result_count": len(result), + "list": result, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理交易行数据失败: {e}") + return_data["msg"] = "处理交易行数据失败" + return return_data + + # 5. 模板渲染 + if not await self.attach_template(return_data, "jiaoyihang.html"): + return return_data + return_data["code"] = 200 + + return return_data + + async def tiebawujia( + self, name: str, limit: int = 5, server: str = "" + ) -> Dict[str, Any]: + """贴吧物价""" + return_data = self.new_result() + + try: + limit = int(limit) + except (TypeError, ValueError): + return_data["msg"] = "记录数量必须是数字" + return return_data + + if limit < 1 or limit > 50: + return_data["msg"] = "贴吧物价记录数量必须在 1-50 之间" + return return_data + + params = { + "server": server, + "name": name, + "limit": limit, + } + + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_tiebawujia", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到贴吧物价记录" + return return_data + + try: + lines = [ + f"贴吧物价:{name}", + f"服务器:{server}", + f"记录数:{len(data)}", + "", + ] + + for index, item in enumerate(data, start=1): + if not isinstance(item, dict): + continue + + item_time = item.get("time", "") + if item_time: + try: + item_time = datetime.fromtimestamp(int(item_time)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + item_time = str(item_time) + + lines.extend( + [ + f"{index}. {item.get('name', '')}", + ( + f"区服:{item.get('zone', '')} " + f"服务器:{item.get('server', '')}" + ), + f"内容:{item.get('context', '')}", + f"回复:{item.get('reply', '')} 楼层:{item.get('floor', '')}", + f"时间:{item_time}", + f"链接:https://tieba.baidu.com/p/{item.get('url', '')}", + "", + ] + ) + + if len(lines) <= 4: + return_data["msg"] = "未查询到贴吧物价记录" + return return_data + + return_data["data"] = "\n".join(lines).rstrip() + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理贴吧物价数据失败: {e}") + return_data["msg"] = "处理贴吧物价数据失败" + return return_data + + return_data["code"] = 200 + + return return_data + + async def diaoluo( + self, name: str, limit: int = 20, server: str = "" + ) -> Dict[str, Any]: + """物品掉落记录""" + return_data = self.new_result() + + try: + limit = int(limit) + except (TypeError, ValueError): + return_data["msg"] = "数量必须是数字" + return return_data + + if limit < 1 or limit > 100: + return_data["msg"] = "掉落记录数量需在 1-100 之间" + return return_data + + # 1. 构造请求参数 + params = {"server": server, "name": name, "limit": limit} + + # 2. 调用基础请求 + data: Optional[List[Dict[str, Any]]] = await self.request( + "jx3_diaoluo", params=params + ) + + if not data or not isinstance(data, list): + return_data["msg"] = "未查询到掉落记录" + return return_data + + # 3. 处理返回数据 + try: + for item in data: + if not isinstance(item, dict): + continue + + drop_time = item.get("time") + if drop_time: + try: + item["time"] = datetime.fromtimestamp(int(drop_time)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + except (TypeError, ValueError, OSError): + item["time"] = str(drop_time) + else: + item["time"] = "" + + return_data["data"] = { + "items": data, + "name": name, + "limit": limit, + "server": server, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + except DATA_PROCESSING_ERRORS as e: + logger.error(f"处理掉落数据失败: {e}") + return_data["msg"] = "处理掉落数据失败" + return return_data + + # 4. 模板渲染 + if not await self.attach_template(return_data, "diaoluo.html"): + return return_data + return_data["code"] = 200 + + return return_data diff --git a/core/sqlite.py b/core/sqlite.py index 419574e..f1ea0ac 100644 --- a/core/sqlite.py +++ b/core/sqlite.py @@ -22,12 +22,16 @@ class AsyncSQLiteDB: await self.close() async def connect(self): + if self.conn is not None: + return self.conn = await aiosqlite.connect(self.db_path) self.conn.row_factory = aiosqlite.Row async def close(self): - if self.conn: - await self.conn.close() + connection = self.conn + self.conn = None + if connection: + await connection.close() # ====================== # 基础执行 diff --git a/data/api_config.json b/data/api_config.json index 5e3a95a..c13b561 100644 --- a/data/api_config.json +++ b/data/api_config.json @@ -1,6 +1,34 @@ { + "_services": { + "jx3api": { + "base_url": "https://www.jx3api.com", + "path_prefix": "", + "adapter": "jx3api" + }, + "jx3box_node": { + "base_url": "https://node.jx3box.com", + "path_prefix": "", + "adapter": "jx3box" + }, + "jx3box_cms": { + "base_url": "https://cms.jx3box.com", + "path_prefix": "", + "adapter": "jx3box" + }, + "jx3box_next2": { + "base_url": "https://next2.jx3box.com", + "path_prefix": "", + "adapter": "jx3box" + }, + "jianxiachaguan": { + "base_url": "https://www.jianxiachaguan.cn", + "path_prefix": "", + "adapter": "generic" + } + }, "jx3_richang":{ - "url":"https://www.jx3api.com/data/active/calendar", + "service":"jx3api", + "path":"active/calendar", "method":"GET", "description":"获取剑网三日常任务", "params":{ @@ -9,7 +37,8 @@ } }, "jx3_richangyuche":{ - "url":"https://www.jx3api.com/data/active/list/calendar", + "service":"jx3api", + "path":"active/list/calendar", "method":"GET", "description":"该 API 用于预测每天的日常任务,您可以通过指定时间范围来获取相关任务数据。", "params":{ @@ -17,7 +46,8 @@ } }, "jx3_xingxiashijian":{ - "url":"https://www.jx3api.com/data/active/celebs", + "service":"jx3api", + "path":"active/celebs", "method":"GET", "description":"查询当前时间的楚天社或云从社的进度。", "params":{ @@ -25,7 +55,8 @@ } }, "jx3_keju":{ - "url":"https://www.jx3api.com/data/exam/search", + "service":"jx3api", + "path":"exam/search", "method":"GET", "description":"此接口用于科举考试时的答案查询,支持模糊查询和拼音首字母查询。", "params":{ @@ -34,7 +65,8 @@ } }, "jx3_huajia":{ - "url":"https://www.jx3api.com/data/home/flower", + "service":"jx3api", + "path":"home/flower", "method":"GET", "description":"此接口用于查询家园系统中特定鲜花的最高价格及其对应的采集线路,包括区服和地图信息。", "params":{ @@ -44,7 +76,8 @@ } }, "jx3_zhuangshi":{ - "url":"https://www.jx3api.com/data/home/furniture", + "service":"jx3api", + "path":"home/furniture", "method":"GET", "description":"此接口用于查询家园系统中特定装饰的详细信息,包括来源、属性和外观等内容。", "params":{ @@ -52,7 +85,8 @@ } }, "jx3_qiwu":{ - "url":"https://www.jx3api.com/data/home/travel", + "service":"jx3api", + "path":"home/travel", "method":"GET", "description":"此接口用于查询指定地图的装饰物品产出信息,包括装饰的属性、来源和外观等内容。", "params":{ @@ -60,7 +94,8 @@ } }, "jx3_xinweng":{ - "url":"https://www.jx3api.com/data/news/allnews", + "service":"jx3api", + "path":"news/allnews", "method":"GET", "description":"此接口用于获取官方最新公告及新闻内容,包括新闻分类、标题及链接等信息。", "params":{ @@ -68,7 +103,8 @@ } }, "jx3_weihu":{ - "url":"https://www.jx3api.com/data/news/announce", + "service":"jx3api", + "path":"news/announce", "method":"GET", "description":"获取《剑网3》官方发布的最新维护与版本更新公告列表,包含公告标题、发布日期及原文链接", "params":{ @@ -76,7 +112,8 @@ } }, "jx3_qufu":{ - "url":"https://www.jx3api.com/data/master/search", + "service":"jx3api", + "path":"master/search", "method":"GET", "description":"此接口用于根据区服简称搜索主服务器及其附属服务器的详细信息", "params":{ @@ -84,7 +121,8 @@ } }, "jx3_zhuangtai":{ - "url":"https://www.jx3api.com/data/status/check", + "service":"jx3api", + "path":"status/check", "method":"GET", "description":"此接口用于根据区服简称搜索主服务器及其附属服务器的详细信息", "params":{ @@ -93,21 +131,24 @@ } }, "jx3_jigai":{ - "url":"https://www.jx3api.com/data/skill/rework", + "service":"jx3api", + "path":"skill/rework", "method":"GET", "description":"获取剑网三技能记录", "params":{ } }, "jx3_xiaoyao":{ - "url":"https://www.jx3api.com/data/school/foods", + "service":"jx3api", + "path":"school/foods", "method":"GET", "description":"小药", "params":{ } }, "jx3_zhenyan":{ - "url":"https://www.jx3api.com/data/school/matrix", + "service":"jx3api", + "path":"school/matrix", "method":"GET", "description":"查询指定心法的阵眼效果", "params":{ @@ -117,7 +158,8 @@ } }, "jx3_qixue":{ - "url":"https://www.jx3api.com/data/school/talent", + "service":"jx3api", + "path":"school/talent", "method":"GET", "description":"查询指定心法的奇穴详细效果", "params":{ @@ -127,7 +169,8 @@ } }, "jx3_jineng":{ - "url":"https://www.jx3api.com/data/school/skills", + "service":"jx3api", + "path":"school/skills", "method":"GET", "description":"查询指定心法的技能详情", "params":{ @@ -137,7 +180,8 @@ } }, "jx3_zilipaixing":{ - "url":"https://www.jx3api.com/data/school/seniority", + "service":"jx3api", + "path":"school/seniority", "method":"GET", "description":"查询指定门派的资历排行榜", "params":{ @@ -148,7 +192,8 @@ } }, "jx3_shilianpaixing":{ - "url":"https://www.jx3api.com/data/rank/trials", + "service":"jx3api", + "path":"rank/trials", "method":"GET", "description":"查询指定心法的木桩试炼排行榜", "params":{ @@ -158,7 +203,8 @@ } }, "jx3_shaohua":{ - "url":"https://www.jx3api.com/data/saohua/random", + "service":"jx3api", + "path":"saohua/random", "method":"GET", "description":"获取剑网三骚话", "params":{ @@ -166,7 +212,8 @@ } }, "jx3_jiemi":{ - "url":"https://www.jx3api.com/data/mech/calculator", + "service":"jx3api", + "path":"mech/calculator", "method":"GET", "description":"查询机关解密结果", "params":{ @@ -174,7 +221,8 @@ } }, "aijx3_shapan":{ - "url":"https://www.jianxiachaguan.cn/api2/aijx3-jxcg/game/get-sand-table-img", + "service":"jianxiachaguan", + "path":"api2/aijx3-jxcg/game/get-sand-table-img", "method":"POST", "description":"获取剑网三沙盘状态", "params":{ @@ -182,7 +230,8 @@ } }, "jx3_baizhan":{ - "url":"https://www.jx3api.com/data/active/monster", + "service":"jx3api", + "path":"active/monster", "method":"GET", "description":"获取本周百战异闻录玩法中刷新的首领列表及其特殊技能效果", "params":{ @@ -190,7 +239,8 @@ } }, "jx3_fuyaojiutian":{ - "url":"https://www.jx3api.com/data/active/next/event", + "service":"jx3api", + "path":"active/next/event", "method":"GET", "description":"获取某区服扶摇九天的下次开启时间", "params":{ @@ -199,7 +249,8 @@ } }, "jx3_zhueevent":{ - "url":"https://www.jx3api.com/data/smite/records", + "service":"jx3api", + "path":"smite/records", "method":"GET", "description":"查询诛恶事件记录", "params":{ @@ -207,7 +258,8 @@ } }, "jx3_benrichitu":{ - "url":"https://www.jx3api.com/data/chitu/records", + "service":"jx3api", + "path":"chitu/records", "method":"GET", "description":"查询本日赤兔记录", "params":{ @@ -215,7 +267,8 @@ } }, "jx3_benzhouchitu":{ - "url":"https://www.jx3api.com/data/chitu/week/records", + "service":"jx3api", + "path":"chitu/week/records", "method":"GET", "description":"查询本周赤兔记录", "params":{ @@ -223,7 +276,8 @@ } }, "jx3_zhengyingpaimai":{ - "url":"https://www.jx3api.com/data/auction/records", + "service":"jx3api", + "path":"auction/records", "method":"GET", "description":"此接口用于查询阵营拍卖的记录,包括拍卖的物品名称、金额以及相关信息。支持通过区服名称和物品名称进行精确或模糊查询。", "params":{ @@ -234,7 +288,8 @@ } }, "jx3_zhenyingevent":{ - "url":"https://www.jx3api.com/data/fenxian/records", + "service":"jx3api", + "path":"fenxian/records", "method":"GET", "description":"查询跨服阵营事件记录", "params":{ @@ -242,7 +297,8 @@ } }, "jx3_guanaishouling":{ - "url":"https://www.jx3api.com/data/mine/cart", + "service":"jx3api", + "path":"mine/cart", "method":"GET", "description":"查询各服务器关隘首领刷新记录", "params":{ @@ -250,7 +306,8 @@ } }, "jx3_bangzhanjilu":{ - "url":"https://www.jx3api.com/data/battle/records", + "service":"jx3api", + "path":"battle/records", "method":"GET", "description":"查询指定服务器帮战记录", "params":{ @@ -259,7 +316,8 @@ } }, "jx3_tongzhanyy":{ - "url":"https://www.jx3api.com/data/duowan/statistics", + "service":"jx3api", + "path":"duowan/statistics", "method":"GET", "description":"查询指定服务器统战歪歪信息", "params":{ @@ -268,7 +326,8 @@ } }, "jx3_dilujilu":{ - "url":"https://www.jx3api.com/data/steed/records", + "service":"jx3api", + "path":"steed/records", "method":"GET", "description":"此接口用于查询的卢马的刷新、捕获及拍卖记录,包括捕获者、拍卖价格及相关时间信息", "params":{ @@ -277,7 +336,8 @@ } }, "jx3_yanhuachaxun":{ - "url":"https://www.jx3api.com/data/show/records", + "service":"jx3api", + "path":"show/records", "method":"GET", "description":"此接口用于查询烟花赠送与接收的历史记录,数据可能存在遗漏", "params":{ @@ -287,7 +347,8 @@ } }, "jx3_pianzhi":{ - "url":"https://www.jx3api.com/data/fraud/detailed", + "service":"jx3api", + "path":"fraud/detailed", "method":"GET", "description":"此接口用于通过用户的 QQ 号搜索其在贴吧上的行骗记录,包括贴吧名称、记录标题和详细内容。", "params":{ @@ -296,7 +357,8 @@ } }, "jx3_juesheqiyu":{ - "url":"https://www.jx3api.com/data/event/records", + "service":"jx3api", + "path":"event/records", "method":"GET", "description":"此接口用于查询指定角色的奇遇触发记录,包括奇遇事件、状态及触发时间。注意,数据可能存在遗漏", "params":{ @@ -306,7 +368,8 @@ } }, "jx3_weizuoqiyu":{ - "url":"https://www.jx3api.com/data/event/unfinished", + "service":"jx3api", + "path":"event/unfinished", "method":"GET", "description":"此接口用于查询指定角色未做奇遇记录,数据可能存在遗漏", "params":{ @@ -316,7 +379,8 @@ } }, "jx3_qiyutongji":{ - "url":"https://www.jx3api.com/data/event/statistics", + "service":"jx3api", + "path":"event/statistics", "method":"POST", "description":"获取剑网三某区服奇遇触发记录", "params":{ @@ -327,7 +391,8 @@ } }, "jx3_jinqiqiyu":{ - "url":"https://www.jx3api.com/data/event/recent", + "service":"jx3api", + "path":"event/recent", "method":"GET", "description":"查询指定服务器最近触发的奇遇记录列表", "params":{ @@ -336,7 +401,8 @@ } }, "jx3_qiyuhuizong":{ - "url":"https://www.jx3api.com/data/event/collect", + "service":"jx3api", + "path":"event/collect", "method":"GET", "description":"统计指定服务器最近一段时间内各类奇遇触发汇总", "params":{ @@ -346,7 +412,8 @@ } }, "jx3_zhanji":{ - "url":"https://www.jx3api.com/data/arena/recent", + "service":"jx3api", + "path":"arena/recent", "method":"GET", "description":"此接口用于查询角色近期的名剑战绩记录,包括角色基本信息、战绩详情以及比赛趋势。", "params":{ @@ -358,7 +425,8 @@ } }, "jx3_mingjianpaihang":{ - "url":"https://www.jx3api.com/data/arena/awesome", + "service":"jx3api", + "path":"arena/awesome", "method":"GET", "description":"查询名剑大会的排行榜信息,包括玩家排名、分数、胜率等详细竞技数据", "params":{ @@ -369,7 +437,8 @@ } }, "jx3_mingjiantongji":{ - "url":"https://www.jx3api.com/data/arena/schools", + "service":"jx3api", + "path":"arena/schools", "method":"GET", "description":"统计全服名剑大会各门派当前赛季与上赛季数量", "params":{ @@ -379,7 +448,8 @@ } }, "jx3_tuanduizhaomu":{ - "url":"https://www.jx3api.com/data/recruit/search", + "service":"jx3api", + "path":"recruit/search", "method":"GET", "description":"此接口用于查询指定区服的团队招募信息,包括活动类型、队长信息及当前队伍状态。", "params":{ @@ -390,7 +460,8 @@ } }, "jx3_shitu":{ - "url":"https://www.jx3api.com/data/mentor/search", + "service":"jx3api", + "path":"mentor/search", "method":"GET", "description":"查询游戏内师徒系统拜师/收徒招募信息", "params":{ @@ -401,7 +472,8 @@ } }, "jx3_rank_statistical":{ - "url":"https://www.jx3api.com/data/rank/statistics", + "service":"jx3api", + "path":"rank/statistics", "method":"GET", "description":"查询全服或指定服务器的各类游戏内榜单数据", "params":{ @@ -411,7 +483,8 @@ } }, "jx3_jueshexinxi":{ - "url":"https://www.jx3api.com/data/role/detail", + "service":"jx3api", + "path":"role/detail", "method":"GET", "description":"获取角色信息", "params":{ @@ -421,7 +494,8 @@ } }, "jx3_jieshemingpian":{ - "url":"https://www.jx3api.com/data/card/record", + "service":"jx3api", + "path":"card/record", "method":"GET", "description":"此接口用于查询指定角色的名片墙信息,包括角色展示图片和展示数据的唯一标识。", "params":{ @@ -431,7 +505,8 @@ } }, "jx3_jingnai":{ - "url":"https://www.jx3api.com/data/role/monster", + "service":"jx3api", + "path":"role/monster", "method":"GET", "description":"查询指定角色在百战异闻录玩法中的精力、耐力数值和已习得技能列表", "params":{ @@ -441,7 +516,8 @@ } }, "jx3_allmingpian":{ - "url":"https://www.jx3api.com/data/card/records", + "service":"jx3api", + "path":"card/records", "method":"GET", "description":"获取角色全部名片", "params":{ @@ -451,7 +527,8 @@ } }, "jx3_shuijimingpian":{ - "url":"https://www.jx3api.com/data/card/random", + "service":"jx3api", + "path":"card/random", "method":"GET", "description":"此接口用于随机读取一张符合条件的角色名片。", "params":{ @@ -462,7 +539,8 @@ } }, "jx3_jinjia":{ - "url":"https://www.jx3api.com/data/trade/demon", + "service":"jx3api", + "path":"trade/demon", "method":"GET", "description":"金价比例信息", "params":{ @@ -472,7 +550,8 @@ } }, "jx3_waiguan":{ - "url":"https://www.jx3api.com/data/trade/item/search", + "service":"jx3api", + "path":"trade/item/search", "method":"GET", "description":"统计外观物品的模糊搜索结果", "params":{ @@ -481,7 +560,8 @@ } }, "jx3_wujia":{ - "url":"https://www.jx3api.com/data/trade/records", + "service":"jx3api", + "path":"trade/records", "method":"GET", "description":"此接口用于统计指定物品的黑市价格信息,包括成交、出售、收购等详细记录。", "params":{ @@ -491,8 +571,10 @@ } }, "jx3_jiaoyihang":{ - "url":"https://next2.jx3box.com/api/auction/", + "service":"jx3box_next2", + "path":"api/auction/", "method":"POST", + "retry": true, "description":"获取剑网三区服交易行物品价格数据", "params":{ "item_ids": [], @@ -501,7 +583,8 @@ } }, "jx3box_trade_items":{ - "url":"https://cms.jx3box.com/api/cms/pvx/item/group", + "service":"jx3box_cms", + "path":"api/cms/pvx/item/group", "method":"GET", "description":"魔盒交易行物品分组列表", "params":{ @@ -509,7 +592,8 @@ } }, "jx3_tiebawujia":{ - "url":"https://www.jx3api.com/data/tieba/item/records", + "service":"jx3api", + "path":"tieba/item/records", "method":"GET", "description":"查询指定物品的贴吧价格记录", "params":{ @@ -520,7 +604,8 @@ } }, "jx3_diaoluo":{ - "url":"https://www.jx3api.com/data/reward/statistics", + "service":"jx3api", + "path":"reward/statistics", "method":"GET", "description":"统计全服或指定服务器中特定贵重物品在团队秘境中的掉落记录", "params":{ @@ -531,7 +616,8 @@ } }, "jx3_shuama":{ - "url":"https://www.jx3api.com/data/ranch/records", + "service":"jx3api", + "path":"ranch/records", "method":"GET", "description":"获取某区服坐骑刷新时间", "params":{ @@ -540,7 +626,8 @@ } }, "jx3_bagua":{ - "url":"https://www.jx3api.com/data/tieba/random", + "service":"jx3api", + "path":"tieba/random", "method":"GET", "description":"此接口用于随机搜索指定分类的贴吧帖子,支持多种分类查询。", "params":{ @@ -551,7 +638,8 @@ } }, "jx3box_qiyugonglue":{ - "url":"https://node.jx3box.com/serendipities", + "service":"jx3box_node", + "path":"serendipities", "method":"GET", "description":"魔盒根据名称查询对应奇遇dwID", "params":{ @@ -562,7 +650,8 @@ } }, "jx3box_shuamamsg":{ - "url":"https://next2.jx3box.com/api/game/reporter/horse", + "service":"jx3box_next2", + "path":"api/game/reporter/horse", "method":"GET", "description":"魔盒获取最新的刷马消息", "params":{ @@ -574,7 +663,8 @@ } }, "jx3box_hong":{ - "url":"https://cms.jx3box.com/api/cms/posts", + "service":"jx3box_cms", + "path":"api/cms/posts", "method":"GET", "description":"魔盒获取宏推荐列表", "params":{ @@ -587,7 +677,8 @@ } }, "jx3box_peizhuang":{ - "url":"https://cms.jx3box.com/api/cms/app/pz", + "service":"jx3box_cms", + "path":"api/cms/app/pz", "method":"GET", "description":"魔盒获取宏配置列表", "params":{ @@ -601,7 +692,8 @@ } }, "jx3box_achievement_menus":{ - "url":"https://node.jx3box.com/api/node/achievement/menus", + "service":"jx3box_node", + "path":"api/node/achievement/menus", "method":"GET", "description":"魔盒资历菜单列表", "params":{ @@ -610,13 +702,40 @@ } }, "jx3box_achievement_points":{ - "url":"https://node.jx3box.com/api/node/achievement/points", + "service":"jx3box_node", + "path":"api/node/achievement/points", "method":"GET", "description":"魔盒资历点数列表", "params":{ "client": "std" } + }, + "jx3box_user_achievements": { + "service": "jx3box_next2", + "path": "api/next2/user-achievements", + "method": "GET", + "description": "获取角色资历数据", + "params": {} + }, + "jx3box_serendipity_achievement": { + "service": "jx3box_node", + "path": "serendipity/{dw_id}/achievement", + "method": "GET", + "description": "获取奇遇对应成就 ID", + "params": {} + }, + "jx3box_achievement_wiki": { + "service": "jx3box_cms", + "path": "api/cms/wiki/post/type/achievement/source/{achievement_id}", + "method": "GET", + "description": "获取奇遇攻略详情", + "params": {} + }, + "jx3box_hong_detail": { + "service": "jx3box_cms", + "path": "api/cms/post/{pid}", + "method": "GET", + "description": "获取宏详情", + "params": {} } - - } diff --git a/main.py b/main.py index d46ca37..7dfeef7 100644 --- a/main.py +++ b/main.py @@ -31,7 +31,7 @@ class Jx3ApiPlugin(Star): if self.prefix.get("enable"): logger.info(f"已启用指令前缀功能,前缀为:{self.prefix.get('text')}") else: - logger.info(f"未启用指令前缀功能。") + logger.info("未启用指令前缀功能。") # 默认服务器 self.server = self.conf.get("server","梦江南") @@ -54,6 +54,9 @@ class Jx3ApiPlugin(Star): async def initialize(self): """可选择实现异步的插件初始化方法,当实例化该插件类之后会自动调用该方法。""" try: + # 显式创建 HTTP Session,避免首次并发请求时重复创建连接池 + await self.jx3api.start() + # 数据库初始化 await self.init_bilei_data() await self.init_tuishong_data() @@ -65,9 +68,12 @@ class Jx3ApiPlugin(Star): # 开启后台推送 await self.jx3at.init_tasks() - except Exception as e: + except Exception: if self.jx3at is not None: await self.jx3at.destroy() + await self.jx3api.close() + await self.local_sql_db.close() + await self.plugin_sql_db.close() logger.exception("功能模块初始化失败") raise