@chrysb/alphaclaw 0.9.6 → 0.9.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 +3 -2
- package/lib/public/assets/icons/whatsapp.svg +14 -0
- package/lib/public/css/tailwind.generated.css +1 -1
- package/lib/public/dist/app.bundle.js +2031 -1925
- package/lib/public/js/components/agents-tab/create-channel-modal.js +30 -13
- package/lib/public/js/components/channel-login-modal.js +82 -0
- package/lib/public/js/components/channels.js +347 -1
- package/lib/public/js/components/general/index.js +56 -8
- package/lib/public/js/components/modal-shell.js +18 -2
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +11 -6
- package/lib/public/js/components/pairings.js +1 -1
- package/lib/public/js/components/welcome/index.js +0 -1
- package/lib/public/js/components/welcome/use-welcome.js +1 -1
- package/lib/public/js/lib/api.js +23 -0
- package/lib/public/js/lib/channel-provider-availability.js +1 -1
- package/lib/server/agents/channels.js +268 -4
- package/lib/server/agents/service.js +2 -0
- package/lib/server/agents/shared.js +133 -42
- package/lib/server/alphaclaw-version.js +7 -3
- package/lib/server/commands.js +5 -1
- package/lib/server/constants.js +7 -0
- package/lib/server/gateway.js +61 -18
- package/lib/server/onboarding/import/secret-detector.js +9 -0
- package/lib/server/onboarding/openclaw.js +39 -0
- package/lib/server/onboarding/validation.js +1 -1
- package/lib/server/routes/agents.js +39 -0
- package/lib/server/routes/pairings.js +2 -2
- package/lib/server/watchdog-notify.js +54 -13
- package/lib/server.js +1 -0
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ const kChannelEnvKeys = {
|
|
|
14
14
|
telegram: "TELEGRAM_BOT_TOKEN",
|
|
15
15
|
discord: "DISCORD_BOT_TOKEN",
|
|
16
16
|
slack: "SLACK_BOT_TOKEN",
|
|
17
|
+
whatsapp: "WHATSAPP_OWNER_NUMBER",
|
|
17
18
|
};
|
|
18
19
|
const kChannelExtraEnvKeys = {
|
|
19
20
|
slack: ["SLACK_APP_TOKEN"],
|
|
@@ -22,6 +23,7 @@ const kChannelTokenFields = {
|
|
|
22
23
|
telegram: "botToken",
|
|
23
24
|
discord: "token",
|
|
24
25
|
slack: "botToken",
|
|
26
|
+
// WhatsApp uses owner number, not a bot token field
|
|
25
27
|
};
|
|
26
28
|
const kChannelExtraTokenFields = {
|
|
27
29
|
slack: ["appToken"],
|
|
@@ -30,6 +32,13 @@ const kChannelLabels = {
|
|
|
30
32
|
telegram: "Telegram",
|
|
31
33
|
discord: "Discord",
|
|
32
34
|
slack: "Slack",
|
|
35
|
+
whatsapp: "WhatsApp",
|
|
36
|
+
};
|
|
37
|
+
const kChannelProviderAliases = {
|
|
38
|
+
wa: "whatsapp",
|
|
39
|
+
"whats-app": "whatsapp",
|
|
40
|
+
whats_app: "whatsapp",
|
|
41
|
+
"whats app": "whatsapp",
|
|
33
42
|
};
|
|
34
43
|
const kMaskedChannelToken = "********";
|
|
35
44
|
|
|
@@ -39,6 +48,44 @@ const shellEscapeArg = (value) =>
|
|
|
39
48
|
const resolveCredentialsDirPath = ({ OPENCLAW_DIR }) =>
|
|
40
49
|
path.join(OPENCLAW_DIR, "credentials");
|
|
41
50
|
|
|
51
|
+
const resolveWhatsAppCredentialCandidatePaths = ({
|
|
52
|
+
OPENCLAW_DIR,
|
|
53
|
+
accountId,
|
|
54
|
+
}) => {
|
|
55
|
+
const credentialsDir = resolveCredentialsDirPath({ OPENCLAW_DIR });
|
|
56
|
+
const normalizedAccountId = normalizeChannelAccountId(accountId);
|
|
57
|
+
return [
|
|
58
|
+
path.join(credentialsDir, "whatsapp", normalizedAccountId, "creds.json"),
|
|
59
|
+
...(normalizedAccountId === "default"
|
|
60
|
+
? [path.join(credentialsDir, "creds.json")]
|
|
61
|
+
: []),
|
|
62
|
+
];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const hasSavedWhatsAppCredentials = ({
|
|
66
|
+
fsImpl,
|
|
67
|
+
OPENCLAW_DIR,
|
|
68
|
+
accountId,
|
|
69
|
+
}) => {
|
|
70
|
+
const candidatePaths = resolveWhatsAppCredentialCandidatePaths({
|
|
71
|
+
OPENCLAW_DIR,
|
|
72
|
+
accountId,
|
|
73
|
+
});
|
|
74
|
+
const matches = candidatePaths.map((targetPath) => {
|
|
75
|
+
try {
|
|
76
|
+
const exists = !!String(fsImpl.readFileSync(targetPath, "utf8") || "").trim();
|
|
77
|
+
return { path: targetPath, exists };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return {
|
|
80
|
+
path: targetPath,
|
|
81
|
+
exists: false,
|
|
82
|
+
error: String(error?.message || error || "read failed"),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return matches.some((entry) => entry.exists);
|
|
87
|
+
};
|
|
88
|
+
|
|
42
89
|
const resolveAgentWorkspacePath = ({ OPENCLAW_DIR, agentId }) =>
|
|
43
90
|
path.join(
|
|
44
91
|
OPENCLAW_DIR,
|
|
@@ -149,7 +196,7 @@ const normalizeChannelProvider = (value) => {
|
|
|
149
196
|
.trim()
|
|
150
197
|
.toLowerCase();
|
|
151
198
|
if (!provider || !kChannelEnvKeys[provider]) {
|
|
152
|
-
throw new Error(
|
|
199
|
+
throw new Error(`Unsupported channel provider "${provider}"`);
|
|
153
200
|
}
|
|
154
201
|
return provider;
|
|
155
202
|
};
|
|
@@ -397,6 +444,26 @@ const resolveCredentialPairingAccountId = ({ channelId, fileName }) => {
|
|
|
397
444
|
return normalizeChannelAccountId(rawAccountId);
|
|
398
445
|
};
|
|
399
446
|
|
|
447
|
+
const hasImplicitWhatsAppSelfPairing = ({
|
|
448
|
+
fsImpl,
|
|
449
|
+
OPENCLAW_DIR,
|
|
450
|
+
channelId,
|
|
451
|
+
accountId,
|
|
452
|
+
accountConfig,
|
|
453
|
+
}) => {
|
|
454
|
+
if (String(channelId || "").trim() !== "whatsapp") return false;
|
|
455
|
+
if (!accountConfig || typeof accountConfig !== "object") return false;
|
|
456
|
+
if (accountConfig.selfChatMode === false) return false;
|
|
457
|
+
if (String(accountConfig.dmPolicy || "").trim().toLowerCase() === "disabled") {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
return hasSavedWhatsAppCredentials({
|
|
461
|
+
fsImpl,
|
|
462
|
+
OPENCLAW_DIR,
|
|
463
|
+
accountId,
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
|
|
400
467
|
const readPairedCountsByAccount = ({
|
|
401
468
|
fsImpl,
|
|
402
469
|
OPENCLAW_DIR,
|
|
@@ -412,30 +479,33 @@ const readPairedCountsByAccount = ({
|
|
|
412
479
|
);
|
|
413
480
|
const credentialsDir = resolveCredentialsDirPath({ OPENCLAW_DIR });
|
|
414
481
|
try {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
482
|
+
if (String(channelId || "").trim() !== "whatsapp") {
|
|
483
|
+
const files = fsImpl
|
|
484
|
+
.readdirSync(credentialsDir)
|
|
485
|
+
.filter(
|
|
486
|
+
(fileName) =>
|
|
487
|
+
String(fileName || "").startsWith(
|
|
488
|
+
`${String(channelId || "").trim()}-`,
|
|
489
|
+
) && String(fileName || "").endsWith("-allowFrom.json"),
|
|
490
|
+
);
|
|
491
|
+
for (const fileName of files) {
|
|
492
|
+
const accountId = resolveCredentialPairingAccountId({
|
|
493
|
+
channelId,
|
|
494
|
+
fileName,
|
|
495
|
+
});
|
|
496
|
+
if (!accountId || !counts.has(accountId)) continue;
|
|
497
|
+
const filePath = path.join(credentialsDir, fileName);
|
|
498
|
+
const parsed = JSON.parse(fsImpl.readFileSync(filePath, "utf8"));
|
|
499
|
+
const pairedCount = Array.isArray(parsed?.allowFrom)
|
|
500
|
+
? parsed.allowFrom.length
|
|
501
|
+
: 0;
|
|
502
|
+
counts.set(accountId, Number(counts.get(accountId) || 0) + pairedCount);
|
|
503
|
+
}
|
|
435
504
|
}
|
|
436
505
|
} catch {}
|
|
437
506
|
|
|
438
507
|
for (const accountId of counts.keys()) {
|
|
508
|
+
if (String(channelId || "").trim() === "whatsapp") continue;
|
|
439
509
|
const accountConfig =
|
|
440
510
|
accountId === "default" &&
|
|
441
511
|
!(config.accounts && typeof config.accounts === "object")
|
|
@@ -449,6 +519,26 @@ const readPairedCountsByAccount = ({
|
|
|
449
519
|
);
|
|
450
520
|
}
|
|
451
521
|
|
|
522
|
+
for (const accountId of counts.keys()) {
|
|
523
|
+
if (Number(counts.get(accountId) || 0) > 0) continue;
|
|
524
|
+
const accountConfig =
|
|
525
|
+
accountId === "default" &&
|
|
526
|
+
!(config.accounts && typeof config.accounts === "object")
|
|
527
|
+
? config
|
|
528
|
+
: config.accounts?.[accountId] || {};
|
|
529
|
+
if (
|
|
530
|
+
hasImplicitWhatsAppSelfPairing({
|
|
531
|
+
fsImpl,
|
|
532
|
+
OPENCLAW_DIR,
|
|
533
|
+
channelId,
|
|
534
|
+
accountId,
|
|
535
|
+
accountConfig,
|
|
536
|
+
})
|
|
537
|
+
) {
|
|
538
|
+
counts.set(accountId, 1);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
452
542
|
return counts;
|
|
453
543
|
};
|
|
454
544
|
|
|
@@ -509,27 +599,26 @@ const listConfiguredChannelAccounts = ({ fsImpl, OPENCLAW_DIR, cfg }) => {
|
|
|
509
599
|
});
|
|
510
600
|
return {
|
|
511
601
|
channel: String(channelId || "").trim(),
|
|
512
|
-
accounts: normalizedAccountIds
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}),
|
|
602
|
+
accounts: normalizedAccountIds.map((accountId) => {
|
|
603
|
+
const accountConfig =
|
|
604
|
+
accountId === "default" && accountIds.length === 0
|
|
605
|
+
? config
|
|
606
|
+
: accountsConfig?.[accountId] || {};
|
|
607
|
+
return {
|
|
608
|
+
id: accountId,
|
|
609
|
+
name: String(accountConfig?.name || "").trim(),
|
|
610
|
+
envKey: deriveChannelEnvKey({ provider: channelId, accountId }),
|
|
611
|
+
boundAgentId:
|
|
612
|
+
boundAccountMap.get(
|
|
613
|
+
`${String(channelId || "").trim()}:${accountId}`,
|
|
614
|
+
) || "",
|
|
615
|
+
paired: Number(pairedCounts.get(accountId) || 0),
|
|
616
|
+
status:
|
|
617
|
+
Number(pairedCounts.get(accountId) || 0) > 0
|
|
618
|
+
? "paired"
|
|
619
|
+
: "configured",
|
|
620
|
+
};
|
|
621
|
+
}),
|
|
533
622
|
};
|
|
534
623
|
})
|
|
535
624
|
.filter(Boolean);
|
|
@@ -673,6 +762,8 @@ module.exports = {
|
|
|
673
762
|
kMaskedChannelToken,
|
|
674
763
|
shellEscapeArg,
|
|
675
764
|
resolveCredentialsDirPath,
|
|
765
|
+
resolveWhatsAppCredentialCandidatePaths,
|
|
766
|
+
hasSavedWhatsAppCredentials,
|
|
676
767
|
resolveAgentWorkspacePath,
|
|
677
768
|
loadConfig,
|
|
678
769
|
saveConfig,
|
|
@@ -96,7 +96,7 @@ const extractTemplateVersions = (pkg) => ({
|
|
|
96
96
|
latestOpenclawVersion: normalizeOpenclawVersion(pkg?.dependencies?.openclaw),
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
const fetchLatestVersionFromRegistry = async ({ fetchImpl }) => {
|
|
99
|
+
const fetchLatestVersionFromRegistry = async ({ fetchImpl, version = null }) => {
|
|
100
100
|
if (typeof fetchImpl !== "function") {
|
|
101
101
|
throw new Error("Fetch is not available for AlphaClaw version checks");
|
|
102
102
|
}
|
|
@@ -109,7 +109,8 @@ const fetchLatestVersionFromRegistry = async ({ fetchImpl }) => {
|
|
|
109
109
|
response,
|
|
110
110
|
"Failed to fetch latest AlphaClaw version",
|
|
111
111
|
);
|
|
112
|
-
const latestVersion =
|
|
112
|
+
const latestVersion =
|
|
113
|
+
normalizeVersion(version) || normalizeVersion(data?.["dist-tags"]?.latest);
|
|
113
114
|
const latestOpenclawVersion = latestVersion
|
|
114
115
|
? normalizeOpenclawVersion(
|
|
115
116
|
data?.versions?.[latestVersion]?.dependencies?.openclaw,
|
|
@@ -143,7 +144,10 @@ const fetchTemplatePackageVersions = async ({
|
|
|
143
144
|
const versions = extractTemplateVersions(data);
|
|
144
145
|
if (!versions.latestOpenclawVersion && versions.latestVersion) {
|
|
145
146
|
try {
|
|
146
|
-
const registry = await fetchLatestVersionFromRegistry({
|
|
147
|
+
const registry = await fetchLatestVersionFromRegistry({
|
|
148
|
+
fetchImpl,
|
|
149
|
+
version: versions.latestVersion,
|
|
150
|
+
});
|
|
147
151
|
versions.latestOpenclawVersion = registry.latestOpenclawVersion || null;
|
|
148
152
|
} catch {}
|
|
149
153
|
}
|
package/lib/server/commands.js
CHANGED
|
@@ -32,7 +32,10 @@ const createCommands = ({ gatewayEnv }) => {
|
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
const clawCmd = (
|
|
35
|
+
const clawCmd = (
|
|
36
|
+
cmd,
|
|
37
|
+
{ quiet = false, timeoutMs = 15000, killSignal = "SIGTERM" } = {},
|
|
38
|
+
) =>
|
|
36
39
|
new Promise((resolve) => {
|
|
37
40
|
if (!quiet) console.log(`[alphaclaw] Running: openclaw ${cmd}`);
|
|
38
41
|
exec(
|
|
@@ -40,6 +43,7 @@ const createCommands = ({ gatewayEnv }) => {
|
|
|
40
43
|
{
|
|
41
44
|
env: gatewayEnv(),
|
|
42
45
|
timeout: timeoutMs,
|
|
46
|
+
killSignal,
|
|
43
47
|
},
|
|
44
48
|
(err, stdout, stderr) => {
|
|
45
49
|
const result = {
|
package/lib/server/constants.js
CHANGED
|
@@ -256,6 +256,12 @@ const kKnownVars = [
|
|
|
256
256
|
group: "channels",
|
|
257
257
|
hint: "From Basic Information → App-Level Tokens (xapp-...)",
|
|
258
258
|
},
|
|
259
|
+
{
|
|
260
|
+
key: "WHATSAPP_OWNER_NUMBER",
|
|
261
|
+
label: "WhatsApp Owner Number",
|
|
262
|
+
group: "channels",
|
|
263
|
+
hint: "E.164 number, e.g. +15551234567",
|
|
264
|
+
},
|
|
259
265
|
{
|
|
260
266
|
key: "MISTRAL_API_KEY",
|
|
261
267
|
label: "Mistral API Key",
|
|
@@ -358,6 +364,7 @@ const kChannelDefs = {
|
|
|
358
364
|
telegram: { envKey: "TELEGRAM_BOT_TOKEN" },
|
|
359
365
|
discord: { envKey: "DISCORD_BOT_TOKEN" },
|
|
360
366
|
slack: { envKey: "SLACK_BOT_TOKEN", extraEnvKeys: ["SLACK_APP_TOKEN"] },
|
|
367
|
+
whatsapp: { envKey: "WHATSAPP_OWNER_NUMBER", sync: false },
|
|
361
368
|
};
|
|
362
369
|
const kProtectedBrowsePaths = new Set(
|
|
363
370
|
Array.isArray(kBrowseFilePolicies?.protectedPaths)
|
package/lib/server/gateway.js
CHANGED
|
@@ -371,6 +371,32 @@ const getChannelStatus = () => {
|
|
|
371
371
|
);
|
|
372
372
|
const credDir = `${OPENCLAW_DIR}/credentials`;
|
|
373
373
|
const channels = {};
|
|
374
|
+
const hasImplicitWhatsAppSelfPairing = ({ accountId, accountConfig }) => {
|
|
375
|
+
if (!accountConfig || typeof accountConfig !== "object") return false;
|
|
376
|
+
if (accountConfig.selfChatMode === false) return false;
|
|
377
|
+
if (String(accountConfig.dmPolicy || "").trim().toLowerCase() === "disabled") {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
const candidatePaths = [
|
|
381
|
+
`${credDir}/whatsapp/${accountId}/creds.json`,
|
|
382
|
+
...(accountId === "default" ? [`${credDir}/creds.json`] : []),
|
|
383
|
+
];
|
|
384
|
+
const matches = candidatePaths.map((targetPath) => {
|
|
385
|
+
try {
|
|
386
|
+
return {
|
|
387
|
+
path: targetPath,
|
|
388
|
+
exists: !!String(fs.readFileSync(targetPath, "utf8") || "").trim(),
|
|
389
|
+
};
|
|
390
|
+
} catch (error) {
|
|
391
|
+
return {
|
|
392
|
+
path: targetPath,
|
|
393
|
+
exists: false,
|
|
394
|
+
error: String(error?.message || error || "read failed"),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
return matches.some((entry) => entry.exists);
|
|
399
|
+
};
|
|
374
400
|
|
|
375
401
|
for (const ch of Object.keys(kChannelDefs)) {
|
|
376
402
|
const channelConfig =
|
|
@@ -404,27 +430,30 @@ const getChannelStatus = () => {
|
|
|
404
430
|
Array.from(configuredAccountIds).map((accountId) => [accountId, 0]),
|
|
405
431
|
);
|
|
406
432
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
433
|
+
if (ch !== "whatsapp") {
|
|
434
|
+
const files = fs
|
|
435
|
+
.readdirSync(credDir)
|
|
436
|
+
.filter(
|
|
437
|
+
(f) => f.startsWith(`${ch}-`) && f.endsWith("-allowFrom.json"),
|
|
438
|
+
);
|
|
439
|
+
for (const file of files) {
|
|
440
|
+
const accountId = resolveCredentialPairingAccountId({
|
|
441
|
+
channel: ch,
|
|
442
|
+
fileName: file,
|
|
443
|
+
});
|
|
444
|
+
if (!accountId || !configuredAccountIds.has(accountId)) continue;
|
|
445
|
+
const data = JSON.parse(
|
|
446
|
+
fs.readFileSync(`${credDir}/${file}`, "utf8"),
|
|
447
|
+
);
|
|
448
|
+
const nextCount =
|
|
449
|
+
Number(pairedByAccount.get(accountId) || 0)
|
|
450
|
+
+ (Array.isArray(data.allowFrom) ? data.allowFrom.length : 0);
|
|
451
|
+
pairedByAccount.set(accountId, nextCount);
|
|
452
|
+
}
|
|
425
453
|
}
|
|
426
454
|
} catch {}
|
|
427
455
|
for (const [accountId, accountConfig] of accountEntries) {
|
|
456
|
+
if (ch === "whatsapp") continue;
|
|
428
457
|
const inlineAllowFrom = accountConfig?.allowFrom;
|
|
429
458
|
if (!Array.isArray(inlineAllowFrom)) continue;
|
|
430
459
|
const normalizedAccountId = normalizeChannelAccountId(accountId);
|
|
@@ -432,6 +461,20 @@ const getChannelStatus = () => {
|
|
|
432
461
|
Number(pairedByAccount.get(normalizedAccountId) || 0) + inlineAllowFrom.length;
|
|
433
462
|
pairedByAccount.set(normalizedAccountId, nextCount);
|
|
434
463
|
}
|
|
464
|
+
if (ch === "whatsapp") {
|
|
465
|
+
for (const [accountId, accountConfig] of accountEntries) {
|
|
466
|
+
const normalizedAccountId = normalizeChannelAccountId(accountId);
|
|
467
|
+
if (Number(pairedByAccount.get(normalizedAccountId) || 0) > 0) continue;
|
|
468
|
+
if (
|
|
469
|
+
hasImplicitWhatsAppSelfPairing({
|
|
470
|
+
accountId: normalizedAccountId,
|
|
471
|
+
accountConfig,
|
|
472
|
+
})
|
|
473
|
+
) {
|
|
474
|
+
pairedByAccount.set(normalizedAccountId, 1);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
435
478
|
const accounts = Object.fromEntries(
|
|
436
479
|
Array.from(pairedByAccount.entries()).map(([accountId, paired]) => [
|
|
437
480
|
accountId,
|
|
@@ -311,6 +311,15 @@ const extractPreFillValues = ({ fs, baseDir, configFiles = [] }) => {
|
|
|
311
311
|
if (channels.discord?.token && !isAlreadyEnvRef(channels.discord.token)) {
|
|
312
312
|
preFill.DISCORD_BOT_TOKEN = channels.discord.token;
|
|
313
313
|
}
|
|
314
|
+
const whatsAppAllowFrom = Array.isArray(channels.whatsapp?.allowFrom)
|
|
315
|
+
? channels.whatsapp.allowFrom
|
|
316
|
+
: [];
|
|
317
|
+
const whatsAppOwner = whatsAppAllowFrom.find(
|
|
318
|
+
(v) => v && !isAlreadyEnvRef(String(v)),
|
|
319
|
+
);
|
|
320
|
+
if (whatsAppOwner) {
|
|
321
|
+
preFill.WHATSAPP_OWNER_NUMBER = String(whatsAppOwner);
|
|
322
|
+
}
|
|
314
323
|
|
|
315
324
|
const braveKey = cfg.tools?.web?.search?.apiKey;
|
|
316
325
|
if (braveKey && !isAlreadyEnvRef(braveKey)) {
|
|
@@ -198,6 +198,19 @@ const applyFreshOnboardingChannels = ({ cfg, varMap }) => {
|
|
|
198
198
|
ensurePluginAllowed({ cfg, pluginKey: "slack" });
|
|
199
199
|
console.log("[onboard] Slack configured");
|
|
200
200
|
}
|
|
201
|
+
if (varMap.WHATSAPP_OWNER_NUMBER) {
|
|
202
|
+
cfg.channels.whatsapp = {
|
|
203
|
+
enabled: true,
|
|
204
|
+
allowFrom: [varMap.WHATSAPP_OWNER_NUMBER],
|
|
205
|
+
groupAllowFrom: [varMap.WHATSAPP_OWNER_NUMBER],
|
|
206
|
+
dmPolicy: "allowlist",
|
|
207
|
+
groupPolicy: "allowlist",
|
|
208
|
+
selfChatMode: true,
|
|
209
|
+
};
|
|
210
|
+
cfg.plugins.entries.whatsapp = { enabled: true };
|
|
211
|
+
ensurePluginAllowed({ cfg, pluginKey: "whatsapp" });
|
|
212
|
+
console.log("[onboard] WhatsApp configured");
|
|
213
|
+
}
|
|
201
214
|
ensureUsageTrackerPluginEntry(cfg);
|
|
202
215
|
};
|
|
203
216
|
|
|
@@ -280,6 +293,32 @@ const writeManagedImportOpenclawConfig = ({ fs, openclawDir, varMap }) => {
|
|
|
280
293
|
ensurePluginAllowed({ cfg, pluginKey: "slack" });
|
|
281
294
|
}
|
|
282
295
|
|
|
296
|
+
if (varMap.WHATSAPP_OWNER_NUMBER) {
|
|
297
|
+
const existingWhatsApp = cfg.channels.whatsapp || {};
|
|
298
|
+
const existingAllowFrom = Array.isArray(existingWhatsApp.allowFrom)
|
|
299
|
+
? existingWhatsApp.allowFrom
|
|
300
|
+
: [];
|
|
301
|
+
const ownerRef = "${WHATSAPP_OWNER_NUMBER}";
|
|
302
|
+
cfg.channels.whatsapp = {
|
|
303
|
+
...existingWhatsApp,
|
|
304
|
+
enabled: true,
|
|
305
|
+
allowFrom: existingAllowFrom.includes(ownerRef)
|
|
306
|
+
? existingAllowFrom
|
|
307
|
+
: [...existingAllowFrom, ownerRef],
|
|
308
|
+
groupAllowFrom: existingAllowFrom.includes(ownerRef)
|
|
309
|
+
? existingAllowFrom
|
|
310
|
+
: [...existingAllowFrom, ownerRef],
|
|
311
|
+
dmPolicy: "allowlist",
|
|
312
|
+
groupPolicy: "allowlist",
|
|
313
|
+
selfChatMode: true,
|
|
314
|
+
};
|
|
315
|
+
cfg.plugins.entries.whatsapp = {
|
|
316
|
+
...(cfg.plugins.entries.whatsapp || {}),
|
|
317
|
+
enabled: true,
|
|
318
|
+
};
|
|
319
|
+
ensurePluginAllowed({ cfg, pluginKey: "whatsapp" });
|
|
320
|
+
}
|
|
321
|
+
|
|
283
322
|
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
284
323
|
};
|
|
285
324
|
|
|
@@ -94,7 +94,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
|
|
|
94
94
|
return hasAnyAi;
|
|
95
95
|
})();
|
|
96
96
|
const hasGithub = !!(githubToken && githubRepoInput);
|
|
97
|
-
const hasChannel = !!(varMap.TELEGRAM_BOT_TOKEN || varMap.DISCORD_BOT_TOKEN || (varMap.SLACK_BOT_TOKEN && varMap.SLACK_APP_TOKEN));
|
|
97
|
+
const hasChannel = !!(varMap.TELEGRAM_BOT_TOKEN || varMap.DISCORD_BOT_TOKEN || (varMap.SLACK_BOT_TOKEN && varMap.SLACK_APP_TOKEN) || varMap.WHATSAPP_OWNER_NUMBER);
|
|
98
98
|
|
|
99
99
|
if (!hasAi) {
|
|
100
100
|
if (selectedProvider === "openai-codex") {
|
|
@@ -124,6 +124,45 @@ const registerAgentRoutes = ({
|
|
|
124
124
|
}
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
app.post("/api/channels/accounts/login", async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const body = req.body || {};
|
|
130
|
+
const result = await agentsService.runChannelAccountLogin({
|
|
131
|
+
provider: body.provider,
|
|
132
|
+
accountId: body.accountId,
|
|
133
|
+
});
|
|
134
|
+
return res.json({
|
|
135
|
+
ok: true,
|
|
136
|
+
completed: !!result?.ok,
|
|
137
|
+
stdout: String(result?.stdout || ""),
|
|
138
|
+
stderr: String(result?.stderr || ""),
|
|
139
|
+
code: result?.code ?? null,
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const status = String(error.message || "").includes("only supported")
|
|
143
|
+
? 400
|
|
144
|
+
: 500;
|
|
145
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.get("/api/channels/accounts/login-status", (req, res) => {
|
|
150
|
+
try {
|
|
151
|
+
const provider = String(req.query?.provider || "").trim();
|
|
152
|
+
const accountId = String(req.query?.accountId || "").trim() || "default";
|
|
153
|
+
const result = agentsService.getChannelAccountLoginStatus({
|
|
154
|
+
provider,
|
|
155
|
+
accountId,
|
|
156
|
+
});
|
|
157
|
+
return res.json({ ok: true, ...result });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const status = String(error.message || "").includes("only supported")
|
|
160
|
+
? 400
|
|
161
|
+
: 500;
|
|
162
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
127
166
|
app.delete("/api/channels/accounts", async (req, res) => {
|
|
128
167
|
try {
|
|
129
168
|
const body = req.body || {};
|
|
@@ -5,7 +5,7 @@ const { buildManagedPaths } = require("../internal-files-migration");
|
|
|
5
5
|
const { parseJsonObjectFromNoisyOutput } = require("../utils/json");
|
|
6
6
|
const { quoteShellArg } = require("../utils/shell");
|
|
7
7
|
|
|
8
|
-
const kAllowedPairingChannels = new Set(["telegram", "discord", "slack"]);
|
|
8
|
+
const kAllowedPairingChannels = new Set(["telegram", "discord", "slack", "whatsapp"]);
|
|
9
9
|
const kSafePairingArgPattern = /^[\w\-:.]+$/;
|
|
10
10
|
const kDevicesListCliTimeoutMs = 5000;
|
|
11
11
|
const quoteCliArg = (value) => quoteShellArg(value, { strategy: "single" });
|
|
@@ -112,7 +112,7 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
const pending = [];
|
|
115
|
-
const channels = ["telegram", "discord", "slack"];
|
|
115
|
+
const channels = ["telegram", "discord", "slack", "whatsapp"];
|
|
116
116
|
|
|
117
117
|
for (const ch of channels) {
|
|
118
118
|
try {
|
|
@@ -2,8 +2,10 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { OPENCLAW_DIR } = require("./constants");
|
|
4
4
|
const { createSlackApi } = require("./slack-api");
|
|
5
|
+
const { quoteShellArg } = require("./utils/shell");
|
|
5
6
|
|
|
6
7
|
const kSlackBotEnvKey = "SLACK_BOT_TOKEN";
|
|
8
|
+
const kWhatsAppOwnerNumberEnvKey = "WHATSAPP_OWNER_NUMBER";
|
|
7
9
|
|
|
8
10
|
const normalizeAccountId = (value) =>
|
|
9
11
|
String(value || "").trim().toLowerCase() || "default";
|
|
@@ -106,6 +108,7 @@ const createWatchdogNotifier = ({
|
|
|
106
108
|
telegramApi,
|
|
107
109
|
discordApi,
|
|
108
110
|
slackApi,
|
|
111
|
+
clawCmd = null,
|
|
109
112
|
readEnvFile = () => [],
|
|
110
113
|
createSlackApi: createSlackApiFactory = createSlackApi,
|
|
111
114
|
fsImpl = fs,
|
|
@@ -116,7 +119,17 @@ const createWatchdogNotifier = ({
|
|
|
116
119
|
telegram: { sent: 0, failed: 0, skipped: false, targets: 0 },
|
|
117
120
|
discord: { sent: 0, failed: 0, skipped: false, targets: 0 },
|
|
118
121
|
slack: { sent: 0, failed: 0, skipped: false, targets: 0 },
|
|
122
|
+
whatsapp: { sent: 0, failed: 0, skipped: false, targets: 0 },
|
|
119
123
|
};
|
|
124
|
+
const envVars = typeof readEnvFile === "function" ? readEnvFile() : [];
|
|
125
|
+
const envMap = new Map(
|
|
126
|
+
(Array.isArray(envVars) ? envVars : [])
|
|
127
|
+
.map((entry) => [
|
|
128
|
+
String(entry?.key || "").trim(),
|
|
129
|
+
String(entry?.value || "").trim(),
|
|
130
|
+
])
|
|
131
|
+
.filter(([key]) => key),
|
|
132
|
+
);
|
|
120
133
|
const telegramTargets = getPairedIds({
|
|
121
134
|
channel: "telegram",
|
|
122
135
|
fsImpl,
|
|
@@ -174,17 +187,6 @@ const createWatchdogNotifier = ({
|
|
|
174
187
|
summary.slack.skipped = true;
|
|
175
188
|
} else {
|
|
176
189
|
const eventType = opts.eventType || "info"; // crash, recovery, health, info
|
|
177
|
-
const envVars = typeof readEnvFile === "function" ? readEnvFile() : [];
|
|
178
|
-
|
|
179
|
-
const envMap = new Map(
|
|
180
|
-
(Array.isArray(envVars) ? envVars : [])
|
|
181
|
-
.map((entry) => [
|
|
182
|
-
String(entry?.key || "").trim(),
|
|
183
|
-
String(entry?.value || "").trim(),
|
|
184
|
-
])
|
|
185
|
-
.filter(([key]) => key),
|
|
186
|
-
);
|
|
187
|
-
|
|
188
190
|
for (const [accountId, slackTargets] of slackTargetsByAccount.entries()) {
|
|
189
191
|
if (!slackTargets.length) continue;
|
|
190
192
|
const envKey = deriveSlackBotEnvKey(accountId);
|
|
@@ -261,8 +263,47 @@ const createWatchdogNotifier = ({
|
|
|
261
263
|
}
|
|
262
264
|
}
|
|
263
265
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
+
const whatsAppOwnerNumber = String(
|
|
267
|
+
envMap.get(kWhatsAppOwnerNumberEnvKey) ||
|
|
268
|
+
process.env[kWhatsAppOwnerNumberEnvKey] ||
|
|
269
|
+
"",
|
|
270
|
+
).trim();
|
|
271
|
+
const whatsappTargets = whatsAppOwnerNumber ? [whatsAppOwnerNumber] : [];
|
|
272
|
+
summary.whatsapp.targets = whatsappTargets.length;
|
|
273
|
+
if (!clawCmd || whatsappTargets.length === 0) {
|
|
274
|
+
summary.whatsapp.skipped = true;
|
|
275
|
+
} else {
|
|
276
|
+
for (const target of whatsappTargets) {
|
|
277
|
+
try {
|
|
278
|
+
const result = await clawCmd(
|
|
279
|
+
`message send --channel whatsapp --target ${quoteShellArg(
|
|
280
|
+
String(target || "").trim(),
|
|
281
|
+
)} --message ${quoteShellArg(String(message || ""))}`,
|
|
282
|
+
{ quiet: true, timeoutMs: 30000 },
|
|
283
|
+
);
|
|
284
|
+
if (!result?.ok) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
String(result?.stderr || result?.stdout || "WhatsApp send failed"),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
summary.whatsapp.sent += 1;
|
|
290
|
+
} catch (err) {
|
|
291
|
+
summary.whatsapp.failed += 1;
|
|
292
|
+
console.error(`[watchdog] whatsapp notification failed for ${target}: ${err.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const sent =
|
|
298
|
+
summary.telegram.sent +
|
|
299
|
+
summary.discord.sent +
|
|
300
|
+
summary.slack.sent +
|
|
301
|
+
summary.whatsapp.sent;
|
|
302
|
+
const failed =
|
|
303
|
+
summary.telegram.failed +
|
|
304
|
+
summary.discord.failed +
|
|
305
|
+
summary.slack.failed +
|
|
306
|
+
summary.whatsapp.failed;
|
|
266
307
|
return {
|
|
267
308
|
ok: sent > 0,
|
|
268
309
|
sent,
|