fix(feishu): prevent duplicate delivery when message tool uses generic provider (openclaw#31538) thanks @jlgrimes

Verified:
- pnpm exec vitest run src/auto-reply/reply/agent-runner-payloads.test.ts src/auto-reply/reply/followup-runner.test.ts
- pnpm check (fails on unrelated baseline type errors outside PR scope)

Co-authored-by: jlgrimes <8084595+jlgrimes@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Jared Grimes
2026-03-02 07:35:58 -06:00
committed by GitHub
parent 06306501ab
commit aa5d173bec
3 changed files with 54 additions and 4 deletions

View File

@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Feishu/Duplicate replies: suppress same-target reply dispatch when message-tool sends use generic provider metadata (`provider: "message"`) and normalize `lark`/`feishu` provider aliases during duplicate-target checks, preventing double-delivery in Feishu sessions. (#31526)
- Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.
- Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.
- BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.

View File

@@ -86,6 +86,34 @@ describe("buildReplyPayloads media filter integration", () => {
expect(replyPayloads).toHaveLength(0);
});
it("suppresses same-target replies when message tool target provider is generic", () => {
const { replyPayloads } = buildReplyPayloads({
...baseParams,
payloads: [{ text: "hello world!" }],
messageProvider: "heartbeat",
originatingChannel: "feishu",
originatingTo: "ou_abc123",
messagingToolSentTexts: ["different message"],
messagingToolSentTargets: [{ tool: "message", provider: "message", to: "ou_abc123" }],
});
expect(replyPayloads).toHaveLength(0);
});
it("suppresses same-target replies when target provider is channel alias", () => {
const { replyPayloads } = buildReplyPayloads({
...baseParams,
payloads: [{ text: "hello world!" }],
messageProvider: "heartbeat",
originatingChannel: "feishu",
originatingTo: "ou_abc123",
messagingToolSentTexts: ["different message"],
messagingToolSentTargets: [{ tool: "message", provider: "lark", to: "ou_abc123" }],
});
expect(replyPayloads).toHaveLength(0);
});
it("does not suppress same-target replies when accountId differs", () => {
const { replyPayloads } = buildReplyPayloads({
...baseParams,

View File

@@ -1,5 +1,6 @@
import { isMessagingToolDuplicate } from "../../agents/pi-embedded-helpers.js";
import type { MessagingToolSend } from "../../agents/pi-embedded-runner.js";
import { normalizeChannelId } from "../../channels/plugins/index.js";
import type { ReplyToMode } from "../../config/types.js";
import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js";
import { normalizeOptionalAccountId } from "../../routing/account-id.js";
@@ -144,13 +145,30 @@ export function filterMessagingToolMediaDuplicates(params: {
});
}
const PROVIDER_ALIAS_MAP: Record<string, string> = {
lark: "feishu",
};
function normalizeProviderForComparison(value?: string): string | undefined {
const trimmed = value?.trim();
if (!trimmed) {
return undefined;
}
const lowered = trimmed.toLowerCase();
const normalizedChannel = normalizeChannelId(trimmed);
if (normalizedChannel) {
return normalizedChannel;
}
return PROVIDER_ALIAS_MAP[lowered] ?? lowered;
}
export function shouldSuppressMessagingToolReplies(params: {
messageProvider?: string;
messagingToolSentTargets?: MessagingToolSend[];
originatingTo?: string;
accountId?: string;
}): boolean {
const provider = params.messageProvider?.trim().toLowerCase();
const provider = normalizeProviderForComparison(params.messageProvider);
if (!provider) {
return false;
}
@@ -164,13 +182,16 @@ export function shouldSuppressMessagingToolReplies(params: {
return false;
}
return sentTargets.some((target) => {
if (!target?.provider) {
const targetProvider = normalizeProviderForComparison(target?.provider);
if (!targetProvider) {
return false;
}
if (target.provider.trim().toLowerCase() !== provider) {
const isGenericMessageProvider = targetProvider === "message";
if (!isGenericMessageProvider && targetProvider !== provider) {
return false;
}
const targetKey = normalizeTargetForProvider(provider, target.to);
const targetNormalizationProvider = isGenericMessageProvider ? provider : targetProvider;
const targetKey = normalizeTargetForProvider(targetNormalizationProvider, target.to);
if (!targetKey) {
return false;
}