From 4e03938334242f84e2876c470cbe43cbc58418c0 Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Tue, 31 Mar 2026 18:07:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E4=BA=A4=E4=BA=92Ag?= =?UTF-8?q?ent=E6=94=AF=E6=8C=81=E5=9B=BE=E7=89=87=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E7=9A=84=E6=89=8B=E5=8A=A8=E5=B7=A5=E5=8D=95=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/agent_dispatcher.py | 226 +++++++++++++++++++++++++++-- app/services/session_manager.py | 12 +- app/services/work_order_service.py | 4 + 3 files changed, 230 insertions(+), 12 deletions(-) diff --git a/app/services/agent_dispatcher.py b/app/services/agent_dispatcher.py index 2517abb..a0142bd 100644 --- a/app/services/agent_dispatcher.py +++ b/app/services/agent_dispatcher.py @@ -55,9 +55,12 @@ class AgentDispatcher: return "AI助手未启用,请联系管理员配置。" self._pending_images[user_id] = [] + session = get_session_manager().get(user_id) try: - reply = await self._langgraph_chat(user_id, content) + reply = await self._handle_manual_order_message(user_id, session, content.strip()) + if reply is None: + reply = await self._langgraph_chat(user_id, content) except Exception as e: logger.error(f"Agent对话失败: {e}", exc_info=True) reply = "抱歉,AI助手暂时无法响应,请稍后重试。" @@ -104,17 +107,30 @@ class AgentDispatcher: session.pending_alarm_id = handling_alarm_id return reply - # 4. VLM 分析 - from app.services.agent.prompts import IMAGE_ANALYZE_PROMPT - analysis = await self._analyze_image(presign_url, IMAGE_ANALYZE_PROMPT) + if session.state in { + "waiting_manual_order_area", + "waiting_manual_order_remark", + "waiting_manual_order_confirm", + } and session.pending_manual_order_images: + session.pending_manual_order_images.append(permanent_url) + if session.state == "waiting_manual_order_area": + return ( + f"已追加图片,当前共 {len(session.pending_manual_order_images)} 张。\n" + f"请选择区域:\n{self._format_area_options(session.pending_manual_order_area_options)}\n" + "请回复区域编号。" + ) + if session.state == "waiting_manual_order_remark": + return ( + f"已追加图片,当前共 {len(session.pending_manual_order_images)} 张。\n" + "请继续补充备注信息,可不填,回复“无”即可。" + ) + return ( + f"已追加图片,当前共 {len(session.pending_manual_order_images)} 张。\n" + "请回复“确认”创建工单,或回复“取消”放弃。" + ) - if analysis.get("has_anomaly"): - desc = analysis.get("description", "异常情况") - reply = f"检测到异常:{desc}\n\n如需上报,请描述具体位置和情况。" - else: - reply = "未检测到明显安全隐患。如有疑问请描述情况。" - - return reply + del presign_url + return await self._start_manual_order_flow(session, permanent_url) # ==================== LangGraph 对话 ==================== @@ -161,6 +177,194 @@ class AgentDispatcher: # ==================== 共用方法 ==================== + async def _start_manual_order_flow(self, session, image_url: str) -> str: + """启动手动工单创建流程。""" + session.pending_manual_order_images.append(image_url) + session.pending_manual_order_remark = "" + session.pending_manual_order_area_id = "" + session.pending_manual_order_area_name = "" + session.pending_manual_order_area_options = self._list_notify_areas() + session.state = "waiting_manual_order_area" + + if not session.pending_manual_order_area_options: + session.reset() + return "已收到图片,但当前没有可选区域,请联系管理员先配置通知区域。" + + return ( + "已收到图片,准备创建手动工单。\n" + f"请选择区域:\n{self._format_area_options(session.pending_manual_order_area_options)}\n" + "请回复区域编号。" + ) + + async def _handle_manual_order_message(self, user_id: str, session, content: str) -> Optional[str]: + """处理手动工单创建状态机。""" + if session.state == "waiting_manual_order_area": + area = self._match_area_option(content, session.pending_manual_order_area_options) + if not area: + return ( + "未识别到有效区域,请回复区域编号。\n" + f"{self._format_area_options(session.pending_manual_order_area_options)}" + ) + + assignees = self._get_area_assignees(area["area_id"]) + if not assignees: + return f"区域【{area['area_name']}】当前未绑定责任人,请重新选择其他区域。" + + session.pending_manual_order_area_id = area["area_id"] + session.pending_manual_order_area_name = area["area_name"] + session.state = "waiting_manual_order_remark" + return f"已选择区域:【{area['area_name']}】。\n请补充备注信息,可不填,回复“无”即可。" + + if session.state == "waiting_manual_order_remark": + session.pending_manual_order_remark = "" if content in {"无", "没有", "none", "None"} else content + session.state = "waiting_manual_order_confirm" + assignees = self._get_area_assignees(session.pending_manual_order_area_id) + assignee_names = "、".join(person["person_name"] for person in assignees) + remark = session.pending_manual_order_remark or "无" + return ( + "请确认是否创建工单:\n" + f"区域:【{session.pending_manual_order_area_name}】\n" + f"备注:{remark}\n" + f"图片:{len(session.pending_manual_order_images)} 张\n" + f"派发对象:{assignee_names}\n" + "回复“确认”创建,回复“取消”放弃。" + ) + + if session.state == "waiting_manual_order_confirm": + if content in {"取消", "算了", "不用了"}: + session.reset() + return "已取消本次手动工单创建。" + + if content not in {"确认", "是", "创建", "提交"}: + return "请回复“确认”创建工单,或回复“取消”放弃。" + + result = await self._create_manual_order(user_id, session) + session.reset() + return result + + return None + + @staticmethod + def _list_notify_areas() -> List[Dict[str, str]]: + from app.models import NotifyArea, get_session + + db = get_session() + try: + areas = ( + db.query(NotifyArea) + .filter(NotifyArea.enabled == 1) + .order_by(NotifyArea.area_name.asc()) + .all() + ) + return [ + {"index": str(idx), "area_id": area.area_id, "area_name": area.area_name} + for idx, area in enumerate(areas, start=1) + ] + finally: + db.close() + + @staticmethod + def _format_area_options(areas: List[Dict[str, str]]) -> str: + return "\n".join(f"{item['index']}. {item['area_name']}" for item in areas) + + @staticmethod + def _match_area_option(content: str, areas: List[Dict[str, str]]) -> Optional[Dict[str, str]]: + normalized = content.strip() + for item in areas: + if normalized == item["index"] or normalized == item["area_name"]: + return item + return None + + @staticmethod + def _get_area_assignees(area_id: str) -> List[Dict[str, str]]: + from app.models import AreaPersonBinding, get_session + + db = get_session() + try: + persons = ( + db.query(AreaPersonBinding) + .filter( + AreaPersonBinding.area_id == area_id, + AreaPersonBinding.enabled == 1, + ) + .order_by(AreaPersonBinding.notify_level.asc(), AreaPersonBinding.id.asc()) + .all() + ) + return [ + { + "person_name": person.person_name, + "wechat_uid": person.wechat_uid, + "role": person.role, + } + for person in persons + ] + finally: + db.close() + + async def _create_manual_order(self, user_id: str, session) -> str: + """创建手动工单并通知区域绑定人员。""" + assignees = self._get_area_assignees(session.pending_manual_order_area_id) + if not assignees: + return f"区域【{session.pending_manual_order_area_name}】当前未绑定责任人,工单未创建。" + + from app.services.wechat_service import get_wechat_service + from app.services.work_order_service import get_work_order_service + + area_name = session.pending_manual_order_area_name + remark = session.pending_manual_order_remark or "无" + title = f"【手动上报】{area_name}异常情况" + description_lines = [ + "来源:企微手动上报", + f"上报人:{user_id}", + f"区域:{area_name}", + f"备注:{remark}", + f"图片数量:{len(session.pending_manual_order_images)}", + ] + if session.pending_manual_order_images: + description_lines.append("图片链接:") + description_lines.extend(session.pending_manual_order_images) + + attachments = [ + {"type": "image", "url": image_url} + for image_url in session.pending_manual_order_images + ] + + primary_assignee = assignees[0] + work_order = get_work_order_service().create_work_order( + title=title, + description="\n".join(description_lines), + priority="medium", + assignee_uid=primary_assignee["wechat_uid"], + assignee_name=primary_assignee["person_name"], + attachments=attachments, + department=area_name, + ) + if not work_order: + return "工单创建失败,请稍后重试。" + + notify_message = ( + f"收到新的手动工单:\n" + f"工单号:{work_order.order_no}\n" + f"区域:{area_name}\n" + f"备注:{remark}" + ) + wechat = get_wechat_service() + for assignee in assignees: + try: + await wechat.send_text_message(assignee["wechat_uid"], notify_message) + except Exception as e: + logger.error( + f"手动工单通知失败: order={work_order.order_no}, " + f"user={assignee['wechat_uid']}, error={e}" + ) + + return ( + f"工单已创建。\n" + f"工单号:{work_order.order_no}\n" + f"区域:【{area_name}】\n" + f"已派发给:{'、'.join(person['person_name'] for person in assignees)}" + ) + async def _analyze_image(self, image_url: str, prompt: str) -> Dict: """VLM 分析图片内容""" try: diff --git a/app/services/session_manager.py b/app/services/session_manager.py index e3d369e..a3aa8d1 100644 --- a/app/services/session_manager.py +++ b/app/services/session_manager.py @@ -18,13 +18,18 @@ class UserSession: def __init__(self, user_id: str): self.user_id = user_id - self.state = "idle" # idle / waiting_close_photo + self.state = "idle" # idle / waiting_close_photo / waiting_manual_order_area / waiting_manual_order_remark / waiting_manual_order_confirm self.pending_image_url = "" # 暂存的图片 COS key self.pending_analysis = "" # VLM 分析结果描述 self.pending_alarm_type = "" # VLM 识别的告警类型 self.pending_order_id = "" # 待结单的工单 ID self.pending_alarm_id = "" # 关联的告警 ID self.pending_images: List[str] = [] # 暂存用户上传的图片 URL(用于工单结单) + self.pending_manual_order_images: List[str] = [] + self.pending_manual_order_area_id = "" + self.pending_manual_order_area_name = "" + self.pending_manual_order_area_options: List[Dict] = [] + self.pending_manual_order_remark = "" self.history: List[Dict] = [] # 对话历史 [{"role": "user/assistant", "content": ...}] self.updated_at = time.time() @@ -43,6 +48,11 @@ class UserSession: self.pending_order_id = "" self.pending_alarm_id = "" self.pending_images = [] + self.pending_manual_order_images = [] + self.pending_manual_order_area_id = "" + self.pending_manual_order_area_name = "" + self.pending_manual_order_area_options = [] + self.pending_manual_order_remark = "" def add_history(self, role: str, content): """添加对话记录,content 可以是 str 或 list(多模态)""" diff --git a/app/services/work_order_service.py b/app/services/work_order_service.py index 451bcb1..5ea6663 100644 --- a/app/services/work_order_service.py +++ b/app/services/work_order_service.py @@ -30,6 +30,8 @@ class WorkOrderService: assignee_uid: str = "", assignee_name: str = "", alarm_id: str = "", + attachments: Optional[List[Dict]] = None, + department: str = "", ) -> Optional[WorkOrder]: """创建工单""" db = get_session() @@ -42,6 +44,8 @@ class WorkOrderService: priority=WorkOrderPriority(priority) if priority in valid_priorities else WorkOrderPriority.MEDIUM, assignee_id=assignee_uid, assignee_name=assignee_name, + department=department, + attachments=attachments or [], status=WorkOrderStatus.CREATED, ) if alarm_id: