diff --git a/metadata/scripts/paimon_moe.py b/metadata/scripts/paimon_moe.py
new file mode 100644
index 0000000..69a2570
--- /dev/null
+++ b/metadata/scripts/paimon_moe.py
@@ -0,0 +1,14 @@
+from utils.const import PROJECT_ROOT
+from aiofiles import open as async_open
+from httpx import AsyncClient, URL
+
+
+async def update_paimon_moe_zh(overwrite: bool = True):
+ path = PROJECT_ROOT.joinpath("metadata/data/paimon_moe_zh.json")
+ if not overwrite and path.exists():
+ return
+ host = URL("https://raw.fastgit.org/MadeBaruna/paimon-moe/main/src/locales/items/zh.json")
+ client = AsyncClient()
+ text = (await client.get(host)).text
+ async with async_open(path, mode="w", encoding="utf-8") as file:
+ await file.write(text)
diff --git a/metadata/shortname.py b/metadata/shortname.py
index ed199c5..e1bf44c 100644
--- a/metadata/shortname.py
+++ b/metadata/shortname.py
@@ -4,7 +4,7 @@ import functools
from metadata.genshin import WEAPON_DATA
-__all__ = ["roles", "weapons", "roleToId", "roleToName", "weaponToName", "weaponToId"]
+__all__ = ["roles", "weapons", "roleToId", "roleToName", "weaponToName", "weaponToId", "not_real_roles"]
# noinspection SpellCheckingInspection
roles = {
diff --git a/modules/apihelper/error.py b/modules/apihelper/error.py
index 8208a26..94c38f2 100644
--- a/modules/apihelper/error.py
+++ b/modules/apihelper/error.py
@@ -5,14 +5,6 @@ class APIHelperException(Exception):
pass
-class GachaLogException(APIHelperException):
- pass
-
-
-class GachaLogAccountNotFound(GachaLogException):
- pass
-
-
class NetworkException(APIHelperException):
pass
diff --git a/modules/gacha_log/const.py b/modules/gacha_log/const.py
new file mode 100644
index 0000000..6783b8c
--- /dev/null
+++ b/modules/gacha_log/const.py
@@ -0,0 +1,13 @@
+from genshin.models import BannerType
+
+PAIMONMOE_VERSION = 3
+UIGF_VERSION = "v2.2"
+
+
+GACHA_TYPE_LIST = {
+ BannerType.NOVICE: "新手祈愿",
+ BannerType.PERMANENT: "常驻祈愿",
+ BannerType.WEAPON: "武器祈愿",
+ BannerType.CHARACTER1: "角色祈愿",
+ BannerType.CHARACTER2: "角色祈愿",
+}
diff --git a/modules/gacha_log/error.py b/modules/gacha_log/error.py
new file mode 100644
index 0000000..38b85fd
--- /dev/null
+++ b/modules/gacha_log/error.py
@@ -0,0 +1,29 @@
+class GachaLogException(Exception):
+ pass
+
+
+class GachaLogFileError(GachaLogException):
+ pass
+
+
+class GachaLogNotFound(GachaLogException):
+ pass
+
+
+class GachaLogAccountNotFound(GachaLogException):
+ pass
+
+
+class GachaLogInvalidAuthkey(GachaLogException):
+ pass
+
+
+class GachaLogMixedProvider(GachaLogException):
+ pass
+
+
+class PaimonMoeGachaLogFileError(GachaLogFileError):
+ def __init__(self, file_version: int, support_version: int):
+ super().__init__("Paimon.Moe version not supported")
+ self.support_version = support_version
+ self.file_version = file_version
diff --git a/modules/gacha_log/helpers.py b/modules/gacha_log/helpers.py
new file mode 100644
index 0000000..3c5a540
--- /dev/null
+++ b/modules/gacha_log/helpers.py
@@ -0,0 +1,9 @@
+def from_url_get_authkey(url: str) -> str:
+ """从 UEL 解析 authkey
+ :param url: URL
+ :return: authkey
+ """
+ try:
+ return url.split("authkey=")[1].split("&")[0]
+ except IndexError:
+ return url
diff --git a/modules/apihelper/gacha_log.py b/modules/gacha_log/log.py
similarity index 60%
rename from modules/apihelper/gacha_log.py
rename to modules/gacha_log/log.py
index 30ae6bb..f52f5bf 100644
--- a/modules/apihelper/gacha_log.py
+++ b/modules/gacha_log/log.py
@@ -1,138 +1,47 @@
import contextlib
import datetime
import json
-import time
+from io import BytesIO
from pathlib import Path
-from typing import List, Dict, Tuple, Optional, Union
+from typing import List, Tuple, Optional
import aiofiles
from genshin import Client, InvalidAuthkey
from genshin.models import BannerType
-from pydantic import BaseModel, validator
+from openpyxl import load_workbook
from core.base.assets import AssetsService
from metadata.pool.pool import get_pool_by_id
-from metadata.shortname import roleToId, weaponToId, not_real_roles
-from modules.apihelper.error import GachaLogAccountNotFound
-from utils.const import PROJECT_ROOT
-from utils.log import logger
-
-GACHA_LOG_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "gacha_log")
-GACHA_LOG_PATH.mkdir(parents=True, exist_ok=True)
-GACHA_TYPE_LIST = {
- BannerType.NOVICE: "新手祈愿",
- BannerType.PERMANENT: "常驻祈愿",
- BannerType.WEAPON: "武器祈愿",
- BannerType.CHARACTER1: "角色祈愿",
- BannerType.CHARACTER2: "角色祈愿",
-}
-
-
-class FiveStarItem(BaseModel):
- name: str
- icon: str
- count: int
- type: str
- isUp: bool
- isBig: bool
- time: datetime.datetime
-
-
-class FourStarItem(BaseModel):
- name: str
- icon: str
- count: int
- type: str
- time: datetime.datetime
-
-
-class GachaItem(BaseModel):
- id: str
- name: str
- gacha_type: str
- item_type: str
- rank_type: str
- time: datetime.datetime
-
- @validator("name")
- def name_validator(cls, v):
- if item_id := (roleToId(v) or weaponToId(v)):
- if item_id not in not_real_roles:
- return v
- raise ValueError("Invalid name")
-
- @validator("gacha_type")
- def check_gacha_type(cls, v):
- if v not in {"100", "200", "301", "302", "400"}:
- raise ValueError("gacha_type must be 200, 301, 302 or 400")
- return v
-
- @validator("item_type")
- def check_item_type(cls, item):
- if item not in {"角色", "武器"}:
- raise ValueError("error item type")
- return item
-
- @validator("rank_type")
- def check_rank_type(cls, rank):
- if rank not in {"5", "4", "3"}:
- raise ValueError("error rank type")
- return rank
-
-
-class GachaLogInfo(BaseModel):
- user_id: str
- uid: str
- update_time: datetime.datetime
- item_list: Dict[str, List[GachaItem]] = {
- "角色祈愿": [],
- "武器祈愿": [],
- "常驻祈愿": [],
- "新手祈愿": [],
- }
-
-
-class Pool:
- def __init__(self, five: List[str], four: List[str], name: str, to: str, **kwargs):
- self.five = five
- self.real_name = name
- self.name = "、".join(self.five)
- self.four = four
- self.from_ = kwargs.get("from")
- self.to = to
- self.from_time = datetime.datetime.strptime(self.from_, "%Y-%m-%d %H:%M:%S")
- self.to_time = datetime.datetime.strptime(self.to, "%Y-%m-%d %H:%M:%S")
- self.start = self.from_time
- self.start_init = False
- self.end = self.to_time
- self.dict = {}
- self.count = 0
-
- def parse(self, item: Union[FiveStarItem, FourStarItem]):
- if self.from_time <= item.time <= self.to_time:
- if self.dict.get(item.name):
- self.dict[item.name]["count"] += 1
- else:
- self.dict[item.name] = {
- "name": item.name,
- "icon": item.icon,
- "count": 1,
- "rank_type": 5 if isinstance(item, FiveStarItem) else 4,
- }
-
- def count_item(self, item: List[GachaItem]):
- for i in item:
- if self.from_time <= i.time <= self.to_time:
- self.count += 1
- if not self.start_init:
- self.start = i.time
- self.end = i.time
-
- def to_list(self):
- return list(self.dict.values())
+from metadata.shortname import roleToId, weaponToId
+from modules.gacha_log.const import GACHA_TYPE_LIST, PAIMONMOE_VERSION
+from modules.gacha_log.error import (
+ GachaLogAccountNotFound,
+ GachaLogInvalidAuthkey,
+ GachaLogException,
+ GachaLogFileError,
+ GachaLogNotFound,
+ PaimonMoeGachaLogFileError,
+ GachaLogMixedProvider,
+)
+from modules.gacha_log.models import (
+ GachaItem,
+ FiveStarItem,
+ FourStarItem,
+ Pool,
+ GachaLogInfo,
+ UIGFGachaType,
+ ItemType,
+ ImportType,
+ UIGFModel,
+ UIGFInfo,
+ UIGFItem,
+)
class GachaLog:
+ def __init__(self, gacha_log_path: Path):
+ self.gacha_log_path = gacha_log_path
+
@staticmethod
async def load_json(path):
async with aiofiles.open(path, "r", encoding="utf-8") as f:
@@ -145,9 +54,8 @@ class GachaLog:
return await f.write(json.dumps(data, ensure_ascii=False, indent=4))
await f.write(data)
- @staticmethod
async def load_history_info(
- user_id: str, uid: str, only_status: bool = False
+ self, user_id: str, uid: str, only_status: bool = False
) -> Tuple[Optional[GachaLogInfo], bool]:
"""读取历史抽卡记录数据
:param user_id: 用户id
@@ -155,26 +63,25 @@ class GachaLog:
:param only_status: 是否只读取状态
:return: 抽卡记录数据
"""
- file_path = GACHA_LOG_PATH / f"{user_id}-{uid}.json"
+ file_path = self.gacha_log_path / f"{user_id}-{uid}.json"
if only_status:
return None, file_path.exists()
if not file_path.exists():
return GachaLogInfo(user_id=user_id, uid=uid, update_time=datetime.datetime.now()), False
try:
- return GachaLogInfo.parse_obj(await GachaLog.load_json(file_path)), True
+ return GachaLogInfo.parse_obj(await self.load_json(file_path)), True
except json.decoder.JSONDecodeError:
return GachaLogInfo(user_id=user_id, uid=uid, update_time=datetime.datetime.now()), False
- @staticmethod
- async def remove_history_info(user_id: str, uid: str) -> bool:
+ async def remove_history_info(self, user_id: str, uid: str) -> bool:
"""删除历史抽卡记录数据
:param user_id: 用户id
:param uid: 原神uid
:return: 是否删除成功
"""
- file_path = GACHA_LOG_PATH / f"{user_id}-{uid}.json"
- file_bak_path = GACHA_LOG_PATH / f"{user_id}-{uid}.json.bak"
- file_export_path = GACHA_LOG_PATH / f"{user_id}-{uid}-uigf.json"
+ file_path = self.gacha_log_path / f"{user_id}-{uid}.json"
+ file_bak_path = self.gacha_log_path / f"{user_id}-{uid}.json.bak"
+ file_export_path = self.gacha_log_path / f"{user_id}-{uid}-uigf.json"
with contextlib.suppress(Exception):
file_bak_path.unlink(missing_ok=True)
with contextlib.suppress(Exception):
@@ -187,15 +94,14 @@ class GachaLog:
return True
return False
- @staticmethod
- async def save_gacha_log_info(user_id: str, uid: str, info: GachaLogInfo):
+ async def save_gacha_log_info(self, user_id: str, uid: str, info: GachaLogInfo):
"""保存抽卡记录数据
:param user_id: 用户id
:param uid: 原神uid
:param info: 抽卡记录数据
"""
- save_path = GACHA_LOG_PATH / f"{user_id}-{uid}.json"
- save_path_bak = GACHA_LOG_PATH / f"{user_id}-{uid}.json.bak"
+ save_path = self.gacha_log_path / f"{user_id}-{uid}.json"
+ save_path_bak = self.gacha_log_path / f"{user_id}-{uid}.json.bak"
# 将旧数据备份一次
with contextlib.suppress(PermissionError):
if save_path.exists():
@@ -203,82 +109,73 @@ class GachaLog:
save_path_bak.unlink()
save_path.rename(save_path.parent / f"{save_path.name}.bak")
# 写入数据
- await GachaLog.save_json(save_path, info.json())
+ await self.save_json(save_path, info.json())
- @staticmethod
- async def gacha_log_to_uigf(user_id: str, uid: str) -> Tuple[bool, str, Optional[Path]]:
+ async def gacha_log_to_uigf(self, user_id: str, uid: str) -> Optional[Path]:
"""抽卡日记转换为 UIGF 格式
:param user_id: 用户ID
:param uid: 游戏UID
:return: 转换是否成功、转换信息、UIGF文件目录
"""
- data, state = await GachaLog.load_history_info(user_id, uid)
+ data, state = await self.load_history_info(user_id, uid)
if not state:
- return False, "派蒙还没有找到你导入的任何抽卡记录哦,快试试导入吧~", None
- save_path = GACHA_LOG_PATH / f"{user_id}-{uid}-uigf.json"
- uigf_dict = {
- "info": {
- "uid": uid,
- "lang": "zh-cn",
- "export_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- "export_timestamp": int(time.time()),
- "export_app": "TGPaimonBot",
- "export_app_version": "v3",
- "uigf_version": "v2.2",
- },
- "list": [],
- }
+ raise GachaLogNotFound
+ save_path = self.gacha_log_path / f"{user_id}-{uid}-uigf.json"
+ info = UIGFModel(
+ info=UIGFInfo(uid=uid, export_app=ImportType.TGPaimonBot.value, export_app_version="v3"), list=[]
+ )
for items in data.item_list.values():
for item in items:
- uigf_dict["list"].append(
- {
- "gacha_type": item.gacha_type,
- "item_id": "",
- "count": "1",
- "time": item.time.strftime("%Y-%m-%d %H:%M:%S"),
- "name": item.name,
- "item_type": item.item_type,
- "rank_type": item.rank_type,
- "id": item.id,
- "uigf_gacha_type": item.gacha_type,
- }
+ info.list.append(
+ UIGFItem(
+ id=item.id,
+ name=item.name,
+ gacha_type=item.gacha_type,
+ item_type=item.item_type,
+ rank_type=item.rank_type,
+ time=item.time.strftime("%Y-%m-%d %H:%M:%S"),
+ uigf_gacha_type=item.gacha_type,
+ )
)
- await GachaLog.save_json(save_path, uigf_dict)
- return True, "", save_path
+ await self.save_json(save_path, info.dict())
+ return save_path
@staticmethod
- async def verify_data(data: List[GachaItem]):
+ async def verify_data(data: List[GachaItem]) -> bool:
try:
total = len(data)
five_star = len([i for i in data if i.rank_type == "5"])
four_star = len([i for i in data if i.rank_type == "4"])
if total > 50:
if total <= five_star * 15:
- return False, "检测到您将要导入的抽卡记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。"
+ raise GachaLogFileError("检测到您将要导入的抽卡记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。")
if four_star < five_star:
- return False, "检测到您将要导入的抽卡记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。"
- return True, ""
+ raise GachaLogFileError("检测到您将要导入的抽卡记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。")
+ return True
except Exception as exc: # pylint: disable=W0703
- logger.warning(f"抽卡记录数据验证失败 {repr(exc)}")
- return False, "导入失败,数据格式错误"
+ raise GachaLogFileError from exc
- @staticmethod
- async def import_gacha_log_data(
- user_id: int, client: Client, data: dict, verify_uid: bool = True
- ) -> Tuple[bool, str]:
+ async def import_gacha_log_data(self, user_id: int, client: Client, data: dict, verify_uid: bool = True) -> int:
new_num = 0
try:
- if not verify_uid:
- data["info"]["uid"] = str(client.uid)
uid = data["info"]["uid"]
- if int(uid) != client.uid:
+ if not verify_uid:
+ uid = client.uid
+ elif int(uid) != client.uid:
raise GachaLogAccountNotFound
+ try:
+ import_type = ImportType(data["info"]["export_app"])
+ except ValueError:
+ import_type = ImportType.UNKNOWN
# 检查导入数据是否合法
all_items = [GachaItem(**i) for i in data["list"]]
- status, text = await GachaLog.verify_data(all_items)
- if not status:
- return text
- gacha_log, _ = await GachaLog.load_history_info(str(user_id), uid)
+ await self.verify_data(all_items)
+ gacha_log, status = await self.load_history_info(str(user_id), uid)
+ if import_type == ImportType.PAIMONMOE:
+ if status and gacha_log.get_import_type != ImportType.PAIMONMOE:
+ raise GachaLogMixedProvider
+ elif status and gacha_log.get_import_type == ImportType.PAIMONMOE:
+ raise GachaLogMixedProvider
# 将唯一 id 放入临时数据中,加快查找速度
temp_id_data = {
pool_name: [i.id for i in pool_data] for pool_name, pool_data in gacha_log.item_list.items()
@@ -291,30 +188,30 @@ class GachaLog:
new_num += 1
for i in gacha_log.item_list.values():
# 检查导入后的数据是否合法
- status, text = await GachaLog.verify_data(i)
- if not status:
- return text
+ await self.verify_data(i)
i.sort(key=lambda x: (x.time, x.id))
gacha_log.update_time = datetime.datetime.now()
- await GachaLog.save_gacha_log_info(str(user_id), uid, gacha_log)
- return "导入完成,本次没有新增数据" if new_num == 0 else f"导入完成,本次共新增{new_num}条抽卡记录"
- except GachaLogAccountNotFound:
- return "导入失败,文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同"
+ gacha_log.import_type = import_type.value
+ await self.save_gacha_log_info(str(user_id), uid, gacha_log)
+ return new_num
+ except GachaLogAccountNotFound as e:
+ raise GachaLogAccountNotFound("导入失败,文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同") from e
+ except GachaLogMixedProvider as e:
+ raise GachaLogMixedProvider from e
except Exception as exc:
- logger.warning(f"导入失败,数据格式错误 {repr(exc)}")
- return "导入失败,数据格式错误"
+ raise GachaLogException from exc
- @staticmethod
- async def get_gacha_log_data(user_id: int, client: Client, authkey: str) -> str:
- """
- 使用authkey获取抽卡记录数据,并合并旧数据
+ async def get_gacha_log_data(self, user_id: int, client: Client, authkey: str) -> int:
+ """使用authkey获取抽卡记录数据,并合并旧数据
:param user_id: 用户id
:param client: genshin client
:param authkey: authkey
:return: 更新结果
"""
new_num = 0
- gacha_log, _ = await GachaLog.load_history_info(str(user_id), str(client.uid))
+ gacha_log, _ = await self.load_history_info(str(user_id), str(client.uid))
+ if gacha_log.get_import_type == ImportType.PAIMONMOE:
+ raise GachaLogMixedProvider
# 将唯一 id 放入临时数据中,加快查找速度
temp_id_data = {pool_name: [i.id for i in pool_data] for pool_name, pool_data in gacha_log.item_list.items()}
try:
@@ -340,13 +237,14 @@ class GachaLog:
gacha_log.item_list[pool_name].append(item)
temp_id_data[pool_name].append(item.id)
new_num += 1
- except InvalidAuthkey:
- return "更新数据失败,authkey 无效"
+ except InvalidAuthkey as exc:
+ raise GachaLogInvalidAuthkey from exc
for i in gacha_log.item_list.values():
i.sort(key=lambda x: (x.time, x.id))
gacha_log.update_time = datetime.datetime.now()
- await GachaLog.save_gacha_log_info(str(user_id), str(client.uid), gacha_log)
- return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条抽卡记录"
+ gacha_log.import_type = ImportType.UIGF.value
+ await self.save_gacha_log_info(str(user_id), str(client.uid), gacha_log)
+ return new_num
@staticmethod
def check_avatar_up(name: str, gacha_time: datetime.datetime) -> bool:
@@ -364,8 +262,7 @@ class GachaLog:
return False
return True
- @staticmethod
- async def get_all_5_star_items(data: List[GachaItem], assets: AssetsService, pool_name: str = "角色祈愿"):
+ async def get_all_5_star_items(self, data: List[GachaItem], assets: AssetsService, pool_name: str = "角色祈愿"):
"""
获取所有5星角色
:param data: 抽卡记录
@@ -384,7 +281,7 @@ class GachaLog:
"icon": (await assets.avatar(roleToId(item.name)).icon()).as_uri(),
"count": count,
"type": "角色",
- "isUp": GachaLog.check_avatar_up(item.name, item.time) if pool_name == "角色祈愿" else False,
+ "isUp": self.check_avatar_up(item.name, item.time) if pool_name == "角色祈愿" else False,
"isBig": (not result[-1].isUp) if result and pool_name == "角色祈愿" else False,
"time": item.time,
}
@@ -567,8 +464,7 @@ class GachaLog:
return f"{pool_name} · 非"
return pool_name
- @staticmethod
- async def get_analysis(user_id: int, client: Client, pool: BannerType, assets: AssetsService):
+ async def get_analysis(self, user_id: int, client: Client, pool: BannerType, assets: AssetsService):
"""
获取抽卡记录分析数据
:param user_id: 用户id
@@ -577,26 +473,26 @@ class GachaLog:
:param assets: 资源服务
:return: 分析数据
"""
- gacha_log, status = await GachaLog.load_history_info(str(user_id), str(client.uid))
+ gacha_log, status = await self.load_history_info(str(user_id), str(client.uid))
if not status:
- return "派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~"
+ raise GachaLogNotFound
pool_name = GACHA_TYPE_LIST[pool]
data = gacha_log.item_list[pool_name]
total = len(data)
if total == 0:
- return "派蒙没有找到这个卡池的抽卡记录,快来私聊派蒙导入吧~"
- all_five, no_five_star = await GachaLog.get_all_5_star_items(data, assets, pool_name)
- all_four, no_four_star = await GachaLog.get_all_4_star_items(data, assets)
+ raise GachaLogNotFound
+ all_five, no_five_star = await self.get_all_5_star_items(data, assets, pool_name)
+ all_four, no_four_star = await self.get_all_4_star_items(data, assets)
summon_data = None
if pool == BannerType.CHARACTER1:
- summon_data = GachaLog.get_301_pool_data(total, all_five, no_five_star, no_four_star)
- pool_name = GachaLog.count_fortune(pool_name, summon_data)
+ summon_data = self.get_301_pool_data(total, all_five, no_five_star, no_four_star)
+ pool_name = self.count_fortune(pool_name, summon_data)
elif pool == BannerType.WEAPON:
- summon_data = GachaLog.get_302_pool_data(total, all_five, all_four, no_five_star, no_four_star)
- pool_name = GachaLog.count_fortune(pool_name, summon_data, True)
+ summon_data = self.get_302_pool_data(total, all_five, all_four, no_five_star, no_four_star)
+ pool_name = self.count_fortune(pool_name, summon_data, True)
elif pool == BannerType.PERMANENT:
- summon_data = GachaLog.get_200_pool_data(total, all_five, all_four, no_five_star, no_four_star)
- pool_name = GachaLog.count_fortune(pool_name, summon_data)
+ summon_data = self.get_200_pool_data(total, all_five, all_four, no_five_star, no_four_star)
+ pool_name = self.count_fortune(pool_name, summon_data)
last_time = data[0].time.strftime("%Y-%m-%d %H:%M")
first_time = data[-1].time.strftime("%Y-%m-%d %H:%M")
return {
@@ -611,10 +507,10 @@ class GachaLog:
"fourLog": all_four[:18],
}
- @staticmethod
- async def get_pool_analysis(user_id: int, client: Client, pool: BannerType, assets: AssetsService, group: bool):
- """
- 获取抽卡记录分析数据
+ async def get_pool_analysis(
+ self, user_id: int, client: Client, pool: BannerType, assets: AssetsService, group: bool
+ ) -> dict:
+ """获取抽卡记录分析数据
:param user_id: 用户id
:param client: genshin client
:param pool: 池子类型
@@ -622,16 +518,16 @@ class GachaLog:
:param group: 是否群组
:return: 分析数据
"""
- gacha_log, status = await GachaLog.load_history_info(str(user_id), str(client.uid))
+ gacha_log, status = await self.load_history_info(str(user_id), str(client.uid))
if not status:
- return "派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~"
+ raise GachaLogNotFound
pool_name = GACHA_TYPE_LIST[pool]
data = gacha_log.item_list[pool_name]
total = len(data)
if total == 0:
- return "派蒙没有找到这个卡池的抽卡记录,快来私聊派蒙导入吧~"
- all_five, _ = await GachaLog.get_all_5_star_items(data, assets, pool_name)
- all_four, _ = await GachaLog.get_all_4_star_items(data, assets)
+ raise GachaLogNotFound
+ all_five, _ = await self.get_all_5_star_items(data, assets, pool_name)
+ all_four, _ = await self.get_all_4_star_items(data, assets)
pool_data = []
up_pool_data = [Pool(**i) for i in get_pool_by_id(pool.value)]
for up_pool in up_pool_data:
@@ -657,3 +553,128 @@ class GachaLog:
"pool": pool_data[:6] if group else pool_data,
"hasMore": len(pool_data) > 6,
}
+
+ @staticmethod
+ def convert_xlsx_to_uigf(data: BytesIO, zh_dict: dict) -> dict:
+ """转换 paimone.moe 或 非小酋 导出 xlsx 数据为 UIGF 格式
+ :param zh_dict:
+ :param data: paimon.moe 导出的 xlsx 数据
+ :return: UIGF 格式数据
+ """
+
+ def from_paimon_moe(
+ uigf_gacha_type: UIGFGachaType, item_type: str, name: str, date_string: str, rank_type: int, _id: int
+ ) -> UIGFItem:
+ item_type = ItemType.CHARACTER if item_type == "Character" else ItemType.WEAPON
+ return UIGFItem(
+ id=str(_id),
+ name=zh_dict[name],
+ gacha_type=uigf_gacha_type,
+ item_type=item_type,
+ rank_type=str(rank_type),
+ time=date_string,
+ uigf_gacha_type=uigf_gacha_type,
+ )
+
+ def from_uigf(
+ uigf_gacha_type: str,
+ gacha__type: str,
+ item_type: str,
+ name: str,
+ date_string: str,
+ rank_type: str,
+ _id: str,
+ ) -> UIGFItem:
+ return UIGFItem(
+ id=_id,
+ name=name,
+ gacha_type=gacha__type,
+ item_type=item_type,
+ rank_type=rank_type,
+ time=date_string,
+ uigf_gacha_type=uigf_gacha_type,
+ )
+
+ def from_fxq(
+ uigf_gacha_type: UIGFGachaType, item_type: str, name: str, date_string: str, rank_type: int, _id: int
+ ) -> UIGFItem:
+ item_type = ItemType.CHARACTER if item_type == "角色" else ItemType.WEAPON
+ return UIGFItem(
+ id=str(_id),
+ name=name,
+ gacha_type=uigf_gacha_type,
+ item_type=item_type,
+ rank_type=str(rank_type),
+ time=date_string,
+ uigf_gacha_type=uigf_gacha_type,
+ )
+
+ wb = load_workbook(data)
+ wb_len = len(wb.worksheets)
+
+ if wb_len == 6:
+ import_type = ImportType.PAIMONMOE
+ elif wb_len == 5:
+ import_type = ImportType.UIGF
+ elif wb_len == 4:
+ import_type = ImportType.FXQ
+ else:
+ raise GachaLogFileError("xlsx 格式错误")
+
+ paimonmoe_sheets = {
+ UIGFGachaType.BEGINNER: "Beginners' Wish",
+ UIGFGachaType.STANDARD: "Standard",
+ UIGFGachaType.CHARACTER: "Character Event",
+ UIGFGachaType.WEAPON: "Weapon Event",
+ }
+ fxq_sheets = {
+ UIGFGachaType.BEGINNER: "新手祈愿",
+ UIGFGachaType.STANDARD: "常驻祈愿",
+ UIGFGachaType.CHARACTER: "角色活动祈愿",
+ UIGFGachaType.WEAPON: "武器活动祈愿",
+ }
+ data = UIGFModel(info=UIGFInfo(export_app=import_type.value), list=[])
+ if import_type == ImportType.PAIMONMOE:
+ ws = wb["Information"]
+ if ws["B2"].value != PAIMONMOE_VERSION:
+ raise PaimonMoeGachaLogFileError(file_version=ws["B2"].value, support_version=PAIMONMOE_VERSION)
+ count = 1
+ for gacha_type in paimonmoe_sheets:
+ ws = wb[paimonmoe_sheets[gacha_type]]
+ for row in ws.iter_rows(min_row=2, values_only=True):
+ if row[0] is None:
+ break
+ data.list.append(from_paimon_moe(gacha_type, row[0], row[1], row[2], row[3], count))
+ count += 1
+ elif import_type == ImportType.UIGF:
+ ws = wb["原始数据"]
+ type_map = {}
+ count = 0
+ for row in ws["1"]:
+ if row.value is None:
+ break
+ type_map[row.value] = count
+ count += 1
+ for row in ws.iter_rows(min_row=2, values_only=True):
+ if row[0] is None:
+ break
+ data.list.append(
+ from_uigf(
+ row[type_map["uigf_gacha_type"]],
+ row[type_map["gacha_type"]],
+ row[type_map["item_type"]],
+ row[type_map["name"]],
+ row[type_map["time"]],
+ row[type_map["rank_type"]],
+ row[type_map["id"]],
+ )
+ )
+ else:
+ for gacha_type in fxq_sheets:
+ ws = wb[fxq_sheets[gacha_type]]
+ for row in ws.iter_rows(min_row=2, values_only=True):
+ if row[0] is None:
+ break
+ data.list.append(from_fxq(gacha_type, row[2], row[1], row[0], row[3], row[6]))
+
+ return json.loads(data.json())
diff --git a/modules/gacha_log/models.py b/modules/gacha_log/models.py
new file mode 100644
index 0000000..764dd47
--- /dev/null
+++ b/modules/gacha_log/models.py
@@ -0,0 +1,174 @@
+import datetime
+from enum import Enum
+from typing import List, Dict, Union, Any
+
+from pydantic import BaseModel, validator
+
+from metadata.shortname import roleToId, weaponToId, not_real_roles
+from modules.gacha_log.const import UIGF_VERSION
+
+
+class ImportType(Enum):
+ TGPaimonBot = "TGPaimonBot"
+ PAIMONMOE = "PAIMONMOE"
+ FXQ = "FXQ"
+ UIGF = "UIGF"
+ UNKNOWN = "UNKNOWN"
+
+
+class FiveStarItem(BaseModel):
+ name: str
+ icon: str
+ count: int
+ type: str
+ isUp: bool
+ isBig: bool
+ time: datetime.datetime
+
+
+class FourStarItem(BaseModel):
+ name: str
+ icon: str
+ count: int
+ type: str
+ time: datetime.datetime
+
+
+class GachaItem(BaseModel):
+ id: str
+ name: str
+ gacha_type: str
+ item_type: str
+ rank_type: str
+ time: datetime.datetime
+
+ @validator("name")
+ def name_validator(cls, v):
+ if item_id := (roleToId(v) or weaponToId(v)):
+ if item_id not in not_real_roles:
+ return v
+ raise ValueError("Invalid name")
+
+ @validator("gacha_type")
+ def check_gacha_type(cls, v):
+ if v not in {"100", "200", "301", "302", "400"}:
+ raise ValueError("gacha_type must be 200, 301, 302 or 400")
+ return v
+
+ @validator("item_type")
+ def check_item_type(cls, item):
+ if item not in {"角色", "武器"}:
+ raise ValueError("error item type")
+ return item
+
+ @validator("rank_type")
+ def check_rank_type(cls, rank):
+ if rank not in {"5", "4", "3"}:
+ raise ValueError("error rank type")
+ return rank
+
+
+class GachaLogInfo(BaseModel):
+ user_id: str
+ uid: str
+ update_time: datetime.datetime
+ import_type: str = ""
+ item_list: Dict[str, List[GachaItem]] = {
+ "角色祈愿": [],
+ "武器祈愿": [],
+ "常驻祈愿": [],
+ "新手祈愿": [],
+ }
+
+ @property
+ def get_import_type(self) -> ImportType:
+ try:
+ return ImportType(self.import_type)
+ except ValueError:
+ return ImportType.UNKNOWN
+
+
+class Pool:
+ def __init__(self, five: List[str], four: List[str], name: str, to: str, **kwargs):
+ self.five = five
+ self.real_name = name
+ self.name = "、".join(self.five)
+ self.four = four
+ self.from_ = kwargs.get("from")
+ self.to = to
+ self.from_time = datetime.datetime.strptime(self.from_, "%Y-%m-%d %H:%M:%S")
+ self.to_time = datetime.datetime.strptime(self.to, "%Y-%m-%d %H:%M:%S")
+ self.start = self.from_time
+ self.start_init = False
+ self.end = self.to_time
+ self.dict = {}
+ self.count = 0
+
+ def parse(self, item: Union[FiveStarItem, FourStarItem]):
+ if self.from_time <= item.time <= self.to_time:
+ if self.dict.get(item.name):
+ self.dict[item.name]["count"] += 1
+ else:
+ self.dict[item.name] = {
+ "name": item.name,
+ "icon": item.icon,
+ "count": 1,
+ "rank_type": 5 if isinstance(item, FiveStarItem) else 4,
+ }
+
+ def count_item(self, item: List[GachaItem]):
+ for i in item:
+ if self.from_time <= i.time <= self.to_time:
+ self.count += 1
+ if not self.start_init:
+ self.start = i.time
+ self.end = i.time
+
+ def to_list(self):
+ return list(self.dict.values())
+
+
+class ItemType(Enum):
+ CHARACTER = "角色"
+ WEAPON = "武器"
+
+
+class UIGFGachaType(Enum):
+ BEGINNER = "100"
+ STANDARD = "200"
+ CHARACTER = "301"
+ WEAPON = "302"
+ CHARACTER2 = "400"
+
+
+class UIGFItem(BaseModel):
+ id: str
+ name: str
+ count: str = "1"
+ gacha_type: UIGFGachaType
+ item_id: str = ""
+ item_type: ItemType
+ rank_type: str
+ time: str
+ uigf_gacha_type: UIGFGachaType
+
+
+class UIGFInfo(BaseModel):
+ uid: str = "0"
+ lang: str = "zh-cn"
+ export_time: str = ""
+ export_timestamp: int = 0
+ export_app: str = ""
+ export_app_version: str = ""
+ uigf_version: str = UIGF_VERSION
+
+ def __init__(self, **data: Any):
+ super().__init__(**data)
+ if not self.export_time:
+ self.export_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ self.export_timestamp = int(datetime.datetime.now().timestamp())
+
+
+class UIGFModel(BaseModel):
+ info: UIGFInfo
+ list: List[UIGFItem]
diff --git a/plugins/genshin/gacha/gacha_log.py b/plugins/genshin/gacha/gacha_log.py
index 7043660..b58eb3d 100644
--- a/plugins/genshin/gacha/gacha_log.py
+++ b/plugins/genshin/gacha/gacha_log.py
@@ -1,13 +1,10 @@
import json
-from datetime import datetime
-from enum import Enum
from io import BytesIO
from os import sep
import genshin
+from aiofiles import open as async_open
from genshin.models import BannerType
-from modules.apihelper.gacha_log import GachaLog as GachaLogService
-from openpyxl import load_workbook
from telegram import Update, User, Message, Document, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.constants import ChatAction
from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters, ConversationHandler
@@ -20,8 +17,20 @@ from core.plugin import Plugin, handler, conversation
from core.template import TemplateService
from core.user import UserService
from core.user.error import UserNotFoundError
+from metadata.scripts.paimon_moe import update_paimon_moe_zh
from modules.apihelper.hyperion import SignIn
+from modules.gacha_log.error import (
+ GachaLogInvalidAuthkey,
+ PaimonMoeGachaLogFileError,
+ GachaLogFileError,
+ GachaLogNotFound,
+ GachaLogAccountNotFound,
+ GachaLogMixedProvider,
+)
+from modules.gacha_log.helpers import from_url_get_authkey
+from modules.gacha_log.log import GachaLog
from utils.bot import get_all_args
+from utils.const import PROJECT_ROOT
from utils.decorators.admins import bot_admins_rights_check
from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts
@@ -29,10 +38,13 @@ from utils.helpers import get_genshin_client
from utils.log import logger
from utils.models.base import RegionEnum
+GACHA_LOG_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "gacha_log")
+GACHA_LOG_PAIMON_MOE_PATH = PROJECT_ROOT.joinpath("metadata/data/paimon_moe_zh.json")
+GACHA_LOG_PATH.mkdir(parents=True, exist_ok=True)
INPUT_URL, INPUT_FILE, CONFIRM_DELETE = range(10100, 10103)
-class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
+class GachaLogPlugin(Plugin.Conversation, BasePlugin.Conversation):
"""抽卡记录导入/导出/分析"""
def __init__(
@@ -46,20 +58,17 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
self.user_service = user_service
self.assets_service = assets
self.cookie_service = cookie_service
+ self.zh_dict = None
+ self.gacha_log = GachaLog(GACHA_LOG_PATH)
- @staticmethod
- def from_url_get_authkey(url: str) -> str:
- """从 UEL 解析 authkey
- :param url: URL
- :return: authkey
- """
- try:
- return url.split("authkey=")[1].split("&")[0]
- except IndexError:
- return url
+ async def __async_init__(self):
+ await update_paimon_moe_zh(False)
+ async with async_open(GACHA_LOG_PAIMON_MOE_PATH, "r", encoding="utf-8") as load_f:
+ self.zh_dict = json.loads(await load_f.read())
- @staticmethod
- async def _refresh_user_data(user: User, data: dict = None, authkey: str = None, verify_uid: bool = True) -> str:
+ async def _refresh_user_data(
+ self, user: User, data: dict = None, authkey: str = None, verify_uid: bool = True
+ ) -> str:
"""刷新用户数据
:param user: 用户
:param data: 数据
@@ -70,153 +79,25 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
logger.debug("尝试获取已绑定的原神账号")
client = await get_genshin_client(user.id, need_cookie=False)
if authkey:
- return await GachaLogService.get_gacha_log_data(user.id, client, authkey)
+ new_num = await self.gacha_log.get_gacha_log_data(user.id, client, authkey)
+ return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条抽卡记录"
if data:
- return await GachaLogService.import_gacha_log_data(user.id, client, data, verify_uid)
+ new_num = await self.gacha_log.import_gacha_log_data(user.id, client, data, verify_uid)
+ return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条抽卡记录"
+ except GachaLogNotFound:
+ return "派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~"
+ except GachaLogAccountNotFound:
+ return "导入失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同"
+ except GachaLogFileError:
+ return "导入失败,数据格式错误"
+ except GachaLogInvalidAuthkey:
+ return "更新数据失败,authkey 无效"
+ except GachaLogMixedProvider:
+ return "导入失败,你已经通过其他方式导入过抽卡记录了,本次无法导入"
except UserNotFoundError:
logger.info(f"未查询到用户({user.full_name} {user.id}) 所绑定的账号信息")
return "派蒙没有找到您所绑定的账号信息,请先私聊派蒙绑定账号"
- @staticmethod
- def convert_paimonmoe_to_uigf(data: BytesIO) -> dict:
- """转换 paimone.moe 或 非小酋 导出 xlsx 数据为 UIGF 格式
- :param data: paimon.moe 导出的 xlsx 数据
- :return: UIGF 格式数据
- """
- PAIMONMOE_VERSION = 3
- PM2UIGF_VERSION = 1
- PM2UIGF_NAME = "paimon_moe_to_uigf"
- UIGF_VERSION = "v2.2"
-
- with open(f"resources{sep}json{sep}zh.json", "r") as load_f:
- zh_dict = json.load(load_f)
-
- class XlsxType(Enum):
- PAIMONMOE = 1
- FXQ = 2
-
- class ItemType(Enum):
- CHARACTER = "角色"
- WEAPON = "武器"
-
- class UIGFGachaType(Enum):
- BEGINNER = 100
- STANDARD = 200
- CHARACTER = 301
- WEAPON = 302
-
- class Qiyr:
- def __init__(
- self, uigf_gacha_type: UIGFGachaType, item_type: ItemType, name: str, time: datetime, p: int, _id: int
- ) -> None:
- self.uigf_gacha_type = uigf_gacha_type
- self.item_type = item_type
- self.name = name
- self.time = time
- self.rank_type = p
- self.id = _id
-
- def qy2_json(self):
- return {
- "gacha_type": self.uigf_gacha_type.value, # 注意!
- "item_id": "",
- "count": -1,
- "time": self.time.strftime("%Y-%m-%d %H:%M:%S"),
- "name": self.name,
- "item_type": self.item_type.value,
- "rank_type": self.rank_type,
- "id": self.id,
- "uigf_gacha_type": self.uigf_gacha_type.value,
- }
-
- def from_paimon_moe(uigf_gacha_type: UIGFGachaType, item_type: str, name: str, time: str, p: int) -> Qiyr:
- item_type = ItemType.CHARACTER if item_type == "Character" else ItemType.WEAPON
- name = zh_dict[name]
-
- time = datetime.strptime(time, "%Y-%m-%d %H:%M:%S")
- return Qiyr(uigf_gacha_type, item_type, name, time, p, 0)
-
- def from_fxq(uigf_gacha_type: UIGFGachaType, item_type: str, name: str, time: str, p: int, _id: int) -> Qiyr:
- item_type = ItemType.CHARACTER if item_type == "角色" else ItemType.WEAPON
- time = datetime.strptime(time, "%Y-%m-%d %H:%M:%S")
- return Qiyr(uigf_gacha_type, item_type, name, time, p, _id)
-
- class uigf:
- qiyes: list[Qiyr]
- uid: int
- export_time: datetime
- export_app: str = PM2UIGF_NAME
- export_app_version: str = PM2UIGF_VERSION
- uigf_version = UIGF_VERSION
- lang = "zh-cn"
-
- def __init__(self, qiyes: list[Qiyr], uid: int, export_time: datetime) -> None:
- self.uid = uid
- self.qiyes = qiyes
- self.qiyes.sort(key=lambda x: x.time)
- if self.qiyes[0].id == 0: # 如果是从paimon.moe导入的,那么就给id赋值
- for index, _ in enumerate(self.qiyes):
- self.qiyes[index].id = index + 1
- self.export_time = export_time
- self.export_time = export_time
-
- def export_json(self) -> dict:
- json_d = {
- "info": {
- "uid": self.uid,
- "lang": self.lang,
- "export_time": self.export_time.strftime("%Y-%m-%d %H:%M:%S"),
- "export_timestamp": self.export_time.timestamp(),
- "export_app": self.export_app,
- "export_app_version": self.export_app_version,
- "uigf_version": self.uigf_version,
- },
- "list": [],
- }
- for qiye in self.qiyes:
- json_d["list"].append(qiye.qy2_json())
- return json_d
-
- wb = load_workbook(data)
-
- xlsx_type = XlsxType.PAIMONMOE if len(wb.worksheets) == 6 else XlsxType.FXQ # 判断是paimon.moe还是非小酋导出的
-
- paimonmoe_sheets = {
- UIGFGachaType.BEGINNER: "Beginners' Wish",
- UIGFGachaType.STANDARD: "Standard",
- UIGFGachaType.CHARACTER: "Character Event",
- UIGFGachaType.WEAPON: "Weapon Event",
- }
- fxq_sheets = {
- UIGFGachaType.BEGINNER: "新手祈愿",
- UIGFGachaType.STANDARD: "常驻祈愿",
- UIGFGachaType.CHARACTER: "角色活动祈愿",
- UIGFGachaType.WEAPON: "武器活动祈愿",
- }
- qiyes = []
- if xlsx_type == XlsxType.PAIMONMOE:
- ws = wb["Information"]
- if ws["B2"].value != PAIMONMOE_VERSION:
- raise Exception("PaimonMoe version not supported")
- export_time = datetime.strptime(ws["B3"].value, "%Y-%m-%d %H:%M:%S")
- for gacha_type in paimonmoe_sheets:
- ws = wb[paimonmoe_sheets[gacha_type]]
- for row in ws.iter_rows(min_row=2, values_only=True):
- if row[0] is None:
- break
- qiyes.append(from_paimon_moe(gacha_type, row[0], row[1], row[2], row[3]))
- else:
- export_time = datetime.now()
- for gacha_type in fxq_sheets:
- ws = wb[fxq_sheets[gacha_type]]
- for row in ws.iter_rows(min_row=2, values_only=True):
- if row[0] is None:
- break
- qiyes.append(from_fxq(gacha_type, row[2], row[1], row[0], row[3], row[6]))
-
- u = uigf(qiyes, 0, export_time)
- return u.export_json()
-
async def import_from_file(self, user: User, message: Message, document: Document = None) -> None:
if not document:
document = message.document
@@ -226,9 +107,11 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
elif document.file_name.endswith(".json"):
file_type = "json"
else:
- await message.reply_text("文件格式错误,请发送符合 UIGF 标准的 json 格式的抽卡记录文件或者 paimon.moe、非小酋导出的 xlsx 格式的抽卡记录文件")
+ await message.reply_text("文件格式错误,请发送符合 UIGF 标准的抽卡记录文件或者 paimon.moe、非小酋导出的 xlsx 格式的抽卡记录文件")
+ return
if document.file_size > 2 * 1024 * 1024:
await message.reply_text("文件过大,请发送小于 2 MB 的文件")
+ return
try:
data = BytesIO()
await (await document.get_file()).download(out=data)
@@ -236,9 +119,17 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
# bytesio to json
data = data.getvalue().decode("utf-8")
data = json.loads(data)
- else:
- data = self.convert_paimonmoe_to_uigf(data)
- except UnicodeDecodeError:
+ elif file_type == "xlsx":
+ data = self.gacha_log.convert_xlsx_to_uigf(data, self.zh_dict)
+ except PaimonMoeGachaLogFileError as exc:
+ await message.reply_text(
+ "导入失败,PaimonMoe的抽卡记录当前版本不支持\n" f"支持抽卡记录的版本为 {exc.support_version},你的抽卡记录版本为 {exc.file_version}"
+ )
+ return
+ except GachaLogFileError:
+ await message.reply_text("文件解析失败,请检查文件是否符合 UIGF 标准")
+ return
+ except (KeyError, IndexError, ValueError):
await message.reply_text("文件解析失败,请检查文件编码是否正确或符合 UIGF 标准")
return
except Exception as exc:
@@ -265,7 +156,7 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
user = update.effective_user
args = get_all_args(context)
logger.info(f"用户 {user.full_name}[{user.id}] 导入抽卡记录命令请求")
- authkey = self.from_url_get_authkey(args[0] if args else "")
+ authkey = from_url_get_authkey(args[0] if args else "")
if not args:
if message.document:
await self.import_from_file(user, message)
@@ -299,7 +190,8 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
await message.reply_text(
"开始导入祈愿历史记录:请通过 https://paimon.moe/wish/import 获取抽卡记录链接后发送给我"
"(非 paimon.moe 导出的文件数据)\n\n"
- "> 你还可以向派蒙发送从其他工具导出的 UIGF JSON 标准的记录文件\n"
+ "> 你还可以向派蒙发送从其他工具导出的 UIGF 标准的记录文件\n"
+ "> 或者从 paimon.moe 、非小酋 导出的 xlsx 记录文件\n"
"> 在绑定 Cookie 时添加 stoken 可能有特殊效果哦(仅限国服)\n"
"注意:导入的数据将会与旧数据进行合并。",
parse_mode="html",
@@ -324,7 +216,7 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
if message.document:
await self.import_from_file(user, message)
return ConversationHandler.END
- authkey = self.from_url_get_authkey(message.text)
+ authkey = from_url_get_authkey(message.text)
reply = await message.reply_text("小派蒙正在从米哈游服务器获取数据,请稍后")
await message.reply_chat_action(ChatAction.TYPING)
text = await self._refresh_user_data(user, authkey=authkey)
@@ -346,7 +238,7 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
except UserNotFoundError:
await message.reply_text("你还没有导入抽卡记录哦~")
return ConversationHandler.END
- _, status = await GachaLogService.load_history_info(str(user.id), str(client.uid), only_status=True)
+ _, status = await self.gacha_log.load_history_info(str(user.id), str(client.uid), only_status=True)
if not status:
await message.reply_text("你还没有导入抽卡记录哦~")
return ConversationHandler.END
@@ -361,7 +253,7 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
message = update.effective_message
user = update.effective_user
if message.text == "确定":
- status = await GachaLogService.remove_history_info(str(user.id), str(context.chat_data["uid"]))
+ status = await self.gacha_log.remove_history_info(str(user.id), str(context.chat_data["uid"]))
await message.reply_text("抽卡记录已删除" if status else "抽卡记录删除失败")
return ConversationHandler.END
await message.reply_text("已取消")
@@ -380,12 +272,18 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
if cid < 0:
raise ValueError("Invalid cid")
client = await get_genshin_client(cid, need_cookie=False)
- _, status = await GachaLogService.load_history_info(str(cid), str(client.uid), only_status=True)
+ _, status = await self.gacha_log.load_history_info(str(cid), str(client.uid), only_status=True)
if not status:
await message.reply_text("该用户还没有导入抽卡记录")
return
- status = await GachaLogService.remove_history_info(str(cid), str(client.uid))
+ status = await self.gacha_log.remove_history_info(str(cid), str(client.uid))
await message.reply_text("抽卡记录已强制删除" if status else "抽卡记录删除失败")
+ except GachaLogNotFound:
+ await message.reply_text("派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~")
+ except GachaLogAccountNotFound:
+ await message.reply_text("导入失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同")
+ except GachaLogFileError:
+ await message.reply_text("导入失败,数据格式错误")
except UserNotFoundError:
await message.reply_text("该用户暂未绑定账号")
except (ValueError, IndexError):
@@ -402,12 +300,15 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
try:
client = await get_genshin_client(user.id, need_cookie=False)
await message.reply_chat_action(ChatAction.TYPING)
- state, text, path = await GachaLogService.gacha_log_to_uigf(str(user.id), str(client.uid))
- if state:
- await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT)
- await message.reply_document(document=open(path, "rb+"), caption="抽卡记录导出文件")
- else:
- await message.reply_text(text)
+ path = await self.gacha_log.gacha_log_to_uigf(str(user.id), str(client.uid))
+ await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT)
+ await message.reply_document(document=open(path, "rb+"), caption="抽卡记录导出文件 - UIGF V2.2")
+ except GachaLogNotFound:
+ await message.reply_text("派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~")
+ except GachaLogAccountNotFound:
+ await message.reply_text("导入失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同")
+ except GachaLogFileError:
+ await message.reply_text("导入失败,数据格式错误")
except UserNotFoundError:
logger.info(f"未查询到用户({user.full_name} {user.id}) 所绑定的账号信息")
if filters.ChatType.GROUPS.filter(message):
@@ -433,7 +334,7 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
try:
client = await get_genshin_client(user.id, need_cookie=False)
await message.reply_chat_action(ChatAction.TYPING)
- data = await GachaLogService.get_analysis(user.id, client, pool_type, self.assets_service)
+ data = await self.gacha_log.get_analysis(user.id, client, pool_type, self.assets_service)
if isinstance(data, str):
reply_message = await message.reply_text(data)
if filters.ChatType.GROUPS.filter(message):
@@ -445,6 +346,12 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
"genshin/gacha_log/gacha_log.html", data, full_page=True, query_selector=".body_box"
)
await message.reply_photo(png_data)
+ except GachaLogNotFound:
+ await message.reply_text("派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~")
+ except GachaLogAccountNotFound:
+ await message.reply_text("导入失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同")
+ except GachaLogFileError:
+ await message.reply_text("导入失败,数据格式错误")
except UserNotFoundError:
logger.info(f"未查询到用户({user.full_name} {user.id}) 所绑定的账号信息")
if filters.ChatType.GROUPS.filter(message):
@@ -476,7 +383,7 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
client = await get_genshin_client(user.id, need_cookie=False)
group = filters.ChatType.GROUPS.filter(message)
await message.reply_chat_action(ChatAction.TYPING)
- data = await GachaLogService.get_pool_analysis(user.id, client, pool_type, self.assets_service, group)
+ data = await self.gacha_log.get_pool_analysis(user.id, client, pool_type, self.assets_service, group)
if isinstance(data, str):
reply_message = await message.reply_text(data)
if filters.ChatType.GROUPS.filter(message):
@@ -496,6 +403,12 @@ class GachaLog(Plugin.Conversation, BasePlugin.Conversation):
else:
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
await message.reply_photo(png_data)
+ except GachaLogNotFound:
+ await message.reply_text("派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~")
+ except GachaLogAccountNotFound:
+ await message.reply_text("导入失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同")
+ except GachaLogFileError:
+ await message.reply_text("导入失败,数据格式错误")
except (UserNotFoundError, CookiesNotFoundError):
logger.info(f"未查询到用户({user.full_name} {user.id}) 所绑定的账号信息")
if filters.ChatType.GROUPS.filter(message):
diff --git a/plugins/system/get_chat.py b/plugins/system/get_chat.py
index b2e275e..db2023f 100644
--- a/plugins/system/get_chat.py
+++ b/plugins/system/get_chat.py
@@ -11,7 +11,7 @@ from core.plugin import Plugin, handler
from core.sign import SignServices
from core.user import UserService
from core.user.error import UserNotFoundError
-from modules.apihelper.gacha_log import GachaLog
+from modules.gacha_log.log import GachaLog
from utils.bot import get_all_args
from utils.decorators.admins import bot_admins_rights_check
from utils.helpers import get_genshin_client
diff --git a/plugins/system/refresh_metadata.py b/plugins/system/refresh_metadata.py
index eaf0270..45823a2 100644
--- a/plugins/system/refresh_metadata.py
+++ b/plugins/system/refresh_metadata.py
@@ -3,6 +3,7 @@ from telegram import Update
from core.plugin import Plugin, handler
from metadata.scripts.honey import update_honey_metadata
from metadata.scripts.metadatas import update_metadata_from_ambr, update_metadata_from_github
+from metadata.scripts.paimon_moe import update_paimon_moe_zh
from utils.decorators.admins import bot_admins_rights_check
from utils.log import logger
@@ -19,6 +20,7 @@ class MetadataPlugin(Plugin):
msg = await message.reply_text("正在刷新元数据,请耐心等待...")
logger.info("正在从 github 上获取元数据")
await update_metadata_from_github()
+ await update_paimon_moe_zh()
logger.info("正在从 ambr 上获取元数据")
await update_metadata_from_ambr()
logger.info("正在从 honey 上获取元数据")
diff --git a/resources/json/zh.json b/resources/json/zh.json
deleted file mode 100644
index c7dbbdb..0000000
--- a/resources/json/zh.json
+++ /dev/null
@@ -1 +0,0 @@
-{"Kate":"凯特","Kamisato Ayaka":"神里绫华","Jean":"琴","Traveler":"旅行者","Lisa":"丽莎","Barbara":"芭芭拉","Kaeya":"凯亚","Diluc":"迪卢克","Razor":"雷泽","Amber":"安柏","Venti":"温迪","Xiangling":"香菱","Beidou":"北斗","Xingqiu":"行秋","Xiao":"魈","Ningguang":"凝光","Klee":"可莉","Zhongli":"钟离","Fischl":"菲谢尔","Bennett":"班尼特","Tartaglia":"达达利亚","Noelle":"诺艾尔","Qiqi":"七七","Chongyun":"重云","Ganyu":"甘雨","Albedo":"阿贝多","Diona":"迪奥娜","Mona":"莫娜","Keqing":"刻晴","Sucrose":"砂糖","Xinyan":"辛焱","Rosaria":"罗莎莉亚","Hu Tao":"胡桃","Kaedehara Kazuha":"枫原万叶","Yanfei":"烟绯","Yoimiya":"宵宫","Thoma":"托马","Eula":"优菈","Raiden Shogun":"雷电将军","Sayu":"早柚","Sangonomiya Kokomi":"珊瑚宫心海","Gorou":"五郎","Kujou Sara":"九条裟罗","Arataki Itto":"荒泷一斗","Yae Miko":"八重神子","Shikanoin Heizou":"鹿野院平藏","Yelan":"夜兰","Aloy":"埃洛伊","Shenhe":"申鹤","Yun Jin":"云堇","Kuki Shinobu":"久岐忍","Kamisato Ayato":"神里绫人","Collei":"柯莱","Dori":"多莉","Tighnari":"提纳里","Nilou":"妮露","Cyno":"赛诺","Candace":"坎蒂丝","Naked Model #1":"裸模1号","Naked Man":"裸男","Akuliya":"阿葵丽雅","Yaoyao":"瑶瑶","Shiro Maiden":"白盒少女","Greatsword Maiden":"大剑少女","Lance Warrioress":"长枪成女","Rx White-Box":"Rx白盒","Female Lead New Normal Attack":"女主新普攻","Male Lead New Normal Attack":"男主新普攻","Dull Blade":"无锋剑","Silver Sword":"银剑","Cool Steel":"冷刃","Harbinger of Dawn":"黎明神剑","Traveler's Handy Sword":"旅行剑","Dark Iron Sword":"暗铁剑","Fillet Blade":"吃虎鱼刀","Skyrider Sword":"飞天御剑","Favonius Sword":"西风剑","The Flute":"笛剑","Sacrificial Sword":"祭礼剑","Royal Longsword":"宗室长剑","Lion's Roar":"匣里龙吟","Prototype Rancour":"试作斩岩","Iron Sting":"铁蜂刺","Blackcliff Longsword":"黑岩长剑","The Black Sword":"黑剑","The Alley Flash":"暗巷闪光","Sword of Descension":"降临之剑","Festering Desire":"腐殖之剑","Amenoma Kageuchi":"天目影打刀","Cinnabar Spindle":"辰砂之纺锤","Kagotsurube Isshin":"笼钓瓶一心","Sapwood Blade":"原木刀","Xiphos' Moonlight":"西福斯的月光","Prized Isshin Blade":"「一心传」名刀","Aquila Favonia":"风鹰剑","Skyward Blade":"天空之刃","Freedom-Sworn":"苍古自由之誓","Summit Shaper":"斫峰之刃","Primordial Jade Cutter":"磐岩结绿","Mistsplitter Reforged":"雾切之回光","Haran Geppaku Futsu":"波乱月白经津","Key of Khaj-Nisut":"圣显之钥","Waster Greatsword":"训练大剑","Old Merc's Pal":"佣兵重剑","Ferrous Shadow":"铁影阔剑","Bloodtainted Greatsword":"沐浴龙血的剑","White Iron Greatsword":"白铁大剑","Quartz":"石英大剑","Debate Club":"以理服人","Skyrider Greatsword":"飞天大御剑","Favonius Greatsword":"西风大剑","The Bell":"钟剑","Sacrificial Greatsword":"祭礼大剑","Royal Greatsword":"宗室大剑","Rainslasher":"雨裁","Prototype Archaic":"试作古华","Whiteblind":"白影剑","Blackcliff Slasher":"黑岩斩刀","Serpent Spine":"螭骨剑","Lithic Blade":"千岩古剑","Snow-Tombed Starsilver":"雪葬的星银","Luxurious Sea-Lord":"衔珠海皇","Katsuragikiri Nagamasa":"桂木斩长正","Makhaira Aquamarine":"玛海菈的水色","Akuoumaru":"恶王丸","Forest Regalia":"森林王器","Skyward Pride":"天空之傲","Wolf's Gravestone":"狼的末路","Song of Broken Pines":"松籁响起之时","The Unforged":"无工之剑","Redhorn Stonethresher":"赤角石溃杵","Beginner's Protector":"新手长枪","Iron Point":"铁尖枪","White Tassel":"白缨枪","Halberd":"钺矛","Black Tassel":"黑缨枪","The Flagstaff":"「旗杆」","Dragon's Bane":"匣里灭辰","Prototype Starglitter":"试作星镰","Crescent Pike":"流月针","Blackcliff Pole":"黑岩刺枪","Deathmatch":"决斗之枪","Lithic Spear":"千岩长枪","Favonius Lance":"西风长枪","Royal Spear":"宗室猎枪","Dragonspine Spear":"龙脊长枪","Kitain Cross Spear":"喜多院十文字","\"The Catch\"":"「渔获」","Wavebreaker's Fin":"断浪长鳍","Moonpiercer":"贯月矢","Missive Windspear":"风信之锋","Staff of Homa":"护摩之杖","Skyward Spine":"天空之脊","Vortex Vanquisher":"贯虹之槊","Primordial Jade Winged-Spear":"和璞鸢","Calamity Queller":"息灾","Engulfing Lightning":"薙草之稻光","Staff of the Scarlet Sands":"赤沙之杖","Apprentice's Notes":"学徒笔记","Pocket Grimoire":"口袋魔导书","Magic Guide":"魔导绪论","Thrilling Tales of Dragon Slayers":"讨龙英杰谭","Otherworldly Story":"异世界行记","Emerald Orb":"翡玉法球","Twin Nephrite":"甲级宝珏","Amber Bead":"琥珀玥","Favonius Codex":"西风秘典","The Widsith":"流浪乐章","Sacrificial Fragments":"祭礼残章","Royal Grimoire":"宗室秘法录","Solar Pearl":"匣里日月","Prototype Amber":"试作金珀","Mappa Mare":"万国诸海图谱","Blackcliff Agate":"黑岩绯玉","Eye of Perception":"昭心","Wine and Song":"暗巷的酒与诗","Frostbearer":"忍冬之果","Dodoco Tales":"嘟嘟可故事集","Hakushin Ring":"白辰之环","Oathsworn Eye":"证誓之明瞳","Wandering Evenstar":"流浪的晚星","Fruit of Fulfillment":"盈满之实","Skyward Atlas":"天空之卷","Lost Prayer to the Sacred Winds":"四风原典","Memory of Dust":"尘世之锁","Everlasting Moonglow":"不灭月华","Kagura's Verity":"神乐之真意","Hunter's Bow":"猎弓","Seasoned Hunter's Bow":"历练的猎弓","Raven Bow":"鸦羽弓","Sharpshooter's Oath":"神射手之誓","Recurve Bow":"反曲弓","Slingshot":"弹弓","Messenger":"信使","Ebony Bow":"黑檀弓","Favonius Warbow":"西风猎弓","The Stringless":"绝弦","Sacrificial Bow":"祭礼弓","Royal Bow":"宗室长弓","Rust":"弓藏","Prototype Crescent":"试作澹月","Compound Bow":"钢轮弓","Blackcliff Warbow":"黑岩战弓","The Viridescent Hunt":"苍翠猎弓","Alley Hunter":"暗巷猎手","Fading Twilight":"落霞","Mitternachts Waltz":"幽夜华尔兹","Windblume Ode":"风花之颂","Hamayumi":"破魔之弓","Predator":"掠食者","Mouun's Moon":"曚云之月","King's Squire":"王下近侍","End of the Line":"竭泽","Skyward Harp":"天空之翼","Amos' Bow":"阿莫斯之弓","Elegy for the End":"终末嗟叹之诗","Polar Star":"冬极白星","Aqua Simulacra":"若水","Thundering Pulse":"飞雷之弦振","Hunter's Path":"猎人之径","Adventurer":"冒险家","Lucky Dog":"幸运儿","Traveling Doctor":"游医","Resolution of Sojourner":"行者之心","Tiny Miracle":"奇迹","Berserker":"战狂","Instructor":"教官","The Exile":"流放者","Defender's Will":"守护之心","Brave Heart":"勇士之心","Martial Artist":"武人","Gambler":"赌徒","Scholar":"学士","Prayers for Illumination":"祭火之人","Prayers for Destiny":"祭水之人","Prayers for Wisdom":"祭雷之人","Prayers to Springtime":"祭冰之人","Blizzard Strayer":"冰风迷途的勇士","Thundersoother":"平息鸣雷的尊者","Lavawalker":"渡过烈火的贤人","Maiden Beloved":"被怜爱的少女","Gladiator's Finale":"角斗士的终幕礼","Viridescent Venerer":"翠绿之影","Wanderer's Troupe":"流浪大地的乐团","Thundering Fury":"如雷的盛怒","Crimson Witch of Flames":"炽烈的炎之魔女","Noblesse Oblige":"昔日宗室之仪","Bloodstained Chivalry":"染血的骑士道","Archaic Petra":"悠古的磐岩","Retracing Bolide":"逆飞的流星","Heart of Depth":"沉沦之心","Tenacity of the Millelith":"千岩牢固","Pale Flame":"苍白之火","Shimenawa's Reminiscence":"追忆之注连","Emblem of Severed Fate":"绝缘之旗印","Husk of Opulent Dreams":"华馆梦醒形骸记","Ocean-Hued Clam":"海染砗磲","Vermillion Hereafter":"辰砂往生录","Echoes of an Offering":"来歆余响","Deepwood Memories":"深林的记忆","Gilded Dreams":"饰金之梦","Mystic Enhancement Ore":"精锻用魔矿","Fine Enhancement Ore":"精锻用良矿","Enhancement Ore":"精锻用杂矿","Mora":"摩拉","Hero's Wit":"大英雄的经验","Adventurer's Experience":"冒险家的经验","Wanderer's Advice":"流浪者的经验","Crown of Insight":"智识之冕","Fetters of the Dandelion Gladiator":"狮牙斗士的枷锁","Chaos Device":"混沌装置","Divining Scroll":"导能绘卷","Chains of the Dandelion Gladiator":"狮牙斗士的铁链","Chaos Circuit":"混沌回路","Sealed Scroll":"封魔绘卷","Shackles of the Dandelion Gladiator":"狮牙斗士的镣铐","Chaos Core":"混沌炉心","Forbidden Curse Scroll":"禁咒绘卷","Dream of the Dandelion Gladiator":"狮牙斗士的理想","Tile of Decarabian's Tower":"高塔孤王的破瓦","Heavy Horn":"沉重号角","Firm Arrowhead":"牢固的箭簇","Debris of Decarabian's City":"高塔孤王的残垣","Black Bronze Horn":"黑铜号角","Sharp Arrowhead":"锐利的箭簇","Fragment of Decarabian's Epic":"高塔孤王的断片","Black Crystal Horn":"黑晶号角","Weathered Arrowhead":"历战的箭簇","Scattered Piece of Decarabian's Dream":"高塔孤王的碎梦","Slime Condensate":"史莱姆凝液","Slime Secretions":"史莱姆清","Slime Concentrate":"史莱姆原浆","Boreal Wolf's Milk Tooth":"凛风奔狼的始龀","Dead Ley Line Branch":"地脉的旧枝","Boreal Wolf's Cracked Tooth":"凛风奔狼的裂齿","Dead Ley Line Leaves":"地脉的枯叶","Boreal Wolf's Broken Fang":"凛风奔狼的断牙","Boreal Wolf's Nostalgia":"凛风奔狼的怀乡","Grain of Aerosiderite":"漆黑陨铁的一粒","Fragile Bone Shard":"脆弱的骨片","Damaged Mask":"破损的面具","Piece of Aerosiderite":"漆黑陨铁的一片","Sturdy Bone Shard":"结实的骨片","Stained Mask":"污秽的面具","Bit of Aerosiderite":"漆黑陨铁的一角","Fossilized Bone Shard":"石化的骨片","Ominous Mask":"不祥的面具","Chunk of Aerosiderite":"漆黑陨铁的一块","Mist Veiled Lead Elixir":"雾海云间的铅丹","Mist Grass Pollen":"雾虚花粉","Treasure Hoarder Insignia":"寻宝鸦印","Mist Veiled Mercury Elixir":"雾海云间的汞丹","Mist Grass":"雾虚草囊","Silver Raven Insignia":"藏银鸦印","Mist Veiled Gold Elixir":"雾海云间的金丹","Mist Grass Wick":"雾虚灯芯","Golden Raven Insignia":"攫金鸦印","Mist Veiled Primo Elixir":"雾海云间的转还","Luminous Sands from Guyun":"孤云寒林的光砂","Hunter's Sacrificial Knife":"猎兵祭刀","Recruit's Insignia":"新兵的徽记","Lustrous Stone from Guyun":"孤云寒林的辉岩","Agent's Sacrificial Knife":"特工祭刀","Sergeant's Insignia":"士官的徽记","Relic from Guyun":"孤云寒林的圣骸","Inspector's Sacrificial Knife":"督察长祭刀","Lieutenant's Insignia":"尉官的徽记","Divine Body from Guyun":"孤云寒林的神体","Whopperflower Nectar":"骗骗花蜜","Shimmering Nectar":"微光花蜜","Energy Nectar":"原素花蜜","Prithiva Topaz Sliver":"坚牢黄玉碎屑","Cecilia":"塞西莉亚花","Prithiva Topaz Fragment":"坚牢黄玉断片","Basalt Pillar":"玄岩之塔","Prithiva Topaz Chunk":"坚牢黄玉块","Prithiva Topaz Gemstone":"坚牢黄玉","Teachings of Ballad":"「诗文」的教导","Guide to Ballad":"「诗文」的指引","Philosophies of Ballad":"「诗文」的哲学","Tusk of Monoceros Caeli":"吞天之鲸·只角","Agnidus Agate Sliver":"燃愿玛瑙碎屑","Small Lamp Grass":"小灯草","Agnidus Agate Fragment":"燃愿玛瑙断片","Everflame Seed":"常燃火种","Agnidus Agate Chunk":"燃愿玛瑙块","Agnidus Agate Gemstone":"燃愿玛瑙","Teachings of Freedom":"「自由」的教导","Guide to Freedom":"「自由」的指引","Philosophies of Freedom":"「自由」的哲学","Dvalin's Sigh":"东风的吐息","Varunada Lazurite Sliver":"涤净青金碎屑","Philanemo Mushroom":"慕风蘑菇","Varunada Lazurite Fragment":"涤净青金断片","Cleansing Heart":"净水之心","Varunada Lazurite Chunk":"涤净青金块","Varunada Lazurite Gemstone":"涤净青金","Ring of Boreas":"北风之环","Vajrada Amethyst Sliver":"最胜紫晶碎屑","Noctilucous Jade":"夜泊石","Vajrada Amethyst Fragment":"最胜紫晶断片","Lightning Prism":"雷光棱镜","Vajrada Amethyst Chunk":"最胜紫晶块","Vajrada Amethyst Gemstone":"最胜紫晶","Teachings of Gold":"「黄金」的教导","Guide to Gold":"「黄金」的指引","Philosophies of Gold":"「黄金」的哲学","Windwheel Aster":"风车菊","Teachings of Resistance":"「抗争」的教导","Guide to Resistance":"「抗争」的指引","Philosophies of Resistance":"「抗争」的哲学","Dvalin's Plume":"东风之翎","Shivada Jade Sliver":"哀叙冰玉碎屑","Cor Lapis":"石珀","Shivada Jade Fragment":"哀叙冰玉断片","Hoarfrost Core":"极寒之核","Shivada Jade Chunk":"哀叙冰玉块","Shivada Jade Gemstone":"哀叙冰玉","Teachings of Diligence":"「勤劳」的教导","Guide to Diligence":"「勤劳」的指引","Philosophies of Diligence":"「勤劳」的哲学","Calla Lily":"嘟嘟莲","Shard of a Foul Legacy":"魔王之刃·残片","Spirit Locket of Boreas":"北风的魂匣","Vayuda Turquoise Sliver":"自在松石碎屑","Dandelion Seed":"蒲公英籽","Vayuda Turquoise Fragment":"自在松石断片","Hurricane Seed":"飓风之种","Vayuda Turquoise Chunk":"自在松石块","Vayuda Turquoise Gemstone":"自在松石","Teachings of Prosperity":"「繁荣」的教导","Guide to Prosperity":"「繁荣」的指引","Philosophies of Prosperity":"「繁荣」的哲学","Valberry":"落落莓","Dvalin's Claw":"东风之爪","Glaze Lily":"琉璃百合","Violetgrass":"琉璃袋","Tail of Boreas":"北风之尾","Wolfhook":"钩钩果","Starconch":"星螺","Brilliant Diamond Sliver":"璀璨原钻碎屑","Brilliant Diamond Fragment":"璀璨原钻断片","Brilliant Diamond Chunk":"璀璨原钻块","Brilliant Diamond Gemstone":"璀璨原钻","Jueyun Chili":"绝云椒椒","Silk Flower":"霓裳花","Qingxin":"清心","Shadow of the Warrior":"武炼之魂·孤影","Juvenile Jade":"未熟之玉","Bloodjade Branch":"血玉之枝","Crystalline Bloom":"晶凝之华","Dragon Lord's Crown":"龙王之冕","Sea Ganoderma":"海灵芝","Marionette Core":"魔偶机心","Gilded Scale":"鎏金之鳞","Coral Branch of a Distant Sea":"远海夷地的瑚枝","Jeweled Branch of a Distant Sea":"远海夷地的玉枝","Jade Branch of a Distant Sea":"远海夷地的琼枝","Narukami's Wisdom":"鸣神御灵的明惠","Narukami's Joy":"鸣神御灵的欢喜","Narukami's Affection":"鸣神御灵的亲爱","Narukami's Valor":"鸣神御灵的勇武","Mask of the Wicked Lieutenant":"今昔剧画之恶尉","Mask of the Tiger's Bite":"今昔剧画之虎啮","Mask of the One-Horned":"今昔剧画之一角","Mask of the Kijin":"今昔剧画之鬼人","Teachings of Transience":"「浮世」的教导","Guide to Transience":"「浮世」的指引","Philosophies of Transience":"「浮世」的哲学","Teachings of Elegance":"「风雅」的教导","Guide to Elegance":"「风雅」的指引","Philosophies of Elegance":"「风雅」的哲学","Teachings of Light":"「天光」的教导","Guide to Light":"「天光」的指引","Philosophies of Light":"「天光」的哲学","Perpetual Heart":"恒常机关之心","Smoldering Pearl":"阴燃之珠","Old Handguard":"破旧的刀镡","Kageuchi Handguard":"影打刀镡","Famed Handguard":"名刀镡","Chaos Gear":"混沌机关","Chaos Axis":"混沌枢纽","Chaos Oculus":"混沌真眼","Dismal Prism":"黯淡棱镜","Crystal Prism":"水晶棱镜","Polarizing Prism":"偏光棱镜","Sakura Bloom":"绯樱绣球","Crystal Marrow":"晶化骨髓","Dendrobium":"血斛","Naku Weed":"鸣草","Amakumo Fruit":"天云草实","Storm Beads":"雷霆数珠","Molten Moment":"熔毁之刻","Ashen Heart":"灰烬之心","Spectral Husk":"浮游干核","Spectral Heart":"浮游幽核","Spectral Nucleus":"浮游晶化核","Sango Pearl":"珊瑚真珠","Dew of Repudiation":"排异之露","Hellfire Butterfly":"狱火之蝶","Concealed Claw":"隐兽指爪","Concealed Unguis":"隐兽利爪","Concealed Talon":"隐兽鬼爪","Fluorescent Fungus":"幽灯蕈","Onikabuto":"鬼兜虫","Riftborn Regalia":"兽境王器","Dragonheir's False Fin":"龙嗣伪鳍","The Meaning of Aeons":"万劫之真意","Mudra of the Malefic General":"凶将之手眼","Gloomy Statuette":"晦暗刻像","Dark Statuette":"夤夜刻像","Deathly Statuette":"幽邃刻像","Tears of the Calamitous God":"祸神之禊泪","Runic Fang":"符纹之齿","Teachings of Admonition":"「诤言」的教导","Guide to Admonition":"「诤言」的指引","Philosophies of Admonition":"「诤言」的哲学","Teachings of Ingenuity":"「巧思」的教导","Guide to Ingenuity":"「巧思」的指引","Philosophies of Ingenuity":"「巧思」的哲学","Teachings of Praxis":"「笃行」的教导","Guide to Praxis":"「笃行」的指引","Philosophies of Praxis":"「笃行」的哲学","Nagadus Emerald Sliver":"生长碧翡碎屑","Nagadus Emerald Fragment":"生长碧翡断片","Nagadus Emerald Chunk":"生长碧翡块","Nagadus Emerald Gemstone":"生长碧翡","Majestic Hooked Beak":"蕈王钩喙","Nilotpala Lotus":"月莲","Fungal Spores":"蕈兽孢子","Luminescent Pollen":"荧光孢粉","Crystalline Cyst Dust":"孢囊晶尘","Rukkhashava Mushrooms":"树王圣体菇","Echo of Scorching Might":"烈日威权的残响","Remnant Glow of Scorching Might":"烈日威权的余光","Dream of Scorching Might":"烈日威权的梦想","Olden Days of Scorching Might":"烈日威权的旧日","Inactivated Fungal Nucleus":"失活菌核","Dormant Fungal Nucleus":"休眠菌核","Robust Fungal Nucleus":"茁壮菌核","Faded Red Satin":"褪色红绸","Trimmed Red Silk":"镶边红绸","Rich Red Brocade":"织金红绸","Copper Talisman of the Forest Dew":"谧林涓露的铜符","Iron Talisman of the Forest Dew":"谧林涓露的铁符","Silver Talisman of the Forest Dew":"谧林涓露的银符","Golden Talisman of the Forest Dew":"谧林涓露的金符","Chaos Storage":"混沌容器","Chaos Module":"混沌模块","Chaos Bolt":"混沌锚栓","Oasis Garden's Reminiscence":"绿洲花园的追忆","Oasis Garden's Kindness":"绿洲花园的恩惠","Oasis Garden's Mourning":"绿洲花园的哀思","Oasis Garden's Truth":"绿洲花园的真谛","Traveler (Electro)":"旅行者 (雷元素)","Traveler (Anemo)":"旅行者 (风元素)","Traveler (Geo)":"旅行者 (岩元素)","Traveler (Dendro)":"旅行者 (草)"}
\ No newline at end of file