Files
iot-device-management-service/app/routers/wechat_callback.py
16337 63a8d5a8f2 告警-工单解耦:企微交互+Agent全面切换到工单驱动
Part A: 数据层
- 新增 WechatCardState 模型(order_id ↔ alarm_id 映射 + response_code)
- 新建 models_iot.py(IoT 工单只读 ORM:ops_order + security_ext + clean_ext)
- config.py 新增 IOT_DATABASE_URL 配置

Part B: 企微解耦(alarm_id → order_id)
- wechat_service: response_code 存储迁移到 wechat_card_state,集中 helper
- 卡片发送/更新方法改用 order_id,按钮 key: confirm_{order_id}
- wechat_callback: 按钮解析改 order_id,反查 alarm_id(可空)
- wechat_notify_api: send-card/sync-status 以 orderId 为主键
- yudao_aiot_alarm: 卡片操作改用 order_id,删重复 helper

Part C: Agent 工具全面改为工单驱动
- 新建 order_query.py(查 IoT ops_order,支持安保+保洁工单)
- 新建 order_action.py(操作工单状态 + 提交处理结果)
- 更新 prompts.py 为工单助手
- 更新工具注册(__init__.py)

Part D: 日报改为工单驱动
- daily_report_service 从查 alarm_event 改为查 IoT ops_order + 扩展表
- 支持安保+保洁工单统计
2026-03-31 10:49:42 +08:00

279 lines
11 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.

"""
企微回调路由
处理安保人员在企微卡片上的操作(前往处理/误报忽略)。
接收企微用户消息并路由到交互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")
# ---- 图片消息 → Agent 图片分析 ----
if msg_type == "image":
user_id = msg.get("FromUserName", "")
media_id = msg.get("MediaId", "")
logger.info(f"收到企微图片: user={user_id}, media_id={media_id}")
asyncio.create_task(_process_agent_image(user_id, media_id))
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_agent_image(user_id: str, media_id: str):
"""异步处理图片消息:下载+持久化+智能路由(待处理工单关联 or VLM分析"""
try:
from app.services.agent_dispatcher import get_agent_dispatcher
from app.services.wechat_service import get_wechat_service
wechat = get_wechat_service()
await wechat.send_text_message(user_id, "收到图片,正在处理...")
dispatcher = get_agent_dispatcher()
reply = await dispatcher.handle_image(user_id, media_id)
await wechat.send_text_message(user_id, reply)
except Exception as e:
logger.error(f"Agent图片处理失败: user={user_id}, error={e}", exc_info=True)
try:
from app.services.wechat_service import get_wechat_service
wechat = get_wechat_service()
await wechat.send_text_message(user_id, "图片处理失败,请稍后重试。")
except Exception:
pass
async def _process_card_button_click(msg: dict):
"""
处理模板卡片按钮点击事件(两步状态机)
按钮 key 格式为 {action}_{order_id}以工单ID为主键驱动。
第一步按钮:
- confirm_{order_id}: 确认接单 → 调 IoT /confirm卡片更新到第二步
- ignore_{order_id}: 误报忽略 → 调 IoT /false-alarm终态
第二步按钮:
- complete_{order_id}: 已处理完成 → 调 IoT /submit终态
- false_{order_id}: 标记误报 → 调 IoT /false-alarm终态
终态按钮:
- done_{order_id}: 已完成,忽略
"""
try:
from app.services.wechat_service import get_wechat_service
from app.services.work_order_client import get_work_order_client
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}, task={task_id}"
)
# 解析 action 和 order_id
if event_key.startswith("confirm_"):
action = "confirm"
order_id = event_key[len("confirm_"):]
elif event_key.startswith("ignore_"):
action = "ignore"
order_id = event_key[len("ignore_"):]
elif event_key.startswith("complete_"):
action = "complete"
order_id = event_key[len("complete_"):]
elif event_key.startswith("false_"):
action = "false"
order_id = event_key[len("false_"):]
elif event_key.startswith("done_"):
return # 终态按钮,忽略
else:
logger.warning(f"未知的按钮 key: {event_key}")
return
wechat = get_wechat_service()
wo_client = get_work_order_client()
# 保存 response_code以 order_id 为 key
if response_code:
alarm_id = wechat.get_alarm_id_for_order(order_id)
wechat.save_response_code(order_id, response_code, alarm_id=alarm_id)
# 反查 alarm_id可空手动工单没有关联告警
alarm_id = wechat.get_alarm_id_for_order(order_id)
# ---- 确认接单:调 IoT /confirm ----
if action == "confirm":
if wo_client.enabled:
success = await wo_client.confirm_order(order_id)
if success:
logger.info(f"IoT工单已确认等待sync-status回调: order={order_id}, by={user_id}")
return # IoT 回调 sync-status 会更新告警+卡片
logger.warning(f"IoT工单确认失败: order={order_id}")
# 有告警时更新本地告警状态
if alarm_id:
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="CONFIRMED",
handle_status="HANDLING", handler=user_id, remark="企微确认接单")
# 更新卡片
if response_code:
await wechat.update_alarm_card_step2(
response_code=response_code,
user_ids=[user_id],
order_id=order_id,
operator_name=user_id,
)
# ---- 误报忽略:调 IoT /false-alarm ----
elif action == "ignore":
if wo_client.enabled:
success = await wo_client.false_alarm(order_id)
if success:
logger.info(f"IoT工单已标记误报等待sync-status回调: order={order_id}, by={user_id}")
return
if alarm_id:
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="FALSE",
handle_status="IGNORED", handler=user_id, remark="企微误报忽略")
if response_code:
await wechat.update_alarm_card_terminal(
response_code=response_code, user_ids=[user_id],
order_id=order_id, action="ignore", operator_name=user_id,
)
# ---- 已处理完成step2按钮调 IoT /submit ----
elif action == "complete":
if wo_client.enabled:
success = await wo_client.submit_order(order_id, result=f"已处理 by {user_id}")
if success:
logger.info(f"IoT工单已提交等待sync-status回调: order={order_id}")
return
if alarm_id:
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="CLOSED",
handle_status="DONE", handler=user_id, remark="企微已处理")
if response_code:
await wechat.update_alarm_card_terminal(
response_code=response_code, user_ids=[user_id],
order_id=order_id, action="complete", operator_name=user_id,
)
# ---- 标记误报step2按钮----
elif action == "false":
if wo_client.enabled:
success = await wo_client.false_alarm(order_id)
if success:
logger.info(f"IoT工单已标记误报等待sync-status回调: order={order_id}")
return
if alarm_id:
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="FALSE",
handle_status="IGNORED", handler=user_id, remark="企微标记误报")
if response_code:
await wechat.update_alarm_card_terminal(
response_code=response_code, user_ids=[user_id],
order_id=order_id, action="false", 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})