Files
Artemis-Eapil/apps/admin_console/services/task_extensions.py
T

206 lines
8.0 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 reviewed_batch_sessions(batch_id: str, store: AutomationStore) -> set[str]:
"""Read current human review values once per batch, without loading history."""
manifest = next((item for item in active_manifests() if item.id == "task-review"), None)
if manifest is None or manifest.workspace is None:
return set()
with store.connection() as conn:
rows = conn.execute(
"""SELECT j.session_id, json_extract(r.payload, '$."task-review".values') AS review_values
FROM automation_jobs j JOIN automation_records r
ON r.kind='task_extensions' AND r.id=j.session_id
WHERE j.batch_id=?""",
(batch_id,),
).fetchall()
reviewed = set()
for row in rows:
values = json.loads(row["review_values"] or "{}")
if any(
isinstance(value := values.get(field.id), str)
and value.strip()
and (field.kind == "text" or value in field.options)
for field in manifest.workspace.fields
):
reviewed.add(row["session_id"])
return reviewed
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()