diff --git a/CHANGELOG.md b/CHANGELOG.md index d691f53..4c60b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## 更新记录 +### version: 3.4.8(2026-09-07): + +插件配置新增 TLS 证书验证开关并默认开启,统一作用于 JX3API、JX3BOX HTTPS 请求和事件推送 WSS 连接;旧配置未包含该项时同样按开启处理,仅显式关闭时跳过证书验证。 + +WebUI 的全部区服选项改为直接读取本地区服别名表中的标准区服;顶部“刷新数据”仅重新读取管理页数据,移除远程区服目录刷新接口,不再由 WebUI 请求 `/server/status/check`。 + +最终图片缓存的静态资源签名由文件大小和修改时间改为“相对路径 + 文件内容 SHA-256”,并采用分块读取。相同内容的插件重新安装后可继续命中保留的图片缓存,只有模板、样式或图片内容实际变化时才使缓存键失效;升级到本版时旧签名生成的图片缓存会统一失效一次并按原过期时间清理。 + ### version: 3.4.7(2026-09-07): 移除 JX3API 请求失败时使用七天内过期缓存的兜底逻辑。接口缓存过期后重新请求上游,权限拒绝、业务错误及网络失败均不再返回旧数据;强制刷新失败时也会清除该请求原有的接口缓存。 diff --git a/README.md b/README.md index d719562..1e201ea 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ pip install -r data/plugins/astrbot_plugin_jx3/requirements.txt | `aiohttp` | 异步 HTTP 请求与连接复用 | | `aiofiles` | 异步读取 HTML 模板 | | `aiosqlite` | 异步访问本地 SQLite 数据库 | -| `matplotlib` | 当前依赖清单保留的绘图依赖;v3.4.7 业务代码未直接导入 | +| `matplotlib` | 当前依赖清单保留的绘图依赖;v3.4.8 业务代码未直接导入 | ## 插件配置 @@ -86,11 +86,12 @@ pip install -r data/plugins/astrbot_plugin_jx3/requirements.txt | `jx3api_ticket` | `string` | 空 | 部分名剑和心法接口需要的推栏 Ticket | | `jx3api_wss` | `string` | `wss://socket.nicemoe.cn` | JX3API 事件通道地址 | | `jx3api_wss_token` | `string` | 空 | 事件版令牌;免费事件无需填写 | +| `tls_verify` | `bool` | `true` | 验证 JX3API、JX3BOX 和事件推送连接的 TLS 证书;仅建议在受信任的调试环境中临时关闭 | | `image_render_quality.device_scale_factor` | `string` | `1.0` | 渲染清晰度,可选 `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 地址或事件版令牌后需要重新加载插件。 +会话控制、区服绑定、区服/心法别名、避雷记录、事件订阅及缓存配置均保存在本地 SQLite。修改 WebSocket 地址、事件版令牌或 TLS 证书验证开关后需要重新加载插件。 ### Token 与 Ticket @@ -314,7 +315,7 @@ AstrBot 插件详情页中的“剑网三插件管理”通过 Plugin Pages 桥 事件推送配置保存后立即用于后续事件分发,仍受会话访问模式和绑定区服限制。新增时可以选择已有会话或输入完整的 AstrBot 会话 ID,重复新增会提示编辑已有配置;编辑时会话 ID 只读。空事件选择表示不接收任何事件;删除仅移除该会话的推送配置,保留区服绑定、访问名单和其他会话数据。免费事件与令牌事件分组展示,令牌事件仍需配置 `jx3api_wss_token`。 -页面顶部通过 JX3API `POST /token/stats` 展示当前 Token 的等级、已用次数、剩余次数和有效状态,成功结果在进程内保留 30 秒。普通打开或刷新 WebUI 时,区服目录、别名和绑定直接读取当前内存/SQLite 数据,不会请求区服状态接口;只有点击“刷新区服列表”时才会强制请求 `/server/status/check` 并更新当前区服目录。插件初始化时会执行一次 `server_list()` 建立用于参数消歧的有效区服目录:缓存有效时读取接口缓存,缓存不存在或已过期时才请求上游。 +页面顶部通过 JX3API `POST /token/stats` 展示当前 Token 的等级、已用次数、剩余次数和有效状态,成功结果在进程内保留 30 秒。WebUI 的全部区服选项直接读取本地 `server_aliases` 表中的标准区服,顶部“刷新数据”只重新读取管理页数据,不会请求 `/server/status/check`。插件初始化时仍会执行一次 `server_list()` 建立用于指令参数消歧的有效区服目录:缓存有效时读取接口缓存,缓存不存在或已过期时才请求上游。 缓存管理的接口默认时间为 300 秒,图片默认时间为 600 秒;每个 JX3API 接口和每个图片指令都可以单独覆盖,填写 `0` 表示关闭,恢复默认则重新继承全局时间。图片指令优先查询最终图片缓存,命中后不会再调用上游接口或重新渲染;图文结果会连同正文一起复用。单独清除接口会同时删除该路径所有参数组合的内存与 SQLite 缓存,单独清除图片会删除该指令生成的缓存文件,两种操作均不改变已配置时间。 @@ -483,7 +484,7 @@ AstrBot 的渲染接口接收完整 HTML 字符串,因此插件不会依赖渲 `api_response_cache` 仅保存通过 JX3API 查询入口成功取得的原始 JSON;Token 和 Ticket 不写入缓存键或正文,凭据摘要只用于避免更换账号后误用旧缓存。同一缓存键的并发请求通过异步锁合并。读取顺序为“内存热缓存 → SQLite → 上游接口”;两级缓存使用同一项 TTL,内存按总字节数限制,SQLite 按记录条数限制。缓存过期后重新请求上游,权限拒绝、业务错误和网络失败均返回失败结果,不再使用旧数据兜底;强制刷新失败也会清除该请求原有的接口缓存。每 60 秒根据当前有效 TTL 自动清理过期接口记录,启动时同步淘汰历史超额记录;插件停用和初始化失败时回收清理任务。 -图片缓存不会把二进制写入 SQLite。图片消息处理器会在请求业务数据前,使用当前指令名、完整有效参数、二轮选择项、必要的会话范围、静态资源签名和截图参数生成 SHA-256 键;命中时直接把本地路径交给消息事件发送。未命中时才请求数据并渲染,再把 AstrBot 临时渲染结果复制到 `cache/images/`;SQLite 的 `image_render_cache` 只保存文件索引、大小、时间、最近访问记录和图文消息正文。超过 WebUI 配置的容量(默认 512 MB)后按最近最少使用顺序清理。 +图片缓存不会把二进制写入 SQLite。图片消息处理器会在请求业务数据前,使用当前指令名、完整有效参数、二轮选择项、必要的会话范围、静态资源签名和截图参数生成 SHA-256 键;静态资源签名按相对路径和文件内容计算,不受重新安装产生的文件修改时间变化影响。命中时直接把本地路径交给消息事件发送。未命中时才请求数据并渲染,再把 AstrBot 临时渲染结果复制到 `cache/images/`;SQLite 的 `image_render_cache` 只保存文件索引、大小、时间、最近访问记录和图文消息正文。超过 WebUI 配置的容量(默认 512 MB)后按最近最少使用顺序清理。 ### 7. 实时事件推送 @@ -563,20 +564,20 @@ git diff --check ## 当前版本状态 -以下内容是对 v3.4.7 当前源码的静态核对结果,部署和二次开发前应注意: +以下内容是对 v3.4.8 当前源码的静态核对结果,部署和二次开发前应注意: 1. 查询图片使用浅色高对比主题和放大的内容区域;渲染清晰度、JPEG/PNG 格式及 JPEG 质量由 `image_render_quality` 配置组控制,提高清晰度或使用 PNG 会增加图片体积与渲染耗时。 2. 所有 HTML 渲染图片底部都会显示数据时间;最终图片缓存命中后保留原时间并跳过接口请求和重新渲染。要让下一次查询同时获取最新上游数据并重新生成图片,需要一并清除或关闭对应接口缓存和图片缓存。 3. `帮会排行`、`阵营排行`、`其他排行` 使用固定的“数字序号 → 榜单名称”首轮菜单,次轮查询统一截取接口返回的前 50 条;取得有效选择后会先停止超时计时器,避免渲染期间重复输出。 4. 区服绑定 WebUI 的新增及行内编辑均只接受标准区服,聊天指令仍可使用区服别名;区服别名页展示完整标准区服目录并只允许行内编辑别名,心法别名页隐藏配装 ID,后端保存接口也只更新已有心法的别名。“恢复默认”使用页面内二次点击确认,确认后会用随包 JSON 完整覆盖对应数据库表,当前自定义别名无法从页面撤销恢复。 -5. 普通打开或刷新 WebUI 不会刷新远程区服目录;初始化插件时会先读取 `/server/status/check` 的接口缓存,缓存不可用才请求上游,点击“刷新区服列表”则会强制请求上游。WebUI 顶部 Token 统计最多每 30 秒请求一次 `/token/stats`。 +5. WebUI 的区服选项只使用本地区服别名表中的标准区服,打开或刷新页面均不会请求 `/server/status/check`;插件初始化时仍会优先读取该接口的缓存,缓存不可用才请求上游。WebUI 顶部 Token 统计最多每 30 秒请求一次 `/token/stats`。 6. 会话控制默认放行全部会话;切换白名单后只有白名单记录可执行插件指令及接收推送,切换黑名单后仅拦截黑名单记录。名单记录会保留在数据库中,切换模式不会删除已有配置。 7. 避雷记录按当前消息的 `unified_msg_origin` 隔离;升级前的无会话归属记录保留在普通会话不可访问的历史公共数据区。 8. `决斗挑战` 当前处理器把模式声明为整数但使用中文字符串映射,实际输入无法切换到私密模式并会回退为公开;修正处理器类型前不应在外部说明中宣称私密模式可用。 9. aiocqhttp 图片发送返回 `retcode=1200` 时只记录警告,不再追加“猪脑过载”提示;该回执代表发送结果不确定,仍建议结合平台日志确认实际送达情况。 10. 带区服参数的指令在入口层统一进行会话绑定补齐和别名解析;新增此类指令时参数名应继续使用 `server`。 11. 资历分布使用固定的“数字序号 → 大类名称”首轮菜单,通用会话裁剪出单项字典后交给 JX3API `/tuilan/achievement` 次轮查询;模板兼容总览与指定 `subclass` 的不同 `data.total` 层级,并直接使用接口角色字段、`pieces`、`seniority` 和 `score/totalScore` 渲染,JX3BOX 不再参与资历查询。 -12. `APIClient` 当前默认 `ssl_verify=False`,即外部 HTTPS 请求不校验证书。对传输安全有要求的部署应先评估并调整该设置。 +12. 外部 HTTPS 与 WSS 连接默认验证 TLS 证书;只有在插件配置中显式关闭 `tls_verify` 才会跳过验证。生产环境应保持开启。 13. debug 日志会记录请求 Query、Body 和完整响应,其中可能包含 Token、Ticket 或其他敏感字段;不要公开原始调试日志。 ## 注意事项 diff --git a/_conf_schema.json b/_conf_schema.json index 73bf902..d843b72 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -41,6 +41,12 @@ "default": "", "hint": "用于付费事件推送,不填写只推送免费事件。" }, + "tls_verify": { + "description": "TLS 证书验证", + "type": "bool", + "default": true, + "hint": "验证 JX3API、JX3BOX 和事件推送连接的 TLS 证书。建议保持开启;仅在受信任的调试环境中临时关闭。" + }, "image_render_quality": { "description": "图片渲染质量", "type": "object", diff --git a/core/cache.py b/core/cache.py index b8ff2ce..9ed1671 100644 --- a/core/cache.py +++ b/core/cache.py @@ -154,22 +154,29 @@ class CacheService: @staticmethod def _build_asset_signature(roots: Iterable[Path]) -> str: - parts: list[str] = [] - for root in roots: + signature = hashlib.sha256() + for root_index, root in enumerate(roots): path = Path(root) if not path.exists(): continue for item in sorted( - candidate for candidate in path.rglob("*") if candidate.is_file() + (candidate for candidate in path.rglob("*") if candidate.is_file()), + key=lambda candidate: candidate.relative_to(path).as_posix(), ): try: - stat = item.stat() + content_hash = hashlib.sha256() + with item.open("rb") as file: + while chunk := file.read(1024 * 1024): + content_hash.update(chunk) except OSError: continue - parts.append( - f"{item.relative_to(path)}:{stat.st_size}:{stat.st_mtime_ns}" - ) - return hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest() + relative_path = item.relative_to(path).as_posix() + signature.update(str(root_index).encode("ascii")) + signature.update(b"\0") + signature.update(relative_path.encode("utf-8")) + signature.update(b"\0") + signature.update(content_hash.digest()) + return signature.hexdigest() async def initialize(self): self.image_dir.mkdir(parents=True, exist_ok=True) diff --git a/core/event_push.py b/core/event_push.py index 61ac11b..682ff80 100644 --- a/core/event_push.py +++ b/core/event_push.py @@ -334,6 +334,7 @@ class EventPushService: self.session_control = session_control self.url = str(config.get("jx3api_wss", "") or DEFAULT_WSS_URL).strip() self.token = str(config.get("jx3api_wss_token", "") or "").strip() + self.tls_verify = config.get("tls_verify", True) is not False self._runner: asyncio.Task | None = None self._session: ClientSession | None = None self._websocket: aiohttp.ClientWebSocketResponse | None = None @@ -408,6 +409,7 @@ class EventPushService: connection_url, heartbeat=45, autoping=True, + ssl=self.tls_verify, ) as websocket: self._websocket = websocket retry_count = 0 diff --git a/core/jx3api_data.py b/core/jx3api_data.py index bd2f9b3..63b8208 100644 --- a/core/jx3api_data.py +++ b/core/jx3api_data.py @@ -86,10 +86,12 @@ class JX3APIService: sqlite: AsyncSQLiteDB, cache: Optional["CacheService"] = None, ): - # 实例化 API Client - self._api: APIClient = APIClient() # 引用插件配置文件 self._config = config + # 仅显式配置为 false 时关闭证书验证,旧配置默认安全开启。 + self._api: APIClient = APIClient( + ssl_verify=self._config.get("tls_verify", True) is not False + ) # 引用sqlite self._sql_db = sqlite self._cache = cache diff --git a/core/jx3box_data.py b/core/jx3box_data.py index d8773a5..b9cf8b6 100644 --- a/core/jx3box_data.py +++ b/core/jx3box_data.py @@ -23,10 +23,12 @@ JX3BOX_API_BASE_URLS = { class JX3BOXService: def __init__(self, config: AstrBotConfig, sqlite: AsyncSQLiteDB, cache_sqlite: Optional[AsyncSQLiteDB] = None): - # 实例化 API Client - self._api: APIClient = APIClient() # 引用插件配置文件 self._config = config + # 仅显式配置为 false 时关闭证书验证,旧配置默认安全开启。 + self._api: APIClient = APIClient( + ssl_verify=self._config.get("tls_verify", True) is not False + ) # 引用sqlite self._sql_db = sqlite self._cache_db = cache_sqlite or sqlite diff --git a/core/request.py b/core/request.py index 893decd..b019b19 100644 --- a/core/request.py +++ b/core/request.py @@ -26,7 +26,7 @@ class APIClient: 3. 支持异步上下文管理器 (Async Context Manager)。 """ - def __init__(self, base_timeout: int = 10, ssl_verify: bool = False): + def __init__(self, base_timeout: int = 10, ssl_verify: bool = True): self.base_timeout = base_timeout self.ssl_verify = ssl_verify self._session: Optional[ClientSession] = None diff --git a/core/webui.py b/core/webui.py index 13385c9..2e4f76d 100644 --- a/core/webui.py +++ b/core/webui.py @@ -62,7 +62,6 @@ class WebUIService: ("aliases/restore", self.restore_aliases, ["POST"], "恢复默认区服别名"), ("kungfu/save", self.save_kungfu, ["POST"], "保存心法别名"), ("kungfu/restore", self.restore_kungfu, ["POST"], "恢复默认心法别名"), - ("servers/refresh", self.refresh_servers, ["POST"], "刷新区服目录"), ( "session-control/mode", self.save_session_control_mode, @@ -141,7 +140,7 @@ class WebUIService: "subscriptions": subscriptions, "aliases": aliases, "kungfu": kungfu, - "servers": self.server_binding.standard_servers(), + "servers": [item["server"] for item in aliases], "events": {str(action): name for action, name in EVENT_NAMES.items()}, "free_event_actions": sorted(FREE_EVENT_ACTIONS), "session_control": session_control, @@ -244,13 +243,6 @@ class WebUIService: return error_response(str(exc), status_code=500) return json_response({"restored": restored}) - async def refresh_servers(self): - servers = await self.jx3api.server_list(force_refresh=True) - if not servers: - return error_response("区服目录刷新失败", status_code=502) - await self.server_binding.update_server_catalog(servers) - return json_response({"servers": self.server_binding.standard_servers()}) - async def save_session_control_mode(self): try: payload = await self._json_payload() diff --git a/main.py b/main.py index 4bf0300..d8b2a81 100644 --- a/main.py +++ b/main.py @@ -26,7 +26,7 @@ PLUGIN_NAME = "astrbot_plugin_jx3" @register("astrbot_plugin_jx3", "fxdyz", "聚合剑网三游戏数据,提供查询、图片渲染、本地避雷和实时事件推送。", - "3.4.7", + "3.4.8", "https://github.com/qsc20001102/astrbot_plugin_jx3" ) class Jx3ApiPlugin(Star): diff --git a/metadata.yaml b/metadata.yaml index 8645d56..0e0a3f4 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,7 +1,7 @@ name: astrbot_plugin_jx3 display_name: 剑网三游戏数据查询工具 desc: 聚合剑网三游戏数据,提供查询、图片渲染、本地避雷和实时事件推送。 -version: 3.4.7 +version: 3.4.8 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 8002c7f..5bac52d 100644 --- a/pages/server-management/app.js +++ b/pages/server-management/app.js @@ -1101,9 +1101,8 @@ byId("refresh").addEventListener("click", async (event) => { const control = event.currentTarget; control.disabled = true; try { - await bridge.apiPost("servers/refresh", {}); await loadData(); - showToast("区服目录与页面数据已刷新"); + showToast("页面数据已刷新"); } catch (error) { showToast(error?.message || "刷新失败", true); } finally {