合并编辑器素材库分支

合并 codex/editor-asset-library 到 AI 游戏创作 App 分支。

保留 AI 游戏创作客户端配置决策,并合入素材库分支新增文档。

排除素材库分支误带的 .env.local 本机密钥改动。
This commit is contained in:
AIGameCreator App
2026-06-30 15:23:41 +08:00
197 changed files with 18543 additions and 4560 deletions
@@ -64,7 +64,24 @@
└─ manifest.txt
```
角色动作单图层导出时,直接生成独立 ZIP:
角色动作单图层右键“导出为”展开二级菜单:
- `序列帧导出(zip)` 生成普通序列帧 ZIP,保留完整 `frames/`,并由前端把成功读取的帧拼成 `preview.gif`。
- `Spine 导出(zip)` 生成 Spine JSON ZIP,保留 `frames/`,并附带 `skeleton.json`。
普通序列帧 ZIP 结构:
```text
角色动作-Sequence.zip
├─ frames/
│ ├─ frame-01.png
│ └─ frame-02.png
├─ preview.gif
├─ metadata.json
└─ manifest.txt
```
Spine JSON ZIP 结构:
```text
角色动作-SpineJSON.zip
@@ -149,9 +166,10 @@ assetObjectId > objectKey > sourceAssetId > src
4. 对每个素材源读取 Blob:
- `data:image/...` 直接转换为 Blob。
- 同源或可访问 URL 使用 `fetch` 拉取 Blob。
- 私有 generated / OSS 素材必须走同源 `/api/assets/read-bytes` 读取字节;不要在导出流程里直接 `fetch` OSS 签名 URL,避免浏览器 CORS 拦截。
- 私有 generated / OSS 素材先走 `/api/assets/read-url` 换签并由浏览器直接 `fetch` OSS 签名 URL;只有换签或 OSS 字节读取失败时,才 fallback 到同源 `/api/assets/read-bytes`。
- `mediaType="image-sequence"` 逐帧读取 `imageSequenceFrames`,写入 `sequences/<编号-标题>/frames/`。
- `mediaType="image-sequence"` 同步写入 `skeleton.json`,供 Spine Editor 或 runtime 以 slot attachment timeline 方式播放序列帧。
- 画布素材 ZIP 内的 `mediaType="image-sequence"` 同步写入 `skeleton.json`,供 Spine Editor 或 runtime 以 slot attachment timeline 方式播放序列帧。
- 单图层普通序列帧导出写入 `preview.gif`,由前端基于成功读取的帧生成动画预览,不依赖压缩包读取时再临时播放 PNG。
- 拉取失败时记录失败项,不中断整个导出。
5. 使用 `JSZip` 写入 `images/`、`metadata.json` 和 `manifest.txt`。
6. `zip.generateAsync({ type: 'blob' })` 生成文件。
@@ -176,6 +194,8 @@ assetObjectId > objectKey > sourceAssetId > src
- 图层的锁定、翻转、分组状态写入元数据。
- `data:image` 图片不经过网络请求即可导出。
- URL 图片 fetch 失败时不中断其他素材导出。
- 动作图层右键菜单把 `导出为` 作为一级入口,二级菜单提供 `序列帧导出(zip)` 和 `Spine 导出(zip)`。
- 单图层普通序列帧 ZIP 包含 `frames/`、`preview.gif`、`metadata.json` 和 `manifest.txt`,且不包含 `skeleton.json`。
- 序列帧导出的 `skeleton.json` 可被独立验证器解析并预览。
## Spine JSON 验证器
File diff suppressed because one or more lines are too long
@@ -0,0 +1,54 @@
# React 组件测试准则
更新时间:`2026-06-26`
## 背景
当前前端测试已经覆盖大量 React 组件、hook 和页面流程,但部分用例把组件内部状态、DOM 探针、按钮顺序、图标 class 或完整文案当成契约。这样的测试能快速发现改动,却也会让 UI 结构、交互文案和实现拆分变得很难迭代。
后续新增或重写 React 测试时,默认按本准则选择断言层级。已有测试不要求一次性批量迁移;当某个测试因正常重构频繁破碎,或本次任务正好修改该区域,就顺手收紧到稳定契约。
## 分层口径
1. 用户流程测试验证用户可感知结果。
优先使用 Testing Library 的 `userEvent`、role、label、可见弹窗、URL、提交结果、错误提示和外部 callback。测试名应描述业务行为,不描述组件内部状态机步骤。
2. 稳定契约测试验证对外边界。
对请求 DTO、callback payload、路由变化、持久化边界和后端回包映射,使用关键字段断言或 `expect.objectContaining(...)`。对象仍在演化时,不要断言完整对象、完整数组顺序或所有默认字段。
3. 模型和 hook 测试验证纯逻辑。
复杂状态机优先沉到 model 或 hook 的公开返回契约中测试。hook 测试使用 `renderHook` 直接调用公开方法和读取公开状态,不额外制造 `data-testid` 仪表盘组件来拼接内部字段。
## 避免的写法
- 不为了读取内部状态新建测试专用 DOM,例如 `<span data-testid="active-id">`、`textContent` 拼接内部数组或状态名。
- 不把图标实现、第三方库 class、DOM 层级、完整按钮顺序当成稳定契约,除非产品明确要求该顺序或可访问语义本身就是行为。
- 不用精确长文案锁死可变提示词、分享文案、错误文案或 UI 标签;需要断言时只断言稳定语义片段,或改断言 payload / 状态码 / 目标 callback。
- 不在页面级测试里 mock 出一套与真实页面差异很大的“假壳”,再把假壳内部状态当作用户验收结果。页面壳测试可以保留,但应尽量断言 URL、标题、公开 callback 和真实可见行为。
## 推荐写法
- 交互优先用 `userEvent`,只有测试低层 pointer / wheel / drag 等浏览器事件细节时再用 `fireEvent`。
- 查询优先用 role / label / alt / text 的用户语义;只有无可访问语义的画布参考线、不可见测量节点或渲染边界,才使用 `data-testid`。
- `data-testid` 名称必须描述用户或稳定渲染边界,例如 `image-canvas-editor-snap-guide-vertical`;不要描述 React 私有 state 名称。
- 测试用 fixture 只包含本行为需要的字段。演化中的 payload 使用 `expect.objectContaining(...)` 或 helper 生成默认对象,避免一处契约加字段导致大量无关用例碎裂。
- 当测试是为防止历史回归,应在测试名或邻近注释中说明防的是什么行为,而不是记录实现步骤。
## 试点调整
`src/components/image-editor/useCanvasGenerationDialogs.test.tsx` 已从测试专用 DOM 仪表盘改为 `renderHook`,直接验证 hook 公开契约:打开、归档、激活、更新、删除、恢复和 ID 递增。
`src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx` 已去掉图标 class 和完整按钮顺序快照式断言,保留用户可操作按钮、工具切换 callback、可访问 pressed 状态和 hover / focus 打开选项的契约。
## 验证
修改 React 测试后,优先运行触达文件的定向测试,例如:
```bash
npm run test -- src/components/image-editor/useCanvasGenerationDialogs.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx --reporter=dot
```
涉及共享组件、路由壳或跨页面交互时,再追加对应页面测试、`npm run typecheck`、`npm run check:encoding` 和 `git diff --check`。
@@ -13,7 +13,8 @@
- `granularity` 默认为 `day`。
- `anchor` 使用北京时间日历日期,保留给 `day` / `week` / `month` 兼容旧查询口径。`week` 和 `month` 选择包含该日期的自然周 / 自然月。
- `period` 使用 `startDate` / `endDate` 作为闭区间自定义时段,最多 366 天。
- 前端统一展示起始日期和终止日期两个日期选择器;点击“本日 / 本周 / 本月”会按当前终止日期自动填充对应自然日 / 自然周 / 自然月范围,手动修改起止日期或点击“本时段”后按当前日期范围查询。
- 前端统一展示起始日期和终止日期两个日期选择器;点击“本日 / 本周 / 本月”按当天真实日期自动填充对应自然日 / 自然周 / 自然月范围并刷新,手动修改起止日期后按当前日期范围查询。
- 前端默认日期和“本日 / 本周 / 本月”快捷入口都按 `Asia/Shanghai` 计算,不使用浏览器本地时区。
- 前端每 5 分钟自动刷新一次,同时保留手动刷新按钮。
## 指标口径
@@ -21,7 +22,7 @@
- 生产素材数:`editor_project_resource` 中 `source_type = 'generated'` 的资源,按 `created_at` 映射到北京时间业务日。
- 消耗泥点数:`profile_wallet_ledger` 中 `source_type = asset_operation_consume` 且 `amount_delta < 0` 的流水绝对值,按 `created_at` 映射到北京时间业务日。
- 总注册用户:`profile_dashboard_state` 行数。
- 新增用户数:`profile_dashboard_state` 中 `created_at` 落在当前筛选时间窗内的账号数,按北京时间业务日归属,支持本日 / 本周 / 本月 / 本时段切换。
- 新增用户数:`profile_dashboard_state` 中 `created_at` 落在当前筛选时间窗内的账号数,按北京时间业务日归属,支持本日 / 本周 / 本月快捷日期范围。
- 访问次数:`tracking_daily_stat` 中 `scope_kind = site` 的日聚合次数。它表示站点级成功路由 / 站点级事件,不把用户级钱包、任务、生成等业务操作混入访问次数。
- 访问人数:当前数据只具备登录用户维度,按 `tracking_daily_stat` 中 `scope_kind = user` 的 `scope_id` 去重;匿名访问人数需要未来补充稳定 visitor id 后才能统计。
- 当前使用人数:最近 5 分钟内 `tracking_event` 中有 `user_id` 的登录用户去重;不跟随页面选择的历史日 / 周 / 月。
@@ -35,6 +36,13 @@
- 前端页面:`apps/admin-web/src/pages/AdminDashboardPage.tsx`
- 后台路由:`apps/admin-web/src/app/adminRoutes.ts`
## 埋点后台查询
- `GET /admin/api/tracking/events` 读取 `tracking_event` 原始事实,支持 `eventKey`、`userId`、`scopeKind`、`scopeId`、`startDate`、`endDate` 和 `limit` 筛选。日期使用北京时间日历日期 `YYYY-MM-DD`,后端转换为 `day_key` 闭区间。
- 日常列表查询仍限制最多 1000 条;后台“导出全部”传 `exportAll=true`,上限放宽到 100000 条,仍复用当前筛选条件和日期范围。
- `GET /admin/api/tracking/event-keys` 从已落库的 `tracking_event` 中扫描真实 `event_key` 和出现过的 `scope_kind`,由 api-server 去重后返回,前端只用静态清单补中文标题和备注。
- 任务配置页的 Event Key 候选使用同一个真实 key 列表,并只默认展示出现过 `user` scope 的 key;自定义 key 入口保留给提前配置未写入的新埋点。
## 验证
- `cargo test -p api-server --manifest-path server-rs/Cargo.toml admin`
@@ -38,7 +38,7 @@
队列状态对前端只通过 `api-server` BFF 暴露,不允许前端直接查询 SpacetimeDB private table:
- `GET /api/runtime/external-generation/queue-overview`:当前账号队列概览,用于兼容旧展示和轻量状态读取。返回 pending、running、未确认终态数量和更新时间。
- `GET /api/runtime/external-generation/jobs?limit=20&includeAcknowledgedTerminal=false`:当前账号正式生成任务列表,用于 `我的` 页签任务列表和完成 / 失败提示。返回每个任务的 job id、kind、source、可展示 label、状态、进度、错误、`priceMudPoints`、`refundLedgerId`、`notificationAcknowledgedAt` 和时间戳。默认不返回已确认的终态任务。
- `GET /api/runtime/external-generation/jobs?limit=20&includeAcknowledgedTerminal=false`:当前账号正式生成任务列表,用于 `我的` 页签任务列表和完成 / 失败提示。返回每个任务的 job id、kind、source、可展示 label、状态、进度、错误、`priceMudPoints`、`refundLedgerId`、`notificationAcknowledgedAt` 和时间戳。默认不返回已确认的终态任务;需要拆分活跃和完成列表时可追加 `statuses=running,queued` 或 `statuses=completed,failed`,BFF 仍只返回当前账号任务。
- `POST /api/runtime/external-generation/jobs/acknowledge`:生成完成 / 失败提示展示后由前端后台调用,BFF 只传当前账号 job ids,后端只确认属于当前账号且已终态的任务。
- `GET /api/runtime/external-generation/jobs/{jobId}`:单 job 状态,用于生成页轮询某次动作。返回 `jobId`、`jobKind`、`sourceModule`、`sourceEntityId`、`status`、`attempt`、`maxAttempts`、`createdAt`、`startedAt`、`completedAt`、`updatedAt`、可展示的 `requestLabel`、可展示的 `lastErrorMessage`、以及业务侧下一次轮询所需的 source 标识。