@@ -24,6 +24,7 @@ class Main(star.Star):
2424 "web_search_tavily" ,
2525 "tavily_extract_web_page" ,
2626 "web_search_bocha" ,
27+ "web_search_brave" ,
2728 ]
2829
2930 def __init__ (self , context : star .Context ) -> None :
@@ -33,6 +34,8 @@ def __init__(self, context: star.Context) -> None:
3334
3435 self .bocha_key_index = 0
3536 self .bocha_key_lock = asyncio .Lock ()
37+ self .brave_key_index = 0
38+ self .brave_key_lock = asyncio .Lock ()
3639
3740 # 将 str 类型的 key 迁移至 list[str],并保存
3841 cfg = self .context .get_config ()
@@ -57,6 +60,14 @@ def __init__(self, context: star.Context) -> None:
5760 provider_settings ["websearch_bocha_key" ] = []
5861 cfg .save_config ()
5962
63+ brave_key = provider_settings .get ("websearch_brave_key" )
64+ if isinstance (brave_key , str ):
65+ if brave_key :
66+ provider_settings ["websearch_brave_key" ] = [brave_key ]
67+ else :
68+ provider_settings ["websearch_brave_key" ] = []
69+ cfg .save_config ()
70+
6071 self .bing_search = Bing ()
6172 self .sogo_search = Sogo ()
6273 self .baidu_initialized = False
@@ -430,6 +441,50 @@ async def _web_search_bocha(
430441 results .append (result )
431442 return results
432443
444+ async def _get_brave_key (self , cfg : AstrBotConfig ) -> str :
445+ """并发安全的从列表中获取并轮换 Brave API 密钥。"""
446+ brave_keys = cfg .get ("provider_settings" , {}).get ("websearch_brave_key" , [])
447+
448+ async with self .brave_key_lock :
449+ key = brave_keys [self .brave_key_index ]
450+ self .brave_key_index = (self .brave_key_index + 1 ) % len (brave_keys )
451+ return key
452+
453+ async def _web_search_brave (
454+ self ,
455+ cfg : AstrBotConfig ,
456+ payload : dict ,
457+ ) -> list [SearchResult ]:
458+ """使用 Brave 搜索引擎进行搜索"""
459+ brave_key = await self ._get_brave_key (cfg )
460+ url = "https://api.search.brave.com/res/v1/web/search"
461+ header = {
462+ "Accept" : "application/json" ,
463+ "X-Subscription-Token" : brave_key ,
464+ }
465+ async with aiohttp .ClientSession (trust_env = True ) as session :
466+ async with session .get (
467+ url ,
468+ params = payload ,
469+ headers = header ,
470+ ) as response :
471+ if response .status != 200 :
472+ reason = await response .text ()
473+ raise Exception (
474+ f"Brave web search failed: { reason } , status: { response .status } " ,
475+ )
476+ data = await response .json ()
477+ rows = data .get ("web" , {}).get ("results" , [])
478+ results = []
479+ for item in rows :
480+ result = SearchResult (
481+ title = item .get ("title" , "" ),
482+ url = item .get ("url" , "" ),
483+ snippet = item .get ("description" , "" ),
484+ )
485+ results .append (result )
486+ return results
487+
433488 @llm_tool ("web_search_bocha" )
434489 async def search_from_bocha (
435490 self ,
@@ -537,6 +592,64 @@ async def search_from_bocha(
537592 ret = json .dumps ({"results" : ret_ls }, ensure_ascii = False )
538593 return ret
539594
595+ @llm_tool ("web_search_brave" )
596+ async def search_from_brave (
597+ self ,
598+ event : AstrMessageEvent ,
599+ query : str ,
600+ count : int = 10 ,
601+ country : str = "US" ,
602+ search_lang : str = "zh-hans" ,
603+ freshness : str = "" ,
604+ ) -> str :
605+ """
606+ A web search tool based on Brave Search API.
607+
608+ Args:
609+ query(string): Required. Search query.
610+ count(number): Optional. Number of results to return. Range: 1–20. Default is 10.
611+ country(string): Optional. Country code for region-specific results (e.g., "US", "CN").
612+ search_lang(string): Optional. Brave language code (e.g., "zh-hans", "en", "en-gb").
613+ freshness(string): Optional. "day", "week", "month", "year".
614+ """
615+ logger .info (f"web_searcher - search_from_brave: { query } " )
616+ cfg = self .context .get_config (umo = event .unified_msg_origin )
617+ if not cfg .get ("provider_settings" , {}).get ("websearch_brave_key" , []):
618+ raise ValueError ("Error: Brave API key is not configured in AstrBot." )
619+
620+ if count < 1 :
621+ count = 1
622+ if count > 20 :
623+ count = 20
624+
625+ payload = {
626+ "q" : query ,
627+ "count" : count ,
628+ "country" : country ,
629+ "search_lang" : search_lang ,
630+ }
631+ if freshness in ["day" , "week" , "month" , "year" ]:
632+ payload ["freshness" ] = freshness
633+
634+ results = await self ._web_search_brave (cfg , payload )
635+ if not results :
636+ return "Error: Brave web searcher does not return any results."
637+
638+ ret_ls = []
639+ ref_uuid = str (uuid .uuid4 ())[:4 ]
640+ for idx , result in enumerate (results , 1 ):
641+ index = f"{ ref_uuid } .{ idx } "
642+ ret_ls .append (
643+ {
644+ "title" : f"{ result .title } " ,
645+ "url" : f"{ result .url } " ,
646+ "snippet" : f"{ result .snippet } " ,
647+ "index" : index ,
648+ }
649+ )
650+ ret = json .dumps ({"results" : ret_ls }, ensure_ascii = False )
651+ return ret
652+
540653 @filter .on_llm_request (priority = - 10000 )
541654 async def edit_web_search_tools (
542655 self ,
@@ -575,6 +688,7 @@ async def edit_web_search_tools(
575688 tool_set .remove_tool ("tavily_extract_web_page" )
576689 tool_set .remove_tool ("AIsearch" )
577690 tool_set .remove_tool ("web_search_bocha" )
691+ tool_set .remove_tool ("web_search_brave" )
578692 elif provider == "tavily" :
579693 web_search_tavily = func_tool_mgr .get_func ("web_search_tavily" )
580694 tavily_extract_web_page = func_tool_mgr .get_func ("tavily_extract_web_page" )
@@ -586,6 +700,7 @@ async def edit_web_search_tools(
586700 tool_set .remove_tool ("fetch_url" )
587701 tool_set .remove_tool ("AIsearch" )
588702 tool_set .remove_tool ("web_search_bocha" )
703+ tool_set .remove_tool ("web_search_brave" )
589704 elif provider == "baidu_ai_search" :
590705 try :
591706 await self .ensure_baidu_ai_search_mcp (event .unified_msg_origin )
@@ -597,6 +712,7 @@ async def edit_web_search_tools(
597712 tool_set .remove_tool ("web_search_tavily" )
598713 tool_set .remove_tool ("tavily_extract_web_page" )
599714 tool_set .remove_tool ("web_search_bocha" )
715+ tool_set .remove_tool ("web_search_brave" )
600716 except Exception as e :
601717 logger .error (f"Cannot Initialize Baidu AI Search MCP Server: { e } " )
602718 elif provider == "bocha" :
@@ -608,3 +724,14 @@ async def edit_web_search_tools(
608724 tool_set .remove_tool ("AIsearch" )
609725 tool_set .remove_tool ("web_search_tavily" )
610726 tool_set .remove_tool ("tavily_extract_web_page" )
727+ tool_set .remove_tool ("web_search_brave" )
728+ elif provider == "brave" :
729+ web_search_brave = func_tool_mgr .get_func ("web_search_brave" )
730+ if web_search_brave and web_search_brave .active :
731+ tool_set .add_tool (web_search_brave )
732+ tool_set .remove_tool ("web_search" )
733+ tool_set .remove_tool ("fetch_url" )
734+ tool_set .remove_tool ("AIsearch" )
735+ tool_set .remove_tool ("web_search_tavily" )
736+ tool_set .remove_tool ("tavily_extract_web_page" )
737+ tool_set .remove_tool ("web_search_bocha" )
0 commit comments