Pārlūkot izejas kodu

feat: 新增对话日志系列接口,重构问题传参数据结构,调试新增记录接口

hotchicken1996 1 mēnesi atpakaļ
vecāks
revīzija
12ee8bbca8

BIN
client/src/assets/imgs/ai/delete-icon.png


+ 6 - 4
client/src/hooks/web/useSSE.ts

@@ -6,6 +6,7 @@ interface UseSSEQueryBody {
   onMessage?: (data: any) => void
   onError?: (err: any) => void
   onSuccess?: (data: AResultRecord) => void
+  onOpen?: (text: string) => void // 对话开始回调
 }
 
 export interface AResultRecord {
@@ -14,7 +15,7 @@ export interface AResultRecord {
 }
 
 export const useSSE = (body: UseSSEQueryBody) => {
-  const { mutationFn, onMessage, onError, onSuccess } = body
+  const { mutationFn, onMessage, onError, onSuccess, onOpen } = body
   const data = reactive<AResultRecord>({ content: '', result: {} })
   const loading = ref<boolean>(false)
   const ctrlAbout = new AbortController()
@@ -35,7 +36,7 @@ export const useSSE = (body: UseSSEQueryBody) => {
     onSuccess?.(data)
   }
 
-  const mutation = (paramBody: any) => {
+  const mutate = (paramBody: any) => {
     loading.value = true
     data.content = ''
     data.result = {}
@@ -52,7 +53,8 @@ export const useSSE = (body: UseSSEQueryBody) => {
       },
       contentChange,
       resultChange,
-      answerDone
+      answerDone,
+      onOpen
     })
   }
 
@@ -60,5 +62,5 @@ export const useSSE = (body: UseSSEQueryBody) => {
     ctrlAbout.abort()
   })
 
-  return { data, ctrlAbout, loading, mutation }
+  return { data, ctrlAbout, loading, mutate }
 }

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

@@ -6,6 +6,7 @@ export interface SSEApi {
   onError?: (err: any) => void
   contentChange?: (text: string) => void // 思考过程更新
   resultChange?: (key: string, result: object) => void // 结果更新
+  onOpen?: (text: string) => void // 对话开始回调,返回问题
 }
 
 /**

+ 37 - 2
client/src/service/aiService.ts

@@ -1,10 +1,14 @@
 import { SSEApi } from '@/interface/ai'
 import { fetchEventSource } from '@microsoft/fetch-event-source'
+import request from '@/config/axios'
+import { promises } from 'dns'
 
 const baseUrl = import.meta.env.VITE_AI_BASE_URL
+const manageBaseUrl = import.meta.env.VITE_AI_MANAGER_URL
 
 export const kbChat = (body: SSEApi): void => {
-  const { paramBody, signal, onMessage, onError, contentChange, resultChange, answerDone } = body
+  const { paramBody, signal, onMessage, onError, contentChange, resultChange, answerDone, onOpen } =
+    body
 
   const queryBody = {
     mode: 'local_kb',
@@ -32,6 +36,8 @@ export const kbChat = (body: SSEApi): void => {
       if (e.status !== 200) {
         // 接口异常代码
         console.log('请求异常')
+      } else {
+        onOpen?.(paramBody.query)
       }
     },
     async onmessage(event) {
@@ -56,4 +62,33 @@ export const kbChat = (body: SSEApi): void => {
   })
 }
 
-// "[[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)
+export const getChatHistories = async (params?: any) => {
+  const data = await request.get({ url: '/ai/chat/list', params }, '/business')
+  console.log('data: ', data)
+  return data
+}
+
+export const createChatHistory = async (title: string): Promise<string> => {
+  return await request.post({ url: '/ai/chat/create', data: { title } }, '/business')
+}
+
+export const deleteChatHistory = async (id: string) => {
+  const result = await request.get({ url: '/ai/chat/delete', params: { id } }, '/business')
+  return result
+}
+
+export const collectChatHistory = async (data?: any) => {
+  const result = await request.post({ url: '/ai/question/collect', data }, '/business')
+  return result
+}
+
+export const getChatHistory = async (chatId: string) => {
+  const result = await request.get(
+    {
+      url: '/ai/question/getList-by-chatId',
+      params: { chatId }
+    },
+    '/business'
+  )
+  return result
+}

+ 71 - 23
client/src/views/OaSystem/aiQA/components/ChatContent.vue

@@ -6,50 +6,98 @@ 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, type AResultRecord } from '@/hooks/web/useSSE'
-import { kbChat } from '@/service/aiService'
+import { collectChatHistory, createChatHistory, getChatHistory, kbChat } from '@/service/aiService'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
 
 interface ChatContentProps {
-  currentQuestion: string[]
-  changeQuestion: (q: string, init?: boolean) => void
+  currentQuestion: { text: string; id?: string }
+  changeQuestion: (q: string, id?: string) => void
 }
 
+const queryClient = useQueryClient()
 const props = defineProps<ChatContentProps>()
 const { currentQuestion } = toRefs(props)
 const { changeQuestion } = props
 const textarea = ref('')
-const contents = reactive<{ string: AResultRecord }>({}) //对话记录,以问题为key保存
-
-const { data, mutation: startChat } = useSSE({
+const contents = reactive<{ string?: AResultRecord }>({}) //对话记录,以问题为key保存
+const currentQuestionId = ref('')
+
+// 对话
+const {
+  data,
+  mutate: startChat,
+  loading
+} = useSSE({
   mutationFn: kbChat,
   onSuccess: (res) => {
-    const question = currentQuestion.value[currentQuestion.value.length - 1]
+    const question = Object.keys(contents)?.at(-1)
     contents[question] = res
+    //  todo 保存当前历史
+  }
+})
+
+// 创建历史
+const { mutate: createHistory } = useMutation({
+  mutationFn: createChatHistory,
+  onSuccess: (id) => {
+    currentQuestionId.value = id
+    // 刷新历史记录列表
+    void queryClient.invalidateQueries({ queryKey: ['chatHistoryList'] })
+  }
+})
+
+// 更新历史
+const { mutate: collectHistory } = useMutation({
+  mutationFn: collectChatHistory,
+  onSuccess: (res) => {
+    console.log('collectHistory success:', res)
+  }
+})
+
+// 获取历史详情
+useQuery({
+  queryKey: ['getChatHistory', currentQuestion],
+  queryFn: async () => {
+    if ((currentQuestion.value?.id ?? '') === '') return null
+    return await getChatHistory(currentQuestion.value.id)
+  },
+  onSuccess: (res) => {
+    console.log('getChatHistory success:', res)
   }
 })
 
 const handleClear = (): void => {
-  changeQuestion('', true)
+  changeQuestion('')
 }
 
-const handleSend = (): void => {
-  changeQuestion(textarea.value)
+const handleSend = (text): void => {
+  if ((text ?? '') === '') {
+    return
+  }
+  if ((currentQuestionId.value ?? '') === '') {
+    // 创建历史
+    createHistory(text)
+  }
   textarea.value = ''
+  startChat({ query: text })
+  contents[text] = undefined
 }
 
 /*监控问题变化,开启问答*/
 watch(
   currentQuestion,
   (newVal) => {
-    const length = newVal?.length ?? 0
-    const qText = newVal[length - 1]
-    if (qText) {
-      startChat({ query: qText })
-    }
-    if (length <= 1) {
-      //首条对话, 重置对话记录
+    const { text, id } = newVal
+    if ((text ?? '') === '') {
+      //重置对话记录
       Object.keys(contents).forEach((key) => {
         delete contents[key]
       })
+    } else {
+      handleSend(text)
+    }
+    if ((id ?? '') !== '') {
+      // 加载历史记录
     }
   },
   { deep: true }
@@ -59,14 +107,14 @@ watch(
 <template>
   <div class="chat-content">
     <div class="message-body">
-      <template v-if="(currentQuestion?.length ?? 0) > 0">
-        <template v-for="question in currentQuestion" :key="question">
-          <QChat>{{ question }}</QChat>
+      <template v-if="(Object.keys(contents)?.length ?? 0) > 0">
+        <template v-for="(value, key) in contents" :key="key">
+          <QChat>{{ key }}</QChat>
           <!--   思维链    -->
-          <AChat v-if="contents?.[question] == null"> {{ data.content }}...</AChat>
+          <AChat v-if="value == null"> {{ data.content }}...</AChat>
           <AChat v-else>
             <!--     回答结果     -->
-            <AResult :data="contents[question]" />
+            <AResult :data="value" />
           </AChat>
         </template>
       </template>
@@ -89,7 +137,7 @@ watch(
         resize="none"
         placeholder="请输入问题,可以通过回车换行"
       />
-      <div class="q-btn" @click="handleSend">
+      <div class="q-btn" @click="handleSend(textarea)">
         <img :src="sendImg" alt="发送" />
       </div>
     </div>

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

@@ -37,7 +37,7 @@ const entries = [
         class="example-entry"
         v-for="entry in entries"
         :key="entry"
-        @click="changeQuestion(entry, true)"
+        @click="changeQuestion(entry)"
       >
         <img class="icon" :src="fireIcon" alt="" />
         <span>{{ entry }}</span>

+ 40 - 2
client/src/views/OaSystem/aiQA/components/historyChat.vue

@@ -1,11 +1,36 @@
 <script setup lang="ts">
 import newChatIcon from '@/assets/imgs/ai/new_chat_icon.png'
 import msgIcon from '@/assets/imgs/ai/msg-icon.png'
+import deleteIcon from '@/assets/imgs/ai/delete-icon.png'
+import { useMutation, useQuery } from '@tanstack/vue-query'
+import { createChatHistory, deleteChatHistory, getChatHistories } from '@/service/aiService'
 
+interface HistoryChatProps {
+  changeQuestion: (q: string, id?: string) => void
+}
+
+const props = defineProps<HistoryChatProps>()
+const { changeQuestion } = props
+
+const text = ref('')
 const searchKey = ref('')
 const handleNewChat = (): void => {
-  console.log('handleNewChat')
+  changeQuestion('')
 }
+
+const { data, refetch } = useQuery(
+  ['chatHistoryList', searchKey],
+  async () => await getChatHistories()
+)
+
+// 删除历史
+const { mutate: deleteHistory } = useMutation({
+  mutationFn: deleteChatHistory,
+  onSuccess: () => {
+    // 刷新历史记录列表
+    void refetch?.()
+  }
+})
 </script>
 
 <template>
@@ -15,12 +40,13 @@ const handleNewChat = (): void => {
       新建对话
     </div>
     <div class="search-content">
-      <el-input prefix-icon="Search" type="text" v-model="searchKey" placeholder="搜素历史记录" />
+      <el-input prefix-icon="Search" type="text" v-model="text" placeholder="搜素历史记录" />
     </div>
     <div class="histories-content">
       <div class="history-body">
         <img class="icon" :src="msgIcon" alt="" />
         <span class="text">村庄规划的编制要点是什么?</span>
+        <!--        <img class="delete-btn" :src="deleteIcon" alt="" />-->
       </div>
     </div>
     <div class="footer">
@@ -89,6 +115,7 @@ const handleNewChat = (): void => {
     overflow-y: auto;
 
     .history-body {
+      position: relative;
       width: 100%;
       margin-top: 12px;
       display: flex;
@@ -106,8 +133,19 @@ const handleNewChat = (): void => {
         margin-right: 8px;
       }
 
+      .delete-btn {
+        position: absolute;
+        right: 10px;
+        display: none;
+        width: 16px;
+      }
+
       &:hover {
         background-color: #e9f0fb;
+
+        .delete-btn {
+          display: block;
+        }
       }
     }
   }

+ 5 - 9
client/src/views/OaSystem/aiQA/index.vue

@@ -4,21 +4,17 @@ import ChatContent from '@/views/OaSystem/aiQA/components/ChatContent.vue'
 
 defineOptions({ name: 'AIQA' })
 
-const currentQuestion = ref([])
+const currentQuestion = reactive({ text: '', id: '' })
 
-const changeQuestion = (q: string, init?: boolean): void => {
-  if (init) {
-    currentQuestion.value = []
-  }
-  if ((q ?? '') !== '') {
-    currentQuestion.value.push(q)
-  }
+const changeQuestion = (q: string, id = ''): void => {
+  currentQuestion.text = q
+  currentQuestion.id = id
 }
 </script>
 
 <template>
   <div class="ai-q-a-page">
-    <HistoryChat />
+    <HistoryChat :changeQuestion="changeQuestion" />
     <div class="right-part">
       <ChatContent :currentQuestion="currentQuestion" :changeQuestion="changeQuestion" />
     </div>