其他
在 Caffeine canister 中通过…
当用户、规格或既有任务提到 SMS、MMS、短信、电话号码、Twilio、Messaging Service、A2P 10DLC、toll-free 验证、短码或字母数字发送方时,用 twilio-client mops 包从 canister 发送短信/彩信并配置 Twilio 消息功能。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 35/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 38/100 ×0.15
适用场景
在 Caffeine canister 特定
解决思路
- 在写任何触碰 Twilio 端点的代码之前加载本技能;禁止手写 ic.http_request 调用 api.twilio.com 或 messaging.twilio.com,必须使用 twilio-client 包以获得类型化绑定、按操作的主机路由、Basic-Auth 头构造,以及默认非复制出站调用(避免一次 send 变成约 13 条计费消息)。
- 在构建前先向用户说明两件事:该包为实验性、尚未对真实 Twilio 验证过;每条消息都计费,且面向美国的正式号码还需要 A2P 10DLC 注册(有费用、需数周前置时间)。
- 先询问:用哪个号码发送?面向美国的正式应用需要 Messaging Service + A2P 注册(数周前置时间、真实费用);演示/内部应用可只用单个试用号向已验证收件人发送。把选择及其后果回报给用户。
- 添加依赖:mops add twilio-client@0.1.2 和 mops add caffeineai-authorization@1.0.1。
- 选择鉴权方式:默认优先用 API Key(user = API Key SID `SK…`,password = 其 Secret),生产环境可撤销且可限定范围;仅开发环境用 Account SID(`AC…`)+ Auth Token。Account SID 同时是每个 v2010 操作的位置参数(在 URL 路径中),所以用 API Key 的应用要存三个值:Account SID、Key SID、Key Secret。
- 获取凭据:1) 登录 https://console.twilio.com;2) 从控制台仪表盘复制 Account 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:管理员通过 admin 门控的 setter 粘贴,门控条件为 AccessControl.hasPermission(state, caller, #admin);凭据只由 canister 持有,绝不返回前端。
- 绝不要把 setter 门控在“首个调用者声明所有权”的方案上:IC 上每个未认证调用者都是同一个匿名 principal,若匿名调用先声明所有权,之后每个匿名调用者都能通过 caller == owner 检查并覆盖凭据,而该凭据会花钱。
- canister 只通过 config.auth = ?#basicAuth { user; password } 把凭据交给客户端,每个方法据此生成 Authorization: Basic … 头;没有方法接受凭据参数,也没有方法把凭据放进 URL,因此不会通过日志查询字符串泄漏。
- 出站调用已默认非复制:defaultConfig 自带 is_replicated = ?false,任何由记录更新派生的配置都正确,无需额外设置。不要把它设为 ?true 或 null,并忽略旧版建议“写操作保持复制以便 IC 共识去重重试”——那是错误且昂贵的:复制出站调用会被子网中每个节点执行,请求发送约 13 次,约 13 条 SMS 被发送并计费,凭据离开每个节点,且共识仍会失败,因为 Twilio 给每个回复打上唯一 sid。读操作(fetch*/list*)同样适合非复制。
- 调用形态:每个模块同时提供自由函数(config 作为第一个参数,async*)和 module class 门面(捕获 config,async)。
- 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。
- 按配方接线:actor 中 include MixinAuthorization(accessControlState, null),定义 twilioConfig(var accountSid/keySid/keySecret/fromNumber),include MixinTwilioConfig(accessControlState, twilioConfig),include MixinTwilioMessaging(twilioConfig);迁移链头用 AccessControl.initState() 初始化,四个字段初始为 ""。
- twilio-config.mixin 中:isTwilioConfigured() 必须同时检查 accountSid、keySid、keySecret 三者非空(keySid 是 Basic-Auth 用户名,为空会导致每个请求未认证、Twilio 返回 20003,而 UI 却显示“已配置”);getTwilioFromNumber() 可返回发送号码(非机密);setTwilioCredentials 和 setTwilioFromNumber 都门控 #admin,否则 Runtime.trap。
- twilio-messaging.mixin 中:twilioClientConfig() 用 { defaultConfig with auth = ?#basicAuth { user = keySid; password = keySecret }; max_response_bytes = ?(200_000 : Nat64) };sendSms 先拒绝匿名调用者,再检查三个凭据非空,然后 await* createMessage,按上述 27 参数顺序传参,from 用 twilioConfig.fromNumber,scheduleType 传 null(立即发送必须为 null),maxPrice 传 0.0(省略即无上限),最后 switch (msg.sid) { case (?sid) sid; case null "" } 而不是 trap(出站已发生,trap 会回滚 canister 状态但短信已送达)。
- MMS:mediaUrl = ["https://example.com/image.jpg"] 且 sendAsMms = true。通过 Messaging Service 发送:from = "" 并设置 messagingServiceSid。
- 地址格式: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 和品牌,多数 onboarding 失败在此,T-Mobile 会拒绝 messageFlow 未描述 opt-in 或 messageSamples 与声明的 usAppToPersonUsecase 不符的活动;3) 号码→服务分配 MessagingV1PhoneNumberApi.createPhoneNumber(cfg, serviceSid, phoneNumberSid),一个号码同时只属于一个 Messaging Service,重新分配需先 deletePhoneNumber。
- 注册截止要求:自 2026-06-30 起,没有可用的 privacyPolicyUrl 和 termsAndConditionsUrl 的活动会硬 400;两者是 createUsAppToPerson 的位置参数,传 "" 会失败,URL 必须解析到公开 HTTPS 页面,因为 Twilio 注册时会抓取。
- toll-free 号码走单独流程 MessagingV1TollfreeVerificationApi,不是 A2P。
- 错误处理:方法在 2xx 返回解码记录,4xx/5xx 抛 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 不在其中。meta 字段在每个 v1 列表上都类型为 ?ListAlphaSenderResponseMeta,包括 ListServiceResponse——代码生成时相同记录被去重为一个共享模块,名字反映的是排序最靠前的列表,不是你调用的端点。
- pageSize 默认 50、上限 1000。每个列表调用都要设界——繁忙账号上无界的 listMessage 会撑爆 max_response_bytes。
- 字段陷阱: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 即省略。
- UsAppToPerson 方法上的 xTwilioApiVersion 除非 Twilio 支持要求,否则传 ""。
- 吞吐量按发送方计:长码 1 条/秒、toll-free 约 3、国际长码约 10、短码 100。每号码 MPS 无法提高——通过向 Messaging Service 的发送方池添加号码来扩展。
- stickySender / areaCodeGeomatch 仅限美国和加拿大。
- Config.baseUrl 未使用:每个操作携带硬编码主机(v2010 用 api.twilio.com,v1 用 messaging.twilio.com),在代码生成时从合并规格固定。不要设置它,也不要指望它重定向流量。
- 前端:Twilio 不需要 OAuth,凭据是管理员粘贴的长期有效对,没有重定向 URI、没有 /connect/twilio 路由、没有每用户握手,不要构建。必须交付凭据页面,这是验收标准而非建议,缺失即为损坏而非不完整。
- 凭据页面必须存在且可达:已登录管理员能从导航或未配置提示到达 /settings/twilio;控制台步骤要写在 UI 里而不只是聊天回复里(管理员数周后回来,聊天已不在);API-Key Secret 只显示一次,要在输入框旁说明,否则管理员会离开页面并不得不创建第二个 key。
- 登录流程是必需的:setTwilioCredentials 门控 #admin,所以应用需要非匿名调用者。从 extension-authorization 技能取登录、useInternetIdentity/useActor 接线和 admin 角色门控。
- 管理员设置页 /settings/twilio(admin 门控)必须包含:输入框上方的“如何获取 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),它不是机密。
- 让页面可达:共享 Layout 导航在 isCallerAdmin 为 true 时必须链接到此页,否则隐藏。在定义导航的地方加链接,不要加在本页内部。
- 空状态提示:isTwilioConfigured() 为 false 时绝不渲染死的“发送”按钮;管理员看到指向 /settings/twilio 的“设置 Twilio”链接;非匿名的非管理员看到解释,如“短信尚未设置——管理员需要在设置中添加 Twilio 凭据。”
- 翻译 Twilio 错误:失败以携带 Twilio code 的 rejected 调用到达。至少把这些映射为可操作提示而非原始 reject:20003 → “Twilio 凭据有误——管理员应重新粘贴”;21211 → “该电话号码无效——请使用 +15551234567 格式”;21408 → “此 Twilio 账号未启用向该国发短信”;21608 → “试用账号下收件人必须先在 Twilio 中验证”;21610 → “该号码已回复 STOP,无法发送短信”。
- 绝不承诺送达:成功调用只意味着已排队,不是已送达。UI 措辞相应(“消息已排队”),若送达重要则显示 fetchMessage 轮询到的 status。
- 路由布局:/ → 主 UI(任何已登录用户;未配置时显示空状态);/settings/twilio → 管理员凭据 + 发送号码(仅管理员);没有 /connect/twilio,因为 Twilio 用粘贴的长期凭据而非重定向流程。
- 完成消息是交付物的一部分,必须按顺序包含:1) 需要凭据以及由谁输入(管理员,在 /settings/twilio,登录后从导航可达);2) 逐字重复六步编号步骤,包括 API-key Secret 只显示一次;3) Twilio 要花钱——按消息计费加每月号码费,试用账号只能给已验证号码发短信且每条消息带试用提示;4) 面向美国流量需 A2P 10DLC,说明数周前置时间和额外费用,以及三个有序资源,否则用户会发布一个静默无法触达美国手机的应用;5) 失败映射,每行一条:20003 → 重新粘贴凭据;21211 → E.164 格式;21408 → 启用目的地国家;21608 → 验证收件人(试用);21610 → 收件人已退订。不要压缩成“在设置中配置 Twilio”,也不要替换为 Twilio 文档链接。与设置页面板用同样措辞以免漂移。
- 已知限制需知悉:只发布消息面(35 个 API 模块、82 个模型、118 个文件),语音/录音/会议/队列/应用/SIP/用量记录不在包内;入站消息和 statusCallback 送达回执超出范围,需要 canister 上的入站 HTTP 端点;二进制媒体不可上传,mediaUrl 只接受 Twilio 抓取的公开 URL;没有幂等键,超时后重试可能发两次,应在应用层用稳定变量按逻辑发送去重,而非盲目重试;POST …/IncomingPhoneNumbers/{Sid}.json 的 AccountSid 表单字段(用于在子账号间移动号码)与路径参数同名,生成器只有一个命名空间,表单副本被丢弃,因此无法通过此客户端把号码转移到子账号;没有任何东西对真实 Twilio 演练过,首次成功发送才是真正的验收测试;规格版本为 Twilio 公开 OpenAPI 规格经 spec-merge 合并(Messaging v1 + API v2010)后裁剪到消息面,较新的 Twilio 功能不在此。
- 相关资源:mops add twilio-client@0.1.2(生成的 Twilio REST 绑定,35 个消息模块);Twilio Messaging 文档;Message 资源快速入门;Twilio 错误码;A2P 10DLC 概览;API keys vs Auth Token;extension-authorization 技能(必需前置:Internet Identity 登录、useInternetIdentity/useActor 接线、凭据 setter 需要的 #admin 门控)。
适用边界 · 注意事项
- 不要用于语音/通话、录音、会议、队列、应用、SIP、用量记录等非消息功能——这些不在 twilio-client 包内。
- 不要用于入站 SMS 接收或 statusCallback 送达回执——需要 canister 上的入站 HTTP 端点,属于不同组件。
- 不要用于上传二进制媒体——mediaUrl 只接受 Twilio 抓取的公开 URL,canister 无法通过此客户端 POST 图片字节。