180 lines
6.9 KiB
Python
180 lines
6.9 KiB
Python
"""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
|
|
from apps.admin_console.services.task_import import case_title
|
|
|
|
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("任务记录不存在")
|
|
case = job.get("case") or {}
|
|
navigation = None
|
|
if batch_id := job.get("batch_id"):
|
|
jobs = [item for item in store.jobs(batch_id) if item.get("status") in TERMINAL]
|
|
ids = [item["session_id"] for item in jobs]
|
|
index = ids.index(session_id) if session_id in ids else -1
|
|
navigation = {
|
|
"scope": "batch",
|
|
"position": index + 1,
|
|
"total": len(ids),
|
|
"previous": ids[index - 1] if index > 0 else None,
|
|
"next": ids[index + 1] if 0 <= index < len(ids) - 1 else None,
|
|
}
|
|
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,
|
|
"case_title": case_title(case),
|
|
"original_case": case.get("original_case"),
|
|
"navigation": navigation,
|
|
}
|
|
|
|
|
|
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()
|