Files
opendance-web/contracts/rest-api.yaml
T
zijunchiang 53bf182476 feat(models): 发布 v0.2.0 —— GLB/VRM 主力格式、自动缩略图与分析、模型审核与骨骼预览
需求 1:系统确定以 GLB / VRM 为模型主力格式
- 服务端新增支持库:
  - model-inspector.ts:解析 GLB/VRM 的面数、顶点、材质、贴图、形变目标、
    **骨骼树(名称/层级/是否蒙皮/humanoid 映射/子节点数)**、humanoid 覆盖率与可驱动性
  - model-thumbnail.ts:**纯 TS 软件光栅化**(z-buffer + Lambert + 完整蒙皮矩阵)
    生成 512×512 PNG 缩略图,零原生依赖、不装 headless-gl
  - vrm-writer.ts:GLB → VRM 1.0(写入 VRMC_vrm 扩展与 humanoid 映射)
  - gltf-packer.ts:glTF(.gltf + .bin + 贴图)→ 单文件 GLB
- humanoid 候选表与 Godot 客户端 humanoid_map.gd **逐字对齐**,并有单测断言两边一致
  (服务端判定的"可驱动" = 客户端实际能否驱动)

需求 2:模型管理
- .glb / .vrm / .gltf **直接导入**(不重编码,GLB 含 VRM 扩展会自动识别为 VRM)
- .pmx / .fbx 服务端转换:assimp → .glb →(失败)→ .gltf 后自行打包成 GLB
  →(MODEL_CONVERT_FORMAT=vrm 时)补 VRMC_vrm 产出 VRM 1.0;每步 assimp info 回读校验
- .zip **自动搜索包内可用模型**:.vrm → .glb → .gltf(直接导入)优先,
  其次 .pmx → .fbx(转换);同优先级内按"指名字 → 目录最浅 → 文件最大"
- 入库后自动分析(models.stats)与生成缩略图(models.thumbnail_asset_id),
  失败均不阻断入库;管理端列表显示小缩略图、预览放开到 vrm/glb/gltf

需求 3:审核功能
- 上传的模型状态**一律 pending**(不再自动发布、不再自动设默认)
- 审核界面新增模型面板:体积、面数、顶点、材质、贴图、骨骼、形变目标等指标,
  humanoid 覆盖进度、可驱动性结论(缺哪些必需骨骼)、warnings,以及
  **骨骼详细预览**(层级缩进、蒙皮/Humanoid 徽章、子节点数、搜索保留祖先链、
  仅看蒙皮、限高滚动 + 只渲染前 400 行)
- 审核通过可带 setPrimary=true 一并设为默认模型(此时才写源码种子);
  POST /api/models/:id/primary 仅允许 published(否则 409)
- scripts/import-default-model.mjs 改为"上传 → 审核通过 + 设默认"流程

其它:
- models 新增 stats 列(迁移 AddModelStats1789460000000,已在真实库执行)
- 契约升至 rest-api/data-models/model-formats 0.4.0、database 0.3.0(另两库已同步)
- 版本号 0.1.0 → 0.2.0(package.json ×2 + lockfile + project.yaml)
- 文档:README.txt / DSH.md / project.yaml / tasks 005·007·010 全面更新;
  顶层开发书更新为 v1.4

验证:168 项单测通过(10 文件);契约 6 项与 29 条路由一致;typecheck/build 通过;
真实端到端:MMD 压缩包 → GLB + 分析(面数 9071/骨骼 238/humanoid 0-22) + 缩略图(15KB PNG);
GLB 直接导入 sha256 与源文件一致;审核通过后 defaultModelId 由 VRM 变为该模型
2026-09-15 20:37:53 +08:00

861 lines
29 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.0.3
info:
title: OpenDance Studio Web 管理服务端 API
version: 0.4.0
description: |
OpenDance Studio 的唯一数据源与契约权威。
契约版本见 contracts/VERSION。字段语义见 contracts/data-models.md。
- 所有 JSON 字段 camelCase
- 列表接口统一 { items, total, page, pageSize }
- 来源五元组(sourceType/sourceName/sourceUrl/license/author)在三类资产上均为必填
servers:
- url: http://localhost:3000
description: 本地开发(软件 API 端口)
- url: http://<host>:3000
description: >-
Linux 业务服务部署的**软件 API 端口**(`PORT`,默认 3000)。Windows/Godot 客户端与
Android App 调用本端口的 /api、/health、/static、/contracts。
管理端页面在**管理端口**(`ADMIN_PORT`,默认 3001),该端口同时提供全部接口;
API 端口不提供管理页面,未命中的路径一律 404。
反向代理由另外的服务提供(非本项目交付物),按用途分别转发到对应端口即可。
tags:
- name: health
- name: songs
- name: motions
- name: models
- name: reviews
- name: catalog
- name: assets
paths:
/health:
get:
tags: [health]
summary: 健康检查(含数据库连通性)
responses:
'200':
description: 服务与数据库均正常
content:
application/json:
schema:
$ref: '#/components/schemas/Health'
'503':
description: 数据库不可用
content:
application/json:
schema:
$ref: '#/components/schemas/Health'
/api/songs:
get:
tags: [songs]
summary: 曲目列表
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
- name: q
in: query
schema: { type: string }
description: 按 title/artist/album 模糊匹配
- name: status
in: query
schema: { $ref: '#/components/schemas/PublishStatus' }
- name: artist
in: query
schema: { type: string }
responses:
'200':
description: 分页曲目
content:
application/json:
schema:
$ref: '#/components/schemas/SongPage'
post:
tags: [songs]
summary: 新建曲目
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SongCreate'
responses:
'201':
description: 已创建
content:
application/json:
schema: { $ref: '#/components/schemas/Song' }
'400': { $ref: '#/components/responses/BadRequest' }
/api/songs/{id}:
parameters: [{ $ref: '#/components/parameters/Id' }]
get:
tags: [songs]
summary: 曲目详情
responses:
'200':
description: 曲目
content:
application/json:
schema: { $ref: '#/components/schemas/Song' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [songs]
summary: 更新曲目
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/SongUpdate' }
responses:
'200':
description: 更新后的曲目
content:
application/json:
schema: { $ref: '#/components/schemas/Song' }
'400': { $ref: '#/components/responses/BadRequest' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [songs]
summary: 删除曲目(软删除)
responses:
'204': { description: 已删除 }
'404': { $ref: '#/components/responses/NotFound' }
/api/songs/{id}/audio:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
tags: [songs]
summary: 上传曲目音频
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file: { type: string, format: binary }
responses:
'201':
description: 上传后的曲目
content:
application/json:
schema: { $ref: '#/components/schemas/Song' }
'400': { $ref: '#/components/responses/BadRequest' }
/api/songs/{id}/cover:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
tags: [songs]
summary: 上传曲目封面
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file: { type: string, format: binary }
responses:
'201':
description: 上传后的曲目
content:
application/json:
schema: { $ref: '#/components/schemas/Song' }
'400': { $ref: '#/components/responses/BadRequest' }
/api/motions:
get:
tags: [motions]
summary: 动作列表
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
- name: q
in: query
schema: { type: string }
- name: songId
in: query
schema: { type: integer, format: int64 }
- name: format
in: query
schema: { type: string, enum: [bvh, vmd, fbx, gltf, glb, keypoints-json] }
- name: status
in: query
schema: { $ref: '#/components/schemas/PublishStatus' }
responses:
'200':
description: 分页动作
content:
application/json:
schema: { $ref: '#/components/schemas/MotionPage' }
post:
tags: [motions]
summary: 上传动作文件 + 来源信息
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file, name, sourceType, sourceName, sourceUrl, license, author]
properties:
file: { type: string, format: binary }
name: { type: string }
songId: { type: integer, format: int64 }
fps: { type: number }
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
license: { type: string }
author: { type: string }
status: { $ref: '#/components/schemas/PublishStatus' }
autoPreprocess: { type: boolean, default: false }
responses:
'201':
description: 已创建
content:
application/json:
schema: { $ref: '#/components/schemas/Motion' }
'400': { $ref: '#/components/responses/BadRequest' }
'422':
description: 文件格式不支持或无法解析
/api/motions/{id}:
parameters: [{ $ref: '#/components/parameters/Id' }]
get:
tags: [motions]
summary: 动作详情
responses:
'200':
description: 动作
content:
application/json:
schema: { $ref: '#/components/schemas/Motion' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [motions]
summary: 更新动作元数据
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/MotionUpdate' }
responses:
'200':
description: 更新后的动作
content:
application/json:
schema: { $ref: '#/components/schemas/Motion' }
'400': { $ref: '#/components/responses/BadRequest' }
delete:
tags: [motions]
summary: 删除动作(软删除)
responses:
'204': { description: 已删除 }
/api/motions/{id}/preprocess:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
tags: [motions]
summary: 触发 BVH/FBX → MediaPipe33 关键点序列预处理(同步执行)
description: |
同步执行并返回最终状态:成功时 preprocessState=done;
失败时返回 422,且 Motion.preprocessError 记录可读原因。
responses:
'200':
description: 预处理完成
content:
application/json:
schema: { $ref: '#/components/schemas/Motion' }
'404': { $ref: '#/components/responses/NotFound' }
'422':
description: 预处理失败,reason 见 preprocessError
/api/motions/{id}/keypoints:
parameters: [{ $ref: '#/components/parameters/Id' }]
get:
tags: [motions]
summary: 下载关键点序列(NDJSON,每行一帧)
responses:
'200':
description: |
application/x-ndjson,每行一个帧对象:
{"frame":0,"timeMs":0,"points":[[x,y,z,conf], …33],"angles":[9 个余弦]}
content:
application/x-ndjson:
schema: { type: string }
'404': { $ref: '#/components/responses/NotFound' }
'409':
description: 尚未完成预处理
/api/models:
get:
tags: [models]
summary: 模型列表
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
- name: q
in: query
schema: { type: string }
- name: format
in: query
schema: { type: string, enum: [vrm, glb, gltf, fbx] }
- name: status
in: query
schema: { $ref: '#/components/schemas/PublishStatus' }
responses:
'200':
description: 分页模型
content:
application/json:
schema: { $ref: '#/components/schemas/ModelPage' }
post:
tags: [models]
summary: 上传模型(GLB/VRM 直接导入;PMX/FBX 服务端转 GLB 或 VRM)
description: |
系统以 **GLB / VRM 为主力格式**(两个客户端都原生加载):
- `.glb` / `.vrm` / `.gltf`:**直接导入**,不重编码
- `.pmx` / `.fbx`:服务端转换,**优先 GLB、其次 VRM**
- `.zip`:**自动搜索包内可用模型** —— `.vrm` → `.glb` → `.gltf`(直接导入)
优先,其次 `.pmx` → `.fbx`(转换)。压缩包原件作为 `model-archive` 资产留存。
转换链路(`model-convert.ts`):`assimp export → .glb` →(失败)`→ .gltf` 后
由服务端自行打包成 GLB →(`MODEL_CONVERT_FORMAT=vrm` 时)在 GLB 之上补
`VRMC_vrm` 扩展产出 VRM 1.0。每一步都 `assimp info` **回读校验**,
并把过程写进 `conversionNote`;贴图会内嵌进 GLB,保证单文件可用。
入库时自动做两件事并写入 `stats` / `thumbnailUrl`:模型分析(面数、骨骼树、
humanoid 覆盖、可驱动性)与缩略图生成。
**状态一律 `pending`(待审核)**:模型必须审核通过后才可能被客户端消费;
设为默认模型请在审核通过时带 `setPrimary=true`,或调用
`POST /api/models/{id}/primary`(仅已发布模型可设)。
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file, name, sourceType, sourceName, sourceUrl, license, author]
properties:
file: { type: string, format: binary }
name: { type: string }
isPrimary: { type: boolean }
# 注意:上传不再提供"自动发布/自动设为默认",模型一律进入待审核
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
license: { type: string }
author: { type: string }
status: { $ref: '#/components/schemas/PublishStatus' }
responses:
'201':
description: 已创建
content:
application/json:
schema: { $ref: '#/components/schemas/Model' }
'400': { $ref: '#/components/responses/BadRequest' }
'422':
description: 不支持的模型格式
/api/models/{id}:
parameters: [{ $ref: '#/components/parameters/Id' }]
get:
tags: [models]
summary: 模型详情
responses:
'200':
description: 模型
content:
application/json:
schema: { $ref: '#/components/schemas/Model' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [models]
summary: 更新模型元数据
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ModelUpdate' }
responses:
'200':
description: 更新后的模型
content:
application/json:
schema: { $ref: '#/components/schemas/Model' }
'400': { $ref: '#/components/responses/BadRequest' }
delete:
tags: [models]
summary: 删除模型(软删除)
responses:
'204': { description: 已删除 }
/api/models/{id}/primary:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
tags: [models]
summary: 设为全局默认模型(互斥,同事务清零其余)
responses:
'200':
description: 设置后的模型
content:
application/json:
schema: { $ref: '#/components/schemas/Model' }
'404': { $ref: '#/components/responses/NotFound' }
/api/reviews:
get:
tags: [reviews]
summary: 审核流水 / 待审列表
parameters:
- name: targetType
in: query
schema: { type: string, enum: [song, motion, model] }
- name: targetId
in: query
schema: { type: integer, format: int64 }
- name: status
in: query
schema: { $ref: '#/components/schemas/PublishStatus' }
description: 传入时改为返回该状态的待审资产列表(合并三类)
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
responses:
'200':
description: 审核流水或待审资产
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/ReviewPage'
- $ref: '#/components/schemas/PendingPage'
/api/reviews/{targetType}/{id}/submit:
parameters:
- name: targetType
in: path
required: true
schema: { type: string, enum: [song, motion, model] }
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 提交审核
requestBody:
content:
application/json:
schema:
type: object
properties:
reviewer: { type: string }
comment: { type: string }
responses:
'200': { description: 状态已流转 }
'409': { description: 当前状态不允许该操作 }
/api/reviews/{targetType}/{id}/approve:
parameters:
- name: targetType
in: path
required: true
schema: { type: string, enum: [song, motion, model] }
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 审核通过 → published
requestBody:
content:
application/json:
schema:
type: object
properties:
reviewer: { type: string }
comment: { type: string }
responses:
'200': { description: 状态已流转 }
'409': { description: 当前状态不允许该操作 }
/api/reviews/{targetType}/{id}/withdraw:
parameters:
- name: targetType
in: path
required: true
schema: { type: string, enum: [song, motion, model] }
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 撤回审核 → draft
requestBody:
content:
application/json:
schema:
type: object
properties:
reviewer: { type: string }
comment: { type: string }
responses:
'200': { description: 状态已流转 }
'409': { description: 当前状态不允许该操作 }
/api/reviews/{targetType}/{id}/history:
parameters:
- name: targetType
in: path
required: true
schema: { type: string, enum: [song, motion, model] }
- $ref: '#/components/parameters/Id'
get:
tags: [reviews]
summary: 单个资产的审核历史
responses:
'200':
description: 审核流水(按时间倒序)
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/ReviewRecord' }
'404': { $ref: '#/components/responses/NotFound' }
/api/reviews/{targetType}/{id}/reject:
parameters:
- name: targetType
in: path
required: true
schema: { type: string, enum: [song, motion, model] }
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 审核驳回 → rejected
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [comment]
properties:
reviewer: { type: string }
comment: { type: string }
responses:
'200': { description: 状态已流转 }
'400': { description: 驳回必须填写理由 }
'409': { description: 当前状态不允许该操作 }
/api/catalog/export:
get:
tags: [catalog]
summary: 客户端一次性拉取已发布目录
responses:
'200':
description: CatalogBundle
content:
application/json:
schema: { $ref: '#/components/schemas/CatalogBundle' }
/api/catalog/version:
get:
tags: [catalog]
summary: 契约版本与目录指纹(客户端增量同步用)
responses:
'200':
description: 版本信息
content:
application/json:
schema:
type: object
properties:
contractVersion: { type: object, additionalProperties: { type: string } }
revision: { type: string, description: 目录内容 sha256,变化即需重新拉取 }
components:
parameters:
Id:
name: id
in: path
required: true
schema: { type: integer, format: int64 }
Page:
name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
PageSize:
name: pageSize
in: query
schema: { type: integer, minimum: 1, maximum: 200, default: 20 }
responses:
BadRequest:
description: 参数校验失败(含来源五元组缺失)
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
NotFound:
description: 资源不存在
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
schemas:
SourceType:
type: string
enum: [original, licensed, cc, public_domain, user_upload, other]
PublishStatus:
type: string
enum: [draft, pending, published, rejected]
MotionFormat:
type: string
enum: [bvh, vmd, fbx, gltf, glb, keypoints-json]
ModelFormat:
type: string
enum: [vrm, glb, gltf, fbx]
ApiError:
type: object
properties:
statusCode: { type: integer }
message: { oneOf: [{ type: string }, { type: array, items: { type: string } }] }
error: { type: string }
SourceFields:
type: object
required: [sourceType, sourceName, sourceUrl, license, author]
properties:
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string, maxLength: 200 }
sourceUrl: { type: string, maxLength: 500, description: 必须为 http(s) URL }
license: { type: string, maxLength: 100 }
author: { type: string, maxLength: 200 }
Health:
type: object
properties:
status: { type: string, enum: [ok, degraded] }
uptimeSec: { type: number }
version: { type: string }
contractVersion: { type: object, additionalProperties: { type: string } }
database:
type: object
properties:
connected: { type: boolean }
server: { type: string, description: '如 MariaDB 10.11.19' }
database: { type: string }
migrationsPending: { type: integer }
Song:
allOf:
- $ref: '#/components/schemas/SourceFields'
- type: object
properties:
id: { type: integer, format: int64 }
title: { type: string }
artist: { type: string }
album: { type: string, nullable: true }
language: { type: string, nullable: true }
durationMs: { type: integer, nullable: true }
bpm: { type: number, nullable: true }
difficulty: { type: integer, nullable: true, minimum: 1, maximum: 5 }
tags: { type: array, items: { type: string } }
coverUrl: { type: string, nullable: true }
audioUrl: { type: string, nullable: true }
status: { $ref: '#/components/schemas/PublishStatus' }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
SongCreate:
allOf:
- $ref: '#/components/schemas/SourceFields'
- type: object
required: [title, artist]
properties:
title: { type: string, maxLength: 200 }
artist: { type: string, maxLength: 200 }
album: { type: string, maxLength: 200 }
language: { type: string }
durationMs: { type: integer }
bpm: { type: number }
difficulty: { type: integer, minimum: 1, maximum: 5 }
tags: { type: array, items: { type: string } }
status: { $ref: '#/components/schemas/PublishStatus' }
SongUpdate:
type: object
description: 所有字段可选;提供 source* 时必须五元组同时提供
properties:
title: { type: string }
artist: { type: string }
album: { type: string }
language: { type: string }
durationMs: { type: integer }
bpm: { type: number }
difficulty: { type: integer, minimum: 1, maximum: 5 }
tags: { type: array, items: { type: string } }
status: { $ref: '#/components/schemas/PublishStatus' }
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
license: { type: string }
author: { type: string }
SongPage:
type: object
properties:
items: { type: array, items: { $ref: '#/components/schemas/Song' } }
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
Motion:
allOf:
- $ref: '#/components/schemas/SourceFields'
- type: object
properties:
id: { type: integer, format: int64 }
songId: { type: integer, format: int64, nullable: true }
name: { type: string }
format: { $ref: '#/components/schemas/MotionFormat' }
fileUrl: { type: string, nullable: true }
fileSize: { type: integer, nullable: true }
fps: { type: number, nullable: true }
frameCount: { type: integer, nullable: true }
durationMs: { type: integer, nullable: true }
keypointFormat: { type: string, nullable: true }
keypointUrl: { type: string, nullable: true }
keypointFrames: { type: integer, nullable: true }
preprocessState: { type: string, enum: [none, running, done, failed] }
preprocessError: { type: string, nullable: true }
status: { $ref: '#/components/schemas/PublishStatus' }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
MotionUpdate:
type: object
properties:
name: { type: string }
songId: { type: integer, format: int64, nullable: true }
fps: { type: number }
status: { $ref: '#/components/schemas/PublishStatus' }
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
license: { type: string }
author: { type: string }
MotionPage:
type: object
properties:
items: { type: array, items: { $ref: '#/components/schemas/Motion' } }
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
Model:
allOf:
- $ref: '#/components/schemas/SourceFields'
- type: object
properties:
id: { type: integer, format: int64 }
name: { type: string }
format: { $ref: '#/components/schemas/ModelFormat' }
vrmVersion: { type: string, nullable: true }
isPrimary: { type: boolean }
fileUrl: { type: string, nullable: true }
fileSize: { type: integer, nullable: true }
thumbnailUrl: { type: string, nullable: true }
conversionNote:
type: string
nullable: true
description: 由压缩包转换而来时的说明;直传模型为 null
fileSha256:
type: string
nullable: true
description: 模型文件(转换产物)的 sha256 —— 每次转换都会变(Assimp 导出非字节级确定),仅作记录
stats:
type: object
nullable: true
description: >-
模型分析结果(面数 / 顶点数 / 贴图 / 骨骼树 / humanoid 覆盖 / 可驱动性),
审核界面据此判断"能不能驱动与评分"。结构见 contracts/model-formats.md §1.1
sourceSha256:
type: string
nullable: true
description: 上传压缩包的 sha256(稳定);默认模型匹配以它为准,非压缩包上传为 null
status: { $ref: '#/components/schemas/PublishStatus' }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
ModelUpdate:
type: object
properties:
name: { type: string }
status: { $ref: '#/components/schemas/PublishStatus' }
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
license: { type: string }
author: { type: string }
ModelPage:
type: object
properties:
items: { type: array, items: { $ref: '#/components/schemas/Model' } }
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
ReviewRecord:
type: object
properties:
id: { type: integer, format: int64 }
targetType: { type: string, enum: [song, motion, model] }
targetId: { type: integer, format: int64 }
action: { type: string, enum: [submit, approve, reject, withdraw] }
fromStatus: { $ref: '#/components/schemas/PublishStatus' }
toStatus: { $ref: '#/components/schemas/PublishStatus' }
reviewer: { type: string, nullable: true }
comment: { type: string, nullable: true }
createdAt: { type: string, format: date-time }
ReviewPage:
type: object
properties:
items: { type: array, items: { $ref: '#/components/schemas/ReviewRecord' } }
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
PendingPage:
type: object
properties:
items:
type: array
items:
type: object
properties:
targetType: { type: string, enum: [song, motion, model] }
targetId: { type: integer, format: int64 }
name: { type: string }
status: { $ref: '#/components/schemas/PublishStatus' }
source: { $ref: '#/components/schemas/SourceFields' }
updatedAt: { type: string, format: date-time }
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
CatalogBundle:
type: object
properties:
contractVersion: { type: object, additionalProperties: { type: string } }
generatedAt: { type: string, format: date-time }
songs: { type: array, items: { $ref: '#/components/schemas/Song' } }
motions: { type: array, items: { $ref: '#/components/schemas/Motion' } }
models: { type: array, items: { $ref: '#/components/schemas/Model' } }
defaultModelId: { type: integer, format: int64, nullable: true }