OpenClaw Discord Skill:用 message 工具完成 Discord 消息工作流的设计与实现
【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw
本文以 OpenClaw 仓库中 Discord 插件内置的 discord skill 为主体,完整还原该 skill 定义的工作流规则(ID 选取、线程与 forum 约束、破坏性操作确认)、交互式组件(Components V2)的 JSON 用法及其硬性限制,并结合 discord 插件源码 逐条印证channels.discord.actions.*配置门控如何动态生成message工具的动作清单与参数 schema,以及 send 类动作为何在本地执行、其余动作交由 gateway 处理。读完后你可以理解:为什么模型"不能假设不存在的 action",以及一个 Discord 消息请求从工具 schema 发现到实际执行的完整链路。
Skill 的定位与 frontmatter
该 skill 位于 Discord 插件内部:extensions/discord/skills/discord/SKILL.md,其 frontmatter 声明了 skill 的基本契约:
--- name: discord description: "Discord messaging workflows through OpenClaw's message tool." metadata: { "openclaw": { "emoji": "🎮", "requires": { "config": ["channels.discord"] } } } allowed-tools: ["message"] ---三个关键点:
requires.config: ["channels.discord"]表示只有配置中启用了 Discord 通道(存在channels.discord配置)时,该 skill 才会对 Agent 生效。这与源码中describeDiscordMessageTool的发现逻辑一致:当没有任何"已启用且已配置"的 Discord 账号时,直接返回actions: []、capabilities: []、schema: null,即工具不暴露任何 Discord 动作(见 channel-actions.ts)。allowed-tools: ["message"]表明该 skill 的全部能力收敛在单一的message工具上,调用时固定携带channel: "discord"。- 原文档开宗明义:"工具 schema 会列出当前账号
channels.discord.actions.*门控允许的动作;不要假设不存在的动作。"这不是空话,而是由源码保证的——下文展开。
动作清单由配置门控动态生成
Skill 的核心原则是"以工具 schema 为准,而非静态动作目录"。在 channel-actions.ts 的describeDiscordMessageTool中可以看到完整的门控→动作映射:
actions.*配置键 | 默认值 | 解锁的 message 动作 |
|---|---|---|
| (无门控) | — | send(始终可用,只要有可发现的账号) |
polls | 开 | poll |
reactions | 开 | react、reactions、emoji-list |
messages | 开 | upload-file、read、edit、delete |
pins | 开 | pin、unpin、list-pins |
permissions | 开 | permissions |
threads | 开 | thread-create、thread-list、thread-reply |
search | 开 | search |
stickers | 开 | sticker |
memberInfo | 开 | member-info |
roleInfo | 开 | role-info |
emojiUploads | 开 | emoji-upload |
stickerUploads | 开 | sticker-upload |
roles | 关 | role-add、role-remove |
channelInfo | 开 | channel-info、channel-list |
channels | 开 | channel-create、channel-edit、channel-delete、channel-move、category-create、category-edit、category-delete |
voiceStatus | 开 | voice-status |
events | 开 | event-list、event-create |
moderation | 关 | timeout、kick、ban |
presence | 关 | set-presence |
这些配置键与 config-schema.ts 中actions的严格 schema 一一对应(reactions、stickers、emojiUploads、stickerUploads、polls、permissions、messages、threads、pins、search、memberInfo、roleInfo、roles、channelInfo、voiceStatus、events、moderation、channels、presence,均为可选布尔值)。注意三个默认关闭的门控:roles、moderation、presence——涉及角色增删、禁言/踢出/封禁、状态设置这类高敏感操作,需要显式开启,这与 skill 文档"不要假设不可用动作"的告诫相互印证。
门控的取值来源有双层结构,见 accounts.ts 的createDiscordActionGate:
- 基础层:
cfg.channels.discord.actions(通道级默认); - 账号层:
cfg.channels.discord.accounts.<accountId>.actions(单账号覆盖)。
当未指定accountId时,channel-actions.ts 的resolveDiscordActionDiscovery会对所有已启用且已配置的账号做并集(createUnionActionGate):任一账号开启某门控,该动作就会出现在工具 schema 中;指定了accountId后则只看该账号自己的门控。这就是 skill 中"当多个 Discord 账号可能适用时,请传accountId"的底层依据——传与不传,决定的是动作清单的并集还是单集。
Skill 工作流规则及其源码依据
Skill 文档给出的四条工作流规则,每一条都能落到具体实现约束上:
1. 优先使用稳定的guildId、channelId、messageId、userId;多账号时传accountId。Discord 的 Snowflake ID 是全局稳定标识。配置层面,config-schema.ts 的DiscordIdSchema明确允许数字或字符串形式的 ID,并对数字做了安全整数校验(非安全整数会被要求"在配置文件中加引号"),说明整个体系以 Snowflake 字符串为一等公民。
2. 用户指代含糊时,先解析出精确消息再执行编辑、删除、置顶、 moderation 或 reaction。这一条对应动作侧的实现:编辑/删除/置顶等动作都归入messages/pins/moderation门控(见上表),执行入口统一走 handle-action.ts 与 runtime.moderation.ts,后者带有独立的鉴权测试 runtime.moderation.authz.test.ts。源码结构表明 moderation 类动作在执行前会做请求者身份核验,因此"先确认精确目标"既是行为要求也是安全边界。
3. 线程回复留在原线程内;forum 父频道不能接收 components,应发到已创建的 forum 线程中。这是 Discord 平台侧的真实限制:forum 类型的父频道是只读的消息容器,只有其下自动创建的帖子线程才可承载交互组件。工具层对线程语义有专门的适配——channel-actions.ts 中的resolveDiscordThreadReplyTarget/resolveDiscordThreadReplyDeliveryAlias会把threadId归一化为channel:<threadId>目标,thread-reply动作通过messageActionTargetAliases声明threadId别名(第 278-286 行),并在matchesCurrentConversation命中时直接投递到当前会话线程,保证"线程回复留在原线程"。
4. 破坏性或 moderation 动作必须确认,除非用户已明确指定目标与动作。配合默认关闭的moderation、roles门控,以及requiresTrustedRequesterSender(guild-admin 类动作需要受信任请求者,见 channel-actions.ts),skill 文档把"确认"写成了 Agent 的硬性行为约束。
交互式组件:Components V2 的用法与硬性限制
Skill 文档给出的组件示例(原文完整保留):
{ "action": "send", "channel": "discord", "to": "channel:123", "message": "Choose an option", "components": { "blocks": [ { "type": "actions", "buttons": [ { "label": "Approve", "style": "success", "callbackData": "approve" }, { "label": "Decline", "style": "danger", "callbackData": "decline" } ] } ] } }两条硬性约束来自原文,且都能在源码中找到执行点:
约束一:components必须是结构化对象或原生组件数组,绝不能是占位字符串。在 channel-actions.ts 的prepareSendPayload中,ctx.params.components先经coerceDiscordComponentParam强制转换(定义于 components.parse.ts);若转换结果是函数(非法输入形态),直接返回null拒绝发送。合法对象再经readDiscordComponentSpec解析为组件 spec,原生数组则作为nativeComponents直通,最终写入channelData.discord.components。schema 侧同样如此:describeDiscordMessageTool为send动作声明的components属性是blocks(V2 块数组:text、buttons、selects、media、containers、separators)加可选modal的对象结构(第 229-262 行),与 components.ts 导出的类型体系(DiscordComponentBlock、DiscordComponentButtonSpec、DiscordModalSpec等)及构建器(buildDiscordComponentMessage、createDiscordFormModal)保持一致。
约束二:Components V2 不得与 legacyembeds混用。这一点在prepareSendPayload中被写成显式守卫:
if ((componentSpec || nativeComponents) && embeds?.length) { return null; // 组件与 embeds 同时出现 → 拒绝 }即两者同时存在时整个 payload 被拒绝,而不是静默降级,这正是 skill 文档要求 Agent"不要组合"的原因。
组件点击后的回传链路同样有源码支撑:custom id 的编解码集中在 component-custom-id.ts(buildDiscordComponentCustomId/parseDiscordComponentCustomId),交互分发在 interactive-dispatch.ts,按钮/选择器事件最终被格式化为Clicked "Approve".这类文本回灌给模型(formatDiscordComponentEventText)。
执行位置:本地动作与 gateway 动作的分流
从源码结构看,Discord 动作并非全部在同一位置执行。channel-actions.ts 定义了localExecutionActions白名单:
const localExecutionActions = new Set<ChannelMessageActionName>([ "send", "poll", "upload-file", "thread-reply", "sticker", "emoji-upload", "sticker-upload", "event-create", ]);白名单内的动作以local模式执行,源码注释解释了原因:"Credential-only Discord actions run in the gateway when one is available. Send/file-style actions stay local because core owns their thread, media, component, and client-local payload semantics."——发送、上传、线程回复这类动作涉及核心拥有的线程路由、媒体文件与本地组件语义,必须留在客户端本地处理;而编辑、删除、置顶、moderation 等"只需凭证"的动作在可用时交给 gateway 执行。此外supportsAction明确排除poll(Discord 投票走独立本地路径)。理解这个分流,有助于在排查"某个动作为什么没执行/在哪里执行"时定位正确的日志与进程边界。
提示注入与"不要维护重复目录"
Skill 文档最后一句值得单独强调:"Discord mention 语法、组件可用性和表单提示会自动注入。请遵循当前的提示与工具 schema,而不是复制的动作目录。"
结合源码可以推断其设计动机:动作清单、schema 描述(例如react动作的emoji参数说明会根据emoji-list是否可用动态变化,见 channel-actions.ts)在每次工具发现时实时计算。若 skill 文档里硬编码一份动作目录,配置变更后必然失真;因此文档只固化工作流规则与组件约束这类不变量,把能力清单完全交给运行时 schema。这也是编写其他 OpenClaw channel skill 时可参考的模式:frontmatter 声明依赖配置与允许工具,正文写行为规则,能力细节交给门控与 schema 动态生成。
相关源码索引
| 主题 | 路径 |
|---|---|
| Skill 定义(本文主体) | extensions/discord/skills/discord/SKILL.md |
| 工具发现与动作门控映射 | extensions/discord/src/channel-actions.ts |
| 账号配置合并与 action gate | extensions/discord/src/accounts.ts |
channels.discord配置 schema(含actions.*) | extensions/discord/src/config-schema.ts |
| 组件解析/构建/模态框 | extensions/discord/src/components.ts |
| 动作执行入口 | extensions/discord/src/actions/handle-action.ts |
| moderation 执行与鉴权测试 | extensions/discord/src/actions/runtime.moderation.ts、runtime.moderation.authz.test.ts |
| 门控行为的契约测试 | extensions/discord/src/channel-actions.contract.test.ts、channel-actions.test.ts |
适用前提:以上所有结论均基于当前仓库中 Discord 插件的源码与 skill 文档,需配置channels.discord(含有效 bot token,账号可通过token配置项或DISCORD_BOT_TOKEN环境变量提供,见 accounts.ts)方可生效;动作的最终可见集合由通道级与账号级actions.*门控共同决定。
【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考