diff --git a/src/renderer/src/hooks/useMessageOperations.ts b/src/renderer/src/hooks/useMessageOperations.ts index 40cda0213f..8cc82063b3 100644 --- a/src/renderer/src/hooks/useMessageOperations.ts +++ b/src/renderer/src/hooks/useMessageOperations.ts @@ -79,7 +79,7 @@ export function useMessageOperations(topic: Topic) { ) /** - * 编辑消息。(目前仅更新 Redux state)。 / Edits a message. (Currently only updates Redux state). + * 编辑消息。 / Edits a message. * 使用 newMessagesActions.updateMessage. */ const editMessage = useCallback( @@ -92,17 +92,12 @@ export function useMessageOperations(topic: Topic) { const messageUpdates: Partial & Pick = { id: messageId, + updatedAt: new Date().toISOString(), ...updates } // Call the thunk with topic.id and only message updates - const success = await dispatch(updateMessageAndBlocksThunk(topic.id, messageUpdates, [])) - - if (success) { - console.log(`[useMessageOperations] Successfully edited message ${messageId} properties.`) - } else { - console.error(`[useMessageOperations] Failed to edit message ${messageId} properties.`) - } + await dispatch(updateMessageAndBlocksThunk(topic.id, messageUpdates, [])) }, [dispatch, topic.id] ) @@ -133,9 +128,16 @@ export function useMessageOperations(topic: Topic) { const files = findFileBlocks(message).map((block) => block.file) const usage = await estimateUserPromptUsage({ content: editedContent, files }) + const messageUpdates: Partial & Pick = { + id: message.id, + updatedAt: new Date().toISOString(), + usage + } - await dispatch(updateMessageAndBlocksThunk(topic.id, { id: message.id, usage }, [])) - + await dispatch( + newMessagesActions.updateMessage({ topicId: topic.id, messageId: message.id, updates: messageUpdates }) + ) + // 对于message的修改会在下面的thunk中保存 await dispatch(resendUserMessageWithEditThunk(topic.id, message, mainTextBlockId, editedContent, assistant)) }, [dispatch, topic.id] @@ -313,29 +315,23 @@ export function useMessageOperations(topic: Topic) { * Uses the generalized thunk for persistence. */ const editMessageBlocks = useCallback( - // messageId?: string - async (blockUpdatesListRaw: Partial[]) => { + async (messageId: string, updates: Partial) => { if (!topic?.id) { console.error('[editMessageBlocks] Topic prop is not valid.') return } - if (!blockUpdatesListRaw || blockUpdatesListRaw.length === 0) { - console.warn('[editMessageBlocks] Received empty block updates list.') - return + + const blockUpdatesListProcessed = { + updatedAt: new Date().toISOString(), + ...updates } - const blockUpdatesListProcessed = blockUpdatesListRaw.map((update) => ({ - ...update, + const messageUpdates: Partial & Pick = { + id: messageId, updatedAt: new Date().toISOString() - })) - - const success = await dispatch(updateMessageAndBlocksThunk(topic.id, null, blockUpdatesListProcessed)) - - if (success) { - // console.log(`[useMessageOperations] Successfully processed block updates for message ${messageId}.`) - } else { - // console.error(`[useMessageOperations] Failed to process block updates for message ${messageId}.`) } + + await dispatch(updateMessageAndBlocksThunk(topic.id, messageUpdates, [blockUpdatesListProcessed])) }, [dispatch, topic.id] ) diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index 2e6d4eb724..06c5fd89ee 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -107,14 +107,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) => // Convert class to object with functions since class only has static methods // 只有静态方法,没必要用class,可以export {} export const TopicManager = { - async getTopicLimit(limit: number) { - return await db.topics - .orderBy('updatedAt') // 按 updatedAt 排序(默认升序) - .reverse() // 逆序(变成降序) - .limit(limit) // 取前 10 条 - .toArray() - }, - async getTopic(id: string) { return await db.topics.get(id) }, diff --git a/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx index b04a5dfe2d..0d9156a885 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx @@ -75,7 +75,6 @@ const MainTextBlock: React.FC = ({ block, citationBlockId, role, mentions let processedContent = content const firstCitation = formattedCitations[0] if (firstCitation?.metadata) { - console.log('groundingSupport, ', firstCitation.metadata) firstCitation.metadata.forEach((support: GroundingSupport) => { const citationNums = support.groundingChunkIndices! @@ -126,12 +125,14 @@ const MainTextBlock: React.FC = ({ block, citationBlockId, role, mentions title: citation.title || citation.hostname || '', content: citation.content?.substring(0, 200) } + const isLink = citation.url.startsWith('http') const citationJson = encodeHTML(JSON.stringify(supData)) // Handle both plain references [N] and pre-formatted links [N](url) const plainRefRegex = new RegExp(`\\[${citationNum}\\]`, 'g') - const citationTag = `[${citationNum}](${citation.url})` + const supTag = `${citationNum}` + const citationTag = isLink ? `[${supTag}](${citation.url})` : supTag content = content.replace(plainRefRegex, citationTag) }) diff --git a/src/renderer/src/pages/home/Messages/MessageHeader.tsx b/src/renderer/src/pages/home/Messages/MessageHeader.tsx index 268b28ac99..cf25fd36f1 100644 --- a/src/renderer/src/pages/home/Messages/MessageHeader.tsx +++ b/src/renderer/src/pages/home/Messages/MessageHeader.tsx @@ -102,7 +102,7 @@ const MessageHeader: FC = memo(({ assistant, model, message }) => { {username} - {dayjs(message.createdAt).format('MM/DD HH:mm')} + {dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')} diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 918bd9cf9d..8a99e491dd 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -164,7 +164,7 @@ const MessageMenubar: FC = (props) => { if (resendMessage) { resendUserMessageWithEdit(message, editedText, assistant) } else { - editMessageBlocks([{ ...findMainTextBlocks(message)[0], content: editedText }]) + editMessageBlocks(message.id, { id: findMainTextBlocks(message)[0].id, content: editedText }) } // // 更新消息内容,保留图片信息 // await editMessage(message.id, { diff --git a/src/renderer/src/services/StreamProcessingService.ts b/src/renderer/src/services/StreamProcessingService.ts index 1d8bef15b3..4abf71bd6a 100644 --- a/src/renderer/src/services/StreamProcessingService.ts +++ b/src/renderer/src/services/StreamProcessingService.ts @@ -40,7 +40,7 @@ export function createStreamProcessor(callbacks: StreamProcessorCallbacks = {}) // The returned function processes a single chunk or a final signal return (chunk: Chunk) => { try { - // console.log(`[${new Date().toLocaleString()}] createStreamProcessor ${chunk.type}`, chunk) + console.log(`[${new Date().toLocaleString()}] createStreamProcessor ${chunk.type}`, chunk) // 1. Handle the manual final signal first if (chunk?.type === ChunkType.BLOCK_COMPLETE) { callbacks.onComplete?.(AssistantMessageStatus.SUCCESS, chunk?.response) diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index 80fea11787..ac69002148 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -91,6 +91,7 @@ const updateExistingMessageAndBlocksInDB = async ( const newMessages = [...topic.messages] // Apply the updates passed in updatedMessage Object.assign(newMessages[messageIndex], updatedMessage) + console.log('updateExistingMessageAndBlocksInDB', updatedMessage) await db.topics.update(updatedMessage.topicId, { messages: newMessages }) } else { console.error(`[updateExistingMsg] Message ${updatedMessage.id} not found in topic ${updatedMessage.topicId}`) @@ -106,44 +107,46 @@ const updateExistingMessageAndBlocksInDB = async ( } // 更新单个块的逻辑,用于更新消息中的单个块 -const throttledBlockUpdate = throttle((id, blockUpdate) => { - const state = store.getState() - const block = state.messageBlocks.entities[id] +const throttledBlockUpdate = throttle(async (id, blockUpdate) => { + // const state = store.getState() + // const block = state.messageBlocks.entities[id] // throttle是异步函数,可能会在complete事件触发后才执行 - if ( - blockUpdate.status === MessageBlockStatus.STREAMING && - (block?.status === MessageBlockStatus.SUCCESS || block?.status === MessageBlockStatus.ERROR) - ) - return + // if ( + // blockUpdate.status === MessageBlockStatus.STREAMING && + // (block?.status === MessageBlockStatus.SUCCESS || block?.status === MessageBlockStatus.ERROR) + // ) + // return store.dispatch(updateOneBlock({ id, changes: blockUpdate })) + await db.message_blocks.update(id, blockUpdate) }, 150) -// 修改: 节流更新单个块的内容/状态到数据库 (仅用于 Text/Thinking Chunks) -export const throttledBlockDbUpdate = throttle( - async (blockId: string, blockChanges: Partial) => { - // Check if blockId is valid before attempting update - if (!blockId) { - console.warn('[DB Throttle Block Update] Attempted to update with null/undefined blockId. Skipping.') - return - } - const state = store.getState() - const block = state.messageBlocks.entities[blockId] - // throttle是异步函数,可能会在complete事件触发后才执行 - if ( - blockChanges.status === MessageBlockStatus.STREAMING && - (block?.status === MessageBlockStatus.SUCCESS || block?.status === MessageBlockStatus.ERROR) - ) - return - try { - await db.message_blocks.update(blockId, blockChanges) - } catch (error) { - console.error(`[DB Throttle Block Update] Failed for block ${blockId}:`, error) - } - }, - 300, // 可以调整节流间隔 - { leading: false, trailing: true } -) +const cancelThrottledBlockUpdate = throttledBlockUpdate.cancel + +// // 修改: 节流更新单个块的内容/状态到数据库 (仅用于 Text/Thinking Chunks) +// export const throttledBlockDbUpdate = throttle( +// async (blockId: string, blockChanges: Partial) => { +// // Check if blockId is valid before attempting update +// if (!blockId) { +// console.warn('[DB Throttle Block Update] Attempted to update with null/undefined blockId. Skipping.') +// return +// } +// const state = store.getState() +// const block = state.messageBlocks.entities[blockId] +// // throttle是异步函数,可能会在complete事件触发后才执行 +// if ( +// blockChanges.status === MessageBlockStatus.STREAMING && +// (block?.status === MessageBlockStatus.SUCCESS || block?.status === MessageBlockStatus.ERROR) +// ) +// return +// try { +// } catch (error) { +// console.error(`[DB Throttle Block Update] Failed for block ${blockId}:`, error) +// } +// }, +// 300, // 可以调整节流间隔 +// { leading: false, trailing: true } +// ) // 新增: 通用的、非节流的函数,用于保存消息和块的更新到数据库 const saveUpdatesToDB = async ( @@ -152,6 +155,7 @@ const saveUpdatesToDB = async ( messageUpdates: Partial, // 需要更新的消息字段 blocksToUpdate: MessageBlock[] // 需要更新/创建的块 ) => { + console.log('messageUpdates', messageUpdates) try { const messageDataToSave: Partial & Pick = { id: messageId, @@ -338,7 +342,7 @@ const fetchAndProcessAssistantResponseImpl = async ( status: MessageBlockStatus.STREAMING } throttledBlockUpdate(lastBlockId, blockChanges) - throttledBlockDbUpdate(lastBlockId, blockChanges) + // throttledBlockDbUpdate(lastBlockId, blockChanges) } else { const newBlock = createMainTextBlock(assistantMsgId, accumulatedContent, { status: MessageBlockStatus.STREAMING, @@ -349,7 +353,7 @@ const fetchAndProcessAssistantResponseImpl = async ( } } }, - onTextComplete: (finalText) => { + onTextComplete: async (finalText) => { if (lastBlockType === MessageBlockType.MAIN_TEXT && lastBlockId) { const changes = { content: finalText, @@ -366,8 +370,8 @@ const fetchAndProcessAssistantResponseImpl = async ( { response: { source: WebSearchSource.OPENROUTER, results: extractedUrls } }, { status: MessageBlockStatus.SUCCESS } ) - handleBlockTransition(citationBlock, MessageBlockType.CITATION) - saveUpdatedBlockToDB(citationBlock.id, assistantMsgId, topicId, getState) + await handleBlockTransition(citationBlock, MessageBlockType.CITATION) + // saveUpdatedBlockToDB(citationBlock.id, assistantMsgId, topicId, getState) } } } else { @@ -396,7 +400,7 @@ const fetchAndProcessAssistantResponseImpl = async ( thinking_millsec: thinking_millsec } throttledBlockUpdate(lastBlockId, blockChanges) - throttledBlockDbUpdate(lastBlockId, blockChanges) + // throttledBlockDbUpdate(lastBlockId, blockChanges) } else { const newBlock = createThinkingBlock(assistantMsgId, accumulatedThinking, { status: MessageBlockStatus.STREAMING, @@ -467,7 +471,7 @@ const fetchAndProcessAssistantResponseImpl = async ( const citationBlock = createCitationBlock(assistantMsgId, {}, { status: MessageBlockStatus.PROCESSING }) citationBlockId = citationBlock.id handleBlockTransition(citationBlock, MessageBlockType.CITATION) - saveUpdatedBlockToDB(citationBlock.id, assistantMsgId, topicId, getState) + // saveUpdatedBlockToDB(citationBlock.id, assistantMsgId, topicId, getState) }, onExternalToolComplete: (externalToolResult: ExternalToolResult) => { if (citationBlockId) { @@ -486,9 +490,9 @@ const fetchAndProcessAssistantResponseImpl = async ( const citationBlock = createCitationBlock(assistantMsgId, {}, { status: MessageBlockStatus.PROCESSING }) citationBlockId = citationBlock.id handleBlockTransition(citationBlock, MessageBlockType.CITATION) - saveUpdatedBlockToDB(citationBlock.id, assistantMsgId, topicId, getState) + // saveUpdatedBlockToDB(citationBlock.id, assistantMsgId, topicId, getState) }, - onLLMWebSearchComplete(llmWebSearchResult) { + onLLMWebSearchComplete: async (llmWebSearchResult) => { if (citationBlockId) { const changes: Partial = { response: llmWebSearchResult, @@ -544,6 +548,7 @@ const fetchAndProcessAssistantResponseImpl = async ( } }, onError: async (error) => { + cancelThrottledBlockUpdate() console.dir(error, { depth: null }) const isErrorTypeAbort = isAbortError(error) let pauseErrorLanguagePlaceholder = '' @@ -585,6 +590,8 @@ const fetchAndProcessAssistantResponseImpl = async ( }) }, onComplete: async (status: AssistantMessageStatus, response?: Response) => { + cancelThrottledBlockUpdate() + const finalStateOnComplete = getState() const finalAssistantMsg = finalStateOnComplete.messages.entities[assistantMsgId] @@ -623,7 +630,7 @@ const fetchAndProcessAssistantResponseImpl = async ( updates: messageUpdates }) ) - + console.log('onComplete: saveUpdatesToDB', messageUpdates) saveUpdatesToDB(assistantMsgId, topicId, messageUpdates, []) EventEmitter.emit(EVENT_NAMES.MESSAGE_COMPLETE, { id: assistantMsgId, topicId, status }) @@ -871,6 +878,7 @@ export const resendMessageThunk = const blockIdsToDelete = [...(originalMsg.blocks || [])] const resetMsg = resetAssistantMessage(originalMsg, { status: AssistantMessageStatus.PENDING, + updatedAt: new Date().toISOString(), ...(assistantMessagesToReset.length === 1 ? { model: assistant.model } : {}) }) @@ -973,7 +981,8 @@ export const regenerateAssistantResponseThunk = // 5. Reset the message entity in Redux const resetAssistantMsg = resetAssistantMessage(messageToResetEntity, { - status: AssistantMessageStatus.PENDING + status: AssistantMessageStatus.PENDING, + updatedAt: new Date().toISOString() }) dispatch( newMessagesActions.updateMessage({ diff --git a/src/renderer/src/types/newMessage.ts b/src/renderer/src/types/newMessage.ts index 6e9c7450cb..6005d75db8 100644 --- a/src/renderer/src/types/newMessage.ts +++ b/src/renderer/src/types/newMessage.ts @@ -163,7 +163,7 @@ export type Message = { assistantId: string topicId: string createdAt: string - // updatedAt?: string + updatedAt?: string status: UserMessageStatus | AssistantMessageStatus // 消息元数据 diff --git a/src/renderer/src/utils/messageUtils/create.ts b/src/renderer/src/utils/messageUtils/create.ts index 12a19958a2..fdd90c3fde 100644 --- a/src/renderer/src/utils/messageUtils/create.ts +++ b/src/renderer/src/utils/messageUtils/create.ts @@ -384,7 +384,7 @@ export function resetMessage( */ export const resetAssistantMessage = ( originalMessage: Message, - updates?: Partial> // Primarily allow updating status + updates?: Partial> // Primarily allow updating status ): Message => { // Ensure we are only resetting assistant messages if (originalMessage.role !== 'assistant') {