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