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

313 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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%。