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;
|
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>> {
|
async function listExistingAgentIdsOnDisk(): Promise<Set<string>> {
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
const agentsDir = join(getOpenClawConfigDir(), 'agents');
|
const agentsDir = join(getOpenClawConfigDir(), 'agents');
|
||||||
@@ -477,7 +453,7 @@ async function buildSnapshotFromConfig(config: AgentConfigDocument): Promise<Age
|
|||||||
accountToAgent.get(`${channelType}:${accountId}`)
|
accountToAgent.get(`${channelType}:${accountId}`)
|
||||||
|| (
|
|| (
|
||||||
accountId === DEFAULT_ACCOUNT_ID && !hasExplicitAccountBindingForChannel
|
accountId === DEFAULT_ACCOUNT_ID && !hasExplicitAccountBindingForChannel
|
||||||
? (channelToAgent.get(channelType) || defaultAgentIdNorm)
|
? channelToAgent.get(channelType)
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -672,7 +648,6 @@ export async function assignChannelToAgent(agentId: string, channelType: string)
|
|||||||
throw new Error(`Agent "${agentId}" not found`);
|
throw new Error(`Agent "${agentId}" not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertAgentNotBoundToOtherChannel(config.bindings, agentId, channelType);
|
|
||||||
const accountId = resolveAccountIdForAgent(agentId);
|
const accountId = resolveAccountIdForAgent(agentId);
|
||||||
config.bindings = upsertBindingsForChannel(config.bindings, channelType, agentId, accountId);
|
config.bindings = upsertBindingsForChannel(config.bindings, channelType, agentId, accountId);
|
||||||
await writeOpenClawConfig(config);
|
await writeOpenClawConfig(config);
|
||||||
@@ -696,7 +671,6 @@ export async function assignChannelAccountToAgent(
|
|||||||
throw new Error('accountId is required');
|
throw new Error('accountId is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
assertAgentNotBoundToOtherChannel(config.bindings, agentId, channelType);
|
|
||||||
config.bindings = upsertBindingsForChannel(config.bindings, channelType, agentId, accountId.trim());
|
config.bindings = upsertBindingsForChannel(config.bindings, channelType, agentId, accountId.trim());
|
||||||
await writeOpenClawConfig(config);
|
await writeOpenClawConfig(config);
|
||||||
logger.info('Assigned channel account to agent', { agentId, channelType, accountId: accountId.trim() });
|
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();
|
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