Files
wecom_it_smart_desk/jumpserver-webcli/ANALYSIS.md
T

15 KiB
Raw Blame History

JP-webcli 自动化部署工具 — 能力·限制·优化分析

分析日期:2026-06-25 | 基于 webcli_v7Playwright 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-   │  │ <canvas> 元素    │  │   │
│  │  │ rows      │  │                  │  │   │
│  │  └──────────┘  └──────────────────┘  │   │
│  └──────────────────────────────────────┘   │
│               ↑  v7 从这里读文本              │
│  ┌────────────────────────────────────┐     │
│  │  v8 从这里读文本 → buffer.active    │     │
│  └────────────────────────────────────┘     │
└─────────────────────────────────────────────┘

关键优势: 无论 xterm.js 使用哪种渲染器(DOM/Canvas/WebGL),缓冲区始终可用——这是 xterm.js 的核心数据结构,不依赖渲染层。

3.2 探测脚本(已创建)

文件: ~/.workbuddy/skills/jumpserver-automation/scripts/probe_xterm_buffer_v2.py

运行方式:

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 输出捕获核心代码(预览)

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() 之后、终端加载之前注入拦截代码:

# 在页面加载时注入 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 浏览器):

    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 = nullscreen_textcontent → 需要 Monkey Patch 方案
  4. 实现 webcli_v8.py:基于探测结果选择最佳捕获策略