// ST-BME: 任务预设与兼容迁移层 const TASK_TYPES = [ "extract", "recall", "compress", "synopsis", "reflection", "consolidation", ]; const TASK_TYPE_META = { extract: { label: "提取", description: "从当前对话批次中抽取结构化记忆。", }, recall: { label: "召回", description: "根据上下文筛选最相关的记忆节点。", }, compress: { label: "压缩", description: "合并并压缩高层节点内容。", }, synopsis: { label: "概要", description: "生成阶段性的全局剧情提要。", }, reflection: { label: "反思", description: "沉淀长期趋势、触发点与建议。", }, consolidation: { label: "整合", description: "分析新旧记忆的冲突、去重与进化。", }, }; const BUILTIN_BLOCK_DEFINITIONS = [ { sourceKey: "taskName", name: "任务名", role: "system", description: "当前任务类型标识。", }, { sourceKey: "systemInstruction", name: "系统说明", role: "system", description: "任务系统级说明或通用约束。", }, { sourceKey: "outputRules", name: "输出规则", role: "system", description: "用于声明 JSON 或结构化输出要求。", }, { sourceKey: "schema", name: "Schema", role: "system", description: "节点类型或字段定义。", }, { sourceKey: "recentMessages", name: "最近消息", role: "user", description: "最近对话上下文或历史片段。", }, { sourceKey: "userMessage", name: "用户消息", role: "user", description: "当前用户输入内容。", }, { sourceKey: "candidateNodes", name: "候选节点", role: "user", description: "召回或整合阶段的候选节点列表。", }, { sourceKey: "graphStats", name: "图统计", role: "user", description: "图谱状态或当前图概览。", }, { sourceKey: "currentRange", name: "当前范围", role: "user", description: "当前处理的消息或楼层范围。", }, { sourceKey: "nodeContent", name: "节点内容", role: "user", description: "待压缩或待处理的节点正文。", }, { sourceKey: "eventSummary", name: "事件摘要", role: "user", description: "近期事件线摘要。", }, { sourceKey: "characterSummary", name: "角色摘要", role: "user", description: "近期角色状态摘要。", }, { sourceKey: "threadSummary", name: "主线摘要", role: "user", description: "活跃主线或当前线程摘要。", }, { sourceKey: "contradictionSummary", name: "矛盾摘要", role: "user", description: "近期冲突或矛盾信息。", }, ]; const DEFAULT_TASK_PROFILE_VERSION = 1; const DEFAULT_PROFILE_ID = "default"; const LEGACY_PROMPT_FIELD_MAP = { extract: "extractPrompt", recall: "recallPrompt", compress: "compressPrompt", synopsis: "synopsisPrompt", reflection: "reflectionPrompt", consolidation: "consolidationPrompt", }; function nowIso() { return new Date().toISOString(); } function cloneJson(value) { return JSON.parse(JSON.stringify(value ?? null)); } function createUniqueId(prefix = "profile") { return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`; } function normalizeRole(role) { const value = String(role || "system").trim().toLowerCase(); if (["system", "user", "assistant"].includes(value)) { return value; } return "system"; } function normalizeInjectionMode(mode) { const value = String(mode || "append").trim().toLowerCase(); if (["append", "prepend", "relative"].includes(value)) { return value; } return "append"; } function normalizePromptBlock(taskType, block = {}, index = 0) { const fallbackType = String(block?.type || "custom"); return { id: String(block?.id || createPromptBlockId(taskType)), name: typeof block?.name === "string" ? block.name : "", type: fallbackType, enabled: block?.enabled !== false, role: normalizeRole(block?.role), sourceKey: typeof block?.sourceKey === "string" ? block.sourceKey : "", sourceField: typeof block?.sourceField === "string" ? block.sourceField : "", content: typeof block?.content === "string" ? block.content : "", injectionMode: normalizeInjectionMode(block?.injectionMode), order: Number.isFinite(Number(block?.order)) ? Number(block.order) : index, }; } function normalizeRegexLocalRule(rule = {}, taskType = "task", index = 0) { return { id: String(rule?.id || createRegexRuleId(taskType)), script_name: String( rule?.script_name || rule?.scriptName || `本地规则 ${index + 1}`, ), enabled: rule?.enabled !== false, find_regex: String(rule?.find_regex || rule?.findRegex || ""), replace_string: String( rule?.replace_string ?? rule?.replaceString ?? "", ), trim_strings: Array.isArray(rule?.trim_strings) ? rule.trim_strings.map((item) => String(item || "")) : typeof rule?.trim_strings === "string" ? rule.trim_strings : "", source: { user_input: rule?.source?.user_input === undefined ? true : Boolean(rule.source.user_input), ai_output: rule?.source?.ai_output === undefined ? true : Boolean(rule.source.ai_output), }, destination: { prompt: rule?.destination?.prompt === undefined ? true : Boolean(rule.destination.prompt), display: Boolean(rule?.destination?.display), }, min_depth: Number.isFinite(Number(rule?.min_depth)) ? Number(rule.min_depth) : 0, max_depth: Number.isFinite(Number(rule?.max_depth)) ? Number(rule.max_depth) : 9999, }; } function normalizeTaskProfilesState(taskProfiles = {}) { return ensureTaskProfiles({ taskProfiles }); } function getDefaultProfileDescription(taskType) { return TASK_TYPE_META[taskType]?.description || ""; } export function createPromptBlockId(taskType = "task") { return createUniqueId(`${taskType}-block`); } export function createRegexRuleId(taskType = "task") { return createUniqueId(`${taskType}-rule`); } export function createProfileId(taskType = "task") { return createUniqueId(`${taskType}-profile`); } export function createDefaultTaskProfiles() { const profiles = {}; for (const taskType of TASK_TYPES) { profiles[taskType] = { activeProfileId: DEFAULT_PROFILE_ID, profiles: [createDefaultTaskProfile(taskType)], }; } return profiles; } export function createDefaultTaskProfile(taskType) { const legacyPromptField = LEGACY_PROMPT_FIELD_MAP[taskType]; return { id: DEFAULT_PROFILE_ID, name: "默认预设", taskType, version: DEFAULT_TASK_PROFILE_VERSION, builtin: true, enabled: true, description: getDefaultProfileDescription(taskType), promptMode: "legacy-compatible", updatedAt: nowIso(), blocks: [ { id: "legacy-system", name: "兼容主提示词", type: "legacyPrompt", enabled: true, role: "system", sourceField: legacyPromptField, sourceKey: "", content: "", injectionMode: "append", order: 0, }, ], generation: { max_context_tokens: null, max_completion_tokens: null, reply_count: null, stream: false, temperature: null, top_p: null, top_k: null, top_a: null, min_p: null, seed: null, frequency_penalty: null, presence_penalty: null, repetition_penalty: null, squash_system_messages: null, reasoning_effort: null, request_thoughts: null, enable_function_calling: null, enable_web_search: null, character_name_prefix: null, wrap_user_messages_in_quotes: null, }, regex: { enabled: false, inheritStRegex: true, sources: { global: true, preset: true, character: true, }, stages: { finalPrompt: true, "input.userMessage": false, "input.recentMessages": false, "input.candidateText": false, "input.finalPrompt": false, rawResponse: false, beforeParse: false, "output.rawResponse": false, "output.beforeParse": false, }, localRules: [], }, metadata: { migratedFromLegacy: false, legacyPromptField, }, }; } export function createCustomPromptBlock(taskType, overrides = {}) { return normalizePromptBlock(taskType, { id: createPromptBlockId(taskType), name: "自定义块", type: "custom", enabled: true, role: "system", sourceKey: "", sourceField: "", content: "", injectionMode: "append", order: 0, ...overrides, }); } export function createBuiltinPromptBlock(taskType, sourceKey = "", overrides = {}) { const definition = BUILTIN_BLOCK_DEFINITIONS.find((item) => item.sourceKey === sourceKey) || BUILTIN_BLOCK_DEFINITIONS[0]; return normalizePromptBlock(taskType, { id: createPromptBlockId(taskType), name: definition?.name || "内置块", type: "builtin", enabled: true, role: definition?.role || "system", sourceKey: definition?.sourceKey || sourceKey, sourceField: "", content: "", injectionMode: "append", order: 0, ...overrides, }); } export function createLocalRegexRule(taskType, overrides = {}) { return normalizeRegexLocalRule( { id: createRegexRuleId(taskType), script_name: "本地规则", enabled: true, find_regex: "", replace_string: "", trim_strings: "", source: { user_input: true, ai_output: true, }, destination: { prompt: true, display: false, }, min_depth: 0, max_depth: 9999, ...overrides, }, taskType, 0, ); } export function ensureTaskProfiles(settings = {}) { const existing = settings.taskProfiles; const defaults = createDefaultTaskProfiles(); if (!existing || typeof existing !== "object") { return defaults; } const normalized = {}; for (const taskType of TASK_TYPES) { const current = existing[taskType] || {}; const defaultBucket = defaults[taskType]; const profiles = Array.isArray(current.profiles) && current.profiles.length > 0 ? current.profiles.map((profile) => normalizeTaskProfile(taskType, profile, settings), ) : defaultBucket.profiles; const activeProfileId = typeof current.activeProfileId === "string" && profiles.some((profile) => profile.id === current.activeProfileId) ? current.activeProfileId : profiles[0]?.id || DEFAULT_PROFILE_ID; normalized[taskType] = { activeProfileId, profiles, }; } return normalized; } export function normalizeTaskProfile(taskType, profile = {}, settings = {}) { const base = createDefaultTaskProfile(taskType); const legacyPromptField = LEGACY_PROMPT_FIELD_MAP[taskType]; const blocks = 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), ); return { ...base, ...profile, id: String(profile?.id || base.id), name: String(profile?.name || base.name), taskType, builtin: profile?.builtin === undefined ? profile?.id === DEFAULT_PROFILE_ID : Boolean(profile?.builtin), enabled: profile?.enabled !== false, description: typeof profile?.description === "string" ? profile.description : base.description, promptMode: String(profile?.promptMode || base.promptMode), updatedAt: typeof profile?.updatedAt === "string" && profile.updatedAt ? profile.updatedAt : nowIso(), blocks, generation: { ...base.generation, ...(profile?.generation || {}), }, regex: { ...base.regex, ...(profile?.regex || {}), sources: { ...base.regex.sources, ...(profile?.regex?.sources || {}), }, stages: { ...base.regex.stages, ...(profile?.regex?.stages || {}), }, localRules: Array.isArray(profile?.regex?.localRules) ? profile.regex.localRules.map((rule, index) => normalizeRegexLocalRule(rule, taskType, index), ) : [], }, metadata: { ...base.metadata, ...(profile?.metadata || {}), legacyPromptField, legacyPromptSnapshot: typeof settings?.[legacyPromptField] === "string" ? settings[legacyPromptField] : "", }, }; } export function migrateLegacyTaskProfiles(settings = {}) { const alreadyMigrated = Number(settings.taskProfilesVersion) >= DEFAULT_TASK_PROFILE_VERSION; const nextTaskProfiles = ensureTaskProfiles(settings); let changed = !alreadyMigrated; for (const taskType of TASK_TYPES) { const legacyField = LEGACY_PROMPT_FIELD_MAP[taskType]; const legacyPrompt = typeof settings?.[legacyField] === "string" ? settings[legacyField] : ""; const bucket = nextTaskProfiles[taskType]; if (!bucket || !Array.isArray(bucket.profiles) || bucket.profiles.length === 0) { nextTaskProfiles[taskType] = { activeProfileId: DEFAULT_PROFILE_ID, profiles: [createDefaultTaskProfile(taskType)], }; changed = true; continue; } const firstProfile = bucket.profiles[0]; if ( firstProfile?.id === DEFAULT_PROFILE_ID && firstProfile?.metadata?.migratedFromLegacy !== true && legacyPrompt ) { firstProfile.metadata = { ...(firstProfile.metadata || {}), migratedFromLegacy: true, legacyPromptField: legacyField, legacyPromptSnapshot: legacyPrompt, }; changed = true; } } return { changed, taskProfilesVersion: DEFAULT_TASK_PROFILE_VERSION, taskProfiles: nextTaskProfiles, }; } export function getActiveTaskProfile(settings = {}, taskType) { const taskProfiles = ensureTaskProfiles(settings); const bucket = taskProfiles?.[taskType]; if (!bucket?.profiles?.length) { return createDefaultTaskProfile(taskType); } return ( bucket.profiles.find((profile) => profile.id === bucket.activeProfileId) || bucket.profiles[0] ); } export function getLegacyPromptForTask(settings = {}, taskType) { const field = LEGACY_PROMPT_FIELD_MAP[taskType]; return typeof settings?.[field] === "string" ? settings[field] : ""; } export function getLegacyPromptFieldForTask(taskType) { return LEGACY_PROMPT_FIELD_MAP[taskType] || ""; } export function getTaskTypeMeta(taskType) { return { id: taskType, label: TASK_TYPE_META[taskType]?.label || taskType, description: TASK_TYPE_META[taskType]?.description || "", }; } export function getTaskTypeOptions() { return TASK_TYPES.map((taskType) => getTaskTypeMeta(taskType)); } export function getTaskTypes() { return [...TASK_TYPES]; } export function getBuiltinBlockDefinitions() { return BUILTIN_BLOCK_DEFINITIONS.map((definition) => ({ ...definition })); } export function cloneTaskProfile(profile = {}, options = {}) { const taskType = String(options.taskType || profile.taskType || "extract"); const cloned = normalizeTaskProfile(taskType, cloneJson(profile)); const nextName = String(options.name || "").trim() || `${cloned.name} 副本`; const nextProfile = { ...cloned, id: createProfileId(taskType), taskType, name: nextName, builtin: false, updatedAt: nowIso(), blocks: (Array.isArray(cloned.blocks) ? cloned.blocks : []).map( (block, index) => normalizePromptBlock( taskType, { ...block, id: createPromptBlockId(taskType), order: index, }, index, ), ), regex: { ...(cloned.regex || {}), localRules: Array.isArray(cloned?.regex?.localRules) ? cloned.regex.localRules.map((rule, index) => normalizeRegexLocalRule( { ...rule, id: createRegexRuleId(taskType), }, taskType, index, ), ) : [], }, metadata: { ...(cloned.metadata || {}), clonedFromId: cloned.id || "", clonedAt: nowIso(), }, }; return nextProfile; } export function upsertTaskProfile( taskProfiles = {}, taskType, profile, options = {}, ) { const normalizedState = normalizeTaskProfilesState(taskProfiles); const bucket = normalizedState[taskType] || { activeProfileId: DEFAULT_PROFILE_ID, profiles: [], }; const normalizedProfile = normalizeTaskProfile(taskType, { ...(profile || {}), updatedAt: nowIso(), }); const nextProfiles = [...bucket.profiles]; const existingIndex = nextProfiles.findIndex( (item) => item.id === normalizedProfile.id, ); if (existingIndex >= 0) { nextProfiles.splice(existingIndex, 1, normalizedProfile); } else if (normalizedProfile.id === DEFAULT_PROFILE_ID) { nextProfiles.unshift(normalizedProfile); } else { nextProfiles.push(normalizedProfile); } normalizedState[taskType] = { activeProfileId: options.setActive === false ? bucket.activeProfileId : normalizedProfile.id, profiles: nextProfiles.map((item, index) => normalizeTaskProfile(taskType, { ...item, blocks: Array.isArray(item.blocks) ? item.blocks.map((block, blockIndex) => ({ ...block, order: Number.isFinite(Number(block?.order)) ? Number(block.order) : blockIndex, })) : [], builtin: item.id === DEFAULT_PROFILE_ID ? true : item.builtin, updatedAt: item.id === normalizedProfile.id ? normalizedProfile.updatedAt : item.updatedAt, }), ), }; return normalizedState; } export function setActiveTaskProfileId(taskProfiles = {}, taskType, profileId) { const normalizedState = normalizeTaskProfilesState(taskProfiles); const bucket = normalizedState[taskType]; if (!bucket?.profiles?.some((profile) => profile.id === profileId)) { return normalizedState; } normalizedState[taskType] = { ...bucket, activeProfileId: profileId, }; return normalizedState; } export function deleteTaskProfile(taskProfiles = {}, taskType, profileId) { if (!profileId) return normalizeTaskProfilesState(taskProfiles); const normalizedState = normalizeTaskProfilesState(taskProfiles); const bucket = normalizedState[taskType]; if (!bucket?.profiles?.length) { return normalizedState; } const remaining = bucket.profiles.filter((profile) => profile.id !== profileId); if (remaining.length === 0) { normalizedState[taskType] = { activeProfileId: DEFAULT_PROFILE_ID, profiles: [createDefaultTaskProfile(taskType)], }; return normalizedState; } normalizedState[taskType] = { activeProfileId: remaining.some( (profile) => profile.id === bucket.activeProfileId, ) ? bucket.activeProfileId : remaining[0].id, profiles: remaining, }; return normalizedState; } export function restoreDefaultTaskProfile(taskProfiles = {}, taskType) { const normalizedState = normalizeTaskProfilesState(taskProfiles); const bucket = normalizedState[taskType] || { activeProfileId: DEFAULT_PROFILE_ID, profiles: [], }; const defaultProfile = createDefaultTaskProfile(taskType); const remaining = (bucket.profiles || []).filter( (profile) => profile.id !== DEFAULT_PROFILE_ID, ); normalizedState[taskType] = { activeProfileId: DEFAULT_PROFILE_ID, profiles: [defaultProfile, ...remaining], }; return normalizedState; } export function exportTaskProfile(taskProfiles = {}, taskType, profileId = "") { const normalizedState = normalizeTaskProfilesState(taskProfiles); const bucket = normalizedState[taskType]; const profile = bucket?.profiles?.find((item) => item.id === profileId) || bucket?.profiles?.[0]; if (!profile) { throw new Error(`Task profile not found: ${taskType}/${profileId}`); } return { format: "st-bme-task-profile", version: DEFAULT_TASK_PROFILE_VERSION, taskType, exportedAt: nowIso(), profile: cloneJson(profile), }; } export function importTaskProfile( taskProfiles = {}, rawInput, preferredTaskType = "", ) { const parsed = typeof rawInput === "string" ? JSON.parse(rawInput) : cloneJson(rawInput); const candidate = parsed?.profile && typeof parsed.profile === "object" ? parsed.profile : parsed; const importedTaskType = String( preferredTaskType || parsed?.taskType || candidate?.taskType || "", ).trim(); if (!TASK_TYPES.includes(importedTaskType)) { throw new Error(`Unsupported task type: ${importedTaskType || "(empty)"}`); } const bucket = normalizeTaskProfilesState(taskProfiles)[importedTaskType]; const baseName = String(candidate?.name || "").trim() || "导入预设"; const importedProfile = normalizeTaskProfile(importedTaskType, { ...candidate, id: createProfileId(importedTaskType), taskType: importedTaskType, name: baseName, builtin: false, updatedAt: nowIso(), metadata: { ...(candidate?.metadata || {}), importedAt: nowIso(), }, blocks: Array.isArray(candidate?.blocks) && candidate.blocks.length > 0 ? candidate.blocks.map((block, index) => ({ ...block, id: createPromptBlockId(importedTaskType), order: index, })) : createDefaultTaskProfile(importedTaskType).blocks, regex: { ...(candidate?.regex || {}), localRules: Array.isArray(candidate?.regex?.localRules) ? candidate.regex.localRules.map((rule) => ({ ...rule, id: createRegexRuleId(importedTaskType), })) : [], }, }); const nextTaskProfiles = upsertTaskProfile( { ...normalizeTaskProfilesState(taskProfiles), [importedTaskType]: bucket, }, importedTaskType, importedProfile, { setActive: true }, ); return { taskProfiles: nextTaskProfiles, taskType: importedTaskType, profile: importedProfile, }; }