|
|
@@ -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:
|