diff --git a/CHANGELOG.md b/CHANGELOG.md index 807f0465127..408814fbffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai - Slack/delivery: preserve Slack Web API missing-scope details in outbound delivery errors, so queued retry state identifies the OAuth scope to add. Fixes #62391. Thanks @alexey-pelykh. - Slack/DMs: send text/block-only proactive DMs directly with `chat.postMessage(channel=)` while keeping conversation resolution for uploads and threaded sends. Fixes #62042. Thanks @MarkMolina. - Slack/routing: match route bindings written with Slack target syntax such as `channel:C...`, `user:U...`, or `<@U...>`, so bound Slack peers route to the configured agent instead of `main`. Fixes #41608. Thanks @Winnsolutionsadmin. +- Slack/message actions: prefer the account bound to the outbound target peer before falling back to the agent's first channel account, so multi-workspace sends use the intended Slack account. Supersedes #66807. Thanks @rijhsinghani. - Slack/delivery: retry Slack Web API writes only when the SDK wraps a DNS request failure such as `EAI_AGAIN`, so transient resolver hiccups can recover without retrying platform errors that may duplicate messages. Fixes #68789. Thanks @sonnyb9. - Slack/message actions: forward agent-scoped media roots through the bundled upload-file action path, so workspace files can be attached without failing the local-media guard. Fixes #64625. Thanks @benpchandler. - Slack/mentions: resolve `` user-group mentions through Slack `usergroups.users.list` and treat them as explicit mentions only when the bot user is a member, so mention-gated agent channels wake for real user-group mentions without config-only allowlists. Fixes #73827. Thanks @CG-Intelligence-Agent-Jack. diff --git a/src/infra/outbound/message-action-runner.plugin-dispatch.test.ts b/src/infra/outbound/message-action-runner.plugin-dispatch.test.ts index 265d33050f3..edf83823d21 100644 --- a/src/infra/outbound/message-action-runner.plugin-dispatch.test.ts +++ b/src/infra/outbound/message-action-runner.plugin-dispatch.test.ts @@ -1320,13 +1320,45 @@ describe("runMessageAction plugin dispatch", () => { }, expectedAccountId: "account-b", }, + { + name: "prefers the account bound to the target peer", + args: { + cfg: { + bindings: [ + { + agentId: "agent-b", + match: { + channel: "accountchat", + accountId: "wrong-peer", + peer: { kind: "channel", id: "C_OTHER" }, + }, + }, + { + agentId: "agent-b", + match: { + channel: "accountchat", + accountId: "account-peer", + peer: { kind: "channel", id: "C_TARGET" }, + }, + }, + { + agentId: "agent-b", + match: { channel: "accountchat", accountId: "agent-fallback" }, + }, + ], + } as OpenClawConfig, + agentId: "agent-b", + target: "channel:C_TARGET", + }, + expectedAccountId: "account-peer", + }, ])("$name", async ({ args, expectedAccountId }) => { await runMessageAction({ ...args, action: "send", params: { channel: "accountchat", - target: "channel:123", + target: "target" in args ? args.target : "channel:123", message: "hi", }, }); diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 5383827aa4c..144d5d09dff 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -6,6 +6,7 @@ import { readStringParam, } from "../../agents/tools/common.js"; import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js"; +import { normalizeChatType, type ChatType } from "../../channels/chat-type.js"; import { getChannelPlugin } from "../../channels/plugins/index.js"; import { dispatchChannelMessageAction } from "../../channels/plugins/message-action-dispatch.js"; import type { @@ -25,8 +26,7 @@ import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js"; import { resolveAgentScopedOutboundMediaAccess } from "../../media/read-capability.js"; import { hasPollCreationParams } from "../../poll-params.js"; import { resolvePollMaxSelections } from "../../polls.js"; -import { buildChannelAccountBindings } from "../../routing/bindings.js"; -import { normalizeAgentId } from "../../routing/session-key.js"; +import { resolveFirstBoundAccountId } from "../../routing/bound-account-read.js"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -72,6 +72,7 @@ import { } from "./outbound-policy.js"; import { executePollAction, executeSendAction } from "./outbound-send-service.js"; import { ensureOutboundSessionEntry, resolveOutboundSessionRoute } from "./outbound-session.js"; +import { normalizeTargetForProvider } from "./target-normalization.js"; import { resolveChannelTarget, type ResolvedMessagingTarget } from "./target-resolver.js"; import { extractToolPayload } from "./tool-payload.js"; @@ -286,6 +287,80 @@ async function resolveChannel( return selection.channel; } +function addCandidateAndUnprefixedAlias(candidates: Set, value?: string | null) { + const normalized = normalizeOptionalString(value); + if (!normalized) { + return; + } + candidates.add(normalized); + const unprefixed = normalized.replace(/^(channel|group|user):/i, "").trim(); + if (unprefixed && unprefixed !== normalized) { + candidates.add(unprefixed); + } +} + +function normalizeTargetForAccountBinding(channel: ChannelId, target: string): string | undefined { + try { + return normalizeTargetForProvider(channel, target); + } catch { + return undefined; + } +} + +function inferPeerKindForAccountBinding(channel: ChannelId, target: string): ChatType | undefined { + const inferred = normalizeChatType( + getChannelPlugin(channel)?.messaging?.inferTargetChatType?.({ to: target }), + ); + if (inferred) { + return inferred; + } + const normalized = normalizeTargetForAccountBinding(channel, target); + const candidates = [target, normalized].filter((value): value is string => Boolean(value)); + if (candidates.some((value) => /^user:/i.test(value))) { + return "direct"; + } + if (candidates.some((value) => /^(channel|group):/i.test(value))) { + return "channel"; + } + return undefined; +} + +function resolveTargetBoundAccountId(params: { + cfg: OpenClawConfig; + channel: ChannelId; + args: Record; + agentId?: string; +}): string | undefined { + if (!params.agentId) { + return undefined; + } + const target = + normalizeOptionalString(params.args.to) ?? normalizeOptionalString(params.args.channelId) ?? ""; + if (!target) { + return resolveFirstBoundAccountId({ + cfg: params.cfg, + channelId: params.channel, + agentId: params.agentId, + }); + } + + const candidates = new Set(); + addCandidateAndUnprefixedAlias(candidates, target); + addCandidateAndUnprefixedAlias( + candidates, + normalizeTargetForAccountBinding(params.channel, target), + ); + const [peerId, ...exactPeerIdAliases] = Array.from(candidates); + return resolveFirstBoundAccountId({ + cfg: params.cfg, + channelId: params.channel, + agentId: params.agentId, + peerId, + exactPeerIdAliases, + peerKind: inferPeerKindForAccountBinding(params.channel, target), + }); +} + async function resolveActionTarget(params: { cfg: OpenClawConfig; channel: ChannelId; @@ -941,11 +1016,12 @@ export async function runMessageAction( const channel = await resolveChannel(cfg, params, input.toolContext); let accountId = readStringParam(params, "accountId") ?? input.defaultAccountId; if (!accountId && resolvedAgentId) { - const byAgent = buildChannelAccountBindings(cfg).get(channel); - const boundAccountIds = byAgent?.get(normalizeAgentId(resolvedAgentId)); - if (boundAccountIds && boundAccountIds.length > 0) { - accountId = boundAccountIds[0]; - } + accountId = resolveTargetBoundAccountId({ + cfg, + channel, + args: params, + agentId: resolvedAgentId, + }); } if (accountId) { params.accountId = accountId;