# M11: 企业版功能(Enterprise Features) ## 模块概述 本模块覆盖 Chatwoot 企业版专属功能,包括:SLA 策略与应用(SlaPolicy + AppliedSla + SlaEvent)、审计日志(AuditLog)、自定义角色权限系统(CustomRole)、组织/公司管理(Company)、坐席容量策略(AgentCapacityPolicy + InboxCapacityLimit)、语音通话(Call + Voice 系列 Service)、SAML SSO 单点登录(AccountSamlSettings)、SidekiqCron 定时任务调度。这些功能在社区版基础上增强了合规、安全、权限精细化、组织管理和语音通信能力。 --- ### 功能1:SLA 策略管理(SlaPolicy CRUD + 应用到对话) - **功能描述**:账户级 SLA(Service Level Agreement)策略管理。管理员可创建 SLA 策略,设定首响时间(FRT)、下次响应时间(NRT)、解决时间(RT)阈值,并将策略绑定到对话。系统通过定时 Job 定期评估 SLA 是否达标/违规,并自动生成 SlaEvent 通知。 - **用户操作流程(UI交互步骤)**: 1. 进入 Settings → SLA 页面 2. 点击"Add SLA Policy",填写名称、描述 3. 设定三个时间阈值:first_response_time_threshold(首响)、next_response_time_threshold(下次响应)、resolution_time_threshold(解决),单位为秒 4. 可勾选"only_during_business_hours",仅在工作时间内计算 SLA 5. 保存后,可在 Inbox 设置中将 SLA 策略绑定到收件箱,新进入的对话自动应用该 SLA 6. 对话列表中可查看 SLA 状态标识(active/hit/missed/active_with_misses) - **涉及的API端点 + 请求/响应格式**: - `GET /api/v1/accounts/{account_id}/sla_policies` — 获取 SLA 策略列表 - `POST /api/v1/accounts/{account_id}/sla_policies` — 创建 SLA 策略 - 请求:`{ sla_policy: { name: "Standard SLA", description: "...", first_response_time_threshold: 300, next_response_time_threshold: 600, resolution_time_threshold: 86400, only_during_business_hours: false } }` - 响应:策略对象 `{ id, name, description, first_response_time_threshold, next_response_time_threshold, resolution_time_threshold, only_during_business_hours, account_id }` - `GET /api/v1/accounts/{account_id}/sla_policies/{id}` — 获取单个 SLA 策略 - `PUT /api/v1/accounts/{account_id}/sla_policies/{id}` — 更新 SLA 策略 - `DELETE /api/v1/accounts/{account_id}/sla_policies/{id}` — 删除 SLA 策略(异步通过 DeleteObjectJob) - **涉及的数据模型 + 关键字段**: - `SlaPolicy`(表 `sla_policies`): - `id` (bigint, PK) - `name` (string, not null) — 策略名称 - `description` (string) — 策略描述 - `first_response_time_threshold` (float) — 首响时间阈值(秒) - `next_response_time_threshold` (float) — 下次响应时间阈值(秒) - `resolution_time_threshold` (float) — 解决时间阈值(秒) - `only_during_business_hours` (boolean, default false) — 仅在工作时间计算 - `account_id` (bigint, not null, FK) - `AppliedSla`(表 `applied_slas`): - `id` (bigint, PK) - `sla_status` (integer, enum) — active(0)/hit(1)/missed(2)/active_with_misses(3) - `account_id` (bigint, not null, FK) - `sla_policy_id` (bigint, not null, FK) - `conversation_id` (bigint, not null, FK) - 唯一索引:`(account_id, sla_policy_id, conversation_id)` - `SlaEvent`(表 `sla_events`): - `id` (bigint, PK) - `event_type` (integer, enum) — frt(0)/nrt(1)/rt(2) - `meta` (jsonb) — 事件元数据 - `account_id`, `applied_sla_id`, `conversation_id`, `inbox_id`, `sla_policy_id` (bigint, FK) - 关联:`SlaPolicy has_many :conversations, :applied_slas`;`AppliedSla has_many :sla_events`;`AppliedSla belongs_to :sla_policy, :conversation, :account` - **涉及的业务逻辑(service层)**: - **Sla::EvaluateAppliedSlaService** — 核心 SLA 评估逻辑: - 依次检查 FRT/NRT/RT 三个阈值 - 首响检查:`conversation.created_at + sla_policy.first_response_time_threshold`,对比首条回复时间 - 下次响应检查:等待客户回复后,对比等待时间 + 阈值 - 解决时间检查:对话创建时间 + 阈值 - 达标时调用 `handle_hit_sla`,违规时调用 `handle_missed_sla` 生成 SlaEvent - **Sla::TriggerSlasForAccountsJob** → **Sla::ProcessAccountAppliedSlasJob** → **Sla::ProcessAppliedSlaJob** — 定时评估链路,遍历所有 active/active_with_misses 状态的 AppliedSla - **rake sla:apply_to_conversations** — 批量给已有对话应用 SLA 策略的迁移任务 - **涉及的自动化/规则/事件**: - 对话创建时,如 Inbox 绑定了 SLA 策略,自动创建 AppliedSla(conversation callback) - SlaEvent 创建后自动发送通知(`after_create_commit :create_notifications`) - 对话 resolve 后,EvaluateAppliedSlaService 判断 SLA 是否最终达标 - **涉及的权限/Policy**: - `SlaPolicyPolicy`:index/show → administrator || agent;create/update/destroy → administrator - **GoChat 实现要点**: - SLA 三种阈值(FRT/NRT/RT)是核心概念,需要完整建模 - 定时评估 Job 需要设计好调度频率(Chatwoot 每5分钟通过 TriggerScheduledItemsJob 触发) - `only_during_business_hours` 需要结合 M3 的 BusinessHours 计算 - AppliedSla 状态机:active → active_with_misses → hit/missed --- ### 功能2:审计日志(AuditLog) - **功能描述**:企业版审计日志,基于 `audited` gem 记录账户内关键资源的变更操作(创建/更新/删除),包括变更内容、操作人、IP 地址等。管理员可在审计日志页面按时间倒序查看所有变更记录,用于合规审计和安全追溯。 - **用户操作流程(UI交互步骤)**: 1. 账户需启用 `audit_logs` feature flag 2. 进入账户设置 → Audit Logs 页面 3. 可按时间倒序浏览所有变更记录(每页15条) 4. 每条记录显示:操作类型(action)、被审计对象(auditable_type + auditable_id)、变更内容(audited_changes)、操作人(username/email)、IP(remote_address)、时间戳 - **涉及的API端点 + 请求/响应格式**: - `GET /api/v1/accounts/{account_id}/audit_logs` — 获取审计日志(分页) - 请求参数:`page`(页码) - 响应:审计记录列表,含 `action, auditable_type, auditable_id, audited_changes, username, remote_address, request_uuid, created_at, associated_type, associated_id` - **涉及的数据模型 + 关键字段**: - `Enterprise::AuditLog`(继承 `Audited::Audit`,表 `audits`): - `id` (bigint, PK) - `action` (string) — 操作类型:create/update/destroy - `auditable_type` (string) — 被审计对象类型(Account/Inbox/User/AccountUser 等) - `auditable_id` (bigint) — 被审计对象 ID - `audited_changes` (jsonb) — 变更内容 - `associated_type` (string) — 关联对象类型(通常是 Account) - `associated_id` (bigint) — 关联对象 ID - `user_id` (bigint) — 操作人 ID - `username` (string) — 操作人邮箱(自动填充) - `remote_address` (string) — IP 地址 - `request_uuid` (string) — 请求唯一标识 - `version` (integer) — 版本号 - `comment` (string) - `user_type` (string) - `created_at` (datetime) - `Enterprise::AuditLog` 在 `after_save` 时自动补充 `associated_type/associated_id/username` - **涉及的业务逻辑(service层)**: - 通过 `audited` gem 的 `audited` 声明自动记录变更,无需独立 service - 各模型通过 concern 声明审计范围: - `Enterprise::Audit::Account` — `audited except: :updated_at, on: [:update]` - `Enterprise::Audit::Inbox` — `audited associated_with: :account, on: [:create, :update]` - `Enterprise::Audit::AccountUser` — `audited only: [:availability, :role, :account_id, :inviter_id, :user_id], on: [:create, :update], associated_with: :account` - `Enterprise::Audit::User` — `audited only: [:availability, :display_name, :email, :name]`(手动记录登录/登出) - `Enterprise::Audit::Conversation` — `audited only: [], on: [:destroy]`(仅记录删除) - 其他:AutomationRule, Webhook, Macro, AgentBot, Team, TeamMember, InboxMember 等均有审计 concern - 全局配置:`config/initializers/audited.rb` → `config.audit_class = 'Enterprise::AuditLog'` - **涉及的自动化/规则/事件**: - 模型变更时自动写入 `audits` 表(audited gem callback) - `Enterprise::AuditLog.after_save :log_additional_information` — 自动补充关联信息和操作人邮箱 - **涉及的权限/Policy**: - `Api::V1::Accounts::AuditLogsController` — 仅 administrator 可访问(`before_action :check_admin_authorization?`) - 需要账户启用 `audit_logs` feature flag - **GoChat 实现要点**: - 审计日志需基于变更追踪机制(类似 audited gem),在核心模型上声明审计字段 - 需记录操作人、IP、变更内容、时间戳 - 分页查询接口,按时间倒序排列 - associated_type/associated_id 用于按账户维度聚合审计记录 - feature flag 控制审计功能的启用 --- ### 功能3:自定义角色权限系统(CustomRole) - **功能描述**:企业版自定义角色系统,允许管理员在账户级创建自定义角色,定义细粒度权限列表,并将角色分配给 AccountUser。自定义角色扩展了默认的三级角色体系(administrator/agent/supervisor),使权限控制更灵活。 - **用户操作流程(UI交互步骤)**: 1. 进入 Settings → Custom Roles 页面 2. 点击"Add Role",填写角色名称、描述 3. 从权限列表勾选所需权限 4. 创建后可在 Agent Management 中将此角色分配给坐席 5. 坐席权限将基于其 CustomRole 的 permissions 列表生效 - **涉及的API端点 + 请求/响应格式**: - `GET /api/v1/accounts/{account_id}/custom_roles` — 获取自定义角色列表 - `POST /api/v1/accounts/{account_id}/custom_roles` — 创建自定义角色 - 请求:`{ custom_role: { name: "Sales Agent", description: "...", permissions: ["conversation_manage", "contact_manage"] } }` - 响应:角色对象 `{ id, name, description, permissions, account_id }` - `GET /api/v1/accounts/{account_id}/custom_roles/{id}` — 获取单个角色 - `PUT /api/v1/accounts/{account_id}/custom_roles/{id}` — 更新角色 - `DELETE /api/v1/accounts/{account_id}/custom_roles/{id}` — 删除角色 - **涉及的数据模型 + 关键字段**: - `CustomRole`(表 `custom_roles`): - `id` (bigint, PK) - `name` (string) — 角色名称 - `description` (string) — 角色描述 - `permissions` (text array, default []) — 权限列表 - `account_id` (bigint, not null, FK) - 可用权限列表(`PERMISSIONS` 常量): - `conversation_manage` — 管理所有对话 - `conversation_unassigned_manage` — 管理未分配对话并可认领 - `conversation_participating_manage` — 管理自己参与的对话 - `contact_manage` — 管理联系人 - `report_manage` — 管理报表 - `knowledge_base_manage` — 管理知识库 - 关联:`CustomRole has_many :account_users, dependent: :nullify` - `Enterprise::AccountUser` 扩展:`permissions` 方法返回 `custom_role.permissions + ['custom_role']`(当有自定义角色时),否则返回默认角色权限 - **涉及的业务逻辑(service层)**: - 无独立 service,CRUD 在 controller 完成 - 权限合并逻辑在 `Enterprise::AccountUser#permissions` 中 - 删除 CustomRole 时,关联的 AccountUser 的 custom_role_id 设为 NULL(nullify),回归默认角色权限 - **涉及的自动化/规则/事件**: - AccountUser 权限检查时,如存在 custom_role 则优先使用其 permissions 列表 - CustomRole 删除后,AccountUser 自动回退到默认角色 - **涉及的权限/Policy**: - `CustomRolePolicy`:所有操作(index/show/create/update/destroy)仅 administrator 可执行 - **GoChat 实现要点**: - 权限列表需设计为可扩展的枚举/常量体系 - AccountUser 需支持 custom_role_id 外键 - 权限合并逻辑:CustomRole.permissions + 默认角色权限 - 删除角色时注意 nullify 而非 restrict_with_error - 当前6项权限可能需要扩展(如 automation_manage、inbox_manage 等) --- ### 功能4:组织/公司管理(Company) - **功能描述**:企业版组织管理功能,允许账户创建"公司"(Company)实体,将联系人按组织分组。每个公司有名称、域名、描述、自定义属性,支持头像和 favicon 自动获取。联系人可归属于公司,系统可根据联系人邮箱域名自动关联公司。 - **用户操作流程(UI交互步骤)**: 1. 进入 Contacts → Companies 页面(需启用 `companies` feature flag) 2. 点击"Add Company",填写名称、域名、描述 3. 可上传公司头像 4. 如填入域名,系统自动获取 favicon 5. 创建后可将联系人手动归属到该公司 6. 系统也可根据联系人邮箱自动关联(业务邮箱检测 → 域名匹配 → 自动建公司) 7. 在公司详情页可查看关联联系人列表、活动时间线 - **涉及的API端点 + 请求/响应格式**: - `GET /api/v1/accounts/{account_id}/companies` — 获取公司列表(分页25条/页,支持排序/过滤) - `GET /api/v1/accounts/{account_id}/companies/search?q=xxx` — 搜索公司(按名称/域名) - `POST /api/v1/accounts/{account_id}/companies` — 创建公司 - 请求:`{ company: { name: "Acme Corp", domain: "acme.com", description: "...", additional_attributes: {}, custom_attributes: {} } }` - 响应:公司对象 `{ id, name, domain, description, contacts_count, last_activity_at, additional_attributes, custom_attributes, account_id }` - `GET /api/v1/accounts/{account_id}/companies/{id}` — 获取单个公司 - `PUT /api/v1/accounts/{account_id}/companies/{id}` — 更新公司(custom_attributes 为 merge 逻辑) - `DELETE /api/v1/accounts/{account_id}/companies/{id}` — 删除公司(仅 administrator) - `DELETE /api/v1/accounts/{account_id}/companies/{id}/destroy_custom_attributes` — 删除指定自定义属性 - `POST /api/v1/accounts/{account_id}/companies/{id}/avatar` — 上传/更新头像 - **涉及的数据模型 + 关键字段**: - `Company`(表 `companies`): - `id` (bigint, PK) - `name` (string, not null, 最大255字符) — 公司名称 - `domain` (string, 可选, 同账户下唯一) — 公司域名 - `description` (text, 最大限制) — 公司描述 - `contacts_count` (integer) — 关联联系人数(counter cache) - `last_activity_at` (datetime) — 最后活动时间 - `additional_attributes` (jsonb) — 附加属性 - `custom_attributes` (jsonb) — 自定义属性(merge 语义) - `account_id` (bigint, not null, FK) - 唯一索引:`(account_id, domain) WHERE domain IS NOT NULL` - 关联:`Company has_many :contacts, dependent: :nullify`;`Company include Avatarable` - **涉及的业务逻辑(service层)**: - **Companies::BusinessEmailDetectorService** — 检测邮箱是否为业务邮箱(排除 disposable/free provider),用于自动关联判断 - **Companies::ContactMembershipService** — 公司-联系人关联管理: - `assign(contact:)` — 将联系人归属公司,更新 company.last_activity_at - `remove(contact:)` — 移除联系人归属 - **Contacts::CompanyAssociationService** — 自动根据邮箱域名关联公司: - 检测是否为业务邮箱 → 提取域名 → find_or_create_by(domain) → 自动归属 - Company model: - `after_create_commit :fetch_favicon` — 域名存在时异步获取 favicon - `record_activity_at!(timestamp)` — 滚动更新 last_activity_at(5分钟间隔去重) - `search_by_name_or_domain(query)` — 名称/域名搜索 scope - **涉及的自动化/规则/事件**: - 创建公司时如有域名,自动获取 favicon - 联系人创建/更新时,如邮箱为业务邮箱,自动关联对应公司 - last_activity_at 滚动更新(ACTIVITY_ROLLUP_INTERVAL = 5分钟) - **涉及的权限/Policy**: - `CompanyPolicy`:index/search/show/create/update → 所有账户用户;destroy → 仅 administrator - **GoChat 实现要点**: - Company 是联系人分组维度,需与 Contact 模型建立 belongs_to 关系 - custom_attributes 需支持 merge 语义(更新时合并而非替换) - domain 唯一约束(同账户下)需注意 NULL domain 不参与唯一检查 - 自动关联逻辑:邮箱域名 → 业务邮箱检测 → find_or_create Company → assign Contact - contacts_count counter cache 需维护 - 需启用 `companies` feature flag --- ### 功能5:坐席容量策略(AgentCapacityPolicy + InboxCapacityLimit) - **功能描述**:企业版坐席容量策略,用于限制每个坐席在特定收件箱中的最大同时处理对话数。管理员创建容量策略,设定排除规则(如超过N小时的老对话不计入容量、特定标签对话不计入),并为策略关联收件箱及各自的对话上限。 - **用户操作流程(UI交互步骤)**: 1. 进入 Settings → Agent Capacity Policies 页面 2. 点击"Add Policy",填写策略名称、描述 3. 设定排除规则:exclude_older_than_hours(超时老对话排除)、excluded_labels(特定标签对话排除) 4. 为策略关联收件箱,并设定每个收件箱的 conversation_limit(对话上限) 5. 将策略分配给坐席(AccountUser.agent_capacity_policy_id) - **涉及的API端点 + 请求/响应格式**: - `GET /api/v1/accounts/{account_id}/agent_capacity_policies` — 获取容量策略列表 - `POST /api/v1/accounts/{account_id}/agent_capacity_policies` — 创建容量策略 - 请求:`{ agent_capacity_policy: { name: "Standard Capacity", description: "...", exclusion_rules: { exclude_older_than_hours: 24, excluded_labels: ["spam"] } } }` - 响应:策略对象 `{ id, name, description, exclusion_rules, account_id }` - `GET /api/v1/accounts/{account_id}/agent_capacity_policies/{id}` — 获取单个策略 - `PUT /api/v1/accounts/{account_id}/agent_capacity_policies/{id}` — 更新策略 - `DELETE /api/v1/accounts/{account_id}/agent_capacity_policies/{id}` — 删除策略 - **涉及的数据模型 + 关键字段**: - `AgentCapacityPolicy`(表 `agent_capacity_policies`): - `id` (bigint, PK) - `name` (string, 最大255字符, not null) — 策略名称 - `description` (text) — 策略描述 - `exclusion_rules` (jsonb, not null) — 排除规则: - `exclude_older_than_hours` — 超过N小时的活跃对话不计入容量 - `excluded_labels` — 带特定标签的对话不计入容量 - `account_id` (bigint, not null, FK) - `InboxCapacityLimit`(表 `inbox_capacity_limits`): - `id` (bigint, PK) - `agent_capacity_policy_id` (bigint, not null, FK) - `inbox_id` (bigint, not null, FK) - `conversation_limit` (integer, not null, ≥ 0) — 该收件箱的对话上限 - 唯一索引:`(agent_capacity_policy_id, inbox_id)` - 关联: - `AgentCapacityPolicy has_many :inbox_capacity_limits, :inboxes (through), :account_users (dependent: :nullify)` - `AccountUser` 增加 `agent_capacity_policy_id` 外键 - **涉及的业务逻辑(service层)**: - 无独立 service,CRUD 在 controller 完成 - 容量计算逻辑(在自动分配系统中使用): - 遍历坐席的所有 inbox_capacity_limits - 计算每个收件箱的活跃对话数(排除规则过滤) - 判断是否达到上限 - **涉及的自动化/规则/事件**: - 删除策略时,关联的 AccountUser 的 agent_capacity_policy_id 被设为 NULL - 容量策略在自动分配时作为坐席可分配判断依据 - **涉及的权限/Policy**: - `AgentCapacityPolicyPolicy`:所有操作仅 administrator 可执行 - **GoChat 实现要点**: - exclusion_rules 作为 jsonb 存储,灵活支持扩展 - InboxCapacityLimit 是策略和收件箱的中间表,需唯一约束 - 容量计算需排除规则过滤(超时对话 + 标签排除) - 与自动分配系统(M5)深度集成 - AccountUser 需增加 agent_capacity_policy_id 字段 --- ### 功能6:语音通话(Call + Voice Services) - **功能描述**:企业版语音通话功能,支持通过 Twilio 和 WhatsApp 两种通话提供商进行语音通话。支持呼入/呼出通话,基于 Twilio Conference 实现坐席接听/拒接/挂断,通话状态实时更新,支持录音和转录。呼出通话在对话中创建 voice_call 类型消息气泡。 - **用户操作流程(UI交互步骤)**: 1. 呼出通话:在联系人详情页点击"Call",选择语音收件箱 → 系统发起 Twilio/WhatsApp 通话 2. 呼入通话:客户拨打 Twilio 号码 → 系统创建 Call + 对话 → 坐席看到 ringing 状态 3. 坐席接听:点击接听按钮 → 通话状态变为 in_progress → 坐席被自动分配到对话 4. 通话结束:任一方挂断 → 状态变为 completed/no_answer/failed → 记录时长 5. 录音/转录:通话完成后可查看录音和转录文本 - **涉及的API端点 + 请求/响应格式**: - `POST /api/v1/accounts/{account_id}/contacts/{contact_id}/calls` — 发起呼出通话 - 请求:`{ inbox_id: 1, conversation_id: 42 }`(可选关联现有对话) - 响应:`{ conversation_id, inbox_id, call_sid, conference_sid }` - `POST /api/v1/accounts/{account_id}/whatsapp_calls/initiate` — WhatsApp 呼出 - `GET /api/v1/accounts/{account_id}/whatsapp_calls/{id}` — 获取通话详情 - `POST /api/v1/accounts/{account_id}/whatsapp_calls/{id}/accept` — 接听 - `POST /api/v1/accounts/{account_id}/whatsapp_calls/{id}/reject` — 拒接 - `POST /api/v1/accounts/{account_id}/whatsapp_calls/{id}/terminate` — 挂断 - `POST /api/v1/accounts/{account_id}/whatsapp_calls/{id}/upload_recording` — 上传录音 - Twilio Webhook 端点: - `POST /twilio/voice/{phone}/status` — 通话状态回调 - `POST /twilio/voice/{phone}/call_twiml` — TwiML 响应 - `POST /twilio/voice/{phone}/conference_status` — 会议状态回调 - `POST /twilio/voice/{phone}/recording_status` — 录音状态回调 - **涉及的数据模型 + 关键字段**: - `Call`(表 `calls`): - `id` (bigint, PK) - `direction` (integer, enum) — incoming(0)/outgoing(1) - `status` (string, default "ringing") — ringing/in_progress/completed/no_answer/failed - `provider` (integer, enum) — twilio(0)/whatsapp(1) - `duration_seconds` (integer) — 通话时长(秒) - `end_reason` (string) — 结束原因 - `started_at` (datetime) — 开始时间 - `transcript` (text) — 转录文本 - `meta` (jsonb) — conference_sid, twilio_conference_sid, recording_sid, parent_call_sid, initiated_at, ended_at - `provider_call_id` (string, not null) — 提供商侧通话 ID(唯一约束) - `accepted_by_agent_id` (bigint) — 接听坐席 ID - `account_id`, `contact_id`, `conversation_id`, `inbox_id`, `message_id` (bigint, FK) - `recording` (ActiveStorage attachment) — 录音文件 - TERMINAL_STATUSES = completed/no_answer/failed - 关联:`Call belongs_to :account, :inbox, :conversation, :contact, :message (optional), :accepted_by_agent (User, optional)` - **涉及的业务逻辑(service层)**: - **Voice::OutboundCallBuilder** — 呼出通话构建: - 事务性创建 ContactInbox → Conversation → Call → Message(voice_call) - 通过 `inbox.channel.initiate_call` 发起 Twilio 通话 - **Voice::InboundCallBuilder** — 呼入通话构建: - 查找或创建 ContactInbox → Contact → Conversation → Call → Message - 支持并发防重复(RecordNotUnique 处理) - **Voice::StatusUpdateService** — 通话状态更新: - Twilio 状态映射:queued/initiated/ringing → ringing; in-progress/answered → in_progress; completed → completed; busy/no-answer → no_answer; failed/canceled → failed - **Voice::CallStatus::Manager** — 通话状态机管理: - 处理状态更新,记录 started_at/duration_seconds - 通话结束时更新 meta、时长 - 触发 message.touch 使前端实时更新 - **Voice::Conference::Manager** — 会议事件处理: - conference-start → ringing; participant-join → in_progress + 自动分配坐席; participant-leave → 处理离开; conference-end → finalize - 坐席接听时自动 claim_for_user + auto_assign_conversation - **Voice::Provider::Twilio::ConferenceService** — Twilio 会议管理: - ensure_conference_sid, mark_agent_joined, end_conference - claim_call + assign_conversation(首次接听获胜) - **Voice::Provider::Twilio::TokenService** — 生成 Twilio Access Token(JWT)+ Voice Grant - **Voice::CallMessageBuilder** — 创建/更新 voice_call 类型消息: - content_type: 'voice_call', 包含 call_id/call_sid/call_source/call_direction/status - **Voice::RecordingStatusService** — 处理 Twilio 录音回调,异步附件化录音文件 - **Whatsapp::CallService** — WhatsApp 通话操作(accept/reject/terminate) - **Whatsapp::IncomingCallService** — WhatsApp 呼入处理 - **Whatsapp::CallPermissionReplyService** — WhatsApp 通话权限回复 - **涉及的自动化/规则/事件**: - 通话创建时自动在对话中插入 voice_call 消息气泡 - 坐席接听时自动 claim_call + auto_assign(如对话无现有 assignee) - 通话结束时更新 duration_seconds、transcript - 录音状态回调异步处理附件 - Twilio Conference 事件实时驱动状态更新 - **涉及的权限/Policy**: - 呼出通话:需 authorize contact (:show) + inbox (:show),且 inbox 需启用 voice - WhatsApp 通话:需确保 calling_enabled + SDP offer + contact phone - **GoChat 实现要点**: - 通话模型需支持多 provider(Twilio/WhatsApp)和方向(呼入/呼出) - 状态机:ringing → in_progress → completed/no_answer/failed - Conference 模式:坐席和客户通过会议桥连接 - 录音附件化处理需异步 - 通话消息(voice_call content_type)与对话系统集成 - Twilio Access Token 生成(JWT + VoiceGrant) - 需 feature flag 控制 voice 通话功能启用 --- ### 功能7:SAML SSO 单点登录(AccountSamlSettings) - **功能描述**:企业版 SAML 2.0 单点登录,允许账户配置 SAML IdP 参数,用户通过企业身份提供商(如 Okta、Azure AD)登录 Chatwoot。支持角色映射(SAML 属性 → Chatwoot 角色),创建/更新 SAML 配置后自动将账户用户 provider 转为 saml。 - **用户操作流程(UI交互步骤)**: 1. 管理员进入 Settings → SAML SSO 页面(需启用 `saml` feature flag + 全局 ENABLE_SAML_SSO_LOGIN) 2. 填写 IdP 参数:SSO URL、Certificate(X509)、IdP Entity ID 3. SP Entity ID 自动生成(基于 FRONTEND_URL + account_id) 4. 可配置 role_mappings(SAML 属性 → Chatwoot 角色) 5. 保存后,账户的登录页面出现 SAML 登录按钮 6. 用户点击 SAML 登录 → 重定向到 IdP → 认证成功后回调 Chatwoot 7. SamlUserBuilder 查找或创建用户,关联到账户 - **涉及的API端点 + 请求/响应格式**: - `GET /api/v1/accounts/{account_id}/saml_settings` — 获取 SAML 配置 - `POST /api/v1/accounts/{account_id}/saml_settings` — 创建 SAML 配置 - 请求:`{ saml_settings: { sso_url: "https://idp.example.com/saml/sso", certificate: "X509 cert string", idp_entity_id: "idp_entity", role_mappings: { "admin" => "administrator" } } }` - 响应:SAML 配置对象 - `PUT /api/v1/accounts/{account_id}/saml_settings` — 更新配置 - `DELETE /api/v1/accounts/{account_id}/saml_settings` — 删除配置(重置用户 provider 为 email) - **涉及的数据模型 + 关键字段**: - `AccountSamlSettings`(表 `account_saml_settings`): - `id` (bigint, PK) - `sso_url` (string, not null) — IdP SSO URL - `certificate` (text, not null) — X509 证书(需验证有效性) - `idp_entity_id` (string, not null) — IdP Entity ID - `sp_entity_id` (string) — SP Entity ID(自动生成) - `role_mappings` (json) — SAML 角色映射配置 - `account_id` (bigint, not null, FK, 唯一) - 关联:`AccountSamlSettings belongs_to :account`;Account `has_one :saml_settings` - **涉及的业务逻辑(service层)**: - **SamlUserBuilder** — SAML 认证后用户构建: - 查找已有用户(by email)→ 如用户属于账户则直接登录 - 不属于账户则抛出 AuthenticationFailed - 新用户创建时设置 provider: 'saml', uid, 自动确认邮箱 - 角色映射:根据 SAML 属性 + role_mappings 配置分配 AccountUser 角色 - **Enterprise::DeviseOverrides::OmniauthCallbacksController** — SAML 认证回调处理: - `redirect_callbacks` → SAML provider 时走 `omniauth_success` - `handle_saml_auth` → 检查 SAML 是否启用 → SamlUserBuilder → 登录/错误处理 - 支持 mobile 和 web 两种 RelayState 处理 - AccountSamlSettings: - `certificate_must_be_valid_x509` — 验证证书格式 - `certificate_fingerprint` — 计算证书指纹 - `set_sp_entity_id` — 自动生成 SP Entity ID(FRONTEND_URL/saml/sp/{account_id}) - `after_create_commit :update_account_users_provider` — 创建后将用户 provider 转为 saml - `after_destroy_commit :reset_account_users_provider` — 删除后重置为 email - **涉及的自动化/规则/事件**: - SAML 配置创建后,账户用户 provider 自动转为 'saml' - SAML 配置删除后,用户 provider 重置为 'email' - SP Entity ID 自动生成 - 证书有效性验证 - **涉及的权限/Policy**: - `AccountSamlSettingsPolicy`:所有操作仅 administrator 可执行 - 需启用 `saml` feature flag + 全局 ENABLE_SAML_SSO_LOGIN - **GoChat 实现要点**: - 需集成 SAML 2.0 协议(可参考 ruby-saml 或类似库) - X509 证书验证和指纹计算 - SP Entity ID 自动生成逻辑 - 用户查找/创建/角色映射需事务性处理 - Omniauth 回调流程适配 - 账户级 SAML 配置(每个账户独立 IdP) - role_mappings 灵活映射 SAML 属性到 Chatwoot 角色 --- ### 功能8:SidekiqCron 定时任务调度 - **功能描述**:Chatwoot 使用 sidekiq-cron gem 管理 Sidekiq 定时任务。通过 `config/schedule.yml` 定义任务计划,在服务启动时自动加载并持久化到 Redis。定时任务涵盖系统巡检、SLA 评估触发、IMAP 邮件收取、数据清理、报表生成等。 - **用户操作流程(UI交互步骤)**: - 此功能为后台运维功能,无用户 UI 交互 - 管理员可在 Sidekiq Web UI(/sidekiq/cron)查看和管理定时任务 - **涉及的数据模型 + 关键字段**: - 无独立数据模型,任务定义存储在 `config/schedule.yml` 和 Redis 中 - **核心定时任务列表**: - `trigger_scheduled_items_job` — */5 * * * *(每5分钟)— 触发 SLA 评估等调度任务 - `trigger_hourly_scheduled_items_job` — 0 * * * *(每小时)— 低频定时任务触发 - `trigger_imap_email_inboxes_job` — */1 * * * *(每分钟)— IMAP 邮件收取 - `internal_check_new_versions_job` — 0 0 * * *(每日 00:00 UTC)— 版本检查 + 日级调度 - `remove_stale_contact_inboxes_job` — 30 22 * * *(每日 22:30 UTC)— 清理过期 ContactInbox - `remove_stale_redis_keys_job` — 30 22 * * *(每日 22:30 UTC)— 清理过期 Redis Key - SLA 相关:通过 `TriggerScheduledItemsJob` → `Sla::TriggerSlasForAccountsJob` 链路触发 - **涉及的业务逻辑**: - `config/initializers/sidekiq.rb` — Sidekiq 服务启动时: - 加载 `config/schedule.yml` - `Sidekiq::Cron::Job.load_from_hash!` — upsert 任务并清理已删除条目 - 显式清理 legacy 动态任务(如 bulk_auto_assignment_job) - `TriggerScheduledItemsJob` — 核心调度入口,每5分钟执行,触发 SLA 评估等子任务 - `Internal::TriggerDailyScheduledItemsJob` — 日级调度 - `Internal::TriggerHourlyScheduledItemsJob` — 小时级调度 - **涉及的自动化/规则/事件**: - 服务启动时自动加载 schedule.yml 到 Redis - 部署时自动清理已删除的 schedule 条目 - 任务执行日志(Sidekiq logger) - **GoChat 实现要点**: - 如使用 Go 实现,需设计定时任务调度框架(可参考 cron 表达式 + 分布式锁) - SLA 评估链路:定时触发 → 遍历账户 → 遍历 AppliedSla → EvaluateAppliedSlaService - 任务需要幂等性(防止重复执行) - 需考虑分布式环境下的任务竞争(Redis 锁 / DB 锁) - 任务配置建议外部化(YAML / DB 配置),支持动态调整 - 清理类任务(stale data)需在低峰时段执行 --- ## 模块间依赖关系 | 依赖模块 | 依赖说明 | |---------|---------| | M1-Account | SLA/CustomRole/Company/AgentCapacityPolicy/SAML 均属于 Account 级别;AuditLog 的 associated 关联 Account | | M3-Conversation | SLA AppliedSla/SlaEvent 关联 Conversation;Call 关联 Conversation | | M4-Contact | Company 关联 Contact(belongs_to);Call 关联 Contact;CompanyAssociationService 自动关联 | | M5-Team/Assignment | AgentCapacityPolicy 与自动分配系统集成;Call 接听时 auto_assign_conversation | | M2-Inbox | SlaPolicy 绑定到 Inbox;InboxCapacityLimit 关联 Inbox;Call 关联 Inbox;Voice 需 Inbox 启用 voice | | M6-Automation | Audit 审计 AutomationRule 变更 | | M8-Notification | SlaEvent 创建后发送通知 | | BusinessHours | SLA only_during_business_hours 依赖工作时间计算 | --- ## Feature Flags 依赖 | Feature Flag | 所属功能 | 说明 | |-------------|---------|-----| | `audit_logs` | AuditLog | 控制审计日志功能启用 | | `companies` | Company | 控制组织管理功能启用 | | `saml` | SAML SSO | 控制 SAML 单点登录启用 | | `voice`(隐含) | Call/Voice | 控制语音通话功能启用 | | `sla`(隐含) | SLA | 控制 SLA 功能启用 | --- ## 企业版功能全景总结 Chatwoot 企业版功能通过 Feature Flags 逐项控制启用,每个功能模块独立可配。核心设计模式: 1. **Concern 扩展**:Enterprise::Concerns::Account 通过 `included` 块注入 `has_many :sla_policies, :custom_roles, :agent_capacity_policies, :companies, :calls, has_one :saml_settings` 等关联 2. **Policy 红线**:所有企业版功能的写操作(create/update/destroy)均需 administrator 权限 3. **审计追踪**:Enterprise::AuditLog 继承 Audited::Audit,全局配置审计类,核心模型通过 concern 声明审计范围 4. **Feature Flag 门控**:每个企业版功能都有对应的 feature flag,controller 层 before_action 检查 5. **定时调度**:SLA 评估等周期性任务通过 SidekiqCron + TriggerScheduledItemsJob 链路触发 6. **异步处理**:SLA 评估、录音附件、favicon 获取等均通过 Sidekiq Job 异步执行