Wekan Webhook Data 完整指南:Outgoing Webhook 请求体结构与全部事件类型详解 Wekan Webhook Data 完整指南Outgoing Webhook 请求体结构与全部事件类型详解【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan导读本指南以 Wekan 官方文档 Webhook-data.md 为主体逐事件类型解析 outgoing webhook 激活时随 POST 请求发送的 JSON 请求体payload结构与字段含义。文中涵盖卡片Card、卡片内容Card content、看板Board、列表List、泳道Swimlane五大类共 27 种事件的完整 payload 示例并结合 server/notifications/outgoing.js 与 server/models/activities.js 等源码讲解 payload 的构造机制、环境变量控制、双向 webhook 响应处理与 SSRF 安全防护。读完本文你将能针对任意 Wekan 事件准确解析 webhook 请求、编写接收端程序并根据自身需求定制与保护 webhook 集成。Webhook 请求体Payload的总体结构当某个集成Integration被激活后每当看板上发生其订阅的活动ActivityWekan 会向集成配置的 URL 发起一次 HTTP POST 请求把相关信息放在请求体中发送出去。从源码 server/notifications/outgoing.js 可以看到outgoing webhook 的请求体由三部分构成{ text: ..., ...: ..., description: ... }text一段人类可读的、已经本地化翻译的事件描述文本末尾通常附带指向该卡片或看板的链接http://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}。它的构造逻辑为text ${params.user} ${descriptionText}\n${params.url}见 outgoing.js。description活动的机器可读标识符是字符串形式的act-前缀动作码例如act-createCard、act-moveCard。下游程序可以通过该字段精确判断事件类型。其余字段从活动上下文抽取的结构化属性如cardId、listId、boardId、user、card等具体包含哪些字段取决于webhooksAtbts白名单见下文“payload 字段由环境变量控制”一节。description字段同时还是集成的“订阅过滤器”只有集成activities列表中包含该 description或包含all时该集成才会收到这次 webhook 推送。这一匹配逻辑位于 server/models/activities.jsconst integrations await ReactiveCache.getIntegrations({ boardId: { $in: integrationBoardIds }, enabled: true, activities: { $in: [description, all] }, });也就是说集成既可订阅all全部活动也可订阅一个或多个具体的act-*动作码。一、Cards卡片事件创建卡片Creation看板上创建新卡片时触发payload 如下{ text: {{wekan-username}} created card \{{card-title}}\ to list \{{list-name}}\ at swimlane \{{swimlane-name}}\ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, listId: {{list-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, swimlaneId: {{swimlane-id}}, description: act-createCard }移动卡片Move卡片在列表之间移动时触发{ text: {{wekan-username}} moved card \{{card-title}}\ at board \{{board-name}}\ from list \{{old-list-name}}\ at swimlane \{{swimlane-name}}\ to list \{{new-list-name}}\ at swimlane \{{swimlane-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, listId: {{new-list-id}}, oldListId: {{old-list-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, swimlaneId: {{swimlane-id}}, description: act-moveCard }注意移动事件额外携带oldListId来源列表 ID和listId目标列表 ID可用于跟踪卡片的位置变更轨迹。卡片归档Archival卡片被移入归档时触发{ text: {{wekan-username}} Card \{{card-title}}\ at list \{{list-name}}\ at swimlane \{{swimlane-name}}\ at board \{{board-name}}\ moved to Archive\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, listId: {{list-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, swimlaneId: {{swimlane-id}}, description: act-archivedCard }卡片恢复Restored卡片从归档恢复时触发{ text: {{wekan-username}} restored card \{{card-title}}\ to list \{{list-name}}\ at swimlane \{{swimlane-name}}\ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, listId: {{list-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, swimlaneId: {{swimlane-id}}, description: act-restoredCard }二、Card content卡片内容事件卡片内容发生变化时触发的事件集合订阅后可实现诸如评论同步、附件监控、清单统计等自动化能力。评论创建Comment creation{ text: {{wekan-username}} commented on card \{{card-title}}\: \{{comment}}\ at list __list__ at swimlane __swimlane__ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, boardId: {{board-id}}, comment: {{comment}}, user: {{wekan-username}}, card: {{card-title}}, commentId: {{comment-id}}, description: act-addComment }评论编辑Comment edit{ text: {{wekan-username}} commented on card \{{card-title}}\: \{{comment}}\ at list __list__ at swimlane __swimlane__ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, listId: {{list-id}}, boardId: {{board-id}}, comment: {{comment}}, user: {{wekan-username}}, card: {{card-title}}, commentId: {{comment-id}}, swimlaneId: {{swimlane-id}}, description: act-editComment }添加标签AddLabel{ text: {{wekan-username}} Added label __label__ to card \{{card-title}}\ at list \{{list-name}}\ at swimlane \{{swimlane-name}}\ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, listId: {{list-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, swimlaneId: {{swimlane-id}}, description: act-addedLabel }添加成员Join member{ text: {{wekan-username}} added member {{wekan-username}} to card \{{card-title}}\ at list \{{list-name}}\ at swimlane \{{swimlane-name}}\ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, listId: {{list-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, swimlaneId: {{swimlane-id}}, description: act-joinMember }设置自定义字段Set custom field{ text: {{wekan-username}} act-setCustomField\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, description: act-setCustomField }添加附件Add attachment{ text: {{wekan-username}} added attachment {{attachment-id}} to card \{{card-title}}\ at list __list__ at swimlane __swimlane__ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, description: act-addAttachment }删除附件Delete attachment{ text: {{wekan-username}} deleted attachment __attachment__ at card \{{card-title}}\ at list __list__ at swimlane __swimlane__ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, description: act-deleteAttachment }添加清单Add checklist{ text: {{wekan-username}} added checklist \{{checklist-name}}\ to card \{{card-title}}\ at list __list__ at swimlane __swimlane__ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, description: act-addChecklist }移除清单Remove checklist{ text: {{wekan-username}} removed checklist \{{checklist-name}}\ from card \{{card-title}}\ at list __list__ at swimlane __swimlane__ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, description: act-removeChecklist }清单未完成Uncomplete checklist{ text: {{wekan-username}} uncompleted checklist \{{checklist-name}}\ at card \{{card-title}}\ at list __list__ at swimlane __swimlane__ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, description: act-uncompleteChecklist }添加清单项Add checklist item{ text: {{wekan-username}} added checklist item {{checklistitem-name}} to checklist \{{checklist-name}}\ at card \{{card-title}}\ at list __list__ at swimlane __swimlane__ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, description: act-addChecklistItem }清单项勾选Checked item{ text: {{wekan-username}} checked {{checklist-name}} of checklist \{{checklist-name}}\ at card \{{card-title}}\ at list __list__ at swimlane __swimlane__ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, description: act-checkedItem }移除清单项Removed checklist item{ text: {{wekan-username}} act-removedChecklistItem\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}/{{card-id}}, cardId: {{card-id}}, boardId: {{board-id}}, user: {{wekan-username}}, card: {{card-title}}, description: act-removedChecklistItem }三、Board看板事件创建自定义字段Create custom field在看板上创建自定义字段时触发{ text: {{wekan-username}} created custom field {{customfield-name}} to card __card__ at list __list__ at swimlane __swimlane__ at board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}, boardId: {{board-id}}, user: {{wekan-username}}, description: act-createCustomField }注意该事件的text中链接指向看板页面/b/{{board-id}}/{{board-name}}而非具体卡片。四、Lists列表事件创建列表Create list{ text: {{wekan-username}} added list \{{list-name}}\ to board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}, listId: {{list-id}}, boardId: {{board-id}}, user: {{wekan-username}}, description: act-createList }列表归档Archived list{ text: {{wekan-username}} List \{{list-name}}\ at swimlane __swimlane__ at board \{{board-name}}\ moved to Archive\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}, listId: {{list-id}}, boardId: {{board-id}}, user: {{wekan-username}}, description: act-archivedList }删除列表Remove list{ text: {{wekan-username}} act-removeList\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}, listId: {{list-id}}, boardId: {{board-id}}, user: {{wekan-username}}, description: act-removeList }五、Swimlane泳道事件创建泳道Create swimlane{ text: {{wekan-username}} created swimlane \{{swimlane-name}}\ to board \{{board-name}}\\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}, boardId: {{board-id}}, user: {{wekan-username}}, swimlaneId: {{swimlane-id}}, description: act-createSwimlane }泳道归档Archived swimlane{ text: {{wekan-username}} Swimlane \{{swimlane-name}}\ at board \{{board-name}}\ moved to Archive\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}, boardId: {{board-id}}, user: {{wekan-username}}, swimlaneId: {{swimlane-id}}, description: act-archivedSwimlane }删除泳道Remove swimlane{ text: {{wekan-username}} act-removeSwimlane\nhttp://{{wekan-host}}/b/{{board-id}}/{{board-name}}, boardId: {{board-id}}, user: {{wekan-username}}, swimlaneId: {{swimlane-id}}, description: act-removeSwimlane }事件类型速查表为了便于下游程序按description分发事件将上述全部事件汇总如下分类事件description 动作码特有字段Cards创建卡片act-createCardcardId、listId、swimlaneIdCards移动卡片act-moveCardcardId、listId、oldListId、swimlaneIdCards卡片归档act-archivedCardcardId、listId、swimlaneIdCards卡片恢复act-restoredCardcardId、listId、swimlaneIdCard content评论创建act-addCommentcardId、comment、commentIdCard content评论编辑act-editCommentcardId、listId、comment、commentId、swimlaneIdCard content添加标签act-addedLabelcardId、listId、swimlaneIdCard content添加成员act-joinMembercardId、listId、swimlaneIdCard content设置自定义字段act-setCustomFieldcardIdCard content添加附件act-addAttachmentcardIdCard content删除附件act-deleteAttachmentcardIdCard content添加清单act-addChecklistcardIdCard content移除清单act-removeChecklistcardIdCard content清单未完成act-uncompleteChecklistcardIdCard content添加清单项act-addChecklistItemcardIdCard content清单项勾选act-checkedItemcardIdCard content移除清单项act-removedChecklistItemcardIdBoard创建自定义字段act-createCustomField无Lists创建列表act-createListlistIdLists列表归档act-archivedListlistIdLists删除列表act-removeListlistIdSwimlane创建泳道act-createSwimlaneswimlaneIdSwimlane泳道归档act-archivedSwimlaneswimlaneIdSwimlane删除泳道act-removeSwimlaneswimlaneId源码级原理payload 是如何构造出来的1. 活动钩子触发 webhook所有 webhook 推送的起点是活动集合的after.insert钩子实现在 server/models/activities.js。每当一条活动记录被写入该钩子会按boardId当前看板 ID以及全局集成 ID_global见 models/integrations.js 中的GLOBAL_WEBHOOK_ID查询启用的集成用activities: { $in: [description, all] }过滤出订阅了该活动或订阅了全部的集成对每个匹配的集成调用Meteor.call(outgoingWebhooks, integration, description, params)。关键设计是错误隔离与火并忘fire-and-forget每个 webhook 都通过safeDeliver()包裹即使某个接收端缓慢、不可达或返回错误也不会中断活动写入本身或用户正在执行的原始操作例如添加/移除卡片成员这对应历史缺陷 #1402 的修复。2.outgoingWebhooks方法组装请求体Meteor 方法outgoingWebhooks负责把活动参数组装成最终 payload核心步骤为参数加引号card、list、oldList、board、oldBoard、comment、checklist、swimlane、oldSwimlane、labelId、label、attachment、attachmentId等文本型参数在放入text前会被双引号包裹outgoing.js。文本本地化description即act-*动作码作为 i18n key通过TAPi18n.__(description, quoteParams, user.getLanguage())翻译为用户语言的自然语句outgoing.js。方法会先加载该用户的语言包对应缺陷 #5875避免回退到英文。拼接text最终文本为${params.user} ${descriptionText}\n${params.url}其中params.url即指向卡片或看板的链接。组装结构化字段把白名单中存在的参数逐个复制到 payload 顶层最后附加description动作码。3. payload 字段由WEBHOOKS_ATTRIBUTES环境变量控制从 outgoing.js 可以看到payload 中携带哪些字段由webhooksAtbts白名单决定const webhooksAtbts (process.env.WEBHOOKS_ATTRIBUTES process.env.WEBHOOKS_ATTRIBUTES.split(,)) || [ cardId, listId, oldListId, boardId, comment, user, username, card, commentId, swimlaneId, customField, customFieldValue, labelId, label, attachmentId, ];默认情况下payload 包含上述全部 15 个字段取活动上下文中有值的部分其中user是显示名username是登录名对应缺陷 #3113。如果部署时设置了WEBHOOKS_ATTRIBUTES环境变量逗号分隔的字段名列表则会整体覆盖默认白名单只发送你明确列出的字段。这为隐私敏感或带宽受限的集成场景提供了裁剪手段。4. 请求头与请求方式请求方法固定为POSTContent-Type: application/jsonoutgoing.js。若集成为其配置了token请求会携带X-Wekan-Token: token请求头接收端可据此校验请求确实来自 Wekan。5. 如何关闭某个事件的推送在 outgoing.js 中有一处重要约定如果某个活动的翻译文本恰好为-则该 webhook 直接跳过、不发送。这意味着如果你订阅了all但希望屏蔽个别事件可以通过将对应act-*动作码的翻译值设为-来禁用该 hook。同理若翻译文本为空请求体text长度为零时也不会发送outgoing.js。双向 WebhookBidirectional与响应处理除默认的outgoing-webhooks单向外Wekan 还支持bidirectional-webhooks双向类型定义于 models/integrations.js 的Integrations.ConstIntegrations.Const { GLOBAL_WEBHOOK_ID: _global, ONEWAY: outgoing-webhooks, TWOWAY: bidirectional-webhooks, get WEBHOOK_TYPES() { return [this.ONEWAY, this.TWOWAY]; }, };双向 webhook 在请求体上有一个关键差异outgoing.js单向模式发送组装好的value含text而双向模式发送{ description, ...clonedParams }即原始动作码与活动参数把文本渲染工作交给接收端。接收端返回2xx且响应体为合法 JSON 时Wekan 会尝试处理响应数据入口为 outgoing.js 的responseFunc仅允许双向集成写回单向集成的响应会被忽略outgoing.js。防止跨看板注入响应中的boardId必须与集成所属看板一致outgoing.js。只更新已存在评论响应须同时包含commentId、cardId、boardId与comment文本且目标评论必须真实存在Webhook 响应不能新建评论outgoing.js。回环锁Lock机制为避免 echo 覆盖用户正在输入的内容方法内置了以commentId为键的锁Lock见 outgoing.js区分 echo 回写与正常回写并在 500ms/1000ms 后自动释放。这也是该文件顶部注释“只有双向 webhook 才能通过响应覆盖评论”的落地保障。安全机制SSRF 防护、Token 与集成校验webhook 本质是让服务端向任意 URL 发起请求因此 Wekan 在源码中为它叠加了多层防护Schema 层快速校验models/integrations.js 中url字段的custom()校验器只允许http:/https:协议并同步拦截私网、回环、链路本地等明显违禁的目标如127.x、10.x、192.168.x、169.254.x、::1、fe80:、fc00:、localhost、.local等。但它只是“第一道非权威防线”正如注释强调的它无法解析 DNS不能作为 SSRF 的唯一边界——这曾是漏洞GHSA-66m2-4wfr-c45pDnsBleed的根因。REST 写路径权威校验server/models/integrations.js 与 PUT 更新路径 在保存/更新集成 URL 时调用validateAttachmentUrl()做 DNS 感知校验非法 URL 返回400与invalid-webhook-url错误。投递期防 DNS 重绑定实际发送使用fetchSafe()见 server/lib/ssrfGuard.js只解析一次 DNS、把连接固定到已校验的 IP 上、并禁止重定向从而彻底阻断 DNS-rebinding SSRF 攻击outgoing.js。调用方与集成的双向校验outgoingWebhooks不信任调用者传入的integration对象而是按url boardId回查数据库确认真实集成存在并校验调用者是该看板成员否则直接返回outgoing.js。这防止任何已登录用户驱动其无权访问的看板上的 webhook以及借双向响应路径越权改写评论。可选的X-Wekan-Token为集成配置 token 后每次请求都会携带该头供接收端做请求来源认证。集成管理 REST API 概览webhook 集成Integration本身可通过 REST API 管理端点在 server/models/integrations.js方法路径说明GET/api/boards/:boardId/integrations列出看板上的集成不返回 tokenGET/api/boards/:boardId/integrations/:intId查询单个集成POST/api/boards/:boardId/integrations创建集成需要看板管理员权限body 含urlPUT/api/boards/:boardId/integrations/:intId更新enabled、title、url、token、activitiesPOST/api/boards/:boardId/integrations/:intId/activities向集成的订阅列表追加活动DELETE/api/boards/:boardId/integrations/:intId/activities从订阅列表移除活动DELETE/api/boards/:boardId/integrations/:intId删除集成其中activities字段即上文所述的订阅过滤器填入all订阅全部事件或填入一个/多个act-*动作码实现精细化订阅Integrations集合的 schema 与默认值enabled: true、type: outgoing-webhooks、activities: [all]定义于 models/integrations.js。小结Wekan 的 outgoing webhook 以“活动驱动”为核心任何act-*事件被写入活动集合后都会按集成的订阅规则被转发为 JSON POST 请求。接收端应优先依据description字段分发事件依据cardId/listId/boardId/swimlaneId等结构化字段定位资源依据user/username/comment等字段提取上下文并可通过text直接向用户展示可读描述。结合WEBHOOKS_ATTRIBUTES裁剪字段、activities过滤事件、X-Wekan-Token认证与源码内置的多层 SSRF 防护你可以安全地将 Wekan 看板事件流无缝接入自有的工单系统、消息机器人或数据分析管道。【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考