This commit is contained in:
182
src/services/storyHistory.ts
Normal file
182
src/services/storyHistory.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user