agent-Specialization/scripts/qq_bot/message_handler.py

578 lines
22 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""消息处理器"""
import asyncio
import logging
import re
from typing import Optional, Dict, Any, List
from .config import TARGET_GROUP_ID, POLL_INTERVAL, POLL_TIMEOUT
from .napcat_client import NapCatClient
from .web_api_client import WebAPIClient
from .state_manager import StateManager
from . import config # 导入 config 模块,而不是导入 BOT_QQ_ID
logger = logging.getLogger(__name__)
class MessageHandler:
"""消息处理器"""
def __init__(
self,
napcat_client: NapCatClient,
web_client: WebAPIClient,
state_manager: StateManager,
):
self.napcat = napcat_client
self.web = web_client
self.state = state_manager
async def handle_message(self, message: str, user_id: int) -> bool:
"""处理群消息
Args:
message: 消息内容
user_id: 发送者 QQ 号
Returns:
是否处理了该消息
"""
logger.debug(f"handle_message 被调用: message={message}, user_id={user_id}")
logger.debug(f"BOT_QQ_ID={config.BOT_QQ_ID}")
# 检查是否 @ 机器人
is_mentioned = self._is_mentioned(message)
logger.debug(f"是否 @ 机器人: {is_mentioned}")
if not is_mentioned:
logger.debug("未 @ 机器人,忽略消息")
return False
# 移除 @ 标记
clean_msg = self._clean_message(message)
logger.info(f"处理消息: {clean_msg}")
# 解析指令
if clean_msg.startswith("/"):
logger.info(f"处理指令: {clean_msg}")
await self._handle_command(clean_msg)
else:
# 普通消息
logger.info(f"处理普通消息: {clean_msg}")
await self._handle_chat_message(clean_msg)
return True
def _is_mentioned(self, message: str) -> bool:
"""检查是否 @ 了机器人"""
logger.debug(f"检查是否 @ 机器人: BOT_QQ_ID={config.BOT_QQ_ID}, message={message}")
if not config.BOT_QQ_ID:
logger.warning("BOT_QQ_ID 未设置")
return False
# 检查 CQ 码格式的 at
pattern = rf"\[CQ:at,qq={config.BOT_QQ_ID}\]"
result = bool(re.search(pattern, message))
logger.debug(f"匹配结果: {result}, pattern={pattern}")
return result
def _clean_message(self, message: str) -> str:
"""清理消息,移除 @ 标记"""
# 移除所有 CQ:at 标记
cleaned = re.sub(r"\[CQ:at,qq=\d+\]", "", message)
return cleaned.strip()
async def _handle_command(self, command: str):
"""处理指令"""
parts = command.split(maxsplit=1)
cmd = parts[0].lower()
args = parts[1] if len(parts) > 1 else ""
try:
if cmd == "/help":
await self._cmd_help()
elif cmd == "/test":
await self._cmd_test()
elif cmd == "/new":
await self._cmd_new()
elif cmd == "/resume":
limit = int(args) if args.isdigit() else 10
await self._cmd_resume(limit)
elif cmd == "/switch":
if not args:
await self.napcat.send_group_msg(
TARGET_GROUP_ID, "❌ 请提供对话 ID例如/switch conv_xxx"
)
return
await self._cmd_switch(args.strip())
elif cmd == "/stop":
await self._cmd_stop()
else:
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 未知指令: {cmd}\n发送 /help 查看帮助"
)
except Exception as e:
logger.error(f"处理指令失败: {e}", exc_info=True)
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 指令执行失败: {str(e)}"
)
async def _cmd_help(self):
"""显示帮助信息"""
help_text = """🤖 QQ Bot 使用帮助
📝 基本指令:
/help - 显示此帮助信息
/new - 创建新对话
/resume [数量] - 查看最近的对话列表默认10条
/switch <对话ID> - 切换到指定对话
/stop - 停止当前运行的任务
/test - 测试自定义工具是否可用
💬 发送消息:
直接 @ 我并输入消息即可与 AI 对话
例如:@机器人 今天天气怎么样?
📌 注意事项:
• 只能在空闲时发送消息或执行指令(/stop 除外)
• 运行期间的消息会被忽略
• 模型可以使用工具执行命令、搜索网页等"""
await self.napcat.send_group_msg(TARGET_GROUP_ID, help_text)
async def _cmd_test(self):
"""测试自定义工具"""
try:
# 发送一个简单的测试消息
message = "请告诉我你有哪些可用的工具?特别是有没有 send_qq_file 工具?"
await self._send_message(message)
except Exception as e:
logger.error(f"测试失败: {e}", exc_info=True)
await self.napcat.send_group_msg(TARGET_GROUP_ID, f"❌ 测试失败: {str(e)}")
async def _cmd_new(self):
"""创建新对话"""
state = self.state.get_state()
if state.is_running():
await self.napcat.send_group_msg(
TARGET_GROUP_ID, "❌ 当前有任务正在运行,请等待完成或使用 /stop 停止"
)
return
result = await self.web.create_conversation(thinking_mode=False, mode="fast")
if result.get("success"):
conv_id = result.get("conversation_id")
state.set_conversation(conv_id)
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"✅ 已创建新对话: {conv_id}"
)
else:
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 创建对话失败: {result.get('error')}"
)
async def _cmd_resume(self, limit: int):
"""获取最近对话列表"""
state = self.state.get_state()
if state.is_running():
await self.napcat.send_group_msg(
TARGET_GROUP_ID, "❌ 当前有任务正在运行,请等待完成或使用 /stop 停止"
)
return
result = await self.web.list_conversations(limit=limit)
if not result.get("success"):
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 获取对话列表失败: {result.get('error')}"
)
return
conversations = result.get("data", {}).get("conversations", [])
if not conversations:
await self.napcat.send_group_msg(TARGET_GROUP_ID, "📋 暂无对话记录")
return
# 格式化对话列表
lines = [f"📋 最近 {len(conversations)} 条对话:\n"]
for conv in conversations:
conv_id = conv.get("id", "")
title = conv.get("title", "无标题")
lines.append(f"{conv_id}\n {title}")
message = "\n".join(lines)
await self.napcat.send_group_msg(TARGET_GROUP_ID, message)
async def _cmd_switch(self, conversation_id: str):
"""切换对话"""
state = self.state.get_state()
if state.is_running():
await self.napcat.send_group_msg(
TARGET_GROUP_ID, "❌ 当前有任务正在运行,请等待完成或使用 /stop 停止"
)
return
result = await self.web.load_conversation(conversation_id)
if result.get("success"):
state.set_conversation(conversation_id)
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"✅ 已切换到对话: {conversation_id}"
)
else:
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 切换对话失败: {result.get('error')}"
)
async def _cmd_stop(self):
"""停止当前任务"""
state = self.state.get_state()
if not state.is_running():
await self.napcat.send_group_msg(TARGET_GROUP_ID, " 当前没有运行中的任务")
return
if not state.task_id:
await self.napcat.send_group_msg(TARGET_GROUP_ID, "❌ 任务 ID 丢失")
state.finish_task()
return
result = await self.web.cancel_task(state.task_id)
if result.get("success"):
await self.napcat.send_group_msg(TARGET_GROUP_ID, "✅ 已停止任务")
state.finish_task()
else:
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 停止任务失败: {result.get('error')}"
)
async def _handle_chat_message(self, message: str):
"""处理普通聊天消息"""
state = self.state.get_state()
# 检查是否空闲
if state.is_running():
logger.info("任务运行中,忽略消息")
return
# 检查是否有对话
if not state.conversation_id:
# 自动创建对话
logger.info("自动创建对话...")
result = await self.web.create_conversation(thinking_mode=False, mode="fast")
if result.get("success"):
conv_id = result.get("conversation_id")
state.set_conversation(conv_id)
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"✅ 已自动创建对话: {conv_id}"
)
else:
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 创建对话失败: {result.get('error')}"
)
return
# 发送消息
try:
logger.info(f"发送消息到对话 {state.conversation_id}: {message}")
result = await self.web.send_message(message, state.conversation_id)
if not result.get("success"):
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 发送消息失败: {result.get('error')}"
)
return
task_id = result.get("data", {}).get("task_id")
if not task_id:
await self.napcat.send_group_msg(TARGET_GROUP_ID, "❌ 未获取到任务 ID")
return
logger.info(f"任务已创建: {task_id}")
state.start_task(task_id)
# 开始轮询任务
logger.info(f"开始轮询任务 {task_id}")
await self._poll_task(task_id)
except Exception as e:
logger.error(f"处理消息失败: {e}", exc_info=True)
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 处理消息失败: {str(e)}"
)
state.finish_task()
async def _poll_task(self, task_id: str):
"""轮询任务直到完成"""
state = self.state.get_state()
timeout_counter = 0
max_timeout = POLL_TIMEOUT / POLL_INTERVAL
logger.info(f"开始轮询任务 {task_id},间隔 {POLL_INTERVAL}")
while state.is_running():
try:
logger.debug(f"轮询任务 {task_id}offset={state.last_offset}")
result = await self.web.poll_task(task_id, state.last_offset)
if not result.get("success"):
logger.error(f"轮询任务失败: {result}")
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 任务执行失败: {result.get('error')}"
)
state.finish_task()
break
data = result.get("data", {})
status = data.get("status")
events = data.get("events", [])
next_offset = data.get("next_offset", state.last_offset)
logger.debug(f"任务状态: {status}, 事件数: {len(events)}, next_offset: {next_offset}")
# 更新偏移量
state.last_offset = next_offset
# 处理事件
if events:
logger.info(f"处理 {len(events)} 个事件")
await self._process_events(events)
# 检查任务状态
if status in {"succeeded", "failed", "cancelled"}:
logger.info(f"任务完成,状态: {status}")
if status == "failed":
await self.napcat.send_group_msg(
TARGET_GROUP_ID, "❌ 任务执行失败"
)
elif status == "cancelled":
await self.napcat.send_group_msg(
TARGET_GROUP_ID, " 任务已取消"
)
state.finish_task()
break
# 超时检查
timeout_counter += 1
if timeout_counter > max_timeout:
logger.warning("任务超时")
await self.napcat.send_group_msg(
TARGET_GROUP_ID, "⚠️ 任务超时,已自动停止"
)
await self.web.cancel_task(task_id)
state.finish_task()
break
# 等待下次轮询
await asyncio.sleep(POLL_INTERVAL)
except Exception as e:
logger.error(f"轮询任务异常: {e}", exc_info=True)
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 任务执行异常: {str(e)}"
)
state.finish_task()
break
async def _process_events(self, events: List[Dict[str, Any]]):
"""处理任务事件"""
for event in events:
event_type = event.get("type")
event_data = event.get("data", {})
logger.debug(f"收到事件: type={event_type}, data_keys={list(event_data.keys())}")
try:
if event_type == "tool_start":
# 工具开始执行
tool_name = data.get("name", "")
logger.info(f"工具开始: {tool_name}")
await self._handle_tool_start(event_data)
# 检测到 send_qq_file 工具,记录工具 ID
if tool_name == "send_qq_file":
tool_id = event_data.get("id", "")
logger.info(f"检测到 send_qq_file 工具调用ID: {tool_id}")
# 保存到状态,等待 update_action
state = self.state.get_state()
if not hasattr(state, 'pending_send_file_tools'):
state.pending_send_file_tools = set()
state.pending_send_file_tools.add(tool_id)
elif event_type == "update_action":
# 工具执行完成(包含结果)
tool_id = event_data.get("id", "")
status = event_data.get("status", "")
result = event_data.get("result", {})
# 检查是否是 send_qq_file 工具
state = self.state.get_state()
if hasattr(state, 'pending_send_file_tools') and tool_id in state.pending_send_file_tools:
logger.info(f"收到 send_qq_file 工具结果status: {status}")
state.pending_send_file_tools.remove(tool_id)
# 处理文件发送
if status == "success":
await self._handle_send_file_result(result)
elif event_type == "text_end":
# 模型输出完成
await self._handle_text_end(event_data)
except Exception as e:
logger.error(f"处理事件失败: {event_type}, {e}", exc_info=True)
async def _handle_tool_start(self, data: Dict[str, Any]):
"""处理工具调用开始"""
tool_name = data.get("name", "unknown")
arguments = data.get("arguments", {})
# 格式化工具调用信息
lines = [f"[工具] {tool_name}"]
# 提取关键参数
if tool_name == "run_command":
cmd = arguments.get("command", "")
lines.append(f"参数: {cmd}")
elif tool_name == "web_search":
query = arguments.get("query", "")
lines.append(f"参数: {query}")
elif tool_name == "read_file":
path = arguments.get("path", "")
lines.append(f"参数: {path}")
elif tool_name == "write_file":
path = arguments.get("path", "")
lines.append(f"参数: {path}")
else:
# 其他工具,显示所有参数
if arguments:
args_str = ", ".join(f"{k}={v}" for k, v in arguments.items())
lines.append(f"参数: {args_str}")
message = "\n".join(lines)
await self.napcat.send_group_msg(TARGET_GROUP_ID, message)
async def _handle_text_end(self, data: Dict[str, Any]):
"""处理模型输出完成"""
content = data.get("full_content", "")
if content:
# 发送模型输出
await self.napcat.send_group_msg(TARGET_GROUP_ID, content)
async def _handle_send_file_result(self, result: Dict[str, Any]):
"""处理 send_qq_file 工具结果"""
try:
# result 可能是字符串或字典
if isinstance(result, str):
import json
result_data = json.loads(result)
else:
result_data = result
if not result_data.get("success"):
error = result_data.get("error", "未知错误")
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 发送文件失败: {error}"
)
return
file_path = result_data.get("file_path", "")
description = result_data.get("description", "")
if not file_path:
logger.warning("send_qq_file 工具未提供文件路径")
return
# 转换容器内路径到宿主机路径
# 容器内: /workspace/xxx -> 宿主机: users/qqbot/project/xxx
if file_path.startswith("/workspace/"):
relative_path = file_path[len("/workspace/"):]
# 获取项目根目录
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
host_path = os.path.join(project_root, "users", "qqbot", "project", relative_path)
else:
# 如果不是 /workspace 开头,假设是相对路径
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
host_path = os.path.join(project_root, "users", "qqbot", "project", file_path)
logger.info(f"转换文件路径: {file_path} -> {host_path}")
# 检查文件是否存在
if not os.path.exists(host_path):
logger.error(f"文件不存在: {host_path}")
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 文件不存在: {file_path}"
)
return
# 发送文件到 QQ 群
await self.napcat.send_file(TARGET_GROUP_ID, host_path, description)
logger.info(f"已发送文件: {host_path}")
except json.JSONDecodeError as e:
logger.error(f"解析工具输出失败: {e}")
except Exception as e:
logger.error(f"发送文件失败: {e}", exc_info=True)
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 发送文件失败: {str(e)}"
)
async def _handle_tool_result(self, data: Dict[str, Any]):
"""处理工具执行结果"""
tool_name = data.get("name", "")
# 只处理 send_qq_file 工具
if tool_name != "send_qq_file":
return
# 解析工具输出
output = data.get("output", "")
if not output:
logger.warning("send_qq_file 工具无输出")
return
try:
import json
result = json.loads(output)
if not result.get("success"):
error = result.get("error", "未知错误")
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 发送文件失败: {error}"
)
return
file_path = result.get("file_path", "")
description = result.get("description", "")
if not file_path:
logger.warning("send_qq_file 工具未提供文件路径")
return
# 转换容器内路径到宿主机路径
# 容器内: /workspace/xxx -> 宿主机: users/qqbot/project/xxx
if file_path.startswith("/workspace/"):
relative_path = file_path[len("/workspace/"):]
# 获取项目根目录
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
host_path = os.path.join(project_root, "users", "qqbot", "project", relative_path)
else:
# 如果不是 /workspace 开头,假设是相对路径
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
host_path = os.path.join(project_root, "users", "qqbot", "project", file_path)
logger.info(f"转换文件路径: {file_path} -> {host_path}")
# 检查文件是否存在
if not os.path.exists(host_path):
logger.error(f"文件不存在: {host_path}")
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 文件不存在: {file_path}"
)
return
# 发送文件到 QQ 群
await self.napcat.send_file(TARGET_GROUP_ID, host_path, description)
logger.info(f"已发送文件: {host_path}")
except json.JSONDecodeError as e:
logger.error(f"解析工具输出失败: {e}")
except Exception as e:
logger.error(f"发送文件失败: {e}", exc_info=True)
await self.napcat.send_group_msg(
TARGET_GROUP_ID, f"❌ 发送文件失败: {str(e)}"
)