mcpField
C
connector-twilio

connector-twilio

github.com/caffeinelabs其他211.7k 次安装6 天前更新
代码仓库

关于 connector-twilio

这是一个实验性的 Motoko 连接器,可从 Caffeine canister 使用 Twilio 消息功能,涵盖 SMS、MMS 和消息配置,不包含语音或通话 API;消息发送尚未经过验证。

主要功能

  1. 01发送 SMS 和 MMS 消息
  2. 02查询和列出消息记录
  3. 03管理消息服务与电话号码
  4. 04支持美国 A2P 10DLC 注册流程
  5. 05通过管理员权限配置凭据

适用场景

  1. 01从 Caffeine canister 发送通知
  2. 02查询发送状态或检索已发消息
  3. 03设置电话号码和 Twilio Messaging Service
  4. 04为美国消息注册 A2P 10DLC

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/caffeinelabs/skills --skill connector-twilio

安装后在 Claude Code 中输入/connector-twilio确认已加载。

详细介绍

Twilio 连接器(实验性)

从 Caffeine canister 发送 SMS / MMS 并配置 Twilio 消息服务。

⚠️ 实验性([email protected])——此客户端从未成功发送过消息。 它的写入路径直到最近才有可能正常工作:此前,每次写入都会丢弃参数并发送空请求体。现在,线路格式与 Twilio 文档一致(表单编码的请求体、百分号编码的值、省略可选字段),且全部 118 个文件均通过类型检查,但结构正确不代表已验证。将首次成功发送作为验收测试;在此之前,不要向用户宣传 Twilio 是完全受支持的平台功能。发送需要付费,因此实验失败也并非没有代价。

范围——此软件包仅涵盖消息功能。 twilio-client 精简为 35 个 API 模块(包含完整的 Messaging v1,以及 v2010 消息路径:Account、Message、Media、IncomingPhoneNumber 及其变体、AvailablePhoneNumber、A2P 注册)。Voice / 通话、录音、会议、队列、应用、SIP 和用量记录均不在此软件包中——如果构建需要这些功能,它们不属于此连接器。(统计:35 个 API 模块、82 个模型、118 个文件,全部通过类型检查。)

编排器路由说明

当用户、规格或先前任务提及发送短信、SMS / MMS、通过电话通知某人、购买或列出电话号码,或任何 Twilio 消息服务概念时,加载此技能。直接对 *.twilio.com 发起 ic.http_request 属于反模式,因为这会手动重新实现身份验证、主机路由、百分号编码和 JSON 解析;而且,如果实现草率,每条消息会发送约 13 次。

意图 → 功能映射:

用户意图功能
发送 SMS使用 from = Twilio 号码的 Api20100401MessageApi.createMessage
发送 MMS(图片)同上,设置 mediaUrl = ["https://…"] 和 sendAsMms = true
通过 Messaging Service 发送(美国流量推荐)同上,设置 from = "" + messagingServiceSid
检查送达状态fetchMessage(status、error_code)
列出 / 搜索已发送的消息listMessage(分页)
查看自有号码或浏览可用号码Api20100401IncomingPhoneNumberApi、…AvailablePhoneNumberCountryApi
设置 Messaging ServiceMessagingV1ServiceApi.createService
注册美国 A2P 10DLCMessagingV1BrandRegistrationApi → MessagingV1UsAppToPersonApi → MessagingV1PhoneNumberApi(按此顺序——见美国 A2P 10DLC)
验证免费电话号码MessagingV1TollfreeVerificationApi

Twilio 凭据需要由真人登录控制台获取,因此后端编译通过并不代表构建完成——还需要让应用告诉管理员去哪里获取凭据,并提供粘贴凭据的位置。请参阅身份验证模型,然后查看前端,了解必须随产品发布的页面,并在完成消息中重复这些步骤。

编写代码前先询问:由哪个号码发送? 面向美国用户的生产应用需要 Messaging Service + A2P 注册(准备时间需数周,并产生实际费用);演示 / 内部应用可以使用单个试用号码,但只能向已验证的收件人发送。请将选择及其影响反馈给发出请求的用户。

认证模型 — HTTP Basic,两种形式

两种形式使用相同的 #basicAuth { user; password } 凭证,客户端对它们的处理方式也完全相同;区别在于影响范围。

形式user / password适用场景
API Key (默认 — 优先使用此项)API Key SID (SK…) / 对应的 Secret生产环境。可撤销且权限受限:泄露一个密钥不会导致整个账户失陷。
账户 SID + Auth Token账户 SID (AC…) / Auth Token仅用于开发环境。Auth Token 就代表整个账户 — 它可以创建子账户、购买号码并产生费用。

无论使用哪种形式,每个 v2010 操作都必须将账户 SID (AC…) 作为位置参数传入(它位于 URL 路径中)。因此,使用 API Key 的应用需要存储三个值:账户 SID、Key SID、Key Secret。

获取凭证
  1. 在 https://console.twilio.com 登录。
  2. 账户 SID (AC…) 位于控制台仪表板上 — 将其复制。
  3. 在生产环境中,前往 Account → API keys & tokens → Create API key(Standard);复制 SID (SK…) 和 Secret。Secret 只会显示一次 — 如果管理员离开此页面,就无法找回,只能重新生成。 仅用于开发环境时,可改用仪表板上的 Auth Token。
  4. 购买发送号码:前往 Phone Numbers → Manage → Buy a number,并勾选 SMS 功能(并非每个号码都支持该功能)。
  5. 对于 试用账户:在 Phone Numbers → Verified Caller IDs 下验证每个收件人,否则发送会因 21608 而失败;试用消息还会带有 "Sent from your Twilio trial account" 前缀。
将凭证交给 canister

管理员通过一个仅限管理员的 setter 粘贴凭证 — 该 setter 通过 AccessControl.hasPermission(state, caller, #admin) 进行权限校验。凭证仅由 canister 持有,绝不会返回给前端。

⚠️ 绝不要通过“首个调用者获得所有权”的机制来保护 setter。 在 IC 上,所有未经身份验证的调用者都是同一个匿名 principal。因此,如果某个匿名调用先声明了所有权,所有匿名调用者都会通过 caller == owner 检查,并可以覆盖凭证 — 而这组凭证还会产生费用。

canister 仅通过 config.auth = ?#basicAuth { user; password } 将凭证交给客户端,每个方法都会将其转换为 Authorization: Basic … 标头。没有任何方法接受凭证参数,也没有方法将凭证放入 URL,因此凭证不会通过记录的查询字符串泄露。

Outcall 已默认不复制 — 更正此前的指导

defaultConfig 已设置 is_replicated = ?false,因此,通过记录更新从它派生的配置无需修改即可正确使用。不必记住任何内容,也不必额外设置。

⚠️ 不要将其设为 ?true 或 null,并忽略任何建议这样设置的旧说明。 此 SKILL 的旧版本声称写入应保持复制“以便 IC 共识去重重试”。这是错误的,而且成本高昂。 复制的 outcall 会由子网中的每个节点执行:请求会发送约 13 次,因此会发送约 13 条 SMS,并产生约 13 次费用;凭证也会离开每个节点。此外,由于 Twilio 会为每条回复加上唯一的 sid(因此各条响应的字节内容永远无法完全一致),共识仍会失败。这是 Gmail 连接器通过 slack-client 0.1.0 导致约 13 封重复邮件的同类缺陷。

读取操作(fetch* / list*)同样适合采用不复制方式:你需要的正是某个节点所见的消息日志,而且这也是成本更低的路径。

后端

添加依赖项

以下示例中的管理员权限校验需要在客户端之外添加授权组件:

mops add [email protected]
mops add [email protected]

调用形式 — 自由函数或类门面

每个模块都提供这两种形式。自由函数将 config 作为第一个参数,并且是 async*; module class 会捕获 config,并且是 async:

// Illustrative sketch, not a file to copy: `cfg`/`accountSid` are assumed to
// exist and the argument lists are elided. Marked motoko-check:skip for that
// reason — the compiled examples are the three mixins below.
import MessageApi "mo:twilio-client/Apis/Api20100401MessageApi";

// free function — config passed explicitly
let m = await* MessageApi.createMessage(cfg, accountSid, /* … */);

// class facade — config captured once
let messages = MessageApi.Api20100401MessageApi(cfg);
let m2 = await messages.createMessage(accountSid, /* … */);

所有参数都是位置参数,createMessage 一共有 27 个参数。 对于不使用的参数,传入 "" / false / 0 / 0.0 / [] / null — 可选枚举参数恰好使用 ?T,因此传入 null 会将其从请求中省略。 请仔细核对;空字符串放错位置会悄悄发送错误的字段。参数顺序如下:

config, accountSid, to, statusCallback, applicationSid, maxPrice, provideFeedback, attempt, validityPeriod, forceDelivery, contentRetention, addressRetention, smartEncoded, persistentAction, trafficType, shortenUrls, scheduleType, sendAt, sendAsMms, contentVariables, riskCheck, from, fallbackFrom, messagingServiceSid, body, mediaUrl, contentSid

操作步骤

import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import MixinTwilioConfig "mixins/twilio-config";
import MixinTwilioMessaging "mixins/twilio-messaging";

actor {
  let accessControlState : AccessControl.AccessControlState;
  include MixinAuthorization(accessControlState, null);

  // Admin-held Twilio credentials — never returned to the frontend.
  let twilioConfig : {
    var accountSid : Text; // AC… — also a positional arg on every v2010 call
    var keySid : Text; // SK… (or the Account SID again, in dev)
    var keySecret : Text; // the API-key secret (or the Auth Token, in dev)
    var fromNumber : Text; // E.164, e.g. "+15551234567"
  };
  include MixinTwilioConfig(accessControlState, twilioConfig);
  include MixinTwilioMessaging(twilioConfig);
};

迁移链的头部:

import AccessControl "mo:caffeineai-authorization/access-control";

module {
  type NewActor = {
    accessControlState : AccessControl.AccessControlState;
    twilioConfig : {
      var accountSid : Text;
      var keySid : Text;
      var keySecret : Text;
      var fromNumber : Text;
    };
  };

  public func migration(_old : {}) : NewActor {
    {
      accessControlState = AccessControl.initState();
      twilioConfig = {
        var accountSid = "";
        var keySid = "";
        var keySecret = "";
        var fromNumber = "";
      };
    };
  };
};
import AccessControl "mo:caffeineai-authorization/access-control";
import Runtime "mo:core/Runtime";

mixin (
  accessControlState : AccessControl.AccessControlState,
  twilioConfig : {
    var accountSid : Text;
    var keySid : Text;
    var keySecret : Text;
    var fromNumber : Text;
  },
) {
  // All THREE are required, and this must agree with the guard in
  // twilio-messaging.mo: `keySid` is the Basic-Auth *username*, so a blank one
  // means every request goes out unauthenticated and Twilio answers 20003 —
  // while the UI cheerfully reports "Configured".
  public query func isTwilioConfigured() : async Bool {
    twilioConfig.accountSid.size() > 0 and twilioConfig.keySid.size() > 0 and twilioConfig.keySecret.size() > 0;
  };

  // The sending number is not a secret — the UI may display it.
  public query func getTwilioFromNumber() : async Text {
    twilioConfig.fromNumber;
  };

  // Admin-only. NOTE `#admin` — never a first-caller-claims-ownership check,
  // which the shared anonymous principal would defeat.
  public shared ({ caller }) func setTwilioCredentials(
    accountSid : Text,
    keySid : Text,
    keySecret : Text,
  ) : async () {
    if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
      Runtime.trap("Unauthorized: Only admins can set Twilio credentials");
    };
    twilioConfig.accountSid := accountSid;
    twilioConfig.keySid := keySid;
    twilioConfig.keySecret := keySecret;
  };

  public shared ({ caller }) func setTwilioFromNumber(number : Text) : async () {
    if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
      Runtime.trap("Unauthorized: Only admins can set the sending number");
    };
    twilioConfig.fromNumber := number;
  };
};
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
import { createMessage } "mo:twilio-client/Apis/Api20100401MessageApi";
import { defaultConfig; type Config } "mo:twilio-client/Config";

mixin (
  twilioConfig : {
    var accountSid : Text;
    var keySid : Text;
    var keySecret : Text;
    var fromNumber : Text;
  },
) {
  // Credentials ride config.auth; defaultConfig is already non-replicated.
  func twilioClientConfig() : Config {
    {
      defaultConfig with
      auth = ?#basicAuth { user = twilioConfig.keySid; password = twilioConfig.keySecret };
      max_response_bytes = ?(200_000 : Nat64);
    };
  };

  /// Send an SMS to `to` (E.164). Returns the message SID.
  public shared ({ caller }) func sendSms(to : Text, body : Text) : async Text {
    if (caller.isAnonymous()) Runtime.trap("Sign in to send messages");
    // Same three-way check as isTwilioConfigured(): accountSid goes in the URL
    // path, keySid is the Basic-Auth user, keySecret the password. Missing any
    // one of them fails at Twilio, not here, so check before spending cycles.
    if (
      twilioConfig.accountSid.size() == 0 or twilioConfig.keySid.size() == 0 or twilioConfig.keySecret.size() == 0
    ) {
      Runtime.trap("Twilio is not configured (an admin must set all three credentials)");
    };
    let msg = await* createMessage(
      twilioClientConfig(),
      twilioConfig.accountSid, // accountSid — in the URL path, not the credential
      to, // to (E.164)
      "", // statusCallback
      "", // applicationSid
      0.0, // maxPrice (0 = no cap)
      false, // provideFeedback
      0, // attempt
      0, // validityPeriod
      false, // forceDelivery
      null, // contentRetention (omitted)
      null, // addressRetention (omitted)
      false, // smartEncoded
      [], // persistentAction
      null, // trafficType (omitted)
      false, // shortenUrls
      null, // scheduleType — MUST be null for an immediate send
      "", // sendAt (scheduled sends only)
      false, // sendAsMms
      "", // contentVariables
      null, // riskCheck (omitted)
      twilioConfig.fromNumber, // from  (use EITHER from OR messagingServiceSid)
      "", // fallbackFrom
      "", // messagingServiceSid
      body, // body
      [], // mediaUrl (set for MMS)
      "", // contentSid (Content API templates)
    );
    // `sid` is optional in the generated model because the spec marks it
    // nullable, though Twilio always sets it on a successful create. Fall back
    // to "" rather than trapping: the outcall has already happened, so a trap
    // would roll back this canister's own state while the SMS stays delivered.
    switch (msg.sid) { case (?sid) sid; case null "" };
  };
};

对于 MMS:设置 mediaUrl = ["https://example.com/image.jpg"] 和 sendAsMms = true。 要通过 Messaging Service 发送,请将 from = "",并改为设置 messagingServiceSid。

地址格式 — E.164,以及发送方选择

  • to 必须采用 E.164 格式:+、国家代码,且不能有空格、连字符或括号 — "+15551234567"。"555-1234" 会因 21211 错误而失败。在前端进行规范化, 并在 canister 中再次规范化;不要单独信任任何一方。
  • from 和 messagingServiceSid — 必须且只能设置一个。 两者同时设置会报错。 对于非美国流量和演示,直接使用 from 号码即可;发往美国的生产流量 应通过 Messaging Service 发送(发送方池、固定发送方;A2P 注册也绑定到该服务)。
  • 发送号码必须具备 SMS 功能,并非所有可购买的号码都具备此功能。浏览 Api20100401AvailablePhoneNumberCountryApi 时,请按此功能筛选。

美国 A2P 10DLC — 按顺序创建三个资源

任何美国长号码向美国目的地发送短信之前,必须先创建这三项资源。缺少这些资源时, 美国运营商会直接拒绝流量。

  1. 品牌注册 — MessagingV1BrandRegistrationApi.createBrandRegistrations, 引用 Trust Hub 的 customerProfileBundleSid + a2PProfileBundleSid (需在此流程之外创建)。在开发环境中传入 mock = true 可跳过费用。状态初始为 PENDING,数小时至数天后会变为 APPROVED / FAILED;如果企业信息不完整、 格式不一致,或与注册信息不匹配,注册就会失败。
  2. A2P 活动 — MessagingV1UsAppToPersonApi.createUsAppToPerson, 同时引用 Messaging Service 和品牌。大多数入驻失败都发生在这一步。 如果 messageFlow 未说明用户如何选择加入,或 messageSamples 与声明的 usAppToPersonUsecase 不匹配,T-Mobile 就会拒绝该活动。
  3. 号码 → 服务分配 — MessagingV1PhoneNumberApi.createPhoneNumber(cfg, serviceSid, phoneNumberSid)。 一个号码同一时间只能属于一个 Messaging Service;重新分配前必须先调用 deletePhoneNumber。

当前生效的注册截止日期: 如果没有可用的 privacyPolicyUrl 和 termsAndConditionsUrl,自 2026-06-30 起请求会直接返回 400 错误。在 createUsAppToPerson 中,这两个 URL 都是位置参数,传入 "" 会失败;URL 必须指向 公开的 HTTPS 页面,因为 Twilio 会在注册期间访问这些页面。

免费电话使用单独的流程 — MessagingV1TollfreeVerificationApi — 不走 A2P。

可用的 API 范围

本文配方侧重文档和消息功能:

模块用途
Api20100401MessageApi发送 / 获取 / 列出 / 更新 / 删除消息
Api20100401MediaApi, …MediaInstanceApi消息上的 MMS 媒体
Api20100401IncomingPhoneNumberApi(+ Local/Mobile/TollFree)你拥有的号码;删除 = 释放
Api20100401AvailablePhoneNumberCountryApi浏览可购买的号码
Api20100401BalanceApi, …AccountApi账户余额和账户记录
Api20100401UserDefinedMessageApi(+ Subscription)用户定义的消息事件
MessagingV1ServiceApiMessaging Services(发送方池)
MessagingV1BrandRegistrationApi(+ Otp, BrandVettingApi)A2P 品牌
MessagingV1UsAppToPersonApi(+ UsecaseApi)A2P 活动
MessagingV1PhoneNumberApi, …ShortCodeApi, …AlphaSenderApi, …ChannelSenderApi发送方池成员
MessagingV1TollfreeVerificationApi免费电话号码验证
MessagingV1Linkshortening*, …DomainConfig*, …DomainCertsApi品牌化链接缩短
MessagingV1DeactivationsApi运营商停用列表

此包不包含(已从生成的接口中裁剪):通话、录音、会议、参与者、队列、应用、SIP 域和凭据、用量记录和触发器、地址、密钥、令牌、余额交易。此包仅提供消息功能;以上内容均不适用于此连接器。

错误和分页

  • 方法在 2xx 状态码时返回解码后的记录,在 4xx/5xx 状态码时throw Error.reject("HTTP <status> body[…]: …")。已启用 diagnostics,因此 reject 文本会包含 Twilio 自身的错误正文(code、message、more_info)。请用 try { … } catch (e) { Error.message(e) } 进行包装。
  • 值得映射为实际 UI 文本的代码:20003 身份验证失败(凭据错误),21211 To 无效,21408 区域未获授权(在控制台中启用目标国家/地区的地理权限),21608 试用账户中的收件人未验证,21610 收件人已退订(STOP),21703 发送方池已耗尽,21704 Messaging Service 没有号码,21714 池大小已达上限。
  • 2xx 不代表已送达。 createMessage 会返回 status = #queued 或 #accepted;送达是异步的。请轮询 fetchMessage,检查是否为 #delivered / #undelivered / #failed,并读取 error_code;也可以配置 statusCallback URL(需要入站 HTTP 端点,本文不涉及)。
  • 两个 API 版本的分页方式不同。 v2010 列表——listMessage 和所有其他 Api20100401* 列表——返回顶层 next_page_uri / previous_page_uri(?Text,以及 /2010-04-01/… 这样的路径,而非完整 URL)。Messaging v1 列表(listService、listPhoneNumber、A2P 注册表)的分页信息则嵌套在 meta 下,字段为 next_page_url / previous_page_url(完整 URL)和 page_size。70 个列表响应中只有 10 个使用 meta 格式;listMessage 不在其中。每个 v1 列表的 meta 字段都被类型化为 ?ListAlphaSenderResponseMeta,包括 ListServiceResponse——代码生成时,相同记录会去重并合并到一个共享模块中,因此该名称取决于排序最靠前的列表,而非你调用的端点。
  • pageSize 默认为 50,最大为 1000。每次列表调用都应设置上限;在繁忙账户上不设上限的 listMessage 会导致 max_response_bytes 超限。

查看这两种格式:

// Illustrative sketch, not a file to copy — `res` is assumed to be the decoded
// list response. Marked motoko-check:skip for that reason.

// v2010 (listMessage and every other Api20100401* list): top-level, a path
switch (res.next_page_uri) { case (?path) { /* fetch the next page */ }; case null {} };

// Messaging v1 (listService, listPhoneNumber, the A2P registries): nested, a full URL
switch (res.meta) { case (?m) { m.next_page_url }; case null null };

字段注意事项

  • createService 中的 usecase 是 Text,不是变体。有效值:notifications、 marketing、verification、discussion、poll、undeclared。任何其他值都会返回 400。
  • usAppToPersonUsecase 是一个不同的枚举,其可用项取决于品牌层级——查询 MessagingV1UsAppToPersonUsecaseApi.fetchUsAppToPersonUsecase,了解特定品牌可以使用哪些值。
  • 可选枚举参数的类型为 ?T——传入 null 表示省略,且应优先采用这种方式。 这些变体是封闭的:contentRetention 为 #retain/#discard, addressRetention 为 #retain/#obfuscate,trafficType 为 #free, scheduleType 为 #fixed,riskCheck 为 #enable/#disable。没有 #Text 变通选项——无法表示规范中未列出的值。立即发送时, 为 scheduleType 传入 ?#fixed 会导致 400: Twilio 会将其视为定时消息,随后发现没有 SendAt。除非你明确需要相应行为,否则这些参数的正确值都是 null。
  • **maxPrice 为 0.0 时会被省略,这正是你想要的。**发送 MaxPrice=0 会将消息价格上限设为零,导致 Twilio 拒绝付费投递;省略该参数则表示“无上限”。传入 0.0 即可省略。
  • xTwilioApiVersion(用于 UsAppToPerson 方法)——除非 Twilio 支持人员另有要求,否则传入 ""。
  • **吞吐量按发送方计算:**长号码为每秒 1 条消息,免费电话约为 3 条, 国际长号码约为 10 条,短号码为 100 条。无法提高单个号码的 MPS—— 可通过向 Messaging Service 的发送方池添加号码来扩容。
  • stickySender / areaCodeGeomatch 仅适用于美国和加拿大。
  • Config.baseUrl 未被使用。每项操作都带有硬编码主机 (v2010 使用 api.twilio.com,v1 使用 messaging.twilio.com),并在代码生成时从合并后的规范中固定下来。不要设置它,也不要指望它能重定向流量。

前端

Twilio 不需要 OAuth:凭据是一组由管理员粘贴的长期有效信息,因此不需要重定向 URI、不需要 /connect/twilio 路由,也不需要按用户进行握手。不要构建这些功能。Twilio 集成必须提供的功能是让管理员能够获取并输入凭据的页面——这是验收标准,不是建议;缺少这些功能的构建是有缺陷的,而非仅仅不完整:

  • **凭据页面必须存在且可访问。**没有输入凭据入口的“发送 SMS”功能无法使用。已登录的管理员必须能够从导航栏或未配置提示访问 /settings/twilio。
  • 控制台操作步骤必须显示在 UI 中,不能只在聊天回复中提供——管理员可能在数周后才回来,而聊天记录已不复存在。
  • **API-Key 密钥由 Twilio 仅显示一次。**应在输入框旁注明这一点,否则管理员离开页面后就不得不再创建一个密钥。
  1. 必须提供登录流程。setTwilioCredentials 仅允许 #admin,因此应用需要支持非匿名调用方。请从 extension-authorization 中获取登录、 useInternetIdentity / useActor 接线代码和管理员角色门控。

  2. 管理设置页面 — /settings/twilio(仅管理员可访问)。要求:

    • 在输入框上方显示“如何获取 Twilio 凭据”面板,将其描述为一次性约 5 分钟的设置流程,并列出以下编号步骤(完成消息必须逐字重复这些步骤):
      1. 登录 https://console.twilio.com;
      2. 从控制面板复制 Account SID(AC…);
      3. 选择 Account → API keys & tokens → Create API key(Standard);复制 SID(SK…)和 Secret — Secret 只会显示一次;
      4. 选择 Phone Numbers → Manage → Buy a number,并启用 SMS 功能;
      5. 在下方粘贴这三个值和号码,然后保存;
      6. 使用试用账户时,在 Verified Caller IDs 下验证每个收件人。 提供一个便捷链接,用于打开 Twilio 控制台。
    • 三个输入框:Account SID(纯文本,不是密钥)、Key SID、Key Secret(密码输入框)。绑定到 setTwilioCredentials;成功后清除 Secret;保留可重新提交的表单,因为密钥可能会轮换。
    • 一个绑定到 setTwilioFromNumber 的发送号码字段,旁边提供 E.164 格式示例(+15551234567),并进行客户端验证。
    • 状态由 isTwilioConfigured()(Bool)驱动 — “已配置”/“未配置”。该谓词要求全部三个值,包括 Key SID:它是 Basic-Auth 用户名,因此留空会导致每个请求都未经身份验证,Twilio 会返回 20003,而页面却显示已配置。绝不要回显 Secret,即使是掩码形式也不行。可以显示发送号码(getTwilioFromNumber);它不是密钥。
    • 确保页面可访问。 当 isCallerAdmin 为 true 时,共享 Layout 导航栏必须链接到此页面;否则隐藏该链接。在导航栏定义处添加链接,不要在此页面内添加。
  3. 空状态提示。 当 isTwilioConfigured() 为 false 时,绝不要显示不可用的“发送”按钮:管理员应看到指向 /settings/twilio 的“设置 Twilio”链接;非匿名的非管理员用户应看到说明,例如:“尚未设置短信功能 — 管理员需要在设置中添加 Twilio 凭据。”

  4. 翻译 Twilio 错误。 失败会以携带 Twilio code 的拒绝调用形式返回。至少将以下错误映射为解决操作,而不是显示原始拒绝信息:

    • 20003 → “Twilio 凭据有误 — 管理员应重新粘贴凭据”
    • 21211 → “电话号码无效 — 请使用 +15551234567 格式”
    • 21408 → “此 Twilio 账户尚未启用向该国家/地区发送短信的功能”
    • 21608 → “使用试用账户时,必须先在 Twilio 中验证收件人”
    • 21610 → “该号码已回复 STOP,无法向其发送短信”
  5. 绝不要承诺消息已送达。 调用成功表示消息已排队,而非已送达。界面应使用相应措辞(“消息已排队”);如果需要关注送达情况,则显示通过 fetchMessage 轮询获得的 status。

建议的路由布局:

/                 →  Main UI (any signed-in user; empty-state when unconfigured)
/settings/twilio  →  Admin credentials + sending number (admin-only)
# No /connect/twilio: Twilio uses pasted long-lived credentials, not a redirect flow.

撰写者必须告知 Caffeine 用户的内容

在人工创建 Twilio 账户、购买号码并粘贴凭据之前,应用无法发送任何内容——因此,完成消息是交付内容的一部分,而不是对它的总结。它必须按以下顺序包含:

  1. 说明需要凭据,以及由谁填写——管理员在登录后前往 /settings/twilio 填写,该页面可从导航栏访问。
  2. 原文列出 Frontend 第 2 项中的六个编号步骤,包括说明 API 密钥的 Secret 仅显示一次。
  3. 说明 Twilio 服务需要付费——按消息计费,另加每月号码费用;试用账户只能向已验证的号码发送短信,而且每条消息都会带有试用通知前缀。
  4. 对于发往美国的流量:说明 A2P 10DLC 要求,明确提到需要数周的准备时间和额外费用,并按顺序列出三个资源——否则,用户发布的应用可能会无法向美国手机发送消息,而且不会收到任何提示。
  5. 逐行列出故障排查映射:20003 → 重新粘贴凭据;21211 → 使用 E.164 格式;21408 → 启用目标国家/地区;21608 → 验证收件人(试用账户);21610 → 收件人已退订。

不要将这些内容缩减成“在 Settings 中配置 Twilio”,也不要换成 Twilio 文档链接。此处的措辞必须与设置页面面板中的措辞一致,以免两处内容不一致。

已知限制

  • 仅提供消息功能。 此软件包已精简至消息路径;不包含语音、录音、SIP、用量以及其他功能。
  • 不在范围内的是接收消息。 接收 SMS 和 statusCallback 投递回执需要 canister 上有一个入站 HTTP 端点;这属于另一个组件,不属于此客户端。
  • 无法上传二进制媒体。 mediaUrl 接受 Twilio 会抓取的公开 URL;canister 无法通过此客户端 POST 图像字节。
  • 没有幂等键。 Twilio 的消息 API 不支持幂等键,因此超时后重试可能会导致重复发送。应在应用层进行防护(为每次逻辑发送使用一个稳定变量去重键),而不是盲目重试。非副本默认设置消除了约 13 倍的放大效应,但不会改变重试语义。
  • 有一个字段被丢弃。 POST …/IncomingPhoneNumbers/{Sid}.json 接受一个 AccountSid 表单字段(用于在子账户之间移动号码),同时 AccountSid 也是它的路径参数。生成器对两者使用同一个命名空间,因此表单字段副本会被丢弃,无法通过此客户端将号码转移到子账户。其他所有端点均不受影响。
  • 这里的任何内容都未经过真实 Twilio 环境的测试。 线路格式至少在结构上是正确的——写入操作会发送一个包含百分号编码参数的 application/x-www-form-urlencoded 请求体,这是 Twilio 要求的格式——但尚未发起任何调用。应将首次成功发送视为真正的验收测试。
  • 规范版本: 根据 Twilio 发布的 OpenAPI 规范(由 spec-merge 合并的 Messaging v1 + API v2010)生成,之后精简至消息功能范围。此处不包含这些规范中缺失的较新 Twilio 功能。

相关内容

评论

支持 Markdown