mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user