xsh_1997 2 kuukautta sitten
vanhempi
commit
82bc8cf349

+ 7 - 1
ruoyi-ui-app/api/diseaseTreatment/aiOnlineConsult.js

@@ -252,6 +252,7 @@ function streamRequestHeaders() {
252
 function createStreamState(options) {
252
 function createStreamState(options) {
253
   const { onDelta, onSessionId } = options
253
   const { onDelta, onSessionId } = options
254
   return {
254
   return {
255
+    startMs: Date.now(),
255
     buffer: '',
256
     buffer: '',
256
     fullText: '',
257
     fullText: '',
257
     sessionId: null,
258
     sessionId: null,
@@ -262,7 +263,11 @@ function createStreamState(options) {
262
 
263
 
263
 function finishStreamState(state) {
264
 function finishStreamState(state) {
264
   flushSseBuffer(state, state.callbacks)
265
   flushSseBuffer(state, state.callbacks)
265
-  return { content: state.fullText, sessionId: state.sessionId }
266
+  return {
267
+    content: state.fullText,
268
+    sessionId: state.sessionId,
269
+    durationMs: Date.now() - state.startMs
270
+  }
266
 }
271
 }
267
 
272
 
268
 // #ifdef H5
273
 // #ifdef H5
@@ -358,6 +363,7 @@ function sendChatMessageStream(body, options) {
358
 /**
363
 /**
359
  * 聊天 POST /v1/chat/completions(默认 stream: true)
364
  * 聊天 POST /v1/chat/completions(默认 stream: true)
360
  * options: onDelta, onSessionId, onReadyTask(task), signal(H5)
365
  * options: onDelta, onSessionId, onReadyTask(task), signal(H5)
366
+ * 流式 resolve:{ content, sessionId, durationMs },durationMs 为整段流式请求耗时(毫秒)
361
  */
367
  */
362
 export function sendChatMessage(data, options = {}) {
368
 export function sendChatMessage(data, options = {}) {
363
   const body = { stream: true, ...(data || {}) }
369
   const body = { stream: true, ...(data || {}) }

+ 12 - 7
ruoyi-ui-app/package-a/ai-assistant/index.vue

@@ -1104,12 +1104,13 @@ export default {
1104
         if (norm.sendTime) row.sendTime = norm.sendTime
1104
         if (norm.sendTime) row.sendTime = norm.sendTime
1105
       }
1105
       }
1106
     },
1106
     },
1107
-    queuePendingPersist(consultSessionId, persistBody, aiReplyContent, llmSessionId) {
1107
+    queuePendingPersist(consultSessionId, persistBody, aiReplyContent, llmSessionId, costTime) {
1108
       this.pendingPersist = {
1108
       this.pendingPersist = {
1109
         consultSessionId,
1109
         consultSessionId,
1110
         persistBody: { ...persistBody },
1110
         persistBody: { ...persistBody },
1111
         aiReplyContent,
1111
         aiReplyContent,
1112
-        llmSessionId: llmSessionId != null ? llmSessionId : null
1112
+        llmSessionId: llmSessionId != null ? llmSessionId : null,
1113
+        costTime: costTime != null ? costTime : null
1113
       }
1114
       }
1114
     },
1115
     },
1115
     flushPendingPersist() {
1116
     flushPendingPersist() {
@@ -1120,14 +1121,15 @@ export default {
1120
       return sendAiConsultMessage(pending.consultSessionId, {
1121
       return sendAiConsultMessage(pending.consultSessionId, {
1121
         ...pending.persistBody,
1122
         ...pending.persistBody,
1122
         aiReplyContent: pending.aiReplyContent,
1123
         aiReplyContent: pending.aiReplyContent,
1123
-        realSessionId: pending.llmSessionId != null ? pending.llmSessionId : null
1124
+        realSessionId: pending.llmSessionId != null ? pending.llmSessionId : null,
1125
+        costTime: pending.costTime != null ? pending.costTime : undefined
1124
       })
1126
       })
1125
         .then(() => {
1127
         .then(() => {
1126
           this.pendingPersist = null
1128
           this.pendingPersist = null
1127
         })
1129
         })
1128
         .catch(() => {})
1130
         .catch(() => {})
1129
     },
1131
     },
1130
-    persistConsultExchange(persistBody, aiReplyContent) {
1132
+    persistConsultExchange(persistBody, aiReplyContent, costTime) {
1131
       const reply = (aiReplyContent || '').trim()
1133
       const reply = (aiReplyContent || '').trim()
1132
       const consultSessionId = this.consultSessionIdForApi()
1134
       const consultSessionId = this.consultSessionIdForApi()
1133
       if (!consultSessionId || !persistBody || !reply) {
1135
       if (!consultSessionId || !persistBody || !reply) {
@@ -1137,7 +1139,8 @@ export default {
1137
       return sendAiConsultMessage(consultSessionId, {
1139
       return sendAiConsultMessage(consultSessionId, {
1138
         ...persistBody,
1140
         ...persistBody,
1139
         aiReplyContent: reply,
1141
         aiReplyContent: reply,
1140
-        realSessionId: llmSessionId || null
1142
+        realSessionId: llmSessionId || null,
1143
+        costTime: costTime != null ? costTime : undefined
1141
       })
1144
       })
1142
         .then((res) => {
1145
         .then((res) => {
1143
           this.mergePersistResult(res.data, reply)
1146
           this.mergePersistResult(res.data, reply)
@@ -1151,7 +1154,7 @@ export default {
1151
           return true
1154
           return true
1152
         })
1155
         })
1153
         .catch((err) => {
1156
         .catch((err) => {
1154
-          this.queuePendingPersist(consultSessionId, persistBody, reply, llmSessionId)
1157
+          this.queuePendingPersist(consultSessionId, persistBody, reply, llmSessionId, costTime)
1155
           const msg = (err && err.message) || this.$t('aiOnlineConsult.saveSessionFailed')
1158
           const msg = (err && err.message) || this.$t('aiOnlineConsult.saveSessionFailed')
1156
           uni.showToast({ title: msg, icon: 'none' })
1159
           uni.showToast({ title: msg, icon: 'none' })
1157
           return false
1160
           return false
@@ -1236,6 +1239,7 @@ export default {
1236
 
1239
 
1237
       let aiReplyContent = ''
1240
       let aiReplyContent = ''
1238
       let aiMsg = null
1241
       let aiMsg = null
1242
+      let costTime = null
1239
       this.abortStreamRequest()
1243
       this.abortStreamRequest()
1240
       this.freezeFeedScrollTopForStream()
1244
       this.freezeFeedScrollTopForStream()
1241
       this.sending = true
1245
       this.sending = true
@@ -1264,6 +1268,7 @@ export default {
1264
         if (llmSid) {
1268
         if (llmSid) {
1265
           this.rememberLlmSessionId(llmSid)
1269
           this.rememberLlmSessionId(llmSid)
1266
         }
1270
         }
1271
+        costTime = streamResult.durationMs != null ? streamResult.durationMs : null
1267
         this.clearStreamRevealTimer()
1272
         this.clearStreamRevealTimer()
1268
         aiReplyContent = (streamResult.content || aiReplyContent || '').trim() || this.$t('aiOnlineConsult.noContent')
1273
         aiReplyContent = (streamResult.content || aiReplyContent || '').trim() || this.$t('aiOnlineConsult.noContent')
1269
         if (!aiMsg) {
1274
         if (!aiMsg) {
@@ -1297,7 +1302,7 @@ export default {
1297
         }
1302
         }
1298
         this.$nextTick(() => this.scheduleScrollToBottom())
1303
         this.$nextTick(() => this.scheduleScrollToBottom())
1299
       }
1304
       }
1300
-      await this.persistConsultExchange(persistBody, (aiReplyContent || '').trim())
1305
+      await this.persistConsultExchange(persistBody, (aiReplyContent || '').trim(), costTime)
1301
     },
1306
     },
1302
     validateMediaFile(filePath, fileName, kind) {
1307
     validateMediaFile(filePath, fileName, kind) {
1303
       const rule = MEDIA_RULES[kind]
1308
       const rule = MEDIA_RULES[kind]

+ 1 - 1
ruoyi-ui-app/vite.config.js

@@ -3,7 +3,7 @@ import uni from '@dcloudio/vite-plugin-uni'
3
 import { LLM_PROXY_TARGET } from './config/llm.js'
3
 import { LLM_PROXY_TARGET } from './config/llm.js'
4
 
4
 
5
 /** 与 config/index.js 中 DEV_API_HOST 保持一致 */
5
 /** 与 config/index.js 中 DEV_API_HOST 保持一致 */
6
-const DEV_API_TARGET = 'http://192.168.1.10:8010'
6
+const DEV_API_TARGET = 'http://192.168.1.6:8010'
7
 
7
 
8
 const llmTarget = String(LLM_PROXY_TARGET || '').replace(/\/$/, '')
8
 const llmTarget = String(LLM_PROXY_TARGET || '').replace(/\/$/, '')
9
 
9
 

+ 16 - 1
ruoyi-ui/src/api/diseaseTreatment/aiOnlineConsult.js

@@ -39,6 +39,15 @@ export function sendAiConsultMessage(sessionId, data) {
39
   })
39
   })
40
 }
40
 }
41
 
41
 
42
+/** 隐藏/删除 AI 诊断会话(vet_visible=0,列表不再展示) */
43
+export function hideAiConsultSession(sessionId) {
44
+  return request({
45
+    url: "/diseaseTreatment/onlineConsult/ai/session/" + sessionId + "/hide",
46
+    method: "post",
47
+    headers: { repeatSubmit: false }
48
+  })
49
+}
50
+
42
 /**
51
 /**
43
  * 从大模型 SSE/JSON 解析会话 id。
52
  * 从大模型 SSE/JSON 解析会话 id。
44
  * 网关流式首包及后续块中的 `id` 即为 sessionId(与 OpenAI chat id 一致)。
53
  * 网关流式首包及后续块中的 `id` 即为 sessionId(与 OpenAI chat id 一致)。
@@ -210,6 +219,7 @@ function streamRequestHeaders() {
210
 
219
 
211
 /** 流式:fetch + ReadableStream 逐块解析(axios XHR 常会缓冲到结束才触发 progress) */
220
 /** 流式:fetch + ReadableStream 逐块解析(axios XHR 常会缓冲到结束才触发 progress) */
212
 async function sendChatMessageStream(data, options) {
221
 async function sendChatMessageStream(data, options) {
222
+  const startMs = Date.now()
213
   const { onDelta, onSessionId, signal } = options
223
   const { onDelta, onSessionId, signal } = options
214
   const state = { buffer: "", fullText: "", sessionId: null }
224
   const state = { buffer: "", fullText: "", sessionId: null }
215
   const callbacks = { onDelta, onSessionId }
225
   const callbacks = { onDelta, onSessionId }
@@ -247,13 +257,18 @@ async function sendChatMessageStream(data, options) {
247
   }
257
   }
248
   appendSseChunk(state, decoder.decode(), callbacks)
258
   appendSseChunk(state, decoder.decode(), callbacks)
249
   flushSseBuffer(state, callbacks)
259
   flushSseBuffer(state, callbacks)
250
-  return { content: state.fullText, sessionId: state.sessionId }
260
+  return {
261
+    content: state.fullText,
262
+    sessionId: state.sessionId,
263
+    durationMs: Date.now() - startMs
264
+  }
251
 }
265
 }
252
 
266
 
253
 /**
267
 /**
254
  * 聊天 POST /v1/chat/completions(默认 stream: true)
268
  * 聊天 POST /v1/chat/completions(默认 stream: true)
255
  * - stream: false → JSON(走 request 默认响应)
269
  * - stream: false → JSON(走 request 默认响应)
256
  * - 其余情况 → SSE,options 可传 onDelta / onSessionId / signal
270
  * - 其余情况 → SSE,options 可传 onDelta / onSessionId / signal
271
+ * - 流式 resolve:{ content, sessionId, durationMs },durationMs 为整段流式请求耗时(毫秒)
257
  */
272
  */
258
 export function sendChatMessage(data, options = {}) {
273
 export function sendChatMessage(data, options = {}) {
259
   const body = { stream: true, ...(data || {}) }
274
   const body = { stream: true, ...(data || {}) }

+ 2 - 0
ruoyi-ui/src/lang/bo/diseaseTreatment.js

@@ -161,6 +161,8 @@ export default {
161
     selectSession: "གླེང་མོལ་འདེམ་ནམ་གསར་བཟོ།",
161
     selectSession: "གླེང་མོལ་འདེམ་ནམ་གསར་བཟོ།",
162
     emptyChat: "AI ལན་འདྲི་འགོ་བཙུགས",
162
     emptyChat: "AI ལན་འདྲི་འགོ་བཙུགས",
163
     newSessionBtn: "གླེང་མོལ་གསར་བཟོ།",
163
     newSessionBtn: "གླེང་མོལ་གསར་བཟོ།",
164
+    deleteSession: "གླེང་མོལ་འདི་སུབ་པ།",
165
+    confirmDeleteSession: "གླེང་མོལ「{title}」སུབ་དགོས་སམ?སུབ་རྗེས་སླར་གསོ་མི་ཐུབ།",
164
     chatHeadAi: "AI ལག་རོགས།",
166
     chatHeadAi: "AI ལག་རོགས།",
165
     disclaimerTitle: "འགན་འཁྲི་བཤད་པ།",
167
     disclaimerTitle: "འགན་འཁྲི་བཤད་པ།",
166
     thinking: "བསམ་བཞིན་པ",
168
     thinking: "བསམ་བཞིན་པ",

+ 2 - 0
ruoyi-ui/src/lang/zh/diseaseTreatment.js

@@ -161,6 +161,8 @@ export default {
161
     selectSession: "请选择或新建会话",
161
     selectSession: "请选择或新建会话",
162
     emptyChat: "开始与 AI 助手对话",
162
     emptyChat: "开始与 AI 助手对话",
163
     newSessionBtn: "新增会话",
163
     newSessionBtn: "新增会话",
164
+    deleteSession: "删除此会话",
165
+    confirmDeleteSession: "是否删除会话「{title}」?删除后无法恢复。",
164
     chatHeadAi: "AI 助手",
166
     chatHeadAi: "AI 助手",
165
     disclaimerTitle: "免责声明",
167
     disclaimerTitle: "免责声明",
166
     thinking: "思考中",
168
     thinking: "思考中",

+ 68 - 8
ruoyi-ui/src/views/diseaseTreatment/onlineConsult/ai/index.vue

@@ -57,6 +57,14 @@
57
                 </div>
57
                 </div>
58
                 <div class="session-item__preview">{{ s.lastMessagePreview || dash }}</div>
58
                 <div class="session-item__preview">{{ s.lastMessagePreview || dash }}</div>
59
               </div>
59
               </div>
60
+              <el-button
61
+                v-hasPermi="['diseaseTreatment:aiOnlineConsult:remove']"
62
+                type="text"
63
+                icon="el-icon-delete"
64
+                class="session-item__delete"
65
+                :title="dtT('deleteSession')"
66
+                @click.stop="handleDeleteSession(s)"
67
+              />
60
             </div>
68
             </div>
61
             <el-empty
69
             <el-empty
62
               v-if="!listLoading && !sessions.length"
70
               v-if="!listLoading && !sessions.length"
@@ -294,6 +302,7 @@ import {
294
   createAiConsultSession,
302
   createAiConsultSession,
295
   listAiConsultMessages,
303
   listAiConsultMessages,
296
   sendAiConsultMessage,
304
   sendAiConsultMessage,
305
+  hideAiConsultSession,
297
   sendChatMessage,
306
   sendChatMessage,
298
   extractLlmSessionIdFromJson,
307
   extractLlmSessionIdFromJson,
299
   getModelList
308
   getModelList
@@ -439,7 +448,7 @@ export default {
439
   methods: {
448
   methods: {
440
     getModelList() {
449
     getModelList() {
441
       getModelList().then((res) => {
450
       getModelList().then((res) => {
442
-        console.log(res)
451
+       
443
       })
452
       })
444
     },
453
     },
445
     renderChatMarkdown,
454
     renderChatMarkdown,
@@ -657,6 +666,33 @@ export default {
657
       this.loadMessages(false)
666
       this.loadMessages(false)
658
       this.startPolling()
667
       this.startPolling()
659
     },
668
     },
669
+    handleDeleteSession(session) {
670
+      const sid = this.resolveSessionId(session)
671
+      if (!sid) {
672
+        return
673
+      }
674
+      const title = this.sessionTitle(session)
675
+      this.$modal
676
+        .confirm(this.dtT("confirmDeleteSession", { title }))
677
+        .then(() => hideAiConsultSession(sid))
678
+        .then(() => {
679
+          this.$modal.msgSuccess(this.dtCommon("msgDelOk"))
680
+          this.clearLlmSessionId(sid)
681
+          this.sessions = this.sessions.filter((s) => this.resolveSessionId(s) !== sid)
682
+          if (this.pendingPersist && this.pendingPersist.consultSessionId === sid) {
683
+            this.pendingPersist = null
684
+          }
685
+          if (this.activeSessionId === sid) {
686
+            this.abortStreamRequest()
687
+            this.stopPolling()
688
+            this.activeSessionId = ""
689
+            this.messages = []
690
+            this.draft = ""
691
+            this.sending = false
692
+          }
693
+        })
694
+        .catch(() => {})
695
+    },
660
     loadMessages(older) {
696
     loadMessages(older) {
661
       if (!this.activeSessionId) {
697
       if (!this.activeSessionId) {
662
         return
698
         return
@@ -981,12 +1017,13 @@ export default {
981
         }
1017
         }
982
       }
1018
       }
983
     },
1019
     },
984
-    queuePendingPersist(consultSessionId, persistBody, aiReplyContent, llmSessionId) {
1020
+    queuePendingPersist(consultSessionId, persistBody, aiReplyContent, llmSessionId, costTime) {
985
       this.pendingPersist = {
1021
       this.pendingPersist = {
986
         consultSessionId,
1022
         consultSessionId,
987
         persistBody: { ...persistBody },
1023
         persistBody: { ...persistBody },
988
         aiReplyContent,
1024
         aiReplyContent,
989
-        llmSessionId: llmSessionId != null ? llmSessionId : null
1025
+        llmSessionId: llmSessionId != null ? llmSessionId : null,
1026
+        costTime: costTime != null ? costTime : null
990
       }
1027
       }
991
     },
1028
     },
992
     async flushPendingPersist() {
1029
     async flushPendingPersist() {
@@ -998,14 +1035,15 @@ export default {
998
         await sendAiConsultMessage(pending.consultSessionId, {
1035
         await sendAiConsultMessage(pending.consultSessionId, {
999
           ...pending.persistBody,
1036
           ...pending.persistBody,
1000
           aiReplyContent: pending.aiReplyContent,
1037
           aiReplyContent: pending.aiReplyContent,
1001
-          realSessionId: pending.llmSessionId != null ? pending.llmSessionId : null
1038
+          realSessionId: pending.llmSessionId != null ? pending.llmSessionId : null,
1039
+          costTime: pending.costTime != null ? pending.costTime : undefined
1002
         })
1040
         })
1003
         this.pendingPersist = null
1041
         this.pendingPersist = null
1004
       } catch (err) {
1042
       } catch (err) {
1005
         // 离开页面时仍失败则保留队列,下次进入同页可再试(仅内存,刷新会丢)
1043
         // 离开页面时仍失败则保留队列,下次进入同页可再试(仅内存,刷新会丢)
1006
       }
1044
       }
1007
     },
1045
     },
1008
-    async persistConsultExchange(persistBody, aiReplyContent) {
1046
+    async persistConsultExchange(persistBody, aiReplyContent, costTime) {
1009
       const reply = (aiReplyContent || "").trim()
1047
       const reply = (aiReplyContent || "").trim()
1010
       if (!this.activeSessionId || !persistBody || !reply) {
1048
       if (!this.activeSessionId || !persistBody || !reply) {
1011
         return true
1049
         return true
@@ -1015,7 +1053,8 @@ export default {
1015
       const saveBody = {
1053
       const saveBody = {
1016
         ...persistBody,
1054
         ...persistBody,
1017
         aiReplyContent: reply,
1055
         aiReplyContent: reply,
1018
-        realSessionId: llmSessionId || null
1056
+        realSessionId: llmSessionId || null,
1057
+        costTime: costTime != null ? costTime : undefined
1019
       }
1058
       }
1020
       try {
1059
       try {
1021
         const res = await sendAiConsultMessage(consultSessionId, saveBody)
1060
         const res = await sendAiConsultMessage(consultSessionId, saveBody)
@@ -1030,7 +1069,7 @@ export default {
1030
         }
1069
         }
1031
         return true
1070
         return true
1032
       } catch (err) {
1071
       } catch (err) {
1033
-        this.queuePendingPersist(consultSessionId, persistBody, reply, llmSessionId)
1072
+        this.queuePendingPersist(consultSessionId, persistBody, reply, llmSessionId, costTime)
1034
         const msg = (err && err.message) || this.dtT("saveSessionFailed")
1073
         const msg = (err && err.message) || this.dtT("saveSessionFailed")
1035
         this.$modal.msgWarning(msg)
1074
         this.$modal.msgWarning(msg)
1036
         return false
1075
         return false
@@ -1191,6 +1230,7 @@ export default {
1191
 
1230
 
1192
       let aiReplyContent = ""
1231
       let aiReplyContent = ""
1193
       let aiMsg = null
1232
       let aiMsg = null
1233
+      let costTime = null
1194
       this.abortStreamRequest()
1234
       this.abortStreamRequest()
1195
       const abortController = new AbortController()
1235
       const abortController = new AbortController()
1196
       this.streamAbortController = abortController
1236
       this.streamAbortController = abortController
@@ -1216,6 +1256,7 @@ export default {
1216
         if (llmSid) {
1256
         if (llmSid) {
1217
           this.rememberLlmSessionId(session, llmSid)
1257
           this.rememberLlmSessionId(session, llmSid)
1218
         }
1258
         }
1259
+        costTime = streamResult.durationMs != null ? streamResult.durationMs : null
1219
         this.clearStreamRevealTimer()
1260
         this.clearStreamRevealTimer()
1220
         aiReplyContent = (streamResult.content || aiReplyContent || "").trim() || this.dtT("noContent")
1261
         aiReplyContent = (streamResult.content || aiReplyContent || "").trim() || this.dtT("noContent")
1221
         if (!aiMsg) {
1262
         if (!aiMsg) {
@@ -1246,7 +1287,7 @@ export default {
1246
         this.sending = false
1287
         this.sending = false
1247
         this.$nextTick(() => this.scrollChatBottom())
1288
         this.$nextTick(() => this.scrollChatBottom())
1248
       }
1289
       }
1249
-      await this.persistConsultExchange(persistBody, (aiReplyContent || "").trim())
1290
+      await this.persistConsultExchange(persistBody, (aiReplyContent || "").trim(), costTime)
1250
     },
1291
     },
1251
     extOf(fileName) {
1292
     extOf(fileName) {
1252
       if (!fileName || fileName.lastIndexOf(".") < 0) {
1293
       if (!fileName || fileName.lastIndexOf(".") < 0) {
@@ -1424,6 +1465,25 @@ export default {
1424
   }
1465
   }
1425
 }
1466
 }
1426
 
1467
 
1468
+.session-item__delete {
1469
+  flex-shrink: 0;
1470
+  align-self: center;
1471
+  padding: 4px;
1472
+  margin-left: 4px;
1473
+  color: #909399;
1474
+  opacity: 0;
1475
+  transition: opacity 0.15s, color 0.15s;
1476
+
1477
+  &:hover {
1478
+    color: #f56c6c;
1479
+  }
1480
+}
1481
+
1482
+.session-item:hover .session-item__delete,
1483
+.session-item.is-active .session-item__delete {
1484
+  opacity: 1;
1485
+}
1486
+
1427
 .session-item__avatar {
1487
 .session-item__avatar {
1428
   flex-shrink: 0;
1488
   flex-shrink: 0;
1429
   background: #dcdfe6;
1489
   background: #dcdfe6;