Skip to content

Commit aa5d173

Browse files
jlgrimesTakhoffman
andauthored
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 <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent 0630650 commit aa5d173

File tree

3 files changed

+54
-4
lines changed

3 files changed

+54
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
4141

4242
### Fixes
4343

44+
- 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)
4445
- 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.
4546
- 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.
4647
- 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.

src/auto-reply/reply/agent-runner-payloads.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,34 @@ describe("buildReplyPayloads media filter integration", () => {
8686
expect(replyPayloads).toHaveLength(0);
8787
});
8888

89+
it("suppresses same-target replies when message tool target provider is generic", () => {
90+
const { replyPayloads } = buildReplyPayloads({
91+
...baseParams,
92+
payloads: [{ text: "hello world!" }],
93+
messageProvider: "heartbeat",
94+
originatingChannel: "feishu",
95+
originatingTo: "ou_abc123",
96+
messagingToolSentTexts: ["different message"],
97+
messagingToolSentTargets: [{ tool: "message", provider: "message", to: "ou_abc123" }],
98+
});
99+
100+
expect(replyPayloads).toHaveLength(0);
101+
});
102+
103+
it("suppresses same-target replies when target provider is channel alias", () => {
104+
const { replyPayloads } = buildReplyPayloads({
105+
...baseParams,
106+
payloads: [{ text: "hello world!" }],
107+
messageProvider: "heartbeat",
108+
originatingChannel: "feishu",
109+
originatingTo: "ou_abc123",
110+
messagingToolSentTexts: ["different message"],
111+
messagingToolSentTargets: [{ tool: "message", provider: "lark", to: "ou_abc123" }],
112+
});
113+
114+
expect(replyPayloads).toHaveLength(0);
115+
});
116+
89117
it("does not suppress same-target replies when accountId differs", () => {
90118
const { replyPayloads } = buildReplyPayloads({
91119
...baseParams,

src/auto-reply/reply/reply-payloads.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isMessagingToolDuplicate } from "../../agents/pi-embedded-helpers.js";
22
import type { MessagingToolSend } from "../../agents/pi-embedded-runner.js";
3+
import { normalizeChannelId } from "../../channels/plugins/index.js";
34
import type { ReplyToMode } from "../../config/types.js";
45
import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js";
56
import { normalizeOptionalAccountId } from "../../routing/account-id.js";
@@ -144,13 +145,30 @@ export function filterMessagingToolMediaDuplicates(params: {
144145
});
145146
}
146147

148+
const PROVIDER_ALIAS_MAP: Record<string, string> = {
149+
lark: "feishu",
150+
};
151+
152+
function normalizeProviderForComparison(value?: string): string | undefined {
153+
const trimmed = value?.trim();
154+
if (!trimmed) {
155+
return undefined;
156+
}
157+
const lowered = trimmed.toLowerCase();
158+
const normalizedChannel = normalizeChannelId(trimmed);
159+
if (normalizedChannel) {
160+
return normalizedChannel;
161+
}
162+
return PROVIDER_ALIAS_MAP[lowered] ?? lowered;
163+
}
164+
147165
export function shouldSuppressMessagingToolReplies(params: {
148166
messageProvider?: string;
149167
messagingToolSentTargets?: MessagingToolSend[];
150168
originatingTo?: string;
151169
accountId?: string;
152170
}): boolean {
153-
const provider = params.messageProvider?.trim().toLowerCase();
171+
const provider = normalizeProviderForComparison(params.messageProvider);
154172
if (!provider) {
155173
return false;
156174
}
@@ -164,13 +182,16 @@ export function shouldSuppressMessagingToolReplies(params: {
164182
return false;
165183
}
166184
return sentTargets.some((target) => {
167-
if (!target?.provider) {
185+
const targetProvider = normalizeProviderForComparison(target?.provider);
186+
if (!targetProvider) {
168187
return false;
169188
}
170-
if (target.provider.trim().toLowerCase() !== provider) {
189+
const isGenericMessageProvider = targetProvider === "message";
190+
if (!isGenericMessageProvider && targetProvider !== provider) {
171191
return false;
172192
}
173-
const targetKey = normalizeTargetForProvider(provider, target.to);
193+
const targetNormalizationProvider = isGenericMessageProvider ? provider : targetProvider;
194+
const targetKey = normalizeTargetForProvider(targetNormalizationProvider, target.to);
174195
if (!targetKey) {
175196
return false;
176197
}

0 commit comments

Comments
 (0)