Просмотр исходного кода

feat: 新增课程标题透传功能,优化WebSocket连接和AI对话逻辑

1. 调整websocket地址配置,恢复使用yrs域名
2. 新增courseTitle参数透传,在AI聊天中使用课程上下文
3. 新增互动工具推荐功能,新增对应agent和prompt逻辑
4. 修复生成H5页面相关类型命名,统一为generate_interactive_web
5. 恢复并完善WebSocket token自动更新逻辑
6. 优化注释格式与代码缩进
lsc 3 недель назад
Родитель
Сommit
788b51a649

+ 93 - 11
src/components/CollapsibleToolbar/componets/aiChat.vue

@@ -119,10 +119,10 @@
             </circle>
             </circle>
           </svg>
           </svg>
           <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }"
           <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }"
-            v-if="message.jsonData?.gType !== 'chat' && message.jsonData?.gType !== 'generate_h5page' && !message.chatloading && message.aiContent"
+            v-if="message.jsonData?.gType !== 'chat' && message.jsonData?.gType !== 'generate_interactive_web' && message.jsonData?.gType !== 'generate_tool_recommend' && !message.chatloading && message.aiContent"
             @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm }}</button>
             @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm }}</button>
           <button type="button" class="coco-interactive-web-card"
           <button type="button" class="coco-interactive-web-card"
-            v-if="message.jsonData?.gType === 'generate_h5page' && !message.chatloading" :class="{ 'loading-state': create_app_loading }" @click="!create_app_loading && handle_add_aiWeb()">
+            v-if="message.jsonData?.gType === 'generate_interactive_web' && !message.chatloading" :class="{ 'loading-state': create_app_loading }" @click="!create_app_loading && handle_add_aiWeb()">
             <div class="submenu-icon loading-icon" v-show="create_app_loading">
             <div class="submenu-icon loading-icon" v-show="create_app_loading">
               <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
               <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
                 <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
                 <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
@@ -260,9 +260,11 @@ interface ChatMessage {
 const props = withDefaults(defineProps<{
 const props = withDefaults(defineProps<{
   userid?: string | null
   userid?: string | null
   userJson?: any
   userJson?: any
+  courseTitle?: string | null
 }>(), {
 }>(), {
   userid: null,
   userid: null,
   userJson: null,
   userJson: null,
+  courseTitle: null,
 })
 })
 
 
 
 
@@ -431,7 +433,7 @@ const gType = ref('chat')
 
 
 const sendAction = async (action: string) => {
 const sendAction = async (action: string) => {
   const md = new MarkdownIt()
   const md = new MarkdownIt()
-  if (gType.value === 'chat' || gType.value === 'generate_h5page') {
+  if (gType.value === 'chat' || gType.value === 'generate_interactive_web' || gType.value === 'generate_tool_recommend') {
     const result = chat_no_stream(action, agentid1.value, props.userid || '', lang.lang)
     const result = chat_no_stream(action, agentid1.value, props.userid || '', lang.lang)
     noStreamController.value = result
     noStreamController.value = result
     console.log(result)
     console.log(result)
@@ -450,13 +452,13 @@ const sendAction = async (action: string) => {
     }
     }
 
 
     // generate_qa // generate_choice_question // generate_photo // generate_poll
     // generate_qa // generate_choice_question // generate_photo // generate_poll
-    if (gType.value !== 'chat' && gType.value !== 'generate_h5page') {
+    if (gType.value !== 'chat' && gType.value !== 'generate_interactive_web' && gType.value !== 'generate_tool_recommend') {
       messages.value.at(-1).jsonData = {
       messages.value.at(-1).jsonData = {
         gType: gType.value,
         gType: gType.value,
         isGenerate: false
         isGenerate: false
       }
       }
     }
     }
-    else if (gType.value === 'generate_h5page') {
+    else if (gType.value === 'generate_interactive_web') {
       messages.value.at(-1).jsonData = {
       messages.value.at(-1).jsonData = {
         gType: gType.value,
         gType: gType.value,
         isGenerate: false
         isGenerate: false
@@ -469,8 +471,9 @@ const sendAction = async (action: string) => {
     }
     }
 
 
     if (content.includes(lang.ssAiChatQuickGenWebTitle)) {
     if (content.includes(lang.ssAiChatQuickGenWebTitle)) {
+      gType.value = 'generate_interactive_web'
       messages.value.at(-1).jsonData = {
       messages.value.at(-1).jsonData = {
-        gType: 'generate_h5page',
+        gType: 'generate_interactive_web',
         isGenerate: false
         isGenerate: false
       }
       }
       messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
       messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
@@ -480,6 +483,14 @@ const sendAction = async (action: string) => {
 
 
       return
       return
     }
     }
+
+    if (content.includes(lang.ssAiChatQuickAction5)) {
+      gType.value = 'generate_tool_recommend'
+      messages.value.at(-1).jsonData = {
+        gType: 'generate_tool_recommend',
+        isGenerate: false
+      }
+    }
   }
   }
   else {
   else {
     messages.value.at(-1).jsonData = {
     messages.value.at(-1).jsonData = {
@@ -487,8 +498,62 @@ const sendAction = async (action: string) => {
       isGenerate: false
       isGenerate: false
     }
     }
   }
   }
-
-
+  const getTypeLabel = (type?: number) => {
+    const typeMap: Record<number, string> = {
+      45: lang.ssChoiceQ,
+      15: lang.ssQATest,
+      72: lang.ssAiApp,
+      73: lang.ssHPage,
+      81: lang.ssHPage,
+      74: lang.ssVideo,
+      75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
+      76: lang.ssCreative,
+      77: lang.ssEnglishSpeakingTool,
+      78: lang.ssVote,
+      79: lang.ssPhoto,
+    }
+    return typeMap[type || 0] || lang.ssUnknown
+  }
+  // 提取所有页面中 type 为 'text' 的元素的纯文本内容
+  const allTextContents = slidesStore.slides
+    .map((slide: any, index: number) => {
+      const elements = slide.elements.filter((element: any) => {
+        if (element.type === 'text' || (element.type === 'shape' && element.text && element.text.content)) {
+          return true
+        }
+        // 如果 element.type 包含 frame 且 tooltype 是对应的工具,输出 lang.ssInteract(对应工具)
+        if (element.type === 'frame' && element.toolType) {
+          const toolLabel = getTypeLabel(element.toolType)
+          if (toolLabel !== lang.ssUnknown) {
+            return true
+          }
+        }
+        return false
+      })
+      const pageContents = elements.map((textElement: any) => {
+        let content = ''
+        if (textElement.type === 'shape') {
+          // 创建一个临时元素来解析 HTML 并提取纯文本
+          const tempElement = document.createElement('div')
+          tempElement.innerHTML = textElement.text.content
+          content = tempElement.textContent || tempElement.innerText || ''
+        }
+        else if (textElement.type === 'frame' && textElement.toolType) {
+          // 如果是 frame 类型且有 toolType,输出交互工具标签
+          content = lang.ssInteract + `(${getTypeLabel(textElement.toolType)})`
+        }
+        else {
+          // 创建一个临时元素来解析 HTML 并提取纯文本
+          const tempElement = document.createElement('div')
+          tempElement.innerHTML = textElement.content
+          content = tempElement.textContent || tempElement.innerText || ''
+        }
+        return content
+      }).filter((content: string) => content.trim() !== '')
+      
+      const pageContent = pageContents.length > 0 ? pageContents.join(' ') : '无文本内容'
+      return `第${index + 1}页的内容:${pageContent}`
+    }) || []
 
 
   // 提取当前页面中 type 为 'text' 的元素的纯文本内容
   // 提取当前页面中 type 为 'text' 的元素的纯文本内容
   const textContents = slidesStore.currentSlide?.elements
   const textContents = slidesStore.currentSlide?.elements
@@ -508,12 +573,27 @@ const sendAction = async (action: string) => {
 
 
     })
     })
     .filter(content => content.trim() !== '') || []
     .filter(content => content.trim() !== '') || []
+
+  console.log('allTextContents', allTextContents)
   console.log('textContents', textContents)
   console.log('textContents', textContents)
-  const prompt = `
-  #当前页面内容 ${textContents.length > 0 ? textContents.join('\n') : '无文本内容'}
+  let prompt = ''
+  let agentid = agentid2.value
+  if (gType.value === 'generate_tool_recommend') {
+    agentid = agentid3.value
+    prompt = `
+  #课程标题(course_title): ${props.courseTitle || ''}
+  #当前页面内容(all_pages_content): ${allTextContents.length > 0 ? allTextContents.join('\n') : '无文本内容'}
   #query: ${action}
   #query: ${action}
   `
   `
-  chat_stream(prompt, agentid2.value, props.userid || '', lang.lang, (event) => {
+  }
+  else {
+    prompt = `
+  #课程标题(course_title): ${props.courseTitle || ''}
+  #当前页面内容(current_page_content): ${textContents.length > 0 ? textContents.join('\n') : '无文本内容'}
+  #query: ${action}
+  `
+  }
+  chat_stream(prompt, agentid, props.userid || '', lang.lang, (event) => {
     if (event.type === 'message') {
     if (event.type === 'message') {
       messages.value.at(-1).aiContent = md.render(event.data)
       messages.value.at(-1).aiContent = md.render(event.data)
 
 
@@ -700,6 +780,7 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
 
 
 const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
 const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
 const agentid2 = ref('f86aa63c-b7b7-4d03-9b37-b59f116d36f3')// 生成内容
 const agentid2 = ref('f86aa63c-b7b7-4d03-9b37-b59f116d36f3')// 生成内容
+const agentid3 = ref('caaa43ab-41a5-466a-94bc-74e0c9265068')// 互动工具推荐
 
 
 const isQuickActions = ref<string[]>([])
 const isQuickActions = ref<string[]>([])
 const toggleQuickActions = (section: string) => {
 const toggleQuickActions = (section: string) => {
@@ -795,6 +876,7 @@ onMounted(() => {
   session_name.value = uuidv4()
   session_name.value = uuidv4()
   getAgentModel(agentid1.value)
   getAgentModel(agentid1.value)
   getAgentModel(agentid2.value)
   getAgentModel(agentid2.value)
+  getAgentModel(agentid3.value)
 })
 })
 </script>
 </script>
 
 

+ 3 - 1
src/components/CollapsibleToolbar/index2.vue

@@ -115,7 +115,7 @@
         </div>
         </div>
       </div>
       </div>
       <div class="submenu-content">
       <div class="submenu-content">
-        <AiChat :userid="props.userid" :userJson="userJson" />
+        <AiChat :userid="props.userid" :userJson="userJson" :courseTitle="props.courseTitle" />
       </div>
       </div>
     </div>
     </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'uploadFile' }">
     <div class="submenu" :class="{ visible: activeSubmenu === 'uploadFile' }">
@@ -742,9 +742,11 @@ interface ContentItem {
 const props = withDefaults(defineProps<{
 const props = withDefaults(defineProps<{
   defaultCollapsed?: boolean
   defaultCollapsed?: boolean
   userid?: string | null
   userid?: string | null
+  courseTitle?: string | null
 }>(), {
 }>(), {
   defaultCollapsed: false,
   defaultCollapsed: false,
   userid: null,
   userid: null,
+  courseTitle: null,
 })
 })
 
 
 
 

+ 2 - 2
src/services/course.ts

@@ -1,8 +1,8 @@
 import axios from './config'
 import axios from './config'
 
 
 export const API_URL = 'https://pbl.cocorobo.cn/api/pbl/'
 export const API_URL = 'https://pbl.cocorobo.cn/api/pbl/'
-export const yweb_socket = 'wss://yjs.cocorobo.cn'
-// export const yweb_socket = 'wss://yrs.cocorobo.cn'
+// export const yweb_socket = 'wss://yjs.cocorobo.cn'
+export const yweb_socket = 'wss://yrs.cocorobo.cn'
 
 
 /**
 /**
  * 获取课程详情
  * 获取课程详情

+ 1 - 1
src/views/Editor/index3.vue

@@ -162,7 +162,7 @@
     </div>
     </div>
     <!-- <EditorHeader class="layout-header" /> -->
     <!-- <EditorHeader class="layout-header" /> -->
     <div class="layout-content">
     <div class="layout-content">
-      <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" :userid="props.userid" />
+      <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" :userid="props.userid" :courseTitle="courseTitle" />
       <div class="layout-content-center">
       <div class="layout-content-center">
         <CanvasTool class="center-top"  :userid="props.userid"/>
         <CanvasTool class="center-top"  :userid="props.userid"/>
         <Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 60}px  - 120px)` }"
         <Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 60}px  - 120px)` }"

+ 18 - 18
src/views/Student/index.vue

@@ -4067,8 +4067,8 @@ const getAuthToken = async (): Promise<string> => {
     //   authUrl = '/yjs-auth/auth/token'
     //   authUrl = '/yjs-auth/auth/token'
     // }
     // }
     // else {
     // else {
-      // 生产环境:如果服务器有代理则使用代理,否则直接访问(需要服务器配置 CORS)
-      // 或者通过后端 API 获取 token
+    // 生产环境:如果服务器有代理则使用代理,否则直接访问(需要服务器配置 CORS)
+    // 或者通过后端 API 获取 token
     let baseUrl = 'https://yjsredis.cocorobo.cn/'
     let baseUrl = 'https://yjsredis.cocorobo.cn/'
     baseUrl = baseUrl.replace(/\/+$/, '')
     baseUrl = baseUrl.replace(/\/+$/, '')
     authUrl = `${baseUrl}/auth/token`
     authUrl = `${baseUrl}/auth/token`
@@ -4144,17 +4144,17 @@ const createWebSocketConnection = async (type = 1) => {
     }
     }
     
     
     // 获取认证 token
     // 获取认证 token
-    // try {
-    //   authToken.value = await getAuthToken()
-    //   console.log('🔐 认证 token 获取成功,准备连接 WebSocket')
-    // }
-    // catch (error) {
-    //   console.error('🔐 获取认证 token 失败,连接可能失败:', error)
-    //   connectionStatus.value = 'disconnected'
-    //   isConnecting.value = false
-    //   handleDisconnection()
-    //   return
-    // }
+    try {
+      authToken.value = await getAuthToken()
+      console.log('🔐 认证 token 获取成功,准备连接 WebSocket')
+    }
+    catch (error) {
+      console.error('🔐 获取认证 token 失败,连接可能失败:', error)
+      connectionStatus.value = 'disconnected'
+      isConnecting.value = false
+      handleDisconnection()
+      return
+    }
 
 
     docSocket.value = new Y.Doc()
     docSocket.value = new Y.Doc()
     docSocket.value.gc = true
     docSocket.value.gc = true
@@ -4162,15 +4162,15 @@ const createWebSocketConnection = async (type = 1) => {
       api.yweb_socket,
       api.yweb_socket,
       'PPT' + props.courseid,
       'PPT' + props.courseid,
       docSocket.value,
       docSocket.value,
-      // { params: { yauth: authToken.value } }
+      { params: { yauth: authToken.value } }
     )
     )
     
     
     // 启动定期更新 token
     // 启动定期更新 token
     // 30分钟后再次更新
     // 30分钟后再次更新
-    // if (authTokenUpdateTimer.value) {
-    //   clearTimeout(authTokenUpdateTimer.value)
-    // }
-    // authTokenUpdateTimer.value = setTimeout(updateAuthToken, 30 * 60 * 1000) as unknown as NodeJS.Timeout
+    if (authTokenUpdateTimer.value) {
+      clearTimeout(authTokenUpdateTimer.value)
+    }
+    authTokenUpdateTimer.value = setTimeout(updateAuthToken, 30 * 60 * 1000) as unknown as NodeJS.Timeout
 
 
     providerSocket.value.on('status', (event: any) => {
     providerSocket.value.on('status', (event: any) => {
       console.log('👉 WebSocket状态:', event.status)
       console.log('👉 WebSocket状态:', event.status)