@openclaw/zalouser 2026.3.13 → 2026.5.1-beta.2

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.
Files changed (67) hide show
  1. package/README.md +4 -3
  2. package/api.ts +9 -0
  3. package/channel-plugin-api.ts +3 -0
  4. package/contract-api.ts +2 -0
  5. package/doctor-contract-api.ts +1 -0
  6. package/index.ts +29 -24
  7. package/openclaw.plugin.json +288 -1
  8. package/package.json +38 -11
  9. package/runtime-api.ts +67 -0
  10. package/secret-contract-api.ts +4 -0
  11. package/setup-entry.ts +9 -0
  12. package/setup-plugin-api.ts +2 -0
  13. package/src/accounts.runtime.ts +1 -0
  14. package/src/accounts.test-mocks.ts +7 -3
  15. package/src/accounts.test.ts +53 -1
  16. package/src/accounts.ts +38 -24
  17. package/src/channel-api.ts +20 -0
  18. package/src/channel.adapters.ts +390 -0
  19. package/src/channel.directory.test.ts +47 -40
  20. package/src/channel.runtime.ts +12 -0
  21. package/src/channel.sendpayload.test.ts +41 -23
  22. package/src/channel.setup.test.ts +33 -0
  23. package/src/channel.setup.ts +12 -0
  24. package/src/channel.test.ts +231 -20
  25. package/src/channel.ts +176 -685
  26. package/src/config-schema.ts +5 -5
  27. package/src/directory.ts +54 -0
  28. package/src/doctor-contract.ts +156 -0
  29. package/src/doctor.test.ts +77 -0
  30. package/src/doctor.ts +37 -0
  31. package/src/group-policy.test.ts +4 -4
  32. package/src/group-policy.ts +4 -2
  33. package/src/monitor.account-scope.test.ts +2 -1
  34. package/src/monitor.group-gating.test.ts +162 -8
  35. package/src/monitor.ts +233 -173
  36. package/src/probe.ts +3 -2
  37. package/src/qr-temp-file.ts +1 -1
  38. package/src/reaction.ts +5 -2
  39. package/src/runtime.ts +6 -3
  40. package/src/security-audit.test.ts +80 -0
  41. package/src/security-audit.ts +71 -0
  42. package/src/send.test.ts +2 -2
  43. package/src/send.ts +3 -3
  44. package/src/session-route.ts +121 -0
  45. package/src/setup-core.ts +33 -0
  46. package/src/setup-surface.test.ts +363 -0
  47. package/src/setup-surface.ts +470 -0
  48. package/src/setup-test-helpers.ts +42 -0
  49. package/src/shared.ts +92 -0
  50. package/src/status-issues.test.ts +1 -13
  51. package/src/status-issues.ts +8 -2
  52. package/src/test-helpers.ts +1 -1
  53. package/src/text-styles.test.ts +1 -1
  54. package/src/text-styles.ts +5 -2
  55. package/src/tool.test.ts +66 -3
  56. package/src/tool.ts +76 -14
  57. package/src/types.ts +3 -3
  58. package/src/zalo-js.credentials.test.ts +465 -0
  59. package/src/zalo-js.test-mocks.ts +89 -0
  60. package/src/zalo-js.ts +491 -274
  61. package/src/zca-client.test.ts +24 -0
  62. package/src/zca-client.ts +24 -58
  63. package/src/zca-constants.ts +55 -0
  64. package/test-api.ts +21 -0
  65. package/tsconfig.json +16 -0
  66. package/CHANGELOG.md +0 -107
  67. package/src/onboarding.ts +0 -340
package/src/tool.test.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { sendImageZalouser, sendLinkZalouser, sendMessageZalouser } from "./send.js";
3
- import { executeZalouserTool } from "./tool.js";
3
+ import { createZalouserTool, executeZalouserTool } from "./tool.js";
4
4
  import {
5
5
  checkZaloAuthenticated,
6
6
  getZaloUserInfo,
@@ -30,8 +30,8 @@ const mockGetUserInfo = vi.mocked(getZaloUserInfo);
30
30
  const mockListFriends = vi.mocked(listZaloFriendsMatching);
31
31
  const mockListGroups = vi.mocked(listZaloGroupsMatching);
32
32
 
33
- function extractDetails(result: Awaited<ReturnType<typeof executeZalouserTool>>): unknown {
34
- const text = result.content[0]?.text ?? "{}";
33
+ function extractDetails(result: { content?: Array<{ type: string; text?: string }> }): unknown {
34
+ const text = result.content?.[0]?.text ?? "{}";
35
35
  return JSON.parse(text) as unknown;
36
36
  }
37
37
 
@@ -69,6 +69,69 @@ describe("executeZalouserTool", () => {
69
69
  expect(extractDetails(result)).toEqual({ success: true, messageId: "m-1" });
70
70
  });
71
71
 
72
+ it("defaults send routing from ambient deliveryContext target", async () => {
73
+ mockSendMessage.mockResolvedValueOnce({ ok: true, messageId: "m-ambient" });
74
+ const tool = createZalouserTool({
75
+ deliveryContext: {
76
+ channel: "zalouser",
77
+ to: "zalouser:g-ambient",
78
+ },
79
+ });
80
+
81
+ const result = await tool.execute("tool-1", {
82
+ action: "send",
83
+ message: "hello",
84
+ });
85
+
86
+ expect(mockSendMessage).toHaveBeenCalledWith("g-ambient", "hello", {
87
+ profile: undefined,
88
+ isGroup: true,
89
+ });
90
+ expect(extractDetails(result)).toEqual({ success: true, messageId: "m-ambient" });
91
+ });
92
+
93
+ it("keeps explicit threadId over ambient delivery defaults", async () => {
94
+ mockSendMessage.mockResolvedValueOnce({ ok: true, messageId: "m-explicit" });
95
+ const tool = createZalouserTool({
96
+ deliveryContext: {
97
+ channel: "zalouser",
98
+ to: "zalouser:g-ambient",
99
+ },
100
+ });
101
+
102
+ await tool.execute("tool-1", {
103
+ action: "send",
104
+ threadId: "u-explicit",
105
+ message: "hello",
106
+ isGroup: false,
107
+ });
108
+
109
+ expect(mockSendMessage).toHaveBeenCalledWith("u-explicit", "hello", {
110
+ profile: undefined,
111
+ isGroup: false,
112
+ });
113
+ });
114
+
115
+ it("does not route send actions from foreign ambient thread defaults", async () => {
116
+ const tool = createZalouserTool({
117
+ deliveryContext: {
118
+ channel: "slack",
119
+ to: "channel:C123",
120
+ threadId: "1710000000.000100",
121
+ },
122
+ });
123
+
124
+ const result = await tool.execute("tool-1", {
125
+ action: "send",
126
+ message: "hello",
127
+ });
128
+
129
+ expect(mockSendMessage).not.toHaveBeenCalled();
130
+ expect(extractDetails(result)).toEqual({
131
+ error: "threadId and message required for send action",
132
+ });
133
+ });
134
+
72
135
  it("returns tool error when send action fails", async () => {
73
136
  mockSendMessage.mockResolvedValueOnce({ ok: false, error: "blocked" });
74
137
  const result = await executeZalouserTool("tool-1", {
package/src/tool.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import type { AnyAgentTool, OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
2
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
3
+ import { Type } from "typebox";
2
4
  import { sendImageZalouser, sendLinkZalouser, sendMessageZalouser } from "./send.js";
5
+ import { parseZalouserOutboundTarget } from "./session-route.js";
3
6
  import {
4
7
  checkZaloAuthenticated,
5
8
  getZaloUserInfo,
@@ -10,8 +13,8 @@ import {
10
13
  const ACTIONS = ["send", "image", "link", "friends", "groups", "me", "status"] as const;
11
14
 
12
15
  type AgentToolResult = {
13
- content: Array<{ type: string; text: string }>;
14
- details?: unknown;
16
+ content: Array<{ type: "text"; text: string }>;
17
+ details: unknown;
15
18
  };
16
19
 
17
20
  function stringEnum<T extends readonly string[]>(
@@ -25,7 +28,7 @@ function stringEnum<T extends readonly string[]>(
25
28
  });
26
29
  }
27
30
 
28
- export const ZalouserToolSchema = Type.Object(
31
+ const ZalouserToolSchema = Type.Object(
29
32
  {
30
33
  action: stringEnum(ACTIONS, { description: `Action to perform: ${ACTIONS.join(", ")}` }),
31
34
  threadId: Type.Optional(Type.String({ description: "Thread ID for messaging" })),
@@ -48,6 +51,8 @@ type ToolParams = {
48
51
  url?: string;
49
52
  };
50
53
 
54
+ type ZalouserToolContext = Pick<OpenClawPluginToolContext, "deliveryContext">;
55
+
51
56
  function json(payload: unknown): AgentToolResult {
52
57
  return {
53
58
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
@@ -55,21 +60,62 @@ function json(payload: unknown): AgentToolResult {
55
60
  };
56
61
  }
57
62
 
63
+ function resolveAmbientZalouserTarget(context?: ZalouserToolContext): {
64
+ threadId?: string;
65
+ isGroup?: boolean;
66
+ } {
67
+ const deliveryContext = context?.deliveryContext;
68
+ const rawTarget = deliveryContext?.to;
69
+ if (
70
+ (deliveryContext?.channel === undefined || deliveryContext.channel === "zalouser") &&
71
+ typeof rawTarget === "string" &&
72
+ rawTarget.trim()
73
+ ) {
74
+ try {
75
+ return parseZalouserOutboundTarget(rawTarget);
76
+ } catch {
77
+ // Ignore unrelated delivery targets; explicit tool params still win.
78
+ }
79
+ }
80
+ if (deliveryContext?.channel && deliveryContext.channel !== "zalouser") {
81
+ return {};
82
+ }
83
+ const ambientThreadId = deliveryContext?.threadId;
84
+ if (typeof ambientThreadId === "string" && ambientThreadId.trim()) {
85
+ return { threadId: ambientThreadId.trim() };
86
+ }
87
+ if (typeof ambientThreadId === "number" && Number.isFinite(ambientThreadId)) {
88
+ return { threadId: String(ambientThreadId) };
89
+ }
90
+ return {};
91
+ }
92
+
93
+ function resolveZalouserSendTarget(params: ToolParams, context?: ZalouserToolContext) {
94
+ const explicitThreadId = typeof params.threadId === "string" ? params.threadId.trim() : "";
95
+ const ambientTarget = resolveAmbientZalouserTarget(context);
96
+ return {
97
+ threadId: explicitThreadId || ambientTarget.threadId,
98
+ isGroup: typeof params.isGroup === "boolean" ? params.isGroup : ambientTarget.isGroup,
99
+ };
100
+ }
101
+
58
102
  export async function executeZalouserTool(
59
103
  _toolCallId: string,
60
104
  params: ToolParams,
61
105
  _signal?: AbortSignal,
62
106
  _onUpdate?: unknown,
107
+ context?: ZalouserToolContext,
63
108
  ): Promise<AgentToolResult> {
64
109
  try {
65
110
  switch (params.action) {
66
111
  case "send": {
67
- if (!params.threadId || !params.message) {
112
+ const target = resolveZalouserSendTarget(params, context);
113
+ if (!target.threadId || !params.message) {
68
114
  throw new Error("threadId and message required for send action");
69
115
  }
70
- const result = await sendMessageZalouser(params.threadId, params.message, {
116
+ const result = await sendMessageZalouser(target.threadId, params.message, {
71
117
  profile: params.profile,
72
- isGroup: params.isGroup,
118
+ isGroup: target.isGroup,
73
119
  });
74
120
  if (!result.ok) {
75
121
  throw new Error(result.error || "Failed to send message");
@@ -78,16 +124,17 @@ export async function executeZalouserTool(
78
124
  }
79
125
 
80
126
  case "image": {
81
- if (!params.threadId) {
127
+ const target = resolveZalouserSendTarget(params, context);
128
+ if (!target.threadId) {
82
129
  throw new Error("threadId required for image action");
83
130
  }
84
131
  if (!params.url) {
85
132
  throw new Error("url required for image action");
86
133
  }
87
- const result = await sendImageZalouser(params.threadId, params.url, {
134
+ const result = await sendImageZalouser(target.threadId, params.url, {
88
135
  profile: params.profile,
89
136
  caption: params.message,
90
- isGroup: params.isGroup,
137
+ isGroup: target.isGroup,
91
138
  });
92
139
  if (!result.ok) {
93
140
  throw new Error(result.error || "Failed to send image");
@@ -96,13 +143,14 @@ export async function executeZalouserTool(
96
143
  }
97
144
 
98
145
  case "link": {
99
- if (!params.threadId || !params.url) {
146
+ const target = resolveZalouserSendTarget(params, context);
147
+ if (!target.threadId || !params.url) {
100
148
  throw new Error("threadId and url required for link action");
101
149
  }
102
- const result = await sendLinkZalouser(params.threadId, params.url, {
150
+ const result = await sendLinkZalouser(target.threadId, params.url, {
103
151
  profile: params.profile,
104
152
  caption: params.message,
105
- isGroup: params.isGroup,
153
+ isGroup: target.isGroup,
106
154
  });
107
155
  if (!result.ok) {
108
156
  throw new Error(result.error || "Failed to send link");
@@ -142,7 +190,21 @@ export async function executeZalouserTool(
142
190
  }
143
191
  } catch (err) {
144
192
  return json({
145
- error: err instanceof Error ? err.message : String(err),
193
+ error: formatErrorMessage(err),
146
194
  });
147
195
  }
148
196
  }
197
+
198
+ export function createZalouserTool(context?: ZalouserToolContext): AnyAgentTool {
199
+ return {
200
+ name: "zalouser",
201
+ label: "Zalo Personal",
202
+ description:
203
+ "Send messages and access data via Zalo personal account. " +
204
+ "Actions: send (text message), image (send image URL), link (send link), " +
205
+ "friends (list/search friends), groups (list groups), me (profile info), status (auth check).",
206
+ parameters: ZalouserToolSchema,
207
+ execute: async (toolCallId, params, signal, onUpdate) =>
208
+ await executeZalouserTool(toolCallId, params as ToolParams, signal, onUpdate, context),
209
+ } satisfies AnyAgentTool;
210
+ }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Style } from "./zca-client.js";
1
+ import type { Style } from "./zca-constants.js";
2
2
 
3
3
  export type ZcaFriend = {
4
4
  userId: string;
@@ -61,6 +61,7 @@ export type ZaloSendOptions = {
61
61
  caption?: string;
62
62
  isGroup?: boolean;
63
63
  mediaLocalRoots?: readonly string[];
64
+ mediaReadFile?: (filePath: string) => Promise<Buffer>;
64
65
  textMode?: "markdown" | "plain";
65
66
  textChunkMode?: "length" | "newline";
66
67
  textChunkLimit?: number;
@@ -84,10 +85,9 @@ export type ZaloAuthStatus = {
84
85
  message: string;
85
86
  };
86
87
 
87
- export type ZalouserToolConfig = { allow?: string[]; deny?: string[] };
88
+ type ZalouserToolConfig = { allow?: string[]; deny?: string[] };
88
89
 
89
90
  export type ZalouserGroupConfig = {
90
- allow?: boolean;
91
91
  enabled?: boolean;
92
92
  requireMention?: boolean;
93
93
  tools?: ZalouserToolConfig;