UNPKG

@cloudbase/cloudbase-mcp

Version:

CloudBase MCP Server — operate Tencent CloudBase (database, auth, functions, storage, hosting) from AI coding tools via the Model Context Protocol. Part of CloudBase AI Toolkit.

1,191 lines (1,134 loc) 152 kB
export const __webpack_id__ = 341; export const __webpack_ids__ = [341]; export const __webpack_modules__ = { /***/ 2341: /***/ (function(__unused_webpack_module, exports, __webpack_require__) { var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.InteractiveServer = void 0; exports.getInteractiveServer = getInteractiveServer; exports.resetInteractiveServer = resetInteractiveServer; exports.getInteractiveServerSafe = getInteractiveServerSafe; const express_1 = __importDefault(__webpack_require__(2674)); const http_1 = __importDefault(__webpack_require__(3782)); const child_process_1 = __webpack_require__(5814); const open_1 = __importDefault(__webpack_require__(1622)); const ws_1 = __webpack_require__(6220); const index_js_1 = __webpack_require__(9529); const logger_js_1 = __webpack_require__(3039); function isVSCodeEnvironment() { return Boolean(process.env.VSCODE_IPC_HOOK_CLI || process.env.VSCODE_PID || process.env.TERM_PROGRAM === "vscode"); } /** * Validates that a URL is safe to open, blocking SSRF targets. * Only allows http/https schemes and rejects private/reserved IP ranges * and sensitive hostnames. */ function isUrlSafeToOpen(url) { let parsed; try { parsed = new URL(url); } catch { return { safe: false, reason: "Invalid URL format" }; } // Only allow http and https schemes if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { return { safe: false, reason: `Unsupported scheme: ${parsed.protocol}` }; } const hostname = parsed.hostname.toLowerCase(); // Block common sensitive / internal hostnames const blockedHostnames = [ "localhost", "localhost.localdomain", "ip6-localhost", "ip6-loopback", "metadata.google.internal", "metadata.internal", ]; if (blockedHostnames.includes(hostname)) { return { safe: false, reason: `Blocked hostname: ${hostname}` }; } // Block hostnames ending with .internal or .local if (hostname.endsWith(".internal") || hostname.endsWith(".local")) { return { safe: false, reason: `Blocked internal hostname: ${hostname}` }; } // Block IP addresses in private/reserved ranges (RFC 1918, loopback, link-local, etc.) const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; const match = ipv4Regex.exec(hostname); if (match) { const octets = [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10), parseInt(match[4], 10)]; // Loopback: 127.0.0.0/8 if (octets[0] === 127) { return { safe: false, reason: "Blocked loopback IP address" }; } // Link-local: 169.254.0.0/16 if (octets[0] === 169 && octets[1] === 254) { return { safe: false, reason: "Blocked link-local IP address" }; } // RFC 1918 private: 10.0.0.0/8 if (octets[0] === 10) { return { safe: false, reason: "Blocked private IP address (10.x.x.x)" }; } // RFC 1918 private: 172.16.0.0/12 if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) { return { safe: false, reason: "Blocked private IP address (172.16-31.x.x)" }; } // RFC 1918 private: 192.168.0.0/16 if (octets[0] === 192 && octets[1] === 168) { return { safe: false, reason: "Blocked private IP address (192.168.x.x)" }; } // Broadcast: 0.0.0.0 if (octets[0] === 0 && octets[1] === 0 && octets[2] === 0 && octets[3] === 0) { return { safe: false, reason: "Blocked broadcast address" }; } } // Block IPv6 loopback and link-local if (hostname.startsWith("[") && hostname.endsWith("]")) { const ipv6 = hostname.slice(1, -1).toLowerCase(); if (ipv6 === "::1" || ipv6 === "0:0:0:0:0:0:0:1" || ipv6 === "::" || ipv6 === "0:0:0:0:0:0:0:0") { return { safe: false, reason: "Blocked IPv6 loopback/reserved address" }; } if (ipv6.startsWith("fe80:")) { return { safe: false, reason: "Blocked IPv6 link-local address" }; } if (ipv6.startsWith("fc") || ipv6.startsWith("fd")) { return { safe: false, reason: "Blocked IPv6 unique-local address" }; } } return { safe: true }; } function parseBrowserCommand(browser) { const parts = browser.match(/(?:[^\s"]+|"[^"]*")+/g) || []; return parts.map((item) => item.replace(/^"(.*)"$/, "$1")); } async function openUrlByBrowserEnv(url) { const browser = process.env.BROWSER?.trim(); if (!browser) { return false; } const [command, ...args] = parseBrowserCommand(browser); if (!command) { return false; } const finalArgs = args.map((arg) => (arg === "%s" ? url : arg)); if (!args.includes("%s")) { finalArgs.push(url); } return new Promise((resolve) => { (0, child_process_1.execFile)(command, finalArgs, (execError) => { resolve(!execError); }); }); } function shouldUseBrowserEnvFallback() { return process.platform === "linux" && isVSCodeEnvironment(); } // Dynamic open behavior with IDE awareness and VSCode/Linux fallback async function openUrl(url, options, mcpServer) { // mcpServer 是 ExtendedMcpServer 实例,它有 server 和 ide 属性 // server 属性是 MCP server 的内部 server 实例,有 sendLoggingMessage 方法 const currentIde = mcpServer?.ide || process.env.INTEGRATION_IDE; const internalServer = mcpServer?.server; // 内部的 server 实例 (0, logger_js_1.debug)(`[openUrl] Checking IDE: ${currentIde}`); (0, logger_js_1.debug)(`[openUrl] mcpServer type: ${typeof mcpServer}, has server: ${!!mcpServer?.server}, has ide: ${!!mcpServer?.ide}`); if (internalServer) { (0, logger_js_1.debug)(`[openUrl] internalServer type: ${typeof internalServer}, has sendLoggingMessage: ${typeof internalServer.sendLoggingMessage === 'function'}`); } // 检查是否为 CodeBuddy IDE (优先使用 mcpServer.ide,回退到环境变量) if (currentIde === "CodeBuddy" && internalServer && typeof internalServer.sendLoggingMessage === 'function') { try { // internalServer 是 MCP server 的内部 server 实例,有 sendLoggingMessage 方法 internalServer.sendLoggingMessage({ level: "notice", data: { type: "tcb", url: url, }, }); (0, logger_js_1.info)(`CodeBuddy IDE: 已发送网页打开通知 - ${url}`); return; } catch (err) { (0, logger_js_1.error)(`Failed to send logging message for ${url}: ${err instanceof Error ? err.message : err}`, err instanceof Error ? err : new Error(String(err))); // 如果发送通知失败,在 CodeBuddy IDE 中不打开网页,直接返回 (0, logger_js_1.warn)(`CodeBuddy IDE: 发送通知失败,不打开网页 - ${url}`); return; } } // 默认行为:直接打开网页(带 VSCode/Linux 降级逻辑) (0, logger_js_1.debug)(`[openUrl] Opening URL in browser: ${url}`); try { const openOptions = options ? { url: true, ...options } : { url: true }; const child = await (0, open_1.default)(url, openOptions); if (child?.once) { child.once("error", async () => { if (shouldUseBrowserEnvFallback()) { (0, logger_js_1.debug)("[openUrl] open() failed, trying BROWSER fallback"); await openUrlByBrowserEnv(url); } }); } return; } catch (err) { (0, logger_js_1.error)(`Failed to open ${url} ${options} ${err instanceof Error ? err.message : err} `, err instanceof Error ? err : new Error(String(err))); if (shouldUseBrowserEnvFallback()) { (0, logger_js_1.debug)("[openUrl] open() threw, trying BROWSER fallback"); await openUrlByBrowserEnv(url); return; } (0, logger_js_1.warn)(`Please manually open: ${url}`); } } class InteractiveServer { app; server; wss; port = 0; isRunning = false; currentResolver = null; sessionData = new Map(); _mcpServer = null; // 保存 MCP server 实例引用 // 公共 getter 和 setter get mcpServer() { return this._mcpServer; } set mcpServer(server) { this._mcpServer = server; } DEFAULT_PORT = 3721; FALLBACK_PORTS = [ 3722, 3723, 3724, 3725, 3726, 3727, 3728, 3729, 3730, 3731, 3732, 3733, 3734, 3735, ]; /** Idle timeout for HTTP/WS connections (e.g. long login). Avoids "connection disconnected" after ~1 min. */ static SERVER_IDLE_MS = 30 * 60 * 1000; // 30 minutes /** WebSocket ping interval to keep connection alive past proxies/firewalls. */ static WS_PING_INTERVAL_MS = 25 * 1000; // 25 seconds constructor(mcpServer) { this._mcpServer = mcpServer; this.app = (0, express_1.default)(); this.server = http_1.default.createServer(this.app); this.applyServerTimeouts(); this.wss = new ws_1.WebSocketServer({ server: this.server }); this.setupExpress(); this.setupWebSocket(); process.on("exit", () => this.cleanup()); process.on("SIGINT", () => this.cleanup()); process.on("SIGTERM", () => this.cleanup()); } cleanup() { if (this.isRunning) { (0, logger_js_1.debug)("Cleaning up interactive server resources..."); this.server.close(); this.wss.close(); this.isRunning = false; } } /** Apply timeouts so long-lived login does not cause "connection disconnected". */ applyServerTimeouts() { this.server.timeout = 0; if (typeof this.server.keepAliveTimeout === "number") { this.server.keepAliveTimeout = InteractiveServer.SERVER_IDLE_MS; } if (typeof this.server.headersTimeout === "number") { this.server.headersTimeout = Math.max(this.server.headersTimeout || 0, InteractiveServer.SERVER_IDLE_MS); } } setupExpress() { this.app.use(express_1.default.json()); this.app.get("/env-setup/:sessionId", (req, res) => { const { sessionId } = req.params; const sessionData = this.sessionData.get(sessionId); if (!sessionData) { res.status(404).send("会话不存在或已过期"); return; } res.send(this.getEnvSetupHTML(sessionData.envs, sessionData.accountInfo, sessionData.errorContext, // Pass error context sessionId // Pass sessionId for retry functionality )); }); this.app.get("/clarification/:sessionId", (req, res) => { const { sessionId } = req.params; const sessionData = this.sessionData.get(sessionId); if (!sessionData) { res.status(404).send("会话不存在或已过期"); return; } res.send(this.getClarificationHTML(sessionData.message, sessionData.options)); }); this.app.post("/api/submit", (req, res) => { const { type, data } = req.body; (0, logger_js_1.debug)("Received submit request", { type, data }); if (this.currentResolver) { (0, logger_js_1.info)("Resolving with user data"); this.currentResolver({ type, data }); this.currentResolver = null; } else { (0, logger_js_1.warn)("No resolver waiting for response"); } res.json({ success: true }); }); this.app.post("/api/cancel", (req, res) => { (0, logger_js_1.info)("Received cancel request"); if (this.currentResolver) { (0, logger_js_1.info)("Resolving with cancelled status"); this.currentResolver({ type: "clarification", data: null, cancelled: true, }); this.currentResolver = null; } else { (0, logger_js_1.warn)("No resolver waiting for cancellation"); } res.json({ success: true }); }); this.app.post("/api/switch", (req, res) => { (0, logger_js_1.info)("Received switch account request"); if (this.currentResolver) { (0, logger_js_1.info)("Resolving with switch status"); this.currentResolver({ type: "envId", data: null, switch: true, }); this.currentResolver = null; } else { (0, logger_js_1.warn)("No resolver waiting for switch"); } res.json({ success: true }); }); this.app.post("/api/retry-init-tcb", (req, res) => { const { sessionId } = req.body; (0, logger_js_1.info)("Received retry InitTcb request", { sessionId }); // Mark session for retry const sessionData = this.sessionData.get(sessionId); if (sessionData) { sessionData.retryInitTcb = true; (0, logger_js_1.debug)("Marked session for InitTcb retry", { sessionId }); } res.json({ success: true, message: "重试请求已提交,页面将刷新" }); }); // Universal URL opener API - ensures proper handling in different IDEs (e.g., CodeBuddy) this.app.post("/api/open-url", async (req, res) => { const { url } = req.body; if (!url) { res.status(400).json({ success: false, error: "URL is required" }); return; } if (typeof url !== "string") { res.status(400).json({ success: false, error: "URL must be a string" }); return; } // Validate URL to prevent SSRF const validation = isUrlSafeToOpen(url); if (!validation.safe) { (0, logger_js_1.warn)(`Blocked open-url request for unsafe URL: ${url} (${validation.reason})`); res.status(403).json({ success: false, error: `URL not allowed: ${validation.reason}` }); return; } (0, logger_js_1.info)("Received open URL request", { url }); try { // Pass mcpServer directly - it's an ExtendedMcpServer instance with server property await openUrl(url, undefined, this._mcpServer); res.json({ success: true }); } catch (err) { (0, logger_js_1.error)("Failed to open URL", err instanceof Error ? err : new Error(String(err))); res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) }); } }); } setupWebSocket() { this.wss.on("connection", (ws) => { (0, logger_js_1.debug)("WebSocket client connected"); // Keep connection alive during long login so proxies/firewalls do not close it const pingInterval = setInterval(() => { if (ws.readyState === ws_1.WebSocket.OPEN) { ws.ping(); } }, InteractiveServer.WS_PING_INTERVAL_MS); ws.on("close", () => { clearInterval(pingInterval); (0, logger_js_1.debug)("WebSocket client disconnected"); }); ws.on("message", async (message) => { try { const data = JSON.parse(message.toString()); (0, logger_js_1.debug)("WebSocket message received", data); // Handle session registration if (data.type === 'registerSession' && data.sessionId) { (0, logger_js_1.debug)("Registering WebSocket for session:", data.sessionId); const sessionData = this.sessionData.get(data.sessionId); if (sessionData) { sessionData.ws = ws; (0, logger_js_1.debug)("WebSocket registered successfully for session:", data.sessionId); } else { (0, logger_js_1.debug)("Session not found:", data.sessionId); } return; } // Handle refresh environment list request if (data.type === 'refreshEnvList') { (0, logger_js_1.debug)("Handling refreshEnvList request"); try { // Find the session ID for this WebSocket connection let targetSessionId = null; for (const [sessionId, sessionData] of this.sessionData.entries()) { if (sessionData.ws === ws) { targetSessionId = sessionId; break; } } if (targetSessionId) { const sessionData = this.sessionData.get(targetSessionId); if (sessionData && sessionData.manager) { // Re-fetch environment list using the same API and parameters as initial query let envResult; try { // Use DescribeEnvs with filter parameters (same as initial query) const queryParams = { EnvTypes: ["weda", "baas"], IsVisible: false, Channels: ["dcloud", "iotenable", "tem", "scene_module"], }; envResult = await sessionData.manager.commonService("tcb", "2018-06-08").call({ Action: "DescribeEnvs", Param: queryParams, }); // Transform response format to match original listEnvs() format if (envResult && envResult.EnvList) { envResult = { EnvList: envResult.EnvList }; } else if (envResult && envResult.Data && envResult.Data.EnvList) { envResult = { EnvList: envResult.Data.EnvList }; } else { // Fallback to listEnvs if format is unexpected (0, logger_js_1.debug)("Unexpected response format, falling back to listEnvs()"); envResult = await sessionData.manager.env.listEnvs(); } } catch (error) { (0, logger_js_1.debug)("DescribeEnvs failed, falling back to listEnvs():", error instanceof Error ? error : new Error(String(error))); // Fallback to original method on error envResult = await sessionData.manager.env.listEnvs(); } const envs = envResult?.EnvList || []; // Update session data sessionData.envs = envs; // Send updated environment list to client ws.send(JSON.stringify({ type: 'envListRefreshed', envs: envs, success: true })); (0, logger_js_1.info)(`Environment list refreshed, found ${envs.length} environments`); } else { ws.send(JSON.stringify({ type: 'envListRefreshed', success: false, error: '无法获取环境管理器' })); } } else { ws.send(JSON.stringify({ type: 'envListRefreshed', success: false, error: '会话不存在' })); } } catch (err) { (0, logger_js_1.error)("Failed to refresh environment list", err instanceof Error ? err : new Error(String(err))); ws.send(JSON.stringify({ type: 'envListRefreshed', success: false, error: err instanceof Error ? err.message : '刷新失败' })); } return; } if (this.currentResolver) { this.currentResolver(data); this.currentResolver = null; } } catch (err) { (0, logger_js_1.error)("WebSocket message parsing error", err instanceof Error ? err : new Error(String(err))); } }); }); } async start() { if (this.isRunning) { (0, logger_js_1.debug)(`Interactive server already running on port ${this.port}`); return this.port; } return new Promise((resolve, reject) => { (0, logger_js_1.info)("Starting interactive server..."); const tryPorts = [this.DEFAULT_PORT, ...this.FALLBACK_PORTS]; let currentIndex = 0; const tryNextPort = () => { if (currentIndex >= tryPorts.length) { const err = new Error(`All ${tryPorts.length} ports are in use (${tryPorts.join(", ")}), failed to start server`); (0, logger_js_1.error)("Server start failed", err); reject(err); return; } const portToTry = tryPorts[currentIndex]; currentIndex++; (0, logger_js_1.debug)(`Trying to start server on port ${portToTry} (attempt ${currentIndex}/${tryPorts.length})`); tryPort(portToTry); }; const tryPort = (portToTry) => { // 清除之前的所有监听器 this.server.removeAllListeners("error"); this.server.removeAllListeners("listening"); // 设置错误处理 const errorHandler = (err) => { if (err.code === "EADDRINUSE") { (0, logger_js_1.warn)(`Port ${portToTry} is in use, trying next port...`); // 清理当前尝试 this.server.removeAllListeners("error"); this.server.removeAllListeners("listening"); tryNextPort(); } else { (0, logger_js_1.error)("Server error", err); reject(err); } }; // Host: default 127.0.0.1 for security (prevents SSRF from remote access); set INTERACTIVE_SERVER_HOST=0.0.0.0 to allow remote connections const host = process.env.INTERACTIVE_SERVER_HOST ?? "127.0.0.1"; // 设置成功监听处理 const listeningHandler = () => { const address = this.server.address(); if (address && typeof address === "object") { this.port = address.port; this.isRunning = true; (0, logger_js_1.info)(`Interactive server started successfully on http://${host}:${this.port}`); // 移除临时监听器 this.server.removeListener("error", errorHandler); this.server.removeListener("listening", listeningHandler); resolve(this.port); } else { const err = new Error("Failed to get server address"); (0, logger_js_1.error)("Server start error", err); reject(err); } }; this.server.once("error", errorHandler); this.server.once("listening", listeningHandler); try { this.server.listen(portToTry, host); } catch (err) { (0, logger_js_1.error)(`Failed to bind to port ${portToTry}:`, err instanceof Error ? err : new Error(String(err))); tryNextPort(); } }; tryNextPort(); }); } async stop() { if (!this.isRunning) { (0, logger_js_1.debug)("Interactive server is not running, nothing to stop"); return; } (0, logger_js_1.info)("Stopping interactive server..."); return new Promise((resolve, reject) => { // 设置超时,防止无限等待 const timeout = setTimeout(() => { (0, logger_js_1.warn)("Server close timeout, forcing cleanup"); this.isRunning = false; this.port = 0; resolve(); }, 30000); try { // 首先关闭WebSocket服务器,等待其完全关闭 this.wss.close(() => { (0, logger_js_1.debug)("WebSocket server closed"); // WebSocket关闭后,再关闭HTTP服务器 this.server.close((err) => { clearTimeout(timeout); if (err) { (0, logger_js_1.error)("Error closing server:", err); reject(err); } else { (0, logger_js_1.info)("Interactive server stopped successfully"); this.isRunning = false; this.port = 0; // 重新创建整个服务器实例以便下次使用 this.server = http_1.default.createServer(this.app); this.applyServerTimeouts(); this.wss = new ws_1.WebSocketServer({ server: this.server }); this.setupWebSocket(); (0, logger_js_1.debug)("HTTP and WebSocket servers recreated for next use"); resolve(); } }); }); } catch (err) { clearTimeout(timeout); (0, logger_js_1.error)("Error stopping server:", err instanceof Error ? err : new Error(String(err))); this.isRunning = false; this.port = 0; reject(err); } }); } async collectEnvId(availableEnvs, accountInfo, errorContext, // EnvSetupContext manager, // CloudBase manager instance for refreshing env list mcpServer) { try { // CRITICAL: Clean up any existing unresolved request to prevent hanging if (this.currentResolver) { (0, logger_js_1.warn)("[collectEnvId] Found existing unresolved request, cleaning up..."); const oldResolver = this.currentResolver; this.currentResolver = null; // Resolve the old request as cancelled to prevent it from hanging forever oldResolver({ type: "envId", data: null, cancelled: true }); } // Update mcpServer if provided, or use existing this._mcpServer as fallback (0, logger_js_1.debug)(`[collectEnvId] Received mcpServer parameter: type=${typeof mcpServer}, is null=${mcpServer === null}, is undefined=${mcpServer === undefined}, has server=${!!mcpServer?.server}, has ide=${!!mcpServer?.ide}`); (0, logger_js_1.debug)(`[collectEnvId] Current this._mcpServer before update: type=${typeof this._mcpServer}, is null=${this._mcpServer === null}, is undefined=${this._mcpServer === undefined}, has server=${!!this._mcpServer?.server}, has ide=${!!this._mcpServer?.ide}`); // Use mcpServer parameter if provided, otherwise keep existing this._mcpServer const effectiveMcpServer = mcpServer || this._mcpServer; if (mcpServer) { this._mcpServer = mcpServer; (0, logger_js_1.debug)(`[collectEnvId] Updated mcpServer from parameter, has server: ${!!mcpServer?.server}, has ide: ${!!mcpServer?.ide}`); } else if (this._mcpServer) { (0, logger_js_1.debug)(`[collectEnvId] mcpServer parameter is falsy, using existing this._mcpServer`); } else { (0, logger_js_1.warn)(`[collectEnvId] WARNING: Both mcpServer parameter and this._mcpServer are undefined! IDE detection will fail.`); } (0, logger_js_1.debug)(`[collectEnvId] Effective mcpServer: type=${typeof effectiveMcpServer}, has server=${!!effectiveMcpServer?.server}, has ide=${!!effectiveMcpServer?.ide}`); (0, logger_js_1.info)("Starting environment ID collection..."); (0, logger_js_1.debug)(`Available environments: ${availableEnvs.length}`); (0, logger_js_1.debug)(`Account info:`, accountInfo); (0, logger_js_1.debug)(`Error context:`, { hasInitTcbError: !!errorContext?.initTcbError, hasCreateEnvError: !!errorContext?.createEnvError, initTcbError: errorContext?.initTcbError, createEnvError: errorContext?.createEnvError }); const port = await this.start(); const sessionId = Math.random().toString(36).substring(2, 15); this.sessionData.set(sessionId, { envs: availableEnvs, accountInfo, errorContext, // Store error context manager, // Store manager for refreshing env list ws: null // Will be set when WebSocket connects }); (0, logger_js_1.debug)(`Created session: ${sessionId}`); setTimeout(() => { this.sessionData.delete(sessionId); (0, logger_js_1.debug)(`Session ${sessionId} expired`); }, 5 * 60 * 1000); const url = `http://localhost:${port}/env-setup/${sessionId}`; (0, logger_js_1.info)(`Opening browser: ${url}`); // Check if this is CodeBuddy IDE and notification was sent (no browser opened) const isCodeBuddy = effectiveMcpServer?.ide === "CodeBuddy" || process.env.INTEGRATION_IDE === "CodeBuddy"; let notificationSent = false; try { // Use effectiveMcpServer (from parameter or this._mcpServer) for openUrl (0, logger_js_1.debug)(`[collectEnvId] Using effectiveMcpServer for openUrl: type=${typeof effectiveMcpServer}, has server=${!!effectiveMcpServer?.server}, has ide=${!!effectiveMcpServer?.ide}`); (0, logger_js_1.debug)(`[collectEnvId] effectiveMcpServer keys: ${effectiveMcpServer ? Object.keys(effectiveMcpServer).slice(0, 10).join(', ') : 'null'}`); // Store the original openUrl to check if it returns early (notification sent) const openUrlResult = await openUrl(url, { wait: false }, effectiveMcpServer); if (isCodeBuddy && openUrlResult === undefined) { notificationSent = true; (0, logger_js_1.debug)("[collectEnvId] CodeBuddy notification sent, no browser opened"); } (0, logger_js_1.info)("Browser opened successfully"); } catch (browserError) { (0, logger_js_1.error)("Failed to open browser", browserError instanceof Error ? browserError : new Error(String(browserError))); (0, logger_js_1.warn)(`Please manually open: ${url}`); } (0, logger_js_1.info)("Waiting for user selection..."); // Use same 10 minutes for all IDEs so long login (re-auth, switch account) does not close the server const timeoutDuration = 10 * 60 * 1000; (0, logger_js_1.debug)(`[collectEnvId] Using timeout duration: ${timeoutDuration / 1000} seconds`); return new Promise((resolve) => { this.currentResolver = (result) => { // 用户选择完成后,关闭服务器 this.currentResolver = null; this.stop().catch((err) => { (0, logger_js_1.debug)("Error stopping server after user selection:", err); }); resolve(result); }; const timeoutId = setTimeout(() => { if (this.currentResolver) { (0, logger_js_1.warn)(`Request timeout after ${timeoutDuration / 1000} seconds, resolving with cancelled`); this.currentResolver = null; // 超时后也关闭服务器 this.stop().catch((err) => { (0, logger_js_1.debug)("Error stopping server after timeout:", err); }); resolve({ type: "envId", data: null, cancelled: true, timeout: true, timeoutDuration }); } }, timeoutDuration); // Store timeout ID so we can clear it if resolved early // Note: We can't clear it here, but the timeout will be cleared when the promise resolves // The timeout will be automatically cleared when the promise resolves or rejects }); } catch (err) { // Clean up currentResolver on error if (this.currentResolver) { this.currentResolver = null; } (0, logger_js_1.error)("Error in collectEnvId", err instanceof Error ? err : new Error(String(err))); throw err; } } async clarifyRequest(message, options) { const port = await this.start(); // 生成会话ID并存储数据 const sessionId = Math.random().toString(36).substring(2, 15); this.sessionData.set(sessionId, { message, options }); // 设置会话过期时间(5分钟) setTimeout(() => { this.sessionData.delete(sessionId); }, 5 * 60 * 1000); const url = `http://localhost:${port}/clarification/${sessionId}`; // Pass mcpServer directly - it's an ExtendedMcpServer instance with server property await openUrl(url, undefined, this._mcpServer); return new Promise((resolve) => { this.currentResolver = (result) => { // 用户选择完成后,关闭服务器 this.stop().catch((err) => { (0, logger_js_1.debug)("Error stopping server after user selection:", err); }); resolve(result); }; }); } escapeHtml(text) { if (!text) return ''; return text .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#039;'); } getEnvSetupHTML(envs, accountInfo, errorContext, // EnvSetupContext sessionId) { // Extract error information const initTcbError = errorContext?.initTcbError; const createEnvError = errorContext?.createEnvError; (0, logger_js_1.debug)("getEnvSetupHTML called with:", { envCount: envs?.length || 0, hasInitTcbError: !!initTcbError, hasCreateEnvError: !!createEnvError, hasAccountInfo: !!accountInfo?.uin, sessionId }); // Get current IDE information const currentIde = this._mcpServer?.ide || process.env.INTEGRATION_IDE; // Use new template system return (0, index_js_1.renderEnvSetupPage)({ envs, accountInfo, errorContext, sessionId, wsPort: this.port, ide: currentIde }); } // Keep the old implementation for reference (can be removed later) getEnvSetupHTML_OLD(envs, accountInfo, errorContext, sessionId) { const accountDisplay = accountInfo?.uin ? `UIN: ${accountInfo.uin}` : ""; const hasAccountInfo = !!accountInfo?.uin; const hasEnvs = (envs || []).length > 0; // Extract error information const initTcbError = errorContext?.initTcbError; const createEnvError = errorContext?.createEnvError; const hasErrors = !!(initTcbError || createEnvError); const hasInitError = !!initTcbError; return ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>CloudBase AI Toolkit - 环境配置</title> <style> @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap'); * { margin: 0; padding: 0; box-sizing: border-box; } :root { --primary-color: #1a1a1a; --primary-hover: #000000; --accent-color: #67E9E9; --accent-hover: #2BCCCC; --text-primary: #ffffff; --text-secondary: #a0a0a0; --border-color: rgba(255, 255, 255, 0.15); --bg-secondary: rgba(255, 255, 255, 0.08); --bg-glass: rgba(26, 26, 26, 0.95); --shadow: 0 25px 50px rgba(0, 0, 0, 0.3), 0 10px 20px rgba(0, 0, 0, 0.2); --font-mono: 'JetBrains Mono', 'SF Mono', 'Monaco', monospace; --header-bg: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 50%, #0d1117 100%); } body { font-family: var(--font-mono); background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; position: relative; overflow-x: hidden; overflow-y: auto; } /* Custom scrollbar styles */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); border-radius: 4px; } ::-webkit-scrollbar-thumb { background: var(--accent-color); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: var(--accent-hover); } body::before { content: ''; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse"><path d="M 20 0 L 0 0 0 20" fill="none" stroke="rgba(255,255,255,0.02)" stroke-width="1"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>') repeat; pointer-events: none; z-index: -1; } body::after { content: ''; position: fixed; top: 50%; left: 50%; width: 500px; height: 500px; background: radial-gradient(circle, rgba(103, 233, 233, 0.05) 0%, transparent 70%); transform: translate(-50%, -50%); pointer-events: none; z-index: -1; animation: pulse 8s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 0.3; transform: translate(-50%, -50%) scale(1); } 50% { opacity: 0.6; transform: translate(-50%, -50%) scale(1.1); } } .modal { background: var(--bg-glass); backdrop-filter: blur(20px); border-radius: 20px; box-shadow: var(--shadow); border: 2px solid var(--border-color); width: 100%; max-width: 520px; overflow: hidden; animation: modalIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); position: relative; } .modal::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.02) 50%, transparent 70%); animation: shimmer 3s infinite; pointer-events: none; } @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes modalIn { from { opacity: 0; transform: scale(0.9) translateY(-20px); } to { opacity: 1; transform: scale(1) translateY(0); } } .header { background: var(--header-bg); color: var(--text-primary); padding: 24px 28px; display: flex; align-items: center; justify-content: space-between; position: relative; overflow: hidden; } .header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.03) 50%, transparent 70%); animation: headerShimmer 4s infinite; pointer-events: none; } @keyframes headerShimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } .header-left { display: flex; align-items: center; gap: 16px; z-index: 1; } .logo { width: 32px; height: 32px; filter: drop-shadow(0 4px 8px rgba(0,0,0,0.2)); animation: logoFloat 3s ease-in-out infinite; } @keyframes logoFloat { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-3px); } } .title { font-size: 20px; font-weight: 700; text-shadow: 0 2px 4px rgba(0,0,0,0.1); } .github-link { color: var(--text-primary); text-decoration: none; display: flex; align-items: center; gap: 8px; font-size: 14px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255, 255, 255, 0.12); backdrop-filter: blur(10px); padding: 8px 16px; border-radius: 8px; font-weight: 500; z-index: 1; transition: all 0.3s ease; } .github-link:hover { background: rgba(255,255,255,0.15); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); } .content { padding: 32px 24px; position: relative; } .content-title { font-size: 24px; font-weight: 700; color: var(--text-primary); margin-bottom: 8px; animation: fadeInUp 0.8s ease-out 0.2s both; } .content-subtitle { color: var(--text-secondary); margin-bottom: 16px; line-height: 1.5; animation: fadeInUp 0.8s ease-out 0.4s both; } .account-bar { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; background: rgba(103, 233, 233, 0.06); border: 1px solid rgba(103, 233, 233, 0.15); border-radius: 8px; margin-bottom: 20px; animation: fadeInUp 0.8s ease-out 0.5s both; } .account-info { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--accent-color); } .account-info svg { flex-shrink: 0; opacity: 0.8; } .account-info span { font-family: var(--font-mono); } .btn-switch { display: flex; align-items: center; gap: 6px; padding: 6px 12px; font-size: 12px; background: transparent; border: 1px solid rgba(103, 233, 233, 0.3); border-radius: 6px; color: var(--accent-color); cursor: pointer; transition: all 0.2s ease; font-family: var(--font-mono); } .btn-switch:hover { background: rgba(103, 233, 233, 0.1); border-color: var(--accent-color); } .btn-switch svg { flex-shrink: 0; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .env-list { border: 1px solid var(--border-color); border-radius: 12px; margin-bottom: 24px; max-height: 300px; overflow-y: auto; overflow-x: hidden; background: rgba(255, 255, 255, 0.03); animation: fadeInUp 0.8s ease-out 0.6s both; } .env-item { padding: 16px 20px; border-bottom: 1px solid var(--border-color); cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; gap: 14px; position: relative; overflow: hidden; color: var(--text-primary); } .env-item::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 0; background: var(--accent-color); transition: width 0.3s ease; } .env-item:last-child { border-bottom: none; } .env-item:hover { background: var(--bg-secondary); transform: translateX(5px); } .env-item:hover::before { width: 4px; } .env-item.selected { background: rgba(103, 233, 233, 0.1); border-left: 4px solid var(--accent-color); transform: translateX(5px); } .env-icon { width: 20px; height: 20px; color: var(--accent-color); flex-shrink: 0; animation: iconGlow 2s ease-in-out infinite; } @keyframes iconGlow { 0%, 100% { filter: drop-shadow(0 0 2px rgba(103, 233, 233, 0.3)); } 50% { filter: drop-shadow(0 0 8px rgba(103, 233, 233, 0.6)); } } .env-info { flex: 1; } .env-name { font-weight: 600; color: var(--text-primary); margin-bottom: 4px; } .env-alias { color: var(--text-secondary); font-size: 14px; } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; text-align: center; animation: fadeIn 0.8s ease-out; } .empty-icon { margin-bottom: 24px; color: var(--text-secondary); opacity: 0.6; } .empty-title { font-size: 20px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px; } .empty-message { font-size: 14px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 32px; max-width: 400px; } .error-banner { margin-bottom: 24px; padding: 16px; background: rgba(255, 193, 7, 0.1); border: 1px solid rgba(255, 193, 7, 0.3); border-radius: 12px; animation: fadeIn 0.5s ease-out; } .error-item { margin-bottom: 16px; } .error-item:last-child { margin-bottom: 0; } .error-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; color: #ffc107;