核心修复:
- 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
26 KiB
26 KiB
AGENTS.md - OpsXShell 项目开发指南
本文件为 AI 编程助手提供项目开发指导。
项目概述
OpsXShell 是跨平台 SSH/SFTP 终端工具 · 简洁高效 · 安全可靠 · 基于 Electron + React + TypeScript 构建。
核心功能
- 🔐 SSH 终端 - 支持密码、密钥、SSH Agent 多种认证方式
- 📁 SFTP 文件管理 - 双栏可视化文件管理,拖拽上传下载
- 🔄 端口转发 - 本地/远程端口转发支持
- 📑 多标签页 - 类浏览器标签管理,支持会话恢复
- 🎨 现代 UI - 基于 Tailwind CSS 的深色主题界面
- 🔒 安全可靠 - 敏感数据使用 AES-256-GCM 加密存储
- 🌐 跨平台 - 支持 Windows、macOS、Linux
- ⚡ Zmodem 支持 - 集成 rz/sz 文件传输协议
当前版本状态
- 版本: v2.0.0
- 状态: 已发布 (Release) - 安全增强版
- 已完成: 核心 SSH/SFTP 功能、加密存储、多标签页系统、Zmodem 传输
- v2.0 优化: safeStorage 主密钥加密、路径遍历防护、PBKDF2 600K 迭代、类型安全、工程化建设、性能优化
- 技术债务: 部分组件仍在迁移到新的连接池架构
技术栈
| 层级 | 技术 | 版本 |
|---|---|---|
| 框架 | Electron | 28+ |
| 前端框架 | React | 18 |
| 语言 | TypeScript | 5 |
| 构建工具 | Vite | 5 |
| 状态管理 | Zustand | 4.4+ |
| 终端仿真 | XTerm.js | 5.5+ |
| SSH/SFTP | ssh2 | 1.15+ |
| Zmodem | zmodem-ts | 1.0.8 |
| 数据库 | better-sqlite3 | 9.4+ |
| 样式 | Tailwind CSS | 3.3+ |
| 图标 | Lucide React | 0.294+ |
| 测试 | Vitest | 4.1+ |
项目结构
opsxshell/
├── electron/ # Electron 主进程
│ ├── main.ts # 主进程入口 - 窗口创建、应用生命周期、菜单
│ ├── preload.ts # 上下文桥接,暴露 API 到渲染进程
│ ├── tsconfig.json # 主进程 TS 配置 (CommonJS)
│ ├── ipc/ # IPC 处理器(按功能模块组织)
│ │ ├── index.ts # IPC 注册入口
│ │ ├── ssh.ipc.ts # SSH 连接、Shell、端口转发
│ │ ├── sftp.ipc.ts # SFTP 文件操作
│ │ ├── session.ipc.ts # 会话 CRUD
│ │ ├── settings.ipc.ts # 应用设置
│ │ ├── dialog.ipc.ts # 文件对话框
│ │ ├── file.ipc.ts # 文件系统操作
│ │ ├── zmodem.ipc.ts # Zmodem 传输
│ │ └── app.ipc.ts # 应用信息
│ └── services/ # 主进程服务
│ ├── ssh-service.ts # SSH 连接、Shell 会话、SFTP 操作
│ ├── database-service.ts # SQLite 持久化存储
│ ├── crypto.ts # AES-256-GCM 加密工具
│ ├── key-service.ts # 主密钥管理
│ └── zmodem-service.ts # Zmodem 文件传输服务
├── src/ # 渲染进程 (React)
│ ├── components/ # UI 组件
│ │ ├── index.ts # 🆕 统一导出
│ │ ├── App.tsx # 根组件
│ │ ├── SessionDialog.tsx # 会话编辑对话框
│ │ ├── QuickConnectDialog.tsx # 快速连接对话框
│ │ ├── WelcomeView.tsx # 欢迎页面
│ │ │
│ │ ├── layout/ # 🆕 布局组件
│ │ │ ├── Sidebar.tsx # 侧边栏
│ │ │ ├── TabBar.tsx # 标签栏
│ │ │ ├── TabContent.tsx # 标签内容
│ │ │ └── StatusBar.tsx # 状态栏
│ │ │
│ │ ├── sftp/ # SFTP 模块
│ │ │ ├── index.ts
│ │ │ ├── SFTPView.tsx # SFTP 主视图
│ │ │ ├── Breadcrumb.tsx
│ │ │ ├── ConnectionSelector.tsx
│ │ │ ├── ContextMenu.tsx
│ │ │ ├── FileRow.tsx
│ │ │ ├── SortHeader.tsx
│ │ │ ├── TransferItem.tsx
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ │
│ │ ├── terminal/ # 终端模块
│ │ │ ├── index.ts
│ │ │ ├── TerminalView.tsx # 终端视图
│ │ │ └── types.ts
│ │ │
│ │ ├── settings/ # 🆕 设置模块
│ │ │ └── SettingsView.tsx # 设置页面
│ │ │
│ │ └── common/ # 🆕 通用组件
│ │ ├── index.ts
│ │ ├── Button.tsx # 按钮
│ │ ├── Modal.tsx # 模态框
│ │ ├── Loading.tsx # 加载状态
│ │ └── ErrorBoundary.tsx # 错误边界
│ ├── stores/ # Zustand 状态管理
│ │ ├── index.ts # 🆕 统一导出
│ │ ├── tabStore.ts # 标签页状态管理
│ │ ├── settingsStore.ts # 应用设置
│ │ ├── terminalSnapshotStore.ts # 终端快照
│ │ └── connection/ # 连接管理架构
│ │ ├── index.ts # 统一导出
│ │ ├── types.ts # 类型定义
│ │ ├── poolStore.ts # 连接池核心
│ │ ├── lifecycleStore.ts # 连接生命周期
│ │ ├── storeHooks.ts # Store Hooks
│ │ └── sessionHelper.ts # 会话配置获取
│ ├── themes/ # 主题配置
│ │ └── terminal.ts # 终端主题
│ │
│ ├── hooks/ # 🆕 自定义 Hooks
│ │ ├── index.ts # 统一导出
│ │ ├── useTerminal.ts # 终端管理
│ │ ├── useSFTP.ts # SFTP 操作
│ │ ├── useConnection.ts # 连接管理
│ │ └── useSettings.ts # 设置操作
│ │
│ ├── constants/ # 🆕 常量管理
│ │ ├── index.ts # 统一导出
│ │ ├── terminal.ts # 终端常量
│ │ ├── sftp.ts # SFTP 常量
│ │ └── keybindings.ts # 快捷键
│ │
│ ├── utils/ # 工具函数
│ │ ├── index.ts # 统一导出
│ │ ├── logger.ts # 统一日志系统
│ │ ├── error-handler.ts # 统一错误处理
│ │ ├── validators.ts # 输入校验
│ │ └── formatters.ts # 格式化工具
│ │
│ ├── electron.d.ts # Electron API 类型声明
│ ├── main.tsx # React 渲染入口
│ └── index.css # 全局样式(Tailwind + 自定义样式)
├── shared/ # 共享类型定义(主进程/渲染进程共用)
│ └── types/
│ ├── session.types.ts # 会话相关类型
│ ├── connection.types.ts # 连接相关类型
│ ├── sftp.types.ts # SFTP 类型
│ ├── ipc.types.ts # IPC 类型
│ └── index.ts # 统一导出
├── test/ # 测试文件
│ ├── setup.ts # 测试环境配置(mock Electron API)
│ ├── stores/ # Store 测试
│ │ ├── tabStore.test.ts
│ │ └── connectionPool.test.ts
│ └── components/sftp/ # 组件测试
│ └── utils.test.ts
├── dist/ # 前端构建输出 (Vite)
├── dist-electron/ # 主进程构建输出
├── package.json # 项目配置 + electron-builder 配置
├── vite.config.ts # Vite 配置
├── vitest.config.ts # Vitest 测试配置
├── tailwind.config.js # Tailwind CSS 配置
├── tsconfig.json # 渲染进程 TS 配置
├── tsconfig.node.json # Node 类型配置
├── index.html # 入口 HTML(含 CSP 配置)
└── .eslintrc.cjs # ESLint 配置
架构详解
Electron 双进程模型
┌─────────────────────────────────────────────────────────────┐
│ 主进程 (Node.js) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ ssh-service │ │ database │ │ key-service │ │
│ │ (ssh2) │ │ (SQLite) │ │ (加密密钥管理) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │zmodem-service│ │ IPC │ │
│ │ (zmodem-ts) │ │ Handlers │ │
│ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ipcMain.handle() / event.sender.send() │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ │ contextBridge │ │
│ │ (electron/preload.ts) │ │
│ └──────────────┬──────────────┘ │
└──────────────────────────┼──────────────────────────────────┘
│ IPC (安全隔离)
┌──────────────────────────┼──────────────────────────────────┐
│ 渲染进程 (Chromium) │
│ ┌──────────────┴──────────────┐ │
│ │ window.electronAPI │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ┌─────────────┐ ┌──────┴──────┐ ┌─────────────────────┐ │
│ │ React UI │ │ Zustand │ │ XTerm.js 终端 │ │
│ │ 组件 │ │ Stores │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
IPC 通信接口
渲染进程通过 window.electronAPI 与主进程通信。完整类型定义在 src/electron.d.ts。
核心 IPC 通道
// SSH 操作
const { success, connectionId, error } = await window.electronAPI.sshConnect(config)
const { success, shellId } = await window.electronAPI.sshCreateShell(connectionId, { cols, rows })
await window.electronAPI.sshShellWrite(shellId, data)
await window.electronAPI.sshShellResize(shellId, cols, rows)
await window.electronAPI.sshDisconnect(connectionId)
// Shell 数据推送(主进程 → 渲染进程)
window.electronAPI.onShellData((event, connectionId, shellId, data: Buffer) => {
terminal.write(data)
})
// SFTP 操作
const { sftpId } = await window.electronAPI.sftpConnect(connectionId)
const { files } = await window.electronAPI.sftpListDirectory(sftpId, path)
await window.electronAPI.sftpUpload(sftpId, localPath, remotePath)
// 会话管理
const { sessions } = await window.electronAPI.sessionGetAll()
const { session } = await window.electronAPI.sessionCreate(sessionData)
await window.electronAPI.sessionUpdate(id, updates)
await window.electronAPI.sessionDelete(id)
数据流
-
SSH 连接流程:
QuickConnectDialog/SessionDialog ↓ window.electronAPI.sshConnect() ↓ ipcMain.handle('ssh:connect') ↓ ssh-service.connect() ↓ ssh2.Client.connect() -
Shell 数据流:
ssh2 ClientChannel (data事件) ↓ event.sender.send('ssh:shellData') ↓ ipcRenderer.on('ssh:shellData') ↓ window.electronAPI.onShellData() ↓ XTerm.write(data) -
会话持久化:
connectionStore.addSession() ↓ window.electronAPI.sessionCreate() ↓ database-service.createSession() ↓ SQLite (密码加密存储)
构建和运行命令
环境要求
- Node.js 18+
- npm 9+ 或 yarn 1.22+
开发命令
# 安装依赖
npm install
# 启动开发服务器(Vite + Electron)
npm run dev
# 说明:会先启动 Vite dev server (端口 5173),然后启动 Electron
# 运行 ESLint 检查
npm run lint
# 运行测试
npm run test # 运行所有测试
npm run test:watch # 监视模式
npm run test:coverage # 生成覆盖率报告
生产构建
# 构建生产版本(前端 + Electron 主进程)
npm run build
# 构建步骤分解:
# 1. tsc - 检查渲染进程 TypeScript
# 2. vite build - 构建前端到 dist/
# 3. npm run build:electron - 编译主进程到 dist-electron/
打包分发
# 打包当前平台
npm run dist
# 平台特定打包
npm run dist:win # Windows (NSIS)
npm run dist:mac # macOS (DMG, x64 + arm64)
npm run dist:linux # Linux (AppImage)
# 输出目录: release/
代码规范
文件组织
- 组件:
src/components/*.tsx- PascalCase 命名 - 子组件目录:
src/components/{feature}/*.tsx- 功能相关子组件 - Stores:
src/stores/*Store.ts或src/stores/{feature}/store.ts- camelCase - 服务:
electron/services/*-service.ts- kebab-case - IPC 处理器:
electron/ipc/{feature}.ipc.ts- kebab-case - 类型: 共享类型定义在
shared/types/*.types.ts
命名约定
// 组件: PascalCase
export default function TerminalView() {}
// Hooks: camelCase + use 前缀
const useConnectionStore = create<ConnectionState>(...)
// 接口: PascalCase
interface SSHConfig { ... }
interface TabState { ... }
// 常量: UPPER_SNAKE_CASE (局部) 或 camelCase (模块级)
const ALGORITHM = 'aes-256-gcm'
const keyService = new KeyService()
类型安全
- 所有 IPC 接口必须在
src/electron.d.ts中声明 - 共享类型定义在
shared/types/目录 - 使用严格的 TypeScript 配置 (
strict: true) - Zustand Store 必须定义 State 接口
// 示例:完整的类型定义
interface Tab {
id: string
title: string
type: 'welcome' | 'terminal' | 'sftp' | 'settings'
sessionId?: string
connectionConfig?: SSHConnectionConfig
isActive: boolean
}
interface TabState {
tabs: Tab[]
activeTabId: string | null
addTab: (tab: Omit<Tab, 'id' | 'isActive'>) => void
// ...
}
IPC 错误处理
所有 IPC 处理器必须返回统一的响应格式:
// 主进程
ipcMain.handle('xxx:action', async (event, params) => {
try {
const result = await doSomething(params)
return { success: true, data: result }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// 渲染进程
const result = await window.electronAPI.xxxAction(params)
if (result.success) {
// 处理成功
} else {
// 处理错误: result.error
}
安全规范
✅ 必须遵守
-
始终启用 contextIsolation
webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, // ✅ 必须 nodeIntegration: false, // ✅ 必须 } -
通过 preload 暴露 API
// preload.ts - 唯一使用 node 能力的地方 contextBridge.exposeInMainWorld('electronAPI', api) -
敏感数据加密
- 所有密码使用
simpleEncrypt/simpleDecrypt加密存储 - 主密钥存储在
app.getPath('userData')/encryption.key - 密钥文件权限设置为
0o600
- 所有密码使用
-
资源清理
app.on('before-quit', () => { sshService.disconnectAll() dbService?.close() }) -
CSP 配置
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';">
❌ 禁止行为
- ❌ 在渲染进程直接使用
require('ssh2')或其他 Node 模块 - ❌ 暴露
nodeIntegration: true或contextIsolation: false - ❌ 明文存储密码或密钥
- ❌ 忽略 SSH 连接关闭(会导致资源泄漏)
- ❌ 事件监听不清理(组件卸载时调用
removeAllListeners)
日志与错误处理
统一日志系统
使用 src/utils/logger.ts 替代 console.log:
import { createLogger } from '../utils/logger'
const logger = createLogger('ModuleName')
// 使用
logger.debug('调试信息', { detail: 'xxx' })
logger.info('操作成功', { id: 'xxx' })
logger.warn('警告信息')
logger.error('操作失败', error)
日志级别:
DEBUG- 开发调试信息(生产环境不输出)INFO- 关键操作记录WARN- 警告信息ERROR- 错误信息
统一错误处理
使用 src/utils/error-handler.ts:
import { handleError, createError, ErrorCode } from '../utils/error-handler'
// 1. 转换未知错误
try {
await riskyOperation()
} catch (error) {
const appError = handleError(error, 'context')
// appError.message - 用户友好消息
// appError.recoverable - 是否可恢复
// appError.action - 建议操作
}
// 2. 创建特定错误
const error = createError(ErrorCode.CONNECTION_TIMEOUT, 'detail')
错误码分类:
1xxx- 连接错误11xx- SSH 错误12xx- SFTP 错误13xx- 会话错误14xx- 验证错误9xxx- 系统错误
数据库架构
SQLite 数据库存储在 app.getPath('userData')/opsxshell.db:
表结构
-- 会话表
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
host TEXT NOT NULL,
port INTEGER DEFAULT 22,
username TEXT NOT NULL,
auth_type TEXT CHECK(auth_type IN ('password', 'key', 'agent')),
password TEXT, -- 加密存储
private_key_path TEXT, -- 私钥路径(不加密)
passphrase_encrypted TEXT, -- 私钥密码(加密)
group_name TEXT DEFAULT 'default',
tags TEXT, -- JSON 数组
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 连接历史表
CREATE TABLE connection_history (
id TEXT PRIMARY KEY,
session_id TEXT REFERENCES sessions(id),
connected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
disconnected_at DATETIME,
duration INTEGER,
status TEXT
);
-- 设置表
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT, -- JSON 格式
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 密钥表
CREATE TABLE keys (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT CHECK(type IN ('rsa', 'ed25519', 'ecdsa')),
private_key_encrypted TEXT NOT NULL,
public_key TEXT,
passphrase_encrypted TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 端口转发规则表
CREATE TABLE port_forwards (
id TEXT PRIMARY KEY,
session_id TEXT REFERENCES sessions(id),
type TEXT CHECK(type IN ('local', 'remote', 'dynamic')),
local_host TEXT,
local_port INTEGER,
remote_host TEXT,
remote_port INTEGER,
is_active BOOLEAN DEFAULT FALSE
);
测试策略
测试框架
- 测试运行器: Vitest
- 测试环境: jsdom
- 断言库: @testing-library/jest-dom
- React 测试: @testing-library/react
测试文件位置
test/
├── setup.ts # 测试环境配置
├── stores/ # Store 测试
│ ├── tabStore.test.ts
│ └── connectionPool.test.ts
└── components/ # 组件测试
└── sftp/
└── utils.test.ts
编写测试
import { describe, it, expect, beforeEach } from 'vitest'
import { useTabStore } from '../../src/stores/tabStore'
describe('TabStore', () => {
beforeEach(() => {
// Reset store to initial state
useTabStore.setState({
tabs: [{ id: 'welcome', title: '欢迎', type: 'welcome', isActive: true }],
activeTabId: 'welcome',
})
})
it('应该添加新的终端标签', () => {
const { addTab } = useTabStore.getState()
addTab({ title: 'Terminal', type: 'terminal' })
const { tabs } = useTabStore.getState()
expect(tabs).toHaveLength(2)
})
})
Mock Electron API
测试环境中已配置完整的 Electron API mock,详见 test/setup.ts。
开发指南
添加新的 IPC 接口
-
在
src/electron.d.ts添加类型声明:interface ElectronAPI { newMethod: (param: string) => Promise<IPCResponse> } -
在
electron/preload.ts暴露 API:const api = { newMethod: (param: string) => ipcRenderer.invoke('xxx:newMethod', param), } -
在
electron/ipc/添加处理器:// electron/ipc/feature.ipc.ts export const FEATURE_CHANNELS = { NEW_METHOD: 'xxx:newMethod', } as const export function registerFeatureIPCHandlers(options: Options): void { ipcMain.handle(FEATURE_CHANNELS.NEW_METHOD, async (event, param) => { try { const result = await doSomething(param) return { success: true, data: result } } catch (error: any) { return { success: false, error: error.message } } }) } -
在
electron/ipc/index.ts注册处理器:import { registerFeatureIPCHandlers } from './feature.ipc' export function registerAllIPCHandlers(options: IPCOptions): void { registerFeatureIPCHandlers({ ... }) }
添加新的 UI 组件
- 在
src/components/创建组件文件(PascalCase) - 如果需要状态,在
src/stores/创建对应的 Store - 在
TabContent.tsx添加渲染逻辑(如果是新标签类型)
添加终端主题
-
在
src/themes/terminal.ts添加主题配置:'my-theme': { name: 'My Theme', background: '#xxxxxx', foreground: '#xxxxxx', // ... 所有必需的颜色 } -
在
src/stores/settingsStore.ts的terminalTheme类型中添加新主题
连接池架构(新)
项目正在迁移到新的连接池架构,位于 src/stores/connection/。
连接状态
type ConnectionState =
| 'disconnected' // 初始/已关闭
| 'connecting' // 正在连接
| 'connected' // 活跃连接
| 'background' // 后台保持
| 'suspended' // 挂起(长时间未操作)
| 'error' // 连接错误
| 'reconnecting' // 正在重连
使用新的连接管理
import { useConnectionStore, getSessionConfig } from '../stores/connection'
// 在组件中使用
const connectionStore = useConnectionStore()
// 创建连接
const result = await connectionStore.createConnection(tabId, config)
// 断开连接
await connectionStore.disconnectConnection(connectionId, 'reason')
旧版兼容
src/stores/connectionPool.ts 提供向后兼容的导出,旧代码仍可继续使用:
import { useConnectionPool } from '../stores/connectionPool'
// 实际指向新的实现
常见问题
| 问题 | 解决方案 |
|---|---|
| 开发时 Electron 无法加载页面 | 确保 Vite 开发服务器在端口 5173 运行 |
| SSH 连接失败 | 检查主机、端口、认证方式配置;查看主进程控制台日志 |
| XTerm.js 显示异常 | 确保容器有明确宽高,调用 fitAddon.fit() |
| 数据库初始化失败 | 检查应用数据目录权限 (app.getPath('userData')) |
| 加密解密失败 | 检查主密钥文件是否存在且权限正确 |
| 构建后应用白屏 | 检查 vite.config.ts 中 base: './' 配置 |
| better-sqlite3 构建失败 | 运行 npm run rebuild 或 npx electron-rebuild |