@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
JavaScript
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
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;