refactor: 迁移视觉小说与木鱼 runtime 请求骨架
This commit is contained in:
@@ -46,7 +46,8 @@
|
|||||||
- 决策:新增 `src/services/runtimeRequest.ts`,以 `buildRuntimeApiPath` 统一 runtime path 编码,以 `requestRuntimeJson` 统一 JSON 请求、runtime guest auth 和 retry 合并。Match3D 与 SquareHole runtime client 已先迁移,保留原导出函数名、错误文案、返回契约和重试常量。
|
- 决策:新增 `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 只承接请求骨架。
|
- 追加决策: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 与 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 通过。
|
- 验证方式:`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`。
|
- 关联文档:`docs/technical/【前端架构】RuntimeClientFamily收口计划-2026-06-03.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)。
|
创作中心作品架打开动作由 `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)。
|
公开作品分类、搜索、跨来源去重、今日筛选、排行排序和时间戳解析收口到 `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)。
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
- `buildRuntimeApiPath(basePath, ...segments)`:统一对 runtime path segment 执行 `encodeURIComponent`。
|
- `buildRuntimeApiPath(basePath, ...segments)`:统一对 runtime path segment 执行 `encodeURIComponent`。
|
||||||
- `requestRuntimeJson(params)`:统一设置 method、JSON body、`Content-Type`、runtime guest `Authorization`、auth options 和 retry options。
|
- `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。
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ vi.mock('../apiClient', () => ({
|
|||||||
requestJson: requestJsonMock,
|
requestJson: requestJsonMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
import type { VisualNovelRunSnapshot } from '../../../packages/shared/src/contracts/visualNovel';
|
||||||
import {
|
import {
|
||||||
buildVisualNovelRuntimeCheckpoint,
|
buildVisualNovelRuntimeCheckpoint,
|
||||||
buildVisualNovelSaveArchiveState,
|
buildVisualNovelSaveArchiveState,
|
||||||
type VisualNovelRuntimeStreamOptions,
|
getVisualNovelHistory,
|
||||||
|
getVisualNovelRun,
|
||||||
listVisualNovelGallery,
|
listVisualNovelGallery,
|
||||||
listVisualNovelSaveArchives,
|
listVisualNovelSaveArchives,
|
||||||
putVisualNovelRuntimeSnapshot,
|
putVisualNovelRuntimeSnapshot,
|
||||||
@@ -21,8 +23,8 @@ import {
|
|||||||
resumeVisualNovelSaveArchive,
|
resumeVisualNovelSaveArchive,
|
||||||
startVisualNovelRun,
|
startVisualNovelRun,
|
||||||
streamVisualNovelRuntimeAction,
|
streamVisualNovelRuntimeAction,
|
||||||
|
type VisualNovelRuntimeStreamOptions,
|
||||||
} from './visualNovelRuntimeClient';
|
} from './visualNovelRuntimeClient';
|
||||||
import type { VisualNovelRunSnapshot } from '../../../packages/shared/src/contracts/visualNovel';
|
|
||||||
|
|
||||||
function createMockRun(
|
function createMockRun(
|
||||||
overrides: Partial<VisualNovelRunSnapshot> = {},
|
overrides: Partial<VisualNovelRunSnapshot> = {},
|
||||||
@@ -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 () => {
|
test('streamVisualNovelRuntimeAction posts to the SSE action stream route', async () => {
|
||||||
const response = createSseResponse(
|
const response = createSseResponse(
|
||||||
[
|
[
|
||||||
@@ -146,6 +174,10 @@ test('streamVisualNovelRuntimeAction posts to the SSE action stream route', asyn
|
|||||||
}),
|
}),
|
||||||
signal: undefined,
|
signal: undefined,
|
||||||
}),
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
skipAuth: undefined,
|
||||||
|
skipRefresh: undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(result).toMatchObject({ runId: 'vn-run-route-1' });
|
expect(result).toMatchObject({ runId: 'vn-run-route-1' });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ import {
|
|||||||
fetchWithApiAuth,
|
fetchWithApiAuth,
|
||||||
requestJson,
|
requestJson,
|
||||||
} from '../apiClient';
|
} from '../apiClient';
|
||||||
import { readVisualNovelRuntimeRunFromSse } from './visualNovelRuntimeSse';
|
|
||||||
import {
|
import {
|
||||||
buildRuntimeGuestAuthOptions,
|
buildRuntimeGuestAuthOptions,
|
||||||
buildRuntimeGuestHeaders,
|
buildRuntimeGuestHeaders,
|
||||||
type RuntimeGuestRequestOptions,
|
type RuntimeGuestRequestOptions,
|
||||||
} from '../runtimeGuestAuth';
|
} 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_API_BASE = '/api/runtime/visual-novel';
|
||||||
const VISUAL_NOVEL_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
const VISUAL_NOVEL_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||||
@@ -57,17 +58,13 @@ export type VisualNovelSaveArchiveResumeResponse =
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export async function listVisualNovelGallery() {
|
export async function listVisualNovelGallery() {
|
||||||
return requestJson<VisualNovelWorksResponse>(
|
return requestRuntimeJson<VisualNovelWorksResponse>({
|
||||||
`${VISUAL_NOVEL_RUNTIME_API_BASE}/gallery`,
|
url: buildRuntimeApiPath(VISUAL_NOVEL_RUNTIME_API_BASE, 'gallery'),
|
||||||
{ method: 'GET' },
|
fallbackMessage: '读取视觉小说公开作品列表失败',
|
||||||
'读取视觉小说公开作品列表失败',
|
|
||||||
{
|
|
||||||
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
|
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
|
||||||
// 中文注释:公开广场是游客可读入口,避免未登录态先触发 refresh 再读取公开列表。
|
// 中文注释:公开广场是游客可读入口,避免未登录态先触发 refresh 再读取公开列表。
|
||||||
skipAuth: true,
|
requestOptions: { skipAuth: true, skipRefresh: true },
|
||||||
skipRefresh: true,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildJsonInit(method: 'POST' | 'PUT', payload: unknown): RequestInit {
|
function buildJsonInit(method: 'POST' | 'PUT', payload: unknown): RequestInit {
|
||||||
@@ -117,7 +114,12 @@ export async function startVisualNovelRun(
|
|||||||
) {
|
) {
|
||||||
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<VisualNovelRunResponse>(
|
return requestJson<VisualNovelRunResponse>(
|
||||||
`${VISUAL_NOVEL_RUNTIME_API_BASE}/works/${encodeURIComponent(profileId)}/runs`,
|
buildRuntimeApiPath(
|
||||||
|
VISUAL_NOVEL_RUNTIME_API_BASE,
|
||||||
|
'works',
|
||||||
|
profileId,
|
||||||
|
'runs',
|
||||||
|
),
|
||||||
{
|
{
|
||||||
...buildJsonInit('POST', payload),
|
...buildJsonInit('POST', payload),
|
||||||
headers: buildRuntimeGuestHeaders(options, {
|
headers: buildRuntimeGuestHeaders(options, {
|
||||||
@@ -134,25 +136,24 @@ export async function startVisualNovelRun(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getVisualNovelRun(runId: string) {
|
export async function getVisualNovelRun(runId: string) {
|
||||||
return requestJson<VisualNovelRunResponse>(
|
return requestRuntimeJson<VisualNovelRunResponse>({
|
||||||
`${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}`,
|
url: buildRuntimeApiPath(VISUAL_NOVEL_RUNTIME_API_BASE, 'runs', runId),
|
||||||
{ method: 'GET' },
|
fallbackMessage: '读取视觉小说运行快照失败',
|
||||||
'读取视觉小说运行快照失败',
|
|
||||||
{
|
|
||||||
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
|
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getVisualNovelHistory(runId: string) {
|
export async function getVisualNovelHistory(runId: string) {
|
||||||
return requestJson<VisualNovelHistoryResponse>(
|
return requestRuntimeJson<VisualNovelHistoryResponse>({
|
||||||
`${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/history`,
|
url: buildRuntimeApiPath(
|
||||||
{ method: 'GET' },
|
VISUAL_NOVEL_RUNTIME_API_BASE,
|
||||||
'读取视觉小说历史失败',
|
'runs',
|
||||||
{
|
runId,
|
||||||
|
'history',
|
||||||
|
),
|
||||||
|
fallbackMessage: '读取视觉小说历史失败',
|
||||||
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
|
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function streamVisualNovelRuntimeAction(
|
export async function streamVisualNovelRuntimeAction(
|
||||||
@@ -161,7 +162,13 @@ export async function streamVisualNovelRuntimeAction(
|
|||||||
options: VisualNovelRuntimeStreamOptions = {},
|
options: VisualNovelRuntimeStreamOptions = {},
|
||||||
) {
|
) {
|
||||||
const response = await openVisualNovelRuntimeSsePost(
|
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,
|
payload,
|
||||||
'推进视觉小说失败',
|
'推进视觉小说失败',
|
||||||
options.signal,
|
options.signal,
|
||||||
@@ -179,14 +186,18 @@ export async function regenerateVisualNovelRun(
|
|||||||
runId: string,
|
runId: string,
|
||||||
payload: VisualNovelRegenerateRequest,
|
payload: VisualNovelRegenerateRequest,
|
||||||
) {
|
) {
|
||||||
return requestJson<VisualNovelRunResponse>(
|
return requestRuntimeJson<VisualNovelRunResponse>({
|
||||||
`${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/regenerate`,
|
url: buildRuntimeApiPath(
|
||||||
buildJsonInit('POST', payload),
|
VISUAL_NOVEL_RUNTIME_API_BASE,
|
||||||
'重生成视觉小说历史失败',
|
'runs',
|
||||||
{
|
runId,
|
||||||
|
'regenerate',
|
||||||
|
),
|
||||||
|
method: 'POST',
|
||||||
|
jsonBody: payload,
|
||||||
|
fallbackMessage: '重生成视觉小说历史失败',
|
||||||
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
|
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listVisualNovelSaveArchives(profileId?: string | null) {
|
export async function listVisualNovelSaveArchives(profileId?: string | null) {
|
||||||
|
|||||||
@@ -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());
|
const requestJsonMock = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
@@ -27,6 +27,10 @@ beforeEach(() => {
|
|||||||
requestJsonMock.mockReset();
|
requestJsonMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
test('wooden fish creation keeps image2 generation requests alive long enough', async () => {
|
test('wooden fish creation keeps image2 generation requests alive long enough', async () => {
|
||||||
await import('./woodenFishClient');
|
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');
|
||||||
|
});
|
||||||
|
|||||||
@@ -13,17 +13,14 @@ import type {
|
|||||||
WoodenFishWorkDetailResponse,
|
WoodenFishWorkDetailResponse,
|
||||||
WoodenFishWorkMutationResponse,
|
WoodenFishWorkMutationResponse,
|
||||||
WoodenFishWorkProfileResponse,
|
WoodenFishWorkProfileResponse,
|
||||||
WoodenFishWorksResponse,
|
|
||||||
WoodenFishWorkspaceCreateRequest,
|
WoodenFishWorkspaceCreateRequest,
|
||||||
|
WoodenFishWorksResponse,
|
||||||
WoodenFishWorkSummaryResponse,
|
WoodenFishWorkSummaryResponse,
|
||||||
} from '../../../packages/shared/src/contracts/woodenFish';
|
} from '../../../packages/shared/src/contracts/woodenFish';
|
||||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||||
import { createCreationAgentClient } from '../creation-agent';
|
import { createCreationAgentClient } from '../creation-agent';
|
||||||
import {
|
import { type RuntimeGuestRequestOptions } from '../runtimeGuestAuth';
|
||||||
buildRuntimeGuestAuthOptions,
|
import { buildRuntimeApiPath, requestRuntimeJson } from '../runtimeRequest';
|
||||||
buildRuntimeGuestHeaders,
|
|
||||||
type RuntimeGuestRequestOptions,
|
|
||||||
} from '../runtimeGuestAuth';
|
|
||||||
|
|
||||||
const WOODEN_FISH_API_BASE = '/api/creation/wooden-fish/sessions';
|
const WOODEN_FISH_API_BASE = '/api/creation/wooden-fish/sessions';
|
||||||
const WOODEN_FISH_WORKS_API_BASE = '/api/creation/wooden-fish/works';
|
const WOODEN_FISH_WORKS_API_BASE = '/api/creation/wooden-fish/works';
|
||||||
@@ -58,8 +55,8 @@ export type {
|
|||||||
WoodenFishWorkDetailResponse,
|
WoodenFishWorkDetailResponse,
|
||||||
WoodenFishWorkMutationResponse,
|
WoodenFishWorkMutationResponse,
|
||||||
WoodenFishWorkProfileResponse,
|
WoodenFishWorkProfileResponse,
|
||||||
WoodenFishWorksResponse,
|
|
||||||
WoodenFishWorkspaceCreateRequest,
|
WoodenFishWorkspaceCreateRequest,
|
||||||
|
WoodenFishWorksResponse,
|
||||||
};
|
};
|
||||||
export type CreateWoodenFishSessionRequest = WoodenFishWorkspaceCreateRequest;
|
export type CreateWoodenFishSessionRequest = WoodenFishWorkspaceCreateRequest;
|
||||||
export type WoodenFishSessionSnapshot = WoodenFishSessionSnapshotResponse;
|
export type WoodenFishSessionSnapshot = WoodenFishSessionSnapshotResponse;
|
||||||
@@ -237,23 +234,14 @@ export async function startWoodenFishRuntimeRun(
|
|||||||
profileId: string,
|
profileId: string,
|
||||||
options: WoodenFishRuntimeRequestOptions = {},
|
options: WoodenFishRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
return requestRuntimeJson<WoodenFishRunResponse>({
|
||||||
return requestJson<WoodenFishRunResponse>(
|
url: buildRuntimeApiPath(WOODEN_FISH_RUNTIME_API_BASE, 'runs'),
|
||||||
`${WOODEN_FISH_RUNTIME_API_BASE}/runs`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
jsonBody: { profileId },
|
||||||
'content-type': 'application/json',
|
fallbackMessage: '启动敲木鱼运行态失败',
|
||||||
...buildRuntimeGuestHeaders(options),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ profileId }),
|
|
||||||
},
|
|
||||||
'启动敲木鱼运行态失败',
|
|
||||||
{
|
|
||||||
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
|
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
|
||||||
...requestOptions,
|
requestOptions: options,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkpointWoodenFishRun(
|
export async function checkpointWoodenFishRun(
|
||||||
@@ -261,28 +249,24 @@ export async function checkpointWoodenFishRun(
|
|||||||
payload: Omit<WoodenFishCheckpointRunRequest, 'clientEventId'>,
|
payload: Omit<WoodenFishCheckpointRunRequest, 'clientEventId'>,
|
||||||
options: WoodenFishRuntimeRequestOptions = {},
|
options: WoodenFishRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
|
||||||
const requestPayload: WoodenFishCheckpointRunRequest = {
|
const requestPayload: WoodenFishCheckpointRunRequest = {
|
||||||
...payload,
|
...payload,
|
||||||
clientEventId: `checkpoint-${runId}-${Date.now()}`,
|
clientEventId: `checkpoint-${runId}-${Date.now()}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return requestJson<WoodenFishRunResponse>(
|
return requestRuntimeJson<WoodenFishRunResponse>({
|
||||||
`${WOODEN_FISH_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/checkpoint`,
|
url: buildRuntimeApiPath(
|
||||||
{
|
WOODEN_FISH_RUNTIME_API_BASE,
|
||||||
|
'runs',
|
||||||
|
runId,
|
||||||
|
'checkpoint',
|
||||||
|
),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
jsonBody: requestPayload,
|
||||||
'content-type': 'application/json',
|
fallbackMessage: '保存敲木鱼进度失败',
|
||||||
...buildRuntimeGuestHeaders(options),
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestPayload),
|
|
||||||
},
|
|
||||||
'保存敲木鱼进度失败',
|
|
||||||
{
|
|
||||||
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
|
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
|
||||||
...requestOptions,
|
requestOptions: options,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function finishWoodenFishRun(
|
export async function finishWoodenFishRun(
|
||||||
@@ -290,28 +274,24 @@ export async function finishWoodenFishRun(
|
|||||||
payload: Omit<WoodenFishFinishRunRequest, 'clientEventId'>,
|
payload: Omit<WoodenFishFinishRunRequest, 'clientEventId'>,
|
||||||
options: WoodenFishRuntimeRequestOptions = {},
|
options: WoodenFishRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
|
||||||
const requestPayload: WoodenFishFinishRunRequest = {
|
const requestPayload: WoodenFishFinishRunRequest = {
|
||||||
...payload,
|
...payload,
|
||||||
clientEventId: `finish-${runId}-${Date.now()}`,
|
clientEventId: `finish-${runId}-${Date.now()}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return requestJson<WoodenFishRunResponse>(
|
return requestRuntimeJson<WoodenFishRunResponse>({
|
||||||
`${WOODEN_FISH_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/finish`,
|
url: buildRuntimeApiPath(
|
||||||
{
|
WOODEN_FISH_RUNTIME_API_BASE,
|
||||||
|
'runs',
|
||||||
|
runId,
|
||||||
|
'finish',
|
||||||
|
),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
jsonBody: requestPayload,
|
||||||
'content-type': 'application/json',
|
fallbackMessage: '结束敲木鱼运行失败',
|
||||||
...buildRuntimeGuestHeaders(options),
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestPayload),
|
|
||||||
},
|
|
||||||
'结束敲木鱼运行失败',
|
|
||||||
{
|
|
||||||
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
|
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
|
||||||
...requestOptions,
|
requestOptions: options,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const woodenFishClient = {
|
export const woodenFishClient = {
|
||||||
|
|||||||
Reference in New Issue
Block a user