update
This commit is contained in:
@@ -107,7 +107,7 @@ export const getDbInstance = (): Promise<sqlite3.Database> => {
|
|||||||
await runDatabaseInitializations(db);
|
await runDatabaseInitializations(db);
|
||||||
// +++ 运行数据库迁移 +++
|
// +++ 运行数据库迁移 +++
|
||||||
await runMigrations(db);
|
await runMigrations(db);
|
||||||
console.log('[数据库] 初始化和迁移完成。'); // 添加日志确认
|
console.log('[数据库] 初始化和迁移完成。');
|
||||||
resolve(db);
|
resolve(db);
|
||||||
} catch (initError) {
|
} catch (initError) {
|
||||||
console.error('[数据库] 连接后初始化失败,正在关闭连接...');
|
console.error('[数据库] 连接后初始化失败,正在关闭连接...');
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import appearanceRoutes from './appearance/appearance.routes';
|
|||||||
import sshKeysRouter from './ssh_keys/ssh_keys.routes';
|
import sshKeysRouter from './ssh_keys/ssh_keys.routes';
|
||||||
import quickCommandTagRoutes from './quick-command-tags/quick-command-tag.routes';
|
import quickCommandTagRoutes from './quick-command-tags/quick-command-tag.routes';
|
||||||
import sshSuspendRouter from './ssh-suspend/ssh-suspend.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 { initializeWebSocket } from './websocket';
|
||||||
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware';
|
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware';
|
||||||
|
|
||||||
@@ -69,29 +69,19 @@ import './services/notification.dispatcher.service';
|
|||||||
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
|
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
|
||||||
console.error('---未处理的 Promise Rejection---');
|
console.error('---未处理的 Promise Rejection---');
|
||||||
console.error('原因:', reason);
|
console.error('原因:', reason);
|
||||||
// 可以在这里添加更详细的日志记录,例如将错误发送到监控系统
|
|
||||||
// 注意:根据 Node.js 官方建议,未来版本的 Node.js 可能会默认在 unhandledRejection 时终止进程。
|
|
||||||
// 目前我们选择记录错误并继续运行,但这可能导致应用程序状态不一致。
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 捕获未捕获的同步异常
|
// 捕获未捕获的同步异常
|
||||||
process.on('uncaughtException', (error: Error) => {
|
process.on('uncaughtException', (error: Error) => {
|
||||||
console.error('---未捕获的异常---');
|
console.error('---未捕获的异常---');
|
||||||
console.error('错误:', error);
|
console.error('错误:', error);
|
||||||
// 记录错误,但避免退出进程,尝试让服务器继续运行(有风险)
|
|
||||||
// 在生产环境中,更安全的做法可能是记录错误后优雅地关闭服务器并重启。
|
|
||||||
// process.exit(1); // 强制退出(更安全,但会中断服务)
|
|
||||||
});
|
});
|
||||||
// --- 结束全局错误处理 ---
|
|
||||||
|
|
||||||
|
|
||||||
const initializeEnvironment = async () => {
|
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;
|
||||||
const dataEnvPath = dataEnvPathGlobal; // Use the path defined at the top
|
|
||||||
let keysGenerated = false;
|
let keysGenerated = false;
|
||||||
let keysToAppend = '';
|
let keysToAppend = '';
|
||||||
|
|
||||||
@@ -117,16 +107,10 @@ const initializeEnvironment = async () => {
|
|||||||
if (!process.env.GUACD_HOST) {
|
if (!process.env.GUACD_HOST) {
|
||||||
console.warn('[ENV Init] GUACD_HOST 未设置,将使用默认值 "localhost"');
|
console.warn('[ENV Init] GUACD_HOST 未设置,将使用默认值 "localhost"');
|
||||||
process.env.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) {
|
if (!process.env.GUACD_PORT) {
|
||||||
console.warn('[ENV Init] GUACD_PORT 未设置,将使用默认值 "4822"');
|
console.warn('[ENV Init] GUACD_PORT 未设置,将使用默认值 "4822"');
|
||||||
process.env.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/quick-commands', quickCommandsRoutes);
|
||||||
app.use('/api/v1/terminal-themes', terminalThemeRoutes);
|
app.use('/api/v1/terminal-themes', terminalThemeRoutes);
|
||||||
app.use('/api/v1/appearance', appearanceRoutes);
|
app.use('/api/v1/appearance', appearanceRoutes);
|
||||||
app.use('/api/v1/ssh-keys', sshKeysRouter); // +++ Register SSH Key routes +++
|
app.use('/api/v1/ssh-keys', sshKeysRouter);
|
||||||
app.use('/api/v1/quick-command-tags', quickCommandTagRoutes); // +++ Register Quick Command Tag routes +++
|
app.use('/api/v1/quick-command-tags', quickCommandTagRoutes);
|
||||||
app.use('/api/v1/ssh-suspend', sshSuspendRouter); // +++ Register SSH Suspend routes +++
|
app.use('/api/v1/ssh-suspend', sshSuspendRouter);
|
||||||
app.use('/api/v1/transfers', transfersRoutes()); // 新增:注册传输路由
|
app.use('/api/v1/transfers', transfersRoutes());
|
||||||
|
|
||||||
// 状态检查接口
|
// 状态检查接口
|
||||||
app.get('/api/v1/status', (req: Request, res: Response) => {
|
app.get('/api/v1/status', (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
|
|||||||
{ key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // 数据库中使用空字符串
|
{ key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // 数据库中使用空字符串
|
||||||
{ key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // 数据库中使用空字符串
|
{ key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // 数据库中使用空字符串
|
||||||
{ key: 'terminalBackgroundEnabled', value: defaults.terminalBackgroundEnabled },
|
{ key: 'terminalBackgroundEnabled', value: defaults.terminalBackgroundEnabled },
|
||||||
{ key: 'terminalBackgroundOverlayOpacity', value: defaults.terminalBackgroundOverlayOpacity }, // 新增
|
{ key: 'terminalBackgroundOverlayOpacity', value: defaults.terminalBackgroundOverlayOpacity },
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/conn
|
|||||||
interface ConnectionBase {
|
interface ConnectionBase {
|
||||||
id: number;
|
id: number;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
type: 'SSH' | 'RDP' | 'VNC'; // Add type field
|
type: 'SSH' | 'RDP' | 'VNC';
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -16,8 +16,8 @@ interface ConnectionBase {
|
|||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
last_connected_at: number | null;
|
last_connected_at: number | null;
|
||||||
ssh_key_id?: number | null; // +++ Add ssh_key_id here as well +++
|
ssh_key_id?: number | null;
|
||||||
notes?: string | null; // 新增备注字段
|
notes?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionWithTagsRow implicitly includes 'type' and 'ssh_key_id' via ConnectionBase
|
// ConnectionWithTagsRow implicitly includes 'type' and 'ssh_key_id' via ConnectionBase
|
||||||
@@ -36,7 +36,7 @@ export interface FullConnectionData extends ConnectionBase {
|
|||||||
encrypted_password?: string | null;
|
encrypted_password?: string | null;
|
||||||
encrypted_private_key?: string | null;
|
encrypted_private_key?: string | null;
|
||||||
encrypted_passphrase?: string | null;
|
encrypted_passphrase?: string | null;
|
||||||
notes?: string | null; // 新增备注字段
|
notes?: string | null;
|
||||||
tag_ids?: number[];
|
tag_ids?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export class TransfersController {
|
|||||||
res.status(202).json(task); // 202 Accepted 表示请求已接受处理,但尚未完成
|
res.status(202).json(task); // 202 Accepted 表示请求已接受处理,但尚未完成
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[TransfersController] Error initiating transfer:', 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) });
|
res.status(500).json({ message: 'Failed to initiate transfer.', error: error instanceof Error ? error.message : String(error) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import * as fs from 'fs/promises';
|
|
||||||
import * as os from 'os';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as crypto from 'crypto';
|
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 { Client, ConnectConfig, SFTPWrapper } from 'ssh2';
|
||||||
import { InitiateTransferPayload, TransferTask, TransferSubTask } from './transfers.types';
|
import { InitiateTransferPayload, TransferTask, TransferSubTask } from './transfers.types';
|
||||||
import { getConnectionWithDecryptedCredentials } from '../services/connection.service';
|
import { getConnectionWithDecryptedCredentials } from '../services/connection.service';
|
||||||
import type { ConnectionWithTags, DecryptedConnectionCredentials } from '../types/connection.types';
|
import type { ConnectionWithTags, DecryptedConnectionCredentials } from '../types/connection.types';
|
||||||
// import { logger } from '../utils/logger'; // 假设的日志工具路径
|
|
||||||
|
|
||||||
export class TransfersService {
|
export class TransfersService {
|
||||||
private transferTasks: Map<string, TransferTask> = new Map();
|
private transferTasks: Map<string, TransferTask> = new Map();
|
||||||
private taskAbortControllers: Map<string, AbortController> = new Map(); // +++ 用于存储任务的 AbortController +++
|
private taskAbortControllers: Map<string, AbortController> = new Map(); // +++ 用于存储任务的 AbortController +++
|
||||||
private readonly TEMP_KEY_PREFIX = 'nexus_target_key_';
|
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() {
|
constructor() {
|
||||||
console.info('[TransfersService] Initialized.');
|
console.info('[TransfersService] Initialized.');
|
||||||
@@ -23,8 +21,8 @@ export class TransfersService {
|
|||||||
const taskId = uuidv4();
|
const taskId = uuidv4();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const subTasks: TransferSubTask[] = [];
|
const subTasks: TransferSubTask[] = [];
|
||||||
const abortController = new AbortController(); // +++ 创建 AbortController +++
|
const abortController = new AbortController();
|
||||||
this.taskAbortControllers.set(taskId, abortController); // +++ 存储 AbortController +++
|
this.taskAbortControllers.set(taskId, abortController);
|
||||||
|
|
||||||
// 每个 (目标服务器, 源文件) 组合都是一个子任务
|
// 每个 (目标服务器, 源文件) 组合都是一个子任务
|
||||||
for (const connectionId of payload.connectionIds) { // 目标服务器ID列表
|
for (const connectionId of payload.connectionIds) { // 目标服务器ID列表
|
||||||
@@ -91,10 +89,6 @@ export class TransfersService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 确保在 AbortController Map 中移除,以防内存泄漏(如果任务不再处理)
|
|
||||||
// 也可以在任务彻底结束后移除
|
|
||||||
// this.taskAbortControllers.delete(taskId); // 暂时不在这里删除,可能在 processTransferTask 的 finally 中
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
console.warn(`[TransfersService] No AbortController found for task ${taskId} to cancel.`);
|
console.warn(`[TransfersService] No AbortController found for task ${taskId} to cancel.`);
|
||||||
@@ -299,7 +293,6 @@ export class TransfersService {
|
|||||||
});
|
});
|
||||||
subTaskExecutionPromises.push(taskPromise);
|
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) {
|
if (currentSubTaskIndex === totalSubTasks && currentlyActiveSubTasks === 0 && !signal.aborted) {
|
||||||
console.info(`[TransfersService] Task ${taskId}: All sub-tasks processed (no active, no more to launch).`);
|
console.info(`[TransfersService] Task ${taskId}: All sub-tasks processed (no active, no more to launch).`);
|
||||||
signal.removeEventListener('abort', onAbortOverall);
|
signal.removeEventListener('abort', onAbortOverall);
|
||||||
@@ -343,7 +336,7 @@ export class TransfersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.finalizeOverallTaskStatus(taskId); // Ensure final status is set
|
this.finalizeOverallTaskStatus(taskId); // Ensure final status is set
|
||||||
this.taskAbortControllers.delete(taskId); // +++ Clean up AbortController +++
|
this.taskAbortControllers.delete(taskId);
|
||||||
if (task) { // task 可能未定义如果 taskId 错误
|
if (task) { // task 可能未定义如果 taskId 错误
|
||||||
console.info(`[TransfersService] Task ${taskId} processing finished. Final status: ${task.status}.`);
|
console.info(`[TransfersService] Task ${taskId} processing finished. Final status: ${task.status}.`);
|
||||||
} else {
|
} else {
|
||||||
@@ -355,10 +348,8 @@ export class TransfersService {
|
|||||||
private async checkCommandOnSource(client: Client, command: string): Promise<string | null> {
|
private async checkCommandOnSource(client: Client, command: string): Promise<string | null> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const checkCmd = `command -v ${this.escapeShellArg(command)} 2>/dev/null`;
|
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) => {
|
client.exec(checkCmd, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.warn(`[Roo Debug][transfers.service.ts] Error checking for command '${command}' on source:`, err);
|
|
||||||
return resolve(null);
|
return resolve(null);
|
||||||
}
|
}
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
@@ -367,15 +358,12 @@ export class TransfersService {
|
|||||||
.on('close', (code: number) => {
|
.on('close', (code: number) => {
|
||||||
const foundPath = stdout.trim();
|
const foundPath = stdout.trim();
|
||||||
if (code === 0 && foundPath) {
|
if (code === 0 && foundPath) {
|
||||||
console.error(`[Roo Debug][transfers.service.ts] checkCommandOnSource: Command '${command}' found at '${foundPath}'.`);
|
|
||||||
resolve(foundPath);
|
resolve(foundPath);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnSource: Command '${command}' not found (exit code: ${code}).`);
|
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.stderr.on('data', (data: Buffer) => { // Should be empty due to 2>/dev/null, but good to have
|
.stderr.on('data', (data: Buffer) => {
|
||||||
console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnSource: STDERR for '${command}': ${data.toString()}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -386,7 +374,6 @@ export class TransfersService {
|
|||||||
const connectConfig = this.buildSshConnectConfig(targetConnection, targetCredentials);
|
const connectConfig = this.buildSshConnectConfig(targetConnection, targetCredentials);
|
||||||
let foundCommandPath: string | null = null;
|
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 {
|
try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -407,10 +394,8 @@ export class TransfersService {
|
|||||||
|
|
||||||
foundCommandPath = await new Promise((resolve) => {
|
foundCommandPath = await new Promise((resolve) => {
|
||||||
const checkCmd = `command -v ${this.escapeShellArg(command)} 2>/dev/null`;
|
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) => {
|
targetClient.exec(checkCmd, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.warn(`[Roo Debug][transfers.service.ts] Error checking for command '${command}' on target ${targetConnection.host}:`, err);
|
|
||||||
return resolve(null);
|
return resolve(null);
|
||||||
}
|
}
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
@@ -419,20 +404,16 @@ export class TransfersService {
|
|||||||
.on('close', (code: number) => {
|
.on('close', (code: number) => {
|
||||||
const pathOutput = stdout.trim();
|
const pathOutput = stdout.trim();
|
||||||
if (code === 0 && pathOutput) {
|
if (code === 0 && pathOutput) {
|
||||||
console.error(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Command '${command}' found at '${pathOutput}' on target ${targetConnection.host}.`);
|
|
||||||
resolve(pathOutput);
|
resolve(pathOutput);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: Command '${command}' not found on target ${targetConnection.host} (exit code: ${code}).`);
|
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.stderr.on('data', (data: Buffer) => {
|
.stderr.on('data', (data: Buffer) => {
|
||||||
console.warn(`[Roo Debug][transfers.service.ts] checkCommandOnTargetServer: STDERR for '${command}' on target ${targetConnection.host}: ${data.toString()}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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
|
foundCommandPath = null; // Ensure it's null on error
|
||||||
} finally {
|
} finally {
|
||||||
targetClient.end();
|
targetClient.end();
|
||||||
@@ -441,7 +422,6 @@ export class TransfersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async uploadKeyToSourceViaSftp(client: Client, privateKeyContent: string, remotePath: string): Promise<void> {
|
private async uploadKeyToSourceViaSftp(client: Client, privateKeyContent: string, remotePath: string): Promise<void> {
|
||||||
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
|
const SFTP_UPLOAD_TIMEOUT_MS = 30000; // 30 seconds timeout for SFTP key upload
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -450,8 +430,6 @@ export class TransfersService {
|
|||||||
|
|
||||||
const cleanupAndReject = (errMsg: string, errObj?: any) => {
|
const cleanupAndReject = (errMsg: string, errObj?: any) => {
|
||||||
if (timeoutHandle) clearTimeout(timeoutHandle);
|
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();
|
sftpSession?.end();
|
||||||
reject(new Error(errMsg));
|
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.`);
|
cleanupAndReject(`SFTP upload to ${remotePath} timed out after ${SFTP_UPLOAD_TIMEOUT_MS / 1000}s.`);
|
||||||
}, SFTP_UPLOAD_TIMEOUT_MS);
|
}, 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) => {
|
client.sftp((err, sftp) => {
|
||||||
sftpSession = sftp; // Store session for potential cleanup
|
sftpSession = sftp; // Store session for potential cleanup
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -469,7 +446,6 @@ export class TransfersService {
|
|||||||
if (!sftp) {
|
if (!sftp) {
|
||||||
return cleanupAndReject(`SFTP session error: SFTP object is null.`);
|
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 });
|
const stream = sftp.createWriteStream(remotePath, { mode: 0o600 });
|
||||||
|
|
||||||
stream.on('error', (writeErr: Error) => {
|
stream.on('error', (writeErr: Error) => {
|
||||||
@@ -479,19 +455,13 @@ export class TransfersService {
|
|||||||
// Listen to 'close' instead of 'finish' for more reliability
|
// Listen to 'close' instead of 'finish' for more reliability
|
||||||
stream.on('close', () => {
|
stream.on('close', () => {
|
||||||
if (timeoutHandle) clearTimeout(timeoutHandle);
|
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}`);
|
console.info(`[TransfersService] Private key for target successfully uploaded to source at ${remotePath}`);
|
||||||
sftp.end();
|
sftp.end();
|
||||||
resolve();
|
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;
|
let keyContentToWrite = privateKeyContent;
|
||||||
if (!keyContentToWrite.endsWith('\n')) {
|
if (!keyContentToWrite.endsWith('\n')) {
|
||||||
console.error(`[Roo Debug][transfers.service.ts] uploadKeyToSourceViaSftp: privateKeyContent does not end with a newline. Appending one.`);
|
|
||||||
keyContentToWrite += '\n';
|
keyContentToWrite += '\n';
|
||||||
}
|
}
|
||||||
stream.end(keyContentToWrite);
|
stream.end(keyContentToWrite);
|
||||||
@@ -544,8 +514,7 @@ export class TransfersService {
|
|||||||
commandParts.push(options.sshPassCommand);
|
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);
|
commandParts.push(executableCommand);
|
||||||
|
|
||||||
if (commandType === 'rsync') {
|
if (commandType === 'rsync') {
|
||||||
@@ -568,12 +537,6 @@ export class TransfersService {
|
|||||||
commandParts.push(remoteFullDest);
|
commandParts.push(remoteFullDest);
|
||||||
|
|
||||||
} else { // scp
|
} 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 StrictHostKeyChecking=no'); // For scp, pass as direct option
|
||||||
commandParts.push('-o UserKnownHostsFile=/dev/null'); // For scp, pass as direct option
|
commandParts.push('-o UserKnownHostsFile=/dev/null'); // For scp, pass as direct option
|
||||||
if (isDir) commandParts.push('-r');
|
if (isDir) commandParts.push('-r');
|
||||||
@@ -601,14 +564,12 @@ private async executeRemoteTransferOnSource(
|
|||||||
transferMethodPreference: 'auto' | 'rsync' | 'scp',
|
transferMethodPreference: 'auto' | 'rsync' | 'scp',
|
||||||
signal: AbortSignal // +++ Add AbortSignal parameter +++
|
signal: AbortSignal // +++ Add AbortSignal parameter +++
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
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');
|
if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError');
|
||||||
this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 0, `Initializing remote transfer for ${sourceItem.name}`);
|
this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 0, `Initializing remote transfer for ${sourceItem.name}`);
|
||||||
let tempTargetKeyPathOnSource: string | undefined;
|
let tempTargetKeyPathOnSource: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError');
|
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.
|
// 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 */);
|
const sshpassPath = await this.checkCommandOnSource(sourceSshClient, 'sshpass' /*, signal */);
|
||||||
if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError');
|
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 */);
|
const scpPathOnSource = await this.checkCommandOnSource(sourceSshClient, 'scp' /*, signal */);
|
||||||
if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError');
|
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 executableCommandPath: string | null = null;
|
||||||
let commandTypeForLogic: 'rsync' | 'scp' | undefined = undefined; // Initialize as undefined
|
let commandTypeForLogic: 'rsync' | 'scp' | undefined = undefined; // Initialize as undefined
|
||||||
@@ -626,7 +586,6 @@ private async executeRemoteTransferOnSource(
|
|||||||
if (transferMethodPreference === 'auto') {
|
if (transferMethodPreference === 'auto') {
|
||||||
if (rsyncPathOnSource) {
|
if (rsyncPathOnSource) {
|
||||||
// Source has rsync, check target
|
// 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 */);
|
rsyncPathOnTarget = await this.checkCommandOnTargetServer(targetConnection, targetCredentials, 'rsync' /*, signal */);
|
||||||
if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError');
|
if (signal.aborted) throw new DOMException('Transfer cancelled by user.', 'AbortError');
|
||||||
if (rsyncPathOnTarget) {
|
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}.`);
|
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 targetClientForMkdir = new Client();
|
||||||
const targetConnectConfigForMkdir = this.buildSshConnectConfig(targetConnection, targetCredentials);
|
const targetConnectConfigForMkdir = this.buildSshConnectConfig(targetConnection, targetCredentials);
|
||||||
try {
|
try {
|
||||||
@@ -685,8 +643,8 @@ private async executeRemoteTransferOnSource(
|
|||||||
await new Promise<void>((resolveMkdir, rejectMkdir) => {
|
await new Promise<void>((resolveMkdir, rejectMkdir) => {
|
||||||
let mkdirStreamClosed = false;
|
let mkdirStreamClosed = false;
|
||||||
const onAbortMkdir = () => {
|
const onAbortMkdir = () => {
|
||||||
if (!mkdirStreamClosed) { // Only if stream/connection is still active
|
if (!mkdirStreamClosed) {
|
||||||
targetClientForMkdir.end(); // Attempt to close the connection
|
targetClientForMkdir.end();
|
||||||
}
|
}
|
||||||
rejectMkdir(new DOMException('Mkdir operation cancelled by user.', 'AbortError'));
|
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'));
|
return rejectMkdir(new DOMException('Mkdir operation cancelled by user (on ready).', 'AbortError'));
|
||||||
}
|
}
|
||||||
const mkdirCommand = `mkdir -p ${this.escapeShellArg(remoteTargetPathOnTarget)}`;
|
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) => {
|
targetClientForMkdir.exec(mkdirCommand, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
signal.removeEventListener('abort', onAbortMkdir);
|
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()}`));
|
rejectMkdir(new Error(`Failed to create directory ${remoteTargetPathOnTarget} on ${targetConnection.host}. Exit code: ${code}. Stderr: ${mkdirStderr.trim()}`));
|
||||||
}
|
}
|
||||||
}).on('data', (data: Buffer) => {
|
}).on('data', (data: Buffer) => {
|
||||||
// stdout from mkdir -p is usually empty
|
|
||||||
}).stderr.on('data', (data: Buffer) => {
|
}).stderr.on('data', (data: Buffer) => {
|
||||||
mkdirStderr += data.toString();
|
mkdirStderr += data.toString();
|
||||||
console.warn(`[Roo Debug][transfers.service.ts] Sub-task ${subTaskId}: STDERR (mkdir on target): ${data.toString()}`);
|
}).on('error', (streamErr: Error) => {
|
||||||
}).on('error', (streamErr: Error) => { // Handle stream errors specifically
|
|
||||||
mkdirStreamClosed = true;
|
mkdirStreamClosed = true;
|
||||||
signal.removeEventListener('abort', onAbortMkdir);
|
signal.removeEventListener('abort', onAbortMkdir);
|
||||||
targetClientForMkdir.end();
|
targetClientForMkdir.end();
|
||||||
@@ -731,11 +686,9 @@ private async executeRemoteTransferOnSource(
|
|||||||
});
|
});
|
||||||
}).on('error', (connErr: Error) => {
|
}).on('error', (connErr: Error) => {
|
||||||
signal.removeEventListener('abort', onAbortMkdir);
|
signal.removeEventListener('abort', onAbortMkdir);
|
||||||
// targetClientForMkdir.end(); // .end() might not be needed if 'close' always follows 'error'
|
|
||||||
rejectMkdir(connErr);
|
rejectMkdir(connErr);
|
||||||
}).on('close', () => { // This 'close' is for the client connection itself
|
}).on('close', () => {
|
||||||
signal.removeEventListener('abort', onAbortMkdir); // Ensure cleanup if closed for other reasons
|
signal.removeEventListener('abort', onAbortMkdir);
|
||||||
// console.info(`[TransfersService] SSH connection for mkdir to target ${targetConnection.host} closed.`);
|
|
||||||
}).connect(targetConnectConfigForMkdir);
|
}).connect(targetConnectConfigForMkdir);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -743,17 +696,14 @@ private async executeRemoteTransferOnSource(
|
|||||||
this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 8, `Target directory ensured. Preparing transfer command.`);
|
this.updateSubTaskStatus(taskId, subTaskId, 'transferring', 8, `Target directory ensured. Preparing transfer command.`);
|
||||||
|
|
||||||
} catch (mkdirError: any) {
|
} 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) {
|
if (targetClientForMkdir && (targetClientForMkdir as any)._sock && !(targetClientForMkdir as any)._sock.destroyed) {
|
||||||
try { targetClientForMkdir.end(); } catch (e) { /* ignore */ }
|
try { targetClientForMkdir.end(); } catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
console.error(`[TransfersService] Sub-task ${subTaskId}: Failed to ensure target directory ${remoteTargetPathOnTarget} on ${targetConnection.host}:`, mkdirError.message);
|
console.error(`[TransfersService] Sub-task ${subTaskId}: Failed to ensure target directory ${remoteTargetPathOnTarget} on ${targetConnection.host}:`, mkdirError.message);
|
||||||
if (mkdirError.name === 'AbortError') {
|
if (mkdirError.name === 'AbortError') {
|
||||||
this.updateSubTaskStatus(taskId, subTaskId, 'cancelled', undefined, `Directory creation cancelled: ${mkdirError.message}`);
|
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}`);
|
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
|
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 = () => {
|
const onAbortCmd = () => {
|
||||||
if (!streamClosed) {
|
if (!streamClosed) {
|
||||||
console.warn(`[TransfersService] Abort signal received for command stream of sub-task ${subTaskId}. Attempting to close stream.`);
|
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'));
|
rejectCmd(new DOMException('Command cancelled by user.', 'AbortError'));
|
||||||
};
|
};
|
||||||
@@ -840,7 +786,6 @@ private async executeRemoteTransferOnSource(
|
|||||||
stream.stderr.on('data', (data: Buffer) => {
|
stream.stderr.on('data', (data: Buffer) => {
|
||||||
if (signal.aborted) return;
|
if (signal.aborted) return;
|
||||||
stderrCombined += data.toString();
|
stderrCombined += data.toString();
|
||||||
console.warn(`[Roo Debug][transfers.service.ts] STDERR Sub-task ${subTaskId}: ${data.toString()}`);
|
|
||||||
});
|
});
|
||||||
stream.on('close', (code: number | null) => {
|
stream.on('close', (code: number | null) => {
|
||||||
streamClosed = true;
|
streamClosed = true;
|
||||||
@@ -882,7 +827,6 @@ private async executeRemoteTransferOnSource(
|
|||||||
}
|
}
|
||||||
throw error; // Re-throw to be caught by processSingleSubTaskWrapper
|
throw error; // Re-throw to be caught by processSingleSubTaskWrapper
|
||||||
} finally {
|
} finally {
|
||||||
console.info(`[Roo Debug][transfers.service.ts] executeRemoteTransferOnSource FINALLY for sub-task ${subTaskId}`);
|
|
||||||
if (tempTargetKeyPathOnSource) {
|
if (tempTargetKeyPathOnSource) {
|
||||||
try {
|
try {
|
||||||
// TODO: Make deleteFileOnSourceViaSftp accept signal
|
// TODO: Make deleteFileOnSourceViaSftp accept signal
|
||||||
@@ -1001,7 +945,6 @@ private async executeRemoteTransferOnSource(
|
|||||||
|
|
||||||
if (numSubTasks === 0) {
|
if (numSubTasks === 0) {
|
||||||
task.overallProgress = 0;
|
task.overallProgress = 0;
|
||||||
// task.status remains as set by initiate or direct updateOverallTaskStatus if no subtasks.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,12 +956,9 @@ private async executeRemoteTransferOnSource(
|
|||||||
break;
|
break;
|
||||||
case 'failed':
|
case 'failed':
|
||||||
failedCount++;
|
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;
|
break;
|
||||||
case 'transferring':
|
case 'transferring':
|
||||||
case 'connecting': // consider connecting as in-progress for overall status
|
case 'connecting':
|
||||||
inProgressCount++;
|
inProgressCount++;
|
||||||
totalProgress += (st.progress !== undefined ? st.progress : (st.status === 'connecting' ? 5 : 0)); // Small progress for connecting
|
totalProgress += (st.progress !== undefined ? st.progress : (st.status === 'connecting' ? 5 : 0)); // Small progress for connecting
|
||||||
break;
|
break;
|
||||||
@@ -1043,8 +983,6 @@ private async executeRemoteTransferOnSource(
|
|||||||
} else if (queuedCount === numSubTasks) {
|
} else if (queuedCount === numSubTasks) {
|
||||||
newOverallStatus = 'queued'; // All subtasks are still queued
|
newOverallStatus = 'queued'; // All subtasks are still queued
|
||||||
} else {
|
} 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
|
newOverallStatus = 'in-progress'; // Or 'partially-completed' if completedCount > 0
|
||||||
if (completedCount > 0 && queuedCount > 0 && failedCount === 0 && inProgressCount === 0) {
|
if (completedCount > 0 && queuedCount > 0 && failedCount === 0 && inProgressCount === 0) {
|
||||||
newOverallStatus = 'partially-completed'; // More accurate for this specific mix
|
newOverallStatus = 'partially-completed'; // More accurate for this specific mix
|
||||||
@@ -1056,7 +994,6 @@ private async executeRemoteTransferOnSource(
|
|||||||
task.status = newOverallStatus;
|
task.status = newOverallStatus;
|
||||||
}
|
}
|
||||||
task.updatedAt = new Date();
|
task.updatedAt = new Date();
|
||||||
// console.debug(`[TransfersService] Task ${taskId} overall progress: ${task.overallProgress}%, status: ${task.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private finalizeOverallTaskStatus(taskId: string): void {
|
private finalizeOverallTaskStatus(taskId: string): void {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface ConnectionBase {
|
|||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
last_connected_at: number | null;
|
last_connected_at: number | null;
|
||||||
notes?: string | null; // 新增备注字段
|
notes?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectionWithTags extends ConnectionBase {
|
export interface ConnectionWithTags extends ConnectionBase {
|
||||||
@@ -31,7 +31,7 @@ export interface CreateConnectionInput {
|
|||||||
ssh_key_id?: number | null; // +++ Add ssh_key_id +++
|
ssh_key_id?: number | null; // +++ Add ssh_key_id +++
|
||||||
proxy_id?: number | null;
|
proxy_id?: number | null;
|
||||||
tag_ids?: number[];
|
tag_ids?: number[];
|
||||||
notes?: string | null; // 新增备注字段
|
notes?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export interface UpdateConnectionInput {
|
|||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
ssh_key_id?: number | null; // +++ Add ssh_key_id +++
|
ssh_key_id?: number | null; // +++ Add ssh_key_id +++
|
||||||
proxy_id?: number | null;
|
proxy_id?: number | null;
|
||||||
notes?: string | null; // 新增备注字段
|
notes?: string | null;
|
||||||
tag_ids?: number[];
|
tag_ids?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,10 +63,10 @@ export interface FullConnectionData {
|
|||||||
encrypted_password: string | null;
|
encrypted_password: string | null;
|
||||||
encrypted_private_key: string | null;
|
encrypted_private_key: string | null;
|
||||||
encrypted_passphrase: string | null;
|
encrypted_passphrase: string | null;
|
||||||
ssh_key_id?: number | null; // +++ Add ssh_key_id +++
|
ssh_key_id?: number | null;
|
||||||
proxy_id: number | null;
|
proxy_id: number | null;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
notes: string | null; // 新增备注字段 (数据库原始字段)
|
notes: string | null;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
last_connected_at: number | null;
|
last_connected_at: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const initialFormData = {
|
|||||||
port: 1080, // 默认 SOCKS5 端口
|
port: 1080, // 默认 SOCKS5 端口
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
tag_ids: [] as number[], // 新增 tag_ids 字段
|
tag_ids: [] as number[],
|
||||||
};
|
};
|
||||||
const formData = reactive({ ...initialFormData });
|
const formData = reactive({ ...initialFormData });
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick, type PropType, onUnmounted, computed } from 'vue'; // Added watch, nextTick, computed
|
import { ref, watch, nextTick, type PropType, onUnmounted, computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import SendFilesModal from './SendFilesModal.vue';
|
import SendFilesModal from './SendFilesModal.vue';
|
||||||
import type { ContextMenuItem } from '../composables/file-manager/useFileManagerContextMenu';
|
import type { ContextMenuItem } from '../composables/file-manager/useFileManagerContextMenu';
|
||||||
import type { FileListItem } from '../types/sftp.types'; // Import FileListItem
|
import type { FileListItem } from '../types/sftp.types';
|
||||||
import { useDeviceDetection } from '../composables/useDeviceDetection';
|
import { useDeviceDetection } from '../composables/useDeviceDetection';
|
||||||
import { useSessionStore } from '../stores/session.store'; // +++ 导入 session store +++
|
import { useSessionStore } from '../stores/session.store';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isVisible: {
|
isVisible: {
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ const connectionStatus = ref<'disconnected' | 'connecting' | 'connected' | 'erro
|
|||||||
const isResizing = ref(false);
|
const isResizing = ref(false);
|
||||||
const resizeStartX = ref(0);
|
const resizeStartX = ref(0);
|
||||||
const resizeStartY = ref(0);
|
const resizeStartY = ref(0);
|
||||||
const initialModalWidthForResize = ref(0); // Renamed to avoid conflict if other 'initialModalWidth' exists
|
const initialModalWidthForResize = ref(0);
|
||||||
const initialModalHeightForResize = ref(0); // Renamed
|
const initialModalHeightForResize = ref(0);
|
||||||
const statusMessage = ref('');
|
const statusMessage = ref('');
|
||||||
const keyboard = ref<any | null>(null);
|
const keyboard = ref<any | null>(null);
|
||||||
const mouse = ref<any | null>(null);
|
const mouse = ref<any | null>(null);
|
||||||
@@ -42,7 +42,7 @@ const isDraggingRestoreButton = ref(false);
|
|||||||
const restoreButtonPosition = ref({ x: 16, y: window.innerHeight / 2 - 25 }); // 16px from left, vertically centered
|
const restoreButtonPosition = ref({ x: 16, y: window.innerHeight / 2 - 25 }); // 16px from left, vertically centered
|
||||||
let dragOffsetX = 0;
|
let dragOffsetX = 0;
|
||||||
let dragOffsetY = 0;
|
let dragOffsetY = 0;
|
||||||
let hasDragged = false; // 新增 hasDragged 标志
|
let hasDragged = false;
|
||||||
|
|
||||||
const MIN_MODAL_WIDTH = 1024;
|
const MIN_MODAL_WIDTH = 1024;
|
||||||
const MIN_MODAL_HEIGHT = 768;
|
const MIN_MODAL_HEIGHT = 768;
|
||||||
@@ -431,7 +431,7 @@ watch(desiredModalWidth, (newWidth, oldWidth) => {
|
|||||||
console.log(`[RDP 模态框] 宽度监听触发,但值 (${newWidth}) 未改变。跳过保存。`);
|
console.log(`[RDP 模态框] 宽度监听触发,但值 (${newWidth}) 未改变。跳过保存。`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[RDP 模态框] 监听 desiredModalWidth 触发: ${oldWidth} -> ${newWidth}`); // 添加日志
|
console.log(`[RDP 模态框] 监听 desiredModalWidth 触发: ${oldWidth} -> ${newWidth}`);
|
||||||
// 保存前验证新宽度
|
// 保存前验证新宽度
|
||||||
const validatedWidth = Math.max(MIN_MODAL_WIDTH, Number(newWidth) || MIN_MODAL_WIDTH);
|
const validatedWidth = Math.max(MIN_MODAL_WIDTH, Number(newWidth) || MIN_MODAL_WIDTH);
|
||||||
// 防抖保存 *验证后* 的宽度
|
// 防抖保存 *验证后* 的宽度
|
||||||
|
|||||||
@@ -210,11 +210,6 @@ const getGroupId = (group: GroupedConnection): string => {
|
|||||||
|
|
||||||
const toggleTagGroupExpansion = (group: GroupedConnection) => {
|
const toggleTagGroupExpansion = (group: GroupedConnection) => {
|
||||||
const groupId = getGroupId(group);
|
const groupId = getGroupId(group);
|
||||||
// If state is undefined, default to true (expanded), then toggle it.
|
|
||||||
// So, if undefined, it becomes !(true) = false. If defined, it's just toggled.
|
|
||||||
// To make it default to expanded and then collapse on first click:
|
|
||||||
// expandedTagGroups.value[groupId] = !(expandedTagGroups.value[groupId] ?? true);
|
|
||||||
// To make it default to collapsed and then expand on first click (if true means expanded):
|
|
||||||
expandedTagGroups.value[groupId] = !(expandedTagGroups.value[groupId] ?? true);
|
expandedTagGroups.value[groupId] = !(expandedTagGroups.value[groupId] ?? true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -276,8 +271,6 @@ const groupedConnections = computed<GroupedConnection[]>(() => {
|
|||||||
groups[tag.id].connections.push(conn);
|
groups[tag.id].connections.push(conn);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Connection has a tag ID that doesn't exist in tagsStore, treat as untagged for this modal
|
|
||||||
// Or handle as an error, or create a "missing tag" group
|
|
||||||
if (!untaggedConnections.some(c => c.id === conn.id)) {
|
if (!untaggedConnections.some(c => c.id === conn.id)) {
|
||||||
untaggedConnections.push(conn);
|
untaggedConnections.push(conn);
|
||||||
}
|
}
|
||||||
@@ -359,11 +352,6 @@ const toggleTagGroupSelection = (group: GroupedConnection) => {
|
|||||||
|
|
||||||
watch(() => props.visible, (newValue) => {
|
watch(() => props.visible, (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
// Reset state when modal becomes visible, except perhaps targetPath if desired
|
|
||||||
// searchTerm.value = '';
|
|
||||||
// selectedConnectionIds.value = [];
|
|
||||||
// transferMethod.value = 'auto';
|
|
||||||
// If stores might be empty, fetch again or ensure they are fresh
|
|
||||||
if (connectionsStore.connections.length === 0) {
|
if (connectionsStore.connections.length === 0) {
|
||||||
connectionsStore.fetchConnections().catch(error => console.error(t('sendFilesModal.errorFetchingConnections'), error));
|
connectionsStore.fetchConnections().catch(error => console.error(t('sendFilesModal.errorFetchingConnections'), error));
|
||||||
}
|
}
|
||||||
@@ -428,7 +416,6 @@ const toggleIndividualConnectionSelection = (connectionId: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getConnectionIconClass = (connectionType?: string): string => {
|
const getConnectionIconClass = (connectionType?: string): string => {
|
||||||
// Ensure connectionType is treated as optional and provide a default if undefined
|
|
||||||
const type = connectionType?.toLowerCase();
|
const type = connectionType?.toLowerCase();
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'rdp': return 'fas fa-desktop';
|
case 'rdp': return 'fas fa-desktop';
|
||||||
@@ -438,9 +425,8 @@ const getConnectionIconClass = (connectionType?: string): string => {
|
|||||||
case 'local': return 'fas fa-laptop';
|
case 'local': return 'fas fa-laptop';
|
||||||
case 'serial': return 'fas fa-microchip';
|
case 'serial': return 'fas fa-microchip';
|
||||||
case 'docker': return 'fab fa-docker';
|
case 'docker': return 'fab fa-docker';
|
||||||
default: return 'fas fa-server'; // Default icon for unknown or undefined types
|
default: return 'fas fa-server';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fallback i18n messages are now removed as they are expected to be in the locale JSON files.
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ const initializeEditableState = () => {
|
|||||||
editableEditorFontSize.value = currentEditorFontSize.value; // <-- 新增
|
editableEditorFontSize.value = currentEditorFontSize.value; // <-- 新增
|
||||||
localTerminalBackgroundEnabled.value = isTerminalBackgroundEnabled.value; // <-- 重新添加:在此处初始化
|
localTerminalBackgroundEnabled.value = isTerminalBackgroundEnabled.value; // <-- 重新添加:在此处初始化
|
||||||
editableTerminalBackgroundOverlayOpacity.value = currentTerminalBackgroundOverlayOpacity.value; // 初始化蒙版透明度
|
editableTerminalBackgroundOverlayOpacity.value = currentTerminalBackgroundOverlayOpacity.value; // 初始化蒙版透明度
|
||||||
console.log(`[StyleCustomizer initializeEditableState] Initializing localTerminalBackgroundEnabled to: ${localTerminalBackgroundEnabled.value} (from store: ${isTerminalBackgroundEnabled.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 editableTerminalBackgroundOverlayOpacity to: ${editableTerminalBackgroundOverlayOpacity.value} (from store: ${currentTerminalBackgroundOverlayOpacity.value})`);
|
||||||
uploadError.value = null;
|
uploadError.value = null;
|
||||||
importError.value = null;
|
importError.value = null;
|
||||||
saveThemeError.value = null;
|
saveThemeError.value = null;
|
||||||
@@ -143,7 +143,7 @@ watch([
|
|||||||
newSettings?.terminalBackgroundEnabled !== oldSettings?.terminalBackgroundEnabled ||
|
newSettings?.terminalBackgroundEnabled !== oldSettings?.terminalBackgroundEnabled ||
|
||||||
newSettings?.terminalBackgroundOverlayOpacity !== oldSettings?.terminalBackgroundOverlayOpacity; // 检查相关设置是否变化
|
newSettings?.terminalBackgroundOverlayOpacity !== oldSettings?.terminalBackgroundOverlayOpacity; // 检查相关设置是否变化
|
||||||
if (!isEditingTheme.value || newActiveThemeId !== oldActiveThemeId || settingsChanged) {
|
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(); // 调用修改后的初始化函数
|
initializeEditableState(); // 调用修改后的初始化函数
|
||||||
} else {
|
} else {
|
||||||
// 如果正在编辑,只更新非编辑相关的部分 (不包括 UI 主题和终端背景开关,因为它们由 initializeEditableState 处理)
|
// 如果正在编辑,只更新非编辑相关的部分 (不包括 UI 主题和终端背景开关,因为它们由 initializeEditableState 处理)
|
||||||
@@ -165,10 +165,10 @@ watch(isTerminalBackgroundEnabled, (newValue) => {
|
|||||||
// 只有当本地状态与 store 状态不一致时才更新本地状态
|
// 只有当本地状态与 store 状态不一致时才更新本地状态
|
||||||
// 这避免了 handleToggleTerminalBackground 触发的不必要更新
|
// 这避免了 handleToggleTerminalBackground 触发的不必要更新
|
||||||
if (localTerminalBackgroundEnabled.value !== newValue) {
|
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;
|
localTerminalBackgroundEnabled.value = newValue;
|
||||||
} else {
|
} 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
|
// 移除单独监听 isTerminalBackgroundEnabled 的 watcher
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useRoute } from 'vue-router';
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue';
|
import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue';
|
||||||
import TabBarContextMenu from './TabBarContextMenu.vue';
|
import TabBarContextMenu from './TabBarContextMenu.vue';
|
||||||
import TransferProgressModal from './TransferProgressModal.vue'; // 导入传输进度模态框
|
import TransferProgressModal from './TransferProgressModal.vue';
|
||||||
import { useSessionStore } from '../stores/session.store';
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store';
|
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store';
|
||||||
import { useLayoutStore, type PaneName } from '../stores/layout.store';
|
import { useLayoutStore, type PaneName } from '../stores/layout.store';
|
||||||
@@ -15,7 +15,7 @@ import { useWorkspaceEventEmitter, useWorkspaceEventSubscriber, useWorkspaceEven
|
|||||||
import type { SessionTabInfoWithStatus } from '../stores/session/types'; // 路径修正
|
import type { SessionTabInfoWithStatus } from '../stores/session/types'; // 路径修正
|
||||||
|
|
||||||
|
|
||||||
const { t } = useI18n(); // 初始化 i18n
|
const { t } = useI18n();
|
||||||
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
|
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
|
||||||
const onWorkspaceEvent = useWorkspaceEventSubscriber(); // +++ 获取事件订阅器 +++
|
const onWorkspaceEvent = useWorkspaceEventSubscriber(); // +++ 获取事件订阅器 +++
|
||||||
const offWorkspaceEvent = useWorkspaceEventOff(); // +++ 获取事件取消订阅器 +++
|
const offWorkspaceEvent = useWorkspaceEventOff(); // +++ 获取事件取消订阅器 +++
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ interface TransferTask {
|
|||||||
updatedAt: string | Date;
|
updatedAt: string | Date;
|
||||||
subTasks: TransferSubTask[];
|
subTasks: TransferSubTask[];
|
||||||
overallProgress?: number;
|
overallProgress?: number;
|
||||||
sourceConnectionId?: number; // 新增:源连接ID (可选)
|
sourceConnectionId?: number;
|
||||||
remoteTargetPath?: string; // 新增:目标路径 (可选)
|
remoteTargetPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transferTasks = ref<TransferTask[]>([]);
|
const transferTasks = ref<TransferTask[]>([]);
|
||||||
@@ -133,8 +133,8 @@ const getDisplayStatus = (status: string): string => {
|
|||||||
'partially-completed': 'transferProgressModal.status.partiallyCompleted',
|
'partially-completed': 'transferProgressModal.status.partiallyCompleted',
|
||||||
'connecting': 'transferProgressModal.status.connecting',
|
'connecting': 'transferProgressModal.status.connecting',
|
||||||
'transferring': 'transferProgressModal.status.transferring',
|
'transferring': 'transferProgressModal.status.transferring',
|
||||||
'cancelling': 'transferProgressModal.status.cancelling', // +++ 新增状态翻译键 +++
|
'cancelling': 'transferProgressModal.status.cancelling',
|
||||||
'cancelled': 'transferProgressModal.status.cancelled', // +++ 新增状态翻译键 +++
|
'cancelled': 'transferProgressModal.status.cancelled',
|
||||||
};
|
};
|
||||||
// 提供一个默认的回退文本,以防i18n key缺失
|
// 提供一个默认的回退文本,以防i18n key缺失
|
||||||
const defaultText = status.charAt(0).toUpperCase() + status.slice(1).replace('-', ' ');
|
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'
|
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} 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已处理首次加载
|
}, { immediate: false }); // immediate: false 避免在组件初始化时立即执行,onMounted已处理首次加载
|
||||||
|
|
||||||
// --- 原有:模态框可见性控制 ---
|
// --- 模态框可见性控制 ---
|
||||||
const internalVisible = ref(props.visible);
|
const internalVisible = ref(props.visible);
|
||||||
|
|
||||||
// 监听 props.visible 的变化来更新 internalVisible
|
// 监听 props.visible 的变化来更新 internalVisible
|
||||||
@@ -221,17 +221,10 @@ const handleCancelTask = async (taskId: string) => {
|
|||||||
// 更新UI,将任务状态临时设置为 'cancelling' 或禁用按钮
|
// 更新UI,将任务状态临时设置为 'cancelling' 或禁用按钮
|
||||||
const task = transferTasks.value.find(t => t.taskId === taskId);
|
const task = transferTasks.value.find(t => t.taskId === taskId);
|
||||||
if (task) {
|
if (task) {
|
||||||
// 优选: 如果后端会快速更新状态并通过轮询反映, 前端可能不需要立即改变状态。
|
|
||||||
// 否则, 可以临时改变: task.status = 'cancelling';
|
|
||||||
// 另一种方法是添加一个 loading 状态到按钮上
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await apiClient.post(`/transfers/cancel/${taskId}`);
|
await apiClient.post(`/transfers/cancel/${taskId}`);
|
||||||
// 可以添加成功提示
|
|
||||||
// uiNotificationsStore.showSuccess(t('transferProgressModal.cancelRequested', '已发送终止请求。'));
|
|
||||||
|
|
||||||
// 前端优化:立即将任务状态设置为 'cancelling' 以提供即时反馈
|
|
||||||
// 这样用户点击后能马上看到状态变为“终止中”,后续轮询会从后端获取权威状态。
|
|
||||||
const taskBeingCancelled = transferTasks.value.find(t => t.taskId === taskId);
|
const taskBeingCancelled = transferTasks.value.find(t => t.taskId === taskId);
|
||||||
if (taskBeingCancelled && ['queued', 'in-progress', 'connecting', 'transferring'].includes(taskBeingCancelled.status)) {
|
if (taskBeingCancelled && ['queued', 'in-progress', 'connecting', 'transferring'].includes(taskBeingCancelled.status)) {
|
||||||
taskBeingCancelled.status = 'cancelling';
|
taskBeingCancelled.status = 'cancelling';
|
||||||
@@ -241,8 +234,6 @@ const handleCancelTask = async (taskId: string) => {
|
|||||||
fetchTransferTasks();
|
fetchTransferTasks();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`Failed to cancel task ${taskId}:`, error);
|
console.error(`Failed to cancel task ${taskId}:`, error);
|
||||||
// uiNotificationsStore.showError(error.response?.data?.message || error.message || t('transferProgressModal.error.cancelFailed', '终止任务失败。'));
|
|
||||||
// 如果任务状态之前被临时修改,可能需要回滚
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ const handleMenuAction = (action: 'add' | 'edit' | 'delete' | 'clone') => { //
|
|||||||
closeContextMenu(); // 先关闭菜单
|
closeContextMenu(); // 先关闭菜单
|
||||||
|
|
||||||
if (action === 'add') {
|
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'); // 改为触发事件
|
// router.push('/connections/add'); // 改为触发事件
|
||||||
emitWorkspaceEvent('connection:requestAdd');
|
emitWorkspaceEvent('connection:requestAdd');
|
||||||
} else if (conn) {
|
} else if (conn) {
|
||||||
@@ -516,7 +516,7 @@ const handleTagMenuAction = (action: 'connectAll' | 'manageTag' | 'deleteAllConn
|
|||||||
// 确保是已标记的组
|
// 确保是已标记的组
|
||||||
if (group.tagId === null) {
|
if (group.tagId === null) {
|
||||||
uiNotificationsStore.addNotification({
|
uiNotificationsStore.addNotification({
|
||||||
message: t('workspaceConnectionList.cannotDeleteFromUntagged'), // 新增i18n
|
message: t('workspaceConnectionList.cannotDeleteFromUntagged'),
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -524,13 +524,13 @@ const handleTagMenuAction = (action: 'connectAll' | 'manageTag' | 'deleteAllConn
|
|||||||
// 确保组内有连接
|
// 确保组内有连接
|
||||||
if (group.connections.length === 0) {
|
if (group.connections.length === 0) {
|
||||||
uiNotificationsStore.addNotification({
|
uiNotificationsStore.addNotification({
|
||||||
message: t('workspaceConnectionList.noConnectionsToDeleteInGroup', { groupName: group.groupName }), // 新增i18n
|
message: t('workspaceConnectionList.noConnectionsToDeleteInGroup', { groupName: group.groupName }),
|
||||||
type: 'info',
|
type: 'info',
|
||||||
});
|
});
|
||||||
return;
|
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 connectionIdsToDelete = group.connections.map(conn => conn.id);
|
||||||
|
|
||||||
const deletePromises = connectionIdsToDelete.map(connId =>
|
const deletePromises = connectionIdsToDelete.map(connId =>
|
||||||
@@ -547,13 +547,13 @@ const handleTagMenuAction = (action: 'connectAll' | 'manageTag' | 'deleteAllConn
|
|||||||
|
|
||||||
if (successfulDeletes > 0) {
|
if (successfulDeletes > 0) {
|
||||||
uiNotificationsStore.addNotification({
|
uiNotificationsStore.addNotification({
|
||||||
message: t('workspaceConnectionList.allConnectionsInGroupDeletedSuccess', { count: successfulDeletes, groupName: group.groupName }), // 新增i18n
|
message: t('workspaceConnectionList.allConnectionsInGroupDeletedSuccess', { count: successfulDeletes, groupName: group.groupName }),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (failedDeletes > 0) {
|
if (failedDeletes > 0) {
|
||||||
uiNotificationsStore.addNotification({
|
uiNotificationsStore.addNotification({
|
||||||
message: t('workspaceConnectionList.someConnectionsInGroupDeleteFailed', { count: failedDeletes, groupName: group.groupName }), // 新增i18n
|
message: t('workspaceConnectionList.someConnectionsInGroupDeleteFailed', { count: failedDeletes, groupName: group.groupName }),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -365,8 +365,7 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti
|
|||||||
|
|
||||||
// --- 返回状态和处理函数 ---
|
// --- 返回状态和处理函数 ---
|
||||||
return {
|
return {
|
||||||
// isDraggingOver, // 不再导出
|
showExternalDropOverlay,
|
||||||
showExternalDropOverlay, // 新增导出
|
|
||||||
dragOverTarget,
|
dragOverTarget,
|
||||||
draggedItem, // 需要暴露以供 handleDragOverRow 等函数内部判断
|
draggedItem, // 需要暴露以供 handleDragOverRow 等函数内部判断
|
||||||
// --- 事件处理器 ---
|
// --- 事件处理器 ---
|
||||||
@@ -374,7 +373,7 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti
|
|||||||
handleDragOver,
|
handleDragOver,
|
||||||
handleDragLeave,
|
handleDragLeave,
|
||||||
handleDrop, // 容器的 drop (主要用于清理)
|
handleDrop, // 容器的 drop (主要用于清理)
|
||||||
handleOverlayDrop, // 新增导出:蒙版的 drop
|
handleOverlayDrop,
|
||||||
handleDragStart,
|
handleDragStart,
|
||||||
handleDragEnd,
|
handleDragEnd,
|
||||||
handleDragOverRow,
|
handleDragOverRow,
|
||||||
|
|||||||
@@ -38,20 +38,20 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
|||||||
|
|
||||||
// 表单数据模型
|
// 表单数据模型
|
||||||
const initialFormData = {
|
const initialFormData = {
|
||||||
type: 'SSH' as 'SSH' | 'RDP' | 'VNC', // Use uppercase to match ConnectionInfo
|
type: 'SSH' as 'SSH' | 'RDP' | 'VNC',
|
||||||
name: '',
|
name: '',
|
||||||
host: '',
|
host: '',
|
||||||
port: 22,
|
port: 22,
|
||||||
username: '',
|
username: '',
|
||||||
auth_method: 'password' as 'password' | 'key', // SSH specific
|
auth_method: 'password' as 'password' | 'key',
|
||||||
password: '',
|
password: '',
|
||||||
private_key: '', // SSH specific (for direct input) - This field seems unused in the new logic, but kept for initialData consistency
|
private_key: '',
|
||||||
passphrase: '', // SSH specific (for direct input) - This field seems unused, kept for consistency
|
passphrase: '',
|
||||||
selected_ssh_key_id: null as number | null, // +++ Add field for selected key ID +++
|
selected_ssh_key_id: null as number | null,
|
||||||
proxy_id: null as number | null,
|
proxy_id: null as number | null,
|
||||||
tag_ids: [] as number[], // 新增 tag_ids 字段
|
tag_ids: [] as number[],
|
||||||
notes: '', // 新增备注字段
|
notes: '',
|
||||||
vncPassword: '', // VNC specific password
|
vncPassword: '',
|
||||||
};
|
};
|
||||||
const formData = reactive({ ...initialFormData });
|
const formData = reactive({ ...initialFormData });
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTerminalResize = (dimensions: { cols: number; rows: number }) => {
|
const handleTerminalResize = (dimensions: { cols: number; rows: number }) => {
|
||||||
// 添加日志,确认从 WorkspaceView 收到的尺寸
|
|
||||||
console.log(`[SSH ${sessionId}] handleTerminalResize called with:`, dimensions);
|
console.log(`[SSH ${sessionId}] handleTerminalResize called with:`, dimensions);
|
||||||
// 只有在连接状态下才发送 resize 命令给后端
|
// 只有在连接状态下才发送 resize 命令给后端
|
||||||
if (isConnected.value) {
|
if (isConnected.value) {
|
||||||
|
|||||||
@@ -235,9 +235,9 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
|||||||
* @param enabled 是否启用
|
* @param enabled 是否启用
|
||||||
*/
|
*/
|
||||||
async function setTerminalBackgroundEnabled(enabled: boolean) {
|
async function setTerminalBackgroundEnabled(enabled: boolean) {
|
||||||
console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 调用,准备发送给后端的值: ${enabled}`); // 添加日志
|
console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 调用,准备发送给后端的值: ${enabled}`);
|
||||||
await updateAppearanceSettings({ terminalBackgroundEnabled: enabled });
|
await updateAppearanceSettings({ terminalBackgroundEnabled: enabled });
|
||||||
console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 更新后端调用完成。`); // 添加日志
|
console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 更新后端调用完成。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -55,17 +55,17 @@ interface AuthState {
|
|||||||
user: UserInfo | null;
|
user: UserInfo | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
loginRequires2FA: boolean; // 新增状态:标记登录是否需要 2FA
|
loginRequires2FA: boolean;
|
||||||
// 存储 IP 黑名单数据 (虽然 actions 在这里,但 state 结构保持)
|
// 存储 IP 黑名单数据 (虽然 actions 在这里,但 state 结构保持)
|
||||||
ipBlacklist: {
|
ipBlacklist: {
|
||||||
entries: any[]; // TODO: Define a proper type for blacklist entries
|
entries: any[]; // TODO: Define a proper type for blacklist entries
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
needsSetup: boolean; // 是否需要初始设置
|
needsSetup: boolean; // 是否需要初始设置
|
||||||
publicCaptchaConfig: PublicCaptchaConfig | null; // Public CAPTCHA config
|
publicCaptchaConfig: PublicCaptchaConfig | null;
|
||||||
passkeys: PasskeyInfo[] | null; // Store for user's passkeys
|
passkeys: PasskeyInfo[] | null;
|
||||||
passkeysLoading: boolean; // Loading state for passkeys
|
passkeysLoading: boolean;
|
||||||
hasPasskeysAvailable: boolean; // Indicates if passkeys are available for login
|
hasPasskeysAvailable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface ConnectionInfo {
|
|||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
last_connected_at: number | null;
|
last_connected_at: number | null;
|
||||||
notes?: string | null; // 新增备注字段
|
notes?: string | null;
|
||||||
vncPassword?: string; // VNC specific password
|
vncPassword?: string; // VNC specific password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 新增 Action: 更新特定容器节点的子节点大小
|
// 更新特定容器节点的子节点大小
|
||||||
function updateNodeSizes(nodeId: string, childrenSizes: { index: number; size: number }[]) {
|
function updateNodeSizes(nodeId: string, childrenSizes: { index: number; size: number }[]) {
|
||||||
console.log(`[Layout Store] 请求更新节点 ${nodeId} 的子节点大小:`, childrenSizes);
|
console.log(`[Layout Store] 请求更新节点 ${nodeId} 的子节点大小:`, childrenSizes);
|
||||||
const originalJson = JSON.stringify(layoutTree.value); // Store original state
|
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} 或大小未改变。`);
|
console.log(`[Layout Store] 未找到节点 ${nodeId} 或大小未改变。`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 新增 Action: 切换布局(Header/Footer)的可见性
|
// 切换布局(Header/Footer)的可见性
|
||||||
function toggleLayoutVisibility() {
|
function toggleLayoutVisibility() {
|
||||||
isLayoutVisible.value = !isLayoutVisible.value;
|
isLayoutVisible.value = !isLayoutVisible.value;
|
||||||
console.log(`[Layout Store] 布局可见性切换为: ${isLayoutVisible.value}`);
|
console.log(`[Layout Store] 布局可见性切换为: ${isLayoutVisible.value}`);
|
||||||
// 注意:这个状态目前不与后端同步
|
// 注意:这个状态目前不与后端同步
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增 Action: 从后端加载主导航栏可见性设置
|
// 从后端加载主导航栏可见性设置
|
||||||
async function loadHeaderVisibility() {
|
async function loadHeaderVisibility() {
|
||||||
console.log('[Layout Store] Attempting to load header visibility from backend...');
|
console.log('[Layout Store] Attempting to load header visibility from backend...');
|
||||||
try {
|
try {
|
||||||
@@ -453,7 +453,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增 Action: 切换主导航栏可见性并同步到后端
|
// 切换主导航栏可见性并同步到后端
|
||||||
async function toggleHeaderVisibility() {
|
async function toggleHeaderVisibility() {
|
||||||
const newValue = !isHeaderVisible.value;
|
const newValue = !isHeaderVisible.value;
|
||||||
console.log(`[Layout Store] Toggling header visibility to: ${newValue}`);
|
console.log(`[Layout Store] Toggling header visibility to: ${newValue}`);
|
||||||
@@ -471,19 +471,19 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增 Action: 获取系统内置的默认布局
|
// 获取系统内置的默认布局
|
||||||
function getSystemDefaultLayout(): LayoutNode {
|
function getSystemDefaultLayout(): LayoutNode {
|
||||||
console.log('[Layout Store] Getting system default layout.');
|
console.log('[Layout Store] Getting system default layout.');
|
||||||
return getDefaultLayout(); // 直接调用内部函数
|
return getDefaultLayout(); // 直接调用内部函数
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增 Action: 获取系统内置的默认侧栏配置
|
// 获取系统内置的默认侧栏配置
|
||||||
function getSystemDefaultSidebarPanes(): { left: PaneName[], right: PaneName[] } {
|
function getSystemDefaultSidebarPanes(): { left: PaneName[], right: PaneName[] } {
|
||||||
console.log('[Layout Store] Getting system default sidebar panes.');
|
console.log('[Layout Store] Getting system default sidebar panes.');
|
||||||
return getDefaultSidebarPanes();
|
return getDefaultSidebarPanes();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增 Action: 将当前主布局树持久化到后端和 localStorage
|
// 将当前主布局树持久化到后端和 localStorage
|
||||||
async function persistLayoutTree() { // Make async
|
async function persistLayoutTree() { // Make async
|
||||||
// ... (existing try/catch logic for backend and localStorage) ...
|
// ... (existing try/catch logic for backend and localStorage) ...
|
||||||
// Ensure apiClient calls are awaited if they return promises
|
// 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
|
async function persistSidebarPanes() { // Make async
|
||||||
// ... (existing try/catch logic for backend and localStorage) ...
|
// ... (existing try/catch logic for backend and localStorage) ...
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ const unsubscribeFromWorkspaceEvents = useWorkspaceEventOff();
|
|||||||
|
|
||||||
// --- 本地方法 (仅处理 UI 状态) ---
|
// --- 本地方法 (仅处理 UI 状态) ---
|
||||||
const handleRequestAddConnection = () => {
|
const handleRequestAddConnection = () => {
|
||||||
console.log('[WorkspaceView] handleRequestAddConnection 被调用!'); // 添加日志确认事件到达
|
console.log('[WorkspaceView] handleRequestAddConnection 被调用!');
|
||||||
connectionToEdit.value = null;
|
connectionToEdit.value = null;
|
||||||
showAddEditForm.value = true;
|
showAddEditForm.value = true;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user