import { randomUUID } from 'crypto'; import { getDb } from '../db'; import type { Message, AssistantMessage, TextContent, ToolCall } from '@earendil-works/pi-ai'; export interface DbMessage { id: string; conversation_id: string; role: string; content: string; tool_calls: string | null; created_at: string; } export function dbToPiMessages(dbMessages: DbMessage[]): Message[] { const sanitized = sanitizeHistory(dbMessages); return sanitized.map(dbToPiMessage); } function dbToPiMessage(msg: DbMessage): Message { if (msg.role === 'user') { return { role: 'user', content: msg.content, timestamp: Date.parse(msg.created_at) || Date.now() }; } if (msg.role === 'assistant') { const content: (TextContent | ToolCall)[] = []; const textContent = msg.content && msg.content !== '(调用工具)' ? msg.content : ''; if (textContent) { content.push({ type: 'text', text: textContent }); } if (msg.tool_calls) { try { const parsed = JSON.parse(msg.tool_calls); const blocks = Array.isArray(parsed) ? parsed : parsed.content_blocks; if (Array.isArray(blocks)) { for (const b of blocks) { if (b.type === 'tool_use') { content.push({ type: 'toolCall', id: b.id, name: b.name, arguments: b.input || b.arguments || {}, }); } } } } catch {} } return { role: 'assistant', content, api: 'unknown' as any, provider: 'unknown', model: 'unknown', usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, stopReason: 'stop', timestamp: Date.parse(msg.created_at) || Date.now(), }; } if (msg.role === 'tool') { try { const { tool_use_id, content: resultContent } = JSON.parse(msg.content); return { role: 'toolResult', toolCallId: tool_use_id, toolName: '', content: [{ type: 'text', text: resultContent }], isError: false, timestamp: Date.parse(msg.created_at) || Date.now(), }; } catch { return { role: 'toolResult', toolCallId: 'unknown', toolName: '', content: [{ type: 'text', text: msg.content }], isError: true, timestamp: Date.parse(msg.created_at) || Date.now(), }; } } return { role: 'user', content: msg.content, timestamp: Date.now() }; } function sanitizeHistory(messages: DbMessage[]): DbMessage[] { const cleaned = messages.filter((m) => { if (m.role === 'assistant' && m.content.startsWith('抱歉,出错了:')) return false; return true; }); const result: DbMessage[] = []; for (const msg of cleaned) { if (msg.role === 'tool') { let hasPrecedingToolCall = false; for (let j = result.length - 1; j >= 0; j--) { const prev = result[j]; if (prev.role === 'assistant') { if (prev.tool_calls) { try { const parsed = JSON.parse(prev.tool_calls); const blocks = Array.isArray(parsed) ? parsed : parsed.content_blocks; if (blocks?.some((b: any) => b.type === 'tool_use')) { hasPrecedingToolCall = true; } } catch {} } break; } if (prev.role === 'tool') continue; break; } if (!hasPrecedingToolCall) continue; } result.push(msg); } return result; } export function saveUserMessage(convId: string, content: string): string { const id = randomUUID(); getDb().prepare( 'INSERT INTO messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)' ).run(id, convId, 'user', content); return id; } export function saveAssistantMessage(convId: string, msg: AssistantMessage): string { const id = randomUUID(); const textParts = msg.content.filter((c): c is TextContent => c.type === 'text'); const text = textParts.map((c) => c.text).join(''); const toolCalls = msg.content.filter((c): c is ToolCall => c.type === 'toolCall'); if (toolCalls.length > 0) { const dbToolCalls = toolCalls.map((tc) => ({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.arguments, })); const contentBlocks = text ? [{ type: 'text', text }, ...dbToolCalls] : dbToolCalls; getDb().prepare( 'INSERT INTO messages (id, conversation_id, role, content, tool_calls) VALUES (?, ?, ?, ?, ?)' ).run(id, convId, 'assistant', text || '(调用工具)', JSON.stringify(contentBlocks)); } else { getDb().prepare( 'INSERT INTO messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)' ).run(id, convId, 'assistant', text); } return id; } export function saveToolResult(convId: string, toolCallId: string, toolName: string, result: string, isError: boolean): string { const id = randomUUID(); getDb().prepare( 'INSERT INTO messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)' ).run(id, convId, 'tool', JSON.stringify({ tool_use_id: toolCallId, tool_name: toolName, content: result })); return id; }