# Termdock

一个面向移动端与桌面端的 Web 终端，由 tmux 持久托管会话，xterm.js + WebGL 负责渲染，Express + WebSocket 提供后端通信。

![License](https://img.shields.io/badge/license-MIT-green)

## 功能特性

### 终端能力

- **xterm.js + WebGL 渲染**：使用 `@xterm/addon-webgl` 加速绘制，自动处理上下文丢失与纹理刷新
- **tmux 持久会话**：所有会话由 tmux 托管，关闭页面/掉线后仍可恢复，支持 `detach`、`destroy`、强杀
- **WebSocket 双向通信**：单条持久连接同时承载输入与输出（取代旧的 SSE + POST 方案）
- **自动重连**：网络或后端中断后会自动尝试 attach 回原会话
- **鼠标支持**：完整透传 SGR 鼠标协议，vim、htop、tmux copy-mode 内的滚动/点击都按预期工作
- **OSC 0 CWD 嗅探**：标签页可动态显示当前进程或目录，无需轮询 `/proc`

### 多会话与标签栏

- **多会话管理**：创建、切换、重启、重命名（双击标签）、强杀
- **预渲染所有会话**：避免页面切换时 WebGL 上下文丢失
- **磁盘持久化**：会话布局与 tmux 元数据落盘，重启后自动恢复

### 移动端体验

- **Swiper 翻页**：左右滑动在多个终端之间切换，与终端内滚动手势已做冲突隔离
- **触摸优先的设置抽屉**：分 Tab、可滑动、长按 destroy
- **可定制虚拟键盘**：内置 Esc / Tab / Ctrl / Alt / Cmd / 方向键 / Enter / Backspace，并支持自定义工具条预设
- **手势**：点击 = 鼠标左键，长按 = 右键，捏合缩放调字号，触摸滑动 = 终端滚动
- **iOS 适配**：处理选择菜单、键盘弹起、翻页与会话恢复时的纹理刷新等细节

### 安全与认证

- **密码保护**（可选）：通过 `termdock --set-password` 启用，登录页 + Cookie 会话
- **登录限流**：基于来源 IP 的指数退避，防暴力破解
- **CSRF 防护**：所有写入接口要求 CSRF token
- **WebSocket 升级鉴权**：未登录的 upgrade 请求会被 401 拒绝
- **路径校验**：内置 `pathValidator` 防止路径穿越

### PWA

- 自托管 JetBrains Mono NL + Symbols Nerd Font（含 Bold）
- 完整的 PWA 图标 / 启动屏 / manifest，可安装到主屏幕全屏运行
- Service Worker 缓存静态资源，支持离线打开

## 技术栈

- **前端**：React 18 + TypeScript + Vite 7
- **终端渲染**：`@xterm/xterm` + `@xterm/addon-webgl` + `@xterm/addon-fit`
- **状态管理**：Zustand
- **触摸滑动**：Swiper
- **拖拽排序**：dnd-kit
- **后端**：Express 5 + ws + node-pty + tmux
- **样式**:Tailwind CSS
- **图标**：Remix Icon + Nerd Fonts

## 快速开始

### macOS 桌面版

桌面版内置 Node.js、Termdock 服务端、tmux、Git、rg 和 mkcert，运行时不依赖
Homebrew、Node.js 或预装 CLI。首次打开会检测并引导安装 `td` / `termdock`；
桌面版和 CLI 完全共用 `~/.termdock`，也可以连接本机已有服务或任意 Termdock
地址。若要接管正在运行的本机 CLI 服务，必须先由用户确认。

正式版可从 [GitHub Releases](https://github.com/Jovines/termdock/releases)
下载 DMG；安装后的桌面版支持在应用菜单中检查并安装更新。

构建、版本管理和签名说明见
[Termdock for macOS](docs/macos-desktop.md)。

### 一行命令启动

```bash
npx termdock
```

默认监听 `0.0.0.0:9834`，并在后台运行。常用变体：

```bash
npx termdock --host 127.0.0.1 --port 4000
termdock --foreground            # 前台运行
termdock --status                # 查看后台状态
termdock --stop                  # 停止后台服务
td update                        # 从 npm 官方源升级全局 CLI
```

`td update`（也可写作 `td upgrade`）会优先使用 npm 官方源
`https://registry.npmjs.org` 查询并安装最新正式版；仅当官方源查询或安装失败时，
才会回退到本机 `.npmrc` 配置的源（npm 命令不再传入 `--registry`）。
如果后台服务正在运行，升级不会打断现有终端会话；可在方便时执行
`td --stop && td`，让服务切换到新版本。

npm CLI 服务也会自动更新：启动 15 秒后检查一次，之后每 6 小时检查。
新版会在后台安装，但无论是否有网页连接都绝不会自动重启。更新状态会持久保存，
用户打开网页后可在左侧边栏“更多”按钮看到更新圆点和提醒；只有用户明确确认后
才会重启，未确认的提醒会一直保留。tmux 和由 PTY host 托管的会话可跨此次重启继续使用。
macOS 桌面版仍使用独立的签名更新机制，不运行 npm 自动更新器。

### 设置访问密码（强烈推荐）

如果服务暴露在 LAN 上，**务必先设置密码**，否则任何能访问到主机/端口的人都能执行 shell 命令：

```bash
# 交互式设置（输入隐藏）
termdock --set-password

# 通过管道设置（CI / 脚本场景）
echo "my-secret" | termdock --set-password

# 关闭鉴权
termdock --clear-password
```

密码状态存放在 `~/.termdock/auth.json`（mode 0600，scrypt 哈希，不可逆）。修改密码会使所有已登录会话失效。

服务在未设置密码时启动会打印醒目的安全警告。

### 局域网 HTTPS 访问（手机 / 本机）

Termdock 可以为当前机器发布一个产品化的局域网地址：

```text
https://<name>.termdock.local:9834
```

`<name>` 会在首次启动时自动生成一个 4 位默认值，也可以在设置抽屉里的「本地访问」中自定义。自定义名称不做人为长度限制，但必须是合法 hostname label。

第一版仍然保留 `:9834` 端口；无端口的 `https://<name>.termdock.local` 需要后续单独引入 443 代理/Helper。

第一次直接运行 `termdock` 时，CLI 会先引导你选择 `.termdock.local` 前缀，并询问是否立刻启用 HTTPS；选择启用后会自动安装/配置 mkcert、生成证书，然后继续启动服务。

使用建议：

```bash
# 1. 先启用密码，避免把 shell 暴露给同一内网其他人
termdock --set-password

# 2. 自动准备本地 HTTPS 证书
#    若未安装 mkcert，会自动通过 Homebrew 执行 brew install mkcert
termdock --setup-local-https

# 3. 正常启动；如果 ~/.termdock/certs/ 下已有证书，会自动启用 HTTPS
termdock

# 4. 查看正式地址和手机首次接入地址
termdock --status
```

该命令会生成并保存：

```text
~/.termdock/certs/termdock-local.pem
~/.termdock/certs/termdock-local-key.pem
~/.termdock/certs/rootCA.pem
```

也可以手动覆盖证书路径：

```bash
termdock --https-cert <cert.pem> --https-key <key.pem> --https-ca <rootCA.pem>
```

手机首次接入时，先在同一 Wi‑Fi 下打开 `termdock --status` 或服务启动日志输出的 onboarding 地址，例如：

```text
http://192.168.1.23:52741/onboarding
```

该页面会提供 CA 证书下载和 iPhone / Android 安装步骤；如果只想直接下载证书，也可以打开更短的：

```text
http://192.168.1.23:52741/ca
```

安装并信任 CA 后，再打开正式地址：

```text
https://<name>.termdock.local:9834
```

注意：mDNS 依赖同一局域网的 `.local` 组播；访客 Wi‑Fi、客户端隔离、VPN 或部分企业网络可能会阻止解析。此时 `localhost` 访问仍然可用，但手机上的漂亮域名可能不可用。

### 分享和安装 Agent 插件

Agent 适配可以作为独立 Git 仓库分享。插件仓库根目录必须包含 `manifest.json`，可以同时携带 `icon.svg` 和标题生成等辅助脚本：

```text
my-agent-termdock-plugin/
├── manifest.json
├── icon.svg          # 可选
└── scripts/          # 可选；manifest 中用 {pluginDir} 引用
```

安装和维护命令：

```bash
td plugin-install https://github.com/owner/my-agent-termdock-plugin
td plugin-list --json
td plugin-check my-agent
td plugin-update my-agent
td plugin-doctor my-agent --json
td plugin-hooks my-agent install
td plugin-hooks my-agent uninstall
td plugin-remove my-agent
```

`plugin-install/update/remove` 管理完整插件包；`plugin-hooks` 只管理写入 Agent 原生配置文件的 Termdock hook 条目，两者生命周期互相独立。设置界面提供相同操作。

安全上，安装插件只会注册声明式能力，不会自动安装 hooks，也不会因打开设置页就执行插件的模型探测命令。只有用户启用该插件的自动标题后，才会调用它声明的 CLI。插件 hook 目标必须是用户目录内、不经过符号链接的 JSON 文件；实际 hook 命令由 Termdock 生成，插件不能注入任意 shell。仍应只安装你信任且审查过的仓库。

自动标题插件必须区分“始终传入的参数”和“选中模型后才传入的整组参数”。例如 TraeX 使用 `-c model="..."` 时：

```json
{
  "titleNamer": {
    "command": "traecli",
    "modelArgs": ["-c", "model=\"{model}\""],
    "args": ["-p", "{prompt}"],
    "models": {
      "command": "node",
      "args": ["{pluginDir}/scripts/list-models.mjs"]
    }
  }
}
```

`modelArgs` 是一个原子参数组：用户或 Termdock 选中模型时整组前置，没有模型时整组省略，此时明确使用 Agent CLI 默认模型。模型命令可输出 JSON 数组，也可输出 `{ "models": [...], "recommendedModel": "..." }`；模型 ID 字段支持 `id` / `name` / `model`，并可选提供 `displayName`、`description`、`isDefault` 和 `isEconomical`。原生 CLI 字段仍无法匹配时，用 `{pluginDir}` 内的脚本实时转换，禁止硬编码模型列表。

自动选择顺序是：插件顶层 `recommendedModel` → 第一个 `isEconomical: true` → 第一个 `isDefault: true` → 不传模型并使用 Agent CLI 默认值。Termdock 不再根据模型名称或说明猜测价格；用户手动选择始终优先于自动选择。

`td plugin-doctor <slug> --json` 会显式运行一次模型探测，报告 `titleNamer`、可用模型数、自动选择结果、被忽略字段和修复建议；它不会调用付费的标题生成命令。

开发本地插件时可直接传目录；旧的 manifest-only 命令仍可使用：

```bash
td plugin-install ./my-agent-termdock-plugin
td plugin-create ./manifest.json
```

插件作者和 Agent 可以运行 `td agent-plugin --json` 获取机器可读的当前协议、manifest schema、状态模型和全部公共命令。v1 manifest 会返回可直接交给 AI 修复的迁移说明。

### 从源码安装

```bash
git clone https://github.com/Jovines/termdock.git
cd termdock
./install-local.sh
```

脚本会执行 `npm install` → `npm rebuild node-pty --build-from-source` → `npm run build` → `npm install -g .`。在 macOS 上会额外检查 `node-pty` 的 `spawn-helper` 是否生成成功，若失败会提示安装 Xcode Command Line Tools。

若你开启了访问密码，并希望在自动化脚本里无交互访问（不关闭鉴权），可先尝试复用 cookie，再按需登录刷新：

```bash
# 首先直接尝试复用已有 cookie（推荐）
bash auth-login.sh

# 仅当 cookie 失效时，再提供原密码刷新登录态
export TERMDOCK_PASSWORD="<your-termdock-password>"
bash auth-login.sh

# 自动化请求统一带 cookie
curl -b ~/.termdock/automation.cookies http://localhost:9834/api/auth/status
```

这不会创建第二套密码，也不会关闭鉴权。

卸载：

```bash
./uninstall-local.sh
```

### 开发模式

```bash
# 同时启动前后端
npm run dev

# 或分开启动
npm run dev:client   # Vite 前端：9833
npm run dev:server   # tsx watch 后端：9835
```

开发期请访问 `http://localhost:9833`，Vite 会把 API/WebSocket 代理到后端开发端口 9835。正式/本地安装服务独立使用 9834。

### 构建

```bash
npm run build
```

输出：

- `dist/client/`：前端静态资源
- `dist/server/`：Node.js 服务端 + CLI 入口

直接运行构建产物：

```bash
node dist/server/cli.js
# 或
npm start
```

## 系统依赖

- **Node.js ≥ 18**
- **tmux**：会话托管必需，请确保 `tmux` 在 PATH 中
- **macOS**：需安装 Xcode Command Line Tools 以便 `node-pty` 编译 `spawn-helper`
- **Linux**：通常需要 `build-essential`、`python3` 才能编译 `node-pty`

### tmux 焦点跟踪

Termdock 复用系统默认 tmux server。每次创建、复用、切换或通过 CLI attach tmux 会话时，Termdock 会自动确保 shared tmux server 的 `focus-events` 为 `on`，并在浏览器焦点变化时把 focus in/out 事件按需转发给 tmux 内部请求了 focus tracking 的程序（例如 Claude Code、Vim、fzf 等）。这是单向增强项：Termdock 不会在会话关闭后把 `focus-events` 自动恢复为 `off`。如果你手动维护 `~/.tmux.conf`，也可以显式加入：

```tmux
set -g focus-events on
```

## 项目结构

```
termdock/
├── src/
│   ├── main.tsx                          # 应用入口
│   ├── App.tsx                           # 根组件
│   ├── index.css                         # 全局样式
│   ├── lib/
│   │   ├── terminal/                     # xterm 适配层、主题、API
│   │   ├── stores/                       # Zustand store
│   │   │   ├── useTerminalStore.ts
│   │   │   └── useMultiSessionStore.ts
│   │   ├── components/
│   │   │   ├── MultiTerminalView.tsx     # 多会话 Swiper 主视图
│   │   │   ├── auth/LoginScreen.tsx      # 登录页
│   │   │   ├── terminal/                 # 终端视图、错误/加载、移动键盘
│   │   │   ├── settings/                 # 调试面板、工具条预设
│   │   │   ├── ui/ErrorBoundary.tsx
│   │   │   └── views/TerminalView.tsx
│   │   ├── hooks/                        # 字号、滚动、断连清理、视口高度等
│   │   └── utils/                        # 错误处理、调试
│   └── server/
│       ├── cli.ts                        # CLI 入口（前后台、密码管理）
│       ├── entry.ts                      # Express + WebSocket 启动
│       ├── config.ts                     # 端口配置（dev: 9833/9835，prod: 9834）
│       ├── routes/
│       │   ├── auth.ts                   # 登录 / 登出 / 状态
│       │   └── terminal.ts               # 终端 + tmux 路由
│       └── utils/
│           ├── authProtection.ts         # scrypt 密码哈希、会话、限流
│           ├── csrfProtection.ts
│           └── pathValidator.ts
├── public/                               # PWA 图标、字体、manifest
├── install-local.sh / uninstall-local.sh
├── package.json
├── vite.config.ts
└── tsconfig.json
```

## 主要 API 端点

> 写入接口需要登录后获取 CSRF token，并通过 Cookie + token 一起调用。

### 鉴权

| 方法 | 端点 | 描述 |
|------|------|------|
| GET  | `/api/auth/status` | 查询是否启用鉴权 / 当前 cookie 是否有效 |
| POST | `/api/auth/login`  | 登录（限流） |
| POST | `/api/auth/logout` | 登出 |
| GET  | `/api/csrf-token`  | 获取 CSRF token（需登录） |

### 终端 / tmux

| 方法 | 端点 | 描述 |
|------|------|------|
| POST | `/api/terminal/create` | 创建新会话 |
| GET  | `/api/terminal/:sessionId/ws` *(WebSocket)* | 双向通信通道 |
| POST | `/api/terminal/:sessionId/input` | 发送输入（HTTP 兜底） |
| POST | `/api/terminal/:sessionId/resize` | 调整终端尺寸 |
| POST | `/api/terminal/:sessionId/tmux` | 执行 tmux 控制命令 |
| POST | `/api/terminal/:sessionId/restart` | 重启会话 |
| POST | `/api/terminal/:sessionId/detach` | 断开 attach |
| GET  | `/api/terminal/:sessionId/attach` | 重新 attach |
| GET  | `/api/terminal/:sessionId/health` | 健康检查 |
| DELETE | `/api/terminal/:sessionId` | 关闭会话 |
| POST | `/api/terminal/force-kill` | 强制结束 |
| GET  | `/api/terminal/tmux/sessions` | 列出所有 tmux 会话 |
| DELETE | `/api/terminal/tmux/sessions/:name` | 销毁指定 tmux 会话 |
| GET  | `/api/terminal/processes` | 进程列表 |

## 主题

当前内置 **Flexoki Dark** —— 一套低对比度、暖色调的配色，长时间阅读更舒适。
主题定义在 `src/lib/terminal/theme.ts`，可按需扩展。

## 配置

### CLI 参数

```
--host <host>        绑定地址（默认 0.0.0.0）
--port <port>        监听端口（默认 9834）
--foreground         前台运行
--status             查看后台服务状态
--stop               停止后台服务
--set-password       设置 / 修改访问密码（交互式）
--clear-password     清除密码并关闭鉴权
-h, --help           查看帮助
```

### 环境变量

```bash
PORT=9834                      # 正式/安装后服务端口；dev:server 使用 9835
HOST=0.0.0.0                   # 绑定地址
NODE_ENV=development           # 运行环境
TERM=xterm-256color            # 终端类型
SHELL=/bin/zsh                 # 默认 shell
MAX_TERMINAL_SESSIONS=20       # 最大会话数
TERMINAL_IDLE_TIMEOUT=1800000  # 空闲超时 (毫秒)
```

### 状态目录

```
~/.termdock/
├── auth.json        # 密码哈希（mode 0600，仅在启用鉴权时存在）
├── server.json      # 后台进程 PID / 端口
└── server.log       # 后台运行日志
```

## 移动端控制

### 虚拟按键

Esc / Tab / Ctrl / Alt / Cmd / ↑↓←→ / Enter / Backspace，并支持在设置中自定义工具条预设。

### 触摸交互

- **点击**：模拟鼠标左键
- **长按**：模拟鼠标右键
- **滑动**（终端区）：滚动当前会话内容
- **滑动**（边缘）：在多个会话之间翻页
- **捏合缩放**：调整字号

## 发布到 npm

```bash
npm publish
```

`prepublishOnly` 钩子会自动执行 `npm run build`。发布前请确认：

- npm 包名可用
- `repository` / `homepage` / `bugs` 字段已更新
- README、版本号已同步

## 浏览器支持

- Chrome 88+
- Firefox 79+
- Safari 14+（含 iOS 14+）
- Edge 88+

## 许可证

MIT
