lalala 4 месяцев назад
Родитель
Сommit
13f9d824c9

+ 2 - 0
.env.example

@@ -8,6 +8,8 @@
 VITE_TITLE = '本地开发'
 # 本地后端服务器端口
 VITE_BASE_URL = 'http://localhost:8080'
+# 请求超时(毫秒)。默认 30000;需更快失败可设小些
+# VITE_REQUEST_TIMEOUT = 30000
 # 线上正式服务器地址
 # VITE_BASE_URL = 'https://air-nju.seec.seecoder.cn/'
 # 线上http服务器地址

+ 14 - 5
src/apis/ai.ts

@@ -8,6 +8,12 @@ interface StreamHandlers {
     onComplete?: () => void;
 }
 
+export type StreamRequestOptions = StreamHandlers & {
+    /** 与 userId 一并用于 dialogueId 失效时后端按作业+用户纠偏/补建 Mongo 会话 */
+    assignmentId?: number
+    userId?: number
+}
+
 const extractStreamContent = (payload: unknown): string => {
     if (typeof payload === 'string') {
         return payload
@@ -101,13 +107,16 @@ const aiApis = {
             timeoutErrorMessage: '请求超时,请稍后重试'
         })
     },
-    async requestAIStream(dialogueId: string, model: string, messages: IDialogue[], handlers: StreamHandlers = {}) {
+    async requestAIStream(dialogueId: string, model: string, messages: IDialogue[], options: StreamRequestOptions = {}) {
+        const { onChunk, onComplete, assignmentId, userId } = options
         let processedLength = 0
         let streamBuffer = ''
 
         const response = await axiosInstance.put(`${AI_PREFIX}/stream`, {
             model,
-            messages
+            messages,
+            ...(assignmentId != null ? { assignmentId } : {}),
+            ...(userId != null ? { userId } : {})
         }, {
             params: {
                 dialogueId
@@ -133,7 +142,7 @@ const aiApis = {
 
                 const blocks = streamBuffer.split('\n\n')
                 streamBuffer = blocks.pop() ?? ''
-                blocks.forEach((block) => handleSSEBlock(block, handlers.onChunk))
+                blocks.forEach((block) => handleSSEBlock(block, onChunk))
             }
         })
 
@@ -142,10 +151,10 @@ const aiApis = {
         }
 
         if (streamBuffer.trim()) {
-            handleSSEBlock(streamBuffer, handlers.onChunk)
+            handleSSEBlock(streamBuffer, onChunk)
         }
 
-        handlers.onComplete?.()
+        onComplete?.()
     },
     rewrite(assignmentId: number, userId: number) {
         return axiosInstance.post(`${AI_PREFIX}/rewrite`, {}, {

+ 6 - 1
src/apis/axios.config.ts

@@ -2,13 +2,18 @@ import axios from "axios";
 
 // !!!本地开发不要直接修改baseURL,请看 .env.example中的配置说明!!!
 const BASEURL = import.meta.env.VITE_BASE_URL;
+const REQUEST_TIMEOUT =
+    Number(import.meta.env.VITE_REQUEST_TIMEOUT) > 0
+        ? Number(import.meta.env.VITE_REQUEST_TIMEOUT)
+        : 30_000;
 
 const axiosInstance = axios.create({
     baseURL: BASEURL,
     headers: {
         'Content-Type': 'application/json',
     },
-    timeout: 10000, 
+    // 10s 在访问远程/冷库 / Mongo 拉 AI 对话时易 ECONNABORTED;编辑页会并发多接口
+    timeout: REQUEST_TIMEOUT,
 })
 //请求拦截器
 axiosInstance.interceptors.request.use(

+ 6 - 1
src/components/AIChatter/AIChatter.vue

@@ -154,6 +154,9 @@
     dialogueId: string,
     messages: IDialogue[]
     isTeacher: boolean
+    /** 与 userId 一起传入,供流式在 dialogueId 与 Mongo 不一致时后端补建/纠偏 */
+    assignmentId?: number
+    userId?: number
   }>(), {
     isTeacher: false
   })
@@ -299,7 +302,9 @@
           assistantMessage.content += chunk
           handleMessageList()
           nextTick(scrollToEnd)
-        }
+        },
+        assignmentId: props.assignmentId,
+        userId: props.userId
       })
     } catch (_err:any) {
       console.log(_err);

+ 15 - 15
src/router/index.ts

@@ -16,24 +16,24 @@ const router = createRouter({
     },
 
     // 登录与注册
-    // {
-    //   path: "/",
-    //   redirect: () => {
-    //     window.location.href = "https://p-nju.seec.seecoder.cn/login?from=https://air-nju.seec.seecoder.cn";
-    //    //  window.location.href = "http://localhost:8000/login?from=http://localhost:4000";
-    //     // {本地门户首页地址}?from={本地eai首页地址}
-    //     return "/"; // 占位返回值,实际不会执行
-    // },
-    // },
     {
       path: "/",
-      redirect: "/login"
+      redirect: () => {
+        window.location.href = "https://p-nju.seec.seecoder.cn/login?from=https://air-nju.seec.seecoder.cn";
+       //  window.location.href = "http://localhost:8000/login?from=http://localhost:4000";
+        // {本地门户首页地址}?from={本地eai首页地址}
+        return "/"; // 占位返回值,实际不会执行
     },
-    {
-      path: '/login',
-      name: 'login',
-      component: () => import('../views/LoginPage/LoginPage.vue')
     },
+    // {
+    //   path: "/",
+    //   redirect: "/login"
+    // },
+    // {
+    //   path: '/login',
+    //   name: 'login',
+    //   component: () => import('../views/LoginPage/LoginPage.vue')
+    // },
     {
       path: '/register',
       name: 'register',
@@ -260,7 +260,7 @@ const router = createRouter({
  */
   router.beforeEach(async (to) => {
     const store = useStore();
-    const publicPaths = [ '/login', '/register', '/passwordChange', '/admin/login' ];
+    const publicPaths = [  '/register', '/passwordChange', '/admin/login' ];
     // 1. 检查URL中的token
     const urlToken = new URLSearchParams(window.location.search).get('token');
     if (urlToken) {

+ 41 - 25
src/views/EditPage/EditPage.vue

@@ -8,7 +8,13 @@
   <div class="my-work" >
     <div class="my-work-slider">
       <el-button class="back" color="#4A90C4" style="color: #FFFFFF;" @click="handleBack" :disabled="isExamMode">返回</el-button>
-      <AIChatter :dialogueId="dataForm.dialogueId" :messages="dataForm.messages" :is-teacher="false" />
+      <AIChatter
+        :dialogue-id="dataForm.dialogueId"
+        :messages="dataForm.messages"
+        :is-teacher="false"
+        :assignment-id="assignmentId"
+        :user-id="store.user.id"
+      />
       <Dictionaries />
     </div>
     <div
@@ -251,29 +257,35 @@
       });
   }
 
-  async function fetchData(){
-    await apis.getAIDialogues(assignmentId, store.user.id)
-        .then((_res:any) => {
-          const data = _res.data.data
-          Object.assign(dataForm, {
-            dialogueId: data == null ? '': data?.dialogueId,
-            messages: data == null ? []: data?.messages.content,
-            engagement: dataForm.engagement
-          })
-
-        })
+  async function fetchData() {
+    // 与串行相比,总耗时任取 max 而非 sum,减轻超时概率
+    const [aiResult, enResult] = await Promise.allSettled([
+      apis.getAIDialogues(assignmentId, store.user.id),
+      apis.getEngagement(store.user.id, assignmentId)
+    ])
+
+    if (aiResult.status === 'fulfilled') {
+      const data = (aiResult.value as { data: { data: any } })?.data?.data
+      Object.assign(dataForm, {
+        dialogueId: data == null ? '' : data?.dialogueId,
+        messages: data == null ? [] : data?.messages?.content,
+        engagement: dataForm.engagement
+      })
+    } else {
+      console.log('getAIDialogues 失败', aiResult.reason)
+    }
 
-    await apis.getEngagement(store.user.id, assignmentId)
-        .then((_res:any) => {
-          console.log(_res)
-          Object.assign(dataForm, {
-            dialogueId: dataForm.dialogueId,
-            messages: dataForm.messages,
-            engagement: _res.data.data
-          })
-        }).catch((_err:any) => {
-          console.log("错误", _err)
-        })
+    if (enResult.status === 'fulfilled') {
+      const _res = (enResult as PromiseFulfilledResult<{ data: { data: engagementVO } }>).value
+      console.log(_res)
+      Object.assign(dataForm, {
+        dialogueId: dataForm.dialogueId,
+        messages: dataForm.messages,
+        engagement: _res.data.data
+      })
+    } else {
+      console.log('getEngagement 错误', (enResult as PromiseRejectedResult).reason)
+    }
   }
 
   emitter.on('exam-submitted', () => {
@@ -282,8 +294,12 @@
   })
 
   onMounted(async () => {
-    await fetchAssignmentData();
-    await Promise.all([fetchData(), fetchExamTime()]);
+    // 首屏三路与 fetchData 内两路均并行,避免墙钟时间累加
+    await Promise.all([
+      fetchAssignmentData(),
+      fetchData(),
+      fetchExamTime()
+    ])
 
     if (isExamMode.value && dataForm.engagement.submitted) {
       ElMessage.warning('您已完成交卷,无法再次编辑');

+ 6 - 1
src/views/Home/component/ContentSelector/ContentSelector.vue

@@ -16,7 +16,12 @@
       <!-- 根据selectedContent的值动态显示内容 -->  
       <div v-if="selectedContent === 0" class="content-item">  
         <!-- TODO:调取AI对话内容,并显示在这里 -->  
-        <AIChatter :dialogueId="dataForm.dialogueId" :messages="dataForm.messages" />  
+        <AIChatter
+          :dialogue-id="dataForm.dialogueId"
+          :messages="dataForm.messages"
+          :assignment-id="assignmentId"
+          :user-id="store.user.id"
+        />
       </div>  
     </div>  
   </div>