openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
230 lines (229 loc) • 10.7 kB
JavaScript
import { c as normalizeOptionalLowercaseString } from "./string-coerce-1VuiLaaR.mjs";
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import "./session-key-DY3MOX4N.mjs";
import { n as normalizeAccountId } from "./account-id-ifCOO7_9.mjs";
import { i as listAgentIds } from "./agent-roster-DH6r_8f4.mjs";
import { S as tryResolveAgentOperationAgentId, m as resolveConfiguredAgentId, t as AgentSelectionRequiredError } from "./agent-scope-config-BEk6-ThS.mjs";
import { n as resolveChannelSetupExecutionAdapter } from "./setup-contract-DgLM9zTf.mjs";
import { p as readCurrentConfigForPolicyCheckAsync, u as readConfigFileSnapshotForWrite } from "./io.runtime-B134MOMz.mjs";
import "./config-VnHbY_QV.mjs";
import { n as getLoadedChannelPlugin, t as getChannelPlugin } from "./registry-Bcrk1FVy.mjs";
import "./plugins-BXkKReMp.mjs";
import { n as resolveChannelAccount } from "./account-resolution-DCHDXWZ0.mjs";
import { c as formatUnknownChannelMessage } from "./error-format-B-v9u991.mjs";
import { t as applyAgentBindings } from "./agents.bindings-CW_gwV7c.mjs";
import { t as resolveChannelSetupOwner } from "./owner-DOtRVzav.mjs";
import { i as withCommandPluginMetadata } from "./config-validation-Drf1V2Dj.mjs";
import { t as describeBinding } from "./agents.binding-format-B08nnBYq.mjs";
import { t as persistChannelPluginConfig } from "./plugin-config-persistence-tVO3tOsG.mjs";
//#region src/commands/channels/add-wizard.ts
function unresolvedInitialWizardChannelTarget(channel) {
return {
kind: "unresolved",
message: formatUnknownChannelMessage({ channel })
};
}
/** Select a setup owner before workspace-scoped channel discovery. */
async function selectChannelSetupOwner(writeSnapshot, prompter, requestedAgentId) {
const cfg = writeSnapshot.snapshot.sourceConfig;
try {
return resolveChannelSetupOwner(cfg, requestedAgentId);
} catch (error) {
if (!(error instanceof AgentSelectionRequiredError)) throw error;
}
const selectedAgent = await prompter.select({
message: "Set up channels for agent",
options: listAgentIds(cfg).map((agentId) => ({
value: { agentId },
label: agentId
}))
});
if (!isRecord(selectedAgent) || typeof selectedAgent.agentId !== "string") throw new Error("Invalid channel setup owner selection");
writeSnapshot.writeOptions.assertConfigPathForWrite?.();
const currentConfig = await readCurrentConfigForPolicyCheckAsync({
configPath: writeSnapshot.snapshot.path,
env: process.env
});
writeSnapshot.writeOptions.assertConfigPathForWrite?.();
const agentId = resolveConfiguredAgentId(currentConfig, selectedAgent.agentId);
return resolveChannelSetupOwner(currentConfig, agentId);
}
/** Resolve omitted, matched, and unmatched channel targets without collapsing caller intent. */
async function resolveInitialWizardChannelTarget(raw, cfg, workspaceDir) {
if (raw === void 0) return { kind: "omitted" };
const normalized = normalizeOptionalLowercaseString(raw);
if (!normalized) return unresolvedInitialWizardChannelTarget("");
const [{ listActiveChannelSetupPlugins }, { resolveChannelSetupEntries }] = await Promise.all([import("./setup-registry--K-wOVvp.mjs"), import("./discovery-D_zCg_TK.mjs")]);
const resolved = resolveChannelSetupEntries({
cfg,
installedPlugins: listActiveChannelSetupPlugins(),
workspaceDir: workspaceDir ?? resolveChannelSetupOwner(cfg).workspaceDir
});
const matchedEntry = resolved.entries.find((candidate) => normalizeOptionalLowercaseString(candidate.id) === normalized) ?? resolved.entries.find((candidate) => (candidate.meta.aliases ?? []).some((alias) => normalizeOptionalLowercaseString(alias) === normalized));
return matchedEntry ? {
kind: "resolved",
channel: matchedEntry.id
} : unresolvedInitialWizardChannelTarget(raw.trim());
}
/** Run the interactive channel-setup flow and persist the resulting config. */
async function runChannelsAddWizardFlow(params) {
const { writeSnapshot, runtime, prompter } = params;
const { sourceConfig: cfg, hash: baseHash } = writeSnapshot.snapshot;
const [{ buildAgentSummaries }, onboardChannels] = await Promise.all([import("./agents.config-aNu1XMNu.mjs"), import("./channel-setup-CklOnTjd.mjs")]);
const channelSetup = onboardChannels.createChannelSetupHooks({
runtime,
...params.beforePersistentEffect ? { beforePersistentEffect: params.beforePersistentEffect } : {}
});
let selection = [];
const accountIds = {};
const resolvedPlugins = /* @__PURE__ */ new Map();
await prompter.intro("Channel setup");
let nextConfig = await onboardChannels.setupChannels(cfg, runtime, prompter, {
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {},
...params.initialChannel ? {
initialSelection: [params.initialChannel],
finishAfterInitialSelection: true
} : {},
allowDisable: false,
allowIMessageInstall: true,
allowSignalInstall: true,
...params.beforePersistentEffect ? { beforePersistentEffect: params.beforePersistentEffect } : {},
...params.assertPersistentEffectCurrent ? { assertPersistentEffectCurrent: params.assertPersistentEffectCurrent } : {},
...params.deferDeviceLinkToClient ? { deferDeviceLinkToClient: true } : {},
onPostWriteHook: channelSetup.onPostWriteHook,
promptAccountIds: true,
deferStatusUntilSelection: true,
skipStatusNote: true,
onSelection: (value) => {
selection = value;
},
onAccountId: (channel, accountId) => {
accountIds[channel] = accountId;
},
onResolvedPlugin: (channel, plugin) => {
resolvedPlugins.set(channel, plugin);
}
});
const commitWizardConfig = async (config) => {
await params.beforePersistentEffect?.();
const committed = await persistChannelPluginConfig({
cfg: config,
pluginInstalled: false,
writeOptions: writeSnapshot.writeOptions,
baseHash,
runtime
});
await channelSetup.runPostWriteHooks(committed.path);
};
if (selection.length === 0) {
if (nextConfig !== cfg) {
await commitWizardConfig(nextConfig);
await prompter.outro("Channels updated.");
return;
}
await prompter.outro("No channel changes made.");
return;
}
const usesTargetedDefaults = params.initialChannel !== void 0 && selection.length === 1 && selection[0] === params.initialChannel;
if (usesTargetedDefaults ? false : await prompter.confirm({
message: "Name these channel accounts now? (optional)",
initialValue: false
})) await withCommandPluginMetadata({
config: nextConfig,
workspaceDir: params.workspaceDir
}, async () => {
for (const channel of selection) {
const accountId = accountIds[channel] ?? "default";
const plugin = resolvedPlugins.get(channel) ?? getLoadedChannelPlugin(channel);
const account = plugin ? await resolveChannelAccount({
plugin,
cfg: nextConfig,
accountId
}) : void 0;
const existingName = (plugin?.config.describeAccount?.(account, nextConfig))?.name ?? account?.name;
const name = await prompter.text({
message: `${channel} display name for account "${accountId}"`,
initialValue: existingName
});
if (name?.trim()) {
const namingPlugin = plugin ?? getChannelPlugin(channel);
const apply = namingPlugin ? resolveChannelSetupExecutionAdapter(namingPlugin)?.applyAccountName : void 0;
if (apply) nextConfig = apply({
cfg: nextConfig,
accountId: normalizeAccountId(accountId),
name
});
}
}
});
const bindTargets = selection.flatMap((channel) => {
const accountId = accountIds[channel]?.trim();
return accountId ? [{
channel,
accountId
}] : [];
});
if (bindTargets.length > 0) {
const agentSummaries = await buildAgentSummaries(nextConfig);
if (usesTargetedDefaults && agentSummaries.length <= 1 ? false : usesTargetedDefaults ? true : await prompter.confirm({
message: "Route these channel accounts to agents now?",
initialValue: true
})) {
const owner = tryResolveAgentOperationAgentId(nextConfig);
const defaultAgentId = owner === void 0 ? void 0 : resolveConfiguredAgentId(nextConfig, owner);
for (const target of bindTargets) {
const targetAgentId = await prompter.select({
message: `Send ${target.channel}/${target.accountId} messages to agent`,
options: agentSummaries.map((agent) => ({
value: agent.id,
label: agent.isDefault ? `${agent.id} (default)` : agent.id
})),
initialValue: params.agentId ?? defaultAgentId
});
const bindingResult = applyAgentBindings(nextConfig, [{
agentId: targetAgentId,
match: {
channel: target.channel,
accountId: target.accountId
}
}]);
nextConfig = bindingResult.config;
if (bindingResult.added.length > 0 || bindingResult.updated.length > 0) await prompter.note([...bindingResult.added.map((binding) => `Added: ${describeBinding(binding)}`), ...bindingResult.updated.map((binding) => `Updated: ${describeBinding(binding)}`)].join("\n"), "Routing bindings");
if (bindingResult.conflicts.length > 0) await prompter.note(["Skipped bindings already claimed by another agent:", ...bindingResult.conflicts.map((conflict) => `- ${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`)].join("\n"), "Routing bindings");
}
}
}
await commitWizardConfig(nextConfig);
params.onConfigured?.(selection.map((channel) => ({
channel,
accountId: accountIds[channel] ?? "default"
})));
await prompter.outro("Channels updated.");
}
/**
* Gateway entry for `wizard.start {flow:"channels"}`. Unlike the CLI path this
* must never call runtime.exit — failures throw and surface as wizard errors.
*/
async function runChannelsSetupWizard(opts, runtime, prompter) {
const writeSnapshot = await readConfigFileSnapshotForWrite();
const { snapshot } = writeSnapshot;
if (snapshot.exists && !snapshot.valid) throw new Error("OpenClaw config is invalid; run `openclaw doctor --fix`, then retry channel setup.");
const cfg = snapshot.sourceConfig;
const { agentId, workspaceDir } = await selectChannelSetupOwner(writeSnapshot, prompter);
const target = await resolveInitialWizardChannelTarget(opts.channel, cfg, workspaceDir);
if (target.kind === "unresolved") throw new Error(target.message);
await runChannelsAddWizardFlow({
writeSnapshot,
agentId,
runtime,
prompter,
workspaceDir,
...target.kind === "resolved" ? { initialChannel: target.channel } : {},
deferDeviceLinkToClient: true,
...opts.onConfigured ? { onConfigured: opts.onConfigured } : {},
...opts.beforePersistentEffect ? { beforePersistentEffect: opts.beforePersistentEffect } : {},
...opts.assertPersistentEffectCurrent ? { assertPersistentEffectCurrent: opts.assertPersistentEffectCurrent } : {}
});
}
//#endregion
export { resolveInitialWizardChannelTarget, runChannelsAddWizardFlow, runChannelsSetupWizard, selectChannelSetupOwner };