From 0f3824c90b47db3a6d0f19571a8083819342a3e5 Mon Sep 17 00:00:00 2001 From: DanielSu Date: Mon, 23 Mar 2026 18:00:09 +0800 Subject: [PATCH 1/3] feat:Brave Search API adapted --- astrbot/builtin_stars/web_searcher/main.py | 129 ++++++++++++++++++ astrbot/core/astr_agent_hooks.py | 3 +- astrbot/core/config/default.py | 19 ++- astrbot/dashboard/routes/chat.py | 2 +- astrbot/dashboard/routes/live_chat.py | 2 +- compose.yml | 10 +- dashboard/src/components/chat/MessageList.vue | 7 +- .../en-US/features/config-metadata.json | 4 + .../ru-RU/features/config-metadata.json | 4 + .../zh-CN/features/config-metadata.json | 4 + docs/en/dev/astrbot-config.md | 15 +- docs/zh/dev/astrbot-config.md | 15 +- 12 files changed, 200 insertions(+), 14 deletions(-) diff --git a/astrbot/builtin_stars/web_searcher/main.py b/astrbot/builtin_stars/web_searcher/main.py index cca1b43fb4..a86377305e 100644 --- a/astrbot/builtin_stars/web_searcher/main.py +++ b/astrbot/builtin_stars/web_searcher/main.py @@ -24,6 +24,7 @@ class Main(star.Star): "web_search_tavily", "tavily_extract_web_page", "web_search_bocha", + "web_search_brave", ] def __init__(self, context: star.Context) -> None: @@ -33,6 +34,8 @@ def __init__(self, context: star.Context) -> None: self.bocha_key_index = 0 self.bocha_key_lock = asyncio.Lock() + self.brave_key_index = 0 + self.brave_key_lock = asyncio.Lock() # 将 str 类型的 key 迁移至 list[str],并保存 cfg = self.context.get_config() @@ -57,6 +60,14 @@ def __init__(self, context: star.Context) -> None: provider_settings["websearch_bocha_key"] = [] cfg.save_config() + brave_key = provider_settings.get("websearch_brave_key") + if isinstance(brave_key, str): + if brave_key: + provider_settings["websearch_brave_key"] = [brave_key] + else: + provider_settings["websearch_brave_key"] = [] + cfg.save_config() + self.bing_search = Bing() self.sogo_search = Sogo() self.baidu_initialized = False @@ -430,6 +441,52 @@ async def _web_search_bocha( results.append(result) return results + async def _get_brave_key(self, cfg: AstrBotConfig) -> str: + """并发安全的从列表中获取并轮换 Brave API 密钥。""" + brave_keys = cfg.get("provider_settings", {}).get("websearch_brave_key", []) + if not brave_keys: + raise ValueError("错误:Brave API密钥未在AstrBot中配置。") + + async with self.brave_key_lock: + key = brave_keys[self.brave_key_index] + self.brave_key_index = (self.brave_key_index + 1) % len(brave_keys) + return key + + async def _web_search_brave( + self, + cfg: AstrBotConfig, + payload: dict, + ) -> list[SearchResult]: + """使用 Brave 搜索引擎进行搜索""" + brave_key = await self._get_brave_key(cfg) + url = "https://api.search.brave.com/res/v1/web/search" + header = { + "Accept": "application/json", + "X-Subscription-Token": brave_key, + } + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + url, + params=payload, + headers=header, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"Brave web search failed: {reason}, status: {response.status}", + ) + data = await response.json() + rows = data.get("web", {}).get("results", []) + results = [] + for item in rows: + result = SearchResult( + title=item.get("title", ""), + url=item.get("url", ""), + snippet=item.get("description", ""), + ) + results.append(result) + return results + @llm_tool("web_search_bocha") async def search_from_bocha( self, @@ -537,6 +594,64 @@ async def search_from_bocha( ret = json.dumps({"results": ret_ls}, ensure_ascii=False) return ret + @llm_tool("web_search_brave") + async def search_from_brave( + self, + event: AstrMessageEvent, + query: str, + count: int = 10, + country: str = "US", + search_lang: str = "zh-hans", + freshness: str = "", + ) -> str: + """ + A web search tool based on Brave Search API. + + Args: + query(string): Required. Search query. + count(number): Optional. Number of results to return. Range: 1–20. Default is 10. + country(string): Optional. Country code for region-specific results (e.g., "US", "CN"). + search_lang(string): Optional. Brave language code (e.g., "zh-hans", "en", "en-gb"). + freshness(string): Optional. "day", "week", "month", "year". + """ + logger.info(f"web_searcher - search_from_brave: {query}") + cfg = self.context.get_config(umo=event.unified_msg_origin) + if not cfg.get("provider_settings", {}).get("websearch_brave_key", []): + raise ValueError("Error: Brave API key is not configured in AstrBot.") + + if count < 1: + count = 1 + if count > 20: + count = 20 + + payload = { + "q": query, + "count": count, + "country": country, + "search_lang": search_lang, + } + if freshness in ["day", "week", "month", "year"]: + payload["freshness"] = freshness + + results = await self._web_search_brave(cfg, payload) + if not results: + return "Error: Brave web searcher does not return any results." + + ret_ls = [] + ref_uuid = str(uuid.uuid4())[:4] + for idx, result in enumerate(results, 1): + index = f"{ref_uuid}.{idx}" + ret_ls.append( + { + "title": f"{result.title}", + "url": f"{result.url}", + "snippet": f"{result.snippet}", + "index": index, + } + ) + ret = json.dumps({"results": ret_ls}, ensure_ascii=False) + return ret + @filter.on_llm_request(priority=-10000) async def edit_web_search_tools( self, @@ -575,6 +690,7 @@ async def edit_web_search_tools( tool_set.remove_tool("tavily_extract_web_page") tool_set.remove_tool("AIsearch") tool_set.remove_tool("web_search_bocha") + tool_set.remove_tool("web_search_brave") elif provider == "tavily": web_search_tavily = func_tool_mgr.get_func("web_search_tavily") tavily_extract_web_page = func_tool_mgr.get_func("tavily_extract_web_page") @@ -586,6 +702,7 @@ async def edit_web_search_tools( tool_set.remove_tool("fetch_url") tool_set.remove_tool("AIsearch") tool_set.remove_tool("web_search_bocha") + tool_set.remove_tool("web_search_brave") elif provider == "baidu_ai_search": try: await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin) @@ -597,6 +714,7 @@ async def edit_web_search_tools( tool_set.remove_tool("web_search_tavily") tool_set.remove_tool("tavily_extract_web_page") tool_set.remove_tool("web_search_bocha") + tool_set.remove_tool("web_search_brave") except Exception as e: logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}") elif provider == "bocha": @@ -608,3 +726,14 @@ async def edit_web_search_tools( tool_set.remove_tool("AIsearch") tool_set.remove_tool("web_search_tavily") tool_set.remove_tool("tavily_extract_web_page") + tool_set.remove_tool("web_search_brave") + elif provider == "brave": + web_search_brave = func_tool_mgr.get_func("web_search_brave") + if web_search_brave and web_search_brave.active: + tool_set.add_tool(web_search_brave) + tool_set.remove_tool("web_search") + tool_set.remove_tool("fetch_url") + tool_set.remove_tool("AIsearch") + tool_set.remove_tool("web_search_tavily") + tool_set.remove_tool("tavily_extract_web_page") + tool_set.remove_tool("web_search_bocha") diff --git a/astrbot/core/astr_agent_hooks.py b/astrbot/core/astr_agent_hooks.py index 09bf32deb4..e3dfb989f3 100644 --- a/astrbot/core/astr_agent_hooks.py +++ b/astrbot/core/astr_agent_hooks.py @@ -59,7 +59,8 @@ async def on_tool_end( platform_name = run_context.context.event.get_platform_name() if ( platform_name == "webchat" - and tool.name in ["web_search_tavily", "web_search_bocha"] + and tool.name + in ["web_search_tavily", "web_search_bocha", "web_search_brave"] and len(run_context.messages) > 0 and tool_result and len(tool_result.content) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 0f43dbd06d..1249b0512a 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -109,6 +109,7 @@ "websearch_provider": "default", "websearch_tavily_key": [], "websearch_bocha_key": [], + "websearch_brave_key": [], "websearch_baidu_app_builder_key": "", "web_search_link": False, "display_reasoning_text": False, @@ -3080,7 +3081,13 @@ class ChatProviderTemplate(TypedDict): "provider_settings.websearch_provider": { "description": "网页搜索提供商", "type": "string", - "options": ["default", "tavily", "baidu_ai_search", "bocha"], + "options": [ + "default", + "tavily", + "baidu_ai_search", + "bocha", + "brave", + ], "condition": { "provider_settings.web_search": True, }, @@ -3105,6 +3112,16 @@ class ChatProviderTemplate(TypedDict): "provider_settings.web_search": True, }, }, + "provider_settings.websearch_brave_key": { + "description": "Brave API Key", + "type": "list", + "items": {"type": "string"}, + "hint": "可添加多个 Key 进行轮询。", + "condition": { + "provider_settings.websearch_provider": "brave", + "provider_settings.web_search": True, + }, + }, "provider_settings.websearch_baidu_app_builder_key": { "description": "百度千帆智能云 APP Builder API Key", "type": "string", diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index c79ad1e355..e3fcea0231 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -218,7 +218,7 @@ def _extract_web_search_refs( Returns: 包含 used 列表的字典,记录被引用的搜索结果 """ - supported = ["web_search_tavily", "web_search_bocha"] + supported = ["web_search_tavily", "web_search_bocha", "web_search_brave"] # 从 accumulated_parts 中找到所有 web_search_tavily 的工具调用结果 web_search_results = {} tool_call_parts = [ diff --git a/astrbot/dashboard/routes/live_chat.py b/astrbot/dashboard/routes/live_chat.py index 8d0af938d0..662eaa05cb 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -198,7 +198,7 @@ def _extract_web_search_refs( self, accumulated_text: str, accumulated_parts: list ) -> dict: """从消息中提取 web_search 引用。""" - supported = ["web_search_tavily", "web_search_bocha"] + supported = ["web_search_tavily", "web_search_bocha", "web_search_brave"] web_search_results = {} tool_call_parts = [ p diff --git a/compose.yml b/compose.yml index 044d484cb1..5124c7dafb 100644 --- a/compose.yml +++ b/compose.yml @@ -1,17 +1,15 @@ +--- # 当接入 QQ NapCat 时,请使用这个 compose 文件一键部署: https://github.com/NapNeko/NapCat-Docker/blob/main/compose/astrbot.yml - services: astrbot: image: soulter/astrbot:latest container_name: astrbot restart: always - ports: # mappings description: https://github.com/AstrBotDevs/AstrBot/issues/497 - - "6185:6185" # 必选,AstrBot WebUI 端口 - - "6199:6199" # 可选, QQ 个人号 WebSocket 端口 + ports: # mappings description: https://github.com/AstrBotDevs/AstrBot/issues/497 + - 6184:6185 # 必选,AstrBot WebUI 端口 # - "6195:6195" # 可选, 企业微信 Webhook 端口 # - "6196:6196" # 可选, QQ 官方接口 Webhook 端口 - environment: - - TZ=Asia/Shanghai + environment: [TZ=Asia/Shanghai] volumes: - ./data:/AstrBot/data # - /etc/timezone:/etc/timezone:ro diff --git a/dashboard/src/components/chat/MessageList.vue b/dashboard/src/components/chat/MessageList.vue index ca86331a86..b68006556c 100644 --- a/dashboard/src/components/chat/MessageList.vue +++ b/dashboard/src/components/chat/MessageList.vue @@ -302,8 +302,11 @@ export default { } part.tool_calls.forEach(toolCall => { - // 检查是否是 web_search_tavily 工具调用 - if (toolCall.name !== 'web_search_tavily' || !toolCall.result) { + // 检查是否是支持引用解析的 web_search 工具调用 + if ( + !['web_search_tavily', 'web_search_bocha', 'web_search_brave'].includes(toolCall.name) || + !toolCall.result + ) { return; } diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index 43aae5984b..0d46f79b54 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -121,6 +121,10 @@ "description": "BoCha API Key", "hint": "Multiple keys can be added for rotation." }, + "websearch_brave_key": { + "description": "Brave API Key", + "hint": "Multiple keys can be added for rotation." + }, "websearch_baidu_app_builder_key": { "description": "Baidu Qianfan Smart Cloud APP Builder API Key", "hint": "Reference: [https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)" diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json index 06f60dd40a..cac0cd40dd 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json +++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json @@ -121,6 +121,10 @@ "description": "API-ключ BoCha", "hint": "Можно добавить несколько ключей для ротации." }, + "websearch_brave_key": { + "description": "API-ключ Brave", + "hint": "Можно добавить несколько ключей для ротации." + }, "websearch_baidu_app_builder_key": { "description": "API-ключ Baidu Qianfan APP Builder", "hint": "Ссылка: [https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)" diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index 7b59a981d5..843821d2af 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -123,6 +123,10 @@ "description": "BoCha API Key", "hint": "可添加多个 Key 进行轮询。" }, + "websearch_brave_key": { + "description": "Brave API Key", + "hint": "可添加多个 Key 进行轮询。" + }, "websearch_baidu_app_builder_key": { "description": "百度千帆智能云 APP Builder API Key", "hint": "参考:[https://console.bce.baidu.com/iam/#/iam/apikey/list](https://console.bce.baidu.com/iam/#/iam/apikey/list)" diff --git a/docs/en/dev/astrbot-config.md b/docs/en/dev/astrbot-config.md index a33f14105f..40de14e8e7 100644 --- a/docs/en/dev/astrbot-config.md +++ b/docs/en/dev/astrbot-config.md @@ -60,6 +60,8 @@ The default AstrBot configuration is as follows: "web_search": False, "websearch_provider": "default", "websearch_tavily_key": [], + "websearch_bocha_key": [], + "websearch_brave_key": [], "web_search_link": False, "display_reasoning_text": False, "identifier": False, @@ -286,16 +288,27 @@ Whether to enable AstrBot's built-in web search capability. Default is `false`. #### `provider_settings.websearch_provider` -Web search provider type. Default is `default`. Currently supports `default` and `tavily`. +Web search provider type. Default is `default`. Currently supports `default`, `tavily`, `bocha`, `baidu_ai_search`, and `brave`. - `default`: Works best when Google is accessible. If Google fails, it tries Bing and Sogou in order. - `tavily`: Uses the Tavily search engine. +- `bocha`: Uses the BoCha search engine. +- `baidu_ai_search`: Uses Baidu AI Search (MCP). +- `brave`: Uses Brave Search API. #### `provider_settings.websearch_tavily_key` API Key list for the Tavily search engine. Required when using `tavily` as the web search provider. +#### `provider_settings.websearch_bocha_key` + +API Key list for the BoCha search engine. Required when using `bocha` as the web search provider. + +#### `provider_settings.websearch_brave_key` + +API Key list for the Brave search engine. Required when using `brave` as the web search provider. + #### `provider_settings.web_search_link` Whether to prompt the model to include links to search results in the reply. Default is `false`. diff --git a/docs/zh/dev/astrbot-config.md b/docs/zh/dev/astrbot-config.md index 10a804515f..1ab9d9929d 100644 --- a/docs/zh/dev/astrbot-config.md +++ b/docs/zh/dev/astrbot-config.md @@ -60,6 +60,8 @@ AstrBot 默认配置如下: "web_search": False, "websearch_provider": "default", "websearch_tavily_key": [], + "websearch_bocha_key": [], + "websearch_brave_key": [], "web_search_link": False, "display_reasoning_text": False, "identifier": False, @@ -286,16 +288,27 @@ ID 白名单。填写后,将只处理所填写的 ID 发来的消息事件。 #### `provider_settings.websearch_provider` -网页搜索提供商类型。默认为 `default`。目前支持 `default` 和 `tavily`。 +网页搜索提供商类型。默认为 `default`。目前支持 `default`、`tavily`、`bocha`、`baidu_ai_search`、`brave`。 - `default`:能访问 Google 时效果最佳。如果 Google 访问失败,程序会依次访问 Bing, Sogo 搜索引擎。 - `tavily`:使用 Tavily 搜索引擎。 +- `bocha`:使用 BoCha 搜索引擎。 +- `baidu_ai_search`:使用百度 AI Search(MCP)。 +- `brave`:使用 Brave Search API。 #### `provider_settings.websearch_tavily_key` Tavily 搜索引擎的 API Key 列表。使用 `tavily` 作为网页搜索提供商时需要填写。 +#### `provider_settings.websearch_bocha_key` + +BoCha 搜索引擎的 API Key 列表。使用 `bocha` 作为网页搜索提供商时需要填写。 + +#### `provider_settings.websearch_brave_key` + +Brave 搜索引擎的 API Key 列表。使用 `brave` 作为网页搜索提供商时需要填写。 + #### `provider_settings.web_search_link` 是否在回复中提示模型附上搜索结果的链接。默认为 `false`。 From a552395c9fb841f141ff3bd0e7a19f9d28cff3c6 Mon Sep 17 00:00:00 2001 From: DanielSu Date: Mon, 23 Mar 2026 19:04:43 +0800 Subject: [PATCH 2/3] Modified hint message --- astrbot/core/config/default.py | 2 +- dashboard/src/i18n/locales/en-US/features/config-metadata.json | 2 +- dashboard/src/i18n/locales/ru-RU/features/config-metadata.json | 2 +- dashboard/src/i18n/locales/zh-CN/features/config-metadata.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 1249b0512a..fbcfbd9108 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -3113,7 +3113,7 @@ class ChatProviderTemplate(TypedDict): }, }, "provider_settings.websearch_brave_key": { - "description": "Brave API Key", + "description": "Brave Search API Key", "type": "list", "items": {"type": "string"}, "hint": "可添加多个 Key 进行轮询。", diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index 0d46f79b54..c1f25221c9 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -122,7 +122,7 @@ "hint": "Multiple keys can be added for rotation." }, "websearch_brave_key": { - "description": "Brave API Key", + "description": "Brave Search API Key", "hint": "Multiple keys can be added for rotation." }, "websearch_baidu_app_builder_key": { diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json index cac0cd40dd..537783cf89 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json +++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json @@ -122,7 +122,7 @@ "hint": "Можно добавить несколько ключей для ротации." }, "websearch_brave_key": { - "description": "API-ключ Brave", + "description": "API-ключ Brave Search", "hint": "Можно добавить несколько ключей для ротации." }, "websearch_baidu_app_builder_key": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index 843821d2af..6702f1912f 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -124,7 +124,7 @@ "hint": "可添加多个 Key 进行轮询。" }, "websearch_brave_key": { - "description": "Brave API Key", + "description": "Brave Search API Key", "hint": "可添加多个 Key 进行轮询。" }, "websearch_baidu_app_builder_key": { From d5f9b86fc208cac25eeec578521ab324e736769e Mon Sep 17 00:00:00 2001 From: DanielSu Date: Mon, 23 Mar 2026 19:28:22 +0800 Subject: [PATCH 3/3] Modified according to AI reviews --- astrbot/builtin_stars/web_searcher/main.py | 2 -- compose.yml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/astrbot/builtin_stars/web_searcher/main.py b/astrbot/builtin_stars/web_searcher/main.py index a86377305e..f167a2c5b3 100644 --- a/astrbot/builtin_stars/web_searcher/main.py +++ b/astrbot/builtin_stars/web_searcher/main.py @@ -444,8 +444,6 @@ async def _web_search_bocha( async def _get_brave_key(self, cfg: AstrBotConfig) -> str: """并发安全的从列表中获取并轮换 Brave API 密钥。""" brave_keys = cfg.get("provider_settings", {}).get("websearch_brave_key", []) - if not brave_keys: - raise ValueError("错误:Brave API密钥未在AstrBot中配置。") async with self.brave_key_lock: key = brave_keys[self.brave_key_index] diff --git a/compose.yml b/compose.yml index 5124c7dafb..3435bf19a6 100644 --- a/compose.yml +++ b/compose.yml @@ -6,7 +6,7 @@ services: container_name: astrbot restart: always ports: # mappings description: https://github.com/AstrBotDevs/AstrBot/issues/497 - - 6184:6185 # 必选,AstrBot WebUI 端口 + - 6185:6185 # 必选,AstrBot WebUI 端口 # - "6195:6195" # 可选, 企业微信 Webhook 端口 # - "6196:6196" # 可选, QQ 官方接口 Webhook 端口 environment: [TZ=Asia/Shanghai]