- Create suyao_app_service.py and suyao_ws_handler.py for 苏瑶 Feishu bot
- Add SUYAO_APP_ID/SUYAO_APP_SECRET/SUYAO_AGENT_ID config fields
- Fix node config extraction bug (n.get("config") → n.get("data")) in all WS handlers
- Add _build_memory_config_from_node() to support per-agent memory settings
(max_history_messages, vector_memory_top_k, persist_to_db, etc.)
- Create 苏瑶1号 (Plan A: long context), 苏瑶2号 (Plan B: emotion tracking),
苏瑶3号 (Plan C: knowledge graph + RAG) agents with different memory strategies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
138 lines
4.5 KiB
Python
138 lines
4.5 KiB
Python
"""苏瑶飞书应用 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 缓存(tenant_access_token 有效期 2 小时,提前 5 分钟刷新)
|
||
_token_cache: dict = {"token": None, "expires_at": 0}
|
||
|
||
|
||
def _get_tenant_access_token() -> Optional[str]:
|
||
"""获取苏瑶应用的 tenant_access_token(带缓存)。"""
|
||
now = time.time()
|
||
if _token_cache["token"] and now < _token_cache["expires_at"] - 300:
|
||
return _token_cache["token"]
|
||
|
||
app_id = settings.SUYAO_APP_ID
|
||
app_secret = settings.SUYAO_APP_SECRET
|
||
if not app_id or not app_secret:
|
||
logger.warning("苏瑶应用未配置(SUYAO_APP_ID / SUYAO_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 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
|