feat(web_search): 新增 Metaso 搜索引擎支持

为联网搜索工具集成了新的搜索引擎 Metaso,为用户提供更多搜索选择。

- 在搜索引擎基类 `BaseSearchEngine` 中添加了可选的 `read_url` 方法,为未来支持直接读取网页内容奠定基础。
- 更新了插件加载、工具执行逻辑和配置文件模板,以完整支持 Metaso 引擎。
- 将默认回复生成器的任务超时时间从 15 秒延长至 45 秒,以适应联网搜索等耗时较长的操作,提高稳定性。
This commit is contained in:
tt-P607
2025-10-18 00:46:17 +08:00
committed by Windpicker-owo
parent f1342516ca
commit b86f92a457
6 changed files with 126 additions and 6 deletions

View File

@@ -1303,7 +1303,7 @@ class DefaultReplyer:
}
# 设置超时
timeout = 15.0 # 秒
timeout = 45.0 # 秒
async def get_task_result(task_name, task):
try:

View File

@@ -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:
"""

View File

@@ -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 []

View File

@@ -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,6 +52,7 @@ class WEBSEARCHPLUGIN(BasePlugin):
ddg_engine = DDGSearchEngine()
bing_engine = BingSearchEngine()
searxng_engine = SearXNGSearchEngine()
metaso_engine = MetasoSearchEngine()
# 报告每个引擎的状态
engines_status = {
@@ -58,6 +61,7 @@ class WEBSEARCHPLUGIN(BasePlugin):
"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]

View File

@@ -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]:

View File

@@ -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]