This commit is contained in:
eric
2026-03-29 00:34:53 -05:00
parent e0b3ac43fc
commit af927c6dca
8 changed files with 1281 additions and 21 deletions

119
web.py
View File

@@ -35,6 +35,14 @@ from src.control_plane import (
system_snapshot,
write_root_file,
)
from src.config_governance import (
delete_managed_file as governed_delete_managed_file,
inspect_file,
list_backups,
optimize_content,
restore_backup,
save_managed_file as governed_save_managed_file,
)
from src.hub_routes import configure_hub, router as hub_router
from src.web_process_backend import WebProcessBackend, force_kill_ffmpeg, strip_ansi_codes
@@ -186,6 +194,35 @@ app.add_middleware(
)
class ProcessNameBody(BaseModel):
process: str = Field(..., min_length=1, max_length=160)
class ServiceActionPostBody(BaseModel):
service: str = Field(..., min_length=1, max_length=64)
action: str = Field(..., pattern="^(start|stop|restart)$")
class ManagedFileBody(BaseModel):
root: str = Field(..., min_length=1, max_length=64)
path: str = Field(..., min_length=1, max_length=240)
content: str = ""
class ConfigAnalyzeBody(ManagedFileBody):
pass
class ConfigOptimizeBody(ManagedFileBody):
action: str = Field(..., min_length=1, max_length=64)
class ConfigRestoreBody(BaseModel):
root: str = Field(..., min_length=1, max_length=64)
path: str = Field(..., min_length=1, max_length=240)
backup_id: str = Field(..., min_length=1, max_length=320)
async def read_log_path(path: Optional[str], lines: int) -> str:
if not path:
return "无日志路径\n"
@@ -370,8 +407,8 @@ async def get_managed_file(
@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", ""))
data = ManagedFileBody(**(await request.json()))
return governed_save_managed_file(data.root, data.path, data.content)
except KeyError as exc:
return JSONResponse({"error": f"Missing field: {exc}"}, status_code=400)
except (PermissionError, ValueError) as exc:
@@ -384,7 +421,7 @@ async def delete_managed_file(
path: str = Query(..., description="相对路径"),
):
try:
return delete_root_file(root, path)
return governed_delete_managed_file(root, path)
except KeyError as exc:
return JSONResponse({"error": str(exc)}, status_code=404)
except FileNotFoundError:
@@ -393,6 +430,65 @@ async def delete_managed_file(
return JSONResponse({"error": str(exc)}, status_code=400)
@app.get("/config_insights")
async def get_config_insights(
root: str = Query(..., description="管理目录 id"),
path: str = Query(..., description="相对路径"),
):
try:
return inspect_file(root, path)
except KeyError as exc:
return JSONResponse({"error": str(exc)}, status_code=404)
except (PermissionError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.post("/config_insights")
async def post_config_insights(body: ConfigAnalyzeBody):
try:
return inspect_file(body.root, body.path, draft_content=body.content)
except KeyError as exc:
return JSONResponse({"error": str(exc)}, status_code=404)
except (PermissionError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.get("/config_backups")
async def get_config_backups(
root: str = Query(..., description="管理目录 id"),
path: str = Query(..., description="相对路径"),
limit: int = Query(12, ge=1, le=50),
):
try:
return {"backups": list_backups(root, path, limit=limit)}
except KeyError as exc:
return JSONResponse({"error": str(exc)}, status_code=404)
except (PermissionError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.post("/config_optimize")
async def post_config_optimize(body: ConfigOptimizeBody):
try:
return optimize_content(body.root, body.path, body.action, body.content)
except KeyError as exc:
return JSONResponse({"error": str(exc)}, status_code=404)
except (PermissionError, ValueError) as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
@app.post("/config_restore")
async def post_config_restore(body: ConfigRestoreBody):
try:
return restore_backup(body.root, body.path, body.backup_id)
except KeyError as exc:
return JSONResponse({"error": str(exc)}, status_code=404)
except FileNotFoundError:
return JSONResponse({"error": f"Backup not found: {body.backup_id}"}, 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)
@@ -436,10 +532,8 @@ async def save_config(request: Request, process: str = Query(..., description="
except Exception:
return JSONResponse({"error": "无效的 JSON 数据"}, status_code=400)
config_file = CONFIG_DIR / "youtube.ini"
try:
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
config_file.write_text(content, encoding="utf-8")
governed_save_managed_file("app-config", "youtube.ini", content)
return {"message": "配置保存成功"}
except OSError as e:
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
@@ -480,24 +574,13 @@ async def save_url_config(request: Request, process: str = Query(..., descriptio
except Exception:
return JSONResponse({"error": "无效的 JSON 数据"}, status_code=400)
config_file = CONFIG_DIR / "URL_config.ini"
try:
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
config_file.write_text(content, encoding="utf-8")
governed_save_managed_file("app-config", "URL_config.ini", content)
return {"message": "配置保存成功"}
except OSError as e:
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
class ProcessNameBody(BaseModel):
process: str = Field(..., min_length=1, max_length=160)
class ServiceActionPostBody(BaseModel):
service: str = Field(..., min_length=1, max_length=64)
action: str = Field(..., pattern="^(start|stop|restart)$")
def _not_found_hint(process: str) -> str:
if process_backend.mode == "local":
return (