feat: 2026-07-12~13 全量更新 - AI对话链路改造+H5 v4/v5+坐席端v5+上下文感知诊断+知识库迭代3
## H5 员工端 v4 (2026-07-13 00:48 已部署)
- 人工按钮三态文案统一为"人工坐席"
- 按钮位置移至发送键和语音按钮上方(垂直堆叠)
- 点按钮直接调 store.shakeAgent(),删除 CallAgentModal 弹窗动画
- 截图快捷键提示改为"截图->粘贴:Alt+Shift+A-Ctrl+V ---> Ctrl+V"
- 移动端隐藏截图提示(CSS 媒体查询)
- AI转人工提示改为"已为您呼叫人工坐席,请稍等!"
- 坐席接入提示改为"坐席正在查看您的信息,请等待处理回复!"
- 删除"摇铃呼叫坐席"入口和文案
- 删除孤儿组件 MessageList.vue + shake 动画 CSS
## H5 员工端 v5 (2026-07-13 02:08 已部署)
- RightPanel v2.1:删除"软件安装"和"资源权限"标签页
- 移除标签栏,智能推荐(DynamicRecommend)直接展示
- 删除 SoftwareDownloads/ApprovalLinks 引用和相关 CSS
## AI 对话链路全栈改造 Phase 1-6 (已部署)
- Phase 1: Dify JSON输出 + 后端blocking解析 + 双WS推送 + 错误降级
- Phase 2: 关键词收窄(~25强意图词) + 两级分类Prompt + 删除前端checkApprovalIntent
- Phase 3: WS扩展(ai_thinking+dynamic_recommend) + ai_structured气泡 + RightPanel v2 + 选项回传
- Phase 4: VisionService接入 + 图片消息融合(5秒窗口) + 降级策略
- Phase 5: 坐席端ai_thinking指示器 + ai_structured/byod_card渲染 + handleNewMessage修复
- Phase 6: diagnosis_stage(6值) + response_time_ms计时 + 慢响应告警(>10s)
## 坐席端 v5 (2026-07-13 01:38 已部署)
- ai_structured/byod_card 只读渲染
- AI思考指示器 UI
- handleNewMessage 透传 msg_type/extra_data 修复
- 布局优化v2.0: QuickReplyBar L1+L2悬浮 + ReplyBox左右分区 + 右栏260/560px切换
- 键盘快捷键v2.3: 纯数字路由 + ESC分层撤销 + Shift+Space用event.code
## 上下文感知智能诊断闭环 (2026-07-12 已部署)
- 三层诊断(API→Script→AI) + 三段排队(VIP→info_locked→not locked)
- 答题插队 + 五场景关闭
- 迁移052(6表+6列) + queue_service + quiz_service + closing_service
- H5前端: QueueWaiting + RightPanel双Tab + InputBar三态 + ResolveConfirmCard
- 坐席前端: pending_close结单流程 + 信息锁定(Dify步骤完成+有效回答率≥70%)
## 知识库迭代3 (2026-07-12 已部署)
- 分诊交互(H5+坐席+Dify独立应用)
- 拓扑预览(ECharts只读)
- 代答排除(4种匹配器: keyword/regex/intent/category)
- 迁移051 + 44文件43测试通过
## 后端变更
- 6个Python文件改造(h5_ai_task.py/h5.py/ai_service.py/closing_service.py等)
- funny_phrase_service.py: shake/connected/keyword 默认文案更新
- session_service.py: 企微消息文案同步
- 新增: queue.py/quiz.py/triage.py/exclusion_rules.py 等API端点
- 新增: diagnostic.py/quiz.py/triage_session.py 等模型
- 新增: closing_service/queue_service/quiz_service/triage_service 等服务
## 文档更新
- CHANGELOG.md: 新增 [未发布] 区全部变更记录
- 项目管理主文档 v2.5: 新增v0.7.3版本 + 已完成看板 + 最近搞定
- 版本记录: 新增v0.7.3条目
- AI对话链路实施计划: Phase 1-6 全部标记✅已实施
- 新增架构图/时序图/类图(mermaid)
## 部署路径修正
- 服务器项目根路径: /opt/wecom-it-desk/
- 所有前端dist均为ro bind mount,只能在宿主机源路径操作
- 服务器nginx /h5/ 是静态文件服务(非proxy_pass)
- elFinder上传二进制不可靠(MD5不匹配),改用base64分块上传
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
# =============================================================================
|
||||
# 企微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,
|
||||
}
|
||||
Reference in New Issue
Block a user