fix(binding): allow one agent to bind multiple channels while preserving per-channel account replacement (#541)
This commit is contained in:
@@ -297,30 +297,6 @@ function upsertBindingsForChannel(
|
||||
return nextBindings.length > 0 ? nextBindings : undefined;
|
||||
}
|
||||
|
||||
function assertAgentNotBoundToOtherChannel(
|
||||
bindings: unknown,
|
||||
agentId: string,
|
||||
nextChannelType: string,
|
||||
): void {
|
||||
if (!Array.isArray(bindings)) return;
|
||||
const normalizedAgentId = normalizeAgentIdForBinding(agentId);
|
||||
if (!normalizedAgentId) return;
|
||||
|
||||
const conflictChannels = new Set<string>();
|
||||
for (const binding of bindings) {
|
||||
if (!isChannelBinding(binding)) continue;
|
||||
if (normalizeAgentIdForBinding(binding.agentId || '') !== normalizedAgentId) continue;
|
||||
const boundChannel = binding.match?.channel;
|
||||
if (!boundChannel || boundChannel === nextChannelType) continue;
|
||||
conflictChannels.add(boundChannel);
|
||||
}
|
||||
|
||||
if (conflictChannels.size > 0) {
|
||||
const channels = Array.from(conflictChannels).sort().join(', ');
|
||||
throw new Error(`Agent "${agentId}" is already bound to channel(s): ${channels}. One agent can only bind one channel.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function listExistingAgentIdsOnDisk(): Promise<Set<string>> {
|
||||
const ids = new Set<string>();
|
||||
const agentsDir = join(getOpenClawConfigDir(), 'agents');
|
||||
@@ -477,7 +453,7 @@ async function buildSnapshotFromConfig(config: AgentConfigDocument): Promise<Age
|
||||
accountToAgent.get(`${channelType}:${accountId}`)
|
||||
|| (
|
||||
accountId === DEFAULT_ACCOUNT_ID && !hasExplicitAccountBindingForChannel
|
||||
? (channelToAgent.get(channelType) || defaultAgentIdNorm)
|
||||
? channelToAgent.get(channelType)
|
||||
: undefined
|
||||
);
|
||||
|
||||
@@ -672,7 +648,6 @@ export async function assignChannelToAgent(agentId: string, channelType: string)
|
||||
throw new Error(`Agent "${agentId}" not found`);
|
||||
}
|
||||
|
||||
assertAgentNotBoundToOtherChannel(config.bindings, agentId, channelType);
|
||||
const accountId = resolveAccountIdForAgent(agentId);
|
||||
config.bindings = upsertBindingsForChannel(config.bindings, channelType, agentId, accountId);
|
||||
await writeOpenClawConfig(config);
|
||||
@@ -696,7 +671,6 @@ export async function assignChannelAccountToAgent(
|
||||
throw new Error('accountId is required');
|
||||
}
|
||||
|
||||
assertAgentNotBoundToOtherChannel(config.bindings, agentId, channelType);
|
||||
config.bindings = upsertBindingsForChannel(config.bindings, channelType, agentId, accountId.trim());
|
||||
await writeOpenClawConfig(config);
|
||||
logger.info('Assigned channel account to agent', { agentId, channelType, accountId: accountId.trim() });
|
||||
|
||||
@@ -260,4 +260,107 @@ describe('agent config lifecycle', () => {
|
||||
};
|
||||
expect(feishu.accounts?.test2).toBeDefined();
|
||||
});
|
||||
|
||||
it('allows the same agent to bind multiple different channels', async () => {
|
||||
await writeOpenClawJson({
|
||||
agents: {
|
||||
list: [
|
||||
{ id: 'main', name: 'Main', default: true },
|
||||
],
|
||||
},
|
||||
channels: {
|
||||
feishu: { enabled: true },
|
||||
telegram: { enabled: true },
|
||||
},
|
||||
});
|
||||
|
||||
const { assignChannelAccountToAgent, listAgentsSnapshot } = await import('@electron/utils/agent-config');
|
||||
|
||||
await assignChannelAccountToAgent('main', 'feishu', 'default');
|
||||
await assignChannelAccountToAgent('main', 'telegram', 'default');
|
||||
|
||||
const snapshot = await listAgentsSnapshot();
|
||||
expect(snapshot.channelAccountOwners['feishu:default']).toBe('main');
|
||||
expect(snapshot.channelAccountOwners['telegram:default']).toBe('main');
|
||||
});
|
||||
|
||||
it('replaces previous account binding for the same agent and channel', async () => {
|
||||
await writeOpenClawJson({
|
||||
agents: {
|
||||
list: [
|
||||
{ id: 'main', name: 'Main', default: true },
|
||||
],
|
||||
},
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
defaultAccount: 'default',
|
||||
accounts: {
|
||||
default: { enabled: true, appId: 'main-app' },
|
||||
alt: { enabled: true, appId: 'alt-app' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { assignChannelAccountToAgent, listAgentsSnapshot } = await import('@electron/utils/agent-config');
|
||||
|
||||
await assignChannelAccountToAgent('main', 'feishu', 'default');
|
||||
await assignChannelAccountToAgent('main', 'feishu', 'alt');
|
||||
|
||||
const snapshot = await listAgentsSnapshot();
|
||||
expect(snapshot.channelAccountOwners['feishu:default']).toBeUndefined();
|
||||
expect(snapshot.channelAccountOwners['feishu:alt']).toBe('main');
|
||||
});
|
||||
|
||||
it('keeps a single owner for the same channel account', async () => {
|
||||
await writeOpenClawJson({
|
||||
agents: {
|
||||
list: [
|
||||
{ id: 'main', name: 'Main', default: true },
|
||||
{ id: 'test2', name: 'test2' },
|
||||
],
|
||||
},
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
default: { enabled: true, appId: 'main-app' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { assignChannelAccountToAgent, listAgentsSnapshot } = await import('@electron/utils/agent-config');
|
||||
|
||||
await assignChannelAccountToAgent('main', 'feishu', 'default');
|
||||
await assignChannelAccountToAgent('test2', 'feishu', 'default');
|
||||
|
||||
const snapshot = await listAgentsSnapshot();
|
||||
expect(snapshot.channelAccountOwners['feishu:default']).toBe('test2');
|
||||
});
|
||||
|
||||
it('can clear one channel account binding without affecting another channel on the same agent', async () => {
|
||||
await writeOpenClawJson({
|
||||
agents: {
|
||||
list: [
|
||||
{ id: 'main', name: 'Main', default: true },
|
||||
],
|
||||
},
|
||||
channels: {
|
||||
feishu: { enabled: true },
|
||||
telegram: { enabled: true },
|
||||
},
|
||||
});
|
||||
|
||||
const { assignChannelAccountToAgent, clearChannelBinding, listAgentsSnapshot } = await import('@electron/utils/agent-config');
|
||||
|
||||
await assignChannelAccountToAgent('main', 'feishu', 'default');
|
||||
await assignChannelAccountToAgent('main', 'telegram', 'default');
|
||||
await clearChannelBinding('feishu', 'default');
|
||||
|
||||
const snapshot = await listAgentsSnapshot();
|
||||
expect(snapshot.channelAccountOwners['feishu:default']).toBeUndefined();
|
||||
expect(snapshot.channelAccountOwners['telegram:default']).toBe('main');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user