@openclaw/feishu 2026.3.13 → 2026.5.2-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 +1827 -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 +95 -7
- 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 +778 -775
- 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 +1253 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +135 -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 +406 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +63 -1
- 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 +33 -95
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +116 -20
- package/src/directory.ts +60 -92
- 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 +403 -26
- package/src/media.ts +509 -132
- 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 +218 -312
- 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 +108 -48
- package/src/monitor.startup.test.ts +11 -9
- 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 +220 -60
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +65 -7
- package/src/monitor.webhook-security.test.ts +122 -0
- package/src/monitor.webhook.test-helpers.ts +44 -26
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +616 -37
- 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 +14 -9
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +4 -34
- 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 +660 -29
- package/src/reply-dispatcher.ts +407 -154
- 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 +105 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +414 -95
- 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 +453 -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
15
|
hasProcessedFeishuMessage,
|
|
16
16
|
recordProcessedFeishuMessage,
|
|
17
|
-
releaseFeishuMessageProcessing,
|
|
18
|
-
tryBeginFeishuMessageProcessing,
|
|
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,175 +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,
|
|
267
|
-
processingClaimHeld: true,
|
|
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)}`);
|
|
268
252
|
});
|
|
269
|
-
await enqueue(chatId, task);
|
|
270
|
-
};
|
|
271
|
-
const resolveSenderDebounceId = (event: FeishuMessageEvent): string | undefined => {
|
|
272
|
-
const senderId =
|
|
273
|
-
event.sender.sender_id.open_id?.trim() || event.sender.sender_id.user_id?.trim();
|
|
274
|
-
return senderId || undefined;
|
|
275
|
-
};
|
|
276
|
-
const resolveDebounceText = (event: FeishuMessageEvent): string => {
|
|
277
|
-
const botOpenId = botOpenIds.get(accountId);
|
|
278
|
-
const parsed = parseFeishuMessageEvent(event, botOpenId, botNames.get(accountId));
|
|
279
|
-
return parsed.content.trim();
|
|
280
|
-
};
|
|
281
|
-
const recordSuppressedMessageIds = async (
|
|
282
|
-
entries: FeishuMessageEvent[],
|
|
283
|
-
dispatchMessageId?: string,
|
|
284
|
-
) => {
|
|
285
|
-
const keepMessageId = dispatchMessageId?.trim();
|
|
286
|
-
const suppressedIds = new Set(
|
|
287
|
-
entries
|
|
288
|
-
.map((entry) => entry.message.message_id?.trim())
|
|
289
|
-
.filter((id): id is string => Boolean(id) && (!keepMessageId || id !== keepMessageId)),
|
|
290
|
-
);
|
|
291
|
-
if (suppressedIds.size === 0) {
|
|
292
253
|
return;
|
|
293
254
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
error(
|
|
299
|
-
`feishu[${accountId}]: failed to record merged dedupe id ${messageId}: ${String(err)}`,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
255
|
+
try {
|
|
256
|
+
await params.task();
|
|
257
|
+
} catch (err) {
|
|
258
|
+
error(`${params.errorMessage}: ${String(err)}`);
|
|
302
259
|
}
|
|
303
260
|
};
|
|
304
|
-
const isMessageAlreadyProcessed = async (entry: FeishuMessageEvent): Promise<boolean> => {
|
|
305
|
-
return await hasProcessedFeishuMessage(entry.message.message_id, accountId, log);
|
|
306
|
-
};
|
|
307
|
-
const inboundDebouncer = core.channel.debounce.createInboundDebouncer<FeishuMessageEvent>({
|
|
308
|
-
debounceMs: inboundDebounceMs,
|
|
309
|
-
buildKey: (event) => {
|
|
310
|
-
const chatId = event.message.chat_id?.trim();
|
|
311
|
-
const senderId = resolveSenderDebounceId(event);
|
|
312
|
-
if (!chatId || !senderId) {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
const rootId = event.message.root_id?.trim();
|
|
316
|
-
const threadKey = rootId ? `thread:${rootId}` : "chat";
|
|
317
|
-
return `feishu:${accountId}:${chatId}:${threadKey}:${senderId}`;
|
|
318
|
-
},
|
|
319
|
-
shouldDebounce: (event) => {
|
|
320
|
-
if (event.message.message_type !== "text") {
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
const text = resolveDebounceText(event);
|
|
324
|
-
if (!text) {
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
return !core.channel.text.hasControlCommand(text, cfg);
|
|
328
|
-
},
|
|
329
|
-
onFlush: async (entries) => {
|
|
330
|
-
const last = entries.at(-1);
|
|
331
|
-
if (!last) {
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
if (entries.length === 1) {
|
|
335
|
-
await dispatchFeishuMessage(last);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
const dedupedEntries = dedupeFeishuDebounceEntriesByMessageId(entries);
|
|
339
|
-
const freshEntries: FeishuMessageEvent[] = [];
|
|
340
|
-
for (const entry of dedupedEntries) {
|
|
341
|
-
if (!(await isMessageAlreadyProcessed(entry))) {
|
|
342
|
-
freshEntries.push(entry);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
const dispatchEntry = freshEntries.at(-1);
|
|
346
|
-
if (!dispatchEntry) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
await recordSuppressedMessageIds(dedupedEntries, dispatchEntry.message.message_id);
|
|
350
|
-
const combinedText = freshEntries
|
|
351
|
-
.map((entry) => resolveDebounceText(entry))
|
|
352
|
-
.filter(Boolean)
|
|
353
|
-
.join("\n");
|
|
354
|
-
const mergedMentions = resolveFeishuDebounceMentions({
|
|
355
|
-
entries: freshEntries,
|
|
356
|
-
botOpenId: botOpenIds.get(accountId),
|
|
357
|
-
});
|
|
358
|
-
if (!combinedText.trim()) {
|
|
359
|
-
await dispatchFeishuMessage({
|
|
360
|
-
...dispatchEntry,
|
|
361
|
-
message: {
|
|
362
|
-
...dispatchEntry.message,
|
|
363
|
-
mentions: mergedMentions ?? dispatchEntry.message.mentions,
|
|
364
|
-
},
|
|
365
|
-
});
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
await dispatchFeishuMessage({
|
|
369
|
-
...dispatchEntry,
|
|
370
|
-
message: {
|
|
371
|
-
...dispatchEntry.message,
|
|
372
|
-
message_type: "text",
|
|
373
|
-
content: JSON.stringify({ text: combinedText }),
|
|
374
|
-
mentions: mergedMentions ?? dispatchEntry.message.mentions,
|
|
375
|
-
},
|
|
376
|
-
});
|
|
377
|
-
},
|
|
378
|
-
onError: (err, entries) => {
|
|
379
|
-
for (const entry of entries) {
|
|
380
|
-
releaseFeishuMessageProcessing(entry.message.message_id, accountId);
|
|
381
|
-
}
|
|
382
|
-
error(`feishu[${accountId}]: inbound debounce flush failed: ${String(err)}`);
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
261
|
|
|
386
262
|
eventDispatcher.register({
|
|
387
|
-
"im.message.receive_v1":
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
try {
|
|
405
|
-
await processMessage();
|
|
406
|
-
} catch (err) {
|
|
407
|
-
releaseFeishuMessageProcessing(messageId, accountId);
|
|
408
|
-
error(`feishu[${accountId}]: error handling message: ${String(err)}`);
|
|
409
|
-
}
|
|
410
|
-
},
|
|
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
|
+
}),
|
|
411
279
|
"im.message.message_read_v1": async () => {
|
|
412
280
|
// Ignore read receipts
|
|
413
281
|
},
|
|
414
282
|
"im.chat.member.bot.added_v1": async (data) => {
|
|
415
283
|
try {
|
|
416
|
-
const event = data
|
|
284
|
+
const event = parseFeishuBotAddedEventPayload(data);
|
|
285
|
+
if (!event) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
417
288
|
log(`feishu[${accountId}]: bot added to chat ${event.chat_id}`);
|
|
418
289
|
} catch (err) {
|
|
419
290
|
error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
|
|
@@ -421,63 +292,94 @@ function registerEventHandlers(
|
|
|
421
292
|
},
|
|
422
293
|
"im.chat.member.bot.deleted_v1": async (data) => {
|
|
423
294
|
try {
|
|
424
|
-
const
|
|
425
|
-
|
|
295
|
+
const chatId = parseFeishuBotRemovedChatId(data);
|
|
296
|
+
if (!chatId) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
log(`feishu[${accountId}]: bot removed from chat ${chatId}`);
|
|
426
300
|
} catch (err) {
|
|
427
301
|
error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
|
|
428
302
|
}
|
|
429
303
|
},
|
|
304
|
+
"drive.notice.comment_add_v1": createFeishuDriveCommentNoticeHandler({
|
|
305
|
+
cfg,
|
|
306
|
+
accountId,
|
|
307
|
+
runtime,
|
|
308
|
+
fireAndForget,
|
|
309
|
+
}),
|
|
430
310
|
"im.message.reaction.created_v1": async (data) => {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
const promise = handleFeishuMessage({
|
|
445
|
-
cfg,
|
|
446
|
-
event: syntheticEvent,
|
|
447
|
-
botOpenId: myBotId,
|
|
448
|
-
botName: botNames.get(accountId),
|
|
449
|
-
runtime,
|
|
450
|
-
chatHistories,
|
|
451
|
-
accountId,
|
|
452
|
-
});
|
|
453
|
-
if (fireAndForget) {
|
|
454
|
-
promise.catch((err) => {
|
|
455
|
-
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,
|
|
456
322
|
});
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
error(`feishu[${accountId}]: error handling reaction event: ${String(err)}`);
|
|
473
|
-
}
|
|
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
|
+
});
|
|
474
338
|
},
|
|
475
|
-
"im.message.reaction.deleted_v1": async () => {
|
|
476
|
-
|
|
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
|
+
});
|
|
477
368
|
},
|
|
369
|
+
"application.bot.menu_v6": createFeishuBotMenuHandler({
|
|
370
|
+
cfg,
|
|
371
|
+
accountId,
|
|
372
|
+
runtime,
|
|
373
|
+
chatHistories,
|
|
374
|
+
fireAndForget,
|
|
375
|
+
}),
|
|
478
376
|
"card.action.trigger": async (data: unknown) => {
|
|
479
377
|
try {
|
|
480
|
-
const event = data
|
|
378
|
+
const event = parseFeishuCardActionEventPayload(data);
|
|
379
|
+
if (!event) {
|
|
380
|
+
error(`feishu[${accountId}]: ignoring malformed card action payload`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
481
383
|
const promise = handleFeishuCardAction({
|
|
482
384
|
cfg,
|
|
483
385
|
event,
|
|
@@ -509,6 +411,7 @@ export type MonitorSingleAccountParams = {
|
|
|
509
411
|
runtime?: RuntimeEnv;
|
|
510
412
|
abortSignal?: AbortSignal;
|
|
511
413
|
botOpenIdSource?: BotOpenIdSource;
|
|
414
|
+
fireAndForget?: boolean;
|
|
512
415
|
};
|
|
513
416
|
|
|
514
417
|
export async function monitorSingleAccount(params: MonitorSingleAccountParams): Promise<void> {
|
|
@@ -521,16 +424,13 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
|
|
|
521
424
|
botOpenIdSource.kind === "prefetched"
|
|
522
425
|
? { botOpenId: botOpenIdSource.botOpenId, botName: botOpenIdSource.botName }
|
|
523
426
|
: await fetchBotIdentityForMonitor(account, { runtime, abortSignal });
|
|
524
|
-
const botOpenId = botIdentity
|
|
525
|
-
const botName = botIdentity.botName?.trim();
|
|
526
|
-
botOpenIds.set(accountId, botOpenId ?? "");
|
|
527
|
-
if (botName) {
|
|
528
|
-
botNames.set(accountId, botName);
|
|
529
|
-
} else {
|
|
530
|
-
botNames.delete(accountId);
|
|
531
|
-
}
|
|
427
|
+
const { botOpenId } = applyBotIdentityState(accountId, botIdentity);
|
|
532
428
|
log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
|
|
533
429
|
|
|
430
|
+
if (!botOpenId && !abortSignal?.aborted) {
|
|
431
|
+
startBotIdentityRecovery({ account, accountId, runtime, abortSignal });
|
|
432
|
+
}
|
|
433
|
+
|
|
534
434
|
const connectionMode = account.config.connectionMode ?? "websocket";
|
|
535
435
|
if (connectionMode === "webhook" && !account.verificationToken?.trim()) {
|
|
536
436
|
throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`);
|
|
@@ -544,19 +444,25 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
|
|
|
544
444
|
log(`feishu[${accountId}]: dedup warmup loaded ${warmupCount} entries from disk`);
|
|
545
445
|
}
|
|
546
446
|
|
|
547
|
-
|
|
548
|
-
|
|
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 });
|
|
549
452
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
453
|
+
registerEventHandlers(eventDispatcher, {
|
|
454
|
+
cfg,
|
|
455
|
+
accountId,
|
|
456
|
+
runtime,
|
|
457
|
+
chatHistories,
|
|
458
|
+
fireAndForget: params.fireAndForget ?? true,
|
|
459
|
+
});
|
|
557
460
|
|
|
558
|
-
|
|
559
|
-
|
|
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();
|
|
560
467
|
}
|
|
561
|
-
return monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher });
|
|
562
468
|
}
|