135 lines
4.3 KiB
TypeScript
135 lines
4.3 KiB
TypeScript
import {buildRuntimeItemAiIntent} from '../data/runtimeItemNarrative';
|
|
import type {
|
|
RuntimeItemAiIntent,
|
|
RuntimeItemGenerationContext,
|
|
RuntimeItemPlan,
|
|
} from '../types';
|
|
import { requestJson } from './apiClient';
|
|
import {requestChatMessageContent} from './llmClient';
|
|
import {parseJsonResponseText} from './llmParsers';
|
|
import {
|
|
buildRuntimeItemIntentPrompt,
|
|
RUNTIME_ITEM_INTENT_SYSTEM_PROMPT,
|
|
} from './runtimeItemAiPrompt';
|
|
|
|
const RUNTIME_ITEM_INTENT_TIMEOUT_MS = 9000;
|
|
|
|
function coerceString(value: unknown, fallback: string) {
|
|
return typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
}
|
|
|
|
function coerceStringArray(value: unknown, fallback: string[], limit: number) {
|
|
if (!Array.isArray(value)) {
|
|
return fallback;
|
|
}
|
|
|
|
const normalized = value
|
|
.map(item => (typeof item === 'string' ? item.trim() : ''))
|
|
.filter(Boolean)
|
|
.slice(0, limit);
|
|
|
|
return normalized.length > 0 ? normalized : fallback;
|
|
}
|
|
|
|
function sanitizeRuntimeItemAiIntent(
|
|
rawIntent: unknown,
|
|
fallback: RuntimeItemAiIntent,
|
|
): RuntimeItemAiIntent {
|
|
if (!rawIntent || typeof rawIntent !== 'object') {
|
|
return fallback;
|
|
}
|
|
|
|
const intent = rawIntent as Record<string, unknown>;
|
|
const desiredFunctionalBias = coerceStringArray(
|
|
intent.desiredFunctionalBias,
|
|
fallback.desiredFunctionalBias,
|
|
2,
|
|
).filter(
|
|
(
|
|
item,
|
|
): item is RuntimeItemAiIntent['desiredFunctionalBias'][number] =>
|
|
['heal', 'mana', 'cooldown', 'guard', 'damage'].includes(item),
|
|
);
|
|
const tone = coerceString(intent.tone, fallback.tone);
|
|
|
|
return {
|
|
shortNameSeed: coerceString(intent.shortNameSeed, fallback.shortNameSeed),
|
|
sourcePhrase: coerceString(intent.sourcePhrase, fallback.sourcePhrase),
|
|
reasonToAppear: coerceString(intent.reasonToAppear, fallback.reasonToAppear),
|
|
relationHooks: coerceStringArray(intent.relationHooks, fallback.relationHooks, 2),
|
|
desiredBuildTags: coerceStringArray(intent.desiredBuildTags, fallback.desiredBuildTags, 3),
|
|
desiredFunctionalBias:
|
|
desiredFunctionalBias.length > 0
|
|
? desiredFunctionalBias
|
|
: fallback.desiredFunctionalBias,
|
|
tone: ['grim', 'mysterious', 'martial', 'ritual', 'survival'].includes(tone)
|
|
? (tone as RuntimeItemAiIntent['tone'])
|
|
: fallback.tone,
|
|
visibleClue: coerceString(intent.visibleClue, fallback.visibleClue ?? ''),
|
|
witnessMark: coerceString(intent.witnessMark, fallback.witnessMark ?? ''),
|
|
unfinishedBusiness: coerceString(
|
|
intent.unfinishedBusiness,
|
|
fallback.unfinishedBusiness ?? '',
|
|
),
|
|
hiddenHook: coerceString(intent.hiddenHook, fallback.hiddenHook ?? ''),
|
|
reactionHooks: coerceStringArray(
|
|
intent.reactionHooks,
|
|
fallback.reactionHooks ?? [],
|
|
4,
|
|
),
|
|
namingPattern: coerceString(intent.namingPattern, fallback.namingPattern ?? ''),
|
|
};
|
|
}
|
|
|
|
export async function generateRuntimeItemAiIntents(params: {
|
|
context: RuntimeItemGenerationContext;
|
|
plans: RuntimeItemPlan[];
|
|
}) {
|
|
const fallbackIntents = params.plans.map(plan =>
|
|
buildRuntimeItemAiIntent(params.context, plan),
|
|
);
|
|
|
|
if (typeof window !== 'undefined') {
|
|
try {
|
|
const response = await requestJson<{
|
|
intents?: unknown[];
|
|
}>(
|
|
'/api/runtime/items/runtime-intent',
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(params),
|
|
},
|
|
'运行时物品意图生成失败',
|
|
);
|
|
const rawIntents = Array.isArray(response.intents) ? response.intents : [];
|
|
return params.plans.map((_, index) =>
|
|
sanitizeRuntimeItemAiIntent(rawIntents[index], fallbackIntents[index]!),
|
|
);
|
|
} catch (error) {
|
|
console.warn(
|
|
'[runtimeItemAiDirector] backend intent generation failed, using deterministic fallback',
|
|
error,
|
|
);
|
|
return fallbackIntents;
|
|
}
|
|
}
|
|
|
|
const content = await requestChatMessageContent(
|
|
RUNTIME_ITEM_INTENT_SYSTEM_PROMPT,
|
|
buildRuntimeItemIntentPrompt(params),
|
|
{
|
|
timeoutMs: RUNTIME_ITEM_INTENT_TIMEOUT_MS,
|
|
debugLabel: 'runtime-item-intent',
|
|
},
|
|
);
|
|
const parsed = parseJsonResponseText(content) as {
|
|
intents?: unknown[];
|
|
};
|
|
const rawIntents = Array.isArray(parsed.intents) ? parsed.intents : [];
|
|
|
|
return params.plans.map((_, index) =>
|
|
sanitizeRuntimeItemAiIntent(rawIntents[index], fallbackIntents[index]!),
|
|
);
|
|
}
|