diff --git a/schoolNewsServ/.bin/dify/部署注意点.md b/schoolNewsServ/.bin/dify/部署注意点.md new file mode 100644 index 0000000..9c2ac1b --- /dev/null +++ b/schoolNewsServ/.bin/dify/部署注意点.md @@ -0,0 +1,9 @@ +# 1.工作流修改 +## 动态知识库检索工作流 +修改dataset的apikey + +## 思政小帮手对话流 +修改“动态知识库检索”的入参 + +# 2. 后端修改 +修改yaml的配置内容 \ No newline at end of file diff --git a/schoolNewsServ/admin/src/main/resources/application.yml b/schoolNewsServ/admin/src/main/resources/application.yml index e2d73dd..eaa1118 100644 --- a/schoolNewsServ/admin/src/main/resources/application.yml +++ b/schoolNewsServ/admin/src/main/resources/application.yml @@ -111,6 +111,7 @@ school-news: - "/public/**" - "/static/**" - "/file/download/**" + - "/ai/chat/stream/**" crawler: @@ -158,6 +159,10 @@ crontab: type: Boolean value: true + +# dify +dify: + knowledgeApiKey: dataset-nupqKP4LONpzdXmGthIrbjeJ # 文件存储配置 file: storage: diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java index 6dfb20a..72060c0 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java @@ -514,6 +514,13 @@ public class DifyApiClient { } } } + // 流正常读取完毕,如果没有收到 [DONE] 标记,也当作完成 + logger.info("SSE流读取完毕"); + callback.onComplete(); + } catch (java.io.EOFException e) { + // EOFException 通常表示流正常结束(Dify可能没有发送[DONE]标记) + logger.info("SSE流已结束(EOF)"); + callback.onComplete(); } catch (Exception e) { logger.error("流式响应处理异常", e); callback.onError(e); diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java index b66520e..2d4a826 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java @@ -31,9 +31,9 @@ public class DifyConfig { * Dify API密钥(默认密钥,可被智能体的密钥覆盖) */ // private String apiKey="app-PTHzp2DsLPyiUrDYTXBGxL1f"; - private String apiKey="app-fwOqGFLTsZtekCQYlOmj9f8x"; + private String apiKey="app-6VXXDHRC8fiTijeRfnwzOyGn"; - private String knowledgeApiKey="dataset-HeDK9gHBqPnI4rBZ2q2Hm7rV"; + private String knowledgeApiKey="dataset-nupqKP4LONpzdXmGthIrbjeJ"; /** * 请求超时时间(秒) diff --git a/schoolNewsServ/study/src/main/resources/mapper/LearningRecordMapper.xml b/schoolNewsServ/study/src/main/resources/mapper/LearningRecordMapper.xml index 732b510..4039bb9 100644 --- a/schoolNewsServ/study/src/main/resources/mapper/LearningRecordMapper.xml +++ b/schoolNewsServ/study/src/main/resources/mapper/LearningRecordMapper.xml @@ -148,7 +148,7 @@ complete_time = #{completeTime}, - last_learn_time = #{lastLearnTime}, + last_learn_time = #{updateTime}, updater = #{updater}, @@ -298,8 +298,8 @@ ) current_dept ON user_dept.dept_path LIKE CONCAT(current_dept.dept_path, '%') WHERE lr.deleted = 0 - AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY) - AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY) + AND lr.update_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY) + AND lr.update_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY) GROUP BY lr.resource_type ORDER BY totalDuration DESC @@ -326,8 +326,8 @@ ) current_dept ON user_dept.dept_path LIKE CONCAT(current_dept.dept_path, '%') WHERE lr.deleted = 0 - AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY) - AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY) + AND lr.update_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY) + AND lr.update_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY) GROUP BY lr.user_id, u.username ORDER BY totalDuration DESC LIMIT 10 @@ -357,8 +357,8 @@ WHERE lr.deleted = 0 AND lr.resource_type = 2 - AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY) - AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY) + AND lr.update_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY) + AND lr.update_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY) GROUP BY lr.resource_id, c.name ORDER BY learnerCount DESC, totalDuration DESC LIMIT 10 @@ -388,8 +388,8 @@ WHERE lr.deleted = 0 AND lr.resource_type = 1 - AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY) - AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY) + AND lr.update_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY) + AND lr.update_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY) GROUP BY lr.resource_id, r.title ORDER BY learnerCount DESC, totalDuration DESC LIMIT 10 @@ -419,7 +419,7 @@ LEFT JOIN tb_learning_record lr ON tu.user_id = lr.user_id AND tu.task_id = lr.task_id AND lr.deleted = 0 WHERE tu.deleted = 0 AND tu.status = 2 - AND tu.complete_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY) + AND tu.complete_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY) AND tu.complete_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY) GROUP BY tu.user_id, u.username ORDER BY completedTaskCount DESC, totalDuration DESC diff --git a/schoolNewsWeb/src/apis/ai/chat.ts b/schoolNewsWeb/src/apis/ai/chat.ts index d1d2d7a..28090fb 100644 --- a/schoolNewsWeb/src/apis/ai/chat.ts +++ b/schoolNewsWeb/src/apis/ai/chat.ts @@ -127,7 +127,35 @@ export const chatApi = { // 监听错误事件 eventSource.addEventListener('error', (event: any) => { - const error = new Error(event.data || '对话失败'); + // 如果 data 为空,说明是正常的流结束信号(有些工作流不发送end事件) + if (!event.data || event.data.trim() === '') { + console.log('[SSE流结束] 收到空error事件,当作正常结束处理'); + + // 触发结束回调(如果有的话) + callback?.onMessageEnd?.({ + conversationId: request.conversationId, + messageId: '', + answer: fullMessage + } as any); + + eventSource.close(); + + // 正常结束 + resolve({ + code: 200, + success: true, + login: true, + auth: true, + data: { + conversationId: request.conversationId, + answer: fullMessage + } as any, + message: '对话已结束' + }); + return; + } + + const error = new Error(event.data); callback?.onError?.(error); eventSource.close(); reject(error); @@ -358,7 +386,34 @@ export const chatApi = { // 监听错误事件 eventSource.addEventListener('error', (event: any) => { - const error = new Error(event.data || '重新生成失败'); + // 如果 data 为空,说明是正常的流结束信号(有些工作流不发送end事件) + if (!event.data || event.data.trim() === '') { + console.log('[SSE流结束-重新生成] 收到空error事件,当作正常结束处理'); + + // 触发结束回调(如果有的话) + callback?.onMessageEnd?.({ + messageId: messageId, + answer: fullMessage + } as any); + + eventSource.close(); + + // 正常结束 + resolve({ + code: 200, + success: true, + login: true, + auth: true, + data: { + messageId: messageId, + answer: fullMessage + } as any, + message: '重新生成已结束' + }); + return; + } + + const error = new Error(event.data); callback?.onError?.(error); eventSource.close(); reject(error); diff --git a/schoolNewsWeb/src/views/admin/manage/study/StudyRecordsView.vue b/schoolNewsWeb/src/views/admin/manage/study/StudyRecordsView.vue index 4b45a3c..9d6bde8 100644 --- a/schoolNewsWeb/src/views/admin/manage/study/StudyRecordsView.vue +++ b/schoolNewsWeb/src/views/admin/manage/study/StudyRecordsView.vue @@ -180,7 +180,14 @@ function initDurationChart(data: any[]) { const duration = params.value; const hours = Math.floor(duration / 3600); const minutes = Math.floor((duration % 3600) / 60); - return `${params.name}
学习时长: ${hours}小时${minutes}分钟
学习人数: ${params.data.userCount}人`; + const secs = duration % 60; + + let timeStr = ''; + if (hours > 0) timeStr += `${hours}小时`; + if (minutes > 0) timeStr += `${minutes}分钟`; + if (secs > 0 || timeStr === '') timeStr += `${secs}秒`; + + return `${params.name}
学习时长: ${timeStr}
学习人数: ${params.data.userCount}人`; } }, legend: { @@ -204,7 +211,18 @@ function initDurationChart(data: any[]) { const duration = params.value; const hours = Math.floor(duration / 3600); const minutes = Math.floor((duration % 3600) / 60); - return `${params.name}\n${hours}h${minutes}m`; + const secs = duration % 60; + + let timeStr = ''; + if (hours > 0) { + timeStr = `${hours}h${minutes}m`; + } else if (minutes > 0) { + timeStr = `${minutes}m${secs}s`; + } else { + timeStr = `${secs}s`; + } + + return `${params.name}\n${timeStr}`; } }, emphasis: { @@ -233,12 +251,26 @@ function initDurationChart(data: any[]) { // 格式化时长 function formatDuration(seconds: number): string { + if (!seconds || seconds === 0) { + return '0秒'; + } + const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + let result = ''; if (hours > 0) { - return `${hours}小时${minutes}分钟`; + result += `${hours}小时`; } - return `${minutes}分钟`; + if (minutes > 0) { + result += `${minutes}分钟`; + } + if (secs > 0 || result === '') { + result += `${secs}秒`; + } + + return result; } // 获取排名样式 @@ -294,7 +326,7 @@ function getRankClass(index: number): string { &:hover { background: #e8edf3; - transform: translateX(5px); + // transform: translateX(5px); } .rank {