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,