

























@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
55import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js";
66import type { FeishuMessageEvent } from "./bot.js";
77import { handleFeishuMessage } from "./bot.js";
8+import { createFeishuMessageReceiveHandler } from "./monitor.message-handler.js";
89import { setFeishuRuntime } from "./runtime.js";
9101011type ConfiguredBindingRoute = ReturnType<typeof ConversationRuntime.resolveConfiguredBindingRoute>;
@@ -3000,6 +3001,58 @@ describe("handleFeishuMessage command authorization", () => {
30003001expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
30013002});
300230033004+it("dedupes Feishu media by message_id plus file_key", async () => {
3005+mockShouldComputeCommandAuthorized.mockReturnValue(false);
3006+3007+const cfg: ClawdbotConfig = {
3008+channels: {
3009+feishu: {
3010+dmPolicy: "open",
3011+},
3012+},
3013+} as ClawdbotConfig;
3014+const createAudioEvent = (fileKey: string): FeishuMessageEvent => ({
3015+sender: {
3016+sender_id: {
3017+open_id: "ou-audio-dedup",
3018+},
3019+},
3020+message: {
3021+message_id: "msg-audio-reused-id",
3022+chat_id: "oc-dm",
3023+chat_type: "p2p",
3024+message_type: "audio",
3025+content: JSON.stringify({
3026+file_key: fileKey,
3027+duration: 1200,
3028+}),
3029+},
3030+});
3031+3032+await dispatchMessage({ cfg, event: createAudioEvent("file_audio_first") });
3033+await dispatchMessage({ cfg, event: createAudioEvent("file_audio_second") });
3034+await dispatchMessage({ cfg, event: createAudioEvent("file_audio_first") });
3035+3036+expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(2);
3037+expect(mockDownloadMessageResourceFeishu).toHaveBeenCalledTimes(2);
3038+expect(mockDownloadMessageResourceFeishu).toHaveBeenNthCalledWith(
3039+1,
3040+expect.objectContaining({
3041+messageId: "msg-audio-reused-id",
3042+fileKey: "file_audio_first",
3043+type: "file",
3044+}),
3045+);
3046+expect(mockDownloadMessageResourceFeishu).toHaveBeenNthCalledWith(
3047+2,
3048+expect.objectContaining({
3049+messageId: "msg-audio-reused-id",
3050+fileKey: "file_audio_second",
3051+type: "file",
3052+}),
3053+);
3054+});
3055+30033056it("skips empty-text messages with no media to prevent blank user turns in session (#74634)", async () => {
30043057// Feishu can deliver { "text": "" } events (empty-text or media-stripped
30053058// messages). Writing blank user content to the session causes downstream
@@ -3038,3 +3091,73 @@ describe("handleFeishuMessage command authorization", () => {
30383091expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
30393092});
30403093});
3094+3095+describe("createFeishuMessageReceiveHandler media dedupe", () => {
3096+it("keeps same-id media variants distinct at receive time", async () => {
3097+const handleMessage = vi.fn(async () => undefined);
3098+const core = {
3099+channel: {
3100+debounce: {
3101+resolveInboundDebounceMs: vi.fn(() => 0),
3102+createInboundDebouncer: vi.fn(
3103+(options: { onFlush: (entries: FeishuMessageEvent[]) => Promise<void> | void }) => ({
3104+enqueue: async (event: FeishuMessageEvent) => {
3105+await options.onFlush([event]);
3106+},
3107+}),
3108+),
3109+},
3110+text: {
3111+hasControlCommand: vi.fn(() => false),
3112+},
3113+},
3114+} as unknown as PluginRuntime;
3115+const createAudioEvent = (fileKey: string): FeishuMessageEvent => ({
3116+sender: {
3117+sender_id: {
3118+open_id: "ou-audio-receive-dedup",
3119+},
3120+},
3121+message: {
3122+message_id: "msg-audio-receive-reused-id",
3123+chat_id: "oc-dm",
3124+chat_type: "p2p",
3125+message_type: "audio",
3126+content: JSON.stringify({
3127+file_key: fileKey,
3128+duration: 1200,
3129+}),
3130+},
3131+});
3132+const handler = createFeishuMessageReceiveHandler({
3133+cfg: { channels: { feishu: { dmPolicy: "open" } } } as ClawdbotConfig,
3134+ core,
3135+accountId: "receive-media-dedupe",
3136+chatHistories: new Map(),
3137+ handleMessage,
3138+resolveDebounceText: () => "",
3139+hasProcessedMessage: vi.fn(async () => false),
3140+recordProcessedMessage: vi.fn(async () => true),
3141+});
3142+3143+await handler(createAudioEvent("file_audio_receive_first"));
3144+await handler(createAudioEvent("file_audio_receive_second"));
3145+await handler(createAudioEvent("file_audio_receive_first"));
3146+3147+expect(handleMessage).toHaveBeenCalledTimes(2);
3148+expect(handleMessage).toHaveBeenNthCalledWith(
3149+1,
3150+expect.objectContaining({
3151+event: createAudioEvent("file_audio_receive_first"),
3152+processingClaimHeld: true,
3153+}),
3154+);
3155+expect(handleMessage).toHaveBeenNthCalledWith(
3156+2,
3157+expect.objectContaining({
3158+event: createAudioEvent("file_audio_receive_second"),
3159+processingClaimHeld: true,
3160+}),
3161+);
3162+});
3163+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。