This commit is contained in:
eric
2026-03-28 08:39:12 -05:00
parent 4b3c6cbee4
commit b84f9b751c
49 changed files with 3728 additions and 771 deletions

199
web.py
View File

@@ -8,9 +8,30 @@ from typing import Optional
from fastapi import FastAPI, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.responses import FileResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
from src.android_control import (
adb_available,
android_action,
android_device_overview,
android_packages,
android_screenshot,
android_ui_nodes,
list_android_devices,
)
from src.control_plane import (
delete_root_file,
list_root_files,
list_root_summaries,
load_hardware_profile,
query_services,
read_root_file,
service_action,
stack_summary,
system_snapshot,
write_root_file,
)
from src.web_process_backend import WebProcessBackend, force_kill_ffmpeg, strip_ansi_codes
# ---------------- 配置(根目录结构) ----------------
@@ -205,9 +226,159 @@ async def server_info(refresh: bool = Query(False, description="为 true 时重
"docker_compose_default_port": 8101,
"process_dropdown_order": "douyin_youtube → youtube → tiktok → obs → web",
},
"stack": stack_summary(),
}
@app.get("/stack/summary")
async def get_stack_summary():
return stack_summary()
@app.get("/stack/services")
async def get_stack_services():
return {"services": await query_services()}
@app.get("/android/devices")
async def get_android_devices():
return {
"available": adb_available(),
"devices": await list_android_devices(),
}
@app.get("/android/packages")
async def get_android_packages(
serial: str = Query(..., description="Android device serial"),
query: str = Query("", description="Package name filter"),
limit: int = Query(60, ge=1, le=200, description="Maximum number of packages"),
):
try:
return {"packages": await android_packages(serial, query=query, limit=limit)}
except (RuntimeError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.get("/android/ui")
async def get_android_ui(
serial: str = Query(..., description="Android device serial"),
limit: int = Query(80, ge=1, le=200, description="Maximum number of nodes"),
):
try:
return {"nodes": await android_ui_nodes(serial, limit=limit)}
except (RuntimeError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.get("/android/overview")
async def get_android_overview(serial: str = Query(..., description="Android device serial")):
try:
return await android_device_overview(serial)
except (RuntimeError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.get("/android/screenshot")
async def get_android_screenshot(serial: str = Query(..., description="Android device serial")):
try:
content = await android_screenshot(serial)
except (RuntimeError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
return Response(
content=content,
media_type="image/png",
headers={"Cache-Control": "no-store, max-age=0"},
)
@app.post("/android/action")
async def post_android_action(request: Request):
try:
data = await request.json()
action = str(data.get("action", "")).strip()
serial = str(data.get("serial", "")).strip()
payload = data.get("payload", {})
return await android_action(action, serial, payload)
except (RuntimeError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.get("/service_action")
async def run_service_action(
service: str = Query(..., description="服务 id"),
action: str = Query(..., description="start / stop / restart"),
):
try:
return await service_action(service, action)
except (KeyError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.get("/managed_roots")
async def get_managed_roots():
return {"roots": list_root_summaries()}
@app.get("/managed_files")
async def get_managed_files(root: str = Query(..., description="管理目录 id")):
try:
return {"files": list_root_files(root)}
except KeyError as exc:
return JSONResponse({"error": str(exc)}, status_code=404)
@app.get("/managed_file")
async def get_managed_file(
root: str = Query(..., description="管理目录 id"),
path: str = Query(..., description="相对路径"),
):
try:
return read_root_file(root, path)
except KeyError as exc:
return JSONResponse({"error": str(exc)}, status_code=404)
except FileNotFoundError:
return JSONResponse({"error": f"File not found: {path}"}, status_code=404)
except (PermissionError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.post("/managed_file")
async def save_managed_file(request: Request):
try:
data = await request.json()
return write_root_file(data["root"], data["path"], data.get("content", ""))
except KeyError as exc:
return JSONResponse({"error": f"Missing field: {exc}"}, status_code=400)
except (PermissionError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.delete("/managed_file")
async def delete_managed_file(
root: str = Query(..., description="管理目录 id"),
path: str = Query(..., description="相对路径"),
):
try:
return delete_root_file(root, path)
except KeyError as exc:
return JSONResponse({"error": str(exc)}, status_code=404)
except FileNotFoundError:
return JSONResponse({"error": f"File not found: {path}"}, status_code=404)
except (PermissionError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.get("/hardware_profile")
async def hardware_profile(refresh: bool = Query(False, description="重新探测硬件")):
return await load_hardware_profile(force_refresh=refresh)
@app.get("/system_snapshot")
async def get_system_snapshot():
return system_snapshot()
# ---------------- 配置路由youtube.ini仅 youtube ----------------
@app.get("/get_config")
async def get_config(process: str = Query(..., description="进程名")):
@@ -411,6 +582,32 @@ async def index():
return FileResponse(BASE_DIR / "index.html")
def _console_route_html(name: str) -> Optional[Path]:
direct = CONSOLE_OUT / f"{name}.html"
if direct.is_file():
return direct
nested = CONSOLE_OUT / name / "index.html"
if nested.is_file():
return nested
return None
@app.get("/shellcrash", include_in_schema=False)
async def shellcrash_page():
route_file = _console_route_html("shellcrash")
if route_file:
return FileResponse(route_file)
return await index()
@app.get("/android", include_in_schema=False)
async def android_page():
route_file = _console_route_html("android")
if route_file:
return FileResponse(route_file)
return await index()
_next_dir = CONSOLE_OUT / "_next"
if _next_dir.is_dir():
app.mount("/_next", StaticFiles(directory=str(_next_dir)), name="next_static")