feat: 新增 compute_gateway、compute_poller、agent 模块,重构前端 dist
- 新增 backend/app/modules/compute_gateway(client/sync)计算网关模块 - 新增 backend/app/workers/compute_poller 计算轮询 worker - 新增 compute/agent/process_manager 进程管理器 - 新增 scripts/ 脚本目录 - 更新 Docker 部署配置(app/compute/nginx) - 更新后端平台 API、数据库 SQL、core 配置 - 更新前端多个视图组件及 API 模块 - 重构 frontend/dist 构建产物(新 hash) - 更新多项文档 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -271,7 +271,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
||||
| POST | `/modelTF/dataset-manage` | 创建数据集 |
|
||||
| PUT | `/modelTF/dataset-manage/{id}` | 更新数据集 |
|
||||
| DELETE | `/modelTF/dataset-manage/{id}` | 删除数据集 |
|
||||
| POST | `/modelTF/dataset-manage/upload/{dataset_id}` | 上传文件,字段名 `files` |
|
||||
| POST | `/modelTF/dataset-manage/upload/{dataset_id}` | 上传文件,字段名 `files`;默认同步到启用的算力节点 `/data/yg-ft/datasets/{dataset_id}/` |
|
||||
| GET | `/modelTF/dataset-manage/download/{dataset_id}` | 打包下载数据集 |
|
||||
| GET | `/modelTF/dataset-manage/download/{dataset_id}/{file_id}` | 下载单文件 |
|
||||
|
||||
@@ -451,7 +451,9 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
||||
| GET | `/modelTF/fine-tune/{id}` | 训练任务详情 |
|
||||
| GET | `/modelTF/fine-tune/check-name?name=xxx` | 任务名查重 |
|
||||
| POST | `/modelTF/fine-tune` | 创建训练任务记录 |
|
||||
| POST | `/modelTF/fine-tune/start` | 启动训练 |
|
||||
| POST | `/modelTF/fine-tune/{id}/command-preview` | 训练创建页/详情页命令预览,返回目标节点、Compute Job payload 和 LLaMA-Factory 命令 |
|
||||
| POST | `/modelTF/fine-tune/{id}/preflight` | 训练创建页启动前预检,校验节点、模型路径、数据集路径、引擎命令和训练参数 |
|
||||
| POST | `/modelTF/fine-tune/start` | 启动训练,应用侧选择算力节点并提交 Compute Job |
|
||||
| PUT | `/modelTF/fine-tune/{id}` | 更新任务 |
|
||||
| POST | `/modelTF/fine-tune/stop/{id}` | 停止任务 |
|
||||
| DELETE | `/modelTF/fine-tune/{id}` | 删除任务 |
|
||||
@@ -494,6 +496,49 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
||||
}
|
||||
```
|
||||
|
||||
训练启动前检查和命令预览:
|
||||
|
||||
- 页面模块:`/fine-tune/create` 创建训练任务的“参数确认/启动训练”区域;`/training-log/:id` 训练详情页的“任务配置/命令查看”区域。
|
||||
- `POST /modelTF/fine-tune/{id}/command-preview`:不做远端路径强校验,只返回应用侧调度出的算力节点、标准 Compute Job payload、训练引擎命令和工作目录,供前端展示最终 LLaMA-Factory 启动命令。
|
||||
- `POST /modelTF/fine-tune/{id}/preflight`:启动前强校验,真实 `llama_factory` 会检查目标节点连通性、模型路径、数据集目录、LLaMA-Factory HOME、训练命令是否可用;`smoke` 引擎用于自动化闭环验收,会跳过模型/数据集路径检查。
|
||||
- `POST /modelTF/fine-tune/start`:内部先执行 preflight,预检失败返回 `409` 且任务保持 `pending`,预检通过后再写入 `syncing/queued/running` 运行态并提交 Compute Job。
|
||||
|
||||
请求体可传启动覆盖参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"requested_node_id": "node_xxx",
|
||||
"gpus": [0],
|
||||
"batch_size": 1,
|
||||
"learning_rate": 0.0002,
|
||||
"n_epochs": 1
|
||||
}
|
||||
```
|
||||
|
||||
响应结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"node": {
|
||||
"id": "node_xxx",
|
||||
"code": "gpu-node-01",
|
||||
"scheduler_status": "online",
|
||||
"gpu_count": 1
|
||||
},
|
||||
"job_payload": {},
|
||||
"preview": {
|
||||
"engine": "llama_factory",
|
||||
"command": ["llamafactory-cli", "train", "..."],
|
||||
"command_text": "llamafactory-cli train ...",
|
||||
"work_dir": "/app/LLaMA-Factory",
|
||||
"path_checks": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 训练日志详情页
|
||||
|
||||
训练日志页还会联合调用:
|
||||
@@ -841,7 +886,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
||||
| POST | `/modelTF/compute/nodes` | 新增算力节点 |
|
||||
| GET | `/modelTF/compute/nodes/{id}` | 算力节点详情 |
|
||||
| PUT | `/modelTF/compute/nodes/{id}` | 编辑节点地址、权重、标签、路径和启用状态 |
|
||||
| POST | `/modelTF/compute/nodes/{id}/test-connection` | 测试 Compute API/File Gateway 连通性 |
|
||||
| POST | `/modelTF/compute/nodes/{id}/test-connection` | 测试 Compute API/File Gateway 连通性,并同步节点健康信息和 GPU 清单 |
|
||||
| POST | `/modelTF/compute/nodes/{id}/enable` | 启用节点 |
|
||||
| POST | `/modelTF/compute/nodes/{id}/disable` | 禁用节点,不接收新任务 |
|
||||
| POST | `/modelTF/compute/nodes/{id}/drain` | 进入维护模式,已有任务跑完后下线 |
|
||||
@@ -853,6 +898,7 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
||||
| GET | `/modelTF/compute/jobs/{id}` | 算力任务详情 |
|
||||
| POST | `/modelTF/compute/jobs/{id}/retry` | 重试任务 |
|
||||
| POST | `/modelTF/compute/jobs/{id}/priority` | 调整优先级 |
|
||||
| GET | `/modelTF/compute/jobs/{id}/logs` | 拉取算力任务训练日志,支持 tail/分页 |
|
||||
| POST | `/modelTF/internal/compute-sync/jobs/poll` | 应用平台主动轮询并同步算力任务状态 |
|
||||
| POST | `/modelTF/internal/compute-sync/resources` | 调度前同步数据集/模型到目标节点 |
|
||||
|
||||
@@ -862,6 +908,82 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
||||
- 每个可执行训练的节点都需要部署 `Compute API`、`Compute Agent`、`File Gateway` 和宿主机挂载的 LLaMA-Factory。
|
||||
- 节点之间默认不互相访问,应用平台主动访问所有节点的 Compute API/File Gateway。
|
||||
- 调度支持 `auto` 和 `manual`:普通用户默认自动调度,管理员或高级用户可手动指定节点。
|
||||
- 节点地址、权重、标签、启用状态、最大并发和本地路径都由 `/compute` 算力节点页面维护。
|
||||
- 连接测试由应用后端发起,依次探测算力侧 `GET /modelTF/v1/compute/health` 和 `GET /modelTF/compute/resources/gpus`;返回包可为裸 JSON,也可为 `{code,message,data}` 包装结构。
|
||||
|
||||
新增/编辑节点请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "gpu-node-01",
|
||||
"name": "A800 Node 01",
|
||||
"api_base_url": "http://10.10.20.31:19100",
|
||||
"file_gateway_url": "http://10.10.20.31:19101",
|
||||
"enabled": true,
|
||||
"scheduler_status": "offline",
|
||||
"scheduler_weight": 100,
|
||||
"tags": ["A800", "80GB", "llama_factory"],
|
||||
"max_parallel_jobs": 4,
|
||||
"data_root": "/data/yg-ft",
|
||||
"model_root": "/data/yg-ft/models",
|
||||
"log_root": "/opt/yg-ft/logs/training",
|
||||
"description": "北京机房训练节点"
|
||||
}
|
||||
```
|
||||
|
||||
启动成功后,响应中的训练任务会包含 `compute_node_id`、`compute_job_id`、`process_id`、`status`、`progress`、`output_dir`、`log_file` 等字段。应用侧后台 worker 会按 `COMPUTE_POLL_INTERVAL_SECONDS` 定时调用目标算力节点查询 Compute Job,并回写训练任务状态。
|
||||
|
||||
算力任务日志查询参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tail_lines` | int | 否 | 默认 `200`,返回最后 N 行,范围 `1-5000` |
|
||||
| `offset` | int | 否 | 从第 N 行开始读取;当传入 `offset` 或 `limit` 时分页优先,忽略默认 tail 行数 |
|
||||
| `limit` | int | 否 | 分页读取行数,范围 `1-5000` |
|
||||
|
||||
响应字段包括 `content`、`metrics`、`total_lines`、`offset`、`limit`、`has_more`、`next_offset`。前端训练详情页、训练日志页和算力队列页可以用该接口增量读取日志,避免一次性拉取大文件。
|
||||
|
||||
任务维度实时日志接口:`GET /modelTF/fine-tune/{task_id}/logs?tail_lines=500`。该接口由应用后端按任务绑定的 `compute_node_id` 和 `compute_job_id` 转发到目标算力节点日志接口;如果训练尚未创建 Compute Job 或远端日志暂时不可达,则返回任务 `failure_reason`,用于页面展示启动失败、预检失败和远端训练失败原因。
|
||||
|
||||
算力任务重试:
|
||||
|
||||
```json
|
||||
{
|
||||
"force": false,
|
||||
"priority": "high",
|
||||
"requested_node_id": "node_xxx",
|
||||
"gpus": [0]
|
||||
}
|
||||
```
|
||||
|
||||
默认只允许 `failed`、`stopped` 任务重试;如确需重新执行已完成任务,需要显式传 `force=true`。重试会清空旧的运行时字段,重新调度节点并创建新的 Compute Job。
|
||||
|
||||
算力任务优先级:
|
||||
|
||||
```json
|
||||
{
|
||||
"priority": "low|normal|high|urgent"
|
||||
}
|
||||
```
|
||||
|
||||
第一版优先级写入任务 payload,并影响 `/modelTF/compute/queue` 的展示排序;后续如接入独立队列调度器,可保持接口不变,将该字段映射到调度器优先级。
|
||||
|
||||
连接测试响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"node_id": "node_xxx",
|
||||
"success": true,
|
||||
"latency_ms": 35,
|
||||
"gpu_count": 8,
|
||||
"health": {
|
||||
"status": "ok",
|
||||
"api_version": "v1",
|
||||
"execution_mode": "real",
|
||||
"capabilities": ["gpu_discovery", "llama_factory", "file_gateway", "job_polling"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
算力节点响应字段:
|
||||
|
||||
@@ -882,6 +1004,9 @@ page=1&page_size=20&keyword=xxx&sort=-created_at
|
||||
"data_root": "/data/yg-ft",
|
||||
"model_root": "/data/yg-ft/models",
|
||||
"log_root": "/opt/yg-ft/logs/compute",
|
||||
"api_version": "v1",
|
||||
"capabilities": ["gpu_discovery", "llama_factory"],
|
||||
"description": "北京机房训练节点",
|
||||
"last_health_check_at": "2026-07-20T12:00:00+08:00",
|
||||
"health_detail": {
|
||||
"compute_api": "ok",
|
||||
@@ -916,11 +1041,15 @@ GPU 响应字段:
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| POST | `/modelTF/compute/jobs` | 创建训练/评测/数据处理/推理任务 |
|
||||
| POST | `/modelTF/compute/jobs/preview` | 算力节点训练命令预览,不启动进程 |
|
||||
| POST | `/modelTF/compute/jobs/validate` | 算力节点训练启动前预检,校验参数、路径和引擎命令 |
|
||||
| GET | `/modelTF/compute/jobs/{id}` | 查询任务 |
|
||||
| POST | `/modelTF/compute/jobs/{id}/stop` | 停止任务 |
|
||||
| GET | `/modelTF/compute/jobs/{id}/logs` | 拉取日志 |
|
||||
| POST | `/modelTF/compute/files/check-paths` | 算力节点本地路径可用性检查 |
|
||||
| GET | `/modelTF/compute/resources/gpus` | 查询 GPU |
|
||||
| POST | `/modelTF/compute/files/upload` | 上传到算力本地磁盘 |
|
||||
| POST | `/modelTF/compute/files/import-local` | 从算力服务器本地路径导入到 `YG_FT_DATA_ROOT` |
|
||||
| GET | `/modelTF/compute/files/{id}/download` | 下载文件 |
|
||||
|
||||
创建算力任务:
|
||||
@@ -957,6 +1086,48 @@ GPU 响应字段:
|
||||
}
|
||||
```
|
||||
|
||||
当前 LLaMA-Factory 训练作业最小 payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "ft_xxx",
|
||||
"name": "finance-sft-001",
|
||||
"engine": "llama_factory",
|
||||
"base_model": "/data/yg-ft/models/Qwen2.5-7B",
|
||||
"model_name_or_path": "/data/yg-ft/models/Qwen2.5-7B",
|
||||
"dataset": "finance_train",
|
||||
"dataset_dir": "/data/yg-ft/datasets",
|
||||
"output_dir": "/data/yg-ft/outputs/finance-sft-001",
|
||||
"template": "qwen",
|
||||
"train_method": "lora",
|
||||
"gpus": [0],
|
||||
"batch_size": 2,
|
||||
"learning_rate": 0.0002,
|
||||
"n_epochs": 3,
|
||||
"save_steps": 50
|
||||
}
|
||||
```
|
||||
|
||||
应用侧轮询同步响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"synced": 1,
|
||||
"failed": [],
|
||||
"items": [
|
||||
{
|
||||
"id": "ft_xxx",
|
||||
"status": "running",
|
||||
"progress": 35,
|
||||
"compute_job_id": "ft_xxx",
|
||||
"process_id": 52341,
|
||||
"output_dir": "/data/yg-ft/outputs/finance-sft-001",
|
||||
"log_file": "/opt/yg-ft/logs/training/ft_xxx.log"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
手动指定节点时:
|
||||
|
||||
```json
|
||||
|
||||
@@ -228,6 +228,20 @@ GPU 算力服务器部署:
|
||||
|
||||
多节点任务调度由应用平台统一完成。应用平台从 `compute_nodes` 读取节点地址、权重、标签、启用状态、维护状态和健康检查结果;从 `resource_replicas` 判断目标节点是否已有所需数据集/模型副本;缺失时创建 `resource_sync_jobs`,通过目标节点 File Gateway 同步资源。
|
||||
|
||||
当前实现已支持在 `/compute` 算力节点页面新增和编辑节点。运维人员维护 `Compute API` 地址、`File Gateway` 地址、权重、标签、启用状态、最大并发和本地路径后,点击连接测试会由应用后端主动访问目标节点健康检查和 GPU 清单接口,并将 `health_detail`、`gpu_count`、`gpu_devices/gpus` 同步到 PostgreSQL。真实 GPU 服务器优先通过 `nvidia-smi` 发现 GPU;特殊环境可用 `COMPUTE_GPU_COUNT` 等环境变量声明兼容清单。
|
||||
|
||||
训练运行闭环:
|
||||
|
||||
- 前端启动训练后,Backend API 按 `compute_nodes` 的启用状态、调度状态、权重和并行任务数选择节点。
|
||||
- Backend API 向目标节点 `POST /modelTF/compute/jobs` 提交 LLaMA-Factory 训练作业,并在 `fine_tune_tasks.compute_job_id` 记录算力任务 ID。
|
||||
- Compute API 在真实模式下启动 `llamafactory-cli train` 子进程,训练日志写入 `TRAINING_LOG_ROOT/{job_id}.log`。
|
||||
- Backend API 启动后会运行应用侧轮询 worker,按 `COMPUTE_POLL_INTERVAL_SECONDS` 主动查询目标节点 `GET /modelTF/compute/jobs/{id}`,同步任务状态、进度、PID、输出目录、日志路径和产物索引。
|
||||
- 停止训练时,Backend API 优先调用目标节点 `POST /modelTF/compute/jobs/{id}/stop`,再回写应用任务状态。
|
||||
- 失败或停止任务可以通过 `POST /modelTF/compute/jobs/{id}/retry` 重试;重试会清空旧运行态,重新调度节点并创建 Compute Job。
|
||||
- 训练日志通过 `GET /modelTF/compute/jobs/{id}/logs` 读取,支持 `tail_lines`、`offset`、`limit`,用于训练详情页、训练日志页和日志平台采集。
|
||||
- Compute API 使用 `COMPUTE_SERVICE_TOKEN` 做服务间鉴权,应用侧请求携带 `X-Compute-Token`;健康检查接口保持可公开探活。
|
||||
- Compute API 会把本机训练作业登记到 `TRAINING_LOG_ROOT/compute-jobs.json`,服务重启后可恢复任务索引并继续暴露状态和日志。
|
||||
|
||||
调度策略:
|
||||
|
||||
- 默认自动调度,按节点健康、标签、GPU 空闲、队列长度、节点权重和资源副本命中率排序。
|
||||
@@ -279,7 +293,7 @@ COMPUTE_API_BASE_URL=https://compute.internal:19100
|
||||
COMPUTE_SERVICE_TOKEN=***
|
||||
FILE_GATEWAY_BASE_URL=https://compute.internal:19101
|
||||
COMPUTE_STATUS_SYNC_MODE=polling
|
||||
COMPUTE_POLL_INTERVAL_SECONDS=10
|
||||
COMPUTE_POLL_INTERVAL_SECONDS=3
|
||||
COMPUTE_POLL_BATCH_SIZE=100
|
||||
```
|
||||
|
||||
@@ -290,10 +304,21 @@ COMPUTE_ENV=prod
|
||||
COMPUTE_HOST_ID=gpu-node-01
|
||||
COMPUTE_API_PORT=19100
|
||||
FILE_GATEWAY_PORT=19101
|
||||
COMPUTE_AUTH_ENABLED=true
|
||||
COMPUTE_SERVICE_TOKEN=***
|
||||
ENABLE_APP_CALLBACK=false
|
||||
LLAMA_FACTORY_HOME=/app/LLaMA-Factory
|
||||
YG_FT_DATA_ROOT=/data/yg-ft
|
||||
YG_FT_DATA_ROOT_HOST=./data/yg-ft
|
||||
YG_FT_MODEL_ROOT=/data/yg-ft/models
|
||||
YG_FT_MODEL_ROOT_HOST=./data/yg-ft/models
|
||||
YG_FT_DATASET_ROOT=/data/yg-ft/datasets
|
||||
YG_FT_DATASET_ROOT_HOST=./data/yg-ft/datasets
|
||||
YG_FT_OUTPUT_ROOT=/data/yg-ft/outputs
|
||||
YG_FT_OUTPUT_ROOT_HOST=./data/yg-ft/outputs
|
||||
TRAINING_LOG_ROOT=/opt/yg-ft/logs/training
|
||||
TRAINING_LOG_ROOT_HOST=./data/yg-ft/logs/training
|
||||
COMPUTE_LOG_ROOT_HOST=./data/yg-ft/logs/compute
|
||||
LOG_DIR=/opt/yg-ft/logs/compute
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3
|
||||
```
|
||||
@@ -393,7 +418,7 @@ gpu-node-03 -> http://10.10.20.33:19100 / http://10.10.20.33:19101
|
||||
```env
|
||||
ENABLE_APP_CALLBACK=false
|
||||
COMPUTE_SERVICE_TOKEN=change_me
|
||||
YG_FT_DATA_ROOT_HOST=/data/yg-ft
|
||||
YG_FT_DATA_ROOT_HOST=./data/yg-ft
|
||||
```
|
||||
|
||||
## 12. 仍需确认的问题
|
||||
|
||||
@@ -1061,8 +1061,20 @@ CREATE TABLE IF NOT EXISTS compute_nodes (
|
||||
name varchar(150) NOT NULL,
|
||||
host varchar(200) NOT NULL,
|
||||
api_base_url text NOT NULL,
|
||||
file_gateway_url text NOT NULL DEFAULT '',
|
||||
storage_node_id uuid REFERENCES storage_nodes(id) ON DELETE SET NULL,
|
||||
status varchar(40) NOT NULL DEFAULT 'online',
|
||||
scheduler_status varchar(40) NOT NULL DEFAULT 'online',
|
||||
scheduler_weight integer NOT NULL DEFAULT 100,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
max_parallel_jobs integer NOT NULL DEFAULT 1,
|
||||
data_root text NOT NULL DEFAULT '/data/yg-ft',
|
||||
model_root text NOT NULL DEFAULT '/data/yg-ft/models',
|
||||
log_root text NOT NULL DEFAULT '/opt/yg-ft/logs/training',
|
||||
api_version varchar(40) NOT NULL DEFAULT 'v1',
|
||||
capabilities jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
description text,
|
||||
health_detail jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
agent_version varchar(80),
|
||||
gpu_count integer NOT NULL DEFAULT 0,
|
||||
last_heartbeat_at timestamptz,
|
||||
@@ -1434,10 +1446,15 @@ ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES t
|
||||
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
|
||||
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required';
|
||||
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS compute_node_id uuid REFERENCES compute_nodes(id) ON DELETE SET NULL;
|
||||
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL;
|
||||
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS resume_checkpoint_id uuid REFERENCES fine_tune_checkpoints(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_scope_status
|
||||
ON fine_tune_tasks(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_compute_job
|
||||
ON fine_tune_tasks(compute_job_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_node_status
|
||||
ON fine_tune_tasks(compute_node_id, status, created_at DESC) WHERE deleted_at IS NULL;
|
||||
|
||||
ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
|
||||
ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
|
||||
|
||||
Reference in New Issue
Block a user