# JP-webcli 自动化部署工具 — 能力·限制·优化分析 > 分析日期:2026-06-25 | 基于 webcli_v7(Playwright Web CLI 方案) --- ## 一、现有能力矩阵 ### 1.1 已验证的核心能力 | 能力 | 实现方式 | 可靠性 | 备注 | |------|---------|--------|------| | 自动登录 JumpServer | Playwright 填写表单 | ✅ 稳定 | 用户名 + 密码 | | OTP 双因素认证 | pyotp 自动生成 + 自动填写 | ✅ 稳定 | 发现"双回车"问题已修复 | | 资产列表导航 | 文本匹配 + ElementUI 按钮定位 | ✅ 稳定 | 依赖 DOM 结构 | | 连接对话框确认 | 多选择器回退 + JS fallback | ⚠️ 中等 | 7 种选择器兜底 | | Web 终端命令输入 | `keyboard.type()` + `Enter` | ✅ 稳定 | 支持 >10KB base64 载荷 | | SFTP 文件上传 | `set_input_files` | ⚠️ 中等 | 依赖 webkitdirectory 属性 | | 截图保存 | `page.screenshot()` | ✅ 稳定 | 用于人工验证 | | 分块 base64 传输 | 8 chunks × ~3-4KB | ✅ 稳定 | Hotfix#120 验证通过 | ### 1.2 已完成的部署验证 | Hotfix | 日期 | 关键验证 | |--------|------|---------| | #116 扫码获取 | 2026-06-22 | 首次 Web CLI 端到端部署 | | #120 扫码自动确认 | 2026-06-23 | SFTP 分块上传 + 29KB 源码部署 | | #48 Nginx Upstream | 2026-06-24 | 配置修复 + HTTPS 验证 | | SSO 单点登录 | 2026-06-25 | 环境变量注入 | --- ## 二、当前限制与已知问题 ### 2.1 P0 — 输出捕获不可靠(当前焦点) **问题描述:** - v7 使用 `terminal_page.keyboard.type()` 发送命令后,通过截图 + `.inner_html()` 读取输出 - 截图需要人工观察,无法程序化判断执行成功/失败 - DOM 文本提取 (`inner_html` / `text_content`) 依赖于 xterm.js 使用 **DOM 渲染器**;如果使用 Canvas/WebGL 渲染器,`.xterm-rows` 不存在 **失败模式:** ``` 场景 A: xterm.js 使用 Canvas 渲染器 → .xterm-rows 不存在 → text_content() 返回空 → 输出丢失 场景 B: 命令输出超过可视区域 → inner_html() 只包含可视行 → 输出不完整 场景 C: 命令尚未执行完毕就读取 → 捕获到不完整输出 → 误判为成功/失败 ``` **解决方案(进行中):** 见第三节 xterm.js 结构化捕获方案 ### 2.2 P1 — 硬编码路径和配置 | 问题 | 影响 | 修复方向 | |------|------|---------| | 脚本路径硬编码在 `~/.workbuddy/skills/` | 无法在不同机器间移植 | 改为项目相对路径 | | `gen-deploy-commands.py` 中的容器名硬编码 | 容器名变化时部署脚本失效 | 参数化 | | OTP secret 存储在脚本目录 | 多项目共享困难 | 统一凭证管理 | ### 2.3 P1 — 缺乏执行结果自动判定 **当前行为:** 截图保存后由人工观察判断执行结果 **期望行为:** 自动解析输出,判断 exit code、错误关键词、预期输出 **阻塞因素:** 首先需要可靠的结构化输出捕获(P0) ### 2.4 P2 — 环境依赖 | 依赖 | 影响 | |------|------| | Chrome/Chromium 浏览器 | 无法在无 GUI 服务器上运行 | | Playwright (~500MB) | 首次安装磁盘占用大 | | Windows 系统 | `keyboard.type()` 依赖系统键盘布局 | | JumpServer 页面 DOM 结构 | ElementUI 版本升级可能破坏选择器 | ### 2.5 P2 — 缺乏健康检查和告警 - 无 JumpServer 页面可达性检查 - 部署失败无企微通知 - 无命令执行超时后的自动重试策略 --- ## 三、xterm.js 结构化输出捕获方案(v8 设计) ### 3.1 核心原理 xterm.js 内部维护一个**行缓冲区**(`terminal.buffer.active`),所有终端输出都以结构化形式存储在此缓冲区中。通过 Playwright 的 `page.evaluate()` 可以直接访问该缓冲区。 ``` ┌─────────────────────────────────────────────┐ │ xterm.js 架构 │ │ │ │ ┌──────────┐ ┌──────────────────────┐ │ │ │ 输入事件 │───▶│ Terminal.write() │ │ │ │ keyboard │ │ Terminal.writeln() │ │ │ └──────────┘ └──────────┬───────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────┐ │ │ │ Buffer (内部) │ │ │ │ ┌────────────────────────────────┐ │ │ │ │ │ buffer.active (主缓冲区) │ │ │ │ │ │ - getLine(y) → BufferLine │ │ │ │ │ │ - translateToString() → str │ │ │ │ │ │ - length, baseY, cursorX/Y │ │ │ │ │ └────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────┐ │ │ │ │ │ buffer.normal (回滚缓冲区) │ │ │ │ │ └────────────────────────────────┘ │ │ │ └──────────────┬───────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────┐ │ │ │ 渲染器 (Renderer) │ │ │ │ ┌──────────┐ ┌──────────────────┐ │ │ │ │ │ DOM 渲染 │ │ Canvas/WebGL渲染 │ │ │ │ │ │ .xterm- │ │ 元素 │ │ │ │ │ │ rows │ │ │ │ │ │ │ └──────────┘ └──────────────────┘ │ │ │ └──────────────────────────────────────┘ │ │ ↑ v7 从这里读文本 │ │ ┌────────────────────────────────────┐ │ │ │ v8 从这里读文本 → buffer.active │ │ │ └────────────────────────────────────┘ │ └─────────────────────────────────────────────┘ ``` **关键优势:** 无论 xterm.js 使用哪种渲染器(DOM/Canvas/WebGL),缓冲区始终可用——这是 xterm.js 的核心数据结构,不依赖渲染层。 ### 3.2 探测脚本(已创建) **文件:** `~/.workbuddy/skills/jumpserver-automation/scripts/probe_xterm_buffer_v2.py` **运行方式:** ```powershell cd $env:USERPROFILE\.workbuddy\skills\jumpserver-automation\scripts & "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" probe_xterm_buffer_v2.py ``` **探测内容:** 1. **全局 xterm 实例**:搜索 `window` 上的 Terminal 实例 (`_core`, `buffer`, `options` 等特征属性),读 `buffer.active` 2. **DOM .xterm-rows 结构**:遍历 `.xterm-rows > div > span` 3. **辅助 textarea**:读取 `.xterm-helper-textarea` 4. **渲染器检测**:判断 DOM vs Canvas 渲染器,检测 React Fiber 5. **综合提取**:自动选择最佳方法并输出结构化文本 **输出文件:** - `webcli_output/probe_v2_pre_cmd.json` — 命令执行前探测 - `webcli_output/probe_v2_results.json` — 综合结果 - `webcli_output/probe_v2_output.txt` — 纯文本终端输出 - `webcli_output/probe_v2_final.png` — 截图对照 ### 3.3 预期结果与应对策略 | 探测结果 | 含义 | v8 策略 | |---------|------|---------| | `best_method = global_instance` | Terminal 实例在 window 上暴露 | ✅ 直接用 `buffer.active.getLine()` 读取 | | `best_method = dom_rows` | DOM 渲染器可用 | ⚠️ 可工作但依赖渲染器选择,降级为备用方案 | | `best_method = screen_textcontent` | 仅能通过 DOM 文本提取 | ❌ 需注入 Monkey Patch 拦截 `terminal.write()` | | `best_method = null` | 所有方法均失败 | ❌ 需 Monkey Patch 或重新评估方案 | ### 3.4 v8 输出捕获核心代码(预览) ```python def capture_terminal_output(page, wait_seconds=10): """ 通过 xterm.js 缓冲区结构化捕获终端输出 返回: { "text": str, "line_count": int, "method": str, "cols": int, "rows": int } """ page.wait_for_timeout(wait_seconds * 1000) result = page.evaluate(""" () => { // 策略1: 查找全局 Terminal 实例 const xtermKeys = ['_core', 'buffer', 'options', 'cols', 'rows']; const allKeys = Object.getOwnPropertyNames(window); for (const key of allKeys) { try { const val = window[key]; if (!val || typeof val !== 'object') continue; const matchCount = xtermKeys.filter(p => p in val).length; if (matchCount >= 3 && val.buffer && val.buffer.active) { const buf = val.buffer.active; const lines = []; const total = buf.length; const start = Math.max(0, total - Math.max(val.rows, 100)); for (let y = start; y < total; y++) { try { lines.push(buf.getLine(y).translateToString(true)); } catch(e) { lines.push(''); } } return { method: 'buffer.active', text: lines.join('\\n'), line_count: lines.length, cols: val.cols, rows: val.rows, cursor: { x: buf.cursorX, y: buf.cursorY } }; } } catch(e) {} } // 策略2: DOM fallback const rowsEl = document.querySelector('.xterm-rows'); if (rowsEl) { const lines = []; rowsEl.querySelectorAll(':scope > div').forEach(div => { let text = ''; div.querySelectorAll('span').forEach(s => text += s.textContent || ''); lines.push(text); }); return { method: 'dom_rows', text: lines.join('\\n'), line_count: lines.length }; } return { method: 'none', text: '', line_count: 0 }; } """) return result ``` ### 3.5 Monkey Patch 方案(兜底) 如果 xterm.js 实例完全没有暴露在全局作用域,可以在 `page.goto()` 之后、终端加载之前注入拦截代码: ```python # 在页面加载时注入 Monkey Patch page.add_init_script(""" // 拦截 xterm.js 的 write/writeln 方法 const origDefineProperty = Object.defineProperty; let capturedOutput = []; // 定时检查是否有 xterm 实例被创建 const observer = new MutationObserver(() => { const term = document.querySelector('.xterm'); if (term && term.__xterm_captured !== true) { term.__xterm_captured = true; // 通过终端 DOM 元素的内部引用获取 Terminal 实例 // 这里的实现依赖具体的 JumpServer/Luna 版本 } }); observer.observe(document.body, { childList: true, subtree: true }); window.__xterm_output = () => capturedOutput.join('\\n'); """) ``` --- ## 四、不适用的场景分析 ### 4.1 Web CLI 方案本质局限 | 场景 | 为什么不适用 | 替代方案 | |------|------------|---------| | **完全无人值守 CI/CD** | Playwright 需要 GUI 浏览器,headless 模式不可靠(JumpServer 部分元素需要可视化渲染) | SSH Key 认证(v17)或 Koko API | | **大规模并行部署** | 每个浏览器实例 ~500MB 内存,并行 5+ 实例会耗尽资源 | Ansible/SSH 批量执行 | | **跨平台(macOS/Linux)** | `keyboard.type()` 受键盘布局影响;Chrome channel 名称不同 | 使用 `page.keyboard.insertText()` 替代 | | **网络不稳定环境** | Web CLI 依赖持久 WebSocket 连接,断连后需重新登录+MFA | SSH 自带重连机制更可靠 | | **非交互式后台任务** | Web CLI 本质是模拟人工操作,不适合长时间后台运行 | systemd timer / cron job | | **JumpServer 不可用时** | 整个方案的前提条件不成立 | 直接 SSH 到目标服务器(内网场景) | ### 4.2 截图方式的不适用场景 | 场景 | 失败原因 | |------|---------| | 终端输出超长(>1000行) | 截图只能捕获可视区域 | | 输出包含非 ASCII 字符渲染异常 | 截图无法提取文本内容用于程序判定 | | 颜色/格式区分的信息 | 截图后颜色信息丢失(除非用彩色截图) | | 需要 grep/正则匹配输出内容 | 截图是像素,无法进行文本搜索 | --- ## 五、优化路线图 ### 近期(本周) ``` [P0] probe_xterm_buffer_v2 运行 + 分析结果 └→ 确认 xterm.js 缓冲区访问方式 [P0] webcli_v8: 结构化输出捕获 ├─ 用 buffer.active 替代 inner_html ├─ 自动判断命令执行成功/失败 └─ 输出保存为结构化 JSON [P1] webcli_v8: 命令执行超时 + 重试 ├─ 等待策略从固定 sleep 改为轮询 buffer └─ 失败自动重试(最多 3 次) ``` ### 中期(本月) ``` [P1] 路径参数化 + 配置文件 ├─ 所有硬编码路径→配置文件 └─ gen-deploy-commands.py 容器名参数化 [P1] 部署结果企微通知 └─ 成功/失败推送企微卡片消息 [P2] 健康检查脚本 └─ JumpServer 页面可达性检测 ``` ### 远期(探索) ``` [P2] SSH Key 认证方案 (v17) └─ 完全无人值守自动化 [P2] JumpServer Koko API └─ 不依赖浏览器的 WebSocket 终端连接 [P2] 跨平台支持 └─ macOS/Linux Playwright 兼容 ``` --- ## 六、下一步操作 1. **运行探测脚本**(需要你手动执行,因为需要 OTP + Chrome 浏览器): ```powershell cd $env:USERPROFILE\.workbuddy\skills\jumpserver-automation\scripts & "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" probe_xterm_buffer_v2.py ``` 2. **查看探测结果**:检查 `webcli_output/probe_v2_results.json`,确认 `best_method` 字段 3. **根据结果选择 v8 实现方案**: - 如果 `best_method = global_instance` → 直接用缓冲区读取,实现最简单 - 如果 `best_method = dom_rows` → 可用但需验证跨版本兼容性 - 如果 `best_method = null` 或 `screen_textcontent` → 需要 Monkey Patch 方案 4. **实现 webcli_v8.py**:基于探测结果选择最佳捕获策略