heartbeat-service.js
4.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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;
}
/**
* 初始化并启动全局的心跳监控
*/
start() {
console.log('[Heartbeat] 💓 开始初始化 Agent 心跳服务...');
if (!fs.existsSync(DATA_DIR)) return;
const agents = fs.readdirSync(DATA_DIR).filter(f => {
const p = path.join(DATA_DIR, f);
return fs.lstatSync(p).isDirectory() && fs.existsSync(path.join(p, 'config.json'));
});
for (const agentId of agents) {
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;