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 + 扩展表
- 支持安保+保洁工单统计
279 lines
11 KiB
Python
279 lines
11 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")
|
||
|
||
# ---- 图片消息 → 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})
|