fix(feishu): drop self-authored receive events (#90572)

This commit is contained in:
Yuan
2026-06-16 16:11:51 +09:00
committed by GitHub
parent 7fc124dcf1
commit 385d1ada91
2 changed files with 120 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
// Feishu tests cover monitor.message handler plugin behavior.
import { describe, expect, it, vi } from "vitest";
import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js";
import type { FeishuMessageEvent } from "./event-types.js";
import { createFeishuMessageReceiveHandler } from "./monitor.message-handler.js";
type MessageReceiveHandlerContext = Parameters<typeof createFeishuMessageReceiveHandler>[0];
type HandleMessageParams = Parameters<MessageReceiveHandlerContext["handleMessage"]>[0];
function createTextEvent(params: {
messageId: string;
senderOpenId: string;
senderType: "bot" | "user";
}): FeishuMessageEvent {
return {
sender: {
sender_id: { open_id: params.senderOpenId },
sender_type: params.senderType,
},
message: {
message_id: params.messageId,
chat_id: "oc_chat_1",
chat_type: "p2p",
message_type: "text",
content: JSON.stringify({ text: "hello" }),
},
};
}
function createHandler() {
let onFlush: ((entries: FeishuMessageEvent[]) => Promise<void>) | undefined;
const enqueue = vi.fn(async (event: FeishuMessageEvent) => {
await onFlush?.([event]);
});
const channelRuntime = {
commands: {
isControlCommandMessage: () => false,
},
debounce: {
resolveInboundDebounceMs: () => 0,
createInboundDebouncer: vi.fn((params: { onFlush: typeof onFlush }) => {
onFlush = params.onFlush;
return { enqueue };
}),
},
} as unknown as PluginRuntime["channel"];
const handleMessage = vi.fn(async (_params: HandleMessageParams) => {});
const handler = createFeishuMessageReceiveHandler({
cfg: {} as ClawdbotConfig,
channelRuntime,
accountId: "default",
chatHistories: new Map(),
handleMessage,
resolveDebounceText: () => "hello",
hasProcessedMessage: vi.fn(async () => false),
recordProcessedMessage: vi.fn(async () => true),
getBotOpenId: () => "ou_bot",
});
return { handler, handleMessage, enqueue };
}
describe("createFeishuMessageReceiveHandler self-message filtering", () => {
it("drops the current bot before debounce and processing claims", async () => {
const { handler, handleMessage, enqueue } = createHandler();
await handler(
createTextEvent({
messageId: "om_reused",
senderOpenId: "ou_bot",
senderType: "bot",
}),
);
await handler(
createTextEvent({
messageId: "om_reused",
senderOpenId: "ou_user",
senderType: "user",
}),
);
expect(enqueue).toHaveBeenCalledTimes(1);
expect(handleMessage).toHaveBeenCalledTimes(1);
expect(handleMessage.mock.calls[0]?.[0]?.event.sender.sender_id.open_id).toBe("ou_user");
});
it("keeps peer bot and user messages flowing to dispatch", async () => {
const { handler, handleMessage, enqueue } = createHandler();
await handler(
createTextEvent({
messageId: "om_other_bot",
senderOpenId: "ou_other_bot",
senderType: "bot",
}),
);
await handler(
createTextEvent({
messageId: "om_user",
senderOpenId: "ou_user",
senderType: "user",
}),
);
expect(enqueue).toHaveBeenCalledTimes(2);
expect(handleMessage).toHaveBeenCalledTimes(2);
expect(
handleMessage.mock.calls.map(([params]) => params.event.sender.sender_id.open_id),
).toEqual(["ou_other_bot", "ou_user"]);
});
});

View File

@@ -321,6 +321,14 @@ export function createFeishuMessageReceiveHandler({
return;
}
const messageId = event.message?.message_id?.trim();
const botOpenId = getBotOpenId(accountId)?.trim();
const senderOpenId = event.sender.sender_id.open_id?.trim();
if (botOpenId && senderOpenId === botOpenId) {
// Feishu bot receive events identify their sender by open_id. Drop this
// account's bot before it can consume a claim or debounce slot.
log(`feishu[${accountId}]: dropping self-authored message ${messageId ?? "unknown"}`);
return;
}
const messageDedupeKey = resolveFeishuMessageDedupeKey(event);
if (!tryBeginFeishuMessageProcessing(messageDedupeKey, accountId)) {
log(`feishu[${accountId}]: dropping duplicate event for message ${messageId}`);