617 lines
32 KiB
Markdown
617 lines
32 KiB
Markdown
# M8 通知与Webhook — Chatwoot 功能梳理文档
|
||
|
||
> 参照仓库:chatwoot-reference
|
||
> 产出日期:2026-05-22
|
||
> 版本基准:Chatwoot v3.x
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [通知模型与类型](#1-通知模型与类型)
|
||
2. [通知设置(NotificationSetting)](#2-通知设置notificationsetting)
|
||
3. [通知订阅(NotificationSubscription / Push订阅)](#3-通知订阅notificationsubscription--push订阅)
|
||
4. [通知生成与投递流程](#4-通知生成与投递流程)
|
||
5. [NotificationListener — 事件驱动的通知创建](#5-notificationlistener--事件驱动的通知创建)
|
||
6. [ActionCable实时推送](#6-actioncable实时推送)
|
||
7. [Push通知(浏览器Push + FCM)](#7-push通知浏览器push--fcm)
|
||
8. [Email通知](#8-email通知)
|
||
9. [通知Snooze与去重](#9-通知snooze与去重)
|
||
10. [Webhook(Account Webhook)](#10-webhookaccount-webhook)
|
||
11. [IntegrationHook(集成Hook)](#11-integrationhook集成hook)
|
||
12. [WebhookListener — 事件驱动的Webhook投递](#12-webhooklistener--事件驱动的webhook投递)
|
||
13. [HookListener — 集成Hook执行](#13-hooklistener--集成hook执行)
|
||
14. [InstallationWebhook(平台级事件推送)](#14-installationwebhook平台级事件推送)
|
||
15. [事件分发体系(Dispatcher架构)](#15-事件分发体系dispatcher架构)
|
||
|
||
---
|
||
|
||
## 1. 通知模型与类型
|
||
|
||
### 功能描述
|
||
Notification 是 Chatwoot 应用内通知的核心数据模型,记录针对特定用户的通知条目。每个通知绑定到一个 primary_actor(目前仅 Conversation)和一个可选的 secondary_actor(如 Message 或 User),通过 polymorphic 关联实现。通知类型通过 enum 定义,涵盖对话创建、分配、提及、新消息、SLA违规等场景。
|
||
|
||
### 用户操作流程
|
||
1. 坐席登录后在侧边通知面板看到未读通知列表
|
||
2. 点击某条通知可标记为已读,跳转至对应对话
|
||
3. 可批量标记全部已读、删除单条通知、删除全部/已读通知
|
||
4. 可对通知进行" snooze "(延迟提醒),到期后自动恢复为未读
|
||
5. 通知计数(unread_count + count)实时更新
|
||
|
||
### 涉及的API端点
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| GET | `/api/v1/accounts/{account_id}/notifications` | 获取通知列表(支持分页、过滤 read/snoozed) |
|
||
| PATCH | `/api/v1/accounts/{account_id}/notifications/{id}` | 标记单条通知已读 |
|
||
| POST | `/api/v1/accounts/{account_id}/notifications/{id}/unread` | 标记通知为未读 |
|
||
| POST | `/api/v1/accounts/{account_id}/notifications/{id}/snooze` | snooze 通知 |
|
||
| DELETE | `/api/v1/accounts/{account_id}/notifications/{id}` | 删除单条通知 |
|
||
| POST | `/api/v1/accounts/{account_id}/notifications/read_all` | 全部标记已读(可按 primary_actor 过滤) |
|
||
| POST | `/api/v1/accounts/{account_id}/notifications/destroy_all` | 删除全部/已读通知 |
|
||
| GET | `/api/v1/accounts/{account_id}/notifications/unread_count` | 获取未读通知数 |
|
||
|
||
### 涉及的数据模型 + 关键字段
|
||
- **Notification**(`notifications` 表):
|
||
- `id` (bigint, PK)
|
||
- `notification_type` (integer, not null) — enum 见下表
|
||
- `primary_actor_type` / `primary_actor_id` (string + bigint, polymorphic, not null)
|
||
- `secondary_actor_type` / `secondary_actor_id` (string + bigint, polymorphic, optional)
|
||
- `user_id` (bigint, not null, FK) — 通知目标用户
|
||
- `account_id` (bigint, not null, FK)
|
||
- `read_at` (datetime) — 已读时间
|
||
- `snoozed_until` (datetime) — snooze到期时间
|
||
- `last_activity_at` (datetime) — 最后活动时间
|
||
- `meta` (jsonb) — 附加元数据
|
||
- `created_at`, `updated_at`
|
||
- **NOTIFICATION_TYPES enum**:
|
||
|
||
| 类型 | 值 | 说明 |
|
||
|------|------|------|
|
||
| conversation_creation | 1 | 新对话创建 |
|
||
| conversation_assignment | 2 | 对话被分配给坐席 |
|
||
| assigned_conversation_new_message | 3 | 被分配的对话收到新消息 |
|
||
| conversation_mention | 4 | 对话中被 @提及 |
|
||
| participating_conversation_new_message | 5 | 参与中的对话收到新消息 |
|
||
| sla_missed_first_response | 6 | SLA首次响应违规 |
|
||
| sla_missed_next_response | 7 | SLA后续响应违规 |
|
||
| sla_missed_resolution | 8 | SLA解决时间违规 |
|
||
|
||
- PRIMARY_ACTORS 目前仅允许 `Conversation`
|
||
- 性能索引:`idx_notifications_performance (user_id, account_id, snoozed_until, read_at)`
|
||
- 唯一索引:`uniq_primary_actor_per_account_notifications`, `uniq_secondary_actor_per_account_notifications`
|
||
|
||
### 涉及的业务逻辑
|
||
- **NotificationBuilder**(`app/builders/notification_builder.rb`):
|
||
- 构建通知前检查:`user_subscribed_to_notification?`、blocked contact 过滤、`user_can_access_conversation?`(权限验证)
|
||
- secondary_actor 默认为 Current.user
|
||
- conversation_creation 类型需订阅确认才创建
|
||
- **NotificationFinder**(`app/finders/notification_finder.rb`):
|
||
- 分页查询:RESULTS_PER_PAGE = 15
|
||
- 支持过滤:snoozed / read 两种状态(通过 `includes` 参数)
|
||
- 提供 `unread_count` 和 `count`
|
||
- 默认按 `last_activity_at` 降序排列
|
||
- **Notification::MarkConversationReadService**:批量将某对话的未读通知标记为已读
|
||
- **Notification::DeleteNotificationJob**:异步批量删除全部或已读通知(type: :all / :read)
|
||
|
||
### 涉及的自动化/规则/事件
|
||
- `before_create :set_last_activity_at` — 创建时设置活动时间
|
||
- `after_create_commit :process_notification_delivery, :dispatch_create_event` — 创建后触发 Push/Email 投递 + ActionCable 事件
|
||
- `after_update_commit :dispatch_update_event` — 更新后触发 ActionCable 事件
|
||
- `after_destroy_commit :dispatch_destroy_event` — 删除后触发 ActionCable 事件(传序列化数据避免反序列化错误)
|
||
- `Notification::RemoveDuplicateNotificationJob` — 创建后去重(保留最新,删除同一 user+primary_actor 的旧通知)
|
||
|
||
---
|
||
|
||
## 2. 通知设置(NotificationSetting)
|
||
|
||
### 功能描述
|
||
NotificationSetting 是每个用户在每个账户下的通知偏好配置,使用 FlagShihTzu 位运算存储 email 和 push 两种投递渠道的开关。每种通知类型(conversation_creation, conversation_assignment 等)都可以独立开启/关闭 email 和 push 投递。
|
||
|
||
### 用户操作流程
|
||
1. 坐席进入 Profile → Notification Preferences
|
||
2. 对每种通知类型分别勾选是否接收 Email 通知和 Push 通知
|
||
3. 保存后即时生效
|
||
|
||
### 涉及的API端点
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| GET | `/api/v1/accounts/{account_id}/notification_settings` | 获取当前用户在该账户的通知设置 |
|
||
| PATCH | `/api/v1/accounts/{account_id}/notification_settings` | 更新通知设置 |
|
||
|
||
- 请求参数:`{ notification_settings: { selected_email_flags: [...], selected_push_flags: [...] } }`
|
||
|
||
### 涉及的数据模型 + 关键字段
|
||
- **NotificationSetting**(`notification_settings` 表):
|
||
- `id` (bigint, PK)
|
||
- `email_flags` (integer, default 0) — 位运算存储每种通知类型的 email 开关
|
||
- `push_flags` (integer, default 0) — 位运算存储每种通知类型的 push 开关
|
||
- `account_id` (integer, FK)
|
||
- `user_id` (integer, FK)
|
||
- `created_at`, `updated_at`
|
||
- 唯一索引:`by_account_user (account_id, user_id)`
|
||
- EMAIL_NOTIFICATION_FLAGS:从 Notification::NOTIFICATION_TYPES 派生,格式 `email_conversation_creation`, `email_conversation_assignment` 等
|
||
- PUSH_NOTIFICATION_FLAGS:同理,格式 `push_conversation_creation` 等
|
||
- 位运算查询模式:`flag_query_mode: :bit_operator`
|
||
|
||
### 涉及的业务逻辑
|
||
- **NotificationSettingsController#update**:
|
||
- 通过 `selected_email_flags` 和 `selected_push_flags` 数组参数更新位标志
|
||
- 使用 FlagShihTzu 的 `selected_*_flags=` 方法批量设置
|
||
|
||
---
|
||
|
||
## 3. 通知订阅(NotificationSubscription / Push订阅)
|
||
|
||
### 功能描述
|
||
NotificationSubscription 记录用户订阅 Push 通知的设备/浏览器信息,支持 browser_push(Web Push API)和 fcm(Firebase Cloud Messaging)两种类型。每个订阅有唯一 identifier(浏览器 endpoint 或 FCM device_id),用于防止重复订阅和跨账户迁移。
|
||
|
||
### 用户操作流程
|
||
1. 坐席在浏览器中授权 Push 通知权限
|
||
2. 前端生成 Push subscription 对象(含 endpoint, p256dh, auth)
|
||
3. 调用 API 创建 NotificationSubscription
|
||
4. 如果同一浏览器已登录多个账户,系统自动将订阅迁移到当前用户
|
||
|
||
### 涉及的API端点
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| POST | `/api/v1/notification_subscriptions` | 创建/更新 Push 订阅 |
|
||
| DELETE | `/api/v1/notification_subscriptions` | 删除 Push 订阅(按 push_token) |
|
||
|
||
- 创建请求:`{ notification_subscription: { subscription_type: 'browser_push'/'fcm', subscription_attributes: { endpoint, p256dh, auth } / { device_id, push_token } } }`
|
||
|
||
### 涉及的数据模型 + 关键字段
|
||
- **NotificationSubscription**(`notification_subscriptions` 表):
|
||
- `id` (bigint, PK)
|
||
- `subscription_type` (integer, not null) — enum: browser_push(1), fcm(2)
|
||
- `identifier` (text) — 唯一标识(browser endpoint 或 FCM device_id)
|
||
- `subscription_attributes` (jsonb, not null) — 包含 endpoint, keys(p256dh, auth) 或 device_id, push_token 等
|
||
- `user_id` (bigint, not null, FK)
|
||
- `created_at`, `updated_at`
|
||
- 唯一索引:`index_notification_subscriptions_on_identifier`
|
||
|
||
### 涉及的业务逻辑
|
||
- **NotificationSubscriptionBuilder**:
|
||
- 根据 subscription_type 从 subscription_attributes 中提取 identifier
|
||
- 如果 identifier 已存在且属于不同用户,自动迁移(`move_subscription_to_user`)
|
||
- 如果 identifier 已存在且属于同一用户,更新订阅信息
|
||
- 否则新建订阅
|
||
|
||
---
|
||
|
||
## 4. 通知生成与投递流程
|
||
|
||
### 功能描述
|
||
完整的通知生命周期:事件触发 → NotificationListener 捕获 → NotificationBuilder 创建 Notification → after_create_commit 触发投递 → Push/Email 双通道发送 → ActionCable 实时推送前端更新。
|
||
|
||
### 通知投递流程图
|
||
|
||
```
|
||
事件(如 conversation.created)
|
||
→ Dispatcher.dispatch(event_name, timestamp, data)
|
||
→ SyncDispatcher → ActionCableListener(实时推送)
|
||
→ AsyncDispatcher → EventDispatcherJob → NotificationListener
|
||
→ NotificationBuilder.perform → 创建 Notification
|
||
→ after_create_commit:
|
||
1. process_notification_delivery:
|
||
- PushNotificationJob → PushNotificationService → browser_push / fcm / ChatwootHub
|
||
- EmailNotificationJob → EmailNotificationService → ConversationNotificationsMailer
|
||
- RemoveDuplicateNotificationJob → 去重
|
||
2. dispatch_create_event → SyncDispatcher → ActionCableListener.notification_created
|
||
```
|
||
|
||
### 投递条件检查
|
||
- **Push 投递条件**:用户订阅了 push_{notification_type}? → 检查 notification_setting.push_flags
|
||
- **Email 投递条件**:
|
||
1. 用户订阅了 email_{notification_type}? → 检查 notification_setting.email_flags
|
||
2. notification.read_at 为 nil(尚未已读则发邮件)
|
||
3. user.confirmed_at 不为 nil(邮箱已确认)
|
||
4. account.within_email_rate_limit?(不超过每日发送限额)
|
||
|
||
---
|
||
|
||
## 5. NotificationListener — 事件驱动的通知创建
|
||
|
||
### 功能描述
|
||
NotificationListener 是异步事件监听器,订阅 AsyncDispatcher 中的事件,根据事件类型调用 NotificationBuilder 创建通知。监听以下事件:
|
||
|
||
### 监听事件列表
|
||
|
||
| 事件方法 | 事件名 | 通知逻辑 |
|
||
|------|------|------|
|
||
| conversation_created | conversation.created | 对 inbox 所有成员创建 conversation_creation 通知 |
|
||
| conversation_bot_handoff | conversation.bot_handoff | bot转人工时同上逻辑 |
|
||
| assignee_changed | assignee.changed | 为 assignee 创建 conversation_assignment 通知(需 notifiable_assignee_change 且 assignee 不为空) |
|
||
| message_created | message.created | 调用 MentionService + NewMessageNotificationService |
|
||
|
||
### 消息创建事件的子逻辑
|
||
- **Messages::MentionService**:
|
||
- 仅处理 private 消息且包含 @提及 的内容
|
||
- 解析 `mention://user/{id}` 和 `mention://team/{id}` 格式
|
||
- 团队提及会展开为所有团队成员的 user_ids
|
||
- 过滤不在 inbox 成员/管理员中的提及用户
|
||
- 为每个被提及用户创建 conversation_mention 通知
|
||
- 同时将提及用户添加为对话参与者
|
||
- **Messages::NewMessageNotificationService**:
|
||
- 仅处理 `message.notifiable?` 的消息
|
||
- 为对话 assignee 创建 assigned_conversation_new_message 通知(排除 sender 本身)
|
||
- 为对话参与者创建 participating_conversation_new_message 通知
|
||
- 去重检查:already_notified? 防止同一条消息重复通知同一用户
|
||
|
||
---
|
||
|
||
## 6. ActionCable实时推送
|
||
|
||
### 功能描述
|
||
ActionCable 是 Chatwoot 实时通信的核心机制,通过 WebSocket 将各类事件推送至前端。SyncDispatcher 中的 ActionCableListener 在事件发生时同步广播,通过 ActionCableBroadcastJob 异步执行实际广播。
|
||
|
||
### 用户操作流程
|
||
1. 前端通过 WebSocket 连接到 RoomChannel(参数:pubsub_token, user_id)
|
||
2. 连接成功后,stream_from 用户的 pubsub_token 和账户级 "account_{id}" 频道
|
||
3. 前端 BaseActionCableConnector.js 监听各类事件并更新 Vuex/Pinia store
|
||
|
||
### ActionCableListener 监听事件
|
||
|
||
| 事件方法 | 事件名 | 广播目标 | 推送数据 |
|
||
|------|------|------|------|
|
||
| notification_created | notification.created | 通知所属用户的 pubsub_token | notification.push_event_data + unread_count + count |
|
||
| notification_updated | notification.updated | 同上 | 同上 |
|
||
| notification_deleted | notification.deleted | 同上 | {id} + unread_count + count |
|
||
| account_cache_invalidated | account.cache_invalidated | 账户所有 agents 的 tokens | cache_keys |
|
||
| message_created | message.created | inbox members + admins + contact_inbox | message.push_event_data |
|
||
| message_updated | message.updated | 同上 | 同上 |
|
||
| conversation_created | conversation.created | inbox members + admins | conversation.push_event_data |
|
||
| conversation_updated | conversation.updated | 同上 | 同上 |
|
||
| assignee_changed | assignee.changed | inbox members | conversation.push_event_data |
|
||
| team_changed | team.changed | 同上 | 同上 |
|
||
| conversation_contact_changed | conversation.contact_changed | 同上 | 同上 |
|
||
| conversation_typing_on | conversation.typing_on | inbox members + contact_inbox (排除当前 typing 用户) | conversation + user push_event_data |
|
||
| conversation_typing_off | conversation.typing_off | 同上 | 同上 |
|
||
| contact_created/updated/deleted | contact.* | account_{id} 频道 | contact push_event_data |
|
||
| conversation_mentioned | conversation.mentioned | 被提及用户的 token | conversation push_event_data |
|
||
|
||
### 广播机制
|
||
- **broadcast(account, tokens, event_name, data)** 方法:
|
||
- tokens 为 pubsub_token 数组(包含用户 token 和账户级 token)
|
||
- 通过 `ActionCableBroadcastJob.perform_later` 异步广播
|
||
- 对 CONVERSATION_UPDATE_EVENTS 类事件,会重新查询最新数据避免前端乱序
|
||
- **RoomChannel**:
|
||
- 连接验证:通过 pubsub_token + user_id 找到合法用户
|
||
- stream_from 用户 pubsub_token + "account_{account_id}" 频道
|
||
- 订阅时更新 OnlineStatusTracker(在线状态追踪)
|
||
- **Pubsubable concern**:User 模型包含,自动生成 pubsub_token,密码修改时轮换 token
|
||
|
||
---
|
||
|
||
## 7. Push通知(浏览器Push + FCM)
|
||
|
||
### 功能描述
|
||
Push 通知是通知到达用户设备的通道,支持三种方式:浏览器 Web Push(VAPID)、Firebase Cloud Messaging(FCM)和 ChatwootHub 云端推送。PushNotificationService 根据用户的通知订阅逐一尝试推送。
|
||
|
||
### 推送渠道
|
||
|
||
| 渠道 | 条件 | 实现方式 |
|
||
|------|------|------|
|
||
| Browser Push | VapidService.public_key 存在 && subscription.browser_push? | WebPush.payload_send(VAPID签名) |
|
||
| FCM Push | FCM_PROJECT_ID/KEY 配置 && subscription.fcm? | Notification::FcmService → FCM.new → push |
|
||
| ChatwootHub | ChatwootApp.chatwoot_cloud? | ChatwootHub.push_notification_url → HTTP POST |
|
||
|
||
### 涉及的业务逻辑
|
||
- **Notification::PushNotificationService**:
|
||
- 先检查 `user_subscribed_to_notification?`(push_flags 位检查)
|
||
- 遍历 `user.notification_subscriptions`,对每个订阅尝试 browser_push → fcm → chatwoot_hub
|
||
- push_message 包含:title, tag(防重复), url(跳转到对话页面)
|
||
- **VapidService**:管理 VAPID 密钥(从 GlobalConfig/InstallationConfig 读取或自动生成)
|
||
- **Notification::FcmService**:Google Auth ServiceAccountCredentials → 生成 access_token → FCM client push
|
||
- **Notification::PushTestService**:推送测试功能,返回 success/failure/skipped 状态用于前端展示
|
||
|
||
### 涉及的配置
|
||
- `VAPID_KEYS`(InstallationConfig)— Web Push VAPID 密钥对
|
||
- `FCM_PROJECT_ID`, `FCM_SERVICE_ACCOUNT_KEY`(EnvironmentConfig)— FCM 配置
|
||
- `CHATWOOT_CLOUD`(InstallationConfig)— 是否启用 ChatwootHub 推送
|
||
|
||
---
|
||
|
||
## 8. Email通知
|
||
|
||
### 功能描述
|
||
Email 通知通过 AgentNotifications::ConversationNotificationsMailer 发送,每种通知类型对应一个邮件方法,邮件内容使用 Liquid 模板渲染,支持自定义邮件模板。
|
||
|
||
### 邮件类型对应表
|
||
|
||
| 通知类型 | Mailer方法 | 邮件主题 |
|
||
|------|------|------|
|
||
| conversation_creation | conversation_creation(conversation, agent, _user) | "{agent_name}, A new conversation [ID-{display_id}] has been created in {inbox_name}" |
|
||
| conversation_assignment | conversation_assignment(conversation, agent, _user) | "{agent_name}, A new conversation [ID-{display_id}] has been assigned to you." |
|
||
| conversation_mention | conversation_mention(conversation, agent, message) | "{agent_name}, You have been mentioned in conversation [ID-{display_id}]" |
|
||
| assigned_conversation_new_message | assigned_conversation_new_message(conversation, agent, message) | "{agent_name}, A new message in conversation [ID-{display_id}]" |
|
||
| participating_conversation_new_message | participating_conversation_new_message(conversation, agent, message) | 同上 |
|
||
| sla_missed_* | sla_missed_first_response / next / resolution | SLA违规通知 |
|
||
|
||
### 投递条件
|
||
- **Notification::EmailNotificationService**:
|
||
1. `notification.read_at` 为 nil — 用户尚未通过 Push 已读(避免重复通知)
|
||
2. `notification.user.confirmed_at` 不为 nil — 邮箱已验证
|
||
3. `user_subscribed_to_notification?` — email_flags 位检查
|
||
4. `notification.account.within_email_rate_limit?` — 邮件日限额检查
|
||
5. 通过后 `send_notification_email` → `deliver_later` → 增加邮件计数
|
||
|
||
### Email 限额
|
||
- **AccountEmailRateLimitable** concern:
|
||
- 日限额来源优先级:account.limits.emails → GlobalConfig ACCOUNT_EMAILS_LIMIT → ChatwootApp.max_limit
|
||
- 使用 Redis 计数(key: `ACCOUNT_OUTBOUND_EMAIL_COUNT:{account_id}:{date}`)
|
||
- TTL: 25小时
|
||
- 仅 ChatwootApp.chatwoot_cloud? 时生效
|
||
|
||
---
|
||
|
||
## 9. 通知Snooze与去重
|
||
|
||
### 功能描述
|
||
Snooze 功能允许用户将通知暂时延后处理,到期后自动恢复为未读。去重机制确保同一用户对同一 primary_actor(对话)不会产生多条冗余通知。
|
||
|
||
### Snooze
|
||
- **API**:`POST /api/v1/accounts/{account_id}/notifications/{id}/snooze`
|
||
- 设置 `snoozed_until` 时间戳
|
||
- **Notification::ReopenSnoozedNotificationsJob**(定时任务):
|
||
- 查询 snoozed_until 在过去3天至当前时间之间的通知
|
||
- 将其 snoozed_until 清空、read_at 清空、meta 记录 last_snoozed_at
|
||
- 更新 last_activity_at 和 updated_at 使通知重新出现在未读列表
|
||
|
||
### 去重
|
||
- **Notification::RemoveDuplicateNotificationJob**:
|
||
- 创建通知后异步执行
|
||
- 查找同一 user_id + primary_actor_id 的所有通知
|
||
- 保留最新一条(`order(created_at: :desc).first`),删除其余
|
||
- 防止同一对话反复触发时产生重复通知
|
||
|
||
---
|
||
|
||
## 10. Webhook(Account Webhook)
|
||
|
||
### 功能描述
|
||
Webhook 是 Chatwoot 向外部系统推送事件数据的标准机制。账户级 Webhook 由管理员配置 URL + 订阅事件列表,当匹配事件发生时,WebhookListener 构建 payload 并通过 WebhookJob 异步发送 HTTP POST 请求。
|
||
|
||
### 用户操作流程
|
||
1. 管理员进入 Settings → Integrations → Webhooks
|
||
2. 点击 "Add Webhook",填写 URL、名称、订阅事件列表
|
||
3. 可选择关联到特定 Inbox(inbox_type webhook)或账户全局(account_type webhook)
|
||
4. 创建后系统自动生成 secret 用于签名验证
|
||
5. 可通过 "Reset Secret" 重新生成签名密钥
|
||
|
||
### 涉及的API端点
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| GET | `/api/v1/accounts/{account_id}/webhooks` | 列出账户所有 Webhook |
|
||
| POST | `/api/v1/accounts/{account_id}/webhooks` | 创建 Webhook |
|
||
| PATCH | `/api/v1/accounts/{account_id}/webhooks/{id}` | 更新 Webhook |
|
||
| DELETE | `/api/v1/accounts/{account_id}/webhooks/{id}` | 删除 Webhook |
|
||
|
||
- 创建请求:`{ webhook: { url, name, inbox_id, subscriptions: [...] } }`
|
||
|
||
### 涉及的数据模型 + 关键字段
|
||
- **Webhook**(`webhooks` 表):
|
||
- `id` (bigint, PK)
|
||
- `url` (text) — Webhook 接收端 URL(唯一,同账户下不可重复)
|
||
- `name` (string) — Webhook 名称
|
||
- `secret` (string) — HMAC 签名密钥(has_secure_token 自动生成,支持加密存储)
|
||
- `subscriptions` (jsonb) — 订阅事件列表(数组)
|
||
- `webhook_type` (integer) — enum: account_type(0), inbox_type(1)
|
||
- `account_id` (integer, FK)
|
||
- `inbox_id` (integer, FK, optional) — inbox_type webhook 关联的收件箱
|
||
- `created_at`, `updated_at`
|
||
- 唯一索引:`index_webhooks_on_account_id_and_url`
|
||
- **WebhookSecretable** concern:提供 `has_secure_token :secret` + `encrypts :secret` + `reset_secret!`
|
||
|
||
### 允许的订阅事件(ALLOWED_WEBHOOK_EVENTS)
|
||
- conversation_status_changed
|
||
- conversation_updated
|
||
- conversation_created
|
||
- contact_created
|
||
- contact_updated
|
||
- message_created
|
||
- message_updated
|
||
- webwidget_triggered
|
||
- inbox_created
|
||
- inbox_updated
|
||
- conversation_typing_on
|
||
- conversation_typing_off
|
||
|
||
### 签名机制
|
||
- 请求头包含:
|
||
- `Content-Type: application/json`
|
||
- `Accept: application/json`
|
||
- `X-Chatwoot-Delivery: {uuid}` — 投递唯一ID
|
||
- `X-Chatwoot-Timestamp: {unix_timestamp}` — 时间戳
|
||
- `X-Chatwoot-Signature: sha256={HMAC-SHA256(secret, timestamp.body)}` — 签名
|
||
|
||
---
|
||
|
||
## 11. IntegrationHook(集成Hook)
|
||
|
||
### 功能描述
|
||
Integrations::Hook 是第三方集成的执行单元,每个 Hook 关联一个 app_id(如 slack, dialogflow, openai, notion, leadsquared, linear 等),绑定到账户级或收件箱级。Hook 接收事件后通过 HookJob 路由到对应集成处理器。
|
||
|
||
### 涉及的API端点
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| POST | `/api/v1/accounts/{account_id}/integrations/hooks` | 创建 Hook |
|
||
| PATCH | `/api/v1/accounts/{account_id}/integrations/hooks/{id}` | 更新 Hook(status, settings) |
|
||
| POST | `/api/v1/accounts/{account_id}/integrations/hooks/{id}/process_event` | 手动触发 Hook 处理事件 |
|
||
| DELETE | `/api/v1/accounts/{account_id}/integrations/hooks/{id}` | 删除 Hook |
|
||
|
||
- 创建请求:`{ hook: { app_id, inbox_id, status, settings: {...} } }`
|
||
|
||
### 涉及的数据模型 + 关键字段
|
||
- **Integrations::Hook**(`integrations_hooks` 表):
|
||
- `id` (bigint, PK)
|
||
- `app_id` (string) — 集成应用标识(slack, dialogflow, openai, notion, leadsquared, linear 等)
|
||
- `account_id` (integer, FK)
|
||
- `inbox_id` (integer, FK, optional) — inbox hook 必填,account hook 可为空
|
||
- `hook_type` (integer) — enum: account(0), inbox(1),根据 app.params[:hook_type] 自动设置
|
||
- `status` (integer) — enum: disabled(0), enabled(1)
|
||
- `access_token` (string) — OAuth token(加密存储,可重授权)
|
||
- `settings` (jsonb) — 集成配置(如 Slack channel_id, Dialogflow project_id 等)
|
||
- `reference_id` (string) — 外部引用ID
|
||
- `created_at`, `updated_at`
|
||
- 验证:
|
||
- `account_id` 必填
|
||
- `app_id` 必填
|
||
- `inbox_id` 在 inbox hook 时必填
|
||
- `settings` 需通过 app.params[:settings_json_schema] 的 JSON Schema 验证
|
||
- `app_id` 在同一账户下唯一(除非 app.params[:allow_multiple_hooks])
|
||
- feature_flag 需在账户中启用
|
||
- openai 需验证 API Key 有效性
|
||
|
||
### 涉及的业务逻辑
|
||
- **HookJob**(MutexApplicationJob,队列 medium):
|
||
- INTEGRATION_PROCESSORS 路由表:
|
||
- slack → process_slack_integration → SendOnSlackJob / UpdateSlackMessageJob
|
||
- dialogflow → process_dialogflow_integration → DialogflowJob
|
||
- google_translate → google_translate_integration
|
||
- leadsquared → process_leadsquared_integration_with_lock
|
||
- linear → process_linear_integration
|
||
- 跳过 disabled 的 hook
|
||
- 使用 Mutex 防并发
|
||
|
||
### 集成列表(从 apps.yml 加载)
|
||
- Slack、Dialogflow、Google Translate、Leadsquared、Linear、Notion、OpenAI(已迁移至 Captain)
|
||
|
||
---
|
||
|
||
## 12. WebhookListener — 事件驱动的Webhook投递
|
||
|
||
### 功能描述
|
||
WebhookListener 是异步事件监听器,订阅 AsyncDispatcher 中的事件,构建 payload 并投递到账户级 Webhook 和 API Inbox Webhook。
|
||
|
||
### 监听事件列表
|
||
|
||
| 事件方法 | 事件名 | Payload内容 | 投递目标 |
|
||
|------|------|------|------|
|
||
| conversation_status_changed | conversation.status_changed | conversation.webhook_data + event + changed_attributes | account webhooks + api inbox webhooks |
|
||
| conversation_updated | conversation.updated | 同上 | 同上 |
|
||
| conversation_created | conversation.created | conversation.webhook_data + event | 同上 |
|
||
| message_created | message.created | message.webhook_data + event(需 webhook_sendable?) | 同上 |
|
||
| message_updated | message.updated | 同上 | 同上 |
|
||
| webwidget_triggered | webwidget.triggered | contact_inbox.webhook_data + event + event_info | 同上 |
|
||
| contact_created | contact.created | contact.webhook_data + event | 仅 account webhooks |
|
||
| contact_updated | contact.updated | 同上 + changed_attributes | 同上 |
|
||
| inbox_created | inbox.created | inbox push_data + event | 仅 account webhooks |
|
||
| inbox_updated | inbox.updated | 同上 + changed_attributes | 同上 |
|
||
| conversation_typing_on/off | conversation.typing_* | event + user + conversation + is_private | inbox webhooks + api inbox |
|
||
|
||
### 投递逻辑
|
||
- **deliver_account_webhooks**:遍历 `account.webhooks.account_type`,过滤匹配 subscriptions 的 webhook,调用 WebhookJob
|
||
- **deliver_api_inbox_webhooks**:仅 Channel::Api 类型且 webhook_url 不空的 inbox,调用 WebhookJob
|
||
- **deliver_webhook_payloads**:同时投递上述两种
|
||
- WebhookJob 参数:url, payload, webhook_type(:account_webhook/:api_inbox_webhook/:agent_bot_webhook), secret, delivery_id
|
||
|
||
---
|
||
|
||
## 13. HookListener — 集成Hook执行
|
||
|
||
### 功能描述
|
||
HookListener 监听事件并执行 IntegrationHooks(集成 Hook)。不同于 WebhookListener 直接 HTTP POST,HookListener 路由到各集成的专有处理器(如 Slack 发消息、Dialogflow AI回复等)。
|
||
|
||
### 监听事件列表
|
||
|
||
| 事件方法 | 事件名 | 执行逻辑 |
|
||
|------|------|------|
|
||
| message_created | message.created | 对所有 account.hooks 执行(过滤 inbox 匹配 + 事件支持) → HookJob |
|
||
| message_updated | message.updated | 同上 |
|
||
| contact_created | contact.created | 仅执行 account_hooks(无 inbox 限制) |
|
||
| contact_updated | contact.updated | 同上 |
|
||
| conversation_created | conversation.created | 仅执行 account_hooks |
|
||
| conversation_resolved | conversation.resolved | 仅在 status == resolved 时执行 account_hooks |
|
||
|
||
### 执行逻辑
|
||
- **execute_hooks**:遍历 message.account.hooks,过滤 inbox 不匹配的和事件不支持的,调用 HookJob.perform_later
|
||
- **execute_account_hooks**:遍历 account.hooks.account_hooks,过滤事件不支持,调用 HookJob.perform_later
|
||
- **supported_hook_event?**:检查 hook.app.params[:hook_events] 是否包含当前事件名
|
||
|
||
---
|
||
|
||
## 14. InstallationWebhook(平台级事件推送)
|
||
|
||
### 功能描述
|
||
InstallationWebhookListener 是平台级(跨账户)事件监听器,仅在 InstallationConfig 中配置了 `INSTALLATION_EVENTS_WEBHOOK_URL` 时生效。用于 SaaS/Cloud 部署场景下将平台级事件(如账户创建)推送到外部系统。
|
||
|
||
### 监听事件
|
||
- **account_created**:当新账户创建时,推送 payload = account.webhook_data + event + users(管理员列表)
|
||
- 投递:WebhookJob.perform_later(webhook_url, payload)
|
||
|
||
### 配置
|
||
- `INSTALLATION_EVENTS_WEBHOOK_URL`(InstallationConfig)— 接收端 URL
|
||
- 仅在 URL 存在时投递
|
||
|
||
---
|
||
|
||
## 15. 事件分发体系(Dispatcher架构)
|
||
|
||
### 功能描述
|
||
Chatwoot 采用 Wisper 发布/订阅模式实现事件分发。Dispatcher 是全局单例,在应用初始化时加载所有 Listener。事件分为同步和异步两种分发路径:
|
||
|
||
### 架构层次
|
||
|
||
```
|
||
Dispatcher (Singleton)
|
||
├── SyncDispatcher (同步)
|
||
│ └── listeners: [ActionCableListener, AgentBotListener]
|
||
│ └── dispatch → Events::Base → publish → 同步回调
|
||
└── AsyncDispatcher (异步)
|
||
│ └── listeners: [AutomationRuleListener, CampaignListener, CsatSurveyListener,
|
||
│ HookListener, InstallationWebhookListener, NotificationListener,
|
||
│ ParticipationListener, UnreadCounts::Listener, ReportingEventListener,
|
||
│ WebhookListener]
|
||
│ └── dispatch → EventDispatcherJob(critical队列) → publish_event → 异步回调
|
||
```
|
||
|
||
### 事件触发方式
|
||
- 业务代码中调用 `Dispatcher.dispatch(event_name, timestamp, data)`
|
||
- 例:`Rails.configuration.dispatcher.dispatch(CONVERSATION_CREATED, Time.zone.now, conversation: conversation)`
|
||
- Dispatcher.dispatch 同时触发 Sync 和 Async 两条路径
|
||
|
||
### Events::Base
|
||
- 属性:name, timestamp, data
|
||
- `method_name`:将事件名 `conversation.created` 转为 `conversation_created` 方法名
|
||
- Listener 中对应方法接收 `Events::Base` 对象
|
||
|
||
### 事件类型定义(Events::Types)
|
||
- 安装级事件:ACCOUNT_CREATED, ACCOUNT_CACHE_INVALIDATED
|
||
- 对话事件:CONVERSATION_CREATED/UPDATED/DELETED/READ/BOT_HANDOFF/OPENED/RESOLVED/STATUS_CHANGED/CONTACT_CHANGED/TYPING_ON/OFF/MENTIONED
|
||
- 分配事件:ASSIGNEE_CHANGED, TEAM_CHANGED
|
||
- 消息事件:MESSAGE_CREATED, FIRST_REPLY_CREATED, REPLY_CREATED, MESSAGE_UPDATED
|
||
- 联系人事件:CONTACT_CREATED/UPDATED/DELETED/MERGED
|
||
- 通知事件:NOTIFICATION_CREATED/UPDATED/DELETED
|
||
- Inbox事件:INBOX_CREATED/UPDATED
|
||
- WebWidget事件:WEBWIDGET_TRIGGERED
|
||
|
||
---
|
||
|
||
## 跨功能依赖
|
||
|
||
| 本模块功能 | 依赖的其他模块 |
|
||
|------|------|
|
||
| 通知创建 | M3 Conversation(primary_actor)、M1 User(user/secondary_actor) |
|
||
| ActionCable 广播 | M2 Inbox(inbox.members 确定广播目标)、M3 Conversation(push_event_data) |
|
||
| Webhook 投递 | M3 Conversation/Message(webhook_data)、M4 Contact(webhook_data) |
|
||
| Hook 执行 | M2 Inbox(hook.inbox)、M6 Automation(HookJob 路由) |
|
||
| Email 限额 | M1 Account(limits.emails)、AccountEmailRateLimitable |
|
||
| Push 订阅 | M1 User(notification_subscriptions) |
|
||
| 通知权限 | M5 Team(inbox membership、conversation access) |
|
||
|
||
---
|
||
|
||
## GoChat 实现建议
|
||
|
||
| 功能 | 建议 |
|
||
|------|------|
|
||
| Notification 模型 | 保留 polymorphic actor 设计,但建议将 notification_type 改为 string enum(Go 没有 integer enum 习惯) |
|
||
| NotificationSetting | 用 JSON/map 存储各通知类型开关,而非位运算(Go 无 FlagShihTzu 对应) |
|
||
| ActionCable | 替换为 WebSocket/gorilla 或 nats/gorilla pub-sub,前端用相同事件名协议 |
|
||
| Push 通知 | 浏览器 Push 用 webpush-go,FCM 用 firebase-admin-go,可选 ChatwootHub 模式 |
|
||
| Email 通知 | 使用模板引擎(如 mailgun/sendgrid template),邮件限额用 Redis 计数 |
|
||
| Webhook | 保留签名机制(HMAC-SHA256),HTTP POST 用带超时的 SafeFetch 模式 |
|
||
| Hook 路由 | 用接口/策略模式替代 HookJob 的 INTEGRATION_PROCESSORS 硬编码路由 |
|
||
| Dispatcher | 替换 Wisper 为 Go channel/event-bus,同步/异步用 goroutine+worker pool |
|
||
| Snooze | 保留定时任务恢复机制,可用 cron 或 tick scheduler |
|
||
| 去重 | 保留 RemoveDuplicate 模式,创建后异步清理 | |