From 8bd0eb54248f3ec45e0342e47ea8b12d2ef22912 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 18:46:48 +0000 Subject: [PATCH] fix(outbound): land #38944 from @Narcooo Co-authored-by: Narcooo --- CHANGELOG.md | 1 + src/infra/outbound/channel-target.ts | 8 ++++++-- .../outbound/message-action-normalization.test.ts | 15 +++++++++++++++ .../outbound/message-action-normalization.ts | 4 +++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 121bb67b185..d24c43d9a04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -245,6 +245,7 @@ Docs: https://docs.openclaw.ai - Security/Nostr: harden profile mutation/import loopback guards by failing closed on non-loopback forwarded client headers (`x-forwarded-for` / `x-real-ip`) and rejecting `sec-fetch-site: cross-site`; adds regression coverage for proxy-forwarded and browser cross-site mutation attempts. - CLI/bootstrap Node version hint maintenance: replace hardcoded nvm `22` instructions in `openclaw.mjs` with `MIN_NODE_MAJOR` interpolation so future minimum-Node bumps keep startup guidance in sync automatically. (#39056) Thanks @onstash. - Discord/native slash command auth: honor `commands.allowFrom.discord` (and `commands.allowFrom["*"]`) in guild slash-command pre-dispatch authorization so allowlisted senders are no longer incorrectly rejected as unauthorized. (#38794) Thanks @jskoiz and @thewilloftheshadow. +- Outbound/message target normalization: ignore empty legacy `to`/`channelId` fields when explicit `target` is provided so valid target-based sends no longer fail legacy-param validation; includes regression coverage. (#38944) Thanks @Narcooo. ## 2026.3.2 diff --git a/src/infra/outbound/channel-target.ts b/src/infra/outbound/channel-target.ts index 21b577e7ca6..c71ffd1e58a 100644 --- a/src/infra/outbound/channel-target.ts +++ b/src/infra/outbound/channel-target.ts @@ -6,13 +6,17 @@ export const CHANNEL_TARGET_DESCRIPTION = export const CHANNEL_TARGETS_DESCRIPTION = "Recipient/channel targets (same format as --target); accepts ids or names when the directory is available."; +function hasNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + export function applyTargetToParams(params: { action: string; args: Record; }): void { const target = typeof params.args.target === "string" ? params.args.target.trim() : ""; - const hasLegacyTo = typeof params.args.to === "string"; - const hasLegacyChannelId = typeof params.args.channelId === "string"; + const hasLegacyTo = hasNonEmptyString(params.args.to); + const hasLegacyChannelId = hasNonEmptyString(params.args.channelId); const mode = MESSAGE_ACTION_TARGET_MODE[params.action as keyof typeof MESSAGE_ACTION_TARGET_MODE] ?? "none"; diff --git a/src/infra/outbound/message-action-normalization.test.ts b/src/infra/outbound/message-action-normalization.test.ts index 8acf557ef38..5f0647b955c 100644 --- a/src/infra/outbound/message-action-normalization.test.ts +++ b/src/infra/outbound/message-action-normalization.test.ts @@ -17,6 +17,21 @@ describe("normalizeMessageActionInput", () => { expect("channelId" in normalized).toBe(false); }); + it("ignores empty-string legacy target fields when explicit target is present", () => { + const normalized = normalizeMessageActionInput({ + action: "send", + args: { + target: "1214056829", + channelId: "", + to: " ", + }, + }); + + expect(normalized.target).toBe("1214056829"); + expect(normalized.to).toBe("1214056829"); + expect("channelId" in normalized).toBe(false); + }); + it("maps legacy target fields into canonical target", () => { const normalized = normalizeMessageActionInput({ action: "send", diff --git a/src/infra/outbound/message-action-normalization.ts b/src/infra/outbound/message-action-normalization.ts index 4047a7e26ee..a4b4f4829bd 100644 --- a/src/infra/outbound/message-action-normalization.ts +++ b/src/infra/outbound/message-action-normalization.ts @@ -19,11 +19,13 @@ export function normalizeMessageActionInput(params: { const explicitTarget = typeof normalizedArgs.target === "string" ? normalizedArgs.target.trim() : ""; + const hasLegacyTargetFields = + typeof normalizedArgs.to === "string" || typeof normalizedArgs.channelId === "string"; const hasLegacyTarget = (typeof normalizedArgs.to === "string" && normalizedArgs.to.trim().length > 0) || (typeof normalizedArgs.channelId === "string" && normalizedArgs.channelId.trim().length > 0); - if (explicitTarget && hasLegacyTarget) { + if (explicitTarget && hasLegacyTargetFields) { delete normalizedArgs.to; delete normalizedArgs.channelId; }