feat(models): 模型一律转 VRM(契约 0.5.0)+ 可用性门槛 + 管理端版本号展示

按《DSH 开发文档 v1.6》4.9 与第十一章实现,Web 端第一步:

1. 契约先行(先改 contracts/ 再同步三端):
   * model-formats.md -> 0.5.0:模型主力格式改为 VRM,所有上传模型一律转 VRM
     (.pmx/.fbx: assimp -> GLB -> VRM;glb/gltf: 包装为 VRM);
     新增 humanoid 硬门槛与"不达标回退 GLB + usable=false"的交付表;
   * data-models.md -> 0.5.0:Model 新增 usable / unusableReason,明确
     usable=false 不进入 catalog/export;format 说明改为主力 vrm;
   * VERSION 与 contract-versions.ts 同步(三端契约文件逐字节一致)。
2. 实现:
   * MODEL_CONVERT_FORMAT 默认改为 vrm(deploy/.env.example 同步);
   * 新增 isUsableModel()/unusableReason():usable 等价于"已产出 VRM",
     原因从 stats 里带出 humanoid 覆盖度;
   * catalog/export 与 catalog/version 指纹都只统计 format=vrm 的已发布模型,
     resolveDefaultModelId 同样只在可用模型中选;
   * 管理端:模型列表与审核面板显示「不可用」及原因;
   * 管理端左下角展示当前版本号(构建期从 web-server/package.json 注入
     __APP_VERSION__,与 GET /health 同源),并去掉写死的"契约 rest-api 0.1.0"。
3. 测试:新增 test/model-usability.spec.ts 5 项;规模 16 文件 / 225 项。
4. 版本 0.3.0。
This commit is contained in:
opendance
2026-09-17 00:05:02 +08:00
parent eebccd55b4
commit 39bfd390e3
27 changed files with 311 additions and 49 deletions
+4
View File
@@ -160,6 +160,10 @@ export interface Model extends SourceFields {
name: string;
format: ModelFormat;
vrmVersion: string | null;
/** 是否可用于客户端(= 已产出 VRM);false 的模型不进入客户端目录(v0.5.0) */
usable: boolean;
/** usable=false 的原因,例如「humanoid 不达标(0/22),未产出 VRM」 */
unusableReason: string | null;
isPrimary: boolean;
fileUrl: string | null;
fileSize: number | null;
+7 -1
View File
@@ -1,5 +1,7 @@
import { NavLink, Outlet } from 'react-router-dom';
import { APP_VERSION_LABEL } from '../lib/version';
interface NavItem {
to: string;
label: string;
@@ -44,9 +46,13 @@ export function Layout() {
))}
</nav>
<div className="sidebar-foot">
{/* 当前版本号:左下角固定展示,来源 web-server/package.json(与 /health 同源) */}
<span className="app-version" title="当前版本号">
{APP_VERSION_LABEL}
</span>
<span>API 基址</span>
<code>{API_BASE === '' ? '同源(dev proxy / nginx)' : API_BASE}</code>
<span className="muted">契约 rest-api 0.1.0</span>
<span className="muted">契约版本见「概览」页</span>
</div>
</aside>
<main className="content">
@@ -203,6 +203,17 @@ function ModelStatsView({ model, stats }: { model: Model; stats: ModelStats }) {
<Metric label="骨骼最大层级" value={formatCount(stats.maxBoneDepth)} />
</div>
{/* 可用性门槛(v0.5.0):未产出 VRM 的模型不进入客户端目录 */}
{!model.usable ? (
<div className="banner banner-danger" role="alert">
<strong>不可用:该模型不会进入客户端目录</strong>
<span>{model.unusableReason ?? '未产出 VRM'}</span>
<span className="muted">
依据契约 model-formats 1.1,所有模型一律转 VRM;humanoid 必需骨齐全后重新导入即可转为可用。
</span>
</div>
) : null}
{/* 可驱动性结论 */}
<div className={`banner ${BANNER_CLASS[drivable.kind]}`} role={drivable.kind === 'error' ? 'alert' : 'status'}>
<strong>
+10
View File
@@ -0,0 +1,10 @@
/**
* 应用版本号(构建期由 vite.config.ts 从 web-server/package.json 注入)。
* 页面左下角必须展示它,且与 `GET /health` 的 version、发布产物文件名一致
* (开发文档 v1.5 第十一章)。SSR / 测试环境下回退为 '0.0.0'。
*/
export const APP_VERSION: string =
typeof __APP_VERSION__ === 'string' && __APP_VERSION__.length > 0 ? __APP_VERSION__ : '0.0.0';
/** 展示用:带 v 前缀 */
export const APP_VERSION_LABEL = `v${APP_VERSION}`;
+8
View File
@@ -492,6 +492,14 @@ export function ModelsPage() {
</td>
<td>
<FormatBadge format={model.format} kind="model" />
{!model.usable ? (
<span
className="badge badge-rejected"
title={model.unusableReason ?? '未产出 VRM,不进入客户端目录'}
>
不可用
</span>
) : null}
</td>
<td className="mono">{model.vrmVersion ?? '—'}</td>
<td>
+8
View File
@@ -173,6 +173,14 @@ h3 {
color: var(--text-mute);
}
.app-version {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
font-weight: 600;
color: var(--text-strong, #e8ecf3);
letter-spacing: 0.02em;
}
.sidebar-foot code {
color: var(--text-dim);
word-break: break-all;
+3
View File
@@ -7,3 +7,6 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
/** 构建期注入的应用版本号(来自 web-server/package.json) */
declare const __APP_VERSION__: string;