diff --git a/src/renderer/src/assets/images/apps/aistudio.png b/src/renderer/src/assets/images/apps/aistudio.png new file mode 100644 index 0000000000..c7cb2adebe Binary files /dev/null and b/src/renderer/src/assets/images/apps/aistudio.png differ diff --git a/src/renderer/src/assets/images/apps/aistudio.svg b/src/renderer/src/assets/images/apps/aistudio.svg deleted file mode 100644 index 2c08015593..0000000000 --- a/src/renderer/src/assets/images/apps/aistudio.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/renderer/src/config/minapps.ts b/src/renderer/src/config/minapps.ts index 81a4a98723..eeefb218d2 100644 --- a/src/renderer/src/config/minapps.ts +++ b/src/renderer/src/config/minapps.ts @@ -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' diff --git a/src/renderer/src/config/models/vision.ts b/src/renderer/src/config/models/vision.ts index fe4bc9912c..d93c677638 100644 --- a/src/renderer/src/config/models/vision.ts +++ b/src/renderer/src/config/models/vision.ts @@ -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 /** * 判断模型是否支持图片增强(包括编辑、增强、修复等) diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index d76e7c9192..3b5eb80fdf 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -237,6 +237,7 @@ const Chat: FC = (props) => { ) : ( )} + {messageNavigation === 'buttons' && } )} diff --git a/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx b/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx index 611216919a..7f7900b8c5 100644 --- a/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx +++ b/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx @@ -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 = ({ 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(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 = ({ 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 ( - + @@ -79,6 +108,7 @@ const AgentSessionMessages: React.FC = ({ agentId, sessionId }) => { + {messageNavigation === 'anchor' && } ) } diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index f37e829a2a..12e3e04988 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -163,7 +163,7 @@ const Messages: React.FC = ({ 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 { diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index 51061818b3..b210c532ce 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -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 & Pick, @@ -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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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): Promise => { + 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): Promise => { + 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 => { + 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 => { + 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 + } +} diff --git a/src/renderer/src/store/thunk/messageThunk.v2.ts b/src/renderer/src/store/thunk/messageThunk.v2.ts deleted file mode 100644 index 7d39c6a5ff..0000000000 --- a/src/renderer/src/store/thunk/messageThunk.v2.ts +++ /dev/null @@ -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 - * -------------------------------------------------------------------------- - */ - -/** - * 注意:该文件名虽然带着v2,下面注释可能也提到了v2,但该文件和v2 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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): Promise => { - 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): Promise => { - 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 => { - 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 => { - 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 - } -}