@nextclaw/channel-plugin-feishu 0.2.12 → 0.2.14

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 (102) hide show
  1. package/README.md +3 -1
  2. package/index.ts +65 -0
  3. package/openclaw.plugin.json +3 -7
  4. package/package.json +33 -9
  5. package/skills/feishu-doc/SKILL.md +211 -0
  6. package/skills/feishu-doc/references/block-types.md +103 -0
  7. package/skills/feishu-drive/SKILL.md +97 -0
  8. package/skills/feishu-perm/SKILL.md +119 -0
  9. package/skills/feishu-wiki/SKILL.md +111 -0
  10. package/src/accounts.test.ts +371 -0
  11. package/src/accounts.ts +244 -0
  12. package/src/async.ts +62 -0
  13. package/src/bitable.ts +725 -0
  14. package/src/bot.card-action.test.ts +63 -0
  15. package/src/bot.checkBotMentioned.test.ts +193 -0
  16. package/src/bot.stripBotMention.test.ts +134 -0
  17. package/src/bot.test.ts +2107 -0
  18. package/src/bot.ts +1556 -0
  19. package/src/card-action.ts +79 -0
  20. package/src/channel.test.ts +48 -0
  21. package/src/channel.ts +369 -0
  22. package/src/chat-schema.ts +24 -0
  23. package/src/chat.test.ts +89 -0
  24. package/src/chat.ts +130 -0
  25. package/src/client.test.ts +324 -0
  26. package/src/client.ts +196 -0
  27. package/src/config-schema.test.ts +247 -0
  28. package/src/config-schema.ts +306 -0
  29. package/src/dedup.ts +203 -0
  30. package/src/directory.test.ts +40 -0
  31. package/src/directory.ts +156 -0
  32. package/src/doc-schema.ts +182 -0
  33. package/src/docx-batch-insert.test.ts +90 -0
  34. package/src/docx-batch-insert.ts +187 -0
  35. package/src/docx-color-text.ts +149 -0
  36. package/src/docx-table-ops.ts +298 -0
  37. package/src/docx.account-selection.test.ts +70 -0
  38. package/src/docx.test.ts +445 -0
  39. package/src/docx.ts +1460 -0
  40. package/src/drive-schema.ts +46 -0
  41. package/src/drive.ts +228 -0
  42. package/src/dynamic-agent.ts +131 -0
  43. package/src/external-keys.test.ts +20 -0
  44. package/src/external-keys.ts +19 -0
  45. package/src/feishu-command-handler.ts +59 -0
  46. package/src/media.test.ts +523 -0
  47. package/src/media.ts +484 -0
  48. package/src/mention.ts +133 -0
  49. package/src/monitor.account.ts +562 -0
  50. package/src/monitor.reaction.test.ts +653 -0
  51. package/src/monitor.startup.test.ts +190 -0
  52. package/src/monitor.startup.ts +64 -0
  53. package/src/monitor.state.defaults.test.ts +46 -0
  54. package/src/monitor.state.ts +155 -0
  55. package/src/monitor.test-mocks.ts +45 -0
  56. package/src/monitor.transport.ts +264 -0
  57. package/src/monitor.ts +95 -0
  58. package/src/monitor.webhook-e2e.test.ts +214 -0
  59. package/src/monitor.webhook-security.test.ts +142 -0
  60. package/src/monitor.webhook.test-helpers.ts +98 -0
  61. package/src/onboarding.status.test.ts +25 -0
  62. package/src/onboarding.test.ts +143 -0
  63. package/src/onboarding.ts +489 -0
  64. package/src/outbound.test.ts +356 -0
  65. package/src/outbound.ts +176 -0
  66. package/src/perm-schema.ts +52 -0
  67. package/src/perm.ts +176 -0
  68. package/src/policy.test.ts +154 -0
  69. package/src/policy.ts +123 -0
  70. package/src/post.test.ts +105 -0
  71. package/src/post.ts +274 -0
  72. package/src/probe.test.ts +270 -0
  73. package/src/probe.ts +156 -0
  74. package/src/reactions.ts +153 -0
  75. package/src/reply-dispatcher.test.ts +513 -0
  76. package/src/reply-dispatcher.ts +397 -0
  77. package/src/runtime.ts +6 -0
  78. package/src/secret-input.ts +13 -0
  79. package/src/send-message.ts +71 -0
  80. package/src/send-result.ts +29 -0
  81. package/src/send-target.test.ts +74 -0
  82. package/src/send-target.ts +29 -0
  83. package/src/send.reply-fallback.test.ts +189 -0
  84. package/src/send.test.ts +168 -0
  85. package/src/send.ts +481 -0
  86. package/src/streaming-card.test.ts +54 -0
  87. package/src/streaming-card.ts +374 -0
  88. package/src/targets.test.ts +70 -0
  89. package/src/targets.ts +107 -0
  90. package/src/tool-account-routing.test.ts +129 -0
  91. package/src/tool-account.ts +70 -0
  92. package/src/tool-factory-test-harness.ts +76 -0
  93. package/src/tool-result.test.ts +32 -0
  94. package/src/tool-result.ts +14 -0
  95. package/src/tools-config.test.ts +21 -0
  96. package/src/tools-config.ts +22 -0
  97. package/src/types.ts +103 -0
  98. package/src/typing.test.ts +144 -0
  99. package/src/typing.ts +210 -0
  100. package/src/wiki-schema.ts +55 -0
  101. package/src/wiki.ts +233 -0
  102. package/index.js +0 -27
@@ -0,0 +1,79 @@
1
+ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
2
+ import { resolveFeishuAccount } from "./accounts.js";
3
+ import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
4
+
5
+ export type FeishuCardActionEvent = {
6
+ operator: {
7
+ open_id: string;
8
+ user_id: string;
9
+ union_id: string;
10
+ };
11
+ token: string;
12
+ action: {
13
+ value: Record<string, unknown>;
14
+ tag: string;
15
+ };
16
+ context: {
17
+ open_id: string;
18
+ user_id: string;
19
+ chat_id: string;
20
+ };
21
+ };
22
+
23
+ export async function handleFeishuCardAction(params: {
24
+ cfg: ClawdbotConfig;
25
+ event: FeishuCardActionEvent;
26
+ botOpenId?: string;
27
+ runtime?: RuntimeEnv;
28
+ accountId?: string;
29
+ }): Promise<void> {
30
+ const { cfg, event, runtime, accountId } = params;
31
+ const account = resolveFeishuAccount({ cfg, accountId });
32
+ const log = runtime?.log ?? console.log;
33
+
34
+ // Extract action value
35
+ const actionValue = event.action.value;
36
+ let content = "";
37
+ if (typeof actionValue === "object" && actionValue !== null) {
38
+ if ("text" in actionValue && typeof actionValue.text === "string") {
39
+ content = actionValue.text;
40
+ } else if ("command" in actionValue && typeof actionValue.command === "string") {
41
+ content = actionValue.command;
42
+ } else {
43
+ content = JSON.stringify(actionValue);
44
+ }
45
+ } else {
46
+ content = String(actionValue);
47
+ }
48
+
49
+ // Construct a synthetic message event
50
+ const messageEvent: FeishuMessageEvent = {
51
+ sender: {
52
+ sender_id: {
53
+ open_id: event.operator.open_id,
54
+ user_id: event.operator.user_id,
55
+ union_id: event.operator.union_id,
56
+ },
57
+ },
58
+ message: {
59
+ message_id: `card-action-${event.token}`,
60
+ chat_id: event.context.chat_id || event.operator.open_id,
61
+ chat_type: event.context.chat_id ? "group" : "p2p",
62
+ message_type: "text",
63
+ content: JSON.stringify({ text: content }),
64
+ },
65
+ };
66
+
67
+ log(
68
+ `feishu[${account.accountId}]: handling card action from ${event.operator.open_id}: ${content}`,
69
+ );
70
+
71
+ // Dispatch as normal message
72
+ await handleFeishuMessage({
73
+ cfg,
74
+ event: messageEvent,
75
+ botOpenId: params.botOpenId,
76
+ runtime,
77
+ accountId,
78
+ });
79
+ }
@@ -0,0 +1,48 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/feishu";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ const probeFeishuMock = vi.hoisted(() => vi.fn());
5
+
6
+ vi.mock("./probe.js", () => ({
7
+ probeFeishu: probeFeishuMock,
8
+ }));
9
+
10
+ import { feishuPlugin } from "./channel.js";
11
+
12
+ describe("feishuPlugin.status.probeAccount", () => {
13
+ it("uses current account credentials for multi-account config", async () => {
14
+ const cfg = {
15
+ channels: {
16
+ feishu: {
17
+ enabled: true,
18
+ accounts: {
19
+ main: {
20
+ appId: "cli_main",
21
+ appSecret: "secret_main",
22
+ enabled: true,
23
+ },
24
+ },
25
+ },
26
+ },
27
+ } as OpenClawConfig;
28
+
29
+ const account = feishuPlugin.config.resolveAccount(cfg, "main");
30
+ probeFeishuMock.mockResolvedValueOnce({ ok: true, appId: "cli_main" });
31
+
32
+ const result = await feishuPlugin.status?.probeAccount?.({
33
+ account,
34
+ timeoutMs: 1_000,
35
+ cfg,
36
+ });
37
+
38
+ expect(probeFeishuMock).toHaveBeenCalledTimes(1);
39
+ expect(probeFeishuMock).toHaveBeenCalledWith(
40
+ expect.objectContaining({
41
+ accountId: "main",
42
+ appId: "cli_main",
43
+ appSecret: "secret_main",
44
+ }),
45
+ );
46
+ expect(result).toMatchObject({ ok: true, appId: "cli_main" });
47
+ });
48
+ });
package/src/channel.ts ADDED
@@ -0,0 +1,369 @@
1
+ import {
2
+ collectAllowlistProviderRestrictSendersWarnings,
3
+ formatAllowFromLowercase,
4
+ mapAllowFromEntries,
5
+ } from "openclaw/plugin-sdk/compat";
6
+ import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
7
+ import {
8
+ buildProbeChannelStatusSummary,
9
+ buildRuntimeAccountStatusSnapshot,
10
+ createDefaultChannelRuntimeState,
11
+ DEFAULT_ACCOUNT_ID,
12
+ PAIRING_APPROVED_MESSAGE,
13
+ } from "openclaw/plugin-sdk/feishu";
14
+ import {
15
+ resolveFeishuAccount,
16
+ resolveFeishuCredentials,
17
+ listFeishuAccountIds,
18
+ resolveDefaultFeishuAccountId,
19
+ } from "./accounts.js";
20
+ import {
21
+ listFeishuDirectoryPeers,
22
+ listFeishuDirectoryGroups,
23
+ listFeishuDirectoryPeersLive,
24
+ listFeishuDirectoryGroupsLive,
25
+ } from "./directory.js";
26
+ import { feishuOnboardingAdapter } from "./onboarding.js";
27
+ import { feishuOutbound } from "./outbound.js";
28
+ import { resolveFeishuGroupToolPolicy } from "./policy.js";
29
+ import { probeFeishu } from "./probe.js";
30
+ import { sendMessageFeishu } from "./send.js";
31
+ import { normalizeFeishuTarget, looksLikeFeishuId, formatFeishuTarget } from "./targets.js";
32
+ import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
33
+
34
+ const meta: ChannelMeta = {
35
+ id: "feishu",
36
+ label: "Feishu",
37
+ selectionLabel: "Feishu/Lark (飞书)",
38
+ docsPath: "/channels/feishu",
39
+ docsLabel: "feishu",
40
+ blurb: "飞书/Lark enterprise messaging.",
41
+ aliases: ["lark"],
42
+ order: 70,
43
+ };
44
+
45
+ const secretInputJsonSchema = {
46
+ oneOf: [
47
+ { type: "string" },
48
+ {
49
+ type: "object",
50
+ additionalProperties: false,
51
+ required: ["source", "provider", "id"],
52
+ properties: {
53
+ source: { type: "string", enum: ["env", "file", "exec"] },
54
+ provider: { type: "string", minLength: 1 },
55
+ id: { type: "string", minLength: 1 },
56
+ },
57
+ },
58
+ ],
59
+ } as const;
60
+
61
+ function setFeishuNamedAccountEnabled(
62
+ cfg: ClawdbotConfig,
63
+ accountId: string,
64
+ enabled: boolean,
65
+ ): ClawdbotConfig {
66
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
67
+ return {
68
+ ...cfg,
69
+ channels: {
70
+ ...cfg.channels,
71
+ feishu: {
72
+ ...feishuCfg,
73
+ accounts: {
74
+ ...feishuCfg?.accounts,
75
+ [accountId]: {
76
+ ...feishuCfg?.accounts?.[accountId],
77
+ enabled,
78
+ },
79
+ },
80
+ },
81
+ },
82
+ };
83
+ }
84
+
85
+ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
86
+ id: "feishu",
87
+ meta: {
88
+ ...meta,
89
+ },
90
+ pairing: {
91
+ idLabel: "feishuUserId",
92
+ normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
93
+ notifyApproval: async ({ cfg, id }) => {
94
+ await sendMessageFeishu({
95
+ cfg,
96
+ to: id,
97
+ text: PAIRING_APPROVED_MESSAGE,
98
+ });
99
+ },
100
+ },
101
+ capabilities: {
102
+ chatTypes: ["direct", "channel"],
103
+ polls: false,
104
+ threads: true,
105
+ media: true,
106
+ reactions: true,
107
+ edit: true,
108
+ reply: true,
109
+ },
110
+ agentPrompt: {
111
+ messageToolHints: () => [
112
+ "- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.",
113
+ "- Feishu supports interactive cards for rich messages.",
114
+ ],
115
+ },
116
+ groups: {
117
+ resolveToolPolicy: resolveFeishuGroupToolPolicy,
118
+ },
119
+ mentions: {
120
+ stripPatterns: () => ['<at user_id="[^"]*">[^<]*</at>'],
121
+ },
122
+ reload: { configPrefixes: ["channels.feishu"] },
123
+ configSchema: {
124
+ schema: {
125
+ type: "object",
126
+ additionalProperties: false,
127
+ properties: {
128
+ enabled: { type: "boolean" },
129
+ defaultAccount: { type: "string" },
130
+ appId: { type: "string" },
131
+ appSecret: secretInputJsonSchema,
132
+ encryptKey: secretInputJsonSchema,
133
+ verificationToken: secretInputJsonSchema,
134
+ domain: {
135
+ oneOf: [
136
+ { type: "string", enum: ["feishu", "lark"] },
137
+ { type: "string", format: "uri", pattern: "^https://" },
138
+ ],
139
+ },
140
+ connectionMode: { type: "string", enum: ["websocket", "webhook"] },
141
+ webhookPath: { type: "string" },
142
+ webhookHost: { type: "string" },
143
+ webhookPort: { type: "integer", minimum: 1 },
144
+ dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
145
+ allowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
146
+ groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
147
+ groupAllowFrom: {
148
+ type: "array",
149
+ items: { oneOf: [{ type: "string" }, { type: "number" }] },
150
+ },
151
+ requireMention: { type: "boolean" },
152
+ groupSessionScope: {
153
+ type: "string",
154
+ enum: ["group", "group_sender", "group_topic", "group_topic_sender"],
155
+ },
156
+ topicSessionMode: { type: "string", enum: ["disabled", "enabled"] },
157
+ replyInThread: { type: "string", enum: ["disabled", "enabled"] },
158
+ historyLimit: { type: "integer", minimum: 0 },
159
+ dmHistoryLimit: { type: "integer", minimum: 0 },
160
+ textChunkLimit: { type: "integer", minimum: 1 },
161
+ chunkMode: { type: "string", enum: ["length", "newline"] },
162
+ mediaMaxMb: { type: "number", minimum: 0 },
163
+ renderMode: { type: "string", enum: ["auto", "raw", "card"] },
164
+ accounts: {
165
+ type: "object",
166
+ additionalProperties: {
167
+ type: "object",
168
+ properties: {
169
+ enabled: { type: "boolean" },
170
+ name: { type: "string" },
171
+ appId: { type: "string" },
172
+ appSecret: secretInputJsonSchema,
173
+ encryptKey: secretInputJsonSchema,
174
+ verificationToken: secretInputJsonSchema,
175
+ domain: { type: "string", enum: ["feishu", "lark"] },
176
+ connectionMode: { type: "string", enum: ["websocket", "webhook"] },
177
+ webhookHost: { type: "string" },
178
+ webhookPath: { type: "string" },
179
+ webhookPort: { type: "integer", minimum: 1 },
180
+ },
181
+ },
182
+ },
183
+ },
184
+ },
185
+ },
186
+ config: {
187
+ listAccountIds: (cfg) => listFeishuAccountIds(cfg),
188
+ resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
189
+ defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
190
+ setAccountEnabled: ({ cfg, accountId, enabled }) => {
191
+ const account = resolveFeishuAccount({ cfg, accountId });
192
+ const isDefault = accountId === DEFAULT_ACCOUNT_ID;
193
+
194
+ if (isDefault) {
195
+ // For default account, set top-level enabled
196
+ return {
197
+ ...cfg,
198
+ channels: {
199
+ ...cfg.channels,
200
+ feishu: {
201
+ ...cfg.channels?.feishu,
202
+ enabled,
203
+ },
204
+ },
205
+ };
206
+ }
207
+
208
+ // For named accounts, set enabled in accounts[accountId]
209
+ return setFeishuNamedAccountEnabled(cfg, accountId, enabled);
210
+ },
211
+ deleteAccount: ({ cfg, accountId }) => {
212
+ const isDefault = accountId === DEFAULT_ACCOUNT_ID;
213
+
214
+ if (isDefault) {
215
+ // Delete entire feishu config
216
+ const next = { ...cfg } as ClawdbotConfig;
217
+ const nextChannels = { ...cfg.channels };
218
+ delete (nextChannels as Record<string, unknown>).feishu;
219
+ if (Object.keys(nextChannels).length > 0) {
220
+ next.channels = nextChannels;
221
+ } else {
222
+ delete next.channels;
223
+ }
224
+ return next;
225
+ }
226
+
227
+ // Delete specific account from accounts
228
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
229
+ const accounts = { ...feishuCfg?.accounts };
230
+ delete accounts[accountId];
231
+
232
+ return {
233
+ ...cfg,
234
+ channels: {
235
+ ...cfg.channels,
236
+ feishu: {
237
+ ...feishuCfg,
238
+ accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
239
+ },
240
+ },
241
+ };
242
+ },
243
+ isConfigured: (account) => account.configured,
244
+ describeAccount: (account) => ({
245
+ accountId: account.accountId,
246
+ enabled: account.enabled,
247
+ configured: account.configured,
248
+ name: account.name,
249
+ appId: account.appId,
250
+ domain: account.domain,
251
+ }),
252
+ resolveAllowFrom: ({ cfg, accountId }) => {
253
+ const account = resolveFeishuAccount({ cfg, accountId });
254
+ return mapAllowFromEntries(account.config?.allowFrom);
255
+ },
256
+ formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }),
257
+ },
258
+ security: {
259
+ collectWarnings: ({ cfg, accountId }) => {
260
+ const account = resolveFeishuAccount({ cfg, accountId });
261
+ const feishuCfg = account.config;
262
+ return collectAllowlistProviderRestrictSendersWarnings({
263
+ cfg,
264
+ providerConfigPresent: cfg.channels?.feishu !== undefined,
265
+ configuredGroupPolicy: feishuCfg?.groupPolicy,
266
+ surface: `Feishu[${account.accountId}] groups`,
267
+ openScope: "any member",
268
+ groupPolicyPath: "channels.feishu.groupPolicy",
269
+ groupAllowFromPath: "channels.feishu.groupAllowFrom",
270
+ });
271
+ },
272
+ },
273
+ setup: {
274
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
275
+ applyAccountConfig: ({ cfg, accountId }) => {
276
+ const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
277
+
278
+ if (isDefault) {
279
+ return {
280
+ ...cfg,
281
+ channels: {
282
+ ...cfg.channels,
283
+ feishu: {
284
+ ...cfg.channels?.feishu,
285
+ enabled: true,
286
+ },
287
+ },
288
+ };
289
+ }
290
+
291
+ return setFeishuNamedAccountEnabled(cfg, accountId, true);
292
+ },
293
+ },
294
+ onboarding: feishuOnboardingAdapter,
295
+ messaging: {
296
+ normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? undefined,
297
+ targetResolver: {
298
+ looksLikeId: looksLikeFeishuId,
299
+ hint: "<chatId|user:openId|chat:chatId>",
300
+ },
301
+ },
302
+ directory: {
303
+ self: async () => null,
304
+ listPeers: async ({ cfg, query, limit, accountId }) =>
305
+ listFeishuDirectoryPeers({
306
+ cfg,
307
+ query: query ?? undefined,
308
+ limit: limit ?? undefined,
309
+ accountId: accountId ?? undefined,
310
+ }),
311
+ listGroups: async ({ cfg, query, limit, accountId }) =>
312
+ listFeishuDirectoryGroups({
313
+ cfg,
314
+ query: query ?? undefined,
315
+ limit: limit ?? undefined,
316
+ accountId: accountId ?? undefined,
317
+ }),
318
+ listPeersLive: async ({ cfg, query, limit, accountId }) =>
319
+ listFeishuDirectoryPeersLive({
320
+ cfg,
321
+ query: query ?? undefined,
322
+ limit: limit ?? undefined,
323
+ accountId: accountId ?? undefined,
324
+ }),
325
+ listGroupsLive: async ({ cfg, query, limit, accountId }) =>
326
+ listFeishuDirectoryGroupsLive({
327
+ cfg,
328
+ query: query ?? undefined,
329
+ limit: limit ?? undefined,
330
+ accountId: accountId ?? undefined,
331
+ }),
332
+ },
333
+ outbound: feishuOutbound,
334
+ status: {
335
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
336
+ buildChannelSummary: ({ snapshot }) =>
337
+ buildProbeChannelStatusSummary(snapshot, {
338
+ port: snapshot.port ?? null,
339
+ }),
340
+ probeAccount: async ({ account }) => await probeFeishu(account),
341
+ buildAccountSnapshot: ({ account, runtime, probe }) => ({
342
+ accountId: account.accountId,
343
+ enabled: account.enabled,
344
+ configured: account.configured,
345
+ name: account.name,
346
+ appId: account.appId,
347
+ domain: account.domain,
348
+ ...buildRuntimeAccountStatusSnapshot({ runtime, probe }),
349
+ port: runtime?.port ?? null,
350
+ }),
351
+ },
352
+ gateway: {
353
+ startAccount: async (ctx) => {
354
+ const { monitorFeishuProvider } = await import("./monitor.js");
355
+ const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
356
+ const port = account.config?.webhookPort ?? null;
357
+ ctx.setStatus({ accountId: ctx.accountId, port });
358
+ ctx.log?.info(
359
+ `starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`,
360
+ );
361
+ return monitorFeishuProvider({
362
+ config: ctx.cfg,
363
+ runtime: ctx.runtime,
364
+ abortSignal: ctx.abortSignal,
365
+ accountId: ctx.accountId,
366
+ });
367
+ },
368
+ },
369
+ };
@@ -0,0 +1,24 @@
1
+ import { Type, type Static } from "@sinclair/typebox";
2
+
3
+ const CHAT_ACTION_VALUES = ["members", "info"] as const;
4
+ const MEMBER_ID_TYPE_VALUES = ["open_id", "user_id", "union_id"] as const;
5
+
6
+ export const FeishuChatSchema = Type.Object({
7
+ action: Type.Unsafe<(typeof CHAT_ACTION_VALUES)[number]>({
8
+ type: "string",
9
+ enum: [...CHAT_ACTION_VALUES],
10
+ description: "Action to run: members | info",
11
+ }),
12
+ chat_id: Type.String({ description: "Chat ID (from URL or event payload)" }),
13
+ page_size: Type.Optional(Type.Number({ description: "Page size (1-100, default 50)" })),
14
+ page_token: Type.Optional(Type.String({ description: "Pagination token" })),
15
+ member_id_type: Type.Optional(
16
+ Type.Unsafe<(typeof MEMBER_ID_TYPE_VALUES)[number]>({
17
+ type: "string",
18
+ enum: [...MEMBER_ID_TYPE_VALUES],
19
+ description: "Member ID type (default: open_id)",
20
+ }),
21
+ ),
22
+ });
23
+
24
+ export type FeishuChatParams = Static<typeof FeishuChatSchema>;
@@ -0,0 +1,89 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { registerFeishuChatTools } from "./chat.js";
3
+
4
+ const createFeishuClientMock = vi.hoisted(() => vi.fn());
5
+
6
+ vi.mock("./client.js", () => ({
7
+ createFeishuClient: createFeishuClientMock,
8
+ }));
9
+
10
+ describe("registerFeishuChatTools", () => {
11
+ const chatGetMock = vi.hoisted(() => vi.fn());
12
+ const chatMembersGetMock = vi.hoisted(() => vi.fn());
13
+
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ createFeishuClientMock.mockReturnValue({
17
+ im: {
18
+ chat: { get: chatGetMock },
19
+ chatMembers: { get: chatMembersGetMock },
20
+ },
21
+ });
22
+ });
23
+
24
+ it("registers feishu_chat and handles info/members actions", async () => {
25
+ const registerTool = vi.fn();
26
+ registerFeishuChatTools({
27
+ config: {
28
+ channels: {
29
+ feishu: {
30
+ enabled: true,
31
+ appId: "app_id",
32
+ appSecret: "app_secret", // pragma: allowlist secret
33
+ tools: { chat: true },
34
+ },
35
+ },
36
+ } as any,
37
+ logger: { debug: vi.fn(), info: vi.fn() } as any,
38
+ registerTool,
39
+ } as any);
40
+
41
+ expect(registerTool).toHaveBeenCalledTimes(1);
42
+ const tool = registerTool.mock.calls[0]?.[0];
43
+ expect(tool?.name).toBe("feishu_chat");
44
+
45
+ chatGetMock.mockResolvedValueOnce({
46
+ code: 0,
47
+ data: { name: "group name", user_count: 3 },
48
+ });
49
+ const infoResult = await tool.execute("tc_1", { action: "info", chat_id: "oc_1" });
50
+ expect(infoResult.details).toEqual(
51
+ expect.objectContaining({ chat_id: "oc_1", name: "group name", user_count: 3 }),
52
+ );
53
+
54
+ chatMembersGetMock.mockResolvedValueOnce({
55
+ code: 0,
56
+ data: {
57
+ has_more: false,
58
+ page_token: "",
59
+ items: [{ member_id: "ou_1", name: "member1", member_id_type: "open_id" }],
60
+ },
61
+ });
62
+ const membersResult = await tool.execute("tc_2", { action: "members", chat_id: "oc_1" });
63
+ expect(membersResult.details).toEqual(
64
+ expect.objectContaining({
65
+ chat_id: "oc_1",
66
+ members: [expect.objectContaining({ member_id: "ou_1", name: "member1" })],
67
+ }),
68
+ );
69
+ });
70
+
71
+ it("skips registration when chat tool is disabled", () => {
72
+ const registerTool = vi.fn();
73
+ registerFeishuChatTools({
74
+ config: {
75
+ channels: {
76
+ feishu: {
77
+ enabled: true,
78
+ appId: "app_id",
79
+ appSecret: "app_secret", // pragma: allowlist secret
80
+ tools: { chat: false },
81
+ },
82
+ },
83
+ } as any,
84
+ logger: { debug: vi.fn(), info: vi.fn() } as any,
85
+ registerTool,
86
+ } as any);
87
+ expect(registerTool).not.toHaveBeenCalled();
88
+ });
89
+ });