# ============================================================================= # 企微IT智能服务台 — 分诊交互 API # ============================================================================= # 说明:分诊交互相关接口,包括: # H5 端(5个): # POST /api/h5/triage/start — 发起分诊 # POST /api/h5/triage/step — 提交步骤选择 # POST /api/h5/triage/skip — 跳过步骤 # POST /api/h5/triage/transfer — 转人工 # POST /api/h5/triage/complete — 分诊完成 # 坐席端(7个): # GET /api/agent/triage/pending — 待分诊列表 # GET /api/agent/triage/stats — 统计概要 # GET /api/agent/triage/history — 历史列表 # GET /api/agent/triage/export — 导出 xlsx # GET /api/agent/triage/{triage_id} — 分诊详情 # POST /api/agent/triage/{triage_id}/route — 路由操作 # POST /api/agent/triage/{triage_id}/exclude-options — 排除选项 # # 注意:固定路径路由(/history, /export)必须在参数路由(/{triage_id})之前注册, # 否则 FastAPI 会将 "history"/"export" 误匹配为 triage_id。 # ============================================================================= import logging from typing import Optional from fastapi import APIRouter, Depends, Query, Response from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import get_current_user, UserInfo from app.schemas.triage import ( TriageStartRequest, TriageStepRequest, TriageSkipRequest, TriageTransferRequest, TriageCompleteRequest, TriageRouteRequest, TriageExcludeOptionsRequest, ) from app.services.triage_service import get_triage_service logger = logging.getLogger(__name__) router = APIRouter() # 坐席端认证依赖(延迟导入避免循环依赖) def _get_current_agent(): from app.api.agents import get_current_agent return get_current_agent # ============================================================================= # H5 端接口(5个) # ============================================================================= @router.post("/h5/triage/start") async def start_triage( body: TriageStartRequest, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """发起分诊。 员工在 H5 端发送问题后,调用此接口发起 AI 分诊。 后端创建分诊会话,调用 Dify 分诊应用分析问题并生成分步选择题。 5秒超时自动转人工。 - **conversation_id**: 会话ID - **question**: 员工问题文本 """ service = get_triage_service() result = await service.start_triage( db=db, conversation_id=body.conversation_id, question=body.question, user_id=current_user.employee_id, user_name=current_user.name, user_dept=current_user.department, ) if result.get("status") == "timeout": return { "code": 0, "message": result.get("message", "分诊超时,已自动转人工"), "data": result, } return { "code": 0, "message": "success", "data": result, } @router.post("/h5/triage/step") async def submit_step( body: TriageStepRequest, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """提交步骤选择。 员工选择某个选项后,提交到后端记录上下文并获取下一步骤。 - **triage_id**: 分诊会话ID - **step_index**: 当前步骤序号(0-based) - **selected_label**: 选择的选项标签 """ service = get_triage_service() result = await service.submit_step( db=db, triage_id=body.triage_id, step_index=body.step_index, selected_label=body.selected_label, ) if "error" in result: return {"code": 404, "message": result["error"], "data": None} return { "code": 0, "message": "success", "data": result, } @router.post("/h5/triage/skip") async def skip_step( body: TriageSkipRequest, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """跳过步骤。 员工跳过当前步骤,直接进入下一步。 - **triage_id**: 分诊会话ID - **step_index**: 要跳过的步骤序号 """ service = get_triage_service() result = await service.skip_step( db=db, triage_id=body.triage_id, step_index=body.step_index, ) if "error" in result: return {"code": 404, "message": result["error"], "data": None} return { "code": 0, "message": "success", "data": result, } @router.post("/h5/triage/transfer") async def transfer_to_human( body: TriageTransferRequest, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """转人工。 员工主动选择转人工,后端记录上下文并将会话状态改为转人工。 - **triage_id**: 分诊会话ID - **context**: 已收集的上下文列表 """ service = get_triage_service() result = await service.transfer_to_human( db=db, triage_id=body.triage_id, context=body.context, ) if "error" in result: return {"code": 404, "message": result["error"], "data": None} return { "code": 0, "message": "已转接人工坐席", "data": result, } @router.post("/h5/triage/complete") async def complete_triage( body: TriageCompleteRequest, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """分诊完成。 所有步骤完成后,调用 Dify 生成最终 AI 回复。 - **triage_id**: 分诊会话ID - **context**: 已收集的上下文列表 """ service = get_triage_service() result = await service.complete_triage( db=db, triage_id=body.triage_id, context=body.context, ) if "error" in result: return {"code": 404, "message": result["error"], "data": None} return { "code": 0, "message": "success", "data": result, } # ============================================================================= # 坐席端接口(7个) # ============================================================================= @router.get("/agent/triage/pending") async def list_pending( urgency: Optional[str] = Query(default=None, description="紧急度筛选:high/medium/low"), problem_type: Optional[str] = Query(default=None, description="问题类型筛选"), page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=20, ge=1, le=100, description="每页数量"), agent=Depends(_get_current_agent()), db: AsyncSession = Depends(get_db), ): """获取待分诊列表(按紧急度排序)。 返回状态为 pending/triaging 的分诊会话,按紧急度排序(high > medium > low)。 **需要坐席认证。** """ service = get_triage_service() result = await service.list_pending( db=db, urgency=urgency, problem_type=problem_type, page=page, page_size=page_size, ) return { "code": 0, "message": "success", "data": result, } @router.get("/agent/triage/stats") async def get_stats( agent=Depends(_get_current_agent()), db: AsyncSession = Depends(get_db), ): """获取分诊看板统计概要。 返回6项统计指标:待分诊数/今日已分诊/AI自答/转人工/自动审批/平均耗时。 **需要坐席认证。** """ service = get_triage_service() result = await service.get_stats(db=db) return { "code": 0, "message": "success", "data": result, } @router.get("/agent/triage/history") async def get_history( date_from: Optional[str] = Query(default=None, description="开始日期(ISO格式)"), date_to: Optional[str] = Query(default=None, description="结束日期(ISO格式)"), route_action: Optional[str] = Query(default=None, description="路由动作筛选"), page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=20, ge=1, le=100, description="每页数量"), agent=Depends(_get_current_agent()), db: AsyncSession = Depends(get_db), ): """获取已分诊历史列表。 返回状态为 routed/skipped/timeout 的分诊会话。 **需要坐席认证。** """ service = get_triage_service() result = await service.get_history( db=db, date_from=date_from, date_to=date_to, route_action=route_action, page=page, page_size=page_size, ) return { "code": 0, "message": "success", "data": result, } @router.get("/agent/triage/export") async def export_sessions( date_from: Optional[str] = Query(default=None, description="开始日期(ISO格式)"), date_to: Optional[str] = Query(default=None, description="结束日期(ISO格式)"), agent=Depends(_get_current_agent()), db: AsyncSession = Depends(get_db), ): """导出分诊记录为 xlsx。 导出基础字段 + 分诊步骤详情。 **需要坐席认证。** """ service = get_triage_service() xlsx_data = await service.export_sessions( db=db, date_from=date_from, date_to=date_to, ) return Response( content=xlsx_data, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={ "Content-Disposition": "attachment; filename=triage_sessions.xlsx", }, ) @router.get("/agent/triage/{triage_id}") async def get_detail( triage_id: str, agent=Depends(_get_current_agent()), db: AsyncSession = Depends(get_db), ): """获取分诊详情。 返回分诊会话的完整数据,包括用户画像、问题描述、AI分析结果、已收集上下文等。 **需要坐席认证。** """ service = get_triage_service() result = await service.get_detail(db=db, triage_id=triage_id) if not result: return {"code": 404, "message": "分诊会话不存在", "data": None} return { "code": 0, "message": "success", "data": result, } @router.post("/agent/triage/{triage_id}/route") async def route_session( triage_id: str, body: TriageRouteRequest, agent=Depends(_get_current_agent()), db: AsyncSession = Depends(get_db), ): """坐席路由操作(覆盖 AI 建议)。 坐席可选择4种路由动作:ai_self(AI自答)/ human(转人工)/ auto_approval(自动审批)/ skip(跳过)。 **需要坐席认证。** """ service = get_triage_service() result = await service.route_session( db=db, triage_id=triage_id, route_action=body.route_action, route_note=body.route_note, operator_id=agent.user_id if hasattr(agent, "user_id") else str(agent.id), ) if not result: return {"code": 404, "message": "分诊会话不存在", "data": None} return { "code": 0, "message": "路由操作成功", "data": result, } @router.post("/agent/triage/{triage_id}/exclude-options") async def exclude_options( triage_id: str, body: TriageExcludeOptionsRequest, agent=Depends(_get_current_agent()), db: AsyncSession = Depends(get_db), ): """坐席排除/推荐分诊选项(WS 推送到 H5)。 坐席可排除某些选项或推荐某个选项,通过 WebSocket 实时推送到 H5 端。 **需要坐席认证。** """ service = get_triage_service() result = await service.exclude_options( db=db, triage_id=triage_id, excluded_labels=body.excluded_labels, recommended_label=body.recommended_label, ) if "error" in result: return {"code": 404, "message": result["error"], "data": None} return { "code": 0, "message": "success", "data": result, }