feat: add favorite task templates and optimize evidence exports
This commit is contained in:
1 parent
e1ea099592
commit
a4a5430922
46 files changed
+1714
-188
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user