Усилить heartbeat AI Workspace Hub
This commit is contained in:
parent
c10aa7c301
commit
4e57287b5a
|
|
@ -6,6 +6,8 @@ import { WebSocket, WebSocketServer } from "ws";
|
||||||
const DEFAULT_WS_PATH = "/api/ai-workspace/hub";
|
const DEFAULT_WS_PATH = "/api/ai-workspace/hub";
|
||||||
const MAX_EVENTS_PER_AGENT = 5000;
|
const MAX_EVENTS_PER_AGENT = 5000;
|
||||||
const MAX_REQUEST_META_PER_AGENT = 1000;
|
const MAX_REQUEST_META_PER_AGENT = 1000;
|
||||||
|
const HEARTBEAT_INTERVAL_MS = 30000;
|
||||||
|
const AGENT_STALE_MS = 75000;
|
||||||
|
|
||||||
const config = readConfig();
|
const config = readConfig();
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
@ -80,12 +82,20 @@ httpServer.on("upgrade", (req, socket, head) => {
|
||||||
wss.on("connection", attachAgent);
|
wss.on("connection", attachAgent);
|
||||||
|
|
||||||
const heartbeat = setInterval(() => {
|
const heartbeat = setInterval(() => {
|
||||||
for (const agent of agentsByCode.values()) {
|
for (const agent of Array.from(agentsByCode.values())) {
|
||||||
|
if (!isSocketOpen(agent) || isAgentStale(agent)) {
|
||||||
|
removeAgent(agent, "bridge_agent_stale");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
|
agent.awaitingPong = true;
|
||||||
|
agent.lastPingAt = new Date().toISOString();
|
||||||
agent.socket.ping();
|
agent.socket.ping();
|
||||||
} catch {}
|
} catch {
|
||||||
|
removeAgent(agent, "bridge_agent_ping_failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, HEARTBEAT_INTERVAL_MS);
|
||||||
|
|
||||||
httpServer.listen(config.port, "0.0.0.0", () => {
|
httpServer.listen(config.port, "0.0.0.0", () => {
|
||||||
console.log(`NODE.DC AI Workspace Hub listening on http://0.0.0.0:${config.port}`);
|
console.log(`NODE.DC AI Workspace Hub listening on http://0.0.0.0:${config.port}`);
|
||||||
|
|
@ -105,8 +115,7 @@ function attachAgent(socket, req) {
|
||||||
|
|
||||||
const previous = agentsByCode.get(pairingCode);
|
const previous = agentsByCode.get(pairingCode);
|
||||||
if (previous) {
|
if (previous) {
|
||||||
settleAll(previous, Object.assign(new Error("agent_replaced"), { status: 503 }));
|
removeAgent(previous, "agent_replaced");
|
||||||
previous.socket.close(4000, "agent_replaced");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
@ -115,8 +124,14 @@ function attachAgent(socket, req) {
|
||||||
pairingCode,
|
pairingCode,
|
||||||
machineName: cleanString(url.searchParams.get("machineName"), 120),
|
machineName: cleanString(url.searchParams.get("machineName"), 120),
|
||||||
host: "",
|
host: "",
|
||||||
|
runtimeStatus: "",
|
||||||
|
runtimeError: "",
|
||||||
|
runtimeCheckedAt: "",
|
||||||
connectedAt: now,
|
connectedAt: now,
|
||||||
lastSeenAt: now,
|
lastSeenAt: now,
|
||||||
|
lastPingAt: "",
|
||||||
|
awaitingPong: false,
|
||||||
|
closed: false,
|
||||||
pending: new Map(),
|
pending: new Map(),
|
||||||
requestMeta: previous?.requestMeta || new Map(),
|
requestMeta: previous?.requestMeta || new Map(),
|
||||||
events: previous?.events || [],
|
events: previous?.events || [],
|
||||||
|
|
@ -124,12 +139,12 @@ function attachAgent(socket, req) {
|
||||||
agentsByCode.set(pairingCode, agent);
|
agentsByCode.set(pairingCode, agent);
|
||||||
pushAgentEvent(agent, "", { kind: "connected", message: "Bridge agent connected." });
|
pushAgentEvent(agent, "", { kind: "connected", message: "Bridge agent connected." });
|
||||||
|
|
||||||
socket.on("message", (raw) => handleAgentMessage(agent, raw));
|
socket.on("pong", () => {
|
||||||
socket.on("close", () => {
|
agent.lastSeenAt = new Date().toISOString();
|
||||||
pushAgentEvent(agent, "", { kind: "disconnected", message: "Bridge agent disconnected." });
|
agent.awaitingPong = false;
|
||||||
if (agentsByCode.get(pairingCode) === agent) agentsByCode.delete(pairingCode);
|
|
||||||
settleAll(agent, Object.assign(new Error("bridge_agent_offline"), { status: 503 }));
|
|
||||||
});
|
});
|
||||||
|
socket.on("message", (raw) => handleAgentMessage(agent, raw));
|
||||||
|
socket.on("close", () => removeAgent(agent, "bridge_agent_offline"));
|
||||||
socket.on("error", () => {});
|
socket.on("error", () => {});
|
||||||
|
|
||||||
sendJson(socket, {
|
sendJson(socket, {
|
||||||
|
|
@ -141,6 +156,7 @@ function attachAgent(socket, req) {
|
||||||
|
|
||||||
function handleAgentMessage(agent, raw) {
|
function handleAgentMessage(agent, raw) {
|
||||||
agent.lastSeenAt = new Date().toISOString();
|
agent.lastSeenAt = new Date().toISOString();
|
||||||
|
agent.awaitingPong = false;
|
||||||
let message = null;
|
let message = null;
|
||||||
try {
|
try {
|
||||||
message = JSON.parse(String(raw));
|
message = JSON.parse(String(raw));
|
||||||
|
|
@ -151,6 +167,9 @@ function handleAgentMessage(agent, raw) {
|
||||||
if (message?.type === "hello") {
|
if (message?.type === "hello") {
|
||||||
agent.machineName = cleanString(message.machineName || agent.machineName, 120);
|
agent.machineName = cleanString(message.machineName || agent.machineName, 120);
|
||||||
agent.host = cleanString(message.host, 120);
|
agent.host = cleanString(message.host, 120);
|
||||||
|
agent.runtimeStatus = cleanString(message.runtimeStatus, 40);
|
||||||
|
agent.runtimeError = cleanString(message.runtimeError, 1000);
|
||||||
|
agent.runtimeCheckedAt = cleanString(message.runtimeCheckedAt, 80);
|
||||||
pushAgentEvent(agent, "", {
|
pushAgentEvent(agent, "", {
|
||||||
kind: "hello",
|
kind: "hello",
|
||||||
message: "Bridge agent hello received.",
|
message: "Bridge agent hello received.",
|
||||||
|
|
@ -269,6 +288,7 @@ function pushAgentEvent(agent, requestId, rawEvent = {}) {
|
||||||
pairingCode: agent.pairingCode,
|
pairingCode: agent.pairingCode,
|
||||||
requestId: cleanRequestId,
|
requestId: cleanRequestId,
|
||||||
threadId: rawEvent.threadId == null ? cleanString(meta?.threadId, 160) : cleanString(rawEvent.threadId, 160),
|
threadId: rawEvent.threadId == null ? cleanString(meta?.threadId, 160) : cleanString(rawEvent.threadId, 160),
|
||||||
|
sessionId: rawEvent.sessionId == null ? "" : cleanString(rawEvent.sessionId, 160),
|
||||||
threadTitle: rawEvent.threadTitle == null ? cleanString(meta?.threadTitle, 240) : cleanString(rawEvent.threadTitle, 240),
|
threadTitle: rawEvent.threadTitle == null ? cleanString(meta?.threadTitle, 240) : cleanString(rawEvent.threadTitle, 240),
|
||||||
userMessage: rawEvent.userMessage == null ? cleanString(meta?.userMessage, 12000) : cleanString(rawEvent.userMessage, 12000),
|
userMessage: rawEvent.userMessage == null ? cleanString(meta?.userMessage, 12000) : cleanString(rawEvent.userMessage, 12000),
|
||||||
remoteControlMode: rawEvent.remoteControlMode == null ? cleanString(meta?.remoteControlMode, 40) : cleanString(rawEvent.remoteControlMode, 40),
|
remoteControlMode: rawEvent.remoteControlMode == null ? cleanString(meta?.remoteControlMode, 40) : cleanString(rawEvent.remoteControlMode, 40),
|
||||||
|
|
@ -296,15 +316,47 @@ function publicAgent(agent) {
|
||||||
pairingCode: agent.pairingCode,
|
pairingCode: agent.pairingCode,
|
||||||
machineName: agent.machineName,
|
machineName: agent.machineName,
|
||||||
host: agent.host,
|
host: agent.host,
|
||||||
|
runtimeStatus: agent.runtimeStatus,
|
||||||
|
runtimeError: agent.runtimeError,
|
||||||
|
runtimeCheckedAt: agent.runtimeCheckedAt,
|
||||||
connectedAt: agent.connectedAt,
|
connectedAt: agent.connectedAt,
|
||||||
lastSeenAt: agent.lastSeenAt,
|
lastSeenAt: agent.lastSeenAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAgentOnline(agent) {
|
function isSocketOpen(agent) {
|
||||||
return Boolean(agent?.socket && agent.socket.readyState === WebSocket.OPEN);
|
return Boolean(agent?.socket && agent.socket.readyState === WebSocket.OPEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAgentStale(agent) {
|
||||||
|
const lastSeenMs = Date.parse(agent?.lastSeenAt || agent?.connectedAt || "");
|
||||||
|
if (!Number.isFinite(lastSeenMs)) return true;
|
||||||
|
return Date.now() - lastSeenMs > AGENT_STALE_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAgentOnline(agent) {
|
||||||
|
return isSocketOpen(agent) && !isAgentStale(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAgent(agent, reason = "bridge_agent_offline") {
|
||||||
|
if (!agent || agent.closed) return;
|
||||||
|
agent.closed = true;
|
||||||
|
const error = Object.assign(new Error(reason), { status: reason === "agent_replaced" ? 409 : 503 });
|
||||||
|
pushAgentEvent(agent, "", {
|
||||||
|
kind: "disconnected",
|
||||||
|
message: reason === "agent_replaced"
|
||||||
|
? "Bridge agent connection replaced."
|
||||||
|
: reason === "bridge_agent_stale"
|
||||||
|
? "Bridge agent heartbeat timed out."
|
||||||
|
: "Bridge agent disconnected.",
|
||||||
|
});
|
||||||
|
if (agentsByCode.get(agent.pairingCode) === agent) agentsByCode.delete(agent.pairingCode);
|
||||||
|
settleAll(agent, error);
|
||||||
|
try {
|
||||||
|
if (isSocketOpen(agent)) agent.socket.terminate();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
function settleAll(agent, error) {
|
function settleAll(agent, error) {
|
||||||
for (const pending of agent.pending.values()) {
|
for (const pending of agent.pending.values()) {
|
||||||
clearTimeout(pending.timeout);
|
clearTimeout(pending.timeout);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue