Merge branch 'fix/next-release-bugs' of github.com:CherryHQ/cherry-studio into fix/next-release-bugs

This commit is contained in:
suyao 2025-05-09 21:27:08 +08:00
commit 9ba630b5e8
No known key found for this signature in database
9 changed files with 81 additions and 83 deletions

View File

@ -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<Message> & Pick<Message, 'id'> = {
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<Message> & Pick<Message, 'id'> = {
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<MessageBlock>[]) => {
async (messageId: string, updates: Partial<MessageBlock>) => {
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<Message> & Pick<Message, 'id'> = {
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]
)

View File

@ -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)
},

View File

@ -75,7 +75,6 @@ const MainTextBlock: React.FC<Props> = ({ 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<Props> = ({ 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 [<sup>N</sup>](url)
const plainRefRegex = new RegExp(`\\[${citationNum}\\]`, 'g')
const citationTag = `[<sup data-citation='${citationJson}'>${citationNum}</sup>](${citation.url})`
const supTag = `<sup data-citation='${citationJson}'>${citationNum}</sup>`
const citationTag = isLink ? `[${supTag}](${citation.url})` : supTag
content = content.replace(plainRefRegex, citationTag)
})

View File

@ -102,7 +102,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
{username}
</UserName>
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap>
</AvatarWrapper>
</Container>

View File

@ -164,7 +164,7 @@ const MessageMenubar: FC<Props> = (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, {

View File

@ -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)

View File

@ -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<MessageBlock>) => {
// 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<MessageBlock>) => {
// // 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<Message>, // 需要更新的消息字段
blocksToUpdate: MessageBlock[] // 需要更新/创建的块
) => {
console.log('messageUpdates', messageUpdates)
try {
const messageDataToSave: Partial<Message> & Pick<Message, 'id' | 'topicId'> = {
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<CitationMessageBlock> = {
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({

View File

@ -163,7 +163,7 @@ export type Message = {
assistantId: string
topicId: string
createdAt: string
// updatedAt?: string
updatedAt?: string
status: UserMessageStatus | AssistantMessageStatus
// 消息元数据

View File

@ -384,7 +384,7 @@ export function resetMessage(
*/
export const resetAssistantMessage = (
originalMessage: Message,
updates?: Partial<Pick<Message, 'status'>> // Primarily allow updating status
updates?: Partial<Pick<Message, 'status' | 'updatedAt'>> // Primarily allow updating status
): Message => {
// Ensure we are only resetting assistant messages
if (originalMessage.role !== 'assistant') {