@lofa199419/waha-v2 2026.3.6 → 2026.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/bin/wa +20 -0
- package/bin/wa-adv +9 -0
- package/bin/waha-advanced-entrypoint +12 -0
- package/bin/waha-cli +11 -0
- package/bin/waha_cli.py +308 -0
- package/index.ts +27 -10
- package/package.json +19 -1
- package/scripts/install-openclaw-extension.mjs +106 -0
- package/skills/waha-v2/SKILL.md +81 -168
- package/src/accounts.ts +9 -0
- package/src/channel.ts +33 -29
- package/src/client.ts +52 -0
- package/src/config-schema.ts +127 -0
- package/src/gateway.ts +4 -0
- package/src/types.ts +43 -0
- package/src/webhook.ts +486 -22
- package/SESSION_HANDOFF_2026-03-05.md +0 -55
- package/SESSION_HANDOFF_2026-03-05_E2E_STATUS.md +0 -84
- package/openclaw-waha-v2-2026.3.2.tgz +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# `@lofa199419/waha-v2`
|
|
2
|
+
|
|
3
|
+
OpenClaw WAHA v2 channel plugin with a packaged local CLI wrapper for WAHA session and messaging operations.
|
|
4
|
+
|
|
5
|
+
## What ships
|
|
6
|
+
|
|
7
|
+
- OpenClaw plugin files: `index.ts`, `src/`, `openclaw.plugin.json`
|
|
8
|
+
- Skill docs: `skills/waha-v2/SKILL.md`
|
|
9
|
+
- Local CLI wrappers:
|
|
10
|
+
- `bin/waha-cli`
|
|
11
|
+
- `bin/wa`
|
|
12
|
+
- `bin/wa-adv`
|
|
13
|
+
- `bin/waha-advanced-entrypoint`
|
|
14
|
+
- `bin/waha_cli.py`
|
|
15
|
+
|
|
16
|
+
## Install into an OpenClaw state dir
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @lofa199419/waha-v2
|
|
20
|
+
node node_modules/@lofa199419/waha-v2/scripts/install-openclaw-extension.mjs --state-dir /data/.openclaw --force
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or with `OPENCLAW_STATE_DIR`:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
OPENCLAW_STATE_DIR=/data/.openclaw node node_modules/@lofa199419/waha-v2/scripts/install-openclaw-extension.mjs --force
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Runtime configuration
|
|
30
|
+
|
|
31
|
+
The plugin package does not ship secrets. Provide WAHA configuration through your container or startup flow and render:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
/data/.openclaw/extensions/waha-v2/bin/.env
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Expected values:
|
|
38
|
+
|
|
39
|
+
```dotenv
|
|
40
|
+
WAHA_URL=https://your-waha-host
|
|
41
|
+
WAHA_API_KEY=your-api-key
|
|
42
|
+
WAHA_SESSION_DEFAULT=power
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Publish checklist
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm pack --dry-run
|
|
49
|
+
npm publish --access public
|
|
50
|
+
```
|
package/bin/wa
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
4
|
+
if [[ -f "$DIR/.env" ]]; then
|
|
5
|
+
set -a
|
|
6
|
+
source "$DIR/.env"
|
|
7
|
+
set +a
|
|
8
|
+
fi
|
|
9
|
+
ARGS=("$@")
|
|
10
|
+
HAS_SESSION=0
|
|
11
|
+
for ((i=0; i<${#ARGS[@]}; i++)); do
|
|
12
|
+
if [[ "${ARGS[$i]}" == "--session" ]]; then
|
|
13
|
+
HAS_SESSION=1
|
|
14
|
+
break
|
|
15
|
+
fi
|
|
16
|
+
done
|
|
17
|
+
if [[ $HAS_SESSION -eq 0 && -n "${WAHA_SESSION_DEFAULT:-}" ]]; then
|
|
18
|
+
ARGS+=(--session "$WAHA_SESSION_DEFAULT")
|
|
19
|
+
fi
|
|
20
|
+
exec "$DIR/waha-cli" "${ARGS[@]}"
|
package/bin/wa-adv
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
ENTRYPOINT="$DIR/waha_cli.py"
|
|
6
|
+
|
|
7
|
+
if [[ ! -f "$ENTRYPOINT" ]]; then
|
|
8
|
+
echo "waha-v2: missing WAHA CLI shim at $ENTRYPOINT" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
exec python3 "$ENTRYPOINT" "$@"
|
package/bin/waha-cli
ADDED
package/bin/waha_cli.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import urllib.request
|
|
9
|
+
|
|
10
|
+
VERSION = "3.0.0"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def env(name: str, default: str | None = None) -> str | None:
|
|
14
|
+
return os.environ.get(name, default)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def require_env(name: str) -> str:
|
|
18
|
+
value = env(name)
|
|
19
|
+
if not value:
|
|
20
|
+
print(f"Missing required env: {name}", file=sys.stderr)
|
|
21
|
+
sys.exit(2)
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def default_session() -> str:
|
|
26
|
+
return (
|
|
27
|
+
env("WAHA_SESSION_DEFAULT")
|
|
28
|
+
or env("WAHA_DEFAULT_SESSION")
|
|
29
|
+
or "default"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize_base_url(value: str) -> str:
|
|
34
|
+
return value.rstrip("/")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def waha_request(method: str, path: str, body: dict | None = None, query: dict | None = None):
|
|
38
|
+
base_url = normalize_base_url(require_env("WAHA_URL"))
|
|
39
|
+
api_key = require_env("WAHA_API_KEY")
|
|
40
|
+
url = f"{base_url}{path}"
|
|
41
|
+
if query:
|
|
42
|
+
filtered = {k: v for k, v in query.items() if v is not None}
|
|
43
|
+
if filtered:
|
|
44
|
+
url = f"{url}?{urllib.parse.urlencode(filtered)}"
|
|
45
|
+
data = None
|
|
46
|
+
headers = {
|
|
47
|
+
"X-Api-Key": api_key,
|
|
48
|
+
"User-Agent": f"waha-cli/{VERSION}",
|
|
49
|
+
"Accept": "application/json, text/plain;q=0.9, */*;q=0.8",
|
|
50
|
+
}
|
|
51
|
+
if body is not None:
|
|
52
|
+
data = json.dumps(body).encode("utf-8")
|
|
53
|
+
headers["Content-Type"] = "application/json"
|
|
54
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
55
|
+
try:
|
|
56
|
+
with urllib.request.urlopen(req) as resp:
|
|
57
|
+
payload = resp.read()
|
|
58
|
+
content_type = resp.headers.get("content-type", "")
|
|
59
|
+
if "application/json" in content_type:
|
|
60
|
+
return json.loads(payload.decode("utf-8"))
|
|
61
|
+
return payload.decode("utf-8")
|
|
62
|
+
except urllib.error.HTTPError as exc:
|
|
63
|
+
try:
|
|
64
|
+
error_body = exc.read().decode("utf-8")
|
|
65
|
+
parsed = json.loads(error_body)
|
|
66
|
+
message = parsed.get("message", error_body)
|
|
67
|
+
except Exception:
|
|
68
|
+
message = exc.reason
|
|
69
|
+
print(f"WAHA API error ({exc.code}): {message}", file=sys.stderr)
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
except urllib.error.URLError as exc:
|
|
72
|
+
print(f"WAHA API request failed: {exc.reason}", file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def print_json(data):
|
|
77
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def print_help():
|
|
81
|
+
print(
|
|
82
|
+
f"""waha-cli v{VERSION}
|
|
83
|
+
|
|
84
|
+
Usage:
|
|
85
|
+
waha-cli --help
|
|
86
|
+
waha-cli --version
|
|
87
|
+
waha-cli <command> [options]
|
|
88
|
+
|
|
89
|
+
Session and Auth:
|
|
90
|
+
waha-list-sessions
|
|
91
|
+
waha-get-session --session NAME
|
|
92
|
+
waha-create-session --name NAME
|
|
93
|
+
waha-start-session --session NAME
|
|
94
|
+
waha-stop-session --session NAME
|
|
95
|
+
waha-restart-session --session NAME
|
|
96
|
+
waha-delete-session --session NAME
|
|
97
|
+
waha-logout-session --session NAME
|
|
98
|
+
waha-get-qr-code --session NAME
|
|
99
|
+
waha-request-pairing-code --session NAME --phone-number NUMBER
|
|
100
|
+
waha-check-auth-status --session NAME
|
|
101
|
+
|
|
102
|
+
Chats and Messages:
|
|
103
|
+
waha-get-chats [--session NAME] [--limit N] [--offset N]
|
|
104
|
+
waha-get-messages --chat-id ID [--session NAME] [--limit N] [--offset N]
|
|
105
|
+
waha-send-text --chat-id ID --text TEXT [--session NAME]
|
|
106
|
+
|
|
107
|
+
Optional env:
|
|
108
|
+
WAHA_SESSION_DEFAULT / WAHA_DEFAULT_SESSION
|
|
109
|
+
Required env:
|
|
110
|
+
WAHA_URL
|
|
111
|
+
WAHA_API_KEY"""
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def add_session_arg(parser):
|
|
116
|
+
parser.add_argument("--session", default=default_session())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_parser():
|
|
120
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
121
|
+
parser.add_argument("--help", action="store_true")
|
|
122
|
+
parser.add_argument("--version", action="store_true")
|
|
123
|
+
parser.add_argument("command", nargs="?")
|
|
124
|
+
parser.add_argument("rest", nargs=argparse.REMAINDER)
|
|
125
|
+
return parser
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def cmd_list_sessions(_args):
|
|
129
|
+
print_json(waha_request("GET", "/api/sessions"))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def cmd_get_session(args):
|
|
133
|
+
print_json(waha_request("GET", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}"))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def cmd_create_session(args):
|
|
137
|
+
body = {"name": args.name}
|
|
138
|
+
print_json(waha_request("POST", "/api/sessions", body))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cmd_simple_post(args, suffix):
|
|
142
|
+
print_json(
|
|
143
|
+
waha_request(
|
|
144
|
+
"POST",
|
|
145
|
+
f"/api/sessions/{urllib.parse.quote(args.session, safe='')}/{suffix}",
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_delete_session(args):
|
|
151
|
+
waha_request("DELETE", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}")
|
|
152
|
+
print(f'Session "{args.session}" deleted successfully.')
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def cmd_logout_session(args):
|
|
156
|
+
waha_request("POST", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}/logout")
|
|
157
|
+
print(f'Session "{args.session}" logged out successfully.')
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def cmd_get_qr_code(args):
|
|
161
|
+
qr = waha_request(
|
|
162
|
+
"GET",
|
|
163
|
+
f"/api/{urllib.parse.quote(args.session, safe='')}/auth/qr",
|
|
164
|
+
query={"format": "raw"},
|
|
165
|
+
)
|
|
166
|
+
if isinstance(qr, dict) and "value" in qr:
|
|
167
|
+
qr = qr["value"]
|
|
168
|
+
print(f'QR Code value for session "{args.session}":')
|
|
169
|
+
print(qr)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def cmd_request_pairing_code(args):
|
|
173
|
+
result = waha_request(
|
|
174
|
+
"POST",
|
|
175
|
+
f"/api/{urllib.parse.quote(args.session, safe='')}/auth/request-code",
|
|
176
|
+
{"phoneNumber": args.phone_number},
|
|
177
|
+
)
|
|
178
|
+
code = result.get("code", "<missing>")
|
|
179
|
+
print(
|
|
180
|
+
f'Pairing code requested for {args.phone_number} on session "{args.session}".\n'
|
|
181
|
+
f"Code: {code}\n"
|
|
182
|
+
"Enter this code in WhatsApp on your phone."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def cmd_check_auth_status(args):
|
|
187
|
+
info = waha_request("GET", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}")
|
|
188
|
+
messages = {
|
|
189
|
+
"STOPPED": "Session is stopped. Start it first.",
|
|
190
|
+
"STARTING": "Session is starting up...",
|
|
191
|
+
"SCAN_QR_CODE": "Waiting for QR code scan. Use waha-get-qr-code or waha-request-pairing-code.",
|
|
192
|
+
"WORKING": "Session is authenticated and working.",
|
|
193
|
+
"FAILED": "Session has failed. Try restarting or re-authenticating.",
|
|
194
|
+
}
|
|
195
|
+
text = [
|
|
196
|
+
f"Session: {args.session}",
|
|
197
|
+
f"Status: {info.get('status', 'UNKNOWN')}",
|
|
198
|
+
messages.get(info.get("status"), f"Unknown status: {info.get('status')}"),
|
|
199
|
+
]
|
|
200
|
+
me = info.get("me")
|
|
201
|
+
if isinstance(me, dict):
|
|
202
|
+
if me.get("id"):
|
|
203
|
+
text.append(f"Account ID: {me['id']}")
|
|
204
|
+
if me.get("pushName"):
|
|
205
|
+
text.append(f"Name: {me['pushName']}")
|
|
206
|
+
print("\n".join(text))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def cmd_get_chats(args):
|
|
210
|
+
print_json(
|
|
211
|
+
waha_request(
|
|
212
|
+
"GET",
|
|
213
|
+
f"/api/{urllib.parse.quote(args.session, safe='')}/chats",
|
|
214
|
+
query={"limit": args.limit, "offset": args.offset},
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def cmd_get_messages(args):
|
|
220
|
+
query = {"limit": args.limit, "offset": args.offset}
|
|
221
|
+
print_json(
|
|
222
|
+
waha_request(
|
|
223
|
+
"GET",
|
|
224
|
+
f"/api/{urllib.parse.quote(args.session, safe='')}/chats/{urllib.parse.quote(args.chat_id, safe='')}/messages",
|
|
225
|
+
query=query,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def cmd_send_text(args):
|
|
231
|
+
result = waha_request(
|
|
232
|
+
"POST",
|
|
233
|
+
"/api/sendText",
|
|
234
|
+
{"session": args.session, "chatId": args.chat_id, "text": args.text},
|
|
235
|
+
)
|
|
236
|
+
print(f"Message sent successfully.\nMessage ID: {result.get('id', '<missing>')}")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
COMMANDS = {
|
|
240
|
+
"waha-list-sessions": ("List sessions", cmd_list_sessions),
|
|
241
|
+
"waha-get-session": ("Get session", cmd_get_session),
|
|
242
|
+
"waha-create-session": ("Create session", cmd_create_session),
|
|
243
|
+
"waha-start-session": ("Start session", lambda args: cmd_simple_post(args, "start")),
|
|
244
|
+
"waha-stop-session": ("Stop session", lambda args: cmd_simple_post(args, "stop")),
|
|
245
|
+
"waha-restart-session": ("Restart session", lambda args: cmd_simple_post(args, "restart")),
|
|
246
|
+
"waha-delete-session": ("Delete session", cmd_delete_session),
|
|
247
|
+
"waha-logout-session": ("Logout session", cmd_logout_session),
|
|
248
|
+
"waha-get-qr-code": ("Get QR code", cmd_get_qr_code),
|
|
249
|
+
"waha-request-pairing-code": ("Request pairing code", cmd_request_pairing_code),
|
|
250
|
+
"waha-check-auth-status": ("Check auth status", cmd_check_auth_status),
|
|
251
|
+
"waha-get-chats": ("Get chats", cmd_get_chats),
|
|
252
|
+
"waha-list-chats": ("List chats", cmd_get_chats),
|
|
253
|
+
"waha-get-messages": ("Get messages", cmd_get_messages),
|
|
254
|
+
"waha-send-text": ("Send text", cmd_send_text),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def command_parser(command: str):
|
|
259
|
+
parser = argparse.ArgumentParser(prog=f"waha-cli {command}")
|
|
260
|
+
if command in {"waha-get-session", "waha-start-session", "waha-stop-session", "waha-restart-session", "waha-delete-session", "waha-logout-session", "waha-get-qr-code", "waha-check-auth-status"}:
|
|
261
|
+
add_session_arg(parser)
|
|
262
|
+
elif command == "waha-create-session":
|
|
263
|
+
parser.add_argument("--name", required=True)
|
|
264
|
+
elif command == "waha-request-pairing-code":
|
|
265
|
+
add_session_arg(parser)
|
|
266
|
+
parser.add_argument("--phone-number", required=True)
|
|
267
|
+
elif command in {"waha-get-chats", "waha-list-chats"}:
|
|
268
|
+
add_session_arg(parser)
|
|
269
|
+
parser.add_argument("--limit", type=int, default=50)
|
|
270
|
+
parser.add_argument("--offset", type=int, default=0)
|
|
271
|
+
elif command == "waha-get-messages":
|
|
272
|
+
add_session_arg(parser)
|
|
273
|
+
parser.add_argument("--chat-id", required=True)
|
|
274
|
+
parser.add_argument("--limit", type=int, default=20)
|
|
275
|
+
parser.add_argument("--offset", type=int, default=0)
|
|
276
|
+
elif command == "waha-send-text":
|
|
277
|
+
add_session_arg(parser)
|
|
278
|
+
parser.add_argument("--chat-id", required=True)
|
|
279
|
+
parser.add_argument("--text", required=True)
|
|
280
|
+
return parser
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main():
|
|
284
|
+
parser = build_parser()
|
|
285
|
+
args = parser.parse_args()
|
|
286
|
+
|
|
287
|
+
if args.help or not args.command:
|
|
288
|
+
print_help()
|
|
289
|
+
return 0
|
|
290
|
+
if args.version:
|
|
291
|
+
print(VERSION)
|
|
292
|
+
return 0
|
|
293
|
+
|
|
294
|
+
command = args.command.replace("_", "-")
|
|
295
|
+
handler_entry = COMMANDS.get(command)
|
|
296
|
+
if handler_entry is None:
|
|
297
|
+
print(f"Unknown command: {args.command}", file=sys.stderr)
|
|
298
|
+
print_help()
|
|
299
|
+
return 2
|
|
300
|
+
|
|
301
|
+
cmd_parser = command_parser(command)
|
|
302
|
+
cmd_args = cmd_parser.parse_args(args.rest)
|
|
303
|
+
handler_entry[1](cmd_args)
|
|
304
|
+
return 0
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
sys.exit(main())
|
package/index.ts
CHANGED
|
@@ -33,22 +33,31 @@ const plugin = {
|
|
|
33
33
|
api.registerChannel({ plugin: wahaV2Plugin });
|
|
34
34
|
|
|
35
35
|
// Inbound webhook — matches both:
|
|
36
|
-
// /webhooks/waha-v2
|
|
36
|
+
// /webhooks/waha-v2 (legacy, routes by session name in payload)
|
|
37
37
|
// /webhooks/waha-v2/{accountId} (preferred, unambiguous — accountId from path)
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
// Newer OpenClaw plugin SDK requires explicit route auth and no longer supports
|
|
39
|
+
// registerHttpHandler or dynamic path params in plugin routes.
|
|
40
|
+
api.registerHttpRoute({
|
|
41
|
+
path: WAHA_V2_WEBHOOK_BASE,
|
|
42
|
+
auth: "plugin",
|
|
43
|
+
match: "prefix",
|
|
44
|
+
handler: async (req, res) => {
|
|
45
|
+
const url = req.url ?? "";
|
|
46
|
+
if (!url.startsWith(WAHA_V2_WEBHOOK_BASE)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const suffix = url.slice(WAHA_V2_WEBHOOK_BASE.length).split("?")[0] ?? "";
|
|
50
|
+
const accountId = suffix.startsWith("/") ? suffix.slice(1) || undefined : undefined;
|
|
51
|
+
await handleWahaV2WebhookRequest(req, res, api.runtime.config.loadConfig(), accountId);
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
47
54
|
});
|
|
48
55
|
|
|
49
56
|
// Session management API routes — used by UI, CLI, and setup flows.
|
|
50
57
|
api.registerHttpRoute({
|
|
51
58
|
path: WAHA_V2_ROUTE_STATUS,
|
|
59
|
+
auth: "gateway",
|
|
60
|
+
match: "exact",
|
|
52
61
|
handler: async (req, res) => {
|
|
53
62
|
await handleWahaV2StatusRoute(req, res, api.runtime.config.loadConfig());
|
|
54
63
|
},
|
|
@@ -56,6 +65,8 @@ const plugin = {
|
|
|
56
65
|
|
|
57
66
|
api.registerHttpRoute({
|
|
58
67
|
path: WAHA_V2_ROUTE_START,
|
|
68
|
+
auth: "gateway",
|
|
69
|
+
match: "exact",
|
|
59
70
|
handler: async (req, res) => {
|
|
60
71
|
await handleWahaV2StartRoute(req, res, api.runtime.config.loadConfig());
|
|
61
72
|
},
|
|
@@ -63,6 +74,8 @@ const plugin = {
|
|
|
63
74
|
|
|
64
75
|
api.registerHttpRoute({
|
|
65
76
|
path: WAHA_V2_ROUTE_QR,
|
|
77
|
+
auth: "gateway",
|
|
78
|
+
match: "exact",
|
|
66
79
|
handler: async (req, res) => {
|
|
67
80
|
await handleWahaV2QrRoute(req, res, api.runtime.config.loadConfig());
|
|
68
81
|
},
|
|
@@ -70,6 +83,8 @@ const plugin = {
|
|
|
70
83
|
|
|
71
84
|
api.registerHttpRoute({
|
|
72
85
|
path: WAHA_V2_ROUTE_REQUEST_CODE,
|
|
86
|
+
auth: "gateway",
|
|
87
|
+
match: "exact",
|
|
73
88
|
handler: async (req, res) => {
|
|
74
89
|
await handleWahaV2RequestCodeRoute(req, res, api.runtime.config.loadConfig());
|
|
75
90
|
},
|
|
@@ -77,6 +92,8 @@ const plugin = {
|
|
|
77
92
|
|
|
78
93
|
api.registerHttpRoute({
|
|
79
94
|
path: WAHA_V2_ROUTE_WAIT,
|
|
95
|
+
auth: "gateway",
|
|
96
|
+
match: "exact",
|
|
80
97
|
handler: async (req, res) => {
|
|
81
98
|
await handleWahaV2WaitRoute(req, res, api.runtime.config.loadConfig());
|
|
82
99
|
},
|
package/package.json
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lofa199419/waha-v2",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "OpenClaw WAHA v2 channel plugin — independent WhatsApp HTTP API integration",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts",
|
|
9
|
+
"src",
|
|
10
|
+
"openclaw.plugin.json",
|
|
11
|
+
"skills",
|
|
12
|
+
"bin/waha-cli",
|
|
13
|
+
"bin/wa",
|
|
14
|
+
"bin/wa-adv",
|
|
15
|
+
"bin/waha-advanced-entrypoint",
|
|
16
|
+
"bin/waha_cli.py",
|
|
17
|
+
"scripts/install-openclaw-extension.mjs",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"pack:check": "npm pack --dry-run",
|
|
22
|
+
"install:openclaw": "node scripts/install-openclaw-extension.mjs"
|
|
23
|
+
},
|
|
7
24
|
"dependencies": {
|
|
8
25
|
"axios": "1.6.0",
|
|
9
26
|
"waha-node": "1.0.0",
|
|
@@ -35,6 +52,7 @@
|
|
|
35
52
|
"install": {
|
|
36
53
|
"npmSpec": "@lofa199419/waha-v2",
|
|
37
54
|
"localPath": "extensions/waha-v2",
|
|
55
|
+
"script": "node scripts/install-openclaw-extension.mjs",
|
|
38
56
|
"defaultChoice": "npm"
|
|
39
57
|
}
|
|
40
58
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const packageRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
6
|
+
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const out = {
|
|
9
|
+
stateDir: process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || "", ".openclaw"),
|
|
10
|
+
force: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
14
|
+
const arg = argv[i];
|
|
15
|
+
if (arg === "--state-dir") {
|
|
16
|
+
out.stateDir = argv[i + 1];
|
|
17
|
+
i += 1;
|
|
18
|
+
} else if (arg === "--force") {
|
|
19
|
+
out.force = true;
|
|
20
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
21
|
+
out.help = true;
|
|
22
|
+
} else {
|
|
23
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function exists(target) {
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(target);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function ensureDir(target) {
|
|
40
|
+
await fs.mkdir(target, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function copyFile(relPath, destinationRoot) {
|
|
44
|
+
const source = path.join(packageRoot, relPath);
|
|
45
|
+
const destination = path.join(destinationRoot, relPath);
|
|
46
|
+
await ensureDir(path.dirname(destination));
|
|
47
|
+
await fs.copyFile(source, destination);
|
|
48
|
+
const stat = await fs.stat(source);
|
|
49
|
+
await fs.chmod(destination, stat.mode);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function copyDirectory(relPath, destinationRoot) {
|
|
53
|
+
const source = path.join(packageRoot, relPath);
|
|
54
|
+
const destination = path.join(destinationRoot, relPath);
|
|
55
|
+
await ensureDir(path.dirname(destination));
|
|
56
|
+
await fs.cp(source, destination, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
const args = parseArgs(process.argv.slice(2));
|
|
61
|
+
if (args.help) {
|
|
62
|
+
console.log("Usage: node scripts/install-openclaw-extension.mjs [--state-dir PATH] [--force]");
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!args.stateDir) {
|
|
67
|
+
throw new Error("Missing OpenClaw state dir. Pass --state-dir or set OPENCLAW_STATE_DIR.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const extensionRoot = path.join(args.stateDir, "extensions", "waha-v2");
|
|
71
|
+
if (await exists(extensionRoot)) {
|
|
72
|
+
if (!args.force) {
|
|
73
|
+
throw new Error(`Target already exists: ${extensionRoot}. Re-run with --force to replace it.`);
|
|
74
|
+
}
|
|
75
|
+
await fs.rm(extensionRoot, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await ensureDir(extensionRoot);
|
|
79
|
+
|
|
80
|
+
const directories = ["src", "skills", "scripts"];
|
|
81
|
+
const files = [
|
|
82
|
+
"index.ts",
|
|
83
|
+
"openclaw.plugin.json",
|
|
84
|
+
"package.json",
|
|
85
|
+
"README.md",
|
|
86
|
+
"bin/waha-cli",
|
|
87
|
+
"bin/wa",
|
|
88
|
+
"bin/wa-adv",
|
|
89
|
+
"bin/waha-advanced-entrypoint",
|
|
90
|
+
"bin/waha_cli.py",
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
for (const relPath of directories) {
|
|
94
|
+
await copyDirectory(relPath, extensionRoot);
|
|
95
|
+
}
|
|
96
|
+
for (const relPath of files) {
|
|
97
|
+
await copyFile(relPath, extensionRoot);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`Installed waha-v2 into ${extensionRoot}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main().catch((error) => {
|
|
104
|
+
console.error(`[waha-v2 install] ${error.message}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|