diff --git a/backend/app/services/renshenguo2_app_service.py b/backend/app/services/renshenguo2_app_service.py index a86ec11..6491793 100644 --- a/backend/app/services/renshenguo2_app_service.py +++ b/backend/app/services/renshenguo2_app_service.py @@ -87,6 +87,32 @@ def send_message_to_user( return False +def download_image_from_feishu(message_id: str, image_key: str) -> Optional[bytes]: + """从飞书下载图片内容(使用人参果1号应用凭证)。""" + token = _get_tenant_access_token() + if not token: + return None + try: + with httpx.Client(timeout=30) as client: + resp = client.get( + f"https://open.feishu.cn/open-apis/im/v1/messages/{message_id}/resources/{image_key}", + headers={"Authorization": f"Bearer {token}"}, + params={"type": "image"}, + ) + if resp.is_success and resp.content: + logger.info("人参果1号 飞书图片下载成功: image_key=%s size=%d", + image_key[:20], len(resp.content)) + return resp.content + else: + result = resp.json() if resp.content else {} + logger.warning("人参果1号 图片下载失败: code=%s msg=%s", + result.get("code"), result.get("msg")) + return None + except Exception as e: + logger.warning("人参果1号 图片下载异常: %s", e) + return None + + def send_plain_text(open_id: str, text: str) -> bool: token = _get_tenant_access_token() if not token: diff --git a/backend/app/services/renshenguo2_ws_handler.py b/backend/app/services/renshenguo2_ws_handler.py index d42d72f..17cd97c 100644 --- a/backend/app/services/renshenguo2_ws_handler.py +++ b/backend/app/services/renshenguo2_ws_handler.py @@ -44,6 +44,25 @@ def _get_message_text(data) -> Optional[str]: return None +def _get_image_key(data) -> Optional[str]: + """从图片消息中提取 image_key,返回 None 表示不是图片消息。""" + try: + ev = data.event + msg = getattr(ev, "message", None) + if not msg: + return None + msg_type = getattr(msg, "message_type", "") + if msg_type != "image": + return None + content_str = getattr(msg, "content", None) + if not content_str: + return None + parsed = json.loads(content_str) + return parsed.get("image_key", None) + except Exception: + return None + + def _get_sender_open_id(data) -> Optional[str]: try: ev = data.event @@ -127,13 +146,17 @@ async def _handle_message_async(data): union_id = _get_sender_union_id(data) chat_type = _get_chat_type(data) text = _get_message_text(data) + image_key = _get_image_key(data) + message_id = _get_message_id(data) if not open_id or chat_type != "p2p": return - logger.info("人参果1号收到消息: open_id=%s text=%s", open_id[:20], text[:50] if text else "(空)") - - if not text: + if text: + logger.info("人参果1号收到文本消息: open_id=%s text=%s", open_id[:20], text[:80]) + elif image_key and message_id: + logger.info("人参果1号收到图片消息: open_id=%s image_key=%s", open_id[:20], image_key[:30]) + else: return from sqlalchemy.orm import Session @@ -163,6 +186,34 @@ async def _handle_message_async(data): _reply_to_feishu(open_id, "正在思考,请稍候...") + # ─── 图片消息:下载飞书图片到工作区 ─────────────────── + image_attachment_path = None + if image_key and message_id and not text: + from app.services.renshenguo2_app_service import download_image_from_feishu + from app.services.builtin_tools import _local_file_workspace_root + + image_bytes = download_image_from_feishu(message_id, image_key) + if image_bytes: + ws_root = _local_file_workspace_root() + uploads_dir = ws_root / "agent_workspaces" / str(agent.id) / "images" + uploads_dir.mkdir(parents=True, exist_ok=True) + import time as _time + image_filename = f"feishu_img_{_time.strftime('%Y%m%d_%H%M%S')}.jpg" + image_path = uploads_dir / image_filename + image_path.write_bytes(image_bytes) + image_attachment_path = str(image_path.relative_to(ws_root)).replace("\\", "/") + text = ( + f"用户通过飞书发来了一张图片。" + f"图片已保存到工作区路径: {image_attachment_path}\n" + f"请先用 image_ocr 提取图片中的文字," + f"如果文字不足以理解内容,再用 image_vision 分析图片," + f"然后根据分析结果回复用户。" + ) + logger.info("人参果1号图片已保存: %s (%d bytes)", image_attachment_path, len(image_bytes)) + else: + _reply_to_feishu(open_id, "图片下载失败,请稍后重试。") + return + from app.agent_runtime import AgentRuntime, AgentConfig, AgentLLMConfig, AgentToolConfig, AgentMemoryConfig wc = agent.workflow_config or {} diff --git a/backend/app/services/renshenguo_app_service.py b/backend/app/services/renshenguo_app_service.py index e47759f..dd66be1 100644 --- a/backend/app/services/renshenguo_app_service.py +++ b/backend/app/services/renshenguo_app_service.py @@ -87,6 +87,40 @@ def send_message_to_user( return False +def download_image_from_feishu(message_id: str, image_key: str) -> Optional[bytes]: + """从飞书下载图片内容。 + + Args: + message_id: 飞书消息 ID + image_key: 图片 key(来自消息 content 中的 image_key) + + Returns: + 图片二进制数据,失败返回 None + """ + token = _get_tenant_access_token() + if not token: + return None + try: + with httpx.Client(timeout=30) as client: + resp = client.get( + f"https://open.feishu.cn/open-apis/im/v1/messages/{message_id}/resources/{image_key}", + headers={"Authorization": f"Bearer {token}"}, + params={"type": "image"}, + ) + if resp.is_success and resp.content: + logger.info("飞书图片下载成功: message_id=%s image_key=%s size=%d", + message_id, image_key[:20], len(resp.content)) + return resp.content + else: + result = resp.json() if resp.content else {} + logger.warning("飞书图片下载失败: code=%s msg=%s", + result.get("code"), result.get("msg")) + return None + except Exception as e: + logger.warning("飞书图片下载异常: %s", e) + return None + + def send_plain_text(open_id: str, text: str) -> bool: token = _get_tenant_access_token() if not token: diff --git a/backend/app/services/renshenguo_ws_handler.py b/backend/app/services/renshenguo_ws_handler.py index 5b5030b..48dc4d3 100644 --- a/backend/app/services/renshenguo_ws_handler.py +++ b/backend/app/services/renshenguo_ws_handler.py @@ -25,6 +25,15 @@ def _get_message_id(data) -> Optional[str]: return None +def _get_message_id(data) -> Optional[str]: + try: + ev = data.event + msg = getattr(ev, "message", None) + return getattr(msg, "message_id", None) if msg else None + except Exception: + return None + + def _get_message_text(data) -> Optional[str]: try: ev = data.event @@ -44,6 +53,25 @@ def _get_message_text(data) -> Optional[str]: return None +def _get_image_key(data) -> Optional[str]: + """从图片消息中提取 image_key,返回 None 表示不是图片消息。""" + try: + ev = data.event + msg = getattr(ev, "message", None) + if not msg: + return None + msg_type = getattr(msg, "message_type", "") + if msg_type != "image": + return None + content_str = getattr(msg, "content", None) + if not content_str: + return None + parsed = json.loads(content_str) + return parsed.get("image_key", None) + except Exception: + return None + + def _get_sender_open_id(data) -> Optional[str]: try: ev = data.event @@ -127,13 +155,17 @@ async def _handle_message_async(data): union_id = _get_sender_union_id(data) chat_type = _get_chat_type(data) text = _get_message_text(data) + image_key = _get_image_key(data) + message_id = _get_message_id(data) if not open_id or chat_type != "p2p": return - logger.info("人参果收到消息: open_id=%s text=%s", open_id[:20], text[:50] if text else "(空)") - - if not text: + if text: + logger.info("人参果收到文本消息: open_id=%s text=%s", open_id[:20], text[:80]) + elif image_key and message_id: + logger.info("人参果收到图片消息: open_id=%s image_key=%s", open_id[:20], image_key[:30]) + else: return from sqlalchemy.orm import Session @@ -163,6 +195,34 @@ async def _handle_message_async(data): _reply_to_feishu(open_id, "正在思考,请稍候...") + # ─── 图片消息:下载飞书图片到工作区 ─────────────────── + image_attachment_path: Optional[str] = None + if image_key and message_id and not text: + from app.services.renshenguo_app_service import download_image_from_feishu + from app.services.builtin_tools import _local_file_workspace_root, _sanitize_tool_path_string + + image_bytes = download_image_from_feishu(message_id, image_key) + if image_bytes: + ws_root = _local_file_workspace_root() + uploads_dir = ws_root / "agent_workspaces" / str(agent.id) / "images" + uploads_dir.mkdir(parents=True, exist_ok=True) + import time as _time + image_filename = f"feishu_img_{_time.strftime('%Y%m%d_%H%M%S')}.jpg" + image_path = uploads_dir / image_filename + image_path.write_bytes(image_bytes) + image_attachment_path = str(image_path.relative_to(ws_root)).replace("\\", "/") + text = ( + f"用户通过飞书发来了一张图片。" + f"图片已保存到工作区路径: {image_attachment_path}\n" + f"请先用 image_ocr 提取图片中的文字," + f"如果文字不足以理解内容,再用 image_vision 分析图片," + f"然后根据分析结果回复用户。" + ) + logger.info("人参果图片已保存: %s (%d bytes)", image_attachment_path, len(image_bytes)) + else: + _reply_to_feishu(open_id, "图片下载失败,请稍后重试。") + return + from app.agent_runtime import AgentRuntime, AgentConfig, AgentLLMConfig, AgentToolConfig, AgentMemoryConfig wc = agent.workflow_config or {}