外观
开发指南
status: 与 main 分支当前实现一致(2026-10-05)
audience: 准备在本机跑起项目、提交第一个改动的开发者
scope: 环境准备、启动/构建/测试、常见开发任务步骤、编码规范、调试方法
related: 系统架构与通信协议见 architecture.md
1. 环境要求
| 依赖 | 版本要求 | 说明 |
|---|---|---|
| Node.js | ≥ 20.19(建议 22 LTS) | Vite 8 的最低要求;仓库未附 .nvmrc |
| Rust | ≥ 1.90(stable) | Cargo.toml 声明 rust-version = "1.90"、edition = "2024";用 rustup 安装 |
| Xcode 命令行工具 | 最新 | macOS 编译原生依赖与 Tauri 必需:xcode-select --install |
| Docker(可选) | 任意近期版本 | 仅跑 SSH 端到端测试时需要 |
注意:SSH 实现使用纯 Rust 的 russh,不需要安装旧文档提到的 libssh2/openssl。
macOS 安装示例:
bash
# Rust(如尚未安装)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Node 建议用 nvm/fnm 管理;Tauri CLI 已在前端 devDependencies 中,无需全局安装Windows / Linux 的额外依赖参考 Tauri 2 官方前置依赖文档(WebView2、webkit2gtk 等),日常开发以 macOS 为主。
2. 首次启动
方式 A:只调前端(最快,无桌面壳)
bash
cd frontend
npm install
npm run dev
# 打开 http://localhost:5173浏览器模式下 isTauri === false:主机列表使用 src/data/mockHosts.ts,点击连接会提示"浏览器模式无 Tauri 后端",适合调试 UI、弹窗、设置、布局等纯前端逻辑。
方式 B:完整桌面应用(Tauri,前后端一起跑)
bash
# 任选其一:
# 1) 从 Rust 侧启动(tauri.conf.json 的 beforeDevCommand 会自动起前端 vite)
cd src-tauri
cargo tauri dev
# 2) 或从前端侧启动(@tauri-apps/cli 会自动定位 ../src-tauri)
cd frontend
npm install
npm run tauri dev- Vite 固定占用 5173(
strictPort: true),端口被占用会直接报错; - 桌面窗口加载的也是 5173,前端代码改动经 HMR 生效;右键菜单/弹层若未热更新,在窗口内按 ⌘R 强制刷新;
- Rust 代码改动会触发后端重新编译并重启窗口。
3. 构建与静态检查
bash
# 前端:先 vue-tsc 全量类型检查,再 vite 打包(类型不过即失败)
cd frontend
npm run build
# Rust 侧常规检查(在 src-tauri 目录)
cargo check
cargo clippy # 仓库未强制,但建议提交前本地跑
cargo test # 含内联单测;e2e 在无 Docker 容器时自动跳过
# 完整桌面安装包
cargo tauri build注意:
- TS 配置开启了
noUnusedLocals/noUnusedParameters/noFallthroughCasesInSwitch,未使用变量会导致构建失败; - 仓库尚未配置 ESLint / Prettier / rustfmt.toml,格式请遵循第 5 节的既有风格。
4. 测试
4.1 Rust 内联单测
协议编解码等纯逻辑(如 ssh/frame.rs)使用文件内 #[cfg(test)] mod tests,直接:
bash
cd src-tauri && cargo test4.2 SSH 端到端测试(需要本地容器)
测试文件:src-tauri/tests/ssh_e2e.rs,连接 127.0.0.1:2222(账号 test / rhost123)。无容器时测试自动跳过(连接失败视为环境缺失,不误报失败)。
启动测试用 sshd(e2e 默认目标):
bash
docker run -d --name rhost-test-sshd -p 2222:2222 \
-e PASSWORD_ACCESS=true -e USER_NAME=test -e USER_PASSWORD=rhost123 \
lscr.io/linuxserver/openssh-server:latest仓库还自带 MOTD 全功能测试容器(Debian,含 lastlog/procps,端口 2223,供手工验证 MOTD/指标):
bash
docker build -t rhost-sshd-debian docker/debian-sshd
docker run -d --name rhost-test-sshd-debian -p 2223:22 rhost-sshd-debian
# 应用中新建主机 127.0.0.1:2223,账号 test / rhost1234.3 前端
目前没有单元测试框架,前端改动以 npm run build 类型检查 + 浏览器/桌面手动验证为准。
5. 编码规范
5.1 通用风格(前端)
- TypeScript 严格模式;单引号、无分号、2 空格缩进(与全仓库现有代码保持一致);
- 组件统一
<script setup lang="ts">,组合式 API; - 命名:组件/类型/接口
PascalCase;变量/函数camelCase;常量UPPER_SNAKE_CASE; - 跨组件共享类型放
src/types.ts,帧/指标等与后端对应的类型就近定义在 store 中,并注释"与后端 xxx 对应"; - 不用
any掩盖结构问题;invoke<T>()显式标注返回类型。
5.2 状态管理(无 Pinia)
- 全局状态 = store 文件里的模块级单例:
export const xxx = ref(...),直接 import 使用; - 高频整帧替换数据用
shallowRef(new Map())+ 手动triggerRef,避免深响应式开销; - 组件不直接散落
invokeSSH 业务:优先在 store 中封装动作函数(参考stores/session.ts的openSession/closeSession/sendInput); - 所有后端调用必须判
isTauri,浏览器模式给出 mock 或降级提示,不允许直接抛异常。
5.3 组件与 UI 约定
- 全局弹窗在
App.vue常驻挂载,通过 store 的showXxxref 控制显隐; - 弹层/右键菜单用
<Teleport to="body">,显隐用defineModel<boolean>('visible')契约,外部点击/Esc/滚动关闭,document 级监听必须成对移除(参考components/wb/ContextMenu.vue); - 拦截 xterm 等组件原生事件(copy/paste)时,在 capture 阶段注册抢在内部 handler 前,并保证不干扰 Ctrl+C 等无选区信号;
- 反馈统一走
toast(msg, type, duration)(composables/useToast.ts),错误一般先console.error留痕再 toast; - 样式:全局令牌(
--bg/--panel/--border/--green…/--rhost-status-*)定义在src/style.css,组件内用<style scoped>;全站等宽字体、深色基调;不要引入新的 UI/CSS 框架。
5.4 Rust 风格(本仓库注释密度是硬要求)
- 模块顶部写
//!:分层职责、存储/并发策略,复杂流程配数据流图(参考ssh/session.rs、ssh/sftp.rs); - 每个
pub项与结构体字段写///,重点解释为什么、边界条件、单位;魔数常量化并注释取值理由(如MERGE_BYTES = 4096、64KB 分块、30s RTT); - 业务错误在模块内定义
thiserror枚举(如SshError),ipc.rs只负责.map_err(|e| e.to_string()); - 跨层错误字符串前缀即协议(如
KEY_ENCRYPTED:),改动前先搜前端消费方;既有前缀清单见architecture.md§7.3(含主机密钥的HOSTKEY_UNKNOWN:/HOSTKEY_MISMATCH:,payload 三段{algo}|{fingerprint}|{pubkey},{pubkey}为 OpenSSH 格式完整公钥); - 异步铁律:
- russh 是纯异步,禁止用
spawn_blocking包 SSH 读写; - 会话池访问统一"clone Arc → 立即释放锁 → 再 await",不持锁 await;
- 后台任务树挂
CancellationToken,会话断开必须能全部退出;暂停用watch<bool>,背压用有界 mpsc;
- russh 是纯异步,禁止用
- 文件 IO 一律 tokio fs,固定缓冲流式读写,禁止整文件入内存;落盘用"临时文件 + rename"原子写;
- 远端采集/解析一律可降级:缺字段给
N/A/0,任何异常不得影响连接主流程; - serde 跨层结构加
#[serde(rename_all = "camelCase")]。
5.5 Git 提交
使用 Conventional Commits + 中文描述:
feat: 新增 SFTP 断点续传
fix: 修复彩色提示符在无 locale 容器下的乱码
style(favicon): 更新图标设计
refactor(sftp): 传输队列改为串行调度6. 常见开发任务(操作手册)
6.1 新增一个 invoke 命令
src-tauri/src/ipc.rs(或对应模块文件)定义入参结构(Deserialize+ camelCase)与#[tauri::command] pub async fn xxx(...) -> Result<T, String>,业务逻辑调用ssh/等模块;- 在
src-tauri/src/lib.rs的invoke_handler!列表登记命令名; - 前端在对应 store 中
await invoke<ResultT>('xxx', { camelCaseArgs }),isTauri分支外提供降级; - 如涉及权限,在
src-tauri/capabilities/default.json增补。
6.2 新增一个设置项
frontend/src/stores/settings.ts:在AppSettings接口加字段并写 JSDoc 说明、在DEFAULT_SETTINGS给默认值;frontend/src/components/SettingsModal.vue:在PANELS对应分组的rows加一行(key/title/desc/kind/options/keywords),控件渲染由数据驱动,无需改模板;搜索、脏检测、单项重置自动生效;- 消费方读
savedSettings.xxx(注意部分终端构造参数如 scrollback 只对新建终端生效)。
6.3 新增一种终端帧类型(前后端双改,谨慎)
- 后端
ssh/frame.rs的FrameType加变体与注释(说明 payload 与时机),编码处发送; - 前端
stores/session.ts同步FRAME_*常量,并在handleFrame加分支; - JSON payload 字段风格与既有帧保持一致(指标类 snake_case,其他 camelCase),TS 接口写对齐注释;
- 补内联单测(encode/decode roundtrip)。
6.4 新增全局弹窗
参考 components/PasswordModal.vue:store 中放 showXxx 单例;App.vue 常驻挂载;遮罩点击/Esc 关闭;必要时用 Promise + resolve 做"等待用户输入"(composables/usePasswordPrompt.ts 模式)。
6.5 加日志与排错
- Rust 用
log::debug!/error!,带 session id 与目标地址;dev 构建日志落盘:~/Library/Logs/com.rhost.app/rhost.log - 桌面窗口可打开 WebView 开发者工具查看前端日志与网络(dev 构建);
- 前端兜底分支的
console.warn/error保留,便于排查 Tauri 能力缺失(如剪贴板、opener)。
7. 开发约束速查(来自既有实现,勿轻易违反)
- 一条 SSH 连接同一时刻只持有一个 SFTP 通道:传输队列串行、管理操作用 per-session Mutex;
- 终端数据只走二进制帧 Channel,禁止改成全局 Event/Emitter;
- 64KB 固定分块、传输任务独立取消令牌;断点续传按"相等跳过 / 偏小续传 / 偏大截断";
- 传输成功才刷新对侧目录,失败/取消不刷新;
- 密码只进系统钥匙串,连接配置 JSON 不含任何敏感凭据;
- MOTD/彩色提示符注入由后端完成并 hold 初始化输出,前端不参与时序编排;
- 终端 UTF-8 全链路透传,不解析/篡改 PTY 业务字节流(OSC 6667 等自有标记除外)。
8. 已知的文档/工程偏差
- 根目录
README.md的部分技术描述已过时(写的是 Pinia、ssh2、sled、spawn_blocking、旧目录结构),与当前实现不符时以本文档与代码为准; - 仓库暂无 lint/format 配置与 CI,提交前至少保证
npm run build与cargo test通过; - 代码中注释带"逻辑待实现"的功能(如 SFTP 覆盖策略的部分分支、SHA256 断点校验)以
settings.ts字段注释为准,勿假设已生效。