diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 4e2aec4d..752aec27 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -45,7 +45,8 @@ - 背景: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 只承接请求骨架。 -- 影响范围:`src/services/runtimeRequest.ts`、Match3D / SquareHole / Big Fish / Bark Battle runtime client、后续 Puzzle / Jump Hop / Visual Novel runtime client 迁移。 +- 追加决策:Puzzle 的 start / get / swap / drag / next-level / leaderboard 与 Jump Hop 的 start / jump / restart 也迁入 `runtimeRequest.ts`;Puzzle `pause` 与 `props` 仍保留原账号态 auth options,不直接接入 runtime guest auth。 +- 影响范围:`src/services/runtimeRequest.ts`、Match3D / SquareHole / Big Fish / Bark Battle / Puzzle / Jump Hop runtime client、后续 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 通过。 - 关联文档:`docs/technical/【前端架构】RuntimeClientFamily收口计划-2026-06-03.md`。 diff --git a/docs/README.md b/docs/README.md index 36579f3f..175f634a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -43,7 +43,7 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 创作中心作品架打开动作由 `CreationWorkShelfItem.actions.open` 统一承载,Hub 不再按玩法 `kind` 分发,规则见 [【前端架构】WorkShelfModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91WorkShelfModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 -小游戏 runtime client 的路径编码、JSON 请求、runtime guest auth 与 retry 选项收口到 `src/services/runtimeRequest.ts`,Match3D、SquareHole、Big Fish 与 Bark Battle 已先迁移,规则见 [【前端架构】RuntimeClientFamily收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RuntimeClientFamily%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 +小游戏 runtime client 的路径编码、JSON 请求、runtime guest auth 与 retry 选项收口到 `src/services/runtimeRequest.ts`,Match3D、SquareHole、Big Fish、Bark Battle、Puzzle 公开 / 推荐运行态请求与 Jump Hop 正式 run 请求已先迁移,规则见 [【前端架构】RuntimeClientFamily收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RuntimeClientFamily%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 公开作品分类、搜索、跨来源去重、今日筛选、排行排序和时间戳解析收口到 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts`,规则见 [【前端架构】PublicGalleryViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91PublicGalleryViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 diff --git a/docs/technical/【前端架构】RuntimeClientFamily收口计划-2026-06-03.md b/docs/technical/【前端架构】RuntimeClientFamily收口计划-2026-06-03.md index 51ef3431..5f334e3d 100644 --- a/docs/technical/【前端架构】RuntimeClientFamily收口计划-2026-06-03.md +++ b/docs/technical/【前端架构】RuntimeClientFamily收口计划-2026-06-03.md @@ -11,7 +11,7 @@ - `buildRuntimeApiPath(basePath, ...segments)`:统一对 runtime path segment 执行 `encodeURIComponent`。 - `requestRuntimeJson(params)`:统一设置 method、JSON body、`Content-Type`、runtime guest `Authorization`、auth options 和 retry options。 -`match3dRuntimeClient.ts`、`squareHoleRuntimeClient.ts`、`bigFishRuntimeClient.ts` 与 `barkBattleRuntimeClient.ts` 已迁入此 **Module**,并保留原有导出函数名、错误文案、返回契约和重试常量。点击 / 投入 / 成绩提交等玩法专属 payload 归一化仍留在各自 client 内,避免把领域规则塞进通用请求 **Implementation**。 +`match3dRuntimeClient.ts`、`squareHoleRuntimeClient.ts`、`bigFishRuntimeClient.ts`、`barkBattleRuntimeClient.ts`、`puzzleRuntimeClient.ts` 的公开 / 推荐运行态请求,以及 `jumpHopClient.ts` 的正式 run 请求已迁入此 **Module**,并保留原有导出函数名、错误文案、返回契约和重试常量。点击 / 投入 / 成绩提交等玩法专属 payload 归一化仍留在各自 client 内,避免把领域规则塞进通用请求 **Implementation**。 ## 约定 @@ -22,7 +22,7 @@ ## 后续深化 -下一批可迁移 Puzzle、Jump Hop 与 Visual Novel runtime client。迁移顺序以测试覆盖和请求形状接近度为准,优先迁移已有 guest launch 或 client 单测覆盖的函数。 +下一批可迁移 Visual Novel runtime client,并评估 Puzzle `pause` / `props` 是否应继续保留账号态 auth options。迁移顺序以测试覆盖和请求形状接近度为准,优先迁移已有 guest launch 或 client 单测覆盖的函数。 ## 验证 diff --git a/src/services/jump-hop/jumpHopClient.runtime.test.ts b/src/services/jump-hop/jumpHopClient.runtime.test.ts new file mode 100644 index 00000000..fa534194 --- /dev/null +++ b/src/services/jump-hop/jumpHopClient.runtime.test.ts @@ -0,0 +1,108 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const apiClientMocks = vi.hoisted(() => ({ + requestJson: vi.fn(), +})); + +vi.mock('../apiClient', async () => { + const actual = + await vi.importActual('../apiClient'); + return { + ...actual, + requestJson: apiClientMocks.requestJson, + }; +}); + +import { + restartJumpHopRuntimeRun, + startJumpHopRuntimeRun, + submitJumpHopJump, +} from './jumpHopClient'; + +describe('jumpHopClient runtime requests', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(Date, 'now').mockReturnValue(1780000000000); + apiClientMocks.requestJson.mockResolvedValue({ runId: 'run-1' }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('starts runs through the shared runtime request skeleton', async () => { + await startJumpHopRuntimeRun('profile/1', { + runtimeGuestToken: 'runtime-guest-token', + }); + + expect(apiClientMocks.requestJson).toHaveBeenCalledWith( + '/api/runtime/jump-hop/runs', + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer runtime-guest-token', + }, + body: JSON.stringify({ profileId: 'profile/1' }), + }), + '启动跳一跳运行态失败', + expect.objectContaining({ + skipAuth: true, + skipRefresh: true, + }), + ); + }); + + it('submits jump input with a generated client event id', async () => { + await submitJumpHopJump( + 'run/1', + { chargeMs: 320 }, + { runtimeGuestToken: 'runtime-guest-token' }, + ); + + expect(apiClientMocks.requestJson).toHaveBeenCalledWith( + '/api/runtime/jump-hop/runs/run%2F1/jump', + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer runtime-guest-token', + }, + body: JSON.stringify({ + chargeMs: 320, + clientEventId: 'jump-run/1-1780000000000', + }), + }), + '提交跳一跳起跳失败', + expect.objectContaining({ + skipAuth: true, + skipRefresh: true, + }), + ); + }); + + it('restarts runs with the same guest auth request skeleton', async () => { + await restartJumpHopRuntimeRun('run/1', { + runtimeGuestToken: 'runtime-guest-token', + }); + + expect(apiClientMocks.requestJson).toHaveBeenCalledWith( + '/api/runtime/jump-hop/runs/run%2F1/restart', + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer runtime-guest-token', + }, + body: JSON.stringify({ + clientActionId: 'restart-run/1-1780000000000', + }), + }), + '重新开始跳一跳失败', + expect.objectContaining({ + skipAuth: true, + skipRefresh: true, + }), + ); + }); +}); diff --git a/src/services/jump-hop/jumpHopClient.ts b/src/services/jump-hop/jumpHopClient.ts index d1e7fe13..ac4a6a50 100644 --- a/src/services/jump-hop/jumpHopClient.ts +++ b/src/services/jump-hop/jumpHopClient.ts @@ -12,8 +12,8 @@ import type { JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, JumpHopWorkProfileResponse, - JumpHopWorksResponse, JumpHopWorkspaceCreateRequest, + JumpHopWorksResponse, JumpHopWorkSummaryResponse, } from '../../../packages/shared/src/contracts/jumpHop'; import { @@ -21,11 +21,8 @@ import { requestJson, } from '../apiClient'; import { createCreationAgentClient } from '../creation-agent'; -import { - buildRuntimeGuestAuthOptions, - buildRuntimeGuestHeaders, - type RuntimeGuestRequestOptions, -} from '../runtimeGuestAuth'; +import { type RuntimeGuestRequestOptions } from '../runtimeGuestAuth'; +import { buildRuntimeApiPath, requestRuntimeJson } from '../runtimeRequest'; const JUMP_HOP_API_BASE = '/api/creation/jump-hop/sessions'; const JUMP_HOP_WORKS_API_BASE = '/api/creation/jump-hop/works'; @@ -51,8 +48,8 @@ export type { JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, JumpHopWorkProfileResponse, - JumpHopWorksResponse, JumpHopWorkspaceCreateRequest, + JumpHopWorksResponse, }; export type CreateJumpHopSessionRequest = { themeText: string; @@ -234,22 +231,13 @@ export async function startJumpHopRuntimeRun( profileId: string, options: JumpHopRuntimeRequestOptions = {}, ) { - const requestOptions = buildRuntimeGuestAuthOptions(options); - return requestJson( - `${JUMP_HOP_RUNTIME_API_BASE}/runs`, - { - method: 'POST', - headers: { - 'content-type': 'application/json', - ...buildRuntimeGuestHeaders(options), - }, - body: JSON.stringify({ profileId }), - }, - '启动跳一跳运行态失败', - { - ...requestOptions, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath(JUMP_HOP_RUNTIME_API_BASE, 'runs'), + method: 'POST', + jsonBody: { profileId }, + fallbackMessage: '启动跳一跳运行态失败', + requestOptions: options, + }); } export async function submitJumpHopJump( @@ -257,47 +245,38 @@ export async function submitJumpHopJump( payload: { chargeMs: number }, options: JumpHopRuntimeRequestOptions = {}, ) { - const requestOptions = buildRuntimeGuestAuthOptions(options); const requestPayload = { chargeMs: payload.chargeMs, clientEventId: `jump-${runId}-${Date.now()}`, }; - return requestJson( - `${JUMP_HOP_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/jump`, - { - method: 'POST', - headers: { - 'content-type': 'application/json', - ...buildRuntimeGuestHeaders(options), - }, - body: JSON.stringify(requestPayload), - }, - '提交跳一跳起跳失败', - requestOptions, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath(JUMP_HOP_RUNTIME_API_BASE, 'runs', runId, 'jump'), + method: 'POST', + jsonBody: requestPayload, + fallbackMessage: '提交跳一跳起跳失败', + requestOptions: options, + }); } export async function restartJumpHopRuntimeRun( runId: string, options: JumpHopRuntimeRequestOptions = {}, ) { - const requestOptions = buildRuntimeGuestAuthOptions(options); - return requestJson( - `${JUMP_HOP_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/restart`, - { - method: 'POST', - headers: { - 'content-type': 'application/json', - ...buildRuntimeGuestHeaders(options), - }, - body: JSON.stringify({ - clientActionId: `restart-${runId}-${Date.now()}`, - }), + return requestRuntimeJson({ + url: buildRuntimeApiPath( + JUMP_HOP_RUNTIME_API_BASE, + 'runs', + runId, + 'restart', + ), + method: 'POST', + jsonBody: { + clientActionId: `restart-${runId}-${Date.now()}`, }, - '重新开始跳一跳失败', - requestOptions, - ); + fallbackMessage: '重新开始跳一跳失败', + requestOptions: options, + }); } export const jumpHopClient = { diff --git a/src/services/puzzle-runtime/puzzleRuntimeClient.test.ts b/src/services/puzzle-runtime/puzzleRuntimeClient.test.ts new file mode 100644 index 00000000..68f3accc --- /dev/null +++ b/src/services/puzzle-runtime/puzzleRuntimeClient.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const apiClientMocks = vi.hoisted(() => ({ + requestJson: vi.fn(), +})); + +vi.mock('../apiClient', async () => { + const actual = + await vi.importActual('../apiClient'); + return { + ...actual, + requestJson: apiClientMocks.requestJson, + }; +}); + +import { + getPuzzleRun, + swapPuzzlePieces, + updatePuzzleRunPause, +} from './puzzleRuntimeClient'; + +describe('puzzleRuntimeClient', () => { + beforeEach(() => { + vi.clearAllMocks(); + apiClientMocks.requestJson.mockResolvedValue({ runId: 'run-1' }); + }); + + it('reads runs through the shared encoded runtime path', async () => { + await getPuzzleRun('run/1'); + + expect(apiClientMocks.requestJson).toHaveBeenCalledWith( + '/api/runtime/puzzle/runs/run%2F1', + { method: 'GET' }, + '读取拼图运行快照失败', + expect.objectContaining({ + retry: expect.objectContaining({ maxRetries: 1 }), + }), + ); + }); + + it('submits puzzle swaps through the shared json request skeleton', async () => { + await swapPuzzlePieces('run/1', { + firstPieceId: 'piece-a', + secondPieceId: 'piece-b', + }); + + expect(apiClientMocks.requestJson).toHaveBeenCalledWith( + '/api/runtime/puzzle/runs/run%2F1/swap', + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + firstPieceId: 'piece-a', + secondPieceId: 'piece-b', + }), + }), + '交换拼图块失败', + expect.objectContaining({ + retry: expect.objectContaining({ retryUnsafeMethods: true }), + }), + ); + }); + + it('keeps pause requests on account auth options instead of guest auth', async () => { + await updatePuzzleRunPause( + 'run/1', + { paused: true }, + { + authImpact: 'local', + runtimeGuestToken: 'runtime-guest-token', + skipRefresh: true, + }, + ); + + const [, init, , options] = apiClientMocks.requestJson.mock.calls[0]; + expect(init).toEqual( + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ paused: true }), + }), + ); + expect(init.headers).not.toHaveProperty('Authorization'); + expect(options).toEqual( + expect.objectContaining({ + authImpact: 'local', + skipRefresh: true, + }), + ); + expect(options).not.toMatchObject({ skipAuth: true }); + }); +}); diff --git a/src/services/puzzle-runtime/puzzleRuntimeClient.ts b/src/services/puzzle-runtime/puzzleRuntimeClient.ts index be9393ef..501d6a9e 100644 --- a/src/services/puzzle-runtime/puzzleRuntimeClient.ts +++ b/src/services/puzzle-runtime/puzzleRuntimeClient.ts @@ -1,6 +1,6 @@ import type { - DragPuzzlePieceRequest, AdvancePuzzleNextLevelRequest, + DragPuzzlePieceRequest, PuzzleRunResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest, @@ -12,11 +12,8 @@ import { type ApiRetryOptions, requestJson, } from '../apiClient'; -import { - buildRuntimeGuestAuthOptions, - buildRuntimeGuestHeaders, - type RuntimeGuestRequestOptions, -} from '../runtimeGuestAuth'; +import { type RuntimeGuestRequestOptions } from '../runtimeGuestAuth'; +import { buildRuntimeApiPath, requestRuntimeJson } from '../runtimeRequest'; const PUZZLE_RUNTIME_API_BASE = '/api/runtime/puzzle/runs'; const PUZZLE_RUNTIME_READ_RETRY: ApiRetryOptions = { @@ -42,38 +39,25 @@ export async function startPuzzleRun( payload: StartPuzzleRunRequest, options: PuzzleRuntimeRequestOptions = {}, ) { - const requestOptions = buildRuntimeGuestAuthOptions(options); - return requestJson( - PUZZLE_RUNTIME_API_BASE, - { - method: 'POST', - headers: buildRuntimeGuestHeaders(options, { - 'Content-Type': 'application/json', - }), - body: JSON.stringify(payload), - }, - '启动拼图玩法失败', - { - retry: PUZZLE_RUNTIME_WRITE_RETRY, - ...requestOptions, - }, - ); + return requestRuntimeJson({ + url: PUZZLE_RUNTIME_API_BASE, + method: 'POST', + jsonBody: payload, + fallbackMessage: '启动拼图玩法失败', + retry: PUZZLE_RUNTIME_WRITE_RETRY, + requestOptions: options, + }); } /** * 读取拼图运行态快照。 */ export async function getPuzzleRun(runId: string) { - return requestJson( - `${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}`, - { - method: 'GET', - }, - '读取拼图运行快照失败', - { - retry: PUZZLE_RUNTIME_READ_RETRY, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId), + fallbackMessage: '读取拼图运行快照失败', + retry: PUZZLE_RUNTIME_READ_RETRY, + }); } /** @@ -83,18 +67,13 @@ export async function swapPuzzlePieces( runId: string, payload: SwapPuzzlePiecesRequest, ) { - return requestJson( - `${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/swap`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }, - '交换拼图块失败', - { - retry: PUZZLE_RUNTIME_WRITE_RETRY, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'swap'), + method: 'POST', + jsonBody: payload, + fallbackMessage: '交换拼图块失败', + retry: PUZZLE_RUNTIME_WRITE_RETRY, + }); } /** @@ -104,18 +83,13 @@ export async function dragPuzzlePieceOrGroup( runId: string, payload: DragPuzzlePieceRequest, ) { - return requestJson( - `${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/drag`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }, - '拖动拼图块失败', - { - retry: PUZZLE_RUNTIME_WRITE_RETRY, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'drag'), + method: 'POST', + jsonBody: payload, + fallbackMessage: '拖动拼图块失败', + retry: PUZZLE_RUNTIME_WRITE_RETRY, + }); } /** @@ -126,7 +100,6 @@ export async function advancePuzzleNextLevel( payload: AdvancePuzzleNextLevelRequest = {}, options: PuzzleRuntimeRequestOptions = {}, ) { - const requestOptions = buildRuntimeGuestAuthOptions(options); const targetProfileId = payload.targetProfileId?.trim() ?? ''; const preferSimilarWork = payload.preferSimilarWork === true; const requestPayload = { @@ -134,27 +107,14 @@ export async function advancePuzzleNextLevel( ...(preferSimilarWork ? { preferSimilarWork: true } : {}), }; const hasRequestPayload = Object.keys(requestPayload).length > 0; - return requestJson( - `${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/next-level`, - { - method: 'POST', - ...(hasRequestPayload - ? { - headers: buildRuntimeGuestHeaders(options, { - 'Content-Type': 'application/json', - }), - body: JSON.stringify(requestPayload), - } - : { - headers: buildRuntimeGuestHeaders(options), - }), - }, - '进入下一关失败', - { - retry: PUZZLE_RUNTIME_WRITE_RETRY, - ...requestOptions, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'next-level'), + method: 'POST', + ...(hasRequestPayload ? { jsonBody: requestPayload } : {}), + fallbackMessage: '进入下一关失败', + retry: PUZZLE_RUNTIME_WRITE_RETRY, + requestOptions: options, + }); } /** @@ -165,22 +125,14 @@ export async function submitPuzzleLeaderboard( payload: SubmitPuzzleLeaderboardRequest, options: PuzzleRuntimeRequestOptions = {}, ) { - const requestOptions = buildRuntimeGuestAuthOptions(options); - return requestJson( - `${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/leaderboard`, - { - method: 'POST', - headers: buildRuntimeGuestHeaders(options, { - 'Content-Type': 'application/json', - }), - body: JSON.stringify(payload), - }, - '提交拼图排行榜失败', - { - retry: PUZZLE_RUNTIME_LEADERBOARD_RETRY, - ...requestOptions, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'leaderboard'), + method: 'POST', + jsonBody: payload, + fallbackMessage: '提交拼图排行榜失败', + retry: PUZZLE_RUNTIME_LEADERBOARD_RETRY, + requestOptions: options, + }); } /** @@ -192,7 +144,7 @@ export async function updatePuzzleRunPause( options: PuzzleRuntimeRequestOptions = {}, ) { return requestJson( - `${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/pause`, + buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'pause'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -218,7 +170,7 @@ export async function usePuzzleRuntimeProp( options: PuzzleRuntimeRequestOptions = {}, ) { return requestJson( - `${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/props`, + buildRuntimeApiPath(PUZZLE_RUNTIME_API_BASE, runId, 'props'), { method: 'POST', headers: { 'Content-Type': 'application/json' },