✨ 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
@@ -0,0 +1,72 @@
|
||||
"""Avatar 模型与非破坏命令历史
|
||||
|
||||
Revision ID: e66d791926f2
|
||||
Revises: 4e679f11da46
|
||||
Create Date: 2026-09-14 10:24:23.502856
|
||||
|
||||
"""
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'e66d791926f2'
|
||||
down_revision: str | Sequence[str] | None = '4e679f11da46'
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('avatar_model',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('domain_id', sa.String(), nullable=False),
|
||||
sa.Column('character_id', sa.String(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('format', sa.String(), nullable=False),
|
||||
sa.Column('file_id', sa.String(), nullable=False),
|
||||
sa.Column('status', sa.String(), nullable=False),
|
||||
sa.Column('validation_report', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('source_info', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('license_terms', sa.String(), nullable=True),
|
||||
sa.Column('current_command_id', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.CheckConstraint("format IN ('live2d', 'vrm', 'gltf')", name=op.f('ck_avatar_model_format_values')),
|
||||
sa.CheckConstraint("status IN ('pending', 'validated', 'rejected')", name=op.f('ck_avatar_model_status_values')),
|
||||
sa.ForeignKeyConstraint(['character_id'], ['character.id'], name=op.f('fk_avatar_model_character'), ondelete='set null'),
|
||||
sa.ForeignKeyConstraint(['current_command_id'], ['avatar_command.id'], name=op.f('fk_avatar_model_avatar_command'), ondelete='set null', use_alter=True),
|
||||
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_avatar_model_personal_domain'), ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['file_id'], ['file_asset.id'], name=op.f('fk_avatar_model_file_asset'), ondelete='restrict'),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_avatar_model'))
|
||||
)
|
||||
op.create_index(op.f('ix_avatar_model_domain_id'), 'avatar_model', ['domain_id'], unique=False)
|
||||
op.create_table('avatar_command',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('avatar_id', sa.String(), nullable=False),
|
||||
sa.Column('sequence', sa.Integer(), nullable=False),
|
||||
sa.Column('command_type', sa.String(), nullable=False),
|
||||
sa.Column('payload', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('undone', sa.Boolean(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.CheckConstraint("command_type IN ('param_map', 'expression', 'physics', 'motion', 'material', 'texture', 'bone', 'attachment', 'lighting', 'camera')", name=op.f('ck_avatar_command_command_type_values')),
|
||||
sa.ForeignKeyConstraint(['avatar_id'], ['avatar_model.id'], name=op.f('fk_avatar_command_avatar_model'), ondelete='cascade'),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_avatar_command'))
|
||||
)
|
||||
op.create_index(op.f('ix_avatar_command_avatar_id'), 'avatar_command', ['avatar_id'], unique=False)
|
||||
op.create_index('uq_avatar_command_seq', 'avatar_command', ['avatar_id', 'sequence'], unique=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('uq_avatar_command_seq', table_name='avatar_command')
|
||||
op.drop_index(op.f('ix_avatar_command_avatar_id'), table_name='avatar_command')
|
||||
op.drop_table('avatar_command')
|
||||
op.drop_index(op.f('ix_avatar_model_domain_id'), table_name='avatar_model')
|
||||
op.drop_table('avatar_model')
|
||||
# ### end Alembic commands ###
|
||||
Reference in new issue
Block a user