evolution-logger.js 6.01 KB
/**
 * Evolution Logger — 结构化日志输出
 *
 * 将进化过程的关键事件写入日志文件:
 *   logs/evolution-<runId>-<timestamp>.log
 *
 * 每行一个 JSON 事件,方便 grep/jq 分析。
 * 同时仍然打印到 console(不影响终端体验)。
 */
const fs = require('fs');
const path = require('path');

const LOGS_DIR = path.resolve(__dirname, '../../logs');

class EvolutionLogger {
  constructor(runId, skillName) {
    this.runId = runId || 'unknown';
    this.skillName = skillName || '?';
    this.startTime = Date.now();
    this.events = [];

    // 确保日志目录存在
    if (!fs.existsSync(LOGS_DIR)) {
      fs.mkdirSync(LOGS_DIR, { recursive: true });
    }

    const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
    this.filePath = path.join(LOGS_DIR, `evolution-${this.skillName}-${ts}.log`);
    this.stream = fs.createWriteStream(this.filePath, { flags: 'a' });

    this._write('session_start', {
      runId: this.runId,
      skillName: this.skillName,
      startedAt: new Date().toISOString(),
    });
  }

  /**
   * 记录一个事件
   * @param {string} event - 事件类型
   * @param {object} data - 事件数据
   * @param {string} [message] - 人类可读消息
   */
  log(event, data = {}, message) {
    const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
    const entry = {
      t: new Date().toISOString(),
      elapsed: `${elapsed}s`,
      event,
      ...data,
    };

    // 写文件
    this._write(event, entry);

    // 打印到终端
    const msg = message || this._formatMessage(event, data);
    if (msg) console.log(`[Evolution] ${msg}`);

    this.events.push(entry);
    return entry;
  }

  /** Phase 级别事件 */
  phase(name, data = {}) {
    return this.log('phase', { phase: name, ...data },
      `\n═══ ${name} ═══`);
  }

  /** Step 级别事件 */
  step(name, data = {}) {
    return this.log('step', { step: name, ...data });
  }

  /** LLM 调用 */
  llmCall(purpose, data = {}) {
    return this.log('llm_call', { purpose, ...data },
      `  🤖 LLM: ${purpose}${data.backend ? ` [${data.backend}]` : ''}`);
  }

  /** LLM 响应 */
  llmResult(purpose, data = {}) {
    const status = data.success ? '✅' : '❌';
    return this.log('llm_result', { purpose, ...data },
      `  ${status} LLM 结果: ${purpose}${data.outputLength ? ` (${data.outputLength} chars)` : ''}`);
  }

  /** 基因组事件 */
  genome(action, data = {}) {
    return this.log('genome', { action, ...data });
  }

  /** 错误 */
  error(context, err) {
    return this.log('error', {
      context,
      message: err.message || String(err),
      stack: err.stack?.split('\n').slice(0, 3).join(' | '),
    }, `  ❌ ${context}: ${err.message || err}`);
  }

  /** 结束 */
  end(summary = {}) {
    const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
    this._write('session_end', {
      runId: this.runId,
      skillName: this.skillName,
      elapsed: `${elapsed}s`,
      totalEvents: this.events.length,
      ...summary,
    });

    this.stream.end();
    console.log(`\n📋 进化日志已保存: ${this.filePath}`);
    return this.filePath;
  }

  /** 生成人类可读的摘要报告 */
  writeSummary() {
    const summaryPath = this.filePath.replace('.log', '.summary.md');
    const lines = ['# 进化运行摘要\n'];
    lines.push(`- **Skill**: ${this.skillName}`);
    lines.push(`- **Run ID**: ${this.runId}`);
    lines.push(`- **开始**: ${new Date(this.startTime).toISOString()}`);
    lines.push(`- **耗时**: ${((Date.now() - this.startTime) / 1000).toFixed(1)}s`);
    lines.push(`- **总事件数**: ${this.events.length}\n`);

    // 按阶段分组
    lines.push('## 事件时间线\n');
    lines.push('| 时间 | 事件 | 详情 |');
    lines.push('|------|------|------|');

    for (const e of this.events) {
      const detail = this._summarizeEvent(e);
      if (detail) {
        lines.push(`| ${e.elapsed} | ${e.event} | ${detail} |`);
      }
    }

    // LLM 调用统计
    const llmCalls = this.events.filter(e => e.event === 'llm_call');
    const llmResults = this.events.filter(e => e.event === 'llm_result');
    const llmErrors = llmResults.filter(e => !e.success);

    lines.push('\n## LLM 调用统计\n');
    lines.push(`- 总调用: ${llmCalls.length}`);
    lines.push(`- 成功: ${llmResults.length - llmErrors.length}`);
    lines.push(`- 失败: ${llmErrors.length}`);

    if (llmErrors.length > 0) {
      lines.push('\n### 失败详情\n');
      for (const e of llmErrors) {
        lines.push(`- **${e.purpose}**: ${e.message || '未知错误'}`);
      }
    }

    // 基因组统计
    const genomeEvents = this.events.filter(e => e.event === 'genome');
    if (genomeEvents.length > 0) {
      lines.push('\n## 基因组操作\n');
      const actions = {};
      for (const e of genomeEvents) {
        actions[e.action] = (actions[e.action] || 0) + 1;
      }
      for (const [action, count] of Object.entries(actions)) {
        lines.push(`- ${action}: ${count} 次`);
      }
    }

    fs.writeFileSync(summaryPath, lines.join('\n'), 'utf-8');
    console.log(`📊 摘要报告: ${summaryPath}`);
    return summaryPath;
  }

  // ── 内部方法 ──

  _write(event, data) {
    try {
      this.stream.write(JSON.stringify(data) + '\n');
    } catch { /* 静默 */ }
  }

  _formatMessage(event, data) {
    switch (event) {
      case 'step':
        return `  📌 ${data.step}${data.count !== undefined ? ` (${data.count})` : ''}`;
      case 'genome':
        return `  🧬 ${data.action}: ${data.id?.slice(0, 8) || ''}${data.fitness ? ` fitness=${(data.fitness * 100).toFixed(1)}%` : ''}`;
      case 'llm_request':
        return `  📤 → LLM [${data.backend}] (${data.promptLength} chars)`;
      case 'llm_response':
        return `  📥 ← LLM [${data.backend}] (${data.outputLength} chars, ${(data.durationMs / 1000).toFixed(1)}s)`;
      case 'llm_parsed':
        return `  ✅ JSON 解析成功`;
      default:
        return null;
    }
  }
}

module.exports = { EvolutionLogger };