Browse Source

治理(飞书): 清理脚本式测试与废弃事件入口

删除误放在生产包中的 chat_test 和未使用 websocket_event,整理飞书客户端异常处理与旧凭据兼容逻辑,保留现有飞书工具和 Agent 调用入口。
SamLee 13 giờ trước cách đây
mục cha
commit
a88a316dc9

+ 50 - 26
agent/agent/tools/builtin/feishu/chat.py

@@ -3,21 +3,33 @@ import os
 import base64
 import httpx
 import asyncio
+import logging
 from typing import Optional, List, Dict, Any
 from .feishu_client import FeishuClient
 from agent.tools import tool, ToolResult, ToolContext
 from agent.trace.models import MessageContent
 
+
+logger = logging.getLogger(__name__)
+
 # 凭据只能由运行环境注入;缺失时 FeishuClient 会 fail-closed。
 FEISHU_APP_ID = os.getenv("FEISHU_APP_ID", "")
 FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET", "")
 
-CONTACTS_FILE = os.path.join(
-    os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")),
-    "config",
-    "feishu_contacts.json",
+CONTACTS_FILE = os.getenv(
+    "FEISHU_CONTACTS_FILE",
+    os.path.join(
+        os.path.abspath(
+            os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
+        ),
+        "config",
+        "feishu_contacts.json",
+    ),
+)
+CHAT_HISTORY_DIR = os.getenv(
+    "FEISHU_CHAT_HISTORY_DIR",
+    os.path.join(os.path.dirname(__file__), "chat_history"),
 )
-CHAT_HISTORY_DIR = os.path.join(os.path.dirname(__file__), "chat_history")
 UNREAD_SUMMARY_FILE = os.path.join(CHAT_HISTORY_DIR, "chat_summary.json")
 
 # ==================== 一、文件内使用的功能函数 ====================
@@ -30,7 +42,8 @@ def load_contacts() -> List[Dict[str, Any]]:
     try:
         with open(CONTACTS_FILE, "r", encoding="utf-8") as f:
             return json.load(f)
-    except Exception:
+    except (OSError, json.JSONDecodeError, TypeError) as exc:
+        logger.warning("Failed to load Feishu contacts from %s: %s", CONTACTS_FILE, exc)
         return []
 
 
@@ -39,8 +52,8 @@ def save_contacts(contacts: List[Dict[str, Any]]):
     try:
         with open(CONTACTS_FILE, "w", encoding="utf-8") as f:
             json.dump(contacts, f, ensure_ascii=False, indent=2)
-    except Exception as e:
-        print(f"保存联系人失败: {e}")
+    except OSError as exc:
+        logger.warning("Failed to save Feishu contacts to %s: %s", CONTACTS_FILE, exc)
 
 
 def list_contacts_info() -> List[Dict[str, str]]:
@@ -102,7 +115,8 @@ 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"chat_{contact_name}.json")
+    safe_name = contact_name.replace("/", "_").replace("\\", "_")
+    return os.path.join(CHAT_HISTORY_DIR, f"chat_{safe_name}.json")
 
 
 def load_chat_history(contact_name: str) -> List[Dict[str, Any]]:
@@ -111,7 +125,8 @@ def load_chat_history(contact_name: str) -> List[Dict[str, Any]]:
         try:
             with open(path, "r", encoding="utf-8") as f:
                 return json.load(f)
-        except Exception:
+        except (OSError, json.JSONDecodeError, TypeError) as exc:
+            logger.warning("Failed to load Feishu chat history %s: %s", path, exc)
             return []
     return []
 
@@ -121,8 +136,8 @@ def save_chat_history(contact_name: str, history: List[Dict[str, Any]]):
     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}")
+    except OSError as exc:
+        logger.warning("Failed to save Feishu chat history %s: %s", path, exc)
 
 
 def update_unread_count(contact_name: str, increment: int = 1, reset: bool = False):
@@ -133,7 +148,12 @@ def update_unread_count(contact_name: str, increment: int = 1, reset: bool = Fal
         try:
             with open(UNREAD_SUMMARY_FILE, "r", encoding="utf-8") as f:
                 summary = json.load(f)
-        except Exception:
+        except (OSError, json.JSONDecodeError, TypeError) as exc:
+            logger.warning(
+                "Failed to load Feishu unread summary %s: %s",
+                UNREAD_SUMMARY_FILE,
+                exc,
+            )
             summary = {}
 
     if reset:
@@ -144,8 +164,12 @@ def update_unread_count(contact_name: str, increment: int = 1, reset: bool = Fal
     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}")
+    except OSError as exc:
+        logger.warning(
+            "Failed to update Feishu unread summary %s: %s",
+            UNREAD_SUMMARY_FILE,
+            exc,
+        )
 
 
 # ==================== 三、@tool 工具 ====================
@@ -259,8 +283,8 @@ async def feishu_send_message_to_contact(
                             last_res = client.send_image(
                                 to=receive_id, image=image_bytes
                             )
-                        except Exception as e:
-                            print(f"解析 base64 图片失败: {e}")
+                        except Exception as exc:
+                            logger.warning("Failed to decode Feishu base64 image: %s", exc)
                     elif url.startswith("http://") or url.startswith("https://"):
                         # 处理网络 URL
                         try:
@@ -270,8 +294,8 @@ 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}")
+                        except Exception as exc:
+                            logger.warning("Failed to download Feishu image: %s", exc)
                     else:
                         # 处理本地文件路径
                         try:
@@ -281,9 +305,9 @@ async def feishu_send_message_to_contact(
                                     to=receive_id, image=local_path
                                 )
                             else:
-                                print(f"本地图片文件不存在: {local_path}")
-                        except Exception as e:
-                            print(f"读取本地图片失败: {e}")
+                                logger.warning("Local Feishu image does not exist: %s", local_path)
+                        except Exception as exc:
+                            logger.warning("Failed to read local Feishu image: %s", exc)
         elif isinstance(content, dict):
             # 如果是单块格式也支持一下
             item_type = content.get("type")
@@ -324,8 +348,8 @@ async def feishu_send_message_to_contact(
                 save_chat_history(contact_name, history)
                 # 机器人回复了,将该联系人的未读计数重置为 0
                 update_unread_count(contact_name, reset=True)
-            except Exception as e:
-                print(f"记录发送的消息失败: {e}")
+            except Exception as exc:
+                logger.warning("Failed to record sent Feishu message: %s", exc)
 
             return ToolResult(
                 title=f"消息已成功发送至 {contact_name}",
@@ -461,8 +485,8 @@ def _convert_feishu_msg_to_openai_content(
                         "image_url": {"url": f"data:image/png;base64,{b64_str}"},
                     }
                 )
-        except Exception as e:
-            print(f"转换图片消息失败: {e}")
+        except Exception as exc:
+            logger.warning("Failed to convert Feishu image message: %s", exc)
             blocks.append({"type": "text", "text": "[图片内容获取失败]"})
     elif msg_type == "post":
         blocks.append({"type": "text", "text": raw_content})

+ 0 - 79
agent/agent/tools/builtin/feishu/chat_test.py

@@ -1,79 +0,0 @@
-import asyncio
-import os
-
-from agent.tools.builtin.feishu.chat import feishu_send_message_to_contact
-
-
-async def feishu_tools():
-    print("开始测试飞书工具...\n")
-
-    # # 1. 测试获取联系人列表
-    # print("--- 测试: feishu_get_contact_list ---")
-    # result_list = await feishu_get_contact_list()
-    # print(f"标题: {result_list.title}")
-    # print(f"输出: {result_list.output}")
-    # print("-" * 30 + "\n")
-    #
-    # # 2. 测试发送消息 (以 '谭景玉' 为例,请确保 contacts.json 中有此人且信息正确)
-    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}")
-
-    # 测试发送多模态消息 (文本 + 图片)
-    # 注意:这里的图片 URL 需要是一个可访问的地址,或者你可以使用 base64 格式
-    # multimodal_content = [
-    #     {"type": "text", "text": "这是一条多模态测试消息:"},
-    #     {"type": "image_url", "image_url": {"url": "https://www.baidu.com/img/flexible/logo/pc/result.png"}}
-    # ]
-    # print(f"\n正在发送多模态消息...")
-    # result_send_multi = await feishu_send_message_to_contact(contact_name, multimodal_content)
-    # print(f"标题: {result_send_multi.title}")
-    # # print(f"输出: {result_send_multi.output}")
-    # if result_send_multi.error:
-    #     print(f"错误: {result_send_multi.error}")
-    # print("-" * 30 + "\n")
-
-    # # 3. 测试获取回复
-    # print(f"--- 测试: feishu_get_contact_replies (对象: {contact_name}) ---")
-    # result_replies = await feishu_get_contact_replies(contact_name)
-    # print(f"标题: {result_replies.title}")
-    # 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=os.environ.get("FEISHU_TEST_PAGE_TOKEN"),
-    # )
-    # print(f"标题: {result_history.title}")
-    # print(f"历史记录输出: {result_history.output}")
-    # print("-" * 30 + "\n")
-
-
-if __name__ == "__main__":
-    missing = [
-        name
-        for name in ("FEISHU_APP_ID", "FEISHU_APP_SECRET")
-        if not os.environ.get(name)
-    ]
-    if missing:
-        raise RuntimeError(
-            f"missing required environment variables: {', '.join(missing)}"
-        )
-
-    try:
-        asyncio.run(feishu_tools())
-    except KeyboardInterrupt:
-        pass
-    except Exception as e:
-        print(f"测试过程中出现异常: {e}")

+ 24 - 14
agent/agent/tools/builtin/feishu/feishu_agent.py

@@ -27,11 +27,12 @@ import zipfile
 from typing import Dict, List, Any, Optional
 from collections import defaultdict
 
-PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
-if PROJECT_ROOT not in sys.path:
-    sys.path.insert(0, PROJECT_ROOT)
-
-from agent.tools.builtin.feishu.feishu_client import FeishuClient, FeishuMessageEvent, FeishuDomain, ReceiveIdType
+from agent.tools.builtin.feishu.feishu_client import (
+    FeishuClient,
+    FeishuMessageEvent,
+    FeishuDomain,
+    ReceiveIdType,
+)
 from agent.tools.builtin.feishu.chat import (
     FEISHU_APP_ID,
     FEISHU_APP_SECRET,
@@ -41,15 +42,22 @@ from agent.tools.builtin.feishu.chat import (
 )
 from agent.llm.qwen import qwen_llm_call
 
-logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(name)s] %(levelname)s %(message)s')
 logger = logging.getLogger("FeishuAgent")
 
 # ===== 配置 =====
 
+PROJECT_ROOT = os.getenv(
+    "FEISHU_AGENT_WORKSPACE",
+    os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")),
+)
 MODEL = os.getenv("FEISHU_AGENT_MODEL", "qwen3.5-397b-a17b")
 MAX_HISTORY = 50
 MAX_TOOL_ROUNDS = 10  # 工具调用最大循环次数
-ALLOWED_CONTACTS = {"关涛"}
+ALLOWED_CONTACTS = {
+    item.strip()
+    for item in os.getenv("FEISHU_AGENT_ALLOWED_CONTACTS", "关涛").split(",")
+    if item.strip()
+}
 
 DEFAULT_SYSTEM_PROMPT = """你是一个友好、有帮助的 AI 助手,正在通过飞书和用户对话。
 你可以使用工具来浏览本地目录、读取文件内容、执行 bash 命令。
@@ -337,7 +345,7 @@ def _tool_run_bash(command: str) -> str:
 def _tool_send_image(path: str) -> str:
     """发送图片到飞书"""
     from agent.tools.builtin.feishu.chat import FEISHU_APP_ID, FEISHU_APP_SECRET
-    from agent.tools.builtin.feishu.feishu_client import FeishuClient, FeishuDomain, ReceiveIdType
+    from agent.tools.builtin.feishu.feishu_client import FeishuClient, FeishuDomain
 
     if not _current_chat_id:
         return "错误:无法获取当前会话 ID"
@@ -422,7 +430,7 @@ def _output_reader(proc: subprocess.Popen, pid: int):
             if len(_background_processes[pid]["output_lines"]) > 500:
                 _background_processes[pid]["output_lines"] = _background_processes[pid]["output_lines"][-500:]
     except Exception:
-        pass
+        logger.debug("Feishu background stdout reader stopped", exc_info=True)
 
     # stderr 也读
     try:
@@ -433,7 +441,7 @@ def _output_reader(proc: subprocess.Popen, pid: int):
             if len(_background_processes[pid]["output_lines"]) > 500:
                 _background_processes[pid]["output_lines"] = _background_processes[pid]["output_lines"][-500:]
     except Exception:
-        pass
+        logger.debug("Feishu background stderr reader stopped", exc_info=True)
 
 
 def _tool_run_background(command: str, name: str = "") -> str:
@@ -446,7 +454,6 @@ def _tool_run_background(command: str, name: str = "") -> str:
     env["VIRTUAL_ENV"] = os.path.join(PROJECT_ROOT, ".venv")
     env["PYTHONIOENCODING"] = "utf-8"
     env["PYTHONUNBUFFERED"] = "1"  # 强制 Python 无缓冲输出
-    env["PYTHONIOENCODING"] = "utf-8"  # 强制 Python IO 编码为 utf-8
 
     try:
         proc = subprocess.Popen(
@@ -494,7 +501,7 @@ def _tool_stop_process(pid: int) -> str:
     try:
         proc.wait(timeout=5)
     except Exception:
-        pass
+        logger.debug("Feishu background process did not exit cleanly", exc_info=True)
 
     _background_processes.pop(pid, None)
     return f"已停止进程: PID={pid} ({name})"
@@ -539,7 +546,7 @@ def _tool_send_input(pid: int, text: str) -> str:
 
     proc = info["proc"]
     if proc.poll() is not None:
-        return f"进程已退出,无法发送输入"
+        return "进程已退出,无法发送输入"
 
     try:
         # 使用控制文件方式(更可靠)
@@ -549,7 +556,6 @@ def _tool_send_input(pid: int, text: str) -> str:
         return f"已向 PID={pid} 发送控制指令: {text.strip()}"
     except Exception as e:
         return f"发送输入失败: {e}"
-        return f"发送输入失败: {e}"
 
 
 class FeishuAgent:
@@ -888,5 +894,9 @@ class FeishuAgent:
 
 
 if __name__ == "__main__":
+    logging.basicConfig(
+        level=logging.INFO,
+        format="%(asctime)s [%(name)s] %(levelname)s %(message)s",
+    )
     agent = FeishuAgent()
     agent.start()

+ 1 - 3
agent/agent/tools/builtin/feishu/feishu_client.py

@@ -43,7 +43,6 @@ from lark_oapi.api.im.v1 import (
     ListMessageResponse,
 )
 
-logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)
 
 
@@ -929,6 +928,7 @@ class FeishuClient:
 # ==================== 使用示例 ====================
 
 if __name__ == "__main__":
+    logging.basicConfig(level=logging.INFO)
     # 从环境变量获取配置
     APP_ID = os.getenv("FEISHU_APP_ID")
     APP_SECRET = os.getenv("FEISHU_APP_SECRET")
@@ -987,7 +987,5 @@ if __name__ == "__main__":
             blocking=True,
         )
 
-        # res = client.get_message_list(chat_id='oc_56e85f0e2c97405d176729b62d8f56e5', start_time=0, end_time=1770623620)
-        # print(f"获取消息列表结果: {json.dumps(res, indent=4, ensure_ascii=False)}")
     except KeyboardInterrupt:
         print("\n退出")

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

@@ -1,92 +0,0 @@
-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()

+ 0 - 1
agent/tests/test_legacy_feishu_credentials.py

@@ -8,7 +8,6 @@ import pytest
 
 _FEISHU_FILES = (
     "chat.py",
-    "chat_test.py",
     "feishu_client.py",
 )
 _CREDENTIAL_NAMES = {"FEISHU_APP_ID", "FEISHU_APP_SECRET"}