diff --git a/packages/backend/src/database/connection.ts b/packages/backend/src/database/connection.ts index b084560..58f3667 100644 --- a/packages/backend/src/database/connection.ts +++ b/packages/backend/src/database/connection.ts @@ -107,7 +107,7 @@ export const getDbInstance = (): Promise => { await runDatabaseInitializations(db); // +++ 运行数据库迁移 +++ await runMigrations(db); - console.log('[数据库] 初始化和迁移完成。'); // 添加日志确认 + console.log('[数据库] 初始化和迁移完成。'); resolve(db); } catch (initError) { console.error('[数据库] 连接后初始化失败,正在关闭连接...'); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b5e6817..4a211eb 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -53,7 +53,7 @@ import appearanceRoutes from './appearance/appearance.routes'; import sshKeysRouter from './ssh_keys/ssh_keys.routes'; import quickCommandTagRoutes from './quick-command-tags/quick-command-tag.routes'; import sshSuspendRouter from './ssh-suspend/ssh-suspend.routes'; -import { transfersRoutes } from './transfers/transfers.routes'; // 新增:导入传输路由 +import { transfersRoutes } from './transfers/transfers.routes'; import { initializeWebSocket } from './websocket'; import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; @@ -69,29 +69,19 @@ import './services/notification.dispatcher.service'; process.on('unhandledRejection', (reason: any, promise: Promise) => { console.error('---未处理的 Promise Rejection---'); console.error('原因:', reason); - // 可以在这里添加更详细的日志记录,例如将错误发送到监控系统 - // 注意:根据 Node.js 官方建议,未来版本的 Node.js 可能会默认在 unhandledRejection 时终止进程。 - // 目前我们选择记录错误并继续运行,但这可能导致应用程序状态不一致。 }); // 捕获未捕获的同步异常 process.on('uncaughtException', (error: Error) => { console.error('---未捕获的异常---'); console.error('错误:', error); - // 记录错误,但避免退出进程,尝试让服务器继续运行(有风险) - // 在生产环境中,更安全的做法可能是记录错误后优雅地关闭服务器并重启。 - // process.exit(1); // 强制退出(更安全,但会中断服务) }); - // --- 结束全局错误处理 --- + const initializeEnvironment = async () => { - // 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. - // Use the globally defined path for data .env - const dataEnvPath = dataEnvPathGlobal; // Use the path defined at the top + const dataEnvPath = dataEnvPathGlobal; let keysGenerated = false; let keysToAppend = ''; @@ -117,16 +107,10 @@ const initializeEnvironment = async () => { if (!process.env.GUACD_HOST) { console.warn('[ENV Init] GUACD_HOST 未设置,将使用默认值 "localhost"'); process.env.GUACD_HOST = 'localhost'; - // Optionally add to keysToAppend if you want to save the default - // keysToAppend += `\nGUACD_HOST=localhost`; - // keysGenerated = true; // Mark if you want to save } if (!process.env.GUACD_PORT) { console.warn('[ENV Init] GUACD_PORT 未设置,将使用默认值 "4822"'); process.env.GUACD_PORT = '4822'; - // Optionally add to keysToAppend - // keysToAppend += `\nGUACD_PORT=4822`; - // keysGenerated = true; // Mark if you want to save } @@ -271,10 +255,10 @@ const startServer = () => { app.use('/api/v1/quick-commands', quickCommandsRoutes); app.use('/api/v1/terminal-themes', terminalThemeRoutes); app.use('/api/v1/appearance', appearanceRoutes); - app.use('/api/v1/ssh-keys', sshKeysRouter); // +++ Register SSH Key routes +++ - app.use('/api/v1/quick-command-tags', quickCommandTagRoutes); // +++ Register Quick Command Tag routes +++ - app.use('/api/v1/ssh-suspend', sshSuspendRouter); // +++ Register SSH Suspend routes +++ - app.use('/api/v1/transfers', transfersRoutes()); // 新增:注册传输路由 + app.use('/api/v1/ssh-keys', sshKeysRouter); + app.use('/api/v1/quick-command-tags', quickCommandTagRoutes); + app.use('/api/v1/ssh-suspend', sshSuspendRouter); + app.use('/api/v1/transfers', transfersRoutes()); // 状态检查接口 app.get('/api/v1/status', (req: Request, res: Response) => { diff --git a/packages/backend/src/repositories/appearance.repository.ts b/packages/backend/src/repositories/appearance.repository.ts index a3e81b4..1204c55 100644 --- a/packages/backend/src/repositories/appearance.repository.ts +++ b/packages/backend/src/repositories/appearance.repository.ts @@ -120,7 +120,7 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise< { key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // 数据库中使用空字符串 { key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // 数据库中使用空字符串 { key: 'terminalBackgroundEnabled', value: defaults.terminalBackgroundEnabled }, - { key: 'terminalBackgroundOverlayOpacity', value: defaults.terminalBackgroundOverlayOpacity }, // 新增 + { key: 'terminalBackgroundOverlayOpacity', value: defaults.terminalBackgroundOverlayOpacity }, ]; try { diff --git a/packages/backend/src/repositories/connection.repository.ts b/packages/backend/src/repositories/connection.repository.ts index 0055fb8..f2847f7 100644 --- a/packages/backend/src/repositories/connection.repository.ts +++ b/packages/backend/src/repositories/connection.repository.ts @@ -7,7 +7,7 @@ import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/conn interface ConnectionBase { id: number; name: string | null; - type: 'SSH' | 'RDP' | 'VNC'; // Add type field + type: 'SSH' | 'RDP' | 'VNC'; host: string; port: number; username: string; @@ -16,8 +16,8 @@ interface ConnectionBase { created_at: number; updated_at: number; last_connected_at: number | null; - ssh_key_id?: number | null; // +++ Add ssh_key_id here as well +++ -notes?: string | null; // 新增备注字段 + ssh_key_id?: number | null; +notes?: string | null; } // ConnectionWithTagsRow implicitly includes 'type' and 'ssh_key_id' via ConnectionBase @@ -36,7 +36,7 @@ export interface FullConnectionData extends ConnectionBase { encrypted_password?: string | null; encrypted_private_key?: string | null; encrypted_passphrase?: string | null; -notes?: string | null; // 新增备注字段 +notes?: string | null; tag_ids?: number[]; } diff --git a/packages/backend/src/transfers/transfers.controller.ts b/packages/backend/src/transfers/transfers.controller.ts index 3a7008a..6e6663e 100644 --- a/packages/backend/src/transfers/transfers.controller.ts +++ b/packages/backend/src/transfers/transfers.controller.ts @@ -44,7 +44,6 @@ export class TransfersController { res.status(202).json(task); // 202 Accepted 表示请求已接受处理,但尚未完成 } catch (error) { console.error('[TransfersController] Error initiating transfer:', error); - // next(error) 可以将错误传递给全局错误处理中间件 res.status(500).json({ message: 'Failed to initiate transfer.', error: error instanceof Error ? error.message : String(error) }); } } diff --git a/packages/backend/src/transfers/transfers.service.ts b/packages/backend/src/transfers/transfers.service.ts index a377d63..c471f01 100644 --- a/packages/backend/src/transfers/transfers.service.ts +++ b/packages/backend/src/transfers/transfers.service.ts @@ -1,19 +1,17 @@ -import * as fs from 'fs/promises'; -import * as os from 'os'; import * as path from 'path'; import * as crypto from 'crypto'; -import { v4 as uuidv4 } from 'uuid'; // 用于生成唯一ID +import { v4 as uuidv4 } from 'uuid'; import { Client, ConnectConfig, SFTPWrapper } from 'ssh2'; import { InitiateTransferPayload, TransferTask, TransferSubTask } from './transfers.types'; import { getConnectionWithDecryptedCredentials } from '../services/connection.service'; import type { ConnectionWithTags, DecryptedConnectionCredentials } from '../types/connection.types'; -// import { logger } from '../utils/logger'; // 假设的日志工具路径 + export class TransfersService { private transferTasks: Map = new Map(); private taskAbortControllers: Map = new Map(); // +++ 用于存储任务的 AbortController +++ private readonly TEMP_KEY_PREFIX = 'nexus_target_key_'; - private readonly MAX_CONCURRENT_SUB_TASKS = 5; // Maximum concurrent sub-tasks + private readonly MAX_CONCURRENT_SUB_TASKS = 5; constructor() { console.info('[TransfersService] Initialized.'); @@ -23,8 +21,8 @@ export class TransfersService { const taskId = uuidv4(); const now = new Date(); const subTasks: TransferSubTask[] = []; - const abortController = new AbortController(); // +++ 创建 AbortController +++ - this.taskAbortControllers.set(taskId, abortController); // +++ 存储 AbortController +++ + const abortController = new AbortController(); + this.taskAbortControllers.set(taskId, abortController); // 每个 (目标服务器, 源文件) 组合都是一个子任务 for (const connectionId of payload.connectionIds) { // 目标服务器ID列表 @@ -91,10 +89,6 @@ export class TransfersService { } }); - // 确保在 AbortController Map 中移除,以防内存泄漏(如果任务不再处理) - // 也可以在任务彻底结束后移除 - // this.taskAbortControllers.delete(taskId); // 暂时不在这里删除,可能在 processTransferTask 的 finally 中 - return true; } console.warn(`[TransfersService] No AbortController found for task ${taskId} to cancel.`); @@ -299,7 +293,6 @@ export class TransfersService { }); subTaskExecutionPromises.push(taskPromise); } - // If all tasks were launched and some are still active, or if all tasks were skipped due to early cancellation if (currentSubTaskIndex === totalSubTasks && currentlyActiveSubTasks === 0 && !signal.aborted) { console.info(`[TransfersService] Task ${taskId}: All sub-tasks processed (no active, no more to launch).`); signal.removeEventListener('abort', onAbortOverall); @@ -343,7 +336,7 @@ export class TransfersService { } } this.finalizeOverallTaskStatus(taskId); // Ensure final status is set - this.taskAbortControllers.delete(taskId); // +++ Clean up AbortController +++ + this.taskAbortControllers.delete(taskId); if (task) { // task 可能未定义如果 taskId 错误 console.info(`[TransfersService] Task ${taskId} processing finished. Final status: ${task.status}.`); } else { @@ -355,10 +348,8 @@ export class TransfersService { private async checkCommandOnSource(client: Client, command: string): Promise { return new Promise((resolve) => { const checkCmd = `command -v ${this.escapeShellArg(command)} 2>/dev/null`; - console.error(`[Roo Debug][transfers.service.ts] checkCommandOnSource: Executing: ${checkCmd}`); client.exec(checkCmd, (err, stream) => { if (err) { - console.warn(`[Roo Debug][transfers.service.ts] Error checking for command '${command}' on source:`, err); return resolve(null); } let stdout = ''; @@ -367,15 +358,12 @@ export class TransfersService { .on('close', (code: number) => { const foundPath = stdout.trim(); if (code === 0 && foundPath) { - console.error(`[Roo Debug][transfers.service.ts] checkCommandOnSource: Command '${command}' found at '${foundPath}'.`); resolve(foundPath); } else { - console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnSource: Command '${command}' not found (exit code: ${code}).`); resolve(null); } }) - .stderr.on('data', (data: Buffer) => { // Should be empty due to 2>/dev/null, but good to have - console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnSource: STDERR for '${command}': ${data.toString()}`); + .stderr.on('data', (data: Buffer) => { }); }); }); @@ -386,7 +374,6 @@ export class TransfersService { const connectConfig = this.buildSshConnectConfig(targetConnection, targetCredentials); let foundCommandPath: string | null = null; - console.error(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Attempting to connect to target ${targetConnection.host} to check for command '${command}'.`); try { await new Promise((resolve, reject) => { @@ -407,10 +394,8 @@ export class TransfersService { foundCommandPath = await new Promise((resolve) => { const checkCmd = `command -v ${this.escapeShellArg(command)} 2>/dev/null`; - console.error(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Executing on target: ${checkCmd}`); targetClient.exec(checkCmd, (err, stream) => { if (err) { - console.warn(`[Roo Debug][transfers.service.ts] Error checking for command '${command}' on target ${targetConnection.host}:`, err); return resolve(null); } let stdout = ''; @@ -419,20 +404,16 @@ export class TransfersService { .on('close', (code: number) => { const pathOutput = stdout.trim(); if (code === 0 && pathOutput) { - console.error(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Command '${command}' found at '${pathOutput}' on target ${targetConnection.host}.`); resolve(pathOutput); } else { - console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Command '${command}' not found on target ${targetConnection.host} (exit code: ${code}).`); resolve(null); } }) .stderr.on('data', (data: Buffer) => { - console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: STDERR for '${command}' on target ${targetConnection.host}: ${data.toString()}`); }); }); }); } catch (error) { - console.error(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Failed to check command '${command}' on target ${targetConnection.host}:`, error); foundCommandPath = null; // Ensure it's null on error } finally { targetClient.end(); @@ -441,7 +422,6 @@ export class TransfersService { } private async uploadKeyToSourceViaSftp(client: Client, privateKeyContent: string, remotePath: string): Promise { - console.error(`[Roo Debug][transfers.service.ts] ENTERING uploadKeyToSourceViaSftp for remotePath: ${remotePath}`); const SFTP_UPLOAD_TIMEOUT_MS = 30000; // 30 seconds timeout for SFTP key upload return new Promise((resolve, reject) => { @@ -450,8 +430,6 @@ export class TransfersService { const cleanupAndReject = (errMsg: string, errObj?: any) => { if (timeoutHandle) clearTimeout(timeoutHandle); - if (errObj) console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp error: ${errMsg}`, errObj); - else console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp error: ${errMsg}`); sftpSession?.end(); reject(new Error(errMsg)); }; @@ -460,7 +438,6 @@ export class TransfersService { cleanupAndReject(`SFTP upload to ${remotePath} timed out after ${SFTP_UPLOAD_TIMEOUT_MS / 1000}s.`); }, SFTP_UPLOAD_TIMEOUT_MS); - console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp: Calling client.sftp(). Timeout set for ${SFTP_UPLOAD_TIMEOUT_MS}ms.`); client.sftp((err, sftp) => { sftpSession = sftp; // Store session for potential cleanup if (err) { @@ -469,7 +446,6 @@ export class TransfersService { if (!sftp) { return cleanupAndReject(`SFTP session error: SFTP object is null.`); } - console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp: client.sftp() CALLBACK success. SFTP session obtained. Creating write stream to ${remotePath}`); const stream = sftp.createWriteStream(remotePath, { mode: 0o600 }); stream.on('error', (writeErr: Error) => { @@ -479,19 +455,13 @@ export class TransfersService { // Listen to 'close' instead of 'finish' for more reliability stream.on('close', () => { if (timeoutHandle) clearTimeout(timeoutHandle); - console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp: WriteStream ON CLOSE for ${remotePath}. Key upload likely successful.`); console.info(`[TransfersService] Private key for target successfully uploaded to source at ${remotePath}`); sftp.end(); resolve(); }); - console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp: Previewing privateKeyContent before stream.end(). Length: ${privateKeyContent.length}`); - console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp: Key content START: <<<${privateKeyContent.substring(0, 70)}>>>`); - console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp: Key content END: <<<${privateKeyContent.substring(Math.max(0, privateKeyContent.length - 70))}>>>`); - console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp: Calling stream.end() to write key content.`); let keyContentToWrite = privateKeyContent; if (!keyContentToWrite.endsWith('\n')) { - console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp: privateKeyContent does not end with a newline. Appending one.`); keyContentToWrite += '\n'; } stream.end(keyContentToWrite); @@ -544,8 +514,7 @@ export class TransfersService { commandParts.push(options.sshPassCommand); } - // Use the full path here (should be safe, no special chars from command -v) - // Arguments will still be quoted later. + commandParts.push(executableCommand); if (commandType === 'rsync') { @@ -568,12 +537,6 @@ export class TransfersService { commandParts.push(remoteFullDest); } else { // scp - // For scp, SSH options are typically passed directly if scp is a wrapper around ssh, or via scp's own options that map to ssh options. - // Common scp implementations accept -P for port and -i for identity file directly. - // StrictHostKeyChecking and UserKnownHostsFile are ssh options. - // We build the ssh part for scp separately if needed, or rely on scp passing -o options. - // Let's assume scp will pass these -o options to its underlying ssh call. - // If not, a more complex construction of scp's ssh command via -S might be needed. commandParts.push('-o StrictHostKeyChecking=no'); // For scp, pass as direct option commandParts.push('-o UserKnownHostsFile=/dev/null'); // For scp, pass as direct option if (isDir) commandParts.push('-r'); @@ -601,14 +564,12 @@ private async executeRemoteTransferOnSource( transferMethodPreference: 'auto' | 'rsync' | 'scp', signal: AbortSignal // +++ Add AbortSignal parameter +++ ): Promise { - console.error(`[Roo Debug][transfers.service.ts] ENTERING executeRemoteTransferOnSource for sub-task ${subTaskId}, item: ${sourceItem.name}`); if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError'); this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 0, `Initializing remote transfer for ${sourceItem.name}`); let tempTargetKeyPathOnSource: string | undefined; try { if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError'); - console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Starting try block in executeRemoteTransferOnSource.`); // Pass signal to these check commands if they are made to support it. For now, they are quick. const sshpassPath = await this.checkCommandOnSource(sourceSshClient, 'sshpass' /*, signal */); if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError'); @@ -617,7 +578,6 @@ private async executeRemoteTransferOnSource( const scpPathOnSource = await this.checkCommandOnSource(sourceSshClient, 'scp' /*, signal */); if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError'); - console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Source checks -> sshpass: ${sshpassPath}, rsync: ${rsyncPathOnSource}, scp: ${scpPathOnSource}`); let executableCommandPath: string | null = null; let commandTypeForLogic: 'rsync' | 'scp' | undefined = undefined; // Initialize as undefined @@ -626,7 +586,6 @@ private async executeRemoteTransferOnSource( if (transferMethodPreference === 'auto') { if (rsyncPathOnSource) { // Source has rsync, check target - console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: 'auto' mode, rsync found on source. Checking target...`); rsyncPathOnTarget = await this.checkCommandOnTargetServer(targetConnection, targetCredentials, 'rsync' /*, signal */); if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError'); if (rsyncPathOnTarget) { @@ -677,7 +636,6 @@ private async executeRemoteTransferOnSource( // +++ 自动创建目标目录 +++ this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 6, `Ensuring target directory ${this.escapeShellArg(remoteTargetPathOnTarget)} exists on ${targetConnection.host}.`); - console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Ensuring target directory exists: ${remoteTargetPathOnTarget} on ${targetConnection.host}`); const targetClientForMkdir = new Client(); const targetConnectConfigForMkdir = this.buildSshConnectConfig(targetConnection, targetCredentials); try { @@ -685,8 +643,8 @@ private async executeRemoteTransferOnSource( await new Promise((resolveMkdir, rejectMkdir) => { let mkdirStreamClosed = false; const onAbortMkdir = () => { - if (!mkdirStreamClosed) { // Only if stream/connection is still active - targetClientForMkdir.end(); // Attempt to close the connection + if (!mkdirStreamClosed) { + targetClientForMkdir.end(); } rejectMkdir(new DOMException('Mkdir operation cancelled by user.', 'AbortError')); }; @@ -699,7 +657,6 @@ private async executeRemoteTransferOnSource( return rejectMkdir(new DOMException('Mkdir operation cancelled by user (on ready).', 'AbortError')); } const mkdirCommand = `mkdir -p ${this.escapeShellArg(remoteTargetPathOnTarget)}`; - console.error(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: Executing on target for mkdir: ${mkdirCommand}`); targetClientForMkdir.exec(mkdirCommand, (err, stream) => { if (err) { signal.removeEventListener('abort', onAbortMkdir); @@ -718,11 +675,9 @@ private async executeRemoteTransferOnSource( rejectMkdir(new Error(`Failed to create directory ${remoteTargetPathOnTarget} on ${targetConnection.host}. Exit code: ${code}. Stderr: ${mkdirStderr.trim()}`)); } }).on('data', (data: Buffer) => { - // stdout from mkdir -p is usually empty }).stderr.on('data', (data: Buffer) => { mkdirStderr += data.toString(); - console.warn(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: STDERR (mkdir on target): ${data.toString()}`); - }).on('error', (streamErr: Error) => { // Handle stream errors specifically + }).on('error', (streamErr: Error) => { mkdirStreamClosed = true; signal.removeEventListener('abort', onAbortMkdir); targetClientForMkdir.end(); @@ -731,11 +686,9 @@ private async executeRemoteTransferOnSource( }); }).on('error', (connErr: Error) => { signal.removeEventListener('abort', onAbortMkdir); - // targetClientForMkdir.end(); // .end() might not be needed if 'close' always follows 'error' rejectMkdir(connErr); - }).on('close', () => { // This 'close' is for the client connection itself - signal.removeEventListener('abort', onAbortMkdir); // Ensure cleanup if closed for other reasons - // console.info(`[TransfersService] SSH connection for mkdir to target ${targetConnection.host} closed.`); + }).on('close', () => { + signal.removeEventListener('abort', onAbortMkdir); }).connect(targetConnectConfigForMkdir); }); @@ -743,17 +696,14 @@ private async executeRemoteTransferOnSource( this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 8, `Target directory ensured. Preparing transfer command.`); } catch (mkdirError: any) { - // Ensure client is closed on error if it's still somehow connected - // (though on 'error' or exec stream 'close'/'error', it should be handled) if (targetClientForMkdir && (targetClientForMkdir as any)._sock && !(targetClientForMkdir as any)._sock.destroyed) { try { targetClientForMkdir.end(); } catch (e) { /* ignore */ } } console.error(`[TransfersService] Sub-task ${subTaskId}: Failed to ensure target directory ${remoteTargetPathOnTarget} on ${targetConnection.host}:`, mkdirError.message); if (mkdirError.name === 'AbortError') { this.updateSubTaskStatus(taskId, subTaskId, 'cancelled', undefined, `Directory creation cancelled: ${mkdirError.message}`); - throw mkdirError; // Re-throw AbortError to be handled by the main try-catch + throw mkdirError; } - // For other errors, update status to failed and throw a new error to be caught by main try-catch this.updateSubTaskStatus(taskId, subTaskId, 'failed', undefined, `Failed to create target directory: ${mkdirError.message}`); throw new Error(`Failed to create target directory ${remoteTargetPathOnTarget}: ${mkdirError.message}`); // This will be caught by the outer try-catch } @@ -792,10 +742,6 @@ private async executeRemoteTransferOnSource( const onAbortCmd = () => { if (!streamClosed) { console.warn(`[TransfersService] Abort signal received for command stream of sub-task ${subTaskId}. Attempting to close stream.`); - // execStream?.close(); // 'execStream' is not defined here, should be 'stream' from exec callback - // It's tricky to access the stream here to close it directly. - // The main mechanism will be the timeout and the client connection eventually closing if task is aborted. - // Or, if ssh2's stream object can be made available to this scope, call .close() or .destroy(). } rejectCmd(new DOMException('Command cancelled by user.', 'AbortError')); }; @@ -840,7 +786,6 @@ private async executeRemoteTransferOnSource( stream.stderr.on('data', (data: Buffer) => { if (signal.aborted) return; stderrCombined += data.toString(); - console.warn(`[Roo Debug][transfers.service.ts] STDERR Sub-task ${subTaskId}: ${data.toString()}`); }); stream.on('close', (code: number | null) => { streamClosed = true; @@ -882,7 +827,6 @@ private async executeRemoteTransferOnSource( } throw error; // Re-throw to be caught by processSingleSubTaskWrapper } finally { - console.info(`[Roo Debug][transfers.service.ts] executeRemoteTransferOnSource FINALLY for sub-task ${subTaskId}`); if (tempTargetKeyPathOnSource) { try { // TODO: Make deleteFileOnSourceViaSftp accept signal @@ -1001,7 +945,6 @@ private async executeRemoteTransferOnSource( if (numSubTasks === 0) { task.overallProgress = 0; - // task.status remains as set by initiate or direct updateOverallTaskStatus if no subtasks. return; } @@ -1013,12 +956,9 @@ private async executeRemoteTransferOnSource( break; case 'failed': failedCount++; - // Failed tasks are "done" but contribute 0 to success progress. - // Depending on definition, they could count as 100 for task "completion" progress. - // Here, only successful completion adds to progress towards 100%. break; case 'transferring': - case 'connecting': // consider connecting as in-progress for overall status + case 'connecting': inProgressCount++; totalProgress += (st.progress !== undefined ? st.progress : (st.status === 'connecting' ? 5 : 0)); // Small progress for connecting break; @@ -1043,8 +983,6 @@ private async executeRemoteTransferOnSource( } else if (queuedCount === numSubTasks) { newOverallStatus = 'queued'; // All subtasks are still queued } else { - // Fallback or unexpected mixed state, treat as in-progress generally - // This case implies some completed, some queued, no failed, no in-progress items. newOverallStatus = 'in-progress'; // Or 'partially-completed' if completedCount > 0 if (completedCount > 0 && queuedCount > 0 && failedCount === 0 && inProgressCount === 0) { newOverallStatus = 'partially-completed'; // More accurate for this specific mix @@ -1056,7 +994,6 @@ private async executeRemoteTransferOnSource( task.status = newOverallStatus; } task.updatedAt = new Date(); - // console.debug(`[TransfersService] Task ${taskId} overall progress: ${task.overallProgress}%, status: ${task.status}`); } private finalizeOverallTaskStatus(taskId: string): void { diff --git a/packages/backend/src/types/connection.types.ts b/packages/backend/src/types/connection.types.ts index 8737e5c..443b9fa 100644 --- a/packages/backend/src/types/connection.types.ts +++ b/packages/backend/src/types/connection.types.ts @@ -10,7 +10,7 @@ export interface ConnectionBase { created_at: number; updated_at: number; last_connected_at: number | null; -notes?: string | null; // 新增备注字段 +notes?: string | null; } export interface ConnectionWithTags extends ConnectionBase { @@ -31,7 +31,7 @@ export interface CreateConnectionInput { ssh_key_id?: number | null; // +++ Add ssh_key_id +++ proxy_id?: number | null; tag_ids?: number[]; -notes?: string | null; // 新增备注字段 +notes?: string | null; } @@ -47,7 +47,7 @@ export interface UpdateConnectionInput { passphrase?: string; ssh_key_id?: number | null; // +++ Add ssh_key_id +++ proxy_id?: number | null; -notes?: string | null; // 新增备注字段 +notes?: string | null; tag_ids?: number[]; } @@ -63,10 +63,10 @@ export interface FullConnectionData { encrypted_password: string | null; encrypted_private_key: string | null; encrypted_passphrase: string | null; - ssh_key_id?: number | null; // +++ Add ssh_key_id +++ + ssh_key_id?: number | null; proxy_id: number | null; created_at: number; -notes: string | null; // 新增备注字段 (数据库原始字段) +notes: string | null; updated_at: number; last_connected_at: number | null; } diff --git a/packages/frontend/src/components/AddProxyForm.vue b/packages/frontend/src/components/AddProxyForm.vue index 1e590c1..321943f 100644 --- a/packages/frontend/src/components/AddProxyForm.vue +++ b/packages/frontend/src/components/AddProxyForm.vue @@ -28,7 +28,7 @@ const initialFormData = { port: 1080, // 默认 SOCKS5 端口 username: '', password: '', - tag_ids: [] as number[], // 新增 tag_ids 字段 + tag_ids: [] as number[], }; const formData = reactive({ ...initialFormData }); diff --git a/packages/frontend/src/components/FileManagerContextMenu.vue b/packages/frontend/src/components/FileManagerContextMenu.vue index 45e7ea7..1c01670 100644 --- a/packages/frontend/src/components/FileManagerContextMenu.vue +++ b/packages/frontend/src/components/FileManagerContextMenu.vue @@ -1,11 +1,11 @@ diff --git a/packages/frontend/src/components/StyleCustomizer.vue b/packages/frontend/src/components/StyleCustomizer.vue index 771eed3..65663c8 100644 --- a/packages/frontend/src/components/StyleCustomizer.vue +++ b/packages/frontend/src/components/StyleCustomizer.vue @@ -98,8 +98,8 @@ const initializeEditableState = () => { editableEditorFontSize.value = currentEditorFontSize.value; // <-- 新增 localTerminalBackgroundEnabled.value = isTerminalBackgroundEnabled.value; // <-- 重新添加:在此处初始化 editableTerminalBackgroundOverlayOpacity.value = currentTerminalBackgroundOverlayOpacity.value; // 初始化蒙版透明度 - console.log(`[StyleCustomizer initializeEditableState] Initializing localTerminalBackgroundEnabled to: ${localTerminalBackgroundEnabled.value} (from store: ${isTerminalBackgroundEnabled.value})`); // 添加日志 - console.log(`[StyleCustomizer initializeEditableState] Initializing editableTerminalBackgroundOverlayOpacity to: ${editableTerminalBackgroundOverlayOpacity.value} (from store: ${currentTerminalBackgroundOverlayOpacity.value})`); // 新增日志 + console.log(`[StyleCustomizer initializeEditableState] Initializing localTerminalBackgroundEnabled to: ${localTerminalBackgroundEnabled.value} (from store: ${isTerminalBackgroundEnabled.value})`); + console.log(`[StyleCustomizer initializeEditableState] Initializing editableTerminalBackgroundOverlayOpacity to: ${editableTerminalBackgroundOverlayOpacity.value} (from store: ${currentTerminalBackgroundOverlayOpacity.value})`); uploadError.value = null; importError.value = null; saveThemeError.value = null; @@ -143,7 +143,7 @@ watch([ newSettings?.terminalBackgroundEnabled !== oldSettings?.terminalBackgroundEnabled || newSettings?.terminalBackgroundOverlayOpacity !== oldSettings?.terminalBackgroundOverlayOpacity; // 检查相关设置是否变化 if (!isEditingTheme.value || newActiveThemeId !== oldActiveThemeId || settingsChanged) { - console.log(`[StyleCustomizer Watch] Triggering re-initialization. isEditing: ${isEditingTheme.value}, themeIdChanged: ${newActiveThemeId !== oldActiveThemeId}, settingsChanged: ${settingsChanged}`); // 添加日志 + console.log(`[StyleCustomizer Watch] Triggering re-initialization. isEditing: ${isEditingTheme.value}, themeIdChanged: ${newActiveThemeId !== oldActiveThemeId}, settingsChanged: ${settingsChanged}`); initializeEditableState(); // 调用修改后的初始化函数 } else { // 如果正在编辑,只更新非编辑相关的部分 (不包括 UI 主题和终端背景开关,因为它们由 initializeEditableState 处理) @@ -165,10 +165,10 @@ watch(isTerminalBackgroundEnabled, (newValue) => { // 只有当本地状态与 store 状态不一致时才更新本地状态 // 这避免了 handleToggleTerminalBackground 触发的不必要更新 if (localTerminalBackgroundEnabled.value !== newValue) { - console.log(`[StyleCustomizer Watch isTerminalBackgroundEnabled] Store changed to ${newValue}, updating local state.`); // 添加日志 + console.log(`[StyleCustomizer Watch isTerminalBackgroundEnabled] Store changed to ${newValue}, updating local state.`); localTerminalBackgroundEnabled.value = newValue; } else { - console.log(`[StyleCustomizer Watch isTerminalBackgroundEnabled] Store changed to ${newValue}, but local state already matches. No update needed.`); // 添加日志 + console.log(`[StyleCustomizer Watch isTerminalBackgroundEnabled] Store changed to ${newValue}, but local state already matches. No update needed.`); } }); // 移除单独监听 isTerminalBackgroundEnabled 的 watcher diff --git a/packages/frontend/src/components/TerminalTabBar.vue b/packages/frontend/src/components/TerminalTabBar.vue index 2dfb4a8..dbc9a65 100644 --- a/packages/frontend/src/components/TerminalTabBar.vue +++ b/packages/frontend/src/components/TerminalTabBar.vue @@ -6,7 +6,7 @@ import { useRoute } from 'vue-router'; import { storeToRefs } from 'pinia'; import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue'; import TabBarContextMenu from './TabBarContextMenu.vue'; -import TransferProgressModal from './TransferProgressModal.vue'; // 导入传输进度模态框 +import TransferProgressModal from './TransferProgressModal.vue'; import { useSessionStore } from '../stores/session.store'; import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store'; import { useLayoutStore, type PaneName } from '../stores/layout.store'; @@ -15,7 +15,7 @@ import { useWorkspaceEventEmitter, useWorkspaceEventSubscriber, useWorkspaceEven import type { SessionTabInfoWithStatus } from '../stores/session/types'; // 路径修正 -const { t } = useI18n(); // 初始化 i18n +const { t } = useI18n(); const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++ const onWorkspaceEvent = useWorkspaceEventSubscriber(); // +++ 获取事件订阅器 +++ const offWorkspaceEvent = useWorkspaceEventOff(); // +++ 获取事件取消订阅器 +++ diff --git a/packages/frontend/src/components/TransferProgressModal.vue b/packages/frontend/src/components/TransferProgressModal.vue index 44c056c..30af084 100644 --- a/packages/frontend/src/components/TransferProgressModal.vue +++ b/packages/frontend/src/components/TransferProgressModal.vue @@ -65,8 +65,8 @@ interface TransferTask { updatedAt: string | Date; subTasks: TransferSubTask[]; overallProgress?: number; - sourceConnectionId?: number; // 新增:源连接ID (可选) - remoteTargetPath?: string; // 新增:目标路径 (可选) + sourceConnectionId?: number; + remoteTargetPath?: string; } const transferTasks = ref([]); @@ -133,8 +133,8 @@ const getDisplayStatus = (status: string): string => { 'partially-completed': 'transferProgressModal.status.partiallyCompleted', 'connecting': 'transferProgressModal.status.connecting', 'transferring': 'transferProgressModal.status.transferring', - 'cancelling': 'transferProgressModal.status.cancelling', // +++ 新增状态翻译键 +++ - 'cancelled': 'transferProgressModal.status.cancelled', // +++ 新增状态翻译键 +++ + 'cancelling': 'transferProgressModal.status.cancelling', + 'cancelled': 'transferProgressModal.status.cancelled', }; // 提供一个默认的回退文本,以防i18n key缺失 const defaultText = status.charAt(0).toUpperCase() + status.slice(1).replace('-', ' '); @@ -150,7 +150,7 @@ const formatDate = (dateInput: string | Date): string => { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } catch (e) { - return String(dateInput); // Fallback if date is invalid + return String(dateInput); } }; @@ -185,7 +185,7 @@ watch(() => props.visible, (newVisible) => { } }, { immediate: false }); // immediate: false 避免在组件初始化时立即执行,onMounted已处理首次加载 -// --- 原有:模态框可见性控制 --- +// --- 模态框可见性控制 --- const internalVisible = ref(props.visible); // 监听 props.visible 的变化来更新 internalVisible @@ -221,17 +221,10 @@ const handleCancelTask = async (taskId: string) => { // 更新UI,将任务状态临时设置为 'cancelling' 或禁用按钮 const task = transferTasks.value.find(t => t.taskId === taskId); if (task) { - // 优选: 如果后端会快速更新状态并通过轮询反映, 前端可能不需要立即改变状态。 - // 否则, 可以临时改变: task.status = 'cancelling'; - // 另一种方法是添加一个 loading 状态到按钮上 + } await apiClient.post(`/transfers/cancel/${taskId}`); - // 可以添加成功提示 - // uiNotificationsStore.showSuccess(t('transferProgressModal.cancelRequested', '已发送终止请求。')); - - // 前端优化:立即将任务状态设置为 'cancelling' 以提供即时反馈 - // 这样用户点击后能马上看到状态变为“终止中”,后续轮询会从后端获取权威状态。 const taskBeingCancelled = transferTasks.value.find(t => t.taskId === taskId); if (taskBeingCancelled && ['queued', 'in-progress', 'connecting', 'transferring'].includes(taskBeingCancelled.status)) { taskBeingCancelled.status = 'cancelling'; @@ -241,8 +234,6 @@ const handleCancelTask = async (taskId: string) => { fetchTransferTasks(); } catch (error: any) { console.error(`Failed to cancel task ${taskId}:`, error); - // uiNotificationsStore.showError(error.response?.data?.message || error.message || t('transferProgressModal.error.cancelFailed', '终止任务失败。')); - // 如果任务状态之前被临时修改,可能需要回滚 } }; diff --git a/packages/frontend/src/components/WorkspaceConnectionList.vue b/packages/frontend/src/components/WorkspaceConnectionList.vue index fb221e2..cca7ef0 100644 --- a/packages/frontend/src/components/WorkspaceConnectionList.vue +++ b/packages/frontend/src/components/WorkspaceConnectionList.vue @@ -383,7 +383,7 @@ const handleMenuAction = (action: 'add' | 'edit' | 'delete' | 'clone') => { // closeContextMenu(); // 先关闭菜单 if (action === 'add') { - console.log('[WorkspaceConnectionList] handleMenuAction called with action: add. Emitting request-add-connection...'); // 添加日志 + console.log('[WorkspaceConnectionList] handleMenuAction called with action: add. Emitting request-add-connection...'); // router.push('/connections/add'); // 改为触发事件 emitWorkspaceEvent('connection:requestAdd'); } else if (conn) { @@ -516,7 +516,7 @@ const handleTagMenuAction = (action: 'connectAll' | 'manageTag' | 'deleteAllConn // 确保是已标记的组 if (group.tagId === null) { uiNotificationsStore.addNotification({ - message: t('workspaceConnectionList.cannotDeleteFromUntagged'), // 新增i18n + message: t('workspaceConnectionList.cannotDeleteFromUntagged'), type: 'warning', }); return; @@ -524,13 +524,13 @@ const handleTagMenuAction = (action: 'connectAll' | 'manageTag' | 'deleteAllConn // 确保组内有连接 if (group.connections.length === 0) { uiNotificationsStore.addNotification({ - message: t('workspaceConnectionList.noConnectionsToDeleteInGroup', { groupName: group.groupName }), // 新增i18n + message: t('workspaceConnectionList.noConnectionsToDeleteInGroup', { groupName: group.groupName }), type: 'info', }); return; } - if (confirm(t('workspaceConnectionList.confirmDeleteAllConnectionsInGroup', { count: group.connections.length, groupName: group.groupName }))) { // 新增i18n + if (confirm(t('workspaceConnectionList.confirmDeleteAllConnectionsInGroup', { count: group.connections.length, groupName: group.groupName }))) { const connectionIdsToDelete = group.connections.map(conn => conn.id); const deletePromises = connectionIdsToDelete.map(connId => @@ -547,13 +547,13 @@ const handleTagMenuAction = (action: 'connectAll' | 'manageTag' | 'deleteAllConn if (successfulDeletes > 0) { uiNotificationsStore.addNotification({ - message: t('workspaceConnectionList.allConnectionsInGroupDeletedSuccess', { count: successfulDeletes, groupName: group.groupName }), // 新增i18n + message: t('workspaceConnectionList.allConnectionsInGroupDeletedSuccess', { count: successfulDeletes, groupName: group.groupName }), type: 'success', }); } if (failedDeletes > 0) { uiNotificationsStore.addNotification({ - message: t('workspaceConnectionList.someConnectionsInGroupDeleteFailed', { count: failedDeletes, groupName: group.groupName }), // 新增i18n + message: t('workspaceConnectionList.someConnectionsInGroupDeleteFailed', { count: failedDeletes, groupName: group.groupName }), type: 'error', }); } diff --git a/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts b/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts index 37d61bc..b8ab4da 100644 --- a/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts +++ b/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts @@ -365,8 +365,7 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti // --- 返回状态和处理函数 --- return { - // isDraggingOver, // 不再导出 - showExternalDropOverlay, // 新增导出 + showExternalDropOverlay, dragOverTarget, draggedItem, // 需要暴露以供 handleDragOverRow 等函数内部判断 // --- 事件处理器 --- @@ -374,7 +373,7 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti handleDragOver, handleDragLeave, handleDrop, // 容器的 drop (主要用于清理) - handleOverlayDrop, // 新增导出:蒙版的 drop + handleOverlayDrop, handleDragStart, handleDragEnd, handleDragOverRow, diff --git a/packages/frontend/src/composables/useAddConnectionForm.ts b/packages/frontend/src/composables/useAddConnectionForm.ts index fc06467..e117720 100644 --- a/packages/frontend/src/composables/useAddConnectionForm.ts +++ b/packages/frontend/src/composables/useAddConnectionForm.ts @@ -38,20 +38,20 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon // 表单数据模型 const initialFormData = { - type: 'SSH' as 'SSH' | 'RDP' | 'VNC', // Use uppercase to match ConnectionInfo + type: 'SSH' as 'SSH' | 'RDP' | 'VNC', name: '', host: '', port: 22, username: '', - auth_method: 'password' as 'password' | 'key', // SSH specific + auth_method: 'password' as 'password' | 'key', password: '', - private_key: '', // SSH specific (for direct input) - This field seems unused in the new logic, but kept for initialData consistency - passphrase: '', // SSH specific (for direct input) - This field seems unused, kept for consistency - selected_ssh_key_id: null as number | null, // +++ Add field for selected key ID +++ + private_key: '', + passphrase: '', + selected_ssh_key_id: null as number | null, proxy_id: null as number | null, - tag_ids: [] as number[], // 新增 tag_ids 字段 - notes: '', // 新增备注字段 - vncPassword: '', // VNC specific password + tag_ids: [] as number[], + notes: '', + vncPassword: '', }; const formData = reactive({ ...initialFormData }); diff --git a/packages/frontend/src/composables/useSshTerminal.ts b/packages/frontend/src/composables/useSshTerminal.ts index 15e3ff0..9a0f0f8 100644 --- a/packages/frontend/src/composables/useSshTerminal.ts +++ b/packages/frontend/src/composables/useSshTerminal.ts @@ -86,7 +86,6 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD }; const handleTerminalResize = (dimensions: { cols: number; rows: number }) => { - // 添加日志,确认从 WorkspaceView 收到的尺寸 console.log(`[SSH ${sessionId}] handleTerminalResize called with:`, dimensions); // 只有在连接状态下才发送 resize 命令给后端 if (isConnected.value) { diff --git a/packages/frontend/src/stores/appearance.store.ts b/packages/frontend/src/stores/appearance.store.ts index 9470d26..a717551 100644 --- a/packages/frontend/src/stores/appearance.store.ts +++ b/packages/frontend/src/stores/appearance.store.ts @@ -235,9 +235,9 @@ export const useAppearanceStore = defineStore('appearance', () => { * @param enabled 是否启用 */ async function setTerminalBackgroundEnabled(enabled: boolean) { - console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 调用,准备发送给后端的值: ${enabled}`); // 添加日志 + console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 调用,准备发送给后端的值: ${enabled}`); await updateAppearanceSettings({ terminalBackgroundEnabled: enabled }); - console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 更新后端调用完成。`); // 添加日志 + console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 更新后端调用完成。`); } /** diff --git a/packages/frontend/src/stores/auth.store.ts b/packages/frontend/src/stores/auth.store.ts index 42078b9..ef0e956 100644 --- a/packages/frontend/src/stores/auth.store.ts +++ b/packages/frontend/src/stores/auth.store.ts @@ -55,17 +55,17 @@ interface AuthState { user: UserInfo | null; isLoading: boolean; error: string | null; - loginRequires2FA: boolean; // 新增状态:标记登录是否需要 2FA + loginRequires2FA: boolean; // 存储 IP 黑名单数据 (虽然 actions 在这里,但 state 结构保持) ipBlacklist: { entries: any[]; // TODO: Define a proper type for blacklist entries total: number; }; needsSetup: boolean; // 是否需要初始设置 - publicCaptchaConfig: PublicCaptchaConfig | null; // Public CAPTCHA config - passkeys: PasskeyInfo[] | null; // Store for user's passkeys - passkeysLoading: boolean; // Loading state for passkeys - hasPasskeysAvailable: boolean; // Indicates if passkeys are available for login + publicCaptchaConfig: PublicCaptchaConfig | null; + passkeys: PasskeyInfo[] | null; + passkeysLoading: boolean; + hasPasskeysAvailable: boolean; } export const useAuthStore = defineStore('auth', { diff --git a/packages/frontend/src/stores/connections.store.ts b/packages/frontend/src/stores/connections.store.ts index 7296639..ac22593 100644 --- a/packages/frontend/src/stores/connections.store.ts +++ b/packages/frontend/src/stores/connections.store.ts @@ -16,7 +16,7 @@ export interface ConnectionInfo { created_at: number; updated_at: number; last_connected_at: number | null; -notes?: string | null; // 新增备注字段 +notes?: string | null; vncPassword?: string; // VNC specific password } diff --git a/packages/frontend/src/stores/layout.store.ts b/packages/frontend/src/stores/layout.store.ts index 1be2a1a..04ea470 100644 --- a/packages/frontend/src/stores/layout.store.ts +++ b/packages/frontend/src/stores/layout.store.ts @@ -411,7 +411,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { } - // 新增 Action: 更新特定容器节点的子节点大小 + // 更新特定容器节点的子节点大小 function updateNodeSizes(nodeId: string, childrenSizes: { index: number; size: number }[]) { console.log(`[Layout Store] 请求更新节点 ${nodeId} 的子节点大小:`, childrenSizes); const originalJson = JSON.stringify(layoutTree.value); // Store original state @@ -426,14 +426,14 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { console.log(`[Layout Store] 未找到节点 ${nodeId} 或大小未改变。`); } } - // 新增 Action: 切换布局(Header/Footer)的可见性 + // 切换布局(Header/Footer)的可见性 function toggleLayoutVisibility() { isLayoutVisible.value = !isLayoutVisible.value; console.log(`[Layout Store] 布局可见性切换为: ${isLayoutVisible.value}`); // 注意:这个状态目前不与后端同步 } - // 新增 Action: 从后端加载主导航栏可见性设置 + // 从后端加载主导航栏可见性设置 async function loadHeaderVisibility() { console.log('[Layout Store] Attempting to load header visibility from backend...'); try { @@ -453,7 +453,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { } } - // 新增 Action: 切换主导航栏可见性并同步到后端 + // 切换主导航栏可见性并同步到后端 async function toggleHeaderVisibility() { const newValue = !isHeaderVisible.value; console.log(`[Layout Store] Toggling header visibility to: ${newValue}`); @@ -471,19 +471,19 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { } } - // 新增 Action: 获取系统内置的默认布局 + // 获取系统内置的默认布局 function getSystemDefaultLayout(): LayoutNode { console.log('[Layout Store] Getting system default layout.'); return getDefaultLayout(); // 直接调用内部函数 } - // 新增 Action: 获取系统内置的默认侧栏配置 + // 获取系统内置的默认侧栏配置 function getSystemDefaultSidebarPanes(): { left: PaneName[], right: PaneName[] } { console.log('[Layout Store] Getting system default sidebar panes.'); return getDefaultSidebarPanes(); } - // 新增 Action: 将当前主布局树持久化到后端和 localStorage + // 将当前主布局树持久化到后端和 localStorage async function persistLayoutTree() { // Make async // ... (existing try/catch logic for backend and localStorage) ... // Ensure apiClient calls are awaited if they return promises @@ -504,7 +504,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { } } - // 新增 Action: 将当前侧栏配置持久化到后端和 localStorage + // 将当前侧栏配置持久化到后端和 localStorage async function persistSidebarPanes() { // Make async // ... (existing try/catch logic for backend and localStorage) ... try { diff --git a/packages/frontend/src/views/WorkspaceView.vue b/packages/frontend/src/views/WorkspaceView.vue index b235ec0..31cf551 100644 --- a/packages/frontend/src/views/WorkspaceView.vue +++ b/packages/frontend/src/views/WorkspaceView.vue @@ -214,7 +214,7 @@ const unsubscribeFromWorkspaceEvents = useWorkspaceEventOff(); // --- 本地方法 (仅处理 UI 状态) --- const handleRequestAddConnection = () => { - console.log('[WorkspaceView] handleRequestAddConnection 被调用!'); // 添加日志确认事件到达 + console.log('[WorkspaceView] handleRequestAddConnection 被调用!'); connectionToEdit.value = null; showAddEditForm.value = true; };