浏览器多标签终端系统,支持本地 PTY 和 SSH Shell,断线自动恢复。
- 后端:Spring Boot + WebSocket + pty4j + Apache SSHD
- 前端:Vite + React + TypeScript + xterm.js
- 生产部署:Node 反向代理(
11947)→ 后端(11946)
- JDK 21+
- Maven 3.9+
- Node.js 20+
先准备本地后端覆盖配置(本地私有,不提交):
cp application.example.yml backend/application.yml后端和前端分别启动:
# 终端 1:启动后端(127.0.0.1:11946)
cd backend
mvn spring-boot:run
# 终端 2:启动前端(localhost:11947,自动代理到后端)
cd frontend
npm install
npm run dev访问 http://localhost:11947/term/(Web)或 http://localhost:11947/appterm/(App WebView)。
# 构建
./release-scripts/mac/package.sh
# 启动
./release-scripts/mac/start.sh
# 停止
./release-scripts/mac/stop.sh脚本目录约定:release-scripts/mac 放 .sh,release-scripts/windows 放 .ps1 / .bat。
默认端口:后端 127.0.0.1:11946,前端 0.0.0.0:11947。
- 根目录仅保留模板文件:
.env.example、application.example.yml - 根目录
.env不是必需文件 backend/application.yml用于本地开发(已 gitignore)release/.env+release/application.yml用于发布运行
常用复制命令:
# 本地开发后端复杂配置
cp application.example.yml backend/application.yml
# release 运行配置
cp .env.example release/.env
cp application.example.yml release/application.yml配置文件加载顺序(后者覆盖前者):
backend/src/main/resources/application.yml(内置默认)- 运行目录
application.yml(例如backend/application.yml、release/application.yml) - 运行目录
.env(通过spring.config.import导入)
关键配置项:
| 配置项 | 默认值 | 说明 |
|---|---|---|
terminal.default-command |
codex |
默认终端命令 |
terminal.workdir-browse-root |
${user.home} |
目录浏览根路径 |
terminal.allowed-origins |
http://*,https://* |
CORS 允许来源 |
terminal.detached-session-ttl-seconds |
3600 |
断开后会话保留时长(秒) |
terminal.ring-buffer-max-bytes |
4194304 |
输出缓冲大小(4MB) |
auth.enabled |
true |
是否启用密码认证 |
auth.username |
admin |
登录用户名 |
auth.password-hash-bcrypt |
内置 demo hash(默认密码 password) |
bcrypt 密码哈希 |
terminal.ssh.enabled |
true |
是否启用 SSH 功能 |
terminal.ssh.master-key |
${TERMINAL_SSH_MASTER_KEY:} |
SSH 凭据加密主密钥(推荐通过环境变量注入) |
文件 frontend/.env:
| 变量 | 默认值 | 说明 |
|---|---|---|
VITE_API_BASE |
空(同源) | API 基础地址,为空时使用同源路径 |
VITE_COPILOT_REFRESH_MS |
2000 |
Copilot 自动刷新间隔(毫秒) |
VITE_DEV_PROXY_TARGET |
http://127.0.0.1:11946 |
开发模式代理目标 |
| 变量 | 默认值 | 说明 |
|---|---|---|
BACKEND_HOST |
优先从 release/.env 读取,回退脚本默认值 127.0.0.1 |
后端监听地址 |
BACKEND_PORT |
优先从 release/.env 读取,回退脚本默认值 11946 |
后端监听端口 |
FRONTEND_HOST |
0.0.0.0 |
前端监听地址 |
FRONTEND_PORT |
11947 |
前端监听端口 |
BACKEND_ORIGIN |
自动拼接 | 前端代理的后端地址 |
BACKEND_JAVA_OPTS |
-Xms256m -Xmx512m |
JVM 参数 |
BACKEND_ARGS |
空 | 附加 Spring 启动参数 |
TERMINAL_SSH_MASTER_KEY |
- | SSH 凭据加密主密钥 |
release-scripts/mac/start.sh 仅识别发布目录下的 .env,并强制要求同时存在 application.yml。任一缺失会立即失败并提示修复。
访问 /term/ 时需要用户名密码登录。
设置密码(bcrypt,推荐):
# macOS
htpasswd -nbBC 10 '' 'your-password' | cut -d: -f2
# 或使用 Python
python3 -c "import bcrypt; print(bcrypt.hashpw(b'your-password', bcrypt.gensalt(10)).decode())"将生成的哈希写入 release/.env 的 AUTH_PASSWORD_HASH_BCRYPT:
AUTH_PASSWORD_HASH_BCRYPT='<your-bcrypt-hash>'当前的默认密码是 password
登录限流:默认 60 秒窗口内最多 10 次失败尝试(按 IP + 用户名)。
访问 /appterm/ 时使用 JWT Bearer Token(适用于嵌入 WebView 场景)。
- App 通过 React Native WebView bridge 提供 token(消息:
auth_token,请求:auth_refresh_request,回包:auth_refresh_result) - 401 时自动向 App 请求新 token 并重放请求
- 后端支持
local-public-key优先验签,未配置时回退到jwks-uri
app-auth:
enabled: true
local-public-key: "MIIBIj..." # RSA 公钥(优先)
jwks-uri: "https://..." # JWKS 端点(备用)
issuer: "your-issuer"
audience: "your-audience"/term/ 与 /appterm/ 都支持通过 URL 参数控制会话:
sessionId=<uuid>:切换到对应 session 的 tab(若该 session 已存在)。openNewSession=1或openNewSession=true:打开「新建会话」弹窗(用户手动 create/cancel)。openNonce=<any>:兼容保留字段,可选,不再作为触发前置条件。
示例:
/term/?sessionId=<uuid>
/appterm/?openNewSession=1
/term/?sessionId=<uuid>&openNewSession=1
行为规则:
sessionId与openNewSession可以同时存在,并且同时生效。- 用户切换 tab 时,URL 中
sessionId会同步为当前激活会话。 - 新建弹窗关闭(create 或 cancel)后,
openNewSession与openNonce会自动清理。 - 若通过
/term或/appterm(无尾斜杠)访问,服务端会在补全为/term/、/appterm/时保留原 query 参数。
- 浏览器刷新/断网不会销毁后端会话
- 前端重连时携带
lastSeenSeq,服务端自动补发缺失输出 - 全部客户端断开后,会话默认保留 1 小时(
detached-session-ttl-seconds) - 点击标签页关闭按钮会显式销毁会话
- 设置 SSH 主密钥(生产环境用环境变量):
export TERMINAL_SSH_MASTER_KEY="replace-with-a-strong-secret"本地开发建议在 backend/application.yml 中覆盖 terminal.ssh.master-key(backend/application.yml 已 gitignore)。
- 创建 SSH 凭据(密码或私钥二选一):
curl -X POST http://127.0.0.1:11947/term/api/ssh/credentials \
-H "content-type: application/json" \
-d '{
"host": "10.0.0.2",
"port": 22,
"username": "ubuntu",
"password": "***"
}'- 前端「新建会话」选择 SSH_SHELL,从已保存配置中选择即可。
curl -X POST http://127.0.0.1:11947/term/api/ssh/exec \
-H "content-type: application/json" \
-d '{
"credentialId": "<credential-id>",
"command": "uname -a",
"cwd": "/tmp",
"timeoutSeconds": 120
}'点击顶部 Copilot 按钮打开右侧栏:
- Summary:实时显示会话上下文(Context + Screen Text),默认 2 秒刷新,可一键复制
- Agent:创建 Agent Run、审批高风险步骤、中止运行、Quick Command 发送
可在 backend/application.yml(开发)或 release/application.yml(发布)中注册多个终端工具:
terminal:
cli-clients:
- id: codex
label: Codex
command: codex
args: []
workdir: .
env: {}
pre-commands:
- export https_proxy="http://127.0.0.1:8001"
shell: /bin/zsh
- id: claude
label: Claude Code
command: claude
args: []
workdir: .
shell: /bin/zsh新建会话时可选择不同的 CLI 客户端。
# 默认输出到 release/
./release-scripts/mac/package.sh
# 自定义输出目录
./release-scripts/mac/package.sh /tmp/term-release
# 指定环境
APP_ENV=development ./release-scripts/mac/package.sh /tmp/term-release-dev
release-scripts/mac/package.sh仅负责构建与 release 产物准备,不负责写入运行时.env/application.yml。 运行时配置由安装流程(setup)或运维在 release 目录提供。
打包产物结构:
release/
├── backend/
│ └── app.jar
├── frontend/
│ ├── dist/
│ ├── server.js
│ ├── node_modules/
│ ├── package.json
│ └── package-lock.json
├── logs/
├── run/
└── release-scripts/
├── mac/
│ ├── start.sh
│ └── stop.sh
└── windows/
├── *.ps1(可选)
└── *.bat(可选)
./release-scripts/mac/start.sh
# 或指定目录
./release-scripts/mac/start.sh /tmp/term-release启动前必须准备:
cp .env.example /tmp/term-release/.env
cp application.example.yml /tmp/term-release/application.yml./release-scripts/mac/stop.sh
# 或指定目录
./release-scripts/mac/stop.sh /tmp/term-releaseNginx 反向代理职责:
80 → 443重定向443TLS 终止- 所有流量反代到
http://127.0.0.1:11947 - 透传 WebSocket Upgrade
# 后端测试
cd backend && mvn test
# 前端检查
cd frontend
npm run lint # ESLint
npx tsc --noEmit # TypeScript 类型检查
npm run build # 生产构建
# 健康检查
curl http://127.0.0.1:11947/healthzserver.js 路由规则:
| 路径 | 行为 |
|---|---|
/healthz |
返回 200 OK |
/term/api/*、/appterm/api/* |
HTTP 反向代理到后端(分别 rewrite 到 /webapi/*、/appapi/*) |
/term/ws/*、/appterm/ws/* |
WebSocket 反向代理到后端(rewrite 到 /ws/*) |
/term/assets/*、/appterm/assets/* |
返回静态资源(dist/assets) |
/term/、/appterm/ |
返回 index.html(SPA 入口) |