const fs = require('fs'); const path = require('path'); const http = require('http'); const config = require('../config'); // Agent 目录由 getAgentDir() 统一路由 function getAgentDir(id) { if (id && id.includes(':')) { const [compId, subId] = id.split(':'); return path.resolve(__dirname, `../../data/companies/${compId}/agents/${subId}`); } return path.resolve(__dirname, `../../system/agents/${id}`); } class HeartbeatService { constructor() { this.timers = new Map(); // agentId → NodeJS.Timeout this.activeSessions = new Map(); // agentId → { convId, startedAt } — 防并发互斥 this.port = config.port || 3000; } /** * 初始化并启动全局的心跳监控 (支持全球与所有公司级 Agent) */ start() { console.log('[Heartbeat] 💓 开始初始化 Agent 心跳服务...'); const allAgents = []; const rootDir = path.resolve(__dirname, '../../data'); // 1. 扫描系统级 Agents (system/agents/*) const sysAgentsDir = path.resolve(__dirname, '../../system/agents'); if (fs.existsSync(sysAgentsDir)) { const globals = fs.readdirSync(sysAgentsDir).filter(f => fs.lstatSync(path.join(sysAgentsDir, f)).isDirectory()); for (const ag of globals) { if (fs.existsSync(path.join(sysAgentsDir, ag, 'config.json'))) allAgents.push(ag); } } // 2. 扫描公司级 Agents (data/companies/*/agents/*) const companiesDir = path.join(rootDir, 'companies'); if (fs.existsSync(companiesDir)) { const comps = fs.readdirSync(companiesDir).filter(f => fs.lstatSync(path.join(companiesDir, f)).isDirectory()); for (const compId of comps) { const compAgentDir = path.join(companiesDir, 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) { // 🔒 互斥检查:如果该 Agent 已有活跃心跳会话,跳过本次 const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 分钟安全阀 if (this.activeSessions.has(agentId)) { const active = this.activeSessions.get(agentId); const elapsed = Date.now() - active.startedAt; if (elapsed < SESSION_TIMEOUT) { console.log(`[Heartbeat] ⏭️ 跳过 ${agentId} — 已有活跃会话 ${active.convId} (${(elapsed / 1000).toFixed(0)}s)`); return; } // 超时强制释放(防死锁) console.warn(`[Heartbeat] ⚠️ 强制释放超时会话: ${agentId} / ${active.convId} (${(elapsed / 1000).toFixed(0)}s)`); this.activeSessions.delete(agentId); } const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19); console.log(`[Heartbeat] 💓 触发 ${agentId} 主动心跳 (${timestamp})...`); const convId = `heartbeat-${Date.now().toString(36)}`; // 🔒 注册活跃会话 this.activeSessions.set(agentId, { convId, startedAt: Date.now() }); 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 releaseSession = () => { this.activeSessions.delete(agentId); console.log(`[Heartbeat] 🔓 释放 ${agentId} 会话锁 (${convId})`); }; const req = http.request(options, (res) => { let body = ''; res.on('data', (chunk) => body += chunk); res.on('end', () => { releaseSession(); if (res.statusCode >= 200 && res.statusCode < 300) { console.log(`[Heartbeat] 📨 ${agentId} 成功完成心跳推理会话 (${convId})`); } else { console.warn(`[Heartbeat] ⚠️ ${agentId} 心跳触发异常: ${res.statusCode} ${body}`); } }); }); req.on('error', (err) => { releaseSession(); console.error(`[Heartbeat] ❌ ${agentId} 心跳请求失败: ${err.message}`); }); req.write(postData); req.end(); } } // 导出单例 const heartbeatService = new HeartbeatService(); module.exports = heartbeatService;