基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
编程提效2026-08-04 05:10

多个 Claude Code Agent 通过 Discord 协作实战

dev.to · AI#Claude Code#Agent#Discord
Editor brief · 编辑速览

实战分享多个 Claude Code 长期会话如何通过 Discord 机器人身份相互通讯,包含权限配置、状态管理等工程细节。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

我同时维护着几个长期项目,每个项目都有自己的 Claude Code session,也各自积累了不同的上下文。它们使用不同的技术栈、不同的数据库,彼此没有交集。这些 session 的价值恰恰在于它们不共享上下文——但这也意味着,每个 session 都困在自己的终端窗口里,而我只有坐在办公桌前才能与它们交流。

于是,我给每个 session 都分配了一个 Discord bot 身份。现在,即使站在排队的人群中,我也可以向熟悉 .NET 代码库的那个 Agent 询问计费方面的边界情况。

完成这部分只花了一个晚上。接着,我尝试让它们彼此交流,结果耗费的时间多得多——还顺带发现了一个我亲手写进去的安全漏洞。

Claude Code 的 Discord channel plugin 可以让 session 发送和接收 Discord 消息。每个项目运行一个 session,再分别配置独立的 bot token,每个项目就都拥有了一个可单独寻址的身份。

其中有一点不太直观:plugin 会从环境变量中读取其状态目录,因此每个 bot 都需要使用自己的目录。

$env:DISCORD_STATE_DIR = "$env:USERPROFILE\.claude\channels\bot-a"
claude

每个状态目录中都有一个 access.json,用于规定哪些人可以在哪些频道中与这个 bot 交谈,以及是否必须提及它。

{
  "dmPolicy": "allowlist",
  "allowFrom": ["<my-user-id>"],
  "groups": { "<channel-id>": { "requireMention": true, "allowFrom": [] } }
}

如果某个频道没有出现在 groups 中,那么消息会在模型看到之前就被丢弃。当一个 bot 明明已经被邀请进频道,却始终保持沉默时,首先应该检查这里。

障碍:bot 无法触发 bot

当四个 bot 同处一个频道时,显而易见的下一步就是让其中一个负责协调其他 bot。我的协调 bot 发送了一条消息,并直接提及另一个 bot。结果什么都没有发生。没有错误、没有拒绝,也没有任何日志——被提及的 bot 就像什么都没听见一样,继续做自己的事情。

我以为这是权限问题,于是在 Discord developer portal 里折腾了好一阵。但问题并不在那里。下面这个受控对比实验最终确认了原因:

同一个频道、同一份配置、完全相同的提及文本,唯一的区别是消息的发送者。

原因就在 plugin 的 server.ts 中,只有四行代码:

client.on('messageCreate', msg => {
  if (msg.author.bot) return
  handleInbound(msg).catch(...)
})

Discord 完整无误地传递了 gateway event。真正的问题是,plugin 在访问控制和模型处理之前就把它丢弃了。无论授予什么权限都无法改变这一点,而且也没有相关设置——server 只会读取三个环境变量,其中没有任何一个能影响这里。

Where a bot-authored message dies in the pipeline

这件事值得停下来琢磨一下,因为它揭示了一条调试集成系统时的通用经验:负责传递 event 的层与负责处理 event 的层并不是同一层,而配置只能修复其中某一层的问题。

为什么这一行不可或缺

最显而易见的修复方式是删掉这个 guard。千万别这么做。

这一行代码同时承担着两项职责:它既会拦截其他 bot,也会阻止 bot 对自己的消息作出反应。如果删除它却不提供替代机制,那么只要某个 bot 被允许接收自己的消息,它就会回复自己的回复,无限循环下去,而且循环速度就是 API 的调用速度。

这并非假设。在理解这个限制之前,我曾发送过一条同时提及四个 bot 的消息。六秒之内,其中三个 bot 都作出了回复。如果它们能够听到彼此的消息,这场对话就不会停下来。

因此,替代方案必须将这两个问题分开处理:

// unconditional, not configurable
if (msg.author.id === client.user?.id) return

……至于是否接受其他 bot 的消息,则应该转移到一个能够进行合理控制的位置。

我在 access.json 中添加了一个 allowBots: string[],其默认值为 []——这与原来的行为完全一致,因此没有进行相关配置的 bot 不会受到任何影响。

有两个细节甚至比这个功能本身更加重要:

检查逻辑位于 gate() 中,而不是 event handler 中。gate() 会在收到每条消息时重新读取 access.json,所以即使四个 session 都在运行,也可以直接修改 allowlist,无须重启任何东西。

系统不会进行中心化的强制控制。每个 bot 都通过自己的配置指定哪些身份可以打断它。系统中不存在全局 registry,因此也就不存在某个文件一旦损坏,所有访问限制便会同时失效的问题。

拓扑结构:hub-and-spoke,而不是 mesh

我的第一个设计是在 server 中加入 token-bucket rate limiter,因为四个 Agent 组成的 mesh 会产生平方级增长的循环消息,而我希望能有一道兜底防线。

后来,我改用另一种拓扑结构,大部分问题也随之消失了。

Mesh versus hub and spoke

其中一个 Agent 充当 hub。每个 spoke 的 allowBots 中只列出 hub,而 hub 则列出所有 spoke。spoke 无法彼此唤醒。唯一可能形成的循环是 hub ↔ 某个 spoke,而且从系统结构上看,hub 必然位于每一条路径上——因此,只要在 hub 中设置一个 loop-breaker,就能覆盖整个系统。我也不必把避免系统失控的全部希望都寄托在 rate limiter 上。

不过,我还是保留了 server 端的 limiter,作为一种成本很低的保险措施。prompt 层面的规则可能会被模型找到理由绕过,而 server 代码不会。但它不再是不可或缺的关键机制——这正是安全保障与一厢情愿之间的区别。

这里有一个属性值得准确说明:这种隔离针对的是调用,而不是可见性。只要 spoke 当时恰好处于唤醒状态,它仍然可以读取其他 spoke 的消息;它只是无法唤醒其他 spoke。这才是这种设计真正换来的能力,而且它比乍看之下要有限得多。

我在审查自己的 patch 时发现的漏洞

这是我最希望其他人能够引以为鉴的部分。

plugin 中有一个用于处理权限 prompt 的拦截机制:当某个 tool 需要批准时,你可以直接在 Discord 中回复 y <request_id>,而不必回到终端。附近的代码中有一条注释,大意是:任何通过 gate() 的身份都必然位于 allowFrom 中,所以发送者就是用户本人。

这条假设在代码最初编写时确实成立。但我加入的 allowBots 改动让它不再成立——现在,一个 bot 即使没有出现在 allowFrom 中,也可以通过 gate()。

这意味着某个 Agent 原本可能代替我批准 tool 权限。

要利用这个漏洞,攻击者需要知道五个字符的 request_id,而这个 ID 只会发送到 allowlist 中的 DM,因此这属于 defence in depth 层面的问题,还不算门户大开。修复只需要两行代码:显式检查 allowFrom,并彻底排除由 bot 发送的消息,与 button handler 一直以来的处理方式保持一致。

真正值得关注的是这个错误的形态。单独看这份 patch,它是正确的;但与其他代码结合后,它却是错误的,因为它让一百行之外某条注释中记录的假设失效了。任何 type checker 都无法捕获这种问题。最终发现它的唯一原因,是我在自以为已经完成工作之后,又重新读了一遍文件中的其余代码。

四个耗费了我数小时的问题

BOM 悄无声息地重置了所有 bot 的配置。在 Windows PowerShell 5.1 中,Set-Content -Encoding utf8 会写入 UTF-8 BOM。JSON.parse 遇到它时会抛出异常,而 server 对无法解析的 JSON 的处理方式,是将文件重命名为 access.json.corrupt-<epoch>,然后从默认配置重新开始——没有 allowlist,也没有频道注册信息。bot 会继续运行,只是不再接收任何消息。十分钟之内,四个 bot 全部中招了。

应该检查字节,而不是配置值,因为除了真正起决定作用的那个 JSON parser,其他 JSON parser 几乎都能容忍 BOM:

Get-ChildItem "$env:USERPROFILE\.claude\channels\*\access.json" |
  ForEach-Object { (Get-Content $_.FullName -AsByteStream -TotalCount 3) -join ' ' }

239 187 191 就是 BOM。应该使用 [System.IO.File]::WriteAllText 写入这些文件,因为它在所有 PowerShell 版本中都不会写入 BOM。

fork 出来的 plugin 不在 approved-channels allowlist 中。安装自己修改后的 fork 之后,出站消息仍然完全正常,但入站通知会在 session 看到之前就被丢弃。bot 看起来像是没有响应,而不是出了故障,配置中也看不出任何异常。这个问题的典型特征是:回复可以正常发出,却收不到任何消息。答案藏在 MCP logs 的一行日志中,同时还需要在启动参数里明确指定这个 fork。

Agent 永远不会被告知自己的 user ID。它能看到发送者的 ID,却永远看不到自己的。因此,它无法验证某次提及是否真的是针对自己——当我发送一条消息,同时提及四个 bot 并表示“你是 orchestrator”时,每个 bot 都只能猜测这句话指的是谁。结果三个 bot 都猜错了。其中一个甚至根据频道历史推断自己的身份,随后开始在消息末尾签上另一个 bot 的名字。

这个问题需要用文字解决,而不是代码:分配角色时,要用名称称呼 bot,并明确写出对应的 ID。只提供 ID 没有用,因为接收者无法自行核对。

无论从哪个方向看,沉默的含义都很模糊。Agent 正忙和消息根本没有送达,看起来完全一样。Agent 忽略了你的报告,与它已经执行操作却保持沉默,看起来也完全一样。我在一周之内同时遇到了这两种故障——一条从未送达的消息被误解成拒绝,以及一个 orchestrator 在收到修正后悄悄执行了操作却没有确认。从发送者的角度看,后者与被忽略没有任何区别。

如果你也要构建这样的系统,就应该规定:任何会改变共享状态的操作都必须进行确认。它只需要多发送一条消息,却能消除一整类混乱。

如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。

Original source

本文由 AI 翻译整理,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
用 NumPy 实现 Transformer 自注意机制可视化
下一篇
MCP 服务器高工具数优化:渐进式披露架构

资讯速览

dev.to · AI · 2026-08-04 05:10

AI 评分 8.0 / 10

相关标签

#Claude Code#Agent#Discord

继续阅读

  • Prompt Injection 的根本症结:授权架构缺陷
  • 用 NumPy 实现 Transformer 自注意机制可视化
  • MCP 服务器高工具数优化:渐进式披露架构
  • AI代码审查工具陷阱:无状态导致重复评论
  • CLAUDE.md指南:如何识别真正值得的指令