diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index a0f9e194..2d7c4a95 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -384,6 +384,9 @@ from src.services.observability_contract_matrix import ( from src.services.offsite_escrow_readiness_status import ( load_latest_offsite_escrow_readiness_status, ) +from src.services.openclaw_live_ops_scene_state import ( + load_openclaw_live_ops_scene_state, +) from src.services.p0_cicd_baseline_source_readiness import ( load_latest_p0_cicd_baseline_source_readiness, ) @@ -889,6 +892,29 @@ async def get_agent_autonomous_runtime_control() -> dict[str, Any]: ) from exc +@router.get( + "/openclaw-live-ops-scene-state", + response_model=dict[str, Any], + summary="取得 OpenClaw Live Ops Space 場景狀態", + description=( + "從 AI Agent autonomous runtime control 的 public-safe trace ledger / " + "work item progress 產生 OpenClaw 持續動畫工作室 scene state。" + "此端點不讀 raw session / SQLite、不讀 secret、不暴露 Telegram 原始 payload、" + "不執行 runtime action、不寫 host/K8s。" + ), +) +async def get_openclaw_live_ops_scene_state() -> dict[str, Any]: + """Return public-safe OpenClaw live ops animation scene state.""" + try: + return await load_openclaw_live_ops_scene_state() + except ValueError as exc: + logger.error("openclaw_live_ops_scene_state_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="OpenClaw Live Ops Space 場景狀態無效", + ) from exc + + @router.get( "/automation-backlog-snapshot", response_model=dict[str, Any], diff --git a/apps/api/src/services/openclaw_live_ops_scene_state.py b/apps/api/src/services/openclaw_live_ops_scene_state.py new file mode 100644 index 00000000..c1e22cce --- /dev/null +++ b/apps/api/src/services/openclaw_live_ops_scene_state.py @@ -0,0 +1,323 @@ +"""OpenClaw Live Ops Space scene state. + +This service maps the public-safe AI Agent autonomous runtime control readback +into a small animation contract. It never reads raw sessions, SQLite, secrets, +or runtime logs, and it never performs runtime actions. +""" + +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any + +from src.services.ai_agent_autonomous_runtime_control import ( + build_ai_agent_autonomous_runtime_control_with_live_readback, +) + +_SCHEMA_VERSION = "openclaw_live_ops_scene_state_v1" +_SOURCE_SCHEMA_VERSION = "ai_agent_autonomous_runtime_control_v1" +_ZONES = [ + { + "zone_id": "control", + "label": "OpenClaw Control", + "kind": "control_plane", + "position": {"x": 48, "y": 38}, + }, + { + "zone_id": "mcp", + "label": "MCP", + "kind": "tool_context", + "position": {"x": 18, "y": 30}, + }, + { + "zone_id": "rag", + "label": "RAG", + "kind": "knowledge_retrieval", + "position": {"x": 24, "y": 62}, + }, + { + "zone_id": "playbook", + "label": "PlayBook", + "kind": "procedure_trust", + "position": {"x": 54, "y": 70}, + }, + { + "zone_id": "verifier", + "label": "Verifier", + "kind": "post_apply_verification", + "position": {"x": 78, "y": 34}, + }, + { + "zone_id": "telegram", + "label": "Telegram Receipt", + "kind": "receipt_delivery", + "position": {"x": 82, "y": 66}, + }, + { + "zone_id": "deploy", + "label": "Deploy Readback", + "kind": "release_truth", + "position": {"x": 48, "y": 18}, + }, +] +_ZONE_BY_STAGE = { + "mcp_context": "mcp", + "service_log_evidence": "rag", + "executor_log_projection": "rag", + "playbook_trust": "playbook", + "post_apply_verifier": "verifier", + "telegram_receipt": "telegram", + "trace_ledger": "control", + "work_item_progress": "control", + "deploy_readback": "deploy", + "km_writeback": "playbook", +} +_ZONE_POSITIONS = {zone["zone_id"]: zone["position"] for zone in _ZONES} + + +async def load_openclaw_live_ops_scene_state() -> dict[str, Any]: + """Build scene state from the live autonomous runtime control readback.""" + runtime_control = await build_ai_agent_autonomous_runtime_control_with_live_readback() + return build_openclaw_live_ops_scene_state(runtime_control) + + +def build_openclaw_live_ops_scene_state( + runtime_control: dict[str, Any], +) -> dict[str, Any]: + """Map autonomous runtime control into the OpenClaw scene contract.""" + if runtime_control.get("schema_version") != _SOURCE_SCHEMA_VERSION: + raise ValueError(f"runtime_control.schema_version must be {_SOURCE_SCHEMA_VERSION}") + + readback = _dict(runtime_control.get("runtime_receipt_readback")) + trace = _dict(readback.get("trace_ledger")) + progress = _dict(readback.get("work_item_progress")) + work_items = _work_items(progress) + agents = _agents(trace, work_items) + source_connected = bool(trace) or bool(work_items) + safety = _dict(trace.get("public_safety")) + payload = { + "schema_version": _SCHEMA_VERSION, + "generated_at": datetime.now(UTC).isoformat(timespec="seconds"), + "status": "live_readback_connected" if source_connected else "waiting_for_live_readback", + "experience": { + "name": "OpenClaw Live Ops Space", + "mode": "continuous_animated_operations_room", + "route": "/zh-TW/openclaw/live-ops-space", + }, + "source": { + "source_endpoint": "/api/v1/agents/agent-autonomous-runtime-control", + "source_schema_version": runtime_control.get("schema_version"), + "deploy_readback_marker": _dict(runtime_control.get("program_status")).get( + "deploy_readback_marker" + ), + "trace_ledger_schema_version": trace.get("schema_version"), + "work_item_progress_schema_version": progress.get("schema_version"), + "live_source_connected": source_connected, + }, + "room": { + "room_id": "openclaw-live-ops", + "layout": "isometric_ops_room", + "zones": _ZONES, + "animation_loop": { + "enabled": True, + "tick_ms": 4200, + "motion_model": "idle_walk_work_wait_verify", + }, + }, + "agents": agents, + "work_items": work_items, + "rollups": { + "zone_count": len(_ZONES), + "agent_count": len(agents), + "work_item_count": len(work_items), + "animated_entity_count": len(agents) + len(work_items), + "completed_work_item_count": sum( + 1 for item in work_items if item.get("status") == "completed" + ), + "blocked_work_item_count": sum( + 1 for item in work_items if item.get("status") == "blocked" + ), + "source_stage_count": _int(trace.get("stage_count")), + "recorded_stage_count": _int(trace.get("recorded_stage_count")), + }, + "boundaries": { + "raw_session_read_allowed": False, + "sqlite_read_allowed": False, + "secret_value_display_allowed": False, + "internal_reasoning_display_allowed": False, + "telegram_unredacted_payload_display_allowed": False, + "runtime_action_performed": False, + "host_or_k8s_write_performed": False, + "uses_public_safe_trace_ledger": safety.get("reads_raw_sessions") is not True, + }, + } + _validate_scene_state(payload) + return payload + + +def _agents(trace: dict[str, Any], work_items: list[dict[str, Any]]) -> list[dict[str, Any]]: + stages = [ + item + for item in _list(trace.get("stages")) + if isinstance(item, dict) and str(item.get("stage_id") or "") + ] + if not stages: + stages = [ + { + "stage_id": "work_item_progress", + "display_name": "Work Item Progress", + "recorded": bool(work_items), + "total": len(work_items), + "recent": 0, + } + ] + agents: list[dict[str, Any]] = [] + for index, stage in enumerate(stages[:8]): + stage_id = str(stage.get("stage_id") or f"stage_{index}") + zone_id = _ZONE_BY_STAGE.get(stage_id, "control") + position = _position_for(zone_id, index) + state = _agent_state(stage) + agents.append( + { + "agent_id": f"openclaw-{stage_id.replace('_', '-')}", + "label": str(stage.get("display_name") or stage_id).strip(), + "zone_id": zone_id, + "state": state, + "animation": _animation_for_state(state, index), + "position": position, + "current_task": { + "stage_id": stage_id, + "recorded": stage.get("recorded") is True, + "total": _int(stage.get("total")), + "recent": _int(stage.get("recent")), + "feeds_learning": stage.get("feeds_learning") is True, + "required_for_closed_loop": ( + stage.get("required_for_closed_loop") is True + ), + }, + } + ) + return agents + + +def _work_items(progress: dict[str, Any]) -> list[dict[str, Any]]: + ordered = [ + item + for item in _list(progress.get("ordered_items")) + if isinstance(item, dict) and str(item.get("work_item_id") or "") + ] + items: list[dict[str, Any]] = [] + for index, item in enumerate(ordered[:12]): + status = str(item.get("status") or "pending") + zone_id = _zone_for_work_item(item) + items.append( + { + "work_item_id": str(item.get("work_item_id") or ""), + "title": str(item.get("title") or item.get("work_item_id") or ""), + "priority": str(item.get("priority") or ""), + "status": status, + "zone_id": zone_id, + "animation": _animation_for_state(_state_from_status(status), index), + "position": _position_for(zone_id, index + 2), + "next_controlled_action": str( + item.get("next_controlled_action") or "" + ), + "exit_criteria": str(item.get("exit_criteria") or ""), + } + ) + return items + + +def _zone_for_work_item(item: dict[str, Any]) -> str: + text = " ".join( + str(item.get(key) or "") + for key in ("work_item_id", "title", "next_controlled_action", "exit_criteria") + ).lower() + if "telegram" in text: + return "telegram" + if "verifier" in text or "verify" in text: + return "verifier" + if "km" in text or "knowledge" in text: + return "playbook" + if "deploy" in text: + return "deploy" + if "mcp" in text: + return "mcp" + if "rag" in text or "log" in text: + return "rag" + return "control" + + +def _position_for(zone_id: str, index: int) -> dict[str, int]: + base = _dict(_ZONE_POSITIONS.get(zone_id)) + x = _int(base.get("x")) + ((index % 3) - 1) * 4 + y = _int(base.get("y")) + ((index % 2) * 4) + return {"x": max(6, min(92, x)), "y": max(8, min(86, y))} + + +def _agent_state(stage: dict[str, Any]) -> str: + if stage.get("recorded") is not True: + return "waiting" + if _int(stage.get("recent")) > 0: + return "working" + if stage.get("feeds_learning") is True: + return "verified" + return "idle" + + +def _state_from_status(status: str) -> str: + if status == "completed": + return "verified" + if status == "blocked": + return "blocked" + if status == "in_progress": + return "working" + return "waiting" + + +def _animation_for_state(state: str, index: int) -> str: + if state == "verified": + return "pulse_verified" + if state == "blocked": + return "waiting_blink" + if state == "working": + return "typing_loop" if index % 2 else "walk_and_work_loop" + if state == "waiting": + return "idle_scan_loop" + return "idle_breathing_loop" + + +def _validate_scene_state(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != _SCHEMA_VERSION: + raise ValueError(f"schema_version must be {_SCHEMA_VERSION}") + if not _list(_dict(payload.get("room")).get("zones")): + raise ValueError("room.zones must be present") + if not _list(payload.get("agents")): + raise ValueError("agents must be present") + boundaries = _dict(payload.get("boundaries")) + for key in ( + "raw_session_read_allowed", + "sqlite_read_allowed", + "secret_value_display_allowed", + "internal_reasoning_display_allowed", + "telegram_unredacted_payload_display_allowed", + "runtime_action_performed", + "host_or_k8s_write_performed", + ): + if boundaries.get(key) is not False: + raise ValueError(f"boundaries.{key} must remain false") + + +def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + +def _int(value: Any) -> int: + try: + return int(value) + except (TypeError, ValueError): + return 0 diff --git a/apps/api/tests/test_credential_escrow_evidence_intake_readiness_api.py b/apps/api/tests/test_credential_escrow_evidence_intake_readiness_api.py index efe39dd5..17d23c4c 100644 --- a/apps/api/tests/test_credential_escrow_evidence_intake_readiness_api.py +++ b/apps/api/tests/test_credential_escrow_evidence_intake_readiness_api.py @@ -9,8 +9,8 @@ from fastapi.testclient import TestClient from src.api.v1.agents import router from src.services.credential_escrow_evidence_intake_readiness import ( load_latest_credential_escrow_evidence_intake_readiness, - validate_credential_escrow_evidence_refs, validate_credential_escrow_evidence_owner_response, + validate_credential_escrow_evidence_refs, ) ESCROW_ITEMS = [ diff --git a/apps/api/tests/test_openclaw_live_ops_scene_state_api.py b/apps/api/tests/test_openclaw_live_ops_scene_state_api.py new file mode 100644 index 00000000..8fa0fc3b --- /dev/null +++ b/apps/api/tests/test_openclaw_live_ops_scene_state_api.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +import src.api.v1.agents as agents_api +from src.api.v1.agents import router +from src.services.openclaw_live_ops_scene_state import ( + build_openclaw_live_ops_scene_state, +) + + +def test_openclaw_live_ops_scene_state_maps_runtime_control_to_animation_contract(): + payload = build_openclaw_live_ops_scene_state(_runtime_control_payload()) + + assert payload["schema_version"] == "openclaw_live_ops_scene_state_v1" + assert payload["status"] == "live_readback_connected" + assert payload["experience"]["name"] == "OpenClaw Live Ops Space" + assert payload["room"]["layout"] == "isometric_ops_room" + assert payload["room"]["animation_loop"]["enabled"] is True + assert payload["source"]["source_endpoint"] == ( + "/api/v1/agents/agent-autonomous-runtime-control" + ) + assert payload["source"]["live_source_connected"] is True + assert payload["rollups"]["zone_count"] >= 6 + assert payload["rollups"]["agent_count"] >= 2 + assert payload["rollups"]["work_item_count"] == 3 + assert payload["rollups"]["animated_entity_count"] >= 5 + assert any(agent["zone_id"] == "mcp" for agent in payload["agents"]) + assert any(agent["zone_id"] == "verifier" for agent in payload["agents"]) + assert any(item["status"] == "blocked" for item in payload["work_items"]) + assert payload["boundaries"]["raw_session_read_allowed"] is False + assert payload["boundaries"]["sqlite_read_allowed"] is False + assert payload["boundaries"]["secret_value_display_allowed"] is False + assert payload["boundaries"]["runtime_action_performed"] is False + + +def test_openclaw_live_ops_scene_state_endpoint_returns_public_safe_scene( + monkeypatch, +): + async def fake_scene_state(): + return build_openclaw_live_ops_scene_state(_runtime_control_payload()) + + monkeypatch.setattr( + agents_api, + "load_openclaw_live_ops_scene_state", + fake_scene_state, + ) + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/openclaw-live-ops-scene-state") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "openclaw_live_ops_scene_state_v1" + assert data["room"]["animation_loop"]["enabled"] is True + assert data["rollups"]["agent_count"] >= 2 + assert data["boundaries"]["telegram_unredacted_payload_display_allowed"] is False + assert data["boundaries"]["host_or_k8s_write_performed"] is False + + +def _runtime_control_payload() -> dict: + return { + "schema_version": "ai_agent_autonomous_runtime_control_v1", + "program_status": { + "deploy_readback_marker": ( + "p2_416_d1n_autonomous_runtime_control_prod_readback_v2" + ) + }, + "runtime_receipt_readback": { + "trace_ledger": { + "schema_version": "ai_agent_autonomous_trace_ledger_v1", + "stage_count": 3, + "recorded_stage_count": 2, + "missing_required_stage_ids": ["telegram_receipt"], + "public_safety": { + "reads_raw_sessions": False, + "stores_secret_values": False, + "stores_unredacted_telegram_payload": False, + "stores_internal_reasoning": False, + }, + "stages": [ + { + "stage_id": "mcp_context", + "display_name": "MCP context", + "recorded": True, + "total": 42, + "recent": 5, + "feeds_learning": True, + "required_for_closed_loop": True, + }, + { + "stage_id": "post_apply_verifier", + "display_name": "Post apply verifier", + "recorded": True, + "total": 18, + "recent": 0, + "feeds_learning": True, + "required_for_closed_loop": True, + }, + { + "stage_id": "telegram_receipt", + "display_name": "Telegram receipt", + "recorded": False, + "total": 0, + "recent": 0, + "feeds_learning": False, + "required_for_closed_loop": True, + }, + ], + }, + "work_item_progress": { + "schema_version": "ai_agent_automation_work_item_progress_v1", + "ordered_items": [ + { + "work_item_id": "P0-006", + "priority": "P0", + "title": "StockPlatform freshness retry readback", + "status": "blocked", + "next_controlled_action": "wait retry then verify", + "exit_criteria": "STOCK_FRESHNESS_STATUS=ok", + }, + { + "work_item_id": "P0-005", + "priority": "P0", + "title": "Credential escrow evidence refs", + "status": "in_progress", + "next_controlled_action": "validate evidence refs", + "exit_criteria": "owner_response_accepted_count=1", + }, + { + "work_item_id": "P0-003", + "priority": "P0", + "title": "Gitea private inventory closeout", + "status": "completed", + "next_controlled_action": "", + "exit_criteria": "active_blocker_count=0", + }, + ], + }, + }, + } diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 302bcb20..15990b63 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1247,7 +1247,45 @@ "statusOk": "正常", "statusWarning": "警告", "messageOk": "所有系統運作正常,無需處理。", - "messageWarning": "{host} 狀態異常,建議檢查相關服務。" + "messageWarning": "{host} 狀態異常,建議檢查相關服務。", + "liveOpsSpace": { + "title": "Live Ops Space", + "status": { + "loading": "Loading scene", + "ready": "Scene connected", + "degraded": "Waiting for readback" + }, + "actions": { + "refresh": "Refresh" + }, + "source": { + "marker": "marker", + "updated": "updated" + }, + "panels": { + "rollups": "Scene metrics", + "boundaries": "Safety boundaries", + "workItems": "Work items" + }, + "metrics": { + "agents": "Agents", + "workItems": "Work items", + "animated": "Animated", + "blocked": "Blocked" + }, + "boundary": { + "closed": "closed", + "open": "open" + }, + "states": { + "working": "working", + "verified": "verified", + "blocked": "blocked", + "waiting": "waiting", + "idle": "idle" + }, + "empty": "Scene state is not available yet." + } }, "ai": { "title": "AI 決策引擎", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 484a5069..b5ab70c6 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1247,7 +1247,45 @@ "statusOk": "正常", "statusWarning": "警告", "messageOk": "所有系統運作正常,無需處理。", - "messageWarning": "{host} 狀態異常,建議檢查相關服務。" + "messageWarning": "{host} 狀態異常,建議檢查相關服務。", + "liveOpsSpace": { + "title": "OpenClaw 持續工作室", + "status": { + "loading": "讀取場景中", + "ready": "場景已連線", + "degraded": "等待讀回" + }, + "actions": { + "refresh": "重新讀取" + }, + "source": { + "marker": "部署 marker", + "updated": "更新" + }, + "panels": { + "rollups": "場景指標", + "boundaries": "安全邊界", + "workItems": "工作項目" + }, + "metrics": { + "agents": "Agent", + "workItems": "工作項", + "animated": "動畫物件", + "blocked": "阻擋" + }, + "boundary": { + "closed": "關閉", + "open": "開啟" + }, + "states": { + "working": "工作中", + "verified": "已驗證", + "blocked": "阻擋", + "waiting": "等待", + "idle": "待命" + }, + "empty": "尚未取得場景狀態。" + } }, "ai": { "title": "AI 決策引擎", diff --git a/apps/web/src/app/[locale]/openclaw/live-ops-space/page.tsx b/apps/web/src/app/[locale]/openclaw/live-ops-space/page.tsx new file mode 100644 index 00000000..76f47e58 --- /dev/null +++ b/apps/web/src/app/[locale]/openclaw/live-ops-space/page.tsx @@ -0,0 +1,466 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useLocale, useTranslations } from "next-intl"; +import { + Activity, + AlertTriangle, + Bot, + BrainCircuit, + CheckCircle2, + MessageSquareText, + RefreshCw, + Rocket, + SearchCheck, + ShieldCheck, + Wrench, +} from "lucide-react"; + +import { AppLayout } from "@/components/layout"; +import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; +import { cn } from "@/lib/utils"; + +type SceneStatus = "loading" | "ready" | "degraded"; + +type SceneZone = { + zone_id: string; + label: string; + kind: string; + position: { x: number; y: number }; +}; + +type SceneAgent = { + agent_id: string; + label: string; + zone_id: string; + state: string; + animation: string; + position: { x: number; y: number }; + current_task?: { + stage_id?: string | null; + recorded?: boolean | null; + total?: number | null; + recent?: number | null; + feeds_learning?: boolean | null; + required_for_closed_loop?: boolean | null; + }; +}; + +type SceneWorkItem = { + work_item_id: string; + title: string; + priority: string; + status: string; + zone_id: string; + animation: string; + position: { x: number; y: number }; + next_controlled_action?: string | null; + exit_criteria?: string | null; +}; + +type SceneState = { + schema_version: "openclaw_live_ops_scene_state_v1"; + generated_at: string; + status: string; + source: { + deploy_readback_marker?: string | null; + live_source_connected?: boolean | null; + }; + room: { + zones: SceneZone[]; + animation_loop: { + enabled: boolean; + tick_ms: number; + motion_model: string; + }; + }; + agents: SceneAgent[]; + work_items: SceneWorkItem[]; + rollups: { + zone_count: number; + agent_count: number; + work_item_count: number; + animated_entity_count: number; + completed_work_item_count: number; + blocked_work_item_count: number; + source_stage_count: number; + recorded_stage_count: number; + }; + boundaries: Record; +}; + +const API_BASE = getRuntimeApiBaseUrl(); + +const zoneIconByKind: Record = { + control_plane: BrainCircuit, + tool_context: Wrench, + knowledge_retrieval: SearchCheck, + procedure_trust: ShieldCheck, + post_apply_verification: CheckCircle2, + receipt_delivery: MessageSquareText, + release_truth: Rocket, +}; + +function stateLabel(t: ReturnType, state: string): string { + if (state === "working") return t("states.working"); + if (state === "verified") return t("states.verified"); + if (state === "blocked") return t("states.blocked"); + if (state === "waiting") return t("states.waiting"); + return t("states.idle"); +} + +function stateClass(state: string): string { + if (state === "working") return "border-[#4a90d9] bg-[#eef5ff] text-[#1f5b9b]"; + if (state === "verified") return "border-[#8fc29a] bg-[#f0faf2] text-[#17602a]"; + if (state === "blocked") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"; + if (state === "waiting") return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"; + return "border-[#d8d3c7] bg-white text-[#5f5b52]"; +} + +function stateFromWorkItemStatus(status: string): string { + if (status === "completed") return "verified"; + if (status === "in_progress") return "working"; + if (status === "blocked") return "blocked"; + return "waiting"; +} + +function shortValue(value?: string | null): string { + if (!value) return "--"; + return value.length > 18 ? `${value.slice(0, 14)}...` : value; +} + +function formatNumber(value: number | null | undefined): string { + return new Intl.NumberFormat("zh-TW").format(Number(value ?? 0)); +} + +export default function OpenClawLiveOpsSpacePage({ + params, +}: { + params: { locale: string }; +}) { + const t = useTranslations("openclaw.liveOpsSpace"); + const locale = useLocale(); + const [scene, setScene] = useState(null); + const [status, setStatus] = useState("loading"); + const [updatedAt, setUpdatedAt] = useState(null); + + const fetchSceneState = useCallback(async () => { + if (!API_BASE) { + setStatus("degraded"); + return; + } + const controller = new AbortController(); + const timeout = window.setTimeout(() => controller.abort(), 12_000); + try { + const response = await fetch( + `${API_BASE}/api/v1/agents/openclaw-live-ops-scene-state`, + { cache: "no-store", signal: controller.signal }, + ); + if (!response.ok) { + setStatus("degraded"); + return; + } + const payload = (await response.json()) as SceneState; + setScene(payload); + setUpdatedAt(new Date()); + setStatus("ready"); + } catch { + setStatus("degraded"); + } finally { + window.clearTimeout(timeout); + } + }, []); + + useEffect(() => { + void fetchSceneState(); + const timer = window.setInterval(fetchSceneState, 15_000); + return () => window.clearInterval(timer); + }, [fetchSceneState]); + + const zones = scene?.room.zones ?? []; + const agents = scene?.agents ?? []; + const workItems = scene?.work_items ?? []; + const boundaryOk = useMemo(() => { + if (!scene) return false; + return [ + "raw_session_read_allowed", + "sqlite_read_allowed", + "secret_value_display_allowed", + "runtime_action_performed", + "host_or_k8s_write_performed", + ].every((key) => scene.boundaries[key] === false); + }, [scene]); + + return ( + +
+
+
+
+
+
+

+ OpenClaw +

+

+ {t("title")} +

+
+
+ + {status === "ready" ? ( + + +
+
+ +
+
+
+ + {zones.map((zone) => { + const Icon = zoneIconByKind[zone.kind] ?? Activity; + return ( +
+ + + + {zone.label} + +
+ ); + })} + + {workItems.map((item, index) => ( +
+ {item.work_item_id} +
+ ))} + + {agents.map((agent, index) => ( +
+
+
+
+ {agent.label} +
+
+ ))} + +
+
+ + {t("source.marker")} {shortValue(scene?.source.deploy_readback_marker)} + + + {t("source.updated")}{" "} + {updatedAt + ? updatedAt.toLocaleTimeString(locale, { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }) + : "--"} + +
+
+
+ + +
+
+
+ +
+ ); +} diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index c0b716eb..554cce94 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -15,6 +15,24 @@ - 沒有讀 secret / token / `.env` / raw sessions / SQLite / auth,沒有寫 credential marker,沒有寫 StockPlatform DB。 - 沒有使用 GitHub / `gh` / GitHub API / GitHub Actions。 +## 2026-06-29 — 21:18 P1 OpenClaw Live Ops Space scene state 與持續動畫工作室 + +**照優先順序推進策略**: +- P0-006 已由上游讀回推進到 StockPlatform freshness / ingestion recovered;目前只剩 fresh all-host reboot window 證明,不應讓下一個可交付 P1 空轉。 +- P1-OPENCLAW-LIVE-OPS-SPACE 依既有 priority work-order 接續實作,不取代 P0 runtime truth,也不切換 OpenClaw 核心。 + +**完成內容**: +- 新增 `openclaw_live_ops_scene_state` 服務與 GET `/api/v1/agents/openclaw-live-ops-scene-state`,把 `agent-autonomous-runtime-control` 的 public-safe `trace_ledger` / `work_item_progress` 投影成 2D isometric scene contract。 +- 新增 `/zh-TW/openclaw/live-ops-space`,顯示 OpenClaw / AI Agent 持續工作室、MCP / RAG / PlayBook / verifier / Telegram receipt / deploy readback zones、Agent avatar、Work Item token 與持續動畫 loop。 +- scene state 明確關閉 raw session、SQLite、secret、Telegram unredacted payload、runtime action、host/K8s write 邊界。 + +**驗證**: +- `pytest apps/api/tests/test_openclaw_live_ops_scene_state_api.py apps/api/tests/test_ai_agent_autonomous_runtime_control.py apps/api/tests/test_credential_escrow_evidence_intake_readiness_api.py apps/api/tests/test_delivery_closure_workbench_api.py apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py`:30 passed。 +- `pnpm --dir apps/web typecheck`、`NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web build`:通過。 +- Playwright smoke(本機 Chrome + API route interception):desktop 1440 / mobile 390 都有動畫、無水平 overflow、截圖非空。 + +**邊界**:未讀 raw sessions / SQLite / auth / `.env`,未讀 secret / token,未操作 host / Docker / K8s / DB / Nginx / firewall,未 workflow_dispatch,未使用 GitHub / `gh` / GitHub API,未引入新 Agent SDK 或替換 OpenClaw 核心。 + ## 2026-06-29 — 20:36 P0-005 non-blocking evidence refs validator **照優先順序修正執行策略**: