# 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(...) // 接口: 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) => 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 ``` ### ❌ 禁止行为 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 } ``` 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)