自定义 CRM(通用 API)中的错误处理、重试和调用顺序

本文档介绍了当您的 CRM 端点返回 HTTP 200 "Success." 以外的响应时,CCAI Platform 的自定义 CRM(通用 API)功能的行为。本文档旨在为配置 API 账号和 API 记录端点的集成商提供行为参考。

执行顺序和关系概览

当互动触发自定义 CRM 集成时,平台会按以下顺序调用您的端点:

  1. 账号级操作(API 账号)- 联系人

    示例:

    • 查找联系人。

    • 创建联系人。

    • 更新联系人。

  2. 记录级操作(API 记录)- 工单或支持请求

    示例:

    • 查找票券。

    • 创建工单。

    • 更新工单。

  3. 会话后操作(如果已配置)

    示例:

    • 添加评论或备注。

    • 上传附件或转写内容。

    • 应用收尾更新。

前面的步骤通常会生成后续步骤可能使用的 ID。例如,如果 contact_id 包含在端点载荷模板中,则可以将其传递给工单创建操作;在典型的通用 API 设置中,ticket_id 是会话后操作所必需的。请注意,并非每个端点都配置为提取 ID - 请参阅重要细微差别:2xx + 提取 ID 的端点上缺少 ID。

重要细微差别:2xx + 提取 ID 的端点上缺少 ID

并非每个自定义 CRM 端点都会从收到的响应中提取 ID。此行为仅适用于配置了 response_data_location 和(对于创建端点)custom_id_location 的端点,通常是查找端点(提取现有记录的 ID)和创建端点(提取新创建记录的 ID)。评论、附件和更新等端点不会从其响应中提取 ID。

缺少 ID 对提取 ID 的端点的影响

  • 对于查找端点:没有匹配记录(因此没有可提取的 ID)的 200 OK 响应是正常的“未找到”行为。如果您的实例启用了 skip_account_creation 或 skip_record_creation,平台会故意继续向下游传递 null contact_id 或 ticket_id,而不是尝试创建。这是这些配置下的预期行为,并非配置错误。

  • 对于创建端点:如果 200 OK 响应在配置的位置不包含新创建的 ID,则属于配置或响应协定问题。HTTP 请求成功,因此不会触发重试,但下游步骤可能会在数据缺失的情况下继续执行。

  • 在这两种情况下,故障(如果是故障)都会显示为数据缺失,而不是端点错误,这在调试期间可能会造成混淆。

集成商指南

  • 对于创建操作,请确保在配置的位置返回预期 ID,而不仅仅是返回 2xx。将缺失的 ID 视为值得调查的配置或响应协定问题。

  • 对于查找操作,返回 200 OK 且不匹配是可以接受的。如果您的实例有意依赖“不创建即查找”模式(例如 skip_account_creation = true),请记录该决策,以便下游端点知道如何妥善处理缺失的 ID。

  • 在测试中,请验证以下内容:

    • 对于创建端点,ID 会可靠地出现在响应中,并且位于自定义 CRM 中配置的确切位置。

    • 对于查询端点,平台的下游行为与实例的跳过标志配置一致。

错误处理:概览

当端点返回非成功响应(非 2xx)或不响应时:

  • 平台可能会重试请求:

    • 对于某些状态,会立即发送一次。

    • 对于可重试的失败,重试时间会更长。

  • 当平台继续重试上游步骤时,下游步骤可能会延迟。

  • 某些失败会被视为不可重试(例如,配置或验证失败)。

重试和退避行为中介绍了详细的重试规则。链式调用和依赖项规则中介绍了故障与下游调用之间的关系。

链接和依赖项规则

自定义 CRM 行为由实例的 CRM 配置(具体而言,即跳过 CRM 账号和记录创建中记录的设置)控制。本文档通过后端配置名称(skip_account_creation、skip_account_lookup、skip_record_creation)来引用这些设置,因为它们在以下 JSON 配置示例中就是这样显示的。

在这些设置中,平台倾向于在可能的情况下继续执行链(例如,在查找失败且启用了账号创建功能时创建匿名联系人),而不是始终“硬停止”。

账号故障对记录操作的影响

即使账号步骤未生成联系人,记录步骤(工单/支持请求)也可能会运行,但确切行为取决于实例的配置。

根据您的配置,该平台可以:

  • 创建匿名联系人作为后备,然后继续创建工单。

  • 如果账号查找或账号创建配置为跳过,则继续执行记录步骤,而不使用 contact_id。

这对您的端点有何影响

  • 创建工单时缺少 contact_id 通常表示实例配置中停用了账号查找或账号创建功能。

  • 如果您的实例需要账号,请将缺失的 contact_id 视为值得调查的信号,而不是在没有联系人的情况下静默插入工单。

  • 如果您的配置有意允许在没有联系人的情况下创建工单,请确保您的工单端点以明确且可预测的方式处理这种情况。

记录失败对会话后操作的影响

对于配置了工单查询的通用 API 设置,在 ticket_id 存在之前,平台不会分派会话后调用,例如 call_finished、chat_finished、email_finished、save_call_recording、save_chat_history、update_ticket 和 done_ticket。

如果在没有 ticket_id 的情况下调用会话后终结点,通常表示存在以下情况之一:

  • 实例配置已停用票券查询功能,这在典型的通用 API 设置中并不常见。

  • 载荷模板省略了 ticket_id 变量,这是一个值得调查的配置问题。

这对您的端点有何影响

  • 如果会话后调用中缺少 ticket_id,应将其视为值得调查的配置信号,而不是默默接受的预期行为。

  • 如果您的配置有意不要求进行票券查找,请将您的会话后处理端点配置为妥善处理缺失的 ticket_id。例如,记录问题并执行空操作,或者返回清晰、结构良好的错误消息,而不是崩溃。

重试和退避行为

重试发生在三个层级:

  1. HTTP 级立即重试(快速进行第二次尝试)。

  2. 适配器级重试(针对网络故障和特定瞬时响应的快速恢复)。

  3. 命令级重试(持续数天的长尾重试)。

HTTP 级立即重试(快速重试)

对于某些暂时性状态,平台最多会尝试 2 次(1 次初始尝试 + 1 次立即重试)。这通常适用于以下错误状态:

  • 408、413、429、500、502、503、504、521、522和 524。

适配器级重试(快速网络恢复)

在 HTTP 调用和命令级时间表之间,CRM 适配器 (command_runner.js) 中的中间重试层会处理根本不产生 HTTP 响应或与特定瞬时消息匹配的故障:

  • 立即重新排队:由 TCP/套接字级错误触发 - ECONNRESET、ENOTFOUND、ESOCKETTIMEDOUT、ETIMEDOUT、ECONNREFUSED、EHOSTUNREACH、EPIPE、EAI_AGAIN。这些是在收到 HTTP 响应之前发生的网络故障。

  • 延迟 30 秒重试:当出错提示为 "Too Early" (HTTP 425)、"Service Unavailable" 或 "Unordered Collection" 时触发。

  • 此层级不可重试:所有其他错误都会进入命令级重试时间表(第 3 层)。

为什么这对集成商至关重要

当平台无法与您的端点建立或维持 TCP 连接时,就会出现这些错误。这意味着,这些错误通常表示您的服务器存在问题(服务器宕机、拒绝连接或在请求过程中断开连接),而不仅仅是内部基础设施问题。实际效果是,某些网络故障的恢复速度比 7 天命令级时间表快得多,通常在几秒钟内即可恢复。

命令级重试(约 7 天,14 次尝试)

如果操作失败且可以重试,平台可能会在约 7 天内重试最多 14 次,具体时间安排如下:

  10s → 1m → 5m → 30m → 1h → 6h → 6h → 12h → 1d → 1d → 1d → 1d → 1d → 1d

这种长尾重试行为可应用于:

  • 账号操作。

  • 记录操作。

  • 会话后操作。

不可重试的失败

某些失败会被视为不可重试,例如:

  • 配置或验证失败(例如,未配置或未正确配置所需的端点)。

  • 平台检测到的某些内部错误类别。

如果您需要确保将失败视为永久性失败(不重试),请与支持团队或实施团队联系,确认在您的特定设置中指示不可重试失败的正确方法。

超时和网络故障

该平台会处理两条不同的路径:

  1. 应用级超时:当超出配置的 HTTP 超时(请求已发送但未收到响应)时,平台会引发内部错误,可在命令级(第 3 层)重试。这意味着请求遵循标准退避时间表(在约 7 天内最多重试 14 次)。

  2. TCP/套接字级错误:当连接在收到 HTTP 响应之前在网络级失败时(例如 ETIMEDOUT、ESOCKETTIMEDOUT 和 ECONNREFUSED),适配器层(第 2 层)会捕获这些错误并立即重新将命令加入队列。从这些故障中恢复通常要快得多,通常在几秒钟内,而不是遵循 7 天的时间表。

为什么区分两者很重要

  • 应用超时(您的服务器接受了连接,但未及时响应)重试速度较慢。

  • 网络级故障(根本无法访问您的服务器)的重试速度要快得多。

  • 在这两种情况下,请求可能都已得到部分处理,因此端点应具有幂等性 - 请参阅构建弹性端点(幂等性和设计提示)。

状态代码和超时行为矩阵(附示例)

下表和示例介绍了 CCAI Platform 如何处理不同的 HTTP 状态代码、超时和网络故障。

结果摘要表

下表中的 ID 相关行仅适用于配置为从响应中提取 ID 的端点,例如查找或创建端点。大多数其他端点不执行响应 ID 提取。

结果 示例 立即进行 HTTP 重试? 长尾重试(约 7 天)? 对下游的影响
ID 提取端点成功 2xx + 配置的响应位置中存在预期 ID。 否 否 下游系统会使用提取的 ID 继续处理。
查找无匹配项 2xx,但在配置的位置未找到任何记录。 否 否 正常的“未找到”行为。启用 skip_account_creation 或 skip_record_creation 后,下游会故意继续使用 null ID。
创建不带 ID 的返回 2xx 2xx,但新记录的 ID 在配置的位置缺失。 否 否 下游可能会在数据缺失的情况下继续。视为配置或响应合同问题。
非 ID 提取端点成功 2xx(来自不提取 ID 的端点,例如备注、更新或附件操作)。 否 否 下游会根据命令链和实例配置继续执行。
暂时性错误 408、413、429、500、502、503、504、521、522和 524。 是(总共两次尝试机会) 通常是 重试期间,下游步骤可能会延迟。
其他错误 400、401、403、404 和其他 4xx/5xx。 否 通常是 重试期间,下游步骤可能会延迟。
配置/验证失败 配置错误、缺少端点。 不适用 否 链可能会停止或部分完成,具体取决于配置。
网络级故障(TCP/套接字) ECONNRESET、ENOTFOUND、ESOCKETTIMEDOUT、ETIMEDOUT、ECONNREFUSED、EHOSTUNREACH、EPIPE和 EAI_AGAIN。 不适用 否(由适配器第 2 层处理) 适配器会立即重新排队;恢复通常很快(几秒钟)。下游步骤可能会短暂延迟。
应用级超时 HTTP timeout exceeded(未收到回复)。 不适用 通常是(第 3 层) 在命令级重试期间,下游步骤可能会延迟。

采用最少通用 API 配置的实际示例

一个基准自定义 CRM 通用 API 设置,其中包含链执行的三个端点:查找或创建联系人、查找或创建工单,以及会话后评论。您可以以此为起点,并根据您的 CRM 具体情况进行调整。

实例级设置

{
  "lookup_method": "genericApi",
  "auth_method": "basic",
  "basic_auth_username": "ccai-integration",
  "basic_auth_password": "<secret>",
  "api_request_timeout": 10,
  "case_lookup_url": "https://crm.example.com/api/cases/{RECORD_ID}",
  "contact_lookup_url": "https://crm.example.com/api/contacts/{ACCOUNT_ID}",
  "skip_account_lookup": false,
  "skip_account_creation": false,
  "skip_record_creation": false
}

响应形状

CRM 的响应形状因客户而异。response_data_location 是一个 lodash 样式的路径,可到达已解析响应正文中的相关对象。无需将响应封装在顶级 data 字段中,以下示例默认显示实际的扁平形状。

例如,如果 CRM 将资源嵌套在 data.contact 下,则会设置 response_data_location: "data.contact" 和响应正文 { "contact": { "id": "c_99812" } };其余配置保持不变。

端点 1 - contact_find_endpoint

通过手机号码或邮箱查找联系人。

{
  "request_url": "https://crm.example.com/api/contacts/search?phone={PHONE_NUMBER}&email={EMAIL}",
  "request_method": "GET",
  "request_parameters": {},
  "response_data_location": "data"
}

成功:找到联系人

200 OK
Content-Type: application/json
{ "id": "c_42018", "first_name": "Jane", "last_name": "Doe" }

找不到此联系人

404 Not Found

平台会将 404 视为“未找到”(而非错误)。如果 skip_account_creation 为 false,则流会继续执行 contact_create_endpoint。

“未找到”模式的替代模式:同样有效

200 OK
{}

当没有匹配的记录时,某些 CRM 会返回包含空数据的 200 OK,而不是 404。平台对两者的处理方式相同,都会触发下一步(如果已启用,则为创建)。

暂时性故障

503 Service Unavailable
}

触发第 1 层立即 HTTP 重试(总共尝试 2 次)。如果仍然失败,则会回退到第 3 层命令级重试。

端点 2 - contact_create_endpoint

创建联系人。custom_id_location 字段会告知平台在响应中的哪个位置查找新联系人的 ID。

{
  "request_url": "https://crm.example.com/api/contacts",
  "request_method": "POST",
  "request_parameters": {
    "first_name": "{FIRST_NAME}",
    "last_name": "{LAST_NAME}",
    "email": "{EMAIL}",
    "phone": "{PHONE_NUMBER}",
    "external_session_id": "{SESSION_TYPE}-{SESSION_ID}"
  },
  "response_data_location": "data",
  "custom_id_location": "id"
}

成功创建

201 Created
{ "id": "c_99812", "first_name": "John", "last_name": "Doe" }

2xx,但配置的位置缺少 ID

200 OK
{}

端点返回了 200 OK,但在配置的位置没有 id。平台会记录警告并将 null contact_id 转发到下游(不抛出异常,不重试)。请将此视为配置问题:对于端点从此路径返回的任何 2xx,response_data_location 和 custom_id_location 应可靠地生成 ID。

端点 3 - case_create_endpoint

创建工单。上一步中的 contact_id 可用作 {ACCOUNT_ID}。

{
  "request_url": "https://crm.example.com/api/cases",
  "request_method": "POST",
  "request_parameters": {
    "subject": "{TICKET_SUBJECT}",
    "description": "{TICKET_DESCRIPTION}",
    "contact_id": "{ACCOUNT_ID}",
    "channel": "{CHANNEL}",
    "external_session_id": "{SESSION_TYPE}-{SESSION_ID}"
  },
  "response_data_location": "data",
  "custom_id_location": "id"
}

成功创建

201 Created
{ "id": "case_77103", "status": "new" }

使用 response_data_location: "data" 时,平台会将路径解析为已解析的响应正文。然后,它会读取 custom_id_location: "id" → ticket_id = "case_77103" 处的字段,该字段会流向会话后。

端点 4 - case_comment_endpoint

此端点会在通话或聊天结束后向工单添加会话摘要评论。RECORD_ID 变量包含第 3 步生成的 ticket_id。

{
  "request_url": "https://crm.example.com/api/cases/{RECORD_ID}/comments",
  "request_method": "POST",
  "request_parameters": {
    "body": "{COMMENT_BODY}",
    "agent_email": "{AGENT_EMAIL}",
    "channel": "{CHANNEL}",
    "duration": "{CALL_DURATION}",
    "external_session_id": "{SESSION_TYPE}-{SESSION_ID}"
  }
}

对于配置了 case_lookup_url 的通用 API 实例,在成功完成凭证创建步骤之前,平台不会分派此端点。因此,此处缺少 RECORD_ID 表示存在配置问题,而不是暂时性状态。

变体:账号创建功能已停用

此示例与前面给出的最小通用 API 配置示例非常相似,只是在这里,您的实例不允许在 CRM 中创建联系人。账号查找功能会运行,但账号创建功能不会运行。

实例设置更改

{
  "skip_account_creation": true,
  "skip_account_lookup": false
}

实例设置更改包括以下行为:

  • 账号步骤仅调用 contact_find_endpoint。如果未找到任何联系人,则不会调用 contact_create_endpoint。

  • 记录步骤会收到一个 null contact_id。平台会继续创建工单,而不会出现 contact_id - {ACCOUNT_ID} 解析为空,并且 contact_id 字段会从 case_create_endpoint 载荷中完全删除。

  • 这意味着,在此配置中,您的工单创建端点必须能够在没有 contact_id 的情况下创建工单。如果您的 CRM 要求每个工单都必须有关联的联系人,则此配置不兼容。

case_create_endpoint 中的防御模式

如果没有 contact_id,您的 CRM 系统对来电者的了解会比平时少。您可以使用 =DEFAULT_VALUE 辅助程序来捕获请求中可用的任何身份信息,并将其作为后备信号传递:

"request_parameters": {
  "subject": "{TICKET_SUBJECT}",
  "contact_id": "{ACCOUNT_ID}",
  "contact_origin": "=DEFAULT_VALUE(\"{EMAIL}\", \"{PHONE_NUMBER}\", \"anonymous\")"
}

=DEFAULT_VALUE 辅助函数会选择第一个非空值,因此当有电子邮件地址或手机号码时,您的 CRM 会收到相应信息;只有在两者都没有时,才会收到字面量字符串 "anonymous"。这对于标记未附加任何标识性联系信息的工单非常有用。

变体:仅限查询的工单流程(不创建记录)

账号操作正常运行,但无法创建工单。该集成只会将评论附加到通过查找显示的现有工单。

实例设置更改

{
  "skip_record_creation": true,
  "case_lookup_url": "https://crm.example.com/api/cases/{RECORD_ID}"
}

这些变更涵盖以下行为:

  • 记录步骤会调用 case_find_endpoint 并使用返回的票券。如果未找到工单,则链会在记录步骤停止,不会调用 case_create_endpoint。

  • 只有在成功查找工单后,系统才会分派会话后端点(case_comment_endpoint、case_update_endpoint 等)。Rails 端的 ready? 保护可确保在没有 ticket_id 出现的情况下,这些命令会保持排队状态(并最终过期)。

  • 实际影响:在此配置中,您的会话后处理端点将仅接收与已知票券关联的会话。您无需处理 "missing ticket_id" 分支。

该平台会将 SESSION_TYPE 和 SESSION_ID 作为模板变量公开。应用复合键模式的最简洁方法是将其直接嵌入到每个相关请求正文中,这样您的端点就有了可供使用的规范键,而无需您编写额外的逻辑:

"request_parameters": {
  "...your fields...": "...",
  "ccai_session_key": "{SESSION_TYPE}-{SESSION_ID}"
}

对于同一调用,链中每个端点(账号 > 记录 > 会话后)的每次重试都会生成相同的 ccai_session_key。在 CRM 的创建或 upsert 逻辑中,将其用作去重锚点。例如,对 (ccai_session_key, endpoint_kind) 执行 upsert 操作将安全地吸收重试,而不会产生重复记录。

构建弹性端点(幂等性和设计提示)

由于请求可能会重复(立即重试 + 几天内重试),因此您的端点应具有幂等性:

  • 使用稳定的复合互动标识符作为幂等性密钥:

    • 将练习类型与练习 ID 结合使用,例如 call-12345 或 chat-12345。

    • 请勿仅依赖 call_id 或 chat_id。语音和聊天 ID 可能会重叠,因为每个序列都可以从 1 开始。

  • 如果您收到疑似重复的请求:

    • 检查载荷,确定它是否确实是重复的。某些共享端点(例如评论或备注端点)可供多种命令类型使用。

    • 对于创建操作,在正常成功的情况下不应出现重复请求,但如果 CRM 处理了请求,然后在平台收到成功响应之前返回错误或超时,则可能会出现重复请求。

    • 确认重复创建请求后,返回之前创建的记录 ID,而不是其他记录。

  • 明确处理缺失的 ID:

    • 创建工单时缺少 contact_id 通常表示账号查找或账号创建功能已停用。如果您的实例需要账号,请进行调查,而不是在没有联系人的情况下静默创建工单。

    • 对于配置了工单查询的典型通用 API 设置,会话后调用中不应缺少 ticket_id。在将请求视为正常的空操作之前,请先检查工单查找配置和载荷模板。

  • 对于更新,最好将字段设置为特定值,而不是递增计数器,这样重复调用才是安全的。

问题排查和可观测性

在调试自定义 CRM 集成时,请注意以下模式:

  • 对同一端点的重复调用:

    • 将时间戳与命令级重试(约 7 天,14 次尝试)中的重试时间表进行比较。

    • 使用复合会话标识符(例如会话类型 + 通话或聊天会话 ID)关联事件。

    • CCAI Platform 导出的日志有助于显示通话或聊天会话 ID 以及 API 事件,以便进行跟踪。

    • 检查您的端点是否持续返回可重试的错误(例如 500),而不是不可重试的配置失败。

  • “不会停止”的重试:

    • 检查端点返回的错误响应。

    • 如果您希望出现永久性失败,请与支持团队联系,获取有关如何为您的配置指示不可重试错误的指导。

  • 端点从未被调用:

    • 请确认以下事项:

      • 上游步骤(账号/记录)已成功完成,并生成了必要的 ID。

      • 已在自定义 CRM 设置中正确配置并启用端点。

    • 配置错误或停用的端点,或者链中较早的故障,可能会阻止某些步骤运行。

  • 关联日志:

    • 在您这边记录完整的请求和响应,包括互动 ID(call_id/chat_id)。

    • 使用该 ID 将您的 CRM 日志与平台在一段时间内的行为(尤其是在重试期间)相关联。