From 6116d7bc6db03e023a5603404a55a97bdd1eda1c Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Thu, 23 Apr 2026 17:52:06 +0800 Subject: [PATCH] fix: auto align legacy planner fake-default profiles --- ena-planner/ena-planner-presets.js | 50 ++++++ prompting/prompt-profiles.js | 254 ++++++++++++++++++++++++++++- tests/task-profile-migration.mjs | 221 +++++++++++++++++++++++++ 3 files changed, 522 insertions(+), 3 deletions(-) diff --git a/ena-planner/ena-planner-presets.js b/ena-planner/ena-planner-presets.js index 0ec3bcd..5a7ba9e 100644 --- a/ena-planner/ena-planner-presets.js +++ b/ena-planner/ena-planner-presets.js @@ -66,6 +66,56 @@ export const PLANNER_ASSISTANT_SEED = ` 先梳理玩家意图、当前局势、BME 记忆里的关键约束和最近的剧情推进,再给出下一步 plot 与 note。 `; +export const LEGACY_PLANNER_SYSTEM_PROMPT = `你是一位剧情规划师(Story Planner)。你的工作是在幕后为互动叙事提供方向指引,而不是直接扮演角色或撰写正文。 + +## 你会收到的信息 +- 角色卡:当前角色的设定(描述、性格、场景) +- 世界书:世界观设定和规则 +- 结构化记忆(BME):由记忆图谱整理出的长期记忆 + - [Memory - Core]:规则、摘要、长期约束 + - [Memory - Recalled]:与当前情境相关的人物状态、事件、地点、剧情线 +- 聊天历史:最近的 AI 回复片段 +- 历史规划:之前生成的 块 +- 玩家输入:玩家刚刚发出的指令或行动 + +## 你的任务 +根据以上信息,为下一轮 AI 回复规划剧情走向。 + +## 输出格式(严格遵守) +只输出以下两个标签,不要输出任何其他内容: + + +(剧情走向指引:接下来应该发生什么。包括场景推进、NPC 反应、事件触发、伏笔推进等。写给 AI 看的导演指令,不是给玩家看的正文。简洁、具体、可执行。) + + + +(写作注意事项:这一轮回复应该怎么写。包括叙事节奏、情绪基调、应避免的问题、需要保持的连贯性等。同样是给 AI 的元指令,不是正文。) + + +## 规划原则 +1. 尊重玩家意图:玩家输入是最高优先级。 +2. 保持连贯:与 BME 记忆、历史规划和世界规则一致。 +3. 推进而非重复:每轮规划都应推动剧情前进。 +4. 留有余地:给方向,不要把正文细节写死。 +5. 遵守世界观:世界书中的规则和设定属于硬约束。 + +如有思考过程,请放在 中(会被自动剔除)。`; + +export const LEGACY_DEFAULT_PROMPT_BLOCKS = [ + { + id: "ena-default-system-001", + role: "system", + name: "Ena Planner System", + content: LEGACY_PLANNER_SYSTEM_PROMPT, + }, + { + id: "ena-default-assistant-001", + role: "assistant", + name: "Assistant Seed", + content: PLANNER_ASSISTANT_SEED, + }, +]; + // --------------------------------------------------------------------------- // Legacy compat — kept so any code importing DEFAULT_PROMPT_BLOCKS still works // --------------------------------------------------------------------------- diff --git a/prompting/prompt-profiles.js b/prompting/prompt-profiles.js index 7406549..59b4a5b 100644 --- a/prompting/prompt-profiles.js +++ b/prompting/prompt-profiles.js @@ -2,6 +2,7 @@ import { DEFAULT_PROMPT_BLOCKS as DEFAULT_PLANNER_PROMPT_BLOCKS, + LEGACY_PLANNER_SYSTEM_PROMPT, PLANNER_HEADING, PLANNER_ROLE, PLANNER_IDENTITY_ACK, @@ -1120,6 +1121,235 @@ function normalizePromptBlock(taskType, block = {}, index = 0) { }; } +function sortPromptBlocksForComparison(blocks = []) { + return [...(Array.isArray(blocks) ? blocks : [])] + .map((block, index) => ({ ...block, _orderIndex: index })) + .sort((left, right) => { + const leftOrder = Number.isFinite(Number(left?.order)) + ? Number(left.order) + : left._orderIndex; + const rightOrder = Number.isFinite(Number(right?.order)) + ? Number(right.order) + : right._orderIndex; + return leftOrder - rightOrder; + }); +} + +function buildPromptBlockComparisonPayload(blocks = []) { + return sortPromptBlocksForComparison(blocks).map((block) => ({ + role: normalizeRole(block?.role), + type: String(block?.type || "custom"), + sourceKey: String(block?.sourceKey || ""), + content: String(block?.content || "").trim(), + enabled: block?.enabled !== false, + })); +} + +function buildLegacyPlannerDefaultLikeBlocks() { + return [ + normalizePromptBlock( + "planner", + { + id: "planner-legacy-default-system", + name: "Ena Planner System", + type: "custom", + enabled: true, + role: "system", + sourceKey: "", + sourceField: "", + content: LEGACY_PLANNER_SYSTEM_PROMPT, + injectionMode: "relative", + order: 0, + }, + 0, + ), + normalizePromptBlock( + "planner", + { + id: "planner-legacy-default-char", + name: "角色卡", + type: "builtin", + enabled: true, + role: "system", + sourceKey: "plannerCharacterCard", + sourceField: "", + content: "", + injectionMode: "relative", + order: 1, + }, + 1, + ), + normalizePromptBlock( + "planner", + { + id: "planner-legacy-default-worldbook", + name: "世界书", + type: "builtin", + enabled: true, + role: "system", + sourceKey: "plannerWorldbook", + sourceField: "", + content: "", + injectionMode: "relative", + order: 2, + }, + 2, + ), + normalizePromptBlock( + "planner", + { + id: "planner-legacy-default-recent-chat", + name: "最近聊天", + type: "builtin", + enabled: true, + role: "system", + sourceKey: "plannerRecentChat", + sourceField: "", + content: "", + injectionMode: "relative", + order: 3, + }, + 3, + ), + normalizePromptBlock( + "planner", + { + id: "planner-legacy-default-memory", + name: "BME 记忆", + type: "builtin", + enabled: true, + role: "system", + sourceKey: "plannerMemory", + sourceField: "", + content: "", + injectionMode: "relative", + order: 4, + }, + 4, + ), + normalizePromptBlock( + "planner", + { + id: "planner-legacy-default-previous-plots", + name: "历史 plot", + type: "builtin", + enabled: true, + role: "system", + sourceKey: "plannerPreviousPlots", + sourceField: "", + content: "", + injectionMode: "relative", + order: 5, + }, + 5, + ), + normalizePromptBlock( + "planner", + { + id: "planner-legacy-default-user-input", + name: "玩家输入", + type: "builtin", + enabled: true, + role: "user", + sourceKey: "plannerUserInput", + sourceField: "", + content: "", + injectionMode: "relative", + order: 6, + }, + 6, + ), + normalizePromptBlock( + "planner", + { + id: "planner-legacy-default-seed", + name: "Assistant Seed", + type: "custom", + enabled: true, + role: "assistant", + sourceKey: "", + sourceField: "", + content: PLANNER_ASSISTANT_SEED, + injectionMode: "relative", + order: 7, + }, + 7, + ), + ]; +} + +function isPlannerLegacyDefaultLikeProfile(profile = {}) { + if (String(profile?.taskType || "") !== "planner") { + return false; + } + if (profile?.builtin !== false) { + return false; + } + if (profile?.metadata?.migratedFromLegacy !== true) { + return false; + } + const legacySource = String(profile?.metadata?.enaLegacySource || "").trim(); + if (!legacySource) { + return false; + } + return ( + JSON.stringify(buildPromptBlockComparisonPayload(profile?.blocks || [])) === + JSON.stringify( + buildPromptBlockComparisonPayload(buildLegacyPlannerDefaultLikeBlocks()), + ) + ); +} + +function alignPlannerLegacyDefaultLikeProfiles( + profiles = [], + defaultProfile = null, + activeProfileId = "", +) { + if (!Array.isArray(profiles) || !defaultProfile) { + return { + profiles, + activeProfileId, + }; + } + + const defaultBlocks = cloneJson(defaultProfile.blocks || []); + const defaultGenerationSignature = JSON.stringify(defaultProfile.generation || {}); + let nextActiveProfileId = String(activeProfileId || ""); + let changed = false; + + const nextProfiles = profiles.map((profile) => { + if (!isPlannerLegacyDefaultLikeProfile(profile)) { + return profile; + } + changed = true; + if ( + JSON.stringify(profile?.generation || {}) === defaultGenerationSignature && + String(profile?.id || "") === nextActiveProfileId + ) { + nextActiveProfileId = DEFAULT_PROFILE_ID; + } + return { + ...profile, + updatedAt: nowIso(), + blocks: cloneJson(defaultBlocks), + metadata: { + ...(profile?.metadata || {}), + plannerLegacyDefaultAligned: true, + plannerLegacyDefaultAlignedAt: String( + defaultProfile?.metadata?.defaultTemplateUpdatedAt || + defaultProfile?.updatedAt || + "", + ), + }, + }; + }); + + return { + profiles: changed ? nextProfiles : profiles, + activeProfileId: nextActiveProfileId, + }; +} + function normalizeRegexLocalRule(rule = {}, taskType = "task", index = 0) { return { id: String(rule?.id || createRegexRuleId(taskType)), @@ -1981,10 +2211,28 @@ export function ensureTaskProfiles(settings = {}) { ]; } + let preferredActiveProfileId = + typeof current.activeProfileId === "string" ? current.activeProfileId : ""; + if (taskType === "planner") { + const defaultProfile = + profiles.find((profile) => String(profile?.id || "") === DEFAULT_PROFILE_ID) || + defaultBucket.profiles.find( + (profile) => String(profile?.id || "") === DEFAULT_PROFILE_ID, + ) || + null; + const alignedPlannerProfiles = alignPlannerLegacyDefaultLikeProfiles( + profiles, + defaultProfile, + preferredActiveProfileId, + ); + profiles = alignedPlannerProfiles.profiles; + preferredActiveProfileId = alignedPlannerProfiles.activeProfileId; + } + const activeProfileId = - typeof current.activeProfileId === "string" && - profiles.some((profile) => profile.id === current.activeProfileId) - ? current.activeProfileId + typeof preferredActiveProfileId === "string" && + profiles.some((profile) => profile.id === preferredActiveProfileId) + ? preferredActiveProfileId : profiles[0]?.id || DEFAULT_PROFILE_ID; normalized[taskType] = { diff --git a/tests/task-profile-migration.mjs b/tests/task-profile-migration.mjs index 8947310..6a4598f 100644 --- a/tests/task-profile-migration.mjs +++ b/tests/task-profile-migration.mjs @@ -1,4 +1,8 @@ import assert from "node:assert/strict"; +import { + LEGACY_PLANNER_SYSTEM_PROMPT, + PLANNER_ASSISTANT_SEED, +} from "../ena-planner/ena-planner-presets.js"; import { createDefaultTaskProfiles, ensureTaskProfiles, @@ -176,6 +180,223 @@ assert.deepEqual( assert.equal(defaults.planner.profiles[0].generation.stream, true); assert.equal(defaults.planner.profiles[0].generation.temperature, 1); +const currentDefaultPlanner = defaults.planner.profiles[0]; +const cloneValue = (value) => JSON.parse(JSON.stringify(value)); + +function buildLegacyPlannerDefaultLikeBlocks() { + return [ + { + id: "planner-legacy-default-system", + name: "Ena Planner System", + type: "custom", + enabled: true, + role: "system", + sourceKey: "", + sourceField: "", + content: LEGACY_PLANNER_SYSTEM_PROMPT, + injectionMode: "relative", + order: 0, + }, + { + id: "planner-legacy-default-char", + name: "角色卡", + type: "builtin", + enabled: true, + role: "system", + sourceKey: "plannerCharacterCard", + sourceField: "", + content: "", + injectionMode: "relative", + order: 1, + }, + { + id: "planner-legacy-default-worldbook", + name: "世界书", + type: "builtin", + enabled: true, + role: "system", + sourceKey: "plannerWorldbook", + sourceField: "", + content: "", + injectionMode: "relative", + order: 2, + }, + { + id: "planner-legacy-default-recent-chat", + name: "最近聊天", + type: "builtin", + enabled: true, + role: "system", + sourceKey: "plannerRecentChat", + sourceField: "", + content: "", + injectionMode: "relative", + order: 3, + }, + { + id: "planner-legacy-default-memory", + name: "BME 记忆", + type: "builtin", + enabled: true, + role: "system", + sourceKey: "plannerMemory", + sourceField: "", + content: "", + injectionMode: "relative", + order: 4, + }, + { + id: "planner-legacy-default-previous-plots", + name: "历史 plot", + type: "builtin", + enabled: true, + role: "system", + sourceKey: "plannerPreviousPlots", + sourceField: "", + content: "", + injectionMode: "relative", + order: 5, + }, + { + id: "planner-legacy-default-user-input", + name: "玩家输入", + type: "builtin", + enabled: true, + role: "user", + sourceKey: "plannerUserInput", + sourceField: "", + content: "", + injectionMode: "relative", + order: 6, + }, + { + id: "planner-legacy-default-seed", + name: "Assistant Seed", + type: "custom", + enabled: true, + role: "assistant", + sourceKey: "", + sourceField: "", + content: PLANNER_ASSISTANT_SEED, + injectionMode: "relative", + order: 7, + }, + ]; +} + +function createLegacyPlannerDefaultLikeProfile(overrides = {}) { + return { + id: "planner-legacy-default-like", + taskType: "planner", + builtin: false, + name: "ENA 当前配置", + promptMode: "block-based", + enabled: true, + updatedAt: "2026-04-23T00:00:00.000Z", + blocks: buildLegacyPlannerDefaultLikeBlocks(), + generation: cloneValue(currentDefaultPlanner.generation), + metadata: { + migratedFromLegacy: true, + enaLegacySource: "legacy-working-copy", + }, + ...overrides, + blocks: Array.isArray(overrides.blocks) + ? overrides.blocks + : buildLegacyPlannerDefaultLikeBlocks(), + generation: { + ...cloneValue(currentDefaultPlanner.generation), + ...(overrides.generation || {}), + }, + metadata: { + migratedFromLegacy: true, + enaLegacySource: "legacy-working-copy", + ...(overrides.metadata || {}), + }, + }; +} + +const legacyPlannerDefaultLikeProfile = createLegacyPlannerDefaultLikeProfile(); +const alignedLegacyPlannerDefaults = ensureTaskProfiles({ + taskProfilesVersion: 3, + taskProfiles: { + planner: { + activeProfileId: legacyPlannerDefaultLikeProfile.id, + profiles: [cloneValue(currentDefaultPlanner), legacyPlannerDefaultLikeProfile], + }, + }, +}); +const alignedLegacyPlannerProfile = alignedLegacyPlannerDefaults.planner.profiles.find( + (profile) => profile.id === legacyPlannerDefaultLikeProfile.id, +); +assert.equal(alignedLegacyPlannerDefaults.planner.activeProfileId, "default"); +assert.deepEqual( + alignedLegacyPlannerProfile.blocks.map((block) => block.sourceKey || block.id), + currentDefaultPlanner.blocks.map((block) => block.sourceKey || block.id), +); +assert.equal(alignedLegacyPlannerProfile.metadata.plannerLegacyDefaultAligned, true); + +const legacyPlannerCustomGenerationProfile = createLegacyPlannerDefaultLikeProfile({ + id: "planner-legacy-custom-generation", + generation: { + temperature: 0.7, + }, +}); +const alignedLegacyPlannerCustomGeneration = ensureTaskProfiles({ + taskProfilesVersion: 3, + taskProfiles: { + planner: { + activeProfileId: legacyPlannerCustomGenerationProfile.id, + profiles: [ + cloneValue(currentDefaultPlanner), + legacyPlannerCustomGenerationProfile, + ], + }, + }, +}); +const alignedLegacyPlannerCustomGenerationProfile = + alignedLegacyPlannerCustomGeneration.planner.profiles.find( + (profile) => profile.id === legacyPlannerCustomGenerationProfile.id, + ); +assert.equal( + alignedLegacyPlannerCustomGeneration.planner.activeProfileId, + legacyPlannerCustomGenerationProfile.id, +); +assert.deepEqual( + alignedLegacyPlannerCustomGenerationProfile.blocks.map( + (block) => block.sourceKey || block.id, + ), + currentDefaultPlanner.blocks.map((block) => block.sourceKey || block.id), +); +assert.equal(alignedLegacyPlannerCustomGenerationProfile.generation.temperature, 0.7); + +const customizedLegacyPlannerBlocks = buildLegacyPlannerDefaultLikeBlocks(); +customizedLegacyPlannerBlocks[0].content = `${customizedLegacyPlannerBlocks[0].content}\n\n自定义补充`; +const customizedLegacyPlannerProfile = createLegacyPlannerDefaultLikeProfile({ + id: "planner-legacy-customized", + blocks: customizedLegacyPlannerBlocks, +}); +const preservedCustomizedLegacyPlanner = ensureTaskProfiles({ + taskProfilesVersion: 3, + taskProfiles: { + planner: { + activeProfileId: customizedLegacyPlannerProfile.id, + profiles: [cloneValue(currentDefaultPlanner), customizedLegacyPlannerProfile], + }, + }, +}); +const preservedCustomizedLegacyPlannerProfile = + preservedCustomizedLegacyPlanner.planner.profiles.find( + (profile) => profile.id === customizedLegacyPlannerProfile.id, + ); +assert.equal( + preservedCustomizedLegacyPlanner.planner.activeProfileId, + customizedLegacyPlannerProfile.id, +); +assert.match( + preservedCustomizedLegacyPlannerProfile.blocks[0].content, + /自定义补充/, +); + const upgradedLegacyDefault = getActiveTaskProfile( { taskProfilesVersion: 1,