Discord Bot+FastAPI集成发送消息遇RuntimeError及后续问题求助
问题描述
我正在开发一个与Discord Bot集成的API,尝试通过Bot向频道发送消息时触发了RuntimeError。改用Discord Webhook发送消息可成功,但Webhook不返回消息ID,无法通过Bot为消息添加反应;同时我不确定调用反应相关函数是否也会触发相同的RuntimeError。曾尝试直接调用Discord API但未成功,担心仍会出现相同报错,寻求解决方案。
完整报错栈
ERROR: Exception in ASGI application Traceback (most recent call last): File "/home/container/.local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 407, in run_asgi result = await app( # type: ignore[func-returns-value] File "/home/container/.local/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__ return await self.app(scope, receive, send) File "/home/container/.local/lib/python3.10/site-packages/fastapi/applications.py", line 271, in __call__ await super().__call__(scope, receive, send) File "/home/container/.local/lib/python3.10/site-packages/starlette/applications.py", line 118, in __call__ await self.middleware_stack(scope, receive, send) File "/home/container/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__ raise exc File "/home/container/.local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__ await self.app(scope, receive, _send) File "/home/container/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__ raise exc File "/home/container/.local/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__ await self.app(scope, receive, sender) File "/home/container/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__ raise e File "/home/container/.local/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__ await self.app(scope, receive, send) File "/home/container/.local/lib/python3.10/site-packages/starlette/routing.py", line 706, in __call__ await route.handle(scope, receive, send) File "/home/container/.local/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle await self.app(scope, receive, send) File "/home/container/.local/lib/python3.10/site-packages/starlette/routing.py", line 66, in app response = await func(request) File "/home/container/.local/lib/python3.10/site-packages/fastapi/routing.py", line 237, in app raw_response = await run_endpoint_function( File "/home/container/.local/lib/python3.10/site-packages/fastapi/routing.py", line 163, in run_endpoint_function return await dependant.call(**values) File "/home/container/bot.py", line 345, in test m = await channel.send(embed=embed) File "/home/container/.local/lib/python3.10/site-packages/discord/abc.py", line 1561, in send data = await state.http.send_message(channel.id, params=params) File "/home/container/.local/lib/python3.10/site-packages/discord/http.py", line 624, in request async with self.__session.request(method, url, **kwargs) as response: File "/home/container/.local/lib/python3.10/site-packages/aiohttp/client.py", line 1141, in __aenter__ self._resp = await self._coro File "/home/container/.local/lib/python3.10/site-packages/aiohttp/client.py", line 467, in _request with timer: File "/home/container/.local/lib/python3.10/site-packages/aiohttp/helpers.py", line 701, in __enter__ raise RuntimeError( RuntimeError: Timeout context manager should be used inside a task
API调用代码
class ClanCreateBody(BaseModel): clan_tag: str user_id: int username: str @app.get("/createclan/") async def test(body: ClanCreateBody, AuthToken: str | None = Header(default=None)): if AuthToken == "testtoken": pass else: return {"Invalid": "Token"} check_emoji = bool(emoji.get_emoji_regexp().search(body.clan_tag)) if check_emoji: return {"Invalid": "Emojis are not allowed in clan tags"} if not body.clan_tag: return {"Invalid": "There is no clan tag"} # Check they don't already have a clan has_clan, clan = await bot.id_has_clan_both(body.user_id) if has_clan: if clan.role_id == -1: return {"Invalid": "User is already in a clan"} else: return {"Invalid": "User is already in a clan"} # Check this clan name isn't already taken name_is_taken = await bot.clan_name_is_taken(body.clan_tag) if name_is_taken: return {"Invalid": "This clan name is already taken!"} # It's good to create channel = bot.get_channel(secret_file["staff_channel_id"]) embed = discord.Embed( title="Clan creation request", description=f"Clan name: `{body.clan_tag}`\nRequester: {body.username}\n\n" f"To accept this please react with a tick.\n" f"To reject this please react with a cross.", color=embed_color, timestamp=discord.utils.utcnow(), ) embed.set_footer(text=f"{body.user_id} : Sent from web") m = await channel.send(embed=embed) await m.add_reaction("✅") await m.add_reaction("❎") await bot.db.pending_clans.insert( { "_id": body.clan_tag.lower(), "name": body.clan_tag, "role_id": -1, "clan_points": 0, "total_koth_wins":0, "total_roam_wins":0, "total_maze_wins":0, "leader_id": body.id, "members": [], "co_leaders": [], "pending_invites": [], "pending_message_id": m.id, "created_at": datetime.datetime.now().strftime("%-I:%M %p %m/%d/%Y"), "discord_invite": "Set with !clansetdiscord", "video_url": "!Set with !clannew", "channel_id": None, } ) for member in staff: try: sname = secret_file["server_name"] await member.send(embed = discord.Embed(title= f"New Clan Application on {sname}",description=f"{body.user_id} wants to make **{body.clan_tag}** [Click Here]({m.jump_url}) to accept/decline that.",color=embed_color)) except (discord.HTTPException, discord.Forbidden): pass return {"Status": "Request sent"}
解决方案
这个报错的核心原因是:aiohttp的超时上下文管理器没有在合法的asyncio任务环境中运行,本质是FastAPI的ASGI请求上下文与discord.py的事件循环不兼容导致的。以下是三种可行的解决方式:
方式1:用discord.py的事件循环创建独立任务执行Discord操作
将发送消息、添加反应、数据库插入等Discord相关逻辑,放到独立的异步函数中,通过discord.py的事件循环创建任务执行,脱离FastAPI的请求上下文:
# 定义独立的异步处理函数 async def handle_clan_request(channel, embed, bot, body, staff, secret_file): m = await channel.send(embed=embed) await m.add_reaction("✅") await m.add_reaction("❎") # 修复原代码bug:leader_id应该取body.user_id而非body.id await bot.db.pending_clans.insert( { "_id": body.clan_tag.lower(), "name": body.clan_tag, "role_id": -1, "clan_points": 0, "total_koth_wins":0, "total_roam_wins":0, "total_maze_wins":0, "leader_id": body.user_id, "members": [], "co_leaders": [], "pending_invites": [], "pending_message_id": m.id, "created_at": datetime.datetime.now().strftime("%-I:%M %p %m/%d/%Y"), "discord_invite": "Set with !clansetdiscord", "video_url": "!Set with !clannew", "channel_id": None, } ) for member in staff: try: sname = secret_file["server_name"] await member.send(embed = discord.Embed(title= f"New Clan Application on {sname}",description=f"{body.user_id} wants to make **{body.clan_tag}** [Click Here]({m.jump_url}) to accept/decline that.",color=embed_color)) except (discord.HTTPException, discord.Forbidden): pass # 在原test函数中,替换原发送逻辑为创建任务 bot.loop.create_task(handle_clan_request(channel, embed, bot, body, staff, secret_file))
方式2:让FastAPI与discord.py共享同一个事件循环
配置uvicorn使用discord.py的事件循环启动FastAPI,确保上下文兼容:
from uvicorn import Config, Server # 初始化bot后获取它的事件循环 loop = bot.loop # 配置FastAPI使用该循环 config = Config(app=app, loop=loop) server = Server(config) # 启动服务 loop.run_until_complete(server.serve())
方式3:直接调用Discord REST API(可获取消息ID)
绕过discord.py的会话,用aiohttp直接调用Discord官方API,发送消息后拿到ID再调用添加反应的接口:
import aiohttp async def send_and_react(channel_id, embed, bot_token): headers = {"Authorization": f"Bot {bot_token}"} async with aiohttp.ClientSession() as session: # 发送消息 send_url = f"https://discord.com/api/v9/channels/{channel_id}/messages" payload = {"embeds": [embed.to_dict()]} async with session.post(send_url, json=payload, headers=headers) as resp: if resp.status != 200: raise Exception(f"发送消息失败: {await resp.text()}") message_data = await resp.json() message_id = message_data["id"] # 添加✅反应 react_url = f"https://discord.com/api/v9/channels/{channel_id}/messages/{message_id}/reactions/%E2%9C%85/@me" await session.put(react_url, headers=headers) # 添加❎反应 react_url = f"https://discord.com/api/v9/channels/{channel_id}/messages/{message_id}/reactions/%E2%9D%8E/@me" await session.put(react_url, headers=headers) return message_id
内容的提问来源于stack exchange,提问作者Murder
相关产品推荐
相关产品推荐

