780 lines
21 KiB
TypeScript
780 lines
21 KiB
TypeScript
import type {
|
||
CreateCustomWorldAgentSessionRequest,
|
||
CreateCustomWorldAgentSessionResponse,
|
||
CustomWorldAgentActionRequest,
|
||
CustomWorldAgentOperationRecord,
|
||
CustomWorldAgentSessionSnapshot,
|
||
CustomWorldDraftCardDetail,
|
||
GetCustomWorldAgentCardDetailResponse,
|
||
ListCustomWorldWorksResponse,
|
||
SendCustomWorldAgentMessageRequest,
|
||
} from '../../packages/shared/src/contracts/customWorldAgent';
|
||
import type {
|
||
AnswerCustomWorldSessionQuestionRequest,
|
||
CreateCustomWorldSessionRequest,
|
||
CustomWorldGenerationProgress,
|
||
CustomWorldSessionRecord,
|
||
CustomWorldSessionSummary,
|
||
GenerateCustomWorldProfileInput,
|
||
GenerateCustomWorldProfileOptions,
|
||
} from '../../packages/shared/src/contracts/runtime';
|
||
import type {
|
||
CharacterChatReplyRequest,
|
||
CharacterChatSuggestionsRequest,
|
||
CharacterChatSummaryRequest,
|
||
NpcChatDialogueRequest,
|
||
NpcRecruitDialogueRequest,
|
||
PlainTextResponse,
|
||
} from '../../packages/shared/src/contracts/story';
|
||
import { parseApiErrorMessage } from '../../packages/shared/src/http';
|
||
import type {
|
||
AIResponse,
|
||
Character,
|
||
CharacterChatTurn,
|
||
CustomWorldProfile,
|
||
Encounter,
|
||
SceneHostileNpc,
|
||
StoryMoment,
|
||
WorldType,
|
||
} from '../types';
|
||
import type {
|
||
CustomWorldSceneImageRequest,
|
||
CustomWorldSceneImageResult,
|
||
StoryGenerationContext,
|
||
StoryRequestOptions,
|
||
TextStreamOptions,
|
||
} from './ai';
|
||
import { fetchWithApiAuth, requestJson } from './apiClient';
|
||
import { type CharacterChatTargetStatus } from './characterChatPrompt';
|
||
import { parseLineListContent } from './llmParsers';
|
||
|
||
const RUNTIME_API_BASE = '/api/runtime';
|
||
|
||
type LegacyAiModule = typeof import('./ai');
|
||
|
||
let legacyAiModulePromise: Promise<LegacyAiModule> | null = null;
|
||
|
||
async function loadLegacyAiModule() {
|
||
if (!legacyAiModulePromise) {
|
||
legacyAiModulePromise = import('./ai');
|
||
}
|
||
|
||
return legacyAiModulePromise;
|
||
}
|
||
|
||
async function requestPostJson<T>(
|
||
url: string,
|
||
payload: unknown,
|
||
fallbackMessage: string,
|
||
) {
|
||
return requestJson<T>(
|
||
url,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
},
|
||
fallbackMessage,
|
||
);
|
||
}
|
||
|
||
async function requestPlainText(
|
||
url: string,
|
||
payload: unknown,
|
||
fallbackMessage: string,
|
||
) {
|
||
return requestPostJson<PlainTextResponse>(url, payload, fallbackMessage);
|
||
}
|
||
|
||
async function requestPlainTextStream(
|
||
url: string,
|
||
payload: unknown,
|
||
options: TextStreamOptions = {},
|
||
) {
|
||
const response = await fetchWithApiAuth(url, {
|
||
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 accumulatedText = '';
|
||
|
||
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);
|
||
|
||
for (const rawLine of eventBlock.split(/\r?\n/u)) {
|
||
const line = rawLine.trim();
|
||
if (!line.startsWith('data:')) {
|
||
continue;
|
||
}
|
||
|
||
const data = line.slice(5).trim();
|
||
if (!data || data === '[DONE]') {
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
const parsed = JSON.parse(data);
|
||
const delta = parsed?.choices?.[0]?.delta?.content;
|
||
if (typeof delta === 'string' && delta.length > 0) {
|
||
accumulatedText += delta;
|
||
options.onUpdate?.(accumulatedText);
|
||
}
|
||
} catch {
|
||
// Ignore malformed SSE frames.
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return accumulatedText.trim();
|
||
}
|
||
|
||
export async function generateInitialStory(
|
||
world: WorldType,
|
||
character: Character,
|
||
monsters: SceneHostileNpc[],
|
||
context: StoryGenerationContext,
|
||
requestOptions: StoryRequestOptions = {},
|
||
): Promise<AIResponse> {
|
||
if (typeof window === 'undefined') {
|
||
const aiClient = await loadLegacyAiModule();
|
||
return aiClient.generateInitialStory(
|
||
world,
|
||
character,
|
||
monsters,
|
||
context,
|
||
requestOptions,
|
||
);
|
||
}
|
||
|
||
return requestJson<AIResponse>(
|
||
`${RUNTIME_API_BASE}/story/initial`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
worldType: world,
|
||
character,
|
||
monsters,
|
||
context,
|
||
requestOptions,
|
||
}),
|
||
},
|
||
'剧情开局生成失败',
|
||
);
|
||
}
|
||
|
||
export async function generateNextStep(
|
||
world: WorldType,
|
||
character: Character,
|
||
monsters: SceneHostileNpc[],
|
||
history: StoryMoment[],
|
||
choice: string,
|
||
context: StoryGenerationContext,
|
||
requestOptions: StoryRequestOptions = {},
|
||
): Promise<AIResponse> {
|
||
if (typeof window === 'undefined') {
|
||
const aiClient = await loadLegacyAiModule();
|
||
return aiClient.generateNextStep(
|
||
world,
|
||
character,
|
||
monsters,
|
||
history,
|
||
choice,
|
||
context,
|
||
requestOptions,
|
||
);
|
||
}
|
||
|
||
return requestJson<AIResponse>(
|
||
`${RUNTIME_API_BASE}/story/continue`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
worldType: world,
|
||
character,
|
||
monsters,
|
||
history,
|
||
choice,
|
||
context,
|
||
requestOptions,
|
||
}),
|
||
},
|
||
'剧情续写失败',
|
||
);
|
||
}
|
||
|
||
export async function generateCharacterPanelChatSuggestions(
|
||
world: WorldType,
|
||
playerCharacter: Character,
|
||
targetCharacter: Character,
|
||
storyHistory: StoryMoment[],
|
||
context: StoryGenerationContext,
|
||
conversationHistory: CharacterChatTurn[],
|
||
conversationSummary: string,
|
||
targetStatus: CharacterChatTargetStatus,
|
||
) {
|
||
if (typeof window === 'undefined') {
|
||
const aiClient = await loadLegacyAiModule();
|
||
return aiClient.generateCharacterPanelChatSuggestions(
|
||
world,
|
||
playerCharacter,
|
||
targetCharacter,
|
||
storyHistory,
|
||
context,
|
||
conversationHistory,
|
||
conversationSummary,
|
||
targetStatus,
|
||
);
|
||
}
|
||
|
||
const payload = {
|
||
worldType: world,
|
||
playerCharacter,
|
||
targetCharacter,
|
||
storyHistory,
|
||
context,
|
||
conversationHistory,
|
||
conversationSummary,
|
||
targetStatus,
|
||
} satisfies CharacterChatSuggestionsRequest;
|
||
|
||
const { text } = await requestPlainText(
|
||
`${RUNTIME_API_BASE}/chat/character/suggestions`,
|
||
payload,
|
||
'角色聊天建议生成失败',
|
||
);
|
||
return parseLineListContent(text, 3);
|
||
}
|
||
|
||
export async function generateCharacterPanelChatSummary(
|
||
world: WorldType,
|
||
playerCharacter: Character,
|
||
targetCharacter: Character,
|
||
storyHistory: StoryMoment[],
|
||
context: StoryGenerationContext,
|
||
conversationHistory: CharacterChatTurn[],
|
||
previousSummary: string,
|
||
targetStatus: CharacterChatTargetStatus,
|
||
) {
|
||
if (typeof window === 'undefined') {
|
||
const aiClient = await loadLegacyAiModule();
|
||
return aiClient.generateCharacterPanelChatSummary(
|
||
world,
|
||
playerCharacter,
|
||
targetCharacter,
|
||
storyHistory,
|
||
context,
|
||
conversationHistory,
|
||
previousSummary,
|
||
targetStatus,
|
||
);
|
||
}
|
||
|
||
const payload = {
|
||
worldType: world,
|
||
playerCharacter,
|
||
targetCharacter,
|
||
storyHistory,
|
||
context,
|
||
conversationHistory,
|
||
previousSummary,
|
||
targetStatus,
|
||
} satisfies CharacterChatSummaryRequest;
|
||
|
||
const { text } = await requestPlainText(
|
||
`${RUNTIME_API_BASE}/chat/character/summary`,
|
||
payload,
|
||
'角色聊天摘要生成失败',
|
||
);
|
||
return text.trim();
|
||
}
|
||
|
||
export async function generateCustomWorldProfile(
|
||
input: GenerateCustomWorldProfileInput | string,
|
||
options: GenerateCustomWorldProfileOptions = {},
|
||
): Promise<CustomWorldProfile> {
|
||
const normalizedInput =
|
||
typeof input === 'string'
|
||
? {
|
||
settingText: input,
|
||
creatorIntent: null,
|
||
generationMode: 'full' as const,
|
||
}
|
||
: {
|
||
settingText: input.settingText,
|
||
creatorIntent: input.creatorIntent ?? null,
|
||
generationMode: input.generationMode === 'fast' ? 'fast' as const : 'full' as const,
|
||
};
|
||
|
||
const session = await createCustomWorldSession({
|
||
settingText: normalizedInput.settingText,
|
||
creatorIntent:
|
||
normalizedInput.creatorIntent as Record<string, unknown> | null,
|
||
generationMode: normalizedInput.generationMode,
|
||
});
|
||
|
||
const fallbackAnswerMap: Record<string, string> = {
|
||
world_hook:
|
||
typeof normalizedInput.creatorIntent?.worldHook === 'string' &&
|
||
normalizedInput.creatorIntent.worldHook.trim()
|
||
? normalizedInput.creatorIntent.worldHook.trim()
|
||
: normalizedInput.settingText.trim().slice(0, 120) || '这是一个围绕失衡秩序展开的世界。',
|
||
player_premise:
|
||
typeof normalizedInput.creatorIntent?.playerPremise === 'string' &&
|
||
normalizedInput.creatorIntent.playerPremise.trim()
|
||
? normalizedInput.creatorIntent.playerPremise.trim()
|
||
: '玩家是一名被卷入局势中心的行动者。',
|
||
opening_situation:
|
||
typeof normalizedInput.creatorIntent?.openingSituation === 'string' &&
|
||
normalizedInput.creatorIntent.openingSituation.trim()
|
||
? normalizedInput.creatorIntent.openingSituation.trim()
|
||
: '故事开局时,玩家正身处风暴边缘,必须立刻判断立场与风险。',
|
||
core_conflict:
|
||
Array.isArray(normalizedInput.creatorIntent?.coreConflicts) &&
|
||
normalizedInput.creatorIntent.coreConflicts.length > 0
|
||
? normalizedInput.creatorIntent.coreConflicts
|
||
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
||
.filter(Boolean)
|
||
.join(';')
|
||
: '旧秩序与新威胁正在同时逼近,各方都在争夺主动权。',
|
||
};
|
||
|
||
for (const question of session.questions ?? []) {
|
||
if (question.answer?.trim()) {
|
||
continue;
|
||
}
|
||
|
||
const answer = fallbackAnswerMap[question.id] || normalizedInput.settingText.trim();
|
||
await answerCustomWorldSessionQuestion(session.sessionId, {
|
||
questionId: question.id,
|
||
answer,
|
||
});
|
||
}
|
||
|
||
return streamCustomWorldSessionGeneration(session.sessionId, options);
|
||
}
|
||
|
||
export async function streamCustomWorldSessionGeneration(
|
||
sessionId: string,
|
||
options: GenerateCustomWorldProfileOptions = {},
|
||
): Promise<CustomWorldProfile> {
|
||
const response = await fetchWithApiAuth(
|
||
`${RUNTIME_API_BASE}/custom-world/sessions/${encodeURIComponent(sessionId)}/generate/stream`,
|
||
{
|
||
method: 'GET',
|
||
signal: options.signal,
|
||
},
|
||
);
|
||
if (!response.ok) {
|
||
const responseText = await response.text();
|
||
throw new Error(parseApiErrorMessage(responseText, '生成自定义世界失败'));
|
||
}
|
||
if (!response.body) {
|
||
throw new Error('自定义世界生成流不可用');
|
||
}
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder('utf-8');
|
||
let buffer = '';
|
||
let latestProfile: Record<string, unknown> | 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 = '';
|
||
for (const rawLine of eventBlock.split(/\r?\n/u)) {
|
||
const line = rawLine.trim();
|
||
if (!line) {
|
||
continue;
|
||
}
|
||
if (line.startsWith('event:')) {
|
||
eventName = line.slice(6).trim();
|
||
continue;
|
||
}
|
||
if (!line.startsWith('data:')) {
|
||
continue;
|
||
}
|
||
|
||
const payloadText = line.slice(5).trim();
|
||
if (!payloadText) {
|
||
continue;
|
||
}
|
||
|
||
const payload = JSON.parse(payloadText) as Record<string, unknown>;
|
||
if (eventName === 'progress') {
|
||
if (
|
||
typeof payload.phaseId === 'string'
|
||
&& typeof payload.phaseLabel === 'string'
|
||
&& typeof payload.phaseDetail === 'string'
|
||
&& typeof payload.overallProgress === 'number'
|
||
&& Array.isArray(payload.steps)
|
||
) {
|
||
options.onProgress?.(payload as unknown as CustomWorldGenerationProgress);
|
||
} else {
|
||
options.onProgress?.({
|
||
phaseId: 'finalize',
|
||
phaseLabel: typeof payload.phase === 'string' ? payload.phase : 'generating',
|
||
phaseDetail: typeof payload.phase === 'string' ? payload.phase : 'generating',
|
||
overallProgress:
|
||
typeof payload.progress === 'number' ? payload.progress / 100 : 0,
|
||
completedWeight:
|
||
typeof payload.progress === 'number' ? payload.progress : 0,
|
||
totalWeight: 100,
|
||
elapsedMs: 0,
|
||
estimatedRemainingMs: null,
|
||
activeStepIndex: 0,
|
||
steps: [],
|
||
});
|
||
}
|
||
}
|
||
if (eventName === 'result' && payload.profile && typeof payload.profile === 'object') {
|
||
latestProfile = payload.profile as Record<string, unknown>;
|
||
}
|
||
if (eventName === 'error') {
|
||
throw new Error(
|
||
typeof payload.message === 'string'
|
||
? payload.message
|
||
: '生成自定义世界失败',
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!latestProfile) {
|
||
throw new Error('自定义世界生成未返回结果');
|
||
}
|
||
|
||
return latestProfile as unknown as CustomWorldProfile;
|
||
}
|
||
|
||
export async function generateCustomWorldSceneImage(
|
||
...args: [CustomWorldSceneImageRequest]
|
||
) {
|
||
const aiClient = await loadLegacyAiModule();
|
||
return aiClient.generateCustomWorldSceneImage(...args);
|
||
}
|
||
|
||
export async function createCustomWorldSession(payload: {
|
||
settingText: string;
|
||
creatorIntent?: Record<string, unknown> | null;
|
||
generationMode: 'fast' | 'full';
|
||
}) {
|
||
return requestJson<CustomWorldSessionSummary>(
|
||
`${RUNTIME_API_BASE}/custom-world/sessions`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload satisfies CreateCustomWorldSessionRequest),
|
||
},
|
||
'创建自定义世界会话失败',
|
||
);
|
||
}
|
||
|
||
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 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 getCustomWorldSession(sessionId: string) {
|
||
return requestJson<CustomWorldSessionRecord>(
|
||
`${RUNTIME_API_BASE}/custom-world/sessions/${encodeURIComponent(sessionId)}`,
|
||
{
|
||
method: 'GET',
|
||
},
|
||
'读取自定义世界会话失败',
|
||
);
|
||
}
|
||
|
||
export async function answerCustomWorldSessionQuestion(
|
||
sessionId: string,
|
||
payload: { questionId: string; answer: string },
|
||
) {
|
||
return requestJson<CustomWorldSessionSummary>(
|
||
`${RUNTIME_API_BASE}/custom-world/sessions/${encodeURIComponent(sessionId)}/answers`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload satisfies AnswerCustomWorldSessionQuestionRequest),
|
||
},
|
||
'提交自定义世界补充设定失败',
|
||
);
|
||
}
|
||
|
||
export async function streamCharacterPanelChatReply(
|
||
world: WorldType,
|
||
playerCharacter: Character,
|
||
targetCharacter: Character,
|
||
storyHistory: StoryMoment[],
|
||
context: StoryGenerationContext,
|
||
conversationHistory: CharacterChatTurn[],
|
||
conversationSummary: string,
|
||
playerMessage: string,
|
||
targetStatus: CharacterChatTargetStatus,
|
||
options: TextStreamOptions = {},
|
||
) {
|
||
if (typeof window === 'undefined') {
|
||
const aiClient = await loadLegacyAiModule();
|
||
return aiClient.streamCharacterPanelChatReply(
|
||
world,
|
||
playerCharacter,
|
||
targetCharacter,
|
||
storyHistory,
|
||
context,
|
||
conversationHistory,
|
||
conversationSummary,
|
||
playerMessage,
|
||
targetStatus,
|
||
options,
|
||
);
|
||
}
|
||
|
||
const payload = {
|
||
worldType: world,
|
||
playerCharacter,
|
||
targetCharacter,
|
||
storyHistory,
|
||
context,
|
||
conversationHistory,
|
||
conversationSummary,
|
||
playerMessage,
|
||
targetStatus,
|
||
} satisfies CharacterChatReplyRequest;
|
||
|
||
const reply = await requestPlainTextStream(
|
||
`${RUNTIME_API_BASE}/chat/character/reply/stream`,
|
||
payload,
|
||
options,
|
||
);
|
||
return reply.trim();
|
||
}
|
||
|
||
export async function streamNpcChatDialogue(
|
||
world: WorldType,
|
||
character: Character,
|
||
encounter: Encounter,
|
||
monsters: SceneHostileNpc[],
|
||
history: StoryMoment[],
|
||
context: StoryGenerationContext,
|
||
topic: string,
|
||
resultSummary: string,
|
||
options: TextStreamOptions = {},
|
||
) {
|
||
if (typeof window === 'undefined') {
|
||
const aiClient = await loadLegacyAiModule();
|
||
return aiClient.streamNpcChatDialogue(
|
||
world,
|
||
character,
|
||
encounter,
|
||
monsters,
|
||
history,
|
||
context,
|
||
topic,
|
||
resultSummary,
|
||
options,
|
||
);
|
||
}
|
||
|
||
const payload = {
|
||
worldType: world,
|
||
character,
|
||
encounter,
|
||
monsters,
|
||
history,
|
||
context,
|
||
topic,
|
||
resultSummary,
|
||
} satisfies NpcChatDialogueRequest;
|
||
|
||
const dialogue = await requestPlainTextStream(
|
||
`${RUNTIME_API_BASE}/chat/npc/dialogue/stream`,
|
||
payload,
|
||
options,
|
||
);
|
||
return dialogue.trim();
|
||
}
|
||
|
||
export async function streamNpcRecruitDialogue(
|
||
world: WorldType,
|
||
character: Character,
|
||
encounter: Encounter,
|
||
monsters: SceneHostileNpc[],
|
||
history: StoryMoment[],
|
||
context: StoryGenerationContext,
|
||
invitationText: string,
|
||
recruitSummary: string,
|
||
options: TextStreamOptions = {},
|
||
) {
|
||
if (typeof window === 'undefined') {
|
||
const aiClient = await loadLegacyAiModule();
|
||
return aiClient.streamNpcRecruitDialogue(
|
||
world,
|
||
character,
|
||
encounter,
|
||
monsters,
|
||
history,
|
||
context,
|
||
invitationText,
|
||
recruitSummary,
|
||
options,
|
||
);
|
||
}
|
||
|
||
const payload = {
|
||
worldType: world,
|
||
character,
|
||
encounter,
|
||
monsters,
|
||
history,
|
||
context,
|
||
invitationText,
|
||
recruitSummary,
|
||
} satisfies NpcRecruitDialogueRequest;
|
||
|
||
const dialogue = await requestPlainTextStream(
|
||
`${RUNTIME_API_BASE}/chat/npc/recruit/stream`,
|
||
payload,
|
||
options,
|
||
);
|
||
return dialogue.trim();
|
||
}
|
||
|
||
export type {
|
||
CustomWorldGenerationProgress,
|
||
CustomWorldSceneImageResult,
|
||
GenerateCustomWorldProfileInput,
|
||
GenerateCustomWorldProfileOptions,
|
||
StoryGenerationContext,
|
||
StoryRequestOptions,
|
||
TextStreamOptions,
|
||
};
|