Files
Genarrative/server-node/src/services/customWorldSessionStore.ts

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);
}
}