✨ feat: WP-09 Live2D/3D——模型导入校验与非破坏命令历史

- 迁移 18:avatar_model(live2d/vrm/gltf、validated/rejected、校验报告、来源许可、当前命令指针)+ avatar_command(十类命令检查约束、(avatar,sequence) 唯一、undone 标记)
- GLB/glTF 解析器(纯 Python):GLB magic/版本/chunk 结构校验、JSON 场景图引用一致性(scene→node→mesh→material 逐层索引越界检测)、VRM 0.x/1.0 扩展检测与 meta 提取
- 资源上限(恶意模型文件防线):文件 200MB、节点 10 万、网格 1 万、纹理 2 千、材质 5 千、动画 1 千——超限入库 rejected 并保留报告
- Cubism model3.json 校验:Version/FileReferences 必需、moc3 引用、纹理列表、表情/动作/HitArea 计数
- 上传 MIME 白名单扩展 model/ 与 application/json
- 命令历史:十类命令(param_map/expression/physics/motion/material/texture/bone/attachment/lighting/camera)追加(sequence 单调)、撤销(标记 undone 非破坏)、rejected 模型不接受编辑(422 前置校验)
- 测试 111 项全绿(+9):合法 GLB/VRM 检测/损坏文件/引用越界/超限/Cubism 正反例/命令历史与撤销幂等/rejected 拒编
This commit is contained in:
chenxin committed 2026-09-14 10:34:52 +08:00
1 parent 9dafb8fe72
commit 7c57d27494
7 files changed
+839

No files matched your search

+69
View File
@@ -1333,3 +1333,72 @@ class PluginExecution(Base):
name="status_values",
),
)
class AvatarModel(Base):
"""Avatar 模型资产(WP-09):live2d/vrm/gltf;校验结果与来源许可。"""
__tablename__ = "avatar_model"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("avatar_")
)
domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
character_id: Mapped[str | None] = mapped_column(
ForeignKey("character.id", ondelete="set null"), nullable=True
)
name: Mapped[str] = mapped_column(String, nullable=False)
format: Mapped[str] = mapped_column(String, nullable=False)
file_id: Mapped[str] = mapped_column(
ForeignKey("file_asset.id", ondelete="restrict"), nullable=False
)
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
validation_report: Mapped[dict[str, Any]] = mapped_column(
JSONB, nullable=False, default=dict
)
source_info: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
license_terms: Mapped[str | None] = mapped_column(String, nullable=True)
current_command_id: Mapped[str | None] = mapped_column(
ForeignKey("avatar_command.id", ondelete="set null", use_alter=True), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("format IN ('live2d', 'vrm', 'gltf')", name="format_values"),
CheckConstraint(
"status IN ('pending', 'validated', 'rejected')", name="status_values"
),
)
class AvatarCommand(Base):
"""非破坏编辑命令历史(WP-09/ADR-006):命令与版本可扩展到 2.0 LLM 改模。"""
__tablename__ = "avatar_command"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("command_")
)
avatar_id: Mapped[str] = mapped_column(
ForeignKey("avatar_model.id", ondelete="cascade"), nullable=False, index=True
)
sequence: Mapped[int] = mapped_column(nullable=False)
command_type: Mapped[str] = mapped_column(String, nullable=False)
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
undone: Mapped[bool] = mapped_column(nullable=False, default=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"command_type IN ('param_map', 'expression', 'physics', 'motion', "
"'material', 'texture', 'bone', 'attachment', 'lighting', 'camera')",
name="command_type_values",
),
Index("uq_avatar_command_seq", "avatar_id", "sequence", unique=True),
)