✨ 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:
1 parent
9dafb8fe72
commit
7c57d27494
7 files changed
+839
No files matched your search
@@ -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),
|
||||
)
|
||||
Reference in new issue
Block a user