@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,393 @@
1
+ import { resolveFeishuAccount } from "./accounts.js";
2
+ import { createEventDispatcher } from "./client.js";
3
+ import { withTicket } from "./lark-ticket.js";
4
+ import { raceWithTimeoutAndAbort } from "./async.js";
5
+ import { getFeishuRuntime } from "./runtime.js";
6
+ import { isMentionForwardRequest } from "./mention.js";
7
+ import { getMessageFeishu } from "./send.js";
8
+ import { hasProcessedFeishuMessage, recordProcessedFeishuMessage, releaseFeishuMessageProcessing, tryBeginFeishuMessageProcessing, warmupDedupFromDisk } from "./dedup.js";
9
+ import { handleFeishuMessage, parseFeishuMessageEvent } from "./bot.js";
10
+ import { handleFeishuCardAction } from "./card-action.js";
11
+ import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
12
+ import { botNames, botOpenIds } from "./monitor.state.js";
13
+ import { monitorWebSocket, monitorWebhook } from "./monitor.transport.js";
14
+ import * as crypto from "crypto";
15
+ //#region src/monitor.account.ts
16
+ const FEISHU_REACTION_VERIFY_TIMEOUT_MS = 1500;
17
+ async function resolveReactionSyntheticEvent(params) {
18
+ const { cfg, accountId, event, botOpenId, fetchMessage = getMessageFeishu, verificationTimeoutMs = FEISHU_REACTION_VERIFY_TIMEOUT_MS, logger, uuid = () => crypto.randomUUID() } = params;
19
+ const emoji = event.reaction_type?.emoji_type;
20
+ const messageId = event.message_id;
21
+ const senderId = event.user_id?.open_id;
22
+ if (!emoji || !messageId || !senderId) return null;
23
+ const reactionNotifications = resolveFeishuAccount({
24
+ cfg,
25
+ accountId
26
+ }).config.reactionNotifications ?? "own";
27
+ if (reactionNotifications === "off") return null;
28
+ if (event.operator_type === "app" || senderId === botOpenId) return null;
29
+ if (emoji === "Typing") return null;
30
+ if (reactionNotifications === "own" && !botOpenId) {
31
+ logger?.(`feishu[${accountId}]: bot open_id unavailable, skipping reaction ${emoji} on ${messageId}`);
32
+ return null;
33
+ }
34
+ const reactedMsg = await raceWithTimeoutAndAbort(fetchMessage({
35
+ cfg,
36
+ messageId,
37
+ accountId
38
+ }), { timeoutMs: verificationTimeoutMs }).then((result) => result.status === "resolved" ? result.value : null).catch(() => null);
39
+ const isBotMessage = reactedMsg?.senderType === "app" || reactedMsg?.senderOpenId === botOpenId;
40
+ if (!reactedMsg || reactionNotifications === "own" && !isBotMessage) {
41
+ logger?.(`feishu[${accountId}]: ignoring reaction on non-bot/unverified message ${messageId} (sender: ${reactedMsg?.senderOpenId ?? "unknown"})`);
42
+ return null;
43
+ }
44
+ const fallbackChatType = reactedMsg.chatType;
45
+ const resolvedChatType = normalizeFeishuChatType(event.chat_type) ?? fallbackChatType;
46
+ if (!resolvedChatType) {
47
+ logger?.(`feishu[${accountId}]: skipping reaction ${emoji} on ${messageId} without chat type context`);
48
+ return null;
49
+ }
50
+ const syntheticChatIdRaw = event.chat_id ?? reactedMsg.chatId;
51
+ const syntheticChatId = syntheticChatIdRaw?.trim() ? syntheticChatIdRaw : `p2p:${senderId}`;
52
+ const syntheticChatType = resolvedChatType;
53
+ return {
54
+ sender: {
55
+ sender_id: { open_id: senderId },
56
+ sender_type: "user"
57
+ },
58
+ message: {
59
+ message_id: `${messageId}:reaction:${emoji}:${uuid()}`,
60
+ chat_id: syntheticChatId,
61
+ chat_type: syntheticChatType,
62
+ message_type: "text",
63
+ content: JSON.stringify({ text: `[reacted with ${emoji} to message ${messageId}]` })
64
+ }
65
+ };
66
+ }
67
+ function normalizeFeishuChatType(value) {
68
+ return value === "group" || value === "private" || value === "p2p" ? value : void 0;
69
+ }
70
+ /**
71
+ * Per-chat serial queue that ensures messages from the same chat are processed
72
+ * in arrival order while allowing different chats to run concurrently.
73
+ */
74
+ function createChatQueue() {
75
+ const queues = /* @__PURE__ */ new Map();
76
+ return (chatId, task) => {
77
+ const next = (queues.get(chatId) ?? Promise.resolve()).then(task, task);
78
+ queues.set(chatId, next);
79
+ next.finally(() => {
80
+ if (queues.get(chatId) === next) queues.delete(chatId);
81
+ });
82
+ return next;
83
+ };
84
+ }
85
+ function mergeFeishuDebounceMentions(entries) {
86
+ const merged = /* @__PURE__ */ new Map();
87
+ for (const entry of entries) for (const mention of entry.message.mentions ?? []) {
88
+ const stableId = mention.id.open_id?.trim() || mention.id.user_id?.trim() || mention.id.union_id?.trim();
89
+ const mentionName = mention.name?.trim();
90
+ const mentionKey = mention.key?.trim();
91
+ const fallback = mentionName && mentionKey ? `${mentionName}|${mentionKey}` : mentionName || mentionKey;
92
+ const key = stableId || fallback;
93
+ if (!key || merged.has(key)) continue;
94
+ merged.set(key, mention);
95
+ }
96
+ if (merged.size === 0) return;
97
+ return Array.from(merged.values());
98
+ }
99
+ function dedupeFeishuDebounceEntriesByMessageId(entries) {
100
+ const seen = /* @__PURE__ */ new Set();
101
+ const deduped = [];
102
+ for (const entry of entries) {
103
+ const messageId = entry.message.message_id?.trim();
104
+ if (!messageId) {
105
+ deduped.push(entry);
106
+ continue;
107
+ }
108
+ if (seen.has(messageId)) continue;
109
+ seen.add(messageId);
110
+ deduped.push(entry);
111
+ }
112
+ return deduped;
113
+ }
114
+ function resolveFeishuDebounceMentions(params) {
115
+ const { entries, botOpenId } = params;
116
+ if (entries.length === 0) return;
117
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
118
+ const entry = entries[index];
119
+ if (isMentionForwardRequest(entry, botOpenId)) return mergeFeishuDebounceMentions([entry]);
120
+ }
121
+ const merged = mergeFeishuDebounceMentions(entries);
122
+ if (!merged) return;
123
+ const normalizedBotOpenId = botOpenId?.trim();
124
+ if (!normalizedBotOpenId) return;
125
+ const botMentions = merged.filter((mention) => mention.id.open_id?.trim() === normalizedBotOpenId);
126
+ return botMentions.length > 0 ? botMentions : void 0;
127
+ }
128
+ function registerEventHandlers(eventDispatcher, context) {
129
+ const { cfg, accountId, runtime, chatHistories, fireAndForget } = context;
130
+ const core = getFeishuRuntime();
131
+ const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
132
+ cfg,
133
+ channel: "feishu"
134
+ });
135
+ const log = runtime?.log ?? console.log;
136
+ const error = runtime?.error ?? console.error;
137
+ const enqueue = createChatQueue();
138
+ const dispatchFeishuMessage = async (event) => {
139
+ const chatId = event.message.chat_id?.trim() || "unknown";
140
+ const task = () => withTicket({
141
+ accountId,
142
+ messageId: event.message.message_id,
143
+ chatId: event.message.chat_id,
144
+ senderOpenId: event.sender.sender_id.open_id?.trim() || void 0,
145
+ chatType: event.message.chat_type,
146
+ threadId: event.message.thread_id?.trim() || event.message.root_id?.trim() || void 0,
147
+ startTime: Date.now()
148
+ }, () => handleFeishuMessage({
149
+ cfg,
150
+ event,
151
+ botOpenId: botOpenIds.get(accountId),
152
+ botName: botNames.get(accountId),
153
+ runtime,
154
+ chatHistories,
155
+ accountId,
156
+ processingClaimHeld: true
157
+ }));
158
+ await enqueue(chatId, task);
159
+ };
160
+ const resolveSenderDebounceId = (event) => {
161
+ return event.sender.sender_id.open_id?.trim() || event.sender.sender_id.user_id?.trim() || void 0;
162
+ };
163
+ const resolveDebounceText = (event) => {
164
+ return parseFeishuMessageEvent(event, botOpenIds.get(accountId), botNames.get(accountId)).content.trim();
165
+ };
166
+ const recordSuppressedMessageIds = async (entries, dispatchMessageId) => {
167
+ const keepMessageId = dispatchMessageId?.trim();
168
+ const suppressedIds = new Set(entries.map((entry) => entry.message.message_id?.trim()).filter((id) => Boolean(id) && (!keepMessageId || id !== keepMessageId)));
169
+ if (suppressedIds.size === 0) return;
170
+ for (const messageId of suppressedIds) try {
171
+ await recordProcessedFeishuMessage(messageId, accountId, log);
172
+ } catch (err) {
173
+ error(`feishu[${accountId}]: failed to record merged dedupe id ${messageId}: ${String(err)}`);
174
+ }
175
+ };
176
+ const isMessageAlreadyProcessed = async (entry) => {
177
+ return await hasProcessedFeishuMessage(entry.message.message_id, accountId, log);
178
+ };
179
+ const inboundDebouncer = core.channel.debounce.createInboundDebouncer({
180
+ debounceMs: inboundDebounceMs,
181
+ buildKey: (event) => {
182
+ const chatId = event.message.chat_id?.trim();
183
+ const senderId = resolveSenderDebounceId(event);
184
+ if (!chatId || !senderId) return null;
185
+ const rootId = event.message.root_id?.trim();
186
+ return `feishu:${accountId}:${chatId}:${rootId ? `thread:${rootId}` : "chat"}:${senderId}`;
187
+ },
188
+ shouldDebounce: (event) => {
189
+ if (event.message.message_type !== "text") return false;
190
+ const text = resolveDebounceText(event);
191
+ if (!text) return false;
192
+ return !core.channel.text.hasControlCommand(text, cfg);
193
+ },
194
+ onFlush: async (entries) => {
195
+ const last = entries.at(-1);
196
+ if (!last) return;
197
+ if (entries.length === 1) {
198
+ await dispatchFeishuMessage(last);
199
+ return;
200
+ }
201
+ const dedupedEntries = dedupeFeishuDebounceEntriesByMessageId(entries);
202
+ const freshEntries = [];
203
+ for (const entry of dedupedEntries) if (!await isMessageAlreadyProcessed(entry)) freshEntries.push(entry);
204
+ const dispatchEntry = freshEntries.at(-1);
205
+ if (!dispatchEntry) return;
206
+ await recordSuppressedMessageIds(dedupedEntries, dispatchEntry.message.message_id);
207
+ const combinedText = freshEntries.map((entry) => resolveDebounceText(entry)).filter(Boolean).join("\n");
208
+ const mergedMentions = resolveFeishuDebounceMentions({
209
+ entries: freshEntries,
210
+ botOpenId: botOpenIds.get(accountId)
211
+ });
212
+ if (!combinedText.trim()) {
213
+ await dispatchFeishuMessage({
214
+ ...dispatchEntry,
215
+ message: {
216
+ ...dispatchEntry.message,
217
+ mentions: mergedMentions ?? dispatchEntry.message.mentions
218
+ }
219
+ });
220
+ return;
221
+ }
222
+ await dispatchFeishuMessage({
223
+ ...dispatchEntry,
224
+ message: {
225
+ ...dispatchEntry.message,
226
+ message_type: "text",
227
+ content: JSON.stringify({ text: combinedText }),
228
+ mentions: mergedMentions ?? dispatchEntry.message.mentions
229
+ }
230
+ });
231
+ },
232
+ onError: (err, entries) => {
233
+ for (const entry of entries) releaseFeishuMessageProcessing(entry.message.message_id, accountId);
234
+ error(`feishu[${accountId}]: inbound debounce flush failed: ${String(err)}`);
235
+ }
236
+ });
237
+ eventDispatcher.register({
238
+ "im.message.receive_v1": async (data) => {
239
+ const event = data;
240
+ const messageId = event.message?.message_id?.trim();
241
+ if (!tryBeginFeishuMessageProcessing(messageId, accountId)) {
242
+ log(`feishu[${accountId}]: dropping duplicate event for message ${messageId}`);
243
+ return;
244
+ }
245
+ const processMessage = async () => {
246
+ await inboundDebouncer.enqueue(event);
247
+ };
248
+ if (fireAndForget) {
249
+ processMessage().catch((err) => {
250
+ releaseFeishuMessageProcessing(messageId, accountId);
251
+ error(`feishu[${accountId}]: error handling message: ${String(err)}`);
252
+ });
253
+ return;
254
+ }
255
+ try {
256
+ await processMessage();
257
+ } catch (err) {
258
+ releaseFeishuMessageProcessing(messageId, accountId);
259
+ error(`feishu[${accountId}]: error handling message: ${String(err)}`);
260
+ }
261
+ },
262
+ "im.message.message_read_v1": async () => {},
263
+ "im.chat.member.bot.added_v1": async (data) => {
264
+ try {
265
+ log(`feishu[${accountId}]: bot added to chat ${data.chat_id}`);
266
+ } catch (err) {
267
+ error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
268
+ }
269
+ },
270
+ "im.chat.member.bot.deleted_v1": async (data) => {
271
+ try {
272
+ log(`feishu[${accountId}]: bot removed from chat ${data.chat_id}`);
273
+ } catch (err) {
274
+ error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
275
+ }
276
+ },
277
+ "im.message.reaction.created_v1": async (data) => {
278
+ const processReaction = async () => {
279
+ const event = data;
280
+ const myBotId = botOpenIds.get(accountId);
281
+ const syntheticEvent = await resolveReactionSyntheticEvent({
282
+ cfg,
283
+ accountId,
284
+ event,
285
+ botOpenId: myBotId,
286
+ logger: log
287
+ });
288
+ if (!syntheticEvent) return;
289
+ const promise = withTicket({
290
+ accountId,
291
+ messageId: syntheticEvent.message.message_id,
292
+ chatId: syntheticEvent.message.chat_id,
293
+ senderOpenId: syntheticEvent.sender.sender_id.open_id?.trim() || void 0,
294
+ chatType: syntheticEvent.message.chat_type,
295
+ threadId: syntheticEvent.message.thread_id?.trim() || syntheticEvent.message.root_id?.trim() || void 0,
296
+ startTime: Date.now()
297
+ }, () => handleFeishuMessage({
298
+ cfg,
299
+ event: syntheticEvent,
300
+ botOpenId: myBotId,
301
+ botName: botNames.get(accountId),
302
+ runtime,
303
+ chatHistories,
304
+ accountId
305
+ }));
306
+ if (fireAndForget) {
307
+ promise.catch((err) => {
308
+ error(`feishu[${accountId}]: error handling reaction: ${String(err)}`);
309
+ });
310
+ return;
311
+ }
312
+ await promise;
313
+ };
314
+ if (fireAndForget) {
315
+ processReaction().catch((err) => {
316
+ error(`feishu[${accountId}]: error handling reaction event: ${String(err)}`);
317
+ });
318
+ return;
319
+ }
320
+ try {
321
+ await processReaction();
322
+ } catch (err) {
323
+ error(`feishu[${accountId}]: error handling reaction event: ${String(err)}`);
324
+ }
325
+ },
326
+ "im.message.reaction.deleted_v1": async () => {},
327
+ "card.action.trigger": async (data) => {
328
+ try {
329
+ const promise = handleFeishuCardAction({
330
+ cfg,
331
+ event: data,
332
+ botOpenId: botOpenIds.get(accountId),
333
+ runtime,
334
+ accountId
335
+ });
336
+ if (fireAndForget) promise.catch((err) => {
337
+ error(`feishu[${accountId}]: error handling card action: ${String(err)}`);
338
+ });
339
+ else await promise;
340
+ } catch (err) {
341
+ error(`feishu[${accountId}]: error handling card action: ${String(err)}`);
342
+ }
343
+ }
344
+ });
345
+ }
346
+ async function monitorSingleAccount(params) {
347
+ const { cfg, account, runtime, abortSignal } = params;
348
+ const { accountId } = account;
349
+ const log = runtime?.log ?? console.log;
350
+ const botOpenIdSource = params.botOpenIdSource ?? { kind: "fetch" };
351
+ const botIdentity = botOpenIdSource.kind === "prefetched" ? {
352
+ botOpenId: botOpenIdSource.botOpenId,
353
+ botName: botOpenIdSource.botName
354
+ } : await fetchBotIdentityForMonitor(account, {
355
+ runtime,
356
+ abortSignal
357
+ });
358
+ const botOpenId = botIdentity.botOpenId;
359
+ const botName = botIdentity.botName?.trim();
360
+ botOpenIds.set(accountId, botOpenId ?? "");
361
+ if (botName) botNames.set(accountId, botName);
362
+ else botNames.delete(accountId);
363
+ log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
364
+ const connectionMode = account.config.connectionMode ?? "websocket";
365
+ if (connectionMode === "webhook" && !account.verificationToken?.trim()) throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`);
366
+ if (connectionMode === "webhook" && !account.encryptKey?.trim()) throw new Error(`Feishu account "${accountId}" webhook mode requires encryptKey`);
367
+ const warmupCount = await warmupDedupFromDisk(accountId, log);
368
+ if (warmupCount > 0) log(`feishu[${accountId}]: dedup warmup loaded ${warmupCount} entries from disk`);
369
+ const eventDispatcher = createEventDispatcher(account);
370
+ registerEventHandlers(eventDispatcher, {
371
+ cfg,
372
+ accountId,
373
+ runtime,
374
+ chatHistories: /* @__PURE__ */ new Map(),
375
+ fireAndForget: true
376
+ });
377
+ if (connectionMode === "webhook") return monitorWebhook({
378
+ account,
379
+ accountId,
380
+ runtime,
381
+ abortSignal,
382
+ eventDispatcher
383
+ });
384
+ return monitorWebSocket({
385
+ account,
386
+ accountId,
387
+ runtime,
388
+ abortSignal,
389
+ eventDispatcher
390
+ });
391
+ }
392
+ //#endregion
393
+ export { monitorSingleAccount, resolveReactionSyntheticEvent };
@@ -0,0 +1,11 @@
1
+ import { ClawdbotConfig, RuntimeEnv } from "./nextclaw-sdk/types.js";
2
+ //#region src/monitor.d.ts
3
+ type MonitorFeishuOpts = {
4
+ config?: ClawdbotConfig;
5
+ runtime?: RuntimeEnv;
6
+ abortSignal?: AbortSignal;
7
+ accountId?: string;
8
+ };
9
+ declare function monitorFeishuProvider(opts?: MonitorFeishuOpts): Promise<void>;
10
+ //#endregion
11
+ export { monitorFeishuProvider };
@@ -0,0 +1,58 @@
1
+ import { listEnabledFeishuAccounts, resolveFeishuAccount } from "./accounts.js";
2
+ import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
3
+ import { stopFeishuMonitorState } from "./monitor.state.js";
4
+ import { monitorSingleAccount } from "./monitor.account.js";
5
+ //#region src/monitor.ts
6
+ async function monitorFeishuProvider(opts = {}) {
7
+ const cfg = opts.config;
8
+ if (!cfg) throw new Error("Config is required for Feishu monitor");
9
+ const log = opts.runtime?.log ?? console.log;
10
+ if (opts.accountId) {
11
+ const account = resolveFeishuAccount({
12
+ cfg,
13
+ accountId: opts.accountId
14
+ });
15
+ if (!account.enabled || !account.configured) throw new Error(`Feishu account "${opts.accountId}" not configured or disabled`);
16
+ return monitorSingleAccount({
17
+ cfg,
18
+ account,
19
+ runtime: opts.runtime,
20
+ abortSignal: opts.abortSignal
21
+ });
22
+ }
23
+ const accounts = listEnabledFeishuAccounts(cfg);
24
+ if (accounts.length === 0) throw new Error("No enabled Feishu accounts configured");
25
+ log(`feishu: starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`);
26
+ const monitorPromises = [];
27
+ for (const account of accounts) {
28
+ if (opts.abortSignal?.aborted) {
29
+ log("feishu: abort signal received during startup preflight; stopping startup");
30
+ break;
31
+ }
32
+ const { botOpenId, botName } = await fetchBotIdentityForMonitor(account, {
33
+ runtime: opts.runtime,
34
+ abortSignal: opts.abortSignal
35
+ });
36
+ if (opts.abortSignal?.aborted) {
37
+ log("feishu: abort signal received during startup preflight; stopping startup");
38
+ break;
39
+ }
40
+ monitorPromises.push(monitorSingleAccount({
41
+ cfg,
42
+ account,
43
+ runtime: opts.runtime,
44
+ abortSignal: opts.abortSignal,
45
+ botOpenIdSource: {
46
+ kind: "prefetched",
47
+ botOpenId,
48
+ botName
49
+ }
50
+ }));
51
+ }
52
+ await Promise.all(monitorPromises);
53
+ }
54
+ function stopFeishuMonitor(accountId) {
55
+ stopFeishuMonitorState(accountId);
56
+ }
57
+ //#endregion
58
+ export { monitorFeishuProvider, stopFeishuMonitor };
@@ -0,0 +1,24 @@
1
+ import { probeFeishu } from "./probe.js";
2
+ function isTimeoutErrorMessage(message) {
3
+ return message?.toLowerCase().includes("timeout") || message?.toLowerCase().includes("timed out") ? true : false;
4
+ }
5
+ function isAbortErrorMessage(message) {
6
+ return message?.toLowerCase().includes("aborted") ?? false;
7
+ }
8
+ async function fetchBotIdentityForMonitor(account, options = {}) {
9
+ if (options.abortSignal?.aborted) return {};
10
+ const timeoutMs = options.timeoutMs ?? 1e4;
11
+ const result = await probeFeishu(account, {
12
+ timeoutMs,
13
+ abortSignal: options.abortSignal
14
+ });
15
+ if (result.ok) return {
16
+ botOpenId: result.botOpenId,
17
+ botName: result.botName
18
+ };
19
+ if (options.abortSignal?.aborted || isAbortErrorMessage(result.error)) return {};
20
+ if (isTimeoutErrorMessage(result.error)) (options.runtime?.error ?? console.error)(`feishu[${account.accountId}]: bot info probe timed out after ${timeoutMs}ms; continuing startup`);
21
+ return {};
22
+ }
23
+ //#endregion
24
+ export { fetchBotIdentityForMonitor };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,80 @@
1
+ import { WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, createFixedWindowRateLimiter, createWebhookAnomalyTracker } from "./nextclaw-sdk/network-webhook.js";
2
+ import "./nextclaw-sdk/feishu.js";
3
+ //#region src/monitor.state.ts
4
+ const wsClients = /* @__PURE__ */ new Map();
5
+ const httpServers = /* @__PURE__ */ new Map();
6
+ const botOpenIds = /* @__PURE__ */ new Map();
7
+ const botNames = /* @__PURE__ */ new Map();
8
+ const FEISHU_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
9
+ const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 3e4;
10
+ const FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS = {
11
+ windowMs: 6e4,
12
+ maxRequests: 120,
13
+ maxTrackedKeys: 4096
14
+ };
15
+ const FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS = {
16
+ maxTrackedKeys: 4096,
17
+ ttlMs: 360 * 6e4,
18
+ logEvery: 25
19
+ };
20
+ function coercePositiveInt(value, fallback) {
21
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
22
+ const normalized = Math.floor(value);
23
+ return normalized > 0 ? normalized : fallback;
24
+ }
25
+ function resolveFeishuWebhookRateLimitDefaultsForTest(defaults) {
26
+ const resolved = defaults;
27
+ return {
28
+ windowMs: coercePositiveInt(resolved?.windowMs, FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.windowMs),
29
+ maxRequests: coercePositiveInt(resolved?.maxRequests, FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.maxRequests),
30
+ maxTrackedKeys: coercePositiveInt(resolved?.maxTrackedKeys, FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.maxTrackedKeys)
31
+ };
32
+ }
33
+ function resolveFeishuWebhookAnomalyDefaultsForTest(defaults) {
34
+ const resolved = defaults;
35
+ return {
36
+ maxTrackedKeys: coercePositiveInt(resolved?.maxTrackedKeys, FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.maxTrackedKeys),
37
+ ttlMs: coercePositiveInt(resolved?.ttlMs, FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.ttlMs),
38
+ logEvery: coercePositiveInt(resolved?.logEvery, FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.logEvery)
39
+ };
40
+ }
41
+ const feishuWebhookRateLimitDefaults = resolveFeishuWebhookRateLimitDefaultsForTest(WEBHOOK_RATE_LIMIT_DEFAULTS);
42
+ const feishuWebhookAnomalyDefaults = resolveFeishuWebhookAnomalyDefaultsForTest(WEBHOOK_ANOMALY_COUNTER_DEFAULTS);
43
+ const feishuWebhookRateLimiter = createFixedWindowRateLimiter({
44
+ windowMs: feishuWebhookRateLimitDefaults.windowMs,
45
+ maxRequests: feishuWebhookRateLimitDefaults.maxRequests,
46
+ maxTrackedKeys: feishuWebhookRateLimitDefaults.maxTrackedKeys
47
+ });
48
+ const feishuWebhookAnomalyTracker = createWebhookAnomalyTracker({
49
+ maxTrackedKeys: feishuWebhookAnomalyDefaults.maxTrackedKeys,
50
+ ttlMs: feishuWebhookAnomalyDefaults.ttlMs,
51
+ logEvery: feishuWebhookAnomalyDefaults.logEvery
52
+ });
53
+ function recordWebhookStatus(runtime, accountId, path, statusCode) {
54
+ feishuWebhookAnomalyTracker.record({
55
+ key: `${accountId}:${path}:${statusCode}`,
56
+ statusCode,
57
+ log: runtime?.log ?? console.log,
58
+ message: (count) => `feishu[${accountId}]: webhook anomaly path=${path} status=${statusCode} count=${count}`
59
+ });
60
+ }
61
+ function stopFeishuMonitorState(accountId) {
62
+ if (accountId) {
63
+ wsClients.delete(accountId);
64
+ const server = httpServers.get(accountId);
65
+ if (server) {
66
+ server.close();
67
+ httpServers.delete(accountId);
68
+ }
69
+ botOpenIds.delete(accountId);
70
+ botNames.delete(accountId);
71
+ return;
72
+ }
73
+ wsClients.clear();
74
+ for (const server of httpServers.values()) server.close();
75
+ httpServers.clear();
76
+ botOpenIds.clear();
77
+ botNames.clear();
78
+ }
79
+ //#endregion
80
+ export { FEISHU_WEBHOOK_BODY_TIMEOUT_MS, FEISHU_WEBHOOK_MAX_BODY_BYTES, botNames, botOpenIds, feishuWebhookRateLimiter, httpServers, recordWebhookStatus, stopFeishuMonitorState, wsClients };