@clawdbot/zalouser 2026.1.16

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/src/monitor.ts ADDED
@@ -0,0 +1,372 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+
3
+ import type { RuntimeEnv } from "../../../src/runtime.js";
4
+ import {
5
+ isControlCommandMessage,
6
+ shouldComputeCommandAuthorized,
7
+ } from "../../../src/auto-reply/command-detection.js";
8
+ import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js";
9
+ import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js";
10
+ import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js";
11
+ import { sendMessageZalouser } from "./send.js";
12
+ import type { CoreConfig, ResolvedZalouserAccount, ZcaMessage } from "./types.js";
13
+ import { runZcaStreaming } from "./zca.js";
14
+
15
+ export type ZalouserMonitorOptions = {
16
+ account: ResolvedZalouserAccount;
17
+ config: CoreConfig;
18
+ runtime: RuntimeEnv;
19
+ abortSignal: AbortSignal;
20
+ statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
21
+ };
22
+
23
+ export type ZalouserMonitorResult = {
24
+ stop: () => void;
25
+ };
26
+
27
+ const ZALOUSER_TEXT_LIMIT = 2000;
28
+
29
+ function logVerbose(deps: CoreChannelDeps, runtime: RuntimeEnv, message: string): void {
30
+ if (deps.shouldLogVerbose()) {
31
+ runtime.log(`[zalouser] ${message}`);
32
+ }
33
+ }
34
+
35
+ function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {
36
+ if (allowFrom.includes("*")) return true;
37
+ const normalizedSenderId = senderId.toLowerCase();
38
+ return allowFrom.some((entry) => {
39
+ const normalized = entry.toLowerCase().replace(/^(zalouser|zlu):/i, "");
40
+ return normalized === normalizedSenderId;
41
+ });
42
+ }
43
+
44
+ function startZcaListener(
45
+ runtime: RuntimeEnv,
46
+ profile: string,
47
+ onMessage: (msg: ZcaMessage) => void,
48
+ onError: (err: Error) => void,
49
+ abortSignal: AbortSignal,
50
+ ): ChildProcess {
51
+ let buffer = "";
52
+
53
+ const { proc, promise } = runZcaStreaming(["listen", "-r", "-k"], {
54
+ profile,
55
+ onData: (chunk) => {
56
+ buffer += chunk;
57
+ const lines = buffer.split("\n");
58
+ buffer = lines.pop() ?? "";
59
+ for (const line of lines) {
60
+ const trimmed = line.trim();
61
+ if (!trimmed) continue;
62
+ try {
63
+ const parsed = JSON.parse(trimmed) as ZcaMessage;
64
+ onMessage(parsed);
65
+ } catch {
66
+ // ignore non-JSON lines
67
+ }
68
+ }
69
+ },
70
+ onError,
71
+ });
72
+
73
+ proc.stderr?.on("data", (data: Buffer) => {
74
+ const text = data.toString().trim();
75
+ if (text) runtime.error(`[zalouser] zca stderr: ${text}`);
76
+ });
77
+
78
+ void promise.then((result) => {
79
+ if (!result.ok && !abortSignal.aborted) {
80
+ onError(new Error(result.stderr || `zca listen exited with code ${result.exitCode}`));
81
+ }
82
+ });
83
+
84
+ abortSignal.addEventListener(
85
+ "abort",
86
+ () => {
87
+ proc.kill("SIGTERM");
88
+ },
89
+ { once: true },
90
+ );
91
+
92
+ return proc;
93
+ }
94
+
95
+ async function processMessage(
96
+ message: ZcaMessage,
97
+ account: ResolvedZalouserAccount,
98
+ config: CoreConfig,
99
+ deps: CoreChannelDeps,
100
+ runtime: RuntimeEnv,
101
+ statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
102
+ ): Promise<void> {
103
+ const { threadId, content, timestamp, metadata } = message;
104
+ if (!content?.trim()) return;
105
+
106
+ const isGroup = metadata?.isGroup ?? false;
107
+ const senderId = metadata?.fromId ?? threadId;
108
+ const senderName = metadata?.senderName ?? "";
109
+ const chatId = threadId;
110
+
111
+ const dmPolicy = account.config.dmPolicy ?? "pairing";
112
+ const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
113
+ const rawBody = content.trim();
114
+ const shouldComputeAuth = shouldComputeCommandAuthorized(rawBody, config);
115
+ const storeAllowFrom =
116
+ !isGroup && (dmPolicy !== "open" || shouldComputeAuth)
117
+ ? await deps.readChannelAllowFromStore("zalouser").catch(() => [])
118
+ : [];
119
+ const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
120
+ const useAccessGroups = config.commands?.useAccessGroups !== false;
121
+ const senderAllowedForCommands = isSenderAllowed(senderId, effectiveAllowFrom);
122
+ const commandAuthorized = shouldComputeAuth
123
+ ? resolveCommandAuthorizedFromAuthorizers({
124
+ useAccessGroups,
125
+ authorizers: [{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands }],
126
+ })
127
+ : undefined;
128
+
129
+ if (!isGroup) {
130
+ if (dmPolicy === "disabled") {
131
+ logVerbose(deps, runtime, `Blocked zalouser DM from ${senderId} (dmPolicy=disabled)`);
132
+ return;
133
+ }
134
+
135
+ if (dmPolicy !== "open") {
136
+ const allowed = senderAllowedForCommands;
137
+
138
+ if (!allowed) {
139
+ if (dmPolicy === "pairing") {
140
+ const { code, created } = await deps.upsertChannelPairingRequest({
141
+ channel: "zalouser",
142
+ id: senderId,
143
+ meta: { name: senderName || undefined },
144
+ });
145
+
146
+ if (created) {
147
+ logVerbose(deps, runtime, `zalouser pairing request sender=${senderId}`);
148
+ try {
149
+ await sendMessageZalouser(
150
+ chatId,
151
+ deps.buildPairingReply({
152
+ channel: "zalouser",
153
+ idLine: `Your Zalo user id: ${senderId}`,
154
+ code,
155
+ }),
156
+ { profile: account.profile },
157
+ );
158
+ statusSink?.({ lastOutboundAt: Date.now() });
159
+ } catch (err) {
160
+ logVerbose(
161
+ deps,
162
+ runtime,
163
+ `zalouser pairing reply failed for ${senderId}: ${String(err)}`,
164
+ );
165
+ }
166
+ }
167
+ } else {
168
+ logVerbose(
169
+ deps,
170
+ runtime,
171
+ `Blocked unauthorized zalouser sender ${senderId} (dmPolicy=${dmPolicy})`,
172
+ );
173
+ }
174
+ return;
175
+ }
176
+ }
177
+ }
178
+
179
+ if (isGroup && isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
180
+ logVerbose(deps, runtime, `zalouser: drop control command from unauthorized sender ${senderId}`);
181
+ return;
182
+ }
183
+
184
+ const peer = isGroup ? { kind: "group" as const, id: chatId } : { kind: "group" as const, id: senderId };
185
+
186
+ const route = deps.resolveAgentRoute({
187
+ cfg: config,
188
+ channel: "zalouser",
189
+ accountId: account.accountId,
190
+ peer: {
191
+ // Use "group" kind to avoid dmScope=main collapsing all DMs into the main session.
192
+ kind: peer.kind,
193
+ id: peer.id,
194
+ },
195
+ });
196
+
197
+ const rawBody = content.trim();
198
+ const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
199
+ const body = deps.formatAgentEnvelope({
200
+ channel: "Zalo Personal",
201
+ from: fromLabel,
202
+ timestamp: timestamp ? timestamp * 1000 : undefined,
203
+ body: rawBody,
204
+ });
205
+
206
+ const ctxPayload = finalizeInboundContext({
207
+ Body: body,
208
+ RawBody: rawBody,
209
+ CommandBody: rawBody,
210
+ From: isGroup ? `zalouser:group:${chatId}` : `zalouser:${senderId}`,
211
+ To: `zalouser:${chatId}`,
212
+ SessionKey: route.sessionKey,
213
+ AccountId: route.accountId,
214
+ ChatType: isGroup ? "group" : "direct",
215
+ ConversationLabel: fromLabel,
216
+ SenderName: senderName || undefined,
217
+ SenderId: senderId,
218
+ CommandAuthorized: commandAuthorized,
219
+ Provider: "zalouser",
220
+ Surface: "zalouser",
221
+ MessageSid: message.msgId ?? `${timestamp}`,
222
+ OriginatingChannel: "zalouser",
223
+ OriginatingTo: `zalouser:${chatId}`,
224
+ });
225
+
226
+ await deps.dispatchReplyWithBufferedBlockDispatcher({
227
+ ctx: ctxPayload,
228
+ cfg: config,
229
+ dispatcherOptions: {
230
+ deliver: async (payload) => {
231
+ await deliverZalouserReply({
232
+ payload: payload as { text?: string; mediaUrls?: string[]; mediaUrl?: string },
233
+ profile: account.profile,
234
+ chatId,
235
+ isGroup,
236
+ runtime,
237
+ deps,
238
+ statusSink,
239
+ });
240
+ },
241
+ onError: (err, info) => {
242
+ runtime.error(
243
+ `[${account.accountId}] Zalouser ${info.kind} reply failed: ${String(err)}`,
244
+ );
245
+ },
246
+ },
247
+ });
248
+ }
249
+
250
+ async function deliverZalouserReply(params: {
251
+ payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string };
252
+ profile: string;
253
+ chatId: string;
254
+ isGroup: boolean;
255
+ runtime: RuntimeEnv;
256
+ deps: CoreChannelDeps;
257
+ statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
258
+ }): Promise<void> {
259
+ const { payload, profile, chatId, isGroup, runtime, deps, statusSink } = params;
260
+
261
+ const mediaList = payload.mediaUrls?.length
262
+ ? payload.mediaUrls
263
+ : payload.mediaUrl
264
+ ? [payload.mediaUrl]
265
+ : [];
266
+
267
+ if (mediaList.length > 0) {
268
+ let first = true;
269
+ for (const mediaUrl of mediaList) {
270
+ const caption = first ? payload.text : undefined;
271
+ first = false;
272
+ try {
273
+ logVerbose(deps, runtime, `Sending media to ${chatId}`);
274
+ await sendMessageZalouser(chatId, caption ?? "", {
275
+ profile,
276
+ mediaUrl,
277
+ isGroup,
278
+ });
279
+ statusSink?.({ lastOutboundAt: Date.now() });
280
+ } catch (err) {
281
+ runtime.error(`Zalouser media send failed: ${String(err)}`);
282
+ }
283
+ }
284
+ return;
285
+ }
286
+
287
+ if (payload.text) {
288
+ const chunks = deps.chunkMarkdownText(payload.text, ZALOUSER_TEXT_LIMIT);
289
+ logVerbose(deps, runtime, `Sending ${chunks.length} text chunk(s) to ${chatId}`);
290
+ for (const chunk of chunks) {
291
+ try {
292
+ await sendMessageZalouser(chatId, chunk, { profile, isGroup });
293
+ statusSink?.({ lastOutboundAt: Date.now() });
294
+ } catch (err) {
295
+ runtime.error(`Zalouser message send failed: ${String(err)}`);
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ export async function monitorZalouserProvider(
302
+ options: ZalouserMonitorOptions,
303
+ ): Promise<ZalouserMonitorResult> {
304
+ const { account, config, abortSignal, statusSink, runtime } = options;
305
+
306
+ const deps = await loadCoreChannelDeps();
307
+ let stopped = false;
308
+ let proc: ChildProcess | null = null;
309
+ let restartTimer: ReturnType<typeof setTimeout> | null = null;
310
+ let resolveRunning: (() => void) | null = null;
311
+
312
+ const stop = () => {
313
+ stopped = true;
314
+ if (restartTimer) {
315
+ clearTimeout(restartTimer);
316
+ restartTimer = null;
317
+ }
318
+ if (proc) {
319
+ proc.kill("SIGTERM");
320
+ proc = null;
321
+ }
322
+ resolveRunning?.();
323
+ };
324
+
325
+ const startListener = () => {
326
+ if (stopped || abortSignal.aborted) {
327
+ resolveRunning?.();
328
+ return;
329
+ }
330
+
331
+ logVerbose(
332
+ deps,
333
+ runtime,
334
+ `[${account.accountId}] starting zca listener (profile=${account.profile})`,
335
+ );
336
+
337
+ proc = startZcaListener(
338
+ runtime,
339
+ account.profile,
340
+ (msg) => {
341
+ logVerbose(deps, runtime, `[${account.accountId}] inbound message`);
342
+ statusSink?.({ lastInboundAt: Date.now() });
343
+ processMessage(msg, account, config, deps, runtime, statusSink).catch((err) => {
344
+ runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`);
345
+ });
346
+ },
347
+ (err) => {
348
+ runtime.error(`[${account.accountId}] zca listener error: ${String(err)}`);
349
+ if (!stopped && !abortSignal.aborted) {
350
+ logVerbose(deps, runtime, `[${account.accountId}] restarting listener in 5s...`);
351
+ restartTimer = setTimeout(startListener, 5000);
352
+ } else {
353
+ resolveRunning?.();
354
+ }
355
+ },
356
+ abortSignal,
357
+ );
358
+ };
359
+
360
+ // Create a promise that stays pending until abort or stop
361
+ const runningPromise = new Promise<void>((resolve) => {
362
+ resolveRunning = resolve;
363
+ abortSignal.addEventListener("abort", () => resolve(), { once: true });
364
+ });
365
+
366
+ startListener();
367
+
368
+ // Wait for the running promise to resolve (on abort/stop)
369
+ await runningPromise;
370
+
371
+ return { stop };
372
+ }
@@ -0,0 +1,312 @@
1
+ import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js";
2
+ import type { WizardPrompter } from "../../../src/wizard/prompts.js";
3
+
4
+ import {
5
+ listZalouserAccountIds,
6
+ resolveDefaultZalouserAccountId,
7
+ resolveZalouserAccountSync,
8
+ normalizeAccountId,
9
+ checkZcaAuthenticated,
10
+ } from "./accounts.js";
11
+ import { runZcaInteractive, checkZcaInstalled } from "./zca.js";
12
+ import { DEFAULT_ACCOUNT_ID, type CoreConfig } from "./types.js";
13
+
14
+ const channel = "zalouser" as const;
15
+
16
+ function setZalouserDmPolicy(
17
+ cfg: CoreConfig,
18
+ dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
19
+ ): CoreConfig {
20
+ const allowFrom =
21
+ dmPolicy === "open"
22
+ ? [...(cfg.channels?.zalouser?.allowFrom ?? []), "*"].filter(
23
+ (v, i, a) => a.indexOf(v) === i,
24
+ )
25
+ : undefined;
26
+ return {
27
+ ...cfg,
28
+ channels: {
29
+ ...cfg.channels,
30
+ zalouser: {
31
+ ...cfg.channels?.zalouser,
32
+ dmPolicy,
33
+ ...(allowFrom ? { allowFrom } : {}),
34
+ },
35
+ },
36
+ } as CoreConfig;
37
+ }
38
+
39
+ async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
40
+ await prompter.note(
41
+ [
42
+ "Zalo Personal Account login via QR code.",
43
+ "",
44
+ "Prerequisites:",
45
+ "1) Install zca-cli",
46
+ "2) You'll scan a QR code with your Zalo app",
47
+ "",
48
+ "Docs: https://docs.clawd.bot/channels/zalouser",
49
+ ].join("\n"),
50
+ "Zalo Personal Setup",
51
+ );
52
+ }
53
+
54
+ async function promptZalouserAllowFrom(params: {
55
+ cfg: CoreConfig;
56
+ prompter: WizardPrompter;
57
+ accountId: string;
58
+ }): Promise<CoreConfig> {
59
+ const { cfg, prompter, accountId } = params;
60
+ const resolved = resolveZalouserAccountSync({ cfg, accountId });
61
+ const existingAllowFrom = resolved.config.allowFrom ?? [];
62
+ const entry = await prompter.text({
63
+ message: "Zalouser allowFrom (user id)",
64
+ placeholder: "123456789",
65
+ initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
66
+ validate: (value) => {
67
+ const raw = String(value ?? "").trim();
68
+ if (!raw) return "Required";
69
+ if (!/^\d+$/.test(raw)) return "Use a numeric Zalo user id";
70
+ return undefined;
71
+ },
72
+ });
73
+ const normalized = String(entry).trim();
74
+ const merged = [
75
+ ...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
76
+ normalized,
77
+ ];
78
+ const unique = [...new Set(merged)];
79
+
80
+ if (accountId === DEFAULT_ACCOUNT_ID) {
81
+ return {
82
+ ...cfg,
83
+ channels: {
84
+ ...cfg.channels,
85
+ zalouser: {
86
+ ...cfg.channels?.zalouser,
87
+ enabled: true,
88
+ dmPolicy: "allowlist",
89
+ allowFrom: unique,
90
+ },
91
+ },
92
+ } as CoreConfig;
93
+ }
94
+
95
+ return {
96
+ ...cfg,
97
+ channels: {
98
+ ...cfg.channels,
99
+ zalouser: {
100
+ ...cfg.channels?.zalouser,
101
+ enabled: true,
102
+ accounts: {
103
+ ...(cfg.channels?.zalouser?.accounts ?? {}),
104
+ [accountId]: {
105
+ ...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
106
+ enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true,
107
+ dmPolicy: "allowlist",
108
+ allowFrom: unique,
109
+ },
110
+ },
111
+ },
112
+ },
113
+ } as CoreConfig;
114
+ }
115
+
116
+ async function promptAccountId(params: {
117
+ cfg: CoreConfig;
118
+ prompter: WizardPrompter;
119
+ label: string;
120
+ currentId: string;
121
+ listAccountIds: (cfg: CoreConfig) => string[];
122
+ defaultAccountId: string;
123
+ }): Promise<string> {
124
+ const { cfg, prompter, label, currentId, listAccountIds, defaultAccountId } = params;
125
+ const existingIds = listAccountIds(cfg);
126
+ const options = [
127
+ ...existingIds.map((id) => ({
128
+ value: id,
129
+ label: id === defaultAccountId ? `${id} (default)` : id,
130
+ })),
131
+ { value: "__new__", label: "Create new account" },
132
+ ];
133
+
134
+ const selected = await prompter.select({
135
+ message: `${label} account`,
136
+ options,
137
+ initialValue: currentId,
138
+ });
139
+
140
+ if (selected === "__new__") {
141
+ const newId = await prompter.text({
142
+ message: "New account ID",
143
+ placeholder: "work",
144
+ validate: (value) => {
145
+ const raw = String(value ?? "").trim().toLowerCase();
146
+ if (!raw) return "Required";
147
+ if (!/^[a-z0-9_-]+$/.test(raw)) return "Use lowercase alphanumeric, dash, or underscore";
148
+ if (existingIds.includes(raw)) return "Account already exists";
149
+ return undefined;
150
+ },
151
+ });
152
+ return String(newId).trim().toLowerCase();
153
+ }
154
+
155
+ return selected as string;
156
+ }
157
+
158
+ const dmPolicy: ChannelOnboardingDmPolicy = {
159
+ label: "Zalo Personal",
160
+ channel,
161
+ policyKey: "channels.zalouser.dmPolicy",
162
+ allowFromKey: "channels.zalouser.allowFrom",
163
+ getCurrent: (cfg) => ((cfg as CoreConfig).channels?.zalouser?.dmPolicy ?? "pairing") as "pairing",
164
+ setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg as CoreConfig, policy),
165
+ };
166
+
167
+ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
168
+ channel,
169
+ dmPolicy,
170
+ getStatus: async ({ cfg }) => {
171
+ const ids = listZalouserAccountIds(cfg as CoreConfig);
172
+ let configured = false;
173
+ for (const accountId of ids) {
174
+ const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId });
175
+ const isAuth = await checkZcaAuthenticated(account.profile);
176
+ if (isAuth) {
177
+ configured = true;
178
+ break;
179
+ }
180
+ }
181
+ return {
182
+ channel,
183
+ configured,
184
+ statusLines: [`Zalo Personal: ${configured ? "logged in" : "needs QR login"}`],
185
+ selectionHint: configured ? "recommended · logged in" : "recommended · QR login",
186
+ quickstartScore: configured ? 1 : 15,
187
+ };
188
+ },
189
+ configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds, forceAllowFrom }) => {
190
+ // Check zca is installed
191
+ const zcaInstalled = await checkZcaInstalled();
192
+ if (!zcaInstalled) {
193
+ await prompter.note(
194
+ [
195
+ "The `zca` binary was not found in PATH.",
196
+ "",
197
+ "Install zca-cli, then re-run onboarding:",
198
+ "Docs: https://docs.clawd.bot/channels/zalouser",
199
+ ].join("\n"),
200
+ "Missing Dependency",
201
+ );
202
+ return { cfg, accountId: DEFAULT_ACCOUNT_ID };
203
+ }
204
+
205
+ const zalouserOverride = accountOverrides.zalouser?.trim();
206
+ const defaultAccountId = resolveDefaultZalouserAccountId(cfg as CoreConfig);
207
+ let accountId = zalouserOverride
208
+ ? normalizeAccountId(zalouserOverride)
209
+ : defaultAccountId;
210
+
211
+ if (shouldPromptAccountIds && !zalouserOverride) {
212
+ accountId = await promptAccountId({
213
+ cfg: cfg as CoreConfig,
214
+ prompter,
215
+ label: "Zalo Personal",
216
+ currentId: accountId,
217
+ listAccountIds: listZalouserAccountIds,
218
+ defaultAccountId,
219
+ });
220
+ }
221
+
222
+ let next = cfg as CoreConfig;
223
+ const account = resolveZalouserAccountSync({ cfg: next, accountId });
224
+ const alreadyAuthenticated = await checkZcaAuthenticated(account.profile);
225
+
226
+ if (!alreadyAuthenticated) {
227
+ await noteZalouserHelp(prompter);
228
+
229
+ const wantsLogin = await prompter.confirm({
230
+ message: "Login via QR code now?",
231
+ initialValue: true,
232
+ });
233
+
234
+ if (wantsLogin) {
235
+ await prompter.note(
236
+ "A QR code will appear in your terminal.\nScan it with your Zalo app to login.",
237
+ "QR Login",
238
+ );
239
+
240
+ // Run interactive login
241
+ const result = await runZcaInteractive(["auth", "login"], {
242
+ profile: account.profile,
243
+ });
244
+
245
+ if (!result.ok) {
246
+ await prompter.note(
247
+ `Login failed: ${result.stderr || "Unknown error"}`,
248
+ "Error",
249
+ );
250
+ } else {
251
+ const isNowAuth = await checkZcaAuthenticated(account.profile);
252
+ if (isNowAuth) {
253
+ await prompter.note("Login successful!", "Success");
254
+ }
255
+ }
256
+ }
257
+ } else {
258
+ const keepSession = await prompter.confirm({
259
+ message: "Zalo Personal already logged in. Keep session?",
260
+ initialValue: true,
261
+ });
262
+ if (!keepSession) {
263
+ await runZcaInteractive(["auth", "logout"], { profile: account.profile });
264
+ await runZcaInteractive(["auth", "login"], { profile: account.profile });
265
+ }
266
+ }
267
+
268
+ // Enable the channel
269
+ if (accountId === DEFAULT_ACCOUNT_ID) {
270
+ next = {
271
+ ...next,
272
+ channels: {
273
+ ...next.channels,
274
+ zalouser: {
275
+ ...next.channels?.zalouser,
276
+ enabled: true,
277
+ profile: account.profile !== "default" ? account.profile : undefined,
278
+ },
279
+ },
280
+ } as CoreConfig;
281
+ } else {
282
+ next = {
283
+ ...next,
284
+ channels: {
285
+ ...next.channels,
286
+ zalouser: {
287
+ ...next.channels?.zalouser,
288
+ enabled: true,
289
+ accounts: {
290
+ ...(next.channels?.zalouser?.accounts ?? {}),
291
+ [accountId]: {
292
+ ...(next.channels?.zalouser?.accounts?.[accountId] ?? {}),
293
+ enabled: true,
294
+ profile: account.profile,
295
+ },
296
+ },
297
+ },
298
+ },
299
+ } as CoreConfig;
300
+ }
301
+
302
+ if (forceAllowFrom) {
303
+ next = await promptZalouserAllowFrom({
304
+ cfg: next,
305
+ prompter,
306
+ accountId,
307
+ });
308
+ }
309
+
310
+ return { cfg: next, accountId };
311
+ },
312
+ };