UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

446 lines (445 loc) 19.1 kB
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { v as resolveDefaultAgentDir } from "./agent-scope-config-DcbEhP0R.js"; import { h as normalizeSecretInputString, l as hasConfiguredSecretInput, t as DEFAULT_SECRET_PROVIDER_ALIAS } from "./types.secrets-kC0nOetj.js"; import { l as normalizePluginsConfig, u as resolveEffectiveEnableState } from "./config-state-BkU1frVq.js"; import { a as resolveAgentModelPrimaryValue } from "./model-input-BuGMCNOz.js"; import { t as resolveDefaultModelForAgent } from "./model-selection-config-BrdmmqKD.js"; import { n as enablePluginInConfig, r as enablePluginWithCapabilityConsent } from "./enable-3OwjFbBV.js"; import { t as resolveWebSearchInstallCatalogEntries } from "./web-search-install-catalog-mZmYPP1I.js"; import { t as resolveAgentHarnessPolicy } from "./policy-D9i1QMuw.js"; import "./model-selection-di2kjKCB.js"; import { i as hasAuthProfileForProvider } from "./model-config.helpers-zn_oC6Fp.js"; import { t as sortPluginEntriesById } from "./plugin-entry-order-DxrT0ucv.js"; import { t as resolvePluginWebSearchProviders } from "./web-search-providers.runtime.js"; import { a as resolveWebSearchProviderId } from "./runtime-CPS9j3kB.js"; import { n as t } from "./i18n-hynzGFbD.js"; import { t as createPluginCapabilityConsentPrompter } from "./plugin-capability-consent-CaBGom29.js"; import { t as sortFlowContributionsByLabel } from "./types-CnTXyUgM.js"; //#region src/flows/search-setup.ts const SEARCH_INSTALL_CATALOG_ENTRY = Symbol("search-install-catalog-entry"); const WEB_SEARCH_DOCS_URL = "https://docs.openclaw.ai/tools/web"; const CODEX_HOSTED_SEARCH_PROVIDER_ID = "codex"; function resolveSearchProviderCredentialLabel(entry) { if (entry.requiresCredential === false) return `${entry.label} setup`; return normalizeOptionalString(entry.credentialLabel) || `${entry.label} API key`; } function listSearchProviderOptions(config) { return resolveSearchProviderOptions(config); } function showsSearchProviderInSetup(entry) { return entry.onboardingScopes?.includes("text-inference") ?? false; } function resolveSearchProviderOptions(config) { return resolveSearchProviderSetupContributions(config).map((contribution) => contribution.provider); } function buildSearchProviderSetupContribution(params) { return { id: `search:setup:${params.provider.id}`, kind: "search", surface: "setup", provider: params.provider, option: { value: params.provider.id, label: params.provider.label, ...params.provider.hint ? { hint: params.provider.hint } : {}, ...params.provider.docsUrl ? { docs: { path: params.provider.docsUrl } } : {} }, source: params.source }; } function resolveSearchProviderSetupContributions(config) { const runtimeProviders = sortPluginEntriesById(resolvePluginWebSearchProviders({ config, env: process.env, mode: "setup" })); const seenProviderIds = new Set(runtimeProviders.map((provider) => provider.id)); const seenPluginIds = new Set(runtimeProviders.map((provider) => provider.pluginId)); const normalizedPluginsConfig = normalizePluginsConfig(config?.plugins); const installCatalogProviders = resolveWebSearchInstallCatalogEntries().filter((entry) => !seenProviderIds.has(entry.provider.id) && !seenPluginIds.has(entry.pluginId) && resolveEffectiveEnableState({ id: entry.pluginId, origin: "global", config: normalizedPluginsConfig, rootConfig: config, enabledByDefault: true }).enabled).map((entry) => Object.assign({}, entry.provider, { [SEARCH_INSTALL_CATALOG_ENTRY]: entry })); const providers = sortPluginEntriesById([...runtimeProviders, ...installCatalogProviders]); return sortFlowContributionsByLabel(providers.filter(showsSearchProviderInSetup).map((provider) => buildSearchProviderSetupContribution({ provider, source: SEARCH_INSTALL_CATALOG_ENTRY in provider ? "install-catalog" : "runtime" }))); } function defaultModelUsesCodexRuntime(config) { if (!resolveAgentModelPrimaryValue(config.agents?.defaults?.model)) return false; const defaultModel = resolveDefaultModelForAgent({ cfg: config }); if (defaultModel.provider === CODEX_HOSTED_SEARCH_PROVIDER_ID) return true; return resolveAgentHarnessPolicy({ provider: defaultModel.provider, modelId: defaultModel.model, config }).runtime === "codex"; } function prioritizeSearchProvider(providers, preferredProvider) { if (!preferredProvider) return [...providers]; const preferred = providers.find((provider) => provider.id === preferredProvider); if (!preferred) return [...providers]; return [preferred, ...providers.filter((provider) => provider.id !== preferredProvider)]; } function resolveSearchProviderEntry(config, provider) { return resolveSearchProviderOptions(config).find((entry) => entry.id === provider); } function hasKeyInEnv(entry) { return entry.envVars.some((k) => Boolean(normalizeOptionalString(process.env[k]))); } function providerNeedsCredential(entry) { return entry.requiresCredential !== false; } function formatAuthProviderLabel(providerId) { return providerId === "xai" ? "xAI" : providerId; } function providerIsReady(config, entry) { if (!providerNeedsCredential(entry)) return true; if (entry.authProviderId && hasAuthProfileForProvider({ provider: entry.authProviderId, agentDir: resolveDefaultAgentDir(config) })) return true; return hasExistingKey(config, entry.id) || hasKeyInEnv(entry); } function formatSearchProviderOptionLabel(label, note) { const normalizedNote = normalizeOptionalString(note); return normalizedNote ? `${label} (${normalizedNote})` : label; } function rawKeyValue(config, provider) { return resolveSearchProviderEntry(config, provider)?.getConfiguredCredentialValue?.(config); } function resolveExistingKey(config, provider) { return normalizeSecretInputString(rawKeyValue(config, provider)); } function hasExistingKey(config, provider) { return hasConfiguredSecretInput(rawKeyValue(config, provider)); } function buildSearchEnvRef(config, provider) { const entry = resolveSearchProviderEntry(config, provider) ?? listSearchProviderOptions(config).find((candidate) => candidate.id === provider) ?? listSearchProviderOptions().find((candidate) => candidate.id === provider); const resolvedEnvVar = entry?.envVars.find((k) => Boolean(normalizeOptionalString(process.env[k]))) ?? entry?.envVars[0]; if (!resolvedEnvVar) throw new Error(`No env var mapping for search provider "${provider}" at ${entry?.credentialPath ?? "unknown path"} in secret-input-mode=ref.`); return { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id: resolvedEnvVar }; } function resolveSearchSecretInput(config, provider, key, secretInputMode) { if (secretInputMode === "ref") return buildSearchEnvRef(config, provider); return key; } function applySearchKey(config, provider, key) { const providerEntry = resolveSearchProviderEntry(config, provider); if (!providerEntry) return config; const search = { ...config.tools?.web?.search, provider, enabled: true }; const next = applySearchProviderSelectionConfig({ ...config, tools: { ...config.tools, web: { ...config.tools?.web, search } } }, providerEntry); providerEntry.setConfiguredCredentialValue?.(next, key); return next; } function applySearchProviderSelectionConfig(config, providerEntry) { if (providerEntry.applySelectionConfig) return providerEntry.applySelectionConfig(config); if (providerEntry.pluginId) return enablePluginInConfig(config, providerEntry.pluginId).config; return config; } function applySearchProviderSelection(config, provider) { const providerEntry = resolveSearchProviderEntry(config, provider); if (!providerEntry) return config; const search = { ...config.tools?.web?.search, provider, enabled: true }; return applySearchProviderSelectionConfig({ ...config, tools: { ...config.tools, web: { ...config.tools?.web, search } } }, providerEntry); } function preserveDisabledState(original, result) { if (original.tools?.web?.search?.enabled !== false) return result; const next = { ...result, tools: { ...result.tools, web: { ...result.tools?.web, search: { ...result.tools?.web?.search, enabled: false } } } }; const provider = next.tools?.web?.search?.provider; if (typeof provider !== "string") return next; const providerEntry = resolveSearchProviderEntry(original, provider); if (!providerEntry?.pluginId) return next; const pluginId = providerEntry.pluginId; const originalPluginEntry = (original.plugins?.entries)?.[pluginId]; const resultPluginEntry = (next.plugins?.entries)?.[pluginId]; const nextPlugins = { ...next.plugins }; if (Array.isArray(original.plugins?.allow)) nextPlugins.allow = [...original.plugins.allow]; else delete nextPlugins.allow; if (resultPluginEntry || originalPluginEntry) { const nextEntries = { ...nextPlugins.entries }; const patchedEntry = { ...resultPluginEntry }; if (typeof originalPluginEntry?.enabled === "boolean") patchedEntry.enabled = originalPluginEntry.enabled; else delete patchedEntry.enabled; nextEntries[pluginId] = patchedEntry; nextPlugins.entries = nextEntries; } return { ...next, plugins: nextPlugins }; } function completedSearchSetup(config) { return { outcome: "completed", config }; } async function finalizeSearchProviderSetup(params) { let next = params.nextConfig; const installEntry = params.entry[SEARCH_INSTALL_CATALOG_ENTRY]; if (installEntry && next.tools?.web?.search?.enabled !== false) { const { ensureOnboardingPluginInstalled } = await import("./onboarding-plugin-install-CeahhHcF.js"); const installed = await ensureOnboardingPluginInstalled({ cfg: next, entry: { pluginId: installEntry.pluginId, label: installEntry.label, install: installEntry.install, ...installEntry.trustedSourceLinkedOfficialInstall ? { trustedSourceLinkedOfficialInstall: true } : {} }, prompter: params.prompter, runtime: params.runtime, autoConfirmSingleSource: true, ...params.opts?.beforePersistentEffect ? { beforePersistentEffect: params.opts.beforePersistentEffect } : {} }); if (!installed.installed) { if (installed.status === "skipped") return { outcome: "kept-current", config: params.originalConfig, reason: "provider-install-skipped", providerId: params.entry.id }; return { outcome: "install-failed", config: params.originalConfig, providerId: params.entry.id, reason: installed.status === "timed_out" ? "timed-out" : "failed" }; } next = installed.cfg; } if (params.opts?.preserveDisabledSearchState !== false) next = preserveDisabledState(params.originalConfig, next); if (!params.entry.runSetup) return completedSearchSetup(next); next = await params.entry.runSetup({ config: next, runtime: params.runtime, prompter: params.prompter, quickstartDefaults: params.opts?.quickstartDefaults, secretInputMode: params.opts?.secretInputMode }); return completedSearchSetup(params.opts?.preserveDisabledSearchState === false ? next : preserveDisabledState(params.originalConfig, next)); } async function runSearchSetupFlow(config, runtime, prompter, opts) { const availableProviderOptions = resolveSearchProviderOptions(config); const codexRecommended = defaultModelUsesCodexRuntime(config) && availableProviderOptions.some((entry) => entry.id === CODEX_HOSTED_SEARCH_PROVIDER_ID); const providerOptions = prioritizeSearchProvider(availableProviderOptions, codexRecommended ? CODEX_HOSTED_SEARCH_PROVIDER_ID : void 0); if (providerOptions.length === 0) { await prompter.note([ t("wizard.search.noProvidersByPolicy"), t("wizard.search.noProvidersAction"), t("wizard.search.docsLine", { url: WEB_SEARCH_DOCS_URL }) ].join("\n"), t("wizard.search.title")); return { outcome: "kept-current", config, reason: "no-providers" }; } await prompter.note([ t("wizard.search.intro"), t("wizard.search.chooseProvider"), t("wizard.search.docsLine", { url: WEB_SEARCH_DOCS_URL }) ].join("\n"), t("wizard.search.title")); const existingProvider = config.tools?.web?.search?.provider; const defaultChoice = (() => { if (existingProvider && providerOptions.some((entry) => entry.id === existingProvider)) return existingProvider; if (codexRecommended) return CODEX_HOSTED_SEARCH_PROVIDER_ID; const searchForAutoDetect = { ...config.tools?.web?.search, provider: void 0 }; const autoDetectedId = resolveWebSearchProviderId({ config, search: searchForAutoDetect, providers: [...providerOptions] }); const autoDetected = providerOptions.find((entry) => entry.id === autoDetectedId); if (autoDetected) return autoDetected.id; const detected = providerOptions.find((entry) => providerNeedsCredential(entry) && providerIsReady(config, entry)); if (detected) return detected.id; return "__skip__"; })(); const options = providerOptions.map((entry) => { const credentialHint = entry.requiresCredential === false ? t("wizard.search.keyFree") : providerIsReady(config, entry) ? t("wizard.search.configured") : entry.credentialLabel ? t("wizard.search.credentialRequired", { label: entry.credentialLabel }) : t("wizard.search.apiKeyRequired"); const hint = [normalizeOptionalString(entry.hint), credentialHint].filter(Boolean).join(" · "); return { value: entry.id, label: formatSearchProviderOptionLabel(entry.label, hint) }; }); const choice = await prompter.select({ message: t("wizard.search.providerPrompt"), options: [...options, { value: "__skip__", label: t("common.skipForNow"), hint: t("wizard.search.configureLaterHint") }], initialValue: defaultChoice, searchable: true }); if (choice === "__skip__") return { outcome: "kept-current", config, reason: "user-skipped" }; const entry = resolveSearchProviderEntry(config, choice) ?? providerOptions.find((e) => e.id === choice); if (!entry) return { outcome: "kept-current", config, reason: "provider-unavailable", providerId: choice }; if (entry.pluginId && !(SEARCH_INSTALL_CATALOG_ENTRY in entry) && (opts?.preserveDisabledSearchState === false || config.tools?.web?.search?.enabled !== false)) { const enabled = await enablePluginWithCapabilityConsent(config, entry.pluginId, { onCapabilityConsent: createPluginCapabilityConsentPrompter(prompter, opts?.beforePersistentEffect) }); if (!enabled.enabled) { await prompter.note(enabled.reason ?? "Plugin could not be enabled.", "Web search unavailable"); return { outcome: "install-failed", config: enabled.config, providerId: choice, reason: "failed" }; } } const finalizeSelection = (nextConfig) => finalizeSearchProviderSetup({ originalConfig: config, nextConfig, entry, runtime, prompter, opts }); const credentialLabel = resolveSearchProviderCredentialLabel(entry); const existingKey = resolveExistingKey(config, choice); const keyConfigured = hasExistingKey(config, choice); const envAvailable = hasKeyInEnv(entry); const agentDir = resolveDefaultAgentDir(config); const authProviderId = entry.authProviderId; const providerAuthProfileAvailable = authProviderId ? hasAuthProfileForProvider({ provider: authProviderId, agentDir }) : false; const oauthAuthProfileAvailable = authProviderId && providerAuthProfileAvailable ? hasAuthProfileForProvider({ provider: authProviderId, agentDir, type: "oauth" }) : false; const needsCredential = providerNeedsCredential(entry); if (opts?.quickstartDefaults && (providerAuthProfileAvailable || keyConfigured || envAvailable)) return await finalizeSelection(existingKey ? applySearchKey(config, choice, existingKey) : applySearchProviderSelection(config, choice)); if (!needsCredential) { await prompter.note([ `${entry.label} works without an API key.`, "OpenClaw will enable the plugin and use it as your web_search provider.", `Docs: ${entry.docsUrl ?? "https://docs.openclaw.ai/tools/web"}` ].join("\n"), "Web search"); return await finalizeSelection(applySearchProviderSelection(config, choice)); } if (entry.credentialNote) await prompter.note(entry.credentialNote, entry.label); if (oauthAuthProfileAvailable && authProviderId) { const authProviderLabel = formatAuthProviderLabel(authProviderId); await prompter.note([ `${entry.label} can use your existing ${authProviderLabel} OAuth sign-in for web_search.`, "No separate API key is required; API-key auth remains available as a fallback.", `Docs: ${entry.docsUrl ?? WEB_SEARCH_DOCS_URL}` ].join("\n"), "Web search"); return await finalizeSelection(applySearchProviderSelection(config, choice)); } if (providerAuthProfileAvailable && authProviderId) { const authProviderLabel = formatAuthProviderLabel(authProviderId); await prompter.note([ `${entry.label} can use your existing ${authProviderLabel} auth profile for web_search.`, "No separate web-search key is required; API-key auth remains available as a fallback.", `Docs: ${entry.docsUrl ?? WEB_SEARCH_DOCS_URL}` ].join("\n"), "Web search"); return await finalizeSelection(applySearchProviderSelection(config, choice)); } if (opts?.secretInputMode === "ref") { if (keyConfigured) return await finalizeSelection(applySearchProviderSelection(config, choice)); const ref = buildSearchEnvRef(config, choice); await prompter.note([ "Secret references enabled — OpenClaw will store a reference instead of the API key.", `Env var: ${ref.id}${envAvailable ? " (detected)" : ""}.`, ...envAvailable ? [] : [`Set ${ref.id} in the Gateway environment.`], "Docs: https://docs.openclaw.ai/tools/web" ].join("\n"), "Web search"); return await finalizeSelection(applySearchKey(config, choice, ref)); } const keyInput = await prompter.text({ message: keyConfigured ? `${credentialLabel} (leave blank to keep current)` : envAvailable ? `${credentialLabel} (leave blank to use env var)` : credentialLabel, placeholder: keyConfigured ? "Leave blank to keep current" : entry.placeholder, sensitive: true }); const key = normalizeOptionalString(keyInput) ?? ""; if (key) return await finalizeSelection(applySearchKey(config, choice, resolveSearchSecretInput(config, choice, key, opts?.secretInputMode))); if (existingKey) return await finalizeSelection(applySearchKey(config, choice, existingKey)); if (keyConfigured || envAvailable) return await finalizeSelection(applySearchProviderSelection(config, choice)); await prompter.note([ `No ${credentialLabel} stored — web_search won't work until a key is available.`, `Get your key at: ${entry.signupUrl}`, "Docs: https://docs.openclaw.ai/tools/web" ].join("\n"), "Web search"); const search = { ...config.tools?.web?.search, enabled: false, provider: choice }; return completedSearchSetup(applySearchProviderSelectionConfig({ ...config, tools: { ...config.tools, web: { ...config.tools?.web, search } } }, entry)); } //#endregion export { applySearchKey, applySearchProviderSelection, hasExistingKey, hasKeyInEnv, listSearchProviderOptions, resolveExistingKey, resolveSearchProviderOptions, runSearchSetupFlow };