You need to sign in before continuing.
conversation-logger.js 6.68 KB
/**
 * 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 };