perf: optimize/message performance (#3181)

* feat: Add message pause and resume functionality

- Implement pauseMessage and pauseMessages methods in useMessageOperations hook
- Update Inputbar to use new pauseMessages method for stopping message generation
- Remove deprecated pause-related code from ApiService and store
- Simplify message generation and pause logic across providers
- Enhance message state management with more granular control over streaming messages

* feat: Enhance topic management with sequence-based sorting and lazy loading

- Add sequence field to topics for better sorting
- Implement lazy loading mechanism for topic messages
- Modify Redux store to support per-topic loading states
- Update database schema to use sequence as an auto-incrementing primary key
- Optimize message initialization and retrieval process

* refactor(database): Enhance topic management with timestamps and upgrade logic

- Modify database schema to include createdAt and updatedAt for topics
- Add database hooks for automatic timestamp handling
- Refactor topic upgrade process to support new timestamp fields
- Remove redundant upgradesV6.ts file
- Update topic retrieval to use updatedAt for sorting
- Improve database consistency and tracking of topic modifications

* refactor: Streamline message state management and remove unused code

- Remove commented-out code in multiple components
- Delete initializeMessagesState thunk from messages store
- Simplify message sending and streaming logic
- Remove unnecessary console logs
- Optimize MessageStream component with memo
- Using loading to control message generation within a single session
- Lift the restriction on not being able to switch topics in message generation

* refactor(database): Remove version 6 database version and hooks

- Remove version 6 database schema definition
- Delete automatic timestamp hooks for topics
- Clean up unused database upgrade and hook code

* refactor(Messages): Optimize message state management and remove redundant code

- Remove duplicate imports and redundant code blocks
- Simplify message sending and streaming logic in messages store
- Enhance throttling mechanism for message updates
- Remove commented-out code and unused function parameters
- Improve error handling and loading state management
- Optimize message synchronization with database

* fix:console
This commit is contained in:
MyPrototypeWhat 2025-03-11 17:31:44 +08:00 committed by GitHub
parent a25c0e657b
commit c69c750144
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 171 additions and 186 deletions

View File

@ -1,7 +1,7 @@
import { FileType, KnowledgeItem, Topic, TranslateHistory } from '@renderer/types' import { FileType, KnowledgeItem, Topic, TranslateHistory } from '@renderer/types'
import { Dexie, type EntityTable } from 'dexie' import { Dexie, type EntityTable } from 'dexie'
import { upgradeToV5, upgradeToV6 } from './upgrades' import { upgradeToV5 } from './upgrades'
// Database declaration (move this to its own module also) // Database declaration (move this to its own module also)
export const db = new Dexie('CherryStudio') as Dexie & { export const db = new Dexie('CherryStudio') as Dexie & {
files: EntityTable<FileType, 'id'> files: EntityTable<FileType, 'id'>
@ -46,27 +46,4 @@ db.version(5)
}) })
.upgrade((tx) => upgradeToV5(tx)) .upgrade((tx) => upgradeToV5(tx))
db.version(6)
.stores({
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
topics: '&id, messages, createdAt, updatedAt',
settings: '&id, value',
knowledge_notes: '&id, baseId, type, content, created_at, updated_at',
translate_history: '&id, sourceText, targetText, sourceLanguage, targetLanguage, createdAt'
})
.upgrade((tx) => upgradeToV6(tx))
// Add hooks for automatic timestamp handling
db.topics.hook('creating', (_, obj: any) => {
const now = new Date().toISOString()
obj.createdAt = now
obj.updatedAt = now
})
db.topics.hook('updating', (modifications: any) => {
if (typeof modifications === 'object') {
modifications.updatedAt = new Date().toISOString()
}
})
export default db export default db

View File

@ -37,6 +37,7 @@ export async function upgradeToV5(tx: Transaction): Promise<void> {
} }
} }
// 为每个 topic 添加时间戳,兼容老数据,默认按照最新的时间戳来,不确定是否要加
export async function upgradeToV6(tx: Transaction): Promise<void> { export async function upgradeToV6(tx: Transaction): Promise<void> {
const topics = await tx.table('topics').toArray() const topics = await tx.table('topics').toArray()

View File

@ -1,5 +1,6 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import store from '@renderer/store'
import { import {
clearStreamMessage, clearStreamMessage,
clearTopicMessages, clearTopicMessages,
@ -13,6 +14,7 @@ import {
updateMessages updateMessages
} from '@renderer/store/messages' } from '@renderer/store/messages'
import type { Assistant, Message, Topic } from '@renderer/types' import type { Assistant, Message, Topic } from '@renderer/types'
import { abortCompletion } from '@renderer/utils/abortController'
import { useCallback } from 'react' import { useCallback } from 'react'
/** /**
* Hook * Hook
@ -149,6 +151,52 @@ export function useMessageOperations(topic: Topic) {
// */ // */
// const getMessages = useCallback(() => messages, [messages]) // const getMessages = useCallback(() => messages, [messages])
/**
*
*/
const pauseMessage = useCallback(
async (messageId: string) => {
// 1. 调用 abort
abortCompletion(messageId)
// 2. 更新消息状态
await editMessage(messageId, { status: 'paused' })
// 3. 清理流式消息
clearStreamMessageAction(messageId)
},
[editMessage, clearStreamMessageAction]
)
const pauseMessages = useCallback(async () => {
// 从 store 获取当前 topic 的所有流式消息
const streamMessages = store.getState().messages.streamMessagesByTopic[topic.id]
if (streamMessages) {
// 获取所有流式消息的 askId
const askIds = new Set(
Object.values(streamMessages)
.map((msg) => msg.askId)
.filter(Boolean)
)
// 对每个 askId 执行暂停
for (const askId of askIds) {
await pauseMessage(askId)
}
}
}, [topic.id, pauseMessage])
/**
* /
*
*/
const resumeMessage = useCallback(
async (message: Message, assistant: Assistant) => {
return resendMessageAction(message, assistant)
},
[resendMessageAction]
)
return { return {
messages, messages,
loading, loading,
@ -163,6 +211,9 @@ export function useMessageOperations(topic: Topic) {
commitStreamMessage: commitStreamMessageAction, commitStreamMessage: commitStreamMessageAction,
clearStreamMessage: clearStreamMessageAction, clearStreamMessage: clearStreamMessageAction,
createNewContext, createNewContext,
clearTopicMessages: clearTopicMessagesAction clearTopicMessages: clearTopicMessagesAction,
pauseMessage,
pauseMessages,
resumeMessage
} }
} }

View File

@ -22,15 +22,15 @@ import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService' import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService' import { getUserMessage } from '@renderer/services/MessagesService'
import { estimateMessageUsage, estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService'
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
import WebSearchService from '@renderer/services/WebSearchService' import WebSearchService from '@renderer/services/WebSearchService'
import store, { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { sendMessage as _sendMessage } from '@renderer/store/messages' import { sendMessage as _sendMessage } from '@renderer/store/messages'
import { setGenerating, setSearching } from '@renderer/store/runtime' import { setSearching } from '@renderer/store/runtime'
import { Assistant, FileType, KnowledgeBase, MCPServer, Message, Model, Topic } from '@renderer/types' import { Assistant, FileType, KnowledgeBase, MCPServer, Message, Model, Topic } from '@renderer/types'
import { classNames, delay, getFileExtension } from '@renderer/utils' import { classNames, delay, getFileExtension } from '@renderer/utils'
import { abortCompletion } from '@renderer/utils/abortController'
import { getFilesFromDropEvent } from '@renderer/utils/input' import { getFilesFromDropEvent } from '@renderer/utils/input'
import { documentExts, imageExts, textExts } from '@shared/config/constant' import { documentExts, imageExts, textExts } from '@shared/config/constant'
import { Button, Popconfirm, Tooltip } from 'antd' import { Button, Popconfirm, Tooltip } from 'antd'
@ -84,7 +84,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const containerRef = useRef(null) const containerRef = useRef(null)
const { searching } = useRuntime() const { searching } = useRuntime()
const { isBubbleStyle } = useMessageStyle() const { isBubbleStyle } = useMessageStyle()
const { loading } = useMessageOperations(topic) const { loading, pauseMessages } = useMessageOperations(topic)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const [spaceClickCount, setSpaceClickCount] = useState(0) const [spaceClickCount, setSpaceClickCount] = useState(0)
const spaceClickTimer = useRef<NodeJS.Timeout>() const spaceClickTimer = useRef<NodeJS.Timeout>()
@ -140,14 +140,27 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
try { try {
// Dispatch the sendMessage action with all options // Dispatch the sendMessage action with all options
const uploadedFiles = await FileManager.uploadFiles(files) const uploadedFiles = await FileManager.uploadFiles(files)
dispatch( const userMessage = getUserMessage({ assistant, topic, type: 'text', content: text })
_sendMessage(text, assistant, topic, {
files: uploadedFiles, if (uploadedFiles) {
knowledgeBaseIds: selectedKnowledgeBases?.map((base) => base.id), userMessage.files = uploadedFiles
mentionModels, }
enabledMCPs const knowledgeBaseIds = selectedKnowledgeBases?.map((base) => base.id)
}) if (knowledgeBaseIds) {
) userMessage.knowledgeBaseIds = knowledgeBaseIds
}
if (mentionModels) {
userMessage.mentions = mentionModels
}
if (enabledMCPs) {
userMessage.enabledMCPs = enabledMCPs
}
userMessage.usage = await estimateMessageUsage(userMessage)
currentMessageId.current = userMessage.id
dispatch(_sendMessage(userMessage, assistant, topic))
// Clear input // Clear input
setText('') setText('')
@ -158,7 +171,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
} catch (error) { } catch (error) {
console.error('Failed to send message:', error) console.error('Failed to send message:', error)
} }
}, [inputEmpty, files, dispatch, text, assistant, selectedKnowledgeBases, mentionModels, enabledMCPs]) }, [inputEmpty, files, dispatch, text, assistant, topic, selectedKnowledgeBases, mentionModels, enabledMCPs, loading])
const translate = async () => { const translate = async () => {
if (isTranslating) { if (isTranslating) {
@ -282,24 +295,23 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
clickAssistantToShowTopic && setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0) clickAssistantToShowTopic && setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
}, [addTopic, assistant, clickAssistantToShowTopic, setActiveTopic, setModel]) }, [addTopic, assistant, clickAssistantToShowTopic, setActiveTopic, setModel])
const onPause = async () => {
await pauseMessages()
}
const clearTopic = async () => { const clearTopic = async () => {
if (loading) { if (loading) {
onPause() await onPause()
await delay(1) await delay(1)
} }
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES) EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
} }
const onPause = () => {
if (currentMessageId.current) {
abortCompletion(currentMessageId.current)
}
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
store.dispatch(setGenerating(false))
}
const onNewContext = () => { const onNewContext = () => {
if (loading) return onPause() if (loading) {
onPause()
return
}
EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT) EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)
} }

View File

@ -52,7 +52,6 @@ const MessageStream: React.FC<MessageStreamProps> = ({
// 在hooks调用后进行条件判断 // 在hooks调用后进行条件判断
const isStreaming = !!(streamMessage && streamMessage.id === _message.id) const isStreaming = !!(streamMessage && streamMessage.id === _message.id)
const message = isStreaming ? streamMessage : regularMessage const message = isStreaming ? streamMessage : regularMessage
console.log('isStreaming', isStreaming)
return ( return (
<MessageStreamContainer> <MessageStreamContainer>
<MessageItem <MessageItem

View File

@ -67,7 +67,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
}, []) }, [])
const onClearMessages = useCallback((topic: Topic) => { const onClearMessages = useCallback((topic: Topic) => {
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true) // window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
store.dispatch(setGenerating(false)) store.dispatch(setGenerating(false))
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES, topic) EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES, topic)
}, []) }, [])

View File

@ -1,4 +1,5 @@
import { fetchSuggestions } from '@renderer/services/ApiService' import { fetchSuggestions } from '@renderer/services/ApiService'
import { getUserMessage } from '@renderer/services/MessagesService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { sendMessage } from '@renderer/store/messages' import { sendMessage } from '@renderer/store/messages'
import { Assistant, Message, Suggestion } from '@renderer/types' import { Assistant, Message, Suggestion } from '@renderer/types'
@ -6,6 +7,7 @@ import { last } from 'lodash'
import { FC, memo, useEffect, useState } from 'react' import { FC, memo, useEffect, useState } from 'react'
import BeatLoader from 'react-spinners/BeatLoader' import BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
messages: Message[] messages: Message[]
@ -22,7 +24,9 @@ const Suggestions: FC<Props> = ({ assistant, messages }) => {
const [loadingSuggestions, setLoadingSuggestions] = useState(false) const [loadingSuggestions, setLoadingSuggestions] = useState(false)
const handleSuggestionClick = async (content: string) => { const handleSuggestionClick = async (content: string) => {
await dispatch(sendMessage(content, assistant, assistant.topics[0])) const userMessage = getUserMessage({ assistant, topic: assistant.topics[0], type: 'text', content })
await dispatch(sendMessage(userMessage, assistant, assistant.topics[0]))
} }
const suggestionsHandle = async () => { const suggestionsHandle = async () => {

View File

@ -323,10 +323,10 @@ export default class AnthropicProvider extends BaseProvider {
resolve() resolve()
}) })
.on('error', (error) => reject(error)) .on('error', (error) => reject(error))
}).finally(cleanup) })
} }
await processStream(body) await processStream(body).finally(cleanup)
} }
public async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) { public async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) {

View File

@ -222,7 +222,7 @@ export default class GeminiProvider extends BaseProvider {
const { abortController, cleanup } = this.createAbortController(lastUserMessage?.id) const { abortController, cleanup } = this.createAbortController(lastUserMessage?.id)
const { signal } = abortController const { signal } = abortController
const userMessagesStream = await chat.sendMessageStream(messageContents.parts, { signal }).finally(cleanup) const userMessagesStream = await chat.sendMessageStream(messageContents.parts, { signal })
let time_first_token_millsec = 0 let time_first_token_millsec = 0
const processStream = async (stream: GenerateContentStreamResult) => { const processStream = async (stream: GenerateContentStreamResult) => {
@ -275,8 +275,8 @@ export default class GeminiProvider extends BaseProvider {
parts: fcallParts parts: fcallParts
}) })
const newChat = geminiModel.startChat({ history }) const newChat = geminiModel.startChat({ history })
const newStream = await newChat.sendMessageStream(fcRespParts, { signal }).finally(cleanup) const newStream = await newChat.sendMessageStream(fcRespParts, { signal })
await processStream(newStream) await processStream(newStream).finally(cleanup)
} }
} }
@ -298,7 +298,7 @@ export default class GeminiProvider extends BaseProvider {
} }
} }
await processStream(userMessagesStream) await processStream(userMessagesStream).finally(cleanup)
} }
async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) { async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) {

View File

@ -428,7 +428,6 @@ export default class OpenAIProvider extends BaseProvider {
signal signal
} }
) )
.finally(cleanup)
await processStream(newStream) await processStream(newStream)
} }
@ -469,9 +468,8 @@ export default class OpenAIProvider extends BaseProvider {
signal signal
} }
) )
.finally(cleanup)
await processStream(stream) await processStream(stream).finally(cleanup)
} }
async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) { async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) {

View File

@ -3,7 +3,6 @@ import i18n from '@renderer/i18n'
import store from '@renderer/store' import store from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime' import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Model, Provider, Suggestion } from '@renderer/types' import { Assistant, Message, Model, Provider, Suggestion } from '@renderer/types'
import { addAbortController } from '@renderer/utils/abortController'
import { formatMessageError } from '@renderer/utils/error' import { formatMessageError } from '@renderer/utils/error'
import { cloneDeep, findLast, isEmpty } from 'lodash' import { cloneDeep, findLast, isEmpty } from 'lodash'
@ -31,24 +30,15 @@ export async function fetchChatCompletion({
assistant: Assistant assistant: Assistant
onResponse: (message: Message) => void onResponse: (message: Message) => void
}) { }) {
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, false)
const provider = getAssistantProvider(assistant) const provider = getAssistantProvider(assistant)
const webSearchProvider = WebSearchService.getWebSearchProvider() const webSearchProvider = WebSearchService.getWebSearchProvider()
const AI = new AiProvider(provider) const AI = new AiProvider(provider)
store.dispatch(setGenerating(true)) // store.dispatch(setGenerating(true))
onResponse({ ...message }) // onResponse({ ...message })
const pauseFn = (message: Message) => { // addAbortController(message.askId ?? message.id)
message.status = 'paused'
EventEmitter.emit(EVENT_NAMES.RECEIVE_MESSAGE, message)
store.dispatch(setGenerating(false))
onResponse({ ...message, status: 'paused' })
}
addAbortController(message.askId ?? message.id, pauseFn.bind(null, message))
try { try {
let _messages: Message[] = [] let _messages: Message[] = []
@ -136,9 +126,6 @@ export async function fetchChatCompletion({
message.error = formatMessageError(error) message.error = formatMessageError(error)
} }
// Update message status
message.status = window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED) ? 'paused' : message.status
// Emit chat completion event // Emit chat completion event
EventEmitter.emit(EVENT_NAMES.RECEIVE_MESSAGE, message) EventEmitter.emit(EVENT_NAMES.RECEIVE_MESSAGE, message)
onResponse(message) onResponse(message)

View File

@ -3,10 +3,9 @@ import db from '@renderer/databases'
import { TopicManager } from '@renderer/hooks/useTopic' import { TopicManager } from '@renderer/hooks/useTopic'
import { fetchChatCompletion } from '@renderer/services/ApiService' import { fetchChatCompletion } from '@renderer/services/ApiService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getAssistantMessage, getUserMessage, resetAssistantMessage } from '@renderer/services/MessagesService' import { getAssistantMessage, resetAssistantMessage } from '@renderer/services/MessagesService'
import { estimateMessageUsage } from '@renderer/services/TokenService'
import type { AppDispatch, RootState } from '@renderer/store' import type { AppDispatch, RootState } from '@renderer/store'
import type { Assistant, FileType, MCPServer, Message, Model, Topic } from '@renderer/types' import type { Assistant, Message, Topic } from '@renderer/types'
import { clearTopicQueue, getTopicQueue, waitForTopicQueue } from '@renderer/utils/queue' import { clearTopicQueue, getTopicQueue, waitForTopicQueue } from '@renderer/utils/queue'
import { throttle } from 'lodash' import { throttle } from 'lodash'
@ -205,24 +204,21 @@ export const {
clearStreamMessage clearStreamMessage
} = messagesSlice.actions } = messagesSlice.actions
const handleResponseMessageUpdate = (message, topicId, dispatch, getState) => { const handleResponseMessageUpdate = (message, topicId, dispatch) => {
dispatch(setStreamMessage({ topicId, message }))
// When message is complete, commit to messages and sync with DB // When message is complete, commit to messages and sync with DB
if (message.status !== 'pending') { // if (message.status !== 'pending') {
if (message.status === 'success') { // if (message.status === 'success') {
EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME) // EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME)
} // }
if (message.status !== 'sending') { // if (message.status !== 'sending') {
dispatch(commitStreamMessage({ topicId, messageId: message.id })) // dispatch(commitStreamMessage({ topicId, messageId: message.id }))
const state = getState() // const state = getState()
const topicMessages = state.messages.messagesByTopic[topicId] // const topicMessages = state.messages.messagesByTopic[topicId]
if (topicMessages) { // if (topicMessages) {
syncMessagesWithDB(topicId, topicMessages) // syncMessagesWithDB(topicId, topicMessages)
} // }
dispatch(setTopicLoading({ topicId, loading: false })) // }
} // }
}
} }
// Helper function to sync messages with database // Helper function to sync messages with database
@ -240,16 +236,12 @@ const syncMessagesWithDB = async (topicId: string, messages: Message[]) => {
// Modified sendMessage thunk // Modified sendMessage thunk
export const sendMessage = export const sendMessage =
( (
content: string, userMessage: Message,
assistant: Assistant, assistant: Assistant,
topic: Topic, topic: Topic,
options?: { options?: {
files?: FileType[]
knowledgeBaseIds?: string[]
mentionModels?: Model[]
resendUserMessage?: Message
resendAssistantMessage?: Message resendAssistantMessage?: Message
enabledMCPs?: MCPServer[] isMentionModel?: boolean
} }
) => ) =>
async (dispatch: AppDispatch, getState: () => RootState) => { async (dispatch: AppDispatch, getState: () => RootState) => {
@ -262,41 +254,11 @@ export const sendMessage =
dispatch(clearTopicMessages(topic.id)) dispatch(clearTopicMessages(topic.id))
} }
// 判断是否重发消息
const isResend = !!options?.resendUserMessage
// 使用用户消息
let userMessage: Message
if (isResend) {
userMessage = options.resendUserMessage
} else {
// 创建新的用户消息
userMessage = getUserMessage({ assistant, topic, type: 'text', content })
if (options?.files) {
userMessage.files = options.files
}
if (options?.knowledgeBaseIds) {
userMessage.knowledgeBaseIds = options.knowledgeBaseIds
}
if (options?.mentionModels) {
userMessage.mentions = options.mentionModels
}
if (options?.enabledMCPs) {
userMessage.enabledMCPs = options.enabledMCPs
}
userMessage.usage = await estimateMessageUsage(userMessage)
}
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE) EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE)
// 处理助手消息 // 处理助手消息
// let assistantMessage: Message
let assistantMessages: Message[] = [] let assistantMessages: Message[] = []
// 使用助手消息 if (options?.resendAssistantMessage) {
if (isResend && options.resendAssistantMessage) {
// 直接使用传入的助手消息,进行重置 // 直接使用传入的助手消息,进行重置
const messageToReset = options.resendAssistantMessage const messageToReset = options.resendAssistantMessage
const { model, id } = messageToReset const { model, id } = messageToReset
@ -307,9 +269,9 @@ export const sendMessage =
assistantMessages.push(resetMessage) assistantMessages.push(resetMessage)
} else { } else {
// 不是重发情况 // 不是重发情况
// 为每个被 mention 的模型创建一个助手消息 if (userMessage.mentions?.length) {
if (options?.mentionModels?.length) { // 为每个被 mention 的模型创建一个助手消息
assistantMessages = options.mentionModels.map((m) => { assistantMessages = userMessage.mentions.map((m) => {
const assistantMessage = getAssistantMessage({ assistant: { ...assistant, model: m }, topic }) const assistantMessage = getAssistantMessage({ assistant: { ...assistant, model: m }, topic })
assistantMessage.model = m assistantMessage.model = m
assistantMessage.askId = userMessage.id assistantMessage.askId = userMessage.id
@ -323,20 +285,16 @@ export const sendMessage =
assistantMessage.status = 'sending' assistantMessage.status = 'sending'
assistantMessages.push(assistantMessage) assistantMessages.push(assistantMessage)
} }
}
// 如果不是重发
!options?.resendAssistantMessage &&
dispatch( dispatch(
addMessage({ addMessage({
topicId: topic.id, topicId: topic.id,
messages: !isResend ? [userMessage, ...assistantMessages] : assistantMessages messages: !options?.isMentionModel ? [userMessage, ...assistantMessages] : assistantMessages
}) })
) )
}
const queue = getTopicQueue(topic.id) const queue = getTopicQueue(topic.id)
for (const assistantMessage of assistantMessages) { for (const assistantMessage of assistantMessages) {
// console.log('assistantMessage', assistantMessage)
// Set as stream message instead of adding to messages // Set as stream message instead of adding to messages
dispatch(setStreamMessage({ topicId: topic.id, message: assistantMessage })) dispatch(setStreamMessage({ topicId: topic.id, message: assistantMessage }))
@ -348,10 +306,9 @@ export const sendMessage =
await syncMessagesWithDB(topic.id, currentTopicMessages) await syncMessagesWithDB(topic.id, currentTopicMessages)
} }
// 保证请求有序,防止请求静态,限制并发数量 // 保证请求有序,防止请求静态,限制并发数量
queue.add(async () => { await queue.add(async () => {
try { try {
const state = getState() const messages = getState().messages.messagesByTopic[topic.id]
const messages = state.messages.messagesByTopic[topic.id]
if (!messages) { if (!messages) {
dispatch(clearTopicMessages(topic.id)) dispatch(clearTopicMessages(topic.id))
return return
@ -369,7 +326,13 @@ export const sendMessage =
} }
// 节流 // 节流
const throttledDispatch = throttle(handleResponseMessageUpdate, 100, { trailing: true }) // 100ms的节流时间应足够平衡用户体验和性能 const throttledDispatch = throttle(
(topicId, message) => dispatch(setStreamMessage({ topicId, message })),
100,
{ trailing: true }
) // 100ms的节流时间应足够平衡用户体验和性能
let resultMessage: Message = { ...assistantMessage }
await fetchChatCompletion({ await fetchChatCompletion({
message: { ...assistantMessage }, message: { ...assistantMessage },
@ -382,12 +345,27 @@ export const sendMessage =
assistant: assistantWithModel, assistant: assistantWithModel,
onResponse: async (msg) => { onResponse: async (msg) => {
// 允许在回调外维护一个最新的消息状态每次都更新这个对象但只通过节流函数分发到Redux // 允许在回调外维护一个最新的消息状态每次都更新这个对象但只通过节流函数分发到Redux
const updatedMsg = { ...msg, status: msg.status || 'pending', content: msg.content || '' } const updateMessage = { ...msg, status: msg.status || 'pending', content: msg.content || '' }
resultMessage = {
...assistantMessage,
...updateMessage
}
// 创建节流函数限制Redux更新频率 // 创建节流函数限制Redux更新频率
// 使用节流函数更新Redux // 使用节流函数更新Redux
throttledDispatch({ ...assistantMessage, ...updatedMsg }, topic.id, dispatch, getState) throttledDispatch(topic.id, resultMessage)
} }
}) })
if (resultMessage?.status === 'success') {
EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME)
}
if (resultMessage?.status !== 'sending') {
dispatch(commitStreamMessage({ topicId: topic.id, messageId: assistantMessage.id }))
const state = getState()
const topicMessages = state.messages.messagesByTopic[topic.id]
if (topicMessages) {
syncMessagesWithDB(topic.id, topicMessages)
}
}
} catch (error: any) { } catch (error: any) {
console.error('Error in chat completion:', error) console.error('Error in chat completion:', error)
dispatch( dispatch(
@ -402,6 +380,9 @@ export const sendMessage =
} }
}) })
} }
// 等待所有请求完成,设置loading
await queue.onIdle()
dispatch(setTopicLoading({ topicId: topic.id, loading: false }))
} catch (error: any) { } catch (error: any) {
console.error('Error in sendMessage:', error) console.error('Error in sendMessage:', error)
dispatch(setError(error.message)) dispatch(setError(error.message))
@ -425,8 +406,7 @@ export const resendMessage =
const assistantMessage = topicMessages.find((m) => m.role === 'assistant' && m.askId === message.id) const assistantMessage = topicMessages.find((m) => m.role === 'assistant' && m.askId === message.id)
return dispatch( return dispatch(
sendMessage(message.content, assistant, topic, { sendMessage(message, assistant, topic, {
resendUserMessage: message,
resendAssistantMessage: assistantMessage resendAssistantMessage: assistantMessage
}) })
) )
@ -434,55 +414,38 @@ export const resendMessage =
// 如果是助手消息,找到对应的用户消息 // 如果是助手消息,找到对应的用户消息
const userMessage = topicMessages.find((m) => m.id === message.askId && m.role === 'user') const userMessage = topicMessages.find((m) => m.id === message.askId && m.role === 'user')
console.log('topicMessages,topicMessages', topicMessages)
if (!userMessage) { if (!userMessage) {
console.error('Cannot find original user message to resend') console.error('Cannot find original user message to resend')
return dispatch(setError('Cannot find original user message to resend')) return dispatch(setError('Cannot find original user message to resend'))
} }
if (isMentionModel) { if (isMentionModel) {
// @ // @,追加助手消息
return dispatch( return dispatch(sendMessage(userMessage, assistant, topic, { isMentionModel }))
sendMessage(userMessage.content, assistant, topic, {
resendUserMessage: userMessage
})
)
} }
dispatch( dispatch(
sendMessage(userMessage.content, assistant, topic, { sendMessage(userMessage, assistant, topic, {
resendUserMessage: userMessage,
resendAssistantMessage: message resendAssistantMessage: message
}) })
) )
} catch (error: any) { } catch (error: any) {
console.error('Error in resendMessage:', error) console.error('Error in resendMessage:', error)
dispatch(setError(error.message)) dispatch(setError(error.message))
} finally {
dispatch(setTopicLoading({ topicId: topic.id, loading: false }))
} }
} }
// Modified loadTopicMessages thunk // Modified loadTopicMessages thunk
export const loadTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDispatch) => { export const loadTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDispatch) => {
// 设置会话的loading状态
dispatch(setTopicLoading({ topicId: topic.id, loading: true }))
dispatch(setCurrentTopic(topic))
try { try {
// 设置会话的loading状态 // 使用 getTopic 获取会话对象
dispatch(setTopicLoading({ topicId: topic.id, loading: true })) const topicWithDB = await TopicManager.getTopic(topic.id)
dispatch(setCurrentTopic(topic)) if (topicWithDB) {
try { // 如果数据库中有会话,加载消息,保存会话
// 使用 getTopic 获取会话对象 dispatch(loadTopicMessages({ topicId: topic.id, messages: topicWithDB.messages }))
const topicWithDB = await TopicManager.getTopic(topic.id)
if (topicWithDB) {
// 如果数据库中有会话,加载消息,保存会话
dispatch(loadTopicMessages({ topicId: topic.id, messages: topicWithDB.messages }))
}
// else {
// // 如果找不到,可以将当前会话设为 null
// dispatch(setCurrentTopic(null))
// }
} catch (error) {
console.error('Failed to get complete topic:', error)
dispatch(setCurrentTopic(null))
} }
} catch (error) { } catch (error) {
dispatch(setError(error instanceof Error ? error.message : 'Failed to load messages')) dispatch(setError(error instanceof Error ? error.message : 'Failed to load messages'))
@ -491,7 +454,6 @@ export const loadTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDisp
dispatch(setTopicLoading({ topicId: topic.id, loading: false })) dispatch(setTopicLoading({ topicId: topic.id, loading: false }))
} }
} }
// Modified clearMessages thunk // Modified clearMessages thunk
export const clearTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDispatch) => { export const clearTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDispatch) => {
try { try {
@ -521,9 +483,6 @@ export const clearTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDis
// 修改的 updateMessages thunk同时更新缓存 // 修改的 updateMessages thunk同时更新缓存
export const updateMessages = (topic: Topic, messages: Message[]) => async (dispatch: AppDispatch) => { export const updateMessages = (topic: Topic, messages: Message[]) => async (dispatch: AppDispatch) => {
try { try {
// 设置会话的loading状态
dispatch(setTopicLoading({ topicId: topic.id, loading: true }))
// 更新数据库 // 更新数据库
await db.topics.update(topic.id, { messages }) await db.topics.update(topic.id, { messages })
@ -531,9 +490,6 @@ export const updateMessages = (topic: Topic, messages: Message[]) => async (disp
dispatch(loadTopicMessages({ topicId: topic.id, messages })) dispatch(loadTopicMessages({ topicId: topic.id, messages }))
} catch (error) { } catch (error) {
dispatch(setError(error instanceof Error ? error.message : 'Failed to update messages')) dispatch(setError(error instanceof Error ? error.message : 'Failed to update messages'))
} finally {
// 清除会话的loading状态
dispatch(setTopicLoading({ topicId: topic.id, loading: false }))
} }
} }