- 群聊:image截图 + news图文卡片 + @人员text 组合推送 - 个人:button_interaction模板卡片(前往处理/误报忽略) - 新增媒体上传能力(upload_media),支持从COS URL下载后上传企微 - 新增群聊配置 WECHAT_GROUP_CHAT_ID - 删除 alarm_detail.html H5页面及相关接口 - 清理 wechat_callback.py 移除旧的H5回调和群聊测试接口 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
181 lines
6.2 KiB
Python
181 lines
6.2 KiB
Python
"""
|
||
企微回调路由
|
||
|
||
处理安保人员在企微卡片上的操作(前往处理/误报忽略)。
|
||
接收企微用户消息并路由到交互Agent。
|
||
"""
|
||
|
||
import asyncio
|
||
from fastapi import APIRouter, Depends, Query, Request
|
||
from fastapi.responses import PlainTextResponse
|
||
|
||
from app.yudao_compat import YudaoResponse
|
||
from app.services.alarm_event_service import get_alarm_event_service, AlarmEventService
|
||
from app.utils.logger import logger
|
||
|
||
router = APIRouter(prefix="/api/wechat", tags=["企微回调"])
|
||
|
||
|
||
# ==================== 交互Agent消息回调 ====================
|
||
|
||
@router.get("/agent/callback")
|
||
async def wechat_agent_verify(
|
||
msg_signature: str = Query(...),
|
||
timestamp: str = Query(...),
|
||
nonce: str = Query(...),
|
||
echostr: str = Query(...),
|
||
):
|
||
"""企微回调URL验证(首次配置时调用)"""
|
||
from app.services.wechat_crypto import WeChatCrypto
|
||
try:
|
||
crypto = WeChatCrypto()
|
||
echo = crypto.verify_url(msg_signature, timestamp, nonce, echostr)
|
||
return PlainTextResponse(content=echo)
|
||
except Exception as e:
|
||
logger.error(f"企微URL验证失败: {e}")
|
||
return PlainTextResponse(content="", status_code=403)
|
||
|
||
|
||
@router.post("/agent/callback")
|
||
async def wechat_agent_message(
|
||
request: Request,
|
||
msg_signature: str = Query(...),
|
||
timestamp: str = Query(...),
|
||
nonce: str = Query(...),
|
||
):
|
||
"""接收企微用户消息,路由到交互Agent"""
|
||
body = await request.body()
|
||
|
||
from app.services.wechat_crypto import WeChatCrypto
|
||
try:
|
||
crypto = WeChatCrypto()
|
||
msg = crypto.decrypt_message(body, msg_signature, timestamp, nonce)
|
||
except Exception as e:
|
||
logger.error(f"企微消息解密失败: {e}")
|
||
return PlainTextResponse(content="success")
|
||
|
||
msg_type = msg.get("MsgType", "")
|
||
event = msg.get("Event", "")
|
||
|
||
# ---- 模板卡片按钮点击事件 ----
|
||
if msg_type == "event" and event == "template_card_event":
|
||
asyncio.create_task(_process_card_button_click(msg))
|
||
return PlainTextResponse(content="success")
|
||
|
||
# ---- 文本消息 → Agent 对话 ----
|
||
if msg_type == "text":
|
||
user_id = msg.get("FromUserName", "")
|
||
content = msg.get("Content", "")
|
||
logger.info(f"收到企微消息: user={user_id}, content={content[:50]}")
|
||
asyncio.create_task(_process_agent_message(user_id, content))
|
||
return PlainTextResponse(content="success")
|
||
|
||
return PlainTextResponse(content="success")
|
||
|
||
|
||
async def _process_agent_message(user_id: str, content: str):
|
||
"""异步处理消息并主动回复"""
|
||
try:
|
||
from app.services.agent_dispatcher import get_agent_dispatcher
|
||
from app.services.wechat_service import get_wechat_service
|
||
|
||
dispatcher = get_agent_dispatcher()
|
||
reply = await dispatcher.handle_message(user_id, content)
|
||
|
||
wechat = get_wechat_service()
|
||
await wechat.send_text_message(user_id, reply)
|
||
except Exception as e:
|
||
logger.error(f"Agent消息处理失败: user={user_id}, error={e}", exc_info=True)
|
||
|
||
|
||
async def _process_card_button_click(msg: dict):
|
||
"""
|
||
处理模板卡片按钮点击事件
|
||
|
||
企微回调 XML 解密后包含:
|
||
- FromUserName: 点击者 userid
|
||
- EventKey: 按钮 key (handle_{alarm_id} / ignore_{alarm_id})
|
||
- TaskId: 卡片的 task_id (alarm_id)
|
||
- ResponseCode: 用于更新卡片状态(一次性)
|
||
"""
|
||
try:
|
||
from app.services.wechat_service import get_wechat_service
|
||
|
||
user_id = msg.get("FromUserName", "")
|
||
event_key = msg.get("EventKey", "")
|
||
task_id = msg.get("TaskId", "")
|
||
response_code = msg.get("ResponseCode", "")
|
||
|
||
logger.info(
|
||
f"卡片按钮点击: user={user_id}, key={event_key}, "
|
||
f"task={task_id}"
|
||
)
|
||
|
||
# 解析 action 和 alarm_id
|
||
if event_key.startswith("handle_"):
|
||
action = "handle"
|
||
alarm_id = event_key[len("handle_"):]
|
||
elif event_key.startswith("ignore_"):
|
||
action = "ignore"
|
||
alarm_id = event_key[len("ignore_"):]
|
||
elif event_key.startswith("done_"):
|
||
# 已处理状态的按钮,忽略
|
||
return
|
||
else:
|
||
logger.warning(f"未知的按钮 key: {event_key}")
|
||
return
|
||
|
||
# 更新告警状态
|
||
action_map = {
|
||
"handle": {
|
||
"alarm_status": "CONFIRMED",
|
||
"handle_status": "HANDLING",
|
||
"remark": "企微卡片-前往处理",
|
||
},
|
||
"ignore": {
|
||
"alarm_status": "FALSE",
|
||
"handle_status": "IGNORED",
|
||
"remark": "企微卡片-标记误报",
|
||
},
|
||
}
|
||
|
||
action_cfg = action_map.get(action)
|
||
if action_cfg:
|
||
service = get_alarm_event_service()
|
||
service.handle_alarm(
|
||
alarm_id=alarm_id,
|
||
alarm_status=action_cfg["alarm_status"],
|
||
handle_status=action_cfg["handle_status"],
|
||
handler=user_id,
|
||
remark=action_cfg["remark"],
|
||
)
|
||
logger.info(f"告警状态已更新: alarm={alarm_id}, action={action}, by={user_id}")
|
||
|
||
# 更新卡片按钮状态(变灰 + 显示处理结果)
|
||
if response_code:
|
||
wechat = get_wechat_service()
|
||
await wechat.update_alarm_card(
|
||
response_code=response_code,
|
||
user_ids=[user_id],
|
||
alarm_id=alarm_id,
|
||
action=action,
|
||
operator_name=user_id,
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理卡片按钮点击失败: {e}", exc_info=True)
|
||
|
||
|
||
# ==================== Agent测试接口(开发用) ====================
|
||
|
||
@router.post("/agent/test")
|
||
async def test_agent_message(
|
||
user_id: str = Query(default="test_user"),
|
||
content: str = Query(..., description="测试消息内容"),
|
||
):
|
||
"""测试Agent对话(开发用,无加密,直接返回回复)"""
|
||
from app.services.agent_dispatcher import get_agent_dispatcher
|
||
dispatcher = get_agent_dispatcher()
|
||
reply = await dispatcher.handle_message(user_id, content)
|
||
return YudaoResponse.success({"user_id": user_id, "content": content, "reply": reply})
|