fix: align default task profiles with runtime prompt blocks

This commit is contained in:
Youzini-afk
2026-03-27 03:34:47 +08:00
parent 7417eb1cbc
commit 88db5d95ea
7 changed files with 572 additions and 102 deletions

View File

@@ -209,7 +209,7 @@ const defaultSettings = {
compressPrompt: "", compressPrompt: "",
synopsisPrompt: "", synopsisPrompt: "",
reflectionPrompt: "", reflectionPrompt: "",
taskProfilesVersion: 1, taskProfilesVersion: 2,
taskProfiles: createDefaultTaskProfiles(), taskProfiles: createDefaultTaskProfiles(),
// ====== v2 增强设置 ====== // ====== v2 增强设置 ======

View File

@@ -1705,7 +1705,7 @@ function _refreshTaskProfileWorkspace(settings = _getSettings?.() || {}) {
function _patchTaskProfiles(taskProfiles, extraPatch = {}, options = {}) { function _patchTaskProfiles(taskProfiles, extraPatch = {}, options = {}) {
return _patchSettings( return _patchSettings(
{ {
taskProfilesVersion: 1, taskProfilesVersion: 2,
taskProfiles, taskProfiles,
...extraPatch, ...extraPatch,
}, },

View File

@@ -147,7 +147,7 @@ const BUILTIN_BLOCK_DEFINITIONS = [
}, },
]; ];
const DEFAULT_TASK_PROFILE_VERSION = 1; const DEFAULT_TASK_PROFILE_VERSION = 2;
const DEFAULT_PROFILE_ID = "default"; const DEFAULT_PROFILE_ID = "default";
const LEGACY_PROMPT_FIELD_MAP = { const LEGACY_PROMPT_FIELD_MAP = {
@@ -375,6 +375,230 @@ const DEFAULT_TASK_BLOCKS = {
}, },
}; };
const COMMON_DEFAULT_BLOCK_BLUEPRINTS = [
{
id: "default-role",
name: "角色定义",
type: "custom",
role: "system",
contentKey: "role",
},
{
id: "default-char-desc",
name: "角色描述",
type: "builtin",
role: "system",
sourceKey: "charDescription",
},
{
id: "default-user-persona",
name: "用户设定",
type: "builtin",
role: "system",
sourceKey: "userPersona",
},
{
id: "default-wi-before",
name: "世界书前块",
type: "builtin",
role: "system",
sourceKey: "worldInfoBefore",
},
{
id: "default-wi-after",
name: "世界书后块",
type: "builtin",
role: "system",
sourceKey: "worldInfoAfter",
},
];
const TASK_CONTEXT_BLOCK_BLUEPRINTS = {
extract: [
{
id: "default-recent-messages",
name: "最近消息",
type: "builtin",
role: "user",
sourceKey: "recentMessages",
},
{
id: "default-graph-stats",
name: "图统计",
type: "builtin",
role: "user",
sourceKey: "graphStats",
},
{
id: "default-schema",
name: "Schema",
type: "builtin",
role: "user",
sourceKey: "schema",
},
{
id: "default-current-range",
name: "当前范围",
type: "builtin",
role: "user",
sourceKey: "currentRange",
},
],
recall: [
{
id: "default-recent-messages",
name: "最近消息",
type: "builtin",
role: "user",
sourceKey: "recentMessages",
},
{
id: "default-user-message",
name: "用户消息",
type: "builtin",
role: "user",
sourceKey: "userMessage",
},
{
id: "default-candidate-nodes",
name: "候选节点",
type: "builtin",
role: "user",
sourceKey: "candidateNodes",
},
{
id: "default-graph-stats",
name: "图统计",
type: "builtin",
role: "user",
sourceKey: "graphStats",
},
],
consolidation: [
{
id: "default-candidate-nodes",
name: "候选节点",
type: "builtin",
role: "user",
sourceKey: "candidateNodes",
},
{
id: "default-graph-stats",
name: "图统计",
type: "builtin",
role: "user",
sourceKey: "graphStats",
},
],
compress: [
{
id: "default-node-content",
name: "节点内容",
type: "builtin",
role: "user",
sourceKey: "nodeContent",
},
{
id: "default-current-range",
name: "当前范围",
type: "builtin",
role: "user",
sourceKey: "currentRange",
},
{
id: "default-graph-stats",
name: "图统计",
type: "builtin",
role: "user",
sourceKey: "graphStats",
},
],
synopsis: [
{
id: "default-event-summary",
name: "事件摘要",
type: "builtin",
role: "user",
sourceKey: "eventSummary",
},
{
id: "default-character-summary",
name: "角色摘要",
type: "builtin",
role: "user",
sourceKey: "characterSummary",
},
{
id: "default-thread-summary",
name: "主线摘要",
type: "builtin",
role: "user",
sourceKey: "threadSummary",
},
{
id: "default-graph-stats",
name: "图统计",
type: "builtin",
role: "user",
sourceKey: "graphStats",
},
],
reflection: [
{
id: "default-event-summary",
name: "事件摘要",
type: "builtin",
role: "user",
sourceKey: "eventSummary",
},
{
id: "default-character-summary",
name: "角色摘要",
type: "builtin",
role: "user",
sourceKey: "characterSummary",
},
{
id: "default-thread-summary",
name: "主线摘要",
type: "builtin",
role: "user",
sourceKey: "threadSummary",
},
{
id: "default-contradiction-summary",
name: "矛盾摘要",
type: "builtin",
role: "user",
sourceKey: "contradictionSummary",
},
{
id: "default-graph-stats",
name: "图统计",
type: "builtin",
role: "user",
sourceKey: "graphStats",
},
],
};
const DEFAULT_TRAILING_BLOCK_BLUEPRINTS = [
{
id: "default-format",
name: "输出格式",
type: "custom",
role: "system",
contentKey: "format",
},
{
id: "default-rules",
name: "行为规则",
type: "custom",
role: "system",
contentKey: "rules",
},
];
export { DEFAULT_TASK_BLOCKS }; export { DEFAULT_TASK_BLOCKS };
function nowIso() { function nowIso() {
@@ -494,9 +718,91 @@ export function createDefaultTaskProfiles() {
return profiles; return profiles;
} }
function buildDefaultTaskProfileBlocks(taskType) {
const defaults = DEFAULT_TASK_BLOCKS[taskType] || {};
const blueprints = [
...COMMON_DEFAULT_BLOCK_BLUEPRINTS,
...(TASK_CONTEXT_BLOCK_BLUEPRINTS[taskType] || []),
...DEFAULT_TRAILING_BLOCK_BLUEPRINTS,
];
return blueprints.map((blueprint, index) => ({
id: blueprint.id,
name: blueprint.name,
type: blueprint.type,
enabled: true,
role: blueprint.role,
sourceKey: blueprint.sourceKey || "",
sourceField: "",
content:
blueprint.type === "custom"
? String(defaults?.[blueprint.contentKey] || "")
: "",
injectionMode: "relative",
order: index,
}));
}
function mergeDefaultTaskProfileBlocks(taskType, existingBlocks = []) {
const canonicalBlocks = buildDefaultTaskProfileBlocks(taskType);
const existingById = new Map(
(Array.isArray(existingBlocks) ? existingBlocks : [])
.filter((block) => block && typeof block === "object")
.map((block) => [String(block.id || ""), block]),
);
const merged = canonicalBlocks.map((canonicalBlock, index) => {
const existing = existingById.get(canonicalBlock.id);
if (!existing) {
return {
...canonicalBlock,
order: index,
};
}
return {
...canonicalBlock,
...existing,
id: canonicalBlock.id,
name:
typeof existing.name === "string" && existing.name
? existing.name
: canonicalBlock.name,
type: canonicalBlock.type,
role:
typeof existing.role === "string" && existing.role
? existing.role
: canonicalBlock.role,
sourceKey: canonicalBlock.sourceKey || "",
content:
canonicalBlock.type === "custom"
? typeof existing.content === "string"
? existing.content
: canonicalBlock.content
: typeof existing.content === "string"
? existing.content
: "",
injectionMode:
typeof existing.injectionMode === "string" && existing.injectionMode
? existing.injectionMode
: canonicalBlock.injectionMode,
order: index,
};
});
const canonicalIds = new Set(canonicalBlocks.map((block) => block.id));
const extraBlocks = (Array.isArray(existingBlocks) ? existingBlocks : [])
.filter((block) => block && typeof block === "object")
.filter((block) => !canonicalIds.has(String(block.id || "")))
.map((block, index) => ({
...block,
order: canonicalBlocks.length + index,
}));
return [...merged, ...extraBlocks];
}
export function createDefaultTaskProfile(taskType) { export function createDefaultTaskProfile(taskType) {
const legacyPromptField = LEGACY_PROMPT_FIELD_MAP[taskType]; const legacyPromptField = LEGACY_PROMPT_FIELD_MAP[taskType];
const defaults = DEFAULT_TASK_BLOCKS[taskType] || {};
return { return {
id: DEFAULT_PROFILE_ID, id: DEFAULT_PROFILE_ID,
name: "默认预设", name: "默认预设",
@@ -507,92 +813,7 @@ export function createDefaultTaskProfile(taskType) {
description: getDefaultProfileDescription(taskType), description: getDefaultProfileDescription(taskType),
promptMode: "block-based", promptMode: "block-based",
updatedAt: nowIso(), updatedAt: nowIso(),
blocks: [ blocks: buildDefaultTaskProfileBlocks(taskType),
{
id: "default-role",
name: "角色定义",
type: "custom",
enabled: true,
role: "system",
sourceKey: "",
sourceField: "",
content: defaults.role || "",
injectionMode: "relative",
order: 0,
},
{
id: "default-char-desc",
name: "角色描述",
type: "builtin",
enabled: true,
role: "system",
sourceKey: "charDescription",
sourceField: "",
content: "",
injectionMode: "relative",
order: 1,
},
{
id: "default-user-persona",
name: "用户设定",
type: "builtin",
enabled: true,
role: "system",
sourceKey: "userPersona",
sourceField: "",
content: "",
injectionMode: "relative",
order: 2,
},
{
id: "default-wi-before",
name: "世界书前块",
type: "builtin",
enabled: true,
role: "system",
sourceKey: "worldInfoBefore",
sourceField: "",
content: "",
injectionMode: "relative",
order: 3,
},
{
id: "default-wi-after",
name: "世界书后块",
type: "builtin",
enabled: true,
role: "system",
sourceKey: "worldInfoAfter",
sourceField: "",
content: "",
injectionMode: "relative",
order: 4,
},
{
id: "default-format",
name: "输出格式",
type: "custom",
enabled: true,
role: "system",
sourceKey: "",
sourceField: "",
content: defaults.format || "",
injectionMode: "relative",
order: 5,
},
{
id: "default-rules",
name: "行为规则",
type: "custom",
enabled: true,
role: "system",
sourceKey: "",
sourceField: "",
content: defaults.rules || "",
injectionMode: "relative",
order: 6,
},
],
generation: { generation: {
max_context_tokens: null, max_context_tokens: null,
max_completion_tokens: null, max_completion_tokens: null,
@@ -758,12 +979,16 @@ export function ensureTaskProfiles(settings = {}) {
export function normalizeTaskProfile(taskType, profile = {}, settings = {}) { export function normalizeTaskProfile(taskType, profile = {}, settings = {}) {
const base = createDefaultTaskProfile(taskType); const base = createDefaultTaskProfile(taskType);
const legacyPromptField = LEGACY_PROMPT_FIELD_MAP[taskType]; const legacyPromptField = LEGACY_PROMPT_FIELD_MAP[taskType];
const blocks = const isBuiltinDefaultProfile =
String(profile?.id || base.id) === DEFAULT_PROFILE_ID &&
profile?.builtin !== false;
const rawBlocks =
Array.isArray(profile.blocks) && profile.blocks.length > 0 Array.isArray(profile.blocks) && profile.blocks.length > 0
? profile.blocks.map((block, index) => ? isBuiltinDefaultProfile
normalizePromptBlock(taskType, block, index), ? mergeDefaultTaskProfileBlocks(taskType, profile.blocks)
) : profile.blocks
: base.blocks.map((block, index) => : base.blocks;
const blocks = rawBlocks.map((block, index) =>
normalizePromptBlock(taskType, block, index), normalizePromptBlock(taskType, block, index),
); );

View File

@@ -22,6 +22,7 @@ async function loadDefaultSettings() {
compress: { activeProfileId: "default", profiles: [] }, compress: { activeProfileId: "default", profiles: [] },
synopsis: { activeProfileId: "default", profiles: [] }, synopsis: { activeProfileId: "default", profiles: [] },
reflection: { activeProfileId: "default", profiles: [] }, reflection: { activeProfileId: "default", profiles: [] },
consolidation: { activeProfileId: "default", profiles: [] },
}; };
}, },
}); });
@@ -44,7 +45,7 @@ assert.equal(defaultSettings.recallDiffusionTopK, 100);
assert.equal(defaultSettings.recallLlmCandidatePool, 30); assert.equal(defaultSettings.recallLlmCandidatePool, 30);
assert.equal(defaultSettings.recallLlmContextMessages, 4); assert.equal(defaultSettings.recallLlmContextMessages, 4);
assert.equal(defaultSettings.injectDepth, 9999); assert.equal(defaultSettings.injectDepth, 9999);
assert.equal(defaultSettings.taskProfilesVersion, 1); assert.equal(defaultSettings.taskProfilesVersion, 2);
assert.ok(defaultSettings.taskProfiles); assert.ok(defaultSettings.taskProfiles);
assert.ok(defaultSettings.taskProfiles.extract); assert.ok(defaultSettings.taskProfiles.extract);
assert.ok(defaultSettings.taskProfiles.recall); assert.ok(defaultSettings.taskProfiles.recall);

View File

@@ -0,0 +1,93 @@
import assert from "node:assert/strict";
import { registerHooks } from "node:module";
const extensionsShimSource = [
"export function getContext() {",
" return {",
" chat: [],",
" chatMetadata: {},",
" extensionSettings: {},",
" powerUserSettings: {},",
" characters: {},",
" characterId: null,",
" name1: '',",
" name2: '',",
" chatId: 'test-chat',",
" };",
"}",
].join("\n");
registerHooks({
resolve(specifier, context, nextResolve) {
if (
specifier === "../../../extensions.js" ||
specifier === "../../../../extensions.js"
) {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(extensionsShimSource)}`,
};
}
return nextResolve(specifier, context);
},
});
const { buildTaskLlmPayload, buildTaskPrompt } = await import("../prompt-builder.js");
const { createDefaultTaskProfiles } = await import("../prompt-profiles.js");
const settings = {
taskProfilesVersion: 2,
taskProfiles: createDefaultTaskProfiles(),
};
const extractPromptBuild = await buildTaskPrompt(settings, "extract", {
taskName: "extract",
charDescription: "角色描述",
userPersona: "用户设定",
recentMessages: "A: 你好\nB: 世界",
graphStats: "node_count=3",
schema: "event(title, summary)",
currentRange: "1 ~ 2",
});
const extractPayload = buildTaskLlmPayload(extractPromptBuild, "fallback-user");
assert.equal(extractPayload.userPrompt, "");
assert.deepEqual(
extractPayload.promptMessages
.map((message) => message.sourceKey)
.filter(Boolean),
[
"charDescription",
"userPersona",
"recentMessages",
"graphStats",
"schema",
"currentRange",
],
);
const recallPromptBuild = await buildTaskPrompt(settings, "recall", {
taskName: "recall",
charDescription: "角色描述",
userPersona: "用户设定",
recentMessages: "上下文",
userMessage: "用户最新发言",
candidateNodes: "候选 1\n候选 2",
graphStats: "candidate_count=2",
});
const recallPayload = buildTaskLlmPayload(recallPromptBuild, "fallback-user");
assert.equal(recallPayload.userPrompt, "");
assert.deepEqual(
recallPayload.promptMessages
.map((message) => message.sourceKey)
.filter(Boolean),
[
"charDescription",
"userPersona",
"recentMessages",
"userMessage",
"candidateNodes",
"graphStats",
],
);
console.log("prompt-builder-defaults tests passed");

View File

@@ -15,7 +15,7 @@ const legacySettings = {
}; };
const migrated = migrateLegacyTaskProfiles(legacySettings); const migrated = migrateLegacyTaskProfiles(legacySettings);
assert.equal(migrated.taskProfilesVersion, 1); assert.equal(migrated.taskProfilesVersion, 2);
assert.ok(migrated.taskProfiles); assert.ok(migrated.taskProfiles);
assert.ok(migrated.taskProfiles.extract); assert.ok(migrated.taskProfiles.extract);
assert.ok(migrated.taskProfiles.recall); assert.ok(migrated.taskProfiles.recall);
@@ -30,7 +30,7 @@ const extractProfile = getActiveTaskProfile(
assert.equal(extractProfile.taskType, "extract"); assert.equal(extractProfile.taskType, "extract");
assert.equal(extractProfile.id, "default"); assert.equal(extractProfile.id, "default");
assert.ok(Array.isArray(extractProfile.blocks)); assert.ok(Array.isArray(extractProfile.blocks));
assert.equal(extractProfile.blocks.length, 7); assert.equal(extractProfile.blocks.length, 11);
assert.deepEqual( assert.deepEqual(
extractProfile.blocks.map((block) => block.name), extractProfile.blocks.map((block) => block.name),
[ [
@@ -39,13 +39,45 @@ assert.deepEqual(
"用户设定", "用户设定",
"世界书前块", "世界书前块",
"世界书后块", "世界书后块",
"最近消息",
"图统计",
"Schema",
"当前范围",
"输出格式", "输出格式",
"行为规则", "行为规则",
], ],
); );
assert.deepEqual( assert.deepEqual(
extractProfile.blocks.map((block) => block.type), extractProfile.blocks.map((block) => block.type),
["custom", "builtin", "builtin", "builtin", "builtin", "custom", "custom"], [
"custom",
"builtin",
"builtin",
"builtin",
"builtin",
"builtin",
"builtin",
"builtin",
"builtin",
"custom",
"custom",
],
);
assert.deepEqual(
extractProfile.blocks.map((block) => block.role),
[
"system",
"system",
"system",
"system",
"system",
"user",
"user",
"user",
"user",
"system",
"system",
],
); );
assert.equal( assert.equal(
extractProfile.metadata.legacyPromptField, extractProfile.metadata.legacyPromptField,
@@ -62,5 +94,124 @@ assert.ok(defaults.recall.profiles.length > 0);
assert.ok(defaults.compress.profiles.length > 0); assert.ok(defaults.compress.profiles.length > 0);
assert.ok(defaults.synopsis.profiles.length > 0); assert.ok(defaults.synopsis.profiles.length > 0);
assert.ok(defaults.reflection.profiles.length > 0); assert.ok(defaults.reflection.profiles.length > 0);
assert.deepEqual(
defaults.recall.profiles[0].blocks.map((block) => block.sourceKey || block.id),
[
"default-role",
"charDescription",
"userPersona",
"worldInfoBefore",
"worldInfoAfter",
"recentMessages",
"userMessage",
"candidateNodes",
"graphStats",
"default-format",
"default-rules",
],
);
assert.deepEqual(
defaults.synopsis.profiles[0].blocks.map((block) => block.sourceKey || block.id),
[
"default-role",
"charDescription",
"userPersona",
"worldInfoBefore",
"worldInfoAfter",
"eventSummary",
"characterSummary",
"threadSummary",
"graphStats",
"default-format",
"default-rules",
],
);
const upgradedLegacyDefault = getActiveTaskProfile(
{
taskProfilesVersion: 1,
taskProfiles: {
extract: {
activeProfileId: "default",
profiles: [
{
id: "default",
taskType: "extract",
builtin: true,
blocks: [
{
id: "default-role",
name: "角色定义",
type: "custom",
role: "system",
content: "保留我自己的角色定义",
order: 0,
},
{
id: "default-char-desc",
name: "角色描述",
type: "builtin",
role: "system",
sourceKey: "charDescription",
order: 1,
},
{
id: "default-user-persona",
name: "用户设定",
type: "builtin",
role: "system",
sourceKey: "userPersona",
order: 2,
},
{
id: "default-wi-before",
name: "世界书前块",
type: "builtin",
role: "system",
sourceKey: "worldInfoBefore",
order: 3,
},
{
id: "default-wi-after",
name: "世界书后块",
type: "builtin",
role: "system",
sourceKey: "worldInfoAfter",
order: 4,
},
{
id: "default-format",
name: "输出格式",
type: "custom",
role: "system",
content: "保留我自己的输出格式",
order: 5,
},
{
id: "default-rules",
name: "行为规则",
type: "custom",
role: "system",
content: "保留我自己的行为规则",
order: 6,
},
],
},
],
},
},
},
"extract",
);
assert.equal(upgradedLegacyDefault.blocks.length, 11);
assert.equal(upgradedLegacyDefault.blocks[0].content, "保留我自己的角色定义");
assert.equal(upgradedLegacyDefault.blocks[9].content, "保留我自己的输出格式");
assert.equal(upgradedLegacyDefault.blocks[10].content, "保留我自己的行为规则");
assert.deepEqual(
upgradedLegacyDefault.blocks
.slice(5, 9)
.map((block) => block.sourceKey),
["recentMessages", "graphStats", "schema", "currentRange"],
);
console.log("task-profile-migration tests passed"); console.log("task-profile-migration tests passed");

View File

@@ -51,7 +51,7 @@ const activeProfile = getActiveTaskProfile(
"extract", "extract",
); );
assert.equal(activeProfile.name, "激进提取"); assert.equal(activeProfile.name, "激进提取");
assert.equal(activeProfile.blocks.length, 9); assert.equal(activeProfile.blocks.length, 13);
const builtinBlock = activeProfile.blocks.find( const builtinBlock = activeProfile.blocks.find(
(block) => block.type === "builtin" && block.sourceKey === "userMessage", (block) => block.type === "builtin" && block.sourceKey === "userMessage",
); );