Pārlūkot izejas kodu

refactor: 聊天输入逻辑

王晓东 1 mēnesi atpakaļ
vecāks
revīzija
605f0b880b

+ 3 - 3
project.private.config.json

@@ -9,9 +9,9 @@
     "miniprogram": {
       "list": [
         {
-          "name": "pages/agent/index",
-          "pathName": "pages/agent/index",
-          "query": "agentId=p_2e73c9d7efaYfDo2-agent_1016",
+          "name": "pages/contact/index",
+          "pathName": "pages/contact/index",
+          "query": "",
           "scene": null,
           "launchMode": "default"
         }

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

@@ -18,7 +18,7 @@ import { EContentType, TMessage } from "@/types/bot";
 import { getLoginId, isSuccess } from "@/utils";
 import { useState } from "react";
 import { AvatarMedia } from "../AvatarMedia";
-import { useTextChat } from "@/store/textChat";
+import { useTextChat } from "@/store/textChatStore";
 import { useTextToSpeech } from './textToSpeech'
 import { handleCopy } from '@/utils/messageUtils'
 interface Props {
@@ -33,7 +33,7 @@ export default ({ agent, text, message, showUser=false, textReasoning = "" }: Pr
   const [isLike, setIsLike] = useState(message.isLike);
   const [isSpeaking, setIsSpeaking] = useState(false);
   const loginId = getLoginId();
-  const { setMessageRespeaking, setMessageSpeakersStopHandle, messageSpeakersStopHandle, isReacting } = useTextChat()
+  const { setMessageRespeaking, setMessageStopHandle, messageStopHandle, isReacting } = useTextChat()
   const { startSpeech, stopSpeech, onPlayerStatusChanged } = useTextToSpeech()
 
   
@@ -51,8 +51,8 @@ export default ({ agent, text, message, showUser=false, textReasoning = "" }: Pr
       return;
     }
 
-    if(messageSpeakersStopHandle){
-      messageSpeakersStopHandle()
+    if(messageStopHandle){
+      messageStopHandle()
     }
     if(isSpeaking){
       stopSpeech()
@@ -70,7 +70,7 @@ export default ({ agent, text, message, showUser=false, textReasoning = "" }: Pr
       loginId: loginId,
     })
 
-    setMessageSpeakersStopHandle(stopSpeech)
+    setMessageStopHandle(stopSpeech)
     
     setIsSpeaking(true)
     setMessageRespeaking(true)
@@ -164,12 +164,6 @@ export default ({ agent, text, message, showUser=false, textReasoning = "" }: Pr
 
       <View className={`${style.message} ${style.messageRobot} gap-10`}>
         <View className={`${style.messageContent}`}>
-          {/* {textReasoning && <View className={style.deepThinkContainer}>
-            <View className="font-bold">深度思考:</View>
-            <Text>
-              {textReasoning}
-            </Text>
-          </View>} */}
 
           {
             text.length === 0 ? <ThinkAnimation></ThinkAnimation> : <>
@@ -183,7 +177,7 @@ export default ({ agent, text, message, showUser=false, textReasoning = "" }: Pr
           <View onClick={(e) => handleCopy(e, text)}>
             <IconCopy />
           </View>
-          <View onClick={(e) => handlePlayAudio(e, text)}>
+          <View className={isReacting ? 'text-opacity-60' : ''} onClick={(e) => handlePlayAudio(e, text)}>
             <IconSpeaker color={isSpeaking} />
           </View>
           {/* <IconSpeaker></IconSpeaker> */}

+ 2 - 2
src/components/chat-message/index.tsx

@@ -12,9 +12,9 @@ interface Props {
 }
 export default ({agent, text, role, message, textReasoning=''}:Props) => {
   if(role === EChatRole.User ){
-    return <Message text={text}/>
+    return <Message text={text as string}/>
   }
-  return <MessageRobot message={message} textReasoning={textReasoning} text={text} agent={agent}/>
+  return <MessageRobot message={message} textReasoning={textReasoning} text={text as string} agent={agent}/>
 }
 
 

+ 2 - 2
src/components/chat-message/textToSpeech.ts

@@ -4,7 +4,7 @@ import { useAudioStreamPlayer } from "@/utils/audioStreamPlayer";
 // 在消息框内点击播放音频消息 
 export const useTextToSpeech = () => {
   // 聊天消息流式音频播报
-  const { setFistChunk, pushBase64ToQuene, playChunk, stopPlayChunk, setEndChunk, onPlayerStatusChanged } =
+  const { setFistChunk, pushBase64ToQuene, playChunk, stopPlayChunk, setIsLastChunk, onPlayerStatusChanged } =
     useAudioStreamPlayer();
 
   
@@ -42,7 +42,7 @@ export const useTextToSpeech = () => {
       onComplete: async () => {
         isFirstChunk = true;
         // chunk 接收已结束
-        setEndChunk()
+        setIsLastChunk()
       },
       onError: () => {},
     });

+ 124 - 0
src/pages/chat/ARCHITECTURE_FIX.md

@@ -0,0 +1,124 @@
+# 聊天页面架构修复说明
+
+## 🚨 发现的问题
+
+### 多实例问题 (Critical Issue)
+
+在重构过程中发现了一个严重的架构问题:`useChatInput` Hook 被多个组件同时使用,导致创建了多个独立实例:
+
+1. **InputBar/index.tsx** - 输入栏组件
+2. **RecommendQuestions/index.tsx** - 推荐问题组件  
+3. **ChatGreeting/index.tsx** - 欢迎问候组件
+
+### 问题影响
+
+1. **重复副作用**: 每个实例都会执行相同的 `useEffect` 清理逻辑
+2. **状态管理冲突**: 多个实例操作同一个全局 `useTextChat` store
+3. **资源浪费**: 多个定时器、事件监听器同时运行
+4. **竞态条件**: 多个实例可能同时修改聊天状态,导致不可预测的行为
+
+## 🔧 解决方案: 状态提升 (State Lifting)
+
+### 修复策略
+
+采用 React 最佳实践中的"状态提升"模式:
+
+1. **单一实例**: 将 `useChatInput` 提升到聊天页面主组件中,确保只有一个实例
+2. **Props 传递**: 通过 props 将需要的方法传递给子组件
+3. **状态集中管理**: 在主组件中统一管理 InputBar 的状态(isVoice, disabled)
+
+### 修复前后对比
+
+#### 修复前 ❌
+```typescript
+// InputBar 组件
+const {handleBeforeSend, sendChatMessage} = useChatInput({agent, ...});
+
+// RecommendQuestions 组件  
+const {sendChatMessage, setQuestions} = useChatInput({agent, ...});
+
+// ChatGreeting 组件
+const {sendChatMessage} = useChatInput({agent});
+```
+**问题**: 3个独立的 `useChatInput` 实例同时运行
+
+#### 修复后 ✅
+```typescript
+// 主组件 (chat/index.tsx)
+const chatInputActions = useChatInput({
+  agent,
+  enableOutputAudioStream: streamVoiceEnable,
+  setShowWelcome,
+  setIsVoice,
+  setDisabled,
+});
+
+// 子组件通过 props 接收
+<InputBar chatInputActions={chatInputActions} />
+<RecommendQuestions chatInputActions={chatInputActions} />
+<ChatGreeting chatInputActions={chatInputActions} />
+```
+**优势**: 单一 `useChatInput` 实例,通过 props 共享
+
+### 架构改进
+
+```
+Before:
+┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
+│   InputBar      │    │RecommendQuestions│   │  ChatGreeting   │
+│                 │    │                 │    │                 │
+│ useChatInput()  │    │ useChatInput()  │    │ useChatInput()  │
+│      ↓          │    │      ↓          │    │      ↓          │
+│ [Instance 1]    │    │ [Instance 2]    │    │ [Instance 3]    │
+└─────────────────┘    └─────────────────┘    └─────────────────┘
+         ↓                       ↓                       ↓
+    ┌─────────────────────────────────────────────────────────┐
+    │              useTextChat (Global Store)               │
+    │                   ⚠️  Conflicts!                      │
+    └─────────────────────────────────────────────────────────┘
+
+After:
+┌─────────────────────────────────────────────────────────────┐
+│                    Chat Page (Main)                        │
+│                                                             │
+│  const chatInputActions = useChatInput({...})              │
+│                        ↓                                    │
+│                 [Single Instance]                          │
+│                        ↓                                    │
+│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │
+│  │  InputBar   │  │Recommend... │  │ChatGreeting │        │
+│  │             │  │             │  │             │        │
+│  │ props: {...}│  │ props: {...}│  │ props: {...}│        │
+│  └─────────────┘  └─────────────┘  └─────────────┘        │
+└─────────────────────────────────────────────────────────────┘
+                            ↓
+    ┌─────────────────────────────────────────────────────────┐
+    │              useTextChat (Global Store)               │
+    │                   ✅  Clean!                           │
+    └─────────────────────────────────────────────────────────┘
+```
+
+## 📊 修复效果
+
+### 性能提升
+- **内存使用**: 减少了2个额外的 Hook 实例
+- **副作用**: 从3个 `useEffect` 清理函数减少到1个
+- **定时器**: 避免了多个定时器的冲突
+
+### 代码质量
+- **可维护性**: 统一的状态管理,更容易调试和维护
+- **可预测性**: 消除了多实例导致的竞态条件
+- **架构清晰**: 清晰的数据流向,符合 React 最佳实践
+
+### 类型安全
+- **接口定义**: 明确的 `chatInputActions` 接口定义
+- **Props 传递**: 类型安全的 props 传递机制
+
+## 🎯 最佳实践总结
+
+1. **单一职责**: 每个 Hook 实例应该只负责一个特定的业务逻辑
+2. **状态提升**: 当多个组件需要共享状态时,将状态提升到最近的共同父组件
+3. **Props 传递**: 通过 props 显式传递依赖,而不是在每个组件中创建独立实例
+4. **副作用管理**: 确保副作用(useEffect)只在需要的地方执行一次
+
+这次修复不仅解决了多实例问题,还提高了代码的整体架构质量,使其更符合 React 的设计理念。

+ 128 - 0
src/pages/chat/README.md

@@ -0,0 +1,128 @@
+# 聊天页面重构说明
+
+## 重构概述
+
+原始的聊天页面组件(`index.tsx`)包含了大量的业务逻辑,代码行数超过350行,难以维护。通过专业的重构方法,我们将复杂的逻辑分离到不同的自定义 Hooks 中,使代码更加模块化、可维护和可测试。
+
+## 架构设计
+
+### 📁 文件结构
+
+```
+src/pages/chat/
+├── hooks/                    # 自定义 Hooks
+│   ├── index.ts             # 统一导出
+│   ├── useChatMessages.ts   # 消息管理
+│   ├── useChatScrollManager.ts # 滚动管理
+│   ├── useChatUI.ts         # UI状态管理
+│   └── useChatAgent.ts      # 智能体数据管理
+├── constants/               # 常量配置
+│   └── index.ts            # 页面常量
+├── components/              # 组件
+├── index.tsx               # 主组件(重构后)
+└── README.md               # 说明文档
+```
+
+### 🔧 Hooks 职责分离
+
+#### 1. `useChatMessages` - 消息管理
+- **职责**: 处理消息历史记录获取、消息分组和格式化
+- **功能**:
+  - 无限滚动加载历史消息
+  - 消息内容解析和格式化
+  - 按 sessionId 分组消息
+  - 计算消息组的时间显示
+
+#### 2. `useChatScrollManager` - 滚动管理
+- **职责**: 管理滚动相关的逻辑,包括自动滚动、键盘适配等
+- **功能**:
+  - 首次进入自动滚动到底部
+  - 键盘弹起时的高度适配
+  - 消息更新时的滚动触发
+  - 滚动事件处理
+
+#### 3. `useChatUI` - UI状态管理
+- **职责**: 管理UI相关的状态,如欢迎页显示、背景设置、输入框高度等
+- **功能**:
+  - 欢迎页显示逻辑
+  - 背景图片/视频处理
+  - 导航栏样式适配
+  - 输入框高度计算
+  - 安全区域适配
+
+#### 4. `useChatAgent` - 智能体数据管理
+- **职责**: 智能体数据的获取和管理
+- **功能**:
+  - 根据访客模式获取不同数据
+  - 智能体信息的缓存和更新
+
+### 📋 常量管理
+
+`constants/index.ts` 包含:
+- 滚动相关常量(延迟时间、默认位置等)
+- UI相关常量(安全区域、颜色配置等)
+- 选择器ID常量
+- 样式类名常量
+
+### 🎯 重构收益
+
+1. **代码可维护性**: 每个 Hook 职责单一,易于理解和修改
+2. **可测试性**: 独立的 Hook 可以单独进行单元测试
+3. **可复用性**: Hook 可以在其他页面中复用
+4. **代码可读性**: 主组件逻辑清晰,专注于渲染和事件处理
+5. **类型安全**: 完整的 TypeScript 类型注解
+
+### 📊 重构前后对比
+
+| 指标 | 重构前 | 重构后 |
+|------|--------|--------|
+| 主组件行数 | ~350行 | ~200行 |
+| 函数复杂度 | 高 | 低 |
+| 逻辑分离 | 无 | 4个专门Hook |
+| 可测试性 | 低 | 高 |
+| 可维护性 | 低 | 高 |
+
+### 🔄 使用方式
+
+```typescript
+// 在主组件中使用
+const { agent } = useChatAgent(agentId, isVisitor);
+
+const {
+  historyList,
+  groupedMessages,
+  messagesLength,
+  loadMore,
+  pageIndex,
+  mutate,
+} = useChatMessages(agentId, isVisitor);
+
+const {
+  scrollViewRef,
+  scrollTop,
+  keyboardHeight,
+  marginTopOffset,
+  handleScrollToUpper,
+  handleTouchMove,
+  handleScrollToLower,
+} = useChatScrollManager(messagesLength, pageIndex, loadMore);
+
+const {
+  showWelcome,
+  haveBg,
+  inputContainerHeight,
+  inputContainerBottomOffset,
+  setShowWelcome,
+  getBgContent,
+  createNavLeftRenderer,
+} = useChatUI(agent, rawHistoryListLength, currentMessageListLength);
+```
+
+### 🚀 后续优化建议
+
+1. **性能优化**: 可以进一步使用 `useMemo` 和 `useCallback` 优化性能
+2. **错误处理**: 在各个 Hook 中添加错误边界处理
+3. **加载状态**: 添加更细粒度的加载状态管理
+4. **缓存策略**: 实现更智能的数据缓存策略
+
+这种重构方式遵循了 React 最佳实践,提高了代码质量和开发效率。

+ 5 - 5
src/pages/chat/components/ChatGreeting/index.tsx

@@ -1,14 +1,14 @@
 import { View } from "@tarojs/components";
 import style from "./index.module.less";
 import { TAgentDetail } from "@/types/agent";
-import { useChatInput } from "../input-bar/chat.ts";
 interface IProps {
   agent: TAgentDetail | null;
+  chatInputActions: {
+    sendChatMessage: (message: string) => Promise<void>;
+  };
 }
-export default ({ agent }: IProps) => {
-  const { sendChatMessage } = useChatInput({
-    agent,
-  });
+export default ({ agent, chatInputActions }: IProps) => {
+  const { sendChatMessage } = chatInputActions;
 
   const handleClick = (q: string) => {
     sendChatMessage(q);

+ 4 - 4
src/pages/chat/components/input-bar/TextInputBar.tsx → src/pages/chat/components/InputBar/TextInputBar.tsx

@@ -3,7 +3,7 @@ import IconMic from "@/components/icon/IconMic"
 import IconStop from "@/components/icon/IconStop"
 import { useState } from "react";
 import WemetaInput from '@/components/wemeta-input'
-import { useTextChat } from "@/store/textChat";
+import { useTextChat } from "@/store/textChatStore";
 interface Props {
   disabled: boolean,
   onIconClick: () => void
@@ -11,7 +11,7 @@ interface Props {
 }
 export default ({disabled, onIconClick, onSend}:Props) => {
   const [value, setValue] = useState('')
-  const {messageSpeakersStopHandle} = useTextChat()
+  const {messageStopHandle} = useTextChat()
   
   const handleInput = (value: string)=> {
     setValue(value)
@@ -25,12 +25,12 @@ export default ({disabled, onIconClick, onSend}:Props) => {
     setValue('')
   }
   const handleStopClick = ()=> {
-    messageSpeakersStopHandle?.()
+    messageStopHandle?.()
   }
   const iconButton = ()=> {
     return <View className="flex items-center gap-12">
         <View className="flex-center" onClick={onIconClick}><IconMic /></View>
-        {!!messageSpeakersStopHandle && <View className="flex-center" onClick={handleStopClick}><IconStop /></View>}
+        {!!messageStopHandle && <View className="flex-center" onClick={handleStopClick}><IconStop /></View>}
     </View>
   }
   return <>

+ 4 - 4
src/pages/chat/components/input-bar/VoiceInputBar.tsx → src/pages/chat/components/InputBar/VoiceInputBar.tsx

@@ -6,7 +6,7 @@ import { useState } from "react";
 import style from './index.module.less'
 import { speechToText } from "@/service/chat";
 import { isSuccess } from "@/utils";
-import { useTextChat } from "@/store/textChat";
+import { useTextChat } from "@/store/textChatStore";
 interface Props {
   disabled: boolean,
   onIconClick: () => void
@@ -17,7 +17,7 @@ interface Props {
 }
 export default ({agentId,disabled, onIconClick, onSend, beforeSend, onError}:Props) => {
   const [speechStatus, setSpeechStatus] = useState(0);
-  const {messageSpeakersStopHandle} = useTextChat()
+  const {messageStopHandle} = useTextChat()
   const { start, stop, onStop } = useVoiceRecord('wav');
   const onLongPress = (e?: CommonEvent) => {
     e?.stopPropagation();
@@ -61,7 +61,7 @@ export default ({agentId,disabled, onIconClick, onSend, beforeSend, onError}:Pro
   });
 
   const handleStopClick = ()=> {
-    messageSpeakersStopHandle?.();
+    messageStopHandle?.();
   }
 
 
@@ -81,7 +81,7 @@ export default ({agentId,disabled, onIconClick, onSend, beforeSend, onError}:Pro
         </View>
         <View className="flex gap-12 items-center">
           {idle && <View className="flex items-center" onClick={onIconClick}><IconKeyboard/></View>}
-          {!!messageSpeakersStopHandle && <View className="flex items-center" onClick={handleStopClick}><IconStop/></View>}
+          {!!messageStopHandle && <View className="flex items-center" onClick={handleStopClick}><IconStop/></View>}
         </View>
       </View>
     

+ 0 - 0
src/pages/chat/components/input-bar/index.module.less → src/pages/chat/components/InputBar/index.module.less


+ 15 - 15
src/pages/chat/components/input-bar/index.tsx → src/pages/chat/components/InputBar/index.tsx

@@ -1,26 +1,26 @@
-import { useState } from "react";
+
 import TextInputBar from "./TextInputBar";
 import VoiceInputBar from "./VoiceInputBar";
 import { TAgentDetail } from "@/types/agent";
-import { TMessage, TRobotMessage } from "@/types/bot";
-import { useChatInput } from './chat'
 interface Props {
   agent: TAgentDetail | null;
-  histories: (TMessage|TRobotMessage)[];
+  // histories: (TMessage|TRobotMessage)[];
   setShowWelcome: (b: boolean) => void;
-  enableOutputAudioStream: boolean
+  enableOutputAudioStream: boolean;
+  chatInputActions: {
+    handleBeforeSend: () => void;
+    handleVoiceError: () => void;
+    sendVoiceChatMessage: (message: string) => void;
+    sendChatMessage: (message: string) => Promise<void>;
+  };
+  // InputBar 内部状态
+  isVoice: boolean;
+  setIsVoice: (isVoice: boolean) => void;
+  disabled: boolean;
 }
 
-export default ({ agent, enableOutputAudioStream, setShowWelcome }: Props) => {
-  const [isVoice, setIsVoice] = useState(false);
-  const [disabled, setDisabled] = useState(false);
-  const {handleBeforeSend, handleVoiceError, sendVoiceChatMessage, sendChatMessage} = useChatInput({
-    agent,
-    setShowWelcome,
-    setIsVoice,
-    setDisabled,
-    enableOutputAudioStream,
-  })
+export default ({ agent, enableOutputAudioStream, setShowWelcome, chatInputActions, isVoice, setIsVoice, disabled }: Props) => {
+  const {handleBeforeSend, handleVoiceError, sendVoiceChatMessage, sendChatMessage} = chatInputActions;
 
   const handleTextInputBarSwitch = () => {
     console.log("voice input on");

+ 88 - 64
src/pages/chat/components/input-bar/chat.ts → src/pages/chat/components/InputBar/useChatInput.ts

@@ -1,13 +1,13 @@
-import { useEffect, useState } from "react";
+import { useEffect } from "react";
 import { requestTextToChat } from "@/service/chat";
-import { useTextChat } from "@/store/textChat";
+import { useTextChat } from "@/store/textChatStore";
 import { TAgentDetail } from "@/types/agent";
 import { delay, getLoginId, isSuccess } from "@/utils";
 import { useAudioStreamPlayer } from "@/utils/audioStreamPlayer";
 
 import { EChatRole, EContentType } from "@/types/bot";
 
-import { usePostMessage, saveAgentChatContentToServer } from "./message";
+import { usePostMessage, saveAgentChatContentToServer, sendMyMessageToServer } from "./useMessage";
 
 import { getRecommendPrompt } from "@/service/chat";
 
@@ -19,7 +19,7 @@ interface Props {
   setDisabled?: (b: boolean) => void;
 }
 
-let stopReceiveChunk: (() => void) | undefined;
+let stopChunk: (() => void) | undefined;
 export const useChatInput = ({
   agent,
   enableOutputAudioStream,
@@ -28,7 +28,7 @@ export const useChatInput = ({
 }: Props) => {
   let myMsgUk = "";
   let mySessionId = "";
-
+  
   // 聊天框 store
   const {
     pushRobotMessage,
@@ -39,21 +39,36 @@ export const useChatInput = ({
     deleteMessage,
     setQuestions,
     questions,
-    messageSpeakersStopHandle,
-    setMessageSpeakersStopHandle,
+    messageStopHandle,
+    setMessageStopHandle,
     setReacting,
     setScrollTop,
     setAutoScroll,
   } = useTextChat();
 
   // 聊天框内消息定时上报
-  const { startTimedMessage, stopTimedMessage, saveMessageToServer } =
+  const { startTimedMessage, stopTimedMessage } =
     usePostMessage(getCurrentRobotMessage);
 
   // 聊天消息流式音频播报
-  const { setFistChunk, pushBase64ToQuene, playChunk, stopPlayChunk, stopPlay } =
+  const {isLastChunk, initPlay, setFistChunk, pushBase64ToQuene, playChunk, stopPlayChunk, stopPlay, onPlayerStatusChanged, setIsLastChunk } =
     useAudioStreamPlayer();
 
+  const clearActions = ()=> {
+    stopChunk?.();
+    stopTimedMessage();
+    stopPlayChunk();
+    setMessageStopHandle(null)
+    setReacting(false)
+    setDisabled?.(false);
+  }
+
+  const enableUserInput = ()=> {
+    setReacting(false)
+    setMessageStopHandle(null)
+  }
+
+  
   // 聊天
   const chatWithGpt = async (
     message: string,
@@ -68,71 +83,63 @@ export const useChatInput = ({
       return;
     }
 
-    if(messageSpeakersStopHandle){
-      messageSpeakersStopHandle()
+    if(messageStopHandle){
+      messageStopHandle()
     }
     setShowWelcome?.(false);
     setQuestions([]);
-    stopPlayChunk();
+    initPlay();
     setReacting(true)
 
     let currentRobotMsgUk = "";
     await delay(300);
     setDisabled?.(true);
     
-
-    const newMsg = {
-      content: message,
-      contentType: EContentType.TextPlain,
-      role: EChatRole.User,
-    };
-    // 将发送的消息上报
-    const myMsgResponse = await saveMessageToServer({
-      loginId,
-      messages: [
-        {
-          ...newMsg,
-          saveStatus: 2,
-          isStreaming: false,
-          msgUk,
-        },
-      ],
+    const submitMyMessageSuccess = await sendMyMessageToServer({
       agentId: agent.agentId,
-      sessionId,
-    });
-
-    // todo: 如果上报失败,要不要继续让智能体回复??
-    if (!isSuccess(myMsgResponse.status)) {
+      message,
+      loginId,
+      msgUk,
+      sessionId
+    })
+    
+    // todo: 如果自己发送的消息失败了,需要显示错误信息提示用户?
+    if (!submitMyMessageSuccess) {
       setReacting(false)
-      return setDisabled?.(false);
+      setDisabled?.(false);
+      return;
     }
+    
+    
 
     let isFirstChunk = true;
+    console.log('==== start new chat ====')
     // 发起文本聊天
-    const {reqTask, stopChunk} = requestTextToChat({
+    const request = requestTextToChat({
       params: {
         agentId: agent.agentId,
         isEnableOutputAudioStream: enableOutputAudioStream,
         isEnableSearch: false,
         isEnableThinking: false,
         loginId,
-        messages: [newMsg],
+        messages: [{
+          content: message,
+          contentType: EContentType.TextPlain,
+          role: EChatRole.User,
+        }],
         sessionId,
       },
       onStart: () => {
-        setMessageSpeakersStopHandle(()=> {
-          stopReceiveChunk?.();
-          stopTimedMessage();
-          stopPlayChunk();
-          // setReacting(false)
+
+        setMessageStopHandle(()=> {
           const currentRobotMessage = getCurrentRobotMessage();
           if(currentRobotMessage?.content.length === 0){
             deleteMessage(currentRobotMessage.msgUk);
           }
-          setDisabled?.(false)
-          // 清掉句柄
-          setMessageSpeakersStopHandle(null)
+          clearActions()
+          console.log('按下停止接收键')
         })
+
         // 推一个空回复,用于展示 ui
         const blankMessage = {
           role: EChatRole.Assistant,
@@ -177,7 +184,7 @@ export const useChatInput = ({
         // }
         if (isFirstChunk) {
           isFirstChunk = false;
-          setFistChunk(audioStr, reqTask);
+          setFistChunk(audioStr, request.reqTask);
         } else {
           pushBase64ToQuene(audioStr);
         }
@@ -186,6 +193,7 @@ export const useChatInput = ({
       onReceived: (m) => {
         updateRobotMessage(m.content, m.body ?? {});
       },
+      // 流式结束
       onFinished: async (m) => {
         const currentRobotMessage = getCurrentRobotMessage();
         console.log(
@@ -193,7 +201,13 @@ export const useChatInput = ({
           currentRobotMessage,
           m?.body?.content
         );
-
+        // 标记 chunk 已接收完毕
+        setIsLastChunk()
+        // 如果是非声音朗读,则需要隐藏 停止按钮
+        if(!enableOutputAudioStream){
+          enableUserInput()
+        }
+        
         stopTimedMessage();
         if (!agent.agentId) {
           return;
@@ -220,13 +234,16 @@ export const useChatInput = ({
           setQuestions(response.data.questions);
         }
       },
+      // 此处表示发起的请求已结束,但流式可能还在继续
       onComplete: async () => {
-        stopTimedMessage();
-        setReacting(false)
-        console.log("回复 onComplete");
         // 为防止服务端没有终止消息,当接口请求结束时强制再保存一次消息体,以防定时保存的消息体漏掉最后一部分
         const currentRobotMessage = getCurrentRobotMessage();
-        if (currentRobotMessage && agent.agentId) {
+        console.log("--------- onComplete", agent.agentId, currentRobotMessage?.robot?.agentId );
+        if (agent.agentId && (currentRobotMessage?.robot?.agentId  === agent.agentId)) {
+          console.log("回复 onComplete", agent, currentRobotMessage?.robot?.agentId );
+          stopTimedMessage();
+          setDisabled?.(false);
+          isFirstChunk = true;
           saveAgentChatContentToServer(
             currentRobotMessage,
             loginId,
@@ -234,19 +251,16 @@ export const useChatInput = ({
             sessionId
           );
         }
-
-        setDisabled?.(false);
-
-        isFirstChunk = true;
+        
       },
       onError: () => {
         setDisabled?.(false);
         setReacting(false)
-        deleteMessage(currentRobotMsgUk);
+        currentRobotMsgUk && deleteMessage(currentRobotMsgUk);
       },
     });
 
-    stopReceiveChunk = stopChunk
+    stopChunk = request.stopChunk
   };
   // 聊天录制语音转文本后发送
   const sendVoiceChatMessage = (message: string) => {
@@ -280,23 +294,33 @@ export const useChatInput = ({
     deleteMessage(myMsgUk);
   };
 
+  // 由于要等播放完毕后才能隐藏“停止按钮”,
+  // 等待音频播放完毕才能更改状态
+  onPlayerStatusChanged((status)=> {
+    console.log('播放器播放状态====', status)
+    if(status === 'stop'){
+      enableUserInput()
+    }
+  })
   // 如果停止语音输出,则停止播放
   useEffect(() => {
     if (!enableOutputAudioStream) {
       stopPlay();
+      // 如果刚好是最后的 chunk 了则需要立即隐藏 '停止按钮'
+      if(isLastChunk){
+        enableUserInput()
+      }
     }
   }, [enableOutputAudioStream]);
-
+  
   // 清理
   useEffect(() => {
+    console.log('chat input ')
     return () => {
-      stopReceiveChunk?.();
-      stopTimedMessage();
-      stopPlayChunk();
-      setReacting(false)
-      setScrollTop(99999);
+      clearActions();
+      setScrollTop(0);
       setAutoScroll(true);
-      setMessageSpeakersStopHandle(null)
+      
       console.log('clear chat')
     };
   }, []);

+ 196 - 0
src/pages/chat/components/InputBar/useMessage.ts

@@ -0,0 +1,196 @@
+import { appendMessages } from "@/service/chat"
+import { type TAppendMessages, TRobotMessage, EContentType, EChatRole } from "@/types/bot";
+import { isSuccess } from "@/utils";
+import { useRef, useEffect, useState } from "react";
+
+
+export const sendMyMessageToServer = async (data: {
+  message: string, 
+  loginId: string, 
+  agentId: string,
+  msgUk: string,
+  sessionId: string,
+})=> {
+  let result = false
+  const newMsg = {
+    content: data.message,
+    contentType: EContentType.TextPlain,
+    role: EChatRole.User,
+  };
+  try{
+    // 将发送的消息上报
+    const myMsgResponse = await saveMessageToServer({
+      loginId: data.loginId,
+      messages: [
+        {
+          ...newMsg,
+          saveStatus: 2,
+          isStreaming: false,
+          msgUk: data.msgUk,
+        },
+      ],
+      agentId: data.agentId,
+      sessionId: data.sessionId,
+    });
+    if(isSuccess(myMsgResponse.status)){
+      result = true;
+    }
+  }catch(e){
+    result = false;
+  }
+  return result
+}
+
+export const saveMessageToServer = (data: TAppendMessages) => {
+  const postData = {
+    ...data,
+    messages: data.messages.map((message)=> {
+      
+      if(message.contentType === EContentType.AiseekQA){
+        message.content = JSON.stringify(message.content)
+        return message
+      }
+      return message
+    })
+  }
+  return appendMessages(postData)
+}
+
+
+// 保存智能体消息内容至服务器
+export const saveAgentChatContentToServer = async (currentRobotMessage: TRobotMessage, loginId:string, agentId:string, sessionId: string) => {
+  const currentContentType = currentRobotMessage?.body?.contentType
+  let content = ''
+  // audio chunk 不需要上报,在初始时已经上报
+  if(currentContentType === EContentType.AiseekAudioChunk && currentRobotMessage?.body?.content.sentenceBegin){
+    // 只保存文本至服务器
+    content = (currentRobotMessage?.content as string) ?? ''
+  }else if(currentContentType === EContentType.AiseekQA ){
+    // 也原样保存至服务器
+    content = (currentRobotMessage?.content as string) ?? ''
+  }else{
+    content = (currentRobotMessage?.content as string) ?? ''
+  }
+
+  const robot = currentRobotMessage.robot ?? {}
+  await saveMessageToServer({
+    loginId,
+    messages: [{
+      saveStatus: 2,
+      content: content,
+      contentType: currentContentType ?? EContentType.TextPlain,
+      isStreaming: true,
+      robot: {...robot},
+      role: currentRobotMessage.role,
+      msgUk: currentRobotMessage.msgUk,
+    }],
+    agentId: agentId,
+    sessionId,
+  })
+}
+
+// 定时上报智能体回复的消息体
+
+export const usePostMessage = (getCurrentRobotMessage: () => TRobotMessage | undefined) => {
+  const timerRef = useRef<NodeJS.Timeout | null>(null);
+  const isRunningRef = useRef(false);
+  const dataRef = useRef<any>({});
+  const [isRunning, setIsRunning] = useState(false);
+  // 保存间隔时间的ref,方便在回调中访问最新值
+  const intervalRef = useRef<number>(5000);
+
+  // 组件卸载时清理定时器
+  useEffect(() => {
+    return () => {
+      stopTimedMessage();
+    };
+  }, []);
+
+  // 实际执行发送逻辑的函数
+  const sendMessage = async () => {
+    // 如果已经停止运行,直接返回
+    if (!isRunningRef.current) {
+      return;
+    }
+
+    try {
+      const msg = getCurrentRobotMessage();
+      if (!msg) {
+        // 即使没有消息,也需要继续下一次定时
+        scheduleNextSend();
+        return;
+      }
+
+      const message = {
+        ...msg,
+        body: undefined,
+        isStreaming: msg.isStreaming ?? false,
+        contentType: msg.contentType ?? EContentType.TextPlain,
+        saveStatus: msg.saveStatus ?? 1
+      };
+      // 空消息不上报
+      if(message.content === ''){
+        return;
+      }
+    // 这里使用闭包保存的data值,注意如果data会变化需要特殊处理
+      // 可以将data也存入ref中,确保每次获取最新值
+      await saveMessageToServer({
+        ...dataRef.current,
+        messages: [message]
+      });
+      console.log('消息发送成功');
+    } catch (error) {
+      console.error('消息发送失败:', error);
+    } finally {
+      // 无论成功失败,都安排下一次发送
+      scheduleNextSend();
+    }
+  };
+
+  // 安排下一次发送
+  const scheduleNextSend = () => {
+    if (isRunningRef.current) {
+      timerRef.current = setTimeout(sendMessage, intervalRef.current);
+    }
+  };
+
+  // 开始定时发送消息
+  const startTimedMessage = (data: TAppendMessages, interval: number = 5000) => {
+    if (isRunningRef.current) {
+      console.warn('定时任务已在运行中');
+      return;
+    }
+    dataRef.current = data;
+    // 更新配置
+    intervalRef.current = interval;
+    isRunningRef.current = true;
+    setIsRunning(true);
+    console.log('开始定时发送消息,间隔:', interval, 'ms');
+
+    // 立即执行第一次发送
+    sendMessage();
+  };
+
+  // 停止定时发送消息
+  const stopTimedMessage = () => {
+    if (timerRef.current) {
+      clearTimeout(timerRef.current);
+      timerRef.current = null;
+    }
+    isRunningRef.current = false;
+    setIsRunning(false);
+    console.log('定时发送消息已停止');
+  };
+
+  // 检查定时任务是否正在运行
+  const isTimedMessageRunning = () => {
+    return isRunning;
+  };
+
+  return {
+    saveMessageToServer,
+    startTimedMessage,
+    stopTimedMessage,
+    isTimedMessageRunning,
+  };
+};

+ 0 - 0
src/pages/chat/components/personal-card/index.tsx → src/pages/chat/components/PersonalCard/index.tsx


+ 8 - 7
src/pages/chat/components/RecommendQuestions/index.tsx

@@ -1,18 +1,19 @@
 import { View, ScrollView } from "@tarojs/components";
 
-import { useChatInput } from "../input-bar/chat.ts";
 import { TAgentDetail } from "@/types/agent";
 
 interface IProps {
   agent: TAgentDetail;
-  enableOutputAudioStream: boolean
+  enableOutputAudioStream: boolean;
+  chatInputActions: {
+    sendChatMessage: (message: string) => Promise<void>;
+    setQuestions: (questions: string[]) => void;
+    questions: string[];
+  };
 }
 
-export default function Index({ agent, enableOutputAudioStream }: IProps) {
-  const { sendChatMessage, setQuestions, questions } = useChatInput({
-    agent,
-    enableOutputAudioStream,
-  });
+export default function Index({ agent, enableOutputAudioStream, chatInputActions }: IProps) {
+  const { sendChatMessage, setQuestions, questions } = chatInputActions;
 
   const handleClick = (q: string) => {
     sendChatMessage(q);

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

@@ -1,182 +0,0 @@
-import { appendMessages } from "@/service/chat"
-import { type TAppendMessages, TRobotMessage, EContentType } from "@/types/bot";
-import { useRef, useEffect, useState } from "react";
-
-export const saveMessageToServer = (data: TAppendMessages) => {
-  const postData = {
-    ...data,
-    messages: data.messages.map((message)=> {
-      
-      if(message.contentType === EContentType.AiseekQA){
-        message.content = JSON.stringify(message.content)
-        return message
-      }
-      return message
-    })
-  }
-  return appendMessages(postData)
-}
-
-
-// 保存智能体消息内容至服务器
-export const saveAgentChatContentToServer = async (currentRobotMessage: TRobotMessage, loginId:string, agentId:string, sessionId: string) => {
-  const currentContentType = currentRobotMessage?.body?.contentType
-  let content = ''
-  // audio chunk 不需要上报,在初始时已经上报
-  if(currentContentType === EContentType.AiseekAudioChunk && currentRobotMessage?.body?.content.sentenceBegin){
-    // 只保存文本至服务器
-    content = (currentRobotMessage?.content as string) ?? ''
-  }else if(currentContentType === EContentType.AiseekQA ){
-    // 也原样保存至服务器
-    content = (currentRobotMessage?.content as string) ?? ''
-  }else{
-    content = (currentRobotMessage?.content as string) ?? ''
-  }
-
-
-  await saveMessageToServer({
-    loginId,
-    messages: [{
-      saveStatus: 2,
-      content: content,
-      contentType: currentContentType ?? EContentType.TextPlain,
-      isStreaming: true,
-      role: currentRobotMessage.role,
-      msgUk: currentRobotMessage.msgUk,
-    }],
-    agentId: agentId,
-    sessionId,
-  })
-}
-
-// 定时上报智能体回复的消息体
-
-export const usePostMessage = (getCurrentRobotMessage:() => TRobotMessage | undefined)=> {
-  const timerRef = useRef<NodeJS.Timeout | null>(null);
-  const isRunningRef = useRef(false);
-  const [isRunning, setIsRunning] = useState(false);
-
-  // 组件卸载时清理定时器
-  useEffect(() => {
-    return () => {
-      if (timerRef.current) {
-        clearInterval(timerRef.current);
-        timerRef.current = null;
-      }
-      isRunningRef.current = false;
-      setIsRunning(false);
-    };
-  }, []);
-
-  // 开始定时发送消息
-  const startTimedMessage = (data: TAppendMessages, interval: number = 5000) => {
-    if (isRunningRef.current) {
-      console.warn('定时任务已在运行中');
-      return;
-    }
-
-    isRunningRef.current = true;
-    setIsRunning(true);
-    console.log('开始定时发送消息,间隔:', interval, 'ms');
-
-    const sendMessage = async () => {
-      try {
-        // 获取当前智能体回答,上报至服务端
-        const msg = getCurrentRobotMessage()
-        if(!msg){
-          return 
-        }
-        const message = {
-          ...msg,
-          body: undefined, // 删除保存在 msg 内的 body 信息 不需要上报至服务器
-          isStreaming: msg.isStreaming ?? false,
-          contentType: msg.contentType ?? EContentType.TextPlain,
-          saveStatus: msg.saveStatus ?? 1
-        }
-
-        await saveMessageToServer({
-          ...data,
-          messages: [message]
-        });
-        console.log('定时发送消息成功');
-      } catch (error) {
-        console.error('定时发送消息失败:', error);
-      }
-    };
-
-    // 立即发送一次
-    // sendMessage();
-
-    // 设置定时器,每 interval 毫秒发送一次
-    timerRef.current = setInterval(sendMessage, interval);
-  };
-
-  // 停止定时发送消息
-  const stopTimedMessage = () => {
-    if (timerRef.current) {
-      clearInterval(timerRef.current);
-      timerRef.current = null;
-    }
-    isRunningRef.current = false;
-    setIsRunning(false);
-    console.log('定时发送消息已停止');
-  };
-
-  // 检查定时任务是否正在运行
-  const isTimedMessageRunning = () => {
-    return isRunning;
-  };
-
-  return {
-    saveMessageToServer,
-    startTimedMessage,
-    stopTimedMessage,
-    isTimedMessageRunning,
-  }
-}
-
-/*
-使用示例:
-
-import { usePostMessage } from './message';
-
-const MyComponent = () => {
-  const { startTimedMessage, stopTimedMessage, isTimedMessageRunning } = usePostMessage();
-  
-  const handleStartTimer = () => {
-    const messageData = {
-      agentId: 'your-agent-id',
-      loginId: 'your-login-id',
-      sessionId: 'your-session-id',
-      messages: [{
-        content: '定时消息内容',
-        contentType: EContentType.TextPlain,
-        isStreaming: false,
-        msgUk: 'unique-message-id',
-        role: EChatRole.User
-      }]
-    };
-    
-    // 开始定时发送,每5秒发送一次
-    startTimedMessage(messageData, 5000);
-  };
-  
-  const handleStopTimer = () => {
-    stopTimedMessage();
-  };
-  
-  const isRunning = isTimedMessageRunning();
-  
-  return (
-    <View>
-      <Button onClick={handleStartTimer} disabled={isRunning}>
-        开始定时发送
-      </Button>
-      <Button onClick={handleStopTimer} disabled={!isRunning}>
-        停止定时发送
-      </Button>
-      <Text>状态: {isRunning ? '运行中' : '已停止'}</Text>
-    </View>
-  );
-};
-*/

+ 54 - 0
src/pages/chat/constants/index.ts

@@ -0,0 +1,54 @@
+/**
+ * 聊天页面相关常量配置
+ */
+
+// 滚动相关常量
+export const SCROLL_CONSTANTS = {
+  // 首次滚动延迟时间(毫秒)
+  INITIAL_SCROLL_DELAY: 1000,
+  // 键盘高度更新延迟时间(毫秒)
+  KEYBOARD_HEIGHT_UPDATE_DELAY: 100,
+  // 默认滚动位置
+  DEFAULT_SCROLL_TOP: 999999,
+  // 滚动动画延迟
+  SCROLL_ANIMATION_DELAY: 100,
+} as const;
+
+// UI相关常量
+export const UI_CONSTANTS = {
+  // 底部安全区域补偿高度
+  BOTTOM_SAFE_AREA_COMPENSATION: 12,
+  // 导航栏颜色配置
+  NAV_COLORS: {
+    LIGHT: {
+      frontColor: "#000000" as const,
+      backgroundColor: "transparent" as const,
+    },
+    DARK: {
+      frontColor: "#ffffff" as const,
+      backgroundColor: "transparent" as const,
+    },
+  },
+} as const;
+
+// 选择器ID常量
+export const SELECTOR_IDS = {
+  MESSAGE_LIST: "#messageList",
+  SCROLL_VIEW: "#scrollView",
+  INPUT_CONTAINER: "#inputContainer",
+} as const;
+
+// 消息相关常量
+export const MESSAGE_CONSTANTS = {
+  // 分页大小(如果需要的话)
+  DEFAULT_PAGE_SIZE: 20,
+  // 时间格式化相关
+  TIME_SEPARATOR_REGEX: /\-/g,
+  TIME_REPLACEMENT: "/",
+} as const;
+
+// 样式类名常量
+export const STYLE_CLASSES = {
+  // 时间显示样式
+  
+} as const;

+ 8 - 0
src/pages/chat/hooks/index.ts

@@ -0,0 +1,8 @@
+/**
+ * 聊天页面 Hooks 统一导出
+ */
+
+export { useChatMessages } from './useChatMessages';
+export { useChatScrollManager } from './useChatScrollManager';
+export { useChatUI } from './useChatUI';
+export { useChatAgent } from './useChatAgent';

+ 39 - 0
src/pages/chat/hooks/useChatAgent.ts

@@ -0,0 +1,39 @@
+import { useEffect } from 'react';
+import { useDidShow } from '@tarojs/taro';
+import { useAgentStore } from '@/store/agentStore';
+
+/**
+ * 聊天智能体数据管理 Hook
+ * 负责智能体数据的获取和管理
+ */
+export const useChatAgent = (agentId: string, isVisitor?: string): {
+  agent: any;
+  isVisitor: boolean;
+} => {
+  const { fetchAgent, fetchAgentProfile } = useAgentStore();
+  
+  // 获取智能体数据
+  const agent = useAgentStore((state) => {
+    if (isVisitor === "true") {
+      return state.agentProfile;
+    }
+    return state.agent;
+  });
+
+  // 根据是否为访客模式获取不同的智能体数据
+  useEffect(() => {
+    if (agentId) {
+      if (isVisitor === "true") {
+        fetchAgentProfile(agentId);
+      } else {
+        fetchAgent(agentId);
+      }
+    }
+  }, [agentId, isVisitor, fetchAgent, fetchAgentProfile]);
+
+  return {
+    agent,
+    isVisitor: isVisitor === "true",
+    // 可以添加更多智能体相关的状态和方法
+  };
+};

+ 96 - 0
src/pages/chat/hooks/useChatMessages.ts

@@ -0,0 +1,96 @@
+import { useMemo } from 'react';
+import { useLoadMoreInfinite, createKey } from '@/utils/loadMoreInfinite';
+import { getMessageHistories } from '@/service/chat';
+import { useTextChat } from '@/store/textChatStore';
+import { formatMessageItem } from '@/utils/messageUtils';
+import { TMessage, TRobotMessage, TAnyMessage } from '@/types/bot';
+
+/**
+ * 聊天消息管理 Hook
+ * 负责处理消息历史记录获取、消息分组和格式化
+ */
+export const useChatMessages = (agentId: string, isVisitor?: string): {
+  historyList: TAnyMessage[];
+  messageList: TAnyMessage[];
+  parsedList: TAnyMessage[];
+  groupedMessages: { dt: string; list: TAnyMessage[] }[];
+  messagesLength: number;
+  loadMore: () => void;
+  pageIndex: number;
+  mutate: () => void;
+} => {
+  const messageList = useTextChat((state) => state.list);
+
+  // 获取历史聊天记录的 fetcher 函数
+  const fetcher = async ([_url, { nextId, pageSize }]) => {
+    const _nextId = nextId ? decodeURIComponent(nextId) : nextId;
+    const res = await getMessageHistories({
+      agentId,
+      startId: _nextId,
+      pageSize,
+    });
+    return res.data;
+  };
+
+  // 使用无限滚动加载历史消息
+  const { list, loadMore, pageIndex, mutate } = useLoadMoreInfinite<
+    TMessage[] | TRobotMessage[]
+  >(createKey(`messeagehistories${isVisitor}${agentId}`), fetcher);
+
+  // 解析消息体 content,并展平数组
+  const parsedList = list
+    .flat()
+    .filter((item) => !!item.content.length)
+    .map(formatMessageItem);
+
+  // 按 sessionId 分组消息,并记录每组的最早时间
+  // 最后返回数组,以按组显示聊天记录时间 隔开每一组聊天记录
+  const groupedMessages = useMemo(() => {
+    const allMessages = [...[...parsedList].reverse(), ...messageList];
+    
+    const resultMap = allMessages.reduce((acc, item) => {
+      const { sessionId, msgTime } = item;
+      if (!sessionId || !msgTime) {
+        return acc;
+      }
+      
+      let _msgTime = msgTime.replace(/\-/g, "/");
+      if (!acc[sessionId]) {
+        acc[sessionId] = {
+          dt: _msgTime, // 初始化当前组的最早时间
+          list: [item], // 初始化当前组的记录列表
+        };
+      } else {
+        // 更新最早时间(如果当前记录的 msgTime 更早)
+        if (new Date(_msgTime) < new Date(acc[sessionId].dt)) {
+          acc[sessionId].dt = msgTime;
+        }
+        // 将记录添加到当前组
+        acc[sessionId].list.push(item);
+      }
+      return acc;
+    }, {} as Record<string, { dt: string; list: TAnyMessage[] }>);
+
+    // 转换为最终数组格式
+    return Object.values(resultMap);
+  }, [parsedList, messageList]);
+
+  // 消息总长度(用于触发滚动等副作用)
+  const messagesLength = useMemo(() => groupedMessages.length, [groupedMessages.length]);
+
+  return {
+    // 原始数据(保持原始结构供其他用途)
+    historyList: parsedList, // 已经展平的消息列表
+    messageList,
+    parsedList,
+    
+    // 处理后的数据
+    groupedMessages,
+    messagesLength,
+    
+    // 操作方法
+    loadMore,
+    pageIndex,
+    mutate,
+  };
+};

+ 96 - 0
src/pages/chat/hooks/useChatScrollManager.ts

@@ -0,0 +1,96 @@
+import { useEffect, useRef } from 'react';
+import { useTextChat } from '@/store/textChatStore';
+import { useKeyboard } from '../components/keyboard';
+
+/**
+ * 聊天滚动管理 Hook
+ * 负责处理滚动相关的逻辑,包括自动滚动、键盘适配等
+ */
+export const useChatScrollManager = (messagesLength: number, pageIndex: number, loadMore?: () => void): {
+  scrollViewRef: React.MutableRefObject<any>;
+  scrollTop: number;
+  keyboardHeight: number;
+  marginTopOffset: number;
+  handleScrollToUpper: () => void;
+  handleTouchMove: () => void;
+  handleScrollToLower: () => void;
+  setScrollTop: (num?: number) => void;
+  setAutoScroll: (b: boolean) => void;
+} => {
+  const scrollViewRef = useRef<any>(null);
+  const initialScrolledRef = useRef(false);
+  const prevLengthRef = useRef(messagesLength);
+
+  const { setScrollTop, setAutoScroll } = useTextChat();
+  const scrollTop = useTextChat((state) => state.scrollTop);
+
+  // 键盘高度管理
+  const { keyboardHeight, marginTopOffset, triggerHeightUpdate } = useKeyboard(
+    scrollViewRef,
+    "#messageList",
+    "#scrollView"
+  );
+
+  // 首次进入界面滚动到底部
+  useEffect(() => {
+    // 仅首次 pageIndex === 1 且已经有消息时滚动一次
+    if (pageIndex === 1 && messagesLength > 0 && !initialScrolledRef.current) {
+      initialScrolledRef.current = true;
+
+      // 延迟1秒再滚动,确保 DOM 已完成渲染
+      setTimeout(() => {
+        setScrollTop(999999);
+        console.log("首次进入,滚动到底部");
+      }, 600);
+    }
+  }, [pageIndex, messagesLength, setScrollTop]);
+
+  // 监听消息列表变化,触发键盘高度重新计算
+  useEffect(() => {
+    // 只在长度真正变化时才触发
+    if (prevLengthRef.current !== messagesLength) {
+      prevLengthRef.current = messagesLength;
+
+      // 使用 setTimeout 确保 DOM 更新完成后再计算高度
+      const timer = setTimeout(() => {
+        triggerHeightUpdate();
+      }, 100);
+
+      return () => clearTimeout(timer);
+    }
+  }, [messagesLength, triggerHeightUpdate]);
+
+  // 滚动事件处理
+  const handleScrollToUpper = () => {
+    console.log("onscroll");
+    loadMore?.();
+  };
+
+  const handleTouchMove = () => {
+    console.log("set auto scroll false");
+    setAutoScroll(false);
+  };
+
+  const handleScrollToLower = () => {
+    setAutoScroll(true);
+  };
+
+  return {
+    // refs
+    scrollViewRef,
+    
+    // 滚动相关状态
+    scrollTop,
+    keyboardHeight,
+    marginTopOffset,
+    
+    // 事件处理函数
+    handleScrollToUpper,
+    handleTouchMove,
+    handleScrollToLower,
+    
+    // 工具方法
+    setScrollTop,
+    setAutoScroll,
+  };
+};

+ 109 - 0
src/pages/chat/hooks/useChatUI.tsx

@@ -0,0 +1,109 @@
+import { useState, useEffect } from 'react';
+import Taro from '@tarojs/taro';
+import { View } from "@tarojs/components";
+import { useAppStore } from '@/store/appStore';
+import { useTextChat } from '@/store/textChatStore';
+
+/**
+ * 聊天UI状态管理 Hook
+ * 负责处理UI相关的状态,如欢迎页显示、背景设置、输入框高度等
+ */
+export const useChatUI = (
+  agent: any,
+  historyListLength: number,
+  messageListLength: number
+): {
+  showWelcome: boolean;
+  haveBg: boolean;
+  inputContainerHeight: number;
+  inputContainerBottomOffset: number;
+  setShowWelcome: (show: boolean) => void;
+  getBgContent: () => string;
+  createNavLeftRenderer: (PersonalCard: any, IconArrowLeftWhite24: any, IconArrowLeft: any) => () => JSX.Element;
+} => {
+  const [showWelcome, setShowWelcome] = useState(!historyListLength);
+  const [inputContainerHeight, setInputContainerHeight] = useState(0);
+  const scrollTop = useTextChat((state)=> state.scrollTop)
+  
+  const bottomSafeHeight = useAppStore((state) => state.bottomSafeHeight);
+
+  // 判断是否有背景
+  const haveBg = !!agent?.enabledChatBg && !!agent.avatarUrl?.length;
+
+  // 计算输入框底部偏移量
+  // 针对没有 safeArea?.bottom 的手机,需要额外增加 12 高度
+  const inputContainerBottomOffset = bottomSafeHeight <= 0 ? 12 : 0;
+
+  // 获取背景内容
+  const getBgContent = () => {
+    if (!agent?.avatarUrl || !agent?.enabledChatBg) {
+      return "";
+    }
+    return agent?.avatarUrl;
+  };
+
+  // 渲染导航栏左侧内容的工厂函数
+  const createNavLeftRenderer = (PersonalCard: any, IconArrowLeftWhite24: any, IconArrowLeft: any) => {
+    return () => {
+      return (
+        <View
+          className="flex items-center gap-8"
+          onClick={() => Taro.navigateBack()}
+        >
+          {haveBg ? <IconArrowLeftWhite24 /> : <IconArrowLeft />}
+          <PersonalCard agent={agent} haveBg={haveBg} />
+          {scrollTop}
+        </View>
+      );
+    };
+  };
+
+  // 是否显示欢迎UI
+  useEffect(() => {
+    setShowWelcome(!messageListLength && !historyListLength);
+  }, [historyListLength, messageListLength]);
+
+  // 设置导航栏颜色
+  useEffect(() => {
+    if (haveBg) {
+      Taro.setNavigationBarColor({
+        frontColor: "#ffffff",
+        backgroundColor: "transparent",
+      });
+    }
+    return () => {
+      Taro.setNavigationBarColor({
+        frontColor: "#000000",
+        backgroundColor: "transparent",
+      });
+    };
+  }, [haveBg]);
+
+  // 计算输入框容器高度
+  useEffect(() => {
+    const query = Taro.createSelectorQuery();
+    query
+      .select("#inputContainer")
+      .boundingClientRect((rect: any) => {
+        if (rect) {
+          setInputContainerHeight(rect.height - bottomSafeHeight);
+        }
+      })
+      .exec();
+  }, [agent, bottomSafeHeight]);
+
+  return {
+    // 状态
+    showWelcome,
+    haveBg,
+    inputContainerHeight,
+    inputContainerBottomOffset,
+    
+    // 设置方法
+    setShowWelcome,
+    
+    // 工具方法
+    getBgContent,
+    createNavLeftRenderer,
+  };
+};

+ 112 - 227
src/pages/chat/index.tsx

@@ -1,33 +1,30 @@
 import { View, ScrollView } from "@tarojs/components";
 import NavBarNormal from "@/components/NavBarNormal/index";
 import PageCustom from "@/components/page-custom/index";
-import Taro, { useDidShow, useRouter, useUnload } from "@tarojs/taro";
+import { useDidShow, useRouter, useUnload } from "@tarojs/taro";
 import ChatMessage from "@/components/chat-message";
-import InputBar from "./components/input-bar";
-import { useEffect, useState, useRef, useMemo } from "react";
-import { useTextChat } from "@/store/textChat";
+import InputBar from "./components/InputBar";
+import { useEffect, useState } from "react";
+import { useTextChat } from "@/store/textChatStore";
 import { formatMessageTime } from "@/utils/timeUtils";
-import { formatMessageItem } from "@/utils/messageUtils";
-import {
-  TRobotMessage,
-  TMessage,
-  TAnyMessage,
-} from "@/types/bot";
 
 import ChatGreeting from "./components/ChatGreeting";
 import IconArrowLeftWhite24 from "@/components/icon/IconArrowLeftWhite24";
 import IconArrowLeft from "@/components/icon/icon-arrow-left";
-import PersonalCard from "./components/personal-card";
+import PersonalCard from "./components/PersonalCard";
 import ButtonEnableStreamVoice from "./components/OptionButtons/ButtonEnableStreamVoice";
-import { useAgentStore } from "@/store/agentStore";
-
-import { useLoadMoreInfinite, createKey } from "@/utils/loadMoreInfinite";
-import { getMessageHistories } from "@/service/chat";
 
 import RecommendQuestions from "./components/RecommendQuestions";
-import { useKeyboard } from "./components/keyboard";
-import { useAppStore } from "@/store/appStore";
-import { usePersistentState } from '@/hooks/usePersistentState';
+import { usePersistentState } from "@/hooks/usePersistentState";
+
+// 导入我们抽离的 hooks 和常量
+import {
+  useChatMessages,
+  useChatScrollManager,
+  useChatUI,
+  useChatAgent,
+} from "./hooks";
+import { useChatInput } from "./components/InputBar/useChatInput";
 
 export default function Index() {
   const router = useRouter();
@@ -37,217 +34,84 @@ export default function Index() {
     return <View>没有相应的智能体</View>;
   }
 
-  const { fetchAgent, fetchAgentProfile } = useAgentStore();
-
-  const bottomSafeHeight = useAppStore( state => state.bottomSafeHeight)
-
-  const agent = useAgentStore((state) => {
-    if (isVisitor === "true") {
-      return state.agentProfile;
-    }
-    return state.agent;
-  });
-  const scrollViewRef = useRef<any>(null);
-  const messageList = useTextChat((state) => state.list);
-  const autoScroll = useTextChat((state) => state.autoScroll);
-  const [inputContainerHeight,setInputContainerHeight]= useState(0)
-  const [streamVoiceEnable, setStreamVoiceEnable]= usePersistentState('streamVoiceEnable', false)
-
-  const { keyboardHeight, marginTopOffset, triggerHeightUpdate } = useKeyboard(
+  // 使用抽离的 hooks
+  const { agent } = useChatAgent(agentId, isVisitor);
+  
+  const {
+    historyList,
+    groupedMessages,
+    messagesLength,
+    loadMore,
+    pageIndex,
+    mutate,
+  } = useChatMessages(agentId, isVisitor);
+
+  // 获取原始历史消息列表长度(用于UI状态判断)
+  const rawHistoryListLength = historyList.length;
+
+  const {
     scrollViewRef,
-    "#messageList",
-    "#scrollView"
+    scrollTop,
+    keyboardHeight,
+    marginTopOffset,
+    handleScrollToUpper,
+    handleTouchMove,
+    handleScrollToLower,
+  } = useChatScrollManager(messagesLength, pageIndex, loadMore);
+
+  // 获取当前消息列表长度
+  const currentMessageListLength = useTextChat((state) => state.list.length);
+  
+  const {
+    showWelcome,
+    haveBg,
+    inputContainerHeight,
+    inputContainerBottomOffset,
+    setShowWelcome,
+    getBgContent,
+    createNavLeftRenderer,
+  } = useChatUI(agent, rawHistoryListLength, currentMessageListLength);
+
+  const [streamVoiceEnable, setStreamVoiceEnable] = usePersistentState(
+    "streamVoiceEnable",
+    false
   );
 
-  // 输入框容器
-  // 针对没有 safeArea?.bottom 的手机,需要额外增加 12 高度
-  let inputContainerBottomOffset = 0
-  if(bottomSafeHeight <= 0){
-    inputContainerBottomOffset = 12
-  }
+  // InputBar 相关状态
+  const [isVoice, setIsVoice] = useState(false);
+  const [disabled, setDisabled] = useState(false);
 
-  const { destroy, setScrollTop, genSessionId, setAutoScroll } = useTextChat();
-  const scrollTop = useTextChat((state) => state.scrollTop);
+  const { destroy, genSessionId } = useTextChat();
 
-  // 放在组件里
-const initialScrolledRef = useRef(false);
-
-  const fetcher = async ([_url, { nextId, pageSize }]) => {
-    const _nextId = nextId ? decodeURIComponent(nextId) : nextId;
-    const res = await getMessageHistories({
-      agentId,
-      startId: _nextId,
-      pageSize,
-    });
-    return res.data;
-  };
-
-  // 获取历史聊天记录
-  const { list, loadMore, pageIndex, mutate } = useLoadMoreInfinite<
-    TMessage[] | TRobotMessage[]
-  >(createKey(`messeagehistories${isVisitor}${agentId}`), fetcher);
-
-  // 解析消息体 content 
-  const parsedList = list.filter((item)=> !!item.content.length).map(formatMessageItem);
-  // 1. 按 sessionId 分组,并记录每组的最早时间
-  const resultMap = useMemo(() => {
-    const allMessages = [...[...parsedList].reverse(), ...messageList];
-    return allMessages.reduce((acc, item) => {
-      const { sessionId, msgTime } = item;
-      if (!sessionId || !msgTime) {
-        return acc;
-      }
-      let _msgTime = msgTime.replace(/\-/g, "/");
-      if (!acc[sessionId]) {
-        acc[sessionId] = {
-          dt: _msgTime, // 初始化当前组的最早时间
-          list: [item], // 初始化当前组的记录列表
-        };
-      } else {
-        // 更新最早时间(如果当前记录的 msgTime 更早)
-        if (new Date(_msgTime) < new Date(acc[sessionId].dt)) {
-          acc[sessionId].dt = msgTime;
-          console.log('yoyoyo')
-        }
-        // 将记录添加到当前组
-        acc[sessionId].list.push(item);
-      }
-      return acc;
-    }, {}) as {
-      dt: string;
-      list: TAnyMessage[];
-    }[];
-  }, [parsedList, messageList]);
-
-  // 2. 转换为最终数组格式
-  const result = Object.values(resultMap);
-
-  const messagesLength = useMemo(() => result.length, [result.length]);
-  const prevLengthRef = useRef(messagesLength);
-
-  const [showWelcome, setShowWelcome] = useState(!list.length);
-  const haveBg =  (!!agent?.enabledChatBg && !!agent.avatarUrl?.length)
-
-  //  加载更多
-  const onScrollToUpper = () => {
-    console.log("onscroll");
-    loadMore();
-  };
+  // 统一的聊天输入逻辑 - 只在主组件中实例化一次
+  const chatInputActions = useChatInput({
+    agent,
+    enableOutputAudioStream: streamVoiceEnable,
+    setShowWelcome,
+    setIsVoice,
+    setDisabled,
+  });
 
-  const handleTouchMove = () => {
-    console.log("set auto scroll false");
-    setAutoScroll(false);
-  };
-  
+  // 页面显示时刷新数据
   useDidShow(() => {
     mutate();
   });
 
-  useEffect(() => {
-    if (agentId) {
-      if (isVisitor) {
-        fetchAgentProfile(agentId);
-      } else {
-        fetchAgent(agentId);
-      }
-    }
-  }, [agentId, isVisitor]);
-
-  // 是否显示欢迎 ui
-  useEffect(() => {
-    setShowWelcome(!messageList.length && !list.length);
-  }, [list, messageList]);
-
-  // 首次进入界面滚动到底
-  // 已有:messagesLength 是一个 number(来源于 result.length)
-  useEffect(() => {
-  // 仅首次 pageIndex === 1 且已经有消息时滚动一次
-  if (pageIndex === 1 && messagesLength > 0 && !initialScrolledRef.current) {
-    initialScrolledRef.current = true;
-    
-    // 下1秒再滚,确保 DOM 已完成渲染
-    setTimeout(() => {
-      setScrollTop()
-      console.log("首次进入,滚动到底部");
-    }, 1000);
-  }
-}, [pageIndex, messagesLength]);
-
   // 首次进入聊天生成 session id
   useEffect(() => {
     genSessionId();
-  }, []);
-
-  // 监听消息列表变化,触发键盘高度重新计算
-  useEffect(() => {
-    // 只在长度真正变化时才触发
-    if (prevLengthRef.current !== messagesLength) {
-      prevLengthRef.current = messagesLength;
-
-      // 使用 setTimeout 确保 DOM 更新完成后再计算高度
-      const timer = setTimeout(() => {
-        triggerHeightUpdate();
-      }, 100);
-
-      return () => clearTimeout(timer);
-    }
-  }, [messagesLength, triggerHeightUpdate]);
+  }, [genSessionId]);
 
+  // 页面卸载时清理
   useUnload(() => {
     destroy();
   });
 
-  const renderNavLeft = () => {
-    
-    return (
-      <View
-        className="flex items-center gap-8"
-        onClick={() => Taro.navigateBack()}
-      >
-        {haveBg ? <IconArrowLeftWhite24 /> : <IconArrowLeft />}
-        
-        <PersonalCard agent={agent} haveBg={haveBg} />
-      </View>
-    );
-  };
-
-  // 大背景可以是视频,也可以是图片
-  const getBgContent = () => {
-    if (!agent?.avatarUrl || !!!agent?.enabledChatBg) {
-      return "";
-    }
-    return agent?.avatarUrl;
-  };
-
-  useEffect(() => {
-    if(haveBg){
-      Taro.setNavigationBarColor({
-        frontColor: "#ffffff",
-        backgroundColor: "transparent",
-      });
-    }
-    return () => {
-      Taro.setNavigationBarColor({
-        frontColor: "#000000",
-        backgroundColor: "transparent",
-      });
-    };
-  }, [haveBg]);
+  // 加载更多的处理函数(已经在 useChatScrollManager 中处理)
+  const onScrollToUpper = handleScrollToUpper;
 
-  useEffect(()=> {
-    const query = Taro.createSelectorQuery();
-    // 输入框高度
-    query
-      .select('#inputContainer')
-      .boundingClientRect((rect: any) => {
-        if (rect) {
-          
-          // console.log('ScrollView height:', rect.height)
-          setInputContainerHeight(rect.height - bottomSafeHeight);
-        }
-      })
-      .exec();
-  }, [agent])
+  // 使用工厂函数创建导航栏左侧渲染器
+  const renderNavLeft = createNavLeftRenderer(PersonalCard, IconArrowLeftWhite24, IconArrowLeft);
 
   return (
     <PageCustom
@@ -273,18 +137,24 @@ const initialScrolledRef = useRef(false);
           scrollTop={scrollTop}
           scrollWithAnimation
           onScrollToUpper={onScrollToUpper}
-          onScrollToLower={() => setAutoScroll(true)}
+          onScrollToLower={handleScrollToLower}
         >
           <View
             id="messageList"
             className="flex flex-col gap-16 px-18"
             onTouchMove={handleTouchMove}
           >
-            {showWelcome && <ChatGreeting agent={agent} />}
-            {result.map((group) => {
+            {showWelcome && <ChatGreeting agent={agent} chatInputActions={chatInputActions} />}
+            {groupedMessages.map((group, groupIndex) => {
               return (
-                <>
-                  <View className={`text-12 leading-20 block text-center w-full ${(agent?.enabledChatBg && !!agent?.avatarUrl?.length) ? 'text-white-70' : 'text-black-25'}`}>
+                <View key={groupIndex} className="flex flex-col gap-16">
+                  <View
+                    className={`text-12 leading-20 block text-center w-full ${
+                      haveBg
+                        ? "text-white-70"
+                        : "text-black-25"
+                    }`}
+                  >
                     {formatMessageTime(group.dt)}
                   </View>
                   {group.list.map((message) => {
@@ -298,21 +168,29 @@ const initialScrolledRef = useRef(false);
                         role={message.role}
                         text={message.content}
                         message={message}
-                      ></ChatMessage>
+                      />
                     );
                   })}
-                </>
+                </View>
               );
             })}
           </View>
           <View className="pb-40 pt-8">
-            {agent && <RecommendQuestions enableOutputAudioStream={streamVoiceEnable} agent={agent} />}
+            {agent && (
+              <RecommendQuestions
+                enableOutputAudioStream={streamVoiceEnable}
+                agent={agent}
+                chatInputActions={chatInputActions}
+              />
+            )}
           </View>
         </ScrollView>
-        <View className="w-full h-60" style={{
-          height: inputContainerHeight,
-        }}>
-        </View>
+        <View
+          className="w-full h-60"
+          style={{
+            height: inputContainerHeight,
+          }}
+        ></View>
         <View
           className="bottom-bar px-16 pt-12 z-50"
           id="inputContainer"
@@ -321,19 +199,26 @@ const initialScrolledRef = useRef(false);
           }}
         >
           <View className="bg-[#F5FAFF]">
-            {agent &&
+            {agent && (
               <View className="flex flex-col w-full gap-8">
                 <InputBar
                   enableOutputAudioStream={streamVoiceEnable}
                   agent={agent}
-                  histories={list}
+                  // histories={historyList}
                   setShowWelcome={setShowWelcome}
-                ></InputBar>
+                  chatInputActions={chatInputActions}
+                  isVoice={isVoice}
+                  setIsVoice={setIsVoice}
+                  disabled={disabled}
+                />
                 <View>
-                  <ButtonEnableStreamVoice setEnable={setStreamVoiceEnable} enable={streamVoiceEnable}></ButtonEnableStreamVoice>
+                  <ButtonEnableStreamVoice
+                    setEnable={setStreamVoiceEnable}
+                    enable={streamVoiceEnable}
+                  />
                 </View>
               </View>
-            }
+            )}
           </View>
         </View>
       </View>

+ 10 - 3
src/service/chat.ts

@@ -122,10 +122,11 @@ export const requestTextToSpeech = ({
       },
       responseType: "arraybuffer",
       success: function (res) {
-        console.log("text to speech 服务端响应 >>", res);
-        onComplete?.()
+        // console.log("text to speech 服务端响应 >>", res);
+        // onComplete?.()
       },
       complete: function(res) {
+        console.log("text to speech 服务端响应 complete", res);
         onComplete?.()
       }
     });
@@ -201,10 +202,13 @@ export const requestTextToChat = ({
       responseType: "arraybuffer",
       success: function (res) {
         console.log("服务端响应 >>", res);
-        onComplete?.()
       },
       complete: function(res) {
+        console.log(reqTask)
+        console.log("reqTask >>", reqTask, res);
+       if(reqTask) {
         onComplete?.()
+       }
       }
     });
 
@@ -217,6 +221,9 @@ export const requestTextToChat = ({
 
   const stopChunk = () => {
     reqTask?.offChunkReceived(onChunkReceived);
+    reqTask?.abort();
+    reqTask = null
+    console.log('stop reqTask: v1/chat/completions')
   };
 
   return {reqTask: reqTask, stopChunk};

+ 8 - 41
src/store/textChat.ts → src/store/textChatStore.ts

@@ -15,12 +15,6 @@ type TRobotMessageWithOptionalId = Omit<TRobotMessage, "msgUk"> & {
   reasoningContent?: string; // 添加 reasoningContent
 };
 
-type TCurrentTextToSpeech = {
-  agentId: string,
-  loginId: string,
-  msgUk: string,
-  text: string
-}
 
 const INIT_CURRENT_ROBOT_MSG_UK = ''
 
@@ -33,7 +27,7 @@ export interface TextChat {
   sessionId: string|null
   isReacting: boolean // 是否正在聊天交互
   messageRespeaking: boolean; // 当前正在重放回复消息内容 text to speech
-  messageSpeakersStopHandle: (()=>void) | null; // 点击消息框内重播播放器停止方法引用
+  messageStopHandle: (()=>void) | null; // 停止接收智能文本体消或语音消息的句柄
   // 显示聊天历史
   setAutoScroll: (b: boolean) => void // 是否自动滚动
   genSessionId: () => void // 进入聊天界面后,本次的 sessionId
@@ -47,14 +41,12 @@ export interface TextChat {
   updateMessage: (content: string, msgUk: string) => string;
   // 更新机器人汽泡框内的内容实现 gpt 的效果
   updateRobotMessage: (content: string, body?: Record<string,any>, saveStatus?: number, replaceContent?: boolean) => void;
-  updateRobotReasoningMessage: (msgUk: string, reasoningContent: string, body?:Record<string,any>) => void;
   getCurrentRobotMessage:() => TRobotMessage|undefined
   deleteMessage: (msgUk: string) => void;
   // 清空
   destroy: () => void;
-  fetchMessageHistories: (data: TGetMessageHistoriesParams) => void
   setMessageRespeaking: (respeaking: boolean)=> void
-  setMessageSpeakersStopHandle: (data: (()=> void) | null) => void
+  setMessageStopHandle: (func: (()=> void) | null) => void
   setReacting: (reacting: boolean)=> void
 }
 
@@ -65,19 +57,19 @@ const generateUk = () => {
 
 export const useTextChat = create<TextChat>((set, get) => ({
   currentRobotMsgUk: INIT_CURRENT_ROBOT_MSG_UK,
-  scrollTop: 999999,
+  scrollTop: 0,
   autoScroll: true,
   list: [],
   sessionId: null,
   questions: [],
   isReacting: false,
   messageRespeaking: false,
-  messageSpeakersStopHandle: null,
+  messageStopHandle: null,
   setMessageRespeaking: (respeaking)=> {
     set({messageRespeaking: respeaking})
   },
-  setMessageSpeakersStopHandle: (func)=> {
-    set({messageSpeakersStopHandle: func})
+  setMessageStopHandle: (func)=> {
+    set({messageStopHandle: func})
   },
   setReacting: (isReacting)=> {
     set({isReacting})
@@ -100,7 +92,7 @@ export const useTextChat = create<TextChat>((set, get) => ({
       list: [],
       questions: [],
       currentRobotMsgUk: INIT_CURRENT_ROBOT_MSG_UK,
-      scrollTop: 9999,
+      scrollTop: 0,
       sessionId: null,
     });
   },
@@ -145,7 +137,7 @@ export const useTextChat = create<TextChat>((set, get) => ({
     return {msgUk, sessionId}
   },
   setScrollTop: (num?: number)=> {
-    console.log(get().autoScroll, get().scrollTop, 'xxxxx')
+    console.log('setScrollTop')
     set((state) => {
       if(num!== undefined){
         return {
@@ -192,20 +184,6 @@ export const useTextChat = create<TextChat>((set, get) => ({
     const currentMessage = state.list.find((message)=>  message.msgUk === state.currentRobotMsgUk) as TRobotMessage
     return currentMessage
   },
-  updateRobotReasoningMessage: (msgUk, reasoningContent, body={}) => {
-    set((state) => {
-      // 由于 currentRobotMsgUk 可能发生变化, 所以需要传递 msgUk
-      // console.log(state.currentRobotMsgUk, msgUk)
-      const updatedList = state.list.map((message) => {
-        if (message.msgUk === msgUk) {
-          //@ts-ignore
-          return { ...message, body, reasoningContent: message.reasoningContent + reasoningContent } as TRobotMessage // 更新 reasoningContent
-        }
-        return message; // 返回未修改的 message
-      });
-      return { list: updatedList, scrollTop: state.autoScroll ?   state.scrollTop + 1 : state.scrollTop}; // 返回新的状态
-    });
-  },
   deleteMessage: (msgUk)=> {
     set((state) => {
       // 如果对话框是空的,则删除
@@ -215,16 +193,5 @@ export const useTextChat = create<TextChat>((set, get) => ({
       });
       return { list: filtered, scrollTop: state.scrollTop + 1 }; // 返回新的状态
     });
-  },
-  // 停止当前机器人说话输出框输出
-  stopCurrentRobotMessaging: ()=> {
-    set({currentRobotMsgUk: INIT_CURRENT_ROBOT_MSG_UK})
-  },
-  fetchMessageHistories: async (data: TGetMessageHistoriesParams) => {
-    const response = await getMessageHistories(data)
-    console.log(response)
-    if(response.status){
-      
-    }
   }
 }));

+ 1 - 1
src/types/bot.ts

@@ -101,7 +101,7 @@ export type TAppendMessage  = {
     msgUk: string,
     saveStatus: number,  //0|1|2
     role: TChatRole
-}
+} & TAnyMessage
 export type TAppendMessages = {
   agentId: string,
   loginId: string,

+ 22 - 14
src/utils/audioStreamPlayer.ts

@@ -48,7 +48,7 @@ export class AudioStreamPlayer {
   private totalLength = WAV_HEADER_LENGTH;
   private playing = false;
   private wavHeader: ArrayBuffer | null = null;
-  private endChunk = false;
+  public isLastChunk = false;
   private playStatusChangedCallback?: (status: TPlayStatus) => void;
 
   constructor() {
@@ -64,7 +64,7 @@ export class AudioStreamPlayer {
       this.requestTask = _requestTask;
     }
     this.enablePlay = true;
-    this.endChunk = false;
+    this.isLastChunk = false;
     
     let chunk = decode(base64Str);
     this.emptyQuene();
@@ -86,8 +86,8 @@ export class AudioStreamPlayer {
   };
 
   // 标定是否为最后一个 chunk 
-  setEndChunk = () => {
-    this.endChunk = true;
+  setIsLastChunk = () => {
+    this.isLastChunk = true;
   }
 
   private emptyQuene = () => {
@@ -131,7 +131,7 @@ export class AudioStreamPlayer {
         this.source.onended = () => {
           console.info("play end");
           this.playing = false;
-          if (!this.chunks.length && this.endChunk) {
+          if (!this.chunks.length && this.isLastChunk) {
             this.changeStatus('stop');
           } else {
             this.playChunk();
@@ -150,19 +150,25 @@ export class AudioStreamPlayer {
     this.playStatusChangedCallback = callback;
   }
 
+  initPlay = ()=> {
+    this.source?.stop();
+    this.emptyQuene();
+    this.enablePlay = false;
+  }
+
   stopPlayChunk = () => {
     // 如果有请求任务取消
-    if (this.requestTask) {
-      //@ts-ignore
-      this.requestTask?.abort?.();
-      //@ts-ignore
-      this.requestTask?.offChunkReceived?.();
-    }
+    // if (this.requestTask) {
+    //   //@ts-ignore
+    //   this.requestTask?.abort?.();
+    //   //@ts-ignore
+    //   this.requestTask?.offChunkReceived?.();
+    // }
     this.source?.stop();
     this.emptyQuene();
-
+    this.changeStatus('stop')
     this.enablePlay = false;
-    this.changeStatus('stop');
+    
   };
 
   // 获取当前播放状态
@@ -206,9 +212,10 @@ export const useAudioStreamPlayer = () => {
   }, []);
   
   return {
+    initPlay: playerRef.current.initPlay,
     pushChunk2Quene: playerRef.current.pushChunk2Quene,
     pushBase64ToQuene: playerRef.current.pushBase64ToQuene,
-    setEndChunk: playerRef.current.setEndChunk,
+    setIsLastChunk: playerRef.current.setIsLastChunk,
     playChunk: playerRef.current.playChunk,
     stopPlayChunk: playerRef.current.stopPlayChunk,
     stopPlay: playerRef.current.stopPlay,
@@ -216,5 +223,6 @@ export const useAudioStreamPlayer = () => {
     onPlayerStatusChanged: playerRef.current.onPlayerStatusChanged,
     getPlayingStatus: playerRef.current.getPlayingStatus,
     destroy: playerRef.current.destroy,
+    isLastChunk: playerRef.current.isLastChunk,
   };
 };