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
+345
View File
@@ -0,0 +1,345 @@
<!--
=============================================================================
企微IT智能服务台 终端设备报修页面
=============================================================================
说明小鱼终端大屏设备报修页面
- 设备类型选择投影仪/视频会议设备/空调/桌椅/网络/其他
- 故障描述文本输入支持快捷选项
- 匿名报修未登录时也可提交
- 提交后创建 IT 工单 + 通知管理员
- 成功/失败反馈自动返回状态页
- 1920×1080 横屏深色主题适配
=============================================================================
-->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTerminalStore } from '@/stores/terminal'
import { submitRepair } from '@/api/meetingroom'
import { useTerminal } from '@/composables/useTerminal'
import type { DeviceTypeOption } from '@/types/meetingroom'
// ==========================================================================
// 路由 & Store & 终端
// ==========================================================================
const route = useRoute()
const router = useRouter()
const store = useTerminalStore()
const { close: closePage } = useTerminal()
// ==========================================================================
// 设备类型选项
// ==========================================================================
/** 设备类型列表(与后端 repair_service.py 的 DEVICE_TYPE_LABELS 对应) */
const deviceTypeOptions: DeviceTypeOption[] = [
{ value: 'projector', label: '投影仪', icon: '📽️' },
{ value: 'video_conf', label: '视频会议设备', icon: '📹' },
{ value: 'aircon', label: '空调', icon: '❄️' },
{ value: 'desk_chair', label: '桌椅', icon: '🪑' },
{ value: 'network', label: '网络', icon: '🌐' },
{ value: 'other', label: '其他设备', icon: '🔧' },
]
// ==========================================================================
// 表单状态
// ==========================================================================
/** 选中的设备类型 */
const selectedDeviceType = ref<string>('')
/** 故障描述 */
const faultDescription = ref('')
/** 提交中 */
const submitting = ref(false)
/** 提交结果 */
const submitResult = ref<{ success: boolean; repairId?: number; message: string } | null>(null)
/** 快捷故障描述选项 */
const quickFaultOptions = [
'无法开机',
'画面模糊',
'声音异常',
'连接失败',
'设备老化',
'其他问题',
]
// ==========================================================================
// 计算属性
// ==========================================================================
/** 终端SN */
const terminalSn = computed(() => route.params.sn as string || store.terminalSn)
/** 会议室信息 */
const meetingroomId = computed(() => store.meetingroomInfo?.meetingroom_id ?? null)
const meetingroomName = computed(() => store.meetingroomName)
/** 是否可提交 */
const canSubmit = computed(() => {
return selectedDeviceType.value !== '' && faultDescription.value.trim().length > 0 && !submitting.value
})
/** 选中设备类型的显示信息 */
const selectedDeviceInfo = computed(() => {
return deviceTypeOptions.find(d => d.value === selectedDeviceType.value)
})
// ==========================================================================
// 方法
// ==========================================================================
/** 选择设备类型 */
function selectDeviceType(value: string) {
selectedDeviceType.value = value
}
/** 快速填充故障描述 */
function quickFillFault(text: string) {
// 如果已有内容,追加而不是覆盖
if (faultDescription.value && !faultDescription.value.includes(text)) {
faultDescription.value = `${faultDescription.value}${text}`
} else {
faultDescription.value = text
}
}
/** 提交报修 */
async function handleSubmit() {
if (!canSubmit.value) return
if (!meetingroomId.value) {
submitResult.value = { success: false, message: '会议室信息缺失,无法提交报修' }
return
}
submitting.value = true
submitResult.value = null
try {
const result = await submitRepair({
terminal_sn: terminalSn.value,
meetingroom_id: meetingroomId.value,
meetingroom_name: meetingroomName.value,
device_type: selectedDeviceType.value,
fault_description: faultDescription.value.trim(),
// 如果已登录,附带用户信息;未登录则匿名提交
reporter_name: store.isLoggedIn ? store.loginUser?.name : undefined,
reporter_userid: store.isLoggedIn ? store.loginUser?.userid : undefined,
})
submitResult.value = {
success: true,
repairId: result.repair_id,
message: '报修已提交,IT人员将尽快处理',
}
// 3秒后自动返回状态页
setTimeout(() => {
goBack()
}, 3000)
} catch (e: any) {
submitResult.value = {
success: false,
message: e.message || '提交失败,请稍后重试或联系IT管理员',
}
} finally {
submitting.value = false
}
}
/** 返回状态页 */
function goBack() {
const sn = route.params.sn as string
router.replace(`/${sn}`)
}
/** 关闭页面(小鱼终端返回主页) */
function handleClose() {
closePage()
}
// ==========================================================================
// 生命周期
// ==========================================================================
onMounted(() => {
// 如果终端绑定信息未加载,尝试加载
if (!store.meetingroomInfo && terminalSn.value) {
store.loadBinding(terminalSn.value)
}
})
</script>
<template>
<div class="w-full h-screen flex items-center justify-center bg-bg-primary p-4 md:p-8">
<div class="bg-bg-card rounded-2xl border border-border-subtle shadow-2xl w-full max-w-[900px] max-h-[92vh] overflow-y-auto animate-slide-up">
<!-- ================================================================ -->
<!-- 标题栏 -->
<!-- ================================================================ -->
<div class="flex items-center justify-between p-6 border-b border-border-subtle">
<div>
<h2 class="text-2xl font-bold text-text-primary">🔧 设备报修</h2>
<p class="text-sm text-text-secondary mt-1">
{{ meetingroomName }}
<span v-if="store.location"> · {{ store.location }}</span>
</p>
</div>
<div class="flex items-center gap-3">
<button
class="px-4 py-2 rounded-lg text-base text-text-secondary border border-border-subtle hover:text-text-primary transition-colors"
@click="handleClose"
>
关闭
</button>
<button
class="text-text-secondary hover:text-text-primary text-2xl"
@click="goBack"
>
</button>
</div>
</div>
<!-- ================================================================ -->
<!-- 提交成功状态 -->
<!-- ================================================================ -->
<div v-if="submitResult?.success" class="p-12 text-center">
<div class="text-6xl mb-4"></div>
<h3 class="text-3xl font-bold text-status-free mb-3">报修已提交</h3>
<p class="text-lg text-text-secondary mb-2">
{{ submitResult.message }}
</p>
<p v-if="submitResult.repairId" class="text-base text-text-muted">
报修编号: #{{ submitResult.repairId }}
</p>
<p class="text-sm text-text-muted mt-4">
3 秒后自动返回...
</p>
</div>
<!-- ================================================================ -->
<!-- 报修表单 -->
<!-- ================================================================ -->
<div v-else class="p-6 space-y-6">
<!-- 报修人信息 -->
<div class="flex items-center gap-4">
<span class="text-lg text-text-secondary min-w-[100px]">报修人</span>
<div class="px-4 py-2 bg-bg-input rounded-lg text-lg text-text-primary">
<span v-if="store.isLoggedIn">
{{ store.loginUser?.name || store.loginUser?.userid }}
</span>
<span v-else class="text-text-muted">匿名报修终端提交</span>
</div>
</div>
<!-- 设备类型选择 -->
<div>
<div class="flex items-center gap-4 mb-3">
<span class="text-lg text-text-secondary min-w-[100px]">设备类型</span>
<span class="text-sm text-text-muted">请选择故障设备</span>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 gap-3 md:gap-4 ml-0 md:ml-[116px]">
<button
v-for="opt in deviceTypeOptions"
:key="opt.value"
class="flex items-center gap-3 px-5 py-4 rounded-xl border-2 transition-all"
:class="selectedDeviceType === opt.value
? 'border-status-free bg-status-free/10 text-text-primary'
: 'border-border-subtle bg-bg-input text-text-secondary hover:text-text-primary hover:border-border-subtle'"
@click="selectDeviceType(opt.value)"
>
<span class="text-3xl">{{ opt.icon }}</span>
<span class="text-lg font-medium">{{ opt.label }}</span>
</button>
</div>
</div>
<!-- 故障描述 -->
<div>
<div class="flex items-center gap-4 mb-3">
<span class="text-lg text-text-secondary min-w-[100px]">故障描述</span>
<span class="text-sm text-text-muted">请描述故障现象</span>
</div>
<div class="ml-0 md:ml-[116px]">
<textarea
v-model="faultDescription"
placeholder="请输入故障描述,例如:投影仪无法开机,指示灯不亮..."
rows="4"
maxlength="500"
class="w-full px-4 py-3 bg-bg-input rounded-xl text-lg text-text-primary border border-border-subtle focus:border-status-free focus:outline-none resize-none"
/>
<div class="flex items-center justify-between mt-2">
<!-- 快捷选项 -->
<div class="flex gap-2 flex-wrap">
<button
v-for="quick in quickFaultOptions"
:key="quick"
class="px-3 py-1 text-sm bg-bg-input text-text-secondary rounded border border-border-subtle hover:text-text-primary hover:border-status-free transition-colors"
@click="quickFillFault(quick)"
>
{{ quick }}
</button>
</div>
<!-- 字数统计 -->
<span class="text-sm text-text-muted">
{{ faultDescription.length }} / 500
</span>
</div>
</div>
</div>
<!-- 选中设备预览 -->
<div
v-if="selectedDeviceInfo"
class="flex items-center gap-3 p-4 bg-bg-input rounded-xl"
>
<span class="text-3xl">{{ selectedDeviceInfo.icon }}</span>
<div>
<div class="text-lg text-text-primary font-medium">
{{ selectedDeviceInfo.label }}
</div>
<div class="text-sm text-text-secondary">
将创建IT工单并通知管理员
</div>
</div>
</div>
<!-- 提交失败提示 -->
<div
v-if="submitResult && !submitResult.success"
class="p-4 bg-status-busy/10 border border-status-busy/30 rounded-xl text-status-busy text-lg"
>
{{ submitResult.message }}
</div>
</div>
<!-- ================================================================ -->
<!-- 底部操作栏 -->
<!-- ================================================================ -->
<div
v-if="!submitResult?.success"
class="flex items-center justify-end gap-4 p-6 border-t border-border-subtle"
>
<button
class="px-8 py-3 rounded-xl text-button font-medium bg-bg-input text-text-secondary border border-border-subtle hover:text-text-primary transition-colors"
@click="goBack"
>
取消
</button>
<button
class="px-10 py-3 rounded-xl text-button font-bold transition-all"
:class="[
canSubmit
? 'bg-status-free text-white hover:scale-105'
: 'bg-text-muted text-text-secondary cursor-not-allowed',
]"
:disabled="!canSubmit"
@click="handleSubmit"
>
{{ submitting ? '提交中...' : '提交报修' }}
</button>
</div>
</div>
</div>
</template>