diff --git a/src/auto-reply/reply/commands-acp/context.test.ts b/src/auto-reply/reply/commands-acp/context.test.ts index 7fc9093b0ad..ad591588b30 100644 --- a/src/auto-reply/reply/commands-acp/context.test.ts +++ b/src/auto-reply/reply/commands-acp/context.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; import { __testing as sessionBindingTesting, @@ -425,6 +425,10 @@ describe("commands-acp context", () => { sessionBindingTesting.resetSessionBindingAdaptersForTests(); }); + afterEach(() => { + setMinimalAcpContextRegistryForTests(); + }); + it("resolves channel/account/thread context from originating fields", () => { const params = buildCommandTestParams("/acp sessions", baseCfg, { Provider: "discord", @@ -501,6 +505,58 @@ describe("commands-acp context", () => { expect(resolveAcpCommandConversationId(params)).toBe("123456789"); }); + it("uses the plugin default account when ACP context omits AccountId", () => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "line", + source: "test", + plugin: { + ...createChannelTestPluginBase({ + id: "line", + label: "LINE", + config: { + listAccountIds: () => ["default", "work"], + defaultAccountId: () => "work", + }, + }), + bindings: { + resolveCommandConversation: ({ + originatingTo, + commandTo, + fallbackTo, + }: { + originatingTo?: string; + commandTo?: string; + fallbackTo?: string; + }) => { + const conversationId = + parseLineConversationIdFromTargetForTest(originatingTo) ?? + parseLineConversationIdFromTargetForTest(commandTo) ?? + parseLineConversationIdFromTargetForTest(fallbackTo); + return conversationId ? { conversationId } : null; + }, + }, + }, + }, + ]), + ); + + const params = buildCommandTestParams("/acp status", baseCfg, { + Provider: "line", + Surface: "line", + OriginatingChannel: "line", + OriginatingTo: "line:user:U1234567890abcdef1234567890abcdef", + }); + + expect(resolveAcpCommandBindingContext(params)).toEqual({ + channel: "line", + accountId: "work", + threadId: undefined, + conversationId: "U1234567890abcdef1234567890abcdef", + }); + }); + it("builds canonical telegram topic conversation ids from originating chat + thread", () => { const params = buildCommandTestParams("/acp status", baseCfg, { Provider: "telegram", diff --git a/src/auto-reply/reply/commands-acp/context.ts b/src/auto-reply/reply/commands-acp/context.ts index e1cdb5de7f5..c1a1b2af843 100644 --- a/src/auto-reply/reply/commands-acp/context.ts +++ b/src/auto-reply/reply/commands-acp/context.ts @@ -13,7 +13,11 @@ export function resolveAcpCommandChannel(params: HandleCommandsParams): string { } export function resolveAcpCommandAccountId(params: HandleCommandsParams): string { - return resolveConversationBindingAccountIdFromMessage(params.ctx); + return resolveConversationBindingAccountIdFromMessage({ + ctx: params.ctx, + cfg: params.cfg, + commandChannel: params.command.channel, + }); } export function resolveAcpCommandThreadId(params: HandleCommandsParams): string | undefined { diff --git a/src/auto-reply/reply/conversation-binding-input.ts b/src/auto-reply/reply/conversation-binding-input.ts index f438f9281d1..1af631c0907 100644 --- a/src/auto-reply/reply/conversation-binding-input.ts +++ b/src/auto-reply/reply/conversation-binding-input.ts @@ -1,6 +1,7 @@ import { normalizeConversationText } from "../../acp/conversation-id.js"; import { resolveConversationBindingContext } from "../../channels/conversation-binding-context.js"; import type { OpenClawConfig } from "../../config/config.js"; +import { getActivePluginChannelRegistry } from "../../plugins/runtime.js"; import type { MsgContext } from "../templating.js"; import type { HandleCommandsParams } from "./commands-types.js"; @@ -27,9 +28,21 @@ function resolveBindingChannel(ctx: BindingMsgContext, commandChannel?: string | return normalizeConversationText(raw).toLowerCase(); } -function resolveBindingAccountId(ctx: BindingMsgContext): string { - const accountId = normalizeConversationText(ctx.AccountId); - return accountId || "default"; +function resolveBindingAccountId(params: { + ctx: BindingMsgContext; + cfg: OpenClawConfig; + commandChannel?: string | null; +}): string { + const channel = resolveBindingChannel(params.ctx, params.commandChannel); + const plugin = getActivePluginChannelRegistry()?.channels.find( + (entry) => entry.plugin.id === channel, + )?.plugin; + const accountId = normalizeConversationText(params.ctx.AccountId); + return ( + accountId || + normalizeConversationText(plugin?.config.defaultAccountId?.(params.cfg)) || + "default" + ); } function resolveBindingThreadId(threadId: string | number | null | undefined): string | undefined { @@ -45,10 +58,15 @@ export function resolveConversationBindingContextFromMessage(params: { parentSessionKey?: string | null; commandTo?: string | null; }): ReturnType { + const channel = resolveBindingChannel(params.ctx); return resolveConversationBindingContext({ cfg: params.cfg, - channel: resolveBindingChannel(params.ctx), - accountId: resolveBindingAccountId(params.ctx), + channel, + accountId: resolveBindingAccountId({ + ctx: params.ctx, + cfg: params.cfg, + commandChannel: channel, + }), chatType: params.ctx.ChatType, threadId: resolveBindingThreadId(params.ctx.MessageThreadId), threadParentId: params.ctx.ThreadParentId, @@ -83,8 +101,12 @@ export function resolveConversationBindingChannelFromMessage( return resolveBindingChannel(ctx, commandChannel); } -export function resolveConversationBindingAccountIdFromMessage(ctx: BindingMsgContext): string { - return resolveBindingAccountId(ctx); +export function resolveConversationBindingAccountIdFromMessage(params: { + ctx: BindingMsgContext; + cfg: OpenClawConfig; + commandChannel?: string | null; +}): string { + return resolveBindingAccountId(params); } export function resolveConversationBindingThreadIdFromMessage( diff --git a/src/channels/conversation-binding-context.test.ts b/src/channels/conversation-binding-context.test.ts new file mode 100644 index 00000000000..f5297b28cae --- /dev/null +++ b/src/channels/conversation-binding-context.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; +import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js"; +import { resolveConversationBindingContext } from "./conversation-binding-context.js"; + +describe("resolveConversationBindingContext", () => { + afterEach(() => { + setActivePluginRegistry(createTestRegistry()); + }); + + it("uses the plugin default account when accountId is omitted", () => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "line", + source: "test", + plugin: { + ...createChannelTestPluginBase({ + id: "line", + label: "LINE", + config: { + listAccountIds: () => ["default", "work"], + defaultAccountId: () => "work", + }, + }), + bindings: { + resolveCommandConversation: ({ + originatingTo, + commandTo, + fallbackTo, + }: { + originatingTo?: string; + commandTo?: string; + fallbackTo?: string; + }) => { + const conversationId = [originatingTo, commandTo, fallbackTo] + .map((candidate) => candidate?.trim().replace(/^line:/i, "")) + .map((candidate) => candidate?.replace(/^user:/i, "")) + .find((candidate) => candidate && candidate.length > 0); + return conversationId ? { conversationId } : null; + }, + }, + }, + }, + ]), + ); + + expect( + resolveConversationBindingContext({ + cfg: {} as OpenClawConfig, + channel: "line", + originatingTo: "line:user:U1234567890abcdef1234567890abcdef", + }), + ).toEqual({ + channel: "line", + accountId: "work", + conversationId: "U1234567890abcdef1234567890abcdef", + }); + }); +}); diff --git a/src/channels/conversation-binding-context.ts b/src/channels/conversation-binding-context.ts index 8f7d5df2cad..7eb74caca7c 100644 --- a/src/channels/conversation-binding-context.ts +++ b/src/channels/conversation-binding-context.ts @@ -59,6 +59,18 @@ function shouldDefaultParentConversationToSelf(plugin?: ChannelPlugin): boolean return plugin?.bindings?.selfParentConversationByDefault === true; } +function resolveBindingAccountId(params: { + rawAccountId?: string | null; + plugin?: ChannelPlugin; + cfg: OpenClawConfig; +}): string { + return ( + normalizeText(params.rawAccountId) || + normalizeText(params.plugin?.config.defaultAccountId?.(params.cfg)) || + "default" + ); +} + function resolveChannelTargetId(params: { channel: string; target?: string | null; @@ -124,9 +136,13 @@ export function resolveConversationBindingContext( if (!channel) { return null; } - const accountId = normalizeText(params.accountId) || "default"; - const threadId = normalizeText(params.threadId != null ? String(params.threadId) : undefined); const loadedPlugin = getLoadedChannelPlugin(channel); + const accountId = resolveBindingAccountId({ + rawAccountId: params.accountId, + plugin: loadedPlugin, + cfg: params.cfg, + }); + const threadId = normalizeText(params.threadId != null ? String(params.threadId) : undefined); const resolvedByProvider = loadedPlugin?.bindings?.resolveCommandConversation?.({ accountId,