refactor: delete contracts

This commit is contained in:
tao.chen
2026-07-31 13:41:18 +08:00
parent 2874ccdce3
commit 72b7adb8c4
20 changed files with 0 additions and 6455 deletions
-15
View File
@@ -1,15 +0,0 @@
# Contracts
模块接口契约公共目录:
```text
openapi/ HTTP OpenAPI 3 契约
events/ MySQL Outbox 内部事件 JSON Schema
runtime/ Runtime Adapter 契约
locks/ Notebook 编辑锁契约
```
各服务实现必须以本目录中的版本化契约为准。
快速 Demo 的跨模块边界、状态枚举和事件路由统一见
`demo-core-v1.md`
-1
View File
@@ -1 +0,0 @@
"""Executable cross-service contracts."""
-56
View File
@@ -1,56 +0,0 @@
# 快速 Demo 核心契约 V1
冻结日期:2026-07-27
状态:`frozen-target`
## 1. 契约边界
Demo 公共 HTTP API 由两部分共同组成:
1. `openapi/platform-api-v1.yaml`:已经运行的脚本、稳定版本详情、数据资源和文件编辑锁接口。
2. `openapi/demo-core-extension-v1.yaml`:第 15 小步冻结、后续小步实现的上下文、Jupyter 票据、稳定版本列表、调度和运行查询接口。
内部 Storage 与 Runtime 接口继续分别以
`openapi/storage-api-internal-v1.yaml`
`openapi/runtime-api-internal-v1.yaml` 为准。
## 2. 不可变约束
- 主标识使用 ULID;调度节点只引用不可变的 `versions_id`
- 当前认证上下文由 `X-User-ID``X-Workspace-ID` 提供;以后换成 JWT 时不得改变业务 DTO。
- 修改调度方案必须提交 `workflow_version`,冲突返回 `412`
- 创建调度、立即运行、启停和取消使用 `Idempotency-Key`
- `schedule_runs.schedule_snapshot` 固化本次执行 DAG,后续编辑不影响已创建的运行。
- 浏览器不能拿到 Jupyter 内部 Token,只能获得短期访问 Cookie。
## 3. 状态值
- 调度运行:`queued / running / succeeded / failed / cancelled / timed_out`
- 节点运行:`queued / running / succeeded / failed / skipped / cancelled / timed_out`
- 失败策略:`stop / continue`
- 触发类型:`manual / cron / api / retry`
## 4. MySQL Outbox 与 HTTP 推送
| 事件 | 写入方 | 处理方 |
|---|---|---|
| `schedule.run.requested` | Backend / Cron Dispatcher | Schedule Executor |
| `job.node.execute` | Schedule Executor | Schedule Executor Worker |
| `job.node.finished` | Schedule Executor Worker | Schedule Executor |
业务事务先写 `outbox_events`。Backend 对“立即运行”执行一次内部 HTTP 推送,
Executor 同时轮询 MySQL 作为兜底;`consumer_inbox` 防止同一事件重复执行。
## 5. 模块所有权
| 模块 | 拥有的数据与职责 |
|---|---|
| Platform API | 脚本、稳定版本、调度定义、运行查询、Outbox 写入 |
| Runtime Manager | Workspace/Jupyter 生命周期与内部运行态 |
| Nginx | 统一入口、Jupyter HTTP/WebSocket 代理 |
| Schedule Orchestrator | 消费调度请求、解析 DAG、推进节点状态 |
| Job Worker | 读取稳定版本、执行节点、保存日志/结果、报告终态 |
| Storage Service | RustFS 对象元数据与预签名访问 |
模块间不得复制定义 DTO、状态枚举、错误结构或事件字段;契约变更必须先
升级本目录中的版本化文件。
-11
View File
@@ -1,11 +0,0 @@
# Database Event Schemas
内部事件以 JSON Schema Draft 2020-12 定义:
- `event-envelope-v1.json`:公共事件信封;
- `schedule-run-requested-v1.json`:请求启动一次调度运行;
- `job-node-execute-v1.json`:请求执行一个稳定版本节点;
- `job-node-finished-v1.json`:节点终态。
事件先随业务事务写入 MySQL `outbox_events`Schedule Executor 直接轮询处理;
`consumer_inbox` 提供幂等保护。字段和状态值不得在生产者或消费者中另行定义。
-72
View File
@@ -1,72 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "event-envelope-v1.json",
"title": "Model Platform Event Envelope V1",
"description": "MySQL Outbox 中所有内部业务事件共用的不可变信封。",
"type": "object",
"additionalProperties": false,
"required": [
"event_id",
"event_type",
"schema_version",
"occurred_at",
"producer",
"trace_id",
"aggregate_type",
"aggregate_id",
"idempotency_key",
"payload"
],
"properties": {
"event_id": {
"$ref": "#/$defs/ulid"
},
"event_type": {
"type": "string",
"pattern": "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9_]*)+$"
},
"schema_version": {
"const": 1
},
"occurred_at": {
"type": "string",
"format": "date-time"
},
"producer": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"trace_id": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"aggregate_type": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"aggregate_id": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"idempotency_key": {
"type": "string",
"minLength": 8,
"maxLength": 128
},
"payload": {
"type": "object"
}
},
"$defs": {
"ulid": {
"type": "string",
"minLength": 26,
"maxLength": 26,
"pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
}
}
}
-125
View File
@@ -1,125 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "job-node-execute-v1.json",
"title": "Job Node Execute V1",
"description": "调度编排器请求 Job Worker 执行一个稳定版本节点。",
"allOf": [
{
"$ref": "event-envelope-v1.json"
},
{
"type": "object",
"properties": {
"event_type": {
"const": "job.node.execute"
},
"aggregate_type": {
"const": "schedule_node_run"
},
"payload": {
"$ref": "job-node-execute-v1.json#/$defs/payload"
}
}
}
],
"$defs": {
"ulid": {
"type": "string",
"minLength": 26,
"maxLength": 26,
"pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
},
"payload": {
"type": "object",
"additionalProperties": false,
"required": [
"workspace_id",
"run_id",
"node_run_id",
"node_id",
"versions_id",
"attempt_no",
"script_type",
"artifact_object_id",
"artifact_path",
"timeout_seconds",
"arguments"
],
"properties": {
"workspace_id": {
"$ref": "job-node-execute-v1.json#/$defs/ulid"
},
"run_id": {
"$ref": "job-node-execute-v1.json#/$defs/ulid"
},
"node_run_id": {
"$ref": "job-node-execute-v1.json#/$defs/ulid"
},
"node_id": {
"$ref": "job-node-execute-v1.json#/$defs/ulid"
},
"versions_id": {
"$ref": "job-node-execute-v1.json#/$defs/ulid"
},
"attempt_no": {
"type": "integer",
"minimum": 1,
"maximum": 11
},
"script_type": {
"enum": [
"python",
"notebook"
]
},
"artifact_object_id": {
"$ref": "job-node-execute-v1.json#/$defs/ulid"
},
"artifact_path": {
"type": "string",
"minLength": 1,
"maxLength": 1024
},
"timeout_seconds": {
"type": "integer",
"minimum": 1,
"maximum": 86400
},
"arguments": {
"type": "array",
"maxItems": 100,
"items": {
"type": "string",
"maxLength": 1000
}
}
}
}
},
"examples": [
{
"event_id": "01K123456789ABCDEFGHJKMNPZ",
"event_type": "job.node.execute",
"schema_version": 1,
"occurred_at": "2026-07-27T08:00:01Z",
"producer": "schedule-orchestrator",
"trace_id": "req-demo-0001",
"aggregate_type": "schedule_node_run",
"aggregate_id": "01K123456789ABCDEFGHJKMNQ0",
"idempotency_key": "01K123456789ABCDEFGHJKMNQ0:1",
"payload": {
"workspace_id": "01K123456789ABCDEFGHJKMNPS",
"run_id": "01K123456789ABCDEFGHJKMNPR",
"node_run_id": "01K123456789ABCDEFGHJKMNQ0",
"node_id": "01K123456789ABCDEFGHJKMNPW",
"versions_id": "01K123456789ABCDEFGHJKMNPX",
"attempt_no": 1,
"script_type": "python",
"artifact_object_id": "01K123456789ABCDEFGHJKMNPY",
"artifact_path": "versions/prepare/v1.py",
"timeout_seconds": 600,
"arguments": []
}
}
]
}
-162
View File
@@ -1,162 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "job-node-finished-v1.json",
"title": "Job Node Finished V1",
"description": "Job Worker 报告一次节点执行的终态,调度编排器据此推进 DAG。",
"allOf": [
{
"$ref": "event-envelope-v1.json"
},
{
"type": "object",
"properties": {
"event_type": {
"const": "job.node.finished"
},
"aggregate_type": {
"const": "schedule_node_run"
},
"payload": {
"$ref": "job-node-finished-v1.json#/$defs/payload"
}
}
}
],
"$defs": {
"ulid": {
"type": "string",
"minLength": 26,
"maxLength": 26,
"pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
},
"nullableUlid": {
"oneOf": [
{
"$ref": "job-node-finished-v1.json#/$defs/ulid"
},
{
"type": "null"
}
]
},
"payload": {
"type": "object",
"additionalProperties": false,
"required": [
"workspace_id",
"run_id",
"node_run_id",
"node_id",
"versions_id",
"attempt_no",
"node_status",
"exit_code",
"started_at",
"finished_at",
"duration_ms",
"logs_object_id",
"result_object_id",
"error_code",
"error_message"
],
"properties": {
"workspace_id": {
"$ref": "job-node-finished-v1.json#/$defs/ulid"
},
"run_id": {
"$ref": "job-node-finished-v1.json#/$defs/ulid"
},
"node_run_id": {
"$ref": "job-node-finished-v1.json#/$defs/ulid"
},
"node_id": {
"$ref": "job-node-finished-v1.json#/$defs/ulid"
},
"versions_id": {
"$ref": "job-node-finished-v1.json#/$defs/ulid"
},
"attempt_no": {
"type": "integer",
"minimum": 1,
"maximum": 11
},
"node_status": {
"enum": [
"succeeded",
"failed",
"cancelled",
"timed_out"
]
},
"exit_code": {
"type": [
"integer",
"null"
]
},
"started_at": {
"type": "string",
"format": "date-time"
},
"finished_at": {
"type": "string",
"format": "date-time"
},
"duration_ms": {
"type": "integer",
"minimum": 0
},
"logs_object_id": {
"$ref": "job-node-finished-v1.json#/$defs/nullableUlid"
},
"result_object_id": {
"$ref": "job-node-finished-v1.json#/$defs/nullableUlid"
},
"error_code": {
"type": [
"string",
"null"
],
"maxLength": 64
},
"error_message": {
"type": [
"string",
"null"
],
"maxLength": 2000
}
}
}
},
"examples": [
{
"event_id": "01K123456789ABCDEFGHJKMNQ1",
"event_type": "job.node.finished",
"schema_version": 1,
"occurred_at": "2026-07-27T08:00:03Z",
"producer": "job-worker",
"trace_id": "req-demo-0001",
"aggregate_type": "schedule_node_run",
"aggregate_id": "01K123456789ABCDEFGHJKMNQ0",
"idempotency_key": "01K123456789ABCDEFGHJKMNQ0:1:finished",
"payload": {
"workspace_id": "01K123456789ABCDEFGHJKMNPS",
"run_id": "01K123456789ABCDEFGHJKMNPR",
"node_run_id": "01K123456789ABCDEFGHJKMNQ0",
"node_id": "01K123456789ABCDEFGHJKMNPW",
"versions_id": "01K123456789ABCDEFGHJKMNPX",
"attempt_no": 1,
"node_status": "succeeded",
"exit_code": 0,
"started_at": "2026-07-27T08:00:01Z",
"finished_at": "2026-07-27T08:00:03Z",
"duration_ms": 2000,
"logs_object_id": null,
"result_object_id": null,
"error_code": null,
"error_message": null
}
}
]
}
@@ -1,255 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schedule-run-requested-v1.json",
"title": "Schedule Run Requested V1",
"description": "Platform API 或 Cron Dispatcher 请求调度编排器启动一次固化 DAG。",
"allOf": [
{
"$ref": "event-envelope-v1.json"
},
{
"type": "object",
"properties": {
"event_type": {
"const": "schedule.run.requested"
},
"aggregate_type": {
"const": "schedule_run"
},
"payload": {
"$ref": "schedule-run-requested-v1.json#/$defs/payload"
}
}
}
],
"$defs": {
"ulid": {
"type": "string",
"minLength": 26,
"maxLength": 26,
"pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
},
"node": {
"type": "object",
"additionalProperties": false,
"required": [
"node_id",
"node_key",
"versions_id",
"script_type",
"artifact_object_id",
"artifact_path",
"timeout_seconds",
"retry_count",
"retry_interval_sec",
"arguments"
],
"properties": {
"node_id": {
"$ref": "schedule-run-requested-v1.json#/$defs/ulid"
},
"node_key": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"versions_id": {
"$ref": "schedule-run-requested-v1.json#/$defs/ulid"
},
"script_type": {
"enum": [
"python",
"notebook"
]
},
"artifact_object_id": {
"$ref": "schedule-run-requested-v1.json#/$defs/ulid"
},
"artifact_path": {
"type": "string",
"minLength": 1,
"maxLength": 1024
},
"timeout_seconds": {
"type": "integer",
"minimum": 1,
"maximum": 86400
},
"retry_count": {
"type": "integer",
"minimum": 0,
"maximum": 10
},
"retry_interval_sec": {
"type": "integer",
"minimum": 0,
"maximum": 3600
},
"arguments": {
"type": "array",
"maxItems": 100,
"items": {
"type": "string",
"maxLength": 1000
}
}
}
},
"edge": {
"type": "object",
"additionalProperties": false,
"required": [
"source_node_id",
"target_node_id"
],
"properties": {
"source_node_id": {
"$ref": "schedule-run-requested-v1.json#/$defs/ulid"
},
"target_node_id": {
"$ref": "schedule-run-requested-v1.json#/$defs/ulid"
}
}
},
"snapshot": {
"type": "object",
"additionalProperties": false,
"required": [
"schedule_name",
"workflow_version",
"max_concurrency",
"failure_policy",
"nodes",
"edges"
],
"properties": {
"schedule_name": {
"type": "string",
"minLength": 1,
"maxLength": 255
},
"workflow_version": {
"type": "integer",
"minimum": 1
},
"max_concurrency": {
"type": "integer",
"minimum": 1,
"maximum": 100
},
"failure_policy": {
"enum": [
"stop",
"continue"
]
},
"nodes": {
"type": "array",
"minItems": 1,
"maxItems": 100,
"items": {
"$ref": "schedule-run-requested-v1.json#/$defs/node"
}
},
"edges": {
"type": "array",
"maxItems": 500,
"items": {
"$ref": "schedule-run-requested-v1.json#/$defs/edge"
}
}
}
},
"payload": {
"type": "object",
"additionalProperties": false,
"required": [
"workspace_id",
"schedule_id",
"run_id",
"workflow_version",
"trigger_type",
"triggered_by",
"schedule_snapshot"
],
"properties": {
"workspace_id": {
"$ref": "schedule-run-requested-v1.json#/$defs/ulid"
},
"schedule_id": {
"$ref": "schedule-run-requested-v1.json#/$defs/ulid"
},
"run_id": {
"$ref": "schedule-run-requested-v1.json#/$defs/ulid"
},
"workflow_version": {
"type": "integer",
"minimum": 1
},
"trigger_type": {
"enum": [
"manual",
"cron",
"api",
"retry"
]
},
"triggered_by": {
"oneOf": [
{
"$ref": "schedule-run-requested-v1.json#/$defs/ulid"
},
{
"type": "null"
}
]
},
"schedule_snapshot": {
"$ref": "schedule-run-requested-v1.json#/$defs/snapshot"
}
}
}
},
"examples": [
{
"event_id": "01K123456789ABCDEFGHJKMNPQ",
"event_type": "schedule.run.requested",
"schema_version": 1,
"occurred_at": "2026-07-27T08:00:00Z",
"producer": "platform-api",
"trace_id": "req-demo-0001",
"aggregate_type": "schedule_run",
"aggregate_id": "01K123456789ABCDEFGHJKMNPR",
"idempotency_key": "run-demo-0001",
"payload": {
"workspace_id": "01K123456789ABCDEFGHJKMNPS",
"schedule_id": "01K123456789ABCDEFGHJKMNPT",
"run_id": "01K123456789ABCDEFGHJKMNPR",
"workflow_version": 1,
"trigger_type": "manual",
"triggered_by": "01K123456789ABCDEFGHJKMNPV",
"schedule_snapshot": {
"schedule_name": "每日模型演示",
"workflow_version": 1,
"max_concurrency": 1,
"failure_policy": "stop",
"nodes": [
{
"node_id": "01K123456789ABCDEFGHJKMNPW",
"node_key": "prepare",
"versions_id": "01K123456789ABCDEFGHJKMNPX",
"script_type": "python",
"artifact_object_id": "01K123456789ABCDEFGHJKMNPY",
"artifact_path": "versions/prepare/v1.py",
"timeout_seconds": 600,
"retry_count": 0,
"retry_interval_sec": 5,
"arguments": []
}
],
"edges": []
}
}
}
]
}
-3
View File
@@ -1,3 +0,0 @@
# File Edit Lock
当前版本见 [file-edit-lock-v1.md](file-edit-lock-v1.md)。MySQL 租约是实时锁权威。
-38
View File
@@ -1,38 +0,0 @@
# 文件编辑锁契约 v1
## 公共接口
```text
POST /api/v1/files/{storage_object_id}/lock
POST /api/v1/file-locks/{edit_session_id}/heartbeat
DELETE /api/v1/file-locks/{edit_session_id}
```
三个接口均要求 `X-User-ID``X-Workspace-ID`。加锁成功返回一次性原始
`lock_token`;心跳和释放请求体均为:
```json
{"lock_token": "raw-token-returned-by-acquire"}
```
原始 token 只由客户端持有,数据库只保存 SHA-256 摘要。
## MySQL 租约
`edit_sessions` 是实时锁权威:
- `session_status=active``expires_at > now()` 表示锁有效;
- 同一 `storage_object_id` 同时只能存在一个有效编辑会话;
- 心跳更新 `last_heartbeat_at``expires_at`
- 主动释放将状态改为 `closed`
- 后台清理将超时租约改为 `expired`
加锁、心跳和释放都在数据库事务中校验 `edit_session_id + lock_token_hash`
部署时 Runtime 保持单副本;若扩展到多副本,应为加锁查询增加数据库行锁或唯一
租约表约束。
## 状态与错误
- 冲突返回 HTTP 409、错误码 `FILE_LOCK_CONFLICT`,并包含当前编辑者和租约到期时间;
- 错误 token 返回 HTTP 403,且不得续期或释放现有锁;
- 浏览器建议每 15 秒心跳,默认租约为 45 秒。
-11
View File
@@ -1,11 +0,0 @@
# OpenAPI 契约
- `platform-api-v1.yaml`:浏览器访问的公共业务接口。
- `demo-core-extension-v1.yaml`:第 15 小步冻结的 Demo 目标扩展接口;
`platform-api-v1.yaml` 合并后构成完整公共 API v1。
- `storage-api-internal-v1.yaml`Storage Service 内部接口。
- `runtime-api-internal-v1.yaml`Runtime Manager 内部接口。
现有服务契约由对应 FastAPI 应用生成;目标扩展契约先冻结、后实现,不能
标记为已上线。所有文件均纳入结构校验。公共接口统一使用 `/api/v1`
内部服务接口统一使用 `/internal/v1`
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,699 +0,0 @@
openapi: 3.1.0
info:
title: runtime-manager service
version: 0.1.0
paths:
/:
get:
summary: Root
operationId: root__get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
type: string
type: object
title: Response Root Get
/health/live:
get:
summary: Live
operationId: live_health_live_get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
type: string
type: object
title: Response Live Health Live Get
/api/v1/health:
get:
summary: Public Health
operationId: public_health_api_v1_health_get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
type: string
type: object
title: Response Public Health Api V1 Health Get
/health/ready:
get:
summary: Ready
operationId: ready_health_ready_get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties: true
type: object
title: Response Ready Health Ready Get
/internal/v1/file-locks/acquire:
post:
summary: Acquire File Lock
operationId: acquire_file_lock_internal_v1_file_locks_acquire_post
parameters:
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AcquireFileLockRequest'
responses:
'201':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Acquire File Lock Internal V1 File Locks Acquire Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/file-locks/{edit_session_id}/heartbeat:
post:
summary: Heartbeat File Lock
operationId: heartbeat_file_lock_internal_v1_file_locks__edit_session_id__heartbeat_post
parameters:
- name: edit_session_id
in: path
required: true
schema:
type: string
title: Edit Session Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/FileLockTokenRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Heartbeat File Lock Internal V1 File Locks Edit Session Id Heartbeat
Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/file-locks/{edit_session_id}:
delete:
summary: Release File Lock
operationId: release_file_lock_internal_v1_file_locks__edit_session_id__delete
parameters:
- name: edit_session_id
in: path
required: true
schema:
type: string
title: Edit Session Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/FileLockTokenRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Release File Lock Internal V1 File Locks Edit Session Id Delete
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/jupyter/access-tickets/{edit_session_id}:
post:
summary: Create Jupyter Access Ticket
operationId: create_jupyter_access_ticket_internal_v1_jupyter_access_tickets__edit_session_id__post
parameters:
- name: edit_session_id
in: path
required: true
schema:
type: string
title: Edit Session Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/FileLockTokenRequest'
responses:
'201':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Create Jupyter Access Ticket Internal V1 Jupyter Access Tickets Edit
Session Id Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/jupyter/authorize:
get:
summary: Authorize Jupyter Proxy
operationId: authorize_jupyter_proxy_internal_v1_jupyter_authorize_get
parameters:
- name: X-Original-URI
in: header
required: true
schema:
type: string
title: X-Original-Uri
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
- name: jupyter_access
in: cookie
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Jupyter Access
responses:
'204':
description: Successful Response
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/runtimes/ensure:
post:
summary: Ensure Runtime
operationId: ensure_runtime_internal_v1_runtimes_ensure_post
parameters:
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EnsureRuntimeApiRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Ensure Runtime Internal V1 Runtimes Ensure Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/runtimes/{runtime_id}:
get:
summary: Get Runtime
operationId: get_runtime_internal_v1_runtimes__runtime_id__get
parameters:
- name: runtime_id
in: path
required: true
schema:
type: string
title: Runtime Id
- name: workspace_id
in: query
required: true
schema:
type: string
minLength: 26
maxLength: 26
title: Workspace Id
- name: user_id
in: query
required: true
schema:
type: string
minLength: 26
maxLength: 26
title: User Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Get Runtime Internal V1 Runtimes Runtime Id Get
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
delete:
summary: Stop Runtime
operationId: stop_runtime_internal_v1_runtimes__runtime_id__delete
parameters:
- name: runtime_id
in: path
required: true
schema:
type: string
title: Runtime Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StopRuntimeApiRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Stop Runtime Internal V1 Runtimes Runtime Id Delete
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/runtimes/{runtime_id}/restart:
post:
summary: Restart Runtime
operationId: restart_runtime_internal_v1_runtimes__runtime_id__restart_post
parameters:
- name: runtime_id
in: path
required: true
schema:
type: string
title: Runtime Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RuntimeIdentityRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Restart Runtime Internal V1 Runtimes Runtime Id Restart Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/runtimes/{runtime_id}/sessions:
post:
summary: Create Runtime Session
operationId: create_runtime_session_internal_v1_runtimes__runtime_id__sessions_post
parameters:
- name: runtime_id
in: path
required: true
schema:
type: string
title: Runtime Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRuntimeSessionApiRequest'
responses:
'201':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Create Runtime Session Internal V1 Runtimes Runtime Id Sessions Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/runtimes/{runtime_id}/sessions/{session_id}:
delete:
summary: Terminate Runtime Session
operationId: terminate_runtime_session_internal_v1_runtimes__runtime_id__sessions__session_id__delete
parameters:
- name: runtime_id
in: path
required: true
schema:
type: string
title: Runtime Id
- name: session_id
in: path
required: true
schema:
type: string
title: Session Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RuntimeIdentityRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Terminate Runtime Session Internal V1 Runtimes Runtime Id Sessions Session
Id Delete
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/health/runtime:
get:
summary: Internal Health
operationId: internal_health_internal_health_runtime_get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
type: string
type: object
title: Response Internal Health Internal Health Runtime Get
components:
schemas:
AcquireFileLockRequest:
properties:
workspace_id:
type: string
maxLength: 26
minLength: 26
title: Workspace Id
storage_object_id:
type: string
maxLength: 26
minLength: 26
title: Storage Object Id
user_id:
type: string
maxLength: 26
minLength: 26
title: User Id
request_id:
anyOf:
- type: string
maxLength: 64
minLength: 1
- type: 'null'
title: Request Id
additionalProperties: false
type: object
required:
- workspace_id
- storage_object_id
- user_id
title: AcquireFileLockRequest
CreateRuntimeSessionApiRequest:
properties:
workspace_id:
type: string
maxLength: 26
minLength: 26
title: Workspace Id
user_id:
type: string
maxLength: 26
minLength: 26
title: User Id
request_id:
anyOf:
- type: string
maxLength: 64
minLength: 1
- type: 'null'
title: Request Id
storage_object_id:
type: string
maxLength: 26
minLength: 26
title: Storage Object Id
relative_path:
type: string
maxLength: 1024
minLength: 1
title: Relative Path
additionalProperties: false
type: object
required:
- workspace_id
- user_id
- storage_object_id
- relative_path
title: CreateRuntimeSessionApiRequest
EnsureRuntimeApiRequest:
properties:
workspace_id:
type: string
maxLength: 26
minLength: 26
title: Workspace Id
user_id:
type: string
maxLength: 26
minLength: 26
title: User Id
request_id:
anyOf:
- type: string
maxLength: 64
minLength: 1
- type: 'null'
title: Request Id
additionalProperties: false
type: object
required:
- workspace_id
- user_id
title: EnsureRuntimeApiRequest
FileLockTokenRequest:
properties:
workspace_id:
type: string
maxLength: 26
minLength: 26
title: Workspace Id
user_id:
type: string
maxLength: 26
minLength: 26
title: User Id
lock_token:
type: string
maxLength: 256
minLength: 32
title: Lock Token
additionalProperties: false
type: object
required:
- workspace_id
- user_id
- lock_token
title: FileLockTokenRequest
HTTPValidationError:
properties:
detail:
items:
$ref: '#/components/schemas/ValidationError'
type: array
title: Detail
type: object
title: HTTPValidationError
RuntimeIdentityRequest:
properties:
workspace_id:
type: string
maxLength: 26
minLength: 26
title: Workspace Id
user_id:
type: string
maxLength: 26
minLength: 26
title: User Id
request_id:
anyOf:
- type: string
maxLength: 64
minLength: 1
- type: 'null'
title: Request Id
additionalProperties: false
type: object
required:
- workspace_id
- user_id
title: RuntimeIdentityRequest
StopRuntimeApiRequest:
properties:
workspace_id:
type: string
maxLength: 26
minLength: 26
title: Workspace Id
user_id:
type: string
maxLength: 26
minLength: 26
title: User Id
request_id:
anyOf:
- type: string
maxLength: 64
minLength: 1
- type: 'null'
title: Request Id
reason:
type: string
maxLength: 64
minLength: 1
title: Reason
default: client_request
additionalProperties: false
type: object
required:
- workspace_id
- user_id
title: StopRuntimeApiRequest
ValidationError:
properties:
loc:
items:
anyOf:
- type: string
- type: integer
type: array
title: Location
msg:
type: string
title: Message
type:
type: string
title: Error Type
type: object
required:
- loc
- msg
- type
title: ValidationError
@@ -1,550 +0,0 @@
openapi: 3.1.0
info:
title: storage-api service
version: 0.1.0
paths:
/:
get:
summary: Root
operationId: root__get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
type: string
type: object
title: Response Root Get
/health/live:
get:
summary: Live
operationId: live_health_live_get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
type: string
type: object
title: Response Live Health Live Get
/api/v1/health:
get:
summary: Public Health
operationId: public_health_api_v1_health_get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
type: string
type: object
title: Response Public Health Api V1 Health Get
/health/ready:
get:
summary: Ready
operationId: ready_health_ready_get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties: true
type: object
title: Response Ready Health Ready Get
/internal/v1/uploads:
post:
summary: Create Upload
operationId: create_upload_internal_v1_uploads_post
parameters:
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUploadRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Create Upload Internal V1 Uploads Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/uploads/{upload_id}/complete:
post:
summary: Complete Upload
operationId: complete_upload_internal_v1_uploads__upload_id__complete_post
parameters:
- name: upload_id
in: path
required: true
schema:
type: string
title: Upload Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CompleteUploadRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Complete Upload Internal V1 Uploads Upload Id Complete Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/uploads/{upload_id}/abort:
post:
summary: Abort Upload
operationId: abort_upload_internal_v1_uploads__upload_id__abort_post
parameters:
- name: upload_id
in: path
required: true
schema:
type: string
title: Upload Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Abort Upload Internal V1 Uploads Upload Id Abort Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/objects:
post:
summary: Create Server Object
operationId: create_server_object_internal_v1_objects_post
parameters:
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ServerObjectRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Create Server Object Internal V1 Objects Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/workspace-objects:
post:
summary: Register Workspace Object
operationId: register_workspace_object_internal_v1_workspace_objects_post
parameters:
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterWorkspaceObjectRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Register Workspace Object Internal V1 Workspace Objects Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/objects/{storage_object_id}/download-url:
post:
summary: Create Download Url
operationId: create_download_url_internal_v1_objects__storage_object_id__download_url_post
parameters:
- name: storage_object_id
in: path
required: true
schema:
type: string
title: Storage Object Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DownloadUrlRequest'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Create Download Url Internal V1 Objects Storage Object Id Download Url
Post
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/v1/objects/{storage_object_id}:
delete:
summary: Delete Object
operationId: delete_object_internal_v1_objects__storage_object_id__delete
parameters:
- name: storage_object_id
in: path
required: true
schema:
type: string
title: Storage Object Id
- name: X-Service-Token
in: header
required: true
schema:
type: string
title: X-Service-Token
responses:
'200':
description: Successful Response
content:
application/json:
schema:
type: object
additionalProperties: true
title: Response Delete Object Internal V1 Objects Storage Object Id Delete
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/internal/health/storage:
get:
summary: Internal Health
operationId: internal_health_internal_health_storage_get
responses:
'200':
description: Successful Response
content:
application/json:
schema:
additionalProperties:
type: string
type: object
title: Response Internal Health Internal Health Storage Get
components:
schemas:
CompleteUploadRequest:
properties:
usage_type:
type: string
enum:
- data_resource
- version_artifact
- snapshot
- run_log
- run_result
title: Usage Type
visibility:
type: string
enum:
- private
- workspace
- public
title: Visibility
default: private
is_immutable:
type: boolean
title: Is Immutable
default: false
additionalProperties: false
type: object
required:
- usage_type
title: CompleteUploadRequest
CreateUploadRequest:
properties:
workspace_id:
type: string
maxLength: 26
minLength: 26
title: Workspace Id
user_id:
type: string
maxLength: 26
minLength: 26
title: User Id
usage_type:
type: string
enum:
- data_resource
- version_artifact
- snapshot
- run_log
- run_result
title: Usage Type
file_name:
type: string
maxLength: 255
minLength: 1
title: File Name
content_type:
type: string
maxLength: 255
minLength: 1
title: Content Type
expected_size_bytes:
type: integer
maximum: 104857600.0
minimum: 0.0
title: Expected Size Bytes
expected_hash:
anyOf:
- type: string
maxLength: 64
minLength: 64
- type: 'null'
title: Expected Hash
idempotency_key:
type: string
maxLength: 128
minLength: 8
title: Idempotency Key
url_scope:
type: string
enum:
- public
- internal
title: Url Scope
default: public
additionalProperties: false
type: object
required:
- workspace_id
- user_id
- usage_type
- file_name
- content_type
- expected_size_bytes
- idempotency_key
title: CreateUploadRequest
DownloadUrlRequest:
properties:
expires_seconds:
type: integer
maximum: 3600.0
minimum: 30.0
title: Expires Seconds
default: 300
additionalProperties: false
type: object
title: DownloadUrlRequest
HTTPValidationError:
properties:
detail:
items:
$ref: '#/components/schemas/ValidationError'
type: array
title: Detail
type: object
title: HTTPValidationError
RegisterWorkspaceObjectRequest:
properties:
workspace_id:
type: string
maxLength: 26
minLength: 26
title: Workspace Id
user_id:
type: string
maxLength: 26
minLength: 26
title: User Id
relative_path:
type: string
maxLength: 1024
minLength: 1
title: Relative Path
usage_type:
type: string
enum:
- working_copy
- public_script
title: Usage Type
visibility:
type: string
enum:
- private
- workspace
- public
title: Visibility
default: private
additionalProperties: false
type: object
required:
- workspace_id
- user_id
- relative_path
- usage_type
title: RegisterWorkspaceObjectRequest
ServerObjectRequest:
properties:
workspace_id:
type: string
maxLength: 26
minLength: 26
title: Workspace Id
user_id:
type: string
maxLength: 26
minLength: 26
title: User Id
usage_type:
type: string
enum:
- data_resource
- version_artifact
- snapshot
- run_log
- run_result
title: Usage Type
file_name:
type: string
maxLength: 255
minLength: 1
title: File Name
content_type:
type: string
maxLength: 255
minLength: 1
title: Content Type
content_base64:
type: string
minLength: 1
title: Content Base64
visibility:
type: string
enum:
- private
- workspace
- public
title: Visibility
default: private
is_immutable:
type: boolean
title: Is Immutable
default: false
idempotency_key:
type: string
maxLength: 128
minLength: 8
title: Idempotency Key
additionalProperties: false
type: object
required:
- workspace_id
- user_id
- usage_type
- file_name
- content_type
- content_base64
- idempotency_key
title: ServerObjectRequest
ValidationError:
properties:
loc:
items:
anyOf:
- type: string
- type: integer
type: array
title: Location
msg:
type: string
title: Message
type:
type: string
title: Error Type
type: object
required:
- loc
- msg
- type
title: ValidationError
-26
View File
@@ -1,26 +0,0 @@
# Runtime Adapter 契约
`runtime_adapter.py` 是 Runtime Manager 与具体运行环境之间的可执行契约。
当前 Compose 环境使用 `SharedJupyterAdapter`。它把共享 Jupyter Server 作为
首期 Provider,并在 MySQL 中为每个 `Workspace` 建立并复用一个逻辑
Runtime。每个打开的 Notebook 在该 Runtime 中建立独立 Jupyter Session
每个 Session 绑定自己的 Kernel。
后续 Docker/Kubernetes Provider 必须实现同一 Protocol,公共锁接口和
`runtime_instances` 状态模型保持不变。
核心约束:
- `ensure_running` 对同一作用域必须幂等;
- Runtime 的唯一复用作用域是 Workspace,不能按用户重复创建;
- Runtime 的期望状态、实际状态和租约写入 MySQL;
- Notebook 对应独立 Jupyter Session/Kernel,其创建和终止只能由 Runtime
Manager 调用;
- Provider 不能依赖 Platform API 的进程内状态;
- `proxy_base_path` 与 Jupyter 内部 `base_url` 必须一致。
- Provider 调用必须携带内部服务凭据,不能通过关闭 XSRF 检查规避认证;
- Workspace 的目录、文件权限必须同时满足 Platform 原子写入和 Jupyter
读写,当前 Compose 环境使用共享 Unix 组完成。
Jupyter 浏览器代理、访问票据和 WebSocket 约束见
`jupyter-proxy-v1.md`
-1
View File
@@ -1 +0,0 @@
"""Runtime adapter contract package."""
-65
View File
@@ -1,65 +0,0 @@
# Jupyter 代理与访问票据契约 V1
## 1. 浏览器访问流程
1. 前端携带用户、Workspace、`edit_session_id``lock_token` 调用
`POST /api/v1/jupyter/access-tickets`
2. Platform API 校验编辑锁、Runtime 与 Jupyter Session 的归属及状态。
3. 成功后返回 `/jupyter/...` 地址,并设置短期
`jupyter_access` Cookie。
4. 前端在当前平台页面的编辑区内嵌同源 `/jupyter/...` 页面,不新开浏览器
标签页。
5. 内嵌页面经 Nginx 访问 Jupyter HTTP/WebSocketNginx 先执行内部鉴权
子请求,再把内部 Token 注入上游请求。
一个 Workspace 只建立或复用一个 Jupyter Runtime/Server;同一 Server
中的每个 Notebook 使用独立 Jupyter Session 和独立 Kernel。
浏览器响应、URL、JavaScript 和日志中都不得出现 Jupyter 内部 Token。
## 2. 访问票据
- 票据绑定:`user_id``workspace_id``edit_session_id`
`runtime_id``jupyter_session_id`
- 默认有效期:60 秒;最长不超过 5 分钟。
- Cookie`HttpOnly; SameSite=Lax; Path=/jupyter/`,生产环境必须增加
`Secure`
- 票据只授权当前 Workspace 的 Jupyter 路径,不能跨 Workspace 使用。
- Jupyter 页面必须与平台使用同一站点入口,并允许同源 iframe 嵌入。
## 3. Nginx 内部鉴权接口
```http
GET /internal/v1/jupyter/authorize
Cookie: jupyter_access=<opaque-ticket>
X-Original-URI: /jupyter/...
X-Request-ID: <request-id>
```
该接口只允许 Nginx 在内部网络调用:
- `204`:票据有效,响应头提供上游地址和内部认证信息。
- `401`:票据缺失、伪造或过期。
- `403`:用户、Workspace、Session 不匹配,或编辑锁/Runtime 已失效。
成功响应头:
```http
X-Jupyter-Upstream: http://jupyter:8888
X-Jupyter-Authorization: token <internal-token>
X-Workspace-ID: <workspace-id>
```
Nginx 必须删除客户端传入的 `Authorization`,使用内部鉴权结果重建上游
认证头。
## 4. WebSocket 约束
- `/jupyter/api/kernels/*/channels` 与其他 WebSocket 路径使用 HTTP/1.1。
- 转发 `Upgrade``Connection``Host``Origin` 和请求 ID。
- 关闭代理缓冲,读超时不小于 3600 秒。
- HTTP 和 WebSocket 使用同一票据校验规则。
- 连接关闭只断开交互连接,不自动停止 Workspace Runtime。
- Compose Demo 使用最长 5 分钟票据;前端必须继续发送编辑锁心跳,并在
票据到期前重新签发。
- Jupyter Lab HTML 中出现的内部 Token 必须由 Nginx 在返回浏览器前清除。
-70
View File
@@ -1,70 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import Protocol
@dataclass(frozen=True)
class EnsureRuntimeRequest:
runtime_id: str
workspace_id: str
workspace_code: str
owner_user_id: str
@dataclass(frozen=True)
class RuntimeEndpoint:
runtime_id: str
runtime_type: str
provider: str
runtime_ref: str
internal_url: str
proxy_base_path: str
@dataclass(frozen=True)
class RuntimeHealth:
runtime_id: str
healthy: bool
checked_at: datetime
detail: str | None = None
@dataclass(frozen=True)
class CreateSessionRequest:
runtime_id: str
workspace_code: str
relative_path: str
@dataclass(frozen=True)
class RuntimeSession:
runtime_id: str
session_id: str
jupyter_url: str
reused: bool
class RuntimeAdapter(Protocol):
async def ensure_running(
self,
request: EnsureRuntimeRequest,
) -> RuntimeEndpoint: ...
async def stop(self, runtime_id: str, reason: str) -> None: ...
async def restart(self, runtime_id: str) -> RuntimeEndpoint: ...
async def health(self, runtime_id: str) -> RuntimeHealth: ...
async def create_session(
self,
request: CreateSessionRequest,
) -> RuntimeSession: ...
async def terminate_session(
self,
runtime_id: str,
session_id: str,
) -> None: ...
@@ -1,122 +0,0 @@
# 调度定义 API 契约 V1
冻结日期:2026-07-28
状态:`implemented`
## 1. 范围
本契约只覆盖调度定义,不触发任务执行:
- 调度方案增删改查;
- 稳定版本制品列表;
- 节点和连线增删改;
- 五段 Cron 校验与未来时间预览;
- DAG 完整性和有向无环校验。
立即运行、Cron 自动触发和 Executor 执行已经由独立 Schedule 服务承接。
## 2. 请求上下文
所有接口都要求:
| Header | 含义 |
|---|---|
| `X-User-ID` | 当前用户 |
| `X-Workspace-ID` | 当前 Workspace |
| `X-Request-ID` | 请求追踪 ID;可省略,由服务端生成 |
服务端只返回当前 Workspace 的调度。节点引用的 `versions_id` 必须属于当前
Workspace,且当前用户有权读取。
## 3. HTTP 接口
| 方法 | 路径 | 作用 |
|---|---|---|
| `GET` | `/api/v1/schedule-artifacts` | 查询可加入调度的稳定版本 |
| `POST` | `/api/v1/cron/preview` | 校验 Cron 并预览未来时间 |
| `GET` | `/api/v1/schedules` | 查询调度列表 |
| `POST` | `/api/v1/schedules` | 新建调度 |
| `GET` | `/api/v1/schedules/{schedule_id}` | 查询调度及完整 DAG |
| `PUT/PATCH` | `/api/v1/schedules/{schedule_id}` | 修改调度基本信息 |
| `DELETE` | `/api/v1/schedules/{schedule_id}` | 软删除调度 |
| `POST` | `/api/v1/schedules/{schedule_id}/nodes` | 新建节点 |
| `PUT` | `/api/v1/schedules/{schedule_id}/nodes/{node_id}` | 修改节点 |
| `DELETE` | `/api/v1/schedules/{schedule_id}/nodes/{node_id}` | 删除节点及关联连线 |
| `POST` | `/api/v1/schedules/{schedule_id}/edges` | 新建有向连线 |
| `PUT` | `/api/v1/schedules/{schedule_id}/edges/{edge_id}` | 修改连线条件 |
| `DELETE` | `/api/v1/schedules/{schedule_id}/edges/{edge_id}` | 删除连线 |
| `POST` | `/api/v1/schedules/{schedule_id}/validate` | 校验当前 DAG |
成功响应统一为:
```json
{
"request_id": "01...",
"data": {},
"meta": {}
}
```
## 4. 并发修改契约
新建调度时 `workflow_version=1`。每次修改调度、节点或连线都必须在请求体中
提交当前 `workflow_version`,成功后版本号加一。
版本不一致时返回 `412 Precondition Failed`
```json
{
"detail": {
"code": "WORKFLOW_VERSION_CONFLICT",
"message": "schedule was modified by another request",
"expected": 3,
"current": 4
}
}
```
前端收到 `412` 后必须重新读取调度,不得用旧画布直接覆盖。
## 5. 调度与 Cron 约束
- `trigger_type``manual / cron / api`
- `failure_policy``stop / continue`
- Cron 固定为五段:`minute hour day month weekday`
- 时区使用 IANA 名称,例如 `Asia/Shanghai`
- `cron` 类型必须提供 `cron_expression`,其他类型不得提供;
- 新建的空调度不能直接启用;
- 只有 DAG 校验通过的调度才能设为 `enabled=true`
- `next_run_at` 在数据库和接口中按 UTC 保存和返回。
Cron 预览请求示例:
```json
{
"cron_expression": "*/5 * * * *",
"timezone": "Asia/Shanghai",
"count": 5,
"base_time": "2026-07-28T08:00:00+08:00"
}
```
## 6. 节点与 DAG 约束
- 节点必须引用不可变稳定版本 `versions_id`,不能引用工作副本;
- 同一个调度内 `node_key` 唯一;
- 节点保存超时、重试、位置、参数和环境引用;
- 连线的起点、终点必须属于同一调度;
- 不允许自环、重复连线和有向环;
- 删除节点会同时删除与该节点相连的边;
- 已有运行历史的节点不能物理删除;
- 校验结果返回根节点、叶节点、拓扑顺序和错误列表。
## 7. 典型状态码
| 状态码 | 场景 |
|---|---|
| `201` | 调度、节点或连线创建成功 |
| `404` | 调度、稳定版本、节点或连线不存在/不可见 |
| `409` | 名称冲突、重复连线、DAG 成环或无效 DAG 启用 |
| `412` | `workflow_version` 已过期 |
| `422` | 请求字段、Cron、时区或节点归属不合法 |