Files
evotraders/frontend/src/services/websocket.js
cillin 4b5ac86b83 feat: Add evaluation hooks, skill adaptation and team pipeline config
- Add EvaluationHook for post-execution agent evaluation
- Add SkillAdaptationHook for dynamic skill adaptation
- Add team/ directory with team coordination logic
- Add TEAM_PIPELINE.yaml for smoke_fullstack pipeline config
- Update RuntimeView, TraderView and RuntimeSettingsPanel UI
- Add runtimeApi and websocket services
- Add runtime_state.json to smoke_fullstack state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 18:52:12 +08:00

283 lines
7.3 KiB
JavaScript

/**
* WebSocket Client with Dynamic Port Resolution
* Handles connection, reconnection, and heartbeat
* Fetches Gateway port from API before connecting
*/
import { WS_URL } from "../config/constants";
// Global port cache
let cachedGatewayPort = null;
let cachedWsUrl = null;
/**
* Fetch Gateway WebSocket port from API
*/
export async function fetchGatewayPort() {
try {
const response = await fetch('/api/runtime/gateway/port');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.is_running && data.port) {
cachedGatewayPort = data.port;
cachedWsUrl = data.ws_url;
return { port: data.port, wsUrl: data.ws_url };
}
return null;
} catch (error) {
console.warn('[Gateway] Failed to fetch port:', error);
return null;
}
}
/**
* Get cached or default WebSocket URL
*/
export function getWebSocketUrl() {
if (cachedWsUrl) {
return cachedWsUrl;
}
return WS_URL;
}
/**
* Clear cached port (call when Gateway restarts)
*/
export function clearGatewayCache() {
cachedGatewayPort = null;
cachedWsUrl = null;
}
export class ReadOnlyClient {
constructor(onEvent, { wsUrl = null, reconnectDelay = 3000, heartbeatInterval = 5000 } = {}) {
this.onEvent = onEvent;
this.wsUrl = wsUrl; // null = auto-resolve from API
this.baseReconnectDelay = reconnectDelay;
this.reconnectDelay = reconnectDelay;
this.maxReconnectDelay = 30000;
this.heartbeatInterval = heartbeatInterval;
this.ws = null;
this.shouldReconnect = false;
this.reconnectTimer = null;
this.heartbeatTimer = null;
this.reconnectAttempts = 0;
this.lastPongTime = 0;
this.isConnecting = false;
}
async connect() {
this.shouldReconnect = true;
this.reconnectAttempts = 0;
this.reconnectDelay = this.baseReconnectDelay;
await this._connect();
}
async _connect() {
if (!this.shouldReconnect || this.isConnecting) {
return;
}
this.isConnecting = true;
// Resolve WebSocket URL if not set
let targetUrl = this.wsUrl;
if (!targetUrl) {
// Try to fetch from API first
const gatewayInfo = await fetchGatewayPort();
if (gatewayInfo) {
targetUrl = gatewayInfo.wsUrl;
console.log(`[WebSocket] Resolved Gateway port: ${gatewayInfo.port}`);
} else {
// Fallback to default
targetUrl = WS_URL;
console.log(`[WebSocket] Using default URL: ${targetUrl}`);
}
}
// Clear any existing connection
if (this.ws) {
this.ws.onopen = null;
this.ws.onmessage = null;
this.ws.onerror = null;
this.ws.onclose = null;
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
this.ws.close();
}
this.ws = null;
}
try {
this.ws = new WebSocket(targetUrl);
this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.reconnectDelay = this.baseReconnectDelay;
this.lastPongTime = Date.now();
this._safeEmit({ type: "system", content: "已连接实时数据服务" });
console.log("WebSocket connected to", targetUrl);
this._startHeartbeat();
this.isConnecting = false;
};
this.ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data);
// Update pong time for any message (server is alive)
this.lastPongTime = Date.now();
if (msg.type === "pong") {
return;
}
console.log("[WebSocket] Message received:", msg.type || "unknown");
this._safeEmit(msg);
} catch (e) {
console.error("[WebSocket] Parse error:", e);
}
};
this.ws.onerror = (error) => {
console.error("WebSocket error:", error);
this.isConnecting = false;
};
this.ws.onclose = (event) => {
const code = event.code || "未知";
console.log(`[WebSocket] Connection closed: Code=${code}, WasClean=${event.wasClean}`);
this._stopHeartbeat();
this.ws = null;
this.isConnecting = false;
// Always attempt reconnect if shouldReconnect is true
if (this.shouldReconnect) {
this.reconnectAttempts++;
// Exponential backoff with cap
this.reconnectDelay = Math.min(
this.baseReconnectDelay * Math.pow(1.5, this.reconnectAttempts),
this.maxReconnectDelay
);
this._safeEmit({
type: "system",
content: "正在尝试连接数据服务..."
});
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
this.reconnectTimer = setTimeout(() => {
console.log(`[WebSocket] Reconnect attempt ${this.reconnectAttempts}...`);
this._connect();
}, this.reconnectDelay);
}
};
} catch (error) {
console.error("[WebSocket] Connection error:", error);
this.isConnecting = false;
if (this.shouldReconnect) {
this.reconnectTimer = setTimeout(() => {
this._connect();
}, this.reconnectDelay);
}
}
}
_safeEmit(msg) {
try {
this.onEvent(msg);
} catch (e) {
console.error("[WebSocket] Error in event handler:", e);
}
}
_startHeartbeat() {
this._stopHeartbeat();
this.lastPongTime = Date.now();
this.heartbeatTimer = setInterval(() => {
this._sendPing();
// Check for stale connection (no response in 60s)
const timeSinceLastPong = Date.now() - this.lastPongTime;
if (timeSinceLastPong > 60000 && this.ws) {
console.warn("[WebSocket] Connection appears stale, forcing reconnect");
this.ws.close();
}
}, this.heartbeatInterval);
}
_sendPing() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(JSON.stringify({ type: "ping" }));
} catch (e) {
console.error("Heartbeat send error:", e);
}
}
}
_stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
send(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
const messageStr = typeof message === "string" ? message : JSON.stringify(message);
this.ws.send(messageStr);
return true;
} catch (e) {
console.error("Send error:", e);
return false;
}
} else {
console.warn("WebSocket is not connected, cannot send message");
return false;
}
}
disconnect() {
this.shouldReconnect = false;
this._stopHeartbeat();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.onopen = null;
this.ws.onmessage = null;
this.ws.onerror = null;
this.ws.onclose = null;
try {
this.ws.close();
} catch (e) {
console.error("Close error:", e);
}
}
this.ws = null;
this.isConnecting = false;
}
/**
* Reconnect with new port (call after Gateway restart)
*/
async reconnectWithNewPort() {
console.log("[WebSocket] Reconnecting with new port...");
clearGatewayCache();
this.disconnect();
this.shouldReconnect = true;
await this.connect();
}
}