diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17e0fae..05caa30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
### version: 3.4.3(2026-09-05):
+接入 JX3API `POST /token/stats` 令牌统计接口,使用插件配置的 `jx3api_token` 查询等级、已用次数、剩余次数和有效状态;WebUI 顶部恢复为四项令牌统计,接口不可用或未配置令牌时显示未获取状态且不影响其他管理数据。
+
重构功能帮助图片:由不规则卡片区块改为统一的“功能分区 + 指令列表 + 说明”三列结构,每条指令行预留默认空白的说明区域,并整体放大分区、功能名、指令及辅助文字;按照当前 `command_map` 校正 18 个分区和 92 个真实触发词,角色记录指令由“聊天”更新为“发言”,同时补入“小药”“骗子”并移除未注册的“副本”指令。
新增基于 WebUI 与 SQLite 的“会话控制”:默认使用“全部会话”模式,也可切换为白名单或黑名单,并维护任意 AstrBot 会话 ID 及备注。访问策略同时作用于插件指令与实时事件推送;空白名单不放行任何会话,空黑名单放行全部会话。
@@ -10,7 +12,7 @@
插件 WebUI 的区服别名和心法别名页新增“恢复默认”操作;采用页面内可见的二次点击确认,不依赖嵌入页面可能无法显示的浏览器原生确认框。确认后分别读取随插件分发的 `data/server_aliases.json` 和 `data/kungfu.json`,在事务中完整重写对应数据库表并刷新页面数据。
-移除 WebUI 顶部统计区域;“会话控制”移动为第一个默认页签,并新增醒目的当前生效模式状态。模式卡片明确区分当前模式与尚未保存的选择,切换按钮同步显示目标模式。“会话绑定”更名为“区服绑定”并保留在“事件推送”之后。
+移除 WebUI 原有的会话绑定和事件推送数量统计,顶部区域改为显示 JX3API 令牌等级、已用次数、剩余次数和有效状态;“会话控制”移动为第一个默认页签,并新增醒目的当前生效模式状态。模式卡片明确区分当前模式与尚未保存的选择,切换按钮同步显示目标模式。“会话绑定”更名为“区服绑定”并保留在“事件推送”之后。
重构 WebUI 区服绑定交互:顶部入口改为“添加绑定”,会话 ID 可自定义但绑定区服只能从标准区服下拉框选择;列表编辑时会话 ID 保持只读,仅在对应绑定区服列内切换下拉框进行保存,解除绑定改为点击后直接删除对应记录。Web API 同步拒绝别名及任意文本作为绑定区服。
diff --git a/README.md b/README.md
index dd94590..23e72e8 100644
--- a/README.md
+++ b/README.md
@@ -291,7 +291,7 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通
### WebUI 插件管理
-AstrBot 插件详情页中的“剑网三插件管理”页面提供五个页签:会话控制、事件推送、区服绑定、区服别名和心法别名,不再显示顶部统计区域。会话控制为默认页签,页面会明确标出当前真正生效的模式,并区分尚未保存的模式选择。该功能默认使用“全部会话”,也可切换为白名单或黑名单;会话 ID 可从已有绑定和订阅记录中选择,也可直接输入,并可维护名单类型和备注。空白名单不放行任何会话,空黑名单放行全部会话,名单策略同时作用于插件指令和实时事件推送。
+AstrBot 插件详情页中的“剑网三插件管理”页面提供五个页签:会话控制、事件推送、区服绑定、区服别名和心法别名。顶部通过 JX3API `POST /token/stats` 展示当前配置令牌的等级、已用次数、剩余次数和有效状态;未配置令牌或接口暂时不可用时显示未获取,不影响其他管理功能。会话控制为默认页签,页面会明确标出当前真正生效的模式,并区分尚未保存的模式选择。该功能默认使用“全部会话”,也可切换为白名单或黑名单;会话 ID 可从已有绑定和订阅记录中选择,也可直接输入,并可维护名单类型和备注。空白名单不放行任何会话,空黑名单放行全部会话,名单策略同时作用于插件指令和实时事件推送。
区服绑定页可使用自定义会话 ID 添加记录,绑定区服只能从标准区服下拉框选择。编辑已有记录时会话 ID 保持只读,只在绑定区服列内修改,点击解除绑定会直接删除该行。事件推送页显示所有会话的总开关及订阅编号。区服别名页展示完整标准区服目录,标准名称只读,只能行内编辑别名;心法别名页隐藏 JX3BOX 配装 ID,标准心法名称只读,同样只允许行内编辑最多 5 个别名。两个别名页均可使用随插件分发的 JSON 种子完整覆盖数据库并恢复默认配置。管理页通过 AstrBot Plugin Pages 桥接调用插件 Web API,不直接访问 Dashboard 凭据。
diff --git a/core/jx3api_data.py b/core/jx3api_data.py
index b8cc5f2..845316a 100644
--- a/core/jx3api_data.py
+++ b/core/jx3api_data.py
@@ -116,6 +116,49 @@ class JX3APIService:
}
)
+ async def token_stats(self) -> Optional[Dict[str, Any]]:
+ """查询当前配置 JX3API Token 的等级、用量及有效状态。"""
+ if not str(self.token or "").strip():
+ return None
+
+ try:
+ data = await self._api.post(
+ "https://www.jx3api.com/token/stats",
+ data={"token": self.token},
+ out_key="data",
+ success_codes=(200, "200"),
+ return_error=True,
+ )
+ except Exception as exc:
+ logger.warning(f"查询 JX3API Token 统计失败: {exc}")
+ return None
+
+ if isinstance(data, APIErrorResponse):
+ logger.warning(
+ f"JX3API Token 统计返回错误: "
+ f"code={data.code}, msg={data.message or '未知错误'}"
+ )
+ return None
+ if not isinstance(data, dict):
+ return None
+
+ def nonnegative_int(value: Any) -> Optional[int]:
+ if isinstance(value, bool):
+ return None
+ try:
+ number = int(value)
+ except (TypeError, ValueError):
+ return None
+ return number if number >= 0 else None
+
+ valid = data.get("valid")
+ return {
+ "level": nonnegative_int(data.get("level")),
+ "used": nonnegative_int(data.get("used")),
+ "remaining": nonnegative_int(data.get("remaining")),
+ "valid": valid if isinstance(valid, bool) else None,
+ }
+
def _init_return_data(self) -> Dict[str, Any]:
"""初始化标准的返回数据结构"""
diff --git a/core/webui.py b/core/webui.py
index 37865f0..e643932 100644
--- a/core/webui.py
+++ b/core/webui.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import asyncio
import re
from typing import TYPE_CHECKING, Any
@@ -87,11 +88,21 @@ class WebUIService:
raise ValueError("别名必须是字符串或数组")
async def dashboard(self):
- bindings = await self.server_binding.list_bindings()
- subscriptions = await self.event_push.list_subscription_statuses()
- aliases = await self.server_binding.list_aliases()
- kungfu = await self.kungfu_alias.list_kungfu()
- session_control = await self.session_control.get_state()
+ (
+ bindings,
+ subscriptions,
+ aliases,
+ kungfu,
+ session_control,
+ token_stats,
+ ) = await asyncio.gather(
+ self.server_binding.list_bindings(),
+ self.event_push.list_subscription_statuses(),
+ self.server_binding.list_aliases(),
+ self.kungfu_alias.list_kungfu(),
+ self.session_control.get_state(),
+ self.jx3api.token_stats(),
+ )
return json_response(
{
"bindings": bindings,
@@ -103,6 +114,7 @@ class WebUIService:
str(action): name for action, name in EVENT_NAMES.items()
},
"session_control": session_control,
+ "token_stats": token_stats,
}
)
diff --git a/pages/server-management/app.js b/pages/server-management/app.js
index bef6740..a4da9a9 100644
--- a/pages/server-management/app.js
+++ b/pages/server-management/app.js
@@ -7,6 +7,7 @@ const state = {
servers: [],
events: {},
session_control: { mode: "all", entries: [] },
+ token_stats: null,
};
const editing = { bindingSession: null, controlSession: null, aliasServer: null, kungfuPzid: null };
const restoreConfirmationTimers = new WeakMap();
@@ -14,6 +15,33 @@ let toastTimer;
const byId = (id) => document.getElementById(id);
+function formatUsageCount(value) {
+ return Number.isSafeInteger(value) && value >= 0
+ ? value.toLocaleString("zh-CN")
+ : "—";
+}
+
+function renderTokenStats() {
+ const stats = state.token_stats;
+ byId("token-level").textContent = Number.isSafeInteger(stats?.level)
+ ? `LV.${stats.level}`
+ : "—";
+ byId("token-used").textContent = formatUsageCount(stats?.used);
+ byId("token-remaining").textContent = formatUsageCount(stats?.remaining);
+
+ const status = byId("token-valid");
+ status.classList.remove("token-status--valid", "token-status--invalid");
+ if (stats?.valid === true) {
+ status.textContent = "有效";
+ status.classList.add("token-status--valid");
+ } else if (stats?.valid === false) {
+ status.textContent = "无效";
+ status.classList.add("token-status--invalid");
+ } else {
+ status.textContent = "未获取";
+ }
+}
+
function showToast(message, isError = false) {
const toast = byId("toast");
toast.textContent = message;
@@ -525,6 +553,7 @@ function renderKungfu() {
}
function render() {
+ renderTokenStats();
renderServerOptions();
renderSessionOptions();
renderSessionControl();
diff --git a/pages/server-management/index.html b/pages/server-management/index.html
index 033b7a1..85fac96 100644
--- a/pages/server-management/index.html
+++ b/pages/server-management/index.html
@@ -17,6 +17,25 @@
+
+
+ 令牌等级
+ —
+
+
+ 已用次数
+ —
+
+
+ 剩余次数
+ —
+
+
+ 令牌状态
+ 未获取
+
+
+