# 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 不会出现在
任何你能看到的地方。