fix: 修复 'ToolResult' object has no attribute 'get'
根因: cli_tools.process_tool_call 在启用 UnifiedToolManager 时直接返回
ToolResult 对象,而 7 个调用点全部按 dict 用 result.get('result'),触发
AttributeError;FC 路径 _execute_tool_by_name 也可能把 ToolResult 透传出去,
后续 result[:5000] 会再炸 TypeError。
- tool_result.py: 新增 normalize_tool_text / normalize_tool_dict,
统一收敛 ToolResult / dict / str / None 三种历史返回形态。
- cli_tools.process_tool_call: 统一返回 dict(附带 success/error_code/
tool_result),回退 handler 路径同样归一。
- cli_core._execute_tool_by_name: handler / memory / scheduler 返回值
归一成 str,保证 FC 路径可安全切片。
- openai/ollama/manual 引擎、websocket_server、cli_clawli: 取工具结果改用
normalize_tool_text(带本地兜底实现,避免循环导入)。
- cli_clawli: 顺带修复只回传最后一个工具结果的 bug,改为汇总全部。
- 测试: 23 项全过;纯格式/讯飞 500 降级回归冒烟通过。
This commit is contained in:
1 parent
dfc42d8583
commit
a63b9d2f97
8 files changed
+129
-20
No files matched your search
@@ -18,6 +18,17 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
|
|||||||
project_root = os.path.dirname(current_dir)
|
project_root = os.path.dirname(current_dir)
|
||||||
sys.path.insert(0, project_root)
|
sys.path.insert(0, project_root)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from xcli_core.tool_result import normalize_tool_text
|
||||||
|
except Exception:
|
||||||
|
def normalize_tool_text(result, default="无结果"):
|
||||||
|
"""兜底归一化:ToolResult / dict / str 统一成字符串"""
|
||||||
|
if result is None:
|
||||||
|
return default
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return str(result.get("result", default))
|
||||||
|
return str(result)
|
||||||
|
|
||||||
# 通信用的临时目录
|
# 通信用的临时目录
|
||||||
COMM_DIR = os.path.join(tempfile.gettempdir(), "xiaoli_manual_engine")
|
COMM_DIR = os.path.join(tempfile.gettempdir(), "xiaoli_manual_engine")
|
||||||
|
|
||||||
@@ -246,7 +257,7 @@ while True:
|
|||||||
# 把用户消息写入文件
|
# 把用户消息写入文件
|
||||||
msg = user_input
|
msg = user_input
|
||||||
if tool_results:
|
if tool_results:
|
||||||
msg = f"[工具结果] {tool_results.get('result', '无结果')}\n\n{user_input}"
|
msg = f"[工具结果] {normalize_tool_text(tool_results)}\n\n{user_input}"
|
||||||
|
|
||||||
to_human = paths["msg_to_human"]
|
to_human = paths["msg_to_human"]
|
||||||
with open(to_human, 'w', encoding='utf-8') as f:
|
with open(to_human, 'w', encoding='utf-8') as f:
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ except Exception:
|
|||||||
def resolve_input_value(value, field_name="", default_prompt=None):
|
def resolve_input_value(value, field_name="", default_prompt=None):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
try:
|
||||||
|
from xcli_core.tool_result import normalize_tool_text
|
||||||
|
except Exception:
|
||||||
|
def normalize_tool_text(result, default="无结果"):
|
||||||
|
"""兜底归一化:ToolResult / dict / str 统一成字符串"""
|
||||||
|
if result is None:
|
||||||
|
return default
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return str(result.get("result", default))
|
||||||
|
return str(result)
|
||||||
|
|
||||||
|
|
||||||
class OllamaAI:
|
class OllamaAI:
|
||||||
"""基于Ollama的本地AI引擎"""
|
"""基于Ollama的本地AI引擎"""
|
||||||
@@ -164,7 +175,7 @@ Ollama AI引擎插件帮助信息
|
|||||||
|
|
||||||
# 如果有工具结果,将其作为上下文返回给AI处理
|
# 如果有工具结果,将其作为上下文返回给AI处理
|
||||||
if tool_results:
|
if tool_results:
|
||||||
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
|
tool_result_text = f"工具执行结果: {normalize_tool_text(tool_results)}"
|
||||||
messages = self._build_messages_with_history(system_prompt, tool_result_text)
|
messages = self._build_messages_with_history(system_prompt, tool_result_text)
|
||||||
else:
|
else:
|
||||||
messages = self._build_messages_with_history(system_prompt, user_input)
|
messages = self._build_messages_with_history(system_prompt, user_input)
|
||||||
|
|||||||
@@ -19,6 +19,17 @@ except Exception:
|
|||||||
def get_system_config(key, default=None):
|
def get_system_config(key, default=None):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
try:
|
||||||
|
from xcli_core.tool_result import normalize_tool_text
|
||||||
|
except Exception:
|
||||||
|
def normalize_tool_text(result, default="无结果"):
|
||||||
|
"""兜底归一化:ToolResult / dict / str 统一成字符串"""
|
||||||
|
if result is None:
|
||||||
|
return default
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return str(result.get("result", default))
|
||||||
|
return str(result)
|
||||||
|
|
||||||
# ── 常见模型的上下文窗口(token 数)──
|
# ── 常见模型的上下文窗口(token 数)──
|
||||||
# OpenAI /chat/completions 协议本身不返回模型窗口,需本地维护。
|
# OpenAI /chat/completions 协议本身不返回模型窗口,需本地维护。
|
||||||
MODEL_CONTEXT_WINDOWS = {
|
MODEL_CONTEXT_WINDOWS = {
|
||||||
@@ -172,7 +183,7 @@ OpenAI 兼容格式引擎帮助信息
|
|||||||
|
|
||||||
# 处理工具结果
|
# 处理工具结果
|
||||||
if tool_results:
|
if tool_results:
|
||||||
tool_result_text = f"工具执行结果: {tool_results.get('result', '无结果')}"
|
tool_result_text = f"工具执行结果: {normalize_tool_text(tool_results)}"
|
||||||
messages = self._build_messages_with_history(system_prompt, tool_result_text)
|
messages = self._build_messages_with_history(system_prompt, tool_result_text)
|
||||||
else:
|
else:
|
||||||
messages = self._build_messages_with_history(system_prompt, user_input)
|
messages = self._build_messages_with_history(system_prompt, user_input)
|
||||||
|
|||||||
+13
-2
@@ -24,6 +24,17 @@ import os
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Dict, List
|
from typing import Optional, Dict, List
|
||||||
|
|
||||||
|
try:
|
||||||
|
from xcli_core.tool_result import normalize_tool_text
|
||||||
|
except Exception:
|
||||||
|
def normalize_tool_text(result, default="无结果"):
|
||||||
|
"""兜底归一化:ToolResult / dict / str 统一成字符串"""
|
||||||
|
if result is None:
|
||||||
|
return default
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return str(result.get("result", default))
|
||||||
|
return str(result)
|
||||||
|
|
||||||
# 全局变量
|
# 全局变量
|
||||||
websocket_server = None # 当前活动的服务器实例(供外部访问)
|
websocket_server = None # 当前活动的服务器实例(供外部访问)
|
||||||
proxy_client = None
|
proxy_client = None
|
||||||
@@ -583,7 +594,7 @@ class LocalServer:
|
|||||||
await self._send_tool_status(websocket, tool_name, tool_args, "calling")
|
await self._send_tool_status(websocket, tool_name, tool_args, "calling")
|
||||||
|
|
||||||
result = self.cli.process_tool_call(tool_data)
|
result = self.cli.process_tool_call(tool_data)
|
||||||
result_text = result.get('result', '无结果')
|
result_text = normalize_tool_text(result)
|
||||||
results.append(result_text)
|
results.append(result_text)
|
||||||
|
|
||||||
if websocket:
|
if websocket:
|
||||||
@@ -957,7 +968,7 @@ class ProxyClient:
|
|||||||
results = []
|
results = []
|
||||||
for tool_data in tool_calls:
|
for tool_data in tool_calls:
|
||||||
result = self.cli.process_tool_call(tool_data)
|
result = self.cli.process_tool_call(tool_data)
|
||||||
results.append(result.get('result', '无结果'))
|
results.append(normalize_tool_text(result))
|
||||||
|
|
||||||
self.cli.shared_conversation_history.append({
|
self.cli.shared_conversation_history.append({
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import time
|
|||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
|
|
||||||
from .constants import CLAWLI_SERVER_AVAILABLE, WEBSOCKET_AVAILABLE
|
from .constants import CLAWLI_SERVER_AVAILABLE, WEBSOCKET_AVAILABLE
|
||||||
|
from .tool_result import normalize_tool_text
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import clawli_server
|
import clawli_server
|
||||||
@@ -96,6 +97,7 @@ class ClawliMixin:
|
|||||||
tool_calls = self._extract_tool_calls(json_data)
|
tool_calls = self._extract_tool_calls(json_data)
|
||||||
|
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
|
result_texts = []
|
||||||
for tool_data in tool_calls:
|
for tool_data in tool_calls:
|
||||||
tool_name = tool_data.get('tool', '未知工具')
|
tool_name = tool_data.get('tool', '未知工具')
|
||||||
tool_args = tool_data.get('args', '')
|
tool_args = tool_data.get('args', '')
|
||||||
@@ -104,7 +106,8 @@ class ClawliMixin:
|
|||||||
clawli_server.send_tool_status(tool_name, tool_args, "calling")
|
clawli_server.send_tool_status(tool_name, tool_args, "calling")
|
||||||
|
|
||||||
result = self.process_tool_call(tool_data)
|
result = self.process_tool_call(tool_data)
|
||||||
result_text = result.get('result', '无结果')
|
result_text = normalize_tool_text(result)
|
||||||
|
result_texts.append(result_text)
|
||||||
|
|
||||||
if CLAWLI_SERVER_AVAILABLE and clawli_server:
|
if CLAWLI_SERVER_AVAILABLE and clawli_server:
|
||||||
clawli_server.send_tool_status(tool_name, tool_args, "success", result_text)
|
clawli_server.send_tool_status(tool_name, tool_args, "success", result_text)
|
||||||
@@ -114,7 +117,8 @@ class ClawliMixin:
|
|||||||
"content": response
|
"content": response
|
||||||
})
|
})
|
||||||
|
|
||||||
tool_result_str = "\n".join([f"工具执行结果: {r}" for r in [result.get('result', '')]])
|
# 汇总全部工具结果(此前只回传了最后一个)
|
||||||
|
tool_result_str = "\n".join([f"工具执行结果: {r}" for r in result_texts])
|
||||||
self._process_clawli_user_message(f"{tool_result_str}\n请根据工具执行结果继续回答。")
|
self._process_clawli_user_message(f"{tool_result_str}\n请根据工具执行结果继续回答。")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -812,7 +812,13 @@ multi 操作支持一次修改多处:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _execute_tool_by_name(self, tool_name: str, tool_args: str) -> str:
|
def _execute_tool_by_name(self, tool_name: str, tool_args: str) -> str:
|
||||||
"""根据名称执行工具(FC 调用路径)"""
|
"""根据名称执行工具(FC 调用路径)
|
||||||
|
|
||||||
|
注意:插件 handler 可能返回 ToolResult / dict / str,
|
||||||
|
这里统一归一成 str,保证调用方能安全做切片与拼接。
|
||||||
|
"""
|
||||||
|
from .tool_result import normalize_tool_text
|
||||||
|
|
||||||
# 安全检查
|
# 安全检查
|
||||||
safety = get_safety()
|
safety = get_safety()
|
||||||
allowed, msg = safety.check(tool_name, tool_args)
|
allowed, msg = safety.check(tool_name, tool_args)
|
||||||
@@ -823,20 +829,20 @@ multi 操作支持一次修改多处:
|
|||||||
tool = self.liugin_manager.get_tool_by_name(tool_name)
|
tool = self.liugin_manager.get_tool_by_name(tool_name)
|
||||||
if tool and 'handler' in tool:
|
if tool and 'handler' in tool:
|
||||||
try:
|
try:
|
||||||
return tool['handler'](tool_args)
|
return normalize_tool_text(tool['handler'](tool_args), default="执行完成")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"工具执行失败: {e}"
|
return f"工具执行失败: {e}"
|
||||||
|
|
||||||
# memory 内置工具
|
# memory 内置工具
|
||||||
if tool_name == 'memory' and hasattr(self, 'memory_manager'):
|
if tool_name == 'memory' and hasattr(self, 'memory_manager'):
|
||||||
return self.memory_manager.handle_tool(tool_args)
|
return normalize_tool_text(self.memory_manager.handle_tool(tool_args), default="执行完成")
|
||||||
|
|
||||||
# scheduler 内置工具
|
# scheduler 内置工具
|
||||||
if tool_name == 'scheduler':
|
if tool_name == 'scheduler':
|
||||||
for t in self.liugin_manager.tools:
|
for t in self.liugin_manager.tools:
|
||||||
if t.get('name') == 'scheduler' and 'handler' in t:
|
if t.get('name') == 'scheduler' and 'handler' in t:
|
||||||
try:
|
try:
|
||||||
return t['handler'](tool_args)
|
return normalize_tool_text(t['handler'](tool_args), default="执行完成")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"工具执行失败: {e}"
|
return f"工具执行失败: {e}"
|
||||||
|
|
||||||
|
|||||||
+10
-9
@@ -7,6 +7,7 @@ from colorama import Fore, Style
|
|||||||
|
|
||||||
from .constants import UNIFIED_TOOL_MANAGER_AVAILABLE
|
from .constants import UNIFIED_TOOL_MANAGER_AVAILABLE
|
||||||
from .safety import get_safety
|
from .safety import get_safety
|
||||||
|
from .tool_result import normalize_tool_dict, normalize_tool_text
|
||||||
|
|
||||||
|
|
||||||
class ToolMixin:
|
class ToolMixin:
|
||||||
@@ -105,18 +106,18 @@ class ToolMixin:
|
|||||||
result = self.liugin_manager.execute(tool_name, args, **arguments)
|
result = self.liugin_manager.execute(tool_name, args, **arguments)
|
||||||
else:
|
else:
|
||||||
result = self.liugin_manager.execute(tool_name, args)
|
result = self.liugin_manager.execute(tool_name, args)
|
||||||
return result
|
# execute() 返回 ToolResult,必须归一成 dict——
|
||||||
|
# 否则上层 result.get('result') 会报
|
||||||
|
# "'ToolResult' object has no attribute 'get'"
|
||||||
|
return normalize_tool_dict(result)
|
||||||
|
|
||||||
# 回退:直接调用 handler(旧逻辑)
|
# 回退:直接调用 handler(旧逻辑,handler 可能返回 ToolResult/str)
|
||||||
for tool in self.liugin_manager.tools:
|
for tool in self.liugin_manager.tools:
|
||||||
if tool['name'] == tool_name:
|
if tool['name'] == tool_name:
|
||||||
try:
|
try:
|
||||||
result = tool['handler'](args)
|
return normalize_tool_dict(tool['handler'](args))
|
||||||
if result is None:
|
|
||||||
result = "无结果"
|
|
||||||
return {"result": result}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"result": f"工具执行错误: {e}"}
|
return {"result": f"工具执行错误: {e}", "success": False}
|
||||||
|
|
||||||
return {"result": f"未找到工具: {tool_name}"}
|
return {"result": f"未找到工具: {tool_name}"}
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ class ToolMixin:
|
|||||||
return {"index": index, "result": "工具执行已取消"}
|
return {"index": index, "result": "工具执行已取消"}
|
||||||
try:
|
try:
|
||||||
result = self.process_tool_call(tool_data)
|
result = self.process_tool_call(tool_data)
|
||||||
tool_result = result.get('result', '无结果')
|
tool_result = normalize_tool_text(result)
|
||||||
with history_lock:
|
with history_lock:
|
||||||
self.shared_conversation_history.append({
|
self.shared_conversation_history.append({
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
@@ -245,7 +246,7 @@ class ToolMixin:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
tool_results = self.process_tool_call(tool_data)
|
tool_results = self.process_tool_call(tool_data)
|
||||||
full_result = tool_results.get('result', '无结果')
|
full_result = normalize_tool_text(tool_results)
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
self._display_tool_result(tool_name, tool_args, full_result, current_time)
|
self._display_tool_result(tool_name, tool_args, full_result, current_time)
|
||||||
|
|||||||
@@ -117,3 +117,57 @@ class ToolResult:
|
|||||||
def to_string(self) -> str:
|
def to_string(self) -> str:
|
||||||
"""转为字符串(兼容旧插件 handle() 返回值)"""
|
"""转为字符串(兼容旧插件 handle() 返回值)"""
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 归一化辅助(跨层兼容:ToolResult / dict / str / None)──
|
||||||
|
|
||||||
|
def normalize_tool_text(result: Any, default: str = "无结果") -> str:
|
||||||
|
"""把任意工具返回值统一成字符串。
|
||||||
|
|
||||||
|
工具链里三种返回形态都可能出现:
|
||||||
|
- ToolResult(新插件 / UnifiedToolManager.execute)
|
||||||
|
- {"result": ...}(旧插件 / to_dict)
|
||||||
|
- 纯字符串(最老的 handler)
|
||||||
|
统一收敛成字符串,避免上层对 ToolResult 调用 .get() 报
|
||||||
|
"'ToolResult' object has no attribute 'get'"。
|
||||||
|
"""
|
||||||
|
if result is None:
|
||||||
|
return default
|
||||||
|
if isinstance(result, ToolResult):
|
||||||
|
text = str(result)
|
||||||
|
elif isinstance(result, dict):
|
||||||
|
text = result.get("result", result.get("content", ""))
|
||||||
|
if isinstance(text, (dict, list)):
|
||||||
|
import json as _json
|
||||||
|
try:
|
||||||
|
text = _json.dumps(text, ensure_ascii=False)
|
||||||
|
except Exception:
|
||||||
|
text = str(text)
|
||||||
|
elif not isinstance(text, str):
|
||||||
|
text = str(text) if text is not None else ""
|
||||||
|
else:
|
||||||
|
text = str(result)
|
||||||
|
text = text.strip() if isinstance(text, str) else str(text)
|
||||||
|
return text if text else default
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_tool_dict(result: Any, default: str = "无结果") -> dict:
|
||||||
|
"""把任意工具返回值统一成 {"result": str, ...} 结构。
|
||||||
|
|
||||||
|
额外附带 success / error_code / tool_result,方便需要细节的调用方,
|
||||||
|
同时保证 .get('result') 一定可用。
|
||||||
|
"""
|
||||||
|
text = normalize_tool_text(result, default)
|
||||||
|
if isinstance(result, ToolResult):
|
||||||
|
return {
|
||||||
|
"result": text,
|
||||||
|
"success": result.success,
|
||||||
|
"error_code": result.error_code,
|
||||||
|
"tool_result": result,
|
||||||
|
}
|
||||||
|
if isinstance(result, dict):
|
||||||
|
out = dict(result)
|
||||||
|
out["result"] = text
|
||||||
|
out.setdefault("success", True)
|
||||||
|
return out
|
||||||
|
return {"result": text, "success": True}
|
||||||
Reference in new issue
Block a user