From b86f92a457f428e86e0f0fd115fe1137ecaae875 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sat, 18 Oct 2025 00:46:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(web=5Fsearch):=20=E6=96=B0=E5=A2=9E=20Meta?= =?UTF-8?q?so=20=E6=90=9C=E7=B4=A2=E5=BC=95=E6=93=8E=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为联网搜索工具集成了新的搜索引擎 Metaso,为用户提供更多搜索选择。 - 在搜索引擎基类 `BaseSearchEngine` 中添加了可选的 `read_url` 方法,为未来支持直接读取网页内容奠定基础。 - 更新了插件加载、工具执行逻辑和配置文件模板,以完整支持 Metaso 引擎。 - 将默认回复生成器的任务超时时间从 15 秒延长至 45 秒,以适应联网搜索等耗时较长的操作,提高稳定性。 --- src/chat/replyer/default_generator.py | 2 +- .../built_in/web_search_tool/engines/base.py | 8 +- .../web_search_tool/engines/metaso_engine.py | 107 ++++++++++++++++++ .../built_in/web_search_tool/plugin.py | 8 +- .../web_search_tool/tools/web_search.py | 2 + template/bot_config_template.toml | 5 +- 6 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 src/plugins/built_in/web_search_tool/engines/metaso_engine.py diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 4756472f7..3be84e40c 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -1303,7 +1303,7 @@ class DefaultReplyer: } # 设置超时 - timeout = 15.0 # 秒 + timeout = 45.0 # 秒 async def get_task_result(task_name, task): try: diff --git a/src/plugins/built_in/web_search_tool/engines/base.py b/src/plugins/built_in/web_search_tool/engines/base.py index 4fd2c452a..ed10af5fb 100644 --- a/src/plugins/built_in/web_search_tool/engines/base.py +++ b/src/plugins/built_in/web_search_tool/engines/base.py @@ -3,7 +3,7 @@ Base search engine interface """ from abc import ABC, abstractmethod -from typing import Any +from typing import Any, Optional class BaseSearchEngine(ABC): @@ -24,6 +24,12 @@ class BaseSearchEngine(ABC): """ pass + async def read_url(self, url: str) -> Optional[str]: + """ + 读取URL内容,如果引擎不支持则返回None + """ + return None + @abstractmethod def is_available(self) -> bool: """ diff --git a/src/plugins/built_in/web_search_tool/engines/metaso_engine.py b/src/plugins/built_in/web_search_tool/engines/metaso_engine.py new file mode 100644 index 000000000..7e89f6653 --- /dev/null +++ b/src/plugins/built_in/web_search_tool/engines/metaso_engine.py @@ -0,0 +1,107 @@ +""" +Metaso Search Engine (Chat Completions Mode) +""" +import json +from typing import Any, List + +import httpx + +from src.common.logger import get_logger +from src.plugin_system.apis import config_api + +from ..utils.api_key_manager import create_api_key_manager_from_config +from .base import BaseSearchEngine + +logger = get_logger(__name__) + + +class MetasoClient: + """A client to interact with the Metaso API.""" + + def __init__(self, api_key: str): + self.api_key = api_key + self.base_url = "https://metaso.cn/api/v1" + self.headers = { + "Authorization": f"Bearer {self.api_key}", + "Accept": "application/json", + "Content-Type": "application/json", + } + + async def search(self, query: str, **kwargs) -> List[dict[str, Any]]: + """Perform a search using the Metaso Chat Completions API.""" + payload = {"model": "fast", "stream": True, "messages": [{"role": "user", "content": query}]} + search_url = f"{self.base_url}/chat/completions" + full_response_content = "" + + async with httpx.AsyncClient(timeout=90.0) as client: + try: + async with client.stream("POST", search_url, headers=self.headers, json=payload) as response: + response.raise_for_status() + async for line in response.aiter_lines(): + if line.startswith("data:"): + data_str = line[len("data:") :].strip() + if data_str == "[DONE]": + break + try: + data = json.loads(data_str) + delta = data.get("choices", [{}])[0].get("delta", {}) + content_chunk = delta.get("content") + if content_chunk: + full_response_content += content_chunk + except json.JSONDecodeError: + logger.warning(f"Metaso stream: could not decode JSON line: {data_str}") + continue + + if not full_response_content: + logger.warning("Metaso search returned an empty stream.") + return [] + + return [ + { + "title": query, + "url": "https://metaso.cn/", + "snippet": full_response_content, + "provider": "Metaso (Chat)", + } + ] + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error occurred while searching with Metaso Chat: {e.response.text}") + return [] + except Exception as e: + logger.error(f"An error occurred while searching with Metaso Chat: {e}", exc_info=True) + return [] + + +class MetasoSearchEngine(BaseSearchEngine): + """Metaso Search Engine implementation.""" + + def __init__(self): + self._initialize_clients() + + def _initialize_clients(self): + """Initialize Metaso clients.""" + metaso_api_keys = config_api.get_global_config("web_search.metaso_api_keys", None) + self.api_manager = create_api_key_manager_from_config( + metaso_api_keys, lambda key: MetasoClient(api_key=key), "Metaso" + ) + + def is_available(self) -> bool: + """Check if the Metaso search engine is available.""" + return self.api_manager.is_available() + + async def search(self, args: dict[str, Any]) -> list[dict[str, Any]]: + """Execute a Metaso search.""" + if not self.is_available(): + return [] + + query = args["query"] + try: + metaso_client = self.api_manager.get_next_client() + if not metaso_client: + logger.error("Could not get Metaso client.") + return [] + + return await metaso_client.search(query) + except Exception as e: + logger.error(f"Metaso search failed: {e}", exc_info=True) + return [] diff --git a/src/plugins/built_in/web_search_tool/plugin.py b/src/plugins/built_in/web_search_tool/plugin.py index 33e67bcab..f9980985a 100644 --- a/src/plugins/built_in/web_search_tool/plugin.py +++ b/src/plugins/built_in/web_search_tool/plugin.py @@ -22,6 +22,7 @@ class WEBSEARCHPLUGIN(BasePlugin): 提供网络搜索和URL解析功能,支持多种搜索引擎: - Exa (需要API密钥) - Tavily (需要API密钥) + - Metaso (需要API密钥) - DuckDuckGo (免费) - Bing (免费) """ @@ -43,6 +44,7 @@ class WEBSEARCHPLUGIN(BasePlugin): from .engines.exa_engine import ExaSearchEngine from .engines.searxng_engine import SearXNGSearchEngine from .engines.tavily_engine import TavilySearchEngine + from .engines.metaso_engine import MetasoSearchEngine # 实例化所有搜索引擎,这会触发API密钥管理器的初始化 exa_engine = ExaSearchEngine() @@ -50,14 +52,16 @@ class WEBSEARCHPLUGIN(BasePlugin): ddg_engine = DDGSearchEngine() bing_engine = BingSearchEngine() searxng_engine = SearXNGSearchEngine() - - # 报告每个引擎的状态 + metaso_engine = MetasoSearchEngine() + + # 报告每个引擎的状态 engines_status = { "Exa": exa_engine.is_available(), "Tavily": tavily_engine.is_available(), "DuckDuckGo": ddg_engine.is_available(), "Bing": bing_engine.is_available(), "SearXNG": searxng_engine.is_available(), + "Metaso": metaso_engine.is_available(), } available_engines = [name for name, available in engines_status.items() if available] diff --git a/src/plugins/built_in/web_search_tool/tools/web_search.py b/src/plugins/built_in/web_search_tool/tools/web_search.py index 637349534..2fcd46065 100644 --- a/src/plugins/built_in/web_search_tool/tools/web_search.py +++ b/src/plugins/built_in/web_search_tool/tools/web_search.py @@ -15,6 +15,7 @@ from ..engines.ddg_engine import DDGSearchEngine from ..engines.exa_engine import ExaSearchEngine from ..engines.searxng_engine import SearXNGSearchEngine from ..engines.tavily_engine import TavilySearchEngine +from ..engines.metaso_engine import MetasoSearchEngine from ..utils.formatters import deduplicate_results, format_search_results logger = get_logger("web_search_tool") @@ -51,6 +52,7 @@ class WebSurfingTool(BaseTool): "ddg": DDGSearchEngine(), "bing": BingSearchEngine(), "searxng": SearXNGSearchEngine(), + "metaso": MetasoSearchEngine(), } async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index f8cf393db..ae2110d59 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "7.3.1" +version = "7.3.2" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -489,11 +489,12 @@ enable_web_search_tool = true # 是否启用联网搜索tool enable_url_tool = true # 是否启用URL解析tool tavily_api_keys = ["None"]# Tavily API密钥列表,支持轮询机制 exa_api_keys = ["None"]# EXA API密钥列表,支持轮询机制 +metaso_api_keys = ["None"]# Metaso API密钥列表,支持轮询机制 searxng_instances = [] # SearXNG 实例 URL 列表 searxng_api_keys = []# SearXNG 实例 API 密钥列表 # 搜索引擎配置 -enabled_engines = ["ddg"] # 启用的搜索引擎列表,可选: "exa", "tavily", "ddg","bing" +enabled_engines = ["ddg"] # 启用的搜索引擎列表,可选: "exa", "tavily", "ddg","bing", "metaso" search_strategy = "single" # 搜索策略: "single"(使用第一个可用引擎), "parallel"(并行使用所有启用的引擎), "fallback"(按顺序尝试,失败则尝试下一个) [sleep_system]