20 KiB
模型平台接口文档
本文档面向前端开发者与第三方集成方。所有接口的入口是 Nginx 网关(默认
http://localhost:8888),除/api/v1/auth/jupyter由 Nginxauth_request自动调用,其他接口都通过/api/v1/...同源访问。服务端基础 URL 示例:
http://localhost:8888通用响应外壳:
{ "request_id": "01HXY...", "data": { ... }, "meta": {} }错误响应为标准 HTTP 4xx / 5xx,body 为
{"detail": "..."}或{"code": "...", "message": "...", "details": {}}。
目录
- 鉴权
- 统一约定
- 脚本 / Notebook (
/api/v1/scripts/...) - 调度 (
/api/v1/schedules/...+/api/v1/schedule-runs/...) - 数据资源 (
/api/v1/data-resources/...) - 管理后台 (
/api/v1/admin/...) - Jupyter 路由 (Nginx
auth_request) - 对象存储控制面 (
/internal/v1/...,同进程 RPC) - 健康检查
一、鉴权
平台用 JWT (HS256)。登录后,前端在后续请求里任选一种携带方式:
- Cookie(推荐用于浏览器):登录成功后后端种
Authorization或自定义 cookie;前端无需手写。 Authorization: Bearer <token>(推荐用于脚本与第三方)。
JWT_SECRET 由后端从 Settings.jwt_secret 读取,前端不需要知道,只需要保证登录态带过来即可。
- 当
Authorization与Cookie同时存在时,后端优先使用Authorization。 - 缺失或过期 → HTTP
401。 - 有效但用户不在 workspace → HTTP
403(由 Nginxauth_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 校验)。
- 查询参数:
名 类型 必填 说明 pathstring 是 相对路径,例如 scripts/etl - 响应:
{ "data": { "path": "scripts/etl", "status": "deleted", "deleted_scripts": 3, "versions_preserved": true } }
3.4 GET /api/v1/scripts
列出当前 workspace 内全部 active 脚本。不受 is_locked 影响(读路径不锁)。
- 响应:
data为ScriptPayload数组(见 §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|notebookcontent是 文本内容( .ipynb必须是合法 JSON,含cells数组)visibility否 private|workspace(默认) |publicparent_path否 父目录路径
3.7 POST /api/v1/scripts/upload?file_name=...&parent_path=...&visibility=...
multipart/binary 形式上传大文件(走 presigned PUT)。
- 查询参数:
file_name(必填)、parent_path、visibility - 请求体: 原始文件字节(
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 == 1→ 403 -
请求体:
{"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 端点,经StorageClientHTTP 客户端访问。Backend 通过 ASGIauth_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) |
附录 D — 跨域与 Cookie
- Nginx 同源代理,前端与 API 同源,不需要 CORS 配置。
- 鉴权通过 Cookie 或
Authorization头携带(见 §一)。 - 上传类接口要求
Idempotency-Key,前端应在请求构造时就生成稳定 UUID 并缓存,失败重试时复用同一 key。