fix(whatsapp): sync sidecar on start and forward all group inbounds

Copy bridge files when starting WhatsApp sidecar, enforce group policy only in gateway with richer @ detection, and log skipped group messages for debugging.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-06-04 19:52:25 +08:00
parent 9657b313a3
commit dcb779a691
5 changed files with 73 additions and 48 deletions

View file

@ -709,6 +709,16 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
triggers=list(group_policy.triggers),
metadata=inbound.metadata if isinstance(inbound.metadata, dict) else {},
):
import logging
logging.getLogger(__name__).info(
"whatsapp group inbound skipped chat=%s user=%s mentions=%s require_mention=%s text=%r",
inbound.external_chat_id,
inbound.external_user_id,
list(inbound.mentions or []),
group_policy.require_mention,
text[:120],
)
return {"ok": True, "replies": []}
reply = ""
reply_attachments: list[dict[str, Any]] = []

View file

@ -35,6 +35,16 @@ if (-not (Test-Path (Join-Path $sidecarRoot "baileys_runner.ts"))) {
throw "whatsapp sidecar not installed. Run whatsapp_install.ps1 first."
}
$bridgeSrc = Join-Path $oclawRoot "runtime\\operations\\whatsapp_bridge"
if (Test-Path $bridgeSrc) {
foreach ($name in @("baileys_runner.ts", "auth.ts", "qr.ts")) {
$srcPath = Join-Path $bridgeSrc $name
if (Test-Path $srcPath) {
Copy-Item -Path $srcPath -Destination (Join-Path $sidecarRoot $name) -Force
}
}
}
$healthUrl = ($GatewayBaseUrl.TrimEnd("/") + "/health")
for ($i = 0; $i -lt $GatewayWaitSeconds; $i++) {
try {

View file

@ -30,12 +30,6 @@ const PROXY_URL = (
process.env.http_proxy ||
""
).trim();
const GROUP_REQUIRE_MENTION = String(process.env.AIA_WHATSAPP_GROUP_REQUIRE_MENTION ?? "1").trim() !== "0";
const GROUP_TRIGGERS = String(process.env.AIA_WHATSAPP_GROUP_TRIGGERS || "/oclaw,|oclaw")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
function log(msg: string): void {
process.stdout.write(`${new Date().toISOString()} [baileys-whatsapp] ${msg}\n`);
}
@ -74,7 +68,10 @@ function unwrapMessage(m: proto.IMessage | null | undefined): proto.IMessage | n
function messageContextInfo(m: proto.IMessage | null | undefined): proto.IContextInfo | null | undefined {
const u = unwrapMessage(m);
if (!u) return null;
const top = (u as proto.IMessage & { messageContextInfo?: proto.IMessageContextInfo }).messageContextInfo;
const fromTop = top?.mentionedJid?.length ? top : null;
return (
(fromTop as unknown as proto.IContextInfo) ||
u.extendedTextMessage?.contextInfo ||
u.imageMessage?.contextInfo ||
u.videoMessage?.contextInfo ||
@ -86,6 +83,23 @@ function messageContextInfo(m: proto.IMessage | null | undefined): proto.IContex
);
}
function extractMentionsFromUpsert(msg: proto.IWebMessageInfo): string[] {
const fromBody = extractMentions(msg.message);
const outer = (msg as proto.IWebMessageInfo & { messageContextInfo?: { mentionedJid?: string[] } })
.messageContextInfo?.mentionedJid;
const outerList = Array.isArray(outer) ? outer.map((j) => String(j || "").trim()).filter(Boolean) : [];
const merged = [...fromBody, ...outerList];
const seen = new Set<string>();
const out: string[] = [];
for (const j of merged) {
const key = jidPhone(j) || j;
if (seen.has(key)) continue;
seen.add(key);
out.push(j);
}
return out;
}
function jidPhone(jid: string): string {
const head = String(jid || "").split("@")[0]?.split(":")[0] || "";
return head.replace(/\D/g, "");
@ -115,32 +129,6 @@ function extractQuoteContext(m: proto.IMessage | null | undefined): { participan
};
}
function shouldProcessGroupMessage(params: {
isGroup: boolean;
text: string;
mentions: string[];
botJid: string;
quotedParticipant?: string;
}): boolean {
if (!params.isGroup) return true;
const bot = String(params.botJid || "").trim();
if (bot && params.quotedParticipant && jidsSameUser(bot, params.quotedParticipant)) {
return true;
}
if (bot) {
for (const m of params.mentions || []) {
const raw = String(m || "").trim();
if (!raw) continue;
if (jidsSameUser(raw, bot)) return true;
}
}
const body = String(params.text || "");
for (const t of GROUP_TRIGGERS) {
if (t && body.includes(t)) return true;
}
return !GROUP_REQUIRE_MENTION;
}
function isStatusOrBroadcastJid(jid: string): boolean {
const low = String(jid || "").toLowerCase();
return low === "status@broadcast" || low.endsWith("@broadcast");
@ -508,25 +496,12 @@ async function main(): Promise<void> {
const from = isGroup ? String(key.participant || "").trim() : remoteJid;
const userId = from ? jidNormalizedUser(from) : jidNormalizedUser(remoteJid);
const chatId = jidNormalizedUser(remoteJid);
const mentions = extractMentions(msg.message);
const mentions = extractMentionsFromUpsert(msg);
const quote = extractQuoteContext(msg.message);
const botJidRaw = sock?.user?.id ? String(sock.user.id).trim() : "";
const botJid = botJidRaw ? jidNormalizedUser(botJidRaw) : "";
const isReplyToBot = Boolean(botJidRaw && quote.participant && jidsSameUser(botJidRaw, quote.participant));
if (
!shouldProcessGroupMessage({
isGroup,
text,
mentions,
botJid: botJidRaw,
quotedParticipant: quote.participant,
})
) {
if (VERBOSE) log(`skip group message chat=${chatId} user=${userId} (no mention/trigger)`);
continue;
}
const groupName = isGroup ? await resolveGroupName(chatId) : "";
const raw = {
@ -548,9 +523,15 @@ async function main(): Promise<void> {
isGroup,
mentions,
groupName: groupName || undefined,
botJid: botJid || undefined,
botJid: botJidRaw || botJid || undefined,
});
if (VERBOSE) log(`inbound posting chat=${chatId} user=${userId} textLen=${text.length}`);
if (VERBOSE || isGroup) {
log(
`inbound group=${isGroup} chat=${chatId} user=${userId} mentions=${mentions.length} replyToBot=${isReplyToBot} textLen=${text.length}`,
);
} else if (VERBOSE) {
log(`inbound posting chat=${chatId} user=${userId} textLen=${text.length}`);
}
const out = await postInbound(inbound);
const replies = Array.isArray(out.replies) ? (out.replies as Json[]) : [];
if (VERBOSE) log(`inbound ok replies=${replies.length}`);

View file

@ -51,6 +51,21 @@ def _metadata_raw(metadata: dict[str, Any] | None) -> dict[str, Any]:
return raw if isinstance(raw, dict) else {}
def text_mentions_bot(*, text: str, bot_jid: str | None) -> bool:
"""Fallback when WhatsApp omits mentionedJid but user visibly @-mentions the bot."""
bot = str(bot_jid or "").strip()
if not bot:
return False
phone = jid_phone(bot)
if len(phone) < 6:
return False
body = str(text or "")
if "@" not in body:
return False
digits_in_text = re.sub(r"\D", "", body)
return phone in digits_in_text
def is_reply_to_bot(*, metadata: dict[str, Any] | None, bot_jid: str | None) -> bool:
raw = _metadata_raw(metadata)
if raw.get("isReplyToBot") is True or raw.get("is_reply_to_bot") is True:
@ -185,6 +200,8 @@ def should_process_group_inbound(
for m in list(mentions or []):
if jids_same_user(str(m or ""), bot):
return True
if text_mentions_bot(text=text, bot_jid=bot_jid):
return True
trigger_list = [str(t) for t in (triggers or []) if str(t)]
body = str(text or "")
if trigger_list and any(t in body for t in trigger_list):
@ -244,4 +261,5 @@ __all__ = [
"session_user_key",
"should_process_group_inbound",
"should_send_channel_reply_text",
"text_mentions_bot",
]