# 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 |