feat: 完善模型平台相关功能

This commit is contained in:
Winnie
2026-07-31 19:10:37 +08:00
parent 86988bdaef
commit 49ee2c0a4a
24 changed files with 529 additions and 125 deletions
+4 -1
View File
@@ -1,6 +1,9 @@
FROM python:3.12-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PYTHONPATH=/app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
PATH="/app/.venv/bin:${PATH}"
WORKDIR /app
# 从官方 Rclone 镜像直接复制 rclone 二进制文件(高效、稳定)
COPY --from=rclone/rclone:latest /usr/local/bin/rclone /usr/local/bin/rclone
+27 -11
View File
@@ -21,18 +21,34 @@ REMOTE_BUCKET = settings.remote_bucket
RCLONE_PROCESS: subprocess.Popen | None = None
def is_mountpoint(path: Path) -> bool:
result = subprocess.run(
["mountpoint", "-q", str(path)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return result.returncode == 0
def is_rclone_mount(path: Path) -> bool:
"""Return True only for the FUSE mount created by this service.
Docker volumes and bind mounts are mount points too, so ``mountpoint -q``
cannot distinguish the writable mount target from an active rclone mount.
Linux exposes the filesystem type after `` - `` in ``mountinfo``.
"""
target = str(path.resolve())
try:
lines = Path("/proc/self/mountinfo").read_text().splitlines()
except OSError:
return False
for line in lines:
fields = line.split()
if len(fields) < 10 or fields[4].replace("\\040", " ") != target:
continue
try:
separator = fields.index("-")
except ValueError:
continue
if separator + 1 < len(fields) and fields[separator + 1] == "fuse.rclone":
return True
return False
def start_rclone_mount() -> None:
global RCLONE_PROCESS
if is_mountpoint(WORKSPACES_ROOT):
if is_rclone_mount(WORKSPACES_ROOT):
logger.info(f"Mountpoint already exists: {WORKSPACES_ROOT}")
return
@@ -62,7 +78,7 @@ def start_rclone_mount() -> None:
timeout = 20
while timeout > 0:
if is_mountpoint(WORKSPACES_ROOT):
if is_rclone_mount(WORKSPACES_ROOT):
logger.info(f"rclone mount ready: {WORKSPACES_ROOT}")
return
if RCLONE_PROCESS.poll() is not None:
@@ -85,10 +101,10 @@ def stop_rclone_mount() -> None:
logger.warning("Force killing rclone")
RCLONE_PROCESS.kill()
if is_mountpoint(WORKSPACES_ROOT):
if is_rclone_mount(WORKSPACES_ROOT):
logger.info(f"Unmount {WORKSPACES_ROOT}")
result = subprocess.run(["fusermount3", "-u", str(WORKSPACES_ROOT)])
if result.returncode != 0:
subprocess.run(["umount", "-l", str(WORKSPACES_ROOT)])
logger.info("rclone stopped")
logger.info("rclone stopped")
+9 -3
View File
@@ -13,6 +13,7 @@ import os
import secrets
import subprocess
import time
from pathlib import Path
from typing import TypedDict
from fastapi import HTTPException
@@ -23,6 +24,7 @@ from common.utils import get_free_port, start_process
from runtime.mount import WORKSPACES_ROOT
PUBLIC_BASE_URL = settings.public_base_url
JUPYTER_PROCESS_CWD = Path("/app")
class JupyterProcessRecord(TypedDict):
@@ -106,7 +108,7 @@ async def start_workspace(ws_id: str) -> dict:
"--allow-root",
f"--ServerApp.token={token}",
f"--ServerApp.base_url={base_path}",
"--notebook-dir=.",
f"--ServerApp.root_dir={workspace_path}",
"--ServerApp.terminals_enabled=False",
"--NotebookApp.terminals_enabled=False",
"--ServerApp.allow_origin=*",
@@ -116,7 +118,11 @@ async def start_workspace(ws_id: str) -> dict:
]
try:
process, log_file = start_process(cmd, workspace_path)
# Keep the server process cwd off the rclone/FUSE mount. Remote
# S3 directory refreshes can replace a virtual directory inode,
# leaving a long-lived cwd marked "(deleted)" and making
# os.getcwd() fail while Jupyter starts kernels or nbconvert.
process, log_file = start_process(cmd, JUPYTER_PROCESS_CWD)
except Exception as e:
logger.error(f"Failed to start Jupyter for {ws_id}: {e}")
raise HTTPException(
@@ -264,4 +270,4 @@ async def scan_workspaces() -> None:
except Exception as err:
logger.error(f"Startup failed for workspace '{entry}': {err}")
await asyncio.gather(*[_start(entry) for entry in entries])
await asyncio.gather(*[_start(entry) for entry in entries])