feat: release 1.5.1 with human review and batch filters
This commit is contained in:
1 parent
53c5680260
commit
bdd1aa2c14
36 files changed
+672
-57
No files matched your search
@@ -149,6 +149,52 @@ class AutomationStore:
|
||||
def preferences(self) -> dict:
|
||||
return self.get("settings", "reports") or {"formats": ["docx", "xlsx"]}
|
||||
|
||||
def save_case_review(self, session_id: str, status: str, reason: str) -> dict:
|
||||
"""Append reviews atomically; invalidate the batch's generated report."""
|
||||
identifier(session_id)
|
||||
if status not in {"passed", "failed"}:
|
||||
raise ValueError("人工复验结果只能为通过或不通过")
|
||||
review = {
|
||||
"status": status,
|
||||
"reason": reason.strip()
|
||||
or ("人工复验确认通过。" if status == "passed" else "人工复验确认不通过。"),
|
||||
"reviewed_at": time.time(),
|
||||
}
|
||||
with self.connection() as conn:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
row = conn.execute(
|
||||
"SELECT payload FROM automation_records WHERE kind='case_review' AND id=?",
|
||||
(session_id,),
|
||||
).fetchone()
|
||||
record = json.loads(row[0]) if row else {"reviews": []}
|
||||
record["reviews"].append(review)
|
||||
conn.execute(
|
||||
"INSERT INTO automation_records VALUES (?,?,?,?) ON CONFLICT(kind,id) DO UPDATE SET payload=excluded.payload,updated_at=excluded.updated_at",
|
||||
(
|
||||
"case_review",
|
||||
session_id,
|
||||
json.dumps(record, ensure_ascii=False),
|
||||
review["reviewed_at"],
|
||||
),
|
||||
)
|
||||
job = conn.execute(
|
||||
"SELECT batch_id FROM automation_jobs WHERE session_id=?", (session_id,)
|
||||
).fetchone()
|
||||
if job and job[0]:
|
||||
batch = conn.execute(
|
||||
"SELECT payload FROM automation_records WHERE kind='batch' AND id=?", (job[0],)
|
||||
).fetchone()
|
||||
if not batch or json.loads(batch[0]).get("status") == "deleting":
|
||||
raise ValueError("批次正在删除,不能保存复验")
|
||||
payload = json.loads(batch[0])
|
||||
payload["report_ready"] = False
|
||||
conn.execute(
|
||||
"UPDATE automation_records SET payload=?,updated_at=? WHERE kind='batch' AND id=?",
|
||||
(json.dumps(payload, ensure_ascii=False), review["reviewed_at"], job[0]),
|
||||
)
|
||||
conn.commit()
|
||||
return review
|
||||
|
||||
def create_batch(self, batch: dict, jobs: list[dict], payload: dict) -> dict:
|
||||
"""Commit a batch and all its jobs atomically; repeated request IDs never duplicate work."""
|
||||
request_id = payload.get("request_id")
|
||||
@@ -247,6 +293,11 @@ class AutomationStore:
|
||||
batch = json.loads(row[0])
|
||||
if batch.get("status") != "deleting":
|
||||
raise ValueError("批次尚未准备删除")
|
||||
conn.execute(
|
||||
"DELETE FROM automation_records WHERE kind='case_review' AND id IN "
|
||||
"(SELECT session_id FROM automation_jobs WHERE batch_id=?)",
|
||||
(batch_id,),
|
||||
)
|
||||
conn.execute("DELETE FROM automation_jobs WHERE batch_id=?", (batch_id,))
|
||||
conn.execute(
|
||||
"DELETE FROM automation_records WHERE kind='batch' AND id=?",
|
||||
@@ -260,9 +311,14 @@ class AutomationStore:
|
||||
with self.connection() as conn:
|
||||
if session_id:
|
||||
conn.execute("DELETE FROM automation_jobs WHERE session_id=?", (session_id,))
|
||||
conn.execute(
|
||||
"DELETE FROM automation_records WHERE kind='case_review' AND id=?",
|
||||
(session_id,),
|
||||
)
|
||||
else:
|
||||
conn.execute("DELETE FROM automation_jobs")
|
||||
conn.execute("DELETE FROM automation_records WHERE kind='batch'")
|
||||
conn.execute("DELETE FROM automation_records WHERE kind='case_review'")
|
||||
conn.commit()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Human review overlays the AI verdict without changing execution evidence."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from apps.admin_console.services.automation_store import AutomationStore
|
||||
from artemis.runtime.case_result import read_case_result
|
||||
|
||||
|
||||
def reviewed_result(
|
||||
session_id: str, ai_result: dict | None, storage: AutomationStore
|
||||
) -> dict | None:
|
||||
original = {**ai_result, "source": "ai"} if ai_result else None
|
||||
record = storage.get("case_review", session_id)
|
||||
reviews = (record or {}).get("reviews") or []
|
||||
if not reviews:
|
||||
return original
|
||||
latest = reviews[-1]
|
||||
return {
|
||||
"status": latest["status"],
|
||||
"reason": latest["reason"],
|
||||
"evidence": [],
|
||||
"source": "human",
|
||||
"reviewed_at": latest["reviewed_at"],
|
||||
"ai_result": original,
|
||||
}
|
||||
|
||||
|
||||
def read_reviewed_result(
|
||||
session_id: str,
|
||||
execution_status: str,
|
||||
error: str = "",
|
||||
*,
|
||||
storage: AutomationStore,
|
||||
root: Path | None = None,
|
||||
) -> dict | None:
|
||||
return reviewed_result(
|
||||
session_id,
|
||||
read_case_result(session_id, execution_status, error, root=root),
|
||||
storage,
|
||||
)
|
||||
@@ -24,6 +24,7 @@ from apps.admin_console.database.repositories.step_repository import StepReposit
|
||||
from apps.admin_console.services.automation_store import store, identifier
|
||||
from apps.admin_console.services.task_import import execution_title
|
||||
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()
|
||||
@@ -65,7 +66,10 @@ def text(value) -> str:
|
||||
def verdict(outcome: dict | None) -> str:
|
||||
result = (outcome or {}).get("test_result")
|
||||
if isinstance(result, dict) and result.get("status") in RESULT_LABELS:
|
||||
return RESULT_LABELS[result["status"]]
|
||||
label = RESULT_LABELS[result["status"]]
|
||||
if result.get("source"):
|
||||
label += "(人工确认)" if result["source"] == "human" else "(AI确认)"
|
||||
return label
|
||||
tests = (outcome or {}).get("tests") or {}
|
||||
if tests.get("failed", 0):
|
||||
return "验证失败"
|
||||
@@ -183,9 +187,10 @@ def session_report(session_id: str, db_path=None) -> dict:
|
||||
case = meta.get("case") or {}
|
||||
status = meta.get("terminal_status") or meta.get("status") or session.get("status", "unknown")
|
||||
error = failure_reason(directory, status, meta.get("error"))
|
||||
test_result = resolve_case_result(outcome, status, error)
|
||||
ai_result = resolve_case_result(outcome, status, error)
|
||||
test_result = reviewed_result(session_id, ai_result.model_dump() if ai_result else None, store)
|
||||
if test_result:
|
||||
outcome = {**outcome, "test_result": test_result.model_dump()}
|
||||
outcome = {**outcome, "test_result": test_result}
|
||||
verification = verdict(outcome)
|
||||
if status in {"failed", "interrupted"} and verification.startswith("未验证"):
|
||||
verification = "未验证(任务异常终止)"
|
||||
@@ -204,7 +209,7 @@ def session_report(session_id: str, db_path=None) -> dict:
|
||||
"expected": case.get("expected", ""),
|
||||
"status": status,
|
||||
"verdict": verification,
|
||||
"test_result": test_result.model_dump() if test_result else None,
|
||||
"test_result": test_result,
|
||||
"outcome": outcome,
|
||||
"content": content,
|
||||
"steps": steps,
|
||||
@@ -241,6 +246,13 @@ def metadata(report: dict) -> list[tuple]:
|
||||
("预期结果", report["expected"] or "未指定"),
|
||||
("执行状态", STATUS.get(report["status"], report["status"])),
|
||||
("验证结论", report["verdict"]),
|
||||
("人工复验时间", time_text((report.get("test_result") or {}).get("reviewed_at"))),
|
||||
(
|
||||
"原 AI 结论",
|
||||
verdict({"test_result": (report.get("test_result") or {}).get("ai_result")})
|
||||
if (report.get("test_result") or {}).get("source") == "human"
|
||||
else "—",
|
||||
),
|
||||
("判定依据", (report.get("test_result") or {}).get("reason") or "未记录"),
|
||||
(
|
||||
"验收证据",
|
||||
|
||||
Reference in new issue
Block a user