Files
CherryHQ-cherry-studio/docs/references/messaging/message-system.md
SuYao 611944599f refactor(deps): replace lodash with es-toolkit/compat and drop it (#16528)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
2026-06-29 12:28:57 +08:00

14 KiB

Message System

This document describes the Cherry Studio message system architecture, including message lifecycle, state management, and operation interfaces.

Message Lifecycle

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 MessageBlocks. 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:

{
  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 MessageBlocks. 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 MessageBlocks 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):

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

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 MessageBlocks.
  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 MessageBlocks (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 MessageBlocks for a topic from the database.
  • Flow: Fetch Topic and messages from DB, get all related MessageBlocks, 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

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).
  • 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.