title, date, draft, tags, categories
| title |
date |
draft |
tags |
categories |
| CrowdRoom · 资产库(Asset Library)模块设计(v0.2) |
2026-05-20 |
false |
| CrowdRoom |
| 众包 |
| 3D 重建 |
| 导航 |
| Web |
| 数据结构 |
|
|
CrowdRoom · 资产库(Asset Library)模块设计(v0.2)
版本:v0.2(2026-05-19)· 子任务 10 产出
定位:把 01_data_schema.md §3.9 中仅作为"平台 CC0 公共素材池"存在的 public.assets 表,升级为一个支持「平台爬取导入 + 用户 UGC 上传 + Remix 衍生」三源流的资产库子系统;同时逐条兑现 05_object_replacement_handbook.md §15 移交的 AL-1 ~ AL-10 硬契约。
本文档不修改任何 v0.2 既有架构——所有对 01_data_schema.md / 02_api_contract.md / 04_web_app_plan.md / 10_governance.md 的扩展统一以「建议增量」标注;落地需在下一版(v0.3)对应文档中追认。
文档语言:简体中文;DDL / TS / JSON 字段命名:英文(与既有契约对齐)。
0. 阅读导航
| 你的角色 |
重点章节 |
| 后端 / DBA |
§2 增量 ALTER · §6 端点表 |
| Edge Function / Worker |
§4 平台导入管线 · §5.2 UGC 预处理 worker · §5.3 自动初筛 |
| Web 前端(AssetPicker) |
§3 Storage 路径 · §6 E-A3 检索 · §7 排序公式 |
| Web 前端(上传向导) |
§5.1 5 步向导 · §6 E-A1/E-A2 |
| 运营 / 审核员 |
§5.3 审核工作台 · §8 创作者信任分 · §9 协议 |
| 法务 |
§9 版权 · §10 收藏与署名 |
| QA |
§14 M1 验收 · §15 风险 |
| Reviewer(一眼对账) |
§2.6 AL 映射表 · §13 关系矩阵 · §14.2 AL 对照表 |
1. 资产库定位与产品形态
1.1 一句话定义
"CrowdRoom 资产库 = 平台 CC0 公共资产(Quaternius / Poly Haven / ambientCG 等爬取/导入)+ 用户上传自制资产(UGC,CC0 或 CC-BY 二选一)+ 资产 Remix 衍生(用户基于已有资产二次创作)。"
它对应 05_object_replacement_handbook.md §4 AssetPicker 的唯一上游数据源——AssetPicker 拉的所有家具/材质卡片均来自本资产库;同时把 10_governance.md §4 P-W-4 决策的「平台资产 CC0 白名单」从一张孤立的内部素材表,扩展为一个有用户参与、有创作者飞轮的子社区。
1.2 三类资产对照
| # |
类型 |
来源 |
协议(MVP) |
准入流程 |
默认 trust_score |
AssetPicker 排序权重 |
| T1 平台 CC0 |
source_type='platform' |
运营批量爬取/导入(Quaternius、Poly Haven、ambientCG、Sketchfab CC0 子集) |
一律 CC0 |
自动入库 review_status='approved' |
80 |
高(默认头部) |
| T2 用户上传 |
source_type='user_upload' |
普通用户走 §5 上传向导 |
CC0(MVP 强制);CC-BY 见 v0.5 ROADMAP |
自动初筛 + 人工 24-72 h 审 |
30(新手)/ 50(Trusted Creator 跳过人工) |
中(按 §7 综合公式) |
| T3 Remix 衍生 |
source_type='remix_of' + parent_asset_id |
用户 fork 已有资产 → 改 PBR / mesh → 发布 |
继承父协议(CC0 → 可选;CC-BY → 必须 CC-BY) |
同 T2 |
继承父 × 0.8 |
中低(避免刷量) |
MVP 一致性声明(与 10_governance.md §4 P-W-4 对齐):
- MVP 阶段 T1 全 CC0;T2 / T3 用户上传强制 CC0(
04_web_app_plan.md §5.4 顶部「仅显示 CC0」开关始终为 ON 且不可关,等同于"全库即 CC0")
- v0.5(DAU > 5 000)后才开放 CC-BY 4.0 上传选项;CC-BY-SA / CC-BY-NC / 付费素材永不接(避免 Remix 树协议污染)
- 本资产库不引入 CrowdRoom 平台默认的 CC BY-NC 协议——资产是给所有 Remix 重用的"乐高积木",必须 NC-free
1.3 与 v0.2 既有设计的边界
| 边界 |
v0.2 既有 |
本文档扩展 |
public.assets 表行数 |
1 个 Storage 文件 + 9 列字段 |
仍是 1 张表,仅 ALTER 追加 13 列(§2) |
| 资产上传能力 |
无(仅运营 service_role 写) |
新增 8 个 E-A1~E-A8 端点(§6) |
| AssetPicker 检索维度 |
kind / semantic_class / tags |
追加 style_tag / volume_m3 / dominant_color / source_type / license |
| 治理流程 |
仅"内部抽查"(10_governance.md §4 P-W-4 步骤 3) |
复用 10_governance.md §2.1 既有 NSFW + 敏感词自动审核流,新增「资产审核队列」分支 |
2. 数据模型增强(AL-1 / AL-2 / AL-4 / AL-8 落地)
2.1 设计原则
- 不破坏 v0.2 —
01_data_schema.md §3.9 既有 9 列字段(id / kind / name / semantic_class / glb_path / pbr / thumbnail_path / license / source_url / tags / created_at)保持不动;本节仅做 ALTER TABLE ... ADD COLUMN IF NOT EXISTS 增量
- AL 契约逐字段落地 — 每一列都对应 AL-1 ~ AL-10 中的某一条(见 §2.6 映射表)
- 三张支撑表 —
asset_review_queue(审核工单)、asset_reports(举报)、asset_collections(用户收藏/官方风格集)拆为独立表,避免 assets 表膨胀
2.2 assets 表增量 ALTER
2.3 license 列 CHECK 约束(AL-7 落地)
2.4 索引(AL-4 / AL-10 落地:复合检索 < 200 ms)
2.5 RLS 补丁(覆盖既有 §3.9 policy)
2.6 AL-1 ~ AL-10 字段映射表(对账核心)
| 契约 ID |
AL 要求一句话 |
落地字段 / 索引 |
文档章节 |
| AL-1 |
家具资产必须有 anchor + forward/up + bbox |
anchor_local jsonb、bbox_local jsonb(§2.2) |
§2.2 / §4 / §5.2 |
| AL-2 |
公开资产 semantic_class 非空率 ≥ 99% |
semantic_class(v0.2 既有)+ assets_semantic_class_btree;§5.1.3 上传向导强制选 + §4 worker import 时 fallback unknown 标 reject |
§2.4 / §5.1 |
| AL-3 |
tags[] 必含风格 tag(7 类),每件至少 1 个 |
style_tag text(§2.2 enum 7 类)+ assets_style_btree;worker 自动从 CLIP 推断 + 上传向导默认必填 |
§2.2 / §4 / §5.1 |
| AL-4 |
物化 volume_m3 + dominant_color;GIN tags |
volume_m3 numeric + dominant_color text + assets_tags_gin + assets_volume_btree(§2.2 / §2.4) |
§2.2 / §6 E-A3 |
| AL-5 |
.glb ≤ 1 MB(家具)/ ≤ 200 KB(材质贴图集) |
§3 Storage 路径硬约束 + §5.2 worker 步骤 W-5 拒收超标 |
§3 / §5.2 |
| AL-6 |
必须 128×128 webp 缩略图 |
thumb_128.webp(§3 路径) + §5.2 worker 步骤 W-9 自动生成 |
§3 / §5.2 |
| AL-7 |
license 二选一 `CC0 |
CC-BY` |
license CHECK 白名单(§2.3)+ §9 协议管理 |
| AL-8 |
材质 pbr jsonb 必含 base_color_tex/normal_tex/roughness/metallic |
pbr jsonb(v0.2 既有)+ §5.2 worker 步骤 W-7 schema 校验 |
§5.2 W-7 |
| AL-9 |
asset_id ↔ glb_path 不可重定向 |
§3 路径硬规则 public/assets/{asset_id}/model.glb;asset_id 永不复用、glb_path 不允许 UPDATE(policy 限制) |
§3 |
| AL-10 |
GET /assets?bbox_filter=&style=&semantic= 复合检索 |
E-A3 端点(§6) + §2.4 复合索引 |
§6 / §7 |
结论:AL-1 ~ AL-10 每一条都对应到至少一个具体字段/索引/Worker 步骤/端点,10/10 闭环。
2.7 支撑表 DDL
2.7.1 asset_review_queue(审核工单)
2.7.2 asset_reports(侵权 / 违规举报)
联动:累计 ≥ 3 条独立 asset_reports 行未 resolved 时,触发 assets.review_status 自动改为 pending + 工单进 asset_review_queue 重审(priority=1),与 10_governance.md §2.2「3 独立举报自动隐藏」对齐。
2.7.3 asset_collections(用户收藏夹 / 官方风格集)
2.7.4 creator_profiles(创作者信任分扩展)
2.8 字段总览
| 项 |
数量 |
新增 assets 列 |
13 列 |
新增 users 列(创作者扩展) |
4 列 |
| 新增表 |
3 张(asset_review_queue、asset_reports、asset_collections) |
| 新增索引 |
6(assets)+ 5(支撑表)+ 1(users 创作者)= 12 个 |
| 新增 trigger |
2 个(bump_asset_use_count + assets_glb_path_lock,后者见 §3.3) |
| 新增 RLS policy |
4(assets)+ 7(支撑表)= 11 条 |
3. Storage 目录与命名约定(AL-5 / AL-6 / AL-9 落地)
3.1 公开 bucket 目录扩展
与 01_data_schema.md §4 的 rooms/ / private/rooms/ 双 bucket 模型并列:本目录复用 v0.2 既有的 public bucket(与 rooms/ 同 bucket,仅顶层 prefix 不同),无需新建 bucket。
3.2 命名硬规则(AL-9 落地)
| 规则 |
说明 |
| R-1 asset_id 永不复用 |
资产被 withdrawn 后 id 不释放;新上传始终拿 gen_random_uuid() |
| R-2 model.glb 路径不可重定向 |
glb_path = 'public/assets/{asset_id}/model.glb' 写死;trigger assets_glb_path_lock(§3.3)拒绝任何 UPDATE 改 glb_path(service_role 也不放行,避免误操作) |
| R-3 thumb_128.webp 命名固定 |
不允许 thumb1.webp / cover.webp 等变体——AssetPicker 拼路径用 |
| R-4 .tmp 隔离 |
Worker 写入时先到 public/assets/{asset_id}/.tmp/,转码完成 rename 到正式路径,避免半成品被读 |
| R-5 attribution.txt UTF-8 |
多个原作者用 \n 分隔;格式:"<creator_name>" <https://url> CC-BY-4.0 |
3.3 glb_path 不变性的 SQL 兜底
4. 平台资产导入管线(爬虫 / 外部库映射)
4.1 来源清单
| 来源 |
协议 |
类型 |
MVP 数量目标 |
P1 目标 |
备注 |
| Quaternius(github.com/quaternius) |
CC0 |
家具 / 装饰 |
300 件 |
2 000 件 |
Furniture Kit、Plant Pack 等 |
| Poly Haven(polyhaven.com) |
CC0 |
材质 PBR |
150 套 |
800 套 |
base/normal/roughness/metallic/ao 五贴图集 |
| ambientCG |
CC0 |
材质 PBR |
100 套 |
500 套 |
同上 |
| Sketchfab CC0 子集 |
CC0 |
家具 |
50 件 |
200 件 |
每件单独人工 verify 协议(Sketchfab 标错率高) |
| Free3D CC0 子集 |
CC0 |
家具 |
20 件 |
100 件 |
P2 再放量 |
4.2 导入 worker 流程图
4.3 导入脚本骨架(TypeScript / Node.js,从 Quaternius 批量)
// scripts/import_quaternius.ts
//
// 用法:pnpm tsx scripts/import_quaternius.ts --pack=furniture-kit --limit=50
//
// 前置依赖(package.json):
// "@gltf-transform/core", "@gltf-transform/extensions", "@gltf-transform/functions",
// "@supabase/supabase-js", "sharp", "open-clip-ts"
// 环境变量:SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY / QUATERNIUS_REPO_DIR
import { NodeIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
import { weld, dedup, meshopt, textureCompress } from '@gltf-transform/functions';
import { createClient } from '@supabase/supabase-js';
import sharp from 'sharp';
import { readdir } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';
const SEMANTIC_FROM_FILENAME: Record<string, string> = {
bed: 'bed', chair: 'chair', table: 'table', sofa: 'sofa', desk: 'table',
shelf: 'storage', dresser: 'storage', tv: 'television',
};
const STYLE_FROM_PACK: Record<string, string> = {
'furniture-kit': 'minimal', 'midcentury': 'retro', 'modern': 'nordic',
};
const supa = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
async function importOne(srcPath: string, pack: string) {
const asset_id = randomUUID().replace(/-/g, '');
const name = srcPath.split('/').pop()!.replace(/\.glb$/, '');
// 1. 加载 + 压缩
const doc = await io.read(srcPath);
await doc.transform(
weld(), dedup(),
meshopt({ level: 'medium' }),
textureCompress({ encoder: 'ktx2', targetFormat: 'auto', quality: 90 })
);
const glb = await io.writeBinary(doc);
if (glb.byteLength > 1_000_000) {
console.warn(`[skip ${name}] ${glb.byteLength}B > 1 MB`); return;
}
// 2. bbox / volume
const scene = doc.getRoot().listScenes()[0];
let min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
scene.traverse((node) => {
const mesh = node.getMesh(); if (!mesh) return;
mesh.listPrimitives().forEach(p => {
const pos = p.getAttribute('POSITION'); if (!pos) return;
for (let i = 0; i < pos.getCount(); i++) {
const v = [0, 0, 0]; pos.getElement(i, v);
for (let k = 0; k < 3; k++) {
if (v[k] < min[k]) min[k] = v[k];
if (v[k] > max[k]) max[k] = v[k];
}
}
});
});
const volume_m3 = (max[0]-min[0]) * (max[1]-min[1]) * (max[2]-min[2]);
// 3. anchor = bbox 底面中心;forward/up 默认 +Z/+Y(AL-1)
const anchor_local = {
position: [(min[0]+max[0])/2, min[1], (min[2]+max[2])/2],
forward_axis: '+Z', up_axis: '+Y',
};
// 4. dominant_color(用首个材质的 baseColorFactor 近似;正式版用 K-Means)
const mat = doc.getRoot().listMaterials()[0];
const bc = mat?.getBaseColorFactor() ?? [0.6, 0.6, 0.6, 1];
const dominant_color = '#' + bc.slice(0, 3)
.map((c: number) => Math.round(c * 255).toString(16).padStart(2, '0')).join('');
// 5. CLIP tags(伪代码;正式实现挂 open-clip-ts 对预设词典 zero-shot 分类)
const tags = await clipExtractTags(srcPath);
const matchedKey = Object.keys(SEMANTIC_FROM_FILENAME)
.find(k => name.toLowerCase().includes(k));
const semantic_class = matchedKey ? SEMANTIC_FROM_FILENAME[matchedKey] : 'storage';
const style_tag = STYLE_FROM_PACK[pack] ?? 'minimal';
// 6. 缩略图(假设外部已渲好同名 .png;正式版走 headless three.js)
const thumb128 = await sharp(srcPath.replace('.glb', '.png'))
.resize(128, 128).webp({ quality: 85 }).toBuffer();
const preview512 = await sharp(srcPath.replace('.glb', '.png'))
.resize(512, 512).webp({ quality: 90 }).toBuffer();
// 7. attribution.txt(CC0 也保留来源 URL)
const attribution =
`Quaternius "${name}" — CC0 Public Domain\n` +
`https://github.com/quaternius/${pack}\n`;
// 8. 上传 Storage
const root_path = `assets/${asset_id}`;
await Promise.all([
supa.storage.from('public').upload(`${root_path}/model.glb`, glb,
{ contentType: 'model/gltf-binary' }),
supa.storage.from('public').upload(`${root_path}/thumb_128.webp`, thumb128,
{ contentType: 'image/webp' }),
supa.storage.from('public').upload(`${root_path}/preview_512.webp`, preview512,
{ contentType: 'image/webp' }),
supa.storage.from('public').upload(`${root_path}/attribution.txt`,
Buffer.from(attribution, 'utf-8'), { contentType: 'text/plain' }),
]);
// 9. INSERT assets 行(service_role 绕过 RLS)
const { error } = await supa.from('assets').insert({
id: asset_id, kind: 'furniture', name, semantic_class,
glb_path: `public/${root_path}/model.glb`,
thumbnail_path: `public/${root_path}/thumb_128.webp`,
license: 'CC0',
source_url: `https://github.com/quaternius/${pack}`,
tags, style_tag,
anchor_local, bbox_local: { min, max },
volume_m3, dominant_color,
source_type: 'platform', review_status: 'approved', trust_score: 80,
});
if (error) throw error;
console.log(`[ok] ${name} → ${asset_id} (${(glb.byteLength/1024).toFixed(1)} KB)`);
}
async function clipExtractTags(p: string): Promise<string[]> {
// 伪代码:真实实现挂 open-clip-ts,对 ['wood','metal','fabric','glass',...] 做多标签分类
return ['wood', 'natural', 'mid_century'];
}
(async () => {
const args = Object.fromEntries(
process.argv.slice(2).map(s => s.replace(/^--/, '').split('='))
);
const dir = join(process.env.QUATERNIUS_REPO_DIR!, args.pack);
const files = (await readdir(dir)).filter(f => f.endsWith('.glb'));
for (const f of files.slice(0, parseInt(args.limit ?? '50', 10))) {
try { await importOne(join(dir, f), args.pack); }
catch (e) { console.error(`[fail] ${f}: ${(e as Error).message}`); }
}
})();
审计要点:导入脚本以 SUPABASE_SERVICE_ROLE_KEY 运行,绕过 RLS;仅运营人员可在受控环境执行;每次执行写一行到运营内部审计表(建议增量;不展开 DDL)。
4.4 CC-BY 资产的差异化处理(v0.5 起开放)
| 步骤 |
CC0(MVP) |
CC-BY(v0.5+) |
attribution.txt |
可选 |
必须,格式 "<creator>" <https://url> CC-BY-4.0 |
| AssetPicker 卡片 |
不显示作者 |
卡片底部小字「by {creator}」 |
| Remix 引用时 |
无追加 |
overlay 自动追加 attribution: { asset_id, creator, license } |
| Remix 详情页 |
不显示 |
页脚「Assets in this remix」区块列出全部 CC-BY 资产 + 作者署名 |
5. 用户上传 UGC 资产的完整流程(最核心章节)
5.1 上传入口与 UX
| ID |
路由 |
描述 |
| R-18(建议增量) |
/me/assets |
用户主页「我的资产」Tab(列表 + 状态徽章) |
| R-18a(建议增量) |
/me/assets/new |
上传向导(5 步) |
| R-18b(建议增量) |
/me/assets/[asset_id]/edit |
编辑元数据(仅 review_status='pending' 时可改) |
| R-19(建议增量) |
/assets |
公共资产浏览页(AssetPicker 的独立网页版) |
| R-19a(建议增量) |
/assets/[asset_id] |
资产详情页(含使用此资产的 Remix 列表) |
| R-17b(建议增量) |
/admin/assets |
审核员资产工作台(扩展自既有 R-17 /admin/reports) |
5.1.2 入口可见性
| 位置 |
CTA 文案 |
触达条件 |
发现页 / 底部 |
「成为创作者 — 上传你的第一件资产 →」 |
登录用户且 creator_uploaded_count=0 |
用户主页 /u/[handle] |
顶部 Tab「我的资产」 |
仅自己可见上传按钮;他人看到的是已 approved 资产列表 |
| Remix 编辑器 AssetPicker 底部 |
「找不到合适的?上传自己的 →」按钮 |
登录用户始终可见 |
| Header 用户菜单 |
「我的资产」菜单项 |
登录用户始终可见 |
5.1.3 上传向导 5 步(/me/assets/new)
| 步骤 |
UI 元素 |
校验 |
| 1 选文件 |
Dropzone + 文件选择器;接受 .glb / .usdz / .gltf+bin+textures.zip |
单文件 ≤ 10 MB(worker 端再压到 ≤ 1 MB) |
| 2 自动预览 |
R3F <Canvas> 内嵌即时渲染 + OrbitControls;下方 forward_axis / up_axis 6 选 1 单选器 |
必须确认朝向,否则下一步按钮禁用 |
| 3 元数据 |
name(≤ 80 字符)/ kind 单选(家具/材质)/ semantic_class 16 选 1(家具)或 6 选 1(材质)/ style_tag 7 选 1 / tags ≤ 8 个 |
semantic_class、style_tag 必填(AL-2 / AL-3) |
| 4 协议 |
单选「我声明此资产为原创或合法授权」+「我同意按 CC0 发布(MVP 强制)」 |
两个 checkbox 必勾 |
| 5 提交 |
点击「提交审核」→ 调 E-A1 拿 presigned URL → PUT Storage → 调 E-A2 标 complete → 入审核队列 |
— |
5.2 自动化预处理 Worker
触发自 E-A2 POST /functions/v1/asset-upload-complete;复用 02_api_contract.md §3 既有 Transcode Worker 的基础设施(同 Cloud Run / Docker / 重试策略),但走独立的 asset- 分支函数集。
5.2.1 完整 18 步序列图
5.2.2 步骤明细表
| ID |
步骤 |
工具 |
失败码 |
是否阻塞 |
| W-1 |
格式校验 |
mime + 文件头 magic bytes 检查 |
ASSET_FORMAT_UNSUPPORTED |
阻塞 |
| W-2 |
安全扫描 |
自定义 glTF 解析:检查 extras.script / 异常 binary chunk 长度 |
ASSET_MALICIOUS |
阻塞 |
| W-3 |
usdz→glb |
usdzconvert |
USDZ_DECODE_FAILED |
阻塞 |
| W-4 |
压缩 |
gltf-transform meshopt + textureCompress(ktx2) |
MESH_COMPRESSION_FAILED |
阻塞 |
| W-5 |
三角面数限制 |
自研:scene.traverse 累加 indices.count/3 |
ASSET_OVER_BUDGET(家具 > 50k tri / 材质 quad > 5k tri) |
阻塞 |
| W-6 |
bbox / volume / anchor |
同 §4.3 import 脚本逻辑 |
— |
— |
| W-7 |
PBR 校验(材质) |
检查 pbr.base_color_tex / normal_tex / roughness / metallic 全在 |
ASSET_PBR_INCOMPLETE(AL-8) |
阻塞 |
| W-8 |
dominant_color + tags |
K-Means + CLIP zero-shot |
CLIP_INFERENCE_FAILED |
可降级(用空 tags 进人工审) |
| W-9 |
缩略图 |
headless three.js → sharp resize webp |
THUMBNAIL_FAILED |
可降级(用占位图) |
| W-10 |
Auto Moderator |
NSFWJS + 人脸 detection(贴图扫描) |
ASSET_NSFW_BLOCKED / ASSET_PII_DETECTED |
直接 rejected |
5.3 资产审核(人工 + 自动)
5.3.1 自动初筛(Mod 端)
| 信号 |
阈值 |
动作 |
NSFW(NSFWJS 对 preview_512.webp) |
score ≥ 0.9 |
直接 review_status='rejected' + rejection_reason='auto_nsfw' |
| NSFW |
0.5 ≤ score < 0.9 |
入队列 priority=2(高于普通新上传) |
| 三角面数(家具) |
> 50 000 tri |
直接 rejected + rejection_reason='over_triangle_budget' |
| 三角面数(材质 quad) |
> 5 000 tri |
同上 |
| 贴图含人脸(face detection) |
任一面孔 |
rejected + rejection_reason='pii_face_detected' |
| 与既有资产 SHA256 重复 |
命中 |
rejected + rejection_reason='duplicate_of=<asset_id>' |
| 重复上传同一文件 |
同 uploader 24h 内 ≥ 3 次失败 |
触发用户级速率限制(与 §8 信任分联动) |
5.3.2 人工审核工作台(/admin/assets,建议增量 to 04_web_app_plan.md R-17)
布局(沿用既有 /admin/reports 风格):
| 区域 |
内容 |
| 左侧列表 |
asset_review_queue 按 priority ASC, enqueued_at ASC 排序;徽章显示 sla 剩余时间 |
| 右上预览 |
R3F <Canvas> 360° 旋转预览;显示 triangle_count / volume_m3 / bbox 与压缩后大小 |
| 右下元数据 |
uploader、semantic_class、style_tag、tags、license、auto_signals JSON 摘要 |
| 底部动作栏 |
3 按钮:Approve / Approve with edits / Reject(弹窗填 reason) + Escalate(升给 owner-team) |
5.3.3 三种审核动作
| 动作 |
后果 |
trust_score 起步 |
| Approve |
review_status='approved',立即可在 AssetPicker 中被搜到 |
30(默认新上传) |
| Approve with edits |
审核员先补全 anchor_local / tags / style_tag 等字段,再走 Approve |
30 |
| Reject |
必填 rejection_reason;asset 软删(deleted_at),文件物理保留 7 天供作者下载备份 |
— |
5.3.4 SLA 与超时回退
- SLA 72 h(与
10_governance.md §3.1 MVP 1 兼职 reviewer + 30-50 工单/日上限对齐)
- 超时:72 h 未审,资产自动以
trust_score=20 上架(风险,需明确告知 reviewer 团队)+ 工单升级到 owner-team
- 与
10_governance.md §2.1 既有 NSFW/敏感词流水线复用阈值:NSFW ≥ 0.9 硬下架,0.5-0.9 入队列
5.4 资产 Remix(用户基于已有资产二次创作)
5.4.1 触发入口
- 资产详情页
/assets/[asset_id] 右上「Fork 这个资产」按钮(MVP 仅 kind='material' 开放;家具 Remix 延后到 v0.5)
- Fork 后进入轻量编辑器:调 base_color / roughness / metallic 滑块;不允许改 mesh
5.4.2 数据流
5.4.3 协议传染规则(与 §9 一致)
| 父 license |
子可选 license |
强制行为 |
| CC0 |
CC0 或 CC-BY(v0.5+) |
子选 CC-BY 时仅署"作为 remix 作者的自己" |
| CC-BY-4.0 |
必须 CC-BY-4.0 |
子的 attribution.txt 必须叠加父的原作者署名(追加,不替换) |
6. API 端点增量(AL-10 落地)
建议增量 to 02_api_contract.md §2.1 端点全表,新增 8 条 E-A1~E-A8,沿用既有命名风格与速率限流维度。
6.1 端点全表
| ID |
METHOD |
路径 |
类型 |
入参 |
出参 |
鉴权 |
速率 |
业务错误码 |
| E-A1 |
POST |
/functions/v1/asset-upload-init |
Edge |
{ filename, size, mime, kind } |
{ asset_id, presigned_put_url, expires_at } |
user |
10/h/user |
FILE_TOO_LARGE, ASSET_FORMAT_UNSUPPORTED, QUOTA_EXCEEDED, CREATOR_FROZEN |
| E-A2 |
POST |
/functions/v1/asset-upload-complete |
Edge |
{ asset_id, name, semantic_class, style_tag, tags[], license, kind, anchor_local? } |
{ asset_id, review_status: 'pending', queue_position } |
user(owner) |
10/h/user |
ASSET_NOT_FOUND, ASSET_SOURCE_MISSING, ASSET_METADATA_INVALID |
| E-A3 |
GET |
/rest/v1/assets?...(详见 §6.2) |
PostgREST |
query string |
Asset[] |
anon |
60/min/IP |
RLS_DENIED, SEARCH_QUERY_TOO_SHORT |
| E-A4 |
POST |
/functions/v1/asset-review-action |
Edge |
{ asset_id, action, reason?, edits? } |
{ ok, new_review_status } |
reviewer |
100/h/reviewer |
ASSET_NOT_FOUND, ASSET_ALREADY_REVIEWED, REVIEWER_FORBIDDEN |
| E-A5 |
POST |
/functions/v1/asset-fork |
Edge |
{ parent_asset_id, name, pbr_override?, license } |
{ new_asset_id, review_status: 'pending' } |
user |
5/h/user |
ASSET_NOT_FOUND, ASSET_FORK_LICENSE_CONFLICT, ASSET_FORK_NON_REMIXABLE |
| E-A6 |
POST |
/functions/v1/asset-report |
Edge |
{ asset_id, reason, detail? } |
{ report_id } |
user |
5/h/user |
ASSET_NOT_FOUND, REPORT_DUPLICATE |
| E-A7 |
POST |
/rest/v1/asset_collections |
PostgREST |
{ name, asset_ids[], is_public, description? } |
AssetCollection |
user |
20/h/user |
RLS_DENIED, COLLECTION_NAME_TAKEN |
| E-A8 |
DELETE |
/rest/v1/assets?id=eq.{asset_id} |
PostgREST |
path |
204 |
owner(pending) |
5/h/user |
RLS_DENIED, ASSET_ALREADY_APPROVED |
注:E-A8 仅 review_status='pending' 时允许 owner 走 PostgREST DELETE(由 §2.5 policy assets_delete_owner_pending 强制);approved 后须改走 E-A4 reviewer 路径(reviewer 可执行 withdrawn 动作,软删并清 Storage 文件)。
6.2 E-A3 资产检索查询参数语义(核心,AL-10 落地)
6.2.1 bbox_filter=lo,hi 语义
- 用户在 AssetPicker 选中一个家具,其原 OBB 体量
vol_target m³;前端拼 lo = vol_target * 0.5, hi = vol_target * 1.5
- 实现:在 RPC
search_assets 中翻译为 WHERE volume_m3 BETWEEN $lo AND $hi
- 容差:默认 ±50%(含 0.5×~1.5×;过严会让候选集小于 5 件,过松失去过滤意义);与
05_object_replacement_handbook.md §4.2 推荐的 ±20% 兼容(参数化由前端控制)
- 索引:
assets_volume_btree(§2.4)
6.2.2 color=hue:200,tol:20 语义
- 在 HSL 色空间筛主色相 ± tol(角度 0-360)
- 实现:服务端把
dominant_color 的 hex → HSL,做 WHERE ABS(hue_diff(...)) < $tol
- 简化版(MVP):先用六色桶(warm/cold/neutral/red/blue/green)做粗筛,避免每次 SQL 调用 HSL 转换函数
- 进阶(P2):LAB 距离
< 阈值,更感知一致
6.2.3 复合查询 SQL 实现示例
- 性能目标:100k 行规模下,
semantic_class + bbox_filter + style_tag 三维过滤 + 排序 ≤ 200 ms(依赖 §2.4 复合索引)
- CDN 缓存:60 s(按 query string 哈希);登录态附
Cache-Control: private
6.3 错误码增量
建议增量 to 02_api_contract.md §7:
| 错误码 |
HTTP |
含义 |
出现端点 |
ASSET_FORMAT_UNSUPPORTED |
415 |
不接受 .obj / .fbx |
E-A1, W-1 |
ASSET_OVER_BUDGET |
413 |
三角面数或文件大小超限 |
W-5 |
ASSET_PBR_INCOMPLETE |
400 |
材质 PBR 字段缺失(AL-8) |
W-7 |
ASSET_NSFW_BLOCKED |
403 |
NSFW score ≥ 0.9 自动下架 |
Mod |
ASSET_PII_DETECTED |
403 |
贴图含人脸/证件 |
Mod |
ASSET_MALICIOUS |
400 |
文件结构异常或含 embedded 脚本 |
W-2 |
ASSET_ALREADY_REVIEWED |
409 |
同一 asset 不允许重复审 |
E-A4 |
ASSET_FORK_LICENSE_CONFLICT |
400 |
父 CC-BY 子选 CC0 不允许 |
E-A5 |
ASSET_FORK_NON_REMIXABLE |
403 |
父资产 kind='furniture' 在 MVP 不允许 fork |
E-A5 |
CREATOR_FROZEN |
403 |
creator_trust_score < 0 上传被冻结 |
E-A1 |
REVIEWER_FORBIDDEN |
403 |
无 reviewer role |
E-A4 |
COLLECTION_NAME_TAKEN |
409 |
同 owner 收藏夹重名 |
E-A7 |
ASSET_ALREADY_APPROVED |
409 |
approved 资产不可走 owner DELETE |
E-A8 |
7. AssetPicker 排序与发现策略
7.1 默认排序综合公式
AssetPicker 默认查询 order=trust_score.desc, use_count.desc,但实际后端按下列加权重排(在 RPC search_assets 顶层包一层):
7.2 冷启动策略
| 阶段 |
规则 |
| 上传后 0-7 天 |
trust_score 固定 30,禁止变化;优先暴露给 Remix 编辑器底部「新上传推荐」窗口(占 10% 流量配额) |
| 第 8 天 |
根据 download_count / use_count / report_count 进入 §8 信任分动态算法 |
| 资产首次被 use 时 |
立即将 trust_score += 2(实时反馈),但单 7 天窗口内增量上限 ≤ 10 |
7.3 三类资产权重差异
source_type |
默认 trust_score |
排序额外因子 |
platform |
80 |
× 1.0 |
user_upload |
30 → 动态 |
× 0.85(同分数时让位 platform) |
remix_of |
round(parent × 0.8) |
× 0.7(避免刷量) |
7.4 防作弊
- 同 IP 同 asset 24h 内 use_count 增量上限 = 3(trigger 端去重)
- 同 uploader 自我点赞收藏不计 use_count
trust_score 24h 内变化幅度 ≤ ±15(防恶意刷分/被刷)
8. 创作者声誉与信任分(AL 衍生需求)
8.1 双重信任分模型
| 维度 |
字段 |
范围 |
含义 |
| 资产级 |
assets.trust_score |
0-100 |
单件资产的可信度,影响 §7 排序 |
| 创作者级 |
users.creator_trust_score |
-100 ~ 100 |
创作者整体声誉,影响上传权限与跳审 |
8.2 creator_trust_score 计分规则
| 事件 |
变化 |
触发位置 |
| 初始注册 |
= 50 |
users 默认值 |
| 资产 approve |
+5 |
E-A4 action=approve |
| 资产 reject |
-10 |
E-A4 action=reject |
| 资产被举报且 reviewer 判定违规 |
-20 |
asset_reports.resolution='confirmed_violation' |
| 资产被 use_count 累计 ≥ 100 |
+10 |
trigger(每件资产仅奖励一次) |
资产被收藏(加入 asset_collections)累计 ≥ 100 次 |
+5 |
trigger |
| 主动撤回 pending 资产 |
0 |
E-A8 |
| 申诉胜诉(rejected → approved) |
+15 |
reviewer 修正 |
8.3 阈值规则
| 区间 |
状态 |
上传体验 |
< 0 |
冻结 |
E-A1 直接 CREATOR_FROZEN;不再接受上传 |
0 ~ 29 |
低信任 |
上传必须走人工预审(自动初筛不能放行;优先级降至 priority=7) |
30 ~ 79 |
标准 |
默认流程 |
80 ~ 100 |
Trusted Creator |
上传可跳过人工审核(仅自动审,approved 后直接上架);creator_badge='trusted' |
≥ 95 且 creator_uploaded_count ≥ 50 |
Pro Creator |
同上 + 优先展示在「创作者推荐」位 |
8.4 与 v0.2 既有信任分系统的对齐
本节是 10_governance.md §3.2 P1 信任分系统的具体落地:
| v0.2 §3.2 P1 规则 |
本节对应 |
| 初始信任分 50 |
users.creator_trust_score DEFAULT 50 |
| 每次成功举报 +5 |
§8.2「资产被举报判违规 -20」是被举报方;举报方 +5 走 10_governance.md 现有 user-level 信任分(独立维度),不在本表 |
| 满 100 分可申请志愿者审核团 |
creator_trust_score = 100 AND creator_uploaded_count >= 50 → 弹申请入口;具体审核团 SOP 走 v0.5 |
9. 版权与协议管理(AL-7 落地)
9.1 MVP 与 v0.5 的两阶段策略
| 阶段 |
用户可选 license |
UX |
| MVP(v0.2~v0.4) |
仅 CC0 |
上传向导 §5.1.3 Step 4 默认且禁用切换;显示「MVP 期所有资产强制 CC0 — 详见 社区准则 §4」 |
| v0.5+(DAU > 5 000) |
CC0 或 CC-BY-4.0 |
上传向导 Step 4 加单选;选 CC-BY 必填「原作者名 + 原始 URL」(即便是自己原创,URL 可填自己主页) |
- ✅ 接受:CC0、CC-BY-4.0(v0.5+)
- ❌ 永不接受:
- CC-BY-SA(病毒条款,污染 Remix 树)
- CC-BY-NC(与 CrowdRoom 平台 BY-NC 双重叠加,堵死 Remix 商业化)
- 任何商业素材(涉及税务、对账、退款,超 MVP 预算)
9.3 attribution 自动透传链路
9.4 侵权举报与下架流程
9.5 「占位 box」fallback 规则
- Web 端加载 overlay 时,对每个
op.asset_id 调 E-A3 校验存在;
- 若 asset 已
withdrawn 或 deleted,渲染端用一个灰色 OBB 占位 mesh 替代(尺寸取自原 target_item.obb),上方浮一行 "Asset withdrawn"
- 与
05_object_replacement_handbook.md §9 applyOverlay 的「asset 加载失败兜底」分支兼容
10. 资产收藏 / 集合 / 个人主页(社区飞轮)
10.1 用户收藏夹(asset_collections,is_official=false)
- 用户可建多个收藏夹(如 "我的客厅灵感"、"侘寂风备选")
- 每个收藏夹
asset_ids[] 上限 200 个;超出走「分页拆分」CTA
- 收藏夹可
is_public=true 公开,被发现页「灵感板」抓取展示
- 在 AssetPicker 顶栏可一键「+ 加入收藏夹」(弹出收藏夹选择 + 新建)
10.2 官方风格集(is_official=true)
- 仅 reviewer / owner-team 可创建(service_role 写)
- 典型示例:
- "侘寂风家具 30 件套"(30 件
style_tag='wabi_sabi' 的精选)
- "Quaternius Spring 2026 新品"
- "亚洲设计师精选"(v0.5+ 创作者 spotlight)
- 发现页
/ 顶部「编辑精选」位轮播展示
10.3 创作者个人主页扩展
扩展 04_web_app_plan.md §1.1 R-4 /u/[handle] 既有页:
| 新增模块 |
内容 |
| 「我的资产」Tab |
网格列出该用户 approved 资产;右上 stats:上传数 / 总使用数 / 总下载数 |
| Trusted Creator 徽章 |
头像右下角小盾牌(creator_badge='trusted') |
| Pro Creator 徽章 |
头像右下角金色齿轮(creator_badge='pro') |
| 「联系/赞赏」按钮 |
v0.5+;MVP 隐藏 |
| 收藏夹列表(仅自己可见私有) |
公开收藏夹列出 |
11. 移动端可访问性(iOS App 是否能上传资产?)
11.1 决策:MVP iOS App 不支持上传资产到资产库
| 维度 |
理由 |
| 用户画像 |
iOS 端用户的核心动作是「扫描房间」(RoomPlan),不是「建模家具」 |
| 工具链 |
自制 .glb 的工具链都在 PC(Blender / 3ds Max / Rhino),手机上无创作场景 |
| 审核成本 |
手机端上传更容易出现"随手拍照伪装 3D"的低质内容,自动初筛拦不住 |
| 开发成本 |
iOS 端走 §5 完整上传向导需重写 SwiftUI;MVP 期 ROI 低 |
11.2 v0.5 起的轻量入口(候选)
- iPhone 拍照 → Object Capture(iOS 17+)→ 自动生成 .glb → 走简化版 §5 流程
- 仅支持「桌面摆件」级小物体(< 30 cm 边长)
- 自动
style_tag='unknown',强制进入 priority=2 人工审
11.3 iOS App 仍可以做的资产相关动作
| 动作 |
是否支持 |
| 浏览资产库(只读) |
✅(MVP 即支持,复用 Web 的 E-A3) |
收藏资产到 asset_collections |
✅ |
| 举报资产(E-A6) |
✅ |
| 上传资产 |
❌(MVP)→ ✅(v0.5 Object Capture 路径) |
| Fork / Remix 资产 |
❌(MVP)→ ✅(v0.5) |
12. 性能与存储成本
12.1 存储成本估算
| 资产数 |
平均单件 |
总存储 |
Cloudflare R2 月成本 |
Supabase Storage 月成本 |
| 1 万 |
600 KB(model+thumb+preview) |
~6 GB |
~$0.10 |
免费档内 |
| 10 万 |
600 KB |
~60 GB |
~$1.50 |
$1.25(超 1 GB 后 $0.021/GB) |
| 100 万 |
600 KB |
~600 GB |
~$15 |
$12.6 → 此规模迁 R2 |
与 04_web_app_plan.md §10 / 10_governance.md §4 P-W-6「Cloudflare 免费档 + R2 出口费」一致;MVP 期资产库不会成为成本瓶颈。
12.2 AssetPicker 加载性能
| 场景 |
目标 |
实现 |
| 第一屏 20 个卡片 |
TTI < 1 s |
20 × 128×128 webp ≈ 200 KB(CDN gzip 后),4G 网络 < 800 ms |
| 滚动加载下一屏 |
< 300 ms |
E-A3 limit=20 + offset 分页,CDN 60s 缓存命中率 > 80% |
| 详情页 360° 预览 |
TTI < 2 s |
预加载 model.glb(≤ 1 MB,CDN)+ 异步加载 preview_3d.glb |
12.3 检索性能
| 规模 |
三维过滤(semantic+style+bbox) |
索引 |
| 10k 行 |
~30 ms |
btree |
| 100k 行 |
~120 ms |
btree + GIN |
| 1M 行 |
~400 ms(警戒) |
需要切 ElasticSearch / Meilisearch(P2,纳入 ROADMAP) |
触发条件:总 approved 资产数 > 100k → 启动 outbox 同步到 Meilisearch;保留 Postgres 为权威源(与 01_data_schema.md §1 D5 全文搜索演进策略一致)。
12.4 Worker 处理能力
| 资源 |
单次处理时间 |
MVP 并发 |
| 用户上传 .glb(5 MB → 1 MB) |
~30 s(含 KTX2 + CLIP + 缩略图) |
5 并发,60 件/小时 |
| 平台 import 批量 |
~10 s/件 |
串行,360 件/小时(夜间跑) |
MVP 容量:60 件/小时 × 24 h = 1 440 件/日上限,远高于预期 50 件/日新增。
13. 与 v0.2 设计的关系矩阵
一张表,每行:本文档章节 / 关联的 v0.2 文档章节 / 关系类型。Reviewer 可一眼看清哪些是新增、哪些是已有。
14. M1 验收清单
14.1 12 条 checkbox 验收(M1 = MVP 8 周窗口内必达)
14.2 AL-1 ~ AL-10 契约最终对照表(双重显式,与 §2.6 互为冗余)
| 契约 |
验收条件 |
测试方法 |
M1 状态 |
| AL-1 |
所有 kind='furniture' 资产 anchor_local + bbox_local 非空率 100% |
SELECT count(*) FROM assets WHERE kind='furniture' AND (anchor_local='{}' OR bbox_local='{}') = 0 |
[ ] |
| AL-2 |
semantic_class 非空率 ≥ 99%(01_data_schema.md §3.9 既有字段) |
同上 SQL 校验 |
[ ] |
| AL-3 |
style_tag 非空率 ≥ 95%;7 类 enum 覆盖 |
SELECT style_tag, count(*) FROM assets GROUP BY 1 |
[ ] |
| AL-4 |
volume_m3 > 0 非空率 100%;dominant_color 非空率 ≥ 95%;GIN tags 索引存在 |
\d+ public.assets 看索引 + 数据校验 |
[ ] |
| AL-5 |
100% 平台资产 model.glb ≤ 1 MB(家具)/ ≤ 200 KB(材质) |
Storage 对象大小巡检脚本 |
[ ] |
| AL-6 |
100% 资产有 thumb_128.webp(128×128) |
Storage 路径存在性巡检 |
[ ] |
| AL-7 |
license CHECK IN ('CC0','CC-BY-4.0') 强制;100% 资产协议 = CC0(MVP 阶段) |
SELECT DISTINCT license FROM assets |
[ ] |
| AL-8 |
kind='material' 资产 100% 含 pbr.base_color_tex / normal_tex / roughness / metallic |
JSONB key existence SQL |
[ ] |
| AL-9 |
assets_glb_path_lock trigger 启用;任何 UPDATE 改 glb_path 都抛错 |
故意 UPDATE 测试 |
[ ] |
| AL-10 |
E-A3 接受 bbox_filter / style / semantic / color 复合参数;响应 ≤ 200 ms |
k6 压测 100k 行规模 |
[ ] |
结论:14.2 对照表 + §2.6 字段映射表共同实现 AL-1 ~ AL-10 的双重显式对账。
15. 风险与开放问题
| # |
风险 |
影响 |
缓解策略 |
| R-1 |
UGC 资产质量参差 → AssetPicker 体验下降 |
用户找不到好资产,Remix 转化率下降 |
§7 排序公式让 platform 资产默认排前;冷启动 7 天压制 user_upload;§8 信任分淘汰低质创作者 |
| R-2 |
版权审核难度大(用户「原创」声明无法机器验证) |
法律风险,版权方追责 |
MVP 强制 CC0(用户放弃所有权);侵权举报 24 h 内下架 + Storage 删除 + 占位 box fallback;保留申诉通道 |
| R-3 |
资产规模 > 100k 后检索性能退化 |
E-A3 响应 > 200 ms,AssetPicker 卡顿 |
§12.3 触发条件:> 100k 行 → outbox 同步到 Meilisearch;Postgres 保留为权威源(P2,纳入 ROADMAP) |
| R-4 |
平台资产爬取的法律边界(Sketchfab CC0 标错率高) |
误把非 CC0 当 CC0,被原作者追责 |
§4.1 Sketchfab 子集每件单独人工 verify;保留 source_url + attribution.txt 即便 CC0 |
| R-5 |
Remix 衍生的协议传染(CC-BY 链可能很长) |
长链署名累积,UI 难以全部展示 |
§9.3 attribution 透传链路 trigger 自动汇总;详情页页脚折叠展示「View all attributions」 |
| R-6 |
iOS 不能上传资产是否伤害创作者生态 |
移动端创作者流失 |
§11.2 v0.5 引入 Object Capture 轻量入口;MVP 阶段通过「Web 上传 + 邮件分享给 iOS 用户」过渡 |
章节版本:v0.2 · 草案(子任务 10 产出,2026-05-19)
关键收获:
- v0.2 既有
assets 表 13 列字段增量 + 3 张支撑表 = 完整的「平台 + UGC + Remix」三源流资产库
- 8 个新端点 E-A1~E-A8 + 1 个 RPC
search_assets = AssetPicker 复合检索 + UGC 全生命周期
- 10 条 AL 契约 100% 落地(§2.6 字段映射 + §14.2 验收对照 双重显式)
- 与
10_governance.md §4 P-W-4 决策严格一致(MVP 全 CC0,v0.5 起 CC-BY)
对外契约:
| 契约 |
给谁 |
一句话 |
assets 表 13 列 ALTER + 3 张支撑表 |
DBA / 后端 |
在 v0.2 §3.9 既有 schema 上做幂等 ALTER;不破坏既有迁移 |
| 8 个新端点 E-A1~E-A8 |
iOS / Web / 运营 |
全部走 Edge Function 或 PostgREST,沿用 v0.2 鉴权/限流维度 |
| AL-1 ~ AL-10 全部 10 条契约达成 |
05_object_replacement_handbook.md 物品替换闭环 |
资产库提供的元数据足以让 AssetPicker 在 200 ms 内做 OBB 对齐替换 |
| MVP CC0 强制 / v0.5 CC-BY 二选一 |
法务 / 运营 |
与 10_governance.md §4 P-W-4 决策一致 |
| iOS App 仅消费不生产 |
iOS 团队 |
MVP 不接 UGC 上传;v0.5 起评估 Object Capture 轻量入口 |