统一推荐页游客运行态与切换队列
统一推荐页各玩法正式 runtime 的游客鉴权透传。 收口推荐页首页展示队列和嵌入运行态切换队列。 补齐未登录读档、签名资产和个人数据读取的游客态处理。 新增运行态 HUD 小尺寸 logo 资源并更新拼图与抓鹅展示。 补充推荐切换、runtime guest 启动和客户端请求回归测试。 更新玩法链路、后端契约和团队记忆文档。
This commit is contained in:
@@ -279,7 +279,7 @@
|
||||
- 背景:Match3D、SquareHole、Puzzle、Jump Hop 等 runtime client 重复手写 path segment 编码、JSON header / body、runtime guest token、auth options 和 retry options,新增玩法容易遗漏同一请求骨架。
|
||||
- 决策:新增 `src/services/runtimeRequest.ts`,以 `buildRuntimeApiPath` 统一 runtime path 编码,以 `requestRuntimeJson` 统一 JSON 请求、runtime guest auth 和 retry 合并。Match3D 与 SquareHole runtime client 已先迁移,保留原导出函数名、错误文案、返回契约和重试常量。
|
||||
- 追加决策:Big Fish 与 Bark Battle runtime client 也迁入 `runtimeRequest.ts`;玩法专属 payload 归一化(如 Bark Battle start / finish 自动补 `workId`、`runId`)仍留在各玩法 client,通用 Module 只承接请求骨架。
|
||||
- 追加决策:Puzzle 的 start / get / swap / drag / next-level / leaderboard 与 Jump Hop 的 start / jump / restart 也迁入 `runtimeRequest.ts`;Puzzle `pause` 与 `props` 仍保留原账号态 auth options,不直接接入 runtime guest auth。
|
||||
- 追加决策:Puzzle 的 start / get / swap / drag / next-level / leaderboard / pause / props 与 Jump Hop 的 start / jump / restart 也迁入 `runtimeRequest.ts`;只要调用方传入 Runtime Guest Token,所有正式 runtime 请求都统一带局部 Authorization、`skipAuth` 与 `skipRefresh`。
|
||||
- 追加决策:Wooden Fish 的 start / checkpoint / finish 与 Visual Novel 的 gallery / run / history / regenerate JSON 请求也迁入 `runtimeRequest.ts`;Wooden Fish 的 `clientEventId` 生成仍留在木鱼 client,Visual Novel start 因 `timeoutMs`、SSE 因流式 `fetchWithApiAuth` 仍暂留原实现。
|
||||
- 影响范围:`src/services/runtimeRequest.ts`、Match3D / SquareHole / Big Fish / Bark Battle / Puzzle / Jump Hop / Wooden Fish / Visual Novel runtime client。
|
||||
- 验证方式:`npm run test -- src/services/runtimeRequest.test.ts src/services/recommendedRuntimeGuestLaunch.test.ts src/services/match3d-runtime/match3dRuntimeAdapter.test.ts`、`npm run typecheck`、`npm run check:encoding`、相关文件 ESLint 通过。
|
||||
@@ -428,7 +428,7 @@
|
||||
## 2026-05-25 抓大鹅运行态 HUD 收敛为拼图同款低遮挡样式
|
||||
|
||||
- 背景:抓大鹅游玩阶段 UI 需要继续对齐拼图运行态的观感,同时移除右上角设置入口、灰白半透底板和显眼锅壳,让棋盘区域更专注。
|
||||
- 决策:抓大鹅运行态只保留左上透明返回按钮,右上不再显示设置入口;顶部关卡名和倒计时直接复用拼图同款的铭牌 + 下挂计时牌结构、同色板、同造型和 `media/logo.png` 产品 logo;底部备选栏和道具图标保持交互边界但不再显示灰白半透底;中央容器图层可以视觉隐藏,但棋盘命中边界和既有交互逻辑保留。
|
||||
- 决策:抓大鹅运行态只保留左上透明返回按钮,右上不再显示设置入口;顶部关卡名和倒计时直接复用拼图同款的铭牌 + 下挂计时牌结构、同色板、同造型和 `media/logo-runtime-hud.webp` 产品 logo 小图;底部备选栏和道具图标保持交互边界但不再显示灰白半透底;中央容器图层可以视觉隐藏,但棋盘命中边界和既有交互逻辑保留。
|
||||
- 影响范围:`src/components/match3d-runtime/Match3DRuntimeShell.tsx`、`src/components/match3d-runtime/Match3DRuntimeShell.test.tsx`、`src/index.css`、抓大鹅玩法链路文档。
|
||||
- 验证方式:运行态页面不再渲染“打开抓大鹅设置”,顶部仍显示关卡名和倒计时,底部槽位和道具按钮 class 中不含旧白底视觉;相关测试通过后保持该口径。
|
||||
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||
|
||||
@@ -1149,6 +1149,7 @@
|
||||
- 追加处理:generated 私有图片换签 `/api/assets/read-url` 也属于展示层后台请求;推荐页拼图运行态挂载后会立即解析封面图,若换签 401 触发全局鉴权事件,也会表现成“进入拼图作品后瞬间未登录”。资源换签失败只应让当前图片为空,不应清 token、广播 auth 事件或主动 refresh。
|
||||
- 追加处理:从推荐页点进公开拼图作品并启动完整运行态后,`startPuzzleRun`、通关自动 `submitPuzzleLeaderboard`、下一关 `advancePuzzleNextLevel` 和重开同样属于当前玩法局部同步;这些请求失败时只应留在拼图错误态,不应清 token 或广播 auth 事件。
|
||||
- 追加处理:通关后 `refreshSaveArchives()`、首屏 bootstrap 的个人看板/作品架/浏览历史读写也只是平台投影刷新,失败应显示局部错误,不能充当全局登录态判定。
|
||||
- 追加处理:未登录推荐页启动任一公开正式玩法时,`/api/runtime/*` 局内路由必须使用 `RuntimePrincipal`,前端通过 `PlatformEntryFlowShellImpl` 的统一 request options helper 给 start / checkpoint / finish / input / drop / click / restart / time-up / leaderboard / next-level 等动作透传 runtime guest token;公开 runtime detail 读取如跳一跳、敲木鱼必须显式 `skipAuth/skipRefresh`,匿名推荐流不能补读受保护创作详情,否则会在真正开局前打出 `/api/auth/refresh 401`。
|
||||
- 验证:`npm run test -- src/services/apiClient.test.ts src/services/assetReadUrlService.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation starts embedded puzzle"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle runtime uses frontend move merge logic and backend leaderboard"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle similar work keeps current run level progression"`。
|
||||
- 关联:`src/services/apiClient.ts`、`src/services/assetReadUrlService.ts`、`src/services/puzzle-runtime/puzzleRuntimeClient.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/technical/RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md`。
|
||||
|
||||
|
||||
@@ -125,11 +125,14 @@ npm run check:server-rs-ddd
|
||||
|
||||
`/api/runtime/puzzle/runs*` 当前接受 `RuntimePrincipal`,可同时识别登录用户 Bearer 和 runtime guest token。推荐页嵌入运行态的正式开局、交换、拖拽、下一关、暂停、道具与排行榜请求,应由前端在登录态下继续携带账号 access token;匿名游客仅在确认为未登录时走 runtime guest token。不要再把拼图 runtime 当成只认普通 Bearer 的纯账号接口。
|
||||
|
||||
公开正式 runtime 的启动与局内同步动作统一接受 `RuntimePrincipal`,包括拼图、拼消消、跳一跳、敲木鱼、抓大鹅 Match3D、方洞挑战、视觉小说、大鱼吃小鱼和汪汪声浪。登录用户仍使用账号 Bearer;未登录推荐页或公开运行态使用 Runtime Guest Token,后端以 `principal.subject()` 作为本局 owner / player subject,并用 `WorkPlayTrackingDraft::runtime_principal(...)` 记录游玩。创作、个人作品、删除、发布、Remix、点赞等账号或所有权动作不得改成 runtime guest 鉴权。
|
||||
|
||||
抓大鹅 Match3D `api-server` 内部拆分:
|
||||
|
||||
- `server-rs/crates/api-server/src/modules/match3d.rs` 继续负责路由装配和 body limit;对外 handler 名称保持不变。
|
||||
- `server-rs/crates/api-server/src/match3d.rs` 只作为聚合入口,保留共享 import / 常量 / 内部类型、模块声明和 handler re-export。
|
||||
- `server-rs/crates/api-server/src/match3d/handlers.rs` 承接 Axum handler,负责 extract、鉴权上下文、调用 SpacetimeDB facade / 编排 helper,并返回 HTTP 响应。
|
||||
- `/api/runtime/match3d/works/{profile_id}/runs`、`/api/runtime/match3d/runs/{run_id}`、`/click`、`/stop`、`/restart` 与 `/time-up` 属于正式运行态局部请求,必须接受 `RuntimePrincipal`;登录用户使用账号 Bearer,推荐页匿名游客使用 runtime guest token,后端以 principal subject 作为本局 owner,不得退回只认普通 Bearer 的路由。
|
||||
- `server-rs/crates/api-server/src/match3d/draft.rs` 承接 Agent session、草稿编译、题材 / 难度 / 物品计划和草稿持久化编排。
|
||||
- `server-rs/crates/api-server/src/match3d/works.rs` 承接作品 CRUD、封面 / 背景 / 容器资产生成入口、发布 / Remix / 点赞 / 游玩记录和作品级 helper。
|
||||
- `server-rs/crates/api-server/src/match3d/item_assets.rs` 承接物品生成批次编排、append / replace / delete / sort / merge、计费外层和草稿素材映射;sheet prompt、绿幕 / 近白底透明化、切图和切片持久化复用 `generated_asset_sheets` 通用模块。
|
||||
|
||||
@@ -146,12 +146,13 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列
|
||||
- 拼图试玩和正式运行态刷新恢复不复用创作私有 query。进入 `/runtime/puzzle` 时必须写入 `runtimeProfileId`、草稿 `runtimeSessionId`、可选 `runtimeLevelId`、公开作品 `work` 和 `mode=draft|published`;进入运行态的导航顺序必须先切到 `/runtime/puzzle`,再写这些 runtime query,避免被阶段导航清掉后刷新停在“正在进入拼图关卡”。
|
||||
- 结果页生成关卡图时若关卡名为空,前端必须传 `shouldAutoNameLevel=true`,后端复用首关命名契约先按画面描述生成关卡名,再在图片生成后用视觉命名结果精修,并把生成名和 UI 背景提示词随本次关卡快照写回。
|
||||
- 拼图运行态背景优先读取当前关卡 `levelBackgroundImageSrc/levelBackgroundImageObjectKey`,旧数据才兼容 `uiBackgroundImageSrc/uiBackgroundImageObjectKey`;本地试玩、直达指定关卡和正式 `next-level` 推进时,目标关卡缺关卡背景时必须继承同作品首个可用关卡背景,仍缺失时才沿用当前运行态快照背景或默认 UI。运行态按钮视觉优先读取当前关卡 `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`,先按透明 alpha 自动边界检测识别 spritesheet 中的独立按钮展示矩形,再按原图位置从左到右、从上到下映射到返回、设置、下一关、提示、原图、冻结;同一组件还要按较高 alpha 阈值派生紧致点击热区,透明留白和柔边低 alpha 区域尽量不响应点击。检测失败时回退旧固定六格裁切,缺失时才用现有图标按钮兜底。有 spritesheet 时,返回、设置和下一关的点击容器只提供透明点击区,不再叠加默认白色圆形底、胶囊主按钮底或额外文字;下一关按钮在通关弹窗和底部入口中都直接使用 spritesheet 裁切出的 next 素材作为按钮本体。底部提示、原图、冻结三枚素材按检测矩形的原始宽高比显示,不能强行拉伸成正圆或铺满整列。底部道具区不再使用连片胶囊背景,提示、原图、冻结三个按钮均匀分布;运行态只展示按钮素材本身,不额外叠加“提示 / 原图 / 冻结”文字。
|
||||
- 推荐页本身不是登录门禁入口,未登录用户点击底部或侧边栏的推荐 Tab 应直接进入嵌入运行态,不主动打开登录弹窗。推荐页嵌入运行态必须按真实身份分流:已登录用户或本地已有 access token 时,启动拼图和后续排行榜 / 下一关等正式请求继续走账号 Bearer;只有确认为匿名访客时才申请并透传 runtime guest token。`/api/runtime/puzzle/runs*` 后端统一接受 `RuntimePrincipal`,可识别账号用户和匿名 runtime guest;推荐卡片的后台读写请求仍使用 local auth impact,避免单卡 401 清空整站登录态。创作、个人作品、删除、发布、Remix 等账号或所有权动作仍保持普通用户鉴权。
|
||||
- 推荐页本身不是登录门禁入口,未登录用户点击底部或侧边栏的推荐 Tab 应直接进入嵌入运行态,不主动打开登录弹窗。推荐页嵌入运行态必须按真实身份分流:已登录用户或本地已有 access token 时,正式 runtime 启动与后续局内动作继续走账号 Bearer;只有确认为匿名访客时才申请并透传 runtime guest token。平台壳统一通过 `buildRecommendRuntimeRequestOptions(...)` 为各玩法的 start / checkpoint / finish / input / drop / click / restart / time-up / leaderboard / next-level 等动作生成局部 request options,不允许每个玩法各写一套匿名分支。后端 `/api/runtime/*` 正式运行态写请求统一接受 `RuntimePrincipal`,可识别账号用户和匿名 runtime guest;推荐卡片的后台读写请求仍使用 local auth impact,避免单卡 401 清空整站登录态。创作、个人作品、删除、发布、Remix 等账号或所有权动作仍保持普通用户鉴权。
|
||||
- 推荐页作品队列只能通过 `buildPlatformRecommendFeedEntries(...)` 生成,首页卡片窗口、桌面推荐格、嵌入 runtime 自动启动和上一条 / 下一条切换都必须消费同一队列。不得在首页和 `PlatformEntryFlowShellImpl` 内分别按“最新列表顺序”和“评分推荐顺序”各算一套相邻作品,否则连续切换会出现视觉上跳过作品或回跳。
|
||||
- 拼图运行态棋盘不叠加分块蒙版、描边、阴影、选中底色或合并块 SVG 轮廓;拼图片本体需要裁切为圆角形状,单块使用独立圆角裁切,合并块使用 SVG 原生 `clipPath` 裁切整体外轮廓,外凸角和内凹角分别计算半径,内凹角半径要比外凸角更明显以避免手机 WebView 中看起来仍是直角。原图道具只在用户主动确认后打开独立原图查看层,不在当前拼图棋盘上叠加原图。
|
||||
- 拼图运行态拖拽必须完全跟随手指或鼠标位置,`pointermove` 期间即时写入可见拼块的 transform,不依赖等待后端回包、React 重渲染或下一帧动画队列;进入拖动后不展示拼块选中态或“已选择”提示,松手后再提交目标格同步规则真相。
|
||||
- 拼图运行态的提示、设置等点击弹层跟随当前运行态主色主题,使用普通圆角主题面板,不复用像素九宫格素材框。
|
||||
- 拼图运行态壳层自身要补齐 `platform-ui-shell` / `platform-theme` / `platform-theme--light|dark`,不能依赖外层平台壳来提供主题变量;`/puzzle` 直达页和平台内嵌页都必须渲染同一套主题语义类。
|
||||
- 拼图运行态顶部关卡信息采用游戏化铭牌样式:橘棕横向关卡名牌承载 `第 N 关` 和关卡名,左侧固定使用 `media/logo.png` 卡通形象;倒计时作为下挂米白小牌独立显示,紧贴铭牌但不遮挡棋盘。该样式只改变运行态 HUD 视觉,不改变计时、暂停、失败同步或关卡推进规则。
|
||||
- 拼图运行态顶部关卡信息采用游戏化铭牌样式:橘棕横向关卡名牌承载 `第 N 关` 和关卡名,左侧固定使用 `media/logo-runtime-hud.webp` 卡通形象小图;倒计时作为下挂米白小牌独立显示,紧贴铭牌但不遮挡棋盘。该样式只改变运行态 HUD 视觉,不改变计时、暂停、失败同步或关卡推进规则。
|
||||
- 拼图运行态进行中关卡的 `elapsedMs` 仍是结算字段,设置面板的“当前用时”必须按 `startedAtMs`、暂停累计和冻结累计实时派生;不要直接把进行中的 `currentLevel.elapsedMs` 当作展示值。
|
||||
- 推荐页嵌入拼图运行态时,通关结算弹层必须挂到页面级 fixed 浮层,不能留在推荐卡片视觉区内的 absolute 覆盖层;推荐页滑动卡片和运行态视口都使用 `overflow: hidden`,半屏内容区会裁剪排行榜、下一关按钮和相似作品卡。
|
||||
- 推荐页嵌入拼图运行态时,“下一关”应优先切到相似作品;如果当前推荐候选为空,才回退到同作品下一关,避免匿名推荐流在多关卡作品上持续停留在同一作品内。下一关请求 pending 期间必须保留当前 `PuzzleRuntimeShell` 和棋盘,不得把推荐卡整体切回 `加载中...` 占位态;局部同步状态由拼图运行态自己的 busy 表现承接。后端返回的新关卡属于其它作品时,前端必须同步 `selectedPuzzleDetail`、推荐页 `puzzleGalleryEntries` 缓存和 `activeRecommendEntryKey`,让底部作品信息、分享 / 点赞 / 改造和下一次“下一个”基准都指向新作品。
|
||||
@@ -304,7 +305,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列
|
||||
- 难度只决定本局加载的物品种类数量:轻松 3、标准 9、进阶 15、硬核 20。硬核仍保留 21 次消除和 63 件总物品,运行态按 20 种素材循环复用,不要求生成第 21 种素材。
|
||||
- 运行态启动前要预加载 `generatedItemAssets[].imageViews[]`、顶层 `generatedBackgroundAsset`、物品挂载 `backgroundAsset` 中的背景、UI spritesheet 和物品 spritesheet;首次生成自动试玩、结果页手动试玩、推荐流和公开详情启动都必须传入提升后的 profile。卡片摘要缺图集字段时,进入运行态前必须补读 work detail。补读后的 profile 也要再次提升 `generatedItemAssets[].backgroundAsset`,确保背景和图集字段传给 `Match3DRuntimeShell`。
|
||||
- 背景图作为运行态全屏背景,图内已经保留容器;旧 `containerImage*` 只作为历史透明容器兼容字段。若 `containerImage*` 与 `uiSpritesheetImage*` 同源,运行态不得把 UI spritesheet 当中心容器图叠到棋盘上。
|
||||
- 抓大鹅运行态 HUD 需贴近拼图顶部信息条的视觉口径:左上只保留透明返回按钮;右上不再暴露设置入口;顶部关卡名和倒计时直接复用拼图同款的铭牌 + 下挂计时牌结构、同色板和同造型,并在牌面左侧挂上 `media/logo.png` 产品 logo;下方备选栏和道具图标只保留内容与交互边界,不再显示灰白半透底板;中央容器图层视觉可隐藏,但棋盘命中边界仍保留。
|
||||
- 抓大鹅运行态 HUD 需贴近拼图顶部信息条的视觉口径:左上只保留透明返回按钮;右上不再暴露设置入口;顶部关卡名和倒计时直接复用拼图同款的铭牌 + 下挂计时牌结构、同色板和同造型,并在牌面左侧挂上 `media/logo-runtime-hud.webp` 产品 logo 小图;下方备选栏和道具图标只保留内容与交互边界,不再显示灰白半透底板;中央容器图层视觉可隐藏,但棋盘命中边界仍保留。
|
||||
- generated 私有图换签未完成时,局内物品先隐藏等待,不得短暂显示默认积木;同一批资源在重启 run 时保留已解析签名 URL,只有资源源列表变化或换签失败后才允许进入兜底视觉。
|
||||
- `itemSize` 只缩放生成 2D 图片本体:`大`、`中`、`小` 均按相对尺寸缩放,其中 `大` 也比原始图片略小,`中` 和 `小` 进一步缩小;不改变后端下发的布局半径、点击半径或三消规则。
|
||||
- 物品进入底部物品栏时按同类型插入:如果物品栏已有同类物品,新物品插到该类型最后一个物品后面,后续物品整体后移;没有同类时追加到当前末尾。达到三件同类时,在飞入物品栏动画结束后,左侧和右侧同类物品向中间合成,三件一起消失,播放合成音效,不展示星星图标,后面的物品再向前补位。该动效只是前端表现层,后端和本地试玩仍负责权威插入、指定点击类型清除与补位后的槽位快照。
|
||||
|
||||
BIN
media/logo-runtime-hud.webp
Normal file
BIN
media/logo-runtime-hud.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -37,7 +37,7 @@ use time::{Duration as TimeDuration, OffsetDateTime};
|
||||
use crate::{
|
||||
api_response::json_success_body,
|
||||
asset_billing::execute_billable_asset_operation_with_cost,
|
||||
auth::AuthenticatedAccessToken,
|
||||
auth::{AuthenticatedAccessToken, RuntimePrincipal},
|
||||
generated_image_assets::{
|
||||
GeneratedImageAssetAdapter, GeneratedImageAssetDataUrl,
|
||||
adapter::{GeneratedImageAssetAdapterMetadata, GeneratedImageAssetPersistInput},
|
||||
@@ -506,13 +506,13 @@ pub async fn get_bark_battle_runtime_config(
|
||||
State(state): State<AppState>,
|
||||
Path(work_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, &work_id, "workId")?;
|
||||
|
||||
let config = state
|
||||
.spacetime_client()
|
||||
.get_bark_battle_runtime_config(work_id, Some(authenticated.claims().user_id().to_string()))
|
||||
.get_bark_battle_runtime_config(work_id, Some(principal.subject().to_string()))
|
||||
.await
|
||||
.map_err(|error| {
|
||||
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
|
||||
@@ -526,7 +526,7 @@ pub async fn start_bark_battle_run(
|
||||
State(state): State<AppState>,
|
||||
Path(work_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<BarkBattleRunStartRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let maybe_payload = payload.ok().map(|Json(payload)| payload);
|
||||
@@ -543,7 +543,7 @@ pub async fn start_bark_battle_run(
|
||||
};
|
||||
ensure_non_empty(&request_context, &work_id, "workId")?;
|
||||
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let owner_user_id = principal.subject().to_string();
|
||||
let runtime_config = state
|
||||
.spacetime_client()
|
||||
.get_bark_battle_runtime_config(work_id.clone(), Some(owner_user_id.clone()))
|
||||
@@ -593,12 +593,13 @@ pub async fn start_bark_battle_run(
|
||||
record_work_play_start_after_success(
|
||||
&state,
|
||||
&request_context,
|
||||
WorkPlayTrackingDraft::new(
|
||||
WorkPlayTrackingDraft::runtime_principal(
|
||||
BARK_BATTLE_PLAY_TYPE_ID,
|
||||
work_id.clone(),
|
||||
&authenticated,
|
||||
&principal,
|
||||
"/api/runtime/bark-battle/...",
|
||||
)
|
||||
.owner_user_id(owner_user_id.clone())
|
||||
.extra(json!({
|
||||
"runId": run_snapshot.run_id,
|
||||
"workId": work_id,
|
||||
@@ -607,6 +608,7 @@ pub async fn start_bark_battle_run(
|
||||
"difficultyPreset": runtime_config.difficulty_preset,
|
||||
"sourceRoute": request.source_route,
|
||||
"clientRuntimeVersion": request.client_runtime_version,
|
||||
"principalKind": principal.kind().as_str(),
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
@@ -638,12 +640,12 @@ pub async fn get_bark_battle_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, &run_id, "runId")?;
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.get_bark_battle_run(run_id, authenticated.claims().user_id().to_string())
|
||||
.get_bark_battle_run(run_id, principal.subject().to_string())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
|
||||
@@ -657,7 +659,7 @@ pub async fn finish_bark_battle_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<BarkBattleRunFinishRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = bark_battle_json(payload, &request_context)?;
|
||||
@@ -698,7 +700,7 @@ pub async fn finish_bark_battle_run(
|
||||
.finish_bark_battle_run(BarkBattleRunFinishRecordInput {
|
||||
run_id,
|
||||
run_token: payload.run_token,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
work_id: payload.work_id.clone(),
|
||||
config_version: u64::from(payload.config_version),
|
||||
ruleset_version: payload.ruleset_version.clone(),
|
||||
|
||||
@@ -63,7 +63,7 @@ use crate::{
|
||||
},
|
||||
api_response::json_success_body,
|
||||
asset_billing::execute_billable_asset_operation,
|
||||
auth::AuthenticatedAccessToken,
|
||||
auth::{AuthenticatedAccessToken, RuntimePrincipal},
|
||||
character_visual_assets::try_apply_background_alpha_to_png,
|
||||
http_error::AppError,
|
||||
platform_errors::map_oss_error,
|
||||
@@ -224,7 +224,7 @@ pub async fn record_big_fish_play(
|
||||
State(state): State<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<RecordBigFishPlayRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
@@ -242,7 +242,7 @@ pub async fn record_big_fish_play(
|
||||
.spacetime_client()
|
||||
.record_big_fish_play(BigFishPlayReportRecordInput {
|
||||
session_id: session_id.clone(),
|
||||
user_id: authenticated.claims().user_id().to_string(),
|
||||
user_id: principal.subject().to_string(),
|
||||
elapsed_ms: payload.elapsed_ms.unwrap_or(0),
|
||||
reported_at_micros: current_utc_micros(),
|
||||
})
|
||||
@@ -254,13 +254,14 @@ pub async fn record_big_fish_play(
|
||||
record_work_play_start_after_success(
|
||||
&state,
|
||||
&request_context,
|
||||
WorkPlayTrackingDraft::new(
|
||||
WorkPlayTrackingDraft::runtime_principal(
|
||||
"big-fish",
|
||||
session_id.clone(),
|
||||
&authenticated,
|
||||
&principal,
|
||||
"/api/runtime/big-fish/sessions/{session_id}/play",
|
||||
)
|
||||
.run_id(session_id.clone()),
|
||||
.run_id(session_id.clone())
|
||||
.owner_user_id(principal.subject().to_string()),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -279,7 +280,7 @@ pub async fn start_big_fish_run(
|
||||
State(state): State<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, &session_id, "sessionId")?;
|
||||
|
||||
@@ -288,7 +289,7 @@ pub async fn start_big_fish_run(
|
||||
.start_big_fish_run(BigFishRunStartRecordInput {
|
||||
run_id: build_prefixed_uuid_id("big-fish-run-"),
|
||||
session_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
started_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
@@ -339,13 +340,13 @@ pub async fn get_big_fish_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, &run_id, "runId")?;
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.get_big_fish_run(run_id, authenticated.claims().user_id().to_string())
|
||||
.get_big_fish_run(run_id, principal.subject().to_string())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
@@ -363,7 +364,7 @@ pub async fn submit_big_fish_input(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<SubmitBigFishInputRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
@@ -384,7 +385,7 @@ pub async fn submit_big_fish_input(
|
||||
.spacetime_client()
|
||||
.submit_big_fish_input(BigFishInputSubmitRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
x: payload.x,
|
||||
y: payload.y,
|
||||
submitted_at_micros: current_utc_micros(),
|
||||
|
||||
@@ -69,7 +69,7 @@ use crate::{
|
||||
execute_billable_asset_operation_with_cost, map_asset_operation_wallet_error,
|
||||
should_skip_asset_operation_billing_for_connectivity,
|
||||
},
|
||||
auth::AuthenticatedAccessToken,
|
||||
auth::{AuthenticatedAccessToken, RuntimePrincipal},
|
||||
config::AppConfig,
|
||||
generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha,
|
||||
http_error::AppError,
|
||||
|
||||
@@ -1171,7 +1171,7 @@ pub async fn start_match3d_run(
|
||||
State(state): State<AppState>,
|
||||
Path(profile_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<StartMatch3DRunRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let maybe_payload = payload.ok().map(|Json(payload)| payload);
|
||||
@@ -1191,7 +1191,7 @@ pub async fn start_match3d_run(
|
||||
.spacetime_client()
|
||||
.start_match3d_run(Match3DRunStartRecordInput {
|
||||
run_id: build_prefixed_uuid_id(MATCH3D_RUN_ID_PREFIX),
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
profile_id: profile_id.clone(),
|
||||
started_at_ms: current_utc_ms(),
|
||||
item_type_count_override: maybe_payload
|
||||
@@ -1211,15 +1211,17 @@ pub async fn start_match3d_run(
|
||||
record_work_play_start_after_success(
|
||||
&state,
|
||||
&request_context,
|
||||
WorkPlayTrackingDraft::new(
|
||||
WorkPlayTrackingDraft::runtime_principal(
|
||||
"match3d",
|
||||
profile_id.clone(),
|
||||
&authenticated,
|
||||
&principal,
|
||||
"/api/runtime/match3d/...",
|
||||
)
|
||||
.profile_id(profile_id.clone())
|
||||
.owner_user_id(principal.subject().to_string())
|
||||
.extra(json!({
|
||||
"runId": run.run_id,
|
||||
"principalKind": principal.kind().as_str(),
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
@@ -1236,13 +1238,13 @@ pub async fn get_match3d_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, MATCH3D_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.get_match3d_run(run_id, authenticated.claims().user_id().to_string())
|
||||
.get_match3d_run(run_id, principal.subject().to_string())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
match3d_error_response(
|
||||
@@ -1264,7 +1266,7 @@ pub async fn click_match3d_item(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<ClickMatch3DItemRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = match3d_json(payload, &request_context, MATCH3D_RUNTIME_PROVIDER)?;
|
||||
@@ -1286,7 +1288,7 @@ pub async fn click_match3d_item(
|
||||
.spacetime_client()
|
||||
.click_match3d_item(Match3DRunClickRecordInput {
|
||||
run_id: payload.run_id.unwrap_or(run_id),
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
item_instance_id: payload.item_instance_id,
|
||||
client_snapshot_version: payload.client_snapshot_version.min(u32::MAX as u64) as u32,
|
||||
client_event_id: payload.client_event_id,
|
||||
@@ -1313,7 +1315,7 @@ pub async fn stop_match3d_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<StopMatch3DRunRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let _ = payload.ok();
|
||||
@@ -1323,7 +1325,7 @@ pub async fn stop_match3d_run(
|
||||
.spacetime_client()
|
||||
.stop_match3d_run(Match3DRunStopRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
stopped_at_ms: current_utc_ms(),
|
||||
})
|
||||
.await
|
||||
@@ -1347,7 +1349,7 @@ pub async fn restart_match3d_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, MATCH3D_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||
|
||||
@@ -1356,7 +1358,7 @@ pub async fn restart_match3d_run(
|
||||
.restart_match3d_run(Match3DRunRestartRecordInput {
|
||||
source_run_id: run_id,
|
||||
next_run_id: build_prefixed_uuid_id(MATCH3D_RUN_ID_PREFIX),
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
restarted_at_ms: current_utc_ms(),
|
||||
})
|
||||
.await
|
||||
@@ -1380,7 +1382,7 @@ pub async fn finish_match3d_time_up(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, MATCH3D_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||
|
||||
@@ -1388,7 +1390,7 @@ pub async fn finish_match3d_time_up(
|
||||
.spacetime_client()
|
||||
.finish_match3d_time_up(Match3DRunTimeUpRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
finished_at_ms: current_utc_ms(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::require_bearer_auth,
|
||||
auth::{require_bearer_auth, require_runtime_principal_auth},
|
||||
bark_battle::{
|
||||
create_bark_battle_draft, delete_bark_battle_work, finish_bark_battle_run,
|
||||
generate_bark_battle_image_asset, get_bark_battle_run, get_bark_battle_runtime_config,
|
||||
@@ -66,26 +66,28 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
"/api/runtime/bark-battle/works/{work_id}/config",
|
||||
get(get_bark_battle_runtime_config).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/bark-battle/works/{work_id}/runs",
|
||||
post(start_bark_battle_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/bark-battle/runs/{run_id}",
|
||||
get(get_bark_battle_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/bark-battle/runs/{run_id}/finish",
|
||||
post(finish_bark_battle_run)
|
||||
.route_layer(middleware::from_fn_with_state(state, require_bearer_auth)),
|
||||
post(finish_bark_battle_run).route_layer(middleware::from_fn_with_state(
|
||||
state,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::require_bearer_auth,
|
||||
auth::{require_bearer_auth, require_runtime_principal_auth},
|
||||
big_fish::{
|
||||
create_big_fish_session, delete_big_fish_work, execute_big_fish_action, get_big_fish_run,
|
||||
get_big_fish_session, get_big_fish_works, list_big_fish_gallery,
|
||||
@@ -85,35 +85,35 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
"/api/runtime/big-fish/sessions/{session_id}/play",
|
||||
post(record_big_fish_play).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/big-fish/works/{session_id}/play",
|
||||
post(record_big_fish_play).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/big-fish/sessions/{session_id}/runs",
|
||||
post(start_big_fish_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/big-fish/runs/{run_id}",
|
||||
get(get_big_fish_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/big-fish/runs/{run_id}/input",
|
||||
post(submit_big_fish_input).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::require_bearer_auth,
|
||||
auth::{require_bearer_auth, require_runtime_principal_auth},
|
||||
match3d::{
|
||||
click_match3d_item, compile_match3d_agent_draft, create_match3d_agent_session,
|
||||
delete_match3d_work, execute_match3d_agent_action, finish_match3d_time_up,
|
||||
@@ -139,42 +139,42 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
"/api/runtime/match3d/works/{profile_id}/runs",
|
||||
post(start_match3d_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/match3d/runs/{run_id}",
|
||||
get(get_match3d_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/match3d/runs/{run_id}/click",
|
||||
post(click_match3d_item).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/match3d/runs/{run_id}/stop",
|
||||
post(stop_match3d_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/match3d/runs/{run_id}/restart",
|
||||
post(restart_match3d_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/match3d/runs/{run_id}/time-up",
|
||||
post(finish_match3d_time_up).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::require_bearer_auth,
|
||||
auth::{require_bearer_auth, require_runtime_principal_auth},
|
||||
square_hole::{
|
||||
compile_square_hole_agent_draft, create_square_hole_agent_session, delete_square_hole_work,
|
||||
drop_square_hole_shape, execute_square_hole_agent_action, finish_square_hole_time_up,
|
||||
@@ -101,42 +101,42 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
"/api/runtime/square-hole/works/{profile_id}/runs",
|
||||
post(start_square_hole_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/square-hole/runs/{run_id}",
|
||||
get(get_square_hole_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/square-hole/runs/{run_id}/drop",
|
||||
post(drop_square_hole_shape).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/square-hole/runs/{run_id}/stop",
|
||||
post(stop_square_hole_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/square-hole/runs/{run_id}/restart",
|
||||
post(restart_square_hole_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/square-hole/runs/{run_id}/time-up",
|
||||
post(finish_square_hole_time_up).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::require_bearer_auth,
|
||||
auth::{require_bearer_auth, require_runtime_principal_auth},
|
||||
state::AppState,
|
||||
vector_engine_audio_generation::{
|
||||
create_background_music_task, create_sound_effect_task,
|
||||
@@ -151,33 +151,35 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
"/api/runtime/visual-novel/works/{profile_id}/runs",
|
||||
post(start_visual_novel_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/visual-novel/runs/{run_id}",
|
||||
get(get_visual_novel_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/visual-novel/runs/{run_id}/actions/stream",
|
||||
post(stream_visual_novel_action).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/visual-novel/runs/{run_id}/history",
|
||||
get(list_visual_novel_history).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/visual-novel/runs/{run_id}/regenerate",
|
||||
post(regenerate_visual_novel_run)
|
||||
.route_layer(middleware::from_fn_with_state(state, require_bearer_auth)),
|
||||
post(regenerate_visual_novel_run).route_layer(middleware::from_fn_with_state(
|
||||
state,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ use crate::generated_image_assets::{
|
||||
use crate::{
|
||||
ai_generation_drafts::{AiGenerationDraftContext, AiGenerationDraftWriter},
|
||||
api_response::json_success_body,
|
||||
auth::AuthenticatedAccessToken,
|
||||
auth::{AuthenticatedAccessToken, RuntimePrincipal},
|
||||
http_error::AppError,
|
||||
openai_image_generation::{
|
||||
DownloadedOpenAiImage, build_openai_image_http_client, create_openai_image_generation,
|
||||
@@ -739,7 +739,7 @@ pub async fn start_square_hole_run(
|
||||
State(state): State<AppState>,
|
||||
Path(profile_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<StartSquareHoleRunRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let maybe_payload = payload.ok().map(|Json(payload)| payload);
|
||||
@@ -758,7 +758,7 @@ pub async fn start_square_hole_run(
|
||||
.spacetime_client()
|
||||
.start_square_hole_run(SquareHoleRunStartRecordInput {
|
||||
run_id: build_prefixed_uuid_id(SQUARE_HOLE_RUN_ID_PREFIX),
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
profile_id: profile_id.clone(),
|
||||
started_at_ms: current_utc_ms(),
|
||||
})
|
||||
@@ -774,15 +774,17 @@ pub async fn start_square_hole_run(
|
||||
record_work_play_start_after_success(
|
||||
&state,
|
||||
&request_context,
|
||||
WorkPlayTrackingDraft::new(
|
||||
WorkPlayTrackingDraft::runtime_principal(
|
||||
"square-hole",
|
||||
profile_id.clone(),
|
||||
&authenticated,
|
||||
&principal,
|
||||
"/api/runtime/square-hole/...",
|
||||
)
|
||||
.profile_id(profile_id.clone())
|
||||
.owner_user_id(principal.subject().to_string())
|
||||
.extra(json!({
|
||||
"runId": run.run_id,
|
||||
"principalKind": principal.kind().as_str(),
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
@@ -799,7 +801,7 @@ pub async fn get_square_hole_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
@@ -810,7 +812,7 @@ pub async fn get_square_hole_run(
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.get_square_hole_run(run_id, authenticated.claims().user_id().to_string())
|
||||
.get_square_hole_run(run_id, principal.subject().to_string())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
square_hole_error_response(
|
||||
@@ -832,7 +834,7 @@ pub async fn drop_square_hole_shape(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<DropSquareHoleShapeRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = square_hole_json(payload, &request_context, SQUARE_HOLE_RUNTIME_PROVIDER)?;
|
||||
@@ -859,7 +861,7 @@ pub async fn drop_square_hole_shape(
|
||||
.spacetime_client()
|
||||
.drop_square_hole_shape(SquareHoleRunDropRecordInput {
|
||||
run_id: payload.run_id.unwrap_or(run_id),
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
hole_id: payload.hole_id,
|
||||
client_snapshot_version: payload.client_snapshot_version,
|
||||
client_event_id: payload.client_event_id,
|
||||
@@ -887,7 +889,7 @@ pub async fn stop_square_hole_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<StopSquareHoleRunRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let _ = payload.ok();
|
||||
@@ -902,7 +904,7 @@ pub async fn stop_square_hole_run(
|
||||
.spacetime_client()
|
||||
.stop_square_hole_run(SquareHoleRunStopRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
stopped_at_ms: current_utc_ms(),
|
||||
})
|
||||
.await
|
||||
@@ -926,7 +928,7 @@ pub async fn restart_square_hole_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
@@ -940,7 +942,7 @@ pub async fn restart_square_hole_run(
|
||||
.restart_square_hole_run(SquareHoleRunRestartRecordInput {
|
||||
source_run_id: run_id,
|
||||
next_run_id: build_prefixed_uuid_id(SQUARE_HOLE_RUN_ID_PREFIX),
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
restarted_at_ms: current_utc_ms(),
|
||||
})
|
||||
.await
|
||||
@@ -964,7 +966,7 @@ pub async fn finish_square_hole_time_up(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
@@ -977,7 +979,7 @@ pub async fn finish_square_hole_time_up(
|
||||
.spacetime_client()
|
||||
.finish_square_hole_time_up(SquareHoleRunTimeUpRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
finished_at_ms: current_utc_ms(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -30,7 +30,7 @@ use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body,
|
||||
auth::AuthenticatedAccessToken,
|
||||
auth::{AuthenticatedAccessToken, RuntimePrincipal},
|
||||
http_error::AppError,
|
||||
prompt::visual_novel as vn_prompt,
|
||||
request_context::RequestContext,
|
||||
@@ -434,7 +434,7 @@ pub async fn start_visual_novel_run(
|
||||
State(state): State<AppState>,
|
||||
Path(profile_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<contract::VisualNovelStartRunRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = parse_json_payload(&request_context, payload)?;
|
||||
@@ -453,7 +453,7 @@ pub async fn start_visual_novel_run(
|
||||
.spacetime_client()
|
||||
.start_visual_novel_run(VisualNovelRunStartRecordInput {
|
||||
run_id: build_prefixed_uuid_id(domain::VISUAL_NOVEL_RUN_ID_PREFIX),
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
profile_id: profile_id.clone(),
|
||||
mode: run_mode_to_wire(&payload.mode).to_string(),
|
||||
snapshot_json: None,
|
||||
@@ -467,16 +467,18 @@ pub async fn start_visual_novel_run(
|
||||
record_work_play_start_after_success(
|
||||
&state,
|
||||
&request_context,
|
||||
WorkPlayTrackingDraft::new(
|
||||
WorkPlayTrackingDraft::runtime_principal(
|
||||
"visual-novel",
|
||||
profile_id.clone(),
|
||||
&authenticated,
|
||||
&principal,
|
||||
"/api/runtime/visual-novel/...",
|
||||
)
|
||||
.profile_id(profile_id.clone())
|
||||
.owner_user_id(principal.subject().to_string())
|
||||
.extra(json!({
|
||||
"mode": run_mode_to_wire(&payload.mode),
|
||||
"runId": run.run_id,
|
||||
"principalKind": principal.kind().as_str(),
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
@@ -493,12 +495,12 @@ pub async fn get_visual_novel_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&run_id, "runId")?;
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.get_visual_novel_run(run_id, authenticated.claims().user_id().to_string())
|
||||
.get_visual_novel_run(run_id, principal.subject().to_string())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
visual_novel_error_response(&request_context, map_spacetime_error(error))
|
||||
@@ -516,13 +518,13 @@ pub async fn stream_visual_novel_action(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<contract::VisualNovelRuntimeActionRequest>, JsonRejection>,
|
||||
) -> Result<Response, Response> {
|
||||
let Json(payload) = parse_json_payload(&request_context, payload)?;
|
||||
ensure_non_empty(&run_id, "runId")?;
|
||||
ensure_non_empty(&payload.client_event_id, "clientEventId")?;
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let owner_user_id = principal.subject().to_string();
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.get_visual_novel_run(run_id.clone(), owner_user_id.clone())
|
||||
@@ -569,12 +571,12 @@ pub async fn list_visual_novel_history(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&run_id, "runId")?;
|
||||
let history = state
|
||||
.spacetime_client()
|
||||
.list_visual_novel_runtime_history(run_id, authenticated.claims().user_id().to_string())
|
||||
.list_visual_novel_runtime_history(run_id, principal.subject().to_string())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
visual_novel_error_response(&request_context, map_spacetime_error(error))
|
||||
@@ -595,13 +597,13 @@ pub async fn regenerate_visual_novel_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<contract::VisualNovelRegenerateRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = parse_json_payload(&request_context, payload)?;
|
||||
ensure_non_empty(&run_id, "runId")?;
|
||||
ensure_non_empty(&payload.history_entry_id, "historyEntryId")?;
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let owner_user_id = principal.subject().to_string();
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.get_visual_novel_run(run_id.clone(), owner_user_id.clone())
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct PublicWorkInteractionConfigAdminUpsertInput {
|
||||
pub public_work_interactions_json: String,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for PublicWorkInteractionConfigAdminUpsertInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::creation_entry_config_procedure_result_type::CreationEntryConfigProcedureResult;
|
||||
use super::public_work_interaction_config_admin_upsert_input_type::PublicWorkInteractionConfigAdminUpsertInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct UpsertPublicWorkInteractionConfigArgs {
|
||||
pub input: PublicWorkInteractionConfigAdminUpsertInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for UpsertPublicWorkInteractionConfigArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `upsert_public_work_interaction_config`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait upsert_public_work_interaction_config {
|
||||
fn upsert_public_work_interaction_config(
|
||||
&self,
|
||||
input: PublicWorkInteractionConfigAdminUpsertInput,
|
||||
) {
|
||||
self.upsert_public_work_interaction_config_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn upsert_public_work_interaction_config_then(
|
||||
&self,
|
||||
input: PublicWorkInteractionConfigAdminUpsertInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl upsert_public_work_interaction_config for super::RemoteProcedures {
|
||||
fn upsert_public_work_interaction_config_then(
|
||||
&self,
|
||||
input: PublicWorkInteractionConfigAdminUpsertInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, CreationEntryConfigProcedureResult>(
|
||||
"upsert_public_work_interaction_config",
|
||||
UpsertPublicWorkInteractionConfigArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import jumpHopRuntimeLevelLogo from '../../../media/logo.png';
|
||||
import jumpHopRuntimeLevelLogo from '../../../media/logo-runtime-hud.webp';
|
||||
import type {
|
||||
JumpHopRuntimeRunSnapshotResponse,
|
||||
JumpHopTileFaceAsset,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import match3DRuntimeLevelLogo from '../../../media/logo.png';
|
||||
import match3DRuntimeLevelLogo from '../../../media/logo-runtime-hud.webp';
|
||||
import type {
|
||||
Match3DClickItemRequest,
|
||||
Match3DClickItemResult,
|
||||
|
||||
@@ -318,6 +318,7 @@ import {
|
||||
submitRpgProfileFeedback,
|
||||
} from '../../services/rpg-entry/rpgProfileClient';
|
||||
import { requestRpgRuntimeJson } from '../../services/rpg-runtime/rpgRuntimeRequest';
|
||||
import { type RuntimeGuestRequestOptions } from '../../services/runtimeGuestAuth';
|
||||
import { squareHoleCreationClient } from '../../services/square-hole-creation';
|
||||
import {
|
||||
dropSquareHoleShape,
|
||||
@@ -372,13 +373,16 @@ import {
|
||||
type CreationWorkShelfItem,
|
||||
isPersistedBarkBattleDraftGenerating,
|
||||
} from '../custom-world-home/creationWorkShelf';
|
||||
import { selectAdjacentPlatformRecommendEntry } from '../rpg-entry/rpgEntryPublicGalleryViewModel';
|
||||
import {
|
||||
buildPlatformRecommendFeedEntries,
|
||||
selectAdjacentPlatformRecommendEntry,
|
||||
} from '../rpg-entry/rpgEntryPublicGalleryViewModel';
|
||||
import {
|
||||
isBigFishGalleryEntry,
|
||||
isEdutainmentGalleryEntry,
|
||||
isJumpHopGalleryEntry,
|
||||
isPuzzleGalleryEntry,
|
||||
isPuzzleClearGalleryEntry,
|
||||
isPuzzleGalleryEntry,
|
||||
mapPuzzleClearWorkToPlatformGalleryCard,
|
||||
mapPuzzleWorkToPlatformGalleryCard,
|
||||
type PlatformPublicGalleryCard,
|
||||
@@ -492,7 +496,6 @@ import {
|
||||
import {
|
||||
canExposePublicWork,
|
||||
EDUTAINMENT_HIDDEN_MESSAGE,
|
||||
filterGeneralPublicWorks,
|
||||
} from './platformEdutainmentVisibility';
|
||||
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
|
||||
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
|
||||
@@ -523,7 +526,6 @@ import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView';
|
||||
import { PlatformErrorDialog } from './PlatformErrorDialog';
|
||||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||
import { resolvePlatformGenerationProgressTickDecision } from './platformGenerationProgressTickModel';
|
||||
import { buildPlatformRecommendedEntries } from './platformRecommendation';
|
||||
import {
|
||||
buildMatch3DProfileFromSession,
|
||||
hasMatch3DRuntimeAsset,
|
||||
@@ -1476,6 +1478,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
useState<MiniGameDraftGenerationState | null>(null);
|
||||
const [jumpHopError, setJumpHopError] = useState<string | null>(null);
|
||||
const [isJumpHopBusy, setIsJumpHopBusy] = useState(false);
|
||||
const [barkBattleRuntimeRequestOptions, setBarkBattleRuntimeRequestOptions] =
|
||||
useState<RuntimeGuestRequestOptions | null>(null);
|
||||
const [puzzleClearSession, setPuzzleClearSession] =
|
||||
useState<PuzzleClearSessionSnapshotResponse | null>(null);
|
||||
const [puzzleClearRun, setPuzzleClearRun] = useState<
|
||||
@@ -1579,6 +1583,34 @@ export function PlatformEntryFlowShellImpl({
|
||||
useState<PuzzleRuntimeReturnStage>('puzzle-gallery-detail');
|
||||
const [puzzleRuntimeAuthMode, setPuzzleRuntimeAuthMode] =
|
||||
useState<PuzzleRuntimeAuthMode>('default');
|
||||
const buildRecommendRuntimeRequestOptions = useCallback(
|
||||
async (
|
||||
input: {
|
||||
kind?: RecommendRuntimeKind;
|
||||
embedded?: boolean;
|
||||
forcePublicRuntime?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
const shouldUseRuntimeOptions = Boolean(
|
||||
input.forcePublicRuntime ||
|
||||
input.embedded ||
|
||||
(input.kind && activeRecommendRuntimeKind === input.kind),
|
||||
);
|
||||
|
||||
return shouldUseRuntimeOptions
|
||||
? buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
},
|
||||
[activeRecommendRuntimeKind, authUi],
|
||||
);
|
||||
const buildPuzzleRuntimeRequestOptions = useCallback(
|
||||
() =>
|
||||
buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle',
|
||||
forcePublicRuntime: puzzleRuntimeAuthMode === 'isolated',
|
||||
}),
|
||||
[buildRecommendRuntimeRequestOptions, puzzleRuntimeAuthMode],
|
||||
);
|
||||
const [isPuzzleLeaderboardBusy, setIsPuzzleLeaderboardBusy] = useState(false);
|
||||
const submittedPuzzleLeaderboardKeysRef = useRef(new Set<string>());
|
||||
const puzzleStartInFlightKeyRef = useRef<string | null>(null);
|
||||
@@ -2910,10 +2942,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
} = publicGalleryFeeds;
|
||||
const recommendRuntimeEntries = useMemo(
|
||||
() =>
|
||||
buildPlatformRecommendedEntries({
|
||||
featuredEntries: filterGeneralPublicWorks(featuredGalleryEntries),
|
||||
latestEntries: filterGeneralPublicWorks(latestGalleryEntries),
|
||||
}),
|
||||
buildPlatformRecommendFeedEntries(
|
||||
featuredGalleryEntries,
|
||||
latestGalleryEntries,
|
||||
),
|
||||
[featuredGalleryEntries, latestGalleryEntries],
|
||||
);
|
||||
|
||||
@@ -6971,15 +7003,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileId: targetProfileId,
|
||||
mode: 'play' as const,
|
||||
};
|
||||
const runtimeGuestOptions =
|
||||
options.embedded || workDetail.summary.publishStatus === 'draft'
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
const runtimeRequestOptions = await buildRecommendRuntimeRequestOptions(
|
||||
{
|
||||
kind: 'visual-novel',
|
||||
embedded: options.embedded,
|
||||
},
|
||||
);
|
||||
const { run } = options.embedded
|
||||
? await startVisualNovelRun(
|
||||
targetProfileId,
|
||||
startRunPayload,
|
||||
runtimeGuestOptions,
|
||||
runtimeRequestOptions,
|
||||
)
|
||||
: await startVisualNovelRun(targetProfileId, startRunPayload);
|
||||
setVisualNovelWork(workDetail);
|
||||
@@ -7005,7 +7039,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsVisualNovelBusy,
|
||||
setSelectionStage,
|
||||
@@ -7027,14 +7061,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
setVisualNovelError(null);
|
||||
setIsVisualNovelBusy(true);
|
||||
try {
|
||||
const runtimeGuestOptions =
|
||||
activeRecommendRuntimeKind === 'visual-novel'
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'visual-novel',
|
||||
});
|
||||
const nextRun = await streamVisualNovelRuntimeAction(
|
||||
visualNovelRun.runId,
|
||||
payload,
|
||||
runtimeGuestOptions,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setVisualNovelRun(nextRun);
|
||||
} catch (error) {
|
||||
@@ -7046,8 +7080,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
isVisualNovelBusy,
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsVisualNovelBusy,
|
||||
@@ -7496,22 +7529,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
setJumpHopError(null);
|
||||
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions =
|
||||
options.embedded || shouldUseRecommendRuntimeGuestAuth(authUi)
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'jump-hop',
|
||||
embedded: options.embedded,
|
||||
forcePublicRuntime: shouldUseRecommendRuntimeGuestAuth(authUi),
|
||||
});
|
||||
setJumpHopRuntimeRequestOptions(
|
||||
runtimeGuestOptions.runtimeGuestToken?.trim()
|
||||
? {
|
||||
runtimeGuestToken: runtimeGuestOptions.runtimeGuestToken,
|
||||
authImpact: runtimeGuestOptions.authImpact,
|
||||
skipAuth: runtimeGuestOptions.skipAuth,
|
||||
skipRefresh: runtimeGuestOptions.skipRefresh,
|
||||
notifyAuthStateChange:
|
||||
runtimeGuestOptions.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized:
|
||||
runtimeGuestOptions.clearAuthOnUnauthorized,
|
||||
}
|
||||
Object.keys(runtimeRequestOptions).length > 0
|
||||
? runtimeRequestOptions
|
||||
: null,
|
||||
);
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
@@ -7521,7 +7547,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
.getWorkDetail(normalizedProfileId)
|
||||
.catch(() => null),
|
||||
jumpHopClient.startRun(normalizedProfileId, {
|
||||
...runtimeGuestOptions,
|
||||
...runtimeRequestOptions,
|
||||
runtimeMode: 'published',
|
||||
}),
|
||||
]);
|
||||
@@ -7548,7 +7574,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsJumpHopBusy(false);
|
||||
}
|
||||
},
|
||||
[authUi, setSelectionStage],
|
||||
[authUi, buildRecommendRuntimeRequestOptions, setSelectionStage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -7989,15 +8015,16 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleClearError(null);
|
||||
setPuzzleClearRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle-clear',
|
||||
embedded: options.embedded,
|
||||
});
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
puzzleClearClient
|
||||
.getRuntimeWorkDetail(normalizedProfileId)
|
||||
.catch(() => null),
|
||||
puzzleClearClient.startRun(normalizedProfileId, runtimeGuestOptions),
|
||||
puzzleClearClient.startRun(normalizedProfileId, runtimeRequestOptions),
|
||||
]);
|
||||
if (detail?.item) {
|
||||
setPuzzleClearWork(detail.item);
|
||||
@@ -8022,7 +8049,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsPuzzleClearBusy(false);
|
||||
}
|
||||
},
|
||||
[authUi, setSelectionStage],
|
||||
[buildRecommendRuntimeRequestOptions, setSelectionStage],
|
||||
);
|
||||
|
||||
const retryPuzzleClearLevelRun = useCallback(async () => {
|
||||
@@ -8047,7 +8074,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsPuzzleClearBusy(true);
|
||||
setPuzzleClearError(null);
|
||||
try {
|
||||
const response = await puzzleClearClient.retryLevel(runId);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle-clear',
|
||||
});
|
||||
const response = await puzzleClearClient.retryLevel(
|
||||
runId,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setPuzzleClearRun(response.run);
|
||||
} catch (error) {
|
||||
setPuzzleClearError(
|
||||
@@ -8059,6 +8093,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}, [
|
||||
puzzleClearRun,
|
||||
puzzleClearWork,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
setSelectionStage,
|
||||
startPuzzleClearTestRunFromProfile,
|
||||
]);
|
||||
@@ -8084,7 +8119,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsPuzzleClearBusy(true);
|
||||
setPuzzleClearError(null);
|
||||
try {
|
||||
const response = await puzzleClearClient.advanceNextLevel(runId);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle-clear',
|
||||
});
|
||||
const response = await puzzleClearClient.advanceNextLevel(
|
||||
runId,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setPuzzleClearRun(response.run);
|
||||
} catch (error) {
|
||||
setPuzzleClearError(
|
||||
@@ -8093,7 +8135,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
} finally {
|
||||
setIsPuzzleClearBusy(false);
|
||||
}
|
||||
}, [puzzleClearRun, puzzleClearWork, setSelectionStage]);
|
||||
}, [
|
||||
puzzleClearRun,
|
||||
puzzleClearWork,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
setSelectionStage,
|
||||
]);
|
||||
|
||||
const markPuzzleClearLevelTimeUp = useCallback(async () => {
|
||||
const runId = puzzleClearRun?.runId;
|
||||
@@ -8107,14 +8154,21 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await puzzleClearClient.markTimeUp(runId);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle-clear',
|
||||
});
|
||||
const response = await puzzleClearClient.markTimeUp(
|
||||
runId,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setPuzzleClearRun(response.run);
|
||||
} catch (error) {
|
||||
setPuzzleClearError(
|
||||
resolveRpgCreationErrorMessage(error, '同步拼消消倒计时失败。'),
|
||||
);
|
||||
}
|
||||
}, [puzzleClearRun]);
|
||||
}, [puzzleClearRun, buildRecommendRuntimeRequestOptions]);
|
||||
|
||||
const swapPuzzleClearCardsInRun = useCallback(
|
||||
async (payload: {
|
||||
@@ -8133,7 +8187,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await puzzleClearClient.swapCards(runId, payload);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'puzzle-clear',
|
||||
});
|
||||
const response = await puzzleClearClient.swapCards(
|
||||
runId,
|
||||
payload,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setPuzzleClearRun(response.run);
|
||||
} catch (error) {
|
||||
setPuzzleClearError(
|
||||
@@ -8141,7 +8203,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
}
|
||||
},
|
||||
[puzzleClearRun],
|
||||
[puzzleClearRun, buildRecommendRuntimeRequestOptions],
|
||||
);
|
||||
|
||||
const compileWoodenFishSession = useCallback(
|
||||
@@ -8493,16 +8555,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
setWoodenFishError(null);
|
||||
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'wooden-fish',
|
||||
embedded: options.embedded,
|
||||
});
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||
options.embedded
|
||||
? woodenFishClient.startRun(
|
||||
normalizedProfileId,
|
||||
runtimeGuestOptions,
|
||||
runtimeRequestOptions,
|
||||
)
|
||||
: woodenFishClient.startRun(normalizedProfileId),
|
||||
]);
|
||||
@@ -8529,7 +8592,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsWoodenFishBusy(false);
|
||||
}
|
||||
},
|
||||
[authUi, setSelectionStage],
|
||||
[buildRecommendRuntimeRequestOptions, setSelectionStage],
|
||||
);
|
||||
|
||||
const checkpointWoodenFishRuntimeRun = useCallback(
|
||||
@@ -8541,10 +8604,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (!runId) {
|
||||
return;
|
||||
}
|
||||
const response = await woodenFishClient.checkpointRun(runId, payload);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'wooden-fish',
|
||||
});
|
||||
const response = await woodenFishClient.checkpointRun(
|
||||
runId,
|
||||
payload,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setWoodenFishRun(response.run);
|
||||
},
|
||||
[woodenFishRun?.runId],
|
||||
[buildRecommendRuntimeRequestOptions, woodenFishRun?.runId],
|
||||
);
|
||||
|
||||
const executePuzzleAction = puzzleFlow.executeAction;
|
||||
@@ -9015,9 +9086,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
try {
|
||||
let runtimeProfile: Match3DWorkProfile | Match3DWorkSummary = profile;
|
||||
const canReadProtectedMatch3DDetail =
|
||||
!options.embedded || !shouldUseRecommendRuntimeGuestAuth(authUi);
|
||||
if (
|
||||
!hasMatch3DRuntimeAsset(profile.generatedItemAssets) ||
|
||||
!hasMatch3DRuntimeBackgroundAsset(profile)
|
||||
canReadProtectedMatch3DDetail &&
|
||||
(!hasMatch3DRuntimeAsset(profile.generatedItemAssets) ||
|
||||
!hasMatch3DRuntimeBackgroundAsset(profile))
|
||||
) {
|
||||
try {
|
||||
const { item } = await getMatch3DWorkDetail(profile.profileId);
|
||||
@@ -9043,12 +9117,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
runtimeProfile.generatedBackgroundAsset,
|
||||
{ expireSeconds: 300 },
|
||||
);
|
||||
const runtimeGuestOptions =
|
||||
options.authMode === 'isolated'
|
||||
? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS
|
||||
: await buildRecommendRuntimeAuthOptions(authUi, options.embedded);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'match3d',
|
||||
embedded: options.embedded,
|
||||
forcePublicRuntime: options.authMode === 'isolated',
|
||||
});
|
||||
const runtimeOptions = {
|
||||
...runtimeGuestOptions,
|
||||
...runtimeRequestOptions,
|
||||
...(typeof options.itemTypeCountOverride === 'number'
|
||||
? { itemTypeCountOverride: options.itemTypeCountOverride }
|
||||
: {}),
|
||||
@@ -9097,6 +9173,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
[
|
||||
isMatch3DBusy,
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
match3dFlow,
|
||||
resolveMatch3DErrorMessage,
|
||||
resolveMatch3DRuntimeAdapter,
|
||||
@@ -9120,12 +9197,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSquareHoleError(null);
|
||||
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'square-hole',
|
||||
embedded: options.embedded,
|
||||
});
|
||||
const { run } = options.embedded
|
||||
? await startSquareHoleRun(profile.profileId, runtimeGuestOptions)
|
||||
? await startSquareHoleRun(profile.profileId, runtimeRequestOptions)
|
||||
: await startSquareHoleRun(profile.profileId);
|
||||
setSquareHoleRun(run);
|
||||
setSquareHoleRuntimeReturnStage(returnStage);
|
||||
@@ -9157,7 +9235,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
isSquareHoleBusy,
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
resolveSquareHoleErrorMessage,
|
||||
setSelectionStage,
|
||||
setSquareHoleError,
|
||||
@@ -9275,14 +9353,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
bigFishInputInFlightRef.current = true;
|
||||
try {
|
||||
const runtimeGuestOptions =
|
||||
activeRecommendRuntimeKind === 'big-fish'
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'big-fish',
|
||||
});
|
||||
const { run } = await submitBigFishRuntimeInput(
|
||||
bigFishRun.runId,
|
||||
payload,
|
||||
runtimeGuestOptions,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setBigFishRun(run);
|
||||
} catch (error) {
|
||||
@@ -9294,8 +9372,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
bigFishRun,
|
||||
resolveBigFishErrorMessage,
|
||||
setBigFishError,
|
||||
@@ -9310,21 +9387,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const elapsedMs = Math.max(1_000, Date.now() - bigFishRuntimeStartedAt);
|
||||
setBigFishRuntimeStartedAt(null);
|
||||
const reportPromise =
|
||||
activeRecommendRuntimeKind === 'big-fish'
|
||||
? buildRecommendRuntimeAuthOptions(authUi, true).then(
|
||||
(runtimeAuthOptions) =>
|
||||
recordBigFishPlay(sessionId, { elapsedMs }, runtimeAuthOptions),
|
||||
)
|
||||
: recordBigFishPlay(sessionId, { elapsedMs });
|
||||
const reportPromise = buildRecommendRuntimeRequestOptions({
|
||||
kind: 'big-fish',
|
||||
}).then((runtimeRequestOptions) =>
|
||||
recordBigFishPlay(sessionId, { elapsedMs }, runtimeRequestOptions),
|
||||
);
|
||||
void reportPromise.catch((error) => {
|
||||
setBigFishError(
|
||||
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩时长失败。'),
|
||||
);
|
||||
});
|
||||
}, [
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
bigFishRun?.sessionId,
|
||||
bigFishRuntimeStartedAt,
|
||||
resolveBigFishErrorMessage,
|
||||
@@ -9642,14 +9716,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileId: currentLevel.profileId,
|
||||
levelId: resolvePuzzleRestartLevelId(currentRun, detailItem),
|
||||
};
|
||||
const runtimeGuestOptions =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const { run } =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
|
||||
: await startPuzzleRun(startRunPayload);
|
||||
const runtimeRequestOptions = await buildPuzzleRuntimeRequestOptions();
|
||||
const { run } = await startPuzzleRun(
|
||||
startRunPayload,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setSelectedPuzzleDetail(detailItem);
|
||||
puzzleRunRef.current = run;
|
||||
setPuzzleRun(run);
|
||||
@@ -9663,7 +9734,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}, [
|
||||
isPuzzleBusy,
|
||||
puzzleRun,
|
||||
puzzleRuntimeAuthMode,
|
||||
buildPuzzleRuntimeRequestOptions,
|
||||
resolvePuzzleErrorMessage,
|
||||
selectedPuzzleDetail,
|
||||
setIsPuzzleBusy,
|
||||
@@ -9775,16 +9846,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const submitLeaderboardPromise =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? buildRecommendRuntimeGuestOptions().then((runtimeGuestOptions) =>
|
||||
submitPuzzleLeaderboard(
|
||||
puzzleRun.runId,
|
||||
payload,
|
||||
runtimeGuestOptions,
|
||||
),
|
||||
)
|
||||
: submitPuzzleLeaderboard(puzzleRun.runId, payload);
|
||||
const submitLeaderboardPromise = buildPuzzleRuntimeRequestOptions().then(
|
||||
(runtimeRequestOptions) =>
|
||||
submitPuzzleLeaderboard(puzzleRun.runId, payload, runtimeRequestOptions),
|
||||
);
|
||||
|
||||
void submitLeaderboardPromise
|
||||
.then(({ run }) => {
|
||||
@@ -9808,7 +9873,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
authUi?.user?.displayName,
|
||||
platformBootstrap,
|
||||
puzzleRun,
|
||||
puzzleRuntimeAuthMode,
|
||||
buildPuzzleRuntimeRequestOptions,
|
||||
resolvePuzzleErrorMessage,
|
||||
setPuzzleError,
|
||||
]);
|
||||
@@ -9838,10 +9903,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeGuestOptions =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const runtimeRequestOptions = await buildPuzzleRuntimeRequestOptions();
|
||||
const targetProfileId = _target?.profileId?.trim() ?? '';
|
||||
if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) {
|
||||
const itemPromise =
|
||||
@@ -9850,18 +9912,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
: getPuzzleGalleryDetail(targetProfileId).then(
|
||||
(response) => response.item,
|
||||
);
|
||||
const advancePromise =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
{
|
||||
targetProfileId,
|
||||
},
|
||||
runtimeGuestOptions,
|
||||
)
|
||||
: advancePuzzleNextLevel(puzzleRun.runId, {
|
||||
targetProfileId,
|
||||
});
|
||||
const advancePromise = advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
{
|
||||
targetProfileId,
|
||||
},
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
const [{ run }, item] = await Promise.all([
|
||||
advancePromise,
|
||||
itemPromise,
|
||||
@@ -9878,14 +9935,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const { run } =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
{},
|
||||
runtimeGuestOptions,
|
||||
)
|
||||
: await advancePuzzleNextLevel(puzzleRun.runId, {});
|
||||
const { run } = await advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
{},
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
setPuzzleRun(run);
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
|
||||
@@ -9898,7 +9952,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
isPuzzleBusy,
|
||||
isPuzzleLeaderboardBusy,
|
||||
puzzleRun,
|
||||
puzzleRuntimeAuthMode,
|
||||
buildPuzzleRuntimeRequestOptions,
|
||||
resolvePuzzleErrorMessage,
|
||||
selectedPuzzleDetail,
|
||||
setIsPuzzleBusy,
|
||||
@@ -12436,12 +12490,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
setBigFishRuntimeReturnStage(returnStage);
|
||||
setBigFishRun(null);
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'big-fish',
|
||||
embedded: options.embedded,
|
||||
});
|
||||
const { run } = options.embedded
|
||||
? await startBigFishRuntimeRun(sessionId, runtimeGuestOptions)
|
||||
? await startBigFishRuntimeRun(sessionId, runtimeRequestOptions)
|
||||
: await startBigFishRuntimeRun(sessionId);
|
||||
setBigFishRuntimeStartedAt(Date.now());
|
||||
setBigFishRun(run);
|
||||
@@ -12452,7 +12507,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
}
|
||||
const recordPlayPromise = options.embedded
|
||||
? recordBigFishPlay(sessionId, { elapsedMs: 0 }, runtimeGuestOptions)
|
||||
? recordBigFishPlay(sessionId, { elapsedMs: 0 }, runtimeRequestOptions)
|
||||
: recordBigFishPlay(sessionId, { elapsedMs: 0 });
|
||||
void recordPlayPromise.catch((error) => {
|
||||
setBigFishError(
|
||||
@@ -12468,7 +12523,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
authUi,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
bigFishFlow,
|
||||
resolveBigFishErrorMessage,
|
||||
setBigFishError,
|
||||
@@ -12495,12 +12550,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
setBarkBattleRuntimeReturnStage(returnStage);
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({
|
||||
kind: 'bark-battle',
|
||||
embedded: options.embedded,
|
||||
});
|
||||
setBarkBattleRuntimeRequestOptions(
|
||||
Object.keys(runtimeRequestOptions).length > 0
|
||||
? runtimeRequestOptions
|
||||
: null,
|
||||
);
|
||||
const runResponse = options.embedded
|
||||
? await startBarkBattleRun(item.workId, {}, runtimeGuestOptions)
|
||||
? await startBarkBattleRun(item.workId, {}, runtimeRequestOptions)
|
||||
: await startBarkBattleRun(item.workId);
|
||||
void runResponse;
|
||||
selectionStageRef.current = 'bark-battle-runtime';
|
||||
@@ -12521,7 +12582,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[authUi, resolveBarkBattleErrorMessage, setSelectionStage],
|
||||
[
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
resolveBarkBattleErrorMessage,
|
||||
setSelectionStage,
|
||||
],
|
||||
);
|
||||
|
||||
const startSelectedPublicWork = useCallback(() => {
|
||||
@@ -12975,10 +13040,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
match3dFlow.setIsBusy(true);
|
||||
setMatch3DError(null);
|
||||
void resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
)
|
||||
.restartRun(match3dRun.runId)
|
||||
void buildRecommendRuntimeRequestOptions({ kind: 'match3d' })
|
||||
.then((runtimeRequestOptions) =>
|
||||
resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
).restartRun(match3dRun.runId, runtimeRequestOptions),
|
||||
)
|
||||
.then(({ run }) => {
|
||||
setMatch3DRun(run);
|
||||
})
|
||||
@@ -12992,24 +13059,28 @@ export function PlatformEntryFlowShellImpl({
|
||||
});
|
||||
}}
|
||||
onOptimisticRunChange={setMatch3DRun}
|
||||
onClickItem={(payload) => {
|
||||
onClickItem={async (payload) => {
|
||||
const runId = payload.runId ?? match3dRun?.runId;
|
||||
if (!runId) {
|
||||
return Promise.reject(new Error('抓大鹅运行态缺少 runId。'));
|
||||
}
|
||||
const runtimeRequestOptions =
|
||||
await buildRecommendRuntimeRequestOptions({ kind: 'match3d' });
|
||||
return resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
).clickItem(runId, payload);
|
||||
).clickItem(runId, payload, runtimeRequestOptions);
|
||||
}}
|
||||
onTimeExpired={() => {
|
||||
if (!match3dRun?.runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
void resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
)
|
||||
.finishTimeUp(match3dRun.runId)
|
||||
void buildRecommendRuntimeRequestOptions({ kind: 'match3d' })
|
||||
.then((runtimeRequestOptions) =>
|
||||
resolveMatch3DRuntimeAdapter(
|
||||
activeMatch3DRuntimeProfile?.profileId,
|
||||
).finishTimeUp(match3dRun.runId, runtimeRequestOptions),
|
||||
)
|
||||
.then(({ run }) => {
|
||||
setMatch3DRun(run);
|
||||
})
|
||||
@@ -13143,9 +13214,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
squareHoleRun?.runId &&
|
||||
squareHoleRun.status.toLowerCase() === 'running'
|
||||
) {
|
||||
void stopSquareHoleRun(squareHoleRun.runId).catch(
|
||||
() => undefined,
|
||||
);
|
||||
void buildRecommendRuntimeRequestOptions({
|
||||
kind: 'square-hole',
|
||||
})
|
||||
.then((runtimeRequestOptions) =>
|
||||
stopSquareHoleRun(
|
||||
squareHoleRun.runId,
|
||||
undefined,
|
||||
runtimeRequestOptions,
|
||||
),
|
||||
)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
setActiveRecommendRuntimeKind(null);
|
||||
}}
|
||||
@@ -13156,7 +13235,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
squareHoleFlow.setIsBusy(true);
|
||||
setSquareHoleError(null);
|
||||
void restartSquareHoleRun(squareHoleRun.runId)
|
||||
void buildRecommendRuntimeRequestOptions({
|
||||
kind: 'square-hole',
|
||||
})
|
||||
.then((runtimeRequestOptions) =>
|
||||
restartSquareHoleRun(squareHoleRun.runId, runtimeRequestOptions),
|
||||
)
|
||||
.then(({ run }) => {
|
||||
setSquareHoleRun(run);
|
||||
})
|
||||
@@ -13178,14 +13262,26 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (!runId) {
|
||||
return Promise.reject(new Error('方洞挑战运行态缺少 runId。'));
|
||||
}
|
||||
return dropSquareHoleShape(runId, payload);
|
||||
return buildRecommendRuntimeRequestOptions({
|
||||
kind: 'square-hole',
|
||||
}).then((runtimeRequestOptions) =>
|
||||
dropSquareHoleShape(runId, payload, runtimeRequestOptions),
|
||||
);
|
||||
}}
|
||||
onTimeExpired={() => {
|
||||
if (!squareHoleRun?.runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
void finishSquareHoleTimeUp(squareHoleRun.runId)
|
||||
void buildRecommendRuntimeRequestOptions({
|
||||
kind: 'square-hole',
|
||||
})
|
||||
.then((runtimeRequestOptions) =>
|
||||
finishSquareHoleTimeUp(
|
||||
squareHoleRun.runId,
|
||||
runtimeRequestOptions,
|
||||
),
|
||||
)
|
||||
.then(({ run }) => {
|
||||
setSquareHoleRun(run);
|
||||
})
|
||||
@@ -13230,6 +13326,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
workId={barkBattlePublishedConfig.workId}
|
||||
publishedConfig={barkBattlePublishedConfig}
|
||||
runtimeMode="published"
|
||||
runtimeRequestOptions={barkBattleRuntimeRequestOptions ?? undefined}
|
||||
onExit={() => {
|
||||
setActiveRecommendRuntimeKind(null);
|
||||
}}
|
||||
@@ -13260,7 +13357,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
activeRecommendEntryKey,
|
||||
activeRecommendRuntimeKind,
|
||||
barkBattlePublishedConfig,
|
||||
barkBattleRuntimeRequestOptions,
|
||||
babyObjectMatchDraft,
|
||||
buildRecommendRuntimeRequestOptions,
|
||||
bigFishError,
|
||||
bigFishRun,
|
||||
bigFishRuntimeShare,
|
||||
@@ -13297,6 +13396,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
recommendRuntimeEntries,
|
||||
remodelCurrentPuzzleRuntimeWork,
|
||||
resolveMatch3DErrorMessage,
|
||||
resolveMatch3DRuntimeAdapter,
|
||||
resolveSquareHoleErrorMessage,
|
||||
reportBigFishObservedPlayTime,
|
||||
restartBigFishRun,
|
||||
@@ -16663,6 +16763,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
workId={barkBattlePublishedConfig.workId}
|
||||
publishedConfig={barkBattlePublishedConfig}
|
||||
runtimeMode={barkBattleRuntimeMode}
|
||||
runtimeRequestOptions={
|
||||
barkBattleRuntimeRequestOptions ?? undefined
|
||||
}
|
||||
onExit={() => {
|
||||
if (
|
||||
barkBattleRuntimeReturnStage === 'bark-battle-result' &&
|
||||
|
||||
@@ -724,7 +724,7 @@ test('顶部不显示作者,关卡标题和倒计时使用游戏铭牌结构',
|
||||
const levelLogo = screen.getByTestId(
|
||||
'puzzle-runtime-level-logo',
|
||||
) as HTMLImageElement;
|
||||
expect(levelLogo.getAttribute('src')).toContain('logo.png');
|
||||
expect(levelLogo.getAttribute('src')).toContain('logo-runtime-hud.webp');
|
||||
expect(levelLogo.closest('.puzzle-runtime-level-logo')).toBeTruthy();
|
||||
expect(document.querySelector('.puzzle-runtime-level-mascot')).toBeNull();
|
||||
expect(timer.closest('.puzzle-runtime-timer-card')).toBeTruthy();
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import puzzleLevelLogo from '../../../media/logo.png';
|
||||
import puzzleLevelLogo from '../../../media/logo-runtime-hud.webp';
|
||||
import type {
|
||||
DragPuzzlePieceRequest,
|
||||
PuzzleBoardSnapshot,
|
||||
|
||||
@@ -7810,6 +7810,113 @@ test('logged out home recommendation next starts the next puzzle work', async ()
|
||||
});
|
||||
});
|
||||
|
||||
test('home recommendation next follows the same scored queue shown in preview', async () => {
|
||||
const user = userEvent.setup();
|
||||
const quietWork = {
|
||||
workId: 'puzzle-work-public-quiet',
|
||||
profileId: 'puzzle-profile-public-quiet',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: 'puzzle-session-public-quiet',
|
||||
authorDisplayName: '拼图作者',
|
||||
levelName: '安静拼图',
|
||||
summary: '列表里排在前面但热度较低。',
|
||||
themeTags: ['安静', '拼图'],
|
||||
coverImageSrc: null,
|
||||
coverAssetId: null,
|
||||
publicationStatus: 'published',
|
||||
updatedAt: '2026-04-25T10:00:00.000Z',
|
||||
publishedAt: '2026-04-25T10:00:00.000Z',
|
||||
playCount: 40,
|
||||
likeCount: 0,
|
||||
publishReady: true,
|
||||
} satisfies PuzzleWorkSummary;
|
||||
const hotWork = {
|
||||
...quietWork,
|
||||
workId: 'puzzle-work-public-hot',
|
||||
profileId: 'puzzle-profile-public-hot',
|
||||
sourceSessionId: 'puzzle-session-public-hot',
|
||||
levelName: '热门拼图',
|
||||
summary: '推荐评分更高,应该先展示。',
|
||||
playCount: 120,
|
||||
updatedAt: '2026-04-25T09:00:00.000Z',
|
||||
publishedAt: '2026-04-25T09:00:00.000Z',
|
||||
} satisfies PuzzleWorkSummary;
|
||||
const middleWork = {
|
||||
...quietWork,
|
||||
workId: 'puzzle-work-public-middle',
|
||||
profileId: 'puzzle-profile-public-middle',
|
||||
sourceSessionId: 'puzzle-session-public-middle',
|
||||
levelName: '中间拼图',
|
||||
summary: '推荐评分排在后面。',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-04-25T08:00:00.000Z',
|
||||
publishedAt: '2026-04-25T08:00:00.000Z',
|
||||
} satisfies PuzzleWorkSummary;
|
||||
|
||||
vi.mocked(listPuzzleGallery).mockResolvedValue({
|
||||
items: [quietWork, hotWork, middleWork],
|
||||
});
|
||||
vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
|
||||
item:
|
||||
profileId === hotWork.profileId
|
||||
? hotWork
|
||||
: profileId === middleWork.profileId
|
||||
? middleWork
|
||||
: quietWork,
|
||||
}));
|
||||
vi.mocked(startPuzzleRun).mockImplementation(async (payload) => ({
|
||||
run: buildMockPuzzleRun(
|
||||
payload.profileId,
|
||||
payload.profileId === hotWork.profileId
|
||||
? hotWork.levelName
|
||||
: payload.profileId === middleWork.profileId
|
||||
? middleWork.levelName
|
||||
: quietWork.levelName,
|
||||
),
|
||||
}));
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: hotWork.profileId,
|
||||
levelId: null,
|
||||
},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
await screen.findByLabelText('热门拼图 作品信息', undefined, {
|
||||
timeout: 3000,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
const nextPreview = document.querySelector(
|
||||
'.platform-recommend-swipe-page--next',
|
||||
);
|
||||
expect(nextPreview).toBeTruthy();
|
||||
expect(
|
||||
within(nextPreview as HTMLElement).getByLabelText('安静拼图 作品信息'),
|
||||
).toBeTruthy();
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: '下一个' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: quietWork.profileId,
|
||||
levelId: null,
|
||||
},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
await screen.findByLabelText('安静拼图 作品信息', undefined, {
|
||||
timeout: 3000,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('home recommendation keeps cover while switching during a pending puzzle start', async () => {
|
||||
const user = userEvent.setup();
|
||||
const firstWork = {
|
||||
@@ -8142,6 +8249,54 @@ test('home recommendation Match3D runtime keeps profile generated models when ca
|
||||
});
|
||||
});
|
||||
|
||||
test('logged out home recommendation Match3D runtime skips protected detail and starts with guest auth', async () => {
|
||||
const match3dCard: Match3DWorkSummary = {
|
||||
workId: 'match3d-work-card-guest',
|
||||
profileId: 'match3d-profile-card-guest',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: 'match3d-session-card-guest',
|
||||
gameName: '游客抓大鹅',
|
||||
themeText: '游客果园',
|
||||
summary: '游客可直接游玩。',
|
||||
tags: ['果园', '抓大鹅'],
|
||||
coverImageSrc: null,
|
||||
referenceImageSrc: null,
|
||||
clearCount: 3,
|
||||
difficulty: 5,
|
||||
publicationStatus: 'published',
|
||||
playCount: 3,
|
||||
updatedAt: '2026-04-25T10:30:00.000Z',
|
||||
publishedAt: '2026-04-25T10:30:00.000Z',
|
||||
publishReady: true,
|
||||
generatedItemAssets: [],
|
||||
};
|
||||
|
||||
vi.mocked(listMatch3DGallery).mockResolvedValue({
|
||||
items: [match3dCard],
|
||||
});
|
||||
vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
|
||||
item: match3dCard,
|
||||
});
|
||||
match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
|
||||
run: buildMockMatch3DRun(match3dCard.profileId),
|
||||
});
|
||||
|
||||
render(<TestWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-card-guest',
|
||||
expect.objectContaining({
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
skipRefresh: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
|
||||
'match3d-profile-card-guest',
|
||||
);
|
||||
});
|
||||
|
||||
test('home recommendation Match3D runtime keeps image, music and UI assets without requiring models', async () => {
|
||||
const match3dCard: Match3DWorkSummary = {
|
||||
workId: 'match3d-work-card-image-only',
|
||||
@@ -9357,6 +9512,7 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
elapsedMs: 18_000,
|
||||
nickname: '测试玩家',
|
||||
},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9377,6 +9533,7 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||
clearedFirstLevel.runId,
|
||||
{},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -9539,6 +9696,7 @@ test('formal puzzle similar work keeps current run level progression', async ()
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||
clearedThirdLevel.runId,
|
||||
{ targetProfileId: 'puzzle-profile-similar-2' },
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(startPuzzleRun).not.toHaveBeenCalled();
|
||||
|
||||
@@ -189,6 +189,27 @@ test('public gallery ViewModel builds recommend feed from general public entries
|
||||
).toEqual([latestPuzzle]);
|
||||
});
|
||||
|
||||
test('public gallery ViewModel keeps recommend feed in scored runtime order', () => {
|
||||
const quietPuzzle = buildPuzzleEntry({
|
||||
profileId: 'quiet',
|
||||
worldName: '安静拼图',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-03T00:00:00.000Z',
|
||||
publishedAt: '2026-05-03T00:00:00.000Z',
|
||||
});
|
||||
const hotPuzzle = buildPuzzleEntry({
|
||||
profileId: 'hot',
|
||||
worldName: '热门拼图',
|
||||
playCount: 120,
|
||||
updatedAt: '2026-05-02T00:00:00.000Z',
|
||||
publishedAt: '2026-05-02T00:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(buildPlatformRecommendFeedEntries([], [quietPuzzle, hotPuzzle])).toEqual(
|
||||
[hotPuzzle, quietPuzzle],
|
||||
);
|
||||
});
|
||||
|
||||
test('public gallery ViewModel selects recommend feed window with wraparound neighbors', () => {
|
||||
const firstEntry = buildPuzzleEntry({ profileId: 'first' });
|
||||
const secondEntry = buildJumpHopEntry({ profileId: 'second' });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { filterGeneralPublicWorks } from '../platform-entry/platformEdutainmentVisibility';
|
||||
import { getPlatformPublicGalleryEntryKey } from '../platform-entry/platformPublicGalleryFlow';
|
||||
import { buildPlatformRecommendedEntries } from '../platform-entry/platformRecommendation';
|
||||
import {
|
||||
buildPlatformWorldDisplayTags,
|
||||
isBarkBattleGalleryEntry,
|
||||
@@ -146,9 +147,10 @@ export function buildPlatformRecommendFeedEntries(
|
||||
featuredEntries: PlatformPublicGalleryCard[],
|
||||
latestEntries: PlatformPublicGalleryCard[],
|
||||
) {
|
||||
return dedupePlatformPublicGalleryEntries(
|
||||
filterGeneralPublicWorks([...featuredEntries, ...latestEntries]),
|
||||
);
|
||||
return buildPlatformRecommendedEntries({
|
||||
featuredEntries: filterGeneralPublicWorks(featuredEntries),
|
||||
latestEntries: filterGeneralPublicWorks(latestEntries),
|
||||
});
|
||||
}
|
||||
|
||||
export function selectAdjacentPlatformRecommendEntry(
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import woodenFishRuntimeLogo from '../../../media/logo.png';
|
||||
import woodenFishRuntimeLogo from '../../../media/logo-runtime-hud.webp';
|
||||
import type {
|
||||
WoodenFishRuntimeRunSnapshotResponse,
|
||||
WoodenFishWordCounter,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
BarkBattleServerResult,
|
||||
} from '../../../../packages/shared/src/contracts/barkBattle';
|
||||
import {
|
||||
type BarkBattleRuntimeRequestOptions,
|
||||
finishBarkBattleRun,
|
||||
startBarkBattleRun,
|
||||
} from '../../../services/bark-battle-runtime';
|
||||
@@ -31,6 +32,7 @@ type BarkBattleRuntimeShellProps = {
|
||||
workId?: string;
|
||||
publishedConfig?: BarkBattlePublishedConfig | null;
|
||||
runtimeMode?: BarkBattleRuntimeMode;
|
||||
runtimeRequestOptions?: BarkBattleRuntimeRequestOptions;
|
||||
onExit?: () => void;
|
||||
};
|
||||
|
||||
@@ -266,6 +268,7 @@ export function BarkBattleRuntimeShell({
|
||||
workId,
|
||||
publishedConfig,
|
||||
runtimeMode = 'draft',
|
||||
runtimeRequestOptions,
|
||||
onExit,
|
||||
}: BarkBattleRuntimeShellProps) {
|
||||
const initialConfig = useMemo(
|
||||
@@ -404,20 +407,24 @@ export function BarkBattleRuntimeShell({
|
||||
0,
|
||||
runtimeConfigRef.current.roundDurationMs - nextSnapshot.remainingMs,
|
||||
);
|
||||
void finishBarkBattleRun(activeRun.runId, {
|
||||
runId: activeRun.runId,
|
||||
runToken: activeRun.runToken,
|
||||
workId: activeRun.workId,
|
||||
configVersion: activeRun.configVersion,
|
||||
rulesetVersion: activeRun.rulesetVersion,
|
||||
difficultyPreset: activeRun.difficultyPreset,
|
||||
clientStartedAt: startedAt,
|
||||
clientFinishedAt: finishedAt,
|
||||
durationMs,
|
||||
derivedMetrics: buildDerivedMetrics(),
|
||||
clientResult: resolveClientResult(nextSnapshot.winner),
|
||||
clientRuntimeVersion: BARK_BATTLE_CLIENT_RUNTIME_VERSION,
|
||||
})
|
||||
void finishBarkBattleRun(
|
||||
activeRun.runId,
|
||||
{
|
||||
runId: activeRun.runId,
|
||||
runToken: activeRun.runToken,
|
||||
workId: activeRun.workId,
|
||||
configVersion: activeRun.configVersion,
|
||||
rulesetVersion: activeRun.rulesetVersion,
|
||||
difficultyPreset: activeRun.difficultyPreset,
|
||||
clientStartedAt: startedAt,
|
||||
clientFinishedAt: finishedAt,
|
||||
durationMs,
|
||||
derivedMetrics: buildDerivedMetrics(),
|
||||
clientResult: resolveClientResult(nextSnapshot.winner),
|
||||
clientRuntimeVersion: BARK_BATTLE_CLIENT_RUNTIME_VERSION,
|
||||
},
|
||||
runtimeRequestOptions,
|
||||
)
|
||||
.then(() => {
|
||||
appendDebugEvent('正式成绩已提交');
|
||||
})
|
||||
@@ -433,6 +440,7 @@ export function BarkBattleRuntimeShell({
|
||||
buildDerivedMetrics,
|
||||
controller,
|
||||
isPublishedRuntime,
|
||||
runtimeRequestOptions,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -447,14 +455,18 @@ export function BarkBattleRuntimeShell({
|
||||
pendingRunStartRef.current = (async () => {
|
||||
try {
|
||||
setRuntimeError(null);
|
||||
const started = await startBarkBattleRun(replacementConfig.workId, {
|
||||
// 中文注释:公开广场摘要可能滞后于已发布运行配置;正式开局以服务端当前发布快照为准。
|
||||
sourceRoute:
|
||||
typeof window === 'undefined'
|
||||
? 'bark-battle-runtime'
|
||||
: window.location.pathname,
|
||||
clientRuntimeVersion: BARK_BATTLE_CLIENT_RUNTIME_VERSION,
|
||||
});
|
||||
const started = await startBarkBattleRun(
|
||||
replacementConfig.workId,
|
||||
{
|
||||
// 中文注释:公开广场摘要可能滞后于已发布运行配置;正式开局以服务端当前发布快照为准。
|
||||
sourceRoute:
|
||||
typeof window === 'undefined'
|
||||
? 'bark-battle-runtime'
|
||||
: window.location.pathname,
|
||||
clientRuntimeVersion: BARK_BATTLE_CLIENT_RUNTIME_VERSION,
|
||||
},
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
const serverRuntimeConfig = buildRuntimeConfigFromServerConfig(
|
||||
started.runtimeConfig,
|
||||
);
|
||||
@@ -491,7 +503,13 @@ export function BarkBattleRuntimeShell({
|
||||
})();
|
||||
}
|
||||
return pendingRunStartRef.current ?? Promise.resolve(true);
|
||||
}, [appendDebugEvent, controller, isPublishedRuntime, replacementConfig]);
|
||||
}, [
|
||||
appendDebugEvent,
|
||||
controller,
|
||||
isPublishedRuntime,
|
||||
replacementConfig,
|
||||
runtimeRequestOptions,
|
||||
]);
|
||||
|
||||
const syncSnapshot = useCallback(() => {
|
||||
const nextSnapshot = controller.getSnapshot();
|
||||
|
||||
@@ -58,10 +58,14 @@ export function startBigFishRun(
|
||||
});
|
||||
}
|
||||
|
||||
export function getBigFishRun(runId: string) {
|
||||
export function getBigFishRun(
|
||||
runId: string,
|
||||
options: BigFishRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<BigFishRunResponse>({
|
||||
url: buildRuntimeApiPath(BIG_FISH_RUNTIME_API_BASE, 'runs', runId),
|
||||
fallbackMessage: '读取大鱼吃小鱼玩法失败',
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -139,4 +139,14 @@ test('jump hop work detail preserves flattened back button asset', async () => {
|
||||
const response = await jumpHopClient.getWorkDetail('profile-1');
|
||||
|
||||
expect(response.item.backButtonAsset).toEqual(backButtonAsset);
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/jump-hop/works/profile-1',
|
||||
{ method: 'GET' },
|
||||
'读取跳一跳作品详情失败',
|
||||
expect.objectContaining({
|
||||
retry: expect.objectContaining({ maxRetries: 1 }),
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
JumpHopWorkSummaryResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import {
|
||||
type ApiRequestOptions,
|
||||
type ApiRetryOptions,
|
||||
requestJson,
|
||||
} from '../apiClient';
|
||||
@@ -196,10 +197,19 @@ export async function getJumpHopWorkDetail(
|
||||
options.audience === 'creation'
|
||||
? JUMP_HOP_WORKS_API_BASE
|
||||
: `${JUMP_HOP_RUNTIME_API_BASE}/works`;
|
||||
const requestOptions: ApiRequestOptions =
|
||||
options.audience === 'creation'
|
||||
? {}
|
||||
: {
|
||||
retry: JUMP_HOP_RUNTIME_READ_RETRY,
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
};
|
||||
const response = await requestJson<JumpHopWorkDetailResponse>(
|
||||
`${base}/${encodeURIComponent(profileId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取跳一跳作品详情失败',
|
||||
requestOptions,
|
||||
);
|
||||
return normalizeJumpHopWorkDetailResponse(response);
|
||||
}
|
||||
|
||||
@@ -64,30 +64,63 @@ test('server Match3D runtime adapter forwards the full runtime seam lazily', asy
|
||||
stopRun: vi.fn().mockResolvedValue(stopResponse),
|
||||
};
|
||||
const adapter = createServerMatch3DRuntimeAdapter(dependencies);
|
||||
const runtimeRequestOptions = {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
skipRefresh: true,
|
||||
};
|
||||
|
||||
expect(await adapter.startRun('server-profile-1', { skipRefresh: true })).toBe(
|
||||
startResponse,
|
||||
expect(
|
||||
await adapter.startRun('server-profile-1', runtimeRequestOptions),
|
||||
).toBe(startResponse);
|
||||
expect(await adapter.getRun('server-run-start', runtimeRequestOptions)).toBe(
|
||||
getResponse,
|
||||
);
|
||||
expect(await adapter.getRun('server-run-start')).toBe(getResponse);
|
||||
expect(await adapter.clickItem('server-run-start', clickPayload)).toEqual({
|
||||
expect(
|
||||
await adapter.clickItem(
|
||||
'server-run-start',
|
||||
clickPayload,
|
||||
runtimeRequestOptions,
|
||||
),
|
||||
).toEqual({
|
||||
status: 'Accepted',
|
||||
run: buildMockRun('server-run-click'),
|
||||
});
|
||||
expect(await adapter.restartRun('server-run-start')).toBe(restartResponse);
|
||||
expect(await adapter.stopRun('server-run-restart')).toBe(stopResponse);
|
||||
expect(await adapter.finishTimeUp('server-run-start')).toBe(finishResponse);
|
||||
expect(
|
||||
await adapter.restartRun('server-run-start', runtimeRequestOptions),
|
||||
).toBe(restartResponse);
|
||||
expect(await adapter.stopRun('server-run-restart', runtimeRequestOptions)).toBe(
|
||||
stopResponse,
|
||||
);
|
||||
expect(
|
||||
await adapter.finishTimeUp('server-run-start', runtimeRequestOptions),
|
||||
).toBe(finishResponse);
|
||||
|
||||
expect(dependencies.startRun).toHaveBeenCalledWith('server-profile-1', {
|
||||
skipRefresh: true,
|
||||
});
|
||||
expect(dependencies.getRun).toHaveBeenCalledWith('server-run-start');
|
||||
expect(dependencies.startRun).toHaveBeenCalledWith(
|
||||
'server-profile-1',
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
expect(dependencies.getRun).toHaveBeenCalledWith(
|
||||
'server-run-start',
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
expect(dependencies.clickItem).toHaveBeenCalledWith(
|
||||
'server-run-start',
|
||||
clickPayload,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
expect(dependencies.restartRun).toHaveBeenCalledWith(
|
||||
'server-run-start',
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
expect(dependencies.stopRun).toHaveBeenCalledWith(
|
||||
'server-run-restart',
|
||||
undefined,
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
expect(dependencies.finishTimeUp).toHaveBeenCalledWith(
|
||||
'server-run-start',
|
||||
runtimeRequestOptions,
|
||||
);
|
||||
expect(dependencies.restartRun).toHaveBeenCalledWith('server-run-start');
|
||||
expect(dependencies.stopRun).toHaveBeenCalledWith('server-run-restart');
|
||||
expect(dependencies.finishTimeUp).toHaveBeenCalledWith('server-run-start');
|
||||
});
|
||||
|
||||
test('local Match3D runtime adapter exposes the same runtime seam as the server client', async () => {
|
||||
|
||||
@@ -24,14 +24,27 @@ export type Match3DRuntimeAdapter = {
|
||||
profileId: string,
|
||||
options?: Match3DRuntimeRequestOptions,
|
||||
) => Promise<Match3DRunResponse>;
|
||||
getRun: (runId: string) => Promise<Match3DRunResponse>;
|
||||
getRun: (
|
||||
runId: string,
|
||||
options?: Match3DRuntimeRequestOptions,
|
||||
) => Promise<Match3DRunResponse>;
|
||||
clickItem: (
|
||||
runId: string,
|
||||
payload: Match3DClickItemRequest,
|
||||
options?: Match3DRuntimeRequestOptions,
|
||||
) => Promise<Match3DClickItemResult>;
|
||||
restartRun: (runId: string) => Promise<Match3DRunResponse>;
|
||||
stopRun: (runId: string) => Promise<Match3DRunResponse>;
|
||||
finishTimeUp: (runId: string) => Promise<Match3DRunResponse>;
|
||||
restartRun: (
|
||||
runId: string,
|
||||
options?: Match3DRuntimeRequestOptions,
|
||||
) => Promise<Match3DRunResponse>;
|
||||
stopRun: (
|
||||
runId: string,
|
||||
options?: Match3DRuntimeRequestOptions,
|
||||
) => Promise<Match3DRunResponse>;
|
||||
finishTimeUp: (
|
||||
runId: string,
|
||||
options?: Match3DRuntimeRequestOptions,
|
||||
) => Promise<Match3DRunResponse>;
|
||||
};
|
||||
|
||||
export type LocalMatch3DRuntimeAdapterOptions = {
|
||||
@@ -63,12 +76,13 @@ export function createServerMatch3DRuntimeAdapter(
|
||||
defaultServerMatch3DRuntimeAdapterDependencies,
|
||||
): Match3DRuntimeAdapter {
|
||||
return {
|
||||
clickItem: (runId, payload) => dependencies.clickItem(runId, payload),
|
||||
finishTimeUp: (runId) => dependencies.finishTimeUp(runId),
|
||||
getRun: (runId) => dependencies.getRun(runId),
|
||||
restartRun: (runId) => dependencies.restartRun(runId),
|
||||
clickItem: (runId, payload, options) =>
|
||||
dependencies.clickItem(runId, payload, options),
|
||||
finishTimeUp: (runId, options) => dependencies.finishTimeUp(runId, options),
|
||||
getRun: (runId, options) => dependencies.getRun(runId, options),
|
||||
restartRun: (runId, options) => dependencies.restartRun(runId, options),
|
||||
startRun: (profileId, options) => dependencies.startRun(profileId, options),
|
||||
stopRun: (runId) => dependencies.stopRun(runId),
|
||||
stopRun: (runId, options) => dependencies.stopRun(runId, undefined, options),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -89,11 +89,15 @@ export function startMatch3DRun(
|
||||
/**
|
||||
* 读取抓大鹅运行态快照。
|
||||
*/
|
||||
export function getMatch3DRun(runId: string) {
|
||||
export function getMatch3DRun(
|
||||
runId: string,
|
||||
options: Match3DRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<Match3DRunResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'runs', runId),
|
||||
fallbackMessage: '读取抓大鹅运行快照失败',
|
||||
retry: MATCH3D_RUNTIME_READ_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,6 +107,7 @@ export function getMatch3DRun(runId: string) {
|
||||
export async function clickMatch3DItem(
|
||||
runId: string,
|
||||
payload: Match3DClickItemRequest,
|
||||
options: Match3DRuntimeRequestOptions = {},
|
||||
) {
|
||||
const response = await requestRuntimeJson<Match3DClickResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'runs', runId, 'click'),
|
||||
@@ -113,6 +118,7 @@ export async function clickMatch3DItem(
|
||||
},
|
||||
fallbackMessage: '确认抓大鹅点击失败',
|
||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
|
||||
return mapClickConfirmation(payload, response.confirmation);
|
||||
@@ -126,6 +132,7 @@ export function stopMatch3DRun(
|
||||
payload: StopMatch3DRunRequest = {
|
||||
clientActionId: `match3d-stop-${Date.now()}`,
|
||||
},
|
||||
options: Match3DRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<Match3DRunResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'runs', runId, 'stop'),
|
||||
@@ -133,30 +140,39 @@ export function stopMatch3DRun(
|
||||
jsonBody: payload,
|
||||
fallbackMessage: '停止抓大鹅玩法失败',
|
||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于当前 run 重开一局。
|
||||
*/
|
||||
export function restartMatch3DRun(runId: string) {
|
||||
export function restartMatch3DRun(
|
||||
runId: string,
|
||||
options: Match3DRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<Match3DRunResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'runs', runId, 'restart'),
|
||||
method: 'POST',
|
||||
fallbackMessage: '重新开始抓大鹅玩法失败',
|
||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端倒计时归零后通知后端确认失败状态。
|
||||
*/
|
||||
export function finishMatch3DTimeUp(runId: string) {
|
||||
export function finishMatch3DTimeUp(
|
||||
runId: string,
|
||||
options: Match3DRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<Match3DRunResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'runs', runId, 'time-up'),
|
||||
method: 'POST',
|
||||
fallbackMessage: '同步抓大鹅倒计时失败',
|
||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('puzzleRuntimeClient', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps pause requests on account auth options instead of guest auth', async () => {
|
||||
it('uses runtime guest auth for pause requests when provided', async () => {
|
||||
await updatePuzzleRunPause(
|
||||
'run/1',
|
||||
{ paused: true },
|
||||
@@ -76,17 +76,19 @@ describe('puzzleRuntimeClient', () => {
|
||||
expect(init).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer runtime-guest-token',
|
||||
},
|
||||
body: JSON.stringify({ paused: true }),
|
||||
}),
|
||||
);
|
||||
expect(init.headers).not.toHaveProperty('Authorization');
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
authImpact: 'local',
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
}),
|
||||
);
|
||||
expect(options).not.toMatchObject({ skipAuth: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,10 +8,7 @@ import type {
|
||||
UpdatePuzzleRuntimePauseRequest,
|
||||
UsePuzzleRuntimePropRequest,
|
||||
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
||||
import {
|
||||
type ApiRetryOptions,
|
||||
requestJson,
|
||||
} from '../apiClient';
|
||||
import { type ApiRetryOptions } from '../apiClient';
|
||||
import { type RuntimeGuestRequestOptions } from '../runtimeGuestAuth';
|
||||
import { buildRuntimeApiPath, requestRuntimeJson } from '../runtimeRequest';
|
||||
|
||||
@@ -52,11 +49,15 @@ export async function startPuzzleRun(
|
||||
/**
|
||||
* 读取拼图运行态快照。
|
||||
*/
|
||||
export async function getPuzzleRun(runId: string) {
|
||||
export async function getPuzzleRun(
|
||||
runId: string,
|
||||
options: PuzzleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<PuzzleRunResponse>({
|
||||
url: buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId),
|
||||
fallbackMessage: '读取拼图运行快照失败',
|
||||
retry: PUZZLE_RUNTIME_READ_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,6 +67,7 @@ export async function getPuzzleRun(runId: string) {
|
||||
export async function swapPuzzlePieces(
|
||||
runId: string,
|
||||
payload: SwapPuzzlePiecesRequest,
|
||||
options: PuzzleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<PuzzleRunResponse>({
|
||||
url: buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'swap'),
|
||||
@@ -73,6 +75,7 @@ export async function swapPuzzlePieces(
|
||||
jsonBody: payload,
|
||||
fallbackMessage: '交换拼图块失败',
|
||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,6 +85,7 @@ export async function swapPuzzlePieces(
|
||||
export async function dragPuzzlePieceOrGroup(
|
||||
runId: string,
|
||||
payload: DragPuzzlePieceRequest,
|
||||
options: PuzzleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<PuzzleRunResponse>({
|
||||
url: buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'drag'),
|
||||
@@ -89,6 +93,7 @@ export async function dragPuzzlePieceOrGroup(
|
||||
jsonBody: payload,
|
||||
fallbackMessage: '拖动拼图块失败',
|
||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,22 +146,14 @@ export async function updatePuzzleRunPause(
|
||||
payload: UpdatePuzzleRuntimePauseRequest,
|
||||
options: PuzzleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestJson<PuzzleRunResponse>(
|
||||
buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'pause'),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'更新拼图计时状态失败',
|
||||
{
|
||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||
authImpact: options.authImpact,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
return requestRuntimeJson<PuzzleRunResponse>({
|
||||
url: buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'pause'),
|
||||
method: 'POST',
|
||||
jsonBody: payload,
|
||||
fallbackMessage: '更新拼图计时状态失败',
|
||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,22 +164,14 @@ export async function usePuzzleRuntimeProp(
|
||||
payload: UsePuzzleRuntimePropRequest,
|
||||
options: PuzzleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestJson<PuzzleRunResponse>(
|
||||
buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'props'),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'使用拼图道具失败',
|
||||
{
|
||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||
authImpact: options.authImpact,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
return requestRuntimeJson<PuzzleRunResponse>({
|
||||
url: buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'props'),
|
||||
method: 'POST',
|
||||
jsonBody: payload,
|
||||
fallbackMessage: '使用拼图道具失败',
|
||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
export const puzzleRuntimeClient = {
|
||||
|
||||
@@ -13,17 +13,56 @@ vi.mock('./apiClient', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
import { startBarkBattleRun } from './bark-battle-runtime/barkBattleRuntimeClient';
|
||||
import { startBigFishRun } from './big-fish-runtime/bigFishRuntimeClient';
|
||||
import {
|
||||
finishBarkBattleRun,
|
||||
getBarkBattleRuntimeConfig,
|
||||
startBarkBattleRun,
|
||||
} from './bark-battle-runtime/barkBattleRuntimeClient';
|
||||
import {
|
||||
getBigFishRun,
|
||||
recordBigFishPlay,
|
||||
startBigFishRun,
|
||||
submitBigFishInput,
|
||||
} from './big-fish-runtime/bigFishRuntimeClient';
|
||||
import { startJumpHopRuntimeRun } from './jump-hop/jumpHopClient';
|
||||
import { startMatch3DRun } from './match3d-runtime/match3dRuntimeClient';
|
||||
import {
|
||||
clickMatch3DItem,
|
||||
finishMatch3DTimeUp,
|
||||
getMatch3DRun,
|
||||
restartMatch3DRun,
|
||||
startMatch3DRun,
|
||||
stopMatch3DRun,
|
||||
} from './match3d-runtime/match3dRuntimeClient';
|
||||
import {
|
||||
advancePuzzleNextLevel,
|
||||
dragPuzzlePieceOrGroup,
|
||||
getPuzzleRun,
|
||||
startPuzzleRun,
|
||||
submitPuzzleLeaderboard,
|
||||
swapPuzzlePieces,
|
||||
updatePuzzleRunPause,
|
||||
usePuzzleRuntimeProp,
|
||||
} from './puzzle-runtime/puzzleRuntimeClient';
|
||||
import { startSquareHoleRun } from './square-hole-runtime/squareHoleRuntimeClient';
|
||||
import { startVisualNovelRun } from './visual-novel-runtime/visualNovelRuntimeClient';
|
||||
import { puzzleClearClient } from './puzzle-clear/puzzleClearClient';
|
||||
import {
|
||||
dropSquareHoleShape,
|
||||
finishSquareHoleTimeUp,
|
||||
getSquareHoleRun,
|
||||
restartSquareHoleRun,
|
||||
startSquareHoleRun,
|
||||
stopSquareHoleRun,
|
||||
} from './square-hole-runtime/squareHoleRuntimeClient';
|
||||
import {
|
||||
checkpointWoodenFishRun,
|
||||
finishWoodenFishRun,
|
||||
startWoodenFishRuntimeRun,
|
||||
} from './wooden-fish/woodenFishClient';
|
||||
import {
|
||||
getVisualNovelHistory,
|
||||
getVisualNovelRun,
|
||||
regenerateVisualNovelRun,
|
||||
startVisualNovelRun,
|
||||
} from './visual-novel-runtime/visualNovelRuntimeClient';
|
||||
|
||||
describe('recommended runtime guest launch clients', () => {
|
||||
beforeEach(() => {
|
||||
@@ -31,6 +70,25 @@ describe('recommended runtime guest launch clients', () => {
|
||||
apiClientMocks.requestJson.mockResolvedValue({ run: {} });
|
||||
});
|
||||
|
||||
function expectRuntimeGuestRequest(expectedUrl: string, expectedMethod: string) {
|
||||
const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0];
|
||||
expect(url).toBe(expectedUrl);
|
||||
expect(init).toEqual(
|
||||
expect.objectContaining({
|
||||
method: expectedMethod,
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer runtime-guest-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'jump-hop',
|
||||
@@ -82,6 +140,14 @@ describe('recommended runtime guest launch clients', () => {
|
||||
}),
|
||||
expectedUrl: '/api/runtime/bark-battle/works/bark-battle-work-1/runs',
|
||||
},
|
||||
{
|
||||
name: 'wooden-fish',
|
||||
start: () =>
|
||||
startWoodenFishRuntimeRun('wooden-fish-profile-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/wooden-fish/runs',
|
||||
},
|
||||
{
|
||||
name: 'puzzle',
|
||||
start: () =>
|
||||
@@ -187,4 +253,397 @@ describe('recommended runtime guest launch clients', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'puzzle get run',
|
||||
run: () =>
|
||||
getPuzzleRun('run-puzzle-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1',
|
||||
expectedMethod: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'puzzle swap',
|
||||
run: () =>
|
||||
swapPuzzlePieces(
|
||||
'run-puzzle-1',
|
||||
{ firstPieceId: 'piece-a', secondPieceId: 'piece-b' },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1/swap',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'puzzle drag',
|
||||
run: () =>
|
||||
dragPuzzlePieceOrGroup(
|
||||
'run-puzzle-1',
|
||||
{ pieceId: 'piece-a', targetRow: 1, targetCol: 2 },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1/drag',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'puzzle pause',
|
||||
run: () =>
|
||||
updatePuzzleRunPause(
|
||||
'run-puzzle-1',
|
||||
{ paused: true },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1/pause',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'puzzle prop',
|
||||
run: () =>
|
||||
usePuzzleRuntimeProp(
|
||||
'run-puzzle-1',
|
||||
{ propKind: 'extendTime' },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/puzzle/runs/run-puzzle-1/props',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'puzzle-clear get run',
|
||||
run: () =>
|
||||
puzzleClearClient.getRun('puzzle-clear-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/puzzle-clear/runs/puzzle-clear-run-1',
|
||||
expectedMethod: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'puzzle-clear swap',
|
||||
run: () =>
|
||||
puzzleClearClient.swapCards(
|
||||
'puzzle-clear-run-1',
|
||||
{ fromRow: 0, fromCol: 0, toRow: 0, toCol: 1 },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/puzzle-clear/runs/puzzle-clear-run-1/swap',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'puzzle-clear retry',
|
||||
run: () =>
|
||||
puzzleClearClient.retryLevel('puzzle-clear-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl:
|
||||
'/api/runtime/puzzle-clear/runs/puzzle-clear-run-1/retry-level',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'puzzle-clear next',
|
||||
run: () =>
|
||||
puzzleClearClient.advanceNextLevel('puzzle-clear-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl:
|
||||
'/api/runtime/puzzle-clear/runs/puzzle-clear-run-1/next-level',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'puzzle-clear time up',
|
||||
run: () =>
|
||||
puzzleClearClient.markTimeUp('puzzle-clear-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/puzzle-clear/runs/puzzle-clear-run-1/time-up',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'square-hole get run',
|
||||
run: () =>
|
||||
getSquareHoleRun('square-hole-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/square-hole/runs/square-hole-run-1',
|
||||
expectedMethod: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'square-hole drop',
|
||||
run: () =>
|
||||
dropSquareHoleShape(
|
||||
'square-hole-run-1',
|
||||
{
|
||||
holeId: 'hole-1',
|
||||
clientSnapshotVersion: 1,
|
||||
clientEventId: 'event-1',
|
||||
droppedAtMs: 1_700_000_000_000,
|
||||
},
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/square-hole/runs/square-hole-run-1/drop',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'square-hole stop',
|
||||
run: () =>
|
||||
stopSquareHoleRun(
|
||||
'square-hole-run-1',
|
||||
{ clientActionId: 'stop-1' },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/square-hole/runs/square-hole-run-1/stop',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'square-hole restart',
|
||||
run: () =>
|
||||
restartSquareHoleRun('square-hole-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/square-hole/runs/square-hole-run-1/restart',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'square-hole time up',
|
||||
run: () =>
|
||||
finishSquareHoleTimeUp('square-hole-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/square-hole/runs/square-hole-run-1/time-up',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'big-fish get run',
|
||||
run: () =>
|
||||
getBigFishRun('big-fish-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/big-fish/runs/big-fish-run-1',
|
||||
expectedMethod: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'big-fish input',
|
||||
run: () =>
|
||||
submitBigFishInput(
|
||||
'big-fish-run-1',
|
||||
{ x: 0.25, y: 0.75 },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/big-fish/runs/big-fish-run-1/input',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'big-fish play report',
|
||||
run: () =>
|
||||
recordBigFishPlay(
|
||||
'big-fish-session-1',
|
||||
{ elapsedMs: 1_000 },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl:
|
||||
'/api/runtime/big-fish/sessions/big-fish-session-1/play',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'bark-battle config',
|
||||
run: () =>
|
||||
getBarkBattleRuntimeConfig('bark-battle-work-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl:
|
||||
'/api/runtime/bark-battle/works/bark-battle-work-1/config',
|
||||
expectedMethod: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'bark-battle finish',
|
||||
run: () =>
|
||||
finishBarkBattleRun(
|
||||
'bark-battle-run-1',
|
||||
{
|
||||
runId: 'bark-battle-run-1',
|
||||
runToken: 'run-token',
|
||||
workId: 'bark-battle-work-1',
|
||||
configVersion: 1,
|
||||
rulesetVersion: 'v1',
|
||||
difficultyPreset: 'normal',
|
||||
clientStartedAt: '2026-06-10T00:00:00Z',
|
||||
clientFinishedAt: '2026-06-10T00:00:10Z',
|
||||
durationMs: 10_000,
|
||||
derivedMetrics: {
|
||||
triggerCount: 1,
|
||||
maxVolume: 0.8,
|
||||
averageVolume: 0.4,
|
||||
finalEnergy: 10,
|
||||
comboMax: 1,
|
||||
},
|
||||
},
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/bark-battle/runs/bark-battle-run-1/finish',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'wooden-fish checkpoint',
|
||||
run: () =>
|
||||
checkpointWoodenFishRun(
|
||||
'wooden-fish-run-1',
|
||||
{ totalTapCount: 8, wordCounters: [] },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl:
|
||||
'/api/runtime/wooden-fish/runs/wooden-fish-run-1/checkpoint',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'wooden-fish finish',
|
||||
run: () =>
|
||||
finishWoodenFishRun(
|
||||
'wooden-fish-run-1',
|
||||
{ totalTapCount: 8, wordCounters: [] },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/wooden-fish/runs/wooden-fish-run-1/finish',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'visual-novel get run',
|
||||
run: () =>
|
||||
getVisualNovelRun('visual-novel-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/visual-novel/runs/visual-novel-run-1',
|
||||
expectedMethod: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'visual-novel history',
|
||||
run: () =>
|
||||
getVisualNovelHistory('visual-novel-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl:
|
||||
'/api/runtime/visual-novel/runs/visual-novel-run-1/history',
|
||||
expectedMethod: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'visual-novel regenerate',
|
||||
run: () =>
|
||||
regenerateVisualNovelRun(
|
||||
'visual-novel-run-1',
|
||||
{ historyEntryId: 'history-1', clientEventId: 'event-1' },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl:
|
||||
'/api/runtime/visual-novel/runs/visual-novel-run-1/regenerate',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
])(
|
||||
'$name uses the shared runtime guest bearer token without touching login auth',
|
||||
async ({ run, expectedUrl, expectedMethod }) => {
|
||||
await run();
|
||||
|
||||
expectRuntimeGuestRequest(expectedUrl, expectedMethod);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'get run',
|
||||
run: () =>
|
||||
getMatch3DRun('match3d-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/match3d/runs/match3d-run-1',
|
||||
expectedMethod: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'restart',
|
||||
run: () =>
|
||||
restartMatch3DRun('match3d-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/match3d/runs/match3d-run-1/restart',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'stop',
|
||||
run: () =>
|
||||
stopMatch3DRun(
|
||||
'match3d-run-1',
|
||||
{ clientActionId: 'stop-1' },
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
),
|
||||
expectedUrl: '/api/runtime/match3d/runs/match3d-run-1/stop',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'time up',
|
||||
run: () =>
|
||||
finishMatch3DTimeUp('match3d-run-1', {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
expectedUrl: '/api/runtime/match3d/runs/match3d-run-1/time-up',
|
||||
expectedMethod: 'POST',
|
||||
},
|
||||
])(
|
||||
'match3d $name uses the runtime guest bearer token without touching login auth',
|
||||
async ({ run, expectedUrl, expectedMethod }) => {
|
||||
await run();
|
||||
|
||||
const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0];
|
||||
expect(url).toBe(expectedUrl);
|
||||
expect(init).toEqual(
|
||||
expect.objectContaining({
|
||||
method: expectedMethod,
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer runtime-guest-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('match3d click uses the runtime guest bearer token without touching login auth', async () => {
|
||||
apiClientMocks.requestJson.mockResolvedValueOnce({
|
||||
confirmation: {
|
||||
accepted: true,
|
||||
run: {},
|
||||
clearedItemInstanceIds: [],
|
||||
},
|
||||
});
|
||||
|
||||
await clickMatch3DItem(
|
||||
'match3d-run-1',
|
||||
{
|
||||
runId: 'match3d-run-1',
|
||||
itemInstanceId: 'item-1',
|
||||
clientActionId: 'action-1',
|
||||
clientEventId: 'event-1',
|
||||
clickedAtMs: 1_700_000_000_000,
|
||||
clientSnapshotVersion: 1,
|
||||
},
|
||||
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||
);
|
||||
|
||||
const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0];
|
||||
expect(url).toBe('/api/runtime/match3d/runs/match3d-run-1/click');
|
||||
expect(init).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer runtime-guest-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,11 +49,15 @@ export function startSquareHoleRun(
|
||||
/**
|
||||
* 读取方洞挑战运行态快照。
|
||||
*/
|
||||
export function getSquareHoleRun(runId: string) {
|
||||
export function getSquareHoleRun(
|
||||
runId: string,
|
||||
options: SquareHoleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<SquareHoleRunResponse>({
|
||||
url: buildRuntimeApiPath(SQUARE_HOLE_RUNTIME_API_BASE, 'runs', runId),
|
||||
fallbackMessage: '读取方洞挑战运行快照失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_READ_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,6 +67,7 @@ export function getSquareHoleRun(runId: string) {
|
||||
export function dropSquareHoleShape(
|
||||
runId: string,
|
||||
payload: DropSquareHoleShapeRequest,
|
||||
options: SquareHoleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<SquareHoleDropResponse>({
|
||||
url: buildRuntimeApiPath(
|
||||
@@ -78,6 +83,7 @@ export function dropSquareHoleShape(
|
||||
},
|
||||
fallbackMessage: '确认方洞挑战投入失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,6 +95,7 @@ export function stopSquareHoleRun(
|
||||
payload: StopSquareHoleRunRequest = {
|
||||
clientActionId: `square-hole-stop-${Date.now()}`,
|
||||
},
|
||||
options: SquareHoleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<SquareHoleRunResponse>({
|
||||
url: buildRuntimeApiPath(SQUARE_HOLE_RUNTIME_API_BASE, 'runs', runId, 'stop'),
|
||||
@@ -96,13 +103,17 @@ export function stopSquareHoleRun(
|
||||
jsonBody: payload,
|
||||
fallbackMessage: '停止方洞挑战失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于当前 run 重开一局。
|
||||
*/
|
||||
export function restartSquareHoleRun(runId: string) {
|
||||
export function restartSquareHoleRun(
|
||||
runId: string,
|
||||
options: SquareHoleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<SquareHoleRunResponse>({
|
||||
url: buildRuntimeApiPath(
|
||||
SQUARE_HOLE_RUNTIME_API_BASE,
|
||||
@@ -113,13 +124,17 @@ export function restartSquareHoleRun(runId: string) {
|
||||
method: 'POST',
|
||||
fallbackMessage: '重新开始方洞挑战失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端倒计时归零后通知后端确认失败状态。
|
||||
*/
|
||||
export function finishSquareHoleTimeUp(runId: string) {
|
||||
export function finishSquareHoleTimeUp(
|
||||
runId: string,
|
||||
options: SquareHoleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<SquareHoleRunResponse>({
|
||||
url: buildRuntimeApiPath(
|
||||
SQUARE_HOLE_RUNTIME_API_BASE,
|
||||
@@ -130,6 +145,7 @@ export function finishSquareHoleTimeUp(runId: string) {
|
||||
method: 'POST',
|
||||
fallbackMessage: '同步方洞挑战倒计时失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -135,15 +135,22 @@ export async function startVisualNovelRun(
|
||||
);
|
||||
}
|
||||
|
||||
export async function getVisualNovelRun(runId: string) {
|
||||
export async function getVisualNovelRun(
|
||||
runId: string,
|
||||
options: VisualNovelRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<VisualNovelRunResponse>({
|
||||
url: buildRuntimeApiPath(VISUAL_NOVEL_RUNTIME_API_BASE, 'runs', runId),
|
||||
fallbackMessage: '读取视觉小说运行快照失败',
|
||||
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getVisualNovelHistory(runId: string) {
|
||||
export async function getVisualNovelHistory(
|
||||
runId: string,
|
||||
options: VisualNovelRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<VisualNovelHistoryResponse>({
|
||||
url: buildRuntimeApiPath(
|
||||
VISUAL_NOVEL_RUNTIME_API_BASE,
|
||||
@@ -153,6 +160,7 @@ export async function getVisualNovelHistory(runId: string) {
|
||||
),
|
||||
fallbackMessage: '读取视觉小说历史失败',
|
||||
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -185,6 +193,7 @@ export async function streamVisualNovelRuntimeAction(
|
||||
export async function regenerateVisualNovelRun(
|
||||
runId: string,
|
||||
payload: VisualNovelRegenerateRequest,
|
||||
options: VisualNovelRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRuntimeJson<VisualNovelRunResponse>({
|
||||
url: buildRuntimeApiPath(
|
||||
@@ -197,6 +206,7 @@ export async function regenerateVisualNovelRun(
|
||||
jsonBody: payload,
|
||||
fallbackMessage: '重生成视觉小说历史失败',
|
||||
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,30 @@ test('wooden fish list works uses creation works endpoint', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('wooden fish runtime work detail reads public profile without auth refresh coupling', async () => {
|
||||
const { woodenFishClient } = await import('./woodenFishClient');
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
item: {
|
||||
summary: {
|
||||
profileId: 'profile-1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await woodenFishClient.getWorkDetail('profile-1');
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/runtime/wooden-fish/works/profile-1',
|
||||
{ method: 'GET' },
|
||||
'读取敲木鱼作品详情失败',
|
||||
expect.objectContaining({
|
||||
retry: expect.objectContaining({ maxRetries: 1 }),
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('wooden fish start run uses runtime guest json skeleton', async () => {
|
||||
const { woodenFishClient } = await import('./woodenFishClient');
|
||||
requestJsonMock.mockResolvedValueOnce({ run: { runId: 'run-1' } });
|
||||
|
||||
@@ -17,7 +17,11 @@ import type {
|
||||
WoodenFishWorksResponse,
|
||||
WoodenFishWorkSummaryResponse,
|
||||
} from '../../../packages/shared/src/contracts/woodenFish';
|
||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||
import {
|
||||
type ApiRequestOptions,
|
||||
type ApiRetryOptions,
|
||||
requestJson,
|
||||
} from '../apiClient';
|
||||
import { createCreationAgentClient } from '../creation-agent';
|
||||
import { type RuntimeGuestRequestOptions } from '../runtimeGuestAuth';
|
||||
import { buildRuntimeApiPath, requestRuntimeJson } from '../runtimeRequest';
|
||||
@@ -176,11 +180,19 @@ export function executeWoodenFishCreationAction(
|
||||
.then(normalizeWoodenFishActionResponse);
|
||||
}
|
||||
|
||||
export async function getWoodenFishWorkDetail(profileId: string) {
|
||||
export async function getWoodenFishWorkDetail(
|
||||
profileId: string,
|
||||
options: ApiRequestOptions = {
|
||||
retry: WOODEN_FISH_RUNTIME_READ_RETRY,
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
) {
|
||||
const response = await requestJson<WoodenFishWorkDetailResponse>(
|
||||
`${WOODEN_FISH_RUNTIME_API_BASE}/works/${encodeURIComponent(profileId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取敲木鱼作品详情失败',
|
||||
options,
|
||||
);
|
||||
return normalizeWoodenFishWorkDetailResponse(response);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user