feat: prepare 2.2.0 with UI and execution fixes
Add dark theme, richer Markdown and readable plugin results. Improve Android input evidence, response integrity and diagnostics. Unify lifecycle, guidance receipts and evidence sources across clients. Keep direct action dispatch without an added AI quota approval layer. Align version metadata at 2.2.0 and bundle Helper 1.2.2. Validation: 3063 backend tests before the version bump; 44 after. Frontend: 317 tests and production build passed.
This commit is contained in:
1 parent
83bcfd54d5
commit
fac4ccb760
146 files changed
+5689
-2033
No files matched your search
@@ -175,6 +175,7 @@ async def on_terminal(item: dict, status: str):
|
||||
logger.exception("Report finalization failed for %s", sid)
|
||||
job["report_error"] = str(exc)[:500]
|
||||
job.pop("terminal_status", None)
|
||||
job["finalized_at"] = time.time()
|
||||
store.save_job(job)
|
||||
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ def export_index(
|
||||
"status": report["status"],
|
||||
"verdict": report["verdict"],
|
||||
"steps": report.get("steps", []),
|
||||
"evidence_sources": report.get("evidence_sources"),
|
||||
"calls": calls,
|
||||
"recordings": recordings,
|
||||
"android_videos": ["任务录屏/安卓/" + name for _, name in android_videos],
|
||||
|
||||
@@ -14,6 +14,7 @@ from apps.admin_console.services.export_cache import (
|
||||
reusable,
|
||||
)
|
||||
from apps.admin_console.services.request_diagnostics import diagnostics_json
|
||||
from apps.admin_console.services.task_lifecycle import guidance_details
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT
|
||||
@@ -166,6 +167,7 @@ def failure_reason(directory: Path, status: str, recorded: str | None) -> str:
|
||||
|
||||
def session_report(session_id: str, db_path=None) -> dict:
|
||||
from apps.admin_console.services.exploration_evidence import exploration_metadata
|
||||
from artemis.data_engine.evidence_sources import recorded_sources
|
||||
|
||||
identifier(session_id)
|
||||
meta = store.job(session_id) or {}
|
||||
@@ -213,6 +215,7 @@ def session_report(session_id: str, db_path=None) -> dict:
|
||||
)
|
||||
return {
|
||||
"id": session_id,
|
||||
**guidance_details(session_id, db_path=repo.db_path),
|
||||
"exploration": exploration_metadata(session_id),
|
||||
"name": execution_title(meta) or session.get("initial_goal", session_id)[:120],
|
||||
"iteration": meta.get("iteration", 1),
|
||||
@@ -225,6 +228,7 @@ def session_report(session_id: str, db_path=None) -> dict:
|
||||
"outcome": outcome,
|
||||
"content": content,
|
||||
"steps": steps,
|
||||
"evidence_sources": recorded_sources(steps),
|
||||
"start": session.get("start_time") or meta.get("started_at"),
|
||||
"end": session.get("end_time") or meta.get("ended_at"),
|
||||
"error": error,
|
||||
@@ -246,6 +250,74 @@ def time_text(value) -> str:
|
||||
)
|
||||
|
||||
|
||||
def lifecycle_text(value: dict) -> str:
|
||||
labels = {
|
||||
"total": "总历时",
|
||||
"queued": "后台排队",
|
||||
"resource_wait": "等待设备/资源",
|
||||
"paused": "暂停与示范",
|
||||
"execution": "执行与整理",
|
||||
"final_review": "最终验收",
|
||||
"report_export": "报告导出",
|
||||
}
|
||||
durations = value.get("durations") or {}
|
||||
return (
|
||||
"\n".join(
|
||||
f"{label}:{round(durations[key], 1)} 秒"
|
||||
if durations.get(key) is not None
|
||||
else f"{label}:未记录"
|
||||
for key, label in labels.items()
|
||||
)
|
||||
+ "\n执行含初始化与报告整理,不代表模型速度。"
|
||||
)
|
||||
|
||||
|
||||
def guidance_text(items: list[dict]) -> str:
|
||||
labels = {
|
||||
"pending": "已接收,等待读取",
|
||||
"reading": "执行器正在读取",
|
||||
"consumed": "执行器已读取",
|
||||
"unapplied": "任务结束,未记录消费",
|
||||
"unconfirmed": "任务结束,读取未确认",
|
||||
}
|
||||
return (
|
||||
"\n".join(
|
||||
f"{item['id']} · {labels.get(item['status'], '状态未知')} · 接收 {time_text(item.get('accepted_at') or item.get('timestamp'))}"
|
||||
+ (f" · 读取 {time_text(item['consumed_at'])}" if item.get("consumed_at") else "")
|
||||
for item in items
|
||||
)
|
||||
or "无指导记录"
|
||||
)
|
||||
|
||||
|
||||
def source_text(value: dict) -> str:
|
||||
labels = {
|
||||
"android": "安卓界面",
|
||||
"computer": "电脑界面",
|
||||
"api": "接口数据",
|
||||
"terminal": "终端",
|
||||
"file": "文件",
|
||||
"other": "其他",
|
||||
}
|
||||
lines = []
|
||||
for item in value.get("sources", []):
|
||||
references = ", ".join(
|
||||
" / ".join(
|
||||
f"{key}={ref[key]}" for key in ("step_id", "call_id", "source_id") if ref.get(key)
|
||||
)
|
||||
or "编号未记录"
|
||||
for ref in item.get("references", [])
|
||||
)
|
||||
lines.append(
|
||||
f"{labels.get(item['kind'], '其他')} · {item['label'] or item['target_id']} · {references}"
|
||||
)
|
||||
if value.get("sources_omitted"):
|
||||
lines.append(f"另 {value['sources_omitted']} 个来源见原始步骤记录。")
|
||||
lines.append(f"来源未完整记录的步骤:{value.get('unattributed_step_count', '未知')}。")
|
||||
lines.append(value.get("note") or "来源清单不代表断言已验证。")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def metadata(report: dict) -> list[tuple]:
|
||||
return [
|
||||
("任务编号", report["id"]),
|
||||
@@ -272,6 +344,9 @@ def metadata(report: dict) -> list[tuple]:
|
||||
("任务目标", report["goal"]),
|
||||
("预期结果", report["expected"] or "未指定"),
|
||||
("执行状态", STATUS.get(report["status"], report["status"])),
|
||||
("耗时分项", lifecycle_text(report.get("lifecycle") or {})),
|
||||
("指导消费", guidance_text(report.get("instructions") or [])),
|
||||
("证据来源与边界", source_text(report.get("evidence_sources") or {})),
|
||||
("验证结论", report["verdict"]),
|
||||
("人工复验时间", time_text((report.get("test_result") or {}).get("reviewed_at"))),
|
||||
(
|
||||
@@ -558,7 +633,19 @@ def word_report(report: dict, path: Path):
|
||||
details = [
|
||||
(label, value)
|
||||
for label, value in metadata(report)
|
||||
if label in {"任务编号", "用例编号", "检测项目", "测试子项", "开始时间", "结束时间", "设备"}
|
||||
if label
|
||||
in {
|
||||
"任务编号",
|
||||
"用例编号",
|
||||
"检测项目",
|
||||
"测试子项",
|
||||
"开始时间",
|
||||
"结束时间",
|
||||
"设备",
|
||||
"耗时分项",
|
||||
"指导消费",
|
||||
"证据来源与边界",
|
||||
}
|
||||
]
|
||||
word_table(doc, details, (3.2, 13.3))
|
||||
doc.add_heading("任务目标与预期" if not report["expected"] else "任务目标", 2)
|
||||
|
||||
@@ -62,6 +62,7 @@ def request_diagnostics(
|
||||
field: datetime.fromtimestamp(value[field], UTC).isoformat()
|
||||
for field in (
|
||||
"started_at",
|
||||
"request_sent_at",
|
||||
"headers_at",
|
||||
"first_output_at",
|
||||
"last_output_at",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""One read-only lifecycle view for web, MCP and exported reports."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
from artemis.config import DB_PATH
|
||||
from artemis.runtime.task_control import list_instructions, read_state
|
||||
from artemis.runtime.task_progress import progress_snapshot
|
||||
|
||||
|
||||
def _records(session_id: str, path: Path):
|
||||
if not path.is_file():
|
||||
return None, None, []
|
||||
with sqlite3.connect(path.resolve().as_uri() + "?mode=ro", uri=True) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
tables = {
|
||||
row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
}
|
||||
session = (
|
||||
conn.execute("SELECT * FROM sessions WHERE session_id=?", (session_id,)).fetchone()
|
||||
if "sessions" in tables
|
||||
else None
|
||||
)
|
||||
record = (
|
||||
conn.execute(
|
||||
"SELECT payload FROM automation_jobs WHERE session_id=?", (session_id,)
|
||||
).fetchone()
|
||||
if "automation_jobs" in tables
|
||||
else None
|
||||
)
|
||||
rows = (
|
||||
conn.execute(
|
||||
"SELECT payload FROM traces WHERE session_id=? AND type='final_review_diagnostic'",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
if "traces" in tables
|
||||
else []
|
||||
)
|
||||
return session, record, rows
|
||||
|
||||
|
||||
def task_lifecycle(session_id: str, *, db_path=None, now=None) -> dict:
|
||||
session, record, rows = _records(session_id, Path(db_path) if db_path else DB_PATH)
|
||||
try:
|
||||
job = json.loads(record[0]) if record else {}
|
||||
except (ValueError, TypeError):
|
||||
job = {}
|
||||
reviews = []
|
||||
for row in rows:
|
||||
try:
|
||||
value = json.loads(row[0])
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
reviews.append(value)
|
||||
session = dict(session) if session else {}
|
||||
control = read_state(session_id)
|
||||
if session.get("pid") and control.get("pid") != session["pid"]:
|
||||
control = {}
|
||||
value = progress_snapshot(
|
||||
job=job if isinstance(job, dict) else {},
|
||||
session=session,
|
||||
control=control,
|
||||
reviews=reviews,
|
||||
now=now,
|
||||
)
|
||||
instructions = list_instructions(session_id, task_status=value["status"])
|
||||
value["guidance_counts"] = {
|
||||
status: sum(item["status"] == status for item in instructions)
|
||||
for status in ("pending", "reading", "consumed", "unapplied", "unconfirmed")
|
||||
}
|
||||
return value
|
||||
|
||||
|
||||
def guidance_details(session_id: str, *, db_path=None) -> dict:
|
||||
lifecycle = task_lifecycle(session_id, db_path=db_path)
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"lifecycle": lifecycle,
|
||||
"instructions": list_instructions(session_id, task_status=lifecycle["status"]),
|
||||
}
|
||||
@@ -47,6 +47,7 @@ from artemis.runtime import (
|
||||
request_cancel,
|
||||
trace_store,
|
||||
)
|
||||
from artemis.runtime.cancellation import CancellationReason
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -136,6 +137,7 @@ class TaskQueueService:
|
||||
pid=pid_int,
|
||||
process_created_at=created_at,
|
||||
reason=reason,
|
||||
cancel_reason=CancellationReason.USER_STOP,
|
||||
)
|
||||
if not written:
|
||||
return cls._hard_kill(pid_int, created_at), False
|
||||
@@ -518,7 +520,11 @@ class TaskQueueService:
|
||||
|
||||
job = store.job(str(sess_id)) if sess_id else None
|
||||
if job:
|
||||
job.update(status="running", started_at=time.time())
|
||||
job.update(
|
||||
status="running",
|
||||
started_at=task_item["start_time"],
|
||||
worker_started_at=task_item["start_time"],
|
||||
)
|
||||
store.save_job(job)
|
||||
|
||||
# A fresh launch clears a stale stop request left over for this run
|
||||
|
||||
Reference in new issue
Block a user