Merge de80b423944703501694fbfd2af037c099c8011e into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
ide-rea 2026-03-21 02:45:48 +00:00 committed by GitHub
commit 359581ddd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 401 additions and 11 deletions

4
.github/labeler.yml vendored
View File

@ -325,3 +325,7 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/fal/**"
"extensions: baidu":
- changed-files:
- any-glob-to-any-file:
- "extensions/baidu/**"

View File

@ -39,6 +39,7 @@ Scope intent:
- `plugins.entries.perplexity.config.webSearch.apiKey`
- `plugins.entries.firecrawl.config.webSearch.apiKey`
- `plugins.entries.tavily.config.webSearch.apiKey`
- `plugins.entries.baidu.config.webSearch.apiKey`
- `tools.web.search.apiKey`
- `tools.web.search.gemini.apiKey`
- `tools.web.search.grok.apiKey`

View File

@ -447,6 +447,13 @@
"secretShape": "secret_input",
"optIn": true
},
{
"id": "plugins.entries.baidu.config.webSearch.apiKey",
"configFile": "openclaw.json",
"path": "plugins.entries.baidu.config.webSearch.apiKey",
"secretShape": "secret_input",
"optIn": true
},
{
"id": "plugins.entries.brave.config.webSearch.apiKey",
"configFile": "openclaw.json",

View File

@ -11,7 +11,7 @@ title: "Web Tools"
OpenClaw ships two lightweight web tools:
- `web_search` — Search the web using Brave Search API, Firecrawl Search, Gemini with Google Search grounding, Grok, Kimi, Perplexity Search API, or Tavily Search API.
- `web_search` — Search the web using Baidu Search API, Brave Search API, Firecrawl Search, Gemini with Google Search grounding, Grok, Kimi, Perplexity Search API, or Tavily Search API.
- `web_fetch` — HTTP fetch + readable extraction (HTML → markdown/text).
These are **not** browser automation. For JS-heavy sites or logins, use the
@ -33,6 +33,7 @@ See [Brave Search setup](/tools/brave-search), [Perplexity Search setup](/tools/
| Provider | Result shape | Provider-specific filters | Notes | API key |
| ------------------------- | ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------- |
| **Baidu** | Structured results with snippets | — | Uses Baidu Search | `BAIDU_SEARCH_API_KEY` |
| **Brave Search API** | Structured results with snippets | `country`, `language`, `ui_lang`, time | Supports Brave `llm-context` mode | `BRAVE_API_KEY` |
| **Firecrawl Search** | Structured results with snippets | Use `firecrawl_search` for Firecrawl-specific search options | Best for pairing search with Firecrawl scraping/extraction | `FIRECRAWL_API_KEY` |
| **Gemini** | AI-synthesized answers + citations | — | Uses Google Search grounding | `GEMINI_API_KEY` |
@ -45,13 +46,14 @@ See [Brave Search setup](/tools/brave-search), [Perplexity Search setup](/tools/
The table above is alphabetical. If no `provider` is explicitly set, runtime auto-detection checks providers in this order:
1. **Brave**`BRAVE_API_KEY` env var or `plugins.entries.brave.config.webSearch.apiKey`
2. **Gemini**`GEMINI_API_KEY` env var or `plugins.entries.google.config.webSearch.apiKey`
3. **Grok**`XAI_API_KEY` env var or `plugins.entries.xai.config.webSearch.apiKey`
4. **Kimi**`KIMI_API_KEY` / `MOONSHOT_API_KEY` env var or `plugins.entries.moonshot.config.webSearch.apiKey`
5. **Perplexity**`PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `plugins.entries.perplexity.config.webSearch.apiKey`
6. **Firecrawl**`FIRECRAWL_API_KEY` env var or `plugins.entries.firecrawl.config.webSearch.apiKey`
7. **Tavily**`TAVILY_API_KEY` env var or `plugins.entries.tavily.config.webSearch.apiKey`
1. **Baidu**`BAIDU_SEARCH_API_KEY` env var or `plugins.entries.baidu.config.webSearch.apiKey`
2. **Brave**`BRAVE_API_KEY` env var or `plugins.entries.brave.config.webSearch.apiKey`
3. **Gemini**`GEMINI_API_KEY` env var or `plugins.entries.google.config.webSearch.apiKey`
4. **Grok**`XAI_API_KEY` env var or `plugins.entries.xai.config.webSearch.apiKey`
5. **Kimi**`KIMI_API_KEY` / `MOONSHOT_API_KEY` env var or `plugins.entries.moonshot.config.webSearch.apiKey`
6. **Perplexity**`PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `plugins.entries.perplexity.config.webSearch.apiKey`
7. **Firecrawl**`FIRECRAWL_API_KEY` env var or `plugins.entries.firecrawl.config.webSearch.apiKey`
8. **Tavily**`TAVILY_API_KEY` env var or `plugins.entries.tavily.config.webSearch.apiKey`
If no keys are found, it falls back to Brave (you'll get a missing-key error prompting you to configure one).
@ -65,6 +67,12 @@ Runtime SecretRef behavior:
Use `openclaw configure --section web` to set up your API key and choose a provider.
### Baidu Search
1. Visit the [Baidu AI Search Console](https://console.bce.baidu.com/ai-search/qianfan/ais/console/apiKey)
2. Generate a new API key or select an existing one(format: `bce-v3/ALTAK-...`)
3. Copy the API key and use it with OpenClaw
### Brave Search
1. Create a Brave Search API account at [brave.com/search/api](https://brave.com/search/api/)
@ -94,6 +102,7 @@ See [Perplexity Search API Docs](https://docs.perplexity.ai/guides/search-quicks
**Via config:** run `openclaw configure --section web`. It stores the key under the provider-specific config path:
- Baidu: `plugins.entries.baidu.config.webSearch.apiKey`
- Brave: `plugins.entries.brave.config.webSearch.apiKey`
- Firecrawl: `plugins.entries.firecrawl.config.webSearch.apiKey`
- Gemini: `plugins.entries.google.config.webSearch.apiKey`
@ -106,6 +115,7 @@ All of these fields also support SecretRef objects.
**Via environment:** set provider env vars in the Gateway process environment:
- Baidu: `BAIDU_SEARCH_API_KEY`
- Brave: `BRAVE_API_KEY`
- Firecrawl: `FIRECRAWL_API_KEY`
- Gemini: `GEMINI_API_KEY`
@ -118,6 +128,32 @@ For a gateway install, put these in `~/.openclaw/.env` (or your service environm
### Config examples
**Baidu Search:**
```json5
{
plugins: {
entries: {
baidu: {
config: {
webSearch: {
apiKey: "YOUR_BAIDU_API_KEY", // optional if BRAVE_API_KEY is set // pragma: allowlist secret
},
},
},
},
},
tools: {
web: {
search: {
enabled: true,
provider: "baidu",
},
},
},
}
```
**Brave Search:**
```json5
@ -355,6 +391,7 @@ Search the web using your configured provider.
- `tools.web.search.enabled` must not be `false` (default: enabled)
- API key for your chosen provider:
- **Baidu**: `BAIDU_SEARCH_API_KEY` or `plugins.entries.baidu.config.webSearch.apiKey`
- **Brave**: `BRAVE_API_KEY` or `plugins.entries.brave.config.webSearch.apiKey`
- **Firecrawl**: `FIRECRAWL_API_KEY` or `plugins.entries.firecrawl.config.webSearch.apiKey`
- **Gemini**: `GEMINI_API_KEY` or `plugins.entries.google.config.webSearch.apiKey`

11
extensions/baidu/index.ts Normal file
View File

@ -0,0 +1,11 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createBaiduWebSearchProvider } from "./src/baidu-web-search-provider.ts";
export default definePluginEntry({
id: "baidu",
name: "Baidu Plugin",
description: "Bundled Baidu plugin",
register(api) {
api.registerWebSearchProvider(createBaiduWebSearchProvider());
},
});

View File

@ -0,0 +1,29 @@
{
"id": "baidu",
"providerAuthEnvVars": {
"brave": ["BAIDU_SEARCH_API_KEY"]
},
"uiHints": {
"webSearch.apiKey": {
"label": "Baidu Search API Key",
"help": "Baidu Search API key (fallback: BAIDU_SEARCH_API_KEY env var).",
"sensitive": true,
"placeholder": "bce-..."
}
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"webSearch": {
"type": "object",
"additionalProperties": false,
"properties": {
"apiKey": {
"type": ["string", "object"]
}
}
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"name": "@openclaw/baidu-plugin",
"version": "2026.3.14",
"private": true,
"description": "OpenClaw Baidu plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@ -0,0 +1,259 @@
import { Type } from "@sinclair/typebox";
import {
buildSearchCacheKey,
DEFAULT_SEARCH_COUNT,
MAX_SEARCH_COUNT,
readCachedSearchPayload,
readConfiguredSecretString,
readNumberParam,
readProviderEnvValue,
readStringParam,
resolveSearchCacheTtlMs,
resolveSearchCount,
resolveSearchTimeoutSeconds,
resolveProviderWebSearchPluginConfig,
setProviderWebSearchPluginConfigValue,
type SearchConfigRecord,
type WebSearchProviderPlugin,
type WebSearchProviderToolDefinition,
withTrustedWebSearchEndpoint,
wrapWebContent,
writeCachedSearchPayload,
} from "openclaw/plugin-sdk/provider-web-search";
const BAIDU_SEARCH_API_ENDPOINT = "https://qianfan.baidubce.com/v2/ai_search/web_search";
type BaiduConfig = {
apiKey?: string;
};
type BaiduSearchResult = {
title?: string;
url?: string;
snippet?: string;
date?: string;
website?: string;
};
type BaiduSearchResponse = {
references?: BaiduSearchResult[];
};
function createBaiduSchema() {
return Type.Object({
query: Type.String({ description: "Search query string." }),
count: Type.Optional(
Type.Number({
description: "Number of results to return (1-20).",
minimum: 1,
maximum: MAX_SEARCH_COUNT,
}),
),
country: Type.Optional(Type.String({ description: "Not supported by Baidu." })),
language: Type.Optional(Type.String({ description: "Not supported by Baidu." })),
freshness: Type.Optional(Type.String({ description: "Not supported by Baidu." })),
date_after: Type.Optional(Type.String({ description: "Not supported by Baidu." })),
date_before: Type.Optional(Type.String({ description: "Not supported by Baidu." })),
});
}
function resolveBaiduConfig(searchConfig?: SearchConfigRecord): BaiduConfig {
const baidu = searchConfig?.baidu;
return baidu && typeof baidu === "object" && !Array.isArray(baidu) ? (baidu as BaiduConfig) : {};
}
function resolveBaiduApiKey(baidu?: BaiduConfig): string | undefined {
return (
readConfiguredSecretString(baidu?.apiKey, "tools.web.search.baidu.apiKey") ??
readProviderEnvValue(["BAIDU_SEARCH_API_KEY"])
);
}
async function runBaiduSearch(params: {
query: string;
apiKey: string;
timeoutSeconds: number;
count: number;
}): Promise<{ results: BaiduSearchResult[] }> {
const body: Record<string, unknown> = {
resource_type_filter: [
{ type: "web", top_k: params.count > 0 ? params.count : DEFAULT_SEARCH_COUNT },
],
messages: [
{
role: "user",
content: params.query,
},
],
};
return withTrustedWebSearchEndpoint(
{
url: BAIDU_SEARCH_API_ENDPOINT,
timeoutSeconds: params.timeoutSeconds,
init: {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${params.apiKey}`,
"X-Appbuilder-From": "openclaw",
},
body: JSON.stringify(body),
},
},
async (res) => {
if (!res.ok) {
const detail = await res.text();
throw new Error(`Baidu API error (${res.status}): ${detail || res.statusText}`);
}
const data = (await res.json()) as BaiduSearchResponse;
const results = Array.isArray(data.references) ? data.references : [];
const mapped = results.map((entry) => {
const snippet = entry.snippet ?? "";
const title = entry.title ?? "";
const url = entry.url ?? "";
const site = entry.website ?? "";
return {
title: title ? wrapWebContent(title, "web_search") : "",
url, // Keep raw for tool chaining
snippet: snippet ? wrapWebContent(snippet, "web_search") : "",
date: entry.date || undefined,
site: site,
};
});
return { results: mapped };
},
);
}
function createBaiduToolDefinition(
searchConfig?: SearchConfigRecord,
): WebSearchProviderToolDefinition {
return {
description: "Search the web using Baidu Search",
parameters: createBaiduSchema(),
execute: async (args) => {
const params = args as Record<string, unknown>;
for (const name of ["country", "language", "freshness", "date_after", "date_before"]) {
if (readStringParam(params, name)) {
const label =
name === "country"
? "country filtering"
: name === "language"
? "language filtering"
: name === "freshness"
? "freshness filtering"
: "date_after/date_before filtering";
return {
error: name.startsWith("date_") ? "unsupported_date_filter" : `unsupported_${name}`,
message: `${label} is not supported by the baidu provider. Only Brave and Perplexity support ${name === "country" ? "country filtering" : name === "language" ? "language filtering" : name === "freshness" ? "freshness" : "date filtering"}.`,
docs: "https://docs.openclaw.ai/tools/web",
};
}
}
const baiduConfig = resolveBaiduConfig(searchConfig);
const apiKey = resolveBaiduApiKey(baiduConfig);
if (!apiKey) {
return {
error: "missing_baidu_search_api_key",
message:
"web_search (baidu) needs a Baidu Search API key. Set BAIDU_SEARCH_API_KEY in the Gateway environment, or configure plugins.entries.baidu.config.webSearch.apiKey.",
docs: "https://docs.openclaw.ai/tools/web",
};
}
const query = readStringParam(params, "query", { required: true });
const count =
readNumberParam(params, "count", { integer: true }) ??
searchConfig?.maxResults ??
undefined;
const cacheKey = buildSearchCacheKey([
"baidu",
query,
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
]);
const cached = readCachedSearchPayload(cacheKey);
if (cached) {
return cached;
}
const start = Date.now();
const { results } = await runBaiduSearch({
query,
apiKey,
timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig),
count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
});
const payload = {
query: params.query,
provider: "baidu",
tookMs: Date.now() - start,
externalContent: {
untrusted: true,
source: "web_search",
provider: "baidu",
wrapped: true,
},
results: results,
};
writeCachedSearchPayload(cacheKey, payload, resolveSearchCacheTtlMs(searchConfig));
return payload;
},
};
}
export function createBaiduWebSearchProvider(): WebSearchProviderPlugin {
return {
id: "baidu",
label: "Baidu Search",
hint: "Structured results",
envVars: ["BAIDU_SEARCH_API_KEY"],
placeholder: "bce...",
signupUrl: "https://console.bce.baidu.com/ai-search/qianfan/ais/console/apiKey",
docsUrl: "https://docs.openclaw.ai/tools/web",
autoDetectOrder: 5,
credentialPath: "plugins.entries.baidu.config.webSearch.apiKey",
inactiveSecretPaths: ["plugins.entries.baidu.config.webSearch.apiKey"],
getCredentialValue: (searchConfig) => {
const baidu = searchConfig?.baidu;
return baidu && typeof baidu === "object" && !Array.isArray(baidu)
? (baidu as Record<string, unknown>).apiKey
: undefined;
},
setCredentialValue: (searchConfigTarget, value) => {
const scoped = searchConfigTarget.baidu;
if (!scoped || typeof scoped !== "object" || Array.isArray(scoped)) {
searchConfigTarget.baidu = { apiKey: value };
return;
}
(scoped as Record<string, unknown>).apiKey = value;
},
getConfiguredCredentialValue: (config) =>
resolveProviderWebSearchPluginConfig(config, "baidu")?.apiKey,
setConfiguredCredentialValue: (configTarget, value) => {
setProviderWebSearchPluginConfigValue(configTarget, "baidu", "apiKey", value);
},
createTool: (ctx) =>
createBaiduToolDefinition(
(() => {
const searchConfig = ctx.searchConfig as SearchConfigRecord | undefined;
const pluginConfig = resolveProviderWebSearchPluginConfig(ctx.config, "baidu");
if (!pluginConfig) {
return searchConfig;
}
return {
...(searchConfig ?? {}),
baidu: {
...resolveBaiduConfig(searchConfig),
...pluginConfig,
},
} as SearchConfigRecord;
})(),
),
};
}
export const __testing = {
resolveBaiduConfig,
resolveBaiduApiKey,
runBaiduSearch,
} as const;

2
pnpm-lock.yaml generated
View File

@ -245,6 +245,8 @@ importers:
extensions/anthropic: {}
extensions/baidu: {}
extensions/bluebubbles:
dependencies:
zod:

View File

@ -12,6 +12,7 @@ const GENERIC_WEB_SEARCH_KEYS = new Set([
]);
const LEGACY_PROVIDER_MAP = {
baidu: "baidu",
brave: "brave",
firecrawl: "firecrawl",
gemini: "google",
@ -213,7 +214,14 @@ function normalizeLegacyWebSearchConfigRecord<T extends JsonRecord>(
});
}
for (const providerId of ["firecrawl", "gemini", "grok", "kimi", "perplexity"] as const) {
for (const providerId of [
"baidu",
"firecrawl",
"gemini",
"grok",
"kimi",
"perplexity",
] as const) {
const scoped = copyLegacyProviderConfig(search, providerId);
if (!scoped || Object.keys(scoped).length === 0) {
continue;

View File

@ -66,6 +66,7 @@ describe("bundled web search metadata", () => {
it("keeps bundled web search compat ids aligned with bundled manifests", () => {
expect(resolveBundledWebSearchPluginIds({})).toEqual([
"baidu",
"brave",
"firecrawl",
"google",

View File

@ -6,6 +6,7 @@ describe("resolveBundledPluginWebSearchProviders", () => {
const providers = resolveBundledPluginWebSearchProviders({});
expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([
"baidu:baidu",
"brave:brave",
"google:gemini",
"xai:grok",
@ -15,6 +16,7 @@ describe("resolveBundledPluginWebSearchProviders", () => {
"tavily:tavily",
]);
expect(providers.map((provider) => provider.credentialPath)).toEqual([
"plugins.entries.baidu.config.webSearch.apiKey",
"plugins.entries.brave.config.webSearch.apiKey",
"plugins.entries.google.config.webSearch.apiKey",
"plugins.entries.xai.config.webSearch.apiKey",
@ -42,6 +44,7 @@ describe("resolveBundledPluginWebSearchProviders", () => {
});
expect(providers.map((provider) => provider.pluginId)).toEqual([
"baidu",
"brave",
"google",
"xai",
@ -96,6 +99,7 @@ describe("resolveBundledPluginWebSearchProviders", () => {
});
expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([
"baidu:baidu",
"brave:brave",
"google:gemini",
"xai:grok",

View File

@ -23,7 +23,7 @@ vi.mock("../plugins/web-search-providers.runtime.js", () => ({
}));
function createTestProvider(params: {
id: "brave" | "gemini" | "grok" | "kimi" | "perplexity" | "firecrawl" | "tavily";
id: "baidu" | "brave" | "gemini" | "grok" | "kimi" | "perplexity" | "firecrawl" | "tavily";
pluginId: string;
order: number;
}): PluginWebSearchProviderEntry {
@ -49,7 +49,7 @@ function createTestProvider(params: {
getCredentialValue: readSearchConfigKey,
setCredentialValue: (searchConfigTarget, value) => {
const providerConfig =
params.id === "brave" || params.id === "firecrawl"
params.id === "brave" || params.id === "firecrawl" || params.id === "baidu"
? searchConfigTarget
: ((searchConfigTarget[params.id] ??= {}) as { apiKey?: unknown });
providerConfig.apiKey = value;
@ -77,6 +77,7 @@ function createTestProvider(params: {
function buildTestWebSearchProviders(): PluginWebSearchProviderEntry[] {
return [
createTestProvider({ id: "baidu", pluginId: "baidu", order: 5 }),
createTestProvider({ id: "brave", pluginId: "brave", order: 10 }),
createTestProvider({ id: "gemini", pluginId: "google", order: 20 }),
createTestProvider({ id: "grok", pluginId: "xai", order: 30 }),
@ -168,6 +169,9 @@ function buildConfigForOpenClawTarget(entry: SecretRegistryEntry, envId: string)
"webhook",
);
}
if (entry.id === "plugins.entries.baidu.config.webSearch.apiKey") {
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "baidu");
}
if (entry.id === "plugins.entries.brave.config.webSearch.apiKey") {
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "brave");
}

View File

@ -733,6 +733,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
includeInConfigure: true,
includeInAudit: true,
},
{
id: "plugins.entries.baidu.config.webSearch.apiKey",
targetType: "plugins.entries.baidu.config.webSearch.apiKey",
configFile: "openclaw.json",
pathPattern: "plugins.entries.baidu.config.webSearch.apiKey",
secretShape: SECRET_INPUT_SHAPE,
expectedResolvedValue: "string",
includeInPlan: true,
includeInConfigure: true,
includeInAudit: true,
},
{
id: "plugins.entries.brave.config.webSearch.apiKey",
targetType: "plugins.entries.brave.config.webSearch.apiKey",