@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-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.
- package/dist/index.d.ts +23 -0
- package/dist/index.js +45 -0
- package/dist/src/accounts.js +141 -0
- package/dist/src/app-scope-checker.js +36 -0
- package/dist/src/async.js +34 -0
- package/dist/src/auth-errors.js +72 -0
- package/dist/src/bitable.js +495 -0
- package/dist/src/bot.d.ts +35 -0
- package/dist/src/bot.js +941 -0
- package/dist/src/calendar-calendar.js +54 -0
- package/dist/src/calendar-event-attendee.js +98 -0
- package/dist/src/calendar-event.js +193 -0
- package/dist/src/calendar-freebusy.js +40 -0
- package/dist/src/calendar-shared.js +23 -0
- package/dist/src/calendar.js +16 -0
- package/dist/src/card-action.js +49 -0
- package/dist/src/channel.d.ts +7 -0
- package/dist/src/channel.js +413 -0
- package/dist/src/chat-schema.js +25 -0
- package/dist/src/chat.js +87 -0
- package/dist/src/client.d.ts +16 -0
- package/dist/src/client.js +112 -0
- package/dist/src/config-schema.d.ts +357 -0
- package/dist/src/dedup.js +126 -0
- package/dist/src/device-flow.js +109 -0
- package/dist/src/directory.js +101 -0
- package/dist/src/doc-schema.js +148 -0
- package/dist/src/docx-batch-insert.js +104 -0
- package/dist/src/docx-color-text.js +80 -0
- package/dist/src/docx-table-ops.js +197 -0
- package/dist/src/docx.js +858 -0
- package/dist/src/domains.js +14 -0
- package/dist/src/drive-schema.js +41 -0
- package/dist/src/drive.js +126 -0
- package/dist/src/dynamic-agent.js +93 -0
- package/dist/src/external-keys.js +13 -0
- package/dist/src/feishu-fetch.js +12 -0
- package/dist/src/identity.js +92 -0
- package/dist/src/lark-ticket.js +11 -0
- package/dist/src/media.d.ts +75 -0
- package/dist/src/media.js +304 -0
- package/dist/src/mention.d.ts +52 -0
- package/dist/src/mention.js +82 -0
- package/dist/src/monitor.account.d.ts +1 -0
- package/dist/src/monitor.account.js +393 -0
- package/dist/src/monitor.d.ts +11 -0
- package/dist/src/monitor.js +58 -0
- package/dist/src/monitor.startup.js +24 -0
- package/dist/src/monitor.state.d.ts +1 -0
- package/dist/src/monitor.state.js +80 -0
- package/dist/src/monitor.transport.js +167 -0
- package/dist/src/nextclaw-sdk/account-id.js +15 -0
- package/dist/src/nextclaw-sdk/core-channel.js +150 -0
- package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
- package/dist/src/nextclaw-sdk/dedupe.js +164 -0
- package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
- package/dist/src/nextclaw-sdk/feishu.js +14 -0
- package/dist/src/nextclaw-sdk/history.js +69 -0
- package/dist/src/nextclaw-sdk/network-body.js +180 -0
- package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
- package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
- package/dist/src/nextclaw-sdk/network.js +4 -0
- package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
- package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
- package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
- package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
- package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
- package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
- package/dist/src/nextclaw-sdk/secrets.js +4 -0
- package/dist/src/nextclaw-sdk/types.d.ts +242 -0
- package/dist/src/oauth.js +171 -0
- package/dist/src/onboarding.js +381 -0
- package/dist/src/outbound.js +150 -0
- package/dist/src/perm-schema.js +49 -0
- package/dist/src/perm.js +90 -0
- package/dist/src/policy.js +61 -0
- package/dist/src/post.js +160 -0
- package/dist/src/probe.d.ts +11 -0
- package/dist/src/probe.js +85 -0
- package/dist/src/raw-request.js +24 -0
- package/dist/src/reactions.d.ts +67 -0
- package/dist/src/reactions.js +91 -0
- package/dist/src/reply-dispatcher.js +250 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/secret-input.js +3 -0
- package/dist/src/send-result.js +12 -0
- package/dist/src/send-target.js +22 -0
- package/dist/src/send.d.ts +51 -0
- package/dist/src/send.js +265 -0
- package/dist/src/sheets-shared.js +193 -0
- package/dist/src/sheets.js +95 -0
- package/dist/src/streaming-card.js +263 -0
- package/dist/src/targets.js +39 -0
- package/dist/src/task-comment.js +76 -0
- package/dist/src/task-shared.js +13 -0
- package/dist/src/task-subtask.js +79 -0
- package/dist/src/task-task.js +144 -0
- package/dist/src/task-tasklist.js +136 -0
- package/dist/src/task.js +16 -0
- package/dist/src/token-store.js +154 -0
- package/dist/src/tool-account.js +65 -0
- package/dist/src/tool-result.js +18 -0
- package/dist/src/tool-scopes.js +62 -0
- package/dist/src/tools-config.js +30 -0
- package/dist/src/types.d.ts +43 -0
- package/dist/src/typing.js +145 -0
- package/dist/src/uat-client.js +102 -0
- package/dist/src/user-tool-client.js +132 -0
- package/dist/src/user-tool-helpers.js +110 -0
- package/dist/src/user-tool-result.js +10 -0
- package/dist/src/wiki-schema.js +45 -0
- package/dist/src/wiki.js +144 -0
- package/package.json +8 -4
- 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 };
|