|
@@ -3,21 +3,33 @@ import os
|
|
|
import base64
|
|
import base64
|
|
|
import httpx
|
|
import httpx
|
|
|
import asyncio
|
|
import asyncio
|
|
|
|
|
+import logging
|
|
|
from typing import Optional, List, Dict, Any
|
|
from typing import Optional, List, Dict, Any
|
|
|
from .feishu_client import FeishuClient
|
|
from .feishu_client import FeishuClient
|
|
|
from agent.tools import tool, ToolResult, ToolContext
|
|
from agent.tools import tool, ToolResult, ToolContext
|
|
|
from agent.trace.models import MessageContent
|
|
from agent.trace.models import MessageContent
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
|
|
+
|
|
|
# 凭据只能由运行环境注入;缺失时 FeishuClient 会 fail-closed。
|
|
# 凭据只能由运行环境注入;缺失时 FeishuClient 会 fail-closed。
|
|
|
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.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")
|
|
UNREAD_SUMMARY_FILE = os.path.join(CHAT_HISTORY_DIR, "chat_summary.json")
|
|
|
|
|
|
|
|
# ==================== 一、文件内使用的功能函数 ====================
|
|
# ==================== 一、文件内使用的功能函数 ====================
|
|
@@ -30,7 +42,8 @@ def load_contacts() -> List[Dict[str, Any]]:
|
|
|
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 (OSError, json.JSONDecodeError, TypeError) as exc:
|
|
|
|
|
+ logger.warning("Failed to load Feishu contacts from %s: %s", CONTACTS_FILE, exc)
|
|
|
return []
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
@@ -39,8 +52,8 @@ def save_contacts(contacts: List[Dict[str, Any]]):
|
|
|
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:
|
|
|
|
|
- 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]]:
|
|
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:
|
|
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")
|
|
|
|
|
|
|
+ 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]]:
|
|
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:
|
|
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 (OSError, json.JSONDecodeError, TypeError) as exc:
|
|
|
|
|
+ logger.warning("Failed to load Feishu chat history %s: %s", path, exc)
|
|
|
return []
|
|
return []
|
|
|
return []
|
|
return []
|
|
|
|
|
|
|
@@ -121,8 +136,8 @@ def save_chat_history(contact_name: str, history: List[Dict[str, Any]]):
|
|
|
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:
|
|
|
|
|
- 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):
|
|
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:
|
|
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 (OSError, json.JSONDecodeError, TypeError) as exc:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "Failed to load Feishu unread summary %s: %s",
|
|
|
|
|
+ UNREAD_SUMMARY_FILE,
|
|
|
|
|
+ exc,
|
|
|
|
|
+ )
|
|
|
summary = {}
|
|
summary = {}
|
|
|
|
|
|
|
|
if reset:
|
|
if reset:
|
|
@@ -144,8 +164,12 @@ def update_unread_count(contact_name: str, increment: int = 1, reset: bool = Fal
|
|
|
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:
|
|
|
|
|
- print(f"更新未读摘要失败: {e}")
|
|
|
|
|
|
|
+ except OSError as exc:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "Failed to update Feishu unread summary %s: %s",
|
|
|
|
|
+ UNREAD_SUMMARY_FILE,
|
|
|
|
|
+ exc,
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== 三、@tool 工具 ====================
|
|
# ==================== 三、@tool 工具 ====================
|
|
@@ -259,8 +283,8 @@ async def feishu_send_message_to_contact(
|
|
|
last_res = client.send_image(
|
|
last_res = client.send_image(
|
|
|
to=receive_id, image=image_bytes
|
|
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://"):
|
|
elif url.startswith("http://") or url.startswith("https://"):
|
|
|
# 处理网络 URL
|
|
# 处理网络 URL
|
|
|
try:
|
|
try:
|
|
@@ -270,8 +294,8 @@ async def feishu_send_message_to_contact(
|
|
|
last_res = client.send_image(
|
|
last_res = client.send_image(
|
|
|
to=receive_id, image=img_resp.content
|
|
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:
|
|
else:
|
|
|
# 处理本地文件路径
|
|
# 处理本地文件路径
|
|
|
try:
|
|
try:
|
|
@@ -281,9 +305,9 @@ async def feishu_send_message_to_contact(
|
|
|
to=receive_id, image=local_path
|
|
to=receive_id, image=local_path
|
|
|
)
|
|
)
|
|
|
else:
|
|
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):
|
|
elif isinstance(content, dict):
|
|
|
# 如果是单块格式也支持一下
|
|
# 如果是单块格式也支持一下
|
|
|
item_type = content.get("type")
|
|
item_type = content.get("type")
|
|
@@ -324,8 +348,8 @@ async def feishu_send_message_to_contact(
|
|
|
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)
|
|
|
- except Exception as e:
|
|
|
|
|
- print(f"记录发送的消息失败: {e}")
|
|
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ logger.warning("Failed to record sent Feishu message: %s", exc)
|
|
|
|
|
|
|
|
return ToolResult(
|
|
return ToolResult(
|
|
|
title=f"消息已成功发送至 {contact_name}",
|
|
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}"},
|
|
"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": "[图片内容获取失败]"})
|
|
blocks.append({"type": "text", "text": "[图片内容获取失败]"})
|
|
|
elif msg_type == "post":
|
|
elif msg_type == "post":
|
|
|
blocks.append({"type": "text", "text": raw_content})
|
|
blocks.append({"type": "text", "text": raw_content})
|