feat: unify recommend anonymous runtime guest auth

- Route recommended runtime launches through shared runtime guest token handling
- Extend recommend-page anonymous play beyond jump-hop
- Add regression coverage for runtime guest launch clients
- Update docs to reflect the full anonymous-play matrix
This commit is contained in:
kdletters
2026-05-25 14:03:38 +08:00
parent 9a0bc6b129
commit c1dcf074bb
23 changed files with 820 additions and 236 deletions

View File

@@ -25,6 +25,7 @@ import type {
AuthWechatStartResponse,
LogoutResponse,
PublicUserSearchResponse,
RuntimeGuestTokenResponse,
} from '../../packages/shared/src/contracts/auth';
import type { RedeemProfileReferralInviteCodeResponse } from '../../packages/shared/src/contracts/runtime';
import {
@@ -61,6 +62,42 @@ const PUBLIC_AUTH_REQUEST_OPTIONS = {
skipRefresh: true,
} satisfies ApiRequestOptions;
const runtimeGuestTokenCache: {
value: RuntimeGuestTokenResponse | null;
} = {
value: null,
};
function isRuntimeGuestTokenFresh(response: RuntimeGuestTokenResponse | null) {
if (!response?.expiresAt) {
return false;
}
const expiresAtMs = Date.parse(response.expiresAt);
return Number.isFinite(expiresAtMs) && expiresAtMs - Date.now() > 15_000;
}
export function clearRuntimeGuestTokenCache() {
runtimeGuestTokenCache.value = null;
}
export async function ensureRuntimeGuestToken() {
if (isRuntimeGuestTokenFresh(runtimeGuestTokenCache.value)) {
return runtimeGuestTokenCache.value!;
}
const response = await requestJson<RuntimeGuestTokenResponse>(
'/api/auth/runtime-guest-token',
{
method: 'POST',
},
'获取匿名运行态身份失败',
PUBLIC_AUTH_REQUEST_OPTIONS,
);
runtimeGuestTokenCache.value = response;
return response;
}
const LAST_LOGIN_PHONE_STORAGE_KEY = 'genarrative:last-login-phone';
export function normalizePhoneInput(phoneInput: string) {

View File

@@ -6,10 +6,14 @@ import type {
BarkBattleRuntimeConfig,
} from '../../../packages/shared/src/contracts/barkBattle';
import {
type ApiRequestOptions,
type ApiRetryOptions,
requestJson,
} from '../apiClient';
import {
buildRuntimeGuestAuthOptions,
buildRuntimeGuestHeaders,
type RuntimeGuestRequestOptions,
} from '../runtimeGuestAuth';
const BARK_BATTLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
@@ -24,28 +28,20 @@ const BARK_BATTLE_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
retryUnsafeMethods: true,
};
export type BarkBattleRuntimeRequestOptions = Pick<
ApiRequestOptions,
| 'authImpact'
| 'skipRefresh'
| 'notifyAuthStateChange'
| 'clearAuthOnUnauthorized'
>;
export type BarkBattleRuntimeRequestOptions = RuntimeGuestRequestOptions;
export function getBarkBattleRuntimeConfig(
workId: string,
options: BarkBattleRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<BarkBattleRuntimeConfig>(
`/api/runtime/bark-battle/works/${encodeURIComponent(workId)}/config`,
{ method: 'GET' },
{ method: 'GET', headers: buildRuntimeGuestHeaders(options) },
'读取汪汪声浪大作战配置失败',
{
retry: BARK_BATTLE_RUNTIME_READ_RETRY,
authImpact: options.authImpact,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}
@@ -55,11 +51,12 @@ export function startBarkBattleRun(
payload: Partial<BarkBattleRunStartRequest> = {},
options: BarkBattleRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<BarkBattleRunStartResponse>(
`/api/runtime/bark-battle/works/${encodeURIComponent(workId)}/runs`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: buildRuntimeGuestHeaders(options, { 'Content-Type': 'application/json' }),
body: JSON.stringify({
...payload,
workId: payload.workId ?? workId,
@@ -68,10 +65,7 @@ export function startBarkBattleRun(
'启动汪汪声浪大作战正式局失败',
{
retry: BARK_BATTLE_RUNTIME_WRITE_RETRY,
authImpact: options.authImpact,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}
@@ -80,16 +74,14 @@ export function getBarkBattleRun(
runId: string,
options: BarkBattleRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<unknown>(
`/api/runtime/bark-battle/runs/${encodeURIComponent(runId)}`,
{ method: 'GET' },
{ method: 'GET', headers: buildRuntimeGuestHeaders(options) },
'读取汪汪声浪大作战单局失败',
{
retry: BARK_BATTLE_RUNTIME_READ_RETRY,
authImpact: options.authImpact,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}
@@ -99,11 +91,12 @@ export function finishBarkBattleRun(
payload: BarkBattleRunFinishRequest,
options: BarkBattleRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<BarkBattleFinishResponse>(
`/api/runtime/bark-battle/runs/${encodeURIComponent(runId)}/finish`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: buildRuntimeGuestHeaders(options, { 'Content-Type': 'application/json' }),
body: JSON.stringify({
...payload,
runId: payload.runId ?? runId,
@@ -112,10 +105,7 @@ export function finishBarkBattleRun(
'提交汪汪声浪大作战成绩失败',
{
retry: BARK_BATTLE_RUNTIME_WRITE_RETRY,
authImpact: options.authImpact,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}

View File

@@ -5,10 +5,14 @@ import type {
} from '../../../packages/shared/src/contracts/bigFish';
import type { BigFishWorksResponse } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import {
type ApiRequestOptions,
type ApiRetryOptions,
requestJson,
} from '../apiClient';
import {
buildRuntimeGuestAuthOptions,
buildRuntimeGuestHeaders,
type RuntimeGuestRequestOptions,
} from '../runtimeGuestAuth';
const BIG_FISH_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
@@ -16,13 +20,7 @@ const BIG_FISH_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
maxDelayMs: 360,
retryUnsafeMethods: true,
};
type BigFishRuntimeRequestOptions = Pick<
ApiRequestOptions,
| 'authImpact'
| 'skipRefresh'
| 'notifyAuthStateChange'
| 'clearAuthOnUnauthorized'
>;
type BigFishRuntimeRequestOptions = RuntimeGuestRequestOptions;
/**
* 上报大鱼吃小鱼正式游玩。elapsedMs 为 0 时仅标记玩过作品。
@@ -32,20 +30,20 @@ export function recordBigFishPlay(
payload: RecordBigFishPlayRequest,
options: BigFishRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<BigFishWorksResponse>(
`/api/runtime/big-fish/sessions/${encodeURIComponent(sessionId)}/play`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: buildRuntimeGuestHeaders(options, {
'Content-Type': 'application/json',
}),
body: JSON.stringify(payload),
},
'记录大鱼吃小鱼游玩失败',
{
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
authImpact: options.authImpact,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}
@@ -54,18 +52,17 @@ export function startBigFishRun(
sessionId: string,
options: BigFishRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<BigFishRunResponse>(
`/api/runtime/big-fish/sessions/${encodeURIComponent(sessionId)}/runs`,
{
method: 'POST',
headers: buildRuntimeGuestHeaders(options),
},
'启动大鱼吃小鱼玩法失败',
{
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
authImpact: options.authImpact,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}
@@ -83,17 +80,22 @@ export function getBigFishRun(runId: string) {
export function submitBigFishInput(
runId: string,
payload: SubmitBigFishInputRequest,
options: BigFishRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<BigFishRunResponse>(
`/api/runtime/big-fish/runs/${encodeURIComponent(runId)}/input`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: buildRuntimeGuestHeaders(options, {
'Content-Type': 'application/json',
}),
body: JSON.stringify(payload),
},
'同步大鱼吃小鱼输入失败',
{
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
...requestOptions,
},
);
}

View File

@@ -17,11 +17,15 @@ import type {
JumpHopWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/jumpHop';
import {
type ApiRequestOptions,
type ApiRetryOptions,
requestJson,
} from '../apiClient';
import { createCreationAgentClient } from '../creation-agent';
import {
buildRuntimeGuestAuthOptions,
buildRuntimeGuestHeaders,
type RuntimeGuestRequestOptions,
} from '../runtimeGuestAuth';
const JUMP_HOP_API_BASE = '/api/creation/jump-hop/sessions';
const JUMP_HOP_WORKS_API_BASE = '/api/creation/jump-hop/works';
@@ -31,14 +35,7 @@ const JUMP_HOP_RUNTIME_READ_RETRY: ApiRetryOptions = {
baseDelayMs: 120,
maxDelayMs: 360,
};
type JumpHopRuntimeRequestOptions = Pick<
ApiRequestOptions,
| 'authImpact'
| 'skipAuth'
| 'skipRefresh'
| 'notifyAuthStateChange'
| 'clearAuthOnUnauthorized'
>;
type JumpHopRuntimeRequestOptions = RuntimeGuestRequestOptions;
export type {
JumpHopActionRequest,
@@ -237,22 +234,20 @@ export async function startJumpHopRuntimeRun(
profileId: string,
options: JumpHopRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<JumpHopRunResponse>(
`${JUMP_HOP_RUNTIME_API_BASE}/runs`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
...buildRuntimeGuestHeaders(options),
},
body: JSON.stringify({ profileId }),
},
'启动跳一跳运行态失败',
{
authImpact: options.authImpact,
skipAuth: options.skipAuth,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}
@@ -260,7 +255,9 @@ export async function startJumpHopRuntimeRun(
export async function submitJumpHopJump(
runId: string,
payload: { chargeMs: number },
options: JumpHopRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
const requestPayload = {
chargeMs: payload.chargeMs,
clientEventId: `jump-${runId}-${Date.now()}`,
@@ -272,26 +269,34 @@ export async function submitJumpHopJump(
method: 'POST',
headers: {
'content-type': 'application/json',
...buildRuntimeGuestHeaders(options),
},
body: JSON.stringify(requestPayload),
},
'提交跳一跳起跳失败',
requestOptions,
);
}
export async function restartJumpHopRuntimeRun(runId: string) {
export async function restartJumpHopRuntimeRun(
runId: string,
options: JumpHopRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<JumpHopRunResponse>(
`${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()}`,
}),
},
'重新开始跳一跳失败',
requestOptions,
);
}

View File

@@ -9,10 +9,14 @@ import type {
StopMatch3DRunRequest,
} from '../../../packages/shared/src/contracts/match3dRuntime';
import {
type ApiRequestOptions,
type ApiRetryOptions,
requestJson,
} from '../apiClient';
import {
buildRuntimeGuestAuthOptions,
buildRuntimeGuestHeaders,
type RuntimeGuestRequestOptions,
} from '../runtimeGuestAuth';
const MATCH3D_RUNTIME_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
@@ -25,13 +29,7 @@ const MATCH3D_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
maxDelayMs: 360,
retryUnsafeMethods: true,
};
export type Match3DRuntimeRequestOptions = Pick<
ApiRequestOptions,
| 'authImpact'
| 'skipRefresh'
| 'notifyAuthStateChange'
| 'clearAuthOnUnauthorized'
> & {
export type Match3DRuntimeRequestOptions = RuntimeGuestRequestOptions & {
itemTypeCountOverride?: number | null;
};
@@ -76,6 +74,7 @@ export function startMatch3DRun(
profileId: string,
options: Match3DRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
const payload: StartMatch3DRunRequest = {
profileId,
itemTypeCountOverride: options.itemTypeCountOverride ?? null,
@@ -85,16 +84,15 @@ export function startMatch3DRun(
`/api/runtime/match3d/works/${encodeURIComponent(profileId)}/runs`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: buildRuntimeGuestHeaders(options, {
'Content-Type': 'application/json',
}),
body: JSON.stringify(payload),
},
'启动抓大鹅玩法失败',
{
retry: MATCH3D_RUNTIME_WRITE_RETRY,
authImpact: options.authImpact,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}

View File

@@ -9,10 +9,14 @@ import type {
UsePuzzleRuntimePropRequest,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import {
type ApiRequestOptions,
type ApiRetryOptions,
requestJson,
} from '../apiClient';
import {
buildRuntimeGuestAuthOptions,
buildRuntimeGuestHeaders,
type RuntimeGuestRequestOptions,
} from '../runtimeGuestAuth';
const PUZZLE_RUNTIME_API_BASE = '/api/runtime/puzzle/runs';
const PUZZLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
@@ -26,13 +30,7 @@ const PUZZLE_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
maxDelayMs: 360,
retryUnsafeMethods: true,
};
type PuzzleRuntimeRequestOptions = Pick<
ApiRequestOptions,
| 'authImpact'
| 'skipRefresh'
| 'notifyAuthStateChange'
| 'clearAuthOnUnauthorized'
>;
type PuzzleRuntimeRequestOptions = RuntimeGuestRequestOptions;
/**
* 从某个已发布拼图作品开始一次 run。
@@ -41,20 +39,20 @@ export async function startPuzzleRun(
payload: StartPuzzleRunRequest,
options: PuzzleRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<PuzzleRunResponse>(
PUZZLE_RUNTIME_API_BASE,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: buildRuntimeGuestHeaders(options, {
'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,
...requestOptions,
},
);
}
@@ -125,6 +123,7 @@ export async function advancePuzzleNextLevel(
payload: AdvancePuzzleNextLevelRequest = {},
options: PuzzleRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
const targetProfileId = payload.targetProfileId?.trim() ?? '';
return requestJson<PuzzleRunResponse>(
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/next-level`,
@@ -132,18 +131,19 @@ export async function advancePuzzleNextLevel(
method: 'POST',
...(targetProfileId
? {
headers: { 'Content-Type': 'application/json' },
headers: buildRuntimeGuestHeaders(options, {
'Content-Type': 'application/json',
}),
body: JSON.stringify({ targetProfileId }),
}
: {}),
: {
headers: buildRuntimeGuestHeaders(options),
}),
},
'进入下一关失败',
{
retry: PUZZLE_RUNTIME_WRITE_RETRY,
authImpact: options.authImpact,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}

View File

@@ -0,0 +1,113 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const apiClientMocks = vi.hoisted(() => ({
requestJson: vi.fn(),
}));
vi.mock('./apiClient', async () => {
const actual =
await vi.importActual<typeof import('./apiClient')>('./apiClient');
return {
...actual,
requestJson: apiClientMocks.requestJson,
};
});
import { startBigFishRun } from './big-fish-runtime/bigFishRuntimeClient';
import { startBarkBattleRun } from './bark-battle-runtime/barkBattleRuntimeClient';
import { startJumpHopRuntimeRun } from './jump-hop/jumpHopClient';
import { startMatch3DRun } from './match3d-runtime/match3dRuntimeClient';
import { startPuzzleRun } from './puzzle-runtime/puzzleRuntimeClient';
import { startSquareHoleRun } from './square-hole-runtime/squareHoleRuntimeClient';
import { startVisualNovelRun } from './visual-novel-runtime/visualNovelRuntimeClient';
describe('recommended runtime guest launch clients', () => {
beforeEach(() => {
vi.clearAllMocks();
apiClientMocks.requestJson.mockResolvedValue({ run: {} });
});
it.each([
{
name: 'jump-hop',
start: () =>
startJumpHopRuntimeRun('jump-hop-profile-1', {
runtimeGuestToken: 'runtime-guest-token',
}),
expectedUrl: '/api/runtime/jump-hop/runs',
},
{
name: 'visual-novel',
start: () =>
startVisualNovelRun(
'visual-novel-profile-1',
{ profileId: 'visual-novel-profile-1', mode: 'play' },
{ runtimeGuestToken: 'runtime-guest-token' },
),
expectedUrl: '/api/runtime/visual-novel/works/visual-novel-profile-1/runs',
},
{
name: 'match3d',
start: () =>
startMatch3DRun('match3d-profile-1', {
runtimeGuestToken: 'runtime-guest-token',
}),
expectedUrl: '/api/runtime/match3d/works/match3d-profile-1/runs',
},
{
name: 'square-hole',
start: () =>
startSquareHoleRun('square-hole-profile-1', {
runtimeGuestToken: 'runtime-guest-token',
}),
expectedUrl: '/api/runtime/square-hole/works/square-hole-profile-1/runs',
},
{
name: 'big-fish',
start: () =>
startBigFishRun('big-fish-session-1', {
runtimeGuestToken: 'runtime-guest-token',
}),
expectedUrl: '/api/runtime/big-fish/sessions/big-fish-session-1/runs',
},
{
name: 'bark-battle',
start: () =>
startBarkBattleRun('bark-battle-work-1', {}, {
runtimeGuestToken: 'runtime-guest-token',
}),
expectedUrl: '/api/runtime/bark-battle/works/bark-battle-work-1/runs',
},
{
name: 'puzzle',
start: () =>
startPuzzleRun(
{ profileId: 'puzzle-profile-1', levelId: 'level-1' },
{ runtimeGuestToken: 'runtime-guest-token' },
),
expectedUrl: '/api/runtime/puzzle/runs',
},
])(
'$name start request uses the runtime guest bearer token without touching login auth',
async ({ start, expectedUrl }) => {
await start();
const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0];
expect(url).toBe(expectedUrl);
expect(init).toEqual(
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer runtime-guest-token',
}),
}),
);
expect(options).toEqual(
expect.objectContaining({
skipAuth: true,
skipRefresh: true,
}),
);
},
);
});

View File

@@ -0,0 +1,40 @@
import type { ApiRequestOptions } from './apiClient';
export type RuntimeGuestRequestOptions = Pick<
ApiRequestOptions,
| 'authImpact'
| 'skipAuth'
| 'skipRefresh'
| 'notifyAuthStateChange'
| 'clearAuthOnUnauthorized'
> & {
runtimeGuestToken?: string;
};
export function buildRuntimeGuestHeaders(
options: Pick<RuntimeGuestRequestOptions, 'runtimeGuestToken'>,
headers: Record<string, string> = {},
) {
const runtimeGuestToken = options.runtimeGuestToken?.trim();
if (!runtimeGuestToken) {
return headers;
}
return {
...headers,
Authorization: `Bearer ${runtimeGuestToken}`,
};
}
export function buildRuntimeGuestAuthOptions<
TOptions extends RuntimeGuestRequestOptions,
>(options: TOptions) {
const runtimeGuestToken = options.runtimeGuestToken?.trim();
return {
authImpact: options.authImpact,
skipAuth: runtimeGuestToken ? true : options.skipAuth,
skipRefresh: runtimeGuestToken ? true : options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
} satisfies ApiRequestOptions;
}

View File

@@ -5,10 +5,14 @@ import type {
StopSquareHoleRunRequest,
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
import {
type ApiRequestOptions,
type ApiRetryOptions,
requestJson,
} from '../apiClient';
import {
buildRuntimeGuestAuthOptions,
buildRuntimeGuestHeaders,
type RuntimeGuestRequestOptions,
} from '../runtimeGuestAuth';
const SQUARE_HOLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
@@ -21,13 +25,7 @@ const SQUARE_HOLE_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
maxDelayMs: 360,
retryUnsafeMethods: true,
};
type SquareHoleRuntimeRequestOptions = Pick<
ApiRequestOptions,
| 'authImpact'
| 'skipRefresh'
| 'notifyAuthStateChange'
| 'clearAuthOnUnauthorized'
>;
type SquareHoleRuntimeRequestOptions = RuntimeGuestRequestOptions;
/**
* 基于作品启动一局方洞挑战正式 run。
@@ -36,20 +34,20 @@ export function startSquareHoleRun(
profileId: string,
options: SquareHoleRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<SquareHoleRunResponse>(
`/api/runtime/square-hole/works/${encodeURIComponent(profileId)}/runs`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: buildRuntimeGuestHeaders(options, {
'Content-Type': 'application/json',
}),
body: JSON.stringify({ profileId }),
},
'启动方洞挑战失败',
{
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
authImpact: options.authImpact,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}

View File

@@ -19,12 +19,16 @@ import type {
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes';
import {
type ApiRequestOptions,
type ApiRetryOptions,
fetchWithApiAuth,
requestJson,
} from '../apiClient';
import { readVisualNovelRuntimeRunFromSse } from './visualNovelRuntimeSse';
import {
buildRuntimeGuestAuthOptions,
buildRuntimeGuestHeaders,
type RuntimeGuestRequestOptions,
} from '../runtimeGuestAuth';
const VISUAL_NOVEL_RUNTIME_API_BASE = '/api/runtime/visual-novel';
const VISUAL_NOVEL_RUNTIME_READ_RETRY: ApiRetryOptions = {
@@ -39,16 +43,11 @@ const VISUAL_NOVEL_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
retryUnsafeMethods: true,
};
export type VisualNovelRuntimeStreamOptions = TextStreamOptions & {
onEvent?: (event: VisualNovelRuntimeStreamEvent) => void;
};
type VisualNovelRuntimeRequestOptions = Pick<
ApiRequestOptions,
| 'authImpact'
| 'skipRefresh'
| 'notifyAuthStateChange'
| 'clearAuthOnUnauthorized'
>;
export type VisualNovelRuntimeStreamOptions = TextStreamOptions &
RuntimeGuestRequestOptions & {
onEvent?: (event: VisualNovelRuntimeStreamEvent) => void;
};
type VisualNovelRuntimeRequestOptions = RuntimeGuestRequestOptions;
export type VisualNovelSaveArchiveResumeResponse =
ProfileSaveArchiveResumeResponse<
@@ -84,11 +83,20 @@ async function openVisualNovelRuntimeSsePost(
payload: unknown,
fallbackMessage: string,
signal?: AbortSignal,
options: RuntimeGuestRequestOptions = {},
) {
const response = await fetchWithApiAuth(url, {
...buildJsonInit('POST', payload),
signal,
});
const requestOptions = buildRuntimeGuestAuthOptions(options);
const response = await fetchWithApiAuth(
url,
{
...buildJsonInit('POST', payload),
headers: buildRuntimeGuestHeaders(options, {
'Content-Type': 'application/json',
}),
signal,
},
requestOptions,
);
if (!response.ok) {
const responseText = await response.text();
@@ -107,17 +115,20 @@ export async function startVisualNovelRun(
payload: VisualNovelStartRunRequest,
options: VisualNovelRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<VisualNovelRunResponse>(
`${VISUAL_NOVEL_RUNTIME_API_BASE}/works/${encodeURIComponent(profileId)}/runs`,
buildJsonInit('POST', payload),
{
...buildJsonInit('POST', payload),
headers: buildRuntimeGuestHeaders(options, {
'Content-Type': 'application/json',
}),
},
'启动视觉小说运行失败',
{
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
timeoutMs: 15000,
authImpact: options.authImpact,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
...requestOptions,
},
);
}
@@ -154,6 +165,7 @@ export async function streamVisualNovelRuntimeAction(
payload,
'推进视觉小说失败',
options.signal,
options,
);
return readVisualNovelRuntimeRunFromSse(response, {

View File

@@ -18,6 +18,11 @@ import type {
} from '../../../packages/shared/src/contracts/woodenFish';
import { type ApiRetryOptions, requestJson } from '../apiClient';
import { createCreationAgentClient } from '../creation-agent';
import {
buildRuntimeGuestAuthOptions,
buildRuntimeGuestHeaders,
type RuntimeGuestRequestOptions,
} from '../runtimeGuestAuth';
const WOODEN_FISH_API_BASE = '/api/creation/wooden-fish/sessions';
const WOODEN_FISH_WORKS_API_BASE = '/api/creation/wooden-fish/works';
@@ -27,6 +32,13 @@ const WOODEN_FISH_RUNTIME_READ_RETRY: ApiRetryOptions = {
baseDelayMs: 120,
maxDelayMs: 360,
};
const WOODEN_FISH_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 120,
maxDelayMs: 360,
retryUnsafeMethods: true,
};
type WoodenFishRuntimeRequestOptions = RuntimeGuestRequestOptions;
export type {
WoodenFishActionRequest,
@@ -204,24 +216,35 @@ export async function publishWoodenFishWork(profileId: string) {
return normalizeWoodenFishWorkMutationResponse(response);
}
export async function startWoodenFishRuntimeRun(profileId: string) {
export async function startWoodenFishRuntimeRun(
profileId: string,
options: WoodenFishRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
return requestJson<WoodenFishRunResponse>(
`${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,
},
);
}
export async function checkpointWoodenFishRun(
runId: string,
payload: Omit<WoodenFishCheckpointRunRequest, 'clientEventId'>,
options: WoodenFishRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
const requestPayload: WoodenFishCheckpointRunRequest = {
...payload,
clientEventId: `checkpoint-${runId}-${Date.now()}`,
@@ -233,17 +256,24 @@ export async function checkpointWoodenFishRun(
method: 'POST',
headers: {
'content-type': 'application/json',
...buildRuntimeGuestHeaders(options),
},
body: JSON.stringify(requestPayload),
},
'保存敲木鱼进度失败',
{
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
...requestOptions,
},
);
}
export async function finishWoodenFishRun(
runId: string,
payload: Omit<WoodenFishFinishRunRequest, 'clientEventId'>,
options: WoodenFishRuntimeRequestOptions = {},
) {
const requestOptions = buildRuntimeGuestAuthOptions(options);
const requestPayload: WoodenFishFinishRunRequest = {
...payload,
clientEventId: `finish-${runId}-${Date.now()}`,
@@ -255,10 +285,15 @@ export async function finishWoodenFishRun(
method: 'POST',
headers: {
'content-type': 'application/json',
...buildRuntimeGuestHeaders(options),
},
body: JSON.stringify(requestPayload),
},
'结束敲木鱼运行失败',
{
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
...requestOptions,
},
);
}