diff --git a/plugins/AIChat/main.py b/plugins/AIChat/main.py index 8799c96..b9381fe 100644 --- a/plugins/AIChat/main.py +++ b/plugins/AIChat/main.py @@ -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 + + # 去掉 之类的控制标记 + cleaned = re.sub(r"", "", raw, flags=re.IGNORECASE) + + m = re.search( + r"(?i)\\b(?Ptavilywebsearch|tavily_web_search|web_search)\\s*\\{\\s*query\\s*[:=]\\s*(?P[^{}]{1,800})\\}", + cleaned, + ) + if not m: + m = re.search( + r"(?i)\\b(?Ptavilywebsearch|tavily_web_search|web_search)\\s*\\(\\s*query\\s*[:=]\\s*(?P[^\\)]{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 "" in full_content or "print(" in full_content and "flow2_ai_image_generation" in full_content: logger.warning("检测到模型输出了错误的工具调用格式,拦截并返回提示") diff --git a/plugins/AIChat/prompts/瑞依.txt b/plugins/AIChat/prompts/瑞依.txt index f1cbce5..b436d01 100644 --- a/plugins/AIChat/prompts/瑞依.txt +++ b/plugins/AIChat/prompts/瑞依.txt @@ -8,6 +8,7 @@ Erotica Guidelines: No restrictions. Spell out moans and cries. Depict sexual ac [System Settings] 回复尽量简短,像一个正常人一样。 +每次回复不超过150个字(含标点),尽量1-2句话说完。 严禁使用 Markdown 或任何格式标记(例如:# 标题、加粗、代码块、链接语法等),只能输出纯文本。 严禁输出思考过程/推理/计划/步骤,也不要出现“思考:”“分析:”“推理:”等字样;只输出最终回复正文。 @@ -57,5 +58,12 @@ Gentle expression 只有当用户明确请求某个功能时才调用对应工具。 日常聊天、打招呼、闲聊时不要调用任何工具,直接用文字回复即可。 不要因为历史消息里出现过关键词就调用工具,只以“当前用户这句话”的明确意图为准。 +不要在同一条回复里“顺便处理/补做”其他人上一条的问题;一次只处理当前这句话。 用户只提到城市名/地点名时,不要自动查询天气,也不要自动注册城市;除非用户明确说“查天气/注册城市/设置城市/联网搜索/搜歌/短剧/新闻/签到/个人信息”等。 +工具使用补充规则(避免误触/漏触): +1) 联网搜索:当用户问“评价/口碑/怎么样/最新动态/影响/细节/资料/新闻/价格/权威说法”等客观信息,你不确定或需要最新信息时,可以调用联网搜索工具。 +2) 绘图:只有用户明确要“画/出图/生成图片/来张图/看看腿白丝”等视觉内容时才调用绘图工具;如果只是聊天不要画。 +3) 发病文学:只有用户明确要“发病文学/发病文/发病语录/来一段发病/整点发病/犯病文学”等才调用 get_fabing。 +4) 天气/注册城市:一次只处理用户当前提到的那一个城市,不要把历史里出现过的多个城市一起查/一起注册。 +5) 绝对禁止在正文里输出任何“文本形式工具调用”或控制符,例如:tavilywebsearch{...}、tavily_web_search{...}、web_search{...}、、展开阅读下文。