230 lines
6.3 KiB
TypeScript
230 lines
6.3 KiB
TypeScript
import crypto from 'node:crypto';
|
|
|
|
import type { JsonObject } from '../../../packages/shared/src/contracts/common.js';
|
|
import type {
|
|
CustomWorldGenerationMode,
|
|
CustomWorldQuestion,
|
|
CustomWorldSessionRecord,
|
|
CustomWorldSessionStatus,
|
|
} from '../../../packages/shared/src/contracts/runtime.js';
|
|
import type { RuntimeRepositoryPort } from '../repositories/runtimeRepository.js';
|
|
|
|
export type CustomWorldSession = {
|
|
sessionId: string;
|
|
status: CustomWorldSessionStatus;
|
|
settingText: string;
|
|
creatorIntent: JsonObject | null;
|
|
generationMode: CustomWorldGenerationMode;
|
|
questions: CustomWorldQuestion[];
|
|
result?: JsonObject;
|
|
lastError?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
function cloneSession(session: CustomWorldSession) {
|
|
return JSON.parse(JSON.stringify(session)) as CustomWorldSession;
|
|
}
|
|
|
|
function toSessionRecord(session: CustomWorldSession): CustomWorldSessionRecord {
|
|
return {
|
|
sessionId: session.sessionId,
|
|
status: session.status,
|
|
settingText: session.settingText,
|
|
creatorIntent: session.creatorIntent,
|
|
generationMode: session.generationMode,
|
|
questions: session.questions,
|
|
result: session.result,
|
|
lastError: session.lastError,
|
|
createdAt: session.createdAt,
|
|
updatedAt: session.updatedAt,
|
|
};
|
|
}
|
|
|
|
function toSession(record: CustomWorldSessionRecord) {
|
|
return cloneSession({
|
|
sessionId: record.sessionId,
|
|
status: record.status,
|
|
settingText: record.settingText,
|
|
creatorIntent: record.creatorIntent ?? null,
|
|
generationMode: record.generationMode,
|
|
questions: record.questions,
|
|
result: record.result,
|
|
lastError: record.lastError,
|
|
createdAt: record.createdAt,
|
|
updatedAt: record.updatedAt,
|
|
});
|
|
}
|
|
|
|
function hasPendingQuestion(questions: CustomWorldQuestion[]) {
|
|
return questions.some((question) => !question.answer?.trim());
|
|
}
|
|
|
|
function buildClarificationQuestions(
|
|
settingText: string,
|
|
creatorIntent: JsonObject | null,
|
|
) {
|
|
const questions: CustomWorldQuestion[] = [];
|
|
const worldHook =
|
|
typeof creatorIntent?.worldHook === 'string' ? creatorIntent.worldHook.trim() : '';
|
|
const playerPremise =
|
|
typeof creatorIntent?.playerPremise === 'string' ? creatorIntent.playerPremise.trim() : '';
|
|
const openingSituation =
|
|
typeof creatorIntent?.openingSituation === 'string'
|
|
? creatorIntent.openingSituation.trim()
|
|
: '';
|
|
const coreConflicts = Array.isArray(creatorIntent?.coreConflicts)
|
|
? creatorIntent.coreConflicts
|
|
: [];
|
|
|
|
if (!worldHook && settingText.trim().length < 24) {
|
|
questions.push({
|
|
id: 'world_hook',
|
|
label: '世界核心',
|
|
question: '请用一句话补充这个世界最核心的命题或独特卖点。',
|
|
});
|
|
}
|
|
if (!playerPremise) {
|
|
questions.push({
|
|
id: 'player_premise',
|
|
label: '玩家身份',
|
|
question: '玩家在这个世界里是什么身份、立场或来历?',
|
|
});
|
|
}
|
|
if (!openingSituation) {
|
|
questions.push({
|
|
id: 'opening_situation',
|
|
label: '开局处境',
|
|
question: '故事开局时,玩家正处于什么局面?',
|
|
});
|
|
}
|
|
if (coreConflicts.length === 0) {
|
|
questions.push({
|
|
id: 'core_conflict',
|
|
label: '核心冲突',
|
|
question: '这个世界当前最核心的冲突、危机或悬念是什么?',
|
|
});
|
|
}
|
|
|
|
return questions;
|
|
}
|
|
|
|
export class CustomWorldSessionStore {
|
|
constructor(
|
|
private readonly runtimeRepository: RuntimeRepositoryPort,
|
|
) {}
|
|
|
|
async create(
|
|
userId: string,
|
|
settingText: string,
|
|
creatorIntent: JsonObject | null,
|
|
generationMode: CustomWorldGenerationMode,
|
|
) {
|
|
const sessionId = `custom-world-session-${crypto.randomBytes(16).toString('hex')}`;
|
|
const now = new Date().toISOString();
|
|
const session: CustomWorldSession = {
|
|
sessionId,
|
|
status: 'ready_to_generate',
|
|
settingText,
|
|
creatorIntent,
|
|
generationMode,
|
|
questions: buildClarificationQuestions(settingText, creatorIntent),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
|
|
if (hasPendingQuestion(session.questions)) {
|
|
session.status = 'clarifying';
|
|
}
|
|
|
|
await this.runtimeRepository.upsertCustomWorldSession(
|
|
userId,
|
|
sessionId,
|
|
toSessionRecord(session),
|
|
);
|
|
return cloneSession(session);
|
|
}
|
|
|
|
async list(userId: string) {
|
|
const sessions = await this.runtimeRepository.listCustomWorldSessions(userId);
|
|
return sessions.map((session) => toSession(session));
|
|
}
|
|
|
|
async get(userId: string, sessionId: string) {
|
|
const session = await this.runtimeRepository.getCustomWorldSession(
|
|
userId,
|
|
sessionId,
|
|
);
|
|
return session ? toSession(session) : null;
|
|
}
|
|
|
|
async answer(
|
|
userId: string,
|
|
sessionId: string,
|
|
questionId: string,
|
|
answer: string,
|
|
) {
|
|
const session = await this.get(userId, sessionId);
|
|
if (!session) {
|
|
return null;
|
|
}
|
|
|
|
const question = session.questions.find((item) => item.id === questionId);
|
|
if (!question) {
|
|
return null;
|
|
}
|
|
|
|
question.answer = answer;
|
|
session.status = hasPendingQuestion(session.questions)
|
|
? 'clarifying'
|
|
: 'ready_to_generate';
|
|
session.updatedAt = new Date().toISOString();
|
|
await this.runtimeRepository.upsertCustomWorldSession(
|
|
userId,
|
|
sessionId,
|
|
toSessionRecord(session),
|
|
);
|
|
return cloneSession(session);
|
|
}
|
|
|
|
async updateStatus(
|
|
userId: string,
|
|
sessionId: string,
|
|
status: CustomWorldSessionStatus,
|
|
lastError = '',
|
|
) {
|
|
const session = await this.get(userId, sessionId);
|
|
if (!session) {
|
|
return null;
|
|
}
|
|
|
|
session.status = status;
|
|
session.lastError = lastError || undefined;
|
|
session.updatedAt = new Date().toISOString();
|
|
await this.runtimeRepository.upsertCustomWorldSession(
|
|
userId,
|
|
sessionId,
|
|
toSessionRecord(session),
|
|
);
|
|
return cloneSession(session);
|
|
}
|
|
|
|
async setResult(userId: string, sessionId: string, result: JsonObject) {
|
|
const session = await this.get(userId, sessionId);
|
|
if (!session) {
|
|
return null;
|
|
}
|
|
|
|
session.status = 'completed';
|
|
session.lastError = undefined;
|
|
session.result = JSON.parse(JSON.stringify(result)) as JsonObject;
|
|
session.updatedAt = new Date().toISOString();
|
|
await this.runtimeRepository.upsertCustomWorldSession(
|
|
userId,
|
|
sessionId,
|
|
toSessionRecord(session),
|
|
);
|
|
return cloneSession(session);
|
|
}
|
|
}
|