fxdyz
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
## 更新记录
|
||||
|
||||
### version: 3.4.3(2026-09-05):
|
||||
|
||||
重构功能帮助图片:由不规则卡片区块改为统一的“功能分区 + 指令列表 + 说明”三列结构,每条指令行预留默认空白的说明区域,并整体放大分区、功能名、指令及辅助文字;按照当前 `command_map` 校正 18 个分区和 92 个真实触发词,角色记录指令由“聊天”更新为“发言”,同时补入“小药”“骗子”并移除未注册的“副本”指令。
|
||||
|
||||
新增基于 WebUI 与 SQLite 的“会话控制”:默认使用“全部会话”模式,也可切换为白名单或黑名单,并维护任意 AstrBot 会话 ID 及备注。访问策略同时作用于插件指令与实时事件推送;空白名单不放行任何会话,空黑名单放行全部会话。
|
||||
|
||||
整理 WebUI 后端结构:新增独立的 `core/webui.py` 服务,统一负责管理页路由注册、Dashboard 数据聚合、请求校验和保存、删除、恢复接口;`main.py` 仅保留服务构造与注册调用,现有 Web API 路径和前端调用协议不变。
|
||||
|
||||
插件 WebUI 的区服别名和心法别名页新增“恢复默认”操作;采用页面内可见的二次点击确认,不依赖嵌入页面可能无法显示的浏览器原生确认框。确认后分别读取随插件分发的 `data/server_aliases.json` 和 `data/kungfu.json`,在事务中完整重写对应数据库表并刷新页面数据。
|
||||
|
||||
移除 WebUI 顶部统计区域;“会话控制”移动为第一个默认页签,并新增醒目的当前生效模式状态。模式卡片明确区分当前模式与尚未保存的选择,切换按钮同步显示目标模式。“会话绑定”更名为“区服绑定”并保留在“事件推送”之后。
|
||||
|
||||
重构 WebUI 区服绑定交互:顶部入口改为“添加绑定”,会话 ID 可自定义但绑定区服只能从标准区服下拉框选择;列表编辑时会话 ID 保持只读,仅在对应绑定区服列内切换下拉框进行保存,解除绑定改为点击后直接删除对应记录。Web API 同步拒绝别名及任意文本作为绑定区服。
|
||||
|
||||
### version: 3.4.2(2026-09-04):
|
||||
|
||||
图片模板整体调整为浅色背景、深色高对比文本和更大的字号,缩小内容与金色边框之间的留白并扩大主体区域;同步检查并调整多个独立页面的字号、间距及表格布局,提高长图阅读体验。
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 中文触发词覆盖活动、名剑、排行、交易、阵营、角色、奇遇、副本、家园、社区等场景。
|
||||
- 中文触发词覆盖活动、名剑、排行、交易、阵营、角色、奇遇、百战、家园、社区等场景。
|
||||
- 支持纯文本、远程图片、HTML/Jinja2 渲染图片和图文消息链。
|
||||
- 支持 `宏`、`资历` 以及帮会、阵营、其他排行榜的 10 秒两轮交互,超时默认选择第一项。
|
||||
- 查询图片统一使用浅色高对比主题,并可在插件配置页调整渲染清晰度、输出格式和 JPEG 质量。
|
||||
@@ -231,7 +231,7 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通
|
||||
|
||||
| 指令 | 说明与输出 | 凭据 |
|
||||
| --- | --- | --- |
|
||||
| `聊天 服务器 角色 [条数] [页数]` | 角色聊天记录,默认 20 条、第 1 页;图片 | Token |
|
||||
| `发言 服务器 角色 [条数] [页数]` | 角色发言记录,默认 20 条、第 1 页;图片 | Token |
|
||||
| `统战 [服务器]` | 统战频道统计;文本 | 无 |
|
||||
| `小药 [心法]` | 小吃小药推荐;图片 | 无 |
|
||||
| `骗子 UID [服务器]` | 查询欺诈记录;文本 | Token |
|
||||
@@ -274,7 +274,7 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通
|
||||
| `避雷修改 ID 名称 备注` | 按 ID 更新记录,同时覆盖修改时间和修改人 |
|
||||
| `避雷删除 ID` | 按 ID 删除记录 |
|
||||
|
||||
避雷功能没有内置权限校验。只要能够触发插件指令,当前实现就允许新增、修改和删除记录;公开群聊部署时应在 AstrBot 或消息平台层配置访问控制。
|
||||
避雷功能没有会话内的用户角色校验。在“会话控制”放行的会话中,只要能够触发插件指令,当前实现就允许新增、修改和删除记录;如需限制到指定管理员,仍应在 AstrBot 或消息平台层配置权限。
|
||||
|
||||
### 实时事件推送
|
||||
|
||||
@@ -291,7 +291,9 @@ JX3API Token 可从 [JX3API](https://www.jx3api.com/) 获取。推栏 Ticket 通
|
||||
|
||||
### WebUI 插件管理
|
||||
|
||||
AstrBot 插件详情页中的“剑网三插件管理”页面提供四个页签:会话绑定、事件订阅、区服别名和心法别名。页面可以新增、修改或解除任意会话绑定,查看所有会话的事件总开关及订阅编号。区服别名页展示完整标准区服目录,标准名称只读,只能行内编辑别名;心法别名页隐藏 JX3BOX 配装 ID,标准心法名称只读,同样只允许行内编辑最多 5 个别名。管理页通过 AstrBot Plugin Pages 桥接调用插件 Web API,不直接访问 Dashboard 凭据。
|
||||
AstrBot 插件详情页中的“剑网三插件管理”页面提供五个页签:会话控制、事件推送、区服绑定、区服别名和心法别名,不再显示顶部统计区域。会话控制为默认页签,页面会明确标出当前真正生效的模式,并区分尚未保存的模式选择。该功能默认使用“全部会话”,也可切换为白名单或黑名单;会话 ID 可从已有绑定和订阅记录中选择,也可直接输入,并可维护名单类型和备注。空白名单不放行任何会话,空黑名单放行全部会话,名单策略同时作用于插件指令和实时事件推送。
|
||||
|
||||
区服绑定页可使用自定义会话 ID 添加记录,绑定区服只能从标准区服下拉框选择。编辑已有记录时会话 ID 保持只读,只在绑定区服列内修改,点击解除绑定会直接删除该行。事件推送页显示所有会话的总开关及订阅编号。区服别名页展示完整标准区服目录,标准名称只读,只能行内编辑别名;心法别名页隐藏 JX3BOX 配装 ID,标准心法名称只读,同样只允许行内编辑最多 5 个别名。两个别名页均可使用随插件分发的 JSON 种子完整覆盖数据库并恢复默认配置。管理页通过 AstrBot Plugin Pages 桥接调用插件 Web API,不直接访问 Dashboard 凭据。
|
||||
|
||||
## 业务流程
|
||||
|
||||
@@ -299,7 +301,9 @@ AstrBot 插件详情页中的“剑网三插件管理”页面提供四个页签
|
||||
flowchart LR
|
||||
A["AstrBot 消息事件"] --> B["前缀检查与空白切分"]
|
||||
B --> C["command_map 查找处理器"]
|
||||
C --> D["MessageBuilder 参数适配"]
|
||||
C --> K{"会话控制是否放行"}
|
||||
K -->|是| D["MessageBuilder 参数适配"]
|
||||
K -->|否| X["停止处理"]
|
||||
D --> E1["JX3APIService"]
|
||||
D --> E2["JX3BOXService"]
|
||||
D --> E3["BiLeidata / EventPushService / KungfuAliasService"]
|
||||
@@ -322,13 +326,15 @@ flowchart LR
|
||||
1. 读取前缀、Token、Ticket 和 WebSocket 配置。
|
||||
2. 计算插件目录、AstrBot 数据目录、本地 SQLite 和模板目录路径。
|
||||
3. 把 `templates/img`、`templates/img/sand`、`templates/sect`、`templates/serendipity` 中的图片读取为 base64 Data URL。
|
||||
4. 创建本地数据库、两个数据服务、心法别名服务、避雷服务、事件推送服务和消息构建器。
|
||||
4. 创建本地数据库、两个数据服务、心法别名服务、会话控制服务、避雷服务、事件推送服务、WebUI 服务和消息构建器。
|
||||
|
||||
异步初始化阶段会连接数据库并创建以下本地表:
|
||||
|
||||
- `bilei`:避雷记录。
|
||||
- `event_push_subscriptions`:按 AstrBot 会话保存事件总开关和每个事件编号的订阅开关。
|
||||
- `session_server_bindings`:两列结构,保存会话 ID 与绑定区服。
|
||||
- `session_control_settings`:保存当前会话控制模式,首次初始化默认为 `all`。
|
||||
- `session_control_entries`:保存白名单或黑名单会话 ID 及备注。
|
||||
- `server_aliases`:保存标准区服名及其 JSON 别名列表。
|
||||
- `kungfu`:保存 JX3BOX 配装 ID、标准心法名及最多 5 个别名。
|
||||
- `trade_item_cache`:JX3BOX 交易行基础物品数据缓存及更新时间。
|
||||
@@ -431,7 +437,7 @@ AstrBot 的渲染接口接收完整 HTML 字符串,因此插件不会依赖渲
|
||||
|
||||
| 文件 | 生命周期 | 内容 |
|
||||
| --- | --- | --- |
|
||||
| AstrBot 插件数据目录下的 `local_data.db` | 运行时创建和维护 | 心法与别名、避雷记录、会话区服绑定、区服别名、事件订阅及基础数据缓存 |
|
||||
| AstrBot 插件数据目录下的 `local_data.db` | 运行时创建和维护 | 心法与别名、避雷记录、会话控制、会话区服绑定、区服别名、事件订阅及基础数据缓存 |
|
||||
| `data/kungfu.json` | 随插件分发,只在初始化时读取 | 32 条默认心法、别名和 JX3BOX 配装 ID |
|
||||
| `data/server_aliases.json` | 随插件分发,只在初始化时读取 | 默认标准区服及其别名 |
|
||||
|
||||
@@ -441,7 +447,7 @@ AstrBot 的渲染接口接收完整 HTML 字符串,因此插件不会依赖渲
|
||||
|
||||
`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`。
|
||||
每条消息按 `action` 识别事件,正文优先读取 `detail`,同时兼容 `data`。分发时先应用会话控制策略,再联查会话区服绑定:正文包含 `server` 时仅保留同区服或未绑定区服的接收会话,正文不含 `server` 时不做区服过滤。之后只有会话的 `enabled=1` 且对应 `action_编号=1` 时才发送。会话 ID 直接取当前消息的 `unified_msg_origin`。
|
||||
|
||||
表中使用 `action_1001` 这类列名保存各事件开关,并在新增事件编号时自动为旧数据库补列。升级时旧的 `tuishong` 轮询状态表会被删除。
|
||||
|
||||
@@ -465,6 +471,8 @@ astrbot_plugin_jx3/
|
||||
│ ├── message.py # 文本、图片、消息链和两轮会话构建
|
||||
│ ├── request.py # aiohttp 请求封装
|
||||
│ ├── event_push.py # WebSocket 事件通道与会话订阅
|
||||
│ ├── session_control.py # 全部会话、白名单与黑名单访问策略
|
||||
│ ├── webui.py # 插件管理页路由、数据聚合与请求处理
|
||||
│ ├── server_binding.py # 会话区服绑定、别名与区服目录
|
||||
│ ├── kungfu_alias.py # 本地心法目录初始化、校验与 WebUI 写入
|
||||
│ ├── bilei_data.py # 避雷数据增删改查
|
||||
@@ -512,16 +520,17 @@ git diff --check
|
||||
|
||||
## 当前版本状态
|
||||
|
||||
以下内容是对 v3.4.2 当前源码的静态核对结果,部署和二次开发前应注意:
|
||||
以下内容是对 v3.4.3 当前源码的静态核对结果,部署和二次开发前应注意:
|
||||
|
||||
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 日志。不要公开调试日志,建议二次开发时移除敏感值输出。
|
||||
3. 区服绑定 WebUI 的新增及行内编辑均只接受标准区服,聊天指令仍可使用区服别名;区服别名页展示完整标准区服目录并只允许行内编辑别名,心法别名页隐藏配装 ID,后端保存接口也只更新已有心法的别名。“恢复默认”使用页面内二次点击确认,确认后会用随包 JSON 完整覆盖对应数据库表,当前自定义别名无法从页面撤销恢复。
|
||||
4. 会话控制默认放行全部会话;切换白名单后只有白名单记录可执行插件指令及接收推送,切换黑名单后仅拦截黑名单记录。名单记录会保留在数据库中,切换模式不会删除已有配置。
|
||||
5. aiocqhttp 图片发送返回 `retcode=1200` 时只记录警告,不再追加“猪脑过载”提示;该回执代表发送结果不确定,仍建议结合平台日志确认实际送达情况。
|
||||
6. 带区服参数的指令在入口层统一进行会话绑定补齐和别名解析;新增此类指令时参数名应继续使用 `server`。
|
||||
7. 资历分布使用固定的“数字序号 → 大类名称”首轮菜单,通用会话裁剪出单项字典后交给 JX3API `/tuilan/achievement` 次轮查询;模板兼容总览与指定 `subclass` 的不同 `data.total` 层级,并直接使用接口角色字段、`pieces`、`seniority` 和 `score/totalScore` 渲染,JX3BOX 不再参与资历查询。
|
||||
8. `APIClient` 当前默认 `ssl_verify=False`,即外部 HTTPS 请求不校验证书。对传输安全有要求的部署应先评估并调整该设置。
|
||||
9. JX3API 服务初始化时会把 Token 和 Ticket 写入 debug 日志。不要公开调试日志,建议二次开发时移除敏感值输出。
|
||||
|
||||
## 注意事项
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from astrbot.api.star import Context
|
||||
|
||||
from .sqlite import AsyncSQLiteDB
|
||||
from .server_binding import ServerBindingService
|
||||
from .session_control import SessionControlService
|
||||
|
||||
|
||||
DEFAULT_WSS_URL = "wss://socket.nicemoe.cn"
|
||||
@@ -294,11 +295,13 @@ class EventPushService:
|
||||
config: AstrBotConfig,
|
||||
sqlite: AsyncSQLiteDB,
|
||||
server_binding: ServerBindingService,
|
||||
session_control: SessionControlService,
|
||||
):
|
||||
self.context = context
|
||||
self.config = config
|
||||
self.sql = sqlite
|
||||
self.server_binding = server_binding
|
||||
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._runner: Optional[asyncio.Task] = None
|
||||
@@ -520,6 +523,7 @@ class EventPushService:
|
||||
return [
|
||||
(str(row["session_id"]), str(row.get("server") or "").strip())
|
||||
for row in rows
|
||||
if self.session_control.is_allowed(row["session_id"])
|
||||
]
|
||||
|
||||
async def list_subscription_statuses(self) -> list[dict[str, Any]]:
|
||||
|
||||
+34
-3
@@ -31,6 +31,18 @@ class KungfuAliasService:
|
||||
await self._seed_defaults()
|
||||
|
||||
async def _seed_defaults(self):
|
||||
records = self._load_seed_defaults()
|
||||
for values in records:
|
||||
await self.sql.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO kungfu
|
||||
(pzid, name, name1, name2, name3, name4, name5)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
values,
|
||||
)
|
||||
|
||||
def _load_seed_defaults(self) -> list[tuple[Any, ...]]:
|
||||
try:
|
||||
records = json.loads(self.seed_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
@@ -39,21 +51,40 @@ class KungfuAliasService:
|
||||
if not isinstance(records, list):
|
||||
raise RuntimeError("心法种子数据必须是数组")
|
||||
|
||||
normalized_records: list[tuple[Any, ...]] = []
|
||||
seen_pzids: set[int] = set()
|
||||
for record in records:
|
||||
if not isinstance(record, dict):
|
||||
continue
|
||||
pzid = self._parse_pzid(record.get("pzid"))
|
||||
if pzid in seen_pzids:
|
||||
continue
|
||||
seen_pzids.add(pzid)
|
||||
name = self._clean(record.get("name"))
|
||||
aliases = self._normalize_aliases(name, record.get("aliases") or [])
|
||||
values = [*aliases, *([None] * (self.MAX_ALIASES - len(aliases)))]
|
||||
await self.sql.execute(
|
||||
normalized_records.append((pzid, name, *values))
|
||||
return normalized_records
|
||||
|
||||
async def restore_defaults(self) -> int:
|
||||
"""使用随插件分发的 JSON 种子完整重写心法表。"""
|
||||
records = self._load_seed_defaults()
|
||||
statements: list[tuple[str, tuple[Any, ...]]] = [
|
||||
("DELETE FROM kungfu", ()),
|
||||
]
|
||||
statements.extend(
|
||||
(
|
||||
"""
|
||||
INSERT OR IGNORE INTO kungfu
|
||||
INSERT INTO kungfu
|
||||
(pzid, name, name1, name2, name3, name4, name5)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(pzid, name, *values),
|
||||
values,
|
||||
)
|
||||
for values in records
|
||||
)
|
||||
await self.sql.execute_transaction(statements)
|
||||
return len(records)
|
||||
|
||||
async def list_kungfu(self) -> list[dict[str, Any]]:
|
||||
rows = await self.sql.fetch_all(
|
||||
|
||||
+1
-1
@@ -486,7 +486,7 @@ class MessageBuilder:
|
||||
return await self.T2I_image_msg(event, lambda: self.jx3api.qixue(name,0))
|
||||
|
||||
async def liaotian(self, event: AstrMessageEvent, server:str, name: str, limit:int = 20, page:int = 1):
|
||||
""" 聊天 服务器 角色 条数 页数"""
|
||||
""" 发言 服务器 角色 条数 页数"""
|
||||
return await self.T2I_image_msg(event, lambda: self.jx3api.juesheliaotian(server,name,limit,page))
|
||||
|
||||
async def tongzhanyy(self, event: AstrMessageEvent, server: str = ""):
|
||||
|
||||
+55
-9
@@ -14,7 +14,9 @@ class ServerBindingService:
|
||||
self.sql = sqlite
|
||||
self.seed_path = seed_path
|
||||
self._remote_servers: set[str] = set()
|
||||
self._standard_servers: set[str] = set()
|
||||
self._known_servers: set[str] = set()
|
||||
self._standard_lookup: dict[str, str] = {}
|
||||
self._server_lookup: dict[str, str] = {}
|
||||
|
||||
async def initialize(self):
|
||||
@@ -38,6 +40,17 @@ class ServerBindingService:
|
||||
await self._reload_cache()
|
||||
|
||||
async def _seed_aliases(self):
|
||||
records = self._load_seed_aliases()
|
||||
for server, aliases_json in records:
|
||||
await self.sql.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO server_aliases (server, aliases)
|
||||
VALUES (?, ?)
|
||||
""",
|
||||
(server, aliases_json),
|
||||
)
|
||||
|
||||
def _load_seed_aliases(self) -> list[tuple[str, str]]:
|
||||
try:
|
||||
records = json.loads(self.seed_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
@@ -46,13 +59,21 @@ class ServerBindingService:
|
||||
if not isinstance(records, list):
|
||||
raise RuntimeError("区服别名种子数据必须是数组")
|
||||
|
||||
normalized_records: list[tuple[str, str]] = []
|
||||
seen_servers: set[str] = set()
|
||||
for record in records:
|
||||
if not isinstance(record, dict):
|
||||
continue
|
||||
server = self._clean(record.get("server"))
|
||||
raw_aliases = record.get("aliases") or []
|
||||
if not server or not isinstance(raw_aliases, list):
|
||||
server_key = self._key(server)
|
||||
if (
|
||||
not server
|
||||
or not isinstance(raw_aliases, list)
|
||||
or server_key in seen_servers
|
||||
):
|
||||
continue
|
||||
seen_servers.add(server_key)
|
||||
aliases = []
|
||||
seen: set[str] = set()
|
||||
for value in raw_aliases:
|
||||
@@ -61,20 +82,35 @@ class ServerBindingService:
|
||||
if alias and key != self._key(server) and key not in seen:
|
||||
seen.add(key)
|
||||
aliases.append(alias)
|
||||
await self.sql.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO server_aliases (server, aliases)
|
||||
VALUES (?, ?)
|
||||
""",
|
||||
(server, json.dumps(aliases, ensure_ascii=False)),
|
||||
normalized_records.append(
|
||||
(server, json.dumps(aliases, ensure_ascii=False))
|
||||
)
|
||||
return normalized_records
|
||||
|
||||
async def restore_default_aliases(self) -> int:
|
||||
"""使用随插件分发的 JSON 种子完整重写区服别名表。"""
|
||||
records = self._load_seed_aliases()
|
||||
statements: list[tuple[str, tuple[Any, ...]]] = [
|
||||
("DELETE FROM server_aliases", ()),
|
||||
]
|
||||
statements.extend(
|
||||
(
|
||||
"INSERT INTO server_aliases (server, aliases) VALUES (?, ?)",
|
||||
(server, aliases_json),
|
||||
)
|
||||
for server, aliases_json in records
|
||||
)
|
||||
await self.sql.execute_transaction(statements)
|
||||
await self._reload_cache()
|
||||
return len(records)
|
||||
|
||||
async def _reload_cache(self):
|
||||
bindings = await self.list_bindings()
|
||||
alias_rows = await self.list_aliases()
|
||||
known = set(self._remote_servers)
|
||||
standard = set(self._remote_servers)
|
||||
standard.update(row["server"] for row in alias_rows)
|
||||
known = set(standard)
|
||||
known.update(row["server"] for row in bindings)
|
||||
known.update(row["server"] for row in alias_rows)
|
||||
|
||||
lookup = {self._key(server): server for server in known}
|
||||
for row in alias_rows:
|
||||
@@ -84,7 +120,11 @@ class ServerBindingService:
|
||||
# 新增的官方区服名优先于历史别名,避免目录更新后误解析。
|
||||
lookup.setdefault(self._key(alias), server)
|
||||
|
||||
self._standard_servers = standard
|
||||
self._known_servers = known
|
||||
self._standard_lookup = {
|
||||
self._key(server): server for server in standard
|
||||
}
|
||||
self._server_lookup = lookup
|
||||
|
||||
async def update_server_catalog(self, servers: Iterable[str]):
|
||||
@@ -98,6 +138,12 @@ class ServerBindingService:
|
||||
def known_servers(self) -> list[str]:
|
||||
return sorted(self._known_servers)
|
||||
|
||||
def standard_servers(self) -> list[str]:
|
||||
return sorted(self._standard_servers)
|
||||
|
||||
def resolve_standard_server(self, value: Any) -> str:
|
||||
return self._standard_lookup.get(self._key(value), "")
|
||||
|
||||
def is_known_server(self, value: Any) -> bool:
|
||||
return self._key(value) in self._server_lookup
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
from typing import Any
|
||||
|
||||
from .sqlite import AsyncSQLiteDB
|
||||
|
||||
|
||||
class SessionControlService:
|
||||
"""使用 SQLite 保存并缓存插件的会话访问策略。"""
|
||||
|
||||
MODE_ALL = "all"
|
||||
MODE_WHITELIST = "whitelist"
|
||||
MODE_BLACKLIST = "blacklist"
|
||||
MODES = {MODE_ALL, MODE_WHITELIST, MODE_BLACKLIST}
|
||||
|
||||
def __init__(self, sqlite: AsyncSQLiteDB):
|
||||
self.sql = sqlite
|
||||
self._mode = self.MODE_ALL
|
||||
self._entries: dict[str, dict[str, str]] = {}
|
||||
|
||||
async def initialize(self):
|
||||
await self.sql.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS session_control_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
await self.sql.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS session_control_entries (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
list_type TEXT NOT NULL CHECK(list_type IN ('whitelist', 'blacklist')),
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
await self.sql.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO session_control_settings (key, value)
|
||||
VALUES ('mode', 'all')
|
||||
"""
|
||||
)
|
||||
await self._reload()
|
||||
|
||||
async def _reload(self):
|
||||
setting = await self.sql.fetch_one(
|
||||
"SELECT value FROM session_control_settings WHERE key='mode'"
|
||||
)
|
||||
mode = str(setting.get("value") or "") if setting else ""
|
||||
self._mode = mode if mode in self.MODES else self.MODE_ALL
|
||||
|
||||
rows = await self.sql.fetch_all(
|
||||
"""
|
||||
SELECT session_id, list_type, remark
|
||||
FROM session_control_entries
|
||||
ORDER BY session_id
|
||||
"""
|
||||
)
|
||||
self._entries = {
|
||||
str(row["session_id"]): {
|
||||
"session_id": str(row["session_id"]),
|
||||
"list_type": str(row["list_type"]),
|
||||
"remark": str(row.get("remark") or ""),
|
||||
}
|
||||
for row in rows
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_session_id(session_id: Any) -> str:
|
||||
value = str(session_id or "").strip()
|
||||
if not value:
|
||||
raise ValueError("会话 ID 不能为空")
|
||||
if len(value) > 512:
|
||||
raise ValueError("会话 ID 不能超过 512 个字符")
|
||||
return value
|
||||
|
||||
def is_allowed(self, session_id: Any) -> bool:
|
||||
"""同步检查缓存策略,供高频消息和推送分发入口调用。"""
|
||||
value = str(session_id or "").strip()
|
||||
if self._mode == self.MODE_ALL:
|
||||
return True
|
||||
|
||||
entry = self._entries.get(value)
|
||||
if self._mode == self.MODE_WHITELIST:
|
||||
return bool(entry and entry["list_type"] == self.MODE_WHITELIST)
|
||||
return not (entry and entry["list_type"] == self.MODE_BLACKLIST)
|
||||
|
||||
async def set_mode(self, mode: Any):
|
||||
value = str(mode or "").strip().lower()
|
||||
if value not in self.MODES:
|
||||
raise ValueError("会话控制模式无效")
|
||||
await self.sql.execute(
|
||||
"""
|
||||
INSERT INTO session_control_settings (key, value, updated_at)
|
||||
VALUES ('mode', ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value=excluded.value,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
""",
|
||||
(value,),
|
||||
)
|
||||
self._mode = value
|
||||
|
||||
async def save_entry(
|
||||
self,
|
||||
session_id: Any,
|
||||
list_type: Any,
|
||||
remark: Any = "",
|
||||
):
|
||||
normalized_session_id = self._normalize_session_id(session_id)
|
||||
normalized_type = str(list_type or "").strip().lower()
|
||||
if normalized_type not in {self.MODE_WHITELIST, self.MODE_BLACKLIST}:
|
||||
raise ValueError("名单类型必须是白名单或黑名单")
|
||||
normalized_remark = str(remark or "").strip()
|
||||
if len(normalized_remark) > 200:
|
||||
raise ValueError("备注不能超过 200 个字符")
|
||||
|
||||
await self.sql.execute(
|
||||
"""
|
||||
INSERT INTO session_control_entries (
|
||||
session_id, list_type, remark, updated_at
|
||||
) VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(session_id) DO UPDATE SET
|
||||
list_type=excluded.list_type,
|
||||
remark=excluded.remark,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
""",
|
||||
(normalized_session_id, normalized_type, normalized_remark),
|
||||
)
|
||||
self._entries[normalized_session_id] = {
|
||||
"session_id": normalized_session_id,
|
||||
"list_type": normalized_type,
|
||||
"remark": normalized_remark,
|
||||
}
|
||||
|
||||
async def delete_entry(self, session_id: Any):
|
||||
normalized_session_id = self._normalize_session_id(session_id)
|
||||
await self.sql.delete(
|
||||
"session_control_entries",
|
||||
"session_id=?",
|
||||
(normalized_session_id,),
|
||||
)
|
||||
self._entries.pop(normalized_session_id, None)
|
||||
|
||||
async def get_state(self) -> dict[str, Any]:
|
||||
return {
|
||||
"mode": self._mode,
|
||||
"entries": sorted(
|
||||
(dict(entry) for entry in self._entries.values()),
|
||||
key=lambda entry: entry["session_id"],
|
||||
),
|
||||
}
|
||||
@@ -37,6 +37,20 @@ class AsyncSQLiteDB:
|
||||
async with self.conn.execute(sql, params):
|
||||
await self.conn.commit()
|
||||
|
||||
async def execute_transaction(
|
||||
self,
|
||||
statements: List[Tuple[str, Tuple[Any, ...]]],
|
||||
):
|
||||
"""在同一事务内顺序执行多条参数化 SQL。"""
|
||||
await self.conn.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
for sql, params in statements:
|
||||
await self.conn.execute(sql, params)
|
||||
await self.conn.commit()
|
||||
except Exception:
|
||||
await self.conn.rollback()
|
||||
raise
|
||||
|
||||
async def fetch_one(self, sql: str, params: Tuple = ()) -> Optional[Dict[str, Any]]:
|
||||
async with self.conn.execute(sql, params) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from astrbot.api.star import Context
|
||||
from astrbot.api.web import error_response, json_response, request
|
||||
|
||||
from .event_push import EVENT_NAMES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .event_push import EventPushService
|
||||
from .jx3api_data import JX3APIService
|
||||
from .kungfu_alias import KungfuAliasService
|
||||
from .server_binding import ServerBindingService
|
||||
from .session_control import SessionControlService
|
||||
|
||||
|
||||
class WebUIService:
|
||||
"""注册插件管理页接口,并处理 WebUI 的数据读写。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
jx3api: JX3APIService,
|
||||
event_push: EventPushService,
|
||||
server_binding: ServerBindingService,
|
||||
kungfu_alias: KungfuAliasService,
|
||||
session_control: SessionControlService,
|
||||
):
|
||||
self.jx3api = jx3api
|
||||
self.event_push = event_push
|
||||
self.server_binding = server_binding
|
||||
self.kungfu_alias = kungfu_alias
|
||||
self.session_control = session_control
|
||||
|
||||
def register(self, context: Context, plugin_name: str):
|
||||
routes = (
|
||||
("dashboard", self.dashboard, ["GET"], "读取会话管理数据"),
|
||||
("bindings/save", self.save_binding, ["POST"], "保存会话区服绑定"),
|
||||
("bindings/delete", self.delete_binding, ["POST"], "删除会话区服绑定"),
|
||||
("aliases/save", self.save_aliases, ["POST"], "保存区服别名"),
|
||||
("aliases/delete", self.delete_aliases, ["POST"], "删除区服别名"),
|
||||
("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,
|
||||
["POST"],
|
||||
"保存会话控制模式",
|
||||
),
|
||||
(
|
||||
"session-control/save",
|
||||
self.save_session_control_entry,
|
||||
["POST"],
|
||||
"保存会话控制名单",
|
||||
),
|
||||
(
|
||||
"session-control/delete",
|
||||
self.delete_session_control_entry,
|
||||
["POST"],
|
||||
"删除会话控制名单",
|
||||
),
|
||||
)
|
||||
for path, handler, methods, description in routes:
|
||||
context.register_web_api(
|
||||
f"/{plugin_name}/{path}",
|
||||
handler,
|
||||
methods,
|
||||
description,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _json_payload() -> dict[str, Any]:
|
||||
payload = await request.json(default={})
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("请求正文必须是 JSON 对象")
|
||||
return payload
|
||||
|
||||
@staticmethod
|
||||
def _parse_aliases(raw_aliases: Any) -> list[str]:
|
||||
if isinstance(raw_aliases, str):
|
||||
return re.split(r"[,,;;\n]+", raw_aliases)
|
||||
if isinstance(raw_aliases, list):
|
||||
return [str(value) for value in raw_aliases]
|
||||
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()
|
||||
return json_response(
|
||||
{
|
||||
"bindings": bindings,
|
||||
"subscriptions": subscriptions,
|
||||
"aliases": aliases,
|
||||
"kungfu": kungfu,
|
||||
"servers": self.server_binding.standard_servers(),
|
||||
"events": {
|
||||
str(action): name for action, name in EVENT_NAMES.items()
|
||||
},
|
||||
"session_control": session_control,
|
||||
}
|
||||
)
|
||||
|
||||
async def save_binding(self):
|
||||
try:
|
||||
payload = await self._json_payload()
|
||||
server = self.server_binding.resolve_standard_server(
|
||||
payload.get("server")
|
||||
)
|
||||
if not server:
|
||||
raise ValueError("绑定区服必须选择标准区服")
|
||||
await self.server_binding.set_binding(
|
||||
str(payload.get("session_id") or ""),
|
||||
server,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"saved": True})
|
||||
|
||||
async def delete_binding(self):
|
||||
try:
|
||||
payload = await self._json_payload()
|
||||
session_id = str(payload.get("session_id") or "")
|
||||
if not session_id.strip():
|
||||
raise ValueError("会话 ID 不能为空")
|
||||
await self.server_binding.delete_binding(session_id)
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"deleted": True})
|
||||
|
||||
async def save_aliases(self):
|
||||
try:
|
||||
payload = await self._json_payload()
|
||||
await self.server_binding.set_aliases(
|
||||
str(payload.get("server") or ""),
|
||||
self._parse_aliases(payload.get("aliases", [])),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"saved": True})
|
||||
|
||||
async def delete_aliases(self):
|
||||
try:
|
||||
payload = await self._json_payload()
|
||||
server = str(payload.get("server") or "")
|
||||
if not server.strip():
|
||||
raise ValueError("标准区服名不能为空")
|
||||
await self.server_binding.delete_aliases(server)
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"deleted": True})
|
||||
|
||||
async def restore_aliases(self):
|
||||
try:
|
||||
restored = await self.server_binding.restore_default_aliases()
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
return error_response(str(exc), status_code=500)
|
||||
return json_response({"restored": restored})
|
||||
|
||||
async def save_kungfu(self):
|
||||
try:
|
||||
payload = await self._json_payload()
|
||||
await self.kungfu_alias.save_aliases(
|
||||
payload.get("pzid"),
|
||||
self._parse_aliases(payload.get("aliases", [])),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"saved": True})
|
||||
|
||||
async def restore_kungfu(self):
|
||||
try:
|
||||
restored = await self.kungfu_alias.restore_defaults()
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
return error_response(str(exc), status_code=500)
|
||||
return json_response({"restored": restored})
|
||||
|
||||
async def refresh_servers(self):
|
||||
servers = await self.jx3api.server_list()
|
||||
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()
|
||||
await self.session_control.set_mode(payload.get("mode"))
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"saved": True})
|
||||
|
||||
async def save_session_control_entry(self):
|
||||
try:
|
||||
payload = await self._json_payload()
|
||||
await self.session_control.save_entry(
|
||||
payload.get("session_id"),
|
||||
payload.get("list_type"),
|
||||
payload.get("remark"),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"saved": True})
|
||||
|
||||
async def delete_session_control_entry(self):
|
||||
try:
|
||||
payload = await self._json_payload()
|
||||
await self.session_control.delete_entry(payload.get("session_id"))
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"deleted": True})
|
||||
@@ -1,5 +1,4 @@
|
||||
import inspect
|
||||
import re
|
||||
from pathlib import Path
|
||||
from sys import maxsize
|
||||
from typing import cast
|
||||
@@ -8,15 +7,15 @@ from astrbot.api.event import filter, AstrMessageEvent
|
||||
from astrbot.api.star import Context, Star, register, StarTools
|
||||
from astrbot.api import logger
|
||||
from astrbot.api import AstrBotConfig
|
||||
from astrbot.api.web import error_response, json_response, request
|
||||
|
||||
from .core.sqlite import AsyncSQLiteDB
|
||||
from .core.jx3api_data import JX3APIService
|
||||
from .core.jx3box_data import JX3BOXService
|
||||
from .core.event_push import EVENT_NAMES, EventPushService
|
||||
from .core.event_push import EventPushService
|
||||
from .core.bilei_data import BiLeidata
|
||||
from .core.kungfu_alias import KungfuAliasService
|
||||
from .core.server_binding import ServerBindingService
|
||||
from .core.session_control import SessionControlService
|
||||
from .core.webui import WebUIService
|
||||
from .core.message import MessageBuilder
|
||||
from .core.fun_basic import load_as_base64
|
||||
|
||||
@@ -26,7 +25,7 @@ PLUGIN_NAME = "astrbot_plugin_jx3"
|
||||
@register("astrbot_plugin_jx3",
|
||||
"fxdyz",
|
||||
"聚合剑网三游戏数据,提供查询、图片渲染、本地避雷和实时事件推送。",
|
||||
"3.4.2",
|
||||
"3.4.3",
|
||||
"https://github.com/qsc20001102/astrbot_plugin_jx3"
|
||||
)
|
||||
class Jx3ApiPlugin(Star):
|
||||
@@ -49,7 +48,7 @@ class Jx3ApiPlugin(Star):
|
||||
# 构造所有类
|
||||
self.create_all()
|
||||
# 注册插件管理页接口
|
||||
self._register_web_apis(context)
|
||||
self.webui.register(context, PLUGIN_NAME)
|
||||
|
||||
|
||||
# 声明指令集
|
||||
@@ -66,6 +65,7 @@ class Jx3ApiPlugin(Star):
|
||||
await self.init_trade_item_cache_data()
|
||||
await self.kungfu_alias.initialize()
|
||||
await self.server_binding.initialize()
|
||||
await self.session_control.initialize()
|
||||
|
||||
# 获取区服目录,用于识别完整参数与区服别名。
|
||||
await self.server_binding.update_server_catalog(
|
||||
@@ -165,11 +165,20 @@ class Jx3ApiPlugin(Star):
|
||||
self.local_sql_db,
|
||||
self.server_alias_seed_path,
|
||||
)
|
||||
self.session_control = SessionControlService(self.local_sql_db)
|
||||
self.event_push = EventPushService(
|
||||
cast(Context, self.context),
|
||||
self.conf,
|
||||
self.local_sql_db,
|
||||
self.server_binding,
|
||||
self.session_control,
|
||||
)
|
||||
self.webui = WebUIService(
|
||||
self.jx3api,
|
||||
self.event_push,
|
||||
self.server_binding,
|
||||
self.kungfu_alias,
|
||||
self.session_control,
|
||||
)
|
||||
self.jx3cmd = MessageBuilder(
|
||||
self.jx3api,
|
||||
@@ -278,7 +287,7 @@ class Jx3ApiPlugin(Star):
|
||||
"资历排行": self. jx3cmd.zilipaixing,
|
||||
"技能": self. jx3cmd.jineng,
|
||||
"奇穴": self. jx3cmd.qixue,
|
||||
"聊天": self. jx3cmd.liaotian,
|
||||
"发言": self. jx3cmd.liaotian,
|
||||
"统战": self. jx3cmd.tongzhanyy,
|
||||
"小药": self. jx3cmd.xiaoyao,
|
||||
"骗子": self. jx3cmd.pianzhi,
|
||||
@@ -493,6 +502,13 @@ class Jx3ApiPlugin(Star):
|
||||
event.stop_event()
|
||||
event.should_call_llm(True)
|
||||
|
||||
if not self.session_control.is_allowed(event.unified_msg_origin):
|
||||
logger.info(
|
||||
f"会话控制已拦截插件指令:command={cmd}, "
|
||||
f"session={event.unified_msg_origin}"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
args = await self._prepare_server_args(handler, event, args)
|
||||
ret = await self._call_with_auto_args(handler, event, args)
|
||||
@@ -526,117 +542,3 @@ class Jx3ApiPlugin(Star):
|
||||
"""解除当前会话的区服绑定。"""
|
||||
await self.server_binding.delete_binding(event.unified_msg_origin)
|
||||
await event.send(event.plain_result("当前会话已解除区服绑定。"))
|
||||
|
||||
def _register_web_apis(self, context: Context):
|
||||
routes = (
|
||||
("dashboard", self.page_dashboard, ["GET"], "读取会话管理数据"),
|
||||
("bindings/save", self.page_save_binding, ["POST"], "保存会话区服绑定"),
|
||||
("bindings/delete", self.page_delete_binding, ["POST"], "删除会话区服绑定"),
|
||||
("aliases/save", self.page_save_aliases, ["POST"], "保存区服别名"),
|
||||
("aliases/delete", self.page_delete_aliases, ["POST"], "删除区服别名"),
|
||||
("kungfu/save", self.page_save_kungfu, ["POST"], "保存心法别名"),
|
||||
("servers/refresh", self.page_refresh_servers, ["POST"], "刷新区服目录"),
|
||||
)
|
||||
for path, handler, methods, description in routes:
|
||||
context.register_web_api(
|
||||
f"/{PLUGIN_NAME}/{path}",
|
||||
handler,
|
||||
methods,
|
||||
description,
|
||||
)
|
||||
|
||||
async def page_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()
|
||||
return json_response(
|
||||
{
|
||||
"bindings": bindings,
|
||||
"subscriptions": subscriptions,
|
||||
"aliases": aliases,
|
||||
"kungfu": kungfu,
|
||||
"servers": self.server_binding.known_servers(),
|
||||
"events": {str(action): name for action, name in EVENT_NAMES.items()},
|
||||
}
|
||||
)
|
||||
|
||||
async def page_save_binding(self):
|
||||
payload = await request.json(default={})
|
||||
if not isinstance(payload, dict):
|
||||
return error_response("请求正文必须是 JSON 对象", status_code=400)
|
||||
try:
|
||||
await self.server_binding.set_binding(
|
||||
str(payload.get("session_id") or ""),
|
||||
str(payload.get("server") or ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"saved": True})
|
||||
|
||||
async def page_delete_binding(self):
|
||||
payload = await request.json(default={})
|
||||
session_id = str(payload.get("session_id") or "") if isinstance(payload, dict) else ""
|
||||
if not session_id.strip():
|
||||
return error_response("会话 ID 不能为空", status_code=400)
|
||||
await self.server_binding.delete_binding(session_id)
|
||||
return json_response({"deleted": True})
|
||||
|
||||
async def page_save_aliases(self):
|
||||
payload = await request.json(default={})
|
||||
if not isinstance(payload, dict):
|
||||
return error_response("请求正文必须是 JSON 对象", status_code=400)
|
||||
raw_aliases = payload.get("aliases", [])
|
||||
if isinstance(raw_aliases, str):
|
||||
aliases = re.split(r"[,,;;\n]+", raw_aliases)
|
||||
elif isinstance(raw_aliases, list):
|
||||
aliases = [str(value) for value in raw_aliases]
|
||||
else:
|
||||
return error_response("别名必须是字符串或数组", status_code=400)
|
||||
try:
|
||||
await self.server_binding.set_aliases(
|
||||
str(payload.get("server") or ""),
|
||||
aliases,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"saved": True})
|
||||
|
||||
async def page_delete_aliases(self):
|
||||
payload = await request.json(default={})
|
||||
server = str(payload.get("server") or "") if isinstance(payload, dict) else ""
|
||||
if not server.strip():
|
||||
return error_response("标准区服名不能为空", status_code=400)
|
||||
await self.server_binding.delete_aliases(server)
|
||||
return json_response({"deleted": True})
|
||||
|
||||
async def page_save_kungfu(self):
|
||||
payload = await request.json(default={})
|
||||
if not isinstance(payload, dict):
|
||||
return error_response("请求正文必须是 JSON 对象", status_code=400)
|
||||
raw_aliases = payload.get("aliases", [])
|
||||
if isinstance(raw_aliases, str):
|
||||
aliases = re.split(r"[,,;;\n]+", raw_aliases)
|
||||
elif isinstance(raw_aliases, list):
|
||||
aliases = [str(value) for value in raw_aliases]
|
||||
else:
|
||||
return error_response("别名必须是字符串或数组", status_code=400)
|
||||
try:
|
||||
await self.kungfu_alias.save_aliases(
|
||||
payload.get("pzid"),
|
||||
aliases,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return error_response(str(exc), status_code=400)
|
||||
return json_response({"saved": True})
|
||||
|
||||
async def page_refresh_servers(self):
|
||||
servers = await self.jx3api.server_list()
|
||||
if not servers:
|
||||
return error_response("区服目录刷新失败", status_code=502)
|
||||
await self.server_binding.update_server_catalog(servers)
|
||||
return json_response({"servers": self.server_binding.known_servers()})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: astrbot_plugin_jx3
|
||||
display_name: 剑网三游戏数据查询工具
|
||||
desc: 聚合剑网三游戏数据,提供查询、图片渲染、本地避雷和实时事件推送。
|
||||
version: 3.4.2
|
||||
version: 3.4.3
|
||||
author: 飞翔大野猪
|
||||
repo: https://github.com/qsc20001102/astrbot_plugin_jx3
|
||||
astrbot_version: ">=4.24.1"
|
||||
|
||||
+348
-19
@@ -1,6 +1,15 @@
|
||||
const bridge = window.AstrBotPluginPage;
|
||||
const state = { bindings: [], subscriptions: [], aliases: [], kungfu: [], servers: [], events: {} };
|
||||
const editing = { aliasServer: null, kungfuPzid: null };
|
||||
const state = {
|
||||
bindings: [],
|
||||
subscriptions: [],
|
||||
aliases: [],
|
||||
kungfu: [],
|
||||
servers: [],
|
||||
events: {},
|
||||
session_control: { mode: "all", entries: [] },
|
||||
};
|
||||
const editing = { bindingSession: null, controlSession: null, aliasServer: null, kungfuPzid: null };
|
||||
const restoreConfirmationTimers = new WeakMap();
|
||||
let toastTimer;
|
||||
|
||||
const byId = (id) => document.getElementById(id);
|
||||
@@ -69,24 +78,226 @@ function inlineAliasEditor(aliases, label, onSave, onCancel) {
|
||||
return { input, controls: [saveButton, cancelButton] };
|
||||
}
|
||||
|
||||
function createServerSelect(selectedServer = "", label = "绑定区服") {
|
||||
const select = document.createElement("select");
|
||||
select.className = "inline-editor";
|
||||
select.required = true;
|
||||
select.setAttribute("aria-label", label);
|
||||
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "请选择标准区服";
|
||||
placeholder.disabled = true;
|
||||
placeholder.defaultSelected = true;
|
||||
select.append(placeholder);
|
||||
|
||||
state.servers.forEach((server) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = server;
|
||||
option.textContent = server;
|
||||
select.append(option);
|
||||
});
|
||||
select.value = state.servers.includes(selectedServer) ? selectedServer : "";
|
||||
return select;
|
||||
}
|
||||
|
||||
function inlineServerEditor(item, onSave, onCancel) {
|
||||
const select = createServerSelect(item.server, `${item.session_id}的绑定区服`);
|
||||
const saveButton = button("保存", "", async () => {
|
||||
if (!select.reportValidity()) return;
|
||||
select.disabled = true;
|
||||
saveButton.disabled = true;
|
||||
cancelButton.disabled = true;
|
||||
const saved = await onSave(select.value);
|
||||
if (!saved) {
|
||||
select.disabled = false;
|
||||
saveButton.disabled = false;
|
||||
cancelButton.disabled = false;
|
||||
select.focus();
|
||||
}
|
||||
});
|
||||
const cancelButton = button("取消", "", onCancel);
|
||||
select.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
saveButton.click();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
cancelButton.click();
|
||||
}
|
||||
});
|
||||
queueMicrotask(() => select.focus());
|
||||
return { select, controls: [saveButton, cancelButton] };
|
||||
}
|
||||
|
||||
function bindingMap() {
|
||||
return new Map(state.bindings.map((item) => [item.session_id, item.server]));
|
||||
}
|
||||
|
||||
function renderSummary() {
|
||||
byId("binding-count").textContent = String(state.bindings.length);
|
||||
byId("subscription-count").textContent = String(state.subscriptions.filter((item) => item.enabled).length);
|
||||
byId("alias-count").textContent = String(state.aliases.reduce((total, item) => total + item.aliases.length, 0));
|
||||
byId("kungfu-count").textContent = String(state.kungfu.length);
|
||||
}
|
||||
|
||||
function renderServerOptions() {
|
||||
const list = byId("server-options");
|
||||
list.replaceChildren(...state.servers.map((server) => {
|
||||
const select = byId("binding-server");
|
||||
const currentValue = select.value;
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "请选择标准区服";
|
||||
placeholder.disabled = true;
|
||||
placeholder.defaultSelected = true;
|
||||
select.replaceChildren(placeholder, ...state.servers.map((server) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = server;
|
||||
option.textContent = server;
|
||||
return option;
|
||||
}));
|
||||
select.value = state.servers.includes(currentValue) ? currentValue : "";
|
||||
}
|
||||
|
||||
function renderSessionOptions() {
|
||||
const sessionIds = new Set([
|
||||
...state.bindings.map((item) => item.session_id),
|
||||
...state.subscriptions.map((item) => item.session_id),
|
||||
...state.session_control.entries.map((item) => item.session_id),
|
||||
]);
|
||||
byId("session-options").replaceChildren(...[...sessionIds].sort().map((sessionId) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = sessionId;
|
||||
return option;
|
||||
}));
|
||||
}
|
||||
|
||||
function controlModeCopy(mode) {
|
||||
if (mode === "whitelist") {
|
||||
return "只有白名单中的会话可以使用插件和接收事件推送;白名单为空时不放行任何会话。";
|
||||
}
|
||||
if (mode === "blacklist") {
|
||||
return "黑名单中的会话会被拦截;黑名单为空时放行全部会话。";
|
||||
}
|
||||
return "所有会话都可以使用插件并接收已订阅的事件推送;下方名单暂不生效。";
|
||||
}
|
||||
|
||||
function controlModeLabel(mode) {
|
||||
if (mode === "whitelist") return "白名单";
|
||||
if (mode === "blacklist") return "黑名单";
|
||||
return "全部会话";
|
||||
}
|
||||
|
||||
function updateModeSelection(selectedMode) {
|
||||
const activeMode = state.session_control?.mode || "all";
|
||||
document.querySelectorAll(".mode-option").forEach((option) => {
|
||||
const input = option.querySelector('input[name="control_mode"]');
|
||||
option.classList.toggle("is-selected", input?.value === selectedMode);
|
||||
option.classList.toggle("is-active-mode", input?.value === activeMode);
|
||||
});
|
||||
const saveButton = byId("control-mode-save");
|
||||
saveButton.textContent = selectedMode === activeMode
|
||||
? "当前模式已生效"
|
||||
: `切换为${controlModeLabel(selectedMode)}`;
|
||||
}
|
||||
|
||||
function renderSessionControl() {
|
||||
const control = state.session_control || { mode: "all", entries: [] };
|
||||
document.querySelectorAll('input[name="control_mode"]').forEach((input) => {
|
||||
input.checked = input.value === control.mode;
|
||||
});
|
||||
updateModeSelection(control.mode);
|
||||
byId("control-mode-label").textContent = controlModeLabel(control.mode);
|
||||
byId("control-mode-hint").textContent = controlModeCopy(control.mode);
|
||||
|
||||
const body = byId("control-entries-body");
|
||||
if (!control.entries.length) {
|
||||
body.replaceChildren(emptyRow(4, "暂无白名单或黑名单会话"));
|
||||
return;
|
||||
}
|
||||
|
||||
body.replaceChildren(...control.entries.map((item) => {
|
||||
const row = document.createElement("tr");
|
||||
const session = document.createElement("td");
|
||||
const listType = document.createElement("td");
|
||||
const remark = document.createElement("td");
|
||||
const actions = document.createElement("td");
|
||||
session.dataset.label = "会话 ID";
|
||||
listType.dataset.label = "名单类型";
|
||||
remark.dataset.label = "备注";
|
||||
actions.dataset.label = "操作";
|
||||
actions.className = "actions";
|
||||
session.textContent = item.session_id;
|
||||
|
||||
if (editing.controlSession === item.session_id) {
|
||||
row.classList.add("is-editing");
|
||||
const typeSelect = document.createElement("select");
|
||||
typeSelect.className = "inline-editor inline-editor--compact";
|
||||
typeSelect.setAttribute("aria-label", `${item.session_id}的名单类型`);
|
||||
typeSelect.append(
|
||||
new Option("白名单", "whitelist"),
|
||||
new Option("黑名单", "blacklist"),
|
||||
);
|
||||
typeSelect.value = item.list_type;
|
||||
|
||||
const remarkInput = document.createElement("input");
|
||||
remarkInput.className = "inline-editor";
|
||||
remarkInput.maxLength = 200;
|
||||
remarkInput.value = item.remark || "";
|
||||
remarkInput.placeholder = "备注(可选)";
|
||||
remarkInput.setAttribute("aria-label", `${item.session_id}的备注`);
|
||||
|
||||
const saveButton = button("保存", "", async () => {
|
||||
typeSelect.disabled = true;
|
||||
remarkInput.disabled = true;
|
||||
saveButton.disabled = true;
|
||||
cancelButton.disabled = true;
|
||||
editing.controlSession = null;
|
||||
const saved = await mutate(
|
||||
"session-control/save",
|
||||
{ session_id: item.session_id, list_type: typeSelect.value, remark: remarkInput.value },
|
||||
"会话名单已保存",
|
||||
);
|
||||
if (!saved) {
|
||||
editing.controlSession = item.session_id;
|
||||
renderSessionControl();
|
||||
}
|
||||
});
|
||||
const cancelButton = button("取消", "", () => {
|
||||
editing.controlSession = null;
|
||||
renderSessionControl();
|
||||
});
|
||||
remarkInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
saveButton.click();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
cancelButton.click();
|
||||
}
|
||||
});
|
||||
remark.append(remarkInput);
|
||||
listType.append(typeSelect);
|
||||
actions.append(saveButton, cancelButton);
|
||||
queueMicrotask(() => typeSelect.focus());
|
||||
} else {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = `list-badge list-badge--${item.list_type}`;
|
||||
badge.textContent = item.list_type === "whitelist" ? "白名单" : "黑名单";
|
||||
listType.append(badge);
|
||||
remark.textContent = item.remark || "—";
|
||||
actions.append(
|
||||
button("编辑", "", () => {
|
||||
editing.controlSession = item.session_id;
|
||||
renderSessionControl();
|
||||
}),
|
||||
button("删除", "link-button--danger", async (event) => {
|
||||
const deleteButton = event.currentTarget;
|
||||
deleteButton.disabled = true;
|
||||
const deleted = await mutate(
|
||||
"session-control/delete",
|
||||
{ session_id: item.session_id },
|
||||
"会话名单已删除",
|
||||
);
|
||||
if (!deleted) deleteButton.disabled = false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
row.append(session, listType, remark, actions);
|
||||
return row;
|
||||
}));
|
||||
}
|
||||
|
||||
function renderBindings() {
|
||||
@@ -104,19 +315,50 @@ function renderBindings() {
|
||||
server.dataset.label = "绑定区服";
|
||||
actions.dataset.label = "操作";
|
||||
session.textContent = item.session_id;
|
||||
server.textContent = item.server;
|
||||
actions.className = "actions";
|
||||
if (editing.bindingSession === item.session_id) {
|
||||
row.classList.add("is-editing");
|
||||
const editor = inlineServerEditor(
|
||||
item,
|
||||
async (selectedServer) => {
|
||||
editing.bindingSession = null;
|
||||
const saved = await mutate(
|
||||
"bindings/save",
|
||||
{ session_id: item.session_id, server: selectedServer },
|
||||
"绑定信息已保存",
|
||||
);
|
||||
if (!saved) {
|
||||
editing.bindingSession = item.session_id;
|
||||
renderBindings();
|
||||
}
|
||||
return saved;
|
||||
},
|
||||
() => {
|
||||
editing.bindingSession = null;
|
||||
renderBindings();
|
||||
},
|
||||
);
|
||||
server.append(editor.select);
|
||||
actions.append(...editor.controls);
|
||||
} else {
|
||||
server.textContent = item.server;
|
||||
actions.append(
|
||||
button("编辑", "", () => {
|
||||
byId("binding-session").value = item.session_id;
|
||||
byId("binding-server").value = item.server;
|
||||
byId("binding-server").focus();
|
||||
editing.bindingSession = item.session_id;
|
||||
renderBindings();
|
||||
}),
|
||||
button("解除绑定", "link-button--danger", async () => {
|
||||
if (!window.confirm(`确认解除会话 ${item.session_id} 的区服绑定?`)) return;
|
||||
await mutate("bindings/delete", { session_id: item.session_id }, "绑定已解除");
|
||||
button("解除绑定", "link-button--danger", async (event) => {
|
||||
const control = event.currentTarget;
|
||||
control.disabled = true;
|
||||
const deleted = await mutate(
|
||||
"bindings/delete",
|
||||
{ session_id: item.session_id },
|
||||
"绑定已解除",
|
||||
);
|
||||
if (!deleted) control.disabled = false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
row.append(session, server, actions);
|
||||
return row;
|
||||
}));
|
||||
@@ -283,8 +525,9 @@ function renderKungfu() {
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderSummary();
|
||||
renderServerOptions();
|
||||
renderSessionOptions();
|
||||
renderSessionControl();
|
||||
renderBindings();
|
||||
renderSubscriptions();
|
||||
renderAliases();
|
||||
@@ -309,6 +552,50 @@ async function mutate(endpoint, payload, successMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
function resetRestoreConfirmation(control) {
|
||||
const timer = restoreConfirmationTimers.get(control);
|
||||
if (timer) clearTimeout(timer);
|
||||
restoreConfirmationTimers.delete(control);
|
||||
delete control.dataset.confirming;
|
||||
control.classList.remove("button--danger");
|
||||
control.textContent = "恢复默认";
|
||||
}
|
||||
|
||||
function confirmRestoreInPage(control, confirmation) {
|
||||
if (control.dataset.confirming === "true") {
|
||||
resetRestoreConfirmation(control);
|
||||
return true;
|
||||
}
|
||||
|
||||
control.dataset.confirming = "true";
|
||||
control.classList.add("button--danger");
|
||||
control.textContent = "再次点击确认";
|
||||
showToast(confirmation);
|
||||
restoreConfirmationTimers.set(
|
||||
control,
|
||||
setTimeout(() => resetRestoreConfirmation(control), 5000),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
async function restoreDefaults(control, endpoint, confirmation, successMessage, resetEditing) {
|
||||
if (!confirmRestoreInPage(control, confirmation)) return;
|
||||
const originalLabel = control.textContent;
|
||||
control.disabled = true;
|
||||
control.textContent = "恢复中…";
|
||||
resetEditing();
|
||||
try {
|
||||
await bridge.apiPost(endpoint, {});
|
||||
await loadData();
|
||||
showToast(successMessage);
|
||||
} catch (error) {
|
||||
showToast(error?.message || "恢复默认失败", true);
|
||||
} finally {
|
||||
control.disabled = false;
|
||||
control.textContent = originalLabel;
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll(".tab").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
document.querySelectorAll(".tab").forEach((item) => {
|
||||
@@ -333,6 +620,48 @@ byId("binding-form").addEventListener("submit", async (event) => {
|
||||
if (saved) event.currentTarget.reset();
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="control_mode"]').forEach((input) => {
|
||||
input.addEventListener("change", () => {
|
||||
updateModeSelection(input.value);
|
||||
});
|
||||
});
|
||||
|
||||
byId("control-mode-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const selected = new FormData(event.currentTarget).get("control_mode");
|
||||
await mutate("session-control/mode", { mode: selected }, "会话控制模式已保存");
|
||||
});
|
||||
|
||||
byId("control-entry-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const saved = await mutate("session-control/save", {
|
||||
session_id: byId("control-session").value,
|
||||
list_type: byId("control-list-type").value,
|
||||
remark: byId("control-remark").value,
|
||||
}, "会话名单已保存");
|
||||
if (saved) event.currentTarget.reset();
|
||||
});
|
||||
|
||||
byId("restore-aliases").addEventListener("click", async (event) => {
|
||||
await restoreDefaults(
|
||||
event.currentTarget,
|
||||
"aliases/restore",
|
||||
"再次点击按钮,确认使用内置 JSON 覆盖当前全部区服别名",
|
||||
"区服别名已恢复默认",
|
||||
() => { editing.aliasServer = null; },
|
||||
);
|
||||
});
|
||||
|
||||
byId("restore-kungfu").addEventListener("click", async (event) => {
|
||||
await restoreDefaults(
|
||||
event.currentTarget,
|
||||
"kungfu/restore",
|
||||
"再次点击按钮,确认使用内置 JSON 覆盖当前全部心法及别名",
|
||||
"心法别名已恢复默认",
|
||||
() => { editing.kungfuPzid = null; },
|
||||
);
|
||||
});
|
||||
|
||||
byId("refresh").addEventListener("click", async (event) => {
|
||||
const control = event.currentTarget;
|
||||
control.disabled = true;
|
||||
|
||||
@@ -12,38 +12,76 @@
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>剑网三插件管理</h1>
|
||||
<p>管理会话区服绑定、事件订阅状态、区服别名与心法别名。</p>
|
||||
<p>管理事件推送、会话访问范围、区服绑定及别名。</p>
|
||||
</div>
|
||||
<button class="button button--secondary" id="refresh" type="button">刷新数据</button>
|
||||
</header>
|
||||
|
||||
<section class="summary" aria-label="数据概览">
|
||||
<div class="summary__item">
|
||||
<span>已绑定会话</span>
|
||||
<strong id="binding-count">—</strong>
|
||||
</div>
|
||||
<div class="summary__item">
|
||||
<span>事件推送会话</span>
|
||||
<strong id="subscription-count">—</strong>
|
||||
</div>
|
||||
<div class="summary__item">
|
||||
<span>区服别名</span>
|
||||
<strong id="alias-count">—</strong>
|
||||
</div>
|
||||
<div class="summary__item">
|
||||
<span>心法条目</span>
|
||||
<strong id="kungfu-count">—</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav class="tabs" aria-label="管理分类" role="tablist">
|
||||
<button class="tab is-active" type="button" role="tab" aria-selected="true" data-tab="bindings">会话绑定</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="subscriptions">事件订阅</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="bindings">区服绑定</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="aliases">区服别名</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false" data-tab="kungfu">心法别名</button>
|
||||
</nav>
|
||||
|
||||
<section class="panel is-active" id="bindings-panel" role="tabpanel">
|
||||
<section class="panel is-active" id="session-control-panel" role="tabpanel">
|
||||
<form class="control-mode" id="control-mode-form">
|
||||
<div class="section-intro control-mode__intro">
|
||||
<div>
|
||||
<h2>会话访问模式</h2>
|
||||
<p id="control-mode-hint">所有会话都可以使用插件并接收已订阅的事件推送。</p>
|
||||
</div>
|
||||
<div class="current-mode" aria-live="polite">
|
||||
<span>当前生效模式</span>
|
||||
<strong id="control-mode-label">全部会话</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-options" role="radiogroup" aria-label="会话访问模式">
|
||||
<label class="mode-option">
|
||||
<input type="radio" name="control_mode" value="all" />
|
||||
<span><strong>全部会话</strong><small>不限制任何会话,默认模式</small></span>
|
||||
</label>
|
||||
<label class="mode-option">
|
||||
<input type="radio" name="control_mode" value="whitelist" />
|
||||
<span><strong>白名单</strong><small>只允许白名单中的会话</small></span>
|
||||
</label>
|
||||
<label class="mode-option">
|
||||
<input type="radio" name="control_mode" value="blacklist" />
|
||||
<span><strong>黑名单</strong><small>只拦截黑名单中的会话</small></span>
|
||||
</label>
|
||||
<button class="button button--primary" id="control-mode-save" type="submit">保存模式</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="editor editor--control" id="control-entry-form">
|
||||
<label>
|
||||
<span>会话 ID</span>
|
||||
<input id="control-session" name="session_id" list="session-options" maxlength="512" placeholder="可选择已有会话或直接输入" required />
|
||||
<datalist id="session-options"></datalist>
|
||||
</label>
|
||||
<label>
|
||||
<span>名单类型</span>
|
||||
<select id="control-list-type" name="list_type" required>
|
||||
<option value="whitelist">白名单</option>
|
||||
<option value="blacklist">黑名单</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>备注(可选)</span>
|
||||
<input id="control-remark" name="remark" maxlength="200" placeholder="例如:测试会话" />
|
||||
</label>
|
||||
<button class="button button--primary" type="submit">添加会话</button>
|
||||
</form>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>会话 ID</th><th>名单类型</th><th>备注</th><th class="actions">操作</th></tr></thead>
|
||||
<tbody id="control-entries-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" id="bindings-panel" role="tabpanel" hidden>
|
||||
<form class="editor" id="binding-form">
|
||||
<label>
|
||||
<span>会话 ID</span>
|
||||
@@ -51,9 +89,11 @@
|
||||
</label>
|
||||
<label>
|
||||
<span>绑定区服</span>
|
||||
<input id="binding-server" name="server" maxlength="64" list="server-options" placeholder="选择或输入标准区服名/别名" required />
|
||||
<select id="binding-server" name="server" required>
|
||||
<option value="" selected disabled>请选择标准区服</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="button button--primary" type="submit">保存绑定</button>
|
||||
<button class="button button--primary" type="submit">添加绑定</button>
|
||||
</form>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@@ -76,8 +116,9 @@
|
||||
</section>
|
||||
|
||||
<section class="panel" id="aliases-panel" role="tabpanel" hidden>
|
||||
<div class="section-intro">
|
||||
<div><h2>区服别名</h2><p>标准区服名称为只读;点击对应行的“编辑”可修改别名。</p></div>
|
||||
<div class="section-intro section-intro--actions">
|
||||
<div><h2>区服别名</h2><p>标准区服名称为只读;点击对应行的“编辑”可修改别名,恢复默认会使用内置 JSON 覆盖当前配置。</p></div>
|
||||
<button class="button button--secondary" id="restore-aliases" type="button">恢复默认</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@@ -88,8 +129,9 @@
|
||||
</section>
|
||||
|
||||
<section class="panel" id="kungfu-panel" role="tabpanel" hidden>
|
||||
<div class="section-intro">
|
||||
<div><h2>心法别名</h2><p>标准心法名称为只读;点击对应行的“编辑”可修改最多 5 个别名。</p></div>
|
||||
<div class="section-intro section-intro--actions">
|
||||
<div><h2>心法别名</h2><p>标准心法名称为只读;点击对应行的“编辑”可修改最多 5 个别名,恢复默认会使用内置 JSON 覆盖当前配置。</p></div>
|
||||
<button class="button button--secondary" id="restore-kungfu" type="button">恢复默认</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@@ -100,7 +142,6 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<datalist id="server-options"></datalist>
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
<script type="module" src="./app.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -35,35 +35,48 @@
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--text); font-size: 15px; line-height: 1.55; }
|
||||
button, input { font: inherit; }
|
||||
button, input, select { font: inherit; }
|
||||
.shell { width: min(1260px, calc(100% - 40px)); margin: 0 auto; padding: 32px 0 56px; }
|
||||
.page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 24px; }
|
||||
h1 { margin: 0; font-size: clamp(26px, 3vw, 36px); line-height: 1.2; letter-spacing: -0.025em; }
|
||||
.page-header p, .section-intro p { margin: 7px 0 0; color: var(--muted); }
|
||||
.button { min-height: 42px; padding: 9px 18px; border: 1px solid transparent; border-radius: 9px; cursor: pointer; font-weight: 650; transition: 160ms ease; white-space: nowrap; }
|
||||
.button:focus-visible, .tab:focus-visible, input:focus-visible, .link-button:focus-visible { outline: 3px solid color-mix(in srgb, var(--focus) 45%, transparent); outline-offset: 2px; }
|
||||
.button:focus-visible, .tab:focus-visible, input:focus-visible, select:focus-visible, .link-button:focus-visible { outline: 3px solid color-mix(in srgb, var(--focus) 45%, transparent); outline-offset: 2px; }
|
||||
.button:disabled { cursor: wait; opacity: .65; }
|
||||
.button--primary { color: #fff; background: var(--accent); box-shadow: 0 5px 14px color-mix(in srgb, var(--accent) 20%, transparent); }
|
||||
.button--primary:hover { background: var(--accent-hover); transform: translateY(-1px); }
|
||||
.button--secondary { color: var(--text); background: var(--surface); border-color: var(--border); }
|
||||
.button--secondary:hover { border-color: color-mix(in srgb, var(--text) 24%, var(--border)); }
|
||||
.summary { display: grid; grid-template-columns: repeat(4, 1fr); background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow); }
|
||||
.summary__item { display: flex; min-height: 112px; flex-direction: column; justify-content: center; padding: 22px 30px; }
|
||||
.summary__item + .summary__item { border-left: 1px solid var(--border); }
|
||||
.summary span { color: var(--muted); font-weight: 600; }
|
||||
.summary strong { margin-top: 2px; font-size: 30px; line-height: 1.2; }
|
||||
.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; }
|
||||
.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:hover, .tab.is-active { color: var(--accent); }
|
||||
.tab.is-active::after { position: absolute; right: 0; bottom: -1px; left: 0; height: 3px; border-radius: 3px 3px 0 0; background: var(--accent); content: ""; }
|
||||
.panel { margin-top: 18px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; }
|
||||
.panel[hidden] { display: none; }
|
||||
.control-mode { background: var(--surface-subtle); border-bottom: 1px solid var(--border); }
|
||||
.control-mode__intro { display: flex; align-items: center; justify-content: space-between; gap: 24px; border-bottom: 0; padding-bottom: 16px; }
|
||||
.current-mode { display: grid; flex: 0 0 auto; min-width: 210px; gap: 2px; padding: 13px 17px; border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--border)); border-radius: 10px; background: var(--accent-soft); box-shadow: inset 4px 0 0 var(--accent); }
|
||||
.current-mode span { color: var(--muted); font-size: 12px; font-weight: 700; }
|
||||
.current-mode strong { color: var(--accent); font-size: 20px; line-height: 1.35; }
|
||||
.mode-options { display: grid; grid-template-columns: repeat(3, minmax(180px, 1fr)) auto; align-items: stretch; gap: 12px; padding: 0 22px 22px; }
|
||||
.mode-option { position: relative; display: flex; grid-template-columns: none; align-items: flex-start; gap: 10px; min-height: 76px; padding: 14px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface); cursor: pointer; font-size: 14px; }
|
||||
.mode-option:hover, .mode-option.is-selected { border-color: color-mix(in srgb, var(--accent) 58%, var(--border)); background: var(--accent-soft); }
|
||||
.mode-option.is-active-mode::after { position: absolute; top: 8px; right: 8px; padding: 2px 7px; border-radius: 999px; background: var(--accent); color: #fff; content: "当前"; font-size: 11px; font-weight: 700; }
|
||||
.mode-option input { flex: 0 0 auto; width: 18px; height: 18px; margin: 2px 0 0; accent-color: var(--accent); box-shadow: none; }
|
||||
.mode-option span { display: grid; gap: 3px; }
|
||||
.mode-option strong { color: var(--text); font-size: 14px; }
|
||||
.mode-option small { color: var(--muted); font-size: 12px; font-weight: 500; line-height: 1.4; }
|
||||
.mode-options .button { align-self: center; }
|
||||
.editor { display: grid; grid-template-columns: minmax(260px, 1.35fr) minmax(210px, 1fr) auto; align-items: end; gap: 16px; padding: 22px; background: var(--surface-subtle); border-bottom: 1px solid var(--border); }
|
||||
.editor--control { grid-template-columns: minmax(250px, 1.5fr) minmax(150px, .7fr) minmax(190px, 1fr) auto; }
|
||||
label { display: grid; gap: 7px; color: var(--text); font-size: 13px; font-weight: 650; }
|
||||
input { width: 100%; height: 42px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text); transition: border 160ms ease, box-shadow 160ms ease; }
|
||||
input, select { width: 100%; height: 42px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text); transition: border 160ms ease, box-shadow 160ms ease; }
|
||||
input::placeholder { color: color-mix(in srgb, var(--muted) 76%, transparent); }
|
||||
input:focus { border-color: var(--focus); box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 18%, transparent); outline: none; }
|
||||
input:focus, select:focus { border-color: var(--focus); box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 18%, transparent); outline: none; }
|
||||
.section-intro { padding: 22px; border-bottom: 1px solid var(--border); }
|
||||
.section-intro--actions { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
||||
.section-intro h2 { margin: 0; font-size: 18px; }
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; table-layout: auto; border-collapse: collapse; }
|
||||
@@ -74,12 +87,16 @@ tbody tr:hover { background: color-mix(in srgb, var(--accent-soft) 42%, transpar
|
||||
tbody tr.is-editing { background: color-mix(in srgb, var(--accent-soft) 62%, transparent); }
|
||||
.alias-cell { min-width: 320px; }
|
||||
.inline-editor { min-width: 260px; }
|
||||
.inline-editor--compact { min-width: 130px; }
|
||||
.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); }
|
||||
.link-button:disabled { cursor: wait; opacity: .55; }
|
||||
.link-button--danger { color: var(--danger); }
|
||||
.list-badge { display: inline-flex; padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 700; }
|
||||
.list-badge--whitelist { background: color-mix(in srgb, var(--success) 12%, transparent); color: var(--success); }
|
||||
.list-badge--blacklist { background: color-mix(in srgb, var(--danger) 12%, transparent); color: var(--danger); }
|
||||
.state { display: inline-flex; align-items: center; gap: 7px; font-weight: 650; }
|
||||
.state::before { width: 8px; height: 8px; border-radius: 50%; background: currentColor; content: ""; }
|
||||
.state--on { color: var(--success); }
|
||||
@@ -94,12 +111,14 @@ tbody tr.is-editing { background: color-mix(in srgb, var(--accent-soft) 62%, tra
|
||||
@media (max-width: 760px) {
|
||||
.shell { width: min(100% - 24px, 1260px); padding-top: 20px; }
|
||||
.page-header { align-items: stretch; flex-direction: column; }
|
||||
.summary { grid-template-columns: 1fr; }
|
||||
.summary__item { min-height: 86px; padding: 16px 20px; }
|
||||
.summary__item + .summary__item { border-top: 1px solid var(--border); border-left: 0; }
|
||||
.section-intro--actions { align-items: stretch; flex-direction: column; }
|
||||
.tabs { gap: 16px; overflow-x: auto; }
|
||||
.tab { flex: 0 0 auto; }
|
||||
.editor { grid-template-columns: 1fr; }
|
||||
.mode-options { grid-template-columns: 1fr; }
|
||||
.mode-options .button { width: 100%; }
|
||||
.control-mode__intro { align-items: stretch; flex-direction: column; }
|
||||
.current-mode { min-width: 0; }
|
||||
.table-wrap { overflow: visible; }
|
||||
table, tbody, tr, td { display: block; width: 100%; }
|
||||
thead { display: none; }
|
||||
|
||||
+137
-133
@@ -1,197 +1,201 @@
|
||||
{# template-title: 剑网三插件功能介绍 #}
|
||||
{# 每行说明可填写在 command-row 的 data-description 属性中,例如:data-description="这里填写说明" #}
|
||||
<div class="page help-page">
|
||||
<header class="top-info help-header">
|
||||
<div>
|
||||
<div class="top-name">剑网三综合查询插件</div>
|
||||
<div class="top-summary">共 17 个功能分类 · 69 项功能 · 93 个触发词</div>
|
||||
<div class="top-summary">18 个功能分区 · 92 个触发指令</div>
|
||||
</div>
|
||||
<div class="top-desc">
|
||||
指令与参数之间使用空格分隔<br>
|
||||
带有 [ ] 的参数为可选参数
|
||||
<div class="help-rules">
|
||||
<span>参数之间使用空格分隔</span>
|
||||
<span><strong>[ ]</strong> 内为可选参数</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="section-grid">
|
||||
<section class="section span-2">
|
||||
<div class="section-title">活动情报</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">活动日历</div><div class="commands"><span class="command">日常 [延后天数]</span><span class="command">日常预测</span></div></div>
|
||||
<div class="feature"><div class="feature-name">声望社</div><div class="commands"><span class="command">楚天社</span><span class="command">云从社</span><span class="command">披风会</span><span class="command">穹野卫</span></div></div>
|
||||
<div class="feature"><div class="feature-name">关隘首领</div><div class="commands"><span class="command">关隘</span></div></div>
|
||||
<div class="feature"><div class="feature-name">赤兔</div><div class="commands"><span class="command">赤兔</span><span class="command">本周赤兔</span></div></div>
|
||||
<div class="feature"><div class="feature-name">阵营事件</div><div class="commands"><span class="command">阵营奉献 [阵营]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">烟花记录</div><div class="commands"><span class="command">烟花 服务器 角色</span></div></div>
|
||||
<div class="feature"><div class="feature-name">马场查询</div><div class="commands"><span class="command">刷马 服务器</span><span class="command">马场 服务器</span></div></div>
|
||||
<main class="command-directory">
|
||||
<div class="command-columns" aria-hidden="true">
|
||||
<span>功能</span><span>触发指令</span><span>说明</span>
|
||||
</div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">01</span><h2>活动情报</h2><span class="section-count">13 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">活动日历</div><div class="commands"><code class="command">日常 [延后天数]</code><code class="command">日常预测</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">声望社</div><div class="commands"><code class="command">楚天社</code><code class="command">云从社</code><code class="command">披风会</code><code class="command">穹野卫</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">关隘首领</div><div class="commands"><code class="command">关隘</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">赤兔记录</div><div class="commands"><code class="command">赤兔</code><code class="command">本周赤兔</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">阵营事件</div><div class="commands"><code class="command">阵营奉献 [阵营]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">烟花记录</div><div class="commands"><code class="command">烟花 服务器 角色</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">马场查询</div><div class="commands"><code class="command">刷马 服务器</code><code class="command">马场 服务器</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-title">名剑相关</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">名剑战绩</div><div class="commands"><span class="command">战绩 服务器 角色 [模式]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">名剑排行</div><div class="commands"><span class="command">名剑排行 [模式] [数量]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">名剑统计</div><div class="commands"><span class="command">名剑统计 [模式]</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">02</span><h2>名剑相关</h2><span class="section-count">3 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row" data-description="模式可选22、33、55"><div class="feature-name">名剑战绩</div><div class="commands"><code class="command">战绩 服务器 角色 [模式]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">名剑排行</div><div class="commands"><code class="command">名剑排行 [模式] [数量]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">名剑统计</div><div class="commands"><code class="command">名剑统计 [模式]</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section span-3 ranking-section">
|
||||
<div class="section-title">排行榜单</div>
|
||||
<div class="section-body">
|
||||
<div class="feature ranking-feature">
|
||||
<div class="feature-name">排行统计(二轮选择)</div>
|
||||
<div class="ranking-list">
|
||||
<span class="command">帮会排行 服务器</span>
|
||||
<span class="command">阵营排行 服务器</span>
|
||||
<span class="command">其他排行 服务器</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature trial-feature"><div class="feature-name">试炼排行</div><div class="commands"><span class="command">试炼排行 服务器 心法</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">03</span><h2>排行榜单</h2><span class="section-count">4 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">分类排行</div><div class="commands"><code class="command">帮会排行 服务器</code><code class="command">阵营排行 服务器</code><code class="command">其他排行 服务器</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">试炼排行</div><div class="commands"><code class="command">试炼排行 服务器 心法</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section span-2">
|
||||
<div class="section-title">物价交易</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">阵营拍卖</div><div class="commands"><span class="command">阵营拍卖 服务器 [物品] [数量]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">的卢拍卖</div><div class="commands"><span class="command">的卢 服务器</span></div></div>
|
||||
<div class="feature"><div class="feature-name">金价行情</div><div class="commands"><span class="command">金价 服务器</span></div></div>
|
||||
<div class="feature"><div class="feature-name">物品价格</div><div class="commands"><span class="command">物价 外观名称 [服务器]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">成本计算</div><div class="commands"><span class="command">成本 服务器 物品名称</span></div></div>
|
||||
<div class="feature"><div class="feature-name">编号搜索</div><div class="commands"><span class="command">看号 万宝楼编号</span></div></div>
|
||||
<div class="feature"><div class="feature-name">交易行</div><div class="commands"><span class="command">交易行 服务器 物品</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">04</span><h2>资历相关</h2><span class="section-count">2 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">角色资历</div><div class="commands"><code class="command">资历 服务器 角色</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">资历排行</div><div class="commands"><code class="command">资历排行 [服务器] [门派]</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-title">阵营战场</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">帮战记录</div><div class="commands"><span class="command">帮战 服务器</span></div></div>
|
||||
<div class="feature"><div class="feature-name">阵营沙盘</div><div class="commands"><span class="command">沙盘 服务器</span></div></div>
|
||||
<div class="feature"><div class="feature-name">诛恶事件</div><div class="commands"><span class="command">诛恶 服务器</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">05</span><h2>物价交易</h2><span class="section-count">7 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">阵营拍卖</div><div class="commands"><code class="command">阵营拍卖 服务器 [物品] [数量]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">的卢拍卖</div><div class="commands"><code class="command">的卢 服务器</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">金价行情</div><div class="commands"><code class="command">金价 服务器</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">物品价格</div><div class="commands"><code class="command">物价 外观名称 [服务器]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">成本计算</div><div class="commands"><code class="command">成本 服务器 物品名称</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">编号搜索</div><div class="commands"><code class="command">看号 万宝楼编号</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">交易行</div><div class="commands"><code class="command">交易行 服务器 物品</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-title">角色名片</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">名片缓存</div><div class="commands"><span class="command">名片 服务器 角色</span></div></div>
|
||||
<div class="feature"><div class="feature-name">名片历史</div><div class="commands"><span class="command">全名片 服务器 角色</span></div></div>
|
||||
<div class="feature"><div class="feature-name">随机名片</div><div class="commands"><span class="command">随机秀 服务器 [门派] [体型]</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">06</span><h2>阵营战场</h2><span class="section-count">3 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">帮战记录</div><div class="commands"><code class="command">帮战 服务器</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">阵营沙盘</div><div class="commands"><code class="command">沙盘 服务器</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">诛恶事件</div><div class="commands"><code class="command">诛恶 服务器</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section span-2">
|
||||
<div class="section-title">奇遇相关</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">奇遇记录</div><div class="commands"><span class="command">奇遇 服务器 角色</span><span class="command">查询 服务器 角色</span></div></div>
|
||||
<div class="feature"><div class="feature-name">未触发的</div><div class="commands"><span class="command">未出 服务器 角色</span></div></div>
|
||||
<div class="feature"><div class="feature-name">奇遇汇总</div><div class="commands"><span class="command">汇总 服务器 [天数]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">近期奇遇</div><div class="commands"><span class="command">近期 服务器 [数量]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">奇遇统计</div><div class="commands"><span class="command">统计 奇遇 [服务器] [数量]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">奇遇攻略</div><div class="commands"><span class="command">攻略 奇遇</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">07</span><h2>角色名片</h2><span class="section-count">3 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">名片缓存</div><div class="commands"><code class="command">名片 服务器 角色</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">名片历史</div><div class="commands"><code class="command">全名片 服务器 角色</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">随机名片</div><div class="commands"><code class="command">随机秀 服务器 [门派] [体型]</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-title">百战记录</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">角色百战</div><div class="commands"><span class="command">精耐 服务器 角色</span></div></div>
|
||||
<div class="feature"><div class="feature-name">百战首领</div><div class="commands"><span class="command">百战</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">08</span><h2>奇遇相关</h2><span class="section-count">7 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">奇遇记录</div><div class="commands"><code class="command">奇遇 服务器 角色</code><code class="command">查询 服务器 角色</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">未触发奇遇</div><div class="commands"><code class="command">未出 服务器 角色</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">奇遇汇总</div><div class="commands"><code class="command">汇总 服务器 [天数]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">近期奇遇</div><div class="commands"><code class="command">近期 服务器 [数量]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">奇遇统计</div><div class="commands"><code class="command">统计 奇遇 [服务器] [数量]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">奇遇攻略</div><div class="commands"><code class="command">攻略 奇遇</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-title">角色信息</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">成就查询</div><div class="commands"><span class="command">成就 服务器 角色 成就</span></div></div>
|
||||
<div class="feature"><div class="feature-name">角色详情</div><div class="commands"><span class="command">角色 服务器 名称</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">09</span><h2>百战记录</h2><span class="section-count">2 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">角色百战</div><div class="commands"><code class="command">精耐 服务器 角色</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">百战首领</div><div class="commands"><code class="command">百战</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-title">资历相关</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">资历查询</div><div class="commands"><span class="command">资历 服务器 角色</span></div></div>
|
||||
<div class="feature"><div class="feature-name">资历榜单</div><div class="commands"><span class="command">资历排行 [服务器] [门派]</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">10</span><h2>角色信息</h2><span class="section-count">2 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">成就查询</div><div class="commands"><code class="command">成就 服务器 角色 成就</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">角色详情</div><div class="commands"><code class="command">角色 服务器 名称</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-title">心法配装</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">阵眼效果</div><div class="commands"><span class="command">阵眼 心法</span></div></div>
|
||||
<div class="feature"><div class="feature-name">魔盒配装</div><div class="commands"><span class="command">配装 心法</span></div></div>
|
||||
<div class="feature"><div class="feature-name">技能信息</div><div class="commands"><span class="command">技能 心法</span></div></div>
|
||||
<div class="feature"><div class="feature-name">奇穴信息</div><div class="commands"><span class="command">奇穴 心法</span></div></div>
|
||||
<div class="feature"><div class="feature-name">魔盒宏</div><div class="commands"><span class="command">宏 心法</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">11</span><h2>心法配装</h2><span class="section-count">6 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">阵眼效果</div><div class="commands"><code class="command">阵眼 心法</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">魔盒配装</div><div class="commands"><code class="command">配装 心法</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">技能信息</div><div class="commands"><code class="command">技能 心法</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">奇穴信息</div><div class="commands"><code class="command">奇穴 心法</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">魔盒宏</div><div class="commands"><code class="command">宏 心法</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">小药搭配</div><div class="commands"><code class="command">小药 心法</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section span-2">
|
||||
<div class="section-title">游戏社区</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">角色聊天</div><div class="commands"><span class="command">聊天 服务器 角色 [条数] [页数]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">统战歪歪</div><div class="commands"><span class="command">统战 [服务器]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">师徒列表</div><div class="commands"><span class="command">拜师 服务器 [关键词]</span><span class="command">收徒 服务器 [关键词]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">团队招募</div><div class="commands"><span class="command">招募 服务器 [副本]</span><span class="command">团长 服务器 [名称]</span><span class="command">团牌 服务器 [内容]</span></div></div>
|
||||
<div class="feature full-row"><div class="feature-name">骚话语录</div><div class="commands"><span class="command">答案之书</span><span class="command">舔狗语录</span><span class="command">疯狂星期四</span><span class="command">彩虹屁</span><span class="command">毒鸡汤</span><span class="command">朋友圈</span><span class="command">喝什么</span><span class="command">吃什么</span><span class="command">骚话</span><span class="command">渣男语录</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">12</span><h2>游戏社区</h2><span class="section-count">8 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">角色发言</div><div class="commands"><code class="command">发言 服务器 角色 [条数] [页数]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">统战歪歪</div><div class="commands"><code class="command">统战 [服务器]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">骗子查询</div><div class="commands"><code class="command">骗子 UID [服务器]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">师徒列表</div><div class="commands"><code class="command">拜师 服务器 [关键词]</code><code class="command">收徒 服务器 [关键词]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">团队招募</div><div class="commands"><code class="command">招募 服务器 [副本]</code><code class="command">团长 服务器 [名称]</code><code class="command">团牌 服务器 [内容]</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-title">百度贴吧</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">外观记录</div><div class="commands"><span class="command">贴吧物价 名称 [服务器] [数量]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">百度贴吧</div><div class="commands"><span class="command">818 [服务器] [数量]</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">13</span><h2>家园杂项</h2><span class="section-count">3 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">家园鲜花</div><div class="commands"><code class="command">花价 服务器 [名称] [地图]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">家园装饰</div><div class="commands"><code class="command">装饰 名称</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">器物图谱</div><div class="commands"><code class="command">器物 地图名称</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section span-2">
|
||||
<div class="section-title">系统工具</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">维护公告</div><div class="commands"><span class="command">维护 [数量]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">新闻资讯</div><div class="commands"><span class="command">新闻 [数量]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">大区状态</div><div class="commands"><span class="command">区服</span><span class="command">开服 服务器</span></div></div>
|
||||
<div class="feature"><div class="feature-name">技改记录</div><div class="commands"><span class="command">技改</span></div></div>
|
||||
<div class="feature"><div class="feature-name">秘境方位</div><div class="commands"><span class="command">解密</span></div></div>
|
||||
<div class="feature"><div class="feature-name">副本记录</div><div class="commands"><span class="command">副本 服务器 角色</span></div></div>
|
||||
<div class="feature"><div class="feature-name">掉落统计</div><div class="commands"><span class="command">掉落 物品 [服务器] [数量]</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">14</span><h2>系统工具</h2><span class="section-count">8 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">维护公告</div><div class="commands"><code class="command">维护 [数量]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">新闻资讯</div><div class="commands"><code class="command">新闻 [数量]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">科举答案</div><div class="commands"><code class="command">科举 题目 [条数]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">大区状态</div><div class="commands"><code class="command">区服</code><code class="command">开服 服务器</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">技改记录</div><div class="commands"><code class="command">技改</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">秘境方位</div><div class="commands"><code class="command">解密</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">掉落统计</div><div class="commands"><code class="command">掉落 物品 [服务器] [数量]</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-title">杂项功能</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">家园鲜花</div><div class="commands"><span class="command">花价 服务器 [名称] [地图]</span></div></div>
|
||||
<div class="feature"><div class="feature-name">家园装饰</div><div class="commands"><span class="command">装饰 名称</span></div></div>
|
||||
<div class="feature"><div class="feature-name">器物图谱</div><div class="commands"><span class="command">器物 地图名称</span></div></div>
|
||||
<div class="feature"><div class="feature-name">科举答案</div><div class="commands"><span class="command">科举 题目 [条数]</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">15</span><h2>趣味互动</h2><span class="section-count">10 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">随机回答</div><div class="commands"><code class="command">答案之书</code><code class="command">喝什么</code><code class="command">吃什么</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">语录生成</div><div class="commands"><code class="command">舔狗语录</code><code class="command">疯狂星期四</code><code class="command">彩虹屁</code><code class="command">毒鸡汤</code><code class="command">朋友圈</code><code class="command">骚话</code><code class="command">渣男语录</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section span-2 section--danger">
|
||||
<div class="section-title">避雷功能</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">避雷添加</div><div class="commands"><span class="command">避雷添加 名称 备注</span></div></div>
|
||||
<div class="feature"><div class="feature-name">避雷查看</div><div class="commands"><span class="command">避雷查看</span></div></div>
|
||||
<div class="feature"><div class="feature-name">避雷查询</div><div class="commands"><span class="command">避雷查询 名称</span></div></div>
|
||||
<div class="feature"><div class="feature-name">避雷修改</div><div class="commands"><span class="command">避雷修改 ID 名称 备注</span></div></div>
|
||||
<div class="feature"><div class="feature-name">避雷删除</div><div class="commands"><span class="command">避雷删除 ID</span></div></div>
|
||||
<section class="command-section">
|
||||
<header class="command-section__header"><span class="section-index">16</span><h2>百度贴吧</h2><span class="section-count">2 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">外观记录</div><div class="commands"><code class="command">贴吧物价 名称 [服务器] [数量]</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">贴吧内容</div><div class="commands"><code class="command">818 [服务器] [数量]</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section span-3 section--success push-section">
|
||||
<div class="section-title">推送功能</div>
|
||||
<div class="section-body">
|
||||
<div class="feature"><div class="feature-name">绑定区服</div><div class="commands"><span class="command">绑定区服 [区服]</span><span class="command">解绑区服</span></div></div>
|
||||
<div class="feature"><div class="feature-name">事件总开关</div><div class="commands"><span class="command">事件推送 开启/关闭</span></div></div>
|
||||
<div class="feature"><div class="feature-name">订阅事件</div><div class="commands"><span class="command">事件推送 编号 开启/关闭</span></div></div>
|
||||
<div class="feature"><div class="feature-name">订阅状态</div><div class="commands"><span class="command">事件推送 [状态/列表]</span></div></div>
|
||||
<section class="command-section command-section--success">
|
||||
<header class="command-section__header"><span class="section-index">17</span><h2>插件与推送</h2><span class="section-count">4 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">功能帮助</div><div class="commands"><code class="command">功能</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">区服绑定</div><div class="commands"><code class="command">绑定区服 [区服]</code><code class="command">解绑区服</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">事件推送</div><div class="commands"><code class="command">事件推送 [状态/列表]</code><code class="command">事件推送 开启/关闭</code><code class="command">事件推送 编号 开启/关闭</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="command-section command-section--danger">
|
||||
<header class="command-section__header"><span class="section-index">18</span><h2>避雷功能</h2><span class="section-count">5 个触发词</span></header>
|
||||
<div class="command-list">
|
||||
<div class="command-row"><div class="feature-name">新增记录</div><div class="commands"><code class="command">避雷添加 名称 备注</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">查看记录</div><div class="commands"><code class="command">避雷查看</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">查询记录</div><div class="commands"><code class="command">避雷查询 名称</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">修改记录</div><div class="commands"><code class="command">避雷修改 ID 名称 备注</code></div></div>
|
||||
<div class="command-row"><div class="feature-name">删除记录</div><div class="commands"><code class="command">避雷删除 ID</code></div></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer help-footer">
|
||||
指令与参数之间使用空格分隔,带有 <strong>[ ]</strong> 的参数可以省略;会话绑定区服后可省略区服参数,在区服位置填写“全区”可查询全区数据。
|
||||
会话绑定区服后可省略指令中的区服参数;在区服位置填写“全区”可显式查询全区数据。
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+167
-116
@@ -1,167 +1,218 @@
|
||||
.help-page {
|
||||
position: relative;
|
||||
padding: 22px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--jx3-border-soft);
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, #ffffff, #f5f8fa);
|
||||
border-radius: var(--jx3-radius-lg);
|
||||
background: linear-gradient(180deg, #ffffff, #f6f8f9);
|
||||
box-shadow: var(--jx3-shadow-sm);
|
||||
}
|
||||
|
||||
.help-header {
|
||||
.help-page .help-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
gap: 28px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.top-name {
|
||||
.help-page .top-name {
|
||||
color: var(--jx3-gold-bright);
|
||||
font-size: 32px;
|
||||
font-size: 36px;
|
||||
line-height: 1.3;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.help-page .top-summary {
|
||||
margin-top: 4px;
|
||||
color: var(--jx3-text-muted);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.help-rules {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 300px;
|
||||
color: var(--jx3-text-secondary);
|
||||
font-size: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.help-rules strong {
|
||||
color: var(--jx3-gold-bright);
|
||||
}
|
||||
|
||||
.command-directory {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--jx3-border-soft);
|
||||
border-radius: var(--jx3-radius-md);
|
||||
background: var(--jx3-surface);
|
||||
box-shadow: var(--jx3-shadow-sm);
|
||||
}
|
||||
|
||||
.command-columns {
|
||||
display: grid;
|
||||
grid-template-columns: 210px minmax(0, 1fr) 360px;
|
||||
border-bottom: 1px solid var(--jx3-border-soft);
|
||||
background: var(--jx3-surface-muted);
|
||||
color: var(--jx3-text-muted);
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.top-summary {
|
||||
margin-top: 5px;
|
||||
font-size: 18px;
|
||||
.command-columns span {
|
||||
padding: 10px 18px;
|
||||
}
|
||||
|
||||
.top-desc {
|
||||
font-size: 18px;
|
||||
line-height: 1.75;
|
||||
text-align: right;
|
||||
.command-columns span + span {
|
||||
border-left: 1px solid var(--jx3-border-faint);
|
||||
}
|
||||
|
||||
.section-grid {
|
||||
.command-section + .command-section {
|
||||
border-top: 2px solid var(--jx3-border-soft);
|
||||
}
|
||||
|
||||
.command-section__header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
grid-template-columns: 54px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-height: 66px;
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(90deg, #f7efdf, #f5f8fa 76%);
|
||||
}
|
||||
|
||||
.section {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section.span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.section.span-3,
|
||||
.full-row {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 11px 15px;
|
||||
border-bottom: 1px solid rgba(220, 199, 145, 0.3);
|
||||
background: linear-gradient(90deg, rgba(198, 165, 93, 0.24), rgba(105, 138, 194, 0.08));
|
||||
font-size: 22px;
|
||||
.command-section__header h2 {
|
||||
margin: 0;
|
||||
color: var(--jx3-text);
|
||||
font-size: 25px;
|
||||
line-height: 1.35;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.section-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0 18px;
|
||||
padding: 7px 13px 10px;
|
||||
}
|
||||
|
||||
.span-2 .section-body {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
gap: 9px;
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px dashed rgba(186, 205, 239, 0.15);
|
||||
}
|
||||
|
||||
.feature-name {
|
||||
padding-top: 3px;
|
||||
.section-index {
|
||||
display: inline-flex;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--jx3-border-soft);
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
color: var(--jx3-gold-bright);
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
padding: 3px 10px;
|
||||
border: 1px solid var(--jx3-border-faint);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
color: var(--jx3-text-muted);
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.commands {
|
||||
.command-list {
|
||||
border-top: 1px solid var(--jx3-border-faint);
|
||||
}
|
||||
|
||||
.command-row {
|
||||
display: grid;
|
||||
grid-template-columns: 210px minmax(0, 1fr) 360px;
|
||||
align-items: center;
|
||||
min-height: 64px;
|
||||
border-bottom: 1px solid var(--jx3-border-faint);
|
||||
}
|
||||
|
||||
.command-row::after {
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 10px 18px;
|
||||
border-left: 1px solid var(--jx3-border-faint);
|
||||
background: rgba(246, 249, 250, 0.62);
|
||||
color: var(--jx3-text-secondary);
|
||||
content: attr(data-description);
|
||||
font-size: 19px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.command-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.command-row:nth-child(even) {
|
||||
background: var(--jx3-surface-strong);
|
||||
}
|
||||
|
||||
.help-page .feature-name {
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 18px 10px 22px;
|
||||
border-right: 1px solid var(--jx3-border-faint);
|
||||
color: var(--jx3-text);
|
||||
font-size: 21px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.help-page .commands {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
padding: 9px 16px;
|
||||
}
|
||||
|
||||
.command {
|
||||
display: inline-block;
|
||||
padding: 3px 7px;
|
||||
border: 1px solid rgba(157, 184, 228, 0.22);
|
||||
border-radius: 5px;
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
.help-page .command {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
padding: 3px 10px;
|
||||
border: 1px solid #cbd9e3;
|
||||
border-radius: var(--jx3-radius-sm);
|
||||
background: #edf4f8;
|
||||
color: var(--jx3-text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: 20px;
|
||||
line-height: 1.45;
|
||||
font-weight: 600;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ranking-section .section-body {
|
||||
display: block;
|
||||
.command-section--success .command-section__header {
|
||||
background: linear-gradient(90deg, #e6f3eb, #f5f8f6 76%);
|
||||
}
|
||||
|
||||
.ranking-feature {
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
.command-section--success .section-index {
|
||||
border-color: rgba(32, 117, 77, 0.35);
|
||||
color: var(--jx3-green);
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
.command-section--danger .command-section__header {
|
||||
background: linear-gradient(90deg, #f9e7e4, #faf7f5 76%);
|
||||
}
|
||||
|
||||
.ranking-list .command {
|
||||
width: 100%;
|
||||
.command-section--danger .section-index {
|
||||
border-color: rgba(178, 56, 50, 0.35);
|
||||
color: var(--jx3-red);
|
||||
}
|
||||
|
||||
.trial-feature {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.push-section .section-body {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.section--danger {
|
||||
border-color: rgba(213, 106, 99, 0.54) !important;
|
||||
}
|
||||
|
||||
.section--danger .section-title {
|
||||
color: #872d29;
|
||||
background: linear-gradient(90deg, #f9dfdc, #f9f3ee);
|
||||
}
|
||||
|
||||
.section--success {
|
||||
border-color: rgba(98, 188, 146, 0.52) !important;
|
||||
}
|
||||
|
||||
.section--success .section-title {
|
||||
color: #176541;
|
||||
background: linear-gradient(90deg, #dff2e8, #f3f7f4);
|
||||
}
|
||||
|
||||
.help-footer {
|
||||
.help-page .help-footer {
|
||||
margin-top: 17px;
|
||||
padding: 13px 16px 2px;
|
||||
border-top: 1px solid rgba(216, 195, 140, 0.24);
|
||||
font-size: 18px;
|
||||
line-height: 1.8;
|
||||
border-top: 1px solid var(--jx3-border-soft);
|
||||
color: var(--jx3-text-muted);
|
||||
font-size: 20px;
|
||||
line-height: 1.7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.help-footer strong {
|
||||
color: var(--jx3-gold-bright);
|
||||
}
|
||||
|
||||
@@ -118,5 +118,5 @@
|
||||
}
|
||||
|
||||
.jx3-template--helps {
|
||||
--jx3-page-width: 1520px;
|
||||
--jx3-page-width: 1480px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user