fix: auto align legacy planner fake-default profiles

This commit is contained in:
Youzini-afk
2026-04-23 17:52:06 +08:00
parent 3cc90d3d98
commit 6116d7bc6d
3 changed files with 522 additions and 3 deletions

View File

@@ -66,6 +66,56 @@ export const PLANNER_ASSISTANT_SEED = `<think>
先梳理玩家意图、当前局势、BME 记忆里的关键约束和最近的剧情推进,再给出下一步 plot 与 note。 先梳理玩家意图、当前局势、BME 记忆里的关键约束和最近的剧情推进,再给出下一步 plot 与 note。
</think>`; </think>`;
export const LEGACY_PLANNER_SYSTEM_PROMPT = `你是一位剧情规划师Story Planner。你的工作是在幕后为互动叙事提供方向指引而不是直接扮演角色或撰写正文。
## 你会收到的信息
- 角色卡:当前角色的设定(描述、性格、场景)
- 世界书:世界观设定和规则
- 结构化记忆BME由记忆图谱整理出的长期记忆
- [Memory - Core]:规则、摘要、长期约束
- [Memory - Recalled]:与当前情境相关的人物状态、事件、地点、剧情线
- 聊天历史:最近的 AI 回复片段
- 历史规划:之前生成的 <plot> 块
- 玩家输入:玩家刚刚发出的指令或行动
## 你的任务
根据以上信息,为下一轮 AI 回复规划剧情走向。
## 输出格式(严格遵守)
只输出以下两个标签,不要输出任何其他内容:
<plot>
剧情走向指引接下来应该发生什么。包括场景推进、NPC 反应、事件触发、伏笔推进等。写给 AI 看的导演指令,不是给玩家看的正文。简洁、具体、可执行。)
</plot>
<note>
(写作注意事项:这一轮回复应该怎么写。包括叙事节奏、情绪基调、应避免的问题、需要保持的连贯性等。同样是给 AI 的元指令,不是正文。)
</note>
## 规划原则
1. 尊重玩家意图:玩家输入是最高优先级。
2. 保持连贯:与 BME 记忆、历史规划和世界规则一致。
3. 推进而非重复:每轮规划都应推动剧情前进。
4. 留有余地:给方向,不要把正文细节写死。
5. 遵守世界观:世界书中的规则和设定属于硬约束。
如有思考过程,请放在 <thinking> 中(会被自动剔除)。`;
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 // Legacy compat — kept so any code importing DEFAULT_PROMPT_BLOCKS still works
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -2,6 +2,7 @@
import { import {
DEFAULT_PROMPT_BLOCKS as DEFAULT_PLANNER_PROMPT_BLOCKS, DEFAULT_PROMPT_BLOCKS as DEFAULT_PLANNER_PROMPT_BLOCKS,
LEGACY_PLANNER_SYSTEM_PROMPT,
PLANNER_HEADING, PLANNER_HEADING,
PLANNER_ROLE, PLANNER_ROLE,
PLANNER_IDENTITY_ACK, 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) { function normalizeRegexLocalRule(rule = {}, taskType = "task", index = 0) {
return { return {
id: String(rule?.id || createRegexRuleId(taskType)), 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 = const activeProfileId =
typeof current.activeProfileId === "string" && typeof preferredActiveProfileId === "string" &&
profiles.some((profile) => profile.id === current.activeProfileId) profiles.some((profile) => profile.id === preferredActiveProfileId)
? current.activeProfileId ? preferredActiveProfileId
: profiles[0]?.id || DEFAULT_PROFILE_ID; : profiles[0]?.id || DEFAULT_PROFILE_ID;
normalized[taskType] = { normalized[taskType] = {

View File

@@ -1,4 +1,8 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import {
LEGACY_PLANNER_SYSTEM_PROMPT,
PLANNER_ASSISTANT_SEED,
} from "../ena-planner/ena-planner-presets.js";
import { import {
createDefaultTaskProfiles, createDefaultTaskProfiles,
ensureTaskProfiles, ensureTaskProfiles,
@@ -176,6 +180,223 @@ assert.deepEqual(
assert.equal(defaults.planner.profiles[0].generation.stream, true); assert.equal(defaults.planner.profiles[0].generation.stream, true);
assert.equal(defaults.planner.profiles[0].generation.temperature, 1); 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( const upgradedLegacyDefault = getActiveTaskProfile(
{ {
taskProfilesVersion: 1, taskProfilesVersion: 1,