Files
gochat/docs/requirements/M8-notification-and-webhook.md
T
2026-06-04 15:44:48 +08:00

617 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 模式,创建后异步清理 |