Files
aiagent/backend/app/services/renshenguo_app_service.py
renjianbo 66d52ad020 feat: 人参果/人参果1号 飞书图片解析支持
在飞书 WS handler 中新增图片消息识别与下载:
- _get_image_key: 检测飞书 image 类型消息,提取 image_key
- download_image_from_feishu: 调用飞书 API 下载图片二进制
- 图片保存到 agent_workspaces/{agent_id}/images/ 下
- Agent 自动调用 image_ocr + image_vision 分析后回复用户

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 08:02:14 +08:00

140 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""人参果飞书应用 API 服务 — 通过人参果应用发送消息到用户"""
from __future__ import annotations
import json
import logging
import time
from typing import Optional
import httpx
from app.core.config import settings
logger = logging.getLogger(__name__)
_token_cache: dict = {"token": None, "expires_at": 0}
def _get_tenant_access_token() -> Optional[str]:
now = time.time()
if _token_cache["token"] and now < _token_cache["expires_at"] - 300:
return _token_cache["token"]
app_id = settings.RENSHENGUO_APP_ID
app_secret = settings.RENSHENGUO_APP_SECRET
if not app_id or not app_secret:
logger.warning("人参果应用未配置RENSHENGUO_APP_ID / RENSHENGUO_APP_SECRET")
return None
try:
with httpx.Client(timeout=10) as client:
resp = client.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": app_id, "app_secret": app_secret},
)
result = resp.json()
if resp.is_success and result.get("code") == 0:
token = result["tenant_access_token"]
expire = result.get("expire", 7200)
_token_cache["token"] = token
_token_cache["expires_at"] = now + expire
logger.info("人参果 tenant_access_token 获取成功")
return token
else:
logger.warning("人参果 token 获取失败: %s", result)
return None
except Exception as e:
logger.warning("人参果 token 获取异常: %s", e)
return None
def send_message_to_user(
open_id: str, title: str, content: str,
status: str = "info", detail_link: Optional[str] = None,
) -> bool:
token = _get_tenant_access_token()
if not token:
return False
color_map = {"success": "green", "failed": "red", "info": "blue"}
color = color_map.get(status, "blue")
elements = [{"tag": "markdown", "content": content}]
if detail_link:
elements.append({
"tag": "action",
"actions": [{"tag": "button", "text": {"tag": "plain_text", "content": "查看详情"}, "url": detail_link, "type": "default"}],
})
card = {
"config": {"wide_screen_mode": True},
"header": {"title": {"tag": "plain_text", "content": title}, "template": color},
"elements": elements,
}
try:
with httpx.Client(timeout=10) as client:
resp = client.post(
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id",
headers={"Authorization": f"Bearer {token}"},
json={"receive_id": open_id, "msg_type": "interactive", "content": json.dumps(card, ensure_ascii=False)},
)
result = resp.json()
if resp.is_success and result.get("code") == 0:
logger.info("人参果消息发送成功: open_id=%s title=%s", open_id[:20], title)
return True
else:
logger.warning("人参果消息发送失败: code=%s msg=%s", result.get("code"), result.get("msg"))
return False
except Exception as e:
logger.warning("人参果消息发送异常: %s", e)
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:
return False
try:
with httpx.Client(timeout=10) as client:
resp = client.post(
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id",
headers={"Authorization": f"Bearer {token}"},
json={"receive_id": open_id, "msg_type": "text", "content": json.dumps({"text": text}, ensure_ascii=False)},
)
result = resp.json()
return resp.is_success and result.get("code") == 0
except Exception as e:
logger.warning("人参果文本消息发送异常: %s", e)
return False