Files
Genarrative/src/services/storyHistory.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

183 lines
4.8 KiB
TypeScript

import { StoryHistoryRole, StoryMoment, StoryOption } from '../types';
import { sanitizePromptNarrativeText } from './narrativeLanguage';
const RECENT_ROUND_COUNT = 3;
const MAX_SUMMARY_GROUPS = 6;
const ACTION_SUMMARY_LIMIT = 24;
const RESULT_SUMMARY_LIMIT = 80;
type StoryHistoryRound = {
actionText: string | null;
resultTexts: string[];
};
export interface StoryPromptHistory {
recentOriginalRounds: string[];
previousSummary: string | null;
}
function normalizeWhitespace(text: string) {
return text.replace(/\s+/g, ' ').trim();
}
function truncateText(text: string, limit: number) {
if (text.length <= limit) return text;
return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}...`;
}
function hasRoundContent(round: StoryHistoryRound) {
return Boolean(round.actionText) || round.resultTexts.length > 0;
}
function resolveHistoryRole(entry: StoryMoment, index: number): StoryHistoryRole {
if (entry.historyRole) {
return entry.historyRole;
}
return index % 2 === 0 ? 'action' : 'result';
}
function buildStoryRounds(history: StoryMoment[]): StoryHistoryRound[] {
const rounds: StoryHistoryRound[] = [];
let currentRound: StoryHistoryRound | null = null;
for (const [index, entry] of history.entries()) {
const historyRole = resolveHistoryRole(entry, index);
const text = sanitizePromptNarrativeText(
entry.text,
historyRole === 'action'
? '玩家做出了新的决定。'
: '这一轮的局势已经出现了新的变化。',
);
if (!text) {
continue;
}
if (historyRole === 'action') {
if (currentRound && hasRoundContent(currentRound)) {
rounds.push(currentRound);
}
currentRound = {
actionText: text,
resultTexts: [],
};
continue;
}
if (!currentRound) {
currentRound = {
actionText: null,
resultTexts: [],
};
}
currentRound.resultTexts.push(text);
}
if (currentRound && hasRoundContent(currentRound)) {
rounds.push(currentRound);
}
return rounds;
}
function formatRoundOriginal(round: StoryHistoryRound) {
const resultText = round.resultTexts.join('\n').trim() || '本轮没有明显的结果文本。';
return [
round.actionText
? `玩家行动:${round.actionText}`
: '玩家行动:本轮没有明确的行动文本。',
`剧情结果:${resultText}`,
].join('\n');
}
function summarizeRound(round: StoryHistoryRound) {
const actionText = round.actionText ? normalizeWhitespace(round.actionText) : '';
const resultText = normalizeWhitespace(round.resultTexts.join(' '));
if (actionText && resultText) {
return `玩家选择了“${truncateText(actionText, ACTION_SUMMARY_LIMIT)}”,随后${truncateText(resultText, RESULT_SUMMARY_LIMIT)}`;
}
if (resultText) {
return truncateText(resultText, RESULT_SUMMARY_LIMIT);
}
if (actionText) {
return `玩家选择了“${truncateText(actionText, ACTION_SUMMARY_LIMIT)}”。`;
}
return '这一轮没有留下可用的剧情文本。';
}
function _summarizeRoundGroup(rounds: StoryHistoryRound[]) {
const firstRound = rounds[0];
if (!firstRound) {
return '没有可供总结的剧情记录。';
}
if (rounds.length === 1) {
return summarizeRound(firstRound);
}
const secondRound = rounds[1];
if (rounds.length === 2 && secondRound) {
return `${summarizeRound(firstRound)} ${summarizeRound(secondRound)}`;
}
const lastRound = rounds[rounds.length - 1] ?? firstRound;
return `${summarizeRound(firstRound)} 之后又推进了${rounds.length - 2}轮相关剧情。${summarizeRound(lastRound)}`;
}
function summarizeOlderRounds(rounds: StoryHistoryRound[]) {
if (rounds.length === 0) {
return null;
}
const groupSize = Math.max(1, Math.ceil(rounds.length / MAX_SUMMARY_GROUPS));
const summaryLines: string[] = [];
for (let index = 0; index < rounds.length; index += groupSize) {
const group = rounds.slice(index, index + groupSize);
summaryLines.push(`- ${group.map(summarizeRound).join(' ')}`);
}
return summaryLines.join('\n');
}
function summarizeOlderRoundsCompact(rounds: StoryHistoryRound[]) {
return summarizeOlderRounds(rounds);
}
export function createHistoryMoment(
text: string,
historyRole: StoryHistoryRole,
options: StoryOption[] = [],
): StoryMoment {
return {
text,
options,
historyRole,
};
}
export function buildStoryPromptHistory(history: StoryMoment[]): StoryPromptHistory {
const rounds = buildStoryRounds(history);
if (rounds.length === 0) {
return {
recentOriginalRounds: [],
previousSummary: null,
};
}
const recentRounds = rounds.slice(-RECENT_ROUND_COUNT);
const previousRounds = rounds.slice(0, -RECENT_ROUND_COUNT);
return {
recentOriginalRounds: recentRounds.map(formatRoundOriginal),
previousSummary: summarizeOlderRoundsCompact(previousRounds),
};
}