xsh_1997 2 달 전
부모
커밋
82bc8cf349

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

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

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

@@ -1104,12 +1104,13 @@ export default {
1104 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 1108
       this.pendingPersist = {
1109 1109
         consultSessionId,
1110 1110
         persistBody: { ...persistBody },
1111 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 1116
     flushPendingPersist() {
@@ -1120,14 +1121,15 @@ export default {
1120 1121
       return sendAiConsultMessage(pending.consultSessionId, {
1121 1122
         ...pending.persistBody,
1122 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 1127
         .then(() => {
1126 1128
           this.pendingPersist = null
1127 1129
         })
1128 1130
         .catch(() => {})
1129 1131
     },
1130
-    persistConsultExchange(persistBody, aiReplyContent) {
1132
+    persistConsultExchange(persistBody, aiReplyContent, costTime) {
1131 1133
       const reply = (aiReplyContent || '').trim()
1132 1134
       const consultSessionId = this.consultSessionIdForApi()
1133 1135
       if (!consultSessionId || !persistBody || !reply) {
@@ -1137,7 +1139,8 @@ export default {
1137 1139
       return sendAiConsultMessage(consultSessionId, {
1138 1140
         ...persistBody,
1139 1141
         aiReplyContent: reply,
1140
-        realSessionId: llmSessionId || null
1142
+        realSessionId: llmSessionId || null,
1143
+        costTime: costTime != null ? costTime : undefined
1141 1144
       })
1142 1145
         .then((res) => {
1143 1146
           this.mergePersistResult(res.data, reply)
@@ -1151,7 +1154,7 @@ export default {
1151 1154
           return true
1152 1155
         })
1153 1156
         .catch((err) => {
1154
-          this.queuePendingPersist(consultSessionId, persistBody, reply, llmSessionId)
1157
+          this.queuePendingPersist(consultSessionId, persistBody, reply, llmSessionId, costTime)
1155 1158
           const msg = (err && err.message) || this.$t('aiOnlineConsult.saveSessionFailed')
1156 1159
           uni.showToast({ title: msg, icon: 'none' })
1157 1160
           return false
@@ -1236,6 +1239,7 @@ export default {
1236 1239
 
1237 1240
       let aiReplyContent = ''
1238 1241
       let aiMsg = null
1242
+      let costTime = null
1239 1243
       this.abortStreamRequest()
1240 1244
       this.freezeFeedScrollTopForStream()
1241 1245
       this.sending = true
@@ -1264,6 +1268,7 @@ export default {
1264 1268
         if (llmSid) {
1265 1269
           this.rememberLlmSessionId(llmSid)
1266 1270
         }
1271
+        costTime = streamResult.durationMs != null ? streamResult.durationMs : null
1267 1272
         this.clearStreamRevealTimer()
1268 1273
         aiReplyContent = (streamResult.content || aiReplyContent || '').trim() || this.$t('aiOnlineConsult.noContent')
1269 1274
         if (!aiMsg) {
@@ -1297,7 +1302,7 @@ export default {
1297 1302
         }
1298 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 1307
     validateMediaFile(filePath, fileName, kind) {
1303 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 3
 import { LLM_PROXY_TARGET } from './config/llm.js'
4 4
 
5 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 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 52
  * 从大模型 SSE/JSON 解析会话 id。
44 53
  * 网关流式首包及后续块中的 `id` 即为 sessionId(与 OpenAI chat id 一致)。
@@ -210,6 +219,7 @@ function streamRequestHeaders() {
210 219
 
211 220
 /** 流式:fetch + ReadableStream 逐块解析(axios XHR 常会缓冲到结束才触发 progress) */
212 221
 async function sendChatMessageStream(data, options) {
222
+  const startMs = Date.now()
213 223
   const { onDelta, onSessionId, signal } = options
214 224
   const state = { buffer: "", fullText: "", sessionId: null }
215 225
   const callbacks = { onDelta, onSessionId }
@@ -247,13 +257,18 @@ async function sendChatMessageStream(data, options) {
247 257
   }
248 258
   appendSseChunk(state, decoder.decode(), callbacks)
249 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 268
  * 聊天 POST /v1/chat/completions(默认 stream: true)
255 269
  * - stream: false → JSON(走 request 默认响应)
256 270
  * - 其余情况 → SSE,options 可传 onDelta / onSessionId / signal
271
+ * - 流式 resolve:{ content, sessionId, durationMs },durationMs 为整段流式请求耗时(毫秒)
257 272
  */
258 273
 export function sendChatMessage(data, options = {}) {
259 274
   const body = { stream: true, ...(data || {}) }

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

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

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

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

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

@@ -57,6 +57,14 @@
57 57
                 </div>
58 58
                 <div class="session-item__preview">{{ s.lastMessagePreview || dash }}</div>
59 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 68
             </div>
61 69
             <el-empty
62 70
               v-if="!listLoading && !sessions.length"
@@ -294,6 +302,7 @@ import {
294 302
   createAiConsultSession,
295 303
   listAiConsultMessages,
296 304
   sendAiConsultMessage,
305
+  hideAiConsultSession,
297 306
   sendChatMessage,
298 307
   extractLlmSessionIdFromJson,
299 308
   getModelList
@@ -439,7 +448,7 @@ export default {
439 448
   methods: {
440 449
     getModelList() {
441 450
       getModelList().then((res) => {
442
-        console.log(res)
451
+       
443 452
       })
444 453
     },
445 454
     renderChatMarkdown,
@@ -657,6 +666,33 @@ export default {
657 666
       this.loadMessages(false)
658 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 696
     loadMessages(older) {
661 697
       if (!this.activeSessionId) {
662 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 1021
       this.pendingPersist = {
986 1022
         consultSessionId,
987 1023
         persistBody: { ...persistBody },
988 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 1029
     async flushPendingPersist() {
@@ -998,14 +1035,15 @@ export default {
998 1035
         await sendAiConsultMessage(pending.consultSessionId, {
999 1036
           ...pending.persistBody,
1000 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 1041
         this.pendingPersist = null
1004 1042
       } catch (err) {
1005 1043
         // 离开页面时仍失败则保留队列,下次进入同页可再试(仅内存,刷新会丢)
1006 1044
       }
1007 1045
     },
1008
-    async persistConsultExchange(persistBody, aiReplyContent) {
1046
+    async persistConsultExchange(persistBody, aiReplyContent, costTime) {
1009 1047
       const reply = (aiReplyContent || "").trim()
1010 1048
       if (!this.activeSessionId || !persistBody || !reply) {
1011 1049
         return true
@@ -1015,7 +1053,8 @@ export default {
1015 1053
       const saveBody = {
1016 1054
         ...persistBody,
1017 1055
         aiReplyContent: reply,
1018
-        realSessionId: llmSessionId || null
1056
+        realSessionId: llmSessionId || null,
1057
+        costTime: costTime != null ? costTime : undefined
1019 1058
       }
1020 1059
       try {
1021 1060
         const res = await sendAiConsultMessage(consultSessionId, saveBody)
@@ -1030,7 +1069,7 @@ export default {
1030 1069
         }
1031 1070
         return true
1032 1071
       } catch (err) {
1033
-        this.queuePendingPersist(consultSessionId, persistBody, reply, llmSessionId)
1072
+        this.queuePendingPersist(consultSessionId, persistBody, reply, llmSessionId, costTime)
1034 1073
         const msg = (err && err.message) || this.dtT("saveSessionFailed")
1035 1074
         this.$modal.msgWarning(msg)
1036 1075
         return false
@@ -1191,6 +1230,7 @@ export default {
1191 1230
 
1192 1231
       let aiReplyContent = ""
1193 1232
       let aiMsg = null
1233
+      let costTime = null
1194 1234
       this.abortStreamRequest()
1195 1235
       const abortController = new AbortController()
1196 1236
       this.streamAbortController = abortController
@@ -1216,6 +1256,7 @@ export default {
1216 1256
         if (llmSid) {
1217 1257
           this.rememberLlmSessionId(session, llmSid)
1218 1258
         }
1259
+        costTime = streamResult.durationMs != null ? streamResult.durationMs : null
1219 1260
         this.clearStreamRevealTimer()
1220 1261
         aiReplyContent = (streamResult.content || aiReplyContent || "").trim() || this.dtT("noContent")
1221 1262
         if (!aiMsg) {
@@ -1246,7 +1287,7 @@ export default {
1246 1287
         this.sending = false
1247 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 1292
     extOf(fileName) {
1252 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 1487
 .session-item__avatar {
1428 1488
   flex-shrink: 0;
1429 1489
   background: #dcdfe6;