13 KiB
JP-webcli 自动化部署工具 — 能力、限制与优化空间分析
分析时间:2026-06-25 | 基于 v7/v8/v8b/v9_cdp/v10_persistent 全版本代码审查
一、现有能力全景
1.1 核心自动化能力(已验证可用)
| 能力 | 实现方式 | 状态 | 版本 |
|---|---|---|---|
| 自动登录 | Playwright 填写用户名/密码 | ✅ 稳定 | v6+ |
| OTP 双因素认证 | pyotp 本地生成 TOTP | ✅ 自动填充 | v4+ |
| 资产导航 | DOM 选择器定位目标行 | ✅ 支持模糊匹配 | v6+ |
| Web CLI 连接 | 点击连接按钮 → 处理 Luna dialog | ✅ 含轮询重试 | v6+ |
| 命令执行 | keyboard.type() 逐字输入 |
✅ 含 delay 防丢字 | v6+ |
| SFTP 文件上传 | set_input_files() 文件管理器 |
✅ 分块(8 chunk) | v7+ |
| base64 大文件传输 | 分段 echo + base64 -d 还原 | ✅ 250 字符/段 | v8b+ |
| 截图留存 | page.screenshot() |
✅ PNG 格式 | v3+ |
| 会话复用 | persistent_context / CDP connect |
✅ v10 | v10 |
| 轮询等待(替代死等) | 检测 prompt 字符/dialog/terminal | ✅ 40s → 待命 | v10.15 |
| 分阶段部署 | Part A(传输) + B(执行) + C(验证) | ✅ 脚本编排 | v5+ |
1.2 已验证的部署场景
| 场景 | 日期 | 成果 |
|---|---|---|
| Hotfix #116 扫码获取 | 06-22 | auth_qrcode.py 部署成功 |
| Hotfix #120 扫码自动确认 | 06-23 | 29KB qrcode_service.py + 8 chunk SFTP |
| Hotfix #48 Nginx Upstream | 06-24 | 配置修复 + HTTPS 验证 |
| Hotfix SSO 单点登录 | 06-25 | config.py + 环境变量注入 |
二、当前限制(按严重程度排序)
🔴 P0 — 阻塞性问题
2.1 xterm.js 输出无法结构化捕获(当前最紧急)
现状:所有版本(v6–v10)均依赖 截图 作为命令执行结果的唯一验证手段。
根因:xterm.js 将终端内容渲染到 <canvas> 元素,终端缓冲区存储在 JavaScript 内部对象中:
xterm.js 架构:
┌─────────────────────────────┐
│ DOM: <div class="xterm"> │
│ ├── .xterm-viewport │ ← 可滚动视口
│ ├── .xterm-screen │ ← 屏幕容器
│ │ └── <canvas> │ ← ★ 内容渲染到这里
│ └── .xterm-accessibility │ ← 无障碍层(可能为空)
└─────────────────────────────┘
terminal.buffer.active ← ★ 真正的文本在 JS 内存中
├── .baseY / .viewportY ← 滚动位置
├── .length ← 总行数
└── .getLine(y) ← 按行获取
└── .translateToString() ← 转为可见文本
当前所有版本的 "输出检测" 方法:
| 方法 | 代码 | 捕获到的是什么 |
|---|---|---|
page.evaluate("document.body.innerText") |
v10.15 | ⚠️ DOM 文本(不含 canvas 内容) |
page.screenshot() |
所有版本 | ⚠️ 二进制图片,不可解析 |
| prompt 正则匹配 | v10.15 | ⚠️ 只能判定"命令结束了",不能拿到输出 |
导致的失败模式:
-
Hotfix #120 的 18 个脚本分裂:
part1 → part2 → diag → verify → sub → tail → view中,大量 "查看日志" 脚本 (diag/log/tail/view) 的唯一目的是在终端执行cat/tail/curl,然后人工查看截图确认结果。如果能结构化捕获输出,这些脚本可以合并为带条件判断的单一流程。 -
无法自动判断部署成功与否:
run_v7_v5.py的proc.returncode只能判断 Playwright 进程是否正常退出,无法判断远程服务器上的docker restart是否成功、curl是否返回预期响应。 -
无退出码捕获:执行
docker ps && echo $?的结果在 canvas 里,脚本只能"相信"它成功了。 -
日志无法结构化存储:每次部署的输出是截图(PNG),而非文本。无法 grep、无法 diff、无法自动对比两次部署的差异。
-
截图驱动的不稳定性:
- 命令输出超过一屏时,截图只看到最后一段
- 颜色/样式变化可能被误读为错误
- 网络延迟导致截图时机窗口不可控
2.2 无 headless 模式(依赖 Chrome 可见窗口)
现状:v10 明文注释"默认非 headless 模式(JumpServer 可能检测 headless 并拒绝)"。
影响:
- 无法在无 GUI 的服务器上运行
- 必须保持 Chrome 窗口可见(占用桌面空间)
- 无法与用户其他 Chrome 使用并行
2.3 单实例限制
- 同一时间只能运行一个 Playwright 实例
- 多个部署任务必须串行
persistent_context和 CDP connect 互斥
🟡 P1 — 重要限制
| 限制 | 详情 | 影响 |
|---|---|---|
| 长命令输入易截断 | 单次 keyboard.type ~20KB 安全上限,超过需分块 |
29KB 文件需 8 chunk |
| 依赖 Windows Chrome | Playwright channel="chrome" |
无法在 Linux 服务器运行 |
| 磁盘占用大 | Playwright + Chromium ~500MB | 多实例部署开销大 |
| 无命令队列机制 | 没有批量执行 + 结果收集 | 每次部署需人工编排 |
| OTP 需本地密钥文件 | 依赖 otp_secret.key |
换机器需重新配置 |
| 错误恢复依赖人工 | 失败后无自动重试策略 | 夜间部署需人工值守 |
| 硬编码路径 | v7 中 V7/V7_DIR 路径硬编码 | 换环境需修改代码 |
🟢 P2 — 改进空间
| 限制 | 详情 |
|---|---|
| 部署包 >500MB 未迁移 | deploy-server/ 目录 |
| 无企微通知集成 | 部署结果需手动确认 |
| 无 CLI 参数标准 | v7/v8/v9/v10 参数不统一 |
| 无部署历史数据库 | 每次部署成功/失败无结构化记录 |
三、优化空间(按 ROI 排序)
🔴 P0 — 紧急优化
3.1 xterm.js 结构化输出捕获 ✅ 最高优先级
这是从 "截图验证" → "程序化验证" 的关键跃迁。
实现方案:通过 page.evaluate() 直接访问 xterm.js 的内部 buffer:
# 方案 A:读取 xterm.js 内部 buffer(推荐)
def capture_terminal_output(page):
"""从 xterm.js 的 terminal.buffer 中提取全部可见文本"""
return page.evaluate("""() => {
// JumpServer Luna 将 xterm 实例挂载在全局或 DOM 属性上
// 方式1: 通过 xterm 的全局引用
if (window.term) {
const term = window.term;
const buffer = term.buffer.active;
const lines = [];
for (let i = 0; i < buffer.length; i++) {
const line = buffer.getLine(i);
if (line) {
lines.push(line.translateToString());
}
}
return lines.join('\\n');
}
// 方式2: 通过 xterm 的 DOM 属性(如 data-xterm 或 __vue__)
// 方式3: 遍历 window 对象找 xterm 实例
for (const key of Object.keys(window)) {
try {
const obj = window[key];
if (obj && obj.buffer && obj.buffer.active) {
const buffer = obj.buffer.active;
const lines = [];
for (let i = 0; i < buffer.length; i++) {
const line = buffer.getLine(i);
if (line) lines.push(line.translateToString());
}
return lines.join('\\n');
}
} catch(e) {}
}
return null;
}""")
# 方案 B:利用 xterm.js 的无障碍 DOM 层(如果开启)
def capture_via_a11y(page):
"""xterm.js 可选地把内容同步到 .xterm-accessibility 层"""
return page.evaluate("""() => {
const a11y = document.querySelector('.xterm-accessibility');
if (a11y) {
const children = a11y.querySelectorAll('[role="presentation"]');
return Array.from(children).map(el => el.textContent).join('\\n');
}
return null;
}""")
预期收益:
| 改进项 | 当前状态 | 优化后 |
|---|---|---|
| 命令输出获取 | 截图(PNG 二进制) | 结构化文本 |
| 部署成功率判断 | 人工看截图 | 自动 grep exit code / 关键字 |
| 日志存储 | PNG 图片 | 文本(可 diff、可搜索) |
| 脚本数量 | Hotfix #120 需 18 个 | 可合并为 3-5 个带条件分支 |
| 错误诊断速度 | 人工翻截图 | 自动提取错误行 |
| CI/CD 集成 | 不可行 | 可行(输出可解析) |
实施步骤:
- 探测阶段:写一个
probe_xterm_buffer.py,在连上终端后执行page.evaluate(),尝试多种方式找到 xterm 实例引用 - 验证阶段:对比
capture_terminal_output()的输出和截图内容是否一致 - 集成阶段:修改
send_terminal_command()为send_and_capture(),返回结构化文本 - 高级阶段:利用 buffer 实现增量读取(只读新增行),避免重复传输全部内容
风险:JumpServer Luna 可能对 xterm 实例做了封装/私有化。需要先探测具体引用路径。
3.2 命令执行结果自动判定
依赖 3.1 的输出,实现:
def execute_and_verify(terminal_page, command, expected_pattern=None, timeout=60):
"""执行命令并自动判定结果"""
send_command(terminal_page, command)
output = wait_for_prompt_and_capture(terminal_page, timeout)
# 自动提取退出码
exit_code = extract_exit_code(output) # 从 "$?=0" 或显式 echo $?
# 自动匹配预期模式
if expected_pattern:
matched = re.search(expected_pattern, output)
return CommandResult(
output=output,
exit_code=exit_code,
matched=matched,
screenshot=screenshot(terminal_page)
)
🟡 P1 — 高价值优化
3.3 部署流水线引擎
将当前的手动编排的 Phase 1/2/3 流程抽象为通用引擎:
class DeployPipeline:
stages = [
Stage("prepare", commands=[...], verify="docker ps | grep wecom"),
Stage("upload", sftp_files=[...], verify="ls -la /tmp/"),
Stage("deploy", commands=[...], verify="curl -s localhost/api/health"),
Stage("verify", commands=[...], verify="grep 'started' /var/log/app.log"),
]
def run(self):
for stage in self.stages:
result = self.execute_stage(stage)
if not result.ok:
self.rollback()
self.notify_failure(stage, result)
return
self.notify_success()
3.4 headless 模式适配
探索 JumpServer 反 headless 检测的具体方式,针对性绕过:
- 添加
--disable-blink-features=AutomationControlled - 注入 JS 覆盖
navigator.webdriver - 使用
stealth模式的 Playwright 配置
3.5 企微通知集成
部署完成后自动发送企微消息:
✅ Hotfix #XXX 部署成功
目标: 10.90.5.110
耗时: 8m 23s
验证: curl 返回 200 OK
日志: webcli_output/v10_result.txt
🟢 P2 — 长期优化
| 优化项 | 说明 | 依赖 |
|---|---|---|
| SSH Key 认证(v17) | 绕过 OTP 流程,实现无人值守 | JumpServer 管理员配置 |
| API Token 方式 | 直接通过 Koko WebSocket 通信 | JumpServer API 权限 |
| 部署包 pip 化 | pip install jumpserver-webcli |
路径去硬编码 |
| Docker 化执行环境 | Chrome + Playwright 容器化 | headless 模式 |
| 部署历史数据库 | SQLite 记录每次部署 | 输出结构化捕获 |
| 回滚自动化 | 失败自动回滚到上一个版本 | 部署流水线引擎 |
四、技术路线优先级矩阵
紧急度
高 ●●● │ 低 ●○○
┌──────────┼──────────┐
高 ●●●│ xterm.js │ 部署流水 │
│ 输出捕获 │ 线引擎 │
影响面 ├──────────┼──────────┤
│ headless │ SSH Key │
低 ●○○│ 适配 │ 认证 │
└──────────┴──────────┘
五、总结
JP-webcli 自动化工具已经通过了 4 次生产部署验证,核心链路(登录 → 导航 → 命令执行 → 文件传输)稳定可用。当前最大的瓶颈是 xterm.js 输出无法结构化捕获,导致:
- 所有验证依赖截图(不可解析)
- 部署成功/失败无法自动判定
- 脚本过多(单个 hotfix 分裂为 18 个)
- 无法集成 CI/CD
建议立即启动 xterm.js buffer 探测(估计 1-2 小时),确认 JumpServer Luna 中 xterm 实例的引用路径后,一周内完成 send_and_capture() 改造。这将使自动化覆盖率从当前的 ~40% 提升到 ~90%。