"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.XiaoYiRuntime = void 0; exports.getXiaoYiRuntime = getXiaoYiRuntime; exports.setXiaoYiRuntime = setXiaoYiRuntime; const websocket_1 = require("./websocket"); /** * Default timeout configuration */ const DEFAULT_TIMEOUT_CONFIG = { enabled: true, duration: 60000, // 60 seconds message: "任务正在处理中,请稍后", }; /** * Runtime state for XiaoYi channel * Manages single WebSocket connection (single account mode) */ class XiaoYiRuntime { constructor() { this.connection = null; this.pluginRuntime = null; // Store PluginRuntime from OpenClaw this.config = null; this.sessionToTaskIdMap = new Map(); // Map sessionId to taskId // Timeout management this.sessionTimeoutMap = new Map(); this.sessionTimeoutSent = new Set(); this.timeoutConfig = DEFAULT_TIMEOUT_CONFIG; // AbortController management for canceling agent runs this.sessionAbortControllerMap = new Map(); // Track if a session has an active agent run (for concurrent request detection) this.sessionActiveRunMap = new Map(); // Track session start time for timeout detection this.sessionStartTimeMap = new Map(); // 1-hour task timeout mechanism this.sessionTaskTimeoutMap = new Map(); this.sessionPushPendingMap = new Map(); this.taskTimeoutMs = 3600000; // Default 1 hour this.instanceId = `runtime_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; console.log(`XiaoYi: Created new runtime instance: ${this.instanceId}`); } getInstanceId() { return this.instanceId; } /** * Set OpenClaw PluginRuntime (from api.runtime in register()) */ setPluginRuntime(runtime) { console.log(`XiaoYi: [${this.instanceId}] Setting PluginRuntime`); this.pluginRuntime = runtime; } /** * Get OpenClaw PluginRuntime */ getPluginRuntime() { return this.pluginRuntime; } /** * Start connection (single account mode) */ async start(config) { if (this.connection) { console.log("XiaoYi channel already connected"); return; } this.config = config; const manager = new websocket_1.XiaoYiWebSocketManager(config); // Setup basic event handlers (message handling is done in channel.ts) manager.on("error", (error) => { console.error("XiaoYi channel error:", error); }); manager.on("disconnected", () => { console.log("XiaoYi channel disconnected"); }); manager.on("authenticated", () => { console.log("XiaoYi channel authenticated"); }); manager.on("maxReconnectAttemptsReached", (serverId) => { console.error(`XiaoYi channel ${serverId} max reconnect attempts reached`); // Check if the other server is still connected and ready const otherServerId = serverId === 'server1' ? 'server2' : 'server1'; const serverStates = manager.getServerStates(); const otherServerState = otherServerId === 'server1' ? serverStates.server1 : serverStates.server2; if (otherServerState?.connected && otherServerState?.ready) { console.warn(`[${otherServerId}] is still connected and ready, continuing in single-server mode`); console.warn(`System will continue running with ${otherServerId} only`); // Don't stop, continue with the other server return; } // Only stop when both servers have failed console.error("Both servers have reached max reconnect attempts, stopping connection"); console.error(`Server1: ${serverStates.server1.connected ? 'connected' : 'disconnected'}, Server2: ${serverStates.server2.connected ? 'connected' : 'disconnected'}`); this.stop(); }); // Connect await manager.connect(); this.connection = manager; console.log("XiaoYi channel started"); } /** * Stop connection */ stop() { if (this.connection) { this.connection.disconnect(); this.connection = null; console.log("XiaoYi channel stopped"); } // Clear session mappings this.sessionToTaskIdMap.clear(); // Clear all timeouts this.clearAllTimeouts(); // Clear all abort controllers this.clearAllAbortControllers(); // Clear all task timeout state for (const sessionId of this.sessionTaskTimeoutMap.keys()) { this.clearTaskTimeoutState(sessionId); } } /** * Set timeout configuration */ setTimeoutConfig(config) { this.timeoutConfig = { ...this.timeoutConfig, ...config }; console.log(`XiaoYi: Timeout config updated:`, this.timeoutConfig); } /** * Get timeout configuration */ getTimeoutConfig() { return { ...this.timeoutConfig }; } /** * Set timeout for a session * @param sessionId - Session ID * @param callback - Function to call when timeout occurs * @returns The interval ID (for cancellation) * * IMPORTANT: This now uses setInterval instead of setTimeout * - First trigger: after 60 seconds * - Subsequent triggers: every 60 seconds after that * - Cleared when: response received, session completed, or explicitly cleared */ setTimeoutForSession(sessionId, callback) { if (!this.timeoutConfig.enabled) { console.log(`[TIMEOUT] Timeout disabled, skipping for session ${sessionId}`); return undefined; } // Clear existing timeout AND timeout flag if any (reuse session scenario) const hadExistingTimeout = this.sessionTimeoutMap.has(sessionId); const hadSentTimeout = this.sessionTimeoutSent.has(sessionId); this.clearSessionTimeout(sessionId); // Clear the timeout sent flag to allow this session to timeout again if (hadSentTimeout) { this.sessionTimeoutSent.delete(sessionId); console.log(`[TIMEOUT] Previous timeout flag cleared for session ${sessionId} (session reuse)`); } // Use setInterval for periodic timeout triggers // First trigger after duration, then every duration after that const intervalId = setInterval(() => { console.log(`[TIMEOUT] Timeout triggered for session ${sessionId} (will trigger again in ${this.timeoutConfig.duration}ms if still active)`); this.sessionTimeoutSent.add(sessionId); callback(); }, this.timeoutConfig.duration); this.sessionTimeoutMap.set(sessionId, intervalId); const logSuffix = hadExistingTimeout ? " (replacing existing interval)" : ""; console.log(`[TIMEOUT] ${this.timeoutConfig.duration}ms periodic timeout started for session ${sessionId}${logSuffix}`); return intervalId; } /** * Clear timeout interval for a session * @param sessionId - Session ID */ clearSessionTimeout(sessionId) { const intervalId = this.sessionTimeoutMap.get(sessionId); if (intervalId) { clearInterval(intervalId); this.sessionTimeoutMap.delete(sessionId); console.log(`[TIMEOUT] Timeout interval cleared for session ${sessionId}`); } } /** * Check if timeout has been sent for a session * @param sessionId - Session ID */ isSessionTimeout(sessionId) { return this.sessionTimeoutSent.has(sessionId); } /** * Mark session as completed (clear timeout and timeout flag) * @param sessionId - Session ID */ markSessionCompleted(sessionId) { this.clearSessionTimeout(sessionId); this.sessionTimeoutSent.delete(sessionId); console.log(`[TIMEOUT] Session ${sessionId} marked as completed`); } /** * Clear all timeout intervals */ clearAllTimeouts() { for (const [sessionId, intervalId] of this.sessionTimeoutMap.entries()) { clearInterval(intervalId); } this.sessionTimeoutMap.clear(); this.sessionTimeoutSent.clear(); console.log("[TIMEOUT] All timeout intervals cleared"); } /** * Get WebSocket manager */ getConnection() { return this.connection; } /** * Check if connected */ isConnected() { return this.connection ? this.connection.isReady() : false; } /** * Get configuration */ getConfig() { return this.config; } /** * Set taskId for a session */ setTaskIdForSession(sessionId, taskId) { this.sessionToTaskIdMap.set(sessionId, taskId); } /** * Get taskId for a session */ getTaskIdForSession(sessionId) { return this.sessionToTaskIdMap.get(sessionId); } /** * Clear taskId for a session */ clearTaskIdForSession(sessionId) { this.sessionToTaskIdMap.delete(sessionId); } /** * Create and register an AbortController for a session * @param sessionId - Session ID * @returns The AbortController and its signal, or null if session is busy */ createAbortControllerForSession(sessionId) { // Check if there's an active agent run for this session if (this.sessionActiveRunMap.get(sessionId)) { console.log(`[CONCURRENT] Session ${sessionId} has an active agent run, cannot create new AbortController`); return null; } const controller = new AbortController(); this.sessionAbortControllerMap.set(sessionId, controller); this.sessionActiveRunMap.set(sessionId, true); this.sessionStartTimeMap.set(sessionId, Date.now()); console.log(`[ABORT] Created AbortController for session ${sessionId}`); return { controller, signal: controller.signal }; } /** * Check if a session has an active agent run * If session is active but stale (超过 SESSION_STALE_TIMEOUT_MS), automatically clean up * @param sessionId - Session ID * @returns true if session is busy */ isSessionActive(sessionId) { const isActive = this.sessionActiveRunMap.get(sessionId) || false; if (isActive) { // Check if the session has been active for too long const startTime = this.sessionStartTimeMap.get(sessionId); if (startTime) { const elapsed = Date.now() - startTime; if (elapsed > XiaoYiRuntime.SESSION_STALE_TIMEOUT_MS) { // Session is stale, auto-cleanup and return false console.log(`[CONCURRENT] Session ${sessionId} is stale (active for ${elapsed}ms), auto-cleaning`); this.clearAbortControllerForSession(sessionId); this.clearTaskIdForSession(sessionId); this.clearSessionTimeout(sessionId); this.sessionStartTimeMap.delete(sessionId); return false; } } } return isActive; } /** * Abort a session's agent run * @param sessionId - Session ID * @returns true if a controller was found and aborted, false otherwise */ abortSession(sessionId) { const controller = this.sessionAbortControllerMap.get(sessionId); if (controller) { console.log(`[ABORT] Aborting session ${sessionId}`); controller.abort(); this.sessionAbortControllerMap.delete(sessionId); return true; } console.log(`[ABORT] No AbortController found for session ${sessionId}`); return false; } /** * Check if a session has been aborted * @param sessionId - Session ID * @returns true if the session's abort signal was triggered */ isSessionAborted(sessionId) { const controller = this.sessionAbortControllerMap.get(sessionId); return controller ? controller.signal.aborted : false; } /** * Clear the AbortController for a session (call when agent completes successfully) * @param sessionId - Session ID */ clearAbortControllerForSession(sessionId) { const controller = this.sessionAbortControllerMap.get(sessionId); if (controller) { this.sessionAbortControllerMap.delete(sessionId); console.log(`[ABORT] Cleared AbortController for session ${sessionId}`); } // Also clear the active run flag this.sessionActiveRunMap.delete(sessionId); // Clear the session start time this.sessionStartTimeMap.delete(sessionId); console.log(`[CONCURRENT] Session ${sessionId} marked as inactive`); } /** * Clear all AbortControllers */ clearAllAbortControllers() { this.sessionAbortControllerMap.clear(); console.log("[ABORT] All AbortControllers cleared"); } // ==================== PUSH STATE MANAGEMENT HELPERS ==================== /** * Generate a composite key for session+task combination * This ensures each task has its own push state, even within the same session */ getPushStateKey(sessionId, taskId) { return `${sessionId}:${taskId}`; } // ==================== END PUSH STATE MANAGEMENT HELPERS ==================== // ==================== 1-HOUR TASK TIMEOUT METHODS ==================== /** * Set task timeout time (from configuration) */ setTaskTimeout(timeoutMs) { this.taskTimeoutMs = timeoutMs; console.log(`[TASK TIMEOUT] Task timeout set to ${timeoutMs}ms`); } /** * Set a 1-hour task timeout timer for a session * @returns timeout ID */ setTaskTimeoutForSession(sessionId, taskId, callback) { this.clearTaskTimeoutForSession(sessionId); const timeoutId = setTimeout(() => { console.log(`[TASK TIMEOUT] ${this.taskTimeoutMs}ms timeout triggered for session ${sessionId}, task ${taskId}`); callback(sessionId, taskId); }, this.taskTimeoutMs); this.sessionTaskTimeoutMap.set(sessionId, timeoutId); console.log(`[TASK TIMEOUT] ${this.taskTimeoutMs}ms task timeout started for session ${sessionId}`); return timeoutId; } /** * Clear the task timeout timer for a session */ clearTaskTimeoutForSession(sessionId) { const timeoutId = this.sessionTaskTimeoutMap.get(sessionId); if (timeoutId) { clearTimeout(timeoutId); this.sessionTaskTimeoutMap.delete(sessionId); console.log(`[TASK TIMEOUT] Timeout cleared for session ${sessionId}`); } } /** * Check if session+task is waiting for push notification * @param sessionId - Session ID * @param taskId - Task ID (optional, for per-task tracking) */ isSessionWaitingForPush(sessionId, taskId) { const key = taskId ? this.getPushStateKey(sessionId, taskId) : sessionId; return this.sessionPushPendingMap.get(key) === true; } /** * Mark session+task as waiting for push notification * @param sessionId - Session ID * @param taskId - Task ID (optional, for per-task tracking) */ markSessionWaitingForPush(sessionId, taskId) { const key = taskId ? this.getPushStateKey(sessionId, taskId) : sessionId; this.sessionPushPendingMap.set(key, true); const taskInfo = taskId ? `, task ${taskId}` : ''; console.log(`[PUSH] Session ${sessionId}${taskInfo} marked as waiting for push`); } /** * Clear the waiting push state for a session+task * @param sessionId - Session ID * @param taskId - Task ID (optional, for per-task tracking) */ clearSessionWaitingForPush(sessionId, taskId) { const key = taskId ? this.getPushStateKey(sessionId, taskId) : sessionId; this.sessionPushPendingMap.delete(key); const taskInfo = taskId ? `, task ${taskId}` : ''; console.log(`[PUSH] Session ${sessionId}${taskInfo} cleared from waiting for push`); } /** * Clear all task timeout related state for a session */ clearTaskTimeoutState(sessionId) { this.clearTaskTimeoutForSession(sessionId); this.clearSessionWaitingForPush(sessionId); console.log(`[TASK TIMEOUT] All timeout state cleared for session ${sessionId}`); } } exports.XiaoYiRuntime = XiaoYiRuntime; // Maximum time a session can be active before we consider it stale (5 minutes) XiaoYiRuntime.SESSION_STALE_TIMEOUT_MS = 5 * 60 * 1000; // Global runtime instance - use global object to survive module reloads // CRITICAL: Use string key instead of Symbol to ensure consistency across module reloads const GLOBAL_KEY = '__xiaoyi_runtime_instance__'; function getXiaoYiRuntime() { const g = global; if (!g[GLOBAL_KEY]) { console.log("XiaoYi: Creating NEW runtime instance (global storage)"); g[GLOBAL_KEY] = new XiaoYiRuntime(); } else { console.log(`XiaoYi: Reusing EXISTING runtime instance: ${g[GLOBAL_KEY].getInstanceId()}`); } return g[GLOBAL_KEY]; } function setXiaoYiRuntime(runtime) { getXiaoYiRuntime().setPluginRuntime(runtime); }