feat: add prompt-first batches and pausable task guidance
This commit is contained in:
1 parent
cec9898e4c
commit
057f0c3050
44 files changed
+1080
-191
No files matched your search
@@ -32,6 +32,8 @@ STATUS = {
|
||||
"pending": "待执行",
|
||||
"queued": "已排队",
|
||||
"running": "执行中",
|
||||
"task_paused": "等待指导",
|
||||
"resuming": "等待设备恢复",
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +109,11 @@ def session_report(session_id: str, db_path=None) -> dict:
|
||||
"error": meta.get("error", ""),
|
||||
"device": meta.get("device_serial") or text(session.get("device_info") or ""),
|
||||
"tags": case.get("tags", ""),
|
||||
"case_id": case.get("case_id", ""),
|
||||
"category": case.get("category", ""),
|
||||
"subcategory": case.get("subcategory", ""),
|
||||
"preconditions": case.get("preconditions", ""),
|
||||
"task_config": case.get("task_config") or {},
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +128,11 @@ def time_text(value) -> str:
|
||||
def metadata(report: dict) -> list[tuple]:
|
||||
return [
|
||||
("任务编号", report["id"]),
|
||||
("用例编号", report.get("case_id") or "—"),
|
||||
("检测项目", report.get("category") or "—"),
|
||||
("测试子项", report.get("subcategory") or "—"),
|
||||
("前置约束提取", report.get("preconditions") or "—"),
|
||||
("任务配置", text(report.get("task_config") or {})),
|
||||
("任务目标", report["goal"]),
|
||||
("预期结果", report["expected"] or "未指定"),
|
||||
("执行状态", STATUS.get(report["status"], report["status"])),
|
||||
@@ -279,6 +291,88 @@ def export_session(session_id: str, formats: list[str] | None = None) -> list[Pa
|
||||
return outputs
|
||||
|
||||
|
||||
def session_screenshots(report: dict) -> list[tuple[Path, str]]:
|
||||
"""Ordered, path-checked pre/post observations for each recorded step."""
|
||||
screenshots: list[tuple[Path, str]] = []
|
||||
for index, step in enumerate(report["steps"], 1):
|
||||
for phase, key in (("操作前", "pre_image_name"), ("操作后", "post_image_name")):
|
||||
source = image_path(step.get(key))
|
||||
if source:
|
||||
screenshots.append(
|
||||
(
|
||||
source,
|
||||
f"{index:04d}_步骤{int(step.get('step_number') or index):04d}_{phase}{source.suffix.lower()}",
|
||||
)
|
||||
)
|
||||
return screenshots
|
||||
|
||||
|
||||
def session_videos(report: dict) -> list[tuple[Path, str]]:
|
||||
"""Find only verified files attached to this session's recording directory."""
|
||||
session = SessionRepository().get_session_by_id(report["id"]) or {}
|
||||
recording = SessionRepository().get_video_recording_for_session(report["id"]) or {}
|
||||
candidate = recording.get("local_video_path") or session.get("video_filepath")
|
||||
if not candidate:
|
||||
return []
|
||||
source = Path(str(candidate)).resolve()
|
||||
session_root = (TRACES_PATH / identifier(report["id"])).resolve()
|
||||
if not source.is_relative_to(session_root):
|
||||
return []
|
||||
# A resumed recording lives under resume_NNNN; the initial recording is
|
||||
# in its parent, with any later segments in sibling resume folders.
|
||||
directory = source.parent
|
||||
if directory.name.startswith("resume_"):
|
||||
directory = directory.parent
|
||||
if not directory.is_dir() or not directory.is_relative_to(session_root):
|
||||
return []
|
||||
paths = [
|
||||
p
|
||||
for p in directory.rglob("*.mp4")
|
||||
if p.is_file()
|
||||
and p.resolve().is_relative_to(directory.resolve())
|
||||
and (p.parent == directory or p.parent.name.startswith("resume_"))
|
||||
]
|
||||
paths.sort(key=lambda p: (0 if p.parent == directory else 1, p.parent.name, p.name))
|
||||
return [(path, f"{i:04d}_{path.parent.name}_{path.name}") for i, path in enumerate(paths, 1)]
|
||||
|
||||
|
||||
def export_session_media(session_id: str, kind: str) -> Path:
|
||||
if kind not in {"screenshots", "video", "all"}:
|
||||
raise ValueError("不支持的下载类型")
|
||||
with EXPORT_LOCK:
|
||||
report = session_report(session_id)
|
||||
target_dir = EXPORT_ROOT / "sessions" / identifier(session_id)
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
images = session_screenshots(report) if kind in {"screenshots", "all"} else []
|
||||
videos = session_videos(report) if kind in {"video", "all"} else []
|
||||
if kind == "video" and not videos:
|
||||
raise ValueError("此任务没有已保存的录屏")
|
||||
if kind == "screenshots" and not images:
|
||||
raise ValueError("此任务没有可下载的步骤截图")
|
||||
target = target_dir / f"{kind}.zip"
|
||||
temporary = target_dir / f".{kind}.tmp"
|
||||
manifest = {
|
||||
"session_id": session_id,
|
||||
"name": report["name"],
|
||||
"status": report["status"],
|
||||
"verdict": report["verdict"],
|
||||
"screenshots": [name for _, name in images],
|
||||
"videos": [name for _, name in videos],
|
||||
"note": "录屏按时间顺序分段;暂停期间未占用设备,也未录制其他任务。",
|
||||
}
|
||||
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
if kind == "all":
|
||||
for path in export_session(session_id, ["docx", "xlsx"]):
|
||||
archive.write(path, path.name)
|
||||
for source, name in images:
|
||||
archive.write(source, f"步骤截图/{name}")
|
||||
for source, name in videos:
|
||||
archive.write(source, f"任务录屏/{name}")
|
||||
archive.writestr("文件清单.json", json.dumps(manifest, ensure_ascii=False, indent=2))
|
||||
temporary.replace(target)
|
||||
return target
|
||||
|
||||
|
||||
def export_batch(batch_id: str) -> Path:
|
||||
with EXPORT_LOCK:
|
||||
batch = store.get("batch", identifier(batch_id))
|
||||
|
||||
Reference in new issue
Block a user