@nextclaw/channel-plugin-feishu 0.2.28 → 0.2.29-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.
Files changed (116) hide show
  1. package/dist/index.d.ts +23 -0
  2. package/dist/index.js +45 -0
  3. package/dist/src/accounts.js +141 -0
  4. package/dist/src/app-scope-checker.js +36 -0
  5. package/dist/src/async.js +34 -0
  6. package/dist/src/auth-errors.js +72 -0
  7. package/dist/src/bitable.js +495 -0
  8. package/dist/src/bot.d.ts +35 -0
  9. package/dist/src/bot.js +941 -0
  10. package/dist/src/calendar-calendar.js +54 -0
  11. package/dist/src/calendar-event-attendee.js +98 -0
  12. package/dist/src/calendar-event.js +193 -0
  13. package/dist/src/calendar-freebusy.js +40 -0
  14. package/dist/src/calendar-shared.js +23 -0
  15. package/dist/src/calendar.js +16 -0
  16. package/dist/src/card-action.js +49 -0
  17. package/dist/src/channel.d.ts +7 -0
  18. package/dist/src/channel.js +413 -0
  19. package/dist/src/chat-schema.js +25 -0
  20. package/dist/src/chat.js +87 -0
  21. package/dist/src/client.d.ts +16 -0
  22. package/dist/src/client.js +112 -0
  23. package/dist/src/config-schema.d.ts +357 -0
  24. package/dist/src/dedup.js +126 -0
  25. package/dist/src/device-flow.js +109 -0
  26. package/dist/src/directory.js +101 -0
  27. package/dist/src/doc-schema.js +148 -0
  28. package/dist/src/docx-batch-insert.js +104 -0
  29. package/dist/src/docx-color-text.js +80 -0
  30. package/dist/src/docx-table-ops.js +197 -0
  31. package/dist/src/docx.js +858 -0
  32. package/dist/src/domains.js +14 -0
  33. package/dist/src/drive-schema.js +41 -0
  34. package/dist/src/drive.js +126 -0
  35. package/dist/src/dynamic-agent.js +93 -0
  36. package/dist/src/external-keys.js +13 -0
  37. package/dist/src/feishu-fetch.js +12 -0
  38. package/dist/src/identity.js +92 -0
  39. package/dist/src/lark-ticket.js +11 -0
  40. package/dist/src/media.d.ts +75 -0
  41. package/dist/src/media.js +304 -0
  42. package/dist/src/mention.d.ts +52 -0
  43. package/dist/src/mention.js +82 -0
  44. package/dist/src/monitor.account.d.ts +1 -0
  45. package/dist/src/monitor.account.js +393 -0
  46. package/dist/src/monitor.d.ts +11 -0
  47. package/dist/src/monitor.js +58 -0
  48. package/dist/src/monitor.startup.js +24 -0
  49. package/dist/src/monitor.state.d.ts +1 -0
  50. package/dist/src/monitor.state.js +80 -0
  51. package/dist/src/monitor.transport.js +167 -0
  52. package/dist/src/nextclaw-sdk/account-id.js +15 -0
  53. package/dist/src/nextclaw-sdk/core-channel.js +150 -0
  54. package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
  55. package/dist/src/nextclaw-sdk/dedupe.js +164 -0
  56. package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
  57. package/dist/src/nextclaw-sdk/feishu.js +14 -0
  58. package/dist/src/nextclaw-sdk/history.js +69 -0
  59. package/dist/src/nextclaw-sdk/network-body.js +180 -0
  60. package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
  61. package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
  62. package/dist/src/nextclaw-sdk/network.js +4 -0
  63. package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
  64. package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
  65. package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
  66. package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
  67. package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
  68. package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
  69. package/dist/src/nextclaw-sdk/secrets.js +4 -0
  70. package/dist/src/nextclaw-sdk/types.d.ts +242 -0
  71. package/dist/src/oauth.js +171 -0
  72. package/dist/src/onboarding.js +381 -0
  73. package/dist/src/outbound.js +150 -0
  74. package/dist/src/perm-schema.js +49 -0
  75. package/dist/src/perm.js +90 -0
  76. package/dist/src/policy.js +61 -0
  77. package/dist/src/post.js +160 -0
  78. package/dist/src/probe.d.ts +11 -0
  79. package/dist/src/probe.js +85 -0
  80. package/dist/src/raw-request.js +24 -0
  81. package/dist/src/reactions.d.ts +67 -0
  82. package/dist/src/reactions.js +91 -0
  83. package/dist/src/reply-dispatcher.js +250 -0
  84. package/dist/src/runtime.js +5 -0
  85. package/dist/src/secret-input.js +3 -0
  86. package/dist/src/send-result.js +12 -0
  87. package/dist/src/send-target.js +22 -0
  88. package/dist/src/send.d.ts +51 -0
  89. package/dist/src/send.js +265 -0
  90. package/dist/src/sheets-shared.js +193 -0
  91. package/dist/src/sheets.js +95 -0
  92. package/dist/src/streaming-card.js +263 -0
  93. package/dist/src/targets.js +39 -0
  94. package/dist/src/task-comment.js +76 -0
  95. package/dist/src/task-shared.js +13 -0
  96. package/dist/src/task-subtask.js +79 -0
  97. package/dist/src/task-task.js +144 -0
  98. package/dist/src/task-tasklist.js +136 -0
  99. package/dist/src/task.js +16 -0
  100. package/dist/src/token-store.js +154 -0
  101. package/dist/src/tool-account.js +65 -0
  102. package/dist/src/tool-result.js +18 -0
  103. package/dist/src/tool-scopes.js +62 -0
  104. package/dist/src/tools-config.js +30 -0
  105. package/dist/src/types.d.ts +43 -0
  106. package/dist/src/typing.js +145 -0
  107. package/dist/src/uat-client.js +102 -0
  108. package/dist/src/user-tool-client.js +132 -0
  109. package/dist/src/user-tool-helpers.js +110 -0
  110. package/dist/src/user-tool-result.js +10 -0
  111. package/dist/src/wiki-schema.js +45 -0
  112. package/dist/src/wiki.js +144 -0
  113. package/package.json +8 -4
  114. package/src/media.ts +1 -1
  115. package/src/monitor.state.ts +2 -2
  116. package/index.ts +0 -75
@@ -0,0 +1,413 @@
1
+ import { DEFAULT_ACCOUNT_ID } from "./nextclaw-sdk/account-id.js";
2
+ import { buildProbeChannelStatusSummary, buildRuntimeAccountStatusSnapshot, collectAllowlistProviderRestrictSendersWarnings, createDefaultChannelRuntimeState, formatAllowFromLowercase, mapAllowFromEntries } from "./nextclaw-sdk/core-channel.js";
3
+ import { PAIRING_APPROVED_MESSAGE } from "./nextclaw-sdk/core-pairing.js";
4
+ import "./nextclaw-sdk/feishu.js";
5
+ import { listFeishuAccountIds, resolveDefaultFeishuAccountId, resolveFeishuAccount } from "./accounts.js";
6
+ import { looksLikeFeishuId, normalizeFeishuTarget } from "./targets.js";
7
+ import { listFeishuDirectoryGroups, listFeishuDirectoryGroupsLive, listFeishuDirectoryPeers, listFeishuDirectoryPeersLive } from "./directory.js";
8
+ import { probeFeishu } from "./probe.js";
9
+ import { feishuOnboardingAdapter } from "./onboarding.js";
10
+ import { sendMessageFeishu } from "./send.js";
11
+ import { feishuOutbound } from "./outbound.js";
12
+ import { resolveFeishuGroupToolPolicy } from "./policy.js";
13
+ //#region src/channel.ts
14
+ const meta = {
15
+ id: "feishu",
16
+ label: "Feishu",
17
+ selectionLabel: "Feishu/Lark (飞书)",
18
+ docsPath: "/channels/feishu",
19
+ docsLabel: "feishu",
20
+ blurb: "飞书/Lark enterprise messaging.",
21
+ aliases: ["lark"],
22
+ order: 70
23
+ };
24
+ const secretInputJsonSchema = { oneOf: [{ type: "string" }, {
25
+ type: "object",
26
+ additionalProperties: false,
27
+ required: [
28
+ "source",
29
+ "provider",
30
+ "id"
31
+ ],
32
+ properties: {
33
+ source: {
34
+ type: "string",
35
+ enum: [
36
+ "env",
37
+ "file",
38
+ "exec"
39
+ ]
40
+ },
41
+ provider: {
42
+ type: "string",
43
+ minLength: 1
44
+ },
45
+ id: {
46
+ type: "string",
47
+ minLength: 1
48
+ }
49
+ }
50
+ }] };
51
+ function setFeishuNamedAccountEnabled(cfg, accountId, enabled) {
52
+ const feishuCfg = cfg.channels?.feishu;
53
+ return {
54
+ ...cfg,
55
+ channels: {
56
+ ...cfg.channels,
57
+ feishu: {
58
+ ...feishuCfg,
59
+ accounts: {
60
+ ...feishuCfg?.accounts,
61
+ [accountId]: {
62
+ ...feishuCfg?.accounts?.[accountId],
63
+ enabled
64
+ }
65
+ }
66
+ }
67
+ }
68
+ };
69
+ }
70
+ const feishuPlugin = {
71
+ id: "feishu",
72
+ meta: { ...meta },
73
+ pairing: {
74
+ idLabel: "feishuUserId",
75
+ normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
76
+ notifyApproval: async ({ cfg, id }) => {
77
+ await sendMessageFeishu({
78
+ cfg,
79
+ to: id,
80
+ text: PAIRING_APPROVED_MESSAGE
81
+ });
82
+ }
83
+ },
84
+ capabilities: {
85
+ chatTypes: ["direct", "channel"],
86
+ polls: false,
87
+ threads: true,
88
+ media: true,
89
+ reactions: true,
90
+ edit: true,
91
+ reply: true
92
+ },
93
+ agentPrompt: { messageToolHints: () => [
94
+ "- Feishu targeting: omit `target` only when replying in the current Feishu conversation. For proactive sends from UI/CLI/another channel, pass an explicit target such as `user:open_id` or `chat:chat_id`.",
95
+ "- If the current session is not Feishu, never rely on `channel=feishu` alone; resolve the route first (for example from an existing Feishu session) and then send to that explicit target.",
96
+ "- Feishu supports interactive cards for rich messages."
97
+ ] },
98
+ groups: { resolveToolPolicy: resolveFeishuGroupToolPolicy },
99
+ mentions: { stripPatterns: () => ["<at user_id=\"[^\"]*\">[^<]*</at>"] },
100
+ reload: { configPrefixes: ["channels.feishu"] },
101
+ configSchema: { schema: {
102
+ type: "object",
103
+ additionalProperties: false,
104
+ properties: {
105
+ enabled: { type: "boolean" },
106
+ defaultAccount: { type: "string" },
107
+ appId: { type: "string" },
108
+ appSecret: secretInputJsonSchema,
109
+ encryptKey: secretInputJsonSchema,
110
+ verificationToken: secretInputJsonSchema,
111
+ domain: { oneOf: [{
112
+ type: "string",
113
+ enum: ["feishu", "lark"]
114
+ }, {
115
+ type: "string",
116
+ format: "uri",
117
+ pattern: "^https://"
118
+ }] },
119
+ connectionMode: {
120
+ type: "string",
121
+ enum: ["websocket", "webhook"]
122
+ },
123
+ webhookPath: { type: "string" },
124
+ webhookHost: { type: "string" },
125
+ webhookPort: {
126
+ type: "integer",
127
+ minimum: 1
128
+ },
129
+ dmPolicy: {
130
+ type: "string",
131
+ enum: [
132
+ "open",
133
+ "pairing",
134
+ "allowlist"
135
+ ]
136
+ },
137
+ allowFrom: {
138
+ type: "array",
139
+ items: { oneOf: [{ type: "string" }, { type: "number" }] }
140
+ },
141
+ groupPolicy: {
142
+ type: "string",
143
+ enum: [
144
+ "open",
145
+ "allowlist",
146
+ "disabled"
147
+ ]
148
+ },
149
+ groupAllowFrom: {
150
+ type: "array",
151
+ items: { oneOf: [{ type: "string" }, { type: "number" }] }
152
+ },
153
+ requireMention: { type: "boolean" },
154
+ groupSessionScope: {
155
+ type: "string",
156
+ enum: [
157
+ "group",
158
+ "group_sender",
159
+ "group_topic",
160
+ "group_topic_sender"
161
+ ]
162
+ },
163
+ topicSessionMode: {
164
+ type: "string",
165
+ enum: ["disabled", "enabled"]
166
+ },
167
+ replyInThread: {
168
+ type: "string",
169
+ enum: ["disabled", "enabled"]
170
+ },
171
+ historyLimit: {
172
+ type: "integer",
173
+ minimum: 0
174
+ },
175
+ dmHistoryLimit: {
176
+ type: "integer",
177
+ minimum: 0
178
+ },
179
+ textChunkLimit: {
180
+ type: "integer",
181
+ minimum: 1
182
+ },
183
+ chunkMode: {
184
+ type: "string",
185
+ enum: ["length", "newline"]
186
+ },
187
+ mediaMaxMb: {
188
+ type: "number",
189
+ minimum: 0
190
+ },
191
+ renderMode: {
192
+ type: "string",
193
+ enum: [
194
+ "auto",
195
+ "raw",
196
+ "card"
197
+ ]
198
+ },
199
+ accounts: {
200
+ type: "object",
201
+ additionalProperties: {
202
+ type: "object",
203
+ properties: {
204
+ enabled: { type: "boolean" },
205
+ name: { type: "string" },
206
+ appId: { type: "string" },
207
+ appSecret: secretInputJsonSchema,
208
+ encryptKey: secretInputJsonSchema,
209
+ verificationToken: secretInputJsonSchema,
210
+ domain: {
211
+ type: "string",
212
+ enum: ["feishu", "lark"]
213
+ },
214
+ connectionMode: {
215
+ type: "string",
216
+ enum: ["websocket", "webhook"]
217
+ },
218
+ webhookHost: { type: "string" },
219
+ webhookPath: { type: "string" },
220
+ webhookPort: {
221
+ type: "integer",
222
+ minimum: 1
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ } },
229
+ config: {
230
+ listAccountIds: (cfg) => listFeishuAccountIds(cfg),
231
+ resolveAccount: (cfg, accountId) => resolveFeishuAccount({
232
+ cfg,
233
+ accountId
234
+ }),
235
+ defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
236
+ setAccountEnabled: ({ cfg, accountId, enabled }) => {
237
+ resolveFeishuAccount({
238
+ cfg,
239
+ accountId
240
+ });
241
+ if (accountId === "default") return {
242
+ ...cfg,
243
+ channels: {
244
+ ...cfg.channels,
245
+ feishu: {
246
+ ...cfg.channels?.feishu,
247
+ enabled
248
+ }
249
+ }
250
+ };
251
+ return setFeishuNamedAccountEnabled(cfg, accountId, enabled);
252
+ },
253
+ deleteAccount: ({ cfg, accountId }) => {
254
+ if (accountId === "default") {
255
+ const next = { ...cfg };
256
+ const nextChannels = { ...cfg.channels };
257
+ delete nextChannels.feishu;
258
+ if (Object.keys(nextChannels).length > 0) next.channels = nextChannels;
259
+ else delete next.channels;
260
+ return next;
261
+ }
262
+ const feishuCfg = cfg.channels?.feishu;
263
+ const accounts = { ...feishuCfg?.accounts };
264
+ delete accounts[accountId];
265
+ return {
266
+ ...cfg,
267
+ channels: {
268
+ ...cfg.channels,
269
+ feishu: {
270
+ ...feishuCfg,
271
+ accounts: Object.keys(accounts).length > 0 ? accounts : void 0
272
+ }
273
+ }
274
+ };
275
+ },
276
+ isConfigured: (account) => account.configured,
277
+ describeAccount: (account) => ({
278
+ accountId: account.accountId,
279
+ enabled: account.enabled,
280
+ configured: account.configured,
281
+ name: account.name,
282
+ appId: account.appId,
283
+ domain: account.domain
284
+ }),
285
+ resolveAllowFrom: ({ cfg, accountId }) => {
286
+ return mapAllowFromEntries(resolveFeishuAccount({
287
+ cfg,
288
+ accountId
289
+ }).config?.allowFrom);
290
+ },
291
+ formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom })
292
+ },
293
+ security: { collectWarnings: ({ cfg, accountId }) => {
294
+ const account = resolveFeishuAccount({
295
+ cfg,
296
+ accountId
297
+ });
298
+ const feishuCfg = account.config;
299
+ return collectAllowlistProviderRestrictSendersWarnings({
300
+ cfg,
301
+ providerConfigPresent: cfg.channels?.feishu !== void 0,
302
+ configuredGroupPolicy: feishuCfg?.groupPolicy,
303
+ surface: `Feishu[${account.accountId}] groups`,
304
+ openScope: "any member",
305
+ groupPolicyPath: "channels.feishu.groupPolicy",
306
+ groupAllowFromPath: "channels.feishu.groupAllowFrom"
307
+ });
308
+ } },
309
+ setup: {
310
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
311
+ applyAccountConfig: ({ cfg, accountId }) => {
312
+ if (!accountId || accountId === "default") return {
313
+ ...cfg,
314
+ channels: {
315
+ ...cfg.channels,
316
+ feishu: {
317
+ ...cfg.channels?.feishu,
318
+ enabled: true
319
+ }
320
+ }
321
+ };
322
+ return setFeishuNamedAccountEnabled(cfg, accountId, true);
323
+ }
324
+ },
325
+ onboarding: feishuOnboardingAdapter,
326
+ messaging: {
327
+ normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? void 0,
328
+ targetResolver: {
329
+ looksLikeId: looksLikeFeishuId,
330
+ hint: "<chatId|user:openId|chat:chatId>"
331
+ }
332
+ },
333
+ directory: {
334
+ self: async () => null,
335
+ listPeers: async ({ cfg, query, limit, accountId }) => listFeishuDirectoryPeers({
336
+ cfg,
337
+ query: query ?? void 0,
338
+ limit: limit ?? void 0,
339
+ accountId: accountId ?? void 0
340
+ }),
341
+ listGroups: async ({ cfg, query, limit, accountId }) => listFeishuDirectoryGroups({
342
+ cfg,
343
+ query: query ?? void 0,
344
+ limit: limit ?? void 0,
345
+ accountId: accountId ?? void 0
346
+ }),
347
+ listPeersLive: async ({ cfg, query, limit, accountId }) => listFeishuDirectoryPeersLive({
348
+ cfg,
349
+ query: query ?? void 0,
350
+ limit: limit ?? void 0,
351
+ accountId: accountId ?? void 0
352
+ }),
353
+ listGroupsLive: async ({ cfg, query, limit, accountId }) => listFeishuDirectoryGroupsLive({
354
+ cfg,
355
+ query: query ?? void 0,
356
+ limit: limit ?? void 0,
357
+ accountId: accountId ?? void 0
358
+ })
359
+ },
360
+ outbound: feishuOutbound,
361
+ status: {
362
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
363
+ buildChannelSummary: ({ snapshot }) => buildProbeChannelStatusSummary(snapshot, { port: snapshot.port ?? null }),
364
+ probeAccount: async ({ account }) => await probeFeishu(account),
365
+ buildAccountSnapshot: ({ account, runtime, probe }) => ({
366
+ accountId: account.accountId,
367
+ enabled: account.enabled,
368
+ configured: account.configured,
369
+ name: account.name,
370
+ appId: account.appId,
371
+ domain: account.domain,
372
+ ...buildRuntimeAccountStatusSnapshot({
373
+ runtime,
374
+ probe
375
+ }),
376
+ port: runtime?.port ?? null
377
+ })
378
+ },
379
+ gateway: { startAccount: async (ctx) => {
380
+ const { monitorFeishuProvider, stopFeishuMonitor } = await import("./monitor.js");
381
+ const account = resolveFeishuAccount({
382
+ cfg: ctx.cfg,
383
+ accountId: ctx.accountId
384
+ });
385
+ const accountId = account.accountId;
386
+ const port = account.config?.webhookPort ?? null;
387
+ ctx.setStatus({
388
+ accountId,
389
+ port
390
+ });
391
+ ctx.log?.info(`starting feishu[${accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`);
392
+ const monitorTask = (async () => {
393
+ try {
394
+ await monitorFeishuProvider({
395
+ config: ctx.cfg,
396
+ runtime: ctx.runtime,
397
+ abortSignal: ctx.abortSignal,
398
+ accountId
399
+ });
400
+ } catch (error) {
401
+ if (ctx.abortSignal?.aborted) return;
402
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
403
+ ctx.log?.error?.(`feishu[${accountId}]: gateway monitor stopped unexpectedly: ${message}`);
404
+ }
405
+ })();
406
+ return { stop: async () => {
407
+ stopFeishuMonitor(accountId);
408
+ await monitorTask;
409
+ } };
410
+ } }
411
+ };
412
+ //#endregion
413
+ export { feishuPlugin };
@@ -0,0 +1,25 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ //#region src/chat-schema.ts
3
+ const CHAT_ACTION_VALUES = ["members", "info"];
4
+ const MEMBER_ID_TYPE_VALUES = [
5
+ "open_id",
6
+ "user_id",
7
+ "union_id"
8
+ ];
9
+ const FeishuChatSchema = Type.Object({
10
+ action: Type.Unsafe({
11
+ type: "string",
12
+ enum: [...CHAT_ACTION_VALUES],
13
+ description: "Action to run: members | info"
14
+ }),
15
+ chat_id: Type.String({ description: "Chat ID (from URL or event payload)" }),
16
+ page_size: Type.Optional(Type.Number({ description: "Page size (1-100, default 50)" })),
17
+ page_token: Type.Optional(Type.String({ description: "Pagination token" })),
18
+ member_id_type: Type.Optional(Type.Unsafe({
19
+ type: "string",
20
+ enum: [...MEMBER_ID_TYPE_VALUES],
21
+ description: "Member ID type (default: open_id)"
22
+ }))
23
+ });
24
+ //#endregion
25
+ export { FeishuChatSchema };
@@ -0,0 +1,87 @@
1
+ import { createFeishuToolClient, resolveRegisteredFeishuToolsConfig } from "./tool-account.js";
2
+ import { FeishuChatSchema } from "./chat-schema.js";
3
+ //#region src/chat.ts
4
+ function json(data) {
5
+ return {
6
+ content: [{
7
+ type: "text",
8
+ text: JSON.stringify(data, null, 2)
9
+ }],
10
+ details: data
11
+ };
12
+ }
13
+ async function getChatInfo(client, chatId) {
14
+ const res = await client.im.chat.get({ path: { chat_id: chatId } });
15
+ if (res.code !== 0) throw new Error(res.msg);
16
+ const chat = res.data;
17
+ return {
18
+ chat_id: chatId,
19
+ name: chat?.name,
20
+ description: chat?.description,
21
+ owner_id: chat?.owner_id,
22
+ tenant_key: chat?.tenant_key,
23
+ user_count: chat?.user_count,
24
+ chat_mode: chat?.chat_mode,
25
+ chat_type: chat?.chat_type,
26
+ join_message_visibility: chat?.join_message_visibility,
27
+ leave_message_visibility: chat?.leave_message_visibility,
28
+ membership_approval: chat?.membership_approval,
29
+ moderation_permission: chat?.moderation_permission,
30
+ avatar: chat?.avatar
31
+ };
32
+ }
33
+ async function getChatMembers(client, chatId, pageSize, pageToken, memberIdType) {
34
+ const page_size = pageSize ? Math.max(1, Math.min(100, pageSize)) : 50;
35
+ const res = await client.im.chatMembers.get({
36
+ path: { chat_id: chatId },
37
+ params: {
38
+ page_size,
39
+ page_token: pageToken,
40
+ member_id_type: memberIdType ?? "open_id"
41
+ }
42
+ });
43
+ if (res.code !== 0) throw new Error(res.msg);
44
+ return {
45
+ chat_id: chatId,
46
+ has_more: res.data?.has_more,
47
+ page_token: res.data?.page_token,
48
+ members: res.data?.items?.map((item) => ({
49
+ member_id: item.member_id,
50
+ name: item.name,
51
+ tenant_key: item.tenant_key,
52
+ member_id_type: item.member_id_type
53
+ })) ?? []
54
+ };
55
+ }
56
+ function registerFeishuChatTools(api) {
57
+ if (!api.config) {
58
+ api.logger.debug?.("feishu_chat: No config available, skipping chat tools");
59
+ return;
60
+ }
61
+ if (!resolveRegisteredFeishuToolsConfig(api.config).chat) {
62
+ api.logger.debug?.("feishu_chat: chat tool disabled in config");
63
+ return;
64
+ }
65
+ api.registerTool({
66
+ name: "feishu_chat",
67
+ label: "Feishu Chat",
68
+ description: "Feishu chat operations. Actions: members, info",
69
+ parameters: FeishuChatSchema,
70
+ async execute(_toolCallId, params) {
71
+ const p = params;
72
+ try {
73
+ const client = createFeishuToolClient({ api });
74
+ switch (p.action) {
75
+ case "members": return json(await getChatMembers(client, p.chat_id, p.page_size, p.page_token, p.member_id_type));
76
+ case "info": return json(await getChatInfo(client, p.chat_id));
77
+ default: return json({ error: `Unknown action: ${String(p.action)}` });
78
+ }
79
+ } catch (err) {
80
+ return json({ error: err instanceof Error ? err.message : String(err) });
81
+ }
82
+ }
83
+ }, { name: "feishu_chat" });
84
+ api.logger.info?.("feishu_chat: Registered feishu_chat tool");
85
+ }
86
+ //#endregion
87
+ export { registerFeishuChatTools };
@@ -0,0 +1,16 @@
1
+ import { FeishuConfig, FeishuDomain } from "./types.js";
2
+ //#region src/client.d.ts
3
+ /**
4
+ * Credentials needed to create a Feishu client.
5
+ * Both FeishuConfig and ResolvedFeishuAccount satisfy this interface.
6
+ */
7
+ type FeishuClientCredentials = {
8
+ accountId?: string;
9
+ appId?: string;
10
+ appSecret?: string;
11
+ domain?: FeishuDomain;
12
+ httpTimeoutMs?: number;
13
+ config?: Pick<FeishuConfig, "httpTimeoutMs">;
14
+ };
15
+ //#endregion
16
+ export { FeishuClientCredentials };
@@ -0,0 +1,112 @@
1
+ import * as Lark from "@larksuiteoapi/node-sdk";
2
+ import { HttpsProxyAgent } from "https-proxy-agent";
3
+ //#region src/client.ts
4
+ /** Default HTTP timeout for Feishu API requests (30 seconds). */
5
+ const FEISHU_HTTP_TIMEOUT_MS = 3e4;
6
+ const FEISHU_HTTP_TIMEOUT_MAX_MS = 3e5;
7
+ const FEISHU_HTTP_TIMEOUT_ENV_VAR = "OPENCLAW_FEISHU_HTTP_TIMEOUT_MS";
8
+ function getWsProxyAgent() {
9
+ const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY;
10
+ if (!proxyUrl) return void 0;
11
+ return new HttpsProxyAgent(proxyUrl);
12
+ }
13
+ const clientCache = /* @__PURE__ */ new Map();
14
+ function resolveDomain(domain) {
15
+ if (domain === "lark") return Lark.Domain.Lark;
16
+ if (domain === "feishu" || !domain) return Lark.Domain.Feishu;
17
+ return domain.replace(/\/+$/, "");
18
+ }
19
+ /**
20
+ * Create an HTTP instance that delegates to the Lark SDK's default instance
21
+ * but injects a default request timeout to prevent indefinite hangs
22
+ * (e.g. when the Feishu API is slow, causing per-chat queue deadlocks).
23
+ */
24
+ function createTimeoutHttpInstance(defaultTimeoutMs) {
25
+ const base = Lark.defaultHttpInstance;
26
+ function injectTimeout(opts) {
27
+ return {
28
+ timeout: defaultTimeoutMs,
29
+ ...opts
30
+ };
31
+ }
32
+ return {
33
+ request: (opts) => base.request(injectTimeout(opts)),
34
+ get: (url, opts) => base.get(url, injectTimeout(opts)),
35
+ post: (url, data, opts) => base.post(url, data, injectTimeout(opts)),
36
+ put: (url, data, opts) => base.put(url, data, injectTimeout(opts)),
37
+ patch: (url, data, opts) => base.patch(url, data, injectTimeout(opts)),
38
+ delete: (url, opts) => base.delete(url, injectTimeout(opts)),
39
+ head: (url, opts) => base.head(url, injectTimeout(opts)),
40
+ options: (url, opts) => base.options(url, injectTimeout(opts))
41
+ };
42
+ }
43
+ function resolveConfiguredHttpTimeoutMs(creds) {
44
+ const clampTimeout = (value) => {
45
+ return Math.min(Math.max(Math.floor(value), 1), FEISHU_HTTP_TIMEOUT_MAX_MS);
46
+ };
47
+ const fromDirectField = creds.httpTimeoutMs;
48
+ if (typeof fromDirectField === "number" && Number.isFinite(fromDirectField) && fromDirectField > 0) return clampTimeout(fromDirectField);
49
+ const envRaw = process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR];
50
+ if (envRaw) {
51
+ const envValue = Number(envRaw);
52
+ if (Number.isFinite(envValue) && envValue > 0) return clampTimeout(envValue);
53
+ }
54
+ const timeout = creds.config?.httpTimeoutMs;
55
+ if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) return FEISHU_HTTP_TIMEOUT_MS;
56
+ return clampTimeout(timeout);
57
+ }
58
+ /**
59
+ * Create or get a cached Feishu client for an account.
60
+ * Accepts any object with appId, appSecret, and optional domain/accountId.
61
+ */
62
+ function createFeishuClient(creds) {
63
+ const { accountId = "default", appId, appSecret, domain } = creds;
64
+ const defaultHttpTimeoutMs = resolveConfiguredHttpTimeoutMs(creds);
65
+ if (!appId || !appSecret) throw new Error(`Feishu credentials not configured for account "${accountId}"`);
66
+ const cached = clientCache.get(accountId);
67
+ if (cached && cached.config.appId === appId && cached.config.appSecret === appSecret && cached.config.domain === domain && cached.config.httpTimeoutMs === defaultHttpTimeoutMs) return cached.client;
68
+ const client = new Lark.Client({
69
+ appId,
70
+ appSecret,
71
+ appType: Lark.AppType.SelfBuild,
72
+ domain: resolveDomain(domain),
73
+ httpInstance: createTimeoutHttpInstance(defaultHttpTimeoutMs)
74
+ });
75
+ clientCache.set(accountId, {
76
+ client,
77
+ config: {
78
+ appId,
79
+ appSecret,
80
+ domain,
81
+ httpTimeoutMs: defaultHttpTimeoutMs
82
+ }
83
+ });
84
+ return client;
85
+ }
86
+ /**
87
+ * Create a Feishu WebSocket client for an account.
88
+ * Note: WSClient is not cached since each call creates a new connection.
89
+ */
90
+ function createFeishuWSClient(account) {
91
+ const { accountId, appId, appSecret, domain } = account;
92
+ if (!appId || !appSecret) throw new Error(`Feishu credentials not configured for account "${accountId}"`);
93
+ const agent = getWsProxyAgent();
94
+ return new Lark.WSClient({
95
+ appId,
96
+ appSecret,
97
+ domain: resolveDomain(domain),
98
+ loggerLevel: Lark.LoggerLevel.info,
99
+ ...agent ? { agent } : {}
100
+ });
101
+ }
102
+ /**
103
+ * Create an event dispatcher for an account.
104
+ */
105
+ function createEventDispatcher(account) {
106
+ return new Lark.EventDispatcher({
107
+ encryptKey: account.encryptKey,
108
+ verificationToken: account.verificationToken
109
+ });
110
+ }
111
+ //#endregion
112
+ export { createEventDispatcher, createFeishuClient, createFeishuWSClient };