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:
Simon
2026-07-13 02:17:03 +08:00
parent bea288e414
commit 449c6d4875
176 changed files with 46637 additions and 4805 deletions
+421
View File
@@ -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,
}