@openclaw/zalo 2026.3.13 → 2026.5.2-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.
- package/README.md +1 -1
- package/api.ts +9 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +5 -0
- package/index.test.ts +15 -0
- package/index.ts +16 -13
- package/openclaw.plugin.json +514 -1
- package/package.json +31 -5
- package/runtime-api.test.ts +17 -0
- package/runtime-api.ts +75 -0
- package/secret-contract-api.ts +5 -0
- package/setup-api.ts +34 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +70 -0
- package/src/accounts.ts +19 -19
- package/src/actions.runtime.ts +5 -0
- package/src/actions.test.ts +32 -0
- package/src/actions.ts +20 -14
- package/src/api.test.ts +93 -2
- package/src/api.ts +29 -2
- package/src/approval-auth.test.ts +17 -0
- package/src/approval-auth.ts +25 -0
- package/src/channel.directory.test.ts +19 -6
- package/src/channel.runtime.ts +93 -0
- package/src/channel.startup.test.ts +26 -19
- package/src/channel.ts +229 -336
- package/src/config-schema.ts +3 -3
- package/src/group-access.ts +4 -3
- package/src/monitor.group-policy.test.ts +0 -12
- package/src/monitor.image.polling.test.ts +110 -0
- package/src/monitor.lifecycle.test.ts +41 -22
- package/src/monitor.pairing.lifecycle.test.ts +141 -0
- package/src/monitor.polling.media-reply.test.ts +425 -0
- package/src/monitor.reply-once.lifecycle.test.ts +171 -0
- package/src/monitor.ts +460 -206
- package/src/monitor.types.ts +4 -0
- package/src/monitor.webhook.test.ts +392 -62
- package/src/monitor.webhook.ts +73 -36
- package/src/outbound-media.test.ts +182 -0
- package/src/outbound-media.ts +241 -0
- package/src/outbound-payload.contract.test.ts +45 -0
- package/src/probe.ts +1 -1
- package/src/proxy.ts +1 -1
- package/src/runtime-api.ts +75 -0
- package/src/runtime-support.ts +91 -0
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +109 -0
- package/src/secret-input.ts +1 -9
- package/src/send.test.ts +120 -0
- package/src/send.ts +15 -13
- package/src/session-route.ts +32 -0
- package/src/setup-allow-from.ts +94 -0
- package/src/setup-core.ts +149 -0
- package/src/{onboarding.status.test.ts → setup-status.test.ts} +13 -4
- package/src/setup-surface.test.ts +175 -0
- package/src/{onboarding.ts → setup-surface.ts} +59 -177
- package/src/status-issues.test.ts +2 -14
- package/src/status-issues.ts +8 -2
- package/src/test-support/lifecycle-test-support.ts +413 -0
- package/src/test-support/monitor-mocks-test-support.ts +209 -0
- package/src/token.test.ts +15 -0
- package/src/token.ts +8 -17
- package/src/types.ts +2 -2
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -101
- package/src/channel.sendpayload.test.ts +0 -44
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectConditionalChannelFieldAssignments,
|
|
3
|
+
getChannelSurface,
|
|
4
|
+
hasOwnProperty,
|
|
5
|
+
type ResolverContext,
|
|
6
|
+
type SecretDefaults,
|
|
7
|
+
type SecretTargetRegistryEntry,
|
|
8
|
+
} from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
9
|
+
|
|
10
|
+
export const secretTargetRegistryEntries: SecretTargetRegistryEntry[] = [
|
|
11
|
+
{
|
|
12
|
+
id: "channels.zalo.accounts.*.botToken",
|
|
13
|
+
targetType: "channels.zalo.accounts.*.botToken",
|
|
14
|
+
configFile: "openclaw.json",
|
|
15
|
+
pathPattern: "channels.zalo.accounts.*.botToken",
|
|
16
|
+
secretShape: "secret_input",
|
|
17
|
+
expectedResolvedValue: "string",
|
|
18
|
+
includeInPlan: true,
|
|
19
|
+
includeInConfigure: true,
|
|
20
|
+
includeInAudit: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "channels.zalo.accounts.*.webhookSecret",
|
|
24
|
+
targetType: "channels.zalo.accounts.*.webhookSecret",
|
|
25
|
+
configFile: "openclaw.json",
|
|
26
|
+
pathPattern: "channels.zalo.accounts.*.webhookSecret",
|
|
27
|
+
secretShape: "secret_input",
|
|
28
|
+
expectedResolvedValue: "string",
|
|
29
|
+
includeInPlan: true,
|
|
30
|
+
includeInConfigure: true,
|
|
31
|
+
includeInAudit: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "channels.zalo.botToken",
|
|
35
|
+
targetType: "channels.zalo.botToken",
|
|
36
|
+
configFile: "openclaw.json",
|
|
37
|
+
pathPattern: "channels.zalo.botToken",
|
|
38
|
+
secretShape: "secret_input",
|
|
39
|
+
expectedResolvedValue: "string",
|
|
40
|
+
includeInPlan: true,
|
|
41
|
+
includeInConfigure: true,
|
|
42
|
+
includeInAudit: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "channels.zalo.webhookSecret",
|
|
46
|
+
targetType: "channels.zalo.webhookSecret",
|
|
47
|
+
configFile: "openclaw.json",
|
|
48
|
+
pathPattern: "channels.zalo.webhookSecret",
|
|
49
|
+
secretShape: "secret_input",
|
|
50
|
+
expectedResolvedValue: "string",
|
|
51
|
+
includeInPlan: true,
|
|
52
|
+
includeInConfigure: true,
|
|
53
|
+
includeInAudit: true,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export function collectRuntimeConfigAssignments(params: {
|
|
58
|
+
config: { channels?: Record<string, unknown> };
|
|
59
|
+
defaults?: SecretDefaults;
|
|
60
|
+
context: ResolverContext;
|
|
61
|
+
}): void {
|
|
62
|
+
const resolved = getChannelSurface(params.config, "zalo");
|
|
63
|
+
if (!resolved) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const { channel: zalo, surface } = resolved;
|
|
67
|
+
collectConditionalChannelFieldAssignments({
|
|
68
|
+
channelKey: "zalo",
|
|
69
|
+
field: "botToken",
|
|
70
|
+
channel: zalo,
|
|
71
|
+
surface,
|
|
72
|
+
defaults: params.defaults,
|
|
73
|
+
context: params.context,
|
|
74
|
+
topLevelActiveWithoutAccounts: true,
|
|
75
|
+
topLevelInheritedAccountActive: ({ account, enabled }) =>
|
|
76
|
+
enabled && !hasOwnProperty(account, "botToken"),
|
|
77
|
+
accountActive: ({ enabled }) => enabled,
|
|
78
|
+
topInactiveReason: "no enabled Zalo surface inherits this top-level botToken.",
|
|
79
|
+
accountInactiveReason: "Zalo account is disabled.",
|
|
80
|
+
});
|
|
81
|
+
const baseWebhookUrl = typeof zalo.webhookUrl === "string" ? zalo.webhookUrl.trim() : "";
|
|
82
|
+
const accountWebhookUrl = (account: Record<string, unknown>) =>
|
|
83
|
+
hasOwnProperty(account, "webhookUrl")
|
|
84
|
+
? typeof account.webhookUrl === "string"
|
|
85
|
+
? account.webhookUrl.trim()
|
|
86
|
+
: ""
|
|
87
|
+
: baseWebhookUrl;
|
|
88
|
+
collectConditionalChannelFieldAssignments({
|
|
89
|
+
channelKey: "zalo",
|
|
90
|
+
field: "webhookSecret",
|
|
91
|
+
channel: zalo,
|
|
92
|
+
surface,
|
|
93
|
+
defaults: params.defaults,
|
|
94
|
+
context: params.context,
|
|
95
|
+
topLevelActiveWithoutAccounts: baseWebhookUrl.length > 0,
|
|
96
|
+
topLevelInheritedAccountActive: ({ account, enabled }) =>
|
|
97
|
+
enabled && !hasOwnProperty(account, "webhookSecret") && accountWebhookUrl(account).length > 0,
|
|
98
|
+
accountActive: ({ account, enabled }) => enabled && accountWebhookUrl(account).length > 0,
|
|
99
|
+
topInactiveReason:
|
|
100
|
+
"no enabled Zalo webhook surface inherits this top-level webhookSecret (webhook mode is not active).",
|
|
101
|
+
accountInactiveReason:
|
|
102
|
+
"Zalo account is disabled or webhook mode is not active for this account.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const channelSecrets = {
|
|
107
|
+
secretTargetRegistryEntries,
|
|
108
|
+
collectRuntimeConfigAssignments,
|
|
109
|
+
};
|
package/src/secret-input.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildSecretInputSchema,
|
|
3
|
-
hasConfiguredSecretInput,
|
|
4
|
-
normalizeResolvedSecretInputString,
|
|
5
|
-
normalizeSecretInputString,
|
|
6
|
-
} from "openclaw/plugin-sdk/zalo";
|
|
7
|
-
|
|
8
1
|
export {
|
|
9
2
|
buildSecretInputSchema,
|
|
10
|
-
hasConfiguredSecretInput,
|
|
11
3
|
normalizeResolvedSecretInputString,
|
|
12
4
|
normalizeSecretInputString,
|
|
13
|
-
};
|
|
5
|
+
} from "openclaw/plugin-sdk/secret-input";
|
package/src/send.test.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const sendMessageMock = vi.fn();
|
|
4
|
+
const sendPhotoMock = vi.fn();
|
|
5
|
+
const resolveZaloProxyFetchMock = vi.fn();
|
|
6
|
+
|
|
7
|
+
vi.mock("./api.js", () => ({
|
|
8
|
+
sendMessage: (...args: unknown[]) => sendMessageMock(...args),
|
|
9
|
+
sendPhoto: (...args: unknown[]) => sendPhotoMock(...args),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("./proxy.js", () => ({
|
|
13
|
+
resolveZaloProxyFetch: (...args: unknown[]) => resolveZaloProxyFetchMock(...args),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { sendMessageZalo, sendPhotoZalo } from "./send.js";
|
|
17
|
+
|
|
18
|
+
describe("zalo send", () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
sendMessageMock.mockReset();
|
|
21
|
+
sendPhotoMock.mockReset();
|
|
22
|
+
resolveZaloProxyFetchMock.mockReset();
|
|
23
|
+
resolveZaloProxyFetchMock.mockReturnValue(undefined);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("sends text messages through the message API", async () => {
|
|
27
|
+
sendMessageMock.mockResolvedValueOnce({
|
|
28
|
+
ok: true,
|
|
29
|
+
result: { message_id: "z-msg-1" },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = await sendMessageZalo("dm-chat-1", "hello there", {
|
|
33
|
+
token: "zalo-token",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
37
|
+
"zalo-token",
|
|
38
|
+
{
|
|
39
|
+
chat_id: "dm-chat-1",
|
|
40
|
+
text: "hello there",
|
|
41
|
+
},
|
|
42
|
+
undefined,
|
|
43
|
+
);
|
|
44
|
+
expect(sendPhotoMock).not.toHaveBeenCalled();
|
|
45
|
+
expect(result).toEqual({ ok: true, messageId: "z-msg-1" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("routes media-bearing sends through the photo API and uses text as caption", async () => {
|
|
49
|
+
sendPhotoMock.mockResolvedValueOnce({
|
|
50
|
+
ok: true,
|
|
51
|
+
result: { message_id: "z-photo-1" },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await sendMessageZalo("dm-chat-2", "caption text", {
|
|
55
|
+
token: "zalo-token",
|
|
56
|
+
mediaUrl: "https://example.com/photo.jpg",
|
|
57
|
+
caption: "ignored fallback caption",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(sendPhotoMock).toHaveBeenCalledWith(
|
|
61
|
+
"zalo-token",
|
|
62
|
+
{
|
|
63
|
+
chat_id: "dm-chat-2",
|
|
64
|
+
photo: "https://example.com/photo.jpg",
|
|
65
|
+
caption: "caption text",
|
|
66
|
+
},
|
|
67
|
+
undefined,
|
|
68
|
+
);
|
|
69
|
+
expect(sendMessageMock).not.toHaveBeenCalled();
|
|
70
|
+
expect(result).toEqual({ ok: true, messageId: "z-photo-1" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("fails fast for missing token or blank photo URLs", async () => {
|
|
74
|
+
await expect(sendMessageZalo("dm-chat-3", "hello", {})).resolves.toEqual({
|
|
75
|
+
ok: false,
|
|
76
|
+
error: "No Zalo bot token configured",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await expect(
|
|
80
|
+
sendPhotoZalo("dm-chat-4", " ", {
|
|
81
|
+
token: "zalo-token",
|
|
82
|
+
}),
|
|
83
|
+
).resolves.toEqual({
|
|
84
|
+
ok: false,
|
|
85
|
+
error: "No photo URL provided",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(sendMessageMock).not.toHaveBeenCalled();
|
|
89
|
+
expect(sendPhotoMock).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("sends cfg-backed media directly without hosted-media rewrites", async () => {
|
|
93
|
+
sendPhotoMock.mockResolvedValueOnce({
|
|
94
|
+
ok: true,
|
|
95
|
+
result: { message_id: "z-photo-2" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = await sendPhotoZalo("dm-chat-5", "https://example.com/photo.jpg", {
|
|
99
|
+
cfg: {
|
|
100
|
+
channels: {
|
|
101
|
+
zalo: {
|
|
102
|
+
botToken: "zalo-token",
|
|
103
|
+
webhookUrl: "https://gateway.example.com/zalo-webhook",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
} as never,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(sendPhotoMock).toHaveBeenCalledWith(
|
|
110
|
+
"zalo-token",
|
|
111
|
+
{
|
|
112
|
+
chat_id: "dm-chat-5",
|
|
113
|
+
photo: "https://example.com/photo.jpg",
|
|
114
|
+
caption: undefined,
|
|
115
|
+
},
|
|
116
|
+
undefined,
|
|
117
|
+
);
|
|
118
|
+
expect(result).toEqual({ ok: true, messageId: "z-photo-2" });
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/send.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk/
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
2
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
2
3
|
import { resolveZaloAccount } from "./accounts.js";
|
|
3
4
|
import type { ZaloFetch } from "./api.js";
|
|
4
5
|
import { sendMessage, sendPhoto } from "./api.js";
|
|
5
6
|
import { resolveZaloProxyFetch } from "./proxy.js";
|
|
6
7
|
import { resolveZaloToken } from "./token.js";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
type ZaloSendOptions = {
|
|
9
10
|
token?: string;
|
|
10
11
|
accountId?: string;
|
|
11
12
|
cfg?: OpenClawConfig;
|
|
@@ -15,7 +16,7 @@ export type ZaloSendOptions = {
|
|
|
15
16
|
proxy?: string;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
type ZaloSendResult = {
|
|
19
20
|
ok: boolean;
|
|
20
21
|
messageId?: string;
|
|
21
22
|
error?: string;
|
|
@@ -39,7 +40,7 @@ async function runZaloSend(
|
|
|
39
40
|
const result = toZaloSendResult(await send());
|
|
40
41
|
return result.ok ? result : { ok: false, error: failureMessage };
|
|
41
42
|
} catch (err) {
|
|
42
|
-
return { ok: false, error:
|
|
43
|
+
return { ok: false, error: formatErrorMessage(err) };
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -138,14 +139,15 @@ export async function sendPhotoZalo(
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
return await runZaloSend("Failed to send photo", () =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
142
|
+
(async () =>
|
|
143
|
+
sendPhoto(
|
|
144
|
+
context.token,
|
|
145
|
+
{
|
|
146
|
+
chat_id: context.chatId,
|
|
147
|
+
photo: photoUrl.trim(),
|
|
148
|
+
caption: options.caption?.slice(0, 2000),
|
|
149
|
+
},
|
|
150
|
+
context.fetcher,
|
|
151
|
+
))(),
|
|
150
152
|
);
|
|
151
153
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildChannelOutboundSessionRoute,
|
|
3
|
+
stripChannelTargetPrefix,
|
|
4
|
+
stripTargetKindPrefix,
|
|
5
|
+
type ChannelOutboundSessionRouteParams,
|
|
6
|
+
} from "openclaw/plugin-sdk/core";
|
|
7
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
8
|
+
|
|
9
|
+
export function resolveZaloOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) {
|
|
10
|
+
const trimmed = stripChannelTargetPrefix(params.target, "zalo", "zl");
|
|
11
|
+
if (!trimmed) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const isGroup = normalizeLowercaseStringOrEmpty(trimmed).startsWith("group:");
|
|
15
|
+
const peerId = stripTargetKindPrefix(trimmed);
|
|
16
|
+
if (!peerId) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return buildChannelOutboundSessionRoute({
|
|
20
|
+
cfg: params.cfg,
|
|
21
|
+
agentId: params.agentId,
|
|
22
|
+
channel: "zalo",
|
|
23
|
+
accountId: params.accountId,
|
|
24
|
+
peer: {
|
|
25
|
+
kind: isGroup ? "group" : "direct",
|
|
26
|
+
id: peerId,
|
|
27
|
+
},
|
|
28
|
+
chatType: isGroup ? "group" : "direct",
|
|
29
|
+
from: isGroup ? `zalo:group:${peerId}` : `zalo:${peerId}`,
|
|
30
|
+
to: `zalo:${peerId}`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ACCOUNT_ID,
|
|
3
|
+
formatDocsLink,
|
|
4
|
+
mergeAllowFromEntries,
|
|
5
|
+
type ChannelSetupDmPolicy,
|
|
6
|
+
type ChannelSetupWizard,
|
|
7
|
+
type OpenClawConfig,
|
|
8
|
+
} from "openclaw/plugin-sdk/setup";
|
|
9
|
+
import { resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js";
|
|
10
|
+
|
|
11
|
+
type ZaloAccountSetupConfig = {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function noteZaloTokenHelp(
|
|
16
|
+
prompter: Parameters<NonNullable<ChannelSetupWizard["finalize"]>>[0]["prompter"],
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
await prompter.note(
|
|
19
|
+
[
|
|
20
|
+
"1) Open Zalo Bot Platform: https://bot.zaloplatforms.com",
|
|
21
|
+
"2) Create a bot and get the token",
|
|
22
|
+
"3) Token looks like 12345689:abc-xyz",
|
|
23
|
+
"Tip: you can also set ZALO_BOT_TOKEN in your env.",
|
|
24
|
+
`Docs: ${formatDocsLink("/channels/zalo", "zalo")}`,
|
|
25
|
+
].join("\n"),
|
|
26
|
+
"Zalo bot token",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function promptZaloAllowFrom(params: {
|
|
31
|
+
cfg: OpenClawConfig;
|
|
32
|
+
prompter: Parameters<NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>>[0]["prompter"];
|
|
33
|
+
accountId?: string;
|
|
34
|
+
}): Promise<OpenClawConfig> {
|
|
35
|
+
const { cfg, prompter } = params;
|
|
36
|
+
const accountId = params.accountId ?? resolveDefaultZaloAccountId(cfg);
|
|
37
|
+
const resolved = resolveZaloAccount({ cfg, accountId });
|
|
38
|
+
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
|
39
|
+
const entry = await prompter.text({
|
|
40
|
+
message: "Zalo allowFrom (user id)",
|
|
41
|
+
placeholder: "123456789",
|
|
42
|
+
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
|
43
|
+
validate: (value) => {
|
|
44
|
+
const raw = (value ?? "").trim();
|
|
45
|
+
if (!raw) {
|
|
46
|
+
return "Required";
|
|
47
|
+
}
|
|
48
|
+
if (!/^\d+$/.test(raw)) {
|
|
49
|
+
return "Use a numeric Zalo user id";
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
const normalized = entry.trim();
|
|
55
|
+
const unique = mergeAllowFromEntries(existingAllowFrom, [normalized]);
|
|
56
|
+
|
|
57
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
58
|
+
return {
|
|
59
|
+
...cfg,
|
|
60
|
+
channels: {
|
|
61
|
+
...cfg.channels,
|
|
62
|
+
zalo: {
|
|
63
|
+
...cfg.channels?.zalo,
|
|
64
|
+
enabled: true,
|
|
65
|
+
dmPolicy: "allowlist",
|
|
66
|
+
allowFrom: unique,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
} as OpenClawConfig;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const currentAccount = cfg.channels?.zalo?.accounts?.[accountId] as
|
|
73
|
+
| ZaloAccountSetupConfig
|
|
74
|
+
| undefined;
|
|
75
|
+
return {
|
|
76
|
+
...cfg,
|
|
77
|
+
channels: {
|
|
78
|
+
...cfg.channels,
|
|
79
|
+
zalo: {
|
|
80
|
+
...cfg.channels?.zalo,
|
|
81
|
+
enabled: true,
|
|
82
|
+
accounts: {
|
|
83
|
+
...cfg.channels?.zalo?.accounts,
|
|
84
|
+
[accountId]: {
|
|
85
|
+
...currentAccount,
|
|
86
|
+
enabled: currentAccount?.enabled ?? true,
|
|
87
|
+
dmPolicy: "allowlist",
|
|
88
|
+
allowFrom: unique,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
} as OpenClawConfig;
|
|
94
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addWildcardAllowFrom,
|
|
3
|
+
createDelegatedSetupWizardProxy,
|
|
4
|
+
createPatchedAccountSetupAdapter,
|
|
5
|
+
createSetupInputPresenceValidator,
|
|
6
|
+
DEFAULT_ACCOUNT_ID,
|
|
7
|
+
normalizeAccountId,
|
|
8
|
+
type ChannelSetupDmPolicy,
|
|
9
|
+
type ChannelSetupWizard,
|
|
10
|
+
} from "openclaw/plugin-sdk/setup";
|
|
11
|
+
import { resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js";
|
|
12
|
+
import { promptZaloAllowFrom } from "./setup-allow-from.js";
|
|
13
|
+
|
|
14
|
+
const channel = "zalo" as const;
|
|
15
|
+
|
|
16
|
+
type ZaloAccountSetupConfig = {
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
dmPolicy?: string;
|
|
19
|
+
allowFrom?: Array<string | number> | ReadonlyArray<string | number>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const zaloSetupAdapter = createPatchedAccountSetupAdapter({
|
|
23
|
+
channelKey: channel,
|
|
24
|
+
validateInput: createSetupInputPresenceValidator({
|
|
25
|
+
defaultAccountOnlyEnvError: "ZALO_BOT_TOKEN can only be used for the default account.",
|
|
26
|
+
whenNotUseEnv: [
|
|
27
|
+
{
|
|
28
|
+
someOf: ["token", "tokenFile"],
|
|
29
|
+
message: "Zalo requires token or --token-file (or --use-env).",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
}),
|
|
33
|
+
buildPatch: (input) =>
|
|
34
|
+
input.useEnv
|
|
35
|
+
? {}
|
|
36
|
+
: input.tokenFile
|
|
37
|
+
? { tokenFile: input.tokenFile }
|
|
38
|
+
: input.token
|
|
39
|
+
? { botToken: input.token }
|
|
40
|
+
: {},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const zaloDmPolicy: ChannelSetupDmPolicy = {
|
|
44
|
+
label: "Zalo",
|
|
45
|
+
channel,
|
|
46
|
+
policyKey: "channels.zalo.dmPolicy",
|
|
47
|
+
allowFromKey: "channels.zalo.allowFrom",
|
|
48
|
+
resolveConfigKeys: (cfg, accountId) =>
|
|
49
|
+
(accountId ?? resolveDefaultZaloAccountId(cfg)) !== DEFAULT_ACCOUNT_ID
|
|
50
|
+
? {
|
|
51
|
+
policyKey: `channels.zalo.accounts.${accountId ?? resolveDefaultZaloAccountId(cfg)}.dmPolicy`,
|
|
52
|
+
allowFromKey: `channels.zalo.accounts.${accountId ?? resolveDefaultZaloAccountId(cfg)}.allowFrom`,
|
|
53
|
+
}
|
|
54
|
+
: {
|
|
55
|
+
policyKey: "channels.zalo.dmPolicy",
|
|
56
|
+
allowFromKey: "channels.zalo.allowFrom",
|
|
57
|
+
},
|
|
58
|
+
getCurrent: (cfg, accountId) =>
|
|
59
|
+
resolveZaloAccount({
|
|
60
|
+
cfg: cfg,
|
|
61
|
+
accountId: accountId ?? resolveDefaultZaloAccountId(cfg),
|
|
62
|
+
}).config.dmPolicy ?? "pairing",
|
|
63
|
+
setPolicy: (cfg, policy, accountId) => {
|
|
64
|
+
const resolvedAccountId =
|
|
65
|
+
accountId && normalizeAccountId(accountId)
|
|
66
|
+
? (normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID)
|
|
67
|
+
: resolveDefaultZaloAccountId(cfg);
|
|
68
|
+
const resolved = resolveZaloAccount({
|
|
69
|
+
cfg: cfg,
|
|
70
|
+
accountId: resolvedAccountId,
|
|
71
|
+
});
|
|
72
|
+
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
|
73
|
+
return {
|
|
74
|
+
...cfg,
|
|
75
|
+
channels: {
|
|
76
|
+
...cfg.channels,
|
|
77
|
+
zalo: {
|
|
78
|
+
...cfg.channels?.zalo,
|
|
79
|
+
enabled: true,
|
|
80
|
+
dmPolicy: policy,
|
|
81
|
+
...(policy === "open"
|
|
82
|
+
? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) }
|
|
83
|
+
: {}),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const currentAccount = cfg.channels?.zalo?.accounts?.[resolvedAccountId] as
|
|
89
|
+
| ZaloAccountSetupConfig
|
|
90
|
+
| undefined;
|
|
91
|
+
return {
|
|
92
|
+
...cfg,
|
|
93
|
+
channels: {
|
|
94
|
+
...cfg.channels,
|
|
95
|
+
zalo: {
|
|
96
|
+
...cfg.channels?.zalo,
|
|
97
|
+
enabled: true,
|
|
98
|
+
accounts: {
|
|
99
|
+
...cfg.channels?.zalo?.accounts,
|
|
100
|
+
[resolvedAccountId]: {
|
|
101
|
+
...currentAccount,
|
|
102
|
+
enabled: currentAccount?.enabled ?? true,
|
|
103
|
+
dmPolicy: policy,
|
|
104
|
+
...(policy === "open"
|
|
105
|
+
? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) }
|
|
106
|
+
: {}),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
promptAllowFrom: async ({ cfg, prompter, accountId }) =>
|
|
114
|
+
promptZaloAllowFrom({
|
|
115
|
+
cfg,
|
|
116
|
+
prompter,
|
|
117
|
+
accountId: accountId ?? resolveDefaultZaloAccountId(cfg),
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export function createZaloSetupWizardProxy(
|
|
122
|
+
loadWizard: () => Promise<ChannelSetupWizard>,
|
|
123
|
+
): ChannelSetupWizard {
|
|
124
|
+
return createDelegatedSetupWizardProxy({
|
|
125
|
+
channel,
|
|
126
|
+
loadWizard,
|
|
127
|
+
status: {
|
|
128
|
+
configuredLabel: "configured",
|
|
129
|
+
unconfiguredLabel: "needs token",
|
|
130
|
+
configuredHint: "recommended · configured",
|
|
131
|
+
unconfiguredHint: "recommended · newcomer-friendly",
|
|
132
|
+
configuredScore: 1,
|
|
133
|
+
unconfiguredScore: 10,
|
|
134
|
+
},
|
|
135
|
+
credentials: [],
|
|
136
|
+
delegateFinalize: true,
|
|
137
|
+
dmPolicy: zaloDmPolicy,
|
|
138
|
+
disable: (cfg) => ({
|
|
139
|
+
...cfg,
|
|
140
|
+
channels: {
|
|
141
|
+
...cfg.channels,
|
|
142
|
+
zalo: {
|
|
143
|
+
...cfg.channels?.zalo,
|
|
144
|
+
enabled: false,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createPluginSetupWizardStatus } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import type { OpenClawConfig } from "../runtime-api.js";
|
|
4
|
+
import { zaloSetupWizard } from "./setup-surface.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const zaloGetStatus = createPluginSetupWizardStatus({
|
|
7
|
+
id: "zalo",
|
|
8
|
+
meta: {
|
|
9
|
+
label: "Zalo",
|
|
10
|
+
},
|
|
11
|
+
setupWizard: zaloSetupWizard,
|
|
12
|
+
} as never);
|
|
13
|
+
|
|
14
|
+
describe("zalo setup wizard status", () => {
|
|
6
15
|
it("treats SecretRef botToken as configured", async () => {
|
|
7
|
-
const status = await
|
|
16
|
+
const status = await zaloGetStatus({
|
|
8
17
|
cfg: {
|
|
9
18
|
channels: {
|
|
10
19
|
zalo: {
|