From 9c9808adf7f032a564adcd7935d202d4e7554caa Mon Sep 17 00:00:00 2001 From: qsc Date: Fri, 4 Sep 2026 22:38:16 +0800 Subject: [PATCH] fxdyz --- CHANGELOG.md | 14 ++ README.md | 59 ++++---- _conf_schema.json | 38 +++++ core/jx3api_data.py | 114 ++++++++++---- core/kungfu_alias.py | 32 ++-- core/message.py | 174 ++++++++++++---------- data/server_aliases.json | 12 +- main.py | 34 ++--- metadata.yaml | 2 +- pages/server-management/app.js | 165 ++++++++++++++------ pages/server-management/index.html | 34 +---- pages/server-management/style.css | 9 +- templates/pages/helps.html | 24 +-- templates/styles/base.css | 34 ++--- templates/styles/components.css | 102 ++++++------- templates/styles/pages/baizhan.css | 10 +- templates/styles/pages/chengbeng.css | 58 ++++---- templates/styles/pages/chengjiu.css | 50 +++---- templates/styles/pages/fubenjilu.css | 34 ++--- templates/styles/pages/helps.css | 34 ++--- templates/styles/pages/jiaoyihang.css | 6 +- templates/styles/pages/juesheliaotian.css | 4 +- templates/styles/pages/richangyuche.css | 15 +- templates/styles/pages/shapan.css | 4 +- templates/styles/pages/wujia.css | 12 +- templates/styles/pages/xingxiashijian.css | 10 +- templates/styles/pages/zhanji.css | 10 +- templates/styles/pages/zili.css | 16 +- templates/styles/pages/zilipaixing.css | 10 +- templates/styles/tokens.css | 48 +++--- 30 files changed, 658 insertions(+), 510 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaee031..d157627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ ## 更新记录 +### version: 3.4.2(2026-09-04): + +图片模板整体调整为浅色背景、深色高对比文本和更大的字号,缩小内容与金色边框之间的留白并扩大主体区域;同步检查并调整多个独立页面的字号、间距及表格布局,提高长图阅读体验。 + +插件配置页新增“图片渲染质量”分组,可选择 `1.0`、`1.3`、`1.8` 三档渲染清晰度以及 JPEG/PNG 输出格式,并可设置 JPEG 质量;默认使用 `1.3` 倍设备像素比和 JPEG 质量 `100`。 + +将 17 个独立排行榜指令合并为 `帮会排行`、`阵营排行`、`其他排行` 三个两轮会话入口;首轮按分类展示固定序号菜单,第二轮复用排行榜图片流程输出所选榜单,并统一只保留接口数据的前 50 条。 + +修复两轮会话已经取得有效选择后等待计时器仍继续运行,导致耗时查询或图片渲染期间触发超时分支并重复输出第二张图片的问题。 + +修复 aiocqhttp 已成功发送图片但返回 `retcode=1200` 超时回执时,插件继续补发“猪脑过载”错误提示的问题;该回执现在只记录警告。 + +优化插件 WebUI 的别名管理:区服页展示全部标准区服并移除删除操作,心法页隐藏不可修改的配装 ID;标准区服名和标准心法名均保持只读,点击“编辑”后直接在对应别名单元格内修改并保存。 + ### version: 3.4.1(2026-09-04): 带区服参数的查询新增 `全区` 保留值;即使当前会话已绑定区服,也会向接口传入空区服以查询全区数据。 diff --git a/README.md b/README.md index 8b240bb..cd13518 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ - 中文触发词覆盖活动、名剑、排行、交易、阵营、角色、奇遇、副本、家园、社区等场景。 - 支持纯文本、远程图片、HTML/Jinja2 渲染图片和图文消息链。 -- 支持 `宏`、`资历` 两类 10 秒两轮交互,超时默认选择第一项。 +- 支持 `宏`、`资历` 以及帮会、阵营、其他排行榜的 10 秒两轮交互,超时默认选择第一项。 +- 查询图片统一使用浅色高对比主题,并可在插件配置页调整渲染清晰度、输出格式和 JPEG 质量。 - 支持本地 SQLite 避雷记录的增删改查。 - 支持按 AstrBot 会话分别开启总开关和具体 JX3API 实时事件订阅。 - 复用 `aiohttp.ClientSession`,统一处理 GET、POST、JSON、图片和分页请求。 @@ -78,12 +79,15 @@ pip install -r data/plugins/astrbot_plugin_jx3/requirements.txt | 配置项 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| `prefix.enable` | `bool` | `true` | 是否启用指令前缀检查 | +| `prefix.enable` | `bool` | `false` | 是否启用指令前缀检查 | | `prefix.text` | `string` | `剑三` | 指令前缀内容 | | `jx3api_token` | `string` | 空 | JX3API Token | | `jx3api_ticket` | `string` | 空 | 部分名剑和心法接口需要的推栏 Ticket | | `jx3api_wss` | `string` | `wss://socket.nicemoe.cn` | JX3API 事件通道地址 | | `jx3api_wss_token` | `string` | 空 | 事件版令牌;免费事件无需填写 | +| `image_render_quality.device_scale_factor` | `string` | `1.3` | 渲染清晰度,可选 `1.0`、`1.3`、`1.8` | +| `image_render_quality.format` | `string` | `jpeg` | 图片输出格式,可选 `jpeg`、`png` | +| `image_render_quality.jpeg_quality` | `int` | `100` | JPEG 图片质量,范围 `1-100`,PNG 下不生效 | 会话区服绑定、区服别名和事件订阅均保存在本地 SQLite。修改 WebSocket 地址或事件版令牌后需要重新加载插件。 @@ -100,7 +104,7 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通 ## 使用方式 -默认启用前缀,因此推荐发送: +如已启用指令前缀,可发送: ```text 剑三 功能 @@ -108,7 +112,7 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通 剑三 战绩 梦江南 角色名 33 ``` -前缀与指令之间的空格可以省略,例如 `剑三日常` 也能触发。关闭前缀检查后可直接发送 `日常`。 +前缀与指令之间的空格可以省略,例如 `剑三日常` 也能触发。默认关闭前缀检查,可直接发送 `日常`;启用后才要求使用配置的前缀。 参数规则: @@ -161,29 +165,12 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通 | `战绩 服务器 角色 [模式]` | 角色名剑战绩,模式默认 `33`;图片 | Token + Ticket | | `名剑排行 [模式] [数量]` | 名剑大会排行,默认 `33`、50 条;图片 | Token + Ticket | | `名剑统计 [模式]` | 名剑门派统计,模式默认 `33`;图片 | Token + Ticket | +| `帮会排行 服务器` | 回复序号选择神兵宝甲或爱心帮会榜单,最多保留前 50 条;图片 | Token | +| `阵营排行 服务器` | 回复序号选择赛季、上周或本周阵营榜单,最多保留前 50 条;图片 | Token | +| `其他排行 服务器` | 回复序号选择名士、老江湖、名师等其他榜单,最多保留前 50 条;图片 | Token | | `试炼排行 服务器 心法` | 试炼之地排行;图片 | Token | -以下排行榜指令均使用 `指令 服务器` 格式,输出图片并需要 Token: - -```text -名士五十强 -老江湖五十强 -兵甲藏家五十强 -名师五十强 -阵营英雄五十强 -薪火相传五十强 -庐园广记一百强 -浩气神兵宝甲五十强 -恶人神兵宝甲五十强 -浩气爱心帮会五十强 -恶人爱心帮会五十强 -赛季恶人五十强 -赛季浩气五十强 -上周恶人五十强 -上周浩气五十强 -本周恶人五十强 -本周浩气五十强 -``` +三个聚合排行榜指令都会先发送可选榜单菜单;回复对应序号后才查询并渲染结果。已绑定区服的会话可以省略服务器参数。 ### 物价与交易 @@ -304,7 +291,7 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通 ### WebUI 插件管理 -AstrBot 插件详情页中的“剑网三插件管理”页面提供四个页签:会话绑定、事件订阅、区服别名和心法别名。页面可以新增、修改或解除任意会话绑定,查看所有会话的事件总开关及订阅编号,为每个标准区服配置多个别名,并维护心法的 JX3BOX 配装 ID、标准名称和最多 5 个别名。管理页通过 AstrBot Plugin Pages 桥接调用插件 Web API,不直接访问 Dashboard 凭据。 +AstrBot 插件详情页中的“剑网三插件管理”页面提供四个页签:会话绑定、事件订阅、区服别名和心法别名。页面可以新增、修改或解除任意会话绑定,查看所有会话的事件总开关及订阅编号。区服别名页展示完整标准区服目录,标准名称只读,只能行内编辑别名;心法别名页隐藏 JX3BOX 配装 ID,标准心法名称只读,同样只允许行内编辑最多 5 个别名。管理页通过 AstrBot Plugin Pages 桥接调用插件 Web API,不直接访问 Dashboard 凭据。 ## 业务流程 @@ -400,13 +387,13 @@ flowchart LR `core/message.py` 根据业务结果选择输出方式: - `plain_msg()`:纯文本。 -- `T2I_image_msg()`:向模板注入业务数据和本地图标,再调用 AstrBot HTML 渲染器生成 JPEG。 +- `T2I_image_msg()`:向模板注入业务数据和本地图标,再调用 AstrBot HTML 渲染器按插件配置生成图片。 - `image_msg()`:直接发送远程图片 URL 或图片数据。 - `plain_chain()`:发送文本与图片组成的富媒体消息链;结果包含 HTML 正文时,会将其渲染为图片组件追加到消息链。 - `handler_plain_image_msg()`:通用两轮会话;首轮函数直接返回 `{"1": "总览", "2": "杂闻"}` 形式的数字键值对(无 `code`、`msg`、`data` 外层),处理器据此生成序号文本;选择后裁剪为只含所选项的新字典交给次轮函数,再由注入的输出器发送结果;超时固定选择第一项。 -- `zili()`:在指令入口直接调用上述通用两轮会话处理器,并通过标准图片流程输出结果。 +- `hong()`、`zili()` 及三类聚合排行榜入口:保持统一调用结构接入上述两轮会话处理器,并按各自需要选择图文消息链或标准图片流程输出结果。 -图片默认使用质量 `100`、完整页面截图和普通设备缩放级别。模板可通过 `icons.img`、`icons.sect`、`icons.serendipity` 访问通用、门派/心法和奇遇图标。 +图片默认使用 JPEG 质量 `100`、完整页面截图和 `1.3` 倍设备像素比。可以在插件配置页的“图片渲染质量”分组切换 JPEG/PNG、调整 JPEG 质量,并选择 `1.0`、`1.3`、`1.8` 设置渲染清晰度。模板可通过 `icons.img`、`icons.sect`、`icons.serendipity` 访问通用、门派/心法和奇遇图标。 AstrBot 的渲染接口接收完整 HTML 字符串,因此插件不会依赖渲染端读取本地 CSS 文件。`core/template.py` 会异步读取并缓存公共布局、设计变量、基础样式和组件样式,再按需读取页面专属样式及页面片段,组装完成后把单个完整字符串交给渲染器。共享资源只在首次请求时读取一次;标准页面没有同名 CSS 文件也可以正常组合。 @@ -525,12 +512,16 @@ git diff --check ## 当前版本状态 -以下内容是对 v3.4.1 当前源码的静态核对结果,部署和二次开发前应注意: +以下内容是对 v3.4.2 当前源码的静态核对结果,部署和二次开发前应注意: -1. 带区服参数的指令在入口层统一进行会话绑定补齐和别名解析;新增此类指令时参数名应继续使用 `server`。 -2. 资历分布使用固定的“数字序号 → 大类名称”首轮菜单,通用会话裁剪出单项字典后交给 JX3API `/tuilan/achievement` 次轮查询;模板兼容总览与指定 `subclass` 的不同 `data.total` 层级,并直接使用接口角色字段、`pieces`、`seniority` 和 `score/totalScore` 渲染,JX3BOX 不再参与资历查询。 -3. `APIClient` 当前默认 `ssl_verify=False`,即外部 HTTPS 请求不校验证书。对传输安全有要求的部署应先评估并调整该设置。 -4. JX3API 服务初始化时会把 Token 和 Ticket 写入 debug 日志。不要公开调试日志,建议二次开发时移除敏感值输出。 +1. 查询图片使用浅色高对比主题和放大的内容区域;渲染清晰度、JPEG/PNG 格式及 JPEG 质量由 `image_render_quality` 配置组控制,提高清晰度或使用 PNG 会增加图片体积与渲染耗时。 +2. `帮会排行`、`阵营排行`、`其他排行` 使用固定的“数字序号 → 榜单名称”首轮菜单,次轮查询统一截取接口返回的前 50 条;取得有效选择后会先停止超时计时器,避免渲染期间重复输出。 +3. 区服别名 WebUI 展示完整标准区服目录并只允许行内编辑别名;心法别名 WebUI 隐藏配装 ID,后端保存接口也只更新已有心法的别名。 +4. aiocqhttp 图片发送返回 `retcode=1200` 时只记录警告,不再追加“猪脑过载”提示;该回执代表发送结果不确定,仍建议结合平台日志确认实际送达情况。 +5. 带区服参数的指令在入口层统一进行会话绑定补齐和别名解析;新增此类指令时参数名应继续使用 `server`。 +6. 资历分布使用固定的“数字序号 → 大类名称”首轮菜单,通用会话裁剪出单项字典后交给 JX3API `/tuilan/achievement` 次轮查询;模板兼容总览与指定 `subclass` 的不同 `data.total` 层级,并直接使用接口角色字段、`pieces`、`seniority` 和 `score/totalScore` 渲染,JX3BOX 不再参与资历查询。 +7. `APIClient` 当前默认 `ssl_verify=False`,即外部 HTTPS 请求不校验证书。对传输安全有要求的部署应先评估并调整该设置。 +8. JX3API 服务初始化时会把 Token 和 Ticket 写入 debug 日志。不要公开调试日志,建议二次开发时移除敏感值输出。 ## 注意事项 diff --git a/_conf_schema.json b/_conf_schema.json index 2a30491..74f22e8 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -40,5 +40,43 @@ "type": "string", "default": "", "hint": "用于付费事件推送,不填写只推送免费事件。" + }, + "image_render_quality": { + "description": "图片渲染质量", + "type": "object", + "items": { + "device_scale_factor": { + "description": "渲染清晰度", + "type": "string", + "options": [ + "1.0", + "1.3", + "1.8" + ], + "default": "1.3", + "hint": "数值越高越清晰,同时图片更大、渲染耗时更多。" + }, + "format": { + "description": "输出格式", + "type": "string", + "options": [ + "jpeg", + "png" + ], + "default": "jpeg", + "hint": "JPEG 文件通常更小;PNG 为无损格式但文件更大。透明背景的特殊页面仍会自动使用 PNG。" + }, + "jpeg_quality": { + "description": "JPEG 质量", + "type": "int", + "default": 100, + "slider": { + "min": 1, + "max": 100, + "step": 1 + }, + "hint": "仅输出格式为 JPEG 时生效;数值越高细节越完整,文件也越大。" + } + } } } diff --git a/core/jx3api_data.py b/core/jx3api_data.py index f57c24f..b8cc5f2 100644 --- a/core/jx3api_data.py +++ b/core/jx3api_data.py @@ -15,7 +15,62 @@ from .sqlite import AsyncSQLiteDB from .fun_basic import load_template,gold_to_parts,week_to_num,compare_date_str,format_time,format_remaining +ROLE_RANK_NAMES = { + "名士五十强", + "老江湖五十强", + "兵甲藏家五十强", + "名师五十强", + "阵营英雄五十强", + "薪火相传五十强", + "庐园广记一百强", +} +TONG_RANK_NAMES0 = { + "赛季恶人五十强", + "赛季浩气五十强", + "本周恶人五十强", + "本周浩气五十强", +} +TONG_RANK_NAMES1 = { + "浩气神兵宝甲五十强", + "恶人神兵宝甲五十强", + "浩气爱心帮会五十强", + "恶人爱心帮会五十强", +} +TONG_RANK_NAMES2 = { + "上周恶人五十强", + "上周浩气五十强", +} +GUILD_RANK_OPTIONS = { + "1": "浩气神兵宝甲五十强", + "2": "恶人神兵宝甲五十强", + "3": "浩气爱心帮会五十强", + "4": "恶人爱心帮会五十强", +} +CAMP_RANK_OPTIONS = { + "1": "赛季恶人五十强", + "2": "赛季浩气五十强", + "3": "上周恶人五十强", + "4": "上周浩气五十强", + "5": "本周恶人五十强", + "6": "本周浩气五十强", +} +OTHER_RANK_OPTIONS = { + "1": "名士五十强", + "2": "老江湖五十强", + "3": "兵甲藏家五十强", + "4": "名师五十强", + "5": "阵营英雄五十强", + "6": "薪火相传五十强", + "7": "庐园广记一百强", +} + +RANK_NAMES = frozenset().union( + ROLE_RANK_NAMES, + TONG_RANK_NAMES0, + TONG_RANK_NAMES1, + TONG_RANK_NAMES2, +) class JX3APIService: @@ -456,33 +511,36 @@ class JX3APIService: ) + async def banghui_rank_menu(self) -> Dict[str, str]: + """帮会排行榜选择内容。""" + return GUILD_RANK_OPTIONS.copy() + + async def zhenying_rank_menu(self) -> Dict[str, str]: + """阵营排行榜选择内容。""" + return CAMP_RANK_OPTIONS.copy() + + async def qita_rank_menu(self) -> Dict[str, str]: + """其他排行榜选择内容。""" + return OTHER_RANK_OPTIONS.copy() + + async def rank_statistical_select(self, server: 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 + + rank_name = str(next(iter(selected.values())) or "").strip() + if rank_name not in RANK_NAMES: + return_data = self._init_return_data() + return_data["msg"] = "无效排行榜选项" + return return_data + + return await self.rank_statistical(rank_name, server) + async def rank_statistical(self, name: str, server: str) -> Dict[str, Any]: """排行榜单""" - ROLE_RANK_NAMES = { - "名士五十强", - "老江湖五十强", - "兵甲藏家五十强", - "名师五十强", - "阵营英雄五十强", - "薪火相传五十强", - "庐园广记一百强", - } - TONG_RANK_NAMES0 = { - "赛季恶人五十强", - "赛季浩气五十强", - "本周恶人五十强", - "本周浩气五十强", - } - TONG_RANK_NAMES1 = { - "浩气神兵宝甲五十强", - "恶人神兵宝甲五十强", - "浩气爱心帮会五十强", - "恶人爱心帮会五十强", - } - TONG_RANK_NAMES2 = { - "上周恶人五十强", - "上周浩气五十强", - } + if name in ROLE_RANK_NAMES: template_name = "rank_role.html" elif name in TONG_RANK_NAMES0: @@ -494,7 +552,11 @@ class JX3APIService: # 数据处理 async def processor(data: Any, return_data: Dict[str, Any]) -> None: - items = data.get("data", []) + items = data.get("data") or [] + if isinstance(items, list): + items = items[:50] + else: + items = [] return_data["data"] = { "items": items, diff --git a/core/kungfu_alias.py b/core/kungfu_alias.py index eb6f0db..0c3a02b 100644 --- a/core/kungfu_alias.py +++ b/core/kungfu_alias.py @@ -73,17 +73,21 @@ class KungfuAliasService: for row in rows ] - async def save(self, pzid: Any, name: Any, aliases: Iterable[Any]): + async def save_aliases(self, pzid: Any, aliases: Iterable[Any]): + """只更新已有心法的别名,不允许修改 ID 和标准名称。""" normalized_pzid = self._parse_pzid(pzid) - normalized_name = self._clean(name) - if not normalized_name: - raise ValueError("标准心法名不能为空") - if len(normalized_name) > 64: - raise ValueError("标准心法名过长") + rows = await self.list_kungfu() + current = next( + (row for row in rows if row["pzid"] == normalized_pzid), + None, + ) + if current is None: + raise ValueError("心法不存在") + normalized_name = current["name"] normalized_aliases = self._normalize_aliases(normalized_name, aliases) occupied: dict[str, str] = {} - for row in await self.list_kungfu(): + for row in rows: if row["pzid"] == normalized_pzid: continue for value in [row["name"], *row["aliases"]]: @@ -100,17 +104,11 @@ class KungfuAliasService: ] await self.sql.execute( """ - INSERT INTO kungfu (pzid, name, name1, name2, name3, name4, name5) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(pzid) DO UPDATE SET - name=excluded.name, - name1=excluded.name1, - name2=excluded.name2, - name3=excluded.name3, - name4=excluded.name4, - name5=excluded.name5 + UPDATE kungfu + SET name1=?, name2=?, name3=?, name4=?, name5=? + WHERE pzid=? """, - (normalized_pzid, normalized_name, *values), + (*values, normalized_pzid), ) def _normalize_aliases(self, name: str, aliases: Iterable[Any]) -> list[str]: diff --git a/core/message.py b/core/message.py index 76c8053..785a919 100644 --- a/core/message.py +++ b/core/message.py @@ -1,5 +1,6 @@ from typing import Any +from aiocqhttp.exceptions import ActionFailed from astrbot.core import html_renderer from astrbot.api import logger from astrbot.api.event import AstrMessageEvent, MessageChain @@ -16,18 +17,67 @@ from .bilei_data import BiLeidata class MessageBuilder: """回复消息构建""" + + _RENDER_FORMATS = {"jpeg", "png"} + _DEVICE_SCALE_FACTOR_LEVELS = { + 1.0: "normal", + 1.3: "high", + 1.8: "ultra", + } def __init__(self, jx3api: JX3APIService, jx3box: JX3BOXService, bilei: BiLeidata, event_push: EventPushService, - icons: dict[str, dict[str, str]] + icons: dict[str, dict[str, str]], + render_config: dict[str, Any] | None = None, ): self.jx3api = jx3api self.jx3box = jx3box self.bilei = bilei self.event_push = event_push self.icons = icons + self.render_config = render_config if isinstance(render_config, dict) else {} + + + def _build_render_options( + self, + overrides: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """根据插件配置构造 AstrBot HTML 截图参数。""" + image_format = str(self.render_config.get("format", "jpeg")).lower() + if image_format not in self._RENDER_FORMATS: + image_format = "jpeg" + + try: + device_scale_factor = float( + self.render_config.get("device_scale_factor", 1.3) + ) + except (TypeError, ValueError): + device_scale_factor = 1.3 + scale_factor_level = self._DEVICE_SCALE_FACTOR_LEVELS.get( + device_scale_factor, + "high", + ) + + try: + jpeg_quality = int(self.render_config.get("jpeg_quality", 100)) + except (TypeError, ValueError): + jpeg_quality = 100 + jpeg_quality = max(1, min(100, jpeg_quality)) + + options: dict[str, Any] = { + "quality": jpeg_quality, + "device_scale_factor_level": scale_factor_level, + "scale": "device", + "full_page": True, + "omit_background": False, + "type": image_format, + } + options.update(overrides or {}) + if options.get("type") == "png": + options.pop("quality", None) + return options async def html_render( @@ -42,7 +92,7 @@ class MessageBuilder: tmpl, data, return_url=return_url, - options=options, + options=self._build_render_options(options), ) @@ -56,7 +106,7 @@ class MessageBuilder: await event.send(event.plain_result(data["msg"])) except Exception as e: logger.error(f"功能函数执行错误: {e}") - await event.send(event.plain_result("猪脑过载,请稍后再试")) + await event.send(event.plain_result("猪脑过载,请稍后再试")) async def T2I_image_msg( @@ -69,22 +119,25 @@ class MessageBuilder: data = await action() try: if data["code"] == 200: - options = { - "quality": 100, - "device_scale_factor_level": "normal", - "full_page": True, - "omit_background": False, - "type": "jpeg" - } - options.update(render_options or {}) - if options.get("type") == "png": - options.pop("quality", None) data["data"]["icons"] = self.icons - url = await self.html_render(data["temp"], data["data"], options=options) + url = await self.html_render( + data["temp"], + data["data"], + options=render_options, + ) await event.send(event.image_result(url)) else: await event.send(event.plain_result(data["msg"])) + except ActionFailed as e: + if e.retcode == 1200: + logger.warning( + "图片消息发送回执超时,消息可能已经成功送达," + "不再发送错误提示。" + ) + return + logger.error(f"功能函数执行错误: {e}") + await event.send(event.plain_result("猪脑过载,请稍后再试")) except Exception as e: logger.error(f"功能函数执行错误: {e}") await event.send(event.plain_result("猪脑过载,请稍后再试")) @@ -131,7 +184,7 @@ class MessageBuilder: if data.get("data"): chain.message(str(data["data"])) if data.get("temp"): - url = await self.html_render(data["temp"], {}, options={}) + url = await self.html_render(data["temp"], {}) chain.url_image(url) await event.send(chain) except Exception as e: @@ -191,6 +244,9 @@ class MessageBuilder: controller.stop() return + # 已取得有效选择,先结束等待计时,再执行可能超过 timeout 的 + # API 请求与图片渲染,避免超时分支重复发送默认第一项。 + controller.stop() try: await send_selected( new_event, @@ -200,8 +256,6 @@ class MessageBuilder: logger.error(f"二轮查询执行错误: {e}") await new_event.send(MessageChain().message("猪脑过载,请稍后再试")) - controller.stop() - try: await option_select_waiter(event) except TimeoutError: @@ -291,73 +345,29 @@ class MessageBuilder: """ 名剑统计 模式""" return await self.T2I_image_msg(event, lambda: self.jx3api.mingjiantongji(mode)) - async def mingshiwushiqiang(self, event: AstrMessageEvent, server: str): - """ 名士五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("名士五十强", server)) + async def banghuipaihang(self, event: AstrMessageEvent, server: str): + """帮会排行 服务器。""" + return await self.handler_plain_image_msg( + event, + lambda: self.jx3api.banghui_rank_menu(), + lambda selected: self.jx3api.rank_statistical_select(server,selected,), + ) - async def laojianghuwushiqiang(self, event: AstrMessageEvent, server: str): - """ 老江湖五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("老江湖五十强", server)) + async def zhenyingpaihang(self, event: AstrMessageEvent, server: str): + """阵营排行 服务器。""" + return await self.handler_plain_image_msg( + event, + lambda: self.jx3api.zhenying_rank_menu(), + lambda selected: self.jx3api.rank_statistical_select(server,selected,), + ) - async def bingjiacangjiawushiqiang(self, event: AstrMessageEvent, server: str): - """ 兵甲藏家五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("兵甲藏家五十强", server)) - - async def mingshiwushiqiang_mentor(self, event: AstrMessageEvent, server: str): - """ 名师五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("名师五十强", server)) - - async def zhengyingyingxiongwushiqiang(self, event: AstrMessageEvent, server: str): - """ 阵营英雄五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("阵营英雄五十强", server)) - - async def xinhuoxiangchuanwushiqiang(self, event: AstrMessageEvent, server: str): - """ 薪火相传五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("薪火相传五十强", server)) - - async def luyuanguangjiyibaiqiang(self, event: AstrMessageEvent, server: str): - """ 庐园广记一百强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("庐园广记一百强", server)) - - async def haoqishenbingbaojiawushiqiang(self, event: AstrMessageEvent, server: str): - """ 浩气神兵宝甲五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("浩气神兵宝甲五十强", server)) - - async def erenshenbingbaojiawushiqiang(self, event: AstrMessageEvent, server: str ): - """ 恶人神兵宝甲五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("恶人神兵宝甲五十强", server)) - - async def haoqiaixinbanghuiwushiqiang(self, event: AstrMessageEvent, server: str ): - """ 浩气爱心帮会五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("浩气爱心帮会五十强", server)) - - async def erenaixinbanghuiwushiqiang(self, event: AstrMessageEvent, server: str ): - """ 恶人爱心帮会五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("恶人爱心帮会五十强", server)) - - async def saijierenwushiqiang(self, event: AstrMessageEvent, server: str): - """ 赛季恶人五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("赛季恶人五十强", server)) - - async def saijihaoqiwushiqiang(self, event: AstrMessageEvent, server: str ): - """ 赛季浩气五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("赛季浩气五十强", server)) - - async def shangzhouerenwushiqiang(self, event: AstrMessageEvent, server: str ): - """ 上周恶人五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("上周恶人五十强", server)) - - async def shangzhouhaoqiwushiqiang(self, event: AstrMessageEvent, server: str ): - """ 上周浩气五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("上周浩气五十强", server)) - - async def benzhouerenwushiqiang(self, event: AstrMessageEvent, server: str ): - """ 本周恶人五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("本周恶人五十强", server)) - - async def benzhouhaoqiwushiqiang(self, event: AstrMessageEvent, server: str ): - """ 本周浩气五十强 服务器""" - return await self.T2I_image_msg(event, lambda: self.jx3api.rank_statistical("本周浩气五十强", server)) + async def qitapaihang(self, event: AstrMessageEvent, server: str): + """其他排行 服务器。""" + return await self.handler_plain_image_msg( + event, + lambda: self.jx3api.qita_rank_menu(), + lambda selected: self.jx3api.rank_statistical_select(server,selected,), + ) async def shilianpaixing(self, event: AstrMessageEvent, server: str , name: str): """ 试炼排行 服务器 心法 """ diff --git a/data/server_aliases.json b/data/server_aliases.json index 8052f64..6247a26 100644 --- a/data/server_aliases.json +++ b/data/server_aliases.json @@ -3,5 +3,15 @@ {"server": "唯我独尊", "aliases": ["唯满侠"]}, {"server": "乾坤一掷", "aliases": ["华乾"]}, {"server": "剑胆琴心", "aliases": ["煎蛋"]}, - {"server": "斗转星移", "aliases": ["姨妈"]} + {"server": "斗转星移", "aliases": ["姨妈"]}, + {"server": "蝶恋花", "aliases": []}, + {"server": "飞龙在天", "aliases": []}, + {"server": "绝代天骄", "aliases": []}, + {"server": "龙争虎斗", "aliases": []}, + {"server": "眉间雪", "aliases": []}, + {"server": "破阵子", "aliases": []}, + {"server": "山海相逢", "aliases": []}, + {"server": "天鹅坪", "aliases": []}, + {"server": "幽月轮", "aliases": []}, + {"server": "长安城", "aliases": []} ] diff --git a/main.py b/main.py index c792e8b..14b3306 100644 --- a/main.py +++ b/main.py @@ -26,7 +26,7 @@ PLUGIN_NAME = "astrbot_plugin_jx3" @register("astrbot_plugin_jx3", "fxdyz", "聚合剑网三游戏数据,提供查询、图片渲染、本地避雷和实时事件推送。", - "3.4.1", + "3.4.2", "https://github.com/qsc20001102/astrbot_plugin_jx3" ) class Jx3ApiPlugin(Star): @@ -177,6 +177,7 @@ class Jx3ApiPlugin(Star): self.bilei, self.event_push, self.icons, + self.conf.get("image_render_quality", {}), ) @@ -244,24 +245,11 @@ class Jx3ApiPlugin(Star): "战绩": self. jx3cmd.zhanji, "名剑排行": self. jx3cmd.mingjianpaihang, "名剑统计": self. jx3cmd.mingjiantongji, - "名士五十强": self. jx3cmd.mingshiwushiqiang, - "老江湖五十强": self. jx3cmd.laojianghuwushiqiang, - "兵甲藏家五十强": self. jx3cmd.bingjiacangjiawushiqiang, - "名师五十强": self. jx3cmd.mingshiwushiqiang_mentor, - "阵营英雄五十强": self. jx3cmd.zhengyingyingxiongwushiqiang, - "薪火相传五十强": self. jx3cmd.xinhuoxiangchuanwushiqiang, - "庐园广记一百强": self. jx3cmd.luyuanguangjiyibaiqiang, - "浩气神兵宝甲五十强": self. jx3cmd.haoqishenbingbaojiawushiqiang, - "恶人神兵宝甲五十强": self. jx3cmd.erenshenbingbaojiawushiqiang, - "浩气爱心帮会五十强": self. jx3cmd.haoqiaixinbanghuiwushiqiang, - "恶人爱心帮会五十强": self. jx3cmd.erenaixinbanghuiwushiqiang, - "赛季恶人五十强": self. jx3cmd.saijierenwushiqiang, - "赛季浩气五十强": self. jx3cmd.saijihaoqiwushiqiang, - "上周恶人五十强": self. jx3cmd.shangzhouerenwushiqiang, - "上周浩气五十强": self. jx3cmd.shangzhouhaoqiwushiqiang, - "本周恶人五十强": self. jx3cmd.benzhouerenwushiqiang, - "本周浩气五十强": self. jx3cmd.benzhouhaoqiwushiqiang, + "帮会排行": self. jx3cmd.banghuipaihang, + "阵营排行": self. jx3cmd.zhenyingpaihang, + "其他排行": self. jx3cmd.qitapaihang, "试炼排行": self. jx3cmd.shilianpaixing, + "资历": self. jx3cmd.zili, "阵营拍卖": self. jx3cmd.zhengyingpaimai, "的卢": self. jx3cmd.dilujilu, "金价": self. jx3cmd.jinjia, @@ -321,16 +309,16 @@ class Jx3ApiPlugin(Star): "开服": self. jx3cmd.kaifu, "技改": self. jx3cmd.jigai, "解密": self. jx3cmd.jiemi, - "副本": self. jx3cmd.fubeng, "掉落": self. jx3cmd.diaoluo, "宏": self. jx3cmd.hong, - "资历": self. jx3cmd.zili, "交易行": self. jx3cmd.jiaoyihang, "绑定区服": self.bind_server, "解绑区服": self.unbind_server, + "事件推送": self.jx3cmd.shijian_tuisong, + "避雷添加": self.jx3cmd.bilei_add, "避雷查看": self.jx3cmd.bilei_all, "避雷查询": self.jx3cmd.bilei_select, @@ -634,9 +622,8 @@ class Jx3ApiPlugin(Star): else: return error_response("别名必须是字符串或数组", status_code=400) try: - await self.kungfu_alias.save( + await self.kungfu_alias.save_aliases( payload.get("pzid"), - payload.get("name"), aliases, ) except ValueError as exc: @@ -653,6 +640,3 @@ class Jx3ApiPlugin(Star): - - - diff --git a/metadata.yaml b/metadata.yaml index 0a0d17c..c7d1080 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,7 +1,7 @@ name: astrbot_plugin_jx3 display_name: 剑网三游戏数据查询工具 desc: 聚合剑网三游戏数据,提供查询、图片渲染、本地避雷和实时事件推送。 -version: 3.4.1 +version: 3.4.2 author: 飞翔大野猪 repo: https://github.com/qsc20001102/astrbot_plugin_jx3 astrbot_version: ">=4.24.1" diff --git a/pages/server-management/app.js b/pages/server-management/app.js index a4bc55a..271e159 100644 --- a/pages/server-management/app.js +++ b/pages/server-management/app.js @@ -1,5 +1,6 @@ const bridge = window.AstrBotPluginPage; const state = { bindings: [], subscriptions: [], aliases: [], kungfu: [], servers: [], events: {} }; +const editing = { aliasServer: null, kungfuPzid: null }; let toastTimer; const byId = (id) => document.getElementById(id); @@ -32,6 +33,42 @@ function emptyRow(columnCount, message) { return row; } +function aliasText(aliases) { + return aliases.length ? aliases.join("、") : "无"; +} + +function inlineAliasEditor(aliases, label, onSave, onCancel) { + const input = document.createElement("input"); + input.className = "inline-editor"; + input.value = aliases.join(", "); + input.placeholder = "多个别名用逗号分隔"; + input.setAttribute("aria-label", label); + + const saveButton = button("保存", "", async () => { + saveButton.disabled = true; + cancelButton.disabled = true; + const saved = await onSave(input.value); + if (!saved) { + saveButton.disabled = false; + cancelButton.disabled = false; + input.focus(); + } + }); + const cancelButton = button("取消", "", onCancel); + + input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + saveButton.click(); + } else if (event.key === "Escape") { + event.preventDefault(); + cancelButton.click(); + } + }); + queueMicrotask(() => input.focus()); + return { input, controls: [saveButton, cancelButton] }; +} + function bindingMap() { return new Map(state.bindings.map((item) => [item.session_id, item.server])); } @@ -128,11 +165,22 @@ function renderSubscriptions() { function renderAliases() { const body = byId("aliases-body"); - if (!state.aliases.length) { - body.replaceChildren(emptyRow(3, "暂无区服别名配置")); + const aliasesByServer = new Map( + state.aliases.map((item) => [item.server, item.aliases]), + ); + const servers = [...new Set([ + ...state.servers, + ...aliasesByServer.keys(), + ])].sort((left, right) => left.localeCompare(right, "zh-CN")); + if (!servers.length) { + body.replaceChildren(emptyRow(3, "暂无标准区服数据")); return; } - body.replaceChildren(...state.aliases.map((item) => { + body.replaceChildren(...servers.map((serverName) => { + const item = { + server: serverName, + aliases: aliasesByServer.get(serverName) || [], + }; const row = document.createElement("tr"); const server = document.createElement("td"); const aliases = document.createElement("td"); @@ -141,19 +189,40 @@ function renderAliases() { aliases.dataset.label = "别名"; actions.dataset.label = "操作"; server.textContent = item.server; - aliases.textContent = item.aliases.length ? item.aliases.join("、") : "无"; + aliases.className = "alias-cell"; actions.className = "actions"; - actions.append( - button("编辑", "", () => { - byId("alias-server").value = item.server; - byId("alias-values").value = item.aliases.join(", "); - byId("alias-values").focus(); - }), - button("删除", "link-button--danger", async () => { - if (!window.confirm(`确认删除 ${item.server} 的全部别名?`)) return; - await mutate("aliases/delete", { server: item.server }, "别名已删除"); - }), - ); + if (editing.aliasServer === item.server) { + row.classList.add("is-editing"); + const editor = inlineAliasEditor( + item.aliases, + `${item.server}的区服别名`, + async (value) => { + editing.aliasServer = null; + const saved = await mutate( + "aliases/save", + { server: item.server, aliases: value }, + "区服别名已保存", + ); + if (!saved) { + editing.aliasServer = item.server; + renderAliases(); + } + return saved; + }, + () => { + editing.aliasServer = null; + renderAliases(); + }, + ); + aliases.append(editor.input); + actions.append(...editor.controls); + } else { + aliases.textContent = aliasText(item.aliases); + actions.append(button("编辑", "", () => { + editing.aliasServer = item.server; + renderAliases(); + })); + } row.append(server, aliases, actions); return row; })); @@ -162,30 +231,53 @@ function renderAliases() { function renderKungfu() { const body = byId("kungfu-body"); if (!state.kungfu.length) { - body.replaceChildren(emptyRow(4, "暂无心法配置")); + body.replaceChildren(emptyRow(3, "暂无心法配置")); return; } body.replaceChildren(...state.kungfu.map((item) => { const row = document.createElement("tr"); - const pzid = document.createElement("td"); const name = document.createElement("td"); const aliases = document.createElement("td"); const actions = document.createElement("td"); - pzid.dataset.label = "心法 ID"; name.dataset.label = "标准心法名"; aliases.dataset.label = "别名"; actions.dataset.label = "操作"; - pzid.textContent = String(item.pzid); name.textContent = item.name; - aliases.textContent = item.aliases.length ? item.aliases.join("、") : "无"; + aliases.className = "alias-cell"; actions.className = "actions"; - actions.append(button("编辑", "", () => { - byId("kungfu-pzid").value = String(item.pzid); - byId("kungfu-name").value = item.name; - byId("kungfu-aliases").value = item.aliases.join(", "); - byId("kungfu-aliases").focus(); - })); - row.append(pzid, name, aliases, actions); + if (editing.kungfuPzid === item.pzid) { + row.classList.add("is-editing"); + const editor = inlineAliasEditor( + item.aliases, + `${item.name}的心法别名`, + async (value) => { + editing.kungfuPzid = null; + const saved = await mutate( + "kungfu/save", + { pzid: item.pzid, aliases: value }, + "心法别名已保存", + ); + if (!saved) { + editing.kungfuPzid = item.pzid; + renderKungfu(); + } + return saved; + }, + () => { + editing.kungfuPzid = null; + renderKungfu(); + }, + ); + aliases.append(editor.input); + actions.append(...editor.controls); + } else { + aliases.textContent = aliasText(item.aliases); + actions.append(button("编辑", "", () => { + editing.kungfuPzid = item.pzid; + renderKungfu(); + })); + } + row.append(name, aliases, actions); return row; })); } @@ -241,25 +333,6 @@ byId("binding-form").addEventListener("submit", async (event) => { if (saved) event.currentTarget.reset(); }); -byId("alias-form").addEventListener("submit", async (event) => { - event.preventDefault(); - const saved = await mutate("aliases/save", { - server: byId("alias-server").value, - aliases: byId("alias-values").value, - }, "区服别名已保存"); - if (saved) event.currentTarget.reset(); -}); - -byId("kungfu-form").addEventListener("submit", async (event) => { - event.preventDefault(); - const saved = await mutate("kungfu/save", { - pzid: byId("kungfu-pzid").value, - name: byId("kungfu-name").value, - aliases: byId("kungfu-aliases").value, - }, "心法配置已保存"); - if (saved) event.currentTarget.reset(); -}); - byId("refresh").addEventListener("click", async (event) => { const control = event.currentTarget; control.disabled = true; diff --git a/pages/server-management/index.html b/pages/server-management/index.html index a3e7927..953dfa2 100644 --- a/pages/server-management/index.html +++ b/pages/server-management/index.html @@ -76,17 +76,9 @@