feat(diagnostic): Agent-Historie pro Agent — Akkordeon je Nachricht
Jede Host-Aktion wird als Historie-Eintrag festgehalten und in der Diagnostic pro User-Nachricht gruppiert aufklappbar dargestellt (mit Screenshot-Thumbnails). - Brain: trace_id+msg pro chat(); _dispatch_host als Wrapper um _dispatch_host_inner emittiert je Aktion einen Eintrag an POST /internal/agent-history (Datei-Pfad aus Screenshot-Text erkannt). - Bridge: /internal/agent-history persistiert nach /shared/config/agent_history/ <hostId>.jsonl (Ringpuffer 200) + broadcastet agent_history; agent_history_query -> agent_history_list. - RVS: agent_history/_query/_list in ALLOWED_TYPES (sonst still verworfen). - Diagnostic-Server: relay browser<->RVS + /uploads/<file> Bild-Endpoint (nur /shared/uploads, kein Traversal). - index.html: 'Historie'-Button je Host, Modal mit Akkordeon (Gruppe=Nachricht, aufklappbar, Screenshot-Thumbs), Live-Refresh bei agent_history. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3589,6 +3589,17 @@ class ARIABridge:
|
||||
self._hosts[hid]["last_seen"] = time.time()
|
||||
return
|
||||
|
||||
elif msg_type == "agent_history_query":
|
||||
# Diagnostic fragt die gespeicherte Historie eines Agenten ab.
|
||||
hid = (payload.get("host") or "").strip()
|
||||
events = self._agent_history_read(hid) if hid else []
|
||||
await self._send_to_rvs({
|
||||
"type": "agent_history_list",
|
||||
"payload": {"host": hid, "events": events},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
})
|
||||
return
|
||||
|
||||
elif msg_type == "worker_hello":
|
||||
iid = (payload.get("instanceId") or "").strip()
|
||||
if iid:
|
||||
@@ -4580,6 +4591,17 @@ class ARIABridge:
|
||||
timeout=float(data.get("timeout") or 60.0),
|
||||
)
|
||||
await _send_response(writer, 200, result)
|
||||
elif method == "POST" and path == "/internal/agent-history":
|
||||
# Brain: ein Historie-Eintrag pro Host-Aktion (fuer die Diagnostic).
|
||||
try:
|
||||
data = json.loads(body.decode("utf-8", "ignore"))
|
||||
except Exception as exc:
|
||||
await _send_response(writer, 400, {"error": f"bad json: {exc}"})
|
||||
return
|
||||
self._agent_history_append(data)
|
||||
await self._send_to_rvs({"type": "agent_history", "payload": data,
|
||||
"timestamp": int(time.time() * 1000)})
|
||||
await _send_response(writer, 200, {"ok": True})
|
||||
elif method == "POST" and path == "/internal/satellite":
|
||||
# Brain-Tool: Discovery oder Command an einen Satelliten.
|
||||
# body: {op:'discover'|'command', satellite, device?, action?, params?}
|
||||
@@ -4929,6 +4951,51 @@ class ARIABridge:
|
||||
"online": (now - h.get("last_seen", 0)) < 300,
|
||||
} for h in self._hosts.values()]
|
||||
|
||||
_AGENT_HISTORY_DIR = "/shared/config/agent_history"
|
||||
_AGENT_HISTORY_MAX = 200
|
||||
|
||||
def _agent_history_path(self, host: str) -> str:
|
||||
safe = re.sub(r"[^a-zA-Z0-9_-]+", "-", host).strip("-") or "host"
|
||||
return os.path.join(self._AGENT_HISTORY_DIR, f"{safe}.jsonl")
|
||||
|
||||
def _agent_history_append(self, entry: dict) -> None:
|
||||
"""Haengt einen Historie-Eintrag an <hostId>.jsonl (Ringpuffer)."""
|
||||
host = (entry.get("host") or "").strip()
|
||||
if not host:
|
||||
return
|
||||
try:
|
||||
os.makedirs(self._AGENT_HISTORY_DIR, exist_ok=True)
|
||||
path = self._agent_history_path(host)
|
||||
lines = []
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
lines = f.read().splitlines()
|
||||
lines.append(json.dumps(entry, ensure_ascii=False))
|
||||
lines = lines[-self._AGENT_HISTORY_MAX:]
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
except Exception as exc:
|
||||
logger.warning("[history] append fehlgeschlagen: %s", exc)
|
||||
|
||||
def _agent_history_read(self, host: str, limit: int = 200) -> list:
|
||||
try:
|
||||
path = self._agent_history_path(host)
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
out = []
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
out.append(json.loads(line))
|
||||
except Exception:
|
||||
pass
|
||||
return out[-limit:]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
async def _host_request(self, host: str = "", action: str = "",
|
||||
params: Optional[dict] = None,
|
||||
timeout: float = 60.0) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user