637 lines
34 KiB
Markdown
637 lines
34 KiB
Markdown
# 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 清缓存 | |