mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 11:31:02 +00:00
* refactor: share talk event metric extraction * refactor: reuse shared coercion helpers * refactor: reuse shared primitive guards * refactor: reuse shared record guard * refactor: reuse shared primitive helpers * refactor: reuse shared string guards * refactor: reuse shared non-empty string guard * refactor: share plugin primitive coercion helpers * refactor: reuse plugin coercion helpers * refactor: reuse plugin coercion helpers in more plugins * refactor: reuse channel coercion helpers * refactor: reuse monitor coercion helpers * refactor: reuse provider coercion helpers * refactor: reuse core coercion helpers * refactor: reuse runtime coercion helpers * refactor: reuse helper coercion in codex paths * refactor: reuse helper coercion in runtime paths * refactor: reuse codex app-server coercion helpers * refactor: reuse codex record helpers * refactor: reuse migration and qa record helpers * refactor: reuse feishu and core helper guards * refactor: reuse browser and policy coercion helpers * refactor: reuse memory wiki record helper * refactor: share boolean coercion helpers * refactor: reuse finite number coercion * refactor: reuse trimmed string list helpers * refactor: reuse string list normalization * refactor: reuse remaining string list helpers * refactor: reuse string entry normalizer * refactor: share sorted string helpers * refactor: share string list normalization * test: preserve command registry browser imports * refactor: reuse trimmed list helpers * refactor: reuse string dedupe helpers * refactor: reuse local dedupe helpers * refactor: reuse more string dedupe helpers * refactor: reuse command string dedupe helpers * refactor: dedupe memory path lists with helper * refactor: expose string dedupe helpers to plugins * refactor: reuse core string dedupe helpers * refactor: reuse shared unique value helpers * refactor: reuse unique helpers in agent utilities * refactor: reuse unique helpers in config plumbing * refactor: reuse unique helpers in extensions * refactor: reuse unique helpers in core utilities * refactor: reuse unique helpers in qa plugins * refactor: reuse unique helpers in memory plugins * refactor: reuse unique helpers in channel plugins * refactor: reuse unique helpers in core tails * refactor: reuse unique helper in comfy workflow * refactor: reuse unique helpers in test utilities * refactor: expose unique value helper to plugins * refactor: reuse unique helpers for numeric lists * refactor: replace index dedupe filters * refactor: reuse string entry normalization * refactor: reuse string normalization in plugin helpers * refactor: reuse string normalization in extension helpers * refactor: reuse string normalization in channel parsers * refactor: reuse string normalization in memory search * refactor: reuse string normalization in provider parsers * refactor: reuse string normalization in qa helpers * refactor: reuse string normalization in infra parsers * refactor: reuse string normalization in messaging parsers * refactor: reuse string normalization in core parsers * refactor: reuse string normalization in extension parsers * refactor: reuse string normalization in remaining parsers * refactor: reuse string normalization in final parser spots * refactor: reuse string normalization in qa media helpers * refactor: reuse normalization in provider and media lists * refactor: reuse normalization for remaining set filters * refactor: reuse normalization in policy allowlists * refactor: reuse normalization in session and owner lists * refactor: centralize primitive string lists * refactor: reuse lowercase entry helpers * refactor: reuse sorted string helpers * refactor: reuse unique trimmed helpers * refactor: reuse string normalization helpers * refactor: reuse catalog string helpers * refactor: reuse remaining string helpers * refactor: simplify remaining list normalization * refactor: reuse codex auth order normalization * chore: refresh plugin sdk api baseline * fix: make shared string sorting deterministic * chore: refresh plugin sdk api baseline * fix: align host env security ordering
343 lines
10 KiB
TypeScript
343 lines
10 KiB
TypeScript
import { getBundledChannelSetupPlugin } from "../channels/plugins/bundled.js";
|
|
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
|
import { getLoadedChannelPlugin } from "../channels/plugins/index.js";
|
|
import type { ChannelId } from "../channels/plugins/types.public.js";
|
|
import { normalizeChannelId as normalizeBundledChannelId } from "../channels/registry.js";
|
|
import { formatUnknownChannelMessage } from "../cli/error-format.js";
|
|
import { isRouteBinding, listRouteBindings } from "../config/bindings.js";
|
|
import type { AgentRouteBinding } from "../config/types.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { listManifestChannelContributionIds } from "../plugins/manifest-contribution-ids.js";
|
|
import { DEFAULT_ACCOUNT_ID, normalizeAgentId } from "../routing/session-key.js";
|
|
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
|
import { normalizeSortedUniqueStringEntries } from "../shared/string-normalization.js";
|
|
import type { ChannelChoice } from "./onboard-types.js";
|
|
|
|
export { describeBinding } from "./agents.binding-format.js";
|
|
|
|
function bindingMatchKey(match: AgentRouteBinding["match"]) {
|
|
const accountId = normalizeOptionalString(match.accountId) || DEFAULT_ACCOUNT_ID;
|
|
const identityKey = bindingMatchIdentityKey(match);
|
|
return JSON.stringify([identityKey, accountId]);
|
|
}
|
|
|
|
function bindingMatchIdentityKey(match: AgentRouteBinding["match"]) {
|
|
const roles = Array.isArray(match.roles) ? normalizeSortedUniqueStringEntries(match.roles) : [];
|
|
return JSON.stringify([
|
|
match.channel,
|
|
match.peer?.kind ?? "",
|
|
match.peer?.id ?? "",
|
|
match.guildId ?? "",
|
|
match.teamId ?? "",
|
|
roles.join(","),
|
|
]);
|
|
}
|
|
|
|
function canUpgradeBindingAccountScope(params: {
|
|
existing: AgentRouteBinding;
|
|
incoming: AgentRouteBinding;
|
|
normalizedIncomingAgentId: string;
|
|
}): boolean {
|
|
if (!normalizeOptionalString(params.incoming.match.accountId)) {
|
|
return false;
|
|
}
|
|
if (normalizeOptionalString(params.existing.match.accountId)) {
|
|
return false;
|
|
}
|
|
if (normalizeAgentId(params.existing.agentId) !== params.normalizedIncomingAgentId) {
|
|
return false;
|
|
}
|
|
return (
|
|
bindingMatchIdentityKey(params.existing.match) ===
|
|
bindingMatchIdentityKey(params.incoming.match)
|
|
);
|
|
}
|
|
|
|
export function applyAgentBindings(
|
|
cfg: OpenClawConfig,
|
|
bindings: AgentRouteBinding[],
|
|
): {
|
|
config: OpenClawConfig;
|
|
added: AgentRouteBinding[];
|
|
updated: AgentRouteBinding[];
|
|
skipped: AgentRouteBinding[];
|
|
conflicts: Array<{ binding: AgentRouteBinding; existingAgentId: string }>;
|
|
} {
|
|
const existingRoutes = [...listRouteBindings(cfg)];
|
|
const nonRouteBindings = (cfg.bindings ?? []).filter((binding) => !isRouteBinding(binding));
|
|
const existingMatchMap = new Map<string, string>();
|
|
for (const binding of existingRoutes) {
|
|
const key = bindingMatchKey(binding.match);
|
|
if (!existingMatchMap.has(key)) {
|
|
existingMatchMap.set(key, normalizeAgentId(binding.agentId));
|
|
}
|
|
}
|
|
|
|
const added: AgentRouteBinding[] = [];
|
|
const updated: AgentRouteBinding[] = [];
|
|
const skipped: AgentRouteBinding[] = [];
|
|
const conflicts: Array<{ binding: AgentRouteBinding; existingAgentId: string }> = [];
|
|
|
|
for (const binding of bindings) {
|
|
const agentId = normalizeAgentId(binding.agentId);
|
|
const key = bindingMatchKey(binding.match);
|
|
const existingAgentId = existingMatchMap.get(key);
|
|
if (existingAgentId) {
|
|
if (existingAgentId === agentId) {
|
|
skipped.push(binding);
|
|
} else {
|
|
conflicts.push({ binding, existingAgentId });
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const upgradeIndex = existingRoutes.findIndex((candidate) =>
|
|
canUpgradeBindingAccountScope({
|
|
existing: candidate,
|
|
incoming: binding,
|
|
normalizedIncomingAgentId: agentId,
|
|
}),
|
|
);
|
|
if (upgradeIndex >= 0) {
|
|
const current = existingRoutes[upgradeIndex];
|
|
if (!current) {
|
|
continue;
|
|
}
|
|
const previousKey = bindingMatchKey(current.match);
|
|
const upgradedBinding: AgentRouteBinding = {
|
|
...current,
|
|
agentId,
|
|
match: {
|
|
...current.match,
|
|
accountId: binding.match.accountId?.trim(),
|
|
},
|
|
};
|
|
existingRoutes[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 && updated.length === 0) {
|
|
return { config: cfg, added, updated, skipped, conflicts };
|
|
}
|
|
|
|
return {
|
|
config: {
|
|
...cfg,
|
|
bindings: [...existingRoutes, ...added, ...nonRouteBindings],
|
|
},
|
|
added,
|
|
updated,
|
|
skipped,
|
|
conflicts,
|
|
};
|
|
}
|
|
|
|
export function removeAgentBindings(
|
|
cfg: OpenClawConfig,
|
|
bindings: AgentRouteBinding[],
|
|
): {
|
|
config: OpenClawConfig;
|
|
removed: AgentRouteBinding[];
|
|
missing: AgentRouteBinding[];
|
|
conflicts: Array<{ binding: AgentRouteBinding; existingAgentId: string }>;
|
|
} {
|
|
const existingRoutes = listRouteBindings(cfg);
|
|
const nonRouteBindings = (cfg.bindings ?? []).filter((binding) => !isRouteBinding(binding));
|
|
const removeIndexes = new Set<number>();
|
|
const removed: AgentRouteBinding[] = [];
|
|
const missing: AgentRouteBinding[] = [];
|
|
const conflicts: Array<{ binding: AgentRouteBinding; 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 < existingRoutes.length; i += 1) {
|
|
if (removeIndexes.has(i)) {
|
|
continue;
|
|
}
|
|
const current = existingRoutes[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 = existingRoutes[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 nextRouteBindings = existingRoutes.filter((_, index) => !removeIndexes.has(index));
|
|
const nextBindings = [...nextRouteBindings, ...nonRouteBindings];
|
|
return {
|
|
config: {
|
|
...cfg,
|
|
bindings: nextBindings.length > 0 ? nextBindings : undefined,
|
|
},
|
|
removed,
|
|
missing,
|
|
conflicts,
|
|
};
|
|
}
|
|
|
|
function resolveDefaultAccountId(cfg: OpenClawConfig, provider: ChannelId): string {
|
|
const plugin = getBindingChannelPlugin(provider);
|
|
if (!plugin) {
|
|
return DEFAULT_ACCOUNT_ID;
|
|
}
|
|
return resolveChannelDefaultAccountId({ plugin, cfg });
|
|
}
|
|
|
|
function listManifestChannelIds(config: OpenClawConfig): Set<string> {
|
|
return new Set(
|
|
listManifestChannelContributionIds({
|
|
includeDisabled: true,
|
|
config,
|
|
env: process.env,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function normalizeBindingChannelId(
|
|
raw: string | undefined,
|
|
config: OpenClawConfig,
|
|
): ChannelId | null {
|
|
const bundled = normalizeBundledChannelId(raw);
|
|
if (bundled) {
|
|
return bundled;
|
|
}
|
|
const normalized = normalizeOptionalString(raw)?.toLowerCase();
|
|
if (!normalized) {
|
|
return null;
|
|
}
|
|
return listManifestChannelIds(config).has(normalized) ? normalized : null;
|
|
}
|
|
|
|
function getBindingChannelPlugin(channel: ChannelId) {
|
|
return getLoadedChannelPlugin(channel) ?? getBundledChannelSetupPlugin(channel);
|
|
}
|
|
|
|
function resolveBindingAccountId(params: {
|
|
channel: ChannelId;
|
|
config: OpenClawConfig;
|
|
agentId: string;
|
|
explicitAccountId?: string;
|
|
}): string | undefined {
|
|
const explicitAccountId = params.explicitAccountId?.trim();
|
|
if (explicitAccountId) {
|
|
return explicitAccountId;
|
|
}
|
|
|
|
const plugin = getBindingChannelPlugin(params.channel);
|
|
const pluginAccountId = plugin?.setup?.resolveBindingAccountId?.({
|
|
cfg: params.config,
|
|
agentId: params.agentId,
|
|
});
|
|
if (pluginAccountId?.trim()) {
|
|
return pluginAccountId.trim();
|
|
}
|
|
|
|
if (plugin && plugin.config.listAccountIds(params.config).length > 1) {
|
|
return "*";
|
|
}
|
|
|
|
if (plugin?.meta.forceAccountBinding) {
|
|
return resolveDefaultAccountId(params.config, params.channel);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export function buildChannelBindings(params: {
|
|
agentId: string;
|
|
selection: ChannelChoice[];
|
|
config: OpenClawConfig;
|
|
accountIds?: Partial<Record<ChannelChoice, string>>;
|
|
}): AgentRouteBinding[] {
|
|
const bindings: AgentRouteBinding[] = [];
|
|
const agentId = normalizeAgentId(params.agentId);
|
|
for (const channel of params.selection) {
|
|
const match: AgentRouteBinding["match"] = { channel };
|
|
const accountId = resolveBindingAccountId({
|
|
channel,
|
|
config: params.config,
|
|
agentId,
|
|
explicitAccountId: params.accountIds?.[channel],
|
|
});
|
|
if (accountId) {
|
|
match.accountId = accountId;
|
|
}
|
|
bindings.push({ type: "route", agentId, match });
|
|
}
|
|
return bindings;
|
|
}
|
|
|
|
export function parseBindingSpecs(params: {
|
|
agentId: string;
|
|
specs?: string[];
|
|
config: OpenClawConfig;
|
|
}): { bindings: AgentRouteBinding[]; errors: string[] } {
|
|
const bindings: AgentRouteBinding[] = [];
|
|
const errors: string[] = [];
|
|
const specs = params.specs ?? [];
|
|
const agentId = normalizeAgentId(params.agentId);
|
|
for (const raw of specs) {
|
|
const trimmed = raw?.trim();
|
|
if (!trimmed) {
|
|
continue;
|
|
}
|
|
const [channelRaw, accountRaw] = trimmed.split(":", 2);
|
|
const channel = normalizeBindingChannelId(channelRaw, params.config);
|
|
if (!channel) {
|
|
errors.push(formatUnknownChannelMessage({ channel: channelRaw }));
|
|
continue;
|
|
}
|
|
let accountId: string | undefined = accountRaw?.trim();
|
|
if (accountRaw !== undefined && !accountId) {
|
|
errors.push(
|
|
`Invalid binding "${trimmed}". Account id is empty. Use <channel>:<account>, for example telegram:default.`,
|
|
);
|
|
continue;
|
|
}
|
|
accountId = resolveBindingAccountId({
|
|
channel,
|
|
config: params.config,
|
|
agentId,
|
|
explicitAccountId: accountId,
|
|
});
|
|
const match: AgentRouteBinding["match"] = { channel };
|
|
if (accountId) {
|
|
match.accountId = accountId;
|
|
}
|
|
bindings.push({ type: "route", agentId, match });
|
|
}
|
|
return { bindings, errors };
|
|
}
|