conversationLogger.ts•13.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;
}
}