"""Human-authored task metadata owned by the host, scoped to installed plugins.""" import hashlib import json import time from functools import lru_cache from apps.admin_console.services.automation_store import AutomationStore, identifier from apps.admin_console.database.repositories.session_repository import SessionRepository from artemis.plugins.models import PluginManifest from artemis.plugins.store import PluginStore TERMINAL = {"completed", "success", "failed", "cancelled", "interrupted", "skipped"} @lru_cache(maxsize=1) def plugin_store() -> PluginStore: return PluginStore() def active_manifests() -> list[PluginManifest]: return [ PluginManifest.model_validate(plugin["manifest"]) for plugin in plugin_store().list() if plugin["enabled"] and plugin["manifest"].get("workspace") ] def installation_signature() -> str: manifests = [manifest.model_dump(mode="json") for manifest in active_manifests()] return ( hashlib.sha256(json.dumps(manifests, sort_keys=True).encode()).hexdigest() if manifests else "" ) def task_context(session_id: str, store: AutomationStore) -> dict: identifier(session_id) session = SessionRepository(store.path).get_session_by_id(session_id) or {} job = store.job(session_id) or {} if not session and not job: raise ValueError("任务记录不存在") return { "session_id": session_id, "initial_prompt": session.get("initial_goal") or (job.get("case") or {}).get("goal", ""), "editable": (job.get("status") or session.get("status")) in TERMINAL, } def contributions(session_id: str, store: AutomationStore) -> list[dict]: record = store.get("task_extensions", session_id) or {} result = [] for manifest in active_manifests(): assert manifest.workspace is not None saved = record.get(manifest.id) or {} values = saved.get("values", {}) result.append( { "plugin_id": manifest.id, "name": manifest.name, "version": manifest.version, "workspace": manifest.workspace.model_dump(), "revision": saved.get("revision", 0), "values": { field.id: values.get(field.id, "") for field in manifest.workspace.fields }, "history": saved.get("history", []), } ) return result def report_sections(session_id: str, store: AutomationStore) -> list[dict]: return [ { "plugin_id": item["plugin_id"], "name": item["name"], "version": item["version"], "revision": item["revision"], "fields": [ {"id": field["id"], "label": field["label"], "value": item["values"][field["id"]]} for field in item["workspace"]["fields"] if field["report"] ], } for item in contributions(session_id, store) if any(field["report"] for field in item["workspace"]["fields"]) ] def save_values( store: AutomationStore, session_id: str, manifest: PluginManifest, values: dict[str, str], expected_revision: int, request_id: str, ) -> None: """CAS + request receipts prevent lost edits and repeated history entries.""" identifier(session_id) assert manifest.workspace is not None fields = {field.id: field for field in manifest.workspace.fields} if values.keys() - fields.keys(): raise ValueError("包含未声明的扩展字段") for key, value in values.items(): field = fields[key] if len(value) > field.max_length or ( field.kind == "choice" and value and value not in field.options ): raise ValueError(f"字段 {field.label} 的内容不符合插件声明") now = time.time() with store.connection() as conn: conn.execute("BEGIN IMMEDIATE") row = conn.execute( "SELECT payload FROM automation_records WHERE kind='task_extensions' AND id=?", (session_id,), ).fetchone() record = json.loads(row[0]) if row else {} saved = record.get(manifest.id, {"revision": 0, "values": {}, "history": []}) for entry in saved["history"]: if entry["request_id"] == request_id: if entry["patch"] != values: raise ValueError("同一保存请求不能提交不同内容") return if saved["revision"] != expected_revision: raise ValueError("评审已在其他页面修改,请重新加载后保存") updated = {**saved["values"], **values} revision = saved["revision"] + 1 saved["history"].append( { "revision": revision, "saved_at": now, "request_id": request_id, "patch": values, "values": updated, "plugin_version": manifest.version, "fields": [field.model_dump() for field in manifest.workspace.fields], } ) record[manifest.id] = {**saved, "revision": revision, "values": updated} conn.execute( "INSERT INTO automation_records VALUES ('task_extensions',?,?,?) " "ON CONFLICT(kind,id) DO UPDATE SET payload=excluded.payload,updated_at=excluded.updated_at", (session_id, json.dumps(record, ensure_ascii=False), now), ) 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), now, job[0]), ) conn.commit()