冰朔 f772bafa2f
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled
铸渊自检 / health-check (push) Has been cancelled
D129: 联邦托管包 · federation-agent v1.0 · ENTRY+AGENT+DISCUSS+agent.js+install.sh+registry.js
2026-06-10 23:19:34 +08:00

260 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// federation-agent/server/registry.js · v1.0
// 服务器端 · Agent 注册管理中心
// 挂载到 gatekeeper 上,管理所有联邦 Agent 节点
// ICE-GL-ZY001 · D129 · 2026-06-10
const WebSocket = require("ws");
const http = require("http");
// ===== 配置 =====
const PORT = process.env.AGENT_PORT || 3912;
const AUTH_TOKEN = process.env.AGENT_AUTH || "zy_gtw_federation_agent_v1";
// ===== 注册表 =====
class AgentRegistry {
constructor() {
this.agents = new Map(); // agentId → { ws, info, status }
this.personas = new Map(); // personaId → [agentId, ...]
this.users = new Map(); // userId → [agentId, ...]
this.commandQueue = new Map(); // agentId → [pending commands]
}
register(ws, info) {
const { agentId, personaId, userId, hostname, platform, workDir } = info;
const agent = {
ws,
info: { agentId, personaId, userId, hostname, platform, workDir, version: info.version || "1.0.0" },
status: "online",
connectedAt: new Date().toISOString(),
lastHeartbeat: Date.now(),
commandCount: 0,
};
this.agents.set(agentId, agent);
if (personaId) {
if (!this.personas.has(personaId)) this.personas.set(personaId, []);
this.personas.get(personaId).push(agentId);
}
if (userId) {
if (!this.users.has(userId)) this.users.set(userId, []);
this.users.get(userId).push(agentId);
}
console.log(`[REGISTRY] + ${agentId} (${personaId || "未注册"} @ ${userId || "未知用户"} · ${hostname})`);
return agent;
}
unregister(agentId) {
const agent = this.agents.get(agentId);
if (!agent) return;
this.agents.delete(agentId);
if (agent.info.personaId) {
const list = this.personas.get(agent.info.personaId);
if (list) {
const idx = list.indexOf(agentId);
if (idx > -1) list.splice(idx, 1);
}
}
if (agent.info.userId) {
const list = this.users.get(agent.info.userId);
if (list) {
const idx = list.indexOf(agentId);
if (idx > -1) list.splice(idx, 1);
}
}
console.log(`[REGISTRY] - ${agentId} (断开)`);
}
sendCommand(agentId, command) {
const agent = this.agents.get(agentId);
if (!agent || !agent.ws || agent.ws.readyState !== WebSocket.OPEN) {
// 加入待发送队列
if (!this.commandQueue.has(agentId)) this.commandQueue.set(agentId, []);
this.commandQueue.get(agentId).push(command);
return { ok: false, error: "agent offline" };
}
agent.ws.send(JSON.stringify({ type: "command", command }));
agent.commandCount++;
return { ok: true };
}
flushQueue(agentId) {
const queue = this.commandQueue.get(agentId);
if (!queue || queue.length === 0) return 0;
let sent = 0;
while (queue.length > 0) {
const cmd = queue.shift();
const result = this.sendCommand(agentId, cmd);
if (result.ok) sent++;
else break;
}
return sent;
}
broadcast(personaIds, command) {
const results = [];
for (const personaId of personaIds) {
const agentIds = this.personas.get(personaId) || [];
for (const agentId of agentIds) {
results.push({ agentId, ...this.sendCommand(agentId, command) });
}
}
return results;
}
list(target) {
if (target === "all") {
return Array.from(this.agents.values()).map(a => ({
agentId: a.info.agentId,
personaId: a.info.personaId,
userId: a.info.userId,
hostname: a.info.hostname,
status: a.status,
commandCount: a.commandCount,
connectedAt: a.connectedAt,
}));
}
// 按用户过滤
const agentIds = this.users.get(target) || [];
return agentIds.map(id => {
const a = this.agents.get(id);
return a ? {
agentId: a.info.agentId,
personaId: a.info.personaId,
status: a.status,
commandCount: a.commandCount,
} : null;
}).filter(Boolean);
}
healthCheck() {
const now = Date.now();
for (const [agentId, agent] of this.agents) {
if (now - agent.lastHeartbeat > 120000) {
agent.status = "timeout";
console.log(`[REGISTRY] ⚠ ${agentId} 心跳超时`);
}
}
}
}
// ===== WebSocket 服务器 =====
const registry = new AgentRegistry();
const server = http.createServer((req, res) => {
// REST API 端点
if (req.url === "/api/agents" && req.method === "GET") {
const target = new URL(req.url, "http://localhost").searchParams.get("user") || "all";
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(registry.list(target)));
return;
}
if (req.url === "/api/command" && req.method === "POST") {
let body = "";
req.on("data", chunk => body += chunk);
req.on("end", () => {
try {
const { agentId, command } = JSON.parse(body);
const result = registry.sendCommand(agentId, command);
res.writeHead(result.ok ? 200 : 404, { "Content-Type": "application/json" });
res.end(JSON.stringify(result));
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: e.message }));
}
});
return;
}
res.writeHead(404);
res.end("Not Found");
});
const wss = new WebSocket.Server({ server, path: "/agent" });
wss.on("connection", (ws, req) => {
const url = new URL(req.url, "http://localhost");
const agentId = url.searchParams.get("agentId") || "unknown";
const personaId = url.searchParams.get("persona") || "";
const userId = url.searchParams.get("user") || "";
console.log(`[WS] 新连接: ${agentId}`);
// 先不发 welcome等 register 消息
ws.agentId = agentId;
ws.on("message", (data) => {
try {
const msg = JSON.parse(data.toString());
switch (msg.type) {
case "register": {
const agent = registry.register(ws, msg);
ws.send(JSON.stringify({
type: "welcome",
message: `光湖联邦托管 · 注册成功 · ${agent.info.personaId || agentId}`,
personaId: agent.info.personaId,
timestamp: Date.now(),
}));
// 发送队列中的命令
const flushed = registry.flushQueue(agentId);
if (flushed > 0) console.log(`[REGISTRY] → ${agentId} 补发 ${flushed} 条待处理指令`);
break;
}
case "result": {
console.log(`[RESULT] ${agentId}: ${msg.command}${msg.ok ? "OK" : "FAIL"}`);
if (msg.stderr) console.log(` stderr: ${msg.stderr.substring(0, 200)}`);
break;
}
case "heartbeat": {
const agent = registry.agents.get(agentId);
if (agent) agent.lastHeartbeat = Date.now();
break;
}
case "health_report": {
console.log(`[HEALTH] ${agentId}: uptime=${(msg.uptime / 60).toFixed(1)}min · mem=${(msg.memory.heapUsed / 1024 / 1024).toFixed(1)}MB`);
break;
}
case "confirm_request": {
console.log(`[CONFIRM] ${agentId}: 请求确认危险操作: ${msg.operation}`);
break;
}
default:
console.log(`[MSG] ${agentId}: ${msg.type}`);
}
} catch (e) {
console.error(`[ERROR] ${agentId}: ${e.message}`);
}
});
ws.on("close", () => {
registry.unregister(agentId);
});
ws.on("error", (err) => {
console.error(`[WS ERROR] ${agentId}: ${err.message}`);
});
});
// 健康检查定时器
setInterval(() => registry.healthCheck(), 30000);
server.listen(PORT, () => {
console.log(`[REGISTRY] 光湖联邦 Agent 注册中心 · 端口 ${PORT}`);
console.log(`[REGISTRY] WebSocket: ws://0.0.0.0:${PORT}/agent`);
console.log(`[REGISTRY] REST API: http://0.0.0.0:${PORT}/api/agents`);
});
process.on("SIGINT", () => {
console.log("[REGISTRY] 关闭中...");
wss.close();
server.close();
process.exit(0);
});