Skip to content

Commit 80518f2

Browse files
committed
Squashed commit of the following:
commit 5193ef7 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Tue Mar 31 09:47:02 2026 +0200 refactor: change default mode from dedicated to self-chat and reorder UI settings - Change default mode to self-chat across all modules - Update README to reflect self-chat as primary mode with security warning - Move session/media storage from usr/whatsapp to tmp/whatsapp - Reorder config UI: move Mode above Allowed Numbers - Add warning banner when allowed_numbers is empty in self-chat mode - Move Bridge Port and Poll Interval to bottom of settings - Update mode descriptions to clarify self-chat handles both self commit 9fece91 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Tue Mar 31 09:20:35 2026 +0200 refactor: centralize WhatsApp storage paths and improve bridge dependency handling - Add storage_paths.py helper for consistent session/media/runtime paths - Replace hardcoded usr/whatsapp paths across all modules - Fix bridge lock to be event-loop-aware (recreate per loop) - Add automatic dependency reinstall on startup failures - Track bridge startup output for better error diagnostics - Add dependency state tracking with package.json hash validation - Implement force reinstall when node_modules appears commit bc511d2 Author: linuztx <linuztx@gmail.com> Date: Tue Mar 31 09:07:46 2026 +0800 fix: stop poll loop immediately when Node.js is not installed commit a9554e1 Author: linuztx <linuztx@gmail.com> Date: Tue Mar 31 08:49:15 2026 +0800 fix: auto-reinstall corrupt node_modules and stop poll loop after repeated bridge failures _ensure_npm_install now verifies key package exists, not just node_modules dir. Wipes and reinstalls if corrupt. Poll loop stops after 5 consecutive bridge start failures instead of spamming errors and making A0 unusable. commit 61fa1bf Author: linuztx <linuztx@gmail.com> Date: Tue Mar 31 08:38:51 2026 +0800 fix: move allowed_numbers filtering from JS bridge to Python handler The JS bridge used LIDs (internal WhatsApp identifiers) for sender matching which never matched actual phone numbers. Moved filtering to Python handler.py where config is read fresh each poll cycle. - Add senderNumber (resolved phone) to bridge message payload - Filter in poll_messages() with normalized number comparison - Remove --allowed-numbers CLI arg and JS-side filtering - Fix ensure_bridge_http_up not recording _bridge_config - Fix falsy empty-dict check in bridge restart detection commit 64ee177 Author: linuztx <linuztx@gmail.com> Date: Sat Mar 28 23:34:23 2026 +0800 refactor: move email agent instructions to system prompt and update prompt labels commit 0f53b41 Author: linuztx <linuztx@gmail.com> Date: Sat Mar 28 10:59:44 2026 +0800 Add node_modules to gitignore commit eb6a4d3 Author: linuztx <linuztx@gmail.com> Date: Sat Mar 28 10:53:59 2026 +0800 Add WhatsApp plugin thumbnail commit 39bed4f Author: linuztx <linuztx@gmail.com> Date: Sat Mar 28 10:51:47 2026 +0800 refactor: rename allowed_users to allowed_numbers across plugin commit e4991b6 Author: linuztx <linuztx@gmail.com> Date: Fri Mar 27 21:58:29 2026 +0800 improve: move agent instructions from per-message to system prompt commit 4f1be15 Author: linuztx <linuztx@gmail.com> Date: Fri Mar 27 21:00:25 2026 +0800 improve: add macOS port kill support and bridge process destructor cleanup commit f534975 Author: linuztx <linuztx@gmail.com> Date: Fri Mar 27 17:09:56 2026 +0800 improve: remove redundant bridge_manager from execute, rely on poll loop finally commit 9d9dd4b Author: linuztx <linuztx@gmail.com> Date: Fri Mar 27 14:41:14 2026 +0800 fix: stop bridge and poll loop when plugin is disabled or toggled off commit 66b0a7d Author: linuztx <linuztx@gmail.com> Date: Fri Mar 27 11:05:58 2026 +0800 improve: fix allowed users input, auto-strip + prefix, log ignored messages commit 938e7b9 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 23:26:42 2026 +0800 improve: add line break to allowed users description commit 4ef64b9 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 22:44:55 2026 +0800 feat: convert markdown to WhatsApp formatting before sending replies commit f549b49 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 22:34:56 2026 +0800 improve: add progress update instructions to system context prompt commit 66e5d51 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 22:23:32 2026 +0800 fix: stop typing indicator on agent error or generation failure commit 3dd01cd Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 18:31:38 2026 +0800 improve: persistent typing indicator with poll-based refresh commit 8d0ec86 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 17:11:25 2026 +0800 Update README.md commit e664673 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 16:05:44 2026 +0800 feat: add agent prefix to self-chat replies for visual distinction commit 18c5716 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 15:43:01 2026 +0800 fix: clear typing indicator after sending reply in self-chat mode commit 7c653c9 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 14:43:06 2026 +0800 improve: merge WhatsApp Link and Disconnect into single Account field commit 57c95e6 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 14:11:05 2026 +0800 feat: add disconnect account option to switch WhatsApp accounts commit c626953 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 14:00:00 2026 +0800 improve: move mode description inline and reorder Allow Group after Allowed Users commit 18a56ea Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 13:44:17 2026 +0800 fix: remove duplicate typing indicator before sending reply commit 44c90a1 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 13:30:06 2026 +0800 improve: remove sender number from DM prompt commit 64fe7d0 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 13:17:29 2026 +0800 fix: handle documentWithCaptionMessage wrapper for captioned documents commit 00b6657 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 13:06:40 2026 +0800 feat: add attachment reader/writer with RFC and download all media types commit 8041c08 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 11:45:17 2026 +0800 improve: update group prompt and reply instructions commit 71a6eb7 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 11:26:36 2026 +0800 feat: reply to specific messages in group chats with quote commit 6bf63eb Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 09:57:34 2026 +0800 feat: detect replies to bot messages in group chats commit b4492e0 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 09:20:27 2026 +0800 improve: resolve group names and sender LIDs in bridge messages commit 14e673f Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 04:44:50 2026 +0800 feat: add allow_group toggle to respond only when mentioned in group chats commit 40f4884 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 03:20:02 2026 +0800 refactor: rename mode value from bot to dedicated commit 50af7c2 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 02:34:51 2026 +0800 fix: kill orphaned bridge process on port before starting new one commit 45b21c0 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 02:07:45 2026 +0800 improve: auto-restart bridge when config changes commit a12183b Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 01:39:55 2026 +0800 feat: add bot and self-chat mode selection for WhatsApp bridge commit bb8961a Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 00:56:56 2026 +0800 improve: send typing indicator immediately on message receive commit 84c12b0 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 00:29:04 2026 +0800 feat: add WhatsApp integration plugin with Baileys bridge and QR pairing
1 parent 3df7065 commit 80518f2

44 files changed

Lines changed: 5551 additions & 15 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
**/__pycache__/
55
*.py[cod]
66
**/.conda/
7+
**/node_modules/
78

89
#Ignore IDE files
910
.cursor/
Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
"""Inject email conversation context into system prompt for email sessions."""
22

33
from helpers.extension import Extension
4+
from helpers import plugins
45
from agent import LoopData
56
from plugins._email_integration.helpers.dispatcher import CTX_EMAIL_HANDLER
67

78

9+
PLUGIN_NAME = "_email_integration"
10+
11+
812
class EmailContextPrompt(Extension):
913

1014
async def execute(
@@ -16,7 +20,23 @@ async def execute(
1620
if not self.agent:
1721
return
1822

19-
if self.agent.context.data.get(CTX_EMAIL_HANDLER):
20-
system_prompt.append(
21-
self.agent.read_prompt("fw.email.system_context_reply.md")
22-
)
23+
handler_name = self.agent.context.data.get(CTX_EMAIL_HANDLER)
24+
if not handler_name:
25+
return
26+
27+
system_prompt.append(
28+
self.agent.read_prompt("fw.email.system_context_reply.md")
29+
)
30+
31+
config = plugins.get_plugin_config(PLUGIN_NAME) or {}
32+
for h in config.get("handlers", []):
33+
if h.get("name") == handler_name:
34+
instructions = h.get("agent_instructions", "")
35+
if instructions:
36+
system_prompt.append(
37+
self.agent.read_prompt(
38+
"fw.email.user_message_instructions.md",
39+
instructions=instructions,
40+
)
41+
)
42+
break

plugins/_email_integration/helpers/handler.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -394,19 +394,13 @@ def _is_own_email(sender: str, own_address: str) -> bool:
394394

395395
def _build_user_message(agent: Agent, msg: InboundMessage, handler_cfg: dict) -> str:
396396
recipient = handler_cfg.get("username", "")
397-
text = agent.read_prompt(
397+
return agent.read_prompt(
398398
"fw.email.user_message.md",
399399
sender=msg.sender,
400400
recipient=recipient,
401401
subject=msg.subject,
402402
body=msg.body,
403403
)
404-
instructions = handler_cfg.get("agent_instructions", "")
405-
if instructions:
406-
text += agent.read_prompt(
407-
"fw.email.user_message_instructions.md", instructions=instructions,
408-
)
409-
return text
410404

411405

412406
# ------------------------------------------------------------------
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
2-
[Handler instructions: {{instructions}}]
1+
# Email custom rules
2+
{{instructions}}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
2-
[Handler instructions: {{instructions}}]
1+
# Telegram custom rules
2+
{{instructions}}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# WhatsApp Integration Plugin
2+
3+
Communicate with Agent Zero via WhatsApp using a Baileys-based Node.js bridge.
4+
5+
## Requirements
6+
7+
- **Node.js** (v18+) and npm installed on the system
8+
- A WhatsApp account on a phone (for QR code pairing)
9+
10+
## Setup
11+
12+
### Install bridge dependencies
13+
14+
```bash
15+
cd plugins/_whatsapp_integration/whatsapp-bridge
16+
npm install --production
17+
```
18+
19+
Dependencies are auto-installed on first bridge start if missing.
20+
21+
### Configure and pair
22+
23+
1. Enable the plugin in Settings > External > WhatsApp Integration
24+
2. Configure allowed phone numbers
25+
3. Click Show QR Code and scan with WhatsApp on your phone
26+
4. Send a message from an allowed number to start a chat
27+
28+
The WhatsApp session persists across restarts in `tmp/whatsapp/session/`. No re-pairing needed unless you disconnect via settings.
29+
Be careful: if you use your personal number and leave `allowed_numbers` open, other people could misuse your Agent Zero.
30+
31+
## Configuration
32+
33+
| Setting | Description | Default |
34+
|---------|-------------|---------|
35+
| `enabled` | Enable bridge and polling | `false` |
36+
| `mode` | `self-chat` (personal number) or `dedicated` (separate number) | `self-chat` |
37+
| `allow_group` | Respond in group chats when mentioned or replied to | `false` |
38+
| `bridge_port` | Local HTTP port for bridge | `3100` |
39+
| `poll_interval_seconds` | Poll frequency (min 2) | `3` |
40+
| `allowed_numbers` | Phone numbers without + prefix | `[]` (all) |
41+
| `project` | Activate project for WA chats | `""` |
42+
| `agent_instructions` | Extra agent instructions | `""` |
43+
44+
## How It Works
45+
46+
1. The bridge connects to WhatsApp via Baileys and exposes HTTP endpoints on localhost
47+
2. In personal-number mode, you can message your own WhatsApp number to talk to the agent, and the agent can also handle messages that other people send to that number
48+
3. The plugin polls the bridge for new messages every few seconds
49+
4. Incoming messages are routed to existing chats by WhatsApp chat ID or new chats are created
50+
5. Agent responses are sent back via the bridge as WhatsApp messages
51+
6. Media (images, documents) is supported in both directions
52+
53+
## Architecture
54+
55+
```
56+
WhatsApp Phone
57+
↕ (WhatsApp protocol via Baileys)
58+
whatsapp-bridge/bridge.js (Node.js subprocess)
59+
↕ (HTTP API on localhost)
60+
Python helpers (wa_client, handler, bridge_manager)
61+
↕ (Framework extensions)
62+
Agent Zero
63+
```
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Disconnect WhatsApp account by stopping bridge and clearing session."""
2+
3+
import os
4+
import shutil
5+
6+
from helpers.api import ApiHandler, Request
7+
from helpers.errors import format_error
8+
9+
10+
class Disconnect(ApiHandler):
11+
12+
async def process(self, input: dict, request: Request) -> dict:
13+
try:
14+
from plugins._whatsapp_integration.helpers import bridge_manager
15+
from plugins._whatsapp_integration.helpers.storage_paths import get_bridge_session_dir
16+
17+
# Stop bridge first
18+
await bridge_manager.stop_bridge()
19+
20+
# Delete session files
21+
session_dir = get_bridge_session_dir()
22+
if os.path.exists(session_dir):
23+
shutil.rmtree(session_dir, ignore_errors=True)
24+
25+
return {"success": True, "message": "Account disconnected"}
26+
except Exception as e:
27+
return {"success": False, "message": format_error(e)}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""WhatsApp QR code pairing endpoint."""
2+
3+
from helpers.api import ApiHandler, Request
4+
from helpers.errors import format_error
5+
from helpers import plugins
6+
7+
8+
PLUGIN_NAME = "_whatsapp_integration"
9+
10+
11+
class QrCode(ApiHandler):
12+
13+
async def process(self, input: dict, request: Request) -> dict:
14+
config = plugins.get_plugin_config(PLUGIN_NAME) or {}
15+
port = int(config.get("bridge_port", 3100))
16+
mode = config.get("mode", "self-chat")
17+
18+
from plugins._whatsapp_integration.helpers.bridge_manager import (
19+
ensure_bridge_http_up,
20+
get_bridge_url,
21+
is_process_alive,
22+
)
23+
from plugins._whatsapp_integration.helpers.storage_paths import (
24+
get_bridge_media_dir,
25+
get_bridge_session_dir,
26+
)
27+
from plugins._whatsapp_integration.helpers.wa_client import get_qr
28+
29+
session_dir = get_bridge_session_dir()
30+
cache_dir = get_bridge_media_dir()
31+
base_url = get_bridge_url(port)
32+
33+
# Start bridge if not running
34+
if not is_process_alive():
35+
try:
36+
ok = await ensure_bridge_http_up(
37+
port, session_dir, cache_dir, mode=mode,
38+
)
39+
if not ok:
40+
return {
41+
"status": "error",
42+
"message": "Failed to start bridge",
43+
"qr": None,
44+
}
45+
except Exception as e:
46+
return {
47+
"status": "error",
48+
"message": format_error(e),
49+
"qr": None,
50+
}
51+
52+
# Fetch QR status from bridge
53+
try:
54+
data = await get_qr(base_url)
55+
status = data.get("status", "error")
56+
qr = data.get("qr")
57+
58+
if status == "connected":
59+
return {
60+
"status": "connected",
61+
"message": "WhatsApp is already connected",
62+
"qr": None,
63+
}
64+
65+
if status == "waiting_scan" and qr:
66+
return {
67+
"status": "waiting_scan",
68+
"message": "Scan the QR code with WhatsApp",
69+
"qr": qr,
70+
}
71+
72+
return {
73+
"status": "waiting_qr",
74+
"message": "Generating QR code...",
75+
"qr": None,
76+
}
77+
78+
except Exception as e:
79+
return {
80+
"status": "error",
81+
"message": f"Bridge not reachable: {format_error(e)}",
82+
"qr": None,
83+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Start WhatsApp bridge immediately."""
2+
3+
from helpers.api import ApiHandler, Request
4+
from helpers.errors import format_error
5+
from helpers import plugins
6+
7+
8+
PLUGIN_NAME = "_whatsapp_integration"
9+
10+
11+
class Start(ApiHandler):
12+
13+
async def process(self, input: dict, request: Request) -> dict:
14+
config = plugins.get_plugin_config(PLUGIN_NAME) or {}
15+
port = int(config.get("bridge_port", 3100))
16+
mode = config.get("mode", "self-chat")
17+
18+
from plugins._whatsapp_integration.helpers.bridge_manager import (
19+
ensure_bridge_http_up,
20+
is_process_alive,
21+
)
22+
from plugins._whatsapp_integration.helpers.storage_paths import (
23+
get_bridge_media_dir,
24+
get_bridge_session_dir,
25+
)
26+
27+
session_dir = get_bridge_session_dir()
28+
cache_dir = get_bridge_media_dir()
29+
30+
if is_process_alive():
31+
return {"success": True, "message": "Bridge already running"}
32+
33+
try:
34+
ok = await ensure_bridge_http_up(
35+
port, session_dir, cache_dir, mode=mode,
36+
)
37+
if ok:
38+
return {"success": True, "message": "Bridge started"}
39+
return {"success": False, "message": "Failed to start bridge"}
40+
except Exception as e:
41+
return {"success": False, "message": format_error(e)}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Test WhatsApp bridge connection health."""
2+
3+
from helpers.api import ApiHandler, Request
4+
from helpers.errors import format_error
5+
from helpers import plugins
6+
7+
8+
PLUGIN_NAME = "_whatsapp_integration"
9+
10+
11+
class TestConnection(ApiHandler):
12+
13+
async def process(self, input: dict, request: Request) -> dict:
14+
config = input.get("config") or plugins.get_plugin_config(PLUGIN_NAME) or {}
15+
port = int(config.get("bridge_port", 3100))
16+
results: list[dict] = []
17+
18+
await self._test_bridge(port, results)
19+
20+
ok = all(r["ok"] for r in results)
21+
return {"success": ok, "results": results}
22+
23+
async def _test_bridge(self, port: int, results: list[dict]) -> None:
24+
from plugins._whatsapp_integration.helpers.wa_client import get_health
25+
from plugins._whatsapp_integration.helpers.bridge_manager import get_bridge_url
26+
27+
try:
28+
health = await get_health(get_bridge_url(port))
29+
status = health.get("status", "unknown")
30+
queue = health.get("queueLength", 0)
31+
uptime = health.get("uptime", 0)
32+
33+
if status == "connected":
34+
results.append({
35+
"test": "Bridge",
36+
"ok": True,
37+
"message": f"Connected (uptime: {uptime:.0f}s, queue: {queue})",
38+
})
39+
else:
40+
results.append({
41+
"test": "Bridge",
42+
"ok": False,
43+
"message": f"Bridge running but status: {status}",
44+
})
45+
except Exception as e:
46+
results.append({
47+
"test": "Bridge",
48+
"ok": False,
49+
"message": f"Bridge not reachable: {format_error(e)}",
50+
})

0 commit comments

Comments
 (0)