This commit is contained in:
2026-05-01 00:41:33 +08:00
49 changed files with 7540 additions and 157 deletions
@@ -72,7 +72,7 @@
1. 不复用 RPG 的世界、角色、章节、剧情推进结构。
2. 不复用拼图的网格切图、交换、合并块和下一关推荐算法。
3. 不复用大鱼吃小鱼的实时吞噬、实体等级和摇杆移动规则。
4. 不把 Match3D 运行规则写成前端本地真相源。
4. 不把 Match3D 运行规则写成前端本地真相源,但局内即时反馈效果由前端负责呈现
5. 不使用 `server-node` 或 PostgreSQL 作为新增玩法后端。
## 3.3 独立玩法域要求
@@ -110,7 +110,7 @@ Match3D 必须形成独立玩法域,后续技术方案至少需要覆盖:
13. 清空圆形空间中全部物品即胜利。
14. 倒计时结束或备选栏满即失败。
15. 胜利 / 失败后展示结算界面。
16. 点击判定、入槽、消除、失败、胜利必须由后端裁决
16. 点击、入槽、消除、失败、胜利的即时反馈效果由前端先行呈现,后端负责权威确认、状态落库和成绩可信性
---
@@ -128,7 +128,7 @@ Match3D 必须形成独立玩法域,后续技术方案至少需要覆盖:
8. 不做真实物理碰撞结算。
9. 不做必须试玩通关才能发布的门槛。
10. 不把玩法规则说明长文默认写入 UI 面板。
11.前端即时完成规则裁决
11.前端即时反馈当作最终规则真相
12. 不使用 `server-node` 或 PostgreSQL 新增实现。
---
@@ -292,31 +292,36 @@ totalItemCount = clearCount * 3
圆形空间里的物品可以重叠、遮挡、堆叠。
首版使用 2D 逻辑实现遮挡和点击判定
首版使用 2D 逻辑实现遮挡和点击反馈
1. 被完全遮挡的物品不允许点击。
2. 如果物品有局部露出,且露出区域可被点击选中,则允许点击。
3. 具体露出区域判定使用 2D 逻辑的最优方案,不做真实 3D 遮挡
3. 前端基于后端下发的物品层级、位置、半径和可点击快照,执行即时命中检测与选中反馈
4. 后端收到点击意图后做权威确认;如果确认失败,前端必须回滚本次即时反馈。
5. 具体露出区域判定使用 2D 逻辑的最优方案,不做真实 3D 遮挡。
## 8.8 点击入槽
玩家点击通过后,后端裁决该物品可选中
前端播放飞行动画,把物品放入下方备选栏。
玩家点击可见物品后,前端立即播放按压、选中和飞行动画,把物品表现为飞入下方备选栏
飞行动画过程中,物品不再与其他物品产生碰撞。
前端播放即时反馈的同时,必须向后端提交点击意图。后端确认后固化入槽结果;后端拒绝时,前端恢复物品位置和备选栏表现。
## 8.9 备选栏
下方备选栏固定为 `7` 个格子。
1. 每次成功点击后,物品进入备选栏。
2. 备选栏中每出现 `3` 个相同物品 id,自动消除并腾出格子。
3. 如果备选栏满且无法消除,则判定关卡失败
1. 每次点击进入即时反馈流程后,前端先把物品表现为进入备选栏。
2. 备选栏中每出现 `3` 个相同物品 id前端立即播放自动消除效果并腾出格子。
3. 后端确认后固化真实备选栏和消除结果;若后端返回状态不一致,前端必须以最新后端快照校正
4. 如果备选栏满且无法消除,前端可以立即展示失败过渡,最终失败状态以后端确认为准。
## 8.10 胜利
圆形空间内全部物品被消除后,播放胜利动画并展示胜利界面
圆形空间内全部物品被消除后,前端立即播放胜利动画。
正式胜利界面、使用时间和成绩记录以后端确认的运行态为准。
胜利结算页至少展示:
@@ -332,6 +337,8 @@ totalItemCount = clearCount * 3
1. 倒计时结束。
2. 备选栏满。
倒计时归零或备选栏满时,前端立即展示失败过渡;正式失败原因和完成进度以后端确认的运行态为准。
失败结算页至少展示:
1. 失败原因。
@@ -378,33 +385,38 @@ totalItemCount = clearCount * 3
1. 创建玩法草稿。
2. 编译运行时初始局面。
3. 生成物品序列与布局。
4. 判断物品是否可点击
5. 处理点击入槽
6. 判断 `3` 个相同物品 id 消除
7. 判断备选栏满失败
8. 判断倒计时结束失败。
9. 判断清空空间胜利
10. 记录成绩所需的基础数据
4. 下发前端即时反馈所需的物品位置、层级、半径、可点击快照和版本号
5. 权威确认玩家点击意图是否合法
6. 权威确认入槽结果
7. 权威确认 `3` 个相同物品 id 消除
8. 权威确认备选栏满失败。
9. 权威确认倒计时结束失败
10. 权威确认清空空间胜利
11. 记录成绩所需的基础数据。
## 10.2 前端职责
前端负责:
前端负责所有游戏过程中需要即时呈现的反馈效果
1. 展示 Agent 创作界面。
2. 展示结果页和基础编辑表单。
3. 上传参考图片。
4. 展示运行态场景、物品、倒计时和备选栏。
5. 发送玩家点击意图
6. 播放点击、飞入、消除、胜利和失败动画
7. 展示结算界面
5. 基于最新后端快照执行 2D 命中检测、悬停、按压和选中反馈
6. 发送玩家点击意图
7. 在等待后端确认期间,先行播放飞入、入槽、三消、腾格、胜利和失败过渡效果
8. 收到后端确认后,把本地表现校正到权威快照。
9. 展示结算界面。
前端不得自行完成规则裁决
前端可以做即时表现预判,但不得把预判结果作为最终规则真相或成绩来源
## 10.3 防作弊要求
首版即按正式版本搭建规则裁决链路。
首版即按正式版本搭建“前端即时反馈 + 后端权威确认”的链路。
前端不可信任本地点击、消除、胜利或成绩结果;所有关键状态必须后端裁决后下发
前端不可信任本地点击、消除、胜利或成绩结果;所有关键状态必须后端确认后的快照为准
为了保证手感,前端可以先行展示操作反馈;为了防作弊,发布成绩、结算状态、消除计数和运行态持久化必须以后端确认为准。
---
@@ -484,6 +496,8 @@ interface Match3DItemSnapshot {
}
```
`Flying` 可以作为前端表现态使用,不要求后端把飞行动画过程逐帧落库。后端只需要确认物品是否从 `InBoard` 进入 `InTray``Cleared`
## 11.5 备选栏快照
```ts
@@ -672,7 +686,7 @@ Agent 每轮优先追问最影响 demo 生成的一个问题。
13. 倒计时结束或备选栏满后失败。
14. 胜利结算展示使用时间。
15. 失败结算展示完成进度和重新开始按钮。
16. 关键规则由后端裁决,前端不本地判定胜负
16. 局内即时反馈由前端先行呈现,关键状态以后端确认快照校正
17. 相关中文文档通过编码检查。
---
@@ -696,14 +710,15 @@ Agent 每轮优先追问最影响 demo 生成的一个问题。
1. 新增 `module-match3d` 规则。
2. 新增 SpacetimeDB 运行态表和 procedure。
3. 实现开始、点击、消除、失败、胜利。
3. 实现开始、点击确认、消除确认、失败确认、胜利确认
## 阶段 D:前端运行态
1. 展示圆形空间和 2D 物品。
2. 展示 `7` 格备选栏。
3. 接入点击接口和后端快照。
4.飞入、消除、胜负动画
4.点击命中、飞入、入槽、消除、腾格、胜负过渡等即时反馈
5. 补后端确认失败时的前端回滚和快照校正。
## 阶段 E:分发与成绩预留
@@ -715,4 +730,4 @@ Agent 每轮优先追问最影响 demo 生成的一个问题。
## 17. 一句话结论
Match3D 首版不是临时前端 demo,而是以“抓大鹅”模板为外壳、以后端规则裁决为真相源、以独立玩法域为工程边界的单局经典消除玩法链路;首轮先跑通题材创作、结果页、试玩、发布和单局清空胜负闭环。
Match3D 首版不是临时前端 demo,而是以“抓大鹅”模板为外壳、以前端即时反馈保证手感、以后端权威确认保证规则可信、以独立玩法域为工程边界的单局经典消除玩法链路;首轮先跑通题材创作、结果页、试玩、发布和单局清空胜负闭环。
@@ -83,6 +83,7 @@
8. 子面板返回按钮固定摆在面板右上角
9. 设置首页与各级子面板都必须定义单一滚动容器,列表内容必须可稳定滚动,禁止外层与内层同时争夺滚动
10. 二级或三级面板打开后,下层内容必须进入不可交互状态,并把焦点主动转移到当前面板内;禁止对仍保留焦点的祖先节点使用 `aria-hidden`
11. 右上角头像、账号入口等身份入口直达“账号信息”时,只允许展示“账号信息”面板本身,不再同步弹出或保留“设置与账号安全”首页;只有“设置”入口才打开设置首页
---
@@ -0,0 +1,111 @@
# 抓大鹅 Match3D 领域规则与共享契约 Stage1 方案
日期:`2026-04-30`
## 1. 文档目的
本文件承接 [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md),只冻结 B1 + B2 开发范围:
1. 新增 `module-match3d` 纯领域 crate。
2. 新增 Rust shared contracts。
3. 新增 TypeScript shared contracts。
本阶段不实现 SpacetimeDB 表、procedure、`spacetime-client` 调用封装、`api-server` facade 和前端页面。
## 2. Stage1 边界
## 2.1 本阶段做
1. 领域层定义创作配置、作品草稿、作品 profile、运行态快照、物品、托盘、点击确认结果。
2. 领域层提供纯函数:
- 校验创作配置
- 编译默认草稿
- 校验发布字段
- 按确定性 seed 生成初始运行态
- 刷新 2D 可点击快照
- 确认点击、入槽、三消、胜利、托盘满失败
- 确认倒计时失败
3. Rust / TypeScript shared contracts 提供前后端对齐的请求与响应 DTO。
4. 运行态采用“前端即时反馈 + 后端权威确认”契约:
- 前端可先播放点击、飞入、入槽、三消、腾格和胜负过渡。
- 后端确认后返回权威快照。
- 后端拒绝或快照版本不一致时,前端按权威快照回滚或校正。
## 2.2 本阶段不做
1. 不新增 SpacetimeDB 表。
2. 不新增 SpacetimeDB procedure。
3. 不生成新的 SpacetimeDB bindings。
4. 不新增 `api-server` 路由。
5. 不接入平台入口、结果页或运行态 UI。
6. 不接入真实图片生成。
7. 不做排行榜与后续关卡推荐。
## 3. 领域 crate 设计
新增:
```text
server-rs/crates/module-match3d
```
该 crate 是纯领域层,不读写数据库,不访问网络,不依赖浏览器或文件系统。
本阶段虽然不落 SpacetimeDB 表和 procedure,但领域模型已经为后续 SpacetimeDB 接入预留 `spacetime-types` feature。后续在 `spacetime-module` 内使用这些类型时,仍必须遵守 reducer 确定性、`ctx.sender()` 鉴权和表结构迁移约束。
核心类型:
1. `Match3DCreatorConfig`
2. `Match3DResultDraft`
3. `Match3DWorkProfile`
4. `Match3DRunSnapshot`
5. `Match3DItemSnapshot`
6. `Match3DTraySlot`
7. `Match3DClickConfirmation`
核心函数:
1. `build_creator_config`
2. `compile_result_draft`
3. `validate_publish_requirements`
4. `create_work_profile`
5. `publish_work_profile`
6. `start_run_with_seed_at`
7. `confirm_click_at`
8. `resolve_run_timer_at`
## 4. 即时反馈与权威确认
本阶段将点击处理明确拆成两层:
1. 前端即时反馈层
- 读取后端快照中的 `boardVersion`、物品位置、层级、半径和 `clickable`
- 本地做命中检测和动画。
- 立即表现飞入、入槽、三消和胜负过渡。
2. 后端权威确认层
- 校验 `runId``itemInstanceId`、运行态状态和物品是否仍可点击。
- 重新计算入槽、三消、托盘满失败和胜利。
- 返回最新 `Match3DRunSnapshot`
-`boardVersion` 帮前端识别是否需要校正。
`Flying` 只作为前端表现态,不要求后端逐帧落库。后端只确认物品是否已从 `InBoard` 进入 `InTray``Cleared`
运行态领域内部使用 `board_version` 表示权威快照版本;HTTP 与 TypeScript shared contracts 对外使用 `snapshotVersion` / `clientSnapshotVersion`,由后续 `api-server` facade 做字段映射。
## 5. 生成规则 Stage1 口径
1. `clearCount` 必须是正整数。
2. `totalItemCount = clearCount * 3`
3. 难度范围为 `1~10`
4. 首版内置 `10` 种 demo 视觉 key。
5.`clearCount > 10` 时,复用视觉 key,并保证每种物品数量仍为 `3` 的倍数。
6. 初始布局使用确定性 seed 生成圆形空间内的 2D 坐标。
7. 可点击判定只做 2D 近似:若物品被更高层物品完全覆盖,则不可点击;否则可点击。
## 6. 验收
1. `cargo test -p module-match3d` 通过。
2. `cargo test -p shared-contracts match3d` 通过。
3. `npm run check:encoding` 覆盖新增中文文档和新增源码。
4. 本阶段不要求运行 `npm run api-server:maincloud`,因为未修改后端运行服务入口、SpacetimeDB 表或 `api-server` facade。
@@ -0,0 +1,91 @@
# 抓大鹅 Match3D F1 创作入口与 Agent UI 落地记录 2026-04-30
## 1. 阶段边界
本文件承接《MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md》的 F1 包。
F1 只处理前端创作入口、Agent 工作区和等待后端 B5 facade 前的 mock client。它不实现运行态规则,不修改 SpacetimeDB 表,不接 `api-server` 路由。
## 2. 本阶段写入范围
1. `src/components/platform-entry/`
2. `src/components/match3d-creation/`
3. `src/services/match3d-creation/`
4. `packages/shared/src/contracts/match3dAgent.ts`
其中 `packages/shared/src/contracts/match3dAgent.ts` 作为 F1 与后续 B5 的 DTO 对齐点,F1 mock client 不自建脱离共享契约的临时类型。
## 3. 入口接入
平台入口新增可见创作类型:
```text
id: match3d
title: 抓大鹅
subtitle: 经典消除玩法
badge: 可创建
```
入口来源统一走 `getVisiblePlatformCreationTypes()`,因此创作首页首屏卡带与“选择创作类型”弹层会同时出现抓大鹅。
## 4. Agent 工作区
新增 `Match3DAgentWorkspace`,复用通用 `CreationAgentWorkspace`
Agent 只收集三类锚点:
1. 题材主题。
2. 需要消除次数。
3. 难度。
工作区支持参考图片上传入口。图片在 F1 中先以 Data URL 形式随消息 payload 带给 mock clientB5 接入后由后端 facade 替换为正式资产上传与引用。
UI 中不默认展示玩法规则长文,只展示进度、锚点、聊天内容和必要按钮。
## 5. mock client
新增 `src/services/match3d-creation/match3dCreationClient.ts`
mock client 提供:
1. `createMatch3DCreationSession`
2. `getMatch3DCreationSession`
3. `streamMatch3DCreationMessage`
4. `executeMatch3DCreationAction`
mock 行为:
1. 创建本地会话。
2. 从中文输入中提取题材、消除次数和难度。
3. 支持“自动配置”。
4. 当三项配置完整时允许执行 `match3d_compile_draft`
5. 编译后返回 `draft_ready` 会话和草稿。
## 6. 结果承接
F1 新增 `Match3DDraftReadyView` 作为草稿生成后的临时承接页,只展示草稿基础信息并允许返回 Agent 修改。
正式结果页的基础信息编辑、封面图、试玩、发布由 F2 接入,F1 不在这里模拟发布。
## 7. 后续替换点
B5 完成后,只需要把 `match3dCreationClient` 的本地 Map mock 替换为 HTTP/SSE facade
```text
POST /api/creation/match3d/sessions
GET /api/creation/match3d/sessions/:sessionId
POST /api/creation/match3d/sessions/:sessionId/messages/stream
POST /api/creation/match3d/sessions/:sessionId/compile
```
`PlatformEntryFlowShellImpl``Match3DAgentWorkspace` 不应再改一轮业务字段。
## 8. 验收口径
1. 创作首页能看到“抓大鹅 / 经典消除玩法”。
2. 弹层选择“抓大鹅”能进入 Agent 工作区。
3. 输入题材、消除次数、难度后进度到 `100%`
4. 点击“生成结果页”进入草稿承接页。
5. 可从草稿承接页返回 Agent 修改。
6. `npm run check:encoding` 通过。
7. `npm run typecheck` 通过。
@@ -0,0 +1,394 @@
# 抓大鹅 Match3D F2 结果页与发布技术方案
日期:`2026-04-30`
## 1. 文档目的
本文件承接 [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md),只冻结 F2 开发范围:
1. Match3D 待发布结果页。
2. 作品基础信息编辑。
3. 发布前试玩入口。
4. 发布入口。
5. 已发布作品二次编辑恢复口径。
本阶段不实现运行态即时反馈 UI,不实现 SpacetimeDB 表与 procedure,不实现 `api-server` facade。F2 可以先基于 shared contracts 与 mock client 开发,等待 B4+B5 接入真实 HTTP。
---
## 2. 前置依赖
F2 依赖以下已冻结文档:
1. PRD[AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](../prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md)
2. A0[MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md)
3. B1+B2[MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md](./MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md)
F2 可在 B4+B5 之前并行开发,但必须遵守 B2 的 TypeScript contract,不得在前端私自扩字段。
---
## 3. 本阶段做
1. 新增 Match3D 结果页组件目录。
2. 新增 Match3D works service 目录。
3. 展示草稿配置摘要:
- 题材主题
- 需要消除次数
- 难度
- 参考图片预览
4. 支持编辑发布基础信息:
- 游戏名称
- 标签
- 封面图
5. 支持发布前试玩入口。
6. 支持试玩中止后回到结果页继续编辑。
7. 支持发布入口。
8. 支持已发布作品二次编辑的前端恢复路径。
---
## 4. 本阶段不做
1. 不生成题材物品素材。
2. 不生成额外封面图;封面图只接收已有图片、上传图片或后端已有占位结果。
3. 不要求试玩通关后才能发布。
4. 不实现运行态点击、飞入、三消等即时反馈。
5. 不实现首页、分类页和广场投影。
6. 不实现排行榜。
7. 不在 UI 中默认展示玩法规则说明长文。
8. 不把发布校验只写在前端;前端只做即时提示,后端 publish gate 是最终门槛。
---
## 5. 文件落点
## 5.1 前端组件
新增:
```text
src/components/match3d-result/
```
建议文件:
```text
src/components/match3d-result/Match3DResultView.tsx
src/components/match3d-result/Match3DResultView.test.tsx
src/components/match3d-result/index.ts
```
如组件变大,可后续拆分:
```text
Match3DResultHeader.tsx
Match3DResultBasicsForm.tsx
Match3DResultConfigPreview.tsx
Match3DResultPublishPanel.tsx
```
首版不要过早拆太多文件,优先保持可读和低冲突。
## 5.2 前端 service
新增:
```text
src/services/match3d-works/
```
建议文件:
```text
src/services/match3d-works/match3dWorksClient.ts
src/services/match3d-works/index.ts
```
F2 只负责 works 维度:
1. 读取作品详情。
2. 更新作品基础信息。
3. 发布作品。
4. 删除作品可后置,若 F4 需要再补。
运行态启动接口归 `src/services/match3d-runtime/`F2 只调用上层传入的 `onStartTestRun`
---
## 6. shared contracts 使用
F2 只消费 B2 已冻结的 TypeScript contract
```text
packages/shared/src/contracts/match3dWorks.ts
packages/shared/src/contracts/match3dAgent.ts
packages/shared/src/contracts/match3dRuntime.ts
```
必要类型:
1. `Match3DWorkProfile`
2. `Match3DWorkSummary`
3. `Match3DWorkUpdateRequest`
4. `Match3DPublishRequest`
5. `Match3DPublishResult`
6. `Match3DCompileDraftResult`
7. `Match3DCreatorConfig`
F2 不新增独立的前端私有数据结构来表达作品真相;只允许使用局部表单状态承载未保存输入。
---
## 7. 结果页 props contract
建议 `Match3DResultView` props
```ts
type Match3DResultViewProps = {
profile: Match3DWorkProfile;
draft?: Match3DCompileDraftResult | null;
isBusy?: boolean;
error?: string | null;
onBack: () => void;
onStartTestRun: (profile: Match3DWorkProfile) => void;
onPublish: (payload: Match3DPublishRequest) => void;
onSaved?: (profile: Match3DWorkProfile) => void;
};
```
说明:
1. `profile` 是结果页当前作品真相源。
2. `draft` 只用于展示草稿生成附加信息;不能覆盖 `profile` 的发布字段。
3. `onStartTestRun` 进入 F3/B5 运行态链路。
4. `onPublish` 可以先由 mock client 实现,B5 完成后替换为真实 HTTP。
5. `onSaved` 用于把自动保存后的 profile 回写给上层流程控制器。
---
## 8. 页面内容顺序
结果页保持单列表,不做多 Tab。
固定顺序:
1. 顶部返回与保存状态。
2. 封面图。
3. 游戏名称。
4. 标签。
5. 题材主题。
6. 需要消除次数。
7. 难度。
8. 参考图片预览。
9. 试玩按钮。
10. 发布按钮。
UI 只呈现必要信息,不在页面中展示玩法规则说明长文。
---
## 9. 字段编辑规则
## 9.1 游戏名称
1. 必填。
2. 首版建议前端限制 `1~30` 个中文字符等价长度。
3. 默认值来自 Agent 确认题材或系统生成草稿。
## 9.2 标签
1. 必填。
2. 首版建议 `3~6` 个标签,与拼图发布门槛保持一致。
3. 输入支持中文逗号、英文逗号、顿号、换行拆分。
4. 前端需要去重和去空格。
## 9.3 封面图
1. 必填。
2. F2 可先复用参考图片、占位封面或用户上传图。
3. 图片真实存储由现有资产链或后续 B5 facade 处理。
4. 前端不得把本地临时 blob URL 当作已发布封面真相。
## 9.4 题材主题、需要消除次数、难度
首版结果页允许展示并可编辑这些配置。
修改后必须同步保存到作品 profile:
1. `themeText`
2. `clearCount`
3. `difficulty`
注意:
1. `clearCount` 必须为正整数。
2. `difficulty` 必须在 `1~10`
3. 修改配置后,下一次试玩必须基于最新保存配置启动。
---
## 10. 自动保存
F2 建议实现自动保存,口径参考拼图结果页:
1. 输入变更后 `600ms` debounce。
2. 只保存结果页可编辑字段。
3. 保存中展示轻量状态。
4. 保存失败展示轻量错误,不弹长说明。
5. 发布前必须等待最后一次保存完成,或发布 payload 直接携带当前表单字段。
建议状态:
```ts
type Match3DAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
```
---
## 11. 发布门槛
前端即时 blocker
1. 游戏名称为空。
2. 标签数量不在 `3~6`
3. 封面图为空。
4. `clearCount` 不是正整数。
5. `difficulty` 不在 `1~10`
后端 publish gate 是最终门槛,前端不得绕过。
发布不要求试玩通关。
---
## 12. 试玩入口
结果页提供“试玩”入口。
行为:
1. 点击试玩前先保存当前表单。
2. 保存成功后调用 `onStartTestRun(profile)`
3. 上层进入 Match3D 运行态。
4. 运行态停止或返回后,回到同一个结果页继续编辑。
F2 不实现运行态本身;只冻结结果页如何发起试玩。
---
## 13. 发布接口
F2 service 建议接口:
```ts
const MATCH3D_WORKS_API_BASE = '/api/creation/match3d/works';
export async function getMatch3DWorkDetail(profileId: string): Promise<Match3DWorkDetailResponse>;
export async function updateMatch3DWork(
profileId: string,
payload: Match3DWorkUpdateRequest,
): Promise<Match3DWorkMutationResponse>;
export async function publishMatch3DWork(
profileId: string,
payload: Match3DPublishRequest,
): Promise<Match3DPublishResult>;
```
后续 B5 必须提供同名 HTTP facade 或在 service 层做最小适配。
---
## 14. Mock client 口径
F2 可以在真实 B5 接口完成前使用 mock client。
要求:
1. mock 数据必须来自 shared contracts。
2. mock profile 字段必须覆盖发布必填项。
3. mock publish 只能返回“可发布成功”的本地结果,不得伪造平台广场投影。
4. B5 接入后,mock 只能保留为测试 fixture。
---
## 15. 已发布作品二次编辑
进入自己已发布 Match3D 作品时,结果页应支持二次编辑。
规则:
1. 优先通过 `sourceSessionId` 恢复原创作 session。
2. 如果没有 session,则通过 `profileId` 读取作品详情进入结果页。
3. 二次发布不得创建新作品,必须覆盖同一 `profileId`
4. 不清零 `playCount`
5. 不改变作品归属。
---
## 16. 与其它分支的接口边界
## 16.1 依赖 F1
F1 负责创建会话和 Agent UI。F2 接收 F1 编译出的 `profile / draft`,不重复实现 Agent 对话。
## 16.2 依赖 F3
F3 负责运行态 UI。F2 只提供 `onStartTestRun` 入口。
## 16.3 依赖 B5
B5 负责真实 HTTP facade。F2 的 service path 和 DTO 必须按本文冻结,避免后续替换 mock 时改组件结构。
## 16.4 依赖 F4
F4 负责首页、分类页和广场分发。F2 发布成功后只需要把返回 profile 交给上层;不直接刷新广场列表。
---
## 17. 测试要求
建议新增:
```text
src/components/match3d-result/Match3DResultView.test.tsx
```
覆盖:
1. 展示游戏名称、标签、封面图、题材、需要消除次数和难度。
2. 游戏名称为空时发布按钮阻断。
3. 标签数量不足时发布按钮阻断。
4. `clearCount` 非正整数时发布按钮阻断。
5. `difficulty` 超出 `1~10` 时发布按钮阻断。
6. 点击试玩前触发保存。
7. 发布不要求试玩通关。
service 测试可在 B5 接入后补齐。
---
## 18. 验收命令
F2 文档分支:
```powershell
npm run check:encoding -- docs/technical/MATCH3D_F2_RESULT_AND_PUBLISH_2026-04-30.md docs/technical/README.md
```
F2 前端实现分支:
```powershell
npm run check:encoding
npm run typecheck
```
如新增组件测试,补跑对应 `vitest`
---
## 19. 一句话结论
F2 只负责把 Match3D 草稿变成可编辑、可试玩、可发布的作品工作台;它必须复用平台结果页和发布体验,发布不要求试玩通关,并为 B5 真实后端接口与 F3 运行态试玩入口保留清晰边界。
+4
View File
@@ -7,6 +7,10 @@
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
- [SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md](./SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md):记录本地 standalone 启动时报 `mismatched database identity` 的 root-dir/replica 数据残留根因、备份重建步骤和脚本诊断口径。
- [LLM_MODEL_ROUTING_RPG_AND_CREATION_2026-04-30.md](./LLM_MODEL_ROUTING_RPG_AND_CREATION_2026-04-30.md):冻结 RPG 运行时剧情推理使用 `doubao-seed-character-251128``/chat/completions`,以及所有模板创作大模型推理使用 `deepseek-v3-2-251201``/responses`
- [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md):冻结抓大鹅 Match3D 首版 demo 的独立玩法域、表与 procedure、HTTP facade、前端即时反馈/后端权威确认协议,以及可并行开发包。
- [MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md](./MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md):冻结抓大鹅 Match3D B1+B2 的纯领域规则 crate、Rust/TypeScript shared contracts,以及 Stage1 不触碰 SpacetimeDB 表和 api-server 的边界。
- [MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md](./MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md):记录抓大鹅 F1 创作入口、Agent 工作区、参考图入口、本地 mock client 与后续 B5 HTTP facade 替换点。
- [MATCH3D_F2_RESULT_AND_PUBLISH_2026-04-30.md](./MATCH3D_F2_RESULT_AND_PUBLISH_2026-04-30.md):冻结抓大鹅 F2 结果页、基础信息编辑、发布前试玩入口、发布门槛、自动保存和已发布作品二次编辑恢复口径。
- [PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md](./PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md):记录平台首页底部 dock 在手机浏览器地址栏展开时脱离可见区域的根因,以及 `100dvh`、固定底部锚点和安全区占位的修复口径。
- [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md):记录 SpacetimeDB private 表迁移 JSON 导出/导入 procedure、迁移操作员授权、HTTP 413 分片导入、Jenkins 自动迁移回灌和导入脚本参数。
- [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md):记录 `Genarrative-Database-Export` / `Genarrative-Database-Import` 两条 SCM-backed 数据库迁移流水线参数、默认 dry-run、token 边界和 `CHUNK_SIZE` 413 规避参数。
@@ -142,7 +142,7 @@ npm run deploy:rust:remote
5. 执行 `cargo build -p spacetime-module --release --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml`,并把 `spacetime_module.wasm` 复制到目标目录。
6. 把仓库根目录的 `.env``.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下;复制后统一移除 UTF-8 BOM 与 CRLF,并把 `GENARRATIVE_SPACETIME_DATABASE` 覆盖为本次 `--database` 参数,避免 Jenkins 工作区里残留的旧 `.env.local` 覆盖发布包目标库。
7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` 并把 `/api/*``/generated-*``/healthz` 反代到本包内的 `api-server`
8. 在目标目录写入 `start.sh``stop.sh``start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env``.env.local`,兼容 UTF-8 BOM 与 CRLF,再回退到构建时通过 `--database``--api-port``--web-port``--spacetime-host``--spacetime-port` 写入的默认值,并默认导出 `NO_COLOR=1``CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir,不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli``$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr <host>:<port>`,探活必须确认 `server ping` 输出包含 `Server is online:`;普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌;如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不触发自动回灌。
8. 在目标目录写入 `start.sh``stop.sh``start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env``.env.local`,兼容 UTF-8 BOM 与 CRLF,再回退到构建时通过 `--database``--api-port``--web-host``--web-port``--spacetime-host``--spacetime-port` 写入的默认值,其中 Web 默认只监听 `127.0.0.1`并默认导出 `NO_COLOR=1``CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir,不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli``$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr <host>:<port>`,探活必须确认 `server ping` 输出包含 `Server is online:`;普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌;如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不触发自动回灌。
9. 默认执行 `scp -r -i ~\.ssh\dsk.pem build/<timestamp> ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。
SpacetimeDB database 名称必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$`:只能使用小写字母、数字,并用单个短横线分隔;大写字母、点号、下划线、首尾短横线和连续短横线都会触发 `spacetime publish``invalid characters in database name`。发布包构建脚本和 `start.sh` 都会提前拦截这类非法名称。
@@ -174,6 +174,7 @@ build/<timestamp>/
```bash
npm run build:rust:ubuntu -- --name 20260422-153000
npm run build:rust:ubuntu -- --database genarrative-dev --web-port 3000 --api-port 8082 --spacetime-port 3101
npm run build:rust:ubuntu -- --database genarrative-dev --web-host 127.0.0.1 --web-port 3000
npm run build:rust:ubuntu -- --skip-upload
```
@@ -27,6 +27,7 @@ spacetime sql <db> "SELECT * FROM custom_world_gallery_entry"
| RPG 运行时 | `story_session`, `story_event`, `npc_state`, `inventory_slot`, `battle_state`, `treasure_record`, `quest_record`, `quest_log`, `player_progression`, `chapter_progression` |
| 世界创作 | `custom_world_profile`, `custom_world_session`, `custom_world_agent_session`, `custom_world_agent_message`, `custom_world_agent_operation`, `custom_world_draft_card`, `custom_world_gallery_entry` |
| 拼图 | `puzzle_agent_session`, `puzzle_agent_message`, `puzzle_work_profile`, `puzzle_runtime_run` |
| 抓大鹅 Match3D | `match3d_agent_session`, `match3d_agent_message`, `match3d_work_profile`, `match3d_runtime_run` |
| 大鱼吃小鱼 | `big_fish_creation_session`, `big_fish_agent_message`, `big_fish_asset_slot`, `big_fish_runtime_run` |
| 资产 | `asset_object`, `asset_entity_binding` |
| AI 任务 | `ai_task`, `ai_task_stage`, `ai_text_chunk`, `ai_result_reference` |
@@ -449,6 +450,53 @@ SELECT * FROM puzzle_runtime_run WHERE run_id = '<run_id>';
SELECT * FROM puzzle_runtime_run WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
```
## 抓大鹅 Match3D 表
### `match3d_agent_session`
- 作用:抓大鹅 Match3D 创作 Agent 会话表,保存种子、配置 JSON、草稿 JSON 和发布 profile 指针。
- 结构:`session_id PK: String`, `owner_user_id: String`, `seed_text: String`, `current_turn: u32`, `progress_percent: u32`, `stage: String`, `config_json: String`, `draft_json: String`, `last_assistant_reply: String`, `published_profile_id: String`, `created_at: Timestamp`, `updated_at: Timestamp`
- 索引:`owner_user_id`
```sql
SELECT * FROM match3d_agent_session WHERE session_id = '<session_id>';
SELECT * FROM match3d_agent_session WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
```
### `match3d_agent_message`
- 作用:抓大鹅 Match3D 创作 Agent 消息流水。
- 结构:`message_id PK: String`, `session_id: String`, `role: String`, `kind: String`, `text: String`, `created_at: Timestamp`
- 索引:`session_id`
```sql
SELECT * FROM match3d_agent_message WHERE session_id = '<session_id>' ORDER BY created_at ASC;
```
### `match3d_work_profile`
- 作用:抓大鹅 Match3D 作品主表,保存作品基础信息、配置、发布状态和游玩次数。
- 结构:`profile_id PK: String`, `owner_user_id: String`, `source_session_id: String`, `author_display_name: String`, `game_name: String`, `theme_text: String`, `summary_text: String`, `tags_json: String`, `cover_image_src: String`, `cover_asset_id: String`, `clear_count: u32`, `difficulty: u32`, `config_json: String`, `publication_status: String`, `play_count: u32`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`
- 索引:`owner_user_id`, `publication_status`
```sql
SELECT * FROM match3d_work_profile WHERE profile_id = '<profile_id>';
SELECT * FROM match3d_work_profile WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
SELECT * FROM match3d_work_profile WHERE publication_status = 'Published';
```
### `match3d_runtime_run`
- 作用:抓大鹅 Match3D 单局运行态表,保存权威快照、快照版本、胜负状态和成绩基础字段。
- 结构:`run_id PK: String`, `owner_user_id: String`, `profile_id: String`, `status: String`, `snapshot_version: u32`, `started_at_ms: i64`, `duration_limit_ms: i64`, `finished_at_ms: i64`, `elapsed_ms: i64`, `clear_count: u32`, `total_item_count: u32`, `cleared_item_count: u32`, `failure_reason: String`, `snapshot_json: String`, `created_at: Timestamp`, `updated_at: Timestamp`
- 索引:`owner_user_id`, `profile_id`
```sql
SELECT * FROM match3d_runtime_run WHERE run_id = '<run_id>';
SELECT * FROM match3d_runtime_run WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
SELECT * FROM match3d_runtime_run WHERE profile_id = '<profile_id>';
```
## 大鱼吃小鱼表
### `big_fish_creation_session`
@@ -0,0 +1,126 @@
/**
* 抓大鹅 Match3D 创作 Agent 共享契约。
* 字段按 HTTP facade 的 camelCase DTO 命名,后端领域层 snake_case 字段由 facade 映射。
*/
export type Match3DCreationStage =
| 'collecting'
| 'collecting_config'
| 'ready_to_compile'
| 'draft_ready'
| 'draft_compiled'
| 'ready_to_publish'
| 'published'
| string;
export type Match3DAgentMessageRole = 'user' | 'assistant' | 'system' | string;
export type Match3DAgentMessageKind =
| 'chat'
| 'summary'
| 'action_result'
| 'warning'
| string;
export type Match3DAnchorStatus = 'confirmed' | 'missing' | 'inferred' | 'locked' | string;
export interface CreateMatch3DAgentSessionRequest {
seedText?: string;
themeText?: string;
referenceImageSrc?: string | null;
clearCount?: number;
difficulty?: number;
}
export type CreateMatch3DSessionRequest = CreateMatch3DAgentSessionRequest;
export interface SendMatch3DAgentMessageRequest {
clientMessageId: string;
text: string;
quickFillRequested?: boolean;
referenceImageSrc?: string | null;
}
export type SendMatch3DMessageRequest = SendMatch3DAgentMessageRequest;
export interface ExecuteMatch3DAgentActionRequest {
action: string;
gameName?: string;
summary?: string;
tags?: string[];
coverImageSrc?: string | null;
clearCount?: number;
difficulty?: number;
}
export type ExecuteMatch3DActionRequest = ExecuteMatch3DAgentActionRequest;
export interface Match3DAnchorItemResponse {
key: string;
label: string;
value: string;
status: Match3DAnchorStatus;
}
export interface Match3DAnchorPackResponse {
theme: Match3DAnchorItemResponse;
clearCount: Match3DAnchorItemResponse;
difficulty: Match3DAnchorItemResponse;
}
export interface Match3DCreatorConfig {
themeText: string;
referenceImageSrc?: string | null;
clearCount: number;
difficulty: number;
}
export interface Match3DResultDraft {
gameName: string;
themeText: string;
summaryText?: string;
summary?: string;
tags: string[];
coverImageSrc?: string | null;
referenceImageSrc?: string | null;
clearCount: number;
difficulty: number;
totalItemCount?: number;
publishReady?: boolean;
blockers?: string[];
}
export interface Match3DAgentMessage {
id: string;
role: Match3DAgentMessageRole;
kind: Match3DAgentMessageKind;
text: string;
createdAt: string;
}
export type Match3DAgentMessageResponse = Match3DAgentMessage;
export interface Match3DAgentSessionSnapshot {
sessionId: string;
currentTurn: number;
progressPercent: number;
stage: Match3DCreationStage;
anchorPack: Match3DAnchorPackResponse;
config?: Match3DCreatorConfig | null;
draft?: Match3DResultDraft | null;
messages: Match3DAgentMessage[];
lastAssistantReply?: string | null;
publishedProfileId?: string | null;
updatedAt: string;
}
export interface Match3DAgentSessionResponse {
session: Match3DAgentSessionSnapshot;
}
export type Match3DSessionResponse = Match3DAgentSessionResponse;
export interface Match3DAgentActionResponse {
session: Match3DAgentSessionSnapshot;
}
export type Match3DActionResponse = Match3DAgentActionResponse;
@@ -0,0 +1,129 @@
/**
* 抓大鹅 Match3D 运行态共享契约。
* 前端可以使用 Flying 做即时表现;后端权威快照只应返回 InBoard、InTray、Cleared。
*/
export type Match3DRunStatus =
| 'running'
| 'won'
| 'failed'
| 'stopped'
| 'Running'
| 'Won'
| 'Failed'
| 'Stopped'
| string;
export type Match3DItemState =
| 'in_board'
| 'in_tray'
| 'cleared'
| 'InBoard'
| 'Flying'
| 'InTray'
| 'Cleared'
| string;
export type Match3DFailureReason =
| 'time_up'
| 'tray_full'
| 'TimeUp'
| 'TrayFull'
| string;
export type Match3DClickRejectReason =
| 'run_not_active'
| 'snapshot_version_mismatch'
| 'item_not_found'
| 'item_not_in_board'
| 'item_not_clickable'
| 'tray_full'
| string;
export type Match3DClickConfirmStatus =
| 'Accepted'
| 'RejectedNotClickable'
| 'RejectedAlreadyMoved'
| 'RejectedTrayFull'
| 'VersionConflict'
| 'RunFinished';
export interface StartMatch3DRunRequest {
profileId: string;
}
export interface Match3DClickItemRequest {
runId?: string;
itemInstanceId: string;
clientActionId?: string;
snapshotVersion?: number;
clientSnapshotVersion: number;
clientEventId: string;
clickedAtMs: number;
}
export type ClickMatch3DItemRequest = Match3DClickItemRequest;
export interface StopMatch3DRunRequest {
clientActionId: string;
}
export interface Match3DItemSnapshot {
itemInstanceId: string;
itemTypeId: string;
visualKey: string;
x: number;
y: number;
radius: number;
layer: number;
state: Match3DItemState;
clickable: boolean;
traySlotIndex?: number | null;
}
export interface Match3DTraySlot {
slotIndex: number;
itemInstanceId?: string | null;
itemTypeId?: string | null;
visualKey?: string | null;
}
export interface Match3DRunSnapshot {
runId: string;
profileId: string;
ownerUserId?: string;
status: Match3DRunStatus;
snapshotVersion: number;
startedAtMs: number;
durationLimitMs: number;
serverNowMs?: number;
remainingMs: number;
clearCount: number;
totalItemCount: number;
clearedItemCount: number;
boardVersion?: number;
items: Match3DItemSnapshot[];
traySlots: Match3DTraySlot[];
failureReason?: Match3DFailureReason | null;
lastConfirmedActionId?: string | null;
}
export interface Match3DClickConfirmation {
accepted: boolean;
rejectReason?: Match3DClickRejectReason | null;
enteredSlotIndex?: number | null;
clearedItemInstanceIds: string[];
run: Match3DRunSnapshot;
}
export interface Match3DClickItemResult {
status: Match3DClickConfirmStatus;
run: Match3DRunSnapshot;
acceptedItemInstanceId?: string;
clearedItemInstanceIds: string[];
failureReason?: Match3DFailureReason | null;
}
export interface Match3DRunResponse {
run: Match3DRunSnapshot;
}
export interface Match3DClickResponse {
confirmation: Match3DClickConfirmation;
}
@@ -0,0 +1,49 @@
/**
* 抓大鹅 Match3D 作品读写共享契约。
* 首版作品发布必须补齐游戏名称、标签、封面、题材、消除次数和难度。
*/
export type Match3DWorkPublicationStatus = 'draft' | 'published' | string;
export interface PutMatch3DWorkRequest {
gameName: string;
summary: string;
tags: string[];
coverImageSrc?: string | null;
referenceImageSrc?: string | null;
clearCount: number;
difficulty: number;
}
export interface Match3DWorkSummary {
workId: string;
profileId: string;
ownerUserId: string;
sourceSessionId?: string | null;
gameName: string;
themeText: string;
summary: string;
tags: string[];
coverImageSrc?: string | null;
referenceImageSrc?: string | null;
clearCount: number;
difficulty: number;
publicationStatus: Match3DWorkPublicationStatus;
playCount: number;
updatedAt: string;
publishedAt?: string | null;
publishReady: boolean;
}
export interface Match3DWorkProfile extends Match3DWorkSummary {}
export interface Match3DWorksResponse {
items: Match3DWorkSummary[];
}
export interface Match3DWorkDetailResponse {
item: Match3DWorkProfile;
}
export interface Match3DWorkMutationResponse {
item: Match3DWorkProfile;
}
+9 -6
View File
@@ -3,6 +3,15 @@ export * from './contracts/auth';
export type * from './contracts/bigFish';
export * from './contracts/common';
export type * from './contracts/customWorldAgent';
export * from './contracts/match3dAgent';
export * from './contracts/match3dRuntime';
export * from './contracts/match3dWorks';
export * from './contracts/puzzleAgentActions';
export * from './contracts/puzzleAgentDraft';
export * from './contracts/puzzleAgentSession';
export * from './contracts/puzzleResultPreview';
export * from './contracts/puzzleRuntimeSession';
export * from './contracts/puzzleWorkSummary';
export * from './contracts/rpgAgentActions';
export * from './contracts/rpgAgentAnchors';
export * from './contracts/rpgAgentDraft';
@@ -11,12 +20,6 @@ export * from './contracts/rpgCreationFixtures';
export * from './contracts/rpgCreationPreview';
export * from './contracts/rpgCreationResultView';
export * from './contracts/rpgCreationWorkSummary';
export * from './contracts/puzzleAgentActions';
export * from './contracts/puzzleAgentDraft';
export * from './contracts/puzzleAgentSession';
export * from './contracts/puzzleResultPreview';
export * from './contracts/puzzleRuntimeSession';
export * from './contracts/puzzleWorkSummary';
export * from './contracts/rpgRuntimeChat';
export * from './contracts/rpgRuntimeQuestAssist';
export * from './contracts/rpgRuntimeStoryAction';
+3 -3
View File
@@ -191,7 +191,7 @@ BUILD_NAME="$(date +%Y%m%d-%H%M%S)"
DATABASE="xushi-p4wfr"
API_HOST="127.0.0.1"
API_PORT="8082"
WEB_HOST="0.0.0.0"
WEB_HOST="127.0.0.1"
WEB_PORT="25001"
SPACETIME_HOST="127.0.0.1"
SPACETIME_PORT="3101"
@@ -421,7 +421,7 @@ import {fileURLToPath} from 'node:url';
const releaseDir = path.dirname(fileURLToPath(import.meta.url));
const webRoot = path.join(releaseDir, 'web');
const webHost = process.env.GENARRATIVE_WEB_HOST || '0.0.0.0';
const webHost = process.env.GENARRATIVE_WEB_HOST || '127.0.0.1';
const webPort = Number(process.env.GENARRATIVE_WEB_PORT || '3000');
const apiTarget = new URL(process.env.GENARRATIVE_API_TARGET || 'http://127.0.0.1:8082');
const indexPath = path.join(webRoot, 'index.html');
@@ -1215,7 +1215,7 @@ cat >"${TARGET_DIR}/README.md" <<'EOF'
- 启动时会先加载发布目录根部的 \`.env\` 与 \`.env.local\`,再回退到脚本内默认值。
- 环境文件复制进发布包时会移除 UTF-8 BOM 与 CRLF;启动时也会按 \`KEY=value\` 子集解析,跳过不合法行。
- 脚本内默认值来自构建时的 `--database`、`--api-port`、`--web-port`、`--spacetime-host`、`--spacetime-port` 参数。
- 脚本内默认值来自构建时的 `--database`、`--api-port`、`--web-host`、`--web-port`、`--spacetime-host`、`--spacetime-port` 参数Web 默认只监听 `127.0.0.1`
- 默认导出 \`NO_COLOR=1\` 与 \`CARGO_TERM_COLOR=never\`,避免 ANSI 颜色控制码写入日志文件;如确有需要可在启动前显式覆盖。
- \`GENARRATIVE_WEB_HOST\` / \`GENARRATIVE_WEB_PORT\`
- \`GENARRATIVE_API_HOST\` / \`GENARRATIVE_API_PORT\` / \`GENARRATIVE_API_LOG\`
+10
View File
@@ -1562,6 +1562,15 @@ dependencies = [
"spacetimedb",
]
[[package]]
name = "module-match3d"
version = "0.1.0"
dependencies = [
"serde",
"shared-kernel",
"spacetimedb",
]
[[package]]
name = "module-npc"
version = "0.1.0"
@@ -2682,6 +2691,7 @@ dependencies = [
"module-combat",
"module-custom-world",
"module-inventory",
"module-match3d",
"module-npc",
"module-progression",
"module-puzzle",
+2 -1
View File
@@ -15,6 +15,7 @@ members = [
"crates/module-combat",
"crates/module-inventory",
"crates/module-custom-world",
"crates/module-match3d",
"crates/module-npc",
"crates/module-puzzle",
"crates/module-progression",
@@ -52,4 +53,4 @@ incremental = true
[profile.release]
opt-level = 3 # 最大优化等级
lto = "thin" # 启用 Thin LTO,平衡编译时间和性能
codegen-units = 1 # 减少并行代码生成单元,提升优化但增加编译时间
codegen-units = 1 # 减少并行代码生成单元,提升优化但增加编译时间
@@ -0,0 +1,14 @@
[package]
name = "module-match3d"
edition.workspace = true
version.workspace = true
license.workspace = true
[features]
default = []
spacetime-types = ["dep:spacetimedb"]
[dependencies]
serde = { version = "1", features = ["derive"] }
shared-kernel = { path = "../shared-kernel" }
spacetimedb = { workspace = true, optional = true }
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,9 @@ pub mod big_fish;
pub mod big_fish_works;
pub mod creation_agent_document_input;
pub mod llm;
pub mod match3d_agent;
pub mod match3d_runtime;
pub mod match3d_works;
pub mod puzzle_agent;
pub mod puzzle_gallery;
pub mod puzzle_runtime;
@@ -0,0 +1,137 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateMatch3DAgentSessionRequest {
#[serde(default)]
pub seed_text: Option<String>,
#[serde(default)]
pub theme_text: Option<String>,
#[serde(default)]
pub reference_image_src: Option<String>,
#[serde(default)]
pub clear_count: Option<u32>,
#[serde(default)]
pub difficulty: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SendMatch3DAgentMessageRequest {
pub client_message_id: String,
pub text: String,
#[serde(default)]
pub quick_fill_requested: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteMatch3DAgentActionRequest {
pub action: String,
#[serde(default)]
pub game_name: Option<String>,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub tags: Option<Vec<String>>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub clear_count: Option<u32>,
#[serde(default)]
pub difficulty: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DCreatorConfigResponse {
pub theme_text: String,
#[serde(default)]
pub reference_image_src: Option<String>,
pub clear_count: u32,
pub difficulty: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DResultDraftResponse {
pub game_name: String,
pub theme_text: String,
pub summary: String,
pub tags: Vec<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub reference_image_src: Option<String>,
pub clear_count: u32,
pub difficulty: u32,
pub publish_ready: bool,
pub blockers: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DAgentMessageResponse {
pub id: String,
pub role: String,
pub kind: String,
pub text: String,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DAgentSessionSnapshotResponse {
pub session_id: String,
pub current_turn: u32,
pub progress_percent: u32,
pub stage: String,
#[serde(default)]
pub config: Option<Match3DCreatorConfigResponse>,
#[serde(default)]
pub draft: Option<Match3DResultDraftResponse>,
pub messages: Vec<Match3DAgentMessageResponse>,
#[serde(default)]
pub last_assistant_reply: Option<String>,
#[serde(default)]
pub published_profile_id: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DAgentSessionResponse {
pub session: Match3DAgentSessionSnapshotResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DAgentActionResponse {
pub session: Match3DAgentSessionSnapshotResponse,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn create_match3d_session_request_uses_camel_case() {
let payload = serde_json::to_value(CreateMatch3DAgentSessionRequest {
seed_text: Some("水果消除".to_string()),
theme_text: Some("水果".to_string()),
reference_image_src: Some("data:image/png;base64,abc".to_string()),
clear_count: Some(4),
difficulty: Some(3),
})
.expect("payload should serialize");
assert_eq!(payload["seedText"], json!("水果消除"));
assert_eq!(payload["themeText"], json!("水果"));
assert_eq!(
payload["referenceImageSrc"],
json!("data:image/png;base64,abc")
);
assert_eq!(payload["clearCount"], json!(4));
}
}
@@ -0,0 +1,125 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StartMatch3DRunRequest {
pub profile_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ClickMatch3DItemRequest {
#[serde(default)]
pub run_id: Option<String>,
pub item_instance_id: String,
pub client_snapshot_version: u64,
pub client_event_id: String,
pub clicked_at_ms: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct StopMatch3DRunRequest {
pub client_action_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DItemSnapshotResponse {
pub item_instance_id: String,
pub item_type_id: String,
pub visual_key: String,
pub x: f32,
pub y: f32,
pub radius: f32,
pub layer: u32,
pub state: String,
pub clickable: bool,
#[serde(default)]
pub tray_slot_index: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DTraySlotResponse {
pub slot_index: u32,
#[serde(default)]
pub item_instance_id: Option<String>,
#[serde(default)]
pub item_type_id: Option<String>,
#[serde(default)]
pub visual_key: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DRunSnapshotResponse {
pub run_id: String,
pub profile_id: String,
pub owner_user_id: String,
pub status: String,
/// 对外 HTTP 快照版本。领域层内部字段名为 board_versionfacade 需要在这里完成映射。
pub snapshot_version: u64,
pub started_at_ms: u64,
pub duration_limit_ms: u64,
#[serde(default)]
pub server_now_ms: Option<u64>,
pub remaining_ms: u64,
pub clear_count: u32,
pub total_item_count: u32,
pub cleared_item_count: u32,
pub items: Vec<Match3DItemSnapshotResponse>,
pub tray_slots: Vec<Match3DTraySlotResponse>,
#[serde(default)]
pub failure_reason: Option<String>,
#[serde(default)]
pub last_confirmed_action_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DClickConfirmationResponse {
pub accepted: bool,
#[serde(default)]
pub reject_reason: Option<String>,
#[serde(default)]
pub entered_slot_index: Option<u32>,
pub cleared_item_instance_ids: Vec<String>,
pub run: Match3DRunSnapshotResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DRunResponse {
pub run: Match3DRunSnapshotResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DClickResponse {
pub confirmation: Match3DClickConfirmationResponse,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn click_match3d_item_request_uses_camel_case() {
let payload = serde_json::to_value(ClickMatch3DItemRequest {
run_id: Some("run-1".to_string()),
item_instance_id: "item-1".to_string(),
client_snapshot_version: 7,
client_event_id: "event-1".to_string(),
clicked_at_ms: 12_345,
})
.expect("payload should serialize");
assert_eq!(payload["runId"], json!("run-1"));
assert_eq!(payload["itemInstanceId"], json!("item-1"));
assert_eq!(payload["clientSnapshotVersion"], json!(7));
assert_eq!(payload["clientEventId"], json!("event-1"));
assert_eq!(payload["clickedAtMs"], json!(12_345));
}
}
@@ -0,0 +1,89 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PutMatch3DWorkRequest {
pub game_name: String,
pub summary: String,
pub tags: Vec<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub reference_image_src: Option<String>,
pub clear_count: u32,
pub difficulty: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DWorkSummaryResponse {
pub work_id: String,
pub profile_id: String,
pub owner_user_id: String,
#[serde(default)]
pub source_session_id: Option<String>,
pub game_name: String,
pub theme_text: String,
pub summary: String,
pub tags: Vec<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub reference_image_src: Option<String>,
pub clear_count: u32,
pub difficulty: u32,
pub publication_status: String,
pub play_count: u32,
pub updated_at: String,
#[serde(default)]
pub published_at: Option<String>,
pub publish_ready: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DWorkProfileResponse {
#[serde(flatten)]
pub summary: Match3DWorkSummaryResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DWorksResponse {
pub items: Vec<Match3DWorkSummaryResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DWorkDetailResponse {
pub item: Match3DWorkProfileResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DWorkMutationResponse {
pub item: Match3DWorkProfileResponse,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn match3d_work_request_uses_camel_case() {
let payload = serde_json::to_value(PutMatch3DWorkRequest {
game_name: "水果抓大鹅".to_string(),
summary: "水果主题".to_string(),
tags: vec!["水果".to_string()],
cover_image_src: None,
reference_image_src: None,
clear_count: 4,
difficulty: 5,
})
.expect("payload should serialize");
assert_eq!(payload["gameName"], json!("水果抓大鹅"));
assert_eq!(payload["clearCount"], json!(4));
}
}
@@ -18,6 +18,7 @@ module-big-fish = { path = "../module-big-fish", default-features = false, featu
module-combat = { path = "../module-combat", default-features = false, features = ["spacetime-types"] }
module-inventory = { path = "../module-inventory", default-features = false, features = ["spacetime-types"] }
module-custom-world = { path = "../module-custom-world", default-features = false, features = ["spacetime-types"] }
module-match3d = { path = "../module-match3d", default-features = false }
module-npc = { path = "../module-npc", default-features = false, features = ["spacetime-types"] }
module-puzzle = { path = "../module-puzzle", default-features = false, features = ["spacetime-types"] }
module-progression = { path = "../module-progression", default-features = false, features = ["spacetime-types"] }
@@ -31,6 +31,7 @@ mod auth;
mod big_fish;
mod domain_types;
mod entry;
mod match3d;
mod migration;
mod puzzle;
mod runtime;
@@ -41,6 +42,7 @@ pub use auth::*;
pub use big_fish::*;
pub use domain_types::*;
pub use entry::*;
pub use match3d::*;
pub use migration::*;
pub use runtime::*;
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More