From 93807d6d8c2584f9f516b9c6024ff4c4ebf3043e Mon Sep 17 00:00:00 2001 From: xiaocheny214 <187097481+xiaocheny214@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:18:51 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(project):=20=E6=9C=9D=E5=90=91?= =?UTF-8?q?=E4=BD=9C=E4=B8=BA=E5=94=AF=E4=B8=80=E6=96=B9=E5=90=91=E8=A7=84?= =?UTF-8?q?=E6=A0=BC=EF=BC=8C=E4=B8=8D=E5=8F=A6=E5=BB=BA=20SQL=20=E8=BF=81?= =?UTF-8?q?=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 目标结构只留在 ORM。create_all 管新表,schema_sync 管加列; character_perspective 从模型拿掉后不再读写,响应里仍派生给前端过渡。 --- .../server/orchestrator/executor.py | 21 ++++---- .../src/windup_app/server/project/model.py | 2 +- .../app/src/windup_app/web/api/project.py | 51 ++++++++++++++----- .../src/windup_common/models/character.py | 14 ++--- backend/tests/conftest.py | 1 - backend/tests/test_art_style.py | 2 - backend/tests/test_character_api.py | 1 - backend/tests/test_character_contract.py | 4 +- .../tests/test_full_direction_integration.py | 1 - backend/tests/test_generation_api.py | 1 - .../tests/test_generation_orchestration.py | 30 ++++++++--- backend/tests/test_generation_quota.py | 4 -- backend/tests/test_generation_stream_auth.py | 1 - backend/tests/test_media_thumbnail.py | 1 - backend/tests/test_project_api.py | 30 ++++++++++- .../tests/test_render3d_asset_endpoints.py | 2 +- backend/tests/test_render3d_bake_endpoints.py | 2 +- .../tests/test_render3d_route_and_assets.py | 6 +-- backend/tests/test_schema_sync.py | 4 +- backend/tests/test_sensitive_word_api.py | 1 - backend/tests/test_workflow_run_api.py | 1 - openapi.json | 24 ++++----- 22 files changed, 129 insertions(+), 75 deletions(-) diff --git a/backend/packages/app/src/windup_app/server/orchestrator/executor.py b/backend/packages/app/src/windup_app/server/orchestrator/executor.py index bb1716b47..833bef5ce 100644 --- a/backend/packages/app/src/windup_app/server/orchestrator/executor.py +++ b/backend/packages/app/src/windup_app/server/orchestrator/executor.py @@ -99,14 +99,14 @@ def _close_failed(session: Session, task_id: int, error_message: str) -> None: # ── 项目全局约束(Project 表)→ 统合喂给生成逻辑 ───────────────────────── -# character_perspective 游戏视角:1=横版(侧视) 2=俯视 3=2.5D → 生成朝向/视角 -_PERSPECTIVE_FACING: dict[int, str] = {1: "side", 2: "front", 3: "front"} -_PERSPECTIVE_VIEW: dict[int, str] = { +# directional_movement 是唯一方向规格(#664): +# 1=单向 → 横版侧视 / 1 向; 2=四向 → 俯视 / 4 向; 3=八向 → 2.5D / 8 向 +_MOVEMENT_FACING: dict[int, str] = {1: "side", 2: "front", 3: "front"} +_MOVEMENT_VIEW: dict[int, str] = { 1: "side view, horizontal side-scroller", 2: "top-down view", 3: "2.5D three-quarter view", } -# directional_movement 移动方向:1=单向 2=四向 3=八向 → 需生成的方向数 _MOVEMENT_DIRECTIONS: dict[int, int] = {1: 1, 2: 4, 3: 8} @@ -114,9 +114,9 @@ def _close_failed(session: Session, task_id: int, error_message: str) -> None: class ProjectConstraints: """从 Project 取的全局生成约束,统一约束角色图/动作生成。""" - facing: str = "side" # character_perspective → 朝向(须与母版一致 #35) + facing: str = "side" # directional_movement → 朝向(须与母版一致 #35) view: str = "side view, horizontal side-scroller" - perspective: int = 1 # 1横版 2俯视 3 2.5D + perspective: int = 1 # 由朝向派生:1横版 2俯视 3 2.5D directions: int = 1 # directional_movement → 方向数(1/4/8) sprite_w: int = 256 # 输出/切帧尺寸(关键) sprite_h: int = 256 @@ -149,11 +149,12 @@ def _load_constraints(session: Session, project_id: int | None) -> ProjectConstr if p is None: return ProjectConstraints() art_style = ArtStyle.from_stored(p.game_style) + movement = p.directional_movement return ProjectConstraints( - facing=_PERSPECTIVE_FACING.get(p.character_perspective, "side"), - view=_PERSPECTIVE_VIEW.get(p.character_perspective, _PERSPECTIVE_VIEW[1]), - perspective=p.character_perspective, - directions=_MOVEMENT_DIRECTIONS.get(p.directional_movement, 1), + facing=_MOVEMENT_FACING.get(movement, "side"), + view=_MOVEMENT_VIEW.get(movement, _MOVEMENT_VIEW[1]), + perspective=movement if movement in _MOVEMENT_FACING else 1, + directions=_MOVEMENT_DIRECTIONS.get(movement, 1), sprite_w=p.sprite_width, sprite_h=p.sprite_height, style=ArtStyle.phrase_from_stored(p.game_style), diff --git a/backend/packages/app/src/windup_app/server/project/model.py b/backend/packages/app/src/windup_app/server/project/model.py index 6530ae2ac..669f301cd 100644 --- a/backend/packages/app/src/windup_app/server/project/model.py +++ b/backend/packages/app/src/windup_app/server/project/model.py @@ -26,7 +26,7 @@ class Project(Base): user_id: Mapped[int] = mapped_column(BigInteger, nullable=False) workflow_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True) project_name: Mapped[str] = mapped_column(String(20), nullable=False) - character_perspective: Mapped[int] = mapped_column(SmallInteger, nullable=False) + # 朝向是唯一的项目方向规格(#664):1=单向(横版侧视) 2=四向(俯视) 3=八向(2.5D) directional_movement: Mapped[int] = mapped_column(SmallInteger, nullable=False) sprite_width: Mapped[int] = mapped_column(SmallInteger, nullable=False) sprite_height: Mapped[int] = mapped_column(SmallInteger, nullable=False) diff --git a/backend/packages/app/src/windup_app/web/api/project.py b/backend/packages/app/src/windup_app/web/api/project.py index 954589529..22e03f90e 100644 --- a/backend/packages/app/src/windup_app/web/api/project.py +++ b/backend/packages/app/src/windup_app/web/api/project.py @@ -4,7 +4,14 @@ from datetime import datetime from fastapi import APIRouter, Depends, Query, Request -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + computed_field, + field_validator, + model_validator, +) from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session @@ -39,7 +46,9 @@ def _legacy_style_or_none(value: object) -> ArtStyle | str | None: try: return ArtStyle(value.strip()) except ValueError: - return ArtStyle.PIXEL if ArtStyle.from_stored(value) is ArtStyle.PIXEL else value + return ( + ArtStyle.PIXEL if ArtStyle.from_stored(value) is ArtStyle.PIXEL else value + ) def _stored_style(style: ArtStyle | str | None) -> str | None: @@ -53,13 +62,16 @@ def _stored_style(style: ArtStyle | str | None) -> str | None: class ProjectCreate(BaseModel): - """创建项目请求。""" + """创建项目请求。 + + ``character_perspective`` 已退役(#664):旧客户端多传该字段会被忽略, + 方向规格只认 ``directional_movement``。 + """ workflow_id: int | None = None project_name: str | None = Field(default=None, min_length=1, max_length=20) name_context: str | None = None - character_perspective: int = Field(ge=1, le=3) - directional_movement: int = Field(ge=1, le=3) + directional_movement: int = Field(ge=1, le=3, description="1=单向 2=四向 3=八向") sprite_width: int = Field(ge=32, le=2048) sprite_height: int = Field(ge=32, le=2048) game_style: ArtStyle | str = ArtStyle.UNSPECIFIED @@ -80,9 +92,7 @@ class ProjectPatch(BaseModel): game_style: ArtStyle | str | None = None auto_pixelate: bool | None = None - _accept_legacy = field_validator("game_style", mode="before")( - _legacy_style_or_none - ) + _accept_legacy = field_validator("game_style", mode="before")(_legacy_style_or_none) @model_validator(mode="after") def _at_least_one(self) -> "ProjectPatch": @@ -96,14 +106,17 @@ def _at_least_one(self) -> "ProjectPatch": class ProjectOut(BaseModel): - """项目响应。""" + """项目响应。 + + ``character_perspective`` 不再落库,由 ``directional_movement`` 派生, + 让尚未对齐的前端读列表/详情时不至于映射失败。 + """ model_config = ConfigDict(from_attributes=True) id: int workflow_id: int | None project_name: str - character_perspective: int directional_movement: int sprite_width: int sprite_height: int @@ -113,6 +126,12 @@ class ProjectOut(BaseModel): create_at: datetime update_at: datetime + @computed_field + @property + def character_perspective(self) -> int: + """与朝向 1:1:单向→横版、四向→俯视、八向→2.5D。""" + return self.directional_movement + @field_validator("game_style", mode="before") @classmethod def _normalize_style(cls, value: object) -> ArtStyle | str: @@ -128,7 +147,9 @@ def _normalize_style(cls, value: object) -> ArtStyle | str: try: return ArtStyle(text) except ValueError: - return ArtStyle.PIXEL if ArtStyle.from_stored(text) is ArtStyle.PIXEL else text + return ( + ArtStyle.PIXEL if ArtStyle.from_stored(text) is ArtStyle.PIXEL else text + ) class ProjectListOut(ProjectOut): @@ -145,7 +166,9 @@ def create_project( ) -> Response[ProjectOut]: user_id = request.state.current_user.id automatic_name = not (body.project_name or "").strip() - base_name = resolve_project_name(body.project_name, body.name_context, service._namer) + base_name = resolve_project_name( + body.project_name, body.name_context, service._namer + ) fields = body.model_dump(exclude={"project_name", "name_context"}) fields["game_style"] = _stored_style(body.game_style) @@ -166,7 +189,9 @@ def create_project( project = service.create_project( session, user_id=user_id, project_name=project_name, **fields ) - return Response.success(ProjectOut.model_validate(project), message="创建成功") + return Response.success( + ProjectOut.model_validate(project), message="创建成功" + ) except IntegrityError: logger.warning( "[WINDUP] 创建并发重名 | user_id=%s project_name=%s", diff --git a/backend/packages/common/src/windup_common/models/character.py b/backend/packages/common/src/windup_common/models/character.py index 3c3ea8946..0557873be 100644 --- a/backend/packages/common/src/windup_common/models/character.py +++ b/backend/packages/common/src/windup_common/models/character.py @@ -112,8 +112,8 @@ class Facing(str, Enum): - ``FRONT``:身体正对观者(俯视与 2.5D 都归此)。 与 :class:`CharacterView` 的对应关系:SIDE→SIDE;TOP_DOWN / ISOMETRIC→FRONT。 - 两者不合并成一个枚举:view 是项目级美术视角(对应 ``Project.character_perspective``, - 决定母版怎么画),facing 是提示词模板的二选一(只区分"看得到侧面"和"正对镜头")。 + 两者不合并成一个枚举:view 是项目级美术视角(由 ``Project.directional_movement`` + 派生,决定母版怎么画),facing 是提示词模板的二选一(只区分"看得到侧面"和"正对镜头")。 """ SIDE = "side" @@ -121,15 +121,15 @@ class Facing(str, Enum): class CharacterView(str, Enum): - """角色美术视角 —— 与 ``Project.character_perspective``(1/2/3)一一对应。 + """角色美术视角 —— 与 ``Project.directional_movement``(1/2/3)一一对应。 映射固定为 1→side、2→top-down、3→isometric。字符串取值必须逐字一致, 免得调用方再造一套别名(如 topdown / top_down / top-down 三写)。 """ - SIDE = "side" # perspective=1 横版 - TOP_DOWN = "top-down" # perspective=2 俯视 - ISOMETRIC = "isometric" # perspective=3 2.5D + SIDE = "side" # directional_movement=1 单向/横版 + TOP_DOWN = "top-down" # directional_movement=2 四向/俯视 + ISOMETRIC = "isometric" # directional_movement=3 八向/2.5D class Stylize(str, Enum): @@ -216,7 +216,7 @@ class ActionSpec(BaseModel): # 于是"我要 1 色"拿到 2 色且无任何提示 —— 正是本项目最忌讳的静默纠正。 pixel_h: int = Field(default=100, ge=1) # 像素化目标高(角色像素行数) palette_size: int = Field(default=32, ge=2) # 色板色数(1 色的像素画不存在) - # 生成提示词的朝向,**必须与母版朝向一致**(对应 Project.perspective)。 + # 生成提示词的朝向,**必须与母版朝向一致**(由 Project.directional_movement 派生)。 facing: Facing = Facing.SIDE # 项目方向集合中的一个真实源方向。镜像方向不会进入 ActionSpec,因为它不应 # 调用模型;前端/编排层会为每个源方向创建独立 GenerationTask。 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index d9193d0f0..6df049add 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -45,7 +45,6 @@ def insert_project(session, **overrides) -> Project: fields = { "user_id": 1, "project_name": "测试项目", - "character_perspective": 1, "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, diff --git a/backend/tests/test_art_style.py b/backend/tests/test_art_style.py index ec6ea9afe..2e45f6537 100644 --- a/backend/tests/test_art_style.py +++ b/backend/tests/test_art_style.py @@ -8,7 +8,6 @@ def _payload(**overrides): base = { "project_name": "画风", - "character_perspective": 1, "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, @@ -159,7 +158,6 @@ def test_constraints_follow_the_project_style(db_session, stored, stylize, phras project = Project( user_id=1, project_name=f"约束-{stored}", - character_perspective=1, directional_movement=1, sprite_width=64, sprite_height=64, diff --git a/backend/tests/test_character_api.py b/backend/tests/test_character_api.py index 396faa280..bd66d80f6 100644 --- a/backend/tests/test_character_api.py +++ b/backend/tests/test_character_api.py @@ -34,7 +34,6 @@ def _create_project( "/projects", json={ "project_name": name, - "character_perspective": 1, "directional_movement": directional_movement, "sprite_width": 64, "sprite_height": 64, diff --git a/backend/tests/test_character_contract.py b/backend/tests/test_character_contract.py index ab48365da..c44ca0ace 100644 --- a/backend/tests/test_character_contract.py +++ b/backend/tests/test_character_contract.py @@ -70,8 +70,8 @@ def test_action_spec_restricted_fields_reject_typos(field, bad, good, member): assert getattr(ActionSpec(action=ActionType.WALK, **{field: good}), field) is member -def test_character_view_rejects_typos_and_matches_perspective_mapping(): - """view 固定映射 perspective:1 side / 2 top-down / 3 isometric。 +def test_character_view_rejects_typos_and_matches_movement_mapping(): + """view 固定映射朝向规格:1 side / 2 top-down / 3 isometric。 字符串必须逐字一致,免得将来做 int↔str 映射时出现 topdown / top_down / top-down 三种写法。 diff --git a/backend/tests/test_full_direction_integration.py b/backend/tests/test_full_direction_integration.py index d858b1545..29fbf3d3b 100644 --- a/backend/tests/test_full_direction_integration.py +++ b/backend/tests/test_full_direction_integration.py @@ -74,7 +74,6 @@ def _create_eight_way_project(auth_client) -> dict: "/projects", json={ "project_name": "八向集成项目", - "character_perspective": 1, "directional_movement": 3, "sprite_width": 64, "sprite_height": 64, diff --git a/backend/tests/test_generation_api.py b/backend/tests/test_generation_api.py index 0b5cd8c05..60c93e569 100644 --- a/backend/tests/test_generation_api.py +++ b/backend/tests/test_generation_api.py @@ -28,7 +28,6 @@ def _create_project( "/projects", json={ "project_name": name, - "character_perspective": 1, "directional_movement": directional_movement, "sprite_width": 64, "sprite_height": 64, diff --git a/backend/tests/test_generation_orchestration.py b/backend/tests/test_generation_orchestration.py index 4f0f2477f..a35e58e07 100644 --- a/backend/tests/test_generation_orchestration.py +++ b/backend/tests/test_generation_orchestration.py @@ -413,12 +413,12 @@ def generate(self, card, action, master, progress, canvas=None): ) -def test_project_perspective_constrains_facing(session_factory): - # perspective=2 → front(见 executor._PERSPECTIVE_TO_FACING) +def test_project_movement_constrains_facing(session_factory): + # directional_movement=2 四向 → 俯视 front(见 executor._MOVEMENT_FACING) with session_factory() as s: proj = Project( - user_id=1, project_name="p", character_perspective=2, - directional_movement=1, sprite_width=64, sprite_height=64, + user_id=1, project_name="p", + directional_movement=2, sprite_width=64, sprite_height=64, ) s.add(proj) s.commit() @@ -441,7 +441,23 @@ def test_project_perspective_constrains_facing(session_factory): executor.run_action_task(task_id, action_input, project_id) # 带项目约束 - assert spy.seen_facing == "front", "项目 perspective 应约束生成朝向" + assert spy.seen_facing == "front", "项目朝向规格应约束生成朝向" + + +def test_unidirectional_project_keeps_side_facing(session_factory): + with session_factory() as s: + proj = Project( + user_id=1, project_name="side", + directional_movement=1, sprite_width=64, sprite_height=64, + ) + s.add(proj) + s.commit() + from windup_app.server.orchestrator.executor import _load_constraints + + cons = _load_constraints(s, proj.id) + assert cons.facing == "side" + assert cons.perspective == 1 + assert cons.directions == 1 def test_custom_action_reuses_oneshot_route_and_preserves_prompt(session_factory): @@ -577,7 +593,7 @@ def _run_with_project(session_factory, spy, sprite=(64, 64)): """建一个指定 sprite 尺寸的项目,跑一次动作任务,返回 (task_id, project_id)。""" with session_factory() as s: proj = Project( - user_id=1, project_name="p", character_perspective=1, + user_id=1, project_name="p", directional_movement=1, sprite_width=sprite[0], sprite_height=sprite[1], ) s.add(proj) @@ -967,7 +983,7 @@ def _delivered_colors(game_style: str | None, session_factory, monkeypatch) -> i ) with session_factory() as s: project = Project( - user_id=1, project_name=f"画风-{game_style}", character_perspective=1, + user_id=1, project_name=f"画风-{game_style}", directional_movement=1, sprite_width=64, sprite_height=64, game_style=game_style, ) diff --git a/backend/tests/test_generation_quota.py b/backend/tests/test_generation_quota.py index 497d00f50..138300933 100644 --- a/backend/tests/test_generation_quota.py +++ b/backend/tests/test_generation_quota.py @@ -88,7 +88,6 @@ def test_submit_image_generation_reserves_prepaid_credit(auth_client, db_session "/projects", json={ "project_name": "积分项目", - "character_perspective": 1, "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, @@ -121,7 +120,6 @@ def test_submit_image_generation_reserves_per_requested_image(auth_client, db_se "/projects", json={ "project_name": "按张计费", - "character_perspective": 1, "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, @@ -160,7 +158,6 @@ def test_submit_rejects_when_credit_is_insufficient(auth_client, db_session): "/projects", json={ "project_name": "没钱项目", - "character_perspective": 1, "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, @@ -190,7 +187,6 @@ def test_submit_rejects_when_credit_covers_one_image_but_not_three(auth_client, "/projects", json={ "project_name": "一张的钱不够三张", - "character_perspective": 1, "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, diff --git a/backend/tests/test_generation_stream_auth.py b/backend/tests/test_generation_stream_auth.py index a3bc7cca1..8da485ab3 100644 --- a/backend/tests/test_generation_stream_auth.py +++ b/backend/tests/test_generation_stream_auth.py @@ -34,7 +34,6 @@ def _create_project(client, name: str = "SSE 项目") -> dict: return client.post("/projects", json={ "project_name": name, - "character_perspective": 1, "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, diff --git a/backend/tests/test_media_thumbnail.py b/backend/tests/test_media_thumbnail.py index b9c6112f8..2636eea27 100644 --- a/backend/tests/test_media_thumbnail.py +++ b/backend/tests/test_media_thumbnail.py @@ -271,7 +271,6 @@ def test_delete_character_cleans_card_thumbnails(mock_media_service, monkeypatch "/projects", json={ "project_name": "缩略图清理", - "character_perspective": 1, "directional_movement": 1, "sprite_width": 64, "sprite_height": 64, diff --git a/backend/tests/test_project_api.py b/backend/tests/test_project_api.py index babc25b9d..8f12a933c 100644 --- a/backend/tests/test_project_api.py +++ b/backend/tests/test_project_api.py @@ -42,7 +42,6 @@ def _payload(**overrides): """构造合法的创建请求体(对齐 ``ProjectCreate``)。""" base = { "project_name": "像素游戏", - "character_perspective": 1, "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, @@ -64,10 +63,39 @@ def test_create_success(auth_client): assert body["data"]["id"] is not None assert "user_id" not in body["data"] assert body["data"]["project_name"] == "新建" + assert body["data"]["directional_movement"] == 2 + assert body["data"]["character_perspective"] == 2 assert body["data"]["create_at"] assert "timestamp" not in body +def test_create_does_not_require_character_perspective(auth_client): + """#664: 朝向是唯一方向规格,创建不再要求游戏视角。""" + resp = auth_client.post("/projects", json=_payload(project_name="只传朝向")) + + assert resp.json()["code"] == 200 + data = resp.json()["data"] + assert "character_perspective" in data + assert data["character_perspective"] == data["directional_movement"] + + +def test_create_ignores_legacy_character_perspective(auth_client): + """旧客户端仍可能多传 character_perspective;不能当第二真相源写入。""" + resp = auth_client.post( + "/projects", + json=_payload( + project_name="旧字段", + directional_movement=3, + character_perspective=1, + ), + ) + + data = resp.json()["data"] + assert resp.json()["code"] == 200 + assert data["directional_movement"] == 3 + assert data["character_perspective"] == 3 + + def test_create_without_name_extracts_project_title(auth_client): description = "一位提着风灯、披深色斗篷的像素守夜人" diff --git a/backend/tests/test_render3d_asset_endpoints.py b/backend/tests/test_render3d_asset_endpoints.py index c2ff1c010..d0555b55a 100644 --- a/backend/tests/test_render3d_asset_endpoints.py +++ b/backend/tests/test_render3d_asset_endpoints.py @@ -115,7 +115,7 @@ def api(auth_client, engine, render3d): from sqlalchemy.orm import sessionmaker session = sessionmaker(bind=engine)() - session.add(Project(id=1, user_id=1, project_name="p", character_perspective=1, + session.add(Project(id=1, user_id=1, project_name="p", directional_movement=1, sprite_width=64, sprite_height=64)) # 先落 project 再插 character:两者在同一 session 里时 SQLAlchemy 不保证插入顺序, # 而 character.project_id 有真外键 —— 顺序反了就是 FOREIGN KEY constraint failed。 diff --git a/backend/tests/test_render3d_bake_endpoints.py b/backend/tests/test_render3d_bake_endpoints.py index 02b61c820..ea2fc39bf 100644 --- a/backend/tests/test_render3d_bake_endpoints.py +++ b/backend/tests/test_render3d_bake_endpoints.py @@ -104,7 +104,7 @@ def public_view(task_id): def api(auth_client, engine, store): """一个属于 user 1 的 RUNNING 动作任务,外加一条属于别人的同类任务。""" session = sessionmaker(bind=engine)() - session.add(Project(id=1, user_id=1, project_name="p", character_perspective=1, + session.add(Project(id=1, user_id=1, project_name="p", directional_movement=1, sprite_width=64, sprite_height=64)) session.flush() for task_id, user_id in ((TASK_ID, 1), (TASK_ID + 1, 2)): diff --git a/backend/tests/test_render3d_route_and_assets.py b/backend/tests/test_render3d_route_and_assets.py index c39edf36d..f6f9d67fb 100644 --- a/backend/tests/test_render3d_route_and_assets.py +++ b/backend/tests/test_render3d_route_and_assets.py @@ -279,7 +279,7 @@ def test_web_layer_reads_the_outfit_model_url_into_the_task_input(auth_client, d db_session.commit() project = auth_client.post("/projects", json={ - "project_name": "三渲二", "character_perspective": 1, "directional_movement": 2, + "project_name": "三渲二", "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, }).json()["data"] character = auth_client.post("/characters", json={ @@ -317,7 +317,7 @@ def test_web_layer_does_not_guess_an_outfit_when_none_is_given(auth_client, db_s db_session.commit() project = auth_client.post("/projects", json={ - "project_name": "三渲二", "character_perspective": 1, "directional_movement": 2, + "project_name": "三渲二", "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, }).json()["data"] character = auth_client.post("/characters", json={ @@ -347,7 +347,7 @@ def test_unknown_outfit_id_is_rejected_not_ignored(auth_client): """造型 id 对不上要报错。静默当成"没有资产"会让用户以为三渲二不可用, 实际是他把 id 打错了。""" project = auth_client.post("/projects", json={ - "project_name": "三渲二", "character_perspective": 1, "directional_movement": 2, + "project_name": "三渲二", "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, }).json()["data"] character = auth_client.post("/characters", json={ diff --git a/backend/tests/test_schema_sync.py b/backend/tests/test_schema_sync.py index 872fbedb1..66a225474 100644 --- a/backend/tests/test_schema_sync.py +++ b/backend/tests/test_schema_sync.py @@ -93,9 +93,9 @@ def test_it_fixes_the_real_case_a_live_table_missing_a_new_column(): with eng.begin() as conn: # 退回加列之前的库 conn.execute(text(f"ALTER TABLE windup_project DROP COLUMN {new_column}")) conn.execute(text( - "INSERT INTO windup_project (id, project_name, user_id, character_perspective," + "INSERT INTO windup_project (id, project_name, user_id," " directional_movement, sprite_width, sprite_height, create_at, update_at)" - " VALUES (1,'存量项目',1,1,0,256,256,'2026-01-01','2026-01-01')" + " VALUES (1,'存量项目',1,0,256,256,'2026-01-01','2026-01-01')" )) with pytest.raises(Exception, match="sprite_sample_url"): diff --git a/backend/tests/test_sensitive_word_api.py b/backend/tests/test_sensitive_word_api.py index 5b03a518e..f6e215366 100644 --- a/backend/tests/test_sensitive_word_api.py +++ b/backend/tests/test_sensitive_word_api.py @@ -21,7 +21,6 @@ def test_sensitive_prompt_is_rejected_before_task_and_credit( "/projects", json={ "project_name": "过滤项目", - "character_perspective": 1, "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, diff --git a/backend/tests/test_workflow_run_api.py b/backend/tests/test_workflow_run_api.py index 684d19f61..967c98e65 100644 --- a/backend/tests/test_workflow_run_api.py +++ b/backend/tests/test_workflow_run_api.py @@ -5,7 +5,6 @@ def _create_project(auth_client, name: str = "默认项目") -> dict: """创建一个项目并返回响应 data。""" return auth_client.post("/projects", json={ "project_name": name, - "character_perspective": 1, "directional_movement": 2, "sprite_width": 64, "sprite_height": 64, diff --git a/openapi.json b/openapi.json index 479a9391c..26e354d2b 100644 --- a/openapi.json +++ b/openapi.json @@ -2322,20 +2322,15 @@ "type": "object" }, "ProjectCreate": { - "description": "创建项目请求。", + "description": "创建项目请求。\n\n``character_perspective`` 已退役(#664):旧客户端多传该字段会被忽略,\n方向规格只认 ``directional_movement``。", "properties": { "auto_pixelate": { "default": true, "title": "Auto Pixelate", "type": "boolean" }, - "character_perspective": { - "maximum": 3.0, - "minimum": 1.0, - "title": "Character Perspective", - "type": "integer" - }, "directional_movement": { + "description": "1=单向 2=四向 3=八向", "maximum": 3.0, "minimum": 1.0, "title": "Directional Movement", @@ -2413,7 +2408,6 @@ } }, "required": [ - "character_perspective", "directional_movement", "sprite_width", "sprite_height" @@ -2429,6 +2423,8 @@ "type": "boolean" }, "character_perspective": { + "description": "与朝向 1:1:单向→横版、四向→俯视、八向→2.5D。", + "readOnly": true, "title": "Character Perspective", "type": "integer" }, @@ -2511,7 +2507,6 @@ "id", "workflow_id", "project_name", - "character_perspective", "directional_movement", "sprite_width", "sprite_height", @@ -2520,19 +2515,22 @@ "sprite_sample_url", "create_at", "update_at", - "preview_url" + "preview_url", + "character_perspective" ], "title": "ProjectListOut", "type": "object" }, "ProjectOut": { - "description": "项目响应。", + "description": "项目响应。\n\n``character_perspective`` 不再落库,由 ``directional_movement`` 派生,\n让尚未对齐的前端读列表/详情时不至于映射失败。", "properties": { "auto_pixelate": { "title": "Auto Pixelate", "type": "boolean" }, "character_perspective": { + "description": "与朝向 1:1:单向→横版、四向→俯视、八向→2.5D。", + "readOnly": true, "title": "Character Perspective", "type": "integer" }, @@ -2604,7 +2602,6 @@ "id", "workflow_id", "project_name", - "character_perspective", "directional_movement", "sprite_width", "sprite_height", @@ -2612,7 +2609,8 @@ "auto_pixelate", "sprite_sample_url", "create_at", - "update_at" + "update_at", + "character_perspective" ], "title": "ProjectOut", "type": "object" From 8415a7320444ca64400d0bf61af8dbd4c226b126 Mon Sep 17 00:00:00 2001 From: xiaocheny214 <187097481+xiaocheny214@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:27:48 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(header):=20=E7=AD=89=E5=88=9B=E4=BD=9C?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E5=8F=98=E6=88=90=E8=BF=9B=E8=A1=8C=E4=B8=AD?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=86=8D=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rememberActiveRun 后立刻 getByRole 会在订阅尚未刷 UI 时失败。 --- frontend/src/app/layout/app-header.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/layout/app-header.test.tsx b/frontend/src/app/layout/app-header.test.tsx index bd8e7d661..3990ce996 100644 --- a/frontend/src/app/layout/app-header.test.tsx +++ b/frontend/src/app/layout/app-header.test.tsx @@ -166,7 +166,7 @@ describe('AppHeader 进行中任务入口', () => { expect(document.querySelector('[data-active-run-bot]')).toBeNull() act(() => rememberActiveRun('7', '77')) - const entry = screen.getByRole('button', { name: '创作,有任务进行中' }) + const entry = await screen.findByRole('button', { name: '创作,有任务进行中' }) const bot = entry.querySelector('[data-active-run-bot]') expect(bot).toBeTruthy() // 文字保留:这一项不该在图标和文字之间换形态。