This commit is contained in:
orin
2026-07-30 08:45:38 +08:00
commit 25b24ac4cd
1333 changed files with 170092 additions and 0 deletions
+444
View File
@@ -0,0 +1,444 @@
"""Dependency-light HTTP server for the cabin orchestration demo."""
from __future__ import annotations
import argparse
import hmac
import ipaddress
import json
import mimetypes
import os
import re
import socket
from http.cookies import CookieError, SimpleCookie
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import quote, unquote, urlparse
from cognitive_agent.config_loader import load_local_env
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
load_local_env(REPOSITORY_ROOT / ".env")
load_local_env(REPOSITORY_ROOT / ".env.auth")
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.infrastructure.can.hardware import HardwareSafetyError # noqa: E402
from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentError # noqa: E402
STATIC_ROOT = REPOSITORY_ROOT / "apps" / "web-console"
REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]+$")
PUBLIC_AUTH_PATHS = {
"/login",
"/login.html",
"/login.css",
"/login.js",
"/api/auth/status",
"/api/auth/login",
}
def _lan_ipv4_address() -> str | None:
"""Return the IPv4 address selected by the host's default route."""
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
probe.connect(("192.0.2.1", 80))
address = str(probe.getsockname()[0])
return None if address.startswith("127.") else address
except OSError:
return None
finally:
probe.close()
class SeatDemoHandler(BaseHTTPRequestHandler):
service = SeatDemoService()
gesture = GestureCapabilityBridge(REPOSITORY_ROOT)
auth = AuthRuntime(AuthConfig.from_env())
server_version = "CognitiveAgentCabinDemo/2.0"
def do_GET(self) -> None: # noqa: N802 - stdlib handler API
path = unquote(urlparse(self.path).path)
if path == "/favicon.ico":
self.send_response(HTTPStatus.NO_CONTENT)
self._send_security_headers()
self.end_headers()
return
if path == "/api/auth/status":
username = self._session_username()
self._json(
{
"enabled": self.auth.config.enabled,
"authenticated": not self.auth.config.enabled or username is not None,
"username": username,
"session_ttl_seconds": self.auth.config.session_ttl_seconds,
"requests_per_minute": self.auth.config.requests_per_minute,
}
)
return
if path in {"/login", "/login.html"}:
if self.auth.config.enabled and self._session_username() is not None:
self._redirect("/")
return
self._serve_static_file("login.html")
return
if not self._authorize(path):
return
if path == "/api/health":
self._json({"status": "ok", "service": "cabin-cognitive-orchestrator"})
return
if path == "/api/runtime":
self._json(self.service.runtime_info())
return
if path == "/api/gesture/runtime":
self._json(self.gesture.runtime_info())
return
if path == "/api/gesture/model":
self._serve_gesture_model()
return
if path.startswith("/api/requests/"):
self._serve_request(path.removeprefix("/api/requests/"))
return
self._serve_static(path)
def do_POST(self) -> None: # noqa: N802 - stdlib handler API
path = unquote(urlparse(self.path).path)
if path == "/api/auth/login":
self._login()
return
if path == "/api/auth/logout":
self._logout()
return
if not self._authorize(path):
return
if path != "/api/requests":
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
try:
username = self._session_username() or "local"
retry_after = self.auth.api_requests.retry_after(username)
if self.auth.config.enabled and retry_after:
self._json(
{
"error": "rate_limited",
"message": "请求过于频繁,请稍后再试。",
},
HTTPStatus.TOO_MANY_REQUESTS,
{"Retry-After": str(retry_after)},
)
return
if self.auth.config.enabled:
self.auth.api_requests.record(username)
content_length = int(self.headers.get("Content-Length", "0"))
if content_length <= 0 or content_length > 32768:
raise ValueError("request body must be between 1 and 32768 bytes")
payload = json.loads(self.rfile.read(content_length).decode("utf-8"))
if not isinstance(payload, dict):
raise ValueError("request body must be a JSON object")
result = self.service.process(
str(payload.get("utterance", "")),
str(payload.get("provider", "auto")),
str(payload["conversation_id"]) if payload.get("conversation_id") else None,
str(payload.get("execution_mode", "dry_run")),
str(payload["approval_token"])
if payload.get("approval_token")
else None,
)
self._json(result, HTTPStatus.CREATED)
except HardwareSafetyError as exc:
self._json(
{"error": "hardware_safety_gate", "message": str(exc)},
HTTPStatus.FORBIDDEN,
)
except (ValueError, json.JSONDecodeError) as exc:
self._json({"error": "invalid_request", "message": str(exc)}, HTTPStatus.BAD_REQUEST)
except OpenAIIntentError as exc:
self.log_error("OpenAI intent request failed: %s", exc)
self._json(
{
"error": "openai_request_failed",
"message": "GPT 意图解析失败,请检查 API Key、模型权限或网络状态。",
},
HTTPStatus.BAD_GATEWAY,
)
except Exception as exc: # noqa: BLE001 - keep the demo API responsive
self.log_error("request failed: %s", exc)
self._json(
{
"error": "internal_error",
"message": "请求处理失败,请查看服务端日志。",
},
HTTPStatus.INTERNAL_SERVER_ERROR,
)
def _authorize(self, path: str) -> bool:
if not self.auth.config.enabled or path in PUBLIC_AUTH_PATHS:
return True
if self._session_username() is not None:
return True
if path.startswith("/api/"):
self._json(
{
"error": "authentication_required",
"message": "请登录后访问。",
},
HTTPStatus.UNAUTHORIZED,
)
else:
requested = path if path.startswith("/") else "/"
self._redirect(f"/login?next={quote(requested, safe='/')}")
return False
def _login(self) -> None:
if not self.auth.config.enabled:
self._json({"enabled": False, "authenticated": True})
return
client_key = self._client_key()
retry_after = self.auth.login_failures.retry_after(client_key)
if retry_after:
self._json(
{
"error": "login_rate_limited",
"message": "登录尝试过多,请稍后再试。",
},
HTTPStatus.TOO_MANY_REQUESTS,
{"Retry-After": str(retry_after)},
)
return
try:
content_length = int(self.headers.get("Content-Length", "0"))
if content_length <= 0 or content_length > 4096:
raise ValueError("invalid login body")
payload = json.loads(self.rfile.read(content_length).decode("utf-8"))
if not isinstance(payload, dict):
raise ValueError("invalid login body")
username = str(payload.get("username", ""))[:128]
password = str(payload.get("password", ""))[:1024]
except (ValueError, json.JSONDecodeError):
self._json(
{"error": "invalid_request", "message": "登录请求格式无效。"},
HTTPStatus.BAD_REQUEST,
)
return
password_valid = verify_password(password, self.auth.config.password_hash)
username_valid = hmac.compare_digest(
username.encode("utf-8"),
self.auth.config.username.encode("utf-8"),
)
if not (username_valid and password_valid):
self.auth.login_failures.record(client_key)
self._json(
{"error": "invalid_credentials", "message": "用户名或密码错误。"},
HTTPStatus.UNAUTHORIZED,
)
return
self.auth.login_failures.clear(client_key)
token = self.auth.sessions.issue(self.auth.config.username)
attributes = [
f"{self.auth.config.cookie_name}={token}",
"Path=/",
"HttpOnly",
"SameSite=Strict",
f"Max-Age={self.auth.config.session_ttl_seconds}",
]
if self._is_https():
attributes.append("Secure")
self._json(
{
"authenticated": True,
"username": self.auth.config.username,
},
headers={"Set-Cookie": "; ".join(attributes)},
)
def _logout(self) -> None:
attributes = [
f"{self.auth.config.cookie_name}=",
"Path=/",
"HttpOnly",
"SameSite=Strict",
"Max-Age=0",
]
if self._is_https():
attributes.append("Secure")
self._json(
{"authenticated": False},
headers={"Set-Cookie": "; ".join(attributes)},
)
def _session_username(self) -> str | None:
if not self.auth.config.enabled:
return None
cookie_header = self.headers.get("Cookie", "")
if not cookie_header or len(cookie_header) > 4096:
return None
try:
cookies = SimpleCookie()
cookies.load(cookie_header)
morsel = cookies.get(self.auth.config.cookie_name)
return self.auth.sessions.verify(morsel.value if morsel else "")
except (CookieError, ValueError):
return None
def _client_key(self) -> str:
address = self.client_address[0]
direct_address = ipaddress.ip_address(address)
if self.auth.config.trust_proxy_headers and direct_address.is_loopback:
forwarded = (
self.headers.get("CF-Connecting-IP")
or self.headers.get("X-Forwarded-For", "").split(",", 1)[0]
).strip()
try:
address = str(ipaddress.ip_address(forwarded))
except ValueError:
pass
return address
def _is_https(self) -> bool:
if not self.auth.config.trust_proxy_headers:
return False
if self.headers.get("X-Forwarded-Proto", "").lower() == "https":
return True
return '"scheme":"https"' in self.headers.get("CF-Visitor", "").replace(" ", "")
def _serve_request(self, request_id: str) -> None:
if not REQUEST_ID_PATTERN.fullmatch(request_id):
self._json({"error": "invalid_request_id"}, HTTPStatus.BAD_REQUEST)
return
result_path = self.service.artifact_root / request_id / "result.json"
if not result_path.is_file():
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
self._json(json.loads(result_path.read_text(encoding="utf-8")))
def _serve_static(self, path: str) -> None:
relative_path = "index.html" if path in {"", "/"} else path.lstrip("/")
self._serve_static_file(relative_path)
def _serve_static_file(self, relative_path: str) -> None:
candidate = (STATIC_ROOT / relative_path).resolve()
try:
candidate.relative_to(STATIC_ROOT.resolve())
except ValueError:
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
if not candidate.is_file():
candidate = STATIC_ROOT / "index.html"
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")
self.send_header("Content-Length", str(len(content)))
self.send_header("Cache-Control", "no-store")
self._send_security_headers()
self.end_headers()
self.wfile.write(content)
def _serve_gesture_model(self) -> None:
path = self.gesture.model_path
if not path.is_file():
self._json(
{"error": "gesture_model_unavailable", "message": "手势模型文件不可用。"},
HTTPStatus.NOT_FOUND,
)
return
content = path.read_bytes()
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(content)))
self.send_header("Cache-Control", "private, max-age=3600")
self._send_security_headers()
self.end_headers()
self.wfile.write(content)
def _json(
self,
payload: object,
status: HTTPStatus = HTTPStatus.OK,
headers: dict[str, str] | None = None,
) -> None:
content = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.send_header("Cache-Control", "no-store")
for name, value in (headers or {}).items():
self.send_header(name, value)
self._send_security_headers()
self.end_headers()
self.wfile.write(content)
def _redirect(self, location: str) -> None:
self.send_response(HTTPStatus.SEE_OTHER)
self.send_header("Location", location)
self.send_header("Cache-Control", "no-store")
self._send_security_headers()
self.end_headers()
def _send_security_headers(self) -> None:
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header(
"Permissions-Policy",
"camera=(self), microphone=(), geolocation=(), payment=(), usb=()",
)
self.send_header(
"Content-Security-Policy",
"default-src 'self'; "
"script-src 'self' 'wasm-unsafe-eval' https://unpkg.com https://cdn.jsdelivr.net; "
"style-src 'self'; "
"img-src 'self' data:; "
"connect-src 'self' https://cdn.jsdelivr.net; "
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
)
if self._is_https():
self.send_header(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
)
def main() -> None:
parser = argparse.ArgumentParser(description="Run the cabin orchestration demo")
parser.add_argument("--host", default=os.getenv("HOST", "127.0.0.1"))
parser.add_argument("--port", type=int, default=int(os.getenv("PORT", "8000")))
args = parser.parse_args()
server = ThreadingHTTPServer((args.host, args.port), SeatDemoHandler)
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)
if lan_address:
print(
f"Cabin orchestration demo (LAN): http://{lan_address}:{args.port}",
flush=True,
)
print(
"LAN mode: devices on reachable networks can use the GPT-backed API.",
flush=True,
)
else:
print(f"Cabin orchestration demo: http://{args.host}:{args.port}", flush=True)
hardware = SeatDemoHandler.service.hardware_settings
print(
"Execution modes: dry_run + hardware "
f"({'configured' if hardware.configured else 'locked'})",
flush=True,
)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
if __name__ == "__main__":
main()