我运营着一个小型论坛和其他三个网站,一直注意到同一个现象。一旦用户进入论坛,他们很乐意在聊天中交流,但很少有人会专门去论坛问一个简单的问题。他们要么在自己已经所在的地方提问,要么干脆不问。
因此,我开发了一个插件,将论坛的聊天功能嵌入到那些其他网站上。只需添加一个脚本标签,页面角落就会出现一个气泡。访客可以使用他们已有的论坛账号,通过论坛自身的登录界面进行登录,并在与论坛上相同的频道中聊天。他们发送的消息会像其他消息一样出现在论坛的聊天中,因为它们本质上就是论坛消息。
<script src="https://forum.example.com/chat-bridge/widget.js"
data-site-key="your-site-key" defer></script>
为什么它是一个插件而不是一个独立服务
起初,我设计了一个外部桥接器,通过 API 与 Discourse 通信。但后来我阅读了源码,这彻底改变了设计方案,其中的原因可能对任何考虑类似项目的人都有参考价值。
聊天插件只注册了一个细粒度的 API 作用域:create_message。除了发布消息之外,任何操作(如读取频道、获取历史记录、开启私信)都需要全局作用域密钥。为每个用户维护一个全局作用域密钥意味着要持有大量凭证。默认的速率限制是管理员桶每分钟 60 次请求,用户桶每分钟 20 次请求,而聊天客户端稍加使用就会耗尽这些配额。此外,聊天 Webhook 只携带四种消息事件,没有其他内容,因此表情回应、在线状态和已读状态根本无法转发。
当代码直接在 Discourse 内部运行时,这些问题都不复存在,因为它可以直接向 Guardian 发起查询。不需要密钥,没有速率限制上限,而且只有一个数据源,而不是一个可能与论坛数据不一致的缓存。
功能现状
支持频道、消息历史、发送消息、带有用户搜索功能的私信,以及在浏览器中录制的语音消息。对于阅读论坛自身聊天的成员来说,语音消息显示为标准的音频播放器,而不是下载链接,这需要仔细处理才能实现。
每个网站都有自己独立的强调色、角落位置和面板标题,因此三个网站可以看起来像三个不同的产品,而不是同一个小部件的三个副本。访客可以获得可关闭的通知声音、浅色或深色模式覆盖,以及静音对话的功能。静音操作会写入他们真实的 Discourse 成员资料,因此当他们回到论坛时,静音状态依然有效。
所有内容都在 Shadow DOM 中渲染。这运行在我无法控制的页面上,且任何一方的 CSS 都不应破坏另一方的样式。
不支持的功能及原因
不支持语音或视频通话。这是有意为之,超出了项目范围。
消息传递不是即时的。当面板打开时,消息大约需要三秒才能到达。这一点值得解释,因为 Discourse 确实会将聊天事件发布到 MessageBus,看起来应该可以直接工作。
其他域名的浏览器无法向 MessageBus 端点进行身份验证。其 CORS 策略允许四个请求头,但其中没有一个携带 Bearer Token,而通过查询参数进入 Discourse 身份验证的路径仅限于 RSS 和日历端点。唯一有效的请求头 X-Shared-Session-Key 会解析为 UserAuthToken,因此会认证携带它的任何请求,而不仅仅是 MessageBus 请求。将此交给嵌入页面会将营销网站上的跨站脚本(XSS)漏洞转变为完全的论坛账号接管。三秒延迟是更好的权衡。
仓库中记录了一条通往实时通信的安全路径,它使用一个 MessageBus 通道,其不可猜测的名称本身就是一种能力(capability),范围限定于单个小部件会话,而不是用户的账号。我尚未实现它。如果有人想实现,推理过程在 docs/decisions.md 中。
安装前需要理解的关键点
注册一个网站意味着授予该网站跨域访问你论坛的权限,并携带你成员的凭证。这不是副作用,而是其工作机制。
如果你注册的某个网站被入侵,攻击者如果能在该网站上执行 JavaScript,就可以冒充任何访问该网站的成员。不仅限于聊天,而是该成员在论坛上能做的任何事。
因此,只注册你控制的网站。注册合作伙伴或客户的网站意味着你要接受他们的安全性作为你自己的一部分。插件在管理页面输入源(origin)的字段旁边说明了这一点,而不是写在没人会打开的文档里。SECURITY.md 详细说明了插件如何限制影响范围,以及它故意不做什么。
安装
将其添加到你的容器定义中并重新构建一次:
hooks:
after_code:
- exec:
cd: $home/plugins
cmd:
- git clone --depth 1 https://github.com/capodieci/discourse-chat-bridge.git
env:
DISCOURSE_ENABLE_CORS: true
然后在管理后台的设置中启用 chat_bridge_enabled,其他所有内容都在 /chat-bridge/admin 这一页面上,你可以在那里添加网站,它会提供给你脚本标签。
两个能帮人省一下午时间的注意事项。语音消息需要在 authorized_extensions 中包含音频格式,而默认情况下它不包含任何格式,因此管理页面会告诉你具体缺少哪些。另外,chat_allowed_groups 默认为信任级别 1,因此全新账号在获得相应权限之前无法聊天,小部件会对此进行解释,而不是静默失败。
兼容性
已在 Discourse 2026.9.0 和 2026.8.0 上进行测试,并在一论坛的生产环境中使用。
这依赖于非公共 API 的聊天服务对象,因此 Discourse 的版本更新可能会移动它们。仓库包含一个只读的预检脚本,用于断言所有这些对象仍然存在,并在几秒钟内报告结果,而不是在第一个请求时才发现。升级后值得运行一下。
我的期望
希望有人能在一个不是我的论坛上安装它,并告诉我哪里坏了。目前所有验证都是针对一个 Discourse、在一台服务器上、由一个人完成的,这是它最薄弱的地方。
我也希望听到任何人的意见,看是否有人对 MessageBus 问题有比我最终选择的方案更好的答案。
