mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:13:19 +08:00
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:
parent
9657b313a3
commit
dcb779a691
5 changed files with 73 additions and 48 deletions
|
|
@ -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]] = []
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from runtime.orchestration.group_ingest import (
|
|||
resolve_group_policy,
|
||||
session_user_key,
|
||||
should_process_group_inbound,
|
||||
text_mentions_bot,
|
||||
)
|
||||
from interfaces.channels.base import InboundMessage
|
||||
from runtime.application.gateway.inbound_service import _parse_generic_inbound, process_inbound_payload
|
||||
|
|
@ -95,6 +96,11 @@ def test_should_accept_group_reply_to_bot() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_text_mentions_bot_phone_fallback() -> None:
|
||||
assert text_mentions_bot(text="@1234567890 hello", bot_jid="1234567890@s.whatsapp.net") is True
|
||||
assert text_mentions_bot(text="hello", bot_jid="1234567890@s.whatsapp.net") is False
|
||||
|
||||
|
||||
def test_should_accept_group_trigger_without_mention() -> None:
|
||||
assert (
|
||||
should_process_group_inbound(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue