feat: implement assistant message creation via StreamingService

- Added a new method `createAssistantMessage` in StreamingService to facilitate the creation of assistant messages through the Data API, ensuring server-generated message IDs while maintaining client-side data integrity.
- Updated `messageThunk` to utilize the new method for creating assistant messages, replacing previous direct API calls and enhancing the overall message handling process.
- Introduced a conversion method to transform shared message formats from the Data API into the renderer's expected format, streamlining message processing and improving code organization.
This commit is contained in:
fullex 2026-01-04 09:49:32 +08:00
parent 7e8cc430a8
commit 542702ad56
2 changed files with 100 additions and 83 deletions

View File

@ -21,11 +21,12 @@
import { cacheService } from '@data/CacheService'
import { dataApiService } from '@data/DataApiService'
import { loggerService } from '@logger'
import type { Model } from '@renderer/types'
import type { Message, MessageBlock } from '@renderer/types/newMessage'
import { AssistantMessageStatus, MessageBlockStatus } from '@renderer/types/newMessage'
import { isAgentSessionTopicId } from '@renderer/utils/agentSession'
import type { CreateMessageDto, UpdateMessageDto } from '@shared/data/api/schemas/messages'
import type { MessageDataBlock, MessageStats } from '@shared/data/types/message'
import type { Message as SharedMessage, MessageDataBlock, MessageStats } from '@shared/data/types/message'
import { dbService } from '../db'
@ -80,6 +81,18 @@ interface StartSessionOptions {
contextMessages?: Message[]
}
/**
* Options for creating an assistant message
*/
interface CreateAssistantMessageOptions {
parentId: string // askId (user message id)
assistantId: string
modelId?: string
model?: Model
siblingsGroupId?: number
traceId?: string
}
/**
* StreamingService - Manages streaming message state during generation
*
@ -509,8 +522,69 @@ class StreamingService {
}
}
// ============ Assistant Message Creation ============
/**
* Create an assistant message via Data API
*
* The message ID is generated by the server, not locally.
* This method is used for normal topics only (not agent sessions).
*
* @param topicId - Topic ID
* @param options - Creation options including parentId, assistantId, modelId
* @returns Message with server-generated ID in renderer format
*/
async createAssistantMessage(topicId: string, options: CreateAssistantMessageOptions): Promise<Message> {
const { parentId, assistantId, modelId, model, siblingsGroupId = 0, traceId } = options
const createDto: CreateMessageDto = {
parentId,
role: 'assistant',
data: { blocks: [] },
status: 'pending',
siblingsGroupId,
assistantId,
modelId,
traceId
}
const sharedMessage = (await dataApiService.post(`/topics/${topicId}/messages`, {
body: createDto
})) as SharedMessage
logger.debug('Created assistant message via Data API', { topicId, messageId: sharedMessage.id })
return this.convertSharedToRendererMessage(sharedMessage, assistantId, model)
}
// ============ Internal Methods ============
/**
* Convert shared Message format (from Data API) to renderer Message format
*
* For newly created pending messages, blocks are empty.
*
* @param shared - Message from Data API response
* @param assistantId - Assistant ID to include
* @param model - Optional Model object to include
* @returns Renderer-format Message
*/
private convertSharedToRendererMessage(shared: SharedMessage, assistantId: string, model?: Model): Message {
return {
id: shared.id,
topicId: shared.topicId,
role: shared.role,
assistantId,
status: shared.status as AssistantMessageStatus,
blocks: [], // For new pending messages, blocks are empty
createdAt: shared.createdAt,
askId: shared.parentId ?? undefined,
modelId: shared.modelId ?? undefined,
traceId: shared.traceId ?? undefined,
model
}
}
/**
* Convert renderer MessageBlock[] to shared MessageDataBlock[]
* Removes renderer-specific fields: id, status, messageId

View File

@ -15,7 +15,6 @@
* --------------------------------------------------------------------------
*/
import { cacheService } from '@data/CacheService'
import { dataApiService } from '@data/DataApiService'
import { loggerService } from '@logger'
import { AiSdkToChunkAdapter } from '@renderer/aiCore/chunk/AiSdkToChunkAdapter'
import { AgentApiClient } from '@renderer/api/agent'
@ -50,8 +49,6 @@ import {
} from '@renderer/utils/messageUtils/create'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { getTopicQueue, waitForTopicQueue } from '@renderer/utils/queue'
import type { CreateMessageDto } from '@shared/data/api/schemas/messages'
import type { Message as SharedMessage } from '@shared/data/types/message'
import { IpcChannel } from '@shared/IpcChannel'
import { defaultAppHeaders } from '@shared/utils'
import type { TextStreamPart } from 'ai'
@ -78,35 +75,6 @@ import { newMessagesActions, selectMessagesForTopic } from '../newMessage'
const logger = loggerService.withContext('MessageThunk')
/**
* Convert shared Message format (from Data API) to renderer Message format
*
* The Data API returns messages with `data: { blocks: MessageDataBlock[] }` format,
* but the renderer expects `blocks: string[]` format.
*
* For newly created pending messages, blocks are empty, so conversion is straightforward.
* For messages with content, this would need to store blocks separately and return IDs.
*
* @param shared - Message from Data API response
* @param model - Optional Model object to include
* @returns Renderer-format Message
*/
const convertSharedToRendererMessage = (shared: SharedMessage, assistantId: string, model?: Model): Message => {
return {
id: shared.id,
topicId: shared.topicId,
role: shared.role,
assistantId,
status: shared.status as AssistantMessageStatus,
blocks: [], // For new pending messages, blocks are empty
createdAt: shared.createdAt,
askId: shared.parentId ?? undefined,
modelId: shared.modelId ?? undefined,
traceId: shared.traceId ?? undefined,
model
}
}
const finishTopicLoading = async (topicId: string) => {
await waitForTopicQueue(topicId)
store.dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false }))
@ -779,20 +747,15 @@ const dispatchMultiModelResponses = async (
for (const mentionedModel of mentionedModels) {
const assistantForThisMention = { ...assistant, model: mentionedModel }
// Create message via Data API instead of local creation
const createDto: CreateMessageDto = {
// Create message via StreamingService
const assistantMessage = await streamingService.createAssistantMessage(topicId, {
parentId: triggeringMessage.id,
role: 'assistant',
data: { blocks: [] },
status: 'pending',
siblingsGroupId,
assistantId: assistant.id,
modelId: mentionedModel.id,
model: mentionedModel,
siblingsGroupId,
traceId: triggeringMessage.traceId ?? undefined
}
const sharedMessage = await dataApiService.post(`/topics/${topicId}/messages`, { body: createDto })
const assistantMessage = convertSharedToRendererMessage(sharedMessage, assistant.id, mentionedModel)
})
dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
assistantMessageStubs.push(assistantMessage)
@ -1024,20 +987,15 @@ export const sendMessage =
if (mentionedModels && mentionedModels.length > 0) {
await dispatchMultiModelResponses(dispatch, getState, topicId, finalUserMessage, assistant, mentionedModels)
} else {
// Create message via Data API for normal topics
const createDto: CreateMessageDto = {
// Create message via StreamingService for normal topics
const assistantMessage = await streamingService.createAssistantMessage(topicId, {
parentId: finalUserMessage.id,
role: 'assistant',
data: { blocks: [] },
status: 'pending',
siblingsGroupId: 0,
assistantId: assistant.id,
modelId: assistant.model?.id,
model: assistant.model,
siblingsGroupId: 0,
traceId: finalUserMessage.traceId ?? undefined
}
const sharedMessage = await dataApiService.post(`/topics/${topicId}/messages`, { body: createDto })
const assistantMessage = convertSharedToRendererMessage(sharedMessage, assistant.id, assistant.model)
})
dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
@ -1228,20 +1186,15 @@ export const resendMessageThunk =
if (assistantMessagesToReset.length === 0 && !userMessageToResend?.mentions?.length) {
// 没有相关的助手消息且没有提及模型时,使用助手模型创建一条消息
// Create message via Data API
const createDto: CreateMessageDto = {
// Create message via StreamingService
const assistantMessage = await streamingService.createAssistantMessage(topicId, {
parentId: userMessageToResend.id,
role: 'assistant',
data: { blocks: [] },
status: 'pending',
siblingsGroupId: 0,
assistantId: assistant.id,
modelId: assistant.model?.id,
model: assistant.model,
siblingsGroupId: 0,
traceId: userMessageToResend.traceId ?? undefined
}
const sharedMessage = await dataApiService.post(`/topics/${topicId}/messages`, { body: createDto })
const assistantMessage = convertSharedToRendererMessage(sharedMessage, assistant.id, assistant.model)
})
resetDataList.push(assistantMessage)
@ -1277,20 +1230,15 @@ export const resendMessageThunk =
const mentionedModelSet = new Set(userMessageToResend.mentions ?? [])
const newModelSet = new Set([...mentionedModelSet].filter((m) => !originModelSet.has(m)))
for (const model of newModelSet) {
// Create message via Data API for new mentioned models
const createDto: CreateMessageDto = {
// Create message via StreamingService for new mentioned models
const assistantMessage = await streamingService.createAssistantMessage(topicId, {
parentId: userMessageToResend.id,
role: 'assistant',
data: { blocks: [] },
status: 'pending',
siblingsGroupId: 0,
assistantId: assistant.id,
modelId: model.id,
model,
siblingsGroupId: 0,
traceId: userMessageToResend.traceId ?? undefined
}
const sharedMessage = await dataApiService.post(`/topics/${topicId}/messages`, { body: createDto })
const assistantMessage = convertSharedToRendererMessage(sharedMessage, assistant.id, model)
})
resetDataList.push(assistantMessage)
dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
@ -1592,20 +1540,15 @@ export const appendAssistantResponseThunk =
return
}
// 2. Create the new assistant message via Data API
const createDto: CreateMessageDto = {
// 2. Create the new assistant message via StreamingService
const newAssistantMessageStub = await streamingService.createAssistantMessage(topicId, {
parentId: askId, // Crucial: Use the original askId
role: 'assistant',
data: { blocks: [] },
status: 'pending',
siblingsGroupId: 0,
assistantId: assistant.id,
modelId: newModel.id,
model: newModel,
siblingsGroupId: 0,
traceId: traceId ?? undefined
}
const sharedMessage = await dataApiService.post(`/topics/${topicId}/messages`, { body: createDto })
const newAssistantMessageStub = convertSharedToRendererMessage(sharedMessage, assistant.id, newModel)
})
// 3. Update Redux Store
const currentTopicMessageIds = getState().messages.messageIdsByTopic[topicId] || []