diff --git a/prompt-profiles.js b/prompt-profiles.js index 0490533..8e52307 100644 --- a/prompt-profiles.js +++ b/prompt-profiles.js @@ -443,6 +443,19 @@ function getDefaultTaskProfileTemplate(taskType) { return cloneJson(template); } +function getDefaultTaskProfileTemplateStamp(taskType) { + const template = getDefaultTaskProfileTemplate(taskType); + return { + version: Number.isFinite(Number(template?.version)) + ? Number(template.version) + : DEFAULT_TASK_PROFILE_VERSION, + updatedAt: + typeof template?.updatedAt === "string" && template.updatedAt + ? template.updatedAt + : "", + }; +} + function buildDefaultTaskBlockTripletsFromTemplate(taskType) { const template = getDefaultTaskProfileTemplate(taskType); const blocks = Array.isArray(template?.blocks) ? template.blocks : []; @@ -775,8 +788,45 @@ function mergeDefaultTaskProfileBlocks(taskType, existingBlocks = []) { return [...merged, ...extraBlocks]; } +function shouldRefreshBuiltinDefaultProfile(taskType, profile = {}) { + if (String(profile?.id || "") !== DEFAULT_PROFILE_ID || profile?.builtin === false) { + return false; + } + + const expectedStamp = getDefaultTaskProfileTemplateStamp(taskType); + const metadata = profile?.metadata || {}; + const currentVersion = Number.isFinite(Number(metadata?.defaultTemplateVersion)) + ? Number(metadata.defaultTemplateVersion) + : Number.isFinite(Number(profile?.version)) + ? Number(profile.version) + : 0; + const currentUpdatedAt = + typeof metadata?.defaultTemplateUpdatedAt === "string" + ? metadata.defaultTemplateUpdatedAt + : ""; + + if (currentVersion < expectedStamp.version) { + return true; + } + + if ( + expectedStamp.updatedAt && + currentUpdatedAt && + currentUpdatedAt !== expectedStamp.updatedAt + ) { + return true; + } + + if (expectedStamp.updatedAt && !currentUpdatedAt) { + return true; + } + + return false; +} + function createFallbackDefaultTaskProfile(taskType) { const legacyPromptField = LEGACY_PROMPT_FIELD_MAP[taskType]; + const templateStamp = getDefaultTaskProfileTemplateStamp(taskType); return { id: DEFAULT_PROFILE_ID, name: "默认预设", @@ -834,6 +884,8 @@ function createFallbackDefaultTaskProfile(taskType) { metadata: { migratedFromLegacy: false, legacyPromptField, + defaultTemplateVersion: templateStamp.version, + defaultTemplateUpdatedAt: templateStamp.updatedAt, }, }; } @@ -846,6 +898,7 @@ export function createDefaultTaskProfile(taskType) { const legacyPromptField = LEGACY_PROMPT_FIELD_MAP[taskType]; const fallback = createFallbackDefaultTaskProfile(taskType); + const templateStamp = getDefaultTaskProfileTemplateStamp(taskType); return { ...fallback, ...template, @@ -891,6 +944,8 @@ export function createDefaultTaskProfile(taskType) { ...(template?.metadata || {}), migratedFromLegacy: false, legacyPromptField, + defaultTemplateVersion: templateStamp.version, + defaultTemplateUpdatedAt: templateStamp.updatedAt, }, }; } @@ -985,13 +1040,25 @@ export function ensureTaskProfiles(settings = {}) { for (const taskType of TASK_TYPES) { const current = existing[taskType] || {}; const defaultBucket = defaults[taskType]; - const profiles = + let profiles = Array.isArray(current.profiles) && current.profiles.length > 0 ? current.profiles.map((profile) => normalizeTaskProfile(taskType, profile, settings), ) : defaultBucket.profiles; + const defaultIndex = profiles.findIndex( + (profile) => String(profile?.id || "") === DEFAULT_PROFILE_ID, + ); + if (defaultIndex >= 0 && shouldRefreshBuiltinDefaultProfile(taskType, profiles[defaultIndex])) { + const refreshedDefault = createDefaultTaskProfile(taskType); + profiles = [ + ...profiles.slice(0, defaultIndex), + refreshedDefault, + ...profiles.slice(defaultIndex + 1), + ]; + } + const activeProfileId = typeof current.activeProfileId === "string" && profiles.some((profile) => profile.id === current.activeProfileId) diff --git a/tests/task-profile-migration.mjs b/tests/task-profile-migration.mjs index 4f5a1f8..0186751 100644 --- a/tests/task-profile-migration.mjs +++ b/tests/task-profile-migration.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { createDefaultTaskProfiles, + ensureTaskProfiles, getActiveTaskProfile, migrateLegacyTaskProfiles, } from "../prompt-profiles.js"; @@ -218,6 +219,119 @@ assert.equal(upgradedLegacyDefault.blocks[10].content, "保留我自己的输出 assert.equal(upgradedLegacyDefault.blocks[11].content, "保留我自己的行为规则"); assert.equal(upgradedLegacyDefault.blocks[10].role, "user"); assert.equal(upgradedLegacyDefault.blocks[11].role, "user"); + +const currentDefaults = createDefaultTaskProfiles(); +const currentDefaultExtract = currentDefaults.extract.profiles[0]; + +const staleBuiltinDefaults = ensureTaskProfiles({ + taskProfilesVersion: 3, + taskProfiles: { + extract: { + activeProfileId: "default", + profiles: [ + { + ...currentDefaultExtract, + updatedAt: "2000-01-01T00:00:00.000Z", + blocks: currentDefaultExtract.blocks.map((block) => + block.id === "default-role" + ? { ...block, content: "这是过期的默认角色定义" } + : block, + ), + metadata: { + ...(currentDefaultExtract.metadata || {}), + defaultTemplateVersion: + Number(currentDefaultExtract.metadata?.defaultTemplateVersion || 3), + defaultTemplateUpdatedAt: "2000-01-01T00:00:00.000Z", + }, + }, + { + id: "extract-custom-1", + taskType: "extract", + builtin: false, + name: "我的自定义预设", + promptMode: "block-based", + enabled: true, + updatedAt: "2026-04-05T00:00:00.000Z", + blocks: [ + { + id: "custom-block-1", + name: "自定义块", + type: "custom", + enabled: true, + role: "system", + sourceKey: "", + sourceField: "", + content: "保留我的自定义内容", + injectionMode: "append", + order: 0, + }, + ], + generation: { ...(currentDefaultExtract.generation || {}) }, + regex: { + ...(currentDefaultExtract.regex || {}), + localRules: [], + }, + metadata: { + note: "custom-profile-should-stay", + }, + }, + ], + }, + }, +}); +const refreshedDefaultExtract = staleBuiltinDefaults.extract.profiles.find( + (profile) => profile.id === "default", +); +const preservedCustomExtract = staleBuiltinDefaults.extract.profiles.find( + (profile) => profile.id === "extract-custom-1", +); + +assert.ok(refreshedDefaultExtract); +assert.equal( + refreshedDefaultExtract.blocks.find((block) => block.id === "default-role") + ?.content, + currentDefaultExtract.blocks.find((block) => block.id === "default-role") + ?.content, +); +assert.equal( + refreshedDefaultExtract.metadata.defaultTemplateUpdatedAt, + currentDefaultExtract.metadata.defaultTemplateUpdatedAt, +); +assert.ok(preservedCustomExtract); +assert.equal( + preservedCustomExtract.blocks[0].content, + "保留我的自定义内容", +); + +const sameStampBuiltinDefault = ensureTaskProfiles({ + taskProfilesVersion: 3, + taskProfiles: { + extract: { + activeProfileId: "default", + profiles: [ + { + ...currentDefaultExtract, + blocks: currentDefaultExtract.blocks.map((block) => + block.id === "default-role" + ? { ...block, content: "同版本下保留我的默认预设修改" } + : block, + ), + metadata: { + ...(currentDefaultExtract.metadata || {}), + }, + }, + ], + }, + }, +}); +const sameStampDefaultExtract = sameStampBuiltinDefault.extract.profiles.find( + (profile) => profile.id === "default", +); +assert.equal( + sameStampDefaultExtract.blocks.find((block) => block.id === "default-role") + ?.content, + "同版本下保留我的默认预设修改", +); assert.deepEqual( upgradedLegacyDefault.blocks .slice(6, 10)