449c6d4875
## 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分块上传
285 lines
10 KiB
Python
285 lines
10 KiB
Python
# =============================================================================
|
||
# 企微IT智能服务台 — Dify 分诊服务
|
||
# =============================================================================
|
||
# 说明:对接 Dify OpenAI 兼容接口,调用独立分诊应用进行问题分析。
|
||
# 功能:
|
||
# 1. analyze — 首次分诊分析,将问题拆分为分步选择题
|
||
# 2. get_next_step — 根据已选选项动态调整后续步骤
|
||
# 3. generate_reply — 根据收集的上下文生成最终回复
|
||
# 降级处理:Dify 不可用时返回友好错误,不中断主流程。
|
||
# =============================================================================
|
||
|
||
import json
|
||
import logging
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
import httpx
|
||
|
||
from app.config import settings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# =============================================================================
|
||
# Dify 分诊 System Prompt
|
||
# =============================================================================
|
||
TRIAGE_SYSTEM_PROMPT = """你是IT服务台智能分诊引擎,负责分析员工IT问题并拆分为分步选择题。
|
||
|
||
## 任务目标
|
||
1. 识别问题类型(硬件/软件/网络/安全/账号/其他)和具体分类
|
||
2. 评估置信度(0.0-1.0)和紧急度(high/medium/low)
|
||
3. 将复杂问题拆分为分步选择题(简单问题1-2步,复杂问题3-5步)
|
||
4. 每步最多4个选项,每个选项分配概率(0-1),所有选项概率之和为1
|
||
5. 推荐路由渠道(ai_self/human/auto_approval)
|
||
|
||
## 紧急度规则
|
||
- 消息含"紧急/马上/宕机/无法工作/崩溃/死机/蓝屏"→high
|
||
- 消息含"报错/失败/连不上/打不开/不能用"→medium
|
||
- 其余→low
|
||
|
||
## 排除选项
|
||
excluded_options 中的选项不出现在后续步骤中。
|
||
|
||
## 输出约束
|
||
必须输出合法JSON,不要输出解释性文字。JSON格式如下:
|
||
{
|
||
"triage_type": "confirm|transfer|approval",
|
||
"confidence": 0.85,
|
||
"urgency": "high|medium|low",
|
||
"problem_type": "硬件|软件|网络|安全|账号|其他",
|
||
"problem_category": "Outlook",
|
||
"suggested_route": "ai_self|human|auto_approval",
|
||
"matched_knowledge": "匹配到的知识条目描述",
|
||
"match_score": 0.89,
|
||
"context_tags": ["标签1", "标签2"],
|
||
"triage_steps": [
|
||
{
|
||
"question": "步骤问题文本",
|
||
"options": [
|
||
{"label": "选项A", "probability": 0.68},
|
||
{"label": "选项B", "probability": 0.22}
|
||
]
|
||
}
|
||
],
|
||
"total_steps": 3,
|
||
"reply": "AI回复文本(当triage_type=confirm时,引导员工选择)"
|
||
}
|
||
|
||
## 置信度评估
|
||
基于知识库匹配度、问题清晰度、上下文完整度综合评估。"""
|
||
|
||
|
||
class DifyTriageService:
|
||
"""Dify 分诊应用对接服务。
|
||
|
||
通过 OpenAI 兼容接口调用 Dify 独立分诊应用,
|
||
支持首次分析、动态步骤调整和最终回复生成。
|
||
|
||
Attributes:
|
||
api_url: Dify OpenAI 兼容接口地址
|
||
api_key: Dify API Key
|
||
timeout: 请求超时时间(秒)
|
||
"""
|
||
|
||
def __init__(self):
|
||
"""初始化 Dify 分诊服务。"""
|
||
self.api_url = settings.dify_triage_api_url
|
||
self.api_key = settings.dify_triage_api_key
|
||
self.timeout = settings.dify_triage_timeout
|
||
|
||
def is_available(self) -> bool:
|
||
"""检查 Dify 分诊服务是否可用。
|
||
|
||
Returns:
|
||
bool: API URL 和 Key 均已配置时返回 True
|
||
"""
|
||
return bool(self.api_url and self.api_key)
|
||
|
||
async def analyze(
|
||
self,
|
||
question: str,
|
||
context: Optional[List[str]] = None,
|
||
excluded_options: Optional[List[str]] = None,
|
||
step_index: int = 0,
|
||
) -> Dict[str, Any]:
|
||
"""调用 Dify 分诊应用进行首次分析。
|
||
|
||
Args:
|
||
question: 员工问题文本
|
||
context: 已收集的上下文标签(分步选择中累积)
|
||
excluded_options: 坐席已排除的选项标签
|
||
step_index: 当前步骤序号(0=首次分诊)
|
||
|
||
Returns:
|
||
Dict[str, Any]: Dify 返回的分诊结果 JSON
|
||
|
||
Raises:
|
||
RuntimeError: Dify 不可用或返回格式错误
|
||
"""
|
||
if not self.is_available():
|
||
logger.warning("Dify 分诊服务未配置,降级处理")
|
||
raise RuntimeError("Dify 分诊服务未配置")
|
||
|
||
# 构建用户消息内容(JSON 格式传入输入参数)
|
||
user_content = json.dumps(
|
||
{
|
||
"question": question,
|
||
"collected_context": context or [],
|
||
"excluded_options": excluded_options or [],
|
||
"step_index": step_index,
|
||
},
|
||
ensure_ascii=False,
|
||
)
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||
resp = await client.post(
|
||
f"{self.api_url}/v1/chat/completions",
|
||
headers={
|
||
"Authorization": f"Bearer {self.api_key}",
|
||
"Content-Type": "application/json",
|
||
},
|
||
json={
|
||
"model": "triage-engine",
|
||
"messages": [
|
||
{
|
||
"role": "system",
|
||
"content": TRIAGE_SYSTEM_PROMPT,
|
||
},
|
||
{"role": "user", "content": user_content},
|
||
],
|
||
"temperature": 0.3,
|
||
"max_tokens": 2000,
|
||
},
|
||
)
|
||
resp.raise_for_status()
|
||
|
||
# 解析 OpenAI 兼容响应格式
|
||
resp_data = resp.json()
|
||
content = resp_data["choices"][0]["message"]["content"]
|
||
|
||
# Dify 返回的是 JSON 字符串,需要解析
|
||
# 兼容 markdown 代码块包裹的 JSON
|
||
content = content.strip()
|
||
if content.startswith("```json"):
|
||
content = content[7:]
|
||
if content.startswith("```"):
|
||
content = content[3:]
|
||
if content.endswith("```"):
|
||
content = content[:-3]
|
||
content = content.strip()
|
||
|
||
result = json.loads(content)
|
||
logger.info(
|
||
"Dify 分诊分析成功: problem_type=%s, confidence=%s, urgency=%s",
|
||
result.get("problem_type"),
|
||
result.get("confidence"),
|
||
result.get("urgency"),
|
||
)
|
||
return result
|
||
|
||
except httpx.TimeoutException:
|
||
logger.error("Dify 分诊请求超时(%s秒)", self.timeout)
|
||
raise RuntimeError(f"Dify 分诊请求超时({self.timeout}秒)")
|
||
except httpx.HTTPStatusError as e:
|
||
logger.error("Dify 分诊 HTTP 错误: %s, status=%s", e, e.response.status_code)
|
||
raise RuntimeError(f"Dify 分诊服务返回错误: {e.response.status_code}")
|
||
except json.JSONDecodeError as e:
|
||
logger.error("Dify 分诊返回 JSON 解析失败: %s", e)
|
||
raise RuntimeError("Dify 分诊返回格式错误")
|
||
except Exception as e:
|
||
logger.error("Dify 分诊调用异常: %s", e, exc_info=True)
|
||
raise RuntimeError(f"Dify 分诊调用异常: {e}")
|
||
|
||
async def generate_reply(
|
||
self,
|
||
question: str,
|
||
collected_context: List[str],
|
||
) -> Dict[str, Any]:
|
||
"""根据收集的上下文生成最终回复。
|
||
|
||
Args:
|
||
question: 原始问题文本
|
||
collected_context: 分诊过程中收集的所有上下文
|
||
|
||
Returns:
|
||
Dict[str, Any]: 包含 reply 和 confidence 的字典
|
||
|
||
Raises:
|
||
RuntimeError: Dify 不可用或返回格式错误
|
||
"""
|
||
if not self.is_available():
|
||
logger.warning("Dify 分诊服务未配置,降级处理(生成回复)")
|
||
raise RuntimeError("Dify 分诊服务未配置")
|
||
|
||
user_content = json.dumps(
|
||
{
|
||
"question": question,
|
||
"collected_context": collected_context,
|
||
"excluded_options": [],
|
||
"step_index": -1, # -1 表示最终回复生成
|
||
},
|
||
ensure_ascii=False,
|
||
)
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||
resp = await client.post(
|
||
f"{self.api_url}/v1/chat/completions",
|
||
headers={
|
||
"Authorization": f"Bearer {self.api_key}",
|
||
"Content-Type": "application/json",
|
||
},
|
||
json={
|
||
"model": "triage-engine",
|
||
"messages": [
|
||
{
|
||
"role": "system",
|
||
"content": TRIAGE_SYSTEM_PROMPT,
|
||
},
|
||
{"role": "user", "content": user_content},
|
||
],
|
||
"temperature": 0.3,
|
||
"max_tokens": 2000,
|
||
},
|
||
)
|
||
resp.raise_for_status()
|
||
|
||
resp_data = resp.json()
|
||
content = resp_data["choices"][0]["message"]["content"]
|
||
|
||
content = content.strip()
|
||
if content.startswith("```json"):
|
||
content = content[7:]
|
||
if content.startswith("```"):
|
||
content = content[3:]
|
||
if content.endswith("```"):
|
||
content = content[:-3]
|
||
content = content.strip()
|
||
|
||
result = json.loads(content)
|
||
return {
|
||
"reply": result.get("reply", ""),
|
||
"confidence": result.get("confidence", 0.0),
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error("Dify 分诊生成回复异常: %s", e, exc_info=True)
|
||
raise RuntimeError(f"Dify 分诊生成回复异常: {e}")
|
||
|
||
|
||
# 单例
|
||
_dify_triage_service: Optional[DifyTriageService] = None
|
||
|
||
|
||
def get_dify_triage_service() -> DifyTriageService:
|
||
"""获取 DifyTriageService 单例。
|
||
|
||
Returns:
|
||
DifyTriageService: 单例实例
|
||
"""
|
||
global _dify_triage_service
|
||
if _dify_triage_service is None:
|
||
_dify_triage_service = DifyTriageService()
|
||
return _dify_triage_service
|