@chrysb/alphaclaw 0.9.5 → 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 +2 -2
- package/patches/openclaw+2026.4.14.patch +13 -0
- package/patches/openclaw+2026.4.11.patch +0 -13
|
@@ -18,6 +18,7 @@ const kChannelEnvKeys = {
|
|
|
18
18
|
telegram: "TELEGRAM_BOT_TOKEN",
|
|
19
19
|
discord: "DISCORD_BOT_TOKEN",
|
|
20
20
|
slack: "SLACK_BOT_TOKEN",
|
|
21
|
+
whatsapp: "WHATSAPP_OWNER_NUMBER",
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
const kChannelExtraEnvKeys = {
|
|
@@ -229,8 +230,10 @@ export const CreateChannelModal = ({
|
|
|
229
230
|
}
|
|
230
231
|
setName(providerLabel);
|
|
231
232
|
}, [provider, providerHasAccounts, nameEditedManually, isEditMode]);
|
|
233
|
+
const normalizedProvider = String(provider || "").trim();
|
|
232
234
|
const isSingleAccountProvider = isSingleAccountChannelProvider(provider);
|
|
233
|
-
const needsAppToken =
|
|
235
|
+
const needsAppToken = normalizedProvider === "slack";
|
|
236
|
+
const isWhatsApp = normalizedProvider === "whatsapp";
|
|
234
237
|
|
|
235
238
|
const accountId = useMemo(() => {
|
|
236
239
|
if (isEditMode) {
|
|
@@ -419,20 +422,34 @@ export const CreateChannelModal = ({
|
|
|
419
422
|
</label>
|
|
420
423
|
|
|
421
424
|
<label class="block space-y-1">
|
|
422
|
-
<span class="text-xs text-
|
|
423
|
-
${needsAppToken ? "Bot Token" : "Token"}
|
|
425
|
+
<span class="text-xs text-gray-400">
|
|
426
|
+
${isWhatsApp ? "Owner Number" : needsAppToken ? "Bot Token" : "Token"}
|
|
424
427
|
</span>
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
428
|
+
${isWhatsApp
|
|
429
|
+
? html`
|
|
430
|
+
<input
|
|
431
|
+
type="text"
|
|
432
|
+
value=${token}
|
|
433
|
+
onInput=${(event) => setToken(event.target.value)}
|
|
434
|
+
placeholder="+15551234567"
|
|
435
|
+
class="w-full bg-field border border-border rounded-lg px-3 py-2 text-sm font-mono text-body outline-none focus:border-fg-muted"
|
|
436
|
+
/>
|
|
437
|
+
`
|
|
438
|
+
: html`
|
|
439
|
+
<${SecretInput}
|
|
440
|
+
value=${token}
|
|
441
|
+
onInput=${(event) => setToken(event.target.value)}
|
|
442
|
+
placeholder=${token ? "" : "Paste bot token"}
|
|
443
|
+
loading=${loadingToken}
|
|
444
|
+
isSecret=${true}
|
|
445
|
+
inputClass="w-full bg-field border border-border rounded-lg px-3 py-2 text-sm font-mono text-body outline-none focus:border-fg-muted"
|
|
446
|
+
/>
|
|
447
|
+
`}
|
|
433
448
|
<p class="text-xs text-fg-muted">
|
|
434
|
-
|
|
435
|
-
|
|
449
|
+
${isWhatsApp
|
|
450
|
+
? "E.164 format phone number used for allowlist pairing."
|
|
451
|
+
: html`Saved behind the scenes as
|
|
452
|
+
<code class="font-mono text-fg-muted ml-1">${envKey || "CHANNEL_TOKEN"}</code>.`}
|
|
436
453
|
</p>
|
|
437
454
|
</label>
|
|
438
455
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { ActionButton } from "./action-button.js";
|
|
4
|
+
import { CloseIcon } from "./icons.js";
|
|
5
|
+
import { ModalShell } from "./modal-shell.js";
|
|
6
|
+
import { PageHeader } from "./page-header.js";
|
|
7
|
+
|
|
8
|
+
const html = htm.bind(h);
|
|
9
|
+
|
|
10
|
+
export const ChannelLoginModal = ({
|
|
11
|
+
visible = false,
|
|
12
|
+
loading = false,
|
|
13
|
+
title = "Link Channel",
|
|
14
|
+
output = "",
|
|
15
|
+
error = "",
|
|
16
|
+
runDisabled = false,
|
|
17
|
+
runLabel = "Generate QR",
|
|
18
|
+
runLoadingLabel = "Running...",
|
|
19
|
+
closeLabel = "Close",
|
|
20
|
+
onRun = async () => {},
|
|
21
|
+
onClose = () => {},
|
|
22
|
+
}) => {
|
|
23
|
+
if (!visible) return null;
|
|
24
|
+
const hasOutput = !!String(output || "").trim();
|
|
25
|
+
const hasError = !!String(error || "").trim();
|
|
26
|
+
const displayOutput = hasOutput
|
|
27
|
+
? String(output)
|
|
28
|
+
: hasError
|
|
29
|
+
? String(error)
|
|
30
|
+
: "No output yet. Generate QR to start login.";
|
|
31
|
+
return html`
|
|
32
|
+
<${ModalShell}
|
|
33
|
+
visible=${visible}
|
|
34
|
+
onClose=${onClose}
|
|
35
|
+
panelClassName="bg-modal border border-border rounded-xl p-6 max-w-2xl w-full space-y-4"
|
|
36
|
+
>
|
|
37
|
+
<${PageHeader}
|
|
38
|
+
title=${title}
|
|
39
|
+
actions=${html`
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onclick=${onClose}
|
|
43
|
+
class="h-8 w-8 inline-flex items-center justify-center rounded-lg ac-btn-secondary"
|
|
44
|
+
aria-label="Close modal"
|
|
45
|
+
>
|
|
46
|
+
<${CloseIcon} className="w-3.5 h-3.5 text-gray-300" />
|
|
47
|
+
</button>
|
|
48
|
+
`}
|
|
49
|
+
/>
|
|
50
|
+
<div class="space-y-3">
|
|
51
|
+
<p class="text-xs text-gray-500">
|
|
52
|
+
Click "Generate QR" to run channel login and capture terminal output.
|
|
53
|
+
</p>
|
|
54
|
+
<textarea
|
|
55
|
+
readonly
|
|
56
|
+
wrap="off"
|
|
57
|
+
value=${displayOutput}
|
|
58
|
+
class="w-full h-[440px] max-h-[70vh] text-[11px] leading-[1.1] font-mono text-gray-300 bg-black/30 border border-border rounded-lg p-3 outline-none resize-y overflow-auto"
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="flex justify-end gap-2 pt-1">
|
|
62
|
+
<${ActionButton}
|
|
63
|
+
onClick=${onClose}
|
|
64
|
+
disabled=${loading}
|
|
65
|
+
loading=${false}
|
|
66
|
+
tone="secondary"
|
|
67
|
+
size="sm"
|
|
68
|
+
idleLabel=${closeLabel}
|
|
69
|
+
/>
|
|
70
|
+
<${ActionButton}
|
|
71
|
+
onClick=${onRun}
|
|
72
|
+
disabled=${loading || runDisabled}
|
|
73
|
+
loading=${loading}
|
|
74
|
+
tone="primary"
|
|
75
|
+
size="sm"
|
|
76
|
+
idleLabel=${runLabel}
|
|
77
|
+
loadingLabel=${runLoadingLabel}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</${ModalShell}>
|
|
81
|
+
`;
|
|
82
|
+
};
|
|
@@ -8,14 +8,19 @@ import {
|
|
|
8
8
|
import htm from "htm";
|
|
9
9
|
import { AddChannelMenu } from "./add-channel-menu.js";
|
|
10
10
|
import { ChannelAccountStatusBadge } from "./channel-account-status-badge.js";
|
|
11
|
+
import { ChannelLoginModal } from "./channel-login-modal.js";
|
|
11
12
|
import { ConfirmDialog } from "./confirm-dialog.js";
|
|
12
13
|
import { OverflowMenu, OverflowMenuItem } from "./overflow-menu.js";
|
|
13
14
|
import {
|
|
14
15
|
deleteChannelAccount,
|
|
15
16
|
fetchChannelAccounts,
|
|
17
|
+
fetchChannelAccountLoginStatus,
|
|
18
|
+
fetchRestartStatus,
|
|
19
|
+
runChannelAccountLogin,
|
|
16
20
|
updateChannelAccount,
|
|
17
21
|
} from "../lib/api.js";
|
|
18
22
|
import { useCachedFetch } from "../hooks/use-cached-fetch.js";
|
|
23
|
+
import { usePolling } from "../hooks/usePolling.js";
|
|
19
24
|
import {
|
|
20
25
|
isImplicitDefaultAccount,
|
|
21
26
|
resolveChannelAccountLabel,
|
|
@@ -27,11 +32,12 @@ import { showToast } from "./toast.js";
|
|
|
27
32
|
|
|
28
33
|
const html = htm.bind(h);
|
|
29
34
|
|
|
30
|
-
const ALL_CHANNELS = ["telegram", "discord", "slack"];
|
|
35
|
+
const ALL_CHANNELS = ["telegram", "discord", "slack", "whatsapp"];
|
|
31
36
|
const kChannelMeta = {
|
|
32
37
|
telegram: { label: "Telegram", iconSrc: "/assets/icons/telegram.svg" },
|
|
33
38
|
discord: { label: "Discord", iconSrc: "/assets/icons/discord.svg" },
|
|
34
39
|
slack: { label: "Slack", iconSrc: "/assets/icons/slack.svg" },
|
|
40
|
+
whatsapp: { label: "WhatsApp", iconSrc: "/assets/icons/whatsapp.svg" },
|
|
35
41
|
};
|
|
36
42
|
|
|
37
43
|
const getChannelMeta = (channelId = "") => {
|
|
@@ -49,6 +55,48 @@ const getChannelMeta = (channelId = "") => {
|
|
|
49
55
|
const announceRestartRequired = () =>
|
|
50
56
|
window.dispatchEvent(new CustomEvent("alphaclaw:restart-required"));
|
|
51
57
|
|
|
58
|
+
const appendTerminalOutput = (previousOutput = "", nextChunk = "") =>
|
|
59
|
+
[String(previousOutput || "").trim(), String(nextChunk || "").trim()]
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join("\n\n");
|
|
62
|
+
|
|
63
|
+
const cloneLoginModalState = (state = {}) => ({
|
|
64
|
+
loginAccount: state.loginAccount || null,
|
|
65
|
+
loginOutput: String(state.loginOutput || ""),
|
|
66
|
+
loginError: String(state.loginError || ""),
|
|
67
|
+
loginRunning: !!state.loginRunning,
|
|
68
|
+
loginMonitoring: !!state.loginMonitoring,
|
|
69
|
+
loginCompleted: !!state.loginCompleted,
|
|
70
|
+
loginLinked: !!state.loginLinked,
|
|
71
|
+
loginRestartingGateway: !!state.loginRestartingGateway,
|
|
72
|
+
loginRestartedGateway: !!state.loginRestartedGateway,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
let kPreservedChannelLoginModalState = null;
|
|
76
|
+
|
|
77
|
+
const clearChannelLoginModalState = ({
|
|
78
|
+
setLoginAccount,
|
|
79
|
+
setLoginOutput,
|
|
80
|
+
setLoginError,
|
|
81
|
+
setLoginRunning,
|
|
82
|
+
setLoginMonitoring,
|
|
83
|
+
setLoginCompleted,
|
|
84
|
+
setLoginLinked,
|
|
85
|
+
setLoginRestartingGateway,
|
|
86
|
+
setLoginRestartedGateway,
|
|
87
|
+
}) => {
|
|
88
|
+
kPreservedChannelLoginModalState = null;
|
|
89
|
+
setLoginAccount(null);
|
|
90
|
+
setLoginOutput("");
|
|
91
|
+
setLoginError("");
|
|
92
|
+
setLoginRunning(false);
|
|
93
|
+
setLoginMonitoring(false);
|
|
94
|
+
setLoginCompleted(false);
|
|
95
|
+
setLoginLinked(false);
|
|
96
|
+
setLoginRestartingGateway(false);
|
|
97
|
+
setLoginRestartedGateway(false);
|
|
98
|
+
};
|
|
99
|
+
|
|
52
100
|
export const ChannelsCard = ({
|
|
53
101
|
title = "Channels",
|
|
54
102
|
items = [],
|
|
@@ -140,7 +188,9 @@ export const Channels = ({
|
|
|
140
188
|
agents = [],
|
|
141
189
|
onNavigate = () => {},
|
|
142
190
|
onRefreshStatuses = () => {},
|
|
191
|
+
onRestartGateway = async () => ({ ok: false }),
|
|
143
192
|
}) => {
|
|
193
|
+
const preservedLoginState = cloneLoginModalState(kPreservedChannelLoginModalState || {});
|
|
144
194
|
const [saving, setSaving] = useState(false);
|
|
145
195
|
const [createLoadingLabel, setCreateLoadingLabel] = useState("Creating...");
|
|
146
196
|
const [menuOpenId, setMenuOpenId] = useState("");
|
|
@@ -156,6 +206,20 @@ export const Channels = ({
|
|
|
156
206
|
const channelAccounts = Array.isArray(channelAccountsPayload?.channels)
|
|
157
207
|
? channelAccountsPayload.channels
|
|
158
208
|
: [];
|
|
209
|
+
const [loginAccount, setLoginAccount] = useState(preservedLoginState.loginAccount);
|
|
210
|
+
const [loginOutput, setLoginOutput] = useState(preservedLoginState.loginOutput);
|
|
211
|
+
const [loginError, setLoginError] = useState(preservedLoginState.loginError);
|
|
212
|
+
const [loginRunning, setLoginRunning] = useState(preservedLoginState.loginRunning);
|
|
213
|
+
const [loginMonitoring, setLoginMonitoring] = useState(preservedLoginState.loginMonitoring);
|
|
214
|
+
const [loginCompleted, setLoginCompleted] = useState(preservedLoginState.loginCompleted);
|
|
215
|
+
const [loginLinked, setLoginLinked] = useState(preservedLoginState.loginLinked);
|
|
216
|
+
const [loginRestartingGateway, setLoginRestartingGateway] = useState(
|
|
217
|
+
preservedLoginState.loginRestartingGateway,
|
|
218
|
+
);
|
|
219
|
+
const [loginRestartedGateway, setLoginRestartedGateway] = useState(
|
|
220
|
+
preservedLoginState.loginRestartedGateway,
|
|
221
|
+
);
|
|
222
|
+
const [loginRestartStatusChecked, setLoginRestartStatusChecked] = useState(false);
|
|
159
223
|
|
|
160
224
|
const loadChannelAccounts = useCallback(async () => {
|
|
161
225
|
try {
|
|
@@ -163,6 +227,62 @@ export const Channels = ({
|
|
|
163
227
|
} catch {}
|
|
164
228
|
}, [refreshChannelAccounts]);
|
|
165
229
|
|
|
230
|
+
const loginStatusPoll = usePolling(
|
|
231
|
+
() =>
|
|
232
|
+
fetchChannelAccountLoginStatus({
|
|
233
|
+
provider: loginAccount?.provider,
|
|
234
|
+
accountId: loginAccount?.id,
|
|
235
|
+
}),
|
|
236
|
+
1000,
|
|
237
|
+
{
|
|
238
|
+
enabled:
|
|
239
|
+
!!loginAccount &&
|
|
240
|
+
!!loginMonitoring &&
|
|
241
|
+
String(loginAccount?.provider || "").trim() === "whatsapp",
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
const restartStatusPoll = usePolling(fetchRestartStatus, 2000, {
|
|
245
|
+
enabled: !!loginAccount && !!loginRestartingGateway,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const appendLoginOutput = useCallback((nextChunk = "") => {
|
|
249
|
+
setLoginOutput((currentOutput) => appendTerminalOutput(currentOutput, nextChunk));
|
|
250
|
+
}, []);
|
|
251
|
+
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
const nextState = cloneLoginModalState({
|
|
254
|
+
loginAccount,
|
|
255
|
+
loginOutput,
|
|
256
|
+
loginError,
|
|
257
|
+
loginRunning,
|
|
258
|
+
loginMonitoring,
|
|
259
|
+
loginCompleted,
|
|
260
|
+
loginLinked,
|
|
261
|
+
loginRestartingGateway,
|
|
262
|
+
loginRestartedGateway,
|
|
263
|
+
});
|
|
264
|
+
const hasActiveLoginState =
|
|
265
|
+
!!nextState.loginAccount ||
|
|
266
|
+
!!nextState.loginOutput ||
|
|
267
|
+
!!nextState.loginError ||
|
|
268
|
+
!!nextState.loginRunning ||
|
|
269
|
+
!!nextState.loginMonitoring ||
|
|
270
|
+
!!nextState.loginCompleted ||
|
|
271
|
+
!!nextState.loginLinked ||
|
|
272
|
+
!!nextState.loginRestartingGateway ||
|
|
273
|
+
!!nextState.loginRestartedGateway;
|
|
274
|
+
kPreservedChannelLoginModalState = hasActiveLoginState ? nextState : null;
|
|
275
|
+
}, [
|
|
276
|
+
loginAccount,
|
|
277
|
+
loginCompleted,
|
|
278
|
+
loginError,
|
|
279
|
+
loginLinked,
|
|
280
|
+
loginMonitoring,
|
|
281
|
+
loginOutput,
|
|
282
|
+
loginRestartedGateway,
|
|
283
|
+
loginRestartingGateway,
|
|
284
|
+
loginRunning,
|
|
285
|
+
]);
|
|
166
286
|
|
|
167
287
|
const configuredChannelMap = useMemo(
|
|
168
288
|
() =>
|
|
@@ -192,6 +312,51 @@ export const Channels = ({
|
|
|
192
312
|
);
|
|
193
313
|
const showAgentBadge = agents.length > 0;
|
|
194
314
|
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
const handleOpenWhatsAppQr = () => {
|
|
317
|
+
const configuredWhatsApp = channelAccounts.find(
|
|
318
|
+
(entry) => String(entry?.channel || "").trim() === "whatsapp",
|
|
319
|
+
);
|
|
320
|
+
const account = Array.isArray(configuredWhatsApp?.accounts)
|
|
321
|
+
? configuredWhatsApp.accounts[0]
|
|
322
|
+
: null;
|
|
323
|
+
if (!account) return;
|
|
324
|
+
const accountId = String(account?.id || "").trim() || "default";
|
|
325
|
+
const boundAgentId = String(account?.boundAgentId || "").trim();
|
|
326
|
+
const ownerAgentId =
|
|
327
|
+
boundAgentId ||
|
|
328
|
+
(isImplicitDefaultAccount({ accountId, boundAgentId })
|
|
329
|
+
? defaultAgentId
|
|
330
|
+
: "");
|
|
331
|
+
const accountData = {
|
|
332
|
+
id: accountId,
|
|
333
|
+
provider: "whatsapp",
|
|
334
|
+
name: resolveChannelAccountLabel({
|
|
335
|
+
channelId: "whatsapp",
|
|
336
|
+
account,
|
|
337
|
+
providerLabel: getChannelMeta("whatsapp").label || "WhatsApp",
|
|
338
|
+
}),
|
|
339
|
+
ownerAgentId,
|
|
340
|
+
envKey: String(account?.envKey || "").trim(),
|
|
341
|
+
token: String(account?.token || "").trim(),
|
|
342
|
+
};
|
|
343
|
+
setLoginAccount(accountData);
|
|
344
|
+
setLoginOutput("");
|
|
345
|
+
setLoginError("");
|
|
346
|
+
setLoginRunning(false);
|
|
347
|
+
setLoginMonitoring(false);
|
|
348
|
+
setLoginCompleted(false);
|
|
349
|
+
setLoginLinked(false);
|
|
350
|
+
setLoginRestartingGateway(false);
|
|
351
|
+
setLoginRestartedGateway(false);
|
|
352
|
+
setLoginRestartStatusChecked(false);
|
|
353
|
+
};
|
|
354
|
+
window.addEventListener("alphaclaw:open-whatsapp-qr", handleOpenWhatsAppQr);
|
|
355
|
+
return () => {
|
|
356
|
+
window.removeEventListener("alphaclaw:open-whatsapp-qr", handleOpenWhatsAppQr);
|
|
357
|
+
};
|
|
358
|
+
}, [channelAccounts, defaultAgentId]);
|
|
359
|
+
|
|
195
360
|
const handleUpdateChannel = async (payload) => {
|
|
196
361
|
setSaving(true);
|
|
197
362
|
try {
|
|
@@ -259,6 +424,134 @@ export const Channels = ({
|
|
|
259
424
|
setSaving(false);
|
|
260
425
|
}
|
|
261
426
|
};
|
|
427
|
+
const handleRunChannelLogin = async () => {
|
|
428
|
+
if (!loginAccount) return;
|
|
429
|
+
setLoginRunning(true);
|
|
430
|
+
setLoginMonitoring(true);
|
|
431
|
+
setLoginCompleted(false);
|
|
432
|
+
setLoginLinked(false);
|
|
433
|
+
setLoginRestartingGateway(false);
|
|
434
|
+
setLoginRestartedGateway(false);
|
|
435
|
+
setLoginRestartStatusChecked(false);
|
|
436
|
+
setLoginError("");
|
|
437
|
+
setLoginOutput("");
|
|
438
|
+
try {
|
|
439
|
+
const result = await runChannelAccountLogin({
|
|
440
|
+
provider: loginAccount.provider,
|
|
441
|
+
accountId: loginAccount.id,
|
|
442
|
+
});
|
|
443
|
+
const combinedOutput = appendTerminalOutput(result?.stdout || "", result?.stderr || "");
|
|
444
|
+
setLoginOutput(combinedOutput || "No terminal output captured.");
|
|
445
|
+
setLoginCompleted(!!result?.completed);
|
|
446
|
+
if (result?.completed) {
|
|
447
|
+
await loginStatusPoll.refresh();
|
|
448
|
+
}
|
|
449
|
+
} catch (error) {
|
|
450
|
+
setLoginError(String(error?.message || "Could not start channel login"));
|
|
451
|
+
setLoginMonitoring(false);
|
|
452
|
+
} finally {
|
|
453
|
+
setLoginRunning(false);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
useEffect(() => {
|
|
458
|
+
if (!loginAccount || !loginMonitoring || loginLinked || loginRestartingGateway) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!loginStatusPoll.data?.linked) return;
|
|
462
|
+
|
|
463
|
+
let cancelled = false;
|
|
464
|
+
setLoginLinked(true);
|
|
465
|
+
setLoginError("");
|
|
466
|
+
appendLoginOutput("✅ Saved WhatsApp credentials detected.");
|
|
467
|
+
|
|
468
|
+
(async () => {
|
|
469
|
+
setLoginRestartingGateway(true);
|
|
470
|
+
setLoginRestartStatusChecked(false);
|
|
471
|
+
appendLoginOutput("Restarting the gateway so the new WhatsApp session comes online...");
|
|
472
|
+
try {
|
|
473
|
+
const restartResult = await onRestartGateway();
|
|
474
|
+
if (restartResult && restartResult.ok === false) {
|
|
475
|
+
throw new Error(restartResult.error || "Could not restart gateway");
|
|
476
|
+
}
|
|
477
|
+
if (cancelled) return;
|
|
478
|
+
appendLoginOutput("✅ Gateway restart triggered. Waiting for it to come back online...");
|
|
479
|
+
await restartStatusPoll.refresh();
|
|
480
|
+
if (cancelled) return;
|
|
481
|
+
setLoginRestartStatusChecked(true);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (cancelled) return;
|
|
484
|
+
setLoginError(String(error?.message || "Could not restart gateway"));
|
|
485
|
+
appendLoginOutput(
|
|
486
|
+
"WhatsApp linked, but the gateway restart failed. You may need to restart it manually.",
|
|
487
|
+
);
|
|
488
|
+
setLoginRestartStatusChecked(false);
|
|
489
|
+
setLoginRestartingGateway(false);
|
|
490
|
+
}
|
|
491
|
+
})();
|
|
492
|
+
|
|
493
|
+
return () => {
|
|
494
|
+
cancelled = true;
|
|
495
|
+
};
|
|
496
|
+
}, [
|
|
497
|
+
appendLoginOutput,
|
|
498
|
+
loadChannelAccounts,
|
|
499
|
+
loginAccount,
|
|
500
|
+
loginLinked,
|
|
501
|
+
loginMonitoring,
|
|
502
|
+
loginRestartingGateway,
|
|
503
|
+
loginStatusPoll.data?.linked,
|
|
504
|
+
onRefreshStatuses,
|
|
505
|
+
onRestartGateway,
|
|
506
|
+
restartStatusPoll.refresh,
|
|
507
|
+
]);
|
|
508
|
+
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
if (!loginAccount || !loginRestartingGateway) return;
|
|
511
|
+
if (!loginRestartStatusChecked) return;
|
|
512
|
+
const restartInProgress = !!restartStatusPoll.data?.restartInProgress;
|
|
513
|
+
const gatewayRunning = restartStatusPoll.data?.gatewayRunning !== false;
|
|
514
|
+
if (restartInProgress || !gatewayRunning) return;
|
|
515
|
+
|
|
516
|
+
let cancelled = false;
|
|
517
|
+
|
|
518
|
+
(async () => {
|
|
519
|
+
setLoginRestartedGateway(true);
|
|
520
|
+
setLoginMonitoring(false);
|
|
521
|
+
appendLoginOutput("✅ Gateway restart complete.");
|
|
522
|
+
showToast("Channel linked", "success");
|
|
523
|
+
await Promise.all([
|
|
524
|
+
loadChannelAccounts(),
|
|
525
|
+
Promise.resolve(onRefreshStatuses?.()),
|
|
526
|
+
]);
|
|
527
|
+
if (cancelled) return;
|
|
528
|
+
clearChannelLoginModalState({
|
|
529
|
+
setLoginAccount,
|
|
530
|
+
setLoginOutput,
|
|
531
|
+
setLoginError,
|
|
532
|
+
setLoginRunning,
|
|
533
|
+
setLoginMonitoring,
|
|
534
|
+
setLoginCompleted,
|
|
535
|
+
setLoginLinked,
|
|
536
|
+
setLoginRestartingGateway,
|
|
537
|
+
setLoginRestartedGateway,
|
|
538
|
+
});
|
|
539
|
+
})();
|
|
540
|
+
|
|
541
|
+
return () => {
|
|
542
|
+
cancelled = true;
|
|
543
|
+
};
|
|
544
|
+
}, [
|
|
545
|
+
appendLoginOutput,
|
|
546
|
+
loadChannelAccounts,
|
|
547
|
+
loginAccount,
|
|
548
|
+
loginRestartStatusChecked,
|
|
549
|
+
loginRestartingGateway,
|
|
550
|
+
onRefreshStatuses,
|
|
551
|
+
restartStatusPoll.data?.gatewayRunning,
|
|
552
|
+
restartStatusPoll.data?.restartInProgress,
|
|
553
|
+
]);
|
|
554
|
+
|
|
262
555
|
const openCreateChannelModal = (provider) => {
|
|
263
556
|
setMenuOpenId("");
|
|
264
557
|
setEditingAccount({
|
|
@@ -396,6 +689,27 @@ export const Channels = ({
|
|
|
396
689
|
>
|
|
397
690
|
Edit
|
|
398
691
|
</${OverflowMenuItem}>
|
|
692
|
+
${channelId === "whatsapp"
|
|
693
|
+
? html`
|
|
694
|
+
<${OverflowMenuItem}
|
|
695
|
+
onClick=${() => {
|
|
696
|
+
setMenuOpenId("");
|
|
697
|
+
setLoginAccount(accountData);
|
|
698
|
+
setLoginOutput("");
|
|
699
|
+
setLoginError("");
|
|
700
|
+
setLoginRunning(false);
|
|
701
|
+
setLoginMonitoring(false);
|
|
702
|
+
setLoginCompleted(false);
|
|
703
|
+
setLoginLinked(false);
|
|
704
|
+
setLoginRestartingGateway(false);
|
|
705
|
+
setLoginRestartedGateway(false);
|
|
706
|
+
setLoginRestartStatusChecked(false);
|
|
707
|
+
}}
|
|
708
|
+
>
|
|
709
|
+
Link WhatsApp (QR)
|
|
710
|
+
</${OverflowMenuItem}>
|
|
711
|
+
`
|
|
712
|
+
: null}
|
|
399
713
|
<${OverflowMenuItem}
|
|
400
714
|
className="text-status-error hover:text-status-error"
|
|
401
715
|
onClick=${() => {
|
|
@@ -518,6 +832,38 @@ export const Channels = ({
|
|
|
518
832
|
setDeletingAccount(null);
|
|
519
833
|
}}
|
|
520
834
|
/>
|
|
835
|
+
<${ChannelLoginModal}
|
|
836
|
+
visible=${!!loginAccount}
|
|
837
|
+
loading=${loginRunning || loginRestartingGateway}
|
|
838
|
+
title=${`Link ${String(loginAccount?.name || "WhatsApp").trim()} via QR`}
|
|
839
|
+
output=${loginOutput}
|
|
840
|
+
error=${loginError}
|
|
841
|
+
onRun=${handleRunChannelLogin}
|
|
842
|
+
onClose=${() => {
|
|
843
|
+
if (loginRunning || loginRestartingGateway) return;
|
|
844
|
+
clearChannelLoginModalState({
|
|
845
|
+
setLoginAccount,
|
|
846
|
+
setLoginOutput,
|
|
847
|
+
setLoginError,
|
|
848
|
+
setLoginRunning,
|
|
849
|
+
setLoginMonitoring,
|
|
850
|
+
setLoginCompleted,
|
|
851
|
+
setLoginLinked,
|
|
852
|
+
setLoginRestartingGateway,
|
|
853
|
+
setLoginRestartedGateway,
|
|
854
|
+
});
|
|
855
|
+
}}
|
|
856
|
+
runDisabled=${loginRunning || loginRestartingGateway || loginRestartedGateway}
|
|
857
|
+
runLabel=${loginLinked
|
|
858
|
+
? loginRestartingGateway
|
|
859
|
+
? "Restarting..."
|
|
860
|
+
: loginRestartedGateway
|
|
861
|
+
? "Linked"
|
|
862
|
+
: "Awaiting restart..."
|
|
863
|
+
: "Generate QR"}
|
|
864
|
+
runLoadingLabel=${loginRestartingGateway ? "Restarting..." : "Running..."}
|
|
865
|
+
closeLabel=${loginRestartedGateway ? "Done" : "Close"}
|
|
866
|
+
/>
|
|
521
867
|
</div>
|
|
522
868
|
`;
|
|
523
869
|
};
|
|
@@ -5,6 +5,7 @@ import { Channels } from "../channels.js";
|
|
|
5
5
|
import { ChannelOperationsPanel } from "../channel-operations-panel.js";
|
|
6
6
|
import { Pairings } from "../pairings.js";
|
|
7
7
|
import { DevicePairings } from "../device-pairings.js";
|
|
8
|
+
import { ActionButton } from "../action-button.js";
|
|
8
9
|
import { Google } from "../google/index.js";
|
|
9
10
|
import { Features } from "../features.js";
|
|
10
11
|
import { GeneralDoctorWarning } from "../doctor/general-warning.js";
|
|
@@ -14,6 +15,11 @@ import { useGeneralTab } from "./use-general-tab.js";
|
|
|
14
15
|
|
|
15
16
|
const html = htm.bind(h);
|
|
16
17
|
|
|
18
|
+
const openWhatsAppQrModal = () => {
|
|
19
|
+
if (typeof window === "undefined") return;
|
|
20
|
+
window.dispatchEvent(new CustomEvent("alphaclaw:open-whatsapp-qr"));
|
|
21
|
+
};
|
|
22
|
+
|
|
17
23
|
export const GeneralTab = ({
|
|
18
24
|
statusData = null,
|
|
19
25
|
watchdogData = null,
|
|
@@ -39,6 +45,23 @@ export const GeneralTab = ({
|
|
|
39
45
|
isActive,
|
|
40
46
|
restartSignal,
|
|
41
47
|
});
|
|
48
|
+
const whatsappStatus = state.channels?.whatsapp || null;
|
|
49
|
+
const whatsappAccounts =
|
|
50
|
+
whatsappStatus?.accounts && typeof whatsappStatus.accounts === "object"
|
|
51
|
+
? whatsappStatus.accounts
|
|
52
|
+
: {};
|
|
53
|
+
const hasWhatsAppAwaitingPairing =
|
|
54
|
+
Object.keys(whatsappAccounts).length > 0
|
|
55
|
+
? Object.values(whatsappAccounts).some(
|
|
56
|
+
(account) => account && account.status !== "paired",
|
|
57
|
+
)
|
|
58
|
+
: String(whatsappStatus?.status || "").trim() === "configured";
|
|
59
|
+
const showWhatsAppPairingCard =
|
|
60
|
+
state.hasUnpaired &&
|
|
61
|
+
!state.pairingStatusRefreshing &&
|
|
62
|
+
Array.isArray(state.pending) &&
|
|
63
|
+
state.pending.length === 0 &&
|
|
64
|
+
hasWhatsAppAwaitingPairing;
|
|
42
65
|
|
|
43
66
|
return html`
|
|
44
67
|
<div class="space-y-4">
|
|
@@ -64,17 +87,42 @@ export const GeneralTab = ({
|
|
|
64
87
|
agents=${agents}
|
|
65
88
|
onNavigate=${onNavigate}
|
|
66
89
|
onRefreshStatuses=${onRefreshStatuses}
|
|
90
|
+
onRestartGateway=${onRestartGateway}
|
|
67
91
|
/>
|
|
68
92
|
`}
|
|
69
93
|
pairingsSection=${html`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
${showWhatsAppPairingCard
|
|
95
|
+
? html`
|
|
96
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
97
|
+
<h2 class="card-label mb-3">Pending Pairings</h2>
|
|
98
|
+
<div class="text-center py-4 space-y-3">
|
|
99
|
+
<img
|
|
100
|
+
src="/assets/icons/whatsapp.svg"
|
|
101
|
+
alt=""
|
|
102
|
+
class="w-10 h-10 mx-auto"
|
|
103
|
+
aria-hidden="true"
|
|
104
|
+
/>
|
|
105
|
+
<p class="text-body text-sm font-medium">WhatsApp needs to be linked</p>
|
|
106
|
+
<p class="text-fg-dim text-xs">Scan the QR code to finish pairing this channel.</p>
|
|
107
|
+
<${ActionButton}
|
|
108
|
+
onClick=${openWhatsAppQrModal}
|
|
109
|
+
tone="primary"
|
|
110
|
+
size="sm"
|
|
111
|
+
idleLabel="Open QR Code"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
`
|
|
116
|
+
: html`
|
|
117
|
+
<${Pairings}
|
|
118
|
+
pending=${state.pending}
|
|
119
|
+
channels=${state.channels}
|
|
120
|
+
visible=${state.hasUnpaired}
|
|
121
|
+
statusRefreshing=${state.pairingStatusRefreshing}
|
|
122
|
+
onApprove=${actions.handleApprove}
|
|
123
|
+
onReject=${actions.handleReject}
|
|
124
|
+
/>
|
|
125
|
+
`}
|
|
78
126
|
`}
|
|
79
127
|
/>
|
|
80
128
|
<${Features} onSwitchTab=${onSwitchTab} />
|