313 lines
13 KiB
Markdown
313 lines
13 KiB
Markdown
# 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 | ⚠️ 只能判定"命令结束了",不能拿到输出 |
|
||
|
||
**导致的失败模式**:
|
||
|
||
1. **Hotfix #120 的 18 个脚本分裂**:`part1 → part2 → diag → verify → sub → tail → view` 中,大量 "查看日志" 脚本 (diag/log/tail/view) 的唯一目的是在终端执行 `cat`/`tail`/`curl`,然后**人工查看截图**确认结果。如果能结构化捕获输出,这些脚本可以合并为带条件判断的单一流程。
|
||
|
||
2. **无法自动判断部署成功与否**:`run_v7_v5.py` 的 `proc.returncode` 只能判断 Playwright 进程是否正常退出,无法判断远程服务器上的 `docker restart` 是否成功、`curl` 是否返回预期响应。
|
||
|
||
3. **无退出码捕获**:执行 `docker ps && echo $?` 的结果在 canvas 里,脚本只能"相信"它成功了。
|
||
|
||
4. **日志无法结构化存储**:每次部署的输出是截图(PNG),而非文本。无法 grep、无法 diff、无法自动对比两次部署的差异。
|
||
|
||
5. **截图驱动的不稳定性**:
|
||
- 命令输出超过一屏时,截图只看到最后一段
|
||
- 颜色/样式变化可能被误读为错误
|
||
- 网络延迟导致截图时机窗口不可控
|
||
|
||
#### 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:
|
||
|
||
```python
|
||
# 方案 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 集成 | 不可行 | 可行(输出可解析) |
|
||
|
||
**实施步骤**:
|
||
|
||
1. **探测阶段**:写一个 `probe_xterm_buffer.py`,在连上终端后执行 `page.evaluate()`,尝试多种方式找到 xterm 实例引用
|
||
2. **验证阶段**:对比 `capture_terminal_output()` 的输出和截图内容是否一致
|
||
3. **集成阶段**:修改 `send_terminal_command()` 为 `send_and_capture()`,返回结构化文本
|
||
4. **高级阶段**:利用 buffer 实现增量读取(只读新增行),避免重复传输全部内容
|
||
|
||
**风险**:JumpServer Luna 可能对 xterm 实例做了封装/私有化。需要先探测具体引用路径。
|
||
|
||
#### 3.2 命令执行结果自动判定
|
||
|
||
依赖 3.1 的输出,实现:
|
||
```python
|
||
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 流程抽象为通用引擎:
|
||
|
||
```python
|
||
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 输出无法结构化捕获**,导致:
|
||
|
||
1. 所有验证依赖截图(不可解析)
|
||
2. 部署成功/失败无法自动判定
|
||
3. 脚本过多(单个 hotfix 分裂为 18 个)
|
||
4. 无法集成 CI/CD
|
||
|
||
**建议立即启动 xterm.js buffer 探测**(估计 1-2 小时),确认 JumpServer Luna 中 xterm 实例的引用路径后,一周内完成 `send_and_capture()` 改造。这将使自动化覆盖率从当前的 ~40% 提升到 ~90%。
|