v2.0.0: 主题系统+SSH私钥解析修复+会话持久化+连接池优化+UI打磨

核心修复:
- ssh-service.ts: 新增 resolvePrivateKey(),将私钥文件路径(~/.ssh/id_rsa)解析为内容后传给 ssh2,
  解决 'Cannot parse privateKey: Unsupported key format' 错误
- useTerminalConnection.ts + sessionHelper.ts: 根据 authType 过滤字段,password/agent 认证不再传 privateKey
- sessionStore.ts: addSession() 现在先调用 sessionCreate() 持久化到 SQLite,解决恢复标签'无可用连接信息'问题
- database-service.ts: updateSession() 补充 private_key_path 字段持久化
- SessionDialog.tsx: 默认 username='root'、privateKey='~/.ssh/id_rsa'

UI/UX:
- 语义化主题系统(app-theme.ts),强制深色模式,全局无边框设计
- macOS 标题栏支持双击最大化(window:toggleMaximize IPC)
- macOS 菜单栏('OpsXShell' 应用菜单 + Settings Cmd+,)
- WelcomeView 4卡片布局,移除 Settings 按钮
- TabBar/TabItem 背景色统一(bg-dark-800/900),消除白色边框线

架构优化:
- SFTP 生命周期修复(useSFTP.ts useRef + autoConnectAttempted guard)
- 连接池竞态修复,支持快速多标签连接
- IPC 接口规范化,安全错误处理
- Vite 代码分割优化

质量: lint零错误 | tsc零错误 | build成功 | 49 tests pass
This commit is contained in:
Logan.Li committed 2026-04-22 11:15:41 +08:00
1 parent 37e65be823
commit 514d62644e
77 files changed
+2563 -931

No files matched your search

+53 -47
View File
@@ -5,13 +5,14 @@ import fs from 'fs'
import { simpleEncrypt, simpleDecrypt } from './crypto'
import { keyService } from './key-service'
import { createLogger } from '../utils/logger'
import type { Session, SessionCreateRequest, SessionUpdateRequest } from '../../shared/types/session.types'
const logger = createLogger('DatabaseService')
// sql.js statement wrapper to match better-sqlite3 API
interface StatementResult {
id: number
[key: string]: any
[key: string]: unknown
}
class PreparedStatement {
@@ -23,16 +24,18 @@ class PreparedStatement {
this.sql = sql
}
run(...params: any[]): StatementResult {
this.db.run(this.sql, params as any)
return { id: 0 } as StatementResult
run(...params: unknown[]): StatementResult {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.db.run(this.sql, params as any[])
return { id: 0 }
}
get(...params: any[]): any {
get(...params: unknown[]): unknown {
// 使用 exec 模式获取单行
const stmt = this.db.prepare(this.sql)
stmt.bind(params as any)
let result: any = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
stmt.bind(params as any[])
let result: unknown = null
if (stmt.step()) {
result = stmt.getAsObject()
}
@@ -40,10 +43,11 @@ class PreparedStatement {
return result
}
all(...params: any[]): any[] {
all(...params: unknown[]): unknown[] {
const stmt = this.db.prepare(this.sql)
stmt.bind(params as any)
const results: any[] = []
// eslint-disable-next-line @typescript-eslint/no-explicit-any
stmt.bind(params as any[])
const results: unknown[] = []
while (stmt.step()) {
results.push(stmt.getAsObject())
}
@@ -200,7 +204,6 @@ export class DatabaseService {
}
try {
const decrypted = simpleDecrypt(value, this.masterKey)
logger.info('Decrypted field length:', decrypted?.length || 0)
return decrypted
} catch (error) {
logger.error('Decrypt failed:', error)
@@ -209,10 +212,10 @@ export class DatabaseService {
}
// 会话 CRUD 操作
createSession(session: any): any {
createSession(session: SessionCreateRequest): Session | null {
if (!this.db) throw new Error('Database not initialized')
logger.info('Creating session with password:', session.password ? `len=${session.password.length}` : 'null/undefined')
logger.info('Creating session:', session.name, session.host)
const stmt = this.prepare(`
INSERT INTO sessions (id, name, host, port, username, auth_type, password, private_key_path, passphrase_encrypted, group_name, tags)
@@ -221,7 +224,6 @@ export class DatabaseService {
const id = `session-${Date.now()}`
const encryptedPassword = this.encryptField(session.password)
logger.info('Encrypted password:', encryptedPassword ? `len=${encryptedPassword.length}` : 'null')
stmt.run(
id,
@@ -241,56 +243,56 @@ export class DatabaseService {
return this.getSessionById(id)
}
getAllSessions(): any[] {
getAllSessions(): Session[] {
if (!this.db) throw new Error('Database not initialized')
const stmt = this.prepare('SELECT * FROM sessions ORDER BY created_at DESC')
const sessions = stmt.all()
const sessions = stmt.all() as Record<string, unknown>[]
return sessions.map((s: any) => ({
id: s.id,
name: s.name,
host: s.host,
port: s.port,
username: s.username,
authType: s.auth_type,
password: this.decryptField(s.password),
privateKey: s.private_key_path,
passphrase: this.decryptField(s.passphrase_encrypted),
group: s.group_name,
tags: s.tags ? JSON.parse(s.tags) : [],
return sessions.map((s) => ({
id: s.id as string,
name: s.name as string,
host: s.host as string,
port: s.port as number,
username: s.username as string,
authType: s.auth_type as Session['authType'],
password: this.decryptField(s.password as string | null) || undefined,
privateKey: s.private_key_path as string | undefined,
passphrase: this.decryptField(s.passphrase_encrypted as string | null) || undefined,
group: s.group_name as string | undefined,
tags: s.tags ? JSON.parse(s.tags as string) as string[] : [],
}))
}
getSessionById(id: string): any {
getSessionById(id: string): Session | null {
if (!this.db) throw new Error('Database not initialized')
const stmt = this.prepare('SELECT * FROM sessions WHERE id = ?')
const session = stmt.get(id) as any
const session = stmt.get(id) as Record<string, unknown> | undefined
if (session) {
return {
id: session.id,
name: session.name,
host: session.host,
port: session.port,
username: session.username,
authType: session.auth_type,
password: this.decryptField(session.password),
privateKey: session.private_key_path,
passphrase: this.decryptField(session.passphrase_encrypted),
group: session.group_name,
tags: session.tags ? JSON.parse(session.tags) : [],
id: session.id as string,
name: session.name as string,
host: session.host as string,
port: session.port as number,
username: session.username as string,
authType: session.auth_type as Session['authType'],
password: this.decryptField(session.password as string | null) || undefined,
privateKey: session.private_key_path as string | undefined,
passphrase: this.decryptField(session.passphrase_encrypted as string | null) || undefined,
group: session.group_name as string | undefined,
tags: session.tags ? JSON.parse(session.tags as string) as string[] : [],
}
}
return null
}
updateSession(id: string, updates: any): any {
updateSession(id: string, updates: SessionUpdateRequest): Session | null {
if (!this.db) throw new Error('Database not initialized')
const fields: string[] = []
const values: any[] = []
const values: unknown[] = []
if (updates.name !== undefined) {
fields.push('name = ?')
@@ -320,6 +322,10 @@ export class DatabaseService {
fields.push('passphrase_encrypted = ?')
values.push(this.encryptField(updates.passphrase))
}
if (updates.privateKey !== undefined) {
fields.push('private_key_path = ?')
values.push(updates.privateKey || null)
}
if (updates.group !== undefined) {
fields.push('group_name = ?')
values.push(updates.group)
@@ -350,15 +356,15 @@ export class DatabaseService {
}
// 设置操作
getSetting(key: string): any {
getSetting(key: string): unknown {
if (!this.db) throw new Error('Database not initialized')
const stmt = this.prepare('SELECT value FROM settings WHERE key = ?')
const row: any = stmt.get(key)
return row ? JSON.parse(row.value) : null
const row = stmt.get(key) as Record<string, unknown> | undefined
return row ? JSON.parse(row.value as string) : null
}
setSetting(key: string, value: any): void {
setSetting(key: string, value: unknown): void {
if (!this.db) throw new Error('Database not initialized')
const stmt = this.prepare(`