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:
@@ -33,6 +33,12 @@ from app.models.routing_event import RoutingEvent # 路由命中统计(P1)
|
||||
# 会议室预定模块模型
|
||||
from app.models.terminal_room_binding import TerminalRoomBinding
|
||||
from app.models.meetingroom_booking_snapshot import MeetingroomBookingSnapshot
|
||||
from app.models.meetingroom_guide import MeetingroomGuide
|
||||
from app.models.meetingroom_repair import MeetingroomRepair
|
||||
# 知识库迭代 — 分诊 + 排除模块模型
|
||||
from app.models.triage_session import TriageSession
|
||||
from app.models.exclusion_rule import ExclusionRule
|
||||
from app.models.exclusion_log import ExclusionLog
|
||||
# 阶段5 自动化闭环模型
|
||||
from app.models.automation import (
|
||||
AutoSession,
|
||||
@@ -43,6 +49,9 @@ from app.models.automation import (
|
||||
ActionLog,
|
||||
MappingCache,
|
||||
)
|
||||
# 智能诊断修复闭环 + 分层排队 + 答题 + 关闭机制
|
||||
from app.models.diagnostic import DiagnosticTemplate, DiagnosticDispatch, DiagnosticReport
|
||||
from app.models.quiz import QuizQuestion, QuizAnswer, EmployeePoints
|
||||
# 所有模型类的列表,方便遍历
|
||||
__all__ = [
|
||||
"Conversation",
|
||||
@@ -71,6 +80,8 @@ __all__ = [
|
||||
"RoutingEvent",
|
||||
"TerminalRoomBinding",
|
||||
"MeetingroomBookingSnapshot",
|
||||
"MeetingroomGuide",
|
||||
"MeetingroomRepair",
|
||||
"AutoSession",
|
||||
"AutoAction",
|
||||
"ApprovalTicket",
|
||||
@@ -78,4 +89,13 @@ __all__ = [
|
||||
"RuleVersion",
|
||||
"ActionLog",
|
||||
"MappingCache",
|
||||
"TriageSession",
|
||||
"ExclusionRule",
|
||||
"ExclusionLog",
|
||||
"DiagnosticTemplate",
|
||||
"DiagnosticDispatch",
|
||||
"DiagnosticReport",
|
||||
"QuizQuestion",
|
||||
"QuizAnswer",
|
||||
"EmployeePoints",
|
||||
]
|
||||
|
||||
@@ -10,7 +10,7 @@ import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Index, Integer, JSON, String
|
||||
from sqlalchemy import Boolean, DateTime, Index, Integer, JSON, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
@@ -294,9 +294,63 @@ class Conversation(Base):
|
||||
comment="更新时间",
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# ======================================================================
|
||||
# P0新增:排队分层 + 答题插队 + 关闭机制
|
||||
# ======================================================================
|
||||
|
||||
# 答题插队优先级(每答3题前移1位,上限2位)
|
||||
# 计算公式:queue_priority = min(quiz_answered_count // 3, 2)
|
||||
# 排队时段内排序:queue_priority DESC → urgency_score DESC → created_at ASC
|
||||
queue_priority: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
default=0,
|
||||
comment="答题插队优先级(上限2)",
|
||||
)
|
||||
|
||||
# 信息是否锁定(Dify信息梳理步骤完成 + 有效回答占比≥70%)
|
||||
# 排队三段排序:VIP → info_locked=true → info_locked=false
|
||||
info_locked: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
comment="信息是否锁定",
|
||||
)
|
||||
|
||||
# 关闭方(谁关闭了会话)
|
||||
# employee: 员工主动关闭 / agent: 坐席结单 / ai: AI自助解决 / system_timeout: 超时自动
|
||||
resolved_by: Mapped[Optional[str]] = mapped_column(
|
||||
String(20),
|
||||
nullable=True,
|
||||
comment="关闭方: employee/agent/ai/system_timeout",
|
||||
)
|
||||
|
||||
# 关闭方式
|
||||
# ai_self: AI自助解决 / agent_confirm: 坐席结单+员工确认 / employee_initiative: 员工主动 / auto_timeout: 超时
|
||||
resolved_method: Mapped[Optional[str]] = mapped_column(
|
||||
String(30),
|
||||
nullable=True,
|
||||
comment="关闭方式: ai_self/agent_confirm/employee_initiative/auto_timeout",
|
||||
)
|
||||
|
||||
# 结单摘要(坐席结单时填写:问题类型+根因+解决方式)
|
||||
# 用于知识沉淀,调用Dify总结后生成知识条目草稿
|
||||
resolve_summary: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="结单摘要",
|
||||
)
|
||||
|
||||
# 重开时关联的原会话ID(24小时内重开创建新会话,关联原会话上下文)
|
||||
reference_conversation_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(36),
|
||||
nullable=True,
|
||||
comment="重开时关联的原会话ID",
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 索引定义(和架构文档 DDL 严格一致)
|
||||
# --------------------------------------------------------------------------
|
||||
# ----------------------------------------------------------------------
|
||||
__table_args__ = (
|
||||
# 按状态查询(如查询所有排队中的会话)
|
||||
Index("idx_conversations_status", "status"),
|
||||
@@ -310,6 +364,9 @@ class Conversation(Base):
|
||||
Index("idx_conversations_urgency_score", "urgency_score"),
|
||||
# 按最后消息时间倒序查询(最新消息的排前面)
|
||||
Index("idx_conversations_last_message_at", "last_message_at"),
|
||||
# P0新增:排队分层排序支持
|
||||
Index("idx_conversations_queue_priority", "queue_priority"),
|
||||
Index("idx_conversations_info_locked", "info_locked"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 诊断相关模型
|
||||
# =============================================================================
|
||||
# 说明:包含3张表,支撑三层诊断闭环:
|
||||
# 1. diagnostic_templates: 原子化诊断检查项模板库(管理员预置)
|
||||
# 2. diagnostic_reports: 客户端/API采集的诊断报告存储
|
||||
# 3. diagnostic_dispatches: 诊断下发记录,追踪 dispatch→execute→analyze→resolve 闭环
|
||||
#
|
||||
# 三层诊断架构:
|
||||
# Layer 1 — 火绒/联软API静默采集(check_type=api, api_source=huorong/lianruan)
|
||||
# Layer 2 — 客户端脚本兜底(check_type=script, script_template=PowerShell/zsh)
|
||||
# Layer 3 — AI分析报告 + 修复包推送(fix_template + risk_level分级审批)
|
||||
# =============================================================================
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Index, Integer, JSON, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class DiagnosticTemplate(Base):
|
||||
"""诊断模板 — 原子化检查项。
|
||||
|
||||
每条记录是一个独立的检查单元(如"ping网关""查DNS配置"),
|
||||
管理员在后台预置,AI只负责选择哪些检查项组合,不生成脚本内容。
|
||||
|
||||
Attributes:
|
||||
id: 模板唯一标识(UUID)
|
||||
category: 问题类别(network/vpn/email/system/printer/security/office)
|
||||
name: 检查项名称(如"网关连通性检测")
|
||||
check_type: 检查类型(api=服务端API采集 / script=客户端脚本采集)
|
||||
api_source: API来源(huorong/lianruan),仅check_type=api时有效
|
||||
api_method: 调用的API方法名(如get_terminal_detail),仅check_type=api时有效
|
||||
script_template: PowerShell/zsh脚本模板(参数化),仅check_type=script时有效
|
||||
fix_template: 对应的修复脚本模板(可选,部分检查项有配套修复)
|
||||
fix_risk_level: 修复风险等级(low/medium/high),决定审批流程
|
||||
target_condition: 触发此检查项的条件(如"dns_resolution=fail")
|
||||
description: 检查项描述
|
||||
is_active: 是否启用
|
||||
"""
|
||||
|
||||
__tablename__ = "diagnostic_templates"
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
category: Mapped[str] = mapped_column(
|
||||
String(50), nullable=False, comment="问题类别"
|
||||
)
|
||||
name: Mapped[str] = mapped_column(
|
||||
String(200), nullable=False, comment="检查项名称"
|
||||
)
|
||||
check_type: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="api",
|
||||
comment="检查类型: api/script"
|
||||
)
|
||||
api_source: Mapped[Optional[str]] = mapped_column(
|
||||
String(50), nullable=True, comment="API来源: huorong/lianruan"
|
||||
)
|
||||
api_method: Mapped[Optional[str]] = mapped_column(
|
||||
String(100), nullable=True, comment="调用的API方法名"
|
||||
)
|
||||
script_template: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="脚本模板(PowerShell/zsh)"
|
||||
)
|
||||
fix_template: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="修复脚本模板"
|
||||
)
|
||||
fix_risk_level: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="medium",
|
||||
comment="修复风险等级: low/medium/high"
|
||||
)
|
||||
target_condition: Mapped[Optional[str]] = mapped_column(
|
||||
String(200), nullable=True, comment="触发条件"
|
||||
)
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="检查项描述"
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True, comment="是否启用"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now,
|
||||
comment="创建时间"
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now,
|
||||
onupdate=datetime.now, comment="更新时间"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_diag_tpl_category", "category"),
|
||||
Index("idx_diag_tpl_type", "check_type"),
|
||||
Index("idx_diag_tpl_active", "is_active"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<DiagnosticTemplate(id={self.id}, name={self.name}, type={self.check_type})>"
|
||||
|
||||
|
||||
class DiagnosticDispatch(Base):
|
||||
"""诊断下发记录 — 追踪每次诊断的完整生命周期。
|
||||
|
||||
状态流转:dispatched → executed → analyzed → resolved
|
||||
|
||||
Attributes:
|
||||
id: 下发记录ID
|
||||
conversation_id: 关联的会话ID
|
||||
employee_id: 员工ID
|
||||
template_ids: 下发的诊断模板ID列表(JSON数组)
|
||||
script_content: 实际生成的脚本内容(参数化后的最终版本)
|
||||
script_hash: 脚本SHA256哈希(审计追溯)
|
||||
upload_token: 一次性上传token(绑定session+employee+TTL)
|
||||
status: 状态(dispatched/executed/analyzed/resolved)
|
||||
report_id: 关联的诊断报告ID(报告上传后填入)
|
||||
fix_dispatched: 是否已下发修复包
|
||||
created_at: 下发时间
|
||||
completed_at: 完成(resolved)时间
|
||||
"""
|
||||
|
||||
__tablename__ = "diagnostic_dispatches"
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
conversation_id: Mapped[str] = mapped_column(
|
||||
String(36), nullable=False, comment="关联会话ID"
|
||||
)
|
||||
employee_id: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, comment="员工ID"
|
||||
)
|
||||
template_ids: Mapped[list] = mapped_column(
|
||||
JSON, nullable=False, default=list, comment="诊断模板ID列表"
|
||||
)
|
||||
script_content: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="生成的脚本内容"
|
||||
)
|
||||
script_hash: Mapped[Optional[str]] = mapped_column(
|
||||
String(64), nullable=True, comment="脚本SHA256哈希"
|
||||
)
|
||||
upload_token: Mapped[Optional[str]] = mapped_column(
|
||||
String(128), nullable=True, comment="一次性上传token"
|
||||
)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="dispatched",
|
||||
comment="状态: dispatched/executed/analyzed/resolved"
|
||||
)
|
||||
report_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(36), nullable=True, comment="关联诊断报告ID"
|
||||
)
|
||||
fix_dispatched: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=False, comment="是否已下发修复包"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now,
|
||||
comment="下发时间"
|
||||
)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="完成时间"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_diag_dispatch_conv", "conversation_id"),
|
||||
Index("idx_diag_dispatch_employee", "employee_id"),
|
||||
Index("idx_diag_dispatch_status", "status"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<DiagnosticDispatch(id={self.id}, conv={self.conversation_id}, status={self.status})>"
|
||||
|
||||
|
||||
class DiagnosticReport(Base):
|
||||
"""诊断报告 — 存储采集到的检查结果和AI分析结论。
|
||||
|
||||
Attributes:
|
||||
id: 报告ID
|
||||
dispatch_id: 关联的下发记录ID
|
||||
conversation_id: 关联的会话ID
|
||||
employee_id: 员工ID
|
||||
template_ids: 涉及的诊断模板ID列表
|
||||
report_data: 检查结果JSON([{name, status, detail, raw_output}])
|
||||
ai_analysis: AI分析结论JSON({root_cause, confidence, suggested_actions})
|
||||
status: 报告状态(pending/analyzed/resolved)
|
||||
created_at: 报告上传时间
|
||||
"""
|
||||
|
||||
__tablename__ = "diagnostic_reports"
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
dispatch_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(36), nullable=True, comment="关联下发记录ID"
|
||||
)
|
||||
conversation_id: Mapped[str] = mapped_column(
|
||||
String(36), nullable=False, comment="关联会话ID"
|
||||
)
|
||||
employee_id: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, comment="员工ID"
|
||||
)
|
||||
template_ids: Mapped[list] = mapped_column(
|
||||
JSON, nullable=False, default=list, comment="涉及诊断模板ID列表"
|
||||
)
|
||||
report_data: Mapped[Dict[str, Any]] = mapped_column(
|
||||
JSON, nullable=False, default=dict,
|
||||
comment="检查结果: [{name, status(pass/fail/warn/pending), detail, raw_output}]"
|
||||
)
|
||||
ai_analysis: Mapped[Optional[Dict[str, Any]]] = mapped_column(
|
||||
JSON, nullable=True,
|
||||
comment="AI分析: {root_cause, confidence, severity, suggested_actions}"
|
||||
)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="pending",
|
||||
comment="报告状态: pending/analyzed/resolved"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now,
|
||||
comment="报告上传时间"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_diag_report_conv", "conversation_id"),
|
||||
Index("idx_diag_report_employee", "employee_id"),
|
||||
Index("idx_diag_report_status", "status"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<DiagnosticReport(id={self.id}, conv={self.conversation_id}, status={self.status})>"
|
||||
@@ -0,0 +1,120 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 排除命中日志模型
|
||||
# =============================================================================
|
||||
# 说明:对应数据库 exclusion_logs 表
|
||||
# 每次排除规则命中时记录一条日志,用于审计和统计。
|
||||
# =============================================================================
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import DateTime, Index, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class ExclusionLog(Base):
|
||||
"""排除命中日志模型 — 对应 exclusion_logs 表。
|
||||
|
||||
每次排除规则命中时记录一条日志,用于审计追踪和统计分析。
|
||||
|
||||
Attributes:
|
||||
id: 日志ID(UUID)
|
||||
rule_id: 关联的规则ID
|
||||
rule_name: 规则名称(冗余,防止规则删除后日志丢失名称)
|
||||
conversation_id: 会话ID
|
||||
user_id: 员工ID
|
||||
message_content: 触发命中的消息内容
|
||||
match_type: 匹配方式
|
||||
matched_detail: 命中详情(命中的关键词/正则/意图/分类)
|
||||
action_type: 执行的动作类型
|
||||
action_result: 执行结果(success/failed)
|
||||
created_at: 创建时间
|
||||
"""
|
||||
|
||||
__tablename__ = "exclusion_logs"
|
||||
|
||||
# 主键
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
primary_key=True,
|
||||
default=lambda: str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
# 规则关联
|
||||
rule_id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
nullable=False,
|
||||
comment="关联的规则ID",
|
||||
)
|
||||
rule_name: Mapped[Optional[str]] = mapped_column(
|
||||
String(200),
|
||||
nullable=True,
|
||||
comment="规则名称(冗余存储)",
|
||||
)
|
||||
|
||||
# 会话信息
|
||||
conversation_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(36),
|
||||
nullable=True,
|
||||
comment="会话ID",
|
||||
)
|
||||
user_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
nullable=True,
|
||||
comment="员工ID",
|
||||
)
|
||||
|
||||
# 命中详情
|
||||
message_content: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="触发命中的消息内容",
|
||||
)
|
||||
match_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(20),
|
||||
nullable=True,
|
||||
comment="匹配方式",
|
||||
)
|
||||
matched_detail: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="命中详情",
|
||||
)
|
||||
|
||||
# 执行结果
|
||||
action_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(50),
|
||||
nullable=True,
|
||||
comment="执行的动作类型",
|
||||
)
|
||||
action_result: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
default="success",
|
||||
comment="执行结果:success/failed",
|
||||
)
|
||||
|
||||
# 时间戳
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=datetime.now,
|
||||
comment="创建时间",
|
||||
)
|
||||
|
||||
# 索引
|
||||
__table_args__ = (
|
||||
Index("idx_exclusion_logs_rule", "rule_id"),
|
||||
Index("idx_exclusion_logs_created", "created_at"),
|
||||
Index("idx_exclusion_logs_conversation", "conversation_id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""排除日志对象的字符串表示。"""
|
||||
return (
|
||||
f"<ExclusionLog(id={self.id}, rule={self.rule_name}, "
|
||||
f"action={self.action_type}, result={self.action_result})>"
|
||||
)
|
||||
@@ -0,0 +1,146 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 代答排除规则模型
|
||||
# =============================================================================
|
||||
# 说明:对应数据库 exclusion_rules 表
|
||||
# 存储代答排除规则,AI回复前按优先级依次检查,命中则执行对应动作。
|
||||
# =============================================================================
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import DateTime, Index, Integer, JSON, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class ExclusionRule(Base):
|
||||
"""代答排除规则模型 — 对应 exclusion_rules 表。
|
||||
|
||||
存储排除规则配置,支持4种匹配方式(关键词/正则/意图/分类),
|
||||
命中后执行4种动作(转人工/转人工+上下文/仅提示/静默转人工)。
|
||||
|
||||
Attributes:
|
||||
id: 规则ID(UUID)
|
||||
rule_name: 规则名称(唯一)
|
||||
rule_description: 规则描述
|
||||
priority: 优先级(P0/P1/P2/P3)
|
||||
match_type: 匹配方式(keyword/regex/intent/category)
|
||||
match_condition: 匹配条件(关键词列表/正则表达式/意图ID列表/分类名称列表)
|
||||
match_scope: 匹配范围(JSON数组,如 ["ai_auto_reply"])
|
||||
action_type: 命中后动作类型
|
||||
transfer_message: 转人工提示语
|
||||
status: 状态(enabled/disabled)
|
||||
hit_count: 命中次数
|
||||
created_by: 创建人ID
|
||||
created_at: 创建时间
|
||||
updated_at: 更新时间
|
||||
"""
|
||||
|
||||
__tablename__ = "exclusion_rules"
|
||||
|
||||
# 主键
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
primary_key=True,
|
||||
default=lambda: str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
# 规则基本信息
|
||||
rule_name: Mapped[str] = mapped_column(
|
||||
String(200),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
comment="规则名称(唯一)",
|
||||
)
|
||||
rule_description: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="规则描述",
|
||||
)
|
||||
priority: Mapped[str] = mapped_column(
|
||||
String(5),
|
||||
nullable=False,
|
||||
default="P2",
|
||||
comment="优先级:P0/P1/P2/P3",
|
||||
)
|
||||
|
||||
# 匹配配置
|
||||
match_type: Mapped[str] = mapped_column(
|
||||
String(20),
|
||||
nullable=False,
|
||||
comment="匹配方式:keyword/regex/intent/category",
|
||||
)
|
||||
match_condition: Mapped[str] = mapped_column(
|
||||
Text,
|
||||
nullable=False,
|
||||
comment="匹配条件:关键词列表/正则/意图ID列表/分类名称列表",
|
||||
)
|
||||
match_scope: Mapped[List[str]] = mapped_column(
|
||||
JSON,
|
||||
nullable=False,
|
||||
default=lambda: ["ai_auto_reply"],
|
||||
comment="匹配范围",
|
||||
)
|
||||
|
||||
# 命中后动作
|
||||
action_type: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
default="transfer_human",
|
||||
comment="命中后动作:transfer_human/transfer_human_with_context/prompt_transfer/silent_transfer",
|
||||
)
|
||||
transfer_message: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="转人工提示语",
|
||||
)
|
||||
|
||||
# 状态
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(10),
|
||||
nullable=False,
|
||||
default="enabled",
|
||||
comment="状态:enabled/disabled",
|
||||
)
|
||||
hit_count: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
default=0,
|
||||
comment="命中次数",
|
||||
)
|
||||
|
||||
# 审计
|
||||
created_by: Mapped[str] = mapped_column(
|
||||
String(100),
|
||||
nullable=False,
|
||||
comment="创建人ID",
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=datetime.now,
|
||||
comment="创建时间",
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=datetime.now,
|
||||
onupdate=datetime.now,
|
||||
comment="更新时间",
|
||||
)
|
||||
|
||||
# 索引
|
||||
__table_args__ = (
|
||||
Index("idx_exclusion_rules_status", "status"),
|
||||
Index("idx_exclusion_rules_priority", "priority"),
|
||||
Index("idx_exclusion_rules_match_type", "match_type"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""排除规则对象的字符串表示。"""
|
||||
return (
|
||||
f"<ExclusionRule(id={self.id}, name={self.rule_name}, "
|
||||
f"priority={self.priority}, status={self.status})>"
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 会议室操作指南模型
|
||||
# =============================================================================
|
||||
# 说明:存储会议室设备操作指南数据
|
||||
# 终端大屏展示简要步骤 + 二维码指向详细文档
|
||||
# =============================================================================
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class MeetingroomGuide(Base):
|
||||
"""会议室操作指南模型。
|
||||
|
||||
按设备类型分类,每条指南包含终端大屏展示的简要说明
|
||||
和二维码指向的详细文档URL。
|
||||
|
||||
Attributes:
|
||||
id: 自增主键
|
||||
category: 设备类型(projector/video_conf/aircon/phone/other)
|
||||
title: 指南标题(如"投影仪使用指南")
|
||||
brief: 简要操作步骤(终端大屏展示,支持多行文本)
|
||||
detail_url: 详细文档URL(二维码指向的链接)
|
||||
icon: 图标emoji(如"📽️")
|
||||
sort_order: 排序序号(越小越靠前)
|
||||
is_active: 是否启用
|
||||
created_at: 创建时间
|
||||
updated_at: 更新时间
|
||||
"""
|
||||
|
||||
__tablename__ = "meetingroom_guide"
|
||||
|
||||
# 自增主键
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
# 设备类型分类
|
||||
category: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="设备类型")
|
||||
# 指南标题
|
||||
title: Mapped[str] = mapped_column(String(100), nullable=False, comment="指南标题")
|
||||
# 简要操作步骤(终端大屏展示)
|
||||
brief: Mapped[str] = mapped_column(Text, nullable=False, default="", comment="简要操作步骤")
|
||||
# 详细文档URL(二维码指向)
|
||||
detail_url: Mapped[str] = mapped_column(String(500), nullable=False, default="", comment="详细文档URL")
|
||||
# 图标emoji
|
||||
icon: Mapped[str] = mapped_column(String(50), nullable=False, default="📋", comment="图标emoji")
|
||||
# 排序序号
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="排序序号")
|
||||
# 是否启用
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, comment="是否启用")
|
||||
# 创建时间
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
|
||||
# 更新时间
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<MeetingroomGuide(id={self.id}, category='{self.category}', title='{self.title}')>"
|
||||
@@ -0,0 +1,69 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 会议室报修记录模型
|
||||
# =============================================================================
|
||||
# 说明:记录员工通过小鱼终端提交的会议室设备报修
|
||||
# 报修提交后自动创建IT工单会话(Conversation),关联conversation_id
|
||||
# 同时通过企微消息通知IT管理员
|
||||
# =============================================================================
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class MeetingroomRepair(Base):
|
||||
"""会议室报修记录模型。
|
||||
|
||||
员工在终端上发起报修后,记录报修信息并关联创建的IT工单会话。
|
||||
报修状态跟随工单会话状态流转。
|
||||
|
||||
Attributes:
|
||||
id: 自增主键
|
||||
terminal_sn: 终端序列号
|
||||
meetingroom_id: 企微会议室ID
|
||||
meetingroom_name: 会议室名称(冗余)
|
||||
device_type: 故障设备类型(projector/video_conf/aircon/desk_chair/network/other)
|
||||
fault_description: 故障描述
|
||||
reporter_name: 报修人姓名(可能匿名)
|
||||
reporter_userid: 报修人企微userid(可能为空)
|
||||
conversation_id: 关联的IT工单会话ID
|
||||
status: 报修状态(0=待处理 1=处理中 2=已解决 3=已关闭)
|
||||
created_at: 创建时间
|
||||
updated_at: 更新时间
|
||||
"""
|
||||
|
||||
__tablename__ = "meetingroom_repair"
|
||||
|
||||
# 自增主键
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
# 终端序列号
|
||||
terminal_sn: Mapped[str] = mapped_column(String(64), index=True, nullable=False, comment="终端序列号")
|
||||
# 企微会议室ID
|
||||
meetingroom_id: Mapped[int] = mapped_column(Integer, index=True, nullable=False, comment="企微会议室ID")
|
||||
# 会议室名称(冗余,便于报修列表展示)
|
||||
meetingroom_name: Mapped[str] = mapped_column(String(100), nullable=False, default="", comment="会议室名称")
|
||||
# 故障设备类型
|
||||
device_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="故障设备类型")
|
||||
# 故障描述
|
||||
fault_description: Mapped[str] = mapped_column(Text, nullable=False, comment="故障描述")
|
||||
# 报修人姓名(可能匿名)
|
||||
reporter_name: Mapped[str] = mapped_column(String(100), nullable=False, default="匿名", comment="报修人姓名")
|
||||
# 报修人企微userid(可能为空)
|
||||
reporter_userid: Mapped[str] = mapped_column(String(64), nullable=False, default="", comment="报修人企微userid")
|
||||
# 关联的IT工单会话ID
|
||||
conversation_id: Mapped[str] = mapped_column(String(36), index=True, nullable=False, comment="关联IT工单会话ID")
|
||||
# 报修状态
|
||||
status: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="报修状态: 0=待处理 1=处理中 2=已解决 3=已关闭")
|
||||
# 创建时间
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
|
||||
# 更新时间
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<MeetingroomRepair(id={self.id}, room='{self.meetingroom_name}', "
|
||||
f"device='{self.device_type}', status={self.status})>"
|
||||
)
|
||||
@@ -0,0 +1,221 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 答题与积分模型
|
||||
# =============================================================================
|
||||
# 说明:包含3张表,支撑排队等待期间的答题+积分系统:
|
||||
# 1. quiz_questions: IT知识题库(7类×10题=70题起步)
|
||||
# 2. quiz_answers: 答题记录(每次答题一条记录)
|
||||
# 3. employee_points: 员工积分账户(跨会话累积,5级等级体系)
|
||||
#
|
||||
# 答题双模式:
|
||||
# 模式A(info_locked=false)— 诊断题:与当前问题相关的选择题,答案附加到会话上下文
|
||||
# 模式B(info_locked=true) — IT知识题:纯教育性质,提升IT素养
|
||||
#
|
||||
# 插队规则:
|
||||
# queue_priority = min(quiz_answered_count // 3, 2) # 每答3题前移1位,上限2位
|
||||
# 积分规则:
|
||||
# 答对 +10分,答错不扣分,跨会话累积
|
||||
# 0-99 IT小白 → 100-299 IT入门 → 300-599 IT达人 → 600-999 IT专家 → 1000+ IT大师
|
||||
# =============================================================================
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Index, Integer, JSON, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class QuizQuestion(Base):
|
||||
"""IT知识题库 — 排队等待期间向员工推送的选择题。
|
||||
|
||||
分为两类(通过 type 字段区分):
|
||||
- knowledge: IT知识题(模式B,info_locked=true时推送)
|
||||
- diagnostic: 诊断题(模式A,info_locked=false时推送,答案附加到会话上下文)
|
||||
|
||||
诊断题按 problem_category 组织,每类3-5题。
|
||||
知识题按 category 组织,每类10题。
|
||||
|
||||
Attributes:
|
||||
id: 题目唯一标识(UUID)
|
||||
type: 题目类型(knowledge=IT知识题 / diagnostic=诊断题)
|
||||
category: 题目类别(network/vpn/email/system/printer/security/office)
|
||||
problem_category: 诊断题对应的问题类别(仅diagnostic类型有效,如"vpn_disconnect")
|
||||
difficulty: 难度(easy/medium/hard)
|
||||
question: 题目文本
|
||||
options: 选项数组(JSON,["选项A", "选项B", "选项C", "选项D"])
|
||||
correct_index: 正确答案索引(0-3)
|
||||
explanation: 答案解析
|
||||
is_active: 是否启用
|
||||
created_at: 创建时间
|
||||
"""
|
||||
|
||||
__tablename__ = "quiz_questions"
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
type: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="knowledge",
|
||||
comment="题目类型: knowledge(IT知识题) / diagnostic(诊断题)"
|
||||
)
|
||||
category: Mapped[str] = mapped_column(
|
||||
String(50), nullable=False,
|
||||
comment="题目类别: network/vpn/email/system/printer/security/office"
|
||||
)
|
||||
problem_category: Mapped[Optional[str]] = mapped_column(
|
||||
String(100), nullable=True,
|
||||
comment="诊断题对应的问题类别(仅diagnostic类型有效)"
|
||||
)
|
||||
difficulty: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="medium",
|
||||
comment="难度: easy/medium/hard"
|
||||
)
|
||||
question: Mapped[str] = mapped_column(
|
||||
Text, nullable=False, comment="题目文本"
|
||||
)
|
||||
options: Mapped[list] = mapped_column(
|
||||
JSON, nullable=False, comment="选项数组: ['选项A', '选项B', ...]"
|
||||
)
|
||||
correct_index: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, comment="正确答案索引(0-based)"
|
||||
)
|
||||
explanation: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True, comment="答案解析"
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True, comment="是否启用"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now,
|
||||
comment="创建时间"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_quiz_q_type", "type"),
|
||||
Index("idx_quiz_q_category", "category"),
|
||||
Index("idx_quiz_q_active", "is_active"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<QuizQuestion(id={self.id}, type={self.type}, category={self.category})>"
|
||||
|
||||
|
||||
class QuizAnswer(Base):
|
||||
"""答题记录 — 每次答题一条记录。
|
||||
|
||||
Attributes:
|
||||
id: 记录ID
|
||||
employee_id: 员工ID
|
||||
conversation_id: 关联会话ID(可空,非排队时答题无会话)
|
||||
question_id: 题目ID
|
||||
selected_index: 员工选择的答案索引
|
||||
is_correct: 是否答对
|
||||
points_earned: 获得积分(答对=10,答错=0)
|
||||
created_at: 答题时间
|
||||
"""
|
||||
|
||||
__tablename__ = "quiz_answers"
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
employee_id: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, comment="员工ID"
|
||||
)
|
||||
conversation_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(36), nullable=True, comment="关联会话ID"
|
||||
)
|
||||
question_id: Mapped[str] = mapped_column(
|
||||
String(36), nullable=False, comment="题目ID"
|
||||
)
|
||||
selected_index: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, comment="选择的答案索引"
|
||||
)
|
||||
is_correct: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, comment="是否答对"
|
||||
)
|
||||
points_earned: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, comment="获得积分"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now,
|
||||
comment="答题时间"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_quiz_a_employee", "employee_id"),
|
||||
Index("idx_quiz_a_conversation", "conversation_id"),
|
||||
Index("idx_quiz_a_created", "created_at"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<QuizAnswer(id={self.id}, employee={self.employee_id}, correct={self.is_correct})>"
|
||||
|
||||
|
||||
class EmployeePoints(Base):
|
||||
"""员工积分账户 — 跨会话累积,5级等级体系。
|
||||
|
||||
积分规则:答对一题 +10分,答错不扣分。
|
||||
等级体系:
|
||||
0-99 IT小白 (灰色)
|
||||
100-299 IT入门 (蓝色)
|
||||
300-599 IT达人 (绿色)
|
||||
600-999 IT专家 (琥珀)
|
||||
1000+ IT大师 (珊瑚红)
|
||||
|
||||
Attributes:
|
||||
employee_id: 员工ID(主键)
|
||||
total_points: 累计积分
|
||||
answered_count: 答题总数
|
||||
correct_count: 答对总数
|
||||
level: 当前等级名称
|
||||
updated_at: 最后更新时间
|
||||
"""
|
||||
|
||||
__tablename__ = "employee_points"
|
||||
|
||||
employee_id: Mapped[str] = mapped_column(
|
||||
String(64), primary_key=True, comment="员工ID"
|
||||
)
|
||||
total_points: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, comment="累计积分"
|
||||
)
|
||||
answered_count: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, comment="答题总数"
|
||||
)
|
||||
correct_count: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, comment="答对总数"
|
||||
)
|
||||
level: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="IT小白", comment="当前等级"
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now,
|
||||
onupdate=datetime.now, comment="最后更新时间"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<EmployeePoints(employee={self.employee_id}, points={self.total_points}, level={self.level})>"
|
||||
|
||||
@staticmethod
|
||||
def calculate_level(points: int) -> str:
|
||||
"""根据积分计算等级名称。
|
||||
|
||||
Args:
|
||||
points: 当前累计积分
|
||||
|
||||
Returns:
|
||||
等级名称字符串
|
||||
"""
|
||||
if points >= 1000:
|
||||
return "IT大师"
|
||||
elif points >= 600:
|
||||
return "IT专家"
|
||||
elif points >= 300:
|
||||
return "IT达人"
|
||||
elif points >= 100:
|
||||
return "IT入门"
|
||||
else:
|
||||
return "IT小白"
|
||||
@@ -0,0 +1,229 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 分诊会话模型
|
||||
# =============================================================================
|
||||
# 说明:对应数据库 triage_sessions 表
|
||||
# 存储 AI 分诊的完整会话记录,包括分诊步骤、收集的上下文、路由结果等。
|
||||
# =============================================================================
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import DateTime, Float, Index, Integer, JSON, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class TriageSession(Base):
|
||||
"""分诊会话模型 — 对应 triage_sessions 表。
|
||||
|
||||
存储员工发起的 AI 分诊全流程数据,从发起分诊到最终路由。
|
||||
|
||||
Attributes:
|
||||
id: 分诊会话ID(UUID)
|
||||
conversation_id: 关联的企微会话ID
|
||||
user_id: 员工企微UserID
|
||||
user_name: 员工姓名
|
||||
user_dept: 员工部门
|
||||
user_level: 员工IT技能等级
|
||||
device_info: 设备信息
|
||||
request_title: 问题标题
|
||||
request_content: 问题原文
|
||||
source: 来源渠道(wecom_h5 / api / other)
|
||||
problem_type: AI识别的问题类型(硬件/软件/网络/安全/账号/其他)
|
||||
problem_category: AI识别的问题分类
|
||||
confidence: AI置信度(0.0-1.0)
|
||||
urgency: 紧急度(high/medium/low)
|
||||
suggested_route: AI建议路由(ai_self/human/auto_approval)
|
||||
matched_knowledge: 匹配到的知识条目
|
||||
match_score: 知识匹配分数
|
||||
context_tags: 上下文标签列表(JSON数组)
|
||||
triage_steps: 分诊步骤数据(JSON数组)
|
||||
collected_context: 已收集的上下文列表(JSON数组)
|
||||
status: 分诊状态(pending/triaging/routed/skipped/timeout)
|
||||
route_action: 最终路由动作(ai_self/human/auto_approval/skip)
|
||||
route_note: 路由备注
|
||||
operator_id: 操作坐席ID
|
||||
operated_at: 操作时间
|
||||
created_at: 创建时间
|
||||
updated_at: 更新时间
|
||||
"""
|
||||
|
||||
__tablename__ = "triage_sessions"
|
||||
|
||||
# 主键
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
primary_key=True,
|
||||
default=lambda: str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
# 会话关联
|
||||
conversation_id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
nullable=False,
|
||||
comment="关联的企微会话ID",
|
||||
)
|
||||
|
||||
# 用户信息
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(100),
|
||||
nullable=False,
|
||||
comment="员工企微UserID",
|
||||
)
|
||||
user_name: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
nullable=True,
|
||||
comment="员工姓名",
|
||||
)
|
||||
user_dept: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
nullable=True,
|
||||
comment="员工部门",
|
||||
)
|
||||
user_level: Mapped[Optional[str]] = mapped_column(
|
||||
String(20),
|
||||
nullable=True,
|
||||
comment="员工IT技能等级",
|
||||
)
|
||||
device_info: Mapped[Optional[str]] = mapped_column(
|
||||
String(200),
|
||||
nullable=True,
|
||||
comment="设备信息",
|
||||
)
|
||||
|
||||
# 问题描述
|
||||
request_title: Mapped[str] = mapped_column(
|
||||
String(200),
|
||||
nullable=False,
|
||||
comment="问题标题",
|
||||
)
|
||||
request_content: Mapped[str] = mapped_column(
|
||||
Text,
|
||||
nullable=False,
|
||||
comment="问题原文",
|
||||
)
|
||||
source: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
default="wecom_h5",
|
||||
comment="来源渠道",
|
||||
)
|
||||
|
||||
# AI 分诊分析结果
|
||||
problem_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(50),
|
||||
nullable=True,
|
||||
comment="问题类型:硬件/软件/网络/安全/账号/其他",
|
||||
)
|
||||
problem_category: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
nullable=True,
|
||||
comment="问题分类",
|
||||
)
|
||||
confidence: Mapped[Optional[float]] = mapped_column(
|
||||
Float,
|
||||
nullable=True,
|
||||
comment="AI置信度(0.0-1.0)",
|
||||
)
|
||||
urgency: Mapped[str] = mapped_column(
|
||||
String(20),
|
||||
nullable=False,
|
||||
default="medium",
|
||||
comment="紧急度:high/medium/low",
|
||||
)
|
||||
suggested_route: Mapped[Optional[str]] = mapped_column(
|
||||
String(50),
|
||||
nullable=True,
|
||||
comment="AI建议路由:ai_self/human/auto_approval",
|
||||
)
|
||||
matched_knowledge: Mapped[Optional[str]] = mapped_column(
|
||||
String(500),
|
||||
nullable=True,
|
||||
comment="匹配到的知识条目",
|
||||
)
|
||||
match_score: Mapped[Optional[float]] = mapped_column(
|
||||
Float,
|
||||
nullable=True,
|
||||
comment="知识匹配分数",
|
||||
)
|
||||
context_tags: Mapped[List[str]] = mapped_column(
|
||||
JSON,
|
||||
nullable=False,
|
||||
default=list,
|
||||
comment="上下文标签列表",
|
||||
)
|
||||
|
||||
# 分诊步骤数据
|
||||
triage_steps: Mapped[List[Dict[str, Any]]] = mapped_column(
|
||||
JSON,
|
||||
nullable=False,
|
||||
default=list,
|
||||
comment="分诊步骤数据:[{question, options:[{label, probability}]}]",
|
||||
)
|
||||
collected_context: Mapped[List[str]] = mapped_column(
|
||||
JSON,
|
||||
nullable=False,
|
||||
default=list,
|
||||
comment="已收集的上下文列表",
|
||||
)
|
||||
|
||||
# 状态与路由
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(30),
|
||||
nullable=False,
|
||||
default="pending",
|
||||
comment="分诊状态:pending/triaging/routed/skipped/timeout",
|
||||
)
|
||||
route_action: Mapped[Optional[str]] = mapped_column(
|
||||
String(50),
|
||||
nullable=True,
|
||||
comment="最终路由动作:ai_self/human/auto_approval/skip",
|
||||
)
|
||||
route_note: Mapped[Optional[str]] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="路由备注",
|
||||
)
|
||||
operator_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
nullable=True,
|
||||
comment="操作坐席ID",
|
||||
)
|
||||
operated_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=True,
|
||||
comment="操作时间",
|
||||
)
|
||||
|
||||
# 时间戳
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=datetime.now,
|
||||
comment="创建时间",
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=datetime.now,
|
||||
onupdate=datetime.now,
|
||||
comment="更新时间",
|
||||
)
|
||||
|
||||
# 索引
|
||||
__table_args__ = (
|
||||
Index("idx_triage_status", "status"),
|
||||
Index("idx_triage_urgency", "urgency"),
|
||||
Index("idx_triage_conversation", "conversation_id"),
|
||||
Index("idx_triage_created", "created_at"),
|
||||
Index("idx_triage_user", "user_id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""分诊会话对象的字符串表示。"""
|
||||
return (
|
||||
f"<TriageSession(id={self.id}, user={self.user_id}, "
|
||||
f"status={self.status}, urgency={self.urgency})>"
|
||||
)
|
||||
Reference in New Issue
Block a user