import { DiscussionRecord, DiscussionStats, DiscussionExportData, ExportOptions, ChatMessage, MessagePurpose } from './types'; export class DiscussionRecordManager { private static STORAGE_KEY = 'multi-mind-chat-discussion-records'; private static MAX_RECORDS = 50; // 最多保存50条记录 // 保存讨论记录到本地存储 static saveRecord(record: DiscussionRecord): void { try { const existingRecords = this.getAllRecords(); const updatedRecords = [record, ...existingRecords.filter(r => r.id !== record.id)]; // 保持最大记录数限制 const trimmedRecords = updatedRecords.slice(0, this.MAX_RECORDS); localStorage.setItem(this.STORAGE_KEY, JSON.stringify(trimmedRecords)); } catch (error) { console.error('保存讨论记录失败:', error); throw new Error('无法保存讨论记录到本地存储'); } } // 获取所有讨论记录 static getAllRecords(): DiscussionRecord[] { try { const stored = localStorage.getItem(this.STORAGE_KEY); if (!stored) return []; const records = JSON.parse(stored); return records.map((record: any) => ({ ...record, timestamp: new Date(record.timestamp), turns: record.turns.map((turn: any) => ({ ...turn, timestamp: new Date(turn.timestamp) })), notepadUpdates: record.notepadUpdates.map((update: any) => ({ ...update, timestamp: new Date(update.timestamp) })), finalAnswer: record.finalAnswer ? { ...record.finalAnswer, timestamp: new Date(record.finalAnswer.timestamp) } : undefined, interruptedAt: record.interruptedAt ? new Date(record.interruptedAt) : undefined, metadata: { ...record.metadata, exportedAt: record.metadata.exportedAt ? new Date(record.metadata.exportedAt) : undefined } })); } catch (error) { console.error('加载讨论记录失败:', error); return []; } } // 根据ID获取单个记录 static getRecordById(id: string): DiscussionRecord | null { const records = this.getAllRecords(); return records.find(record => record.id === id) || null; } // 删除指定记录 static deleteRecord(id: string): void { try { const records = this.getAllRecords(); const filteredRecords = records.filter(record => record.id !== id); localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filteredRecords)); } catch (error) { console.error('删除讨论记录失败:', error); throw new Error('无法删除讨论记录'); } } // 清空所有记录 static clearAllRecords(): void { try { localStorage.removeItem(this.STORAGE_KEY); } catch (error) { console.error('清空讨论记录失败:', error); throw new Error('无法清空讨论记录'); } } // 计算讨论统计信息 static calculateStats(record: DiscussionRecord): DiscussionStats { const turns = record.turns.filter(turn => turn.durationMs && turn.durationMs > 0); const responseTimes = turns.map(turn => turn.durationMs!); const roleParticipation: Record = {}; // 统计每个角色的参与情况 record.activeRoles.forEach(role => { const roleTurns = turns.filter(turn => turn.roleId === role.id); const roleResponseTimes = roleTurns.map(turn => turn.durationMs!); roleParticipation[role.name] = { turnCount: roleTurns.length, totalResponseTime: roleResponseTimes.reduce((sum, time) => sum + time, 0), averageResponseTime: roleResponseTimes.length > 0 ? roleResponseTimes.reduce((sum, time) => sum + time, 0) / roleResponseTimes.length : 0 }; }); return { totalTurns: turns.length, averageResponseTime: responseTimes.length > 0 ? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length : 0, longestResponseTime: responseTimes.length > 0 ? Math.max(...responseTimes) : 0, shortestResponseTime: responseTimes.length > 0 ? Math.min(...responseTimes) : 0, roleParticipation, notepadUpdateFrequency: record.notepadUpdates.length / Math.max(turns.length, 1) }; } // 生成完整的讨论文本记录 static generateTranscript(record: DiscussionRecord, includeMetadata: boolean = true): string { let transcript = ''; if (includeMetadata) { transcript += `=== Multi-Mind Chat 讨论记录 ===\n`; transcript += `讨论ID: ${record.id}\n`; transcript += `开始时间: ${record.timestamp.toLocaleString()}\n`; transcript += `讨论模式: ${record.discussionMode}\n`; transcript += `参与角色: ${record.activeRoles.map(r => r.name).join(', ')}\n`; transcript += `总耗时: ${(record.totalDuration / 1000).toFixed(2)}秒\n`; if (record.wasInterrupted) { transcript += `状态: 被用户中断 (${record.interruptedAt?.toLocaleString()})\n`; } else if (record.isCompleted) { transcript += `状态: 正常完成\n`; } transcript += `\n=== 用户查询 ===\n`; transcript += `${record.userQuery}\n\n`; } // 添加讨论过程 transcript += `=== 讨论过程 ===\n`; record.turns.forEach((turn, index) => { const timeStr = turn.timestamp.toLocaleTimeString(); const durationStr = turn.durationMs ? ` (${(turn.durationMs / 1000).toFixed(2)}s)` : ''; transcript += `[${timeStr}] ${turn.role}${durationStr}:\n${turn.message}\n\n`; }); // 添加记事本更新历史 if (record.notepadUpdates.length > 0) { transcript += `=== 记事本更新历史 ===\n`; record.notepadUpdates.forEach((update, index) => { const timeStr = update.timestamp.toLocaleTimeString(); transcript += `[${timeStr}] ${update.updater} 更新了记事本:\n${update.content}\n\n`; }); } // 添加最终答案 if (record.finalAnswer) { transcript += `=== 最终答案 ===\n`; const timeStr = record.finalAnswer.timestamp.toLocaleTimeString(); const durationStr = record.finalAnswer.durationMs ? ` (${(record.finalAnswer.durationMs / 1000).toFixed(2)}s)` : ''; transcript += `[${timeStr}] ${record.finalAnswer.provider}${durationStr}:\n${record.finalAnswer.content}\n\n`; } return transcript; } // 导出讨论记录为指定格式 static exportRecord(record: DiscussionRecord, options: ExportOptions): DiscussionExportData { const stats = options.includeStats ? this.calculateStats(record) : {} as DiscussionStats; const transcript = this.generateTranscript(record, options.includeMetadata); return { record: options.includeMetadata ? record : { ...record, metadata: { version: record.metadata.version, messageCount: 0, notepadUpdateCount: 0 } } as DiscussionRecord, stats, fullTranscript: transcript, exportFormat: options.format, exportedAt: new Date().toISOString(), version: '1.0' }; } // 生成Markdown格式的导出 static exportAsMarkdown(record: DiscussionRecord, options: ExportOptions): string { let markdown = `# Multi-Mind Chat 讨论记录\n\n`; if (options.includeMetadata) { markdown += `## 基本信息\n\n`; markdown += `- **讨论ID**: ${record.id}\n`; markdown += `- **开始时间**: ${record.timestamp.toLocaleString()}\n`; markdown += `- **讨论模式**: ${record.discussionMode}\n`; markdown += `- **参与角色**: ${record.activeRoles.map(r => r.name).join(', ')}\n`; markdown += `- **总耗时**: ${(record.totalDuration / 1000).toFixed(2)}秒\n`; if (record.wasInterrupted) { markdown += `- **状态**: ⚠️ 被用户中断 (${record.interruptedAt?.toLocaleString()})\n`; } else if (record.isCompleted) { markdown += `- **状态**: ✅ 正常完成\n`; } markdown += `\n`; } markdown += `## 用户查询\n\n`; markdown += `> ${record.userQuery}\n\n`; if (record.userImage) { markdown += `*用户还上传了图片: ${record.userImage.name} (${(record.userImage.size / 1024).toFixed(1)} KB)*\n\n`; } markdown += `## 讨论过程\n\n`; record.turns.forEach((turn, index) => { const timeStr = turn.timestamp.toLocaleTimeString(); const durationStr = turn.durationMs ? ` *(${(turn.durationMs / 1000).toFixed(2)}s)*` : ''; markdown += `### ${turn.role} - ${timeStr}${durationStr}\n\n`; markdown += `${turn.message}\n\n`; }); if (record.notepadUpdates.length > 0 && options.includeNotepadHistory) { markdown += `## 记事本更新历史\n\n`; record.notepadUpdates.forEach((update, index) => { const timeStr = update.timestamp.toLocaleTimeString(); markdown += `### ${update.updater} - ${timeStr}\n\n`; markdown += `\`\`\`\n${update.content}\n\`\`\`\n\n`; }); } if (record.finalAnswer) { markdown += `## 最终答案\n\n`; const timeStr = record.finalAnswer.timestamp.toLocaleTimeString(); const durationStr = record.finalAnswer.durationMs ? ` *(${(record.finalAnswer.durationMs / 1000).toFixed(2)}s)*` : ''; markdown += `### ${record.finalAnswer.provider} - ${timeStr}${durationStr}\n\n`; markdown += `${record.finalAnswer.content}\n\n`; } if (options.includeStats) { const stats = this.calculateStats(record); markdown += `## 讨论统计\n\n`; markdown += `- **总轮次**: ${stats.totalTurns}\n`; markdown += `- **平均响应时间**: ${(stats.averageResponseTime / 1000).toFixed(2)}秒\n`; markdown += `- **最长响应时间**: ${(stats.longestResponseTime / 1000).toFixed(2)}秒\n`; markdown += `- **最短响应时间**: ${(stats.shortestResponseTime / 1000).toFixed(2)}秒\n`; markdown += `- **记事本更新频率**: ${(stats.notepadUpdateFrequency * 100).toFixed(1)}%\n\n`; markdown += `### 角色参与度\n\n`; Object.entries(stats.roleParticipation).forEach(([role, data]) => { markdown += `- **${role}**: ${data.turnCount}轮, 平均${(data.averageResponseTime / 1000).toFixed(2)}秒\n`; }); } markdown += `\n---\n`; markdown += `*导出时间: ${new Date().toLocaleString()}*\n`; markdown += `*导出版本: Multi-Mind Chat v1.0*\n`; return markdown; } // 下载文件的工具函数 static downloadFile(content: string, filename: string, mimeType: string = 'text/plain'): void { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } // 导出并下载讨论记录 static downloadRecord(record: DiscussionRecord, format: 'json' | 'markdown' | 'txt' = 'json'): void { const timestamp = record.timestamp.toISOString().split('T')[0]; const safeQuery = record.userQuery.substring(0, 20).replace(/[^\w\s-]/g, '').trim(); switch (format) { case 'json': const exportData = this.exportRecord(record, { format: 'json', includeMetadata: true, includeStats: true, includeNotepadHistory: true, includeSystemMessages: false, timestampFormat: 'iso', compressOutput: false }); this.downloadFile( JSON.stringify(exportData, null, 2), `讨论记录-${timestamp}-${safeQuery}.json`, 'application/json' ); break; case 'markdown': const markdownContent = this.exportAsMarkdown(record, { format: 'markdown', includeMetadata: true, includeStats: true, includeNotepadHistory: true, includeSystemMessages: false, timestampFormat: 'local', compressOutput: false }); this.downloadFile( markdownContent, `讨论记录-${timestamp}-${safeQuery}.md`, 'text/markdown' ); break; case 'txt': const txtContent = this.generateTranscript(record, true); this.downloadFile( txtContent, `讨论记录-${timestamp}-${safeQuery}.txt`, 'text/plain' ); break; } } // 搜索讨论记录 static searchRecords(query: string, maxResults: number = 10): DiscussionRecord[] { const records = this.getAllRecords(); const searchLower = query.toLowerCase(); return records .filter(record => record.userQuery.toLowerCase().includes(searchLower) || record.turns.some(turn => turn.message.toLowerCase().includes(searchLower)) || record.finalAnswer?.content.toLowerCase().includes(searchLower) ) .slice(0, maxResults); } // 获取存储使用情况 static getStorageInfo(): { used: number; available: number; recordCount: number } { try { const records = this.getAllRecords(); const dataSize = JSON.stringify(records).length; return { used: dataSize, available: 5242880 - dataSize, // 假设5MB存储限制 recordCount: records.length }; } catch (error) { return { used: 0, available: 0, recordCount: 0 }; } } }