feat: add favorite task templates and optimize evidence exports

This commit is contained in:
chenmingxuan committed 2026-10-18 19:44:51 +08:00
1 parent e1ea099592
commit a4a5430922
46 files changed
+1714 -188

No files matched your search

+91 -34
View File
@@ -5,8 +5,14 @@ from typing import Callable, cast
import json
from pathlib import Path
import re
import threading
import zipfile
from apps.admin_console.services.export_cache import (
compression,
export_lock,
fingerprint,
remember,
reusable,
)
from apps.admin_console.services.request_diagnostics import diagnostics_json
from docx import Document
@@ -28,7 +34,7 @@ from artemis.runtime.case_result import RESULT_LABELS, resolve_case_result
from apps.admin_console.services.case_review import reviewed_result
EXPORT_ROOT = WORKSPACE_ROOT / "artifacts" / "reports"
EXPORT_LOCK = threading.RLock()
BATCH_ARCHIVE_SIGNATURE = "artemis-batch-layout-2"
STATUS = {
"completed": "执行完成",
"success": "执行完成",
@@ -741,9 +747,12 @@ def excel_report(report: dict, path: Path):
wb.save(path)
def export_session(session_id: str, formats: list[str] | None = None) -> list[Path]:
with EXPORT_LOCK:
report = session_report(session_id)
def export_session(
session_id: str, formats: list[str] | None = None, *, report: dict | None = None
) -> list[Path]:
with export_lock("session:" + identifier(session_id)):
report = report if report is not None else session_report(session_id)
signature = fingerprint(report, session_screenshots(report))
directory = EXPORT_ROOT / "sessions" / identifier(session_id)
directory.mkdir(parents=True, exist_ok=True)
outputs = []
@@ -751,9 +760,13 @@ def export_session(session_id: str, formats: list[str] | None = None) -> list[Pa
if fmt not in {"docx", "xlsx"}:
raise ValueError("不支持的报告格式")
path = directory / f"任务报告.{fmt}"
if reusable(path, signature, report["status"]):
outputs.append(path)
continue
temporary = directory / f"生成中.{fmt}"
(word_report if fmt == "docx" else excel_report)(report, temporary)
temporary.replace(path)
remember(path, signature)
outputs.append(path)
return outputs
@@ -845,13 +858,19 @@ def session_artifacts(report: dict) -> list[tuple[Path, str]]:
]
def export_session_media(session_id: str, kind: str) -> Path:
def session_download(session_id: str, kind: str) -> tuple[Path, str]:
with export_lock("session:" + identifier(session_id)):
report = session_report(session_id)
return export_session_media(session_id, kind, report=report), archive_case_name(report)
def export_session_media(session_id: str, kind: str, *, report: dict | None = None) -> Path:
from apps.admin_console.services.evidence_service import export_index, plugin_files
if kind not in {"screenshots", "video", "all"}:
raise ValueError("不支持的下载类型")
with EXPORT_LOCK:
report = session_report(session_id)
with export_lock("session:" + identifier(session_id)):
report = report if report is not None else 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 []
@@ -870,42 +889,61 @@ def export_session_media(session_id: str, kind: str) -> Path:
raise ValueError("此任务没有可下载的步骤截图")
target = target_dir / f"{kind}.zip"
temporary = target_dir / f".{kind}.tmp"
files = images + videos + artifacts + extras
diagnostics = diagnostics_json(session_id) if kind == "all" else None
diagnostic_state = json.loads(diagnostics) if diagnostics else None
if isinstance(diagnostic_state, dict):
diagnostic_state.pop("collected_at", None)
signature = fingerprint(report, files, diagnostic_state)
if reusable(target, signature, report["status"]):
return target
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]
"videos": ["任务录屏/安卓/" + name for _, name in videos]
+ [name for path, name in extras if path.suffix.lower() in {".mp4", ".mkv", ".webm"}],
"artifacts": [name for _, name in artifacts],
"note": "录屏按时间顺序分段;暂停期间未占用设备,也未录制其他任务。",
"snapshot": report["status"]
not in {"completed", "success", "failed", "cancelled", "interrupted", "skipped"},
}
prefix = archive_case_name(report) + "/" if kind == "all" else ""
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, prefix + path.name)
for path in export_session(session_id, ["docx", "xlsx"], report=report):
archive.write(path, prefix + path.name, compress_type=compression(path))
for source, name in images:
archive.write(source, f"{prefix}步骤截图/{name}")
archive.write(source, f"{prefix}步骤截图/{name}", compress_type=compression(source))
for source, name in videos:
archive.write(source, f"{prefix}任务录屏/{name}", compress_type=zipfile.ZIP_STORED)
archive.write(
source, f"{prefix}任务录屏/安卓/{name}", compress_type=zipfile.ZIP_STORED
)
for source, name in artifacts:
archive.write(source, f"{prefix}任务文件/安卓/{name}")
archive.write(
source, f"{prefix}任务文件/安卓/{name}", compress_type=compression(source)
)
for source, name in extras:
archive.write(
source,
prefix + name,
compress_type=zipfile.ZIP_STORED
if source.suffix.lower() in {".mp4", ".mkv", ".webm"}
else zipfile.ZIP_DEFLATED,
compress_type=compression(source),
)
if kind == "all":
archive.writestr(prefix + "模型请求诊断.json", diagnostics_json(session_id))
archive.writestr(prefix + "模型请求诊断.json", diagnostics or "{}")
archive.writestr(
prefix + "证据索引.json",
json.dumps(
export_index(session_id, report, videos),
export_index(
session_id,
report,
videos,
images=images,
artifacts=artifacts,
extras=extras,
),
ensure_ascii=False,
indent=2,
default=str,
@@ -915,13 +953,18 @@ def export_session_media(session_id: str, kind: str) -> Path:
prefix + "文件清单.json", json.dumps(manifest, ensure_ascii=False, indent=2)
)
temporary.replace(target)
if fingerprint(report, files, diagnostic_state) == signature:
remember(target, signature)
return target
def export_batch(batch_id: str, progress: Callable[[int, int, str], None] | None = None) -> Path:
from apps.admin_console.services.evidence_service import export_index, plugin_files
with EXPORT_LOCK:
with export_lock(
"batch:" + identifier(batch_id),
*("session:" + job["session_id"] for job in store.jobs(batch_id)),
):
batch = store.get("batch", identifier(batch_id))
if not batch:
raise ValueError("批次不存在")
@@ -959,34 +1002,47 @@ def export_batch(batch_id: str, progress: Callable[[int, int, str], None] | None
temporary = directory / "批次报告.tmp"
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED) as archive:
for path in outputs:
archive.write(path, path.name)
archive.write(path, path.name, compress_type=compression(path))
for number, report in enumerate(records, 1):
if progress:
progress(number - 1, len(records), f"正在打包任务 {number}/{len(records)}")
prefix = f"任务报告/{number:04d}_{archive_case_name(report)}"
for path in export_session(report["id"], formats):
archive.write(path, f"{prefix}/{path.name}")
for source, name in session_screenshots(report):
archive.write(source, f"{prefix}/步骤截图/{name}")
for source, name in session_videos(report):
images = session_screenshots(report)
videos = session_videos(report)
artifacts = session_artifacts(report)
extras = plugin_files(report["id"])
for path in export_session(report["id"], formats, report=report):
archive.write(path, f"{prefix}/{path.name}", compress_type=compression(path))
for source, name in images:
archive.write(
source, f"{prefix}/任务录屏/{name}", compress_type=zipfile.ZIP_STORED
source, f"{prefix}/步骤截图/{name}", compress_type=compression(source)
)
for source, name in session_artifacts(report):
archive.write(source, f"{prefix}/任务文件/安卓/{name}")
for source, name in plugin_files(report["id"]):
for source, name in videos:
archive.write(
source, f"{prefix}/任务录屏/安卓/{name}", compress_type=zipfile.ZIP_STORED
)
for source, name in artifacts:
archive.write(
source, f"{prefix}/任务文件/安卓/{name}", compress_type=compression(source)
)
for source, name in extras:
archive.write(
source,
f"{prefix}/{name}",
compress_type=zipfile.ZIP_STORED
if source.suffix.lower() in {".mp4", ".mkv", ".webm"}
else zipfile.ZIP_DEFLATED,
compress_type=compression(source),
)
archive.writestr(f"{prefix}/模型请求诊断.json", diagnostics_json(report["id"]))
archive.writestr(
f"{prefix}/证据索引.json",
json.dumps(
export_index(report["id"], report, session_videos(report)),
export_index(
report["id"],
report,
videos,
images=images,
artifacts=artifacts,
extras=extras,
),
ensure_ascii=False,
indent=2,
default=str,
@@ -997,4 +1053,5 @@ def export_batch(batch_id: str, progress: Callable[[int, int, str], None] | None
if progress:
progress(len(records), len(records), "正在完成压缩包")
temporary.replace(target)
remember(target, BATCH_ARCHIVE_SIGNATURE)
return target