核心修复:
- 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
783 lines
26 KiB
Markdown
783 lines
26 KiB
Markdown
# 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 通道
|
||
|
||
```typescript
|
||
// 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)
|
||
```
|
||
|
||
### 数据流
|
||
|
||
1. **SSH 连接流程**:
|
||
```
|
||
QuickConnectDialog/SessionDialog
|
||
↓
|
||
window.electronAPI.sshConnect()
|
||
↓
|
||
ipcMain.handle('ssh:connect')
|
||
↓
|
||
ssh-service.connect()
|
||
↓
|
||
ssh2.Client.connect()
|
||
```
|
||
|
||
2. **Shell 数据流**:
|
||
```
|
||
ssh2 ClientChannel (data事件)
|
||
↓
|
||
event.sender.send('ssh:shellData')
|
||
↓
|
||
ipcRenderer.on('ssh:shellData')
|
||
↓
|
||
window.electronAPI.onShellData()
|
||
↓
|
||
XTerm.write(data)
|
||
```
|
||
|
||
3. **会话持久化**:
|
||
```
|
||
connectionStore.addSession()
|
||
↓
|
||
window.electronAPI.sessionCreate()
|
||
↓
|
||
database-service.createSession()
|
||
↓
|
||
SQLite (密码加密存储)
|
||
```
|
||
|
||
---
|
||
|
||
## 构建和运行命令
|
||
|
||
### 环境要求
|
||
|
||
- Node.js 18+
|
||
- npm 9+ 或 yarn 1.22+
|
||
|
||
### 开发命令
|
||
|
||
```bash
|
||
# 安装依赖
|
||
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 # 生成覆盖率报告
|
||
```
|
||
|
||
### 生产构建
|
||
|
||
```bash
|
||
# 构建生产版本(前端 + Electron 主进程)
|
||
npm run build
|
||
|
||
# 构建步骤分解:
|
||
# 1. tsc - 检查渲染进程 TypeScript
|
||
# 2. vite build - 构建前端到 dist/
|
||
# 3. npm run build:electron - 编译主进程到 dist-electron/
|
||
```
|
||
|
||
### 打包分发
|
||
|
||
```bash
|
||
# 打包当前平台
|
||
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`
|
||
|
||
### 命名约定
|
||
|
||
```typescript
|
||
// 组件: 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 接口
|
||
|
||
```typescript
|
||
// 示例:完整的类型定义
|
||
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 处理器必须返回统一的响应格式:
|
||
|
||
```typescript
|
||
// 主进程
|
||
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
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 安全规范
|
||
|
||
### ✅ 必须遵守
|
||
|
||
1. **始终启用 contextIsolation**
|
||
```typescript
|
||
webPreferences: {
|
||
preload: path.join(__dirname, 'preload.js'),
|
||
contextIsolation: true, // ✅ 必须
|
||
nodeIntegration: false, // ✅ 必须
|
||
}
|
||
```
|
||
|
||
2. **通过 preload 暴露 API**
|
||
```typescript
|
||
// preload.ts - 唯一使用 node 能力的地方
|
||
contextBridge.exposeInMainWorld('electronAPI', api)
|
||
```
|
||
|
||
3. **敏感数据加密**
|
||
- 所有密码使用 `simpleEncrypt/simpleDecrypt` 加密存储
|
||
- 主密钥存储在 `app.getPath('userData')/encryption.key`
|
||
- 密钥文件权限设置为 `0o600`
|
||
|
||
4. **资源清理**
|
||
```typescript
|
||
app.on('before-quit', () => {
|
||
sshService.disconnectAll()
|
||
dbService?.close()
|
||
})
|
||
```
|
||
|
||
5. **CSP 配置**
|
||
```html
|
||
<meta http-equiv="Content-Security-Policy"
|
||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';">
|
||
```
|
||
|
||
### ❌ 禁止行为
|
||
|
||
1. ❌ 在渲染进程直接使用 `require('ssh2')` 或其他 Node 模块
|
||
2. ❌ 暴露 `nodeIntegration: true` 或 `contextIsolation: false`
|
||
3. ❌ 明文存储密码或密钥
|
||
4. ❌ 忽略 SSH 连接关闭(会导致资源泄漏)
|
||
5. ❌ 事件监听不清理(组件卸载时调用 `removeAllListeners`)
|
||
|
||
---
|
||
|
||
## 日志与错误处理
|
||
|
||
### 统一日志系统
|
||
|
||
使用 `src/utils/logger.ts` 替代 `console.log`:
|
||
|
||
```typescript
|
||
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`:
|
||
|
||
```typescript
|
||
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`:
|
||
|
||
### 表结构
|
||
|
||
```sql
|
||
-- 会话表
|
||
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
|
||
```
|
||
|
||
### 编写测试
|
||
|
||
```typescript
|
||
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 接口
|
||
|
||
1. **在 `src/electron.d.ts` 添加类型声明**:
|
||
```typescript
|
||
interface ElectronAPI {
|
||
newMethod: (param: string) => Promise<IPCResponse>
|
||
}
|
||
```
|
||
|
||
2. **在 `electron/preload.ts` 暴露 API**:
|
||
```typescript
|
||
const api = {
|
||
newMethod: (param: string) => ipcRenderer.invoke('xxx:newMethod', param),
|
||
}
|
||
```
|
||
|
||
3. **在 `electron/ipc/` 添加处理器**:
|
||
```typescript
|
||
// 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 }
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
4. **在 `electron/ipc/index.ts` 注册处理器**:
|
||
```typescript
|
||
import { registerFeatureIPCHandlers } from './feature.ipc'
|
||
|
||
export function registerAllIPCHandlers(options: IPCOptions): void {
|
||
registerFeatureIPCHandlers({ ... })
|
||
}
|
||
```
|
||
|
||
### 添加新的 UI 组件
|
||
|
||
1. 在 `src/components/` 创建组件文件(PascalCase)
|
||
2. 如果需要状态,在 `src/stores/` 创建对应的 Store
|
||
3. 在 `TabContent.tsx` 添加渲染逻辑(如果是新标签类型)
|
||
|
||
### 添加终端主题
|
||
|
||
1. 在 `src/themes/terminal.ts` 添加主题配置:
|
||
```typescript
|
||
'my-theme': {
|
||
name: 'My Theme',
|
||
background: '#xxxxxx',
|
||
foreground: '#xxxxxx',
|
||
// ... 所有必需的颜色
|
||
}
|
||
```
|
||
|
||
2. 在 `src/stores/settingsStore.ts` 的 `terminalTheme` 类型中添加新主题
|
||
|
||
---
|
||
|
||
## 连接池架构(新)
|
||
|
||
项目正在迁移到新的连接池架构,位于 `src/stores/connection/`。
|
||
|
||
### 连接状态
|
||
|
||
```typescript
|
||
type ConnectionState =
|
||
| 'disconnected' // 初始/已关闭
|
||
| 'connecting' // 正在连接
|
||
| 'connected' // 活跃连接
|
||
| 'background' // 后台保持
|
||
| 'suspended' // 挂起(长时间未操作)
|
||
| 'error' // 连接错误
|
||
| 'reconnecting' // 正在重连
|
||
```
|
||
|
||
### 使用新的连接管理
|
||
|
||
```typescript
|
||
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` 提供向后兼容的导出,旧代码仍可继续使用:
|
||
|
||
```typescript
|
||
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` |
|
||
|
||
---
|
||
|
||
## 参考资源
|
||
|
||
- [Electron 文档](https://www.electronjs.org/docs)
|
||
- [React 文档](https://react.dev)
|
||
- [XTerm.js 文档](https://xtermjs.org)
|
||
- [ssh2 文档](https://github.com/mscdex/ssh2)
|
||
- [Zustand 文档](https://zustand.docs.pmnd.rs)
|
||
- [Tailwind CSS 文档](https://tailwindcss.com/docs)
|
||
- [better-sqlite3 文档](https://github.com/WiseLibs/better-sqlite3)
|
||
- [zmodem-ts 文档](https://github.com/FGasper/zmodem-ts)
|