mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 17:48:11 +00:00
Agents: upgrade channel binding to account-scoped binding
This commit is contained in:
@@ -58,6 +58,25 @@ describe("agents bind/unbind commands", () => {
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("upgrades existing channel-only binding when accountId is later provided", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {
|
||||
bindings: [{ agentId: "main", match: { channel: "telegram" } }],
|
||||
},
|
||||
});
|
||||
|
||||
await agentsBindCommand({ bind: ["telegram:work"] }, runtime);
|
||||
|
||||
expect(writeConfigFileMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
bindings: [{ agentId: "main", match: { channel: "telegram", accountId: "work" } }],
|
||||
}),
|
||||
);
|
||||
expect(runtime.log).toHaveBeenCalledWith("Updated bindings:");
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("unbinds all routes for an agent", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
|
||||
@@ -8,6 +8,11 @@ import type { ChannelChoice } from "./onboard-types.js";
|
||||
|
||||
function bindingMatchKey(match: AgentBinding["match"]) {
|
||||
const accountId = match.accountId?.trim() || DEFAULT_ACCOUNT_ID;
|
||||
const identityKey = bindingMatchIdentityKey(match);
|
||||
return [identityKey, accountId].join("|");
|
||||
}
|
||||
|
||||
function bindingMatchIdentityKey(match: AgentBinding["match"]) {
|
||||
const roles = Array.isArray(match.roles)
|
||||
? Array.from(
|
||||
new Set(
|
||||
@@ -20,7 +25,6 @@ function bindingMatchKey(match: AgentBinding["match"]) {
|
||||
: [];
|
||||
return [
|
||||
match.channel,
|
||||
accountId,
|
||||
match.peer?.kind ?? "",
|
||||
match.peer?.id ?? "",
|
||||
match.guildId ?? "",
|
||||
@@ -29,6 +33,26 @@ function bindingMatchKey(match: AgentBinding["match"]) {
|
||||
].join("|");
|
||||
}
|
||||
|
||||
function canUpgradeBindingAccountScope(params: {
|
||||
existing: AgentBinding;
|
||||
incoming: AgentBinding;
|
||||
normalizedIncomingAgentId: string;
|
||||
}): boolean {
|
||||
if (!params.incoming.match.accountId?.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (params.existing.match.accountId?.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (normalizeAgentId(params.existing.agentId) !== params.normalizedIncomingAgentId) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
bindingMatchIdentityKey(params.existing.match) ===
|
||||
bindingMatchIdentityKey(params.incoming.match)
|
||||
);
|
||||
}
|
||||
|
||||
export function describeBinding(binding: AgentBinding) {
|
||||
const match = binding.match;
|
||||
const parts = [match.channel];
|
||||
@@ -53,10 +77,11 @@ export function applyAgentBindings(
|
||||
): {
|
||||
config: OpenClawConfig;
|
||||
added: AgentBinding[];
|
||||
updated: AgentBinding[];
|
||||
skipped: AgentBinding[];
|
||||
conflicts: Array<{ binding: AgentBinding; existingAgentId: string }>;
|
||||
} {
|
||||
const existing = cfg.bindings ?? [];
|
||||
const existing = [...(cfg.bindings ?? [])];
|
||||
const existingMatchMap = new Map<string, string>();
|
||||
for (const binding of existing) {
|
||||
const key = bindingMatchKey(binding.match);
|
||||
@@ -66,6 +91,7 @@ export function applyAgentBindings(
|
||||
}
|
||||
|
||||
const added: AgentBinding[] = [];
|
||||
const updated: AgentBinding[] = [];
|
||||
const skipped: AgentBinding[] = [];
|
||||
const conflicts: Array<{ binding: AgentBinding; existingAgentId: string }> = [];
|
||||
|
||||
@@ -81,12 +107,41 @@ export function applyAgentBindings(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const upgradeIndex = existing.findIndex((candidate) =>
|
||||
canUpgradeBindingAccountScope({
|
||||
existing: candidate,
|
||||
incoming: binding,
|
||||
normalizedIncomingAgentId: agentId,
|
||||
}),
|
||||
);
|
||||
if (upgradeIndex >= 0) {
|
||||
const current = existing[upgradeIndex];
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
const previousKey = bindingMatchKey(current.match);
|
||||
const upgradedBinding: AgentBinding = {
|
||||
...current,
|
||||
agentId,
|
||||
match: {
|
||||
...current.match,
|
||||
accountId: binding.match.accountId?.trim(),
|
||||
},
|
||||
};
|
||||
existing[upgradeIndex] = upgradedBinding;
|
||||
existingMatchMap.delete(previousKey);
|
||||
existingMatchMap.set(bindingMatchKey(upgradedBinding.match), agentId);
|
||||
updated.push(upgradedBinding);
|
||||
continue;
|
||||
}
|
||||
|
||||
existingMatchMap.set(key, agentId);
|
||||
added.push({ ...binding, agentId });
|
||||
}
|
||||
|
||||
if (added.length === 0) {
|
||||
return { config: cfg, added, skipped, conflicts };
|
||||
if (added.length === 0 && updated.length === 0) {
|
||||
return { config: cfg, added, updated, skipped, conflicts };
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -95,6 +150,7 @@ export function applyAgentBindings(
|
||||
bindings: [...existing, ...added],
|
||||
},
|
||||
added,
|
||||
updated,
|
||||
skipped,
|
||||
conflicts,
|
||||
};
|
||||
|
||||
@@ -125,7 +125,7 @@ export async function agentsAddCommand(
|
||||
const bindingResult =
|
||||
bindingParse.bindings.length > 0
|
||||
? applyAgentBindings(nextConfig, bindingParse.bindings)
|
||||
: { config: nextConfig, added: [], skipped: [], conflicts: [] };
|
||||
: { config: nextConfig, added: [], updated: [], skipped: [], conflicts: [] };
|
||||
|
||||
await writeConfigFile(bindingResult.config);
|
||||
if (!opts.json) {
|
||||
@@ -145,6 +145,7 @@ export async function agentsAddCommand(
|
||||
model,
|
||||
bindings: {
|
||||
added: bindingResult.added.map(describeBinding),
|
||||
updated: bindingResult.updated.map(describeBinding),
|
||||
skipped: bindingResult.skipped.map(describeBinding),
|
||||
conflicts: bindingResult.conflicts.map(
|
||||
(conflict) => `${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`,
|
||||
|
||||
@@ -150,7 +150,7 @@ export async function agentsBindCommand(
|
||||
}
|
||||
|
||||
const result = applyAgentBindings(cfg, parsed.bindings);
|
||||
if (result.added.length > 0) {
|
||||
if (result.added.length > 0 || result.updated.length > 0) {
|
||||
await writeConfigFile(result.config);
|
||||
if (!opts.json) {
|
||||
logConfigUpdated(runtime);
|
||||
@@ -160,6 +160,7 @@ export async function agentsBindCommand(
|
||||
const payload = {
|
||||
agentId,
|
||||
added: result.added.map(describeBinding),
|
||||
updated: result.updated.map(describeBinding),
|
||||
skipped: result.skipped.map(describeBinding),
|
||||
conflicts: result.conflicts.map(
|
||||
(conflict) => `${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`,
|
||||
@@ -178,10 +179,17 @@ export async function agentsBindCommand(
|
||||
for (const binding of result.added) {
|
||||
runtime.log(`- ${describeBinding(binding)}`);
|
||||
}
|
||||
} else {
|
||||
} else if (result.updated.length === 0) {
|
||||
runtime.log("No new bindings added.");
|
||||
}
|
||||
|
||||
if (result.updated.length > 0) {
|
||||
runtime.log("Updated bindings:");
|
||||
for (const binding of result.updated) {
|
||||
runtime.log(`- ${describeBinding(binding)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.skipped.length > 0) {
|
||||
runtime.log("Already present:");
|
||||
for (const binding of result.skipped) {
|
||||
|
||||
@@ -112,6 +112,34 @@ describe("agents helpers", () => {
|
||||
expect(result.config.bindings).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("applyAgentBindings upgrades channel-only binding to account-specific binding for same agent", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
bindings: [
|
||||
{
|
||||
agentId: "main",
|
||||
match: { channel: "telegram" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = applyAgentBindings(cfg, [
|
||||
{
|
||||
agentId: "main",
|
||||
match: { channel: "telegram", accountId: "work" },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.updated).toHaveLength(1);
|
||||
expect(result.conflicts).toHaveLength(0);
|
||||
expect(result.config.bindings).toEqual([
|
||||
{
|
||||
agentId: "main",
|
||||
match: { channel: "telegram", accountId: "work" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("applyAgentBindings treats role-based bindings as distinct routes", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
bindings: [
|
||||
|
||||
Reference in New Issue
Block a user