diff --git a/.env b/.env index 9b757ea..ae1bf42 100644 --- a/.env +++ b/.env @@ -1,6 +1,13 @@ # local/docker -DEPLOYMENT_MODE=docker +DEPLOYMENT_MODE=local RDP_SERVICE_URL_DOCKER=ws://rdp:8081 - RDP_SERVICE_URL_LOCAL=ws://localhost:8081 +VNC_SERVICE_URL_DOCKER=ws://vnc:8082 +VNC_SERVICE_URL_LOCAL=ws://localhost:8082 + +# Backend API Base URLs +RDP_BACKEND_API_BASE_DOCKER=http://nexus-rdp:9090 +RDP_BACKEND_API_BASE_LOCAL=http://localhost:9090 +VNC_BACKEND_API_BASE_DOCKER=http://nexus-vnc:9091 +VNC_BACKEND_API_BASE_LOCAL=http://localhost:9091 diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a7daa95..bc9ae36 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,10 +1,40 @@ +import dotenv from 'dotenv'; +import path from 'path'; +import fs from 'fs'; // fs is needed for early env loading if data/.env is checked + +// --- 开始环境变量的早期加载 --- +// 1. 加载根目录的 .env 文件 (定义部署模式等) +// 注意: __dirname 在 dist/src 中,所以需要回退三级到项目根目录 +const projectRootEnvPath = path.resolve(__dirname, '../../../.env'); +const rootConfigResult = dotenv.config({ path: projectRootEnvPath }); + +if (rootConfigResult.error && (rootConfigResult.error as NodeJS.ErrnoException).code !== 'ENOENT') { + console.warn(`[ENV Init Early] Warning: Could not load root .env file from ${projectRootEnvPath}. Error: ${rootConfigResult.error.message}`); +} else if (!rootConfigResult.error) { + console.log(`[ENV Init Early] Loaded environment variables from root .env file: ${projectRootEnvPath}`); +} else { + console.log(`[ENV Init Early] Root .env file not found at ${projectRootEnvPath}, proceeding without it.`); +} + +// 2. 加载 data/.env 文件 (定义密钥等) +// 注意: 这个路径是相对于编译后的 dist/src/index.js +const dataEnvPathGlobal = path.resolve(__dirname, '../data/.env'); // Renamed to avoid conflict if 'dataEnvPath' is used later +const dataConfigResultGlobal = dotenv.config({ path: dataEnvPathGlobal }); // Renamed + +if (dataConfigResultGlobal.error && (dataConfigResultGlobal.error as NodeJS.ErrnoException).code !== 'ENOENT') { + console.warn(`[ENV Init Early] Warning: Could not load data .env file from ${dataEnvPathGlobal}. Error: ${dataConfigResultGlobal.error.message}`); +} else if (!dataConfigResultGlobal.error) { + console.log(`[ENV Init Early] Loaded environment variables from data .env file: ${dataEnvPathGlobal}`); +} +// --- 结束环境变量的早期加载 --- + import express = require('express'); import { Request, Response, NextFunction, RequestHandler } from 'express'; import http from 'http'; -import fs from 'fs'; -import path from 'path'; +// import fs from 'fs'; // Moved up +// import path from 'path'; // Moved up import crypto from 'crypto'; -import dotenv from 'dotenv'; +// import dotenv from 'dotenv'; // Moved up import session from 'express-session'; import sessionFileStore from 'session-file-store'; import { getDbInstance } from './database/connection'; @@ -54,38 +84,16 @@ process.on('unhandledRejection', (reason: any, promise: Promise) => { const initializeEnvironment = async () => { - // 1. 加载根目录的 .env 文件 (定义部署模式等) - // 注意: __dirname 在 dist/src 中,所以需要回退三级到项目根目录 - const projectRootEnvPath = path.resolve(__dirname, '../../../.env'); - const rootConfigResult = dotenv.config({ path: projectRootEnvPath }); - // Use type assertion for error code checking - if (rootConfigResult.error && (rootConfigResult.error as NodeJS.ErrnoException).code !== 'ENOENT') { - // 只在文件存在但无法加载时发出警告 - console.warn(`[ENV Init] Warning: Could not load root .env file from ${projectRootEnvPath}. Error: ${rootConfigResult.error.message}`); - } else if (!rootConfigResult.error) { - console.log(`[ENV Init] Loaded environment variables from root .env file: ${projectRootEnvPath}`); - } else { - console.log(`[ENV Init] Root .env file not found at ${projectRootEnvPath}, proceeding without it (expected in non-local deployments where env vars are injected).`); - } + // Env files (root and data/.env) are now loaded at the very top of the file. + // This function will now focus on generating keys if they are missing + // and setting defaults for GUACD variables. - // 2. 加载 data/.env 文件 (定义密钥等) - // 注意: 这个路径是相对于编译后的 dist/src/index.js - const dataEnvPath = path.resolve(__dirname, '../data/.env'); + // Use the globally defined path for data .env + const dataEnvPath = dataEnvPathGlobal; // Use the path defined at the top let keysGenerated = false; let keysToAppend = ''; - // dotenv.config 默认不会覆盖已存在的 process.env 变量 - // 这意味着如果根 .env 和 data/.env 定义了相同的变量,先加载的(根 .env)的值会优先 - const dataConfigResult = dotenv.config({ path: dataEnvPath }); - // Use type assertion for error code checking - if (dataConfigResult.error && (dataConfigResult.error as NodeJS.ErrnoException).code !== 'ENOENT') { - // 只在文件存在但无法加载时发出警告,文件不存在是正常情况 - console.warn(`[ENV Init] Warning: Could not load data .env file from ${dataEnvPath}. Error: ${dataConfigResult.error.message}`); - } else if (!dataConfigResult.error) { - console.log(`[ENV Init] Loaded environment variables from data .env file: ${dataEnvPath}`); - } - - // 2. 检查 ENCRYPTION_KEY + // 检查 ENCRYPTION_KEY (process.env should be populated by early loading) if (!process.env.ENCRYPTION_KEY) { console.log('[ENV Init] ENCRYPTION_KEY 未设置,正在生成...'); const newEncryptionKey = crypto.randomBytes(32).toString('hex'); diff --git a/packages/backend/src/services/guacamole.service.ts b/packages/backend/src/services/guacamole.service.ts index e5b125b..a324c12 100644 --- a/packages/backend/src/services/guacamole.service.ts +++ b/packages/backend/src/services/guacamole.service.ts @@ -1,11 +1,25 @@ import axios from 'axios'; import { ConnectionWithTags } from '../types/connection.types'; -// RDP 后端服务的 Base URL,从环境变量读取,提供默认值 -const RDP_BACKEND_API_BASE = process.env.RDP_BACKEND_API_BASE || 'http://nexus-rdp:9090'; // 假设 RDP 服务名为 nexus-rdp +// RDP 后端服务的 Base URL +const RDP_BACKEND_API_BASE = process.env.DEPLOYMENT_MODE === 'local' + ? (process.env.RDP_BACKEND_API_BASE_LOCAL || 'http://localhost:9090') + : (process.env.RDP_BACKEND_API_BASE_DOCKER || 'http://nexus-rdp:9090'); -// VNC 后端服务的 Base URL,从环境变量读取,提供默认值 -const VNC_BACKEND_API_BASE = process.env.VNC_BACKEND_API_BASE || 'http://nexus-vnc:9091'; // 假设 VNC 服务名为 nexus-vnc,端口为 9091 +console.log(`[GuacamoleService] DEPLOYMENT_MODE: ${process.env.DEPLOYMENT_MODE}`); +console.log(`[GuacamoleService] RDP_BACKEND_API_BASE_LOCAL: ${process.env.RDP_BACKEND_API_BASE_LOCAL}`); +console.log(`[GuacamoleService] RDP_BACKEND_API_BASE_DOCKER: ${process.env.RDP_BACKEND_API_BASE_DOCKER}`); +console.log(`[GuacamoleService] Using RDP Backend API Base: ${RDP_BACKEND_API_BASE}`); + + +// VNC 后端服务的 Base URL +const VNC_BACKEND_API_BASE = process.env.DEPLOYMENT_MODE === 'local' + ? (process.env.VNC_BACKEND_API_BASE_LOCAL || 'http://localhost:9091') + : (process.env.VNC_BACKEND_API_BASE_DOCKER || 'http://nexus-vnc:9091'); + +console.log(`[GuacamoleService] VNC_BACKEND_API_BASE_LOCAL: ${process.env.VNC_BACKEND_API_BASE_LOCAL}`); +console.log(`[GuacamoleService] VNC_BACKEND_API_BASE_DOCKER: ${process.env.VNC_BACKEND_API_BASE_DOCKER}`); +console.log(`[GuacamoleService] Using VNC Backend API Base: ${VNC_BACKEND_API_BASE}`); /** * 从 RDP 后端服务获取 Guacamole 令牌 @@ -107,4 +121,4 @@ export const getVncToken = async (connection: ConnectionWithTags, decryptedPassw } throw new Error(`调用 VNC 后端服务时发生错误: ${error.message}`); } -}; \ No newline at end of file +}; diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue index 6b3d174..c25139a 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -20,6 +20,8 @@ import StyleCustomizer from './components/StyleCustomizer.vue'; import FocusSwitcherConfigurator from './components/FocusSwitcherConfigurator.vue'; // +++ 导入 RDP 模态框组件 +++ import RemoteDesktopModal from './components/RemoteDesktopModal.vue'; +// +++ 导入 VNC 模态框组件 +++ +import VncModal from './components/VncModal.vue'; const { t } = useI18n(); const authStore = useAuthStore(); @@ -33,7 +35,7 @@ const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); const { isStyleCustomizerVisible } = storeToRefs(appearanceStore); const { isLayoutVisible, isHeaderVisible } = storeToRefs(layoutStore); // 添加 isHeaderVisible const { isConfiguratorVisible: isFocusSwitcherVisible } = storeToRefs(focusSwitcherStore); -const { isRdpModalOpen, rdpConnectionInfo } = storeToRefs(sessionStore); // +++ 获取 RDP 状态 +++ +const { isRdpModalOpen, rdpConnectionInfo, isVncModalOpen, vncConnectionInfo } = storeToRefs(sessionStore); // +++ 获取 RDP 和 VNC 状态 +++ const breakpoints = useBreakpoints(breakpointsTailwind); // +++ Initialize Breakpoints +++ const isMobile = breakpoints.smaller('md'); // +++ Define isMobile +++ @@ -319,6 +321,13 @@ const isElementVisibleAndFocusable = (element: HTMLElement): boolean => { @close="sessionStore.closeRdpModal()" /> + + + diff --git a/packages/frontend/src/components/LayoutRenderer.vue b/packages/frontend/src/components/LayoutRenderer.vue index 7529e4c..19ce568 100644 --- a/packages/frontend/src/components/LayoutRenderer.vue +++ b/packages/frontend/src/components/LayoutRenderer.vue @@ -287,10 +287,7 @@ const sidebarProps = computed(() => (paneName: PaneName | null, side: 'left' | ' return { ...baseProps, // Event forwarding - onConnectRequest: (id: number) => { - console.log(`[LayoutRenderer Sidebar] Forwarding 'connect-request' for ID: ${id}`); - emit('connect-request', id); - }, + onConnectRequest: (id: number) => emit('connect-request', id), onOpenNewSession: (id: number) => { console.log(`[LayoutRenderer Sidebar] Forwarding 'open-new-session' for ID: ${id}`); emit('open-new-session', id); diff --git a/packages/frontend/src/components/RemoteDesktopModal.vue b/packages/frontend/src/components/RemoteDesktopModal.vue index f96e759..e80b0a0 100644 --- a/packages/frontend/src/components/RemoteDesktopModal.vue +++ b/packages/frontend/src/components/RemoteDesktopModal.vue @@ -432,7 +432,6 @@ watchEffect(() => { desiredModalHeight.value = finalHeight; }); - onMounted(() => { // 初始尺寸加载现在由 watchEffect 处理 diff --git a/packages/frontend/src/components/VncModal.vue b/packages/frontend/src/components/VncModal.vue new file mode 100644 index 0000000..17cd422 --- /dev/null +++ b/packages/frontend/src/components/VncModal.vue @@ -0,0 +1,431 @@ + + + \ No newline at end of file diff --git a/packages/frontend/src/components/WorkspaceConnectionList.vue b/packages/frontend/src/components/WorkspaceConnectionList.vue index fd097b7..3518dd8 100644 --- a/packages/frontend/src/components/WorkspaceConnectionList.vue +++ b/packages/frontend/src/components/WorkspaceConnectionList.vue @@ -306,15 +306,8 @@ const handleConnect = (connectionId: number, event?: MouseEvent | KeyboardEvent) closeContextMenu(); // 关闭右键菜单 - if (connection.type === 'RDP') { - console.log(`[WkspConnList] RDP connection clicked (ID: ${connectionId}). Calling sessionStore.openRdpModal.`); - // --- 修改:调用 Store Action --- - sessionStore.openRdpModal(connection); - } else { - console.log(`[WkspConnList] Non-RDP connection clicked (ID: ${connectionId}, Type: ${connection.type}). Emitting connect-request.`); - // 对于非 RDP 连接,保持原有逻辑,发出事件给父组件处理 - emit('connect-request', connectionId); - } + // 统一发出 connect-request 事件,让 sessionStore.handleConnectRequest 处理模态框和会话 + emit('connect-request', connectionId); }; // --- 移除 closeRdpModal 方法 --- diff --git a/packages/frontend/src/stores/connections.store.ts b/packages/frontend/src/stores/connections.store.ts index a334594..2025ff2 100644 --- a/packages/frontend/src/stores/connections.store.ts +++ b/packages/frontend/src/stores/connections.store.ts @@ -291,7 +291,7 @@ export const useConnectionsStore = defineStore('connections', { // this.error = null; try { // 调用后端 API GET /connections/:id/vnc-session - const response = await apiClient.get<{ token: string }>(`/connections/${connectionId}/vnc-session`); + const response = await apiClient.post<{ token: string }>(`/connections/${connectionId}/vnc-session`); return response.data.token; } catch (err: any) { console.error(`获取 VNC 会话令牌失败 (连接 ID: ${connectionId}):`, err); diff --git a/packages/frontend/src/stores/session.store.ts b/packages/frontend/src/stores/session.store.ts index 00613bd..de3b9c9 100644 --- a/packages/frontend/src/stores/session.store.ts +++ b/packages/frontend/src/stores/session.store.ts @@ -134,6 +134,10 @@ export const useSessionStore = defineStore('session', () => { const isRdpModalOpen = ref(false); const rdpConnectionInfo = ref(null); + // --- VNC Modal State --- + const isVncModalOpen = ref(false); + const vncConnectionInfo = ref(null); + // --- Getters --- const sessionTabs = computed(() => { return Array.from(sessions.value.values()).map(session => ({ @@ -409,11 +413,14 @@ export const useSessionStore = defineStore('session', () => { * - 否则,打开一个新的会话标签页并导航到 Workspace。 */ const handleConnectRequest = (connection: ConnectionInfo) => { - console.log(`[SessionStore] handleConnectRequest called for connection: ${connection.name} (ID: ${connection.id}, Type: ${connection.type})`); + // console.log(`[SessionStore] handleConnectRequest called for connection: ${connection.name} (ID: ${connection.id}, Type: ${connection.type})`); // 保留原始日志或移除 if (connection.type === 'RDP') { - // RDP: 直接打开模态框 + // RDP: 直接打开 RDP 模态框 openRdpModal(connection); + } else if (connection.type === 'VNC') { + // VNC: 直接打开 VNC 模态框 + openVncModal(connection); } else { // 非 RDP (e.g., SSH): 处理会话和导航 const connIdStr = String(connection.id); @@ -785,17 +792,30 @@ export const useSessionStore = defineStore('session', () => { // --- RDP Modal Actions --- const openRdpModal = (connection: ConnectionInfo) => { - console.log(`[SessionStore] Opening RDP modal for connection: ${connection.name} (ID: ${connection.id})`); + // console.log(`[SessionStore] Opening RDP modal for connection: ${connection.name} (ID: ${connection.id})`); // 保留原始日志或移除 rdpConnectionInfo.value = connection; isRdpModalOpen.value = true; }; const closeRdpModal = () => { - console.log('[SessionStore] Closing RDP modal.'); + // console.log('[SessionStore] Closing RDP modal.'); // 保留原始日志或移除 isRdpModalOpen.value = false; rdpConnectionInfo.value = null; // 清除连接信息 }; + // --- VNC Modal Actions --- + const openVncModal = (connection: ConnectionInfo) => { + // console.log(`[SessionStore] Opening VNC modal for connection: ${connection.name} (ID: ${connection.id})`); // 保留原始日志或移除 + vncConnectionInfo.value = connection; + isVncModalOpen.value = true; + }; + + const closeVncModal = () => { + // console.log('[SessionStore] Closing VNC modal.'); // 保留原始日志或移除 + isVncModalOpen.value = false; + vncConnectionInfo.value = null; // 清除连接信息 + }; + /** * 更新指定会话的命令输入框内容 */ @@ -815,6 +835,8 @@ export const useSessionStore = defineStore('session', () => { activeSessionId, isRdpModalOpen, // 导出 RDP 模态框状态 rdpConnectionInfo, // 导出 RDP 连接信息 + isVncModalOpen, // 导出 VNC 模态框状态 + vncConnectionInfo, // 导出 VNC 连接信息 // Getters sessionTabs, sessionTabsWithStatus, // 导出新的 getter @@ -841,6 +863,8 @@ export const useSessionStore = defineStore('session', () => { // --- RDP Modal Actions --- openRdpModal, // 导出打开 RDP 模态框 Action closeRdpModal, // 导出关闭 RDP 模态框 Action + openVncModal, // 导出打开 VNC 模态框 Action + closeVncModal, // 导出关闭 VNC 模态框 Action // --- 命令输入框 Action --- updateSessionCommandInput, // 导出更新命令输入 Action }; diff --git a/packages/frontend/src/views/WorkspaceView.vue b/packages/frontend/src/views/WorkspaceView.vue index bcf5c3d..9294559 100644 --- a/packages/frontend/src/views/WorkspaceView.vue +++ b/packages/frontend/src/views/WorkspaceView.vue @@ -10,6 +10,7 @@ import TerminalTabBar from '../components/TerminalTabBar.vue'; import LayoutRenderer from '../components/LayoutRenderer.vue'; import LayoutConfigurator from '../components/LayoutConfigurator.vue'; import RemoteDesktopModal from '../components/RemoteDesktopModal.vue'; +import VncModal from '../components/VncModal.vue'; // +++ 引入 VncModal 组件 +++ import Terminal from '../components/Terminal.vue'; // +++ 引入 Terminal 组件 +++ import CommandInputBar from '../components/CommandInputBar.vue'; // +++ 引入 CommandInputBar 组件 +++ import VirtualKeyboard from '../components/VirtualKeyboard.vue'; // +++ 引入 VirtualKeyboard 组件 +++ @@ -33,7 +34,7 @@ const breakpoints = useBreakpoints(breakpointsTailwind); // +++ 初始化 Breakp const isMobile = breakpoints.smaller('md'); // +++ 定义 isMobile (小于 md 断点) +++ // --- 从 Store 获取响应式状态和 Getters --- -const { sessionTabsWithStatus, activeSessionId, activeSession, isRdpModalOpen, rdpConnectionInfo } = storeToRefs(sessionStore); // 使用 storeToRefs 获取 RDP 状态 +const { sessionTabsWithStatus, activeSessionId, activeSession, isRdpModalOpen, rdpConnectionInfo, isVncModalOpen, vncConnectionInfo } = storeToRefs(sessionStore); // 使用 storeToRefs 获取 RDP 和 VNC 状态 const { shareFileEditorTabsBoolean, layoutLockedBoolean } = storeToRefs(settingsStore); // +++ Add layoutLockedBoolean +++ const { orderedTabs: globalEditorTabs, activeTabId: globalActiveEditorTabId } = storeToRefs(fileEditorStore); const { layoutTree } = storeToRefs(layoutStore); // 只获取布局树 @@ -448,14 +449,13 @@ const handleCloseEditorTab = (tabId: string) => { // --- 连接列表操作处理 (用于 WorkspaceConnectionList) --- const handleConnectRequest = (id: number) => { - console.log(`[WorkspaceView] Received 'connect-request' event for ID: ${id}`); - // +++ 修复:传递 ConnectionInfo 而不是 ID +++ - const connectionInfo = connectionsStore.connections.find(c => c.id === id); - if (connectionInfo) { - sessionStore.handleConnectRequest(connectionInfo); - } else { - console.error(`[WorkspaceView] handleConnectRequest: 未找到 ID 为 ${id} 的连接信息。`); - } + const connectionInfo = connectionsStore.connections.find(c => c.id === id); + // console.log(`[WorkspaceView] Received 'connect-request' event for ID: ${id}`); // 保留原始日志或移除 + if (connectionInfo) { + sessionStore.handleConnectRequest(connectionInfo); + } else { + console.error(`[WorkspaceView] handleConnectRequest: Connection info not found for ID ${id}.`); // 保留错误日志 + } }; const handleOpenNewSession = (id: number) => { console.log(`[WorkspaceView] Received 'open-new-session' event for ID: ${id}`); @@ -650,11 +650,8 @@ const toggleVirtualKeyboard = () => { @close="handleCloseLayoutConfigurator" /> - + +