feat: add bot and self-chat mode selection for WhatsApp bridge

This commit is contained in:
linuztx 2026-03-26 01:39:55 +08:00
parent bb8961ab73
commit a12183ba6e
7 changed files with 57 additions and 6 deletions

View file

@ -22,6 +22,7 @@ The WhatsApp session persists across restarts in `usr/whatsapp/sessions/`. No re
| Setting | Description | Default |
|---------|-------------|---------|
| `enabled` | Enable bridge and polling | `false` |
| `mode` | `bot` (separate number) or `self-chat` (personal number) | `bot` |
| `bridge_port` | Local HTTP port for bridge | `3100` |
| `poll_interval_seconds` | Poll frequency (min 2) | `3` |
| `allowed_users` | Phone numbers without + prefix | `[]` (all) |

View file

@ -16,6 +16,7 @@ class QrCode(ApiHandler):
session_dir = files.get_abs_path("usr/whatsapp/sessions")
cache_dir = files.get_abs_path("usr/whatsapp/media")
allowed_users = config.get("allowed_users") or []
mode = config.get("mode", "bot")
from plugins._whatsapp_integration.helpers.bridge_manager import (
ensure_bridge_http_up,
@ -30,7 +31,7 @@ class QrCode(ApiHandler):
if not is_process_alive():
try:
ok = await ensure_bridge_http_up(
port, session_dir, cache_dir, allowed_users,
port, session_dir, cache_dir, allowed_users, mode=mode,
)
if not ok:
return {

View file

@ -1,4 +1,7 @@
enabled: false
mode: bot
# bot: separate phone number — people message the bot directly
# self-chat: personal number — you message yourself to talk to the agent
bridge_port: 3100
poll_interval_seconds: 3
allowed_users: []

View file

@ -55,12 +55,13 @@ async def _poll_loop() -> None:
session_dir = files.get_abs_path("usr/whatsapp/sessions")
cache_dir = files.get_abs_path("usr/whatsapp/media")
allowed_users = config.get("allowed_users") or []
mode = config.get("mode", "bot")
# Start bridge if needed
if not bridge_started or not bridge_manager.is_process_alive():
try:
bridge_started = await bridge_manager.start_bridge(
port, session_dir, cache_dir, allowed_users,
port, session_dir, cache_dir, allowed_users, mode=mode,
)
except Exception as e:
PrintStyle.error(f"WhatsApp bridge start error: {format_error(e)}")

View file

@ -29,6 +29,7 @@ async def start_bridge(
session_dir: str,
cache_dir: str,
allowed_users: list[str] | None = None,
mode: str = "bot",
) -> bool:
global _bridge_process
async with _bridge_lock:
@ -43,6 +44,7 @@ async def start_bridge(
"--port", str(port),
"--session", session_dir,
"--cache-dir", cache_dir,
"--mode", mode,
]
if allowed_users:
cmd += ["--allowed-users", ",".join(allowed_users)]
@ -101,6 +103,7 @@ async def ensure_bridge_http_up(
session_dir: str,
cache_dir: str,
allowed_users: list[str] | None = None,
mode: str = "bot",
) -> bool:
"""Start bridge if needed and wait for HTTP server only (not WA connection)."""
global _bridge_process
@ -116,6 +119,7 @@ async def ensure_bridge_http_up(
"--port", str(port),
"--session", session_dir,
"--cache-dir", cache_dir,
"--mode", mode,
]
if allowed_users:
cmd += ["--allowed-users", ",".join(allowed_users)]

View file

@ -102,6 +102,32 @@
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Mode</div>
<div class="field-description">How the agent connects to WhatsApp</div>
</div>
<div class="field-control">
<select x-model="config.mode">
<option value="bot">Separate bot number</option>
<option value="self-chat">Personal number (self-chat)</option>
</select>
</div>
</div>
<template x-if="config.mode === 'bot'">
<div style="padding: 8px 12px; border-radius: 6px; font-size: 0.85rem; opacity: 0.7;
border: 1px solid var(--border-color, #333); margin-bottom: 8px;">
People message the bot's number directly. Requires a second phone number with WhatsApp.
</div>
</template>
<template x-if="config.mode === 'self-chat'">
<div style="padding: 8px 12px; border-radius: 6px; font-size: 0.85rem; opacity: 0.7;
border: 1px solid var(--border-color, #333); margin-bottom: 8px;">
You message yourself to talk to the agent. Quick to set up but less intuitive UX.
</div>
</template>
<!-- WhatsApp Link section (shown when enabled) -->
<template x-if="config.enabled">
<div style="margin: 12px 0;">

View file

@ -45,6 +45,7 @@ const PORT = parseInt(getArg('port', '3100'), 10);
const SESSION_DIR = getArg('session', path.join(process.env.HOME || '~', '.agent-zero', 'whatsapp', 'session'));
const CACHE_DIR = getArg('cache-dir', path.join(process.env.HOME || '~', '.agent-zero', 'whatsapp', 'media'));
const PAIR_ONLY = args.includes('--pair-only');
const MODE = getArg('mode', 'bot'); // "bot" or "self-chat"
const ALLOWED_USERS = (getArg('allowed-users', '') || '').split(',').map(s => s.trim()).filter(Boolean);
mkdirSync(SESSION_DIR, { recursive: true });
@ -158,14 +159,28 @@ async function startSocket() {
const isGroup = chatId.endsWith('@g.us');
const senderNumber = senderId.replace(/@.*/, '');
// Bot mode: all fromMe messages are echo-backs — skip
if (msg.key.fromMe) continue;
// Handle fromMe messages based on mode
if (msg.key.fromMe) {
if (isGroup || chatId.includes('status')) continue;
if (MODE === 'bot') {
// Bot mode: separate number — all fromMe are echo-backs, skip
continue;
}
// Self-chat mode: only accept messages in the user's own self-chat
const myNumber = (sock.user?.id || '').replace(/:.*@/, '@').replace(/@.*/, '');
const myLid = (sock.user?.lid || '').replace(/:.*@/, '@').replace(/@.*/, '');
const chatNumber = chatId.replace(/@.*/, '');
const isSelfChat = (myNumber && chatNumber === myNumber) || (myLid && chatNumber === myLid);
if (!isSelfChat) continue;
}
// Skip status broadcasts
if (chatId === 'status@broadcast') continue;
// Check allowlist (resolve LID -> phone if needed)
if (ALLOWED_USERS.length > 0) {
if (!msg.key.fromMe && ALLOWED_USERS.length > 0) {
const resolvedNumber = lidToPhone[senderNumber] || senderNumber;
if (!ALLOWED_USERS.includes(resolvedNumber)) continue;
}
@ -458,7 +473,7 @@ if (PAIR_ONLY) {
startSocket();
} else {
app.listen(PORT, '127.0.0.1', () => {
console.log(`[bridge] WhatsApp bridge listening on port ${PORT}`);
console.log(`[bridge] WhatsApp bridge listening on port ${PORT} (mode: ${MODE})`);
console.log(`[bridge] Session: ${SESSION_DIR}`);
if (ALLOWED_USERS.length > 0) {
console.log(`[bridge] Allowed users: ${ALLOWED_USERS.join(', ')}`);