更新内容: - 后端 AI 服务优化(能力分析、知识点解析等) - 前端考试和陪练界面更新 - 修复多个 prompt 和 JSON 解析问题 - 更新 Coze 语音客户端
This commit is contained in:
@@ -98,6 +98,7 @@ export interface JudgeAnswerResponse {
|
||||
*/
|
||||
export interface RecordMistakeRequest {
|
||||
exam_id: number
|
||||
round: number // 考试轮次(1/2/3)
|
||||
question_id?: number | null
|
||||
knowledge_point_id?: number | null
|
||||
question_content: string
|
||||
@@ -154,9 +155,18 @@ export function recordMistake(data: RecordMistakeRequest) {
|
||||
|
||||
/**
|
||||
* 获取错题记录
|
||||
* @param examId 考试ID
|
||||
* @param round 指定轮次(可选),用于获取特定轮次的错题
|
||||
* 第2轮考试时传入round=1,获取第1轮的错题
|
||||
* 第3轮考试时传入round=2,获取第2轮的错题
|
||||
* 不传则获取该考试的所有错题
|
||||
*/
|
||||
export function getMistakes(examId: number) {
|
||||
return http.get<GetMistakesResponse>(`/api/v1/exams/mistakes?exam_id=${examId}`, {
|
||||
export function getMistakes(examId: number, round?: number) {
|
||||
let url = `/api/v1/exams/mistakes?exam_id=${examId}`
|
||||
if (round !== undefined) {
|
||||
url += `&round=${round}`
|
||||
}
|
||||
return http.get<GetMistakesResponse>(url, {
|
||||
showLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,15 +89,16 @@ export class CozeVoiceClient {
|
||||
console.log('[CozeVoice] ✅ 已设置播放音量=1')
|
||||
}
|
||||
|
||||
// 6. 如果有场景提示词,发送初始消息
|
||||
// 注意:参考代码不发送初始消息,而是等待用户先说话
|
||||
// 但我们的场景需要AI先开场白,所以发送场景提示词
|
||||
// ⚠️ 重要:不要在连接后发送场景提示词!
|
||||
// SDK连接时默认设置 need_play_prologue: true,Bot会自动播放开场白
|
||||
// 如果手动发送sendTextMessage会干扰开场白播放流程
|
||||
// 场景信息应该在Bot的系统提示词中预设
|
||||
if (config.scenePrompt) {
|
||||
this.client.sendTextMessage(config.scenePrompt)
|
||||
console.log('[CozeVoice] ✅ 场景提示词已发送')
|
||||
console.log('[CozeVoice] ⚠️ 场景提示词将通过Bot开场白播放,不手动发送')
|
||||
console.log('[CozeVoice] 📝 场景提示词:', config.scenePrompt.substring(0, 100) + '...')
|
||||
}
|
||||
|
||||
console.log('[CozeVoice] 🎉 初始化完成,等待对话...')
|
||||
console.log('[CozeVoice] 🎉 初始化完成,等待Bot开场白和用户对话...')
|
||||
|
||||
} catch (error) {
|
||||
console.error('[CozeVoice] ❌ 连接失败:', error)
|
||||
@@ -146,6 +147,10 @@ export class CozeVoiceClient {
|
||||
|
||||
if (!event) return
|
||||
|
||||
// 🔍 调试:打印所有收到的事件(详细日志)
|
||||
console.log('[CozeVoice] 📨 收到事件:', eventName, '-> event_type:', event.event_type)
|
||||
console.log('[CozeVoice] 📨 事件数据:', JSON.stringify(event.data || {}).substring(0, 200))
|
||||
|
||||
switch (event.event_type) {
|
||||
// 用户开始说话 - 关闭扬声器本地回放(避免用户听到自己的声音)
|
||||
case 'input_audio_buffer.speech_started': {
|
||||
@@ -221,6 +226,43 @@ export class CozeVoiceClient {
|
||||
this.emit('ai_speech_end', {})
|
||||
break
|
||||
}
|
||||
|
||||
// 🔍 调试:捕获服务器错误事件
|
||||
case 'error': {
|
||||
console.error('[CozeVoice] ❌ 服务器返回错误:', event)
|
||||
this.emit('error', { error: event.data?.error || '未知错误' })
|
||||
break
|
||||
}
|
||||
|
||||
// 对话创建事件
|
||||
case WebsocketsEventType.CONVERSATION_CHAT_CREATED: {
|
||||
console.log('[CozeVoice] ✅ 对话创建成功')
|
||||
break
|
||||
}
|
||||
|
||||
// 对话进行中
|
||||
case 'conversation.chat.in_progress': {
|
||||
console.log('[CozeVoice] 🔄 AI正在思考...')
|
||||
break
|
||||
}
|
||||
|
||||
// 对话失败事件
|
||||
case 'conversation.chat.failed': {
|
||||
console.error('[CozeVoice] ❌ 对话失败:', event)
|
||||
this.emit('error', { error: event.data?.last_error?.msg || '对话失败' })
|
||||
break
|
||||
}
|
||||
|
||||
// 对话完成事件
|
||||
case 'conversation.chat.completed': {
|
||||
console.log('[CozeVoice] ✅ 对话完成')
|
||||
break
|
||||
}
|
||||
|
||||
// 默认处理:记录未知事件
|
||||
default: {
|
||||
console.log('[CozeVoice] ⚠️ 未处理的事件类型:', event.event_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -256,6 +256,16 @@ const formatDuration = (seconds: number) => {
|
||||
return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期为 YYYY-MM-DD(使用本地时间,避免时区问题)
|
||||
*/
|
||||
const formatLocalDate = (date: Date): string => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载成绩报告数据
|
||||
*/
|
||||
@@ -263,10 +273,10 @@ const loadReportData = async () => {
|
||||
try {
|
||||
const params: any = {}
|
||||
|
||||
// 添加时间范围参数
|
||||
// 添加时间范围参数(使用本地时间格式化,避免UTC时区偏移问题)
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
params.start_date = dateRange.value[0].toISOString().split('T')[0]
|
||||
params.end_date = dateRange.value[1].toISOString().split('T')[0]
|
||||
params.start_date = formatLocalDate(dateRange.value[0])
|
||||
params.end_date = formatLocalDate(dateRange.value[1])
|
||||
}
|
||||
|
||||
const response = await getExamReport(params)
|
||||
|
||||
@@ -341,8 +341,28 @@ const transformDifyQuestions = (difyQuestions: any[]): any[] => {
|
||||
const options = q.topic?.options || {}
|
||||
// 解析正确答案:支持 "A"、"A,B"、"A、B"、"A:xxx" 等多种格式
|
||||
const correctAnswerStr = String(q.correct || '')
|
||||
// 提取正确答案中的选项字母(A、B、C、D等)
|
||||
const correctLetters = correctAnswerStr.match(/[A-Da-d]/g)?.map(l => l.toUpperCase()) || []
|
||||
|
||||
// 更精确地提取正确答案字母(避免误提取选项内容中的字母)
|
||||
let correctLetters: string[] = []
|
||||
|
||||
// 情况1:纯选项字母格式(如 "A", "B", "A,B", "A、B", "A,B,C")
|
||||
const pureLetterMatch = correctAnswerStr.trim().match(/^[A-Da-d]([,、\s]+[A-Da-d])*$/)
|
||||
if (pureLetterMatch) {
|
||||
correctLetters = correctAnswerStr.match(/[A-Da-d]/g)?.map(l => l.toUpperCase()) || []
|
||||
} else {
|
||||
// 情况2:选项字母+内容格式(如 "A:xxx" 或 "A: xxx")
|
||||
// 只提取开头的选项字母(可能有多个,如 "A,C:xxx")
|
||||
const prefixMatch = correctAnswerStr.match(/^([A-Da-d](?:[,、\s]*[A-Da-d])*)[:::\s]/)
|
||||
if (prefixMatch) {
|
||||
correctLetters = prefixMatch[1].match(/[A-Da-d]/g)?.map(l => l.toUpperCase()) || []
|
||||
} else {
|
||||
// 情况3:只有开头字母(如 "A" 后面直接跟内容)
|
||||
const firstLetterMatch = correctAnswerStr.match(/^([A-Da-d])/)
|
||||
if (firstLetterMatch) {
|
||||
correctLetters = [firstLetterMatch[1].toUpperCase()]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔍 解析题目[${index + 1}] - correct: "${q.correct}", correctLetters: [${correctLetters.join(', ')}]`)
|
||||
|
||||
@@ -481,10 +501,11 @@ const loadQuestions = async () => {
|
||||
// 第一轮考试不传mistake_records参数
|
||||
// 第二、三轮考试传入上一轮的错题记录
|
||||
if (currentRound.value > 1) {
|
||||
// 用同一个exam_id获取错题
|
||||
console.log(`📋 获取第${currentRound.value - 1}轮错题记录 - exam_id: ${currentExamId.value}`)
|
||||
// 用同一个exam_id获取上一轮的错题(传入round参数只获取上一轮的错题)
|
||||
const previousRound = currentRound.value - 1
|
||||
console.log(`📋 获取第${previousRound}轮错题记录 - exam_id: ${currentExamId.value}, round: ${previousRound}`)
|
||||
|
||||
const mistakesRes: any = await getMistakes(currentExamId.value)
|
||||
const mistakesRes: any = await getMistakes(currentExamId.value, previousRound)
|
||||
|
||||
console.log('getMistakes原始响应:', mistakesRes)
|
||||
console.log('getMistakes响应结构:', {
|
||||
@@ -785,6 +806,7 @@ const recordWrongAnswer = async () => {
|
||||
try {
|
||||
await recordMistake({
|
||||
exam_id: currentExamId.value,
|
||||
round: currentRound.value, // 记录当前轮次
|
||||
question_id: null, // AI生成的题目没有question_id
|
||||
knowledge_point_id: q.knowledge_point_id || null,
|
||||
question_content: q.title,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h2 class="course-title">{{ courseInfo.title }}</h2>
|
||||
<span class="powered-by">
|
||||
<el-icon><Connection /></el-icon>
|
||||
由 Dify 对话流驱动
|
||||
智能对话助手
|
||||
</span>
|
||||
</div>
|
||||
<el-button link @click="clearChat">
|
||||
@@ -512,19 +512,37 @@ const scrollToBottom = async () => {
|
||||
const loadCourseInfo = async () => {
|
||||
try {
|
||||
const courseId = parseInt(courseInfo.value.id)
|
||||
if (isNaN(courseId)) {
|
||||
if (isNaN(courseId) || courseId <= 0) {
|
||||
console.warn('无效的课程ID:', courseInfo.value.id)
|
||||
courseInfo.value.title = '未知课程'
|
||||
return
|
||||
}
|
||||
const res: any = await getCourseDetail(courseId)
|
||||
// API 返回格式是 { code: 200, data: { name: "...", ... } }
|
||||
if (res.code === 200 && res.data) {
|
||||
courseInfo.value.title = res.data.title || res.data.name || '未命名课程'
|
||||
console.log('课程详情API响应:', res)
|
||||
|
||||
// 兼容不同的响应格式
|
||||
// 格式1: { code: 200, data: { name: "...", ... } }
|
||||
// 格式2: 直接返回课程对象 { name: "...", ... }
|
||||
let courseData = res
|
||||
if (res.code !== undefined && res.data) {
|
||||
// 标准响应格式
|
||||
if (res.code === 200) {
|
||||
courseData = res.data
|
||||
} else {
|
||||
console.warn('获取课程详情失败:', res.message || '未知错误')
|
||||
courseInfo.value.title = '未知课程'
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 从课程数据中提取名称
|
||||
const courseName = courseData?.title || courseData?.name
|
||||
if (courseName) {
|
||||
courseInfo.value.title = courseName
|
||||
console.log('课程详情已加载:', courseInfo.value.title)
|
||||
} else {
|
||||
console.warn('获取课程详情失败:', res.message || '未知错误')
|
||||
courseInfo.value.title = '未知课程'
|
||||
console.warn('课程数据中缺少名称字段:', courseData)
|
||||
courseInfo.value.title = '未命名课程'
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载课程详情失败:', error)
|
||||
|
||||
@@ -321,8 +321,7 @@ const pdfScaleStyle = computed(() => {
|
||||
const scale = 1 / pdfRenderScale.value
|
||||
return {
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'top left',
|
||||
width: `${100 * pdfRenderScale.value}%`
|
||||
transformOrigin: 'top center'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -911,6 +910,7 @@ onUnmounted(() => {
|
||||
padding: 10px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
flex-shrink: 0;
|
||||
|
||||
.page-controls,
|
||||
.zoom-controls {
|
||||
@@ -933,10 +933,12 @@ onUnmounted(() => {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
// 高清渲染缩放容器
|
||||
.pdf-scale-wrapper {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
:deep(.vue-pdf-embed) {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<div class="header-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="loadingRecommendations"
|
||||
:loading="analyzing"
|
||||
@click="refreshRecommendations"
|
||||
class="refresh-btn"
|
||||
>
|
||||
@@ -405,7 +405,6 @@ const abilityFeedback = ref<AbilityFeedback[]>([])
|
||||
|
||||
// AI 分析和推荐相关
|
||||
const analyzing = ref(false)
|
||||
const loadingRecommendations = ref(false)
|
||||
const selectedPriority = ref('all')
|
||||
|
||||
// 计算属性:过滤后的课程
|
||||
@@ -723,81 +722,11 @@ const analyzeSmartBadgeData = async () => {
|
||||
|
||||
/**
|
||||
* 刷新AI推荐课程
|
||||
* 重新调用AI分析接口,获取最新的课程推荐
|
||||
*/
|
||||
const refreshRecommendations = async () => {
|
||||
loadingRecommendations.value = true
|
||||
|
||||
try {
|
||||
// 模拟AI推荐算法
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
// 根据能力评估结果生成推荐
|
||||
const weakPoints = abilityData.value
|
||||
.filter(item => item.value < 85)
|
||||
.sort((a, b) => a.value - b.value)
|
||||
.slice(0, 3)
|
||||
|
||||
// 更新推荐课程(基于薄弱环节)
|
||||
const newRecommendations = []
|
||||
|
||||
if (weakPoints.some(p => p.name === '沟通技巧')) {
|
||||
newRecommendations.push({
|
||||
id: 1,
|
||||
name: '美容咨询与沟通技巧',
|
||||
description: '提升与客户的沟通能力,学习专业的美容咨询技巧',
|
||||
duration: 8,
|
||||
difficulty: 'medium',
|
||||
learnerCount: 156,
|
||||
priority: 'high',
|
||||
targetWeakPoints: ['沟通技巧'],
|
||||
expectedImprovement: 12,
|
||||
matchScore: 95,
|
||||
progress: 85,
|
||||
examPassed: false
|
||||
})
|
||||
}
|
||||
|
||||
if (weakPoints.some(p => p.name === '应变能力')) {
|
||||
newRecommendations.push({
|
||||
id: 2,
|
||||
name: '皮肤问题识别与处理',
|
||||
description: '学习识别常见皮肤问题,掌握针对性的护理和治疗方案',
|
||||
duration: 12,
|
||||
difficulty: 'hard',
|
||||
learnerCount: 89,
|
||||
priority: 'medium',
|
||||
targetWeakPoints: ['应变能力'],
|
||||
expectedImprovement: 15,
|
||||
matchScore: 88,
|
||||
progress: 95,
|
||||
examPassed: true
|
||||
})
|
||||
}
|
||||
|
||||
if (weakPoints.some(p => p.name === '安全意识')) {
|
||||
newRecommendations.push({
|
||||
id: 3,
|
||||
name: '轻医美项目与效果管理',
|
||||
description: '深入了解各种轻医美项目,掌握效果评估和管理方法',
|
||||
duration: 16,
|
||||
difficulty: 'hard',
|
||||
learnerCount: 234,
|
||||
priority: 'medium',
|
||||
targetWeakPoints: ['安全意识'],
|
||||
expectedImprovement: 10,
|
||||
matchScore: 82,
|
||||
progress: 60,
|
||||
examPassed: false
|
||||
})
|
||||
}
|
||||
|
||||
recommendedCourses.value = newRecommendations
|
||||
|
||||
} catch (error) {
|
||||
ElMessage.error('获取推荐课程失败')
|
||||
} finally {
|
||||
loadingRecommendations.value = false
|
||||
}
|
||||
// 直接调用AI分析智能工牌数据的方法,复用已有逻辑
|
||||
await analyzeSmartBadgeData()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user