diff --git a/CHANGELOG.md b/CHANGELOG.md index a880825..eaee031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ ## 更新记录 +### version: 3.4.1(2026-09-04): + +带区服参数的查询新增 `全区` 保留值;即使当前会话已绑定区服,也会向接口传入空区服以查询全区数据。 + +资历分布由 JX3BOX 重构为 JX3API `/tuilan/achievement` 实时查询,使用固定的总览及 18 个大类菜单;模板直接读取角色信息、`data.total`、`pieces`、`seniority` 与 `score/totalScore`,并分别兼容总览的“大类 → 小类 → 统计”和指定分类的“小类 → 统计”返回结构。 + +重构通用两轮会话处理器:首轮函数直接返回 `{"1": "总览", "2": "杂闻"}` 形式的裸键值对,由处理器生成序号文本;用户选择或 10 秒超时后,将所选项裁剪成单项字典交给次轮函数。资历和宏查询均已接入,宏结果统一复用富媒体消息链发送文本及 HTML 渲染附图。 + +移除 JX3BOX 资历菜单、点数和角色成就拼装逻辑及其本地缓存;旧 `achievement_cache` 仅迁移交易行物品分组后删除,交易行缓存独立保存到 `trade_item_cache`。 + +提高插件指令监听优先级;确认消息属于本插件指令后立即停止后续事件传播,避免重复触发其他插件或默认 LLM 回复。 + +补齐 JX3API 当前 39 种 WebSocket 事件处理,付费事件不再回退输出原始 JSON;新增缺失的扶摇预告、烟花报时、阵营祭天预告及帮会野外宣战事件,并按官方字段生成可读中文消息。 + +统一查询图片和插件 WebUI 的表格样式:表头、内容及图标文字组合水平和垂直居中,移除固定列宽并按内容自动分配;合并跨页面可复用的表格、卡片、状态和布局规则,清理重复或失效的页面 CSS。 + ### version: 3.4.0: 移除插件级默认区服,新增按 AstrBot 会话保存的区服绑定。已绑定会话可省略查询指令中的区服参数,完整填写区服时仍优先查询指定区服。 diff --git a/README.md b/README.md index a2ac70f..8b240bb 100644 --- a/README.md +++ b/README.md @@ -15,19 +15,19 @@ - 中文触发词覆盖活动、名剑、排行、交易、阵营、角色、奇遇、副本、家园、社区等场景。 - 支持纯文本、远程图片、HTML/Jinja2 渲染图片和图文消息链。 -- 支持 `宏`、`资历` 两类 30 秒两轮交互。 +- 支持 `宏`、`资历` 两类 10 秒两轮交互,超时默认选择第一项。 - 支持本地 SQLite 避雷记录的增删改查。 - 支持按 AstrBot 会话分别开启总开关和具体 JX3API 实时事件订阅。 - 复用 `aiohttp.ClientSession`,统一处理 GET、POST、JSON、图片和分页请求。 -- JX3BOX 的 Node、Next2、CMS 请求统一封装,资历与交易行基础数据支持本地快照缓存和过期兜底。 +- JX3BOX 的 Node、Next2、CMS 请求统一封装,交易行基础物品数据支持本地快照缓存和过期兜底。 - 内置 47 个页面片段,通过公共布局与样式在本地组装为完整 HTML,并附带通用、沙盘、门派/心法和奇遇图标资源。 ## 数据来源 | 数据源 | 代码入口 | 主要用途 | | --- | --- | --- | -| [JX3API](https://www.jx3api.com/) | `core/jx3api_data.py`、`core/event_push.py` | 游戏查询、沙盘据点数据及 WebSocket 实时事件 | -| [JX3BOX](https://www.jx3box.com/) | `core/jx3box_data.py` | 奇遇攻略、配装、宏、资历及交易行 | +| [JX3API](https://www.jx3api.com/) | `core/jx3api_data.py`、`core/event_push.py` | 游戏查询、资历分布、沙盘据点数据及 WebSocket 实时事件 | +| [JX3BOX](https://www.jx3box.com/) | `core/jx3box_data.py` | 奇遇攻略、配装、宏及交易行 | | 本地 SQLite | `core/sqlite.py`、`core/bilei_data.py` | 心法别名、避雷记录、事件订阅和基础数据缓存 | 外部数据源的可用性、数据时效和字段结构均不由本插件控制。接口变更、网络异常、凭据权限不足或上游限流都可能导致查询失败。 @@ -117,7 +117,7 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通 - 当前分发器按空白切分消息,因此角色名、物品名、备注等单个参数不能包含空格。 - 未识别的消息会被忽略;已识别的指令会停止继续传播给其他插件。 - 缺少必填参数、数字转换失败或执行异常时,入口层统一回复 `参数错误或执行失败`。 -- 未绑定区服时按指令表提供标准参数。绑定后可以省略 `server` 参数;显式填写完整参数时仍优先使用本次输入的区服。 +- 未绑定区服时按指令表提供标准参数。绑定后可以省略 `server` 参数;显式填写完整参数时仍优先使用本次输入的区服。在区服参数位置填写 `全区` 时,会忽略会话绑定并向接口传入空区服以查询全区数据。 - 标准区服名和 WebUI 中配置的别名都可用于查询。对于区服后的可选参数,分发器通过当前有效区服目录和别名判断首个参数是显式区服还是后续参数。 ### 会话区服绑定 @@ -128,7 +128,7 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通 | `绑定区服 梦江南` | 将当前会话绑定到指定区服;支持区服别名 | | `解绑区服` | 解除当前会话绑定 | -例如会话已经绑定 `梦江南` 后,`战绩 角色名 33` 等价于 `战绩 梦江南 角色名 33`;仍可发送 `战绩 唯我独尊 角色名 33` 临时查询其他区服。 +例如会话已经绑定 `梦江南` 后,`战绩 角色名 33` 等价于 `战绩 梦江南 角色名 33`;仍可发送 `战绩 唯我独尊 角色名 33` 临时查询其他区服,或发送 `战绩 全区 角色名 33` 查询全区数据。 ## 指令参考 @@ -235,10 +235,10 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通 | `资历排行 [服务器] [门派]` | 资历排行榜;图片 | Token + Ticket | | `技能 心法` | 心法技能;图片 | Token + Ticket | | `奇穴 心法` | 心法奇穴;图片 | Token + Ticket | -| `宏 心法` | 先返回宏列表,30 秒内回复序号后返回宏文本和帖子内容 | 无 | -| `资历 服务器 角色` | 先返回 `0-18` 分类菜单,30 秒内回复序号后渲染资历进度图 | Token | +| `宏 心法` | 先返回动态宏列表,10 秒内回复序号后返回宏文本和帖子内容;超时默认选择热度第一项 | 无 | +| `资历 服务器 角色` | 返回固定资历分类菜单,10 秒内回复序号后通过 JX3API 渲染资历进度图 | Token + Ticket | -`资历` 的 `0` 表示总览,`1-18` 依次对应杂闻、武学、修为、装备、技艺、阅读、任务、足迹、战斗、声望、秘境、帮会、阵营、节日、活动、风雨江湖路、家园和剑侠录。第二轮只需回复数字,不需要再次添加插件前缀。 +`资历` 的 `1` 表示总览,`2-19` 固定对应杂闻、武学、修为、装备、技艺、阅读、任务、足迹、战斗、声望、秘境、帮会、阵营、节日、活动、风雨江湖路、家园和剑侠录,不再使用本地资历菜单和点数缓存。第二轮只需回复数字,不需要再次添加插件前缀;10 秒未选择时默认把首项 `{"1": "总览"}` 交给次轮查询并继续渲染。 ### 游戏社区与休闲 @@ -300,7 +300,7 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通 | `事件推送 2001 关闭` | 为当前会话取消订阅事件 `2001` | | `事件推送 列表` | 查看支持的全部事件编号 | -总开关和具体事件开关必须同时开启,消息才会发送到当前会话。事件正文包含 `server` 时,只向绑定该区服和未绑定区服的订阅会话发送;不含服务器的新闻、版本更新等事件不受区服过滤影响。免费事件为:`2001` 开服状态、`2002` 官方新闻、`2003` 版本更新、`2004` 八卦速报、`2005` 关隘首领、`2006` 云从预告。其他事件需要独立的事件版令牌。 +总开关和具体事件开关必须同时开启,消息才会发送到当前会话。事件正文包含 `server` 时,只向绑定该区服和未绑定区服的订阅会话发送;不含服务器的新闻、版本更新等事件不受区服过滤影响。插件已覆盖 JX3API 当前 39 种 WebSocket 事件,并按各事件的官方字段输出可读中文消息。免费事件为:`2001` 开服状态、`2002` 官方新闻、`2003` 版本更新、`2004` 八卦速报、`2005` 关隘首领、`2006` 云丛预告;其他事件需要独立的事件版令牌,可通过 `事件推送 列表` 查看全部编号。 ### WebUI 插件管理 @@ -344,7 +344,7 @@ flowchart LR - `session_server_bindings`:两列结构,保存会话 ID 与绑定区服。 - `server_aliases`:保存标准区服名及其 JSON 别名列表。 - `kungfu`:保存 JX3BOX 配装 ID、标准心法名及最多 5 个别名。 -- `achievement_cache`:JSON 基础数据缓存及更新时间。 +- `trade_item_cache`:JX3BOX 交易行基础物品数据缓存及更新时间。 `kungfu` 表首次创建时从 `data/kungfu.json` 幂等导入 32 条默认心法,`server_aliases` 表从 `data/server_aliases.json` 幂等导入默认区服别名;已存在的本地记录均不会被覆盖。随后启动 JX3API WebSocket 事件通道并建立指令映射。插件停用时会关闭事件通道、两个 HTTP Session 和本地 SQLite 连接。 @@ -364,8 +364,8 @@ flowchart LR 两个数据服务按上游来源拆分: -- `JX3APIService`:以 `https://www.jx3api.com` 为根地址,通过 GET 请求实现主体功能和沙盘据点查询。 -- `JX3BOXService`:通过统一请求入口访问 JX3BOX 的 Node、CMS 和 Next2 服务,负责攻略、配装、宏、资历及交易行。 +- `JX3APIService`:以 `https://www.jx3api.com` 为根地址,通过 GET 请求实现主体功能、资历分布和沙盘据点查询。 +- `JX3BOXService`:通过统一请求入口访问 JX3BOX 的 Node、CMS 和 Next2 服务,负责攻略、配装、宏及交易行。 服务方法通常返回统一结构: @@ -402,9 +402,9 @@ flowchart LR - `plain_msg()`:纯文本。 - `T2I_image_msg()`:向模板注入业务数据和本地图标,再调用 AstrBot HTML 渲染器生成 JPEG。 - `image_msg()`:直接发送远程图片 URL 或图片数据。 -- `plain_chain()`:发送文本与多张图片组成的消息链。 -- `handler_plain_image_msg()`:宏查询的两轮会话。 -- `handler_zili_msg()`:资历查询的两轮会话。 +- `plain_chain()`:发送文本与图片组成的富媒体消息链;结果包含 HTML 正文时,会将其渲染为图片组件追加到消息链。 +- `handler_plain_image_msg()`:通用两轮会话;首轮函数直接返回 `{"1": "总览", "2": "杂闻"}` 形式的数字键值对(无 `code`、`msg`、`data` 外层),处理器据此生成序号文本;选择后裁剪为只含所选项的新字典交给次轮函数,再由注入的输出器发送结果;超时固定选择第一项。 +- `zili()`:在指令入口直接调用上述通用两轮会话处理器,并通过标准图片流程输出结果。 图片默认使用质量 `100`、完整页面截图和普通设备缩放级别。模板可通过 `icons.img`、`icons.sect`、`icons.serendipity` 访问通用、门派/心法和奇遇图标。 @@ -448,11 +448,11 @@ AstrBot 的渲染接口接收完整 HTML 字符串,因此插件不会依赖渲 | `data/kungfu.json` | 随插件分发,只在初始化时读取 | 32 条默认心法、别名和 JX3BOX 配装 ID | | `data/server_aliases.json` | 随插件分发,只在初始化时读取 | 默认标准区服及其别名 | -`achievement_cache` 同时被资历基础数据和交易行物品分组复用。每个接口快照以一条 JSON 记录保存,当前使用 `achievement_menus`、`achievement_points` 和 `trade_item_groups` 三个键。缓存有效期为 30 天;缓存过期后优先全量刷新,上游请求失败时继续使用可解析的旧缓存兜底。资历菜单与点数的刷新接口分别为 JX3BOX Node 的 `/api/node/achievement/menus` 和 `/api/node/achievement/points`。 +`trade_item_cache` 只保存 JX3BOX 交易行物品分组快照,当前键为 `trade_item_groups`。缓存有效期为 30 天;缓存过期后优先全量刷新,上游请求失败时继续使用可解析的旧缓存兜底。升级时会从旧 `achievement_cache` 迁移交易行缓存并删除旧表,历史资历菜单和点数缓存不会继续保留。 ### 7. 实时事件推送 -`core/event_push.py` 与 JX3API WebSocket 保持长连接,每 30 秒发送协议心跳。连接中断后按 1、2、4、8 秒递增重试,最大间隔为 30 秒;插件卸载时会取消心跳和重连任务并关闭连接。 +`core/event_push.py` 与 JX3API WebSocket 保持长连接,每 30 秒发送协议心跳。连接中断后按 1、2、4、8 秒递增重试,最大间隔为 30 秒;插件卸载时会取消心跳和重连任务并关闭连接。当前支持官方 `SocketEventMap` 中的全部 39 种事件,包含奇遇、马驹、扶摇、烟花、的卢、玄晶、阵营、宣战、据点、攻防拍卖与免费资讯事件。 每条消息按 `action` 识别事件,正文优先读取 `detail`,同时兼容 `data`。分发时联查会话区服绑定:正文包含 `server` 时仅保留同区服或未绑定区服的接收会话,正文不含 `server` 时不做区服过滤。之后只有会话的 `enabled=1` 且对应 `action_编号=1` 时才发送。会话 ID 直接取当前消息的 `unified_msg_origin`。 @@ -525,10 +525,10 @@ git diff --check ## 当前版本状态 -以下内容是对 v3.4.0 当前源码的静态核对结果,部署和二次开发前应注意: +以下内容是对 v3.4.1 当前源码的静态核对结果,部署和二次开发前应注意: 1. 带区服参数的指令在入口层统一进行会话绑定补齐和别名解析;新增此类指令时参数名应继续使用 `server`。 -2. `JX3BOXService` 通过统一的 `_base_request()` 分发 Node、Next2 和 CMS 请求;资历菜单与点数缓存过期后分别从 `/api/node/achievement/menus` 和 `/api/node/achievement/points` 刷新。 +2. 资历分布使用固定的“数字序号 → 大类名称”首轮菜单,通用会话裁剪出单项字典后交给 JX3API `/tuilan/achievement` 次轮查询;模板兼容总览与指定 `subclass` 的不同 `data.total` 层级,并直接使用接口角色字段、`pieces`、`seniority` 和 `score/totalScore` 渲染,JX3BOX 不再参与资历查询。 3. `APIClient` 当前默认 `ssl_verify=False`,即外部 HTTPS 请求不校验证书。对传输安全有要求的部署应先评估并调整该设置。 4. JX3API 服务初始化时会把 Token 和 Ticket 写入 debug 日志。不要公开调试日志,建议二次开发时移除敏感值输出。 diff --git a/core/event_push.py b/core/event_push.py index 1f619ce..de1ffdc 100644 --- a/core/event_push.py +++ b/core/event_push.py @@ -23,23 +23,24 @@ EVENT_NAMES = { 1001: "奇遇触发", 1002: "马驹刷新", 1003: "马驹捕获", + 1004: "扶摇预告", 1005: "扶摇开启", 1006: "扶摇点名", - 1008: "的卢每日", + 1007: "烟花报时", + 1008: "的卢预告", 1009: "的卢刷新", 1010: "的卢捕获", 1011: "的卢拍卖", - 1012: "副本掉落 / 私货", + 1012: "玄晶报时", 1013: "阵营拍卖", 1014: "诛恶事件", 1015: "追魂点名", - 1017: "阵营祭祀", - 1018: "关隘首领", + 1016: "阵营祭天预告", + 1017: "阵营祭天终点", 1101: "领地宣战·开始", 1102: "领地宣战·结束", - 1103: "帮会宣战·开始", - 1104: "帮会宣战·结束", - 1105: "帮会约战·完胜", + 1108: "帮会野外宣战·开始", + 1109: "帮会野外宣战·结束", 1111: "抢占粮仓", 1112: "大旗重置", 1113: "大旗被夺", @@ -52,16 +53,237 @@ EVENT_NAMES = { 1120: "小攻防分红", 1121: "大攻防分红", 1122: "大攻防分红(含指挥)", - 1201: "微博更新", 2001: "开服状态", 2002: "官方新闻", 2003: "版本更新", 2004: "八卦速报", 2005: "关隘首领", - 2006: "云从预告", + 2006: "云丛预告", } EVENT_ACTIONS = tuple(EVENT_NAMES) +SERVER_FIELDS = ( + ("大区", "zone", "text"), + ("服务器", "server", "text"), +) +EVENT_FIELDS = { + 1001: SERVER_FIELDS + ( + ("角色", "name", "text"), + ("奇遇", "event", "text"), + ("等级", "level", "text"), + ("时间", "time", "time"), + ), + 1002: SERVER_FIELDS + ( + ("地图", "map_name", "text"), + ("刷新时间", "time", "time"), + ), + 1003: SERVER_FIELDS + ( + ("名称", "name", "text"), + ("地图", "map_name", "text"), + ("马驹", "horse", "text"), + ("等级", "level", "text"), + ("捕获时间", "time", "time"), + ), + 1004: SERVER_FIELDS + (("预告时间", "time", "time"),), + 1005: SERVER_FIELDS + (("开启时间", "time", "time"),), + 1006: SERVER_FIELDS + ( + ("点名角色", "name", "list"), + ("时间", "time", "time"), + ), + 1007: SERVER_FIELDS + ( + ("燃放者", "sender", "text"), + ("接收者", "receiver", "text"), + ("烟花", "firework", "text"), + ("地图", "map_name", "text"), + ("时间", "time", "time"), + ), + 1008: SERVER_FIELDS + ( + ("马驹", "name", "text"), + ("地图", "map_name", "text"), + ("预告时间", "time", "time"), + ), + 1009: SERVER_FIELDS + ( + ("马驹", "name", "text"), + ("地图", "map_name", "text"), + ("刷新时间", "refresh_time", "time"), + ), + 1010: SERVER_FIELDS + ( + ("马驹", "name", "text"), + ("地图", "map_name", "text"), + ("捕获角色", "capture_role_name", "text"), + ("角色阵营", "capture_camp_name", "text"), + ("捕获时间", "capture_time", "time"), + ), + 1011: SERVER_FIELDS + ( + ("马驹", "name", "text"), + ("竞拍角色", "auction_role_name", "text"), + ("角色阵营", "auction_camp_name", "text"), + ("成交金额", "auction_amount", "text"), + ("拍卖时间", "auction_time", "time"), + ), + 1012: SERVER_FIELDS + ( + ("角色", "role_name", "text"), + ("副本", "map_name", "text"), + ("物品", "item_name", "text"), + ("时间", "time", "time"), + ), + 1013: SERVER_FIELDS + ( + ("竞拍角色", "role_name", "text"), + ("阵营", "camp_name", "text"), + ("物品", "item_name", "text"), + ("成交金额", "item_amount", "text"), + ("时间", "time", "time"), + ), + 1014: SERVER_FIELDS + ( + ("地图", "map_name", "text"), + ("时间", "time", "time"), + ), + 1015: SERVER_FIELDS + ( + ("角色所在服", "role_server", "text"), + ("点名角色", "role_name", "text"), + ("时间", "time", "time"), + ), + 1016: SERVER_FIELDS + (("预告时间", "time", "time"),), + 1017: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("帮会", "tong_name", "text"), + ("角色", "role_name", "text"), + ("据点", "castle_name", "text"), + ("时间", "time", "time"), + ), + 1101: SERVER_FIELDS + ( + ("战场类型", "battlefield_type", "text"), + ("宣战帮会", "declaring_tong_name", "text"), + ("应战帮会", "accepting_tong_name", "text"), + ("领地帮会", "battlefield_tong_name", "text"), + ("开始时间", "start_time", "time"), + ), + 1102: SERVER_FIELDS + ( + ("战场类型", "battlefield_type", "text"), + ("宣战帮会", "declaring_tong_name", "text"), + ("应战帮会", "accepting_tong_name", "text"), + ("领地帮会", "battlefield_tong_name", "text"), + ("获胜帮会", "victory_tong_name", "text"), + ("获胜积分", "victory_score", "text"), + ("结束时间", "end_time", "time"), + ), + 1108: SERVER_FIELDS + ( + ("战场类型", "battlefield_type", "text"), + ("宣战帮会", "declaring_tong_name", "text"), + ("应战帮会", "accepting_tong_name", "text"), + ("持续时长(小时)", "duration_hours", "text"), + ("开始时间", "start_time", "time"), + ), + 1109: SERVER_FIELDS + ( + ("战场类型", "battlefield_type", "text"), + ("宣战帮会", "declaring_tong_name", "text"), + ("应战帮会", "accepting_tong_name", "text"), + ("结束时间", "end_time", "time"), + ), + 1111: SERVER_FIELDS + ( + ("据点", "castle_name", "text"), + ("阵营", "camp_name", "text"), + ("时间", "time", "time"), + ), + 1112: SERVER_FIELDS + ( + ("据点", "castle_name", "text"), + ("时间", "time", "time"), + ), + 1113: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("地图", "map_name", "text"), + ("据点", "castle_name", "text"), + ("时间", "time", "time"), + ), + 1114: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("帮会", "tong_name", "text"), + ("据点", "castle_name", "text"), + ("时间", "time", "time"), + ), + 1115: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("据点", "castle_name", "text"), + ("时间", "time", "time"), + ), + 1116: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("贡献帮会", "tong_name", "list"), + ("时间", "time", "time"), + ), + 1117: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("贡献帮会", "tong_name", "list"), + ("时间", "time", "time"), + ), + 1118: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("贡献帮会", "tong_name", "list"), + ("时间", "time", "time"), + ), + 1119: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("竞拍角色", "role_name", "text"), + ("物品", "item_name", "text"), + ("成交金额", "item_amount", "text"), + ("时间", "time", "time"), + ), + 1120: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("分红帮会", "tong_name", "list"), + ("分红金额", "split_amount", "text"), + ("时间", "time", "time"), + ), + 1121: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("分红帮会", "tong_name", "list"), + ("分红金额", "split_amount", "text"), + ("时间", "time", "time"), + ), + 1122: SERVER_FIELDS + ( + ("阵营", "camp_name", "text"), + ("指挥帮会", "chief_tong_name", "text"), + ("分红帮会", "tong_name", "list"), + ("分红金额", "split_amount", "text"), + ("时间", "time", "time"), + ), + 2001: SERVER_FIELDS + ( + ("状态", "status", "status"), + ("时间", "time", "time"), + ), + 2002: ( + ("类型", "type", "text"), + ("标题", "title", "text"), + ("日期", "date", "text"), + ("链接", "url", "text"), + ), + 2003: ( + ("当前版本", "now_version", "text"), + ("最新版本", "new_version", "text"), + ("更新包数量", "package_num", "text"), + ("更新大小", "package_size", "text"), + ), + 2004: ( + ("分类", "tags", "text"), + ("服务器", "server", "text"), + ("发布者", "name", "text"), + ("标题", "title", "text"), + ("日期", "date", "text"), + ("链接", "url", "text"), + ), + 2005: ( + ("服务器", "server", "text"), + ("关卡", "stage", "text"), + ("开始时间", "start", "time"), + ), + 2006: ( + ("事件", "name", "text"), + ("地点", "site", "text"), + ("说明", "desc", "text"), + ("时间", "time", "time"), + ), +} + class EventPushService: """JX3API WebSocket 事件接收、会话订阅与消息分发。""" @@ -424,63 +646,30 @@ class EventPushService: return str(value or "未知") def _format_event(self, action: int, detail: dict[str, Any]) -> str: - if action == 2001: - raw_status = detail.get("status") - server_status = {0: "维护", 1: "开服", "0": "维护", "1": "开服"}.get( - raw_status, - str(raw_status or "未知"), - ) - return ( - "【开服状态】\n" - f"大区:{detail.get('zone', '未知')}\n" - f"服务器:{detail.get('server', '未知')}\n" - f"状态:{server_status}\n" - f"时间:{self._format_timestamp(detail.get('time'))}" - ) - if action == 2002: - return ( - "【官方新闻】\n" - f"类型:{detail.get('type', '未知')}\n" - f"标题:{detail.get('title', '未知')}\n" - f"日期:{detail.get('date', '未知')}\n" - f"链接:{detail.get('url', '无')}" - ) - if action == 2003: - return ( - "【版本更新】\n" - f"当前版本:{detail.get('now_version', '未知')}\n" - f"最新版本:{detail.get('new_version', '未知')}\n" - f"更新包数量:{detail.get('package_num', '未知')}\n" - f"更新大小:{detail.get('package_size', '未知')}" - ) - if action == 2004: - return ( - "【八卦速报】\n" - f"分类:{detail.get('tags', '未知')}\n" - f"区服:{detail.get('zone', '未知')} / {detail.get('server', '未知')}\n" - f"来源:{detail.get('tieba', '未知')}\n" - f"标题:{detail.get('title', '未知')}\n" - f"日期:{detail.get('date', '未知')}\n" - f"链接:{detail.get('url', '无')}" - ) - if action == 2005: - return ( - "【关隘首领】\n" - f"服务器:{detail.get('server', '未知')}\n" - f"关卡:{detail.get('stage', '未知')}\n" - f"开始时间:{self._format_timestamp(detail.get('start'))}" - ) - if action == 2006: - return ( - "【云从预告】\n" - f"事件:{detail.get('name', '未知')}\n" - f"地点:{detail.get('site', '未知')}\n" - f"说明:{detail.get('desc', '无')}\n" - f"时间:{self._format_timestamp(detail.get('time'))}" - ) + fields = EVENT_FIELDS.get(action) + if not fields: + detail_text = json.dumps(detail, ensure_ascii=False, indent=2) + return f"【事件推送 · {action}】\n{detail_text}" - detail_text = json.dumps(detail, ensure_ascii=False, indent=2) - return f"【事件推送 · {action} {EVENT_NAMES[action]}】\n{detail_text}" + lines = [f"【{EVENT_NAMES[action]}】"] + for label, key, value_type in fields: + lines.append(f"{label}:{self._format_field(detail.get(key), value_type)}") + return "\n".join(lines) + + def _format_field(self, value: Any, value_type: str) -> str: + if value_type == "time": + return self._format_timestamp(value) + if value_type == "status": + if value in (0, "0"): + return "维护" + if value in (1, "1"): + return "开服" + return str(value if value is not None and value != "" else "未知") + if value_type == "list": + if isinstance(value, (list, tuple)): + values = [str(item).strip() for item in value if str(item).strip()] + return "、".join(values) if values else "无" + return str(value if value is not None and value != "" else "未知") @staticmethod def _usage_text() -> str: @@ -503,4 +692,4 @@ class EventPushService: for action in EVENT_ACTIONS if action not in FREE_EVENT_ACTIONS ) - return f"免费事件:\n{free}\n\n事件版事件:\n{paid}" + return f"免费事件:\n{free}\n\n令牌事件:\n{paid}" diff --git a/core/jx3api_data.py b/core/jx3api_data.py index 8127bd5..f57c24f 100644 --- a/core/jx3api_data.py +++ b/core/jx3api_data.py @@ -1,7 +1,7 @@ import json import html import re -from datetime import datetime, timedelta +from datetime import datetime from typing import Dict, Any, Optional, List, Union from inspect import isawaitable from typing import Any, Awaitable, Callable, Dict, Optional @@ -19,15 +19,13 @@ from .fun_basic import load_template,gold_to_parts,week_to_num,compare_date_str, class JX3APIService: - def __init__(self, config: AstrBotConfig, sqlite: AsyncSQLiteDB, cache_sqlite: Optional[AsyncSQLiteDB] = None): + def __init__(self, config: AstrBotConfig, sqlite: AsyncSQLiteDB): # 实例化 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 == "": @@ -1067,6 +1065,60 @@ class JX3APIService: ) + async def zili_menu(self) -> Dict[str, str]: + """资历选择内容。""" + return { + "1": "总览", + "2": "杂闻", + "3": "武学", + "4": "修为", + "5": "装备", + "6": "技艺", + "7": "阅读", + "8": "任务", + "9": "足迹", + "10": "战斗", + "11": "声望", + "12": "秘境", + "13": "帮会", + "14": "阵营", + "15": "节日", + "16": "活动", + "17": "风雨江湖路", + "18": "家园", + "19": "剑侠录" + } + + + async def zili(self,server: str,name: str,selected: Dict[str, Any],) -> Dict[str, Any]: + """资历分布""" + if not isinstance(selected, dict) or len(selected) != 1: + return_data = self._init_return_data() + return_data["msg"] = "资历选项数据格式异常" + return return_data + + selected_name = str(next(iter(selected.values())) or "").strip() + selected_subclass = "" if selected_name == "总览" else selected_name + + async def processor(data: Any, return_data: Dict[str, Any]) -> None: + statistics = data.get("data") or {} + categories = statistics.get("total") or {} + if not isinstance(categories, dict) or not categories: + raise ValueError("接口未返回资历统计") + + # 查询总览时 total 为“大类 -> 小类 -> 统计”;指定 subclass 后, + # JX3API 会去掉大类这一层,total 直接变成“小类 -> 统计”。 + data["subclass"] = selected_subclass + return_data["data"] = data + + return await self._request_api( + path="/tuilan/achievement", + params={"server": server,"name": name,"class": 1,"subclass": selected_subclass,"ticket": self.ticket,"token": self.token,}, + processor=processor, + template="zili.html", + ) + + async def jueshe(self,server: str, name: str, history:int) -> Dict[str, Any]: """角色详情""" async def processor(data: Any, return_data: Dict[str, Any]) -> None: diff --git a/core/jx3box_data.py b/core/jx3box_data.py index c2bb3a3..d8773a5 100644 --- a/core/jx3box_data.py +++ b/core/jx3box_data.py @@ -14,30 +14,6 @@ from .request import APIClient from .sqlite import AsyncSQLiteDB from .fun_basic import load_template,gold_to_parts,week_to_num,compare_date_str,format_time,format_remaining -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} - JX3BOX_API_BASE_URLS = { "node": "https://node.jx3box.com", "next2": "https://next2.jx3box.com", @@ -226,11 +202,8 @@ class JX3BOXService: return return_data - async def hong1(self, name: str) -> Dict[str, Any]: - """宏 心法""" - return_data = self._init_return_data() - - # 数据库查询数据 + async def _macro_posts(self, name: str) -> Optional[List[Dict[str, Any]]]: + """按心法查询宏帖子,并保留接口热度排序。""" result = await self._sql_db.select_one( "kungfu", "name=? OR name1=? OR name2=? OR name3=? OR name4=? OR name5=?", @@ -238,52 +211,91 @@ class JX3BOXService: ) if result is None: - return_data["msg"] = "未找到该心法" - return return_data + return None kungfu = result.get("name", None) if kungfu is None: - return_data["msg"] = "未找到该心法" - return return_data + return None logger.debug(f"查询到数据:{kungfu}") - - # 1. 构造请求参数 - params = {"subtype": kungfu} - - # 2. 调用基础请求 data: Optional[Dict[str, Any]] = await self._base_request( "cms", "/api/cms/posts", - params=params, + params={"subtype": kungfu}, ) + if not isinstance(data, dict): + return None - if not data: - return_data["msg"] = "未找到该心法一键宏" - return return_data + return [ + item + for item in data.get("list", []) + if isinstance(item, dict) and item.get("ID") + ] + + + async def hong1(self, name: str) -> Dict[str, str]: + """宏首轮:返回“序号 -> 显示内容”的选择数据。""" + posts = await self._macro_posts(name) + + if not posts: + raise ValueError("未找到该心法一键宏") - # 提取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 + options = { + str(index): f"{post.get('author', '')}\t{post.get('post_title', '')}" + for index, post in enumerate(posts, start=1) + } + + if not options: + raise ValueError("未找到该心法一键宏") except Exception as e: logger.exception("处理返回数据失败") - return_data["msg"] = "处理返回数据失败" - return return_data - - return_data["code"] = 200 + if isinstance(e, ValueError): + raise + raise RuntimeError("处理返回数据失败") from e - return return_data + return options + + + async def hong_select(self,name: str,selected: Dict[str, Any],) -> Dict[str, Any]: + """宏次轮:根据单项选择数据重新定位帖子并查询详情。""" + if not isinstance(selected, dict) or len(selected) != 1: + return_data = self._init_return_data() + return_data["msg"] = "宏选项数据格式异常" + return return_data + + choice, label = next(iter(selected.items())) + posts = await self._macro_posts(name) + if not posts: + return_data = self._init_return_data() + return_data["msg"] = "未找到该心法一键宏" + return return_data + + selected_post = next( + ( + post + for post in posts + if f"{post.get('author', '')}\t{post.get('post_title', '')}" + == str(label) + ), + None, + ) + if selected_post is None: + try: + selected_post = posts[int(choice) - 1] + except (TypeError, ValueError, IndexError): + return_data = self._init_return_data() + return_data["msg"] = "无效宏序号" + return return_data + + pid = str(selected_post.get("ID") or "").strip() + if not pid: + return_data = self._init_return_data() + return_data["msg"] = "宏帖子 ID 无效" + return return_data + + return await self.hong2(pid) async def hong2(self, pid: str) -> Dict[str, Any]: @@ -328,12 +340,12 @@ class JX3BOXService: return return_data - async def _load_achievement_cache(self, key: str) -> tuple[Optional[Any], bool]: - """读取资历基础数据缓存,返回数据和是否已过期""" + async def _load_trade_cache(self, key: str) -> tuple[Optional[Any], bool]: + """读取交易行基础数据缓存,返回数据和是否已过期。""" try: - row = await self._cache_db.select_one("achievement_cache", "key=?", (key,)) + row = await self._cache_db.select_one("trade_item_cache", "key=?", (key,)) except Exception as e: - logger.error(f"读取资历缓存失败: {e}") + logger.error(f"读取交易行缓存失败: {e}") return None, True if not row: @@ -345,16 +357,16 @@ class JX3BOXService: expired = datetime.now() - updated_at > timedelta(days=30) return payload, expired except Exception as e: - logger.error(f"解析资历缓存失败: {e}") + logger.error(f"解析交易行缓存失败: {e}") return None, True - async def _save_achievement_cache(self, key: str, payload: Any): - """写入资历基础数据缓存""" + async def _save_trade_cache(self, key: str, payload: Any): + """写入交易行基础数据缓存。""" try: await self._cache_db.execute( """ - INSERT INTO achievement_cache (key, content, updated_at) + INSERT INTO trade_item_cache (key, content, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET content=excluded.content, @@ -367,31 +379,13 @@ class JX3BOXService: ), ) except Exception as e: - logger.error(f"写入资历缓存失败: {e}") - - - async def _get_achievement_base_data(self, cache_key: str, api_path: 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("node", api_path) - 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 + logger.error(f"写入交易行缓存失败: {e}") 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) + cached, expired = await self._load_trade_cache(cache_key) if isinstance(cached, list) and not expired: return cached @@ -402,7 +396,7 @@ class JX3BOXService: params={"client": "std"}, ) if isinstance(data, list) and data: - await self._save_achievement_cache(cache_key, data) + await self._save_trade_cache(cache_key, data) return data if isinstance(cached, list) and cached: @@ -461,184 +455,6 @@ class JX3BOXService: 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( - 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._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} - api_url = "https://www.jx3api.com/role/detail" - role_data: Optional[Dict[str, Any]] = await self._api.get(api_url, params=role_params, out_key="data") - - 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._base_request( - "next2", - "/api/next2/user-achievements", - params={"jx3id": global_id}, - ) - 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", - "/api/node/achievement/menus", - ) - point_payload = await self._get_achievement_base_data( - "achievement_points", - "/api/node/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) - ) - - 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 jiaoyihang(self, name: str , server: str) -> Dict[str, Any]: """区服交易行""" return_data = self._init_return_data() diff --git a/core/message.py b/core/message.py index 239a450..76c8053 100644 --- a/core/message.py +++ b/core/message.py @@ -1,3 +1,5 @@ +from typing import Any + from astrbot.core import html_renderer from astrbot.api import logger from astrbot.api.event import AstrMessageEvent, MessageChain @@ -14,29 +16,6 @@ from .bilei_data import BiLeidata class MessageBuilder: """回复消息构建""" - ZILI_MENU_TEXT = ( - "请选择资历查询类型:\n" - "0:资历总览\n" - "1:杂闻总览\n" - "2:武学总览\n" - "3:修为总览\n" - "4:装备总览\n" - "5:技艺总览\n" - "6:阅读总览\n" - "7:任务总览\n" - "8:足迹总览\n" - "9:战斗总览\n" - "10:声望总览\n" - "11:秘境总览\n" - "12:帮会总览\n" - "13:阵营总览\n" - "14:节日总览\n" - "15:活动总览\n" - "16:风雨江湖路总览\n" - "17:家园总览\n" - "18:剑侠录总览" - ) - def __init__(self, jx3api: JX3APIService, jx3box: JX3BOXService, @@ -138,94 +117,64 @@ class MessageBuilder: await event.send(event.plain_result("猪脑过载,请稍后再试")) - async def handler_plain_image_msg(self, event: AstrMessageEvent, action1, action2): - """两轮会话消息发送通用,先文本列表等反馈序号在发送图片""" - # 会话触发 + async def plain_image_msg(self, event: AstrMessageEvent, action): + """发送正文文本,并把可选 HTML 正文渲染为附图。""" try: - # 获取一轮数据 - data = await action1() - if data["code"] == 200: - # 发送一轮消息 - await event.send(event.plain_result(data["msg"])) - # 获取触发用户ID - user_id = event.get_sender_id() - - # 二轮会话流程 - @session_waiter(timeout=30) - async def macro_select_waiter(controller: SessionController,new_event: AstrMessageEvent): - # 跳过非触发用户消息 - if new_event.get_sender_id() != user_id: - return - # 获取用户消息 - msg = new_event.get_message_str().strip() - # 判断消息是否为数字 - if not msg.isdigit(): - await new_event.send( - MessageChain().message("输入异常,结束会话") - ) - controller.stop() - return - # 判断数字是否在有效值内 - num = int(msg) - if num < 1 or num > data["data"]["num"]: - await new_event.send( - MessageChain().message("无效序号,结束会话") - ) - controller.stop() - return - - # 获取二轮数据 - try: - data1 = await action2(data["data"]["list"][num]) - if data1["code"] != 200: - await new_event.send( - MessageChain().message("获取详细数据失败") - ) - controller.stop() - return - - # 消息拼接发送 - chain = MessageChain() - msg_text = data1["data"] - chain.message(msg_text) - if data1["temp"] != "": - url = await self.html_render(data1["temp"], {}, options={}) - chain.url_image(url) - await new_event.send(chain) - - except Exception as e: - logger.error(f"功能函数执行错误: {e}") - await new_event.send( - MessageChain().message("猪脑过载,请稍后再试") - ) - - controller.stop() - - # 二轮会话激活 - try: - await macro_select_waiter(event) - except TimeoutError: - await event.send(event.plain_result("选择超时,已结束会话")) - except Exception: - logger.error("选择发生异常", exc_info=True) - - else: - await event.send(event.plain_result(f"未搜索到相关内容")) + data = await action() + if data.get("code") != 200: + await event.send( + event.plain_result(data.get("msg") or "获取详细数据失败") + ) return - + + chain = MessageChain() + if data.get("data"): + chain.message(str(data["data"])) + if data.get("temp"): + url = await self.html_render(data["temp"], {}, options={}) + chain.url_image(url) + await event.send(chain) except Exception as e: logger.error(f"功能函数执行错误: {e}") await event.send(event.plain_result("猪脑过载,请稍后再试")) - async def handler_zili_msg(self, event: AstrMessageEvent, name: str, server: str): - """资历查询专用两轮会话,第一轮文本菜单,第二轮图片""" + async def handler_plain_image_msg(self, event: AstrMessageEvent, action1, action2, result_handler=None, timeout: int = 10): + """通用两轮会话:首轮数字键值对裁剪后交给次轮函数。""" try: - await event.send(event.plain_result(self.ZILI_MENU_TEXT)) - user_id = event.get_sender_id() + option_map = await action1() + if not isinstance(option_map, dict) or not option_map: + await event.send(event.plain_result("未搜索到可选内容")) + return - @session_waiter(timeout=30) - async def zili_select_waiter(controller: SessionController, new_event: AstrMessageEvent): + options = {str(key): value for key, value in option_map.items()} + if not all(key.isdigit() for key in options): + await event.send(event.plain_result("选项序号格式异常")) + return + + menu_lines = ["请选择序号"] + menu_lines.extend( + f"{key}、{label}" + for key, label in options.items() + ) + await event.send(event.plain_result("\n".join(menu_lines))) + user_id = event.get_sender_id() + send_result = result_handler or self.T2I_image_msg + + async def send_selected( + target_event: AstrMessageEvent, + selected: dict[str, Any], + ): + await send_result( + target_event, + lambda: action2(selected), + ) + + @session_waiter(timeout=timeout) + async def option_select_waiter( + controller: SessionController, + new_event: AstrMessageEvent, + ): if new_event.get_sender_id() != user_id: return @@ -236,43 +185,38 @@ class MessageBuilder: return choice = int(msg) - if choice < 0 or choice > 18: + choice_key = str(choice) + if choice_key not in options: await new_event.send(MessageChain().message("无效序号,结束会话")) controller.stop() return try: - data = await self.jx3box.zili(name, server, choice) - if data["code"] != 200: - await new_event.send(MessageChain().message(data.get("msg", "获取资历数据失败"))) - controller.stop() - return - - options = { - "quality": 100, - "device_scale_factor_level": "normal", - "full_page": True, - "omit_background": False, - "type": "jpeg" - } - data["data"]["icons"] = self.icons - url = await self.html_render(data["temp"], data["data"], options=options) - await new_event.send(new_event.image_result(url)) + await send_selected( + new_event, + {choice_key: options[choice_key]}, + ) except Exception as e: - logger.error(f"资历查询执行错误: {e}") + logger.error(f"二轮查询执行错误: {e}") await new_event.send(MessageChain().message("猪脑过载,请稍后再试")) controller.stop() try: - await zili_select_waiter(event) + await option_select_waiter(event) except TimeoutError: - await event.send(event.plain_result("选择超时,已结束会话")) + first_key = next(iter(options)) + await send_selected( + event, + {first_key: options[first_key]}, + ) except Exception: - logger.error("资历选择发生异常", exc_info=True) + logger.error("二轮选择发生异常", exc_info=True) + except (ValueError, RuntimeError) as e: + await event.send(event.plain_result(str(e) or "未搜索到可选内容")) except Exception as e: - logger.error(f"资历会话执行错误: {e}") + logger.error(f"二轮会话执行错误: {e}") await event.send(event.plain_result("猪脑过载,请稍后再试")) @@ -666,11 +610,20 @@ class MessageBuilder: async def hong(self, event: AstrMessageEvent,name: str): """ 宏 心法""" - return await self.handler_plain_image_msg(event, lambda: self.jx3box.hong1(name), self.jx3box.hong2) + return await self.handler_plain_image_msg( + event, + lambda: self.jx3box.hong1(name), + lambda selected: self.jx3box.hong_select(name, selected), + result_handler=self.plain_image_msg, + ) async def zili(self, event: AstrMessageEvent, server: str, name: str): """ 资历 角色名称 服务器""" - return await self.handler_zili_msg(event, name, server) + return await self.handler_plain_image_msg( + event, + lambda: self.jx3api.zili_menu(), + lambda selected: self.jx3api.zili(server, name, selected), + ) async def jiaoyihang(self, event: AstrMessageEvent,server: str, Name: str): """ 交易行 物品名称 服务器""" diff --git a/core/server_binding.py b/core/server_binding.py index 2a03b00..403a1eb 100644 --- a/core/server_binding.py +++ b/core/server_binding.py @@ -8,6 +8,8 @@ from .sqlite import AsyncSQLiteDB class ServerBindingService: """维护会话区服绑定、区服别名与指令解析所需的区服目录。""" + ALL_SERVERS_KEYWORD = "全区" + def __init__(self, sqlite: AsyncSQLiteDB, seed_path: Path): self.sql = sqlite self.seed_path = seed_path @@ -103,6 +105,16 @@ class ServerBindingService: server = self._clean(value) return self._server_lookup.get(self._key(server), server) + def is_all_servers_query(self, value: Any) -> bool: + """判断指令中的区服参数是否要求查询全区。""" + return self._key(value) == self._key(self.ALL_SERVERS_KEYWORD) + + def resolve_query_server(self, value: Any) -> str: + """解析查询区服;“全区”作为保留值转换为接口所需的空字符串。""" + if self.is_all_servers_query(value): + return "" + return self.resolve_server(value) + async def get_binding(self, session_id: str) -> str: row = await self.sql.select_one( "session_server_bindings", diff --git a/main.py b/main.py index 142265c..c792e8b 100644 --- a/main.py +++ b/main.py @@ -26,7 +26,7 @@ PLUGIN_NAME = "astrbot_plugin_jx3" @register("astrbot_plugin_jx3", "fxdyz", "聚合剑网三游戏数据,提供查询、图片渲染、本地避雷和实时事件推送。", - "3.4.0", + "3.4.1", "https://github.com/qsc20001102/astrbot_plugin_jx3" ) class Jx3ApiPlugin(Star): @@ -63,7 +63,7 @@ class Jx3ApiPlugin(Star): try: # 数据库初始化 await self.init_bilei_data() - await self.init_achievement_cache_data() + await self.init_trade_item_cache_data() await self.kungfu_alias.initialize() await self.server_binding.initialize() @@ -155,7 +155,7 @@ class Jx3ApiPlugin(Star): self.local_sql_db = AsyncSQLiteDB(str(self.local_data_path)) # 剑网三功能实例化 self.bilei = BiLeidata(self.local_sql_db) - self.jx3api = JX3APIService(self.conf, self.local_sql_db, self.local_sql_db) + self.jx3api = JX3APIService(self.conf, self.local_sql_db) self.jx3box = JX3BOXService(self.conf, self.local_sql_db, self.local_sql_db) self.kungfu_alias = KungfuAliasService( self.local_sql_db, @@ -196,16 +196,33 @@ class Jx3ApiPlugin(Star): """) - async def init_achievement_cache_data(self): - """初始化资历基础数据缓存表""" + async def init_trade_item_cache_data(self): + """初始化交易行物品缓存,并清理已停用的资历缓存表。""" await self.local_sql_db.execute(""" - CREATE TABLE IF NOT EXISTS achievement_cache( + CREATE TABLE IF NOT EXISTS trade_item_cache( key TEXT PRIMARY KEY, content TEXT NOT NULL, updated_at TEXT NOT NULL ) """) + legacy_cache = await self.local_sql_db.fetch_one( + "SELECT name FROM sqlite_master WHERE type='table' AND name=?", + ("achievement_cache",), + ) + if legacy_cache: + # 旧表曾与交易行共用;仅迁移仍有效的交易行缓存后移除资历缓存。 + await self.local_sql_db.execute( + """ + INSERT OR IGNORE INTO trade_item_cache (key, content, updated_at) + SELECT key, content, updated_at + FROM achievement_cache + WHERE key = ? + """, + ("trade_item_groups",), + ) + await self.local_sql_db.execute("DROP TABLE achievement_cache") + def ini_command_map(self): """初始化指令集""" @@ -373,7 +390,7 @@ class Jx3ApiPlugin(Star): event: AstrMessageEvent, args: list[str], ) -> list[str]: - """为带 server 参数的指令补齐会话绑定,并解析区服别名。""" + """补齐会话绑定、解析区服别名,并支持用“全区”显式传空区服。""" params = [ parameter for parameter in inspect.signature(handler).parameters.values() @@ -404,7 +421,10 @@ class Jx3ApiPlugin(Star): explicit_server = ( has_server_arg and ( - self.server_binding.is_known_server(prepared[server_index]) + self.server_binding.is_all_servers_query( + prepared[server_index] + ) + or self.server_binding.is_known_server(prepared[server_index]) or len(prepared) >= len(params) or ( has_required_parameter_after_server @@ -413,13 +433,13 @@ class Jx3ApiPlugin(Star): ) ) if explicit_server: - prepared[server_index] = self.server_binding.resolve_server( + prepared[server_index] = self.server_binding.resolve_query_server( prepared[server_index] ) else: prepared.insert(server_index, bound_server) elif has_server_arg: - prepared[server_index] = self.server_binding.resolve_server( + prepared[server_index] = self.server_binding.resolve_query_server( prepared[server_index] ) diff --git a/metadata.yaml b/metadata.yaml index 4754777..0a0d17c 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,7 +1,7 @@ name: astrbot_plugin_jx3 display_name: 剑网三游戏数据查询工具 desc: 聚合剑网三游戏数据,提供查询、图片渲染、本地避雷和实时事件推送。 -version: 3.4.0 +version: 3.4.1 author: 飞翔大野猪 repo: https://github.com/qsc20001102/astrbot_plugin_jx3 astrbot_version: ">=4.24.1" diff --git a/pages/server-management/style.css b/pages/server-management/style.css index a428d7b..5a7096b 100644 --- a/pages/server-management/style.css +++ b/pages/server-management/style.css @@ -68,12 +68,12 @@ input:focus { border-color: var(--focus); box-shadow: 0 0 0 3px color-mix(in srg .section-intro { padding: 22px; border-bottom: 1px solid var(--border); } .section-intro h2 { margin: 0; font-size: 18px; } .table-wrap { overflow-x: auto; } -table { width: 100%; border-collapse: collapse; } -th, td { padding: 15px 20px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--border); } +table { width: 100%; table-layout: auto; border-collapse: collapse; } +th, td { padding: 15px 20px; text-align: center; vertical-align: middle; border-bottom: 1px solid var(--border); } th { background: var(--surface-subtle); color: var(--muted); font-size: 13px; font-weight: 700; } tbody tr:last-child td { border-bottom: 0; } tbody tr:hover { background: color-mix(in srgb, var(--accent-soft) 42%, transparent); } -.actions { width: 170px; white-space: nowrap; } +.actions { white-space: nowrap; } .link-button { padding: 4px 7px; border: 0; border-radius: 5px; background: transparent; color: #3568ae; cursor: pointer; font-weight: 650; } [data-theme="dark"] .link-button { color: #8db9f4; } .link-button:hover { background: color-mix(in srgb, currentColor 10%, transparent); } @@ -82,7 +82,7 @@ tbody tr:hover { background: color-mix(in srgb, var(--accent-soft) 42%, transpar .state::before { width: 8px; height: 8px; border-radius: 50%; background: currentColor; content: ""; } .state--on { color: var(--success); } .state--off { color: var(--muted); } -.tags { display: flex; flex-wrap: wrap; gap: 7px; max-width: 620px; } +.tags { display: flex; justify-content: center; flex-wrap: wrap; gap: 7px; margin: 0 auto; } .tag { display: inline-flex; padding: 3px 8px; border-radius: 6px; background: var(--accent-soft); color: var(--accent); font-size: 12px; font-weight: 650; } .empty { padding: 42px 20px; color: var(--muted); text-align: center; } .toast { position: fixed; right: 24px; bottom: 24px; z-index: 5; max-width: min(380px, calc(100vw - 48px)); padding: 13px 18px; border-left: 4px solid var(--success); border-radius: 8px; background: var(--surface); box-shadow: 0 14px 38px rgba(0, 0, 0, .18); opacity: 0; pointer-events: none; transform: translateY(14px); transition: 180ms ease; } @@ -103,9 +103,9 @@ tbody tr:hover { background: color-mix(in srgb, var(--accent-soft) 42%, transpar thead { display: none; } tbody tr { padding: 7px 0; border-bottom: 1px solid var(--border); } tbody tr:last-child { border-bottom: 0; } - td { display: grid; grid-template-columns: minmax(92px, 30%) minmax(0, 1fr); gap: 12px; padding: 8px 16px; border: 0; overflow-wrap: anywhere; } + td { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 12px; padding: 8px 16px; border: 0; overflow-wrap: anywhere; } td::before { color: var(--muted); content: attr(data-label); font-size: 12px; font-weight: 700; } - td.actions { grid-template-columns: minmax(92px, 30%) auto auto; align-items: start; justify-content: start; width: 100%; white-space: normal; } + td.actions { grid-template-columns: max-content auto auto; align-items: center; width: 100%; white-space: normal; } td.empty { display: block; padding: 36px 16px; } td.empty::before { content: none; } } diff --git a/templates/pages/bangzhanjilu.html b/templates/pages/bangzhanjilu.html index a30a5ec..28605d1 100644 --- a/templates/pages/bangzhanjilu.html +++ b/templates/pages/bangzhanjilu.html @@ -20,10 +20,10 @@
{% for item in items %}| 副本名称 | @@ -81,4 +82,3 @@ {% endif %} - diff --git a/templates/pages/guanaishouling.html b/templates/pages/guanaishouling.html index 2e900f0..0f9145f 100644 --- a/templates/pages/guanaishouling.html +++ b/templates/pages/guanaishouling.html @@ -30,9 +30,9 @@ class="camp-haoqi" {% endif %} > -{{ item.camp_name }} | -{{ item.castle }} | -{{ item.str_status }} | +{{ item.camp_name }} | +{{ item.castle }} | +{{ item.str_status }} | {{ item.start_time }} | {{ item.end_time }} | {{ item.remaining_time }} | diff --git a/templates/pages/helps.html b/templates/pages/helps.html index 2f8d682..993e863 100644 --- a/templates/pages/helps.html +++ b/templates/pages/helps.html @@ -206,6 +206,6 @@ diff --git a/templates/pages/jiaoyihang.html b/templates/pages/jiaoyihang.html index c65ecdb..188a98e 100644 --- a/templates/pages/jiaoyihang.html +++ b/templates/pages/jiaoyihang.html @@ -1,4 +1,5 @@ {# template-title: 剑网3交易行价格查询 #} +{# template-components: data-table #}
|---|
| 物品 | -物价 | -数量 | -数据时间 | +物品 | +物价 | +数量 | +数据时间 |
|---|---|---|---|---|---|---|---|
|
-
+ {{ item.name }}
{{ item.szSkillName }} |
{{ item.szBossName }} |
- {{ item.skill_color_text }} |
+ {{ item.skill_color_text }} |
{{ item.nLevel }} |
聊天记录- |