news 2026/9/12 3:17:51

Zulip Outgoing Webhook 负载格式全解析:原生 Zulip 格式与 Slack 兼容格式的字段、转换规则与源码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Zulip Outgoing Webhook 负载格式全解析:原生 Zulip 格式与 Slack 兼容格式的字段、转换规则与源码实现

Zulip Outgoing Webhook 负载格式全解析:原生 Zulip 格式与 Slack 兼容格式的字段、转换规则与源码实现

【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip

Zulip 的 outgoing webhook 允许在特定消息被发送时,将消息内容以 HTTP 请求的形式推送给外部服务(bot 服务器或第三方系统),是实现聊天机器人与外部集成的核心机制。本篇以仓库文档 api_docs/outgoing-webhook-payload.md 为主线,完整讲解 Zulip 原生 JSON 负载与 Slack 兼容表单负载的字段含义、Zulip 服务器到 Slack 格式的字段映射关系、响应约定,并结合 zerver/lib/outgoing_webhook.py 与 zerver/tests/test_outgoing_webhook_interfaces.py 的源码与测试,帮助你精确理解收到的 payload 结构,并正确编写可被 Zulip 识别的响应。

Outgoing webhook 的两种负载格式总览

Zulip 的 outgoing webhook 支持两种负载格式:

  1. 原生 Zulip 格式(Zulip format):服务器以 JSON 形式 POST 一个包含datamessagetokentriggerbot_emailbot_full_name字段的对象,适合为 Zulip 从零编写的集成。
  2. Slack 兼容格式(Slack-compatible format):服务器把 Zulip 消息翻译成 Slack outgoing webhook API 的形式参数(application/x-www-form-urlencoded 风格),有助于将已有的 Slack 集成直接移植到 Zulip,也能让许多已经支持 Slack outgoing webhook 的第三方系统开箱即用。

两种格式由 bot 在创建/编辑时选择的 interface 类型决定。从 zerver/models/bots.py 可以看到两种 interface 的标识符:GENERIC_INTERFACE = "GenericService"SLACK_INTERFACE = "SlackOutgoingWebhookService",它们分别对应 zerver/lib/outgoing_webhook.py 中的GenericOutgoingWebhookServiceSlackOutgoingWebhookService两个实现类。接口分发逻辑位于get_service_interface_class(zerver/lib/outgoing_webhook.py):未知的 interface 名会回退到通用的 Zulip 原生格式。

原生 Zulip 格式(Zulip format)

服务器 POST 的请求负载结构

当消息触发 outgoing webhook 时,Zulip 服务器会向 bot 配置的 URL 发送一个 JSON 请求。该负载的完整字段定义记录在 OpenAPI 规范 zerver/openapi/zulip.yaml 的/zulip-outgoing-webhook端点中,并作为文档页面中{generate_code_example|/zulip-outgoing-webhook:post|fixture}{generate_return_values_table|zulip.yaml|/zulip-outgoing-webhook:post}两个占位符的实际渲染来源。字段说明如下:

字段类型说明
bot_emailstringbot 用户的邮箱
bot_full_namestringbot 用户的完整显示名
datastring消息内容,使用原始的 Zulip-flavored Markdown(未渲染为 HTML)
triggerstring触发本次 outgoing webhook 通知的消息特征,可能的值包括direct_messagemention变更:Zulip 8.0(feature level 201)起,触发器private_message被重命名为direct_message
tokenstring一串字母数字字符,用于认证 webhook 请求(每个 bot 用户使用固定的 token)。创建 bot 时下载的zuliprc文件中可以找到该 token
messageobject触发消息的详细信息字典,格式与GET /messages接口返回的消息一致(MessagesBaseschema),并额外包含rendered_content(消息渲染后的 HTML 内容)

message子对象中常见的字段包括:idsender_idsender_emailsender_full_namesender_realm_strcontent(原始 Markdown)、rendered_content(渲染 HTML)、content_typedisplay_recipient(频道名或私信参与者)、stream_idtype"stream""private")、timestampclientavatar_urlreactionssubmessagestopic_linksrecipient_idis_me_message等。

OpenAPI 规范中给出的完整 JSON 负载示例如下:

{ "data": "@**Outgoing webhook test** Zulip is the world’s most productive group chat!", "trigger": "mention", "token": "xvOzfurIutdRRVLzpXrIIHXJvNfaJLJ0", "message": { "subject": "Verona2", "sender_email": "iago@zulip.com", "timestamp": 1527876931, "client": "website", "submessages": [], "recipient_id": 20, "topic_links": [], "sender_full_name": "Iago", "avatar_url": "https://secure.gravatar.com/avatar/1f4f1575bf002ae562fea8fc4b861b09?d=identicon&version=1", "rendered_content": "<p><span class=\"user-mention\">request_data = { "data": event["command"], "message": message_dict, "bot_email": self.user_profile.email, "bot_full_name": self.user_profile.full_name, "token": self.token, "trigger": event["trigger"], }

然后通过带超时与自定义 User-Agent 的会话以self.session.post(base_url, json=request_data)发送。其中event["trigger"]的取值由消息类型决定(详见下文"触发器的判定"一节)。

测试 zerver/tests/test_outgoing_webhook_interfaces.py 中的test_make_request会构造一条内容为@**test**的频道消息,断言请求负载与预期一致,并调用validate_against_openapi_schema校验其符合/zulip-outgoing-webhook的 OpenAPI schema,同时验证负载中的bot_full_namedatatoken等字段,以及wide_message_dict未被意外修改。

触发器的判定

触发器trigger并非随意取值,而是由服务器按消息场景计算得出。核心逻辑位于 zerver/actions/message_send.py 的get_message_triggered_bot_events

  • 为避免死循环,机器人(sender.is_bot)发送的消息不会生成消息触发的 bot 事件;
  • 频道消息中,如果 bot 出现在实际被提及的用户集合(mentioned_user_ids)中,则触发器为"mention"
  • 私信(单人或群组私信)中,如果 bot 是消息的实际接收者(active_user_ids),则触发器为NotificationTriggers.DIRECT_MESSAGE,即"direct_message"
  • 出现在代码块等非真实提及场景中的 bot 会被过滤掉(注释中特别强调message_triggered_bot_tuples可能包含并未真正被提及的 bot)。

NotificationTriggers枚举定义在 zerver/models/scheduled_jobs.py,除了DIRECT_MESSAGE = "direct_message"外还包含MENTION = "mentioned"TOPIC_WILDCARD_MENTIONSTREAM_WILDCARD_MENTION等值——注意负载中实际使用的频道提及触发器字符串是"mention"(见message_send.py第 638 行),而 OpenAPI 文档同时声明可能值为direct_messagemention

服务器如何消费你的响应

process_success_response(zerver/lib/outgoing_webhook.py)解析 bot 返回的 JSON 响应:

  • 响应必须是合法 JSON;否则抛出Invalid JSON in response错误;
  • 为兼容zulip_botserver2021-05 之前版本返回的json.dumps(""),空的 JSON 字符串会被当作"无需回复"处理;
  • 响应若不是 JSON 对象(dict),视为非法格式;
  • 随后调用对应 service 的process_success提取回复内容。

GenericOutgoingWebhookService.process_success(zerver/lib/outgoing_webhook.py)支持三种响应形式:

  1. {"response_not_required": true}—— 无需回复(不发送任何消息);
  2. {"response_string": "..."}—— 已废弃的回复字段,作为content使用;
  3. {"content": "...", "widget_content": {...}}—— 标准回复,content为 Markdown 文本,widget_content可选,用于发送交互式小组件(会以 JSON 字符串形式传给send_response_message)。

以上行为均有测试覆盖:test_process_success(zerver/tests/test_outgoing_webhook_interfaces.py)验证了response_not_requiredresponse_stringcontent+widget_content与空响应四种情形;test_process_success_response(同文件第 39-67 行)则验证了合法 JSON、非法 JSON 两种路径。

最终,回复消息通过send_response_message(zerver/lib/outgoing_webhook.py)以 bot 的身份发送回原频道/私信并沿用原主题(topic),使用OutgoingWebhookResponse作为 client。

错误处理与失败通知

do_rest_call(zerver/lib/outgoing_webhook.py)统一处理请求执行与异常:

  • 请求默认超时时间为settings.OUTGOING_WEBHOOK_TIMEOUT_SECONDS秒;超时或连接错误(ConnectionErrorChunkedEncodingError)会通过request_retry进入outgoing_webhooks队列重试,并在最终失败后通知 bot 所有者"Bot is unavailable";
  • 2xx 状态码进入process_success_response解析;非 2xx 状态码(如 407 表示配置的 URL 是私有或受限网络)会向 bot 所有者发送包含状态码与响应内容的通知;
  • 其他RequestException异常会记录日志、向频道回发失败消息并通知 bot 所有者。

notify_bot_owner(zerver/lib/outgoing_webhook.py)会向 bot 所有者发送私信,内容包括触发消息链接、异常类型、失败原因、状态码及原始响应内容(以repr形式包裹在代码块中)。

Slack 兼容格式(Slack-compatible format)

设计动机

该格式兼容 Slack 的 outgoing webhook API(即 Slack 文档中的 legacy custom integration "post data" 参数集合)。它的价值在于:已有 Slack 集成的移植几乎可以零改动运行在 Zulip 上,且大量第三方系统已经支持 Slack outgoing webhook,因此可以直接对接 Zulip。

字段转换映射表

下面是文档中给出的完整映射表,说明 Zulip 服务器如何把一条 Zulip 消息翻译成 Slack 兼容的 webhook 格式:

名称说明
token一串字母数字字符,可用于认证 webhook 请求(每个 bot 用户使用固定的 token)
team_idZulip 组织(realm)的 ID,前缀 "T"
team_domainZulip 组织的主机名(hostname)
channel_id频道 ID,前缀 "C"
channel_name频道名称
thread_ts消息发送时的时间戳
timestamp消息发送时的时间戳
user_id消息发送者的用户 ID,前缀 "U"
user_name发送者的全名
text消息内容(Markdown 格式)
trigger_word触发方式
service_idbot 用户的 ID

源码级字段映射实现

SlackOutgoingWebhookService.make_request(zerver/lib/outgoing_webhook.py)是上述映射的实际实现。构造的请求数据是一个元组列表(list of tuples),而非 JSON:

request_data = [ ("token", self.token), ("team_id", f"T{realm.id}"), ("team_domain", realm.host), ("channel_id", f"C{event['message']['stream_id']}"), ("channel_name", event["message"]["display_recipient"]), ("thread_ts", event["message"]["timestamp"]), ("timestamp", event["message"]["timestamp"]), ("user_id", f"U{event['message']['sender_id']}"), ("user_name", event["message"]["sender_full_name"]), ("text", event["command"]), ("trigger_word", event["trigger"]), ("service_id", event["user_profile_id"]), ] return self.session.post(base_url, data=request_data)

要点:

  • 使用data=request_data(而非json=)发送,因此负载以表单参数形式(application/x-www-form-urlencoded)提交;
  • team_id为 realm ID 加"T"前缀;channel_id为 stream ID 加"C"前缀;user_id为 sender ID 加"U"前缀;
  • team_domain是 realm 的主机名(如zulip.example.com),并非 Slack 式的短域名;
  • thread_tstimestamp都取消息发送时间戳;
  • text为原始命令内容event["command"](未渲染 Markdown);
  • trigger_wordevent["trigger"],即mentiondirect_message
  • service_idevent["user_profile_id"](bot 用户的 ID)。

源码注释中(第 121-134 行)保留了 Slack 官方文档对 legacy outgoing webhooks POST 数据的参考示例,字段顺序与之保持一致。

文档给出的完整负载示例(元组列表形式):

[('token', 'v9fpCdldZIej2bco3uoUvGp06PowKFOf'), ('team_id', 'T1512'), ('team_domain', 'zulip.example.com'), ('channel_id', 'C123'), ('channel_name', 'integrations'), ('thread_ts', 1532078950), ('timestamp', 1532078950), ('user_id', 'U21'), ('user_name', 'Full Name'), ('text', '@**test**'), ('trigger_word', 'mention'), ('service_id', 27)]

行为限制:不支持私信

SlackOutgoingWebhookService.make_request开头有一段关键检查(zerver/lib/outgoing_webhook.py):如果触发消息是私信(event["message"]["type"] == "private"),Slack 兼容格式不支持,服务器不会发送任何请求,而是调用fail_with_message在频道里回发"Failure! Slack outgoing webhooks don't support direct messages."

测试test_make_request_private_message(zerver/tests/test_outgoing_webhook_interfaces.py)验证了该行为:session.post不会被调用,返回None,且fail_with_message被触发。

字段映射的测试验证

test_make_request_stream_message(zerver/tests/test_outgoing_webhook_interfaces.py)对频道消息事件逐项断言了 12 个字段的取值,例如token == "abcdef"team_id == "T2"team_domain == "zulip.testserver"channel_id == "C123"channel_name == "integrations"user_id == "U21"user_name == "Sample User"text == "@**test**"trigger_word == "mention"service_id == 12,与文档示例和源码实现完全一致。

Slack 兼容格式的响应处理

SlackOutgoingWebhookService.process_success(zerver/lib/outgoing_webhook.py)只认一个字段:若响应 JSON 中存在text,则将其作为回复内容发送回 Zulip;否则不回复。也就是说,返回{"text": "..."}即可让 bot 在 Zulip 中回消息。对应测试test_process_success(zerver/tests/test_outgoing_webhook_interfaces.py)。

响应约定与常见错误排查

综合两种格式,bot 服务器的响应遵循以下约定:

  • 成功请求:如果返回了数据,Zulip 会解析其中的回复内容(Zulip 原生格式解析content/response_string,Slack 兼容格式解析text);如果没有返回数据(或返回response_not_required: true/ 空 JSON 字符串),则返回空白响应,不发送任何消息。
  • 失败请求:Zulip 会把服务器返回的失败原因或异常消息反馈到频道/通知 bot 所有者。

常见排查点:

  1. 响应必须是合法 JSON 且为对象,否则会触发Invalid JSON in response/Invalid response format错误;
  2. 请求超时或连接错误会触发队列重试,可检查OUTGOING_WEBHOOK_TIMEOUT_SECONDS配置与 bot 服务器进程状态;
  3. 非 2xx 状态码会触发失败消息与所有者通知,其中 407 有专门提示("URL configured for the webhook is for a private or disallowed network");
  4. Slack 兼容格式不适用于私信触发,私信场景请改用 Zulip 原生格式;
  5. bot 发送的消息不会再次触发 outgoing webhook(防止死循环)。

结语

理解 outgoing webhook 的负载格式是正确编写 Zulip 机器人后端的第一步:Zulip 原生格式提供结构化的 JSON 消息数据(含datatriggertoken与完整的message对象),适合深度定制集成;Slack 兼容格式则通过元组列表形式的表单参数复刻 Slack legacy outgoing webhook 协议,便于移植既有集成。两者的字段构造、触发判定、响应解析与失败通知均可在 zerver/lib/outgoing_webhook.py 与 zerver/tests/test_outgoing_webhook_interfaces.py 中找到对应的源码与测试依据,字段级规范以 zerver/openapi/zulip.yaml 中的/zulip-outgoing-webhook定义为最终权威。按照本文的字段映射与响应约定,你可以在任意语言/框架中实现自己的 bot 服务端点。

【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 3:16:29

WorkBuddy开放生态:AI Agent真正走进企业业务系统的关键拼图

1. 先说结论&#xff1a;WorkBuddy开放的不是API&#xff0c;是三年前就该补的那块拼图WorkBuddy开放生态的消息出来以后&#xff0c;圈子里讨论的方向多数集中在"它又接入了多少个模型""技能市场里有多少现成技能"这些表面指标上。我个人的判断不太一样&a…

作者头像 李华
网站建设 2026/9/12 3:15:27

3 步完整导出微信聊天记录:WeChatMsg 快速上手指南

3 步完整导出微信聊天记录&#xff1a;WeChatMsg 快速上手指南 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/WeChatMs…

作者头像 李华
网站建设 2026/9/12 3:14:19

安全储蓄线计算与个人理财平衡策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华