This commit is contained in:
2026-06-24 13:01:52 +08:00
parent f0e535c8ca
commit d8ceb93ea7
9 changed files with 996 additions and 426 deletions
+11 -1
View File
@@ -1,5 +1,15 @@
## 更新记录 ## 更新记录
### version: 2.6
新增 **资历 角色名称 [服务器]** 指令,支持两轮对话选择资历总览或分类总览,并以图片形式展示角色资历点数完成进度。
新增资历菜单与点数基础数据缓存,减少长期稳定接口的重复请求。
调整功能帮助页分类,指令分组改为日常、官方、角色、职业、休闲、阵营、副本、活动、交易、杂项、排行、推送和避雷。
调整指令触发词:**诛恶事件** 改为 **诛恶****排行** 改为 **名剑排行**
### version: 2.5 ### version: 2.5
增加部分新指令,调整功能页面 增加部分新指令,调整功能页面
@@ -58,4 +68,4 @@
### version: 1.0.0 ### version: 1.0.0
大部分重要功能已经实现 大部分重要功能已经实现
+38 -13
View File
@@ -2,24 +2,27 @@
基于 [AstrBot](https://docs.astrbot.app/) 框架开发的剑网三游戏数据查询插件。插件主要通过 JX3API、JX3BOX、茶馆等外部接口获取游戏数据,整理为文本消息、富媒体消息,或通过 `HTML + Jinja2` 模板渲染成图片后发送。 基于 [AstrBot](https://docs.astrbot.app/) 框架开发的剑网三游戏数据查询插件。插件主要通过 JX3API、JX3BOX、茶馆等外部接口获取游戏数据,整理为文本消息、富媒体消息,或通过 `HTML + Jinja2` 模板渲染成图片后发送。
当前插件以“查询型功能”为主,覆盖日常、奇遇、名剑、阵营、交易、职业、排行、副本、活动、名片、百战、推送和本地避雷等场景。部分功能需要配置 JX3API Token,少量职业/角色类接口还需要推栏 ticket。 当前插件以“查询型功能”为主,覆盖日常、官方、角色、职业、休闲、阵营、副本、活动、交易、杂项、排行、推送和本地避雷等场景。部分功能需要配置 JX3API Token,少量职业/角色类接口还需要推栏 ticket。
## 功能概览 ## 功能概览
插件内置 `功能` 指令,会以图片形式展示完整指令列表、参数说明和是否需要令牌。当前帮助页按 16 个功能分组维护,约 87 条指令。 插件内置 `功能` 指令,会以图片形式展示完整指令列表、参数说明和是否需要令牌。当前帮助页按 13 个功能分组维护,约 88 条指令。
主要功能包括: 主要功能包括:
- 日常与官方:日常、月历、名望、公告、维护、区服、开服、状态、技改 - 日常功能:日常、月历、名望、诛恶
- 职业与角色:角色、阵眼、奇穴、技能、小药、宏、配装、资历排行、试炼排行 - 官方功能:公告、维护、技改、区服、开服、状态
- 奇遇:角色奇遇、未做奇遇、奇遇统计、近期奇遇、奇遇汇总、奇遇攻略 - 角色功能:角色、资历、名片、所有名片、奇遇、未做奇遇、战绩、精耐
- 名剑:战绩、名剑排行、名剑统计 - 职业功能:小药、宏、配装、阵眼、奇穴、技能
- 阵营:沙盘、阵营拍卖、阵营事件、关隘首领、帮战记录、统战 YY - 休闲功能:烟花、拜师、收徒、随机名片、攻略、奇遇统计、近期奇遇、奇遇汇总
- 交易:金价、外观物价、交易行、贴吧物价 - 阵营功能:沙盘、阵营拍卖、阵营事件、关隘首领、帮战记录、统战 YY
- 活动:扶摇、诛恶事件、本日赤兔、本周赤兔、刷马、的卢 - 副本功能:百战、招募、掉落、解密
- 其他:拜师、收徒、名片、百战、精耐、招募、掉落、解密、八卦、骗子查询 - 活动功能:扶摇、本日赤兔、本周赤兔、刷马、的卢
- 本地功能:避雷添加、查看、查询、修改、删除 - 交易功能:金价、物价、交易行、贴吧物价
- 杂项功能:科举、花价、装饰、器物、八卦、骗子、骚话。
- 排行功能:名剑统计、名剑排行、资历排行、试炼排行和各类风云录榜单。
- 推送功能:开服推送、新闻推送、刷马推送、赤兔推送。 - 推送功能:开服推送、新闻推送、刷马推送、赤兔推送。
- 避雷功能:避雷查看、添加、删除、修改、查询。
## 安装与依赖 ## 安装与依赖
@@ -43,7 +46,7 @@ pip install -r requirements.txt
- `apscheduler`:后台推送任务调度。 - `apscheduler`:后台推送任务调度。
- `matplotlib`:部分图像/数据展示依赖。 - `matplotlib`:部分图像/数据展示依赖。
插件元信息在 `metadata.yaml` 中维护,要求 AstrBot 版本 `>=4.11.0` 插件元信息在 `metadata.yaml` 中维护,当前版本为 `v2.6`要求 AstrBot 版本 `>=4.11.0`
## 插件配置 ## 插件配置
@@ -166,9 +169,12 @@ pip install -r requirements.txt
```text ```text
日常 日常
诛恶
资历 飞翔大野猪
奇遇 飞翔大野猪 奇遇 飞翔大野猪
未做奇遇 飞翔大野猪 梦江南 未做奇遇 飞翔大野猪 梦江南
战绩 飞翔大野猪 梦江南 33 战绩 飞翔大野猪 梦江南 33
名剑排行 50 33
贴吧物价 狐金 5 梦江南 贴吧物价 狐金 5 梦江南
统战 梦江南 统战 梦江南
``` ```
@@ -234,6 +240,7 @@ astrbot_plugin_jx3/
- `image_msg()`:直接发送图片 URL 或图片数据。 - `image_msg()`:直接发送图片 URL 或图片数据。
- `plain_chain()`:发送文本 + 图片等富媒体消息链。 - `plain_chain()`:发送文本 + 图片等富媒体消息链。
- `handler_plain_image_msg()`:先发文本,再发图片,适用于部分组合型功能。 - `handler_plain_image_msg()`:先发文本,再发图片,适用于部分组合型功能。
- `handler_zili_msg()`:资历查询专用两轮会话,先发送 `0-18` 分类菜单,再按用户选择渲染资历进度图片。
所有可选服务器参数都会通过 `serverdefault()` 补齐默认服务器。 所有可选服务器参数都会通过 `serverdefault()` 补齐默认服务器。
@@ -332,10 +339,28 @@ html_renderer.render_custom_template()
- 避雷记录。 - 避雷记录。
- 推送任务状态缓存。 - 推送任务状态缓存。
- 资历菜单与资历点数基础数据缓存,默认缓存 30 天,接口失败时可使用旧缓存兜底。
`core/sqlite.py` 封装了异步增删改查;`core/bilei_data.py` 基于该封装实现避雷数据管理。 `core/sqlite.py` 封装了异步增删改查;`core/bilei_data.py` 基于该封装实现避雷数据管理。
### 8. 后台推送 ### 8. 资历查询
`资历 角色名称 [服务器]` 是 2.6 新增的两轮对话功能。
第一轮会发送固定分类菜单,用户输入 `0-18` 后进入第二轮:
- `0`:展示角色资历总览,并列出 18 个大类的完成进度。
- `1-18`:展示对应大类总览,并列出该大类下所有子类的完成进度。
实现流程:
1. 通过角色信息接口 `jx3_jueshexinxi` 获取角色 `globalId`
2.`globalId` 请求 JX3BOX 角色资历接口,获取已完成资历 ID 列表。
3. 从资历菜单接口和资历点数接口读取基础数据,并缓存到本地 SQLite。
4. 展开菜单中单个 ID 和数组 ID,按资历点数计算 `已完成点数 / 总点数` 与百分比。
5. 使用 `templates/zili.html` 渲染进度条图片。
### 9. 后台推送
`core/async_task.py` 使用 APScheduler 实现轮询推送。 `core/async_task.py` 使用 APScheduler 实现轮询推送。
+266 -3
View File
@@ -1,5 +1,5 @@
import json import json
from datetime import datetime from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List, Union from typing import Dict, Any, Optional, List, Union
from collections import defaultdict from collections import defaultdict
@@ -34,8 +34,32 @@ TONG_RANK_NAMES = {
"本周浩气五十强", "本周浩气五十强",
} }
ACHIEVEMENT_CHOICES = [
(0, None, "资历总览"),
(1, "1", "杂闻总览"),
(2, "2", "武学总览"),
(3, "3", "修为总览"),
(4, "4", "装备总览"),
(5, "5", "技艺总览"),
(6, "6", "阅读总览"),
(7, "7", "任务总览"),
(8, "8", "足迹总览"),
(9, "9", "战斗总览"),
(10, "10", "声望总览"),
(11, "11", "秘境总览"),
(12, "12", "帮会总览"),
(13, "13", "阵营总览"),
(14, "15", "节日总览"),
(15, "16", "活动总览"),
(16, "17", "风雨江湖路总览"),
(17, "40", "家园总览"),
(18, "41", "剑侠录总览"),
]
ACHIEVEMENT_CHOICE_MAP = {index: (menu_id, title) for index, menu_id, title in ACHIEVEMENT_CHOICES}
class JX3Service: class JX3Service:
def __init__(self, api_data_path: str, config: AstrBotConfig, sqlite: AsyncSQLiteDB): def __init__(self, api_data_path: str, config: AstrBotConfig, sqlite: AsyncSQLiteDB, cache_sqlite: Optional[AsyncSQLiteDB] = None):
# 加载API配置文件 # 加载API配置文件
try: try:
with open(api_data_path, 'r', encoding='utf-8') as f: with open(api_data_path, 'r', encoding='utf-8') as f:
@@ -49,6 +73,7 @@ class JX3Service:
self._config = config self._config = config
# 引用sqlite # 引用sqlite
self._sql_db = sqlite self._sql_db = sqlite
self._cache_db = cache_sqlite or sqlite
# 获取配置中的 Token # 获取配置中的 Token
self.token = self._config.get("jx3api_token", "") self.token = self._config.get("jx3api_token", "")
@@ -2386,6 +2411,244 @@ class JX3Service:
return return_data return return_data
async def _load_achievement_cache(self, key: str) -> tuple[Optional[Dict[str, Any]], bool]:
"""读取资历基础数据缓存,返回数据和是否已过期"""
try:
row = await self._cache_db.select_one("achievement_cache", "key=?", (key,))
except Exception as e:
logger.error(f"读取资历缓存失败: {e}")
return None, True
if not row:
return None, True
try:
payload = json.loads(row.get("content", "{}"))
updated_at = datetime.strptime(row.get("updated_at", ""), "%Y-%m-%d %H:%M:%S")
expired = datetime.now() - updated_at > timedelta(days=30)
return payload, expired
except Exception as e:
logger.error(f"解析资历缓存失败: {e}")
return None, True
async def _save_achievement_cache(self, key: str, payload: Dict[str, Any]):
"""写入资历基础数据缓存"""
try:
await self._cache_db.execute(
"""
INSERT INTO achievement_cache (key, content, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
content=excluded.content,
updated_at=excluded.updated_at
""",
(
key,
json.dumps(payload, ensure_ascii=False),
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
),
)
except Exception as e:
logger.error(f"写入资历缓存失败: {e}")
async def _get_achievement_base_data(self, cache_key: str, config_key: str) -> Optional[Dict[str, Any]]:
"""获取资历菜单或点数数据,优先使用未过期缓存"""
cached, expired = await self._load_achievement_cache(cache_key)
if cached and not expired:
return cached
data: Optional[Dict[str, Any]] = await self._base_request(config_key, "GET")
if data and isinstance(data, dict):
await self._save_achievement_cache(cache_key, data)
return data
if cached:
logger.warning(f"资历基础数据接口失败,使用旧缓存: {cache_key}")
return cached
return None
def _flatten_achievement_ids(self, values: Any) -> list[int]:
"""展开菜单中的单个资历 ID 和数组资历 ID"""
result = []
if isinstance(values, list):
for item in values:
result.extend(self._flatten_achievement_ids(item))
return result
try:
result.append(int(values))
except (TypeError, ValueError):
pass
return result
def _category_achievement_ids(self, category: Dict[str, Any]) -> list[int]:
"""提取分类及其子分类包含的资历 ID"""
ids = self._flatten_achievement_ids(category.get("achievements", []))
for child in category.get("children", []) or []:
if isinstance(child, dict):
ids.extend(self._flatten_achievement_ids(child.get("achievements", [])))
return ids
def _build_achievement_progress(
self,
name: str,
achievement_ids: list[int],
point_map: Dict[str, Any],
completed_ids: set[int],
) -> Dict[str, Any]:
"""按资历点数计算完成进度"""
unique_ids = set(achievement_ids)
total_points = 0
completed_points = 0
completed_count = 0
for achievement_id in unique_ids:
try:
point = int(point_map.get(str(achievement_id), 0) or 0)
except (TypeError, ValueError):
point = 0
total_points += point
if achievement_id in completed_ids:
completed_count += 1
completed_points += point
percent = round(completed_points / total_points * 100, 2) if total_points else 0
return {
"name": name,
"total_points": total_points,
"completed_points": completed_points,
"percent": percent,
"percent_text": f"{percent:.2f}%",
"achievement_count": len(unique_ids),
"completed_count": completed_count,
}
async def zili(self, name: str, server: str, choice: int) -> Dict[str, Any]:
"""角色资历"""
return_data = self._init_return_data()
if choice not in ACHIEVEMENT_CHOICE_MAP:
return_data["msg"] = "无效序号,结束会话"
return return_data
role_params = {"server": server, "name": name, "token": self.token}
role_data: Optional[Dict[str, Any]] = await self._base_request(
"jx3_jueshexinxi", "GET", params=role_params
)
if not role_data or not isinstance(role_data, dict):
return_data["msg"] = "未查询到角色"
return return_data
global_id = role_data.get("globalId")
if not global_id:
return_data["msg"] = "无法获取角色全区 ID"
return return_data
achievement_data = await self._api.get(
"https://next2.jx3box.com/api/next2/user-achievements",
params={"jx3id": global_id},
out_key="data",
)
if not achievement_data or not isinstance(achievement_data, dict):
return_data["msg"] = "未查询到资历数据"
return return_data
completed_ids = set()
achievements = achievement_data.get("achievements") or ""
if isinstance(achievements, str):
for item in achievements.split(","):
try:
completed_ids.add(int(item.strip()))
except ValueError:
continue
elif isinstance(achievements, list):
completed_ids = set(self._flatten_achievement_ids(achievements))
menu_payload = await self._get_achievement_base_data(
"achievement_menus",
"jx3box_achievement_menus",
)
point_payload = await self._get_achievement_base_data(
"achievement_points",
"jx3box_achievement_points",
)
if not menu_payload or not point_payload:
return_data["msg"] = "基础资历数据获取失败"
return return_data
menus = menu_payload.get("menus", {})
points = point_payload.get("points", {})
if not isinstance(menus, dict) or not isinstance(points, dict):
return_data["msg"] = "基础资历数据格式异常"
return return_data
selected_menu_id, selected_title = ACHIEVEMENT_CHOICE_MAP[choice]
items = []
if choice == 0:
total_ids = []
for _, menu_id, title in ACHIEVEMENT_CHOICES[1:]:
category = menus.get(str(menu_id), {})
if not isinstance(category, dict):
continue
category_ids = self._category_achievement_ids(category)
total_ids.extend(category_ids)
items.append(
self._build_achievement_progress(title, category_ids, points, completed_ids)
)
summary = self._build_achievement_progress(selected_title, total_ids, points, completed_ids)
else:
category = menus.get(str(selected_menu_id), {})
if not isinstance(category, dict) or not category:
return_data["msg"] = "未找到该资历分类"
return return_data
category_ids = self._category_achievement_ids(category)
summary = self._build_achievement_progress(selected_title, category_ids, points, completed_ids)
for child in category.get("children", []) or []:
if not isinstance(child, dict):
continue
child_ids = self._flatten_achievement_ids(child.get("achievements", []))
items.append(
self._build_achievement_progress(child.get("name", "未命名"), child_ids, points, completed_ids)
)
try:
return_data["temp"] = await load_template("zili.html")
except FileNotFoundError as e:
logger.error(f"加载模板失败: {e}")
return_data["msg"] = "系统错误:模板文件不存在"
return return_data
return_data["data"] = {
"title": selected_title,
"summary": summary,
"items": items,
"role_name": role_data.get("roleName", name),
"server": role_data.get("serverName", server),
"zone": role_data.get("zoneName", ""),
"force_name": role_data.get("forceName", ""),
"camp_name": role_data.get("campName", ""),
"tong_name": role_data.get("tongName", ""),
"updated_at": achievement_data.get("updated_at", ""),
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
return_data["code"] = 200
return return_data
async def jueshe(self,name: str, server: str) -> Dict[str, Any]: async def jueshe(self,name: str, server: str) -> Dict[str, Any]:
"""角色""" """角色"""
return_data = self._init_return_data() return_data = self._init_return_data()
@@ -2408,7 +2671,7 @@ class JX3Service:
f"服务器:{data.get('zoneName', '')}·{data.get('serverName', '')}\n" f"服务器:{data.get('zoneName', '')}·{data.get('serverName', '')}\n"
f"名称:{data.get('roleName', '')}\n" f"名称:{data.get('roleName', '')}\n"
f"角色ID{data.get('roleId', '')}\n" f"角色ID{data.get('roleId', '')}\n"
f"全区ID{data.get('globalRoleId', '')}\n" f"全区ID{data.get('globalId', '')}\n"
f"职业:{data.get('forceName', '')}·{data.get('bodyName', '')}\n" f"职业:{data.get('forceName', '')}·{data.get('bodyName', '')}\n"
f"帮会:{data.get('tongName', '')}\n" f"帮会:{data.get('tongName', '')}\n"
f"阵营:{data.get('campName', '')}\n" f"阵营:{data.get('campName', '')}\n"
+86
View File
@@ -13,6 +13,29 @@ from .bilei_data import BiLeidata
class MessageBuilder: class MessageBuilder:
"""回复消息构建""" """回复消息构建"""
ZILI_MENU_TEXT = (
"请选择资历查询类型:\n"
"0:资历总览\n"
"1:杂闻总览\n"
"2:武学总览\n"
"3:修为总览\n"
"4:装备总览\n"
"5:技艺总览\n"
"6:阅读总览\n"
"7:任务总览\n"
"8:足迹总览\n"
"9:战斗总览\n"
"10:声望总览\n"
"11:秘境总览\n"
"12:帮会总览\n"
"13:阵营总览\n"
"14:节日总览\n"
"15:活动总览\n"
"16:风雨江湖路总览\n"
"17:家园总览\n"
"18:剑侠录总览"
)
def __init__(self, server: str, jx3api: JX3Service, bilei: BiLeidata, jx3at: AsyncTask, icons: dict[str, dict[str, str]]): def __init__(self, server: str, jx3api: JX3Service, bilei: BiLeidata, jx3at: AsyncTask, icons: dict[str, dict[str, str]]):
self.server = server self.server = server
self.jx3api = jx3api self.jx3api = jx3api
@@ -187,6 +210,64 @@ class MessageBuilder:
await event.send(event.plain_result("猪脑过载,请稍后再试")) await event.send(event.plain_result("猪脑过载,请稍后再试"))
async def handler_zili_msg(self, event: AstrMessageEvent, name: str, server: str):
"""资历查询专用两轮会话,第一轮文本菜单,第二轮图片"""
try:
await event.send(event.plain_result(self.ZILI_MENU_TEXT))
user_id = event.get_sender_id()
@session_waiter(timeout=30)
async def zili_select_waiter(controller: SessionController, new_event: AstrMessageEvent):
if new_event.get_sender_id() != user_id:
return
msg = new_event.get_message_str().strip()
if not msg.isdigit():
await new_event.send(MessageChain().message("输入异常,结束会话"))
controller.stop()
return
choice = int(msg)
if choice < 0 or choice > 18:
await new_event.send(MessageChain().message("无效序号,结束会话"))
controller.stop()
return
try:
data = await self.jx3api.zili(name, server, choice)
if data["code"] != 200:
await new_event.send(MessageChain().message(data.get("msg", "获取资历数据失败")))
controller.stop()
return
options = {
"quality": 100,
"device_scale_factor_level": "normal",
"full_page": True,
"omit_background": False,
"type": "jpeg"
}
data["data"]["icons"] = self.icons
url = await self.html_render(data["temp"], data["data"], options=options)
await new_event.send(new_event.image_result(url))
except Exception as e:
logger.error(f"资历查询执行错误: {e}")
await new_event.send(MessageChain().message("猪脑过载,请稍后再试"))
controller.stop()
try:
await zili_select_waiter(event)
except TimeoutError:
await event.send(event.plain_result("选择超时,已结束会话"))
except Exception:
logger.error("资历选择发生异常", exc_info=True)
except Exception as e:
logger.error(f"资历会话执行错误: {e}")
await event.send(event.plain_result("猪脑过载,请稍后再试"))
async def jx3_helps(self, event: AstrMessageEvent): async def jx3_helps(self, event: AstrMessageEvent):
"""剑三 功能""" """剑三 功能"""
return await self.T2I_image_msg(event, self.jx3api.helps) return await self.T2I_image_msg(event, self.jx3api.helps)
@@ -292,6 +373,11 @@ class MessageBuilder:
return await self.plain_msg(event, self.jx3api.shaohua) return await self.plain_msg(event, self.jx3api.shaohua)
async def jx3_zili(self, event: AstrMessageEvent, name: str, server: str = ""):
"""剑三 资历 角色名称 服务器"""
return await self.handler_zili_msg(event, name, self.serverdefault(server))
async def jx3_jiemi(self, event: AstrMessageEvent): async def jx3_jiemi(self, event: AstrMessageEvent):
"""剑三 解密""" """剑三 解密"""
return await self.plain_msg(event, self.jx3api.jiemi) return await self.plain_msg(event, self.jx3api.jiemi)
+17
View File
@@ -591,6 +591,23 @@
"mount": "", "mount": "",
"star": "1" "star": "1"
} }
},
"jx3box_achievement_menus":{
"url":"https://node.jx3box.com/api/node/achievement/menus",
"method":"GET",
"description":"魔盒资历菜单列表",
"params":{
"general": "1",
"client": "std"
}
},
"jx3box_achievement_points":{
"url":"https://node.jx3box.com/api/node/achievement/points",
"method":"GET",
"description":"魔盒资历点数列表",
"params":{
"client": "std"
}
} }
+17 -4
View File
@@ -17,7 +17,7 @@ from .core.fun_basic import load_as_base64
@register("astrbot_plugin_jx3", @register("astrbot_plugin_jx3",
"fxdyz", "fxdyz",
"通过调用剑网三API接口获取游戏数据,处理发送。", "通过调用剑网三API接口获取游戏数据,处理发送。",
"2.5", "2.6",
"https://github.com/qsc20001102/astrbot_plugin_jx3api" "https://github.com/qsc20001102/astrbot_plugin_jx3api"
) )
class Jx3ApiPlugin(Star): class Jx3ApiPlugin(Star):
@@ -57,6 +57,7 @@ class Jx3ApiPlugin(Star):
# 数据库初始化 # 数据库初始化
await self.init_bilei_data() await self.init_bilei_data()
await self.init_tuishong_data() await self.init_tuishong_data()
await self.init_achievement_cache_data()
# 连接插件数据 # 连接插件数据
await self.plugin_sql_db.connect() await self.plugin_sql_db.connect()
@@ -142,7 +143,7 @@ class Jx3ApiPlugin(Star):
self.plugin_sql_db = AsyncSQLiteDB(str(self.plugin_data_path)) self.plugin_sql_db = AsyncSQLiteDB(str(self.plugin_data_path))
# 剑网三功能实例化 # 剑网三功能实例化
self.bilei = BiLeidata(self.local_sql_db) self.bilei = BiLeidata(self.local_sql_db)
self.jx3api = JX3Service(str(self.api_data_path), self.conf, self.plugin_sql_db) self.jx3api = JX3Service(str(self.api_data_path), self.conf, self.plugin_sql_db, self.local_sql_db)
self.jx3at = AsyncTask(cast(Context, self.context), self.conf, self.jx3api, self.local_sql_db) self.jx3at = AsyncTask(cast(Context, self.context), self.conf, self.jx3api, self.local_sql_db)
self.jx3cmd = MessageBuilder(self.server, self.jx3api, self.bilei, self.jx3at, self.icons) self.jx3cmd = MessageBuilder(self.server, self.jx3api, self.bilei, self.jx3at, self.icons)
self.jx3cmd = MessageBuilder(self.server, self.jx3api, self.bilei, self.jx3at, self.icons) self.jx3cmd = MessageBuilder(self.server, self.jx3api, self.bilei, self.jx3at, self.icons)
@@ -182,6 +183,17 @@ class Jx3ApiPlugin(Star):
""") """)
async def init_achievement_cache_data(self):
"""初始化资历基础数据缓存表"""
await self.local_sql_db.execute("""
CREATE TABLE IF NOT EXISTS achievement_cache(
key TEXT PRIMARY KEY,
content TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
def ini_command_map(self): def ini_command_map(self):
"""初始化指令集""" """初始化指令集"""
self.command_map = { self.command_map = {
@@ -206,6 +218,7 @@ class Jx3ApiPlugin(Star):
"资历排行": self.jx3cmd.jx3_zilipaixing, "资历排行": self.jx3cmd.jx3_zilipaixing,
"试炼排行": self.jx3cmd.jx3_shilianpaixing, "试炼排行": self.jx3cmd.jx3_shilianpaixing,
"骚话": self.jx3cmd.jx3_shaohua, "骚话": self.jx3cmd.jx3_shaohua,
"资历": self.jx3cmd.jx3_zili,
"解密": self.jx3cmd.jx3_jiemi, "解密": self.jx3cmd.jx3_jiemi,
"沙盘": self.jx3cmd.jx3_shapan, "沙盘": self.jx3cmd.jx3_shapan,
"攻略": self.jx3cmd.jx3_qiyugonglue, "攻略": self.jx3cmd.jx3_qiyugonglue,
@@ -213,7 +226,7 @@ class Jx3ApiPlugin(Star):
"配装": self.jx3cmd.jx3_peizhuang, "配装": self.jx3cmd.jx3_peizhuang,
"百战": self.jx3cmd.jx3_baizhan, "百战": self.jx3cmd.jx3_baizhan,
"扶摇": self.jx3cmd.jx3_fuyaojjiutian, "扶摇": self.jx3cmd.jx3_fuyaojjiutian,
"诛恶事件": self.jx3cmd.jx3_zhueevent, "诛恶": self.jx3cmd.jx3_zhueevent,
"本日赤兔": self.jx3cmd.jx3_benrichitu, "本日赤兔": self.jx3cmd.jx3_benrichitu,
"本周赤兔": self.jx3cmd.jx3_benzhouchitu, "本周赤兔": self.jx3cmd.jx3_benzhouchitu,
"拍卖": self.jx3cmd.jx3_zhengyingpaimai, "拍卖": self.jx3cmd.jx3_zhengyingpaimai,
@@ -230,7 +243,7 @@ class Jx3ApiPlugin(Star):
"近期奇遇": self.jx3cmd.jx3_jinqiqiyu, "近期奇遇": self.jx3cmd.jx3_jinqiqiyu,
"奇遇汇总": self.jx3cmd.jx3_qiyuhuizong, "奇遇汇总": self.jx3cmd.jx3_qiyuhuizong,
"战绩": self.jx3cmd.jx3_zhanji, "战绩": self.jx3cmd.jx3_zhanji,
"排行": self.jx3cmd.jx3_mingjianpaihang, "名剑排行": self.jx3cmd.jx3_mingjianpaihang,
"名剑统计": self.jx3cmd.jx3_mingjiantongji, "名剑统计": self.jx3cmd.jx3_mingjiantongji,
"招募": self.jx3cmd.jx3_tuanduizhaomu, "招募": self.jx3cmd.jx3_tuanduizhaomu,
"拜师": self.jx3cmd.jx3_baishi, "拜师": self.jx3cmd.jx3_baishi,
+1 -1
View File
@@ -1,7 +1,7 @@
name: astrbot_plugin_jx3 name: astrbot_plugin_jx3
display_name: 剑网三游戏数据查询工具 display_name: 剑网三游戏数据查询工具
desc: 通过接口调用剑网三API接口获取游戏数据,处理发送。 desc: 通过接口调用剑网三API接口获取游戏数据,处理发送。
version: v2.5 version: v2.6
author: 飞翔大野猪 author: 飞翔大野猪
repo: https://github.com/qsc20001102/astrbot_plugin_jx3 repo: https://github.com/qsc20001102/astrbot_plugin_jx3
astrbot_version: ">=4.11.0" astrbot_version: ">=4.11.0"
+354 -404
View File
File diff suppressed because it is too large Load Diff
+206
View File
@@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>资历查询</title>
<style>
body {
font-family: "Microsoft YaHei", Arial, sans-serif;
background: #f1f2f6;
margin: 0;
padding: 22px;
color: #262b36;
}
.container {
width: 980px;
margin: 0 auto;
}
.header {
background: #ffffff;
border-radius: 14px;
padding: 22px 26px;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
margin-bottom: 16px;
}
.title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
h1 {
margin: 0;
font-size: 32px;
line-height: 1.2;
color: #222;
font-weight: 800;
}
.role {
display: flex;
align-items: center;
gap: 10px;
color: #565d70;
font-size: 16px;
white-space: nowrap;
}
.force-icon {
width: 34px;
height: 34px;
border-radius: 50%;
object-fit: contain;
}
.meta {
margin-top: 12px;
color: #7a7f99;
font-size: 15px;
line-height: 1.7;
}
.summary {
background: #ffffff;
border-radius: 14px;
padding: 22px 26px;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
margin-bottom: 16px;
}
.summary-top,
.item-top {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 14px;
}
.summary-name {
font-size: 24px;
font-weight: 800;
color: #222;
}
.summary-percent {
font-size: 34px;
line-height: 1;
font-weight: 900;
color: #d93939;
font-variant-numeric: tabular-nums;
}
.score {
margin-top: 8px;
color: #656d80;
font-size: 15px;
font-variant-numeric: tabular-nums;
}
.progress {
height: 14px;
margin-top: 14px;
border-radius: 999px;
overflow: hidden;
background: #e7e9ef;
}
.progress-fill {
height: 100%;
min-width: 3px;
border-radius: 999px;
background: linear-gradient(90deg, #d93939, #f59e42);
}
.list {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.item {
background: #ffffff;
border-radius: 12px;
padding: 15px 16px;
box-shadow: 0 3px 12px rgba(0,0,0,0.06);
}
.item-name {
min-width: 0;
font-size: 17px;
font-weight: 800;
color: #303543;
word-break: break-word;
}
.item-percent {
color: #d93939;
font-size: 18px;
font-weight: 900;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.item .score {
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="title-row">
<h1>{{ title }}</h1>
<div class="role">
{% if icons.sect.get(force_name) %}
<img class="force-icon" src="{{ icons.sect[force_name] }}" alt="{{ force_name }}">
{% endif %}
<span>{{ role_name }}</span>
</div>
</div>
<div class="meta">
{{ zone }} · {{ server }} · {{ force_name }} · {{ camp_name }} · 帮会:{{ tong_name }}<br>
资历更新时间:{{ updated_at or "未知" }} · 查询时间:{{ update_time }}
</div>
</div>
<div class="summary">
<div class="summary-top">
<div class="summary-name">{{ summary.name }}</div>
<div class="summary-percent">{{ summary.percent_text }}</div>
</div>
<div class="score">
{{ summary.completed_points }} / {{ summary.total_points }} 点
· {{ summary.completed_count }} / {{ summary.achievement_count }} 项
</div>
<div class="progress">
<div class="progress-fill" style="width: {{ summary.percent }}%;"></div>
</div>
</div>
<div class="list">
{% for item in items %}
<div class="item">
<div class="item-top">
<div class="item-name">{{ item.name }}</div>
<div class="item-percent">{{ item.percent_text }}</div>
</div>
<div class="score">
{{ item.completed_points }} / {{ item.total_points }} 点
· {{ item.completed_count }} / {{ item.achievement_count }} 项
</div>
<div class="progress">
<div class="progress-fill" style="width: {{ item.percent }}%;"></div>
</div>
</div>
{% endfor %}
</div>
</div>
</body>
</html>