cc пре 2 недеља
родитељ
комит
01617ad67f

+ 79 - 15
ai-frontend/src/case/views/data/components/GovernProgressModal.vue

@@ -63,6 +63,7 @@
   import { Icon } from '@/core/components/Icon';
   import { useMessage } from '@/core/hooks/web/useMessage';
   import { calcTask, getGovernTaskSseUrl, testSse } from '@/case/api/govern/governApi';
+  import { getToken } from '@/core/utils/auth';
 
   type GovernRowStatus = 'pending' | 'running' | 'success' | 'fail';
 
@@ -385,22 +386,85 @@
     eventSource?.close();
     sseConnected.value = false;
     errorText.value = '';
-    eventSource = new EventSource(getGovernTaskSseUrl());
-    eventSource.onopen = () => {
-      sseConnected.value = true;
-      errorText.value = '';
-    };
-    eventSource.onmessage = (event) => {
-      handleSseMessage(event.data);
-    };
-    eventSource.onerror = () => {
-      sseConnected.value = false;
-      if (!finishedAt.value && !errorText.value) {
-        errorText.value = 'SSE 连接中断,请稍后重试。';
+    // sa-token 仅从请求头读取 token,原生 EventSource 带不了自定义请求头,
+    // 这里与 cleanProgress.vue 一致改用 fetch 流式读取(EventSource 语义手动对齐)。
+    const controller = new AbortController();
+    // 占位句柄:让 cleanup() 的 eventSource?.close() 语义保持不变,支持 abort 中断
+    const handle = { close: () => controller.abort() } as unknown as EventSource;
+    eventSource = handle;
+
+    void (async () => {
+      try {
+        const token = getToken();
+        const res = await fetch(getGovernTaskSseUrl(), {
+          headers: {
+            Accept: 'text/event-stream',
+            ...(token ? { 'x-token': token as string } : {}),
+          },
+          credentials: 'include',
+          signal: controller.signal,
+        });
+        const contentType = res.headers.get('content-type') || '';
+        if (!res.ok || !contentType.includes('text/event-stream')) {
+          // 未登录等场景后端返回的是普通 Result JSON
+          let msg = 'SSE 连接失败';
+          try {
+            const json = await res.json();
+            msg = json?.message || msg;
+          } catch {
+            /* ignore */
+          }
+          sseConnected.value = false;
+          if (!errorText.value) {
+            errorText.value = msg;
+          }
+          return;
+        }
+        sseConnected.value = true;
+        errorText.value = '';
+        const reader = res.body?.getReader();
+        if (!reader) {
+          errorText.value = 'SSE 连接中断,请稍后重试。';
+          sseConnected.value = false;
+          return;
+        }
+        const decoder = new TextDecoder();
+        let buffer = '';
+        for (;;) {
+          const { value, done } = await reader.read();
+          if (done) break;
+          buffer += decoder.decode(value, { stream: true });
+          let idx = buffer.indexOf('\n\n');
+          while (idx >= 0) {
+            const frame = buffer.slice(0, idx);
+            buffer = buffer.slice(idx + 2);
+            const data = frame
+              .split('\n')
+              .filter((line) => line.startsWith('data:'))
+              .map((line) => line.slice(5).trimStart())
+              .join('\n');
+            if (data) {
+              handleSseMessage(data);
+            }
+            idx = buffer.indexOf('\n\n');
+          }
+        }
+        // 服务端关闭连接:与 EventSource 的 onerror 语义对齐
+        sseConnected.value = false;
+        if (!finishedAt.value && !errorText.value) {
+          errorText.value = 'SSE 连接中断,请稍后重试。';
+        }
+        eventSource = null;
+      } catch (error: any) {
+        if (error?.name !== 'AbortError') {
+          sseConnected.value = false;
+          if (!finishedAt.value && !errorText.value) {
+            errorText.value = 'SSE 连接中断,请稍后重试。';
+          }
+          eventSource = null;
+        }
       }
-      eventSource?.close();
-      eventSource = null;
-    };
+    })();
   };
 
   const startTask = async () => {

+ 2 - 2
ai-server/src/main/java/com/zsjz/ai/common/context/CaseContextHolder.java

@@ -47,14 +47,14 @@ public final class CaseContextHolder {
      * 当前作用域绑定的案件 ID;未绑定时返回 null
      */
     public static Long get() {
-        return CASE_ID.orElse(null);
+        return CASE_ID.isBound() ? CASE_ID.get() : null;
     }
 
     /**
      * 当前作用域绑定的用户 ID;未绑定时返回 null
      */
     public static Long getUserId() {
-        return USER_ID.orElse(null);
+        return USER_ID.isBound() ? USER_ID.get() : null;
     }
 
     // ------------------------------------------------------------------