mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:01:36 +00:00
fix(discord): add media dedup production code for messaging tool pipeline
Wire media URL tracking through the embedded agent pipeline so that media already sent via messaging tools is not delivered again by the reply dispatcher. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
c7681c3cff
commit
838259331f
@@ -10,6 +10,7 @@ import { normalizeReplyPayloadDirectives } from "./reply-delivery.js";
|
||||
import {
|
||||
applyReplyThreading,
|
||||
filterMessagingToolDuplicates,
|
||||
filterMessagingToolMediaDuplicates,
|
||||
isRenderablePayload,
|
||||
shouldSuppressMessagingToolReplies,
|
||||
} from "./reply-payloads.js";
|
||||
@@ -27,6 +28,7 @@ export function buildReplyPayloads(params: {
|
||||
currentMessageId?: string;
|
||||
messageProvider?: string;
|
||||
messagingToolSentTexts?: string[];
|
||||
messagingToolSentMediaUrls?: string[];
|
||||
messagingToolSentTargets?: Parameters<
|
||||
typeof shouldSuppressMessagingToolReplies
|
||||
>[0]["messagingToolSentTargets"];
|
||||
@@ -93,16 +95,22 @@ export function buildReplyPayloads(params: {
|
||||
payloads: replyTaggedPayloads,
|
||||
sentTexts: messagingToolSentTexts,
|
||||
});
|
||||
const mediaFilteredPayloads = filterMessagingToolMediaDuplicates({
|
||||
payloads: dedupedPayloads,
|
||||
sentMediaUrls: params.messagingToolSentMediaUrls ?? [],
|
||||
});
|
||||
// Filter out payloads already sent via pipeline or directly during tool flush.
|
||||
const filteredPayloads = shouldDropFinalPayloads
|
||||
? []
|
||||
: params.blockStreamingEnabled
|
||||
? dedupedPayloads.filter((payload) => !params.blockReplyPipeline?.hasSentPayload(payload))
|
||||
? mediaFilteredPayloads.filter(
|
||||
(payload) => !params.blockReplyPipeline?.hasSentPayload(payload),
|
||||
)
|
||||
: params.directlySentBlockKeys?.size
|
||||
? dedupedPayloads.filter(
|
||||
? mediaFilteredPayloads.filter(
|
||||
(payload) => !params.directlySentBlockKeys!.has(createBlockReplyPayloadKey(payload)),
|
||||
)
|
||||
: dedupedPayloads;
|
||||
: mediaFilteredPayloads;
|
||||
const replyPayloads = suppressMessagingToolReplies ? [] : filteredPayloads;
|
||||
|
||||
return {
|
||||
|
||||
@@ -444,6 +444,7 @@ export async function runReplyAgent(params: {
|
||||
currentMessageId: sessionCtx.MessageSidFull ?? sessionCtx.MessageSid,
|
||||
messageProvider: followupRun.run.messageProvider,
|
||||
messagingToolSentTexts: runResult.messagingToolSentTexts,
|
||||
messagingToolSentMediaUrls: runResult.messagingToolSentMediaUrls,
|
||||
messagingToolSentTargets: runResult.messagingToolSentTargets,
|
||||
originatingTo: sessionCtx.OriginatingTo ?? sessionCtx.To,
|
||||
accountId: sessionCtx.AccountId,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import {
|
||||
applyReplyThreading,
|
||||
filterMessagingToolDuplicates,
|
||||
filterMessagingToolMediaDuplicates,
|
||||
shouldSuppressMessagingToolReplies,
|
||||
} from "./reply-payloads.js";
|
||||
import { resolveReplyToMode } from "./reply-threading.js";
|
||||
@@ -252,13 +253,17 @@ export function createFollowupRunner(params: {
|
||||
payloads: replyTaggedPayloads,
|
||||
sentTexts: runResult.messagingToolSentTexts ?? [],
|
||||
});
|
||||
const mediaFilteredPayloads = filterMessagingToolMediaDuplicates({
|
||||
payloads: dedupedPayloads,
|
||||
sentMediaUrls: runResult.messagingToolSentMediaUrls ?? [],
|
||||
});
|
||||
const suppressMessagingToolReplies = shouldSuppressMessagingToolReplies({
|
||||
messageProvider: queued.run.messageProvider,
|
||||
messagingToolSentTargets: runResult.messagingToolSentTargets,
|
||||
originatingTo: queued.originatingTo,
|
||||
accountId: queued.run.agentAccountId,
|
||||
});
|
||||
const finalPayloads = suppressMessagingToolReplies ? [] : dedupedPayloads;
|
||||
const finalPayloads = suppressMessagingToolReplies ? [] : mediaFilteredPayloads;
|
||||
|
||||
if (finalPayloads.length === 0) {
|
||||
return;
|
||||
|
||||
61
src/auto-reply/reply/reply-payloads.test.ts
Normal file
61
src/auto-reply/reply/reply-payloads.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterMessagingToolMediaDuplicates } from "./reply-payloads.js";
|
||||
|
||||
describe("filterMessagingToolMediaDuplicates", () => {
|
||||
it("strips mediaUrl when it matches sentMediaUrls", () => {
|
||||
const result = filterMessagingToolMediaDuplicates({
|
||||
payloads: [{ text: "hello", mediaUrl: "file:///tmp/photo.jpg" }],
|
||||
sentMediaUrls: ["file:///tmp/photo.jpg"],
|
||||
});
|
||||
expect(result).toEqual([{ text: "hello", mediaUrl: undefined, mediaUrls: undefined }]);
|
||||
});
|
||||
|
||||
it("preserves mediaUrl when it is not in sentMediaUrls", () => {
|
||||
const result = filterMessagingToolMediaDuplicates({
|
||||
payloads: [{ text: "hello", mediaUrl: "file:///tmp/photo.jpg" }],
|
||||
sentMediaUrls: ["file:///tmp/other.jpg"],
|
||||
});
|
||||
expect(result).toEqual([{ text: "hello", mediaUrl: "file:///tmp/photo.jpg" }]);
|
||||
});
|
||||
|
||||
it("filters matching entries from mediaUrls array", () => {
|
||||
const result = filterMessagingToolMediaDuplicates({
|
||||
payloads: [
|
||||
{
|
||||
text: "gallery",
|
||||
mediaUrls: ["file:///tmp/a.jpg", "file:///tmp/b.jpg", "file:///tmp/c.jpg"],
|
||||
},
|
||||
],
|
||||
sentMediaUrls: ["file:///tmp/b.jpg"],
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{ text: "gallery", mediaUrls: ["file:///tmp/a.jpg", "file:///tmp/c.jpg"] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("clears mediaUrls when all entries match", () => {
|
||||
const result = filterMessagingToolMediaDuplicates({
|
||||
payloads: [{ text: "gallery", mediaUrls: ["file:///tmp/a.jpg"] }],
|
||||
sentMediaUrls: ["file:///tmp/a.jpg"],
|
||||
});
|
||||
expect(result).toEqual([{ text: "gallery", mediaUrl: undefined, mediaUrls: undefined }]);
|
||||
});
|
||||
|
||||
it("returns payloads unchanged when no media present", () => {
|
||||
const payloads = [{ text: "plain text" }];
|
||||
const result = filterMessagingToolMediaDuplicates({
|
||||
payloads,
|
||||
sentMediaUrls: ["file:///tmp/photo.jpg"],
|
||||
});
|
||||
expect(result).toStrictEqual(payloads);
|
||||
});
|
||||
|
||||
it("returns payloads unchanged when sentMediaUrls is empty", () => {
|
||||
const payloads = [{ text: "hello", mediaUrl: "file:///tmp/photo.jpg" }];
|
||||
const result = filterMessagingToolMediaDuplicates({
|
||||
payloads,
|
||||
sentMediaUrls: [],
|
||||
});
|
||||
expect(result).toBe(payloads);
|
||||
});
|
||||
});
|
||||
@@ -95,6 +95,31 @@ export function filterMessagingToolDuplicates(params: {
|
||||
return payloads.filter((payload) => !isMessagingToolDuplicate(payload.text ?? "", sentTexts));
|
||||
}
|
||||
|
||||
export function filterMessagingToolMediaDuplicates(params: {
|
||||
payloads: ReplyPayload[];
|
||||
sentMediaUrls: string[];
|
||||
}): ReplyPayload[] {
|
||||
const { payloads, sentMediaUrls } = params;
|
||||
if (sentMediaUrls.length === 0) {
|
||||
return payloads;
|
||||
}
|
||||
const sentSet = new Set(sentMediaUrls);
|
||||
return payloads.map((payload) => {
|
||||
const mediaUrl = payload.mediaUrl;
|
||||
const mediaUrls = payload.mediaUrls;
|
||||
const stripSingle = mediaUrl && sentSet.has(mediaUrl);
|
||||
const filteredUrls = mediaUrls?.filter((u) => !sentSet.has(u));
|
||||
if (!stripSingle && (!mediaUrls || filteredUrls?.length === mediaUrls.length)) {
|
||||
return payload; // No change
|
||||
}
|
||||
return {
|
||||
...payload,
|
||||
mediaUrl: stripSingle ? undefined : mediaUrl,
|
||||
mediaUrls: filteredUrls?.length ? filteredUrls : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeAccountId(value?: string): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed.toLowerCase() : undefined;
|
||||
|
||||
Reference in New Issue
Block a user