tanjingyu 3 недель назад
Родитель
Сommit
a46e91c3b9

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

@@ -20,6 +20,8 @@ from agent.tools.builtin.skill import skill, list_skills
 from agent.tools.builtin.search import search_posts, get_search_suggestions
 from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run_shell,
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
+from agent.tools.builtin.feishu.chat import (feishu_get_chat_history, feishu_get_contact_replies,
+                                         feishu_send_message_to_contact,feishu_get_contact_list)
 
 # 导入浏览器工具以触发注册
 import agent.tools.builtin.browser  # noqa: F401

+ 82 - 8
agent/tools/builtin/feishu/chat.py

@@ -12,10 +12,12 @@ FEISHU_APP_ID = os.getenv("FEISHU_APP_ID", "cli_a90fe317987a9cc9")
 FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET", "nn2dWuXTiRA2N6xodbm4g0qz1AfM2ayi")
 
 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")
 
 # ==================== 一、文件内使用的功能函数 ====================
 
-def _load_contacts() -> List[Dict[str, Any]]:
+def load_contacts() -> List[Dict[str, Any]]:
     """读取 contacts.json 中的所有联系人"""
     if not os.path.exists(CONTACTS_FILE):
         return []
@@ -25,7 +27,7 @@ def _load_contacts() -> List[Dict[str, Any]]:
     except Exception:
         return []
 
-def _save_contacts(contacts: List[Dict[str, Any]]):
+def save_contacts(contacts: List[Dict[str, Any]]):
     """保存联系人信息到 contacts.json"""
     try:
         with open(CONTACTS_FILE, 'w', encoding='utf-8') as f:
@@ -38,7 +40,7 @@ def list_contacts_info() -> List[Dict[str, str]]:
     1. 列出所有联系人信息
     读取 contacts.json 中的每一个联系人的 name、description,以字典列表返回
     """
-    contacts = _load_contacts()
+    contacts = load_contacts()
     return [{"name": c.get("name", ""), "description": c.get("description", "")} for c in contacts]
 
 def get_contact_full_info(name: str) -> Optional[Dict[str, Any]]:
@@ -46,18 +48,25 @@ def get_contact_full_info(name: str) -> Optional[Dict[str, Any]]:
     2. 根据联系人名称获取联系人完整字典信息
     从 contacts.json 中读取每一个联系人做名称匹配,返回数据中的所有字段为一个字典对象
     """
-    contacts = _load_contacts()
+    contacts = load_contacts()
     for c in contacts:
         if c.get("name") == name:
             return c
     return None
 
+def get_contact_by_id(id_value: str) -> Optional[Dict[str, Any]]:
+    """根据 chat_id 或 open_id 获取联系人信息"""
+    contacts = load_contacts()
+    for c in contacts:
+        if c.get("chat_id") == id_value or c.get("open_id") == id_value:
+            return c
+    return None
+
 def update_contact_chat_id(name: str, chat_id: str):
     """
     3. 更新某一个联系人的 chat_id
-    根据第二个函数找出联系人信息,如果信息中的 chat_id 为空,那么就将传进来的 chat_id 更新进去
     """
-    contacts = _load_contacts()
+    contacts = load_contacts()
     updated = False
     for c in contacts:
         if c.get("name") == name:
@@ -66,9 +75,59 @@ def update_contact_chat_id(name: str, chat_id: str):
                 updated = True
             break
     if updated:
-        _save_contacts(contacts)
+        save_contacts(contacts)
+
+# ==================== 二、聊天记录文件管理 ====================
+
+def _ensure_chat_history_dir():
+    if not os.path.exists(CHAT_HISTORY_DIR):
+        os.makedirs(CHAT_HISTORY_DIR)
 
-# ==================== 二、@tool 工具 ====================
+def get_chat_file_path(contact_name: str) -> str:
+    _ensure_chat_history_dir()
+    return os.path.join(CHAT_HISTORY_DIR, f"{contact_name}.json")
+
+def load_chat_history(contact_name: str) -> List[Dict[str, Any]]:
+    path = get_chat_file_path(contact_name)
+    if os.path.exists(path):
+        try:
+            with open(path, 'r', encoding='utf-8') as f:
+                return json.load(f)
+        except Exception:
+            return []
+    return []
+
+def save_chat_history(contact_name: str, history: List[Dict[str, Any]]):
+    path = get_chat_file_path(contact_name)
+    try:
+        with open(path, 'w', encoding='utf-8') as f:
+            json.dump(history, f, ensure_ascii=False, indent=2)
+    except Exception as e:
+        print(f"保存聊天记录失败: {e}")
+
+def update_unread_count(contact_name: str, increment: int = 1, reset: bool = False):
+    """更新未读消息摘要"""
+    _ensure_chat_history_dir()
+    summary = {}
+    if os.path.exists(UNREAD_SUMMARY_FILE):
+        try:
+            with open(UNREAD_SUMMARY_FILE, 'r', encoding='utf-8') as f:
+                summary = json.load(f)
+        except Exception:
+            summary = {}
+    
+    if reset:
+        summary[contact_name] = 0
+    else:
+        summary[contact_name] = summary.get(contact_name, 0) + increment
+    
+    try:
+        with open(UNREAD_SUMMARY_FILE, 'w', encoding='utf-8') as f:
+            json.dump(summary, f, ensure_ascii=False, indent=2)
+    except Exception as e:
+        print(f"更新未读摘要失败: {e}")
+
+# ==================== 三、@tool 工具 ====================
 
 @tool(
     display={
@@ -170,6 +229,21 @@ async def feishu_send_message_to_contact(
         if last_res:
             # 更新 chat_id
             update_contact_chat_id(contact_name, last_res.chat_id)
+
+            # [待开启] 发送即记录:为了维护完整的聊天记录,将机器人发出的消息也保存到本地文件
+            # try:
+            #     history = load_chat_history(contact_name)
+            #     history.append({
+            #         "role": "assistant",
+            #         "message_id": last_res.message_id,
+            #         "content": content if isinstance(content, list) else [{"type": "text", "text": content}]
+            #     })
+            #     save_chat_history(contact_name, history)
+            #     # 机器人回复了,将该联系人的未读计数重置为 0
+            #     update_unread_count(contact_name, reset=True)
+            # except Exception as e:
+            #     print(f"记录发送的消息失败: {e}")
+
             return ToolResult(
                 title=f"消息已成功发送至 {contact_name}",
                 output=f"发送成功。消息 ID: {last_res.message_id}",

+ 15 - 15
agent/tools/builtin/feishu/chat_test.py

@@ -29,15 +29,15 @@ async def feishu_tools():
     contact_name = "谭景玉"
     # print(f"--- 测试: feishu_send_message_to_contact (对象: {contact_name}) ---")
     #
-    # # 测试发送纯文本
-    # text_content = "你好,这是一条来自开发环境的自动化测试消息 (纯文本)。"
-    # print(f"正在发送文本: {text_content}")
-    # result_send_text = await feishu_send_message_to_contact(contact_name, text_content)
-    # print(f"标题: {result_send_text.title}")
-    # print(f"输出: {result_send_text.output}")
-    # if result_send_text.error:
-    #     print(f"错误: {result_send_text.error}")
-    #
+    # 测试发送纯文本
+    text_content = "干活"
+    print(f"正在发送文本: {text_content}")
+    result_send_text = await feishu_send_message_to_contact(contact_name, text_content)
+    print(f"标题: {result_send_text.title}")
+    print(f"输出: {result_send_text.output}")
+    if result_send_text.error:
+        print(f"错误: {result_send_text.error}")
+
     # 测试发送多模态消息 (文本 + 图片)
     # 注意:这里的图片 URL 需要是一个可访问的地址,或者你可以使用 base64 格式
     # multimodal_content = [
@@ -59,12 +59,12 @@ async def feishu_tools():
     # print(f"消息详情: {result_replies.output}")
     # print("-" * 30 + "\n")
 
-    # 4. 测试获取历史记录
-    print(f"--- 测试: feishu_get_chat_history (对象: {contact_name}) ---")
-    result_history = await feishu_get_chat_history(contact_name, page_size=5, page_token="4cXSlmN7uFAnWWU5yfIGMNvUNrBPLlXZREzLcnvUtOcmK2QFKfwEqfbui_UDsR-y8ne0BkzXABiYTAQASh-n7my_3zQp6o3ERRz0bZ4LB5zMvahf8x7OQoso1rjrMaKM")
-    print(f"标题: {result_history.title}")
-    print(f"历史记录输出: {result_history.output}")
-    print("-" * 30 + "\n")
+    # # 4. 测试获取历史记录
+    # print(f"--- 测试: feishu_get_chat_history (对象: {contact_name}) ---")
+    # result_history = await feishu_get_chat_history(contact_name, page_size=5, page_token="4cXSlmN7uFAnWWU5yfIGMNvUNrBPLlXZREzLcnvUtOcmK2QFKfwEqfbui_UDsR-y8ne0BkzXABiYTAQASh-n7my_3zQp6o3ERRz0bZ4LB5zMvahf8x7OQoso1rjrMaKM")
+    # print(f"标题: {result_history.title}")
+    # print(f"历史记录输出: {result_history.output}")
+    # print("-" * 30 + "\n")
 
 if __name__ == "__main__":
     # 模拟环境变量 (如果在系统环境变量中已设置,此处可省略)

+ 92 - 0
agent/tools/builtin/feishu/websocket_event.py

@@ -0,0 +1,92 @@
+import os
+import json
+import logging
+import asyncio
+import sys
+from typing import Optional
+
+# 将项目根目录添加到 python 路径,确保可以作为独立脚本运行
+PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
+if PROJECT_ROOT not in sys.path:
+    sys.path.append(PROJECT_ROOT)
+
+from agent.tools.builtin.feishu.feishu_client import FeishuClient, FeishuMessageEvent, FeishuDomain
+from agent.tools.builtin.feishu.chat import (
+    FEISHU_APP_ID, 
+    FEISHU_APP_SECRET, 
+    get_contact_by_id, 
+    load_chat_history, 
+    save_chat_history, 
+    update_unread_count,
+    _convert_feishu_msg_to_openai_content
+)
+
+# 配置日志
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+logger = logging.getLogger("FeishuWebsocket")
+
+class FeishuMessageListener:
+    def __init__(self):
+        self.client = FeishuClient(
+            app_id=FEISHU_APP_ID,
+            app_secret=FEISHU_APP_SECRET,
+            domain=FeishuDomain.FEISHU
+        )
+
+    def handle_incoming_message(self, event: FeishuMessageEvent):
+        """处理收到的飞书消息事件"""
+        # 1. 识别联系人
+        # 优先使用 sender_open_id 匹配联系人,如果没有则尝试 chat_id
+        contact = get_contact_by_id(event.sender_open_id) or get_contact_by_id(event.chat_id)
+        
+        if not contact:
+            logger.warning(f"收到未知发送者的消息: open_id={event.sender_open_id}, chat_id={event.chat_id}")
+            # 对于未知联系人,我们可以选择忽略,或者记录到 'unknown' 分类
+            contact = {"name": "未知联系人", "open_id": event.sender_open_id}
+
+        contact_name = contact.get("name")
+        logger.info(f"收到来自 [{contact_name}] 的消息: {event.content[:50]}...")
+
+        # 2. 转换为 OpenAI 多模态格式
+        # 构造一个类似 get_message_list 返回的字典对象,以便重用转换逻辑
+        msg_dict = {
+            "message_id": event.message_id,
+            "content_type": event.content_type,
+            "content": event.content, # 对于 text, websocket 传来的已经是解析后的字符串;对于 image 则是原始 JSON 字符串
+            "sender_id": event.sender_open_id,
+            "sender_type": "user" # WebSocket 收到的一般是用户消息,除非是机器人自己的回显(通常会过滤)
+        }
+        
+        openai_content = _convert_feishu_msg_to_openai_content(self.client, msg_dict)
+
+        # 3. 维护聊天记录
+        history = load_chat_history(contact_name)
+        new_message = {
+            "role": "user",
+            "message_id": event.message_id,
+            "timestamp": os.path.getmtime(os.path.join(os.path.dirname(__file__), "chat.py")), # 简单模拟一个时间戳,实际应使用事件时间
+            "content": openai_content
+        }
+        history.append(new_message)
+        save_chat_history(contact_name, history)
+
+        # 4. 更新未读计数
+        update_unread_count(contact_name, increment=1)
+        logger.info(f"已更新 [{contact_name}] 的聊天记录并增加未读计数")
+
+    def start(self):
+        """启动监听"""
+        logger.info("正在启动飞书消息实时监听...")
+        try:
+            self.client.start_websocket(
+                on_message=self.handle_incoming_message,
+                blocking=True
+            )
+        except KeyboardInterrupt:
+            logger.info("监听已停止")
+        except Exception as e:
+            logger.error(f"监听过程中出现错误: {e}")
+
+if __name__ == "__main__":
+    listener = FeishuMessageListener()
+    listener.start()