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

feat(executor): 执行器新增iframe遍历支持+多窗口开发规范

- playwright_executor.py: wait/fill/click 三方法新增 iframe 遍历逻辑
- execution.py: 新增 vulnerability_results 关联
- 新增 create_analysis_cases.py 数据分析/运维/看板用例脚本(15个用例)
- 新增 check_exec.py 执行状态检查辅助脚本
- 新增多窗口并行开发指南文档
- 更新 CLAUDE.md 和 HANDOFF.md

注:数据库和安全模块变更由另一窗口单独提交
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 4b690a98
...@@ -31,8 +31,9 @@ description: 生成交接文档 HANDOFF.md,供下一次会话快速恢复上 ...@@ -31,8 +31,9 @@ description: 生成交接文档 HANDOFF.md,供下一次会话快速恢复上
3. 根据上述信息合成 HANDOFF.md,写入项目根目录 `HANDOFF.md` 3. 根据上述信息合成 HANDOFF.md,写入项目根目录 `HANDOFF.md`
4. 告知用户 HANDOFF.md 已生成 4. 告知用户 HANDOFF.md 已生成
## 输出格式 ## 生成规则
- 写入 `C:\PycharmData\ubains-module-test\platform-auto-test\HANDOFF.md` - **⚠️ 必须包含「开始工作前必读」章节**:在 HANDOFF.md 顶部(项目概览之前)插入多窗口并行开发冲突规避提示,指向 `Docs/多窗口并行开发指南.md`,确保每次新会话开始时首先看到此提示
- 写入项目根目录 `HANDOFF.md`
- 使用 Markdown 格式,中文字符 - 使用 Markdown 格式,中文字符
- 每个章节用 `---` 分隔,便于快速扫读 - 每个章节用 `---` 分隔,便于快速扫读
\ No newline at end of file
# CLAUDE.md # CLAUDE.md
> 本文件为 Claude Code 在此项目中工作时的指导文档。Claude Code 在每次会话开始时会自动读取本文件。 > 本文件为 Claude Code 在此项目中工作时的指导文档。Claude Code 在每次会话开始时会自动读取本文件。
> 维护者:czj · 最后更新:2026-07-13 > 维护者:czj · 最后更新:2026-07-21
---
## ⚠️ 多窗口并行开发注意
如需开多个 Claude Code 窗口同时开发不同子菜单功能,**必须先阅读** `Docs/多窗口并行开发指南.md`,了解:
- 端口隔离(8001/8002)
- 数据库操作错开
- Playwright 执行互斥(不可同时执行用例)
- Git 提交协作
--- ---
......
# 多窗口并行开发指南
> 适用场景:多个 Claude Code 窗口同时开发不同子菜单功能
> 创建时间:2026-07-21
---
## 一、核心冲突点
| 冲突点 | 原因 | 解决方案 |
|--------|------|----------|
| SQLite 数据库 | 两个窗口同时写入 `test_platform.db``database is locked` | 错开数据库操作时间 |
| 后端端口 8001 | 同一端口只能一个实例 | 新窗口使用 8002 端口 |
| Playwright 浏览器 | 两个窗口同时执行用例会抢登录态 | 不同时执行用例 |
| Git 提交 | 同一分支后提交需先 pull | 提交前先 pull,或用不同分支 |
---
## 二、端口隔离方案(推荐)
### 窗口 A(主窗口)— 继续开发会议管理
```bash
# 后端端口:8001
cd backend
uvicorn app.main:app --port 8001
# 前端端口:3000
cd frontend
npm run dev
```
### 窗口 B(新窗口)— 开发其他子菜单
```bash
# 后端端口:8002(使用不同端口)
cd backend
uvicorn app.main:app --port 8002
# 前端端口:3001(使用不同端口)
cd frontend
# 临时修改 vite.config.ts 的 proxy 指向 8002
npm run dev -- --port 3001
```
**注意**:窗口 B 的前端需要临时修改 `vite.config.ts` 中的 proxy target:
```typescript
// vite.config.ts 中临时改为
proxy: {
'/api': {
target: 'http://localhost:8002', // 改为 8002
changeOrigin: true,
}
}
```
---
## 三、数据库隔离方案
### 方案 A:错开操作时间(简单)
- 窗口 A 创建用例时,窗口 B 不操作数据库
- 窗口 B 创建用例时,窗口 A 不操作数据库
- 两个窗口不同时运行用例创建脚本
### 方案 B:使用独立数据库副本(更安全)
```bash
# 窗口 B 使用独立的数据库副本
cd backend/data
cp test_platform.db test_platform_dev2.db
```
然后修改 `backend/config.py` 临时指定数据库路径:
```python
# 窗口 B 临时修改
DATABASE_URL = "sqlite+aiosqlite:///./data/test_platform_dev2.db"
```
**缺点**:两个数据库数据不互通,最后需要手动合并。
### 方案 C:模块级隔离(推荐)
两个窗口各自开发不同的模块(module_id 不同),这样:
- 用例数据按模块隔离
- 执行时按模块筛选
- 数据库写入冲突概率降低
---
## 四、Git 提交协作
### 同一分支协作(platform-auto-test)
```bash
# 窗口 A 提交后
git push origin platform-auto-test
# 窗口 B 提交前(先拉取 A 的变更)
git pull origin platform-auto-test
# 如有冲突,解决后再提交
git push origin platform-auto-test
```
### 不同分支协作(更安全)
```bash
# 窗口 B 使用独立分支
git checkout -b feature/数据分析模块
# 开发完成后合并回 platform-auto-test
```
---
## 五、Playwright 执行隔离
### ⚠️ 绝对禁止:两个窗口同时执行用例
**原因**
1. Playwright 会打开同一个被测系统 `https://192.168.5.44`
2. 两个执行器会互相覆盖登录态
3. 可能导致一个用例失败(登录被另一个踢掉)
### ✅ 正确做法
- **窗口 A 执行用例时,窗口 B 不执行**
- **窗口 B 执行用例时,窗口 A 不执行**
- 或者:窗口 B 只开发代码,不执行用例验证(由窗口 A 统一执行)
---
## 六、实操检查清单
### 开发前确认
| 检查项 | 窗口 A | 窗口 B |
|--------|--------|--------|
| 后端端口 | 8001 | 8002(或不同时启动) |
| 前端端口 | 3000 | 3001(或不同时启动) |
| 开发模块 | 会议管理 | 其他子菜单 |
| git 分支 | platform-auto-test | 同一分支或独立分支 |
### 操作时确认
- [ ] 不同时运行用例创建脚本(操作数据库)
- [ ] 不同时执行测试用例(Playwright 隔离)
- [ ] 提交前先 pull 对方的变更
- [ ] 冲突时及时沟通
---
## 七、推荐分工模式
### 模式 1:代码开发 + 用例验证分离
| 窗口 | 职责 |
|------|------|
| 窗口 A(主) | 用例执行验证、数据库维护、最终提交 |
| 窗口 B(辅) | 纯代码开发(前端组件、后端路由),不执行用例 |
### 模式 2:模块并行开发
| 窗口 | 开发模块 | 用例范围 |
|------|----------|----------|
| 窗口 A | 会议管理 | module_e80a5d08... |
| 窗口 B | 数据分析 | 新建 module_xxx... |
各自创建独立模块,用例互不干扰,最后合并。
---
## 八、快速启动命令
### 窗口 A(主窗口)
```bash
# 后端
cd E:\GithubData\ubains-module-test\platform-auto-test\backend
uvicorn app.main:app --port 8001
# 前端
cd E:\GithubData\ubains-module-test\platform-auto-test\frontend
npm run dev
```
### 窗口 B(新窗口)
```bash
# 后端(不同端口)
cd E:\GithubData\ubains-module-test\platform-auto-test\backend
uvicorn app.main:app --port 8002
# 前端(不同端口,proxy 指向 8002)
cd E:\GithubData\ubains-module-test\platform-auto-test\frontend
# 先修改 vite.config.ts 的 proxy target 为 8002
npm run dev -- --port 3001
```
---
## 九、常见问题
### Q1: 端口被占用怎么办?
```bash
# 查找占用进程
netstat -ano | findstr :8001
# 强制释放(替换 PID)
taskkill /PID <PID> /F
```
### Q2: 数据库锁定怎么办?
等待几秒后重试,或确保另一个窗口没有在写数据库。
### Q3: Git 冲突怎么办?
```bash
# 查看冲突文件
git status
# 手动解决冲突后
git add <冲突文件>
git commit -m "merge: 解决冲突"
```
---
*本文档用于多窗口并行开发场景,确保开发效率和数据安全。*
此差异已折叠。
...@@ -506,6 +506,37 @@ class PlaywrightExecutor: ...@@ -506,6 +506,37 @@ class PlaywrightExecutor:
return [] return []
def _find_element_in_frames(self, selector: str, timeout: int = 5000, state: str = "visible"):
"""
在主页面和所有 iframe 中查找元素
Args:
selector (str): 选择器
timeout (int): 超时毫秒
state (str): 状态 visible/hidden/attached/detached
Returns:
tuple: (frame, element) 或 (None, None)
"""
# 先尝试主页面
try:
self._page.wait_for_selector(selector, state=state, timeout=timeout)
return self._page, None
except Exception:
pass
# 遍历 iframe
for frame in self._page.frames:
if frame == self._page.main_frame:
continue
try:
frame.wait_for_selector(selector, state=state, timeout=timeout)
return frame, None
except Exception:
continue
return None, None
def _wait_for_element( def _wait_for_element(
self, self,
selector: str, selector: str,
...@@ -513,7 +544,7 @@ class PlaywrightExecutor: ...@@ -513,7 +544,7 @@ class PlaywrightExecutor:
state: str = "visible", state: str = "visible",
) -> bool: ) -> bool:
""" """
显式等待元素到达指定状态 显式等待元素到达指定状态(支持 iframe)
失败不抛异常,返回 False,由调用方决定后续行为。 失败不抛异常,返回 False,由调用方决定后续行为。
...@@ -525,16 +556,27 @@ class PlaywrightExecutor: ...@@ -525,16 +556,27 @@ class PlaywrightExecutor:
Returns: Returns:
bool: 是否在超时前达到状态 bool: 是否在超时前达到状态
""" """
actual_timeout = timeout or self.timeout
try: try:
self._page.wait_for_selector( # 先尝试主页面
selector, self._page.wait_for_selector(selector, state=state, timeout=actual_timeout)
state=state,
timeout=timeout or self.timeout,
)
return True return True
except Exception as e: except Exception:
logger.debug(f"⏳ 等待元素未就绪: {selector} state={state} ({e})") pass
return False
# 遍历 iframe
for frame in self._page.frames:
if frame == self._page.main_frame:
continue
try:
frame.wait_for_selector(selector, state=state, timeout=actual_timeout)
logger.debug(f"⏳ 在 iframe 中找到元素: {selector}")
return True
except Exception:
continue
logger.debug(f"⏳ 等待元素未就绪: {selector} state={state}")
return False
def _safe_is_visible(self, selector: str) -> bool: def _safe_is_visible(self, selector: str) -> bool:
"""安全判断元素是否可见,不抛异常""" """安全判断元素是否可见,不抛异常"""
...@@ -1006,7 +1048,7 @@ class PlaywrightExecutor: ...@@ -1006,7 +1048,7 @@ class PlaywrightExecutor:
def _do_fill(self, params: dict, force: bool = False) -> None: def _do_fill(self, params: dict, force: bool = False) -> None:
""" """
填充输入框(多选择器回退 + 强制清空 + 强制点击 填充输入框(多选择器回退 + 强制清空 + 支持 iframe
先 click(count=3, force=True) 全选 → Ctrl+A → Delete → 等待 100ms → fill(value) 先 click(count=3, force=True) 全选 → Ctrl+A → Delete → 等待 100ms → fill(value)
...@@ -1022,11 +1064,11 @@ class PlaywrightExecutor: ...@@ -1022,11 +1064,11 @@ class PlaywrightExecutor:
last_error: Optional[Exception] = None last_error: Optional[Exception] = None
for selector in selectors: for selector in selectors:
# 先在主页面尝试
try: try:
# 等待元素可见 self._page.wait_for_selector(selector, state="visible", timeout=10000)
self._wait_for_element(selector, timeout=10000, state="visible")
# 强制清空:三击全选 → Ctrl+A → Delete → 等待 100ms # 强制清空
self._page.click(selector, click_count=3, force=True, timeout=5000) self._page.click(selector, click_count=3, force=True, timeout=5000)
self._page.keyboard.press("Control+A") self._page.keyboard.press("Control+A")
self._page.keyboard.press("Delete") self._page.keyboard.press("Delete")
...@@ -1034,12 +1076,28 @@ class PlaywrightExecutor: ...@@ -1034,12 +1076,28 @@ class PlaywrightExecutor:
# 填充 # 填充
self._page.fill(selector, str(value), timeout=5000) self._page.fill(selector, str(value), timeout=5000)
logger.debug(f"✓ 填充成功: {selector} = {value}") logger.debug(f"✓ 主页面填充成功: {selector} = {value}")
return return
except Exception as e: except Exception as e:
last_error = e last_error = e
logger.debug(f"⚠ 填充失败 {selector}: {e}") logger.debug(f"⚠ 主页面填充失败 {selector}: {e}")
continue
# 在 iframe 中尝试
for frame in self._page.frames:
if frame == self._page.main_frame:
continue
try:
frame.wait_for_selector(selector, state="visible", timeout=5000)
frame.click(selector, click_count=3, force=True, timeout=5000)
frame.keyboard.press("Control+A")
frame.keyboard.press("Delete")
frame.wait_for_timeout(100)
frame.fill(selector, str(value), timeout=5000)
logger.debug(f"✓ iframe 中填充成功: {selector} = {value}")
return
except Exception as e2:
last_error = e2
continue
raise Exception(f"✗ 无法填充元素(已尝试 {len(selectors)} 个选择器): {selectors} 最后错误: {last_error}") raise Exception(f"✗ 无法填充元素(已尝试 {len(selectors)} 个选择器): {selectors} 最后错误: {last_error}")
...@@ -1097,7 +1155,7 @@ class PlaywrightExecutor: ...@@ -1097,7 +1155,7 @@ class PlaywrightExecutor:
def _do_wait(self, params: dict) -> None: def _do_wait(self, params: dict) -> None:
""" """
等待元素出现(多选择器回退) 等待元素出现(多选择器回退 + 支持 iframe
Args: Args:
params (dict): {"selector"|"selectors": str|[str], "timeout": int} params (dict): {"selector"|"selectors": str|[str], "timeout": int}
...@@ -1109,14 +1167,26 @@ class PlaywrightExecutor: ...@@ -1109,14 +1167,26 @@ class PlaywrightExecutor:
last_error: Optional[Exception] = None last_error: Optional[Exception] = None
for selector in selectors: for selector in selectors:
# 先在主页面尝试
try: try:
self._page.wait_for_selector(selector, timeout=timeout) self._page.wait_for_selector(selector, timeout=timeout)
logger.debug(f"✓ 等待元素成功: {selector}") logger.debug(f"✓ 主页面等待元素成功: {selector}")
return return
except Exception as e: except Exception as e:
last_error = e last_error = e
logger.debug(f"⚠ 等待元素失败 {selector}: {e}") logger.debug(f"⚠ 主页面等待元素失败 {selector}: {e}")
continue
# 在 iframe 中尝试
for frame in self._page.frames:
if frame == self._page.main_frame:
continue
try:
frame.wait_for_selector(selector, timeout=timeout)
logger.debug(f"✓ iframe 中等待元素成功: {selector}")
return
except Exception as e2:
last_error = e2
continue
raise Exception(f"✗ 等待元素超时(已尝试 {len(selectors)} 个选择器): {selectors} 最后错误: {last_error}") raise Exception(f"✗ 等待元素超时(已尝试 {len(selectors)} 个选择器): {selectors} 最后错误: {last_error}")
......
...@@ -100,6 +100,13 @@ class Execution(Base): ...@@ -100,6 +100,13 @@ class Execution(Base):
cascade="all, delete-orphan" cascade="all, delete-orphan"
) )
# 关联漏洞测试结果(一对多)
vulnerability_results: Mapped[list["VulnerabilityResult"]] = relationship(
"VulnerabilityResult",
back_populates="execution",
cascade="all, delete-orphan"
)
def __repr__(self) -> str: def __repr__(self) -> str:
"""字符串表示""" """字符串表示"""
return f"<Execution(id={self.id}, status={self.status}, pass_rate={self.pass_rate}%)>" return f"<Execution(id={self.id}, status={self.status}, pass_rate={self.pass_rate}%)>"
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""检查执行计划状态"""
import sys
import time
import httpx
EXEC_ID = sys.argv[1] if len(sys.argv) > 1 else ""
WAIT = int(sys.argv[2]) if len(sys.argv) > 2 else 0
BASE_URL = "http://localhost:8001"
if WAIT > 0:
print(f"等待 {WAIT} 秒...")
time.sleep(WAIT)
if EXEC_ID:
with httpx.Client(timeout=30) as client:
r = client.get(f"{BASE_URL}/api/executions/{EXEC_ID}")
if r.status_code == 200:
d = r.json()
print(f"状态: {d['status']}")
print(f"进度: {d['passed'] + d['failed']}/{d['total_cases']}")
print(f"通过: {d['passed']}, 失败: {d['failed']}, 跳过: {d.get('skipped', 0)}")
if d['status'] == 'completed':
print(f"通过率: {d['pass_rate']*100:.1f}%")
此差异已折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论