183 lines
4.8 KiB
TypeScript
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),
|
|
};
|
|
}
|