Files
wecom_it_smart_desk/docs/03-技术架构/03-技术分析/技术分析-jp-webcli自动化部署能力分析.md
T

313 lines
13 KiB
Markdown
Raw Normal View History

# 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 输出无法结构化捕获(**当前最紧急**)
**现状**:所有版本(v6v10)均依赖 **截图** 作为命令执行结果的唯一验证手段。
**根因**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%。