/** * ConversationLogger — 对话日志记录器 * * 目录结构: * conversations/{conversationId}/ * ├── meta.json ← {title, createdAt, updatedAt, status, stats, ...} * ├── {messageId}/ ← 每轮对话独立目录 * │ └── messages.log ← [USER] + [MASTER] + [SUB_AGENT] 日志 * └── use/ ← 合并所有轮次(兼容旧前端) * └── messages.log */ const fs = require('fs'); const path = require('path'); class ConversationLogger { /** * @param {string} conversationsDir - conversations 根目录 * @param {string} conversationId - 对话 ID * @param {string} messageId - 本轮消息 ID * @param {{ userId?: string|number }} [options] - 可选参数 */ constructor(conversationsDir, conversationId, messageId, options = {}) { this.conversationsDir = conversationsDir; this.conversationId = conversationId; this.messageId = messageId; this.userId = options.userId ? String(options.userId) : null; this.convDir = path.join(conversationsDir, conversationId); this.messageDir = path.join(this.convDir, messageId); this.useDir = path.join(this.convDir, 'use'); this.trainDir = path.join(this.convDir, 'train'); // 创建目录 fs.mkdirSync(this.messageDir, { recursive: true }); fs.mkdirSync(this.useDir, { recursive: true }); fs.mkdirSync(this.trainDir, { recursive: true }); this._messagesPath = path.join(this.messageDir, 'messages.log'); this._useMessagesPath = path.join(this.useDir, 'messages.log'); // meta.json — 首次创建对话时写入 this._metaPath = path.join(this.convDir, 'meta.json'); } /** * 写入或更新 meta.json(自动追踪 status 和 stats) */ ensureMeta(title) { const now = new Date().toISOString(); let meta; try { meta = JSON.parse(fs.readFileSync(this._metaPath, 'utf-8')); meta.updatedAt = now; if (title && !meta.title) meta.title = title; } catch { meta = { title: title || '', createdAt: now, updatedAt: now, conversationId: this.conversationId, userId: this.userId, }; } // 确保新字段存在(向后兼容旧 meta.json) if (!meta.status) meta.status = 'running'; if (!meta.stats) { meta.stats = { messageCount: 0, toolCallCount: 0, subAgentCount: 0, costUsd: 0, durationApiMs: 0, }; } // 每次 ensureMeta 表示新一轮消息开始 meta.stats.messageCount = (meta.stats.messageCount || 0) + 1; meta.status = 'running'; fs.writeFileSync(this._metaPath, JSON.stringify(meta, null, 2), 'utf-8'); } /** * 累加统计数据 * @param {object} delta - { toolCallCount?, subAgentCount?, costUsd?, durationApiMs? } */ addStats(delta) { try { const meta = JSON.parse(fs.readFileSync(this._metaPath, 'utf-8')); if (!meta.stats) meta.stats = {}; for (const [key, val] of Object.entries(delta)) { meta.stats[key] = (meta.stats[key] || 0) + val; } meta.updatedAt = new Date().toISOString(); fs.writeFileSync(this._metaPath, JSON.stringify(meta, null, 2), 'utf-8'); } catch (err) { console.error(`[ConversationLogger] addStats 失败:`, err.message); } } /** * 标记对话完成/失败,计算总耗时 * @param {'completed'|'failed'|'idle'} status * @param {string} [error] - 失败原因 */ finalize(status, error = null) { try { const meta = JSON.parse(fs.readFileSync(this._metaPath, 'utf-8')); const now = new Date().toISOString(); meta.status = status; meta.updatedAt = now; meta.completedAt = now; meta.durationMs = new Date(now).getTime() - new Date(meta.createdAt).getTime(); if (error) meta.error = error; fs.writeFileSync(this._metaPath, JSON.stringify(meta, null, 2), 'utf-8'); } catch (err) { console.error(`[ConversationLogger] finalize 失败:`, err.message); } } /** * 记录用户消息 */ logUserMessage(content) { const line = `${this._ts()} [USER]\n${content}\n\n`; this._append(this._messagesPath, line); this._append(this._useMessagesPath, line); } /** * 记录流式文本 chunk(Master 或 Sub Agent) * Sub Agent 的输出同时写入独立的 train/{agentName}.log */ logStreamChunk(agentName, text) { if (!text || !text.trim()) return; const tag = agentName.toUpperCase().replace(/-/g, '_'); const line = `${this._ts()} [${tag}] ${text}\n`; this._append(this._messagesPath, line); this._append(this._useMessagesPath, line); // Sub Agent → 独立日志文件 train/{agentName}.log if (agentName !== 'master') { const agentLogPath = path.join(this.trainDir, `${agentName}.log`); this._append(agentLogPath, line); } } /** * 记录进度消息 */ logProgress(step, message) { const line = `${this._ts()} [ORCHESTRATING] ${message}\n`; this._append(this._messagesPath, line); this._append(this._useMessagesPath, line); } /** * 记录工具调用 */ logToolCall(tool, message) { const line = `${this._ts()} [TOOL] ${tool}: ${message}\n`; this._append(this._messagesPath, line); this._append(this._useMessagesPath, line); } /** * 记录最终结果 */ logResult(result) { const content = `${this._ts()} [RESULT]\n${JSON.stringify(result, null, 2)}\n\n`; this._append(this._messagesPath, content); this._append(this._useMessagesPath, content); } /** * 记录错误 */ logError(error) { const line = `${this._ts()} [ERROR] ${error}\n`; this._append(this._messagesPath, line); this._append(this._useMessagesPath, line); } // ─── Internal ─── _ts() { return `[${new Date().toISOString()}]`; } _append(filePath, content) { try { fs.appendFileSync(filePath, content, 'utf-8'); } catch (err) { console.error(`[ConversationLogger] 写入失败 ${filePath}: ${err.message}`); } } } module.exports = { ConversationLogger };