@openclaw/feishu 2026.3.12 → 2026.5.1-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.
- package/api.ts +31 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +70 -53
- package/openclaw.plugin.json +1653 -4
- package/package.json +32 -7
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.test.ts +14 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +115 -22
- package/src/accounts.ts +199 -117
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.test.ts +35 -0
- package/src/async.ts +43 -1
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.test.ts +131 -0
- package/src/bitable.ts +59 -22
- package/src/bot-content.ts +474 -0
- package/src/bot-group-name.test.ts +108 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.broadcast.test.ts +463 -0
- package/src/bot.card-action.test.ts +519 -5
- package/src/bot.checkBotMentioned.test.ts +92 -20
- package/src/bot.helpers.test.ts +118 -0
- package/src/bot.stripBotMention.test.ts +13 -21
- package/src/bot.test.ts +1334 -401
- package/src/bot.ts +798 -786
- package/src/card-action.ts +408 -40
- package/src/card-interaction.test.ts +129 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +47 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.test.ts +99 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.test.ts +914 -3
- package/src/channel.ts +1252 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +84 -28
- package/src/chat.ts +68 -10
- package/src/client.test.ts +212 -103
- package/src/client.ts +115 -21
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.test.ts +169 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.test.ts +486 -0
- package/src/comment-handler.ts +309 -0
- package/src/comment-reaction.test.ts +166 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.test.ts +182 -0
- package/src/comment-shared.ts +365 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +77 -25
- package/src/config-schema.ts +31 -4
- package/src/conversation-id.test.ts +18 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +76 -35
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +119 -20
- package/src/directory.ts +61 -91
- package/src/doc-schema.ts +1 -1
- package/src/docx-batch-insert.test.ts +39 -38
- package/src/docx-batch-insert.ts +55 -19
- package/src/docx-color-text.ts +9 -4
- package/src/docx-table-ops.test.ts +53 -0
- package/src/docx-table-ops.ts +52 -34
- package/src/docx-types.ts +38 -0
- package/src/docx.account-selection.test.ts +12 -3
- package/src/docx.test.ts +314 -74
- package/src/docx.ts +278 -122
- package/src/drive-schema.ts +47 -1
- package/src/drive.test.ts +1219 -0
- package/src/drive.ts +614 -13
- package/src/dynamic-agent.ts +10 -4
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +1 -1
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.test.ts +413 -87
- package/src/media.ts +488 -154
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +32 -51
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +7 -0
- package/src/monitor.account.ts +220 -313
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.bot-menu.test.ts +178 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
- package/src/monitor.cleanup.test.ts +376 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.test.ts +937 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +4 -0
- package/src/monitor.message-handler.ts +339 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.reaction.test.ts +194 -92
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +24 -36
- package/src/monitor.startup.ts +26 -16
- package/src/monitor.state.ts +20 -5
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +2 -2
- package/src/monitor.transport.ts +297 -39
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +272 -0
- package/src/monitor.webhook-security.test.ts +125 -91
- package/src/monitor.webhook.test-helpers.ts +116 -0
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +627 -53
- package/src/outbound.ts +623 -81
- package/src/perm-schema.ts +1 -1
- package/src/perm.ts +1 -7
- package/src/pins.ts +108 -0
- package/src/policy.test.ts +297 -117
- package/src/policy.ts +142 -29
- package/src/post.ts +7 -6
- package/src/probe.test.ts +122 -118
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +23 -60
- package/src/reasoning-preview.test.ts +59 -0
- package/src/reasoning-preview.ts +20 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.test.ts +721 -168
- package/src/reply-dispatcher.ts +422 -172
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -13
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.test.ts +61 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +1 -1
- package/src/send-target.test.ts +9 -3
- package/src/send-target.ts +10 -4
- package/src/send.reply-fallback.test.ts +127 -42
- package/src/send.test.ts +386 -4
- package/src/send.ts +486 -164
- package/src/sequential-key.test.ts +72 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.test.ts +92 -0
- package/src/sequential-queue.ts +16 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
- package/src/setup-surface.ts +581 -0
- package/src/streaming-card.test.ts +138 -2
- package/src/streaming-card.ts +134 -18
- package/src/subagent-hooks.test.ts +603 -0
- package/src/subagent-hooks.ts +397 -0
- package/src/targets.ts +3 -13
- package/src/test-support/lifecycle-test-support.ts +479 -0
- package/src/thread-bindings.test.ts +143 -0
- package/src/thread-bindings.ts +330 -0
- package/src/tool-account-routing.test.ts +66 -8
- package/src/tool-account.test.ts +44 -0
- package/src/tool-account.ts +40 -17
- package/src/tool-factory-test-harness.ts +11 -8
- package/src/tool-result.ts +3 -1
- package/src/tools-config.ts +1 -1
- package/src/types.ts +16 -15
- package/src/typing.ts +10 -6
- package/src/wiki-schema.ts +1 -1
- package/src/wiki.ts +1 -7
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
- package/src/feishu-command-handler.ts +0 -59
- package/src/onboarding.status.test.ts +0 -25
- package/src/onboarding.ts +0 -489
- package/src/send-message.ts +0 -71
- package/src/targets.test.ts +0 -70
package/src/monitor.account.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import * as crypto from "crypto";
|
|
2
|
-
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
3
|
-
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "
|
|
4
|
-
import { resolveFeishuAccount } from "./accounts.js";
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
3
|
+
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "../runtime-api.js";
|
|
5
4
|
import { raceWithTimeoutAndAbort } from "./async.js";
|
|
6
5
|
import {
|
|
7
6
|
handleFeishuMessage,
|
|
@@ -11,33 +10,44 @@ import {
|
|
|
11
10
|
} from "./bot.js";
|
|
12
11
|
import { handleFeishuCardAction, type FeishuCardActionEvent } from "./card-action.js";
|
|
13
12
|
import { createEventDispatcher } from "./client.js";
|
|
13
|
+
import { isRecord, readString } from "./comment-shared.js";
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
tryRecordMessage,
|
|
18
|
-
tryRecordMessagePersistent,
|
|
15
|
+
hasProcessedFeishuMessage,
|
|
16
|
+
recordProcessedFeishuMessage,
|
|
19
17
|
warmupDedupFromDisk,
|
|
20
18
|
} from "./dedup.js";
|
|
21
|
-
import {
|
|
19
|
+
import { applyBotIdentityState, startBotIdentityRecovery } from "./monitor.bot-identity.js";
|
|
20
|
+
import { createFeishuBotMenuHandler } from "./monitor.bot-menu-handler.js";
|
|
21
|
+
import { createFeishuDriveCommentNoticeHandler } from "./monitor.comment-notice-handler.js";
|
|
22
|
+
import { createFeishuMessageReceiveHandler } from "./monitor.message-handler.js";
|
|
22
23
|
import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
|
|
23
24
|
import { botNames, botOpenIds } from "./monitor.state.js";
|
|
25
|
+
import { FeishuRetryableSyntheticEventError } from "./monitor.synthetic-error.js";
|
|
24
26
|
import { monitorWebhook, monitorWebSocket } from "./monitor.transport.js";
|
|
25
27
|
import { getFeishuRuntime } from "./runtime.js";
|
|
26
28
|
import { getMessageFeishu } from "./send.js";
|
|
29
|
+
import { getFeishuSequentialKey } from "./sequential-key.js";
|
|
30
|
+
import { createFeishuThreadBindingManager } from "./thread-bindings.js";
|
|
27
31
|
import type { FeishuChatType, ResolvedFeishuAccount } from "./types.js";
|
|
28
32
|
|
|
29
33
|
const FEISHU_REACTION_VERIFY_TIMEOUT_MS = 1_500;
|
|
30
34
|
|
|
35
|
+
export { FeishuRetryableSyntheticEventError };
|
|
36
|
+
|
|
31
37
|
export type FeishuReactionCreatedEvent = {
|
|
32
38
|
message_id: string;
|
|
33
39
|
chat_id?: string;
|
|
34
40
|
chat_type?: string;
|
|
35
41
|
reaction_type?: { emoji_type?: string };
|
|
36
42
|
operator_type?: string;
|
|
37
|
-
user_id?: { open_id?: string };
|
|
43
|
+
user_id?: { open_id?: string; user_id?: string };
|
|
38
44
|
action_time?: string;
|
|
39
45
|
};
|
|
40
46
|
|
|
47
|
+
export type FeishuReactionDeletedEvent = FeishuReactionCreatedEvent & {
|
|
48
|
+
reaction_id?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
41
51
|
type ResolveReactionSyntheticEventParams = {
|
|
42
52
|
cfg: ClawdbotConfig;
|
|
43
53
|
accountId: string;
|
|
@@ -47,6 +57,7 @@ type ResolveReactionSyntheticEventParams = {
|
|
|
47
57
|
verificationTimeoutMs?: number;
|
|
48
58
|
logger?: (message: string) => void;
|
|
49
59
|
uuid?: () => string;
|
|
60
|
+
action?: "created" | "deleted";
|
|
50
61
|
};
|
|
51
62
|
|
|
52
63
|
export async function resolveReactionSyntheticEvent(
|
|
@@ -61,15 +72,18 @@ export async function resolveReactionSyntheticEvent(
|
|
|
61
72
|
verificationTimeoutMs = FEISHU_REACTION_VERIFY_TIMEOUT_MS,
|
|
62
73
|
logger,
|
|
63
74
|
uuid = () => crypto.randomUUID(),
|
|
75
|
+
action = "created",
|
|
64
76
|
} = params;
|
|
65
77
|
|
|
66
78
|
const emoji = event.reaction_type?.emoji_type;
|
|
67
79
|
const messageId = event.message_id;
|
|
68
80
|
const senderId = event.user_id?.open_id;
|
|
81
|
+
const senderUserId = event.user_id?.user_id;
|
|
69
82
|
if (!emoji || !messageId || !senderId) {
|
|
70
83
|
return null;
|
|
71
84
|
}
|
|
72
85
|
|
|
86
|
+
const { resolveFeishuAccount } = await import("./accounts.js");
|
|
73
87
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
74
88
|
const reactionNotifications = account.config.reactionNotifications ?? "own";
|
|
75
89
|
if (reactionNotifications === "off") {
|
|
@@ -120,7 +134,10 @@ export async function resolveReactionSyntheticEvent(
|
|
|
120
134
|
const syntheticChatType: FeishuChatType = resolvedChatType;
|
|
121
135
|
return {
|
|
122
136
|
sender: {
|
|
123
|
-
sender_id: {
|
|
137
|
+
sender_id: {
|
|
138
|
+
open_id: senderId,
|
|
139
|
+
...(senderUserId ? { user_id: senderUserId } : {}),
|
|
140
|
+
},
|
|
124
141
|
sender_type: "user",
|
|
125
142
|
},
|
|
126
143
|
message: {
|
|
@@ -129,14 +146,19 @@ export async function resolveReactionSyntheticEvent(
|
|
|
129
146
|
chat_type: syntheticChatType,
|
|
130
147
|
message_type: "text",
|
|
131
148
|
content: JSON.stringify({
|
|
132
|
-
text:
|
|
149
|
+
text:
|
|
150
|
+
action === "deleted"
|
|
151
|
+
? `[removed reaction ${emoji} from message ${messageId}]`
|
|
152
|
+
: `[reacted with ${emoji} to message ${messageId}]`,
|
|
133
153
|
}),
|
|
134
154
|
},
|
|
135
155
|
};
|
|
136
156
|
}
|
|
137
157
|
|
|
138
158
|
function normalizeFeishuChatType(value: unknown): FeishuChatType | undefined {
|
|
139
|
-
return value === "group" || value === "
|
|
159
|
+
return value === "group" || value === "topic_group" || value === "private" || value === "p2p"
|
|
160
|
+
? value
|
|
161
|
+
: undefined;
|
|
140
162
|
}
|
|
141
163
|
|
|
142
164
|
type RegisterEventHandlersContext = {
|
|
@@ -147,97 +169,73 @@ type RegisterEventHandlersContext = {
|
|
|
147
169
|
fireAndForget?: boolean;
|
|
148
170
|
};
|
|
149
171
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const queues = new Map<string, Promise<void>>();
|
|
156
|
-
return (chatId: string, task: () => Promise<void>): Promise<void> => {
|
|
157
|
-
const prev = queues.get(chatId) ?? Promise.resolve();
|
|
158
|
-
const next = prev.then(task, task);
|
|
159
|
-
queues.set(chatId, next);
|
|
160
|
-
void next.finally(() => {
|
|
161
|
-
if (queues.get(chatId) === next) {
|
|
162
|
-
queues.delete(chatId);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
return next;
|
|
166
|
-
};
|
|
172
|
+
function parseFeishuBotAddedEventPayload(value: unknown): FeishuBotAddedEvent | null {
|
|
173
|
+
if (!isRecord(value) || !readString(value.chat_id) || !isRecord(value.operator_id)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return value as FeishuBotAddedEvent;
|
|
167
177
|
}
|
|
168
178
|
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const merged = new Map<string, NonNullable<FeishuMessageEvent["message"]["mentions"]>[number]>();
|
|
173
|
-
for (const entry of entries) {
|
|
174
|
-
for (const mention of entry.message.mentions ?? []) {
|
|
175
|
-
const stableId =
|
|
176
|
-
mention.id.open_id?.trim() || mention.id.user_id?.trim() || mention.id.union_id?.trim();
|
|
177
|
-
const mentionName = mention.name?.trim();
|
|
178
|
-
const mentionKey = mention.key?.trim();
|
|
179
|
-
const fallback =
|
|
180
|
-
mentionName && mentionKey ? `${mentionName}|${mentionKey}` : mentionName || mentionKey;
|
|
181
|
-
const key = stableId || fallback;
|
|
182
|
-
if (!key || merged.has(key)) {
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
merged.set(key, mention);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (merged.size === 0) {
|
|
189
|
-
return undefined;
|
|
179
|
+
function parseFeishuBotRemovedChatId(value: unknown): string | null {
|
|
180
|
+
if (!isRecord(value)) {
|
|
181
|
+
return null;
|
|
190
182
|
}
|
|
191
|
-
return
|
|
183
|
+
return readString(value.chat_id) ?? null;
|
|
192
184
|
}
|
|
193
185
|
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const messageId = entry.message.message_id?.trim();
|
|
201
|
-
if (!messageId) {
|
|
202
|
-
deduped.push(entry);
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
if (seen.has(messageId)) {
|
|
206
|
-
continue;
|
|
186
|
+
function firstString(...values: unknown[]): string | undefined {
|
|
187
|
+
for (const value of values) {
|
|
188
|
+
const stringValue = readString(value);
|
|
189
|
+
const trimmed = stringValue?.trim();
|
|
190
|
+
if (trimmed) {
|
|
191
|
+
return trimmed;
|
|
207
192
|
}
|
|
208
|
-
seen.add(messageId);
|
|
209
|
-
deduped.push(entry);
|
|
210
193
|
}
|
|
211
|
-
return
|
|
194
|
+
return undefined;
|
|
212
195
|
}
|
|
213
196
|
|
|
214
|
-
function
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}): FeishuMessageEvent["message"]["mentions"] | undefined {
|
|
218
|
-
const { entries, botOpenId } = params;
|
|
219
|
-
if (entries.length === 0) {
|
|
220
|
-
return undefined;
|
|
221
|
-
}
|
|
222
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
223
|
-
const entry = entries[index];
|
|
224
|
-
if (isMentionForwardRequest(entry, botOpenId)) {
|
|
225
|
-
// Keep mention-forward semantics scoped to a single source message.
|
|
226
|
-
return mergeFeishuDebounceMentions([entry]);
|
|
227
|
-
}
|
|
197
|
+
function parseFeishuCardActionEventPayload(value: unknown): FeishuCardActionEvent | null {
|
|
198
|
+
if (!isRecord(value)) {
|
|
199
|
+
return null;
|
|
228
200
|
}
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
201
|
+
const operator = isRecord(value.operator) ? value.operator : {};
|
|
202
|
+
const action = value.action;
|
|
203
|
+
const context = isRecord(value.context) ? value.context : {};
|
|
204
|
+
if (!isRecord(action)) {
|
|
205
|
+
return null;
|
|
232
206
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
207
|
+
const token = readString(value.token);
|
|
208
|
+
const openId = firstString(operator.open_id, value.open_id, context.open_id);
|
|
209
|
+
const userId = firstString(operator.user_id, value.user_id, context.user_id);
|
|
210
|
+
const unionId = firstString(operator.union_id);
|
|
211
|
+
const tag = readString(action.tag);
|
|
212
|
+
const actionValue = action.value;
|
|
213
|
+
const openMessageId = firstString(value.open_message_id, context.open_message_id);
|
|
214
|
+
const contextOpenId = firstString(context.open_id, openId);
|
|
215
|
+
const contextUserId = firstString(context.user_id, userId);
|
|
216
|
+
const chatId = firstString(context.chat_id, context.open_chat_id);
|
|
217
|
+
if (!token || !openId || !tag || !isRecord(actionValue)) {
|
|
218
|
+
return null;
|
|
236
219
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
220
|
+
return {
|
|
221
|
+
operator: {
|
|
222
|
+
open_id: openId,
|
|
223
|
+
...(userId ? { user_id: userId } : {}),
|
|
224
|
+
...(unionId ? { union_id: unionId } : {}),
|
|
225
|
+
},
|
|
226
|
+
token,
|
|
227
|
+
action: {
|
|
228
|
+
value: actionValue,
|
|
229
|
+
tag,
|
|
230
|
+
},
|
|
231
|
+
...(openMessageId ? { open_message_id: openMessageId } : {}),
|
|
232
|
+
context: {
|
|
233
|
+
...(openMessageId ? { open_message_id: openMessageId } : {}),
|
|
234
|
+
...(contextOpenId ? { open_id: contextOpenId } : {}),
|
|
235
|
+
...(contextUserId ? { user_id: contextUserId } : {}),
|
|
236
|
+
...(chatId ? { chat_id: chatId } : {}),
|
|
237
|
+
},
|
|
238
|
+
};
|
|
241
239
|
}
|
|
242
240
|
|
|
243
241
|
function registerEventHandlers(
|
|
@@ -245,174 +243,48 @@ function registerEventHandlers(
|
|
|
245
243
|
context: RegisterEventHandlersContext,
|
|
246
244
|
): void {
|
|
247
245
|
const { cfg, accountId, runtime, chatHistories, fireAndForget } = context;
|
|
248
|
-
const core = getFeishuRuntime();
|
|
249
|
-
const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
|
|
250
|
-
cfg,
|
|
251
|
-
channel: "feishu",
|
|
252
|
-
});
|
|
253
246
|
const log = runtime?.log ?? console.log;
|
|
254
247
|
const error = runtime?.error ?? console.error;
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
handleFeishuMessage({
|
|
260
|
-
cfg,
|
|
261
|
-
event,
|
|
262
|
-
botOpenId: botOpenIds.get(accountId),
|
|
263
|
-
botName: botNames.get(accountId),
|
|
264
|
-
runtime,
|
|
265
|
-
chatHistories,
|
|
266
|
-
accountId,
|
|
248
|
+
const runFeishuHandler = async (params: { task: () => Promise<void>; errorMessage: string }) => {
|
|
249
|
+
if (fireAndForget) {
|
|
250
|
+
void params.task().catch((err) => {
|
|
251
|
+
error(`${params.errorMessage}: ${String(err)}`);
|
|
267
252
|
});
|
|
268
|
-
await enqueue(chatId, task);
|
|
269
|
-
};
|
|
270
|
-
const resolveSenderDebounceId = (event: FeishuMessageEvent): string | undefined => {
|
|
271
|
-
const senderId =
|
|
272
|
-
event.sender.sender_id.open_id?.trim() || event.sender.sender_id.user_id?.trim();
|
|
273
|
-
return senderId || undefined;
|
|
274
|
-
};
|
|
275
|
-
const resolveDebounceText = (event: FeishuMessageEvent): string => {
|
|
276
|
-
const botOpenId = botOpenIds.get(accountId);
|
|
277
|
-
const parsed = parseFeishuMessageEvent(event, botOpenId, botNames.get(accountId));
|
|
278
|
-
return parsed.content.trim();
|
|
279
|
-
};
|
|
280
|
-
const recordSuppressedMessageIds = async (
|
|
281
|
-
entries: FeishuMessageEvent[],
|
|
282
|
-
dispatchMessageId?: string,
|
|
283
|
-
) => {
|
|
284
|
-
const keepMessageId = dispatchMessageId?.trim();
|
|
285
|
-
const suppressedIds = new Set(
|
|
286
|
-
entries
|
|
287
|
-
.map((entry) => entry.message.message_id?.trim())
|
|
288
|
-
.filter((id): id is string => Boolean(id) && (!keepMessageId || id !== keepMessageId)),
|
|
289
|
-
);
|
|
290
|
-
if (suppressedIds.size === 0) {
|
|
291
253
|
return;
|
|
292
254
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
await tryRecordMessagePersistent(messageId, accountId, log);
|
|
298
|
-
} catch (err) {
|
|
299
|
-
error(
|
|
300
|
-
`feishu[${accountId}]: failed to record merged dedupe id ${messageId}: ${String(err)}`,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
255
|
+
try {
|
|
256
|
+
await params.task();
|
|
257
|
+
} catch (err) {
|
|
258
|
+
error(`${params.errorMessage}: ${String(err)}`);
|
|
303
259
|
}
|
|
304
260
|
};
|
|
305
|
-
const isMessageAlreadyProcessed = async (entry: FeishuMessageEvent): Promise<boolean> => {
|
|
306
|
-
const messageId = entry.message.message_id?.trim();
|
|
307
|
-
if (!messageId) {
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
const memoryKey = `${accountId}:${messageId}`;
|
|
311
|
-
if (hasRecordedMessage(memoryKey)) {
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
return hasRecordedMessagePersistent(messageId, accountId, log);
|
|
315
|
-
};
|
|
316
|
-
const inboundDebouncer = core.channel.debounce.createInboundDebouncer<FeishuMessageEvent>({
|
|
317
|
-
debounceMs: inboundDebounceMs,
|
|
318
|
-
buildKey: (event) => {
|
|
319
|
-
const chatId = event.message.chat_id?.trim();
|
|
320
|
-
const senderId = resolveSenderDebounceId(event);
|
|
321
|
-
if (!chatId || !senderId) {
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
const rootId = event.message.root_id?.trim();
|
|
325
|
-
const threadKey = rootId ? `thread:${rootId}` : "chat";
|
|
326
|
-
return `feishu:${accountId}:${chatId}:${threadKey}:${senderId}`;
|
|
327
|
-
},
|
|
328
|
-
shouldDebounce: (event) => {
|
|
329
|
-
if (event.message.message_type !== "text") {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
const text = resolveDebounceText(event);
|
|
333
|
-
if (!text) {
|
|
334
|
-
return false;
|
|
335
|
-
}
|
|
336
|
-
return !core.channel.text.hasControlCommand(text, cfg);
|
|
337
|
-
},
|
|
338
|
-
onFlush: async (entries) => {
|
|
339
|
-
const last = entries.at(-1);
|
|
340
|
-
if (!last) {
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (entries.length === 1) {
|
|
344
|
-
await dispatchFeishuMessage(last);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
const dedupedEntries = dedupeFeishuDebounceEntriesByMessageId(entries);
|
|
348
|
-
const freshEntries: FeishuMessageEvent[] = [];
|
|
349
|
-
for (const entry of dedupedEntries) {
|
|
350
|
-
if (!(await isMessageAlreadyProcessed(entry))) {
|
|
351
|
-
freshEntries.push(entry);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
const dispatchEntry = freshEntries.at(-1);
|
|
355
|
-
if (!dispatchEntry) {
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
await recordSuppressedMessageIds(dedupedEntries, dispatchEntry.message.message_id);
|
|
359
|
-
const combinedText = freshEntries
|
|
360
|
-
.map((entry) => resolveDebounceText(entry))
|
|
361
|
-
.filter(Boolean)
|
|
362
|
-
.join("\n");
|
|
363
|
-
const mergedMentions = resolveFeishuDebounceMentions({
|
|
364
|
-
entries: freshEntries,
|
|
365
|
-
botOpenId: botOpenIds.get(accountId),
|
|
366
|
-
});
|
|
367
|
-
if (!combinedText.trim()) {
|
|
368
|
-
await dispatchFeishuMessage({
|
|
369
|
-
...dispatchEntry,
|
|
370
|
-
message: {
|
|
371
|
-
...dispatchEntry.message,
|
|
372
|
-
mentions: mergedMentions ?? dispatchEntry.message.mentions,
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
await dispatchFeishuMessage({
|
|
378
|
-
...dispatchEntry,
|
|
379
|
-
message: {
|
|
380
|
-
...dispatchEntry.message,
|
|
381
|
-
message_type: "text",
|
|
382
|
-
content: JSON.stringify({ text: combinedText }),
|
|
383
|
-
mentions: mergedMentions ?? dispatchEntry.message.mentions,
|
|
384
|
-
},
|
|
385
|
-
});
|
|
386
|
-
},
|
|
387
|
-
onError: (err) => {
|
|
388
|
-
error(`feishu[${accountId}]: inbound debounce flush failed: ${String(err)}`);
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
261
|
|
|
392
262
|
eventDispatcher.register({
|
|
393
|
-
"im.message.receive_v1":
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
},
|
|
263
|
+
"im.message.receive_v1": createFeishuMessageReceiveHandler({
|
|
264
|
+
cfg,
|
|
265
|
+
core: getFeishuRuntime(),
|
|
266
|
+
accountId,
|
|
267
|
+
runtime,
|
|
268
|
+
chatHistories,
|
|
269
|
+
fireAndForget,
|
|
270
|
+
handleMessage: handleFeishuMessage,
|
|
271
|
+
resolveDebounceText: ({ event, botOpenId, botName }) =>
|
|
272
|
+
parseFeishuMessageEvent(event, botOpenId, botName).content,
|
|
273
|
+
hasProcessedMessage: hasProcessedFeishuMessage,
|
|
274
|
+
recordProcessedMessage: recordProcessedFeishuMessage,
|
|
275
|
+
getBotOpenId: (id) => botOpenIds.get(id),
|
|
276
|
+
getBotName: (id) => botNames.get(id),
|
|
277
|
+
resolveSequentialKey: getFeishuSequentialKey,
|
|
278
|
+
}),
|
|
410
279
|
"im.message.message_read_v1": async () => {
|
|
411
280
|
// Ignore read receipts
|
|
412
281
|
},
|
|
413
282
|
"im.chat.member.bot.added_v1": async (data) => {
|
|
414
283
|
try {
|
|
415
|
-
const event = data
|
|
284
|
+
const event = parseFeishuBotAddedEventPayload(data);
|
|
285
|
+
if (!event) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
416
288
|
log(`feishu[${accountId}]: bot added to chat ${event.chat_id}`);
|
|
417
289
|
} catch (err) {
|
|
418
290
|
error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
|
|
@@ -420,63 +292,94 @@ function registerEventHandlers(
|
|
|
420
292
|
},
|
|
421
293
|
"im.chat.member.bot.deleted_v1": async (data) => {
|
|
422
294
|
try {
|
|
423
|
-
const
|
|
424
|
-
|
|
295
|
+
const chatId = parseFeishuBotRemovedChatId(data);
|
|
296
|
+
if (!chatId) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
log(`feishu[${accountId}]: bot removed from chat ${chatId}`);
|
|
425
300
|
} catch (err) {
|
|
426
301
|
error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
|
|
427
302
|
}
|
|
428
303
|
},
|
|
304
|
+
"drive.notice.comment_add_v1": createFeishuDriveCommentNoticeHandler({
|
|
305
|
+
cfg,
|
|
306
|
+
accountId,
|
|
307
|
+
runtime,
|
|
308
|
+
fireAndForget,
|
|
309
|
+
}),
|
|
429
310
|
"im.message.reaction.created_v1": async (data) => {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
const promise = handleFeishuMessage({
|
|
444
|
-
cfg,
|
|
445
|
-
event: syntheticEvent,
|
|
446
|
-
botOpenId: myBotId,
|
|
447
|
-
botName: botNames.get(accountId),
|
|
448
|
-
runtime,
|
|
449
|
-
chatHistories,
|
|
450
|
-
accountId,
|
|
451
|
-
});
|
|
452
|
-
if (fireAndForget) {
|
|
453
|
-
promise.catch((err) => {
|
|
454
|
-
error(`feishu[${accountId}]: error handling reaction: ${String(err)}`);
|
|
311
|
+
await runFeishuHandler({
|
|
312
|
+
errorMessage: `feishu[${accountId}]: error handling reaction event`,
|
|
313
|
+
task: async () => {
|
|
314
|
+
const event = data as FeishuReactionCreatedEvent;
|
|
315
|
+
const myBotId = botOpenIds.get(accountId);
|
|
316
|
+
const syntheticEvent = await resolveReactionSyntheticEvent({
|
|
317
|
+
cfg,
|
|
318
|
+
accountId,
|
|
319
|
+
event,
|
|
320
|
+
botOpenId: myBotId,
|
|
321
|
+
logger: log,
|
|
455
322
|
});
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
error(`feishu[${accountId}]: error handling reaction event: ${String(err)}`);
|
|
472
|
-
}
|
|
323
|
+
if (!syntheticEvent) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const promise = handleFeishuMessage({
|
|
327
|
+
cfg,
|
|
328
|
+
event: syntheticEvent,
|
|
329
|
+
botOpenId: myBotId,
|
|
330
|
+
botName: botNames.get(accountId),
|
|
331
|
+
runtime,
|
|
332
|
+
chatHistories,
|
|
333
|
+
accountId,
|
|
334
|
+
});
|
|
335
|
+
await promise;
|
|
336
|
+
},
|
|
337
|
+
});
|
|
473
338
|
},
|
|
474
|
-
"im.message.reaction.deleted_v1": async () => {
|
|
475
|
-
|
|
339
|
+
"im.message.reaction.deleted_v1": async (data) => {
|
|
340
|
+
await runFeishuHandler({
|
|
341
|
+
errorMessage: `feishu[${accountId}]: error handling reaction removal event`,
|
|
342
|
+
task: async () => {
|
|
343
|
+
const event = data as FeishuReactionDeletedEvent;
|
|
344
|
+
const myBotId = botOpenIds.get(accountId);
|
|
345
|
+
const syntheticEvent = await resolveReactionSyntheticEvent({
|
|
346
|
+
cfg,
|
|
347
|
+
accountId,
|
|
348
|
+
event,
|
|
349
|
+
botOpenId: myBotId,
|
|
350
|
+
logger: log,
|
|
351
|
+
action: "deleted",
|
|
352
|
+
});
|
|
353
|
+
if (!syntheticEvent) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const promise = handleFeishuMessage({
|
|
357
|
+
cfg,
|
|
358
|
+
event: syntheticEvent,
|
|
359
|
+
botOpenId: myBotId,
|
|
360
|
+
botName: botNames.get(accountId),
|
|
361
|
+
runtime,
|
|
362
|
+
chatHistories,
|
|
363
|
+
accountId,
|
|
364
|
+
});
|
|
365
|
+
await promise;
|
|
366
|
+
},
|
|
367
|
+
});
|
|
476
368
|
},
|
|
369
|
+
"application.bot.menu_v6": createFeishuBotMenuHandler({
|
|
370
|
+
cfg,
|
|
371
|
+
accountId,
|
|
372
|
+
runtime,
|
|
373
|
+
chatHistories,
|
|
374
|
+
fireAndForget,
|
|
375
|
+
}),
|
|
477
376
|
"card.action.trigger": async (data: unknown) => {
|
|
478
377
|
try {
|
|
479
|
-
const event = data
|
|
378
|
+
const event = parseFeishuCardActionEventPayload(data);
|
|
379
|
+
if (!event) {
|
|
380
|
+
error(`feishu[${accountId}]: ignoring malformed card action payload`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
480
383
|
const promise = handleFeishuCardAction({
|
|
481
384
|
cfg,
|
|
482
385
|
event,
|
|
@@ -508,6 +411,7 @@ export type MonitorSingleAccountParams = {
|
|
|
508
411
|
runtime?: RuntimeEnv;
|
|
509
412
|
abortSignal?: AbortSignal;
|
|
510
413
|
botOpenIdSource?: BotOpenIdSource;
|
|
414
|
+
fireAndForget?: boolean;
|
|
511
415
|
};
|
|
512
416
|
|
|
513
417
|
export async function monitorSingleAccount(params: MonitorSingleAccountParams): Promise<void> {
|
|
@@ -520,16 +424,13 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
|
|
|
520
424
|
botOpenIdSource.kind === "prefetched"
|
|
521
425
|
? { botOpenId: botOpenIdSource.botOpenId, botName: botOpenIdSource.botName }
|
|
522
426
|
: await fetchBotIdentityForMonitor(account, { runtime, abortSignal });
|
|
523
|
-
const botOpenId = botIdentity
|
|
524
|
-
const botName = botIdentity.botName?.trim();
|
|
525
|
-
botOpenIds.set(accountId, botOpenId ?? "");
|
|
526
|
-
if (botName) {
|
|
527
|
-
botNames.set(accountId, botName);
|
|
528
|
-
} else {
|
|
529
|
-
botNames.delete(accountId);
|
|
530
|
-
}
|
|
427
|
+
const { botOpenId } = applyBotIdentityState(accountId, botIdentity);
|
|
531
428
|
log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
|
|
532
429
|
|
|
430
|
+
if (!botOpenId && !abortSignal?.aborted) {
|
|
431
|
+
startBotIdentityRecovery({ account, accountId, runtime, abortSignal });
|
|
432
|
+
}
|
|
433
|
+
|
|
533
434
|
const connectionMode = account.config.connectionMode ?? "websocket";
|
|
534
435
|
if (connectionMode === "webhook" && !account.verificationToken?.trim()) {
|
|
535
436
|
throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`);
|
|
@@ -543,19 +444,25 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
|
|
|
543
444
|
log(`feishu[${accountId}]: dedup warmup loaded ${warmupCount} entries from disk`);
|
|
544
445
|
}
|
|
545
446
|
|
|
546
|
-
|
|
547
|
-
|
|
447
|
+
let threadBindingManager: ReturnType<typeof createFeishuThreadBindingManager> | null = null;
|
|
448
|
+
try {
|
|
449
|
+
const eventDispatcher = createEventDispatcher(account);
|
|
450
|
+
const chatHistories = new Map<string, HistoryEntry[]>();
|
|
451
|
+
threadBindingManager = createFeishuThreadBindingManager({ accountId, cfg });
|
|
548
452
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
453
|
+
registerEventHandlers(eventDispatcher, {
|
|
454
|
+
cfg,
|
|
455
|
+
accountId,
|
|
456
|
+
runtime,
|
|
457
|
+
chatHistories,
|
|
458
|
+
fireAndForget: params.fireAndForget ?? true,
|
|
459
|
+
});
|
|
556
460
|
|
|
557
|
-
|
|
558
|
-
|
|
461
|
+
if (connectionMode === "webhook") {
|
|
462
|
+
return await monitorWebhook({ account, accountId, runtime, abortSignal, eventDispatcher });
|
|
463
|
+
}
|
|
464
|
+
return await monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher });
|
|
465
|
+
} finally {
|
|
466
|
+
threadBindingManager?.stop();
|
|
559
467
|
}
|
|
560
|
-
return monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher });
|
|
561
468
|
}
|