From 6ee18743ad1ccab13928b92b2eda92458afa39b9 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:39:30 +0800 Subject: [PATCH] update --- .gitignore | 1 + .../src/connections/connections.controller.ts | 7 +- packages/backend/src/migrations.ts | 2 +- .../src/repositories/connection.repository.ts | 13 +- .../src/services/connection.service.ts | 13 +- .../src/services/status-monitor.service.ts | 38 +- packages/data/nexus-terminal.db | Bin 110592 -> 0 bytes packages/frontend/src/App.vue | 1 + .../src/components/AddConnectionForm.vue | 10 +- .../frontend/src/components/StatusMonitor.vue | 68 +++- .../components/WorkspaceConnectionList.vue | 333 ++++++++++++++++++ packages/frontend/src/locales/zh.json | 6 + packages/frontend/src/router/index.ts | 6 +- packages/frontend/src/views/WorkspaceView.vue | 241 +++++++++---- 14 files changed, 622 insertions(+), 117 deletions(-) delete mode 100644 packages/data/nexus-terminal.db create mode 100644 packages/frontend/src/components/WorkspaceConnectionList.vue diff --git a/.gitignore b/.gitignore index 3a1e5e1..5d6c39d 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,4 @@ dist *.db /packages/data *.db +*.db diff --git a/packages/backend/src/connections/connections.controller.ts b/packages/backend/src/connections/connections.controller.ts index 7506b29..2a26d8b 100644 --- a/packages/backend/src/connections/connections.controller.ts +++ b/packages/backend/src/connections/connections.controller.ts @@ -20,9 +20,10 @@ const auditLogService = new AuditLogService(); // 实例化 AuditLogService export const createConnection = async (req: Request, res: Response): Promise => { try { // 基本输入验证(更复杂的验证可以在服务层或使用中间件) - const { name, host, username, auth_method, password, private_key } = req.body; - if (!name || !host || !username || !auth_method) { - res.status(400).json({ message: '缺少必要的连接信息 (name, host, username, auth_method)。' }); + // 移除控制器层对 name 的验证,服务层会处理 + const { host, username, auth_method, password, private_key } = req.body; + if (!host || !username || !auth_method) { // 移除 !name 检查 + res.status(400).json({ message: '缺少必要的连接信息 (host, username, auth_method)。' }); // 更新错误消息 return; } if (auth_method === 'password' && !password) { diff --git a/packages/backend/src/migrations.ts b/packages/backend/src/migrations.ts index 71598ca..215f25a 100644 --- a/packages/backend/src/migrations.ts +++ b/packages/backend/src/migrations.ts @@ -88,7 +88,7 @@ CREATE TABLE IF NOT EXISTS proxies ( const createConnectionsTableSQL = ` CREATE TABLE IF NOT EXISTS connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, + name TEXT NULL, -- 允许 name 为空 host TEXT NOT NULL, port INTEGER NOT NULL, username TEXT NOT NULL, diff --git a/packages/backend/src/repositories/connection.repository.ts b/packages/backend/src/repositories/connection.repository.ts index ef583be..b409d5a 100644 --- a/packages/backend/src/repositories/connection.repository.ts +++ b/packages/backend/src/repositories/connection.repository.ts @@ -7,7 +7,7 @@ const db = getDb(); // 注意:这里不包含加密字段,因为 Repository 不应处理解密 interface ConnectionBase { id: number; - name: string; + name: string | null; // 允许 name 为 null host: string; port: number; username: string; @@ -126,15 +126,17 @@ export const findFullConnectionById = async (id: number): Promise => /** * 创建新连接 */ -export const createConnection = async (data: Omit): Promise => { +// Update function signature to accept name as string | null +export const createConnection = async (data: Omit & { name: string | null }): Promise => { return new Promise((resolve, reject) => { const now = Math.floor(Date.now() / 1000); const stmt = db.prepare( `INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ); stmt.run( - data.name, data.host, data.port, data.username, data.auth_method, + data.name ?? null, // Ensure null is passed if name is null/undefined + data.host, data.port, data.username, data.auth_method, data.encrypted_password ?? null, data.encrypted_private_key ?? null, data.encrypted_passphrase ?? null, data.proxy_id ?? null, now, now, @@ -153,7 +155,8 @@ export const createConnection = async (data: Omit>): Promise => { +// Update function signature to accept name as string | null | undefined +export const updateConnection = async (id: number, data: Partial & { name?: string | null }>): Promise => { const fieldsToUpdate: { [key: string]: any } = { ...data }; const params: any[] = []; diff --git a/packages/backend/src/services/connection.service.ts b/packages/backend/src/services/connection.service.ts index a69a582..f4b35e7 100644 --- a/packages/backend/src/services/connection.service.ts +++ b/packages/backend/src/services/connection.service.ts @@ -6,7 +6,7 @@ import { encrypt, decrypt } from '../utils/crypto'; // For now, let's reuse the interfaces from the repository (adjust as needed) export interface ConnectionBase { id: number; - name: string; + name: string | null; // Allow name to be null host: string; port: number; username: string; @@ -23,7 +23,7 @@ export interface ConnectionWithTags extends ConnectionBase { // Input type for creating a connection (from controller) export interface CreateConnectionInput { - name: string; + name?: string; // Name is now optional host: string; port?: number; // Optional, defaults in service/repo username: string; @@ -70,8 +70,9 @@ export const getConnectionById = async (id: number): Promise => { // 1. Validate input (basic validation, more complex validation can be added) - if (!input.name || !input.host || !input.username || !input.auth_method) { - throw new Error('缺少必要的连接信息 (name, host, username, auth_method)。'); + // Removed name validation: if (!input.name || !input.host || !input.username || !input.auth_method) { + if (!input.host || !input.username || !input.auth_method) { // Validate required fields except name + throw new Error('缺少必要的连接信息 (host, username, auth_method)。'); } if (input.auth_method === 'password' && !input.password) { throw new Error('密码认证方式需要提供 password。'); @@ -97,7 +98,7 @@ export const createConnection = async (input: CreateConnectionInput): Promise 解析后的网络统计信息或 null */ private async parseProcNetDev(sshClient: Client): Promise { + let output: string; + try { + // 将命令执行放入 try...catch + output = await this.executeSshCommand(sshClient, 'cat /proc/net/dev'); + } catch (error) { + // 如果命令失败,记录警告并返回 null + console.warn("[StatusMonitor] Failed to execute 'cat /proc/net/dev':", error); + return null; + } + // 如果命令成功,继续解析 try { - const output = await this.executeSshCommand(sshClient, 'cat /proc/net/dev'); const lines = output.split('\n').slice(2); // Skip header lines const stats: NetworkStats = {}; for (const line of lines) { @@ -238,8 +247,9 @@ export class StatusMonitorService { } } return Object.keys(stats).length > 0 ? stats : null; - } catch (error) { - console.error("[StatusMonitor] Error parsing /proc/net/dev:", error); + } catch (parseError) { + // 如果解析失败,记录错误并返回 null + console.error("[StatusMonitor] Error parsing /proc/net/dev output:", parseError); return null; } } @@ -253,14 +263,18 @@ export class StatusMonitorService { try { // 使用 ip route 命令查找默认路由对应的接口 const output = await this.executeSshCommand(sshClient, "ip route get 1.1.1.1 | grep -oP 'dev\\s+\\K\\S+'"); - return output.trim() || null; + const interfaceName = output.trim(); + if (interfaceName) return interfaceName; + // 如果 ip route 没返回有效接口名,也尝试 fallback + console.warn("[StatusMonitor] 'ip route' did not return a valid interface name. Falling back..."); + } catch (error) { - console.warn("[StatusMonitor] Failed to get default interface using 'ip route':", error); - // Fallback: 尝试查找第一个非 lo 接口 - try { - const netDevOutput = await this.executeSshCommand(sshClient, 'cat /proc/net/dev'); - const lines = netDevOutput.split('\n').slice(2); - for (const line of lines) { + console.warn("[StatusMonitor] Failed to get default interface using 'ip route', falling back:", error); + // Fallback: 尝试查找第一个非 lo 接口 + try { + const netDevOutput = await this.executeSshCommand(sshClient, 'cat /proc/net/dev'); + const lines = netDevOutput.split('\n').slice(2); + for (const line of lines) { const iface = line.trim().split(':')[0]; if (iface && iface !== 'lo') { return iface; @@ -269,8 +283,12 @@ export class StatusMonitorService { } catch (fallbackError) { console.error("[StatusMonitor] Failed to fallback to /proc/net/dev for interface:", fallbackError); } + // Ensure null is returned if both primary and fallback fail within the outer catch return null; } + // This part should ideally not be reached if the first try succeeded or the catch block returned. + // Adding a final return null for safety and to satisfy TS if logic paths are complex. + return null; } /** diff --git a/packages/data/nexus-terminal.db b/packages/data/nexus-terminal.db deleted file mode 100644 index 6073a75b2b6c9c260557bd175e962b875f1f7fcf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 110592 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 LRnem6P5$)12Cvim diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue index bd32442..ebc3930 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -19,6 +19,7 @@ const handleLogout = () => {