Merge branch 'main' of github.com:CherryHQ/cherry-studio into v2

This commit is contained in:
fullex 2026-01-03 16:40:51 +08:00
commit f558b99ca3
9 changed files with 308 additions and 328 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -1,27 +0,0 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="256" height="256" rx="32" fill="#0057CE"/>
<mask id="path-2-inside-1_4113_89308" fill="white">
<path d="M169.6 131.626C173.075 129.641 176.32 128.241 180.1 126.943C183.74 125.695 187.444 124.664 191.186 123.735C194.915 122.806 198.682 122.017 202.449 121.228C206.216 120.439 209.958 119.675 213.598 118.314C231.429 111.619 242.221 93.6357 239.612 74.9396C237.003 56.2435 221.692 41.8237 202.691 40.1564C194.062 39.4055 185.726 41.4164 178.013 44.9418C170.326 48.4545 163.288 53.4435 157.166 59.158C144.795 70.676 135.657 85.4649 130.083 101.208C124.47 117.054 122.37 134.095 123.694 150.806C124.356 159.129 125.883 167.504 128.326 175.509C130.719 183.362 134.181 191.469 138.839 198.342C136.828 185.475 138.559 172.175 143.917 160.262C149.262 148.375 158.121 138.193 169.6 131.626Z"/>
</mask>
<path d="M169.6 131.626C173.075 129.641 176.32 128.241 180.1 126.943C183.74 125.695 187.444 124.664 191.186 123.735C194.915 122.806 198.682 122.017 202.449 121.228C206.216 120.439 209.958 119.675 213.598 118.314C231.429 111.619 242.221 93.6357 239.612 74.9396C237.003 56.2435 221.692 41.8237 202.691 40.1564C194.062 39.4055 185.726 41.4164 178.013 44.9418C170.326 48.4545 163.288 53.4435 157.166 59.158C144.795 70.676 135.657 85.4649 130.083 101.208C124.47 117.054 122.37 134.095 123.694 150.806C124.356 159.129 125.883 167.504 128.326 175.509C130.719 183.362 134.181 191.469 138.839 198.342C136.828 185.475 138.559 172.175 143.917 160.262C149.262 148.375 158.121 138.193 169.6 131.626Z" fill="white" stroke="white" stroke-width="32" mask="url(#path-2-inside-1_4113_89308)"/>
<path d="M162.246 150.4C161.915 153.913 163.073 157.464 165.542 160.06C168.011 162.657 171.499 164.031 174.668 165.253C178.13 166.577 181.12 167.658 184.353 169.529C187.433 171.311 190.157 173.526 192.435 176.262C201.802 187.449 200.937 203.867 190.462 214.049C179.988 224.23 163.379 224.778 152.243 215.321C149.404 212.903 146.884 209.798 144.81 206.756C141.654 186.52 147.775 165.317 162.246 150.4Z" fill="white"/>
<mask id="path-4-outside-2_4113_89308" maskUnits="userSpaceOnUse" x="136" y="138.4" width="71" height="92" fill="black">
<rect fill="white" x="136" y="138.4" width="71" height="92"/>
<path d="M162.246 150.4C165.542 153.666 163.073 157.464 165.542 160.06C168.011 162.657 171.499 164.031 174.668 165.253C178.13 166.577 181.12 167.658 184.353 169.529C187.433 171.311 190.157 173.526 192.435 176.262C201.802 187.449 200.937 203.867 190.462 214.049C179.988 224.23 163.379 224.778 152.243 215.321C149.404 212.903 146.884 209.798 144.81 206.756C141.654 186.52 147.775 165.317 162.246 150.4Z"/>
</mask>
<path d="M162.246 150.4C165.542 153.666 163.073 157.464 165.542 160.06C168.011 162.657 171.499 164.031 174.668 165.253C178.13 166.577 181.12 167.658 184.353 169.529C187.433 171.311 190.157 173.526 192.435 176.262C201.802 187.449 200.937 203.867 190.462 214.049C179.988 224.23 163.379 224.778 152.243 215.321C149.404 212.903 146.884 209.798 144.81 206.756C141.654 186.52 147.775 165.317 162.246 150.4Z" stroke="#0057CE" stroke-width="16" mask="url(#path-4-outside-2_4113_89308)"/>
<mask id="path-5-inside-3_4113_89308" fill="white">
<path d="M50.4113 61.9063C63.3547 61.8935 75.9164 69.008 85.0163 76.9879C94.6761 85.4641 102.16 96.2567 107.085 107.991C112.036 119.789 114.416 132.542 114.327 145.282C114.238 157.665 111.769 171.079 106.296 182.394C105.774 167.821 100.123 153.885 90.3107 143.003C88.5926 141.107 86.7981 139.389 84.6599 137.938C82.5218 136.487 80.2691 135.418 77.8382 134.565C73.1164 132.911 67.7838 132.134 62.8711 131.6C57.8057 131.04 52.7149 130.709 47.6622 129.971C42.4695 129.207 37.8114 128.087 33.1787 125.427C19.688 117.715 13.1463 102.009 17.1808 87.1441C21.2153 72.2661 34.846 61.919 50.4113 61.9063Z"/>
</mask>
<path d="M50.4113 61.9063C63.3547 61.8935 75.9164 69.008 85.0163 76.9879C94.6761 85.4641 102.16 96.2567 107.085 107.991C112.036 119.789 114.416 132.542 114.327 145.282C114.238 157.665 111.769 171.079 106.296 182.394C105.774 167.821 100.123 153.885 90.3107 143.003C88.5926 141.107 86.7981 139.389 84.6599 137.938C82.5218 136.487 80.2691 135.418 77.8382 134.565C73.1164 132.911 67.7838 132.134 62.8711 131.6C57.8057 131.04 52.7149 130.709 47.6622 129.971C42.4695 129.207 37.8114 128.087 33.1787 125.427C19.688 117.715 13.1463 102.009 17.1808 87.1441C21.2153 72.2661 34.846 61.919 50.4113 61.9063Z" fill="white" stroke="white" stroke-width="32" mask="url(#path-5-inside-3_4113_89308)"/>
<mask id="path-6-inside-4_4113_89308" fill="white">
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z"/>
</mask>
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z" stroke="white" stroke-width="24" mask="url(#path-6-inside-4_4113_89308)"/>
<mask id="path-7-outside-5_4113_89308" maskUnits="userSpaceOnUse" x="45.3994" y="138.6" width="62" height="79" fill="black">
<rect fill="white" x="45.3994" y="138.6" width="62" height="79"/>
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z"/>
</mask>
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z" fill="white"/>
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z" stroke="#0057CE" stroke-width="16" mask="url(#path-7-outside-5_4113_89308)"/>
</svg>

Before

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -1,7 +1,7 @@
import { loggerService } from '@logger'
import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url'
import AbacusLogo from '@renderer/assets/images/apps/abacus.webp?url'
import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url'
import AIStudioLogo from '@renderer/assets/images/apps/aistudio.png?url'
import ApplicationLogo from '@renderer/assets/images/apps/application.png?url'
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
import BaiduAiSearchLogo from '@renderer/assets/images/apps/baidu-ai-search.webp?url'

View File

@ -75,12 +75,37 @@ const VISION_REGEX = new RegExp(
'i'
)
// For middleware to identify models that must use the dedicated Image API
// All dedicated image generation models (only generate images, no text chat capability)
// These models need:
// 1. Route to dedicated image generation API
// 2. Exclude from reasoning/websearch/tooluse selection
const DEDICATED_IMAGE_MODELS = [
'grok-2-image(?:-[\\w-]+)?',
// OpenAI series
'dall-e(?:-[\\w-]+)?',
'gpt-image-1(?:-[\\w-]+)?',
'imagen(?:-[\\w-]+)?'
'gpt-image(?:-[\\w-]+)?',
// xAI
'grok-2-image(?:-[\\w-]+)?',
// Google
'imagen(?:-[\\w-]+)?',
// Stable Diffusion series
'flux(?:-[\\w-]+)?',
'stable-?diffusion(?:-[\\w-]+)?',
'stabilityai(?:-[\\w-]+)?',
'sd-[\\w-]+',
'sdxl(?:-[\\w-]+)?',
// zhipu
'cogview(?:-[\\w-]+)?',
// Alibaba
'qwen-image(?:-[\\w-]+)?',
// Others
'janus(?:-[\\w-]+)?',
'midjourney(?:-[\\w-]+)?',
'mj-[\\w-]+',
'z-image(?:-[\\w-]+)?',
'longcat-image(?:-[\\w-]+)?',
'hunyuanimage(?:-[\\w-]+)?',
'seedream(?:-[\\w-]+)?',
'kandinsky(?:-[\\w-]+)?'
]
const IMAGE_ENHANCEMENT_MODELS = [
@ -133,13 +158,23 @@ const GENERATE_IMAGE_MODELS_REGEX = new RegExp(GENERATE_IMAGE_MODELS.join('|'),
const MODERN_GENERATE_IMAGE_MODELS_REGEX = new RegExp(MODERN_IMAGE_MODELS.join('|'), 'i')
export const isDedicatedImageGenerationModel = (model: Model): boolean => {
/**
* Check if the model is a dedicated image generation model
* Dedicated image generation models can only generate images, no text chat capability
*
* These models need:
* 1. Route to dedicated image generation API
* 2. Exclude from reasoning/websearch/tooluse selection
*/
export function isDedicatedImageModel(model: Model): boolean {
if (!model) return false
const modelId = getLowerBaseModelName(model.id)
return DEDICATED_IMAGE_MODELS_REGEX.test(modelId)
}
// Backward compatible aliases
export const isDedicatedImageGenerationModel = isDedicatedImageModel
export const isAutoEnableImageGenerationModel = (model: Model): boolean => {
if (!model) return false
@ -195,14 +230,8 @@ export function isPureGenerateImageModel(model: Model): boolean {
return !OPENAI_TOOL_USE_IMAGE_GENERATION_MODELS.some((m) => modelId.includes(m))
}
// TODO: refine the regex
// Text to image models
const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus|midjourney|mj-|imagen|gpt-image/i
export function isTextToImageModel(model: Model): boolean {
const modelId = getLowerBaseModelName(model.id)
return TEXT_TO_IMAGE_REGEX.test(modelId)
}
// Backward compatible alias - now uses unified dedicated image model detection
export const isTextToImageModel = isDedicatedImageModel
/**
*

View File

@ -237,6 +237,7 @@ const Chat: FC<Props> = (props) => {
) : (
<AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} />
)}
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
<AgentSessionInputbar agentId={activeAgentId} sessionId={activeSessionId} />
</>
)}

View File

@ -2,13 +2,17 @@ import { loggerService } from '@logger'
import ContextMenu from '@renderer/components/ContextMenu'
import { useSession } from '@renderer/hooks/agents/useSession'
import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getGroupedMessages } from '@renderer/services/MessagesService'
import { type Topic, TopicType } from '@renderer/types'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { Spin } from 'antd'
import { memo, useMemo } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
import styled from 'styled-components'
import MessageAnchorLine from './MessageAnchorLine'
import MessageGroup from './MessageGroup'
import NarrowLayout from './NarrowLayout'
import PermissionModeDisplay from './PermissionModeDisplay'
@ -26,6 +30,10 @@ const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
const sessionTopicId = useMemo(() => buildAgentSessionTopicId(sessionId), [sessionId])
// Use the same hook as Messages.tsx for consistent behavior
const messages = useTopicMessages(sessionTopicId)
const { messageNavigation } = useSettings()
const scrollContainerRef = useRef<HTMLDivElement>(null)
const { handleScroll: handleScrollPosition } = useScrollPosition(`agent-session-${sessionId}`)
const displayMessages = useMemo(() => {
if (!messages || messages.length === 0) return []
@ -60,8 +68,29 @@ const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
messageCount: messages.length
})
// Scroll to bottom function
const scrollToBottom = useCallback(() => {
if (scrollContainerRef.current) {
requestAnimationFrame(() => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({ top: 0 })
}
})
}
}, [scrollContainerRef])
// Listen for send message events to auto-scroll to bottom
useEffect(() => {
const unsubscribes = [EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, scrollToBottom)]
return () => unsubscribes.forEach((unsub) => unsub())
}, [scrollToBottom])
return (
<MessagesContainer id="messages" className="messages-container">
<MessagesContainer
id="messages"
className="messages-container"
ref={scrollContainerRef}
onScroll={handleScrollPosition}>
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
<ContextMenu>
<ScrollContainer>
@ -79,6 +108,7 @@ const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
</ScrollContainer>
</ContextMenu>
</NarrowLayout>
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
</MessagesContainer>
)
}

View File

@ -163,7 +163,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
const { message: clearMessage } = getUserMessage({ assistant, topic, type: 'clear' })
dispatch(newMessagesActions.addMessage({ topicId: topic.id, message: clearMessage }))
await saveMessageAndBlocksToDB(clearMessage, [])
await saveMessageAndBlocksToDB(topic.id, clearMessage, [])
scrollToBottom()
} finally {

View File

@ -20,6 +20,7 @@ import { AiSdkToChunkAdapter } from '@renderer/aiCore/chunk/AiSdkToChunkAdapter'
import { AgentApiClient } from '@renderer/api/agent'
import db from '@renderer/databases'
import { fetchMessagesSummary, transformMessagesAndFetch } from '@renderer/services/ApiService'
import { dbService } from '@renderer/services/db'
import { DbService } from '@renderer/services/db/DbService'
import FileManager from '@renderer/services/FileManager'
import { BlockManager } from '@renderer/services/messageStreaming/BlockManager'
@ -58,18 +59,18 @@ import { mutate } from 'swr'
import type { AppDispatch, RootState } from '../index'
import { removeManyBlocks, updateOneBlock, upsertManyBlocks, upsertOneBlock } from '../messageBlock'
import { newMessagesActions, selectMessagesForTopic } from '../newMessage'
import {
bulkAddBlocksV2,
clearMessagesFromDBV2,
deleteMessageFromDBV2,
deleteMessagesFromDBV2,
loadTopicMessagesThunkV2,
saveMessageAndBlocksToDBV2,
updateBlocksV2,
updateFileCountV2,
updateMessageV2,
updateSingleBlockV2
} from './messageThunk.v2'
// import {
// bulkAddBlocksV2,
// clearMessagesFromDBV2,
// deleteMessageFromDBV2,
// deleteMessagesFromDBV2,
// loadTopicMessagesThunkV2,
// saveMessageAndBlocksToDBV2,
// updateBlocksV2,
// updateFileCountV2,
// updateMessageV2,
// updateSingleBlockV2
// } from './messageThunk.v2'
const logger = loggerService.withContext('MessageThunk')
@ -364,9 +365,9 @@ const createAgentMessageStream = async (
return createSSEReadableStream(response.body, signal)
}
// TODO: 后续可以将db操作移到Listener Middleware中
export const saveMessageAndBlocksToDB = async (message: Message, blocks: MessageBlock[], messageIndex: number = -1) => {
return saveMessageAndBlocksToDBV2(message.topicId, message, blocks, messageIndex)
}
// export const saveMessageAndBlocksToDB = async (message: Message, blocks: MessageBlock[], messageIndex: number = -1) => {
// return saveMessageAndBlocksToDBV2(message.topicId, message, blocks, messageIndex)
// }
const updateExistingMessageAndBlocksInDB = async (
updatedMessage: Partial<Message> & Pick<Message, 'id' | 'topicId'>,
@ -375,7 +376,7 @@ const updateExistingMessageAndBlocksInDB = async (
try {
// Always update blocks if provided
if (updatedBlocks.length > 0) {
await updateBlocksV2(updatedBlocks)
await updateBlocks(updatedBlocks)
}
// Check if there are message properties to update beyond id and topicId
@ -387,7 +388,7 @@ const updateExistingMessageAndBlocksInDB = async (
return acc
}, {})
await updateMessageV2(updatedMessage.topicId, updatedMessage.id, messageUpdatesPayload)
await updateMessage(updatedMessage.topicId, updatedMessage.id, messageUpdatesPayload)
store.dispatch(updateTopicUpdatedAt({ topicId: updatedMessage.topicId }))
}
@ -433,7 +434,7 @@ const getBlockThrottler = (id: string) => {
})
blockUpdateRafs.set(id, rafId)
await updateSingleBlockV2(id, blockUpdate)
await updateSingleBlock(id, blockUpdate)
}, 150)
blockUpdateThrottlers.set(id, throttler)
@ -894,7 +895,7 @@ export const sendMessage =
userMessage.agentSessionId = activeAgentSession.agentSessionId
}
await saveMessageAndBlocksToDB(userMessage, userMessageBlocks)
await saveMessageAndBlocksToDB(topicId, userMessage, userMessageBlocks)
dispatch(newMessagesActions.addMessage({ topicId, message: userMessage }))
if (userMessageBlocks.length > 0) {
dispatch(upsertManyBlocks(userMessageBlocks))
@ -912,7 +913,7 @@ export const sendMessage =
if (activeAgentSession.agentSessionId && !assistantMessage.agentSessionId) {
assistantMessage.agentSessionId = activeAgentSession.agentSessionId
}
await saveMessageAndBlocksToDB(assistantMessage, [])
await saveMessageAndBlocksToDB(topicId, assistantMessage, [])
dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
queue.add(async () => {
@ -935,7 +936,7 @@ export const sendMessage =
model: assistant.model,
traceId: userMessage.traceId
})
await saveMessageAndBlocksToDB(assistantMessage, [])
await saveMessageAndBlocksToDB(topicId, assistantMessage, [])
dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
queue.add(async () => {
@ -1001,11 +1002,11 @@ export const loadAgentSessionMessagesThunk =
* Loads messages and their blocks for a specific topic from the database
* and updates the Redux store.
*/
export const loadTopicMessagesThunk =
(topicId: string, forceReload: boolean = false) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
return loadTopicMessagesThunkV2(topicId, forceReload)(dispatch, getState)
}
// export const loadTopicMessagesThunk =
// (topicId: string, forceReload: boolean = false) =>
// async (dispatch: AppDispatch, getState: () => RootState) => {
// return loadTopicMessagesThunkV2(topicId, forceReload)(dispatch, getState)
// }
/**
* Thunk to delete a single message and its associated blocks.
@ -1024,7 +1025,7 @@ export const deleteSingleMessageThunk =
try {
dispatch(newMessagesActions.removeMessage({ topicId, messageId }))
cleanupMultipleBlocks(dispatch, blockIdsToDelete)
await deleteMessageFromDBV2(topicId, messageId)
await deleteMessageFromDB(topicId, messageId)
} catch (error) {
logger.error(`[deleteSingleMessage] Failed to delete message ${messageId}:`, error as Error)
}
@ -1063,7 +1064,7 @@ export const deleteMessageGroupThunk =
try {
dispatch(newMessagesActions.removeMessagesByAskId({ topicId, askId }))
cleanupMultipleBlocks(dispatch, blockIdsToDelete)
await deleteMessagesFromDBV2(topicId, messageIdsToDelete)
await deleteMessagesFromDB(topicId, messageIdsToDelete)
} catch (error) {
logger.error(`[deleteMessageGroup] Failed to delete messages with askId ${askId}:`, error as Error)
}
@ -1088,7 +1089,7 @@ export const clearTopicMessagesThunk =
dispatch(newMessagesActions.clearTopicMessages(topicId))
cleanupMultipleBlocks(dispatch, blockIdsToDelete)
await clearMessagesFromDBV2(topicId)
await clearMessagesFromDB(topicId)
} catch (error) {
logger.error(`[clearTopicMessagesThunk] Failed to clear messages for topic ${topicId}:`, error as Error)
}
@ -1409,7 +1410,7 @@ export const updateTranslationBlockThunk =
// 更新Redux状态
dispatch(updateOneBlock({ id: blockId, changes }))
await updateSingleBlockV2(blockId, changes)
await updateSingleBlock(blockId, changes)
// Logger.log(`[updateTranslationBlockThunk] Successfully updated translation block ${blockId}.`)
} catch (error) {
logger.error(`[updateTranslationBlockThunk] Failed to update translation block ${blockId}:`, error as Error)
@ -1480,7 +1481,7 @@ export const appendAssistantResponseThunk =
const insertAtIndex = existingMessageIndex !== -1 ? existingMessageIndex + 1 : currentTopicMessageIds.length
// 4. Update Database (Save the stub to the topic's message list)
await saveMessageAndBlocksToDB(newAssistantMessageStub, [], insertAtIndex)
await saveMessageAndBlocksToDB(topicId, newAssistantMessageStub, [], insertAtIndex)
dispatch(
newMessagesActions.insertMessageAtIndex({ topicId, message: newAssistantMessageStub, index: insertAtIndex })
@ -1632,12 +1633,12 @@ export const cloneMessagesToNewTopicThunk =
// Add the NEW blocks
if (clonedBlocks.length > 0) {
await bulkAddBlocksV2(clonedBlocks)
await bulkAddBlocks(clonedBlocks)
}
// Update file counts
const uniqueFiles = [...new Map(filesToUpdateCount.map((f) => [f.id, f])).values()]
for (const file of uniqueFiles) {
await updateFileCountV2(file.id, 1, false)
await updateFileCount(file.id, 1, false)
}
})
@ -1691,11 +1692,11 @@ export const updateMessageAndBlocksThunk =
}
// Update message properties if provided
if (messageUpdates && Object.keys(messageUpdates).length > 0 && messageId) {
await updateMessageV2(topicId, messageId, messageUpdates)
await updateMessage(topicId, messageId, messageUpdates)
}
// Update blocks if provided
if (blockUpdatesList.length > 0) {
await updateBlocksV2(blockUpdatesList)
await updateBlocks(blockUpdatesList)
}
dispatch(updateTopicUpdatedAt({ topicId }))
@ -1749,3 +1750,197 @@ export const removeBlocksThunk =
throw error
}
}
//以下内容从原 messageThunk.v2.ts 迁移过来,原文件已经删除
//原因v2.ts并不是v2数据重构的一部分而相关命名对v2重构造成重大误解故两文件合并以消除误解
/**
* Load messages for a topic using unified DbService
*/
export const loadTopicMessagesThunk =
(topicId: string, forceReload: boolean = false) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState()
dispatch(newMessagesActions.setCurrentTopicId(topicId))
// Skip if already cached and not forcing reload
if (!forceReload && state.messages.messageIdsByTopic[topicId]) {
return
}
try {
dispatch(newMessagesActions.setTopicLoading({ topicId, loading: true }))
// Unified call - no need to check isAgentSessionTopicId
const { messages, blocks } = await dbService.fetchMessages(topicId)
logger.silly('Loaded messages via DbService', {
topicId,
messageCount: messages.length,
blockCount: blocks.length
})
// Update Redux state with fetched data
if (blocks.length > 0) {
dispatch(upsertManyBlocks(blocks))
}
dispatch(newMessagesActions.messagesReceived({ topicId, messages }))
} catch (error) {
logger.error(`Failed to load messages for topic ${topicId}:`, error as Error)
// Could dispatch an error action here if needed
} finally {
dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false }))
}
}
/**
* Get raw topic data using unified DbService
* Returns topic with messages array
*/
export const getRawTopic = async (topicId: string): Promise<{ id: string; messages: Message[] } | undefined> => {
try {
const rawTopic = await dbService.getRawTopic(topicId)
logger.silly('Retrieved raw topic via DbService', { topicId, found: !!rawTopic })
return rawTopic
} catch (error) {
logger.error('Failed to get raw topic:', { topicId, error })
return undefined
}
}
/**
* Update file reference count
* Only applies to Dexie data source, no-op for agent sessions
*/
export const updateFileCount = async (fileId: string, delta: number, deleteIfZero: boolean = false): Promise<void> => {
try {
// Pass all parameters to dbService, including deleteIfZero
await dbService.updateFileCount(fileId, delta, deleteIfZero)
logger.silly('Updated file count', { fileId, delta, deleteIfZero })
} catch (error) {
logger.error('Failed to update file count:', { fileId, delta, error })
throw error
}
}
/**
* Delete a single message from database
*/
export const deleteMessageFromDB = async (topicId: string, messageId: string): Promise<void> => {
try {
await dbService.deleteMessage(topicId, messageId)
logger.silly('Deleted message via DbService', { topicId, messageId })
} catch (error) {
logger.error('Failed to delete message:', { topicId, messageId, error })
throw error
}
}
/**
* Delete multiple messages from database
*/
export const deleteMessagesFromDB = async (topicId: string, messageIds: string[]): Promise<void> => {
try {
await dbService.deleteMessages(topicId, messageIds)
logger.silly('Deleted messages via DbService', { topicId, count: messageIds.length })
} catch (error) {
logger.error('Failed to delete messages:', { topicId, messageIds, error })
throw error
}
}
/**
* Clear all messages from a topic
*/
export const clearMessagesFromDB = async (topicId: string): Promise<void> => {
try {
await dbService.clearMessages(topicId)
logger.silly('Cleared all messages via DbService', { topicId })
} catch (error) {
logger.error('Failed to clear messages:', { topicId, error })
throw error
}
}
/**
* Save a message and its blocks to database
* Uses unified interface, no need for isAgentSessionTopicId check
*/
export const saveMessageAndBlocksToDB = async (
topicId: string,
message: Message,
blocks: MessageBlock[],
messageIndex: number = -1
): Promise<void> => {
try {
const blockIds = blocks.map((block) => block.id)
const shouldSyncBlocks =
blockIds.length > 0 && (!message.blocks || blockIds.some((id, index) => message.blocks?.[index] !== id))
const messageWithBlocks = shouldSyncBlocks ? { ...message, blocks: blockIds } : message
// Direct call without conditional logic, now with messageIndex
await dbService.appendMessage(topicId, messageWithBlocks, blocks, messageIndex)
logger.silly('Saved message and blocks via DbService', {
topicId,
messageId: message.id,
blockCount: blocks.length,
messageIndex
})
} catch (error) {
logger.error('Failed to save message and blocks:', { topicId, messageId: message.id, error })
throw error
}
}
/**
* Update a message in the database
*/
export const updateMessage = async (topicId: string, messageId: string, updates: Partial<Message>): Promise<void> => {
try {
await dbService.updateMessage(topicId, messageId, updates)
logger.silly('Updated message via DbService', { topicId, messageId })
} catch (error) {
logger.error('Failed to update message:', { topicId, messageId, error })
throw error
}
}
/**
* Update a single message block
*/
export const updateSingleBlock = async (blockId: string, updates: Partial<MessageBlock>): Promise<void> => {
try {
await dbService.updateSingleBlock(blockId, updates)
logger.silly('Updated single block via DbService', { blockId })
} catch (error) {
logger.error('Failed to update single block:', { blockId, error })
throw error
}
}
/**
* Bulk add message blocks (for new blocks)
*/
export const bulkAddBlocks = async (blocks: MessageBlock[]): Promise<void> => {
try {
await dbService.bulkAddBlocks(blocks)
logger.silly('Bulk added blocks via DbService', { count: blocks.length })
} catch (error) {
logger.error('Failed to bulk add blocks:', { count: blocks.length, error })
throw error
}
}
/**
* Update multiple message blocks (upsert operation)
*/
export const updateBlocks = async (blocks: MessageBlock[]): Promise<void> => {
try {
await dbService.updateBlocks(blocks)
logger.silly('Updated blocks via DbService', { count: blocks.length })
} catch (error) {
logger.error('Failed to update blocks:', { count: blocks.length, error })
throw error
}
}

View File

@ -1,248 +0,0 @@
/**
* @deprecated Scheduled for removal in v2.0.0
* --------------------------------------------------------------------------
* NOTICE: V2 DATA&UI REFACTORING (by 0xfullex)
* --------------------------------------------------------------------------
* STOP: Feature PRs affecting this file are currently BLOCKED.
* Only critical bug fixes are accepted during this migration phase.
*
* This file is being refactored to v2 standards.
* Any non-critical changes will conflict with the ongoing work.
*
* 🔗 Context & Status:
* - Contribution Hold: https://github.com/CherryHQ/cherry-studio/issues/10954
* - v2 Refactor PR : https://github.com/CherryHQ/cherry-studio/pull/10162
* --------------------------------------------------------------------------
*/
/**
* v2v2v2 Data Refactor没有任何关系v1的旧代码AI都要产生误判
*/
import { loggerService } from '@logger'
import { dbService } from '@renderer/services/db'
import type { Message, MessageBlock } from '@renderer/types/newMessage'
import type { AppDispatch, RootState } from '../index'
import { upsertManyBlocks } from '../messageBlock'
import { newMessagesActions } from '../newMessage'
const logger = loggerService.withContext('MessageThunkV2')
// =================================================================
// Phase 2.1 - Batch 1: Read-only operations (lowest risk)
// =================================================================
/**
* Load messages for a topic using unified DbService
*/
export const loadTopicMessagesThunkV2 =
(topicId: string, forceReload: boolean = false) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState()
dispatch(newMessagesActions.setCurrentTopicId(topicId))
// Skip if already cached and not forcing reload
if (!forceReload && state.messages.messageIdsByTopic[topicId]) {
return
}
try {
dispatch(newMessagesActions.setTopicLoading({ topicId, loading: true }))
// Unified call - no need to check isAgentSessionTopicId
const { messages, blocks } = await dbService.fetchMessages(topicId)
logger.silly('Loaded messages via DbService', {
topicId,
messageCount: messages.length,
blockCount: blocks.length
})
// Update Redux state with fetched data
if (blocks.length > 0) {
dispatch(upsertManyBlocks(blocks))
}
dispatch(newMessagesActions.messagesReceived({ topicId, messages }))
} catch (error) {
logger.error(`Failed to load messages for topic ${topicId}:`, error as Error)
// Could dispatch an error action here if needed
} finally {
dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false }))
}
}
/**
* Get raw topic data using unified DbService
* Returns topic with messages array
*/
export const getRawTopicV2 = async (topicId: string): Promise<{ id: string; messages: Message[] } | undefined> => {
try {
const rawTopic = await dbService.getRawTopic(topicId)
logger.silly('Retrieved raw topic via DbService', { topicId, found: !!rawTopic })
return rawTopic
} catch (error) {
logger.error('Failed to get raw topic:', { topicId, error })
return undefined
}
}
// =================================================================
// Phase 2.2 - Batch 2: Helper functions
// =================================================================
/**
* Update file reference count
* Only applies to Dexie data source, no-op for agent sessions
*/
export const updateFileCountV2 = async (
fileId: string,
delta: number,
deleteIfZero: boolean = false
): Promise<void> => {
try {
// Pass all parameters to dbService, including deleteIfZero
await dbService.updateFileCount(fileId, delta, deleteIfZero)
logger.silly('Updated file count', { fileId, delta, deleteIfZero })
} catch (error) {
logger.error('Failed to update file count:', { fileId, delta, error })
throw error
}
}
// =================================================================
// Phase 2.3 - Batch 3: Delete operations
// =================================================================
/**
* Delete a single message from database
*/
export const deleteMessageFromDBV2 = async (topicId: string, messageId: string): Promise<void> => {
try {
await dbService.deleteMessage(topicId, messageId)
logger.silly('Deleted message via DbService', { topicId, messageId })
} catch (error) {
logger.error('Failed to delete message:', { topicId, messageId, error })
throw error
}
}
/**
* Delete multiple messages from database
*/
export const deleteMessagesFromDBV2 = async (topicId: string, messageIds: string[]): Promise<void> => {
try {
await dbService.deleteMessages(topicId, messageIds)
logger.silly('Deleted messages via DbService', { topicId, count: messageIds.length })
} catch (error) {
logger.error('Failed to delete messages:', { topicId, messageIds, error })
throw error
}
}
/**
* Clear all messages from a topic
*/
export const clearMessagesFromDBV2 = async (topicId: string): Promise<void> => {
try {
await dbService.clearMessages(topicId)
logger.silly('Cleared all messages via DbService', { topicId })
} catch (error) {
logger.error('Failed to clear messages:', { topicId, error })
throw error
}
}
// =================================================================
// Phase 2.4 - Batch 4: Complex write operations
// =================================================================
/**
* Save a message and its blocks to database
* Uses unified interface, no need for isAgentSessionTopicId check
*/
export const saveMessageAndBlocksToDBV2 = async (
topicId: string,
message: Message,
blocks: MessageBlock[],
messageIndex: number = -1
): Promise<void> => {
try {
const blockIds = blocks.map((block) => block.id)
const shouldSyncBlocks =
blockIds.length > 0 && (!message.blocks || blockIds.some((id, index) => message.blocks?.[index] !== id))
const messageWithBlocks = shouldSyncBlocks ? { ...message, blocks: blockIds } : message
// Direct call without conditional logic, now with messageIndex
await dbService.appendMessage(topicId, messageWithBlocks, blocks, messageIndex)
logger.silly('Saved message and blocks via DbService', {
topicId,
messageId: message.id,
blockCount: blocks.length,
messageIndex
})
} catch (error) {
logger.error('Failed to save message and blocks:', { topicId, messageId: message.id, error })
throw error
}
}
// Note: sendMessageV2 would be implemented here but it's more complex
// and would require more of the supporting code from messageThunk.ts
// =================================================================
// Phase 2.5 - Batch 5: Update operations
// =================================================================
/**
* Update a message in the database
*/
export const updateMessageV2 = async (topicId: string, messageId: string, updates: Partial<Message>): Promise<void> => {
try {
await dbService.updateMessage(topicId, messageId, updates)
logger.silly('Updated message via DbService', { topicId, messageId })
} catch (error) {
logger.error('Failed to update message:', { topicId, messageId, error })
throw error
}
}
/**
* Update a single message block
*/
export const updateSingleBlockV2 = async (blockId: string, updates: Partial<MessageBlock>): Promise<void> => {
try {
await dbService.updateSingleBlock(blockId, updates)
logger.silly('Updated single block via DbService', { blockId })
} catch (error) {
logger.error('Failed to update single block:', { blockId, error })
throw error
}
}
/**
* Bulk add message blocks (for new blocks)
*/
export const bulkAddBlocksV2 = async (blocks: MessageBlock[]): Promise<void> => {
try {
await dbService.bulkAddBlocks(blocks)
logger.silly('Bulk added blocks via DbService', { count: blocks.length })
} catch (error) {
logger.error('Failed to bulk add blocks:', { count: blocks.length, error })
throw error
}
}
/**
* Update multiple message blocks (upsert operation)
*/
export const updateBlocksV2 = async (blocks: MessageBlock[]): Promise<void> => {
try {
await dbService.updateBlocks(blocks)
logger.silly('Updated blocks via DbService', { count: blocks.length })
} catch (error) {
logger.error('Failed to update blocks:', { count: blocks.length, error })
throw error
}
}