diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 7aefb3a2..6ed5ece1 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -46,7 +46,8 @@ - 决策:新增 `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。 -- 影响范围:`src/services/runtimeRequest.ts`、Match3D / SquareHole / Big Fish / Bark Battle / Puzzle / Jump Hop runtime client、后续 Visual Novel runtime client 迁移。 +- 追加决策: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 通过。 - 关联文档:`docs/technical/【前端架构】RuntimeClientFamily收口计划-2026-06-03.md`。 diff --git a/docs/README.md b/docs/README.md index 26f8e492..25f41526 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、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)。 +小游戏 runtime client 的路径编码、JSON 请求、runtime guest auth 与 retry 选项收口到 `src/services/runtimeRequest.ts`,Match3D、SquareHole、Big Fish、Bark Battle、Puzzle 公开 / 推荐运行态请求、Jump Hop / Wooden Fish 正式 run 请求和 Visual Novel 局部 JSON runtime 请求已先迁移,规则见 [【前端架构】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 5f334e3d..c1cb759d 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`、`puzzleRuntimeClient.ts` 的公开 / 推荐运行态请求,以及 `jumpHopClient.ts` 的正式 run 请求已迁入此 **Module**,并保留原有导出函数名、错误文案、返回契约和重试常量。点击 / 投入 / 成绩提交等玩法专属 payload 归一化仍留在各自 client 内,避免把领域规则塞进通用请求 **Implementation**。 +`match3dRuntimeClient.ts`、`squareHoleRuntimeClient.ts`、`bigFishRuntimeClient.ts`、`barkBattleRuntimeClient.ts`、`puzzleRuntimeClient.ts` 的公开 / 推荐运行态请求、`jumpHopClient.ts` 与 `woodenFishClient.ts` 的正式 run 请求,以及 `visualNovelRuntimeClient.ts` 的公开列表、run 读取、history 读取和 regenerate JSON 请求已迁入此 **Module**,并保留原有导出函数名、错误文案、返回契约和重试常量。点击 / 投入 / 成绩提交等玩法专属 payload 归一化仍留在各自 client 内,避免把领域规则塞进通用请求 **Implementation**。 ## 约定 @@ -22,7 +22,7 @@ ## 后续深化 -下一批可迁移 Visual Novel runtime client,并评估 Puzzle `pause` / `props` 是否应继续保留账号态 auth options。迁移顺序以测试覆盖和请求形状接近度为准,优先迁移已有 guest launch 或 client 单测覆盖的函数。 +下一批可评估是否扩展 `requestRuntimeJson` 支持 `timeoutMs` / `signal`,再迁移 Visual Novel start 请求;Visual Novel SSE、平台存档、平台 checkpoint,以及 Puzzle `pause` / `props` 继续保留各自现有 auth / stream 语义,暂不纳入通用 JSON helper。 ## 验证 diff --git a/src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts b/src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts index c226b663..64e1cff4 100644 --- a/src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts +++ b/src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts @@ -10,10 +10,12 @@ vi.mock('../apiClient', () => ({ requestJson: requestJsonMock, })); +import type { VisualNovelRunSnapshot } from '../../../packages/shared/src/contracts/visualNovel'; import { buildVisualNovelRuntimeCheckpoint, buildVisualNovelSaveArchiveState, - type VisualNovelRuntimeStreamOptions, + getVisualNovelHistory, + getVisualNovelRun, listVisualNovelGallery, listVisualNovelSaveArchives, putVisualNovelRuntimeSnapshot, @@ -21,8 +23,8 @@ import { resumeVisualNovelSaveArchive, startVisualNovelRun, streamVisualNovelRuntimeAction, + type VisualNovelRuntimeStreamOptions, } from './visualNovelRuntimeClient'; -import type { VisualNovelRunSnapshot } from '../../../packages/shared/src/contracts/visualNovel'; function createMockRun( overrides: Partial = {}, @@ -108,6 +110,32 @@ test('startVisualNovelRun uses the visual novel runtime work route', async () => ); }); +test('getVisualNovelRun and getVisualNovelHistory use encoded runtime run routes', async () => { + requestJsonMock + .mockResolvedValueOnce({ run: createMockRun() }) + .mockResolvedValueOnce({ entries: [] }); + + await getVisualNovelRun('vn/run-1'); + await getVisualNovelHistory('vn/run-1'); + + expect(requestJsonMock.mock.calls[0]).toEqual([ + '/api/runtime/visual-novel/runs/vn%2Frun-1', + expect.objectContaining({ method: 'GET' }), + '读取视觉小说运行快照失败', + expect.objectContaining({ + retry: expect.objectContaining({ maxRetries: 1 }), + }), + ]); + expect(requestJsonMock.mock.calls[1]).toEqual([ + '/api/runtime/visual-novel/runs/vn%2Frun-1/history', + expect.objectContaining({ method: 'GET' }), + '读取视觉小说历史失败', + expect.objectContaining({ + retry: expect.objectContaining({ maxRetries: 1 }), + }), + ]); +}); + test('streamVisualNovelRuntimeAction posts to the SSE action stream route', async () => { const response = createSseResponse( [ @@ -146,6 +174,10 @@ test('streamVisualNovelRuntimeAction posts to the SSE action stream route', asyn }), signal: undefined, }), + expect.objectContaining({ + skipAuth: undefined, + skipRefresh: undefined, + }), ); expect(result).toMatchObject({ runId: 'vn-run-route-1' }); }); diff --git a/src/services/visual-novel-runtime/visualNovelRuntimeClient.ts b/src/services/visual-novel-runtime/visualNovelRuntimeClient.ts index 8f29ffe4..527d165b 100644 --- a/src/services/visual-novel-runtime/visualNovelRuntimeClient.ts +++ b/src/services/visual-novel-runtime/visualNovelRuntimeClient.ts @@ -23,12 +23,13 @@ import { fetchWithApiAuth, requestJson, } from '../apiClient'; -import { readVisualNovelRuntimeRunFromSse } from './visualNovelRuntimeSse'; import { buildRuntimeGuestAuthOptions, buildRuntimeGuestHeaders, type RuntimeGuestRequestOptions, } from '../runtimeGuestAuth'; +import { buildRuntimeApiPath, requestRuntimeJson } from '../runtimeRequest'; +import { readVisualNovelRuntimeRunFromSse } from './visualNovelRuntimeSse'; const VISUAL_NOVEL_RUNTIME_API_BASE = '/api/runtime/visual-novel'; const VISUAL_NOVEL_RUNTIME_READ_RETRY: ApiRetryOptions = { @@ -57,17 +58,13 @@ export type VisualNovelSaveArchiveResumeResponse = >; export async function listVisualNovelGallery() { - return requestJson( - `${VISUAL_NOVEL_RUNTIME_API_BASE}/gallery`, - { method: 'GET' }, - '读取视觉小说公开作品列表失败', - { - retry: VISUAL_NOVEL_RUNTIME_READ_RETRY, - // 中文注释:公开广场是游客可读入口,避免未登录态先触发 refresh 再读取公开列表。 - skipAuth: true, - skipRefresh: true, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath(VISUAL_NOVEL_RUNTIME_API_BASE, 'gallery'), + fallbackMessage: '读取视觉小说公开作品列表失败', + retry: VISUAL_NOVEL_RUNTIME_READ_RETRY, + // 中文注释:公开广场是游客可读入口,避免未登录态先触发 refresh 再读取公开列表。 + requestOptions: { skipAuth: true, skipRefresh: true }, + }); } function buildJsonInit(method: 'POST' | 'PUT', payload: unknown): RequestInit { @@ -117,7 +114,12 @@ export async function startVisualNovelRun( ) { const requestOptions = buildRuntimeGuestAuthOptions(options); return requestJson( - `${VISUAL_NOVEL_RUNTIME_API_BASE}/works/${encodeURIComponent(profileId)}/runs`, + buildRuntimeApiPath( + VISUAL_NOVEL_RUNTIME_API_BASE, + 'works', + profileId, + 'runs', + ), { ...buildJsonInit('POST', payload), headers: buildRuntimeGuestHeaders(options, { @@ -134,25 +136,24 @@ export async function startVisualNovelRun( } export async function getVisualNovelRun(runId: string) { - return requestJson( - `${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}`, - { method: 'GET' }, - '读取视觉小说运行快照失败', - { - retry: VISUAL_NOVEL_RUNTIME_READ_RETRY, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath(VISUAL_NOVEL_RUNTIME_API_BASE, 'runs', runId), + fallbackMessage: '读取视觉小说运行快照失败', + retry: VISUAL_NOVEL_RUNTIME_READ_RETRY, + }); } export async function getVisualNovelHistory(runId: string) { - return requestJson( - `${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/history`, - { method: 'GET' }, - '读取视觉小说历史失败', - { - retry: VISUAL_NOVEL_RUNTIME_READ_RETRY, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath( + VISUAL_NOVEL_RUNTIME_API_BASE, + 'runs', + runId, + 'history', + ), + fallbackMessage: '读取视觉小说历史失败', + retry: VISUAL_NOVEL_RUNTIME_READ_RETRY, + }); } export async function streamVisualNovelRuntimeAction( @@ -161,7 +162,13 @@ export async function streamVisualNovelRuntimeAction( options: VisualNovelRuntimeStreamOptions = {}, ) { const response = await openVisualNovelRuntimeSsePost( - `${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/actions/stream`, + buildRuntimeApiPath( + VISUAL_NOVEL_RUNTIME_API_BASE, + 'runs', + runId, + 'actions', + 'stream', + ), payload, '推进视觉小说失败', options.signal, @@ -179,14 +186,18 @@ export async function regenerateVisualNovelRun( runId: string, payload: VisualNovelRegenerateRequest, ) { - return requestJson( - `${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/regenerate`, - buildJsonInit('POST', payload), - '重生成视觉小说历史失败', - { - retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath( + VISUAL_NOVEL_RUNTIME_API_BASE, + 'runs', + runId, + 'regenerate', + ), + method: 'POST', + jsonBody: payload, + fallbackMessage: '重生成视觉小说历史失败', + retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY, + }); } export async function listVisualNovelSaveArchives(profileId?: string | null) { diff --git a/src/services/wooden-fish/woodenFishClient.test.ts b/src/services/wooden-fish/woodenFishClient.test.ts index aef88dee..aea47b59 100644 --- a/src/services/wooden-fish/woodenFishClient.test.ts +++ b/src/services/wooden-fish/woodenFishClient.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, expect, test, vi } from 'vitest'; +import { afterEach, beforeEach, expect, test, vi } from 'vitest'; const requestJsonMock = vi.hoisted(() => vi.fn()); @@ -27,6 +27,10 @@ beforeEach(() => { requestJsonMock.mockReset(); }); +afterEach(() => { + vi.restoreAllMocks(); +}); + test('wooden fish creation keeps image2 generation requests alive long enough', async () => { await import('./woodenFishClient'); @@ -50,3 +54,86 @@ test('wooden fish list works uses creation works endpoint', async () => { '读取敲木鱼作品列表失败', ); }); + +test('wooden fish start run uses runtime guest json skeleton', async () => { + const { woodenFishClient } = await import('./woodenFishClient'); + requestJsonMock.mockResolvedValueOnce({ run: { runId: 'run-1' } }); + + await woodenFishClient.startRun('profile/1', { + runtimeGuestToken: 'runtime-guest-token', + }); + + expect(requestJsonMock).toHaveBeenCalledWith( + '/api/runtime/wooden-fish/runs', + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer runtime-guest-token', + }, + body: JSON.stringify({ profileId: 'profile/1' }), + }), + '启动敲木鱼运行态失败', + expect.objectContaining({ + retry: expect.objectContaining({ retryUnsafeMethods: true }), + skipAuth: true, + skipRefresh: true, + }), + ); +}); + +test('wooden fish checkpoint run keeps client event id local to the client', async () => { + const { woodenFishClient } = await import('./woodenFishClient'); + vi.spyOn(Date, 'now').mockReturnValue(1780000000000); + requestJsonMock.mockResolvedValueOnce({ run: { runId: 'run-1' } }); + + await woodenFishClient.checkpointRun( + 'run/1', + { + totalTapCount: 12, + wordCounters: [{ text: '功德', count: 3 }], + }, + { runtimeGuestToken: 'runtime-guest-token' }, + ); + + const [, init] = requestJsonMock.mock.calls[0]; + const body = JSON.parse(init.body); + expect(requestJsonMock.mock.calls[0][0]).toBe( + '/api/runtime/wooden-fish/runs/run%2F1/checkpoint', + ); + expect(body).toEqual({ + totalTapCount: 12, + wordCounters: [{ text: '功德', count: 3 }], + clientEventId: 'checkpoint-run/1-1780000000000', + }); + expect(body).not.toHaveProperty('runId'); + expect(body).not.toHaveProperty('checkpointAtMs'); +}); + +test('wooden fish finish run keeps finish event id local to the client', async () => { + const { woodenFishClient } = await import('./woodenFishClient'); + vi.spyOn(Date, 'now').mockReturnValue(1780000000001); + requestJsonMock.mockResolvedValueOnce({ run: { runId: 'run-1' } }); + + await woodenFishClient.finishRun( + 'run/1', + { + totalTapCount: 18, + wordCounters: [{ text: '清净', count: 2 }], + }, + { runtimeGuestToken: 'runtime-guest-token' }, + ); + + const [, init] = requestJsonMock.mock.calls[0]; + const body = JSON.parse(init.body); + expect(requestJsonMock.mock.calls[0][0]).toBe( + '/api/runtime/wooden-fish/runs/run%2F1/finish', + ); + expect(body).toEqual({ + totalTapCount: 18, + wordCounters: [{ text: '清净', count: 2 }], + clientEventId: 'finish-run/1-1780000000001', + }); + expect(body).not.toHaveProperty('runId'); + expect(body).not.toHaveProperty('finishedAtMs'); +}); diff --git a/src/services/wooden-fish/woodenFishClient.ts b/src/services/wooden-fish/woodenFishClient.ts index f6f31005..8d5efd68 100644 --- a/src/services/wooden-fish/woodenFishClient.ts +++ b/src/services/wooden-fish/woodenFishClient.ts @@ -13,17 +13,14 @@ import type { WoodenFishWorkDetailResponse, WoodenFishWorkMutationResponse, WoodenFishWorkProfileResponse, - WoodenFishWorksResponse, WoodenFishWorkspaceCreateRequest, + WoodenFishWorksResponse, WoodenFishWorkSummaryResponse, } from '../../../packages/shared/src/contracts/woodenFish'; import { type ApiRetryOptions, 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 WOODEN_FISH_API_BASE = '/api/creation/wooden-fish/sessions'; const WOODEN_FISH_WORKS_API_BASE = '/api/creation/wooden-fish/works'; @@ -58,8 +55,8 @@ export type { WoodenFishWorkDetailResponse, WoodenFishWorkMutationResponse, WoodenFishWorkProfileResponse, - WoodenFishWorksResponse, WoodenFishWorkspaceCreateRequest, + WoodenFishWorksResponse, }; export type CreateWoodenFishSessionRequest = WoodenFishWorkspaceCreateRequest; export type WoodenFishSessionSnapshot = WoodenFishSessionSnapshotResponse; @@ -237,23 +234,14 @@ export async function startWoodenFishRuntimeRun( profileId: string, options: WoodenFishRuntimeRequestOptions = {}, ) { - const requestOptions = buildRuntimeGuestAuthOptions(options); - return requestJson( - `${WOODEN_FISH_RUNTIME_API_BASE}/runs`, - { - method: 'POST', - headers: { - 'content-type': 'application/json', - ...buildRuntimeGuestHeaders(options), - }, - body: JSON.stringify({ profileId }), - }, - '启动敲木鱼运行态失败', - { - retry: WOODEN_FISH_RUNTIME_WRITE_RETRY, - ...requestOptions, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath(WOODEN_FISH_RUNTIME_API_BASE, 'runs'), + method: 'POST', + jsonBody: { profileId }, + fallbackMessage: '启动敲木鱼运行态失败', + retry: WOODEN_FISH_RUNTIME_WRITE_RETRY, + requestOptions: options, + }); } export async function checkpointWoodenFishRun( @@ -261,28 +249,24 @@ export async function checkpointWoodenFishRun( payload: Omit, options: WoodenFishRuntimeRequestOptions = {}, ) { - const requestOptions = buildRuntimeGuestAuthOptions(options); const requestPayload: WoodenFishCheckpointRunRequest = { ...payload, clientEventId: `checkpoint-${runId}-${Date.now()}`, }; - return requestJson( - `${WOODEN_FISH_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/checkpoint`, - { - method: 'POST', - headers: { - 'content-type': 'application/json', - ...buildRuntimeGuestHeaders(options), - }, - body: JSON.stringify(requestPayload), - }, - '保存敲木鱼进度失败', - { - retry: WOODEN_FISH_RUNTIME_WRITE_RETRY, - ...requestOptions, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath( + WOODEN_FISH_RUNTIME_API_BASE, + 'runs', + runId, + 'checkpoint', + ), + method: 'POST', + jsonBody: requestPayload, + fallbackMessage: '保存敲木鱼进度失败', + retry: WOODEN_FISH_RUNTIME_WRITE_RETRY, + requestOptions: options, + }); } export async function finishWoodenFishRun( @@ -290,28 +274,24 @@ export async function finishWoodenFishRun( payload: Omit, options: WoodenFishRuntimeRequestOptions = {}, ) { - const requestOptions = buildRuntimeGuestAuthOptions(options); const requestPayload: WoodenFishFinishRunRequest = { ...payload, clientEventId: `finish-${runId}-${Date.now()}`, }; - return requestJson( - `${WOODEN_FISH_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/finish`, - { - method: 'POST', - headers: { - 'content-type': 'application/json', - ...buildRuntimeGuestHeaders(options), - }, - body: JSON.stringify(requestPayload), - }, - '结束敲木鱼运行失败', - { - retry: WOODEN_FISH_RUNTIME_WRITE_RETRY, - ...requestOptions, - }, - ); + return requestRuntimeJson({ + url: buildRuntimeApiPath( + WOODEN_FISH_RUNTIME_API_BASE, + 'runs', + runId, + 'finish', + ), + method: 'POST', + jsonBody: requestPayload, + fallbackMessage: '结束敲木鱼运行失败', + retry: WOODEN_FISH_RUNTIME_WRITE_RETRY, + requestOptions: options, + }); } export const woodenFishClient = {