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:
2026-03-19 18:52:12 +08:00
parent f4a2b7f3af
commit 4b5ac86b83
87 changed files with 5042 additions and 744 deletions

View File

@@ -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();
}
}