@openclaw/zalo 2026.7.2-beta.7 → 2026.8.1-beta.3
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/dist/{actions.runtime-BYBjeo4Q.js → actions.runtime-DUY9_6Ss.js} +1 -1
- package/dist/api.js +2 -2
- package/dist/{channel-idqJI6YK.js → channel-Vdxu1Y3k.js} +19 -9
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.runtime-DsaLk085.js → channel.runtime-UD-M7eDb.js} +18 -35
- package/dist/{monitor-C5cximHM.js → monitor-JdVVhnCm.js} +134 -81
- package/dist/{monitor.webhook-D56OCa3C.js → monitor.webhook-cl1sV6j0.js} +2 -6
- package/dist/runtime-api.js +1 -1
- package/dist/{secret-contract-Bxvjh0Hq.js → secret-contract-Cpsqe5Cv.js} +2 -2
- package/dist/secret-contract-api.js +1 -1
- package/dist/{send-BUvbbedq.js → send-DVMIplpy.js} +6 -4
- package/dist/setup-api.js +2 -2
- package/dist/{setup-core-LzhuTlsD.js → setup-core-CcCJ1E26.js} +19 -25
- package/dist/{setup-surface-CQMbpZye.js → setup-surface-CIpp_MDf.js} +3 -2
- package/dist/setup-surface.js +3 -0
- package/dist/webhook-spool-Bdnu4GjY.js +180 -0
- package/openclaw.plugin.json +99 -0
- package/package.json +8 -5
- package/dist/webhook-spool-DzCPbb3O.js +0 -121
- package/dist/{runtime-api-qxdOyk3M.js → runtime-api-BNim5FCa.js} +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { d as resolveZaloAccount, f as resolveZaloToken } from "./setup-core-
|
|
1
|
+
import { d as resolveZaloAccount, f as resolveZaloToken } from "./setup-core-CcCJ1E26.js";
|
|
2
2
|
import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
|
|
3
3
|
import { stripChannelTargetPrefix, stripTargetKindPrefix } from "openclaw/plugin-sdk/core";
|
|
4
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
5
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
6
6
|
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
7
|
-
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
|
7
|
+
import { assertOkOrThrowProviderError, readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
|
8
8
|
import { resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
9
9
|
import { makeProxyFetch } from "openclaw/plugin-sdk/fetch-runtime";
|
|
10
10
|
//#region extensions/zalo/src/timeouts.ts
|
|
@@ -55,12 +55,14 @@ async function callZaloApi(method, token, body, options) {
|
|
|
55
55
|
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
56
56
|
const fetcher = options?.fetch ?? fetch;
|
|
57
57
|
try {
|
|
58
|
-
const
|
|
58
|
+
const response = await fetcher(url, {
|
|
59
59
|
method: "POST",
|
|
60
60
|
headers: { "Content-Type": "application/json" },
|
|
61
61
|
body: body ? JSON.stringify(body) : void 0,
|
|
62
62
|
signal: controller.signal
|
|
63
|
-
})
|
|
63
|
+
});
|
|
64
|
+
await assertOkOrThrowProviderError(response, `zalo.${method}`);
|
|
65
|
+
const data = await readProviderJsonResponse(response, `zalo.${method}`);
|
|
64
66
|
if (!data.ok) throw new ZaloApiError(data.description ?? `Zalo API error: ${method}`, data.error_code, data.description);
|
|
65
67
|
return data;
|
|
66
68
|
} finally {
|
package/dist/setup-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-
|
|
1
|
+
import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-CcCJ1E26.js";
|
|
2
2
|
import { n as resolveZaloRuntimeGroupPolicy } from "./group-access-8qHRzDHx.js";
|
|
3
3
|
import { loadBundledEntryExportSync } from "openclaw/plugin-sdk/channel-entry-contract";
|
|
4
4
|
//#region extensions/zalo/setup-api.ts
|
|
@@ -23,7 +23,7 @@ function createLazyObjectValue(load) {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
function loadSetupSurfaceModule() {
|
|
26
|
-
return loadBundledEntryExportSync(import.meta.url, { specifier: "./
|
|
26
|
+
return loadBundledEntryExportSync(import.meta.url, { specifier: "./setup-surface.js" });
|
|
27
27
|
}
|
|
28
28
|
const zaloSetupWizard = createLazyObjectValue(() => loadSetupSurfaceModule().zaloSetupWizard);
|
|
29
29
|
//#endregion
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createAccountListHelpers } from "openclaw/plugin-sdk/account-helpers";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
3
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
|
-
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
|
|
5
4
|
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
|
5
|
+
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
|
|
6
6
|
import { buildSecretInputSchema, normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
7
7
|
import { createChannelDmPolicy } from "openclaw/plugin-sdk/channel-dm-policy";
|
|
8
8
|
import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
|
|
@@ -20,8 +20,18 @@ var __exportAll = (all, no_symbols) => {
|
|
|
20
20
|
};
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region extensions/zalo/src/token.ts
|
|
23
|
-
function readTokenFromFile(tokenFile) {
|
|
24
|
-
|
|
23
|
+
function readTokenFromFile(tokenFile, configPath) {
|
|
24
|
+
const result = tryReadSecretFileSync(tokenFile, "Zalo token file", { rejectSymlink: true }, { configPath });
|
|
25
|
+
return result.status === "available" ? {
|
|
26
|
+
token: result.value,
|
|
27
|
+
source: "configFile",
|
|
28
|
+
status: "available"
|
|
29
|
+
} : {
|
|
30
|
+
token: "",
|
|
31
|
+
source: "configFile",
|
|
32
|
+
status: "configured_unavailable",
|
|
33
|
+
credentialDiagnostics: [result.diagnostic]
|
|
34
|
+
};
|
|
25
35
|
}
|
|
26
36
|
function resolveZaloToken(config, accountId, options) {
|
|
27
37
|
const resolvedAccountId = normalizeAccountId(accountId ?? config?.defaultAccount);
|
|
@@ -45,21 +55,8 @@ function resolveZaloToken(config, accountId, options) {
|
|
|
45
55
|
source: "config",
|
|
46
56
|
status: "configured_unavailable"
|
|
47
57
|
};
|
|
48
|
-
const fileToken = readTokenFromFile(accountConfig.tokenFile);
|
|
49
|
-
if (fileToken) return {
|
|
50
|
-
token: fileToken,
|
|
51
|
-
source: "configFile",
|
|
52
|
-
status: "available"
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
if (!accountHasBotToken) {
|
|
56
|
-
const fileToken = readTokenFromFile(accountConfig?.tokenFile);
|
|
57
|
-
if (fileToken) return {
|
|
58
|
-
token: fileToken,
|
|
59
|
-
source: "configFile",
|
|
60
|
-
status: "available"
|
|
61
|
-
};
|
|
62
58
|
}
|
|
59
|
+
if (accountConfig?.tokenFile?.trim()) return readTokenFromFile(accountConfig.tokenFile, `channels.zalo.accounts.${resolvedAccountId}.tokenFile`);
|
|
63
60
|
if (!accountHasBotToken) {
|
|
64
61
|
const token = resolveSecretInputString({
|
|
65
62
|
value: baseConfig?.botToken,
|
|
@@ -76,12 +73,7 @@ function resolveZaloToken(config, accountId, options) {
|
|
|
76
73
|
source: "config",
|
|
77
74
|
status: "configured_unavailable"
|
|
78
75
|
};
|
|
79
|
-
|
|
80
|
-
if (fileToken) return {
|
|
81
|
-
token: fileToken,
|
|
82
|
-
source: "configFile",
|
|
83
|
-
status: "available"
|
|
84
|
-
};
|
|
76
|
+
if (baseConfig?.tokenFile?.trim()) return readTokenFromFile(baseConfig.tokenFile, "channels.zalo.tokenFile");
|
|
85
77
|
}
|
|
86
78
|
if (isDefaultAccount) {
|
|
87
79
|
const envToken = process.env.ZALO_BOT_TOKEN?.trim();
|
|
@@ -126,6 +118,7 @@ function resolveZaloAccountWithMode(params) {
|
|
|
126
118
|
token: tokenResolution.token,
|
|
127
119
|
tokenSource: tokenResolution.source,
|
|
128
120
|
tokenStatus: tokenResolution.status,
|
|
121
|
+
...tokenResolution.credentialDiagnostics ? { credentialDiagnostics: tokenResolution.credentialDiagnostics } : {},
|
|
129
122
|
config: merged
|
|
130
123
|
};
|
|
131
124
|
}
|
|
@@ -244,7 +237,8 @@ const zaloSetupContract = defineChannelSetupContract({
|
|
|
244
237
|
cli: {
|
|
245
238
|
flags: "--use-env",
|
|
246
239
|
description: "Use ZALO_BOT_TOKEN"
|
|
247
|
-
}
|
|
240
|
+
},
|
|
241
|
+
envVars: ["ZALO_BOT_TOKEN"]
|
|
248
242
|
}
|
|
249
243
|
},
|
|
250
244
|
legacyAdapter: zaloSetupAdapter
|
|
@@ -289,4 +283,4 @@ function createZaloSetupWizardProxy(loadWizard) {
|
|
|
289
283
|
});
|
|
290
284
|
}
|
|
291
285
|
//#endregion
|
|
292
|
-
export { noteZaloTokenHelp as a, inspectZaloAccount as c, resolveZaloAccount as d, resolveZaloToken as f, zaloSetupContract as i, listZaloAccountIds as l, normalizeSecretInputString as m, zaloDmPolicy as n, promptZaloAllowFrom as o, buildSecretInputSchema as p, zaloSetupAdapter as r, accounts_exports as s, createZaloSetupWizardProxy as t, resolveDefaultZaloAccountId as u };
|
|
286
|
+
export { noteZaloTokenHelp as a, inspectZaloAccount as c, resolveZaloAccount as d, resolveZaloToken as f, __exportAll as h, zaloSetupContract as i, listZaloAccountIds as l, normalizeSecretInputString as m, zaloDmPolicy as n, promptZaloAllowFrom as o, buildSecretInputSchema as p, zaloSetupAdapter as r, accounts_exports as s, createZaloSetupWizardProxy as t, resolveDefaultZaloAccountId as u };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as noteZaloTokenHelp, c as inspectZaloAccount, n as zaloDmPolicy, o as promptZaloAllowFrom } from "./setup-core-
|
|
1
|
+
import { a as noteZaloTokenHelp, c as inspectZaloAccount, h as __exportAll, n as zaloDmPolicy, o as promptZaloAllowFrom } from "./setup-core-CcCJ1E26.js";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, buildSingleChannelSecretPromptState, createSetupTranslator, createStandardChannelSetupStatus, hasConfiguredSecretInput, promptSingleChannelSecretInput, runSingleChannelSecretStep } from "openclaw/plugin-sdk/setup";
|
|
3
3
|
//#region extensions/zalo/src/setup-surface.ts
|
|
4
|
+
var setup_surface_exports = /* @__PURE__ */ __exportAll({ zaloSetupWizard: () => zaloSetupWizard });
|
|
4
5
|
const t = createSetupTranslator();
|
|
5
6
|
const channel = "zalo";
|
|
6
7
|
function setZaloUpdateMode(cfg, accountId, mode, webhookUrl, webhookSecret, webhookPath) {
|
|
@@ -211,4 +212,4 @@ const zaloSetupWizard = {
|
|
|
211
212
|
dmPolicy: zaloDmPolicy
|
|
212
213
|
};
|
|
213
214
|
//#endregion
|
|
214
|
-
export { zaloSetupWizard };
|
|
215
|
+
export { zaloSetupWizard as n, setup_surface_exports as t };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { t as getZaloRuntime } from "./runtime-api-BNim5FCa.js";
|
|
2
|
+
import { r as ZaloApiError } from "./send-DVMIplpy.js";
|
|
3
|
+
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
|
|
4
|
+
import { normalizeNullableString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
|
+
import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
8
|
+
//#region extensions/zalo/src/webhook-spool.ts
|
|
9
|
+
const ZALO_WEBHOOK_SPOOL_VERSION = 1;
|
|
10
|
+
const ZALO_WEBHOOK_DRAIN_INTERVAL_MS = 500;
|
|
11
|
+
const ZALO_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
|
|
12
|
+
const ZaloWebhookPayloadError = createChannelIngressError("ZaloWebhookPayloadError");
|
|
13
|
+
const nonEmptyWebhookStringSchema = z.string().transform((value) => normalizeNullableString(value)).pipe(z.string());
|
|
14
|
+
const optionalWebhookStringSchema = z.string().optional().catch(void 0);
|
|
15
|
+
const webhookEnvelopeSchema = z.looseObject({
|
|
16
|
+
ok: z.unknown().optional(),
|
|
17
|
+
result: z.looseObject({}).optional().catch(void 0)
|
|
18
|
+
}).transform((envelope) => envelope.ok === true && envelope.result ? envelope.result : envelope);
|
|
19
|
+
const webhookAdmissionSchema = z.looseObject({ message: z.looseObject({
|
|
20
|
+
message_id: nonEmptyWebhookStringSchema,
|
|
21
|
+
chat: z.looseObject({ id: nonEmptyWebhookStringSchema })
|
|
22
|
+
}) });
|
|
23
|
+
const webhookSenderSchema = z.object({
|
|
24
|
+
id: nonEmptyWebhookStringSchema,
|
|
25
|
+
name: optionalWebhookStringSchema,
|
|
26
|
+
display_name: optionalWebhookStringSchema,
|
|
27
|
+
avatar: optionalWebhookStringSchema,
|
|
28
|
+
is_bot: z.boolean().optional().catch(void 0)
|
|
29
|
+
});
|
|
30
|
+
const webhookChatSchema = z.object({
|
|
31
|
+
id: nonEmptyWebhookStringSchema,
|
|
32
|
+
chat_type: z.enum(["PRIVATE", "GROUP"])
|
|
33
|
+
});
|
|
34
|
+
const webhookMessageSchema = z.object({
|
|
35
|
+
message_id: nonEmptyWebhookStringSchema,
|
|
36
|
+
from: webhookSenderSchema,
|
|
37
|
+
chat: webhookChatSchema,
|
|
38
|
+
date: z.number().finite(),
|
|
39
|
+
text: optionalWebhookStringSchema,
|
|
40
|
+
photo_url: optionalWebhookStringSchema,
|
|
41
|
+
caption: optionalWebhookStringSchema,
|
|
42
|
+
sticker: optionalWebhookStringSchema,
|
|
43
|
+
message_type: optionalWebhookStringSchema
|
|
44
|
+
});
|
|
45
|
+
const webhookUpdateSchema = z.object({
|
|
46
|
+
event_name: z.enum([
|
|
47
|
+
"message.text.received",
|
|
48
|
+
"message.image.received",
|
|
49
|
+
"message.sticker.received",
|
|
50
|
+
"message.unsupported.received"
|
|
51
|
+
]),
|
|
52
|
+
message: webhookMessageSchema
|
|
53
|
+
}).superRefine((update, context) => {
|
|
54
|
+
if (update.event_name === "message.text.received" && update.message.text === void 0) context.addIssue({
|
|
55
|
+
code: "custom",
|
|
56
|
+
path: ["message", "text"],
|
|
57
|
+
message: "text event requires message.text"
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
function parseRawRecord(rawEvent) {
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(rawEvent);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new ZaloWebhookPayloadError("Zalo webhook body contains invalid JSON.", { cause: error });
|
|
66
|
+
}
|
|
67
|
+
const envelope = webhookEnvelopeSchema.safeParse(parsed);
|
|
68
|
+
if (!envelope.success) throw new ZaloWebhookPayloadError("Zalo webhook body must be a JSON object.");
|
|
69
|
+
return envelope.data;
|
|
70
|
+
}
|
|
71
|
+
function inspectZaloWebhookEvent(rawEvent) {
|
|
72
|
+
const update = parseRawRecord(rawEvent);
|
|
73
|
+
const admission = webhookAdmissionSchema.safeParse(update);
|
|
74
|
+
if (!admission.success) {
|
|
75
|
+
if (admission.error.issues.some((issue) => issue.path[0] === "message" && (issue.path.length === 1 || issue.path[1] === "message_id"))) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.message_id.");
|
|
76
|
+
if (admission.error.issues.some((issue) => issue.path[0] === "message" && issue.path[1] === "chat")) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.chat.id.");
|
|
77
|
+
throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.message_id.");
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
eventId: admission.data.message.message_id,
|
|
81
|
+
laneKey: `chat:${admission.data.message.chat.id}`,
|
|
82
|
+
update
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function parseClaimedUpdate(payload, claimedId) {
|
|
86
|
+
if (payload.version !== ZALO_WEBHOOK_SPOOL_VERSION || typeof payload.rawEvent !== "string") throw new ZaloWebhookPayloadError("Zalo webhook spool payload is invalid.");
|
|
87
|
+
const facts = inspectZaloWebhookEvent(payload.rawEvent);
|
|
88
|
+
if (facts.eventId !== claimedId) throw new ZaloWebhookPayloadError("Zalo webhook message id changed after durable admission.");
|
|
89
|
+
const parsed = webhookUpdateSchema.safeParse(facts.update);
|
|
90
|
+
if (!parsed.success) {
|
|
91
|
+
const paths = parsed.error.issues.map((issue) => issue.path.join("."));
|
|
92
|
+
if (paths.some((path) => path === "event_name")) throw new ZaloWebhookPayloadError("Zalo webhook event_name is unsupported.");
|
|
93
|
+
if (paths.some((path) => path === "message.from" || path.startsWith("message.from.id"))) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.from.id.");
|
|
94
|
+
if (paths.some((path) => path === "message.chat" || path.startsWith("message.chat.id"))) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.chat.id.");
|
|
95
|
+
if (paths.some((path) => path.startsWith("message.chat.chat_type"))) throw new ZaloWebhookPayloadError("Zalo webhook message has an invalid chat type.");
|
|
96
|
+
if (paths.some((path) => path.startsWith("message.date"))) throw new ZaloWebhookPayloadError("Zalo webhook message has an invalid date.");
|
|
97
|
+
if (paths.some((path) => path.startsWith("message.text"))) throw new ZaloWebhookPayloadError("Zalo text event is missing message.text.");
|
|
98
|
+
throw new ZaloWebhookPayloadError("Zalo webhook event_name is unsupported.");
|
|
99
|
+
}
|
|
100
|
+
const { event_name: eventName, message } = parsed.data;
|
|
101
|
+
return {
|
|
102
|
+
event_name: eventName,
|
|
103
|
+
message: {
|
|
104
|
+
message_id: claimedId,
|
|
105
|
+
from: message.from,
|
|
106
|
+
chat: message.chat,
|
|
107
|
+
date: message.date,
|
|
108
|
+
...message.text !== void 0 ? { text: message.text } : {},
|
|
109
|
+
...message.photo_url !== void 0 ? { photo_url: message.photo_url } : {},
|
|
110
|
+
...message.caption !== void 0 ? { caption: message.caption } : {},
|
|
111
|
+
...message.sticker !== void 0 ? { sticker: message.sticker } : {},
|
|
112
|
+
...message.message_type !== void 0 ? { message_type: message.message_type } : {}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function isZaloAuthenticationFailure(error) {
|
|
117
|
+
let current = error;
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
while (current && typeof current === "object" && !seen.has(current)) {
|
|
120
|
+
seen.add(current);
|
|
121
|
+
const candidate = current;
|
|
122
|
+
if (current instanceof ZaloApiError && (current.errorCode === 401 || current.errorCode === 403) || candidate.status === 401 || candidate.status === 403 || candidate.statusCode === 401 || candidate.statusCode === 403) return true;
|
|
123
|
+
current = candidate.cause;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
function createZaloWebhookIngress(options) {
|
|
128
|
+
const monitor = createChannelIngressMonitor({
|
|
129
|
+
queue: options.queue ?? getZaloRuntime().state.openChannelIngressQueue({ accountId: options.accountId }),
|
|
130
|
+
inspect: (rawEvent) => inspectZaloWebhookEvent(rawEvent),
|
|
131
|
+
payload: {
|
|
132
|
+
storage: "raw-event",
|
|
133
|
+
version: ZALO_WEBHOOK_SPOOL_VERSION,
|
|
134
|
+
serialize: (rawEvent) => rawEvent,
|
|
135
|
+
deserialize: (rawEvent) => rawEvent,
|
|
136
|
+
createClaimError: (kind) => new ZaloWebhookPayloadError(kind === "invalid-version" ? "Zalo webhook spool payload is invalid." : "Zalo webhook identity changed after durable admission.")
|
|
137
|
+
},
|
|
138
|
+
deliver: async (_rawEvent, lifecycle, claim) => {
|
|
139
|
+
const update = parseClaimedUpdate(claim.payload, claim.id);
|
|
140
|
+
await options.deliver(update, bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle);
|
|
141
|
+
},
|
|
142
|
+
pollIntervalMs: ZALO_WEBHOOK_DRAIN_INTERVAL_MS,
|
|
143
|
+
retention: { failedMaxEntries: 5e3 },
|
|
144
|
+
waitForDeliveryIdleBeforeRepump: false,
|
|
145
|
+
runPumpTask: runDetachedWebhookWork,
|
|
146
|
+
deferredClaims: "wait-on-stop",
|
|
147
|
+
drain: {
|
|
148
|
+
adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
|
|
149
|
+
startLimit: ZALO_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
|
|
150
|
+
retryPolicy: {
|
|
151
|
+
maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
|
|
152
|
+
deadLetterMinAgeMs: 0
|
|
153
|
+
},
|
|
154
|
+
resolveNonRetryableFailure: (error) => {
|
|
155
|
+
if (error instanceof ZaloWebhookPayloadError) return {
|
|
156
|
+
reason: "invalid-event",
|
|
157
|
+
message: error.message
|
|
158
|
+
};
|
|
159
|
+
if (isZaloAuthenticationFailure(error)) return {
|
|
160
|
+
reason: "authentication-failed",
|
|
161
|
+
message: formatErrorMessage(error)
|
|
162
|
+
};
|
|
163
|
+
return null;
|
|
164
|
+
},
|
|
165
|
+
onLog: (message) => options.runtime.error?.(`zalo ingress: ${message}`)
|
|
166
|
+
},
|
|
167
|
+
createStoppedError: () => /* @__PURE__ */ new Error("Zalo ingress stopped."),
|
|
168
|
+
onError: (error) => options.runtime.error?.(`zalo ingress drain failed: ${formatErrorMessage(error)}`)
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
accept: async (rawEvent) => {
|
|
172
|
+
await monitor.admit(rawEvent);
|
|
173
|
+
},
|
|
174
|
+
start: monitor.start,
|
|
175
|
+
stop: monitor.stop
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const zaloWebhookIngressRuntime = { createZaloWebhookIngress };
|
|
179
|
+
//#endregion
|
|
180
|
+
export { ZaloWebhookPayloadError, zaloWebhookIngressRuntime };
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "zalo",
|
|
3
|
+
"doctorContract": {},
|
|
3
4
|
"name": "Zalo",
|
|
4
5
|
"description": "OpenClaw Zalo channel plugin for bot and webhook chats.",
|
|
5
6
|
"icon": "https://cdn.simpleicons.org/zalo",
|
|
@@ -26,6 +27,9 @@
|
|
|
26
27
|
"enabled": {
|
|
27
28
|
"type": "boolean"
|
|
28
29
|
},
|
|
30
|
+
"configWrites": {
|
|
31
|
+
"type": "boolean"
|
|
32
|
+
},
|
|
29
33
|
"markdown": {
|
|
30
34
|
"type": "object",
|
|
31
35
|
"properties": {
|
|
@@ -71,6 +75,29 @@
|
|
|
71
75
|
],
|
|
72
76
|
"additionalProperties": false
|
|
73
77
|
},
|
|
78
|
+
{
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"source": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"const": "store"
|
|
84
|
+
},
|
|
85
|
+
"provider": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"pattern": "^[a-z][a-z0-9_-]{0,63}$"
|
|
88
|
+
},
|
|
89
|
+
"id": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"pattern": "^[A-Z][A-Z0-9_]{0,127}$"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"required": [
|
|
95
|
+
"source",
|
|
96
|
+
"provider",
|
|
97
|
+
"id"
|
|
98
|
+
],
|
|
99
|
+
"additionalProperties": false
|
|
100
|
+
},
|
|
74
101
|
{
|
|
75
102
|
"type": "object",
|
|
76
103
|
"properties": {
|
|
@@ -155,6 +182,29 @@
|
|
|
155
182
|
],
|
|
156
183
|
"additionalProperties": false
|
|
157
184
|
},
|
|
185
|
+
{
|
|
186
|
+
"type": "object",
|
|
187
|
+
"properties": {
|
|
188
|
+
"source": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"const": "store"
|
|
191
|
+
},
|
|
192
|
+
"provider": {
|
|
193
|
+
"type": "string",
|
|
194
|
+
"pattern": "^[a-z][a-z0-9_-]{0,63}$"
|
|
195
|
+
},
|
|
196
|
+
"id": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"pattern": "^[A-Z][A-Z0-9_]{0,127}$"
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
"required": [
|
|
202
|
+
"source",
|
|
203
|
+
"provider",
|
|
204
|
+
"id"
|
|
205
|
+
],
|
|
206
|
+
"additionalProperties": false
|
|
207
|
+
},
|
|
158
208
|
{
|
|
159
209
|
"type": "object",
|
|
160
210
|
"properties": {
|
|
@@ -270,6 +320,9 @@
|
|
|
270
320
|
"enabled": {
|
|
271
321
|
"type": "boolean"
|
|
272
322
|
},
|
|
323
|
+
"configWrites": {
|
|
324
|
+
"type": "boolean"
|
|
325
|
+
},
|
|
273
326
|
"markdown": {
|
|
274
327
|
"type": "object",
|
|
275
328
|
"properties": {
|
|
@@ -315,6 +368,29 @@
|
|
|
315
368
|
],
|
|
316
369
|
"additionalProperties": false
|
|
317
370
|
},
|
|
371
|
+
{
|
|
372
|
+
"type": "object",
|
|
373
|
+
"properties": {
|
|
374
|
+
"source": {
|
|
375
|
+
"type": "string",
|
|
376
|
+
"const": "store"
|
|
377
|
+
},
|
|
378
|
+
"provider": {
|
|
379
|
+
"type": "string",
|
|
380
|
+
"pattern": "^[a-z][a-z0-9_-]{0,63}$"
|
|
381
|
+
},
|
|
382
|
+
"id": {
|
|
383
|
+
"type": "string",
|
|
384
|
+
"pattern": "^[A-Z][A-Z0-9_]{0,127}$"
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
"required": [
|
|
388
|
+
"source",
|
|
389
|
+
"provider",
|
|
390
|
+
"id"
|
|
391
|
+
],
|
|
392
|
+
"additionalProperties": false
|
|
393
|
+
},
|
|
318
394
|
{
|
|
319
395
|
"type": "object",
|
|
320
396
|
"properties": {
|
|
@@ -399,6 +475,29 @@
|
|
|
399
475
|
],
|
|
400
476
|
"additionalProperties": false
|
|
401
477
|
},
|
|
478
|
+
{
|
|
479
|
+
"type": "object",
|
|
480
|
+
"properties": {
|
|
481
|
+
"source": {
|
|
482
|
+
"type": "string",
|
|
483
|
+
"const": "store"
|
|
484
|
+
},
|
|
485
|
+
"provider": {
|
|
486
|
+
"type": "string",
|
|
487
|
+
"pattern": "^[a-z][a-z0-9_-]{0,63}$"
|
|
488
|
+
},
|
|
489
|
+
"id": {
|
|
490
|
+
"type": "string",
|
|
491
|
+
"pattern": "^[A-Z][A-Z0-9_]{0,127}$"
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
"required": [
|
|
495
|
+
"source",
|
|
496
|
+
"provider",
|
|
497
|
+
"id"
|
|
498
|
+
],
|
|
499
|
+
"additionalProperties": false
|
|
500
|
+
},
|
|
402
501
|
{
|
|
403
502
|
"type": "object",
|
|
404
503
|
"properties": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/zalo",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.8.1-beta.3",
|
|
4
4
|
"description": "OpenClaw Zalo channel plugin for bot and webhook chats.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"openclaw": ">=2026.
|
|
11
|
+
"openclaw": ">=2026.8.1-beta.3"
|
|
12
12
|
},
|
|
13
13
|
"peerDependenciesMeta": {
|
|
14
14
|
"openclaw": {
|
|
@@ -69,7 +69,10 @@
|
|
|
69
69
|
"cli": {
|
|
70
70
|
"flags": "--use-env",
|
|
71
71
|
"description": "Use ZALO_BOT_TOKEN"
|
|
72
|
-
}
|
|
72
|
+
},
|
|
73
|
+
"envVars": [
|
|
74
|
+
"ZALO_BOT_TOKEN"
|
|
75
|
+
]
|
|
73
76
|
}
|
|
74
77
|
]
|
|
75
78
|
}
|
|
@@ -80,10 +83,10 @@
|
|
|
80
83
|
"minHostVersion": ">=2026.4.10"
|
|
81
84
|
},
|
|
82
85
|
"compat": {
|
|
83
|
-
"pluginApi": ">=2026.
|
|
86
|
+
"pluginApi": ">=2026.8.1-beta.3"
|
|
84
87
|
},
|
|
85
88
|
"build": {
|
|
86
|
-
"openclawVersion": "2026.
|
|
89
|
+
"openclawVersion": "2026.8.1-beta.3"
|
|
87
90
|
},
|
|
88
91
|
"release": {
|
|
89
92
|
"publishToClawHub": true,
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { t as getZaloRuntime } from "./runtime-api-qxdOyk3M.js";
|
|
2
|
-
import { r as ZaloApiError } from "./send-BUvbbedq.js";
|
|
3
|
-
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
|
|
4
|
-
import { normalizeNullableString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
|
-
import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
6
|
-
import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
7
|
-
//#region extensions/zalo/src/webhook-spool.ts
|
|
8
|
-
const ZALO_WEBHOOK_SPOOL_VERSION = 1;
|
|
9
|
-
const ZALO_WEBHOOK_DRAIN_INTERVAL_MS = 500;
|
|
10
|
-
const ZALO_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
|
|
11
|
-
const ZaloWebhookPayloadError = createChannelIngressError("ZaloWebhookPayloadError");
|
|
12
|
-
function parseRawRecord(rawEvent) {
|
|
13
|
-
let parsed;
|
|
14
|
-
try {
|
|
15
|
-
parsed = JSON.parse(rawEvent);
|
|
16
|
-
} catch (error) {
|
|
17
|
-
throw new ZaloWebhookPayloadError("Zalo webhook body contains invalid JSON.", { cause: error });
|
|
18
|
-
}
|
|
19
|
-
if (!isRecord(parsed)) throw new ZaloWebhookPayloadError("Zalo webhook body must be a JSON object.");
|
|
20
|
-
return parsed;
|
|
21
|
-
}
|
|
22
|
-
function resolveUpdateRecord(envelope) {
|
|
23
|
-
if (envelope.ok === true && isRecord(envelope.result)) return envelope.result;
|
|
24
|
-
return envelope;
|
|
25
|
-
}
|
|
26
|
-
function inspectZaloWebhookEvent(rawEvent) {
|
|
27
|
-
const update = resolveUpdateRecord(parseRawRecord(rawEvent));
|
|
28
|
-
const message = isRecord(update.message) ? update.message : null;
|
|
29
|
-
const eventId = normalizeNullableString(message?.message_id);
|
|
30
|
-
if (!eventId) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.message_id.");
|
|
31
|
-
const chatId = normalizeNullableString((isRecord(message?.chat) ? message.chat : null)?.id);
|
|
32
|
-
if (!chatId) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.chat.id.");
|
|
33
|
-
return {
|
|
34
|
-
eventId,
|
|
35
|
-
laneKey: `chat:${chatId}`,
|
|
36
|
-
update
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
function parseClaimedUpdate(payload, claimedId) {
|
|
40
|
-
if (payload.version !== ZALO_WEBHOOK_SPOOL_VERSION || typeof payload.rawEvent !== "string") throw new ZaloWebhookPayloadError("Zalo webhook spool payload is invalid.");
|
|
41
|
-
const facts = inspectZaloWebhookEvent(payload.rawEvent);
|
|
42
|
-
if (facts.eventId !== claimedId) throw new ZaloWebhookPayloadError("Zalo webhook message id changed after durable admission.");
|
|
43
|
-
const eventName = normalizeNullableString(facts.update.event_name);
|
|
44
|
-
if (eventName !== "message.text.received" && eventName !== "message.image.received" && eventName !== "message.sticker.received" && eventName !== "message.unsupported.received") throw new ZaloWebhookPayloadError("Zalo webhook event_name is unsupported.");
|
|
45
|
-
const message = facts.update.message;
|
|
46
|
-
const from = isRecord(message.from) ? message.from : null;
|
|
47
|
-
const chat = isRecord(message.chat) ? message.chat : null;
|
|
48
|
-
if (!normalizeNullableString(from?.id)) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.from.id.");
|
|
49
|
-
if (chat?.chat_type !== "PRIVATE" && chat?.chat_type !== "GROUP") throw new ZaloWebhookPayloadError("Zalo webhook message has an invalid chat type.");
|
|
50
|
-
if (typeof message.date !== "number" || !Number.isFinite(message.date)) throw new ZaloWebhookPayloadError("Zalo webhook message has an invalid date.");
|
|
51
|
-
if (eventName === "message.text.received" && typeof message.text !== "string") throw new ZaloWebhookPayloadError("Zalo text event is missing message.text.");
|
|
52
|
-
return facts.update;
|
|
53
|
-
}
|
|
54
|
-
function errorText(error) {
|
|
55
|
-
return error instanceof Error ? error.message : String(error);
|
|
56
|
-
}
|
|
57
|
-
function isZaloAuthenticationFailure(error) {
|
|
58
|
-
let current = error;
|
|
59
|
-
const seen = /* @__PURE__ */ new Set();
|
|
60
|
-
while (current && typeof current === "object" && !seen.has(current)) {
|
|
61
|
-
seen.add(current);
|
|
62
|
-
const candidate = current;
|
|
63
|
-
if (current instanceof ZaloApiError && (current.errorCode === 401 || current.errorCode === 403) || candidate.status === 401 || candidate.status === 403 || candidate.statusCode === 401 || candidate.statusCode === 403) return true;
|
|
64
|
-
current = candidate.cause;
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
function createZaloWebhookIngress(options) {
|
|
69
|
-
const monitor = createChannelIngressMonitor({
|
|
70
|
-
queue: options.queue ?? getZaloRuntime().state.openChannelIngressQueue({ accountId: options.accountId }),
|
|
71
|
-
inspect: (rawEvent) => inspectZaloWebhookEvent(rawEvent),
|
|
72
|
-
payload: {
|
|
73
|
-
storage: "raw-event",
|
|
74
|
-
version: ZALO_WEBHOOK_SPOOL_VERSION,
|
|
75
|
-
serialize: (rawEvent) => rawEvent,
|
|
76
|
-
deserialize: (rawEvent) => rawEvent,
|
|
77
|
-
createClaimError: (kind) => new ZaloWebhookPayloadError(kind === "invalid-version" ? "Zalo webhook spool payload is invalid." : "Zalo webhook identity changed after durable admission.")
|
|
78
|
-
},
|
|
79
|
-
deliver: async (_rawEvent, lifecycle, claim) => {
|
|
80
|
-
const update = parseClaimedUpdate(claim.payload, claim.id);
|
|
81
|
-
await options.deliver(update, bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle);
|
|
82
|
-
},
|
|
83
|
-
pollIntervalMs: ZALO_WEBHOOK_DRAIN_INTERVAL_MS,
|
|
84
|
-
retention: { failedMaxEntries: 5e3 },
|
|
85
|
-
waitForDeliveryIdleBeforeRepump: false,
|
|
86
|
-
runPumpTask: runDetachedWebhookWork,
|
|
87
|
-
deferredClaims: "wait-on-stop",
|
|
88
|
-
drain: {
|
|
89
|
-
adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
|
|
90
|
-
startLimit: ZALO_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
|
|
91
|
-
retryPolicy: {
|
|
92
|
-
maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
|
|
93
|
-
deadLetterMinAgeMs: 0
|
|
94
|
-
},
|
|
95
|
-
resolveNonRetryableFailure: (error) => {
|
|
96
|
-
if (error instanceof ZaloWebhookPayloadError) return {
|
|
97
|
-
reason: "invalid-event",
|
|
98
|
-
message: error.message
|
|
99
|
-
};
|
|
100
|
-
if (isZaloAuthenticationFailure(error)) return {
|
|
101
|
-
reason: "authentication-failed",
|
|
102
|
-
message: errorText(error)
|
|
103
|
-
};
|
|
104
|
-
return null;
|
|
105
|
-
},
|
|
106
|
-
onLog: (message) => options.runtime.error?.(`zalo ingress: ${message}`)
|
|
107
|
-
},
|
|
108
|
-
createStoppedError: () => /* @__PURE__ */ new Error("Zalo ingress stopped."),
|
|
109
|
-
onError: (error) => options.runtime.error?.(`zalo ingress drain failed: ${errorText(error)}`)
|
|
110
|
-
});
|
|
111
|
-
return {
|
|
112
|
-
accept: async (rawEvent) => {
|
|
113
|
-
await monitor.admit(rawEvent);
|
|
114
|
-
},
|
|
115
|
-
start: monitor.start,
|
|
116
|
-
stop: monitor.stop
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
const zaloWebhookIngressRuntime = { createZaloWebhookIngress };
|
|
120
|
-
//#endregion
|
|
121
|
-
export { ZaloWebhookPayloadError, zaloWebhookIngressRuntime };
|
|
@@ -4,8 +4,8 @@ import { PAIRING_APPROVED_MESSAGE, buildTokenChannelStatusSummary as buildTokenC
|
|
|
4
4
|
import { deliverTextOrMediaReply as deliverTextOrMediaReply$1, isNumericTargetId, sendPayloadWithChunkedTextAndMedia as sendPayloadWithChunkedTextAndMedia$1 } from "openclaw/plugin-sdk/reply-payload";
|
|
5
5
|
import { buildBaseAccountStatusSnapshot } from "openclaw/plugin-sdk/status-helpers";
|
|
6
6
|
import { chunkTextForOutbound as chunkTextForOutbound$1 } from "openclaw/plugin-sdk/text-chunking";
|
|
7
|
-
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildChannelConfigSchema, createDedupeCache, formatPairingApproveHint, jsonResult, normalizeAccountId as normalizeAccountId$1, readStringParam, resolveClientIp } from "openclaw/plugin-sdk/core";
|
|
8
7
|
import { buildSecretInputSchema, hasConfiguredSecretInput as hasConfiguredSecretInput$1, normalizeResolvedSecretInputString, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
8
|
+
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildChannelConfigSchema, createDedupeCache, formatPairingApproveHint, jsonResult, normalizeAccountId as normalizeAccountId$1, readStringParam, resolveClientIp } from "openclaw/plugin-sdk/core";
|
|
9
9
|
import { addWildcardAllowFrom, applyAccountNameToChannelSection, applySetupAccountConfigPatch, buildSingleChannelSecretPromptState as buildSingleChannelSecretPromptState$1, mergeAllowFromEntries as mergeAllowFromEntries$1, migrateBaseNameToDefaultAccount, promptSingleChannelSecretInput as promptSingleChannelSecretInput$1, runSingleChannelSecretStep as runSingleChannelSecretStep$1, setTopLevelChannelDmPolicyWithAllowFrom } from "openclaw/plugin-sdk/setup";
|
|
10
10
|
import { resolveDefaultGroupPolicy as resolveDefaultGroupPolicy$1, resolveOpenProviderRuntimeGroupPolicy as resolveOpenProviderRuntimeGroupPolicy$1, warnMissingProviderGroupPolicyFallbackOnce as warnMissingProviderGroupPolicyFallbackOnce$1 } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
11
11
|
import { createChannelPairingController as createChannelPairingController$1 } from "openclaw/plugin-sdk/channel-pairing";
|