Files
Simon bea288e414 feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
2026-07-11 23:13:10 +08:00

328 lines
9.5 KiB
Python
Raw Permalink 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.
# =============================================================================
# 企微IT智能服务台 — 阶段5 自动化 进度推送
# =============================================================================
# 说明:负责把自动化处置过程实时推送给前端:
# 1. 专用 WS 通道 /ws/automation/{session_id}(坐席工作台 + 员工 H5 均可连)
# 2. 兜底推送:同时向坐席(/ws/{agent_id})和员工(/ws/h5/{employee_id})推送,
# 保证未连专用 WS 时也能收到事件。
# 3. 静默关单调度:处置成功后 N 分钟员工无异议则自动关单。
#
# 事件名统一以 automation. 前缀(见 app.constants)。
# =============================================================================
from __future__ import annotations
import asyncio
import logging
from typing import Any, Dict, Optional, Set
from app.constants import (
AUTOMATION_SILENT_CLOSE_TTL,
AUTOMATION_WS_ACTION_REQUIRED,
AUTOMATION_WS_ERROR,
AUTOMATION_WS_INFO_CORRECTED,
AUTOMATION_WS_INFO_SUPPLEMENTED,
AUTOMATION_WS_PAUSED,
AUTOMATION_WS_PROGRESS,
AUTOMATION_WS_RESOLVED,
AUTOMATION_WS_RESUMED,
AUTOMATION_WS_TAKEOVER,
AUTOMATION_WS_TIMEOUT_CLOSED,
)
from app.services.ws_manager import manager as ws_manager
logger = logging.getLogger(__name__)
# 会话级专用 WS 连接注册表:session_id -> {websocket, ...}
_automation_ws: Dict[str, Set[Any]] = {}
# 会话参与方(用于兜底推送):session_id -> {"agent_id":..,"employee_id":..}
_session_parties: Dict[str, Dict[str, Optional[str]]] = {}
# 静默关单任务:session_id -> asyncio.Task
_silent_close_tasks: Dict[str, asyncio.Task] = {}
def register_ws(session_id: str, websocket: Any) -> None:
"""注册专用 WS 连接。"""
_automation_ws.setdefault(session_id, set()).add(websocket)
def unregister_ws(session_id: str, websocket: Any) -> None:
"""注销专用 WS 连接。"""
conns = _automation_ws.get(session_id)
if conns:
conns.discard(websocket)
if not conns:
_automation_ws.pop(session_id, None)
def set_parties(
session_id: str,
agent_id: Optional[str] = None,
employee_id: Optional[str] = None,
) -> None:
"""记录会话参与方,用于兜底推送。"""
parties = _session_parties.setdefault(
session_id, {"agent_id": None, "employee_id": None}
)
if agent_id is not None:
parties["agent_id"] = agent_id
if employee_id is not None:
parties["employee_id"] = employee_id
def _build_message(event_type: str, session_id: str, data: Any) -> Dict[str, Any]:
"""构造统一 WS 消息信封。"""
return {"type": event_type, "session_id": session_id, "data": data or {}}
async def _send_to_automation_ws(session_id: str, message: Dict[str, Any]) -> None:
"""向专用 WS 连接推送(并清理失效连接)。"""
conns = list(_automation_ws.get(session_id, set()))
for ws in conns:
try:
await ws.send_json(message)
except Exception: # noqa: BLE001
unregister_ws(session_id, ws)
async def _publish(event_type: str, session_id: str, data: Any) -> None:
"""统一推送:专用 WS + 坐席/员工兜底 WS。"""
message = _build_message(event_type, session_id, data)
await _send_to_automation_ws(session_id, message)
parties = _session_parties.get(session_id, {})
# 兜底推送给坐席
if parties.get("agent_id"):
await ws_manager.send_to_agent(parties["agent_id"], message)
# 兜底推送给员工
if parties.get("employee_id"):
await ws_manager.send_to_employee(parties["employee_id"], message)
async def publish_progress(
session_id: str, step: str, message: str, action_id: Optional[str] = None
) -> None:
"""推送进度事件。"""
await _publish(
AUTOMATION_WS_PROGRESS,
session_id,
{"step": step, "message": message, "action_id": action_id},
)
async def publish_action_required(
session_id: str,
action: Any,
ticket: Any,
) -> None:
"""推送需要审批/确认事件(坐席审批或员工 H5 确认)。"""
await _publish(
AUTOMATION_WS_ACTION_REQUIRED,
session_id,
{
"action": {
"id": action.id,
"action_type": action.action_type,
"title": action.title,
"description": action.description,
"risk_level": action.risk_level,
"payload": action.payload,
},
"ticket": {
"id": ticket.id,
"channel": ticket.channel,
"status": ticket.status,
"reason": ticket.reason,
},
},
)
async def publish_resolved(session_id: str, summary: str) -> None:
"""推送处置成功事件。"""
await _publish(AUTOMATION_WS_RESOLVED, session_id, {"summary": summary})
async def publish_takeover(session_id: str, reason: str) -> None:
"""推送转人工事件。"""
await _publish(AUTOMATION_WS_TAKEOVER, session_id, {"reason": reason})
async def publish_error(session_id: str, code: int, message: str) -> None:
"""推送异常事件。"""
await _publish(AUTOMATION_WS_ERROR, session_id, {"code": code, "message": message})
# ==========================================================================
# 复杂场景重构第一阶段 — 新增 WS 事件推送
# ==========================================================================
async def publish_paused(
session_id: str,
title: str,
paused_at: str,
resume_hint: str = "",
) -> None:
"""推送会话暂停事件。
Args:
session_id: 会话ID
title: 会话标题
paused_at: 暂停时间(ISO 字符串)
resume_hint: 恢复提示文案
"""
await _publish(
AUTOMATION_WS_PAUSED,
session_id,
{
"session_id": session_id,
"title": title,
"paused_at": paused_at,
"resume_hint": resume_hint,
},
)
async def publish_resumed(
session_id: str,
title: str,
resumed_at: str,
current_step: str = "",
) -> None:
"""推送会话恢复事件。
Args:
session_id: 会话ID
title: 会话标题
resumed_at: 恢复时间(ISO 字符串)
current_step: 当前步骤描述
"""
await _publish(
AUTOMATION_WS_RESUMED,
session_id,
{
"session_id": session_id,
"title": title,
"resumed_at": resumed_at,
"current_step": current_step,
},
)
async def publish_timeout_closed(
session_id: str,
closed_at: str,
reason: str = "",
) -> None:
"""推送暂停超时关闭事件。
Args:
session_id: 会话ID
closed_at: 关闭时间(ISO 字符串)
reason: 关闭原因
"""
await _publish(
AUTOMATION_WS_TIMEOUT_CLOSED,
session_id,
{
"session_id": session_id,
"closed_at": closed_at,
"reason": reason,
},
)
async def publish_info_corrected(
session_id: str,
field: str,
old_value: str,
new_value: str,
version: int,
) -> None:
"""推送信息更正事件。
Args:
session_id: 会话ID
field: 更正的字段名
old_value: 旧值
new_value: 新值
version: 更正后的版本号
"""
await _publish(
AUTOMATION_WS_INFO_CORRECTED,
session_id,
{
"session_id": session_id,
"field": field,
"old_value": old_value,
"new_value": new_value,
"version": version,
},
)
async def publish_info_supplemented(
session_id: str,
field: str,
supplement_value: str,
new_value: str,
) -> None:
"""推送信息补充事件。
Args:
session_id: 会话ID
field: 补充的字段名
supplement_value: 本次补充的值
new_value: 补充后的完整值
"""
await _publish(
AUTOMATION_WS_INFO_SUPPLEMENTED,
session_id,
{
"session_id": session_id,
"field": field,
"supplement_value": supplement_value,
"new_value": new_value,
},
)
def schedule_silent_close(
session_id: str, ttl: int = AUTOMATION_SILENT_CLOSE_TTL, on_expire=None
) -> None:
"""调度静默关单:ttl 秒后若会话仍为 resolved,则自动关单。
Args:
session_id: 会话ID
ttl: 静默期秒数(默认 600 = 10 分钟)
on_expire: 到期回调 coroutine(通常 = AutoSessionService.auto_close
"""
# 取消已有的同名任务,避免重复调度
existing = _silent_close_tasks.get(session_id)
if existing is not None and not existing.done():
existing.cancel()
if on_expire is None:
return
async def _wait_and_close() -> None:
try:
await asyncio.sleep(ttl)
await on_expire(session_id)
except asyncio.CancelledError: # 被新的调度取消
pass
except Exception as e: # noqa: BLE001
logger.warning(f"静默关单回调异常 session={session_id}: {e}")
finally:
_silent_close_tasks.pop(session_id, None)
_silent_close_tasks[session_id] = asyncio.create_task(_wait_and_close())
def cancel_silent_close(session_id: str) -> None:
"""取消静默关单调度(如会话已被接管/关单)。"""
task = _silent_close_tasks.pop(session_id, None)
if task is not None and not task.done():
task.cancel()