- Updated links in CONTRIBUTING.md and README.md to point to the correct Chinese documentation paths. - Removed outdated files including the English and Chinese versions of the branching strategy, contributing guide, and test plan documents. - Cleaned up references to non-existent documentation in the project structure to streamline the contributor experience.
20 KiB
消息系统
本文档介绍 Cherry Studio 的消息系统架构,包括消息生命周期、状态管理和操作接口。
消息的生命周期
messageBlock.ts 使用指南
该文件定义了用于管理应用程序中所有 MessageBlock 实体的 Redux Slice。它使用 Redux Toolkit 的 createSlice 和 createEntityAdapter 来高效地处理规范化的状态,并提供了一系列 actions 和 selectors 用于与消息块数据交互。
核心目标
- 状态管理: 集中管理所有
MessageBlock的状态。MessageBlock代表消息中的不同内容单元(如文本、代码、图片、引用等)。 - 规范化: 使用
createEntityAdapter将MessageBlock数据存储在规范化的结构中({ ids: [], entities: {} }),这有助于提高性能和简化更新逻辑。 - 可预测性: 提供明确的 actions 来修改状态,并通过 selectors 安全地访问状态。
关键概念
- Slice (
createSlice): Redux Toolkit 的核心 API,用于创建包含 reducer 逻辑、action creators 和初始状态的 Redux 模块。 - Entity Adapter (
createEntityAdapter): Redux Toolkit 提供的工具,用于简化对规范化数据的 CRUD(创建、读取、更新、删除)操作。它会自动生成 reducer 函数和 selectors。 - Selectors: 用于从 Redux store 中派生和计算数据的函数。Selectors 可以被记忆化(memoized),以提高性能。
State 结构
messageBlocks slice 的状态结构由 createEntityAdapter 定义,大致如下:
{
ids: string[]; // 存储所有 MessageBlock ID 的有序列表
entities: { [id: string]: MessageBlock }; // 按 ID 存储 MessageBlock 对象的字典
loadingState: 'idle' | 'loading' | 'succeeded' | 'failed'; // (可选) 其他状态,如加载状态
error: string | null; // (可选) 错误信息
}
Actions
该 slice 导出以下 actions (由 createSlice 和 createEntityAdapter 自动生成或自定义):
-
upsertOneBlock(payload: MessageBlock):- 添加一个新的
MessageBlock或更新一个已存在的MessageBlock。如果 payload 中的id已存在,则执行更新;否则执行插入。
- 添加一个新的
-
upsertManyBlocks(payload: MessageBlock[]):- 添加或更新多个
MessageBlock。常用于批量加载数据(例如,加载一个 Topic 的所有消息块)。
- 添加或更新多个
-
removeOneBlock(payload: string):- 根据提供的
id(payload) 移除单个MessageBlock。
- 根据提供的
-
removeManyBlocks(payload: string[]):- 根据提供的
id数组 (payload) 移除多个MessageBlock。常用于删除消息或清空 Topic 时清理相关的块。
- 根据提供的
-
removeAllBlocks():- 移除 state 中的所有
MessageBlock实体。
- 移除 state 中的所有
-
updateOneBlock(payload: { id: string; changes: Partial<MessageBlock> }):- 更新一个已存在的
MessageBlock。payload需要包含块的id和一个包含要更改的字段的changes对象。
- 更新一个已存在的
-
setMessageBlocksLoading(payload: 'idle' | 'loading'):- (自定义) 设置
loadingState属性。
- (自定义) 设置
-
setMessageBlocksError(payload: string):- (自定义) 设置
loadingState为'failed'并记录错误信息。
- (自定义) 设置
使用示例 (在 Thunk 或其他 Dispatch 的地方):
import { upsertOneBlock, removeManyBlocks, updateOneBlock } from './messageBlock'
import store from './store' // 假设这是你的 Redux store 实例
// 添加或更新一个块
const newBlock: MessageBlock = {
/* ... block data ... */
}
store.dispatch(upsertOneBlock(newBlock))
// 更新一个块的内容
store.dispatch(updateOneBlock({ id: blockId, changes: { content: 'New content' } }))
// 删除多个块
const blockIdsToRemove = ['id1', 'id2']
store.dispatch(removeManyBlocks(blockIdsToRemove))
Selectors
该 slice 导出由 createEntityAdapter 生成的基础 selectors,并通过 messageBlocksSelectors 对象访问:
messageBlocksSelectors.selectIds(state: RootState): string[]: 返回包含所有块 ID 的数组。messageBlocksSelectors.selectEntities(state: RootState): { [id: string]: MessageBlock }: 返回块 ID 到块对象的映射字典。messageBlocksSelectors.selectAll(state: RootState): MessageBlock[]: 返回包含所有块对象的数组。messageBlocksSelectors.selectTotal(state: RootState): number: 返回块的总数。messageBlocksSelectors.selectById(state: RootState, id: string): MessageBlock | undefined: 根据 ID 返回单个块对象,如果找不到则返回undefined。
此外,还提供了一个自定义的、记忆化的 selector:
selectFormattedCitationsByBlockId(state: RootState, blockId: string | undefined): Citation[]:- 接收一个
blockId。 - 如果该 ID 对应的块是
CITATION类型,则提取并格式化其包含的引用信息(来自网页搜索、知识库等),进行去重和重新编号,最后返回一个Citation[]数组,用于在 UI 中显示。 - 如果块不存在或类型不匹配,返回空数组
[]。 - 这个 selector 封装了处理不同引用来源(Gemini, OpenAI, OpenRouter, Zhipu 等)的复杂逻辑。
- 接收一个
使用示例 (在 React 组件或 useSelector 中):
import { useSelector } from 'react-redux'
import { messageBlocksSelectors, selectFormattedCitationsByBlockId } from './messageBlock'
import type { RootState } from './store'
// 获取所有块
const allBlocks = useSelector(messageBlocksSelectors.selectAll)
// 获取特定 ID 的块
const specificBlock = useSelector((state: RootState) => messageBlocksSelectors.selectById(state, someBlockId))
// 获取特定引用块格式化后的引用列表
const formattedCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, citationBlockId))
// 在组件中使用引用数据
// {formattedCitations.map(citation => ...)}
集成
messageBlock.ts slice 通常与 messageThunk.ts 中的 Thunks 紧密协作。Thunks 负责处理异步逻辑(如 API 调用、数据库操作),并在需要时 dispatch messageBlock slice 的 actions 来更新状态。例如,当 messageThunk 接收到流式响应时,它会 dispatch upsertOneBlock 或 updateOneBlock 来实时更新对应的 MessageBlock。同样,删除消息的 Thunk 会 dispatch removeManyBlocks。
理解 messageBlock.ts 的职责是管理状态本身,而 messageThunk.ts 负责触发状态变更的异步流程,这对于维护清晰的应用架构至关重要。
messageThunk.ts 使用指南
该文件包含用于管理应用程序中消息流、处理助手交互以及同步 Redux 状态与 IndexedDB 数据库的核心 Thunk Action Creators。主要围绕 Message 和 MessageBlock 对象进行操作。
核心功能
- 发送/接收消息: 处理用户消息的发送,触发助手响应,并流式处理返回的数据,将其解析为不同的
MessageBlock。 - 状态管理: 确保 Redux store 中的消息和消息块状态与 IndexedDB 中的持久化数据保持一致。
- 消息操作: 提供删除、重发、重新生成、编辑后重发、追加响应、克隆等消息生命周期管理功能。
- Block 处理: 动态创建、更新和保存各种类型的
MessageBlock(文本、思考过程、工具调用、引用、图片、错误、翻译等)。
主要 Thunks
以下是一些关键的 Thunk 函数及其用途:
-
sendMessage(userMessage, userMessageBlocks, assistant, topicId)- 用途: 发送一条新的用户消息。
- 流程:
- 保存用户消息 (
userMessage) 及其块 (userMessageBlocks) 到 Redux 和 DB。 - 检查
@mentions以确定是单模型响应还是多模型响应。 - 创建助手消息(们)的存根 (Stub)。
- 将存根添加到 Redux 和 DB。
- 将核心处理逻辑
fetchAndProcessAssistantResponseImpl添加到该topicId的队列中以获取实际响应。
- 保存用户消息 (
- Block 相关: 主要处理用户消息的初始
MessageBlock保存。
-
fetchAndProcessAssistantResponseImpl(dispatch, getState, topicId, assistant, assistantMessage)- 用途: (内部函数) 获取并处理单个助手响应的核心逻辑,被
sendMessage,resend...,regenerate...,append...等调用。 - 流程:
- 设置 Topic 加载状态。
- 准备上下文消息。
- 调用
fetchChatCompletionAPI 服务。 - 使用
createStreamProcessor处理流式响应。 - 通过各种回调 (
onTextChunk,onThinkingChunk,onToolCallComplete,onImageGenerated,onError,onComplete等) 处理不同类型的事件。
- Block 相关:
- 根据流事件创建初始
UNKNOWN块。 - 实时创建和更新
MAIN_TEXT和THINKING块,使用throttledBlockUpdate和throttledBlockDbUpdate进行节流更新。 - 创建
TOOL,CITATION,IMAGE,ERROR等类型的块。 - 在事件完成时(如
onTextComplete,onToolCallComplete)将块状态标记为SUCCESS或ERROR,并使用saveUpdatedBlockToDB保存最终状态。 - 使用
handleBlockTransition管理非流式块(如TOOL,CITATION)的添加和状态更新。
- 根据流事件创建初始
- 用途: (内部函数) 获取并处理单个助手响应的核心逻辑,被
-
loadTopicMessagesThunk(topicId, forceReload)- 用途: 从数据库加载指定主题的所有消息及其关联的
MessageBlock。 - 流程:
- 从 DB 获取
Topic及其messages列表。 - 根据消息 ID 列表从 DB 获取所有相关的
MessageBlock。 - 使用
upsertManyBlocks将块更新到 Redux。 - 将消息更新到 Redux。
- 从 DB 获取
- Block 相关: 负责将持久化的
MessageBlock加载到 Redux 状态。
- 用途: 从数据库加载指定主题的所有消息及其关联的
-
删除 Thunks
deleteSingleMessageThunk(topicId, messageId): 删除单个消息及其所有MessageBlock。deleteMessageGroupThunk(topicId, askId): 删除一个用户消息及其所有相关的助手响应消息和它们的所有MessageBlock。clearTopicMessagesThunk(topicId): 清空主题下的所有消息及其所有MessageBlock。- Block 相关: 从 Redux 和 DB 中移除指定的
MessageBlock。
-
重发/重新生成 Thunks
resendMessageThunk(topicId, userMessageToResend, assistant): 重发用户消息。会重置(清空 Block 并标记为 PENDING)所有与该用户消息关联的助手响应,然后重新请求生成。resendUserMessageWithEditThunk(topicId, originalMessage, mainTextBlockId, editedContent, assistant): 用户编辑消息内容后重发。先更新用户消息的MAIN_TEXT块内容,然后调用resendMessageThunk。regenerateAssistantResponseThunk(topicId, assistantMessageToRegenerate, assistant): 重新生成单个助手响应。重置该助手消息(清空 Block 并标记为 PENDING),然后重新请求生成。- Block 相关: 删除旧的
MessageBlock,并在重新生成过程中创建新的MessageBlock。
-
appendAssistantResponseThunk(topicId, existingAssistantMessageId, newModel, assistant)- 用途: 在已有的对话上下文中,针对同一个用户问题,使用新选择的模型追加一个新的助手响应。
- 流程:
- 找到现有助手消息以获取原始
askId。 - 创建使用
newModel的新助手消息存根(使用相同的askId)。 - 添加新存根到 Redux 和 DB。
- 将
fetchAndProcessAssistantResponseImpl添加到队列以生成新响应。
- 找到现有助手消息以获取原始
- Block 相关: 为新的助手响应创建全新的
MessageBlock。
-
cloneMessagesToNewTopicThunk(sourceTopicId, branchPointIndex, newTopic)- 用途: 将源主题的部分消息(及其 Block)克隆到一个已存在的新主题中。
- 流程:
- 复制指定索引前的消息。
- 为所有克隆的消息和 Block 生成新的 UUID。
- 正确映射克隆消息之间的
askId关系。 - 复制
MessageBlock内容,更新其messageId指向新的消息 ID。 - 更新文件引用计数(如果 Block 是文件或图片)。
- 将克隆的消息和 Block 保存到新主题的 Redux 状态和 DB 中。
- Block 相关: 创建
MessageBlock的副本,并更新其 ID 和messageId。
-
initiateTranslationThunk(messageId, topicId, targetLanguage, sourceBlockId?, sourceLanguage?)- 用途: 为指定消息启动翻译流程,创建一个初始的
TRANSLATION类型的MessageBlock。 - 流程:
- 创建一个状态为
STREAMING的TranslationMessageBlock。 - 将其添加到 Redux 和 DB。
- 更新原消息的
blocks列表以包含新的翻译块 ID。
- 创建一个状态为
- Block 相关: 创建并保存一个占位的
TranslationMessageBlock。实际翻译内容的获取和填充需要后续步骤。
- 用途: 为指定消息启动翻译流程,创建一个初始的
内部机制和注意事项
- 数据库交互: 通过
saveMessageAndBlocksToDB,updateExistingMessageAndBlocksInDB,saveUpdatesToDB,saveUpdatedBlockToDB,throttledBlockDbUpdate等辅助函数与 IndexedDB (db) 交互,确保数据持久化。 - 状态同步: Thunks 负责协调 Redux Store 和 IndexedDB 之间的数据一致性。
- 队列 (
getTopicQueue): 使用AsyncQueue确保对同一主题的操作(尤其是 API 请求)按顺序执行,避免竞态条件。 - 节流 (
throttle): 对流式响应中频繁的 Block 更新(文本、思考)使用lodash.throttle优化性能,减少 Redux dispatch 和 DB 写入次数。 - 错误处理:
fetchAndProcessAssistantResponseImpl内的回调函数(特别是onError)处理流处理和 API 调用中可能出现的错误,并创建ERROR类型的MessageBlock。
开发者在使用这些 Thunks 时,通常需要提供 dispatch, getState (由 Redux Thunk 中间件注入),以及如 topicId, assistant 配置对象, 相关的 Message 或 MessageBlock 对象/ID 等参数。理解每个 Thunk 的职责和它如何影响消息及块的状态至关重要。
useMessageOperations.ts 使用指南
该文件定义了一个名为 useMessageOperations 的自定义 React Hook。这个 Hook 的主要目的是为 React 组件提供一个便捷的接口,用于执行与特定主题(Topic)相关的各种消息操作。它封装了调用 Redux Thunks (messageThunk.ts) 和 Actions (newMessage.ts, messageBlock.ts) 的逻辑,简化了组件与消息数据交互的代码。
核心目标
- 封装: 将复杂的消息操作逻辑(如删除、重发、重新生成、编辑、翻译等)封装在易于使用的函数中。
- 简化: 让组件可以直接调用这些操作函数,而无需直接与 Redux
dispatch或 Thunks 交互。 - 上下文关联: 所有操作都与传入的
topic对象相关联,确保操作作用于正确的主题。
如何使用
在你的 React 函数组件中,导入并调用 useMessageOperations Hook,并传入当前活动的 Topic 对象。
import React from 'react';
import { useMessageOperations } from '@renderer/hooks/useMessageOperations';
import type { Topic, Message, Assistant, Model } from '@renderer/types';
interface MyComponentProps {
currentTopic: Topic;
currentAssistant: Assistant;
}
function MyComponent({ currentTopic, currentAssistant }: MyComponentProps) {
const {
deleteMessage,
resendMessage,
regenerateAssistantMessage,
appendAssistantResponse,
getTranslationUpdater,
createTopicBranch,
// ... 其他操作函数
} = useMessageOperations(currentTopic);
const handleDelete = (messageId: string) => {
deleteMessage(messageId);
};
const handleResend = (message: Message) => {
resendMessage(message, currentAssistant);
};
const handleAppend = (existingMsg: Message, newModel: Model) => {
appendAssistantResponse(existingMsg, newModel, currentAssistant);
}
// ... 在组件中使用其他操作函数
return (
<div>
{/* Component UI */}
<button onClick={() => handleDelete('some-message-id')}>Delete Message</button>
{/* ... */}
</div>
);
}
返回值
useMessageOperations(topic) Hook 返回一个包含以下函数和值的对象:
-
deleteMessage(id: string):- 删除指定
id的单个消息。 - 内部调用
deleteSingleMessageThunk。
- 删除指定
-
deleteGroupMessages(askId: string):- 删除与指定
askId相关联的一组消息(通常是用户提问及其所有助手回答)。 - 内部调用
deleteMessageGroupThunk。
- 删除与指定
-
editMessage(messageId: string, updates: Partial<Message>):- 更新指定
messageId的消息的部分属性。 - 注意: 目前主要用于更新 Redux 状态
- 内部调用
newMessagesActions.updateMessage。
- 更新指定
-
resendMessage(message: Message, assistant: Assistant):- 重新发送指定的用户消息 (
message),这将触发其所有关联助手响应的重新生成。 - 内部调用
resendMessageThunk。
- 重新发送指定的用户消息 (
-
resendUserMessageWithEdit(message: Message, editedContent: string, assistant: Assistant):- 在用户消息的主要文本块被编辑后,重新发送该消息。
- 会先查找消息的
MAIN_TEXT块 ID,然后调用resendUserMessageWithEditThunk。
-
clearTopicMessages(_topicId?: string):- 清除当前主题(或可选的指定
_topicId)下的所有消息。 - 内部调用
clearTopicMessagesThunk。
- 清除当前主题(或可选的指定
-
createNewContext():- 发出一个全局事件 (
EVENT_NAMES.NEW_CONTEXT),通常用于通知 UI 清空显示,准备新的上下文。不直接修改 Redux 状态。
- 发出一个全局事件 (
-
displayCount:- (非操作函数) 从 Redux store 中获取当前的
displayCount值。
- (非操作函数) 从 Redux store 中获取当前的
-
pauseMessages():- 尝试中止当前主题中正在进行的消息生成(状态为
processing或pending)。 - 通过查找相关的
askId并调用abortCompletion来实现。 - 同时会 dispatch
setTopicLoadingaction 将加载状态设为false。
- 尝试中止当前主题中正在进行的消息生成(状态为
-
resumeMessage(message: Message, assistant: Assistant):- 恢复/重新发送一个用户消息。目前实现为直接调用
resendMessage。
- 恢复/重新发送一个用户消息。目前实现为直接调用
-
regenerateAssistantMessage(message: Message, assistant: Assistant):- 重新生成指定的助手消息 (
message) 的响应。 - 内部调用
regenerateAssistantResponseThunk。
- 重新生成指定的助手消息 (
-
appendAssistantResponse(existingAssistantMessage: Message, newModel: Model, assistant: Assistant):- 针对
existingAssistantMessage所回复的同一用户提问,使用newModel追加一个新的助手响应。 - 内部调用
appendAssistantResponseThunk。
- 针对
-
getTranslationUpdater(messageId: string, targetLanguage: string, sourceBlockId?: string, sourceLanguage?: string):- 用途: 获取一个用于逐步更新翻译块内容的函数。
- 流程:
- 内部调用
initiateTranslationThunk来创建或获取一个TRANSLATION类型的MessageBlock,并获取其blockId。 - 返回一个异步更新函数。
- 内部调用
- 返回的更新函数
(accumulatedText: string, isComplete?: boolean) => void:- 接收累积的翻译文本和完成状态。
- 调用
updateOneBlock更新 Redux 中的翻译块内容和状态 (STREAMING或SUCCESS)。 - 调用
throttledBlockDbUpdate将更新(节流地)保存到数据库。
- 如果初始化失败(Thunk 返回
undefined),则此函数返回null。
-
createTopicBranch(sourceTopicId: string, branchPointIndex: number, newTopic: Topic):- 创建一个主题分支,将
sourceTopicId主题中branchPointIndex索引之前的消息克隆到newTopic中。 - 注意:
newTopic对象必须是调用此函数之前已经创建并添加到 Redux 和数据库中的。 - 内部调用
cloneMessagesToNewTopicThunk。
- 创建一个主题分支,将
依赖
topic: Topic: 必须传入当前操作上下文的主题对象。Hook 返回的操作函数将始终作用于这个主题的topic.id。- Redux
dispatch: Hook 内部使用useAppDispatch获取dispatch函数来调用 actions 和 thunks。
相关 Hooks
在同一文件中还定义了两个辅助 Hook:
-
useTopicMessages(topic: Topic):- 使用
selectMessagesForTopicselector 来获取并返回指定主题的消息列表。
- 使用
-
useTopicLoading(topic: Topic):- 使用
selectNewTopicLoadingselector 来获取并返回指定主题的加载状态。
- 使用
这些 Hook 可以与 useMessageOperations 结合使用,方便地在组件中获取消息数据、加载状态,并执行相关操作。
