This commit is contained in:
2026-04-21 18:27:46 +08:00
parent 04bff9617d
commit 4372ab5be1
358 changed files with 30788 additions and 14737 deletions

View File

@@ -1,14 +1,3 @@
import type {
CreateCustomWorldAgentSessionRequest,
CreateCustomWorldAgentSessionResponse,
CustomWorldAgentActionRequest,
CustomWorldAgentOperationRecord,
CustomWorldAgentSessionSnapshot,
CustomWorldDraftCardDetail,
GetCustomWorldAgentCardDetailResponse,
ListCustomWorldWorksResponse,
SendCustomWorldAgentMessageRequest,
} from '../../packages/shared/src/contracts/customWorldAgent';
import type {
CustomWorldGenerationProgress,
GenerateCustomWorldProfileInput,
@@ -18,22 +7,18 @@ import type {
CharacterChatReplyRequest,
CharacterChatSuggestionsRequest,
CharacterChatSummaryRequest,
NpcChatDialogueRequest,
NpcChatTurnDirective,
NpcChatDialogueRequest,
NpcChatTurnRequest,
NpcChatTurnResult,
NpcRecruitDialogueRequest,
PlainTextResponse,
} from '../../packages/shared/src/contracts/story';
} from '../../packages/shared/src/contracts/rpgRuntimeChat';
import { parseApiErrorMessage } from '../../packages/shared/src/http';
import type {
AIResponse,
Character,
CharacterChatTurn,
CustomWorldLandmark,
CustomWorldNpc,
CustomWorldPlayableNpc,
CustomWorldProfile,
Encounter,
GameState,
SceneHostileNpc,
@@ -41,18 +26,16 @@ import type {
WorldType,
} from '../types';
import type {
CustomWorldSceneImageRequest,
CustomWorldSceneImageResult,
StoryGenerationContext,
StoryRequestOptions,
TextStreamOptions,
CustomWorldSceneImageResult,
} from './aiTypes';
import { fetchWithApiAuth, requestJson } from './apiClient';
import { type CharacterChatTargetStatus } from './characterChatPrompt';
import { parseLineListContent } from './llmParsers';
const RUNTIME_API_BASE = '/api/runtime';
const CUSTOM_WORLD_API_BASE = '/api';
type LegacyAiModule = typeof import('./ai');
@@ -66,12 +49,12 @@ async function loadLegacyAiModule() {
return legacyAiModulePromise;
}
async function requestPostJson<T>(
async function requestPlainText(
url: string,
payload: unknown,
fallbackMessage: string,
) {
return requestJson<T>(
return requestJson<PlainTextResponse>(
url,
{
method: 'POST',
@@ -82,14 +65,6 @@ async function requestPostJson<T>(
);
}
async function requestPlainText(
url: string,
payload: unknown,
fallbackMessage: string,
) {
return requestPostJson<PlainTextResponse>(url, payload, fallbackMessage);
}
async function requestPlainTextStream(
url: string,
payload: unknown,
@@ -349,326 +324,6 @@ export async function generateCharacterPanelChatSummary(
return text.trim();
}
export async function generateCustomWorldProfile(
input: GenerateCustomWorldProfileInput | string,
options: GenerateCustomWorldProfileOptions = {},
): Promise<CustomWorldProfile> {
const normalizedInput =
typeof input === 'string'
? {
settingText: input,
}
: 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
: new Error('世界生成已中断。');
}
const profile = await requestPostJson<CustomWorldProfile>(
`${RUNTIME_API_BASE}/custom-world/profile`,
normalizedInput,
'生成自定义世界失败',
);
if (options.signal?.aborted) {
throw options.signal.reason instanceof Error
? options.signal.reason
: new Error('世界生成已中断。');
}
return profile;
}
export async function generateCustomWorldSceneImage(
payload: CustomWorldSceneImageRequest,
) {
return requestJson<CustomWorldSceneImageResult>(
`${CUSTOM_WORLD_API_BASE}/custom-world/scene-image`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'生成自定义世界场景图失败',
);
}
export async function generateCustomWorldSceneNpc(payload: {
profile: CustomWorldProfile;
landmarkId: string;
}) {
const response = await requestPostJson<{ npc: CustomWorldNpc }>(
`${CUSTOM_WORLD_API_BASE}/custom-world/scene-npc`,
payload,
'生成场景 NPC 失败',
);
return response.npc;
}
async function requestCustomWorldEntity<T>(
payload: {
profile: CustomWorldProfile;
kind: 'playable' | 'story' | 'landmark';
},
fallbackMessage: string,
) {
return requestPostJson<{
kind: 'playable' | 'story' | 'landmark';
entity: T;
}>(`${CUSTOM_WORLD_API_BASE}/custom-world/entity`, payload, fallbackMessage);
}
export async function generateCustomWorldPlayableNpc(payload: {
profile: CustomWorldProfile;
}) {
const response = await requestCustomWorldEntity<CustomWorldPlayableNpc>(
{
...payload,
kind: 'playable',
},
'生成可扮演角色失败',
);
return response.entity;
}
export async function generateCustomWorldStoryNpc(payload: {
profile: CustomWorldProfile;
}) {
const response = await requestCustomWorldEntity<CustomWorldNpc>(
{
...payload,
kind: 'story',
},
'生成场景角色失败',
);
return response.entity;
}
export async function generateCustomWorldLandmark(payload: {
profile: CustomWorldProfile;
}) {
const response = await requestCustomWorldEntity<CustomWorldLandmark>(
{
...payload,
kind: 'landmark',
},
'生成场景失败',
);
return response.entity;
}
export async function listCustomWorldWorks() {
const response = await requestJson<ListCustomWorldWorksResponse>(
`${RUNTIME_API_BASE}/custom-world/works`,
{
method: 'GET',
},
'读取创作作品列表失败',
);
return Array.isArray(response?.items) ? response.items : [];
}
export async function createCustomWorldAgentSession(
payload: CreateCustomWorldAgentSessionRequest,
) {
return requestJson<CreateCustomWorldAgentSessionResponse>(
`${RUNTIME_API_BASE}/custom-world/agent/sessions`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'创建世界共创会话失败',
);
}
export async function getCustomWorldAgentSession(sessionId: string) {
return requestJson<CustomWorldAgentSessionSnapshot>(
`${RUNTIME_API_BASE}/custom-world/agent/sessions/${encodeURIComponent(sessionId)}`,
{
method: 'GET',
},
'读取世界共创会话失败',
);
}
export async function sendCustomWorldAgentMessage(
sessionId: string,
payload: SendCustomWorldAgentMessageRequest,
) {
return requestJson<{ operation: CustomWorldAgentOperationRecord }>(
`${RUNTIME_API_BASE}/custom-world/agent/sessions/${encodeURIComponent(sessionId)}/messages`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'发送共创消息失败',
);
}
export async function streamCustomWorldAgentMessage(
sessionId: string,
payload: SendCustomWorldAgentMessageRequest,
options: TextStreamOptions = {},
) {
const response = await fetchWithApiAuth(
`${RUNTIME_API_BASE}/custom-world/agent/sessions/${encodeURIComponent(sessionId)}/messages/stream`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
);
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, '发送共创消息失败'));
}
if (!response.body) {
throw new Error('streaming response body is unavailable');
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let finalSession: CustomWorldAgentSessionSnapshot | null = null;
for (;;) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
while (buffer.includes('\n\n')) {
const boundary = buffer.indexOf('\n\n');
const eventBlock = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
let eventName = 'message';
const dataLines: string[] = [];
for (const rawLine of eventBlock.split(/\r?\n/u)) {
const line = rawLine.trim();
if (line.startsWith('event:')) {
eventName = line.slice(6).trim() || 'message';
continue;
}
if (line.startsWith('data:')) {
dataLines.push(line.slice(5).trim());
}
}
if (dataLines.length === 0) {
continue;
}
const data = dataLines.join('\n');
let parsed: Record<string, unknown> | null = null;
try {
parsed = JSON.parse(data) as Record<string, unknown>;
} catch {
parsed = null;
}
if (eventName === 'reply_delta' && parsed) {
const text = parsed.text;
if (typeof text === 'string') {
options.onUpdate?.(text);
}
continue;
}
if (eventName === 'session' && parsed?.session) {
finalSession = parsed.session as CustomWorldAgentSessionSnapshot;
continue;
}
if (eventName === 'error' && parsed) {
const message =
typeof parsed.message === 'string' && parsed.message.trim()
? parsed.message.trim()
: '发送共创消息失败';
throw new Error(message);
}
}
}
if (!finalSession) {
throw new Error('共创消息流式结果不完整');
}
return finalSession;
}
export async function executeCustomWorldAgentAction(
sessionId: string,
payload: CustomWorldAgentActionRequest,
) {
return requestJson<{ operation: CustomWorldAgentOperationRecord }>(
`${RUNTIME_API_BASE}/custom-world/agent/sessions/${encodeURIComponent(sessionId)}/actions`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'执行共创操作失败',
);
}
export async function getCustomWorldAgentOperation(
sessionId: string,
operationId: string,
): Promise<CustomWorldAgentOperationRecord> {
const response = await requestJson<
{
operation?: CustomWorldAgentOperationRecord;
data?: CustomWorldAgentOperationRecord;
} & Partial<CustomWorldAgentOperationRecord>
>(
`${RUNTIME_API_BASE}/custom-world/agent/sessions/${encodeURIComponent(sessionId)}/operations/${encodeURIComponent(operationId)}`,
{
method: 'GET',
},
'读取共创操作状态失败',
);
return (response.operation ??
response.data ??
response) as CustomWorldAgentOperationRecord;
}
export async function getCustomWorldAgentCardDetail(
sessionId: string,
cardId: string,
) {
const response = await requestJson<GetCustomWorldAgentCardDetailResponse>(
`${RUNTIME_API_BASE}/custom-world/agent/sessions/${encodeURIComponent(sessionId)}/cards/${encodeURIComponent(cardId)}`,
{
method: 'GET',
},
'读取草稿卡详情失败',
);
return response.card as CustomWorldDraftCardDetail;
}
export async function streamCharacterPanelChatReply(
world: WorldType,
playerCharacter: Character,