diff --git a/CHANGELOG.md b/CHANGELOG.md index f7798b4..f3f975c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## 更新记录 +### version: 2.5: + +增加部分新指令,调整功能页面 + ### version: 2.4: 增加大量新指令 diff --git a/README.md b/README.md index f58a528..65f413c 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,387 @@ -# 剑网三数据查询工具 +# astrbot_plugin_jx3 -## 简介 +基于 [AstrBot](https://docs.astrbot.app/) 框架开发的剑网三游戏数据查询插件。插件主要通过 JX3API、JX3BOX、茶馆等外部接口获取游戏数据,整理为文本消息、富媒体消息,或通过 `HTML + Jinja2` 模板渲染成图片后发送。 -仿照剑网三主流的机器人写了个插件,主要是自用,没有群管理相关的功能,在任何群聊都是使用的一套数据。 +当前插件以“查询型功能”为主,覆盖日常、奇遇、名剑、阵营、交易、职业、排行、副本、活动、名片、百战、推送和本地避雷等场景。部分功能需要配置 JX3API Token,少量职业/角色类接口还需要推栏 ticket。 -插件功能绝大部分都是用AI写的,自己进行了调试。 +## 功能概览 -目前此插件查询功能还没做完,后续会继续完善。 +插件内置 `功能` 指令,会以图片形式展示完整指令列表、参数说明和是否需要令牌。当前帮助页按 16 个功能分组维护,约 87 条指令。 -此插件大部分数据依赖jx3api提供的接口,只有进阶功能需要向其购买token才可使用。其他功能是免token的。 -部分进阶功能除了购买token以外还需要自己去抓包推栏app,获得推栏app的ticket才可正常使用。 +主要功能包括: -## 功能特点 +- 日常与官方:日常、月历、名望、公告、维护、区服、开服、状态、技改。 +- 职业与角色:角色、阵眼、奇穴、技能、小药、宏、配装、资历排行、试炼排行。 +- 奇遇:角色奇遇、未做奇遇、奇遇统计、近期奇遇、奇遇汇总、奇遇攻略。 +- 名剑:战绩、名剑排行、名剑统计。 +- 阵营:沙盘、阵营拍卖、阵营事件、关隘首领、帮战记录、统战 YY。 +- 交易:金价、外观物价、交易行、贴吧物价。 +- 活动:扶摇、诛恶事件、本日赤兔、本周赤兔、刷马、的卢。 +- 其他:拜师、收徒、名片、百战、精耐、招募、掉落、解密、八卦、骗子查询。 +- 本地功能:避雷添加、查看、查询、修改、删除。 +- 推送功能:开服推送、新闻推送、刷马推送、赤兔推送。 -- **后台循环请求**:通过后台一个任务循环请求接口,记录判断。推送最新消息状态 -- **处理接口数据**:将接口返回的数据处理成,玩家能读懂的消息发送 -- **HTML渲染图片**:利用astrbot里面的文转图服务,处理HTML页面渲染成图片 +## 安装与依赖 + +将插件目录放入 AstrBot 的插件目录中,例如: + +```text +data/plugins/astrbot_plugin_jx3 +``` + +安装依赖: + +```bash +pip install -r requirements.txt +``` + +当前依赖: + +- `aiohttp`:异步 HTTP 请求。 +- `aiofiles`:异步文件处理。 +- `aiosqlite`:本地 SQLite 数据库。 +- `apscheduler`:后台推送任务调度。 +- `matplotlib`:部分图像/数据展示依赖。 + +插件元信息在 `metadata.yaml` 中维护,要求 AstrBot 版本 `>=4.11.0`。 ## 插件配置 -**指令前缀** +配置结构由 `_conf_schema.json` 定义,主要配置项如下。 -开启后必须要在指令功能前面加上下面配置的前缀才能正常触发。关闭后直接只有指令功能即可。 +### 指令前缀 -**默认服务器** +```json +{ + "prefix": { + "enable": true, + "text": "剑三" + } +} +``` -配置后在所有功能指令中需要输入服务器参数的可以忽略不写,将按照配置的内容进行补充,填写官方名称。 +- `enable`:是否启用指令前缀检查。 +- `text`:指令前缀内容,默认 `剑三`。 -**JX3API Token** +启用后,用户需要发送类似 `剑三 日常`、`剑三 奇遇 角色名` 才能触发。关闭后可直接发送 `日常`、`奇遇 角色名`。 -使用进阶的功能必须配置,购买在https://www.jx3api.com。 +### 默认服务器 -**推栏ticket** +```json +{ + "server": "梦江南" +} +``` -在进阶功能中,有部分功能依赖推栏APP的数据,所以需要自己抓包推栏的ticket。 -大致操作下载一个抓包工具,打开推栏APP,做一些查询操作。去抓包工具看看发送的请求,参数里面就有ticket。 +许多指令都带有 `[服务器]` 可选参数。用户未填写服务器时,插件会使用这里配置的默认服务器。 -**推送功能** +示例: -配置是否启用,本质就是不停的去接口问数据,数据又更新就推送出来。设定循环请求的间隔时间,不建议太短。下面配置推送的对话列表要要完整的会话ID。例如:QQ:GroupMessage:123456 +```text +奇遇 飞翔大野猪 +``` + +等价于: + +```text +奇遇 飞翔大野猪 梦江南 +``` + +前提是默认服务器配置为 `梦江南`。 + +### JX3API Token + +```json +{ + "jx3api_token": "" +} +``` + +需要令牌的功能必须配置该字段。Token 需要在 [JX3API](https://www.jx3api.com/) 注册并购买后获取。 + +帮助页中标记为“需令牌”的指令,如果未配置 Token,可能无法正常返回数据。 + +### 推栏 Ticket + +```json +{ + "jx3api_ticket": "" +} +``` + +部分接口需要推栏 ticket,例如部分职业、角色、学校类接口。该值通常需要通过抓包推栏 App 的请求参数获得。 + +### 推送配置 + +插件支持 4 类后台推送: + +- `kfts`:开服监控。 +- `xwts`:新闻资讯。 +- `smts`:刷马消息。 +- `ctts`:赤兔消息。 + +每类推送配置都包含: + +- `enable`:是否启用。 +- `time`:轮询间隔,单位秒。 +- `umos`:推送目标会话 ID 列表。 + +示例: + +```json +{ + "kfts": { + "enable": false, + "time": 60, + "umos": ["QQ:GroupMessage:123456"] + } +} +``` + +`umos` 需要填写完整会话 ID,可通过 AstrBot 的 `/std` 等方式获取。推送本质是后台定时请求接口,检测状态变化后向配置的会话发送消息,不建议将轮询间隔设置得过短。 ## 使用方式 -在聊天中输入 **功能** 即可获取插件所有的功能使用说明。 \ No newline at end of file +发送: + +```text +功能 +``` + +即可获取完整指令帮助图。 + +如果开启了指令前缀,则发送: + +```text +剑三 功能 +``` + +指令参数说明: + +- `[]` 表示可选参数。 +- `[服务器]` 未填写时使用插件默认服务器。 +- 数量、天数、模式等参数未填写时使用对应功能的默认值。 + +示例: + +```text +日常 +奇遇 飞翔大野猪 +未做奇遇 飞翔大野猪 梦江南 +战绩 飞翔大野猪 梦江南 33 +贴吧物价 狐金 5 梦江南 +统战 梦江南 +``` + +## 目录结构 + +```text +astrbot_plugin_jx3/ +├─ main.py # AstrBot 插件入口,初始化配置、资源、服务和指令分发 +├─ metadata.yaml # 插件元信息 +├─ _conf_schema.json # AstrBot 后台配置 schema +├─ requirements.txt # Python 依赖 +├─ data/ +│ ├─ api_config.json # 外部接口配置,按功能 key 维护 URL、method、默认参数 +│ └─ plugin_data.db # 插件随包数据,主要用于内置数据 +├─ core/ +│ ├─ jx3_data.py # 业务数据层,调用接口、整理数据、选择模板 +│ ├─ message.py # 消息构建层,负责文本、图片、富媒体回复 +│ ├─ request.py # aiohttp 请求封装,统一处理 GET/POST 和接口返回 +│ ├─ async_task.py # APScheduler 后台推送任务 +│ ├─ bilei_data.py # 本地避雷数据增删改查 +│ ├─ sqlite.py # SQLite 异步封装 +│ └─ fun_basic.py # 模板加载、图片 base64、通用格式化工具 +└─ templates/ + ├─ helps.html # 功能帮助页 + ├─ *.html # 各图片指令的 HTML/Jinja2 模板 + ├─ img/ # 通用图片资源 + ├─ sect/ # 门派/心法图标 + └─ serendipity/ # 奇遇图标 +``` + +## 核心实现逻辑 + +### 1. 插件初始化 + +`main.py` 中的 `Jx3ApiPlugin` 是插件入口。 + +初始化时会完成以下工作: + +1. 读取 AstrBot 插件配置,包括前缀、默认服务器、Token、Ticket 和推送配置。 +2. 计算本地数据目录、插件数据目录、模板目录和接口配置文件路径。 +3. 加载 `templates/img`、`templates/sect`、`templates/serendipity` 中的图片,并转为 base64,供 HTML 模板直接引用。 +4. 初始化 SQLite、JX3Service、AsyncTask、BiLeidata、MessageBuilder。 +5. 在异步初始化阶段创建本地表,并启动后台推送任务。 +6. 构建 `command_map`,将中文触发词映射到对应的消息处理函数。 + +### 2. 指令分发 + +用户消息进入插件后,`main.py` 会: + +1. 根据配置判断是否需要指令前缀。 +2. 将消息按空格切分为 `指令 + 参数`。 +3. 在 `command_map` 中查找对应处理函数。 +4. 使用 `inspect.signature` 根据函数参数数量自动传入用户参数。 +5. 找不到指令时不处理,参数错误时返回提示。 + +### 3. 消息构建 + +`core/message.py` 的 `MessageBuilder` 负责把业务数据发出去,主要有几类输出: + +- `plain_msg()`:发送纯文本。 +- `T2I_image_msg()`:把业务数据传入 HTML 模板,调用 AstrBot 的 HTML 渲染能力生成图片。 +- `image_msg()`:直接发送图片 URL 或图片数据。 +- `plain_chain()`:发送文本 + 图片等富媒体消息链。 +- `handler_plain_image_msg()`:先发文本,再发图片,适用于部分组合型功能。 + +所有可选服务器参数都会通过 `serverdefault()` 补齐默认服务器。 + +### 4. 数据请求与业务处理 + +`core/jx3_data.py` 是主要业务层。每个功能通常对应一个异步方法,例如: + +```python +async def jinqiqiyu(self, server: str) -> Dict[str, Any]: + ... +``` + +业务方法通常遵循统一结构: + +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 +{ + "code": 0, + "msg": "错误提示", + "data": {}, + "temp": "", + "icons": {} +} +``` + +### 5. 接口请求封装 + +`core/request.py` 中的 `APIClient` 负责实际 HTTP 请求。 + +实现特点: + +- 复用 `aiohttp.ClientSession`。 +- 支持 GET 和 POST。 +- 自动识别 JSON、图片或二进制响应。 +- 对 JX3API 常见返回结构做基础校验。 +- 支持通过 `out_key` 抽取返回对象中的指定字段,默认通常取 `data`。 + +`JX3Service._base_request()` 会在业务层进一步统一: + +- 根据 `data/api_config.json` 查找接口。 +- 合并接口默认参数和运行时参数。 +- 根据配置 method 调用 GET 或 POST。 +- 捕获异常并输出日志。 + +### 6. 图片渲染 + +图片输出功能使用 `templates/*.html`。 + +典型数据流: + +```text +JX3Service 整理数据 + ↓ +MessageBuilder.T2I_image_msg() + ↓ +注入 icons + ↓ +html_renderer.render_custom_template() + ↓ +发送图片 +``` + +模板里可以使用: + +- `icons.img`:通用图标。 +- `icons.sect`:门派/心法图标。 +- `icons.serendipity`:奇遇图标。 + +例如门派图标常见写法: + +```jinja2 +{% set icon = icons.sect.get(item.forceName) %} +{% if icon %} + +{% endif %} +``` + +### 7. 本地数据库 + +插件使用两个 SQLite 数据库: + +- 插件目录下的 `data/plugin_data.db`:随插件提供的内置数据。 +- AstrBot 数据目录中的 `local_data.db`:运行期本地数据。 + +本地数据主要用于: + +- 避雷记录。 +- 推送任务状态缓存。 + +`core/sqlite.py` 封装了异步增删改查;`core/bilei_data.py` 基于该封装实现避雷数据管理。 + +### 8. 后台推送 + +`core/async_task.py` 使用 APScheduler 实现轮询推送。 + +当前支持: + +- 开服监控。 +- 新闻资讯。 +- 刷马消息。 +- 赤兔消息。 + +推送逻辑是: + +1. 初始化时读取配置和本地旧状态。 +2. 对启用且配置了推送目标的任务创建 interval job。 +3. 定时调用对应业务方法。 +4. 比较新旧状态。 +5. 状态变化时向配置会话发送消息,并更新本地状态。 + +## 新增功能开发流程 + +新增一个查询功能通常需要改动 4 到 5 个位置: + +1. 在 `data/api_config.json` 添加接口配置。 +2. 在 `core/jx3_data.py` 添加业务方法,完成请求、数据整理和错误处理。 +3. 在 `core/message.py` 添加消息包装方法,选择文本、图片或富媒体输出。 +4. 在 `main.py` 的 `command_map` 注册中文触发指令。 +5. 如果是图片输出,新增 `templates/xxx.html`。 +6. 在 `templates/helps.html` 增加帮助卡片并更新统计。 + +建议保持以下约定: + +- 方法名使用拼音或稳定英文名,避免与现有方法冲突。 +- 接口配置 key 使用 `jx3_功能名` 形式。 +- 文本功能失败时返回清晰的 `msg`。 +- 图片功能返回空数据时不要渲染空图,直接返回文本提示。 +- 可选服务器统一通过 `MessageBuilder.serverdefault()` 处理。 +- 时间戳尽量在业务层格式化后传给模板。 + +## 注意事项 + +- 本插件依赖多个外部接口,接口变更、网络异常、Token 权限不足都会影响结果。 +- JX3API 数据源可能存在延迟或遗漏,奇遇、掉落、排行榜等数据不保证 100% 完整。 +- Token 与 Ticket 属于敏感配置,不要提交到仓库或公开日志。 +- 后台推送会持续请求接口,轮询间隔不要设置过短。 +- HTML 模板渲染依赖 AstrBot 的文转图能力,运行环境需要支持对应渲染服务。 + +## License + +本项目遵循仓库内 `LICENSE` 文件声明的许可协议。 diff --git a/core/jx3_data.py b/core/jx3_data.py index b544272..5b19866 100644 --- a/core/jx3_data.py +++ b/core/jx3_data.py @@ -971,6 +971,62 @@ class JX3Service: 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() @@ -994,6 +1050,47 @@ class JX3Service: 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() @@ -1098,6 +1195,182 @@ class JX3Service: 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() @@ -1138,6 +1411,299 @@ class JX3Service: 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() @@ -2234,6 +2800,79 @@ class JX3Service: 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() diff --git a/core/message.py b/core/message.py index da3b6f0..27fa47e 100644 --- a/core/message.py +++ b/core/message.py @@ -282,11 +282,21 @@ class MessageBuilder: return await self.T2I_image_msg(event, lambda: self.jx3api.zilipaixing(school, self.serverdefault(server))) + 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))) + + async def jx3_shaohua(self, event: AstrMessageEvent,): """剑三 骚话""" return await self.plain_msg(event, self.jx3api.shaohua) + async def jx3_jiemi(self, event: AstrMessageEvent): + """剑三 解密""" + return await self.plain_msg(event, self.jx3api.jiemi) + + async def jx3_shapan(self, event: AstrMessageEvent,server: str = ""): """剑三 沙盘 服务器""" return await self.image_msg(event, lambda: self.jx3api.shapan(self.serverdefault(server))) @@ -302,11 +312,46 @@ class MessageBuilder: return await self.plain_msg(event, lambda: self.jx3api.fuyaojjiutian( self.serverdefault(server))) + async def jx3_zhueevent(self, event: AstrMessageEvent): + """剑三 诛恶事件""" + return await self.T2I_image_msg(event, self.jx3api.zhueevent) + + + async def jx3_benrichitu(self, event: AstrMessageEvent): + """剑三 本日赤兔""" + return await self.plain_msg(event, self.jx3api.benrichitu) + + + async def jx3_benzhouchitu(self, event: AstrMessageEvent): + """剑三 本周赤兔""" + return await self.plain_msg(event, self.jx3api.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)) + async def jx3_zhenyingevent(self, event: AstrMessageEvent): + """剑三 阵营事件""" + return await self.T2I_image_msg(event, self.jx3api.zhenyingevent) + + + async def jx3_guanaishouling(self, event: AstrMessageEvent): + """剑三 关隘首领""" + return await self.T2I_image_msg(event, self.jx3api.guanaishouling) + + + async def jx3_bangzhanjilu(self, event: AstrMessageEvent, server: str = ""): + """剑三 帮战记录 服务器""" + return await self.T2I_image_msg(event, lambda: self.jx3api.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))) + + async def jx3_dilujilu(self, event: AstrMessageEvent,server: str = ""): """剑三 的卢 服务器""" return await self.T2I_image_msg(event, lambda: self.jx3api.dilujilu( self.serverdefault(server))) @@ -520,6 +565,19 @@ class MessageBuilder: return await self.T2I_image_msg(event, lambda: self.jx3api.jiaoyihang(Name, self.serverdefault(server))) + async def jx3_tiebawujia(self, event: AstrMessageEvent, name: str = "狐金", limit_or_server: str = "5", server: str = ""): + """剑三 贴吧物价 物品名称 数量 服务器""" + limit = 5 + target_server = server + + if str(limit_or_server).isdigit(): + limit = int(limit_or_server) + else: + target_server = limit_or_server + + return await self.plain_msg(event, lambda: self.jx3api.tiebawujia(name, limit, self.serverdefault(target_server))) + + async def jx3_diaoluo(self, event: AstrMessageEvent, name: str = "玄晶", limit_or_server: str = "20", server: str = ""): """剑三 掉落 物品名称 数量 服务器""" limit = 20 diff --git a/data/api_config.json b/data/api_config.json index e74d83c..655d768 100644 --- a/data/api_config.json +++ b/data/api_config.json @@ -147,6 +147,16 @@ "token":"" } }, + "jx3_shilianpaixing":{ + "url":"https://www.jx3api.com/data/rank/trials", + "method":"GET", + "description":"查询指定心法的木桩试炼排行榜", + "params":{ + "server": "梦江南", + "name": "花间游", + "token":"" + } + }, "jx3_shaohua":{ "url":"https://www.jx3api.com/data/saohua/random", "method":"GET", @@ -155,6 +165,14 @@ "name": "万花" } }, + "jx3_jiemi":{ + "url":"https://www.jx3api.com/data/mech/calculator", + "method":"GET", + "description":"查询机关解密结果", + "params":{ + "token": "" + } + }, "aijx3_shapan":{ "url":"https://www.jianxiachaguan.cn/api2/aijx3-jxcg/game/get-sand-table-img", "method":"POST", @@ -180,6 +198,30 @@ "token": "" } }, + "jx3_zhueevent":{ + "url":"https://www.jx3api.com/data/smite/records", + "method":"GET", + "description":"查询诛恶事件记录", + "params":{ + "token": "" + } + }, + "jx3_benrichitu":{ + "url":"https://www.jx3api.com/data/chitu/records", + "method":"GET", + "description":"查询本日赤兔记录", + "params":{ + "token": "" + } + }, + "jx3_benzhouchitu":{ + "url":"https://www.jx3api.com/data/chitu/week/records", + "method":"GET", + "description":"查询本周赤兔记录", + "params":{ + "token": "" + } + }, "jx3_zhengyingpaimai":{ "url":"https://www.jx3api.com/data/auction/records", "method":"GET", @@ -191,6 +233,40 @@ "token": "" } }, + "jx3_zhenyingevent":{ + "url":"https://www.jx3api.com/data/fenxian/records", + "method":"GET", + "description":"查询跨服阵营事件记录", + "params":{ + "token": "" + } + }, + "jx3_guanaishouling":{ + "url":"https://www.jx3api.com/data/mine/cart", + "method":"GET", + "description":"查询各服务器关隘首领刷新记录", + "params":{ + "token": "" + } + }, + "jx3_bangzhanjilu":{ + "url":"https://www.jx3api.com/data/battle/records", + "method":"GET", + "description":"查询指定服务器帮战记录", + "params":{ + "server": "梦江南", + "token": "" + } + }, + "jx3_tongzhanyy":{ + "url":"https://www.jx3api.com/data/duowan/statistics", + "method":"GET", + "description":"查询指定服务器统战歪歪信息", + "params":{ + "server": "梦江南", + "token": "" + } + }, "jx3_dilujilu":{ "url":"https://www.jx3api.com/data/steed/records", "method":"GET", @@ -424,6 +500,17 @@ "token": "" } }, + "jx3_tiebawujia":{ + "url":"https://www.jx3api.com/data/tieba/item/records", + "method":"GET", + "description":"查询指定物品的贴吧价格记录", + "params":{ + "server": "梦江南", + "name": "狐金", + "limit": 5, + "token": "" + } + }, "jx3_diaoluo":{ "url":"https://www.jx3api.com/data/reward/statistics", "method":"GET", diff --git a/main.py b/main.py index 93c5d72..dd1f268 100644 --- a/main.py +++ b/main.py @@ -17,7 +17,7 @@ from .core.fun_basic import load_as_base64 @register("astrbot_plugin_jx3", "fxdyz", "通过调用剑网三API接口获取游戏数据,处理发送。", - "2.3", + "2.5", "https://github.com/qsc20001102/astrbot_plugin_jx3api" ) class Jx3ApiPlugin(Star): @@ -204,14 +204,23 @@ class Jx3ApiPlugin(Star): "奇穴": self.jx3cmd.jx3_qixue, "技能": self.jx3cmd.jx3_jineng, "资历排行": self.jx3cmd.jx3_zilipaixing, + "试炼排行": self.jx3cmd.jx3_shilianpaixing, "骚话": self.jx3cmd.jx3_shaohua, + "解密": self.jx3cmd.jx3_jiemi, "沙盘": self.jx3cmd.jx3_shapan, "攻略": self.jx3cmd.jx3_qiyugonglue, "宏": self.jx3cmd.jx3_hong, "配装": self.jx3cmd.jx3_peizhuang, "百战": self.jx3cmd.jx3_baizhan, "扶摇": self.jx3cmd.jx3_fuyaojjiutian, + "诛恶事件": self.jx3cmd.jx3_zhueevent, + "本日赤兔": self.jx3cmd.jx3_benrichitu, + "本周赤兔": self.jx3cmd.jx3_benzhouchitu, "拍卖": self.jx3cmd.jx3_zhengyingpaimai, + "阵营事件": self.jx3cmd.jx3_zhenyingevent, + "关隘首领": self.jx3cmd.jx3_guanaishouling, + "帮战记录": self.jx3cmd.jx3_bangzhanjilu, + "统战": self.jx3cmd.jx3_tongzhanyy, "的卢": self.jx3cmd.jx3_dilujilu, "烟花": self.jx3cmd.jx3_yanhuachaxun, "骗子": self.jx3cmd.jx3_pianzhi, @@ -253,6 +262,7 @@ class Jx3ApiPlugin(Star): "物价": self.jx3cmd.jx3_wujia, "八卦": self.jx3cmd.jx3_bagua, "交易行": self.jx3cmd.jx3_jiaoyihang, + "贴吧物价": self.jx3cmd.jx3_tiebawujia, "掉落": self.jx3cmd.jx3_diaoluo, "开服推送": self.jx3cmd.jx3_kaifhujiank, "新闻推送": self.jx3cmd.jx3_xinwenzhixun, diff --git a/metadata.yaml b/metadata.yaml index 8b59e5d..7bed29e 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,7 +1,7 @@ name: astrbot_plugin_jx3 display_name: 剑网三游戏数据查询工具 desc: 通过接口调用剑网三API接口获取游戏数据,处理发送。 -version: v2.4 +version: v2.5 author: 飞翔大野猪 repo: https://github.com/qsc20001102/astrbot_plugin_jx3 astrbot_version: ">=4.11.0" diff --git a/templates/bangzhanjilu.html b/templates/bangzhanjilu.html new file mode 100644 index 0000000..2f3b08d --- /dev/null +++ b/templates/bangzhanjilu.html @@ -0,0 +1,134 @@ + + + + +帮战记录 + + + + + +
+

帮战记录

+
{{ server }} | 更新时间:{{ update_time }}
+ + + + + + + + + + + + + + + + {% for item in items %} + + + + + + + + + + {% endfor %} + +
区服服务器宣战帮会应战帮会开始时间持续时间结束时间
{{ item.zoneName }}{{ item.serverName }}{{ item.declaringTongName }}{{ item.acceptingTongName }}{{ item.startTime }}{{ item.matchDuration }}{{ item.endTime }}
+
+ + + diff --git a/templates/guanaishouling.html b/templates/guanaishouling.html new file mode 100644 index 0000000..6d09608 --- /dev/null +++ b/templates/guanaishouling.html @@ -0,0 +1,177 @@ + + + + +关隘首领 + + + + + +
+

关隘首领

+
更新时间:{{ update_time }}
+ + {% for group in groups %} +
+
{{ group.server }}
+ + + + + + + + + + + + + + {% for item in group.records %} + + + + + + + + + {% endfor %} + +
归属关隘名称状态开始时间结束时间剩余时间
{{ item.camp_name }}{{ item.castle }}{{ item.str_status }}{{ item.start_time }}{{ item.end_time }}{{ item.remaining_time }}
+
+ {% endfor %} +
+ + + diff --git a/templates/helps.html b/templates/helps.html index f0dcad6..ce99ee0 100644 --- a/templates/helps.html +++ b/templates/helps.html @@ -321,13 +321,13 @@ body { .usage { display: inline-block; max-width: 100%; - padding: 5px 8px; + padding: 6px 9px; border-radius: 9px; background: rgba(23, 32, 51, 0.055); color: #24324f; font-family: Consolas, "SFMono-Regular", "Courier New", monospace; - font-size: 12px; - line-height: 1.35; + font-size: 14px; + line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -369,7 +369,7 @@ body {

剑三机器人功能指令

按使用场景重新分组,保留每条指令的名称、使用示例与说明。卡片底色表示指令是否需要令牌,免令牌与需令牌功能一眼区分。

-
77条指令
+
87条指令
16个功能分组
@@ -491,9 +491,9 @@ body {

杂项功能

- 6 条 + 7 条 免令牌 4 - 需令牌 2 + 需令牌 3
@@ -542,6 +542,15 @@ body { 八卦 类型

查询 818 / 616 / 避雷等八卦记录。

+ +
+
+
解密
+
需令牌
+
+ 解密 +

查询当前机关解密结果与下轮时间。

+
@@ -633,9 +642,9 @@ body {

交易功能

@@ -666,6 +675,15 @@ body { 交易行 物品名称 [服务器]

查询指定物品交易行价格与挂单信息。

+ +
+
+
贴吧物价
+
需令牌
+
+ 贴吧物价 物品名称 [记录数量] [服务器] +

查询指定物品的贴吧交易记录。

+
@@ -801,9 +819,9 @@ body {

阵营功能

- 2 条 + 6 条 免令牌 1 - 需令牌 1 + 需令牌 5
@@ -826,6 +844,42 @@ body {

查询阵营拍卖物品及服务器记录。

+
+
+
阵营事件
+
需令牌
+
+ 阵营事件 +

查询跨服阵营事件记录。

+
+ +
+
+
关隘首领
+
需令牌
+
+ 关隘首领 +

查询各分线关隘首领状态与剩余时间。

+
+ +
+
+
帮战记录
+
需令牌
+
+ 帮战记录 [服务器] +

查询指定服务器帮战记录。

+
+ +
+
+
统战歪歪
+
需令牌
+
+ 统战 [服务器] +

查询指定服务器阵营统战 YY 信息。

+
+
@@ -977,9 +1031,9 @@ body {

活动功能

- 3 条 + 6 条 - 需令牌 3 + 需令牌 6
@@ -993,6 +1047,33 @@ body {

查询指定服务器扶摇九天活动记录。

+
+
+
诛恶事件
+
需令牌
+
+ 诛恶事件 +

查询诛恶事件服务器、地图和时间记录。

+
+ +
+
+
本日赤兔
+
需令牌
+
+ 本日赤兔 +

查询本日赤兔时间、区服和地图记录。

+
+ +
+
+
本周赤兔
+
需令牌
+
+ 本周赤兔 +

查询本周赤兔时间、区服和地图记录。

+
+
马场刷新
@@ -1065,9 +1146,9 @@ body {

排行功能

@@ -1080,6 +1161,15 @@ body { 资历排行 职业 [服务器]

查询指定职业资历排行榜。

+ +
+
+
试炼排行
+
需令牌
+
+ 试炼排行 心法 [服务器] +

查询指定心法木桩试炼排行榜。

+
@@ -1359,4 +1449,4 @@ body { - \ No newline at end of file + diff --git a/templates/shilianpaixing.html b/templates/shilianpaixing.html new file mode 100644 index 0000000..a2b0ac1 --- /dev/null +++ b/templates/shilianpaixing.html @@ -0,0 +1,120 @@ + + + + +试炼排行 + + + + + +
+

{{ name }} 试炼排行

+
{{ server }} | 更新时间:{{ update_time }}
+ + + + + + + + + + + + + + {% for item in items %} + + + + + + + + {% endfor %} + +
排名角色名装备分数最高层数排名积分
{{ loop.index }}{{ item.role_name }}{{ item.equip_score }}{{ item.max_level }}{{ item.total_score }}
+
+ + + diff --git a/templates/zhenyingevent.html b/templates/zhenyingevent.html new file mode 100644 index 0000000..0ed27c7 --- /dev/null +++ b/templates/zhenyingevent.html @@ -0,0 +1,151 @@ + + + + +阵营事件 + + + + + +
+

阵营事件

+
更新时间:{{ update_time }}
+ + + + + + + + + + + + + + {% for item in items %} + + + + + + + + {% endfor %} + +
阵营分线服务器角色名称时间
{{ item.camp_name }}{{ item.fenxian_name }}{{ item.friend_name }}{{ item.role_name }}{{ item.seize_time }}
+
+ + + diff --git a/templates/zhueevent.html b/templates/zhueevent.html new file mode 100644 index 0000000..0df3790 --- /dev/null +++ b/templates/zhueevent.html @@ -0,0 +1,116 @@ + + + + +诛恶事件 + + + + + +
+

诛恶事件

+
更新时间:{{ update_time }}
+ + + + + + + + + + + + {% for item in items %} + + + + + + {% endfor %} + +
服务器地图时间
{{ item.server }}{{ item.map_name }}{{ item.time }}
+
+ + +