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>
This commit is contained in:
@@ -1,14 +1,61 @@
|
||||
/**
|
||||
* WebSocket Client for Read-Only Connection
|
||||
* 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 = WS_URL, reconnectDelay = 3000, heartbeatInterval = 5000 } = {}) {
|
||||
constructor(onEvent, { wsUrl = null, reconnectDelay = 3000, heartbeatInterval = 5000 } = {}) {
|
||||
this.onEvent = onEvent;
|
||||
this.wsUrl = wsUrl;
|
||||
this.wsUrl = wsUrl; // null = auto-resolve from API
|
||||
this.baseReconnectDelay = reconnectDelay;
|
||||
this.reconnectDelay = reconnectDelay;
|
||||
this.maxReconnectDelay = 30000;
|
||||
@@ -19,20 +66,38 @@ export class ReadOnlyClient {
|
||||
this.heartbeatTimer = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.lastPongTime = 0;
|
||||
this.isConnecting = false;
|
||||
}
|
||||
|
||||
connect() {
|
||||
async connect() {
|
||||
this.shouldReconnect = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.reconnectDelay = this.baseReconnectDelay;
|
||||
this._connect();
|
||||
await this._connect();
|
||||
}
|
||||
|
||||
_connect() {
|
||||
if (!this.shouldReconnect) {
|
||||
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;
|
||||
@@ -45,70 +110,84 @@ export class ReadOnlyClient {
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
this.ws = new WebSocket(this.wsUrl);
|
||||
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");
|
||||
this._startHeartbeat();
|
||||
};
|
||||
|
||||
this.ws.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
|
||||
// Update pong time for any message (server is alive)
|
||||
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;
|
||||
};
|
||||
|
||||
if (msg.type === "pong") {
|
||||
return;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
};
|
||||
this.ws.onclose = (event) => {
|
||||
const code = event.code || "未知";
|
||||
console.log(`[WebSocket] Connection closed: Code=${code}, WasClean=${event.wasClean}`);
|
||||
|
||||
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;
|
||||
|
||||
this._stopHeartbeat();
|
||||
this.ws = null;
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
_safeEmit(msg) {
|
||||
@@ -187,5 +266,17 @@ export class ReadOnlyClient {
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user