Files
model-platform/API.md
T
2026-07-31 13:41:37 +08:00

20 KiB

模型平台接口文档

本文档面向前端开发者与第三方集成方。所有接口的入口是 Nginx 网关(默认 http://localhost:8888),除 /api/v1/auth/jupyter 由 Nginx auth_request 自动调用,其他接口都通过 /api/v1/... 同源访问。

服务端基础 URL 示例: http://localhost:8888

通用响应外壳:

{
  "request_id": "01HXY...",
  "data": { ... },
  "meta": {}
}

错误响应为标准 HTTP 4xx / 5xx,body 为 {"detail": "..."}{"code": "...", "message": "...", "details": {}}

目录

  1. 鉴权
  2. 统一约定
  3. 脚本 / Notebook (/api/v1/scripts/...)
  4. 调度 (/api/v1/schedules/... + /api/v1/schedule-runs/...)
  5. 数据资源 (/api/v1/data-resources/...)
  6. 管理后台 (/api/v1/admin/...)
  7. Jupyter 路由 (Nginx auth_request)
  8. 对象存储控制面 (/internal/v1/...,同进程 RPC)
  9. 健康检查

一、鉴权

平台用 JWT (HS256)。登录后,前端在后续请求里任选一种携带方式:

  • Cookie(推荐用于浏览器):登录成功后后端种 Authorization 或自定义 cookie;前端无需手写。
  • Authorization: Bearer <token>(推荐用于脚本与第三方)。

JWT_SECRET 由后端从 Settings.jwt_secret 读取,前端不需要知道,只需要保证登录态带过来即可。

  • AuthorizationCookie 同时存在时,后端优先使用 Authorization
  • 缺失或过期 → HTTP 401
  • 有效但用户不在 workspace → HTTP 403(由 Nginx auth_request 透传给客户端)。

二、统一约定

类别 约定
ID 格式 全部为 ULID(26 字符),如 01HXY9C5B8N3K4P7Q6RT2V0J8D
时间戳 ISO-8601 UTC,毫秒精度,如 2026-07-31T11:23:45.123
时区 所有 *_at 字段均为 UTC,前端需自行转换显示
分页 大列表接口使用 limit (≤200) + 隐式 cursor,无 offset
幂等键 上传类接口要求 Idempotency-Key 请求头,≥8 字符,≤128 字符
软删 删除操作走 is_deleted 软删,不返回 404;再次查询时已软删资源 status="deleted"
排序 列表默认按业务键倒序(更新时间 / 入队时间等)
鉴权头 见 §一

三、脚本 / Notebook

业务概念: Scripts 是用户工作区里的脚本或 notebook,StorageObjects 是它在对象存储里的"工作副本",Versions 是 immutable 的稳定版本。 写操作受 is_locked 门禁 + owner 校验 保护(架构 V3.1 §4)。

3.1 GET /api/v1/workspace-tree

列出当前用户在 workspace 内的目录树(从 StorageObjects.relative_path 派生)。

  • 鉴权: workspace 成员
  • 请求体: 无
  • 响应:
    {
      "request_id": "...",
      "data": {
        "directories": [
          {"path": "scripts", "name": "scripts", "parent_path": ""},
          {"path": "scripts/etl", "name": "etl", "parent_path": "scripts"}
        ]
      },
      "meta": {"directory_count": 2}
    }
    

3.2 POST /api/v1/workspace-directories

创建一个逻辑目录(RustFS 上是隐式前缀,无需落对象)。

  • 请求体:
    {
      "directory_name": "etl",
      "parent_path": "scripts"
    }
    
  • 响应 201:
    {
      "request_id": "...",
      "data": {"path": "scripts/etl", "name": "etl", "parent_path": "scripts"}
    }
    

3.3 DELETE /api/v1/workspace-directories?path=...

删除一个目录(以及目录下当前用户拥有的所有 Scripts,会触发 is_locked 校验)。

  • 查询参数:
    类型 必填 说明
    path string 相对路径,例如 scripts/etl
  • 响应:
    {
      "data": {
        "path": "scripts/etl",
        "status": "deleted",
        "deleted_scripts": 3,
        "versions_preserved": true
      }
    }
    

3.4 GET /api/v1/scripts

列出当前 workspace 内全部 active 脚本。不受 is_locked 影响(读路径不锁)。

  • 响应: dataScriptPayload 数组(见 §3.10)。

3.5 GET /api/v1/scripts/{script_id}

取单个脚本详情。

3.6 POST /api/v1/scripts

创建一个脚本(直接走 create_server_object 上传)。

  • 请求体:
    {
      "script_name": "train.py",
      "script_type": "python",
      "content": "print('hello')",
      "visibility": "workspace",
      "parent_path": "scripts"
    }
    
    字段 必填 说明
    script_name 文件名;后端按 script_type 补齐扩展名(.py / .ipynb)
    script_type python | notebook
    content 文本内容(.ipynb 必须是合法 JSON,含 cells 数组)
    visibility private | workspace (默认) | public
    parent_path 父目录路径

3.7 POST /api/v1/scripts/upload?file_name=...&parent_path=...&visibility=...

multipart/binary 形式上传大文件(走 presigned PUT)。

  • 查询参数: file_name(必填)、parent_pathvisibility
  • 请求体: 原始文件字节(Content-Type 必须与脚本类型匹配)
  • 适用场景: 大于 100 KiB 的 notebook / 资源文件

3.8 PUT /api/v1/scripts/{script_id}

更新脚本工作副本。受 owner + is_locked 双重门禁:

  • admin 总是允许

  • owner 总是允许

  • 非 owner + is_locked == 0 → 允许

  • 非 owner + is_locked == 1403

  • 请求体: {"content": "..."}

3.9 DELETE /api/v1/scripts/{script_id}

软删脚本。版本(Versions)会被保留以供审计。门禁同 §3.8。

3.10 ScriptPayload 字段

字段 类型 说明
script_id ULID
workspace_id ULID
current_object_id ULID 当前工作副本指向的 StorageObjects.storage_object_id
owner_user_id ULID
script_name string
script_type python | notebook
visibility enum
status active | deleted
relative_path string | null 例如 users/alice/scripts/etl/train.py
content_hash string | null SHA-256 十六进制
size_bytes int
created_at / updated_at ISO-8601

3.11 POST /api/v1/scripts/{script_id}/versions

发布一个稳定版本(immutable,绑定到 RUSTFS_VERSION_BUCKET)。门禁同 §3.8。

  • 请求体:
    {
      "source_object_id": "01HXY...",
      "release_note": "首次发布",
      "visibility": "workspace"
    }
    
  • 行为:
    • source_object_id 对应的工作副本内容,算 SHA-256
    • content_hash 已存在则返回 200 + meta.reused = true(去重)
    • 否则把副本内容上 versions 桶,创建 Versions
  • 响应:
    {
      "data": {
        "versions_id": "01HXY...",
        "version_no": 3,
        "version_label": "v3.0",
        "content_hash": "...",
        "file_size_bytes": 2048,
        "artifact_path": "s3://versions/<ws_id>/<ulid>",
        "...": "..."
      },
      "meta": {"reused": false}
    }
    

3.12 GET /api/v1/scripts/{script_id}/versions

列出该脚本的所有版本(倒序)。

3.13 GET /api/v1/versions/{versions_id}

单版本详情。

3.14 DELETE /api/v1/versions/{versions_id}

从调度候选中隐藏此版本(不删除 RustFS 对象)。门禁:owner 校验基于所属 Scripts 的 owner——即"按整本 script 判定",而非"按版本发布者判定"。

3.15 POST /api/v1/versions/{versions_id}/download-url

生成 RustFS 的 presigned download URL。

  • 请求体:
    {"expires_seconds": 300}
    
  • 响应:
    {
      "data": {
        "storage_object_id": "...",
        "presigned_url": "https://<gateway>/storage/<bucket>/<key>?X-Amz-...",
        "method": "GET",
        "expires_in_seconds": 300
      }
    }
    

四、调度

业务概念: Schedules 是 DAG 模板(nodes + edges),ScheduleRuns 是 触发产生的一次执行实例,自带 snapshot 锁住当时的 DAG,ScheduleNodeRuns 是 run 里每个 node 每次尝试的记录。

调度链路(架构 V3.1 §2.3):

手动:  POST /run          ─→ schedule_runs (queued) + outbox_events
cron:  Executor APScheduler tick → POST /run  ─→ 同上
                                                             │
Orchestrator (poll outbox 0.25s)                            │
  → schedule_node_runs (queued) + outbox_events(job.node.execute)
Worker (poll outbox)                                         │
  → 执行 → schedule_node_runs (succeeded/failed) + outbox_events(job.node.finished)
Orchestrator 收 finished → 推进下一个 node / 终结 run

4.1 GET /api/v1/schedule-artifacts

列出可绑定到节点的 Versions(DAG 画布下拉框的素材源)。

4.2 POST /api/v1/cron/preview

预览一个 cron 表达式的未来 5 次触发时间。

  • 请求体:
    {"expression": "0 0 * * *", "timezone": "Asia/Shanghai"}
    

4.3 POST /api/v1/schedules/{schedule_id}/validate

校验 DAG 拓扑(环路检测、孤立节点等)。

4.4 调度模板 CRUD

方法 路径 说明
GET /api/v1/schedules 列当前 workspace 的所有 schedule
POST /api/v1/schedules 创建(返回 201)
GET /api/v1/schedules/{id} 详情(含 nodes + edges)
PUT / PATCH /api/v1/schedules/{id} 改 cron / 时区 / 启用 / max_concurrency / failure_policy
DELETE /api/v1/schedules/{id} 软删

CreateScheduleRequest 字段:

{
  "schedule_name": "nightly-train",
  "trigger_type": "cron",
  "cron_expression": "0 0 * * *",
  "timezone": "Asia/Shanghai",
  "enabled": true,
  "max_concurrency": 3,
  "failure_policy": "stop"
}

4.5 节点 CRUD

方法 路径 说明
POST /api/v1/schedules/{id}/nodes 加节点(必填 versions_id 绑定 Versions)
PUT /api/v1/schedules/{id}/nodes/{node_id} 改节点参数/版本引用/重试策略
DELETE /api/v1/schedules/{id}/nodes/{node_id} 删节点

节点 arguments / env_refs 只存引用,不存明文密钥。

4.6 边 CRUD

方法 路径 说明
POST /api/v1/schedules/{id}/edges 加边(source_node_id / target_node_id)
PUT /api/v1/schedules/{id}/edges/{edge_id} condition_expr
DELETE /api/v1/schedules/{id}/edges/{edge_id} 删边

condition_expr 字段当前仅落库,不参与执行判定。DAG 只能表示依赖, 不能表达"父 node value > 0 才走 A 分支"等条件分支。

4.7 触发与查询

POST /api/v1/schedules/{schedule_id}/run

手动触发一次 run。

  • 必填请求头:Idempotency-Key(≥8 字符)
  • 可选请求体:{"reason": "manual_run"}(默认) | {"reason": "cron"}
  • 响应 202:
    {
      "data": {
        "run_id": "01HXY...",
        "schedule_id": "...",
        "trigger_type": "manual",
        "run_status": "queued",
        "queued_at": "...",
        "schedule_snapshot": {"nodes": [...], "edges": [...]}
      },
      "meta": {"reused": false}
    }
    
    • Idempotency-Key 已存在 → 返回原 run + meta.reused = true
    • 同 key 但元数据不一致 → 409 Conflict
    • DAG 无效 / 节点 > 100 / 边 > 500 → 409 + 错误码 SCHEDULE_DAG_INVALID

GET /api/v1/schedule-runs

列出 run。可选 ?schedule_id=...?status=queued|running|succeeded|failed|cancelled|timed_out 过滤。

GET /api/v1/schedule-runs/{run_id}

单 run 详情 + 所有 node_run

4.8 run 状态机

queued ──→ running ──┬─→ succeeded
                     ├─→ failed
                     ├─→ cancelled   (未实现)
                     └─→ timed_out

五、数据资源

通用二进制资源(数据集、模型 checkpoint、任意文件)。Base 路径 前缀是 /api/v1/data-resources,带脚本/notebook 的 owner 锁。

方法 路径 说明
POST /api/v1/data-resources/uploads 创建上传会话,返回 presigned PUT URL + upload_id
POST /api/v1/data-resources/uploads/{upload_id}/complete 完成上传(写 StorageObjects 行)
GET /api/v1/data-resources 列表(workspace 范围)
GET /api/v1/data-resources/{id} 详情
POST /api/v1/data-resources/{id}/download-url 生成 presigned GET URL
DELETE /api/v1/data-resources/{id} 软删

请求示例(上传):POST /api/v1/data-resources/uploads

{
  "file_name": "data.csv",
  "content_type": "text/csv",
  "expected_size_bytes": 1048576,
  "expected_hash": "<optional sha256 hex>",
  "idempotency_key": "client-uuid-or-similar"
}

完整上传流程(前端应实现的模式):

1. POST /uploads → {upload_id, presigned_url}
2. PUT  presigned_url  with file bytes
3. POST /uploads/{upload_id}/complete
4. 服务器完成 → 200 {data: StorageObjectPayload}

小对象(<100 KiB)捷径:直接调 create_server_object 把字节 base64 放进 content_base64 字段(走 StorageClient.create_server_object),前端无需 走 presigned PUT 三步。


六、管理后台

Base 前缀 /api/v1/admin

方法 路径 说明
GET /api/v1/admin/employees 列员工(workspace 成员)
POST /api/v1/admin/employees 创建员工账号(返回 201)
PATCH /api/v1/admin/employees/{user_id} 改员工信息(角色/状态等)
DELETE /api/v1/admin/employees/{user_id} 软删员工

当前所有 admin 端点要求 is_admin 上下文标志,具体 token 校验流程 见 §一。


七、Jupyter 路由

本节是 Nginx 行为,不是直接 HTTP 端点。前端不要直接调用。

7.1 浏览器 → 用户打开 notebook

用户在前端点击某个 notebook,前端拼出 URL:

GET /jupyter/{workspace_id}/notebooks/{相对路径}.ipynb
GET /jupyter/{workspace_id}/lab/tree/{相对路径}.ipynb
GET /jupyter/{workspace_id}/api/contents/{相对路径}.ipynb
WS  /jupyter/{workspace_id}/api/kernels/...

7.2 Nginx auth_request 鉴权

Nginx 收到上述请求后,发一个内部子请求:

GET /internal-auth
        ↓
Nginx: 抽 X-Original-Workspace-Id + X-Original-URI + Cookie + Authorization
        ↓
GET /api/v1/auth/jupyter
        ↓
Backend 流程:
   1. verify_jwt_token (HS256, JWT_SECRET)
   2. require_workspace_member (WorkspaceMembers JOIN)
   3. extract_notebook_path (只对 /notebooks/*.ipynb 做 lock 校验)
   4. check_notebook_is_locked (Scripts.is_locked + owner)
   5. runtime_client.get_workspace / start_workspace (拿子进程地址 + token)
        ↓
Backend 响应 200 + 响应头:
   x-upstream-addr:        <base_url>:<jupyter 子进程端口>
   x-jupyter-internal-token: <子进程 token>
        ↓
Nginx: auth_request_set 捕获这两个变量,proxy_pass 到子进程并注入
   Authorization: token $jupyter_token
        ↓
浏览器收到响应,**自始至终未接触 Jupyter Token**

7.3 鉴权失败码

状态 触发条件
401 JWT 缺失/过期/校验失败
403 用户不在 workspace / notebook 被锁定且非 owner
404 workspace 不存在
503 runtime 容器不可达(子进程启动失败)

Nginx 把这些状态原样透传给浏览器,前端可在 onerror 里判断。


八、对象存储控制面

路径前缀 /internal/v1/...,前端不要直接调用。这是 backend 内部 异步消息处理(Schedule worker)用的 RPC 端点,经 StorageClient HTTP 客户端访问。Backend 通过 ASGI auth_request_set 路由转发,外部无法 访问。

8.1 POST /internal/v1/uploads

创建上传会话。Idempotency-Key 必填,同 key + 同元数据 → 复用;同 key + 不同元数据 → 409。

{
  "workspace_id": "...",
  "user_id": "...",
  "usage_type": "working_copy",
  "file_name": "train.py",
  "content_type": "text/x-python",
  "expected_size_bytes": 1024,
  "expected_hash": "<optional sha256 hex>",
  "idempotency_key": "..."
}

返回 {upload_id, bucket_name, object_key, presigned_url, expires_in_seconds}

8.2 POST /internal/v1/uploads/{upload_id}/complete

完成上传。从 RustFS 读 HEAD → 校验 hash → 写 StorageObjects 行。

8.3 POST /internal/v1/uploads/{upload_id}/abort

主动放弃。释放 UploadSessions 行,对象不入库。

8.4 POST /internal/v1/objects

单步创建(不走 presigned PUT,字节随请求体直传)。适用 < 100 KiB 对象。

{
  "workspace_id": "...",
  "user_id": "...",
  "usage_type": "run_log",
  "file_name": "log.txt",
  "content_type": "text/plain",
  "content_base64": "PHN0ZXAtY29udGVudD4=",
  "visibility": "private",
  "is_immutable": false,
  "idempotency_key": "..."
}

8.5 POST /internal/v1/objects/{storage_object_id}/download-url

生成 presigned GET URL。

8.6 DELETE /internal/v1/objects/{storage_object_id}

软删。is_immutable == 1 的对象拒绝删除。

8.7 usage_type → 桶路由(自动)

usage_type 实际桶(env var) 默认桶名
working_copy, public_script, data_resource, snapshot RUSTFS_WORKSPACE_BUCKET workspaces
version_artifact RUSTFS_VERSION_BUCKET versions
run_log, run_result RUSTFS_RUN_LOG_BUCKET run-logs

Workspaces.artifact_bucket 非空,优先用 per-workspace 桶(覆盖 usage_type 路由)。


九、健康检查

方法 路径 用途
GET / 简单服务标识
GET /health/live 进程存活(不检查依赖)
GET /health/ready 依赖就绪(可选 TCP 探测列表)
GET /api/v1/health 公开健康检查(前端可访问)

/health/ready 支持 READINESS_TARGETS 环境变量,逗号分隔的 host:port 列表,例如 mysql:3306,rustfs:9000,全部 TCP 通则返回 200,否则 503。


附录 A — 错误码参考

HTTP 业务码 / 含义 触发场景
400 参数错误 Pydantic 校验失败
401 未鉴权 JWT 缺失/无效
403 鉴权失败 非 workspace 成员 / is_locked 阻写
404 不存在 resource_id / script_id / schedule_id 找不到
409 冲突 DAG 无效 / 同 idempotency_key 不同元数据 / 目标已存在 / is_immutable 阻删
412 条件失败 source_object_id 与当前工作副本不一致
413 太大 内容超过 100 MiB / 10 MiB
422 语义错误 文件名非法 / cron 表达式非法 / 路径逃逸
500 内部错误 DB / 存储不可达

附录 B — 状态枚举

类型 取值
StorageObjects.usage_type data_resource / version_artifact / snapshot / run_log / run_result / working_copy / public_script
StorageObjects.object_status available / deleted
StorageObjects.visibility private / workspace / public
Scripts.status active / deleted
Schedules.trigger_type manual / cron / api
Schedules.failure_policy stop / continue
ScheduleRuns.run_status queued / running / succeeded / failed / cancelled / timed_out
ScheduleNodeRuns.node_status queued / running / succeeded / failed / skipped / cancelled / timed_out
UploadSessions.upload_status created / uploading / completed / expired / aborted / failed

附录 C — 通用枚举字段

字段 取值
visibility private / workspace / public
is_locked 0 / 1(Scripts 表 TINYINT)

  • Nginx 同源代理,前端与 API 同源,不需要 CORS 配置。
  • 鉴权通过 Cookie 或 Authorization 头携带(见 §一)。
  • 上传类接口要求 Idempotency-Key,前端应在请求构造时就生成稳定 UUID 并缓存,失败重试时复用同一 key。