@openclaw/zalouser 2026.3.1 → 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/CHANGELOG.md +22 -0
- package/README.md +41 -147
- package/index.ts +3 -5
- package/package.json +5 -3
- package/src/accounts.test.ts +214 -0
- package/src/accounts.ts +24 -51
- package/src/channel.sendpayload.test.ts +117 -0
- package/src/channel.test.ts +123 -1
- package/src/channel.ts +232 -272
- package/src/config-schema.ts +2 -1
- package/src/group-policy.test.ts +49 -0
- package/src/group-policy.ts +78 -0
- package/src/message-sid.test.ts +66 -0
- package/src/message-sid.ts +80 -0
- package/src/monitor.account-scope.test.ts +113 -0
- package/src/monitor.group-gating.test.ts +211 -0
- package/src/monitor.send-mocks.ts +20 -0
- package/src/monitor.ts +321 -260
- package/src/onboarding.ts +106 -171
- package/src/probe.test.ts +60 -0
- package/src/probe.ts +20 -13
- package/src/qr-temp-file.ts +22 -0
- package/src/reaction.test.ts +19 -0
- package/src/reaction.ts +29 -0
- package/src/runtime.ts +1 -1
- package/src/send.test.ts +116 -115
- package/src/send.ts +63 -117
- package/src/status-issues.test.ts +1 -15
- package/src/status-issues.ts +8 -27
- package/src/tool.test.ts +149 -0
- package/src/tool.ts +36 -54
- package/src/types.ts +52 -42
- package/src/zalo-js.ts +1401 -0
- package/src/zca-client.ts +216 -0
- package/src/zca-js-exports.d.ts +22 -0
- package/src/zca.ts +0 -198
package/src/channel.ts
CHANGED
|
@@ -1,25 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildAccountScopedDmSecurityPolicy,
|
|
3
|
+
mapAllowFromEntries,
|
|
4
|
+
} from "openclaw/plugin-sdk/compat";
|
|
1
5
|
import type {
|
|
2
6
|
ChannelAccountSnapshot,
|
|
3
7
|
ChannelDirectoryEntry,
|
|
4
8
|
ChannelDock,
|
|
5
9
|
ChannelGroupContext,
|
|
10
|
+
ChannelMessageActionAdapter,
|
|
6
11
|
ChannelPlugin,
|
|
7
12
|
OpenClawConfig,
|
|
8
13
|
GroupToolPolicyConfig,
|
|
9
|
-
} from "openclaw/plugin-sdk";
|
|
14
|
+
} from "openclaw/plugin-sdk/zalouser";
|
|
10
15
|
import {
|
|
11
16
|
applyAccountNameToChannelSection,
|
|
17
|
+
applySetupAccountConfigPatch,
|
|
18
|
+
buildChannelSendResult,
|
|
19
|
+
buildBaseAccountStatusSnapshot,
|
|
12
20
|
buildChannelConfigSchema,
|
|
13
21
|
DEFAULT_ACCOUNT_ID,
|
|
14
22
|
chunkTextForOutbound,
|
|
15
23
|
deleteAccountFromConfigSection,
|
|
16
24
|
formatAllowFromLowercase,
|
|
17
|
-
|
|
25
|
+
isNumericTargetId,
|
|
18
26
|
migrateBaseNameToDefaultAccount,
|
|
19
27
|
normalizeAccountId,
|
|
20
|
-
|
|
28
|
+
sendPayloadWithChunkedTextAndMedia,
|
|
21
29
|
setAccountEnabledInConfigSection,
|
|
22
|
-
} from "openclaw/plugin-sdk";
|
|
30
|
+
} from "openclaw/plugin-sdk/zalouser";
|
|
23
31
|
import {
|
|
24
32
|
listZalouserAccountIds,
|
|
25
33
|
resolveDefaultZalouserAccountId,
|
|
@@ -29,12 +37,22 @@ import {
|
|
|
29
37
|
type ResolvedZalouserAccount,
|
|
30
38
|
} from "./accounts.js";
|
|
31
39
|
import { ZalouserConfigSchema } from "./config-schema.js";
|
|
40
|
+
import { buildZalouserGroupCandidates, findZalouserGroupEntry } from "./group-policy.js";
|
|
41
|
+
import { resolveZalouserReactionMessageIds } from "./message-sid.js";
|
|
32
42
|
import { zalouserOnboardingAdapter } from "./onboarding.js";
|
|
33
43
|
import { probeZalouser } from "./probe.js";
|
|
34
|
-
import {
|
|
44
|
+
import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
|
|
45
|
+
import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
|
|
35
46
|
import { collectZalouserStatusIssues } from "./status-issues.js";
|
|
36
|
-
import
|
|
37
|
-
|
|
47
|
+
import {
|
|
48
|
+
listZaloFriendsMatching,
|
|
49
|
+
listZaloGroupMembers,
|
|
50
|
+
listZaloGroupsMatching,
|
|
51
|
+
logoutZaloProfile,
|
|
52
|
+
startZaloQrLogin,
|
|
53
|
+
waitForZaloQrLogin,
|
|
54
|
+
getZaloUserInfo,
|
|
55
|
+
} from "./zalo-js.js";
|
|
38
56
|
|
|
39
57
|
const meta = {
|
|
40
58
|
id: "zalouser",
|
|
@@ -51,7 +69,7 @@ const meta = {
|
|
|
51
69
|
function resolveZalouserQrProfile(accountId?: string | null): string {
|
|
52
70
|
const normalized = normalizeAccountId(accountId);
|
|
53
71
|
if (!normalized || normalized === DEFAULT_ACCOUNT_ID) {
|
|
54
|
-
return process.env.ZCA_PROFILE?.trim() || "default";
|
|
72
|
+
return process.env.ZALOUSER_PROFILE?.trim() || process.env.ZCA_PROFILE?.trim() || "default";
|
|
55
73
|
}
|
|
56
74
|
return normalized;
|
|
57
75
|
}
|
|
@@ -84,28 +102,105 @@ function mapGroup(params: {
|
|
|
84
102
|
};
|
|
85
103
|
}
|
|
86
104
|
|
|
87
|
-
function
|
|
88
|
-
params: ChannelGroupContext,
|
|
89
|
-
): GroupToolPolicyConfig | undefined {
|
|
105
|
+
function resolveZalouserGroupPolicyEntry(params: ChannelGroupContext) {
|
|
90
106
|
const account = resolveZalouserAccountSync({
|
|
91
107
|
cfg: params.cfg,
|
|
92
108
|
accountId: params.accountId ?? undefined,
|
|
93
109
|
});
|
|
94
110
|
const groups = account.config.groups ?? {};
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
return findZalouserGroupEntry(
|
|
112
|
+
groups,
|
|
113
|
+
buildZalouserGroupCandidates({
|
|
114
|
+
groupId: params.groupId,
|
|
115
|
+
groupChannel: params.groupChannel,
|
|
116
|
+
includeWildcard: true,
|
|
117
|
+
}),
|
|
99
118
|
);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveZalouserGroupToolPolicy(
|
|
122
|
+
params: ChannelGroupContext,
|
|
123
|
+
): GroupToolPolicyConfig | undefined {
|
|
124
|
+
return resolveZalouserGroupPolicyEntry(params)?.tools;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveZalouserRequireMention(params: ChannelGroupContext): boolean {
|
|
128
|
+
const entry = resolveZalouserGroupPolicyEntry(params);
|
|
129
|
+
if (typeof entry?.requireMention === "boolean") {
|
|
130
|
+
return entry.requireMention;
|
|
105
131
|
}
|
|
106
|
-
return
|
|
132
|
+
return true;
|
|
107
133
|
}
|
|
108
134
|
|
|
135
|
+
const zalouserMessageActions: ChannelMessageActionAdapter = {
|
|
136
|
+
listActions: ({ cfg }) => {
|
|
137
|
+
const accounts = listZalouserAccountIds(cfg)
|
|
138
|
+
.map((accountId) => resolveZalouserAccountSync({ cfg, accountId }))
|
|
139
|
+
.filter((account) => account.enabled);
|
|
140
|
+
if (accounts.length === 0) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
return ["react"];
|
|
144
|
+
},
|
|
145
|
+
supportsAction: ({ action }) => action === "react",
|
|
146
|
+
handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
|
|
147
|
+
if (action !== "react") {
|
|
148
|
+
throw new Error(`Zalouser action ${action} not supported`);
|
|
149
|
+
}
|
|
150
|
+
const account = resolveZalouserAccountSync({ cfg, accountId });
|
|
151
|
+
const threadId =
|
|
152
|
+
(typeof params.threadId === "string" ? params.threadId.trim() : "") ||
|
|
153
|
+
(typeof params.to === "string" ? params.to.trim() : "") ||
|
|
154
|
+
(typeof params.chatId === "string" ? params.chatId.trim() : "") ||
|
|
155
|
+
(toolContext?.currentChannelId?.trim() ?? "");
|
|
156
|
+
if (!threadId) {
|
|
157
|
+
throw new Error("Zalouser react requires threadId (or to/chatId).");
|
|
158
|
+
}
|
|
159
|
+
const emoji = typeof params.emoji === "string" ? params.emoji.trim() : "";
|
|
160
|
+
if (!emoji) {
|
|
161
|
+
throw new Error("Zalouser react requires emoji.");
|
|
162
|
+
}
|
|
163
|
+
const ids = resolveZalouserReactionMessageIds({
|
|
164
|
+
messageId: typeof params.messageId === "string" ? params.messageId : undefined,
|
|
165
|
+
cliMsgId: typeof params.cliMsgId === "string" ? params.cliMsgId : undefined,
|
|
166
|
+
currentMessageId: toolContext?.currentMessageId,
|
|
167
|
+
});
|
|
168
|
+
if (!ids) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"Zalouser react requires messageId + cliMsgId (or a current message context id).",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const result = await sendReactionZalouser({
|
|
174
|
+
profile: account.profile,
|
|
175
|
+
threadId,
|
|
176
|
+
isGroup: params.isGroup === true,
|
|
177
|
+
msgId: ids.msgId,
|
|
178
|
+
cliMsgId: ids.cliMsgId,
|
|
179
|
+
emoji,
|
|
180
|
+
remove: params.remove === true,
|
|
181
|
+
});
|
|
182
|
+
if (!result.ok) {
|
|
183
|
+
throw new Error(result.error || "Failed to react on Zalo message");
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
content: [
|
|
187
|
+
{
|
|
188
|
+
type: "text" as const,
|
|
189
|
+
text:
|
|
190
|
+
params.remove === true
|
|
191
|
+
? `Removed reaction ${emoji} from ${ids.msgId}`
|
|
192
|
+
: `Reacted ${emoji} on ${ids.msgId}`,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
details: {
|
|
196
|
+
messageId: ids.msgId,
|
|
197
|
+
cliMsgId: ids.cliMsgId,
|
|
198
|
+
threadId,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
109
204
|
export const zalouserDock: ChannelDock = {
|
|
110
205
|
id: "zalouser",
|
|
111
206
|
capabilities: {
|
|
@@ -116,14 +211,12 @@ export const zalouserDock: ChannelDock = {
|
|
|
116
211
|
outbound: { textChunkLimit: 2000 },
|
|
117
212
|
config: {
|
|
118
213
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
119
|
-
(resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom
|
|
120
|
-
String(entry),
|
|
121
|
-
),
|
|
214
|
+
mapAllowFromEntries(resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom),
|
|
122
215
|
formatAllowFrom: ({ allowFrom }) =>
|
|
123
216
|
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
|
|
124
217
|
},
|
|
125
218
|
groups: {
|
|
126
|
-
resolveRequireMention:
|
|
219
|
+
resolveRequireMention: resolveZalouserRequireMention,
|
|
127
220
|
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
|
128
221
|
},
|
|
129
222
|
threading: {
|
|
@@ -173,14 +266,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
173
266
|
"messagePrefix",
|
|
174
267
|
],
|
|
175
268
|
}),
|
|
176
|
-
isConfigured: async (account) =>
|
|
177
|
-
// Check if zca auth status is OK for this profile
|
|
178
|
-
const result = await runZca(["auth", "status"], {
|
|
179
|
-
profile: account.profile,
|
|
180
|
-
timeout: 5000,
|
|
181
|
-
});
|
|
182
|
-
return result.ok;
|
|
183
|
-
},
|
|
269
|
+
isConfigured: async (account) => await checkZcaAuthenticated(account.profile),
|
|
184
270
|
describeAccount: (account): ChannelAccountSnapshot => ({
|
|
185
271
|
accountId: account.accountId,
|
|
186
272
|
name: account.name,
|
|
@@ -188,37 +274,32 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
188
274
|
configured: undefined,
|
|
189
275
|
}),
|
|
190
276
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
191
|
-
(resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom
|
|
192
|
-
String(entry),
|
|
193
|
-
),
|
|
277
|
+
mapAllowFromEntries(resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom),
|
|
194
278
|
formatAllowFrom: ({ allowFrom }) =>
|
|
195
279
|
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
|
|
196
280
|
},
|
|
197
281
|
security: {
|
|
198
282
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
199
|
-
|
|
200
|
-
const basePath = resolveChannelAccountConfigBasePath({
|
|
283
|
+
return buildAccountScopedDmSecurityPolicy({
|
|
201
284
|
cfg,
|
|
202
285
|
channelKey: "zalouser",
|
|
203
|
-
accountId
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
policy: account.config.dmPolicy ?? "pairing",
|
|
286
|
+
accountId,
|
|
287
|
+
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
288
|
+
policy: account.config.dmPolicy,
|
|
207
289
|
allowFrom: account.config.allowFrom ?? [],
|
|
208
|
-
|
|
209
|
-
allowFromPath: basePath,
|
|
210
|
-
approveHint: formatPairingApproveHint("zalouser"),
|
|
290
|
+
policyPathSuffix: "dmPolicy",
|
|
211
291
|
normalizeEntry: (raw) => raw.replace(/^(zalouser|zlu):/i, ""),
|
|
212
|
-
};
|
|
292
|
+
});
|
|
213
293
|
},
|
|
214
294
|
},
|
|
215
295
|
groups: {
|
|
216
|
-
resolveRequireMention:
|
|
296
|
+
resolveRequireMention: resolveZalouserRequireMention,
|
|
217
297
|
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
|
218
298
|
},
|
|
219
299
|
threading: {
|
|
220
300
|
resolveReplyToMode: () => "off",
|
|
221
301
|
},
|
|
302
|
+
actions: zalouserMessageActions,
|
|
222
303
|
setup: {
|
|
223
304
|
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
224
305
|
applyAccountName: ({ cfg, accountId, name }) =>
|
|
@@ -243,35 +324,12 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
243
324
|
channelKey: "zalouser",
|
|
244
325
|
})
|
|
245
326
|
: namedConfig;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
...next.channels?.zalouser,
|
|
253
|
-
enabled: true,
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
} as OpenClawConfig;
|
|
257
|
-
}
|
|
258
|
-
return {
|
|
259
|
-
...next,
|
|
260
|
-
channels: {
|
|
261
|
-
...next.channels,
|
|
262
|
-
zalouser: {
|
|
263
|
-
...next.channels?.zalouser,
|
|
264
|
-
enabled: true,
|
|
265
|
-
accounts: {
|
|
266
|
-
...next.channels?.zalouser?.accounts,
|
|
267
|
-
[accountId]: {
|
|
268
|
-
...next.channels?.zalouser?.accounts?.[accountId],
|
|
269
|
-
enabled: true,
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
},
|
|
274
|
-
} as OpenClawConfig;
|
|
327
|
+
return applySetupAccountConfigPatch({
|
|
328
|
+
cfg: next,
|
|
329
|
+
channelKey: "zalouser",
|
|
330
|
+
accountId,
|
|
331
|
+
patch: {},
|
|
332
|
+
});
|
|
275
333
|
},
|
|
276
334
|
},
|
|
277
335
|
messaging: {
|
|
@@ -283,32 +341,14 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
283
341
|
return trimmed.replace(/^(zalouser|zlu):/i, "");
|
|
284
342
|
},
|
|
285
343
|
targetResolver: {
|
|
286
|
-
looksLikeId:
|
|
287
|
-
const trimmed = raw.trim();
|
|
288
|
-
if (!trimmed) {
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
291
|
-
return /^\d{3,}$/.test(trimmed);
|
|
292
|
-
},
|
|
344
|
+
looksLikeId: isNumericTargetId,
|
|
293
345
|
hint: "<threadId>",
|
|
294
346
|
},
|
|
295
347
|
},
|
|
296
348
|
directory: {
|
|
297
|
-
self: async ({ cfg, accountId
|
|
298
|
-
const ok = await checkZcaInstalled();
|
|
299
|
-
if (!ok) {
|
|
300
|
-
throw new Error("Missing dependency: `zca` not found in PATH");
|
|
301
|
-
}
|
|
349
|
+
self: async ({ cfg, accountId }) => {
|
|
302
350
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
303
|
-
const
|
|
304
|
-
profile: account.profile,
|
|
305
|
-
timeout: 10000,
|
|
306
|
-
});
|
|
307
|
-
if (!result.ok) {
|
|
308
|
-
runtime.error(result.stderr || "Failed to fetch profile");
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
const parsed = parseJsonOutput<ZcaUserInfo>(result.stdout);
|
|
351
|
+
const parsed = await getZaloUserInfo(account.profile);
|
|
312
352
|
if (!parsed?.userId) {
|
|
313
353
|
return null;
|
|
314
354
|
}
|
|
@@ -320,92 +360,42 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
320
360
|
});
|
|
321
361
|
},
|
|
322
362
|
listPeers: async ({ cfg, accountId, query, limit }) => {
|
|
323
|
-
const ok = await checkZcaInstalled();
|
|
324
|
-
if (!ok) {
|
|
325
|
-
throw new Error("Missing dependency: `zca` not found in PATH");
|
|
326
|
-
}
|
|
327
363
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
id: String(f.userId),
|
|
338
|
-
name: f.displayName ?? null,
|
|
339
|
-
avatarUrl: f.avatar ?? null,
|
|
340
|
-
raw: f,
|
|
341
|
-
}),
|
|
342
|
-
)
|
|
343
|
-
: [];
|
|
364
|
+
const friends = await listZaloFriendsMatching(account.profile, query);
|
|
365
|
+
const rows = friends.map((friend) =>
|
|
366
|
+
mapUser({
|
|
367
|
+
id: String(friend.userId),
|
|
368
|
+
name: friend.displayName ?? null,
|
|
369
|
+
avatarUrl: friend.avatar ?? null,
|
|
370
|
+
raw: friend,
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
344
373
|
return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
|
|
345
374
|
},
|
|
346
375
|
listGroups: async ({ cfg, accountId, query, limit }) => {
|
|
347
|
-
const ok = await checkZcaInstalled();
|
|
348
|
-
if (!ok) {
|
|
349
|
-
throw new Error("Missing dependency: `zca` not found in PATH");
|
|
350
|
-
}
|
|
351
376
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
let rows = Array.isArray(parsed)
|
|
361
|
-
? parsed.map((g) =>
|
|
362
|
-
mapGroup({
|
|
363
|
-
id: String(g.groupId),
|
|
364
|
-
name: g.name ?? null,
|
|
365
|
-
raw: g,
|
|
366
|
-
}),
|
|
367
|
-
)
|
|
368
|
-
: [];
|
|
369
|
-
const q = query?.trim().toLowerCase();
|
|
370
|
-
if (q) {
|
|
371
|
-
rows = rows.filter((g) => (g.name ?? "").toLowerCase().includes(q) || g.id.includes(q));
|
|
372
|
-
}
|
|
377
|
+
const groups = await listZaloGroupsMatching(account.profile, query);
|
|
378
|
+
const rows = groups.map((group) =>
|
|
379
|
+
mapGroup({
|
|
380
|
+
id: String(group.groupId),
|
|
381
|
+
name: group.name ?? null,
|
|
382
|
+
raw: group,
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
373
385
|
return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
|
|
374
386
|
},
|
|
375
387
|
listGroupMembers: async ({ cfg, accountId, groupId, limit }) => {
|
|
376
|
-
const ok = await checkZcaInstalled();
|
|
377
|
-
if (!ok) {
|
|
378
|
-
throw new Error("Missing dependency: `zca` not found in PATH");
|
|
379
|
-
}
|
|
380
388
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
result.stdout,
|
|
389
|
+
const members = await listZaloGroupMembers(account.profile, groupId);
|
|
390
|
+
const rows = members.map((member) =>
|
|
391
|
+
mapUser({
|
|
392
|
+
id: member.userId,
|
|
393
|
+
name: member.displayName,
|
|
394
|
+
avatarUrl: member.avatar ?? null,
|
|
395
|
+
raw: member,
|
|
396
|
+
}),
|
|
390
397
|
);
|
|
391
|
-
|
|
392
|
-
? parsed
|
|
393
|
-
.map((m) => {
|
|
394
|
-
const id = m.userId ?? (m as { id?: string | number }).id;
|
|
395
|
-
if (!id) {
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
return mapUser({
|
|
399
|
-
id: String(id),
|
|
400
|
-
name: (m as { displayName?: string }).displayName ?? null,
|
|
401
|
-
avatarUrl: (m as { avatar?: string }).avatar ?? null,
|
|
402
|
-
raw: m,
|
|
403
|
-
});
|
|
404
|
-
})
|
|
405
|
-
.filter(Boolean)
|
|
406
|
-
: [];
|
|
407
|
-
const sliced = typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
|
|
408
|
-
return sliced as ChannelDirectoryEntry[];
|
|
398
|
+
return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
|
|
409
399
|
},
|
|
410
400
|
},
|
|
411
401
|
resolver: {
|
|
@@ -426,48 +416,27 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
426
416
|
cfg: cfg,
|
|
427
417
|
accountId: accountId ?? DEFAULT_ACCOUNT_ID,
|
|
428
418
|
});
|
|
429
|
-
const args =
|
|
430
|
-
kind === "user"
|
|
431
|
-
? trimmed
|
|
432
|
-
? ["friend", "find", trimmed]
|
|
433
|
-
: ["friend", "list", "-j"]
|
|
434
|
-
: ["group", "list", "-j"];
|
|
435
|
-
const result = await runZca(args, { profile: account.profile, timeout: 15000 });
|
|
436
|
-
if (!result.ok) {
|
|
437
|
-
throw new Error(result.stderr || "zca lookup failed");
|
|
438
|
-
}
|
|
439
419
|
if (kind === "user") {
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
? parsed.map((f) => ({
|
|
443
|
-
id: String(f.userId),
|
|
444
|
-
name: f.displayName ?? undefined,
|
|
445
|
-
}))
|
|
446
|
-
: [];
|
|
447
|
-
const best = matches[0];
|
|
420
|
+
const friends = await listZaloFriendsMatching(account.profile, trimmed);
|
|
421
|
+
const best = friends[0];
|
|
448
422
|
results.push({
|
|
449
423
|
input,
|
|
450
|
-
resolved: Boolean(best?.
|
|
451
|
-
id: best?.
|
|
452
|
-
name: best?.
|
|
453
|
-
note:
|
|
424
|
+
resolved: Boolean(best?.userId),
|
|
425
|
+
id: best?.userId,
|
|
426
|
+
name: best?.displayName,
|
|
427
|
+
note: friends.length > 1 ? "multiple matches; chose first" : undefined,
|
|
454
428
|
});
|
|
455
429
|
} else {
|
|
456
|
-
const
|
|
457
|
-
const matches = Array.isArray(parsed)
|
|
458
|
-
? parsed.map((g) => ({
|
|
459
|
-
id: String(g.groupId),
|
|
460
|
-
name: g.name ?? undefined,
|
|
461
|
-
}))
|
|
462
|
-
: [];
|
|
430
|
+
const groups = await listZaloGroupsMatching(account.profile, trimmed);
|
|
463
431
|
const best =
|
|
464
|
-
|
|
432
|
+
groups.find((group) => group.name.toLowerCase() === trimmed.toLowerCase()) ??
|
|
433
|
+
groups[0];
|
|
465
434
|
results.push({
|
|
466
435
|
input,
|
|
467
|
-
resolved: Boolean(best?.
|
|
468
|
-
id: best?.
|
|
436
|
+
resolved: Boolean(best?.groupId),
|
|
437
|
+
id: best?.groupId,
|
|
469
438
|
name: best?.name,
|
|
470
|
-
note:
|
|
439
|
+
note: groups.length > 1 ? "multiple matches; chose first" : undefined,
|
|
471
440
|
});
|
|
472
441
|
}
|
|
473
442
|
} catch (err) {
|
|
@@ -498,19 +467,32 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
498
467
|
cfg: cfg,
|
|
499
468
|
accountId: accountId ?? DEFAULT_ACCOUNT_ID,
|
|
500
469
|
});
|
|
501
|
-
|
|
502
|
-
if (!ok) {
|
|
503
|
-
throw new Error(
|
|
504
|
-
"Missing dependency: `zca` not found in PATH. See docs.openclaw.ai/channels/zalouser",
|
|
505
|
-
);
|
|
506
|
-
}
|
|
470
|
+
|
|
507
471
|
runtime.log(
|
|
508
|
-
`
|
|
472
|
+
`Generating QR login for Zalo Personal (account: ${account.accountId}, profile: ${account.profile})...`,
|
|
509
473
|
);
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
474
|
+
|
|
475
|
+
const started = await startZaloQrLogin({
|
|
476
|
+
profile: account.profile,
|
|
477
|
+
timeoutMs: 35_000,
|
|
478
|
+
});
|
|
479
|
+
if (!started.qrDataUrl) {
|
|
480
|
+
throw new Error(started.message || "Failed to start QR login");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const qrPath = await writeQrDataUrlToTempFile(started.qrDataUrl, account.profile);
|
|
484
|
+
if (qrPath) {
|
|
485
|
+
runtime.log(`Scan QR image: ${qrPath}`);
|
|
486
|
+
} else {
|
|
487
|
+
runtime.log("QR generated but could not be written to a temp file.");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const waited = await waitForZaloQrLogin({ profile: account.profile, timeoutMs: 180_000 });
|
|
491
|
+
if (!waited.connected) {
|
|
492
|
+
throw new Error(waited.message || "Zalouser login failed");
|
|
513
493
|
}
|
|
494
|
+
|
|
495
|
+
runtime.log(waited.message);
|
|
514
496
|
},
|
|
515
497
|
},
|
|
516
498
|
outbound: {
|
|
@@ -518,28 +500,28 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
518
500
|
chunker: chunkTextForOutbound,
|
|
519
501
|
chunkerMode: "text",
|
|
520
502
|
textChunkLimit: 2000,
|
|
503
|
+
sendPayload: async (ctx) =>
|
|
504
|
+
await sendPayloadWithChunkedTextAndMedia({
|
|
505
|
+
ctx,
|
|
506
|
+
textChunkLimit: zalouserPlugin.outbound!.textChunkLimit,
|
|
507
|
+
chunker: zalouserPlugin.outbound!.chunker,
|
|
508
|
+
sendText: (nextCtx) => zalouserPlugin.outbound!.sendText!(nextCtx),
|
|
509
|
+
sendMedia: (nextCtx) => zalouserPlugin.outbound!.sendMedia!(nextCtx),
|
|
510
|
+
emptyResult: { channel: "zalouser", messageId: "" },
|
|
511
|
+
}),
|
|
521
512
|
sendText: async ({ to, text, accountId, cfg }) => {
|
|
522
513
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
523
514
|
const result = await sendMessageZalouser(to, text, { profile: account.profile });
|
|
524
|
-
return
|
|
525
|
-
channel: "zalouser",
|
|
526
|
-
ok: result.ok,
|
|
527
|
-
messageId: result.messageId ?? "",
|
|
528
|
-
error: result.error ? new Error(result.error) : undefined,
|
|
529
|
-
};
|
|
515
|
+
return buildChannelSendResult("zalouser", result);
|
|
530
516
|
},
|
|
531
|
-
sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => {
|
|
517
|
+
sendMedia: async ({ to, text, mediaUrl, accountId, cfg, mediaLocalRoots }) => {
|
|
532
518
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
533
519
|
const result = await sendMessageZalouser(to, text, {
|
|
534
520
|
profile: account.profile,
|
|
535
521
|
mediaUrl,
|
|
522
|
+
mediaLocalRoots,
|
|
536
523
|
});
|
|
537
|
-
return
|
|
538
|
-
channel: "zalouser",
|
|
539
|
-
ok: result.ok,
|
|
540
|
-
messageId: result.messageId ?? "",
|
|
541
|
-
error: result.error ? new Error(result.error) : undefined,
|
|
542
|
-
};
|
|
524
|
+
return buildChannelSendResult("zalouser", result);
|
|
543
525
|
},
|
|
544
526
|
},
|
|
545
527
|
status: {
|
|
@@ -562,20 +544,21 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
562
544
|
}),
|
|
563
545
|
probeAccount: async ({ account, timeoutMs }) => probeZalouser(account.profile, timeoutMs),
|
|
564
546
|
buildAccountSnapshot: async ({ account, runtime }) => {
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
const
|
|
547
|
+
const configured = await checkZcaAuthenticated(account.profile);
|
|
548
|
+
const configError = "not authenticated";
|
|
549
|
+
const base = buildBaseAccountStatusSnapshot({
|
|
550
|
+
account: {
|
|
551
|
+
accountId: account.accountId,
|
|
552
|
+
name: account.name,
|
|
553
|
+
enabled: account.enabled,
|
|
554
|
+
configured,
|
|
555
|
+
},
|
|
556
|
+
runtime: configured
|
|
557
|
+
? runtime
|
|
558
|
+
: { ...runtime, lastError: runtime?.lastError ?? configError },
|
|
559
|
+
});
|
|
568
560
|
return {
|
|
569
|
-
|
|
570
|
-
name: account.name,
|
|
571
|
-
enabled: account.enabled,
|
|
572
|
-
configured,
|
|
573
|
-
running: runtime?.running ?? false,
|
|
574
|
-
lastStartAt: runtime?.lastStartAt ?? null,
|
|
575
|
-
lastStopAt: runtime?.lastStopAt ?? null,
|
|
576
|
-
lastError: configured ? (runtime?.lastError ?? null) : (runtime?.lastError ?? configError),
|
|
577
|
-
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
578
|
-
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
561
|
+
...base,
|
|
579
562
|
dmPolicy: account.config.dmPolicy ?? "pairing",
|
|
580
563
|
};
|
|
581
564
|
},
|
|
@@ -608,44 +591,21 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
608
591
|
},
|
|
609
592
|
loginWithQrStart: async (params) => {
|
|
610
593
|
const profile = resolveZalouserQrProfile(params.accountId);
|
|
611
|
-
|
|
612
|
-
const result = await runZca(["auth", "login", "--qr-base64"], {
|
|
594
|
+
return await startZaloQrLogin({
|
|
613
595
|
profile,
|
|
614
|
-
|
|
596
|
+
force: params.force,
|
|
597
|
+
timeoutMs: params.timeoutMs,
|
|
615
598
|
});
|
|
616
|
-
if (!result.ok) {
|
|
617
|
-
return { message: result.stderr || "Failed to start QR login" };
|
|
618
|
-
}
|
|
619
|
-
// The stdout should contain the base64 QR data URL
|
|
620
|
-
const qrMatch = result.stdout.match(/data:image\/png;base64,[A-Za-z0-9+/=]+/);
|
|
621
|
-
if (qrMatch) {
|
|
622
|
-
return { qrDataUrl: qrMatch[0], message: "Scan QR code with Zalo app" };
|
|
623
|
-
}
|
|
624
|
-
return { message: result.stdout || "QR login started" };
|
|
625
599
|
},
|
|
626
600
|
loginWithQrWait: async (params) => {
|
|
627
601
|
const profile = resolveZalouserQrProfile(params.accountId);
|
|
628
|
-
|
|
629
|
-
const statusResult = await runZca(["auth", "status"], {
|
|
602
|
+
return await waitForZaloQrLogin({
|
|
630
603
|
profile,
|
|
631
|
-
|
|
604
|
+
timeoutMs: params.timeoutMs,
|
|
632
605
|
});
|
|
633
|
-
return {
|
|
634
|
-
connected: statusResult.ok,
|
|
635
|
-
message: statusResult.ok ? "Login successful" : statusResult.stderr || "Login pending",
|
|
636
|
-
};
|
|
637
|
-
},
|
|
638
|
-
logoutAccount: async (ctx) => {
|
|
639
|
-
const result = await runZca(["auth", "logout"], {
|
|
640
|
-
profile: ctx.account.profile,
|
|
641
|
-
timeout: 10000,
|
|
642
|
-
});
|
|
643
|
-
return {
|
|
644
|
-
cleared: result.ok,
|
|
645
|
-
loggedOut: result.ok,
|
|
646
|
-
message: result.ok ? "Logged out" : result.stderr,
|
|
647
|
-
};
|
|
648
606
|
},
|
|
607
|
+
logoutAccount: async (ctx) =>
|
|
608
|
+
await logoutZaloProfile(ctx.account.profile || resolveZalouserQrProfile(ctx.accountId)),
|
|
649
609
|
},
|
|
650
610
|
};
|
|
651
611
|
|