openclaw-home-pc/openclaw/extensions/xiaoyi/dist/runtime.js
2026-03-24 04:00:48 +08:00

439 lines
17 KiB
JavaScript

"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);
}