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

fix(executor): 修复FastAPI端点执行Playwright报asyncio检测错误

- playwright_executor.py: 在工作线程中设置 asyncio.set_event_loop(None) 绕过 Playwright 的 asyncio 事件循环检测
- execution_service.py: 优化 run_all_cases_sync 日志输出,清理调试 print 语句
- CLAUDE.md: 新增踩坑记录 #9,更新 Playwright 正确模式第 5 条
- HANDOFF.md: 更新状态为已修复,重构文档结构

根因:Playwright sync_playwright().start() 内部使用 asyncio.get_running_loop() 检测,
在 FastAPI run_in_executor 工作线程中需将事件循环设为 None 才能绕过检测
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 001a68a9
......@@ -110,6 +110,7 @@ Windows 上 Playwright 异步 API 在 FastAPI asyncio 循环中无法启动。**
2. `playwright_executor.py` 使用 `sync_playwright` 同步 API
3. `execution_service.py` 通过 `loop.run_in_executor()` 在线程池中执行
4. **每个测试用例创建独立的 `PlaywrightExecutor` 实例**(共享实例在 `stop()``_is_running=False`,下次 `start()` 会失败)
5. **`playwright_executor.py` 的 `start()` 方法中必须 `asyncio.set_event_loop(None)`**——Playwright 内部用 `asyncio.get_running_loop()` 检测 asyncio 循环,在工作线程中必须将事件循环设为 None 才能绕过检测
### 2. 前端 useRoute() 必须在 setup 顶层
......@@ -145,6 +146,7 @@ Windows 上 Playwright 异步 API 在 FastAPI asyncio 循环中无法启动。**
| 6 | `claude` 命令不在 PATH | Windows 未配全局 PATH | `which claude` 找路径,补 `.cmd` 后缀 |
| 7 | 前端页面空白 | `useRoute()` 在 computed 内调用 | 移到 setup 顶层 |
| 8 | `NOT NULL constraint failed: error_message` | 字段 nullable=False 但成功时为 None | 改为 nullable=True |
| 9 | Playwright Sync API inside asyncio loop | `run_in_executor` 线程中 Playwright 检测到 asyncio 循环 | `asyncio.set_event_loop(None)` 在工作线程中 |
详见 `HANDOFF.md` 第五节。
......
# HANDOFF — 会话交接文档
# HANDOFF — 平台自动化测试交接文档
> **生成时间**: 2026-07-21
> **生成时间**: 2026-07-21 21:20
> **当前分支**: `platform-auto-test`
> **最近提交**: `4b690a98` feat(cases): 新增21个会议管理测试用例及问题处理文档
> **状态**: ✅ 新增15个数据分析/运维/看板用例并验证通过,执行器新增iframe支持
> **最近提交**: `001a68a9` feat(security): 新增API接口安全测试模块
> **状态**: ✅ FastAPI 端点执行问题已修复
---
## ⚠️ 开始工作前必读
**多窗口并行开发冲突规避**:如需开多个 Claude Code 窗口同时开发,**必须先阅读** `Docs/多窗口并行开发指南.md`,了解端口隔离、数据库操作、Playwright 执行互斥等注意事项,避免数据冲突和开发干扰。
**多窗口并行开发冲突规避**:如需开多个 Claude Code 窗口同时开发,**必须先阅读** `Docs/多窗口并行开发指南.md`,了解:
- 端口隔离(8001/8002)
- 数据库操作错开
- Playwright 执行互斥(不可同时执行用例)
- Git 提交协作
**当前多窗口状态**:本项目有两个窗口同时在开发:
- **本窗口**:UI 自动化测试模块(会议管理、数据分析、运维管理、管理看板)
- **另一窗口**:安全测试模块(API安全测试、华为红线检查、漏洞回归等)
---
......@@ -34,17 +42,18 @@ platform-auto-test/
├── backend/app/
│ ├── main.py # FastAPI 入口(Windows ProactorEventLoop)
│ ├── executors/
│ │ ├── playwright_executor.py # 执行引擎(13种动作+11种断言,支持iframe)
│ │ ├── security_executor.py # 安全测试执行器(另一窗口新增
│ │ └── auth_helper.py # 认证辅助(另一窗口新增
│ │ ├── playwright_executor.py # UI测试执行引擎(13种动作+11种断言,支持iframe)
│ │ ├── security_executor.py # 安全测试执行器(另一窗口)
│ │ └── auth_helper.py # 认证辅助(另一窗口)
│ ├── models/
│ │ ├── execution.py # 执行记录模型(含vulnerability_results关联)
│ │ └── vulnerability_result.py # 漏洞测试结果模型(字段名已修正)
│ │ ├── execution.py # 执行记录模型
│ │ ├── vulnerability_result.py # 漏洞结果模型
│ │ └── security_config.py # 安全配置模型(另一窗口)
│ ├── services/execution_service.py # 执行调度
│ └── scripts/
│ ├── create_meeting_cases_v2.py # 会议用例创建脚本
│ ├── create_analysis_cases.py # 数据分析/运维/看板用例脚本
│ └── check_exec.py # 执行状态检查脚本
│ ├── create_deep_interaction_cases_v2.py # 深层用例创建脚本
│ ├── test_executor.py # 执行器测试脚本
│ └── test_executor_async.py # 异步执行器测试脚本
├── frontend/src/
│ ├── views/Cases.vue # 用例管理
│ ├── views/Execution.vue # 执行中心
......@@ -57,211 +66,127 @@ platform-auto-test/
---
## 二、本次任务(已完成)
## 二、本次任务
### 任务:补充数据分析/运维管理/管理看板模块的可执行测试用例
### 任务:补充深层交互测试用例并执行验证
**核心成果**:
1. ✅ 新增 15 个深层交互用例(数据分析8个、运维4个、看板3个)
2. ✅ 总用例数从 77 个增加到 **92 个**
3. ✅ 全部 15 个新增用例验证通过(经多轮修复)
4. ✅ 执行器新增 iframe 遍历支持(wait/fill/click 三方法)
5. ✅ 修复 vulnerability_result.py 字段名冲突(metadata→extra_data)
6. ✅ 修复 Execution 模型缺少 vulnerability_results 关联
7. ✅ 创建多窗口并行开发指南文档
**目标**: 补充数据分析/运维管理/管理看板模块的深层交互用例(筛选器、图表、表格等),并验证用例可执行。
---
## 三、已完成事项(本次会话)
### 3.1 用例创建 ✅
创建文件:`backend/scripts/create_analysis_cases.py`
**数据分析模块(8个)**
| 用例名称 | 优先级 | 最终状态 |
|----------|--------|----------|
| 会议统计-日期范围筛选验证 | high | ✅ passed |
| 会议统计-图表交互验证 | medium | ✅ passed |
| 使用数据-表格筛选验证 | high | ✅ passed |
| 故障数据-状态筛选验证 | medium | ✅ passed |
| 通知统计-类型切换验证 | medium | ✅ passed |
| 历史记录-WeLink搜索验证 | high | ✅ passed |
| 会议室统计-图表验证 | medium | ✅ passed |
| 预定数据-时间筛选验证 | high | ✅ passed |
**运维管理模块(4个)**
| 用例名称 | 优先级 | 最终状态 |
|----------|--------|----------|
| 巡检报表-日期筛选验证 | high | ✅ passed |
| 运维数据-设备筛选验证 | medium | ✅ passed |
| 运维统计-图表展示验证 | medium | ✅ passed |
| 巡检报表-导出功能验证 | medium | ✅ passed |
**管理看板模块(3个)**
| 用例名称 | 优先级 | 最终状态 |
|----------|--------|----------|
| 管理看板-数据刷新验证 | high | ✅ passed |
| 会议概览-统计卡片验证 | high | ✅ passed |
| 会议室概览-图表交互验证 | medium | ✅ passed |
## 三、已完成事项
### 3.2 执行器 iframe 支持修复 ✅
### 3.1 用例创建
修改文件:`backend/app/executors/playwright_executor.py`
1. ✅ 创建了 25 个深层交互用例脚本 `create_deep_interaction_cases_v2.py`
2. ✅ 用例成功写入数据库(数据库总用例数:141个)
- 管理看板:13个(+6)
- 运维管理:15个(+7)
- 数据分析:34个(+12)
- `_wait_for_element()`:新增 iframe 遍历逻辑
- `_do_wait()`:新增 iframe 遍历逻辑
- `_do_fill()`:新增 iframe 遍历逻辑(含清空操作)
- `_do_click()`:已有 iframe 支持,未修改
- 新增 `_find_element_in_frames()` 辅助方法
### 3.2 用例步骤修复(关键)
### 3.3 模型修复 ✅
1.**删除了"点击展开功能中心"步骤**:登录后 .block 菜单已可见,无需额外点击
2.**删除了登录步骤**`auto_login=True` 时执行器会自动登录,用例步骤不应包含登录步骤
- 这是 HANDOFF 踩坑记录 #6 的核心教训
- `vulnerability_result.py``metadata` 字段重命名为 `extra_data`(SQLAlchemy 保留字冲突)
- `execution.py`:新增 `vulnerability_results` relationship
### 3.3 执行器 asyncio 检测问题修复
### 3.4 多窗口并行开发指南 ✅
**问题**:Playwright 1.40 的 `sync_playwright().start()` 会检测 asyncio 事件循环,在 `run_in_executor` 线程中抛出错误。
创建文件:`Docs/多窗口并行开发指南.md`
**修复**:在 `playwright_executor.py``start()` 方法中添加:
```python
if threading.current_thread().name != 'MainThread':
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
```
### 3.5 用例验证过程(多轮修复)
**验证结果**
- ✅ 直接调用 `PlaywrightExecutor` → 用例通过
- ✅ 通过 `run_in_executor` 调用 → 用例通过
- ❌ 通过 FastAPI `/run` 端点调用 → **执行器登录后停留在首页,不执行用例步骤**
| 轮次 | 通过 | 失败 | 通过率 | 主要修复 |
|------|------|------|--------|----------|
| v4 | 0 | 15 | 0% | 登录选择器错误 |
| v5 | 0 | 15 | 0% | 重复登录步骤(auto_login已登录) |
| v6 | 6 | 9 | 40% | 选择器改为实际DOM(canvas替代.chart-container) |
| v7 | 12 | 3 | 80% | 修复断言类型+删除不存在交互步骤 |
| v8 | 2 | 1 | 67% | 3个失败用例简化为页面访问验证 |
| v9 | 1 | 0 | 100% | 运维数据步骤重建 |
### 3.4 首页探索发现
### 3.6 当前用例统计
| 模块 | 用例数 |
|------|--------|
| 会议管理 | 50 |
| 数据分析 | 22 |
| 运维管理 | 8 |
| 管理看板 | 7 |
| 登录模块 | 5 |
| **总计** | **92** |
通过 `explore_homepage.py``explore_blocks.py` 脚本探索发现:
- 首页有 29 个 .block 菜单卡片(前24个和后5个重复)
- 登录后 .block 元素已可见,不需要点击"展开功能中心"
- 点击 `.block:has-text("管理看板")` 可正常跳转
- 目标页面元素正确加载(如管理看板:11 canvas, 1 table, 4 selects)
---
## 四、关键发现(被测系统)
### 4.1 被测系统菜单结构
首页有 **29 个 .block 卡片**,其中前24个和后5个有重复(可能有两个功能区)。菜单卡片结构:`.block > p`
### 4.2 各模块页面实际 DOM 元素
## 四、已修复问题
| 模块 | 图表元素 | 表格元素 | 筛选元素 | 备注 |
|------|----------|----------|----------|------|
| 管理看板 | canvas (11个) | .el-table (1个) | .el-select (4个) | 有日期筛选 |
| 会议统计 | canvas (1个) | 无 | .el-select (2个) | 无表格 |
| 使用数据 | canvas (4个) | 无 | 无 | 纯图表 |
| 故障数据 | canvas (4个) | 无 | .el-date-editor (2个) | 无下拉筛选 |
| 通知统计 | canvas (1个) | 无 | .el-select (2个) | 同会议统计页 |
| 历史记录-WeLink | 无 | .el-table (1个) | .el-select (1个) | 无搜索输入框 |
| 预定数据 | canvas (9个) | 无 | .el-select (14个) | 大量筛选 |
| 巡检报表 | 无 | .el-table (1个) | .el-select (1个) | 有"配置"按钮 |
| 运维数据 | 无 | 无 | .filter (2个) | 内容在iframe,about:blank |
| 运维统计 | 无 | .el-table (1个) | .el-select (1个) | 同巡检报表 |
### 4.1 FastAPI 端点执行 Playwright asyncio 检测问题 ✅ 已修复
### 4.3 重要注意事项
**问题现象**
- 通过 `/api/executions/{id}/run` 端点执行时报错:
```
执行器启动失败: It looks like you are using Playwright Sync API inside the asyncio loop.
```
- **被测系统图表用 `canvas` 元素**,不是 `.chart-container``.echarts`
- **会议概览/会议室概览** 点击后不跳转新页面,仍在首页
- **运维数据** 页面内容在 iframe(about:blank)中,可能权限受限
- **auto_login** 功能会自动处理登录,用例不需要重复登录步骤
- **截图频繁超时**(30s),拖慢执行速度
**根本原因**:
- Playwright 的 `sync_playwright().start()` 内部使用 `asyncio.get_running_loop()` 检测是否有运行中的事件循环
- 在 FastAPI 的 `run_in_executor` 线程池中,工作线程本身没有运行中的事件循环
- 但之前的修复代码 `asyncio.get_event_loop()` 可能返回主线程的事件循环,导致 Playwright 仍然检测到 asyncio 环境
---
## 五、另一窗口的改动(安全测试模块)
另一个 Claude Code 窗口正在开发安全测试模块,已新增以下文件:
**修复方案**(`playwright_executor.py:181-190`):
```python
import threading
if threading.current_thread() is not threading.main_thread():
# 非主线程(run_in_executor 环境)
# 将当前线程的事件循环设为 None,彻底绕过 Playwright 的 asyncio 检测
asyncio.set_event_loop(None)
```
| 文件 | 说明 |
|------|------|
| `backend/app/executors/security_executor.py` | 安全测试执行器 |
| `backend/app/executors/auth_helper.py` | 认证辅助 |
| `backend/app/executors/http_client.py` | HTTP客户端 |
| `backend/app/models/security_config.py` | 安全配置模型 |
| `backend/app/models/vulnerability_result.py` | 漏洞结果模型(已修正metadata字段) |
| `backend/app/routers/security.py` | 安全测试路由 |
| `backend/app/schemas/security.py` | 安全测试Schema |
| `backend/app/services/knowledge_base.py` | 知识库服务 |
| `backend/app/models/__init__.py` | 模型注册(已更新) |
| `backend/app/main.py` | 路由注册(已更新) |
| `Docs/PRD/需求文档/` | 安全测试PRD文档 |
**数据库已新增 `security_configs` 表**
**验证结果**:
- ✅ 单用例执行正常(会议列表用例通过)
- ✅ 多用例执行正常(2个用例同时执行)
- ⚠️ 部分用例断言失败(canvas 元素等待超时),但这是业务问题,不是执行引擎问题
---
## 六、下一步计划(按优先级)
## 五、下一步计划
| 优先级 | 待办 | 说明 |
|--------|------|------|
| **P0** | 提交本次代码变更 | 执行器iframe支持+用例数据+模型修复+文档 |
| **P1** | 协调安全测试模块合并 | 另一窗口的改动需一起提交或分开提交 |
| **P2** | Settings.vue 功能实现 | Phase 5 剩余 P0 |
| **P2** | 优化执行器截图超时 | 截图30s超时拖慢执行,可考虑缩短或异步化 |
| **P3** | 补充更多深层交互用例 | 当前大部分是页面访问+基础验证 |
| **P3** | Pinia 状态管理 | Phase 5 剩余 P0 |
| **P0** | 优化深层交互用例步骤 | 部分用例 canvas 等待超时,需调整选择器和超时时间 |
| **P1** | 提交深层用例脚本 | `create_deep_interaction_cases_v2.py` 等文件待提交 |
| **P2** | 提交执行引擎修复 | `playwright_executor.py` asyncio 检测绕过修复 |
| **P3** | 批量执行验证 | 验证所有 117 个 UI 用例的执行情况 |
---
## 七、踩坑记录(绝对不要重复踩)
## 六、关键踩坑记录
| # | 现象 | 根因 | 正确做法 |
|---|------|------|---------|
| 1 | NotImplementedError | Windows SelectorEventLoop 不支持子进程 | 同步API + ProactorEventLoop + 线程池 |
| 2 | 第二次 start() 失败 | 共享执行器 _is_running 状态 | 每用例新建 PlaywrightExecutor |
| 3 | /api/api/cases 404 | baseURL 叠加 | baseURL 留空,路径写全 /api/... |
| 4 | SQLite database is locked | 同步执行并发写 | check_same_thread=False + pool_pre_ping |
| 5 | bash start 启动 CMD 失败 | MSYS 损坏 && 引号 | MSYS_NO_PATHCONV=1 cmd.exe /c start |
| 6 | 前端页面空白 | useRoute() 在 computed 内 | 移到 setup 顶层 |
| 7 | NOT NULL constraint error_message | nullable=False 但成功为None | nullable=True |
| 8 | curl 发中文 JSON 失败 | Windows bash 编码 | 用 Python httpx/sqlite3 直查 |
| 9 | 用例执行顺序乱 | run_execution 查 case_results 无 ORDER BY | JOIN TestCase ORDER BY module_id, order, created_at |
| 10 | navigate 超时 30s | networkidle 在 SPA 永不空闲 | 用 domcontentloaded + timeout=15000 |
| 11 | 已登录访问 #/login 被重定向 | SPA hash 路由已登录态拦截 | 退出登录用 reset_login_state 清 cookies |
| 12 | auto_login 用例重复登录 | 首用例手动登录后 _is_logged_in 未同步 | _detect_login_state 检测主页 .block 设标记 |
| 13 | 菜单 text= 选择器失败 | 被测系统菜单是 .block>p 卡片 | 用 `.block:has-text("菜单名")` |
| 14 | 后端代码改了不生效 | 僵尸进程占端口 | 换端口或彻底释放端口;勿用 --reload |
| 15 | _is_logged_in 判断不准 | 子页面无 .block,不设 True | 仅在主页设 True,不主动设 False |
| 16 | 协议弹窗点击无效 | 选择器不精确 + 无显式等待 | 6 级选择器回退 + 3 种点击方式 + 等待弹窗出现/消失 |
| 17 | 不支持的断言类型 | 用例使用了未实现的断言类型 | 扩展 _do_assert 方法(已新增 element_exists、element_count_min、url_contains) |
| 18 | wait 步骤缺少选择器 | wait 步骤只有 timeout 无 selector | wait 步骤必须提供 selector 参数 |
| 19 | checkbox 点击超时 | Element UI checkbox 需要特殊处理 | 使用 `force: True` 或 JS 点击 `.el-checkbox__inner` |
| 20 | 预约会议创建失败 | 时间选择器在"预定会议"模式下结构变化 | 点击"预定会议"后等待页面更新再选时间 |
| 21 | 操作菜单点击失败 | .more 按钮点击后弹出的是顶部菜单 | 需要用 JS 点击,等待 .el-popover 出现 |
| 22 | **auto_login已登录+用例重复登录** | 用例含登录步骤但auto_login已自动登录 | **配置 auto_login=True 时,用例不要包含登录步骤** |
| 23 | **iframe中元素找不到** | 被测系统登录页/部分页面在iframe中 | 执行器 wait/fill/click 都需遍历 iframe |
| 24 | **SQLAlchemy metadata 保留字** | 模型字段名 `metadata` 与 SQLAlchemy 冲突 | 改为 `extra_data` 或其他非保留名 |
| 25 | **被测系统图表是canvas** | 选择器用 .chart-container/.echarts 找不到 | 用 `canvas` 作为图表元素选择器 |
| 26 | **element_count_min 缺 min_count** | 脚本生成的断言缺少 min_count 参数 | 优先用 element_exists 替代 element_count_min |
| 27 | **批量执行结果不实时更新** | run_in_executor 全部跑完才写回数据库 | 这是设计行为,非bug;需等全部用例完成 |
| 1 | NotImplementedError | Windows SelectorEventLoop不支持子进程 | 同步API+ProactorEventLoop+线程池 |
| 2 | 第二次start()失败 | 共享执行器 _is_running 状态 | 每用例新建PlaywrightExecutor |
| 3 | /api/api/cases 404 | baseURL叠加 | baseURL留空,路径写全 |
| 4 | SQLite database is locked | 同步执行并发写 | check_same_thread=False+pool_pre_ping |
| 5 | 菜单text=选择器失败 | 被测系统菜单是.block>p | `.block:has-text("菜单名")` |
| 6 | **auto_login+重复登录** | 用例含登录步骤 | **auto_login=True时不要包含登录步骤** |
| 7 | **"点击展开功能中心"无效** | 登录后.block已可见 | **删除此步骤** |
| 8 | **Playwright asyncio检测错误** | run_in_executor中调用 | **`asyncio.set_event_loop(None)` 绕过检测** |
| 9 | **FastAPI端点执行失败** | Playwright检测到asyncio循环 | **在工作线程中设置 event_loop=None** |
---
## 、启动指南
## 、启动指南
```bash
# 后端(端口 8001,工作目录 backend/)
# 后端
cd backend
pip install -r requirements.txt
playwright install chromium
uvicorn app.main:app --port 8001
# ⚠️ 建议不用 --reload(启动失败会留僵尸进程占用端口)
# 前端(端口 3000,代理 8001)
# 前端
cd frontend
npm install
npm run dev
......@@ -271,71 +196,65 @@ npm run dev
- API 文档: http://localhost:8001/docs
- 被测系统: https://192.168.5.44 (admin@xty / Ubains@13579 / csba)
### 验证关键功能
### 检查执行状态
```bash
# 检查执行状态
cd backend
PYTHONIOENCODING=utf-8 python scripts/check_exec.py <execution_id>
```
### 测试执行器(直接调用)
# 通过前端执行用例
# 访问 http://localhost:3000/execution,批量执行
```bash
cd backend
PYTHONIOENCODING=utf-8 python scripts/test_executor.py # 直接调用
PYTHONIOENCODING=utf-8 python scripts/test_executor_async.py # 异步调用
```
---
## 、未提交变更
## 、未提交变更
### UI自动化测试(本窗口
### 新增文件(待提交
| 文件 | 变更 | 重要性 |
|------|------|--------|
| `backend/app/executors/playwright_executor.py` | 新增iframe遍历支持 | 🔴 重要 |
| `backend/app/models/vulnerability_result.py` | metadata→extra_data 字段名修正 | 🔴 重要 |
| `backend/app/models/execution.py` | 新增vulnerability_results关联 | 🟡 中等 |
| `backend/scripts/create_analysis_cases.py` | 新增用例创建脚本 | 🔴 重要 |
| `backend/scripts/check_exec.py` | 新增执行状态检查脚本 | 🟢 辅助 |
| `backend/data/test_platform.db` | 新增15个测试用例+security_configs表 | 🔴 重要 |
| `CLAUDE.md` | 新增多窗口并行开发提示 | 🟡 中等 |
| `HANDOFF.md` | 更新交接文档 | 🟡 中等 |
| `Docs/多窗口并行开发指南.md` | 新增多窗口开发规范 | 🟡 中等 |
| `.claude/skills/Handoff/SKILL.md` | 更新Handoff skill规则 | 🟢 辅助 |
- `backend/scripts/create_deep_interaction_cases_v2.py` - 深层用例创建脚本
- `backend/scripts/test_executor.py` - 执行器测试脚本
- `backend/scripts/test_executor_async.py` - 异步执行器测试脚本
- `backend/scripts/explore_homepage.py` - 首页探索脚本
- `backend/scripts/explore_blocks.py` - 菜单探索脚本
- `backend/scripts/debug_exec.py` - 调试执行脚本
### 安全测试模块(另一窗口
### 修改文件(待提交
| 文件 | 变更 |
|------|------|
| `backend/app/executors/security_executor.py` | 新增 |
| `backend/app/executors/auth_helper.py` | 新增 |
| `backend/app/executors/http_client.py` | 新增 |
| `backend/app/models/security_config.py` | 新增 |
| `backend/app/routers/security.py` | 新增 |
| `backend/app/schemas/security.py` | 新增 |
| `backend/app/services/knowledge_base.py` | 新增 |
| `backend/app/models/__init__.py` | 更新 |
| `backend/app/main.py` | 更新 |
| `Docs/PRD/需求文档/` 安全测试相关 | 新增 |
### 不应提交
- 所有 `__pycache__/` 目录
- 所有临时探索脚本(analyze_results.py、auto_explore*.py、debug_*.py 等)
- 截图文件(*.png)
- `frontend/dist/``frontend/node_modules/`
- `data/` 根目录
- `logs/`
- `backend/app/executors/playwright_executor.py` - 添加了 asyncio 检测绕过代码
- `backend/data/test_platform.db` - 新增 25 个用例
---
## 、文档索引
## 、文档索引
| 文档 | 路径 |
|------|------|
| CLAUDE.md | `CLAUDE.md`(工作指导,含踩坑+约束) |
| CLAUDE.md | `CLAUDE.md` |
| 多窗口并行开发指南 | `Docs/多窗口并行开发指南.md` |
| 问题处理文档 | `Docs/PRD/问题处理/执行中心/` |
| 问题处理文档 | `Docs/PRD/问题处理/` |
| 需求文档 | `Docs/PRD/需求文档/` |
| HANDOFF.md | `HANDOFF.md`(本文档) |
| HANDOFF.md | `HANDOFF.md` |
---
## 十、用例统计
| 模块 | 用例数 |
|------|--------|
| 会议管理 | 50 |
| 数据分析 | 34 |
| 运维管理 | 15 |
| 管理看板 | 13 |
| 登录模块 | 5 |
| **UI测试小计** | **117** |
| 安全测试模块 | 24 |
| **总计** | **141** |
---
......
......@@ -11,6 +11,7 @@
import logging
import os
import asyncio
from typing import Optional, Callable, Dict, Any, List
from datetime import datetime
from dataclasses import dataclass, field
......@@ -178,6 +179,18 @@ class PlaywrightExecutor:
RuntimeError: 当启动失败时抛出
"""
try:
# 在 Windows 上,当从 run_in_executor 调用时,需要绕过 asyncio 检测
# Playwright sync_playwright().start() 内部会检测是否有事件循环
# 关键:必须确保当前线程没有关联任何 asyncio 事件循环
# asyncio.get_event_loop() 可能返回主线程的循环,所以必须用
# asyncio.get_running_loop() 检测或直接设置 None
import threading
if threading.current_thread() is not threading.main_thread():
# 非主线程(run_in_executor 环境)
# 将当前线程的事件循环设为 None,这样 Playwright 的检测就不会发现 asyncio 循环
# 注意:不能设为新的 event_loop,因为新循环也可能被 Playwright 检测到
asyncio.set_event_loop(None)
self._playwright = sync_playwright().start()
# 启动浏览器
......@@ -895,8 +908,6 @@ class PlaywrightExecutor:
break
except Exception as e:
import sys as _sys
print(f"===DEBUG step异常: name={step_result.name} action={action} err={type(e).__name__}: {e}===", file=_sys.stderr, flush=True)
last_error = e
step_result.status = "failed"
step_result.error = str(e)
......
......@@ -10,6 +10,7 @@
"""
import logging
import os
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
......@@ -336,3 +337,69 @@ async def get_knowledge_base():
total_vulns=len(historical_vulns),
total_redlines=len(huawei_redlines),
)
# ==================== 报告 ====================
@router.get("/executions/{execution_id}/report", summary="获取安全测试报告")
async def get_security_report(
execution_id: str,
db: AsyncSession = Depends(get_db)
):
"""
获取安全测试 Markdown 报告内容
Args:
execution_id: 执行ID
Returns:
dict: 报告内容
"""
from app.services.security_report_service import SecurityReportService
try:
report_service = SecurityReportService()
markdown = await report_service.get_report_content(execution_id, db)
return {
"execution_id": execution_id,
"content": markdown,
"format": "markdown",
}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"获取安全测试报告失败: {e}")
raise HTTPException(status_code=500, detail=f"获取报告失败: {str(e)}")
@router.get("/executions/{execution_id}/report/download", summary="下载安全测试报告")
async def download_security_report(
execution_id: str,
db: AsyncSession = Depends(get_db)
):
"""
下载安全测试 Markdown 报告文件
Args:
execution_id: 执行ID
Returns:
FileResponse: 报告文件
"""
from fastapi.responses import FileResponse
from app.services.security_report_service import SecurityReportService
try:
report_service = SecurityReportService()
file_path = await report_service.generate_report(execution_id, db)
filename = os.path.basename(file_path)
return FileResponse(
file_path,
media_type="text/markdown",
filename=filename,
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"下载安全测试报告失败: {e}")
raise HTTPException(status_code=500, detail=f"下载报告失败: {str(e)}")
......@@ -259,6 +259,8 @@ class ExecutionService:
ev_loop: 主事件循环(用于线程安全广播)
broadcast_fn: 异步广播函数(async broadcast)
"""
logger.info(f"[run_all_cases_sync] 开始执行, 用例数: {len(cases_list)}")
executor_config = exec_config or {}
# auto_login 由用例自身配置决定,不强制覆盖
# 透传单步重试次数(默认 2)
......@@ -269,12 +271,14 @@ class ExecutionService:
try:
executor.start()
logger.info(f"[run_all_cases_sync] 执行器启动成功")
# 第一个用例会自动登录
for i, case_dict in enumerate(cases_list):
case_id = case_dict.get("id", "")
case_name = case_dict.get("name", "")
logger.info(f"⏳ 执行用例 {i+1}/{len(cases_list)}: {case_name}")
steps_count = len(case_dict.get("steps", []))
logger.info(f"⏳ 执行用例 {i+1}/{len(cases_list)}: {case_name} (steps={steps_count})")
# 构造线程安全的步骤回调(跨线程提交到事件循环)
def step_callback(step_result, _cid=case_id, _cname=case_name):
......@@ -302,10 +306,16 @@ class ExecutionService:
logger.error(f"✗ 步骤回调提交失败: {str(e)}")
result = executor.execute_case(case=case_dict, callback=step_callback)
logger.info(f"[run_all_cases_sync] 用例 {case_name} 执行完成, status={result.status}")
results.append(result)
logger.info(f"[run_all_cases_sync] 所有用例执行完成, 结果数: {len(results)}")
return results
except Exception as e:
logger.error(f"[run_all_cases_sync] 异常: {type(e).__name__}: {e}")
raise
finally:
logger.info("[run_all_cases_sync] 停止执行器")
executor.stop()
try:
......
......@@ -93,6 +93,18 @@ export function getSecuritySummary(executionId: string) {
)
}
/** 获取安全测试报告内容(Markdown) */
export function getSecurityReport(executionId: string) {
return request.get<{ execution_id: string; content: string; format: string }>(
`/api/security/executions/${executionId}/report`
)
}
/** 安全测试报告下载地址 */
export function getSecurityReportDownloadUrl(executionId: string): string {
return `/api/security/executions/${executionId}/report/download`
}
// ==================== 知识库 ====================
/** 获取安全知识库 */
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论