chore: 整理项目结构,清理归档文件,更新部署配置
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
# JP-webcli 自动化部署工具 — 能力·限制·优化分析
|
||||
|
||||
> 分析日期:2026-06-25 | 基于 webcli_v7(Playwright 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`
|
||||
|
||||
**运行方式:**
|
||||
```powershell
|
||||
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 输出捕获核心代码(预览)
|
||||
|
||||
```python
|
||||
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()` 之后、终端加载之前注入拦截代码:
|
||||
|
||||
```python
|
||||
# 在页面加载时注入 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 浏览器):
|
||||
```powershell
|
||||
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 = null` 或 `screen_textcontent` → 需要 Monkey Patch 方案
|
||||
|
||||
4. **实现 webcli_v8.py**:基于探测结果选择最佳捕获策略
|
||||
@@ -0,0 +1,166 @@
|
||||
# JumpServer 自动化演进日志
|
||||
|
||||
## 概述
|
||||
|
||||
从 2026-06-21 到 2026-06-25,JumpServer 自动化经历了 **5 种技术路线、17+ 个主要版本** 的迭代,最终选择了 Playwright Web CLI 方案作为生产方案。
|
||||
|
||||
## 时间线
|
||||
|
||||
```
|
||||
2026-06-21 ─ 06-22 │ 06-22 ─ 06-23 │ 06-23 ─ 06-25
|
||||
SSH 路线探索 │ Web CLI 路线验证 │ 生产化 + 部署实战
|
||||
│ │
|
||||
• plink.exe (v8) │ • webcli_probe │ • webcli_v4-v7 迭代
|
||||
• Paramiko (v1-v7) │ • webcli_login_v1 │ • SFTP 文件上传
|
||||
• wexpect (v11) │ • webcli_inspect │ • 分块 base64 传输
|
||||
• API auth 探测 │ • webcli_v2-v3 │ • hotfix 实战部署
|
||||
│ │ • SSH Key 方案探索
|
||||
```
|
||||
|
||||
## 各路线详细记录
|
||||
|
||||
### 路线 1: plink.exe (PuTTY CLI) — 已废弃
|
||||
|
||||
| 版本 | 文件 | 发现 |
|
||||
|------|------|------|
|
||||
| v8 | `test_plink_v8.py` | plink 可以连接但输出捕获不稳定 |
|
||||
| - | `test_plink_formats.py` | MFA 提示格式有 3 种变体 |
|
||||
| - | `test_plink_verbose.py` | 详细日志模式可看到完整交互 |
|
||||
|
||||
**废弃原因**:中文编码问题、交互式菜单导航不可靠、plink 不支持 PTY 分配。
|
||||
|
||||
### 路线 2: Paramiko (Python SSH) — 已废弃
|
||||
|
||||
| 版本 | 文件 | 关键发现 |
|
||||
|------|------|---------|
|
||||
| v1-v5 | `test_paramiko_v1.py` ~ `v5.py` | 基本连接可行,但 OTP 流程脆弱 |
|
||||
| v6-v7 | `test_paramiko_v6.py` ~ `v7.py` | 交互式 shell 模式可绕过部分菜单 |
|
||||
| v8 | `jumpserver_paramiko_v8.py` | 尝试 channel.recv 非阻塞读取 |
|
||||
| v9 | `jumpserver_paramiko_v9.py` | 尝试 expect 模式(timeout 可控) |
|
||||
| v12 | `jumpserver_paramiko_v12.py` | SSH Gateway 方式(JumpServer → 目标) |
|
||||
| v13 | `jumpserver_paramiko_v13.py` | PTY 分配尝试 |
|
||||
| v14-v16 | `jumpserver_paramiko_v14.py` ~ `v16.py` | 多线程读写分离 |
|
||||
|
||||
**废弃原因**:JumpServer 堡垒机菜单变化频繁(每次登录可能不同),Paramiko 的 expect 匹配无法适应。OTP 二次认证流程极其脆弱。
|
||||
|
||||
### 路线 3: wexpect (Windows expect) — 已废弃
|
||||
|
||||
| 版本 | 文件 | 发现 |
|
||||
|------|------|------|
|
||||
| v11 | `jumpserver_wexpect_v11.py` | 可以 spawn plink 进程并交互 |
|
||||
| v2 | `jumpserver_wexpect_v2.py` | 中文输出匹配不稳定 |
|
||||
| v3 | `jumpserver_wexpect_v3.py` | timeout 设置和重试逻辑优化 |
|
||||
|
||||
**废弃原因**:wexpect 在 Windows 上不稳定,中文编码问题难以解决,且与 plink 有相同的基础问题。
|
||||
|
||||
### 路线 4: Web CLI (Playwright) — ✅ 当前生产方案
|
||||
|
||||
| 版本 | 文件 | 关键里程碑 |
|
||||
|------|------|-----------|
|
||||
| probe | `jumpserver_webcli_probe.py` | 成功探测 Luna 终端结构 |
|
||||
| login | `jumpserver_webcli_login_v1.py` | 实现自动登录(密码+OTP) |
|
||||
| inspect | `jumpserver_webcli_inspect.py` | 分析 Web 终端的 DOM 结构 |
|
||||
| v2 | `jumpserver_webcli_v2.py` | 基本命令执行 |
|
||||
| v3 | `jumpserver_webcli_v3.py` | 增加截图和输出保存 |
|
||||
| v4 | `jumpserver_webcli_v4.py` | 文件上传探索 |
|
||||
| v5 | `jumpserver_webcli_v5.py` | SFTP 界面自动化 |
|
||||
| v6 | `jumpserver_webcli_v6.py` | 超时控制 + 错误重试 |
|
||||
| **v7** | **`jumpserver_webcli_v7.py`** | **生产版本**:完整部署流程 |
|
||||
|
||||
**v7 核心能力**:
|
||||
- 自动登录 JumpServer(用户名 + 密码 + OTP)
|
||||
- 导航到目标服务器(税友集团 → hz-oa-ai-g-dataquery-90-5-110)
|
||||
- 在 Web 终端中执行任意命令
|
||||
- 截图保存(用于验证)
|
||||
- 输出日志保存
|
||||
- SFTP 文件上传(set_input_files 方式)
|
||||
|
||||
**关键突破**:发现 `OTP 双回车`问题 — 发送验证码后需按 2 次 Enter 触发验证流程。
|
||||
|
||||
### 路线 5: SSH Key 认证 — 开发中
|
||||
|
||||
| 文件 | 状态 |
|
||||
|------|------|
|
||||
| `generate_ssh_keys.py` | 生成 RSA 密钥对 |
|
||||
| `jumpserver_auto_v17_key_auth.py` | 尝试用密钥认证连接 |
|
||||
| `ssh_keys/jumpserver_id_rsa` | 已生成的私钥 |
|
||||
|
||||
**目标**:绕过 OTP 流程,实现完全无人值守的自动化。
|
||||
|
||||
### 路线 6: JumpServer API Token — 探索中
|
||||
|
||||
| 文件 | 发现 |
|
||||
|------|------|
|
||||
| `create_connection_token.py` | 可通过 API 创建连接 Token |
|
||||
| `test_connection_token.py` | Token 可用于 SSH 连接 |
|
||||
| `probe_koko_token.py` | Koko(Web 终端)的 Token 格式不同 |
|
||||
| `probe_terminal_api.py` | 终端 API 端点探测 |
|
||||
|
||||
### 2026-06-25 (下午): xterm.js 输出捕获分析 + probe_v2
|
||||
|
||||
- **创建 `probe_xterm_buffer_v2.py`**:5 种方法探测 xterm.js 缓冲区
|
||||
- 全局 Terminal 实例搜索
|
||||
- DOM .xterm-rows 遍历
|
||||
- helper-textarea 读取
|
||||
- 渲染器类型检测(DOM vs Canvas)
|
||||
- React Fiber 组件树搜索
|
||||
- **创建 `ANALYSIS.md`**:完整的能力-限制-优化分析
|
||||
- 现有能力矩阵 + 已验证部署记录
|
||||
- 输出捕获不可靠问题分析(3 种失败模式)
|
||||
- v8 结构化捕获设计 + Monkey Patch 兜底方案
|
||||
- 不适用的 6 种场景
|
||||
- 短/中/长期优化路线图
|
||||
- **发现**:v7 的 `inner_html()` 输出捕获依赖 DOM 渲染器,Canvas 渲染器下失效
|
||||
|
||||
### 下一步
|
||||
- [ ] 运行 probe_xterm_buffer_v2.py 确认 xterm.js 实例暴露方式
|
||||
- [ ] 根据探测结果实现 webcli_v8.py(结构化缓冲区读取)
|
||||
|
||||
### 2026-06-25 (晚间): v10.16 OTP 等待时间修复
|
||||
|
||||
**问题**:用户反馈 `ensure_logged_in()` 中 OTP 提交后 `wait_for_timeout(5000)` 只有 5 秒,JumpServer 服务端 OTP 校验通常需要 8-15 秒,导致过早判定超时并进入 URL 跳转检测,进而误判登录失败。
|
||||
|
||||
**修复**:
|
||||
1. **MFA 页面消失轮询**:替代固定 5s 等待,改为 500ms × 40 次轮询(max 20s),检测 `input[name="code"]` 消失
|
||||
2. **OTP 错误检测**:轮询中检测页面正文是否含 "验证码错误/OTP 错误/已过期/expired/Invalid" 关键字
|
||||
3. **URL 跳转延长**:workbench URL 检测从 15×1s=15s 延长到 60×500ms=30s
|
||||
4. **超时截图**:30s 未跳转时自动截图留存(`v10_post_otp_stuck.png`)
|
||||
5. **代码安全**:截图前确保 `OUTPUT_DIR` 存在
|
||||
|
||||
**影响范围**:`jumpserver_webcli_v10_persistent.py` — `ensure_logged_in()` 函数 (lines 209-245)
|
||||
|
||||
**副作用**:正常流程下 MFA 页面通常在 3-5s 消失,轮询会在第 6-10 次检查(3-5s)时返回,不影响正常速度。仅在网络慢/服务器高负载时才会触发更长的等待。
|
||||
|
||||
## 成功部署记录
|
||||
|
||||
### Hotfix #116: 扫码获取 (2026-06-22)
|
||||
- 使用 webcli v2-v4
|
||||
- 部署 `auth_qrcode.py`
|
||||
- 5 个部署诊断迭代(diag_v7 ~ diag7_v7)
|
||||
|
||||
### Hotfix #120: 扫码自动确认 (2026-06-23)
|
||||
- 使用 webcli v5 + v7
|
||||
- 部署 `qrcode_service.py`(29KB 源码)
|
||||
- **首次使用 SFTP 分块上传**(8 chunks)
|
||||
- 18 个 webcli 子脚本(part1/2、diag、verify、sub、tail、view)
|
||||
- 从 Part A SFTP 到 Part B 部署到 Part C 验证
|
||||
|
||||
### Hotfix #48: Nginx Upstream (2026-06-24)
|
||||
- 使用 webcli v7
|
||||
- 修复 nginx upstream 配置
|
||||
- 包含 HTTPS/SSL 验证
|
||||
|
||||
### Hotfix SSO: 单点登录启用 (2026-06-25)
|
||||
- 使用 webcli v7
|
||||
- 部署 config.py + 环境变量注入
|
||||
|
||||
## 记录在案的错误与解决方案
|
||||
|
||||
| 错误 | 根因 | 解决方案 | 记录 |
|
||||
|------|------|---------|------|
|
||||
| OTP 验证失败 | 发送验证码后未按2次回车 | `test_double_enter.py` 确认需要 `\r\n` | 代码中固定加 `\n` |
|
||||
| Web CLI 命令输入截断 | 输入框字符限制 ~20KB | 分2批次输入(16KB + 12.8KB) | `upload_chunks_sftp.py` |
|
||||
| Docker cp 路径错误 | 容器内路径与宿主机不同 | 使用 `docker exec` 确认后操作 | webcli_v5_part2.sh |
|
||||
| 容器重启后立即 curl 失败 | 服务启动需要时间 | 加 `sleep 8` 等待 | webcli_v5_cmd.sh |
|
||||
| Luna 终端编码乱码 | .sh 文件非 UTF-8 | 使用 `webcli_v3_cmd.utf8.sh` | webcli_v3 |
|
||||
| SFTP 文件选择器不弹 | webkitdirectory 属性过滤 | v9.8 修复 set_input_files 跳过验证 | upload_chunks_sftp.py |
|
||||
@@ -0,0 +1,192 @@
|
||||
# JumpServer Web CLI 自动化部署工具集
|
||||
|
||||
## 概述
|
||||
|
||||
通过 JumpServer Luna Web 终端的浏览器自动化(Playwright),实现在目标服务器上自动执行命令行操作、文件上传下载等自动化部署、测试、维护能力。
|
||||
|
||||
## 核心架构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 本地 (Windows/PowerShell) │
|
||||
│ ┌─────────────────┐ ┌──────────────────────────────────┐ │
|
||||
│ │ gen-deploy-*.py │──▶│ 生成 base64 编码的命令 + 载荷 │ │
|
||||
│ │ gen-single-shot │ │ 输出到 webcli_*.sh │ │
|
||||
│ └─────────────────┘ └──────────┬───────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────────────▼───────────────────────┐ │
|
||||
│ │ jumpserver_webcli_v7.py │ │
|
||||
│ │ Playwright 浏览器自动化 │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ 1. 打开 JumpServer URL │ │ │
|
||||
│ │ │ 2. 自动登录 (密码 + OTP) │ │ │
|
||||
│ │ │ 3. 导航到目标资产 (10.90.5.110) │ │ │
|
||||
│ │ │ 4. 在 Web 终端中执行命令 │ │ │
|
||||
│ │ │ 5. 截图 + 保存输出 │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └───────────────────────┬───────────────────────────────┘ │
|
||||
└──────────────────────────┼──────────────────────────────────┘
|
||||
│ HTTPS
|
||||
┌──────────────────────────▼──────────────────────────────────┐
|
||||
│ JumpServer (jumpserver.dc.servyou-it.com) │
|
||||
│ Luna Web Terminal ──▶ 税友集团 ──▶ hz-oa-ai-g-dataquery │
|
||||
│ │ │
|
||||
│ 10.90.5.110 │
|
||||
│ ┌──────────┐ │
|
||||
│ │ Docker │ │
|
||||
│ │ wecom_it │ │
|
||||
│ │ _backend │ │
|
||||
│ └──────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 核心文件清单
|
||||
|
||||
### 本地命令生成器(项目内)
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `deploy-staging/gen-deploy-commands.py` | 生成分步部署命令(Part A/B) |
|
||||
| `deploy-staging/gen-single-shot.py` | 生成单次执行命令 |
|
||||
| `deploy-staging/gen-deploy-commands-bash.py` | Bash 版本生成器 |
|
||||
| `deploy-staging/hotfix-*/webcli_*.sh` | 生成的 Web CLI 命令脚本 |
|
||||
| `deploy-staging/hotfix-*/run_v7_*.py` | Playwright 自动化编排脚本 |
|
||||
| `deploy-staging/hotfix-120-qrcode-auto-confirm/upload_chunks_sftp.py` | SFTP 分块上传 |
|
||||
|
||||
### Web CLI 自动化引擎(用户技能目录)
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `~/.workbuddy/skills/jumpserver-automation/scripts/jumpserver_webcli_v7.py` | **当前主力**:Playwright Web CLI 自动化 |
|
||||
| `~/.workbuddy/skills/jumpserver-automation/scripts/probe_xterm_buffer_v2.py` | **xterm.js 缓冲区探测**:5 种方法测试结构化输出捕获 |
|
||||
| `~/.workbuddy/skills/jumpserver-automation/scripts/jumpserver_webcli_v6.py` | v6 版本(支持超时等待) |
|
||||
| `~/.workbuddy/skills/jumpserver-automation/scripts/jumpserver_webcli_v5.py` | v5 版本 |
|
||||
| `~/.workbuddy/skills/jumpserver-automation/scripts/jumpserver_webcli_probe.py` | 初始探测脚本 |
|
||||
| `~/.workbuddy/skills/jumpserver-automation/scripts/jumpserver_config.py` | 跳板机配置 |
|
||||
|
||||
### 部署命令示例(各 hotfix 目录)
|
||||
| 目录 | 用途 |
|
||||
|------|------|
|
||||
| `deploy-staging/hotfix-116-qrcode-scan-get/` | 扫码获取热修复(v1-v4 webcli) |
|
||||
| `deploy-staging/hotfix-120-qrcode-auto-confirm/` | 扫码自动确认热修复(v5 webcli,含 SFTP) |
|
||||
| `deploy-staging/hotfix-48-nginx-upstream/` | Nginx upstream 修复 |
|
||||
| `deploy-staging/hotfix-sso-enabled/` | SSO 启用热修复 |
|
||||
|
||||
## 技术演进历史
|
||||
|
||||
### 演进路线
|
||||
|
||||
```
|
||||
v1-v6: SSH/plink 直接连接
|
||||
├─ v1-v7: Paramiko SSH 库 (Python)
|
||||
│ └─ 问题:堡垒机菜单导航不可靠,认证流程复杂
|
||||
├─ v8-v10: plink.exe (PuTTY CLI)
|
||||
│ └─ 问题:输出捕获不稳定,中文编码问题
|
||||
├─ v11-v16: wexpect (Windows expect)
|
||||
│ └─ 问题:交互式菜单匹配不稳定,超时控制困难
|
||||
└─ 所有 SSH 方法共同问题:
|
||||
├─ 双因素认证(密码+OTP)流程脆弱
|
||||
├─ 堡垒机菜单导航需要精确的 expect 匹配
|
||||
└─ 网络不稳定时重连复杂
|
||||
|
||||
v7+: Web CLI (Playwright 浏览器自动化) ✅ 当前方案
|
||||
├─ webcli_probe: 探测 JumpServer Luna 终端的 Web 结构
|
||||
├─ webcli_v1-v3: 基本登录 + 命令执行
|
||||
├─ webcli_v4-v5: 增加 OTP 自动填充、截图验证
|
||||
├─ webcli_v6: 增加超时等待、错误重试
|
||||
└─ webcli_v7: 当前生产版本
|
||||
优势:
|
||||
├─ 绕过 SSH 认证复杂性,直接操作浏览器
|
||||
├─ 可视化确认执行结果(截图)
|
||||
├─ 支持长命令输入(>10KB base64 载荷)
|
||||
├─ SFTP 文件上传(set_input_files)
|
||||
└─ 稳定的 OTP 自动填充
|
||||
|
||||
v17 (开发中): SSH Key 认证
|
||||
└─ generate_ssh_keys.py + jumpserver_auto_v17_key_auth.py
|
||||
目标:完全自动化,无需浏览器
|
||||
状态:概念验证阶段
|
||||
|
||||
API 方式(探索中)
|
||||
└─ create_connection_token.py, test_connection_token.py
|
||||
目标:通过 JumpServer API 获取 WebSocket token
|
||||
状态:API 可用但 Koko 终端协议未完全理解
|
||||
```
|
||||
|
||||
### 关键经验教训
|
||||
|
||||
1. **不要走 SSH 直接连接路径**:JumpServer 的堡垒机菜单 + OTP 双重认证使得 SSH 自动化极其脆弱。Web CLI 通过浏览器绕过这些问题。
|
||||
|
||||
2. **base64 编码大文件传输**:单次 Web CLI 输入约 10-15KB 安全上限。超过需分块(chunk_a 到 chunk_h),每块 ~3-4KB base64。
|
||||
|
||||
3. **分阶段执行**:部署应分 Part 1(文件传输)+ Part 2(部署执行)+ Part 3(验证),每阶段独立运行,便于定位问题。
|
||||
|
||||
4. **OTP 双回车问题**:发送 OTP 验证码后,某些情况下需要按 2 次回车才能触发验证流程(`test_double_enter.py` 记录了此发现)。
|
||||
|
||||
5. **编码一致性**:所有 .sh 文件必须 UTF-8 编码(`webcli_v3_cmd.utf8.sh`),否则 Web CLI 终端可能乱码。
|
||||
|
||||
6. **等待策略**:Docker 操作后必须 `sleep 8` 以上等待容器重启,否则 `docker ps` 和 curl 验证会失败。
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 标准部署流程
|
||||
|
||||
```powershell
|
||||
# 1. 生成部署命令
|
||||
cd deploy-staging
|
||||
python gen-deploy-commands.py # 生成 part_a.txt / part_b.txt
|
||||
|
||||
# 2. (可选) 生成 webcli shell 脚本
|
||||
python gen-single-shot.py # 生成 single_shot_cmd.txt
|
||||
|
||||
# 3. 通过 Playwright Web CLI 执行
|
||||
cd C:\Users\simon\.workbuddy\skills\jumpserver-automation\scripts
|
||||
python jumpserver_webcli_v7.py -c (Get-Content D:\...\webcli_v5_cmd.sh -Raw)
|
||||
```
|
||||
|
||||
### 文件上传流程(SFTP 模式)
|
||||
|
||||
```powershell
|
||||
# 使用 webcli_v7 的 SFTP 功能上传
|
||||
python upload_chunks_sftp.py
|
||||
# 自动将 chunk_a 到 chunk_h 上传到 /tmp/qrcode_v5/
|
||||
```
|
||||
|
||||
## 部署命令生成器说明
|
||||
|
||||
### gen-deploy-commands.py
|
||||
- **功能**:读取本地 shell 脚本,base64 编码后嵌入 Web CLI 命令
|
||||
- **输入**:`hotfix-qrcode-step5.sh` + `backend/app/config.py`
|
||||
- **输出**:`/tmp/deploy-step5/part_a.txt` + `part_b.txt`
|
||||
- **Part A**:写文件 + 执行部署脚本(前台等待结果)
|
||||
- **Part B**:查看执行日志 + curl 验证
|
||||
|
||||
### gen-single-shot.py
|
||||
- **功能**:将所有操作合并为单条命令(用 `;` 连接)
|
||||
- **优势**:一次粘贴,无需分步操作
|
||||
- **限制**:命令总长度受 Web CLI 输入框限制(~20KB)
|
||||
|
||||
## 任务清单(待办)
|
||||
|
||||
### P0 - 紧急
|
||||
- [x] xterm.js 输出捕获分析 + probe_v2 探测脚本(2026-06-25)
|
||||
- [ ] 运行 probe_xterm_buffer_v2.py 确认缓冲区访问方式
|
||||
- [ ] 实现 webcli_v8.py(结构化缓冲区读取,替代截图验证)
|
||||
- [ ] 将 jumpserver_webcli_v7.py 路径从硬编码改为项目相对路径
|
||||
- [ ] 创建 webcli 健康检查脚本(检测 JumpServer 页面是否可访问)
|
||||
|
||||
### P1 - 重要
|
||||
- [ ] 封装 deploy-webcli 为独立 Python 包(pip install)
|
||||
- [ ] 实现 Web CLI 命令队列(批量执行 + 结果收集)
|
||||
- [ ] 集成 OTP 自动获取到 webcli_v7(当前需手动获取)
|
||||
- [ ] 迁移 gen-deploy-commands.py 中的硬编码路径到项目根路径
|
||||
|
||||
### P2 - 改进
|
||||
- [ ] SSH Key 认证方案验证(v17)
|
||||
- [ ] JumpServer API Token 方式探索
|
||||
- [ ] Web CLI 执行结果自动解析(正则提取 exit code)
|
||||
- [ ] 告警集成:部署失败时企微通知
|
||||
|
||||
### 已知问题
|
||||
- [ ] `deploy-server/` 目录未迁移(含部署包和 docker-compose,>500MB)
|
||||
- [ ] plink.exe 依赖 Windows 环境(无法跨平台)
|
||||
- [ ] webcli_v7 需要 Chrome/Chromium 浏览器
|
||||
- [ ] Playwright 浏览器实例需要 ~500MB 磁盘空间
|
||||
Reference in New Issue
Block a user