feat(onboarding): add web search to onboarding flow (#34009)

* add web search to onboarding flow

* remove post onboarding step (now redundant)

* post-onboarding nudge if no web search set up

* address comments

* fix test mocking

* add enabled: false assertion to the no-key test

* --skip-search cli flag

* use provider that a user has a key for

* add assertions, replace the duplicated switch blocks

* test for quickstart fast-path with existing config key

* address comments

* cover quickstart falls through to key test

* bring back key source

* normalize secret inputs instead of direct string trimming

* preserve enabled: false if it's already set

* handle missing API keys in flow

* doc updates

* hasExistingKey to detect both plaintext strings and SecretRef objects

* preserve enabled state only on the "keep current" paths

* add test for preserving

* better gate flows

* guard against invalid provider values in config

* Update src/commands/configure.wizard.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* format fix

* only mentions env var when it's actually available

* search apiKey fields now typed as SecretInput

* if no provider check if any search provider key is detectable

* handle both kimi keys

* remove .filter(Boolean)

* do not disable web_search after user enables it

* update resolveSearchProvider

* fix(onboarding): skip search key prompt in ref mode

* fix: add onboarding web search step (#34009) (thanks @kesku)

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Shadow <hi@shadowing.dev>
This commit is contained in:
Kesku
2026-03-06 19:09:00 +00:00
committed by GitHub
parent 9a1a63a667
commit 3d7bc5958d
16 changed files with 829 additions and 151 deletions

View File

@@ -166,18 +166,35 @@ async function promptWebToolsConfig(
): Promise<OpenClawConfig> {
const existingSearch = nextConfig.tools?.web?.search;
const existingFetch = nextConfig.tools?.web?.fetch;
const existingProvider = existingSearch?.provider ?? "brave";
const hasPerplexityKey = Boolean(
existingSearch?.perplexity?.apiKey || process.env.PERPLEXITY_API_KEY,
);
const hasBraveKey = Boolean(existingSearch?.apiKey || process.env.BRAVE_API_KEY);
const hasSearchKey = existingProvider === "perplexity" ? hasPerplexityKey : hasBraveKey;
const {
SEARCH_PROVIDER_OPTIONS,
resolveExistingKey,
hasExistingKey,
applySearchKey,
hasKeyInEnv,
} = await import("./onboard-search.js");
type SP = (typeof SEARCH_PROVIDER_OPTIONS)[number]["value"];
const hasKeyForProvider = (provider: string): boolean => {
const entry = SEARCH_PROVIDER_OPTIONS.find((e) => e.value === provider);
if (!entry) {
return false;
}
return hasExistingKey(nextConfig, provider as SP) || hasKeyInEnv(entry);
};
const existingProvider: string = (() => {
const stored = existingSearch?.provider;
if (stored && SEARCH_PROVIDER_OPTIONS.some((e) => e.value === stored)) {
return stored;
}
return SEARCH_PROVIDER_OPTIONS.find((e) => hasKeyForProvider(e.value))?.value ?? "perplexity";
})();
note(
[
"Web search lets your agent look things up online using the `web_search` tool.",
"Choose a provider: Perplexity Search (recommended) or Brave Search.",
"Both return structured results (title, URL, snippet) for fast research.",
"Choose a provider and paste your API key.",
"Docs: https://docs.openclaw.ai/tools/web",
].join("\n"),
"Web search",
@@ -186,30 +203,31 @@ async function promptWebToolsConfig(
const enableSearch = guardCancel(
await confirm({
message: "Enable web_search?",
initialValue: existingSearch?.enabled ?? hasSearchKey,
initialValue:
existingSearch?.enabled ?? SEARCH_PROVIDER_OPTIONS.some((e) => hasKeyForProvider(e.value)),
}),
runtime,
);
let nextSearch = {
let nextSearch: Record<string, unknown> = {
...existingSearch,
enabled: enableSearch,
};
if (enableSearch) {
const providerOptions = SEARCH_PROVIDER_OPTIONS.map((entry) => {
const configured = hasKeyForProvider(entry.value);
return {
value: entry.value,
label: entry.label,
hint: configured ? `${entry.hint} · configured` : entry.hint,
};
});
const providerChoice = guardCancel(
await select({
message: "Choose web search provider",
options: [
{
value: "perplexity",
label: "Perplexity Search",
},
{
value: "brave",
label: "Brave Search",
},
],
options: providerOptions,
initialValue: existingProvider,
}),
runtime,
@@ -217,59 +235,42 @@ async function promptWebToolsConfig(
nextSearch = { ...nextSearch, provider: providerChoice };
if (providerChoice === "perplexity") {
const hasKey = Boolean(existingSearch?.perplexity?.apiKey);
const keyInput = guardCancel(
await text({
message: hasKey
? "Perplexity API key (leave blank to keep current or use PERPLEXITY_API_KEY)"
: "Perplexity API key (paste it here; leave blank to use PERPLEXITY_API_KEY)",
placeholder: hasKey ? "Leave blank to keep current" : "pplx-...",
}),
runtime,
);
const key = String(keyInput ?? "").trim();
if (key) {
nextSearch = {
...nextSearch,
perplexity: { ...existingSearch?.perplexity, apiKey: key },
};
} else if (!hasKey && !process.env.PERPLEXITY_API_KEY) {
note(
[
"No key stored yet, so web_search will stay unavailable.",
"Store a key here or set PERPLEXITY_API_KEY in the Gateway environment.",
"Get your API key at: https://www.perplexity.ai/settings/api",
"Docs: https://docs.openclaw.ai/tools/web",
].join("\n"),
"Web search",
);
}
const entry = SEARCH_PROVIDER_OPTIONS.find((e) => e.value === providerChoice)!;
const existingKey = resolveExistingKey(nextConfig, providerChoice as SP);
const keyConfigured = hasExistingKey(nextConfig, providerChoice as SP);
const envAvailable = entry.envKeys.some((k) => Boolean(process.env[k]?.trim()));
const envVarNames = entry.envKeys.join(" / ");
const keyInput = guardCancel(
await text({
message: keyConfigured
? envAvailable
? `${entry.label} API key (leave blank to keep current or use ${envVarNames})`
: `${entry.label} API key (leave blank to keep current)`
: envAvailable
? `${entry.label} API key (paste it here; leave blank to use ${envVarNames})`
: `${entry.label} API key`,
placeholder: keyConfigured ? "Leave blank to keep current" : entry.placeholder,
}),
runtime,
);
const key = String(keyInput ?? "").trim();
if (key || existingKey) {
const applied = applySearchKey(nextConfig, providerChoice as SP, (key || existingKey)!);
nextSearch = { ...applied.tools?.web?.search };
} else if (keyConfigured || envAvailable) {
nextSearch = { ...nextSearch };
} else {
const hasKey = Boolean(existingSearch?.apiKey);
const keyInput = guardCancel(
await text({
message: hasKey
? "Brave Search API key (leave blank to keep current or use BRAVE_API_KEY)"
: "Brave Search API key (paste it here; leave blank to use BRAVE_API_KEY)",
placeholder: hasKey ? "Leave blank to keep current" : "BSA...",
}),
runtime,
note(
[
"No key stored yet — web_search won't work until a key is available.",
`Store a key here or set ${envVarNames} in the Gateway environment.`,
`Get your API key at: ${entry.signupUrl}`,
"Docs: https://docs.openclaw.ai/tools/web",
].join("\n"),
"Web search",
);
const key = String(keyInput ?? "").trim();
if (key) {
nextSearch = { ...nextSearch, apiKey: key };
} else if (!hasKey && !process.env.BRAVE_API_KEY) {
note(
[
"No key stored yet, so web_search will stay unavailable.",
"Store a key here or set BRAVE_API_KEY in the Gateway environment.",
"Get your API key at: https://brave.com/search/api/",
"Docs: https://docs.openclaw.ai/tools/web",
].join("\n"),
"Web search",
);
}
}
}

View File

@@ -0,0 +1,279 @@
import { describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { SEARCH_PROVIDER_OPTIONS, setupSearch } from "./onboard-search.js";
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: ((code: number) => {
throw new Error(`unexpected exit ${code}`);
}) as RuntimeEnv["exit"],
};
function createPrompter(params: { selectValue?: string; textValue?: string }): {
prompter: WizardPrompter;
notes: Array<{ title?: string; message: string }>;
} {
const notes: Array<{ title?: string; message: string }> = [];
const prompter: WizardPrompter = {
intro: vi.fn(async () => {}),
outro: vi.fn(async () => {}),
note: vi.fn(async (message: string, title?: string) => {
notes.push({ title, message });
}),
select: vi.fn(
async () => params.selectValue ?? "perplexity",
) as unknown as WizardPrompter["select"],
multiselect: vi.fn(async () => []) as unknown as WizardPrompter["multiselect"],
text: vi.fn(async () => params.textValue ?? ""),
confirm: vi.fn(async () => true),
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
};
return { prompter, notes };
}
describe("setupSearch", () => {
it("returns config unchanged when user skips", async () => {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({ selectValue: "__skip__" });
const result = await setupSearch(cfg, runtime, prompter);
expect(result).toBe(cfg);
});
it("sets provider and key for perplexity", async () => {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({
selectValue: "perplexity",
textValue: "pplx-test-key",
});
const result = await setupSearch(cfg, runtime, prompter);
expect(result.tools?.web?.search?.provider).toBe("perplexity");
expect(result.tools?.web?.search?.perplexity?.apiKey).toBe("pplx-test-key");
expect(result.tools?.web?.search?.enabled).toBe(true);
});
it("sets provider and key for brave", async () => {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({
selectValue: "brave",
textValue: "BSA-test-key",
});
const result = await setupSearch(cfg, runtime, prompter);
expect(result.tools?.web?.search?.provider).toBe("brave");
expect(result.tools?.web?.search?.enabled).toBe(true);
expect(result.tools?.web?.search?.apiKey).toBe("BSA-test-key");
});
it("sets provider and key for gemini", async () => {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({
selectValue: "gemini",
textValue: "AIza-test",
});
const result = await setupSearch(cfg, runtime, prompter);
expect(result.tools?.web?.search?.provider).toBe("gemini");
expect(result.tools?.web?.search?.enabled).toBe(true);
expect(result.tools?.web?.search?.gemini?.apiKey).toBe("AIza-test");
});
it("sets provider and key for grok", async () => {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({
selectValue: "grok",
textValue: "xai-test",
});
const result = await setupSearch(cfg, runtime, prompter);
expect(result.tools?.web?.search?.provider).toBe("grok");
expect(result.tools?.web?.search?.enabled).toBe(true);
expect(result.tools?.web?.search?.grok?.apiKey).toBe("xai-test");
});
it("sets provider and key for kimi", async () => {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({
selectValue: "kimi",
textValue: "sk-moonshot",
});
const result = await setupSearch(cfg, runtime, prompter);
expect(result.tools?.web?.search?.provider).toBe("kimi");
expect(result.tools?.web?.search?.enabled).toBe(true);
expect(result.tools?.web?.search?.kimi?.apiKey).toBe("sk-moonshot");
});
it("shows missing-key note when no key is provided and no env var", async () => {
const cfg: OpenClawConfig = {};
const { prompter, notes } = createPrompter({
selectValue: "brave",
textValue: "",
});
const result = await setupSearch(cfg, runtime, prompter);
expect(result.tools?.web?.search?.provider).toBe("brave");
expect(result.tools?.web?.search?.enabled).toBeUndefined();
const missingNote = notes.find((n) => n.message.includes("No API key stored"));
expect(missingNote).toBeDefined();
});
it("keeps existing key when user leaves input blank", async () => {
const cfg: OpenClawConfig = {
tools: {
web: {
search: {
provider: "perplexity",
perplexity: { apiKey: "existing-key" },
},
},
},
};
const { prompter } = createPrompter({
selectValue: "perplexity",
textValue: "",
});
const result = await setupSearch(cfg, runtime, prompter);
expect(result.tools?.web?.search?.perplexity?.apiKey).toBe("existing-key");
expect(result.tools?.web?.search?.enabled).toBe(true);
});
it("advanced preserves enabled:false when keeping existing key", async () => {
const cfg: OpenClawConfig = {
tools: {
web: {
search: {
provider: "perplexity",
enabled: false,
perplexity: { apiKey: "existing-key" },
},
},
},
};
const { prompter } = createPrompter({
selectValue: "perplexity",
textValue: "",
});
const result = await setupSearch(cfg, runtime, prompter);
expect(result.tools?.web?.search?.perplexity?.apiKey).toBe("existing-key");
expect(result.tools?.web?.search?.enabled).toBe(false);
});
it("quickstart skips key prompt when config key exists", async () => {
const cfg: OpenClawConfig = {
tools: {
web: {
search: {
provider: "perplexity",
perplexity: { apiKey: "stored-pplx-key" },
},
},
},
};
const { prompter } = createPrompter({ selectValue: "perplexity" });
const result = await setupSearch(cfg, runtime, prompter, {
quickstartDefaults: true,
});
expect(result.tools?.web?.search?.provider).toBe("perplexity");
expect(result.tools?.web?.search?.perplexity?.apiKey).toBe("stored-pplx-key");
expect(result.tools?.web?.search?.enabled).toBe(true);
expect(prompter.text).not.toHaveBeenCalled();
});
it("quickstart preserves enabled:false when search was intentionally disabled", async () => {
const cfg: OpenClawConfig = {
tools: {
web: {
search: {
provider: "perplexity",
enabled: false,
perplexity: { apiKey: "stored-pplx-key" },
},
},
},
};
const { prompter } = createPrompter({ selectValue: "perplexity" });
const result = await setupSearch(cfg, runtime, prompter, {
quickstartDefaults: true,
});
expect(result.tools?.web?.search?.provider).toBe("perplexity");
expect(result.tools?.web?.search?.perplexity?.apiKey).toBe("stored-pplx-key");
expect(result.tools?.web?.search?.enabled).toBe(false);
expect(prompter.text).not.toHaveBeenCalled();
});
it("quickstart falls through to key prompt when no key and no env var", async () => {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({ selectValue: "grok", textValue: "" });
const result = await setupSearch(cfg, runtime, prompter, {
quickstartDefaults: true,
});
expect(prompter.text).toHaveBeenCalled();
expect(result.tools?.web?.search?.provider).toBe("grok");
expect(result.tools?.web?.search?.enabled).toBeUndefined();
});
it("quickstart skips key prompt when env var is available", async () => {
const orig = process.env.BRAVE_API_KEY;
process.env.BRAVE_API_KEY = "env-brave-key";
try {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({ selectValue: "brave" });
const result = await setupSearch(cfg, runtime, prompter, {
quickstartDefaults: true,
});
expect(result.tools?.web?.search?.provider).toBe("brave");
expect(result.tools?.web?.search?.enabled).toBe(true);
expect(prompter.text).not.toHaveBeenCalled();
} finally {
if (orig === undefined) {
delete process.env.BRAVE_API_KEY;
} else {
process.env.BRAVE_API_KEY = orig;
}
}
});
it("stores env-backed SecretRef when secretInputMode=ref for perplexity", async () => {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({ selectValue: "perplexity" });
const result = await setupSearch(cfg, runtime, prompter, {
secretInputMode: "ref",
});
expect(result.tools?.web?.search?.provider).toBe("perplexity");
expect(result.tools?.web?.search?.perplexity?.apiKey).toEqual({
source: "env",
provider: "default",
id: "PERPLEXITY_API_KEY",
});
expect(prompter.text).not.toHaveBeenCalled();
});
it("stores env-backed SecretRef when secretInputMode=ref for brave", async () => {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({ selectValue: "brave" });
const result = await setupSearch(cfg, runtime, prompter, {
secretInputMode: "ref",
});
expect(result.tools?.web?.search?.provider).toBe("brave");
expect(result.tools?.web?.search?.apiKey).toEqual({
source: "env",
provider: "default",
id: "BRAVE_API_KEY",
});
expect(prompter.text).not.toHaveBeenCalled();
});
it("stores plaintext key when secretInputMode is unset", async () => {
const cfg: OpenClawConfig = {};
const { prompter } = createPrompter({
selectValue: "brave",
textValue: "BSA-plain",
});
const result = await setupSearch(cfg, runtime, prompter);
expect(result.tools?.web?.search?.apiKey).toBe("BSA-plain");
});
it("exports all 5 providers in SEARCH_PROVIDER_OPTIONS", () => {
expect(SEARCH_PROVIDER_OPTIONS).toHaveLength(5);
const values = SEARCH_PROVIDER_OPTIONS.map((e) => e.value);
expect(values).toEqual(["perplexity", "brave", "gemini", "grok", "kimi"]);
});
});

View File

@@ -0,0 +1,319 @@
import type { OpenClawConfig } from "../config/config.js";
import {
DEFAULT_SECRET_PROVIDER_ALIAS,
type SecretInput,
type SecretRef,
hasConfiguredSecretInput,
normalizeSecretInputString,
} from "../config/types.secrets.js";
import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import type { SecretInputMode } from "./onboard-types.js";
export type SearchProvider = "perplexity" | "brave" | "gemini" | "grok" | "kimi";
type SearchProviderEntry = {
value: SearchProvider;
label: string;
hint: string;
envKeys: string[];
placeholder: string;
signupUrl: string;
};
export const SEARCH_PROVIDER_OPTIONS: readonly SearchProviderEntry[] = [
{
value: "perplexity",
label: "Perplexity Search",
hint: "Structured results · domain/language/freshness filters",
envKeys: ["PERPLEXITY_API_KEY"],
placeholder: "pplx-...",
signupUrl: "https://www.perplexity.ai/settings/api",
},
{
value: "brave",
label: "Brave Search",
hint: "Structured results · region-specific",
envKeys: ["BRAVE_API_KEY"],
placeholder: "BSA...",
signupUrl: "https://brave.com/search/api/",
},
{
value: "gemini",
label: "Gemini (Google Search)",
hint: "Google Search grounding · AI-synthesized",
envKeys: ["GEMINI_API_KEY"],
placeholder: "AIza...",
signupUrl: "https://aistudio.google.com/apikey",
},
{
value: "grok",
label: "Grok (xAI)",
hint: "xAI web-grounded responses",
envKeys: ["XAI_API_KEY"],
placeholder: "xai-...",
signupUrl: "https://console.x.ai/",
},
{
value: "kimi",
label: "Kimi (Moonshot)",
hint: "Moonshot web search",
envKeys: ["KIMI_API_KEY", "MOONSHOT_API_KEY"],
placeholder: "sk-...",
signupUrl: "https://platform.moonshot.cn/",
},
] as const;
export function hasKeyInEnv(entry: SearchProviderEntry): boolean {
return entry.envKeys.some((k) => Boolean(process.env[k]?.trim()));
}
function rawKeyValue(config: OpenClawConfig, provider: SearchProvider): unknown {
const search = config.tools?.web?.search;
switch (provider) {
case "brave":
return search?.apiKey;
case "perplexity":
return search?.perplexity?.apiKey;
case "gemini":
return search?.gemini?.apiKey;
case "grok":
return search?.grok?.apiKey;
case "kimi":
return search?.kimi?.apiKey;
}
}
/** Returns the plaintext key string, or undefined for SecretRefs/missing. */
export function resolveExistingKey(
config: OpenClawConfig,
provider: SearchProvider,
): string | undefined {
return normalizeSecretInputString(rawKeyValue(config, provider));
}
/** Returns true if a key is configured (plaintext string or SecretRef). */
export function hasExistingKey(config: OpenClawConfig, provider: SearchProvider): boolean {
return hasConfiguredSecretInput(rawKeyValue(config, provider));
}
/** Build an env-backed SecretRef for a search provider. */
function buildSearchEnvRef(provider: SearchProvider): SecretRef {
const entry = SEARCH_PROVIDER_OPTIONS.find((e) => e.value === provider);
const envVar = entry?.envKeys.find((k) => Boolean(process.env[k]?.trim())) ?? entry?.envKeys[0];
if (!envVar) {
throw new Error(
`No env var mapping for search provider "${provider}" in secret-input-mode=ref.`,
);
}
return { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id: envVar };
}
/** Resolve a plaintext key into the appropriate SecretInput based on mode. */
function resolveSearchSecretInput(
provider: SearchProvider,
key: string,
secretInputMode?: SecretInputMode,
): SecretInput {
if (secretInputMode === "ref") {
return buildSearchEnvRef(provider);
}
return key;
}
export function applySearchKey(
config: OpenClawConfig,
provider: SearchProvider,
key: SecretInput,
): OpenClawConfig {
const search = { ...config.tools?.web?.search, provider, enabled: true };
switch (provider) {
case "brave":
search.apiKey = key;
break;
case "perplexity":
search.perplexity = { ...search.perplexity, apiKey: key };
break;
case "gemini":
search.gemini = { ...search.gemini, apiKey: key };
break;
case "grok":
search.grok = { ...search.grok, apiKey: key };
break;
case "kimi":
search.kimi = { ...search.kimi, apiKey: key };
break;
}
return {
...config,
tools: {
...config.tools,
web: { ...config.tools?.web, search },
},
};
}
function applyProviderOnly(config: OpenClawConfig, provider: SearchProvider): OpenClawConfig {
return {
...config,
tools: {
...config.tools,
web: {
...config.tools?.web,
search: {
...config.tools?.web?.search,
provider,
enabled: true,
},
},
},
};
}
function preserveDisabledState(original: OpenClawConfig, result: OpenClawConfig): OpenClawConfig {
if (original.tools?.web?.search?.enabled !== false) {
return result;
}
return {
...result,
tools: {
...result.tools,
web: { ...result.tools?.web, search: { ...result.tools?.web?.search, enabled: false } },
},
};
}
export type SetupSearchOptions = {
quickstartDefaults?: boolean;
secretInputMode?: SecretInputMode;
};
export async function setupSearch(
config: OpenClawConfig,
_runtime: RuntimeEnv,
prompter: WizardPrompter,
opts?: SetupSearchOptions,
): Promise<OpenClawConfig> {
await prompter.note(
[
"Web search lets your agent look things up online.",
"Choose a provider and paste your API key.",
"Docs: https://docs.openclaw.ai/tools/web",
].join("\n"),
"Web search",
);
const existingProvider = config.tools?.web?.search?.provider;
const options = SEARCH_PROVIDER_OPTIONS.map((entry) => {
const configured = hasExistingKey(config, entry.value) || hasKeyInEnv(entry);
const hint = configured ? `${entry.hint} · configured` : entry.hint;
return { value: entry.value, label: entry.label, hint };
});
const defaultProvider: SearchProvider = (() => {
if (existingProvider && SEARCH_PROVIDER_OPTIONS.some((e) => e.value === existingProvider)) {
return existingProvider;
}
const detected = SEARCH_PROVIDER_OPTIONS.find(
(e) => hasExistingKey(config, e.value) || hasKeyInEnv(e),
);
if (detected) {
return detected.value;
}
return "perplexity";
})();
type PickerValue = SearchProvider | "__skip__";
const choice = await prompter.select<PickerValue>({
message: "Search provider",
options: [
...options,
{
value: "__skip__" as const,
label: "Skip for now",
hint: "Configure later with openclaw configure --section web",
},
],
initialValue: defaultProvider as PickerValue,
});
if (choice === "__skip__") {
return config;
}
const entry = SEARCH_PROVIDER_OPTIONS.find((e) => e.value === choice)!;
const existingKey = resolveExistingKey(config, choice);
const keyConfigured = hasExistingKey(config, choice);
const envAvailable = hasKeyInEnv(entry);
if (opts?.quickstartDefaults && (keyConfigured || envAvailable)) {
const result = existingKey
? applySearchKey(config, choice, existingKey)
: applyProviderOnly(config, choice);
return preserveDisabledState(config, result);
}
if (opts?.secretInputMode === "ref") {
if (keyConfigured) {
return preserveDisabledState(config, applyProviderOnly(config, choice));
}
const ref = buildSearchEnvRef(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 applySearchKey(config, choice, ref);
}
const keyInput = await prompter.text({
message: keyConfigured
? `${entry.label} API key (leave blank to keep current)`
: envAvailable
? `${entry.label} API key (leave blank to use env var)`
: `${entry.label} API key`,
placeholder: keyConfigured ? "Leave blank to keep current" : entry.placeholder,
});
const key = keyInput?.trim() ?? "";
if (key) {
const secretInput = resolveSearchSecretInput(choice, key, opts?.secretInputMode);
return applySearchKey(config, choice, secretInput);
}
if (existingKey) {
return preserveDisabledState(config, applySearchKey(config, choice, existingKey));
}
if (keyConfigured || envAvailable) {
return preserveDisabledState(config, applyProviderOnly(config, choice));
}
await prompter.note(
[
"No API key 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",
);
return {
...config,
tools: {
...config.tools,
web: {
...config.tools?.web,
search: {
...config.tools?.web?.search,
provider: choice,
},
},
},
};
}

View File

@@ -154,6 +154,7 @@ export type OnboardOptions = {
/** @deprecated Legacy alias for `skipChannels`. */
skipProviders?: boolean;
skipSkills?: boolean;
skipSearch?: boolean;
skipHealth?: boolean;
skipUi?: boolean;
nodeManager?: NodeManagerChoice;