
























@@ -1,7 +1,6 @@
11import * as crypto from "node:crypto";
22import type * as Lark from "@larksuiteoapi/node-sdk";
33import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "../runtime-api.js";
4-import { resolveFeishuAccount } from "./accounts.js";
54import { raceWithTimeoutAndAbort } from "./async.js";
65import {
76handleFeishuMessage,
@@ -10,50 +9,30 @@ import {
109type FeishuBotAddedEvent,
1110} from "./bot.js";
1211import { handleFeishuCardAction, type FeishuCardActionEvent } from "./card-action.js";
13-import { maybeHandleFeishuQuickActionMenu } from "./card-ux-launcher.js";
1412import { createEventDispatcher } from "./client.js";
15-import { handleFeishuCommentEvent } from "./comment-handler.js";
1613import { isRecord, readString } from "./comment-shared.js";
1714import {
18-claimUnprocessedFeishuMessage,
1915hasProcessedFeishuMessage,
2016recordProcessedFeishuMessage,
21-releaseFeishuMessageProcessing,
2217warmupDedupFromDisk,
2318} from "./dedup.js";
2419import { applyBotIdentityState, startBotIdentityRecovery } from "./monitor.bot-identity.js";
25-import { parseFeishuDriveCommentNoticeEventPayload } from "./monitor.comment.js";
20+import { createFeishuBotMenuHandler } from "./monitor.bot-menu-handler.js";
21+import { createFeishuDriveCommentNoticeHandler } from "./monitor.comment-notice-handler.js";
2622import { createFeishuMessageReceiveHandler } from "./monitor.message-handler.js";
2723import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
2824import { botNames, botOpenIds } from "./monitor.state.js";
25+import { FeishuRetryableSyntheticEventError } from "./monitor.synthetic-error.js";
2926import { monitorWebhook, monitorWebSocket } from "./monitor.transport.js";
3027import { getFeishuRuntime } from "./runtime.js";
3128import { getMessageFeishu } from "./send.js";
3229import { getFeishuSequentialKey } from "./sequential-key.js";
33-import { createSequentialQueue } from "./sequential-queue.js";
3430import { createFeishuThreadBindingManager } from "./thread-bindings.js";
3531import type { FeishuChatType, ResolvedFeishuAccount } from "./types.js";
36323733const FEISHU_REACTION_VERIFY_TIMEOUT_MS = 1_500;
383439-export class FeishuRetryableSyntheticEventError extends Error {
40-constructor(message: string, options?: ErrorOptions) {
41-super(message, options);
42-this.name = "FeishuRetryableSyntheticEventError";
43-}
44-}
45-46-function isFeishuRetryableSyntheticEventError(
47-error: unknown,
48-): error is FeishuRetryableSyntheticEventError {
49-return (
50-error instanceof FeishuRetryableSyntheticEventError ||
51-(typeof error === "object" &&
52-error !== null &&
53-"name" in error &&
54-error.name === "FeishuRetryableSyntheticEventError")
55-);
56-}
35+export { FeishuRetryableSyntheticEventError };
57365837export type FeishuReactionCreatedEvent = {
5938message_id: string;
@@ -104,6 +83,7 @@ export async function resolveReactionSyntheticEvent(
10483return null;
10584}
1068586+const { resolveFeishuAccount } = await import("./accounts.js");
10787const account = resolveFeishuAccount({ cfg, accountId });
10888const reactionNotifications = account.config.reactionNotifications ?? "own";
10989if (reactionNotifications === "off") {
@@ -187,19 +167,6 @@ type RegisterEventHandlersContext = {
187167fireAndForget?: boolean;
188168};
189169190-type FeishuBotMenuEvent = {
191-event_key?: string;
192-timestamp?: string | number;
193-operator?: {
194-operator_name?: string;
195-operator_id?: { open_id?: string; user_id?: string; union_id?: string };
196-};
197-};
198-199-function readStringOrNumber(value: unknown): string | number | undefined {
200-return typeof value === "string" || typeof value === "number" ? value : undefined;
201-}
202-203170function parseFeishuBotAddedEventPayload(value: unknown): FeishuBotAddedEvent | null {
204171if (!isRecord(value) || !readString(value.chat_id) || !isRecord(value.operator_id)) {
205172return null;
@@ -214,32 +181,6 @@ function parseFeishuBotRemovedChatId(value: unknown): string | null {
214181return readString(value.chat_id) ?? null;
215182}
216183217-function parseFeishuBotMenuEvent(value: unknown): FeishuBotMenuEvent | null {
218-if (!isRecord(value)) {
219-return null;
220-}
221-const operator = value.operator;
222-if (operator !== undefined && !isRecord(operator)) {
223-return null;
224-}
225-return {
226-event_key: readString(value.event_key),
227-timestamp: readStringOrNumber(value.timestamp),
228-operator: operator
229- ? {
230-operator_name: readString(operator.operator_name),
231-operator_id: isRecord(operator.operator_id)
232- ? {
233-open_id: readString(operator.operator_id.open_id),
234-user_id: readString(operator.operator_id.user_id),
235-union_id: readString(operator.operator_id.union_id),
236-}
237- : undefined,
238-}
239- : undefined,
240-};
241-}
242-243184function parseFeishuCardActionEventPayload(value: unknown): FeishuCardActionEvent | null {
244185if (!isRecord(value)) {
245186return null;
@@ -291,25 +232,13 @@ function parseFeishuCardActionEventPayload(value: unknown): FeishuCardActionEven
291232};
292233}
293234294-function buildCommentNoticeQueueKey(event: {
295-notice_meta?: {
296-file_type?: string;
297-file_token?: string;
298-};
299-}): string {
300-const fileType = event.notice_meta?.file_type?.trim() || "unknown";
301-const fileToken = event.notice_meta?.file_token?.trim() || "unknown";
302-return `comment-doc:${fileType}:${fileToken}`;
303-}
304235function registerEventHandlers(
305236eventDispatcher: Lark.EventDispatcher,
306237context: RegisterEventHandlersContext,
307238): void {
308239const { cfg, accountId, runtime, chatHistories, fireAndForget } = context;
309240const log = runtime?.log ?? console.log;
310241const error = runtime?.error ?? console.error;
311-// Non-message lifecycle events still share FIFO execution by resource key.
312-const enqueue = createSequentialQueue();
313242const runFeishuHandler = async (params: { task: () => Promise<void>; errorMessage: string }) => {
314243if (fireAndForget) {
315244void params.task().catch((err) => {
@@ -366,68 +295,12 @@ function registerEventHandlers(
366295error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
367296}
368297},
369-"drive.notice.comment_add_v1": async (data: unknown) => {
370-await runFeishuHandler({
371-errorMessage: `feishu[${accountId}]: error handling drive comment notice`,
372-task: async () => {
373-const event = parseFeishuDriveCommentNoticeEventPayload(data);
374-if (!event) {
375-error(`feishu[${accountId}]: ignoring malformed drive comment notice payload`);
376-return;
377-}
378-const eventId = event.event_id?.trim();
379-const syntheticMessageId = eventId ? `drive-comment:${eventId}` : undefined;
380-if (syntheticMessageId) {
381-const claim = await claimUnprocessedFeishuMessage({
382-messageId: syntheticMessageId,
383-namespace: accountId,
384- log,
385-});
386-if (claim === "duplicate") {
387-log(`feishu[${accountId}]: dropping duplicate comment event ${syntheticMessageId}`);
388-return;
389-}
390-if (claim === "inflight") {
391-log(`feishu[${accountId}]: dropping in-flight comment event ${syntheticMessageId}`);
392-return;
393-}
394-}
395-log(
396-`feishu[${accountId}]: received drive comment notice ` +
397-`event=${event.event_id ?? "unknown"} ` +
398-`type=${event.notice_meta?.notice_type ?? "unknown"} ` +
399-`file=${event.notice_meta?.file_type ?? "unknown"}:${event.notice_meta?.file_token ?? "unknown"} ` +
400-`comment=${event.comment_id ?? "unknown"} ` +
401-`reply=${event.reply_id ?? "none"} ` +
402-`from=${event.notice_meta?.from_user_id?.open_id ?? "unknown"} ` +
403-`mentioned=${event.is_mentioned === true ? "yes" : "no"}`,
404-);
405-try {
406-await enqueue(buildCommentNoticeQueueKey(event), async () => {
407-await handleFeishuCommentEvent({
408- cfg,
409- accountId,
410- event,
411-botOpenId: botOpenIds.get(accountId),
412- runtime,
413-});
414-});
415-if (syntheticMessageId) {
416-await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
417-}
418-} catch (err) {
419-if (syntheticMessageId && !isFeishuRetryableSyntheticEventError(err)) {
420-await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
421-}
422-throw err;
423-} finally {
424-if (syntheticMessageId) {
425-releaseFeishuMessageProcessing(syntheticMessageId, accountId);
426-}
427-}
428-},
429-});
430-},
298+"drive.notice.comment_add_v1": createFeishuDriveCommentNoticeHandler({
299+ cfg,
300+ accountId,
301+ runtime,
302+ fireAndForget,
303+}),
431304"im.message.reaction.created_v1": async (data) => {
432305await runFeishuHandler({
433306errorMessage: `feishu[${accountId}]: error handling reaction event`,
@@ -487,96 +360,13 @@ function registerEventHandlers(
487360},
488361});
489362},
490-"application.bot.menu_v6": async (data) => {
491-try {
492-const event = parseFeishuBotMenuEvent(data);
493-if (!event) {
494-return;
495-}
496-const operatorOpenId = event.operator?.operator_id?.open_id?.trim();
497-const eventKey = event.event_key?.trim();
498-if (!operatorOpenId || !eventKey) {
499-return;
500-}
501-const syntheticEvent: FeishuMessageEvent = {
502-sender: {
503-sender_id: {
504-open_id: operatorOpenId,
505-user_id: event.operator?.operator_id?.user_id,
506-union_id: event.operator?.operator_id?.union_id,
507-},
508-sender_type: "user",
509-},
510-message: {
511-message_id: `bot-menu:${eventKey}:${event.timestamp ?? Date.now()}`,
512-chat_id: `p2p:${operatorOpenId}`,
513-chat_type: "p2p",
514-message_type: "text",
515-content: JSON.stringify({
516-text: `/menu ${eventKey}`,
517-}),
518-},
519-};
520-const syntheticMessageId = syntheticEvent.message.message_id;
521-const claim = await claimUnprocessedFeishuMessage({
522-messageId: syntheticMessageId,
523-namespace: accountId,
524- log,
525-});
526-if (claim === "duplicate") {
527-log(`feishu[${accountId}]: dropping duplicate bot-menu event for ${syntheticMessageId}`);
528-return;
529-}
530-if (claim === "inflight") {
531-log(`feishu[${accountId}]: dropping in-flight bot-menu event for ${syntheticMessageId}`);
532-return;
533-}
534-const handleLegacyMenu = () =>
535-handleFeishuMessage({
536- cfg,
537-event: syntheticEvent,
538-botOpenId: botOpenIds.get(accountId),
539-botName: botNames.get(accountId),
540- runtime,
541- chatHistories,
542- accountId,
543-processingClaimHeld: true,
544-});
545-546-const promise = maybeHandleFeishuQuickActionMenu({
547- cfg,
548- eventKey,
549- operatorOpenId,
550- runtime,
551- accountId,
552-})
553-.then(async (handledMenu) => {
554-if (handledMenu) {
555-await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
556-releaseFeishuMessageProcessing(syntheticMessageId, accountId);
557-return;
558-}
559-return await handleLegacyMenu();
560-})
561-.catch(async (err) => {
562-if (isFeishuRetryableSyntheticEventError(err)) {
563-releaseFeishuMessageProcessing(syntheticMessageId, accountId);
564-} else {
565-await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
566-}
567-throw err;
568-});
569-if (fireAndForget) {
570-promise.catch((err) => {
571-error(`feishu[${accountId}]: error handling bot menu event: ${String(err)}`);
572-});
573-return;
574-}
575-await promise;
576-} catch (err) {
577-error(`feishu[${accountId}]: error handling bot menu event: ${String(err)}`);
578-}
579-},
363+"application.bot.menu_v6": createFeishuBotMenuHandler({
364+ cfg,
365+ accountId,
366+ runtime,
367+ chatHistories,
368+ fireAndForget,
369+}),
580370"card.action.trigger": async (data: unknown) => {
581371try {
582372const event = parseFeishuCardActionEventPayload(data);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。