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:
2025-12-15 13:35:32 +08:00
parent eda2ccb171
commit 9f456c1029
13 changed files with 635 additions and 513 deletions
+194 -98
View File
@@ -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 通过其他方式注入)
}
}