瀏覽代碼

feat: 新增useSSE通用hook,用于处理sse请求;新增对话接口,调试问答文字信息展示

hotchicken1996 1 月之前
父節點
當前提交
bce97b97c9

+ 4 - 0
client/.env.dev

@@ -8,6 +8,10 @@ VITE_DEV=false
  VITE_BASE_URL='http://10.10.10.7:18080'
 #VITE_BASE_URL='https://oa.zjugis.com:28080'
 
+
+# AI问答地址
+VITE_AI_BASE_URL='http://10.10.10.7:8518/knowledge'
+
 # 上传路径
 VITE_UPLOAD_URL='/infra/file/upload'
 

+ 2 - 1
client/build/vite/optimize.ts

@@ -104,7 +104,8 @@ const include = [
   'element-plus/es/components/collapse/style/css',
   'element-plus/es/components/collapse-item/style/css',
   'element-plus/es/components/button-group/style/css',
-  'element-plus/es/components/text/style/css'
+  'element-plus/es/components/text/style/css',
+  '@microsoft/fetch-event-source'
 ]
 
 const exclude = ['@iconify/json']

+ 1 - 0
client/package.json

@@ -32,6 +32,7 @@
     "@form-create/designer": "^3.1.3",
     "@form-create/element-ui": "^3.1.24",
     "@iconify/iconify": "^3.1.1",
+    "@microsoft/fetch-event-source": "^2.0.1",
     "@tanstack/vue-query": "^4.32.6",
     "@videojs-player/vue": "^1.0.0",
     "@vue-office/docx": "^1.3.1",

+ 62 - 0
client/src/hooks/web/useSSE.ts

@@ -0,0 +1,62 @@
+import { ref, reactive, onBeforeUnmount } from 'vue'
+import { SSEApi } from '@/interface/ai'
+
+interface UseSSEQueryBody {
+  mutationFn: (body: SSEApi) => void
+  onMessage?: (data: any) => void
+  onError?: (err: any) => void
+}
+
+interface AResult {
+  content: string // 思考过程
+  result: object // 最终结果(含有多个对象,通过状态为key区分)
+}
+
+const useSSE = (body: UseSSEQueryBody) => {
+  const { mutationFn, onMessage, onError } = body
+  const data = reactive<AResult>({ content: '', result: {} })
+  const loading = ref<boolean>(false)
+  const ctrlAbout = new AbortController()
+
+  // 思考过程更新
+  const contentChange = (text: string): void => {
+    data.content += text
+  }
+
+  // 结果更新
+  const resultChange = (key: string, result: object): void => {
+    data.result[key] = result
+  }
+
+  // 对话结束回调
+  const answerDone = () => {
+    loading.value = false
+  }
+
+  const mutation = (paramBody: any) => {
+    loading.value = true
+    mutationFn?.({
+      paramBody,
+      signal: ctrlAbout.signal,
+      onMessage: (result: any) => {
+        onMessage?.(result)
+      },
+      onError: (err) => {
+        console.log('error: ', err)
+        ctrlAbout.abort()
+        onError?.(err)
+      },
+      contentChange,
+      resultChange,
+      answerDone
+    })
+  }
+
+  onBeforeUnmount(() => {
+    ctrlAbout.abort()
+  })
+
+  return { data, ctrlAbout, loading, mutation }
+}
+
+export default useSSE

+ 94 - 0
client/src/interface/ai.ts

@@ -0,0 +1,94 @@
+import { AbortSignal } from 'node/globals'
+
+export interface SSEApi {
+  paramBody: KbChatParam
+  signal: AbortSignal
+  answerDone: () => void // 对话结束回调
+  onMessage?: (data: any) => void
+  onError?: (err: any) => void
+  contentChange?: (text: string) => void // 思考过程更新
+  resultChange?: (key: string, result: object) => void // 结果更新
+}
+
+/**
+ * KbChatParam
+ */
+export interface KbChatParam {
+  enableThinking?: boolean
+  /**
+   * 历史对话
+   */
+  history?: History[]
+  history_keyword?: string[]
+  kb_name?: string
+  maxTokens?: number
+  /**
+   * 知识来源, local_kb,  temp_kb
+   */
+  mode?: string
+  model?: Model
+  prompt_name?: string
+  /**
+   * 要使用的模版名称
+   */
+  promptName?: string
+  /**
+   * 用户问题
+   */
+  query?: string
+  /**
+   * 暂时看不可用
+   */
+  return_direct?: boolean
+  /**
+   * 排序模型打分阈值
+   */
+  score_threshold?: number
+  /**
+   * 搜索类型
+   */
+  search_type?: SearchType
+  stream?: boolean
+  temperature?: number
+  /**
+   * 匹配向量数
+   */
+  topK?: number
+}
+
+/**
+ * com.zjugis.ai.llm.knowledgeqa.param.KbChatParam.History
+ *
+ * History
+ */
+export interface History {
+  content?: string
+  role?: Role
+}
+
+export enum Role {
+  Assistant = 'ASSISTANT',
+  System = 'SYSTEM',
+  Tool = 'TOOL',
+  Unknown = 'UNKNOWN',
+  User = 'USER'
+}
+
+export enum Model {
+  DeepseekR1 = 'DEEPSEEK_R1',
+  Empty = 'EMPTY',
+  Qwen25 = 'QWEN_25',
+  Qwen3 = 'QWEN3',
+  QwenPlus = 'QWEN_PLUS',
+  Qwq32B = 'QWQ_32B',
+  QwqPlus = 'QWQ_PLUS'
+}
+
+/**
+ * 搜索类型
+ */
+export enum SearchType {
+  Concise = 'CONCISE',
+  InDepth = 'IN_DEPTH',
+  Research = 'RESEARCH'
+}

+ 59 - 0
client/src/service/aiService.ts

@@ -0,0 +1,59 @@
+import { SSEApi } from '@/interface/ai'
+import { fetchEventSource } from '@microsoft/fetch-event-source'
+
+const baseUrl = import.meta.env.VITE_AI_BASE_URL
+
+export const kbChat = (body: SSEApi): void => {
+  const { paramBody, signal, onMessage, onError, contentChange, resultChange, answerDone } = body
+
+  const queryBody = {
+    mode: 'local_kb',
+    kb_name: 'oa_ghcg',
+    search_type: '0',
+    top_k: 20,
+    score_threshold: 0.5,
+    history: [],
+    stream: true,
+    model: 'deepseek-r1',
+    return_direct: false,
+    prompt_name: 'rag_context_qa.md',
+    ...paramBody
+  }
+  void fetchEventSource(`${baseUrl}/api/chat/kb_chat`, {
+    method: 'POST',
+    openWhenHidden: true,
+    headers: {
+      'Content-Type': 'application/json'
+    },
+    body: JSON.stringify(queryBody),
+    signal,
+    onopen(e) {
+      console.log('onopen', e)
+      if (e.status !== 200) {
+        // 接口异常代码
+        console.log('请求异常')
+      }
+    },
+    async onmessage(event) {
+      try {
+        const data = JSON.parse(event.data)
+        console.log('onmessage: ', data)
+        onMessage?.(data)
+        const { status, choices } = data
+        if (status === 2) {
+          // 思考过程
+          contentChange?.(choices?.[0]?.delta?.content)
+        } else {
+          resultChange?.(String(status), data)
+          // 对话完结
+          if (status === 3) answerDone?.()
+        }
+      } catch (e: Error) {
+        console.log('内容处理失败', e.message)
+      }
+    },
+    onerror: onError
+  })
+}
+
+// "[[1]] [北仑区十四五土地整治规划(成果稿)1103.pdf](http://127.0.0.1:20333/knowledge_base/download_doc?knowledge_base_name=oa_ghcg&file_name=%E5%8C%97%E4%BB%91%E5%8C%BA%E5%8D%81%E5%9B%9B%E4%BA%94%E5%9C%9F%E5%9C%B0%E6%95%B4%E6%B2%BB%E8%A7%84%E5%88%92%EF%BC%88%E6%88%90%E6%9E%9C%E7%A8%BF%EF%BC%891103.pdf)

+ 1 - 0
client/src/views/OaSystem/aiQA/components/AChat.vue

@@ -26,6 +26,7 @@ import headSculpture from '@/assets/imgs/ai/headSculpture.png'
   }
 
   .content {
+    max-width: 100%;
     padding: 20px;
     background: linear-gradient(180deg, #f1f7ff 0%, #fbfcff 100%);
     border-radius: 8px 8px 8px 8px;

+ 14 - 2
client/src/views/OaSystem/aiQA/components/ChatContent.vue

@@ -1,8 +1,11 @@
 <script setup lang="ts">
+import { watch } from 'vue'
 import AChat from '@/views/OaSystem/aiQA/components/AChat.vue'
 import ExampleChat from '@/views/OaSystem/aiQA/components/ExampleChat.vue'
 import sendImg from '@/assets/imgs/ai/send.png'
 import QChat from '@/views/OaSystem/aiQA/components/QChat.vue'
+import useSSE from '@/hooks/web/useSSE'
+import { kbChat } from '@/service/aiService'
 
 interface ChatContentProps {
   currentQuestion: string
@@ -12,9 +15,10 @@ interface ChatContentProps {
 const props = defineProps<ChatContentProps>()
 const { currentQuestion } = toRefs(props)
 const { changeQuestion } = props
-
 const textarea = ref('')
 
+const { data, mutation: startChat } = useSSE({ mutationFn: kbChat })
+
 const handleClear = () => {
   changeQuestion('')
 }
@@ -23,6 +27,13 @@ const handleSend = () => {
   changeQuestion(textarea.value)
   textarea.value = ''
 }
+
+/*监控问题变化,开启问答*/
+watch(currentQuestion, (newVal) => {
+  if (newVal) {
+    startChat({ query: newVal })
+  }
+})
 </script>
 
 <template>
@@ -30,7 +41,8 @@ const handleSend = () => {
     <div class="message-body">
       <template v-if="currentQuestion">
         <QChat>{{ currentQuestion }}</QChat>
-        <AChat> 生成中 </AChat>
+        <AChat v-if="data?.result?.['3'] == null"> {{ data.content }}</AChat>
+        <AChat v-else> {{ data?.result?.['3']?.choices?.[0]?.delta?.content }}</AChat>
       </template>
       <template v-else>
         <!--   引导对话   -->

+ 0 - 1
client/src/views/OaSystem/aiQA/index.vue

@@ -7,7 +7,6 @@ defineOptions({ name: 'AIQA' })
 const currentQuestion = ref('')
 
 const changeQuestion = (q: string): void => {
-  console.log('changeQuestion: ', q)
   currentQuestion.value = q
 }
 </script>

+ 3 - 4
client/vite.config.ts

@@ -32,11 +32,10 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
       open: env.VITE_OPEN === 'true',
       // 本地跨域代理. 目前注释的原因:暂时没有用途,server 端已经支持跨域
       proxy: {
-        ['/widewebApi']: {
-          target: 'https://www.zjugis.com',
-          ws: false,
+        ['/aiApi']: {
+          target: 'http://10.10.10.7:8518/knowledge/api',
           changeOrigin: true,
-          rewrite: (path) => path.replace(new RegExp(`^/widewebApi`), ''),
+          rewrite: (path) => path.replace(/^\/aiApi/, ''),
         },
       },
     },