# 云枢 - RelayShell **Repository Path**: lanyun417/relay-shell ## Basic Information - **Project Name**: 云枢 - RelayShell - **Description**: 跨平台 SSH / SFTP / 端口转发客户端,基于 Electron 桌面应用。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-09-16 - **Last Updated**: 2026-09-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: Electron, React, SSH, SFTP, LanYun ## README # 云枢 - RelayShell 跨平台 SSH / SFTP / 端口转发客户端,基于 Electron 桌面应用。 > 云枢(RelayShell)的目标是提供现代化、高性能、安全可靠的远程服务器连接管理体验。 --- ## 目录 - [功能特性](#功能特性) - [技术栈](#技术栈) - [安装与运行](#安装与运行) - [使用说明](#使用说明) - [配置项说明](#配置项说明) - [数据存储](#数据存储) - [安全设计](#安全设计) - [注意事项与已知限制](#注意事项与已知限制) - [开发指南](#开发指南) - [目录结构](#目录结构) --- ## 功能特性 ### SSH 终端 - 多标签页管理,可同时保持多个连接 - 密码 / 私钥 / SSH Agent 三种认证方式 - 断线自动重连(指数退避),重连开关与最大重试次数可配置 - 连接保活(keepalive)防 NAT 超时 - 记住密码:首次连接弹框输入,可勾选保存到系统钥匙串 - **跳板机(ProxyJump)**:会话可指定另一台已保存的 SSH 会话作为跳板,支持链式跳板,连接时自动先连跳板 - 复制 / 粘贴:`Ctrl/Cmd+Shift+C` 复制、`Ctrl/Cmd+Shift+V` 粘贴,支持右键菜单、选中即复制、右键直接粘贴(均可配置) ### 本地终端 - 优先使用 node-pty,缺失时自动回退到 `child_process.spawn` 管道模式 - Windows 下默认 cmd.exe,支持逐字符回显、退格、清屏(`cls` / `clear` / `Ctrl+L`)、`Ctrl+C` 取消输入行 ### SFTP 文件管理 - 远端 / 本地双栏浏览,各自带后退 / 前进导航历史 - 上传 / 下载队列,实时进度与速度显示,**支持随时取消传输** - 新建目录、删除文件与空目录(刻意不做递归删除,防误删) - **重命名**、**修改权限(chmod)** - 文本文件在线编辑(≤ 2 MB、拒绝二进制),无写权限时可经 `sudo` 提权保存 - 远端起始目录取终端当前目录,「跟随」模式实时同步终端 `cd` ### 端口转发 - 本地转发(-L)、远程转发(-R)、动态转发(-D,**真正的 SOCKS5 服务端**) - 规则可随会话自动启动 - 停止 -R 远程转发时显式释放远端监听端口 ### 会话与凭据管理 - 分组树(支持嵌套)、收藏、搜索、标签 - 拖拽会话加入 / 移出分组(含「移动到分组」菜单的无障碍兜底) - **会话导入 / 导出**(JSON 文件,主进程内完成,凭据引用不随文件迁移) - 凭据保险箱:密码 / 私钥加密存储,数据库只存引用标识,支持查看凭据内容 ### 其他 - 浅色 / 深色 / 跟随系统主题,7 套终端配色方案 - 简体中文 / English 双语界面,实时切换 - 托盘图标、关闭最小化到托盘、开机自启 - 空闲自动锁定(分钟可配,0 关闭) - 自定义标题栏窗口控制 - **数据存储目录可自定义**:数据库、凭据、日志等可迁移到任意目录 --- ## 技术栈 | 层 | 技术 | | --- | --- | | 桌面框架 | Electron 44 | | 语言 | TypeScript 5.6 | | UI | React 18 + TailwindCSS 3 + Radix UI + lucide-react | | 状态管理 | Zustand | | 终端 | xterm.js 5 | | SSH | ssh2 | | 数据库 | SQLite(better-sqlite3 优先,自动回退纯 WASM 的 sql.js) | | 凭据存储 | keytar → Electron safeStorage → AES-256-GCM 三级降级 | | 构建 | Electron Forge 7 + Vite 5 | --- ## 安装与运行 ### 环境要求 - Node.js ≥ 20 - Windows / macOS / Linux - (可选)C++ 编译工具链:用于编译 better-sqlite3 / keytar / node-pty 原生模块。**没有也能正常运行**,代码已内置纯 JS 降级方案。 ### 安装依赖 ```bash npm install ``` ### 开发模式启动 ```bash npm start ``` ### 类型检查 / Lint ```bash npm run typecheck # tsc --noEmit npm run lint # ESLint ``` ### 打包 ```bash npm run package # 打包成可运行目录(不生成安装包) npm run make # 生成安装包(Windows: Squirrel/zip,Linux: deb/rpm,macOS: zip) ``` #### 应用图标 图标源文件是 `resources/icons/icon.svg`(viewBox="0 0 512 512")。 设计是**自带底色的现代应用瓦片**:品牌蓝渐变圆角瓦片(`#4F8CFF → #2B3FBF`,与界面 `--accent-primary` 同族)+ 内嵌高光描边 + 白色粗提示符 `>` + 亮青光标胶囊。 改完源文件后运行: ```bash node scripts/generate-icons.mjs ``` 该脚本用**纯 Node 手写的光栅化**(圆角矩形 / 线段的 SDF + 超采样,光标带一点高斯 bloom)生成: - `resources/icons/icon.png` —— 512×512 应用图标 - `resources/icons/icon.ico` —— 16/24/32/48/64/128/256 多尺寸,Windows 可执行文件与安装包 - `resources/icons/tray.png` —— 32×32 托盘图标(macOS / Linux) - `resources/icons/tray.ico` —— 16/20/24/32 托盘图标(Windows,覆盖 100%~200% DPI 缩放) 零依赖设计(不需要 sharp / resvg 这类原生库),任何环境都能一键重生成。改图标时 `icon.svg` 与脚本里的设计常量(`TILE` / `TILE_GRADIENT` / `TILE_RIM` / `CHEVRON` / `CURSOR`) 要同步修改——两边坐标一致,`TILE` 正好铺满 512 视口,平台留白由 `FIT_MAIN`(应用 0.86)与 `FIT_TRAY`(托盘 0.94)控制,因此 SVG 里不额外留边;应用图标与托盘图标共用同一份设计。 瓦片自带不透明底色,深浅任务栏 / 托盘上都清晰(旧版是透明布局 + 深色窗口,在深色任务栏上会糊掉)。 产物输出在 `dist/` 目录: - `dist/云枢 - RelayShell-win32-x64/` —— 可直接运行的应用目录(可执行文件名固定为 `relay.exe`) - `dist/make/squirrel.windows/x64/云枢 - RelayShell-<版本> Setup.exe` —— Windows 安装包 - `dist/make/squirrel.windows/x64/*.nupkg` + `RELEASES` —— Squirrel 自动更新文件 > **关于应用名**:显示名 `云枢 - RelayShell` 定义在 `src/shared/constants/index.ts` 的 > `APP_DISPLAY_NAME`,`package.json` 的 `productName` 与 `forge.config.ts` 的 > `packagerConfig.name` 都取该值。而 `package.json` 的 `name` 固定为 `relay`—— > Squirrel 用它作为应用 id(安装目录 `%LOCALAPPDATA%\relay`、nupkg 名),改掉会导致 > 新版本被当成另一个应用安装、无法覆盖升级。 > **离线打包**:`forge.config.ts` 配置了 `electronZipDir: '.electron-zip'`,打包时优先使用 > 本地已有的 Electron 发行包 zip,避免联网下载(本机访问 GitHub 会失败)。 > > **这一步是自动的**:`npm run make` / `npm run package` 会先执行 > `scripts/prepare-electron-zip.mjs` —— 它把 `node_modules/electron/dist` 打成 > `.electron-zip/electron-v<版本>-<平台>-<架构>.zip`,**已存在时直接跳过**。 > 该目录在 `.gitignore` 里,克隆仓库或清理后无需手工处理,直接 `npm run make` 即可。 > > ⚠️ 若直接调用 `electron-forge make` 而跳过该步骤,且目录不存在,打包会以 > `The specified Electron ZIP directory does not exist: .electron-zip` 失败 > (显式配置了该选项时**不会**回退联网下载)。 > > **运行时依赖**:Forge 的 Vite 插件默认只打包 `/.vite` 产物,而主进程把 `ssh2` 等依赖 > 声明为 external(运行时从 `node_modules` 解析)。因此 `forge.config.ts` 中自定义了 > `packagerConfig.ignore`,按「生产依赖闭包」把 `node_modules` 中运行时需要的模块一并 > 打进 asar,同时排除 devDependencies。新增运行时依赖时无需改动该配置(自动递归解析)。 --- ## 使用说明 ### 快速上手 1. 启动后点击左侧「新建会话」,填写主机、端口、用户名,选择认证方式。 2. 凭据可提前在「凭据」页创建(密码 / 私钥 / 自动生成密钥对),也可连接时临时输入。 3. 双击会话打开终端;工具栏可切换 SFTP 面板与端口转发面板。 4. 「新建本地终端」按钮打开本机 shell。 ### 跳板机(ProxyJump) 在会话编辑表单中选择「跳板机(ProxyJump)」,指定另一台已保存的 SSH 会话。连接时会话流量将经跳板机的 SSH 通道中转;跳板未连接时会自动先建立跳板连接。支持链式跳板(跳板的跳板),自动检测成环配置。 ### 动态转发(SOCKS5) 新建「动态转发 (-D)」规则并启动后,本地监听端口即标准 SOCKS5 代理(无认证,仅支持 CONNECT)。在浏览器或系统代理设置中指向 `127.0.0.1:<监听端口>` 即可。 ### 会话导入 / 导出 侧边栏工具按钮区的下载 / 上传图标: - **导出**:所有会话与分组保存为 JSON 文件(凭据明文不包含在内,也不能包含——凭据存在系统钥匙串)。 - **导入**:从 JSON 文件恢复;同名分组自动复用,会话总是作为新条目导入。 --- ## 配置项说明 设置页中的所有配置项(持久化于 `settings.json`): | 配置 | 说明 | 默认值 | | --- | --- | --- | | 主题 | 浅色 / 深色 / 跟随系统 | 跟随系统 | | 语言 | 简体中文 / English | 简体中文 | | 终端配色 | 7 套内置配色 | relay-dark | | 字号 / 滚动行数 / 字体 / 光标 | 终端显示参数 | 14 / 10000 | | 选中即复制 | 选中文字自动复制 | 关 | | 右键直接粘贴 | 右键无选中时直接粘贴 | 关 | | 终端目录跟踪 | 探测远端 shell 工作目录供 SFTP 跟随 | 开 | | 保活间隔 / 次数 | SSH keepalive | 30s / 3 | | 连接超时 | SSH 连接超时(秒) | 20 | | 自动重连 / 最大重试 | 断线重连策略 | 开 / 5 | | 空闲自动锁定 | 分钟,0 = 关闭 | 0 | | 日志脱敏 | 日志中密码/token 打码 | 开 | | 最小化到托盘 | 关闭窗口时退到托盘 | 关 | | 开机自启 | 登录系统时启动 | 关 | | **数据目录** | 应用数据存储位置,修改后自动迁移并重启 | 系统默认 | --- ## 数据存储 ### 默认位置 | 平台 | 路径 | | --- | --- | | Windows | `%APPDATA%\relay-shell` | | macOS | `~/Library/Application Support/relay-shell` | | Linux | `~/.config/relay-shell` | > **目录名固定为 ASCII 的 `relay-shell`,不跟随产品名**。Electron 默认的 userData 是 > `appData + app.getName()`(取 `package.json` 的 productName),若产品名含中文, > 目录就会变成 `%APPDATA%\云枢 - RelayShell`,对命令行工具 / 日志收集 / 备份脚本都不友好。 > 因此主进程在启动最早期固定了目录名(见 `src/main/index.ts` 的 `pinUserDataDirectory`)。 > > **从旧版本升级**:启动时若 `relay-shell` 不存在,会按优先级把历史目录 > (`Relay` → `云枢 - RelayShell`)**重命名**迁移过来(被占用时退化为复制,旧目录保留作备份)。 > 这些历史目录是 1.0.2 及更早(productName = `Relay`)与 1.0.5~1.0.7(默认路径随产品名变化) > 遗留下来的。 ### 目录内容 | 文件 / 目录 | 内容 | | --- | --- | | `settings.json` | 应用设置(**始终位于默认目录**,不随数据目录迁移) | | `relay.db` | SQLite 数据库:会话、分组、转发规则、凭据元数据 | | `credentials.vault.json` | 凭据保险箱(safeStorage / AES 密文,无明文) | | `.machine-key` | AES 兜底模式的主密钥(权限已收紧为仅当前用户) | | `logs/` | 应用日志(开启「日志脱敏」后敏感信息打码) | | `cache/` | 缓存 | | `keys/` | 生成的密钥对 | ### 自定义数据目录 在 **设置 → 存储 → 数据目录** 中选择新目录,点击「迁移并重启」: 1. 应用会把 `relay.db`、凭据保险箱、机器密钥、`keys/`、`cache/`、`logs/` **复制**到新目录(复制而非移动,迁移失败时旧数据完好); 2. 更新 `settings.json` 中的 `storage.dataDirectory`; 3. 自动重启,此后所有数据读写都在新目录进行。 恢复默认:点「恢复默认」后同样执行迁移并重启。 > 注意:`settings.json` 本身永远在默认 userData 目录——它记录着「数据在哪里」,不能跟着数据目录走。 --- ## 安全设计 - **进程隔离**:渲染进程 `contextIsolation: true`、`nodeIntegration: false`,仅能通过 `window.relay.*` 白名单 API 与主进程通信 - **凭据不落明文**:密码 / 私钥优先写入系统钥匙串(keytar);不可用时用 Electron safeStorage(DPAPI / Keychain / libsecret);最后兜底 AES-256-GCM(主密钥文件权限 0600) - **数据库只存引用**:凭据元数据仅存 service/account 标识 - **外链安全**:所有外链经系统浏览器打开,拦截页面导航与 `window.open`,禁用 `` - **CSP**:渲染层启用内容安全策略(无 `unsafe-eval`) - **sudo 提权保存**:密码仅经 stdin 传给 sudo,不落盘、不写日志 - **删除保护**:SFTP 不支持递归删除目录,删除前有确认弹窗 - **Electron Fuses**:打包时关闭 RunAsNode / Node 选项注入,启用 asar 完整性校验 --- ## 注意事项与已知限制 1. **原生模块可选**:better-sqlite3 / keytar / node-pty 未编译时自动走 JS 降级,功能可用但性能 / 交互略降: - 无 node-pty 时本地终端为管道模式,**不支持 vim / top 等全屏交互程序** - 无 keytar 时凭据经 safeStorage 或 AES-GCM 保存 2. **Windows 本地终端编码**:cmd 外部程序按系统 ANSI 代码页(中文系统为 GBK)输出,应用已统一按 GBK 解码;**中文输入**到管道模式终端仍会乱码(TextEncoder 限制)。 3. **目录跟踪依赖 Linux `/proc`**:macOS / BSD 远端不支持会回落到 exec `pwd`;`sudo su` 到 root 后读不到其 cwd,SFTP 面板会提示无权限,退回普通用户自动恢复。 4. **Ctrl+C 中断**:管道模式本地终端无法中断正在运行的外部命令(Windows 需 Console API);SSH 终端不受影响(有真 tty)。 5. **SFTP 提权上传**:上传走 SFTP 协议无法 sudo;在线编辑保存支持 sudo 提权。 6. **自动更新**:模块仅预留,未接入更新源。 7. 导入会话文件**不包含凭据**——导入后需要重新关联或输入凭据。 --- ## 开发指南 ### 常用命令 ```bash npm start # 开发启动(electron-forge start,Vite HMR) npm run typecheck # 类型检查,改完代码必跑 npm run lint # ESLint npm run package # 打包成可运行目录 npm run make # 生成安装包 node scripts/i18n-check.mjs # 校验中英文字典对齐 ``` ### 新增一个 IPC 调用 1. `src/shared/constants/index.ts` 加通道常量 2. `src/shared/types/ipc.ts` 加方法签名 3. `src/main/ipc/xxx.ipc.ts` 加 handler 4. `src/preload/index.ts` 暴露到 `window.relay.*` 5. 渲染层调用 `window.relay.*` ### 新增数据表字段 1. `src/main/db/schema.ts` **追加**新 version 的 migration(不要改历史 migration) 2. 对应 `*.repo.ts` 的 `COLUMN_MAP` / 行映射补字段 3. `src/shared/types/` 领域类型补字段 ### 构建产物约定(重要) - 主进程产物必须是 `.vite/build/main.js`(`vite.main.config.ts` 中 `lib.fileName` 控制) - 预加载产物固定为 `.vite/build/index.js`(按入口文件名命名) - 渲染层产物在 `.vite/renderer/main_window/` - `MAIN_WINDOW_VITE_DEV_SERVER_URL` / `MAIN_WINDOW_VITE_NAME` 是 Forge 编译期注入的**裸标识符**,不是 `process.env` --- ## 目录结构 ``` relay/ ├─ forge.config.ts # Electron Forge 配置 ├─ vite.*.config.ts # 主进程 / 预加载 / 渲染层构建配置 ├─ src/ │ ├─ shared/ # 三端共享:IPC 常量、类型、主题、默认值 │ ├─ main/ # 主进程 │ │ ├─ index.ts # 入口:bootstrap、生命周期 │ │ ├─ window.ts # 主窗口(dev/prod 加载分支) │ │ ├─ config/ # 设置持久化 │ │ ├─ db/ # SQLite 驱动抽象 + 迁移 + 仓储 │ │ ├─ security/ # 加密 / 凭据保险箱 / 密钥生成 │ │ ├─ ssh/ # 连接管理 / SFTP / 端口转发(含 SOCKS5) │ │ ├─ pty/ # 本地终端(node-pty 回退管道模式) │ │ ├─ ipc/ # 各域 IPC handler │ │ └─ utils/ # 日志 / 路径 / 错误 │ ├─ preload/ # contextBridge 桥接 │ └─ renderer/ # React 渲染层 │ ├─ pages/ # workspace / welcome / settings │ ├─ components/ # 终端 / SFTP / 会话 / 转发 / 凭据 / UI 组件 │ ├─ stores/ # Zustand stores │ ├─ i18n/ # 中英文字典 │ └─ styles/ # 全局样式(CSS 变量主题) └─ scripts/ # i18n 校验等工具脚本 ``` --- ## 许可证 MIT © LanYun