import type { AgentConversation, AgentConversationMessage, AgentConversationSummary, SceneStore, } from '@pascal-app/mcp/storage' import type { UIMessage } from 'ai' import { getSceneStore } from './scene-store-server' const MAX_MESSAGES = 80 const MAX_TEXT_LENGTH = 20_000 const DEFAULT_TITLE = '新对话' type ConversationStore = Required< Pick< SceneStore, | 'createAgentConversation' | 'listAgentConversations' | 'loadAgentConversation' | 'saveAgentConversation' | 'deleteAgentConversation' > > type UnknownPart = { data?: unknown errorText?: unknown state?: unknown text?: unknown toolName?: unknown type?: unknown } function isConversationStore(store: SceneStore): store is SceneStore & ConversationStore { return ( typeof store.createAgentConversation === 'function' && typeof store.listAgentConversations === 'function' && typeof store.loadAgentConversation === 'function' && typeof store.saveAgentConversation === 'function' && typeof store.deleteAgentConversation === 'function' ) } export async function getAgentConversationStore(): Promise { const store = await getSceneStore() if (!isConversationStore(store)) { throw new Error('Agent conversation storage is unavailable') } return store } function toolNameFromPart(part: UnknownPart): string | null { if (typeof part.toolName === 'string' && part.toolName) return part.toolName if (typeof part.type === 'string' && part.type.startsWith('tool-')) return part.type.slice(5) return null } function sanitizePart(part: unknown): unknown | null { if (!part || typeof part !== 'object') return null const value = part as UnknownPart if (value.type === 'text' && typeof value.text === 'string') { return { type: 'text', text: value.text.slice(0, MAX_TEXT_LENGTH) } } if (value.type === 'data-pascal-tool-activity' && value.data && typeof value.data === 'object') { const data = value.data as UnknownPart const toolName = typeof data.toolName === 'string' ? data.toolName : null if (!toolName) return null return { type: 'data-pascal-tool-activity', data: { toolName, state: typeof data.state === 'string' ? data.state : 'output-available', ...(typeof data.errorText === 'string' ? { errorText: data.errorText.slice(0, 500) } : {}), }, } } const toolName = toolNameFromPart(value) if (!toolName) return null return { type: 'data-pascal-tool-activity', data: { toolName, state: typeof value.state === 'string' ? value.state : 'output-available', ...(typeof value.errorText === 'string' ? { errorText: value.errorText.slice(0, 500) } : {}), }, } } export function sanitizeAgentMessages(messages: UIMessage[]): AgentConversationMessage[] { const sanitized: AgentConversationMessage[] = [] for (const [messageIndex, message] of messages.slice(-MAX_MESSAGES).entries()) { if (!['system', 'user', 'assistant'].includes(message.role)) continue const parts: unknown[] = message.parts.map(sanitizePart).filter((part) => part !== null) if (parts.length === 0) continue sanitized.push({ id: typeof message.id === 'string' && message.id.length > 0 ? message.id.slice(0, 128) : `message-${messageIndex}`, role: message.role, parts, }) } return sanitized } export function conversationTitle(messages: UIMessage[]): string { const userMessage = messages.find((message) => message.role === 'user') const textPart = userMessage?.parts.find((part) => part.type === 'text') as | { text?: string } | undefined const title = textPart?.text?.trim().replace(/\s+/g, ' ') return title ? title.slice(0, 32) : DEFAULT_TITLE } export type { AgentConversation, AgentConversationSummary }