diff --git a/src/Undefined/onebot.py b/src/Undefined/onebot.py index f0e9cc6..f39f28c 100644 --- a/src/Undefined/onebot.py +++ b/src/Undefined/onebot.py @@ -10,11 +10,19 @@ import websockets from websockets.asyncio.client import ClientConnection +from Undefined.context import RequestContext from Undefined.utils.logging import log_debug_json, redact_string, sanitize_data logger = logging.getLogger(__name__) +def _mark_message_sent_this_turn() -> None: + ctx = RequestContext.current() + if ctx is None: + return + ctx.set_resource("message_sent_this_turn", True) + + class OneBotClient: """OneBot v11 WebSocket 客户端""" @@ -146,25 +154,29 @@ async def send_group_message( self, group_id: int, message: str | list[dict[str, Any]] ) -> dict[str, Any]: """发送群消息""" - return await self._call_api( + result = await self._call_api( "send_group_msg", { "group_id": group_id, "message": message, }, ) + _mark_message_sent_this_turn() + return result async def send_private_message( self, user_id: int, message: str | list[dict[str, Any]] ) -> dict[str, Any]: """发送私聊消息""" - return await self._call_api( + result = await self._call_api( "send_private_msg", { "user_id": user_id, "message": message, }, ) + _mark_message_sent_this_turn() + return result async def get_group_msg_history( self, diff --git a/src/Undefined/services/ai_coordinator.py b/src/Undefined/services/ai_coordinator.py index 8ba281a..708bd6b 100644 --- a/src/Undefined/services/ai_coordinator.py +++ b/src/Undefined/services/ai_coordinator.py @@ -241,6 +241,8 @@ async def send_like_cb(uid: int, times: int = 1) -> None: # 存储资源到上下文 ai_client = self.ai + memory_storage = self.ai.memory_storage + runtime_config = self.ai.runtime_config sender = self.sender history_manager = self.history_manager onebot_client = self.onebot @@ -320,6 +322,8 @@ async def send_private_cb(uid: int, msg: str) -> None: # 存储资源到上下文 ai_client = self.ai + memory_storage = self.ai.memory_storage + runtime_config = self.ai.runtime_config sender = self.sender history_manager = self.history_manager onebot_client = self.onebot diff --git a/src/Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/config.json b/src/Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/config.json index d0224f6..c0896bb 100644 --- a/src/Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/config.json +++ b/src/Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/config.json @@ -9,6 +9,10 @@ "file_path": { "type": "string", "description": "文件路径(相对于当前工作目录)" + }, + "max_chars": { + "type": "integer", + "description": "可选:最大字符数限制。如果文件内容超过此限制,将被截断。不指定则返回完整内容。建议:小文件不指定,大文件可设置为 50000-100000" } }, "required": ["file_path"] diff --git a/src/Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/handler.py b/src/Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/handler.py index c5b58f1..7ba4c4e 100644 --- a/src/Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/handler.py +++ b/src/Undefined/skills/agents/naga_code_analysis_agent/tools/read_file/handler.py @@ -10,6 +10,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: """读取指定文件的详细内容""" # 支持两个参数名以实现兼容性 file_path = args.get("file_path") or args.get("path", "") + max_chars: int | None = args.get("max_chars") if not file_path: return "错误:文件路径不能为空" @@ -57,8 +58,14 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: f"文件 {file_path} 使用 latin-1 解码,可能包含乱码。" ) - if len(file_content) > 10000: - file_content = file_content[:10000] + "\n... (内容过长,已截断)" + # 字符数截断(如果指定了 max_chars) + total_chars = len(file_content) + if max_chars and total_chars > max_chars: + file_content = file_content[:max_chars] + truncated_info = ( + f"\n\n... (文件共 {total_chars} 字符,已截断到前 {max_chars} 字符)" + ) + file_content += truncated_info return file_content diff --git a/src/Undefined/skills/registry.py b/src/Undefined/skills/registry.py index c800f1c..3f1c2ad 100644 --- a/src/Undefined/skills/registry.py +++ b/src/Undefined/skills/registry.py @@ -168,7 +168,7 @@ def _register_item_from_dir(self, item_dir: Path, prefix: str = "") -> None: item = self._build_skill_item(item_dir, config, handler_path, prefix) self._items[item.name] = item - self._items_schema.append(config) + self._items_schema.append(item.config) self._stats.setdefault(item.name, SkillStats()) if logger.isEnabledFor(logging.DEBUG): diff --git a/src/Undefined/skills/tools/end/config.json b/src/Undefined/skills/tools/end/config.json index 9267056..179c93b 100644 --- a/src/Undefined/skills/tools/end/config.json +++ b/src/Undefined/skills/tools/end/config.json @@ -9,6 +9,10 @@ "summary": { "type": "string", "description": "记录这次做了什么或以后可能要做什么。如果只是end了不做事,不需要填写。如果做了什么或以后可能要做什么,必须填写。这是你短期记忆的来源,十分重要。" + }, + "force": { + "type": "boolean", + "description": "强制结束对话,跳过未发送消息的检查。默认为 false。" } }, "required": [] diff --git a/src/Undefined/skills/tools/end/handler.py b/src/Undefined/skills/tools/end/handler.py index 1b2049d..e00ace4 100644 --- a/src/Undefined/skills/tools/end/handler.py +++ b/src/Undefined/skills/tools/end/handler.py @@ -2,6 +2,7 @@ from typing import Any, Dict import logging +from Undefined.context import RequestContext from Undefined.end_summary_storage import ( EndSummaryLocation, EndSummaryRecord, @@ -12,6 +13,26 @@ logger = logging.getLogger(__name__) +def _parse_force_flag(value: Any) -> bool: + """force 仅接受布尔值,其他类型一律视为 False。""" + return value if isinstance(value, bool) else False + + +def _is_true_flag(value: Any) -> bool: + """上下文标记仅接受布尔 True。""" + return isinstance(value, bool) and value + + +def _was_message_sent_this_turn(context: Dict[str, Any]) -> bool: + if _is_true_flag(context.get("message_sent_this_turn", False)): + return True + + ctx = RequestContext.current() + if ctx is None: + return False + return _is_true_flag(ctx.get_resource("message_sent_this_turn", False)) + + def _build_location(context: Dict[str, Any]) -> EndSummaryLocation | None: request_type = context.get("request_type") if request_type == "group": @@ -39,6 +60,27 @@ def _build_location(context: Dict[str, Any]) -> EndSummaryLocation | None: async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: summary_raw = args.get("summary", "") summary = summary_raw.strip() if isinstance(summary_raw, str) else "" + force_raw = args.get("force", False) + force = _parse_force_flag(force_raw) + if "force" in args and not isinstance(force_raw, bool): + logger.warning( + "[end工具] force 参数类型非法,已按 False 处理: type=%s request_id=%s", + type(force_raw).__name__, + context.get("request_id", "-"), + ) + + # 检查:如果有 summary 但本轮未发送消息,且未强制执行,则拒绝 + if summary and not force: + message_sent = _was_message_sent_this_turn(context) + if not message_sent: + logger.warning( + "[end工具] 拒绝执行:有 summary 但本轮未发送消息,request_id=%s", + context.get("request_id", "-"), + ) + return ( + "拒绝结束对话:你记录了 summary 但本轮未发送任何消息或媒体内容。" + "请先发送消息给用户,或者如果确实不需要发送,请使用 force=true 参数强制结束。" + ) if summary: location = _build_location(context) diff --git a/src/Undefined/skills/toolsets/memory/add/config.json b/src/Undefined/skills/toolsets/memory/add/config.json index 2d40574..9d8ddbd 100644 --- a/src/Undefined/skills/toolsets/memory/add/config.json +++ b/src/Undefined/skills/toolsets/memory/add/config.json @@ -1,7 +1,7 @@ { "type": "function", "function": { - "name": "memory.add", + "name": "add", "description": "保存一条记忆。当你觉得某些信息值得记住(比如用户的偏好、重要的事情、约定等),可以调用此工具保存。就事论事,就人论人,不做会话隔离。上限500条,超出上限会自动移除最旧的记忆。", "parameters": { "type": "object", diff --git a/src/Undefined/skills/toolsets/memory/delete/config.json b/src/Undefined/skills/toolsets/memory/delete/config.json index def0eb7..39957c0 100644 --- a/src/Undefined/skills/toolsets/memory/delete/config.json +++ b/src/Undefined/skills/toolsets/memory/delete/config.json @@ -1,7 +1,7 @@ { "type": "function", "function": { - "name": "memory.delete", + "name": "delete", "description": "根据 UUID 删除一条记忆。", "parameters": { "type": "object", diff --git a/src/Undefined/skills/toolsets/memory/list/config.json b/src/Undefined/skills/toolsets/memory/list/config.json index 4157204..6877a67 100644 --- a/src/Undefined/skills/toolsets/memory/list/config.json +++ b/src/Undefined/skills/toolsets/memory/list/config.json @@ -1,7 +1,7 @@ { "type": "function", "function": { - "name": "memory.list", + "name": "list", "description": "列出所有保存的记忆及其 UUID。当你需要 update 或 delete 时,应先调用此工具查找对应的 UUID。", "parameters": { "type": "object", diff --git a/src/Undefined/skills/toolsets/memory/update/config.json b/src/Undefined/skills/toolsets/memory/update/config.json index 793bbbb..b502972 100644 --- a/src/Undefined/skills/toolsets/memory/update/config.json +++ b/src/Undefined/skills/toolsets/memory/update/config.json @@ -1,7 +1,7 @@ { "type": "function", "function": { - "name": "memory.update", + "name": "update", "description": "更新一条已有的记忆。需要提供该记忆的 UUID。", "parameters": { "type": "object", diff --git a/src/Undefined/skills/toolsets/messages/send_group_sign/config.json b/src/Undefined/skills/toolsets/messages/send_group_sign/config.json index 847a39d..70bb6b9 100644 --- a/src/Undefined/skills/toolsets/messages/send_group_sign/config.json +++ b/src/Undefined/skills/toolsets/messages/send_group_sign/config.json @@ -1,7 +1,7 @@ { "type": "function", "function": { - "name": "messages.send_group_sign", + "name": "send_group_sign", "description": "在当前群聊执行打卡(签到)。仅当在群聊对话中且有打卡功能时可用。", "parameters": { "type": "object", diff --git a/src/Undefined/skills/toolsets/messages/send_message/handler.py b/src/Undefined/skills/toolsets/messages/send_message/handler.py index 025a6ae..3e64e2d 100644 --- a/src/Undefined/skills/toolsets/messages/send_message/handler.py +++ b/src/Undefined/skills/toolsets/messages/send_message/handler.py @@ -182,6 +182,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: else: logger.info("[发送消息] 准备发送私聊 %s: %s", target_id, message[:100]) await sender.send_private_message(target_id, message) + context["message_sent_this_turn"] = True return "消息已发送" except Exception as e: logger.exception( @@ -193,11 +194,12 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: ) return "发送失败:消息服务暂时不可用,请稍后重试" - # 无 sender 时只做兼容回调;仅允许发送到“当前会话”避免误投递 + # 无 sender 时只做兼容回调;仅允许发送到"当前会话"避免误投递 if target_type == "group": if send_message_callback and _is_current_group_target(context, target_id): try: await send_message_callback(message) + context["message_sent_this_turn"] = True return "消息已发送" except Exception as e: logger.exception( @@ -218,6 +220,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: if send_private_message_callback: try: await send_private_message_callback(target_id, message) + context["message_sent_this_turn"] = True return "消息已发送" except Exception as e: logger.exception( @@ -231,6 +234,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: if send_message_callback and _is_current_private_target(context, target_id): try: await send_message_callback(message) + context["message_sent_this_turn"] = True return "消息已发送" except Exception as e: logger.exception( diff --git a/src/Undefined/skills/toolsets/messages/send_private_message/handler.py b/src/Undefined/skills/toolsets/messages/send_private_message/handler.py index ad6c434..e4efe8e 100644 --- a/src/Undefined/skills/toolsets/messages/send_private_message/handler.py +++ b/src/Undefined/skills/toolsets/messages/send_private_message/handler.py @@ -44,6 +44,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: if sender: try: await sender.send_private_message(user_id, message) + context["message_sent_this_turn"] = True return f"私聊消息已发送给用户 {user_id}" except Exception as e: logger.exception( @@ -57,6 +58,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: if send_private_message_callback: try: await send_private_message_callback(user_id, message) + context["message_sent_this_turn"] = True return f"私聊消息已发送给用户 {user_id}" except Exception as e: logger.exception( diff --git a/src/Undefined/skills/toolsets/notices/get/config.json b/src/Undefined/skills/toolsets/notices/get/config.json index 8343074..01bcf35 100644 --- a/src/Undefined/skills/toolsets/notices/get/config.json +++ b/src/Undefined/skills/toolsets/notices/get/config.json @@ -1,7 +1,7 @@ { "type": "function", "function": { - "name": "notices.get", + "name": "get", "description": "根据 notice_id 获取特定群公告的详细内容。", "parameters": { "type": "object", diff --git a/src/Undefined/skills/toolsets/notices/list/config.json b/src/Undefined/skills/toolsets/notices/list/config.json index 4ddabee..8d63229 100644 --- a/src/Undefined/skills/toolsets/notices/list/config.json +++ b/src/Undefined/skills/toolsets/notices/list/config.json @@ -1,7 +1,7 @@ { "type": "function", "function": { - "name": "notices.list", + "name": "list", "description": "综合查询群公告列表。支持按数量、全部获取、关键词搜索及时间范围筛选。", "parameters": { "type": "object", diff --git a/src/Undefined/skills/toolsets/notices/stats/config.json b/src/Undefined/skills/toolsets/notices/stats/config.json index ddd6236..8979764 100644 --- a/src/Undefined/skills/toolsets/notices/stats/config.json +++ b/src/Undefined/skills/toolsets/notices/stats/config.json @@ -1,7 +1,7 @@ { "type": "function", "function": { - "name": "notices.stats", + "name": "stats", "description": "获取群公告的统计信息,如公告总数和最新公告时间。", "parameters": { "type": "object", diff --git a/src/Undefined/utils/scheduler.py b/src/Undefined/utils/scheduler.py index 362b9eb..4fd6e80 100644 --- a/src/Undefined/utils/scheduler.py +++ b/src/Undefined/utils/scheduler.py @@ -426,6 +426,8 @@ async def send_like_cb(uid: int, times: int = 1) -> None: await self.onebot.send_like(uid, times) ai_client = self.ai + memory_storage = self.ai.memory_storage + runtime_config = self.ai.runtime_config sender = self.sender history_manager = self.history_manager onebot_client = self.onebot