This commit is contained in:
2026-04-28 19:36:39 +08:00
parent a9febe7678
commit f0471a4f8d
206 changed files with 18456 additions and 10133 deletions

View File

@@ -3,6 +3,7 @@ export {
executeRpgCreationAction,
getRpgCreationCardDetail,
getRpgCreationOperation,
getRpgCreationResultView,
getRpgCreationSession,
rpgCreationAgentClient,
sendRpgCreationMessage,
@@ -23,10 +24,7 @@ export type {
GenerateCustomWorldProfileInput,
GenerateCustomWorldProfileOptions,
} from './rpgCreationGenerationClient';
export {
generateCustomWorldProfile as generateLegacyCustomWorldProfile,
generateRpgWorldProfile,
} from './rpgCreationGenerationClient';
export { generateRpgWorldProfile } from './rpgCreationGenerationClient';
export {
deleteRpgWorldProfile,
getRpgWorldGalleryDetail,
@@ -39,6 +37,7 @@ export {
} from './rpgCreationLibraryClient';
export {
buildRpgCreationPreviewFromResultPreview,
buildRpgCreationPreviewFromResultView,
buildRpgCreationPreviewFromSession,
rpgCreationPreviewAdapter,
} from './rpgCreationPreviewAdapter';

View File

@@ -2,6 +2,7 @@ import type {
CreateRpgAgentSessionRequest,
CreateRpgAgentSessionResponse,
GetRpgAgentCardDetailResponse,
RpgCreationResultView,
RpgAgentDraftCardDetail,
RpgAgentOperationRecord,
RpgAgentSessionSnapshot,
@@ -46,6 +47,16 @@ export async function getRpgCreationSession(sessionId: string) {
);
}
export async function getRpgCreationResultView(sessionId: string) {
return requestRpgCreationRuntimeJson<RpgCreationResultView>(
`${RPG_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/result-view`,
{
method: 'GET',
},
'读取世界结果页视图失败',
);
}
export async function sendRpgCreationMessage(
sessionId: string,
payload: SendRpgAgentMessageRequest,
@@ -133,6 +144,7 @@ export async function getRpgCreationCardDetail(
export const rpgCreationAgentClient = {
createSession: createRpgCreationSession,
getSession: getRpgCreationSession,
getResultView: getRpgCreationResultView,
sendMessage: sendRpgCreationMessage,
streamMessage: streamRpgCreationMessage,
executeAction: executeRpgCreationAction,

View File

@@ -0,0 +1,50 @@
/* @vitest-environment node */
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { requestJsonMock } = vi.hoisted(() => ({
requestJsonMock: vi.fn(),
}));
import { generateRpgWorldProfile } from './rpgCreationGenerationClient';
vi.mock('../apiClient', () => ({
requestJson: requestJsonMock,
}));
vi.mock('../ai', () => ({
generateCustomWorldProfile: vi.fn(() => {
throw new Error('不应再调用前端 legacy AI 生成链');
}),
}));
describe('rpgCreationGenerationClient node runtime', () => {
beforeEach(() => {
requestJsonMock.mockReset();
requestJsonMock.mockResolvedValue({
id: 'server-rs-profile-1',
name: '服务端世界',
subtitle: '副标题',
summary: '概述',
tone: '基调',
playerGoal: '目标',
settingText: '设定',
});
});
it('uses server-rs profile generation instead of importing legacy ai', async () => {
const profile = await generateRpgWorldProfile('一个在 Node 测试中生成的世界');
expect(profile.id).toBe('server-rs-profile-1');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world/profile',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
settingText: '一个在 Node 测试中生成的世界',
}),
}),
'生成自定义世界失败',
);
});
});

View File

@@ -34,6 +34,9 @@ describe('rpgCreationGenerationClient', () => {
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
settingText: '一个被灵潮反复改写地形的边境世界',
}),
}),
'生成自定义世界失败',
);
@@ -51,4 +54,26 @@ describe('rpgCreationGenerationClient', () => {
expect(requestJsonMock).not.toHaveBeenCalled();
});
it('passes abort signal to the backend request contract', async () => {
const controller = new AbortController();
await generateRpgWorldProfile(
{
settingText: '一个由服务端生成的世界',
generationMode: 'fast',
},
{
signal: controller.signal,
},
);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/runtime/custom-world/profile',
expect.objectContaining({
signal: controller.signal,
}),
'生成自定义世界失败',
);
});
});

View File

@@ -6,18 +6,6 @@ import type {
import type { CustomWorldProfile } from '../../types';
import { requestJson } from '../apiClient';
type LegacyAiModule = typeof import('../ai');
let legacyAiModulePromise: Promise<LegacyAiModule> | null = null;
async function loadLegacyAiModule() {
if (!legacyAiModulePromise) {
legacyAiModulePromise = import('../ai');
}
return legacyAiModulePromise;
}
export async function generateRpgWorldProfile(
input: GenerateCustomWorldProfileInput | string,
options: GenerateCustomWorldProfileOptions = {},
@@ -29,11 +17,6 @@ export async function generateRpgWorldProfile(
}
: input;
if (typeof window === 'undefined') {
const aiClient = await loadLegacyAiModule();
return aiClient.generateCustomWorldProfile(normalizedInput, options);
}
if (options.signal?.aborted) {
throw options.signal.reason instanceof Error
? options.signal.reason
@@ -45,6 +28,7 @@ export async function generateRpgWorldProfile(
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: options.signal,
body: JSON.stringify(normalizedInput),
},
'生成自定义世界失败',

View File

@@ -3,6 +3,7 @@ import { expect, test } from 'vitest';
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import {
buildRpgCreationPreviewFromResultPreview,
buildRpgCreationPreviewFromResultView,
buildRpgCreationPreviewFromSession,
} from './rpgCreationPreviewAdapter';
@@ -211,7 +212,7 @@ test('buildRpgCreationPreviewFromSession prefers server result preview', () => {
expect(profile?.playableNpcs).toEqual([]);
});
test('buildRpgCreationPreviewFromSession falls back to draft legacy result profile', () => {
test('buildRpgCreationPreviewFromSession no longer reads draft legacy result profile', () => {
const profile = buildRpgCreationPreviewFromSession({
...sessionWithPreview,
resultPreview: null,
@@ -226,11 +227,36 @@ test('buildRpgCreationPreviewFromSession falls back to draft legacy result profi
},
});
expect(profile?.name).toBe('草稿内嵌结果页');
expect(profile?.summary).toBe(
'resultPreview 缺失时继续使用 draft 内嵌的结果页快照。',
);
expect(profile?.id).toBe('legacy-result-profile-1');
expect(profile).toBeNull();
});
test('buildRpgCreationPreviewFromResultView consumes backend-selected profile', () => {
const profile = buildRpgCreationPreviewFromResultView({
session: {
...sessionWithPreview,
resultPreview: null,
},
profile: {
...sessionWithPreview.resultPreview!.preview,
id: 'backend-selected-profile-1',
name: '后端结果页真相',
summary: 'legacy 兼容只允许在后端 result-view 内完成。',
},
profileSource: 'draft_profile',
targetStage: 'custom-world-result',
generationViewSource: null,
resultViewSource: 'agent-draft',
canAutosaveLibrary: true,
canSyncResultProfile: true,
publishReady: false,
canEnterWorld: false,
blockerCount: 0,
recoveryAction: 'open_result',
});
expect(profile?.name).toBe('后端结果页真相');
expect(profile?.summary).toBe('legacy 兼容只允许在后端 result-view 内完成。');
expect(profile?.id).toBe('backend-selected-profile-1');
});
test('buildRpgCreationPreviewFromSession does not treat draftProfile as runtime profile', () => {

View File

@@ -1,20 +1,8 @@
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary';
import type { CustomWorldProfile } from '../../types';
function buildCustomWorldProfileFromDraftLegacyResult(
draftProfile: CustomWorldAgentSessionSnapshot['draftProfile'],
): CustomWorldProfile | null {
if (!draftProfile || typeof draftProfile !== 'object') {
return null;
}
return normalizeCustomWorldProfileRecord(
(draftProfile as { legacyResultProfile?: unknown }).legacyResultProfile ??
null,
);
}
export function buildCustomWorldProfileFromResultPreview(
resultPreview:
| CustomWorldAgentSessionSnapshot['resultPreview']
@@ -27,10 +15,13 @@ export function buildCustomWorldProfileFromResultPreview(
export function buildCustomWorldProfileFromAgentSession(
session: CustomWorldAgentSessionSnapshot | null | undefined,
): CustomWorldProfile | null {
return (
buildCustomWorldProfileFromResultPreview(session?.resultPreview) ??
buildCustomWorldProfileFromDraftLegacyResult(session?.draftProfile ?? null)
);
return buildCustomWorldProfileFromResultPreview(session?.resultPreview);
}
export function buildCustomWorldProfileFromResultView(
view: RpgCreationResultView | null | undefined,
): CustomWorldProfile | null {
return normalizeCustomWorldProfileRecord(view?.profile ?? null);
}
/**
@@ -40,9 +31,11 @@ export function buildCustomWorldProfileFromAgentSession(
export const rpgCreationPreviewAdapter = {
buildPreviewFromSession: buildCustomWorldProfileFromAgentSession,
buildPreviewFromResultPreview: buildCustomWorldProfileFromResultPreview,
buildPreviewFromResultView: buildCustomWorldProfileFromResultView,
};
export {
buildCustomWorldProfileFromResultPreview as buildRpgCreationPreviewFromResultPreview,
buildCustomWorldProfileFromAgentSession as buildRpgCreationPreviewFromSession,
buildCustomWorldProfileFromResultView as buildRpgCreationPreviewFromResultView,
};