From cbf7330d0cf2b51556bb98613a199d653b6744f6 Mon Sep 17 00:00:00 2001 From: NiTingKY <2514656615@qq.com> Date: Wed, 3 Jun 2026 17:35:08 +0800 Subject: [PATCH] Add Bocha web search provider --- README.md | 3 + README.zh_cn.md | 5 +- electron/search_engines/bocha.test.ts | 48 ++++++++++++ electron/search_engines/bocha.ts | 78 +++++++++++++++++++ electron/search_engines/search.ts | 22 +++--- .../components/GeneralSettings.vue | 15 ++-- 6 files changed, 153 insertions(+), 18 deletions(-) create mode 100644 electron/search_engines/bocha.test.ts create mode 100644 electron/search_engines/bocha.ts diff --git a/README.md b/README.md index 0b67dc9..578dbd9 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ A user-friendly AI assistant software that supports local AI models, APIs, and k - Supports web search ![Web search](.github/assets/img/6_en.png) + To use Bocha as the default web search engine, set `BOCHA_SEARCH_API_KEY` in the runtime environment. + Optional variables: `BOCHA_SEARCH_API_URL` and `BOCHA_SEARCH_RESULT_COUNT`. + - Supports server-side deployment - MCP Client diff --git a/README.zh_cn.md b/README.zh_cn.md index b27e9f0..6905b4c 100644 --- a/README.zh_cn.md +++ b/README.zh_cn.md @@ -26,6 +26,9 @@ AingDesk是一款简单好用的AI助手,支持知识库、模型API、分享 - 支持联网搜索 ![联网搜索](.github/assets/img/6_zh.png) + 使用 Bocha 作为默认联网搜索引擎时,请在运行环境中设置 `BOCHA_SEARCH_API_KEY`。 + 可选环境变量:`BOCHA_SEARCH_API_URL`、`BOCHA_SEARCH_RESULT_COUNT`。 + - 支持服务器端部署 - 单次多模型同时对话(即将上线) @@ -76,4 +79,4 @@ yarn cd .. yarn yarn dev -``` \ No newline at end of file +``` diff --git a/electron/search_engines/bocha.test.ts b/electron/search_engines/bocha.test.ts new file mode 100644 index 0000000..ae93e92 --- /dev/null +++ b/electron/search_engines/bocha.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "vitest"; +import { mapBochaResponseToSearchResults } from "./bocha"; +import { searchEngines } from "./search"; + +describe("bocha search engine", () => { + test("maps Bocha web search results to AingDesk search results", () => { + const response = { + data: { + webPages: { + value: [ + { + name: "Bocha result", + url: "https://example.com/bocha", + summary: "Result summary", + }, + { + title: "Fallback title", + displayUrl: "https://example.com/fallback", + snippet: "Fallback snippet", + }, + { + name: "", + url: "https://example.com/empty-title", + summary: "Ignored because title is empty", + }, + ], + }, + }, + }; + + expect(mapBochaResponseToSearchResults(response)).toEqual([ + { + title: "Bocha result", + link: "https://example.com/bocha", + content: "Result summary", + }, + { + title: "Fallback title", + link: "https://example.com/fallback", + content: "Fallback snippet", + }, + ]); + }); + + test("registers Bocha as a built-in search provider", () => { + expect(searchEngines).toHaveProperty("bocha"); + }); +}); diff --git a/electron/search_engines/bocha.ts b/electron/search_engines/bocha.ts new file mode 100644 index 0000000..e19a070 --- /dev/null +++ b/electron/search_engines/bocha.ts @@ -0,0 +1,78 @@ +import { SearchResult } from "./utils"; + +const DEFAULT_BOCHA_SEARCH_API_URL = "https://api.bochaai.com/v1/web-search?utm_source=ollama"; +const DEFAULT_BOCHA_SEARCH_RESULT_COUNT = 10; + +const getBochaSearchResultCount = () => { + const value = Number.parseInt(process.env.BOCHA_SEARCH_RESULT_COUNT || "", 10); + if (Number.isNaN(value)) { + return DEFAULT_BOCHA_SEARCH_RESULT_COUNT; + } + return Math.min(Math.max(value, 1), 20); +}; + +const getTextValue = (...values: any[]) => { + for (const value of values) { + if (typeof value === "string" && value.trim()) { + return value.trim(); + } + } + return ""; +}; + +export const mapBochaResponseToSearchResults = (response: any): SearchResult[] => { + const values = response?.data?.webPages?.value || response?.webPages?.value || []; + if (!Array.isArray(values)) { + return []; + } + + return values.map((item: any) => { + const title = getTextValue(item?.name, item?.title); + const link = getTextValue(item?.url, item?.displayUrl); + const content = getTextValue(item?.summary, item?.snippet, item?.description); + return { title, link, content }; + }).filter((result: SearchResult) => result.title && result.link); +}; + +// Bocha Web Search API provider. +export const localBochaSearch = async (query: string): Promise => { + const apiKey = process.env.BOCHA_SEARCH_API_KEY; + if (!apiKey) { + console.error("Bocha search request failed: BOCHA_SEARCH_API_KEY is not set"); + return []; + } + + const url = process.env.BOCHA_SEARCH_API_URL || DEFAULT_BOCHA_SEARCH_API_URL; + const abortController = new AbortController(); + const timeoutId = setTimeout(() => abortController.abort(), 10000); + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Authorization": `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query, + freshness: "noLimit", + summary: true, + count: getBochaSearchResultCount(), + }), + signal: abortController.signal, + }); + + if (!response.ok) { + console.error(`Bocha search request failed: HTTP ${response.status}`); + return []; + } + + const jsonRes = await response.json(); + return mapBochaResponseToSearchResults(jsonRes); + } catch (error) { + console.error("Bocha search request failed:", error); + return []; + } finally { + clearTimeout(timeoutId); + } +}; diff --git a/electron/search_engines/search.ts b/electron/search_engines/search.ts index 2989cc2..f33caac 100644 --- a/electron/search_engines/search.ts +++ b/electron/search_engines/search.ts @@ -1,10 +1,11 @@ import { localBaiduSearch } from "./baidu"; import { SearchResult } from "./utils"; -import { localDuckDuckGoSearch } from "./duckduckgo"; -import { localSogouSearch } from "./sogou"; -import { local360Search } from "./so360"; -import { pub } from '../class/public' -import { agentService } from '../service/agent'; +import { localDuckDuckGoSearch } from "./duckduckgo"; +import { localSogouSearch } from "./sogou"; +import { local360Search } from "./so360"; +import { localBochaSearch } from "./bocha"; +import { pub } from '../class/public' +import { agentService } from '../service/agent'; // 获取模板常量 @@ -125,10 +126,11 @@ export const searchEngines = { duckduckgo: localDuckDuckGoSearch, sogou: localSogouSearch, sougou: localSogouSearch, - google: local360Search, - so360: local360Search, - 360: local360Search, -}; + google: local360Search, + so360: local360Search, + 360: local360Search, + bocha: localBochaSearch, +}; // 搜索网页函数 export const searchWeb = async (provider: string, query: string): Promise => { @@ -369,4 +371,4 @@ export const search = async (query:string,searchProvider:string):Promise - - + { label: $t("百度"), value: "baidu" }, + { label: $t("搜狗"), value: "sogou" }, + { label: $t("360搜索"), value: "360" }, + { label: "Bocha", value: "bocha" }, + ]' style="width:120px" v-model:value="targetNet" @update:value="setSearch" /> + +
Github @@ -136,4 +137,4 @@ getDataSavePath() .theme-setting {} } - \ No newline at end of file +