提交 fd2c2f4d authored 作者: 陈泽健's avatar 陈泽健

fix(executor): 修复容器内 HeadlessChrome UA 导致登录失败——伪装 UA 隐藏自动化特征

- playwright_executor.py: 启动参数加 --disable-blink-features=AutomationControlled / --disable-automation
- new_context() 伪装为正常桌面 Chrome UA、locale=zh-CN、timezone=Asia/Shanghai
- add_init_script 隐藏 navigator.webdriver(双保险)
- 容器内验证:do_login() 返回 True,2 步用例 11.81s 执行通过 
- HANDOFF.md: 同步更新 P1 已解决状态
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 4cff19b6
# HANDOFF — 平台自动化测试系统交接文档(V2 部署架构升级) # HANDOFF — 平台自动化测试系统交接文档(V2 部署架构升级)
> **生成时间**: 2026-07-28 > **生成时间**: 2026-07-28(最后一次更新:**P1 卡点已解决** ✅ — UA 伪装修复 do_login 成功,端到端用例执行通过)
> **当前分支**: `platform-auto-test` > **当前分支**: `platform-auto-test`
> **最近提交**: `c3417d53` docs(deploy): 新增容器化部署Volume挂载优化需求文档和执行计划文档 > **最近提交**: `4cff19b6` feat(deploy): V2部署架构升级 — 合一容器+MySQL(已推送 origin)
> **状态**: ✅ V2 部署架构升级完成并上线(app + mysql 双容器运行中,数据迁移完成,未提交 git) > **状态**: ✅ V2 部署已上线 + 代码已提交;✅ **P1 卡点已解决**:根因确认为 **HeadlessChrome UA 被被测站点区别对待**(容器内 `input:visible=0`),通过伪装 UA + 隐藏自动化特征修复,`do_login()` 返回 True,2 步用例 11.81s 执行通过。修复已部署到服务器容器,**待 commit 提交**。详见第四节 4.2
--- ---
...@@ -79,6 +79,13 @@ ...@@ -79,6 +79,13 @@
4. **清理旧部署** 4. **清理旧部署**
- 已删除服务器旧目录 `~/ubains-module-test/deploy/` - 已删除服务器旧目录 `~/ubains-module-test/deploy/`
### P1 卡点修复 — HeadlessChrome UA 伪装 ✅(本次会话新增)
- **根因**:容器内 Linux headless Chromium 的 UA 含 `HeadlessChrome`,被测站点据此返回不含登录表单的页面(`input:visible=0`),导致 `do_login()` 失败。
- **修复**`playwright_executor.py``start()` 中新增:伪装 UA(正常桌面 Chrome)、隐藏 `navigator.webdriver`、禁用 Blink 自动化特征标记、设置 locale/时区。
- **验证**:容器内 `do_login()` 返回 True,2 步用例 11.81s 执行通过。
- **状态**:已 scp 到服务器重启生效,**待 git commit 提交本地代码**
### 最终验证结果(全部通过) ### 最终验证结果(全部通过)
| 验收项 | 结果 | | 验收项 | 结果 |
...@@ -123,33 +130,67 @@ ...@@ -123,33 +130,67 @@
## 四、卡点和问题 ## 四、卡点和问题
### 4.1 ⚠️ 本次所有变更尚未 git commit(最紧急 ### 4.1 🟡 git commit 待办(P1 修复未提交
V2 部署的代码改造(7 个 model + database.py + main.py + 部署配置 + 迁移脚本)**全部未提交**。下次会话首要任务是用 `/GitCommit` 提交。未提交文件清单见第六节 V2 部署变更已提交(commit `4cff19b6`)。但 **P1 修复(`playwright_executor.py` UA 伪装)尚未 commit**,目前只在服务器容器内生效(已 scp + restart)。下次首要:用 `/GitCommit` skill 提交此修复并推送。详见第六节「待提交变更」
### 4.2 新 MySQL 环境的批量执行验证未做 ### 4.2 ✅ P1 卡点已解决(2026-07-28)— HeadlessChrome UA 伪装
部署和数据迁移已验证,但**未在新环境批量执行 UI 用例**验证 Playwright 在容器内运行 + MySQL 并发写入无 `database is locked` **现象(修复前)**:V2 容器内创建 UI 用例执行后,执行记录永远停在 `status=running``passed/failed=0`,case_results 全 `pending`;最简单 2 步用例跑 180s 无结果;容器内出现僵尸 chrome 进程(State=Z),需 `docker compose restart app` 恢复
### 4.3 前端热更新未实测 **根因(已确认)****HeadlessChrome UA 被被测站点区别对待**。容器内 Linux headless Chromium 的 User-Agent 含 `HeadlessChrome` 字样,被测站点(192.168.5.44)据此返回了**不含登录表单**的页面 —— 现场实测 `input:visible=0``button:visible=0`,导致 `do_login()` 填表/点登录全部失败,返回 False,后续步骤每个都超时累积卡死。本地 Windows 因 UA 不同所以无法复现。
**修复(`backend/app/executors/playwright_executor.py` 的 `start()` 方法)**
1. **启动参数新增**`--disable-blink-features=AutomationControlled``--disable-automation`(隐藏 `navigator.webdriver`)、`--disable-dev-shm-usage``--disable-gpu`(容器兼容)。
2. **`new_context()` 新增**`user_agent=`(伪装为正常桌面 Chrome `Chrome/126.0.0.0`,去掉 HeadlessChrome)、`locale="zh-CN"``timezone_id="Asia/Shanghai"`
3. **`add_init_script`**`Object.defineProperty(navigator, 'webdriver', {get: () => undefined})`(双保险隐藏 webdriver)。
**验证结果(容器内现场实测,修复前 → 修复后)**
| 检查项 | 修复前 | 修复后 |
|--------|--------|--------|
| `navigator.userAgent` | 含 `HeadlessChrome` | 正常 Chrome UA ✅ |
| `navigator.webdriver` | `true` | `undefined` ✅ |
| `input:visible` | **0** | **3** ✅ |
| `button:visible` | **0** | **1** ✅ |
| `do_login()` 返回 | `False` | **`True`** ✅ |
| 登录后 URL | 停在登录页 | `/#/platform?...Home` ✅ |
**端到端验证**:在容器内用修复后的执行器完整跑「新建会议页面访问验证」用例(2 步:auto_login → 点击 `.block:has-text("新建会议")` → 等待 body),结果 **`status=passed`,总用时 11.81s**(修复前 180s 无结果)。**P1 彻底解决。**
**排查过程时间线**
1. 前两次会话确认 do_login 在容器失败、本地成功 → 排除选择器/站点改版,根因收窄到容器环境差异,怀疑 HeadlessChrome UA。
2. 本次会话:直接修改执行器 `start()` 加 UA 伪装 + 隐藏自动化特征,scp 上传 + restart app,容器内跑 `debug_ua_fix.py`(验证 do_login)和 `debug_ui_exec.py`(验证完整用例执行),两者均通过。
热更新机制已实现(volume 挂载 dist/),但未实际「改前端→build→上传→刷新」验证。 **遗留小问题**:登录后立即查 `.block` 数量=0(本地是 29),疑似主页卡片异步加载,需稍等。但 do_login 的成功判断基于 URL + 登录表单消失,不受影响;后续真实步骤(如点击 `.block:has-text`)有 `wait_for_element` 容错,实测能通过。可后续观察,不影响 P1 解决结论。
**复用资源(本次新增)**
- `backend/scripts/debug_ua_fix.py` — 容器内验证 do_login + UA 伪装是否生效(打印 navigator.userAgent/webdriver、input:visible 数量)。
- `backend/scripts/debug_ui_exec.py` — 容器内完整执行一个 2 步用例,端到端验证。
--- ---
## 五、下一步计划 <details>
<summary>📂 历史排查记录(折叠备查,P1 已解决,以下为排查过程)</summary>
**已验证排除的因素**(不是这些原因):
- ❌ Playwright 容器内能否跑:✅ 能。手动 `sync_playwright` 脚本可正常启动 chromium、导航 `https://192.168.5.44`,1.9s 完成。
- ❌ SSL 证书问题:✅ 执行器 `new_context(ignore_https_errors=True)` 正确。
- ❌ MySQL 连接:✅ 连接字符串正确,建执行记录、查用例数据都正常。
- ❌ 执行器 `start()`:✅ 容器内 `executor.start()` 成功。
- ❌ MySQL 并发锁:本次只跑 1 个用例仍卡死,与并发无关。
**本地诊断结论(关键转折)**:本地 Windows headless=True/False 均 `input:visible=3`、do_login 返回 True,排除选择器不匹配/站点改版,根因收窄到容器环境差异,最终确认为 HeadlessChrome UA。
</details>
| 优先级 | 待办 | 说明 | ### 4.3 前端热更新未实测
|--------|------|------|
| **P0** | **git commit 提交本次变更** | model 字段改造 + 部署配置 + 迁移脚本,用 `/GitCommit` skill | 热更新机制已实现(volume 挂载 dist/),但未实际「改前端→build→上传→刷新」验证。**未遇到问题,仅未验证**,优先级低于 4.2。
| **P1** | 新环境批量执行 UI 用例 | 验证 Playwright 容器内运行 + MySQL 无并发锁 |
| **P1** | 实测前端热更新 | 改前端→build→上传→刷新验证 |
| **P2** | HTTPS 配置 | 为前端配置 Let's Encrypt 证书 |
| **P3** | 编写自动化更新脚本 | 简化后续前端/后端更新操作 |
--- ---
## 六、未提交变更(待 git commit ## 六、本次变更清单(✅ 已提交 commit `4cff19b6`
### 修改的文件 ### 修改的文件
...@@ -189,6 +230,16 @@ V2 部署的代码改造(7 个 model + database.py + main.py + 部署配置 + ...@@ -189,6 +230,16 @@ V2 部署的代码改造(7 个 model + database.py + main.py + 部署配置 +
| `deploy/Dockerfile.backend` | 旧后端 Dockerfile(已合并到合一 Dockerfile) | | `deploy/Dockerfile.backend` | 旧后端 Dockerfile(已合并到合一 Dockerfile) |
| `deploy/Dockerfile.frontend` | 旧前端 Dockerfile(已合并) | | `deploy/Dockerfile.frontend` | 旧前端 Dockerfile(已合并) |
### P1 排查可复用的现场资源
| 资源 | 位置 | 用途 |
|------|------|------|
| `debug2.py` | 服务器 `/tmp/debug2.py` + 容器 `/app/debug2.py` | 硬编码用例测执行器(绕过 DB),确认 do_login 失败 |
| `debug3.py` | 服务器 `/tmp/debug3.py` + 容器 `/app/debug3.py` | 直接操作页面,打印登录页 input/button 数量与结构 |
| 登录页截图 | 容器 `/tmp/debug_login.png``/tmp/debug_login_after.png` | 用 `docker cp plat-auto-test-app:/tmp/debug_login.png ./` 拉取查看 |
| 现场测试用例 ID | `case_d4f98d82418b4b22a176da03922fbd66` | 最简单的 2 步 UI 用例(验证-登录测试) |
| 关键代码位置 | `backend/app/executors/playwright_executor.py:222``do_login`) | 登录逻辑,选择器待核对 |
--- ---
## 七、关键踩坑记录(绝对不要重复踩) ## 七、关键踩坑记录(绝对不要重复踩)
......
...@@ -194,20 +194,48 @@ class PlaywrightExecutor: ...@@ -194,20 +194,48 @@ class PlaywrightExecutor:
self._playwright = sync_playwright().start() self._playwright = sync_playwright().start()
# 启动浏览器 # 启动浏览器
# 关键修复(P1 卡点):容器内 Linux headless Chromium 的 UA 含 "HeadlessChrome",
# 被测站点会据此返回不同页面/拦截登录表单渲染(导致 input:visible=0)。
# 解决:隐藏自动化特征 + 伪装成正常桌面 Chrome。
self._browser = self._playwright.chromium.launch( self._browser = self._playwright.chromium.launch(
headless=self.headless, headless=self.headless,
args=[ args=[
"--no-sandbox", "--no-sandbox",
"--disable-setuid-sandbox", "--disable-setuid-sandbox",
# 隐藏自动化特征,避免 navigator.webdriver=true 被站点检测
"--disable-blink-features=AutomationControlled",
"--disable-automation",
# 容器环境兼容(共享内存不足会导致渲染异常/崩溃)
"--disable-dev-shm-usage",
"--disable-gpu",
], ],
) )
# 创建浏览器上下文(忽略 HTTPS 证书错误) # 创建浏览器上下文(忽略 HTTPS 证书错误 + 伪装 UA/locale)
# 伪装成正常桌面 Chrome 的 User-Agent(去掉 HeadlessChrome 标识),
# 统一设为 zh-CN locale 与 Asia/Shanghai 时区,避免容器默认 locale
# 触发被测站点的不同分支。
fake_ua = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36"
)
self._context = self._browser.new_context( self._context = self._browser.new_context(
ignore_https_errors=self.ignore_https_errors, ignore_https_errors=self.ignore_https_errors,
viewport={"width": 1920, "height": 1080}, viewport={"width": 1920, "height": 1080},
user_agent=fake_ua,
locale="zh-CN",
timezone_id="Asia/Shanghai",
) )
# 进一步隐藏 webdriver 痕迹(部分站点通过 navigator.webdriver 检测)
try:
self._context.add_init_script(
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
)
except Exception as e:
logger.debug(f"add_init_script 失败(忽略): {e}")
# 创建页面 # 创建页面
self._page = self._context.new_page() self._page = self._context.new_page()
self._page.set_default_timeout(self.timeout) self._page.set_default_timeout(self.timeout)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论