April 20, 2026

[WhatsApp发信失败与允许名单策略] - WhatsApp Outbound Send Failure with Allowlist Policy

当使用 WhatsApp dmPolicy 'allowlist' 时,出站消息发送失败,错误信息为'Delivering to WhatsApp requires target',原因是 sendTo 目标必须在同一个控制入站访问的 allowFrom 列表中。

🔍 症状

主要错误表现

当尝试使用 message 工具向不在 allowFrom 列表中的联系人发送 WhatsApp 消息时:

Error: Delivering to WhatsApp requires target <E.164|group JID>
    at resolveOutboundTarget (src/whatsapp/resolve-outbound-target.ts:XX)
    at sendWhatsAppMessage (src/whatsapp/sender.ts:XX)

配置上下文

当存在以下配置时会出现此问题:

json { “channels”: { “whatsapp”: { “dmPolicy”: “allowlist”, “allowFrom”: ["+1234567890"] } } }

CLI 诊断命令

bash

尝试向未在列表中的联系人发送消息

$ openclaw tools call message ‘{“to”: “+0987654321”, “body”: “Hello”}’

预期:消息发送成功

实际:错误 - 发送到 WhatsApp 需要目标 <E.164|group JID>

次要症状:令人困惑的安全模型

用户观察到将联系人添加到 allowFrom 具有双重效果:

  • 该联系人现在可以接收来自机器人的出站消息
  • 该联系人也可以发送入站消息来触发机器人

这违反了最小权限原则,并造成安全混淆。

🧠 根因分析

架构分析

根本原因在于入站和出站访问控制逻辑之间的共享数据依赖

文件:src/whatsapp/resolve-outbound-target.ts

typescript export async function resolveOutboundTarget( normalizedTo: string, allowList: string[] ): Promise {

const hasWildcard = allowList.includes("*");

if (hasWildcard || allowList.length === 0) { return { ok: true, to: normalizedTo }; }

if (allowList.includes(normalizedTo)) { return { ok: true, to: normalizedTo }; }

return { ok: false, error: Delivering to WhatsApp requires target <E.164|group JID>, }; }

问题所在:此函数接收 allowFrom 数组作为 allowList 参数,这意味着出站权限由入站配置控制。

文件:src/web/inbound/access-control.ts

typescript export function checkInboundAccess( from: string, allowFrom: string[] ): InboundAccessResult { const hasWildcard = allowFrom.includes("*"); const isAllowed = hasWildcard || allowFrom.includes(from);

return { allowed: isAllowed, reason: isAllowed ? “allowed” : “inbound_not_authorized” }; }

共享控制点问题

配置入站效果出站效果
"allowFrom": ["+1234567890"]仅 +1234567890 可以触发机器人机器人只能发送给 +1234567890
"allowFrom": ["*"]任何人都可以触发机器人机器人可以发送给任何人

设计违规

当前实现违反了关注点分离原则。allowFrom 字段本应为入站访问控制而设计,但被重用于出站授权,造成了意想不到的耦合。

🛠️ 逐步修复

阶段 1:添加配置类型

文件src/config/types.whatsapp.ts

修改前: typescript export interface WhatsAppConfig { dmPolicy: “allowlist” | “open”; allowFrom: string[]; // … other fields }

修改后: typescript export interface WhatsAppConfig { dmPolicy: “allowlist” | “open”; allowFrom: string[]; allowSendTo?: string[]; // 新增:独立的出站白名单 // … other fields }

阶段 2:更新出站解析逻辑

文件src/whatsapp/resolve-outbound-target.ts

修改前: typescript export async function resolveOutboundTarget( normalizedTo: string, allowList: string[] ): Promise {

const hasWildcard = allowList.includes("*");

if (hasWildcard || allowList.length === 0) { return { ok: true, to: normalizedTo }; }

if (allowList.includes(normalizedTo)) { return { ok: true, to: normalizedTo }; }

return { ok: false, error: Delivering to WhatsApp requires target <E.164|group JID>, }; }

修改后: typescript export async function resolveOutboundTarget( normalizedTo: string, sendToList: string[] | undefined, inboundAllowFrom: string[] ): Promise {

// 如果 sendTo 明确配置,则使用它 if (sendToList !== undefined) { const hasWildcard = sendToList.includes("*");

if (hasWildcard || sendToList.length === 0) {
  return { ok: true, to: normalizedTo };
}

if (sendToList.includes(normalizedTo)) {
  return { ok: true, to: normalizedTo };
}

return {
  ok: false,
  error: `Target ${normalizedTo} is not in allowSendTo list`,
};

}

// 回退到遗留行为(使用入站 allowFrom 作为出站) const hasWildcard = inboundAllowFrom.includes("*");

if (hasWildcard || inboundAllowFrom.length === 0) { return { ok: true, to: normalizedTo }; }

if (inboundAllowFrom.includes(normalizedTo)) { return { ok: true, to: normalizedTo }; }

return { ok: false, error: Delivering to WhatsApp requires target <E.164|group JID>, }; }

阶段 3:更新调用方

文件src/whatsapp/sender.ts(或任何调用 resolveOutboundTarget 的地方)

修改前: typescript const target = await resolveOutboundTarget( normalizedTo, config.allowFrom // 传入入站列表用于出站检查 );

修改后: typescript const target = await resolveOutboundTarget( normalizedTo, config.allowSendTo, // 使用专用的出站列表 config.allowFrom // 传入用于向后兼容的回退 );

阶段 4:配置示例

推荐的生产环境配置:

json { “channels”: { “whatsapp”: { “dmPolicy”: “allowlist”, “allowFrom”: ["+1234567890", “+1111111111”], “allowSendTo”: ["*"] } } }

严格的出站配置:

json { “channels”: { “whatsapp”: { “dmPolicy”: “allowlist”, “allowFrom”: ["+1234567890"], “allowSendTo”: [ “+0987654321”, “+1122334455”, “[email protected]” ] } } }

🧪 验证

测试用例 1:发送到白名单中的 SendTo

bash

配置

“allowSendTo”: ["+0987654321"]

$ openclaw tools call message ‘{“to”: “+0987654321”, “body”: “Test”}’

预期输出: json { “ok”: true, “messageId”: “wamid.xxx…”, “timestamp”: “2024-01-15T10:30:00Z” }

测试用例 2:发送到不在列表中的 SendTo

bash

配置

“allowSendTo”: ["+0987654321"]

$ openclaw tools call message ‘{“to”: “+5555555555”, “body”: “Test”}’

预期输出: json { “ok”: false, “error”: “Target +5555555555 is not in allowSendTo list” }

测试用例 3:接收来自白名单发送者的入站消息

bash

配置

“allowFrom”: ["+0987654321"]

“allowSendTo”: ["*"]

发送消息 FROM +0987654321 TO 机器人

预期行为: 消息被处理并触发机器人响应。

测试用例 4:接收来自不在列表中的发送者的入站消息

bash

配置

“allowFrom”: ["+0987654321"]

发送消息 FROM +5555555555 TO 机器人

预期行为: 消息被拒绝,并显示入站访问控制错误。

测试用例 5:通配符 SendTo

bash

配置

“allowSendTo”: ["*"]

$ openclaw tools call message ‘{“to”: “+anyvalidnumber”, “body”: “Test”}’

预期输出: 消息发送成功。

验证脚本

typescript // test/whatsapp-outbound-permissions.test.ts

import { resolveOutboundTarget } from “../src/whatsapp/resolve-outbound-target”;

describe(“resolveOutboundTarget”, () => { test(“allows when target is in sendTo list”, async () => { const result = await resolveOutboundTarget( “+0987654321”, ["+0987654321", “+1122334455”], ["+1234567890"] ); expect(result.ok).toBe(true); });

test(“blocks when target is not in sendTo list”, async () => { const result = await resolveOutboundTarget( “+5555555555”, ["+0987654321"], ["+1234567890"] ); expect(result.ok).toBe(false); expect(result.error).toContain(“not in allowSendTo list”); });

test(“allows wildcard sendTo”, async () => { const result = await resolveOutboundTarget( “+5555555555”, ["*"], ["+1234567890"] ); expect(result.ok).toBe(true); });

test(“falls back to allowFrom when sendTo is undefined”, async () => { const result = await resolveOutboundTarget( “+1234567890”, undefined, // sendTo 未配置 ["+1234567890"] ); expect(result.ok).toBe(true); }); });

⚠️ 常见陷阱

陷阱 1:错误的 E.164 格式

WhatsApp 要求使用 E.164 格式的数字(例如 +1234567890)。使用没有前导 + 的格式会导致静默失败。

bash

错误

$ openclaw tools call message ‘{“to”: “1234567890”, “body”: “Test”}’

正确

$ openclaw tools call message ‘{“to”: “+1234567890”, “body”: “Test”}’

陷阱 2:群组 JID 与电话号码

群组 ID 使用的格式与电话号码不同。确保使用正确的 JID 语法:

json { “allowSendTo”: [ “+1234567890”, // 电话号码 “[email protected]” // 群组 JID ] }

陷阱 3:空数组与 undefined

空数组 allowSendTo: []allowSendTo 为 undefined 时的处理方式不同:

  • "allowSendTo": [] — 阻止所有出站消息
  • "allowSendTo": undefined — 回退到遗留的 allowFrom 行为

陷阱 4:Docker 环境变量映射

使用环境变量进行配置时:

bash

错误 - 这会创建一个字符串,而不是数组

WHATSAPP_ALLOW_SEND_TO=+1234567890,+0987654321

正确 - 使用 JSON 字符串表示数组

WHATSAPP_ALLOW_SEND_TO=["+1234567890","+0987654321"]

陷阱 5:缓存问题

更新配置后,确保运行中的进程重新加载:

bash

重启 OpenClaw 服务

$ systemctl restart openclaw

或对于 Docker

$ docker-compose down && docker-compose up -d

陷阱 6:从旧版配置迁移

没有 allowSendTo 的现有配置应通过回退机制继续工作。但需要进行彻底测试:

typescript // 验证回退机制仍然有效 const sendTo = config.allowSendTo ?? config.allowFrom;

陷阱 7:通配符安全影响

设置 "allowSendTo": ["*"] 允许向任何有效的 WhatsApp 号码发送消息。考虑以下因素:

  • 消息工具的速率限制
  • 额外的应用层授权
  • 记录所有出站消息尝试

🔗 相关错误

  • E_DELIVERY_FAILED — WhatsApp API 拒绝消息时的通用投递失败
  • E_INVALID_TARGET — 目标号码格式无效(不符合 E.164 规范)
  • E_INBOUND_NOT_AUTHORIZED — 因白名单策略拒绝入站消息
  • E_SESSION_NOT_READY — 出站尝试前 WhatsApp 会话未建立
  • E_ALLOWLIST_BLOCKED — 出站目标不在配置的白名单中

相关 GitHub 问题

  • Issue #XXX — WhatsApp dmPolicy 白名单阻止合法出站消息
  • Issue #YYY — 请求:为 WhatsApp 分离入站/出站访问控制
  • Issue #ZZZ — 文档:WhatsApp 渠道安全模型不清晰

配置参考

  • channels.whatsapp.dmPolicy — 控制入站访问模式("allowlist" | "open"
  • channels.whatsapp.allowFrom — 入站白名单(电话号码和群组 JID)
  • channels.whatsapp.allowSendTo — 出站白名单(电话号码和群组 JID)【新增】

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。