feat: 两步卡片状态机 + 安保工单对接 + 先到先得结单

- 新增 work_order_client.py:SHA256签名的工单API客户端(创建/自动结单)
- 企微卡片改为两步交互:确认接单→[已处理完成/标记误报]→终态
- 告警通知后自动创建工单,orderId存入alarm_event_ext
- 边缘端resolve支持先到先得:终态不可被覆盖
- 边缘端resolve后异步触发工单自动结单+卡片终态更新
- 新增WorkOrderConfig配置项

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 16:34:54 +08:00
parent d2085f73be
commit 6cbf89a38b
8 changed files with 521 additions and 57 deletions

View File

@@ -90,16 +90,22 @@ async def _process_agent_message(user_id: str, content: str):
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: 用于更新卡片状态(一次性)
第一步按钮
- confirm_{alarm_id}: 确认接单 → 更新状态为处理中,创建工单,卡片更新到第二步
- ignore_{alarm_id}: 误报忽略 → 终态,自动结单
第二步按钮:
- complete_{alarm_id}: 已处理完成 → 终态,自动结单
- false_{alarm_id}: 标记误报 → 终态,自动结单
终态按钮:
- done_{alarm_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", "")
@@ -107,65 +113,155 @@ async def _process_card_button_click(msg: dict):
response_code = msg.get("ResponseCode", "")
logger.info(
f"卡片按钮点击: user={user_id}, key={event_key}, "
f"task={task_id}"
f"卡片按钮点击: user={user_id}, key={event_key}, task={task_id}"
)
# 解析 action 和 alarm_id
if event_key.startswith("handle_"):
action = "handle"
alarm_id = event_key[len("handle_"):]
if event_key.startswith("confirm_"):
action = "confirm"
alarm_id = event_key[len("confirm_"):]
elif event_key.startswith("ignore_"):
action = "ignore"
alarm_id = event_key[len("ignore_"):]
elif event_key.startswith("complete_"):
action = "complete"
alarm_id = event_key[len("complete_"):]
elif event_key.startswith("false_"):
action = "false"
alarm_id = event_key[len("false_"):]
elif event_key.startswith("done_"):
# 已处理状态的按钮,忽略
return
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": "企微卡片-标记误报",
},
}
wechat = get_wechat_service()
wo_client = get_work_order_client()
service = get_alarm_event_service()
action_cfg = action_map.get(action)
if action_cfg:
service = get_alarm_event_service()
# ---- 第一步:确认接单 ----
if action == "confirm":
# 更新告警状态为处理中
service.handle_alarm(
alarm_id=alarm_id,
alarm_status=action_cfg["alarm_status"],
handle_status=action_cfg["handle_status"],
alarm_status="CONFIRMED",
handle_status="HANDLING",
handler=user_id,
remark=action_cfg["remark"],
remark="企微卡片-确认接单",
)
logger.info(f"告警状态已更新: alarm={alarm_id}, action={action}, by={user_id}")
logger.info(f"告警已确认接单: alarm={alarm_id}, by={user_id}")
# 更新卡片按钮状态(变灰 + 显示处理结果
if response_code:
wechat = get_wechat_service()
await wechat.update_alarm_card(
response_code=response_code,
user_ids=[user_id],
# 更新卡片到第二步(新交互按钮)
if response_code:
await wechat.update_alarm_card_step2(
response_code=response_code,
user_ids=[user_id],
alarm_id=alarm_id,
operator_name=user_id,
)
# ---- 第一步:误报忽略 ----
elif action == "ignore":
service.handle_alarm(
alarm_id=alarm_id,
action=action,
operator_name=user_id,
alarm_status="FALSE",
handle_status="IGNORED",
handler=user_id,
remark="企微卡片-误报忽略",
)
logger.info(f"告警已标记忽略: alarm={alarm_id}, by={user_id}")
# 终态卡片
if response_code:
await wechat.update_alarm_card_terminal(
response_code=response_code,
user_ids=[user_id],
alarm_id=alarm_id,
action="ignore",
operator_name=user_id,
)
# 自动结单
order_id = _get_order_id_for_alarm(alarm_id)
if order_id:
await wo_client.auto_complete_order(order_id, f"误报忽略 by {user_id}")
# ---- 第二步:已处理完成 ----
elif action == "complete":
service.handle_alarm(
alarm_id=alarm_id,
alarm_status="CLOSED",
handle_status="DONE",
handler=user_id,
remark="企微卡片-已处理完成",
)
logger.info(f"告警已处理完成: alarm={alarm_id}, by={user_id}")
# 终态卡片
if response_code:
await wechat.update_alarm_card_terminal(
response_code=response_code,
user_ids=[user_id],
alarm_id=alarm_id,
action="complete",
operator_name=user_id,
)
# 自动结单
order_id = _get_order_id_for_alarm(alarm_id)
if order_id:
await wo_client.auto_complete_order(order_id, f"已处理 by {user_id}")
# ---- 第二步:标记误报 ----
elif action == "false":
service.handle_alarm(
alarm_id=alarm_id,
alarm_status="FALSE",
handle_status="IGNORED",
handler=user_id,
remark="企微卡片-标记误报",
)
logger.info(f"告警已标记误报: alarm={alarm_id}, by={user_id}")
# 终态卡片
if response_code:
await wechat.update_alarm_card_terminal(
response_code=response_code,
user_ids=[user_id],
alarm_id=alarm_id,
action="false",
operator_name=user_id,
)
# 自动结单
order_id = _get_order_id_for_alarm(alarm_id)
if order_id:
await wo_client.auto_complete_order(order_id, f"标记误报 by {user_id}")
except Exception as e:
logger.error(f"处理卡片按钮点击失败: {e}", exc_info=True)
def _get_order_id_for_alarm(alarm_id: str) -> str:
"""从 alarm_event_ext 中获取关联的工单ID"""
from app.models import get_session, AlarmEventExt
db = get_session()
try:
ext = db.query(AlarmEventExt).filter(
AlarmEventExt.alarm_id == alarm_id,
AlarmEventExt.ext_type == "WORK_ORDER",
).first()
if ext and ext.ext_data:
return ext.ext_data.get("order_id", "")
return ""
except Exception as e:
logger.error(f"查询工单ID失败: alarm={alarm_id}, error={e}")
return ""
finally:
db.close()
# ==================== Agent测试接口开发用 ====================
@router.post("/agent/test")