From 3533aeab18b47f4e771ff03957375c3d7690a00a Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:06:29 +0800 Subject: [PATCH] Add global task regex migration and UI --- index.js | 6 + prompting/prompt-profiles.js | 214 ++++++ prompting/task-regex.js | 36 +- runtime/settings-defaults.js | 6 +- tests/helpers/generation-recall-harness.mjs | 4 + ui/panel.js | 726 ++++++++++++++++---- 6 files changed, 859 insertions(+), 133 deletions(-) diff --git a/index.js b/index.js index d4b5187..309b1da 100644 --- a/index.js +++ b/index.js @@ -146,6 +146,7 @@ import { } from "./ui/panel-bridge.js"; import { migrateLegacyTaskProfiles, + migratePerTaskRegexToGlobal, } from "./prompting/prompt-profiles.js"; import { inspectTaskRegexReuse } from "./prompting/task-regex.js"; import { @@ -2885,6 +2886,11 @@ function getSettings() { const migrated = migrateLegacyTaskProfiles(mergedSettings); mergedSettings.taskProfilesVersion = migrated.taskProfilesVersion; mergedSettings.taskProfiles = migrated.taskProfiles; + const regexMigration = migratePerTaskRegexToGlobal(mergedSettings); + if (regexMigration.changed) { + mergedSettings.globalTaskRegex = regexMigration.settings.globalTaskRegex; + mergedSettings.taskProfiles = regexMigration.settings.taskProfiles; + } extension_settings[MODULE_NAME] = mergedSettings; globalThis.__stBmeDebugLoggingEnabled = Boolean( mergedSettings.debugLoggingEnabled, diff --git a/prompting/prompt-profiles.js b/prompting/prompt-profiles.js index d3f8f69..96c45c2 100644 --- a/prompting/prompt-profiles.js +++ b/prompting/prompt-profiles.js @@ -806,6 +806,71 @@ export function normalizeTaskRegexStages(stages = {}) { return normalized; } +export function createDefaultGlobalTaskRegex() { + return { + enabled: true, + inheritStRegex: true, + sources: { + global: true, + preset: true, + character: true, + }, + stages: normalizeTaskRegexStages(DEFAULT_TASK_REGEX_STAGES), + localRules: [], + }; +} + +export function dedupeRegexRules(rules = [], taskType = "task") { + const sourceRules = Array.isArray(rules) ? rules : []; + const deduped = []; + const seen = new Set(); + + for (let index = 0; index < sourceRules.length; index++) { + const normalized = normalizeRegexLocalRule(sourceRules[index], taskType, index); + const key = JSON.stringify({ + enabled: normalized.enabled !== false, + find_regex: normalized.find_regex, + replace_string: normalized.replace_string, + trim_strings: normalized.trim_strings, + source: { + user_input: normalized.source?.user_input !== false, + ai_output: normalized.source?.ai_output !== false, + }, + destination: { + prompt: normalized.destination?.prompt !== false, + display: Boolean(normalized.destination?.display), + }, + min_depth: normalized.min_depth, + max_depth: normalized.max_depth, + }); + if (seen.has(key)) continue; + seen.add(key); + deduped.push(normalized); + } + + return deduped; +} + +export function normalizeGlobalTaskRegex(config = {}, taskType = "global") { + const defaults = createDefaultGlobalTaskRegex(); + const source = + config && typeof config === "object" && !Array.isArray(config) ? config : {}; + + return { + enabled: source.enabled !== false, + inheritStRegex: source.inheritStRegex !== false, + sources: { + ...defaults.sources, + ...(source.sources && typeof source.sources === "object" ? source.sources : {}), + }, + stages: { + ...normalizeTaskRegexStages(defaults.stages), + ...normalizeTaskRegexStages(source.stages || {}), + }, + localRules: dedupeRegexRules(source.localRules, taskType), + }; +} + export function isTaskRegexStageEnabled(stages = {}, stageKey = "") { const normalizedStages = normalizeTaskRegexStages(stages); const normalizedStageKey = normalizeRegexStageKey(stageKey); @@ -829,6 +894,20 @@ export function isTaskRegexStageEnabled(stages = {}, stageKey = "") { return normalizedStages[normalizedStageKey] !== false; } +function buildRegexConfigSignature(config = {}, taskType = "global") { + const normalized = normalizeGlobalTaskRegex(config, taskType); + return JSON.stringify({ + enabled: normalized.enabled !== false, + inheritStRegex: normalized.inheritStRegex !== false, + sources: { + global: normalized.sources?.global !== false, + preset: normalized.sources?.preset !== false, + character: normalized.sources?.character !== false, + }, + stages: normalizeTaskRegexStages(normalized.stages || {}), + }); +} + function normalizeTaskProfilesState(taskProfiles = {}) { return ensureTaskProfiles({ taskProfiles }); } @@ -1358,6 +1437,141 @@ export function migrateLegacyTaskProfiles(settings = {}) { }; } +export function migratePerTaskRegexToGlobal(settings = {}) { + const taskProfiles = ensureTaskProfiles(settings); + const defaultGlobalRegex = normalizeGlobalTaskRegex( + createDefaultGlobalTaskRegex(), + "global", + ); + const existingGlobalRegex = normalizeGlobalTaskRegex( + settings.globalTaskRegex || {}, + "global", + ); + const existingGlobalConfigSignature = buildRegexConfigSignature( + existingGlobalRegex, + "global", + ); + const defaultGlobalConfigSignature = buildRegexConfigSignature( + defaultGlobalRegex, + "global", + ); + const profilesWithLegacyRegex = []; + + for (const taskType of TASK_TYPES) { + const bucket = taskProfiles[taskType]; + const defaultProfileRegex = normalizeGlobalTaskRegex( + createDefaultTaskProfile(taskType).regex || {}, + taskType, + ); + const defaultProfileConfigSignature = buildRegexConfigSignature( + defaultProfileRegex, + taskType, + ); + + for (const profile of Array.isArray(bucket?.profiles) ? bucket.profiles : []) { + const normalizedProfileRegex = normalizeGlobalTaskRegex( + profile?.regex || {}, + taskType, + ); + const profileConfigSignature = buildRegexConfigSignature( + normalizedProfileRegex, + taskType, + ); + const hasRules = normalizedProfileRegex.localRules.length > 0; + const hasConfigDiff = profileConfigSignature !== defaultProfileConfigSignature; + if (!hasRules && !hasConfigDiff) continue; + profilesWithLegacyRegex.push({ + taskType, + profileId: String(profile?.id || ""), + regex: normalizedProfileRegex, + configSignature: profileConfigSignature, + hasConfigDiff, + }); + } + } + + if (profilesWithLegacyRegex.length === 0) { + return { + changed: false, + settings: { + ...settings, + taskProfiles, + }, + }; + } + + const configCandidates = profilesWithLegacyRegex.filter( + (item) => item.hasConfigDiff, + ); + const uniqueCandidateSignatures = [ + ...new Set(configCandidates.map((item) => item.configSignature)), + ]; + if (uniqueCandidateSignatures.length > 1) { + console.warn( + "[ST-BME] 检测到多个任务预设存在冲突的旧正则配置,已按顺序采用第一份并统一迁移。", + configCandidates.map((item) => ({ + taskType: item.taskType, + profileId: item.profileId, + })), + ); + } + + const selectedConfig = + existingGlobalConfigSignature !== defaultGlobalConfigSignature + ? existingGlobalRegex + : configCandidates[0]?.regex || defaultGlobalRegex; + + const mergedLocalRules = dedupeRegexRules( + [ + ...(Array.isArray(existingGlobalRegex.localRules) + ? existingGlobalRegex.localRules + : []), + ...profilesWithLegacyRegex.flatMap((item) => + Array.isArray(item.regex?.localRules) ? item.regex.localRules : [], + ), + ], + "global", + ); + + const nextGlobalRegex = { + ...normalizeGlobalTaskRegex(selectedConfig, "global"), + localRules: mergedLocalRules, + }; + + const nextTaskProfiles = {}; + for (const taskType of TASK_TYPES) { + const bucket = taskProfiles[taskType] || { + activeProfileId: DEFAULT_PROFILE_ID, + profiles: [createDefaultTaskProfile(taskType)], + }; + const legacyProfileIds = new Set( + profilesWithLegacyRegex + .filter((item) => item.taskType === taskType) + .map((item) => item.profileId), + ); + nextTaskProfiles[taskType] = { + ...bucket, + profiles: (Array.isArray(bucket.profiles) ? bucket.profiles : []).map((profile) => + legacyProfileIds.has(String(profile?.id || "")) + ? normalizeTaskProfile(taskType, { + ...profile, + regex: {}, + }) + : normalizeTaskProfile(taskType, profile), + ), + }; + } + + return { + changed: true, + settings: { + ...settings, + globalTaskRegex: nextGlobalRegex, + taskProfiles: nextTaskProfiles, + }, + }; +} + export function getActiveTaskProfile(settings = {}, taskType) { const taskProfiles = ensureTaskProfiles(settings); const bucket = taskProfiles?.[taskType]; diff --git a/prompting/task-regex.js b/prompting/task-regex.js index 8242946..16749a7 100644 --- a/prompting/task-regex.js +++ b/prompting/task-regex.js @@ -8,6 +8,7 @@ import { getHostAdapter } from "../host/adapter/index.js"; import { getActiveTaskProfile, isTaskRegexStageEnabled, + normalizeGlobalTaskRegex, normalizeTaskRegexStages, } from "./prompt-profiles.js"; @@ -1026,6 +1027,29 @@ function applyHostRegexReuseFallback( }; } +function resolveTaskRegexConfig(settings = {}, taskType = "") { + const hasGlobalRegex = + settings?.globalTaskRegex && + typeof settings.globalTaskRegex === "object" && + !Array.isArray(settings.globalTaskRegex); + + if (hasGlobalRegex) { + return { + profile: null, + regexConfig: normalizeGlobalTaskRegex( + settings.globalTaskRegex || {}, + "global", + ), + }; + } + + const profile = getActiveTaskProfile(settings, taskType); + return { + profile, + regexConfig: normalizeGlobalTaskRegex(profile?.regex || {}, taskType || "task"), + }; +} + export function applyHostRegexReuse( settings = {}, taskType, @@ -1041,8 +1065,7 @@ export function applyHostRegexReuse( const normalizedTaskType = String(taskType || "").trim(); const normalizedSourceType = normalizeHostRegexSourceType(sourceType); const normalizedFormatterOptions = normalizeHostFormatterOptions(formatterOptions); - const profile = getActiveTaskProfile(settings, normalizedTaskType); - const regexConfig = profile?.regex || {}; + const { regexConfig } = resolveTaskRegexConfig(settings, taskType); const regexHost = getRegexHost(); const executionState = buildHostRegexExecutionState(regexHost); @@ -1193,8 +1216,7 @@ export function applyTaskRegex( debugCollector = null, role = "system", ) { - const profile = getActiveTaskProfile(settings, taskType); - const regexConfig = profile?.regex || {}; + const { regexConfig } = resolveTaskRegexConfig(settings, taskType); const input = typeof text === "string" ? text : ""; if (!regexConfig.enabled) { @@ -1252,8 +1274,7 @@ export function applyTaskRegex( } export function inspectTaskRegexReuse(settings = {}, taskType = "") { - const profile = getActiveTaskProfile(settings, taskType); - const regexConfig = profile?.regex || {}; + const { profile, regexConfig } = resolveTaskRegexConfig(settings, taskType); const detailed = collectTavernRulesDetailed(regexConfig); const stageConfig = normalizeTaskRegexStages(regexConfig.stages || {}); const localRules = collectLocalRules(regexConfig); @@ -1292,3 +1313,6 @@ export function inspectTaskRegexReuse(settings = {}, taskType = "") { ), }; } + + + diff --git a/runtime/settings-defaults.js b/runtime/settings-defaults.js index 970399f..3c994f7 100644 --- a/runtime/settings-defaults.js +++ b/runtime/settings-defaults.js @@ -1,4 +1,7 @@ -import { createDefaultTaskProfiles } from "../prompting/prompt-profiles.js"; +import { + createDefaultGlobalTaskRegex, + createDefaultTaskProfiles, +} from "../prompting/prompt-profiles.js"; function clampIntValue(value, fallback = 0, min = 0, max = 9999) { const numeric = Number(value); @@ -110,6 +113,7 @@ export const defaultSettings = { reflectionPrompt: "", taskProfilesVersion: 3, taskProfiles: createDefaultTaskProfiles(), + globalTaskRegex: createDefaultGlobalTaskRegex(), // ====== v2 增强设置 ====== enableConsolidation: true, diff --git a/tests/helpers/generation-recall-harness.mjs b/tests/helpers/generation-recall-harness.mjs index 560ca0c..a4b37fa 100644 --- a/tests/helpers/generation-recall-harness.mjs +++ b/tests/helpers/generation-recall-harness.mjs @@ -112,6 +112,10 @@ export function createGenerationRecallHarness(options = {}) { taskProfilesVersion: settings?.taskProfilesVersion || 0, taskProfiles: settings?.taskProfiles || {}, }), + migratePerTaskRegexToGlobal: (settings = {}) => ({ + changed: false, + settings, + }), refreshPanelLiveStateController: () => { context.refreshPanelCalls += 1; }, diff --git a/ui/panel.js b/ui/panel.js index 81e38d1..3a8946d 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -20,10 +20,12 @@ import { } from "../llm/llm-preset-utils.js"; import { cloneTaskProfile, + createDefaultGlobalTaskRegex, createBuiltinPromptBlock, createCustomPromptBlock, createLocalRegexRule, DEFAULT_TASK_BLOCKS, + dedupeRegexRules, ensureTaskProfiles, exportTaskProfile as serializeTaskProfile, getBuiltinBlockDefinitions, @@ -31,6 +33,7 @@ import { getTaskTypeOptions, importTaskProfile as parseImportedTaskProfile, isTaskRegexStageEnabled, + normalizeGlobalTaskRegex, normalizeTaskRegexStages, restoreDefaultTaskProfile, setActiveTaskProfileId, @@ -67,7 +70,6 @@ function getDefaultPromptText(taskType = "") { const TASK_PROFILE_TABS = [ { id: "generation", label: "生成参数" }, { id: "prompt", label: "Prompt 编排" }, - { id: "regex", label: "正则" }, { id: "debug", label: "调试预览" }, ]; @@ -226,6 +228,8 @@ let currentTaskProfileTaskType = "extract"; let currentTaskProfileTabId = "generation"; let currentTaskProfileBlockId = ""; let currentTaskProfileRuleId = ""; +let showGlobalRegexPanel = false; +let currentGlobalRegexRuleId = ""; let currentCognitionOwnerKey = ""; let currentGraphView = "graph"; let fetchedMemoryLLMModels = []; @@ -4784,16 +4788,58 @@ function _bindTaskProfileWorkspace() { try { const text = await file.text(); const settings = _getSettings?.() || {}; - const imported = parseImportedTaskProfile( - settings.taskProfiles || {}, - text, + const parsed = JSON.parse(text); + let nextGlobalTaskRegex = _normalizeGlobalRegexDraft( + settings.globalTaskRegex || {}, ); + const importedGlobalMerge = _mergeImportedGlobalRegex( + nextGlobalTaskRegex, + parsed?.globalTaskRegex, + ); + nextGlobalTaskRegex = importedGlobalMerge.globalTaskRegex; + let imported = parseImportedTaskProfile( + settings.taskProfiles || {}, + parsed, + ); + const legacyRuleMerge = _mergeProfileRegexRulesIntoGlobal( + nextGlobalTaskRegex, + imported.profile, + ); + nextGlobalTaskRegex = legacyRuleMerge.globalTaskRegex; + if (legacyRuleMerge.clearedLegacyRules) { + imported = { + ...imported, + profile: legacyRuleMerge.profile, + taskProfiles: upsertTaskProfile( + imported.taskProfiles, + imported.taskType, + legacyRuleMerge.profile, + { setActive: true }, + ), + }; + } currentTaskProfileTaskType = imported.taskType || currentTaskProfileTaskType; currentTaskProfileBlockId = imported.profile?.blocks?.[0]?.id || ""; currentTaskProfileRuleId = imported.profile?.regex?.localRules?.[0]?.id || ""; - _patchTaskProfiles(imported.taskProfiles); - toastr.success("预设导入成功", "ST-BME"); + _patchSettings( + { + taskProfilesVersion: 3, + taskProfiles: imported.taskProfiles, + globalTaskRegex: nextGlobalTaskRegex, + }, + { + refreshTaskWorkspace: true, + }, + ); + const mergedRuleCount = + importedGlobalMerge.mergedRuleCount + legacyRuleMerge.mergedRuleCount; + toastr.success( + mergedRuleCount > 0 + ? `预设导入成功,${mergedRuleCount} 条正则规则已合并到通用正则规则` + : "预设导入成功", + "ST-BME", + ); } catch (error) { console.error("[ST-BME] 导入任务预设失败:", error); toastr.error(`预设导入失败: ${error?.message || error}`, "ST-BME"); @@ -4817,14 +4863,41 @@ function _bindTaskProfileWorkspace() { } const settings = _getSettings?.() || {}; let mergedProfiles = settings.taskProfiles || {}; + let nextGlobalTaskRegex = _normalizeGlobalRegexDraft( + settings.globalTaskRegex || {}, + ); + const importedGlobalMerge = _mergeImportedGlobalRegex( + nextGlobalTaskRegex, + parsed?.globalTaskRegex, + ); + nextGlobalTaskRegex = importedGlobalMerge.globalTaskRegex; let importedCount = 0; + let mergedLegacyRuleCount = 0; for (const [taskType, entry] of Object.entries(parsed.profiles)) { try { - const imported = parseImportedTaskProfile( + let imported = parseImportedTaskProfile( mergedProfiles, entry, taskType, ); + const legacyRuleMerge = _mergeProfileRegexRulesIntoGlobal( + nextGlobalTaskRegex, + imported.profile, + ); + nextGlobalTaskRegex = legacyRuleMerge.globalTaskRegex; + mergedLegacyRuleCount += legacyRuleMerge.mergedRuleCount; + if (legacyRuleMerge.clearedLegacyRules) { + imported = { + ...imported, + profile: legacyRuleMerge.profile, + taskProfiles: upsertTaskProfile( + imported.taskProfiles, + imported.taskType, + legacyRuleMerge.profile, + { setActive: true }, + ), + }; + } mergedProfiles = imported.taskProfiles; importedCount++; } catch (innerError) { @@ -4835,8 +4908,24 @@ function _bindTaskProfileWorkspace() { toastr.warning("没有成功导入任何预设", "ST-BME"); return; } - _patchTaskProfiles(mergedProfiles); - toastr.success(`已导入 ${importedCount} 个任务预设`, "ST-BME"); + _patchSettings( + { + taskProfilesVersion: 3, + taskProfiles: mergedProfiles, + globalTaskRegex: nextGlobalTaskRegex, + }, + { + refreshTaskWorkspace: true, + }, + ); + const mergedRuleCount = + importedGlobalMerge.mergedRuleCount + mergedLegacyRuleCount; + toastr.success( + mergedRuleCount > 0 + ? `已导入 ${importedCount} 个任务预设,并合并 ${mergedRuleCount} 条通用正则规则` + : `已导入 ${importedCount} 个任务预设`, + "ST-BME", + ); } catch (error) { console.error("[ST-BME] 导入全部预设失败:", error); toastr.error(`导入全部预设失败: ${error?.message || error}`, "ST-BME"); @@ -4851,6 +4940,7 @@ function _bindTaskProfileWorkspace() { function _handleTaskProfileWorkspaceInput(event) { const target = event.target; if (!(target instanceof HTMLElement)) return; + const isGlobalRegexPanel = _isGlobalRegexPanelTarget(target); if (target.matches("[data-block-field]")) { _persistSelectedBlockField(target, false); @@ -4880,13 +4970,18 @@ function _handleTaskProfileWorkspaceInput(event) { target.matches("[data-regex-rule-source]") || target.matches("[data-regex-rule-destination]") ) { - _persistSelectedRegexRuleField(target, false); + if (isGlobalRegexPanel) { + _persistSelectedGlobalRegexRuleField(target, false); + } else { + _persistSelectedRegexRuleField(target, false); + } } } function _handleTaskProfileWorkspaceChange(event) { const target = event.target; if (!(target instanceof HTMLElement)) return; + const isGlobalRegexPanel = _isGlobalRegexPanelTarget(target); if (target.id === "bme-task-profile-select") { const settings = _getSettings?.() || {}; @@ -4912,17 +5007,29 @@ function _handleTaskProfileWorkspaceChange(event) { } if (target.matches("[data-regex-field]")) { - _persistRegexConfigField(target, false); + if (isGlobalRegexPanel) { + _persistGlobalRegexField(target, false); + } else { + _persistRegexConfigField(target, false); + } return; } if (target.matches("[data-regex-source]")) { - _persistRegexSourceField(target, false); + if (isGlobalRegexPanel) { + _persistGlobalRegexSourceField(target, false); + } else { + _persistRegexSourceField(target, false); + } return; } if (target.matches("[data-regex-stage]")) { - _persistRegexStageField(target, false); + if (isGlobalRegexPanel) { + _persistGlobalRegexStageField(target, false); + } else { + _persistRegexStageField(target, false); + } return; } @@ -4931,12 +5038,20 @@ function _handleTaskProfileWorkspaceChange(event) { target.matches("[data-regex-rule-source]") || target.matches("[data-regex-rule-destination]") ) { - _persistSelectedRegexRuleField(target, true); + if (isGlobalRegexPanel) { + _persistSelectedGlobalRegexRuleField(target, true); + } else { + _persistSelectedRegexRuleField(target, true); + } } } function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) { const taskProfiles = ensureTaskProfiles(settings); + const globalTaskRegex = _normalizeGlobalRegexDraft(settings.globalTaskRegex || {}); + const globalRegexRules = Array.isArray(globalTaskRegex.localRules) + ? globalTaskRegex.localRules + : []; const taskTypeOptions = getTaskTypeOptions(); const runtimeDebug = _getRuntimeDebugSnapshot?.() || { hostCapabilities: null, @@ -4970,10 +5085,16 @@ function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) { if (!regexRules.some((rule) => rule.id === currentTaskProfileRuleId)) { currentTaskProfileRuleId = regexRules[0]?.id || ""; } + if (!globalRegexRules.some((rule) => rule.id === currentGlobalRegexRuleId)) { + currentGlobalRegexRuleId = globalRegexRules[0]?.id || ""; + } return { settings, taskProfiles, + globalTaskRegex, + globalRegexRules, + showGlobalRegex: showGlobalRegexPanel, taskTypeOptions, taskType: currentTaskProfileTaskType, taskTabId: currentTaskProfileTabId, @@ -4985,6 +5106,8 @@ function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) { regexRules, selectedRule: regexRules.find((rule) => rule.id === currentTaskProfileRuleId) || null, + selectedGlobalRegexRule: + globalRegexRules.find((rule) => rule.id === currentGlobalRegexRuleId) || null, builtinBlockDefinitions: getBuiltinBlockDefinitions(), runtimeDebug, }; @@ -5636,6 +5759,10 @@ async function _handleTaskProfileWorkspaceClick(event) { currentTaskProfileRuleId = ""; _refreshTaskProfileWorkspace(); return; + case "toggle-global-regex": + showGlobalRegexPanel = !showGlobalRegexPanel; + _refreshTaskProfileWorkspace(); + return; case "switch-task-tab": currentTaskProfileTabId = actionEl.dataset.taskTab || currentTaskProfileTabId; @@ -5655,7 +5782,11 @@ async function _handleTaskProfileWorkspaceClick(event) { _refreshTaskProfileWorkspace(); return; case "select-regex-rule": - currentTaskProfileRuleId = actionEl.dataset.ruleId || ""; + if (_isGlobalRegexPanelTarget(actionEl)) { + currentGlobalRegexRuleId = actionEl.dataset.ruleId || ""; + } else { + currentTaskProfileRuleId = actionEl.dataset.ruleId || ""; + } _refreshTaskProfileWorkspace(); return; case "add-custom-block": @@ -5748,20 +5879,25 @@ async function _handleTaskProfileWorkspaceClick(event) { return; } case "export-profile": - _downloadTaskProfile(state.taskProfiles, currentTaskProfileTaskType, selectedProfile); + _downloadTaskProfile( + state.taskProfiles, + currentTaskProfileTaskType, + selectedProfile, + state.globalTaskRegex, + ); return; case "import-profile": document.getElementById("bme-task-profile-import")?.click(); return; case "export-all-profiles": - _downloadAllTaskProfiles(state.taskProfiles); + _downloadAllTaskProfiles(state.taskProfiles, state.globalTaskRegex); return; case "import-all-profiles": document.getElementById("bme-task-profile-import-all")?.click(); return; case "restore-all-profiles": { const confirmed = window.confirm( - "这会将全部 6 个任务的默认预设恢复为出厂状态。已保存的自定义预设不受影响。是否继续?", + "这会将全部 6 个任务的默认预设恢复为出厂状态。已保存的自定义预设不受影响,通用正则规则也不受影响。是否继续?", ); if (!confirmed) return; const taskTypes = getTaskTypeOptions().map((t) => t.id); @@ -5815,6 +5951,33 @@ async function _handleTaskProfileWorkspaceClick(event) { case "delete-regex-rule": _deleteRegexRule(actionEl.dataset.ruleId); return; + case "add-global-regex-rule": + _updateGlobalTaskRegex((draft) => { + const localRules = Array.isArray(draft.localRules) ? draft.localRules : []; + const nextRule = createLocalRegexRule("global", { + script_name: `通用规则 ${localRules.length + 1}`, + }); + draft.localRules = [...localRules, nextRule]; + return { selectRuleId: nextRule.id }; + }); + return; + case "delete-global-regex-rule": + _deleteGlobalRegexRule(actionEl.dataset.ruleId); + return; + case "select-global-regex-rule": + currentGlobalRegexRuleId = actionEl.dataset.ruleId || ""; + _refreshTaskProfileWorkspace(); + return; + case "restore-global-regex-defaults": { + const confirmed = window.confirm( + "这会将通用正则规则恢复为默认配置。是否继续?", + ); + if (!confirmed) return; + currentGlobalRegexRuleId = ""; + _patchGlobalTaskRegex(createDefaultGlobalTaskRegex(), { refresh: true }); + toastr.success("通用正则规则已恢复默认", "ST-BME"); + return; + } default: return; } @@ -5853,6 +6016,14 @@ function _renderTaskProfileWorkspace(state) { .join("")}
+ @@ -5865,6 +6036,8 @@ function _renderTaskProfileWorkspace(state) {
+ ${state.showGlobalRegex ? _renderGlobalRegexPanel(state) : ""} +
@@ -5921,11 +6094,9 @@ function _renderTaskProfileWorkspace(state) { ${ state.taskTabId === "generation" ? _renderTaskGenerationTab(state) - : state.taskTabId === "regex" - ? _renderTaskRegexTab(state) - : state.taskTabId === "debug" - ? _renderTaskDebugTab(state) - : _renderTaskPromptTab(state) + : state.taskTabId === "debug" + ? _renderTaskDebugTab(state) + : _renderTaskPromptTab(state) }
@@ -5933,7 +6104,6 @@ function _renderTaskProfileWorkspace(state) {
`; } - function _renderTaskPromptTab(state) { return `
@@ -6026,85 +6196,116 @@ function _renderTaskGenerationTab(state) { `; } -function _renderTaskRegexTab(state) { - const regex = state.profile.regex || {}; +function _renderTaskRegexTab(state, options = {}) { + const regex = options.regex || state.profile?.regex || {}; + const regexRules = Array.isArray(options.regexRules) + ? options.regexRules + : state.regexRules; + const selectedRule = + options.selectedRule === undefined ? state.selectedRule : options.selectedRule; const normalizedStages = normalizeTaskRegexStages(regex.stages || {}); + const selectAction = options.selectAction || "select-regex-rule"; + const deleteAction = options.deleteAction || "delete-regex-rule"; + const addAction = options.addAction || "add-regex-rule"; + const addButtonLabel = options.addButtonLabel || "+ 新增规则"; + const wrapperClassName = options.wrapperClassName + ? ` ${options.wrapperClassName}` + : ""; + const sectionTitle = options.sectionTitle || "复用与阶段"; + const sectionSubtitle = + options.sectionSubtitle || + "任务预设可复用酒馆正则,并叠加当前任务自己的附加规则。"; + const rulesTitle = options.rulesTitle || "本地附加规则"; + const rulesSubtitle = + options.rulesSubtitle || + "本地规则只作用于当前任务预设,不会污染宿主酒馆配置。"; + const emptyText = options.emptyText || "当前预设还没有本地正则规则。"; + const defaultNamePrefix = options.defaultNamePrefix || "本地规则"; + const headerExtraActions = options.extraHeaderActions || ""; + const editorState = { + ...state, + selectedRule, + }; + return ` -
+
-
-
-
-
复用与阶段
-
- 任务预设可复用酒馆正则,并叠加当前任务自己的附加规则。 +
+
+
+
${_escHtml(sectionTitle)}
+
+ ${_escHtml(sectionSubtitle)} +
+
+
+ + ${headerExtraActions}
- -
-
- +
+ - -
+ +
- -
- ${[ - ["global", "全局"], - ["preset", "当前预设"], - ["character", "角色卡"], - ] - .map( - ([key, label]) => ` + +
+ ${[ + ["global", "全局"], + ["preset", "当前预设"], + ["character", "角色卡"], + ] + .map( + ([key, label]) => ` + + `, + ) + .join("")} +
+ + +
+ ${TASK_PROFILE_REGEX_STAGES.map( + (stage) => ` - `, - ) - .join("")} -
- - -
- ${TASK_PROFILE_REGEX_STAGES.map( - (stage) => ` -
-
- -
-
-
-
本地附加规则
-
- 本地规则只作用于当前任务预设,不会污染宿主酒馆配置。 -
-
-
- ${state.regexRules.length - ? state.regexRules - .map((rule, index) => _renderRegexRuleListItem(rule, index, state)) - .join("") - : ` -
- 当前预设还没有本地正则规则。 -
- `} +
+
+
+
${_escHtml(rulesTitle)}
+
+ ${_escHtml(rulesSubtitle)} +
+
+ +
+ +
+ ${regexRules.length + ? regexRules + .map((rule, index) => + _renderRegexRuleListItem(rule, index, editorState, { + selectAction, + deleteAction, + defaultNamePrefix, + }) + ) + .join("") + : ` +
+ ${_escHtml(emptyText)} +
+ `} +
-
- ${_renderRegexRuleEditor(state)} + ${_renderRegexRuleEditor(editorState)}
`; } +function _renderGlobalRegexPanel(state) { + return _renderTaskRegexTab( + { + ...state, + selectedRule: state.selectedGlobalRegexRule, + }, + { + regex: state.globalTaskRegex, + regexRules: state.globalRegexRules, + selectedRule: state.selectedGlobalRegexRule, + addAction: "add-global-regex-rule", + selectAction: "select-global-regex-rule", + deleteAction: "delete-global-regex-rule", + addButtonLabel: "+ 新增通用规则", + wrapperClassName: "bme-global-regex-panel", + sectionTitle: "通用正则设置", + sectionSubtitle: "所有任务共享同一套任务正则开关、复用来源、执行阶段与附加规则。", + rulesTitle: "通用附加规则", + rulesSubtitle: "这里维护所有任务共享的附加规则。", + emptyText: "当前还没有通用正则规则。", + defaultNamePrefix: "通用规则", + extraHeaderActions: ` + + `, + }, + ); +} function _formatRegexReuseSourceState(source = {}) { const states = []; states.push(source.enabled ? "已启用" : "已关闭"); @@ -7238,19 +7474,23 @@ function _renderGenerationField(field, value, state = {}) { `; } -function _renderRegexRuleListItem(rule, index, state) { +function _renderRegexRuleListItem(rule, index, state, options = {}) { const isSelected = rule.id === state.selectedRule?.id; + const selectAction = options.selectAction || "select-regex-rule"; + const deleteAction = options.deleteAction || "delete-regex-rule"; + const defaultNamePrefix = options.defaultNamePrefix || "本地规则"; + return `
`; } - function _renderRegexRuleEditor(state) { const rule = state.selectedRule; if (!rule) { @@ -7627,6 +7866,110 @@ function _persistSelectedRegexRuleField(target, refresh) { ); } +function _deleteGlobalRegexRule(ruleId) { + if (!ruleId) return; + _updateGlobalTaskRegex((draft) => { + const localRules = Array.isArray(draft.localRules) ? [...draft.localRules] : []; + const index = localRules.findIndex((item) => item.id === ruleId); + if (index < 0) return null; + localRules.splice(index, 1); + draft.localRules = localRules; + return { + selectRuleId: + localRules[Math.max(0, index - 1)]?.id || localRules[0]?.id || "", + }; + }); +} + +function _persistGlobalRegexField(target, refresh) { + const key = target.dataset.regexField; + if (!key) return; + + _updateGlobalTaskRegex( + (draft) => { + draft[key] = + target instanceof HTMLInputElement && target.type === "checkbox" + ? Boolean(target.checked) + : target.value; + }, + { refresh }, + ); +} + +function _persistGlobalRegexSourceField(target, refresh) { + const sourceKey = target.dataset.regexSource; + if (!sourceKey) return; + + _updateGlobalTaskRegex( + (draft) => { + draft.sources = { + ...(draft.sources || {}), + [sourceKey]: Boolean(target.checked), + }; + }, + { refresh }, + ); +} + +function _persistGlobalRegexStageField(target, refresh) { + const stageKey = target.dataset.regexStage; + if (!stageKey) return; + + _updateGlobalTaskRegex( + (draft) => { + draft.stages = { + ...(draft.stages || {}), + [stageKey]: Boolean(target.checked), + }; + }, + { refresh }, + ); +} + +function _persistSelectedGlobalRegexRuleField(target, refresh) { + _updateGlobalTaskRegex( + (draft) => { + const localRules = Array.isArray(draft.localRules) ? [...draft.localRules] : []; + const rule = localRules.find((item) => item.id === currentGlobalRegexRuleId); + if (!rule) return null; + + if (target.dataset.regexRuleField) { + const field = target.dataset.regexRuleField; + if (target instanceof HTMLInputElement && target.type === "checkbox") { + rule[field] = Boolean(target.checked); + } else if (["min_depth", "max_depth"].includes(field)) { + const parsed = Number.parseInt(String(target.value || "").trim(), 10); + rule[field] = Number.isFinite(parsed) ? parsed : 0; + } else if (field === "trim_strings") { + rule[field] = String(target.value || ""); + } else { + rule[field] = String(target.value || ""); + } + } + + if (target.dataset.regexRuleSource) { + const sourceKey = target.dataset.regexRuleSource; + rule.source = { + ...(rule.source || {}), + [sourceKey]: Boolean(target.checked), + }; + } + + if (target.dataset.regexRuleDestination) { + const destinationKey = target.dataset.regexRuleDestination; + rule.destination = { + ...(rule.destination || {}), + [destinationKey]: Boolean(target.checked), + }; + } + + draft.localRules = localRules; + return { selectRuleId: rule.id }; + }, + { refresh }, + ); +} + function _updateCurrentTaskProfile(mutator, options = {}) { const settings = _getSettings?.() || {}; const taskProfiles = ensureTaskProfiles(settings); @@ -7751,9 +8094,139 @@ function _parseTaskWorkspaceValue(target, valueType = "text") { return String(target.value || "").trim(); } -function _downloadTaskProfile(taskProfiles, taskType, profile) { +function _isGlobalRegexPanelTarget(target) { + return target instanceof HTMLElement && Boolean(target.closest(".bme-global-regex-panel")); +} + +function _normalizeGlobalRegexDraft(regex = {}) { + const normalized = normalizeGlobalTaskRegex(regex || {}, "global"); + return { + ...normalized, + sources: { + ...(normalized.sources || {}), + }, + stages: { + ...normalizeTaskRegexStages(normalized.stages || {}), + }, + localRules: Array.isArray(normalized.localRules) + ? normalized.localRules.map((rule, index) => + createLocalRegexRule("global", { + ...rule, + id: String(rule?.id || `global-rule-${index + 1}`), + }), + ) + : [], + }; +} + +function _mergeImportedGlobalRegex(currentGlobalRegex = {}, importedGlobalRegex = null) { + const current = _normalizeGlobalRegexDraft(currentGlobalRegex); + if ( + !importedGlobalRegex || + typeof importedGlobalRegex !== "object" || + Array.isArray(importedGlobalRegex) + ) { + return { + globalTaskRegex: current, + mergedRuleCount: 0, + replacedConfig: false, + }; + } + + const imported = _normalizeGlobalRegexDraft(importedGlobalRegex); + const mergedRules = dedupeRegexRules( + [ + ...(Array.isArray(current.localRules) ? current.localRules : []), + ...(Array.isArray(imported.localRules) ? imported.localRules : []), + ], + "global", + ); + + return { + globalTaskRegex: { + ...imported, + localRules: mergedRules, + }, + mergedRuleCount: Math.max( + 0, + mergedRules.length - + (Array.isArray(current.localRules) ? current.localRules.length : 0), + ), + replacedConfig: true, + }; +} + +function _mergeProfileRegexRulesIntoGlobal(currentGlobalRegex = {}, profile = null) { + const current = _normalizeGlobalRegexDraft(currentGlobalRegex); + const legacyRules = Array.isArray(profile?.regex?.localRules) + ? profile.regex.localRules + : []; + if (legacyRules.length === 0) { + return { + globalTaskRegex: current, + mergedRuleCount: 0, + profile, + clearedLegacyRules: false, + }; + } + + const mergedRules = dedupeRegexRules( + [...(current.localRules || []), ...legacyRules], + "global", + ); + return { + globalTaskRegex: { + ...current, + localRules: mergedRules, + }, + mergedRuleCount: Math.max(0, mergedRules.length - current.localRules.length), + profile: { + ...(profile || {}), + regex: {}, + }, + clearedLegacyRules: true, + }; +} + +function _patchGlobalTaskRegex(globalTaskRegex, options = {}) { + return _patchSettings( + { + globalTaskRegex: _normalizeGlobalRegexDraft(globalTaskRegex), + }, + { + refreshTaskWorkspace: options.refresh !== false, + }, + ); +} + +function _updateGlobalTaskRegex(mutator, options = {}) { + const settings = _getSettings?.() || {}; + const draft = _normalizeGlobalRegexDraft(_cloneJson(settings.globalTaskRegex || {})); + const mutationResult = mutator?.(draft, { settings }); + if (mutationResult === null) return null; + + const result = mutationResult || {}; + const nextRegex = _normalizeGlobalRegexDraft(result.globalTaskRegex || draft); + if (Object.prototype.hasOwnProperty.call(result, "selectRuleId")) { + currentGlobalRegexRuleId = result.selectRuleId || ""; + } + + return _patchSettings( + { + globalTaskRegex: nextRegex, + ...(result.extraSettingsPatch || {}), + }, + { + refreshTaskWorkspace: + result.refresh === undefined ? options.refresh !== false : result.refresh, + }, + ); +} + +function _downloadTaskProfile(taskProfiles, taskType, profile, globalTaskRegex = {}) { try { const payload = serializeTaskProfile(taskProfiles, taskType, profile?.id || ""); + payload.globalTaskRegex = _normalizeGlobalRegexDraft(globalTaskRegex || {}); const fileName = _sanitizeFileName( `st-bme-${taskType}-${profile?.name || "profile"}.json`, ); @@ -7774,12 +8247,11 @@ function _downloadTaskProfile(taskProfiles, taskType, profile) { toastr.error(`预设导出失败: ${error?.message || error}`, "ST-BME"); } } - function _sanitizeFileName(fileName = "profile.json") { return String(fileName || "profile.json").replace(/[<>:"/\\|?*\x00-\x1f]/g, "-"); } -function _downloadAllTaskProfiles(taskProfiles) { +function _downloadAllTaskProfiles(taskProfiles, globalTaskRegex = {}) { try { const taskTypes = getTaskTypeOptions().map((t) => t.id); const profiles = {}; @@ -7799,6 +8271,7 @@ function _downloadAllTaskProfiles(taskProfiles) { format: "st-bme-all-task-profiles", version: 1, exportedAt: new Date().toISOString(), + globalTaskRegex: _normalizeGlobalRegexDraft(globalTaskRegex || {}), profiles, }; const blob = new Blob([JSON.stringify(payload, null, 2)], { @@ -7818,7 +8291,6 @@ function _downloadAllTaskProfiles(taskProfiles) { toastr.error(`导出全部预设失败: ${error?.message || error}`, "ST-BME"); } } - function _cloneJson(value) { return JSON.parse(JSON.stringify(value ?? null)); } @@ -8356,3 +8828,5 @@ function _getNodeSnippet(node) { function _isMobile() { return window.innerWidth <= 768; } + +