# duckterm **Repository Path**: acomma/duckterm ## Basic Information - **Project Name**: duckterm - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-14 - **Last Updated**: 2026-09-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # DuckTerm 基于 Wails v2 + Svelte 5 + xterm.js 的 SSH 客户端,纯 Go 实现,无 cgo。 功能规划、决策记录与分期路线见 [`docs/plan.md`](docs/plan.md) —— 那是本项目的 唯一事实来源。界面按 [`docs/design/`](docs/design/index.html) 的设计规范实现, 色板、尺寸与圆角与之一一对应。 ## 当前能力 - **连接**:密码、私钥(含 passphrase)、SSH Agent、键盘交互(OTP/2FA)四种认证; `known_hosts` 校验与首次指纹确认(接受并保存 / 仅本次 / 取消,超时即拒绝); keepalive(全局默认 + 每主机覆盖);远端 pty 尺寸协商 - **主机管理**:CRUD、分组、标签、收藏、搜索(名称 / 地址 / 用户 / 分组 / 标签); 新建与编辑共用同一套表单,同一主机不会存成两条 - **多标签**:每个标签独立的会话与终端,各自的回滚缓冲 - **文件传输**:远端与本地双栏(本地在左),上传 / 下载(单文件与目录递归)、 多选、拖放、新建目录、删除、传输队列与进度 - **端口转发**:本地 `-L`、远程 `-R`、动态 `-D`(内置 SOCKS5), 规则持久化、随连接自启、流量与连接计数 - **密钥管理**:生成 ED25519 / RSA / ECDSA,导入 OpenSSH / PEM / **PuTTY PPK (v2 与 v3,含 Argon2)并自动转换为 OpenSSH**,公钥一键复制,关联主机可见 - **设置**:主题(深色 / 浅色 / 纯黑)、终端字体与光标、回滚行数、连接默认值、 可改键的快捷键、安全边界说明 —— 全部落盘 - **界面**:无边框窗口 + 自绘标题栏、活动栏、状态栏,中英双语。 SFTP / 隧道 / 密钥 / 设置是**整屏**的(侧栏一并让位,与设计稿一致), 靠标题栏的 `← 返回` 回到主界面;终端在下面保持挂载,回滚缓冲不丢。 尚未实现:会话日志、自动重连、跳板机(ProxyJump)、代理、分屏、会话广播、 命令片段、rz/sz、GBK 输出转换。设置页里与这些相关的开关按设计稿保留,并在 各自的说明里注明「本版本仅记录」——不做的功能要写在界面上,而不是让用户猜。 ## 构建 **全新检出时步骤有先后,且第一步不直观。** `main.go` 用 `//go:embed all:frontend/dist` 把界面嵌进二进制,而 `frontend/dist` 不入库;`wails generate module` 又要先能编译 Go 才能产出绑定。两者互为前提: ``` go:embed 需要 frontend/dist 存在 wails generate module 需要 Go 能编译 前端 需要生成的绑定 ``` 先给嵌入指令一个占位产物即可打破这个环: ```powershell # 一次性 go install github.com/wailsapp/wails/v2/cmd/wails@latest # 1. 让 go:embed 有东西可嵌(仅在全新检出时会失败) New-Item -ItemType Directory -Force -Path frontend/dist | Out-Null 'placeholder' > frontend/dist/index.html # 2. 生成绑定(App 导出方法有增减时也要重跑) wails generate module # 3. 前端依赖与产物 cd frontend npm install npm run build # 二进制(回到仓库根目录) cd .. wails build -o duckterm.exe ``` 产物在 `build/bin/duckterm.exe`。 日常开发用 `wails dev` —— 它自己生成绑定并热重载前端,不需要上面这些步骤。 `wails build` 同样会跑前端 install/build,只有在 `frontend/wailsjs` 尚不存在时 才必须先手动 `generate module`。 ### 为什么全新检出会失败 `frontend/wailsjs/` 与 `frontend/dist/` 都在 `.gitignore` 里:前者是 CLI 生成物, 后者是构建产物。全新检出直接 `go build` 会报: ``` main.go:16:12: pattern all:frontend/dist: no matching files found ``` 直接跑 `wails generate module` 会报**同样的错** —— 它也要编译这个包。占位文件是 成本最低的突破口。 ## 测试 ```powershell # Go:单元测试 + 进程内 SSH 服务器 + 真实的密钥与转发实现 go test ./... # 前端:纯逻辑、终端包装、真实组件挂载、i18n 与审计自检 cd frontend npm run check ``` `npm run check` 跑三件事,缺一不可:`vitest`(156 个用例)、i18n 审计 (界面里任何漏翻的字面量都会让它非零退出)、以及审计自检 —— 后者故意往组件里塞一个硬编码字符串,确认审计**确实会失败**。 一个永远通过的检查器比没有检查器更糟。 其中两条守卫是"写好了但没接线"这类缺陷的防线,它们读的是文件而不是渲染结果: - `styles.test.js`:组件里出现的每个字面 class 必须能在某个样式表里找到规则。 终端搜索条曾经四个 class 一条样式都没有 —— 元素在、文案在、搜索也能用, 只是长得像浏览器默认输入框。jsdom 不解析层叠,任何渲染型用例都看不见。 - `messages.test.js` 的键存在性检查:源码里 `t('…')` 写到的每个键必须在文案 目录里。`t()` 找不到键时返回键本身,所以漏一个键的表现是按钮上印着 `app.confirm`。 两条都做过反向验证:把样式规则改名、把文案键删掉,它们必须失败。 ### 浏览器巡检(jsdom 之外的那一半) 上面那套跑在 jsdom 里:它不做布局、不解析层叠、也不会画,所以能断言"元素在", 永远断言不了"用户看得见",而且**只会跑用例喂给它的输入**。 `npm run sweep` 补上这一半。它需要一个已构建的 `frontend/dist`: ```powershell cd frontend npm run build npm run sweep # 依次点过四个辅视图、六个设置面板、主题卡、快速连接、 # 主机编辑器的四种认证方式、右键菜单,报告每一步的异常 npm run shot # 只截当前首屏 ``` 除了"有没有抛异常",它还断言几条 jsdom 答不了的**计算样式**:快速连接的输入框 不能有边框和聚焦光晕、辅视图必须和主界面一样大、辅视图打开时侧栏要被盖住而终端 仍然挂载。这些是"长什么样"的问题,只能在真引擎里问。 用的是本机 Chrome / Edge(无头启动,跑完关掉),截图写到 `frontend/shots/`(不入库)。 有步骤抛异常时它以非零退出,所以能当关卡用。 它存在的原因是 `npm run check` 拦不住的一类缺陷:设置页曾用 `structuredClone` 复制 Svelte 的 `$state` 代理,构造组件时抛 `DataCloneError`,界面**什么都不发生** —— 而所有打开该页的用例都把设置留空,正好是唯一能跑通的那条分支。 ### 针对真实主机的集成测试 有一组测试跑在真实 SSH 服务器上 —— 进程内的假服务器证明不了真实主机是否真的会 发出提示符。这组测试默认跳过,需要显式启用: ```powershell # 换成你自己的测试机 $env:DUCKTERM_TEST_HOST = 'user@host:22' $env:DUCKTERM_TEST_USER = '' $env:DUCKTERM_TEST_PASSWORD = '' go test ./... -run Live -v ``` 占位符而不是示例值,是 §1.1 的规矩:**真实凭据、内网地址、个人服务器信息一律不进 git**。 这里原本写着一台真实机器的地址、用户名和口令 —— 已经在历史里了,所以那个口令应当 当作已泄露并换掉;重写历史是另一回事,本文档只保证它不再被后续检出复制。 它们会在远端 `/tmp` 下创建随机命名的目录并在结束时清理,主机密钥只写入临时文件, 不会碰你自己的 `known_hosts`。 ## 数据位置 `~/.duckterm/` 下存放 `sessions.json`、`settings.json`、加密密钥与 `duckterm.log`。 日志每次运行截断,用于诊断 —— 打包后的版本是 GUI 子系统程序,stderr 不会出现在 任何你能看到的地方。