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
@@ -0,0 +1,134 @@
"""add meetingroom repair and guide tables with seed data
Revision ID: 051_add_meetingroom_repair_guide
Revises: 050_add_meetingroom_tables
Create Date: 2026-07-16
新增表:
- meetingroom_guide: 会议室操作指南(终端大屏展示+二维码)
- meetingroom_repair: 会议室报修记录(关联IT工单会话)
同时插入操作指南种子数据
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# Alembic 修订标识符
revision: str = "051_add_meetingroom_repair_guide"
down_revision: Union[str, None] = "050_add_meetingroom_tables"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""升级:创建 meetingroom_guide 和 meetingroom_repair 表,插入种子数据。"""
# 1. 操作指南表
op.create_table(
"meetingroom_guide",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("category", sa.String(50), nullable=False),
sa.Column("title", sa.String(100), nullable=False),
sa.Column("brief", sa.Text(), nullable=False, server_default=""),
sa.Column("detail_url", sa.String(500), nullable=False, server_default=""),
sa.Column("icon", sa.String(50), nullable=False, server_default="📋"),
sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_meetingroom_guide_category", "meetingroom_guide", ["category"])
# 2. 报修记录表
op.create_table(
"meetingroom_repair",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("terminal_sn", sa.String(64), nullable=False),
sa.Column("meetingroom_id", sa.Integer(), nullable=False),
sa.Column("meetingroom_name", sa.String(100), nullable=False, server_default=""),
sa.Column("device_type", sa.String(50), nullable=False),
sa.Column("fault_description", sa.Text(), nullable=False),
sa.Column("reporter_name", sa.String(100), nullable=False, server_default="匿名"),
sa.Column("reporter_userid", sa.String(64), nullable=False, server_default=""),
sa.Column("conversation_id", sa.String(36), nullable=False),
sa.Column("status", sa.Integer(), nullable=False, server_default="0"),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_meetingroom_repair_terminal_sn", "meetingroom_repair", ["terminal_sn"])
op.create_index("ix_meetingroom_repair_meetingroom_id", "meetingroom_repair", ["meetingroom_id"])
op.create_index("ix_meetingroom_repair_conversation_id", "meetingroom_repair", ["conversation_id"])
# 3. 插入操作指南种子数据
guide_table = sa.table(
"meetingroom_guide",
sa.column("category", sa.String),
sa.column("title", sa.String),
sa.column("brief", sa.Text),
sa.column("detail_url", sa.String),
sa.column("icon", sa.String),
sa.column("sort_order", sa.Integer),
sa.column("is_active", sa.Boolean),
)
op.bulk_insert(guide_table, [
{
"category": "projector",
"title": "投影仪使用指南",
"brief": "1. 按遥控器电源键开机\n2. 选择信号源(HDMI1/2\n3. 调整焦距和梯形校正\n4. 使用完毕按电源键关机\n5. 等待散热完成后断电",
"detail_url": "",
"icon": "📽️",
"sort_order": 1,
"is_active": True,
},
{
"category": "video_conf",
"title": "视频会议使用指南",
"brief": "1. 在终端主页选择"视频会议"\n2. 输入会议号或选择预约会议\n3. 点击"加入会议"\n4. 共享屏幕:点击"共享"按钮\n5. 会议结束:点击"挂断"",
"detail_url": "",
"icon": "📹",
"sort_order": 2,
"is_active": True,
},
{
"category": "aircon",
"title": "空调控制指南",
"brief": "1. 遥控器对准空调内机\n2. 按电源键开关机\n3. 模式键切换:制冷/制热/除湿\n4. 温度调节:↑/↓ 键\n5. 风速调节:低/中/高/自动",
"detail_url": "",
"icon": "❄️",
"sort_order": 3,
"is_active": True,
},
{
"category": "phone",
"title": "会议室电话使用指南",
"brief": "1. 摘机或按免提键\n2. 拨打号码后按#键确认\n3. 转接:按转接键 → 输入分机号\n4. 音量调节:侧面音量键\n5. 挂机结束通话",
"detail_url": "",
"icon": "📞",
"sort_order": 4,
"is_active": True,
},
{
"category": "other",
"title": "其他设备指南",
"brief": "如需查看其他设备(电子白板、音响、灯光等)的使用说明,请扫描右侧二维码查看完整设备手册。",
"detail_url": "",
"icon": "📋",
"sort_order": 5,
"is_active": True,
},
])
def downgrade() -> None:
"""回滚:删除 meetingroom_repair 和 meetingroom_guide 表。"""
op.drop_index("ix_meetingroom_repair_conversation_id", table_name="meetingroom_repair")
op.drop_index("ix_meetingroom_repair_meetingroom_id", table_name="meetingroom_repair")
op.drop_index("ix_meetingroom_repair_terminal_sn", table_name="meetingroom_repair")
op.drop_table("meetingroom_repair")
op.drop_index("ix_meetingroom_guide_category", table_name="meetingroom_guide")
op.drop_table("meetingroom_guide")
@@ -0,0 +1,107 @@
"""triage_sessions + exclusion_rules + exclusion_logs
Revision ID: 051_triage_exclusion
Revises: 050_add_meetingroom_tables
Create Date: 2026-07-12
新增表:
- triage_sessions: AI 分诊会话记录
- exclusion_rules: 代答排除规则
- exclusion_logs: 排除命中日志
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "051_triage_exclusion"
down_revision: Union[str, None] = "050_add_meetingroom_tables"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 1. triage_sessions 表 — AI 分诊会话
op.create_table(
"triage_sessions",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("conversation_id", sa.String(36), nullable=False),
sa.Column("user_id", sa.String(100), nullable=False),
sa.Column("user_name", sa.String(100)),
sa.Column("user_dept", sa.String(100)),
sa.Column("user_level", sa.String(20)),
sa.Column("device_info", sa.String(200)),
sa.Column("request_title", sa.String(200), nullable=False),
sa.Column("request_content", sa.Text, nullable=False),
sa.Column("source", sa.String(50), server_default="wecom_h5"),
sa.Column("problem_type", sa.String(50)),
sa.Column("problem_category", sa.String(100)),
sa.Column("confidence", sa.Float),
sa.Column("urgency", sa.String(20), server_default="medium"),
sa.Column("suggested_route", sa.String(50)),
sa.Column("matched_knowledge", sa.String(500)),
sa.Column("match_score", sa.Float),
sa.Column("context_tags", sa.JSON, server_default="[]"),
sa.Column("triage_steps", sa.JSON, server_default="[]"),
sa.Column("collected_context", sa.JSON, server_default="[]"),
sa.Column("status", sa.String(30), server_default="pending"),
sa.Column("route_action", sa.String(50)),
sa.Column("route_note", sa.Text),
sa.Column("operator_id", sa.String(100)),
sa.Column("operated_at", sa.DateTime),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
)
op.create_index("idx_triage_status", "triage_sessions", ["status"])
op.create_index("idx_triage_urgency", "triage_sessions", ["urgency"])
op.create_index("idx_triage_conversation", "triage_sessions", ["conversation_id"])
op.create_index("idx_triage_created", "triage_sessions", ["created_at"])
op.create_index("idx_triage_user", "triage_sessions", ["user_id"])
# 2. exclusion_rules 表 — 代答排除规则
op.create_table(
"exclusion_rules",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("rule_name", sa.String(200), nullable=False, unique=True),
sa.Column("rule_description", sa.Text),
sa.Column("priority", sa.String(5), nullable=False, server_default="P2"),
sa.Column("match_type", sa.String(20), nullable=False),
sa.Column("match_condition", sa.Text, nullable=False),
sa.Column("match_scope", sa.JSON, nullable=False, server_default='["ai_auto_reply"]'),
sa.Column("action_type", sa.String(50), nullable=False, server_default="transfer_human"),
sa.Column("transfer_message", sa.Text),
sa.Column("status", sa.String(10), nullable=False, server_default="enabled"),
sa.Column("hit_count", sa.Integer, server_default="0"),
sa.Column("created_by", sa.String(100), nullable=False),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
)
op.create_index("idx_exclusion_rules_status", "exclusion_rules", ["status"])
op.create_index("idx_exclusion_rules_priority", "exclusion_rules", ["priority"])
op.create_index("idx_exclusion_rules_match_type", "exclusion_rules", ["match_type"])
# 3. exclusion_logs 表 — 排除命中日志
op.create_table(
"exclusion_logs",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("rule_id", sa.String(36), nullable=False),
sa.Column("rule_name", sa.String(200)),
sa.Column("conversation_id", sa.String(36)),
sa.Column("user_id", sa.String(100)),
sa.Column("message_content", sa.Text),
sa.Column("match_type", sa.String(20)),
sa.Column("matched_detail", sa.Text),
sa.Column("action_type", sa.String(50)),
sa.Column("action_result", sa.String(50), server_default="success"),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
)
op.create_index("idx_exclusion_logs_rule", "exclusion_logs", ["rule_id"])
op.create_index("idx_exclusion_logs_created", "exclusion_logs", ["created_at"])
op.create_index("idx_exclusion_logs_conversation", "exclusion_logs", ["conversation_id"])
def downgrade() -> None:
op.drop_table("exclusion_logs")
op.drop_table("exclusion_rules")
op.drop_table("triage_sessions")
@@ -0,0 +1,231 @@
"""diagnostic + queue + quiz + closing mechanism
Revision ID: 052_diag_queue_quiz
Revises: 051_triage_exclusion
Create Date: 2026-07-12
新增表(6张):
- diagnostic_templates: 原子化诊断检查项模板库
- diagnostic_dispatches: 诊断下发记录(dispatched→executed→analyzed→resolved
- diagnostic_reports: 诊断报告存储 + AI分析结论
- quiz_questions: IT知识题库 + 诊断题
- quiz_answers: 答题记录
- employee_points: 员工积分账户(跨会话累积,5级等级)
Conversation表新增字段(6个):
- queue_priority: 答题插队优先级(min(答题数//3, 2),上限2
- info_locked: 信息是否锁定(Dify步骤完成+有效率≥70%
- resolved_by: 关闭方(employee/agent/ai/system_timeout
- resolved_method: 关闭方式(ai_self/agent_confirm/employee_initiative/auto_timeout
- resolve_summary: 结单摘要(问题类型+根因+解决方式)
- reference_conversation_id: 重开时关联的原会话ID
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "052_diag_queue_quiz"
down_revision: Union[str, None] = "051_triage_exclusion"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# =====================================================================
# 1. Conversation表新增字段
# =====================================================================
op.add_column("conversations", sa.Column(
"queue_priority", sa.Integer, nullable=False, server_default="0",
comment="答题插队优先级(上限2)"
))
op.add_column("conversations", sa.Column(
"info_locked", sa.Boolean, nullable=False, server_default=sa.text("false"),
comment="信息是否锁定"
))
op.add_column("conversations", sa.Column(
"resolved_by", sa.String(20), nullable=True,
comment="关闭方: employee/agent/ai/system_timeout"
))
op.add_column("conversations", sa.Column(
"resolved_method", sa.String(30), nullable=True,
comment="关闭方式: ai_self/agent_confirm/employee_initiative/auto_timeout"
))
op.add_column("conversations", sa.Column(
"resolve_summary", sa.Text, nullable=True,
comment="结单摘要"
))
op.add_column("conversations", sa.Column(
"reference_conversation_id", sa.String(36), nullable=True,
comment="重开时关联的原会话ID"
))
# 为排队查询添加索引
op.create_index(
"idx_conversations_queue_priority",
"conversations",
["queue_priority"]
)
op.create_index(
"idx_conversations_info_locked",
"conversations",
["info_locked"]
)
# =====================================================================
# 2. diagnostic_templates — 诊断模板库
# =====================================================================
op.create_table(
"diagnostic_templates",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("category", sa.String(50), nullable=False, comment="问题类别"),
sa.Column("name", sa.String(200), nullable=False, comment="检查项名称"),
sa.Column("check_type", sa.String(20), nullable=False, server_default="api",
comment="检查类型: api/script"),
sa.Column("api_source", sa.String(50), nullable=True, comment="API来源: huorong/lianruan"),
sa.Column("api_method", sa.String(100), nullable=True, comment="调用的API方法名"),
sa.Column("script_template", sa.Text, nullable=True, comment="脚本模板"),
sa.Column("fix_template", sa.Text, nullable=True, comment="修复脚本模板"),
sa.Column("fix_risk_level", sa.String(20), nullable=False, server_default="medium",
comment="修复风险等级: low/medium/high"),
sa.Column("target_condition", sa.String(200), nullable=True, comment="触发条件"),
sa.Column("description", sa.Text, nullable=True, comment="检查项描述"),
sa.Column("is_active", sa.Boolean, nullable=False, server_default=sa.text("true"),
comment="是否启用"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("idx_diag_tpl_category", "diagnostic_templates", ["category"])
op.create_index("idx_diag_tpl_type", "diagnostic_templates", ["check_type"])
op.create_index("idx_diag_tpl_active", "diagnostic_templates", ["is_active"])
# =====================================================================
# 3. diagnostic_dispatches — 诊断下发记录
# =====================================================================
op.create_table(
"diagnostic_dispatches",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("conversation_id", sa.String(36), nullable=False, comment="关联会话ID"),
sa.Column("employee_id", sa.String(64), nullable=False, comment="员工ID"),
sa.Column("template_ids", sa.JSON, nullable=False, server_default="[]",
comment="诊断模板ID列表"),
sa.Column("script_content", sa.Text, nullable=True, comment="生成的脚本内容"),
sa.Column("script_hash", sa.String(64), nullable=True, comment="脚本SHA256哈希"),
sa.Column("upload_token", sa.String(128), nullable=True, comment="一次性上传token"),
sa.Column("status", sa.String(20), nullable=False, server_default="dispatched",
comment="状态: dispatched/executed/analyzed/resolved"),
sa.Column("report_id", sa.String(36), nullable=True, comment="关联诊断报告ID"),
sa.Column("fix_dispatched", sa.Boolean, nullable=False, server_default=sa.text("false"),
comment="是否已下发修复包"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True, comment="完成时间"),
)
op.create_index("idx_diag_dispatch_conv", "diagnostic_dispatches", ["conversation_id"])
op.create_index("idx_diag_dispatch_employee", "diagnostic_dispatches", ["employee_id"])
op.create_index("idx_diag_dispatch_status", "diagnostic_dispatches", ["status"])
# =====================================================================
# 4. diagnostic_reports — 诊断报告存储
# =====================================================================
op.create_table(
"diagnostic_reports",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("dispatch_id", sa.String(36), nullable=True, comment="关联下发记录ID"),
sa.Column("conversation_id", sa.String(36), nullable=False, comment="关联会话ID"),
sa.Column("employee_id", sa.String(64), nullable=False, comment="员工ID"),
sa.Column("template_ids", sa.JSON, nullable=False, server_default="[]",
comment="涉及诊断模板ID列表"),
sa.Column("report_data", sa.JSON, nullable=False, server_default="{}",
comment="检查结果JSON"),
sa.Column("ai_analysis", sa.JSON, nullable=True, comment="AI分析结论JSON"),
sa.Column("status", sa.String(20), nullable=False, server_default="pending",
comment="报告状态: pending/analyzed/resolved"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("idx_diag_report_conv", "diagnostic_reports", ["conversation_id"])
op.create_index("idx_diag_report_employee", "diagnostic_reports", ["employee_id"])
op.create_index("idx_diag_report_status", "diagnostic_reports", ["status"])
# =====================================================================
# 5. quiz_questions — IT知识题库
# =====================================================================
op.create_table(
"quiz_questions",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("type", sa.String(20), nullable=False, server_default="knowledge",
comment="题目类型: knowledge/diagnostic"),
sa.Column("category", sa.String(50), nullable=False, comment="题目类别"),
sa.Column("problem_category", sa.String(100), nullable=True,
comment="诊断题对应的问题类别"),
sa.Column("difficulty", sa.String(20), nullable=False, server_default="medium",
comment="难度: easy/medium/hard"),
sa.Column("question", sa.Text, nullable=False, comment="题目文本"),
sa.Column("options", sa.JSON, nullable=False, comment="选项数组"),
sa.Column("correct_index", sa.Integer, nullable=False, comment="正确答案索引"),
sa.Column("explanation", sa.Text, nullable=True, comment="答案解析"),
sa.Column("is_active", sa.Boolean, nullable=False, server_default=sa.text("true"),
comment="是否启用"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("idx_quiz_q_type", "quiz_questions", ["type"])
op.create_index("idx_quiz_q_category", "quiz_questions", ["category"])
op.create_index("idx_quiz_q_active", "quiz_questions", ["is_active"])
# =====================================================================
# 6. quiz_answers — 答题记录
# =====================================================================
op.create_table(
"quiz_answers",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("employee_id", sa.String(64), nullable=False, comment="员工ID"),
sa.Column("conversation_id", sa.String(36), nullable=True, comment="关联会话ID"),
sa.Column("question_id", sa.String(36), nullable=False, comment="题目ID"),
sa.Column("selected_index", sa.Integer, nullable=False, comment="选择的答案索引"),
sa.Column("is_correct", sa.Boolean, nullable=False, comment="是否答对"),
sa.Column("points_earned", sa.Integer, nullable=False, server_default="0",
comment="获得积分"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("idx_quiz_a_employee", "quiz_answers", ["employee_id"])
op.create_index("idx_quiz_a_conversation", "quiz_answers", ["conversation_id"])
op.create_index("idx_quiz_a_created", "quiz_answers", ["created_at"])
# =====================================================================
# 7. employee_points — 员工积分账户
# =====================================================================
op.create_table(
"employee_points",
sa.Column("employee_id", sa.String(64), primary_key=True),
sa.Column("total_points", sa.Integer, nullable=False, server_default="0",
comment="累计积分"),
sa.Column("answered_count", sa.Integer, nullable=False, server_default="0",
comment="答题总数"),
sa.Column("correct_count", sa.Integer, nullable=False, server_default="0",
comment="答对总数"),
sa.Column("level", sa.String(20), nullable=False, server_default="IT小白",
comment="当前等级"),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
def downgrade() -> None:
# 删除新增表(逆序)
op.drop_table("employee_points")
op.drop_table("quiz_answers")
op.drop_table("quiz_questions")
op.drop_table("diagnostic_reports")
op.drop_table("diagnostic_dispatches")
op.drop_table("diagnostic_templates")
# 删除Conversation表新增索引
op.drop_index("idx_conversations_info_locked", "conversations")
op.drop_index("idx_conversations_queue_priority", "conversations")
# 删除Conversation表新增字段
op.drop_column("conversations", "reference_conversation_id")
op.drop_column("conversations", "resolve_summary")
op.drop_column("conversations", "resolved_method")
op.drop_column("conversations", "resolved_by")
op.drop_column("conversations", "info_locked")
op.drop_column("conversations", "queue_priority")