's'
This commit is contained in:
199
web.py
199
web.py
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user