@@ -23,13 +23,17 @@ class Main(star.Star):
2323 "fetch_url" ,
2424 "web_search_tavily" ,
2525 "tavily_extract_web_page" ,
26+ "web_search_bocha" ,
2627 ]
2728
2829 def __init__ (self , context : star .Context ) -> None :
2930 self .context = context
3031 self .tavily_key_index = 0
3132 self .tavily_key_lock = asyncio .Lock ()
3233
34+ self .bocha_key_index = 0
35+ self .bocha_key_lock = asyncio .Lock ()
36+
3337 # 将 str 类型的 key 迁移至 list[str],并保存
3438 cfg = self .context .get_config ()
3539 provider_settings = cfg .get ("provider_settings" )
@@ -45,6 +49,14 @@ def __init__(self, context: star.Context) -> None:
4549 provider_settings ["websearch_tavily_key" ] = []
4650 cfg .save_config ()
4751
52+ bocha_key = provider_settings .get ("websearch_bocha_key" )
53+ if isinstance (bocha_key , str ):
54+ if bocha_key :
55+ provider_settings ["websearch_bocha_key" ] = [bocha_key ]
56+ else :
57+ provider_settings ["websearch_bocha_key" ] = []
58+ cfg .save_config ()
59+
4860 self .bing_search = Bing ()
4961 self .sogo_search = Sogo ()
5062 self .baidu_initialized = False
@@ -341,7 +353,7 @@ async def search_from_tavily(
341353 }
342354 )
343355 if result .favicon :
344- sp .temorary_cache ["_ws_favicon" ][result .url ] = result .favicon
356+ sp .temporary_cache ["_ws_favicon" ][result .url ] = result .favicon
345357 # ret = "\n".join(ret_ls)
346358 ret = json .dumps ({"results" : ret_ls }, ensure_ascii = False )
347359 return ret
@@ -382,6 +394,160 @@ async def tavily_extract_web_page(
382394 return "Error: Tavily web searcher does not return any results."
383395 return ret
384396
397+ async def _get_bocha_key (self , cfg : AstrBotConfig ) -> str :
398+ """并发安全的从列表中获取并轮换BoCha API密钥。"""
399+ bocha_keys = cfg .get ("provider_settings" , {}).get ("websearch_bocha_key" , [])
400+ if not bocha_keys :
401+ raise ValueError ("错误:BoCha API密钥未在AstrBot中配置。" )
402+
403+ async with self .bocha_key_lock :
404+ key = bocha_keys [self .bocha_key_index ]
405+ self .bocha_key_index = (self .bocha_key_index + 1 ) % len (bocha_keys )
406+ return key
407+
408+ async def _web_search_bocha (
409+ self ,
410+ cfg : AstrBotConfig ,
411+ payload : dict ,
412+ ) -> list [SearchResult ]:
413+ """使用 BoCha 搜索引擎进行搜索"""
414+ bocha_key = await self ._get_bocha_key (cfg )
415+ url = "https://api.bochaai.com/v1/web-search"
416+ header = {
417+ "Authorization" : f"Bearer { bocha_key } " ,
418+ "Content-Type" : "application/json" ,
419+ }
420+ async with aiohttp .ClientSession (trust_env = True ) as session :
421+ async with session .post (
422+ url ,
423+ json = payload ,
424+ headers = header ,
425+ ) as response :
426+ if response .status != 200 :
427+ reason = await response .text ()
428+ raise Exception (
429+ f"BoCha web search failed: { reason } , status: { response .status } " ,
430+ )
431+ data = await response .json ()
432+ data = data ["data" ]["webPages" ]["value" ]
433+ results = []
434+ for item in data :
435+ result = SearchResult (
436+ title = item .get ("name" ),
437+ url = item .get ("url" ),
438+ snippet = item .get ("snippet" ),
439+ favicon = item .get ("siteIcon" ),
440+ )
441+ results .append (result )
442+ return results
443+
444+ @llm_tool ("web_search_bocha" )
445+ async def search_from_bocha (
446+ self ,
447+ event : AstrMessageEvent ,
448+ query : str ,
449+ freshness : str = "noLimit" ,
450+ summary : bool = False ,
451+ include : str = "" ,
452+ exclude : str = "" ,
453+ count : int = 10 ,
454+ ) -> str :
455+ """
456+ A web search tool based on Bocha Search API, used to retrieve web pages
457+ related to the user's query.
458+
459+ Args:
460+ query (string): Required. User's search query.
461+
462+ freshness (string): Optional. Specifies the time range of the search.
463+ Supported values:
464+ - "noLimit": No time limit (default, recommended).
465+ - "oneDay": Within one day.
466+ - "oneWeek": Within one week.
467+ - "oneMonth": Within one month.
468+ - "oneYear": Within one year.
469+ - "YYYY-MM-DD..YYYY-MM-DD": Search within a specific date range.
470+ Example: "2025-01-01..2025-04-06".
471+ - "YYYY-MM-DD": Search on a specific date.
472+ Example: "2025-04-06".
473+ It is recommended to use "noLimit", as the search algorithm will
474+ automatically optimize time relevance. Manually restricting the
475+ time range may result in no search results.
476+
477+ summary (boolean): Optional. Whether to include a text summary
478+ for each search result.
479+ - True: Include summary.
480+ - False: Do not include summary (default).
481+
482+ include (string): Optional. Specifies the domains to include in
483+ the search. Multiple domains can be separated by "|" or ",".
484+ A maximum of 100 domains is allowed.
485+ Examples:
486+ - "qq.com"
487+ - "qq.com|m.163.com"
488+
489+ exclude (string): Optional. Specifies the domains to exclude from
490+ the search. Multiple domains can be separated by "|" or ",".
491+ A maximum of 100 domains is allowed.
492+ Examples:
493+ - "qq.com"
494+ - "qq.com|m.163.com"
495+
496+ count (number): Optional. Number of search results to return.
497+ - Range: 1–50
498+ - Default: 10
499+ The actual number of returned results may be less than the
500+ specified count.
501+ """
502+ logger .info (f"web_searcher - search_from_bocha: { query } " )
503+ cfg = self .context .get_config (umo = event .unified_msg_origin )
504+ # websearch_link = cfg["provider_settings"].get("web_search_link", False)
505+ if not cfg .get ("provider_settings" , {}).get ("websearch_bocha_key" , []):
506+ raise ValueError ("Error: BoCha API key is not configured in AstrBot." )
507+
508+ # build payload
509+ payload = {
510+ "query" : query ,
511+ "count" : count ,
512+ }
513+
514+ # freshness:时间范围
515+ if freshness :
516+ payload ["freshness" ] = freshness
517+
518+ # 是否返回摘要
519+ payload ["summary" ] = summary
520+
521+ # include:限制搜索域
522+ if include :
523+ payload ["include" ] = include
524+
525+ # exclude:排除搜索域
526+ if exclude :
527+ payload ["exclude" ] = exclude
528+
529+ results = await self ._web_search_bocha (cfg , payload )
530+ if not results :
531+ return "Error: BoCha web searcher does not return any results."
532+
533+ ret_ls = []
534+ ref_uuid = str (uuid .uuid4 ())[:4 ]
535+ for idx , result in enumerate (results , 1 ):
536+ index = f"{ ref_uuid } .{ idx } "
537+ ret_ls .append (
538+ {
539+ "title" : f"{ result .title } " ,
540+ "url" : f"{ result .url } " ,
541+ "snippet" : f"{ result .snippet } " ,
542+ "index" : index ,
543+ }
544+ )
545+ if result .favicon :
546+ sp .temporary_cache ["_ws_favicon" ][result .url ] = result .favicon
547+ # ret = "\n".join(ret_ls)
548+ ret = json .dumps ({"results" : ret_ls }, ensure_ascii = False )
549+ return ret
550+
385551 @filter .on_llm_request (priority = - 10000 )
386552 async def edit_web_search_tools (
387553 self ,
@@ -419,6 +585,7 @@ async def edit_web_search_tools(
419585 tool_set .remove_tool ("web_search_tavily" )
420586 tool_set .remove_tool ("tavily_extract_web_page" )
421587 tool_set .remove_tool ("AIsearch" )
588+ tool_set .remove_tool ("web_search_bocha" )
422589 elif provider == "tavily" :
423590 web_search_tavily = func_tool_mgr .get_func ("web_search_tavily" )
424591 tavily_extract_web_page = func_tool_mgr .get_func ("tavily_extract_web_page" )
@@ -429,6 +596,7 @@ async def edit_web_search_tools(
429596 tool_set .remove_tool ("web_search" )
430597 tool_set .remove_tool ("fetch_url" )
431598 tool_set .remove_tool ("AIsearch" )
599+ tool_set .remove_tool ("web_search_bocha" )
432600 elif provider == "baidu_ai_search" :
433601 try :
434602 await self .ensure_baidu_ai_search_mcp (event .unified_msg_origin )
@@ -440,5 +608,15 @@ async def edit_web_search_tools(
440608 tool_set .remove_tool ("fetch_url" )
441609 tool_set .remove_tool ("web_search_tavily" )
442610 tool_set .remove_tool ("tavily_extract_web_page" )
611+ tool_set .remove_tool ("web_search_bocha" )
443612 except Exception as e :
444613 logger .error (f"Cannot Initialize Baidu AI Search MCP Server: { e } " )
614+ elif provider == "bocha" :
615+ web_search_bocha = func_tool_mgr .get_func ("web_search_bocha" )
616+ if web_search_bocha :
617+ tool_set .add_tool (web_search_bocha )
618+ tool_set .remove_tool ("web_search" )
619+ tool_set .remove_tool ("fetch_url" )
620+ tool_set .remove_tool ("AIsearch" )
621+ tool_set .remove_tool ("web_search_tavily" )
622+ tool_set .remove_tool ("tavily_extract_web_page" )
0 commit comments