Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/Undefined/onebot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 客户端"""

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/Undefined/services/ai_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"file_path": {
"type": "string",
"description": "文件路径(相对于当前工作目录)"
},
"max_chars": {
"type": "integer",
"description": "可选:最大字符数限制。如果文件内容超过此限制,将被截断。不指定则返回完整内容。建议:小文件不指定,大文件可设置为 50000-100000"
}
},
"required": ["file_path"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 "错误:文件路径不能为空"
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/Undefined/skills/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions src/Undefined/skills/tools/end/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"summary": {
"type": "string",
"description": "记录这次做了什么或以后可能要做什么。如果只是end了不做事,不需要填写。如果做了什么或以后可能要做什么,必须填写。这是你短期记忆的来源,十分重要。"
},
"force": {
"type": "boolean",
"description": "强制结束对话,跳过未发送消息的检查。默认为 false。"
}
},
"required": []
Expand Down
42 changes: 42 additions & 0 deletions src/Undefined/skills/tools/end/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any, Dict
import logging

from Undefined.context import RequestContext
from Undefined.end_summary_storage import (
EndSummaryLocation,
EndSummaryRecord,
Expand All @@ -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":
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/Undefined/skills/toolsets/memory/add/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "function",
"function": {
"name": "memory.add",
"name": "add",
"description": "保存一条记忆。当你觉得某些信息值得记住(比如用户的偏好、重要的事情、约定等),可以调用此工具保存。就事论事,就人论人,不做会话隔离。上限500条,超出上限会自动移除最旧的记忆。",
"parameters": {
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion src/Undefined/skills/toolsets/memory/delete/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "function",
"function": {
"name": "memory.delete",
"name": "delete",
"description": "根据 UUID 删除一条记忆。",
"parameters": {
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion src/Undefined/skills/toolsets/memory/list/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "function",
"function": {
"name": "memory.list",
"name": "list",
"description": "列出所有保存的记忆及其 UUID。当你需要 update 或 delete 时,应先调用此工具查找对应的 UUID。",
"parameters": {
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion src/Undefined/skills/toolsets/memory/update/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "function",
"function": {
"name": "memory.update",
"name": "update",
"description": "更新一条已有的记忆。需要提供该记忆的 UUID。",
"parameters": {
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "function",
"function": {
"name": "messages.send_group_sign",
"name": "send_group_sign",
"description": "在当前群聊执行打卡(签到)。仅当在群聊对话中且有打卡功能时可用。",
"parameters": {
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/Undefined/skills/toolsets/notices/get/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "function",
"function": {
"name": "notices.get",
"name": "get",
"description": "根据 notice_id 获取特定群公告的详细内容。",
"parameters": {
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion src/Undefined/skills/toolsets/notices/list/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "function",
"function": {
"name": "notices.list",
"name": "list",
"description": "综合查询群公告列表。支持按数量、全部获取、关键词搜索及时间范围筛选。",
"parameters": {
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion src/Undefined/skills/toolsets/notices/stats/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "function",
"function": {
"name": "notices.stats",
"name": "stats",
"description": "获取群公告的统计信息,如公告总数和最新公告时间。",
"parameters": {
"type": "object",
Expand Down
2 changes: 2 additions & 0 deletions src/Undefined/utils/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down