Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions frontend-architecture-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pages -> features -> entities -> shared

`app` 只做启动和路由,不构造服务、不向下注入。

外壳套在哪些页面上也是路由决策:`AppShellRoute` 写在 `app.tsx` 的路由表里,谁在里面谁就有顶栏。目前全部路由都在里面,根路由也是——顶栏悬浮在内容之上、不占布局高度,首屏仍是满幅,而首页同样需要通往项目资产的常驻入口。外壳组件自身不读 pathname,不判断自己该不该出现——那种写法每多一个特殊页面就多一条 `if`;顶栏内部读 pathname 只为高亮当前项,与此无关。外壳也不统一夹居中容器,宽度与留白由页面自己决定:顶栏既然悬浮,避让由页面负责,内容页统一走 `PageContainer`
外壳套在哪些页面上也是路由决策:`AppShellRoute` 写在 `app.tsx` 的路由表里,谁在里面谁就有顶栏。首页、快速开始、Workflow Editor 与 Playtest 使用全局外壳;`/projects/:projectId/*` 是独立项目工作区,由 `ProjectDetailPage` 提供项目级导航,不重复套全局顶栏。外壳组件自身不读 pathname,不判断自己该不该出现;顶栏内部读 pathname 只为高亮当前项。外壳也不统一夹居中容器,宽度与留白由页面自己决定。

### 依赖规则

Expand Down Expand Up @@ -86,18 +86,18 @@ Controller 围绕同一份 WorkflowRun 提供推进、更新、重启和中断

---

## 5. 尚未包含
## 5. 当前实现范围

- 真实请求与数据获取,`XxxApis` 目前只有接口
- 首页之外的页面实现,其余七个路由仍是占位外壳
- 图片上传模块(体量太小,不单独体现)
- 穿戴道具相关(产品侧未设计)
- 第三方登录
- `ProjectApis` 与 `CharacterApis` 实现 PR #75 的 Project、Character HTTP 契约;snake_case 只存在于各实体模块内部的 DTO 映射。
- 项目中心、项目工作区、角色资产库、角色详情按 `Project → Character → Outfit → Action → Frame` 层级读取真实接口。
- 测试通过 HTTP 替身返回契约数据;本模块的生产代码不包含 Mock API、演示实体或 livedemo 资产。

首页已按本文的分层落地:它不依赖 `entities` 与 `features`,两张入口卡片只做路由跳转。首屏那三段制作路径是 `WORKFLOW_STEP_ORDER` 八步的粗粒度概括,写死在页面文案里,改流程时要一并改。
本轮不包含新建项目流程、Workflow Editor 实现、Action Template 后端能力、导出接线、图片上传与登录流程。穿戴道具不作为独立资产层暴露。

首页仍不依赖 `entities` 与 `features`,两张入口卡片只做路由跳转。首屏三段制作路径是 `WORKFLOW_STEP_ORDER` 八步的粗粒度概括,改流程时要一并改。

---

## 6. 未与后端对齐的部分

明细见 `frontend/API_CONTRACT.md`。
PR #75 尚未合并,因此本实现要求先合并该后端 PR;契约明细与仍需后端处理的问题见 `frontend/API_CONTRACT.md`。
143 changes: 50 additions & 93 deletions frontend/API_CONTRACT.md
Original file line number Diff line number Diff line change
@@ -1,119 +1,76 @@
# 前后端接口对齐清单

前端各模块的 `XxxApis` 与后端 2026-07-30 接口文档逐条比对结果
本实现以尚未合并的后端 PR #75 为目标契约,并要求按 **#75 → 本前端 PR** 的顺序合并。`upstream/main` 当前尚未挂载这些接口

后端现有四个相关模块:`project`、`character`、`generation`、`media`。`asset` 与 `wearable` 已按 07-30 评审要求删除。
## 一、本轮已接入

---
### Project

## 一、已经确认的边界

- `WorkflowRun` 是前端固定工作流的运行态。后端不读取、不推进、也不持久化,前端不声明 `WorkflowRunApis`。
- `Character` 不使用独立 `name` 字段;前端已删除。
- 前端保留 `jump` 动作类型,由后端补充对应枚举。
- 查询生成任务统一携带 `projectId + taskId`。
- 前端工作流节点不与后端 `GenerationType` 一一对应,按下表调用:

| 前端工作流节点 | 后端接口 | 后端任务类型 |
| 前端方法 | HTTP | 后端能力 |
|---|---|---|
| `character_template` | `POST /generation/image` | `character_image` |
| `first_frame` | `POST /generation/image`,以上一步角色图作为参考图 | `character_image` |
| `complete_animation` | `POST /generation/action`,以已确认动作首帧作为参考图 | `character_action` |

图片生成和动作生成只返回任务及结果,不自动修改 WorkflowRun 或角色资产。用户最终确认后,前端再通过角色更新接口保存角色图和完整动作数据。

---

## 二、前端预期有、后端目前没有
| `ProjectApis.list` | `GET /projects` | `page`、`page_size`、可选 `user_id` |
| `ProjectApis.get` | `GET /projects/{id}` | 项目详情 |
| `ProjectApis.create` | `POST /projects` | 创建项目记录 |
| `ProjectApis.remove` | `DELETE /projects/{id}` | 删除项目记录 |

**这些接口仍需要确定由后端提供,还是改为前端本地能力。**
`ProjectOut` 的 `user_id`、`workflow_id`、`project_name`、`character_perspective`、`directional_movement`、精灵宽高、画风、参考图和时间字段,均在 `entities/project` 内显式映射为 camelCase。PR #75 没有项目更新端点,因此前端不声明 `ProjectApis.update`。

| 前端接口 | 后端情况 |
|---|---|
| `ActionTemplateApis.listAvailable` | 没有 action template 模块 |
| `ProjectApis.update` | 没有 `PATCH /projects/{project_id}` |
后端枚举按下表映射:

前端已按服务端现状去掉生成任务的 `cancel`——后端没有取消能力,不声明前端用不到的接口。

---

## 三、形状不一致

这些差异可以在前端接口层转换,不要求领域类型与后端 DTO 使用相同命名。

| 项 | 后端 | 前端 |
| 后端值 | `character_perspective` | `directional_movement` |
|---|---|---|
| 角色列表 | `list_characters` 分页,返回 `(list, total)` | `listByProject` 无分页 |
| 更新角色 | `update_character(character_id, **fields)` 部分更新 | `update(character)` 整棵树替换 |
| 等待任务完成 | 提供 `GET /generation/tasks/{task_id}` 轮询 | `GenerationApis.subscribe`,实现时可封装轮询 |
| 图片生成数量 | 入参有 `num_images`,结果只有一个 `image_url` | 角色图候选结果是 `images[]` |
| 动作类型 | `walk` `idle` `attack` `custom`;待增加 `jump` | `walk` `idle` `attack` `jump` `custom` |
| 角色视角 | `character_perspective` 为 `1~3`,文档中 2、3 都写成“正面” | `side` `top-down` `isometric` |

ID 类型后端为 `int`、前端为 `string`,由前端转换层处理,不需要后端改动。
| `1` | `side` | `single` |
| `2` | `top-down` | `four-way` |
| `3` | `isometric` | `eight-way` |

---
### Character 资产树

## 四、后端有、前端没接

| 后端 | 说明 |
|---|---|
| `delete_character` | 前端 `CharacterApis` 没有删除 |
| `Character.description` | 后端存在实体上;前端只在创建入参里,创建完查不到 |
| `Character.reference_image_url` | 后端存在实体上;前端 `Character` 类型没有这个字段 |
| `MediaService.upload` | 前端本次未提交上传模块 |

---

## 五、前端资产字段在后端没有落点
| 前端方法 | HTTP | 后端能力 |
|---|---|---|
| `CharacterApis.listByProject` | `GET /characters?project_id=...` | 项目内角色分页列表 |
| `CharacterApis.get` | `GET /characters/{id}` | 角色详情 |
| `CharacterApis.create` | `POST /characters` | 创建空角色记录 |
| `CharacterApis.update` | `PATCH /characters/{id}` | 更新角色及完整资产树 |
| `CharacterApis.remove` | `DELETE /characters/{id}` | 删除角色及其媒体对象 |

后端 `character_data` 的嵌套结构(见 `character/model.py`)
后端持久化层级为

```text
outfits[] → id / name / preview_url / actions[]
actions[] → id / type / name / loop / fps / frame_count / frames[]
frames[] → index / image_url / duration_ms
Character
└── character_data
└── outfits[]
└── actions[]
└── frames[]
```

前端以下字段在后端结构里没有落点:

- `Action.kind`(preset / custom 来源)
- `Action.keyFrameIndex`
- `Frame.rootMotion`
- `Outfit.candidateCharacterTemplates`(母版候选列表)
- `Outfit.characterTemplateUrl`(每套造型的已确认角色图)
- `Outfit.baseFrames`

`candidateCharacterTemplates` 属于生成过程数据;若只在当前 WorkflowRun 中使用,可以留在前端。其余字段若要随最终资产恢复,需要后端增加字段,或者前端在 MVP 中删除。

---

## 六、概念不一致

后端 `character/model.py` 字段说明:
前端只映射后端真实字段:

> `reference_image_url`: 角色参考图,即旧概念中的 Character Template
- Character:`id`、`project_id`、`name`、`description`、`reference_image_url`、`character_data.version`、`status`
- Outfit:`id`、`name`、`description`、`preview_url`、`actions`
- Action:`id`、`type`、`name`、`loop`、`fps`、`frame_count`、`frames`
- Frame:`index`、`image_url`、`duration_ms`

前端把这两者当成不同的东西:
Outfit、Action、Frame 没有独立端点。`outfit.characterId` 与 `action.outfitId` 仅由嵌套关系推导;修改任一子项时通过 `PATCH Character` 提交完整 `character_data`。

- 用户上传的参考图 —— 创建角色时的输入
- AI 生成后用户选定的角色图(母版)—— `Outfit.characterTemplateUrl`
## 二、本轮明确不实现

**后端合成了一个字段。** 07-30 评审也提到「模板」这个叫法容易与 action template 混淆,暂改称「角色图」。三方对这里是几个概念的理解需要统一。
- 新建项目流程:只保留禁用入口,后续单独实现。
- Workflow Editor 与生成流程:不在 Projects / 资产库模块内创建弹窗或复制生成逻辑。
- Action Template:后端没有模块、存储或 HTTP 接口,只保留带原因的禁用入口。
- 导出:PR #75 没有导出接口;PR #97 是尚未接入资产页的前端打包实现,只保留带原因的禁用入口。
- 穿戴资产:当前产品定义不向用户暴露独立 Wearable 层级。
- GIF:Character 契约只提供 Frame 图片 URL;动作卡预览使用排序后的第一帧,不伪造 GIF 字段。

---
## 三、仍需后端处理

## 待确认
这些问题不由前端降级或伪造数据规避:

- [ ] `ActionTemplateApis` 由后端提供还是前端内置
- [ ] 母版候选几张
- [ ] 参考图与角色图是一个字段还是两个
- [ ] `Character.description` 前端要不要跟着存
- [ ] `Action.kind` / `Action.keyFrameIndex` / `Frame.rootMotion` 是否进入最终资产
- [ ] 上传模块何时提交
1. PR #75 的 `POST /characters` DTO 接收 `name`,但路由没有把 `body.name` 传给 service;前端仍按已声明契约发送 `name`。
2. Project / Character 路由通过 JWT,但资源查询没有按 `request.state.current_user` 强制归属隔离;前端不能代替后端完成权限边界。
3. Project 删除没有级联 Character,可能留下孤立角色;数据一致性由后端修复。

## 已分工
## 四、运行前置

- [x] 前端删除 `WorkflowRunApis`,WorkflowRun 全程由前端管理
- [x] 前端删除 `Character.name`
- [ ] 后端增加 `jump` 动作类型
- 配置 `VITE_API_BASE_URL`。
- PR #75 的 Project / Character 路由要求 Bearer access token。Project、Character 实例已统一使用 `getApiAccessToken`;后续登录模块通过 `registerApiAccessTokenProvider` 提供实际 token。token 的取得、保存与刷新不属于本轮,接入前不能把未鉴权请求视为端到端可用。
- 本模块的生产代码不包含 Mock API 或 livedemo 资产。测试只在 Vitest 中用 HTTP 服务替身验证请求、响应映射与页面行为。
2 changes: 1 addition & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ CI 按上面顺序全跑一遍。

模块划分、依赖规则与命名约定见仓库根目录 `frontend-architecture-v3.md`。

模块边界与接口已经落地,页面实现按模块拆成多个 PR 陆续进来。**目前只有首页是真实现,其余七个路由仍是占位外壳**,`entities` 与 `features` 也只有类型和 `XxxApis` 接口,没有真实请求
`ProjectApis` 与 `CharacterApis` 负责业务 DTO 映射。项目中心、项目工作区、资产库与角色详情已接入 PR #75 的真实接口;测试数据只存在于测试环境的 HTTP 替身中

页面自己决定宽度与留白,`AppShell` 只提供顶栏,不再统一夹一个居中容器。

Expand Down
45 changes: 28 additions & 17 deletions frontend/src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BrowserRouter, Route, Routes } from 'react-router'
import { BrowserRouter, Navigate, Route, Routes } from 'react-router'

import { AssetLibraryPage } from '@/pages/asset-library'
import { CharacterDetailPage } from '@/pages/character-detail'
import { HomePage } from '@/pages/home'
import { NotFoundPage } from '@/pages/not-found'
import { PlaytestPage } from '@/pages/playtest'
Expand All @@ -13,26 +14,36 @@ import { AppShellRoute } from './layout'
/**
* 路由表与全局外壳。
* 页面自己获取所需数据,不再由 app 层构造服务后逐层传入。
* 外壳的边界画在这张表上:全部路由都在里面,包括根路由——顶栏悬浮不占高度,
* 首屏仍是满幅,同时首页也才有通往项目资产的常驻入口
* 外壳的边界画在这张表上:首页与流程页使用全局顶栏;项目工作区使用自己的
* 项目导航,不重复套全局外壳
*/
export function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<AppShellRoute />}>
<Route path="/" element={<HomePage />} />
<Route path="/quick-start" element={<QuickStartPage />} />
<Route path="/quick-start/:runId" element={<QuickStartPage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/projects/:projectId" element={<ProjectDetailPage />} />
<Route path="/projects/:projectId/assets" element={<AssetLibraryPage />} />
<Route path="/workflow-editor/:runId" element={<WorkflowEditorPage />} />
<Route path="/workflow-editor/:runId/:stage" element={<WorkflowEditorPage />} />
<Route path="/playtest/:characterId/:outfitId" element={<PlaytestPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
<AppRoutes />
</BrowserRouter>
)
}

/** 路由声明独立导出,测试用 MemoryRouter 验证直达地址。 */
export function AppRoutes() {
return (
<Routes>
<Route element={<AppShellRoute />}>
<Route path="/" element={<HomePage />} />
<Route path="/quick-start" element={<QuickStartPage />} />
<Route path="/quick-start/:runId" element={<QuickStartPage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/workflow-editor/:runId" element={<WorkflowEditorPage />} />
<Route path="/workflow-editor/:runId/:stage" element={<WorkflowEditorPage />} />
<Route path="/playtest/:characterId/:outfitId" element={<PlaytestPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
<Route path="/projects/:projectId" element={<ProjectDetailPage />}>
<Route index element={<Navigate replace to="assets" />} />
<Route path="assets" element={<AssetLibraryPage />} />
<Route path="assets/:characterId" element={<CharacterDetailPage />} />
</Route>
</Routes>
)
}
2 changes: 1 addition & 1 deletion frontend/src/app/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { App } from './app'
export { App, AppRoutes } from './app'
Loading
Loading