Files
upage-git/app/lib/.server/message.ts
2025-09-24 17:02:44 +08:00

318 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Message } from '@prisma/client';
import type { JsonArray } from '@prisma/client/runtime/library';
import type { TextUIPart, UIMessagePart } from 'ai';
import { createScopedLogger } from '~/lib/.server/logger';
import { prisma } from '~/lib/.server/prisma';
import type { SummaryAnnotation, UPageDataParts, UPageUIMessage } from '~/types/message';
const logger = createScopedLogger('message.server');
/**
* 消息创建参数接口
*/
export interface MessageCreateParams {
chatId: string;
userId: string;
role: string;
content: string;
revisionId?: string;
annotations?: any[];
version?: number;
}
/**
* 消息更新参数接口
*/
export interface MessageUpdateParams {
content?: string;
revisionId?: string;
annotations?: any[];
version?: number;
}
/**
* 消息创建或更新参数接口
*/
export interface MessageUpsertParams {
id: string;
chatId: string;
userId: string;
role: string;
content: string;
revisionId?: string;
annotations?: any[];
version?: number;
}
/**
* 根据ID创建或更新消息upsert操作
* @param params 消息创建或更新参数
* @returns 创建或更新后的消息记录
*/
export async function upsertMessage(params: MessageUpsertParams) {
const { id, chatId, userId, role, content, revisionId, annotations } = params;
try {
const message = await prisma.message.upsert({
where: { id },
update: {
content,
revisionId,
annotations,
},
create: {
id,
chatId,
userId,
role,
content,
revisionId,
annotations,
},
});
logger.info(`[Message] 创建或更新了消息 ${id}`);
return message;
} catch (error) {
logger.error(`[Message] 创建或更新消息 ${id} 失败:`, error);
throw error;
}
}
/**
* 更新消息为遗弃消息。
*
* 此方法将会更新同一 {@param chatId} 下 startMessageId不含与 endMessageId 之间(不含)的所有消息为遗弃消息。
*
* @param chatId 聊天ID
* @param startMessageId 开始消息ID
* @param endMessageId 结束消息ID
*/
export async function updateDiscardedMessage(chatId: string, startMessageId: string) {
try {
const startMessage = await prisma.message.findUnique({
where: { id: startMessageId },
select: { createdAt: true },
});
if (!startMessage) {
logger.error(`[Message] 找不到开始消息 ${startMessageId}`);
return false;
}
// 更新 startMessageId 之后的所有消息为遗弃消息
const result = await prisma.message.updateMany({
where: {
chatId,
createdAt: {
gt: startMessage?.createdAt,
},
},
data: {
isDiscarded: true,
},
});
logger.info(`[Message] 已将聊天 ${chatId}${startMessageId} 之后的 ${result.count} 条消息标记为遗弃`);
return true;
} catch (error) {
logger.error(`[Message] 更新遗弃消息失败:`, error);
throw error;
}
}
/**
* 获取历史聊天消息接口参数
*/
export interface GetHistoryChatMessagesParams {
chatId: string;
rewindTo?: string;
}
/**
* 获取从第一条消息到指定消息之间的所有历史消息
* @param params 包含 chatId 和可选的 rewindTo 参数
* @returns 消息记录列表
*/
export async function getHistoryChatMessages(params: GetHistoryChatMessagesParams): Promise<UPageUIMessage[]> {
const { chatId, rewindTo } = params;
try {
// 如果指定了 rewindTo则获取该消息的创建时间
if (rewindTo) {
const rewindToMessage = await prisma.message.findUnique({
where: { id: rewindTo },
select: { createdAt: true },
});
if (!rewindToMessage) {
logger.warn(`[Message] 获取历史消息: 找不到指定的 rewindTo 消息 ${rewindTo}`);
// 如果找不到指定消息,则返回所有消息
return await getAllChatMessages(chatId);
}
// 获取所有在 rewindTo 消息创建时间之前(包括该消息)的消息
const messages = await prisma.message.findMany({
where: {
chatId,
isDiscarded: false,
createdAt: {
lte: rewindToMessage.createdAt,
},
},
orderBy: {
createdAt: 'asc',
},
});
logger.info(`[Message] 获取了聊天 ${chatId} 中直到消息 ${rewindTo}${messages.length} 条历史消息`);
return messages.map(convertToUIMessage);
} else {
// 如果没有指定 rewindTo则获取所有消息
return await getAllChatMessages(chatId);
}
} catch (error) {
logger.error(`[Message] 获取聊天 ${chatId} 的历史消息失败:`, error);
throw error;
}
}
function convertToUIMessage(message: Message): UPageUIMessage {
if (message.version === 2) {
return {
id: message.id,
role: message.role as 'user' | 'assistant',
parts: message.parts as any[],
metadata: message.metadata as any,
};
}
const parts: UIMessagePart<UPageDataParts, never>[] = [];
if (message.role === 'user') {
const content = JSON.parse(message.content) as TextUIPart;
parts.push({
type: 'text',
text: content.text,
});
} else {
parts.push({
type: 'text',
text: message.content,
});
}
if (message.annotations) {
const messageAnnotations = message.annotations as JsonArray;
messageAnnotations.forEach((annotation) => {
const { type } = annotation as { type: string };
if (type === 'chatSummary') {
parts.push({
type: 'data-summary',
data: annotation as unknown as SummaryAnnotation,
});
}
});
}
return {
id: message.id,
role: message.role as 'user' | 'assistant',
parts,
metadata: message.metadata as any,
};
}
/**
* 获取聊天的所有消息(内部辅助方法)
* @param chatId 聊天ID
* @returns 消息记录列表
*/
async function getAllChatMessages(chatId: string): Promise<UPageUIMessage[]> {
const messages = await prisma.message.findMany({
where: {
chatId,
isDiscarded: false,
},
orderBy: {
createdAt: 'asc',
},
});
logger.info(`[Message] 获取了聊天 ${chatId} 的所有 ${messages.length} 条历史消息`);
return messages.map(convertToUIMessage);
}
/**
* 保存聊天消息列表到数据库
* @param chatId 聊天ID
* @param messages 消息列表UPageUIMessage[]
* @returns 保存结果
*/
export async function saveChatMessages(chatId: string, messages: UPageUIMessage[]): Promise<number> {
if (!messages || messages.length === 0) {
logger.warn('[Message] 保存聊天消息: 没有提供消息数据');
return 0;
}
try {
// 获取聊天的用户ID
const chat = await prisma.chat.findUnique({
where: { id: chatId },
select: { userId: true },
});
if (!chat) {
logger.error(`[Message] 保存聊天消息: 找不到聊天 ${chatId}`);
throw new Error(`找不到聊天 ${chatId}`);
}
const userId = chat.userId;
let savedCount = 0;
// 逐条保存消息
for (const message of messages) {
// 跳过没有ID的消息
if (!message.id) {
logger.warn('[Message] 保存聊天消息: 跳过没有ID的消息');
continue;
}
// 提取消息的文本内容
const textPart = message.parts.find((part) => part.type === 'text');
const content = textPart?.text || '';
// 创建或更新消息
const updateData: any = {
content,
parts: message.parts,
metadata: message.metadata,
version: 2,
};
const createData: any = {
id: message.id,
chatId,
userId,
role: message.role,
content,
parts: message.parts,
metadata: message.metadata,
version: 2,
};
await prisma.message.upsert({
where: { id: message.id },
update: updateData,
create: createData,
});
savedCount++;
}
logger.info(`[Message] 成功保存了聊天 ${chatId}${savedCount} 条消息`);
return savedCount;
} catch (error) {
logger.error(`[Message] 保存聊天消息失败:`, error);
throw error;
}
}