
本文详解如何通过设置 timeout=none、为按钮分配唯一 custom_id 并在启动时注册 view,使 discord.py 的交互式按钮在机器人重启或会话中断(如出现 “shared id none has successfully resumed session”)后仍能正常响应。
本文详解如何通过设置 timeout=none、为按钮分配唯一 custom_id 并在启动时注册 view,使 discord.py 的交互式按钮在机器人重启或会话中断(如出现 “shared id none has successfully resumed session”)后仍能正常响应。
在使用 discord.py 构建交互式应用(如服务器申请审核系统)时,你可能会遇到一种典型故障:按钮点击后显示 “This interaction failed”,控制台却无报错,仅偶现日志 Shared ID None has successfully RESUMED session。这并非网络或权限问题,而是 View 缺乏持久性(persistence)导致的底层状态丢失——当 bot 会话因断连、重启或心跳恢复而重建时,原始 View 实例已从内存中销毁,Discord 服务端无法再将用户点击路由到有效的处理器。
✅ 正确实现持久化 View 的三大关键步骤
1. 禁用超时,并显式声明 timeout=None
默认情况下,discord.ui.View 在 60 秒无交互后自动销毁。持久化 View 必须永久存活(由 bot 主动管理生命周期),因此需在初始化时明确禁用超时:
class ReviewView(discord.ui.View):
def __init__(self, embed_user: discord.User, floor: int):
# 关键:timeout=None 表示该 View 不自动过期
super().__init__(timeout=None)
self.embed_user = embed_user
self.floor = floor
⚠️ 注意:timeout=None 仅表示“不自动超时”,不代表 View 可跨进程/重启存活——它仍需配合后续步骤才能真正持久。
2. 为每个组件(如按钮)分配唯一且稳定的 custom_id
Discord 依赖 custom_id 将用户点击映射到对应按钮处理器。若未指定,框架会生成临时 ID,重启后该 ID 失效,导致交互无法分发。
✅ 正确做法:为按钮硬编码或生成确定性、唯一、可复用的 custom_id。例如:
@discord.ui.button(
label="Accept",
style=discord.ButtonStyle.success,
custom_id="review_accept_button" # ✅ 静态 ID(适用于单实例场景)
)
async def approve(self, interaction: discord.Interaction, button: discord.ui.Button):
# 处理逻辑保持不变...
? 若需支持多个并发申请(即多个 ReviewView 实例),则 custom_id 必须全局唯一。推荐方案是拼接业务标识:
def __init__(self, embed_user: discord.User, floor: int):
super().__init__(timeout=None)
self.embed_user = embed_user
self.floor = floor
# 为按钮动态生成唯一 ID(如基于用户ID+楼层)
self.custom_id_base = f"review_{embed_user.id}_{floor}"
@discord.ui.button(
label="Accept",
style=discord.ButtonStyle.success,
custom_id=lambda self: f"{self.custom_id_base}_accept" # ❌ 错误:lambda 不可序列化
)
# ✅ 正确写法:在 __init__ 中预设,并在装饰器中引用
# → 实际应使用类属性或工厂函数,详见下方完整示例
更健壮的做法是在 View 初始化时预先绑定按钮 ID(避免装饰器内动态计算):
class ReviewView(discord.ui.View):
def __init__(self, embed_user: discord.User, floor: int):
super().__init__(timeout=None)
self.embed_user = embed_user
self.floor = floor
# 生成稳定唯一 ID(确保不重复)
self.accept_id = f"review_accept_{embed_user.id}_{floor}"
# 动态添加按钮(绕过装饰器限制)
self.add_item(discord.ui.Button(
label="Accept",
style=discord.ButtonStyle.success,
custom_id=self.accept_id
))
# 绑定回调(需手动处理)
self.children[-1].callback = self.approve
async def approve(self, interaction: discord.Interaction):
# 此处可安全访问 self.embed_user 和 self.floor
...
但更推荐使用官方推荐的 @discord.ui.button(custom_id=...) + 启动时全局注册 模式(见下文)。
3. 在 bot 启动时注册 View(add_view),而非运行时创建
这是持久化的决定性一步。bot 必须在启动早期(setup_hook)将 View 类注册到内部路由表,使 Discord 的交互请求能在任何时间点被正确分发到对应处理器。
✅ 推荐方式(使用 setup_hook):
# 在 bot 初始化后、登录前注册
async def setup_hook():
# 注意:此处传入的是 View 类(无需实例化),且不能带参数!
bot.add_view(ReviewView()) # ✅ 注册空 View 类(用于匹配 custom_id)
@bot.event
async def on_ready():
print(f'Logged in as {bot.user}')
# 设置 hook(discord.py v2.0+)
bot.setup_hook = setup_hook
⚠️ 关键约束:
- add_view(ReviewView()) 中的 ReviewView() 必须是无参构造的实例(即 __init__ 不能强制要求 embed_user 等运行时参数);
- 因此,持久化 View 的业务数据(如 embed_user, floor)不能存于 View 实例属性中,而应通过 custom_id 编码并解析。
✅ 最佳实践:将状态编码进 custom_id,并在回调中解码:
import json
class ReviewView(discord.ui.View):
def __init__(self):
super().__init__(timeout=None)
@discord.ui.button(
label="Accept",
style=discord.ButtonStyle.success,
custom_id="review_accept" # ✅ 静态 ID,供全局注册
)
async def approve(self, interaction: discord.Interaction, button: discord.ui.Button):
# 从 interaction.message.embeds[0] 或 message.content 中提取原始参数
# 更可靠的方式:在发送 View 时,将必要数据存入 embed 的 footer 或 field(不可见)
# 或 —— 推荐:利用 custom_id 编码(需保证长度 ≤100)
# 示例:custom_id = "review_accept|123456789|3" → 用户ID|楼层
pass
# 发送消息时,使用带参数的 View 实例(仅用于渲染,不参与持久路由)
# 而持久路由由上面注册的无参 View 类处理
view = ReviewView() # 无参实例,仅用于本次发送
await interaction.followup.send(embed=embed, view=view, ephemeral=False)
但更清晰的模式是分离「渲染 View」与「持久 View」:
# 持久化处理器(无状态,仅响应)
class PersistentReviewView(discord.ui.View):
def __init__(self):
super().__init__(timeout=None)
@discord.ui.button(label="Accept", style=discord.ButtonStyle.success, custom_id="persistent_review_accept")
async def approve(self, interaction: discord.Interaction, button: discord.ui.Button):
# 解析 custom_id 或从消息中提取上下文
# 例如:检查 interaction.message.embeds[0].footer.text 是否含 "user:123|floor:3"
embed = interaction.message.embeds[0]
footer = embed.footer.text or ""
if "|" in footer:
parts = footer.split("|")
if len(parts) >= 2:
try:
user_id = int(parts[0].split(":")[1])
floor = int(parts[1].split(":")[1])
# 执行业务逻辑...
await interaction.response.send_message("✅ 已接受申请", ephemeral=True)
except (ValueError, IndexError):
await interaction.response.send_message("❌ 数据解析失败", ephemeral=True)
else:
await interaction.response.send_message("❌ 缺少上下文信息", ephemeral=True)
# 启动注册
async def setup_hook():
bot.add_view(PersistentReviewView())
bot.setup_hook = setup_hook
? 总结:持久化三要素缺一不可
| 要素 | 作用 | 错误示例 | 正确做法 |
|---|---|---|---|
| timeout=None | 防止 View 自动销毁 | super().__init__()(默认 60s) | super().__init__(timeout=None) |
| custom_id | 提供跨重启的交互路由键 | 未设置(自动生成临时 ID) | 显式声明静态或确定性 ID(≤100 字符) |
| bot.add_view(ViewClass()) | 注册处理器到全局路由表 | 仅在命令中 view=View() | 在 setup_hook 中调用 bot.add_view(ViewClass()) |
完成以上配置后,即使 bot 断线重连、容器重启或触发 RESUMED session,按钮交互也将稳定响应,彻底告别 “This interaction failed”。务必测试:重启 bot 后点击历史消息中的按钮,验证是否仍可触发 approve 回调。











