# Termish Web

**给 agent 一个浏览器窗口。** Termish Web 是一个围绕 [herdr](https://herdr.dev)（coding agent 的运行时）构建的 Web 终端管理器：浏览器里用 xterm.js 渲染终端，通过 SSH 连接本机与远程主机，把 herdr 变成「看得见、驱得动、会通知」的存在——任何有浏览器的地方都能用。

> 🇬🇧 [English](README.md) · [FAQ](FAQ.md) · [GitHub](https://github.com/baby2011/termish-web) · [主页](https://termish.dev/web)

> 📱 手机也有：
> [Termish 手机版](https://termish.dev)——Android · iOS · 桌面端的 Mosh 优先终端，
> 同一批 herdr 窗口装进口袋 · [GitHub](https://github.com/icelum/termish)

![Termish Web —— 给 agent 一个浏览器窗口](https://cdn.jsdelivr.net/npm/@termish/web/docs/screenshots/zh.png)

## 为什么需要 Termish Web？

Coding agent（Claude Code、Codex……）最适合跑在一台**一直开着的真实机器**上——你的台式机、NAS、租的服务器。herdr 负责让它们常驻：它在机器上持有真实终端会话，合上盖子工作也不中断。问题在于：herdr 活在终端里，而终端不总在你身边。

Termish Web 解决的就是这件事：

- **任何浏览器，随时接入你的 agent。** 打开本机或 SSH 远程的 herdr 会话——浏览器就是客户端，查看设备零安装。
- **看得见它们在干什么。** 全局 Agent 视图汇聚本机与远程所有 agent——哪个项目、哪台主机、working 还是 idle，实时更新。
- **在 Web 里驱动它们。** 向 agent 窗格发送指令、读取它最近的输出、一键把终端跳到它的工作区。
- **不错过任何完成时刻。** agent 跑完长任务时浏览器弹系统通知——后台标签页也不漏。
- **分享而非口头描述。** 任何会话可生成只读分享链接，对方无需账号、无需配置。

而当你就想要一个终端时，它也是一个完整的 SSH/SFTP 终端管理器：持久会话、分屏工作区、主机管理、审计日志——不依赖 herdr。

**路上呢？** [Termish 手机版](https://github.com/icelum/termish) 是同一批会话的
口袋端——Mosh 让终端在换网、锁屏中保持存活，同一套 herdr 窗口与 agent 监控。

## herdr 是什么？

[herdr](https://herdr.dev) 是你 coding agent 赖以运行的运行时——一个开源（Apache 2.0）终端环境：在笔记本、台式机或租用的服务器上持有真实终端会话，合盖不停工，之后可从任何有键盘的设备重新接入。它识别并托管 20+ 种 agent CLI（Claude Code、Codex……），并有丰富的插件生态。

Termish Web **只通过 herdr 的公开接口**与之集成：运行在 herdr 内时走 Unix socket JSON-RPC（`HERDR_SOCKET_PATH`）；查询远程主机时走 SSH 执行 `herdr agent list` CLI；检测状态时解析 herdr 上报的 iTerm2 进度协议（OSC 9;4）。herdr 是第三方产品，与 Termish Web 无隶属关系。

## 快速开始

### 通过 npm 运行（无需构建）

```bash
npm install -g @termish/web
# 若使用镜像源（如 npmmirror）安装不到最新版，请指定官方源：
npm install -g @termish/web --registry=https://registry.npmjs.org
termish-web            # 默认 :8090（端口被占用自动自增）
termish-web 9000       # 指定端口
```

> 需要 Node.js >= 22.5（依赖实验性的 `node:sqlite`）。

打开 http://localhost:8090。首次启动会自动创建「本机」主机——点它即可立刻打开本机 herdr/终端会话。

### 源码开发

```bash
pnpm install
pnpm dev               # server :8090 + client :5173
pnpm test              # 后端核心逻辑测试
pnpm typecheck         # 全包类型检查
pnpm lint              # eslint
```

打开 http://localhost:5173，点击「+」添加主机（地址 / 认证 / 模式），点击主机开一个会话标签。

### 连接 herdr

- **本机**：在 herdr 终端里启动 `termish-web`（或使用本机主机）——后端自动继承 `HERDR_SOCKET_PATH`，Agent 控制台即刻可用。
- **远程**：添加主机并选择 **herdr 模式**，远程的 `herdr agent list` CLI 会把远程 agent 送进视图。若远程未安装 herdr，界面提供引导式一键安装（TOFU 保护）。

## 功能

### Agent 控制台 —— 浏览器里的 herdr

- **全局 Agent 视图**：三类来源汇聚一堂——本机 herdr socket、远程主机（SSH 查询）、本工作区终端面板；按项目分组、工作项置顶，working/idle 状态实时可见
- **实时状态推送**：状态变化经 SSE（`/api/agents/stream`）推送；底部状态栏常驻「⚡ N 个 agent 工作中」
- **Web 端驱动 agent**：向 agent 窗格发送文本（`pane.send_text`）、读取最近输出（`pane.read`）、点击 agent 即可在你的 herdr 终端中聚焦其工作区
- **完成通知**：working → idle 转换触发浏览器系统通知（含后台标签页），设置面板可开关
- **远程 agent 也可见**：远程主机的 herdr agent 通过 SSH 执行 `herdr agent list` 查询，同样进入视图

### 终端体验

- **两种连接模式**：`herdr`（SSH 后 exec herdr，直接进入 TUI）与 `ssh`（交互 shell）——本质统一为「SSH 会话 + 启动命令」
- **持久会话**：SSH 连接常驻后端，浏览器刷新/关闭只是 detach，重新打开 attach 回同一会话，shell 与 herdr 状态都不丢
- **断线自动重连**：SSH 意外断开按指数退避重连（1s/2s/4s/8s），sessionId 与 scrollback 不丢
- **分屏工作区**：一个工作区（tab）可容纳多个可拖拽调整的面板（左右/上下分屏），终端与 SFTP 混排；切换工作区不断线
- **终端便利**：搜索（Ctrl/Cmd+Shift+F）、选中即复制、VS Code 风格命令面板（Ctrl/Cmd+Shift+P / F1）、5 套终端配色 + 亮/暗主题
- **OSC 感知**：响应颜色查询（OSC 10/11/4/12），herdr 等 TUI 在浏览器里也能正确绘制

### 移动端

- **响应式布局**：手机（≤768px）上侧边栏收进抽屉、分屏面板上下堆叠、触控目标加大
- **终端辅助按键条**：触屏设备显示两行工具栏（移植自 [Termish 手机版](https://github.com/icelum/termish)——装它可享口袋里的 Mosh 漫游会话）——粘滞 Ctrl/Alt 修饰键、一键 ⌃C/⌃D/⌃L/⌃E、Tab、回车、粘贴，方向键跟随 DECCKM（application cursor 模式），手机上也能完整操作 herdr 等 TUI；⌨ 键可收起软键盘腾出屏幕
- **可安装 PWA**：添加到主屏幕，独立窗口启动（含 web manifest 与 iOS 元信息）
- **通知随身**：agent 完成通知同样送达手机浏览器标签页

### 主机与文件

- **主机管理**：添加 / 编辑 / 删除 / 分组；认证支持密码、私钥、SSH Agent；一键导入 `~/.ssh/config`
- **本机自动创建**：首次启动自动添加「本机」主机，默认**本地 PTY 直连**（node-pty，无需 sshd），可选 `~/.ssh` 私钥回环
- **SFTP 文件管理**：浏览 / 预览 / 编辑 / 上传 / 下载（流式、二进制安全）/ 新建目录 / 重命名 / 删除；支持拖拽多文件与目录递归上传。**本机（直连 PTY）主机的 SFTP 直接操作本机文件系统**——无需 sshd、无需认证
- **host key 授信**：首次连接弹窗显示指纹（与 OpenSSH 一致），信任后记住，密钥变化告警；**绝不自动授信**（TOFU 贯穿终端、SFTP、herdr 安装）
- **IPv6 fallback**：双栈域名走 IPv4 失败时自动改用 IPv6 重试
- **活跃指示**：有活跃会话的主机显示绿色圆点

### 安全与数据

- **Token 鉴权**：后端自动生成访问 token，本机浏览器经 `/api/bootstrap` 获取一次；设置 → 安全可随时轮换（旧 token 立即失效）；也可用 `TERMISH_AUTH_TOKEN` 固定
- **凭据加密**：密码/私钥用 AES-256-GCM 加密存储，主密钥存于数据目录 `master.key`（权限 600）
- **默认仅本机**：只监听 `127.0.0.1`，需局域网访问请显式设置 `HOST=0.0.0.0`（风险见安全章节）
- **审计日志**：连接与文件操作记录（`/api/audit-logs`，定长保留 5000 条）
- **SQLite 存储**：主机、设置、已知主机、打开的工作区持久化到 `~/.termish-web/termish.db`
- **只读分享**：任何活跃会话可生成分享链接，观看方经 SSE 流订阅输出，输入被禁用

## 截图

![SFTP 文件管理](https://cdn.jsdelivr.net/npm/@termish/web/docs/screenshots/zh-sftp.png)

![分屏工作区——终端与 SFTP 并排](https://cdn.jsdelivr.net/npm/@termish/web/docs/screenshots/zh-split.png)

## 架构

```
浏览器 (React 19 + Vite + xterm.js)
   ├── REST  /api/*   → 主机、设置、工作区、审计日志、agents、分享
   ├── WS    /ws      → 会话 attach / detach / 输入输出
   └── SSE   /api/agents/stream → agent 状态实时推送（query token 鉴权）
Node 后端 (Koa + ws + ssh2 + node:sqlite)
   ├── SessionManager（持久会话池，SSH 连接与 WebSocket 解耦）
   │    └── SSH ──→ 远程主机 (herdr / shell)
   └── herdr 集成
        ├── Unix socket JSON-RPC（HERDR_SOCKET_PATH）──→ 本机 herdr agents
        ├── SSH exec "herdr agent list" ──→ 远程 herdr agents
        └── OSC 9;4 进度检测 ──→ 每个面板的 agent working/idle 状态
```

**持久会话**是核心：`SshSession` 持有常驻的 SSH 连接与 scrollback 缓冲；浏览器 attach 时订阅数据流（herdr 回放初始化前缀 + resize 触发重绘，ssh 回放历史输出），detach 时不关闭 SSH 连接。

**Agent 状态流**：后端经 herdr socket（本机）与 SSH（远程）拉取 agent 列表，hub 以 1.5s 轮询比对快照，**只广播变化**（SSE）。与此同时，每个终端面板解码 herdr 的 OSC 9;4 进度序列，检测 working → idle 转换——状态栏、Agent 视图、完成通知都由它驱动。

## 目录结构

```
apps/
  server/   Node.js + TS：Koa REST API + WebSocket + SessionManager + SQLite
  client/   React + Vite + TS：侧边栏 + 多标签页 + 终端 + 主题
    components/ui/  统一基础组件（Modal / Button / IconButton / Segmented）
packages/
  shared/   共享协议类型（@termish/shared）
```

## Docker

```bash
docker build -t termish-web .
docker run -d -p 8090:8090 -v termish-data:/app/data termish-web
```

生产模式下 server 单进程托管前端静态文件，访问 http://localhost:8090 即可。数据（SQLite + 主密钥）持久化在 `/app/data` 卷中。

镜像保持**安全默认**：`TERMISH_ALLOW_REMOTE_BOOTSTRAP` **默认关闭**，`/api/bootstrap` 只放行回环客户端。Docker 默认 bridge 网络下 server 看到的源 IP 是网关地址而非回环——如确实需要首次访问自动取 token，请显式开启并固定 token：

```bash
docker run -d -p 127.0.0.1:8090:8090 -v termish-data:/app/data \
  -e TERMISH_ALLOW_REMOTE_BOOTSTRAP=1 \
  -e TERMISH_AUTH_TOKEN=$(openssl rand -hex 32) termish-web
```

**切勿把 `TERMISH_ALLOW_REMOTE_BOOTSTRAP=1` 与暴露到回环之外的端口（`-p 8090:8090`）同时使用**：任何能触达端口的人无需凭据即可取走 token，进而读取全部主机密码/私钥。确需对外暴露时，请在前面加 TLS 反向代理 + 基础认证。

开发用**测试 SSH 主机**（`docker/ssh-host`）：

```bash
docker build -t termish-ssh-host docker/ssh-host
docker run -d -p 2222:22 --name termish-host termish-ssh-host
```

内含 Ubuntu + sshd + 预装 herdr——以 `test` / `test1234` 连接 `127.0.0.1:2222`，可完整演练终端、SFTP、host key 授信与断线重连。

## 作为服务运行

Termish Web 前台运行，收到 SIGINT/SIGTERM 时优雅退出（先关闭所有持久会话）。这是**服务形态的推荐姿势**：不内置 `--daemon`，让系统服务管理器接管生命周期（开机自启、崩溃重启、日志采集）——这是 systemd/launchd 时代的行业共识。

### macOS — launchd

保存为 `~/Library/LaunchAgents/dev.termish.web.plist`，然后 `launchctl load ~/Library/LaunchAgents/dev.termish.web.plist`：

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>dev.termish.web</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/env</string>
        <string>termish-web</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/you/.termish-web/server.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/you/.termish-web/server.log</string>
</dict>
</plist>
```

日志：`tail -f ~/.termish-web/server.log`。停止：`launchctl unload ~/Library/LaunchAgents/dev.termish.web.plist`。

### Linux — systemd

保存为 `/etc/systemd/system/termish-web.service`，然后 `sudo systemctl enable --now termish-web`：

```ini
[Unit]
Description=Termish Web
After=network.target

[Service]
ExecStart=/usr/bin/env termish-web
Restart=on-failure
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target
```

日志：`journalctl -u termish-web -f`。停止：`sudo systemctl stop termish-web`。

## 环境变量

| 变量 | 默认 | 说明 |
|------|------|------|
| `PORT` | `8090` | 监听端口（被占用时自动自增） |
| `HOST` | `127.0.0.1` | 监听地址（暴露局域网用 `0.0.0.0`） |
| `TERMISH_DB_PATH` | `~/.termish-web/termish.db` | SQLite 数据库路径 |
| `TERMISH_CLIENT_DIST` | 包内 `dist/public` | 前端静态资源目录 |
| `TERMISH_AUTH_TOKEN` | 自动生成 | 注入固定访问 token（Docker / 多实例 / 局域网部署） |
| `TERMISH_ALLOW_REMOTE_BOOTSTRAP` | 未设置（默认关） | 设为 `1` 允许非本机经 `/api/bootstrap` 获取 token（Docker bridge 网络需要）。默认关闭——仅在确有必要时开启，且端口不得暴露到回环之外，开启时务必同时固定 `TERMISH_AUTH_TOKEN` |
| `HERDR_SOCKET_PATH` | 继承自环境 | herdr Unix socket 路径；设置后后端即开放本机 Agent 控制台 |

## 安全

- **API/WS 鉴权**：后端自动生成访问 token，本机浏览器首次访问经 `/api/bootstrap` 获取并缓存；非本机访问被拒绝，除非显式设置 `TERMISH_ALLOW_REMOTE_BOOTSTRAP=1`。部署者可用 `TERMISH_AUTH_TOKEN` 固定 token，也可在设置 → 安全中随时轮换（旧 token 立即失效）。
- **凭据加密**：密码/私钥用 AES-256-GCM 加密存储，主密钥存于数据目录 `master.key`（权限 600）。
- **默认仅本机**：只监听 `127.0.0.1`，需要局域网访问请显式设置 `HOST=0.0.0.0` 并自行承担风险。**风险提示：任何拿到 token 的客户端都能读取主机明文凭据（`/api/hosts/:id` 返回密码/私钥）并操作 SFTP**——请固定强 token 并在泄露时立即轮换。
- **主机列表脱敏**：`/api/hosts` 不返回明文凭据，连接时按需单独获取。
- **host key 绝不自动授信**：终端、SFTP、herdr 安装、远程 agent 查询等所有远程操作都要求主机先完成指纹授信——首次连接展示指纹，经确认后才记住。

### 部署检查清单

- **暴露端口时务必固定 token**：任何能触达端口并取得 token 的客户端，都能读取主机明文凭据（`/api/hosts/:id`）并操作 SFTP/终端。端口暴露到回环之外时，请用 `-e TERMISH_AUTH_TOKEN=$(openssl rand -hex 32)` 运行。
- **bootstrap 保持仅回环**：除非确实需要首次访问自动取 token（Docker bridge 网络），否则保持 `TERMISH_ALLOW_REMOTE_BOOTSTRAP` 不设置。切勿与暴露到局域网/公网的端口同时使用——该组合会让任何人无需凭据直接取走 token。
- **优先回环发布**：`-p 127.0.0.1:8090:8090` + SSH 隧道；远程访问用 TLS 反向代理 + 基础认证。
- **疑似泄露即轮换**：设置 → 安全可轮换自动生成的 token（旧 token 立即失效）；若固定了 `TERMISH_AUTH_TOKEN`，则替换环境变量并重启。

## REST API

| 方法 | 路径 | 说明 |
|------|------|------|
| GET / POST | `/api/hosts` | 主机列表 / 创建 |
| GET / PUT / DELETE | `/api/hosts/:id` | 获取（含凭据）/ 更新 / 删除主机 |
| POST | `/api/hosts/:id/touch` | 更新最近使用时间 |
| POST | `/api/hosts/import-ssh-config` | 批量导入 `~/.ssh/config` 中的主机 |
| GET | `/api/hosts/:hostId/agents` | 远程主机上的 herdr agents（SSH 执行查询） |
| GET / PUT | `/api/settings` | 读取 / 合并更新设置 |
| GET / PUT | `/api/tabs` | 读取 / 全量保存打开的工作区 |
| GET / DELETE | `/api/known-hosts` | 已授信主机指纹管理 |
| GET | `/api/audit-logs` | 审计日志 |
| GET / POST | `/api/sftp/:id/:op` | SFTP 操作（list/read/write/mkdir/rename/remove/download/upload） |
| GET | `/api/agents` | 本机 herdr socket 的 agents（herdr 环境外 available=false） |
| GET | `/api/agents/stream` | agent 状态变化 SSE 流（query token 鉴权） |
| GET | `/api/agents/output` | 本机 herdr 窗格最近输出（`pane.read`） |
| POST | `/api/agents/send` | 向本机 herdr 窗格发送文本（`pane.send_text`） |
| POST | `/api/agents/focus` | 在本机 herdr 终端中聚焦某工作区（`workspace.focus`） |
| POST | `/api/sessions/:sessionId/share` | 为活跃会话生成只读分享链接 |
| GET | `/api/share/:token/stream` | 分享会话的 SSE 流（只读，token 校验） |
| GET | `/api/bootstrap` | 获取访问 token（默认仅本机） |
| GET | `/api/whoami` | 当前系统用户 + agent 可用性 |
| POST | `/api/token/rotate` | 轮换访问 token（旧 token 立即失效） |

## WebSocket 协议

统一 JSON 消息，前后端经 `@termish/shared` 共享类型。

客户端 → 服务端：

| type | 说明 |
|------|------|
| `session-create` | 新建持久会话（host / auth / mode / command / 尺寸；`local: true` 为本地 PTY 直连，忽略 host/auth） |
| `session-attach` | 附加到已有会话（刷新恢复） |
| `session-input` / `session-resize` | 输入 / 调整尺寸 |
| `session-detach` / `session-close` | 分离视图 / 关闭会话 |
| `herdr-install` | 在远程主机（或本机 PTY 会话）执行官方脚本安装 herdr |
| `hostkey-response` | host key 授信响应 |

服务端 → 客户端：

| type | 说明 |
|------|------|
| `session-created` / `session-attached` | 会话就绪（attach 带 replay 回放） |
| `session-data` / `session-closed` | 终端输出 / 会话关闭 |
| `session-reconnecting` | 自动重连进度 |
| `herdr-install-output` / `herdr-install-result` | 安装输出流 / 最终结果 |
| `hostkey-verify` | 请求 host key 授信（含指纹 / 算法 / 是否变化） |
| `error` | 错误信息（带 i18n `code` 供前端翻译） |

## SSE 流

| 路径 | 说明 |
|------|------|
| `/api/agents/stream` | agent 状态变化实时推送（15s 心跳；EventSource 无法带 header，用 query token 鉴权） |
| `/api/share/:token/stream` | 分享会话的只读输出（连接时回放） |

## 数据模型

- **hosts**：主机配置（名称 / 地址 / 认证 / 模式 / herdr 路径 / 分组 / 本机连接类型）
- **settings**：键值对（`theme`、`terminalTheme`、语言、通知）
- **known_hosts**：已信任主机指纹（`host:port` → SHA256 指纹）
- **open_tabs**：打开的工作区（工作区 id → 面板；每个面板对应 session id / host / 顺序 / 布局方向）
- **audit_logs**：连接与文件操作记录（定长 5000 条）

## 相关项目

- [Termish 手机版](https://github.com/icelum/termish) — Android · iOS · 桌面端的
  Mosh 优先 SSH/Mosh 终端：同一批 herdr 窗口装进口袋，换网、漫游不掉线。

## 参与贡献

见 [CONTRIBUTING.md](CONTRIBUTING.md)，并请阅读[行为准则](CODE_OF_CONDUCT.md)与[安全政策](SECURITY.md)。

## 第三方声明

**herdr** 是其开发者所有的第三方产品（https://herdr.dev）。本项目仅通过其公开接口（Unix socket JSON-RPC、`herdr agent list` CLI、OSC 9;4 进度协议）与 herdr 集成，herdr 与本项目无隶属关系。其余商标归各自所有者。

## 许可证

[MIT](LICENSE)
