tanjingyu 3 недель назад
Родитель
Сommit
d890e6c4be
3 измененных файлов с 86 добавлено и 29 удалено
  1. 4 0
      agent/tools/builtin/__init__.py
  2. 80 27
      agent/tools/builtin/feishu/chat.py
  3. 2 2
      agent/trace/store.py

+ 4 - 0
agent/tools/builtin/__init__.py

@@ -42,4 +42,8 @@ __all__ = [
     "sandbox_run_shell",
     "sandbox_rebuild_with_ports",
     "sandbox_destroy_environment",
+    "feishu_get_chat_history",
+    "feishu_get_contact_replies",
+    "feishu_send_message_to_contact",
+    "feishu_get_contact_list"
 ]

+ 80 - 27
agent/tools/builtin/feishu/chat.py

@@ -2,6 +2,7 @@ import json
 import os
 import base64
 import httpx
+import asyncio
 from typing import Optional, List, Dict, Any, Union
 from .feishu_client import FeishuClient, FeishuDomain
 from agent.tools import tool, ToolResult, ToolContext
@@ -13,7 +14,7 @@ FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET", "nn2dWuXTiRA2N6xodbm4g0qz1AfM
 
 CONTACTS_FILE = os.path.join(os.path.dirname(__file__), "contacts.json")
 CHAT_HISTORY_DIR = os.path.join(os.path.dirname(__file__), "chat_history")
-UNREAD_SUMMARY_FILE = os.path.join(CHAT_HISTORY_DIR, "unread_summary.json")
+UNREAD_SUMMARY_FILE = os.path.join(CHAT_HISTORY_DIR, "chat_summary.json")
 
 # ==================== 一、文件内使用的功能函数 ====================
 
@@ -85,7 +86,7 @@ def _ensure_chat_history_dir():
 
 def get_chat_file_path(contact_name: str) -> str:
     _ensure_chat_history_dir()
-    return os.path.join(CHAT_HISTORY_DIR, f"{contact_name}.json")
+    return os.path.join(CHAT_HISTORY_DIR, f"chat_{contact_name}.json")
 
 def load_chat_history(contact_name: str) -> List[Dict[str, Any]]:
     path = get_chat_file_path(contact_name)
@@ -143,7 +144,10 @@ def update_unread_count(contact_name: str, increment: int = 1, reset: bool = Fal
 )
 async def feishu_get_contact_list(context: Optional[ToolContext] = None) -> ToolResult:
     """
-    获取所有联系人的名称和描述。不需要参数。
+    获取所有联系人的名称和描述。
+
+    Args:
+        context: 工具执行上下文(可选)
     """
     contacts = list_contacts_info()
     return ToolResult(
@@ -158,27 +162,29 @@ async def feishu_get_contact_list(context: Optional[ToolContext] = None) -> Tool
             "name": "给飞书联系人发送消息",
             "params": {
                 "contact_name": "联系人名称",
-                "content": "消息内容。可以是字符串,也可以是 OpenAI 多模态格式列表 (例如: [{'type': 'text', 'text': '你好'}, {'type': 'image_url', 'image_url': {'url': '...'}}])"
+                "content": "消息内容。OpenAI 多模态格式列表 (例如: [{'type': 'text', 'text': '你好'}, {'type': 'image_url', 'image_url': {'url': '...'}}])"
             }
         },
         "en": {
             "name": "Send Message to Feishu Contact",
             "params": {
                 "contact_name": "Contact Name",
-                "content": "Message content. Can be a string or an OpenAI multimodal list format."
+                "content": "Message content. OpenAI multimodal list format."
             }
         }
     }
 )
 async def feishu_send_message_to_contact(
     contact_name: str,
-    content: Union[str, List[Dict[str, Any]]],
+    content: Any,
     context: Optional[ToolContext] = None
 ) -> ToolResult:
     """
-    给指定的联系人发送消息。支持发送文本和图片。
-    如果内容是 OpenAI 多模态格式,会自动转换为飞书相应的格式并发起多次发送。
-    发送成功后会更新 contacts.json 中的 chat_id。
+    给指定的联系人发送消息。支持发送文本和图片,OpenAI 多模态格式,会自动转换为飞书相应的格式并发起多次发送。
+
+    Args:
+        contact_name: 飞书联系人的名称
+        content: 消息内容。OpenAI 多模态列表格式。
     """
     contact = get_contact_full_info(contact_name)
     if not contact:
@@ -191,6 +197,15 @@ async def feishu_send_message_to_contact(
     if not receive_id:
         return ToolResult(title="发送失败", output="联系人 ID 信息缺失", error="Receiver ID not found in contacts.json")
 
+    # 如果 content 是字符串,尝试解析为 JSON
+    if isinstance(content, str):
+        try:
+            parsed = json.loads(content)
+            if isinstance(parsed, (list, dict)):
+                content = parsed
+        except (json.JSONDecodeError, TypeError):
+            pass
+
     try:
         last_res = None
         if isinstance(content, str):
@@ -223,6 +238,16 @@ async def feishu_send_message_to_contact(
                                 last_res = client.send_image(to=receive_id, image=img_resp.content)
                         except Exception as e:
                             print(f"下载图片失败: {e}")
+        elif isinstance(content, dict):
+            # 如果是单块格式也支持一下
+            item_type = content.get("type")
+            if item_type == "text":
+                last_res = client.send_message(to=receive_id, text=content.get("text", ""))
+            elif item_type == "image_url":
+                # ... 逻辑与上面类似,为了简洁这里也可以统一转成 list 处理
+                content = [content]
+                # 此处递归或重写逻辑,这里选择简单地重新判断
+                return await feishu_send_message_to_contact(contact_name, content, context)
         else:
             return ToolResult(title="发送失败", output="不支持的内容格式", error="Invalid content format")
 
@@ -258,25 +283,33 @@ async def feishu_send_message_to_contact(
         "zh": {
             "name": "获取飞书联系人回复",
             "params": {
-                "contact_name": "联系人名称"
+                "contact_name": "联系人名称",
+                "wait_time_seconds": "可选,如果当前没有新回复,则最多等待指定的秒数。在等待期间会每秒检查一次,一旦有新回复则立即返回。超过时长仍无回复则返回空。"
             }
         },
         "en": {
             "name": "Get Feishu Contact Replies",
             "params": {
-                "contact_name": "Contact Name"
+                "contact_name": "Contact Name",
+                "wait_time_seconds": "Optional. If there are no new replies, wait up to the specified number of seconds. It will check every second and return immediately if a new reply is detected. If no reply is received after the duration, it returns empty."
             }
         }
     }
 )
 async def feishu_get_contact_replies(
     contact_name: str,
+    wait_time_seconds: Optional[int] = None,
     context: Optional[ToolContext] = None
 ) -> ToolResult:
     """
     获取指定联系人的最新回复消息。
     返回的数据格式为 OpenAI 多模态消息内容列表。
     只抓取自上一个机器人消息之后的用户回复。
+
+    Args:
+        contact_name: 飞书联系人的名称
+        wait_time_seconds: 可选的最大轮询等待时间。如果暂时没有新回复,将每秒检查一次直到有回复或超时。
+        context: 工具执行上下文(可选)
     """
     contact = get_contact_full_info(contact_name)
     if not contact:
@@ -289,22 +322,34 @@ async def feishu_get_contact_replies(
     client = FeishuClient(app_id=FEISHU_APP_ID, app_secret=FEISHU_APP_SECRET)
     
     try:
-        msg_list_res = client.get_message_list(chat_id=chat_id)
-        if not msg_list_res or "items" not in msg_list_res:
-            return ToolResult(title="获取失败", output="无法获取消息列表或结果为空")
-
-        openai_blocks = []
-        # 遍历消息列表 (最新的在前)
-        for msg in msg_list_res["items"]:
-            if msg.get("sender_type") == "app":
-                # 碰到机器人的消息即停止
-                break
-            
-            content_blocks = _convert_feishu_msg_to_openai_content(client, msg)
-            openai_blocks.extend(content_blocks)
-
-        # 反转列表以保持时间正序 (旧 -> 新)
-        openai_blocks.reverse()
+        def get_replies():
+            msg_list_res = client.get_message_list(chat_id=chat_id)
+            if not msg_list_res or "items" not in msg_list_res:
+                return []
+
+            openai_blocks = []
+            # 遍历消息列表 (最新的在前)
+            for msg in msg_list_res["items"]:
+                if msg.get("sender_type") == "app":
+                    # 碰到机器人的消息即停止
+                    break
+                
+                content_blocks = _convert_feishu_msg_to_openai_content(client, msg)
+                openai_blocks.extend(content_blocks)
+
+            # 反转列表以保持时间正序 (旧 -> 新)
+            openai_blocks.reverse()
+            return openai_blocks
+
+        openai_blocks = get_replies()
+        
+        # 如果初始没有获取到回复,且设置了等待时间,则开始轮询
+        if not openai_blocks and wait_time_seconds and wait_time_seconds > 0:
+            for _ in range(int(wait_time_seconds)):
+                await asyncio.sleep(1)
+                openai_blocks = get_replies()
+                if openai_blocks:
+                    break
 
         return ToolResult(
             title=f"获取 {contact_name} 回复成功",
@@ -384,6 +429,14 @@ async def feishu_get_chat_history(
     根据联系人名称获取完整的历史聊天记录。
     支持通过时间戳进行范围筛选,并支持分页获取。
     返回的消息按时间倒序排列(最新的在前面)。
+
+    Args:
+        contact_name: 飞书联系人的名称
+        start_time: 筛选起始时间的时间戳(秒),可选
+        end_time: 筛选结束时间的时间戳(秒),可选
+        page_size: 每页消息数量,默认为 20
+        page_token: 分页令牌,用于加载上一页/下一页,可选
+        context: 工具执行上下文(可选)
     """
     contact = get_contact_full_info(contact_name)
     if not contact:

+ 2 - 2
agent/trace/store.py

@@ -308,7 +308,7 @@ class FileSystemTraceStore:
         # 1. 写入 message 文件
         messages_dir = self._get_messages_dir(trace_id)
         message_file = messages_dir / f"{message.message_id}.json"
-        message_file.write_text(json.dumps(message.to_dict(), indent=2, ensure_ascii=False))
+        message_file.write_text(json.dumps(message.to_dict(), indent=2, ensure_ascii=False), encoding="utf-8")
 
         # 2. 更新 trace 统计
         trace = await self.get_trace(trace_id)
@@ -555,7 +555,7 @@ class FileSystemTraceStore:
 
         # 追加到 events.jsonl
         events_file = self._get_events_file(trace_id)
-        with events_file.open('a') as f:
+        with events_file.open('a', encoding='utf-8') as f:
             f.write(json.dumps(event, ensure_ascii=False) + '\n')
 
         return event_id