1
0
tangning 1 месяц назад
Родитель
Сommit
e1e685c1f5
4 измененных файлов с 326 добавлено и 53 удалено
  1. 74 0
      src/util/sse.js
  2. 129 0
      src/util/ssePost.js
  3. 118 53
      src/view/case/records/index.vue
  4. 5 0
      vite.config.ts

+ 74 - 0
src/util/sse.js

@@ -0,0 +1,74 @@
+class SSEClient {
+  constructor(url, options = { }) {
+    this.url = url;
+    this.options = options; // 可选:headers、withCredentials 等
+    this.eventSource = null;
+    this.listeners = new Map(); // 存储事件监听器
+  }
+
+  // 初始化连接
+  connect() {
+    if (this.eventSource) return; // 避免重复连接
+
+    // 支持自定义请求头(如 Token 认证)
+    this.eventSource = new EventSource(this.url, this.options);
+
+    // 监听默认事件
+    this.eventSource.onmessage = (e) => {
+      this.emit('message', e.data);
+    };
+
+    // 连接打开
+    this.eventSource.onopen = () => {
+      this.emit('open');
+    };
+
+    // 错误处理
+    this.eventSource.onerror = (err) => {
+      this.emit('error', err);
+      // 连接关闭时销毁实例
+      if (this.eventSource.readyState === EventSource.CLOSED) {
+        this.destroy();
+      }
+    };
+  }
+
+  // 监听自定义事件
+  on(eventName, callback) {
+    if (!this.listeners.has(eventName)) {
+      this.listeners.set(eventName, []);
+    }
+    this.listeners.get(eventName).push(callback);
+
+    // 注册到 EventSource
+    this.eventSource?.addEventListener(eventName, (e) => {
+      callback(JSON.parse(e.data)); // 统一解析 JSON
+    });
+  }
+
+  // 触发事件(内部使用)
+  emit(eventName, data) {
+    const callbacks = this.listeners.get(eventName);
+    callbacks?.forEach((cb) => cb(data));
+  }
+
+  // 关闭连接并销毁
+  destroy() {
+    if (this.eventSource) {
+      this.eventSource.close();
+      this.eventSource = null;
+      this.listeners.clear();
+      console.log('SSE 连接已销毁');
+    }
+  }
+}
+
+// 导出单例(全局复用一个连接)
+export const sseClient = new SSEClient('/model/workflows/run', {
+  // 可选:携带认证信息(如 Token)
+  // headers: {
+  //   'Authorization': `Bearer ${localStorage.getItem('token')}`
+  // },
+  method: 'POST',
+  withCredentials: true // 跨域时携带 Cookie
+});

+ 129 - 0
src/util/ssePost.js

@@ -0,0 +1,129 @@
+class SSEPostClient {
+  constructor(options = {}) {
+    this.url = 'http://192.168.0.125:1804/model/workflows/run'; // SSE POST 接口地址
+    this.options = {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json', // 可根据需求改为 form-data
+        ...options.headers, // 自定义头(如 Token 认证)
+      },
+      body: JSON.stringify(options.body || {}), // POST 请求体
+      // credentials: options.credentials || 'same-origin', // 跨域携带 Cookie
+    };
+    this.controller = new AbortController(); // 用于中断请求
+    this.signal = this.controller.signal;
+    this.listeners = new Map(); // 事件监听器(message、open、error、close)
+    this.decoder = new TextDecoder('utf-8'); // 解码二进制流
+    this.isConnected = false;
+  }
+
+  // 建立 POST 流式连接
+  async connect() {
+    try {
+      // 发送 POST 请求,开启流式响应
+      const response = await fetch(this.url, {
+        ...this.options,
+        signal: this.signal, // 关联中断信号
+      });
+
+      // 检查响应状态
+      if (!response.ok) {
+        throw new Error(`HTTP 错误!状态码:${response.status}`);
+      }
+
+      // 验证响应格式(必须是流式 event-stream)
+      // if (response.headers.get('Content-Type') !== 'text/event-stream') {
+      //   throw new Error('响应格式错误,需返回 text/event-stream');
+      // }
+
+      // 触发连接成功事件
+      this.isConnected = true;
+      this.emit('open');
+
+      // 解析流式响应(核心)
+      const reader = response.body.getReader();
+      console.log('reader', reader);
+      let remainingData = ''; // 缓存未完整解析的行
+
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) {
+          // 流结束,触发关闭事件
+          this.emit('close');
+          break;
+        }
+
+        // 解码二进制数据为文本,拼接缓存的剩余数据
+        const chunk = this.decoder.decode(value, { stream: true });
+        remainingData += chunk;
+
+        // 按 SSE 格式分割数据(每行以 \n 结尾,每条消息以 \n\n 结尾)
+        const lines = remainingData.split('\n');
+        remainingData = lines.pop() || ''; // 未完整的行留到下一次解析
+
+        // 解析每一行数据(SSE 格式:data: xxx 或 event: xxx)
+        let currentEvent = { type: 'message', data: '' };
+        for (const line of lines) {
+          if (line.trim() === '') {
+            // 空行表示一条消息结束,触发事件
+            if (currentEvent.data) {
+              this.emit(currentEvent.type, currentEvent.data);
+            }
+            currentEvent = { type: 'message', data: '' }; // 重置
+            continue;
+          }
+
+          // 解析 SSE 字段(event: 自定义事件名;data: 消息内容)
+          if (line.startsWith('event:')) {
+            currentEvent.type = line.slice(6).trim();
+          } else if (line.startsWith('data:')) {
+            currentEvent.data += line.slice(5).trim() + '\n'; // 支持多行 data
+          }
+        }
+      }
+    } catch (error) {
+      if (error.name !== 'AbortError') {
+        // 排除手动中断的错误,触发错误事件
+        this.emit('error', error);
+      }
+      this.isConnected = false;
+    }
+  }
+
+  // 监听事件(message、open、error、close、自定义事件)
+  on(eventName, callback) {
+    if (!this.listeners.has(eventName)) {
+      this.listeners.set(eventName, []);
+    }
+    this.listeners.get(eventName).push(callback);
+  }
+
+  // 触发事件(内部使用)
+  emit(eventName, data) {
+    const callbacks = this.listeners.get(eventName);
+    callbacks?.forEach((cb) => {
+      // 自动解析 JSON 格式数据(如果是对象字符串)
+      let parsedData = data;
+      try {
+        parsedData = JSON.parse(data.trim());
+      } catch (e) {
+        // 非 JSON 数据直接返回原文本
+      }
+      cb(parsedData);
+    });
+  }
+
+  // 关闭连接(中断请求)
+  close() {
+    this.controller.abort(); // 中断 fetch 请求
+    this.isConnected = false;
+    this.emit('close');
+    this.listeners.clear(); // 清空监听器
+    console.log('POST 流式连接已关闭');
+  }
+}
+
+// 导出实例(可全局复用,也可按需创建多个)
+export const createSSEPostClient = (url, options) => {
+  return new SSEPostClient(url, options);
+};

+ 118 - 53
src/view/case/records/index.vue

@@ -643,8 +643,10 @@ import { CirclePlus, CircleClose, MagicStick, Download } from "@element-plus/ico
 import { recorderInfoType, ChangeReasonType } from "./formData.ts";
 import { confirm } from "@/helper/message";
 import { chat, abort, listModels } from "@/util/ollama";
+import { createSSEPostClient  } from "@/util/ssePost";
 import { copyTextToClipboard } from "@/util/index";
 const props = defineProps({ caseId: Number, title: String });
+let sseClient = null;
 console.log("router.currentRoute", router.currentRoute.value?.params);
 const fileId = computed(() => router.currentRoute.value?.params?.fileId);
 const caseId = computed(() => router.currentRoute.value?.params?.caseId);
@@ -658,7 +660,8 @@ const aiImgData = ref({
   list: [],
 });
 const caseInfo = computed(() => getCaseInfoData());
-
+const messages = ref([])
+const isConnected = ref();
 const data = reactive({
   title: "",
   inquestNum: "", //现场勘验号
@@ -757,59 +760,121 @@ const handleAI = async () => {
   aiImgData.value.loading = true;
   aiImgData.value.result = "";
   isOption.value = true;
-  const item = aiImgData.value.list.find((i) => i.url == aiImgData.value.src);
-  try {
-    // const res = await getAiByImage({imageUrl: imageUrl})
-    chat("", item.params + item.paramContent).then(async (stream) => {
-      if (!aiImgShow.value) {
-        abort();
-      }
-      for await (const part of stream) {
-        // chatHistory.value.at(idx).text += part.message.content;
-        let tep_mesg = part.message.content;
-        console.log("isThinktep", tep_mesg);
-        //判断是否是deepseek模型
-        // if (testRegex.test(agentInfo.value.model)) {
-        if (tep_mesg == "\u003c/think\u003e") {
-          // isThink.value = true;
-          aiImgData.value.loading = false;
-        }
-        if (!aiImgData.value.loading) {
-          //清除think
-          if (
-            tep_mesg == "\u003cthink\u003e" ||
-            tep_mesg == "\u003c/think\u003e"
-          ) {
-            // chatHistory.value.at(idx).think += "";
-          } else {
-            console.log("isThinktep_mesg", tep_mesg);
-            aiImgData.value.result += tep_mesg;
-            // chatHistory.value.at(idx).think += tep_mesg;
-          }
-          //如果结尾标签则停止拼接
-          if (tep_mesg == "\u003c/think\u003e") {
-            // isThink.value = false;
-          }
-        } else {
-          // aiImgData.value.result += tep_mesg;
-          // chatHistory.value.at(idx).text += tep_mesg;
-        }
-        // } else {
-        //   // chatHistory.value.at(idx).text += tep_mesg;
-        //   aiImgData.value.result += tep_mesg;
-        // }
-        // autoScrollSwitch.value && scrollToBottom(true);
-      }
-      isOption.value = false;
-      console.log("handleAI完成", aiImgData.value.result);
-    });
-    // console.log("handleAI", res)
-    // aiImgData.value.result = res
-    // aiImgData.value.loading = false
-  } catch (error) {
-    console.log("handleAI", error);
-    aiImgData.value.loading = false;
+  // const item = aiImgData.value.list.find((i) => i.url == aiImgData.value.src);
+  initSSE(aiImgData.value.src)
+  // try {
+  // const res = await getAiByImage({imageUrl: imageUrl})
+  //   chat("", item.params + item.paramContent).then(async (stream) => {
+  //     if (!aiImgShow.value) {
+  //       abort();
+  //     }
+  //     for await (const part of stream) {
+  //       // chatHistory.value.at(idx).text += part.message.content;
+  //       let tep_mesg = part.message.content;
+  //       console.log("isThinktep", tep_mesg);
+  //       //判断是否是deepseek模型
+  //       // if (testRegex.test(agentInfo.value.model)) {
+  //       if (tep_mesg == "\u003c/think\u003e") {
+  //         // isThink.value = true;
+  //         aiImgData.value.loading = false;
+  //       }
+  //       if (!aiImgData.value.loading) {
+  //         //清除think
+  //         if (
+  //           tep_mesg == "\u003cthink\u003e" ||
+  //           tep_mesg == "\u003c/think\u003e"
+  //         ) {
+  //           // chatHistory.value.at(idx).think += "";
+  //         } else {
+  //           console.log("isThinktep_mesg", tep_mesg);
+  //           aiImgData.value.result += tep_mesg;
+  //           // chatHistory.value.at(idx).think += tep_mesg;
+  //         }
+  //         //如果结尾标签则停止拼接
+  //         if (tep_mesg == "\u003c/think\u003e") {
+  //           // isThink.value = false;
+  //         }
+  //       } else {
+  //         // aiImgData.value.result += tep_mesg;
+  //         // chatHistory.value.at(idx).text += tep_mesg;
+  //       }
+  //       // } else {
+  //       //   // chatHistory.value.at(idx).text += tep_mesg;
+  //       //   aiImgData.value.result += tep_mesg;
+  //       // }
+  //       // autoScrollSwitch.value && scrollToBottom(true);
+  //     }
+  //     isOption.value = false;
+  //     console.log("handleAI完成", aiImgData.value.result);
+  //   });
+  //   // console.log("handleAI", res)
+  //   // aiImgData.value.result = res
+  //   // aiImgData.value.loading = false
+  // } catch (error) {
+  //   console.log("handleAI", error);
+  //   aiImgData.value.loading = false;
+  // }
+};
+const initSSE = (url) => {
+  // 销毁旧连接(避免重复连接)
+  if (sseClient) {
+    sseClient.close();
   }
+ sseClient = createSSEPostClient({
+    // headers: {
+    //   Authorization: `Bearer ${localStorage.getItem('token')}`, // 认证 Token
+    // },
+    body: {
+      "inputs": {
+        "img": {
+          "type": "image",
+          "transfer_method": "remote_url",
+          "url": 'http://192.168.0.125:1804' + url
+        }
+      },
+      "response_mode": "streaming",
+      "user": "web-4dage"
+    }
+  });
+
+  // 监听连接成功
+  sseClient.on('open', () => {
+    isConnected.value = true;
+    console.log('POST SSE 连接成功');
+  });
+
+  // 监听默认消息事件(服务器未指定 event 时)
+  sseClient.on('message', ({data, outputs}) => {
+    console.log('message连接成功', data);
+    if(data && data.text){
+      aiImgData.value.result += data.text
+    }
+    if(outputs && outputs.text){
+      aiImgData.value.result = outputs.text
+    }
+    aiImgData.value.loading = false
+  });
+
+  // // 监听自定义事件(服务器指定 event: customEvent)
+  // sseClient.on('customEvent', (data) => {
+  //   messages.value.push({ type: '自定义事件', data });
+  // });
+
+  // 监听错误
+  sseClient.on('error', (err) => {
+    isOption.value = false;
+    messages.value.push({ type: '错误', data: err.message });
+    console.error('POST SSE 错误:', err);
+  });
+
+  // 监听连接关闭
+  sseClient.on('close', () => {
+    isOption.value = false;
+    console.log('POST SSE 连接关闭');
+  });
+
+  // 建立连接
+  sseClient.connect();
 };
 const initSignatureAndWitInfo = () => {
   (data.recorderInfo.length === 0 || !data.recorderInfo) &&

+ 5 - 0
vite.config.ts

@@ -57,6 +57,11 @@ export default defineConfig({
         changeOrigin: true,
         secure: false,
       },
+      "/model": {
+        target: url,
+        changeOrigin: true,
+        secure: false,
+      },
       "/service": {
         target: url,
         changeOrigin: true,