feat: integrate calibration extension and self-repair modules

This commit is contained in:
vee
2026-08-07 23:39:45 +08:00
parent 156b10fda1
commit 3bceb0d5a1
41 changed files with 21310 additions and 314 deletions
+394 -4
View File
@@ -14,7 +14,7 @@ from http.cookies import CookieError, SimpleCookie
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, quote, unquote, urlparse
from urllib.parse import ParseResult, parse_qs, quote, unquote, urlparse
from cognitive_agent.config_loader import load_local_env
@@ -28,6 +28,16 @@ load_local_env(REPOSITORY_ROOT / ".env.auth", override=True)
from cognitive_agent.auth import AuthConfig, AuthRuntime, verify_password # noqa: E402
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402
from cognitive_agent.application.calibration.service import ( # noqa: E402
ApiError as CalibrationApiError,
CalibrationService,
)
from cognitive_agent.application.self_extension import ( # noqa: E402
ExtensionIntegrationError,
GatewayError,
SelfExtensionService,
)
from cognitive_agent.application.self_repair import SelfRepairService # noqa: E402
from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
HardwareBusyError,
HardwareSafetyError,
@@ -40,6 +50,7 @@ from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentErro
STATIC_ROOT = REPOSITORY_ROOT / "apps" / "web-console"
mimetypes.add_type("application/manifest+json", ".webmanifest")
mimetypes.add_type("model/gltf-binary", ".glb")
REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]+$")
PUBLIC_AUTH_PATHS = {
"/login",
@@ -68,6 +79,12 @@ def _lan_ipv4_address() -> str | None:
class SeatDemoHandler(BaseHTTPRequestHandler):
service = SeatDemoService()
gesture = GestureCapabilityBridge(REPOSITORY_ROOT)
extension = SelfExtensionService()
calibration = CalibrationService(
REPOSITORY_ROOT / "artifacts" / "tmp" / "self-calibration",
repository_root=REPOSITORY_ROOT,
)
repair = SelfRepairService()
auth = AuthRuntime(AuthConfig.from_env())
server_version = "CognitiveAgentCabinDemo/2.0"
@@ -188,6 +205,37 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if path == "/api/gesture/model":
self._serve_gesture_model()
return
if path == "/api/extension/health":
self._json(self.extension.health())
return
if path == "/api/extension/gscu/info":
self._handle_extension_gateway("info", {})
return
if path.startswith("/api/calibration/"):
if not self._require_calibration_admin(parsed_url):
return
if path == "/api/calibration/state":
self._json(self.calibration.snapshot())
return
if path == "/api/calibration/events":
self._serve_calibration_events()
return
if path == "/api/calibration/video/stream":
self._serve_calibration_video()
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
if path.startswith("/api/self-repair/"):
if not self._require_self_repair_admin(parsed_url):
return
if path == "/api/self-repair/state":
self._json(self.repair.snapshot())
return
if path == "/api/self-repair/health":
self._json(self.repair.health())
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
if path.startswith("/api/requests/"):
self._serve_request(path.removeprefix("/api/requests/"))
return
@@ -212,6 +260,15 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
}:
self._handle_hardware_access_post(path)
return
if path.startswith("/api/extension/"):
self._handle_extension_post(path)
return
if path.startswith("/api/calibration/"):
self._handle_calibration_post(path)
return
if path.startswith("/api/self-repair/"):
self._handle_self_repair_post(path)
return
if path != "/api/requests":
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
@@ -301,6 +358,322 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
HTTPStatus.INTERNAL_SERVER_ERROR,
)
def _handle_extension_post(self, path: str) -> None:
action_map = {
"/api/extension/gscu/learn": "learn",
"/api/extension/gscu/cancel": "cancel",
"/api/extension/gscu/emit": "emit",
"/api/extension/gscu/read": "read",
"/api/extension/gscu/erase": "erase",
}
try:
payload = self._read_json_body(max_bytes=256 * 1024)
action = action_map.get(path)
if action:
self._handle_extension_gateway(action, payload)
return
if path == "/api/extension/assistant/plan":
self._json(self.extension.plan(payload))
return
if path == "/api/extension/vehicle/execute":
result = self.extension.execute_vehicle(
payload,
self.service,
hardware_access_bypass=self._is_unproxied_loopback_request(),
)
self._json(result)
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
except GatewayError as exc:
self._json(
{"success": False, "error": "gscu_unavailable", "message": str(exc)},
HTTPStatus.SERVICE_UNAVAILABLE,
)
except ExtensionIntegrationError as exc:
self._json(
{"success": False, "error": "extension_unavailable", "message": str(exc)},
HTTPStatus.SERVICE_UNAVAILABLE,
)
except HardwareAccessRequiredError as exc:
self._json(
{
"success": False,
"error": "hardware_access_required",
"message": str(exc),
"access_state": exc.state,
},
HTTPStatus.FORBIDDEN,
)
except HardwareBusyError as exc:
self._json(
{
"success": False,
"error": "hardware_busy",
"message": str(exc),
"retry_after_seconds": exc.retry_after_seconds,
},
HTTPStatus.CONFLICT,
{"Retry-After": str(exc.retry_after_seconds)},
)
except HardwareSafetyError as exc:
self._json(
{"success": False, "error": "hardware_safety_gate", "message": str(exc)},
HTTPStatus.FORBIDDEN,
)
except OpenAIIntentError as exc:
self.log_error("extension vehicle intent request failed: %s", exc)
self._json(
{
"success": False,
"error": "openai_request_failed",
"message": "车端 GPT 意图解析失败,请检查模型配置。",
},
HTTPStatus.BAD_GATEWAY,
)
except (ValueError, json.JSONDecodeError) as exc:
self._json(
{"success": False, "error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
except Exception as exc: # noqa: BLE001 - keep extension APIs responsive
self.log_error("self-extension request failed: %s", exc)
self._json(
{
"success": False,
"error": "internal_error",
"message": "自扩展服务处理失败,请查看服务端日志。",
},
HTTPStatus.INTERNAL_SERVER_ERROR,
)
def _handle_calibration_post(self, path: str) -> None:
if not self._require_calibration_admin():
return
try:
payload = self._read_json_body(max_bytes=256 * 1024)
action_map = {
"/api/calibration/can/connect": lambda: self.calibration.connect_can(
dict(payload.get("can", payload))
),
"/api/calibration/start": lambda: self.calibration.calibrate(
None if payload.get("axis", "all") == "all" else int(payload["axis"])
),
"/api/calibration/move": lambda: self.calibration.move(
int(payload["axis"]), int(payload["permille"])
),
"/api/calibration/seek": lambda: self.calibration.seek(
int(payload["axis"]), bool(payload.get("far"))
),
"/api/calibration/stop": self.calibration.stop,
"/api/calibration/settings": lambda: self.calibration.update_settings(
dict(payload)
),
"/api/calibration/mqtt/connect": self.calibration.connect_mqtt,
"/api/calibration/mqtt/disconnect": self.calibration.disconnect_mqtt,
"/api/calibration/mqtt/test": self.calibration.mqtt_test,
"/api/calibration/video/start": self.calibration.start_video,
"/api/calibration/video/stop": self.calibration.stop_video,
}
action = action_map.get(path)
if action is None:
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
requested_mode = str(
dict(payload.get("can", {})).get(
"mode", self.calibration.snapshot()["transport"]["mode"]
)
)
motion_paths = {
"/api/calibration/can/connect",
"/api/calibration/start",
"/api/calibration/move",
"/api/calibration/seek",
"/api/calibration/stop",
}
if path in motion_paths and requested_mode == "hardware":
if not self._require_local_decision_point():
return
self._json({"ok": True, "state": action()})
except CalibrationApiError as exc:
self._json(
{"ok": False, "error": "calibration_error", "message": str(exc)},
HTTPStatus(exc.status),
)
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
self._json(
{"ok": False, "error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
except Exception as exc: # noqa: BLE001 - keep calibration APIs responsive
self.log_error("self-calibration request failed: %s", exc)
self._json(
{
"ok": False,
"error": "internal_error",
"message": "自标定服务处理失败,请查看服务端日志。",
},
HTTPStatus.INTERNAL_SERVER_ERROR,
)
def _handle_self_repair_post(self, path: str) -> None:
if not self._require_self_repair_admin():
return
try:
payload = self._read_json_body(max_bytes=256 * 1024)
physical_paths = {
"/api/self-repair/hardware/start",
"/api/self-repair/hardware/stop",
"/api/self-repair/emergency-stop",
"/api/self-repair/firmware/build-flash",
"/api/self-repair/open-log",
}
if (
path in physical_paths
or (
path in {
"/api/self-repair/healing/start",
"/api/self-repair/healing/reset",
}
and str(payload.get("execution_mode", "shadow")) == "bench"
)
) and not self._require_local_decision_point():
return
if path == "/api/self-repair/healing/start":
self._json({"ok": True, "healing": self.repair.start_healing(payload)})
return
if path == "/api/self-repair/healing/run-all":
self._json({"ok": True, "batch": self.repair.start_batch(payload)})
return
if path == "/api/self-repair/healing/reset":
self._json({"ok": True, **self.repair.reset_healing(payload)})
return
if path == "/api/self-repair/hardware/start":
self._json({"ok": True, "session": self.repair.start_hardware()})
return
if path == "/api/self-repair/hardware/stop":
self._json({"ok": True, "session": self.repair.stop_hardware()})
return
if path == "/api/self-repair/hardware/self-test":
self._json(
{"ok": True, "self_test": self.repair.run_hardware_self_test()}
)
return
if path == "/api/self-repair/emergency-stop":
self._json({"ok": True, "request": self.repair.emergency_stop()})
return
if path == "/api/self-repair/snapshot":
self._json({"ok": True, "path": str(self.repair.save_snapshot())})
return
if path == "/api/self-repair/firmware/build-flash":
self._json(
{
"ok": True,
"firmware": self.repair.build_and_flash_firmware(payload),
}
)
return
if path == "/api/self-repair/open-log":
opened = self.repair.open_log_dir()
self._json({"ok": opened})
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
self._json(
{"ok": False, "error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
except Exception as exc: # noqa: BLE001 - keep self-repair API responsive
self.log_error("self-repair request failed: %s", exc)
self._json(
{
"ok": False,
"error": "internal_error",
"message": "自修复服务处理失败,请查看服务端日志。",
},
HTTPStatus.INTERNAL_SERVER_ERROR,
)
def _require_self_repair_admin(self, parsed_url: ParseResult | None = None) -> bool:
role = self.headers.get("X-Cognitive-Agent-Role", "").strip().lower()
if not role and parsed_url is not None:
role = str(parse_qs(parsed_url.query).get("role", [""])[0]).lower()
if hmac.compare_digest(role, "admin"):
return True
self._json(
{"error": "admin_required", "message": "自修复仅对 admin 开放。"},
HTTPStatus.FORBIDDEN,
)
return False
def _require_calibration_admin(self, parsed_url: ParseResult | None = None) -> bool:
role = self.headers.get("X-Cognitive-Agent-Role", "").strip().lower()
if not role and parsed_url is not None:
role = str(parse_qs(parsed_url.query).get("role", [""])[0]).lower()
if hmac.compare_digest(role, "admin"):
return True
self._json(
{
"error": "admin_required",
"message": "自标定仅对 admin 开放。",
},
HTTPStatus.FORBIDDEN,
)
return False
def _serve_calibration_events(self) -> None:
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "keep-alive")
self.send_header("X-Accel-Buffering", "no")
self._send_security_headers()
self.end_headers()
version = -1
try:
while True:
state = self.calibration.wait_for_version(version, 12.0)
version = state["version"]
data = json.dumps(state, ensure_ascii=False, separators=(",", ":"))
self.wfile.write(f"data: {data}\n\n".encode("utf-8"))
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
pass
def _serve_calibration_video(self) -> None:
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=n7frame")
self.send_header("Cache-Control", "no-store")
self.send_header("Connection", "close")
self._send_security_headers()
self.end_headers()
sequence = -1
try:
while True:
sequence, frame = self.calibration.video.wait_frame(sequence)
if frame is None:
continue
header = (
b"--n7frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
+ str(len(frame)).encode("ascii")
+ b"\r\n\r\n"
)
self.wfile.write(header + frame + b"\r\n")
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
pass
def _handle_extension_gateway(self, action: str, payload: dict[str, object]) -> None:
try:
self._json(self.extension.execute_gateway(action, dict(payload)))
except GatewayError as exc:
self._json(
{"success": False, "error": "gscu_unavailable", "message": str(exc)},
HTTPStatus.SERVICE_UNAVAILABLE,
)
def _handle_hardware_access_post(self, path: str) -> None:
local_path = path.startswith("/api/local/")
if local_path and not self._require_local_decision_point():
@@ -535,7 +908,18 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
content_type, _ = mimetypes.guess_type(candidate.name)
content = candidate.read_bytes()
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", f"{content_type or 'application/octet-stream'}; charset=utf-8")
resolved_content_type = content_type or "application/octet-stream"
if resolved_content_type.startswith("text/") or resolved_content_type in {
"application/javascript",
"application/json",
"application/manifest+json",
"image/svg+xml",
}:
resolved_content_type = f"{resolved_content_type}; charset=utf-8"
self.send_header(
"Content-Type",
resolved_content_type,
)
self.send_header("Content-Length", str(len(content)))
cache_control = "no-cache"
if relative_path.startswith(("vendor/", "assets/", "icons/")):
@@ -603,8 +987,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
"default-src 'self'; "
"script-src 'self' 'wasm-unsafe-eval'; "
"style-src 'self'; "
"img-src 'self' data:; "
"connect-src 'self'; "
"img-src 'self' data: blob:; "
"connect-src 'self' blob:; "
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
)
if self._is_https():
@@ -620,6 +1004,9 @@ def main() -> None:
parser.add_argument("--port", type=int, default=int(os.getenv("PORT", "8000")))
args = parser.parse_args()
server = ThreadingHTTPServer((args.host, args.port), SeatDemoHandler)
SeatDemoHandler.extension.start()
SeatDemoHandler.calibration.start()
SeatDemoHandler.repair.start()
if args.host in {"0.0.0.0", "::"}:
lan_address = _lan_ipv4_address()
print(f"Cabin orchestration demo (local): http://127.0.0.1:{args.port}", flush=True)
@@ -648,6 +1035,9 @@ def main() -> None:
pass
finally:
server.server_close()
SeatDemoHandler.extension.close()
SeatDemoHandler.calibration.close()
SeatDemoHandler.repair.close()
if __name__ == "__main__":