mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-05 21:50:46 +08:00
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
250 lines
14 KiB
Markdown
250 lines
14 KiB
Markdown
# Message System
|
|
|
|
This document describes the Cherry Studio message system architecture, including message lifecycle, state management, and operation interfaces.
|
|
|
|
## Message Lifecycle
|
|
|
|

|
|
|
|
---
|
|
|
|
# messageBlock.ts Usage Guide
|
|
|
|
This file defines the Redux Slice for managing all `MessageBlock` entities in the application. It uses Redux Toolkit's `createSlice` and `createEntityAdapter` to efficiently handle normalized state, providing a set of actions and selectors for interacting with message block data.
|
|
|
|
## Core Goals
|
|
|
|
- **State Management**: Centrally manage the state of all `MessageBlock`s. A `MessageBlock` represents different content units within a message (e.g., text, code, images, citations).
|
|
- **Normalization**: Uses `createEntityAdapter` to store `MessageBlock` data in a normalized structure (`{ ids: [], entities: {} }`), improving performance and simplifying update logic.
|
|
- **Predictability**: Provides explicit actions to modify state and selectors to safely access state.
|
|
|
|
## Key Concepts
|
|
|
|
- **Slice (`createSlice`)**: Redux Toolkit's core API for creating Redux modules containing reducer logic, action creators, and initial state.
|
|
- **Entity Adapter (`createEntityAdapter`)**: A Redux Toolkit utility that simplifies CRUD (Create, Read, Update, Delete) operations on normalized data. It auto-generates reducer functions and selectors.
|
|
- **Selectors**: Functions for deriving and computing data from the Redux store. Selectors can be memoized for improved performance.
|
|
|
|
## State Structure
|
|
|
|
The `messageBlocks` slice state structure is defined by `createEntityAdapter`, roughly:
|
|
|
|
```typescript
|
|
{
|
|
ids: string[]; // Ordered list of all MessageBlock IDs
|
|
entities: { [id: string]: MessageBlock }; // Dictionary mapping ID to MessageBlock objects
|
|
loadingState: 'idle' | 'loading' | 'succeeded' | 'failed';
|
|
error: string | null;
|
|
}
|
|
```
|
|
|
|
## Actions
|
|
|
|
The slice exports the following actions (auto-generated by `createSlice` and `createEntityAdapter` or custom):
|
|
|
|
- **`upsertOneBlock(payload: MessageBlock)`**: Add a new `MessageBlock` or update an existing one. If the `id` in the payload already exists, it performs an update; otherwise it inserts.
|
|
|
|
- **`upsertManyBlocks(payload: MessageBlock[])`**: Add or update multiple `MessageBlock`s. Commonly used for batch loading data (e.g., loading all message blocks for a Topic).
|
|
|
|
- **`removeOneBlock(payload: string)`**: Remove a single `MessageBlock` by the provided `id`.
|
|
|
|
- **`removeManyBlocks(payload: string[])`**: Remove multiple `MessageBlock`s by the provided `id` array. Commonly used when deleting messages or clearing a Topic.
|
|
|
|
- **`removeAllBlocks()`**: Remove all `MessageBlock` entities from state.
|
|
|
|
- **`updateOneBlock(payload: { id: string; changes: Partial<MessageBlock> })`**: Update an existing `MessageBlock`. The `payload` must contain the block's `id` and a `changes` object with the fields to modify.
|
|
|
|
- **`setMessageBlocksLoading(payload: 'idle' | 'loading')`**: (Custom) Set the `loadingState` property.
|
|
|
|
- **`setMessageBlocksError(payload: string)`**: (Custom) Set `loadingState` to `'failed'` and record the error message.
|
|
|
|
**Usage Example (in Thunks or other dispatch locations):**
|
|
|
|
```typescript
|
|
import { upsertOneBlock, removeManyBlocks, updateOneBlock } from './messageBlock'
|
|
import store from './store'
|
|
|
|
// Add or update a block
|
|
const newBlock: MessageBlock = { /* ... block data ... */ }
|
|
store.dispatch(upsertOneBlock(newBlock))
|
|
|
|
// Update a block's content
|
|
store.dispatch(updateOneBlock({ id: blockId, changes: { content: 'New content' } }))
|
|
|
|
// Delete multiple blocks
|
|
const blockIdsToRemove = ['id1', 'id2']
|
|
store.dispatch(removeManyBlocks(blockIdsToRemove))
|
|
```
|
|
|
|
## Selectors
|
|
|
|
The slice exports base selectors generated by `createEntityAdapter`, accessed through `messageBlocksSelectors`:
|
|
|
|
- **`messageBlocksSelectors.selectIds(state: RootState): string[]`**: Returns an array of all block IDs.
|
|
- **`messageBlocksSelectors.selectEntities(state: RootState): { [id: string]: MessageBlock }`**: Returns a dictionary mapping block IDs to block objects.
|
|
- **`messageBlocksSelectors.selectAll(state: RootState): MessageBlock[]`**: Returns an array of all block objects.
|
|
- **`messageBlocksSelectors.selectTotal(state: RootState): number`**: Returns the total number of blocks.
|
|
- **`messageBlocksSelectors.selectById(state: RootState, id: string): MessageBlock | undefined`**: Returns a single block by ID, or `undefined` if not found.
|
|
|
|
**Additionally, a custom memoized selector is provided:**
|
|
|
|
- **`selectFormattedCitationsByBlockId(state: RootState, blockId: string | undefined): Citation[]`**:
|
|
- Takes a `blockId`.
|
|
- If the corresponding block is of `CITATION` type, it extracts and formats citation information (from web search, knowledge base, etc.), deduplicates, renumbers, and returns a `Citation[]` array for UI display.
|
|
- Returns an empty array `[]` if the block doesn't exist or the type doesn't match.
|
|
- This selector encapsulates complex logic for handling different citation sources (Gemini, OpenAI, OpenRouter, Zhipu, etc.).
|
|
|
|
**Usage Example (in React components or `useSelector`):**
|
|
|
|
```typescript
|
|
import { useSelector } from 'react-redux'
|
|
import { messageBlocksSelectors, selectFormattedCitationsByBlockId } from './messageBlock'
|
|
import type { RootState } from './store'
|
|
|
|
// Get all blocks
|
|
const allBlocks = useSelector(messageBlocksSelectors.selectAll)
|
|
|
|
// Get a specific block by ID
|
|
const specificBlock = useSelector((state: RootState) => messageBlocksSelectors.selectById(state, someBlockId))
|
|
|
|
// Get formatted citations for a citation block
|
|
const formattedCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, citationBlockId))
|
|
```
|
|
|
|
## Integration
|
|
|
|
The `messageBlock.ts` slice typically works closely with Thunks in `messageThunk.ts`. Thunks handle async logic (API calls, database operations) and dispatch `messageBlock` slice actions to update state as needed. For example, when `messageThunk` receives a streaming response, it dispatches `upsertOneBlock` or `updateOneBlock` to update the corresponding `MessageBlock` in real-time. Similarly, message deletion Thunks dispatch `removeManyBlocks`.
|
|
|
|
Understanding that `messageBlock.ts` manages **the state itself** while `messageThunk.ts` handles **the async workflows that trigger state changes** is essential for maintaining a clear application architecture.
|
|
|
|
---
|
|
|
|
# messageThunk.ts Usage Guide
|
|
|
|
This file contains core Thunk Action Creators for managing message flows, handling assistant interactions, and synchronizing Redux state with the IndexedDB database. It primarily operates on `Message` and `MessageBlock` objects.
|
|
|
|
## Core Functions
|
|
|
|
1. **Send/Receive Messages**: Handle user message sending, trigger assistant responses, stream-process returned data, and parse it into different `MessageBlock`s.
|
|
2. **State Management**: Ensure consistency between message and message block state in the Redux store and persisted data in IndexedDB.
|
|
3. **Message Operations**: Provide deletion, resend, regeneration, edit-and-resend, append response, clone, and other message lifecycle management functions.
|
|
4. **Block Processing**: Dynamically create, update, and save various types of `MessageBlock`s (text, thinking process, tool calls, citations, images, errors, translations, etc.).
|
|
|
|
## Key Thunks
|
|
|
|
### 1. `sendMessage(userMessage, userMessageBlocks, assistant, topicId)`
|
|
|
|
- **Purpose**: Send a new user message.
|
|
- **Flow**:
|
|
- Save user message and its blocks to Redux and DB.
|
|
- Check `@mentions` to determine single-model or multi-model response.
|
|
- Create assistant message stub(s).
|
|
- Add stubs to Redux and DB.
|
|
- Queue `fetchAndProcessAssistantResponseImpl` for the `topicId` to get actual responses.
|
|
|
|
### 2. `fetchAndProcessAssistantResponseImpl(dispatch, getState, topicId, assistant, assistantMessage)`
|
|
|
|
- **Purpose**: (Internal) Core logic for fetching and processing a single assistant response.
|
|
- **Flow**:
|
|
- Set Topic loading state.
|
|
- Prepare context messages.
|
|
- Call `fetchChatCompletion` API service.
|
|
- Process streaming response using `createStreamProcessor`.
|
|
- Handle different event types via callbacks (`onTextChunk`, `onThinkingChunk`, `onToolCallComplete`, `onImageGenerated`, `onError`, `onComplete`, etc.).
|
|
- **Block handling**: Creates initial `UNKNOWN` blocks from stream events, real-time creates/updates `MAIN_TEXT` and `THINKING` blocks with throttled updates, creates `TOOL`, `CITATION`, `IMAGE`, `ERROR` blocks as needed.
|
|
|
|
### 3. `loadTopicMessagesThunk(topicId, forceReload)`
|
|
|
|
- **Purpose**: Load all messages and associated `MessageBlock`s for a topic from the database.
|
|
- **Flow**: Fetch `Topic` and messages from DB, get all related `MessageBlock`s, upsert blocks and messages to Redux.
|
|
|
|
### 4. Delete Thunks
|
|
|
|
- `deleteSingleMessageThunk(topicId, messageId)`: Delete a single message and all its blocks.
|
|
- `deleteMessageGroupThunk(topicId, askId)`: Delete a user message and all related assistant responses with their blocks.
|
|
- `clearTopicMessagesThunk(topicId)`: Clear all messages and blocks under a topic.
|
|
|
|
### 5. Resend/Regenerate Thunks
|
|
|
|
- `resendMessageThunk(topicId, userMessageToResend, assistant)`: Resend a user message. Resets all associated assistant responses (clears blocks, marks as PENDING), then re-requests generation.
|
|
- `resendUserMessageWithEditThunk(...)`: Update user message's `MAIN_TEXT` block content, then call `resendMessageThunk`.
|
|
- `regenerateAssistantResponseThunk(...)`: Regenerate a single assistant response. Resets the message (clears blocks, marks as PENDING), then re-requests generation.
|
|
|
|
### 6. `appendAssistantResponseThunk(topicId, existingAssistantMessageId, newModel, assistant)`
|
|
|
|
- **Purpose**: Append a new assistant response using a different model for the same user question.
|
|
- Creates a new assistant message stub using `newModel` (with the same `askId`), then queues `fetchAndProcessAssistantResponseImpl`.
|
|
|
|
### 7. `cloneMessagesToNewTopicThunk(sourceTopicId, branchPointIndex, newTopic)`
|
|
|
|
- **Purpose**: Clone messages (and their blocks) from a source topic up to a given index into a new topic.
|
|
- Generates new UUIDs for all cloned messages and blocks, maps `askId` relationships, updates file reference counts.
|
|
|
|
### 8. `initiateTranslationThunk(messageId, topicId, targetLanguage, sourceBlockId?, sourceLanguage?)`
|
|
|
|
- **Purpose**: Create an initial `TRANSLATION` type `MessageBlock` for a message.
|
|
- Creates a `STREAMING` status `TranslationMessageBlock`, adds to Redux and DB, updates the message's `blocks` list.
|
|
|
|
## Internal Mechanisms
|
|
|
|
- **Database Interaction**: Uses helper functions like `saveMessageAndBlocksToDB`, `throttledBlockDbUpdate`, etc. to interact with IndexedDB.
|
|
- **State Synchronization**: Thunks coordinate data consistency between the Redux Store and IndexedDB.
|
|
- **Queue (`getTopicQueue`)**: Uses `AsyncQueue` to ensure operations on the same topic execute sequentially, avoiding race conditions.
|
|
- **Throttling (`throttle`)**: Uses `throttle` from es-toolkit/compat to optimize frequent block updates (text, thinking) during streaming, reducing Redux dispatch and DB write frequency.
|
|
- **Error Handling**: Callbacks in `fetchAndProcessAssistantResponseImpl` (especially `onError`) handle stream processing and API call errors, creating `ERROR` type blocks.
|
|
|
|
---
|
|
|
|
# useMessageOperations.ts Usage Guide
|
|
|
|
This file defines a custom React Hook called `useMessageOperations`. Its main purpose is to provide React components with a convenient interface for performing various message operations related to a specific Topic. It encapsulates the logic for calling Redux Thunks and Actions, simplifying component interaction with message data.
|
|
|
|
## Core Goals
|
|
|
|
- **Encapsulation**: Wrap complex message operation logic (delete, resend, regenerate, edit, translate, etc.) in easy-to-use functions.
|
|
- **Simplification**: Allow components to call operation functions directly without interacting with Redux `dispatch` or Thunks.
|
|
- **Context Association**: All operations are associated with the provided `topic` object.
|
|
|
|
## How to Use
|
|
|
|
```typescript
|
|
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
|
import type { Topic, Message, Assistant, Model } from '@renderer/types'
|
|
|
|
function MyComponent({ currentTopic, currentAssistant }: { currentTopic: Topic; currentAssistant: Assistant }) {
|
|
const {
|
|
deleteMessage,
|
|
resendMessage,
|
|
regenerateAssistantMessage,
|
|
appendAssistantResponse,
|
|
getTranslationUpdater,
|
|
createTopicBranch,
|
|
} = useMessageOperations(currentTopic)
|
|
|
|
// Use the operation functions in your component
|
|
}
|
|
```
|
|
|
|
## Return Values
|
|
|
|
- **`deleteMessage(id: string)`**: Delete a single message by ID.
|
|
- **`deleteGroupMessages(askId: string)`**: Delete a group of messages associated with an `askId`.
|
|
- **`editMessage(messageId: string, updates: Partial<Message>)`**: Update partial attributes of a message.
|
|
- **`resendMessage(message: Message, assistant: Assistant)`**: Resend a user message, triggering regeneration of all associated assistant responses.
|
|
- **`resendUserMessageWithEdit(message: Message, editedContent: string, assistant: Assistant)`**: Resend after editing user message text.
|
|
- **`clearTopicMessages(_topicId?: string)`**: Clear all messages in the current (or specified) topic.
|
|
- **`createNewContext()`**: Emit a global event to clear display and prepare a new context.
|
|
- **`displayCount`**: Current `displayCount` value from Redux store.
|
|
- **`pauseMessages()`**: Abort ongoing message generation for the current topic.
|
|
- **`resumeMessage(message: Message, assistant: Assistant)`**: Resume/resend a user message.
|
|
- **`regenerateAssistantMessage(message: Message, assistant: Assistant)`**: Regenerate a specific assistant message response.
|
|
- **`appendAssistantResponse(existingAssistantMessage: Message, newModel: Model, assistant: Assistant)`**: Append a new assistant response using a different model.
|
|
- **`getTranslationUpdater(messageId, targetLanguage, sourceBlockId?, sourceLanguage?)`**: Get a function for progressively updating a translation block's content. Returns an async updater function or `null` on failure.
|
|
- **`createTopicBranch(sourceTopicId, branchPointIndex, newTopic)`**: Clone messages from a source topic to a new topic (which must already exist in Redux and DB).
|
|
|
|
## Related Hooks
|
|
|
|
- **`useTopicMessages(topic: Topic)`**: Get the message list for a topic.
|
|
- **`useTopicLoading(topic: Topic)`**: Get the loading state for a topic.
|
|
|
|
These hooks can be combined with `useMessageOperations` for convenient data access and operations within components.
|