# AIOps 终端体系实现规格
> 更新:2026-07-05
> 目标:让另一个项目仅阅读本文档,也能实现 WebSSH、Pod exec、kubectl、Workspace 终端和审计回放。
> 范围:xterm.js 前端底座、WebSocket 协议、后端 PTY/SSH/K8s 代理、会话录像、命令审计、实时查看。
---
## 1. 终端类型和职责
| 类型 | 前端渲染 | 后端执行 | 典型入口 | 审计方式 |
|------|----------|----------|----------|----------|
| SSH WebShell | xterm.js | `ssh.Session + PTY` | 服务器管理、WebShell | asciicast + 命令 |
| Pod Terminal | xterm.js | K8s `pods/exec` WebSocket | Pod 详情 | 预留 K8s 审计 |
| kubectl Terminal | xterm.js | 本地 `kubectl` 进程 + PTY | 集群管理 | 建议命令审计 |
| Workspace Terminal | xterm.js | 本地 shell + PTY,限制目录 | 流水线工作区 | 建议命令审计 |
| RDP | Canvas | RDP 协议引擎 | Windows 资产 | 独立图形录像 |
本文只定义字符终端。RDP 详见 [RDP 实现规格](../rdp/implementation-spec.md)。
---
## 2. 统一前端底座
### 2.1 组件结构
```text
Terminal Dialog/Page
├── create Terminal()
├── load FitAddon
├── mount into .xterm-terminal-host
├── open WebSocket
├── onData → ws.send({ status: 0, data })
├── ResizeObserver → fit() → ws.send({ status: 1, cols, rows })
└── ws.onmessage → terminal.write(data)
```
公共工具应提供:
| 方法 | 职责 |
|------|------|
| `mountXterm()` | 延迟挂载、打开 terminal、执行多次初始 fit |
| `fitTerminal()` | 调用 FitAddon,返回 `cols/rows` |
| `attachTerminalResize()` | 监听容器 resize,节流 fit 并同步后端 |
| `disposeTerminal()` | 关闭 WebSocket、移除 observer、销毁 xterm |
### 2.2 DOM 和样式要求
```tsx
```
```css
.xterm-terminal-host {
width: 100%;
height: 100%;
overflow: hidden;
background: #0a0e14;
}
.xterm-terminal-host .xterm {
height: 100%;
}
```
要求:
- 终端容器必须有稳定高度,不能依赖内容撑开。
- Dialog 动画会影响 xterm 测量,终端弹窗建议禁用打开/关闭缩放动画。
- `term.open()` 同一实例只能调用一次。
- `fit()` 后必须把 `cols/rows` 发给后端,否则远端 PTY 仍按旧尺寸输出。
### 2.3 推荐 xterm 配置
```ts
const terminalOptions = {
cursorBlink: true,
convertEol: true,
fontFamily: 'Menlo, Monaco, Consolas, monospace',
fontSize: 13,
lineHeight: 1.15,
scrollback: 5000,
theme: {
background: '#0a0e14',
foreground: '#d8dee9',
cursor: '#f8fafc',
},
};
```
交互要求:
- ESC、Ctrl+C、Ctrl+D 必须传给终端,不应被 Dialog 或页面快捷键抢走。
- 粘贴走 xterm 默认粘贴或 `navigator.clipboard.readText()` 后写入 WebSocket。
- 多标签终端切换时,不要销毁非当前标签的 WebSocket,除非用户主动关闭。
---
## 3. WebSocket 协议
### 3.1 通用 JSON 协议
浏览器到后端:
```json
{ "status": 0, "data": "ls -la\n" }
{ "status": 1, "cols": 120, "rows": 34 }
```
后端到浏览器:
```json
{ "status": 0, "data": "total 8\r\n" }
{ "status": 2, "message": "connected" }
{ "status": -1, "message": "connection failed" }
```
字段语义:
| 字段 | 方向 | 说明 |
|------|------|------|
| `status=0` | 双向 | stdin/stdout 数据 |
| `status=1` | 浏览器到后端 | resize,包含 `cols/rows` |
| `status=2` | 后端到浏览器 | 状态提示,可选 |
| `status=-1` | 后端到浏览器 | 错误提示 |
实现要求:
- 后端读取 WebSocket 后按 `status` 分流。
- stdout/stderr 可以合并写回 `status=0`,也可以扩展 `stream` 字段。
- 输入数据必须原样写入 PTY,不要按行缓存,否则 vim/top 等交互程序不可用。
- resize 事件必须节流,建议 100-200ms。
### 3.2 鉴权
WebSocket 无法稳定设置自定义 Header,因此支持两种方式:
```text
/ws/terminal/:serverId?token=
/ws/pod/exec?...&token=
```
要求:
- 后端优先读 `Authorization`,其次读 `?token=`。
- 鉴权通过后仍要做资源权限校验。
- token 不能写入审计命令和终端录制。
---
## 4. SSH WebShell
### 4.1 后端流程
```text
GET /ws/terminal/:serverId
→ JWTAuth
→ load server asset
→ load credential
→ ssh.Dial
→ session.RequestPty(term, rows, cols, modes)
→ session.Shell()
→ create sys_terminal_sessions
→ goroutine: ws input → ssh stdin
→ goroutine: ssh stdout/stderr → ws output + recorder + audit hub
→ on close: update duration, status, record_data, record_size
```
PTY 参数:
```go
ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
```
### 4.2 命令审计
基础策略:
- 按回车提取命令。
- 不记录每个按键。
- 下一条命令出现前,统计上一条命令后的输出字节数。
- 粘贴多行脚本应合并或标记为多行命令。
命令表:
```text
sys_session_commands
session_id
session_type = terminal
user_id / username
server_id / server_name
command
output_size
timestamp
```
敏感信息建议:
- 对 `password=...`、`token=...`、`AKIA...`、私钥片段做脱敏。
- 命令审计保存脱敏结果,原始录制可按安全策略决定是否保存。
---
## 5. asciicast 录制和回放
### 5.1 录制格式
使用 asciicast v2:
```json
{"version":2,"width":120,"height":34,"timestamp":1783131042,"env":{"TERM":"xterm-256color"}}
[0.120,"o","Last login: ...\r\n"]
[0.450,"o","$ "]
[1.200,"o","ls -la\r\n"]
```
实现要求:
- 第一行是 header。
- 后续每行是 `[timeOffset, "o", output]`。
- 只录制后端输出,不录制用户输入的独立事件,避免重复。
- `RecordData` 建议限制大小,超过上限后截断并标记。
### 5.2 回放
前端回放流程:
```text
GET /api/v1/terminal-sessions/:id/record
→ parse asciicast
→ create read-only xterm
→ 按 timeOffset 调度 terminal.write()
→ 支持播放、暂停、倍速、拖动进度
```
验收:
- 回放画面和原终端输出顺序一致。
- 支持命令书签跳转。
- 删除会话后录制不可访问。
---
## 6. Pod Exec
### 6.1 后端链路
```text
Browser xterm
→ AIOps backend WebSocket
→ Kubernetes API /api/v1/namespaces/{ns}/pods/{pod}/exec
→ channel.k8s.io WebSocket
→ container shell
```
K8s channel:
| Channel | 方向 | 内容 |
|---------|------|------|
| 0 | client to pod | stdin |
| 1 | pod to client | stdout |
| 2 | pod to client | stderr |
| 3 | pod to client | error/status |
| 4 | client to pod | resize |
resize frame:
```json
{ "Width": 120, "Height": 34 }
```
实现要求:
- shell 可选 `/bin/bash`、`/bin/sh`。
- namespace、pod、container 必须做权限校验。
- 容器切换时必须重连。
- Pod exec 不应写入 SSH 的 `sys_terminal_sessions`,建议后续建 K8s 独立会话表。
---
## 7. kubectl 终端
### 7.1 后端链路
```text
GET /ws/kubectl?cluster_id=1
→ JWTAuth + cluster permission
→ create temp kubeconfig
→ exec kubectl with PTY
→ ws input/output bridge
→ cleanup temp kubeconfig and process
```
安全要求:
- kubeconfig 必须按用户和集群隔离。
- 临时文件使用随机名,关闭后删除。
- 禁止复用上一次集群上下文。
- kubectl 权限由 AIOps RBAC 和集群 RBAC 双重约束。
建议审计:
- 记录输入命令。
- 标记 `delete`、`scale`、`rollout restart`、`patch`、`apply` 为高风险。
- 后续接入审批或二次确认。
---
## 8. Workspace 终端
### 8.1 后端链路
```text
GET /ws/workspace/:pipelineRunId/terminal
→ JWTAuth + pipeline permission
→ resolve workspace dir
→ ensure dir is under workspace root
→ start shell with PTY
→ ws input/output bridge
```
路径安全:
- 所有路径必须 `filepath.Clean`。
- `realpath` 后必须仍位于 workspace root 下。
- 禁止用户通过 `../../` 进入任意系统目录。
- Shell 的工作目录固定为本次 pipeline workspace。
---
## 9. 实时审计
### 9.1 SSH 实时查看
```text
active SSH session
→ AuditHub broadcast output
→ /ws/audit/:sessionId
→ LiveMonitorDialog read-only xterm
```
要求:
- 审计端只读,不允许发送 stdin。
- 审计连接不能影响主会话性能。
- 活动会话结束后,审计连接收到关闭提示。
### 9.2 统一审计页
建议页面分区:
| 页签 | 内容 |
|------|------|
| 在线会话 | SSH active + RDP active |
| 历史会话 | SSH asciicast + RDP bitmap replay |
| 会话命令 | SSH 命令 + RDP 操作摘要 |
---
## 10. 数据模型
### 10.1 SSH 会话
```text
sys_terminal_sessions
id
user_id / username
server_id / server_name / server_ip / server_port
client_ip
status: active | closed | failed
duration
record_data
record_size
start_time / end_time
```
### 10.2 命令审计
```text
sys_session_commands
id
session_id
session_type: terminal | rdp
user_id / username
server_id / server_name
command
output_size
timestamp
```
### 10.3 策略配置
```text
sys_audit_settings
retention_days
record_all
session_watermark_enabled
session_record_enabled
command_policy_enabled
approval_required
```
字符终端第一版至少需要:
- `record_all`
- `session_record_enabled`
- `session_watermark_enabled`
- `retention_days`
---
## 11. 错误处理
| 场景 | 前端表现 | 后端处理 |
|------|----------|----------|
| 鉴权失败 | 显示未登录或无权限 | 关闭 WebSocket |
| 凭据错误 | 显示连接失败 | 会话状态 `failed`,保存错误 |
| SSH 超时 | 显示超时 | 释放 session/client |
| PTY resize 失败 | 显示警告或忽略 | 继续会话 |
| WebSocket 断开 | 显示已断开 | 关闭远端 session |
| 用户关闭窗口 | 清理资源 | 更新 `end_time`、`duration` |
实现要求:
- 任何退出路径都必须关闭 PTY、进程、SSH client、WebSocket。
- 后端 goroutine 需要 context 取消,避免泄露。
- 会话状态更新要放在 `defer` 中兜底执行。
---
## 12. 验收清单
### 12.1 WebSSH
1. 打开服务器 SSH 终端成功。
2. `ls`、`top`、`vim`、Ctrl+C、Ctrl+D 可用。
3. 拖动窗口或最大化后,远端 `stty size` 与前端一致。
4. 会话关闭后 `sys_terminal_sessions` 状态为 `closed`。
5. 回放可播放完整输出。
6. 命令审计能看到命令和输出大小。
### 12.2 Pod Exec
1. 选择 namespace/pod/container 后能进入 shell。
2. stdout/stderr 都能显示。
3. resize 后 `stty size` 正确。
4. 容器切换会重连。
5. 无权限 namespace 无法连接。
### 12.3 kubectl
1. 打开指定集群 kubectl 终端。
2. `kubectl get pods -A` 输出正常。
3. 切换集群不会复用旧上下文。
4. 关闭窗口后后端 kubectl 进程退出。
### 12.4 Workspace
1. 打开流水线工作区终端。
2. 默认目录在 workspace root。
3. `pwd`、`ls` 正常。
4. 不能通过路径逃逸访问系统目录。
### 12.5 审计回放
1. 在线会话能实时只读查看。
2. 历史会话能按时间播放。
3. 删除会话后录制和命令记录按策略处理。
4. 关闭录制策略后不写 `record_data`。
---
## 13. 常见问题
| 问题 | 原因 | 处理 |
|------|------|------|
| 终端右侧黑边 | 初始 fit 太早 | mount 后延迟多次 fit |
| resize 后远端仍旧宽度 | 只 fit 前端,未发 cols/rows | WebSocket 发送 `status=1` |
| Ctrl+C 不生效 | 页面快捷键拦截 | xterm 容器 focus 后透传 |
| vim 显示错乱 | PTY 类型或 resize 不正确 | 使用 `xterm-256color` 并同步尺寸 |
| 回放速度不对 | time offset 解析错误 | 按 asciicast v2 第一列调度 |
| WebSocket 断开后进程残留 | 后端未 cancel context | close handler 统一清理 |
| Pod exec 没输出 | K8s channel 未拆分 | 按 channel 1/2 读取 stdout/stderr |