Files
cherry-studio/src/renderer/src/databases/upgrades.ts
T

311 lines
11 KiB
TypeScript

import Logger from '@renderer/config/logger'
import type { LegacyMessage as OldMessage, Topic } from '@renderer/types'
import { FileTypes } from '@renderer/types' // Import FileTypes enum
import { WebSearchSource } from '@renderer/types'
import type {
BaseMessageBlock,
CitationMessageBlock,
Message as NewMessage,
MessageBlock
} from '@renderer/types/newMessage'
import { AssistantMessageStatus, MessageBlockStatus } from '@renderer/types/newMessage'
import { Transaction } from 'dexie'
import { isEmpty } from 'lodash'
import {
createCitationBlock,
createErrorBlock,
createFileBlock,
createImageBlock,
createMainTextBlock,
createThinkingBlock,
createToolBlock,
createTranslationBlock
} from '../utils/messageUtils/create'
export async function upgradeToV5(tx: Transaction): Promise<void> {
const topics = await tx.table('topics').toArray()
const files = await tx.table('files').toArray()
for (const file of files) {
if (file.created_at instanceof Date) {
file.created_at = file.created_at.toISOString()
await tx.table('files').put(file)
}
}
for (const topic of topics) {
let hasChanges = false
for (const message of topic.messages) {
if (message?.metadata?.tavily) {
hasChanges = true
const tavily = message.metadata.tavily
delete message.metadata.tavily
message.metadata.webSearch = {
query: tavily.query,
results:
tavily.results?.map((i) => ({
title: i.title,
url: i.url,
content: i.content
})) || []
}
}
}
if (hasChanges) {
await tx.table('topics').put(topic)
}
}
}
// --- Simplified status mapping functions ---
function mapOldStatusToBlockStatus(oldStatus: OldMessage['status']): MessageBlockStatus {
// Handle statuses that need mapping
if (oldStatus === 'sending' || oldStatus === 'pending' || oldStatus === 'searching') {
return MessageBlockStatus.PROCESSING
}
// For success, paused, error, the values match MessageBlockStatus
if (oldStatus === 'success' || oldStatus === 'paused' || oldStatus === 'error') {
// Cast is safe here as the values are identical
return oldStatus as MessageBlockStatus
}
// Default fallback for any unexpected old status
return MessageBlockStatus.PROCESSING
}
function mapOldStatusToNewMessageStatus(oldStatus: OldMessage['status']): NewMessage['status'] {
// Handle statuses that need mapping
if (oldStatus === 'pending' || oldStatus === 'sending') {
return AssistantMessageStatus.PENDING
}
// For sending, success, paused, error, the values match NewMessage['status']
if (oldStatus === 'searching' || oldStatus === 'success' || oldStatus === 'paused' || oldStatus === 'error') {
// Cast is safe here as the values are identical
return oldStatus as NewMessage['status']
}
// Default fallback
return AssistantMessageStatus.PROCESSING
}
// --- UPDATED UPGRADE FUNCTION for Version 7 ---
export async function upgradeToV7(tx: Transaction): Promise<void> {
Logger.info('Starting DB migration to version 7: Normalizing messages and blocks...')
const oldTopicsTable = tx.table('topics')
const newBlocksTable = tx.table('message_blocks')
const topicUpdates: Record<string, { messages: NewMessage[] }> = {}
await oldTopicsTable.toCollection().each(async (oldTopic: Pick<Topic, 'id'> & { messages: OldMessage[] }) => {
const newMessagesForTopic: NewMessage[] = []
const blocksToCreate: MessageBlock[] = []
if (!oldTopic.messages || !Array.isArray(oldTopic.messages)) {
console.warn(`Topic ${oldTopic.id} has no valid messages array, skipping.`)
topicUpdates[oldTopic.id] = { messages: [] }
return
}
for (const oldMessage of oldTopic.messages) {
const messageBlockIds: string[] = []
const citationDataToCreate: Partial<Omit<CitationMessageBlock, keyof BaseMessageBlock | 'type'>> = {}
let hasCitationData = false
// 2. Thinking Block (Status is SUCCESS)
// 挪到前面,尽量保持与旧版本的一致性
if (oldMessage.reasoning_content?.trim()) {
const block = createThinkingBlock(oldMessage.id, oldMessage.reasoning_content, {
createdAt: oldMessage.createdAt,
thinking_millsec: oldMessage?.metrics?.time_thinking_millsec,
status: MessageBlockStatus.SUCCESS // Thinking block is complete content
})
blocksToCreate.push(block)
messageBlockIds.push(block.id)
}
// 7. Tool Blocks (Status based on original mcpTool status)
// 挪到前面,尽量保持与旧版本的一致性
if (oldMessage.metadata?.mcpTools?.length) {
oldMessage.metadata.mcpTools.forEach((mcpTool) => {
const block = createToolBlock(oldMessage.id, mcpTool.id, {
// Determine status based on original tool status
status: MessageBlockStatus.SUCCESS,
content: mcpTool.response,
error:
mcpTool.status !== 'done'
? { message: 'MCP Tool did not complete', originalStatus: mcpTool.status }
: undefined,
createdAt: oldMessage.createdAt,
metadata: { rawMcpToolResponse: mcpTool }
})
blocksToCreate.push(block)
messageBlockIds.push(block.id)
})
}
// 1. Main Text Block
if (oldMessage.content?.trim()) {
const block = createMainTextBlock(oldMessage.id, oldMessage.content, {
createdAt: oldMessage.createdAt,
status: mapOldStatusToBlockStatus(oldMessage.status),
knowledgeBaseIds: oldMessage.knowledgeBaseIds
})
blocksToCreate.push(block)
messageBlockIds.push(block.id)
}
// 3. Translation Block (Status is SUCCESS)
if (oldMessage.translatedContent?.trim()) {
const block = createTranslationBlock(oldMessage.id, oldMessage.translatedContent, 'unknown', {
createdAt: oldMessage.createdAt,
status: MessageBlockStatus.SUCCESS // Translation block is complete content
})
blocksToCreate.push(block)
messageBlockIds.push(block.id)
}
// 4. File Blocks (Non-Image) and Image Blocks (from Files) (Status is SUCCESS)
if (oldMessage.files?.length) {
oldMessage.files.forEach((file) => {
if (file.type === FileTypes.IMAGE) {
const block = createImageBlock(oldMessage.id, {
file: file,
createdAt: oldMessage.createdAt,
status: MessageBlockStatus.SUCCESS
})
blocksToCreate.push(block)
messageBlockIds.push(block.id)
} else {
const block = createFileBlock(oldMessage.id, file, {
createdAt: oldMessage.createdAt,
status: MessageBlockStatus.SUCCESS
})
blocksToCreate.push(block)
messageBlockIds.push(block.id)
}
})
}
// 5. Image Blocks (from Metadata - AI Generated) (Status is SUCCESS)
if (oldMessage.metadata?.generateImage) {
const block = createImageBlock(oldMessage.id, {
metadata: { generateImageResponse: oldMessage.metadata.generateImage },
createdAt: oldMessage.createdAt,
status: MessageBlockStatus.SUCCESS
})
blocksToCreate.push(block)
messageBlockIds.push(block.id)
}
// 6. Web Search Block - REMOVED, data moved to citation collection
// if (oldMessage.metadata?.webSearch?.results?.length) { ... }
// 8. Collect Citation and Reference Data (Simplified: Independent checks)
if (oldMessage.metadata?.groundingMetadata) {
hasCitationData = true
citationDataToCreate.response = {
results: oldMessage.metadata.groundingMetadata,
source: WebSearchSource.GEMINI
}
}
if (oldMessage.metadata?.annotations?.length) {
hasCitationData = true
citationDataToCreate.response = {
results: oldMessage.metadata.annotations,
source: WebSearchSource.OPENAI_RESPONSE
}
}
if (oldMessage.metadata?.citations?.length) {
hasCitationData = true
citationDataToCreate.response = {
results: oldMessage.metadata.citations,
// 无法区分,统一为Openrouter
source: WebSearchSource.OPENROUTER
}
}
if (oldMessage.metadata?.webSearch) {
hasCitationData = true
citationDataToCreate.response = {
results: oldMessage.metadata.webSearch,
source: WebSearchSource.WEBSEARCH
}
}
if (oldMessage.metadata?.webSearchInfo) {
hasCitationData = true
citationDataToCreate.response = {
results: oldMessage.metadata.webSearchInfo,
// 无法区分,统一为zhipu
source: WebSearchSource.ZHIPU
}
}
if (oldMessage.metadata?.knowledge?.length) {
hasCitationData = true
citationDataToCreate.knowledge = oldMessage.metadata.knowledge
}
// 9. Create Citation Block (if any citation data was found, no need to set citationType)
if (hasCitationData) {
const block = createCitationBlock(
oldMessage.id,
citationDataToCreate as Omit<CitationMessageBlock, keyof BaseMessageBlock | 'type'>,
{
createdAt: oldMessage.createdAt,
status: MessageBlockStatus.SUCCESS
}
)
blocksToCreate.push(block)
messageBlockIds.push(block.id)
}
// 10. Error Block (Status is ERROR)
if (oldMessage.error && typeof oldMessage.error === 'object' && Object.keys(oldMessage.error).length > 0) {
if (isEmpty(oldMessage.content)) {
const block = createErrorBlock(oldMessage.id, oldMessage.error, {
createdAt: oldMessage.createdAt,
status: MessageBlockStatus.ERROR // Error block status is ERROR
})
blocksToCreate.push(block)
messageBlockIds.push(block.id)
}
}
// 11. Create the New Message reference object (Add usage/metrics assignment)
const newMessageReference: NewMessage = {
id: oldMessage.id,
role: oldMessage.role as NewMessage['role'],
assistantId: oldMessage.assistantId || '',
topicId: oldTopic.id,
createdAt: oldMessage.createdAt,
status: mapOldStatusToNewMessageStatus(oldMessage.status),
modelId: oldMessage.modelId,
model: oldMessage.model,
type: oldMessage.type === 'clear' ? 'clear' : undefined,
useful: oldMessage.useful,
askId: oldMessage.askId,
mentions: oldMessage.mentions,
enabledMCPs: oldMessage.enabledMCPs,
usage: oldMessage.usage,
metrics: oldMessage.metrics,
multiModelMessageStyle: oldMessage.multiModelMessageStyle,
foldSelected: oldMessage.foldSelected,
blocks: messageBlockIds
}
newMessagesForTopic.push(newMessageReference)
}
if (blocksToCreate.length > 0) {
await newBlocksTable.bulkPut(blocksToCreate)
}
topicUpdates[oldTopic.id] = { messages: newMessagesForTopic }
})
const updateOperations = Object.entries(topicUpdates).map(([id, data]) => ({ key: id, changes: data }))
if (updateOperations.length > 0) {
await oldTopicsTable.bulkUpdate(updateOperations)
Logger.log(`Updated message references for ${updateOperations.length} topics.`)
}
Logger.log('DB migration to version 7 finished successfully.')
}