refactor: restructure plugins per MTSS standard, consolidate assets to res/
Per docs/plugin-audit-report.md (plugins normalized to Trigger(handlers) → Service(services) → Model(repository/models) + utils): - Split monolithic __init__.py into handlers/services/utils across dailywife, deer_pipe, dice, galgame_card, helldivers_tools, huoziyinshua, learning_chat, makeaquote, mc_server_status, ncm_saying, picfinder_take, picstatus, random_jm_code, regif, steam_info, video_analysis, group_tools - Move static assets under res/: deer_pipe font/img, makeaquote font, helldivers img/templates, huoziyinshua HuoZiYinShua - Add config.py + register_config_items to ncm_saying, random_jm_code, group_tools; learning_chat unified config bridge - Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp - Disable brash_general_supercredits_tools (stub comment only) - bot.py: optional stdout/stderr redirect to log file for Web log viewer, force ANSI colorize on non-TTY sinks - Move runtime data (jm_code.json) out of plugin dir into hexi/data - Docs: plugin-audit-report.md; README reflects removed plugins Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -79,25 +79,6 @@ __plugin_meta__ = PluginMetadata(
|
||||
supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"),
|
||||
)
|
||||
|
||||
help = on_command("steamhelp", aliases={"steam帮助"}, priority=10)
|
||||
bind = on_command("steambind", aliases={"绑定steam"}, priority=10)
|
||||
unbind = on_command("steamunbind", aliases={"解绑steam"}, priority=10)
|
||||
info = on_command("steaminfo", aliases={"steam信息"}, priority=10)
|
||||
# 谁在玩游戏:全群共享 CD,防一人狂点挤占 API 配额(steam.py 内另有总配额限频)
|
||||
check = cooldown(30, scope="group", hint="群里刚有人查过,{secs} 秒后再试")(
|
||||
on_command("steamcheck", aliases={"查看steam", "查steam", "谁在玩游戏"}, priority=10)
|
||||
)
|
||||
enable = on_command("steamenable", aliases={"启用steam"}, priority=10)
|
||||
disable = on_command("steamdisable", aliases={"禁用steam"}, priority=10)
|
||||
# 游戏时长表:单用户 15s CD
|
||||
get_play_time = cooldown(15, hint="查询太频繁了,{secs} 秒后再试")(
|
||||
on_command("steamplaytime", aliases={"游戏时长表"}, priority=10)
|
||||
)
|
||||
# 全量拉取 SteamDB 较重,仅限超级管理员触发
|
||||
update_steamdb = on_command(
|
||||
"steamdbupdate", aliases={"更新steamdb"}, priority=10, permission=SUPERUSER
|
||||
)
|
||||
|
||||
if hasattr(nonebot, "get_plugin_config"):
|
||||
config = nonebot.get_plugin_config(Config)
|
||||
else:
|
||||
@@ -535,430 +516,27 @@ if not config.steam_disable_broadcast_on_startup:
|
||||
else:
|
||||
logger.info("已禁用启动时的 Steam 播报")
|
||||
|
||||
|
||||
@help.handle()
|
||||
async def help_handle():
|
||||
await help.finish(__plugin_meta__.usage)
|
||||
|
||||
|
||||
@bind.handle()
|
||||
async def bind_handle(
|
||||
event: Event, target: Target = Depends(get_target), cmd_arg: Message = CommandArg()
|
||||
):
|
||||
parent_id = target.parent_id or target.id
|
||||
|
||||
arg = cmd_arg.extract_plain_text()
|
||||
|
||||
if not arg.isdigit():
|
||||
await bind.finish(
|
||||
"请输入正确的 Steam ID 或 Steam好友代码,格式: steambind [Steam ID 或 Steam好友代码]"
|
||||
)
|
||||
|
||||
steam_id = get_steam_id(arg)
|
||||
|
||||
if user_data := bind_data.get(parent_id, event.get_user_id()):
|
||||
user_data["steam_id"] = steam_id
|
||||
bind_data.save()
|
||||
|
||||
await bind.finish(f"已更新你的 Steam ID 为 {steam_id}")
|
||||
else:
|
||||
bind_data.add(
|
||||
parent_id,
|
||||
{"user_id": event.get_user_id(), "steam_id": steam_id, "nickname": None},
|
||||
)
|
||||
bind_data.save()
|
||||
|
||||
await bind.finish(f"已绑定你的 Steam ID 为 {steam_id}")
|
||||
|
||||
|
||||
@unbind.handle()
|
||||
async def unbind_handle(event: Event, target: Target = Depends(get_target)):
|
||||
parent_id = target.parent_id or target.id
|
||||
user_id = event.get_user_id()
|
||||
|
||||
if bind_data.get(parent_id, user_id) is not None:
|
||||
bind_data.remove(parent_id, user_id)
|
||||
bind_data.save()
|
||||
|
||||
await unbind.finish("已解绑 Steam ID")
|
||||
else:
|
||||
await unbind.finish("未绑定 Steam ID")
|
||||
|
||||
|
||||
@info.handle()
|
||||
async def info_handle(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
target: Target = Depends(get_target),
|
||||
arg: Message = CommandArg(),
|
||||
):
|
||||
parent_id = target.parent_id or target.id
|
||||
|
||||
uni_arg = await UniMessage.generate(message=arg, event=event, bot=bot)
|
||||
at = uni_arg[At]
|
||||
|
||||
if len(at) != 0:
|
||||
user_id: str = at[0].target
|
||||
user_data = bind_data.get(parent_id, user_id)
|
||||
if user_data is None:
|
||||
await info.finish("该用户未绑定 Steam ID")
|
||||
steam_id = user_data["steam_id"]
|
||||
steam_friend_code = str(int(steam_id) - STEAM_ID_OFFSET)
|
||||
elif arg.extract_plain_text().strip() != "":
|
||||
steam_id = int(arg.extract_plain_text().strip())
|
||||
if steam_id < STEAM_ID_OFFSET:
|
||||
steam_friend_code = steam_id
|
||||
steam_id += STEAM_ID_OFFSET
|
||||
else:
|
||||
steam_friend_code = steam_id - STEAM_ID_OFFSET
|
||||
else:
|
||||
user_data = bind_data.get(parent_id, event.get_user_id())
|
||||
|
||||
if user_data is None:
|
||||
await info.finish(
|
||||
"未绑定 Steam ID, 请使用 “steambind [Steam ID 或 Steam好友代码]” 绑定 Steam ID"
|
||||
)
|
||||
|
||||
steam_id = user_data["steam_id"]
|
||||
steam_friend_code = str(int(steam_id) - STEAM_ID_OFFSET)
|
||||
|
||||
player_data = await get_user_data(steam_id, cache_path, config.proxy)
|
||||
|
||||
# 游戏名本地化(中文名优先)
|
||||
names = await get_game_cn_names(
|
||||
[g.get("appid") for g in player_data["game_data"] if g.get("appid")],
|
||||
db_path,
|
||||
)
|
||||
for g in player_data["game_data"]:
|
||||
g["game_name"] = localize_game_name(g["game_name"], g.get("appid"), names)
|
||||
|
||||
draw_data = [
|
||||
{
|
||||
"game_header": game["game_image"],
|
||||
"game_name": game["game_name"],
|
||||
"game_time": f"{game['play_time']} 小时",
|
||||
"last_play_time": game["last_played"],
|
||||
"achievements": game["achievements"],
|
||||
"completed_achievement_number": game.get("completed_achievement_number"),
|
||||
"total_achievement_number": game.get("total_achievement_number"),
|
||||
}
|
||||
for game in player_data["game_data"]
|
||||
]
|
||||
|
||||
image = draw_player_status(
|
||||
player_data["background"],
|
||||
player_data["avatar"],
|
||||
player_data["player_name"],
|
||||
str(steam_friend_code),
|
||||
player_data["description"],
|
||||
player_data["recent_2_week_play_time"],
|
||||
draw_data,
|
||||
)
|
||||
|
||||
await info.finish(
|
||||
await UniMessage(
|
||||
Image(raw=image_to_bytes(image)),
|
||||
).export(bot)
|
||||
)
|
||||
|
||||
|
||||
# 初始化计数器
|
||||
# 用户命令的 API key 轮询(与播报 key_select_2 独立)
|
||||
current_key_index_1 = 0
|
||||
|
||||
|
||||
def key_select():
|
||||
global current_key_index_1
|
||||
# 获取key列表
|
||||
keys = config.steam_api_key
|
||||
# 获取当前使用的 key
|
||||
current_key: str = keys[current_key_index_1]
|
||||
# 更新计数器
|
||||
current_key = keys[current_key_index_1]
|
||||
current_key_index_1 = (current_key_index_1 + 1) % len(keys)
|
||||
|
||||
return [current_key]
|
||||
|
||||
|
||||
@check.handle()
|
||||
async def check_handle(
|
||||
bot: Bot,
|
||||
target: Target = Depends(get_target),
|
||||
arg: Message = CommandArg(),
|
||||
):
|
||||
if arg.extract_plain_text().strip() != "":
|
||||
return None
|
||||
|
||||
parent_id = target.parent_id or target.id
|
||||
|
||||
steam_ids = bind_data.get_all(parent_id)
|
||||
|
||||
current_key = key_select()
|
||||
|
||||
logger.info(f"当前调用的key:{current_key}")
|
||||
# 用户命令快速失败,不挤占播报配额
|
||||
steam_info = await get_steam_users_info(
|
||||
steam_ids, current_key, config.proxy, wait=False
|
||||
)
|
||||
if steam_info is None:
|
||||
await check.finish("Steam API 请求过于频繁,请稍后再试")
|
||||
|
||||
logger.debug(f"{parent_id} Players info: {steam_info}")
|
||||
|
||||
# 游戏名本地化(中文名优先)
|
||||
players = steam_info["response"]["players"]
|
||||
names = await get_game_cn_names(
|
||||
[int(p["gameid"]) for p in players if p.get("gameid")],
|
||||
db_path,
|
||||
)
|
||||
for p in players:
|
||||
if p.get("gameid"):
|
||||
p["gameextrainfo"] = localize_game_name(
|
||||
p.get("gameextrainfo", ""), int(p["gameid"]), names
|
||||
)
|
||||
|
||||
# 统计所有游玩中的游戏(单人也要显示游戏图标),商店页优先抓取 clienticon
|
||||
game_ids: list[int] = []
|
||||
seen: set[int] = set()
|
||||
for p in players:
|
||||
if p.get("gameid"):
|
||||
gid = int(p["gameid"])
|
||||
if gid not in seen:
|
||||
seen.add(gid)
|
||||
game_ids.append(gid)
|
||||
game_icons = (
|
||||
await get_game_icons(game_ids, config.proxy, db_path) if game_ids else {}
|
||||
)
|
||||
|
||||
# 后台用正在玩的 steamid 补剩余缺的 hash(不阻塞当前出图;商店页为主要手段)
|
||||
asyncio.create_task(_fill_missing_icon_hashes(players))
|
||||
|
||||
parent_avatar, parent_name = await get_parent_info(bot, parent_id)
|
||||
|
||||
steam_status_data = []
|
||||
for player in players:
|
||||
data = await simplize_steam_player_data(
|
||||
player, config.proxy, avatar_path, names
|
||||
)
|
||||
entry = bind_data.get_by_steam_id(parent_id, player["steamid"])
|
||||
data["nickname"] = (
|
||||
await get_group_nickname(bot, parent_id, entry["user_id"])
|
||||
if entry is not None
|
||||
else None
|
||||
)
|
||||
data["game_icon"] = game_icons.get(data["gameid"])
|
||||
steam_status_data.append(data)
|
||||
|
||||
image = draw_friends_status(parent_avatar, parent_name, steam_status_data)
|
||||
|
||||
await target.send(UniMessage(Image(raw=image_to_bytes(image))))
|
||||
|
||||
|
||||
@enable.handle()
|
||||
async def enable_handle(target: Target = Depends(get_target)):
|
||||
parent_id = target.parent_id or target.id
|
||||
|
||||
disable_parent_data.remove(parent_id)
|
||||
disable_parent_data.save()
|
||||
|
||||
await enable.finish("已启用 Steam 播报")
|
||||
|
||||
|
||||
@disable.handle()
|
||||
async def disable_handle(target: Target = Depends(get_target)):
|
||||
parent_id = target.parent_id or target.id
|
||||
|
||||
disable_parent_data.add(parent_id)
|
||||
disable_parent_data.save()
|
||||
|
||||
await disable.finish("已禁用 Steam 播报")
|
||||
|
||||
|
||||
async def _auto_sync_game_names():
|
||||
"""启动时自动同步游戏名映射表:未全量同步过则全量拉取,已标记完成则跳过"""
|
||||
try:
|
||||
if _is_full_sync_done(db_path):
|
||||
logger.info("游戏名映射表已全量同步过,跳过启动同步")
|
||||
return
|
||||
api_key = config.steam_api_key[0]
|
||||
count = await update_steamdb_apps(db_path, api_key)
|
||||
logger.info(f"游戏名映射表首次同步完成: {count} 条")
|
||||
except Exception as e:
|
||||
logger.error(f"游戏名映射表启动同步失败: {e}")
|
||||
|
||||
|
||||
@scheduler.scheduled_job("cron", hour=5, minute=0, id="sync_missing_games")
|
||||
async def _sync_missing_games():
|
||||
"""扫描绑定用户游戏库,自动补全映射表缺失的游戏
|
||||
|
||||
英文名从 GetOwnedGames 响应直接取(零额外请求),中文名留给查询时按需补全。
|
||||
"""
|
||||
try:
|
||||
all_games: dict[str, str] = {}
|
||||
all_icons: dict[int, str] = {}
|
||||
for parent_id in bind_data.content.keys():
|
||||
for steam_id in bind_data.get_all(parent_id):
|
||||
games = await fetch_play_time(steam_id)
|
||||
if games is None: # API 配额不足,跳过该用户
|
||||
logger.warning(
|
||||
f"Steam API 配额已满,跳过 {steam_id} 的游戏同步"
|
||||
)
|
||||
continue
|
||||
for g in games:
|
||||
if g.get("appid"):
|
||||
all_games.setdefault(str(g["appid"]), g["name"])
|
||||
if g.get("clienticon"):
|
||||
all_icons.setdefault(int(g["appid"]), g["clienticon"])
|
||||
if not all_games:
|
||||
return
|
||||
known = _query_game_names(db_path, [int(a) for a in all_games])
|
||||
missing = {
|
||||
aid: name for aid, name in all_games.items() if aid not in known
|
||||
}
|
||||
if missing:
|
||||
_upsert_game_names(
|
||||
db_path,
|
||||
{
|
||||
aid: {"name_en": name, "name_ch": ""}
|
||||
for aid, name in missing.items()
|
||||
},
|
||||
)
|
||||
logger.info(f"补全映射表缺失游戏: {len(missing)} 个")
|
||||
if all_icons:
|
||||
_upsert_game_icon_hashes(db_path, all_icons)
|
||||
logger.info(f"补全映射表游戏图标: {len(all_icons)} 个")
|
||||
except Exception as e:
|
||||
logger.error(f"补全映射表缺失游戏失败: {e}")
|
||||
|
||||
|
||||
@scheduler.scheduled_job(
|
||||
"cron", hour=4, minute=30, id="sync_game_names"
|
||||
# 触发器(handlers)在子包中
|
||||
from .handlers import ( # noqa: E402,F401
|
||||
bind,
|
||||
check,
|
||||
disable,
|
||||
enable,
|
||||
get_play_time,
|
||||
help,
|
||||
info,
|
||||
unbind,
|
||||
update_steamdb,
|
||||
)
|
||||
async def _daily_sync_game_names():
|
||||
"""每日全量同步游戏名映射表(保证表内始终有全部游戏)"""
|
||||
try:
|
||||
api_key = config.steam_api_key[0]
|
||||
count = await update_steamdb_apps(db_path, api_key)
|
||||
logger.info(f"游戏名映射表每日全量同步完成: {count} 条")
|
||||
except Exception as e:
|
||||
logger.error(f"游戏名映射表每日同步失败: {e}")
|
||||
|
||||
|
||||
async def _on_bot_connect(bot):
|
||||
"""bot 连接后后台执行映射表首次同步(不阻塞连接流程)"""
|
||||
asyncio.create_task(_auto_sync_game_names())
|
||||
|
||||
|
||||
nonebot.get_driver().on_bot_connect(_on_bot_connect)
|
||||
|
||||
|
||||
@update_steamdb.handle()
|
||||
async def update_steamdb_handle():
|
||||
await update_steamdb.send("正在拉取 Steam 全量 app 列表...")
|
||||
try:
|
||||
api_key = config.steam_api_key[0]
|
||||
count = await update_steamdb_apps(db_path, api_key)
|
||||
except Exception as e:
|
||||
logger.error(f"SteamDB 数据更新失败: {e}")
|
||||
await update_steamdb.finish(f"SteamDB 数据更新失败: {e}")
|
||||
if count:
|
||||
await update_steamdb.finish(f"Steam 数据更新完成,共 {count} 个游戏")
|
||||
else:
|
||||
await update_steamdb.finish("Steam 数据拉取失败,稍后重试")
|
||||
|
||||
|
||||
@get_play_time.handle()
|
||||
async def get_play_time_handle(
|
||||
bot: Bot,
|
||||
event: MessageEvent,
|
||||
target: Target = Depends(get_target),
|
||||
cmd_arg: Message = CommandArg(),
|
||||
):
|
||||
parent_id = target.parent_id or target.id
|
||||
user_data = None
|
||||
|
||||
logger.info(f"游玩时长表:进入处理, parent={parent_id}")
|
||||
arg_text = cmd_arg.extract_plain_text().strip()
|
||||
|
||||
if arg_text:
|
||||
if not arg_text.isdigit():
|
||||
await get_play_time.finish(
|
||||
"请输入正确的 Steam ID 或 Steam好友代码,格式: steamplaytime [Steam ID 或 Steam好友代码]"
|
||||
)
|
||||
steam_id = get_steam_id(arg_text)
|
||||
else:
|
||||
user_data = bind_data.get(parent_id, event.get_user_id())
|
||||
if user_data is None:
|
||||
await get_play_time.finish(
|
||||
"未绑定 Steam ID,请使用 steamplaytime [Steam ID 或 Steam好友代码] 查询,或先使用 steambind 绑定"
|
||||
)
|
||||
steam_id = user_data["steam_id"]
|
||||
await get_play_time.send("正在获取数据,请耐心等待")
|
||||
logger.info(f"游玩时长表:开始获取数据, steam_id={steam_id}")
|
||||
games = await fetch_play_time(steam_id)
|
||||
if games is None:
|
||||
await get_play_time.finish("Steam API 请求过于频繁,请稍后再试")
|
||||
logger.info(f"游玩时长表:数据获取完成, 共 {len(games)} 款游戏")
|
||||
|
||||
if not games:
|
||||
await get_play_time.finish("没有获取到游玩数据(游戏可能未公开)")
|
||||
|
||||
# 顺手把 GetOwnedGames 返回的 clienticon hash 写入映射表,供 steamcheck 图标使用
|
||||
icon_entries = {
|
||||
int(g["appid"]): g.get("clienticon")
|
||||
for g in games
|
||||
if g.get("appid") and g.get("clienticon")
|
||||
}
|
||||
if icon_entries:
|
||||
_upsert_game_icon_hashes(db_path, icon_entries)
|
||||
|
||||
# 游戏名本地化:仅查本地映射表,有中文用中文,没有保持 API 原始名(不联网补全)
|
||||
appids = [g["appid"] for g in games if g.get("appid")]
|
||||
game_map = _query_game_names(db_path, appids) if appids else {}
|
||||
names = {
|
||||
int(aid): entry["name_ch"]
|
||||
for aid, entry in game_map.items()
|
||||
if entry.get("name_ch")
|
||||
}
|
||||
for g in games:
|
||||
g["name"] = localize_game_name(g["name"], g.get("appid"), names)
|
||||
|
||||
# 分离已游玩(表格/饼图)与全量(柱状图按时长分段,含 0 时长)
|
||||
played = [g for g in games if g["playtime_hours"] > 0]
|
||||
if not played:
|
||||
await get_play_time.finish("没有游玩记录(游戏可能未公开或全部未游玩)")
|
||||
for i, g in enumerate(played, 1):
|
||||
g["rank"] = i
|
||||
|
||||
# 渲染 HTML 图片(Playwright);失败则提示并结束
|
||||
try:
|
||||
from .html_playtime import render_play_time_table
|
||||
|
||||
# 头像与昵称:Steam 昵称常显,插件昵称(群昵称)设置了才显示
|
||||
steam_name, plugin_nick, avatar = "", "", None
|
||||
try:
|
||||
player_data = await get_user_data(steam_id, cache_path, config.proxy)
|
||||
steam_name = player_data.get("player_name", "")
|
||||
avatar = player_data.get("avatar")
|
||||
except Exception as e:
|
||||
logger.warning(f"获取玩家资料失败,省略头像昵称: {e}")
|
||||
if user_data:
|
||||
plugin_nick = await get_group_nickname(
|
||||
bot, parent_id, user_data["user_id"]
|
||||
)
|
||||
|
||||
logger.info(f"游玩时长表:开始渲染 HTML 图片, steam={steam_name}, nick={plugin_nick}")
|
||||
img_bytes = await render_play_time_table(
|
||||
played, games, steam_name, plugin_nick, avatar,
|
||||
theme=config.steam_playtime_theme,
|
||||
)
|
||||
logger.info(f"游玩时长表:渲染完成, {len(img_bytes)} bytes")
|
||||
except Exception as e:
|
||||
logger.error(f"渲染游玩时长表图片失败: {e}", exc_info=True)
|
||||
await get_play_time.finish("游玩时长表渲染失败,请稍后重试")
|
||||
|
||||
# 发送移出 try:finish 成功后抛的 StopPropagation 不能被 except 捕获
|
||||
# (否则图片发出后还会再补一句"渲染失败"),与 info 指令一致先 export
|
||||
# 回复原指令消息,群消息多时不易混淆
|
||||
uni_msg = MessageSegment.reply(event.message_id) + await UniMessage(
|
||||
Image(raw=img_bytes)
|
||||
).export(bot)
|
||||
await get_play_time.finish(uni_msg)
|
||||
|
||||
Reference in New Issue
Block a user