Bläddra i källkod

feat: 聊天接口数据 append 至服务器

王晓东 1 månad sedan
förälder
incheckning
d15a3d2d1f

+ 6 - 6
src/components/chat-message/MessageRobot.tsx

@@ -2,15 +2,15 @@ import { View, Image, Text } from "@tarojs/components";
 import style from './index.module.less'
 import IconCopy from "@/components/icon/icon-copy";
 import IconSpeaker from "@/components/icon/icon-speaker";
-import { ICharacter } from "@/types";
+import { TAgentDetail } from "@/types/agent";
 import Taro from "@tarojs/taro";
 import ThinkAnimation from '../think-animation/index'
 interface Props {
-  character: ICharacter,
+  agent?: TAgentDetail,
   text: string,
   textReasoning: string,
 }
-export default ({character, text, textReasoning=''}:Props) => {
+export default ({agent, text, textReasoning=''}:Props) => {
   const handleCopy = (e: any)=> {
     e.stopPropagation();
     // 手动复制并 toast 提示
@@ -35,15 +35,15 @@ export default ({character, text, textReasoning=''}:Props) => {
   return <View>
       <View className="flex gap-8 mb-10">
         <View className={style.avatarContainer}>
-          {character?.avatar && (
+          {agent?.avatarUrl && (
             <Image
               mode="aspectFill"
               className={style.avatar}
-              src={character.avatar}
+              src={agent.avatarUrl}
             />
           )}
         </View>
-        <View className="font-medium text-16 leading-24">{character?.name}</View>
+        <View className="font-medium text-16 leading-24">{agent?.name}</View>
       </View>
 
       <View className={`${style.message } ${style.messageRobot} gap-10`}>

+ 8 - 5
src/components/chat-message/index.tsx

@@ -1,16 +1,19 @@
-import { ICharacter } from "@/types";
+
+import { EChatRole, TChatRole } from "@/types/bot";
 import Message from "./Message";
 import MessageRobot from "./MessageRobot";
+import { TAgentDetail } from "@/types/agent";
 interface Props {
-  character?: ICharacter,
+  agent?: TAgentDetail,
+  role: TChatRole,
   text: string
   textReasoning?: string
 }
-export default ({character, text, textReasoning=''}:Props) => {
-  if(!character){
+export default ({agent, text, role, textReasoning=''}:Props) => {
+  if(role === EChatRole.User ){
     return <Message text={text}/>
   }
-  return <MessageRobot textReasoning={textReasoning} text={text} character={character}/>
+  return <MessageRobot textReasoning={textReasoning} text={text} agent={agent}/>
 }
 
 

+ 2 - 2
src/components/voice-recorder/index.ts

@@ -5,7 +5,7 @@ interface H5RecordResult {
 }
 type TOnStopRes = (Taro.RecorderManager.OnStopCallbackResult & H5RecordResult) | null
 let audioCtx:Taro.WebAudioContext = Taro.createWebAudioContext();
-export const useVoiceRecord = ()=> {
+export const useVoiceRecord = (format: keyof Taro.RecorderManager.Format = 'mp3')=> {
   let recorder = Taro.getRecorderManager()
   let volumeChangeCallback: (volume: number, n:any)=> void
   let recorderStopCallback: (res: TOnStopRes)=> void
@@ -87,7 +87,7 @@ export const useVoiceRecord = ()=> {
 
       encodeBitRate: 96000, //编码码率
 
-      format: 'mp3', //音频格式,有效值 aac/mp3
+      format: format, //音频格式,有效值 aac/mp3
 
       frameSize: 1
     })

+ 4 - 4
src/pages/agent-gen/components/step/StepConfirm.tsx

@@ -62,10 +62,10 @@ export default React.memo(function Index({prev, pickedAvatar}:IProps) {
         </View>
         <View className={style.confirmChatAvatarBg}>
           <Image
-                mode="widthFix"
-                className="w-full"
-                src={pickedAvatar.avatarUrl}
-              ></Image>
+            mode="widthFix"
+            className="w-full"
+            src={pickedAvatar.avatarUrl}
+          ></Image>
           <View className={style.confirmChatAvatarBgCover}>
               <View className={style.block1}></View>
               <View className={style.block2}></View>

+ 23 - 7
src/pages/chat/components/chat-welcome/index.tsx

@@ -3,7 +3,26 @@ import { View } from '@tarojs/components'
 import style from './index.module.less'
 import PersonalCard from '../personal-card'
 import MessageRobotPlain from '@/components/chat-message/MessageRobotPlain'
+import { useAgentStore } from '@/store/agentStore'
 export default ()=> {
+  const {agent} = useAgentStore()
+
+  if(!agent){
+    return <></>
+  }
+
+  let greeting = ''
+  // 默认打招呼文案
+  if(!agent.greeting){
+    greeting = `Hi~ 你好我是${agent.name}`
+  }else{
+    // 用户自定义文案
+    greeting = agent.greeting
+  }
+
+  // 3个引导提问提示词
+  const questions = agent.questionGuides ?? []
+  
   return (
     <View className='pt-118'>
     <View className={style.container}>
@@ -11,13 +30,10 @@ export default ()=> {
           <PersonalCard />
         </View>
         <View className='flex flex-col gap-8'>
-          <MessageRobotPlain text='Hi~我是张三,
-  想要咨询近视眼手术的事宜,都可以向我提问,茗视光眼科医院是北京出名的眼科医院,可以放心咨询~🎉🎉🎉' />
-          <MessageRobotPlain text='Hi~我是张三,
-  想要咨询近视眼手术的事宜,都可以向我提问,茗视光眼科医院是北京出名的眼科医院,可以放心咨询~🎉🎉🎉' />
-          <MessageRobotPlain text='医院可以做哪些近视手术?' />
-          <MessageRobotPlain text='怎么知道自己能不能做手术?' />
-          <MessageRobotPlain text='痛不痛?马上能看清吗?' />
+          <MessageRobotPlain text={greeting}/>
+          {questions.map(q => {
+            return <MessageRobotPlain text={q} />
+          })}
         </View>
     </View> 
    </View>

+ 11 - 10
src/pages/chat/components/input-bar/VoiceInputBar.tsx

@@ -4,17 +4,19 @@ import { useVoiceRecord } from "@/components/voice-recorder";
 import { useState } from "react";
 import { getChatSession } from "@/store/chat";
 import style from './index.module.less'
-import { audioRecognition } from "@/service/audio";
+import { speechToText } from "@/service/bot";
+import { isSuccess } from "@/utils";
 interface Props {
   disabled: boolean,
   onIconClick: () => void
   onSend: (msg: string)=> void
   beforeSend: ()=> void
   onError: ()=> void
+  agentId?: string
 }
-export default ({disabled, onIconClick, onSend, beforeSend, onError}:Props) => {
+export default ({agentId,disabled, onIconClick, onSend, beforeSend, onError}:Props) => {
   const [speechStatus, setSpeechStatus] = useState(0);
-  const { start, stop, onStop } = useVoiceRecord();
+  const { start, stop, onStop } = useVoiceRecord('wav');
   const onLongPress = (e?: CommonEvent) => {
     e?.stopPropagation();
     setSpeechStatus(1);
@@ -36,17 +38,16 @@ export default ({disabled, onIconClick, onSend, beforeSend, onError}:Props) => {
 
   onStop(async (res) => {
     console.log("record stop:", res);
-    const chatSession = getChatSession()
-    beforeSend()
-    if (!res || !chatSession) {
+    if (!agentId || !res) {
       return;
     }
+    beforeSend()
+    
     
     // 以二进制方式读取文件
-    const r = await audioRecognition(res.tempFilePath)
-    console.log(r);
-    if(r.code === 0){
-      const msg = r.data?.text ?? ''
+    const response = await speechToText(agentId, res.tempFilePath)
+    if(isSuccess(response.status)){
+      const msg = response.data?.text ?? ''
       if(!msg.length){
         return;
       }

+ 77 - 32
src/pages/chat/components/input-bar/index.tsx

@@ -2,20 +2,25 @@ import { useState } from "react";
 import TextInputBar from "./TextInputBar";
 import VoiceInputBar from "./VoiceInputBar";
 import { textChat } from "@/service/bot";
-import { useTextChat } from "@/store/textChat";
+import { TMessage,TRobotMessage, useTextChat } from "@/store/textChat";
 import { TAgentDetail } from "@/types/agent";
-import { delay, generateRandomId, getLoginId } from "@/utils";
+import { delay, getLoginId } from "@/utils";
 import { EAI_MODEL } from "@/consts/enum";
 import { useUnload } from "@tarojs/taro";
-import { EChatRole, EContent } from "@/types/bot";
+import { EChatRole, EContentType } from "@/types/bot";
+
+import { saveMessageToServer } from './message'
+
+
 interface Props {
   agent: TAgentDetail | null;
   aiModel: EAI_MODEL;
+  histories: TMessage|TRobotMessage[];
   setShowWelcome: (b: boolean) => void;
 }
 
 let stopReceiveChunk: (() => void) | undefined;
-export default ({ agent, setShowWelcome }: Props) => {
+export default ({ agent, setShowWelcome, histories }: Props) => {
   const [isVoice, setIsVoice] = useState(false);
   const [disabled, setDisabled] = useState(false);
   const {
@@ -27,7 +32,7 @@ export default ({ agent, setShowWelcome }: Props) => {
     updateMessage,
     deleteMessage,
   } = useTextChat();
-  // const currentRobotMessageId = useTextChat((state)=> state.currentRobotMessageId);
+  
   let myMessageId = "";
 
   const handleTextInputBarSwitch = () => {
@@ -39,9 +44,9 @@ export default ({ agent, setShowWelcome }: Props) => {
     setIsVoice(false);
   };
 
-  const chatWithGpt = async (message: string) => {
+  const chatWithGpt = async (message: string, sessionId: string, msgUk: string) => {
     setShowWelcome(false)
-    let currentRobotMessageId = "";
+    let currentRobotMsgUk = "";
     await delay(300);
     setDisabled(true);
     if (!agent?.agentId) {
@@ -52,19 +57,29 @@ export default ({ agent, setShowWelcome }: Props) => {
       return;
     }
 
-    const greeting = "欢迎光临我的智能体,你想问什么?";
-    const reqMessages = [
-      {
-        content: greeting,
-        contentType: EContent.TextPlain,
-        role: EChatRole.System,
-      },
-      {
-        content: message,
-        contentType: EContent.TextPlain,
-        role: EChatRole.User,
-      },
-    ];
+    // const greeting = "欢迎光临我的智能体,你想问什么?";
+    // {
+    //   content: greeting,
+    //   contentType: EContentType.TextPlain,
+    //   role: EChatRole.System,
+    // },
+    
+    const newMsg = {
+      content: message,
+      contentType: EContentType.TextPlain,
+      role: EChatRole.User,
+    }
+    
+    saveMessageToServer({
+      loginId,
+      messages: [{
+        ...newMsg,
+        isStreaming: false,
+        msgUk,
+      }],
+      agentId: agent.agentId,
+      sessionId,
+    })
 
     // 发起文本聊天
     stopReceiveChunk = textChat({
@@ -74,11 +89,13 @@ export default ({ agent, setShowWelcome }: Props) => {
         isEnableSearch: false,
         isEnableThinking: false,
         loginId,
-        messages: reqMessages,
-        sessionId: generateRandomId(),
+        messages: [newMsg],
+        sessionId,
       },
       onStart: () => {
-        currentRobotMessageId = pushRobotMessage({
+        currentRobotMsgUk = pushRobotMessage({
+          role: EChatRole.Assistant,
+          saveStatus: 2,
           content: "",
           reasoningContent: "",
           robot: {
@@ -93,7 +110,7 @@ export default ({ agent, setShowWelcome }: Props) => {
         if (m.reasoningContent) {
           updateRobotReasoningMessage(
             m.reasoningContent,
-            currentRobotMessageId
+            currentRobotMsgUk
           );
         } else {
           updateRobotMessage(m.content);
@@ -102,30 +119,56 @@ export default ({ agent, setShowWelcome }: Props) => {
       onFinished: () => {
         console.log("回复完毕 ok");
         const currentRobotMessage = getCurrentRobotMessage();
+        console.log(currentRobotMessage,333)
+        if(!agent.agentId){
+          return
+        }
+        setDisabled(false);
         // 如果没有任何回答,则显示
         if (!currentRobotMessage?.content?.length) {
           updateRobotMessage("服务器繁忙...");
+          return 
         }
-        setDisabled(false);
+
+        saveMessageToServer({
+          loginId,
+          messages: [{
+            content: currentRobotMessage.content,
+            contentType: EContentType.TextPlain,
+            isStreaming: false,
+            role: currentRobotMessage.role,
+            msgUk,
+          }],
+          agentId: agent.agentId,
+          sessionId,
+        })
+        
       },
       onError: () => {
-        deleteMessage(currentRobotMessageId);
+        deleteMessage(currentRobotMsgUk);
         setDisabled(false);
       },
     });
   };
   const handleVoiceSend = (message: string) => {
-    updateMessage(message, myMessageId);
-    chatWithGpt(message);
+    // updateMessage(message, myMessageId);
+    // chatWithGpt(message);
   };
-  const handleOnSend = (message: string) => {
-    pushMessage(message);
-    chatWithGpt(message);
+  const handleOnSend = async (message: string) => {
+    if(!agent?.agentId){
+      return
+    }
+    const {sessionId, msgUk} = pushMessage(message);
+    chatWithGpt(message, sessionId, msgUk);
   };
 
   // 推一个自己的空气泡框
   const handleBeforeSend = () => {
-    myMessageId = pushMessage("");
+    if(!agent?.agentId){
+      return
+    }
+    const {msgUk} = pushMessage("");
+    myMessageId = msgUk
   };
 
   // 发生主意识别错误时,删除当前自己发出的气泡框
@@ -140,9 +183,11 @@ export default ({ agent, setShowWelcome }: Props) => {
   });
 
   if (isVoice) {
+    console.log(agent)
     // 语音输入
     return (
       <VoiceInputBar
+        agentId={agent?.agentId}
         disabled={disabled}
         onSend={handleVoiceSend}
         beforeSend={handleBeforeSend}

+ 5 - 0
src/pages/chat/components/input-bar/message.ts

@@ -0,0 +1,5 @@
+import { appendMessages } from "@/service/bot"
+import type { TMessageHistories, TRequestBody, TAppendMessages } from "@/types/bot";
+export const saveMessageToServer = async(data: TAppendMessages) => {
+  await appendMessages(data)
+}

+ 14 - 8
src/pages/chat/components/personal-card/index.tsx

@@ -1,41 +1,47 @@
 
-import { View } from '@tarojs/components'
+import { Image, View } from '@tarojs/components'
 
 import TagCertificated from "@/components/tag-certificated";
 import IconCertificateColor from "@/components/icon/icon-certificate-color";
+import { useAgentStore } from '@/store/agentStore';
 interface IProps {
   size?: 'mini'|'large'
 }
 export default ({size='large'}:IProps) => {
+  
+  const {agent} = useAgentStore()
+
   const renderContent = () => {
     if(size === 'mini'){
     return <>
-      <View className="rounded-full overflow-hidden w-28 h-28 bg-black-6">
+        <View className="rounded-full overflow-hidden w-28 h-28">
+          <Image src={agent?.avatarLogo} mode='widthFix' className='w-28 h-28'></Image>
         </View>
         <View className="flex flex-col flex-1 gap-4">
           <View className="flex items-end gap-4">
-            <View className="text-12 font-medium leading-12">张三</View>
+            <View className="text-12 font-medium leading-12">{agent?.name}</View>
             <View className="text-12 leading-12"><IconCertificateColor/></View>
           </View>
           <View className="flex items-center gap-2">
             <View className="text-gray-45 text-12 leading-12 truncate max-w-[180px]">
-              北京茗视光眼科医院管理有限公司
+              {agent?.entName}
             </View>
           </View>
         </View>
     </>
     }
     return <>
-      <View className="rounded-full overflow-hidden w-60 h-60 bg-black-6">
+        <View className="rounded-full overflow-hidden w-60 h-60">
+          <Image src={agent?.avatarLogo} mode='widthFix' className='w-60 h-60'></Image>
         </View>
         <View className="flex flex-col flex-1">
           <View className="flex items-end gap-8">
-            <View className="text-24 font-medium leading-32">张三</View>
-            <View className="text-12 leading-20">销售医师</View>
+            <View className="text-24 font-medium leading-32">{agent?.name}</View>
+            <View className="text-12 leading-20">{agent?.position}</View>
           </View>
           <View className="flex items-center gap-2">
             <View className="text-12 leading-20 truncate max-w-[188px]">
-              北京茗视光眼科医院管理有限公司
+              {agent?.entName}
             </View>
             <TagCertificated />
           </View>

+ 86 - 0
src/pages/chat/history.ts

@@ -0,0 +1,86 @@
+import {
+  getMessageHistories,
+  type TGetMessageHistoriesParams,
+} from "@/service/bot";
+import { isSuccess } from "@/utils";
+
+import useSWRInfinite from "swr/infinite";
+import { EChatRole } from "@/types/bot";
+import type { TMessage, TRobotMessage } from "@/store/textChat";
+import { TAgentDetail } from "@/types/agent";
+import { useEffect, useRef } from "react";
+export const useChatHistory = (agent: TAgentDetail, setScrollTop: ()=> void)=> {
+  // 1. key 生成函数
+  const getKey = (pageIndex, previousPageData) => {
+    // 没有更多数据时停止
+    if (
+      previousPageData &&
+      (!previousPageData.nextId || loadedCount >= totalCount)
+    )
+      return null;
+    return {
+      agentId: agent.agentId,
+      startId: pageIndex === 0 ? undefined : previousPageData.nextId,
+      pageSize: 10,
+    };
+  };
+
+  // 2. fetcher
+  const fetcher = async (params) => {
+    const res = await getMessageHistories(params);
+    return res.data;
+  };
+
+  // 3. SWR Infinite
+  const { data, size, setSize, isValidating } = useSWRInfinite(
+    getKey,
+    fetcher,
+    {
+      revalidateAll: false,
+      revalidateFirstPage: false,
+    }
+  );
+
+  // 4. 处理数据
+  const messages:TMessage|TRobotMessage[] = data ? data.flatMap((page) => page.data) : [];
+  const totalCount = data?.[0]?.totalCount || 0;
+  const loadedCount = messages.length;
+  const hasMore = loadedCount < totalCount;
+
+  let histories:TMessage|TRobotMessage[] = []
+  if(messages.length){
+    histories = messages.map(message => {
+      if(message.role === EChatRole.User){
+        return {...message, saveStatus: 2}
+      }
+      if(message.role === EChatRole.Assistant){
+        return {...message, robot: {
+          avatar: agent?.avatarUrl ?? "",
+          name: agent?.name ?? "",
+          agentId: agent?.agentId ?? "",
+        }, saveStatus: 2}
+      }
+      return message
+    })
+    
+  }
+  
+  // 用 ref 记录上一次 histories 长度
+  const prevHistoriesLength = useRef(0);
+
+  useEffect(() => {
+    // 只在 histories 长度变大时(即加载更多历史)设置 scrollTop
+    if (histories.length > prevHistoriesLength.current) {
+      setScrollTop();
+      prevHistoriesLength.current = histories.length;
+    }
+  }, [histories.length, setScrollTop]);
+
+  return {
+    isValidating,
+    histories,
+    hasMore,
+    size,
+    setSize,
+  }
+}

+ 46 - 86
src/pages/chat/index.tsx

@@ -11,84 +11,53 @@ import { useTextChat } from "@/store/textChat";
 import { EAI_MODEL } from "@/consts/enum";
 import type { TMessage, TRobotMessage } from "@/store/textChat";
 
-import ChatWelcome from './components/chat-welcome'
+import ChatWelcome from "./components/chat-welcome";
 import IconArrowLeft from "@/components/icon/icon-arrow-left";
-import PersonalCard from './components/personal-card'
+import PersonalCard from "./components/personal-card";
 import { useAgentStore } from "@/store/agentStore";
+
+import { useChatHistory } from './history'
+
+
+
 // 类型谓词函数
-function isRobotMessage(message: TMessage | TRobotMessage): message is TRobotMessage {
-  return 'robot' in message && 'reasoningContent' in message;
+function isRobotMessage(
+  message: TMessage | TRobotMessage
+): message is TRobotMessage {
+  return "robot" in message && "reasoningContent" in message;
 }
 
 export default function Index() {
+  const agent = useAgentStore((state) => state.agent);
+  if(!agent?.agentId){
+    return <View>...</View>
+  }
   const [deepThink, setDeepThink] = useState(EAI_MODEL.DeepseekChat);
-  const [showWelcome, setShowWelcome] = useState(true);
+  
   const [keyboardHeight, setKeyboardHeight] = useState(0);
-  const [contentHeight, setContentHeight] = useState(0);
-  const [scrollViewHeight, setScrollViewHeight] = useState(0);
   const scrollViewRef = useRef<any>(null);
   // const headerHeight = useAppStore((state) => state.headerHeight);
-  const agent = useAgentStore((state)=> state.agent)  
+  
   const messageList = useTextChat((state) => state.list);
-  const { destroy } = useTextChat();
+  const { destroy, setScrollTop  } = useTextChat();
   const scrollTop = useTextChat((state) => state.scrollTop);
-  const { pushRobotMessage } = useTextChat();
 
+  const {setSize, hasMore, isValidating, histories, size} = useChatHistory(agent, setScrollTop)
+  const [showWelcome, setShowWelcome] = useState(!histories.length);
   
-
-  // 计算 marginTopOffset 偏移的距离
-  const marginTopOffset = (() => {
-    if (keyboardHeight <= 0) return 0;
-    // 如果内容超过滚动容器,取键盘弹起高度
-    if(contentHeight > scrollViewHeight){
-      return  -(keyboardHeight)
-    }
-    // 如果内容+键盘弹起高度超过滚动容器, 则取其差值
-    if( contentHeight + keyboardHeight > scrollViewHeight){ 
-      // 内容+键盘弹起高度 - 滚动容器高度
-      return -(contentHeight + keyboardHeight - scrollViewHeight)  
+  
+  // 5. onScrollToUpper 加载更多
+  const onScrollToUpper = () => {
+    if (hasMore && !isValidating) {
+      setSize(size + 1);
     }
-  })();
+  };
+  
 
   useUnload(() => {
     destroy();
   });
 
-  useEffect(() => {
-    // 监听键盘高度变化
-    Taro.onKeyboardHeightChange((res) => {
-      if(res.height <= 0){
-        return setKeyboardHeight(0);
-      }
-      setShowWelcome(false)
-      setKeyboardHeight(res.height - 24);
-    });
-
-    return () => {
-      // 清理监听器
-      Taro.offKeyboardHeightChange();
-    };
-  }, []);
-
-  // 监听内容高度和 ScrollView 高度变化
-  useEffect(() => {
-    if (scrollViewRef.current) {
-      const query = Taro.createSelectorQuery();
-      // 获取聊天内容高度
-      query.select('#messageList').boundingClientRect((rect: any) => {
-        if (rect) {
-          setContentHeight(rect.height);
-        }
-      }).exec();
-      // 获取滚动容器高度
-      query.select('#scrollView').boundingClientRect((rect: any) => {
-        if (rect) {
-          setScrollViewHeight(rect.height);
-        }
-      }).exec();
-    }
-  }, [messageList]);
-
   const switchDeepThink = () => {
     if (deepThink === EAI_MODEL.DeepseekChat) {
       setDeepThink(EAI_MODEL.DeepseekReasoner);
@@ -97,33 +66,16 @@ export default function Index() {
     setDeepThink(EAI_MODEL.DeepseekChat);
   };
 
-  // useEffect(() => {
-  //   // 主动打招呼
-  //   if (agent) {
-  //     const greetingText = `Hi~我是${agent.name},和我聊聊吧~`;
-  //     pushRobotMessage({
-  //       content: greetingText,
-  //       reasoningContent: "",
-  //       robot: {
-  //         avatar: agent.avatarUrl ?? "",
-  //         name: agent.name ?? "",
-  //         agentId: agent.agentId ?? "",
-  //       },
-  //     });
-  //   }
-  // }, []);
-
-  
-  const renderNavLeft = ()=> {
+  const renderNavLeft = () => {
     return (
       <View className="flex items-center gap-8">
         <IconArrowLeft />
-        <View className={showWelcome ? 'hidden' : 'block'}>
+        <View className={showWelcome ? "hidden" : "block"}>
           <PersonalCard size="mini" />
         </View>
       </View>
-    )
-  }
+    );
+  };
 
   return (
     <PageCustom>
@@ -139,16 +91,19 @@ export default function Index() {
           }}
           scrollTop={scrollTop}
           scrollWithAnimation
+          onScrollToUpper={onScrollToUpper}
         >
-          {showWelcome && <ChatWelcome/>}
+          {showWelcome && <ChatWelcome />}
           <View id="messageList" className="flex flex-col gap-8 px-18 pb-140">
-            {messageList.map((message) => {
+            
+            {[...histories, ...messageList].map((message) => {
               const robotMessage = isRobotMessage(message) ? message : null;
               return (
                 <ChatMessage
-                  key={message.messageId}
+                  key={message.msgUk}
                   textReasoning={robotMessage?.reasoningContent}
-                  character={robotMessage?.robot}
+                  agent={agent}
+                  role={message.role}
                   text={message.content}
                 ></ChatMessage>
               );
@@ -158,8 +113,8 @@ export default function Index() {
         <View className="h-140 w-10"></View>
         <View
           className="fixed left-0 right-0 bottom-0 min-h-130 z-50"
-          style={{ 
-            bottom: `${keyboardHeight}px`
+          style={{
+            bottom: `${keyboardHeight}px`,
           }}
         >
           <View
@@ -172,7 +127,12 @@ export default function Index() {
           >
             深度思考(R1)
           </View>
-          <InputBar aiModel={deepThink} agent={agent}  setShowWelcome={setShowWelcome}></InputBar>
+          <InputBar
+            aiModel={deepThink}
+            agent={agent}
+            histories={histories}
+            setShowWelcome={setShowWelcome}
+          ></InputBar>
         </View>
       </View>
     </PageCustom>

+ 1 - 1
src/service/audio.ts

@@ -27,6 +27,6 @@ export const voiceTryout = (voice: string, configs?:any) => {
 // asr 语音识别
 export const audioRecognition = async (tmpPath: string, format: string = 'mp3') => {
   
-  const content = await getTempFileContent<number>(tmpPath, 'hex')
+  const content = await getTempFileContent<number>(tmpPath, 'base64')
   return service.post<{data: number, format:string}, {text:string, duration: number}>(`/v1/audio/asr`, {data: content, format})
 }

+ 16 - 17
src/service/bot.ts

@@ -3,14 +3,11 @@ import Taro from "@tarojs/taro";
 import { getSimpleHeader } from "@/xiaolanbenlib/module/axios.js";
 import JsonChunkParser from "@/utils/jsonChunkParser";
 import request from "@/xiaolanbenlib/module/axios.js";
-import type { TMessage, TRequestBody } from "@/types/bot";
+import type { TMessageHistories, TRequestBody, TAppendMessages } from "@/types/bot";
 import { TextDecoder } from "text-encoding-shim";
+import { getTempFileContent } from "@/utils";
+
 
-export type TMessageHistories = {
-  data: TMessage[][];
-  nextId: string;
-  totalCount: 0;
-};
 
 // 获取与指定智能体的历史会话记录--按智能体维度倒序返回
 export type TGetMessageHistoriesParams = {
@@ -27,17 +24,7 @@ export const getMessageHistories = (data: TGetMessageHistoriesParams) => {
 
 // 保存消息--追加覆盖保存模式
 
-export type TAppendMessages = {
-  "agentId": string,
-  "loginId": string,
-  "messages": {
-    "content": string,
-    "isStreaming": false,
-    "msgUk": string,
-    "role": string
-  }[ ],
-  "sessionId": string
-}
+
 export const appendMessages = (data: TAppendMessages) => {
   return request.post(`${bluebookAiAgent}api/v1/chat/messages/append`, data)
 }
@@ -66,6 +53,18 @@ export const likeMessage = (data: TLikeMessage) => {
 }
 
 
+export const speechToText = async (agentId: string, tempFilePath: string) => {
+  const content = await getTempFileContent<string>(tempFilePath, 'base64')
+  return request.post<{
+    emotions: string[],
+    text: string
+  }>(`${bluebookAiAgent}api/v1/chat/speech/text`, {
+    agentId,
+    speechBase64: content,
+  })
+}
+
+
 
 
 

+ 54 - 31
src/store/textChat.ts

@@ -3,13 +3,19 @@
  */
 
 import { create } from "zustand";
-import { generateRandomId } from '@/utils/index'
+import { generateUUID } from '@/utils/index'
+
+import { getMessageHistories as _getMessageHistories, appendMessages, type TGetMessageHistoriesParams,  } from '@/service/bot'
+import { EChatRole, EContentType, TChatRole } from "@/types/bot";
+
 
-import { getMessageHistories as _getMessageHistories, type TGetMessageHistoriesParams,  } from '@/service/bot'
 
 export type TMessage = {
-  messageId: string;
+  msgUk: string;
   content: string;
+  role: TChatRole
+  isStreaming?: boolean
+  saveStatus: 0|1|2,
 };
 
 export type TRobotMessage = {
@@ -21,22 +27,25 @@ export type TRobotMessage = {
   };
 } & TMessage;
 
-type TRobotMessageWithOptionalId = Omit<TRobotMessage, "messageId"> & {
-  messageId?: string; // 将 messageId 设置为可选
+type TRobotMessageWithOptionalId = Omit<TRobotMessage, "msgUk"> & {
+  msgUk?: string; // 将 messageId 设置为可选
   reasoningContent?: string; // 添加 reasoningContent
 };
 
 
-const INIT_CURRENT_ROBOT_MESSAGE_ID = ''
+const INIT_CURRENT_ROBOT_MSG_UK = ''
 
 export interface TextChat {
-  currentRobotMessageId: string; // 当前正在说话的 AI 机器人 id, 可用于控制是否继续输出文本至当前 message 框内
+  currentRobotMsgUk: string; // 当前正在说话的 AI 机器人 id, 可用于控制是否继续输出文本至当前 message 框内
   scrollTop: number; // 控制聊天内容变化后滚动至最底部
   list: (TMessage | TRobotMessage)[];
+  sessionId: string|null
+  // 显示聊天历史
+  setScrollTop: () => void
   // 将机器人气泡框推入聊天框
   pushRobotMessage: (message: TRobotMessageWithOptionalId) => string;
   // 将自己发出的气泡框推入聊天框
-  pushMessage: (content: string) => string;
+  pushMessage: (content: string) => {msgUk: string, sessionId: string};
   // 更新自己发出的气泡框
   updateMessage: (content: string, messageId: string) => string;
   // 更新机器人汽泡框内的内容实现 gpt 的效果
@@ -50,27 +59,29 @@ export interface TextChat {
 }
 
 // 新messageId 为 index 加 1
-const generateMessageId = () => {
-  return generateRandomId();
+const generateUk = () => {
+  return generateUUID();
 }
 
 export const useTextChat = create<TextChat>((set, get) => ({
-  currentRobotMessageId: INIT_CURRENT_ROBOT_MESSAGE_ID,
+  currentRobotMsgUk: INIT_CURRENT_ROBOT_MSG_UK,
   scrollTop: 9999,
   list: [],
+  sessionId: null,
   destroy: () => {
     set({
       list: [],
-      currentRobotMessageId: INIT_CURRENT_ROBOT_MESSAGE_ID,
+      currentRobotMsgUk: INIT_CURRENT_ROBOT_MSG_UK,
       scrollTop: 9999,
+      sessionId: null,
     });
   },
   pushRobotMessage: (message) => {
-    const messageId = generateMessageId()
+    const msgUk = generateUk()
     set((state) => {
       return {
-        list: [...state.list, { ...message, messageId }],
-        currentRobotMessageId: messageId,
+        list: [...state.list, { ...message, msgUk, role: EChatRole.Assistant }],
+        currentRobotMsgUk: msgUk,
         scrollTop: state.scrollTop + 1,
       };
     });
@@ -81,13 +92,24 @@ export const useTextChat = create<TextChat>((set, get) => ({
         };
       });
     }, 100)
-    return messageId
+    return msgUk
+  },
+  setScrollTop: ()=> {
+    set((state) => {
+      return {
+        scrollTop: state.scrollTop + 1,
+      };
+    });
   },
   pushMessage: (content: string) => {
-    const messageId = generateMessageId()
+    const msgUk = generateUk();
+    const sessionId = generateUUID();
+    const newMessage:TMessage ={ msgUk, content: content, role: EChatRole.User, saveStatus: 0 }
+    
     set((state) => {
       return {
-        list: [...state.list, { messageId, content: content }],
+        sessionId,
+        list: [...state.list,  newMessage],
         scrollTop: state.scrollTop + 1,
       };
     });
@@ -98,24 +120,25 @@ export const useTextChat = create<TextChat>((set, get) => ({
         };
       });
     }, 100)
-    return messageId
+    
+    return {msgUk, sessionId}
   },
-  updateMessage: (content, messageId) => {
+  updateMessage: (content, msgUk) => {
     set((state) => {
       const updatedList = state.list.map((message) => {
-        if (message.messageId === messageId) {
+        if (message.msgUk === msgUk) {
           return { ...message, content: message.content + content }; // 更新 content
         }
         return message; // 返回未修改的 message
       });
       return { list: updatedList, scrollTop: state.scrollTop + 1 }; // 返回新的状态
     });
-    return messageId
+    return msgUk
   },
   updateRobotMessage: (content) => {
     set((state) => {
       const updatedList = state.list.map((message) => {
-        if (message.messageId === state.currentRobotMessageId) {
+        if (message.msgUk === state.currentRobotMsgUk) {
           return { ...message, content: message.content + content }; // 更新 content
         }
         return message; // 返回未修改的 message
@@ -125,15 +148,15 @@ export const useTextChat = create<TextChat>((set, get) => ({
   },
   getCurrentRobotMessage: ()=> {
     const state = get()
-    const currentMessage = state.list.find((message)=>  message.messageId === state.currentRobotMessageId) as TRobotMessage
+    const currentMessage = state.list.find((message)=>  message.msgUk === state.currentRobotMsgUk) as TRobotMessage
     return currentMessage
   },
-  updateRobotReasoningMessage: (reasoningContent, messageId) => {
+  updateRobotReasoningMessage: (reasoningContent, msgUk) => {
     set((state) => {
-      // 由于 currentRobotMessageId 可能发生变化, 所以需要传递 messageId
-      // console.log(state.currentRobotMessageId, messageId)
+      // 由于 currentRobotMsgUk 可能发生变化, 所以需要传递 msgUk
+      // console.log(state.currentRobotMsgUk, msgUk)
       const updatedList = state.list.map((message) => {
-        if (message.messageId === messageId) {
+        if (message.msgUk === msgUk) {
           //@ts-ignore
           return { ...message, reasoningContent: message.reasoningContent + reasoningContent }; // 更新 reasoningContent
         }
@@ -142,19 +165,19 @@ export const useTextChat = create<TextChat>((set, get) => ({
       return { list: updatedList, scrollTop: state.scrollTop + 1 }; // 返回新的状态
     });
   },
-  deleteMessage: (messageId)=> {
+  deleteMessage: (msgUk)=> {
     set((state) => {
       // 如果对话框是空的,则删除
       const filtered = state.list.filter((message) => {
         const isEmptyContent = message.content.length <= 0
-        return (message.messageId !== messageId && isEmptyContent)
+        return (message.msgUk !== msgUk && isEmptyContent)
       });
       return { list: filtered, scrollTop: state.scrollTop + 1 }; // 返回新的状态
     });
   },
   // 停止当前机器人说话输出框输出
   stopCurrentRobotMessaging: ()=> {
-    set({currentRobotMessageId: INIT_CURRENT_ROBOT_MESSAGE_ID})
+    set({currentRobotMsgUk: INIT_CURRENT_ROBOT_MSG_UK})
   },
   getMessageHistories: async (data: TGetMessageHistoriesParams) => {
     const response = await _getMessageHistories(data)

+ 1 - 0
src/types/agent.ts

@@ -28,6 +28,7 @@ export type TAgentDetail = {
   address?: string,
   agentId?: string,
   avatarUrl?: string,
+  avatarLogo?: string,
   components?: TComponentItem[],
   email?: string,
   enabledChatBg?: boolean,

+ 23 - 2
src/types/bot.ts

@@ -1,3 +1,5 @@
+
+
 export type TMessage = {
   content: string,
   dislikeReason: string,
@@ -13,7 +15,7 @@ export type TMessage = {
 
 
 
-export enum EContent {
+export enum EContentType {
   TextPlain = 'text/plain',
   ApplicationJson = 'application/json',
   AiseekAudioChunk = 'aiseek/audio_chunk',
@@ -36,7 +38,7 @@ export enum EChatRole {
 // 或兼容写法:
 export type TChatRole = EChatRole[keyof EChatRole];
 
-export type TContentType = EContent[keyof EContent];
+export type TContentType = EContentType[keyof EContentType];
 
 
 export type TRequestMessage = {
@@ -55,3 +57,22 @@ export type TRequestBody = {
   sessionId: string;
 };
 
+export type TAppendMessage  = {
+    content: any,
+    contentType: TContentType,
+    isStreaming: false,
+    msgUk: string,
+    role: TChatRole
+}
+export type TAppendMessages = {
+  agentId: string,
+  loginId: string,
+  messages: TAppendMessage[],
+  sessionId: string
+}
+
+export type TMessageHistories = {
+  data: TMessage|TRobotMessage[];
+  nextId: string;
+  totalCount: 0;
+};

+ 46 - 0
src/utils/dataFetcher.ts

@@ -0,0 +1,46 @@
+import useSWR, { SWRConfiguration } from 'swr';
+
+type Fetcher<Data> = () => Promise<Data>;
+
+export function useDataFetcher<Data = any, Error = any>(
+  url: string | null,
+  options?: SWRConfiguration<Data, Error>
+) {
+  const fetcher: Fetcher<Data> = async () => {
+    const res = await fetch(url!);
+    if (!res.ok) throw new Error('请求失败');
+    return res.json();
+  };
+
+  const { data, error, isLoading, mutate } = useSWR<Data, Error>(
+    url,
+    fetcher,
+    {
+      // 禁用自动请求
+      initialData: null, // 初始值设为 null
+      revalidateOnMount: false, // 挂载时不验证
+      revalidateIfStale: false, // 不自动重新验证
+      revalidateOnFocus: false, // 聚焦时不验证
+      revalidateOnReconnect: false, // 重连时不验证
+      ...options // 合并用户自定义配置
+    }
+  );
+
+  // 手动触发请求的函数
+  const triggerFetch = async (opts?: {
+    throwOnError?: boolean;
+    optimisticData?: Data;
+  }) => {
+    return mutate(undefined, {
+      revalidate: true,
+      ...opts
+    });
+  };
+
+  return {
+    data,
+    error,
+    isLoading,
+    triggerFetch
+  };
+}    

+ 10 - 29
src/utils/index.ts

@@ -366,36 +366,17 @@ export function generateRandomId(): string {
 
 
 /**
- * 生成符合 RFC 4122 标准的 UUID v4
- * @returns 生成的 UUID 字符串
+ * 生成符合 RFC 4122 版本 4 的 UUID
+ * @returns 格式为 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' 的 UUID 字符串
  */
-export const generateUUID = (): string => {
-  // 创建16字节(128位)的 ArrayBuffer
-  const buffer = new ArrayBuffer(16);
-  const view = new DataView(buffer);
-  
-  // 使用 Taro 的 getRandomValues 方法填充随机值
-  // 注意:Taro 3+ 版本可能需要使用 Taro.getRandomValues
-  const randomValues = Taro.getRandomValues(new Uint8Array(16));
-  for (let i = 0; i < 16; i++) {
-    view.setUint8(i, randomValues[i]);
-  }
-
-  // 设置版本号(第6-7位为4)
-  view.setUint8(6, (view.getUint8(6) & 0x0f) | 0x40);
-  // 设置变体(第8-9位为10)
-  view.setUint8(8, (view.getUint8(8) & 0x3f) | 0x80);
+export function generateUUID(): string {
+  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+    const r = (Math.random() * 16) | 0;
+    const v = c === 'x' ? r : (r & 0x3) | 0x8;
+    return v.toString(16);
+  });
+}
 
-  // 转换为UUID格式字符串
-  const hex: string[] = [];
-  for (let i = 0; i < 16; i++) {
-    hex.push(view.getUint8(i).toString(16).padStart(2, '0'));
-  }
-  
-  return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${
-    hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${
-    hex.slice(10, 16).join('')}`;
-};
 
 
 export const pickNonEmpty = <T extends object>(obj: T): Partial<T> => {
@@ -406,7 +387,7 @@ export const pickNonEmpty = <T extends object>(obj: T): Partial<T> => {
   ) as Partial<T>;
 };
 
-export const getLoginId = ()=> {
+export const getLoginId = () => {
   return Taro.getStorageSync(LOGIN_ID_STORAGE_KEY) // 打开小程序时创建 login_uuid 
 }