# 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 模式,创建后异步清理 |