This commit is contained in:
qsc
2026-09-04 18:29:49 +08:00
parent f872ffae71
commit 3845950862
42 changed files with 808 additions and 1054 deletions
+255 -66
View File
@@ -23,23 +23,24 @@ EVENT_NAMES = {
1001: "奇遇触发",
1002: "马驹刷新",
1003: "马驹捕获",
1004: "扶摇预告",
1005: "扶摇开启",
1006: "扶摇点名",
1008: "的卢每日",
1007: "烟花报时",
1008: "的卢预告",
1009: "的卢刷新",
1010: "的卢捕获",
1011: "的卢拍卖",
1012: "副本掉落 / 私货",
1012: "玄晶报时",
1013: "阵营拍卖",
1014: "诛恶事件",
1015: "追魂点名",
1017: "阵营祭",
1018: "关隘首领",
1016: "阵营祭天预告",
1017: "阵营祭天终点",
1101: "领地宣战·开始",
1102: "领地宣战·结束",
1103: "帮会宣战·开始",
1104: "帮会宣战·结束",
1105: "帮会约战·完胜",
1108: "帮会野外宣战·开始",
1109: "帮会野外宣战·结束",
1111: "抢占粮仓",
1112: "大旗重置",
1113: "大旗被夺",
@@ -52,16 +53,237 @@ EVENT_NAMES = {
1120: "小攻防分红",
1121: "大攻防分红",
1122: "大攻防分红(含指挥)",
1201: "微博更新",
2001: "开服状态",
2002: "官方新闻",
2003: "版本更新",
2004: "八卦速报",
2005: "关隘首领",
2006: "预告",
2006: "预告",
}
EVENT_ACTIONS = tuple(EVENT_NAMES)
SERVER_FIELDS = (
("大区", "zone", "text"),
("服务器", "server", "text"),
)
EVENT_FIELDS = {
1001: SERVER_FIELDS + (
("角色", "name", "text"),
("奇遇", "event", "text"),
("等级", "level", "text"),
("时间", "time", "time"),
),
1002: SERVER_FIELDS + (
("地图", "map_name", "text"),
("刷新时间", "time", "time"),
),
1003: SERVER_FIELDS + (
("名称", "name", "text"),
("地图", "map_name", "text"),
("马驹", "horse", "text"),
("等级", "level", "text"),
("捕获时间", "time", "time"),
),
1004: SERVER_FIELDS + (("预告时间", "time", "time"),),
1005: SERVER_FIELDS + (("开启时间", "time", "time"),),
1006: SERVER_FIELDS + (
("点名角色", "name", "list"),
("时间", "time", "time"),
),
1007: SERVER_FIELDS + (
("燃放者", "sender", "text"),
("接收者", "receiver", "text"),
("烟花", "firework", "text"),
("地图", "map_name", "text"),
("时间", "time", "time"),
),
1008: SERVER_FIELDS + (
("马驹", "name", "text"),
("地图", "map_name", "text"),
("预告时间", "time", "time"),
),
1009: SERVER_FIELDS + (
("马驹", "name", "text"),
("地图", "map_name", "text"),
("刷新时间", "refresh_time", "time"),
),
1010: SERVER_FIELDS + (
("马驹", "name", "text"),
("地图", "map_name", "text"),
("捕获角色", "capture_role_name", "text"),
("角色阵营", "capture_camp_name", "text"),
("捕获时间", "capture_time", "time"),
),
1011: SERVER_FIELDS + (
("马驹", "name", "text"),
("竞拍角色", "auction_role_name", "text"),
("角色阵营", "auction_camp_name", "text"),
("成交金额", "auction_amount", "text"),
("拍卖时间", "auction_time", "time"),
),
1012: SERVER_FIELDS + (
("角色", "role_name", "text"),
("副本", "map_name", "text"),
("物品", "item_name", "text"),
("时间", "time", "time"),
),
1013: SERVER_FIELDS + (
("竞拍角色", "role_name", "text"),
("阵营", "camp_name", "text"),
("物品", "item_name", "text"),
("成交金额", "item_amount", "text"),
("时间", "time", "time"),
),
1014: SERVER_FIELDS + (
("地图", "map_name", "text"),
("时间", "time", "time"),
),
1015: SERVER_FIELDS + (
("角色所在服", "role_server", "text"),
("点名角色", "role_name", "text"),
("时间", "time", "time"),
),
1016: SERVER_FIELDS + (("预告时间", "time", "time"),),
1017: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("帮会", "tong_name", "text"),
("角色", "role_name", "text"),
("据点", "castle_name", "text"),
("时间", "time", "time"),
),
1101: SERVER_FIELDS + (
("战场类型", "battlefield_type", "text"),
("宣战帮会", "declaring_tong_name", "text"),
("应战帮会", "accepting_tong_name", "text"),
("领地帮会", "battlefield_tong_name", "text"),
("开始时间", "start_time", "time"),
),
1102: SERVER_FIELDS + (
("战场类型", "battlefield_type", "text"),
("宣战帮会", "declaring_tong_name", "text"),
("应战帮会", "accepting_tong_name", "text"),
("领地帮会", "battlefield_tong_name", "text"),
("获胜帮会", "victory_tong_name", "text"),
("获胜积分", "victory_score", "text"),
("结束时间", "end_time", "time"),
),
1108: SERVER_FIELDS + (
("战场类型", "battlefield_type", "text"),
("宣战帮会", "declaring_tong_name", "text"),
("应战帮会", "accepting_tong_name", "text"),
("持续时长(小时)", "duration_hours", "text"),
("开始时间", "start_time", "time"),
),
1109: SERVER_FIELDS + (
("战场类型", "battlefield_type", "text"),
("宣战帮会", "declaring_tong_name", "text"),
("应战帮会", "accepting_tong_name", "text"),
("结束时间", "end_time", "time"),
),
1111: SERVER_FIELDS + (
("据点", "castle_name", "text"),
("阵营", "camp_name", "text"),
("时间", "time", "time"),
),
1112: SERVER_FIELDS + (
("据点", "castle_name", "text"),
("时间", "time", "time"),
),
1113: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("地图", "map_name", "text"),
("据点", "castle_name", "text"),
("时间", "time", "time"),
),
1114: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("帮会", "tong_name", "text"),
("据点", "castle_name", "text"),
("时间", "time", "time"),
),
1115: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("据点", "castle_name", "text"),
("时间", "time", "time"),
),
1116: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("贡献帮会", "tong_name", "list"),
("时间", "time", "time"),
),
1117: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("贡献帮会", "tong_name", "list"),
("时间", "time", "time"),
),
1118: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("贡献帮会", "tong_name", "list"),
("时间", "time", "time"),
),
1119: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("竞拍角色", "role_name", "text"),
("物品", "item_name", "text"),
("成交金额", "item_amount", "text"),
("时间", "time", "time"),
),
1120: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("分红帮会", "tong_name", "list"),
("分红金额", "split_amount", "text"),
("时间", "time", "time"),
),
1121: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("分红帮会", "tong_name", "list"),
("分红金额", "split_amount", "text"),
("时间", "time", "time"),
),
1122: SERVER_FIELDS + (
("阵营", "camp_name", "text"),
("指挥帮会", "chief_tong_name", "text"),
("分红帮会", "tong_name", "list"),
("分红金额", "split_amount", "text"),
("时间", "time", "time"),
),
2001: SERVER_FIELDS + (
("状态", "status", "status"),
("时间", "time", "time"),
),
2002: (
("类型", "type", "text"),
("标题", "title", "text"),
("日期", "date", "text"),
("链接", "url", "text"),
),
2003: (
("当前版本", "now_version", "text"),
("最新版本", "new_version", "text"),
("更新包数量", "package_num", "text"),
("更新大小", "package_size", "text"),
),
2004: (
("分类", "tags", "text"),
("服务器", "server", "text"),
("发布者", "name", "text"),
("标题", "title", "text"),
("日期", "date", "text"),
("链接", "url", "text"),
),
2005: (
("服务器", "server", "text"),
("关卡", "stage", "text"),
("开始时间", "start", "time"),
),
2006: (
("事件", "name", "text"),
("地点", "site", "text"),
("说明", "desc", "text"),
("时间", "time", "time"),
),
}
class EventPushService:
"""JX3API WebSocket 事件接收、会话订阅与消息分发。"""
@@ -424,63 +646,30 @@ class EventPushService:
return str(value or "未知")
def _format_event(self, action: int, detail: dict[str, Any]) -> str:
if action == 2001:
raw_status = detail.get("status")
server_status = {0: "维护", 1: "开服", "0": "维护", "1": "开服"}.get(
raw_status,
str(raw_status or "未知"),
)
return (
"【开服状态】\n"
f"大区:{detail.get('zone', '未知')}\n"
f"服务器:{detail.get('server', '未知')}\n"
f"状态:{server_status}\n"
f"时间:{self._format_timestamp(detail.get('time'))}"
)
if action == 2002:
return (
"【官方新闻】\n"
f"类型:{detail.get('type', '未知')}\n"
f"标题:{detail.get('title', '未知')}\n"
f"日期:{detail.get('date', '未知')}\n"
f"链接:{detail.get('url', '')}"
)
if action == 2003:
return (
"【版本更新】\n"
f"当前版本:{detail.get('now_version', '未知')}\n"
f"最新版本:{detail.get('new_version', '未知')}\n"
f"更新包数量:{detail.get('package_num', '未知')}\n"
f"更新大小:{detail.get('package_size', '未知')}"
)
if action == 2004:
return (
"【八卦速报】\n"
f"分类:{detail.get('tags', '未知')}\n"
f"区服:{detail.get('zone', '未知')} / {detail.get('server', '未知')}\n"
f"来源:{detail.get('tieba', '未知')}\n"
f"标题:{detail.get('title', '未知')}\n"
f"日期:{detail.get('date', '未知')}\n"
f"链接:{detail.get('url', '')}"
)
if action == 2005:
return (
"【关隘首领】\n"
f"服务器:{detail.get('server', '未知')}\n"
f"关卡:{detail.get('stage', '未知')}\n"
f"开始时间:{self._format_timestamp(detail.get('start'))}"
)
if action == 2006:
return (
"【云从预告】\n"
f"事件:{detail.get('name', '未知')}\n"
f"地点:{detail.get('site', '未知')}\n"
f"说明:{detail.get('desc', '')}\n"
f"时间:{self._format_timestamp(detail.get('time'))}"
)
fields = EVENT_FIELDS.get(action)
if not fields:
detail_text = json.dumps(detail, ensure_ascii=False, indent=2)
return f"【事件推送 · {action}\n{detail_text}"
detail_text = json.dumps(detail, ensure_ascii=False, indent=2)
return f"【事件推送 · {action} {EVENT_NAMES[action]}\n{detail_text}"
lines = [f"{EVENT_NAMES[action]}"]
for label, key, value_type in fields:
lines.append(f"{label}{self._format_field(detail.get(key), value_type)}")
return "\n".join(lines)
def _format_field(self, value: Any, value_type: str) -> str:
if value_type == "time":
return self._format_timestamp(value)
if value_type == "status":
if value in (0, "0"):
return "维护"
if value in (1, "1"):
return "开服"
return str(value if value is not None and value != "" else "未知")
if value_type == "list":
if isinstance(value, (list, tuple)):
values = [str(item).strip() for item in value if str(item).strip()]
return "".join(values) if values else ""
return str(value if value is not None and value != "" else "未知")
@staticmethod
def _usage_text() -> str:
@@ -503,4 +692,4 @@ class EventPushService:
for action in EVENT_ACTIONS
if action not in FREE_EVENT_ACTIONS
)
return f"免费事件:\n{free}\n\n事件版事件:\n{paid}"
return f"免费事件:\n{free}\n\n令牌事件:\n{paid}"
+56 -4
View File
@@ -1,7 +1,7 @@
import json
import html
import re
from datetime import datetime, timedelta
from datetime import datetime
from typing import Dict, Any, Optional, List, Union
from inspect import isawaitable
from typing import Any, Awaitable, Callable, Dict, Optional
@@ -19,15 +19,13 @@ from .fun_basic import load_template,gold_to_parts,week_to_num,compare_date_str,
class JX3APIService:
def __init__(self, config: AstrBotConfig, sqlite: AsyncSQLiteDB, cache_sqlite: Optional[AsyncSQLiteDB] = None):
def __init__(self, config: AstrBotConfig, sqlite: AsyncSQLiteDB):
# 实例化 API Client
self._api: APIClient = APIClient()
# 引用插件配置文件
self._config = config
# 引用sqlite
self._sql_db = sqlite
self._cache_db = cache_sqlite or sqlite
# 获取配置中的 Token
self.token = self._config.get("jx3api_token", "")
if self.token == "":
@@ -1067,6 +1065,60 @@ class JX3APIService:
)
async def zili_menu(self) -> Dict[str, str]:
"""资历选择内容。"""
return {
"1": "总览",
"2": "杂闻",
"3": "武学",
"4": "修为",
"5": "装备",
"6": "技艺",
"7": "阅读",
"8": "任务",
"9": "足迹",
"10": "战斗",
"11": "声望",
"12": "秘境",
"13": "帮会",
"14": "阵营",
"15": "节日",
"16": "活动",
"17": "风雨江湖路",
"18": "家园",
"19": "剑侠录"
}
async def zili(self,server: str,name: str,selected: Dict[str, Any],) -> Dict[str, Any]:
"""资历分布"""
if not isinstance(selected, dict) or len(selected) != 1:
return_data = self._init_return_data()
return_data["msg"] = "资历选项数据格式异常"
return return_data
selected_name = str(next(iter(selected.values())) or "").strip()
selected_subclass = "" if selected_name == "总览" else selected_name
async def processor(data: Any, return_data: Dict[str, Any]) -> None:
statistics = data.get("data") or {}
categories = statistics.get("total") or {}
if not isinstance(categories, dict) or not categories:
raise ValueError("接口未返回资历统计")
# 查询总览时 total 为“大类 -> 小类 -> 统计”;指定 subclass 后,
# JX3API 会去掉大类这一层,total 直接变成“小类 -> 统计”。
data["subclass"] = selected_subclass
return_data["data"] = data
return await self._request_api(
path="/tuilan/achievement",
params={"server": server,"name": name,"class": 1,"subclass": selected_subclass,"ticket": self.ticket,"token": self.token,},
processor=processor,
template="zili.html",
)
async def jueshe(self,server: str, name: str, history:int) -> Dict[str, Any]:
"""角色详情"""
async def processor(data: Any, return_data: Dict[str, Any]) -> None:
+82 -266
View File
@@ -14,30 +14,6 @@ from .request import APIClient
from .sqlite import AsyncSQLiteDB
from .fun_basic import load_template,gold_to_parts,week_to_num,compare_date_str,format_time,format_remaining
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}
JX3BOX_API_BASE_URLS = {
"node": "https://node.jx3box.com",
"next2": "https://next2.jx3box.com",
@@ -226,11 +202,8 @@ class JX3BOXService:
return return_data
async def hong1(self, name: str) -> Dict[str, Any]:
"""宏 心法"""
return_data = self._init_return_data()
# 数据库查询数据
async def _macro_posts(self, name: str) -> Optional[List[Dict[str, Any]]]:
"""按心法查询宏帖子,并保留接口热度排序。"""
result = await self._sql_db.select_one(
"kungfu",
"name=? OR name1=? OR name2=? OR name3=? OR name4=? OR name5=?",
@@ -238,52 +211,91 @@ class JX3BOXService:
)
if result is None:
return_data["msg"] = "未找到该心法"
return return_data
return None
kungfu = result.get("name", None)
if kungfu is None:
return_data["msg"] = "未找到该心法"
return return_data
return None
logger.debug(f"查询到数据:{kungfu}")
# 1. 构造请求参数
params = {"subtype": kungfu}
# 2. 调用基础请求
data: Optional[Dict[str, Any]] = await self._base_request(
"cms",
"/api/cms/posts",
params=params,
params={"subtype": kungfu},
)
if not isinstance(data, dict):
return None
if not data:
return_data["msg"] = "未找到该心法一键宏"
return return_data
return [
item
for item in data.get("list", [])
if isinstance(item, dict) and item.get("ID")
]
async def hong1(self, name: str) -> Dict[str, str]:
"""宏首轮:返回“序号 -> 显示内容”的选择数据。"""
posts = await self._macro_posts(name)
if not posts:
raise ValueError("未找到该心法一键宏")
# 提取ID
pid_list = [0]
msg = "按照热度排列\n"
n = 1
try:
for m in data.get('list',[]):
msg += f"{n}{m['author']}\t{m['post_title']}\n"
pid_list.append(m["ID"])
n += 1
return_data["msg"] = msg
return_data["data"]["list"] = pid_list
return_data["data"]["num"] = n
options = {
str(index): f"{post.get('author', '')}\t{post.get('post_title', '')}"
for index, post in enumerate(posts, start=1)
}
if not options:
raise ValueError("未找到该心法一键宏")
except Exception as e:
logger.exception("处理返回数据失败")
return_data["msg"] = "处理返回数据失败"
return return_data
return_data["code"] = 200
if isinstance(e, ValueError):
raise
raise RuntimeError("处理返回数据失败") from e
return return_data
return options
async def hong_select(self,name: str,selected: Dict[str, Any],) -> Dict[str, Any]:
"""宏次轮:根据单项选择数据重新定位帖子并查询详情。"""
if not isinstance(selected, dict) or len(selected) != 1:
return_data = self._init_return_data()
return_data["msg"] = "宏选项数据格式异常"
return return_data
choice, label = next(iter(selected.items()))
posts = await self._macro_posts(name)
if not posts:
return_data = self._init_return_data()
return_data["msg"] = "未找到该心法一键宏"
return return_data
selected_post = next(
(
post
for post in posts
if f"{post.get('author', '')}\t{post.get('post_title', '')}"
== str(label)
),
None,
)
if selected_post is None:
try:
selected_post = posts[int(choice) - 1]
except (TypeError, ValueError, IndexError):
return_data = self._init_return_data()
return_data["msg"] = "无效宏序号"
return return_data
pid = str(selected_post.get("ID") or "").strip()
if not pid:
return_data = self._init_return_data()
return_data["msg"] = "宏帖子 ID 无效"
return return_data
return await self.hong2(pid)
async def hong2(self, pid: str) -> Dict[str, Any]:
@@ -328,12 +340,12 @@ class JX3BOXService:
return return_data
async def _load_achievement_cache(self, key: str) -> tuple[Optional[Any], bool]:
"""读取资历基础数据缓存,返回数据和是否已过期"""
async def _load_trade_cache(self, key: str) -> tuple[Optional[Any], bool]:
"""读取交易行基础数据缓存,返回数据和是否已过期"""
try:
row = await self._cache_db.select_one("achievement_cache", "key=?", (key,))
row = await self._cache_db.select_one("trade_item_cache", "key=?", (key,))
except Exception as e:
logger.error(f"读取资历缓存失败: {e}")
logger.error(f"读取交易行缓存失败: {e}")
return None, True
if not row:
@@ -345,16 +357,16 @@ class JX3BOXService:
expired = datetime.now() - updated_at > timedelta(days=30)
return payload, expired
except Exception as e:
logger.error(f"解析资历缓存失败: {e}")
logger.error(f"解析交易行缓存失败: {e}")
return None, True
async def _save_achievement_cache(self, key: str, payload: Any):
"""写入资历基础数据缓存"""
async def _save_trade_cache(self, key: str, payload: Any):
"""写入交易行基础数据缓存"""
try:
await self._cache_db.execute(
"""
INSERT INTO achievement_cache (key, content, updated_at)
INSERT INTO trade_item_cache (key, content, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
content=excluded.content,
@@ -367,31 +379,13 @@ class JX3BOXService:
),
)
except Exception as e:
logger.error(f"写入资历缓存失败: {e}")
async def _get_achievement_base_data(self, cache_key: str, api_path: 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("node", api_path)
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
logger.error(f"写入交易行缓存失败: {e}")
async def _get_trade_item_groups(self) -> Optional[List[Dict[str, Any]]]:
"""获取交易行物品库,优先使用未过期缓存"""
cache_key = "trade_item_groups"
cached, expired = await self._load_achievement_cache(cache_key)
cached, expired = await self._load_trade_cache(cache_key)
if isinstance(cached, list) and not expired:
return cached
@@ -402,7 +396,7 @@ class JX3BOXService:
params={"client": "std"},
)
if isinstance(data, list) and data:
await self._save_achievement_cache(cache_key, data)
await self._save_trade_cache(cache_key, data)
return data
if isinstance(cached, list) and cached:
@@ -461,184 +455,6 @@ class JX3BOXService:
return [row[3] for row in matched[:limit]]
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}
api_url = "https://www.jx3api.com/role/detail"
role_data: Optional[Dict[str, Any]] = await self._api.get(api_url, params=role_params, out_key="data")
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._base_request(
"next2",
"/api/next2/user-achievements",
params={"jx3id": global_id},
)
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",
"/api/node/achievement/menus",
)
point_payload = await self._get_achievement_base_data(
"achievement_points",
"/api/node/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 jiaoyihang(self, name: str , server: str) -> Dict[str, Any]:
"""区服交易行"""
return_data = self._init_return_data()
+79 -126
View File
@@ -1,3 +1,5 @@
from typing import Any
from astrbot.core import html_renderer
from astrbot.api import logger
from astrbot.api.event import AstrMessageEvent, MessageChain
@@ -14,29 +16,6 @@ from .bilei_data import BiLeidata
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,
jx3api: JX3APIService,
jx3box: JX3BOXService,
@@ -138,94 +117,64 @@ class MessageBuilder:
await event.send(event.plain_result("猪脑过载,请稍后再试"))
async def handler_plain_image_msg(self, event: AstrMessageEvent, action1, action2):
"""两轮会话消息发送通用,先文本列表等反馈序号在发送图片"""
# 会话触发
async def plain_image_msg(self, event: AstrMessageEvent, action):
"""发送正文文本,并把可选 HTML 正文渲染为附图。"""
try:
# 获取一轮数据
data = await action1()
if data["code"] == 200:
# 发送一轮消息
await event.send(event.plain_result(data["msg"]))
# 获取触发用户ID
user_id = event.get_sender_id()
# 二轮会话流程
@session_waiter(timeout=30)
async def macro_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
# 判断数字是否在有效值内
num = int(msg)
if num < 1 or num > data["data"]["num"]:
await new_event.send(
MessageChain().message("无效序号,结束会话")
)
controller.stop()
return
# 获取二轮数据
try:
data1 = await action2(data["data"]["list"][num])
if data1["code"] != 200:
await new_event.send(
MessageChain().message("获取详细数据失败")
)
controller.stop()
return
# 消息拼接发送
chain = MessageChain()
msg_text = data1["data"]
chain.message(msg_text)
if data1["temp"] != "":
url = await self.html_render(data1["temp"], {}, options={})
chain.url_image(url)
await new_event.send(chain)
except Exception as e:
logger.error(f"功能函数执行错误: {e}")
await new_event.send(
MessageChain().message("猪脑过载,请稍后再试")
)
controller.stop()
# 二轮会话激活
try:
await macro_select_waiter(event)
except TimeoutError:
await event.send(event.plain_result("选择超时,已结束会话"))
except Exception:
logger.error("选择发生异常", exc_info=True)
else:
await event.send(event.plain_result(f"未搜索到相关内容"))
data = await action()
if data.get("code") != 200:
await event.send(
event.plain_result(data.get("msg") or "获取详细数据失败")
)
return
chain = MessageChain()
if data.get("data"):
chain.message(str(data["data"]))
if data.get("temp"):
url = await self.html_render(data["temp"], {}, options={})
chain.url_image(url)
await event.send(chain)
except Exception as e:
logger.error(f"功能函数执行错误: {e}")
await event.send(event.plain_result("猪脑过载,请稍后再试"))
async def handler_zili_msg(self, event: AstrMessageEvent, name: str, server: str):
"""资历查询专用两轮会话,第一轮文本菜单,第二轮图片"""
async def handler_plain_image_msg(self, event: AstrMessageEvent, action1, action2, result_handler=None, timeout: int = 10):
"""通用两轮会话:首轮数字键值对裁剪后交给次轮函数。"""
try:
await event.send(event.plain_result(self.ZILI_MENU_TEXT))
user_id = event.get_sender_id()
option_map = await action1()
if not isinstance(option_map, dict) or not option_map:
await event.send(event.plain_result("未搜索到可选内容"))
return
@session_waiter(timeout=30)
async def zili_select_waiter(controller: SessionController, new_event: AstrMessageEvent):
options = {str(key): value for key, value in option_map.items()}
if not all(key.isdigit() for key in options):
await event.send(event.plain_result("选项序号格式异常"))
return
menu_lines = ["请选择序号"]
menu_lines.extend(
f"{key}{label}"
for key, label in options.items()
)
await event.send(event.plain_result("\n".join(menu_lines)))
user_id = event.get_sender_id()
send_result = result_handler or self.T2I_image_msg
async def send_selected(
target_event: AstrMessageEvent,
selected: dict[str, Any],
):
await send_result(
target_event,
lambda: action2(selected),
)
@session_waiter(timeout=timeout)
async def option_select_waiter(
controller: SessionController,
new_event: AstrMessageEvent,
):
if new_event.get_sender_id() != user_id:
return
@@ -236,43 +185,38 @@ class MessageBuilder:
return
choice = int(msg)
if choice < 0 or choice > 18:
choice_key = str(choice)
if choice_key not in options:
await new_event.send(MessageChain().message("无效序号,结束会话"))
controller.stop()
return
try:
data = await self.jx3box.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))
await send_selected(
new_event,
{choice_key: options[choice_key]},
)
except Exception as e:
logger.error(f"资历查询执行错误: {e}")
logger.error(f"二轮查询执行错误: {e}")
await new_event.send(MessageChain().message("猪脑过载,请稍后再试"))
controller.stop()
try:
await zili_select_waiter(event)
await option_select_waiter(event)
except TimeoutError:
await event.send(event.plain_result("选择超时,已结束会话"))
first_key = next(iter(options))
await send_selected(
event,
{first_key: options[first_key]},
)
except Exception:
logger.error("资历选择发生异常", exc_info=True)
logger.error("二轮选择发生异常", exc_info=True)
except (ValueError, RuntimeError) as e:
await event.send(event.plain_result(str(e) or "未搜索到可选内容"))
except Exception as e:
logger.error(f"资历会话执行错误: {e}")
logger.error(f"二轮会话执行错误: {e}")
await event.send(event.plain_result("猪脑过载,请稍后再试"))
@@ -666,11 +610,20 @@ class MessageBuilder:
async def hong(self, event: AstrMessageEvent,name: str):
""" 宏 心法"""
return await self.handler_plain_image_msg(event, lambda: self.jx3box.hong1(name), self.jx3box.hong2)
return await self.handler_plain_image_msg(
event,
lambda: self.jx3box.hong1(name),
lambda selected: self.jx3box.hong_select(name, selected),
result_handler=self.plain_image_msg,
)
async def zili(self, event: AstrMessageEvent, server: str, name: str):
""" 资历 角色名称 服务器"""
return await self.handler_zili_msg(event, name, server)
return await self.handler_plain_image_msg(
event,
lambda: self.jx3api.zili_menu(),
lambda selected: self.jx3api.zili(server, name, selected),
)
async def jiaoyihang(self, event: AstrMessageEvent,server: str, Name: str):
""" 交易行 物品名称 服务器"""
+12
View File
@@ -8,6 +8,8 @@ from .sqlite import AsyncSQLiteDB
class ServerBindingService:
"""维护会话区服绑定、区服别名与指令解析所需的区服目录。"""
ALL_SERVERS_KEYWORD = "全区"
def __init__(self, sqlite: AsyncSQLiteDB, seed_path: Path):
self.sql = sqlite
self.seed_path = seed_path
@@ -103,6 +105,16 @@ class ServerBindingService:
server = self._clean(value)
return self._server_lookup.get(self._key(server), server)
def is_all_servers_query(self, value: Any) -> bool:
"""判断指令中的区服参数是否要求查询全区。"""
return self._key(value) == self._key(self.ALL_SERVERS_KEYWORD)
def resolve_query_server(self, value: Any) -> str:
"""解析查询区服;“全区”作为保留值转换为接口所需的空字符串。"""
if self.is_all_servers_query(value):
return ""
return self.resolve_server(value)
async def get_binding(self, session_id: str) -> str:
row = await self.sql.select_one(
"session_server_bindings",