@openclaw/zalo 2026.3.1 → 2026.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/index.ts +2 -4
- package/package.json +3 -2
- package/src/accounts.ts +7 -37
- package/src/actions.ts +2 -2
- package/src/channel.directory.test.ts +1 -1
- package/src/channel.sendpayload.test.ts +102 -0
- package/src/channel.ts +98 -127
- package/src/config-schema.test.ts +30 -0
- package/src/config-schema.ts +4 -3
- package/src/group-access.ts +2 -2
- package/src/monitor.ts +85 -78
- package/src/monitor.webhook.test.ts +159 -36
- package/src/monitor.webhook.ts +98 -94
- package/src/onboarding.status.test.ts +24 -0
- package/src/onboarding.ts +117 -93
- package/src/probe.ts +1 -1
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +13 -0
- package/src/send.ts +29 -24
- package/src/status-issues.ts +1 -1
- package/src/token.test.ts +58 -0
- package/src/token.ts +64 -29
- package/src/types.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.3.7
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.3.3
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.3.2
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
3
21
|
## 2026.3.1
|
|
4
22
|
|
|
5
23
|
### Changes
|
package/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/zalo";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/zalo";
|
|
3
3
|
import { zaloDock, zaloPlugin } from "./src/channel.js";
|
|
4
|
-
import { handleZaloWebhookRequest } from "./src/monitor.js";
|
|
5
4
|
import { setZaloRuntime } from "./src/runtime.js";
|
|
6
5
|
|
|
7
6
|
const plugin = {
|
|
@@ -12,7 +11,6 @@ const plugin = {
|
|
|
12
11
|
register(api: OpenClawPluginApi) {
|
|
13
12
|
setZaloRuntime(api.runtime);
|
|
14
13
|
api.registerChannel({ plugin: zaloPlugin, dock: zaloDock });
|
|
15
|
-
api.registerHttpHandler(handleZaloWebhookRequest);
|
|
16
14
|
},
|
|
17
15
|
};
|
|
18
16
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/zalo",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.7",
|
|
4
4
|
"description": "OpenClaw Zalo channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"undici": "7.22.0"
|
|
7
|
+
"undici": "7.22.0",
|
|
8
|
+
"zod": "^4.3.6"
|
|
8
9
|
},
|
|
9
10
|
"openclaw": {
|
|
10
11
|
"extensions": [
|
package/src/accounts.ts
CHANGED
|
@@ -1,45 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_ACCOUNT_ID,
|
|
4
|
-
normalizeAccountId,
|
|
5
|
-
normalizeOptionalAccountId,
|
|
6
|
-
} from "openclaw/plugin-sdk/account-id";
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import { createAccountListHelpers, type OpenClawConfig } from "openclaw/plugin-sdk/zalo";
|
|
7
3
|
import { resolveZaloToken } from "./token.js";
|
|
8
4
|
import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js";
|
|
9
5
|
|
|
10
6
|
export type { ResolvedZaloAccount };
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
return Object.keys(accounts).filter(Boolean);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function listZaloAccountIds(cfg: OpenClawConfig): string[] {
|
|
21
|
-
const ids = listConfiguredAccountIds(cfg);
|
|
22
|
-
if (ids.length === 0) {
|
|
23
|
-
return [DEFAULT_ACCOUNT_ID];
|
|
24
|
-
}
|
|
25
|
-
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function resolveDefaultZaloAccountId(cfg: OpenClawConfig): string {
|
|
29
|
-
const zaloConfig = cfg.channels?.zalo as ZaloConfig | undefined;
|
|
30
|
-
const preferred = normalizeOptionalAccountId(zaloConfig?.defaultAccount);
|
|
31
|
-
if (
|
|
32
|
-
preferred &&
|
|
33
|
-
listZaloAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
|
34
|
-
) {
|
|
35
|
-
return preferred;
|
|
36
|
-
}
|
|
37
|
-
const ids = listZaloAccountIds(cfg);
|
|
38
|
-
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
39
|
-
return DEFAULT_ACCOUNT_ID;
|
|
40
|
-
}
|
|
41
|
-
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
42
|
-
}
|
|
8
|
+
const { listAccountIds: listZaloAccountIds, resolveDefaultAccountId: resolveDefaultZaloAccountId } =
|
|
9
|
+
createAccountListHelpers("zalo");
|
|
10
|
+
export { listZaloAccountIds, resolveDefaultZaloAccountId };
|
|
43
11
|
|
|
44
12
|
function resolveAccountConfig(
|
|
45
13
|
cfg: OpenClawConfig,
|
|
@@ -62,6 +30,7 @@ function mergeZaloAccountConfig(cfg: OpenClawConfig, accountId: string): ZaloAcc
|
|
|
62
30
|
export function resolveZaloAccount(params: {
|
|
63
31
|
cfg: OpenClawConfig;
|
|
64
32
|
accountId?: string | null;
|
|
33
|
+
allowUnresolvedSecretRef?: boolean;
|
|
65
34
|
}): ResolvedZaloAccount {
|
|
66
35
|
const accountId = normalizeAccountId(params.accountId);
|
|
67
36
|
const baseEnabled = (params.cfg.channels?.zalo as ZaloConfig | undefined)?.enabled !== false;
|
|
@@ -71,6 +40,7 @@ export function resolveZaloAccount(params: {
|
|
|
71
40
|
const tokenResolution = resolveZaloToken(
|
|
72
41
|
params.cfg.channels?.zalo as ZaloConfig | undefined,
|
|
73
42
|
accountId,
|
|
43
|
+
{ allowUnresolvedSecretRef: params.allowUnresolvedSecretRef },
|
|
74
44
|
);
|
|
75
45
|
|
|
76
46
|
return {
|
package/src/actions.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type {
|
|
|
2
2
|
ChannelMessageActionAdapter,
|
|
3
3
|
ChannelMessageActionName,
|
|
4
4
|
OpenClawConfig,
|
|
5
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
-
import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk";
|
|
5
|
+
} from "openclaw/plugin-sdk/zalo";
|
|
6
|
+
import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk/zalo";
|
|
7
7
|
import { listEnabledZaloAccounts } from "./accounts.js";
|
|
8
8
|
import { sendMessageZalo } from "./send.js";
|
|
9
9
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ReplyPayload } from "openclaw/plugin-sdk/zalo";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { zaloPlugin } from "./channel.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("./send.js", () => ({
|
|
6
|
+
sendMessageZalo: vi.fn().mockResolvedValue({ ok: true, messageId: "zl-1" }),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
function baseCtx(payload: ReplyPayload) {
|
|
10
|
+
return {
|
|
11
|
+
cfg: {},
|
|
12
|
+
to: "123456789",
|
|
13
|
+
text: "",
|
|
14
|
+
payload,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("zaloPlugin outbound sendPayload", () => {
|
|
19
|
+
let mockedSend: ReturnType<typeof vi.mocked<(typeof import("./send.js"))["sendMessageZalo"]>>;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
const mod = await import("./send.js");
|
|
23
|
+
mockedSend = vi.mocked(mod.sendMessageZalo);
|
|
24
|
+
mockedSend.mockClear();
|
|
25
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zl-1" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("text-only delegates to sendText", async () => {
|
|
29
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zl-t1" });
|
|
30
|
+
|
|
31
|
+
const result = await zaloPlugin.outbound!.sendPayload!(baseCtx({ text: "hello" }));
|
|
32
|
+
|
|
33
|
+
expect(mockedSend).toHaveBeenCalledWith("123456789", "hello", expect.any(Object));
|
|
34
|
+
expect(result).toMatchObject({ channel: "zalo", messageId: "zl-t1" });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("single media delegates to sendMedia", async () => {
|
|
38
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zl-m1" });
|
|
39
|
+
|
|
40
|
+
const result = await zaloPlugin.outbound!.sendPayload!(
|
|
41
|
+
baseCtx({ text: "cap", mediaUrl: "https://example.com/a.jpg" }),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(mockedSend).toHaveBeenCalledWith(
|
|
45
|
+
"123456789",
|
|
46
|
+
"cap",
|
|
47
|
+
expect.objectContaining({ mediaUrl: "https://example.com/a.jpg" }),
|
|
48
|
+
);
|
|
49
|
+
expect(result).toMatchObject({ channel: "zalo" });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("multi-media iterates URLs with caption on first", async () => {
|
|
53
|
+
mockedSend
|
|
54
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zl-1" })
|
|
55
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zl-2" });
|
|
56
|
+
|
|
57
|
+
const result = await zaloPlugin.outbound!.sendPayload!(
|
|
58
|
+
baseCtx({
|
|
59
|
+
text: "caption",
|
|
60
|
+
mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"],
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(mockedSend).toHaveBeenCalledTimes(2);
|
|
65
|
+
expect(mockedSend).toHaveBeenNthCalledWith(
|
|
66
|
+
1,
|
|
67
|
+
"123456789",
|
|
68
|
+
"caption",
|
|
69
|
+
expect.objectContaining({ mediaUrl: "https://example.com/1.jpg" }),
|
|
70
|
+
);
|
|
71
|
+
expect(mockedSend).toHaveBeenNthCalledWith(
|
|
72
|
+
2,
|
|
73
|
+
"123456789",
|
|
74
|
+
"",
|
|
75
|
+
expect.objectContaining({ mediaUrl: "https://example.com/2.jpg" }),
|
|
76
|
+
);
|
|
77
|
+
expect(result).toMatchObject({ channel: "zalo", messageId: "zl-2" });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("empty payload returns no-op", async () => {
|
|
81
|
+
const result = await zaloPlugin.outbound!.sendPayload!(baseCtx({}));
|
|
82
|
+
|
|
83
|
+
expect(mockedSend).not.toHaveBeenCalled();
|
|
84
|
+
expect(result).toEqual({ channel: "zalo", messageId: "" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("chunking splits long text", async () => {
|
|
88
|
+
mockedSend
|
|
89
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zl-c1" })
|
|
90
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zl-c2" });
|
|
91
|
+
|
|
92
|
+
const longText = "a".repeat(3000);
|
|
93
|
+
const result = await zaloPlugin.outbound!.sendPayload!(baseCtx({ text: longText }));
|
|
94
|
+
|
|
95
|
+
// textChunkLimit is 2000 with chunkTextForOutbound, so it should split
|
|
96
|
+
expect(mockedSend.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
97
|
+
for (const call of mockedSend.mock.calls) {
|
|
98
|
+
expect((call[1] as string).length).toBeLessThanOrEqual(2000);
|
|
99
|
+
}
|
|
100
|
+
expect(result).toMatchObject({ channel: "zalo" });
|
|
101
|
+
});
|
|
102
|
+
});
|
package/src/channel.ts
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildAccountScopedDmSecurityPolicy,
|
|
3
|
+
collectOpenProviderGroupPolicyWarnings,
|
|
4
|
+
buildOpenGroupPolicyRestrictSendersWarning,
|
|
5
|
+
buildOpenGroupPolicyWarning,
|
|
6
|
+
mapAllowFromEntries,
|
|
7
|
+
} from "openclaw/plugin-sdk/compat";
|
|
1
8
|
import type {
|
|
2
9
|
ChannelAccountSnapshot,
|
|
3
10
|
ChannelDock,
|
|
4
11
|
ChannelPlugin,
|
|
5
12
|
OpenClawConfig,
|
|
6
|
-
} from "openclaw/plugin-sdk";
|
|
13
|
+
} from "openclaw/plugin-sdk/zalo";
|
|
7
14
|
import {
|
|
8
15
|
applyAccountNameToChannelSection,
|
|
16
|
+
applySetupAccountConfigPatch,
|
|
17
|
+
buildBaseAccountStatusSnapshot,
|
|
9
18
|
buildChannelConfigSchema,
|
|
10
19
|
buildTokenChannelStatusSummary,
|
|
20
|
+
buildChannelSendResult,
|
|
11
21
|
DEFAULT_ACCOUNT_ID,
|
|
12
22
|
deleteAccountFromConfigSection,
|
|
13
23
|
chunkTextForOutbound,
|
|
14
24
|
formatAllowFromLowercase,
|
|
15
|
-
formatPairingApproveHint,
|
|
16
25
|
migrateBaseNameToDefaultAccount,
|
|
26
|
+
listDirectoryUserEntriesFromAllowFrom,
|
|
17
27
|
normalizeAccountId,
|
|
28
|
+
isNumericTargetId,
|
|
18
29
|
PAIRING_APPROVED_MESSAGE,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
resolveChannelAccountConfigBasePath,
|
|
30
|
+
resolveOutboundMediaUrls,
|
|
31
|
+
sendPayloadWithChunkedTextAndMedia,
|
|
22
32
|
setAccountEnabledInConfigSection,
|
|
23
|
-
} from "openclaw/plugin-sdk";
|
|
33
|
+
} from "openclaw/plugin-sdk/zalo";
|
|
24
34
|
import {
|
|
25
35
|
listZaloAccountIds,
|
|
26
36
|
resolveDefaultZaloAccountId,
|
|
@@ -32,6 +42,7 @@ import { ZaloConfigSchema } from "./config-schema.js";
|
|
|
32
42
|
import { zaloOnboardingAdapter } from "./onboarding.js";
|
|
33
43
|
import { probeZalo } from "./probe.js";
|
|
34
44
|
import { resolveZaloProxyFetch } from "./proxy.js";
|
|
45
|
+
import { normalizeSecretInputString } from "./secret-input.js";
|
|
35
46
|
import { sendMessageZalo } from "./send.js";
|
|
36
47
|
import { collectZaloStatusIssues } from "./status-issues.js";
|
|
37
48
|
|
|
@@ -65,9 +76,7 @@ export const zaloDock: ChannelDock = {
|
|
|
65
76
|
outbound: { textChunkLimit: 2000 },
|
|
66
77
|
config: {
|
|
67
78
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
68
|
-
(resolveZaloAccount({ cfg: cfg, accountId }).config.allowFrom
|
|
69
|
-
String(entry),
|
|
70
|
-
),
|
|
79
|
+
mapAllowFromEntries(resolveZaloAccount({ cfg: cfg, accountId }).config.allowFrom),
|
|
71
80
|
formatAllowFrom: ({ allowFrom }) =>
|
|
72
81
|
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalo|zl):/i }),
|
|
73
82
|
},
|
|
@@ -122,53 +131,57 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
122
131
|
tokenSource: account.tokenSource,
|
|
123
132
|
}),
|
|
124
133
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
125
|
-
(resolveZaloAccount({ cfg: cfg, accountId }).config.allowFrom
|
|
126
|
-
String(entry),
|
|
127
|
-
),
|
|
134
|
+
mapAllowFromEntries(resolveZaloAccount({ cfg: cfg, accountId }).config.allowFrom),
|
|
128
135
|
formatAllowFrom: ({ allowFrom }) =>
|
|
129
136
|
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalo|zl):/i }),
|
|
130
137
|
},
|
|
131
138
|
security: {
|
|
132
139
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
133
|
-
|
|
134
|
-
const basePath = resolveChannelAccountConfigBasePath({
|
|
140
|
+
return buildAccountScopedDmSecurityPolicy({
|
|
135
141
|
cfg,
|
|
136
142
|
channelKey: "zalo",
|
|
137
|
-
accountId
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
policy: account.config.dmPolicy ?? "pairing",
|
|
143
|
+
accountId,
|
|
144
|
+
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
145
|
+
policy: account.config.dmPolicy,
|
|
141
146
|
allowFrom: account.config.allowFrom ?? [],
|
|
142
|
-
|
|
143
|
-
allowFromPath: basePath,
|
|
144
|
-
approveHint: formatPairingApproveHint("zalo"),
|
|
147
|
+
policyPathSuffix: "dmPolicy",
|
|
145
148
|
normalizeEntry: (raw) => raw.replace(/^(zalo|zl):/i, ""),
|
|
146
|
-
};
|
|
149
|
+
});
|
|
147
150
|
},
|
|
148
151
|
collectWarnings: ({ account, cfg }) => {
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
return collectOpenProviderGroupPolicyWarnings({
|
|
153
|
+
cfg,
|
|
151
154
|
providerConfigPresent: cfg.channels?.zalo !== undefined,
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
configuredGroupPolicy: account.config.groupPolicy,
|
|
156
|
+
collect: (groupPolicy) => {
|
|
157
|
+
if (groupPolicy !== "open") {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const explicitGroupAllowFrom = mapAllowFromEntries(account.config.groupAllowFrom);
|
|
161
|
+
const dmAllowFrom = mapAllowFromEntries(account.config.allowFrom);
|
|
162
|
+
const effectiveAllowFrom =
|
|
163
|
+
explicitGroupAllowFrom.length > 0 ? explicitGroupAllowFrom : dmAllowFrom;
|
|
164
|
+
if (effectiveAllowFrom.length > 0) {
|
|
165
|
+
return [
|
|
166
|
+
buildOpenGroupPolicyRestrictSendersWarning({
|
|
167
|
+
surface: "Zalo groups",
|
|
168
|
+
openScope: "any member",
|
|
169
|
+
groupPolicyPath: "channels.zalo.groupPolicy",
|
|
170
|
+
groupAllowFromPath: "channels.zalo.groupAllowFrom",
|
|
171
|
+
}),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
return [
|
|
175
|
+
buildOpenGroupPolicyWarning({
|
|
176
|
+
surface: "Zalo groups",
|
|
177
|
+
openBehavior:
|
|
178
|
+
"with no groupAllowFrom/allowFrom allowlist; any member can trigger (mention-gated)",
|
|
179
|
+
remediation:
|
|
180
|
+
'Set channels.zalo.groupPolicy="allowlist" + channels.zalo.groupAllowFrom',
|
|
181
|
+
}),
|
|
182
|
+
];
|
|
183
|
+
},
|
|
154
184
|
});
|
|
155
|
-
if (groupPolicy !== "open") {
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
158
|
-
const explicitGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((entry) =>
|
|
159
|
-
String(entry),
|
|
160
|
-
);
|
|
161
|
-
const dmAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
|
|
162
|
-
const effectiveAllowFrom =
|
|
163
|
-
explicitGroupAllowFrom.length > 0 ? explicitGroupAllowFrom : dmAllowFrom;
|
|
164
|
-
if (effectiveAllowFrom.length > 0) {
|
|
165
|
-
return [
|
|
166
|
-
`- Zalo groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.zalo.groupPolicy="allowlist" + channels.zalo.groupAllowFrom to restrict senders.`,
|
|
167
|
-
];
|
|
168
|
-
}
|
|
169
|
-
return [
|
|
170
|
-
`- Zalo groups: groupPolicy="open" with no groupAllowFrom/allowFrom allowlist; any member can trigger (mention-gated). Set channels.zalo.groupPolicy="allowlist" + channels.zalo.groupAllowFrom.`,
|
|
171
|
-
];
|
|
172
185
|
},
|
|
173
186
|
},
|
|
174
187
|
groups: {
|
|
@@ -181,13 +194,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
181
194
|
messaging: {
|
|
182
195
|
normalizeTarget: normalizeZaloMessagingTarget,
|
|
183
196
|
targetResolver: {
|
|
184
|
-
looksLikeId:
|
|
185
|
-
const trimmed = raw.trim();
|
|
186
|
-
if (!trimmed) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
return /^\d{3,}$/.test(trimmed);
|
|
190
|
-
},
|
|
197
|
+
looksLikeId: isNumericTargetId,
|
|
191
198
|
hint: "<chatId>",
|
|
192
199
|
},
|
|
193
200
|
},
|
|
@@ -195,19 +202,12 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
195
202
|
self: async () => null,
|
|
196
203
|
listPeers: async ({ cfg, accountId, query, limit }) => {
|
|
197
204
|
const account = resolveZaloAccount({ cfg: cfg, accountId });
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.map((entry) => entry.replace(/^(zalo|zl):/i, "")),
|
|
205
|
-
),
|
|
206
|
-
)
|
|
207
|
-
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
|
208
|
-
.slice(0, limit && limit > 0 ? limit : undefined)
|
|
209
|
-
.map((id) => ({ kind: "user", id }) as const);
|
|
210
|
-
return peers;
|
|
205
|
+
return listDirectoryUserEntriesFromAllowFrom({
|
|
206
|
+
allowFrom: account.config.allowFrom,
|
|
207
|
+
query,
|
|
208
|
+
limit,
|
|
209
|
+
normalizeId: (entry) => entry.replace(/^(zalo|zl):/i, ""),
|
|
210
|
+
});
|
|
211
211
|
},
|
|
212
212
|
listGroups: async () => [],
|
|
213
213
|
},
|
|
@@ -243,47 +243,19 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
243
243
|
channelKey: "zalo",
|
|
244
244
|
})
|
|
245
245
|
: namedConfig;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
? { botToken: input.token }
|
|
260
|
-
: {}),
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
} as OpenClawConfig;
|
|
264
|
-
}
|
|
265
|
-
return {
|
|
266
|
-
...next,
|
|
267
|
-
channels: {
|
|
268
|
-
...next.channels,
|
|
269
|
-
zalo: {
|
|
270
|
-
...next.channels?.zalo,
|
|
271
|
-
enabled: true,
|
|
272
|
-
accounts: {
|
|
273
|
-
...next.channels?.zalo?.accounts,
|
|
274
|
-
[accountId]: {
|
|
275
|
-
...next.channels?.zalo?.accounts?.[accountId],
|
|
276
|
-
enabled: true,
|
|
277
|
-
...(input.tokenFile
|
|
278
|
-
? { tokenFile: input.tokenFile }
|
|
279
|
-
: input.token
|
|
280
|
-
? { botToken: input.token }
|
|
281
|
-
: {}),
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
},
|
|
286
|
-
} as OpenClawConfig;
|
|
246
|
+
const patch = input.useEnv
|
|
247
|
+
? {}
|
|
248
|
+
: input.tokenFile
|
|
249
|
+
? { tokenFile: input.tokenFile }
|
|
250
|
+
: input.token
|
|
251
|
+
? { botToken: input.token }
|
|
252
|
+
: {};
|
|
253
|
+
return applySetupAccountConfigPatch({
|
|
254
|
+
cfg: next,
|
|
255
|
+
channelKey: "zalo",
|
|
256
|
+
accountId,
|
|
257
|
+
patch,
|
|
258
|
+
});
|
|
287
259
|
},
|
|
288
260
|
},
|
|
289
261
|
pairing: {
|
|
@@ -302,17 +274,21 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
302
274
|
chunker: chunkTextForOutbound,
|
|
303
275
|
chunkerMode: "text",
|
|
304
276
|
textChunkLimit: 2000,
|
|
277
|
+
sendPayload: async (ctx) =>
|
|
278
|
+
await sendPayloadWithChunkedTextAndMedia({
|
|
279
|
+
ctx,
|
|
280
|
+
textChunkLimit: zaloPlugin.outbound!.textChunkLimit,
|
|
281
|
+
chunker: zaloPlugin.outbound!.chunker,
|
|
282
|
+
sendText: (nextCtx) => zaloPlugin.outbound!.sendText!(nextCtx),
|
|
283
|
+
sendMedia: (nextCtx) => zaloPlugin.outbound!.sendMedia!(nextCtx),
|
|
284
|
+
emptyResult: { channel: "zalo", messageId: "" },
|
|
285
|
+
}),
|
|
305
286
|
sendText: async ({ to, text, accountId, cfg }) => {
|
|
306
287
|
const result = await sendMessageZalo(to, text, {
|
|
307
288
|
accountId: accountId ?? undefined,
|
|
308
289
|
cfg: cfg,
|
|
309
290
|
});
|
|
310
|
-
return
|
|
311
|
-
channel: "zalo",
|
|
312
|
-
ok: result.ok,
|
|
313
|
-
messageId: result.messageId ?? "",
|
|
314
|
-
error: result.error ? new Error(result.error) : undefined,
|
|
315
|
-
};
|
|
291
|
+
return buildChannelSendResult("zalo", result);
|
|
316
292
|
},
|
|
317
293
|
sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => {
|
|
318
294
|
const result = await sendMessageZalo(to, text, {
|
|
@@ -320,12 +296,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
320
296
|
mediaUrl,
|
|
321
297
|
cfg: cfg,
|
|
322
298
|
});
|
|
323
|
-
return
|
|
324
|
-
channel: "zalo",
|
|
325
|
-
ok: result.ok,
|
|
326
|
-
messageId: result.messageId ?? "",
|
|
327
|
-
error: result.error ? new Error(result.error) : undefined,
|
|
328
|
-
};
|
|
299
|
+
return buildChannelSendResult("zalo", result);
|
|
329
300
|
},
|
|
330
301
|
},
|
|
331
302
|
status: {
|
|
@@ -342,19 +313,19 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
342
313
|
probeZalo(account.token, timeoutMs, resolveZaloProxyFetch(account.config.proxy)),
|
|
343
314
|
buildAccountSnapshot: ({ account, runtime }) => {
|
|
344
315
|
const configured = Boolean(account.token?.trim());
|
|
316
|
+
const base = buildBaseAccountStatusSnapshot({
|
|
317
|
+
account: {
|
|
318
|
+
accountId: account.accountId,
|
|
319
|
+
name: account.name,
|
|
320
|
+
enabled: account.enabled,
|
|
321
|
+
configured,
|
|
322
|
+
},
|
|
323
|
+
runtime,
|
|
324
|
+
});
|
|
345
325
|
return {
|
|
346
|
-
|
|
347
|
-
name: account.name,
|
|
348
|
-
enabled: account.enabled,
|
|
349
|
-
configured,
|
|
326
|
+
...base,
|
|
350
327
|
tokenSource: account.tokenSource,
|
|
351
|
-
running: runtime?.running ?? false,
|
|
352
|
-
lastStartAt: runtime?.lastStartAt ?? null,
|
|
353
|
-
lastStopAt: runtime?.lastStopAt ?? null,
|
|
354
|
-
lastError: runtime?.lastError ?? null,
|
|
355
328
|
mode: account.config.webhookUrl ? "webhook" : "polling",
|
|
356
|
-
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
357
|
-
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
358
329
|
dmPolicy: account.config.dmPolicy ?? "pairing",
|
|
359
330
|
};
|
|
360
331
|
},
|
|
@@ -388,7 +359,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
388
359
|
abortSignal: ctx.abortSignal,
|
|
389
360
|
useWebhook: Boolean(account.config.webhookUrl),
|
|
390
361
|
webhookUrl: account.config.webhookUrl,
|
|
391
|
-
webhookSecret: account.config.webhookSecret,
|
|
362
|
+
webhookSecret: normalizeSecretInputString(account.config.webhookSecret),
|
|
392
363
|
webhookPath: account.config.webhookPath,
|
|
393
364
|
fetcher,
|
|
394
365
|
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ZaloConfigSchema } from "./config-schema.js";
|
|
3
|
+
|
|
4
|
+
describe("ZaloConfigSchema SecretInput", () => {
|
|
5
|
+
it("accepts SecretRef botToken and webhookSecret at top-level", () => {
|
|
6
|
+
const result = ZaloConfigSchema.safeParse({
|
|
7
|
+
botToken: { source: "env", provider: "default", id: "ZALO_BOT_TOKEN" },
|
|
8
|
+
webhookUrl: "https://example.com/zalo",
|
|
9
|
+
webhookSecret: { source: "env", provider: "default", id: "ZALO_WEBHOOK_SECRET" },
|
|
10
|
+
});
|
|
11
|
+
expect(result.success).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("accepts SecretRef botToken and webhookSecret on account", () => {
|
|
15
|
+
const result = ZaloConfigSchema.safeParse({
|
|
16
|
+
accounts: {
|
|
17
|
+
work: {
|
|
18
|
+
botToken: { source: "env", provider: "default", id: "ZALO_WORK_BOT_TOKEN" },
|
|
19
|
+
webhookUrl: "https://example.com/zalo/work",
|
|
20
|
+
webhookSecret: {
|
|
21
|
+
source: "env",
|
|
22
|
+
provider: "default",
|
|
23
|
+
id: "ZALO_WORK_WEBHOOK_SECRET",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
expect(result.success).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/config-schema.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { MarkdownConfigSchema } from "openclaw/plugin-sdk";
|
|
1
|
+
import { MarkdownConfigSchema } from "openclaw/plugin-sdk/zalo";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { buildSecretInputSchema } from "./secret-input.js";
|
|
3
4
|
|
|
4
5
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
|
5
6
|
|
|
@@ -7,10 +8,10 @@ const zaloAccountSchema = z.object({
|
|
|
7
8
|
name: z.string().optional(),
|
|
8
9
|
enabled: z.boolean().optional(),
|
|
9
10
|
markdown: MarkdownConfigSchema,
|
|
10
|
-
botToken:
|
|
11
|
+
botToken: buildSecretInputSchema().optional(),
|
|
11
12
|
tokenFile: z.string().optional(),
|
|
12
13
|
webhookUrl: z.string().optional(),
|
|
13
|
-
webhookSecret:
|
|
14
|
+
webhookSecret: buildSecretInputSchema().optional(),
|
|
14
15
|
webhookPath: z.string().optional(),
|
|
15
16
|
dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
|
|
16
17
|
allowFrom: z.array(allowFromEntry).optional(),
|
package/src/group-access.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { GroupPolicy, SenderGroupAccessDecision } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { GroupPolicy, SenderGroupAccessDecision } from "openclaw/plugin-sdk/zalo";
|
|
2
2
|
import {
|
|
3
3
|
evaluateSenderGroupAccess,
|
|
4
4
|
isNormalizedSenderAllowed,
|
|
5
5
|
resolveOpenProviderRuntimeGroupPolicy,
|
|
6
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
+
} from "openclaw/plugin-sdk/zalo";
|
|
7
7
|
|
|
8
8
|
const ZALO_ALLOW_FROM_PREFIX_RE = /^(zalo|zl):/i;
|
|
9
9
|
|