@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/src/onboarding.ts CHANGED
@@ -3,23 +3,30 @@ import type {
3
3
  ChannelOnboardingDmPolicy,
4
4
  OpenClawConfig,
5
5
  WizardPrompter,
6
- } from "openclaw/plugin-sdk";
6
+ } from "openclaw/plugin-sdk/zalouser";
7
7
  import {
8
- addWildcardAllowFrom,
9
8
  DEFAULT_ACCOUNT_ID,
9
+ formatResolvedUnresolvedNote,
10
10
  mergeAllowFromEntries,
11
11
  normalizeAccountId,
12
- promptAccountId,
13
12
  promptChannelAccessConfig,
14
- } from "openclaw/plugin-sdk";
13
+ resolveAccountIdForConfigure,
14
+ setTopLevelChannelDmPolicyWithAllowFrom,
15
+ } from "openclaw/plugin-sdk/zalouser";
15
16
  import {
16
17
  listZalouserAccountIds,
17
18
  resolveDefaultZalouserAccountId,
18
19
  resolveZalouserAccountSync,
19
20
  checkZcaAuthenticated,
20
21
  } from "./accounts.js";
21
- import type { ZcaFriend, ZcaGroup } from "./types.js";
22
- import { runZca, runZcaInteractive, checkZcaInstalled, parseJsonOutput } from "./zca.js";
22
+ import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
23
+ import {
24
+ logoutZaloProfile,
25
+ resolveZaloAllowFromEntries,
26
+ resolveZaloGroupsByEntries,
27
+ startZaloQrLogin,
28
+ waitForZaloQrLogin,
29
+ } from "./zalo-js.js";
23
30
 
24
31
  const channel = "zalouser" as const;
25
32
 
@@ -66,19 +73,11 @@ function setZalouserDmPolicy(
66
73
  cfg: OpenClawConfig,
67
74
  dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
68
75
  ): OpenClawConfig {
69
- const allowFrom =
70
- dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.zalouser?.allowFrom) : undefined;
71
- return {
72
- ...cfg,
73
- channels: {
74
- ...cfg.channels,
75
- zalouser: {
76
- ...cfg.channels?.zalouser,
77
- dmPolicy,
78
- ...(allowFrom ? { allowFrom } : {}),
79
- },
80
- },
81
- } as OpenClawConfig;
76
+ return setTopLevelChannelDmPolicyWithAllowFrom({
77
+ cfg,
78
+ channel: "zalouser",
79
+ dmPolicy,
80
+ }) as OpenClawConfig;
82
81
  }
83
82
 
84
83
  async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
@@ -86,9 +85,7 @@ async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
86
85
  [
87
86
  "Zalo Personal Account login via QR code.",
88
87
  "",
89
- "Prerequisites:",
90
- "1) Install zca-cli",
91
- "2) You'll scan a QR code with your Zalo app",
88
+ "This plugin uses zca-js directly (no external CLI dependency).",
92
89
  "",
93
90
  "Docs: https://docs.openclaw.ai/channels/zalouser",
94
91
  ].join("\n"),
@@ -110,58 +107,40 @@ async function promptZalouserAllowFrom(params: {
110
107
  .map((entry) => entry.trim())
111
108
  .filter(Boolean);
112
109
 
113
- const resolveUserId = async (input: string): Promise<string | null> => {
114
- const trimmed = input.trim();
115
- if (!trimmed) {
116
- return null;
117
- }
118
- if (/^\d+$/.test(trimmed)) {
119
- return trimmed;
120
- }
121
- const ok = await checkZcaInstalled();
122
- if (!ok) {
123
- return null;
124
- }
125
- const result = await runZca(["friend", "find", trimmed], {
126
- profile: resolved.profile,
127
- timeout: 15000,
128
- });
129
- if (!result.ok) {
130
- return null;
131
- }
132
- const parsed = parseJsonOutput<ZcaFriend[]>(result.stdout);
133
- const rows = Array.isArray(parsed) ? parsed : [];
134
- const match = rows[0];
135
- if (!match?.userId) {
136
- return null;
137
- }
138
- if (rows.length > 1) {
139
- await prompter.note(
140
- `Multiple matches for "${trimmed}", using ${match.displayName ?? match.userId}.`,
141
- "Zalo Personal allowlist",
142
- );
143
- }
144
- return String(match.userId);
145
- };
146
-
147
110
  while (true) {
148
111
  const entry = await prompter.text({
149
- message: "Zalouser allowFrom (username or user id)",
112
+ message: "Zalouser allowFrom (name or user id)",
150
113
  placeholder: "Alice, 123456789",
151
114
  initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
152
115
  validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
153
116
  });
154
117
  const parts = parseInput(String(entry));
155
- const results = await Promise.all(parts.map((part) => resolveUserId(part)));
156
- const unresolved = parts.filter((_, idx) => !results[idx]);
118
+ const resolvedEntries = await resolveZaloAllowFromEntries({
119
+ profile: resolved.profile,
120
+ entries: parts,
121
+ });
122
+
123
+ const unresolved = resolvedEntries.filter((item) => !item.resolved).map((item) => item.input);
157
124
  if (unresolved.length > 0) {
158
125
  await prompter.note(
159
- `Could not resolve: ${unresolved.join(", ")}. Use numeric user ids or ensure zca is available.`,
126
+ `Could not resolve: ${unresolved.join(", ")}. Use numeric user ids or exact friend names.`,
160
127
  "Zalo Personal allowlist",
161
128
  );
162
129
  continue;
163
130
  }
164
- const unique = mergeAllowFromEntries(existingAllowFrom, results.filter(Boolean) as string[]);
131
+
132
+ const resolvedIds = resolvedEntries
133
+ .filter((item) => item.resolved && item.id)
134
+ .map((item) => item.id as string);
135
+ const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
136
+
137
+ const notes = resolvedEntries
138
+ .filter((item) => item.note)
139
+ .map((item) => `${item.input} -> ${item.id} (${item.note})`);
140
+ if (notes.length > 0) {
141
+ await prompter.note(notes.join("\n"), "Zalo Personal allowlist");
142
+ }
143
+
165
144
  return setZalouserAccountScopedConfig(cfg, accountId, {
166
145
  dmPolicy: "allowlist",
167
146
  allowFrom: unique,
@@ -190,49 +169,6 @@ function setZalouserGroupAllowlist(
190
169
  });
191
170
  }
192
171
 
193
- async function resolveZalouserGroups(params: {
194
- cfg: OpenClawConfig;
195
- accountId: string;
196
- entries: string[];
197
- }): Promise<Array<{ input: string; resolved: boolean; id?: string }>> {
198
- const account = resolveZalouserAccountSync({ cfg: params.cfg, accountId: params.accountId });
199
- const result = await runZca(["group", "list", "-j"], {
200
- profile: account.profile,
201
- timeout: 15000,
202
- });
203
- if (!result.ok) {
204
- throw new Error(result.stderr || "Failed to list groups");
205
- }
206
- const groups = (parseJsonOutput<ZcaGroup[]>(result.stdout) ?? []).filter((group) =>
207
- Boolean(group.groupId),
208
- );
209
- const byName = new Map<string, ZcaGroup[]>();
210
- for (const group of groups) {
211
- const name = group.name?.trim().toLowerCase();
212
- if (!name) {
213
- continue;
214
- }
215
- const list = byName.get(name) ?? [];
216
- list.push(group);
217
- byName.set(name, list);
218
- }
219
-
220
- return params.entries.map((input) => {
221
- const trimmed = input.trim();
222
- if (!trimmed) {
223
- return { input, resolved: false };
224
- }
225
- if (/^\d+$/.test(trimmed)) {
226
- return { input, resolved: true, id: trimmed };
227
- }
228
- const matches = byName.get(trimmed.toLowerCase()) ?? [];
229
- const match = matches[0];
230
- return match?.groupId
231
- ? { input, resolved: true, id: String(match.groupId) }
232
- : { input, resolved: false };
233
- });
234
- }
235
-
236
172
  const dmPolicy: ChannelOnboardingDmPolicy = {
237
173
  label: "Zalo Personal",
238
174
  channel,
@@ -246,7 +182,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
246
182
  ? (normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID)
247
183
  : resolveDefaultZalouserAccountId(cfg);
248
184
  return promptZalouserAllowFrom({
249
- cfg: cfg,
185
+ cfg,
250
186
  prompter,
251
187
  accountId: id,
252
188
  });
@@ -260,7 +196,7 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
260
196
  const ids = listZalouserAccountIds(cfg);
261
197
  let configured = false;
262
198
  for (const accountId of ids) {
263
- const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
199
+ const account = resolveZalouserAccountSync({ cfg, accountId });
264
200
  const isAuth = await checkZcaAuthenticated(account.profile);
265
201
  if (isAuth) {
266
202
  configured = true;
@@ -282,35 +218,16 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
282
218
  shouldPromptAccountIds,
283
219
  forceAllowFrom,
284
220
  }) => {
285
- // Check zca is installed
286
- const zcaInstalled = await checkZcaInstalled();
287
- if (!zcaInstalled) {
288
- await prompter.note(
289
- [
290
- "The `zca` binary was not found in PATH.",
291
- "",
292
- "Install zca-cli, then re-run onboarding:",
293
- "Docs: https://docs.openclaw.ai/channels/zalouser",
294
- ].join("\n"),
295
- "Missing Dependency",
296
- );
297
- return { cfg, accountId: DEFAULT_ACCOUNT_ID };
298
- }
299
-
300
- const zalouserOverride = accountOverrides.zalouser?.trim();
301
221
  const defaultAccountId = resolveDefaultZalouserAccountId(cfg);
302
- let accountId = zalouserOverride ? normalizeAccountId(zalouserOverride) : defaultAccountId;
303
-
304
- if (shouldPromptAccountIds && !zalouserOverride) {
305
- accountId = await promptAccountId({
306
- cfg: cfg,
307
- prompter,
308
- label: "Zalo Personal",
309
- currentId: accountId,
310
- listAccountIds: listZalouserAccountIds,
311
- defaultAccountId,
312
- });
313
- }
222
+ const accountId = await resolveAccountIdForConfigure({
223
+ cfg,
224
+ prompter,
225
+ label: "Zalo Personal",
226
+ accountOverride: accountOverrides.zalouser,
227
+ shouldPromptAccountIds,
228
+ listAccountIds: listZalouserAccountIds,
229
+ defaultAccountId,
230
+ });
314
231
 
315
232
  let next = cfg;
316
233
  const account = resolveZalouserAccountSync({ cfg: next, accountId });
@@ -325,23 +242,32 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
325
242
  });
326
243
 
327
244
  if (wantsLogin) {
328
- await prompter.note(
329
- "A QR code will appear in your terminal.\nScan it with your Zalo app to login.",
330
- "QR Login",
331
- );
332
-
333
- // Run interactive login
334
- const result = await runZcaInteractive(["auth", "login"], {
335
- profile: account.profile,
336
- });
337
-
338
- if (!result.ok) {
339
- await prompter.note(`Login failed: ${result.stderr || "Unknown error"}`, "Error");
340
- } else {
341
- const isNowAuth = await checkZcaAuthenticated(account.profile);
342
- if (isNowAuth) {
343
- await prompter.note("Login successful!", "Success");
245
+ const start = await startZaloQrLogin({ profile: account.profile, timeoutMs: 35_000 });
246
+ if (start.qrDataUrl) {
247
+ const qrPath = await writeQrDataUrlToTempFile(start.qrDataUrl, account.profile);
248
+ await prompter.note(
249
+ [
250
+ start.message,
251
+ qrPath
252
+ ? `QR image saved to: ${qrPath}`
253
+ : "Could not write QR image file; use gateway web login UI instead.",
254
+ "Scan + approve on phone, then continue.",
255
+ ].join("\n"),
256
+ "QR Login",
257
+ );
258
+ const scanned = await prompter.confirm({
259
+ message: "Did you scan and approve the QR on your phone?",
260
+ initialValue: true,
261
+ });
262
+ if (scanned) {
263
+ const waited = await waitForZaloQrLogin({
264
+ profile: account.profile,
265
+ timeoutMs: 120_000,
266
+ });
267
+ await prompter.note(waited.message, waited.connected ? "Success" : "Login pending");
344
268
  }
269
+ } else {
270
+ await prompter.note(start.message, "Login pending");
345
271
  }
346
272
  }
347
273
  } else {
@@ -350,12 +276,26 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
350
276
  initialValue: true,
351
277
  });
352
278
  if (!keepSession) {
353
- await runZcaInteractive(["auth", "logout"], { profile: account.profile });
354
- await runZcaInteractive(["auth", "login"], { profile: account.profile });
279
+ await logoutZaloProfile(account.profile);
280
+ const start = await startZaloQrLogin({
281
+ profile: account.profile,
282
+ force: true,
283
+ timeoutMs: 35_000,
284
+ });
285
+ if (start.qrDataUrl) {
286
+ const qrPath = await writeQrDataUrlToTempFile(start.qrDataUrl, account.profile);
287
+ await prompter.note(
288
+ [start.message, qrPath ? `QR image saved to: ${qrPath}` : undefined]
289
+ .filter(Boolean)
290
+ .join("\n"),
291
+ "QR Login",
292
+ );
293
+ const waited = await waitForZaloQrLogin({ profile: account.profile, timeoutMs: 120_000 });
294
+ await prompter.note(waited.message, waited.connected ? "Success" : "Login pending");
295
+ }
355
296
  }
356
297
  }
357
298
 
358
- // Enable the channel
359
299
  next = setZalouserAccountScopedConfig(
360
300
  next,
361
301
  accountId,
@@ -371,14 +311,16 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
371
311
  });
372
312
  }
373
313
 
314
+ const updatedAccount = resolveZalouserAccountSync({ cfg: next, accountId });
374
315
  const accessConfig = await promptChannelAccessConfig({
375
316
  prompter,
376
317
  label: "Zalo groups",
377
- currentPolicy: account.config.groupPolicy ?? "allowlist",
378
- currentEntries: Object.keys(account.config.groups ?? {}),
318
+ currentPolicy: updatedAccount.config.groupPolicy ?? "allowlist",
319
+ currentEntries: Object.keys(updatedAccount.config.groups ?? {}),
379
320
  placeholder: "Family, Work, 123456789",
380
- updatePrompt: Boolean(account.config.groups),
321
+ updatePrompt: Boolean(updatedAccount.config.groups),
381
322
  });
323
+
382
324
  if (accessConfig) {
383
325
  if (accessConfig.policy !== "allowlist") {
384
326
  next = setZalouserGroupPolicy(next, accountId, accessConfig.policy);
@@ -386,9 +328,8 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
386
328
  let keys = accessConfig.entries;
387
329
  if (accessConfig.entries.length > 0) {
388
330
  try {
389
- const resolved = await resolveZalouserGroups({
390
- cfg: next,
391
- accountId,
331
+ const resolved = await resolveZaloGroupsByEntries({
332
+ profile: updatedAccount.profile,
392
333
  entries: accessConfig.entries,
393
334
  });
394
335
  const resolvedIds = resolved
@@ -398,18 +339,12 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
398
339
  .filter((entry) => !entry.resolved)
399
340
  .map((entry) => entry.input);
400
341
  keys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
401
- if (resolvedIds.length > 0 || unresolved.length > 0) {
402
- await prompter.note(
403
- [
404
- resolvedIds.length > 0 ? `Resolved: ${resolvedIds.join(", ")}` : undefined,
405
- unresolved.length > 0
406
- ? `Unresolved (kept as typed): ${unresolved.join(", ")}`
407
- : undefined,
408
- ]
409
- .filter(Boolean)
410
- .join("\n"),
411
- "Zalo groups",
412
- );
342
+ const resolution = formatResolvedUnresolvedNote({
343
+ resolved: resolvedIds,
344
+ unresolved,
345
+ });
346
+ if (resolution) {
347
+ await prompter.note(resolution, "Zalo groups");
413
348
  }
414
349
  } catch (err) {
415
350
  await prompter.note(
@@ -0,0 +1,60 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { probeZalouser } from "./probe.js";
3
+ import { getZaloUserInfo } from "./zalo-js.js";
4
+
5
+ vi.mock("./zalo-js.js", () => ({
6
+ getZaloUserInfo: vi.fn(),
7
+ }));
8
+
9
+ const mockGetUserInfo = vi.mocked(getZaloUserInfo);
10
+
11
+ describe("probeZalouser", () => {
12
+ beforeEach(() => {
13
+ mockGetUserInfo.mockReset();
14
+ });
15
+
16
+ afterEach(() => {
17
+ vi.useRealTimers();
18
+ });
19
+
20
+ it("returns ok=true with user when authenticated", async () => {
21
+ mockGetUserInfo.mockResolvedValueOnce({
22
+ userId: "123",
23
+ displayName: "Alice",
24
+ });
25
+
26
+ await expect(probeZalouser("default")).resolves.toEqual({
27
+ ok: true,
28
+ user: { userId: "123", displayName: "Alice" },
29
+ });
30
+ });
31
+
32
+ it("returns not authenticated when no user info is returned", async () => {
33
+ mockGetUserInfo.mockResolvedValueOnce(null);
34
+ await expect(probeZalouser("default")).resolves.toEqual({
35
+ ok: false,
36
+ error: "Not authenticated",
37
+ });
38
+ });
39
+
40
+ it("returns error when user lookup throws", async () => {
41
+ mockGetUserInfo.mockRejectedValueOnce(new Error("network down"));
42
+ await expect(probeZalouser("default")).resolves.toEqual({
43
+ ok: false,
44
+ error: "network down",
45
+ });
46
+ });
47
+
48
+ it("times out when lookup takes too long", async () => {
49
+ vi.useFakeTimers();
50
+ mockGetUserInfo.mockReturnValueOnce(new Promise(() => undefined));
51
+
52
+ const pending = probeZalouser("default", 10);
53
+ await vi.advanceTimersByTimeAsync(1000);
54
+
55
+ await expect(pending).resolves.toEqual({
56
+ ok: false,
57
+ error: "Not authenticated",
58
+ });
59
+ });
60
+ });
package/src/probe.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { BaseProbeResult } from "openclaw/plugin-sdk";
1
+ import type { BaseProbeResult } from "openclaw/plugin-sdk/zalouser";
2
2
  import type { ZcaUserInfo } from "./types.js";
3
- import { runZca, parseJsonOutput } from "./zca.js";
3
+ import { getZaloUserInfo } from "./zalo-js.js";
4
4
 
5
5
  export type ZalouserProbeResult = BaseProbeResult<string> & {
6
6
  user?: ZcaUserInfo;
@@ -10,18 +10,25 @@ export async function probeZalouser(
10
10
  profile: string,
11
11
  timeoutMs?: number,
12
12
  ): Promise<ZalouserProbeResult> {
13
- const result = await runZca(["me", "info", "-j"], {
14
- profile,
15
- timeout: timeoutMs,
16
- });
13
+ try {
14
+ const user = timeoutMs
15
+ ? await Promise.race([
16
+ getZaloUserInfo(profile),
17
+ new Promise<null>((resolve) =>
18
+ setTimeout(() => resolve(null), Math.max(timeoutMs, 1000)),
19
+ ),
20
+ ])
21
+ : await getZaloUserInfo(profile);
17
22
 
18
- if (!result.ok) {
19
- return { ok: false, error: result.stderr || "Failed to probe" };
20
- }
23
+ if (!user) {
24
+ return { ok: false, error: "Not authenticated" };
25
+ }
21
26
 
22
- const user = parseJsonOutput<ZcaUserInfo>(result.stdout);
23
- if (!user) {
24
- return { ok: false, error: "Failed to parse user info" };
27
+ return { ok: true, user };
28
+ } catch (error) {
29
+ return {
30
+ ok: false,
31
+ error: error instanceof Error ? error.message : String(error),
32
+ };
25
33
  }
26
- return { ok: true, user };
27
34
  }
@@ -0,0 +1,22 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/zalouser";
4
+
5
+ export async function writeQrDataUrlToTempFile(
6
+ qrDataUrl: string,
7
+ profile: string,
8
+ ): Promise<string | null> {
9
+ const trimmed = qrDataUrl.trim();
10
+ const match = trimmed.match(/^data:image\/png;base64,(.+)$/i);
11
+ const base64 = (match?.[1] ?? "").trim();
12
+ if (!base64) {
13
+ return null;
14
+ }
15
+ const safeProfile = profile.replace(/[^a-zA-Z0-9_-]+/g, "-") || "default";
16
+ const filePath = path.join(
17
+ resolvePreferredOpenClawTmpDir(),
18
+ `openclaw-zalouser-qr-${safeProfile}.png`,
19
+ );
20
+ await fsp.writeFile(filePath, Buffer.from(base64, "base64"));
21
+ return filePath;
22
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { normalizeZaloReactionIcon } from "./reaction.js";
3
+
4
+ describe("zalouser reaction alias normalization", () => {
5
+ it("maps common aliases", () => {
6
+ expect(normalizeZaloReactionIcon("like")).toBe("/-strong");
7
+ expect(normalizeZaloReactionIcon("👍")).toBe("/-strong");
8
+ expect(normalizeZaloReactionIcon("heart")).toBe("/-heart");
9
+ expect(normalizeZaloReactionIcon("😂")).toBe(":>");
10
+ });
11
+
12
+ it("defaults empty icon to like", () => {
13
+ expect(normalizeZaloReactionIcon("")).toBe("/-strong");
14
+ });
15
+
16
+ it("passes through unknown custom reactions", () => {
17
+ expect(normalizeZaloReactionIcon("/custom")).toBe("/custom");
18
+ });
19
+ });
@@ -0,0 +1,29 @@
1
+ import { Reactions } from "./zca-client.js";
2
+
3
+ const REACTION_ALIAS_MAP = new Map<string, string>([
4
+ ["like", Reactions.LIKE],
5
+ ["👍", Reactions.LIKE],
6
+ [":+1:", Reactions.LIKE],
7
+ ["heart", Reactions.HEART],
8
+ ["❤️", Reactions.HEART],
9
+ ["<3", Reactions.HEART],
10
+ ["haha", Reactions.HAHA],
11
+ ["laugh", Reactions.HAHA],
12
+ ["😂", Reactions.HAHA],
13
+ ["wow", Reactions.WOW],
14
+ ["😮", Reactions.WOW],
15
+ ["cry", Reactions.CRY],
16
+ ["😢", Reactions.CRY],
17
+ ["angry", Reactions.ANGRY],
18
+ ["😡", Reactions.ANGRY],
19
+ ]);
20
+
21
+ export function normalizeZaloReactionIcon(raw: string): string {
22
+ const trimmed = raw.trim();
23
+ if (!trimmed) {
24
+ return Reactions.LIKE;
25
+ }
26
+ return (
27
+ REACTION_ALIAS_MAP.get(trimmed.toLowerCase()) ?? REACTION_ALIAS_MAP.get(trimmed) ?? trimmed
28
+ );
29
+ }
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk/zalouser";
2
2
 
3
3
  let runtime: PluginRuntime | null = null;
4
4