├── data ├── gsmaterial │ ├── avatar │ │ └── .gitkeep │ ├── item │ │ └── .gitkeep │ ├── weapon │ │ └── .gitkeep │ ├── sub.json │ ├── draw │ │ ├── HYWH-65W.ttf │ │ ├── bg3.140.png │ │ ├── bg4.140.png │ │ ├── bg5.140.png │ │ └── SmileySans-Oblique.ttf │ ├── cache │ │ ├── daily.1.all.jpg │ │ ├── daily.2.all.jpg │ │ ├── daily.3.all.jpg │ │ ├── weekly.all.jpg │ │ ├── weekly.「公子」.jpg │ │ ├── weekly.「女士」.jpg │ │ ├── weekly.安德留斯.jpg │ │ ├── weekly.若陀龙王.jpg │ │ ├── daily.1.avatar.jpg │ │ ├── daily.1.weapon.jpg │ │ ├── daily.2.avatar.jpg │ │ ├── daily.2.weapon.jpg │ │ ├── daily.3.avatar.jpg │ │ ├── daily.3.weapon.jpg │ │ ├── weekly.「正机之神」.jpg │ │ ├── weekly.祸津御建鸣神命.jpg │ │ ├── weekly.风魔龙·特瓦林.jpg │ │ └── weekly.阿佩普的绿洲守望者.jpg │ ├── cookie.json │ ├── config.json │ └── item-alias.json └── 资源压缩包下载地址.txt ├── .gitignore ├── pyproject.toml ├── .github ├── workflows │ ├── publish.yml │ ├── build.yml │ └── update-alias.yml └── update-alias.py ├── LICENSE ├── setup.py ├── nonebot_plugin_gsmaterial ├── __init__.py ├── config.py ├── material_draw.py └── data_source.py └── README.md /data/gsmaterial/avatar/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/gsmaterial/item/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/gsmaterial/weapon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | poetry.lock 2 | venv 3 | test 4 | -------------------------------------------------------------------------------- /data/gsmaterial/sub.json: -------------------------------------------------------------------------------- 1 | { 2 | "群组": [], 3 | "私聊": [] 4 | } -------------------------------------------------------------------------------- /data/资源压缩包下载地址.txt: -------------------------------------------------------------------------------- 1 | https://cdn.monsterx.cn/bot/gsmaterial/gsmaterial.zip -------------------------------------------------------------------------------- /data/gsmaterial/draw/HYWH-65W.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/draw/HYWH-65W.ttf -------------------------------------------------------------------------------- /data/gsmaterial/draw/bg3.140.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/draw/bg3.140.png -------------------------------------------------------------------------------- /data/gsmaterial/draw/bg4.140.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/draw/bg4.140.png -------------------------------------------------------------------------------- /data/gsmaterial/draw/bg5.140.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/draw/bg5.140.png -------------------------------------------------------------------------------- /data/gsmaterial/cache/daily.1.all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/daily.1.all.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/daily.2.all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/daily.2.all.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/daily.3.all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/daily.3.all.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/weekly.all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/weekly.all.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/weekly.「公子」.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/weekly.「公子」.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/weekly.「女士」.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/weekly.「女士」.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/weekly.安德留斯.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/weekly.安德留斯.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/weekly.若陀龙王.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/weekly.若陀龙王.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/daily.1.avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/daily.1.avatar.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/daily.1.weapon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/daily.1.weapon.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/daily.2.avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/daily.2.avatar.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/daily.2.weapon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/daily.2.weapon.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/daily.3.avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/daily.3.avatar.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/daily.3.weapon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/daily.3.weapon.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/weekly.「正机之神」.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/weekly.「正机之神」.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/weekly.祸津御建鸣神命.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/weekly.祸津御建鸣神命.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/weekly.风魔龙·特瓦林.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/weekly.风魔龙·特瓦林.jpg -------------------------------------------------------------------------------- /data/gsmaterial/cache/weekly.阿佩普的绿洲守望者.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/cache/weekly.阿佩普的绿洲守望者.jpg -------------------------------------------------------------------------------- /data/gsmaterial/draw/SmileySans-Oblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsterxcn/nonebot-plugin-gsmaterial/HEAD/data/gsmaterial/draw/SmileySans-Oblique.ttf -------------------------------------------------------------------------------- /data/gsmaterial/cookie.json: -------------------------------------------------------------------------------- 1 | { 2 | "account_id": "272894075", 3 | "cookie_token": "PV6zzXj28UUSUHetJZO2sqEff4sqwdzDAA3Wz3xY", 4 | "stoken": "5CzsKTYLuoCy4Pf5t7y3bHkS0MjljkOm89rOYfGh" 5 | } 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nonebot-plugin-gsmaterial" 3 | version = "0.2.9" 4 | description = "Genshin daily material plugin for NoneBot2" 5 | authors = ["monsterxcn "] 6 | documentation = "https://github.com/monsterxcn/nonebot-plugin-gsmaterial#readme" 7 | license = "MIT" 8 | homepage = "https://github.com/monsterxcn/nonebot-plugin-gsmaterial" 9 | readme = "README.md" 10 | keywords = ["nonebot", "nonebot2", "genshin", "material"] 11 | 12 | [tool.poetry.dependencies] 13 | python = ">=3.8,<4.0" 14 | nonebot2 = ">=2.0.0a16" 15 | nonebot-adapter-onebot = ">=2.0.0b1" 16 | nonebot-plugin-apscheduler = ">=0.1.0" 17 | httpx = ">=0.20.0, <1.0.0" 18 | Pillow = "^9.2.0" 19 | pytz = "*" 20 | 21 | [tool.poetry.dev-dependencies] 22 | 23 | [build-system] 24 | requires = ["poetry-core>=1.0.0"] 25 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # https://clownote.github.io/2021/01/16/blog/PyPiGitHubActions/ 2 | 3 | name: Publish Python 🐍 distributions 📦 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-n-publish: 10 | name: 🐍📦 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Set up Python 3.8 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: 3.8 18 | - name: Install pypa/build 19 | run: >- 20 | python -m 21 | pip install 22 | build 23 | --user 24 | - name: Build a binary wheel and a source tarball 25 | run: >- 26 | python -m 27 | build 28 | --sdist 29 | --wheel 30 | --outdir dist/ 31 | . 32 | - name: Publish distribution 📦 to PyPI 33 | uses: pypa/gh-action-pypi-publish@master 34 | with: 35 | password: ${{ secrets.PYPI_API_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build distributions 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: Build distributions 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Set up Python 3.8 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.8 16 | - name: Install pypa/build 17 | run: >- 18 | python -m 19 | pip install 20 | build 21 | --user 22 | - name: Build binary wheel and source tarball 23 | run: >- 24 | python -m 25 | build 26 | --sdist 27 | --wheel 28 | --outdir dist/ 29 | . 30 | - name: Publish distribution 31 | uses: actions/upload-artifact@v2 32 | with: 33 | name: binary wheel and source tarball 34 | path: | 35 | dist/*.whl 36 | dist/*.tar.gz 37 | if-no-files-found: error 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 monsterxcn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="nonebot-plugin-gsmaterial", 8 | version="0.2.9", 9 | author="monsterxcn", 10 | author_email="monsterxcn@gmail.com", 11 | description="Genshin daily material plugin for NoneBot2", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/monsterxcn/nonebot-plugin-gsmaterial", 15 | project_urls={ 16 | "Bug Tracker": "https://github.com/monsterxcn/nonebot-plugin-gsmaterial/issues", 17 | }, 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | ], 23 | packages=["nonebot-plugin-gsmaterial"], 24 | python_requires=">=3.8,<4.0", 25 | install_requires=[ 26 | "nonebot2>=2.0.0a16", 27 | "nonebot-adapter-onebot>=2.0.0b1", 28 | "nonebot-plugin-apscheduler>=0.1.0", 29 | "httpx>=0.20.0,<1.0.0", 30 | "Pillow>=9.2.0", 31 | "pytz", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /.github/workflows/update-alias.yml: -------------------------------------------------------------------------------- 1 | name: Update alias 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | update: 8 | name: Update alias 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repo 12 | uses: actions/checkout@master 13 | 14 | - name: Setup python 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: 3.8 18 | 19 | - name: Update files 20 | env: 21 | GAMEDATA: ${{ secrets.GAME_DATA_REPO }} 22 | run: | 23 | cd .github 24 | wget -q "${GAMEDATA}/ExcelBinOutput/AvatarExcelConfigData.json" 25 | wget -q "${GAMEDATA}/ExcelBinOutput/WeaponExcelConfigData.json" 26 | wget -q "${GAMEDATA}/TextMap/TextMapCHS.json" 27 | wget -q https://github.com/monsterxcn/nonebot-plugin-gspanel/raw/main/data/gspanel/char-alias.json 28 | wget -q https://github.com/ctrlcvs/xiaoyao-cvs-plugin/raw/master/resources/Atlas_alias/wuqi_tujian.yaml 29 | wget -q https://github.com/DGP-Studio/Snap.Metadata/raw/main/Genshin/CHS/Avatar.json 30 | python update-alias.py 31 | mv -f item-alias.json ../data/gsmaterial/item-alias.json 32 | 33 | - name: Upload files 34 | uses: tvrcgo/upload-to-oss@master 35 | with: 36 | key-id: ${{ secrets.OSS_KEY_ID }} 37 | key-secret: ${{ secrets.OSS_KEY_SECRET }} 38 | region: oss-cn-shanghai 39 | bucket: monsterx 40 | assets: | 41 | data/gsmaterial/item-alias.json:/bot/gsmaterial/item-alias.json 42 | 43 | - name: Commit changes 44 | uses: EndBug/add-and-commit@v9 45 | with: 46 | author_name: github-actions[bot] 47 | author_email: github-actions[bot]@users.noreply.github.com 48 | message: ':wrench: 自动更新别名数据' 49 | add: | 50 | 'data/gsmaterial/item-alias.json' 51 | -------------------------------------------------------------------------------- /.github/update-alias.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | 5 | def read(fn: str): 6 | return json.loads(Path(f"./{fn}").read_text(encoding="UTF-8")) 7 | 8 | 9 | def pares_yml(file_path): 10 | with open(Path(f"./{file_path}"), "r", encoding="UTF-8") as file: 11 | lines = file.readlines() 12 | 13 | result = {} 14 | key = None 15 | for line in lines: 16 | line = line.strip() 17 | if line.endswith(":"): 18 | key = line[:-1] 19 | result[key] = [] 20 | elif key is not None: 21 | if not any( 22 | line.endswith(suf) 23 | for suf in ["专武", "外观", "外观武器", "四星", "4星"] 24 | ): 25 | result[key].append(str(line).lstrip("- ")) 26 | 27 | return result 28 | 29 | 30 | GRE, YEL, END = "\033[32m", "\033[33m", "\033[0m" 31 | AVATAR = read("AvatarExcelConfigData.json") 32 | WEAPONS = read("WeaponExcelConfigData.json") 33 | TEXTMAP_CHS = read("TextMapCHS.json") 34 | AVATAR_DETAIL = read("Avatar.json") 35 | AVATAR_DETAIL = {i["Id"]: i for i in AVATAR_DETAIL} 36 | OLD_ALIAS_PATH = Path("../data/gsmaterial/item-alias.json") 37 | OLD_ALIAS = json.loads(OLD_ALIAS_PATH.read_text(encoding="utf-8")) 38 | AVATAR_ALIAS = read("char-alias.json") 39 | WEAPON_ALIAS = pares_yml("wuqi_tujian.yaml") 40 | Path("weapon_alias.json").write_text( 41 | json.dumps(WEAPON_ALIAS, ensure_ascii=False, indent=2), encoding="utf-8" 42 | ) 43 | 44 | NEW_ALIAS_PATH = Path("./item-alias.json") 45 | new_alias = {} 46 | 47 | for avatar_data in AVATAR: 48 | avatar_id: int = avatar_data["id"] 49 | hs: int = avatar_data["nameTextMapHash"] 50 | name_cn: str = TEXTMAP_CHS.get(str(hs), "未知") 51 | if avatar_id in [10000005, 10000007]: 52 | print(f"角色 旅行者 被跳过") 53 | elif avatar_id > 11000000 or avatar_id == 10000001: 54 | print(f"角色 ({avatar_id}){name_cn} 被跳过") 55 | else: 56 | key = str(avatar_id) 57 | if not OLD_ALIAS.get(key): 58 | new_alias[key] = [name_cn] + AVATAR_ALIAS.get(name_cn, []) 59 | print(f"{GRE}新增角色 ({avatar_id}){name_cn}{END}") 60 | else: 61 | new_alias[key] = OLD_ALIAS[key] 62 | add = [x for x in AVATAR_ALIAS.get(name_cn, []) if x not in OLD_ALIAS[key]] 63 | remove = [ 64 | x 65 | for x in OLD_ALIAS[key] 66 | if x not in AVATAR_ALIAS.get(name_cn, []) and x != name_cn 67 | ] 68 | if remove or add: 69 | print( 70 | "{}角色 ({}){} 别名建议:{} {}{}".format( 71 | YEL, 72 | avatar_id, 73 | name_cn, 74 | " ".join(f"+{x}" for x in add), 75 | " ".join(f"-{x}" for x in remove), 76 | END, 77 | ) 78 | ) 79 | 80 | for weapon_data in WEAPONS: 81 | weapon_id: int = weapon_data["id"] 82 | hs: int = weapon_data["nameTextMapHash"] 83 | name_cn: str = TEXTMAP_CHS.get(str(hs), "未知") 84 | if name_cn == "未知": 85 | print(f"武器 ({weapon_id}) 尚无名称被跳过") 86 | elif weapon_id in [11419, 11420]: 87 | print(f"武器 ({weapon_id})「一心传」名刀 被跳过") 88 | elif name_cn.startswith("(test)"): 89 | print(f"武器 ({weapon_id}){name_cn} 被跳过") 90 | else: 91 | key = str(weapon_id) 92 | if not OLD_ALIAS.get(key): 93 | new_alias[key] = [name_cn] + WEAPON_ALIAS.get(name_cn, []) 94 | print(f"{GRE}新增武器 ({weapon_id}){name_cn}{END}") 95 | else: 96 | new_alias[key] = OLD_ALIAS[key] 97 | add = [x for x in WEAPON_ALIAS.get(name_cn, []) if x not in OLD_ALIAS[key]] 98 | remove = [ 99 | x 100 | for x in OLD_ALIAS[key] 101 | if x not in WEAPON_ALIAS.get(name_cn, []) and x != name_cn 102 | ] 103 | if remove or add: 104 | print( 105 | "{}武器 ({}){} 别名建议:{} {}{}".format( 106 | YEL, 107 | weapon_id, 108 | name_cn, 109 | " ".join(f"+{x}" for x in add), 110 | " ".join(f"-{x}" for x in remove), 111 | END, 112 | ) 113 | ) 114 | 115 | new_alias = {k: v for k, v in sorted(new_alias.items(), key=lambda i: int(i[0]))} 116 | NEW_ALIAS_PATH.write_text( 117 | json.dumps(new_alias, ensure_ascii=False, indent=2), encoding="utf-8" 118 | ) 119 | -------------------------------------------------------------------------------- /nonebot_plugin_gsmaterial/__init__.py: -------------------------------------------------------------------------------- 1 | from asyncio import sleep as async_sleep 2 | from random import randint 3 | from typing import Dict 4 | 5 | from nonebot import get_bot, get_driver, require 6 | from nonebot.adapters.onebot.v11 import Bot 7 | from nonebot.adapters.onebot.v11.event import GroupMessageEvent, MessageEvent 8 | from nonebot.adapters.onebot.v11.message import MessageSegment 9 | from nonebot.plugin import on_command 10 | from nonebot.typing import T_State 11 | 12 | from .config import SCHED_HOUR, SCHED_MINUTE, WEEKLY_BOSS 13 | from .data_source import ( 14 | generate_calc_msg, 15 | generate_daily_msg, 16 | generate_weekly_msg, 17 | sub_helper, 18 | update_config, 19 | ) 20 | 21 | require("nonebot_plugin_apscheduler") 22 | from nonebot_plugin_apscheduler import scheduler # noqa: E402 23 | 24 | mt_daily_matcher = on_command("材料", priority=13) 25 | mt_weekly_matcher = on_command("周本", priority=13) 26 | mt_calc_matcher = on_command("原神计算", priority=13) 27 | driver = get_driver() 28 | driver.on_bot_connect(update_config) 29 | 30 | 31 | @mt_daily_matcher.handle() 32 | async def daily_material(bot: Bot, event: MessageEvent, state: T_State): 33 | # 获取不包含触发关键词的消息文本 34 | arg = str(state["_prefix"]["command_arg"]) 35 | qq = str(event.get_user_id()) 36 | 37 | # 单独响应更新指令 38 | if arg.startswith("更新"): 39 | await update_config() 40 | msg = await generate_daily_msg("update") 41 | await mt_daily_matcher.finish( 42 | MessageSegment.text(msg) 43 | if isinstance(msg, str) 44 | else MessageSegment.image(msg) 45 | ) 46 | 47 | # 单独响应订阅指令 48 | if arg.startswith("订阅"): 49 | is_delete = "删除" in arg 50 | is_group = isinstance(event, GroupMessageEvent) 51 | action = f"{'d' if is_delete else 'a'}{'g' if is_group else 'p'}" 52 | action_id = event.group_id if is_group else qq 53 | if ( 54 | is_group 55 | and qq not in bot.config.superusers 56 | and event.sender.role not in ["admin", "owner"] 57 | ): 58 | await mt_daily_matcher.finish( 59 | f"你没有权限{'删除' if is_delete else '启用'}此群原神每日材料订阅!" 60 | ) 61 | await mt_daily_matcher.finish(await sub_helper(action, action_id)) # type: ignore 62 | 63 | # 识别周几,也可以是纯数字 64 | weekday, timedelta = 0, 0 65 | week_keys = ["一", "四", "1", "4", "二", "五", "2", "5", "三", "六", "3", "6"] 66 | for idx, s in enumerate(week_keys): 67 | if s in arg: 68 | weekday = idx // 4 + 1 69 | arg = arg.replace(f"周{s}", "").replace(s, "").strip() 70 | break 71 | for idx, s in enumerate(["今", "明", "后"]): 72 | if s in arg: 73 | timedelta = idx 74 | arg = arg.replace(f"{s}天", "").replace(f"{s}日", "").strip() 75 | 76 | # 处理正常指令 77 | if any(x in arg for x in ["天赋", "角色"]): 78 | target = "avatar" 79 | elif any(x in arg for x in ["武器"]): 80 | target = "weapon" 81 | elif not arg: 82 | # 只发送了命令触发词,返回每日总图 83 | target = "all" 84 | else: 85 | # 发送了无法被识别为类型的内容,忽略 86 | await mt_daily_matcher.finish() 87 | 88 | # 获取每日材料图片 89 | msg = await generate_daily_msg(target, weekday, timedelta) 90 | await mt_daily_matcher.finish( 91 | MessageSegment.text(msg) if isinstance(msg, str) else MessageSegment.image(msg) 92 | ) 93 | 94 | 95 | @mt_weekly_matcher.handle() 96 | async def weekly_material(bot: Bot, event: MessageEvent, state: T_State): 97 | # 获取不包含触发关键词的消息文本 98 | arg, target = str(state["_prefix"]["command_arg"]), "" 99 | # 处理输入 100 | for boss_alias in WEEKLY_BOSS: 101 | if arg in boss_alias: 102 | target = boss_alias[0] 103 | break 104 | if not arg: 105 | # 只发送了命令触发词,返回周本总图 106 | target = "all" 107 | elif not target: 108 | # 发送了无法被识别为周本名的内容,忽略 109 | await mt_weekly_matcher.finish() 110 | # 获取周本材料图片 111 | msg = await generate_weekly_msg(target) 112 | await mt_weekly_matcher.finish( 113 | MessageSegment.text(msg) if isinstance(msg, str) else MessageSegment.image(msg) 114 | ) 115 | 116 | 117 | @mt_calc_matcher.handle() 118 | async def calc_material(bot: Bot, event: MessageEvent, state: T_State): 119 | arg = str(state["_prefix"]["command_arg"]) 120 | msg = await generate_calc_msg(arg) 121 | await mt_weekly_matcher.finish( 122 | MessageSegment.text(msg) if isinstance(msg, str) else MessageSegment.image(msg) 123 | ) 124 | 125 | 126 | @scheduler.scheduled_job("cron", hour=int(SCHED_HOUR), minute=int(SCHED_MINUTE)) 127 | async def daily_push(): 128 | bot = get_bot() 129 | # 获取订阅配置 130 | cfg = await sub_helper() 131 | assert isinstance(cfg, Dict) 132 | # 更新每日材料图片 133 | msg = await generate_daily_msg("update") 134 | message = ( 135 | MessageSegment.text(msg) if isinstance(msg, str) else MessageSegment.image(msg) 136 | ) 137 | # 推送 138 | for group in cfg.get("群组", []): 139 | await bot.send_group_msg(group_id=group, message=message) 140 | await async_sleep(randint(5, 10)) 141 | for private in cfg.get("私聊", []): 142 | await bot.send_private_msg(user_id=private, message=message) 143 | await async_sleep(randint(5, 10)) 144 | -------------------------------------------------------------------------------- /data/gsmaterial/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "avatar": { 3 | "1": { 4 | "「自由」的哲学-104303": "4芭芭拉10000014,4安柏10000021,5可莉10000029,5达达利亚10000033,4迪奥娜10000039,4砂糖10000043,5埃洛伊10000062", 5 | "「繁荣」的哲学-104312": "5魈10000026,4凝光10000027,5七七10000035,5刻晴10000042,5夜兰10000060,5申鹤10000063", 6 | "「浮世」的哲学-104322": "5宵宫10000049,4托马10000050,5珊瑚宫心海10000054,4鹿野院平藏10000059,4绮良良10000061", 7 | "「诤言」的哲学-104331": "5提纳里10000069,5赛诺10000071,4坎蒂丝10000072,4珐露珊10000076", 8 | "「公平」的哲学-104340": "5林尼10000084,5那维莱特10000087" 9 | }, 10 | "2": { 11 | "「抗争」的哲学-104306": "5琴10000003,5迪卢克10000016,4雷泽10000020,4班尼特10000032,4诺艾尔10000034,5莫娜10000041,5优菈10000051", 12 | "「勤劳」的哲学-104315": "4香菱10000023,4重云10000036,5甘雨10000037,5胡桃10000046,5枫原万叶10000047,4云堇10000064,4瑶瑶10000077", 13 | "「风雅」的哲学-104325": "5神里绫华10000002,4九条裟罗10000056,5荒泷一斗10000057,4久岐忍10000065,5神里绫人10000066", 14 | "「巧思」的哲学-104334": "4多莉10000068,5纳西妲10000073,4莱依拉10000074,5艾尔海森10000078,4卡维10000081", 15 | "「正义」的哲学-104343": "4菲米尼10000085" 16 | }, 17 | "3": { 18 | "「诗文」的哲学-104309": "4丽莎10000006,4凯亚10000015,5温迪10000022,4菲谢尔10000031,5阿贝多10000038,4罗莎莉亚10000045,4米卡10000080", 19 | "「黄金」的哲学-104318": "4北斗10000024,4行秋10000025,5钟离10000030,4辛焱10000044,4烟绯10000048,5白术10000082", 20 | "「天光」的哲学-104328": "5雷电将军10000052,4早柚10000053,4五郎10000055,5八重神子10000058", 21 | "「笃行」的哲学-104337": "4柯莱10000067,5妮露10000070,5流浪者10000075,5迪希雅10000079", 22 | "「秩序」的哲学-104346": "4琳妮特10000083,5莱欧斯利10000086" 23 | } 24 | }, 25 | "weapon": { 26 | "1": { 27 | "高塔孤王的碎梦-114004": "3冷刃11301,4西风剑11401,4宗室长剑11404,4暗巷闪光11410,4辰砂之纺锤11415,4狼牙11424,5风鹰剑11501,3铁影阔剑12301,4钟剑12402,4雪葬的星银12411,5松籁响起之时12503,3魔导绪论14301,4西风秘典14401,4宗室秘法录14404,3鸦羽弓15301,4绝弦15402,4苍翠猎弓15409,4幽夜华尔兹15412", 28 | "谧林涓露的金符-114040": "4原木刀11417,4西福斯的月光11418,5圣显之钥11511,5裁叶萃光11512,4森林王器12417,4鹮穿之喙15419", 29 | "孤云寒林的神体-114016": "4匣里龙吟11405,4黑岩长剑11408,5斫峰之刃11504,4白影剑12407,4千岩古剑12410,3白缨枪13301,4流月针13403,5和璞鸢13505,3翡玉法球14304,4匣里日月14405,4黑岩绯玉14408,4遗祀玉珑14424,5碧落之珑14505,3弹弓15304,4弓藏15405,4黑岩战弓15408,5若水15508", 30 | "远海夷地的金枝-114028": "4天目影打刀11414,5雾切之回光11509,4恶王丸12416,4白辰之环14414,4证誓之明瞳14415,5不灭月华14506", 31 | "悠古弦音的回响-114052": "4灰河渡手11426,4勘探钻机13427,4静谧之曲15425,4测距规15427,5最初的大魔术15512" 32 | }, 33 | "2": { 34 | "凛风奔狼的怀乡-114008": "3黎明神剑11302,4笛剑11402,4黑剑11409,4降临之剑11412,5天空之刃11502,3沐浴龙血的剑12302,4祭礼大剑12403,5天空之傲12501,4决斗之枪13405,4龙脊长枪13409,4风信之锋13419,3讨龙英杰谭14302,4流浪乐章14402,4暗巷的酒与诗14410,4嘟嘟可故事集14413,4无垠蔚蓝之歌14426,5天空之卷14501,3神射手之誓15302,4祭礼弓15403,5天空之翼15501,5终末嗟叹之诗15503", 35 | "雾海云间的转还-114020": "3吃虎鱼刀11305,4试作斩岩11406,5磐岩结绿11505,3以理服人12305,4雨裁12405,4黑岩斩刀12408,5无工之剑12504,3钺矛13302,4匣里灭辰13401,4黑岩刺枪13404,4宗室猎枪13408,5息灾13507,3甲级宝珏14305,4试作金珀14406,4昭心14409,3信使15305,4试作澹月15406", 36 | "鸣神御灵的勇武-114032": "4东花坊时雨11422,5波乱月白经津11510,4桂木斩长正12414,5赤角石溃杵12510,4破魔之弓15414,4掠食者15415,4曚云之月15416,5飞雷之弦振15509", 37 | "绿洲花园的真谛-114044": "4聊聊棒12424,4贯月矢13417,5赤沙之杖13511,4流浪的晚星14416,4盈满之实14417,5千夜浮梦14511", 38 | "纯圣露滴的真粹-114056": "4海渊终曲11425,4船坞长剑11427,4纯水流华14425,5万世流涌大典14514" 39 | }, 40 | "3": { 41 | "狮牙斗士的理想-114012": "3旅行剑11303,4祭礼剑11403,4腐殖之剑11413,5苍古自由之誓11503,3白铁大剑12303,4西风大剑12401,4宗室大剑12404,4饰铁之花12418,5狼的末路12502,4西风长枪13407,5天空之脊13502,3异世界行记14303,4祭礼残章14403,4忍冬之果14412,5四风原典14502,3反曲弓15303,4西风猎弓15401,4宗室长弓15404,4暗巷猎手15410,4风花之颂15413,5阿莫斯之弓15502", 42 | "漆黑陨铁的一块-114024": "3飞天御剑11306,4铁蜂刺11407,3飞天大御剑12306,4试作古华12406,4螭骨剑12409,4衔珠海皇12412,3黑缨枪13303,4试作星镰13402,4千岩长枪13406,5护摩之杖13501,5贯虹之槊13504,4万国诸海图谱14407,5尘世之锁14504,4钢轮弓15407,4落霞15411", 43 | "今昔剧画之鬼人-114036": "4笼钓瓶一心11416,4喜多院十文字13414,4「渔获」13415,4断浪长鳍13416,5薙草之稻光13509,5神乐之真意14509,5冬极白星15507", 44 | "烈日威权的旧日-114048": "4玛海菈的水色12415,5苇海信标12511,5图莱杜拉的回忆14512,4王下近侍15417,4竭泽15418,4烈阳之嗣15424,5猎人之径15511", 45 | "无垢之海的金杯-114060": "4浪影阔剑12425,4便携动力锯12427,4峡湾长歌13424,4公义的酬报13425,5金流监督14513" 46 | } 47 | }, 48 | "weekly": { 49 | "风魔龙·特瓦林": { 50 | "东风之翎-113003": "5琴10000003,5迪卢克10000016,4班尼特10000032", 51 | "东风之爪-113004": "4丽莎10000006,4雷泽10000020,4香菱10000023,4诺艾尔10000034", 52 | "东风的吐息-113005": "4安柏10000021,4北斗10000024,4重云10000036" 53 | }, 54 | "安德留斯": { 55 | "北风之尾-113006": "5温迪10000022,4行秋10000025,5七七10000035", 56 | "北风之环-113007": "4芭芭拉10000014,5可莉10000029,5莫娜10000041,5刻晴10000042", 57 | "北风的魂匣-113008": "4凯亚10000015,4凝光10000027,4菲谢尔10000031,4砂糖10000043" 58 | }, 59 | "「公子」": { 60 | "吞天之鲸·只角-113013": "5钟离10000030,5阿贝多10000038,4辛焱10000044", 61 | "魔王之刃·残片-113014": "5达达利亚10000033,4迪奥娜10000039,5胡桃10000046", 62 | "武炼之魂·孤影-113015": "5魈10000026,5甘雨10000037,4罗莎莉亚10000045" 63 | }, 64 | "若陀龙王": { 65 | "龙王之冕-113017": "5宵宫10000049,5优菈10000051", 66 | "血玉之枝-113018": "5神里绫华10000002,4烟绯10000048,4多莉10000068", 67 | "鎏金之鳞-113019": "5枫原万叶10000047,4早柚10000053,5夜兰10000060" 68 | }, 69 | "「女士」": { 70 | "熔毁之刻-113025": "5雷电将军10000052,4五郎10000055,5埃洛伊10000062", 71 | "狱火之蝶-113026": "4托马10000050,5珊瑚宫心海10000054,5申鹤10000063", 72 | "灰烬之心-113027": "4九条裟罗10000056,5荒泷一斗10000057,4云堇10000064" 73 | }, 74 | "祸津御建鸣神命": { 75 | "凶将之手眼-113032": "5神里绫人10000066,5赛诺10000071", 76 | "祸神之禊泪-113033": "4久岐忍10000065,4柯莱10000067,5妮露10000070,4坎蒂丝10000072", 77 | "万劫之真意-113034": "5八重神子10000058,4鹿野院平藏10000059,5提纳里10000069" 78 | }, 79 | "「正机之神」": { 80 | "傀儡的悬丝-113041": "5纳西妲10000073,4珐露珊10000076,5迪希雅10000079", 81 | "无心的渊镜-113042": "4莱依拉10000074,5艾尔海森10000078,4米卡10000080", 82 | "空行的虚铃-113043": "5流浪者10000075,4瑶瑶10000077" 83 | }, 84 | "阿佩普的绿洲守望者": { 85 | "生长天地之蕨草-113046": "5白术10000082,4菲米尼10000085", 86 | "原初绿洲之初绽-113047": "4卡维10000081,5林尼10000084,5莱欧斯利10000086", 87 | "亘古树海之一瞬-113048": "4绮良良10000061,4琳妮特10000083,5那维莱特10000087" 88 | } 89 | }, 90 | "skip_3": true, 91 | "time": 1692236943 92 | } -------------------------------------------------------------------------------- /nonebot_plugin_gsmaterial/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Tuple 4 | 5 | from httpx import Client 6 | from pytz import UnknownTimeZoneError, timezone 7 | 8 | from nonebot import get_driver 9 | from nonebot.log import logger 10 | 11 | _driver = get_driver() 12 | 13 | 14 | def _init_picture_dir(env_key: str, config_dir: Path) -> Tuple[str, str, Path]: 15 | """根据本地文件决定后续下载文件路径及命名""" 16 | 17 | env_value = getattr(_driver.config, env_key, None) 18 | if not env_value: 19 | sub_dirs = { 20 | "gsmaterial_avatar": "avatar", 21 | "gsmaterial_weapon": "weapon", 22 | "gsmaterial_item": "item", 23 | } 24 | return "name", "png", config_dir / sub_dirs[env_key] 25 | env_value = Path(env_value) 26 | if not env_value.exists(): 27 | raise ValueError(f".env 文件中 {env_key} 填写的路径不存在!") 28 | elif env_value.is_file(): 29 | pic_name = "id" if str(env_value.name[0]).isdigit() else "name" 30 | pic_fmt = env_value.name.split(".")[-1] 31 | pic_dir = env_value.parent 32 | return pic_name, pic_fmt, pic_dir 33 | elif env_value.is_dir(): 34 | pic_name, pic_fmt = "name", "png" 35 | for already_have in env_value.iterdir(): 36 | pic_name = "id" if str(already_have.name[0]).isdigit() else "name" 37 | pic_fmt = already_have.name.split(".")[-1] 38 | break 39 | return pic_name, pic_fmt, env_value 40 | raise ValueError(f".env 文件中 {env_key} 填写的值异常!应填写图片文件或文件夹路径") 41 | 42 | 43 | # 时区设定 44 | # TZ="Asia/Shanghai" 45 | try: 46 | TZ = timezone(str(getattr(_driver.config, "tz", "Asia/Shanghai"))) 47 | except UnknownTimeZoneError: 48 | TZ = timezone("Asia/Shanghai") 49 | 50 | # 材料下载镜像 51 | # GSMATERIAL_MIRROR="https://api.ambr.top/assets/UI/" 52 | DL_MIRROR = str( 53 | getattr(_driver.config, "gsmaterial_mirror", "https://api.ambr.top/assets/UI/") 54 | ) 55 | 56 | # 每日推送时间 57 | # GSMATERIAL_SCHEDULER="8:10" 58 | SCHEDULER_TIME = str(getattr(_driver.config, "gsmaterial_scheduler", "8:10")) 59 | SCHED_HOUR, SCHED_MINUTE = SCHEDULER_TIME.split(":") 60 | 61 | # 每日材料绘制是否跳过三星物品 62 | # GSMATERIAL_SKIP_THREE=True 63 | SKIP_THREE = bool(getattr(_driver.config, "gsmaterial_skip_three", True)) 64 | 65 | # 配置缓存路径 66 | # GSMATERIAL_CONFIG="/path/to/data/gsmaterial" 67 | _default_dir = Path() / "data" / "gsmaterial" 68 | CONFIG_DIR = Path(getattr(_driver.config, "gsmaterial_config", _default_dir)) 69 | CONFIG_DIR.mkdir(parents=True, exist_ok=True) 70 | (CONFIG_DIR / "draw").mkdir(parents=True, exist_ok=True) 71 | (CONFIG_DIR / "cache").mkdir(parents=True, exist_ok=True) 72 | 73 | # 下载缓存路径 74 | # GSMATERIAL_AVATAR="/path/to/avatars" 75 | # GSMATERIAL_AVATAR="/path/to/avatars/10000002.png" 76 | # GSMATERIAL_AVATAR="/path/to/avatars/神里绫华.png" 77 | _avatar, _avatar_fmt, _avatar_dir = _init_picture_dir("gsmaterial_avatar", CONFIG_DIR) 78 | _weapon, _weapon_fmt, _weapon_dir = _init_picture_dir("gsmaterial_weapon", CONFIG_DIR) 79 | _item, _item_fmt, _item_dir = _init_picture_dir("gsmaterial_item", CONFIG_DIR) 80 | _avatar_dir.mkdir(parents=True, exist_ok=True) 81 | _weapon_dir.mkdir(parents=True, exist_ok=True) 82 | _item_dir.mkdir(parents=True, exist_ok=True) 83 | DL_CFG = { 84 | "avatar": {"dir": _avatar_dir, "file": _avatar, "fmt": _avatar_fmt}, 85 | "weapon": {"dir": _weapon_dir, "file": _weapon, "fmt": _weapon_fmt}, 86 | "item": {"dir": _item_dir, "file": _item, "fmt": _item_fmt}, 87 | } 88 | logger.info(f"图片缓存规则:\n{DL_CFG}") 89 | 90 | # 配置文件初始化 91 | if not (CONFIG_DIR / "sub.json").exists(): 92 | (CONFIG_DIR / "sub.json").write_text( 93 | json.dumps({"群组": [], "私聊": []}, ensure_ascii=False, indent=2), encoding="UTF-8" 94 | ) 95 | if not (CONFIG_DIR / "cookie.json").exists(): 96 | (CONFIG_DIR / "cookie.json").write_text( 97 | json.dumps({}, ensure_ascii=False, indent=2), encoding="UTF-8" 98 | ) 99 | 100 | # 角色别名、武器别名初始化 101 | _client = Client(verify=False) 102 | ITEM_ALIAS = _client.get("https://cdn.monsterx.cn/bot/gsmaterial/item-alias.json").json() 103 | (CONFIG_DIR / "item-alias.json").write_text( 104 | json.dumps(ITEM_ALIAS, ensure_ascii=False, indent=2), encoding="utf-8" 105 | ) 106 | 107 | # 素材主键 108 | WEEKLY_BOSS = [ # 暂时拿第一个作为标识 109 | ["风魔龙·特瓦林", "往日的天空之王", "深入风龙废墟", "追忆:暴风般狂啸之龙", "风龙", "风魔龙"], 110 | ["安德留斯", "奔狼的领主", "北风的王狼,奔狼的领主", "狼", "北风狼", "王狼"], 111 | ["「公子」", "愚人众执行官末席", "进入「黄金屋」", "追忆:黄金与孤影", "公子", "达达利亚", "可达鸭", "鸭鸭"], 112 | ["若陀龙王", "被封印的岩龙之王", "「伏龙树」之底", "追忆:摇撼山岳之龙", "若托", "若陀", "龙王"], 113 | ["「女士」", "愚人众执行官第八席", "鸣神岛·天守", "追忆:红莲的真剑试合", "女士", "罗莎琳", "魔女"], 114 | ["祸津御建鸣神命", "雷电之稻妻殿", "梦想乐土之殁", "追忆:永恒的守护者", "雷神", "雷电", "雷军", "将军"], 115 | ["「正机之神」", "七叶寂照秘密主", "净琉璃工坊", "追忆:七叶中尊琉璃坛", "正机", "散兵", "伞兵", "秘密主"], 116 | ["阿佩普的绿洲守望者", "历经千年之「末日」的进化生命", "肇始之乡", "追忆:「它们」也曾完美无瑕", "阿佩普", "绿洲守望者", "草龙", "草龙王"], 117 | ["吞星之鲸", "无光星海彼端的访客", "异界余影", "追忆:星间的巨兽", "鲸鱼", "吞星鲸"], 118 | ["???", "未知", "测试", "未实装", "未上线", "未命名"], 119 | ] 120 | 121 | # API 地址 122 | AMBR = { 123 | "每日采集": "https://api.ambr.top/v2/chs/dailyDungeon", 124 | "角色列表": "https://api.ambr.top/v2/chs/avatar", 125 | "角色详情": "https://api.ambr.top/v2/chs/avatar/{id}", 126 | "武器列表": "https://api.ambr.top/v2/chs/weapon", 127 | "武器详情": "https://api.ambr.top/v2/chs/weapon/{id}", 128 | "材料列表": "https://api.ambr.top/v2/chs/material", 129 | "材料详情": "https://api.ambr.top/v2/chs/material/{id}", 130 | "圣遗物列表": "https://api.ambr.top/v2/chs/reliquary", 131 | "圣遗物详情": "https://api.ambr.top/v2/chs/reliquary/{id}", 132 | "升级材料": "https://api.ambr.top/v2/static/upgrade", 133 | "通告": "https://api.ambr.top/assets/data/event.json", 134 | } 135 | MYS = { 136 | "技能": "https://api-takumi.mihoyo.com/event/e20200928calculate/v1/avatarSkill/list", 137 | "计算": "https://api-takumi.mihoyo.com/event/e20200928calculate/v2/compute", 138 | "_stoken": "https://api-takumi.mihoyo.com/auth/api/getMultiTokenByLoginTicket", 139 | "_cookie": "https://api-takumi.mihoyo.com/auth/api/getCookieAccountInfoBySToken", 140 | } 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

NoneBot Plugin GsMaterial


2 | 3 | 4 |

🤖 用于展示原神游戏秘境材料升级消耗数据的 NoneBot2 插件


5 | 6 | 7 |

8 | license 9 | pypi 10 | python 11 | QQ Chat Group
12 | Code style: black 13 | Imports: isort 14 | Lint: flake8 15 | pre-commit 16 |


17 | 18 | 19 | | ![daily 1 avatar](https://github.com/monsterxcn/nonebot-plugin-gsmaterial/assets/22407052/24b343b6-838a-4976-8566-fc14124cc5dd) | ![daily 2 weapon](https://github.com/monsterxcn/nonebot-plugin-gsmaterial/assets/22407052/ef9a9871-1b28-45dc-b52f-02872a319de7) | ![weekly 8](https://github.com/monsterxcn/nonebot-plugin-gsmaterial/assets/22407052/fd90b333-90e6-4d34-9523-1b991eb51945) | ![clac 10000084](https://github.com/monsterxcn/nonebot-plugin-gsmaterial/assets/22407052/2db48921-0dfa-453f-90b3-82d83af1238b) | 20 | |:--:|:--:|:--:|:--:| 21 | 22 | 23 | ## 安装方法 24 | 25 | 26 | 如果你正在使用 2.0.0.beta1 以上版本 NoneBot2,推荐使用以下命令安装: 27 | 28 | 29 | ```bash 30 | # 从 nb_cli 安装 31 | python -m nb_cli plugin install nonebot-plugin-gsmaterial 32 | 33 | # 或从 PyPI 安装 34 | python -m pip install nonebot-plugin-gsmaterial 35 | ``` 36 | 37 | 38 | ## 插件配置 39 | 40 | 41 | ### 环境变量 42 | 43 | 44 | 一般来说,插件安装完成后无需设置环境变量,只需重启 Bot 即可开始使用。你也可以在 NoneBot2 当前使用的 `.env` 文件中添加下面的环境变量,对插件进行更多配置。环境变量修改后需要重启 Bot 才能生效。 45 | 46 | 47 | - `tz` 时区设置,默认为 `"Asia/Shanghai"` 48 | 49 | 如果定时任务时区异常,请查看 [@nonebot/plugin-apscheduler](https://github.com/nonebot/plugin-apscheduler) 文档添加该依赖插件的 `apscheduler_config` 环境变量配置 50 | 51 | - `gsmaterial_mirror` 角色及武器图标下载镜像,需提供 `UI_AvatarIcon_Layla.png` 等形式的图片,可供选择的镜像有: 52 | 53 | + `https://api.ambr.top/assets/UI/` 安柏计划(默认) 54 | + `https://enka.network/ui/` Enka.Network 55 | + `http://file.microgg.cn/ui/` 小灰灰 56 | 57 | - `gsmaterial_scheduler` 每日材料订阅推送时间,默认为 `"8:10"` 58 | 59 | - `gsmaterial_skip_three` 每日材料是否忽略三星物品,默认为 `true` 60 | 61 | - `gsmaterial_config` 插件缓存目录,默认为 NoneBot2 根目录下 `data/gsmaterial` 文件夹,**填写时路径中的反斜杠 `\` 务必全部替换为正斜杠 `/`** 62 | 63 | - `gsmaterial_avatar` `gsmaterial_weapon` `gsmaterial_item` 64 | 65 | 分别为角色图标、武器图标、物品图标文件夹或文件路径。**一般情况不需要配置**。这些配置针对的是已经使用 [@KimigaiiWuyi/GenshinUID](https://github.com/KimigaiiWuyi/GenshinUID) 等插件在本地下载了 GsMaterial 所需资源的用户,合理配置这些环境变量可以避免 GsMaterial 重复下载。如果启用了这些配置,请注意检查 NoneBot2 启动时由此插件输出的 `图片缓存规则`,确保插件正确识别!配置具体填写的形式如下: 66 | + `/path/to/avatars` 指定某个文件夹。如果 GsMaterial 后续需要补充下载文件,文件命名与当前已有文件的格式一致。如果该文件夹内尚无文件,则 GsMaterial 会在此文件夹下载以 `中文名称.png` 形式命名的文件 67 | + `/path/to/avatars/10000002.png` 指定某个文件。如果 GsMaterial 后续需要补充下载文件,文件命名规则为 `数字 ID.png`。与此同理,如果填入形如 `../神里绫华.jpg`,后续补充下载文件的命名规则就为 `中文名称.jpg` 68 | 69 | [@KimigaiiWuyi/GenshinUID](https://github.com/KimigaiiWuyi/GenshinUID) 用户安装 GsMaterial 后推荐配置: 70 | ``` 71 | gsmaterial_avatar="/path/to/GenshinUID/resource/chars" # 填 chars 文件夹实际路径,不要照抄 72 | gsmaterial_weapon="/path/to/GenshinUID/resource/weapon" # 填 weapon 文件夹实际路径,不要照抄 73 | ``` 74 | 75 | 76 | ### Cookie 配置 77 | 78 | 79 | 如需使用材料计算功能,请在 `gsmaterial_config` 配置的目录下 cookie.json 文件中以字典形式填入米游社 Cookie,文件中至少需要有 `account_id` 和 `cookie_token`。考虑到 `cookie_token` 有效期比较玄学,建议再多配置一个 `stoken` 来自动更新 `cookie_token`。如果获取到的 `stoken` 以 `v2_` 开头,则还需要再配置一个 `mid`。 80 | 81 | **注意**,Cookie 配置不需要普通用户单独配置,只需要 Bot 拥有者配置一个公共 Cookie! 82 | 83 | 最终你可能写入一个像这样的 cookie.json 文件: 84 | 85 | 86 |
最普通的一种
87 | 88 | 89 | ```json 90 | { 91 | "account_id": "272894075", 92 | "cookie_token": "PV6zzXj28UUSUHetJZO2sqEff4sqwdzDAA3Wz3xY", 93 | "stoken": "5CzsKTYLuoCy4Pf5t7y3bHkS0MjljkOm89rOYfGh" 94 | } 95 | ``` 96 | 97 | 98 |
99 | 100 | 101 |
使用 stoken v2 的那种
102 | 103 | 104 | ```json 105 | { 106 | "account_id": "272894075", 107 | "cookie_token_v2": "PV6zzXj28UUSUHetJZO2sqEff4sqwdzDAA3Wz3xY", 108 | "stoken": "v2_efTJdH0uiaDIcoVSINjZY9lHOtSRS5NcfREpDUpXX-AQlLujTP2HWbi14TXHrH_dA1Dxw9TdTGG0LiRONpW=", 109 | "mid": "0cckyppmwl_mhy" 110 | } 111 | ``` 112 | 113 | 114 |
115 | 116 | 117 |
使用 login_ticket 的那种
118 | 119 | 120 | login_ticket 获取方式请参考 https://github.com/monsterxcn/nonebot-plugin-gsmaterial/issues/8#issuecomment-1365705339 121 | 122 | 123 | ```json 124 | { 125 | "account_id": "272894075", 126 | "login_ticket": "5CzsKTYLuoCy4Pf5t7y3bHkS0MjljkOm89rOYfGh", 127 | "mid": "0cckyppmwl_mhy" 128 | } 129 | ``` 130 | 131 | 132 |
133 | 134 | 135 | ## 命令说明 136 | 137 | 138 | 插件响应以下形式的消息: 139 | 140 | 141 | - 以 `材料` 开头的消息 142 | 143 | | 附带参数 | 说明 | 144 | |:-------|:----| 145 | | 空 | 返回今日天赋培养与武器突破材料总图 | 146 | | `天赋` / `角色` | 返回今日天赋培养材料图片 | 147 | | `武器` | 返回今日武器突破材料图片 | 148 | | `周一` / `1` / ... | 返回指定日期的天赋培养与武器突破材料总图 | 149 | | `更新` | 立即更新每日材料数据,并返回最新的今日天赋培养与武器突破材料总图 | 150 | | `订阅` | 启用当前消息来源的每日材料订阅,群组内仅 Bot 管理员、群组创建者、群组管理员可操作 | 151 | | `订阅删除` | 禁用当前消息来源的每日材料订阅,群组内仅 Bot 管理员、群组创建者、群组管理员可操作 | 152 | 153 | - 以 `周本` 开头的消息 154 | 155 | | 附带参数 | 说明 | 156 | |:-------|:----| 157 | | 空 | 返回周本材料总图 | 158 | | `风龙` / `风魔龙` | 返回 *风魔龙·特瓦林* 掉落材料图片 | 159 | | `狼` / `北风狼` / `王狼` | 返回 *安德留斯* 掉落材料图片 | 160 | | `公子` / `达达利亚` / `可达鸭` / `鸭鸭` | 返回 *「公子」* 掉落材料图片 | 161 | | `若托` / `若陀` / `龙王` | 返回 *若陀龙王* 掉落材料图片 | 162 | | `女士` / `罗莎琳` / `魔女` | 返回 *「女士」* 掉落材料图片 | 163 | | `雷神` / `雷电` / `雷军` / `将军` | 返回 *祸津御建鸣神命* 掉落材料图片 | 164 | | `正机` / `散兵` / `伞兵` / `秘密主` | 返回 *「正机之神」* 掉落材料图片 | 165 | | `草龙` / `草龙王` / `阿佩普` / `绿洲守望者` | 返回 *阿佩普的绿洲守望者* 掉落材料图片 | 166 | | `吞星之鲸` / `鲸鱼` / `吞星鲸` | 返回 *吞星之鲸* 掉落材料图片 | 167 | | `测试` / `未知` / `未实装` / `未上线` | 返回 *尚未实装周本* 掉落材料图片(如果有) | 168 | 169 | ![周本总图](https://github.com/monsterxcn/nonebot-plugin-gsmaterial/assets/22407052/952979c8-7c05-4ee5-b065-4ff43069a716) 170 | 171 | - 以 `原神计算` 开头的消息 172 | 173 | 第一个附带参数 **必须** 为角色名称或武器名称(支持别名),并且与后面的参数 **用空格隔开**。 174 | 175 | 计算角色时: 176 | 177 | + 角色等级允许的输入包括 `90`、`81-90` 等 178 | + 天赋等级允许的输入包括 `8`、`1-8`、`888`、`81010`、`8 8 8`、`1-8 1-10 10` 等 179 | + 只计算等级消耗时,可以使用 `111` 作为天赋等级 180 | + 只计算天赋消耗时,可以使用 `1` 作为角色等级,或者不输入角色等级并在天赋等级前添加「天赋」二字 181 | + 同时限定天赋等级和天赋等级时,**必须** 角色等级在前、天赋等级在后,中间用空格或「天赋」二字隔开 182 | + 未限定等级范围时,默认计算角色等级 1-81、三个天赋等级 1-8 消耗的材料 183 | 184 | 计算武器时: 185 | 186 | + 武器等级允许的输入包括 `90`、`81-90`、`81 90` 等 187 | + 未限定等级范围时,默认计算武器等级 1-90 消耗的材料 188 | 189 | 此指令附带参数较为复杂,下面是一些举例: 190 | 191 | + `原神计算琴` 计算 *琴* 角色等级 1-90、三个天赋等级 1-8 消耗材料 192 | + `原神计算琴 81` 计算 *琴* 角色等级 1-**81**、三个天赋等级 1-8 消耗材料 193 | + `原神计算琴 81 111` 计算 *琴* 角色等级 1-**81** 消耗材料 194 | + `原神计算琴 81-90 111` 计算 *琴* 角色等级 **81**-**90** 消耗材料 195 | + `原神计算琴 90 8 8-10 10` 计算 *琴* 角色等级 1-**90**、天赋等级 1-**8** **8**-**10** 1-**10** 消耗材料 196 | + `原神计算琴 1 10` 计算 *琴* 天赋等级 1-**10** 消耗材料 197 | + `原神计算琴 天赋101010` 计算 *琴* 三个天赋等级均 1-**10** 消耗材料 198 | + `原神计算琴 天赋 10 1-8 1-10` 计算 *琴* 天赋等级 1-**10** **1**-**8**、**1**-**10** 消耗材料 199 | + `原神计算狼末 81` 计算 *狼的末路* 等级 1-**81** 消耗材料 200 | + `原神计算狼末 81 88` 计算 *狼的末路* 等级 **81**-**88** 消耗材料 201 | 202 | 203 |
计算角色示例
204 | 205 |
206 | 207 |
计算武器示例
208 | 209 |
210 | 211 | 212 | ## 其他说明 213 | 214 | 215 | - 插件秘境材料数据来源为 [Project Amber](https://ambr.top/chs),所有未实装角色及武器的数据均由该数据库提供。 216 | 217 | - 插件升级材料数据来源为 [米游社养成计算器](#),使用此功能需要有效的 `account_id` 和 `cookie_token`。 218 | 219 | - 插件使用的所有角色及武器图标会在 Bot 连接建立后从环境变量 `GSMATERIAL_MIRROR` 下载,所有计算器所需图标会在查询时从米游社下载。这些资源通常只需下载一次,其下载路径及保存文件名均可通过环境变量控制,具体说明请查看 [环境变量](#环境变量) 第 5 条。 220 | 221 | - 插件的原神每日材料定时推送基于 [@nonebot/plugin-apscheduler](https://github.com/nonebot/plugin-apscheduler),如果 NoneBot2 启动时插件的定时任务未正常注册,可能需要额外添加该插件的环境变量 `apscheduler_autostart=true` 来使 `scheduler` 自动启动。 222 | 223 | 224 | ## 特别鸣谢 225 | 226 | 227 | [@Mrs4s/go-cqhttp](https://github.com/Mrs4s/go-cqhttp) | [@nonebot/nonebot2](https://github.com/nonebot/nonebot2) | [@nonebot/plugin-apscheduler](https://github.com/nonebot/plugin-apscheduler) | [@DGP-Studio/Snap.Metadata](https://github.com/DGP-Studio/Snap.Metadata) | [@ctrlcvs/xiaoyao-cvs-plugin](https://github.com/ctrlcvs/xiaoyao-cvs-plugin) | [Project Amber](https://ambr.top/chs) 228 | -------------------------------------------------------------------------------- /nonebot_plugin_gsmaterial/material_draw.py: -------------------------------------------------------------------------------- 1 | from re import match 2 | from math import ceil 3 | from io import BytesIO 4 | from pathlib import Path 5 | from copy import deepcopy 6 | from typing import Dict, List, Union 7 | 8 | from PIL import Image, ImageDraw, ImageFont 9 | 10 | from nonebot.log import logger 11 | from nonebot.utils import run_sync 12 | 13 | from .config import DL_CFG, CONFIG_DIR, SKIP_THREE 14 | 15 | RESAMPLING = getattr(Image, "Resampling", Image).LANCZOS 16 | 17 | 18 | def font(size: int) -> ImageFont.FreeTypeFont: 19 | """Pillow 绘制字体设置""" 20 | 21 | return ImageFont.truetype( 22 | str(CONFIG_DIR / "draw" / "SmileySans-Oblique.ttf"), size=size # HYWH-65W 23 | ) 24 | 25 | 26 | @run_sync 27 | def circle_corner(mark_img: Image.Image, radius: int = 30) -> Image.Image: 28 | """图片圆角处理""" 29 | 30 | mark_img = mark_img.convert("RGBA") 31 | scale, radius = 5, radius * 5 32 | mark_img = mark_img.resize( 33 | (mark_img.size[0] * scale, mark_img.size[1] * scale), RESAMPLING 34 | ) 35 | w, h = mark_img.size 36 | circle = Image.new("L", (radius * 2, radius * 2), 0) 37 | draw = ImageDraw.Draw(circle) 38 | draw.ellipse((0, 0, radius * 2, radius * 2), fill=255) 39 | alpha = Image.new("L", mark_img.size, 255) 40 | alpha.paste(circle.crop((0, 0, radius, radius)), (0, 0)) 41 | alpha.paste(circle.crop((radius, 0, radius * 2, radius)), (w - radius, 0)) 42 | alpha.paste( 43 | circle.crop((radius, radius, radius * 2, radius * 2)), 44 | (w - radius, h - radius), 45 | ) 46 | alpha.paste(circle.crop((0, radius, radius, radius * 2)), (0, h - radius)) 47 | mark_img.putalpha(alpha) 48 | return mark_img.resize((int(w / scale), int(h / scale)), RESAMPLING) 49 | 50 | 51 | async def draw_materials(config: Dict, needs: List[str], day: int = 0) -> Path: 52 | """原神秘境材料图片绘制""" 53 | 54 | cache_dir = CONFIG_DIR / "cache" 55 | is_weekly, img_and_path = day == 0, [] 56 | rank_bg = { 57 | "3": Image.open(CONFIG_DIR / "draw/bg3.140.png"), 58 | "4": Image.open(CONFIG_DIR / "draw/bg4.140.png"), 59 | "5": Image.open(CONFIG_DIR / "draw/bg5.140.png"), 60 | } 61 | for need in needs: 62 | raw_config = config["weekly" if is_weekly else need][ 63 | need if is_weekly else str(day) 64 | ] 65 | draw_config = { # 剔除 3 星武器 66 | key: ",".join(s for s in value.split(",") if not SKIP_THREE or s[0] != "3") 67 | for key, value in dict(raw_config).items() 68 | if value 69 | } 70 | 71 | # 计算待绘制图片的宽度 72 | title = ( 73 | need 74 | if is_weekly 75 | else {1: "周一/周四 {}材料", 2: "周二/周五 {}材料", 3: "周三/周六 {}材料"}[day].format( 76 | "天赋培养" if need == "avatar" else "武器突破" 77 | ) 78 | ) 79 | title_bbox = font(50).getbbox(title) 80 | total_width = max( 81 | title_bbox[-2] + 50, 82 | max( 83 | [(font(40).getlength(_key.split("-")[0]) + 150) for _key in draw_config] 84 | ), 85 | max([len(draw_config[_key].split(",")[:6]) for _key in draw_config]) 86 | * (170 + 10) 87 | + 10, 88 | ) 89 | 90 | # 计算待绘制图片的高度,每行绘制 6 个角色或武器 91 | line_cnt = sum( 92 | ceil(len(draw_config[_key].split(",")) / 6) for _key in draw_config 93 | ) 94 | total_height = 150 + len(draw_config) * 90 + line_cnt * (160 + 40 + 20) 95 | 96 | # 开始绘制! 97 | img = Image.new("RGBA", (int(total_width), total_height), "#FBFBFB") 98 | drawer = ImageDraw.Draw(img) 99 | 100 | # 绘制标题 101 | drawer.text( 102 | (int((total_width - title_bbox[-2]) / 2), int((150 - title_bbox[-1]) / 2)), 103 | title, 104 | fill="black", 105 | font=font(50), 106 | stroke_fill="grey", 107 | stroke_width=2, 108 | ) 109 | 110 | # 绘制每个分组 111 | startH = 150 112 | for key in draw_config: 113 | # 绘制分组所属材料的图片 114 | key_name, key_id = key.split("-") 115 | try: 116 | _key_icon = deepcopy(rank_bg["4" if need == "avatar" else "5"]) 117 | _key_icon_path = DL_CFG["item"]["dir"] / "{}.{}".format( 118 | key_id 119 | if DL_CFG["item"]["file"] == "id" 120 | else key_name 121 | if key_name != "???" 122 | else key_id, 123 | DL_CFG["item"]["fmt"], 124 | ) 125 | _key_icon_img = ( 126 | Image.open(_key_icon_path) 127 | .resize((140, 140), RESAMPLING) 128 | .convert("RGBA") 129 | ) 130 | _key_icon.paste(_key_icon_img, (0, 0), _key_icon_img) 131 | _key_icon = (await circle_corner(_key_icon, radius=30)).resize( 132 | (80, 80), RESAMPLING 133 | ) 134 | img.paste(_key_icon, (25, startH), _key_icon) 135 | except Exception as e: 136 | logger.opt(exception=e).error(key) 137 | pass 138 | # 绘制分组所属材料的名称 139 | ImageDraw.Draw(img).text( 140 | (125, startH + int((80 - font(40).getbbox("高")[-1]) / 2)), 141 | key_name, 142 | font=font(40), 143 | fill="#333", 144 | ) 145 | 146 | # 绘制当前分组的所有角色/武器 147 | startH += 90 148 | draw_X, draw_Y, cnt = 10, startH, 0 149 | draw_order = sorted( 150 | draw_config[key].split(","), key=lambda x: x[0], reverse=True 151 | ) 152 | for item in draw_order: 153 | if match(r"^[0-9][\u3000-\u9fff]+[0-9]{5,}$", item): 154 | # 5雷电将军10000052,5八重神子10000058,... 155 | _split = -5 if need == "weapon" else -8 156 | rank, name, this_id = item[0], item[1:_split], item[_split:] 157 | else: 158 | rank = 0 159 | name = this_id = item 160 | # 角色/武器图片 161 | try: 162 | _dl_cfg_key = "avatar" if need not in ["avatar", "weapon"] else need 163 | _icon = ( 164 | deepcopy(rank_bg[str(rank)]) 165 | if rank 166 | else Image.new("RGBA", (140, 140), "#818486") 167 | ) 168 | _icon_path = DL_CFG[_dl_cfg_key]["dir"] / "{}.{}".format( 169 | this_id if DL_CFG[_dl_cfg_key]["file"] == "id" else name, 170 | DL_CFG[_dl_cfg_key]["fmt"], 171 | ) 172 | _icon_img = ( 173 | Image.open(_icon_path) 174 | .resize((140, 140), RESAMPLING) 175 | .convert("RGBA") 176 | ) 177 | _icon.paste(_icon_img, (0, 0), _icon_img) 178 | _icon = (await circle_corner(_icon, radius=10)).resize( 179 | (150, 150), RESAMPLING # 140 180 | ) 181 | img.paste(_icon, (draw_X + 10, draw_Y + 10), _icon) 182 | except Exception as e: 183 | logger.opt(exception=e).error(item) 184 | pass 185 | # 角色/武器名称 186 | name_bbox = font(30).getbbox(name) 187 | ImageDraw.Draw(img).text( 188 | ( 189 | int(draw_X + (170 - name_bbox[-2]) / 2), 190 | int(draw_Y + 160 + (40 - name_bbox[-1]) / 2), 191 | ), 192 | name, 193 | font=font(30), 194 | fill="#333", 195 | ) 196 | # 按照 6 个角色/武器一行绘制 197 | draw_X += 170 + 10 198 | cnt += 1 199 | if cnt == 6: 200 | draw_X, cnt = 10, 0 201 | draw_Y += 160 + 40 + 20 202 | 203 | # 一组角色/武器绘制完毕 204 | startH += (160 + 40 + 20) * ceil(len(draw_config[key].split(",")) / 6) 205 | 206 | # 全部绘制完毕,保存图片 207 | cache_file = cache_dir / ( 208 | f"weekly.{need}.jpg" if is_weekly else f"daily.{day}.{need}.jpg" 209 | ) 210 | img.convert("RGB").save(cache_file) 211 | logger.debug(f"{'周本' if is_weekly else '每日'}材料图片生成完毕 {cache_file.name}") 212 | img_and_path.append([img, cache_file]) 213 | 214 | # 仅有一张图片时直接返回 215 | if len(img_and_path) == 1: 216 | return img_and_path[0][1] 217 | 218 | # 存在多张图片时横向合并 219 | width = sum([i[0].size[0] for i in img_and_path]) + (len(img_and_path) - 1) * 25 220 | _weight, height = 0, max([i[0].size[1] for i in img_and_path]) 221 | merge = Image.new("RGBA", (width, height), "#FBFBFB") 222 | for i in img_and_path: 223 | merge.paste(i[0], (_weight, 0), i[0]) 224 | _weight += i[0].size[0] + 25 225 | merge_file = cache_dir / ("weekly.all.jpg" if is_weekly else f"daily.{day}.all.jpg") 226 | merge.convert("RGB").save(merge_file) 227 | logger.info(f"{'周本' if is_weekly else '每日'}材料图片合并完毕 {merge_file.name}") 228 | return merge_file 229 | 230 | 231 | async def draw_calculator(name: str, target: Dict, calculate: Dict) -> Union[bytes, str]: 232 | """原神计算器材料图片绘制""" 233 | height = sum(80 + ceil(len(v) / 2) * 70 + 20 for _, v in calculate.items() if v) + 20 234 | img = Image.new("RGBA", (800, height), "#FEFEFE") 235 | drawer = ImageDraw.Draw(img) 236 | 237 | icon_bg = Image.new("RGBA", (100, 100)) 238 | ImageDraw.Draw(icon_bg).rounded_rectangle( 239 | (0, 0, 100, 100), radius=10, fill="#a58d83", width=0 240 | ) 241 | icon_bg = icon_bg.resize((50, 50), RESAMPLING) 242 | 243 | draw_X, draw_Y = 20, 20 244 | for key, consume in calculate.items(): 245 | if not consume: 246 | continue 247 | 248 | # 背景纯色 249 | block_height = 80 + ceil(len(consume) / 2) * 70 250 | drawer.rectangle( 251 | ((20, draw_Y), (800 - 20 - 1, draw_Y + block_height)), 252 | fill="#f1ede4", 253 | width=0, 254 | ) 255 | 256 | # 标题 257 | if key == "avatar_consume": 258 | title_left, title_right = ( 259 | f"{name}·角色消耗", 260 | f"Lv.{target['avatar_level_current']} >>> Lv.{target['avatar_level_target']}", 261 | ) 262 | elif key == "avatar_skill_consume": 263 | title_left, title_right = f"{name}·天赋消耗", " ".join( 264 | f"Lv.{_skill['level_current']}>{_skill['level_target']}" 265 | for _skill in target["skill_list"] 266 | if _skill["level_current"] != _skill["level_target"] 267 | ) 268 | elif key == "weapon_consume": 269 | title_left, title_right = ( 270 | f"{name}·升级消耗", 271 | f"Lv.{target['weapon']['level_current']} >>> Lv.{target['weapon']['level_target']}", 272 | ) 273 | else: 274 | raise ValueError("材料计算器无法计算圣遗物消耗") 275 | # 左侧标题 276 | drawer.text( 277 | (draw_X + 40, int(draw_Y + (80 - font(40).getbbox("高")[-1]) / 2)), 278 | title_left, 279 | fill="#8b7770", 280 | font=font(40), 281 | ) 282 | # 右侧标题字体大小自适应 283 | _size = 40 - len(title_right.split(" ")) * 3 284 | _text_width, _text_height = font(_size).getbbox(title_right)[-2:] 285 | drawer.text( 286 | (800 - 70 - _text_width, int(draw_Y + (80 - _text_height) / 2)), 287 | title_right, 288 | fill="#8b7770", 289 | font=font(_size), 290 | ) 291 | 292 | # 材料 293 | is_left, _draw_X, _draw_Y = True, draw_X + 30, draw_Y + 80 + 10 294 | for cost in consume: 295 | # 图标背景 296 | img.paste(icon_bg, (_draw_X, _draw_Y), icon_bg) 297 | # 图标 298 | _icon_path = DL_CFG["item"]["dir"] / "{}.{}".format( 299 | cost["id"] if DL_CFG["item"]["file"] == "id" else cost["name"], 300 | DL_CFG["item"]["fmt"], 301 | ) 302 | _icon_img = Image.open(_icon_path).resize((50, 50), RESAMPLING).convert("RGBA") 303 | img.paste(_icon_img, (_draw_X, _draw_Y), _icon_img) 304 | # 名称 × 数量 305 | cost_str = f"{cost['name']} × {cost['num']}" 306 | drawer.text( 307 | (_draw_X + 65, _draw_Y + int((50 - font(30).getbbox(cost_str)[-1]) / 2)), 308 | cost_str, 309 | fill="#967b68", 310 | font=font(30), 311 | ) 312 | if is_left: 313 | _draw_X += 370 314 | else: 315 | _draw_X = draw_X + 30 316 | _draw_Y += 70 317 | is_left = not is_left 318 | 319 | draw_Y += block_height + 20 320 | 321 | buf = BytesIO() 322 | img.save(buf, format="PNG", quality=100) 323 | return buf.getvalue() 324 | -------------------------------------------------------------------------------- /data/gsmaterial/item-alias.json: -------------------------------------------------------------------------------- 1 | { 2 | "11101": [ 3 | "无锋剑", 4 | "无锋" 5 | ], 6 | "11201": [ 7 | "银剑" 8 | ], 9 | "11301": [ 10 | "冷刃" 11 | ], 12 | "11302": [ 13 | "黎明神剑", 14 | "黎明", 15 | "黎明剑" 16 | ], 17 | "11303": [ 18 | "旅行剑" 19 | ], 20 | "11304": [ 21 | "暗铁剑", 22 | "暗铁" 23 | ], 24 | "11305": [ 25 | "吃虎鱼刀", 26 | "吃虎鱼" 27 | ], 28 | "11306": [ 29 | "飞天御剑", 30 | "飞天剑", 31 | "飞天单手剑" 32 | ], 33 | "11401": [ 34 | "西风剑", 35 | "西风单手剑" 36 | ], 37 | "11402": [ 38 | "笛剑" 39 | ], 40 | "11403": [ 41 | "祭礼剑", 42 | "祭礼单手剑" 43 | ], 44 | "11404": [ 45 | "宗室长剑", 46 | "宗室剑", 47 | "宗室单手剑" 48 | ], 49 | "11405": [ 50 | "匣里龙吟", 51 | "龙吟", 52 | "匣里单手剑", 53 | "甲里龙吟" 54 | ], 55 | "11406": [ 56 | "试作斩岩", 57 | "斩岩", 58 | "试做斩岩", 59 | "试做单手剑", 60 | "试作单手剑" 61 | ], 62 | "11407": [ 63 | "铁蜂刺", 64 | "铁封刺", 65 | "铁锋刺" 66 | ], 67 | "11408": [ 68 | "黑岩长剑", 69 | "黑岩剑" 70 | ], 71 | "11409": [ 72 | "黑剑", 73 | "月卡剑" 74 | ], 75 | "11410": [ 76 | "暗巷闪光", 77 | "暗巷剑", 78 | "暗巷单手剑" 79 | ], 80 | "11412": [ 81 | "降临之剑", 82 | "降临剑", 83 | "降临" 84 | ], 85 | "11413": [ 86 | "腐殖之剑", 87 | "腐殖", 88 | "腐殖剑", 89 | "复制剑", 90 | "复制", 91 | "腐蚀剑", 92 | "腐值剑", 93 | "腐值" 94 | ], 95 | "11414": [ 96 | "天目影打刀", 97 | "天目刀", 98 | "天目", 99 | "稻妻锻造单手剑", 100 | "稻妻锻造剑", 101 | "天目影打" 102 | ], 103 | "11415": [ 104 | "辰砂之纺锤", 105 | "辰砂", 106 | "辰砂纺锤", 107 | "纺锤" 108 | ], 109 | "11416": [ 110 | "笼钓瓶一心", 111 | "妖刀", 112 | "红刀", 113 | "笼钓瓶" 114 | ], 115 | "11417": [ 116 | "原木刀", 117 | "木刀", 118 | "须弥锻造剑", 119 | "须弥锻造单手剑" 120 | ], 121 | "11418": [ 122 | "西福斯的月光", 123 | "西弗斯的月光", 124 | "西福斯", 125 | "西弗斯", 126 | "月光剑", 127 | "月光" 128 | ], 129 | "11421": [ 130 | "「一心传」名刀", 131 | "一心传名刀", 132 | "一心传" 133 | ], 134 | "11422": [ 135 | "东花坊时雨", 136 | "东花坊", 137 | "小红伞", 138 | "红伞", 139 | "纸伞", 140 | "雨伞", 141 | "四星伞", 142 | "伞", 143 | "时雨" 144 | ], 145 | "11424": [ 146 | "狼牙", 147 | "狼牙剑" 148 | ], 149 | "11425": [ 150 | "海渊终曲", 151 | "海渊", 152 | "海渊剑", 153 | "终曲", 154 | "终曲剑" 155 | ], 156 | "11426": [ 157 | "灰河渡手", 158 | "灰河", 159 | "灰河剑", 160 | "渡手", 161 | "渡手剑", 162 | "水管" 163 | ], 164 | "11427": [ 165 | "船坞长剑", 166 | "船坞", 167 | "船乌", 168 | "船坞剑" 169 | ], 170 | "11428": [ 171 | "水仙十字之剑", 172 | "水仙之剑", 173 | "十字之剑", 174 | "水仙十字", 175 | "水仙剑", 176 | "十字剑" 177 | ], 178 | "11429": [ 179 | "水仙十字之剑", 180 | "水仙之剑", 181 | "十字之剑", 182 | "水仙十字", 183 | "水仙剑", 184 | "十字剑" 185 | ], 186 | "11501": [ 187 | "风鹰剑", 188 | "风鹰", 189 | "风影", 190 | "疯鸭", 191 | "疯鸭剑", 192 | "风压", 193 | "风压剑" 194 | ], 195 | "11502": [ 196 | "天空之刃", 197 | "天空剑", 198 | "天空单手剑", 199 | "天空刃" 200 | ], 201 | "11503": [ 202 | "苍古自由之誓", 203 | "苍古", 204 | "苍古剑", 205 | "苍古单手剑", 206 | "乐团剑" 207 | ], 208 | "11504": [ 209 | "斫峰之刃", 210 | "斫峰", 211 | "盾剑" 212 | ], 213 | "11505": [ 214 | "磐岩结绿", 215 | "绿箭", 216 | "绿剑" 217 | ], 218 | "11509": [ 219 | "雾切之回光", 220 | "雾切" 221 | ], 222 | "11510": [ 223 | "波乱月白经津", 224 | "波乱月", 225 | "白经津", 226 | "波乱月白", 227 | "波乱", 228 | "月白", 229 | "经津", 230 | "波波津", 231 | "啵啵剑", 232 | "波波剑", 233 | "月经剑" 234 | ], 235 | "11511": [ 236 | "圣显之钥", 237 | "板砖单手剑", 238 | "小板砖", 239 | "圣显", 240 | "板砖" 241 | ], 242 | "11512": [ 243 | "裁叶萃光", 244 | "裁叶翠光", 245 | "白月枝芒", 246 | "菜叶", 247 | "裁叶", 248 | "萃光", 249 | "翠光", 250 | "绿叶" 251 | ], 252 | "11513": [ 253 | "静水流涌之辉", 254 | "静水", 255 | "流涌", 256 | "静水流涌" 257 | ], 258 | "11514": [ 259 | "有乐御簾切" 260 | ], 261 | "12101": [ 262 | "训练大剑" 263 | ], 264 | "12201": [ 265 | "佣兵重剑", 266 | "佣兵大剑" 267 | ], 268 | "12301": [ 269 | "铁影阔剑", 270 | "铁影", 271 | "阔剑" 272 | ], 273 | "12302": [ 274 | "沐浴龙血的剑", 275 | "沐浴龙血", 276 | "龙血剑" 277 | ], 278 | "12303": [ 279 | "白铁大剑", 280 | "白铁", 281 | "白铁剑" 282 | ], 283 | "12304": [ 284 | "石英大剑", 285 | "石英剑" 286 | ], 287 | "12305": [ 288 | "以理服人", 289 | "狼牙棒" 290 | ], 291 | "12306": [ 292 | "飞天大御剑", 293 | "飞天大剑" 294 | ], 295 | "12401": [ 296 | "西风大剑" 297 | ], 298 | "12402": [ 299 | "钟剑" 300 | ], 301 | "12403": [ 302 | "祭礼大剑", 303 | "祭礼双手剑" 304 | ], 305 | "12404": [ 306 | "宗室大剑", 307 | "宗室双手剑" 308 | ], 309 | "12405": [ 310 | "雨裁", 311 | "雨载", 312 | "雨栽" 313 | ], 314 | "12406": [ 315 | "试作古华", 316 | "古华", 317 | "试做古华", 318 | "试作大剑", 319 | "试做大剑" 320 | ], 321 | "12407": [ 322 | "白影剑", 323 | "白影大剑" 324 | ], 325 | "12408": [ 326 | "黑岩斩刀", 327 | "黑岩刀", 328 | "黑岩大剑" 329 | ], 330 | "12409": [ 331 | "螭骨剑", 332 | "螭骨", 333 | "丈育剑", 334 | "离骨剑", 335 | "月卡大剑" 336 | ], 337 | "12410": [ 338 | "千岩古剑", 339 | "千岩剑", 340 | "千岩大剑" 341 | ], 342 | "12411": [ 343 | "雪葬的星银", 344 | "雪葬", 345 | "星银", 346 | "雪葬星银", 347 | "雪山大剑" 348 | ], 349 | "12412": [ 350 | "衔珠海皇", 351 | "海皇", 352 | "咸鱼剑", 353 | "咸鱼大剑" 354 | ], 355 | "12414": [ 356 | "桂木斩长正", 357 | "桂木", 358 | "斩长正", 359 | "稻妻锻造大剑" 360 | ], 361 | "12415": [ 362 | "玛海菈的水色", 363 | "玛海菈", 364 | "玛海拉", 365 | "马海拉", 366 | "水色", 367 | "玛菈", 368 | "玛拉" 369 | ], 370 | "12416": [ 371 | "恶王丸", 372 | "恶丸", 373 | "恶王" 374 | ], 375 | "12417": [ 376 | "森林王器", 377 | "王器", 378 | "须弥锻造大剑" 379 | ], 380 | "12418": [ 381 | "饰铁之花", 382 | "饰铁大剑", 383 | "铁花大剑", 384 | "风花大剑", 385 | "饰铁", 386 | "铁花", 387 | "风花", 388 | "饰铁花剑", 389 | "花剑" 390 | ], 391 | "12424": [ 392 | "聊聊棒", 393 | "聊聊", 394 | "聊聊剑" 395 | ], 396 | "12425": [ 397 | "浪影阔剑", 398 | "浪影", 399 | "浪影剑", 400 | "阔剑" 401 | ], 402 | "12426": [ 403 | "「究极霸王超级魔剑」" 404 | ], 405 | "12427": [ 406 | "便携动力锯", 407 | "便携", 408 | "动力锯", 409 | "电锯", 410 | "电锯大剑" 411 | ], 412 | "12501": [ 413 | "天空之傲", 414 | "天空大剑" 415 | ], 416 | "12502": [ 417 | "狼的末路", 418 | "狼末" 419 | ], 420 | "12503": [ 421 | "松籁响起之时", 422 | "松籁", 423 | "松赖", 424 | "乐团大剑", 425 | "松剑" 426 | ], 427 | "12504": [ 428 | "无工之剑", 429 | "蜈蚣", 430 | "蜈蚣大剑", 431 | "无工大剑", 432 | "盾大剑", 433 | "无工", 434 | "痛苦大剑" 435 | ], 436 | "12510": [ 437 | "赤角石溃杵", 438 | "赤角", 439 | "石溃杵", 440 | "赤杵", 441 | "鬼刀" 442 | ], 443 | "12511": [ 444 | "苇海信标", 445 | "板砖大剑", 446 | "大板砖", 447 | "苇海", 448 | "信标", 449 | "韦海信标", 450 | "沙大剑" 451 | ], 452 | "12512": [ 453 | "裁断", 454 | "栽断", 455 | "斧头", 456 | "大斧", 457 | "大斧头" 458 | ], 459 | "13101": [ 460 | "新手长枪", 461 | "新手枪" 462 | ], 463 | "13201": [ 464 | "铁尖枪", 465 | "铁尖" 466 | ], 467 | "13301": [ 468 | "白缨枪", 469 | "白缨" 470 | ], 471 | "13302": [ 472 | "钺矛", 473 | "越毛" 474 | ], 475 | "13303": [ 476 | "黑缨枪", 477 | "史莱姆枪" 478 | ], 479 | "13304": [ 480 | "「旗杆」" 481 | ], 482 | "13401": [ 483 | "匣里灭辰", 484 | "灭辰", 485 | "匣里长枪", 486 | "匣里枪", 487 | "甲里灭辰" 488 | ], 489 | "13402": [ 490 | "试作星镰", 491 | "试做星镰", 492 | "试作长枪", 493 | "试做长枪", 494 | "星镰", 495 | "试作枪", 496 | "试做枪" 497 | ], 498 | "13403": [ 499 | "流月针", 500 | "针", 501 | "留月针" 502 | ], 503 | "13404": [ 504 | "黑岩刺枪", 505 | "黑岩枪" 506 | ], 507 | "13405": [ 508 | "决斗之枪", 509 | "决斗枪", 510 | "决斗", 511 | "月卡枪" 512 | ], 513 | "13406": [ 514 | "千岩长枪", 515 | "千岩枪" 516 | ], 517 | "13407": [ 518 | "西风长枪", 519 | "西风枪" 520 | ], 521 | "13408": [ 522 | "宗室猎枪", 523 | "宗室枪", 524 | "宗室长枪" 525 | ], 526 | "13409": [ 527 | "龙脊长枪", 528 | "雪山枪", 529 | "雪山长枪" 530 | ], 531 | "13414": [ 532 | "喜多院十文字", 533 | "喜多院", 534 | "十文字", 535 | "稻妻锻造枪" 536 | ], 537 | "13415": [ 538 | "「渔获」", 539 | "渔获", 540 | "鱼叉", 541 | "渔叉", 542 | "鱼获" 543 | ], 544 | "13416": [ 545 | "断浪长鳍", 546 | "断浪", 547 | "断浪长枪", 548 | "断浪枪" 549 | ], 550 | "13417": [ 551 | "贯月矢", 552 | "贯月矢", 553 | "月矢", 554 | "须弥锻造枪" 555 | ], 556 | "13419": [ 557 | "风信之锋", 558 | "风信之峰", 559 | "路标" 560 | ], 561 | "13424": [ 562 | "峡湾长歌", 563 | "峡湾", 564 | "长歌", 565 | "狭湾长歌" 566 | ], 567 | "13425": [ 568 | "公义的酬报", 569 | "公义的报酬", 570 | "酬报", 571 | "报酬" 572 | ], 573 | "13426": [ 574 | "沙中伟贤的对答" 575 | ], 576 | "13427": [ 577 | "勘探钻机", 578 | "钻地机", 579 | "钻机", 580 | "钻地机长枪" 581 | ], 582 | "13501": [ 583 | "护摩之杖", 584 | "护摩", 585 | "护摩枪", 586 | "护膜" 587 | ], 588 | "13502": [ 589 | "天空之脊", 590 | "天空枪", 591 | "薄荷枪", 592 | "薄荷" 593 | ], 594 | "13504": [ 595 | "贯虹之槊", 596 | "贯虹", 597 | "岩枪", 598 | "盾枪" 599 | ], 600 | "13505": [ 601 | "和璞鸢", 602 | "鸟枪", 603 | "绿枪", 604 | "绿叉" 605 | ], 606 | "13507": [ 607 | "息灾" 608 | ], 609 | "13509": [ 610 | "薙草之稻光", 611 | "薙草", 612 | "稻光", 613 | "薙草稻光", 614 | "马尾枪", 615 | "马尾", 616 | "薙刀" 617 | ], 618 | "13511": [ 619 | "赤沙之杖", 620 | "板砖长枪", 621 | "板砖枪", 622 | "赤沙", 623 | "船桨" 624 | ], 625 | "14101": [ 626 | "学徒笔记" 627 | ], 628 | "14201": [ 629 | "口袋魔导书" 630 | ], 631 | "14301": [ 632 | "魔导绪论" 633 | ], 634 | "14302": [ 635 | "讨龙英杰谭", 636 | "讨龙" 637 | ], 638 | "14303": [ 639 | "异世界行记", 640 | "异世界" 641 | ], 642 | "14304": [ 643 | "翡玉法球", 644 | "翡玉", 645 | "法球" 646 | ], 647 | "14305": [ 648 | "甲级宝珏", 649 | "宝珏", 650 | "宝玉", 651 | "甲级宝玉" 652 | ], 653 | "14306": [ 654 | "琥珀玥" 655 | ], 656 | "14401": [ 657 | "西风秘典", 658 | "西风法器", 659 | "西风书" 660 | ], 661 | "14402": [ 662 | "流浪乐章", 663 | "赌狗书", 664 | "赌狗乐章", 665 | "赌狗" 666 | ], 667 | "14403": [ 668 | "祭礼残章", 669 | "祭礼法器", 670 | "祭礼书" 671 | ], 672 | "14404": [ 673 | "宗室秘法录", 674 | "宗室法器", 675 | "宗室书" 676 | ], 677 | "14405": [ 678 | "匣里日月", 679 | "日月", 680 | "匣里法器", 681 | "月卡法器", 682 | "甲里日月" 683 | ], 684 | "14406": [ 685 | "试作金珀", 686 | "金珀", 687 | "试做金珀", 688 | "试做金箔", 689 | "试作法器", 690 | "试做法器" 691 | ], 692 | "14407": [ 693 | "万国诸海图谱", 694 | "万国", 695 | "万国诸海" 696 | ], 697 | "14408": [ 698 | "黑岩绯玉", 699 | "黑岩法器", 700 | "黑岩书" 701 | ], 702 | "14409": [ 703 | "昭心", 704 | "糟心" 705 | ], 706 | "14410": [ 707 | "暗巷的酒与诗", 708 | "暗巷书", 709 | "酒与诗", 710 | "暗巷法器" 711 | ], 712 | "14412": [ 713 | "忍冬之果", 714 | "雪山法器", 715 | "忍冬" 716 | ], 717 | "14413": [ 718 | "嘟嘟可故事集", 719 | "嘟嘟可", 720 | "故事集" 721 | ], 722 | "14414": [ 723 | "白辰之环", 724 | "白辰", 725 | "白辰环", 726 | "稻妻锻造法器" 727 | ], 728 | "14415": [ 729 | "证誓之明瞳", 730 | "证誓", 731 | "明瞳", 732 | "证誓明瞳", 733 | "正誓", 734 | "正誓明瞳" 735 | ], 736 | "14416": [ 737 | "流浪的晚星", 738 | "流浪晚星", 739 | "晚星" 740 | ], 741 | "14417": [ 742 | "盈满之实", 743 | "盈满", 744 | "须弥锻造法器", 745 | "须弥果子" 746 | ], 747 | "14424": [ 748 | "遗祀玉珑", 749 | "遗祀", 750 | "遗己", 751 | "玉珑" 752 | ], 753 | "14425": [ 754 | "纯水流华", 755 | "纯水", 756 | "流华", 757 | "纯水流花" 758 | ], 759 | "14426": [ 760 | "无垠蔚蓝之歌", 761 | "无垠", 762 | "蔚蓝", 763 | "蔚蓝之歌", 764 | "尉蓝之歌", 765 | "蔚蓝法器" 766 | ], 767 | "14501": [ 768 | "天空之卷", 769 | "天空书", 770 | "厕纸" 771 | ], 772 | "14502": [ 773 | "四风原典", 774 | "四风", 775 | "四风书" 776 | ], 777 | "14504": [ 778 | "尘世之锁", 779 | "尘世锁", 780 | "尘世", 781 | "盾书", 782 | "锁", 783 | "痛苦锁" 784 | ], 785 | "14505": [ 786 | "碧落之珑", 787 | "碧落", 788 | "碧落之龙", 789 | "碧珑" 790 | ], 791 | "14506": [ 792 | "不灭月华", 793 | "月华" 794 | ], 795 | "14509": [ 796 | "神乐之真意", 797 | "神乐", 798 | "真意" 799 | ], 800 | "14511": [ 801 | "千夜浮梦", 802 | "千灯", 803 | "夜壶", 804 | "千夜", 805 | "神灯" 806 | ], 807 | "14512": [ 808 | "图莱杜拉的回忆", 809 | "图来杜拉的回忆", 810 | "图莱杜拉", 811 | "图来杜拉", 812 | "铃铛", 813 | "回忆", 814 | "图莱", 815 | "风铃" 816 | ], 817 | "14513": [ 818 | "金流监督", 819 | "金流", 820 | "监督", 821 | "监控器", 822 | "监视器", 823 | "无人机", 824 | "小机器人" 825 | ], 826 | "14514": [ 827 | "万世流涌大典", 828 | "万世", 829 | "大典", 830 | "万世流涌", 831 | "万世涌流" 832 | ], 833 | "14515": [ 834 | "鹤鸣余音", 835 | "鹤鸣", 836 | "余音", 837 | "羽扇", 838 | "鸟扇", 839 | "鹤扇", 840 | "鹤伞", 841 | "扇子" 842 | ], 843 | "15101": [ 844 | "猎弓" 845 | ], 846 | "15201": [ 847 | "历练的猎弓" 848 | ], 849 | "15301": [ 850 | "鸦羽弓", 851 | "鸦羽" 852 | ], 853 | "15302": [ 854 | "神射手之誓", 855 | "神射手", 856 | "脚气弓" 857 | ], 858 | "15303": [ 859 | "反曲弓", 860 | "反曲" 861 | ], 862 | "15304": [ 863 | "弹弓" 864 | ], 865 | "15305": [ 866 | "信使" 867 | ], 868 | "15306": [ 869 | "黑檀弓" 870 | ], 871 | "15401": [ 872 | "西风猎弓", 873 | "西风弓" 874 | ], 875 | "15402": [ 876 | "绝弦", 877 | "绝玄" 878 | ], 879 | "15403": [ 880 | "祭礼弓" 881 | ], 882 | "15404": [ 883 | "宗室长弓", 884 | "宗室弓" 885 | ], 886 | "15405": [ 887 | "弓藏" 888 | ], 889 | "15406": [ 890 | "试作澹月", 891 | "澹月", 892 | "试做澹月", 893 | "试做弓", 894 | "试作弓" 895 | ], 896 | "15407": [ 897 | "钢轮弓", 898 | "钢轮", 899 | "钢弓" 900 | ], 901 | "15408": [ 902 | "黑岩战弓", 903 | "黑岩弓" 904 | ], 905 | "15409": [ 906 | "苍翠猎弓", 907 | "绿弓", 908 | "月卡弓" 909 | ], 910 | "15410": [ 911 | "暗巷猎手", 912 | "暗巷弓" 913 | ], 914 | "15411": [ 915 | "落霞", 916 | "落下", 917 | "洛霞" 918 | ], 919 | "15412": [ 920 | "幽夜华尔兹", 921 | "幽夜", 922 | "幽夜弓", 923 | "华尔兹", 924 | "皇女弓" 925 | ], 926 | "15413": [ 927 | "风花之颂", 928 | "风花", 929 | "风花弓", 930 | "风花颂" 931 | ], 932 | "15414": [ 933 | "破魔之弓", 934 | "破魔", 935 | "破魔弓", 936 | "稻妻锻造弓" 937 | ], 938 | "15415": [ 939 | "掠食者", 940 | "掠食" 941 | ], 942 | "15416": [ 943 | "曚云之月", 944 | "曚云弓", 945 | "蒙云之月", 946 | "濛云之月", 947 | "朦云之月" 948 | ], 949 | "15417": [ 950 | "王下近侍", 951 | "须弥锻造弓", 952 | "近侍" 953 | ], 954 | "15418": [ 955 | "竭泽", 956 | "咸鱼弓", 957 | "鱼弓", 958 | "渔弓", 959 | "金枪鱼弓" 960 | ], 961 | "15419": [ 962 | "鹮穿之喙", 963 | "鹮穿", 964 | "鹮穿之缘", 965 | "鸟穿之喙", 966 | "鸟穿之缘" 967 | ], 968 | "15424": [ 969 | "烈阳之嗣", 970 | "烈阳之饲", 971 | "烈阳", 972 | "烈阳弓" 973 | ], 974 | "15425": [ 975 | "静谧之曲", 976 | "静谧弓", 977 | "静谧", 978 | "精密" 979 | ], 980 | "15427": [ 981 | "测距规", 982 | "测规矩", 983 | "测距弓" 984 | ], 985 | "15501": [ 986 | "天空之翼", 987 | "天空弓" 988 | ], 989 | "15502": [ 990 | "阿莫斯之弓", 991 | "阿莫斯", 992 | "ams", 993 | "痛苦弓" 994 | ], 995 | "15503": [ 996 | "终末嗟叹之诗", 997 | "终末", 998 | "终末弓", 999 | "叹气弓", 1000 | "乐团弓" 1001 | ], 1002 | "15507": [ 1003 | "冬极白星", 1004 | "冬极", 1005 | "冬季", 1006 | "白星" 1007 | ], 1008 | "15508": [ 1009 | "若水", 1010 | "麒麟弓" 1011 | ], 1012 | "15509": [ 1013 | "飞雷之弦振", 1014 | "飞雷", 1015 | "飞雷弓", 1016 | "弦振" 1017 | ], 1018 | "15511": [ 1019 | "猎人之径", 1020 | "猎人", 1021 | "猎人直径" 1022 | ], 1023 | "15512": [ 1024 | "最初的大魔术", 1025 | "大魔术", 1026 | "大魔术弓", 1027 | "魔术", 1028 | "魔术弓" 1029 | ], 1030 | "10000002": [ 1031 | "神里绫华", 1032 | "神里", 1033 | "绫华", 1034 | "零华", 1035 | "零花", 1036 | "0华", 1037 | "0花", 1038 | "神里凌华", 1039 | "凌华", 1040 | "白鹭公主", 1041 | "神里大小姐", 1042 | "小乌龟", 1043 | "龟龟" 1044 | ], 1045 | "10000003": [ 1046 | "琴", 1047 | "团长", 1048 | "代理团长", 1049 | "琴团长", 1050 | "蒲公英骑士" 1051 | ], 1052 | "10000006": [ 1053 | "丽莎", 1054 | "图书管理员", 1055 | "图书馆管理员", 1056 | "蔷薇魔女", 1057 | "丽莎阿姨" 1058 | ], 1059 | "10000014": [ 1060 | "芭芭拉", 1061 | "巴巴拉", 1062 | "拉巴巴", 1063 | "内鬼", 1064 | "加湿器", 1065 | "闪耀偶像", 1066 | "蒙德偶像", 1067 | "偶像", 1068 | "佩奇" 1069 | ], 1070 | "10000015": [ 1071 | "凯亚", 1072 | "盖亚", 1073 | "凯子哥", 1074 | "凯鸭", 1075 | "矿工", 1076 | "矿工头子", 1077 | "骑兵队长", 1078 | "凯子", 1079 | "凝冰渡海真君", 1080 | "亚尔伯里奇" 1081 | ], 1082 | "10000016": [ 1083 | "迪卢克", 1084 | "卢姥爷", 1085 | "姥爷", 1086 | "卢老爷", 1087 | "卢锅巴", 1088 | "正义人", 1089 | "卢本伟", 1090 | "暗夜英雄", 1091 | "卢卢伯爵", 1092 | "落魄了", 1093 | "落魄了家人们", 1094 | "莱艮芬德" 1095 | ], 1096 | "10000020": [ 1097 | "雷泽", 1098 | "狼少年", 1099 | "狼崽子", 1100 | "狼崽", 1101 | "卢皮卡", 1102 | "小狼", 1103 | "小狼狗", 1104 | "狼孩" 1105 | ], 1106 | "10000021": [ 1107 | "安柏", 1108 | "安伯", 1109 | "兔兔伯爵", 1110 | "飞行冠军", 1111 | "蒙德飞行冠军", 1112 | "侦查骑士", 1113 | "点火姬", 1114 | "点火机", 1115 | "打火机", 1116 | "打火姬" 1117 | ], 1118 | "10000022": [ 1119 | "温迪", 1120 | "温蒂", 1121 | "风神", 1122 | "卖唱的", 1123 | "巴巴托斯", 1124 | "巴巴脱丝", 1125 | "芭芭托斯", 1126 | "芭芭脱丝", 1127 | "干点正事", 1128 | "不干正事", 1129 | "吟游诗人", 1130 | "诶嘿", 1131 | "唉嘿", 1132 | "摸鱼" 1133 | ], 1134 | "10000023": [ 1135 | "香菱", 1136 | "香玲", 1137 | "锅巴", 1138 | "厨师", 1139 | "万民堂厨师", 1140 | "香师傅", 1141 | "卯香菱" 1142 | ], 1143 | "10000024": [ 1144 | "北斗", 1145 | "大姐头", 1146 | "大姐", 1147 | "无冕的龙王", 1148 | "龙王" 1149 | ], 1150 | "10000025": [ 1151 | "行秋", 1152 | "秋秋人", 1153 | "秋妹妹", 1154 | "书呆子", 1155 | "飞云商会二少爷" 1156 | ], 1157 | "10000026": [ 1158 | "魈", 1159 | "打桩机", 1160 | "插秧", 1161 | "三眼五显仙人", 1162 | "三眼五显真人", 1163 | "降魔大圣", 1164 | "护法夜叉", 1165 | "无聊", 1166 | "靖妖傩舞", 1167 | "三点五尺仙人" 1168 | ], 1169 | "10000027": [ 1170 | "凝光", 1171 | "富婆", 1172 | "天权星", 1173 | "天权" 1174 | ], 1175 | "10000029": [ 1176 | "可莉", 1177 | "嘟嘟可", 1178 | "火花骑士", 1179 | "蹦蹦炸弹", 1180 | "炸鱼", 1181 | "放火烧山", 1182 | "放火烧山真君", 1183 | "逃跑的太阳", 1184 | "啦啦啦", 1185 | "哒哒哒", 1186 | "炸弹人", 1187 | "禁闭室", 1188 | "太阳", 1189 | "小太阳" 1190 | ], 1191 | "10000030": [ 1192 | "钟离", 1193 | "摩拉克斯", 1194 | "岩王爷", 1195 | "岩神", 1196 | "钟师傅", 1197 | "天动万象", 1198 | "岩王帝君", 1199 | "未来可期", 1200 | "帝君", 1201 | "拒收病婿" 1202 | ], 1203 | "10000031": [ 1204 | "菲谢尔", 1205 | "皇女", 1206 | "小艾米", 1207 | "小艾咪", 1208 | "奥兹", 1209 | "断罪皇女", 1210 | "中二病", 1211 | "中二少女", 1212 | "中二皇女", 1213 | "奥兹发射器", 1214 | "露弗施洛斯", 1215 | "那菲多特" 1216 | ], 1217 | "10000032": [ 1218 | "班尼特", 1219 | "点赞哥", 1220 | "点赞", 1221 | "倒霉少年", 1222 | "倒霉蛋", 1223 | "班神", 1224 | "班爷", 1225 | "倒霉", 1226 | "六星真神" 1227 | ], 1228 | "10000033": [ 1229 | "达达利亚", 1230 | "达达鸭", 1231 | "达达利鸭", 1232 | "公子", 1233 | "玩具销售员", 1234 | "玩具推销员", 1235 | "钱包", 1236 | "鸭鸭", 1237 | "愚人众末席", 1238 | "阿贾克斯" 1239 | ], 1240 | "10000034": [ 1241 | "诺艾尔", 1242 | "女仆", 1243 | "高达", 1244 | "岩王帝姬" 1245 | ], 1246 | "10000035": [ 1247 | "七七", 1248 | "僵尸", 1249 | "肚饿真君", 1250 | "度厄真君", 1251 | "77" 1252 | ], 1253 | "10000036": [ 1254 | "重云", 1255 | "纯阳之体", 1256 | "冰棍" 1257 | ], 1258 | "10000037": [ 1259 | "甘雨", 1260 | "椰羊", 1261 | "椰奶", 1262 | "王小美" 1263 | ], 1264 | "10000038": [ 1265 | "阿贝多", 1266 | "升降机", 1267 | "升降台", 1268 | "电梯", 1269 | "白垩之子", 1270 | "白垩", 1271 | "阿贝少", 1272 | "花呗多", 1273 | "阿贝夕", 1274 | "abd", 1275 | "阿师傅" 1276 | ], 1277 | "10000039": [ 1278 | "迪奥娜", 1279 | "迪欧娜", 1280 | "dio", 1281 | "dio娜", 1282 | "冰猫", 1283 | "猫猫", 1284 | "猫娘", 1285 | "喵喵", 1286 | "调酒师", 1287 | "凯茨莱茵" 1288 | ], 1289 | "10000041": [ 1290 | "莫娜", 1291 | "莫纳", 1292 | "占星术士", 1293 | "占星师", 1294 | "讨龙真君", 1295 | "半部讨龙真君", 1296 | "阿斯托洛吉斯", 1297 | "梅姬斯图斯", 1298 | "梅姬斯图斯姬" 1299 | ], 1300 | "10000042": [ 1301 | "刻晴", 1302 | "刻情", 1303 | "氪晴", 1304 | "刻师傅", 1305 | "刻师父", 1306 | "牛杂", 1307 | "牛杂师傅", 1308 | "斩尽牛杂", 1309 | "屁斜剑法", 1310 | "玉衡星", 1311 | "玉衡", 1312 | "阿晴", 1313 | "啊晴", 1314 | "璃月雷神" 1315 | ], 1316 | "10000043": [ 1317 | "砂糖", 1318 | "雷莹术士", 1319 | "雷萤术士", 1320 | "雷荧术士" 1321 | ], 1322 | "10000044": [ 1323 | "辛焱", 1324 | "辛炎", 1325 | "薪炎", 1326 | "黑妹", 1327 | "摇滚" 1328 | ], 1329 | "10000045": [ 1330 | "罗莎莉亚", 1331 | "罗莎莉娅", 1332 | "白色史莱姆", 1333 | "白史莱姆", 1334 | "修女", 1335 | "罗莎利亚", 1336 | "罗莎利娅", 1337 | "罗沙莉亚", 1338 | "罗沙莉娅", 1339 | "罗沙利亚", 1340 | "罗沙利娅", 1341 | "萝莎莉亚", 1342 | "萝莎莉娅", 1343 | "萝莎利亚", 1344 | "萝莎利娅", 1345 | "萝沙莉亚", 1346 | "萝沙莉娅", 1347 | "萝沙利亚", 1348 | "萝沙利娅" 1349 | ], 1350 | "10000046": [ 1351 | "胡桃", 1352 | "胡淘", 1353 | "往生堂堂主", 1354 | "火化", 1355 | "抬棺的", 1356 | "蝴蝶", 1357 | "核桃", 1358 | "堂主", 1359 | "胡堂主", 1360 | "雪霁梅香", 1361 | "桃子" 1362 | ], 1363 | "10000047": [ 1364 | "枫原万叶", 1365 | "万叶", 1366 | "叶天帝", 1367 | "天帝", 1368 | "叶师傅" 1369 | ], 1370 | "10000048": [ 1371 | "烟绯", 1372 | "烟老师", 1373 | "律师", 1374 | "罗翔" 1375 | ], 1376 | "10000049": [ 1377 | "宵宫", 1378 | "霄宫", 1379 | "烟花", 1380 | "肖宫", 1381 | "肖工", 1382 | "绷带女孩", 1383 | "长野原宵宫" 1384 | ], 1385 | "10000050": [ 1386 | "托马", 1387 | "家政官", 1388 | "太郎丸", 1389 | "地头蛇", 1390 | "男仆", 1391 | "拖马" 1392 | ], 1393 | "10000051": [ 1394 | "优菈", 1395 | "优拉", 1396 | "尤拉", 1397 | "尤菈", 1398 | "浪花骑士", 1399 | "记仇", 1400 | "优菈劳伦斯", 1401 | "劳伦斯" 1402 | ], 1403 | "10000052": [ 1404 | "雷电将军", 1405 | "雷神", 1406 | "将军", 1407 | "雷军", 1408 | "巴尔", 1409 | "阿影", 1410 | "影", 1411 | "巴尔泽布", 1412 | "煮饭婆", 1413 | "奶香一刀", 1414 | "无想一刀", 1415 | "宅女" 1416 | ], 1417 | "10000053": [ 1418 | "早柚", 1419 | "早抽", 1420 | "旱抽", 1421 | "旱柚", 1422 | "小狸猫", 1423 | "狸猫", 1424 | "忍者", 1425 | "貉貉", 1426 | "貉" 1427 | ], 1428 | "10000054": [ 1429 | "珊瑚宫心海", 1430 | "心海", 1431 | "军师", 1432 | "珊瑚宫", 1433 | "书记", 1434 | "观赏鱼", 1435 | "水母", 1436 | "鱼", 1437 | "美人鱼" 1438 | ], 1439 | "10000055": [ 1440 | "五郎", 1441 | "柴犬", 1442 | "希娜", 1443 | "希娜小姐" 1444 | ], 1445 | "10000056": [ 1446 | "九条裟罗", 1447 | "九条", 1448 | "九条沙罗", 1449 | "裟罗", 1450 | "沙罗", 1451 | "天狗" 1452 | ], 1453 | "10000057": [ 1454 | "荒泷一斗", 1455 | "荒龙一斗", 1456 | "荒泷天下第一斗", 1457 | "一斗", 1458 | "一抖", 1459 | "荒泷", 1460 | "1斗", 1461 | "牛牛", 1462 | "斗子哥", 1463 | "牛子哥", 1464 | "牛子", 1465 | "孩子王", 1466 | "斗虫", 1467 | "巧乐兹", 1468 | "放牛的" 1469 | ], 1470 | "10000058": [ 1471 | "八重神子", 1472 | "八重", 1473 | "神子", 1474 | "狐狸", 1475 | "想得美哦", 1476 | "巫女", 1477 | "屑狐狸", 1478 | "八重宫司", 1479 | "婶子", 1480 | "小八", 1481 | "老八" 1482 | ], 1483 | "10000059": [ 1484 | "鹿野院平藏", 1485 | "鹿野苑", 1486 | "鹿野院", 1487 | "平藏", 1488 | "鹿野苑平藏", 1489 | "小鹿" 1490 | ], 1491 | "10000060": [ 1492 | "夜兰", 1493 | "夜阑", 1494 | "叶澜", 1495 | "腋兰", 1496 | "夜天后" 1497 | ], 1498 | "10000061": [ 1499 | "绮良良", 1500 | "稻妻猫猫", 1501 | "绮娘娘", 1502 | "琦良良", 1503 | "良良", 1504 | "快递员", 1505 | "草猫", 1506 | "草猫猫", 1507 | "草喵", 1508 | "草喵喵", 1509 | "猫又" 1510 | ], 1511 | "10000062": [ 1512 | "埃洛伊", 1513 | "艾罗伊", 1514 | "艾洛伊", 1515 | "艾洛一" 1516 | ], 1517 | "10000063": [ 1518 | "申鹤", 1519 | "神鹤", 1520 | "小姨", 1521 | "小姨子", 1522 | "审核" 1523 | ], 1524 | "10000064": [ 1525 | "云堇", 1526 | "云瑾", 1527 | "云先生", 1528 | "云锦", 1529 | "神女劈观" 1530 | ], 1531 | "10000065": [ 1532 | "久岐忍", 1533 | "97忍", 1534 | "小忍", 1535 | "久歧忍", 1536 | "茄忍", 1537 | "茄子", 1538 | "紫茄子", 1539 | "阿忍", 1540 | "忍姐" 1541 | ], 1542 | "10000066": [ 1543 | "神里绫人", 1544 | "绫人", 1545 | "神里凌人", 1546 | "凌人", 1547 | "0人", 1548 | "神人", 1549 | "零人", 1550 | "大舅哥" 1551 | ], 1552 | "10000067": [ 1553 | "柯莱", 1554 | "柯来", 1555 | "科莱", 1556 | "科来", 1557 | "小天使", 1558 | "须弥飞行冠军", 1559 | "见习巡林员", 1560 | "巡林员" 1561 | ], 1562 | "10000068": [ 1563 | "多莉", 1564 | "多利", 1565 | "多力", 1566 | "多丽", 1567 | "奸商" 1568 | ], 1569 | "10000069": [ 1570 | "提纳里", 1571 | "小提", 1572 | "提那里", 1573 | "提这里", 1574 | "缇娜里", 1575 | "提哪里", 1576 | "巡林官" 1577 | ], 1578 | "10000070": [ 1579 | "妮露", 1580 | "尼露", 1581 | "尼禄", 1582 | "妮璐", 1583 | "舞娘", 1584 | "红牛" 1585 | ], 1586 | "10000071": [ 1587 | "赛诺", 1588 | "塞诺", 1589 | "胡狼", 1590 | "大风纪官", 1591 | "大风机关" 1592 | ], 1593 | "10000072": [ 1594 | "坎蒂丝", 1595 | "坎迪斯" 1596 | ], 1597 | "10000073": [ 1598 | "纳西妲", 1599 | "草王", 1600 | "草神", 1601 | "草萝莉", 1602 | "纳西坦", 1603 | "小吉祥", 1604 | "大吉祥", 1605 | "小草神", 1606 | "小草王", 1607 | "大慈树王", 1608 | "小吉祥草王", 1609 | "羽毛球", 1610 | "摩诃善法大吉祥智慧主", 1611 | "智慧之神", 1612 | "智慧主", 1613 | "布耶尔" 1614 | ], 1615 | "10000074": [ 1616 | "莱依拉", 1617 | "来一拉", 1618 | "拉一拉", 1619 | "莱依菈", 1620 | "来依菈", 1621 | "来依拉", 1622 | "来拉", 1623 | "来辣" 1624 | ], 1625 | "10000075": [ 1626 | "流浪者", 1627 | "散兵", 1628 | "国崩", 1629 | "雷电国崩", 1630 | "大炮", 1631 | "雷电大炮", 1632 | "雷大炮", 1633 | "伞兵", 1634 | "斯卡拉姆齐", 1635 | "七叶寂照秘密主", 1636 | "正机之神" 1637 | ], 1638 | "10000076": [ 1639 | "珐露珊", 1640 | "法露珊", 1641 | "法璐珊", 1642 | "法露姗", 1643 | "法璐姗", 1644 | "珐露姗", 1645 | "珐璐姗", 1646 | "百岁山", 1647 | "百岁姗", 1648 | "百岁珊", 1649 | "前辈", 1650 | "仙贝", 1651 | "初音", 1652 | "初音未来" 1653 | ], 1654 | "10000077": [ 1655 | "瑶瑶", 1656 | "遥遥", 1657 | "月桂", 1658 | "萝卜" 1659 | ], 1660 | "10000078": [ 1661 | "艾尔海森", 1662 | "代理贤者", 1663 | "埃尔海森", 1664 | "书记官", 1665 | "海哥", 1666 | "海森", 1667 | "海参" 1668 | ], 1669 | "10000079": [ 1670 | "迪希雅", 1671 | "迪希亚", 1672 | "迪希娅", 1673 | "迪西雅", 1674 | "迪西亚", 1675 | "炎拳" 1676 | ], 1677 | "10000080": [ 1678 | "米卡", 1679 | "米咖", 1680 | "鹦鹉" 1681 | ], 1682 | "10000081": [ 1683 | "卡维", 1684 | "室友", 1685 | "建筑师", 1686 | "设计师", 1687 | "建筑设计师" 1688 | ], 1689 | "10000082": [ 1690 | "白术", 1691 | "长生", 1692 | "白大夫", 1693 | "白医生", 1694 | "白求恩" 1695 | ], 1696 | "10000083": [ 1697 | "琳妮特", 1698 | "林妮特", 1699 | "林尼特", 1700 | "琳尼特", 1701 | "登登", 1702 | "噔噔" 1703 | ], 1704 | "10000084": [ 1705 | "林尼", 1706 | "大魔术师", 1707 | "魔术师", 1708 | "琳妮", 1709 | "林妮" 1710 | ], 1711 | "10000085": [ 1712 | "菲米尼", 1713 | "费米你", 1714 | "费米妮", 1715 | "菲米妮", 1716 | "菲米你", 1717 | "费米尼", 1718 | "非米尼", 1719 | "潜水员" 1720 | ], 1721 | "10000086": [ 1722 | "莱欧斯利", 1723 | "莱欧", 1724 | "枫丹桑博", 1725 | "桑博" 1726 | ], 1727 | "10000087": [ 1728 | "那维莱特", 1729 | "那维", 1730 | "水龙王", 1731 | "水龙", 1732 | "审判官", 1733 | "海獭", 1734 | "龙王" 1735 | ], 1736 | "10000088": [ 1737 | "夏洛蒂", 1738 | "夏洛", 1739 | "夏洛帝", 1740 | "记者", 1741 | "小记者" 1742 | ], 1743 | "10000089": [ 1744 | "芙宁娜", 1745 | "fufu", 1746 | "水神", 1747 | "芙芙", 1748 | "芙卡洛斯" 1749 | ], 1750 | "10000090": [ 1751 | "夏沃蕾" 1752 | ], 1753 | "10000091": [ 1754 | "娜维娅" 1755 | ], 1756 | "10000092": [ 1757 | "嘉明", 1758 | "佳明", 1759 | "嘉铭", 1760 | "家明", 1761 | "镖师", 1762 | "舞狮少年", 1763 | "舞狮" 1764 | ], 1765 | "10000093": [ 1766 | "闲云", 1767 | "流云", 1768 | "刘云", 1769 | "留云", 1770 | "野鹤", 1771 | "那个女人", 1772 | "留云借风", 1773 | "留云借风真君", 1774 | "很会聊天", 1775 | "很会聊天真君" 1776 | ], 1777 | "10000094": [ 1778 | "千织", 1779 | "千只", 1780 | "设计师", 1781 | "裁缝", 1782 | "千织屋老板" 1783 | ] 1784 | } -------------------------------------------------------------------------------- /nonebot_plugin_gsmaterial/data_source.py: -------------------------------------------------------------------------------- 1 | import json 2 | import asyncio 3 | from time import time 4 | from io import BytesIO 5 | from re import findall 6 | from hashlib import md5 7 | from pathlib import Path 8 | from random import randint 9 | from datetime import datetime, timedelta 10 | from typing import Dict, Tuple, Union, Literal, Optional 11 | 12 | from PIL import Image 13 | from httpx import HTTPError, AsyncClient 14 | 15 | from nonebot.log import logger 16 | 17 | from .material_draw import draw_materials, draw_calculator 18 | from .config import ( 19 | TZ, 20 | MYS, 21 | AMBR, 22 | DL_CFG, 23 | DL_MIRROR, 24 | CONFIG_DIR, 25 | ITEM_ALIAS, 26 | SKIP_THREE, 27 | WEEKLY_BOSS, 28 | ) 29 | 30 | _WEEKLY_BOSS = WEEKLY_BOSS[:-1] 31 | 32 | 33 | async def sub_helper( 34 | mode: Literal["r", "ag", "ap", "dg", "dp"] = "r", id: Union[str, int] = "" 35 | ) -> Union[Dict, str]: 36 | """订阅配置助手,支持读取 ``r(ead)`` 配置、添加 ``a(dd)`` 群组 ``g(roup)`` 订阅、添加私聊 ``p(rivate)`` 订阅、删除 ``d(elete)`` 群组订阅、删除私聊订阅""" 37 | 38 | cfg_file = CONFIG_DIR / "sub.json" 39 | sub_cfg = json.loads(cfg_file.read_text(encoding="UTF-8")) 40 | 41 | # 读取订阅配置 42 | if mode == "r": 43 | return sub_cfg 44 | 45 | # 添加及删除订阅配置 46 | write_key = {"g": "群组", "p": "私聊"}[mode[1]] 47 | if mode[0] == "a": 48 | # 添加群组订阅或私聊订阅 49 | if int(id) in list(sub_cfg[write_key]): 50 | return f"已经添加过当前{write_key}的原神每日材料订阅辣!" 51 | sub_cfg[write_key].append(int(id)) 52 | else: 53 | # 删除群组订阅或私聊订阅 54 | if int(id) not in list(sub_cfg[write_key]): 55 | return f"还没有添加过当前{write_key}的原神每日材料订阅哦.." 56 | sub_cfg[write_key].remove(int(id)) 57 | 58 | # 更新写入 59 | cfg_file.write_text( 60 | json.dumps(sub_cfg, ensure_ascii=False, indent=2), encoding="UTF-8" 61 | ) 62 | return f"已{'启用' if mode[0] == 'a' else '禁用'}当前{write_key}的原神每日材料订阅。" 63 | 64 | 65 | async def cookies_helper(cookie: str = "") -> Dict[str, str]: 66 | """Cookie 配置助手,支持读取、刷新、写入""" 67 | 68 | cookie_file = CONFIG_DIR / "cookie.json" 69 | cookie_cfg: Dict[str, str] = json.loads(cookie_file.read_text(encoding="UTF-8")) 70 | standard_keys = [ 71 | "account_id", 72 | "account_mid", 73 | "account_mid_v2", 74 | "cookie_token", 75 | "cookie_token_v2", 76 | "login_ticket", 77 | "login_ticket_v2", 78 | "login_uid", 79 | "login_uid_v2", 80 | "ltmid", 81 | "ltmid_v2", 82 | "ltoken", 83 | "ltoken_v2", 84 | "ltuid", 85 | "ltuid_v2", 86 | "mid", 87 | "stmid", 88 | "stmid_v2", 89 | "stoken", 90 | "stoken_v2", 91 | "stuid", 92 | "stuid_v2", 93 | ] 94 | 95 | # 读取 96 | if not cookie: 97 | if not cookie_cfg: 98 | return {"error": "养成计算器需要米游社 Cookie!"} 99 | else: 100 | # 蒙德飞行冠军安柏应该不会有人没有吧? 101 | check_res = await query_mys("技能", cookie_cfg, {"avatar_id": 10000021}) 102 | if not check_res.get("error"): 103 | # 检验成功才返回,否则尝试刷新 104 | return cookie_cfg 105 | # 写入 106 | else: 107 | cookie_cfg.update(dict(i.strip().split("=", 1) for i in cookie.split(";"))) 108 | check_res = await query_mys("技能", cookie_cfg, {"avatar_id": 10000021}) 109 | # 检验成功保存并返回,否则尝试刷新 110 | if not check_res.get("error"): 111 | # 更新米游社用户 ID 112 | mys_id = ( 113 | cookie_cfg.get("stuid") 114 | or cookie_cfg.get("ltuid") 115 | or cookie_cfg.get("login_uid") 116 | or cookie_cfg.get("account_id") 117 | ) 118 | if mys_id: 119 | cookie_cfg.update( 120 | {k: mys_id for k in ["stuid", "ltuid", "login_uid", "account_id"]} 121 | ) 122 | # 精简 Cookie 字段 123 | simple_cookie_cfg = { 124 | k: cookie_cfg[k] for k in standard_keys if cookie_cfg.get(k) 125 | } 126 | # 写入更新 127 | cookie_cfg.update(simple_cookie_cfg) 128 | cookie_file.write_text( 129 | json.dumps(cookie_cfg, ensure_ascii=False, indent=2), encoding="UTF-8" 130 | ) 131 | return cookie_cfg 132 | 133 | # 更新米游社用户 ID 134 | mys_id = ( 135 | cookie_cfg.get("stuid") 136 | or cookie_cfg.get("ltuid") 137 | or cookie_cfg.get("login_uid") 138 | or cookie_cfg.get("account_id") 139 | ) 140 | if mys_id: 141 | cookie_cfg.update( 142 | {k: mys_id for k in ["stuid", "ltuid", "login_uid", "account_id"]} 143 | ) 144 | 145 | # 更新 cookie_token 146 | if not cookie_cfg.get("stoken") and not cookie_cfg.get("login_ticket"): 147 | # 同时缺少 login_ticket 和 stoken 直接结束 148 | return {"error": f"缺少 stoken 无法自动{'补全' if cookie else '更新过期的'}曲奇!"} 149 | elif not cookie_cfg.get("stoken"): 150 | # 通过 login_ticket 更新 stoken 151 | stoken_res = await query_mys( 152 | "_stoken", 153 | {}, 154 | data={ 155 | "login_ticket": cookie_cfg["login_ticket"], 156 | "token_types": "3", 157 | "uid": cookie_cfg["account_id"], 158 | }, 159 | spec={"mys_id": cookie_cfg["account_id"], "cookie": ""}, 160 | ) 161 | if stoken_res.get("error"): 162 | return stoken_res 163 | try: 164 | cookie_cfg["stoken"] = stoken_res["list"][0]["token"] 165 | cookie_cfg["ltoken"] = stoken_res["list"][1]["token"] 166 | except Exception as e: 167 | logger.opt(exception=e).error("由 login_ticket 获取 stoken 出错") 168 | return {"error": "获取 stoken 出错,无法自动更新过期的曲奇!"} 169 | 170 | # 通过 stoken 更新 cookie_token 171 | user_id_type, user_id = ( 172 | ("mid", cookie_cfg.get("mid")) 173 | if cookie_cfg["stoken"].startswith("v2_") 174 | else ("uid", cookie_cfg["account_id"]) 175 | ) 176 | if not user_id: 177 | # v2 stoken 需要与 mid 同时使用 178 | return {"error": f"stoken v2 缺少 mid 无法自动{'补全' if cookie else '更新过期的'}曲奇!"} 179 | cookie_token_res = await query_mys( 180 | "_cookie", 181 | {}, 182 | data={"stoken": cookie_cfg["stoken"], user_id_type: user_id}, 183 | spec={"mys_id": cookie_cfg["account_id"], "cookie": ""}, 184 | ) 185 | if cookie_token_res.get("error"): 186 | return cookie_token_res 187 | if cookie_token_res.get("cookie_token"): 188 | cookie_cfg["cookie_token"] = cookie_token_res["cookie_token"] 189 | else: 190 | return { 191 | "error": f"由 {'v2 ' if cookie_cfg['stoken'].startswith('v2_') else ''}stoken 获取 cookie_token 失败!" 192 | } 193 | 194 | # 精简 Cookie 字段 195 | simple_cookie_cfg = {k: cookie_cfg[k] for k in standard_keys if cookie_cfg.get(k)} 196 | # 写入更新 197 | cookie_cfg.update(simple_cookie_cfg) 198 | cookie_file.write_text( 199 | json.dumps(cookie_cfg, ensure_ascii=False, indent=2), encoding="UTF-8" 200 | ) 201 | 202 | return cookie_cfg 203 | 204 | 205 | async def query_ambr( 206 | type: Literal["每日采集", "升级材料", "角色列表", "武器列表", "材料列表"], retry: int = 3 207 | ) -> Dict: 208 | """安柏计划数据接口请求""" 209 | 210 | async with AsyncClient() as client: 211 | while retry: 212 | try: 213 | res = await client.get(AMBR[type], timeout=10.0) 214 | return res.json()["data"] 215 | except (HTTPError, json.decoder.JSONDecodeError, KeyError) as e: 216 | retry -= 1 217 | if retry: 218 | await asyncio.sleep(2) 219 | else: 220 | logger.opt(exception=e).error(f"安柏计划 {type} 接口请求出错") 221 | return {} 222 | 223 | 224 | async def get_ds_headers( 225 | mys_id: str, cookie: str, body: str = "", query: str = "" 226 | ) -> Dict: 227 | """含 DS 请求头获取,仅用于补全 Cookie""" 228 | 229 | client = { 230 | "app_version": "2.36.1", 231 | "client_type": "5", 232 | "salt": "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs", 233 | "referer": "https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?bbs_auth_required=true&act_id=e202009291139501&utm_source=bbs&utm_medium=mys&utm_campaign=icon", 234 | } 235 | 236 | device = f"NB-{md5(mys_id.encode()).hexdigest()[:5]}" 237 | ua = ( 238 | f"Mozilla/5.0 (Linux; Android 12; {device}) AppleWebKit/537.36 " 239 | "(KHTML, like Gecko) Chrome/99.0.4844.73 Mobile Safari/537.36 " 240 | f"miHoYoBBS/{client['app_version']}" 241 | ) 242 | 243 | s = client["salt"] 244 | t = str(int(time())) 245 | r = str(randint(100000, 200000)) 246 | m = md5(f"salt={s}&t={t}&r={r}&b={body}&q={query}".encode()).hexdigest() 247 | 248 | return { 249 | "x-rpc-app_version": client["app_version"], 250 | "x-rpc-client_type": client["client_type"], 251 | "user-agent": ua, 252 | "referer": client["referer"], 253 | "ds": f"{t},{r},{m}", 254 | "cookie": cookie, 255 | } 256 | 257 | 258 | async def query_mys( 259 | type: Literal["技能", "计算", "_stoken", "_cookie"], 260 | cookie: Dict, 261 | data: Dict, 262 | spec: Dict = {}, 263 | ) -> Dict: 264 | """米游社计算器接口请求""" 265 | 266 | cookie_join = " ".join(f"{k}={v};" for k, v in cookie.items()) 267 | headers = ( 268 | await get_ds_headers(spec["mys_id"], spec["cookie"]) 269 | if type.startswith("_") 270 | else { 271 | "cookie": cookie_join, # 必需 272 | "host": "api-takumi.mihoyo.com", 273 | "origin": "https://webstatic.mihoyo.com", 274 | "referer": "https://webstatic.mihoyo.com/", 275 | "sec-fetch-dest": "empty", 276 | "sec-fetch-mode": "cors", 277 | "sec-fetch-site": "same-site", 278 | "user-agent": ( 279 | "Mozilla/5.0 (Linux; Android 12; SM-G977N Build/SP1A.210812.016; wv) " 280 | "AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 " 281 | "Chrome/107.0.5304.105 Mobile Safari/537.36 miHoYoBBS/2.40.1" 282 | ), 283 | "x-requested-with": "com.mihoyo.hyperion", 284 | } 285 | ) 286 | 287 | async with AsyncClient() as client: 288 | res_dict = {} 289 | try: 290 | if type != "计算" or type.startswith("_"): 291 | res = await client.get(MYS[type], params=data, headers=headers) 292 | else: 293 | headers["content-type"] = "application/json;charset=UTF-8" 294 | res = await client.post(MYS[type], json=data, headers=headers) 295 | res_dict = res.json() 296 | return res_dict["data"] or { 297 | "error": "[{}] {}".format( 298 | res_dict.get("retcode", "null"), 299 | res_dict.get("message", f"米游社{type}接口请求出错!"), 300 | ) 301 | } 302 | except (HTTPError, json.decoder.JSONDecodeError, KeyError) as e: 303 | logger.opt(exception=e).error(f"米游社 {type} 接口请求出错\n>>>>> {res_dict}") 304 | return { 305 | "error": "[{}] {}".format( 306 | res_dict.get("retcode", "null"), 307 | res_dict.get("message", f"米游社{type}接口请求出错!"), 308 | ) 309 | } 310 | 311 | 312 | async def download( 313 | url: str, type: str = "draw", rename: str = "", retry: int = 3 314 | ) -> Optional[Path]: 315 | """ 316 | 资源下载。图片资源使用 Pillow 保存 317 | * ``param url: str`` 下载链接 318 | * ``param type: str = "draw"`` 下载类型,根据类型决定保存的文件夹 319 | * ``param rename: str = ""`` 下载资源重命名,需要包含文件后缀 320 | * ``param retry: int = 3`` 下载失败重试次数 321 | - ``return: Optional[Path]`` 本地文件路径,出错时返回空 322 | """ 323 | 324 | # 下载链接及保存路径处理 325 | if type == "draw": 326 | # 插件绘图素材,通过阿里云 CDN 下载 327 | f = CONFIG_DIR / "draw" / url 328 | url = f"https://cdn.monsterx.cn/bot/gsmaterial/{url}" 329 | elif type == "mihoyo": 330 | # 通过米游社下载的文件,主要为米游社计算器材料图标 331 | f = DL_CFG["item"]["dir"] / rename 332 | else: 333 | # 可通过镜像下载的文件,主要为角色头像、武器图标、天赋及武器突破材料图标 334 | f = DL_CFG[type]["dir"] / rename 335 | url = DL_MIRROR + url 336 | 337 | # 跳过下载本地已存在的文件 338 | if f.exists(): 339 | # 测试角色图像为白色问号,该图片 st_size = 5105,小于 6KB 均视为无效图片 340 | if not (f.name.lower().endswith("png") and f.stat().st_size < 6144): 341 | return f 342 | 343 | # 远程文件下载 344 | async with AsyncClient(verify=False) as client: 345 | while retry: 346 | try: 347 | if type == "draw": 348 | # 通过阿里云 CDN 下载,可能有字体文件等 349 | async with client.stream("GET", url) as res: 350 | with open(f, "wb") as fb: 351 | async for chunk in res.aiter_bytes(): 352 | fb.write(chunk) 353 | else: 354 | logger.info(f"正在下载文件 {f.name}\n>>>>> {url}") 355 | headers = ( 356 | { 357 | "host": "act-webstatic.mihoyo.com", 358 | "referer": "https://webstatic.mihoyo.com/", 359 | "sec-fetch-dest": "image", 360 | "sec-fetch-mode": "no-cors", 361 | "sec-fetch-site": "same-site", 362 | "user-agent": ( 363 | "Mozilla/5.0 (Linux; Android 12; SM-G977N Build/SP1A.210812.016; wv) " 364 | "AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 " 365 | "Chrome/107.0.5304.105 Mobile Safari/537.36 miHoYoBBS/2.40.1" 366 | ), 367 | "x-requested-with": "com.mihoyo.hyperion", 368 | } 369 | if type == "mihoyo" 370 | else { 371 | "referer": "https://ambr.top/", 372 | "user-agent": ( 373 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, " 374 | "like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.47" 375 | ), 376 | } 377 | ) 378 | res = await client.get(url, headers=headers, timeout=20.0) 379 | userImage = Image.open(BytesIO(res.content)) 380 | userImage.save(f, quality=100) 381 | return f 382 | except Exception as e: 383 | retry -= 1 384 | if retry: 385 | await asyncio.sleep(2) 386 | else: 387 | logger.opt(exception=e).error(f"文件 {f.name} 下载失败!") 388 | 389 | 390 | async def update_config() -> None: 391 | """材料配置更新""" 392 | 393 | # 启动资源下载 394 | init_tasks = [ 395 | download(file, "draw") 396 | for file in [ 397 | "SmileySans-Oblique.ttf", 398 | "bg5.140.png", 399 | "bg4.140.png", 400 | "bg3.140.png", 401 | ] 402 | ] 403 | await asyncio.gather(*init_tasks) 404 | init_tasks.clear() 405 | 406 | logger.info("原神材料配置更新开始...") 407 | 408 | # 获取安柏计划数据 409 | logger.debug("安柏计划数据接口请求...") 410 | domain_res = await query_ambr("每日采集") 411 | update_res = await query_ambr("升级材料") 412 | avatar_res = await query_ambr("角色列表") 413 | weapon_res = await query_ambr("武器列表") 414 | material_res = await query_ambr("材料列表") 415 | if any( 416 | not x for x in [domain_res, avatar_res, weapon_res, update_res, material_res] 417 | ): 418 | logger.info("安柏计划数据不全!更新任务被跳过") 419 | return 420 | 421 | config = {"avatar": {}, "weapon": {}, "weekly": {}, "skip_3": SKIP_THREE, "time": 0} 422 | 423 | # 生成最新每日材料配置 424 | logger.debug("每日材料配置更新 & 对应图片下载...") 425 | for weekday, domains in domain_res.items(): 426 | if weekday not in ["monday", "tuesday", "wednesday"]: 427 | # 跳过材料重复的日期 428 | continue 429 | day_num = {"monday": "1", "tuesday": "2", "wednesday": "3"}[weekday] 430 | config["avatar"][day_num], config["weapon"][day_num] = {}, {} 431 | # 按区域重新排序秘境 432 | # 约 3.2 版本起,安柏计划上游蒙德武器秘境返回的城市 ID 异常,手动纠正为 1 433 | config_order = sorted( 434 | domains, 435 | key=lambda x: ( 436 | 1 437 | if domains[x]["name"] 438 | in ["炼武秘境:水光之城", "炼武秘境:深没之谷", "炼武秘境:渴水的废都", "炼武秘境:云垢"] 439 | else domains[x]["city"] 440 | ), 441 | ) 442 | # 遍历秘境填充对应的角色/武器数据 443 | for domain_key in config_order: 444 | if "精通秘境" in domains[domain_key]["name"]: 445 | item_type, trans = "avatar", avatar_res["items"] 446 | else: # "炼武秘境" in domains[domain_key]["name"] 447 | item_type, trans = "weapon", weapon_res["items"] 448 | material_id = str(domains[domain_key]["reward"][-1]) 449 | material_name = material_res["items"][material_id]["name"] 450 | use_this = [ 451 | id_str 452 | for id_str in update_res[item_type] 453 | if material_id in update_res[item_type][id_str]["items"] 454 | and id_str.isdigit() # 排除旅行者 "10000005-anemo" 等 455 | ] 456 | # 以 "5琴10000003,5优菈10000051,...,[rank][name][id]" 形式写入配置 457 | config[item_type][day_num][f"{material_name}-{material_id}"] = ",".join( 458 | f"{trans[i]['rank']}{trans[i]['name']}{i}" for i in use_this 459 | ) 460 | # 下载图片 461 | domain_tasks = [ 462 | download( 463 | f"UI_ItemIcon_{material_id}.png", 464 | "item", 465 | "{}.{}".format( 466 | material_id 467 | if DL_CFG["item"]["file"] == "id" 468 | else material_name 469 | if material_name != "???" 470 | else material_id, 471 | DL_CFG["item"]["fmt"], 472 | ), 473 | ), 474 | *[ 475 | download( 476 | f"{trans[i]['icon']}.png", 477 | item_type, 478 | # 特殊物品图片重命名为 config 中写入格式(L454) 479 | "{}.{}".format( 480 | ( 481 | i 482 | if DL_CFG[item_type]["file"] == "id" 483 | else trans[i]["name"] 484 | if trans[i]["name"] != "???" 485 | else i 486 | ) 487 | or f"{trans[i]['rank']}{trans[i]['name']}{i}", 488 | DL_CFG[item_type]["fmt"], 489 | ), 490 | ) 491 | for i in use_this 492 | ], 493 | ] 494 | await asyncio.gather(*domain_tasks) 495 | domain_tasks.clear() 496 | 497 | # 获取最新周本材料 498 | logger.debug("周本材料配置更新 & 对应图片下载...") 499 | weekly_material, weekly_tasks = [], [] 500 | for material_id, material in material_res["items"].items(): 501 | # 筛选周本材料 502 | if ( 503 | material["rank"] != 5 504 | or material["type"] != "characterLevelUpMaterial" 505 | or int(material_id) 506 | in [ 507 | 104104, # 璀璨原钻 508 | 104114, # 燃愿玛瑙 509 | 104124, # 涤净青金 510 | 104134, # 生长碧翡 511 | 104144, # 最胜紫晶 512 | 104154, # 自在松石 513 | 104164, # 哀叙冰玉 514 | 104174, # 坚牢黄玉 515 | ] 516 | ): 517 | # 包含计算器素材,但不在此处下载,后续计算时从米游社下载 518 | continue 519 | weekly_material.append(material_id) 520 | if material["icon"]: 521 | weekly_tasks.append( 522 | download( 523 | f"{material['icon']}.png", 524 | "item", 525 | "{}.{}".format( 526 | material_id 527 | if DL_CFG["item"]["file"] == "id" 528 | else material["name"] 529 | if material["name"] != "???" 530 | else material_id, 531 | DL_CFG["item"]["fmt"], 532 | ), 533 | ) 534 | ) 535 | 536 | # 下载最新周本材料图片 537 | await asyncio.gather(*weekly_tasks) 538 | weekly_tasks.clear() 539 | 540 | # 固定已知周本的各个材料键名顺序 541 | config["weekly"] = { 542 | boss_info[0]: { 543 | f"{material_res['items'][material_id]['name']}-{material_id}": "" 544 | for material_id in weekly_material[boss_idx * 3 : boss_idx * 3 + 3] 545 | } 546 | for boss_idx, boss_info in enumerate(_WEEKLY_BOSS) 547 | } 548 | # 未实装周本材料视为 BOSS "???" 的产物 549 | if len(weekly_material) > len(_WEEKLY_BOSS * 3): 550 | config["weekly"]["???"] = { 551 | f"{material_res['items'][material_id]['name']}-{material_id}": "" 552 | for material_id in weekly_material[len(_WEEKLY_BOSS * 3) :] 553 | } 554 | 555 | # 从升级材料中查找使用某周本材料的角色 556 | for avatar_id, avatar in update_res["avatar"].items(): 557 | # 排除旅行者 558 | if not str(avatar_id).isdigit(): 559 | continue 560 | # 将角色升级材料按消耗数量重新排序,周本材料 ID 将排在最后一位 561 | material_id = list( 562 | {k: v for k, v in sorted(avatar["items"].items(), key=lambda i: i[1])} 563 | )[-1] 564 | material_name = material_res["items"][material_id]["name"] 565 | # 确定 config["weekly"] 写入键名,第一层键名为周本 BOSS 名,第二层为 [name]-[id] 材料名 566 | _boss_idx = weekly_material.index(material_id) // 3 567 | _boss_name = ( 568 | _WEEKLY_BOSS[_boss_idx][0] if _boss_idx < len(_WEEKLY_BOSS) else "???" 569 | ) 570 | _material_name = f"{material_res['items'][material_id]['name']}-{material_id}" 571 | # 以 "5琴10000003,5迪卢克10000016,...,[rank][name][id]" 形式写入配置 572 | config["weekly"][_boss_name][_material_name] += "{}{}{}{}".format( 573 | "," if config["weekly"][_boss_name][_material_name] else "", 574 | avatar_res["items"][avatar_id]["rank"], 575 | avatar_res["items"][avatar_id]["name"], 576 | avatar_id, 577 | ) 578 | 579 | # 判断是否需要更新缓存 580 | config_file, redraw_daily, redraw_weekly = CONFIG_DIR / "config.json", True, True 581 | if config_file.exists(): 582 | old_config: Dict = json.loads(config_file.read_text(encoding="UTF-8")) 583 | redraw_daily = any( 584 | old_config.get(key) != config[key] for key in ["avatar", "weapon"] 585 | ) 586 | redraw_weekly = old_config.get("weekly") != config["weekly"] 587 | # 跳过三星物品环境变量改变,强制重绘每日材料图片 588 | if old_config.get("skip_3", True) != config["skip_3"]: 589 | redraw_daily = True 590 | # 更新每日材料图片缓存 591 | if redraw_daily: 592 | logger.debug("每日材料图片缓存生成...") 593 | daily_draw_tasks = [ 594 | draw_materials(config, ["avatar", "weapon"], day) for day in [1, 2, 3] 595 | ] 596 | await asyncio.gather(*daily_draw_tasks) 597 | daily_draw_tasks.clear() 598 | # 更新周本材料图片缓存 599 | if redraw_weekly: 600 | logger.debug("周本材料图片缓存生成...") 601 | bosses_names = WEEKLY_BOSS if config["weekly"].get("???") else _WEEKLY_BOSS 602 | bosses_key = [b[0] for b in bosses_names] 603 | await draw_materials(config, bosses_key) 604 | # 清理未上线周本过期的图片缓存 605 | if not config["weekly"].get("???"): 606 | beta_weekly_pic = CONFIG_DIR / "cache/weekly.???.jpg" 607 | beta_weekly_pic.unlink(missing_ok=True) 608 | 609 | # 补充时间戳 610 | config["time"] = int(time()) 611 | config_file.write_text( 612 | json.dumps(config, ensure_ascii=False, indent=2), encoding="UTF-8" 613 | ) 614 | logger.info("原神材料配置更新完成!") 615 | 616 | 617 | def get_weekday(delta: int = 0) -> int: 618 | """周几整数获取,delta 为向后推迟几天""" 619 | 620 | today = datetime.now(TZ) # 添加时区信息 621 | _delta = (delta - 1) if today.hour < 4 else delta 622 | return (today + timedelta(days=_delta)).weekday() + 1 623 | 624 | 625 | async def generate_daily_msg( 626 | material: Literal["avatar", "weapon", "all", "update"], 627 | weekday: int = 0, 628 | delta: int = 0, 629 | ) -> Union[Path, str]: 630 | """原神每日材料图片生成入口""" 631 | 632 | # 时间判断 633 | weekday = weekday or get_weekday(delta) 634 | if weekday == 7: 635 | return "今天所有天赋培养、武器突破材料都可以获取哦~" 636 | day = weekday % 3 or 3 637 | 638 | # 存在图片缓存且非更新任务时使用缓存 639 | cache_pic = CONFIG_DIR / "cache" / f"daily.{day}.{material}.jpg" 640 | if material != "update" and cache_pic.exists(): 641 | logger.info(f"使用缓存的原神材料图片 {cache_pic.name}") 642 | return cache_pic 643 | 644 | # 根据每日材料配置重新生成图片 645 | config = json.loads((CONFIG_DIR / "config.json").read_text(encoding="UTF-8")) 646 | need_types = [material] if material in ["avatar", "weapon"] else ["avatar", "weapon"] 647 | # 按需绘制素材图片 648 | try: 649 | return await draw_materials(config, need_types, day) 650 | except Exception as e: 651 | logger.opt(exception=e).error("原神每日材料图片生成出错") 652 | return f"[{e.__class__.__name__}] 原神每日材料生成失败" 653 | 654 | 655 | async def generate_weekly_msg(boss: str) -> Union[Path, str]: 656 | """原神周本材料图片生成入口""" 657 | 658 | assert boss in ["all", *[b[0] for b in WEEKLY_BOSS]] 659 | # 存在图片缓存且非更新任务时使用缓存 660 | cache_pic = CONFIG_DIR / f"cache/weekly.{boss}.jpg" 661 | if cache_pic.exists(): 662 | logger.info(f"使用缓存的原神材料图片 {cache_pic.name}") 663 | return cache_pic 664 | 665 | # 根据每日材料配置重新生成图片 666 | config = json.loads((CONFIG_DIR / "config.json").read_text(encoding="UTF-8")) 667 | need_types = [boss] if boss != "all" else [b[0] for b in _WEEKLY_BOSS] 668 | if not config["weekly"].get("???") and boss == "???": 669 | return "当前暂无未上线的周本" 670 | elif config["weekly"].get("???") and boss == "all": 671 | need_types.append("???") 672 | # 按需绘制素材图片 673 | try: 674 | return await draw_materials(config, need_types) 675 | except Exception as e: 676 | logger.opt(exception=e).error("原神周本材料图片生成出错") 677 | return f"[{e.__class__.__name__}] 原神周本材料生成失败" 678 | 679 | 680 | async def get_target(alias: str) -> Tuple[int, str]: 681 | """升级目标 ID 及真实名称提取""" 682 | 683 | alias = alias.lower() 684 | for item_id, item_alias in ITEM_ALIAS.items(): 685 | if alias in item_alias: 686 | return int(item_id), item_alias[0] 687 | return 0, alias 688 | 689 | 690 | async def get_upgrade_target(cookies: Dict[str, str], target_id: int, msg: str) -> Dict: 691 | """计算器升级范围提取""" 692 | # cookie = " ".join(f"{k}={v};" for k, v in cookies.items() if k in ["account_id", "cookie_token"]) 693 | 694 | lvl_regex = r"([0-9]{1,2})([-\s]([0-9]{1,2}))?" 695 | t_lvl_regex = r"(10|[1-9])(-(10|[1-9]))?" 696 | 697 | # 武器升级识别 698 | if target_id < 10000000: 699 | level_target = findall(lvl_regex, msg) 700 | if not level_target: 701 | _lvl_from, _lvl_to = 1, 90 702 | else: 703 | _target = level_target[0] 704 | _lvl_from, _lvl_to = ( 705 | (int(_target[0]), int(_target[-1])) 706 | if _target[-1] 707 | else (1, int(_target[0])) 708 | ) 709 | return ( 710 | {"error": "武器等级超出限制~"} 711 | if _lvl_to > 90 712 | else { 713 | "weapon": { 714 | "id": target_id, 715 | "level_current": _lvl_from, 716 | "level_target": _lvl_to, 717 | }, 718 | # "reliquary_list": [] 719 | } 720 | ) 721 | 722 | # 角色升级识别 723 | # 角色等级,支持 90、70-90、70 90 三种格式 724 | level_input = ( 725 | msg.split("天赋")[0].strip() if "天赋" in msg else msg.split(" ", 1)[0].strip() 726 | ) 727 | level_targets = findall(lvl_regex, level_input) 728 | if not level_targets: 729 | # 消息直接以天赋开头视为不升级角色等级 730 | _lvl_from, _lvl_to = 90 if msg.startswith("天赋") else 1, 90 731 | elif len(level_targets) > 1: 732 | return {"error": f"无法识别的等级「{level_input}」"} 733 | else: 734 | _target = level_targets[0] 735 | _lvl_from, _lvl_to = ( 736 | (int(_target[0]), int(_target[-1])) if _target[-1] else (1, int(_target[0])) 737 | ) 738 | if _lvl_to > 90: 739 | return {"error": "伙伴等级超出限制~"} 740 | msg = msg.lstrip(level_input).strip() 741 | # 天赋等级,支持 8、888、81010、8 8 8、1-8、1-8 1-10 10 等 742 | if msg.startswith("天赋"): 743 | msg = msg.lstrip("天赋").strip() 744 | skill_targets = findall(t_lvl_regex, msg) 745 | if not skill_targets: 746 | _skill_target = [[1, 8]] * 3 747 | else: 748 | _skill_target = [ 749 | [int(_matched[0]), int(_matched[-1])] 750 | if _matched[-1] 751 | else [1, int(_matched[0])] 752 | for _matched in skill_targets 753 | ] 754 | if len(_skill_target) > 3: 755 | return {"error": f"怎么会有 {len(_skill_target)} 个技能的角色呢?"} 756 | if any(_s[1] > 10 for _s in _skill_target): 757 | return {"error": "天赋等级超出限制~"} 758 | 759 | # 获取角色技能数据 760 | skill_list = await query_mys("技能", cookies, {"avatar_id": target_id}) 761 | if skill_list.get("error"): 762 | return skill_list 763 | skill_ids = [ 764 | skill["group_id"] for skill in skill_list["list"] if skill["max_level"] == 10 765 | ] 766 | 767 | return { 768 | "avatar_id": target_id, 769 | "avatar_level_current": _lvl_from, 770 | "avatar_level_target": _lvl_to, 771 | # "element_attr_id": 4, 772 | "skill_list": [ 773 | { 774 | "id": skill_ids[idx], 775 | "level_current": skill_target[0], 776 | "level_target": skill_target[1], 777 | } 778 | for idx, skill_target in enumerate(_skill_target) 779 | # ... 780 | ] 781 | # "weapon": { 782 | # "id": 15508, 783 | # "level_current": 1, 784 | # "level_target": 90 785 | # }, 786 | # "reliquary_list": [] 787 | } 788 | 789 | 790 | async def generate_calc_msg(msg: str) -> Union[bytes, str]: 791 | """原神计算器材料图片生成入口""" 792 | 793 | # 检查 Cookie 中 cookie_token 是否失效并尝试更新 794 | cookie_dict = await cookies_helper() 795 | if cookie_dict.get("error"): 796 | return cookie_dict["error"] 797 | 798 | # 提取待升级物品 ID 及真实名称 799 | target_input = msg.split(" ", 1)[0] 800 | target_id, target_name = await get_target(target_input.strip()) 801 | if not target_id: 802 | return f"无法识别的名称「{target_input}」" 803 | msg = msg.lstrip(target_input).strip() 804 | 805 | # 提取升级范围 806 | target = await get_upgrade_target(cookie_dict, target_id, msg) 807 | if target.get("error"): 808 | return target["error"] 809 | 810 | # 请求米游社计算器 811 | logger.info(f"{target_id}: {target}") 812 | calculate = await query_mys("计算", cookie_dict, target) 813 | if calculate.get("error"): 814 | return calculate["error"] 815 | 816 | # 下载计算器素材图片 817 | for key in calculate.keys(): 818 | consume_tasks = [ 819 | download( 820 | i["icon_url"], 821 | "mihoyo", 822 | f"{i[DL_CFG['item']['file']]}.{DL_CFG['item']['fmt']}", 823 | ) 824 | for i in calculate[key] 825 | ] 826 | logger.info(f"正在下载计算器 {key} 消耗材料的 {len(consume_tasks)} 张图片") 827 | await asyncio.gather(*consume_tasks) 828 | consume_tasks.clear() 829 | 830 | # 绘制计算器材料图片 831 | return await draw_calculator(target_name, target, calculate) 832 | --------------------------------------------------------------------------------