diff --git a/index.js b/index.js index 3ae12ac..4b37579 100644 --- a/index.js +++ b/index.js @@ -209,7 +209,7 @@ const defaultSettings = { compressPrompt: "", synopsisPrompt: "", reflectionPrompt: "", - taskProfilesVersion: 1, + taskProfilesVersion: 2, taskProfiles: createDefaultTaskProfiles(), // ====== v2 增强设置 ====== diff --git a/panel.js b/panel.js index 8903246..fa9cedb 100644 --- a/panel.js +++ b/panel.js @@ -1705,7 +1705,7 @@ function _refreshTaskProfileWorkspace(settings = _getSettings?.() || {}) { function _patchTaskProfiles(taskProfiles, extraPatch = {}, options = {}) { return _patchSettings( { - taskProfilesVersion: 1, + taskProfilesVersion: 2, taskProfiles, ...extraPatch, }, diff --git a/prompt-profiles.js b/prompt-profiles.js index d67c52c..24a8bf5 100644 --- a/prompt-profiles.js +++ b/prompt-profiles.js @@ -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 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 }; function nowIso() { @@ -494,9 +718,91 @@ export function createDefaultTaskProfiles() { 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) { const legacyPromptField = LEGACY_PROMPT_FIELD_MAP[taskType]; - const defaults = DEFAULT_TASK_BLOCKS[taskType] || {}; return { id: DEFAULT_PROFILE_ID, name: "默认预设", @@ -507,92 +813,7 @@ export function createDefaultTaskProfile(taskType) { description: getDefaultProfileDescription(taskType), promptMode: "block-based", updatedAt: nowIso(), - blocks: [ - { - 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, - }, - ], + blocks: buildDefaultTaskProfileBlocks(taskType), generation: { max_context_tokens: null, max_completion_tokens: null, @@ -758,14 +979,18 @@ export function ensureTaskProfiles(settings = {}) { export function normalizeTaskProfile(taskType, profile = {}, settings = {}) { const base = createDefaultTaskProfile(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 - ? profile.blocks.map((block, index) => - normalizePromptBlock(taskType, block, index), - ) - : base.blocks.map((block, index) => - normalizePromptBlock(taskType, block, index), - ); + ? isBuiltinDefaultProfile + ? mergeDefaultTaskProfileBlocks(taskType, profile.blocks) + : profile.blocks + : base.blocks; + const blocks = rawBlocks.map((block, index) => + normalizePromptBlock(taskType, block, index), + ); return { ...base, diff --git a/tests/default-settings.mjs b/tests/default-settings.mjs index 014e85d..f899a6c 100644 --- a/tests/default-settings.mjs +++ b/tests/default-settings.mjs @@ -22,6 +22,7 @@ async function loadDefaultSettings() { compress: { activeProfileId: "default", profiles: [] }, synopsis: { 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.recallLlmContextMessages, 4); assert.equal(defaultSettings.injectDepth, 9999); -assert.equal(defaultSettings.taskProfilesVersion, 1); +assert.equal(defaultSettings.taskProfilesVersion, 2); assert.ok(defaultSettings.taskProfiles); assert.ok(defaultSettings.taskProfiles.extract); assert.ok(defaultSettings.taskProfiles.recall); diff --git a/tests/prompt-builder-defaults.mjs b/tests/prompt-builder-defaults.mjs new file mode 100644 index 0000000..5072e2c --- /dev/null +++ b/tests/prompt-builder-defaults.mjs @@ -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"); diff --git a/tests/task-profile-migration.mjs b/tests/task-profile-migration.mjs index 21f98c6..3ebbc83 100644 --- a/tests/task-profile-migration.mjs +++ b/tests/task-profile-migration.mjs @@ -15,7 +15,7 @@ const legacySettings = { }; const migrated = migrateLegacyTaskProfiles(legacySettings); -assert.equal(migrated.taskProfilesVersion, 1); +assert.equal(migrated.taskProfilesVersion, 2); assert.ok(migrated.taskProfiles); assert.ok(migrated.taskProfiles.extract); assert.ok(migrated.taskProfiles.recall); @@ -30,7 +30,7 @@ const extractProfile = getActiveTaskProfile( assert.equal(extractProfile.taskType, "extract"); assert.equal(extractProfile.id, "default"); assert.ok(Array.isArray(extractProfile.blocks)); -assert.equal(extractProfile.blocks.length, 7); +assert.equal(extractProfile.blocks.length, 11); assert.deepEqual( extractProfile.blocks.map((block) => block.name), [ @@ -39,13 +39,45 @@ assert.deepEqual( "用户设定", "世界书前块", "世界书后块", + "最近消息", + "图统计", + "Schema", + "当前范围", "输出格式", "行为规则", ], ); assert.deepEqual( 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( extractProfile.metadata.legacyPromptField, @@ -62,5 +94,124 @@ assert.ok(defaults.recall.profiles.length > 0); assert.ok(defaults.compress.profiles.length > 0); assert.ok(defaults.synopsis.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"); diff --git a/tests/task-profile-storage.mjs b/tests/task-profile-storage.mjs index 24955fb..cf29dad 100644 --- a/tests/task-profile-storage.mjs +++ b/tests/task-profile-storage.mjs @@ -51,7 +51,7 @@ const activeProfile = getActiveTaskProfile( "extract", ); assert.equal(activeProfile.name, "激进提取"); -assert.equal(activeProfile.blocks.length, 9); +assert.equal(activeProfile.blocks.length, 13); const builtinBlock = activeProfile.blocks.find( (block) => block.type === "builtin" && block.sourceKey === "userMessage", );