service-de-channel-activate

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Activating a Messaging Channel

激活消息通道

What this skill does

本技能的作用

Given a
{CHANNEL_ID}
, reads the channel's
MessagingChannelUsage.Id
, then fires
PATCH /services/data/v{V}/sobjects/MessagingChannelUsage/{MCU_ID}
with body
{"DeploymentStatus":"Provisioning"}
. The server-side chain:
  1. MessagingChannelUsageFunctions.validateBeforeSave
    runs
    validateDeploymentStatus
    validateChannelReadinessOnProvisioning
    . For WhatsApp this confirms consent is configured. Rejected writes return HTTP 400
    FIELD_INTEGRITY_EXCEPTION
    .
  2. MessagingChannelUsageFunctions.saveHook_PostStmtExecuteOnce
    fires unconditionally after the UPDATE statement. It calls
    MessagingChannelUsageFunctionsHelper.handlePostSave
    which registers a post-commit
    TransactionObserver
    .
  3. At commit, the observer calls
    ConversationChannelUsageDeploymentStatusService.handleProvisioning
    (inherited from
    AbstractChannelUsageDeploymentStatusService
    ), which:
    • Calls
      runProvisioning
      switch
      -dispatches by
      MessageType
      for the external callout:
      • WHATS_APP
        registerCsotWhatsAppNumber
        LiveMessageSetupApi.registerWhatsAppNumber
        → Meta
        /register
        + status verification. 15-21s wall-clock.
      • FACEBOOK
        metaGraphApiService.subscribeFacebookPage
        .
      • TEXT
        registerCsotSms
        .
      • AppleBusinessChat
        ,
        Line
        , everything else →
        default
        branch, no external callout, no network wait.
    • On success: writes
      DeploymentStatus = 'Active'
      via PLSQL.
    • On failure: writes
      DeploymentStatus = 'Error'
      plus
      ErrorReason
      /
      ErrorDetails
      .
  4. Inside the same observer, a second pass syncs
    MessagingChannel.IsActive = true
    once MCU reaches
    Active
    (the
    isTransitioningStatus
    flag skips the flip while status is still
    Provisioning
    ).
All synchronous within the PATCH request — the 204 response only comes back after the full chain completes. WhatsApp: ~15-21s (Meta
/register
round-trip). Apple / Line: ~1s (no external call; just the local save-hook + observer + PLSQL write). Verified on wadtesting 2026-04-30.
给定
{CHANNEL_ID}
,读取通道的
MessagingChannelUsage.Id
,然后发起
PATCH /services/data/v{V}/sobjects/MessagingChannelUsage/{MCU_ID}
请求,请求体为
{"DeploymentStatus":"Provisioning"}
。服务端流程如下:
  1. MessagingChannelUsageFunctions.validateBeforeSave
    会执行
    validateDeploymentStatus
    validateChannelReadinessOnProvisioning
    。对于WhatsApp,这一步会确认同意配置已完成。验证不通过的写入请求会返回HTTP 400
    FIELD_INTEGRITY_EXCEPTION
  2. MessagingChannelUsageFunctions.saveHook_PostStmtExecuteOnce
    会在UPDATE语句执行后无条件触发。它会调用
    MessagingChannelUsageFunctionsHelper.handlePostSave
    ,后者会注册一个提交后的
    TransactionObserver
  3. 提交时,观察者会调用
    ConversationChannelUsageDeploymentStatusService.handleProvisioning
    (继承自
    AbstractChannelUsageDeploymentStatusService
    ),该方法会:
    • 调用
      runProvisioning
      ——根据
      MessageType
      分发外部调用:
      • WHATS_APP
        registerCsotWhatsAppNumber
        LiveMessageSetupApi.registerWhatsAppNumber
        → 调用Meta的
        /register
        接口并验证状态,耗时约15-21秒。
      • FACEBOOK
        metaGraphApiService.subscribeFacebookPage
      • TEXT
        registerCsotSms
      • AppleBusinessChat
        Line
        及其他通道 → 进入
        default
        分支,无需外部调用,无需等待网络请求。
    • 成功时:通过PLSQL将
      DeploymentStatus
      设置为
      Active
    • 失败时:将
      DeploymentStatus
      设置为
      Error
      ,并写入
      ErrorReason
      ErrorDetails
  4. 在同一个观察者中,当MCU状态变为
    Active
    后,会同步更新
    MessagingChannel.IsActive = true
    isTransitioningStatus
    标志会在状态仍为
    Provisioning
    时跳过此切换操作)。
整个流程在PATCH请求内同步完成——只有当整个流程执行完毕后才会返回204响应。WhatsApp通道耗时约15-21秒(包含Meta
/register
接口的往返时间);Apple/Line通道耗时约1秒(无需外部调用,仅包含本地保存钩子、观察者及PLSQL写入操作)。已在wadtesting环境验证,验证时间为2026-04-30。

Reference File Index

参考文件索引

Reference fileLoad when
references/phone-verification.md
Stage 3 comes back with
ErrorReason === "VERIFICATION_REQUIRED"
(WhatsApp only) — the phone-number OTP verification sub-flow.
references/worked-examples.md
You want a reference run of the WhatsApp happy-path, Apple activation, a readiness failure, or the already-active no-op.
references/gotchas.md
Troubleshooting an unexpected result, or before modifying this skill — the eleven known gotchas.
参考文件加载时机
references/phone-verification.md
阶段3返回
ErrorReason === "VERIFICATION_REQUIRED"
时(仅适用于WhatsApp)——触发手机号OTP验证子流程。
references/worked-examples.md
需要参考WhatsApp正常流程、Apple激活流程、就绪检查失败流程或已激活无操作流程的运行示例时。
references/gotchas.md
排查异常结果或修改本技能前——查看已知的11个注意事项。

Why REST PATCH instead of Apex?

为何选择REST PATCH而非Apex?

A direct REST PATCH produces the identical save-hook chain as the old
activateChannelUsage
Apex method, with substantially less machinery — no CSRF cookie acquisition, no bootstrap fetch, no Aura response parsing, no double-wrapped
returnValue
. REST semantics are honest: 204 means the transition succeeded; 4xx means it didn't.
Code proof:
MessagingChannelUsageFunctions.saveHook_PostStmtExecuteOnce
fires on any DML path (REST, SOAP, Apex, Metadata API) — there is no Apex-specific gate. The entity XML (
MessagingChannelUsage.entity.xml
) marks
DeploymentStatus
as
editAccess="always"
with no
<readonly>
attribute. The transition validator (
getValidAPIStatusTransitions
) allows
Disabled → Provisioning
(and
New → Provisioning
, and
Error → Provisioning | Deprovisioning
, and
Active → Deprovisioning
). The DB-only transitions
Provisioning → Active | Error
are reserved for the observer's PLSQL call — that's why we write
Provisioning
and let the server pick the terminal state.
直接的REST PATCH操作会触发与旧版
activateChannelUsage
Apex方法完全相同的保存钩子流程,但所需的机制大幅减少——无需获取CSRF Cookie、无需引导请求、无需解析Aura响应、无需双层包装的
returnValue
。REST语义清晰:204表示转换成功;4xx表示转换失败。
代码证明:
MessagingChannelUsageFunctions.saveHook_PostStmtExecuteOnce
会在任何DML路径(REST、SOAP、Apex、Metadata API)下触发——不存在Apex专属的限制。实体XML文件
MessagingChannelUsage.entity.xml
DeploymentStatus
标记为
editAccess="always"
,且无
<readonly>
属性。转换验证器
getValidAPIStatusTransitions
允许
Disabled → Provisioning
(以及
New → Provisioning
Error → Provisioning | Deprovisioning
Active → Deprovisioning
)。仅数据库层面的状态转换
Provisioning → Active | Error
由观察者的PLSQL调用保留——这也是我们写入
Provisioning
状态并让服务端决定最终状态的原因。

When NOT to use this skill

本技能的不适用场景

  • The channel is already
    IsActive=true
    .
    Re-firing is blocked by the API transition validator (
    Active → Provisioning
    is not in
    getValidAPIStatusTransitions()
    ) — the PATCH would return 400. The Stage 1 precondition check catches this and emits
    noop:true
    .
  • Routing isn't configured.
    activateChannelUsage
    used to fail with
    LiveMessageSetupException
    /
    nullQueueId
    at the Apex entry point. With the REST path the same guard lives in
    validateChannelReadinessOnProvisioning
    — write with
    SessionHandlerId=null && FallbackQueueId=null
    → 400
    FIELD_INTEGRITY_EXCEPTION
    . Run
    service-de-channel-routing-configure
    first. The Stage 1 check still runs defensively.
  • The MCU doesn't exist. Can't PATCH a row that's missing. Run the insertion skill first — it always creates the MCU as a side-effect of
    addChannel
    .
  • 通道已处于
    IsActive=true
    状态
    :API转换验证器会阻止重复触发(
    Active → Provisioning
    不在
    getValidAPIStatusTransitions()
    允许的范围内)——PATCH请求会返回400。阶段1的前置检查会捕获此情况并返回
    noop:true
  • 未配置路由:旧版
    activateChannelUsage
    会在Apex入口点因
    LiveMessageSetupException
    /
    nullQueueId
    失败。使用REST路径时,相同的校验逻辑位于
    validateChannelReadinessOnProvisioning
    中——若
    SessionHandlerId=null && FallbackQueueId=null
    ,写入请求会返回400
    FIELD_INTEGRITY_EXCEPTION
    。需先执行
    service-de-channel-routing-configure
    。阶段1的检查仍会进行防御性校验。
  • MCU不存在:无法对不存在的记录执行PATCH操作。需先执行插入技能——该技能会在
    addChannel
    的副作用中创建MCU。

Inputs (from caller)

调用方输入参数

  • {CHANNEL_ID}
    — a 15- or 18-char
    MessagingChannel.Id
    (prefix
    0Mj
    ). The channel must already exist with a non-null
    SessionHandlerId
    or
    FallbackQueueId
    .
  • {ORG_ALIAS}
    — optional; the
    sf
    CLI target-org alias. Default: whatever
    sf config get target-org
    returns. Used for OAuth and SOQL reads.
  • {API_VERSION}
    — optional; REST API version. Default:
    68.0
    . Any version where
    MessagingChannelUsage
    is addressable as a standard sobject is fine (v50+ should work; not exhaustively tested).
Unlike the old Aura-based version of this skill, there are no
{POLL_TIMEOUT_S}
/
{POLL_INTERVAL_S}
inputs — the PATCH is synchronous end-to-end.
  • {CHANNEL_ID}
    ——15或18位的
    MessagingChannel.Id
    (前缀为
    0Mj
    )。通道必须已存在,且
    SessionHandlerId
    FallbackQueueId
    不为空。
  • {ORG_ALIAS}
    ——可选;
    sf
    CLI的目标组织别名。默认值:
    sf config get target-org
    返回的值。用于OAuth认证和SOQL查询。
  • {API_VERSION}
    ——可选;REST API版本。默认值:
    68.0
    。任何可将
    MessagingChannelUsage
    作为标准sobject访问的版本均可(v50+应该可用;未进行全面测试)。
与旧版基于Aura的技能不同,本技能无需
{POLL_TIMEOUT_S}
/
{POLL_INTERVAL_S}
输入参数——PATCH操作是端到端同步的。

Output (to caller)

返回给调用方的结果

Success — channel is live:
json
{"ok": true, "channelId": "0Mj...", "mcuId": "0gL...", "deploymentStatus": "Active", "isActive": true, "messageType": "WhatsApp", "durationMs": 20934}
Success — no-op (already active):
json
{"ok": true, "noop": true, "channelId": "0Mj...", "message": "Channel already active"}
Failure — precondition not met:
json
{"ok": false, "kind": "no-routing",     "hint": "run service-de-channel-routing-configure first"}
{"ok": false, "kind": "no-mcu",         "hint": "no MessagingChannelUsage row for this channel — run the insertion skill first"}
{"ok": false, "kind": "channel-missing","hint": "MessagingChannel id not found"}
Failure — validator or server-side provisioning error:
json
{"ok": false, "kind": "readiness-failed", "errorCode": "FIELD_INTEGRITY_EXCEPTION",
 "message": "...", "hint": "validateChannelReadinessOnProvisioning rejected the write — most commonly missing consent; run service-de-channel-consent-configure first"}
{"ok": false, "kind": "provisioning-error", "mcuId": "0gL...", "errorReason": "MetaRegistrationFailed", "errorDetails": "..."}
{"ok": false, "kind": "verification-failed", "hint": "WhatsApp phone number verification failed or was declined by user"}
{"ok": false, "kind": "verification-request-failed", "errorCode": "...", "message": "...", "hint": "Could not request verification code from Meta"}
Failure — auth / transport:
json
{"ok": false, "kind": "auth",      "hint": "OAuth token invalid / expired — run 'sf org login web'"}
{"ok": false, "kind": "transport", "status": 500, "message": "..."}

成功——通道已启用:
json
{"ok": true, "channelId": "0Mj...", "mcuId": "0gL...", "deploymentStatus": "Active", "isActive": true, "messageType": "WhatsApp", "durationMs": 20934}
成功——无操作(通道已激活):
json
{"ok": true, "noop": true, "channelId": "0Mj...", "message": "Channel already active"}
失败——未满足前置条件:
json
{"ok": false, "kind": "no-routing",     "hint": "run service-de-channel-routing-configure first"}
{"ok": false, "kind": "no-mcu",         "hint": "no MessagingChannelUsage row for this channel — run the insertion skill first"}
{"ok": false, "kind": "channel-missing","hint": "MessagingChannel id not found"}
失败——验证器或服务端预配错误:
json
{"ok": false, "kind": "readiness-failed", "errorCode": "FIELD_INTEGRITY_EXCEPTION",
 "message": "...", "hint": "validateChannelReadinessOnProvisioning rejected the write — most commonly missing consent; run service-de-channel-consent-configure first"}
{"ok": false, "kind": "provisioning-error", "mcuId": "0gL...", "errorReason": "MetaRegistrationFailed", "errorDetails": "..."}
{"ok": false, "kind": "verification-failed", "hint": "WhatsApp phone number verification failed or was declined by user"}
{"ok": false, "kind": "verification-request-failed", "errorCode": "...", "message": "...", "hint": "Could not request verification code from Meta"}
失败——认证/传输错误:
json
{"ok": false, "kind": "auth",      "hint": "OAuth token invalid / expired — run 'sf org login web'"}
{"ok": false, "kind": "transport", "status": 500, "message": "..."}

Stage 1: Precondition checks

阶段1:前置条件检查

Read the channel, then its MCU, as two separate SOQL calls. A combined subquery would be cheaper but fails on orgs where the child relationship is unnameable (see gotcha #4).
bash
sf data query --target-org '{ORG_ALIAS}' \
  --query "SELECT Id, DeveloperName, MessageType, IsActive, SessionHandlerId, FallbackQueueId, MessagingPlatformKey FROM MessagingChannel WHERE Id = '{CHANNEL_ID}'" \
  --json > /tmp/amc-precheck-channel.json

sf data query --target-org '{ORG_ALIAS}' \
  --query "SELECT Id, DeploymentStatus, DeploymentType, ErrorReason, ErrorDetails FROM MessagingChannelUsage WHERE MessagingChannelId = '{CHANNEL_ID}'" \
  --json > /tmp/amc-precheck-mcu.json
Let
channel = /tmp/amc-precheck-channel.json records[0]
and
mcu = /tmp/amc-precheck-mcu.json records[0]
:
ConditionEnvelope
Channel query returned 0 records
{ok:false, kind:"channel-missing", hint:"MessagingChannel id not found"}
channel.IsActive === true
{ok:true, noop:true, channelId, message:"Channel already active"}
— return
channel.SessionHandlerId == null && channel.FallbackQueueId == null
{ok:false, kind:"no-routing", hint:"run service-de-channel-routing-configure first"}
MCU query returned 0 records
{ok:false, kind:"no-mcu", hint:"no MessagingChannelUsage row for this channel — run the insertion skill first"}
OtherwiseRecord
{MCU_ID} = mcu.Id
,
{MESSAGE_TYPE} = channel.MessageType
,
{MESSAGING_PLATFORM_KEY} = channel.MessagingPlatformKey
,
{INITIAL_MCU_STATUS} = mcu.DeploymentStatus
and continue to Stage 2.
Also record
{T0}
(epoch ms at start of Stage 2) so the final envelope can report
durationMs
.
通过两次独立的SOQL查询读取通道及其对应的MCU。虽然联合子查询更高效,但在子关系不可命名的组织中会失败(参见注意事项#4)。
bash
sf data query --target-org '{ORG_ALIAS}' \
  --query "SELECT Id, DeveloperName, MessageType, IsActive, SessionHandlerId, FallbackQueueId, MessagingPlatformKey FROM MessagingChannel WHERE Id = '{CHANNEL_ID}'" \
  --json > /tmp/amc-precheck-channel.json

sf data query --target-org '{ORG_ALIAS}' \
  --query "SELECT Id, DeploymentStatus, DeploymentType, ErrorReason, ErrorDetails FROM MessagingChannelUsage WHERE MessagingChannelId = '{CHANNEL_ID}'" \
  --json > /tmp/amc-precheck-mcu.json
channel = /tmp/amc-precheck-channel.json records[0]
mcu = /tmp/amc-precheck-mcu.json records[0]
条件返回结果
通道查询返回0条记录
{ok:false, kind:"channel-missing", hint:"MessagingChannel id not found"}
channel.IsActive === true
{ok:true, noop:true, channelId, message:"Channel already active"}
— 返回结果
channel.SessionHandlerId == null && channel.FallbackQueueId == null
{ok:false, kind:"no-routing", hint:"run service-de-channel-routing-configure first"}
MCU查询返回0条记录
{ok:false, kind:"no-mcu", hint:"no MessagingChannelUsage row for this channel — run the insertion skill first"}
其他情况记录
{MCU_ID} = mcu.Id
{MESSAGE_TYPE} = channel.MessageType
{MESSAGING_PLATFORM_KEY} = channel.MessagingPlatformKey
{INITIAL_MCU_STATUS} = mcu.DeploymentStatus
,继续执行阶段2。
同时记录
{T0}
(阶段2开始时的时间戳,单位为毫秒),以便最终结果中可以返回
durationMs

Stage 1.1: Fast path for already-provisioning MCU

阶段1.1:已处于预配状态的MCU快速路径

If
{INITIAL_MCU_STATUS} === "Provisioning"
— the MCU is already mid-flight from a prior call in this transaction window. Skip Stage 2 (firing the PATCH) entirely and jump to Stage 3 (verification). This is a rare race guard: the observer is synchronous with the PATCH, so by the time the caller sees the 204 the status is already terminal (
Active
or
Error
) —
Provisioning
should be invisible from outside. If we do see it in the precheck, something wrote
Provisioning
in a separate DML and the observer is still mid-flight — don't fire a second PATCH.

如果
{INITIAL_MCU_STATUS} === "Provisioning"
——MCU已在当前事务窗口中由之前的调用触发预配流程。跳过阶段2(发起PATCH请求),直接进入阶段3(验证状态)。这是一种罕见的竞争防护机制:观察者与PATCH操作同步执行,因此当调用方收到204响应时,状态已变为最终状态(
Active
Error
)——外部应该看不到
Provisioning
状态。如果前置检查中确实看到了该状态,说明有其他DML操作写入了
Provisioning
状态,且观察者仍在执行中——请勿再次发起PATCH请求。

Stage 2: PATCH
MessagingChannelUsage.DeploymentStatus = "Provisioning"

阶段2:PATCH更新
MessagingChannelUsage.DeploymentStatus = "Provisioning"

Use
sf api request rest
so authentication stays inside the CLI's transport — no OAuth token is ever extracted into shell state.
Fire the PATCH. This call can take 15-30 seconds for WhatsApp — the observer runs synchronously, including Meta's
/register
round trip.
sf api request rest
has no separate client-side timeout to raise; it waits on the underlying HTTP call.
bash
sf api request rest \
  "/services/data/v{API_VERSION}/sobjects/MessagingChannelUsage/{MCU_ID}" \
  --method PATCH \
  --target-org '{ORG_ALIAS}' \
  --header 'Content-Type: application/json' \
  --body '{"DeploymentStatus":"Provisioning"}' \
  --include \
  > /tmp/amc-patch-response.txt 2>&1
HTTP_CODE=$(head -1 /tmp/amc-patch-response.txt | grep -oE '[0-9]{3}')
--include
prints the HTTP status/headers block ahead of the (typically empty, on 204) body — read the status line from that block rather than a
-w
-style trailing marker.
Classify by HTTP status:
StatusBodyHandling
204emptySuccess. The observer ran to completion; MCU is
Active
or
Error
. Continue to Stage 3 to read the terminal state.
400
[{errorCode:"FIELD_INTEGRITY_EXCEPTION", message:"..."}]
Validator rejection. See table below.
400
[{errorCode:"INVALID_FIELD_FOR_INSERT_UPDATE" or "MALFORMED_ID", ...}]
Skill bug — the MCU_ID from Stage 1 was wrong, or the body shape is off. Emit
{ok:false, kind:"transport", status:400, message}
.
401(usually empty)Bearer token invalid. Emit
{ok:false, kind:"auth", hint:"OAuth token invalid / expired — run 'sf org login web'"}
.
403
[{errorCode:"INSUFFICIENT_ACCESS"}]
User lacks perm to write
DeploymentStatus
. Emit
{ok:false, kind:"business", errorCode:"INSUFFICIENT_ACCESS", message}
.
5xxvariesTransport. The observer may have partially committed — Stage 3's SOQL is the source of truth. Read MCU state; if it's
Active
, report success with a warning; if
Error
or still
Disabled
, classify as
{ok:false, kind:"transport", status, message}
.
Known 400
FIELD_INTEGRITY_EXCEPTION
messages:
Message fragmentMeaningEnvelope
"invalid deployment status transition"The current status doesn't allow
→ Provisioning
(e.g. MCU is already
Active
— Stage 1 should have caught this, but there's a race window).
Re-read MCU; if now
Active
, emit success-noop. If
Provisioning
, Stage 3 poll. Otherwise emit
{ok:false, kind:"readiness-failed", ...}
.
"consent" / "keyword" / mentions of STOP/HELP
validateChannelReadinessOnProvisioning
rejected — channel doesn't have required consent configured.
{ok:false, kind:"readiness-failed", errorCode:"FIELD_INTEGRITY_EXCEPTION", message, hint:"channel requires a ConsentType and a matching MsgChannelLanguageKeyword record (opt-out keyword + confirmation) before activation — run service-de-channel-consent-configure"}
.
"routing" / "queue" / "SessionHandler"Routing precondition (Stage 1 should have caught, but the validator re-checks).
{ok:false, kind:"no-routing", message, hint:"run service-de-channel-routing-configure first"}
.
otherUnrecognized validator error.
{ok:false, kind:"readiness-failed", errorCode, message}
.

使用
sf api request rest
命令,确保认证操作在CLI的传输层内完成——OAuth令牌不会被提取到shell状态中。
发起PATCH请求。对于WhatsApp,此请求可能需要15-30秒——观察者会同步执行,包括Meta
/register
接口的往返时间。
sf api request rest
没有单独的客户端超时设置;它会等待底层HTTP请求完成。
bash
sf api request rest \
  "/services/data/v{API_VERSION}/sobjects/MessagingChannelUsage/{MCU_ID}" \
  --method PATCH \
  --target-org '{ORG_ALIAS}' \
  --header 'Content-Type: application/json' \
  --body '{"DeploymentStatus":"Provisioning"}' \
  --include \
  > /tmp/amc-patch-response.txt 2>&1
HTTP_CODE=$(head -1 /tmp/amc-patch-response.txt | grep -oE '[0-9]{3}')
--include
参数会在(通常为空的)响应体之前打印HTTP状态/头信息块——从此块中读取状态行,而非使用
-w
风格的尾部标记。
根据HTTP状态码分类处理:
状态码响应体处理方式
204成功。观察者已执行完毕;MCU状态为
Active
Error
。继续执行阶段3读取最终状态。
400
[{errorCode:"FIELD_INTEGRITY_EXCEPTION", message:"..."}]
验证器拒绝请求。参见下方表格。
400
[{errorCode:"INVALID_FIELD_FOR_INSERT_UPDATE" or "MALFORMED_ID", ...}]
技能存在bug——阶段1获取的MCU_ID错误,或请求体格式有误。返回
{ok:false, kind:"transport", status:400, message}
401通常为空Bearer令牌无效。返回
{ok:false, kind:"auth", hint:"OAuth token invalid / expired — run 'sf org login web'"}
403
[{errorCode:"INSUFFICIENT_ACCESS"}]
用户无写入
DeploymentStatus
的权限。返回
{ok:false, kind:"business", errorCode:"INSUFFICIENT_ACCESS", message}
5xx内容不定传输错误。观察者可能已部分提交——阶段3的SOQL查询是权威数据源。读取MCU状态;如果状态为
Active
,则返回成功并附带警告;如果状态为
Error
或仍为
Disabled
,则分类为
{ok:false, kind:"transport", status, message}
已知的400
FIELD_INTEGRITY_EXCEPTION
错误信息:
消息片段含义返回结果
"invalid deployment status transition"当前状态不允许转换为
Provisioning
(例如MCU已处于
Active
状态——阶段1应已捕获此情况,但存在竞争窗口)。
重新读取MCU;如果状态变为
Active
,则返回成功无操作结果。如果状态为
Provisioning
,则进入阶段3轮询。否则返回
{ok:false, kind:"readiness-failed", ...}
"consent" / "keyword" / 提及STOP/HELP
validateChannelReadinessOnProvisioning
拒绝请求——通道未配置所需的同意信息。
{ok:false, kind:"readiness-failed", errorCode:"FIELD_INTEGRITY_EXCEPTION", message, hint:"channel requires a ConsentType and a matching MsgChannelLanguageKeyword record (opt-out keyword + confirmation) before activation — run service-de-channel-consent-configure"}
"routing" / "queue" / "SessionHandler"路由前置条件不满足(阶段1应已捕获,但验证器会重新检查)。
{ok:false, kind:"no-routing", message, hint:"run service-de-channel-routing-configure first"}
其他未识别的验证器错误。
{ok:false, kind:"readiness-failed", errorCode, message}

Stage 3: Read the terminal MCU state

阶段3:读取MCU的最终状态

The PATCH is synchronous, so by the time we're here the MCU is
Active
or
Error
— no polling. Read once:
bash
sf data query --target-org '{ORG_ALIAS}' \
  --query "SELECT Id, DeploymentStatus, ErrorReason, ErrorDetails FROM MessagingChannelUsage WHERE Id = '{MCU_ID}'" \
  --json > /tmp/amc-poststate-mcu.json
StatusHandling
Active
Continue to Stage 4.
Error
with
ErrorReason === "VERIFICATION_REQUIRED"
For WhatsApp channels only: phone number needs OTP verification. Continue to Stage 3.2 (WhatsApp verification flow).
Error
(other)
Emit
{ok:false, kind:"provisioning-error", mcuId, errorReason, errorDetails}
.
Provisioning
Unexpected — the observer was supposed to terminate before the PATCH returned 204. Fall through to a defensive poll (see Stage 3.1).
Disabled
The PATCH returned 204 but the write didn't take? Emit
{ok:false, kind:"transport", message:"PATCH returned 204 but MCU is still Disabled — observer didn't commit"}
.
PATCH操作是同步的,因此当进入此阶段时,MCU状态已变为
Active
Error
——无需轮询。只需读取一次:
bash
sf data query --target-org '{ORG_ALIAS}' \
  --query "SELECT Id, DeploymentStatus, ErrorReason, ErrorDetails FROM MessagingChannelUsage WHERE Id = '{MCU_ID}'" \
  --json > /tmp/amc-poststate-mcu.json
状态处理方式
Active
继续执行阶段4。
Error
ErrorReason === "VERIFICATION_REQUIRED"
仅适用于WhatsApp通道:手机号需要OTP验证。继续执行阶段3.2(WhatsApp验证流程)。
Error
(其他情况)
返回
{ok:false, kind:"provisioning-error", mcuId, errorReason, errorDetails}
Provisioning
异常情况——观察者应在PATCH返回204之前完成状态转换。执行防御性轮询(参见阶段3.1)。
Disabled
PATCH返回204但写入未生效?返回
{ok:false, kind:"transport", message:"PATCH returned 204 but MCU is still Disabled — observer didn't commit"}

Stage 3.2: WhatsApp phone number verification (only if ErrorReason === "VERIFICATION_REQUIRED")

阶段3.2:WhatsApp手机号验证(仅当ErrorReason === "VERIFICATION_REQUIRED"时执行)

This sub-flow only runs for WhatsApp channels when activation fails with
ErrorReason === "VERIFICATION_REQUIRED"
— the phone number needs OTP verification with Meta before it can be registered. If the MCU comes back with
ErrorReason === "VERIFICATION_REQUIRED"
(WhatsApp only), load
references/phone-verification.md
and follow it
to drive the phone-number verification sub-flow (request code → prompt user → validate code → retry activation once).
此子流程仅在WhatsApp通道激活失败且
ErrorReason === "VERIFICATION_REQUIRED"
时运行——手机号需通过Meta的OTP验证才能完成注册。如果MCU返回
ErrorReason === "VERIFICATION_REQUIRED"
(仅适用于WhatsApp),请加载
references/phone-verification.md
并按照其中的步骤
执行手机号验证子流程(请求验证码→提示用户→验证验证码→重试激活一次)。

Stage 3.1: Defensive poll (only if Stage 3 saw
Provisioning
)

阶段3.1:防御性轮询(仅当阶段3检测到
Provisioning
状态时执行)

The observer's external-callout block (WhatsApp/Facebook/SMS) runs synchronously inside the PATCH request — there's no async queue indirection in
runProvisioning
for any message type (verified against the switch/case in
ConversationChannelUsageDeploymentStatusService
). So a
Provisioning
status at Stage 3 should not happen in steady state. Possible causes if it does: an exception thrown after the external call succeeded but before the PLSQL terminal-write ran; unusual instance config with async observer execution; or a future MessageType whose dispatch behavior we haven't accounted for. If Stage 3 returned
Provisioning
, loop:
bash
for i in 1 2 3 4 5 6 7 8 9 10; do
  sleep 3
  STATUS=$(sf data query --target-org '{ORG_ALIAS}' \
    --query "SELECT DeploymentStatus FROM MessagingChannelUsage WHERE Id='{MCU_ID}'" --json \
    | node -e 'console.log(JSON.parse(require("fs").readFileSync(0,"utf8")).result.records[0].DeploymentStatus)')
  case "$STATUS" in
    Provisioning) continue ;;
    Active|Error) break ;;
  esac
done
Max wait: 30s. If still
Provisioning
after the loop: emit
{ok:false, kind:"timeout", mcuId, lastStatus:"Provisioning", elapsedS:30}
. Verified on WhatsApp/wadtesting 2026-04-30: this loop should never actually iterate.

观察者的外部调用块(WhatsApp/Facebook/SMS)在PATCH请求内同步执行——
runProvisioning
中针对任何消息类型均无异步队列间接调用(已通过
ConversationChannelUsageDeploymentStatusService
中的switch/case验证)。因此在稳定状态下,阶段3不应检测到
Provisioning
状态。如果出现这种情况,可能的原因包括:外部调用成功但PLSQL写入最终状态前抛出异常;实例配置特殊导致观察者异步执行;或存在未考虑到的新MessageType分发行为。如果阶段3返回
Provisioning
状态,则执行循环:
bash
for i in 1 2 3 4 5 6 7 8 9 10; do
  sleep 3
  STATUS=$(sf data query --target-org '{ORG_ALIAS}' \
    --query "SELECT DeploymentStatus FROM MessagingChannelUsage WHERE Id='{MCU_ID}'" --json \
    | node -e 'console.log(JSON.parse(require("fs").readFileSync(0,"utf8")).result.records[0].DeploymentStatus)')
  case "$STATUS" in
    Provisioning) continue ;;
    Active|Error) break ;;
  esac
done
最长等待时间:30秒。如果循环结束后状态仍为
Provisioning
,则返回
{ok:false, kind:"timeout", mcuId, lastStatus:"Provisioning", elapsedS:30}
。已在WhatsApp/wadtesting环境验证,验证时间为2026-04-30:此循环实际不应执行。

Stage 4: Verify
MessagingChannel.IsActive

阶段4:验证
MessagingChannel.IsActive

The observer flips
IsActive
inside the same callback as the status terminal write. In principle this is set by the time the PATCH returns. Confirm:
bash
sf data query --target-org '{ORG_ALIAS}' \
  --query "SELECT Id, IsActive FROM MessagingChannel WHERE Id = '{CHANNEL_ID}'" --json > /tmp/amc-verify-channel.json
If
IsActive === true
: compute
durationMs = Date.now() - T0
and emit success. If
IsActive !== true
despite MCU
Active
: the
IsActive
sync pass skipped (e.g.
isTransitioningStatus
was true when the observer ran, which shouldn't happen post-terminal). Emit:
json
{"ok": false, "kind": "provisioning-error", "mcuId": "...", "errorReason": "mcu-active-but-channel-inactive",
 "errorDetails": "MCU DeploymentStatus=Active but MessagingChannel.IsActive=false — observer's IsActive sync pass didn't run. Inspect MessagingChannelUsageFunctionsHelper.handlePostSave."}

观察者会在写入最终状态的同一个回调中切换
IsActive
的值。理论上,当PATCH返回时此值已设置完成。请确认:
bash
sf data query --target-org '{ORG_ALIAS}' \
  --query "SELECT Id, IsActive FROM MessagingChannel WHERE Id = '{CHANNEL_ID}'" --json > /tmp/amc-verify-channel.json
如果
IsActive === true
:计算
durationMs = Date.now() - T0
并返回成功结果。如果MCU状态为
Active
IsActive !== true
:说明
IsActive
同步步骤未执行(例如观察者执行时
isTransitioningStatus
为true,而这在状态变为最终状态后不应发生)。返回:
json
{"ok": false, "kind": "provisioning-error", "mcuId": "...", "errorReason": "mcu-active-but-channel-inactive",
 "errorDetails": "MCU DeploymentStatus=Active but MessagingChannel.IsActive=false — observer's IsActive sync pass didn't run. Inspect MessagingChannelUsageFunctionsHelper.handlePostSave."}

Stage 5: Report to caller

阶段5:向调用方返回结果

Build the success envelope:
json
{"ok": true, "channelId": "{CHANNEL_ID}", "mcuId": "{MCU_ID}", "deploymentStatus": "Active",
 "isActive": true, "messageType": "{MESSAGE_TYPE}", "durationMs": 20934}
If this skill is the leaf (user invoked it directly), render:
  • Success — Activated — MessagingChannel {CHANNEL_ID} ({messageType}) is now live. MCU DeploymentStatus=Active, IsActive=true. (~{durationMs/1000}s)
  • Info: Already active — MessagingChannel {CHANNEL_ID} is IsActive=true. No changes.
    (no-op path)
  • Error: Routing not configured — run 'service-de-channel-routing-configure' skill first.
    (no-routing)
  • Error: No MessagingChannelUsage row — run the insertion skill first.
    (no-mcu)
  • Error: Readiness check failed: {message} — if it mentions consent/keywords, run 'service-de-channel-consent-configure' first.
    (readiness-failed)
  • Error: Activation failed: {errorReason} — {errorDetails}
    (provisioning-error)
  • Error: WhatsApp phone number verification failed: {hint}
    (verification-failed)
  • Error: Could not request verification code: {message}
    (verification-request-failed)
  • Timeout: Activation timed out after {elapsedS}s with MCU.DeploymentStatus={lastStatus}. Defensive-poll limit hit; this is unusual. Check MCU {mcuId} in Setup.
    (timeout)

构建成功结果:
json
{"ok": true, "channelId": "{CHANNEL_ID}", "mcuId": "{MCU_ID}", "deploymentStatus": "Active",
 "isActive": true, "messageType": "{MESSAGE_TYPE}", "durationMs": 20934}
如果本技能是最终节点(用户直接调用),则输出:
  • Success — Activated — MessagingChannel {CHANNEL_ID} ({messageType}) is now live. MCU DeploymentStatus=Active, IsActive=true. (~{durationMs/1000}s)
  • Info: Already active — MessagingChannel {CHANNEL_ID} is IsActive=true. No changes.
    (无操作路径)
  • Error: Routing not configured — run 'service-de-channel-routing-configure' skill first.
    (未配置路由)
  • Error: No MessagingChannelUsage row — run the insertion skill first.
    (无MCU记录)
  • Error: Readiness check failed: {message} — if it mentions consent/keywords, run 'service-de-channel-consent-configure' first.
    (就绪检查失败)
  • Error: Activation failed: {errorReason} — {errorDetails}
    (预配错误)
  • Error: WhatsApp phone number verification failed: {hint}
    (验证失败)
  • Error: Could not request verification code: {message}
    (请求验证码失败)
  • Timeout: Activation timed out after {elapsedS}s with MCU.DeploymentStatus={lastStatus}. Defensive-poll limit hit; this is unusual. Check MCU {mcuId} in Setup.
    (超时)

Worked examples

运行示例

For end-to-end activation traces (WhatsApp happy-path, Apple activation, a readiness-validator failure, and the already-active no-op), see
references/worked-examples.md
.

如需端到端激活跟踪示例(WhatsApp正常流程、Apple激活流程、就绪验证失败流程及已激活无操作流程),请查看
references/worked-examples.md

Gotchas

注意事项

Eleven known gotchas — synchronous PATCH timing for WhatsApp, valid API status transitions, the
IsActive
sync pass, relationship-name variance across orgs, consent preconditions,
DeploymentStatus
picklist casing, the REST-vs-Apex equivalence,
ErrorReason
values,
VERIFICATION_REQUIRED
handling, OAuth token extraction, and the Status-code-409 Admin API conflict. Before troubleshooting an unexpected result or modifying this skill, load
references/gotchas.md
and follow it.
已知的11个注意事项——WhatsApp的同步PATCH时序、有效的API状态转换、
IsActive
同步步骤、不同组织间的关系名称差异、同意前置条件、
DeploymentStatus
选择器大小写、REST与Apex的等效性、
ErrorReason
值、
VERIFICATION_REQUIRED
处理、OAuth令牌提取及状态码409的Admin API冲突。在排查异常结果或修改本技能前,请加载
references/gotchas.md
并按照其中的步骤操作。