wp on webui

This commit is contained in:
2026-02-02 23:37:11 +01:00
parent ce24e0caa0
commit 5e1cb64a81
8 changed files with 278 additions and 99 deletions

View File

@@ -6,6 +6,7 @@ Each session gets its own isolated container with a dedicated working directory.
"""
import os
import re
import uuid
import json
import asyncio
@@ -15,6 +16,7 @@ from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Optional, List
from contextlib import asynccontextmanager
from urllib.parse import urlparse
import docker
from docker.errors import DockerException, NotFound
@@ -358,7 +360,7 @@ class SessionManager:
return session
async def _start_container_async(self, session: SessionData):
"""Start the OpenCode container asynchronously using aiodeocker"""
"""Start the OpenCode container asynchronously using aiodocker"""
try:
# Get and validate resource limits
resource_limits = get_resource_limits()
@@ -378,8 +380,8 @@ class SessionManager:
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""),
"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY", ""),
"GOOGLE_API_KEY": os.getenv("GOOGLE_API_KEY", ""),
# Secure authentication for OpenCode server
"OPENCODE_SERVER_PASSWORD": session.auth_token or "",
# Auth disabled for development - will be added later
# "OPENCODE_SERVER_PASSWORD": session.auth_token or "",
"SESSION_AUTH_TOKEN": session.auth_token or "",
"SESSION_ID": session.session_id,
},
@@ -395,10 +397,8 @@ class SessionManager:
},
)
# For async mode, containers are already started during creation
# For sync mode, we need to explicitly start them
if not self.docker_service.use_async:
await self.docker_service.start_container(container_info.container_id)
# Containers need to be explicitly started after creation
await self.docker_service.start_container(container_info.container_id)
session.container_id = container_info.container_id
session.status = "running"
@@ -458,8 +458,8 @@ class SessionManager:
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""),
"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY", ""),
"GOOGLE_API_KEY": os.getenv("GOOGLE_API_KEY", ""),
# Secure authentication for OpenCode server
"OPENCODE_SERVER_PASSWORD": session.auth_token or "",
# Auth disabled for development - will be added later
# "OPENCODE_SERVER_PASSWORD": session.auth_token or "",
"SESSION_AUTH_TOKEN": session.auth_token or "",
"SESSION_ID": session.session_id,
},
@@ -883,6 +883,118 @@ async def get_session_container_health(session_id: str):
}
# Routes for OpenCode SPA runtime requests
# These paths are hardcoded in the compiled JS and need cookie-based session routing
@app.api_route(
"/global/{path:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
)
async def proxy_global_to_session(request: Request, path: str):
"""Proxy /global/* requests to the current session based on cookie"""
session_id = request.cookies.get("lovdata_session")
if not session_id:
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
# Redirect to session-prefixed path
return await proxy_to_session(request, session_id, f"global/{path}")
@app.api_route(
"/assets/{path:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
)
async def proxy_assets_to_session(request: Request, path: str):
"""Proxy /assets/* requests to the current session based on cookie"""
session_id = request.cookies.get("lovdata_session")
if not session_id:
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
# Redirect to session-prefixed path
return await proxy_to_session(request, session_id, f"assets/{path}")
@app.api_route(
"/provider/{path:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
)
async def proxy_provider_path_to_session(request: Request, path: str):
"""Proxy /provider/* requests to the current session based on cookie"""
session_id = request.cookies.get("lovdata_session")
if not session_id:
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
return await proxy_to_session(request, session_id, f"provider/{path}")
@app.api_route(
"/provider",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
)
async def proxy_provider_to_session(request: Request):
"""Proxy /provider requests to the current session based on cookie"""
session_id = request.cookies.get("lovdata_session")
if not session_id:
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
return await proxy_to_session(request, session_id, "provider")
@app.api_route(
"/project",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
)
async def proxy_project_to_session(request: Request):
"""Proxy /project requests to the current session based on cookie"""
session_id = request.cookies.get("lovdata_session")
if not session_id:
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
return await proxy_to_session(request, session_id, "project")
@app.api_route(
"/path",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
)
async def proxy_path_to_session(request: Request):
"""Proxy /path requests to the current session based on cookie"""
session_id = request.cookies.get("lovdata_session")
if not session_id:
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
return await proxy_to_session(request, session_id, "path")
@app.api_route(
"/find/{path:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
)
async def proxy_find_to_session(request: Request, path: str):
"""Proxy /find/* requests to the current session based on cookie"""
session_id = request.cookies.get("lovdata_session")
if not session_id:
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
return await proxy_to_session(request, session_id, f"find/{path}")
@app.api_route(
"/file",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
)
async def proxy_file_to_session(request: Request):
"""Proxy /file requests to the current session based on cookie"""
session_id = request.cookies.get("lovdata_session")
if not session_id:
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
return await proxy_to_session(request, session_id, "file")
@app.api_route(
"/file/{path:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
)
async def proxy_file_path_to_session(request: Request, path: str):
"""Proxy /file/* requests to the current session based on cookie"""
session_id = request.cookies.get("lovdata_session")
if not session_id:
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
return await proxy_to_session(request, session_id, f"file/{path}")
@app.api_route(
"/session/{session_id}/{path:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
@@ -916,40 +1028,14 @@ async def proxy_to_session(request: Request, session_id: str, path: str):
status_code=404, detail="Session not found or not running"
)
# Dynamically detect the Docker host IP from container perspective
# This supports multiple Docker environments (Docker Desktop, Linux, cloud, etc.)
try:
host_ip = await async_get_host_ip()
logger.info(f"Using detected host IP for proxy: {host_ip}")
except RuntimeError as e:
# Fallback to environment variable or common defaults
host_ip = os.getenv("HOST_IP")
if not host_ip:
# Try common Docker gateway IPs as final fallback
common_gateways = ["172.17.0.1", "192.168.65.1", "host.docker.internal"]
for gateway in common_gateways:
try:
# Test connectivity to gateway
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1.0)
result = sock.connect_ex((gateway, 22))
sock.close()
if result == 0:
host_ip = gateway
logger.warning(f"Using fallback gateway IP: {host_ip}")
break
except Exception:
continue
else:
logger.error(f"Host IP detection failed: {e}")
raise HTTPException(
status_code=500,
detail="Could not determine Docker host IP for proxy routing",
)
container_url = f"http://{host_ip}:{session.port}"
# For DinD architecture: containers run inside docker-daemon with ports mapped
# to docker-daemon's interface, so we proxy through docker-daemon hostname
docker_host = os.getenv("DOCKER_HOST", "http://docker-daemon:2375")
# Extract hostname from DOCKER_HOST (e.g., "http://docker-daemon:2375" -> "docker-daemon")
parsed = urlparse(docker_host)
container_host = parsed.hostname or "docker-daemon"
container_url = f"http://{container_host}:{session.port}"
# Prepare the request URL
url = f"{container_url}/{path}"
@@ -1002,12 +1088,46 @@ async def proxy_to_session(request: Request, session_id: str, path: str):
status_code=response.status_code,
)
# Return the response
return Response(
content=response.content,
# Return the response - inject base tag for HTML to fix asset paths
content = response.content
response_headers = dict(response.headers)
content_type = response.headers.get("content-type", "")
# For HTML responses, rewrite root-relative paths to include session prefix
# OpenCode uses paths like /assets/, /favicon.ico which bypass <base> tag
# We need to prepend the session path to make them work
if "text/html" in content_type:
try:
html = content.decode("utf-8")
session_prefix = f"/session/{session_id}"
# Rewrite src="/..." to src="/session/{id}/..."
html = re.sub(r'src="/', f'src="{session_prefix}/', html)
# Rewrite href="/..." to href="/session/{id}/..."
html = re.sub(r'href="/', f'href="{session_prefix}/', html)
# Rewrite content="/..." for meta tags
html = re.sub(r'content="/', f'content="{session_prefix}/', html)
content = html.encode("utf-8")
response_headers["content-length"] = str(len(content))
except UnicodeDecodeError:
pass # Not valid UTF-8, skip rewriting
# Create response with session tracking cookie
resp = Response(
content=content,
status_code=response.status_code,
headers=dict(response.headers),
headers=response_headers,
)
# Set cookie to track current session for /global/* and /assets/* routing
resp.set_cookie(
key="lovdata_session",
value=session_id,
httponly=True,
samesite="lax",
max_age=86400, # 24 hours
)
return resp
except httpx.TimeoutException as e:
duration_ms = (time.time() - start_time) * 1000