heartbeat-service.js 5.13 KB
const fs = require('fs');
const path = require('path');
const http = require('http');
const config = require('../config');

const DATA_DIR = path.resolve(__dirname, '../../data/agents');
function getAgentDir(id) {
  if (id && id.includes(':')) {
    const [compId, subId] = id.split(':');
    return path.resolve(__dirname, `../../data/${compId}/agents/${subId}`);
  }
  return path.resolve(__dirname, `../../data/agents/${id}`);
}

class HeartbeatService {
  constructor() {
    this.timers = new Map(); // agentId -> NodeJS.Timeout
    this.port = config.port || 3000;
  }

  /**
   * 初始化并启动全局的心跳监控 (支持全球与所有公司级 Agent)
   */
  start() {
    console.log('[Heartbeat] 💓 开始初始化 Agent 心跳服务...');
    const allAgents = [];

    // 1. 扫描全局 Agents
    if (fs.existsSync(DATA_DIR)) {
      const globals = fs.readdirSync(DATA_DIR).filter(f => fs.lstatSync(path.join(DATA_DIR, f)).isDirectory());
      for (const ag of globals) {
        if (fs.existsSync(path.join(DATA_DIR, ag, 'config.json'))) allAgents.push(ag);
      }
    }

    // 2. 扫描公司级 Agents
    const rootDir = path.resolve(__dirname, '../../data');
    if (fs.existsSync(rootDir)) {
      const comps = fs.readdirSync(rootDir).filter(f => fs.lstatSync(path.join(rootDir, f)).isDirectory() && f !== 'agents' && f !== 'templates' && f !== 'skills');
      for (const compId of comps) {
        const compAgentDir = path.join(rootDir, compId, 'agents');
        if (fs.existsSync(compAgentDir)) {
          const cAgents = fs.readdirSync(compAgentDir).filter(f => fs.lstatSync(path.join(compAgentDir, f)).isDirectory());
          for (const ca of cAgents) {
            if (fs.existsSync(path.join(compAgentDir, ca, 'config.json'))) {
              allAgents.push(`${compId}:${ca}`);
            }
          }
        }
      }
    }

    for (const agentId of allAgents) {
      this._setupAgentHeartbeat(agentId);
    }
  }

  /**
   * 停止所有心跳
   */
  stop() {
    for (const [agentId, timer] of this.timers.entries()) {
      clearInterval(timer);
    }
    this.timers.clear();
    console.log('[Heartbeat] 🛑 心跳服务已停止');
  }

  /**
   * 为单个 Agent 根据 config 设置心跳
   */
  _setupAgentHeartbeat(agentId) {
    try {
      const configPath = path.join(getAgentDir(agentId), 'config.json');
      const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));

      if (cfg.heartbeat && cfg.heartbeat.enabled === true) {
        const intervalMs = (cfg.heartbeat.intervalMinutes || 15) * 60 * 1000;
        
        // 确保不会重复启动
        if (this.timers.has(agentId)) {
          clearInterval(this.timers.get(agentId));
        }

        const timer = setInterval(() => this._triggerHeartbeat(agentId), intervalMs);
        this.timers.set(agentId, timer);
        console.log(`[Heartbeat] ✅ 已挂载 ${agentId} 的主动心跳 (间隔: ${cfg.heartbeat.intervalMinutes} 分钟)`);
      }
    } catch (err) {
      console.warn(`[Heartbeat] ⚠️ 读取 ${agentId} 配置失败: ${err.message}`);
    }
  }

  /**
   * 内部方法:向 Agent 的 Chat API 发送一次隐式的系统心跳
   */
  _triggerHeartbeat(agentId) {
    const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
    console.log(`[Heartbeat] 💓 触发 ${agentId} 主动心跳 (${timestamp})...`);

    const convId = `heartbeat-${Date.now().toString(36)}`;
    const heartbeatMessage = [
      `[SYSTEM HEARTBEAT]`,
      `当前系统时间:${timestamp}`,
      `这是一个后台周期的主动心跳触发。`,
      `要求:请根据你的身份和 heartbeat.md 规则,扫描当前最新的全局 OKR 目标状态和节点。`,
      `1. 如果发现某个负责人超时、卡点或产出需要仲裁,请主动使用 dispatch 工具给他们发送通知或驳回。`,
      `2. 如果一切正常,无事发生,请直接回复“[心跳正常]”并且不要使用任何工具。`,
      `无需等待人类确认,请完全自主决策。`
    ].join('\n');

    const postData = JSON.stringify({
      message: heartbeatMessage,
      conversationId: convId,
      isHeartbeat: true // 特殊标记,用于前端或后端识别
    });

    const options = {
      hostname: 'localhost',
      port: this.port,
      path: `/api/agent/${agentId}/chat`,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(postData)
      }
    };

    const req = http.request(options, (res) => {
      let body = '';
      res.on('data', (chunk) => body += chunk);
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          console.log(`[Heartbeat] 📨 ${agentId} 成功启动心跳推理会话 (${convId})`);
        } else {
          console.warn(`[Heartbeat] ⚠️ ${agentId} 心跳触发异常: ${res.statusCode} ${body}`);
        }
      });
    });

    req.on('error', (err) => {
      console.error(`[Heartbeat] ❌ ${agentId} 心跳请求失败: ${err.message}`);
    });

    req.write(postData);
    req.end();
  }
}

// 导出单例
const heartbeatService = new HeartbeatService();
module.exports = heartbeatService;