Files
gochat/docs/requirements/M12-platform-and-integration.md
T
2026-06-04 15:44:48 +08:00

637 lines
34 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.
# M12 平台与集成 — Chatwoot 功能梳理文档
> 基于 Chatwoot 源码深度阅读 + CodeGraph 上下文梳理
> 生成日期:2026-05-22
> 参照仓库:chatwoot-reference
> 模块覆盖:PlatformApp · AgentBot · DashboardApp · DataImport · Webhook · AccessToken认证 · InstallationConfig
---
## 目录
1. [PlatformApp(平台应用 / Partner API)](#1-platformapp平台应用--partner-api)
2. [PlatformAppPermissible(权限授权)](#2-platformapppermissible权限授权)
3. [Platform API 路由体系](#3-platform-api-路由体系)
4. [AccessToken 与 AccessTokenable(API令牌认证)](#4-accesstoken-与-accesstokenableapi令牌认证)
5. [AccessTokenAuthHelper(令牌鉴权中间件)](#5-accesstokenauthhelper令牌鉴权中间件)
6. [AgentBot(智能机器人)](#6-agentbot智能机器人)
7. [AgentBotInbox(Bot-Inbox绑定)](#7-agentbotinboxbot-inbox绑定)
8. [AgentBotListener(Bot事件监听)](#8-agentbotlistenerbot事件监听)
9. [AgentBots::WebhookJob(Bot Webhook投递)](#9-agentbotswebhookjobbot-webhook投递)
10. [DashboardApp(仪表盘扩展应用)](#10-dashboardapp仪表盘扩展应用)
11. [DataImport(数据导入)](#11-dataimport数据导入)
12. [DataImportJob + ContactManager(导入处理流程)](#12-dataimportjob--contactmanager导入处理流程)
13. [Webhook(账户级Webhook)](#13-webhook账户级webhook)
14. [WebhookSecretable(签名密钥机制)](#14-webhooksecretable签名密钥机制)
15. [Webhooks::Trigger(Webhook投递引擎)](#15-webhookstriggerwebhook投递引擎)
16. [InstallationConfig(平台级配置)](#16-installationconfig平台级配置)
17. [GlobalConfig + GlobalConfigService(配置缓存层)](#17-globalconfig--globalconfigservice配置缓存层)
---
## 1. PlatformApp(平台应用 / Partner API)
### 功能描述
PlatformApp 是 Chatwoot 为第三方合作伙伴(Partner)提供的平台级 API 认证主体。每个 PlatformApp 拥有一个 AccessToken,通过该令牌可调用 Platform API(独立于普通 Account API),实现对用户、账户、AgentBot、AccountUser 等资源的 CRUD 管理。PlatformApp 与被操作资源之间通过 `PlatformAppPermissible` 多态关联授权——只有被授权的资源才能被该 PlatformApp 访问/修改。
### 用户操作流程
1. Super Admin 在后台创建 PlatformApp,系统自动生成 AccessToken
2. 第三方系统持有该 AccessToken,通过 `api_access_token` Header 调用 Platform API
3. PlatformController 验证令牌 → 解析出 PlatformApp → 校验 Permissible 授权范围
4. 通过授权校验后,第三方可操作被授权的 Account / User / AgentBot 等资源
5. 第三方可通过 `POST /platform/api/v1/users/:id/token` 获取用户的 SSO 登录令牌
### 涉及的API端点
| 方法 | 路径 | 说明 |
|------|------|------|
| Super Admin CRUD | `/super_admin/platform_apps` | 管理后台管理 PlatformApp |
| GET | `/platform/api/v1/users` | 列出授权用户(需 Permissible) |
| POST | `/platform/api/v1/users` | 创建用户并自动授权 |
| GET | `/platform/api/v1/users/:id` | 查看(需 Permissible) |
| PATCH | `/platform/api/v1/users/:id` | 更新(需 Permissible) |
| DELETE | `/platform/api/v1/users/:id` | 删除(需 Permissible) |
| GET | `/platform/api/v1/users/:id/login` | 获取用户 SSO 登录 URL |
| POST | `/platform/api/v1/users/:id/token` | 获取用户 API Token |
| GET | `/platform/api/v1/accounts` | 列出授权账户 |
| POST | `/platform/api/v1/accounts` | 创建账户并授权 |
| GET | `/platform/api/v1/accounts/:id` | 查看授权账户 |
| PATCH | `/platform/api/v1/accounts/:id` | 更新授权账户(含 features/limits) |
| DELETE | `/platform/api/v1/accounts/:id` | 删除授权账户 |
| GET | `/platform/api/v1/accounts/:account_id/account_users` | 列出账户成员 |
| POST | `/platform/api/v1/accounts/:account_id/account_users` | 添加账户成员 |
| DELETE | `/platform/api/v1/accounts/:account_id/account_users` | 移除账户成员 |
| GET | `/platform/api/v1/agent_bots` | 列出授权 AgentBot |
| POST | `/platform/api/v1/agent_bots` | 创建 AgentBot 并授权 |
| GET | `/platform/api/v1/agent_bots/:id` | 查看授权 AgentBot |
| PATCH | `/platform/api/v1/agent_bots/:id` | 更新授权 AgentBot |
| DELETE | `/platform/api/v1/agent_bots/:id` | 删除授权 AgentBot |
| DELETE | `/platform/api/v1/agent_bots/:id/avatar` | 删除 AgentBot 头像 |
| POST | `/platform/api/v1/accounts/:account_id/email_channel_migrations` | 执行邮箱渠道迁移 |
### 涉及的数据模型+关键字段
- **PlatformApp**(`platform_apps` 表):
- `id` (bigint, PK)
- `name` (string, NOT NULL) — 应用名称
- `created_at`, `updated_at` (datetime)
- 关联:`has_many :platform_app_permissibles, dependent: :destroy_async`
- Concern:`include AccessTokenable`(创建时自动生成 AccessToken)
- 验证:`validates :name, presence: true`
### 涉及的业务逻辑
- **PlatformApp 创建时**:`AccessTokenable` concern 的 `after_create :create_access_token` 自动创建 AccessToken
- **PlatformController**(`app/controllers/platform_controller.rb`):全局鉴权基类
- `ensure_access_token` — 从 Header 中解析 `api_access_token`
- `set_platform_app` — 从 AccessToken owner 解析出 PlatformApp,否则返回 401
- `validate_platform_app_permissible` — 校验当前资源是否在 Permissible 授权范围内
- **各子 Controller**:继承 PlatformController,在 create 操作时自动创建 Permissible 授权关系
---
## 2. PlatformAppPermissible(权限授权)
### 功能描述
PlatformAppPermissible 是 PlatformApp 与被授权资源之间的多态授权桥梁。每个 Permissible 记录代表"某 PlatformApp 可以操作某资源"。资源类型可以是 Account、User、AgentBot 等。创建用户/账户/AgentBot 时,子控制器会自动为 PlatformApp 添加 Permissible 记录。
### 涉及的数据模型+关键字段
- **PlatformAppPermissible**(`platform_app_permissibles` 表):
- `id` (bigint, PK)
- `platform_app_id` (bigint, NOT NULL) — 所属 PlatformApp
- `permissible_id` (bigint, NOT NULL) — 被授权资源 ID
- `permissible_type` (string, NOT NULL) — 被授权资源类型(Account / User / AgentBot 等)
- `created_at`, `updated_at` (datetime)
- 索引:
- `index_platform_app_permissibles_on_permissibles` — (permissible_type, permissible_id)
- `index_platform_app_permissibles_on_platform_app_id` — (platform_app_id)
- `unique_permissibles_index` — (platform_app_id, permissible_id, permissible_type) UNIQUE
- 关联:`belongs_to :platform_app`, `belongs_to :permissible, polymorphic: true`
- 验证:`validates :platform_app, presence: true`; uniqueness scoped by `[permissible_id, permissible_type]`
### 涉及的业务逻辑
- **授权校验**:`PlatformController#validate_platform_app_permissible` 查询 `platform_app_permissibles.find_by(permissible: @resource)`,不在授权范围内返回 401
- **自动授权**:
- `UsersController#create` → `find_or_create_by(permissible: @resource)`
- `AccountsController#create` → `find_or_create_by(permissible: @resource)`
- `AgentBotsController#create` → `find_or_create_by(permissible: @resource)`
- **查询授权资源**:
- `AccountsController#index` → 查询 `where(permissible_type: 'Account')` 并 includes permissible
- `AgentBotsController#index` → 查询 `where(permissible_type: 'AgentBot')`
---
## 3. Platform API 路由体系
### 功能描述
Platform API 是 Chatwoot 提供给合作伙伴的独立 API 套件,使用 AccessToken(而非 User session)鉴权,路由独立于 Account API 体系。所有 Platform API 控制器继承 `PlatformController`,统一走 AccessToken + Permissible 双重校验。
### 路由结构
```
namespace :platform, defaults: { format: 'json' } do
namespace :api do
namespace :v1 do
resources :users, only: [:create, :show, :update, :destroy]
member do
get :login # SSO 登录链接
post :token # 获取用户 API token
end
resources :agent_bots, only: [:index, :create, :show, :update, :destroy]
member do
delete :avatar # 删除头像
end
resources :accounts, only: [:index, :create, :show, :update, :destroy]
resources :account_users, only: [:index, :create]
collection do
delete :destroy # 移除成员
end
resources :email_channel_migrations, only: [:create]
end
end
end
```
### 控制器矩阵
| Controller | 基类 | 特殊 before_action |
|------|------|------|
| `Platform::Api::V1::UsersController` | PlatformController | `set_resource` + `validate_platform_app_permissible`(含 login/token) |
| `Platform::Api::V1::AccountsController` | PlatformController | 标准 set_resource + validate |
| `Platform::Api::V1::AgentBotsController` | PlatformController | 标准 set_resource + validate |
| `Platform::Api::V1::AccountUsersController` | PlatformController | 手动 set_resource + validate |
| `Platform::Api::V1::EmailChannelMigrationsController` | PlatformController | validate_account_permissible + validate_feature_flag + validate_params |
---
## 4. AccessToken 与 AccessTokenable(API令牌认证)
### 功能描述
AccessToken 是 Chatwoot 中多态令牌模型,服务于两类场景:① PlatformApp 的 Partner API 鉴权;② AgentBot 的 Bot API 鉴权。AccessToken 通过 `AccessTokenable` concern 自动创建——任何包含此 concern 的模型在创建后自动生成一条 AccessToken 记录。Token 通过 `has_secure_token` 生成,唯一且不可预测。
### 涉及的数据模型+关键字段
- **AccessToken**(`access_tokens` 表):
- `id` (bigint, PK)
- `owner_type` (string) — 令牌所属者类型(PlatformApp / AgentBot)
- `owner_id` (bigint) — 令牌所属者 ID
- `token` (string, UNIQUE) — 令牌值(`has_secure_token` 自动生成)
- `created_at`, `updated_at` (datetime)
- 索引:`index_access_tokens_on_owner_type_and_owner_id` — (owner_type, owner_id);`index_access_tokens_on_token` — UNIQUE
- 关联:`belongs_to :owner, polymorphic: true`
### AccessTokenable Concern
```ruby
module AccessTokenable
extend ActiveSupport::Concern
included do
has_one :access_token, as: :owner, dependent: :destroy_async
after_create :create_access_token
end
def create_access_token
AccessToken.create!(owner: self)
end
end
```
- 自动在模型创建后生成一条 AccessToken
- `dependent: :destroy_async` — 父模型删除时异步删除令牌
- 包含此 concern 的模型:**PlatformApp**、**AgentBot**
### Token 再生成
- `AgentBotsController#reset_access_token` — 调用 `@agent_bot.access_token.regenerate_token`
- `has_secure_token` 提供了 `regenerate_token` 方法
---
## 5. AccessTokenAuthHelper(令牌鉴权中间件)
### 功能描述
AccessTokenAuthHelper 是用于 Account API(非 Platform API)场景的 AccessToken 鉴权 concern。它允许 AgentBot 通过 AccessToken 代替用户 session 调用特定 Account API 端点。但 Bot 仅能访问有限的端点集合(BOT_ACCESSIBLE_ENDPOINTS),防止越权。
### 核心逻辑
- **ensure_access_token** — 从 Header 解析 `api_access_token`,查找对应 AccessToken 记录
- **authenticate_access_token!** — 验证令牌存在,设置 `@resource = @access_token.owner`,如 owner 是 User 则设置 `Current.user`
- **validate_bot_access_token!** — 如果 owner 是 AgentBot,则检查当前端点是否在 `BOT_ACCESSIBLE_ENDPOINTS` 白名单内
- **BOT_ACCESSIBLE_ENDPOINTS 白名单**:
- `api/v1/accounts/conversations` → `toggle_status, toggle_typing_status, toggle_priority, create, update, custom_attributes`
- `api/v1/accounts/conversations/messages` → `create`
- `api/v1/accounts/conversations/assignments` → `create`
- **allowed_current_user_type?** — 仅 User 和 AgentBot 可设置为 Current.user
---
## 6. AgentBot(智能机器人)
### 功能描述
AgentBot 是 Chatwoot 的智能机器人模型,支持 Webhook 类型(将对话事件推送到外部 URL)。AgentBot 可绑定到 Inbox(通过 AgentBotInbox),也可被直接分配为会话的 assignee(`assignee_agent_bot_id`)。Bot 创建时自动获得 AccessToken 和 Secret(用于 Webhook 签名),支持头像管理。Bot 分为账户级(`account_id` 有值)和全局级(`account_id = nil`)。
### 用户操作流程
1. 管理员在 Agent Bot 设置页创建 AgentBot,填写名称、描述、outgoing_url
2. 系统自动生成 AccessToken + Secret
3. 管理员将 Bot 绑定到 Inbox(AgentBotInbox),Bot 开始监听该 Inbox 的对话事件
4. 事件触发 → AgentBotListener 收集该 Inbox 的 Bot → 通过 AgentBots::WebhookJob 将 payload 推送到 outgoing_url
5. 外部 Bot 服务处理后,通过 AccessToken 鉴权调用 Account API 回写消息/更新会话状态
6. 管理员也可将 Bot 直接设置为会话的 assignee
### 涉及的API端点
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/v1/accounts/:account_id/agent_bots` | 列出 accessible Bot(含全局 + 本账户) |
| POST | `/api/v1/accounts/:account_id/agent_bots` | 创建账户级 Bot |
| GET | `/api/v1/accounts/:account_id/agent_bots/:id` | 查看某 Bot |
| PATCH | `/api/v1/accounts/:account_id/agent_bots/:id` | 更新 Bot |
| DELETE | `/api/v1/accounts/:account_id/agent_bots/:id` | 删除 Bot |
| DELETE | `/api/v1/accounts/:account_id/agent_bots/:id/avatar` | 删除头像 |
| POST | `/api/v1/accounts/:account_id/agent_bots/:id/reset_access_token` | 重置 AccessToken |
| POST | `/api/v1/accounts/:account_id/agent_bots/:id/reset_secret` | 重置 Webhook Secret |
### 涉及的数据模型+关键字段
- **AgentBot**(`agent_bots` 表):
- `id` (bigint, PK)
- `name` (string) — Bot 名称
- `description` (string) — Bot 描述
- `outgoing_url` (string) — Webhook 推送 URL
- `bot_type` (integer, default: webhook) — Bot 类型枚举(目前仅 webhook: 0)
- `bot_config` (jsonb) — Bot 自定义配置
- `secret` (string) — Webhook 签名密钥(`has_secure_token` + 可选加密)
- `account_id` (bigint, nullable) — 所属账户(nil = 全局 Bot)
- 关联:
- `has_many :agent_bot_inboxes, dependent: :destroy_async`
- `has_many :inboxes, through: :agent_bot_inboxes`
- `has_many :messages, as: :sender, dependent: :nullify`
- `has_many :platform_app_permissibles, as: :permissible, dependent: :destroy`
- `has_many :assigned_conversations`(foreign_key: `assignee_agent_bot_id`)
- `belongs_to :account, optional: true`
- Concern:
- `AccessTokenable` — 自动创建 AccessToken
- `Avatarable` — 支持头像
- `WebhookSecretable` — 自动生成 Secret(可加密)
- Scope:`accessible_to(account)` → `where(account_id: [nil, account.id])`
- 方法:
- `push_event_data(inbox)` → `{ id, name, avatar_url, type: 'agent_bot' }`
### 涉及的业务逻辑
- **AgentBotPolicy** — index/show 需 administrator 或 agent;create/update/destroy/reset_access_token/reset_secret/avatar 仅 administrator
- **AgentBotPresenter** — `access_token` 方法:仅当 `Current.account.id == account_id` 时返回 token(跨账户不暴露令牌)
- **AvatarFromUrlJob** — 支持 `avatar_url` 参数远程拉取头像
---
## 7. AgentBotInbox(Bot-Inbox绑定)
### 功能描述
AgentBotInbox 是 AgentBot 与 Inbox 之间的绑定关系模型。每个绑定记录表示"某 Bot 监听某 Inbox"。支持 active/inactive 状态控制,允许管理员在不删除绑定的情况下暂停 Bot 对某 Inbox 的监听。`account_id` 由 Inbox 自动继承。
### 涉及的数据模型+关键字段
- **AgentBotInbox**(`agent_bot_inboxes` 表):
- `id` (bigint, PK)
- `agent_bot_id` (integer, NOT NULL)
- `inbox_id` (integer, NOT NULL)
- `account_id` (integer) — 从 inbox.account_id 自动填充
- `status` (integer, default: active) — 状态枚举:active(0), inactive(1)
- 关联:`belongs_to :inbox`, `belongs_to :agent_bot`, `belongs_to :account`
- `before_validation :ensure_account_id` — 自动从 inbox 继承 account_id
---
## 8. AgentBotListener(Bot事件监听)
### 功能描述
AgentBotListener 是事件驱动的 Bot Webhook 推送监听器,继承 BaseListener。监听对话和消息相关事件,将 payload 推送给与 Inbox 绑定的 Bot。Bot 来源分两种:① Inbox 绑定的活跃 Bot(`AgentBotInbox.active`);② 会话的 assignee Bot(`conversation.assignee_agent_bot`)。
### 监听的事件
| 事件 | 推送内容 |
|------|------|
| `conversation_resolved` | `conversation.webhook_data.merge(event: 'conversation_resolved')` |
| `conversation_opened` | `conversation.webhook_data.merge(event: 'conversation_opened')` |
| `conversation_status_changed` | webhook_data + changed_attributes |
| `conversation_updated` | webhook_data + changed_attributes |
| `message_created` | `message.webhook_data.merge(event: 'message_created')` |
| `message_updated` | `message.webhook_data.merge(event: 'message_updated')` |
| `webwidget_triggered` | inbox 数据 + contact 信息 |
### 涉及的业务逻辑
- **agent_bots_for(inbox, conversation)** — 收集活跃 Bot 列表:
- 会话 assignee Bot(`conversation.assignee_agent_bot`)
- Inbox 绑定的活跃 Bot(`inbox.agent_bot`,仅当 `agent_bot_inbox.active?`)
- 去重合并
- **process_webhook_bot_event** — 如果 Bot 的 `outgoing_url` 不为空,通过 `AgentBots::WebhookJob.perform_later` 异步推送
- 推送参数:`(outgoing_url, payload, :agent_bot_webhook, secret: agent_bot.secret, delivery_id: SecureRandom.uuid)`
---
## 9. AgentBots::WebhookJob(Bot Webhook投递)
### 功能描述
AgentBots::WebhookJob 是 AgentBot 专用的 Webhook 推送 Job,继承 WebhookJob,队列优先级为 `high`。支持 `RetryableError`(HTTP 429/500)的3次重试,每次间隔 3秒。重试失败后调用 `Webhooks::Trigger#handle_failure` 处理。
### 核心逻辑
- `perform(url, payload, webhook_type = :agent_bot_webhook, secret:, delivery_id:)` → 调用 `Webhooks::Trigger.execute`
- 重试策略:`retry_on Webhooks::Trigger::RetryableError, wait: 3.seconds, attempts: 3`
- 失败日志:`Rails.logger.warn("[AgentBots::WebhookJob] attempt #{executions} failed")`
---
## 10. DashboardApp(仪表盘扩展应用)
### 功能描述
DashboardApp 是 Chatwoot 仪表盘侧边栏的 iframe 扩展应用模型。每个 DashboardApp 由账户内用户创建,包含一个 `content` jsonb 字段,存储一个数组——数组中的每个元素定义一个 iframe 面板(type: 'frame', url: 指定 iframe URL)。DashboardApp 用于第三方将自建界面嵌入 Chatwoot 仪表盘侧边栏。
### 用户操作流程
1. 管理员在设置页创建 DashboardApp,填写 title 和 iframe URL 列表
2. 系统验证 content 格式(必须是数组,每个元素包含 type:'frame' + 合法 http/https URL)
3. DashboardApp 创建后关联到创建者和所属账户
4. 仪表盘侧边栏渲染时加载该账户的所有 DashboardApp,每个 app 以 iframe 嵌入
### 涉及的API端点
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/v1/accounts/:account_id/dashboard_apps` | 列出账户所有 DashboardApp |
| GET | `/api/v1/accounts/:account_id/dashboard_apps/:id` | 查看单个 |
| POST | `/api/v1/accounts/:account_id/dashboard_apps` | 创建(自动绑定 user_id) |
| PATCH | `/api/v1/accounts/:account_id/dashboard_apps/:id` | 更新 |
| DELETE | `/api/v1/accounts/:account_id/dashboard_apps/:id` | 删除 |
### 涉及的数据模型+关键字段
- **DashboardApp**(`dashboard_apps` 表):
- `id` (bigint, PK)
- `title` (string, NOT NULL) — 应用标题
- `content` (jsonb) — iframe 配置数组,格式:`[{ type: 'frame', url: 'https://...' }]`
- `account_id` (bigint, NOT NULL) — 所属账户
- `user_id` (bigint) — 创建者
- 关联:`belongs_to :user`, `belongs_to :account`
### Content 验证规则
- 必须是数组(`is_a?(Array)`),非空(`minItems: 1`)
- 每个元素必须包含 `type`(仅允许 `'frame'`)和 `url`(必须 http/https URI)
- 使用 JSONSchemer 校验 schema
- 空白/非数组数据自动重置为 `[]`
---
## 11. DataImport(数据导入)
### 功能描述
DataImport 是 Chatwoot 的批量数据导入模型,目前仅支持 contacts 导入。用户上传 CSV 文件后,系统创建 DataImport 记录并自动触发 DataImportJob 异步处理。DataImport 跟踪导入状态(pending → processing → completed/failed)、已处理记录数、错误信息,并可附加 `import_file`(源 CSV)和 `failed_records`(失败记录 CSV)。
### 用户操作流程
1. 管理员在联系人页面点击"导入",上传 CSV 文件
2. 系统创建 DataImport 记录(`data_type: 'contacts'`),附上 `import_file`
3. `after_create_commit` 触发 `DataImportJob.set(wait: 1.minute).perform_later`(等待文件上传到云存储)
4. Job 解析 CSV → 查找/创建联系人(支持 identifier/email/phone_number 去重合并)
5. 处理完成后更新 `processed_records` / `total_records` / `status`
6. 失败记录保存为 `failed_records` CSV 附件
7. 发送导入完成/失败通知邮件给管理员
### 涉及的API端点
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/accounts/:account_id/contacts/import` | 上传 CSV 导入联系人 |
### 涉及的数据模型+关键字段
- **DataImport**(`data_imports` 表):
- `id` (bigint, PK)
- `data_type` (string, NOT NULL) — 导入类型(目前仅 `'contacts'`)
- `status` (integer, default: pending) — 状态枚举:pending(0), processing(1), completed(2), failed(3)
- `total_records` (integer) — 总记录数
- `processed_records` (integer) — 成功处理记录数
- `processing_errors` (text) — 处理错误描述
- `account_id` (bigint, NOT NULL)
- 附件:`has_one_attached :import_file`,`has_one_attached :failed_records`
- 关联:`belongs_to :account`
- 验证:`data_type` inclusion `{ in: ['contacts'] }`
- 回调:`after_create_commit :process_data_import` → 延迟1分钟触发 DataImportJob
---
## 12. DataImportJob + ContactManager(导入处理流程)
### 功能描述
DataImportJob 是异步 CSV 导入处理器,使用 `DataImport::ContactManager` 执行联系人查找/去重/创建。流程:解析CSV → 查找已有联系人(按 identifier → email → phone_number 优先级) → 合并属性 → 批量创建 → 标签关联 → 失败记录导出 → 邮件通知。
### 核心逻辑
- **DataImportJob**(`app/jobs/data_import_job.rb`):
- 队列:`low`
- 重试:`retry_on ActiveStorage::FileNotFoundError, wait: 1.minute, attempts: 3`
- 流程:
1. 更新 `status: :processing`
2. 解析 CSV → `ContactManager.build_contact` 逐行处理
3. 每行提取 labels,校验是否在账户已批准标签内
4. 不合法标签行 → 加入 `rejected_contacts`
5. 合法行 → 构建联系人属性(name, email, phone_number, identifier, custom_attributes 等)
6. 批量导入联系人(`Contact.import`,batch_size: 1000)
7. 批量应用标签(`ActsAsTaggableOn::Tagging.import`)
8. 保存失败记录 CSV 为 `failed_records` 附件
9. 发送 `AdministratorNotificationsMailer.contact_import_complete` 或 `contact_import_failed`
- **DataImport::ContactManager**(`app/services/data_import/contact_manager.rb`):
- 查找优先级:`identifier → email → phone_number`
- 合并策略:
- `update_contact_with_merged_attributes` — 去重时合并 identifier/name/phone_number/email/custom_attributes
- `format_phone_number` — 自动加 `+` 前缀
- `from_email` — 使用 Contact scope 查询邮箱
- `find_or_initialize_contact` — 如找不到则初始化新联系人
---
## 13. Webhook(账户级Webhook)
### 功能描述
Webhook 是 Chatwoot 的账户级事件推送模型。管理员可在账户或 Inbox 级别创建 Webhook,订阅特定事件(conversation_status_changed, message_created 等),当事件发生时由 WebhookListener 触发 WebhookJob 投递 payload 到指定 URL。Webhook 支持 account_type(账户级)和 inbox_type(Inbox级)两种类型,每个 Webhook 包含 `secret` 用于 HMAC 签名验证。
### 用户操作流程
1. 管理员在设置页创建 Webhook,填写 URL、名称、订阅事件列表
2. 系统自动生成 Secret,验证 URL 格式和订阅事件合法性
3. 事件发生时 → WebhookListener 触发 → WebhookJob 异步推送
4. 推送使用 HMAC-SHA256 签名(`x-chatwoot-signature` Header)
### 涉及的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` | 更新 |
| DELETE | `/api/v1/accounts/:account_id/webhooks/:id` | 删除 |
### 涉及的数据模型+关键字段
- **Webhook**(`webhooks` 表):
- `id` (bigint, PK)
- `name` (string) — Webhook 名称
- `url` (text) — 推送 URL(account_id + url 联合唯一)
- `secret` (string) — HMAC 签名密钥(WebhookSecretable)
- `subscriptions` (jsonb) — 订阅事件数组
- `webhook_type` (integer, default: account_type) — 类型枚举:account_type(0), inbox_type(1)
- `account_id` (integer) — 所属账户
- `inbox_id` (integer, nullable) — 所属 Inbox(仅 inbox_type)
- 关联:`belongs_to :account`, `belongs_to :inbox, optional: true`
- Concern:`WebhookSecretable`
- 验证:`url` 格式(http/https URI regex);`subscriptions` 必须是数组且所有事件在 `ALLOWED_WEBHOOK_EVENTS` 内
### 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
```
---
## 14. WebhookSecretable(签名密钥机制)
### 功能描述
WebhookSecretable 是为 Webhook 和 AgentBot 提供签名密钥的 concern。使用 `has_secure_token :secret` 自动生成密钥,并支持加密存储(`encrypts :secret`,依赖 `Chatwoot.encryption_configured?`)。提供 `reset_secret!` 方法用于密钥轮换。
### 核心逻辑
```ruby
module WebhookSecretable
extend ActiveSupport::Concern
included do
has_secure_token :secret
encrypts :secret if Chatwoot.encryption_configured?
end
def reset_secret!
regenerate_secret
reload
end
end
```
- 包含此 concern 的模型:**Webhook**、**AgentBot**
- 签名使用:Webhooks::Trigger 中 `OpenSSL::HMAC.hexdigest('SHA256', secret, body)` → 生成 `x-chatwoot-signature` Header
---
## 15. Webhooks::Trigger(Webhook投递引擎)
### 功能描述
Webhooks::Trigger 是 Chatwoot 所有 Webhook 推送的核心执行引擎。负责构建 HTTP 请求、设置签名 Header、执行请求、处理失败和重试。支持三种 webhook_type:`account_webhook`、`inbox_webhook`、`agent_bot_webhook`。
### 核心逻辑
- **请求构建**:
- `SafeFetch.fetch(url, method: :post, body: payload.to_json, headers: request_headers)`
- Headers:`Content-Type: application/json`, `Accept: application/json`
- `X-Chatwoot-Delivery` — delivery_id(UUID)
- `x-chatwoot-signature` — HMAC-SHA256 签名(使用 secret + body)
- **超时**:`webhook_timeout`(根据 webhook_type 区分:agent_bot 默认更长)
- **重试机制**:
- `RetryableError` — AgentBot 场景下 HTTP 429/500 触发重试
- AgentBots::WebhookJob:`retry_on RetryableError, wait: 3.seconds, attempts: 3`
- 失败处理:`handle_failure(error)` → 日志记录
- **三种 Job**:
- `WebhookJob`(queue: medium)— account/inbox webhook
- `AgentBots::WebhookJob`(queue: high)— agent_bot webhook,支持重试
- 两者均调用 `Webhooks::Trigger.execute`
---
## 16. InstallationConfig(平台级配置)
### 功能描述
InstallationConfig 是 Chatwoot 的平台级全局配置模型,存储在数据库中,通过 GlobalConfig 缓存层提供高效读取。每个配置项由 `name`(唯一键)和 `serialized_value`(jsonb)组成。配置项分两类:`locked`(不可通过 API 修改)和 `editable`(可通过 Super Admin API 修改)。部分配置修改后需要重启应用(RESTART_REQUIRED_CONFIG_KEYS)。
### 关键配置项
| 配置名 | 说明 | 类型 |
|------|------|------|
| `ENABLE_ACCOUNT_SIGNUP` | 是否允许自助注册 | boolean |
| `ENABLE_SAML_SSO_LOGIN` | 是否启用 SAML SSO | boolean(修改前校验是否有 SAML 用户) |
| `CAPTAIN_OPEN_AI_API_KEY` | Captain AI LLM 密钥 | string(需重启) |
| `CAPTAIN_OPEN_AI_ENDPOINT` | Captain AI LLM Endpoint | string(需重启) |
| `CAPTAIN_OPEN_AI_MODEL` | Captain AI LLM 模型名 | string(需重启) |
| `LANGFUSE_BASE_URL` / `PUBLIC_KEY` / `SECRET_KEY` | Langfuse 可观测配置 | string(需重启) |
| `OTEL_PROVIDER` | OpenTelemetry Provider | string(需重启) |
| `DISPLAY_MANIFEST` | 是否展示 app manifest | boolean |
### 涉及的数据模型+关键字段
- **InstallationConfig**(`installation_configs` 表):
- `id` (bigint, PK)
- `name` (string, NOT NULL, UNIQUE) — 配置键名
- `serialized_value` (jsonb, NOT NULL) — 配置值(serialize YAML,默认 `{}`)
- `locked` (boolean, default: TRUE, NOT NULL) — 是否锁定(锁定项不可通过 API 编辑)
- `created_at`, `updated_at` (datetime)
- 索引:`index_installation_configs_on_name` (UNIQUE),`index_installation_configs_on_name_and_created_at` (UNIQUE)
- Scope:`editable` → `where(locked: false)`
- `default_scope` → `order(created_at: :desc)`
- `before_validation :set_lock` — 新建时默认锁定
- `after_commit :clear_cache` — 修改后清除 GlobalConfig Redis 缓存
- 验证:`validates :name, presence: true`; `saml_sso_users_check` — 关闭 SAML SSO 前检查是否有 SAML 用户
### 涉及的API端点
| 方法 | 路径 | 说明 |
|------|------|------|
| Super Admin CRUD | `/super_admin/installation_configs` | 管理后台管理 InstallationConfig(全量 CRUD) |
### 涉及的业务逻辑
- **value accessor** — `InstallationConfig#value` 读 `serialized_value[:value]`;`InstallationConfig#value=` 写 `{ value: ... }`
- **GlobalConfig 缓存** — 通过 Redis $alfred 缓存,1天过期,fallback 到数据库
- **GlobalConfigService.load(key, default)** — 先查 GlobalConfig 缓存,再 fallback ENV,最后创建 InstallationConfig 记录
- **配置修改需要重启的标记** — `RESTART_REQUIRED_CONFIG_KEYS` 数组
---
## 17. GlobalConfig + GlobalConfigService(配置缓存层)
### 功能描述
GlobalConfig 是 InstallationConfig 的 Redis 缓存读取层,通过 `$alfred`(Redis 连接)缓存配置值,默认1天过期。当 InstallationConfig 修改时,`after_commit :clear_cache` 自动清除缓存,确保读取到最新值。GlobalConfigService 提供便捷方法,支持 ENV fallback 和自动创建 InstallationConfig。
### GlobalConfig 核心逻辑
- **缓存键**:`V1:GLOBAL_CONFIG:{config_key}`
- **读取流程**:
1. 查 Redis 缓存 → 如有直接返回
2. 缓存不存在 → 查 InstallationConfig DB → 写入 Redis(1天过期)
- **类型转换**:通过 ConfigLoader 加载 general_configs 元数据,对 boolean 类型做 `ActiveModel::Type::Boolean.new.cast`
- **clear_cache**:扫描 `$alfred.keys("V1:GLOBAL_CONFIG:*")` 并逐个 expire(0)
### GlobalConfigService 核心逻辑
- **load(key, default)**:
1. `GlobalConfig.get(key)` → 如有值返回
2. fallback `ENV.fetch(key) { default }`
3. `InstallationConfig.where(name: key).first_or_create(value: env_value, locked: false)`
4. `GlobalConfig.clear_cache` → 下次读取走 DB
- **account_signup_enabled?** — `load('ENABLE_ACCOUNT_SIGNUP', 'false')` 转 boolean
---
## 跨模块交互关系
```
PlatformApp ──1:1──→ AccessToken (认证)
PlatformApp ──1:N──→ PlatformAppPermissible (授权)
↕ polymorphic
├── Account
├── User
└── AgentBot
AgentBot ──1:1──→ AccessToken (Bot API认证)
AgentBot ──1:1──→ Secret (Webhook签名)
AgentBot ──1:N──→ AgentBotInbox → Inbox (监听绑定)
AgentBot ──1:N──→ PlatformAppPermissible (Partner授权)
AgentBot ──1:N──→ AssignedConversations (assignee)
AgentBotListener ──→ AgentBots::WebhookJob → Webhooks::Trigger
Webhook ──1:1──→ Secret (HMAC签名)
WebhookListener ──→ WebhookJob → Webhooks::Trigger
DashboardApp ──1:N──→ Account
DashboardApp ──1:1──→ User (创建者)
DataImport ──1:1──→ Account
DataImport ──1:1──→ ImportFile (ActiveStorage)
DataImport ──1:1──→ FailedRecords (ActiveStorage)
DataImportJob ──→ DataImport::ContactManager → Contact
InstallationConfig ──→ GlobalConfig (Redis缓存) ──→ GlobalConfigService (ENV fallback)
```
---
## GoChat 实现建议
| 模块 | 优先级 | 实现要点 |
|------|--------|----------|
| AccessToken + AccessTokenable | P0 | 多态令牌,支持 PlatformApp 和 AgentBot 两种 owner;`has_secure_token` 生成唯一 token |
| PlatformApp + Permissible | P1 | Partner API 认证主体;多态授权矩阵;建议 Go 中用 interface + 泛型实现 polymorphic |
| PlatformController 鉴权 | P1 | Token → PlatformApp 解析 → Permissible 校验;建议中间件链实现 |
| AgentBot + AgentBotInbox | P1 | Bot 模型核心;AccessToken + Secret 双令牌;account_id nullable 支持全局 Bot |
| AgentBotListener | P2 | 事件驱动推送;建议 EventBus + Handler 模式 |
| AgentBots::WebhookJob | P2 | 异步推送 + 重试;建议消息队列 + 指数退避 |
| Webhook + WebhookSecretable | P1 | HMAC-SHA256 签名验证;WebhookJob 异步推送;account/inbox 两种 scope |
| Webhooks::Trigger | P2 | 统一推送引擎;签名 + 超时 + 重试 + 错误处理 |
| DashboardApp | P3 | iframe 扩展;content schema 校验;前端侧边栏 iframe 渲染 |
| DataImport | P2 | CSV 异步导入;联系人去重合并;标签批量关联;进度追踪 |
| InstallationConfig | P1 | 平台级配置 DB 存储;Redis 缓存层;ENV fallback;locked/editable 分类 |
| GlobalConfig | P2 | Redis 缓存 + DB fallback;类型转换;after_commit 清缓存 |