fix: honor default account in conversation bindings

This commit is contained in:
Tak Hoffman
2026-04-03 16:09:36 -05:00
parent 267b6f595c
commit 759598f737
5 changed files with 170 additions and 11 deletions

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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<typeof resolveConversationBindingContext> {
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(

View File

@@ -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",
});
});
});

View File

@@ -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,