feat(openclaw): add live ops space scene state
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 1m7s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 1m7s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
This commit is contained in:
@@ -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],
|
||||
|
||||
323
apps/api/src/services/openclaw_live_ops_scene_state.py
Normal file
323
apps/api/src/services/openclaw_live_ops_scene_state.py
Normal file
@@ -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
|
||||
@@ -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 = [
|
||||
|
||||
144
apps/api/tests/test_openclaw_live_ops_scene_state_api.py
Normal file
144
apps/api/tests/test_openclaw_live_ops_scene_state_api.py
Normal file
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 決策引擎",
|
||||
|
||||
@@ -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 決策引擎",
|
||||
|
||||
466
apps/web/src/app/[locale]/openclaw/live-ops-space/page.tsx
Normal file
466
apps/web/src/app/[locale]/openclaw/live-ops-space/page.tsx
Normal file
@@ -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<string, boolean>;
|
||||
};
|
||||
|
||||
const API_BASE = getRuntimeApiBaseUrl();
|
||||
|
||||
const zoneIconByKind: Record<string, typeof Activity> = {
|
||||
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<typeof useTranslations>, 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<SceneState | null>(null);
|
||||
const [status, setStatus] = useState<SceneStatus>("loading");
|
||||
const [updatedAt, setUpdatedAt] = useState<Date | null>(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 (
|
||||
<AppLayout locale={params.locale} fullBleed>
|
||||
<main className="min-h-screen overflow-x-hidden bg-[#f7f8f7] text-[#141413]">
|
||||
<div className="mx-auto flex w-full max-w-[1520px] flex-col gap-4 px-3 py-4 lg:px-5">
|
||||
<section className="grid min-w-0 items-start gap-4 lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="min-w-0 border border-[#d8d3c7] bg-white">
|
||||
<div className="flex min-w-0 flex-wrap items-center justify-between gap-3 border-b border-[#e5e1d8] px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase text-[#77736a]">
|
||||
OpenClaw
|
||||
</p>
|
||||
<h1 className="mt-1 break-words text-2xl font-semibold leading-tight text-[#141413] sm:text-3xl">
|
||||
{t("title")}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 border px-3 py-1.5 text-xs font-semibold",
|
||||
status === "ready"
|
||||
? "border-[#8fc29a] bg-[#f0faf2] text-[#17602a]"
|
||||
: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
|
||||
)}
|
||||
>
|
||||
{status === "ready" ? (
|
||||
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
{status === "ready"
|
||||
? t("status.ready")
|
||||
: status === "loading"
|
||||
? t("status.loading")
|
||||
: t("status.degraded")}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void fetchSceneState()}
|
||||
className="inline-flex h-9 w-9 items-center justify-center border border-[#d8d3c7] bg-white text-[#4f4b44] transition hover:border-[#4a90d9] hover:text-[#1f5b9b]"
|
||||
title={t("actions.refresh")}
|
||||
aria-label={t("actions.refresh")}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative aspect-[16/10] min-h-[360px] overflow-hidden bg-[#eef3f8] sm:min-h-[500px]">
|
||||
<div className="absolute inset-x-[5%] bottom-[7%] top-[8%] rotate-[-2deg] skew-y-[-7deg] border border-[#c8d6df] bg-[#fdfefe] shadow-[0_28px_70px_rgba(54,72,88,0.16)]" />
|
||||
<div className="absolute inset-x-[7%] bottom-[11%] top-[12%] rotate-[-2deg] skew-y-[-7deg] bg-[linear-gradient(90deg,rgba(74,144,217,0.12)_1px,transparent_1px),linear-gradient(0deg,rgba(74,144,217,0.1)_1px,transparent_1px)] bg-[size:42px_42px]" />
|
||||
|
||||
{zones.map((zone) => {
|
||||
const Icon = zoneIconByKind[zone.kind] ?? Activity;
|
||||
return (
|
||||
<div
|
||||
key={zone.zone_id}
|
||||
className="absolute flex h-14 w-28 -translate-x-1/2 -translate-y-1/2 rotate-[-2deg] items-center gap-2 border border-[#cbd4d8] bg-white/90 px-2 shadow-[0_10px_24px_rgba(54,72,88,0.12)] backdrop-blur"
|
||||
style={{ left: `${zone.position.x}%`, top: `${zone.position.y}%` }}
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center border border-[#d8d3c7] bg-[#f7f8f7] text-[#4a90d9]">
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-[11px] font-semibold text-[#34302a]">
|
||||
{zone.label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{workItems.map((item, index) => (
|
||||
<div
|
||||
key={item.work_item_id}
|
||||
className={cn(
|
||||
"openclaw-work-token absolute h-8 w-20 -translate-x-1/2 -translate-y-1/2 border px-2 py-1 text-[10px] font-semibold shadow-[0_8px_20px_rgba(54,72,88,0.16)]",
|
||||
stateClass(stateFromWorkItemStatus(item.status)),
|
||||
)}
|
||||
style={{
|
||||
left: `${item.position.x}%`,
|
||||
top: `${item.position.y}%`,
|
||||
animationDelay: `${index * 0.35}s`,
|
||||
}}
|
||||
>
|
||||
<span className="block truncate">{item.work_item_id}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{agents.map((agent, index) => (
|
||||
<div
|
||||
key={agent.agent_id}
|
||||
className="openclaw-agent absolute -translate-x-1/2 -translate-y-1/2"
|
||||
style={{
|
||||
left: `${agent.position.x}%`,
|
||||
top: `${agent.position.y}%`,
|
||||
animationDelay: `${index * 0.28}s`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-14 w-14 items-center justify-center border-2 bg-white shadow-[0_16px_30px_rgba(54,72,88,0.2)]",
|
||||
stateClass(agent.state),
|
||||
)}
|
||||
>
|
||||
<Bot className="h-6 w-6" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-1 max-w-28 truncate border border-[#d8d3c7] bg-white px-1.5 py-0.5 text-center text-[10px] font-semibold text-[#34302a] shadow-sm">
|
||||
{agent.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(145deg,rgba(255,255,255,0.18),transparent_45%,rgba(20,20,19,0.04))]" />
|
||||
<div className="absolute bottom-3 left-3 flex flex-wrap gap-2">
|
||||
<span className="border border-[#d8d3c7] bg-white/90 px-2 py-1 font-mono text-[11px] text-[#5f5b52]">
|
||||
{t("source.marker")} {shortValue(scene?.source.deploy_readback_marker)}
|
||||
</span>
|
||||
<span className="border border-[#d8d3c7] bg-white/90 px-2 py-1 font-mono text-[11px] text-[#5f5b52]">
|
||||
{t("source.updated")}{" "}
|
||||
{updatedAt
|
||||
? updatedAt.toLocaleTimeString(locale, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})
|
||||
: "--"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="grid min-w-0 gap-3">
|
||||
<section className="border border-[#d8d3c7] bg-white p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-base font-semibold text-[#141413]">
|
||||
{t("panels.rollups")}
|
||||
</h2>
|
||||
<Activity className="h-4 w-4 text-[#4a90d9]" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
{[
|
||||
["agents", scene?.rollups.agent_count],
|
||||
["workItems", scene?.rollups.work_item_count],
|
||||
["animated", scene?.rollups.animated_entity_count],
|
||||
["blocked", scene?.rollups.blocked_work_item_count],
|
||||
].map(([key, value]) => (
|
||||
<div key={String(key)} className="border border-[#e5e1d8] bg-[#fafafa] p-3">
|
||||
<p className="text-[11px] font-semibold text-[#77736a]">
|
||||
{t(`metrics.${key}`)}
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-2xl font-semibold text-[#141413]">
|
||||
{formatNumber(Number(value ?? 0))}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border border-[#d8d3c7] bg-white p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-base font-semibold text-[#141413]">
|
||||
{t("panels.boundaries")}
|
||||
</h2>
|
||||
<ShieldCheck
|
||||
className={cn("h-4 w-4", boundaryOk ? "text-[#17602a]" : "text-[#8a5a08]")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2">
|
||||
{[
|
||||
"raw_session_read_allowed",
|
||||
"sqlite_read_allowed",
|
||||
"secret_value_display_allowed",
|
||||
"runtime_action_performed",
|
||||
"host_or_k8s_write_performed",
|
||||
].map((key) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex min-w-0 items-center justify-between gap-3 border border-[#e5e1d8] px-3 py-2"
|
||||
>
|
||||
<span className="truncate font-mono text-[11px] text-[#5f5b52]">
|
||||
{key}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"border px-2 py-0.5 text-[10px] font-semibold",
|
||||
scene?.boundaries[key] === false
|
||||
? "border-[#8fc29a] bg-[#f0faf2] text-[#17602a]"
|
||||
: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
|
||||
)}
|
||||
>
|
||||
{scene?.boundaries[key] === false ? t("boundary.closed") : t("boundary.open")}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="min-h-[260px] border border-[#d8d3c7] bg-white p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-base font-semibold text-[#141413]">
|
||||
{t("panels.workItems")}
|
||||
</h2>
|
||||
<SearchCheck className="h-4 w-4 text-[#4a90d9]" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2">
|
||||
{workItems.length === 0 ? (
|
||||
<div className="border border-[#e5e1d8] bg-[#fafafa] p-4 text-sm text-[#77736a]">
|
||||
{t("empty")}
|
||||
</div>
|
||||
) : (
|
||||
workItems.slice(0, 6).map((item) => (
|
||||
<div key={item.work_item_id} className="border border-[#e5e1d8] p-3">
|
||||
<div className="flex min-w-0 items-center justify-between gap-2">
|
||||
<span className="min-w-0 truncate text-sm font-semibold text-[#141413]">
|
||||
{item.title || item.work_item_id}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 border px-2 py-0.5 text-[10px] font-semibold",
|
||||
stateClass(stateFromWorkItemStatus(item.status)),
|
||||
)}
|
||||
>
|
||||
{stateLabel(t, stateFromWorkItemStatus(item.status))}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 truncate font-mono text-[11px] text-[#77736a]">
|
||||
{item.next_controlled_action || item.exit_criteria || "--"}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<style jsx global>{`
|
||||
@keyframes openclaw-agent-drift {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%, -50%) translate3d(0, 0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) translate3d(5px, -7px, 0);
|
||||
}
|
||||
}
|
||||
@keyframes openclaw-work-token-flow {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%, -50%) translate3d(0, 0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) translate3d(9px, 4px, 0);
|
||||
}
|
||||
}
|
||||
.openclaw-agent {
|
||||
animation: openclaw-agent-drift 4.8s ease-in-out infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
.openclaw-work-token {
|
||||
animation: openclaw-work-token-flow 6.2s ease-in-out infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.openclaw-agent,
|
||||
.openclaw-work-token {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
**照優先順序修正執行策略**:
|
||||
|
||||
Reference in New Issue
Block a user