# AIOps 终端体系设计说明 > 更新:2026-07-03 > 范围:SSH WebShell、Kubernetes Pod exec、kubectl 集群终端、流水线 Workspace 终端、审计实时查看与历史回放。RDP 仅在本文中说明边界,完整实现见 `docs/rdp/`。 > 本文用于记录当前实现、设计取舍与后续重构方向。// Logan.Li --- ## 0. 文档阅读路径 本目录描述“字符终端统一底座”,RDP 图形桌面只在总体架构中对齐边界,具体实现见 [docs/rdp](../rdp/README.md)。 | 文档 | 适合读者 | 内容 | |------|----------|------| | [README.md](./README.md) | 产品、架构、联调人员 | 终端体系总体目标、各类终端现状、审计和重构方向 | | [implementation-spec.md](./implementation-spec.md) | 前端、后端、测试工程师 | WebSSH、Pod exec、kubectl、Workspace 终端的可复刻实现规格 | 另一个项目要复刻 AIOps 终端能力时,建议先实现 `implementation-spec.md` 中的统一 xterm/WebSocket/PTY 闭环,再按业务类型接入 SSH、K8s、Workspace 和审计。// Logan.Li --- ## 1. 总体目标 AIOps 的终端体系不是单一功能,而是一组共享基础能力: 1. **实时交互**:浏览器通过 WebSocket 与后端建立交互式会话。 2. **尺寸自适应**:前端终端容器变化后执行 `fit()`,并同步 `cols/rows` 到远端 PTY。 3. **审计留痕**:会话起止、用户、资产、输入动作、命令、输出大小、录制数据可追溯。 4. **历史回放**:SSH 终端使用文本流回放,RDP 使用 Canvas 图形帧回放。 5. **统一体验**:SSH、Pod、kubectl、Workspace 终端共享 xterm.js 适配策略;RDP 使用 Canvas 独立渲染策略。 核心原则: - 终端输入只走实时链路,审计只记录有价值的离散事件。 - `mousemove` 等高频低价值事件不持久化。 - RDP 历史回放采用节流图形帧,不保存每个实时帧,避免存储爆炸。 - 前端终端窗口不手写分散 resize 逻辑,统一走 `frontend/src/lib/xterm-utils.ts`。// Logan.Li --- ## 2. 统一链路 ### 2.1 SSH / Pod / kubectl / Workspace ``` Browser xterm.js ├─ onData → WebSocket JSON {status:0,data} ├─ ResizeObserver + FitAddon └─ resize → WebSocket JSON {status:1,cols,rows} │ ▼ Backend WebSocket Handler ├─ SSH: ssh.Session + PTY ├─ Pod exec: Kubernetes channel.k8s.io WebSocket ├─ kubectl: local kubectl process + PTY └─ Workspace: pipeline workspace shell + PTY │ ▼ Audit / Recording ├─ terminal session metadata ├─ asciicast text stream └─ command records ``` ### 2.2 Windows RDP ``` Browser Canvas ├─ mouse/key/clipboard input → WebSocket JSON └─ bitmap frames ← WebSocket binary RDPR │ ▼ controller/rdp.go ├─ load RDP credential ├─ create RDPSession ├─ start Engine.Run() ├─ broadcast live audit frames └─ throttle snapshot frames for replay │ ▼ pkg/rdp Engine └─ AIOps RDP protocol engine ``` RDP 与 xterm 终端不同:它没有 PTY 的 `cols/rows` 概念,主要优化点是输入事件合并、图形帧编码、Canvas 绘制和录屏抽样。// Logan.Li --- ## 3. 前端终端窗口适配 公共工具:`frontend/src/lib/xterm-utils.ts` | 方法 | 作用 | |------|------| | `fitTerminal()` | 调用 FitAddon,并在 WebSocket 打开时发送 `{status:1,cols,rows}` | | `mountXterm()` | 等容器渲染完成后挂载 xterm,延迟多次 fit,减少右侧黑边 | | `attachTerminalResize()` | 通过 ResizeObserver 监听容器尺寸,节流 fit 和 resize 同步 | 已接入或应接入的组件: | 组件 | 类型 | 说明 | |------|------|------| | `TerminalDialog.tsx` | SSH 单会话终端 | 资产列表打开 SSH | | `WebShell.tsx` | SSH 多标签终端 | WebShell 页面 | | `PodExecTerminal.tsx` | Pod exec | K8s Pod 容器终端 | | `KubectlTerminal.tsx` | kubectl | 集群级 kubectl shell | | `WorkspaceTermDialog.tsx` | Workspace shell | 流水线构建目录终端 | | `LiveMonitorDialog.tsx` | SSH 实时审计 | 只读 xterm | | `ReplayDialog.tsx` | SSH 历史回放 | asciicast 回放 | 终端容器统一使用: ```tsx
``` 全局样式位置:`frontend/src/index.css` ```css .xterm-terminal-host { width: 100%; overflow: hidden; background: #0a0e14; } .xterm-terminal-host .xterm { height: 100%; } ``` 注意事项: - Dialog 动画会影响 xterm 首次测量,因此终端弹框禁用过渡。 - `fit()` 后必须同步后端 PTY 尺寸,否则只是前端铺满,远端程序仍按旧尺寸输出。 - `term.open()` 只能对同一个 xterm 实例执行一次,复用场景需要检查 `term.element`。// Logan.Li --- ## 4. SSH 终端实现 主要文件: | 文件 | 说明 | |------|------| | `backend/controller/webshell.go` | SSH WebSocket、PTY、录制、命令审计 | | `backend/model/terminal_session.go` | SSH 会话模型 | | `backend/model/session_command.go` | 命令审计模型 | | `frontend/src/components/server/TerminalDialog.tsx` | 单资产 SSH 终端 | | `frontend/src/components/server/WebShell.tsx` | 多标签 SSH 终端 | 协议: ```json { "status": 0, "data": "ls -la\n" } { "status": 1, "cols": 120, "rows": 34 } ``` 审计策略: - 会话记录:开始时间、结束时间、用户、资产、状态、录制大小。 - 录制数据:asciicast 文本流。 - 命令记录:按回车提交后提取命令,不记录每个按键。 - 输出统计:下一条命令前累计 stdout 字节数写入上一条命令。 优化方向: - 命令列表按会话聚合展示,减少碎片行。 - 对连续输入、粘贴、多行脚本增加摘要视图。 - 高危命令识别只用于展示和告警,不阻塞正常录制。// Logan.Li --- ## 5. Kubernetes Pod Exec 主要文件: | 文件 | 说明 | |------|------| | `backend/controller/k8s_pod.go` | Pod exec WebSocket 代理 | | `frontend/src/components/kubernetes/PodExecTerminal.tsx` | Pod 终端 UI | 链路: ``` xterm.js └─ WebSocket JSON └─ backend └─ Kubernetes API WebSocket channel.k8s.io ``` K8s channel 映射: | Channel | 方向 | 说明 | |---------|------|------| | 0 | client → pod | stdin | | 1 | pod → client | stdout | | 2 | pod → client | stderr | | 3 | pod → client | error/status | | 4 | client → pod | resize | 前端 resize JSON: ```json { "status": 1, "cols": 120, "rows": 34 } ``` 后端转换为 K8s resize frame: ```json { "Width": 120, "Height": 34 } ``` 注意事项: - Pod exec 不应进入 SSH 命令审计表,除非未来设计 K8s 独立审计模型。 - 容器切换、shell 切换会重连 WebSocket。 - ESC 必须传给终端,Dialog 不能用 ESC 自动关闭。// Logan.Li --- ## 6. kubectl 集群终端 主要文件: | 文件 | 说明 | |------|------| | `backend/controller/kubectl_shell.go` | kubectl shell WebSocket | | `frontend/src/components/kubernetes/KubectlTerminal.tsx` | kubectl 终端 UI | 设计目标: - 给运维提供集群级 kubectl shell。 - 使用与 SSH 相同的 xterm 自适应策略。 - 后端负责 kubeconfig/集群上下文隔离。 风险点: - kubectl shell 权限大,必须依赖 RBAC、审计和操作权限控制。 - 后续应对危险命令做二次确认或审计标记。 - 多集群切换时不能复用旧进程上下文。// Logan.Li --- ## 7. Workspace 终端 主要文件: | 文件 | 说明 | |------|------| | `backend/controller/workspace_files.go` | 流水线工作区文件/终端入口 | | `frontend/src/components/deploy/WorkspaceTermDialog.tsx` | 工作区终端 UI | 用途: - 进入流水线构建目录查看文件。 - 辅助排查构建产物、日志、临时文件。 设计约束: - 默认目录必须限制在 pipeline workspace 内。 - 后端不能允许用户通过路径逃逸进入任意系统目录。 - 终端尺寸适配与 SSH 共用 `xterm-utils`。// Logan.Li --- ## 8. RDP 实现 主要文件: | 文件 | 说明 | |------|------| | `backend/controller/rdp.go` | RDP WebSocket、会话、审计、实时帧推送 | | `backend/controller/rdp_live.go` | RDP 实时审计广播 | | `backend/controller/rdp_session.go` | RDP 会话列表、录制、终止 API | | `backend/pkg/rdp/engine.go` | 引擎选择 | | `backend/pkg/rdp/session_engine.go` | 自研 RDP 引擎主循环 | | `frontend/src/components/server/RdpDialog.tsx` | RDP 实时操作窗口 | | `frontend/src/components/system/RdpAuditViewer.tsx` | RDP 实时审计与历史回放 | ### 8.1 自研 RDP 引擎策略 当前后端固定使用自研 `SessionEngine`,不再通过 `RDP_ENGINE` 切换第三方兜底。已删除的 grdp 目录备份到: ```text /opt/rdp/third-party/grdp-aiops-backup-20260703 ``` 仓内 `backend/third_party` 已清理,Dockerfile 不再复制本地 replace 模块。// Logan.Li ### 8.2 WebSocket 帧协议 服务端到浏览器: ```json { "type": "session", "message": "123", "width": 1024, "height": 800 } { "type": "status", "message": "正在连接 RDP ..." } { "type": "error", "message": "..." } ``` 图像帧优先走二进制: ```text RDPR + x:uint32 + y:uint32 + width:uint32 + height:uint32 + rgba:[] ``` 历史回放帧走节流 PNG: ```json { "type": "bitmap", "x": 0, "y": 0, "width": 300, "height": 200, "image": "data:image/png;base64,..." } ``` 浏览器到服务端: ```json { "type": "mouse_move", "x": 100, "y": 200 } { "type": "mouse_down", "x": 100, "y": 200, "button": 0 } { "type": "mouse_up", "x": 100, "y": 200, "button": 0 } { "type": "wheel", "x": 100, "y": 200, "delta_y": -120 } { "type": "key", "code": "KeyA", "key": "a", "down": true } { "type": "clipboard", "text": "..." } ``` ### 8.3 输入优化 - 前端 `mousemove` 16ms 合并,只保留最新坐标。 - `mouse_down`、`mouse_up` 前先 flush 最新坐标,保证拖拽起止准确。 - 后端输入队列满时丢弃移动事件,关键事件优先。 - RDP 引擎处理输入时不打印每个 `mousemove`,避免日志拖慢实时操作。 ### 8.4 图形优化 - 实时图形帧使用 RGBA 二进制直传,历史回放使用节流 PNG 快照。 - 前端 Canvas 使用 `putImageData()` 绘制二进制帧。 - 右下角帧统计节流刷新,避免每帧 React 重渲染。 - RDP 录屏只保存节流快照,不保存每个实时帧。 ### 8.5 审计策略 持久化: - 会话元数据:用户、资产、开始/结束时间、状态、录制大小。 - 离散操作:点击、滚轮、剪贴板、文件上传下载、输入摘要。 - 历史回放:节流图形快照。 不持久化: - 高频 `mousemove`。 - 每个实时 RGBA 帧。 - 无业务含义的连续坐标变化。 拖拽建议记录: ```text [RDP拖拽] button=left start=(x1,y1) end=(x2,y2) duration=850ms ``` 普通点击建议记录: ```text [RDP点击] button=left x=100 y=200 ``` 键盘输入建议合并: ```text [RDP输入] abc*** # 敏感字段后续可脱敏 ``` --- ## 9. 审计页设计 当前菜单:`/audit/terminal` 建议子页签: | 页签 | 内容 | |------|------| | 在线会话 | SSH 在线会话 + RDP 在线会话 | | 历史会话 | SSH 历史 asciicast 回放 + RDP Canvas 回放 | | 会话命令 | SSH 命令 + RDP 操作摘要,按会话聚合展示 | ### 9.1 在线会话 SSH: - 使用 `LiveMonitorDialog` - 后端 `/ws/audit/:sessionId` - 只读 xterm RDP: - 使用 `RdpAuditViewer mode="live"` - 后端 `/ws/rdp-audit/:sessionId` - 只读 Canvas ### 9.2 历史会话 SSH: - 使用 `ReplayDialog` - 数据源:`terminalSessionApi.getRecord()` - 格式:asciicast 文本 RDP: - 使用 `RdpAuditViewer mode="replay"` - 数据源:`rdpSessionApi.getRecord()` - 格式:节流 PNG bitmap JSON ### 9.3 会话命令 / 操作摘要 问题: - SSH 命令记录适合逐条展示。 - RDP 如果按键、点击、剪贴板都逐条展示,会出现大量低价值行。 建议: 1. 后端写入时尽量合并: - 连续键盘输入合并为 `[RDP输入] ...` - 拖拽只记录开始/结束。 - 点击和滚轮按短时间窗口合并。 2. 前端展示时按会话聚合: - 默认展示会话摘要。 - 展开后看详细事件。 - 高危动作、文件传输、剪贴板单独标记。 --- ## 10. 当前架构质量评估 ### 10.1 做得好的地方 - `backend/pkg/rdp/domain` 定义了引擎接口,控制器不直接依赖协议细节。 - `transport/`、`credssp/`、`rle/`、`display/` 已经有基础分层。 - RDP 依赖已收敛到自研协议栈,构建不再需要 `backend/third_party`。 - xterm 终端已抽出公共适配工具,后续维护成本低。 - RDP 实时帧和历史回放帧开始分离,避免审计存储被实时帧撑爆。 ### 10.2 需要优化的地方 | 文件 | 问题 | 建议 | |------|------|------| | `backend/pkg/rdp/mcs_probe.go` | 1664 行,严重超过红线 | 拆成 `mcs_connect.go`、`gcc_caps.go`、`confirm_active.go`、`client_info.go`、`probe.go` | | `backend/controller/rdp.go` | 约 460 行,承担会话、审计、帧编码、快照 | 拆出 `rdp_sink.go`、`rdp_audit.go`、`rdp_input_audit.go` | | `RdpDialog.tsx` | 实时渲染、输入、工具面板入口集中 | 后续拆出 `useRdpSocket()`、`useRdpCanvas()`、`useRdpInput()` | | `PodExecTerminal.tsx` | 连接、UI、resize 混合 | 可抽 `useXtermWebSocket()` | | `KubectlTerminal.tsx` | 与 Pod exec 重复 | 与 Pod/Workspace 共用 hook | ### 10.3 建议目标结构 后端 RDP: ```text backend/pkg/rdp/ ├── domain/ ├── engine.go ├── session_engine.go ├── input/ │ ├── fastpath.go │ └── keymap.go ├── graphics/ │ ├── framebuffer.go │ ├── codec.go │ └── rle/ ├── mcs/ │ ├── connect.go │ ├── gcc.go │ ├── caps.go │ └── active.go ├── credssp/ └── transport/ ``` 控制器: ```text backend/controller/ ├── rdp.go # 路由入口和会话编排 ├── rdp_sink.go # WebSocket frame sink ├── rdp_live.go # 实时审计广播 ├── rdp_input_audit.go # 点击/拖拽/输入摘要 ├── rdp_session.go # 会话 REST API ├── rdp_staged.go # RDPDR 下载暂存 API └── rdp_cred.go ``` 前端: ```text frontend/src/components/terminal/ ├── useXtermSocket.ts ├── XtermDialogShell.tsx └── terminalTheme.ts frontend/src/components/rdp/ ├── useRdpSocket.ts ├── useRdpCanvas.ts ├── useRdpInput.ts ├── RdpDialog.tsx └── RdpAuditViewer.tsx ``` --- ## 11. 验证清单 终端类修改后建议验证: ```bash cd frontend npx tsc --noEmit npm run build ``` 后端修改后建议验证: ```bash cd backend go build -o aiops . ``` 端到端: ```bash ./run.sh docker up python3 tests/integration_test.py --host localhost --port 8080 ``` 手动检查: - SSH 终端打开、缩放、全屏、输入、回放。 - Pod exec 打开、切 shell、缩放、全屏。 - kubectl 终端打开、缩放、全屏。 - Workspace 终端打开、缩放、全屏。 - RDP 打开、拖动窗口、文件夹拖拽、实时审计、历史回放。 - 会话命令页筛选、导出、从命令跳转回放。// Logan.Li