refactor(storage): 重构消息存储为 2-message 格式
采用 OpenCode 风格的消息存储架构: - 只有 user 和 assistant 两种角色,移除 tool/system - ToolPart 使用状态机模式 (pending → running → completed/error) - 新增 toModelMessages() 转换函数用于调用 AI SDK - 删除 message-merger.ts,存储层直接返回正确格式 主要改动: - parts.ts: ToolState 状态机(pending/running/completed/error) - message.ts: 移除 system role,添加 parentId 关联 - converter.ts: 新增 toModelMessages() 格式转换 - manager.ts: 重构 syncMessages/partsToModelMessages - sessions.ts: 简化路由,直接从 Core Storage 读取
This commit is contained in:
@@ -175,76 +175,118 @@ export class SessionManager {
|
||||
|
||||
for (const messageInfo of messageInfos) {
|
||||
const parts = await PartStorage.getByIds(messageInfo.id, messageInfo.partIds);
|
||||
const modelMessage = this.partsToModelMessage(messageInfo.role, parts);
|
||||
if (modelMessage) {
|
||||
messages.push(modelMessage);
|
||||
}
|
||||
const modelMessages = this.partsToModelMessages(messageInfo.role, parts);
|
||||
messages.push(...modelMessages);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Parts 转换为 AI SDK ModelMessage
|
||||
* 将 Parts 转换为 AI SDK ModelMessage(用于加载历史消息)
|
||||
*
|
||||
* 新逻辑:
|
||||
* - user 消息:直接转换
|
||||
* - assistant 消息:转换文本和工具调用,然后为已完成的工具生成 tool 消息
|
||||
*/
|
||||
private partsToModelMessage(role: string, parts: Part[]): ModelMessage | null {
|
||||
if (parts.length === 0) return null;
|
||||
private partsToModelMessages(role: string, parts: Part[]): ModelMessage[] {
|
||||
if (parts.length === 0) return [];
|
||||
|
||||
// 构建消息内容
|
||||
const content: unknown[] = [];
|
||||
const result: ModelMessage[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
if (role === 'user') {
|
||||
// User 消息:只有文本和文件
|
||||
const content: unknown[] = [];
|
||||
for (const part of parts) {
|
||||
if (part.type === 'text') {
|
||||
content.push({ type: 'text', text: part.text });
|
||||
break;
|
||||
case 'tool':
|
||||
if (role === 'assistant') {
|
||||
content.push({
|
||||
type: 'tool-call',
|
||||
toolCallId: part.toolCallId,
|
||||
toolName: part.toolName,
|
||||
args: part.args,
|
||||
});
|
||||
} else if (role === 'tool') {
|
||||
// Tool result message - AI SDK 的 tool message 格式
|
||||
return {
|
||||
role: 'tool',
|
||||
content: [{
|
||||
type: 'tool-result',
|
||||
toolCallId: part.toolCallId,
|
||||
toolName: part.toolName,
|
||||
result: part.result,
|
||||
}],
|
||||
} as unknown as ModelMessage;
|
||||
}
|
||||
break;
|
||||
case 'file':
|
||||
} else if (part.type === 'file') {
|
||||
content.push({
|
||||
type: 'image',
|
||||
image: part.data,
|
||||
mimeType: part.mimeType,
|
||||
});
|
||||
break;
|
||||
case 'reasoning':
|
||||
// Reasoning 通常作为文本的一部分
|
||||
}
|
||||
}
|
||||
|
||||
if (content.length === 1 && (content[0] as { type: string }).type === 'text') {
|
||||
result.push({
|
||||
role: 'user',
|
||||
content: (content[0] as { text: string }).text,
|
||||
});
|
||||
} else if (content.length > 0) {
|
||||
result.push({
|
||||
role: 'user',
|
||||
content,
|
||||
} as ModelMessage);
|
||||
}
|
||||
|
||||
} else if (role === 'assistant') {
|
||||
// Assistant 消息:文本 + 工具调用
|
||||
const content: unknown[] = [];
|
||||
const completedTools: Array<{ toolCallId: string; toolName: string; output: unknown }> = [];
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.type === 'text') {
|
||||
content.push({ type: 'text', text: part.text });
|
||||
} else if (part.type === 'tool') {
|
||||
// 只有非 pending 状态的工具调用才添加到 AI SDK 消息
|
||||
if (part.state.status !== 'pending') {
|
||||
content.push({
|
||||
type: 'tool-call',
|
||||
toolCallId: part.toolCallId,
|
||||
toolName: part.toolName,
|
||||
args: part.state.input,
|
||||
});
|
||||
|
||||
// 收集已完成的工具结果
|
||||
if (part.state.status === 'completed') {
|
||||
completedTools.push({
|
||||
toolCallId: part.toolCallId,
|
||||
toolName: part.toolName,
|
||||
output: part.state.output,
|
||||
});
|
||||
} else if (part.state.status === 'error') {
|
||||
completedTools.push({
|
||||
toolCallId: part.toolCallId,
|
||||
toolName: part.toolName,
|
||||
output: part.state.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (part.type === 'reasoning') {
|
||||
content.push({ type: 'text', text: `[Reasoning] ${part.text}` });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 assistant 消息
|
||||
if (content.length === 1 && (content[0] as { type: string }).type === 'text') {
|
||||
result.push({
|
||||
role: 'assistant',
|
||||
content: (content[0] as { text: string }).text,
|
||||
});
|
||||
} else if (content.length > 0) {
|
||||
result.push({
|
||||
role: 'assistant',
|
||||
content,
|
||||
} as ModelMessage);
|
||||
}
|
||||
|
||||
// 添加 tool 消息(如果有已完成的工具)
|
||||
if (completedTools.length > 0) {
|
||||
result.push({
|
||||
role: 'tool',
|
||||
content: completedTools.map((t) => ({
|
||||
type: 'tool-result',
|
||||
toolCallId: t.toolCallId,
|
||||
toolName: t.toolName,
|
||||
result: t.output,
|
||||
})),
|
||||
} as unknown as ModelMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// 简化:如果只有一个文本内容,直接使用字符串
|
||||
if (content.length === 1 && (content[0] as { type: string }).type === 'text') {
|
||||
return {
|
||||
role: role as 'user' | 'assistant' | 'system',
|
||||
content: (content[0] as { text: string }).text,
|
||||
} as ModelMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
role: role as 'user' | 'assistant' | 'system' | 'tool',
|
||||
content,
|
||||
} as ModelMessage;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,6 +339,11 @@ export class SessionManager {
|
||||
|
||||
/**
|
||||
* 同步消息到存储(将 AI SDK 消息转换为 Message + Parts)
|
||||
*
|
||||
* 新逻辑:只存储 user 和 assistant 消息
|
||||
* - user 消息:直接存储
|
||||
* - assistant 消息:合并后续的 tool 消息中的工具结果
|
||||
* - tool 消息:跳过(结果合并到 assistant)
|
||||
*/
|
||||
async syncMessages(messages: ModelMessage[]): Promise<void> {
|
||||
if (!this.currentSession) return;
|
||||
@@ -306,59 +353,108 @@ export class SessionManager {
|
||||
// 删除旧消息
|
||||
await MessageStorage.removeBySession(sessionId);
|
||||
|
||||
// 保存新消息
|
||||
for (const message of messages) {
|
||||
const messageInfo = await MessageStorage.create(sessionId, message.role as 'user' | 'assistant' | 'system');
|
||||
// 用于跟踪当前 assistant 消息的工具调用
|
||||
let currentAssistantMsgId: string | null = null;
|
||||
let currentUserMsgId: string | null = null;
|
||||
const toolCallPartIds = new Map<string, string>(); // toolCallId -> partId
|
||||
|
||||
// 将消息内容转换为 Parts
|
||||
const partIds: string[] = [];
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i];
|
||||
|
||||
if (typeof message.content === 'string') {
|
||||
// 简单文本
|
||||
const part = await PartStorage.createText(messageInfo.id, message.content);
|
||||
partIds.push(part.id);
|
||||
} else if (Array.isArray(message.content)) {
|
||||
// 复杂内容(多个 parts)
|
||||
for (const item of message.content) {
|
||||
const itemType = (item as { type: string }).type;
|
||||
if (itemType === 'text') {
|
||||
const part = await PartStorage.createText(messageInfo.id, (item as { text: string }).text);
|
||||
partIds.push(part.id);
|
||||
} else if (itemType === 'tool-call') {
|
||||
const toolCall = item as unknown as { toolCallId: string; toolName: string; args: Record<string, unknown> };
|
||||
const part = await PartStorage.createTool(
|
||||
messageInfo.id,
|
||||
toolCall.toolCallId,
|
||||
toolCall.toolName,
|
||||
toolCall.args
|
||||
);
|
||||
partIds.push(part.id);
|
||||
} else if (itemType === 'tool-result') {
|
||||
const toolResult = item as unknown as { toolCallId: string; toolName: string; result: unknown };
|
||||
const part = await PartStorage.create(messageInfo.id, 'tool', {
|
||||
toolCallId: toolResult.toolCallId,
|
||||
toolName: toolResult.toolName,
|
||||
args: {},
|
||||
status: 'completed',
|
||||
result: toolResult.result,
|
||||
});
|
||||
partIds.push(part.id);
|
||||
} else if (itemType === 'image') {
|
||||
const img = item as unknown as { image: string; mimeType: string };
|
||||
const part = await PartStorage.create(messageInfo.id, 'file', {
|
||||
filename: 'image',
|
||||
mimeType: img.mimeType,
|
||||
data: typeof img.image === 'string' ? img.image : '',
|
||||
});
|
||||
partIds.push(part.id);
|
||||
if (message.role === 'user') {
|
||||
// User 消息
|
||||
const messageInfo = await MessageStorage.create(sessionId, 'user');
|
||||
currentUserMsgId = messageInfo.id;
|
||||
const partIds: string[] = [];
|
||||
|
||||
if (typeof message.content === 'string') {
|
||||
const part = await PartStorage.createText(messageInfo.id, message.content);
|
||||
partIds.push(part.id);
|
||||
} else if (Array.isArray(message.content)) {
|
||||
for (const item of message.content) {
|
||||
const itemType = (item as { type: string }).type;
|
||||
if (itemType === 'text') {
|
||||
const part = await PartStorage.createText(messageInfo.id, (item as { text: string }).text);
|
||||
partIds.push(part.id);
|
||||
} else if (itemType === 'image') {
|
||||
const img = item as unknown as { image: string; mimeType: string };
|
||||
const part = await PartStorage.create(messageInfo.id, 'file', {
|
||||
filename: 'image',
|
||||
mimeType: img.mimeType,
|
||||
data: typeof img.image === 'string' ? img.image : '',
|
||||
});
|
||||
partIds.push(part.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新消息的 partIds
|
||||
if (partIds.length > 0) {
|
||||
await MessageStorage.update(sessionId, messageInfo.id, { partIds });
|
||||
if (partIds.length > 0) {
|
||||
await MessageStorage.update(sessionId, messageInfo.id, { partIds });
|
||||
}
|
||||
|
||||
// 重置工具调用追踪
|
||||
currentAssistantMsgId = null;
|
||||
toolCallPartIds.clear();
|
||||
|
||||
} else if (message.role === 'assistant') {
|
||||
// Assistant 消息
|
||||
const messageInfo = await MessageStorage.create(sessionId, 'assistant', {
|
||||
parentId: currentUserMsgId ?? undefined,
|
||||
});
|
||||
currentAssistantMsgId = messageInfo.id;
|
||||
const partIds: string[] = [];
|
||||
|
||||
if (typeof message.content === 'string') {
|
||||
const part = await PartStorage.createText(messageInfo.id, message.content);
|
||||
partIds.push(part.id);
|
||||
} else if (Array.isArray(message.content)) {
|
||||
for (const item of message.content) {
|
||||
const itemType = (item as { type: string }).type;
|
||||
if (itemType === 'text') {
|
||||
const part = await PartStorage.createText(messageInfo.id, (item as { text: string }).text);
|
||||
partIds.push(part.id);
|
||||
} else if (itemType === 'tool-call') {
|
||||
const toolCall = item as unknown as { toolCallId: string; toolName: string; args: Record<string, unknown> };
|
||||
// 创建 running 状态的工具 Part
|
||||
const part = await PartStorage.createToolRunning(
|
||||
messageInfo.id,
|
||||
toolCall.toolCallId,
|
||||
toolCall.toolName,
|
||||
toolCall.args ?? {}
|
||||
);
|
||||
partIds.push(part.id);
|
||||
toolCallPartIds.set(toolCall.toolCallId, part.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (partIds.length > 0) {
|
||||
await MessageStorage.update(sessionId, messageInfo.id, { partIds });
|
||||
}
|
||||
|
||||
} else if (message.role === 'tool' && currentAssistantMsgId) {
|
||||
// Tool 消息:更新对应 assistant 消息中的工具 Part 状态
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const item of message.content) {
|
||||
const itemType = (item as { type: string }).type;
|
||||
if (itemType === 'tool-result') {
|
||||
const toolResult = item as unknown as { toolCallId: string; toolName: string; result: unknown };
|
||||
const partId = toolCallPartIds.get(toolResult.toolCallId);
|
||||
if (partId) {
|
||||
// 更新工具状态为 completed
|
||||
// 获取原始 start time
|
||||
const part = await PartStorage.get(currentAssistantMsgId, partId);
|
||||
const startTime = part?.type === 'tool' && part.state.status === 'running'
|
||||
? part.state.time.start
|
||||
: Date.now();
|
||||
await PartStorage.setToolCompleted(currentAssistantMsgId, partId, toolResult.result, startTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 不创建新消息,跳过 tool role
|
||||
}
|
||||
// 忽略 system 消息(system prompt 通过其他方式注入)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user