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:
1 parent
37e65be823
commit
514d62644e
77 files changed
+2563
-931
No files matched your search
@@ -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(`
|
||||
|
||||
Reference in new issue
Block a user