This commit is contained in:
2026-04-19 20:33:18 +08:00
parent 692643136f
commit 67c584b4df
123 changed files with 11898 additions and 4082 deletions

View File

@@ -0,0 +1,197 @@
type JsonRecord = Record<string, unknown>;
function readString(value: unknown) {
return typeof value === 'string' && value.trim() ? value.trim() : null;
}
function readNumber(value: unknown, fallback = 0) {
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
}
function describeWorld(worldType: string) {
switch (worldType) {
case 'WUXIA':
return '边城模板';
case 'XIANXIA':
return '灵潮模板';
case 'CUSTOM':
return '自定义世界';
default:
return worldType || '未知世界';
}
}
function describeCharacter(character: JsonRecord) {
return [
`主角:${readString(character.name) ?? '未知角色'}`,
`称号:${readString(character.title) ?? '未知称号'}`,
`描述:${readString(character.description) ?? '暂无'}`,
`性格:${readString(character.personality) ?? '未显式提供'}`,
].join('\n');
}
function describeMonsters(monsters: JsonRecord[]) {
if (monsters.length <= 0) {
return '当前敌对目标:无。';
}
return [
'当前敌对目标:',
...monsters.slice(0, 4).map((monster) => {
const name = readString(monster.name) ?? readString(monster.id) ?? '未知目标';
const hp = readNumber(monster.hp);
const maxHp = Math.max(1, readNumber(monster.maxHp, hp));
return `- ${name}(生命 ${hp}/${maxHp}`;
}),
].join('\n');
}
function describeStoryHistory(history: JsonRecord[]) {
if (history.length <= 0) {
return '近期剧情:暂无。';
}
return [
'近期剧情:',
...history.slice(-4).map((moment) => `- ${readString(moment.text) ?? '(空白)'}`),
].join('\n');
}
function describeRequestOptions(options: {
availableOptions?: Array<Record<string, unknown>>;
optionCatalog?: Array<Record<string, unknown>>;
}) {
const available = options.availableOptions ?? [];
const catalog = options.optionCatalog ?? [];
if (available.length > 0) {
return [
'固定可选项列表:',
...available.map((option, index) => {
const functionId = readString(option.functionId) ?? 'unknown';
const actionText =
readString(option.actionText) ??
readString(option.text) ??
'未提供文案';
return `- 第 ${index + 1} 项 / ${functionId}${actionText}`;
}),
'必须保持数量不变functionId 不变,可以重写 actionText。'.trim(),
].join('\n');
}
if (catalog.length > 0) {
return [
'当前局面可调用的交互选项目录:',
...catalog.map((option, index) => {
const functionId = readString(option.functionId) ?? 'unknown';
const actionText =
readString(option.actionText) ??
readString(option.text) ??
'未提供文案';
return `- 第 ${index + 1} 项 / ${functionId}${actionText}`;
}),
'functionId 只能从上面目录里选择。'.trim(),
].join('\n');
}
return '当前没有固定目录,请根据局势生成合理选项。';
}
function hasNpcOptionCatalog(options: {
availableOptions?: Array<Record<string, unknown>>;
optionCatalog?: Array<Record<string, unknown>>;
}) {
return (options.optionCatalog ?? []).some((option) =>
(readString(option.functionId) ?? '').startsWith('npc_'),
);
}
function isPostNpcChatReevaluation(params: {
choice?: string;
context: JsonRecord;
requestOptions?: {
availableOptions?: Array<Record<string, unknown>>;
optionCatalog?: Array<Record<string, unknown>>;
};
}) {
return (
readString(params.context.lastFunctionId) === 'npc_chat' &&
hasNpcOptionCatalog(params.requestOptions ?? {}) &&
Boolean(readString(params.choice))
);
}
export const SYSTEM_PROMPT = `你是角色扮演 RPG 的剧情推进者,只能返回 JSON 对象不能输出解释、markdown 或代码块。
输出格式必须严格符合:
{
"storyText": "剧情文本",
"encounter": null,
"options": [
{
"functionId": "预定义功能ID",
"actionText": "选项显示文本"
}
]
}
严格规则:
- 所有文本必须是中文。
- 如果提示语给出了固定可选项或可选目录,必须遵守给定的 functionId 范围。
- storyText 必须直接承接当前场景、最近剧情和玩家刚刚的动作。
- options 只允许输出 functionId 和 actionText。
- 如果当前不是“继续推进后下一刻会遇到什么”的场景encounter 必须保持为 null。`;
export function buildUserPrompt(params: {
worldType: string;
character: JsonRecord;
monsters: JsonRecord[];
history: JsonRecord[];
context: JsonRecord;
choice?: string;
requestOptions?: {
availableOptions?: Array<Record<string, unknown>>;
optionCatalog?: Array<Record<string, unknown>>;
};
}) {
const sceneName = readString(params.context.sceneName) ?? '当前区域';
const sceneDescription = readString(params.context.sceneDescription) ?? '暂无场景附加描述。';
const encounterName = readString(params.context.encounterName);
const playerHp = readNumber(params.context.playerHp);
const playerMaxHp = Math.max(1, readNumber(params.context.playerMaxHp, playerHp));
const playerMana = readNumber(params.context.playerMana);
const playerMaxMana = Math.max(1, readNumber(params.context.playerMaxMana, playerMana));
const inBattle = params.context.inBattle === true ? '战斗中' : '非战斗';
const pendingSceneEncounter =
params.context.pendingSceneEncounter === true ? '是' : '否';
const postNpcChatReevaluation = isPostNpcChatReevaluation(params);
return [
`世界:${describeWorld(params.worldType)}`,
`场景:${sceneName}`,
`场景描述:${sceneDescription}`,
encounterName ? `当前面前对象:${encounterName}` : null,
`当前状态:${inBattle}`,
`玩家生命:${playerHp}/${playerMaxHp}`,
`玩家灵力:${playerMana}/${playerMaxMana}`,
`是否需要判断下一刻遭遇:${pendingSceneEncounter}`,
describeCharacter(params.character),
describeMonsters(params.monsters),
describeStoryHistory(params.history),
params.choice ? `玩家刚刚选择:${params.choice}` : '玩家刚进入当前局面。',
describeRequestOptions(params.requestOptions ?? {}),
postNpcChatReevaluation
? '当前这一步是刚结束一轮 NPC 交谈后对眼前局势的再次判断。storyText 必须先落出刚才那段聊天带来的态度变化、气氛变化或新暴露的信息,再进入下一步局势。'
: null,
postNpcChatReevaluation
? '如果输出 npc_ 开头的选项,这些 actionText 必须直接承接刚才聊到的话题、关系变化或对方态度,写成此刻自然浮现的回应,不要退回“继续交谈”“请求援手”“看看能交换什么”这类通用模板。'
: null,
postNpcChatReevaluation
? '当前目录只是合法 function 范围,不代表都要出现;只保留此刻真正自然浮现、和刚才聊天结果有关的选项。'
: null,
params.context.pendingSceneEncounter === true
? '如果这一轮明确是在判断继续推进后下一刻会遇到什么,可以填写 encounter否则 encounter 必须为 null。'
: '当前这一步不是新的遭遇生成流程encounter 必须为 null。',
]
.filter(Boolean)
.join('\n\n');
}