Files
astrbot_plugin_jx3/core/jx3box_data.py
T
2026-08-07 10:51:15 +08:00

667 lines
23 KiB
Python

import json
import html
import re
from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List, Union
from inspect import isawaitable
from typing import Any, Awaitable, Callable, Dict, Optional
from astrbot.api import logger
from astrbot.api import AstrBotConfig
import astrbot.api.message_components as Comp
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}
class JX3BOXService:
def __init__(self, config: AstrBotConfig, sqlite: AsyncSQLiteDB, cache_sqlite: Optional[AsyncSQLiteDB] = None):
# 实例化 API Client
self._api: APIClient = APIClient()
# 引用插件配置文件
self._config = config
# 引用sqlite
self._sql_db = sqlite
self._cache_db = cache_sqlite or sqlite
self.token = self._config.get("jx3api_token", "")
async def close(self):
"""释放底层 APIClient 资源"""
if self._api:
await self._api.close()
def _init_return_data(self) -> Dict[str, Any]:
"""初始化标准的返回数据结构"""
return {
"code": 0,
"msg": "功能函数未执行",
"data": {},
"temp": "",
"icons": {}
}
async def qiyugonglue(self, name: str) -> Dict[str, Any]:
"""奇遇攻略"""
return_data = self._init_return_data()
# 1. 调用基础请求
api_url = "https://node.jx3box.com/serendipities"
data = await self._api.get(api_url, params={"name": name}, out_key="list")
if not data:
return_data["msg"] = "未找到该奇遇"
return return_data
# 提取dwID
dwID = data[0]["dwID"]
url = f"https://node.jx3box.com/serendipity/{dwID}/achievement"
logger.debug(f"获取ID接口地址:{url}")
data1 = await self._api.get(url)
url1 = f"https://cms.jx3box.com/api/cms/wiki/post/type/achievement/source/{data1['achievement_id']}"
logger.debug(f"获取攻略接口地址:{url1}")
# 获取奇遇攻略
data2 = await self._api.get(url1, out_key="data")
if not data2:
return_data["msg"] = "获取攻略数据异常"
return return_data
# 4. 处理数据
try:
return_data["data"] = {}
content = data2["post"]["content"]
return_data["temp"] = content
except Exception as e:
logger.exception("处理返回数据失败")
return_data["msg"] = "处理返回数据失败"
return return_data
return_data["code"] = 200
return return_data
async def peizhuang(self, name: str, tags: str) -> Dict[str, Any]:
"""配装"""
return_data = self._init_return_data()
# 数据库查询数据
result = await self._sql_db.select_one(
"kungfu",
"name=? OR name1=? OR name2=? OR name3=? OR name4=? OR name5=?",
(name, name, name, name, name, name)
)
logger.debug(result)
if result is None:
return_data["msg"] = "未找到该心法"
return return_data
mount = result.get("pzid", None)
if not mount:
return_data["msg"] = "未找到该心法"
return return_data
logger.debug(f"查询到数据:{mount}")
# 1. 构造请求参数
params = {
"per": "10",
"page": "1",
"tags": tags,
"client": "std",
"global_level": "130",
"mount": mount,
"star": "1"
}
# 2. 调用基础请求
api_url = "https://cms.jx3box.com/api/cms/app/pz"
data: Optional[Dict[str, Any]] = await self._api.get(api_url, params=params, out_key="data")
# 验证数据
if not data:
return_data["msg"] = "配装数据获取异常"
return return_data
# 3. 处理返回数据
try:
result_msg = f"{name}--配装\n"
for item in data["list"]:
result_msg += f"【{item['zlp']}】--{item['title']}\n"
result_msg += f"链接:https://www.jx3box.com/pz/view/{item['id']}\n\n"
return_data["data"] = result_msg
except Exception as e:
logger.exception("处理返回数据失败:",e)
return_data["msg"] = "处理返回数据失败"
return return_data
return_data["code"] = 200
return return_data
async def hong1(self, name: str) -> Dict[str, Any]:
"""宏 心法"""
return_data = self._init_return_data()
# 数据库查询数据
result = await self._sql_db.select_one(
"kungfu",
"name=? OR name1=? OR name2=? OR name3=? OR name4=? OR name5=?",
(name, name, name, name, name, name)
)
if result is None:
return_data["msg"] = "未找到该心法"
return return_data
kungfu = result.get("name", None)
if kungfu is None:
return_data["msg"] = "未找到该心法"
return return_data
logger.debug(f"查询到数据:{kungfu}")
# 1. 构造请求参数
params = {"subtype": kungfu}
# 2. 调用基础请求
api_url = "https://cms.jx3box.com/api/cms/posts"
data: Optional[Dict[str, Any]] = await self._api.get(api_url, params=params, out_key="data")
if not data:
return_data["msg"] = "未找到该心法一键宏"
return return_data
# 提取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
except Exception as e:
logger.exception("处理返回数据失败")
return_data["msg"] = "处理返回数据失败"
return return_data
return_data["code"] = 200
return return_data
async def hong2(self, pid: str) -> Dict[str, Any]:
"""宏 心法"""
return_data = self._init_return_data()
url = f"https://cms.jx3box.com/api/cms/post/{pid}"
logger.debug(f"获取宏接口地址:{url}")
data = await self._api.get(url, out_key="data")
if not isinstance(data, dict):
return_data["msg"] = "获取宏数据异常"
return return_data
try:
# 文章正文
return_data["temp"] = data.get("post_content", "")
# 安全获取宏数据,避免 KeyError
macro_list = data.get("post_meta", {}).get("data", [])
msg = ""
for m in macro_list:
msg += f"【宏名称】\n{m.get('name', '')}\n"
msg += f"【使用说明】\n{m.get('desc', '')}\n"
msg += f"【宏脚本】\n{m.get('macro', '')}\n\n"
# 没有宏数据时返回文章信息
if not msg:
msg = (
f"标题:{data.get('post_title', '')}\n"
f"作者:{data.get('author', '')}\n"
"该帖子没有宏数据"
)
return_data["data"] = msg
return_data["code"] = 200
except Exception:
logger.exception("处理返回数据失败")
return_data["msg"] = "处理返回数据失败"
return return_data
async def _load_achievement_cache(self, key: str) -> tuple[Optional[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: 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
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)
if isinstance(cached, list) and not expired:
return cached
api_url = "https://cms.jx3box.com/api/cms/pvx/item/group"
data = await self._api.get(api_url, params={"client":"std"}, out_key="data")
if isinstance(data, list) and data:
await self._save_achievement_cache(cache_key, data)
return data
if isinstance(cached, list) and cached:
logger.warning("交易行物品库接口失败,使用旧缓存")
return cached
return None
def _flatten_trade_items(self, groups: List[Dict[str, Any]]) -> list[Dict[str, Any]]:
"""从交易行物品分组中提取可查询物品"""
items = []
for group in groups:
if not isinstance(group, dict):
continue
for item in group.get("items", []) or []:
if not isinstance(item, dict):
continue
item_id = item.get("item_id")
label = item.get("label")
if not item_id or not label:
continue
items.append(
{
"item_id": str(item_id),
"label": str(label),
"icon": str(item.get("icon") or ""),
}
)
return items
def _match_trade_items(self, items: list[Dict[str, Any]], keyword: str, limit: int = 50) -> list[Dict[str, Any]]:
"""按物品名模糊匹配交易行物品"""
keyword = (keyword or "").strip()
if not keyword:
return []
matched = []
seen = set()
for item in items:
label = item.get("label", "")
item_id = item.get("item_id", "")
if keyword not in label or item_id in seen:
continue
seen.add(item_id)
if label == keyword:
rank = 0
elif label.startswith(keyword):
rank = 1
else:
rank = 2
matched.append((rank, len(label), label, item))
matched.sort(key=lambda row: (row[0], row[1], row[2]))
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._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 jiaoyihang(self, name: str , server: str) -> Dict[str, Any]:
"""区服交易行"""
return_data = self._init_return_data()
item_groups = await self._get_trade_item_groups()
if not item_groups:
return_data["msg"] = "交易行基础物品数据获取失败"
return return_data
trade_items = self._flatten_trade_items(item_groups)
matched_items = self._match_trade_items(trade_items, name, 50)
if not matched_items:
return_data["msg"] = "未找到匹配的交易行物品"
return return_data
item_map = {item["item_id"]: item for item in matched_items}
params = {
"item_ids": list(item_map.keys()),
"server": server,
"aggregate_type": "hourly",
}
api_url = "https://next2.jx3box.com/api/auction/"
price_data: Optional[List[Dict[str, Any]]] = await self._api.post(api_url, data=params, out_key="")
if not price_data or not isinstance(price_data, list):
return_data["msg"] = "未查询到交易行价格数据"
return return_data
try:
result = []
for price_item in price_data:
if not isinstance(price_item, dict):
continue
item_id = str(price_item.get("item_id") or "")
base_item = item_map.get(item_id)
if not base_item:
continue
timestamp = price_item.get("timestamp")
try:
created = datetime.fromtimestamp(int(timestamp)).strftime("%Y-%m-%d %H:%M:%S")
except (TypeError, ValueError, OSError):
created = ""
result.append(
{
"item_id": item_id,
"name": base_item.get("label", ""),
"icon": f"https://icon.jx3box.com/icon/{base_item.get('icon', '')}.png",
"server": price_item.get("server", server),
"price": price_item.get("price", 0),
"price_parts": gold_to_parts(price_item.get("price", 0)),
"sample": price_item.get("sample", 0),
"created": created,
}
)
if not result:
return_data["msg"] = "未查询到交易行价格数据"
return return_data
return_data["data"] = {
"search_name": name,
"server": server,
"matched_count": len(matched_items),
"result_count": len(result),
"list": result,
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
except Exception as e:
logger.error(f"处理交易行数据失败: {e}")
return_data["msg"] = "处理交易行数据失败"
return return_data
# 5. 模板渲染
try:
return_data["temp"] = await load_template("jiaoyihang.html")
return_data["code"] = 200
except FileNotFoundError as e:
logger.error(f"加载模板失败: {e}")
return_data["msg"] = "系统错误:模板文件不存在"
return return_data
return return_data