Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion README.zh_cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`。

- 支持服务器端部署
- 单次多模型同时对话(即将上线)

Expand Down Expand Up @@ -76,4 +79,4 @@ yarn
cd ..
yarn
yarn dev
```
```
48 changes: 48 additions & 0 deletions electron/search_engines/bocha.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
78 changes: 78 additions & 0 deletions electron/search_engines/bocha.ts
Original file line number Diff line number Diff line change
@@ -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<SearchResult[]> => {
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);
}
};
22 changes: 12 additions & 10 deletions electron/search_engines/search.ts
Original file line number Diff line number Diff line change
@@ -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';


// 获取模板常量
Expand Down Expand Up @@ -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<SearchResult[]> => {
Expand Down Expand Up @@ -369,4 +371,4 @@ export const search = async (query:string,searchProvider:string):Promise<SearchR
console.error('Error searching:', error);
}
return [];
}
}
15 changes: 8 additions & 7 deletions frontend/src/views/SoftSettings/components/GeneralSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@
{{ $t("默认搜索引擎") }}
</div>
<n-select :options='[
{ label: $t("百度"), value: "baidu" },
{ label: $t("搜狗"), value: "sogou" },
{ label: $t("360搜索"), value: "360" },
]' style="width:120px" v-model:value="targetNet" @update:value="setSearch" />
</div>
</n-list-item>
{ 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" />
</div>
</n-list-item>
<n-list-item>
<div class="flex justify-between items-center">
<span>Github</span>
Expand Down Expand Up @@ -136,4 +137,4 @@ getDataSavePath()

.theme-setting {}
}
</style>
</style>