@openclaw/zalouser 2026.5.7 → 2026.5.10-beta.1

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.
@@ -3,7 +3,7 @@ import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
3
3
  //#region extensions/zalouser/src/accounts.ts
4
4
  let zalouserAccountsRuntimePromise;
5
5
  async function loadZalouserAccountsRuntime() {
6
- zalouserAccountsRuntimePromise ??= import("./accounts.runtime-uG7S8cXT.js");
6
+ zalouserAccountsRuntimePromise ??= import("./accounts.runtime-Bls9vlnf.js");
7
7
  return await zalouserAccountsRuntimePromise;
8
8
  }
9
9
  const { listAccountIds: listZalouserAccountIds, resolveDefaultAccountId: resolveDefaultZalouserAccountId } = createAccountListHelpers("zalouser");
@@ -1,2 +1,2 @@
1
- import { n as getZaloUserInfo, t as checkZaloAuthenticated } from "./zalo-js-CHCUlY3c.js";
1
+ import { n as getZaloUserInfo, t as checkZaloAuthenticated } from "./zalo-js-QGi0H5K7.js";
2
2
  export { checkZaloAuthenticated, getZaloUserInfo };
@@ -1,11 +1,12 @@
1
- import "./setup-surface-NCOuKu-l.js";
1
+ import "./setup-surface-6MmoBuUf.js";
2
2
  import "./setup-core-CqipqY98.js";
3
3
  import { r as parseZalouserOutboundTarget } from "./session-route-C0-Xr8bt.js";
4
- import "./channel-DLNmGWb8.js";
4
+ import "./channel-BaznOdZe.js";
5
5
  import "./security-audit-BZLhil-V.js";
6
- import { i as listZaloFriendsMatching, n as getZaloUserInfo, s as listZaloGroupsMatching, t as checkZaloAuthenticated } from "./zalo-js-CHCUlY3c.js";
7
- import "./channel.setup-CiDeBFrn.js";
8
- import { i as sendMessageZalouser, n as sendImageZalouser, r as sendLinkZalouser } from "./send-BsmySxe3.js";
6
+ import { i as listZaloFriendsMatching, n as getZaloUserInfo, s as listZaloGroupsMatching, t as checkZaloAuthenticated } from "./zalo-js-QGi0H5K7.js";
7
+ import "./channel.setup-Tevqgs6C.js";
8
+ import { i as sendMessageZalouser, n as sendImageZalouser, r as sendLinkZalouser } from "./send-CeCQ8UZF.js";
9
+ import { stringEnum } from "openclaw/plugin-sdk/channel-actions";
9
10
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
10
11
  import { Type } from "typebox";
11
12
  //#region extensions/zalouser/src/tool.ts
@@ -18,13 +19,6 @@ const ACTIONS = [
18
19
  "me",
19
20
  "status"
20
21
  ];
21
- function stringEnum(values, options = {}) {
22
- return Type.Unsafe({
23
- type: "string",
24
- enum: [...values],
25
- ...options
26
- });
27
- }
28
22
  const ZalouserToolSchema = Type.Object({
29
23
  action: stringEnum(ACTIONS, { description: `Action to perform: ${ACTIONS.join(", ")}` }),
30
24
  threadId: Type.Optional(Type.String({ description: "Thread ID for messaging" })),
package/dist/api.js CHANGED
@@ -1,7 +1,7 @@
1
- import { n as zalouserSetupWizard } from "./setup-surface-NCOuKu-l.js";
1
+ import { n as zalouserSetupWizard } from "./setup-surface-6MmoBuUf.js";
2
2
  import { n as zalouserSetupAdapter, t as createZalouserSetupWizardProxy } from "./setup-core-CqipqY98.js";
3
- import { t as zalouserPlugin } from "./channel-DLNmGWb8.js";
3
+ import { t as zalouserPlugin } from "./channel-BaznOdZe.js";
4
4
  import { n as isZalouserMutableGroupEntry, t as collectZalouserSecurityAuditFindings } from "./security-audit-BZLhil-V.js";
5
- import { t as zalouserSetupPlugin } from "./channel.setup-CiDeBFrn.js";
6
- import { t as createZalouserTool } from "./api-C3SYq_R3.js";
5
+ import { t as zalouserSetupPlugin } from "./channel.setup-Tevqgs6C.js";
6
+ import { t as createZalouserTool } from "./api-Dl_YURKB.js";
7
7
  export { collectZalouserSecurityAuditFindings, createZalouserSetupWizardProxy, createZalouserTool, isZalouserMutableGroupEntry, zalouserPlugin, zalouserSetupAdapter, zalouserSetupPlugin, zalouserSetupWizard };
@@ -1,5 +1,5 @@
1
- import { a as resolveZalouserAccountSync, i as resolveDefaultZalouserAccountId, r as listZalouserAccountIds, t as checkZcaAuthenticated } from "./accounts-C00IMUgd.js";
2
- import { a as isNumericTargetId, i as isDangerousNameMatchingEnabled, n as DEFAULT_ACCOUNT_ID, o as normalizeAccountId, r as chunkTextForOutbound, s as sendPayloadWithChunkedTextAndMedia, t as createZalouserPluginBase } from "./shared-DSy8aIUx.js";
1
+ import { a as resolveZalouserAccountSync, i as resolveDefaultZalouserAccountId, r as listZalouserAccountIds, t as checkZcaAuthenticated } from "./accounts-ClSPNIvj.js";
2
+ import { a as isNumericTargetId, i as isDangerousNameMatchingEnabled, n as DEFAULT_ACCOUNT_ID, o as normalizeAccountId, r as chunkTextForOutbound, s as sendPayloadWithChunkedTextAndMedia, t as createZalouserPluginBase } from "./shared-C4hIhcfY.js";
3
3
  import { a as resolveZalouserReactionMessageIds, o as buildZalouserGroupCandidates, s as findZalouserGroupEntry, t as getZalouserRuntime } from "./runtime-QNU7vLgI.js";
4
4
  import { n as zalouserSetupAdapter, r as writeQrDataUrlToTempFile, t as createZalouserSetupWizardProxy } from "./setup-core-CqipqY98.js";
5
5
  import { i as resolveZalouserOutboundSessionRoute, n as parseZalouserDirectoryGroupId, r as parseZalouserOutboundTarget, t as normalizeZalouserTarget } from "./session-route-C0-Xr8bt.js";
@@ -10,11 +10,12 @@ import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
10
10
  import { createAsyncComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
11
11
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
12
12
  import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
13
+ import { defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
13
14
  import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
14
15
  import { createEmptyChannelResult, createRawChannelSendResultAdapter } from "openclaw/plugin-sdk/channel-send-result";
15
16
  import { createStaticReplyToModeResolver } from "openclaw/plugin-sdk/conversation-runtime";
16
17
  //#region extensions/zalouser/src/channel.adapters.ts
17
- const loadZalouserChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel.runtime-C9WxiAiR.js"));
18
+ const loadZalouserChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel.runtime-85pFFq1m.js"));
18
19
  const ZALOUSER_TEXT_CHUNK_LIMIT = 2e3;
19
20
  function resolveZalouserQrProfile(accountId) {
20
21
  const normalized = normalizeAccountId(accountId);
@@ -47,40 +48,54 @@ function resolveZalouserRequireMention(params) {
47
48
  if (typeof entry?.requireMention === "boolean") return entry.requireMention;
48
49
  return true;
49
50
  }
51
+ async function sendZalouserTextFromContext({ to, text, accountId, cfg }) {
52
+ const { sendMessageZalouser } = await loadZalouserChannelRuntime$1();
53
+ const account = resolveZalouserAccountSync({
54
+ cfg,
55
+ accountId
56
+ });
57
+ const target = parseZalouserOutboundTarget(to);
58
+ return await sendMessageZalouser(target.threadId, text, {
59
+ profile: account.profile,
60
+ isGroup: target.isGroup,
61
+ textMode: "markdown",
62
+ textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId),
63
+ textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId)
64
+ });
65
+ }
66
+ async function sendZalouserMediaFromContext({ to, text, mediaUrl, accountId, cfg, mediaLocalRoots, mediaReadFile }) {
67
+ const { sendMessageZalouser } = await loadZalouserChannelRuntime$1();
68
+ const account = resolveZalouserAccountSync({
69
+ cfg,
70
+ accountId
71
+ });
72
+ const target = parseZalouserOutboundTarget(to);
73
+ return await sendMessageZalouser(target.threadId, text, {
74
+ profile: account.profile,
75
+ isGroup: target.isGroup,
76
+ mediaUrl,
77
+ mediaLocalRoots,
78
+ mediaReadFile,
79
+ textMode: "markdown",
80
+ textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId),
81
+ textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId)
82
+ });
83
+ }
50
84
  const zalouserRawSendResultAdapter = createRawChannelSendResultAdapter({
51
85
  channel: "zalouser",
52
- sendText: async ({ to, text, accountId, cfg }) => {
53
- const { sendMessageZalouser } = await loadZalouserChannelRuntime$1();
54
- const account = resolveZalouserAccountSync({
55
- cfg,
56
- accountId
57
- });
58
- const target = parseZalouserOutboundTarget(to);
59
- return await sendMessageZalouser(target.threadId, text, {
60
- profile: account.profile,
61
- isGroup: target.isGroup,
62
- textMode: "markdown",
63
- textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId),
64
- textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId)
65
- });
66
- },
67
- sendMedia: async ({ to, text, mediaUrl, accountId, cfg, mediaLocalRoots, mediaReadFile }) => {
68
- const { sendMessageZalouser } = await loadZalouserChannelRuntime$1();
69
- const account = resolveZalouserAccountSync({
70
- cfg,
71
- accountId
72
- });
73
- const target = parseZalouserOutboundTarget(to);
74
- return await sendMessageZalouser(target.threadId, text, {
75
- profile: account.profile,
76
- isGroup: target.isGroup,
77
- mediaUrl,
78
- mediaLocalRoots,
79
- mediaReadFile,
80
- textMode: "markdown",
81
- textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId),
82
- textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId)
83
- });
86
+ sendText: sendZalouserTextFromContext,
87
+ sendMedia: sendZalouserMediaFromContext
88
+ });
89
+ const zalouserMessageAdapter = defineChannelMessageAdapter({
90
+ id: "zalouser",
91
+ durableFinal: { capabilities: {
92
+ text: true,
93
+ media: true,
94
+ messageSendingHooks: true
95
+ } },
96
+ send: {
97
+ text: sendZalouserTextFromContext,
98
+ media: sendZalouserMediaFromContext
84
99
  }
85
100
  });
86
101
  const resolveZalouserDmPolicy = createScopedDmSecurityResolver({
@@ -331,8 +346,8 @@ function collectZalouserStatusIssues(accounts) {
331
346
  }
332
347
  //#endregion
333
348
  //#region extensions/zalouser/src/channel.ts
334
- const loadZalouserChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-C9WxiAiR.js"));
335
- const zalouserSetupWizardProxy = createZalouserSetupWizardProxy(async () => (await import("./setup-surface-NCOuKu-l.js").then((n) => n.t)).zalouserSetupWizard);
349
+ const loadZalouserChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-85pFFq1m.js"));
350
+ const zalouserSetupWizardProxy = createZalouserSetupWizardProxy(async () => (await import("./setup-surface-6MmoBuUf.js").then((n) => n.t)).zalouserSetupWizard);
336
351
  function mapUser(params) {
337
352
  return {
338
353
  kind: "user",
@@ -411,6 +426,7 @@ const zalouserPlugin = createChatChannelPlugin({
411
426
  },
412
427
  resolver: zalouserResolverAdapter,
413
428
  auth: zalouserAuthAdapter,
429
+ message: zalouserMessageAdapter,
414
430
  status: createAsyncComputedAccountStatusAdapter({
415
431
  defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
416
432
  collectStatusIssues: collectZalouserStatusIssues,
@@ -448,7 +464,7 @@ const zalouserPlugin = createChatChannelPlugin({
448
464
  setStatus: ctx.setStatus
449
465
  });
450
466
  ctx.log?.info(`[${account.accountId}] starting zalouser provider${userLabel}`);
451
- const { monitorZalouserProvider } = await import("./monitor-dpWp8FkN.js");
467
+ const { monitorZalouserProvider } = await import("./monitor-BfclyB4b.js");
452
468
  return monitorZalouserProvider({
453
469
  account,
454
470
  config: ctx.cfg,
@@ -1,2 +1,2 @@
1
- import { t as zalouserPlugin } from "./channel-DLNmGWb8.js";
1
+ import { t as zalouserPlugin } from "./channel-BaznOdZe.js";
2
2
  export { zalouserPlugin };
@@ -1,6 +1,6 @@
1
1
  import { t as collectZalouserSecurityAuditFindings } from "./security-audit-BZLhil-V.js";
2
- import { a as listZaloGroupMembers, b as waitForZaloQrLogin, c as logoutZaloProfile, i as listZaloFriendsMatching, n as getZaloUserInfo, s as listZaloGroupsMatching, y as startZaloQrLogin } from "./zalo-js-CHCUlY3c.js";
3
- import { a as sendReactionZalouser, i as sendMessageZalouser } from "./send-BsmySxe3.js";
2
+ import { a as listZaloGroupMembers, b as waitForZaloQrLogin, c as logoutZaloProfile, i as listZaloFriendsMatching, n as getZaloUserInfo, s as listZaloGroupsMatching, y as startZaloQrLogin } from "./zalo-js-QGi0H5K7.js";
3
+ import { a as sendReactionZalouser, i as sendMessageZalouser } from "./send-CeCQ8UZF.js";
4
4
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
5
  //#region extensions/zalouser/src/probe.ts
6
6
  async function probeZalouser(profile, timeoutMs) {
@@ -1,5 +1,5 @@
1
- import { n as zalouserSetupWizard } from "./setup-surface-NCOuKu-l.js";
2
- import { t as createZalouserPluginBase } from "./shared-DSy8aIUx.js";
1
+ import { n as zalouserSetupWizard } from "./setup-surface-6MmoBuUf.js";
2
+ import { t as createZalouserPluginBase } from "./shared-C4hIhcfY.js";
3
3
  import { n as zalouserSetupAdapter } from "./setup-core-CqipqY98.js";
4
4
  //#region extensions/zalouser/src/channel.setup.ts
5
5
  const zalouserSetupPlugin = { ...createZalouserPluginBase({
@@ -1,25 +1,19 @@
1
1
  import { c as isZalouserGroupEntryAllowed, i as resolveZalouserMessageSid, o as buildZalouserGroupCandidates, r as formatZalouserMessageSidFull, s as findZalouserGroupEntry, t as getZalouserRuntime } from "./runtime-QNU7vLgI.js";
2
- import { o as listZaloGroups, r as listZaloFriends, u as resolveZaloGroupContext, v as startZaloListener } from "./zalo-js-CHCUlY3c.js";
3
- import { i as sendMessageZalouser, o as sendSeenZalouser, s as sendTypingZalouser, t as sendDeliveredZalouser } from "./send-BsmySxe3.js";
2
+ import { o as listZaloGroups, r as listZaloFriends, u as resolveZaloGroupContext, v as startZaloListener } from "./zalo-js-QGi0H5K7.js";
3
+ import { i as sendMessageZalouser, o as sendSeenZalouser, s as sendTypingZalouser, t as sendDeliveredZalouser } from "./send-CeCQ8UZF.js";
4
4
  import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
5
- import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
5
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeStringEntries } from "openclaw/plugin-sdk/text-runtime";
6
6
  import { mergeAllowlist, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
7
7
  import { KeyedAsyncQueue } from "openclaw/plugin-sdk/core";
8
8
  import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
9
9
  import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
10
10
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
11
- import { DM_GROUP_ACCESS_REASON, resolveDmGroupAccessWithLists } from "openclaw/plugin-sdk/channel-policy";
12
11
  import { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
13
12
  import { implicitMentionKindWhen, resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
14
- import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
15
- import { resolveSenderCommandAuthorization } from "openclaw/plugin-sdk/command-auth";
16
- import { evaluateGroupRouteAccessForPolicy, resolveSenderScopedGroupPolicy } from "openclaw/plugin-sdk/group-access";
13
+ import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
17
14
  import { DEFAULT_GROUP_HISTORY_LIMIT, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
18
15
  //#region extensions/zalouser/src/monitor.ts
19
16
  const ZALOUSER_TEXT_LIMIT = 2e3;
20
- function normalizeZalouserEntry(entry) {
21
- return entry.replace(/^(zalouser|zlu):/i, "").trim();
22
- }
23
17
  function buildNameIndex(items, nameFn) {
24
18
  const index = /* @__PURE__ */ new Map();
25
19
  for (const item of items) {
@@ -52,6 +46,12 @@ function resolveUserAllowlistEntries(entries, byName) {
52
46
  unresolved
53
47
  };
54
48
  }
49
+ function normalizeZalouserAllowEntry(entry) {
50
+ return entry.replace(/^(zalouser|zlu):/i, "").trim();
51
+ }
52
+ function normalizeZalouserSender(value) {
53
+ return normalizeOptionalLowercaseString(normalizeZalouserAllowEntry(value)) || null;
54
+ }
55
55
  function resolveInboundQueueKey(message) {
56
56
  const threadId = message.threadId?.trim() || "unknown";
57
57
  if (message.isGroup) return `group:${threadId}`;
@@ -61,6 +61,29 @@ function resolveZalouserDmSessionScope(config) {
61
61
  const configured = config.session?.dmScope;
62
62
  return configured === "main" || !configured ? "per-channel-peer" : configured;
63
63
  }
64
+ function resolveZalouserRouteAccess(params) {
65
+ if (params.groupPolicy === "disabled") return {
66
+ allowed: false,
67
+ reason: "disabled"
68
+ };
69
+ if (params.matched && params.enabled === false) return {
70
+ allowed: false,
71
+ reason: "route_disabled"
72
+ };
73
+ if (params.groupPolicy !== "allowlist") return { allowed: true };
74
+ if (!params.configured) return {
75
+ allowed: false,
76
+ reason: "empty_allowlist"
77
+ };
78
+ return params.matched ? { allowed: true } : {
79
+ allowed: false,
80
+ reason: "route_not_allowlisted"
81
+ };
82
+ }
83
+ function senderScopedZalouserGroupPolicy(params) {
84
+ if (params.groupPolicy === "disabled") return "disabled";
85
+ return params.groupAllowFrom.length > 0 ? "allowlist" : "open";
86
+ }
64
87
  function resolveZalouserInboundSessionKey(params) {
65
88
  if (params.isGroup) return params.route.sessionKey;
66
89
  const directSessionKey = normalizeLowercaseStringOrEmpty(params.core.channel.routing.buildAgentSessionKey({
@@ -95,14 +118,6 @@ function resolveZalouserInboundSessionKey(params) {
95
118
  function logVerbose(core, runtime, message) {
96
119
  if (core.logging.shouldLogVerbose()) runtime.log(`[zalouser] ${message}`);
97
120
  }
98
- function isSenderAllowed(senderId, allowFrom) {
99
- if (allowFrom.includes("*")) return true;
100
- const normalizedSenderId = normalizeOptionalLowercaseString(senderId);
101
- if (!normalizedSenderId) return false;
102
- return allowFrom.some((entry) => {
103
- return normalizeLowercaseStringOrEmpty(entry).replace(/^(zalouser|zlu):/i, "") === normalizedSenderId;
104
- });
105
- }
106
121
  function resolveGroupRequireMention(params) {
107
122
  const entry = findZalouserGroupEntry(params.groups ?? {}, buildZalouserGroupCandidates({
108
123
  groupId: params.groupId,
@@ -183,11 +198,11 @@ async function processMessage(message, account, config, core, runtime, historySt
183
198
  includeWildcard: true,
184
199
  allowNameMatching
185
200
  }));
186
- const routeAccess = evaluateGroupRouteAccessForPolicy({
201
+ const routeAccess = resolveZalouserRouteAccess({
187
202
  groupPolicy,
188
- routeAllowlistConfigured,
189
- routeMatched: Boolean(groupEntry),
190
- routeEnabled: isZalouserGroupEntryAllowed(groupEntry)
203
+ configured: routeAllowlistConfigured,
204
+ matched: Boolean(groupEntry),
205
+ enabled: isZalouserGroupEntryAllowed(groupEntry)
191
206
  });
192
207
  if (!routeAccess.allowed) {
193
208
  if (routeAccess.reason === "disabled") logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
@@ -198,30 +213,45 @@ async function processMessage(message, account, config, core, runtime, historySt
198
213
  }
199
214
  }
200
215
  const dmPolicy = account.config.dmPolicy ?? "pairing";
201
- const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
202
- const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((v) => String(v));
203
- const senderGroupPolicy = routeAllowlistConfigured && configGroupAllowFrom.length === 0 ? groupPolicy : resolveSenderScopedGroupPolicy({
216
+ const configAllowFrom = normalizeStringEntries(account.config.allowFrom);
217
+ const configGroupAllowFrom = normalizeStringEntries(account.config.groupAllowFrom);
218
+ const senderGroupPolicy = routeAllowlistConfigured && configGroupAllowFrom.length === 0 ? groupPolicy : senderScopedZalouserGroupPolicy({
204
219
  groupPolicy,
205
220
  groupAllowFrom: configGroupAllowFrom
206
221
  });
207
- const storeAllowFrom = !isGroup && dmPolicy !== "allowlist" && dmPolicy !== "open" ? await pairing.readAllowFromStore().catch(() => []) : [];
208
- const accessDecision = resolveDmGroupAccessWithLists({
209
- isGroup,
222
+ const shouldComputeCommandAuth = core.channel.commands.shouldComputeCommandAuthorized(commandBody, config);
223
+ const accessDecision = await resolveStableChannelMessageIngress({
224
+ channelId: "zalouser",
225
+ accountId: account.accountId,
226
+ identity: {
227
+ normalize: normalizeZalouserSender,
228
+ sensitivity: "pii",
229
+ entryIdPrefix: "zalouser-entry"
230
+ },
231
+ cfg: config,
232
+ readStoreAllowFrom: async () => await pairing.readAllowFromStore(),
233
+ subject: { stableId: senderId },
234
+ conversation: {
235
+ kind: isGroup ? "group" : "direct",
236
+ id: isGroup ? "group" : senderId
237
+ },
210
238
  dmPolicy,
211
239
  groupPolicy: senderGroupPolicy,
240
+ policy: { groupAllowFromFallbackToAllowFrom: false },
212
241
  allowFrom: configAllowFrom,
213
242
  groupAllowFrom: configGroupAllowFrom,
214
- storeAllowFrom,
215
- groupAllowFromFallbackToAllowFrom: false,
216
- isSenderAllowed: (allowFrom) => isSenderAllowed(senderId, allowFrom)
243
+ command: shouldComputeCommandAuth ? {
244
+ directGroupAllowFrom: "effective",
245
+ commandGroupAllowFromFallbackToAllowFrom: true
246
+ } : void 0
217
247
  });
218
- if (isGroup && accessDecision.decision !== "allow") {
219
- if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST) logVerbose(core, runtime, "Blocked zalouser group message (no group allowlist)");
220
- else if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED) logVerbose(core, runtime, `Blocked zalouser sender ${senderId} (not in groupAllowFrom/allowFrom)`);
248
+ if (isGroup && accessDecision.senderAccess.decision !== "allow") {
249
+ if (accessDecision.senderAccess.reasonCode === "group_policy_empty_allowlist") logVerbose(core, runtime, "Blocked zalouser group message (no group allowlist)");
250
+ else if (accessDecision.senderAccess.reasonCode === "group_policy_not_allowlisted") logVerbose(core, runtime, `Blocked zalouser sender ${senderId} (not in groupAllowFrom/allowFrom)`);
221
251
  return;
222
252
  }
223
- if (!isGroup && accessDecision.decision !== "allow") {
224
- if (accessDecision.decision === "pairing") {
253
+ if (!isGroup && accessDecision.senderAccess.decision !== "allow") {
254
+ if (accessDecision.senderAccess.decision === "pairing") {
225
255
  await pairing.issueChallenge({
226
256
  senderId,
227
257
  senderIdLine: `Your Zalo user id: ${senderId}`,
@@ -239,25 +269,11 @@ async function processMessage(message, account, config, core, runtime, historySt
239
269
  });
240
270
  return;
241
271
  }
242
- if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.DM_POLICY_DISABLED) logVerbose(core, runtime, `Blocked zalouser DM from ${senderId} (dmPolicy=disabled)`);
272
+ if (accessDecision.senderAccess.reasonCode === "dm_policy_disabled") logVerbose(core, runtime, `Blocked zalouser DM from ${senderId} (dmPolicy=disabled)`);
243
273
  else logVerbose(core, runtime, `Blocked unauthorized zalouser sender ${senderId} (dmPolicy=${dmPolicy})`);
244
274
  return;
245
275
  }
246
- const { commandAuthorized } = await resolveSenderCommandAuthorization({
247
- cfg: config,
248
- rawBody: commandBody,
249
- isGroup,
250
- dmPolicy,
251
- configuredAllowFrom: configAllowFrom,
252
- configuredGroupAllowFrom: configGroupAllowFrom,
253
- senderId,
254
- isSenderAllowed,
255
- channel: "zalouser",
256
- accountId: account.accountId,
257
- readAllowFromStore: async () => storeAllowFrom,
258
- shouldComputeCommandAuthorized: (body, cfg) => core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
259
- resolveCommandAuthorizedFromAuthorizers: (params) => core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params)
260
- });
276
+ const commandAuthorized = accessDecision.commandAccess.requested ? accessDecision.commandAccess.authorized : void 0;
261
277
  const hasControlCommand = core.channel.commands.isControlCommandMessage(commandBody, config);
262
278
  if (isGroup && hasControlCommand && commandAuthorized !== true) {
263
279
  logVerbose(core, runtime, `zalouser: drop control command from unauthorized sender ${senderId}`);
@@ -437,12 +453,50 @@ async function processMessage(message, account, config, core, runtime, historySt
437
453
  CommandAuthorized: commandAuthorized
438
454
  }
439
455
  });
440
- const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
441
- cfg: config,
442
- agentId: route.agentId,
456
+ await core.channel.turn.runAssembled({
443
457
  channel: "zalouser",
444
458
  accountId: account.accountId,
445
- typing: {
459
+ cfg: config,
460
+ agentId: route.agentId,
461
+ routeSessionKey: route.sessionKey,
462
+ storePath,
463
+ ctxPayload,
464
+ recordInboundSession: core.channel.session.recordInboundSession,
465
+ dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
466
+ delivery: {
467
+ preparePayload: (payload) => {
468
+ if (payload.text === void 0) return payload;
469
+ return {
470
+ ...payload,
471
+ text: core.channel.text.convertMarkdownTables(payload.text, core.channel.text.resolveMarkdownTableMode({
472
+ cfg: config,
473
+ channel: "zalouser",
474
+ accountId: account.accountId
475
+ }))
476
+ };
477
+ },
478
+ durable: () => ({ to: normalizedTo }),
479
+ deliver: async (payload) => {
480
+ return await deliverZalouserReply({
481
+ payload,
482
+ profile: account.profile,
483
+ chatId,
484
+ isGroup,
485
+ runtime,
486
+ core,
487
+ config,
488
+ accountId: account.accountId,
489
+ tableMode: "off"
490
+ });
491
+ },
492
+ onDelivered: (_payload, _info, result) => {
493
+ if (result?.visibleReplySent !== false) statusSink?.({ lastOutboundAt: Date.now() });
494
+ },
495
+ onError: (err, info) => {
496
+ runtime.error(`[${account.accountId}] Zalouser ${info.kind} reply failed: ${String(err)}`);
497
+ }
498
+ },
499
+ replyPipeline: { typing: {
446
500
  start: async () => {
447
501
  await sendTypingZalouser(chatId, {
448
502
  profile: account.profile,
@@ -453,61 +507,10 @@ async function processMessage(message, account, config, core, runtime, historySt
453
507
  runtime.error?.(`[${account.accountId}] zalouser typing start failed for ${chatId}: ${String(err)}`);
454
508
  logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`);
455
509
  }
456
- }
457
- });
458
- await core.channel.turn.run({
459
- channel: "zalouser",
460
- accountId: account.accountId,
461
- raw: message,
462
- adapter: {
463
- ingest: () => ({
464
- id: messageSid ?? `${message.timestampMs}`,
465
- timestamp: message.timestampMs,
466
- rawText: rawBody,
467
- textForAgent: rawBody,
468
- textForCommands: commandBody,
469
- raw: message
470
- }),
471
- resolveTurn: () => ({
472
- cfg: config,
473
- channel: "zalouser",
474
- accountId: account.accountId,
475
- agentId: route.agentId,
476
- routeSessionKey: route.sessionKey,
477
- storePath,
478
- ctxPayload,
479
- recordInboundSession: core.channel.session.recordInboundSession,
480
- dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
481
- delivery: {
482
- deliver: async (payload) => {
483
- await deliverZalouserReply({
484
- payload,
485
- profile: account.profile,
486
- chatId,
487
- isGroup,
488
- runtime,
489
- core,
490
- config,
491
- accountId: account.accountId,
492
- statusSink,
493
- tableMode: core.channel.text.resolveMarkdownTableMode({
494
- cfg: config,
495
- channel: "zalouser",
496
- accountId: account.accountId
497
- })
498
- });
499
- },
500
- onError: (err, info) => {
501
- runtime.error(`[${account.accountId}] Zalouser ${info.kind} reply failed: ${String(err)}`);
502
- }
503
- },
504
- dispatcherOptions: replyPipeline,
505
- replyOptions: { onModelSelected },
506
- record: { onRecordError: (err) => {
507
- runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`);
508
- } }
509
- })
510
- }
510
+ } },
511
+ record: { onRecordError: (err) => {
512
+ runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`);
513
+ } }
511
514
  });
512
515
  if (isGroup && historyKey) clearHistoryEntriesIfEnabled({
513
516
  historyMap: historyState.groupHistories,
@@ -516,8 +519,9 @@ async function processMessage(message, account, config, core, runtime, historySt
516
519
  });
517
520
  }
518
521
  async function deliverZalouserReply(params) {
519
- const { payload, profile, chatId, isGroup, runtime, core, config, accountId, statusSink } = params;
522
+ const { payload, profile, chatId, isGroup, runtime, core, config, accountId } = params;
520
523
  const tableMode = params.tableMode ?? "code";
524
+ let visibleReplySent = false;
521
525
  const reply = resolveSendableOutboundReplyParts(payload, { text: core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode) });
522
526
  const chunkMode = core.channel.text.resolveChunkMode(config, "zalouser", accountId);
523
527
  const textChunkLimit = core.channel.text.resolveTextChunkLimit(config, "zalouser", accountId, { fallbackLimit: ZALOUSER_TEXT_LIMIT });
@@ -533,7 +537,7 @@ async function deliverZalouserReply(params) {
533
537
  textChunkMode: chunkMode,
534
538
  textChunkLimit
535
539
  });
536
- statusSink?.({ lastOutboundAt: Date.now() });
540
+ visibleReplySent = true;
537
541
  } catch (err) {
538
542
  runtime.error(`Zalouser message send failed: ${String(err)}`);
539
543
  }
@@ -548,12 +552,13 @@ async function deliverZalouserReply(params) {
548
552
  textChunkMode: chunkMode,
549
553
  textChunkLimit
550
554
  });
551
- statusSink?.({ lastOutboundAt: Date.now() });
555
+ visibleReplySent = true;
552
556
  },
553
557
  onMediaError: (error) => {
554
558
  runtime.error(`Zalouser media send failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
555
559
  }
556
560
  });
561
+ return { visibleReplySent };
557
562
  }
558
563
  async function monitorZalouserProvider(options) {
559
564
  let { account, config } = options;
@@ -564,8 +569,8 @@ async function monitorZalouserProvider(options) {
564
569
  const groupHistories = /* @__PURE__ */ new Map();
565
570
  try {
566
571
  const profile = account.profile;
567
- const allowFromEntries = (account.config.allowFrom ?? []).map((entry) => normalizeZalouserEntry(String(entry))).filter((entry) => entry && entry !== "*");
568
- const groupAllowFromEntries = (account.config.groupAllowFrom ?? []).map((entry) => normalizeZalouserEntry(String(entry))).filter((entry) => entry && entry !== "*");
572
+ const allowFromEntries = (account.config.allowFrom ?? []).map((entry) => normalizeZalouserAllowEntry(String(entry))).filter((entry) => entry && entry !== "*");
573
+ const groupAllowFromEntries = (account.config.groupAllowFrom ?? []).map((entry) => normalizeZalouserAllowEntry(String(entry))).filter((entry) => entry && entry !== "*");
569
574
  const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
570
575
  if (allowNameMatching && (allowFromEntries.length > 0 || groupAllowFromEntries.length > 0)) {
571
576
  const byName = buildNameIndex(await listZaloFriends(profile), (friend) => friend.displayName);
@@ -608,7 +613,7 @@ async function monitorZalouserProvider(options) {
608
613
  const unresolved = [];
609
614
  const nextGroups = { ...groupsConfig };
610
615
  for (const entry of groupKeys) {
611
- const cleaned = normalizeZalouserEntry(entry);
616
+ const cleaned = normalizeZalouserAllowEntry(entry);
612
617
  if (/^\d+$/.test(cleaned)) {
613
618
  if (!nextGroups[cleaned]) nextGroups[cleaned] = groupsConfig[entry];
614
619
  mapping.push(`${entry}→${cleaned}`);
@@ -1,22 +1,20 @@
1
- import { n as zalouserSetupWizard } from "./setup-surface-NCOuKu-l.js";
1
+ import { n as zalouserSetupWizard } from "./setup-surface-6MmoBuUf.js";
2
2
  import { n as setZalouserRuntime } from "./runtime-QNU7vLgI.js";
3
3
  import { n as zalouserSetupAdapter, t as createZalouserSetupWizardProxy } from "./setup-core-CqipqY98.js";
4
- import { t as zalouserPlugin } from "./channel-DLNmGWb8.js";
4
+ import { t as zalouserPlugin } from "./channel-BaznOdZe.js";
5
5
  import { n as isZalouserMutableGroupEntry, t as collectZalouserSecurityAuditFindings } from "./security-audit-BZLhil-V.js";
6
- import { t as zalouserSetupPlugin } from "./channel.setup-CiDeBFrn.js";
7
- import { t as createZalouserTool } from "./api-C3SYq_R3.js";
6
+ import { t as zalouserSetupPlugin } from "./channel.setup-Tevqgs6C.js";
7
+ import { t as createZalouserTool } from "./api-Dl_YURKB.js";
8
8
  import { buildBaseAccountStatusSnapshot } from "openclaw/plugin-sdk/status-helpers";
9
9
  import { formatAllowFromLowercase, mergeAllowlist, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
10
10
  import { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, normalizeAccountId } from "openclaw/plugin-sdk/core";
11
11
  import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
12
12
  import { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
13
13
  import { deliverTextOrMediaReply, isNumericTargetId, resolveSendableOutboundReplyParts, sendPayloadWithChunkedTextAndMedia } from "openclaw/plugin-sdk/reply-payload";
14
+ import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-message";
14
15
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
15
16
  import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
16
17
  import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
17
18
  import { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
18
19
  import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
19
- import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
20
- import { resolveSenderCommandAuthorization } from "openclaw/plugin-sdk/command-auth";
21
- import { evaluateGroupRouteAccessForPolicy, resolveSenderScopedGroupPolicy } from "openclaw/plugin-sdk/group-access";
22
- export { DEFAULT_ACCOUNT_ID, buildBaseAccountStatusSnapshot, buildChannelConfigSchema, chunkTextForOutbound, collectZalouserSecurityAuditFindings, createChannelPairingController, createChannelReplyPipeline, createZalouserSetupWizardProxy, createZalouserTool, deliverTextOrMediaReply, evaluateGroupRouteAccessForPolicy, formatAllowFromLowercase, isDangerousNameMatchingEnabled, isNumericTargetId, isZalouserMutableGroupEntry, loadOutboundMediaFromUrl, mergeAllowlist, normalizeAccountId, resolveDefaultGroupPolicy, resolveInboundMentionDecision, resolveOpenProviderRuntimeGroupPolicy, resolvePreferredOpenClawTmpDir, resolveSendableOutboundReplyParts, resolveSenderCommandAuthorization, resolveSenderScopedGroupPolicy, sendPayloadWithChunkedTextAndMedia, setZalouserRuntime, summarizeMapping, warnMissingProviderGroupPolicyFallbackOnce, zalouserPlugin, zalouserSetupAdapter, zalouserSetupPlugin, zalouserSetupWizard };
20
+ export { DEFAULT_ACCOUNT_ID, buildBaseAccountStatusSnapshot, buildChannelConfigSchema, chunkTextForOutbound, collectZalouserSecurityAuditFindings, createChannelMessageReplyPipeline, createChannelPairingController, createZalouserSetupWizardProxy, createZalouserTool, deliverTextOrMediaReply, formatAllowFromLowercase, isDangerousNameMatchingEnabled, isNumericTargetId, isZalouserMutableGroupEntry, loadOutboundMediaFromUrl, mergeAllowlist, normalizeAccountId, resolveDefaultGroupPolicy, resolveInboundMentionDecision, resolveOpenProviderRuntimeGroupPolicy, resolvePreferredOpenClawTmpDir, resolveSendableOutboundReplyParts, sendPayloadWithChunkedTextAndMedia, setZalouserRuntime, summarizeMapping, warnMissingProviderGroupPolicyFallbackOnce, zalouserPlugin, zalouserSetupAdapter, zalouserSetupPlugin, zalouserSetupWizard };
@@ -1,4 +1,4 @@
1
- import { _ as sendZaloTypingEvent, f as sendZaloDeliveredEvent, g as sendZaloTextMessage, h as sendZaloSeenEvent, m as sendZaloReaction, p as sendZaloLink, x as TextStyle } from "./zalo-js-CHCUlY3c.js";
1
+ import { S as TextStyle, _ as sendZaloTypingEvent, f as sendZaloDeliveredEvent, g as sendZaloTextMessage, h as sendZaloSeenEvent, m as sendZaloReaction, p as sendZaloLink, x as createZalouserSendReceipt } from "./zalo-js-QGi0H5K7.js";
2
2
  //#region extensions/zalouser/src/text-styles.ts
3
3
  const ESCAPE_SENTINEL_START = "";
4
4
  const ESCAPE_SENTINEL_END = "";
@@ -418,7 +418,11 @@ async function sendMessageZalouser(threadId, text, options = {}) {
418
418
  }
419
419
  return lastResult ?? {
420
420
  ok: false,
421
- error: "No message content provided"
421
+ error: "No message content provided",
422
+ receipt: createZalouserSendReceipt({
423
+ threadId,
424
+ kind: "text"
425
+ })
422
426
  };
423
427
  }
424
428
  async function sendImageZalouser(threadId, imageUrl, options = {}) {
@@ -446,7 +450,11 @@ async function sendReactionZalouser(params) {
446
450
  });
447
451
  return {
448
452
  ok: result.ok,
449
- error: result.error
453
+ error: result.error,
454
+ receipt: createZalouserSendReceipt({
455
+ threadId: params.threadId,
456
+ kind: "unknown"
457
+ })
450
458
  };
451
459
  }
452
460
  async function sendDeliveredZalouser(params) {
@@ -1,2 +1,2 @@
1
- import { t as zalouserSetupPlugin } from "./channel.setup-CiDeBFrn.js";
1
+ import { t as zalouserSetupPlugin } from "./channel.setup-Tevqgs6C.js";
2
2
  export { zalouserSetupPlugin };
@@ -1,6 +1,6 @@
1
- import { a as resolveZalouserAccountSync, i as resolveDefaultZalouserAccountId, r as listZalouserAccountIds, t as checkZcaAuthenticated } from "./accounts-C00IMUgd.js";
1
+ import { a as resolveZalouserAccountSync, i as resolveDefaultZalouserAccountId, r as listZalouserAccountIds, t as checkZcaAuthenticated } from "./accounts-ClSPNIvj.js";
2
2
  import { r as writeQrDataUrlToTempFile } from "./setup-core-CqipqY98.js";
3
- import { b as waitForZaloQrLogin, c as logoutZaloProfile, d as resolveZaloGroupsByEntries, l as resolveZaloAllowFromEntries, y as startZaloQrLogin } from "./zalo-js-CHCUlY3c.js";
3
+ import { b as waitForZaloQrLogin, c as logoutZaloProfile, d as resolveZaloGroupsByEntries, l as resolveZaloAllowFromEntries, y as startZaloQrLogin } from "./zalo-js-QGi0H5K7.js";
4
4
  import { DEFAULT_ACCOUNT_ID, addWildcardAllowFrom, formatCliCommand, formatDocsLink, formatResolvedUnresolvedNote, mergeAllowFromEntries, normalizeAccountId, patchScopedAccountConfig } from "openclaw/plugin-sdk/setup";
5
5
  //#region \0rolldown/runtime.js
6
6
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- import { a as resolveZalouserAccountSync, i as resolveDefaultZalouserAccountId, r as listZalouserAccountIds, t as checkZcaAuthenticated } from "./accounts-C00IMUgd.js";
1
+ import { a as resolveZalouserAccountSync, i as resolveDefaultZalouserAccountId, r as listZalouserAccountIds, t as checkZcaAuthenticated } from "./accounts-ClSPNIvj.js";
2
2
  import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-DgqHp8E2.js";
3
3
  import { n as isZalouserMutableGroupEntry } from "./security-audit-BZLhil-V.js";
4
4
  import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
package/dist/test-api.js CHANGED
@@ -1,5 +1,5 @@
1
- import { a as resolveZalouserAccountSync, i as resolveDefaultZalouserAccountId, n as getZcaUserInfo, r as listZalouserAccountIds, t as checkZcaAuthenticated } from "./accounts-C00IMUgd.js";
1
+ import { a as resolveZalouserAccountSync, i as resolveDefaultZalouserAccountId, n as getZcaUserInfo, r as listZalouserAccountIds, t as checkZcaAuthenticated } from "./accounts-ClSPNIvj.js";
2
2
  import { r as parseZalouserOutboundTarget } from "./session-route-C0-Xr8bt.js";
3
- import { a as listZaloGroupMembers, b as waitForZaloQrLogin, c as logoutZaloProfile, d as resolveZaloGroupsByEntries, i as listZaloFriendsMatching, l as resolveZaloAllowFromEntries, n as getZaloUserInfo, s as listZaloGroupsMatching, t as checkZaloAuthenticated, y as startZaloQrLogin } from "./zalo-js-CHCUlY3c.js";
4
- import { i as sendMessageZalouser } from "./send-BsmySxe3.js";
3
+ import { a as listZaloGroupMembers, b as waitForZaloQrLogin, c as logoutZaloProfile, d as resolveZaloGroupsByEntries, i as listZaloFriendsMatching, l as resolveZaloAllowFromEntries, n as getZaloUserInfo, s as listZaloGroupsMatching, t as checkZaloAuthenticated, y as startZaloQrLogin } from "./zalo-js-QGi0H5K7.js";
4
+ import { i as sendMessageZalouser } from "./send-CeCQ8UZF.js";
5
5
  export { checkZaloAuthenticated, checkZcaAuthenticated, getZaloUserInfo, getZcaUserInfo, listZaloFriendsMatching, listZaloGroupMembers, listZaloGroupsMatching, listZalouserAccountIds, logoutZaloProfile, parseZalouserOutboundTarget, resolveDefaultZalouserAccountId, resolveZaloAllowFromEntries, resolveZaloGroupsByEntries, resolveZalouserAccountSync, sendMessageZalouser, startZaloQrLogin, waitForZaloQrLogin };
@@ -1,9 +1,12 @@
1
- import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
1
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, sleep } from "openclaw/plugin-sdk/text-runtime";
2
+ import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-message";
2
3
  import path from "node:path";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import fs from "node:fs";
5
6
  import os from "node:os";
7
+ import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
6
8
  import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
9
+ import { privateFileStoreSync, readRegularFileSync, statRegularFileSync, withTimeout } from "openclaw/plugin-sdk/security-runtime";
7
10
  import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
8
11
  //#region extensions/zalouser/src/zca-constants.ts
9
12
  const ThreadType = {
@@ -66,6 +69,24 @@ function normalizeZaloReactionIcon(raw) {
66
69
  return REACTION_ALIAS_MAP.get(normalizeLowercaseStringOrEmpty(trimmed)) ?? REACTION_ALIAS_MAP.get(trimmed) ?? trimmed;
67
70
  }
68
71
  //#endregion
72
+ //#region extensions/zalouser/src/send-receipt.ts
73
+ function createZalouserSendReceipt(params) {
74
+ const platformMessageIds = (params.platformMessageIds ?? [params.messageId]).map((messageId) => messageId?.trim()).filter((messageId) => Boolean(messageId));
75
+ const threadId = params.threadId?.trim();
76
+ return createMessageReceiptFromOutboundResults({
77
+ results: platformMessageIds.map((messageId) => {
78
+ const result = {
79
+ channel: "zalouser",
80
+ messageId
81
+ };
82
+ if (threadId) result.conversationId = threadId;
83
+ return result;
84
+ }),
85
+ ...threadId ? { threadId } : {},
86
+ kind: params.kind ?? "unknown"
87
+ });
88
+ }
89
+ //#endregion
69
90
  //#region extensions/zalouser/src/zca-client.ts
70
91
  let zcaJsRuntimePromise = null;
71
92
  async function loadZcaJsRuntime() {
@@ -110,76 +131,16 @@ function resolveCredentialsPath(profile, env = process.env) {
110
131
  function isNodeErrorCode(error, code) {
111
132
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
112
133
  }
113
- function ensureCredentialsDir() {
114
- const dir = resolveCredentialsDir();
115
- fs.mkdirSync(dir, {
116
- recursive: true,
117
- mode: 448
118
- });
119
- const stat = fs.lstatSync(dir);
120
- if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Refusing to use non-directory Zalo credentials path");
121
- try {
122
- fs.chmodSync(dir, 448);
123
- } catch {}
124
- return dir;
125
- }
126
134
  function isReadableCredentialFile(filePath) {
127
135
  try {
128
- const stat = fs.lstatSync(filePath);
129
- return stat.isFile() && !stat.isSymbolicLink();
136
+ return !statRegularFileSync(filePath).missing;
130
137
  } catch (error) {
131
138
  if (isNodeErrorCode(error, "ENOENT")) return false;
132
139
  throw error;
133
140
  }
134
141
  }
135
- function assertWritableCredentialTarget(filePath) {
136
- try {
137
- const stat = fs.lstatSync(filePath);
138
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Refusing to write Zalo credentials to symlinked path");
139
- } catch (error) {
140
- if (isNodeErrorCode(error, "ENOENT")) return;
141
- throw error;
142
- }
143
- }
144
142
  function writeCredentialFileAtomic(filePath, payload) {
145
- const dir = ensureCredentialsDir();
146
- assertWritableCredentialTarget(filePath);
147
- const tempPath = path.join(dir, `.${path.basename(filePath)}.tmp-${process.pid}-${randomUUID()}`);
148
- try {
149
- fs.writeFileSync(tempPath, payload, {
150
- encoding: "utf-8",
151
- mode: 384,
152
- flag: "wx"
153
- });
154
- try {
155
- fs.chmodSync(tempPath, 384);
156
- } catch {}
157
- fs.renameSync(tempPath, filePath);
158
- try {
159
- fs.chmodSync(filePath, 384);
160
- } catch {}
161
- } finally {
162
- try {
163
- fs.unlinkSync(tempPath);
164
- } catch {}
165
- }
166
- }
167
- function withTimeout(promise, timeoutMs, label) {
168
- return new Promise((resolve, reject) => {
169
- const timer = setTimeout(() => {
170
- reject(new Error(label));
171
- }, timeoutMs);
172
- promise.then((result) => {
173
- clearTimeout(timer);
174
- resolve(result);
175
- }).catch((err) => {
176
- clearTimeout(timer);
177
- reject(err);
178
- });
179
- });
180
- }
181
- function delay(ms) {
182
- return new Promise((resolve) => setTimeout(resolve, ms));
143
+ privateFileStoreSync(resolveCredentialsDir()).writeText(path.basename(filePath), payload);
183
144
  }
184
145
  function normalizeProfile(profile) {
185
146
  const trimmed = profile?.trim();
@@ -380,7 +341,7 @@ function resolveMediaFileName(params) {
380
341
  const fromPath = path.basename(parsed.pathname).trim();
381
342
  if (fromPath) return fromPath;
382
343
  } catch {}
383
- return `upload.${params.contentType === "image/png" ? "png" : params.contentType === "image/webp" ? "webp" : params.contentType === "image/jpeg" ? "jpg" : params.contentType === "video/mp4" ? "mp4" : params.contentType === "audio/mpeg" ? "mp3" : params.contentType === "audio/ogg" ? "ogg" : params.contentType === "audio/wav" ? "wav" : params.kind === "video" ? "mp4" : params.kind === "audio" ? "mp3" : params.kind === "image" ? "jpg" : "bin"}`;
344
+ return `upload.${extensionForMime(params.contentType)?.replace(/^\./u, "") ?? (params.kind === "video" ? "mp4" : params.kind === "audio" ? "mp3" : params.kind === "image" ? "jpg" : "bin")}`;
384
345
  }
385
346
  function resolveUploadedVoiceAsset(uploaded) {
386
347
  for (const item of uploaded) {
@@ -416,7 +377,7 @@ function readCredentials(profile) {
416
377
  const filePath = resolveCredentialsPath(profile);
417
378
  try {
418
379
  if (!isReadableCredentialFile(filePath)) return null;
419
- const raw = fs.readFileSync(filePath, "utf-8");
380
+ const raw = readRegularFileSync({ filePath }).buffer.toString("utf-8");
420
381
  const parsed = JSON.parse(raw);
421
382
  if (typeof parsed.imei !== "string" || !parsed.imei || !parsed.cookie || typeof parsed.userAgent !== "string" || !parsed.userAgent) return null;
422
383
  const credentials = {
@@ -525,7 +486,7 @@ async function ensureApi(profileInput, timeoutMs = API_LOGIN_TIMEOUT_MS) {
525
486
  cookie: stored.cookie,
526
487
  userAgent: stored.userAgent,
527
488
  language: stored.language
528
- }), timeoutMs, `Timed out restoring Zalo session for profile "${profile}"`);
489
+ }), timeoutMs, { message: `Timed out restoring Zalo session for profile "${profile}"` });
529
490
  apiByProfile.set(profile, api);
530
491
  writeApiCredentials(profile, api, stored);
531
492
  return api;
@@ -669,7 +630,7 @@ async function checkZaloAuthenticated(profileInput) {
669
630
  const profile = normalizeProfile(profileInput);
670
631
  if (!zalouserSessionExists(profile)) return false;
671
632
  try {
672
- await withZaloApi(profile, async (api) => await withTimeout(api.fetchAccountInfo(), 12e3, "Timed out checking Zalo session"), { timeoutMs: 12e3 });
633
+ await withZaloApi(profile, async (api) => await withTimeout(api.fetchAccountInfo(), 12e3, { message: "Timed out checking Zalo session" }), { timeoutMs: 12e3 });
673
634
  return true;
674
635
  } catch {
675
636
  invalidateApi(profile);
@@ -799,7 +760,11 @@ async function sendZaloTextMessage(threadId, text, options = {}) {
799
760
  const trimmedThreadId = threadId.trim();
800
761
  if (!trimmedThreadId) return {
801
762
  ok: false,
802
- error: "No threadId provided"
763
+ error: "No threadId provided",
764
+ receipt: createZalouserSendReceipt({
765
+ threadId,
766
+ kind: "unknown"
767
+ })
803
768
  };
804
769
  return await withZaloApi(profile, async (api) => {
805
770
  const type = options.isGroup ? ThreadType.Group : ThreadType.User;
@@ -831,37 +796,59 @@ async function sendZaloTextMessage(threadId, text, options = {}) {
831
796
  }], trimmedThreadId, type));
832
797
  if (!voiceAsset) throw new Error("Failed to resolve uploaded audio URL for voice message");
833
798
  const voiceUrl = buildZaloVoicePlaybackUrl(voiceAsset);
799
+ const voiceMessageId = extractSendMessageId(await api.sendVoice({ voiceUrl }, trimmedThreadId, type));
834
800
  return {
835
801
  ok: true,
836
- messageId: extractSendMessageId(await api.sendVoice({ voiceUrl }, trimmedThreadId, type)) ?? textMessageId
802
+ messageId: voiceMessageId ?? textMessageId,
803
+ receipt: createZalouserSendReceipt({
804
+ platformMessageIds: [textMessageId, voiceMessageId],
805
+ threadId: trimmedThreadId,
806
+ kind: "voice"
807
+ })
837
808
  };
838
809
  }
810
+ const messageId = extractSendMessageId(await api.sendMessage({
811
+ msg: payloadText,
812
+ ...textStyles ? { styles: textStyles } : {},
813
+ attachments: [{
814
+ data: media.buffer,
815
+ filename: fileName.includes(".") ? fileName : `${fileName}.bin`,
816
+ metadata: { totalSize: media.buffer.length }
817
+ }]
818
+ }, trimmedThreadId, type));
839
819
  return {
840
820
  ok: true,
841
- messageId: extractSendMessageId(await api.sendMessage({
842
- msg: payloadText,
843
- ...textStyles ? { styles: textStyles } : {},
844
- attachments: [{
845
- data: media.buffer,
846
- filename: fileName.includes(".") ? fileName : `${fileName}.bin`,
847
- metadata: { totalSize: media.buffer.length }
848
- }]
849
- }, trimmedThreadId, type))
821
+ messageId,
822
+ receipt: createZalouserSendReceipt({
823
+ messageId,
824
+ threadId: trimmedThreadId,
825
+ kind: "media"
826
+ })
850
827
  };
851
828
  }
852
829
  const payloadText = text.slice(0, 2e3);
853
830
  const textStyles = clampTextStyles(payloadText, options.textStyles);
831
+ const messageId = extractSendMessageId(await api.sendMessage(textStyles ? {
832
+ msg: payloadText,
833
+ styles: textStyles
834
+ } : payloadText, trimmedThreadId, type));
854
835
  return {
855
836
  ok: true,
856
- messageId: extractSendMessageId(await api.sendMessage(textStyles ? {
857
- msg: payloadText,
858
- styles: textStyles
859
- } : payloadText, trimmedThreadId, type))
837
+ messageId,
838
+ receipt: createZalouserSendReceipt({
839
+ messageId,
840
+ threadId: trimmedThreadId,
841
+ kind: "text"
842
+ })
860
843
  };
861
844
  } catch (error) {
862
845
  return {
863
846
  ok: false,
864
- error: toErrorMessage(error)
847
+ error: toErrorMessage(error),
848
+ receipt: createZalouserSendReceipt({
849
+ threadId: trimmedThreadId,
850
+ kind: "unknown"
851
+ })
865
852
  };
866
853
  }
867
854
  }, { shouldPersist: (result) => result.ok });
@@ -942,11 +929,19 @@ async function sendZaloLink(threadId, url, options = {}) {
942
929
  const trimmedUrl = url.trim();
943
930
  if (!trimmedThreadId) return {
944
931
  ok: false,
945
- error: "No threadId provided"
932
+ error: "No threadId provided",
933
+ receipt: createZalouserSendReceipt({
934
+ threadId,
935
+ kind: "unknown"
936
+ })
946
937
  };
947
938
  if (!trimmedUrl) return {
948
939
  ok: false,
949
- error: "No URL provided"
940
+ error: "No URL provided",
941
+ receipt: createZalouserSendReceipt({
942
+ threadId: trimmedThreadId,
943
+ kind: "card"
944
+ })
950
945
  };
951
946
  try {
952
947
  return await withZaloApi(profile, async (api) => {
@@ -955,15 +950,25 @@ async function sendZaloLink(threadId, url, options = {}) {
955
950
  link: trimmedUrl,
956
951
  msg: options.caption
957
952
  }, trimmedThreadId, type);
953
+ const messageId = String(response.msgId);
958
954
  return {
959
955
  ok: true,
960
- messageId: String(response.msgId)
956
+ messageId,
957
+ receipt: createZalouserSendReceipt({
958
+ messageId,
959
+ threadId: trimmedThreadId,
960
+ kind: "card"
961
+ })
961
962
  };
962
963
  }, { shouldPersist: (result) => result.ok });
963
964
  } catch (error) {
964
965
  return {
965
966
  ok: false,
966
- error: toErrorMessage(error)
967
+ error: toErrorMessage(error),
968
+ receipt: createZalouserSendReceipt({
969
+ threadId: trimmedThreadId,
970
+ kind: "card"
971
+ })
967
972
  };
968
973
  }
969
974
  }
@@ -1069,7 +1074,7 @@ async function startZaloQrLogin(params) {
1069
1074
  qrDataUrl: active.qrDataUrl,
1070
1075
  message: "Scan this QR with the Zalo app."
1071
1076
  };
1072
- await delay(150);
1077
+ await sleep(150);
1073
1078
  }
1074
1079
  return { message: "Still preparing QR. Call wait to continue checking login status." };
1075
1080
  }
@@ -1108,7 +1113,7 @@ async function waitForZaloQrLogin(params) {
1108
1113
  message: "Login successful."
1109
1114
  };
1110
1115
  }
1111
- await Promise.race([active.waitPromise, delay(400)]);
1116
+ await Promise.race([active.waitPromise, sleep(400)]);
1112
1117
  }
1113
1118
  return {
1114
1119
  connected: false,
@@ -1276,4 +1281,4 @@ async function resolveZaloAllowFromEntries(params) {
1276
1281
  });
1277
1282
  }
1278
1283
  //#endregion
1279
- export { sendZaloTypingEvent as _, listZaloGroupMembers as a, waitForZaloQrLogin as b, logoutZaloProfile as c, resolveZaloGroupsByEntries as d, sendZaloDeliveredEvent as f, sendZaloTextMessage as g, sendZaloSeenEvent as h, listZaloFriendsMatching as i, resolveZaloAllowFromEntries as l, sendZaloReaction as m, getZaloUserInfo as n, listZaloGroups as o, sendZaloLink as p, listZaloFriends as r, listZaloGroupsMatching as s, checkZaloAuthenticated as t, resolveZaloGroupContext as u, startZaloListener as v, TextStyle as x, startZaloQrLogin as y };
1284
+ export { TextStyle as S, sendZaloTypingEvent as _, listZaloGroupMembers as a, waitForZaloQrLogin as b, logoutZaloProfile as c, resolveZaloGroupsByEntries as d, sendZaloDeliveredEvent as f, sendZaloTextMessage as g, sendZaloSeenEvent as h, listZaloFriendsMatching as i, resolveZaloAllowFromEntries as l, sendZaloReaction as m, getZaloUserInfo as n, listZaloGroups as o, sendZaloLink as p, listZaloFriends as r, listZaloGroupsMatching as s, checkZaloAuthenticated as t, resolveZaloGroupContext as u, startZaloListener as v, createZalouserSendReceipt as x, startZaloQrLogin as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/zalouser",
3
- "version": "2026.5.7",
3
+ "version": "2026.5.10-beta.1",
4
4
  "description": "OpenClaw Zalo Personal Account plugin via native zca-js integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "dependencies": {
11
- "typebox": "1.1.37",
11
+ "typebox": "1.1.38",
12
12
  "zca-js": "2.1.2"
13
13
  },
14
14
  "devDependencies": {
@@ -16,7 +16,7 @@
16
16
  "openclaw": "workspace:*"
17
17
  },
18
18
  "peerDependencies": {
19
- "openclaw": ">=2026.5.7"
19
+ "openclaw": ">=2026.5.10-beta.1"
20
20
  },
21
21
  "peerDependenciesMeta": {
22
22
  "openclaw": {
@@ -53,10 +53,10 @@
53
53
  "minHostVersion": ">=2026.4.10"
54
54
  },
55
55
  "compat": {
56
- "pluginApi": ">=2026.5.7"
56
+ "pluginApi": ">=2026.5.10-beta.1"
57
57
  },
58
58
  "build": {
59
- "openclawVersion": "2026.5.7"
59
+ "openclawVersion": "2026.5.10-beta.1"
60
60
  },
61
61
  "release": {
62
62
  "publishToClawHub": true,