# Model Platform 自托管的 **Jupyter 模型开发平台**:交互式 workspace、DAG 调度、对象存储工件、 按 workspace 隔离的运行时 — 全部经一个 Nginx 网关对外。 > 技术栈:React Router SPA · FastAPI · APScheduler · MySQL · S3 兼容存储 > (或本地文件系统,`STORAGE_BACKEND=local`)· 共享 Jupyter · rclone FUSE 挂载(仅 s3 模式) > 单一入口(Nginx :80);其他服务只在 Docker 内网互通。 ## 它做什么 | 能力 | 位置 | |---|---| | workspace 内 notebook 编辑,行级锁 | `backend/jupyter.py` + `scripts.is_locked` | | Jupyter 鉴权路由(浏览器永远拿不到 runtime token) | `nginx/default.conf` + `auth_request` + `backend/jupyter.py` | | notebook / script / version / run_log 的对象存储(s3 / local 二选一) | `common/storage/` + `backend/scripts.py` | | DAG 调度:节点、边、cron、手动触发、重试、快照 | `backend/schedules.py` + `backend/schedule_runs.py` + `schedule/`(5 个模块) | | DAG 执行走 MySQL Outbox(无 Redis,无进程内队列) | `schedule/orchestrator.py` + `schedule/worker.py` | | 每个 workspace 一个 Jupyter 子进程池,配 asyncio 锁 | `runtime/process.py` | | runtime 内 rclone FUSE 把 workspace 桶挂上来(s3 模式) | `runtime/mount.py` | | 仅 MySQL 持久化(26 张表,软删除,无外键) | `common/db/models/` | ## 架构一览 ``` ┌────────────────────┐ │ Browser (SPA) │ └─────────┬──────────┘ │ HTTPS / WS ┌─────────▼──────────┐ │ Nginx (only :80) │ ← templates/default.conf │ /api/ /jupyter/ /storage/ └────┬───────┬──────┘ │ │ ┌──────────────┘ └─────────────┐ ▼ ▼ ┌──────────────────┐ ┌──────────────────────┐ │ FastAPI Backend │ │ Runtime (Jupyter) │ │ + /internal/v1 │ control │ - rclone FUSE mount │ (P0-1) │ /objects │ token-Auth │ │ │ (storage) ├──────────────►│ - subprocess pool │ │ - DAG CRUD │ │ (per workspace) │ │ - script CRUD │ └──────────┬───────────┘ │ - auth_request │ │ FUSE / shared vol │ - /api/v1/... │ ▼ └────┬──────┬──────┘ ┌──────────────────────┐ │ │ │ Object storage │ │ └──────── HTTP ───────►│ (s3: S3 service / │ ▼ │ local: shared vol) │ ┌────────────┐ │ 4 buckets per usage │ │ MySQL │◄───────── poll ─────│ │ │ - 26 tbls │ └──────────────────────┘ │ - outbox │ │ - jobstore │ └────┬───────┘ ▲ │ outbox poll ┌────┴──────────────────────────┐ │ Schedule Executor │ │ - CronScheduler (APScheduler) │ │ - DispatchOrchestrator │ │ - NodeExecutor (worker) │ │ - SchedulerService (facade) │ └───────────────────────────────┘ ``` 对象存储通过 `STORAGE_BACKEND`(s3 | local)二选一。s3 模式下 4 个 purpose 命名桶 (`workspace` / `version` / `run-log` / `trash`)是独立的 S3 bucket;local 模式下 是 `LOCAL_STORAGE_BASE_DIR` 的子目录,通过 Docker volume `local-storage` 共享。 详见 `DEVELOP.md` §存储。 详细设计见 `ARCHITECTURE.md`。实现的偏离和近期重构记录在 `HANDOVER.md`。 ## 目录结构 ```text frontend/ React Router SPA backend/ FastAPI:公开 API + 内部存储 API runtime/ Jupyter 子进程管理 + rclone FUSE schedule/ DAG 调度器(5 模块:context/scheduler/ orchestrator/worker/service) common/ 配置、SQLAlchemy 模型、存储 SDK、 outbox 事件、jobstore migrations/ Alembic 基线 + 各特性 migration nginx/ (仅概念 — 见下方「容器」一节) scripts/ nginx-entrypoint.sh(模板渲染) docker-compose.yml 4 服务 — web / backend / runtime / schedule default.conf Nginx 模板(挂载,启动时渲染) .env.example common.config.Settings 消费的所有环境变量 ``` ## 容器 | 服务 | 镜像 | 暴露 | 用途 | |---|---|---|---| | `web` | `nginx:alpine` | 宿主机 `:8888` → `:80` | SPA、`/api/` 反向代理、`/jupyter/{ws}/` auth_request 代理、`/storage/` S3 直通(仅 s3 模式) | | `backend` | `Dockerfile` | 仅内网 | DAG CRUD、script CRUD、schedule 触发、`/api/v1/auth/jupyter`、`/internal/v1/objects` 服务间 RPC(共享 `INTERNAL_SERVICE_TOKEN` 鉴权,P0-1)| | `runtime` | `Dockerfile` | 仅内网 | 每个 workspace 一个 Jupyter 子进程池、rclone FUSE 挂载 `workspace` 桶(s3 模式) | | `schedule` | `Dockerfile` | 仅内网 | cron tick + DAG 执行(轮询 MySQL Outbox) | 架构**故意只暴露一个宿主机端口**(网关);其他服务都在 Docker 内网。 这一点在 `docker-compose.yml` 里强制执行 — backend / runtime / schedule 都没有 `ports:`。在 P0-1 之前,backend 与 runtime 曾短暂地把 `8891` / `8892` 映射到宿主机;此映射已被删除,改用 `INTERNAL_SERVICE_TOKEN` 头对 `/internal/v1/*` 做服务间鉴权,见 `API.md §9`。 ## 快速启动 ```bash cp .env.example .env # 编辑 .env — 至少改 MYSQL 密码,以及(s3 模式下)S3 凭据。 # 静态检查 uv sync --all-packages uv run --frozen --package backend python -m compileall -q backend/src common/src uv run --frozen --package schedule python -m compileall -q schedule/src uv run --frozen --package runtime python -m compileall -q runtime/src # 应用 schema uv run --frozen --package backend alembic upgrade head # 启动整套服务 docker compose config # 校验 docker compose up -d --build docker compose ps ``` 访问 `http://localhost:8888`。 ### 日志 ```bash docker compose logs -f backend docker compose logs -f schedule docker compose logs -f runtime ``` ### 停服(保留 MySQL + S3 / local-storage 数据卷) ```bash docker compose down ``` ### 抹数据 ```bash docker compose down -v ``` ## 配置 所有环境变量在 `common/src/common/config.py` 里用 pydantic-settings 的 `Settings` 类一次性声明,外面套一层 `@lru_cache` 单例。新增环境变量: 1. 在 `common/src/common/config.py` 的 `Settings` 里加字段(带合理 default,使 dev 启动不需要设) 2. 在 `.env.example` 加一行带注释 3. 调用点用 `settings.`,永远不要用 `os.environ["..."]` 完整环境变量列表和含义见 `DEVELOP.md`。 ## 存储布局 4 个 purpose 命名桶。从 `StorageObjects.usage_type` 到桶的映射由 **单一入口**(`backend/storage_api.py:resolve_bucket`)决定: | `usage_type` | 桶(环境变量) | 默认名 | |---|---|---| | `working_copy`、`public_script`、`data_resource`、`snapshot` | `S3_WORKSPACE_BUCKET` | `workspace` | | `version_artifact` | `S3_VERSION_BUCKET` | `version` | | `run_log`、`run_result` | `S3_RUN_LOG_BUCKET` | `run-log` | | (软删除目标) | `S3_TRASH_BUCKET` | `trash` | `STORAGE_BACKEND=s3` 模式下是 4 个独立 S3 桶。`STORAGE_BACKEND=local` 模式下 是 `LOCAL_STORAGE_BASE_DIR`(默认 `/data`)下的 4 个子目录: ``` /data/ ├── workspace/ # S3_WORKSPACE_BUCKET ├── version/ # S3_VERSION_BUCKET ├── run_log/ # S3_RUN_LOG_BUCKET └── trash/ # S3_TRASH_BUCKET ``` 某个 workspace 的 `artifact_bucket` 列(非 NULL 时)覆盖该 workspace 的默认桶, 无视 `usage_type` — 适合把付费客户隔离到专属桶。 对象 key 是两层扁平路径 — `workspace_id` 加服务端签发的 `ulid`: ``` //{.} ``` 文件名、扩展名、MIME、逻辑路径都放在 `StorageObjects` 和 `Scripts` 行里,不进 object key — 重新组织存储不需要重写数据库。 Backend 代码从不写容器本地文件系统(`STORAGE_BACKEND=local` 模式除外,那里共享 `local-storage` volume 就是规范存储)。Schedule Executor 在 `tempfile.TemporaryDirectory()` 里暂存节点工件(自动清理)。只有 `runtime` 容器保留宿主 volume — s3 模式下 rclone FUSE 挂载需要;local 模式下是 no-op 透传。 ## 文档 - `README.md`(本文)— 快速导读 - `ARCHITECTURE.md` — 设计图 + 简化历史 - `HANDOVER.md` — 实现偏离、近期重构、待办事项 - `DEVELOP.md` — 开发指南(环境变量、代码规约、常用操作) - `CLAUDE.md` — agent 面向的本仓库规约 ## 许可 内部。