Skip to main content
Glama

AI Conversation Logger

by fablefang
conversationLogger.ts13.2 kB
import type { ConversationEntry, ProjectStats } from '../types/index.js'; import { FileManager } from '../storage/fileManager.js'; import { MarkdownFormatter } from '../storage/markdownFormatter.js'; import { randomUUID } from 'crypto'; import { join } from 'path'; import { validateLogConversation, validateSearchConversations, validateGetContextSuggestions, validateListProjects, type SearchConversationsParams } from '../validation/schemas.js'; import { CONSTANTS, ValidationError, FileOperationError, SearchError } from '../constants/index.js'; export class ConversationLogger { private readonly fileManager: FileManager; constructor() { this.fileManager = new FileManager(); } async logConversation(params: unknown): Promise<{ content: Array<{ type: string; text: string }> }> { try { const validatedParams = validateLogConversation(params); const { userRequest, aiTodoList, aiSummary, fileOperations, title, tags, platform } = validatedParams; // Auto-detect project from current working directory const projectInfo = await this.fileManager.getProjectInfo(); const projectName = projectInfo.name; await this.fileManager.initializeProject(); const now = new Date(); // Use zero-padded date format for better sorting (2025-08-07 instead of 2025-8-7) const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const dateStr = `${year}-${month}-${day}`; const conversationEntry: ConversationEntry = { id: randomUUID(), timestamp: now.toISOString(), project: projectName, platform: platform || CONSTANTS.PLATFORMS.UNKNOWN, userRequest: userRequest.trim(), aiTodoList: aiTodoList || [], aiSummary: aiSummary.trim(), fileOperations: fileOperations || [], title: title?.trim() || undefined, tags: tags || [] }; const dailyLogPath = await this.fileManager.getDailyLogPath(projectName, dateStr); let existingContent = await this.fileManager.readFile(dailyLogPath); // Calculate conversation number const existingConversationCount = (existingContent.match(/## 对话/g) || []).length; const conversationNumber = existingConversationCount + 1; // Generate the conversation markdown const conversationMarkdown = MarkdownFormatter.formatConversationEntry(conversationEntry, conversationNumber); if (!existingContent) { // New file: create header + new conversation const header = MarkdownFormatter.formatDailyLogHeader(projectName, dateStr, projectInfo.root); const finalContent = header + conversationMarkdown; await this.fileManager.writeFile(dailyLogPath, finalContent); } else { // Existing file: just append new conversation const finalContent = existingContent + conversationMarkdown; await this.fileManager.writeFile(dailyLogPath, finalContent); } return { content: [ { type: 'text', text: `✅ 对话已成功记录到项目 "${projectName}"!\n\n` + `📁 文件位置: ${dailyLogPath}\n` + `💻 项目目录: ${projectInfo.root || '未检测到项目'}\n` + `🕒 时间: ${now.toLocaleString('zh-CN')}\n` + `🏷️ 标签: ${(tags && tags.length > 0) ? tags.join(', ') : '无'}\n` + `⚡ 执行任务: ${aiTodoList?.length || 0}\n` + `📂 文件操作: ${fileOperations?.length || 0}\n` + `📝 对话编号: 对话${conversationNumber}` } ] }; } catch (error) { const errorMessage = error instanceof ValidationError ? `参数验证失败: ${error.message}` : error instanceof FileOperationError ? `文件操作失败: ${error.message} (文件: ${error.filePath})` : `记录对话时出错: ${String(error)}`; return { content: [ { type: 'text', text: `❌ ${errorMessage}` } ] }; } } async searchConversations(params: unknown): Promise<{ content: Array<{ type: string; text: string }> }> { try { const validatedParams = validateSearchConversations(params); const { project } = validatedParams; let searchResults = ''; let totalResults = 0; if (project) { const projectResults = await this.searchInProject(project, validatedParams); searchResults += projectResults.content; totalResults += projectResults.count; } else { const projects = await this.fileManager.listProjects(); for (const proj of projects) { if (totalResults >= validatedParams.limit) break; const projectResults = await this.searchInProject(proj, { ...validatedParams, limit: validatedParams.limit - totalResults }); if (projectResults.content) { searchResults += `\n## 项目: ${proj}\n${projectResults.content}`; totalResults += projectResults.count; } } } if (totalResults === 0) { return { content: [ { type: 'text', text: '🔍 未找到匹配的对话记录。\n\n请尝试调整搜索条件:\n- 检查项目名称是否正确\n- 尝试更广泛的关键词\n- 扩大时间范围' } ] }; } return { content: [ { type: 'text', text: `🔍 找到 ${totalResults} 条匹配的对话记录:\n\n${searchResults}` } ] }; } catch (error) { const errorMessage = error instanceof ValidationError ? `参数验证失败: ${error.message}` : error instanceof SearchError ? `搜索失败: ${error.message} (查询: ${error.query})` : `搜索对话时出错: ${String(error)}`; return { content: [ { type: 'text', text: `❌ ${errorMessage}` } ] }; } } async getContextSuggestions(params: unknown): Promise<{ content: Array<{ type: string; text: string }> }> { try { const validatedParams = validateGetContextSuggestions(params); const { currentInput, currentFiles, project } = validatedParams; // 获取项目信息 const projectInfo = await this.fileManager.getProjectInfo(); const searchProject = project || projectInfo.name; // 构建搜索关键词:从输入中提取关键词 const keywords = currentInput.split(/\s+/).filter(word => word.length > 2); // 搜索相关对话 const searchResults = await this.searchInProject(searchProject, { keywords, limit: 5, filePattern: currentFiles && currentFiles.length > 0 ? currentFiles[0] : undefined, tags: [], platform: undefined, days: 30 }); if (searchResults.count === 0) { return { content: [ { type: 'text', text: '💡 暂无相关历史记录建议' } ] }; } return { content: [ { type: 'text', text: `💡 找到 ${searchResults.count} 条相关历史记录:\n\n${searchResults.content}` } ] }; } catch (error) { const errorMessage = error instanceof ValidationError ? `参数验证失败: ${error.message}` : `获取上下文建议时出错: ${String(error)}`; return { content: [ { type: 'text', text: `❌ ${errorMessage}` } ] }; } } async listProjects(params: unknown): Promise<{ content: Array<{ type: string; text: string }> }> { try { const validatedParams = validateListProjects(params); const { includeStats } = validatedParams; const projects = await this.fileManager.listProjects(); if (projects.length === 0) { return { content: [ { type: 'text', text: '📁 暂无项目记录。\n\n使用 log_conversation 工具开始记录对话到新项目。' } ] }; } let result = `📁 共找到 ${projects.length} 个项目:\n\n`; if (includeStats) { for (const project of projects) { const stats = await this.getProjectStats(project); result += `## ${stats.name}\n`; result += `- 总对话数: ${stats.totalConversations}\n`; result += `- 最后活动: ${stats.lastActivity}\n`; result += `- 支持平台: ${stats.platforms.join(', ') || '无'}\n`; result += `- 常用标签: ${stats.tags.slice(0, 5).join(', ') || '无'}\n\n`; } } else { projects.forEach(project => { result += `- ${project}\n`; }); } return { content: [ { type: 'text', text: result } ] }; } catch (error) { const errorMessage = error instanceof ValidationError ? `参数验证失败: ${error.message}` : `获取项目列表时出错: ${String(error)}`; return { content: [ { type: 'text', text: `❌ ${errorMessage}` } ] }; } } private async searchInProject(project: string, params: SearchConversationsParams): Promise<{ content: string; count: number }> { // Search in the ai-logs directory structure (no daily-logs subdirectory) const projectDir = await this.fileManager.getProjectDir(project); try { const { promises: fs } = await import('fs'); const files = await fs.readdir(projectDir); let content = ''; let count = 0; for (const file of files.slice(0, params.limit)) { if (file.endsWith('.md')) { const filePath = join(projectDir, file); const fileContent = await this.fileManager.readFile(filePath); if (this.matchesSearchCriteria(fileContent, params)) { content += `### ${file.replace('.md', '')}\n`; content += this.extractRelevantSections(fileContent, params); content += '\n'; count++; } } } return { content, count }; } catch (error) { throw new SearchError(`搜索项目 ${project} 失败`, String(error)); } } private matchesSearchCriteria(content: string, params: SearchConversationsParams): boolean { const { keywords, platform, tags, filePattern } = params; if (keywords && keywords.length > 0) { const hasKeyword = keywords.some(keyword => content.toLowerCase().includes(keyword.toLowerCase()) ); if (!hasKeyword) return false; } // 支持文件名模式搜索 if (filePattern) { const pattern = filePattern.toLowerCase(); if (!content.toLowerCase().includes(pattern)) { return false; } } if (platform && !content.includes(platform)) { return false; } if (tags && tags.length > 0) { const hasTag = tags.some(tag => content.toLowerCase().includes(tag.toLowerCase()) ); if (!hasTag) return false; } return true; } private extractRelevantSections(content: string, params: SearchConversationsParams): string { const lines = content.split('\n'); const relevantLines: string[] = []; for (let i = 0; i < lines.length && relevantLines.length < CONSTANTS.DEFAULT_SEARCH_LIMIT; i++) { const line = lines[i]; if (line && (line.startsWith('## [') || line.startsWith('### '))) { relevantLines.push(line); } else if (line && params.keywords?.some(keyword => line.toLowerCase().includes(keyword.toLowerCase()) )) { relevantLines.push(line); } } return relevantLines.join('\n'); } private async getProjectStats(project: string): Promise<ProjectStats> { const projectDir = await this.fileManager.getProjectDir(project); const stats: ProjectStats = { name: project, totalConversations: 0, lastActivity: '未知', platforms: [], tags: [] }; try { const { promises: fs } = await import('fs'); const files = await fs.readdir(projectDir); for (const file of files) { if (file.endsWith('.md')) { const content = await this.fileManager.readFile(join(projectDir, file)); const matches = content.match(/## \[/g); if (matches) { stats.totalConversations += matches.length; } stats.lastActivity = file.replace('.md', ''); } } } catch { // Handle error silently } return stats; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/fablefang/ai-conversation-logger-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server