feta:优化联网搜索

This commit is contained in:
2025-12-30 16:04:00 +08:00
parent 5b130485f0
commit dc20e580a0
2 changed files with 162 additions and 1 deletions

View File

@@ -1370,6 +1370,8 @@ class AIChat(PluginBase):
return True
if re.search(r"(什么|咋|怎么|如何|为啥|为什么|哪|哪里|哪个|多少|推荐|值不值得|值得吗|好不好|靠谱吗|评价|口碑|怎么样|如何评价|近况|最新|最近)", t):
return True
if re.search(r"\\b(what|who|when|where|why|how|details?|impact|latest|news|review|rating|price|info|information)\\b", t):
return True
# 明确的实体/对象询问(公会/游戏/公司/项目等)
if re.search(r"(公会|战队|服务器|区服|游戏|公司|品牌|产品|软件|插件|项目|平台|up主|主播|作者|电影|电视剧|小说)", t) and len(t) >= 8:
@@ -1377,6 +1379,42 @@ class AIChat(PluginBase):
return False
def _extract_legacy_text_search_tool_call(self, text: str) -> tuple[str, dict] | None:
"""
解析模型偶发输出的“文本工具调用”写法(例如 tavilywebsearch{query:...}),并转换为真实工具调用参数。
"""
raw = str(text or "")
if not raw:
return None
# 去掉 <ctrl46> 之类的控制标记
cleaned = re.sub(r"<ctrl\\d+>", "", raw, flags=re.IGNORECASE)
m = re.search(
r"(?i)\\b(?P<tool>tavilywebsearch|tavily_web_search|web_search)\\s*\\{\\s*query\\s*[:=]\\s*(?P<q>[^{}]{1,800})\\}",
cleaned,
)
if not m:
m = re.search(
r"(?i)\\b(?P<tool>tavilywebsearch|tavily_web_search|web_search)\\s*\\(\\s*query\\s*[:=]\\s*(?P<q>[^\\)]{1,800})\\)",
cleaned,
)
if not m:
return None
tool = str(m.group("tool") or "").strip().lower()
query = str(m.group("q") or "").strip().strip("\"'`")
if not query:
return None
# 统一映射到项目实际存在的工具名
if tool in ("tavilywebsearch", "tavily_web_search"):
tool_name = "tavily_web_search"
else:
tool_name = "web_search"
return tool_name, {"query": query[:400]}
def _intent_to_allowed_tool_names(self, intent: str) -> set[str]:
intent = str(intent or "").strip().lower()
mapping = {
@@ -1656,7 +1694,7 @@ class AIChat(PluginBase):
allow.add("view_calendar")
# 搜索/资讯
if re.search(r"(联网|搜索|搜一下|搜一搜|搜搜|帮我搜|搜新闻|搜资料|查资料|查新闻|查价格)", t):
if re.search(r"(联网|搜索|搜一下|搜一搜|搜搜|帮我搜|搜新闻|搜资料|查资料|查新闻|查价格|\\bsearch\\b|\\bgoogle\\b|\\blookup\\b|\\bfind\\b|\\bnews\\b|\\blatest\\b|\\bdetails?\\b|\\bimpact\\b)", t):
# 兼容旧工具名与当前插件实现
allow.add("tavily_web_search")
allow.add("web_search")
@@ -2266,6 +2304,11 @@ class AIChat(PluginBase):
# 收集工具
all_tools = self._collect_tools()
available_tool_names = {
t.get("function", {}).get("name", "")
for t in (all_tools or [])
if isinstance(t, dict) and t.get("function", {}).get("name")
}
tools = await self._select_tools_for_message_async(all_tools, user_message=user_message, tool_query=tool_query)
logger.info(f"收集到 {len(all_tools)} 个工具函数,本次启用 {len(tools)}")
if tools:
@@ -2482,6 +2525,45 @@ class AIChat(PluginBase):
logger.warning(f"检测到未提供/未知的工具调用,已忽略: {unsupported}")
tool_calls_data = filtered
# 兼容:模型偶发输出“文本工具调用”写法(不走 tool_calls尝试转成真实工具调用
if not tool_calls_data and full_content:
legacy = self._extract_legacy_text_search_tool_call(full_content)
if legacy:
legacy_tool, legacy_args = legacy
# 兼容:有的模型会用旧名字/文本格式输出搜索工具调用
# 1) 优先映射到“本次提供给模型的工具”(尊重 smart_select
# 2) 若本次未提供搜索工具但用户确实在问信息类问题,可降级启用全局可用的搜索工具(仅限搜索)
preferred = None
if legacy_tool in allowed_tool_names:
preferred = legacy_tool
elif "tavily_web_search" in allowed_tool_names:
preferred = "tavily_web_search"
elif "web_search" in allowed_tool_names:
preferred = "web_search"
elif self._looks_like_info_query(user_message):
if "tavily_web_search" in available_tool_names:
preferred = "tavily_web_search"
elif "web_search" in available_tool_names:
preferred = "web_search"
if preferred:
logger.warning(f"检测到文本形式工具调用,已转换为 Function Calling: {preferred}")
try:
if bot and from_wxid:
await bot.send_text(from_wxid, "我帮你查一下,稍等。")
except Exception:
pass
tool_calls_data = [
{
"id": f"legacy_{uuid.uuid4().hex[:8]}",
"type": "function",
"function": {
"name": preferred,
"arguments": json.dumps(legacy_args, ensure_ascii=False),
},
}
]
logger.info(f"流式 API 响应完成, 内容长度: {len(full_content)}, 工具调用数: {len(tool_calls_data)}")
# 检查是否有函数调用
@@ -4112,6 +4194,11 @@ class AIChat(PluginBase):
"""调用AI API带图片"""
api_config = self.config["api"]
all_tools = self._collect_tools()
available_tool_names = {
t.get("function", {}).get("name", "")
for t in (all_tools or [])
if isinstance(t, dict) and t.get("function", {}).get("name")
}
tools = await self._select_tools_for_message_async(all_tools, user_message=user_message, tool_query=tool_query)
logger.info(f"[图片] 收集到 {len(all_tools)} 个工具函数,本次启用 {len(tools)}")
if tools:
@@ -4338,6 +4425,72 @@ class AIChat(PluginBase):
)
return None
# 兼容:文本形式工具调用
if full_content:
legacy = self._extract_legacy_text_search_tool_call(full_content)
if legacy:
legacy_tool, legacy_args = legacy
# 仅允许转成“本次实际提供给模型的工具”,避免绕过 smart_select
allowed_tool_names = {
t.get("function", {}).get("name", "")
for t in (tools or [])
if isinstance(t, dict) and t.get("function", {}).get("name")
}
preferred = None
if legacy_tool in allowed_tool_names:
preferred = legacy_tool
elif "tavily_web_search" in allowed_tool_names:
preferred = "tavily_web_search"
elif "web_search" in allowed_tool_names:
preferred = "web_search"
elif self._looks_like_info_query(user_message):
if "tavily_web_search" in available_tool_names:
preferred = "tavily_web_search"
elif "web_search" in available_tool_names:
preferred = "web_search"
if preferred:
logger.warning(f"[图片] 检测到文本形式工具调用,已转换为 Function Calling: {preferred}")
try:
if bot and from_wxid:
await bot.send_text(from_wxid, "我帮你查一下,稍等。")
except Exception:
pass
tool_calls_data = [
{
"id": f"legacy_{uuid.uuid4().hex[:8]}",
"type": "function",
"function": {
"name": preferred,
"arguments": json.dumps(legacy_args, ensure_ascii=False),
},
}
]
try:
await self._record_tool_calls_to_context(
tool_calls_data,
from_wxid=from_wxid,
chat_id=chat_id,
is_group=is_group,
user_wxid=user_wxid,
)
except Exception:
pass
asyncio.create_task(
self._execute_tools_async_with_image(
tool_calls_data,
bot,
from_wxid,
chat_id,
user_wxid,
nickname,
is_group,
messages,
image_base64,
)
)
return None
# 检查是否包含错误的工具调用格式
if "<tool_code>" in full_content or "print(" in full_content and "flow2_ai_image_generation" in full_content:
logger.warning("检测到模型输出了错误的工具调用格式,拦截并返回提示")