فهرست منبع

格式:统一旧飞书工具的代码风格

SamLee 2 روز پیش
والد
کامیت
530b6a55aa
2فایلهای تغییر یافته به همراه337 افزوده شده و 211 حذف شده
  1. 164 89
      agent/agent/tools/builtin/feishu/chat.py
  2. 173 122
      agent/agent/tools/builtin/feishu/feishu_client.py

+ 164 - 89
agent/agent/tools/builtin/feishu/chat.py

@@ -12,37 +12,48 @@ from agent.trace.models import MessageContent
 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.path.join(
+    os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")),
+    "config",
+    "feishu_contacts.json",
+)
 CHAT_HISTORY_DIR = os.path.join(os.path.dirname(__file__), "chat_history")
 UNREAD_SUMMARY_FILE = os.path.join(CHAT_HISTORY_DIR, "chat_summary.json")
 
 # ==================== 一、文件内使用的功能函数 ====================
 
+
 def load_contacts() -> List[Dict[str, Any]]:
     """读取 contacts.json 中的所有联系人"""
     if not os.path.exists(CONTACTS_FILE):
         return []
     try:
-        with open(CONTACTS_FILE, 'r', encoding='utf-8') as f:
+        with open(CONTACTS_FILE, "r", encoding="utf-8") as f:
             return json.load(f)
     except Exception:
         return []
 
+
 def save_contacts(contacts: List[Dict[str, Any]]):
     """保存联系人信息到 contacts.json"""
     try:
-        with open(CONTACTS_FILE, 'w', encoding='utf-8') as f:
+        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}")
 
+
 def list_contacts_info() -> List[Dict[str, str]]:
     """
     1. 列出所有联系人信息
     读取 contacts.json 中的每一个联系人的 name、description,以字典列表返回
     """
     contacts = load_contacts()
-    return [{"name": c.get("name", ""), "description": c.get("description", "")} for c in 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]]:
     """
@@ -55,6 +66,7 @@ def get_contact_full_info(name: str) -> Optional[Dict[str, Any]]:
             return c
     return None
 
+
 def get_contact_by_id(id_value: str) -> Optional[Dict[str, Any]]:
     """根据 chat_id 或 open_id 获取联系人信息"""
     contacts = load_contacts()
@@ -63,6 +75,7 @@ def get_contact_by_id(id_value: str) -> Optional[Dict[str, Any]]:
             return c
     return None
 
+
 def update_contact_chat_id(name: str, chat_id: str):
     """
     3. 更新某一个联系人的 chat_id
@@ -78,71 +91,73 @@ def update_contact_chat_id(name: str, chat_id: str):
     if updated:
         save_contacts(contacts)
 
+
 # ==================== 二、聊天记录文件管理 ====================
 
+
 def _ensure_chat_history_dir():
     if not os.path.exists(CHAT_HISTORY_DIR):
         os.makedirs(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")
 
+
 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:
+            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:
+        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:
+            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:
+        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(
     hidden_params=["context"],
     groups=["feishu"],
     display={
-        "zh": {
-            "name": "获取飞书联系人列表",
-            "params": {}
-        },
-        "en": {
-            "name": "Get Feishu Contact List",
-            "params": {}
-        }
-    }
+        "zh": {"name": "获取飞书联系人列表", "params": {}},
+        "en": {"name": "Get Feishu Contact List", "params": {}},
+    },
 )
 async def feishu_get_contact_list(context: Optional[ToolContext] = None) -> ToolResult:
     """
@@ -155,9 +170,10 @@ async def feishu_get_contact_list(context: Optional[ToolContext] = None) -> Tool
     return ToolResult(
         title="获取联系人列表成功",
         output=json.dumps(contacts, ensure_ascii=False, indent=2),
-        metadata={"contacts": contacts}
+        metadata={"contacts": contacts},
     )
 
+
 @tool(
     hidden_params=["context"],
     groups=["feishu"],
@@ -166,22 +182,20 @@ 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. OpenAI multimodal list format."
-            }
-        }
-    }
+                "content": "Message content. OpenAI multimodal list format.",
+            },
+        },
+    },
 )
 async def feishu_send_message_to_contact(
-    contact_name: str,
-    content: MessageContent,
-    context: Optional[ToolContext] = None
+    contact_name: str, content: MessageContent, context: Optional[ToolContext] = None
 ) -> ToolResult:
     """
     给指定的联系人发送消息。支持发送文本和图片,OpenAI 多模态格式,会自动转换为飞书相应的格式并发起多次发送。
@@ -192,14 +206,24 @@ async def feishu_send_message_to_contact(
     """
     contact = get_contact_full_info(contact_name)
     if not contact:
-        return ToolResult(title="发送失败", output=f"未找到联系人: {contact_name}", error="Contact not found")
+        return ToolResult(
+            title="发送失败",
+            output=f"未找到联系人: {contact_name}",
+            error="Contact not found",
+        )
 
     client = FeishuClient(app_id=FEISHU_APP_ID, app_secret=FEISHU_APP_SECRET)
-    
+
     # 确定接收者 ID (优先使用 chat_id,否则使用 open_id)
-    receive_id = contact.get("chat_id") or contact.get("open_id") or contact.get("user_id")
+    receive_id = (
+        contact.get("chat_id") or contact.get("open_id") or contact.get("user_id")
+    )
     if not receive_id:
-        return ToolResult(title="发送失败", output="联系人 ID 信息缺失", error="Receiver ID not found in contacts.json")
+        return ToolResult(
+            title="发送失败",
+            output="联系人 ID 信息缺失",
+            error="Receiver ID not found in contacts.json",
+        )
 
     # 如果 content 是字符串,尝试解析为 JSON
     if isinstance(content, str):
@@ -218,7 +242,9 @@ async def feishu_send_message_to_contact(
             for item in content:
                 item_type = item.get("type")
                 if item_type == "text":
-                    last_res = client.send_message(to=receive_id, text=item.get("text", ""))
+                    last_res = client.send_message(
+                        to=receive_id, text=item.get("text", "")
+                    )
                 elif item_type == "image_url":
                     img_info = item.get("image_url", {})
                     url = img_info.get("url")
@@ -230,7 +256,9 @@ async def feishu_send_message_to_contact(
                             else:
                                 encoded = url
                             image_bytes = base64.b64decode(encoded)
-                            last_res = client.send_image(to=receive_id, image=image_bytes)
+                            last_res = client.send_image(
+                                to=receive_id, image=image_bytes
+                            )
                         except Exception as e:
                             print(f"解析 base64 图片失败: {e}")
                     elif url.startswith("http://") or url.startswith("https://"):
@@ -239,7 +267,9 @@ async def feishu_send_message_to_contact(
                             async with httpx.AsyncClient() as httpx_client:
                                 img_resp = await httpx_client.get(url, timeout=15.0)
                                 img_resp.raise_for_status()
-                                last_res = client.send_image(to=receive_id, image=img_resp.content)
+                                last_res = client.send_image(
+                                    to=receive_id, image=img_resp.content
+                                )
                         except Exception as e:
                             print(f"下载图片失败: {e}")
                     else:
@@ -247,7 +277,9 @@ async def feishu_send_message_to_contact(
                         try:
                             local_path = os.path.abspath(url)
                             if os.path.isfile(local_path):
-                                last_res = client.send_image(to=receive_id, image=local_path)
+                                last_res = client.send_image(
+                                    to=receive_id, image=local_path
+                                )
                             else:
                                 print(f"本地图片文件不存在: {local_path}")
                         except Exception as e:
@@ -256,14 +288,22 @@ async def feishu_send_message_to_contact(
             # 如果是单块格式也支持一下
             item_type = content.get("type")
             if item_type == "text":
-                last_res = client.send_message(to=receive_id, text=content.get("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)
+                return await feishu_send_message_to_contact(
+                    contact_name, content, context
+                )
         else:
-            return ToolResult(title="发送失败", output="不支持的内容格式", error="Invalid content format")
+            return ToolResult(
+                title="发送失败",
+                output="不支持的内容格式",
+                error="Invalid content format",
+            )
 
         if last_res:
             # 更新 chat_id
@@ -272,11 +312,15 @@ async def feishu_send_message_to_contact(
             # [待开启] 发送即记录:为了维护完整的聊天记录,将机器人发出的消息也保存到本地文件
             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}]
-                })
+                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)
@@ -286,12 +330,16 @@ async def feishu_send_message_to_contact(
             return ToolResult(
                 title=f"消息已成功发送至 {contact_name}",
                 output=f"发送成功。消息 ID: {last_res.message_id}",
-                metadata={"message_id": last_res.message_id, "chat_id": last_res.chat_id}
+                metadata={
+                    "message_id": last_res.message_id,
+                    "chat_id": last_res.chat_id,
+                },
             )
         return ToolResult(title="发送失败", output="没有执行成功的发送操作")
     except Exception as e:
         return ToolResult(title="发送异常", output=str(e), error=str(e))
 
+
 @tool(
     hidden_params=["context"],
     groups=["feishu"],
@@ -300,22 +348,22 @@ async def feishu_send_message_to_contact(
             "name": "获取飞书联系人回复",
             "params": {
                 "contact_name": "联系人名称",
-                "wait_time_seconds": "可选,如果当前没有新回复,则最多等待指定的秒数。在等待期间会每秒检查一次,一旦有新回复则立即返回。超过时长仍无回复则返回空。"
-            }
+                "wait_time_seconds": "可选,如果当前没有新回复,则最多等待指定的秒数。在等待期间会每秒检查一次,一旦有新回复则立即返回。超过时长仍无回复则返回空。",
+            },
         },
         "en": {
             "name": "Get Feishu Contact Replies",
             "params": {
                 "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."
-            }
-        }
-    }
+                "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
+    context: Optional[ToolContext] = None,
 ) -> ToolResult:
     """
     获取指定联系人的最新回复消息。
@@ -329,15 +377,24 @@ async def feishu_get_contact_replies(
     """
     contact = get_contact_full_info(contact_name)
     if not contact:
-        return ToolResult(title="获取失败", output=f"未找到联系人: {contact_name}", error="Contact not found")
+        return ToolResult(
+            title="获取失败",
+            output=f"未找到联系人: {contact_name}",
+            error="Contact not found",
+        )
 
     chat_id = contact.get("chat_id")
     if not chat_id:
-        return ToolResult(title="获取失败", output=f"联系人 {contact_name} 尚未建立会话 (无 chat_id)", error="No chat_id")
+        return ToolResult(
+            title="获取失败",
+            output=f"联系人 {contact_name} 尚未建立会话 (无 chat_id)",
+            error="No chat_id",
+        )
 
     client = FeishuClient(app_id=FEISHU_APP_ID, app_secret=FEISHU_APP_SECRET)
-    
+
     try:
+
         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:
@@ -349,7 +406,7 @@ async def feishu_get_contact_replies(
                 if msg.get("sender_type") == "app":
                     # 碰到机器人的消息即停止
                     break
-                
+
                 content_blocks = _convert_feishu_msg_to_openai_content(client, msg)
                 openai_blocks.extend(content_blocks)
 
@@ -358,7 +415,7 @@ async def feishu_get_contact_replies(
             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)):
@@ -369,13 +426,18 @@ async def feishu_get_contact_replies(
 
         return ToolResult(
             title=f"获取 {contact_name} 回复成功",
-            output=json.dumps(openai_blocks, ensure_ascii=False, indent=2) if openai_blocks else "目前没有新的用户回复",
-            metadata={"replies": openai_blocks}
+            output=json.dumps(openai_blocks, ensure_ascii=False, indent=2)
+            if openai_blocks
+            else "目前没有新的用户回复",
+            metadata={"replies": openai_blocks},
         )
     except Exception as e:
         return ToolResult(title="获取回复异常", output=str(e), error=str(e))
 
-def _convert_feishu_msg_to_openai_content(client: FeishuClient, msg: Dict[str, Any]) -> List[Dict[str, Any]]:
+
+def _convert_feishu_msg_to_openai_content(
+    client: FeishuClient, msg: Dict[str, Any]
+) -> List[Dict[str, Any]]:
     """将单条飞书消息内容转换为 OpenAI 多模态格式块列表"""
     blocks = []
     msg_type = msg.get("content_type")
@@ -390,15 +452,15 @@ def _convert_feishu_msg_to_openai_content(client: FeishuClient, msg: Dict[str, A
             image_key = content_dict.get("image_key")
             if image_key and message_id:
                 img_bytes = client.download_message_resource(
-                    message_id=message_id,
-                    file_key=image_key,
-                    resource_type="image"
+                    message_id=message_id, file_key=image_key, resource_type="image"
+                )
+                b64_str = base64.b64encode(img_bytes).decode("utf-8")
+                blocks.append(
+                    {
+                        "type": "image_url",
+                        "image_url": {"url": f"data:image/png;base64,{b64_str}"},
+                    }
                 )
-                b64_str = base64.b64encode(img_bytes).decode('utf-8')
-                blocks.append({
-                    "type": "image_url",
-                    "image_url": {"url": f"data:image/png;base64,{b64_str}"}
-                })
         except Exception as e:
             print(f"转换图片消息失败: {e}")
             blocks.append({"type": "text", "text": "[图片内容获取失败]"})
@@ -406,9 +468,10 @@ def _convert_feishu_msg_to_openai_content(client: FeishuClient, msg: Dict[str, A
         blocks.append({"type": "text", "text": raw_content})
     else:
         blocks.append({"type": "text", "text": f"[{msg_type} 消息]: {raw_content}"})
-    
+
     return blocks
 
+
 @tool(
     hidden_params=["context"],
     groups=["feishu"],
@@ -420,8 +483,8 @@ def _convert_feishu_msg_to_openai_content(client: FeishuClient, msg: Dict[str, A
                 "start_time": "起始时间戳 (秒),可选",
                 "end_time": "结束时间戳 (秒),可选",
                 "page_size": "分页大小,默认 20",
-                "page_token": "分页令牌,用于加载下一页,可选"
-            }
+                "page_token": "分页令牌,用于加载下一页,可选",
+            },
         },
         "en": {
             "name": "Get Feishu Chat History",
@@ -430,10 +493,10 @@ def _convert_feishu_msg_to_openai_content(client: FeishuClient, msg: Dict[str, A
                 "start_time": "Start timestamp (seconds), optional",
                 "end_time": "End timestamp (seconds), optional",
                 "page_size": "Page size, default 20",
-                "page_token": "Page token for next page, optional"
-            }
-        }
-    }
+                "page_token": "Page token for next page, optional",
+            },
+        },
+    },
 )
 async def feishu_get_chat_history(
     contact_name: str,
@@ -441,7 +504,7 @@ async def feishu_get_chat_history(
     end_time: Optional[int] = None,
     page_size: int = 20,
     page_token: Optional[str] = None,
-    context: Optional[ToolContext] = None
+    context: Optional[ToolContext] = None,
 ) -> ToolResult:
     """
     根据联系人名称获取完整的历史聊天记录。
@@ -458,11 +521,19 @@ async def feishu_get_chat_history(
     """
     contact = get_contact_full_info(contact_name)
     if not contact:
-        return ToolResult(title="获取历史失败", output=f"未找到联系人: {contact_name}", error="Contact not found")
+        return ToolResult(
+            title="获取历史失败",
+            output=f"未找到联系人: {contact_name}",
+            error="Contact not found",
+        )
 
     chat_id = contact.get("chat_id")
     if not chat_id:
-        return ToolResult(title="获取历史失败", output=f"联系人 {contact_name} 尚未建立会话 (无 chat_id)", error="No chat_id")
+        return ToolResult(
+            title="获取历史失败",
+            output=f"联系人 {contact_name} 尚未建立会话 (无 chat_id)",
+            error="No chat_id",
+        )
 
     client = FeishuClient(app_id=FEISHU_APP_ID, app_secret=FEISHU_APP_SECRET)
 
@@ -472,7 +543,7 @@ async def feishu_get_chat_history(
             start_time=start_time,
             end_time=end_time,
             page_size=page_size,
-            page_token=page_token
+            page_token=page_token,
         )
 
         if not res or "items" not in res:
@@ -481,24 +552,28 @@ async def feishu_get_chat_history(
         # 将所有消息转换为 OpenAI 多模态格式
         formatted_messages = []
         for msg in res["items"]:
-            formatted_messages.append({
-                "message_id": msg.get("message_id"),
-                "sender_id": msg.get("sender_id"),
-                "sender_type": "assistant" if msg.get("sender_type") == "app" else "user",
-                "create_time": msg.get("create_time"),
-                "content": _convert_feishu_msg_to_openai_content(client, msg)
-            })
+            formatted_messages.append(
+                {
+                    "message_id": msg.get("message_id"),
+                    "sender_id": msg.get("sender_id"),
+                    "sender_type": "assistant"
+                    if msg.get("sender_type") == "app"
+                    else "user",
+                    "create_time": msg.get("create_time"),
+                    "content": _convert_feishu_msg_to_openai_content(client, msg),
+                }
+            )
 
         result_data = {
             "messages": formatted_messages,
             "page_token": res.get("page_token"),
-            "has_more": res.get("has_more")
+            "has_more": res.get("has_more"),
         }
 
         return ToolResult(
             title=f"获取 {contact_name} 历史记录成功",
             output=json.dumps(result_data, ensure_ascii=False, indent=2),
-            metadata=result_data
+            metadata=result_data,
         )
     except Exception as e:
         return ToolResult(title="获取历史异常", output=str(e), error=str(e))

+ 173 - 122
agent/agent/tools/builtin/feishu/feishu_client.py

@@ -28,14 +28,19 @@ from typing import Any, Callable, Dict, List, Optional, Union
 import lark_oapi as lark
 from lark_oapi.api.contact.v3 import GetUserRequest
 from lark_oapi.api.im.v1 import (
-    CreateMessageRequest, CreateMessageRequestBody,
-    ReplyMessageRequest, ReplyMessageRequestBody,
+    CreateMessageRequest,
+    CreateMessageRequestBody,
+    ReplyMessageRequest,
+    ReplyMessageRequestBody,
     GetMessageRequest,
-    CreateImageRequest, CreateImageRequestBody,
+    CreateImageRequest,
+    CreateImageRequestBody,
     GetImageRequest,
-    CreateFileRequest, CreateFileRequestBody,
+    CreateFileRequest,
+    CreateFileRequestBody,
     GetMessageResourceRequest,
-    ListMessageRequest, ListMessageResponse
+    ListMessageRequest,
+    ListMessageResponse,
 )
 
 logging.basicConfig(level=logging.INFO)
@@ -44,18 +49,21 @@ logger = logging.getLogger(__name__)
 
 class FeishuDomain(Enum):
     """飞书域名"""
-    FEISHU = "https://open.feishu.cn"      # 中国版
-    LARK = "https://open.larksuite.com"    # 国际版
+
+    FEISHU = "https://open.feishu.cn"  # 中国版
+    LARK = "https://open.larksuite.com"  # 国际版
 
 
 class ChatType(Enum):
     """聊天类型"""
-    P2P = "p2p"      # 私聊
+
+    P2P = "p2p"  # 私聊
     GROUP = "group"  # 群聊
 
 
 class ReceiveIdType(Enum):
     """接收者ID类型"""
+
     OPEN_ID = "open_id"
     USER_ID = "user_id"
     UNION_ID = "union_id"
@@ -66,6 +74,7 @@ class ReceiveIdType(Enum):
 @dataclass
 class FeishuMessageEvent:
     """飞书消息事件"""
+
     message_id: str
     chat_id: str
     chat_type: ChatType
@@ -74,8 +83,8 @@ class FeishuMessageEvent:
     sender_open_id: str
     sender_user_id: Optional[str] = None
     sender_name: Optional[str] = None
-    root_id: Optional[str] = None      # 根消息ID(话题)
-    parent_id: Optional[str] = None    # 父消息ID(回复)
+    root_id: Optional[str] = None  # 根消息ID(话题)
+    parent_id: Optional[str] = None  # 父消息ID(回复)
     mentions: List[Dict] = field(default_factory=list)
     mentioned_bot: bool = False
 
@@ -83,6 +92,7 @@ class FeishuMessageEvent:
 @dataclass
 class SendResult:
     """发送结果"""
+
     message_id: str
     chat_id: str
 
@@ -124,11 +134,13 @@ class FeishuClient:
         self.verification_token = verification_token
 
         # 创建 Lark 客户端
-        self.client = lark.Client.builder() \
-            .app_id(app_id) \
-            .app_secret(app_secret) \
-            .domain(domain.value) \
+        self.client = (
+            lark.Client.builder()
+            .app_id(app_id)
+            .app_secret(app_secret)
+            .domain(domain.value)
             .build()
+        )
 
         # 缓存
         self._bot_open_id: Optional[str] = None
@@ -159,33 +171,37 @@ class FeishuClient:
             receive_id_type = self._resolve_receive_id_type(to)
 
         # 构建富文本消息 (支持 Markdown)
-        content = json.dumps({
-            "zh_cn": {
-                "content": [[{"tag": "md", "text": text}]]
-            }
-        })
+        content = json.dumps({"zh_cn": {"content": [[{"tag": "md", "text": text}]]}})
 
         if reply_to_message_id:
             # 回复消息
-            request = ReplyMessageRequest.builder() \
-                .message_id(reply_to_message_id) \
-                .request_body(ReplyMessageRequestBody.builder()
+            request = (
+                ReplyMessageRequest.builder()
+                .message_id(reply_to_message_id)
+                .request_body(
+                    ReplyMessageRequestBody.builder()
                     .content(content)
                     .msg_type("post")
-                    .build()) \
+                    .build()
+                )
                 .build()
+            )
 
             response = self.client.im.v1.message.reply(request)
         else:
             # 新消息
-            request = CreateMessageRequest.builder() \
-                .receive_id_type(receive_id_type.value) \
-                .request_body(CreateMessageRequestBody.builder()
+            request = (
+                CreateMessageRequest.builder()
+                .receive_id_type(receive_id_type.value)
+                .request_body(
+                    CreateMessageRequestBody.builder()
                     .receive_id(to)
                     .content(content)
                     .msg_type("post")
-                    .build()) \
+                    .build()
+                )
                 .build()
+            )
 
             response = self.client.im.v1.message.create(request)
 
@@ -193,8 +209,7 @@ class FeishuClient:
             raise Exception(f"发送消息失败: {response.msg} (code: {response.code})")
 
         return SendResult(
-            message_id=response.data.message_id,
-            chat_id=response.data.chat_id
+            message_id=response.data.message_id, chat_id=response.data.chat_id
         )
 
     def send_card(
@@ -222,24 +237,32 @@ class FeishuClient:
         content = json.dumps(card)
 
         if reply_to_message_id:
-            request = ReplyMessageRequest.builder() \
-                .message_id(reply_to_message_id) \
-                .request_body(ReplyMessageRequestBody.builder()
+            request = (
+                ReplyMessageRequest.builder()
+                .message_id(reply_to_message_id)
+                .request_body(
+                    ReplyMessageRequestBody.builder()
                     .content(content)
                     .msg_type("interactive")
-                    .build()) \
+                    .build()
+                )
                 .build()
+            )
 
             response = self.client.im.v1.message.reply(request)
         else:
-            request = CreateMessageRequest.builder() \
-                .receive_id_type(receive_id_type.value) \
-                .request_body(CreateMessageRequestBody.builder()
+            request = (
+                CreateMessageRequest.builder()
+                .receive_id_type(receive_id_type.value)
+                .request_body(
+                    CreateMessageRequestBody.builder()
                     .receive_id(to)
                     .content(content)
                     .msg_type("interactive")
-                    .build()) \
+                    .build()
+                )
                 .build()
+            )
 
             response = self.client.im.v1.message.create(request)
 
@@ -247,8 +270,7 @@ class FeishuClient:
             raise Exception(f"发送卡片失败: {response.msg}")
 
         return SendResult(
-            message_id=response.data.message_id,
-            chat_id=response.data.chat_id
+            message_id=response.data.message_id, chat_id=response.data.chat_id
         )
 
     def send_markdown_card(
@@ -270,16 +292,14 @@ class FeishuClient:
         """
         card = {
             "config": {"wide_screen_mode": True},
-            "elements": [{"tag": "markdown", "content": text}]
+            "elements": [{"tag": "markdown", "content": text}],
         }
         return self.send_card(to, card, reply_to_message_id)
 
     # ==================== 媒体处理 ====================
 
     def upload_image(
-            self,
-            image: Union[bytes, str],
-            image_type: str = "message"
+        self, image: Union[bytes, str], image_type: str = "message"
     ) -> str:
         """
         上传图片
@@ -300,12 +320,16 @@ class FeishuClient:
 
                 # 2. 构建请求
             # 注意:这里直接传入 file_obj
-            request = CreateImageRequest.builder() \
-                .request_body(CreateImageRequestBody.builder()
-                              .image_type(image_type)
-                              .image(file_obj)
-                              .build()) \
+            request = (
+                CreateImageRequest.builder()
+                .request_body(
+                    CreateImageRequestBody.builder()
+                    .image_type(image_type)
+                    .image(file_obj)
+                    .build()
+                )
                 .build()
+            )
 
             # 3. 发起请求
             response = self.client.im.v1.image.create(request)
@@ -330,9 +354,7 @@ class FeishuClient:
         Returns:
             bytes: 图片数据
         """
-        request = GetImageRequest.builder() \
-            .image_key(image_key) \
-            .build()
+        request = GetImageRequest.builder().image_key(image_key).build()
 
         response = self.client.im.v1.image.get(request)
 
@@ -364,24 +386,32 @@ class FeishuClient:
         receive_id_type = self._resolve_receive_id_type(to)
 
         if reply_to_message_id:
-            request = ReplyMessageRequest.builder() \
-                .message_id(reply_to_message_id) \
-                .request_body(ReplyMessageRequestBody.builder()
+            request = (
+                ReplyMessageRequest.builder()
+                .message_id(reply_to_message_id)
+                .request_body(
+                    ReplyMessageRequestBody.builder()
                     .content(content)
                     .msg_type("image")
-                    .build()) \
+                    .build()
+                )
                 .build()
+            )
 
             response = self.client.im.v1.message.reply(request)
         else:
-            request = CreateMessageRequest.builder() \
-                .receive_id_type(receive_id_type.value) \
-                .request_body(CreateMessageRequestBody.builder()
+            request = (
+                CreateMessageRequest.builder()
+                .receive_id_type(receive_id_type.value)
+                .request_body(
+                    CreateMessageRequestBody.builder()
                     .receive_id(to)
                     .content(content)
                     .msg_type("image")
-                    .build()) \
+                    .build()
+                )
                 .build()
+            )
 
             response = self.client.im.v1.message.create(request)
 
@@ -389,8 +419,7 @@ class FeishuClient:
             raise Exception(f"发送图片失败: {response.msg}")
 
         return SendResult(
-            message_id=response.data.message_id,
-            chat_id=response.data.chat_id
+            message_id=response.data.message_id, chat_id=response.data.chat_id
         )
 
     def upload_file(
@@ -423,13 +452,17 @@ class FeishuClient:
             tmp_path = tmp.name
 
         try:
-            request = CreateFileRequest.builder() \
-                .request_body(CreateFileRequestBody.builder()
+            request = (
+                CreateFileRequest.builder()
+                .request_body(
+                    CreateFileRequestBody.builder()
                     .file_type(file_type)
                     .file_name(file_name)
                     .file(open(tmp_path, "rb"))
-                    .build()) \
+                    .build()
+                )
                 .build()
+            )
 
             response = self.client.im.v1.file.create(request)
 
@@ -466,24 +499,32 @@ class FeishuClient:
         receive_id_type = self._resolve_receive_id_type(to)
 
         if reply_to_message_id:
-            request = ReplyMessageRequest.builder() \
-                .message_id(reply_to_message_id) \
-                .request_body(ReplyMessageRequestBody.builder()
+            request = (
+                ReplyMessageRequest.builder()
+                .message_id(reply_to_message_id)
+                .request_body(
+                    ReplyMessageRequestBody.builder()
                     .content(content)
                     .msg_type("file")
-                    .build()) \
+                    .build()
+                )
                 .build()
+            )
 
             response = self.client.im.v1.message.reply(request)
         else:
-            request = CreateMessageRequest.builder() \
-                .receive_id_type(receive_id_type.value) \
-                .request_body(CreateMessageRequestBody.builder()
+            request = (
+                CreateMessageRequest.builder()
+                .receive_id_type(receive_id_type.value)
+                .request_body(
+                    CreateMessageRequestBody.builder()
                     .receive_id(to)
                     .content(content)
                     .msg_type("file")
-                    .build()) \
+                    .build()
+                )
                 .build()
+            )
 
             response = self.client.im.v1.message.create(request)
 
@@ -491,15 +532,11 @@ class FeishuClient:
             raise Exception(f"发送文件失败: {response.msg}")
 
         return SendResult(
-            message_id=response.data.message_id,
-            chat_id=response.data.chat_id
+            message_id=response.data.message_id, chat_id=response.data.chat_id
         )
 
     def download_message_resource(
-        self,
-        message_id: str,
-        file_key: str,
-        resource_type: str = "file"
+        self, message_id: str, file_key: str, resource_type: str = "file"
     ) -> bytes:
         """
         下载消息中的资源文件
@@ -512,11 +549,13 @@ class FeishuClient:
         Returns:
             bytes: 文件数据
         """
-        request = GetMessageResourceRequest.builder() \
-            .message_id(message_id) \
-            .file_key(file_key) \
-            .type(resource_type) \
+        request = (
+            GetMessageResourceRequest.builder()
+            .message_id(message_id)
+            .file_key(file_key)
+            .type(resource_type)
             .build()
+        )
 
         response = self.client.im.v1.message_resource.get(request)
 
@@ -537,9 +576,7 @@ class FeishuClient:
         Returns:
             Dict: 消息详情,失败返回 None
         """
-        request = GetMessageRequest.builder() \
-            .message_id(message_id) \
-            .build()
+        request = GetMessageRequest.builder().message_id(message_id).build()
 
         response = self.client.im.v1.message.get(request)
 
@@ -571,7 +608,14 @@ class FeishuClient:
             "create_time": item.create_time,
         }
 
-    def get_message_list(self, chat_id: str, start_time: Optional[Union[str, int]] = None, end_time: Optional[Union[str, int]] = None, page_size: int = 20, page_token: Optional[str] = None) -> Optional[Dict]:
+    def get_message_list(
+        self,
+        chat_id: str,
+        start_time: Optional[Union[str, int]] = None,
+        end_time: Optional[Union[str, int]] = None,
+        page_size: int = 20,
+        page_token: Optional[str] = None,
+    ) -> Optional[Dict]:
         """
         获取消息列表
 
@@ -585,11 +629,13 @@ class FeishuClient:
         Returns:
             Dict: 包含消息列表和分页信息,失败返回 None
         """
-        builder = ListMessageRequest.builder() \
-            .container_id_type("chat") \
-            .container_id(chat_id) \
-            .sort_type("ByCreateTimeDesc") \
+        builder = (
+            ListMessageRequest.builder()
+            .container_id_type("chat")
+            .container_id(chat_id)
+            .sort_type("ByCreateTimeDesc")
             .page_size(page_size)
+        )
 
         if start_time is not None:
             builder.start_time(str(start_time))
@@ -606,7 +652,8 @@ class FeishuClient:
         # 处理失败返回
         if not response.success():
             logger.error(
-                f"client.im.v1.message.list failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}")
+                f"client.im.v1.message.list failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}"
+            )
             return None
 
         # 构建返回结果
@@ -622,20 +669,22 @@ class FeishuClient:
                 except (json.JSONDecodeError, TypeError):
                     pass
 
-                messages.append({
-                    "message_id": item.message_id,
-                    "chat_id": item.chat_id,
-                    "sender_id": item.sender.id if item.sender else None,
-                    "sender_type": item.sender.sender_type if item.sender else None,
-                    "content": content,
-                    "content_type": item.msg_type,
-                    "create_time": item.create_time,
-                })
+                messages.append(
+                    {
+                        "message_id": item.message_id,
+                        "chat_id": item.chat_id,
+                        "sender_id": item.sender.id if item.sender else None,
+                        "sender_type": item.sender.sender_type if item.sender else None,
+                        "content": content,
+                        "content_type": item.msg_type,
+                        "create_time": item.create_time,
+                    }
+                )
 
         return {
             "items": messages,
             "page_token": response.data.page_token,
-            "has_more": response.data.has_more
+            "has_more": response.data.has_more,
         }
 
     # ==================== 用户信息 ====================
@@ -654,10 +703,9 @@ class FeishuClient:
         if open_id in self._sender_name_cache:
             return {"name": self._sender_name_cache[open_id]}
 
-        request = GetUserRequest.builder() \
-            .user_id(open_id) \
-            .user_id_type("open_id") \
-            .build()
+        request = (
+            GetUserRequest.builder().user_id(open_id).user_id_type("open_id").build()
+        )
 
         response = self.client.contact.v3.user.get(request)
 
@@ -702,8 +750,7 @@ class FeishuClient:
         # 创建事件处理器
         # 注意: lark-oapi SDK 的回调函数只接受一个参数 (data)
         event_handler = lark.EventDispatcherHandler.builder(
-            self.encrypt_key or "",
-            self.verification_token or ""
+            self.encrypt_key or "", self.verification_token or ""
         ).register_p2_im_message_receive_v1(
             lambda data: self._handle_message_event(data, on_message)
         )
@@ -725,7 +772,9 @@ class FeishuClient:
             self.app_id,
             self.app_secret,
             event_handler=handler,
-            domain=lark.FEISHU_DOMAIN if self.domain == FeishuDomain.FEISHU else lark.LARK_DOMAIN,
+            domain=lark.FEISHU_DOMAIN
+            if self.domain == FeishuDomain.FEISHU
+            else lark.LARK_DOMAIN,
             log_level=lark.LogLevel.INFO,
         )
 
@@ -739,9 +788,7 @@ class FeishuClient:
             return thread
 
     def _handle_message_event(
-        self,
-        data,
-        callback: Callable[[FeishuMessageEvent], None]
+        self, data, callback: Callable[[FeishuMessageEvent], None]
     ):
         """处理消息事件"""
         try:
@@ -851,8 +898,8 @@ class FeishuClient:
         """去除 @机器人 的文本"""
         result = text
         for mention in mentions:
-            name = mention.name if hasattr(mention, 'name') else ""
-            key = mention.key if hasattr(mention, 'key') else ""
+            name = mention.name if hasattr(mention, "name") else ""
+            key = mention.key if hasattr(mention, "key") else ""
             if name:
                 result = result.replace(f"@{name}", "").strip()
             if key:
@@ -863,12 +910,18 @@ class FeishuClient:
         """检测文件类型"""
         ext = os.path.splitext(file_name)[1].lower()
         type_map = {
-            ".opus": "opus", ".ogg": "opus",
-            ".mp4": "mp4", ".mov": "mp4", ".avi": "mp4",
+            ".opus": "opus",
+            ".ogg": "opus",
+            ".mp4": "mp4",
+            ".mov": "mp4",
+            ".avi": "mp4",
             ".pdf": "pdf",
-            ".doc": "doc", ".docx": "doc",
-            ".xls": "xls", ".xlsx": "xls",
-            ".ppt": "ppt", ".pptx": "ppt",
+            ".doc": "doc",
+            ".docx": "doc",
+            ".xls": "xls",
+            ".xlsx": "xls",
+            ".ppt": "ppt",
+            ".pptx": "ppt",
         }
         return type_map.get(ext, "stream")
 
@@ -904,9 +957,7 @@ if __name__ == "__main__":
             # 先回复文字
             reply_text = f"收到你的消息: {event.content}"
             client.send_message(
-                to=event.chat_id,
-                text=reply_text,
-                reply_to_message_id=event.message_id
+                to=event.chat_id, text=reply_text, reply_to_message_id=event.message_id
             )
             print(f"  已回复文字: {reply_text}")
 
@@ -933,7 +984,7 @@ if __name__ == "__main__":
             on_message=handle_message,
             on_bot_added=lambda chat_id: print(f"机器人被添加到群: {chat_id}"),
             on_bot_removed=lambda chat_id: print(f"机器人被移出群: {chat_id}"),
-            blocking=True
+            blocking=True,
         )
 
         # res = client.get_message_list(chat_id='oc_56e85f0e2c97405d176729b62d8f56e5', start_time=0, end_time=1770623620)