修两个后端接口问题:
1) /api/v1/workspace-directories 返回为空,目录树结构消失
2) 同 workspace 内脚本/数据互相可见但默认排除 private
后端改动
--------
* list_scripts / list_resources / list_workspace_directories 新增
owner_user_id 可选 query 参数;缺省 = 当前请求者本人(scope 到
workspace/{me}/...),传值时 scope 到该 owner 的子树。前端根加载
默认只见自己一级,其他成员以折叠分组呈现。
* visibility 过滤统一:非 admin 请求者只返回 owner==me 或
visibility ∈ {workspace, public};admin 跳过。owner=me 含自己
的 private,owner=other 只剩其 workspace/public,排除他人 private。
* create_workspace_directory 两个分支 visibility 默认 'public'
(非 private),使跨 owner 目录树可见;响应新增 owner_user_id 字段。
* platform.list_members 鉴权从 system_admin_context 放宽为
系统管理员或该 workspace 活跃成员(让普通用户也能渲染同
workspace 成员名册,用于跨 owner 分组)。
* main.py 注册 platform 模块(随 list_members 改动补齐导入)。
* .env.example 同步 common/config.py 26 个字段。
前端改动
--------
* ScriptExplorer.memberScriptGroups 改由 members 列表播种分组,
display_name 取 members.display_name;inferredDirectories 现在按
owner_user_id 标记,统一跨 owner 目录渲染。删除脚本目录页头与
树分组标题的工作副本数量角标。
* WorkspaceTree 新增 ownerUserId 透传到 store.toggleExpanded;
仅"我"的分组 mount 时 auto-expand,他人分组默认折叠,展开才
调 loadOwnerGroup / owner-scoped loadScripts / loadChildren。
* scriptWorkspaceStore 引入 namespaced cache key
(ownerCacheKey = `${ownerUserId ?? me}:${path}`),loadedScriptPaths
/ loadedChildPaths / loadedOwnerGroups 全部按 owner 隔离;
toggleExpanded 用 loadPath === undefined 区分 group 头与真实
目录,修"他人子目录点击不触发接口"的 loadPath 前缀误判 bug。
* api.ts / AuthContext 透传 ownerUserId 给 listScripts /
listResources / listWorkspaceDirectories。
文档
----
* API.md: §3.2 创建目录 visibility 默认 public + 响应加 owner_user_id;
§3.3.1 GET directories 加 owner_user_id 参数 + 响应字段;
§3.4 GET scripts 改写为 owner 作用域 + visibility 过滤语义;
§五.1 GET data-resources 新增,同一套统一语义;
§7 intro 例外 — GET members 对系统管理员或 workspace 活跃成员开放。
* DEVELOP.md: Code layout 重写以反映 backend api/services/clients/
schemas 拆分 + schedule domain/scheduling/application/execution/
infrastructure 拆分 + common 子包(auth/storage/backends);
Configuration 系统补全 26 个 settings 字段;新增
"Owner-scoping + visibility (cross-owner browsing)" 小节;
Per-service dev 注释用 uv run 的源布局要求;Add a new DAG endpoint /
storage bucket 路径改为 backend/src/backend/api/* 与 services/*。
测试
----
* test_list_scripts_parent_path.py /
test_resources.py 补充 owner_user_id 参数化直接调用 + LIKE
前缀断言(workspace/{owner}/... 前缀)。
Co-Authored-By: Claude <noreply@anthropic.com>
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。
目录结构
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。
快速启动
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。
日志
docker compose logs -f backend
docker compose logs -f schedule
docker compose logs -f runtime
停服(保留 MySQL + S3 / local-storage 数据卷)
docker compose down
抹数据
docker compose down -v
配置
所有环境变量在 common/src/common/config.py 里用 pydantic-settings 的 Settings
类一次性声明,外面套一层 @lru_cache 单例。新增环境变量:
- 在
common/src/common/config.py的Settings里加字段(带合理 default,使 dev 启动不需要设) - 在
.env.example加一行带注释 - 调用点用
settings.<name>,永远不要用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:
<workspace_bucket>/<workspace_id>/<ulid>{.<ext>}
文件名、扩展名、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 面向的本仓库规约
许可
内部。