This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions

View File

@@ -0,0 +1,264 @@
import type {
BasicOkResult,
ProfileSaveArchiveListResponse,
ProfileSaveArchiveResumeResponse,
ProfileSaveArchiveSummary,
RuntimeSaveCheckpointInput,
} from '../../../packages/shared/src/contracts/runtime';
import type {
VisualNovelHistoryResponse,
VisualNovelRegenerateRequest,
VisualNovelRunResponse,
VisualNovelRunSnapshot,
VisualNovelRuntimeActionRequest,
VisualNovelRuntimeStreamEvent,
VisualNovelSaveArchiveState,
VisualNovelStartRunRequest,
VisualNovelWorksResponse,
} from '../../../packages/shared/src/contracts/visualNovel';
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes';
import {
type ApiRetryOptions,
fetchWithApiAuth,
requestJson,
} from '../apiClient';
import { readVisualNovelRuntimeRunFromSse } from './visualNovelRuntimeSse';
const VISUAL_NOVEL_RUNTIME_API_BASE = '/api/runtime/visual-novel';
const VISUAL_NOVEL_RUNTIME_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 180,
maxDelayMs: 480,
};
const VISUAL_NOVEL_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 240,
maxDelayMs: 640,
retryUnsafeMethods: true,
};
export type VisualNovelRuntimeStreamOptions = TextStreamOptions & {
onEvent?: (event: VisualNovelRuntimeStreamEvent) => void;
};
export type VisualNovelSaveArchiveResumeResponse =
ProfileSaveArchiveResumeResponse<
VisualNovelSaveArchiveState,
string,
{ run?: VisualNovelRunSnapshot; archiveState?: VisualNovelSaveArchiveState } | null
>;
export async function listVisualNovelGallery() {
return requestJson<VisualNovelWorksResponse>(
`${VISUAL_NOVEL_RUNTIME_API_BASE}/gallery`,
{ method: 'GET' },
'读取视觉小说公开作品列表失败',
{
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
// 中文注释:公开广场是游客可读入口,避免未登录态先触发 refresh 再读取公开列表。
skipAuth: true,
skipRefresh: true,
},
);
}
function buildJsonInit(method: 'POST' | 'PUT', payload: unknown): RequestInit {
return {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
};
}
async function openVisualNovelRuntimeSsePost(
url: string,
payload: unknown,
fallbackMessage: string,
signal?: AbortSignal,
) {
const response = await fetchWithApiAuth(url, {
...buildJsonInit('POST', payload),
signal,
});
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
}
if (!response.body) {
throw new Error('streaming response body is unavailable');
}
return response;
}
export async function startVisualNovelRun(
profileId: string,
payload: VisualNovelStartRunRequest,
) {
return requestJson<VisualNovelRunResponse>(
`${VISUAL_NOVEL_RUNTIME_API_BASE}/works/${encodeURIComponent(profileId)}/runs`,
buildJsonInit('POST', payload),
'启动视觉小说运行失败',
{
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
timeoutMs: 15000,
},
);
}
export async function getVisualNovelRun(runId: string) {
return requestJson<VisualNovelRunResponse>(
`${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}`,
{ method: 'GET' },
'读取视觉小说运行快照失败',
{
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
},
);
}
export async function getVisualNovelHistory(runId: string) {
return requestJson<VisualNovelHistoryResponse>(
`${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/history`,
{ method: 'GET' },
'读取视觉小说历史失败',
{
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
},
);
}
export async function streamVisualNovelRuntimeAction(
runId: string,
payload: VisualNovelRuntimeActionRequest,
options: VisualNovelRuntimeStreamOptions = {},
) {
const response = await openVisualNovelRuntimeSsePost(
`${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/actions/stream`,
payload,
'推进视觉小说失败',
options.signal,
);
return readVisualNovelRuntimeRunFromSse(response, {
...options,
fallbackMessage: '推进视觉小说失败',
incompleteMessage: '视觉小说流式推进结果不完整',
});
}
export async function regenerateVisualNovelRun(
runId: string,
payload: VisualNovelRegenerateRequest,
) {
return requestJson<VisualNovelRunResponse>(
`${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/regenerate`,
buildJsonInit('POST', payload),
'重生成视觉小说历史失败',
{
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
},
);
}
export async function listVisualNovelSaveArchives(profileId?: string | null) {
const response = await requestJson<ProfileSaveArchiveListResponse>(
'/api/profile/save-archives',
{ method: 'GET' },
'读取视觉小说存档失败',
{
retry: VISUAL_NOVEL_RUNTIME_READ_RETRY,
},
);
const entries = Array.isArray(response?.entries) ? response.entries : [];
const targetProfileId = profileId?.trim();
return entries.filter((entry) => {
if (entry.worldType !== 'visual-novel') {
return false;
}
return !targetProfileId || entry.profileId === targetProfileId;
});
}
export function resumeVisualNovelSaveArchive(worldKey: string) {
return requestJson<VisualNovelSaveArchiveResumeResponse>(
`/api/profile/save-archives/${encodeURIComponent(worldKey)}`,
{ method: 'POST' },
'恢复视觉小说存档失败',
{
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
},
);
}
export function putVisualNovelRuntimeSnapshot(
checkpoint: RuntimeSaveCheckpointInput,
) {
// 中文注释:这里仍然只提交平台 checkpoint真实 run 状态由后端运行时维护,前端不上传整份业务快照。
return requestJson<unknown>(
'/api/runtime/save/snapshot',
buildJsonInit('PUT', checkpoint),
'保存视觉小说存档失败',
{
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
},
);
}
export function deleteVisualNovelRuntimeSnapshot() {
return requestJson<BasicOkResult>(
'/api/runtime/save/snapshot',
{ method: 'DELETE' },
'删除视觉小说存档失败',
{
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
},
);
}
export function buildVisualNovelRuntimeCheckpoint(params: {
run: VisualNovelRunSnapshot;
savedAt?: string;
}): RuntimeSaveCheckpointInput {
return {
sessionId: params.run.runId,
bottomTab: 'adventure',
savedAt: params.savedAt ?? new Date().toISOString(),
};
}
export function buildVisualNovelSaveArchiveState(
run: VisualNovelRunSnapshot,
): VisualNovelSaveArchiveState {
const latestEntry = run.history[run.history.length - 1] ?? null;
return {
runtimeKind: 'visual-novel',
profileId: run.profileId,
runId: run.runId,
currentSceneId: run.currentSceneId,
currentPhaseId: run.currentPhaseId,
historyCursor: latestEntry?.turnIndex ?? 0,
snapshotHash: latestEntry?.snapshotAfterHash ?? null,
};
}
export const visualNovelRuntimeClient = {
listGallery: listVisualNovelGallery,
startRun: startVisualNovelRun,
getRun: getVisualNovelRun,
getHistory: getVisualNovelHistory,
streamAction: streamVisualNovelRuntimeAction,
regenerateRun: regenerateVisualNovelRun,
listSaveArchives: listVisualNovelSaveArchives,
resumeSaveArchive: resumeVisualNovelSaveArchive,
putSnapshot: putVisualNovelRuntimeSnapshot,
deleteSnapshot: deleteVisualNovelRuntimeSnapshot,
};
export type { ProfileSaveArchiveSummary };