fxdyz
This commit is contained in:
+3
-1
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
### version: 3.4.3(2026-09-05):
|
### version: 3.4.3(2026-09-05):
|
||||||
|
|
||||||
|
接入 JX3API `POST /token/stats` 令牌统计接口,使用插件配置的 `jx3api_token` 查询等级、已用次数、剩余次数和有效状态;WebUI 顶部恢复为四项令牌统计,接口不可用或未配置令牌时显示未获取状态且不影响其他管理数据。
|
||||||
|
|
||||||
重构功能帮助图片:由不规则卡片区块改为统一的“功能分区 + 指令列表 + 说明”三列结构,每条指令行预留默认空白的说明区域,并整体放大分区、功能名、指令及辅助文字;按照当前 `command_map` 校正 18 个分区和 92 个真实触发词,角色记录指令由“聊天”更新为“发言”,同时补入“小药”“骗子”并移除未注册的“副本”指令。
|
重构功能帮助图片:由不规则卡片区块改为统一的“功能分区 + 指令列表 + 说明”三列结构,每条指令行预留默认空白的说明区域,并整体放大分区、功能名、指令及辅助文字;按照当前 `command_map` 校正 18 个分区和 92 个真实触发词,角色记录指令由“聊天”更新为“发言”,同时补入“小药”“骗子”并移除未注册的“副本”指令。
|
||||||
|
|
||||||
新增基于 WebUI 与 SQLite 的“会话控制”:默认使用“全部会话”模式,也可切换为白名单或黑名单,并维护任意 AstrBot 会话 ID 及备注。访问策略同时作用于插件指令与实时事件推送;空白名单不放行任何会话,空黑名单放行全部会话。
|
新增基于 WebUI 与 SQLite 的“会话控制”:默认使用“全部会话”模式,也可切换为白名单或黑名单,并维护任意 AstrBot 会话 ID 及备注。访问策略同时作用于插件指令与实时事件推送;空白名单不放行任何会话,空黑名单放行全部会话。
|
||||||
@@ -10,7 +12,7 @@
|
|||||||
|
|
||||||
插件 WebUI 的区服别名和心法别名页新增“恢复默认”操作;采用页面内可见的二次点击确认,不依赖嵌入页面可能无法显示的浏览器原生确认框。确认后分别读取随插件分发的 `data/server_aliases.json` 和 `data/kungfu.json`,在事务中完整重写对应数据库表并刷新页面数据。
|
插件 WebUI 的区服别名和心法别名页新增“恢复默认”操作;采用页面内可见的二次点击确认,不依赖嵌入页面可能无法显示的浏览器原生确认框。确认后分别读取随插件分发的 `data/server_aliases.json` 和 `data/kungfu.json`,在事务中完整重写对应数据库表并刷新页面数据。
|
||||||
|
|
||||||
移除 WebUI 顶部统计区域;“会话控制”移动为第一个默认页签,并新增醒目的当前生效模式状态。模式卡片明确区分当前模式与尚未保存的选择,切换按钮同步显示目标模式。“会话绑定”更名为“区服绑定”并保留在“事件推送”之后。
|
移除 WebUI 原有的会话绑定和事件推送数量统计,顶部区域改为显示 JX3API 令牌等级、已用次数、剩余次数和有效状态;“会话控制”移动为第一个默认页签,并新增醒目的当前生效模式状态。模式卡片明确区分当前模式与尚未保存的选择,切换按钮同步显示目标模式。“会话绑定”更名为“区服绑定”并保留在“事件推送”之后。
|
||||||
|
|
||||||
重构 WebUI 区服绑定交互:顶部入口改为“添加绑定”,会话 ID 可自定义但绑定区服只能从标准区服下拉框选择;列表编辑时会话 ID 保持只读,仅在对应绑定区服列内切换下拉框进行保存,解除绑定改为点击后直接删除对应记录。Web API 同步拒绝别名及任意文本作为绑定区服。
|
重构 WebUI 区服绑定交互:顶部入口改为“添加绑定”,会话 ID 可自定义但绑定区服只能从标准区服下拉框选择;列表编辑时会话 ID 保持只读,仅在对应绑定区服列内切换下拉框进行保存,解除绑定改为点击后直接删除对应记录。Web API 同步拒绝别名及任意文本作为绑定区服。
|
||||||
|
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通
|
|||||||
|
|
||||||
### WebUI 插件管理
|
### WebUI 插件管理
|
||||||
|
|
||||||
AstrBot 插件详情页中的“剑网三插件管理”页面提供五个页签:会话控制、事件推送、区服绑定、区服别名和心法别名,不再显示顶部统计区域。会话控制为默认页签,页面会明确标出当前真正生效的模式,并区分尚未保存的模式选择。该功能默认使用“全部会话”,也可切换为白名单或黑名单;会话 ID 可从已有绑定和订阅记录中选择,也可直接输入,并可维护名单类型和备注。空白名单不放行任何会话,空黑名单放行全部会话,名单策略同时作用于插件指令和实时事件推送。
|
AstrBot 插件详情页中的“剑网三插件管理”页面提供五个页签:会话控制、事件推送、区服绑定、区服别名和心法别名。顶部通过 JX3API `POST /token/stats` 展示当前配置令牌的等级、已用次数、剩余次数和有效状态;未配置令牌或接口暂时不可用时显示未获取,不影响其他管理功能。会话控制为默认页签,页面会明确标出当前真正生效的模式,并区分尚未保存的模式选择。该功能默认使用“全部会话”,也可切换为白名单或黑名单;会话 ID 可从已有绑定和订阅记录中选择,也可直接输入,并可维护名单类型和备注。空白名单不放行任何会话,空黑名单放行全部会话,名单策略同时作用于插件指令和实时事件推送。
|
||||||
|
|
||||||
区服绑定页可使用自定义会话 ID 添加记录,绑定区服只能从标准区服下拉框选择。编辑已有记录时会话 ID 保持只读,只在绑定区服列内修改,点击解除绑定会直接删除该行。事件推送页显示所有会话的总开关及订阅编号。区服别名页展示完整标准区服目录,标准名称只读,只能行内编辑别名;心法别名页隐藏 JX3BOX 配装 ID,标准心法名称只读,同样只允许行内编辑最多 5 个别名。两个别名页均可使用随插件分发的 JSON 种子完整覆盖数据库并恢复默认配置。管理页通过 AstrBot Plugin Pages 桥接调用插件 Web API,不直接访问 Dashboard 凭据。
|
区服绑定页可使用自定义会话 ID 添加记录,绑定区服只能从标准区服下拉框选择。编辑已有记录时会话 ID 保持只读,只在绑定区服列内修改,点击解除绑定会直接删除该行。事件推送页显示所有会话的总开关及订阅编号。区服别名页展示完整标准区服目录,标准名称只读,只能行内编辑别名;心法别名页隐藏 JX3BOX 配装 ID,标准心法名称只读,同样只允许行内编辑最多 5 个别名。两个别名页均可使用随插件分发的 JSON 种子完整覆盖数据库并恢复默认配置。管理页通过 AstrBot Plugin Pages 桥接调用插件 Web API,不直接访问 Dashboard 凭据。
|
||||||
|
|
||||||
|
|||||||
@@ -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]:
|
def _init_return_data(self) -> Dict[str, Any]:
|
||||||
"""初始化标准的返回数据结构"""
|
"""初始化标准的返回数据结构"""
|
||||||
|
|||||||
+17
-5
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
@@ -87,11 +88,21 @@ class WebUIService:
|
|||||||
raise ValueError("别名必须是字符串或数组")
|
raise ValueError("别名必须是字符串或数组")
|
||||||
|
|
||||||
async def dashboard(self):
|
async def dashboard(self):
|
||||||
bindings = await self.server_binding.list_bindings()
|
(
|
||||||
subscriptions = await self.event_push.list_subscription_statuses()
|
bindings,
|
||||||
aliases = await self.server_binding.list_aliases()
|
subscriptions,
|
||||||
kungfu = await self.kungfu_alias.list_kungfu()
|
aliases,
|
||||||
session_control = await self.session_control.get_state()
|
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(
|
return json_response(
|
||||||
{
|
{
|
||||||
"bindings": bindings,
|
"bindings": bindings,
|
||||||
@@ -103,6 +114,7 @@ class WebUIService:
|
|||||||
str(action): name for action, name in EVENT_NAMES.items()
|
str(action): name for action, name in EVENT_NAMES.items()
|
||||||
},
|
},
|
||||||
"session_control": session_control,
|
"session_control": session_control,
|
||||||
|
"token_stats": token_stats,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const state = {
|
|||||||
servers: [],
|
servers: [],
|
||||||
events: {},
|
events: {},
|
||||||
session_control: { mode: "all", entries: [] },
|
session_control: { mode: "all", entries: [] },
|
||||||
|
token_stats: null,
|
||||||
};
|
};
|
||||||
const editing = { bindingSession: null, controlSession: null, aliasServer: null, kungfuPzid: null };
|
const editing = { bindingSession: null, controlSession: null, aliasServer: null, kungfuPzid: null };
|
||||||
const restoreConfirmationTimers = new WeakMap();
|
const restoreConfirmationTimers = new WeakMap();
|
||||||
@@ -14,6 +15,33 @@ let toastTimer;
|
|||||||
|
|
||||||
const byId = (id) => document.getElementById(id);
|
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) {
|
function showToast(message, isError = false) {
|
||||||
const toast = byId("toast");
|
const toast = byId("toast");
|
||||||
toast.textContent = message;
|
toast.textContent = message;
|
||||||
@@ -525,6 +553,7 @@ function renderKungfu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
renderTokenStats();
|
||||||
renderServerOptions();
|
renderServerOptions();
|
||||||
renderSessionOptions();
|
renderSessionOptions();
|
||||||
renderSessionControl();
|
renderSessionControl();
|
||||||
|
|||||||
@@ -17,6 +17,25 @@
|
|||||||
<button class="button button--secondary" id="refresh" type="button">刷新数据</button>
|
<button class="button button--secondary" id="refresh" type="button">刷新数据</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<section class="token-summary" aria-label="JX3API 令牌统计">
|
||||||
|
<div class="token-stat">
|
||||||
|
<span>令牌等级</span>
|
||||||
|
<strong id="token-level">—</strong>
|
||||||
|
</div>
|
||||||
|
<div class="token-stat">
|
||||||
|
<span>已用次数</span>
|
||||||
|
<strong id="token-used">—</strong>
|
||||||
|
</div>
|
||||||
|
<div class="token-stat">
|
||||||
|
<span>剩余次数</span>
|
||||||
|
<strong id="token-remaining">—</strong>
|
||||||
|
</div>
|
||||||
|
<div class="token-stat">
|
||||||
|
<span>令牌状态</span>
|
||||||
|
<strong class="token-status" id="token-valid">未获取</strong>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<nav class="tabs" aria-label="管理分类" role="tablist">
|
<nav class="tabs" aria-label="管理分类" role="tablist">
|
||||||
<button class="tab is-active" type="button" role="tab" aria-selected="true" data-tab="session-control">会话控制</button>
|
<button class="tab is-active" type="button" role="tab" aria-selected="true" data-tab="session-control">会话控制</button>
|
||||||
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="subscriptions">事件推送</button>
|
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="subscriptions">事件推送</button>
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ h1 { margin: 0; font-size: clamp(26px, 3vw, 36px); line-height: 1.2; letter-spac
|
|||||||
.button--secondary:hover { border-color: color-mix(in srgb, var(--text) 24%, var(--border)); }
|
.button--secondary:hover { border-color: color-mix(in srgb, var(--text) 24%, var(--border)); }
|
||||||
.button--danger { color: #fff; background: var(--danger); border-color: var(--danger); }
|
.button--danger { color: #fff; background: var(--danger); border-color: var(--danger); }
|
||||||
.button--danger:hover { background: color-mix(in srgb, var(--danger) 88%, #000); border-color: transparent; }
|
.button--danger:hover { background: color-mix(in srgb, var(--danger) 88%, #000); border-color: transparent; }
|
||||||
|
.token-summary { display: grid; grid-template-columns: repeat(4, 1fr); overflow: hidden; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow); }
|
||||||
|
.token-stat { display: flex; min-height: 104px; flex-direction: column; justify-content: center; padding: 20px 26px; }
|
||||||
|
.token-stat + .token-stat { border-left: 1px solid var(--border); }
|
||||||
|
.token-stat span { color: var(--muted); font-size: 13px; font-weight: 700; }
|
||||||
|
.token-stat strong { margin-top: 3px; font-size: 28px; line-height: 1.3; font-variant-numeric: tabular-nums; }
|
||||||
|
.token-status { color: var(--muted); }
|
||||||
|
.token-status--valid { color: var(--success); }
|
||||||
|
.token-status--invalid { color: var(--danger); }
|
||||||
.tabs { display: flex; gap: 30px; margin-top: 24px; border-bottom: 1px solid var(--border); }
|
.tabs { display: flex; gap: 30px; margin-top: 24px; border-bottom: 1px solid var(--border); }
|
||||||
.tab { position: relative; padding: 14px 4px 15px; border: 0; background: transparent; color: var(--muted); cursor: pointer; font-weight: 650; }
|
.tab { position: relative; padding: 14px 4px 15px; border: 0; background: transparent; color: var(--muted); cursor: pointer; font-weight: 650; }
|
||||||
.tab:hover, .tab.is-active { color: var(--accent); }
|
.tab:hover, .tab.is-active { color: var(--accent); }
|
||||||
@@ -112,6 +120,11 @@ tbody tr.is-editing { background: color-mix(in srgb, var(--accent-soft) 62%, tra
|
|||||||
.shell { width: min(100% - 24px, 1260px); padding-top: 20px; }
|
.shell { width: min(100% - 24px, 1260px); padding-top: 20px; }
|
||||||
.page-header { align-items: stretch; flex-direction: column; }
|
.page-header { align-items: stretch; flex-direction: column; }
|
||||||
.section-intro--actions { align-items: stretch; flex-direction: column; }
|
.section-intro--actions { align-items: stretch; flex-direction: column; }
|
||||||
|
.token-summary { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.token-stat { min-height: 88px; padding: 15px 18px; }
|
||||||
|
.token-stat:nth-child(3) { border-top: 1px solid var(--border); border-left: 0; }
|
||||||
|
.token-stat:nth-child(4) { border-top: 1px solid var(--border); }
|
||||||
|
.token-stat strong { font-size: 24px; }
|
||||||
.tabs { gap: 16px; overflow-x: auto; }
|
.tabs { gap: 16px; overflow-x: auto; }
|
||||||
.tab { flex: 0 0 auto; }
|
.tab { flex: 0 0 auto; }
|
||||||
.editor { grid-template-columns: 1fr; }
|
.editor { grid-template-columns: 1fr; }
|
||||||
|
|||||||
Reference in New Issue
Block a user