mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 03:54:32 +00:00
Agents: add account-scoped bind and routing commands (#27195)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: ad35a458a5
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
c5d040bbea
commit
96c7702526
@@ -8,16 +8,51 @@ 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(
|
||||
match.roles
|
||||
.map((role) => role.trim())
|
||||
.filter(Boolean)
|
||||
.toSorted(),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
return [
|
||||
match.channel,
|
||||
accountId,
|
||||
match.peer?.kind ?? "",
|
||||
match.peer?.id ?? "",
|
||||
match.guildId ?? "",
|
||||
match.teamId ?? "",
|
||||
roles.join(","),
|
||||
].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];
|
||||
@@ -42,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);
|
||||
@@ -55,6 +91,7 @@ export function applyAgentBindings(
|
||||
}
|
||||
|
||||
const added: AgentBinding[] = [];
|
||||
const updated: AgentBinding[] = [];
|
||||
const skipped: AgentBinding[] = [];
|
||||
const conflicts: Array<{ binding: AgentBinding; existingAgentId: string }> = [];
|
||||
|
||||
@@ -70,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 {
|
||||
@@ -84,11 +150,78 @@ export function applyAgentBindings(
|
||||
bindings: [...existing, ...added],
|
||||
},
|
||||
added,
|
||||
updated,
|
||||
skipped,
|
||||
conflicts,
|
||||
};
|
||||
}
|
||||
|
||||
export function removeAgentBindings(
|
||||
cfg: OpenClawConfig,
|
||||
bindings: AgentBinding[],
|
||||
): {
|
||||
config: OpenClawConfig;
|
||||
removed: AgentBinding[];
|
||||
missing: AgentBinding[];
|
||||
conflicts: Array<{ binding: AgentBinding; existingAgentId: string }>;
|
||||
} {
|
||||
const existing = cfg.bindings ?? [];
|
||||
const removeIndexes = new Set<number>();
|
||||
const removed: AgentBinding[] = [];
|
||||
const missing: AgentBinding[] = [];
|
||||
const conflicts: Array<{ binding: AgentBinding; existingAgentId: string }> = [];
|
||||
|
||||
for (const binding of bindings) {
|
||||
const desiredAgentId = normalizeAgentId(binding.agentId);
|
||||
const key = bindingMatchKey(binding.match);
|
||||
let matchedIndex = -1;
|
||||
let conflictingAgentId: string | null = null;
|
||||
for (let i = 0; i < existing.length; i += 1) {
|
||||
if (removeIndexes.has(i)) {
|
||||
continue;
|
||||
}
|
||||
const current = existing[i];
|
||||
if (!current || bindingMatchKey(current.match) !== key) {
|
||||
continue;
|
||||
}
|
||||
const currentAgentId = normalizeAgentId(current.agentId);
|
||||
if (currentAgentId === desiredAgentId) {
|
||||
matchedIndex = i;
|
||||
break;
|
||||
}
|
||||
conflictingAgentId = currentAgentId;
|
||||
}
|
||||
if (matchedIndex >= 0) {
|
||||
const matched = existing[matchedIndex];
|
||||
if (matched) {
|
||||
removeIndexes.add(matchedIndex);
|
||||
removed.push(matched);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (conflictingAgentId) {
|
||||
conflicts.push({ binding, existingAgentId: conflictingAgentId });
|
||||
continue;
|
||||
}
|
||||
missing.push(binding);
|
||||
}
|
||||
|
||||
if (removeIndexes.size === 0) {
|
||||
return { config: cfg, removed, missing, conflicts };
|
||||
}
|
||||
|
||||
const nextBindings = existing.filter((_, index) => !removeIndexes.has(index));
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
bindings: nextBindings.length > 0 ? nextBindings : undefined,
|
||||
},
|
||||
removed,
|
||||
missing,
|
||||
conflicts,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDefaultAccountId(cfg: OpenClawConfig, provider: ChannelId): string {
|
||||
const plugin = getChannelPlugin(provider);
|
||||
if (!plugin) {
|
||||
@@ -97,6 +230,33 @@ function resolveDefaultAccountId(cfg: OpenClawConfig, provider: ChannelId): stri
|
||||
return resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
}
|
||||
|
||||
function resolveBindingAccountId(params: {
|
||||
channel: ChannelId;
|
||||
config: OpenClawConfig;
|
||||
agentId: string;
|
||||
explicitAccountId?: string;
|
||||
}): string | undefined {
|
||||
const explicitAccountId = params.explicitAccountId?.trim();
|
||||
if (explicitAccountId) {
|
||||
return explicitAccountId;
|
||||
}
|
||||
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
const pluginAccountId = plugin?.setup?.resolveBindingAccountId?.({
|
||||
cfg: params.config,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
if (pluginAccountId?.trim()) {
|
||||
return pluginAccountId.trim();
|
||||
}
|
||||
|
||||
if (plugin?.meta.forceAccountBinding) {
|
||||
return resolveDefaultAccountId(params.config, params.channel);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function buildChannelBindings(params: {
|
||||
agentId: string;
|
||||
selection: ChannelChoice[];
|
||||
@@ -107,14 +267,14 @@ export function buildChannelBindings(params: {
|
||||
const agentId = normalizeAgentId(params.agentId);
|
||||
for (const channel of params.selection) {
|
||||
const match: AgentBinding["match"] = { channel };
|
||||
const accountId = params.accountIds?.[channel]?.trim();
|
||||
const accountId = resolveBindingAccountId({
|
||||
channel,
|
||||
config: params.config,
|
||||
agentId,
|
||||
explicitAccountId: params.accountIds?.[channel],
|
||||
});
|
||||
if (accountId) {
|
||||
match.accountId = accountId;
|
||||
} else {
|
||||
const plugin = getChannelPlugin(channel);
|
||||
if (plugin?.meta.forceAccountBinding) {
|
||||
match.accountId = resolveDefaultAccountId(params.config, channel);
|
||||
}
|
||||
}
|
||||
bindings.push({ agentId, match });
|
||||
}
|
||||
@@ -141,17 +301,17 @@ export function parseBindingSpecs(params: {
|
||||
errors.push(`Unknown channel "${channelRaw}".`);
|
||||
continue;
|
||||
}
|
||||
let accountId = accountRaw?.trim();
|
||||
let accountId: string | undefined = accountRaw?.trim();
|
||||
if (accountRaw !== undefined && !accountId) {
|
||||
errors.push(`Invalid binding "${trimmed}" (empty account id).`);
|
||||
continue;
|
||||
}
|
||||
if (!accountId) {
|
||||
const plugin = getChannelPlugin(channel);
|
||||
if (plugin?.meta.forceAccountBinding) {
|
||||
accountId = resolveDefaultAccountId(params.config, channel);
|
||||
}
|
||||
}
|
||||
accountId = resolveBindingAccountId({
|
||||
channel,
|
||||
config: params.config,
|
||||
agentId,
|
||||
explicitAccountId: accountId,
|
||||
});
|
||||
const match: AgentBinding["match"] = { channel };
|
||||
if (accountId) {
|
||||
match.accountId = accountId;
|
||||
|
||||
Reference in New Issue
Block a user