mingding-snap-modifier 2.13.2 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +75 -27
- package/dist/mingding-modifier.js +7592 -2788
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_14.js +7666 -0
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_14_tavernhelper/345/257/274/345/205/245.json +13 -0
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_14_/350/204/232/346/234/254/346/272/220/347/240/201/344/270/226/347/225/214/344/271/246.json +11 -0
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_14_/351/223/276/346/216/245/347/211/210/344/270/226/347/225/214/344/271/246.json +29 -0
- package/docs/v2.14/350/277/255/344/273/243/350/257/264/346/230/216.md +143 -0
- package/package.json +29 -31
- package/dist/mingding-import.json +0 -13
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_13.js +0 -2862
- package/dist//345/221/275/345/256/232/344/271/213/350/257/227/345/205/250/351/207/217/344/277/256/346/224/271/345/231/250_v2_13_tavernhelper/345/257/274/345/205/245.json +0 -13
- package/docs/v2.13/350/277/255/344/273/243/345/267/256/345/210/206/350/257/264/346/230/216.md +0 -40
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "script",
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"name": "【命定之诗】全量修改器 v2.14",
|
|
5
|
+
"id": "mingding-v214",
|
|
6
|
+
"content": "(() => {\n 'use strict';\n const P = window.parent || window;\n if (P.__MINGDING_MODIFIER__ || P.__MINGDING_MODIFIER_2_14__) return;\n\n const __registry = {};\n const __define = (name, fn) => { __registry[name] = { fn, exports: {}, loaded: false }; };\n const __require = (from, req) => {\n const key = resolveRelative(from, req);\n if (!__registry[key]) throw new Error('未加载模块: ' + key);\n if (!__registry[key].loaded) {\n __registry[key].loaded = true;\n const mod = { exports: {} };\n __registry[key].fn(mod, mod.exports, (r) => __require(key, r));\n __registry[key].exports = mod.exports;\n }\n return __registry[key].exports;\n };\n\n function resolveRelative(from, req) {\n if (req[0] !== '.') return req;\n const base = from.includes('/') ? from.split('/') : [];\n const stack = base.slice(0, -1);\n for (const p of req.split('/')) {\n if (p === '.' || p === '') continue;\n if (p === '..') { stack.pop(); continue; }\n stack.push(p);\n }\n return stack.join('/');\n }\n\n__define('apply/apply-flow', function (module, exports, require) {\nconst contract = require('../contracts');\nconst util = require('../util');\nconst patch = require('../data/patch');\nconst transaction = require('./transaction');\nconst bodyMerge = require('./body-merge');\nconst bodyHistory = require('./body-history');\nconst concurrency = require('./concurrency');\nconst edits = require('./operation-edits');\nasync function verifyHostTarget(host, target) {\n if (!host || !target) return false;\n const targetForVerify = Object.assign({}, target, { fingerprint: undefined, textFingerprint: undefined });\n return await host.verifyTarget(targetForVerify);\n}\nfunction buildPreview(snapshot, changeSet, parsedData) {\n const items = [];\n for (const op of (changeSet && changeSet.mvuOps) || []) {\n const oldValue = op.op === 'insert' ? undefined : getByPointer(snapshot && snapshot.mvuData, '/stat_data' + op.path);\n const actualValue = parsedData ? getByPointer(parsedData, '/stat_data' + op.path) : undefined;\n const intendedValue = op.op === 'move' ? getByPointer(snapshot && snapshot.mvuData, '/stat_data' + op.from)\n : op.op === 'delta' ? Number(oldValue) + Number(op.value) : op.value;\n items.push({\n op: op.op,\n path: op.path,\n oldValue: util.deepClone(oldValue),\n newValue: op.op === 'remove' ? undefined : util.deepClone(intendedValue),\n actualValue: parsedData ? util.deepClone(actualValue) : undefined,\n });\n }\n const aux = (changeSet && changeSet.auxiliary) || {};\n if (aux.date !== null && aux.date !== undefined) {\n items.push({ op: 'replace', path: '/date', oldValue: util.deepClone(snapshot.date), newValue: util.deepClone(aux.date) });\n }\n if (aux.calendar !== null && aux.calendar !== undefined) {\n items.push({ op: 'replace', path: '/calendar_float_store', oldValue: util.deepClone(snapshot.calendar), newValue: util.deepClone(aux.calendar) });\n }\n if (aux.chatVariables !== null && aux.chatVariables !== undefined) {\n items.push({ op: 'replace', path: '/chat_metadata/variables', oldValue: util.deepClone(snapshot.chatVariables), newValue: util.deepClone(aux.chatVariables) });\n }\n return items;\n}\nfunction getByPointer(obj, path) {\n return patch.getPath(obj, patch.parsePointer(path));\n}\nfunction hasAnyChange(changeSet) {\n if (!changeSet) return false;\n if (Array.isArray(changeSet.mvuOps) && changeSet.mvuOps.length > 0) return true;\n const aux = changeSet.auxiliary;\n return !!aux && (aux.date !== null && aux.date !== undefined ||\n aux.calendar !== null && aux.calendar !== undefined ||\n aux.chatVariables !== null && aux.chatVariables !== undefined);\n}\nfunction isArrayAppend(oldValue, newValue) {\n if (!Array.isArray(oldValue) || !Array.isArray(newValue) || newValue.length < oldValue.length) return false;\n for (let i = 0; i < oldValue.length; i++) {\n if (!util.deepEqual(oldValue[i], newValue[i])) return false;\n }\n return true;\n}\nfunction validateOpsForApply(ops, oldStatData) {\n const validOps = new Set(['replace', 'delta', 'insert', 'remove', 'move']);\n for (const op of ops || []) {\n if (!op || !validOps.has(op.op) || typeof op.path !== 'string') {\n return { ok: false, reason: '存在不支持的补丁操作' };\n }\n if (!op.path.startsWith('/') || op.path === '/') {\n return { ok: false, reason: '补丁路径必须是非根 JSON Pointer:' + op.path };\n }\n const pathSegments = patch.parsePointer(op.path);\n if (pathSegments.some((segment) => !util.isSafeKey(segment))) {\n return { ok: false, reason: '补丁路径包含不安全键名:' + op.path };\n }\n if (patch.hasReadonlySegment(pathSegments)) {\n return { ok: false, reason: '改变包含以 _ 开头的只读路径:' + op.path };\n }\n if (op.op === 'move') {\n if (typeof op.from !== 'string' || !op.from.startsWith('/') || op.from === '/') {\n return { ok: false, reason: 'move 缺少有效 from 路径' };\n }\n const fromSegments = patch.parsePointer(op.from);\n if (fromSegments.some((segment) => !util.isSafeKey(segment))) {\n return { ok: false, reason: '移动来源包含不安全键名:' + op.from };\n }\n if (patch.hasReadonlySegment(fromSegments)) {\n return { ok: false, reason: '移动来源包含以 _ 开头的只读路径:' + op.from };\n }\n }\n if (['replace', 'delta', 'insert'].includes(op.op) && !Object.prototype.hasOwnProperty.call(op, 'value')) {\n return { ok: false, reason: op.op + ' 缺少 value' };\n }\n if (Object.prototype.hasOwnProperty.call(op, 'value')) {\n const oldAtPath = patch.getPath(oldStatData, patch.parsePointer(op.path));\n const readonlyMutation = patch.findReadonlyMutation(oldAtPath, op.value, patch.parsePointer(op.path));\n if (readonlyMutation) return { ok: false, reason: '补丁值改变只读字段:' + readonlyMutation };\n }\n if (op.path === '/事件/信号' || op.path.indexOf('/事件/信号/') === 0) {\n const oldSignals = oldStatData && oldStatData['事件'] && oldStatData['事件']['信号'];\n if (op.path !== '/事件/信号' || op.op !== 'replace' || !isArrayAppend(oldSignals || [], op.value)) {\n return { ok: false, reason: '事件.信号 只允许保留全部旧项并在末尾追加' };\n }\n }\n }\n return { ok: true };\n}\nfunction serializeOps(ops) {\n return bodyMerge.buildFullPatchText(ops || []);\n}\nfunction errorText(error) {\n return error && error.message ? error.message : String(error);\n}\nasync function checkGeneration(host, action) {\n try {\n if (host && typeof host.checkGeneration === 'function') {\n const result = await host.checkGeneration(action || 'apply');\n if (result && result.ok === false) return { ok: false, reason: result.reason || 'AI 正在生成回复', detail: result };\n if (result && (result.generation === 'generating' ||\n (result.generation && result.generation.state === 'generating'))) {\n return { ok: false, reason: result.reason || 'AI 正在生成回复', detail: result };\n }\n return { ok: true, detail: result || { generation: 'unknown' } };\n }\n if (host && typeof host.isGenerating === 'function' && host.isGenerating() === true) {\n return { ok: false, reason: 'AI 正在生成回复' };\n }\n return { ok: true, detail: { generation: 'unknown' } };\n } catch (e) {\n // 状态探测失败不应把未知转换成生成中;目标与路径冲突检查仍会执行。\n return { ok: true, detail: { generation: 'unknown', error: errorText(e) } };\n }\n}\nfunction verifyChangedPaths(snapshot, changeSet, currentData, currentChatVariables, currentText, auxiliaryEdits, parsedData) {\n const beforeStat = snapshot && snapshot.mvuData && snapshot.mvuData.stat_data;\n const stat = parsedData\n ? concurrency.assertMergeablePaths(beforeStat, parsedData.stat_data, currentData && currentData.stat_data, changeSet && changeSet.mvuOps)\n : concurrency.assertUnchangedPaths(beforeStat, currentData && currentData.stat_data, changeSet && changeSet.mvuOps);\n if (!stat.ok) return stat;\n for (const edit of auxiliaryEdits || buildAuxiliaryEdits(snapshot, changeSet)) {\n const current = edit.key === 'chatVariables' ? currentChatVariables : currentData && currentData[edit.storage];\n for (const op of edit.ops) {\n const checked = parsedData\n ? concurrency.assertMergeablePaths(edit.before, edit.after, current == null ? {} : current, [op], edit.storage)\n : concurrency.assertUnchangedPaths(edit.before, current == null ? {} : current, [op], edit.storage);\n // 自动化可能先保存解析产生的辅助数据;显式辅助修改仍按原约定在解析后应用。\n if (!checked.ok && !(parsedData && edit.key !== 'chatVariables' &&\n concurrency.assertUnchangedPaths(parsedData[edit.storage] == null ? {} : parsedData[edit.storage],\n current == null ? {} : current, [op]).ok)) return checked;\n }\n }\n return { ok: true };\n}\nasync function rebaseSubmission(snapshot, changeSet, current, resolve) {\n const before = snapshot.mvuData.stat_data;\n const desired = edits.intent(before, changeSet.mvuOps);\n const stat = await edits.resolveChanges(before, desired, current.data.stat_data, changeSet.mvuOps, resolve, '角色变量',\n changeSet.mvuOps.some(op => op.op === 'move'));\n const auxiliary = {};\n for (const edit of buildAuxiliaryEdits(snapshot, changeSet)) {\n const now = edit.key === 'chatVariables' ? current.chat : current.data[edit.storage];\n const resolved = await edits.resolveChanges(edit.before, edit.after, now == null ? {} : now, edit.ops, resolve, edit.storage);\n auxiliary[edit.key] = resolved.ops.length ? resolved.data : null;\n }\n return {\n snapshot: Object.assign({}, snapshot, { mvuData: current.data, date: current.data.date,\n calendar: current.data.calendar_float_store, chatVariables: current.chat, messageText: current.text }),\n changeSet: Object.assign({}, changeSet, { mvuOps: concurrency.assertUnchangedPaths(before, current.data.stat_data, changeSet.mvuOps).ok\n ? changeSet.mvuOps : patch.diffStatData(current.data.stat_data, stat.data), auxiliary }),\n };\n}\nfunction buildAuxiliaryEdits(snapshot, changeSet) {\n const aux = changeSet && changeSet.auxiliary || {};\n return [['date', 'date'], ['calendar', 'calendar_float_store'], ['chatVariables', '聊天模板变量']]\n .filter(([key]) => aux[key] !== null && aux[key] !== undefined)\n .map(([key, storage]) => {\n const before = snapshot[key] == null ? {} : snapshot[key];\n return { key, storage, before, after: aux[key], ops: concurrency.diffChanges(before, aux[key]) };\n });\n}\nfunction requireMvu(mvu, needsParse) {\n if (!mvu || ['ready', 'read', 'replace', ...(needsParse ? ['parse'] : [])].some((key) => typeof mvu[key] !== 'function')) {\n throw new Error('MVU 正式接口不可用,请检查角色卡脚本是否已就绪');\n }\n}\n\nfunction checkedMvuData(value, needsStat) {\n if (!util.isPlainObject(value) || needsStat && !util.isPlainObject(value.stat_data)) throw new Error('MVU 返回的数据结构无效');\n return util.deepClone(value);\n}\nasync function verifyBeforeWrite(host, target, action) {\n if (!await verifyHostTarget(host, target)) return { ok: false, reason: '目标已变化,请重新加载' };\n const generation = await checkGeneration(host, action);\n if (!generation.ok) return { ok: false, reason: generation.reason, generation: generation.detail };\n return { ok: true, generation: generation.detail };\n}\nasync function preflightOnce(params) {\n const { host, mvu, snapshot, changeSet } = params;\n const mode = params.mode || contract.APPLY_MODE.DIRECT;\n const preview = buildPreview(snapshot, changeSet);\n if (!hasAnyChange(changeSet)) return { ok: false, preview, reason: '无变更可应用' };\n const guard = validateOpsForApply(changeSet.mvuOps || [], snapshot.mvuData && snapshot.mvuData.stat_data);\n if (!guard.ok) return { ok: false, preview, reason: guard.reason };\n if (mode === contract.APPLY_MODE.BODY && snapshot.target.role !== contract.ROLE.ASSISTANT) {\n return { ok: false, preview, reason: '正文模式只能绑定 AI 消息' };\n }\n const initialGuard = await verifyBeforeWrite(host, snapshot.target, 'preflight');\n if (!initialGuard.ok) return { ok: false, preview, reason: initialGuard.reason };\n try {\n requireMvu(mvu, changeSet.mvuOps.length > 0);\n await mvu.ready();\n const fullData = checkedMvuData(await mvu.read(snapshot.target), changeSet.mvuOps.length > 0);\n const currentChat = await host.readChatVariables(snapshot.target.chatId);\n const text = mode === contract.APPLY_MODE.BODY && changeSet.mvuOps.length ? await host.readMessageText(snapshot.target) : undefined;\n const conflict = verifyChangedPaths(snapshot, changeSet, fullData, currentChat, text);\n if (changeSet.mvuOps.length) {\n serializeOps(changeSet.mvuOps);\n if (mode === contract.APPLY_MODE.BODY) bodyMerge.mergeIntoMessage(text, changeSet.mvuOps);\n }\n return { ok: true, preview, conflictPending: !conflict.ok };\n } catch (e) {\n return { ok: false, preview, reason: '修改准备失败: ' + errorText(e) };\n }\n}\nasync function applyOnce(params) {\n const { host, mvu } = params;\n let { snapshot, changeSet } = params;\n const mode = params.mode || contract.APPLY_MODE.DIRECT;\n let preview = buildPreview(snapshot, changeSet);\n if (!hasAnyChange(changeSet)) return { ok: false, preview, report: null, reason: '无变更可应用' };\n\n const guard = validateOpsForApply(changeSet.mvuOps || [], snapshot.mvuData && snapshot.mvuData.stat_data);\n if (!guard.ok) return { ok: false, preview, report: null, reason: guard.reason };\n if (mode === contract.APPLY_MODE.BODY && snapshot.target.role !== contract.ROLE.ASSISTANT) {\n return { ok: false, preview, report: null, reason: '正文模式只能绑定 AI 消息' };\n }\n const initialGuard = await verifyBeforeWrite(host, snapshot.target, 'apply');\n if (!initialGuard.ok) {\n const prefix = initialGuard.reason === '目标已变化,请重新加载' ? '准备应用时' : '';\n return { ok: false, preview, report: null, reason: prefix + initialGuard.reason };\n }\n\n let auxiliaryEdits = buildAuxiliaryEdits(snapshot, changeSet);\n const hasMvuOps = changeSet.mvuOps.length > 0;\n const bodyMode = mode === contract.APPLY_MODE.BODY && hasMvuOps;\n let mvuChanged = hasMvuOps || auxiliaryEdits.some((edit) => edit.key !== 'chatVariables' && edit.ops.length);\n let chatChanged = auxiliaryEdits.some((edit) => edit.key === 'chatVariables' && edit.ops.length);\n let backup, historyBackup, historyResult, newMvuData, newChatVariables, currentText, mergedText, textChanged, operationBefore;\n const prefixOps = (ops, root) => ops.map(op => Object.assign({}, op, {\n path: root + op.path, ...(op.from ? { from: root + op.from } : {}),\n }));\n const paths = { mvu: [], chat: [] };\n const updatePaths = () => {\n paths.mvu = prefixOps(changeSet.mvuOps, '/stat_data'); paths.chat = [];\n auxiliaryEdits = buildAuxiliaryEdits(snapshot, changeSet);\n for (const edit of auxiliaryEdits) {\n if (edit.key === 'chatVariables') paths.chat.push(...edit.ops);\n else paths.mvu.push(...prefixOps(edit.ops, '/' + edit.storage));\n }\n };\n const omitKeptBodyOps = result => {\n changeSet.mvuOps = changeSet.mvuOps.filter(op => !(result.kept || []).some(path =>\n ['/stat_data' + op.path, ...(op.from ? ['/stat_data' + op.from] : [])].some(p => p === path || p.startsWith(path + '/') || path.startsWith(p + '/') || !path)));\n };\n const warnings = [];\n let parsedAttempted = false;\n let phase = '写入前读取当前状态失败';\n const readCurrent = async () => ({\n data: checkedMvuData(await mvu.read(snapshot.target), hasMvuOps),\n chat: util.deepClone(await host.readChatVariables(snapshot.target.chatId)),\n text: bodyMode ? await host.readMessageText(snapshot.target) : undefined,\n });\n try {\n requireMvu(mvu, hasMvuOps);\n await mvu.ready();\n const initial = await readCurrent();\n ({ snapshot, changeSet } = await rebaseSubmission(snapshot, changeSet, initial, params.resolveConflicts));\n updatePaths();\n if (!hasAnyChange(changeSet)) return { ok: true, preview: [], warnings: ['已保留当前值,本次没有需要写入的修改'], noWrite: true };\n const resolvedGuard = validateOpsForApply(changeSet.mvuOps, snapshot.mvuData.stat_data);\n if (!resolvedGuard.ok) throw new Error(resolvedGuard.reason);\n currentText = initial.text;\n mergedText = bodyMode ? bodyMerge.mergeIntoMessage(currentText, changeSet.mvuOps) : currentText;\n textChanged = bodyMode && mergedText !== currentText;\n backup = transaction.createBackup({ target: snapshot.target, messageVariables: initial.data,\n chatVariables: initial.chat, messageText: currentText, cardHash: params.cardHash, version: params.version });\n phase = '写前备份失败';\n historyBackup = transaction.prepareHistoryBackup(backup, {\n nextMessageText: mergedText, scopes: { mvu: mvuChanged, chat: chatChanged, text: textChanged },\n });\n if (params.operationId) historyBackup.operationId = params.operationId;\n // 长期恢复点保存备份时刻的快照。慢存储先完成,不在解析之后反复等待并追逐整桶静止。\n if (typeof params.beforeWrite === 'function') {\n historyResult = await params.beforeWrite(historyBackup);\n if (historyResult && historyResult.ok === false) throw new Error(historyResult.reason || '历史存储失败');\n if (historyResult && Object.prototype.hasOwnProperty.call(historyResult, 'historyBackup')) historyBackup = historyResult.historyBackup;\n warnings.push(...new Set(historyResult && historyResult.warnings || []));\n }\n phase = '写入前复核';\n const baseline = await readCurrent();\n ({ snapshot, changeSet } = await rebaseSubmission(snapshot, changeSet, baseline, params.resolveConflicts));\n updatePaths();\n if (!hasAnyChange(changeSet)) return { ok: true, historyBackup, historyResult, warnings, noWrite: true };\n const rebasedGuard = validateOpsForApply(changeSet.mvuOps, snapshot.mvuData.stat_data);\n if (!rebasedGuard.ok) throw new Error(rebasedGuard.reason);\n const parseGuard = await verifyBeforeWrite(host, snapshot.target, 'apply-before-parse');\n if (!parseGuard.ok) throw new Error(parseGuard.reason);\n phase = '变量更新解析失败';\n parsedAttempted = changeSet.mvuOps.length > 0;\n const parsed = parsedAttempted\n ? checkedMvuData(await mvu.parse(serializeOps(changeSet.mvuOps), util.deepClone(baseline.data)), true)\n : baseline.data;\n phase = '写入前复核';\n const latest = await readCurrent();\n operationBefore = { messageVariables: baseline.data, chatVariables: baseline.chat };\n const finalGuard = await verifyBeforeWrite(host, snapshot.target, 'apply-before-write');\n if (!finalGuard.ok) throw new Error(finalGuard.reason);\n if (parsedAttempted) {\n // 派生结果只解析一次;有真实冲突时展示实际结果,不为了弹窗重演角色卡自动化。\n const merged = await edits.resolveMvu(async () => {\n const guard = await verifyBeforeWrite(host, snapshot.target, 'resolve-parsed-conflict');\n if (!guard.ok) throw new Error(guard.reason);\n return checkedMvuData(await mvu.read(snapshot.target), true);\n }, baseline.data, parsed, params.resolveConflicts, prefixOps(changeSet.mvuOps, '/stat_data'));\n latest.data = merged.current;\n newMvuData = merged.data;\n operationBefore.messageVariables = edits.amendBefore(operationBefore.messageVariables, merged.overrides);\n omitKeptBodyOps(merged);\n paths.mvu = merged.paths;\n } else newMvuData = util.deepClone(latest.data);\n newChatVariables = util.deepClone(latest.chat || {});\n for (const edit of auxiliaryEdits) {\n if (!edit.ops.length) continue;\n const isChat = edit.key === 'chatVariables';\n const current = isChat ? newChatVariables : newMvuData[edit.storage] || {};\n const resolved = await edits.resolveChanges(isChat ? edit.before : parsed[edit.storage] || edit.before,\n edit.after, current, edit.ops, params.resolveConflicts, edit.storage);\n if (isChat) {\n newChatVariables = resolved.data; paths.chat = resolved.paths;\n operationBefore.chatVariables = edits.amendBefore(operationBefore.chatVariables, resolved.overrides);\n } else {\n newMvuData[edit.storage] = resolved.data; paths.mvu.push(...prefixOps(resolved.paths, '/' + edit.storage));\n operationBefore.messageVariables = edits.amendBefore(operationBefore.messageVariables,\n resolved.overrides.map(op => Object.assign({}, op, { path: '/' + edit.storage + op.path })));\n }\n }\n preview = buildPreview(snapshot, changeSet, newMvuData);\n currentText = latest.text;\n mergedText = bodyMode && changeSet.mvuOps.length ? bodyMerge.mergeIntoMessage(currentText, changeSet.mvuOps) : currentText;\n textChanged = bodyMode && mergedText !== currentText;\n mvuChanged = paths.mvu.length > 0;\n chatChanged = paths.chat.length > 0;\n // 自动失败补偿使用每笔实际写入前的内存值,与用户主动恢复的历史快照区分。\n backup = transaction.createBackup({ target: snapshot.target, messageVariables: latest.data,\n chatVariables: latest.chat, messageText: currentText, cardHash: params.cardHash, version: params.version });\n } catch (e) {\n if (parsedAttempted) warnings.push('角色卡自动化可能已更新统计等数据;本次尚未完成应用,已保留输入和可用的恢复点,请勿反复提交。');\n return { ok: false, preview, report: null, historyBackup, historyResult, warnings, parsedAttempted,\n reason: phase + (parsedAttempted ? ',未完成应用: ' : ',未写入: ') + errorText(e) };\n }\n const written = { mvu: false, chat: false, text: false };\n const report = { ok: true, mode, parts: written, failed: [], warnings };\n // 宿主可能保留传入对象引用;核验基准须在写入前独立冻结,避免被外部修改一并污染。\n const expected = util.deepClone({ messageVariables: mvuChanged ? newMvuData : undefined,\n chatVariables: chatChanged ? newChatVariables : undefined, messageText: textChanged ? mergedText : undefined });\n\n try {\n if (mvuChanged) {\n const final = await edits.resolveLive(async () => {\n const guard = await verifyBeforeWrite(host, snapshot.target, 'apply-final');\n if (!guard.ok) throw new Error(guard.reason);\n const data = checkedMvuData(await mvu.read(snapshot.target), hasMvuOps);\n if (parsedAttempted) {\n const rules = concurrency.assertUnchangedPaths(snapshot.mvuData, data, [{ path: '/schema' }], '角色卡校验规则');\n if (!rules.ok) throw new Error(rules.reason);\n }\n return data;\n }, backup.messageVariables, newMvuData, paths.mvu, params.resolveConflicts, '本次修改字段');\n omitKeptBodyOps(final);\n operationBefore.messageVariables = edits.amendBefore(operationBefore.messageVariables, final.overrides);\n newMvuData = parsedAttempted ? edits.copyReports(newMvuData, final.data) : final.data;\n paths.mvu = final.paths;\n backup.messageVariables = final.current;\n expected.messageVariables = util.deepClone(newMvuData);\n const writeGuard = await verifyBeforeWrite(host, snapshot.target, 'apply-mvu-commit');\n if (!writeGuard.ok) throw new Error(writeGuard.reason);\n // 已合并最近读取的业务数据;这里不再插入备份等待或重复解析。\n if (paths.mvu.length) {\n written.mvu = true;\n await mvu.replace(snapshot.target, newMvuData);\n }\n }\n if (chatChanged) {\n const guard = await verifyBeforeWrite(host, snapshot.target, 'apply-chat-variables');\n if (!guard.ok) throw new Error(guard.reason);\n const resolved = await edits.resolveLive(async () => {\n const guard = await verifyBeforeWrite(host, snapshot.target, 'resolve-chat-conflict');\n if (!guard.ok) throw new Error(guard.reason);\n return util.deepClone(await host.readChatVariables(snapshot.target.chatId));\n }, backup.chatVariables, expected.chatVariables, paths.chat, params.resolveConflicts, '聊天模板变量');\n newChatVariables = resolved.data; paths.chat = resolved.paths;\n operationBefore.chatVariables = edits.amendBefore(operationBefore.chatVariables, resolved.overrides);\n backup.chatVariables = resolved.current;\n expected.chatVariables = util.deepClone(newChatVariables);\n const writeGuard = await verifyBeforeWrite(host, snapshot.target, 'apply-chat-commit');\n if (!writeGuard.ok) throw new Error(writeGuard.reason);\n if (paths.chat.length) {\n written.chat = true;\n await host.replaceChatVariables(snapshot.target.chatId, newChatVariables);\n }\n }\n if (textChanged) {\n const latestWriteText = await host.readMessageText(snapshot.target);\n mergedText = changeSet.mvuOps.length ? bodyMerge.mergeIntoMessage(latestWriteText, changeSet.mvuOps) : latestWriteText;\n backup.messageText = latestWriteText;\n textChanged = mergedText !== latestWriteText;\n expected.messageText = textChanged ? mergedText : undefined;\n if (textChanged) paths.text = bodyHistory.createBodyPatchHistory(latestWriteText, mergedText);\n const guard = await verifyBeforeWrite(host, snapshot.target, 'apply-message-text');\n if (!guard.ok) throw new Error(guard.reason);\n if (textChanged) {\n written.text = true;\n await host.replaceMessageText(snapshot.target, mergedText);\n }\n }\n } catch (e) {\n report.ok = false;\n report.failed.push('写入: ' + errorText(e));\n const compensationFailures = await transaction.compensateWrites({ host, mvu, target: snapshot.target, backup, written, expected });\n report.failed.push(...compensationFailures);\n if (parsedAttempted) warnings.push('自动补偿不撤销角色卡解析期间自行保存的统计;需要恢复该部分时,请检查本次历史恢复点。');\n if (/目标已变化|变更路径已被其它脚本修改|聊天模板变量已被其它脚本修改|目标正文已被其它脚本修改/.test(errorText(e))) {\n return { ok: false, written: Object.values(written).some(Boolean), preview, report, backup, historyBackup, historyResult, warnings, reason: '保存过程中' + errorText(e) };\n }\n return {\n ok: false,\n written: Object.values(written).some(Boolean),\n preview,\n report,\n backup,\n historyBackup,\n historyResult,\n warnings,\n reason: compensationFailures.length ? '应用失败,且补偿回滚未完整完成,请查看历史记录' : '应用失败,已恢复写入前状态',\n };\n }\n\n const verified = await transaction.verifyStoredState({ host, mvu, target: snapshot.target, expected, paths });\n if (!verified.ok) {\n report.ok = false;\n report.failed.push('写后核验: ' + verified.reason);\n return { ok: false, written: true, preview, report, backup, historyBackup, historyResult, warnings,\n reason: '写后核验失败: ' + verified.reason + ';已停止自动补偿,避免覆盖外部修改,请刷新并检查历史' };\n }\n\n // 仅在正文已写入且写后实值核验成功后通知宿主;不伪造消息接收、聊天切换或生成结束。\n if (textChanged && typeof host.notifyMessageEdited === 'function') {\n const notified = await transaction.notifyAndVerify({ host, mvu, target: snapshot.target, expected, paths });\n for (const warning of notified.warnings) if (!warnings.includes(warning)) warnings.push(warning);\n if (!notified.ok) {\n report.ok = false;\n report.failed.push(notified.reason);\n return { ok: false, written: true, preview, report, backup, historyBackup, historyResult, warnings, reason: notified.reason };\n }\n }\n\n return { ok: true, preview, report, backup, historyBackup, historyResult, warnings, parsedData: newMvuData,\n bodyPatchHistory: textChanged ? bodyHistory.createBodyPatchHistory(backup.messageText, expected.messageText) : null,\n operationChanges: edits.operationChanges(operationBefore, expected, paths) };\n}\nasync function restoreBackup(params) {\n const { host, mvu, backup } = params;\n if (!backup || !backup.target) return { ok: false, reason: '历史备份无效' };\n const capability = transaction.getBackupScope(backup);\n const scopes = capability.scopes;\n const full = params.fullSnapshot === true;\n if (!full && (scopes.mvu || scopes.chat) && (!backup.operationChanges || backup.operationChanges.version !== 1)) {\n return { ok: false, reason: '此记录没有保存逐项变更,请使用“完整快照恢复”;不会猜测当时改过哪些字段' };\n }\n if (!scopes.mvu && !scopes.chat && !scopes.text) return { ok: false, reason: capability.partialReason || '记录没有可恢复内容' };\n const initialGeneration = await checkGeneration(host, 'rollback');\n if (!initialGeneration.ok) return { ok: false, reason: initialGeneration.reason };\n const identityOk = typeof host.verifyTargetIdentity === 'function'\n ? await host.verifyTargetIdentity(backup.target)\n : await host.verifyTarget(backup.target);\n if (!identityOk) return { ok: false, reason: '历史记录不属于当前聊天、楼层或 swipe' };\n try { if (scopes.mvu) await mvu.ready(); }\n catch (e) { return { ok: false, reason: 'MVU 未就绪: ' + errorText(e) }; }\n\n let current;\n let restoredText;\n let historyBackup;\n let historyResult;\n const paths = {};\n let expected;\n const warnings = [];\n try {\n current = transaction.createBackup({\n target: backup.target,\n messageVariables: scopes.mvu ? await mvu.read(backup.target) : undefined,\n chatVariables: scopes.chat ? await host.readChatVariables(backup.target.chatId) : undefined,\n messageText: scopes.text ? await host.readMessageText(backup.target) : undefined,\n });\n // 先验证全部指令定位,再写任何变量,不能出现“变量已回滚、指令因冲突未恢复”。\n if (scopes.text) restoredText = bodyHistory.restoreBodyPatchHistory(current.messageText, backup.bodyPatchHistory);\n expected = { messageText: restoredText };\n for (const [key, store] of [['mvu', 'messageVariables'], ['chat', 'chatVariables']]) {\n if (!scopes[key]) continue;\n const resolved = full ? { data: util.deepClone(backup[store]) }\n : await edits.reverse(current[store], backup.operationChanges[key] || [], params.resolveConflicts, '回滚涉及的字段');\n expected[store] = resolved.data;\n if (!full) paths[key] = resolved.paths;\n }\n if (typeof params.beforeWrite === 'function') {\n historyBackup = transaction.prepareHistoryBackup(current, { scopes, nextMessageText: restoredText });\n if (params.operationId) historyBackup.operationId = params.operationId;\n historyResult = await params.beforeWrite(historyBackup);\n if (historyResult && historyResult.ok === false) throw new Error('回滚前备份失败: ' + (historyResult.reason || '历史存储失败'));\n if (historyResult && Object.prototype.hasOwnProperty.call(historyResult, 'historyBackup')) historyBackup = historyResult.historyBackup;\n warnings.push(...historyResult && historyResult.warnings || []);\n }\n } catch (e) { return { ok: false, historyBackup, historyResult, warnings, reason: '回滚前读取失败: ' + errorText(e) }; }\n\n const writeIdentityOk = typeof host.verifyTargetIdentity === 'function'\n ? await host.verifyTargetIdentity(backup.target)\n : await host.verifyTarget(backup.target);\n if (!writeIdentityOk) return { ok: false, historyBackup, historyResult, warnings, reason: '回滚准备期间目标发生变化,已中止' };\n const rollbackGuard = await checkGeneration(host, 'rollback-before-write');\n if (!rollbackGuard.ok) return { ok: false, historyBackup, historyResult, warnings, reason: '回滚准备期间开始生成回复,已中止' };\n\n const written = { mvu: false, chat: false, text: false };\n const report = { ok: true, mode: 'rollback', parts: written, failed: [], warnings };\n const operationBefore = util.deepClone(current);\n try {\n if (scopes.mvu) {\n const mvuGuard = await verifyBeforeWrite(host, backup.target, 'rollback-mvu');\n if (!mvuGuard.ok) throw new Error(mvuGuard.reason);\n const resolved = await edits.resolveLive(() => mvu.read(backup.target), current.messageVariables,\n expected.messageVariables, full ? [{ path: '' }] : paths.mvu, params.resolveConflicts, '回滚涉及的消息变量');\n current.messageVariables = resolved.current; expected.messageVariables = resolved.data;\n if (!full) paths.mvu = resolved.paths;\n const guard = await verifyBeforeWrite(host, backup.target, 'rollback-mvu-commit');\n if (!guard.ok) throw new Error(guard.reason);\n operationBefore.messageVariables = edits.amendBefore(operationBefore.messageVariables, resolved.overrides);\n written.mvu = true;\n await mvu.replace(backup.target, util.deepClone(expected.messageVariables));\n }\n if (scopes.chat) {\n const chatGuard = await verifyBeforeWrite(host, backup.target, 'rollback-chat-variables');\n if (!chatGuard.ok) throw new Error(chatGuard.reason);\n const resolved = await edits.resolveLive(() => host.readChatVariables(backup.target.chatId), current.chatVariables,\n expected.chatVariables, full ? [{ path: '' }] : paths.chat, params.resolveConflicts, '回滚涉及的聊天变量');\n current.chatVariables = resolved.current; expected.chatVariables = resolved.data;\n if (!full) paths.chat = resolved.paths;\n const guard = await verifyBeforeWrite(host, backup.target, 'rollback-chat-commit');\n if (!guard.ok) throw new Error(guard.reason);\n operationBefore.chatVariables = edits.amendBefore(operationBefore.chatVariables, resolved.overrides);\n written.chat = true;\n await host.replaceChatVariables(backup.target.chatId, util.deepClone(expected.chatVariables));\n }\n if (scopes.text) {\n const textGuard = await verifyBeforeWrite(host, backup.target, 'rollback-message-text');\n if (!textGuard.ok) throw new Error(textGuard.reason);\n current.messageText = await host.readMessageText(backup.target);\n restoredText = bodyHistory.restoreBodyPatchHistory(current.messageText, backup.bodyPatchHistory);\n expected.messageText = restoredText;\n paths.text = bodyHistory.createBodyPatchHistory(current.messageText, restoredText);\n written.text = true;\n await host.replaceMessageText(backup.target, restoredText);\n }\n } catch (e) {\n report.ok = false;\n report.failed.push('回滚写入: ' + errorText(e));\n report.failed.push(...await transaction.compensateWrites({ host, mvu, target: backup.target, backup: current, written, expected }));\n return { ok: false, written: Object.values(written).some(Boolean), report, historyBackup, historyResult, warnings, reason: '回滚失败,已尝试恢复回滚前状态' };\n }\n const verified = await transaction.verifyStoredState({ host, mvu, target: backup.target, expected, paths });\n if (!verified.ok) {\n report.ok = false;\n report.failed.push('回滚核验: ' + verified.reason);\n return { ok: false, written: true, report, historyBackup, historyResult, warnings, reason: '回滚写后核验失败: ' + verified.reason +\n ';已停止自动补偿,避免覆盖外部修改,请刷新并检查历史' };\n }\n if (scopes.text && typeof host.notifyMessageEdited === 'function') {\n const notified = await transaction.notifyAndVerify({ host, mvu, target: backup.target, expected, paths });\n for (const warning of notified.warnings) if (!warnings.includes(warning)) warnings.push(warning);\n if (!notified.ok) {\n report.ok = false;\n report.failed.push(notified.reason);\n return { ok: false, written: true, report, historyBackup, historyResult, warnings, reason: notified.reason };\n }\n }\n return Object.assign({ ok: true, report, historyBackup, historyResult, warnings,\n bodyPatchHistory: scopes.text ? bodyHistory.createBodyPatchHistory(current.messageText, expected.messageText) : undefined,\n operationChanges: edits.operationChanges(operationBefore, expected, paths) }, capability);\n}\n\nconst APPLY_MODE = contract.APPLY_MODE;\n\nmodule.exports = {\n buildPreview,\n preflightOnce,\n applyOnce,\n restoreBackup,\n serializeOps,\n hasAnyChange,\n validateOpsForApply,\n isArrayAppend,\n checkGeneration,\n verifyChangedPaths,\n APPLY_MODE,\n};\n\n});\n\n__define('apply/body-history', function (module, exports, require) {\nconst body = require('./body-merge');\nconst ANCHOR_LENGTH = 64;\nfunction scan(text) {\n body.assertBalancedTags(text);\n const spans = [];\n const containers = [];\n const updates = /<UpdateVariable>([\\s\\S]*?)<\\/UpdateVariable>/gi;\n let match;\n while ((match = updates.exec(text))) {\n const start = match.index + '<UpdateVariable>'.length;\n containers.push({ start: match.index, end: updates.lastIndex });\n const tags = /<JSONPatch>([\\s\\S]*?)<\\/JSONPatch>/gi;\n let tag;\n let found = false;\n while ((tag = tags.exec(match[1]))) {\n body.parseOps(tag[1]);\n spans.push({ start: start + tag.index, end: start + tags.lastIndex, raw: tag[0] });\n found = true;\n }\n if (!found) {\n const legacy = body.extractOpsFromInner(match[1]);\n if (legacy.matchStr) {\n const position = start + match[1].indexOf(legacy.matchStr);\n spans.push({ start: position, end: position + legacy.matchStr.length, raw: legacy.matchStr });\n }\n }\n }\n const tags = /<JSONPatch>([\\s\\S]*?)<\\/JSONPatch>/gi;\n while ((match = tags.exec(text))) {\n if (containers.some((c) => match.index >= c.start && match.index < c.end)) continue;\n body.parseOps(match[1]);\n spans.push({ start: match.index, end: tags.lastIndex, raw: match[0] });\n }\n return spans.sort((a, b) => a.start - b.start);\n}\nfunction fingerprint(value) {\n let first = 0;\n let second = 0;\n for (let i = 0; i < value.length; i++) {\n first = (Math.imul(first, 31) + value.charCodeAt(i) + 1) | 0;\n second = (Math.imul(second, 131) + value.charCodeAt(i) + 1) | 0;\n }\n return [first >>> 0, second >>> 0];\n}\n\nfunction signature(spans) {\n const value = JSON.stringify(spans.map((span) => span.raw));\n return [value.length, ...fingerprint(value)].join(':');\n}\n\nfunction contextAt(text, start, end) {\n const left = text.slice(Math.max(0, start - ANCHOR_LENGTH), start);\n const right = text.slice(end, end + ANCHOR_LENGTH);\n return { leftLength: left.length, rightLength: right.length, fingerprint: fingerprint(left + right) };\n}\nfunction decompose(text, spans) {\n let skeleton = '';\n let cursor = 0;\n const slots = new Map();\n for (const span of spans) {\n skeleton += text.slice(cursor, span.start);\n const previous = slots.get(skeleton.length);\n slots.set(skeleton.length, { raw: (previous ? previous.raw : '') + span.raw });\n cursor = span.end;\n }\n return { skeleton: skeleton + text.slice(cursor), slots };\n}\nfunction locateEdits(text, edits) {\n const matches = new Map();\n const emptyGroups = new Map();\n for (const edit of edits) {\n const found = [];\n matches.set(edit, found);\n if (edit.emptyDocument) {\n if (text === '') found.push(0);\n continue;\n }\n const context = edit.context || {};\n const { leftLength, rightLength } = context;\n if (![leftLength, rightLength].every((size) => Number.isInteger(size) && size >= 0 && size <= ANCHOR_LENGTH) ||\n !Array.isArray(context.fingerprint) || context.fingerprint.length !== 2) throw new Error('指令历史定位信息无效');\n const key = context.fingerprint.join(':');\n if (edit.after) {\n const rawMatches = [];\n let at = text.indexOf(edit.after);\n while (at >= 0) {\n rawMatches.push(at);\n const end = at + edit.after.length;\n if (at >= leftLength && end + rightLength <= text.length &&\n fingerprint(text.slice(at - leftLength, at) + text.slice(end, end + rightLength)).join(':') === key) found.push(at);\n if (found.length > 1) break;\n at = text.indexOf(edit.after, at + 1);\n }\n // 指令本身仍唯一时,附近剧情或无关指令变化无需阻止恢复。\n if (!found.length && rawMatches.length === 1) found.push(rawMatches[0]);\n } else {\n const size = leftLength + rightLength;\n if (!size) throw new Error('指令历史缺少定位信息');\n if (!emptyGroups.has(size)) emptyGroups.set(size, new Map());\n const group = emptyGroups.get(size);\n const [first, second] = context.fingerprint;\n if (!group.has(first)) group.set(first, new Map());\n const paired = group.get(first);\n if (!paired.has(second)) paired.set(second, []);\n paired.get(second).push(edit);\n }\n }\n for (const [size, group] of emptyGroups) {\n if (size > text.length) continue;\n let [first, second] = fingerprint(text.slice(0, size));\n let powerFirst = 1;\n let powerSecond = 1;\n for (let i = 1; i < size; i++) {\n powerFirst = Math.imul(powerFirst, 31);\n powerSecond = Math.imul(powerSecond, 131);\n }\n for (let start = 0; start + size <= text.length; start++) {\n const paired = group.get(first >>> 0);\n const candidates = paired && paired.get(second >>> 0);\n if (candidates) for (const edit of candidates) {\n const found = matches.get(edit);\n if (found.length < 2) found.push(start + edit.context.leftLength);\n }\n if (start + size === text.length) break;\n const outgoing = text.charCodeAt(start) + 1;\n const incoming = text.charCodeAt(start + size) + 1;\n first = (Math.imul(first - Math.imul(outgoing, powerFirst), 31) + incoming) | 0;\n second = (Math.imul(second - Math.imul(outgoing, powerSecond), 131) + incoming) | 0;\n }\n }\n return edits.map((edit) => {\n const found = matches.get(edit);\n if (found.length !== 1) throw new Error('指令块或附近内容已变化,无法唯一定位;未回滚任何数据');\n return { start: found[0], end: found[0] + edit.after.length, value: edit.before };\n });\n}\nfunction createBodyPatchHistory(beforeText, afterText) {\n const before = String(beforeText || '');\n const after = String(afterText || '');\n const beforeSpans = scan(before);\n const afterSpans = scan(after);\n const edits = [];\n if (!beforeSpans.length && after.startsWith(before + '\\n\\n') &&\n after.slice(before.length + 2) === body.buildFullPatchText(body.parseExistingOps(after.slice(before.length + 2)))) {\n // 只允许回收本次追加且仍完整未变的修改器容器,不涉及原剧情或原 Analysis。\n edits.push({ before: '', after: after.slice(before.length), context: { leftLength: 0, rightLength: 0, fingerprint: fingerprint('') } });\n } else {\n const oldParts = decompose(before, beforeSpans);\n const newParts = decompose(after, afterSpans);\n if (oldParts.skeleton !== newParts.skeleton) {\n // 回滚本身也可撤销:只记录被准确移除的修改器新建容器及其原插入点。\n const containers = /\\n\\n<UpdateVariable>[\\s\\S]*?<\\/UpdateVariable>/gi;\n let match;\n while ((match = containers.exec(before))) {\n const raw = match[0];\n if (raw.slice(2) !== body.buildFullPatchText(body.parseExistingOps(raw))) continue;\n if (before.slice(0, match.index) + before.slice(containers.lastIndex) !== after) continue;\n const edit = { before: raw, after: '', context: contextAt(after, match.index, match.index) };\n if (!after) edit.emptyDocument = true;\n locateEdits(after, [edit]);\n return { version: 2, afterSignature: signature(afterSpans), edits: [edit] };\n }\n throw new Error('指令历史不能包含剧情或 Analysis 修改');\n }\n const offsets = [...new Set([...oldParts.slots.keys(), ...newParts.slots.keys()])].sort((a, b) => a - b);\n let insertedLength = 0;\n for (const offset of offsets) {\n const oldRaw = (oldParts.slots.get(offset) || {}).raw || '';\n const newRaw = (newParts.slots.get(offset) || {}).raw || '';\n const position = offset + insertedLength;\n if (oldRaw !== newRaw) {\n edits.push({ before: oldRaw, after: newRaw, context: contextAt(after, position, position + newRaw.length) });\n }\n insertedLength += newRaw.length;\n }\n }\n locateEdits(after, edits);\n return { version: 2, afterSignature: signature(afterSpans), edits };\n}\nfunction restoreBodyPatchHistory(currentText, record) {\n if (!record || record.version !== 2 || !Array.isArray(record.edits)) throw new Error('指令历史格式无效');\n const text = String(currentText || '');\n scan(text);\n const located = locateEdits(text, record.edits).sort((a, b) => b.start - a.start);\n let output = text;\n let previousStart = text.length + 1;\n for (const edit of located) {\n if (edit.end > previousStart) throw new Error('指令历史定位重叠,已停止回滚');\n output = output.slice(0, edit.start) + edit.value + output.slice(edit.end);\n previousStart = edit.start;\n }\n return output;\n}\nfunction sameInstructions(first, second) {\n return signature(scan(String(first || ''))) === signature(scan(String(second || '')));\n}\n\nmodule.exports = { createBodyPatchHistory, restoreBodyPatchHistory, sameInstructions };\n\n});\n\n__define('apply/body-merge', function (module, exports, require) {\nconst JSONPATCH_RE = /<JSONPatch>([\\s\\S]*?)<\\/JSONPatch>/gi;\nconst UPDATABLE_RE = /<UpdateVariable>([\\s\\S]*?)<\\/UpdateVariable>/gi;\nfunction parseOps(raw) {\n if (typeof raw !== 'string' || !raw.trim()) throw new Error('JSONPatch 内容为空或格式异常');\n let arr;\n try { arr = JSON.parse(raw.trim()); }\n catch (e) { throw new Error('JSONPatch 不是合法 JSON'); }\n if (!Array.isArray(arr)) throw new Error('JSONPatch 内容不是数组');\n for (const op of arr) {\n if (!op || typeof op !== 'object' || typeof op.path !== 'string' || typeof op.op !== 'string') {\n throw new Error('JSONPatch 存在非法操作项');\n }\n }\n return arr;\n}\nfunction assertBalancedTags(mes) {\n const count = (re) => ((mes || '').match(re) || []).length;\n if (count(/<UpdateVariable>/gi) !== count(/<\\/UpdateVariable>/gi)) {\n throw new Error('UpdateVariable 标签边界不完整');\n }\n if (count(/<JSONPatch>/gi) !== count(/<\\/JSONPatch>/gi)) {\n throw new Error('JSONPatch 标签边界不完整');\n }\n}\nfunction scanPatchContainers(mes) {\n const text = mes || '';\n assertBalancedTags(text);\n const containers = [];\n const ranges = [];\n const updRe = /<UpdateVariable>([\\s\\S]*?)<\\/UpdateVariable>/gi;\n let match;\n while ((match = updRe.exec(text)) !== null) {\n const ops = [];\n const patchRe = /<JSONPatch>([\\s\\S]*?)<\\/JSONPatch>/gi;\n let patchMatch;\n while ((patchMatch = patchRe.exec(match[1])) !== null) ops.push(...parseOps(patchMatch[1]));\n const range = { start: match.index, end: match.index + match[0].length };\n ranges.push(range);\n const legacy = /<JSONPatch>/i.test(match[1]) ? null : extractOpsFromInner(match[1]);\n if (legacy && legacy.matchStr) ops.push(...legacy.ops);\n if (ops.length || /<JSONPatch>/i.test(match[1]) || legacy && legacy.matchStr) {\n containers.push({ blockStart: range.start, blockEnd: range.end, inner: match[1], isUpd: true, ops,\n legacyMatch: legacy && legacy.matchStr });\n }\n }\n const bareRe = /<JSONPatch>([\\s\\S]*?)<\\/JSONPatch>/gi;\n while ((match = bareRe.exec(text)) !== null) {\n const start = match.index;\n if (ranges.some((r) => start >= r.start && start < r.end)) continue;\n containers.push({\n blockStart: start,\n blockEnd: start + match[0].length,\n inner: match[1],\n isUpd: false,\n ops: parseOps(match[1]),\n });\n }\n return containers.sort((a, b) => a.blockStart - b.blockStart);\n}\nfunction locateFirstPatchBlock(mes) {\n return scanPatchContainers(mes)[0] || null;\n}\nfunction parseExistingOps(mes) {\n return scanPatchContainers(mes).flatMap((container) => container.ops);\n}\nfunction mergeIntoMessage(mes, userOps) {\n const text = mes || '';\n const containers = scanPatchContainers(text);\n if (!containers.length) {\n // 无 JSONPatch 块:只追加最小 UpdateVariable 块。未知或旧版标签也属于原正文,\n // 修改器没有得到删除授权,必须逐字保留。\n return text + '\\n\\n' + buildFullPatchText(userOps || []);\n }\n const existing = containers.flatMap((container) => container.ops);\n const merged = mergeOps(existing, userOps);\n const payload = JSON.stringify(merged, null, 2).replace(/<\\//g, '\\\\u003C/');\n const newPatch = '<JSONPatch>\\n' + payload + '\\n</JSONPatch>';\n const first = containers[0];\n let replacement = newPatch;\n if (first.isUpd) {\n let used = false;\n const newInner = first.legacyMatch ? first.inner.replace(first.legacyMatch, () => newPatch) :\n first.inner.replace(/<JSONPatch>[\\s\\S]*?<\\/JSONPatch>/gi, () => {\n if (used) return '';\n used = true;\n return newPatch;\n });\n replacement = '<UpdateVariable>' + newInner + '</UpdateVariable>';\n }\n // 从后往前替换,保证索引不因前方改动漂移;后续容器已并入首块,故移除。\n let output = text;\n for (let i = containers.length - 1; i >= 0; i--) {\n const container = containers[i];\n let value = i === 0 ? replacement : '';\n if (i > 0 && container.isUpd) {\n // 后续块的操作已经并入首块,但其中 Analysis 或其它未知文本仍属消息正文;\n // 只移除 JSONPatch,不删除整个 UpdateVariable 容器的其它内容。\n const preservedInner = container.legacyMatch ? container.inner.replace(container.legacyMatch, '') :\n container.inner.replace(/<JSONPatch>[\\s\\S]*?<\\/JSONPatch>/gi, '');\n value = '<UpdateVariable>' + preservedInner + '</UpdateVariable>';\n }\n output = output.slice(0, container.blockStart) + value + output.slice(container.blockEnd);\n }\n return output;\n}\nfunction mergeOps(existing, userOps) {\n const result = (existing || []).slice();\n for (const u of userOps || []) {\n // 删除所有旧同路径操作;仅删第一个会让后块再次覆盖用户值。\n for (let i = result.length - 1; i >= 0; i--) {\n if (result[i].path === u.path) result.splice(i, 1);\n }\n result.push(u);\n }\n return result;\n}\nfunction buildFullPatchText(ops) {\n const payload = JSON.stringify(ops, null, 2).replace(/<\\//g, '\\\\u003C/');\n return '<UpdateVariable>\\n<Analysis>\\n- 修改器指令注入:按 JSONPatch 更新以下变量(replace 覆盖指定路径)。\\n</Analysis>\\n<JSONPatch>\\n' + payload + '\\n</JSONPatch>\\n</UpdateVariable>';\n}\n\nconst PATH_CN = {\n '/世界/时间': '世界·时间',\n '/世界/地点': '世界·地点',\n '/任务列表': '任务列表',\n '/命运点数': '命运点数',\n '/新闻': '新闻',\n '/主角/种族': '主角·种族',\n '/主角/等级': '主角·等级',\n '/主角/累计经验值': '主角·经验',\n '/主角/升级所需经验': '主角·升级所需',\n '/主角/金钱': '主角·金钱',\n '/主角/属性点': '主角·属性点',\n '/主角/属性/力量': '主角·力量',\n '/主角/属性/敏捷': '主角·敏捷',\n '/主角/属性/体质': '主角·体质',\n '/主角/属性/智力': '主角·智力',\n '/主角/属性/精神': '主角·精神',\n '/主角/资源/生命/当前': '主角·生命当前',\n '/主角/资源/魔力/当前': '主角·魔力当前',\n '/主角/资源/耐力/当前': '主角·耐力当前',\n};\n\nfunction pathCN(p) {\n if (PATH_CN[p]) return PATH_CN[p];\n const parts = String(p || '').split('/').filter(Boolean);\n return parts.length ? parts.slice(-2).join(' · ') : p;\n}\nfunction extractOpsFromInner(inner) {\n const patchMatch = /<JSONPatch>([\\s\\S]*?)<\\/JSONPatch>/i.exec(inner);\n if (patchMatch) {\n try { return { ops: parseOps(patchMatch[1]), type: 'tag', matchStr: patchMatch[0] }; }\n catch (e) { /* 继续兜底 */ }\n }\n const fenceMatch = /```(?:json)?\\s*(\\[\\s*\\{[\\s\\S]*\\}\\s*\\])\\s*```/i.exec(inner);\n if (fenceMatch) {\n try { return { ops: parseOps(fenceMatch[1]), type: 'fence', matchStr: fenceMatch[0] }; }\n catch (e) { /* 继续兜底 */ }\n }\n const arrayMatch = /\\[\\s*\\{[\\s\\S]*\\}\\s*\\]/.exec(inner);\n if (arrayMatch) {\n try { return { ops: parseOps(arrayMatch[0]), type: 'array', matchStr: arrayMatch[0] }; }\n catch (e) { /* 忽略 */ }\n }\n return { ops: [], type: null, matchStr: null };\n}\nfunction parseUpdateBlocksWithAnalysis(mes) {\n const text = mes || '';\n const blocks = [];\n const updRe = /<UpdateVariable>([\\s\\S]*?)<\\/UpdateVariable>/gi;\n let match;\n let blockIndex = 0;\n while ((match = updRe.exec(text)) !== null) {\n const inner = match[1];\n const analysisMatch = /<Analysis>([\\s\\S]*?)<\\/Analysis>/i.exec(inner);\n const analysis = analysisMatch ? analysisMatch[1].trim() : '';\n const extracted = extractOpsFromInner(inner);\n blocks.push({ blockIndex, analysis, ops: extracted.ops, rawInner: inner });\n blockIndex++;\n }\n let narrative = text;\n const firstIdx = text.search(/<UpdateVariable>/i);\n if (firstIdx >= 0) narrative = text.slice(0, firstIdx).trim();\n\n const allOps = [];\n const allAnalysis = [];\n for (const b of blocks) {\n if (b.analysis) allAnalysis.push(b.analysis);\n (b.ops || []).forEach((op, opIdx) => {\n allOps.push(Object.assign({}, op, { blockIndex: b.blockIndex, opIndex: opIdx }));\n });\n }\n if (blocks.length === 0) {\n const bareContainers = scanPatchContainers(text);\n bareContainers.forEach((c, bIdx) => {\n (c.ops || []).forEach((op, opIdx) => {\n allOps.push(Object.assign({}, op, { blockIndex: bIdx, opIndex: opIdx }));\n });\n });\n }\n\n return {\n narrative,\n blocks,\n analysis: allAnalysis.join('\\n\\n'),\n ops: allOps,\n };\n}\nfunction updateDeclaredOp(mes, blockIndex, opIndex, newOp) {\n assertBalancedTags(mes);\n const updRe = /<UpdateVariable>([\\s\\S]*?)<\\/UpdateVariable>/gi;\n let count = 0;\n let matched = false;\n const result = (mes || '').replace(updRe, (match, inner) => {\n if (count++ !== blockIndex) return match;\n matched = true;\n const extracted = extractOpsFromInner(inner);\n if (!extracted.ops || opIndex < 0 || opIndex >= extracted.ops.length) return match;\n const ops = extracted.ops.slice();\n ops[opIndex] = newOp;\n const payload = JSON.stringify(ops, null, 2).replace(/<\\//g, '\\\\u003C/');\n const newPatch = '<JSONPatch>\\n' + payload + '\\n</JSONPatch>';\n let newInner;\n if (extracted.type === 'tag') {\n newInner = inner.replace(/<JSONPatch>([\\s\\S]*?)<\\/JSONPatch>/i, () => newPatch);\n } else if (extracted.matchStr) {\n newInner = inner.replace(extracted.matchStr, () => newPatch);\n } else {\n newInner = inner.trim() + '\\n' + newPatch;\n }\n return '<UpdateVariable>' + newInner + '</UpdateVariable>';\n });\n if (matched) return result;\n\n const bareRe = /<JSONPatch>([\\s\\S]*?)<\\/JSONPatch>/gi;\n count = 0;\n return (mes || '').replace(bareRe, (match, inner) => {\n if (count++ !== blockIndex) return match;\n let ops = parseOps(inner);\n if (opIndex < 0 || opIndex >= ops.length) return match;\n ops[opIndex] = newOp;\n return '<JSONPatch>\\n' + JSON.stringify(ops, null, 2).replace(/<\\//g, '\\\\u003C/') + '\\n</JSONPatch>';\n });\n}\nfunction removeDeclaredOp(mes, blockIndex, opIndex) {\n assertBalancedTags(mes);\n const updRe = /<UpdateVariable>([\\s\\S]*?)<\\/UpdateVariable>/gi;\n let count = 0;\n let matched = false;\n const result = (mes || '').replace(updRe, (match, inner) => {\n if (count++ !== blockIndex) return match;\n matched = true;\n const extracted = extractOpsFromInner(inner);\n if (!extracted.ops || opIndex < 0 || opIndex >= extracted.ops.length) return match;\n const ops = extracted.ops.slice();\n ops.splice(opIndex, 1);\n const payload = JSON.stringify(ops, null, 2).replace(/<\\//g, '\\\\u003C/');\n const newPatch = '<JSONPatch>\\n' + payload + '\\n</JSONPatch>';\n let newInner;\n if (extracted.type === 'tag') {\n newInner = inner.replace(/<JSONPatch>([\\s\\S]*?)<\\/JSONPatch>/i, () => newPatch);\n } else if (extracted.matchStr) {\n newInner = inner.replace(extracted.matchStr, () => newPatch);\n } else {\n newInner = inner.trim() + '\\n' + newPatch;\n }\n return '<UpdateVariable>' + newInner + '</UpdateVariable>';\n });\n if (matched) return result;\n\n const bareRe = /<JSONPatch>([\\s\\S]*?)<\\/JSONPatch>/gi;\n count = 0;\n return (mes || '').replace(bareRe, (match, inner) => {\n if (count++ !== blockIndex) return match;\n let ops = parseOps(inner);\n if (opIndex < 0 || opIndex >= ops.length) return match;\n ops.splice(opIndex, 1);\n return '<JSONPatch>\\n' + JSON.stringify(ops, null, 2).replace(/<\\//g, '\\\\u003C/') + '\\n</JSONPatch>';\n });\n}\n\nmodule.exports = {\n JSONPATCH_RE,\n UPDATABLE_RE,\n PATH_CN,\n pathCN,\n parseOps,\n parseExistingOps,\n locateFirstPatchBlock,\n scanPatchContainers,\n mergeIntoMessage,\n mergeOps,\n buildFullPatchText,\n parseUpdateBlocksWithAnalysis,\n updateDeclaredOp,\n removeDeclaredOp,\n extractOpsFromInner,\n assertBalancedTags,\n};\n\n});\n\n__define('apply/concurrency', function (module, exports, require) {\nconst util = require('../util');\nconst patch = require('../data/patch');\nconst owns = (object, key) => object != null && Object.prototype.hasOwnProperty.call(object, key);\nfunction diffChanges(before, after, prefix = []) {\n if (util.deepEqual(before, after)) return [];\n if (!util.isPlainObject(before) || !util.isPlainObject(after)) {\n return [{ op: 'replace', path: patch.joinPointer(prefix), value: util.deepClone(after) }];\n }\n const changes = [];\n for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {\n const path = patch.joinPointer(prefix.concat(key));\n if (!owns(after, key)) changes.push({ op: 'remove', path });\n else if (!owns(before, key)) changes.push({ op: 'insert', path, value: util.deepClone(after[key]) });\n else changes.push(...diffChanges(before[key], after[key], prefix.concat(key)));\n }\n return changes;\n}\n\nfunction kind(value) {\n return Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value;\n}\nfunction assertUnchangedPaths(before, current, operations, label = '变更路径') {\n for (const op of operations || []) {\n for (const path of [op.path, ...(op.op === 'move' && op.from ? [op.from] : [])]) {\n const segments = patch.parsePointer(path);\n let oldValue = before;\n let latestValue = current;\n for (let index = 0; index <= segments.length; index++) {\n const atLeaf = index === segments.length;\n const mismatched = kind(oldValue) !== kind(latestValue) ||\n (atLeaf || Array.isArray(oldValue)) && !util.deepEqual(oldValue, latestValue);\n if (mismatched) return { ok: false, reason: label + '已被其它脚本修改:' +\n (patch.joinPointer(segments.slice(0, index)) || '(根对象)') };\n if (atLeaf) break;\n const key = segments[index];\n if (owns(oldValue, key) !== owns(latestValue, key)) {\n return { ok: false, reason: label + '已被其它脚本修改:' + patch.joinPointer(segments.slice(0, index + 1)) };\n }\n oldValue = owns(oldValue, key) ? oldValue[key] : undefined;\n latestValue = owns(latestValue, key) ? latestValue[key] : undefined;\n }\n }\n }\n return { ok: true };\n}\nfunction assertMergeablePaths(before, parsed, current, operations, label = '变更路径') {\n for (const op of operations || []) {\n const guard = assertUnchangedPaths(before, current, [op], label);\n if (!guard.ok && !assertUnchangedPaths(parsed, current, [op], label).ok) return guard;\n }\n return { ok: true };\n}\nfunction mergeChanges(current, operations) {\n let result = util.deepClone(current);\n for (const op of operations || []) {\n const segments = patch.parsePointer(op.path);\n if (segments.some((key) => !util.isSafeKey(key))) throw new Error('辅助变量路径包含不安全键名:' + op.path);\n if (!segments.length) {\n result = op.op === 'remove' ? undefined : util.deepClone(op.value);\n continue;\n }\n let parent = result;\n for (const key of segments.slice(0, -1)) {\n if (!owns(parent, key) || !parent[key] || typeof parent[key] !== 'object') throw new Error('辅助变量父字段已变化:' + op.path);\n parent = parent[key];\n }\n if (!parent || typeof parent !== 'object') throw new Error('辅助变量父字段不可写:' + op.path);\n const key = segments[segments.length - 1];\n if (op.op === 'remove') delete parent[key];\n else parent[key] = util.deepClone(op.value);\n }\n return result;\n}\nfunction mergeMvuResult(before, parsed, current) {\n if (![before, parsed, current].every(value => util.isPlainObject(value) && util.isPlainObject(value.stat_data))) {\n throw new Error('MVU 返回的数据结构无效');\n }\n const rules = assertUnchangedPaths(before, current, [{ path: '/schema' }], '角色卡校验规则');\n if (!rules.ok) throw new Error(rules.reason);\n const reports = new Set(['display_data', 'delta_data']);\n const operations = diffChanges(before, parsed).filter(op => !reports.has(patch.parsePointer(op.path)[0]));\n const guard = assertMergeablePaths(before, parsed, current, operations, '校验派生字段');\n if (!guard.ok) throw new Error(guard.reason);\n const data = mergeChanges(current, operations);\n for (const key of reports) {\n if (owns(parsed, key)) data[key] = util.deepClone(parsed[key]);\n else delete data[key];\n }\n return { data, operations };\n}\nfunction restoreWrittenChanges(before, expected, current, reportFields = false) {\n const reverse = diffChanges(expected, before);\n const restore = [];\n for (const op of reverse) {\n if (assertUnchangedPaths(before, current, [op]).ok) continue;\n const guard = assertUnchangedPaths(expected, current, [op], '自动恢复涉及的字段');\n if (!guard.ok) {\n if (reportFields && ['display_data', 'delta_data'].includes(patch.parsePointer(op.path)[0])) continue;\n throw new Error(guard.reason);\n }\n restore.push(op);\n }\n return mergeChanges(current, restore);\n}\n\nmodule.exports = { diffChanges, assertUnchangedPaths, assertMergeablePaths, mergeChanges, mergeMvuResult, restoreWrittenChanges };\n\n});\n\n__define('apply/mvu-adapter', function (module, exports, require) {\nconst contract = require('../contracts');\nconst util = require('../util');\nfunction waitGlobalInitialized(name, getGlobal, timeoutMs) {\n return new Promise((resolve, reject) => {\n const deadline = Date.now() + (timeoutMs || 5000);\n const tick = () => {\n const g = getGlobal && getGlobal();\n if (g && g[name] != null) { resolve(g[name]); return; }\n if (Date.now() > deadline) { reject(new Error('等待全局初始化超时: ' + name)); return; }\n setTimeout(tick, 50);\n };\n tick();\n });\n}\nfunction lookupMvu(win, parent, top) {\n const w = win || (typeof window !== 'undefined' ? window : null);\n if (w && w.Mvu) return w.Mvu;\n if (parent && parent.Mvu) return parent.Mvu;\n if (top && top.Mvu) return top.Mvu;\n return null;\n}\nfunction createMvuAdapter(opts = {}) {\n const getGlobal = opts.getGlobal || (() => (typeof window !== 'undefined' ? window : null));\n const getMvu = opts.getMvu || (() => lookupMvu(getGlobal(), getGlobal() && getGlobal().parent, getGlobal() && getGlobal().top));\n const resolveMvuMessageId = opts.resolveMvuMessageId || ((target) => target.messageId);\n const timeoutMs = opts.timeoutMs || 5000;\n const pendingTimeouts = new Set();\n let destroyed = false;\n\n function assertAlive() {\n if (destroyed) throw new Error('MVU 适配器已销毁');\n }\n\n function timeoutAfter(ms) {\n let entry;\n const promise = new Promise((resolve, reject) => {\n entry = { timer: null, reject };\n entry.timer = setTimeout(() => {\n pendingTimeouts.delete(entry);\n reject(new Error('等待全局初始化超时: ' + contract.MVU_GLOBAL_NAME));\n }, ms);\n pendingTimeouts.add(entry);\n });\n return {\n promise,\n cancel() {\n if (!entry || !pendingTimeouts.has(entry)) return;\n clearTimeout(entry.timer);\n pendingTimeouts.delete(entry);\n },\n };\n }\nasync function waitUntilReady() {\n assertAlive();\n const globalObject = getGlobal && getGlobal();\n const officialWait = opts.waitGlobal || (globalObject && globalObject.waitGlobalInitialized);\n if (typeof officialWait === 'function') {\n const timeout = timeoutAfter(timeoutMs);\n try {\n await Promise.race([\n Promise.resolve().then(() => officialWait.call(globalObject, contract.MVU_GLOBAL_NAME)),\n timeout.promise,\n ]);\n } finally {\n timeout.cancel();\n }\n } else if (!getMvu()) {\n // 正式宿主必须提供等待器;若 Mvu 已经存在则无需轮询,否则明确失败。\n throw new Error('酒馆助手缺少 waitGlobalInitialized,且 Mvu 尚未初始化');\n }\n assertAlive();\n if (!getMvu()) throw new Error('MVU 初始化完成但接口不可访问');\n }\n\n return {\n async ready() {\n await waitUntilReady();\n },\ngetUpdateEventNames() {\n if (destroyed) return [];\n const events = getMvu() && getMvu().events;\n return events && typeof events.VARIABLE_UPDATE_ENDED === 'string' ? [events.VARIABLE_UPDATE_ENDED] : [];\n },\n\n async read(target) {\n assertAlive();\n const mvu = getMvu();\n if (!mvu || typeof mvu.getMvuData !== 'function') {\n throw new Error('MVU 不可用或缺少 getMvuData 接口');\n }\n const data = mvu.getMvuData({ type: 'message', message_id: resolveMvuMessageId(target) });\n if (!data || typeof data !== 'object') throw new Error('MVU read 返回非对象');\n return data;\n },\n\n async parse(updateText, oldData) {\n assertAlive();\n const mvu = getMvu();\n if (!mvu || typeof mvu.parseMessage !== 'function') {\n throw new Error('MVU 不可用或缺少 parseMessage 接口');\n }\n // 禁止传空对象模拟重演\n if (!oldData || typeof oldData !== 'object' || Object.keys(oldData).length === 0) {\n throw new Error('禁止向 parseMessage 传空对象');\n }\n // 隔离解析入参引用;这不能阻止 MVU 事件监听器自行更新宿主,所以写前仍须复核目标路径。\n const result = await mvu.parseMessage(updateText, util.deepClone(oldData));\n if (!result || typeof result !== 'object') throw new Error('MVU parseMessage 返回非对象(可能被 Zod 拒绝)');\n return result;\n },\n\n async replace(target, value) {\n assertAlive();\n const mvu = getMvu();\n if (!mvu || typeof mvu.replaceMvuData !== 'function') {\n throw new Error('MVU 不可用或缺少 replaceMvuData 接口');\n }\n await mvu.replaceMvuData(value, { type: 'message', message_id: resolveMvuMessageId(target) });\n },\n\n destroy() {\n if (destroyed) return;\n destroyed = true;\n for (const entry of pendingTimeouts) {\n clearTimeout(entry.timer);\n entry.reject(new Error('MVU 适配器已销毁'));\n }\n pendingTimeouts.clear();\n },\n };\n}\n\nmodule.exports = { createMvuAdapter, waitGlobalInitialized, lookupMvu };\n\n});\n\n__define('apply/operation-edits', function (module, exports, require) {\nconst util = require('../util');\nconst patch = require('../data/patch');\nconst concurrency = require('./concurrency');\nconst owns = (value, key) => value != null && Object.prototype.hasOwnProperty.call(value, key);\nconst related = (a, b) => a === b || a.startsWith(b + '/') || b.startsWith(a + '/') || !a || !b;\n\nfunction valueAt(data, path) {\n let value = data;\n for (const key of patch.parsePointer(path)) {\n if (!owns(value, key)) return { exists: false };\n value = value[key];\n }\n return value === undefined ? { exists: false } : { exists: true, value: util.deepClone(value) };\n}\nfunction describe(before, after, paths) {\n return concurrency.diffChanges(before, after).filter(op => !paths || paths.some(p => related(p.path, op.path)))\n .map(op => ({ path: op.path, before: valueAt(before, op.path), after: valueAt(after, op.path) }));\n}\n\nfunction operationChanges(before, after, paths = {}) {\n const changes = { version: 1 };\n for (const [key, store] of [['mvu', 'messageVariables'], ['chat', 'chatVariables']]) {\n if (after[store] !== undefined) changes[key] = describe(before[store], after[store], paths[key]);\n }\n return changes;\n}\nfunction intent(before, ops) {\n const data = util.deepClone(before);\n const insert = op => {\n const segments = patch.parsePointer(op.path);\n const parent = patch.getPath(data, segments.slice(0, -1));\n if (!Array.isArray(parent)) return patch.applyOp(data, op);\n const key = segments[segments.length - 1];\n const index = key === '-' ? parent.length : Number(key);\n if (!Number.isInteger(index) || index < 0 || index > parent.length) throw new Error('数组插入位置无效:' + op.path);\n parent.splice(index, 0, util.deepClone(op.value));\n };\n for (const op of ops) {\n if (op.op === 'move') {\n if (op.path.startsWith(op.from + '/')) throw new Error('不能把字段移进自身子项');\n const source = valueAt(data, op.from);\n if (!source.exists) throw new Error('移动来源不存在:' + op.from);\n patch.applyOp(data, { op: 'remove', path: op.from });\n insert({ op: 'insert', path: op.path, value: source.value });\n } else if (op.op === 'insert') insert(op);\n else patch.applyOp(data, op);\n }\n return data;\n}\nfunction boundary(path, trees) {\n const segments = patch.parsePointer(path);\n let values = trees;\n for (let i = 0; i < segments.length; i++) {\n if (values.some(v => !util.isPlainObject(v))) return patch.joinPointer(segments.slice(0, i));\n values = values.map(v => owns(v, segments[i]) ? v[segments[i]] : undefined);\n }\n return path;\n}\nasync function resolveChanges(before, after, current, paths, resolve, label, atomic = false) {\n const selected = [];\n for (const op of paths || concurrency.diffChanges(before, after)) {\n for (const path of [op.path, ...(op.from ? [op.from] : [])]) {\n const expanded = boundary(path, [before, after, current]);\n if (selected.some(p => expanded === p || expanded.startsWith(p + '/') || !p)) continue;\n for (let i = selected.length - 1; i >= 0; i--) if (selected[i].startsWith(expanded + '/') || !expanded) selected.splice(i, 1);\n selected.push(expanded);\n }\n }\n const changes = selected.map(path => ({ path, before: valueAt(before, path), after: valueAt(after, path), current: valueAt(current, path) }))\n .filter(c => !util.deepEqual(c.current, c.after));\n const conflicts = changes.filter(c => !util.deepEqual(c.before, c.current));\n let choices = [];\n if (conflicts.length) {\n if (typeof resolve !== 'function') throw new Error((label || '变更路径') + '已被其它脚本修改:' + conflicts[0].path);\n choices = await resolve(conflicts.map((c, id) => ({ id, label, path: c.path,\n original: c.before, current: c.current, proposed: c.after, atomic })));\n if (!Array.isArray(choices) || choices.length !== conflicts.length || choices.some(c => !['overwrite', 'keep'].includes(c))) {\n throw new Error('已取消冲突处理,未继续写入');\n }\n if (atomic && choices.some(c => c !== choices[0])) throw new Error('移动来源与去向必须采用同一种选择');\n }\n const kept = new Set(conflicts.filter((c, i) => choices[i] === 'keep').map(c => c.path));\n const pathsToWrite = selected.filter(path => !(atomic && kept.size) && !kept.has(path)).map(path => ({ path }));\n const ops = changes.filter(c => pathsToWrite.some(p => p.path === c.path))\n .map(c => c.after.exists ? { op: c.current.exists ? 'replace' : 'insert', path: c.path, value: c.after.value } : { op: 'remove', path: c.path });\n return { data: concurrency.mergeChanges(current, ops), ops, paths: pathsToWrite,\n kept: selected.filter(path => !pathsToWrite.some(p => p.path === path)),\n overrides: conflicts.filter((c, i) => choices[i] === 'overwrite').map(c => ({ path: c.path, state: c.current })) };\n}\nasync function resolveLive(read, before, after, paths, resolve, label) {\n let current = await read();\n const overrides = [];\n const kept = [];\n for (;;) {\n let asked = false;\n const result = await resolveChanges(before, after, current, paths, typeof resolve === 'function'\n ? async rows => { asked = true; return resolve(rows); } : undefined, label);\n overrides.push(...result.overrides); kept.push(...result.kept);\n result.overrides = overrides.slice(); result.kept = kept.slice();\n if (!asked) return Object.assign(result, { current });\n const latest = await read();\n if (concurrency.assertMergeablePaths(current, result.data, latest, result.paths).ok) {\n return Object.assign(result, { current: latest, data: concurrency.mergeChanges(latest, result.ops) });\n }\n // 只在用户实际处理冲突之后再问;没有定时器、没有后台重试,也不重演 MVU。\n before = current; after = result.data; paths = result.paths; current = latest;\n }\n}\nfunction amendBefore(before, overrides) {\n let value = util.deepClone(before);\n for (const { path, state } of overrides || []) {\n if (!path) value = state.exists ? util.deepClone(state.value) : undefined;\n else patch.applyOp(value, state.exists ? { op: 'replace', path, value: state.value } : { op: 'remove', path });\n }\n return value;\n}\n\nfunction copyReports(parsed, data) {\n for (const key of ['display_data', 'delta_data']) {\n if (owns(parsed, key)) data[key] = util.deepClone(parsed[key]);\n else delete data[key];\n }\n return data;\n}\nasync function resolveMvu(read, before, parsed, resolve, requested = []) {\n const paths = concurrency.diffChanges(before, parsed).filter(op => !['display_data', 'delta_data'].includes(patch.parsePointer(op.path)[0])).concat(requested);\n const result = await resolveLive(async () => {\n const current = await read();\n const rules = concurrency.assertUnchangedPaths(before, current, [{ path: '/schema' }], '角色卡校验规则');\n if (!rules.ok) throw new Error(rules.reason);\n return current;\n }, before, parsed, paths, resolve, '规则处理后的变量');\n result.data = copyReports(parsed, result.data);\n return result;\n}\nasync function reverse(current, changes, resolve, label) {\n const before = util.deepClone(current);\n const after = util.deepClone(current);\n const paths = [];\n for (const c of changes || []) {\n if (!c || typeof c.path !== 'string' || !c.before || !c.after || patch.parsePointer(c.path).some(k => !util.isSafeKey(k))) {\n throw new Error('操作回滚记录无效,请使用明确标注的完整快照恢复');\n }\n paths.push({ path: c.path });\n // 创建缺失祖先只用于比较树;实际合并的父结构由 resolveChanges 展示确认。\n for (const [tree, state] of [[before, c.after], [after, c.before]]) {\n if (!c.path) throw new Error('操作回滚记录不能包含整棵变量树');\n patch.applyOp(tree, state.exists ? { op: 'replace', path: c.path, value: state.value } : { op: 'remove', path: c.path });\n }\n }\n return resolveChanges(before, after, current, paths, resolve, label);\n}\n\nmodule.exports = { valueAt, describe, operationChanges, intent, resolveChanges, resolveLive, resolveMvu,\n amendBefore, copyReports, reverse };\n\n});\n\n__define('apply/reincarnation', function (module, exports, require) {\nconst field = require('../data/field-model');\nfunction storeFromVueApp(app) {\n if (!app || !app._context) return null;\n const provides = app._context.provides || {};\n let keys = [];\n try { keys = Reflect.ownKeys(provides); } catch (e) { keys = Object.keys(provides); }\n for (const key of keys) {\n const pinia = provides[key];\n if (!pinia || !pinia._s) continue;\n const store = typeof pinia._s.get === 'function' ? pinia._s.get('character') : pinia._s.character;\n if (store && (store.character !== undefined || typeof store.updateCharacterField === 'function')) return store;\n }\n return null;\n}\nfunction findInDocument(doc) {\n if (!doc || typeof doc.querySelectorAll !== 'function') return null;\n let nodes;\n try { nodes = doc.querySelectorAll('#app,[data-v-app],.creator-app'); }\n catch (e) { return null; }\n for (const node of nodes) {\n const store = storeFromVueApp(node && node.__vue_app__);\n if (store) return store;\n }\n return null;\n}\nfunction detectCreatorApp(getGlobal) {\n const first = getGlobal && getGlobal();\n if (!first) return null;\n const directNames = ['creatorApp', 'CreativeWorkshop'];\n for (const name of directNames) {\n const candidate = first[name];\n if (candidate && (candidate.character !== undefined || typeof candidate.updateCharacterField === 'function')) return candidate;\n }\n\n const queue = [first];\n try { if (first.parent && first.parent !== first) queue.push(first.parent); } catch (e) { /* 跨域 */ }\n try { if (first.top && !queue.includes(first.top)) queue.push(first.top); } catch (e) { /* 跨域 */ }\n const seen = [];\n while (queue.length && seen.length < 64) {\n const win = queue.shift();\n if (!win || seen.includes(win)) continue;\n seen.push(win);\n try {\n const store = findInDocument(win.document);\n if (store) return store;\n const frames = win.frames ? Array.from(win.frames) : [];\n for (const frame of frames) if (!seen.includes(frame)) queue.push(frame);\n } catch (e) { /* 仅忽略不可访问 frame */ }\n }\n return null;\n}\nfunction readReincarnationPoints(params = {}) {\n const creator = detectCreatorApp(params.getGlobal);\n if (!creator) return { found: false, value: null };\n let value;\n try { value = creator.character && creator.character.reincarnationPoints; } catch (e) { /* 继续兜底 */ }\n try {\n if (value === undefined && creator.character && creator.character.value) {\n value = creator.character.value.reincarnationPoints;\n }\n } catch (e) { /* 继续兜底 */ }\n if (value === undefined) value = creator.reincarnationPoints;\n const number = typeof value === 'number' ? value : Number(value);\n return { found: true, value: Number.isFinite(number) ? number : null };\n}\nasync function setReincarnationPoints(params) {\n const points = params.points;\n if (typeof points !== 'number' || !Number.isInteger(points) || points < 0) {\n return { ok: false, reason: '转生点必须为非负整数' };\n }\n const creator = detectCreatorApp(params.getGlobal);\n if (!creator) return { ok: false, reason: '未检测到 creator-app(无法安全更新转生点)' };\n const value = points;\n try {\n if (typeof creator.updateCharacterField === 'function') {\n await creator.updateCharacterField(field.REINCARNATION_POINTS, value);\n } else if (creator.character && creator.character.value) {\n creator.character.value[field.REINCARNATION_POINTS] = value;\n } else if (creator.character) {\n creator.character[field.REINCARNATION_POINTS] = value;\n } else {\n creator[field.REINCARNATION_POINTS] = value;\n }\n return { ok: true, value };\n } catch (e) {\n return { ok: false, reason: '更新转生点失败: ' + (e && e.message || e) };\n }\n}\n\nmodule.exports = {\n detectCreatorApp,\n readReincarnationPoints,\n setReincarnationPoints,\n storeFromVueApp,\n REINCARNATION_POINTS: field.REINCARNATION_POINTS,\n};\n\n});\n\n__define('apply/transaction', function (module, exports, require) {\nconst util = require('../util');\nconst bodyHistory = require('./body-history');\nconst concurrency = require('./concurrency');\nfunction createBackup(params) {\n return {\n createdAt: Date.now(),\n target: util.deepClone(params.target),\n cardHash: params.cardHash || '',\n version: params.version || '',\n messageVariables: util.deepClone(params.messageVariables || {}),\n chatVariables: util.deepClone(params.chatVariables || {}),\n messageText: params.messageText,\n };\n}\nfunction prepareHistoryBackup(backup, options = {}) {\n const scopes = Object.assign({ mvu: true, chat: true, text: false }, options.scopes);\n const entry = Object.assign({}, backup, { historyFormat: 2, scopes });\n delete entry.messageText;\n if (!scopes.mvu) delete entry.messageVariables;\n if (!scopes.chat) delete entry.chatVariables;\n if (scopes.text) {\n if (typeof backup.messageText !== 'string' || typeof options.nextMessageText !== 'string') {\n throw new Error('指令块备份缺少修改前后正文,已停止写入');\n }\n entry.bodyPatchHistory = bodyHistory.createBodyPatchHistory(backup.messageText, options.nextMessageText);\n }\n return entry;\n}\nfunction getBackupScope(backup) {\n const entry = backup || {};\n const modern = entry.historyFormat === 2;\n const scopes = modern ? Object.assign({ mvu: false, chat: false, text: false }, entry.scopes) : {\n mvu: entry.messageVariables !== undefined,\n chat: entry.chatVariables !== undefined,\n text: false,\n };\n const partial = !!entry.bodyOmitted || !modern && (entry.messageText !== undefined || entry.mode === 'body' || /正文|声明/.test(entry.label || ''));\n const labels = [];\n if (scopes.mvu) labels.push('消息变量快照');\n if (scopes.chat) labels.push('聊天模板变量');\n if (scopes.text) labels.push('正文指令块(不回滚剧情)');\n return { scopes, partial, scopeLabel: labels.join('、') || '无可恢复内容',\n partialReason: entry.bodyOmitted ? '当时历史容量不足,仅保存了变量快照;此记录不能恢复正文中的变量指令。'\n : partial ? '旧记录缺少完整的指令块前后记录,只能恢复已保存的变量,不能恢复正文指令。' : '' };\n}\n\nfunction errorText(error) {\n return error && error.message ? error.message : String(error);\n}\nasync function compensateWrites(params) {\n const { host, mvu, target, backup, written, expected } = params;\n const failed = [];\n const sameTarget = async () => !host || typeof host.verifyTargetIdentity !== 'function' || await host.verifyTargetIdentity(target);\n if (!await sameTarget()) {\n return ['目标身份已变化,未执行可能写错聊天的自动回滚'];\n }\n if (written.text && backup && backup.messageText !== undefined) {\n try {\n let restored = backup.messageText;\n if (expected && expected.messageText !== undefined) {\n const current = await host.readMessageText(target);\n restored = bodyHistory.sameInstructions(current, backup.messageText) ? current : bodyHistory.restoreBodyPatchHistory(current,\n bodyHistory.createBodyPatchHistory(backup.messageText, expected.messageText));\n }\n if (!await sameTarget()) throw new Error('目标身份已变化');\n await host.replaceMessageText(target, restored); written.text = false;\n }\n catch (e) { failed.push('正文回滚: ' + errorText(e)); }\n }\n if (written.chat && backup) {\n try {\n const restored = expected && expected.chatVariables !== undefined\n ? concurrency.restoreWrittenChanges(backup.chatVariables, expected.chatVariables, await host.readChatVariables(target.chatId))\n : util.deepClone(backup.chatVariables);\n if (!await sameTarget()) throw new Error('目标身份已变化');\n await host.replaceChatVariables(target.chatId, restored); written.chat = false;\n }\n catch (e) { failed.push('聊天变量回滚: ' + errorText(e)); }\n }\n if (written.mvu && backup) {\n try {\n const restored = expected && expected.messageVariables !== undefined\n ? concurrency.restoreWrittenChanges(backup.messageVariables, expected.messageVariables, await mvu.read(target), true)\n : util.deepClone(backup.messageVariables);\n if (!await sameTarget()) throw new Error('目标身份已变化');\n await mvu.replace(target, restored);\n written.mvu = false;\n }\n catch (e) { failed.push('MVU 回滚: ' + errorText(e)); }\n }\n return failed;\n}\n\nfunction isFullyApplied(report) {\n return !!report && report.ok === true;\n}\nasync function verifyStoredState(params) {\n const { host, mvu, target, expected } = params;\n const paths = params.paths || {};\n const check = (before, actual, operations, label) => {\n if (!util.isPlainObject(actual) || util.isPlainObject(before.stat_data) && !util.isPlainObject(actual.stat_data)) {\n return { ok: false, reason: label + '读取结果无效' };\n }\n if (operations !== undefined) return concurrency.assertUnchangedPaths(before, actual, operations, label);\n return util.deepEqual(before, actual) ? { ok: true } : { ok: false, reason: label + '已变化' };\n };\n try {\n const verify = host.verifyTargetIdentity || host.verifyTarget;\n if (!await verify.call(host, target)) throw new Error('目标聊天、楼层或 swipe 已变化');\n if (expected.messageVariables !== undefined) {\n const checked = check(expected.messageVariables, await mvu.read(target), paths.mvu, '消息变量');\n if (!checked.ok) throw new Error(checked.reason);\n }\n if (expected.chatVariables !== undefined) {\n const checked = check(expected.chatVariables, await host.readChatVariables(target.chatId), paths.chat, '聊天模板变量');\n if (!checked.ok) throw new Error(checked.reason);\n }\n if (expected.messageText !== undefined) {\n const text = await host.readMessageText(target);\n if (paths.text) bodyHistory.restoreBodyPatchHistory(text, paths.text);\n else if (!bodyHistory.sameInstructions(text, expected.messageText)) throw new Error('正文指令已变化');\n }\n if (!await verify.call(host, target)) throw new Error('目标聊天、楼层或 swipe 已变化');\n return { ok: true };\n } catch (e) {\n return { ok: false, reason: errorText(e) };\n }\n}\nasync function notifyAndVerify(params) {\n const { host, target } = params;\n const expected = util.deepClone(params.expected || {});\n const warnings = [];\n try {\n if (typeof host.notifyMessageEdited === 'function') await host.notifyMessageEdited(target.messageId);\n } catch (e) { warnings.push('宿主消息刷新通知失败: ' + errorText(e)); }\n const checked = await verifyStoredState(Object.assign({}, params, { expected }));\n return checked.ok ? { ok: true, warnings } : { ok: false, warnings,\n reason: '写入后通知期间状态冲突: ' + checked.reason + ';未自动覆盖外部修改,请刷新并检查历史' };\n}\n\nmodule.exports = { createBackup, prepareHistoryBackup, getBackupScope, compensateWrites, isFullyApplied, verifyStoredState, notifyAndVerify };\n\n});\n\n__define('contracts', function (module, exports, require) {\nconst MVU_GLOBAL_NAME = 'Mvu';\nconst ROLE = Object.freeze({ USER: 'user', ASSISTANT: 'assistant' });\nconst OP = Object.freeze({\n REPLACE: 'replace',\n DELTA: 'delta',\n INSERT: 'insert',\n REMOVE: 'remove',\n MOVE: 'move',\n});\nconst APPLY_MODE = Object.freeze({ BODY: 'body', DIRECT: 'direct' });\nconst AUXILIARY_KEYS = Object.freeze(['date', 'calendar_float_store']);\n\nmodule.exports = {\n MVU_GLOBAL_NAME,\n ROLE,\n OP,\n APPLY_MODE,\n AUXILIARY_KEYS,\n};\n\n});\n\n__define('data/change-set', function (module, exports, require) {\nconst util = require('../util');\nfunction buildChangeSet(params) {\n const auxiliary = params.auxiliary || {};\n return {\n mvuOps: util.deepClone(params.mvuOps || []),\n auxiliary: {\n date: auxiliary.date === undefined ? null : util.deepClone(auxiliary.date),\n calendar: auxiliary.calendar === undefined ? null : util.deepClone(auxiliary.calendar),\n // 空对象也是有效的“清空全部聊天变量”,不能被 || null 吞掉。\n chatVariables: auxiliary.chatVariables === undefined || auxiliary.chatVariables === null\n ? null\n : util.deepClone(auxiliary.chatVariables),\n },\n summary: util.deepClone(params.summary || {}),\n };\n}\nfunction hasChange(cs) {\n if (!cs) return false;\n if (Array.isArray(cs.mvuOps) && cs.mvuOps.length > 0) return true;\n const a = cs.auxiliary;\n if (!a) return false;\n return a.date !== null || a.calendar !== null || a.chatVariables !== null;\n}\nfunction mergeOps(existing, userOps) {\n const result = util.deepClone(existing || []);\n for (const u of userOps || []) {\n const idx = result.findIndex((r) => r.path === u.path);\n if (idx >= 0) result.splice(idx, 1); // 同路径以用户操作为准\n result.push(util.deepClone(u));\n }\n return result;\n}\n\nmodule.exports = { buildChangeSet, hasChange, mergeOps };\n\n});\n\n__define('data/field-model', function (module, exports, require) {\nconst STATUS_EFFECT_TYPES = Object.freeze(['增益', '减益', '特殊']);\nconst NEWS_COLUMNS = Object.freeze(['阿斯塔利亚快讯', '酒馆留言板', '午后茶会']);\nconst NEWS_COUNT = Object.freeze({ '阿斯塔利亚快讯': 5, '酒馆留言板': 5, '午后茶会': 4 });\nconst EVENT_FIELD = Object.freeze({\n OPEN: '开启',\n END: '结束',\n TITLE: '标题',\n PHASE: '阶段',\n DONE: '已完成事件',\n SIGNAL: '信号',\n});\nconst EXP_REQUIRED_MAX = 'MAX';\nconst LEVEL_MIN = 1;\nconst LEVEL_MAX = 25;\nconst AFFINITY_MIN = -100;\nconst AFFINITY_MAX = 100;\nconst REINCARNATION_POINTS = 'reincarnationPoints';\nfunction validateExpRequired(v) {\n if (typeof v === 'number' && Number.isFinite(v) && v >= 0) return { ok: true };\n if (v === EXP_REQUIRED_MAX) return { ok: true };\n return { ok: false, reason: '升级所需经验必须是 number 或 \"MAX\"' };\n}\nfunction validateLevel(v) {\n if (typeof v !== 'number' || !Number.isFinite(v)) return { ok: false, reason: '等级必须是有限数字' };\n if (v < LEVEL_MIN || v > LEVEL_MAX) return { ok: false, reason: `等级须在 ${LEVEL_MIN}~${LEVEL_MAX}` };\n return { ok: true };\n}\nfunction validateStatusEffectType(v) {\n if (STATUS_EFFECT_TYPES.includes(v)) return { ok: true };\n return { ok: false, reason: `状态效果类型必须是:${STATUS_EFFECT_TYPES.join(' / ')}` };\n}\nfunction dedupeTags(tags) {\n if (!Array.isArray(tags)) return [];\n return Array.from(new Set(tags.map((t) => String(t))));\n}\nfunction validateTags(tags) {\n if (!Array.isArray(tags)) return { ok: false, reason: '标签必须是数组' };\n const deduped = dedupeTags(tags);\n if (deduped.length !== tags.length) return { ok: false, reason: '标签存在重复' };\n for (const t of tags) if (!String(t).trim()) return { ok: false, reason: '标签不能为空字符串' };\n return { ok: true };\n}\nfunction validateQuantity(v) {\n if (typeof v !== 'number' || !Number.isFinite(v)) return { ok: false, reason: '数量必须是数字' };\n if (v < 0) return { ok: false, reason: '数量不能小于 0(设为 0 将由角色卡清理该项目)' };\n return { ok: true };\n}\nfunction isReadonlyPath(path) {\n return path.split('/').some((s) => s.length && s.charAt(0) === '_');\n}\nfunction validateEditor(statData, original) {\n const errors = [];\n if (!statData || typeof statData !== 'object') return { ok: false, errors: ['无 stat_data 数据'] };\n const patch = require('./patch');\n const paths = original ? patch.diffStatData(original, statData).map(op => op.path) : null;\n const relevant = segments => {\n const path = patch.joinPointer(segments);\n return !paths || paths.some(p => p === path || p.startsWith(path + '/') || path.startsWith(p + '/'));\n };\n const add = (segments, message) => { if (relevant(segments)) errors.push(message); };\n\n function validateEntity(entity, label, prefix) {\n if (!entity || typeof entity !== 'object') return;\n if (!relevant(prefix)) return;\n const level = validateLevel(entity['等级']);\n if (!level.ok) add(prefix.concat('等级'), label + '等级:' + level.reason);\n ['生命值', '法力值', '体力值'].forEach((key) => {\n const value = entity[key];\n if (!value || typeof value !== 'object') return;\n if (typeof value['当前'] !== 'number' || !Number.isFinite(value['当前'])) add(prefix.concat(key, '当前'), label + key + '.当前须为有限数字');\n const upper = value['上限'];\n if (!upper || typeof upper !== 'object' || typeof upper['额外'] !== 'number' || !Number.isFinite(upper['额外'])) {\n add(prefix.concat(key, '上限', '额外'), label + key + '.上限.额外须为有限数字');\n }\n });\n Object.entries(entity['状态效果'] || {}).forEach(([name, status]) => {\n if (status && typeof status === 'object' && !validateStatusEffectType(status['类型']).ok) {\n add(prefix.concat('状态效果', name, '类型'), label + '状态效果[' + name + '].类型须为 增益/减益/特殊');\n }\n });\n ['背包', '装备', '技能', '资产'].forEach((category) => {\n Object.entries(entity[category] || {}).forEach(([name, item]) => {\n if (!item || typeof item !== 'object') return;\n if (item['数量'] !== undefined) {\n const quantity = validateQuantity(item['数量']);\n if (!quantity.ok) add(prefix.concat(category, name, '数量'), label + category + '[' + name + '].数量:' + quantity.reason);\n }\n if (item['标签'] !== undefined) {\n const tags = validateTags(item['标签']);\n if (!tags.ok) add(prefix.concat(category, name, '标签'), label + category + '[' + name + '].标签:' + tags.reason);\n }\n });\n });\n }\n\n const hero = statData['主角'];\n if (hero) {\n validateEntity(hero, '主角', ['主角']);\n const ex = validateExpRequired(hero['升级所需经验']);\n if (!ex.ok) add(['主角', '升级所需经验'], '升级所需经验:' + ex.reason);\n }\n const rel = statData['关系列表'] || {};\n Object.entries(rel).forEach(([n, npc]) => {\n if (npc && typeof npc === 'object') {\n validateEntity(npc, 'NPC[' + n + ']', ['关系列表', n]);\n const aff = npc['好感度'];\n if (typeof aff !== 'number' || !Number.isFinite(aff) || aff < AFFINITY_MIN || aff > AFFINITY_MAX) {\n add(['关系列表', n, '好感度'], 'NPC[' + n + '].好感度须在 ' + AFFINITY_MIN + '~' + AFFINITY_MAX + ' 之间');\n }\n }\n });\n const signals = statData['事件'] && statData['事件']['信号'];\n if (signals !== undefined && (!Array.isArray(signals) || signals.some((item) => typeof item !== 'string' || !item.trim()))) {\n add(['事件', '信号'], '事件.信号必须是非空字符串数组');\n }\n return { ok: errors.length === 0, errors };\n}\n\nmodule.exports = {\n STATUS_EFFECT_TYPES,\n NEWS_COLUMNS,\n NEWS_COUNT,\n EVENT_FIELD,\n EXP_REQUIRED_MAX,\n LEVEL_MIN,\n LEVEL_MAX,\n AFFINITY_MIN,\n AFFINITY_MAX,\n REINCARNATION_POINTS,\n validateExpRequired,\n validateLevel,\n validateStatusEffectType,\n dedupeTags,\n validateTags,\n validateQuantity,\n isReadonlyPath,\n validateEditor,\n};\n\n});\n\n__define('data/history', function (module, exports, require) {\nconst util = require('../util');\nconst { getBackupScope } = require('../apply/transaction');\nconst { createHistoryStore } = require('./history-store');\nconst DEFAULT_BYTES_BUDGET = 1024 * 1024;\n\nconst KEY = 'md_modifier_v2_14_history';\nconst LOG_KEY = 'md_modifier_v2_14_logs';\nconst INJECTION_KEY = 'md_modifier_v2_14_injections';\n\nconst MAX_LOGS = 200;\nconst MAX_INJECTIONS = 50;\nfunction getOperationState(entry) {\n const states = {\n pending: { label: '结果待确认', canRestore: true, detail: '仅确认写前备份已保存。若操作中断,请先核对实际数据,再决定是否恢复。' },\n applied: { label: '已应用', canRestore: true, detail: '' },\n 'not-applied': { label: '未应用', canRestore: false, detail: '本次在写入前停止,未改动数据,无需回滚。' },\n 'recovery-needed': { label: '未完整完成', canRestore: true, detail: '本次已尝试写入,请先核对实际数据;需要时可恢复这份写前备份。' },\n unknown: { label: '结果未记录', canRestore: true, detail: '此旧记录没有保存应用结果,不能据此判断当时是否修改成功。' },\n };\n const status = entry && Object.prototype.hasOwnProperty.call(states, entry.operationStatus) ? entry.operationStatus : 'unknown';\n return Object.assign({ status }, states[status]);\n}\nfunction isQuotaError(error) {\n return !!error && (error.name === 'QuotaExceededError' || error.name === 'NS_ERROR_DOM_QUOTA_REACHED' ||\n error.code === 22 || error.code === 1014 || /\\b(?:QuotaExceededError|NS_ERROR_DOM_QUOTA_REACHED)\\b/.test(error.message || ''));\n}\n\nfunction storageFailure(error) {\n const result = { ok: false, reason: error && error.message ? error.message : String(error) };\n if (isQuotaError(error)) result.quotaExceeded = true;\n return result;\n}\n\nfunction trimHistory(records, limit, bytesBudget) {\n let bytes = 2;\n const keep = [];\n for (const entry of records) {\n if (keep.length >= limit) break;\n bytes += approximateBytes(entry) + (keep.length ? 1 : 0);\n if (bytes > bytesBudget) break;\n keep.push(entry);\n }\n return keep;\n}\nfunction additionPlan(records, entry, limit, budget, dropCount, allowCapacityFallback) {\n if (approximateBytes([entry]) > budget) return allowCapacityFallback ? null : capacityConsent();\n const previous = records.filter((item) => !entry.operationId || item.operationId !== entry.operationId);\n const entries = trimHistory([entry].concat(previous), limit, budget);\n entries.length = Math.max(1, entries.length - dropCount);\n const evicted = previous.length - entries.length + 1;\n if (!allowCapacityFallback && entries.length < Math.min(previous.length + 1, limit)) return capacityConsent();\n const warnings = [];\n if (evicted) warnings.push('历史容量限制:已按先后顺序淘汰 ' + evicted + ' 条最旧记录');\n if (entry.bodyOmitted) warnings.push('历史容量不足:本次仅保存变量快照,不能回滚正文指令块');\n return { entries, result: { ok: true, persisted: true, bodyOmitted: !!entry.bodyOmitted, warnings,\n historyBackup: util.deepClone(entry) } };\n}\nfunction capacityConsent() {\n return { result: { ok: false, needsConfirmation: true, reason: '历史存储空间不足,需要确认备份降级后才能继续' } };\n}\n\nfunction snapshotOnly(entry) {\n if (!entry || entry.bodyPatchHistory === undefined) return null;\n const scopes = getBackupScope(entry).scopes;\n if (!scopes.mvu && !scopes.chat) return null;\n const copy = util.deepClone(entry);\n delete copy.bodyPatchHistory;\n if (copy.messageVariables === undefined && copy.chatVariables === undefined) return null;\n copy.scopes = Object.assign({}, scopes, { text: false });\n copy.bodyOmitted = true;\n return copy;\n}\n\nfunction unsavedHistory(entry, records = []) {\n const previousBackupRetained = !!(entry && entry.operationId && records.some((item) => item.operationId === entry.operationId));\n const warnings = ['历史存储容量不足:本次修改没有持久回滚备份;已有历史未被清空'];\n if (previousBackupRetained) warnings.push('本次写入前的最新快照未保存;历史里同次操作的记录只是较早准备阶段快照');\n return { ok: true, persisted: false, bodyOmitted: !!entry && entry.bodyPatchHistory !== undefined,\n historyBackup: null, previousBackupRetained, warnings };\n}\nfunction createHistory(opts = {}) {\n if (opts.store) return createSyncHistory(opts);\n return createNativeHistory(opts);\n}\n\nfunction createSyncHistory(opts) {\n const store = opts.store;\n const bytesBudget = opts.bytesBudget || DEFAULT_BYTES_BUDGET;\n let historyLimit = Math.min(100, Math.max(1, Number(opts.historyLimit) || 20));\n\n function readKey(storageKey, strict = false) {\n try {\n const raw = store.get(storageKey);\n if (!raw) return [];\n const arr = JSON.parse(raw);\n if (!Array.isArray(arr) && strict) throw new Error('历史存储格式损坏:记录不是数组');\n return Array.isArray(arr) ? arr : [];\n } catch (e) { if (strict) throw e; return []; }\n }\n\n function writeKey(storageKey, arr) {\n try {\n store.set(storageKey, JSON.stringify(arr));\n return { ok: true };\n } catch (e) {\n return storageFailure(e);\n }\n }\n\n function readAll() {\n return readKey(KEY);\n }\n\n function writeAll(arr) {\n // 容量不足只淘汰最旧的整条记录,不删单条记录的正文或变量后宣称完整成功。\n let keep = arr.slice();\n let res = writeKey(KEY, keep);\n while (!res.ok && res.quotaExceeded && keep.length > 1) {\n keep.pop();\n res = writeKey(KEY, keep);\n }\n return res;\n }\n\n function trimRecords(arr, limit) {\n return trimHistory(arr, limit, bytesBudget);\n }\n\n return {\n add(entry, options = {}) {\n let cloned;\n let arr;\n try { cloned = util.deepClone(entry); arr = readKey(KEY, true); }\n catch (error) { return storageFailure(error); }\n if (!cloned || typeof cloned !== 'object' || Array.isArray(cloned)) return { ok: false, reason: '历史记录必须是对象' };\n if (cloned && cloned.messageText !== undefined) {\n return { ok: false, reason: '历史不得保存剧情全文,请先生成指令块备份' };\n }\n // 每次失败的 set 不改源值;直到找到可保存的新恢复点,才原子替换旧列表。\n for (let stage = 0; stage < 2; stage++) {\n const candidate = stage === 0 ? cloned : snapshotOnly(cloned);\n if (!candidate) continue;\n for (let drop = 0; ; drop++) {\n const plan = additionPlan(arr, candidate, historyLimit, bytesBudget, drop, options.allowCapacityFallback === true);\n if (!plan) break;\n if (!plan.result.ok) return plan.result;\n const result = writeKey(KEY, plan.entries);\n if (result.ok) return plan.result;\n if (!result.quotaExceeded) return result;\n if (!options.allowCapacityFallback) return capacityConsent().result;\n if (plan.entries.length === 1) break;\n }\n }\n return unsavedHistory(cloned, arr);\n },\n\n list() { return readAll().map((entry) => Object.assign({}, entry, getBackupScope(entry))); },\ncomplete(operationId, outcome = {}) {\n if (!operationId || !['applied', 'not-applied', 'recovery-needed'].includes(outcome.status)) {\n return { ok: false, reason: '历史结果标识无效' };\n }\n let records;\n try { records = readKey(KEY, true); } catch (error) { return storageFailure(error); }\n const entry = records.find((item) => item.operationId === operationId);\n if (!entry) return { ok: false, reason: '对应的恢复记录已不存在' };\n entry.operationStatus = outcome.status;\n entry.completedAt = Date.now();\n if (outcome.operationChanges) entry.operationChanges = util.deepClone(outcome.operationChanges);\n if (outcome.bodyPatchHistory && !entry.bodyOmitted) entry.bodyPatchHistory = util.deepClone(outcome.bodyPatchHistory);\n if (outcome.bodyPatchHistory === null) {\n delete entry.bodyPatchHistory;\n entry.scopes = Object.assign({}, getBackupScope(entry).scopes, { text: false });\n }\n if (outcome.reason) entry.outcomeReason = String(outcome.reason).slice(0, 400);\n else delete entry.outcomeReason;\n if (approximateBytes(records) > bytesBudget) return { ok: false, reason: '历史容量不足以保存操作结果标记' };\n return writeKey(KEY, records);\n },\n\n clear() {\n return writeKey(KEY, []);\n },\n\n budgetBytes() { return bytesBudget; },\n\n historyLimit() { return historyLimit; },\nsetLimit(value) {\n const limit = Number(value);\n if (!Number.isInteger(limit) || limit < 1 || limit > 100) return { ok: false, reason: '历史容量必须是 1 至 100 的整数', limit: historyLimit };\n const records = readAll();\n const keep = trimRecords(records, limit);\n const result = keep.length === records.length ? { ok: true } : writeAll(keep);\n if (result.ok) historyLimit = limit;\n return Object.assign({}, result, { limit: historyLimit });\n },\n\n // 操作日志专区(最多 200 条)\n addLog(logEntry) {\n const logs = readKey(LOG_KEY);\n const item = Object.assign({ timestamp: Date.now() }, logEntry);\n logs.unshift(item);\n if (logs.length > MAX_LOGS) logs.length = MAX_LOGS;\n return writeKey(LOG_KEY, logs);\n },\n\n listLogs() { return readKey(LOG_KEY); },\n\n clearLogs() { return writeKey(LOG_KEY, []); },\n\n // 正文注入记录专区(最多 50 条)\n addInjection(injEntry) {\n const list = readKey(INJECTION_KEY);\n const item = Object.assign({ timestamp: Date.now() }, injEntry);\n list.unshift(item);\n if (list.length > MAX_INJECTIONS) list.length = MAX_INJECTIONS;\n return writeKey(INJECTION_KEY, list);\n },\n\n listInjections() { return readKey(INJECTION_KEY); },\n\n clearInjections() { return writeKey(INJECTION_KEY, []); },\n\n // 快照导出与导入\n exportSnapshot(snapshot) {\n if (!snapshot || typeof snapshot !== 'object') throw new Error('快照数据无效');\n const payload = {\n version: '2.14',\n exportedAt: Date.now(),\n target: snapshot.target || null,\n mvuData: snapshot.mvuData || null,\n date: snapshot.date || null,\n calendar: snapshot.calendar || null,\n chatVariables: snapshot.chatVariables || {},\n };\n return JSON.stringify(payload, null, 2);\n },\n\n importSnapshot(text) {\n if (!text || typeof text !== 'string') throw new Error('导入内容为空');\n let parsed;\n try { parsed = JSON.parse(text); }\n catch (e) { throw new Error('JSON 解析失败:' + (e.message || String(e))); }\n if (!parsed || typeof parsed !== 'object') throw new Error('导入数据非对象');\n let statData = null;\n let date = null;\n let calendar = null;\n let chatVariables = {};\n if (parsed.mvuData && typeof parsed.mvuData === 'object') {\n statData = parsed.mvuData.stat_data || parsed.mvuData;\n date = parsed.date;\n calendar = parsed.calendar;\n chatVariables = parsed.chatVariables || {};\n } else if (parsed.stat_data && typeof parsed.stat_data === 'object') {\n statData = parsed.stat_data;\n date = parsed.date;\n calendar = parsed.calendar || parsed.calendar_float_store;\n chatVariables = parsed.chatVariables || parsed.chat_variables || {};\n } else if (parsed['主角'] || parsed['世界']) {\n statData = parsed;\n } else {\n throw new Error('未识别出有效的角色数据或快照格式');\n }\n return {\n stat_data: statData,\n date,\n calendar,\n chatVariables,\n };\n },\n };\n}\nfunction approximateBytes(value) {\n try {\n const text = JSON.stringify(value);\n if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(text).length;\n if (typeof Buffer !== 'undefined') return Buffer.byteLength(text, 'utf8');\n return unescape(encodeURIComponent(text)).length;\n }\n catch (e) { return 0; }\n}\nfunction browserCapability(opts, key, nativeKey) {\n if (Object.prototype.hasOwnProperty.call(opts, key)) return opts[key];\n try { return typeof window !== 'undefined' ? window[nativeKey] : null; }\n catch (error) { return null; }\n}\n\nfunction createNativeHistory(opts) {\n const backend = createHistoryStore(browserCapability(opts, 'indexedDB', 'indexedDB'), opts.dbName);\n const bytesBudget = opts.bytesBudget || DEFAULT_BYTES_BUDGET;\n let limit = Math.min(100, Math.max(1, Number(opts.historyLimit) || 20));\n let cache = {};\n let initialized;\n let closed = false;\n let queue = Promise.resolve();\n const failure = storageFailure;\n const copy = (key) => util.deepClone(cache[key] || []);\n\n // 事务已结束不代表调用者仍存活;每次异步读回都先核对销毁状态。\n function acceptCache(data) {\n if (closed) throw new Error('历史存储已关闭');\n cache = data;\n }\n\n // 复用同一套容量/格式规则;此临时对象仅计算事务结果,不是持久化降级。\n function policy(entries = {}, historyLimit = limit) {\n const values = {};\n for (const key of Object.keys(entries)) values[key] = JSON.stringify(entries[key]);\n return {\n values,\n api: createSyncHistory(Object.assign({}, opts, {\n historyLimit,\n store: { get: (key) => values[key], set: (key, value) => { values[key] = value; } },\n })),\n };\n }\n\n async function migrateBucket(key, label) {\n const storage = browserCapability(opts, 'legacyStorage', 'localStorage');\n if (!storage) return { ok: true };\n let raw;\n try { raw = typeof storage.getItem === 'function' ? storage.getItem(key) : storage.get(key); }\n catch (error) { return { ok: true, warnings: [label + '无法读取,原存储未改动:' + error.message] }; }\n if (!raw) return { ok: true };\n let records;\n try { records = JSON.parse(raw); if (!Array.isArray(records)) throw new Error('记录不是数组'); }\n catch (error) { return { ok: true, warnings: [label + '格式无效,原存储未改动'] }; }\n let expected = [];\n const committed = await backend.mutate(key, (current) => {\n const eligible = records.filter((entry) => {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry) || entry.messageText !== undefined) return false;\n const target = entry.target;\n return key !== KEY || (target && target.chatId != null && Number.isInteger(target.messageId) && Number.isInteger(target.swipeId));\n });\n // marker 与记录原子提交已防止重复迁移;内容相同也可能是两次真实操作。\n const combined = current.concat(eligible);\n let entries;\n if (key === KEY) {\n const staged = policy({ [KEY]: combined });\n staged.api.setLimit(limit);\n entries = JSON.parse(staged.values[KEY]);\n } else entries = combined.slice(0, key === LOG_KEY ? MAX_LOGS : MAX_INJECTIONS);\n expected = entries.slice(current.length);\n const skipped = records.length - expected.length;\n return { entries, result: {\n ok: true,\n migration: { copied: expected.length, skipped, sourcePreserved: true },\n warnings: skipped ? [label + '有 ' + skipped + ' 条因格式、目标或容量限制未迁移;原 localStorage 记录完整保留'] : [],\n } };\n }, 'migration:' + key);\n const persisted = await backend.read();\n const actual = new Map();\n for (const entry of persisted[key] || []) {\n const text = JSON.stringify(entry);\n actual.set(text, (actual.get(text) || 0) + 1);\n }\n for (const entry of expected) {\n const text = JSON.stringify(entry);\n if (!actual.get(text)) throw new Error(label + '迁移读回核验失败,原存储未改动');\n actual.set(text, actual.get(text) - 1);\n }\n acceptCache(persisted);\n return committed.result;\n }\n\n function ready() {\n if (closed) return Promise.resolve(failure(new Error('历史存储已关闭')));\n if (!initialized) initialized = (async () => {\n try {\n acceptCache(await backend.read());\n const result = { ok: true, warnings: [], migrations: {} };\n for (const [key, label] of [[KEY, '旧历史'], [LOG_KEY, '旧操作日志'], [INJECTION_KEY, '旧正文注入记录']]) {\n const migrated = await migrateBucket(key, label);\n result.warnings.push(...(migrated.warnings || []));\n if (migrated.migration) result.migrations[key] = migrated.migration;\n }\n if (closed) throw new Error('历史存储已关闭');\n return result;\n } catch (error) { return failure(error); }\n })();\n return initialized;\n }\n\n function enqueue(work) {\n const next = queue.then(async () => {\n const state = await ready();\n if (!state.ok) return state;\n try { return await work(state); } catch (error) { return failure(error); }\n });\n queue = next.catch(() => {});\n return next;\n }\n\n const api = {\n ready,\n refresh: () => enqueue(async (state) => {\n acceptCache(await backend.read());\n return state;\n }),\n list: () => copy(KEY).map((entry) => Object.assign(entry, getBackupScope(entry))),\n listLogs: () => copy(LOG_KEY),\n listInjections: () => copy(INJECTION_KEY),\n budgetBytes: () => bytesBudget,\n historyLimit: () => limit,\n exportSnapshot: (snapshot) => policy().api.exportSnapshot(snapshot),\n importSnapshot: (text) => policy().api.importSnapshot(text),\n destroy: () => { closed = true; cache = {}; backend.destroy(); },\n };\n for (const [method, key] of Object.entries({ complete: KEY, clear: KEY, setLimit: KEY, addLog: LOG_KEY, clearLogs: LOG_KEY, addInjection: INJECTION_KEY, clearInjections: INJECTION_KEY })) {\n api[method] = (...args) => enqueue(async () => {\n const committed = await backend.mutate(key, (entries) => {\n const staged = policy({ [key]: entries });\n const result = staged.api[method](...args);\n return { entries: JSON.parse(staged.values[key]), result };\n });\n if (closed) return failure(new Error('历史存储已关闭'));\n cache[key] = committed.entries;\n if (method === 'setLimit' && committed.result.ok) limit = committed.result.limit;\n return committed.result;\n });\n }\n api.add = async (entry, options = {}) => {\n let cloned;\n try { cloned = util.deepClone(entry); }\n catch (error) { return failure(error); }\n if (!cloned || typeof cloned !== 'object' || Array.isArray(cloned)) return { ok: false, reason: '历史记录必须是对象' };\n if (cloned && cloned.messageText !== undefined) return { ok: false, reason: '历史不得保存剧情全文,请先生成指令块备份' };\n let lastReadRecords;\n const result = await enqueue(async () => {\n for (let stage = 0; stage < 2; stage++) {\n const candidate = stage === 0 ? cloned : snapshotOnly(cloned);\n if (!candidate) continue;\n for (let drop = 0; ; drop++) {\n let plan;\n try {\n // quota 重试也必须在新事务中读取最新记录,不能用上次失败的列表覆盖其它页面。\n const committed = await backend.mutate(KEY, (records) => {\n lastReadRecords = records;\n plan = additionPlan(records, candidate, limit, bytesBudget, drop, options.allowCapacityFallback === true);\n return plan || { entries: records, result: { ok: false, quotaExceeded: true } };\n });\n if (closed) return failure(new Error('历史存储已关闭'));\n if (committed.result.needsConfirmation) return committed.result;\n if (!committed.result.ok) break;\n cache[KEY] = committed.entries;\n return committed.result;\n } catch (error) {\n if (!isQuotaError(error)) return failure(error);\n if (!options.allowCapacityFallback) return capacityConsent().result;\n if (!plan || plan.entries.length === 1) break;\n }\n }\n }\n return unsavedHistory(cloned, lastReadRecords || cache[KEY] || []);\n });\n return result.quotaExceeded && !closed ? options.allowCapacityFallback\n ? unsavedHistory(cloned, lastReadRecords || cache[KEY] || []) : capacityConsent().result : result;\n };\n return api;\n}\n\nmodule.exports = { createHistory, DEFAULT_BYTES_BUDGET, approximateBytes, getOperationState };\n\n});\n\n__define('data/history-store', function (module, exports, require) {\nfunction createHistoryStore(factory, name = 'mingding_modifier_v2_14') {\n let closed = false;\n let connection = null;\n let opening = null;\n const pending = new Set();\n\n function open() {\n if (closed) return Promise.reject(new Error('历史存储已关闭'));\n if (opening) return opening;\n opening = new Promise((resolve, reject) => {\n if (!factory || typeof factory.open !== 'function') return reject(new Error('当前浏览器无法使用 IndexedDB,历史备份未保存'));\n const request = factory.open(name, 1);\n let failed = false;\n const fail = (error) => { failed = true; reject(error); };\n request.onupgradeneeded = () => {\n const db = request.result;\n if (closed || failed) { request.transaction.abort(); db.close(); return; }\n const records = db.createObjectStore('records', { keyPath: 'id', autoIncrement: true });\n records.createIndex('bucket', 'bucket');\n db.createObjectStore('meta', { keyPath: 'key' });\n };\n request.onerror = () => fail(request.error || new Error('无法打开历史数据库'));\n request.onblocked = () => fail(new Error('历史数据库升级被其他页面阻塞,请关闭旧页面并重新加载修改器'));\n request.onsuccess = () => {\n if (closed || failed) { request.result.close(); return reject(new Error('历史存储已关闭')); }\n connection = request.result;\n connection.onversionchange = () => destroy();\n resolve(connection);\n };\n });\n return opening;\n }\n\n async function transaction(mode, work) {\n const db = await open();\n if (closed) throw new Error('历史存储已关闭');\n return new Promise((resolve, reject) => {\n const tx = db.transaction(['records', 'meta'], mode);\n pending.add(tx);\n let result;\n let failure;\n tx.oncomplete = () => { pending.delete(tx); resolve(result); };\n tx.onabort = () => { pending.delete(tx); reject(failure || tx.error || new Error('历史存储事务已中止')); };\n tx.onerror = (event) => { failure = (event && event.target && event.target.error) || tx.error || new Error('历史存储写入失败'); };\n const guard = (fn) => (...args) => {\n try { fn(...args); } catch (error) { failure = error; tx.abort(); }\n };\n guard(() => work(tx, (value) => { result = value; }, guard))();\n });\n }\n\n function read() {\n return transaction('readonly', (tx, done, guard) => {\n const rows = tx.objectStore('records').getAll();\n const metadata = tx.objectStore('meta').getAll();\n let count = 0;\n const finish = guard(() => {\n if (++count !== 2) return;\n const byId = new Map(rows.result.map((row) => [row.id, row]));\n const data = {};\n for (const meta of metadata.result) {\n if (!Array.isArray(meta.ids)) continue;\n data[meta.key] = meta.ids.map((id) => byId.get(id)).filter(Boolean).map((row) => JSON.parse(row.text));\n }\n done(data);\n });\n rows.onsuccess = finish;\n metadata.onsuccess = finish;\n });\n }\nfunction mutate(bucket, update, marker) {\n return transaction('readwrite', (tx, done, guard) => {\n const records = tx.objectStore('records');\n const metaStore = tx.objectStore('meta');\n const rowsRequest = records.index('bucket').getAll(bucket);\n const orderRequest = metaStore.get(bucket);\n const markerRequest = marker ? metaStore.get(marker) : null;\n let count = 0;\n const finish = guard(() => {\n if (++count !== (marker ? 3 : 2)) return;\n const rows = rowsRequest.result;\n const byId = new Map(rows.map((row) => [row.id, row]));\n const ordered = ((orderRequest.result || {}).ids || []).map((id) => byId.get(id)).filter(Boolean);\n const entries = ordered.map((row) => JSON.parse(row.text));\n if (markerRequest && markerRequest.result) { done({ entries, result: markerRequest.result.result }); return; }\n const change = update(entries);\n if (!change.result.ok) { done({ entries, result: change.result }); return; }\n // 按原始序列化内容复用行,正常新增备份无需重写其余完整变量树。\n const available = new Map();\n for (const row of ordered) {\n if (!available.has(row.text)) available.set(row.text, []);\n available.get(row.text).push(row.id);\n }\n const ids = new Array(change.entries.length);\n const keep = new Set();\n let writes = 0;\n const saveOrder = () => {\n metaStore.put({ key: bucket, ids });\n if (marker) metaStore.put({ key: marker, result: change.result });\n done(change);\n };\n change.entries.forEach((entry, index) => {\n const text = JSON.stringify(entry);\n const reuse = available.get(text);\n if (reuse && reuse.length) { ids[index] = reuse.shift(); keep.add(ids[index]); return; }\n writes++;\n const add = records.add({ bucket, text });\n add.onsuccess = guard(() => { ids[index] = add.result; if (--writes === 0) saveOrder(); });\n });\n for (const row of rows) if (!keep.has(row.id)) records.delete(row.id);\n if (!writes) saveOrder();\n });\n rowsRequest.onsuccess = finish;\n orderRequest.onsuccess = finish;\n if (markerRequest) markerRequest.onsuccess = finish;\n });\n }\n\n function destroy() {\n closed = true;\n for (const tx of pending) { try { tx.abort(); } catch (error) { /* 已提交事务无需中止。 */ } }\n if (connection) connection.close();\n }\n\n return { read, mutate, destroy };\n}\n\nmodule.exports = { createHistoryStore };\n\n});\n\n__define('data/patch', function (module, exports, require) {\nconst util = require('../util');\nfunction escapeSegment(seg) {\n return String(seg).replace(/~/g, '~0').replace(/\\//g, '~1');\n}\nfunction unescapeSegment(seg) {\n return String(seg).replace(/~1/g, '/').replace(/~0/g, '~');\n}\nfunction joinPointer(segments) {\n if (!segments.length) return '';\n return '/' + segments.map(escapeSegment).join('/');\n}\nfunction parsePointer(ptr) {\n if (ptr === '' || ptr === '/') return [];\n return String(ptr).split('/').slice(1).map(unescapeSegment);\n}\nfunction hasReadonlySegment(segments) {\n return segments.some((s) => typeof s === 'string' && s.charAt(0) === '_');\n}\nfunction getPath(obj, segments) {\n let cur = obj;\n for (const s of segments) {\n if (cur == null) return undefined;\n if (Array.isArray(cur) && !/^\\d+$/.test(String(s))) return undefined;\n cur = cur[s];\n }\n return cur;\n}\nfunction findReadonlyMutation(oldValue, newValue, prefix = []) {\n // 用户明确删除父项时,补丁路径指向父项而非 _ 字段;这与偷偷用父对象 replace\n // 改写只读子字段不同。删除按钮另有确认,允许 Zod 按父项删除语义处理整项。\n if (newValue === undefined && prefix.length && String(prefix[prefix.length - 1]).charAt(0) !== '_') return '';\n if (Array.isArray(oldValue) || Array.isArray(newValue)) {\n const oldArr = Array.isArray(oldValue) ? oldValue : [];\n const newArr = Array.isArray(newValue) ? newValue : [];\n const length = Math.max(oldArr.length, newArr.length);\n for (let i = 0; i < length; i++) {\n const found = findReadonlyMutation(oldArr[i], newArr[i], prefix.concat([String(i)]));\n if (found) return found;\n }\n return '';\n }\n if (!isPlainObject(oldValue) && !isPlainObject(newValue)) return '';\n const oldObj = isPlainObject(oldValue) ? oldValue : {};\n const newObj = isPlainObject(newValue) ? newValue : {};\n const keys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);\n for (const key of keys) {\n if (key.charAt(0) === '_' && !util.deepEqual(oldObj[key], newObj[key])) {\n return joinPointer(prefix.concat([key]));\n }\n if (key.charAt(0) !== '_') {\n const found = findReadonlyMutation(oldObj[key], newObj[key], prefix.concat([key]));\n if (found) return found;\n }\n }\n return '';\n}\nfunction diffObject(oldObj, newObj, prefix) {\n if (!oldObj && !newObj) return [];\n const ops = [];\n const keys = new Set([\n ...Object.keys(oldObj || {}),\n ...Object.keys(newObj || {}),\n ]);\n for (const key of keys) {\n if (key.charAt(0) === '_') continue; // 只读段,跳过 diff(在写入前已确保不会被生成)\n if (!Object.prototype.hasOwnProperty.call(newObj || {}, key)) {\n // 明确删除\n ops.push({ op: 'remove', path: joinPointer(prefix.concat([key])) });\n } else if (!Object.prototype.hasOwnProperty.call(oldObj || {}, key)) {\n // 新增\n ops.push({ op: 'insert', path: joinPointer(prefix.concat([key])), value: newObj[key] });\n } else {\n const ov = oldObj[key];\n const nv = newObj[key];\n if (isPlainObject(ov) && isPlainObject(nv)) {\n const sub = diffObject(ov, nv, prefix.concat([key]));\n ops.push(...sub);\n } else if (!util.deepEqual(ov, nv)) {\n // 数组或标量变化:整体替换\n ops.push({ op: 'replace', path: joinPointer(prefix.concat([key])), value: nv });\n }\n }\n }\n return ops;\n}\nfunction isPlainObject(v) {\n return !!v && typeof v === 'object' && !Array.isArray(v);\n}\nfunction diffStatData(oldStatData, newStatData, opts = {}) {\n const ops = diffObject(oldStatData, newStatData, []);\n // 显式 delta(仅当用户选择增减语义)\n if (opts.allowDelta && opts.deltaPaths) {\n for (const [path, val] of Object.entries(opts.deltaPaths)) {\n if (path.startsWith('_')) continue;\n ops.push({ op: 'delta', path, value: val });\n }\n }\n // 显式 move(仅当用户明确移动)\n if (opts.move && opts.move.from && opts.move.path) {\n ops.push({ op: 'move', from: opts.move.from, path: opts.move.path });\n }\n return ops;\n}\nfunction assertWritable(path) {\n const segments = parsePointer(path);\n if (hasReadonlySegment(segments)) return { ok: false, reason: '路径含以 _ 开头的只读段' };\n return { ok: true };\n}\nfunction appendEventSignal(statData, additions) {\n const arr = Array.isArray(additions) ? additions : [additions];\n if (arr.length === 0) return { ok: false, reason: '无追加信号' };\n for (const a of arr) {\n if (typeof a !== 'string' || !a.trim()) return { ok: false, reason: '信号必须为非空字符串' };\n }\n const base = getPath(statData, ['事件', '信号']) || [];\n if (!Array.isArray(base)) return { ok: false, reason: '事件.信号 当前不是数组,无法安全追加' };\n const merged = base.concat(arr);\n return { ok: true, ops: [{ op: 'replace', path: '/事件/信号', value: merged }] };\n}\nfunction applyOp(obj, op) {\n if (!obj || typeof obj !== 'object' || !op || typeof op.path !== 'string') return obj;\n const segments = parsePointer(op.path);\n if (!segments.length) return obj;\n let cur = obj;\n for (let i = 0; i < segments.length - 1; i++) {\n const k = segments[i];\n if (cur[k] == null || typeof cur[k] !== 'object') {\n cur[k] = {};\n }\n cur = cur[k];\n }\n const lastKey = segments[segments.length - 1];\n if (op.op === 'remove') {\n if (Array.isArray(cur)) {\n const idx = Number(lastKey);\n if (!isNaN(idx)) cur.splice(idx, 1);\n } else {\n delete cur[lastKey];\n }\n } else if (op.op === 'delta') {\n const prev = Number(cur[lastKey]) || 0;\n cur[lastKey] = prev + Number(op.value);\n } else {\n // replace or insert\n try {\n cur[lastKey] = op.value === undefined ? undefined : JSON.parse(JSON.stringify(op.value));\n } catch (e) {\n cur[lastKey] = op.value;\n }\n }\n return obj;\n}\nfunction applyOps(obj, ops) {\n for (const op of ops || []) {\n applyOp(obj, op);\n }\n return obj;\n}\n\nmodule.exports = {\n escapeSegment,\n unescapeSegment,\n joinPointer,\n parsePointer,\n hasReadonlySegment,\n getPath,\n findReadonlyMutation,\n diffObject,\n diffStatData,\n assertWritable,\n appendEventSignal,\n applyOp,\n applyOps,\n};\n\n});\n\n__define('data/snapshot', function (module, exports, require) {\nconst util = require('../util');\nfunction buildSnapshot(params) {\n const snap = {\n target: util.deepClone(params.target),\n mvuData: util.deepClone(params.mvuData || {}),\n date: params.date === undefined ? null : util.deepClone(params.date),\n calendar: params.calendar === undefined ? null : util.deepClone(params.calendar),\n chatVariables: util.deepClone(params.chatVariables || {}),\n messageText: typeof params.messageText === 'string' ? params.messageText : '',\n capturedAt: Date.now(),\n };\n // 深冻结:顶层冻结不足以阻止 UI 通过 date/chatVariables 的嵌套引用污染备份。\n return util.deepFreeze(snap);\n}\nfunction isValidSnapshot(snap) {\n return !!snap && !!snap.target && !!snap.mvuData && typeof snap.mvuData === 'object' &&\n !!snap.mvuData.stat_data && typeof snap.mvuData.stat_data === 'object' && !Array.isArray(snap.mvuData.stat_data);\n}\nasync function captureSnapshot(host, mvu) {\n const target = await host.resolveTarget();\n if (!target) return null;\n if (!mvu || typeof mvu.ready !== 'function' || typeof mvu.read !== 'function') {\n throw new Error('建立快照需要正式 MVU 适配器');\n }\n await mvu.ready();\n const mvuData = await mvu.read(target);\n if (!isValidSnapshot({ target, mvuData })) return null;\n const date = mvuData.date;\n const calendar = mvuData.calendar_float_store;\n const chatVariables = await host.readChatVariables(target.chatId);\n const messageText = await host.readMessageText(target);\n const identityOk = typeof host.verifyTargetIdentity === 'function'\n ? await host.verifyTargetIdentity(target)\n : (typeof host.verifyTarget === 'function'\n ? await host.verifyTarget(Object.assign({}, target, { fingerprint: undefined, textFingerprint: undefined }))\n : true);\n if (!identityOk) {\n throw new Error('快照读取期间目标发生变化,请重试');\n }\n return buildSnapshot({\n target,\n mvuData,\n date,\n calendar,\n chatVariables,\n messageText,\n });\n}\n\nmodule.exports = { buildSnapshot, isValidSnapshot, captureSnapshot };\n\n});\n\n__define('host/generation-state', function (module, exports, require) {\nfunction createGenerationState() {\n let state = 'unknown';\n let source = 'initial';\n let lastEvent = null;\n const diagnostics = [];\n const MAX_DIAGNOSTICS = 20;\n\n function record(entry) {\n diagnostics.push(Object.assign({ at: Date.now() }, entry));\n if (diagnostics.length > MAX_DIAGNOSTICS) diagnostics.splice(0, diagnostics.length - MAX_DIAGNOSTICS);\n }\n\n function setEvent(eventName, args) {\n lastEvent = { name: eventName, dryRun: args && args[2] === true, at: Date.now() };\n if (eventName === 'generation_started' && args && args[2] !== true) {\n state = 'generating';\n source = 'event:generation_started';\n } else if (eventName === 'generation_ended' || eventName === 'generation_stopped') {\n state = 'idle';\n source = 'event:' + eventName;\n } else if (eventName === 'chat_id_changed' || eventName === 'message_swiped') {\n state = 'unknown';\n source = 'event:' + eventName;\n }\n record({ kind: 'event', event: eventName, state, dryRun: lastEvent.dryRun });\n }\n\n function read(probe) {\n let value;\n try { value = probe(); }\n catch (error) {\n record({ kind: 'probe-error', source, error: String(error && error.message || error) });\n return { state: 'unknown', source: 'probe-error' };\n }\n if (typeof value === 'boolean') {\n const resolved = value ? 'generating' : 'idle';\n return { state: resolved, source: 'builtin' };\n }\n record({ kind: 'probe-invalid', valueType: value === null ? 'null' : typeof value });\n return { state: 'unknown', source: 'builtin-invalid' };\n }\n\n function current(probe) {\n const result = typeof probe === 'function' ? read(probe) : { state, source };\n if (result.source === 'builtin') {\n state = result.state;\n source = result.source;\n } else if (result.state === 'unknown' && state !== 'unknown') {\n // 能力探测不可用时,使用已收到的正式事件;绝不把未知值强转为布尔状态。\n return { state, source, lastEvent };\n }\n return { state: result.state, source: result.source, lastEvent };\n }\n\n function check(action, probe) {\n const generation = current(probe);\n const ok = generation.state !== 'generating';\n if (!ok) record({ kind: 'blocked', action: action || 'write', state: generation.state, source: generation.source });\n return ok\n ? { ok: true, generation }\n : { ok: false, reason: 'AI 正在生成回复', generation };\n }\n\n return {\n setEvent,\n current,\n check,\n diagnostics() { return diagnostics.map(item => Object.assign({}, item)); },\n destroy() { diagnostics.length = 0; },\n };\n}\n\nmodule.exports = { createGenerationState };\n\n});\n\n__define('host/host-adapter', function (module, exports, require) {\nconst resolver = require('./target-resolver');\nconst util = require('../util');\nconst contract = require('../contracts');\nconst { createGenerationState } = require('./generation-state');\n\nfunction hostObject(win, parent) { return (win && win.SillyTavern) || (parent && parent.SillyTavern) || null; }\nfunction readContext(win, parent) {\n const host = hostObject(win, parent);\n try { return host && typeof host.getContext === 'function' ? (host.getContext() || {}) : {}; } catch (e) { return {}; }\n}\nfunction readChatArray(win, parent) {\n const host = hostObject(win, parent);\n try {\n if (host && Array.isArray(host.chat)) return host.chat;\n const ctx = readContext(win, parent);\n if (Array.isArray(ctx.chat)) return ctx.chat;\n } catch (e) { /* 回退 */ }\n return parent && Array.isArray(parent.chat) ? parent.chat : [];\n}\nfunction readChatMetadata(win, parent) {\n const host = hostObject(win, parent);\n try {\n if (host && host.chat_metadata && typeof host.chat_metadata === 'object') return host.chat_metadata;\n const ctx = readContext(win, parent);\n if (ctx.chatMetadata && typeof ctx.chatMetadata === 'object') return ctx.chatMetadata;\n } catch (e) { /* 回退 */ }\n return parent && parent.chat_metadata ? parent.chat_metadata : null;\n}\nfunction readChatId(win, parent) {\n const meta = readChatMetadata(win, parent);\n if (meta && meta.chat_id) return meta.chat_id;\n const ctx = readContext(win, parent);\n return (ctx && (ctx.chatId || ctx.chat_id)) || '';\n}\nfunction getEventSource(win, parent) {\n const host = hostObject(win, parent);\n try {\n if (host && host.eventSource) return host.eventSource;\n const ctx = readContext(win, parent);\n if (ctx.eventSource) return ctx.eventSource;\n } catch (e) { /* 回退 */ }\n return parent && parent.eventSource ? parent.eventSource : null;\n}\nfunction getEventTypes(win, parent) {\n const host = hostObject(win, parent);\n try {\n if (host && host.event_types) return host.event_types;\n const ctx = readContext(win, parent);\n if (ctx.eventTypes) return ctx.eventTypes;\n } catch (e) { /* 回退 */ }\n return parent && parent.event_types ? parent.event_types : {};\n}\nasync function saveChatNow(win, parent) {\n const host = hostObject(win, parent);\n if (host && typeof host.saveChat === 'function') return host.saveChat();\n const ctx = readContext(win, parent);\n if (ctx && typeof ctx.saveChat === 'function') return ctx.saveChat();\n throw new Error('宿主缺少可等待的 saveChat 接口');\n}\nfunction currentBucket(msg, swipeId) { return resolver.currentBucket(msg, swipeId); }\nfunction setBucket(msg, bucket, swipeId) {\n if (!msg || typeof msg !== 'object') return false;\n if (msg.variables && typeof msg.variables === 'object' && !Array.isArray(msg.variables)) { msg.variables = bucket; return true; }\n const idx = typeof swipeId === 'number' ? swipeId : (typeof msg.swipe_id === 'number' ? msg.swipe_id : 0);\n if (!Array.isArray(msg.variables)) msg.variables = [];\n msg.variables[idx] = bucket;\n return true;\n}\nfunction lockedMessage(win, parent, target) {\n if (!target) throw new Error('目标引用为空');\n const currentChatId = readChatId(win, parent);\n if (target.chatId && currentChatId && currentChatId !== target.chatId) throw new Error('当前聊天已变化');\n const msg = readChatArray(win, parent)[target.messageId];\n if (!msg) throw new Error('目标消息不存在');\n const swipeId = typeof msg.swipe_id === 'number' ? msg.swipe_id : 0;\n if (swipeId !== target.swipeId) throw new Error('目标 swipe 已变化');\n if ((msg.is_user ? 'user' : 'assistant') !== target.role) throw new Error('目标消息角色已变化');\n return msg;\n}\nfunction probeCandidates(win, parent) {\n return [\n ['window.builtin.duringGenerating', win && win.builtin && win.builtin.duringGenerating, win && win.builtin],\n ['window.TavernHelper.builtin.duringGenerating', win && win.TavernHelper && win.TavernHelper.builtin && win.TavernHelper.builtin.duringGenerating, win && win.TavernHelper && win.TavernHelper.builtin],\n ['parent.TavernHelper.builtin.duringGenerating', parent && parent.TavernHelper && parent.TavernHelper.builtin && parent.TavernHelper.builtin.duringGenerating, parent && parent.TavernHelper && parent.TavernHelper.builtin],\n ];\n}\nfunction eventName(types, key, fallback) { return types && typeof types[key] === 'string' ? types[key] : fallback; }\n\nfunction createHostAdapter(overrides) {\n const win = (overrides && overrides.window) || (typeof window !== 'undefined' ? window : null);\n const parent = (overrides && overrides.parent) || (win && win.parent) || win;\n const listeners = new Set();\n let ownEvents = new WeakSet();\n const generation = createGenerationState();\n let autoSaveChat = true;\n let syncMvu = true;\n let renderWarning = '';\n const types = getEventTypes(win, parent) || {};\n const source = getEventSource(win, parent);\n const bind = (key, fallback, handler) => {\n const name = eventName(types, key, fallback);\n if (!source || !name || typeof source.on !== 'function') return;\n source.on(name, handler); listeners.add({ source, name, handler });\n };\n bind('GENERATION_STARTED', 'generation_started', (...args) => generation.setEvent('generation_started', args));\n bind('GENERATION_ENDED', 'generation_ended', (...args) => generation.setEvent('generation_ended', args));\n bind('GENERATION_STOPPED', 'generation_stopped', (...args) => generation.setEvent('generation_stopped', args));\n bind('CHAT_CHANGED', 'chat_id_changed', (...args) => generation.setEvent('chat_id_changed', args));\n bind('MESSAGE_SWIPED', 'message_swiped', (...args) => generation.setEvent('message_swiped', args));\n function getGenerationState() {\n for (const [name, fn, receiver] of probeCandidates(win, parent)) {\n if (typeof fn !== 'function') continue;\n const result = generation.current(() => fn.call(receiver)); result.source = name; return result;\n }\n return generation.current();\n }\n const api = {\n resolveTarget() { return Promise.resolve(resolver.resolveTarget(readChatArray(win, parent), readChatId(win, parent))); },\n verifyTarget(target) {\n if (!target) return Promise.resolve(false);\n const msg = readChatArray(win, parent)[target.messageId];\n if (!msg) return Promise.resolve(false);\n const currentChatId = readChatId(win, parent);\n if (target.chatId && currentChatId && target.chatId !== currentChatId) return Promise.resolve(false);\n const role = msg.is_user ? (contract.ROLE ? contract.ROLE.USER : 'user') : (contract.ROLE ? contract.ROLE.ASSISTANT : 'assistant');\n const swipeId = typeof msg.swipe_id === 'number' ? msg.swipe_id : 0;\n if (role !== target.role || swipeId !== target.swipeId) return Promise.resolve(false);\n if (target.fingerprint && resolver.makeFingerprint(currentBucket(msg, swipeId)) !== target.fingerprint) return Promise.resolve(false);\n if (target.textFingerprint && resolver.makeFingerprint(resolver.currentMessageText(msg, swipeId)) !== target.textFingerprint) return Promise.resolve(false);\n return Promise.resolve(true);\n },\n verifyTargetIdentity(target) { return this.verifyTarget(target && Object.assign({}, target, { fingerprint: undefined, textFingerprint: undefined })); },\n readMessageVariables(target) { return Promise.resolve(util.deepClone(currentBucket(lockedMessage(win, parent, target), target.swipeId))); },\n async replaceMessageVariables(target, value) { setBucket(lockedMessage(win, parent, target), util.deepClone(value), target.swipeId); if (autoSaveChat) await saveChatNow(win, parent); },\n readChatVariables(chatId) {\n if (readChatId(win, parent) !== chatId) return Promise.reject(new Error('当前聊天已变化'));\n const meta = readChatMetadata(win, parent);\n return Promise.resolve(util.deepClone(meta && meta.variables && typeof meta.variables === 'object' ? meta.variables : {}));\n },\n async replaceChatVariables(chatId, value) {\n if (readChatId(win, parent) !== chatId) throw new Error('当前聊天已变化');\n const meta = readChatMetadata(win, parent); if (!meta) throw new Error('聊天元数据不可用');\n meta.variables = util.deepClone(value || {}); if (autoSaveChat) await saveChatNow(win, parent);\n },\n readMessageText(target) { return Promise.resolve(resolver.currentMessageText(lockedMessage(win, parent, target), target.swipeId)); },\n async replaceMessageText(target, text) {\n const msg = lockedMessage(win, parent, target); msg.mes = String(text);\n if (Array.isArray(msg.swipes)) msg.swipes[target.swipeId] = String(text);\n if (autoSaveChat) await saveChatNow(win, parent);\n const rendered = await tryRefreshMessageDom(win, parent, target, String(text));\n renderWarning = rendered.ok ? '' : rendered.reason;\n },\n // 保留内部方法名以兼容调用者;只发送渲染后通知,不触发编辑前处理链重新执行正文指令。\n async notifyMessageEdited(messageId) {\n const id = Number(messageId); if (!Number.isInteger(id)) throw new Error('消息更新通知需要整数消息楼层');\n const updated = eventName(types, 'MESSAGE_UPDATED', 'message_updated');\n const token = { messageId: id, names: new Set([updated]) }; ownEvents.add(token);\n if (syncMvu && updated && source && typeof source.emit === 'function') await source.emit(updated, id, token);\n if (renderWarning) throw new Error(renderWarning);\n },\n isOwnEvent(name, ...args) {\n const token = args[1];\n return !!token && ownEvents.has(token) && token.names.has(name) && Number(args[0]) === token.messageId;\n },\n setAutoSaveChat(value) { autoSaveChat = value !== false; },\n setSyncMvu(value) { syncMvu = value !== false; },\n broadcastEvents(messageId) { return this.notifyMessageEdited(messageId); },\n subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },\n getGenerationState,\n checkGeneration(action) {\n for (const [name, fn, receiver] of probeCandidates(win, parent)) {\n if (typeof fn !== 'function') continue;\n const result = generation.check(action, () => fn.call(receiver)); result.generation.source = name; return result;\n }\n return generation.check(action);\n },\n getGenerationDiagnostics() { return generation.diagnostics(); },\n isGenerating() { return getGenerationState().state === 'generating'; },\n destroy() {\n for (const item of listeners) {\n try {\n if (item.source && typeof item.source.off === 'function') item.source.off(item.name, item.handler);\n else if (item.source && typeof item.source.removeListener === 'function') item.source.removeListener(item.name, item.handler);\n else if (item.source && typeof item.source.removeEventListener === 'function') item.source.removeEventListener(item.name, item.handler);\n } catch (e) { /* 清理尽力而为 */ }\n }\n listeners.clear(); ownEvents = new WeakSet(); generation.destroy();\n },\n };\n return api;\n}\nasync function tryRefreshMessageDom(win, parent, target, text) {\n try {\n if (!target) return { ok: false, reason: '界面刷新缺少目标楼层' };\n const message = lockedMessage(win, parent, target);\n const context = readContext(win, parent);\n if (typeof context.updateMessageBlock === 'function') {\n await context.updateMessageBlock(target.messageId, message, { rerenderMessage: true });\n return { ok: true };\n }\n const doc = parent && parent.document || win && win.document;\n if (!doc) return { ok: true, skipped: true };\n const mes = doc.querySelector(`div[mesid=\"${target.messageId}\"] .mes_text`);\n if (!mes) return { ok: true, skipped: true }; // 楼层未挂载时由宿主下次显示时渲染。\n if (typeof context.messageFormatting !== 'function') return { ok: false, reason: '宿主缺少正文格式化接口,请重新显示聊天确认内容' };\n mes.innerHTML = await context.messageFormatting(text, message.name, !!message.is_system, !!message.is_user, target.messageId, {}, false);\n return { ok: true };\n } catch (e) {\n // 保存和界面刷新分开报告;调用者不能因界面故障误称正文未保存。\n return { ok: false, reason: '聊天界面刷新失败: ' + (e.message || String(e)) };\n }\n}\nasync function legacyBroadcastHostEvents(win, parent, id) {\n const source = getEventSource(win, parent);\n const types = getEventTypes(win, parent) || {};\n if (!source || typeof source.emit !== 'function') return;\n const updated = eventName(types, 'MESSAGE_UPDATED', 'message_updated');\n const value = Number(id);\n if (!Number.isInteger(value)) throw new Error('消息更新通知需要整数消息楼层');\n if (updated) await source.emit(updated, value);\n}\n\nmodule.exports = {\n createHostAdapter, hostObject, readChatArray, readChatMetadata, readChatId, getEventSource, getEventTypes,\n saveChatNow, currentBucket, setBucket, lockedMessage, broadcastHostEvents: legacyBroadcastHostEvents,\n notifyMessageEdited: async function (win, parent, id) {\n const adapter = createHostAdapter({ window: win, parent }); try { return await adapter.notifyMessageEdited(id); } finally { adapter.destroy(); }\n }, tryRefreshMessageDom,\n};\n\n});\n\n__define('host/lifecycle', function (module, exports, require) {\nfunction pickEventNames(evtTypes) {\n const byKey = {\n CHAT_CHANGED: 'chat_id_changed',\n MESSAGE_EDITED: 'message_edited',\n MESSAGE_UPDATED: 'message_updated',\n MESSAGE_DELETED: 'message_deleted',\n MESSAGE_SWIPED: 'message_swiped',\n GENERATION_ENDED: 'generation_ended',\n GENERATION_STOPPED: 'generation_stopped',\n MESSAGE_RECEIVED: 'message_received',\n };\n if (Array.isArray(evtTypes)) return evtTypes.slice();\n if (evtTypes && typeof evtTypes === 'object') {\n const names = [];\n for (const key of Object.keys(byKey)) {\n const val = evtTypes[key];\n names.push(typeof val === 'string' ? val : byKey[key]);\n }\n return names;\n }\n // 无 event_types:回退正确事件名字符串\n return Object.values(byKey);\n}\nfunction createLifecycle(opts) {\n const onInvalidate = opts.onInvalidate || (() => {});\n const onEvent = opts.onEvent || (() => {});\n const bound = new Set();\n const timers = new Set();\n const observers = new Set();\n const objectUrls = new Set();\n const cleanupFns = new Set();\n let destroyed = false;\nfunction handle(eventName, ...args) {\n onEvent(eventName, ...args);\n onInvalidate({ name: eventName, args });\n }\n\n return {\n notifyInvalidate() {\n if (destroyed) return;\n onInvalidate();\n },\nregisterHostListeners(evtSource, evtTypes) {\n if (!evtSource || destroyed) return;\n // SillyTavern event_types 是对象(如 { CHAT_CHANGED: 'chat_id_changed', MESSAGE_SWIPED: 'message_swiped' })。\n // 提取关键事件的真实事件名;缺失时用正确的事件名字符串回退。\n const names = pickEventNames(evtTypes);\n for (const name of names) {\n const fn = (...args) => handle(name, ...args);\n if (typeof evtSource.on === 'function') evtSource.on(name, fn);\n else if (typeof evtSource.addEventListener === 'function') evtSource.addEventListener(name, fn);\n bound.add({ name, fn, evtSource });\n }\n },\naddTimer(t) {\n if (destroyed) { clearTimeout(t); clearInterval(t); return t; }\n timers.add(t);\n return t;\n },\naddObserver(ob) {\n if (destroyed) { try { ob.disconnect(); } catch (e) { /* 忽略 */ } return ob; }\n observers.add(ob);\n return ob;\n },\naddObjectUrl(url) {\n if (destroyed) { try { URL.revokeObjectURL(url); } catch (e) { /* 忽略 */ } return url; }\n objectUrls.add(url);\n return url;\n },\naddCleanup(fn) {\n if (destroyed) { try { fn(); } catch (e) { /* 忽略 */ } return; }\n cleanupFns.add(fn);\n },\n\n destroy() {\n if (destroyed) return;\n destroyed = true;\n for (const t of timers) { clearInterval(t); clearTimeout(t); }\n timers.clear();\n for (const ob of observers) { try { ob.disconnect(); } catch (e) { /* 忽略 */ } }\n observers.clear();\n for (const u of objectUrls) { try { URL.revokeObjectURL(u); } catch (e) { /* 忽略 */ } }\n objectUrls.clear();\n for (const fn of cleanupFns) { try { fn(); } catch (e) { /* 忽略 */ } }\n cleanupFns.clear();\n for (const b of bound) {\n try {\n if (b.evtSource && b.evtSource.off) b.evtSource.off(b.name, b.fn);\n else if (b.evtSource && b.evtSource.removeEventListener) b.evtSource.removeEventListener(b.name, b.fn);\n } catch (e) { /* 忽略 */ }\n }\n bound.clear();\n },\n };\n}\n\nmodule.exports = { createLifecycle, pickEventNames };\n\n});\n\n__define('host/target-resolver', function (module, exports, require) {\nconst contract = require('../contracts');\nconst ROLE = contract.ROLE;\nfunction hasValidStatData(bucket) {\n return !!bucket && typeof bucket === 'object' && !Array.isArray(bucket) &&\n !!bucket.stat_data && typeof bucket.stat_data === 'object' && !Array.isArray(bucket.stat_data);\n}\nfunction currentMessageText(msg, swipeId) {\n if (!msg || typeof msg !== 'object') return '';\n const index = typeof swipeId === 'number' ? swipeId : (typeof msg.swipe_id === 'number' ? msg.swipe_id : 0);\n if (Array.isArray(msg.swipes) && typeof msg.swipes[index] === 'string') return msg.swipes[index];\n return typeof msg.mes === 'string' ? msg.mes : '';\n}\nfunction currentBucket(msg, swipeId) {\n if (!msg || typeof msg !== 'object') return null;\n const v = msg.variables;\n if (v && typeof v === 'object' && !Array.isArray(v)) return v;\n if (Array.isArray(v)) {\n // 锁定用显式传入的 swipeId;解析目标时未传则回退 msg.swipe_id。\n const idx = typeof swipeId === 'number' ? swipeId : (typeof msg.swipe_id === 'number' ? msg.swipe_id : 0);\n return v[idx] && typeof v[idx] === 'object' ? v[idx] : null;\n }\n return null;\n}\nfunction resolveTarget(chat, chatId) {\n if (!Array.isArray(chat)) return null;\n for (let i = chat.length - 1; i >= 0; i--) {\n const msg = chat[i];\n const bucket = currentBucket(msg);\n if (!hasValidStatData(bucket)) continue;\n const role = msg.is_user ? ROLE.USER : ROLE.ASSISTANT;\n const swipeId = typeof msg.swipe_id === 'number' ? msg.swipe_id : 0;\n return {\n chatId: chatId || '',\n messageId: i,\n swipeId,\n role,\n // 指纹覆盖完整消息变量桶,避免 date/calendar 或未知字段并发变化未被发现。\n fingerprint: makeFingerprint(bucket),\n // 正文模式还需要检测目标消息正文是否在编辑期间变化。\n textFingerprint: makeFingerprint(currentMessageText(msg, swipeId)),\n };\n }\n return null;\n}\nfunction makeFingerprint(value) {\n let text;\n try {\n const serialized = JSON.stringify(value == null ? null : value);\n text = serialized === undefined ? String(value) : serialized;\n }\n catch (e) { return '<unserializable>'; }\n let hash = 2166136261;\n for (let i = 0; i < text.length; i++) {\n hash ^= text.charCodeAt(i);\n hash = Math.imul(hash, 16777619);\n }\n return text.length + ':' + (hash >>> 0).toString(16);\n}\nfunction sameTarget(a, b) {\n if (!a || !b) return false;\n return sameTargetIdentity(a, b) &&\n a.fingerprint === b.fingerprint &&\n a.textFingerprint === b.textFingerprint;\n}\nfunction sameTargetIdentity(a, b) {\n if (!a || !b) return false;\n const chatMatch = !a.chatId || !b.chatId || a.chatId === b.chatId;\n return chatMatch &&\n a.messageId === b.messageId &&\n a.swipeId === b.swipeId &&\n a.role === b.role;\n}\nfunction createTargetResolver(host) {\n return {\n resolve() {\n const chat = host.readChat();\n const chatId = host.readChatId();\n return resolveTarget(chat, chatId);\n },\n };\n}\n\nmodule.exports = {\n hasValidStatData,\n currentBucket,\n currentMessageText,\n resolveTarget,\n makeFingerprint,\n sameTarget,\n sameTargetIdentity,\n createTargetResolver,\n};\n\n});\n\n__define('main', function (module, exports, require) {\nconst hostAdapter = require('./host/host-adapter');\nconst targetResolver = require('./host/target-resolver');\nconst lifecycle = require('./host/lifecycle');\nconst snapshot = require('./data/snapshot');\nconst patch = require('./data/patch');\nconst changeSet = require('./data/change-set');\nconst field = require('./data/field-model');\nconst historyModule = require('./data/history');\nconst mvuAdapter = require('./apply/mvu-adapter');\nconst applyFlow = require('./apply/apply-flow');\nconst bodyMerge = require('./apply/body-merge');\nconst transaction = require('./apply/transaction');\nconst concurrency = require('./apply/concurrency');\nconst operationEdits = require('./apply/operation-edits');\nconst bodyHistory = require('./apply/body-history');\nconst reincarnation = require('./apply/reincarnation');\nconst dirtyState = require('./ui/dirty-state');\nconst panel = require('./ui/panel');\nconst { DEFAULT_PERSISTENT } = require('./ui/ui-state');\nconst util = require('./util');\nconst release = require('./version');\nfunction createModifier(opts) {\n const host = opts.host;\n const mvu = opts.mvu;\n const extras = opts.extras || {};\n const cardHash = extras.cardHash || '';\n const version = extras.version || release.DISPLAY_VERSION;\n const history = extras.history || historyModule.createHistory(extras.historyOptions);\n const ds = dirtyState.createDirtyState();\n const subscribers = new Set();\n let currentSnapshot = null;\n let currentChangeSet = null;\n let editor = null;\n let auxiliaryEditor = null;\n let applyMode = extras.applyMode || DEFAULT_PERSISTENT.settings.applyMode;\n let destroyed = false;\n let panelOpen = true;\n let draftPending = false;\n let stale = false;\n let writeUncertain = false;\n let started = false;\n let loadRequestId = 0;\n let transactionDepth = 0;\n let operationSequence = 0;\n let interactions = extras.interactions || {};\n const resolveConflicts = (rows) => {\n if (destroyed) throw new Error('修改器已关闭');\n if (typeof interactions.resolveConflicts !== 'function') throw new Error('变更路径已被其它脚本修改:' + rows[0].path + ',请在界面中选择覆盖或保留');\n return interactions.resolveConflicts(rows);\n };\n const operationPrefix = Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);\n\n let reloadTimer = null;\n\n function clearReloadTimer() {\n if (reloadTimer) {\n clearTimeout(reloadTimer);\n reloadTimer = null;\n }\n }\n\n function hasPendingDraft() { return draftPending || ds.total() > 0; }\nfunction scheduleReload(delay) {\n clearReloadTimer();\n if (destroyed || !panelOpen || transactionDepth > 0 || hasPendingDraft()) return;\n const debounceMs = typeof delay === 'number' ? delay\n : typeof extras.reloadDebounceMs === 'number' ? extras.reloadDebounceMs : 200;\n reloadTimer = setTimeout(async () => {\n reloadTimer = null;\n if (destroyed || !panelOpen || transactionDepth > 0 || hasPendingDraft()) return;\n try {\n if (host && typeof host.getGenerationState === 'function' &&\n host.getGenerationState().state === 'generating') return;\n await runtime.load({ automatic: true });\n } catch (error) {\n if (!destroyed) notify({ type: 'invalidated', preserved: !!currentSnapshot,\n message: '暂未读到最新变量,已保留当前页面;后续更新会再次同步。', kind: 'warning' });\n }\n }, debounceMs);\n // 唯一待执行计时器由 clearReloadTimer/destroy 管理,不长期登记已经执行完的句柄。\n }\n\n const life = lifecycle.createLifecycle({\n onInvalidate(eventInfo) {\n if (destroyed) return;\n if (eventInfo && host && typeof host.isOwnEvent === 'function' &&\n host.isOwnEvent(eventInfo.name, ...(eventInfo.args || []))) return;\n clearReloadTimer();\n ++loadRequestId;\n stale = true;\n if (transactionDepth > 0) {\n // 事务期间只登记更新;入口和写入边界核验真实身份,不把普通通知冒充聊天切换。\n return;\n }\n if (hasPendingDraft()) {\n notify({\n type: 'invalidated',\n preserved: true,\n kind: 'warning',\n message: '聊天数据有更新,已保留未提交输入;应用时会检查回复位置与修改字段。',\n });\n return;\n }\n if (!panelOpen) {\n notify({ type: 'invalidated', preserved: !!currentSnapshot, kind: 'warning',\n message: '聊天数据有更新,重新打开面板时会自动同步。' });\n return;\n }\n notify({ type: 'invalidated', preserved: !!currentSnapshot, kind: 'warning', message: '聊天数据有更新,正在自动同步。' });\n scheduleReload();\n },\n });\n\n function notify(event) {\n if (destroyed) return;\n for (const listener of subscribers) {\n try { listener(event); } catch (e) { /* UI 监听错误不影响运行时 */ }\n }\n }\n\n function requireSnapshot() {\n if (!currentSnapshot) throw new Error('快照已失效或尚未加载,请先刷新');\n if (writeUncertain) throw new Error('上次写入结果需要检查,请先刷新后再应用');\n return currentSnapshot;\n }\nasync function verifySessionTarget(snap) {\n const verify = host.verifyTargetIdentity || host.verifyTarget;\n const target = Object.assign({}, snap.target, { fingerprint: undefined, textFingerprint: undefined });\n if (destroyed || !await verify.call(host, target)) throw new Error('目标已变化,草稿已保留,请重新加载当前回复');\n if (stale && typeof host.resolveTarget === 'function') {\n const selected = await host.resolveTarget();\n if (!targetResolver.sameTargetIdentity(snap.target, selected)) {\n throw new Error('目标已变化,草稿已保留,请重新加载当前回复');\n }\n }\n if (destroyed) throw new Error('修改器已关闭,未写入');\n }\n\n async function ensureGenerationIdle(action) {\n if (host && typeof host.checkGeneration === 'function') {\n const result = await host.checkGeneration(action);\n if (result && result.ok === false) throw new Error(result.reason || 'AI 正在生成回复');\n return result;\n }\n if (host && typeof host.isGenerating === 'function' && host.isGenerating() === true) {\n throw new Error('AI 正在生成回复');\n }\n return { ok: true, generation: 'unknown' };\n }\n\n async function saveHistory(entry, label, mode) {\n if (destroyed) throw new Error('修改器已关闭,未写入');\n const prepared = Object.assign({}, entry, { label, mode });\n let result = await history.add(prepared);\n if (result && result.needsConfirmation) {\n const accepted = typeof interactions.confirmCapacity === 'function' && await interactions.confirmCapacity(result);\n if (destroyed || accepted !== true) throw new Error('已取消容量降级,未继续修改;已有备份保持不变');\n result = await history.add(prepared, { allowCapacityFallback: true });\n }\n if (result && result.ok === false) throw new Error('备份未保存,已停止写入:' + result.reason);\n if (destroyed) throw new Error('修改器已关闭,未写入变量');\n return Object.assign({ ok: true, persisted: true, warnings: [] }, result);\n }\nfunction recoverySession(label, mode) {\n const operationId = operationPrefix + '-' + (++operationSequence);\n let latest = null;\n return {\n operationId,\n async save(entry) {\n latest = await saveHistory(Object.assign({}, entry, { operationId, operationStatus: 'pending' }), label, mode);\n return latest;\n },\n async finish(result) {\n const warnings = Array.from(new Set([...(result.warnings || []), ...(latest && latest.warnings || [])].filter(Boolean)));\n // parts 在调用宿主写入之前置位;即使随后补偿成功,也保留可能需要人工恢复的入口。\n const attempted = result.written === true || result.attempted === true || result.parsedAttempted === true || !!result.report;\n const status = result.noWrite ? 'not-applied' : result.ok === true ? 'applied' : attempted ? 'recovery-needed' : 'not-applied';\n if (latest && latest.persisted !== false) {\n try {\n if (typeof history.complete !== 'function') throw new Error('当前历史存储不支持结果标记');\n const updated = await history.complete(operationId, { status, reason: result.reason,\n operationChanges: result.operationChanges, bodyPatchHistory: result.bodyPatchHistory });\n if (!updated || updated.ok === false) throw new Error(updated && updated.reason || '存储未确认');\n } catch (error) {\n // 业务结果已确定;结果标签写不进去只能警告,不能把成功修改改报为失败。\n warnings.push('历史应用结果未保存,逐项回滚可能不可用;请核对实际数据,必要时使用完整快照恢复:' + error.message);\n }\n }\n return Object.assign(result, { historySaved: !!latest && latest.persisted !== false, warnings });\n },\n };\n }\nasync function recordActivity(method, entry) {\n if (typeof history[method] !== 'function') return '';\n try {\n if (destroyed) throw new Error('修改器已关闭');\n const result = await history[method](entry);\n if (result && result.ok === false) throw new Error(result.reason || '存储失败');\n return '';\n } catch (error) {\n return (method === 'addInjection' ? '正文注入记录' : '操作日志') + '未保存:' + error.message;\n }\n }\nasync function reloadAfterWrite() {\n try {\n const loaded = await runtime.load();\n return loaded ? { reloaded: true } : { reloaded: false, reloadError: '读取已取消或当前回复没有可编辑的角色变量' };\n } catch (error) { return { reloaded: false, reloadError: error.message || String(error) }; }\n }\nasync function verifyEditTarget(snap, text, action) {\n await ensureGenerationIdle(action);\n await verifySessionTarget(snap);\n if (!bodyHistory.sameInstructions(await host.readMessageText(snap.target), text)) throw new Error('目标正文已变化:变量指令不同,请重新确认后再修改');\n }\n\n async function writeDeclaration(blockIndex, opIndex, newOp, remove) {\n const snap = requireSnapshot();\n const label = remove ? '删除正文声明前备份' : '编辑正文声明前备份';\n let original = await host.readMessageText(snap.target);\n const selected = bodyMerge.parseUpdateBlocksWithAnalysis(snap.messageText).ops.find(op => op.blockIndex === blockIndex && op.opIndex === opIndex);\n const comparable = op => { const copy = Object.assign({}, op); delete copy.blockIndex; delete copy.opIndex; return copy; };\n if (!selected) throw new Error('没有找到这条正文指令');\n const replaceSelected = text => {\n if (bodyHistory.sameInstructions(text, snap.messageText)) return remove\n ? bodyMerge.removeDeclaredOp(text, blockIndex, opIndex) : bodyMerge.updateDeclaredOp(text, blockIndex, opIndex, newOp);\n const matches = bodyMerge.parseUpdateBlocksWithAnalysis(text).ops.filter(op => util.deepEqual(comparable(op), comparable(selected)));\n if (matches.length !== 1) throw new Error('这条正文指令已变化或无法唯一辨认,请重新读取后选择');\n const op = matches[0];\n return remove ? bodyMerge.removeDeclaredOp(text, op.blockIndex, op.opIndex)\n : bodyMerge.updateDeclaredOp(text, op.blockIndex, op.opIndex, newOp);\n };\n let next = replaceSelected(original);\n if (next === original) return { ok: false, reason: '声明未发生变化,未写入' };\n const memory = transaction.createBackup({ target: snap.target, messageText: original, cardHash, version });\n const entry = transaction.prepareHistoryBackup(memory, { nextMessageText: next, scopes: { mvu: false, chat: false, text: true } });\n const recovery = recoverySession(label, 'declaration');\n transactionDepth++;\n let attempted = false;\n let warning = '';\n try {\n await ensureGenerationIdle('edit-declaration'); await verifySessionTarget(snap);\n await recovery.save(entry);\n await ensureGenerationIdle('edit-declaration-before-write'); await verifySessionTarget(snap);\n original = await host.readMessageText(snap.target);\n next = replaceSelected(original);\n await ensureGenerationIdle('edit-declaration-write'); await verifySessionTarget(snap);\n memory.messageText = original;\n const paths = { text: bodyHistory.createBodyPatchHistory(original, next) };\n // 声明编辑只写正文;其它脚本的变量更新不属于本次写入范围。\n attempted = true;\n await host.replaceMessageText(snap.target, next);\n const verified = await transaction.verifyStoredState({ host, mvu, target: snap.target, paths,\n expected: { messageText: next } });\n if (!verified.ok) {\n stale = true; writeUncertain = true;\n return await recovery.finish({ ok: false, written: true, reason: '正文写后核验未通过:' + verified.reason + ';未覆盖外部修改,请刷新并检查历史' });\n }\n const notified = await transaction.notifyAndVerify({ host, mvu, target: snap.target, paths,\n expected: { messageText: next } });\n if (!notified.ok) {\n stale = true; writeUncertain = true;\n return await recovery.finish({ ok: false, written: true, reason: notified.reason });\n }\n warning = notified.warnings.join(';');\n } catch (error) {\n const failed = attempted ? await transaction.compensateWrites({ host, mvu, target: snap.target,\n backup: memory, expected: { messageText: next }, written: { mvu: false, chat: false, text: true } }) : [];\n if (failed.length) { stale = true; writeUncertain = true; }\n return await recovery.finish({ ok: false, attempted, reason: error.message + (failed.length ? ';' + failed.join(';') : '') });\n } finally {\n transactionDepth--;\n if (stale) scheduleReload();\n }\n const outcome = await recovery.finish({ ok: true, operationChanges: { version: 1 },\n bodyPatchHistory: bodyHistory.createBodyPatchHistory(original, next) });\n warning = [warning, await recordActivity('addLog', { action: remove ? '删除正文声明' : '编辑正文声明', summary: ['已修改目标回复的变量指令', ...outcome.warnings].join(';'), ok: true })].filter(Boolean).join(';');\n const refreshed = await reloadAfterWrite();\n if (!refreshed.reloaded) warning = [warning, '正文已保存,重新加载未完成:' + refreshed.reloadError].filter(Boolean).join(';');\n return Object.assign(outcome, { warning }, refreshed);\n }\n\n function resetWorkingCopies(snap) {\n editor = snap ? util.deepClone(snap.mvuData.stat_data || {}) : null;\n auxiliaryEditor = snap ? {\n date: util.deepClone(snap.date),\n calendar: util.deepClone(snap.calendar),\n chatVariables: util.deepClone(snap.chatVariables || {}),\n } : null;\n currentChangeSet = null;\n ds.clearAll();\n }\n\n const runtime = {\nasync load(options = {}) {\n if (destroyed) throw new Error('修改器已销毁');\n const automatic = options.automatic === true;\n const protectedInput = () => automatic && (!panelOpen || transactionDepth > 0 || hasPendingDraft());\n if (protectedInput()) return null;\n clearReloadTimer();\n const requestId = ++loadRequestId;\n try {\n const snap = await snapshot.captureSnapshot(host, mvu);\n if (destroyed || requestId !== loadRequestId || protectedInput()) return null;\n currentSnapshot = snap;\n stale = false;\n writeUncertain = false;\n resetWorkingCopies(snap);\n draftPending = false;\n notify({ type: 'loaded', snapshot: snap });\n return snap;\n } catch (e) {\n if (destroyed || requestId !== loadRequestId || protectedInput()) return null;\n if (!destroyed && requestId === loadRequestId) {\n stale = true;\n if (!automatic) {\n currentSnapshot = null;\n resetWorkingCopies(null);\n }\n }\n notify({ type: 'load-failed', message: e && e.message ? e.message : String(e) });\n throw e;\n }\n },\n\n getEditor() { return editor; },\nsetInteractions(value) { interactions = value || {}; },\n getAuxEditor() { return auxiliaryEditor; },\n getSnapshot() { return currentSnapshot; },\n isStale() { return stale; },\n hasDrafts() { return hasPendingDraft(); },\n setPanelOpen(open) {\n panelOpen = !!open;\n if (!panelOpen) {\n clearReloadTimer();\n ++loadRequestId;\n } else if (stale) scheduleReload(0);\n },\n setDraftPending(value) {\n draftPending = !!value;\n if (draftPending) {\n // input 事件早于 change/失焦;无效 JSON 也必须使进行中的自动读取失效。\n clearReloadTimer();\n ++loadRequestId;\n } else if (stale) scheduleReload();\n },\n getChangeSet() { return currentChangeSet; },\n invalidateChangeSet() { currentChangeSet = null; },\n dirtyState() { return ds; },\nbuildChangeSet(auxiliary) {\n const snap = requireSnapshot();\n const aux = auxiliary || auxiliaryEditor || {};\n const readonlyMutation = patch.findReadonlyMutation(snap.mvuData.stat_data || {}, editor || {});\n if (readonlyMutation) throw new Error('只读字段不能修改:' + readonlyMutation);\n const normalized = {\n date: util.deepEqual(aux.date, snap.date) ? null : aux.date,\n calendar: util.deepEqual(aux.calendar, snap.calendar) ? null : aux.calendar,\n chatVariables: util.deepEqual(aux.chatVariables, snap.chatVariables) ? null : aux.chatVariables,\n };\n currentChangeSet = changeSet.buildChangeSet({\n mvuOps: patch.diffStatData(snap.mvuData.stat_data || {}, editor || {}),\n auxiliary: normalized,\n });\n return currentChangeSet;\n },\n\n preview() {\n if (!currentSnapshot) return [];\n // 工作副本可能在上次预览后继续变化;每次边界操作都重新构建,禁止复用陈旧补丁。\n runtime.buildChangeSet();\n return applyFlow.buildPreview(currentSnapshot, currentChangeSet);\n },\n\n async preflight(options = {}) {\n const snap = requireSnapshot();\n const submitted = runtime.buildChangeSet();\n transactionDepth++;\n try {\n await verifySessionTarget(snap);\n return await applyFlow.preflightOnce({ host, mvu, snapshot: snap,\n changeSet: submitted, mode: options.mode || applyMode });\n } finally {\n transactionDepth--;\n if (stale) scheduleReload();\n }\n },\nasync apply(options = {}) {\n const snap = requireSnapshot();\n // 用户可能在预览/确认前后继续修改;应用必须基于此刻工作副本重新生成 ChangeSet。\n const submitted = runtime.buildChangeSet();\n const count = submitted.mvuOps.length;\n transactionDepth++;\n let result;\n const modeUsed = options.mode || applyMode;\n const recovery = recoverySession(options.label || (modeUsed === applyFlow.APPLY_MODE.BODY ? '正文模式应用' : '直接模式应用'), modeUsed);\n try {\n await verifySessionTarget(snap);\n result = await applyFlow.applyOnce({\n host,\n mvu,\n snapshot: snap,\n changeSet: submitted,\n mode: options.mode || applyMode,\n cardHash,\n version,\n operationId: recovery.operationId,\n beforeWrite: recovery.save,\n resolveConflicts,\n });\n } finally {\n transactionDepth--;\n if (stale) scheduleReload();\n }\n await recovery.finish(result);\n if (!result.ok && result.written) { stale = true; writeUncertain = true; }\n else if (!result.ok && result.parsedAttempted) { stale = true; scheduleReload(); }\n const warnings = result.warnings || [];\n warnings.push(await recordActivity('addLog', {\n action: modeUsed === applyFlow.APPLY_MODE.BODY ? '正文模式应用' : '直接模式应用',\n summary: [result.noWrite ? '已保留当前值,未执行新的写入' : result.ok ? `成功修改 ${count} 处角色变量` : `应用失败: ${result.reason || '未知原因'}`, ...warnings].join(';'),\n ok: result.ok,\n error: result.ok ? null : result.reason,\n }));\n // 若为正文注入,记录到正文注入记录\n if (modeUsed === applyFlow.APPLY_MODE.BODY && result.ok && !result.noWrite && typeof history.addInjection === 'function') {\n warnings.push(await recordActivity('addInjection', {\n messageId: snap.target.messageId,\n count,\n summary: `向回复 #${snap.target.messageId}(候选版本 ${snap.target.swipeId + 1})记录 ${count} 条指令`,\n }));\n }\n result.warnings = warnings.filter(Boolean);\n if (result.ok) {\n Object.assign(result, await reloadAfterWrite());\n }\n return result;\n },\nasync injectBody() {\n return runtime.apply({ mode: applyFlow.APPLY_MODE.BODY, label: '正文模式应用' });\n },\n\n discard() {\n resetWorkingCopies(currentSnapshot);\n draftPending = false;\n notify({ type: 'discarded' });\n if (stale) scheduleReload(0);\n },\ndiscardSingle(pathStr) {\n const snap = requireSnapshot();\n if (pathStr.startsWith('/') && !pathStr.startsWith('/chatVariables/') && !pathStr.startsWith('/date') && !pathStr.startsWith('/calendar')) {\n const segments = patch.parsePointer(pathStr);\n let src = snap.mvuData.stat_data;\n let dst = editor;\n for (let i = 0; i < segments.length - 1; i++) {\n const k = segments[i];\n src = src ? src[k] : undefined;\n if (!dst || typeof dst !== 'object') break;\n dst = dst[k];\n }\n if (dst && typeof dst === 'object' && segments.length > 0) {\n const lastKey = segments[segments.length - 1];\n if (src && src[lastKey] !== undefined) {\n dst[lastKey] = util.deepClone(src[lastKey]);\n } else {\n delete dst[lastKey];\n }\n }\n } else if (pathStr.startsWith('/chatVariables/')) {\n const key = pathStr.slice('/chatVariables/'.length);\n if (snap.chatVariables && snap.chatVariables[key] !== undefined) {\n auxiliaryEditor.chatVariables[key] = util.deepClone(snap.chatVariables[key]);\n } else {\n delete auxiliaryEditor.chatVariables[key];\n }\n } else if (pathStr === '/date') {\n auxiliaryEditor.date = util.deepClone(snap.date);\n } else if (pathStr === '/calendar') {\n auxiliaryEditor.calendar = util.deepClone(snap.calendar);\n }\n runtime.invalidateChangeSet();\n notify({ type: 'discarded-single', path: pathStr });\n },\n\n validateEditor() { return field.validateEditor(editor, currentSnapshot && currentSnapshot.mvuData.stat_data); },\n\n setApplyMode(mode) {\n if (!Object.values(applyFlow.APPLY_MODE).includes(mode)) throw new Error('未知应用模式');\n applyMode = mode;\n notify({ type: 'mode', mode });\n },\n getApplyMode() { return applyMode; },\n async setSettings(settings = {}) {\n if (Object.prototype.hasOwnProperty.call(settings, 'applyMode')) runtime.setApplyMode(settings.applyMode);\n if (host && typeof host.setAutoSaveChat === 'function' && Object.prototype.hasOwnProperty.call(settings, 'autoSaveChat')) {\n host.setAutoSaveChat(settings.autoSaveChat);\n }\n if (host && typeof host.setSyncMvu === 'function' && Object.prototype.hasOwnProperty.call(settings, 'syncMvu')) {\n host.setSyncMvu(settings.syncMvu);\n }\n if (typeof history.setLimit === 'function' && Object.prototype.hasOwnProperty.call(settings, 'historyLimit')) {\n try { return await history.setLimit(settings.historyLimit); }\n catch (error) { return { ok: false, reason: error.message }; }\n }\n return { ok: true };\n },\n\n getHistory() { return history; },\nasync refreshHistory() {\n try {\n const result = typeof history.refresh === 'function' ? await history.refresh()\n : typeof history.ready === 'function' ? await history.ready() : { ok: true };\n if (!destroyed) notify({ type: 'history', result });\n return result || { ok: true };\n } catch (error) { return { ok: false, reason: error.message }; }\n },\n historyList() { return history.list(); },\n async clearHistory() {\n const result = await history.clear();\n notify({ type: 'history' });\n return result;\n },\n logList() { return typeof history.listLogs === 'function' ? history.listLogs() : []; },\n async clearLogs() {\n const result = typeof history.clearLogs === 'function' ? await history.clearLogs() : { ok: true };\n notify({ type: 'logs' });\n return result;\n },\n injectionList() { return typeof history.listInjections === 'function' ? history.listInjections() : []; },\n async clearInjections() {\n const result = typeof history.clearInjections === 'function' ? await history.clearInjections() : { ok: true };\n notify({ type: 'injections' });\n return result;\n },\n\n exportSnapshot() {\n const snap = requireSnapshot();\n return typeof history.exportSnapshot === 'function' ? history.exportSnapshot(snap) : JSON.stringify(snap, null, 2);\n },\n async importSnapshot(text) {\n requireSnapshot();\n if (typeof history.importSnapshot !== 'function') throw new Error('导入能力不可用');\n const data = history.importSnapshot(text);\n if (data.stat_data) editor = util.deepClone(data.stat_data);\n if (data.date !== undefined && auxiliaryEditor) auxiliaryEditor.date = util.deepClone(data.date);\n if (data.calendar !== undefined && auxiliaryEditor) auxiliaryEditor.calendar = util.deepClone(data.calendar);\n if (data.chatVariables !== undefined && auxiliaryEditor) auxiliaryEditor.chatVariables = util.deepClone(data.chatVariables);\n runtime.invalidateChangeSet();\n // 工作副本已替换,必须在等待日志前保护输入,外部消息事件只能标记过期。\n clearReloadTimer();\n ++loadRequestId;\n draftPending = true;\n notify({ type: 'imported' });\n const warning = await recordActivity('addLog', { action: '导入快照', summary: '快照数据已导入到当前工作副本', ok: true });\n return { ok: true, warning, stale };\n },\n\n async rollback(entryOrIndex, options = {}) {\n if (typeof history.ready === 'function') {\n const ready = await history.ready();\n if (ready && ready.ok === false) return ready;\n }\n const entry = typeof entryOrIndex === 'number' ? history.list()[entryOrIndex] : entryOrIndex;\n if (!historyModule.getOperationState(entry).canRestore) return { ok: false, reason: '此记录对应的操作未写入数据,无需回滚' };\n const recovery = recoverySession('回滚前备份', 'rollback');\n let result;\n transactionDepth++;\n try {\n result = await applyFlow.restoreBackup({ host, mvu, backup: entry,\n fullSnapshot: options.fullSnapshot === true, resolveConflicts,\n operationId: recovery.operationId, beforeWrite: recovery.save });\n } finally {\n transactionDepth--;\n if (stale) scheduleReload();\n }\n await recovery.finish(result);\n if (!result.ok && result.written) { stale = true; writeUncertain = true; }\n if (result.ok) {\n const warning = await recordActivity('addLog', { action: '历史回滚', summary: [`已成功回滚到 ${entry.label || '历史记录'}`, ...result.warnings].join(';'), ok: true });\n result.warnings = (result.warnings || []).concat(warning || []);\n Object.assign(result, await reloadAfterWrite());\n } else {\n const warning = await recordActivity('addLog', { action: '回滚失败', summary: result.reason || '回滚异常', ok: false, error: result.reason });\n result.warnings = (result.warnings || []).concat(warning || []);\n }\n return result;\n },\n\n async reprocessMvu() {\n const snap = requireSnapshot();\n const recovery = recoverySession('重新处理变量前备份', 'reprocess');\n let memory;\n let expected;\n let attempted = false;\n let parsedAttempted = false;\n let warning = '';\n let operationChanges;\n transactionDepth++;\n try {\n await verifyEditTarget(snap, snap.messageText, 'reprocess-mvu');\n await mvu.ready();\n const msgText = await host.readMessageText(snap.target);\n if (!bodyHistory.sameInstructions(msgText, snap.messageText)) throw new Error('目标正文已变化,请重新确认指令后再处理');\n const declared = bodyMerge.parseExistingOps(msgText);\n const initial = util.deepClone(await mvu.read(snap.target));\n if (!util.isPlainObject(initial) || !util.isPlainObject(initial.stat_data)) throw new Error('MVU 未返回有效的角色变量');\n // 长期备份记录事务开始时的快照;先完成存储等待,再把最新完整数据交给 MVU。\n const checkpoint = transaction.createBackup({ target: snap.target, messageVariables: initial, cardHash, version });\n await recovery.save(transaction.prepareHistoryBackup(checkpoint, { scopes: { mvu: true, chat: false, text: false } }));\n await verifyEditTarget(snap, msgText, 'reprocess-before-parse');\n let baseline = util.deepClone(await mvu.read(snap.target));\n let parseText = msgText;\n const intended = operationEdits.intent(initial.stat_data, declared);\n if (!concurrency.assertMergeablePaths(initial.stat_data, intended, baseline.stat_data, declared).ok) {\n const resolved = await operationEdits.resolveLive(async () => {\n await verifyEditTarget(snap, msgText, 'reprocess-before-conflict');\n baseline = util.deepClone(await mvu.read(snap.target));\n return baseline.stat_data;\n }, initial.stat_data, intended, declared, resolveConflicts, '正文指令目标');\n const selected = patch.diffStatData(baseline.stat_data, resolved.data);\n if (!selected.length) return await recovery.finish({ ok: true, noWrite: true, warnings: ['已保留当前值,没有重新执行指令'] });\n parseText = applyFlow.serializeOps(selected);\n }\n parsedAttempted = true;\n const parsed = await mvu.parse(parseText, util.deepClone(baseline));\n if (!util.isPlainObject(parsed) || !util.isPlainObject(parsed.stat_data)) throw new Error('MVU 未返回有效的角色变量');\n await verifyEditTarget(snap, msgText, 'reprocess-write');\n const merged = await operationEdits.resolveMvu(async () => {\n await verifyEditTarget(snap, msgText, 'reprocess-conflict');\n return util.deepClone(await mvu.read(snap.target));\n }, baseline, parsed, resolveConflicts, bodyMerge.parseExistingOps(parseText).map(op => Object.assign({}, op, {\n path: '/stat_data' + op.path, ...(op.from ? { from: '/stat_data' + op.from } : {}),\n })));\n const footprint = merged.paths;\n memory = transaction.createBackup({ target: snap.target, messageVariables: merged.current, cardHash, version });\n expected = { messageVariables: util.deepClone(merged.data) };\n operationChanges = operationEdits.operationChanges({ messageVariables: operationEdits.amendBefore(baseline, merged.overrides) }, expected, { mvu: footprint });\n await verifyEditTarget(snap, msgText, 'reprocess-final');\n attempted = true;\n await mvu.replace(snap.target, merged.data);\n const verified = await transaction.verifyStoredState({ host, mvu, target: snap.target, expected, paths: { mvu: footprint } });\n if (!verified.ok) {\n stale = true; writeUncertain = true;\n return await recovery.finish({ ok: false, written: true, reason: '变量写后核验未通过:' + verified.reason + ';未覆盖外部修改,请刷新并检查历史' });\n }\n } catch (e) {\n const failed = attempted ? await transaction.compensateWrites({ host, mvu, target: snap.target,\n backup: memory, expected, written: { mvu: true, chat: false, text: false } }) : [];\n if (attempted && failed.length) { stale = true; writeUncertain = true; }\n else if (parsedAttempted) stale = true;\n warning = await recordActivity('addLog', { action: '重新处理失败', summary: e.message || String(e), ok: false, error: e.message });\n return await recovery.finish({ ok: false, attempted, parsedAttempted, warning,\n warnings: parsedAttempted && !attempted ? ['解析期间可能运行了角色卡自动化,已保留恢复记录;本次未完成变量写入。'] : [],\n reason: '变量处理失败: ' + (e.message || String(e)) + (failed.length ? ';' + failed.join(';') : '') });\n } finally {\n transactionDepth--;\n if (stale) scheduleReload();\n }\n const result = await recovery.finish({ ok: true, attempted, operationChanges });\n warning = await recordActivity('addLog', { action: '重新处理变量', summary: ['重新解析当前正文并写回变量', ...result.warnings].join(';'), ok: true });\n const refreshed = await reloadAfterWrite();\n return Object.assign(result, { warning, message: refreshed.reloaded\n ? '已重新执行正文指令并保存变量'\n : '变量已保存,但重新加载未完成:' + refreshed.reloadError }, refreshed);\n },\n\n async updateDeclaredOp(blockIndex, opIndex, newOp) {\n return writeDeclaration(blockIndex, opIndex, newOp, false);\n },\n\n async removeDeclaredOp(blockIndex, opIndex) {\n return writeDeclaration(blockIndex, opIndex, null, true);\n },\n\n async setReincarnation(points) {\n return reincarnation.setReincarnationPoints({ points, getGlobal: extras.getGlobal });\n },\n readReincarnation() {\n return reincarnation.readReincarnationPoints({ getGlobal: extras.getGlobal });\n },\n\n subscribe(listener) {\n subscribers.add(listener);\n return () => subscribers.delete(listener);\n },\n addCleanup(fn) { life.addCleanup(fn); },\n\n start() {\n if (started) return () => runtime.destroy();\n started = true;\n const win = extras.getWindow ? extras.getWindow() : (typeof window !== 'undefined' ? window : null);\n const par = extras.getParent ? extras.getParent() : (win && win.parent) || win;\n const source = hostAdapter.getEventSource(win, par);\n const types = hostAdapter.getEventTypes(win, par);\n const registered = new Set(lifecycle.pickEventNames(types));\n life.registerHostListeners(source, types);\n if (typeof mvu.getUpdateEventNames === 'function') {\n Promise.resolve().then(async () => {\n if (destroyed) return;\n await mvu.ready();\n if (destroyed) return;\n const names = mvu.getUpdateEventNames().filter(name => typeof name === 'string' && !registered.has(name));\n life.registerHostListeners(source, names);\n }).catch(() => { /* MVU 未就绪时保留宿主通知路径;不启动轮询或请求网络。 */ });\n }\n return () => runtime.destroy();\n },\n\n destroy() {\n if (destroyed) return;\n destroyed = true;\n clearReloadTimer();\n life.destroy();\n if (host && typeof host.destroy === 'function') host.destroy();\n if (mvu && typeof mvu.destroy === 'function') mvu.destroy();\n if (typeof history.destroy === 'function') history.destroy();\n subscribers.clear();\n },\n };\n\n return runtime;\n}\nfunction boot() {\n const P = (typeof window !== 'undefined' && (window.parent || window)) || null;\n if (!P) return;\n if (P.__MINGDING_MODIFIER__) return;\n\n const host = hostAdapter.createHostAdapter();\n const mvu = mvuAdapter.createMvuAdapter();\n const mod = createModifier({\n host,\n mvu,\n extras: {\n version: release.DISPLAY_VERSION,\n cardHash: '85E9D7C050D58FA3F6B29EFECF82F32358EB532CA707E412C676A3BAEC8EB0B4',\n getWindow: () => window,\n getParent: () => P,\n getGlobal: () => window,\n },\n });\n P.__MINGDING_MODIFIER__ = mod;\n P[release.MARKER] = mod;\n mod.addCleanup(() => {\n panel.unmount(P);\n if (P.__MINGDING_MODIFIER__ === mod) delete P.__MINGDING_MODIFIER__;\n if (P[release.MARKER] === mod) delete P[release.MARKER];\n });\n mod.start();\n panel.mount(P, mod);\n\n if (typeof window.addEventListener === 'function') {\n const onPageHide = () => mod.destroy();\n window.addEventListener('pagehide', onPageHide, { once: true });\n mod.addCleanup(() => window.removeEventListener('pagehide', onPageHide));\n }\n}\n\nmodule.exports = { createModifier, boot };\n\n});\n\n__define('ui/a11y', function (module, exports, require) {\nconst util = require('../util');\nconst ARIA = require('./styles').ARIA;\nfunction label(forId, text) {\n return '<label class=\"md-label\" for=\"' + util.esc(forId) + '\">' + util.esc(text) + '</label>';\n}\nfunction tabBtn(opts) {\n const sel = opts.selected ? ' aria-selected=\"true\"' : ' aria-selected=\"false\"';\n const role = ' role=\"' + ARIA.TAB_ROLE + '\"';\n const ariacontrols = opts.panelId ? ' aria-controls=\"' + util.esc(opts.panelId) + '\"' : '';\n return '<button type=\"button\" id=\"' + util.esc(opts.id) + '\"' + role + sel + ariacontrols +\n ' tabindex=\"' + (opts.selected ? '0' : '-1') + '\" class=\"md-tab\">' + util.esc(opts.text) + '</button>';\n}\nfunction tabPanel(id, labelledBy, content) {\n return '<div id=\"' + util.esc(id) + '\" role=\"' + ARIA.TABPANEL_ROLE + '\" aria-labelledby=\"' + util.esc(labelledBy) + '\">' + content + '</div>';\n}\nfunction collapseHeader(opts) {\n const exp = opts.expanded ? 'true' : 'false';\n return '<div class=\"md-collapse\"><button type=\"button\" aria-expanded=\"' + exp + '\" ' +\n 'aria-controls=\"' + util.esc(opts.contentId) + '\" id=\"' + util.esc(opts.id) + '\">' +\n util.esc(opts.buttonLabel) + '</button></div>';\n}\nfunction dialog(opts) {\n return '<div id=\"' + util.esc(opts.id) + '\" role=\"' + ARIA.DIALOG_ROLE + '\" aria-modal=\"true\" ' +\n 'aria-labelledby=\"' + util.esc(opts.id) + '-title\" class=\"md-dialog\">' +\n '<h2 id=\"' + util.esc(opts.id) + '-title\">' + util.esc(opts.title) + '</h2>' +\n '<div class=\"md-dialog-body\">' + opts.content + '</div>' +\n '<button type=\"button\" class=\"md-dialog-close\" data-act=\"close\" aria-label=\"关闭\">关闭</button></div>';\n}\nfunction liveRegion(id, text, assertiveness) {\n const level = assertiveness === 'assertive' ? 'assertive' : 'polite';\n return '<div id=\"' + util.esc(id) + '\" aria-live=\"' + level + '\" class=\"md-live\">' + util.esc(text) + '</div>';\n}\n\nmodule.exports = { label, tabBtn, tabPanel, collapseHeader, dialog, liveRegion, ARIA };\n\n});\n\n__define('ui/dirty-state', function (module, exports, require) {\nfunction createDirtyState() {\n const dirty = new Map(); // tab -> Set<path>\n\n return {\n markDirty(tab, path) {\n if (!tab) return;\n if (!dirty.has(tab)) dirty.set(tab, new Set());\n if (path) dirty.get(tab).add(path);\n },\n\n clearDirty(tab) {\n if (dirty.has(tab)) dirty.delete(tab);\n },\n\n removeDirty(tab, path) {\n if (!tab || !dirty.has(tab)) return;\n const set = dirty.get(tab);\n if (path) set.delete(path);\n if (set.size === 0) dirty.delete(tab);\n },\n\n clearAll() {\n dirty.clear();\n },\n\n isDirty(tab) {\n return dirty.has(tab) && dirty.get(tab).size > 0;\n },\n\n dirtyPaths(tab) {\n return dirty.has(tab) ? Array.from(dirty.get(tab)) : [];\n },\n\n total() {\n let n = 0;\n for (const set of dirty.values()) n += set.size;\n return n;\n },\n\n tabs() {\n return Array.from(dirty.keys());\n },\n };\n}\n\nmodule.exports = { createDirtyState };\n\n});\n\n__define('ui/editor-registry', function (module, exports, require) {\nconst util = require('../util');\nconst patch = require('../data/patch');\nconst DEFINITIONS = Object.freeze({\n status: {\n label: '状态效果',\n fields: [\n ['类型', 'select', ['增益', '减益', '特殊']],\n ['效果', 'textarea'],\n ['层数', 'integer'],\n ['剩余时间', 'text'],\n ['来源', 'text'],\n ],\n create: () => ({ 类型: '增益', 效果: '', 层数: 1, 剩余时间: '1回合', 来源: '修改器' }),\n },\n inventory: {\n label: '背包',\n fields: [['品质', 'text'], ['类型', 'text'], ['数量', 'number'], ['标签', 'csv'], ['效果', 'textarea'], ['描述', 'textarea']],\n create: () => ({ 品质: '普通', 类型: '物品', 数量: 1, 标签: [], 效果: {}, 描述: '' }),\n },\n equipment: {\n label: '装备',\n fields: [['品质', 'text'], ['类型', 'text'], ['位置', 'text'], ['标签', 'csv'], ['效果', 'textarea'], ['描述', 'textarea']],\n create: () => ({ 品质: '普通', 类型: '装备', 位置: '', 标签: [], 效果: {}, 描述: '' }),\n },\n skill: {\n label: '技能',\n fields: [['品质', 'text'], ['类型', 'text'], ['消耗', 'textarea'], ['标签', 'csv'], ['效果', 'textarea'], ['描述', 'textarea']],\n create: () => ({ 品质: '普通', 类型: '技能', 消耗: '', 标签: [], 效果: {}, 描述: '' }),\n },\n asset: {\n label: '资产',\n fields: [['品质', 'text'], ['类型', 'text'], ['位置', 'text'], ['标签', 'csv'], ['总空间', 'text'], ['结算', 'textarea'], ['描述', 'textarea'], ['内部资产', 'tree']],\n create: () => ({ 品质: '普通', 类型: '资产', 位置: '', 标签: [], 总空间: '', 结算: '', 描述: '', 内部资产: {} }),\n },\n task: {\n label: '任务',\n fields: [['状态', 'text'], ['关注度', 'text'], ['进展', 'textarea'], ['详情', 'textarea'], ['目标', 'textarea'], ['奖励', 'textarea']],\n create: () => ({ 状态: '进行中', 关注度: '中', 进展: '', 详情: '', 目标: '', 奖励: '' }),\n },\n news: {\n label: '新闻',\n fields: [['标题', 'text'], ['内容', 'textarea']],\n create: () => ({ 标题: '', 内容: '' }),\n },\n ascensionStep: {\n label: '长阶条目',\n fields: [['被动效果', 'textarea'], ['主动效果', 'textarea'], ['描述', 'textarea']],\n create: () => ({ 被动效果: '', 主动效果: '', 描述: '' }),\n },\n});\n\nconst KEY_KIND = Object.freeze({\n '状态效果': 'status',\n '背包': 'inventory',\n '装备': 'equipment',\n '技能': 'skill',\n '资产': 'asset',\n});\n\nfunction getDefinition(kind) {\n return DEFINITIONS[kind] || null;\n}\n\nfunction kindForKey(key) {\n return KEY_KIND[key] || 'tree';\n}\n\nfunction createDefault(kind) {\n const definition = getDefinition(kind);\n return definition && definition.create ? definition.create() : {};\n}\nfunction inferType(value) {\n if (typeof value === 'boolean') return 'boolean';\n if (typeof value === 'number') return Number.isInteger(value) ? 'integer' : 'number';\n if (Array.isArray(value) && value.every((item) => typeof item === 'string')) return 'lines';\n if (value && typeof value === 'object') return 'tree';\n return typeof value === 'string' && value.length > 80 ? 'textarea' : 'text';\n}\n\nfunction hasReadonly(value) {\n return !!value && typeof value === 'object' && Object.entries(value).some(([key, child]) => key.startsWith('_') || hasReadonly(child));\n}\n\nfunction isFixedField(path, key) {\n const parts = patch.parsePointer(path);\n const kind = parts[parts.length - 2] === '内部资产' ? 'inventory' : kindForKey(parts[parts.length - 2]);\n const definition = getDefinition(kind);\n return !!(definition && definition.fields.some(([name]) => name === key)) ||\n path === '/事件' && ['开启', '结束', '标题', '阶段', '已完成事件', '信号'].includes(key);\n}\nfunction assertPreserved(original, next, path = '') {\n if (path === '/事件/信号' && (!Array.isArray(next) || !Array.isArray(original) || original.some((v, i) => v !== next[i]))) {\n throw new Error('事件信号只能追加,不能覆盖、删除或重排');\n }\n if (!original || typeof original !== 'object') return;\n for (const [key, child] of Object.entries(original)) {\n const replacement = next && typeof next === 'object' ? next[key] : undefined;\n if (key.startsWith('_') && !util.deepEqual(child, replacement)) throw new Error('不能删除或改动包含只读字段的内容');\n assertPreserved(child, replacement, path + '/' + patch.escapeSegment(key));\n }\n}\nfunction editTree(root, path, action, key, nextName, type = 'text', statData = true) {\n if (typeof path !== 'string' || path && !path.startsWith('/')) throw new Error('字段路径无效');\n const parts = patch.parsePointer(path);\n if (parts.some((part) => !util.isSafeKey(part) || part.startsWith('_'))) throw new Error('此字段不可写');\n const target = patch.getPath(root, parts);\n if (!target || typeof target !== 'object') throw new Error('当前内容不是字段集合');\n if (statData && (path === '/事件/信号' || path.startsWith('/事件/信号/'))) throw new Error('请使用事件页的追加信号入口');\n const array = Array.isArray(target);\n const safeName = (name) => {\n if (!name || !util.isSafeKey(name) || name.startsWith('_')) throw new Error('名称不能为空,也不能使用只读或保留名称');\n return name;\n };\n if (action === 'add') {\n key = array ? String(target.length) : safeName(nextName);\n if (Object.prototype.hasOwnProperty.call(target, key)) throw new Error('已存在同名字段,请更换名称');\n const values = { text: '', number: 0, boolean: false, object: {}, array: [] };\n if (!Object.prototype.hasOwnProperty.call(values, type)) throw new Error('不支持的字段类型');\n target[key] = values[type];\n } else {\n if (!Object.prototype.hasOwnProperty.call(target, key)) throw new Error('字段已不存在,请刷新当前编辑器');\n safeName(key);\n if (statData && isFixedField(path, key)) throw new Error('角色卡固定字段不能删除或改名');\n if (hasReadonly(target[key])) throw new Error('此内容包含只读字段,不能删除或改名');\n assertPreserved(target[key], undefined, statData ? path + '/' + patch.escapeSegment(key) : '');\n if (array && (!/^(0|[1-9]\\d*)$/.test(key) || Number(key) >= target.length)) throw new Error('列表条目无效');\n if (action === 'remove') {\n if (array) {\n if (target.slice(Number(key)).some(hasReadonly)) throw new Error('删除将移动后续只读条目,已阻止');\n target.splice(Number(key), 1);\n } else delete target[key];\n } else if (action === 'rename' && !array) {\n safeName(nextName);\n if (nextName !== key && Object.prototype.hasOwnProperty.call(target, nextName)) throw new Error('已存在同名字段,请更换名称');\n if (nextName !== key) { target[nextName] = target[key]; delete target[key]; }\n } else throw new Error('不支持的字段操作');\n }\n return target;\n}\n\nmodule.exports = { DEFINITIONS, getDefinition, kindForKey, createDefault, inferType, hasReadonly, isFixedField, assertPreserved, editTree };\n\n});\n\n__define('ui/panel', function (module, exports, require) {\nconst styles = require('./styles');\nconst util = require('../util');\nconst patch = require('../data/patch');\nconst bodyMerge = require('../apply/body-merge');\nconst release = require('../version');\nconst uiStateModule = require('./ui-state');\nconst editorRegistry = require('./editor-registry');\nconst playerHelp = require('./player-help');\nconst { getBackupScope } = require('../apply/transaction');\nconst { getOperationState } = require('../data/history');\n\nconst CLS = styles.CLS;\nconst BUSINESS_TABS = [\n { key: 'protagonist', label: '主角', icon: 'user' },\n { key: 'npc', label: 'NPC', icon: 'group' },\n { key: 'world', label: '世界', icon: 'globe' },\n { key: 'calendar', label: '日历', icon: 'calendar' },\n { key: 'start', label: '开局', icon: 'spark' },\n { key: 'templates', label: '模板变量', icon: 'variables' },\n];\nconst TOOL_TABS = [\n { key: 'operations', label: '操作', icon: 'wand' },\n { key: 'interpretation', label: '变量解读', icon: 'edit' },\n { key: 'history', label: '历史', icon: 'history' },\n { key: 'settings', label: '设置', icon: 'gear' },\n { key: 'about', label: '关于', icon: 'question' },\n];\nconst RESOURCE_KEYS = [['生命值', 'hp'], ['法力值', 'mp'], ['体力值', 'sp']];\nconst ATTR_KEYS = [['力量', 'str'], ['敏捷', 'agi'], ['体质', 'con'], ['智力', 'int'], ['精神', 'wis']];\nconst COMPLEX_KEYS = ['状态效果', '背包', '装备', '技能', '资产', '登神长阶'];\n\nconst ICON_PATHS = Object.freeze({\n spark: 'M12 2l2.7 7.3L22 12l-7.3 2.7L12 22l-2.7-7.3L2 12l7.3-2.7z',\n wand: 'M7.5 5.6L10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29c-.39-.39-1.02-.39-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41l-2.33-2.35zm-1.03 5.49l-2.12-2.12 2.44-2.44 2.12 2.12-2.44 2.44z',\n user: 'M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5Zm0 2c-3.33 0-10 1.67-10 5v3h20v-3c0-3.33-6.67-5-10-5Z',\n group: 'M9 11a4 4 0 1 0-4-4 4 4 0 0 0 4 4Zm8 1a3 3 0 1 0-3-3 3 3 0 0 0 3 3ZM9 13c-2.67 0-8 1.34-8 4v2h10v-2a5 5 0 0 1 .4-2A8 8 0 0 0 9 13Zm8 1c-1.2 0-2.6.3-3.7.8A5 5 0 0 1 14 17v2h10v-2c0-2.66-5.34-4-7-4Z',\n globe: 'M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm6.9 9h-3a16 16 0 0 0-1.1-5A8 8 0 0 1 18.9 11ZM12 4c1 1.2 1.7 3.7 1.9 7h-3.8c.2-3.3.9-5.8 1.9-7Zm-1.9 9h3.8c-.2 3.3-.9 5.8-1.9 7-1-1.2-1.7-3.7-1.9-7Z',\n calendar: 'M19 4h-1V2h-2v2H8V2H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Zm0 16H5V10h14Z',\n variables: 'M4 5h10v3H4zm0 6h16v3H4zm0 6h12v3H4z',\n edit: 'M3 17.25V21h3.75L17.8 9.94l-3.75-3.75L3 17.25ZM20.7 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75Z',\n history: 'M13 3a9 9 0 1 0 8.5 6h-2.2A7 7 0 1 1 13 5v3l5-4-5-4Zm-2 5h2v5l4 2-1 1.7-5-2.7Z',\n gear: 'M12 8.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Zm8.7 4.8-2-1.5.1-1.5 2-1.6-2-3.4-2.5 1a8 8 0 0 0-1.3-.8L14.7 2h-4l-.4 2.5-1.3.8-2.5-1-2 3.4 2 1.6.1 1.5-2 1.5 2 3.5 2.5-1 1.3.7.4 2.6h4l.4-2.6 1.3-.7 2.5 1Z',\n question: 'M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm1 17h-2v-2h2Zm2-7.7-.9.9A3 3 0 0 0 13 14h-2a4 4 0 0 1 1-2.8l1.1-1.1A1.5 1.5 0 1 0 10.5 9h-2a3.5 3.5 0 1 1 6.5 2.3Z',\n save: 'M4 3h14l2 2v16H4Zm3 2v5h10V5Zm1 9v5h8v-5Z',\n close: 'M6 6l12 12m0-12L6 18',\n});\n\nfunction icon(name, size = 14) {\n const path = ICON_PATHS[name] || ICON_PATHS.spark;\n const stroke = name === 'close' ? ' fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"' : '';\n return `<svg class=\"md-icon\" viewBox=\"0 0 24 24\" width=\"${size}\" height=\"${size}\" aria-hidden=\"true\"><path d=\"${path}\"${stroke}></path></svg>`;\n}\n\nfunction helpBox(content) {\n return `<div class=\"md-help-box\">${content}</div>`;\n}\nfunction playerFailure(message) {\n const text = String(message || '操作未完成');\n if (/quota|exceeded.*storage|配额|存储空间不足/i.test(text)) return '浏览器存储空间不足,请查看本次操作结果与详情。';\n if (/IndexedDB|数据库.*不可用|备份存储.*不可用|数据库.*失败/i.test(text)) return '备份存储不可用,请查看本次操作结果与详情。';\n return text.length > 180 ? '操作未完成,请查看详情后再处理。' : text;\n}\n\nfunction classifyGroup(path) {\n const p = String(path || '');\n if (p.startsWith('/主角') || p.startsWith('/命运点数')) {\n return { name: '主角', icon: 'user', badgeCls: 'md-badge-protagonist', tab: 'protagonist' };\n }\n if (p.startsWith('/关系列表')) {\n return { name: 'NPC', icon: 'group', badgeCls: 'md-badge-npc', tab: 'npc' };\n }\n if (p.startsWith('/世界') || p.startsWith('/事件') || p.startsWith('/任务列表') || p.startsWith('/新闻')) {\n return { name: '世界与事件', icon: 'globe', badgeCls: 'md-badge-world', tab: 'world' };\n }\n if (p.startsWith('/date') || p.startsWith('/calendar')) {\n return { name: '日历', icon: 'calendar', badgeCls: 'md-badge-calendar', tab: 'calendar' };\n }\n if (p.startsWith('/chat') || p.startsWith('/chatVariables')) {\n return { name: '模板变量', icon: 'variables', badgeCls: 'md-badge-templates', tab: 'templates' };\n }\n return { name: '其他与扩展', icon: 'spark', badgeCls: 'md-badge-direct', tab: 'world' };\n}\nfunction syncImportedDirty(mod, dirty) {\n dirty.clearAll();\n try {\n for (const item of mod.preview()) dirty.markDirty(classifyGroup(item.path).tab, item.path);\n return '';\n } catch (error) {\n dirty.markDirty('history', '/imported-snapshot');\n return error.message || String(error);\n }\n}\n\nfunction esc(value) { return util.esc(value); }\nfunction encodeKey(value) { return encodeURIComponent(String(value)).replace(/-/g, '%2D'); }\nfunction decodeKey(value) { return decodeURIComponent(String(value)); }\nfunction pointer(parts) { return patch.joinPointer(parts); }\nfunction clampFloatingPosition(x, y, viewportWidth, viewportHeight, size = 52) {\n const value = uiStateModule.clampGeometry({ x, y }, viewportWidth, viewportHeight, size, size);\n return { x: value.x, y: value.y };\n}\nfunction loadPosition(P) {\n const geometry = uiStateModule.loadPersistent(P).geometry;\n return { x: geometry.x, y: geometry.y };\n}\n\nfunction textInput(id, value, attrs = '') {\n return `<input type=\"text\" id=\"${id}\" value=\"${esc(value == null ? '' : value)}\" ${attrs}>`;\n}\n\nfunction numberInput(id, value, attrs = '') {\n return `<input type=\"number\" id=\"${id}\" value=\"${esc(value == null ? '' : value)}\" ${attrs}>`;\n}\n\nfunction fieldRow(id, label, control, hint = '') {\n return `<div class=\"${CLS.FIELD}\"><label for=\"${id}\">${esc(label)}</label><div class=\"md-control\">${control}` +\n `${hint ? `<div class=\"md-hint\">${esc(hint)}</div>` : ''}<div class=\"md-field-error\" data-error-for=\"${id}\" aria-live=\"polite\"></div></div></div>`;\n}\n\nfunction section(title, body, open = false, badge = '', cardId = '', iconName = 'spark') {\n const stableId = cardId || `card-${encodeKey(title)}`;\n const help = playerHelp.cards[title];\n return `<details class=\"md-section\" data-card-id=\"${esc(stableId)}\" ${open ? 'open' : ''}><summary aria-expanded=\"${open ? 'true' : 'false'}\">${icon(iconName)}<span>${esc(title)}</span>${badge ? `<span class=\"md-count\">${esc(badge)}</span>` : ''}</summary>` +\n `<div class=\"md-section-body\">${help ? `<p class=\"md-note\">${esc(help)}</p>` : ''}${body}</div></details>`;\n}\n\nfunction bindAttrs(path, type, tab, extra = '') {\n return `data-bind=\"${esc(path)}\" data-type=\"${type}\" data-dirty-tab=\"${tab}\" ${extra}`;\n}\n\nfunction auxAttrs(root, path, type, tab, extra = '') {\n return `data-aux-root=\"${root}\" data-aux-path=\"${esc(path)}\" data-type=\"${type}\" data-dirty-tab=\"${tab}\" ${extra}`;\n}\n\nfunction pretty(value) {\n try { return JSON.stringify(value == null ? {} : value, null, 2); }\n catch (e) { return '{}'; }\n}\n\nfunction valueSummary(value) {\n if (value === undefined) return '(删除)';\n const text = typeof value === 'string' ? value : pretty(value);\n return text.length > 500 ? text.slice(0, 500) + '…' : text;\n}\nfunction editableView(value) {\n if (Array.isArray(value)) return value.map(editableView);\n if (!util.isPlainObject(value)) return util.deepClone(value);\n const out = {};\n for (const [key, child] of Object.entries(value)) {\n if (key.charAt(0) !== '_') out[key] = editableView(child);\n }\n return out;\n}\nfunction restoreReadonly(original, edited) {\n if (Array.isArray(edited)) {\n return edited.map((value, index) => restoreReadonly(Array.isArray(original) ? original[index] : undefined, value));\n }\n if (!util.isPlainObject(edited)) return util.deepClone(edited);\n const out = {};\n for (const [key, value] of Object.entries(edited)) {\n if (key.charAt(0) === '_') continue;\n out[key] = restoreReadonly(util.isPlainObject(original) ? original[key] : undefined, value);\n }\n if (util.isPlainObject(original)) {\n for (const [key, value] of Object.entries(original)) {\n if (key.charAt(0) === '_') out[key] = util.deepClone(value);\n }\n }\n return out;\n}\n\nfunction countReadonly(value) {\n if (!value || typeof value !== 'object') return 0;\n if (Array.isArray(value)) return value.reduce((sum, item) => sum + countReadonly(item), 0);\n return Object.entries(value).reduce((sum, [key, child]) => sum + (key.charAt(0) === '_' ? 1 : 0) + countReadonly(child), 0);\n}\n\nfunction jsonEditor(id, label, value, dataAttrs, open = false, uiState = null, flat = false) {\n const readonlyCount = countReadonly(value);\n const note = readonlyCount ? `;已隐藏并保留 ${readonlyCount} 个只读 _ 字段` : '';\n const deferred = uiState && uiState.loadedJson instanceof Set && !uiState.loadedJson.has(id) && !open;\n if (deferred) {\n return section(label,\n `<div class=\"${CLS.EMPTY}\" data-deferred-json=\"${id}\">展开后按需创建编辑器,避免初次打开时渲染大型变量集合。</div>`,\n false,\n Object.keys(value || {}).length\n );\n }\n const editable = editableView(value == null ? {} : value);\n const content =\n `<div class=\"md-json-tools\"><span>${esc(Object.keys(value || {}).length)} 项${esc(note)}</span><button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"format-json\" data-target=\"${id}\">整理缩进</button></div>` +\n `<input type=\"search\" class=\"md-json-filter\" data-json-filter=\"${id}\" placeholder=\"筛选顶层键(按需解析,最多显示 40 项)\" aria-label=\"筛选 ${esc(label)} 顶层键\">` +\n `<div class=\"md-json-index\" data-json-index-for=\"${id}\" hidden></div>` +\n `<textarea id=\"${id}\" class=\"md-json\" spellcheck=\"false\" ${dataAttrs}>${esc(pretty(editable))}</textarea>` +\n `<div class=\"md-field-error\" data-error-for=\"${id}\" aria-live=\"polite\"></div>`;\n return flat ? content : section(label, content,\n open || !!(uiState && uiState.loadedJson instanceof Set && uiState.loadedJson.has(id)),\n Object.keys(value || {}).length\n );\n}\nfunction deferredView(state, id, render) {\n if (!state || !(state.deferredViews instanceof Map)) return render();\n state.deferredViews.set(id, render);\n return `<div data-md-deferred=\"${esc(id)}\"></div>`;\n}\nfunction subgroup(title, content, id, open = true, state = null, pathText = '') {\n const help = playerHelp.cards[title];\n return `<details class=\"md-subgroup\" data-card-id=\"${esc(id)}\" data-view-path=\"${esc(pathText)}\" ${open ? 'open' : ''}><summary>${esc(title)}</summary>` +\n `${help ? `<p class=\"md-note\">${esc(help)}</p>` : ''}<div class=\"md-subgroup-body\">${typeof content === 'function' ? deferredView(state, id, content) : content}</div></details>`;\n}\n\nfunction structureAttrs(pathText, type, tab, extra = '') {\n return `data-structure-bind=\"${esc(pathText)}\" data-type=\"${type}\" data-dirty-tab=\"${tab}\" ${extra}`;\n}\n\nfunction auxStructureAttrs(rootName, pathText, type, tab, extra = '') {\n return `data-structure-aux-root=\"${rootName}\" data-structure-bind=\"${esc(pathText)}\" data-type=\"${type}\" data-dirty-tab=\"${tab}\" ${extra}`;\n}\n\nfunction structuredControl(id, label, value, type, attrs, hint = '', choices = []) {\n let control;\n if (type === 'boolean') {\n control = `<select id=\"${id}\" ${attrs}><option value=\"true\" ${value ? 'selected' : ''}>是</option><option value=\"false\" ${value ? '' : 'selected'}>否</option></select>`;\n } else if (type === 'select') {\n control = `<select id=\"${id}\" ${attrs}>${choices.map((item) => `<option value=\"${esc(item)}\" ${item === value ? 'selected' : ''}>${esc(item)}</option>`).join('')}</select>`;\n } else if (type === 'textarea' || type === 'lines') {\n const text = type === 'lines' && Array.isArray(value) ? value.join('\\n') : value == null ? '' : value;\n control = `<textarea id=\"${id}\" ${attrs}>${esc(text)}</textarea>`;\n } else if (type === 'number' || type === 'integer') {\n control = numberInput(id, value, attrs);\n } else {\n control = textInput(id, value, attrs);\n }\n return fieldRow(id, label, control, hint);\n}\n\nfunction treeTools(path, tab, id, options, key, value, array) {\n const attrs = `data-tree-path=\"${esc(path)}\" data-tree-aux=\"${esc(options.auxRoot || '')}\" data-dirty-tab=\"${tab}\" data-tree-id=\"${id}\" data-tree-scope=\"${options.scope}\"`;\n const button = (action, label, extra = '') => `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"tree-${action}\" ${attrs} ${extra}>${label}</button>`;\n if (key !== undefined) {\n if (key.startsWith('_') || editorRegistry.hasReadonly(value) || !options.auxRoot && editorRegistry.isFixedField(path, key)) return '';\n const keyAttr = `data-tree-key=\"${esc(key)}\"`;\n return `<div class=\"${options.stringList ? 'md-list-actions' : 'md-item-name-row'}\">${array ? '' : textInput(id + '-name', key, `aria-label=\"${options.effects ? '效果名称' : '字段名称'}\"`) + button('rename', '改名', keyAttr)}` +\n button('remove', options.stringList ? icon('close') : '删除', keyAttr + ' aria-label=\"删除条目\" title=\"删除条目\"') +\n (options.stringList ? '</div>' : `<div class=\"md-field-error\" data-error-for=\"${id}-name\"></div></div>`);\n }\n if (options.stringList) return `<div class=\"md-text-list-add\">${textInput(id + '-value', '', 'placeholder=\"新增条目\" aria-label=\"新增文字条目\"')}` +\n button('add', '添加', `data-tree-value-input=\"${id}-value\"`) + `<div class=\"md-field-error\" data-error-for=\"${id}-name\"></div></div>`;\n const content = `<div class=\"md-collection-tools\">${array ? '' : textInput(id + '-name', '', `placeholder=\"${options.effects ? '新效果名称' : '新字段名称'}\" aria-label=\"新增名称\"`)}` +\n (options.effects ? '' : `<select id=\"${id}-type\" aria-label=\"新增内容类型\"><option value=\"text\">文字</option><option value=\"number\">数字</option><option value=\"boolean\">是/否</option><option value=\"object\">字段集合</option><option value=\"array\">列表</option></select>`) +\n button('add', options.effects ? '添加效果' : array ? '添加条目' : '添加字段') + `<div class=\"md-field-error\" data-error-for=\"${id}-name\"></div></div>`;\n return options.effects ? content : subgroup(array ? '添加列表条目' : '添加扩展字段', content, id + '-tools', false, options.state, path);\n}\nfunction renderStructuredTree(value, basePath, tab, idPrefix, options = {}, depth = 0) {\n if (depth > 7) return `<div class=\"md-note\">嵌套层级较深,请使用“原始 JSON 编辑”。</div>`;\n if (!value || typeof value !== 'object') {\n const type = editorRegistry.inferType(value);\n const attrs = options.auxRoot\n ? auxStructureAttrs(options.auxRoot, basePath, type, tab)\n : structureAttrs(basePath, type, tab);\n return structuredControl(`${idPrefix}-value`, '值', value, type, attrs);\n }\n const array = Array.isArray(value);\n const locked = !options.auxRoot && (basePath === '/事件/信号' || basePath.startsWith('/事件/信号/'));\n const fieldName = patch.parsePointer(basePath).slice(-1)[0];\n if (!array && !options.auxRoot && fieldName === '内部资产') return renderCollection(value, basePath, tab, idPrefix, 'inventory', options.state || {});\n const stringList = array && (options.stringList || fieldName === '标签' || value.length > 0 && value.every((item) => typeof item === 'string'));\n options = Object.assign({}, options, { scope: idPrefix, stringList, effects: !array && fieldName === '效果' });\n if (locked) return `<div class=\"md-readonly-list\">${esc(pretty(value))}(仅能在事件卡片追加信号)</div>`;\n let html = '';\n const entries = Array.isArray(value) ? value.map((item, index) => [String(index), item]) : Object.entries(value);\n for (const [key, child] of entries) {\n if (key.charAt(0) === '_') {\n html += `<div class=\"md-readonly-list\"><b>${esc(key)}</b>:只读,由角色卡维护</div>`;\n continue;\n }\n const pathText = basePath + '/' + patch.escapeSegment(key);\n const childId = `${idPrefix}-key-${encodeKey(key)}`;\n const type = child && typeof child === 'object' ? 'tree' : options.effects ? 'textarea' : editorRegistry.inferType(child);\n if (type === 'tree') {\n html += subgroup(array ? `第 ${Number(key) + 1} 项` : key,\n () => treeTools(basePath, tab, childId, options, key, child, array) + renderStructuredTree(child, pathText, tab, childId, options, depth + 1),\n `tree-${childId}`, false, options.state, pathText);\n } else {\n const tools = treeTools(basePath, tab, childId, options, key, child, array);\n const attrs = options.auxRoot\n ? auxStructureAttrs(options.auxRoot, pathText, type, tab)\n : structureAttrs(pathText, type, tab);\n html += stringList\n ? `<div class=\"md-text-list-row\">${textInput(childId, child, attrs + ' aria-label=\"文字条目\"')}${tools}<div class=\"md-field-error\" data-error-for=\"${childId}\"></div><div class=\"md-field-error\" data-error-for=\"${childId}-name\"></div></div>`\n : `<div class=\"${options.effects ? 'md-effect-row' : 'md-tree-field'}\">${tools}${structuredControl(childId, options.effects ? '效果说明' : array ? `第 ${Number(key) + 1} 项` : key, child, type, attrs)}</div>`;\n }\n }\n return (html || `<div class=\"${CLS.EMPTY}\">当前没有可编辑字段</div>`) + treeTools(basePath, tab, idPrefix + '-new', options, undefined, null, array);\n}\n\nfunction renderCollection(value, pathText, tab, editorId, kind, state = {}) {\n const definition = editorRegistry.getDefinition(kind);\n const items = value && typeof value === 'object' ? value : {};\n const names = Object.keys(items).filter((name) => !name.startsWith('_'));\n const selections = state.collectionSelections || (state.collectionSelections = {});\n const selected = names.includes(selections[pathText]) ? selections[pathText] : names[0];\n selections[pathText] = selected || '';\n const selectId = `${editorId}-select-${encodeKey(pathText)}`;\n const label = patch.parsePointer(pathText).slice(-1)[0] === '内部资产' ? '内部资产' : definition && definition.label || '条目';\n let html = `<div class=\"md-collection-workspace\" data-collection-editor=\"${editorId}\" data-collection-path=\"${esc(pathText)}\"><div class=\"md-collection-nav\">` +\n `<label for=\"${selectId}\">${esc(label)}列表 (${names.length})</label>` +\n `<input type=\"search\" id=\"${selectId}-filter\" data-option-filter=\"${selectId}\" placeholder=\"搜索条目名称\" aria-label=\"搜索条目名称\">` +\n `<select id=\"${selectId}\" data-act=\"select-collection\" data-collection-path=\"${esc(pathText)}\" ${names.length ? '' : 'disabled'}>` +\n names.map((name) => `<option value=\"${esc(name)}\" ${name === selected ? 'selected' : ''}>${esc(name)}</option>`).join('') + '</select>' +\n `<div class=\"md-collection-tools\"><input type=\"text\" id=\"${editorId}-new\" placeholder=\"新增条目名称\" aria-label=\"新增条目名称\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"add-collection-item\" data-editor-id=\"${editorId}\" data-collection-kind=\"${kind}\" data-collection-path=\"${esc(pathText)}\">+ 新增</button></div>`;\n const readonly = Object.keys(items).length - names.length;\n html += `${readonly ? `<small class=\"md-note\">另有 ${readonly} 个角色卡只读条目,原样保留。</small>` : ''}</div>`;\n for (const name of selected === undefined ? [] : [selected]) {\n const item = items[name];\n const itemPath = pathText + '/' + patch.escapeSegment(name);\n const itemId = `${editorId}-item-${encodeKey(name)}`;\n let fields = `<div class=\"md-item-name-row\"><input type=\"text\" id=\"${itemId}-name\" value=\"${esc(name)}\" aria-label=\"条目名称\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"rename-collection-item\" data-editor-id=\"${editorId}\" data-name-input=\"${itemId}-name\" data-old-name=\"${encodeKey(name)}\" data-collection-path=\"${esc(pathText)}\">重命名</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"delete-collection-item\" data-editor-id=\"${editorId}\" data-old-name=\"${encodeKey(name)}\" data-collection-path=\"${esc(pathText)}\">删除</button></div>`;\n if (definition && item && typeof item === 'object' && !Array.isArray(item)) {\n const covered = new Set();\n let basic = ''; let groups = '';\n for (const [fieldName, fieldType, choices] of definition.fields) {\n covered.add(fieldName);\n const child = item[fieldName];\n const childPath = itemPath + '/' + patch.escapeSegment(fieldName);\n const complexChild = child && typeof child === 'object' && !(fieldType === 'csv' && Array.isArray(child));\n if (fieldType === 'tree' || complexChild || fieldType === 'csv' && Array.isArray(child)) {\n groups += subgroup(fieldName, () => renderStructuredTree(child || {}, childPath, tab, `${itemId}-${encodeKey(fieldName)}`, { stringList: fieldType === 'csv', state }), `${itemId}-${encodeKey(fieldName)}`, true, state, childPath);\n } else {\n const label = fieldName === '描述' ? ({ inventory: '物品描述', equipment: '装备描述', skill: '技能描述', asset: '资产描述' }[kind] || '条目描述') : fieldName;\n const control = structuredControl(`${itemId}-${encodeKey(fieldName)}`, label, child, fieldType,\n structureAttrs(childPath, fieldType, tab), '', choices);\n if (fieldType === 'textarea') groups += `<div class=\"md-field-group\" data-field-group=\"${esc(fieldName)}\">${control}</div>`;\n else basic += control;\n }\n }\n fields += (basic ? `<div class=\"md-field-group md-basic-fields\"><h4>基本属性</h4><div class=\"${CLS.GRID}\">${basic}</div></div>` : '') + groups;\n const unknown = Object.fromEntries(Object.entries(item).filter(([key]) => !covered.has(key)));\n fields += subgroup('扩展字段', () => renderStructuredTree(unknown, itemPath, tab, `${itemId}-extra`, { state }), `${itemId}-extra`, Object.keys(unknown).length > 0, state, itemPath);\n } else {\n const type = editorRegistry.inferType(item);\n fields += type === 'tree'\n ? renderStructuredTree(item, itemPath, tab, `${itemId}-tree`, { state })\n : structuredControl(`${itemId}-value`, '内容', item, type, structureAttrs(itemPath, type, tab));\n }\n html += `<div class=\"md-collection-detail\" data-collection-detail=\"${esc(pathText)}\" data-selected-item=\"${esc(name)}\"><h3>${esc(name)}</h3>${fields}</div>`;\n }\n return html + `${names.length ? '' : `<div class=\"${CLS.EMPTY}\">当前没有条目</div>`}</div>`;\n}\n\nfunction renderAscension(value, pathText, tab, editorId, state) {\n const asc = value || {};\n let html = structuredControl(`${editorId}-enabled`, '是否开启', asc['是否开启'], 'boolean', structureAttrs(`${pathText}/是否开启`, 'boolean', tab));\n for (const key of ['要素', '权能', '法则']) {\n const collectionPath = `${pathText}/${patch.escapeSegment(key)}`;\n html += subgroup(key, () => renderCollection(asc[key] || {}, collectionPath, tab, `${editorId}-${encodeKey(key)}`, 'ascensionStep', state), `${editorId}-${encodeKey(key)}`, false, state, collectionPath);\n }\n html += structuredControl(`${editorId}-god`, '神位', asc['神位'], 'text', structureAttrs(`${pathText}/神位`, 'text', tab));\n html += subgroup('神国', () => renderStructuredTree(asc['神国'] || {}, `${pathText}/神国`, tab, `${editorId}-realm`, { state }), `${editorId}-realm`, false, state, `${pathText}/神国`);\n return html;\n}\nfunction dualEditor(params) {\n const state = params.state || {};\n const modes = state.editorModes || (state.editorModes = {});\n const mode = modes[params.id] === 'raw' ? 'raw' : 'structured';\n const switcher = `<div class=\"md-editor-mode\" role=\"group\" aria-label=\"${esc(params.label)}编辑方式\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${mode === 'structured' ? CLS.BTN_PRIMARY : ''}\" data-act=\"editor-mode\" data-editor-id=\"${params.id}\" data-editor-mode=\"structured\">结构化编辑</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${mode === 'raw' ? CLS.BTN_PRIMARY : ''}\" data-act=\"editor-mode\" data-editor-id=\"${params.id}\" data-editor-mode=\"raw\">原始 JSON 编辑</button></div>`;\n const body = deferredView(state, `editor-${params.id}`, () => {\n if (mode === 'raw') return jsonEditor(`${params.id}-raw`, params.label + ' · 原始 JSON', params.value || {}, params.rawAttrs, true, state, true);\n if (params.kind === 'ascension') return renderAscension(params.value || {}, params.path, params.tab, params.id, state);\n if (editorRegistry.getDefinition(params.kind)) return renderCollection(params.value || {}, params.path, params.tab, params.id, params.kind, state);\n return renderStructuredTree(params.value || {}, params.path, params.tab, params.id, Object.assign({ state }, params.treeOptions));\n });\n return `<div data-view-path=\"${esc(params.path)}\">` + section(params.label, switcher + body, false, Object.keys(params.value || {}).length, `editor-${params.id}`, params.icon || 'edit') + '</div>';\n}\n\nfunction resourceSection(owner, prefix, basePath, tab) {\n let html = '';\n for (const [key, short] of RESOURCE_KEYS) {\n const value = owner[key] || {};\n const upper = value['上限'] || {};\n html += `<div class=\"md-resource\">` +\n fieldRow(`${prefix}-${short}-cur`, `${key}·当前`, numberInput(`${prefix}-${short}-cur`, value['当前'], bindAttrs(`${basePath}/${key}/当前`, 'number', tab))) +\n fieldRow(`${prefix}-${short}-base`, `${key}·基础`, numberInput(`${prefix}-${short}-base`, upper['_基础'], 'readonly aria-readonly=\"true\"'), '角色卡公式字段,只读') +\n fieldRow(`${prefix}-${short}-extra`, `${key}·额外`, numberInput(`${prefix}-${short}-extra`, upper['额外'], bindAttrs(`${basePath}/${key}/上限/额外`, 'number', tab))) +\n `</div>`;\n }\n return section('资源', `<div class=\"${CLS.GRID}\">${html}</div>`, false);\n}\n\nfunction renderProtagonist(statData, state) {\n const hero = statData['主角'] || {};\n const attrs = hero['属性'] || {};\n const help = helpBox(esc(playerHelp.tabs.protagonist));\n let base = `<div class=\"${CLS.GRID}\">`;\n base += fieldRow('md-h-race', '种族', textInput('md-h-race', hero['种族'], bindAttrs('/主角/种族', 'text', 'protagonist')));\n base += fieldRow('md-h-life', '生命层级', textInput('md-h-life', hero['生命层级'], bindAttrs('/主角/生命层级', 'text', 'protagonist')));\n base += fieldRow('md-h-level', '等级', numberInput('md-h-level', hero['等级'], bindAttrs('/主角/等级', 'integer', 'protagonist', 'min=\"1\" max=\"25\"')));\n base += fieldRow('md-h-exp', '累计经验', numberInput('md-h-exp', hero['累计经验值'], bindAttrs('/主角/累计经验值', 'number', 'protagonist', 'min=\"0\"')));\n base += fieldRow('md-h-expneed', '升级所需', textInput('md-h-expneed', hero['升级所需经验'], bindAttrs('/主角/升级所需经验', 'exp', 'protagonist')), '数字或 25 级的 MAX');\n base += fieldRow('md-h-ap', '属性点', numberInput('md-h-ap', hero['属性点'], bindAttrs('/主角/属性点', 'integer', 'protagonist', 'min=\"0\"')));\n base += fieldRow('md-h-money', '金钱', numberInput('md-h-money', hero['金钱'], bindAttrs('/主角/金钱', 'integer', 'protagonist')));\n base += fieldRow('md-h-fp', '命运点数', numberInput('md-h-fp', statData['命运点数'], bindAttrs('/命运点数', 'integer', 'protagonist', 'min=\"0\"')));\n base += fieldRow('md-h-ids', '身份(逗号)', textInput('md-h-ids', (hero['身份'] || []).join(','), bindAttrs('/主角/身份', 'csv', 'protagonist')));\n base += fieldRow('md-h-occ', '职业(逗号)', textInput('md-h-occ', (hero['职业'] || []).join(','), bindAttrs('/主角/职业', 'csv', 'protagonist')));\n base += '</div>';\n\n let attrHtml = '<div class=\"' + CLS.GRID + '\">';\n for (const [key, short] of ATTR_KEYS) {\n attrHtml += fieldRow(`md-h-${short}`, key, numberInput(`md-h-${short}`, attrs[key], bindAttrs(`/主角/属性/${key}`, 'number', 'protagonist')));\n }\n attrHtml += '</div>';\n\n let complex = '';\n for (const key of COMPLEX_KEYS) {\n const pathText = pointer(['主角', key]);\n complex += dualEditor({\n id: `md-h-editor-${encodeKey(key)}`,\n label: key,\n value: hero[key] || {},\n path: pathText,\n tab: 'protagonist',\n state,\n kind: key === '登神长阶' ? 'ascension' : editorRegistry.kindForKey(key),\n rawAttrs: `data-json-bind=\"${esc(pathText)}\" data-dirty-tab=\"protagonist\"`,\n });\n }\n return help + section('基础与成长', base, true) + section('五维属性', attrHtml, true) +\n resourceSection(hero, 'md-h', '/主角', 'protagonist') + complex;\n}\n\nfunction newNpcTemplate() {\n // _基础 / _隐藏 由角色卡 Zod 的 prefault 生成;UI 不应在新增父对象时夹带只读字段。\n const resource = () => ({ '当前': 0, '上限': { '额外': 0 } });\n return {\n '在场': true,\n '标签': [],\n '种族': '人类',\n '身份': [],\n '职业': [],\n '生命层级': '第一层级/普通',\n '性格': '',\n '喜爱': '',\n '外貌': '',\n '着装': '',\n '等级': 1,\n '属性': { '力量': 5, '敏捷': 5, '体质': 5, '智力': 5, '精神': 5 },\n '生命值': resource(),\n '法力值': resource(),\n '体力值': resource(),\n '状态效果': {},\n '背包': {},\n '装备': {},\n '技能': {},\n '资产': {},\n '登神长阶': { '是否开启': false, '要素': {}, '权能': {}, '法则': {}, '神位': '', '神国': { '名称': '', '描述': '' } },\n '命定契约': false,\n '好感度': 0,\n '心里话': '',\n '背景故事': '',\n };\n}\n\nfunction renderNpc(statData, state) {\n const help = helpBox(esc(playerHelp.tabs.npc));\n const relations = statData['关系列表'] || {};\n const names = Object.keys(relations);\n if (!names.length) {\n return help + `<div class=\"${CLS.EMPTY}\">关系列表为空。可在下方新增 NPC。</div>` + renderNpcActions('');\n }\n if (!relations[state.selectedNpc]) state.selectedNpc = names[0];\n const name = state.selectedNpc;\n const npc = relations[name] || {};\n const basePath = pointer(['关系列表', name]);\n const options = names.map((item) => `<option value=\"${esc(item)}\" ${item === name ? 'selected' : ''}>${esc(item)}</option>`).join('');\n let html = fieldRow('md-npc-filter', '筛选 NPC', `<input type=\"search\" id=\"md-npc-filter\" data-option-filter=\"md-npc-select\" placeholder=\"输入姓名关键词\">`);\n html += fieldRow('md-npc-select', `选择 NPC(${names.length})`, `<select id=\"md-npc-select\" data-act=\"select-npc\">${options}</select>`);\n html += renderNpcActions(name);\n\n let base = `<div class=\"${CLS.GRID}\">`;\n base += fieldRow('md-n-race', '种族', textInput('md-n-race', npc['种族'], bindAttrs(`${basePath}/种族`, 'text', 'npc')));\n base += fieldRow('md-n-life', '生命层级', textInput('md-n-life', npc['生命层级'], bindAttrs(`${basePath}/生命层级`, 'text', 'npc')));\n base += fieldRow('md-n-level', '等级', numberInput('md-n-level', npc['等级'], bindAttrs(`${basePath}/等级`, 'integer', 'npc', 'min=\"1\" max=\"25\"')));\n base += fieldRow('md-n-aff', '好感度', numberInput('md-n-aff', npc['好感度'], bindAttrs(`${basePath}/好感度`, 'number', 'npc', 'min=\"-100\" max=\"100\"')));\n base += fieldRow('md-n-present', '在场', `<select id=\"md-n-present\" ${bindAttrs(`${basePath}/在场`, 'boolean', 'npc')}><option value=\"true\" ${npc['在场'] ? 'selected' : ''}>是</option><option value=\"false\" ${npc['在场'] ? '' : 'selected'}>否</option></select>`);\n base += fieldRow('md-n-contract', '命定契约', `<select id=\"md-n-contract\" ${bindAttrs(`${basePath}/命定契约`, 'boolean', 'npc')}><option value=\"true\" ${npc['命定契约'] ? 'selected' : ''}>是</option><option value=\"false\" ${npc['命定契约'] ? '' : 'selected'}>否</option></select>`);\n base += fieldRow('md-n-tags', '标签(逗号)', textInput('md-n-tags', (npc['标签'] || []).join(','), bindAttrs(`${basePath}/标签`, 'csv', 'npc')));\n base += fieldRow('md-n-ids', '身份(逗号)', textInput('md-n-ids', (npc['身份'] || []).join(','), bindAttrs(`${basePath}/身份`, 'csv', 'npc')));\n base += fieldRow('md-n-occ', '职业(逗号)', textInput('md-n-occ', (npc['职业'] || []).join(','), bindAttrs(`${basePath}/职业`, 'csv', 'npc')));\n base += '</div>';\n\n const attrs = npc['属性'] || {};\n let attrHtml = '<div class=\"' + CLS.GRID + '\">';\n for (const [key, short] of ATTR_KEYS) {\n attrHtml += fieldRow(`md-n-${short}`, key, numberInput(`md-n-${short}`, attrs[key], bindAttrs(`${basePath}/属性/${key}`, 'number', 'npc')));\n }\n attrHtml += '</div>';\n\n let roleplay = '';\n for (const key of ['性格', '喜爱', '外貌', '着装', '心里话', '背景故事']) {\n const id = `md-n-text-${encodeKey(key)}`;\n roleplay += fieldRow(id, key, `<textarea id=\"${id}\" ${bindAttrs(`${basePath}/${key}`, 'text', 'npc')}>${esc(npc[key] || '')}</textarea>`);\n }\n\n let complex = '';\n for (const key of COMPLEX_KEYS) {\n const pathText = pointer(['关系列表', name, key]);\n complex += dualEditor({\n id: `md-n-editor-${encodeKey(key)}`,\n label: key,\n value: npc[key] || {},\n path: pathText,\n tab: 'npc',\n state,\n kind: key === '登神长阶' ? 'ascension' : editorRegistry.kindForKey(key),\n rawAttrs: `data-json-bind=\"${esc(pathText)}\" data-dirty-tab=\"npc\"`,\n });\n }\n return help + html + section('基础资料', base, true) + section('五维属性', attrHtml, true) +\n resourceSection(npc, 'md-n', basePath, 'npc') + section('扮演资料', roleplay, false) + complex;\n}\n\nfunction renderNpcActions(selected) {\n return `<div class=\"md-inline-actions\"><input type=\"text\" id=\"md-npc-new\" placeholder=\"新 NPC 名称\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"add-npc\">新增 NPC</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"delete-npc\" ${selected ? '' : 'disabled'}>删除当前 NPC</button></div>`;\n}\n\nfunction parseWorldTime(value) {\n const match = String(value || '').match(/^(.*?)纪元(\\d+)年(\\d+)月(\\d+)日\\s*(星期[一二三四五六日天]?)\\s*(\\d+):(\\d+)$/);\n return {\n era: match ? match[1] : '复兴',\n year: match ? match[2] : '488',\n month: match ? match[3] : '1',\n day: match ? match[4] : '1',\n weekday: match ? match[5] : '星期一',\n hour: match ? match[6] : '00',\n minute: match ? match[7] : '00',\n };\n}\nfunction formatJsonError(source, error) {\n const message = error && error.message ? error.message : String(error || '未知 JSON 错误');\n const match = message.match(/(?:position|at position)\\s+(\\d+)/i);\n if (!match) return message;\n const position = Math.min(String(source || '').length, Math.max(0, Number(match[1]) || 0));\n const before = String(source || '').slice(0, position);\n const line = before.split('\\n').length;\n const lastBreak = before.lastIndexOf('\\n');\n const column = position - lastBreak;\n return `第 ${line} 行、第 ${column} 列:${message}`;\n}\n\nfunction renderWorld(statData, state, mod) {\n const world = statData['世界'] || {};\n const event = statData['事件'] || {};\n const news = statData['新闻'] || {};\n const time = parseWorldTime(world['时间']);\n\n const help = helpBox(esc(playerHelp.tabs.world));\n\n let base = `<div class=\"${CLS.GRID}\" data-world-time-group>`;\n base += fieldRow('md-w-era', '纪元名', textInput('md-w-era', time.era, 'data-world-time-part=\"era\"'));\n base += fieldRow('md-w-year', '年', numberInput('md-w-year', time.year, 'data-world-time-part=\"year\" min=\"0\"'));\n base += fieldRow('md-w-month', '月', numberInput('md-w-month', time.month, 'data-world-time-part=\"month\" min=\"1\" max=\"12\"'));\n base += fieldRow('md-w-day', '日', numberInput('md-w-day', time.day, 'data-world-time-part=\"day\" min=\"1\" max=\"31\"'));\n base += fieldRow('md-w-weekday', '星期', textInput('md-w-weekday', time.weekday, 'data-world-time-part=\"weekday\"'));\n base += fieldRow('md-w-hour', '时', numberInput('md-w-hour', time.hour, 'data-world-time-part=\"hour\" min=\"0\" max=\"23\"'));\n base += fieldRow('md-w-minute', '分', numberInput('md-w-minute', time.minute, 'data-world-time-part=\"minute\" min=\"0\" max=\"59\"'));\n base += fieldRow('md-w-location', '地点', textInput('md-w-location', world['地点'], bindAttrs('/世界/地点', 'text', 'world')));\n base += '</div>';\n\n let eventHtml = `<div class=\"${CLS.GRID}\">`;\n eventHtml += fieldRow('md-e-open', '开启', `<select id=\"md-e-open\" ${bindAttrs('/事件/开启', 'boolean', 'world')}><option value=\"true\" ${event['开启'] ? 'selected' : ''}>是</option><option value=\"false\" ${event['开启'] ? '' : 'selected'}>否</option></select>`);\n eventHtml += fieldRow('md-e-end', '结束', `<select id=\"md-e-end\" ${bindAttrs('/事件/结束', 'boolean', 'world')}><option value=\"true\" ${event['结束'] ? 'selected' : ''}>是</option><option value=\"false\" ${event['结束'] ? '' : 'selected'}>否</option></select>`);\n eventHtml += fieldRow('md-e-title', '标题', textInput('md-e-title', event['标题'], bindAttrs('/事件/标题', 'text', 'world')));\n eventHtml += fieldRow('md-e-phase', '阶段', textInput('md-e-phase', event['阶段'], bindAttrs('/事件/阶段', 'text', 'world')));\n eventHtml += '</div>';\n eventHtml += fieldRow('md-e-done', '已完成事件', `<textarea id=\"md-e-done\" ${bindAttrs('/事件/已完成事件', 'lines', 'world')}>${esc((event['已完成事件'] || []).join('\\n'))}</textarea>`);\n eventHtml += `<div class=\"md-readonly-list\"><b>既有信号(只读)</b>:${esc((event['信号'] || []).join(',') || '无')}</div>` +\n `<div class=\"md-inline-actions\"><input type=\"text\" id=\"md-event-signal\" placeholder=\"追加新信号,多个用逗号分隔\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"append-signal\">追加信号</button></div>`;\n\n let newsHtml = '';\n for (const key of ['阿斯塔利亚快讯', '酒馆留言板', '午后茶会']) {\n const pathText = pointer(['新闻', key]);\n newsHtml += dualEditor({ id: `md-news-${encodeKey(key)}`, label: key, value: news[key] || {}, path: pathText,\n tab: 'world', state, kind: 'news', rawAttrs: `data-json-bind=\"${esc(pathText)}\" data-dirty-tab=\"world\"`, icon: 'globe' });\n }\n\n // 扫描 DLC / 扩展变量\n const standardKeys = new Set(['主角', '关系列表', '世界', '事件', '任务列表', '新闻', '命运点数']);\n const dlcKeys = Object.keys(statData || {}).filter((k) => !standardKeys.has(k) && k.charAt(0) !== '_');\n let dlcHtml = '';\n for (const key of dlcKeys) {\n const val = statData[key];\n const pathText = '/' + patch.escapeSegment(key);\n dlcHtml += dualEditor({\n id: `md-dlc-${encodeKey(key)}`,\n label: key,\n value: val || {},\n path: pathText,\n tab: 'world',\n state,\n kind: 'tree',\n rawAttrs: `data-json-bind=\"${esc(pathText)}\" data-dirty-tab=\"world\"`,\n icon: 'spark',\n });\n }\n\n const snap = mod && typeof mod.getSnapshot === 'function' ? mod.getSnapshot() : null;\n const msgVars = (snap && snap.messageVariables) || {};\n const standardMsgVars = new Set(['stat_data', 'date', 'calendar_float_store', 'templates']);\n const extraMsgKeys = Object.keys(msgVars).filter((k) => !standardMsgVars.has(k) && k.charAt(0) !== '_');\n for (const key of extraMsgKeys) {\n const val = msgVars[key];\n dlcHtml += dualEditor({\n id: `md-msg-dlc-${encodeKey(key)}`,\n label: key + ' (扩展变量)',\n value: val || {},\n path: '',\n tab: 'world',\n state,\n kind: 'tree',\n treeOptions: { auxRoot: key },\n rawAttrs: `data-json-aux-subset=\"${esc(key)}\" data-dirty-tab=\"world\"`,\n icon: 'spark',\n });\n }\n\n const totalDlc = dlcKeys.length + extraMsgKeys.length;\n\n const eventExtra = Object.fromEntries(Object.entries(event).filter(([key]) => !['开启', '结束', '标题', '阶段', '已完成事件', '信号'].includes(key)));\n return help + section('时间与地点', base, true) + section('事件', eventHtml, true) +\n dualEditor({ id: 'md-world-tasks', label: '任务列表', value: statData['任务列表'] || {}, path: '/任务列表', tab: 'world', state,\n kind: 'task', rawAttrs: 'data-json-bind=\"/任务列表\" data-dirty-tab=\"world\"', icon: 'history' }) +\n section('新闻三栏', newsHtml, false) +\n (Object.keys(eventExtra).length ? dualEditor({ id: 'md-event-extra', label: '事件扩展字段', value: eventExtra, path: '/事件', tab: 'world', state,\n kind: 'tree', rawAttrs: 'data-json-subset=\"/事件\" data-json-subset-keys=\"开启,结束,标题,阶段,已完成事件,信号\" data-dirty-tab=\"world\"', icon: 'spark' }) : '') +\n (totalDlc ? section('活跃 DLC / 扩展变量', dlcHtml, true, totalDlc, 'world-dlc-section', 'spark') : '');\n}\n\nfunction renderCalendar(aux, state) {\n const help = helpBox(esc(playerHelp.tabs.calendar));\n const date = aux.date || {};\n const calendar = aux.calendar || {};\n const npcs = date.npcs || {};\n let npcHtml = '';\n for (const [name, item] of Object.entries(npcs)) {\n const base = pointer(['npcs', name]);\n npcHtml += `<div class=\"md-calendar-row\" data-filter-text=\"${esc(name.toLocaleLowerCase())}\"><b>${esc(name)}</b>` +\n numberInput(`md-cal-${encodeKey(name)}-lv`, item.level, auxAttrs('date', `${base}/level`, 'integer', 'calendar', 'min=\"1\" max=\"25\" aria-label=\"等级\"')) +\n numberInput(`md-cal-${encodeKey(name)}-exp`, item.exp, auxAttrs('date', `${base}/exp`, 'number', 'calendar', 'min=\"0\" aria-label=\"经验\"')) +\n textInput(`md-cal-${encodeKey(name)}-req`, item.required_exp, auxAttrs('date', `${base}/required_exp`, 'exp', 'calendar', 'aria-label=\"升级所需\"')) + '</div>';\n }\n if (!npcHtml) npcHtml = `<div class=\"${CLS.EMPTY}\">当前还没有 NPC 成长记录</div>`;\n const known = calendar.runtime && Array.isArray(calendar.runtime.known_tags) ? calendar.runtime.known_tags : [];\n let html = fieldRow('md-calendar-filter', '筛选 NPC', `<input type=\"search\" id=\"md-calendar-filter\" data-row-filter=\".md-calendar-row\" placeholder=\"输入姓名关键词\">`);\n html += section('NPC 等级 / 经验 / 升级所需', npcHtml, true, Object.keys(npcs).length);\n const log = date.log || {};\n let logFields = `<div class=\"${CLS.GRID}\">`;\n for (const [key, label] of [['deathCount', '死亡次数'], ['totalFPGained', '累计获得命运点数'], ['bankruptcyCount', '破产次数'], ['maxCurrencyDebt', '最大负债']]) {\n logFields += fieldRow(`md-date-log-${key}`, label, numberInput(`md-date-log-${key}`, log[key], auxAttrs('date', `/log/${key}`, 'number', 'calendar')));\n }\n logFields += '</div>';\n html += section('游戏统计', logFields, false, '', 'calendar-date-log', 'history');\n let configFields = '';\n for (const [key, label] of [['npcLevelUpWithPlayer', 'NPC 随玩家升级'], ['requiresContractForExp', '契约才能获得经验'], ['ascensionLawReady', '登神法则就绪']]) {\n configFields += fieldRow(`md-date-${key}`, label, `<select id=\"md-date-${key}\" ${auxAttrs('date', `/${key}`, 'boolean', 'calendar')}><option value=\"true\" ${date[key] ? 'selected' : ''}>是</option><option value=\"false\" ${date[key] ? '' : 'selected'}>否</option></select>`);\n }\n html += section('NPC 成长规则', configFields, false, '', 'calendar-date-config', 'gear');\n html += fieldRow('md-cal-known', '日历标签', textInput('md-cal-known', known.join(','), auxAttrs('calendar', '/runtime/known_tags', 'csv', 'calendar')), playerHelp.cards['日历标签']);\n const dateExtra = Object.fromEntries(Object.entries(date).filter(([key]) => !['npcs', 'log', 'npcLevelUpWithPlayer', 'requiresContractForExp', 'ascensionLawReady'].includes(key)));\n if (Object.keys(dateExtra).length) {\n html += dualEditor({ id: 'md-date-extra', label: '成长扩展记录', value: dateExtra, path: '', tab: 'calendar', state, kind: 'tree',\n treeOptions: { auxRoot: 'date' }, rawAttrs: 'data-json-aux-subset=\"date\" data-json-subset-keys=\"npcs,log\" data-dirty-tab=\"calendar\"', icon: 'variables' });\n }\n const calendarExtra = util.deepClone(calendar);\n if (calendarExtra.runtime) delete calendarExtra.runtime.known_tags;\n html += dualEditor({ id: 'md-calendar-extra', label: '日历扩展记录', value: calendarExtra, path: '', tab: 'calendar', state, kind: 'tree',\n treeOptions: { auxRoot: 'calendar' }, rawAttrs: 'data-json-aux-subset=\"calendar\" data-json-subset-path=\"/runtime/known_tags\" data-dirty-tab=\"calendar\"', icon: 'calendar' });\n return help + html;\n}\n\nfunction renderStart(mod) {\n const info = mod && typeof mod.readReincarnation === 'function' ? mod.readReincarnation() : { found: false, value: null };\n const help = helpBox(esc(playerHelp.tabs.start));\n return help + section('自定义开局',\n `<div class=\"md-note\">状态:<b>${info.found ? '已找到自定义开局页面' : '请先打开角色卡的自定义开局页面'}</b>。</div>` +\n fieldRow('md-reincarnation', '转生点', numberInput('md-reincarnation', info.value == null ? 1000 : info.value, `min=\"0\" ${info.found ? '' : 'disabled'}`)) +\n `<div class=\"md-inline-actions\"><button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_PRIMARY}\" data-act=\"set-reincarnation\" ${info.found ? '' : 'disabled'}>写入</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"set-reincarnation-1000\" ${info.found ? '' : 'disabled'}>重置 1000</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"set-reincarnation-dev\" ${info.found ? '' : 'disabled'}>设为 666666</button></div>`,\n true\n );\n}\n\nfunction renderTemplates(aux, state) {\n const help = helpBox(esc(playerHelp.tabs.templates));\n const variables = aux.chatVariables || {};\n const names = Object.keys(variables);\n const lazy = state.deferredViews instanceof Map;\n const selected = names.includes(state.selectedTemplate) ? state.selectedTemplate : names[0];\n state.selectedTemplate = selected || '';\n // 摘要不序列化对象;完整字符串和原始 JSON 只在实际查看时生成。\n const summary = (value) => Array.isArray(value) ? `列表 · ${value.length} 项` : value === null ? '空值'\n : typeof value === 'object' ? `字段集合 · ${Object.keys(value).length} 项`\n : typeof value === 'string' ? `文字 · ${value.slice(0, 48).replace(/\\s+/g, ' ')}${value.length > 48 ? '…' : ''}`\n : `${typeof value === 'boolean' ? '是/否' : '数字'} · ${value}`;\n const renderValue = (key, value) => {\n const encoded = encodeKey(key);\n const valueType = typeof value;\n let control;\n let dataType = 'string';\n if (valueType === 'boolean') {\n dataType = 'boolean';\n control = `<select id=\"md-tpl-${encoded}\" data-chat-key=\"${encoded}\" data-chat-type=\"boolean\"><option value=\"true\" ${value ? 'selected' : ''}>是</option><option value=\"false\" ${value ? '' : 'selected'}>否</option></select>`;\n } else if (valueType === 'number') {\n dataType = 'number';\n control = numberInput(`md-tpl-${encoded}`, value, `data-chat-key=\"${encoded}\" data-chat-type=\"number\"`);\n } else if (valueType === 'string') {\n control = `<textarea id=\"md-tpl-${encoded}\" data-chat-key=\"${encoded}\" data-chat-type=\"string\">${esc(value)}</textarea>`;\n } else {\n return `<div class=\"md-template-complex\"><div class=\"md-item-name-row\"><b>${esc(key)}</b>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"delete-chat-var\" data-key=\"${encoded}\" aria-label=\"删除 ${esc(key)}\">删除</button></div>` +\n dualEditor({ id: `md-tpl-editor-${encoded}`, label: key, value, path: '/' + patch.escapeSegment(key), tab: 'templates', state,\n kind: 'tree', treeOptions: { auxRoot: 'chatVariables' },\n rawAttrs: `data-json-aux-root=\"chatVariables\" data-json-aux-path=\"/${patch.escapeSegment(key)}\" data-dirty-tab=\"templates\"`, icon: 'variables' }) + '</div>';\n }\n return `<div class=\"md-template-row\"><label for=\"md-tpl-${encoded}\">${esc(key)}</label>` +\n `<div class=\"md-template-control\" data-value-type=\"${dataType}\">${control}</div>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"delete-chat-var\" data-key=\"${encoded}\" aria-label=\"删除 ${esc(key)}\">删除</button>` +\n `<div class=\"md-field-error\" data-error-for=\"md-tpl-${encoded}\" aria-live=\"polite\"></div></div>`;\n };\n let rows = names.map((key) => `<div data-template-pane=\"${esc(encodeKey(key))}\" ${lazy && key !== selected ? 'hidden' : ''}>` +\n deferredView(state, `template-${encodeKey(key)}`, () => renderValue(key, variables[key])) + '</div>').join('');\n if (!rows) rows = `<div class=\"${CLS.EMPTY}\">当前聊天没有模板变量</div>`;\n return help + `<div class=\"md-note\">全部修改先暂存;对象/数组必须保持合法 JSON,字符串按原样保存。</div>` +\n `<div class=\"md-inline-actions\"><input type=\"text\" id=\"md-chat-new-key\" placeholder=\"新变量名\"><button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"add-chat-var\">新增</button></div>` +\n `<div class=\"md-collection-nav md-template-nav\"><label for=\"md-template-select\">模板变量 (${names.length})</label>` +\n `<input type=\"search\" id=\"md-template-filter\" data-option-filter=\"md-template-select\" placeholder=\"搜索变量名称\" aria-label=\"搜索模板变量\">` +\n `<select id=\"md-template-select\" data-act=\"select-template\" ${names.length ? '' : 'disabled'}>` +\n names.map((key) => `<option value=\"${esc(key)}\" ${key === selected ? 'selected' : ''}>${esc(key)} · ${esc(summary(variables[key]))}</option>`).join('') + '</select></div>' + rows;\n}\n\nfunction renderOperations(mod, state) {\n let items = [];\n try {\n items = mod && typeof mod.preview === 'function'\n ? mod.preview()\n : (mod && typeof mod.dirtyInfo === 'function' ? (mod.dirtyInfo().items || []) : []);\n } catch (e) {\n return `<div class=\"md-warning\">获取待提交变更失败:${esc(e.message || String(e))}</div>`;\n }\n\n const mode = mod && typeof mod.getApplyMode === 'function' ? mod.getApplyMode() : 'direct';\n const help = helpBox(esc(playerHelp.tabs.operations));\n\n const toolbar = `<div class=\"md-operations-toolbar\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"generate-patch-instruction\">${icon('edit')} 生成正文差异指令</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"reprocess-mvu\">${icon('spark')} 重新执行正文指令</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"discard\" ${items.length ? '' : 'disabled'}>放弃全部修改</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_PRIMARY}\" data-act=\"apply\" ${items.length ? '' : 'disabled'}>${icon('save')} 应用全部修改</button>` +\n `</div>`;\n\n if (!items.length) {\n return help + toolbar + `<div class=\"${CLS.EMPTY}\">当前没有未提交的变量变更。在主角、NPC、世界等页面修改后会自动暂存到这里。</div>`;\n }\n\n const opNames = { replace: '修改', delta: '增减', insert: '新增', remove: '删除', move: '移动' };\n\n let listHtml = `<div class=\"md-operations-list\">`;\n items.forEach((item, index) => {\n const info = classifyGroup(item.path);\n const cnName = bodyMerge.pathCN(item.path);\n const isAux = item.path.startsWith('/date') || item.path.startsWith('/calendar') || item.path.startsWith('/chat');\n const destBadge = mode === 'body'\n ? (isAux ? `<span class=\"md-badge md-badge-direct\">直接提交到辅助变量</span>` : `<span class=\"md-badge md-badge-body\">写入正文声明</span>`)\n : `<span class=\"md-badge md-badge-direct\">直接写入</span>`;\n\n listHtml += `<div class=\"md-operation-card\" data-op-index=\"${index}\">` +\n `<div class=\"md-operation-header\">` +\n `<div class=\"md-op-title\">` +\n `<span class=\"md-badge ${info.badgeCls}\">${info.name}</span> ` +\n `<b>${esc(cnName)}</b> ` +\n `<span class=\"md-badge md-badge-${item.op}\">${esc(opNames[item.op] || item.op)}</span> ` +\n `${destBadge}` +\n `</div>` +\n `<div class=\"md-op-actions\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"jump-field\" data-path=\"${esc(item.path)}\" data-tab=\"${info.tab}\">跳转</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"discard-single\" data-path=\"${esc(item.path)}\" data-tab=\"${info.tab}\">撤销</button>` +\n `</div>` +\n `</div>` +\n `<div class=\"md-op-path\"><code>${esc(item.path)}</code></div>` +\n `<div class=\"md-op-diff-grid\">` +\n `<div class=\"md-diff-box\"><span class=\"md-diff-label\">原始值</span><pre>${esc(valueSummary(item.oldValue))}</pre></div>` +\n `<div class=\"md-diff-box\"><span class=\"md-diff-label\">暂存值</span><pre>${esc(valueSummary(item.newValue))}</pre></div>` +\n `</div>` +\n `</div>`;\n });\n listHtml += `</div>`;\n\n return help + toolbar + section('操作中心与变更暂存', listHtml, true, items.length, 'operations-list', 'wand');\n}\n\nfunction renderInterpretation(mod, state) {\n const snap = mod && typeof mod.getSnapshot === 'function' ? mod.getSnapshot() : null;\n if (!snap) return `<div class=\"${CLS.EMPTY}\">尚未加载目标快照。请进入命定之诗聊天后刷新。</div>`;\n\n const help = helpBox(esc(playerHelp.tabs.interpretation));\n\n let analysis = '';\n let existing = [];\n let parseError = '';\n try {\n const parsed = bodyMerge.parseUpdateBlocksWithAnalysis(snap.messageText || '');\n analysis = parsed.analysis;\n existing = parsed.ops;\n } catch (e) {\n parseError = e && e.message ? e.message : String(e);\n }\n\n let pending = [];\n let pendingError = '';\n try {\n if (mod && typeof mod.buildChangeSet === 'function') {\n const cs = mod.buildChangeSet();\n pending = (cs && cs.mvuOps) || [];\n }\n } catch (e) {\n pendingError = e && e.message ? e.message : String(e);\n }\n\n const target = snap.target || {};\n const targetText = `聊天 ${target.chatId || '未知'} · 回复 ${target.messageId} · 候选版本 ${Number(target.swipeId || 0) + 1}`;\n\n const toolbar = `<div class=\"md-interpretation-toolbar\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"reprocess-mvu\">${icon('spark')} 重新执行正文指令</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"refresh\">${icon('spark')} 重新加载快照</button>` +\n `</div>`;\n\n let analysisHtml = '';\n if (analysis) {\n analysisHtml = `<div class=\"md-interpret-analysis\">` +\n `<div class=\"md-interpret-analysis-title\">${icon('spark')} AI 伴随叙述 / 变量变更说明</div>` +\n `<div class=\"md-interpret-analysis-content\">${esc(analysis)}</div>` +\n `</div>`;\n }\n\n const opNames = { replace: '修改', delta: '增减', insert: '新增', remove: '删除', move: '移动' };\n\n function renderOpVal(op) {\n if (op.op === 'remove') return '<span style=\"color:var(--md-danger,#ff7597);font-weight:600\">(已删除)</span>';\n const valStr = valueSummary(op.value);\n if (op.op === 'insert') return `<span style=\"color:var(--md-ok,#52c480);font-weight:600\">+ 新增:</span>${esc(valStr)}`;\n if (op.op === 'delta') return `<span style=\"color:#ffbe4d;font-weight:600\">Δ 增减:</span>${esc(valStr)}`;\n return `<span style=\"color:#79a8ff;font-weight:600\">→ 改为:</span>${esc(valStr)}`;\n }\n\n let existingBody = '';\n if (parseError) {\n existingBody = `<div class=\"md-warning\">正文补丁无法安全解析:${esc(parseError)}</div>`;\n } else if (!existing.length) {\n existingBody = `<div class=\"${CLS.EMPTY}\">锁定消息正文中没有 <update_variable> 或 <JSONPatch> 变量声明。</div>`;\n } else {\n existingBody = `<div class=\"md-interpret-ops-list\">` +\n existing.map((op, idx) => {\n const cn = bodyMerge.pathCN(op.path);\n const opName = opNames[op.op] || op.op;\n return `<div class=\"md-interpret-op\" data-op=\"${op.op}\">` +\n `<div class=\"md-interpret-op-header\">` +\n `<div><b>#${idx + 1}</b> <span class=\"md-badge md-badge-${op.op}\">${esc(opName)}</span> <span>${esc(cn)}</span> <code>${esc(op.path)}</code></div>` +\n `<div class=\"md-interpret-op-actions\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"edit-declared-op\" data-block-index=\"${op.blockIndex !== undefined ? op.blockIndex : 0}\" data-op-index=\"${op.opIndex !== undefined ? op.opIndex : idx}\">编辑</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"delete-declared-op\" data-block-index=\"${op.blockIndex !== undefined ? op.blockIndex : 0}\" data-op-index=\"${op.opIndex !== undefined ? op.opIndex : idx}\">删除</button>` +\n `</div>` +\n `</div>` +\n `<div class=\"md-interpret-op-val\">${renderOpVal(op)}</div>` +\n `</div>`;\n }).join('') + `</div>`;\n }\n\n let pendingBody = '';\n if (pendingError) {\n pendingBody = `<div class=\"md-warning\">当前变更无法构建:${esc(pendingError)}</div>`;\n } else if (!pending.length) {\n pendingBody = `<div class=\"${CLS.EMPTY}\">当前没有待应用的人物或世界数据修改。</div>`;\n } else {\n pendingBody = `<div class=\"md-interpret-ops-list\">` +\n pending.map((op, idx) => {\n const cn = bodyMerge.pathCN(op.path);\n const opName = opNames[op.op] || op.op;\n return `<div class=\"md-interpret-op\" data-op=\"${op.op}\">` +\n `<div class=\"md-interpret-op-header\">` +\n `<div><b>#${idx + 1}</b> <span class=\"md-badge md-badge-${op.op}\">${esc(opName)}</span> <span>${esc(cn)}</span> <code>${esc(op.path)}</code></div>` +\n `</div>` +\n `<div class=\"md-interpret-op-val\">${renderOpVal(op)}</div>` +\n `</div>`;\n }).join('') + `</div>`;\n }\n\n return help +\n `<div class=\"md-note\">${esc(targetText)}。本页修改的是回复中的指令;要撤销已生效的数值变化,请选择历史中的对应备份。</div>` +\n toolbar + analysisHtml +\n section('目标正文中的历史变量声明', existingBody, true, existing.length, 'interpret-existing', 'edit') +\n section('当前待提交变量操作', pendingBody, true, pending.length, 'interpret-pending', 'wand');\n}\n\nfunction renderHistory(mod, state) {\n const help = helpBox(esc(playerHelp.tabs.history));\n const status = state && state.historyStatus || {};\n const failed = status.result && status.result.ok === false;\n const disabled = status.loading || failed;\n const phase = status.loading ? 'loading' : failed ? 'error' : status.result ? 'ready' : 'unread';\n const migrations = Object.values(status.result && status.result.migrations || {});\n const copied = migrations.reduce((sum, item) => sum + Number(item.copied || 0), 0);\n const skipped = migrations.reduce((sum, item) => sum + Number(item.skipped || 0), 0);\n const message = status.loading ? '正在读取本机保存的历史与记录…' : failed ? '历史读取失败,下面的列表可能不是最新记录。请重试。' : status.result ? '历史与记录已读取。' : '打开本页后读取历史与记录。';\n const historyStatus = `<div class=\"md-history-status\" data-history-state=\"${phase}\" role=\"status\" aria-live=\"polite\"><span data-history-message>${message}</span>` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"refresh-history\" ${status.loading ? 'disabled' : ''}>重新读取历史</button>` +\n (migrations.length ? `<p class=\"md-note\" data-history-migration>旧存储迁移:已复制 ${copied} 条,未迁移 ${skipped} 条;旧存储中的原记录仍完整保留。</p>` : '') +\n (failed ? `<details class=\"md-log-details\"><summary>读取失败详情</summary><pre>${esc(status.result.reason || '未知存储错误')}</pre></details>` : '') +\n (status.result && status.result.warnings || []).map((warning) => `<p class=\"md-note\">${esc(warning)}</p>`).join('') + '</div>';\n\n // 1. 快照备份与导入导出\n const snapHtml = `<div class=\"md-history-section\">` +\n `<div class=\"md-note\">导出人物、世界、日历和聊天变量的快照;不包含剧情和正文指令。导入后先暂存,核对并应用后才写入聊天。</div>` +\n `<div class=\"md-inline-actions\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_PRIMARY}\" data-act=\"export-snapshot\">📥 导出当前快照</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"import-snapshot\">📤 导入快照</button>` +\n `</div>` +\n `</div>`;\n\n // 2. 修改历史与回滚\n const list = mod && typeof mod.historyList === 'function' ? mod.historyList() : [];\n const settings = (state && state.settings) || (mod && mod.getSettings ? mod.getSettings() : {}) || {};\n const limit = settings.historyLimit || 20;\n\n let historyRows = list.map((entry, index) => {\n const target = entry.target || {};\n const time = entry.createdAt ? new Date(entry.createdAt).toLocaleString('zh-CN', { hour12: false }) : '未知时间';\n const scope = getBackupScope(entry);\n const outcome = getOperationState(entry);\n const canUndo = !!entry.operationChanges || !scope.scopes.mvu && !scope.scopes.chat && scope.scopes.text;\n const modeBadge = `<span class=\"md-badge ${scope.partial ? 'md-badge-warn' : 'md-badge-direct'}\">${entry.bodyOmitted ? '容量不足:仅变量恢复' : scope.partial ? '旧记录:部分恢复' : '按记录范围恢复'}</span>`;\n return `<div class=\"md-history-row\" data-operation-status=\"${outcome.status}\">` +\n `<div><b>${esc(entry.label || '修改前备份')}</b> <span class=\"md-badge ${outcome.status === 'applied' ? 'md-badge-direct' : 'md-badge-warn'}\">${esc(outcome.label)}</span> ${modeBadge}<small>${esc(time)} · 回复 ${esc(target.messageId)} · 候选版本 ${esc(Number(target.swipeId || 0) + 1)}</small><small>聊天:${esc(target.chatId)}</small><small>恢复范围:${esc(scope.scopeLabel)}</small>${outcome.detail ? `<small>${esc(outcome.detail)}</small>` : ''}${entry.outcomeReason ? `<small>${esc(entry.outcomeReason)}</small>` : ''}${scope.partial ? `<small>${esc(scope.partialReason)}</small>` : ''}</div>` +\n `<div class=\"md-inline-actions\"><button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"rollback\" data-index=\"${index}\" ${disabled || !outcome.canRestore || !canUndo ? 'disabled' : ''}>撤销本次操作</button>` +\n (scope.scopes.mvu || scope.scopes.chat ? `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"restore-full\" data-index=\"${index}\" ${disabled || !outcome.canRestore ? 'disabled' : ''}>完整快照恢复</button>` : '') + `</div>` +\n `</div>`;\n }).join('');\n if (!historyRows) historyRows = `<div class=\"${CLS.EMPTY}\">${disabled ? '等待历史读取成功后显示记录。' : '暂无修改历史。写入前会尝试保存恢复记录;容量不足时请留意操作结果中的警告。'}</div>`;\n\n const rollbackHtml = `<div class=\"md-history-section\">` +\n `<div class=\"md-note\">已保存 ${list.length} 条,最多保留 ${limit} 条,达到条数上限时替换最旧记录。空间不足而需要额外删减备份时会先询问;取消就不继续修改。“撤销本次操作”只还原该次改过的字段及相关正文指令,保留后来无关的修改。“完整快照恢复”会覆盖记录范围内后来发生的变量变化。两种方式都不恢复剧情;旧记录没有逐项变更信息时只能使用完整快照恢复。</div>` +\n `${historyRows}` +\n `<div class=\"md-inline-actions\"><button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"clear-history\" ${list.length && !disabled ? '' : 'disabled'}>清空回滚历史</button></div>` +\n `</div>`;\n\n // 3. 操作日志\n const logs = mod && typeof mod.logList === 'function' ? mod.logList() : [];\n let logRows = logs.map((log) => {\n const time = log.timestamp ? new Date(log.timestamp).toLocaleTimeString('zh-CN', { hour12: false }) : '';\n const statusBadge = log.ok ? `<span class=\"md-badge md-badge-protagonist\">成功</span>` : `<span class=\"md-badge md-badge-warn\">失败</span>`;\n const summary = playerFailure(log.summary);\n const detail = log.error || (summary !== log.summary ? log.summary : '');\n return `<div class=\"md-log-item\">` +\n `<span class=\"md-log-time\">${esc(time)}</span> ` +\n `<span class=\"md-badge md-badge-world\">${esc(log.action)}</span> ` +\n `${statusBadge} ` +\n `<span class=\"md-log-summary\">${esc(summary)}</span>` +\n (detail ? `<details class=\"md-log-details\"><summary>查看详情</summary><pre>${esc(detail)}</pre></details>` : '') +\n `</div>`;\n }).join('');\n if (!logRows) logRows = `<div class=\"${CLS.EMPTY}\">暂无操作日志(最多保留 200 条)。</div>`;\n\n const logHtml = `<div class=\"md-history-section\">` +\n `<div class=\"md-note\">记录应用、回滚、导入和重新处理等操作,最多保留 200 条;失败详情可以展开查看。</div>` +\n `<div class=\"md-log-list\">${logRows}</div>` +\n `<div class=\"md-inline-actions\"><button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"clear-logs\" ${logs.length && !disabled ? '' : 'disabled'}>清空操作日志</button></div>` +\n `</div>`;\n\n // 4. 正文注入记录\n const injections = mod && typeof mod.injectionList === 'function' ? mod.injectionList() : [];\n let injectionRows = injections.map((inj) => {\n const time = inj.timestamp ? new Date(inj.timestamp).toLocaleTimeString('zh-CN', { hour12: false }) : '';\n return `<div class=\"md-injection-item\">` +\n `<span class=\"md-log-time\">${esc(time)}</span> ` +\n `<span class=\"md-badge md-badge-body\">楼层 #${inj.messageId}</span> ` +\n `<span class=\"md-badge md-badge-protagonist\">${inj.count} 条指令</span> ` +\n `<span>${esc(inj.summary)}</span>` +\n `</div>`;\n }).join('');\n if (!injectionRows) injectionRows = `<div class=\"${CLS.EMPTY}\">暂无正文注入记录(最多保留 50 条)。</div>`;\n\n const injectionHtml = `<div class=\"md-history-section\">` +\n `<div class=\"md-note\">记录正文模式下写入目标 AI 消息的指令详情,最多保留 50 条。</div>` +\n `<div class=\"md-injection-list\">${injectionRows}</div>` +\n `<div class=\"md-inline-actions\"><button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"clear-injections\" ${injections.length && !disabled ? '' : 'disabled'}>清空注入记录</button></div>` +\n `</div>`;\n\n return help + historyStatus +\n section('1. 快照备份与导入导出', snapHtml, true, '', 'hist-snapshot', 'save') +\n section('2. 修改历史与回滚', rollbackHtml, true, list.length, 'hist-rollback', 'history') +\n section('3. 操作日志', logHtml, false, logs.length, 'hist-logs', 'variables') +\n section('4. 正文注入记录', injectionHtml, false, injections.length, 'hist-injections', 'edit');\n}\n\nfunction renderSettings(mod, state) {\n const currentSettings = (state && state.settings) || (mod && mod.getSettings ? mod.getSettings() : {}) || {};\n const currentTheme = (state && state.themeFamily) ? { family: state.themeFamily, mode: state.themeMode } : (typeof uiStore !== 'undefined' && uiStore ? uiStore.theme() : {});\n const mode = mod && mod.getApplyMode ? mod.getApplyMode() : (currentSettings.applyMode || 'direct');\n const family = currentTheme.family || currentSettings.themeFamily || 'classic';\n const theme = currentTheme.mode || currentSettings.themeMode || uiStateModule.DEFAULT_PERSISTENT.theme.mode;\n const rememberWindow = currentSettings.rememberWindow !== false;\n const historyLimit = Number.isInteger(currentSettings.historyLimit) ? currentSettings.historyLimit : 20;\n const syncMvu = currentSettings.syncMvu !== false;\n const autoSaveChat = currentSettings.autoSaveChat !== false;\n\n const help = helpBox(esc(playerHelp.tabs.settings));\n\n let baseSettings = `<div class=\"${CLS.GRID}\">` +\n fieldRow('md-set-apply-mode', '默认应用方式',\n `<select id=\"md-set-apply-mode\" data-act=\"setting-change\" data-setting-key=\"applyMode\">` +\n `<option value=\"direct\" ${mode === 'direct' ? 'selected' : ''}>直接写入 (默认)</option>` +\n `<option value=\"body\" ${mode === 'body' ? 'selected' : ''}>正文注入模式</option>` +\n `</select>`,\n '两种方式都会检查并保存实际数值;正文方式还把修改指令合并到目标回复,供以后查看。不会请求模型生成回复。'\n ) +\n fieldRow('md-set-theme-family', '界面样式',\n `<select id=\"md-set-theme-family\" data-act=\"setting-change\" data-setting-key=\"themeFamily\">` +\n `<option value=\"classic\" ${family === 'classic' ? 'selected' : ''}>经典主题 (默认)</option>` +\n `<option value=\"simple\" ${family === 'simple' ? 'selected' : ''}>简洁主题</option>` +\n `</select>`,\n '经典主题提供成熟的暗金层级与卡片设计;简洁主题为极简风格。'\n ) +\n fieldRow('md-set-theme-mode', '配色方案',\n `<select id=\"md-set-theme-mode\" data-act=\"setting-change\" data-setting-key=\"themeMode\">` +\n `<option value=\"dark\" ${theme === 'dark' ? 'selected' : ''}>固定深色</option>` +\n `<option value=\"auto\" ${theme === 'auto' ? 'selected' : ''}>跟随酒馆主题</option>` +\n `<option value=\"light\" ${theme === 'light' ? 'selected' : ''}>固定浅色</option>` +\n `</select>`\n ) +\n fieldRow('md-set-remember-win', '记住窗口位置与尺寸',\n `<select id=\"md-set-remember-win\" data-act=\"setting-change\" data-setting-key=\"rememberWindow\">` +\n `<option value=\"true\" ${rememberWindow ? 'selected' : ''}>是 (默认)</option>` +\n `<option value=\"false\" ${!rememberWindow ? 'selected' : ''}>否</option>` +\n `</select>`\n ) +\n fieldRow('md-set-history-limit', '回滚历史保留上限 (1-100)',\n numberInput('md-set-history-limit', historyLimit, `data-act=\"setting-change\" data-setting-key=\"historyLimit\" min=\"1\" max=\"100\"`),\n '默认保留 20 条,受总存储配额保护'\n ) +\n `</div>`;\n\n let advSettings = `<div class=\"md-note\">⚠ 高级选项默认开启。关闭后可能导致变量显示不同步或需要手动刷新/保存聊天。</div>` +\n `<div class=\"${CLS.GRID}\">` +\n fieldRow('md-set-sync-mvu', '应用后同步触发 MVU 刷新',\n `<select id=\"md-set-sync-mvu\" data-act=\"setting-change-adv\" data-setting-key=\"syncMvu\">` +\n `<option value=\"true\" ${syncMvu ? 'selected' : ''}>开启 (默认)</option>` +\n `<option value=\"false\" ${!syncMvu ? 'selected' : ''}>关闭 (不建议)</option>` +\n `</select>`,\n '开启后应用成功时通知酒馆助手刷新变量'\n ) +\n fieldRow('md-set-auto-save', '应用后自动保存聊天',\n `<select id=\"md-set-auto-save\" data-act=\"setting-change-adv\" data-setting-key=\"autoSaveChat\">` +\n `<option value=\"true\" ${autoSaveChat ? 'selected' : ''}>开启 (默认)</option>` +\n `<option value=\"false\" ${!autoSaveChat ? 'selected' : ''}>关闭 (不建议)</option>` +\n `</select>`,\n '开启后写入成功时触发酒馆 chat 保存'\n ) +\n `</div>`;\n\n const resetBtn = `<div class=\"md-inline-actions\">` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"reset-settings\">恢复默认设置</button>` +\n `</div>`;\n\n return help +\n section('基础设置', baseSettings, true, '', 'settings-base', 'gear') +\n section('高级设置', advSettings, false, '', 'settings-adv', 'spark') +\n resetBtn;\n}\n\nfunction renderAbout() {\n return section('命定之诗修改器 v' + release.DISPLAY_VERSION,\n `<p>作者:${esc(playerHelp.about.author)}</p><p>${esc(playerHelp.about.intro)}</p><p class=\"md-note\">${esc(playerHelp.about.support)}</p>`, true);\n}\nfunction renderTabContent(tabKey, statData, aux = {}, state = {}, mod = null) {\n if (['operations', 'interpretation', 'history', 'settings', 'about', 'start', 'templates', 'calendar'].indexOf(tabKey) < 0 && !statData) {\n return `<div class=\"${CLS.EMPTY}\">尚未读取角色数据,请进入命定之诗聊天后刷新。</div>`;\n }\n if (tabKey === 'protagonist') return renderProtagonist(statData, state);\n if (tabKey === 'npc') return renderNpc(statData, state);\n if (tabKey === 'world') return renderWorld(statData, state, mod);\n if (tabKey === 'calendar') return renderCalendar(aux || {}, state);\n if (tabKey === 'start') return renderStart(mod);\n if (tabKey === 'templates') return renderTemplates(aux || {}, state);\n if (tabKey === 'operations') return renderOperations(mod, state);\n if (tabKey === 'interpretation') return renderInterpretation(mod, state);\n if (tabKey === 'history') return renderHistory(mod, state);\n if (tabKey === 'settings') return renderSettings(mod, state);\n if (tabKey === 'about') return renderAbout();\n return `<div class=\"${CLS.EMPTY}\">未知页面</div>`;\n}\nfunction setPath(root, pointerText, value, validationPath = pointerText) {\n const segments = patch.parsePointer(pointerText);\n if (!segments.length) throw new Error('不允许替换工作区根对象');\n let current = root;\n for (let i = 0; i < segments.length - 1; i++) {\n const key = segments[i];\n if (!util.isSafeKey(key) || key.charAt(0) === '_') throw new Error('路径包含不可写字段');\n if (!current[key] || typeof current[key] !== 'object') current[key] = {};\n current = current[key];\n }\n const last = segments[segments.length - 1];\n if (!util.isSafeKey(last) || last.charAt(0) === '_') throw new Error('路径包含不可写字段');\n editorRegistry.assertPreserved(current[last], value, validationPath);\n current[last] = value;\n}\n\nfunction getPath(root, pointerText) {\n return patch.getPath(root, patch.parsePointer(pointerText));\n}\n\nfunction parseControlValue(raw, type, element) {\n if (type === 'text') return raw;\n if (type === 'boolean') return raw === 'true';\n if (type === 'csv') return Array.from(new Set(raw.split(/[,,]/).map((s) => s.trim()).filter(Boolean)));\n if (type === 'lines') return raw.split(/\\r?\\n/).map((s) => s.trim()).filter(Boolean);\n if (type === 'exp') {\n const normalized = String(raw).trim();\n if (!normalized) throw new Error('必须是非负数字或 MAX');\n if (normalized.toUpperCase() === 'MAX') return 'MAX';\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0) throw new Error('必须是非负数字或 MAX');\n return value;\n }\n if (type === 'number' || type === 'integer') {\n if (String(raw).trim() === '') throw new Error('请输入有效数字');\n const value = Number(raw);\n if (!Number.isFinite(value)) throw new Error('请输入有效数字');\n if (element && element.min !== '' && value < Number(element.min)) throw new Error(`不能小于 ${element.min}`);\n if (element && element.max !== '' && value > Number(element.max)) throw new Error(`不能大于 ${element.max}`);\n if (type === 'integer' && !Number.isInteger(value)) throw new Error('请输入整数');\n return value;\n }\n return raw;\n}\n\nfunction assertSafeTree(value) {\n if (!value || typeof value !== 'object') return;\n if (Array.isArray(value)) { value.forEach(assertSafeTree); return; }\n for (const [key, child] of Object.entries(value)) {\n if (!util.isSafeKey(key) || key.charAt(0) === '_') throw new Error('JSON 中不能新增或编辑 _ / 原型相关键');\n assertSafeTree(child);\n }\n}\n\nfunction replaceSubset(target, edited, excludedKeys) {\n const next = Object.assign({}, target);\n for (const key of Object.keys(next)) if (!excludedKeys.includes(key) && !key.startsWith('_')) delete next[key];\n for (const [key, value] of Object.entries(edited)) if (!excludedKeys.includes(key)) next[key] = value;\n editorRegistry.assertPreserved(target, next);\n for (const key of Object.keys(target)) {\n if (!excludedKeys.includes(key) && key.charAt(0) !== '_') delete target[key];\n }\n for (const [key, value] of Object.entries(edited)) {\n if (!excludedKeys.includes(key)) target[key] = value;\n }\n}\nfunction mount(P, mod) {\n const doc = P && P.document;\n if (!doc || !doc.body || typeof doc.createElement !== 'function' || typeof doc.getElementById !== 'function') return;\n styles.injectStyle(doc);\n unmount(P);\n const uiStore = uiStateModule.createUiStateStore(P);\n const state = uiStore.state;\n const initialSettings = Promise.resolve(typeof mod.setSettings === 'function' ? mod.setSettings(uiStore.settings()) : { ok: true });\n const ds = mod.dirtyState();\n const pages = new Map();\n const pageViews = new Map();\n let controlsChanged = true;\n let previousBusy = null;\n let panelOpened = false;\n let normalGeometry = null;\n let disposed = false;\n const openDialogs = new Set();\n let historyRequest = 0;\n let importPreviewError = '';\n const historyUi = { loading: false, stale: true, result: null };\n\n const root = doc.createElement('div');\n root.id = CLS.ROOT;\n root.className = CLS.ROOT;\n doc.body.appendChild(root);\n const fab = doc.createElement('button');\n fab.type = 'button';\n fab.id = CLS.FAB;\n fab.innerHTML = icon('spark', 20);\n fab.title = '打开命定之诗修改器';\n fab.setAttribute('aria-label', '打开命定之诗修改器');\n doc.body.appendChild(fab);\n\n const tabs = BUSINESS_TABS.concat(TOOL_TABS);\n const tabHtml = tabs.map((tab) => `<button type=\"button\" id=\"md-tab-${tab.key}\" role=\"tab\" aria-controls=\"md-page-${tab.key}\" aria-selected=\"${tab.key === state.activeTab}\" tabindex=\"${tab.key === state.activeTab ? '0' : '-1'}\" data-tab=\"${tab.key}\" class=\"${CLS.TAB} ${tab.key === state.activeTab ? CLS.TAB_ACTIVE : ''}\">${icon(tab.icon)}<span>${esc(tab.label)}</span><span class=\"md-badge\" ${ds.isDirty(tab.key) ? '' : 'hidden'}>●</span></button>`).join('');\n root.innerHTML = `<header class=\"${CLS.HEADER}\"><span class=\"md-title\">${icon('spark', 16)}<span>命定之诗修改器 v${release.DISPLAY_VERSION}</span></span>` +\n `<button type=\"button\" class=\"md-header-btn\" data-act=\"refresh\" title=\"重新读取变量\" aria-label=\"重新读取变量\">↻</button>` +\n `<button type=\"button\" class=\"md-header-btn\" data-act=\"minimize\" title=\"收起到悬浮球\" aria-label=\"收起到悬浮球\">—</button>` +\n `<button type=\"button\" class=\"md-header-btn\" data-act=\"fullscreen\" title=\"切换全屏\" aria-label=\"切换全屏\">⛶</button>` +\n `<button type=\"button\" class=\"md-header-btn\" data-act=\"close\" title=\"关闭面板\" aria-label=\"关闭面板\">×</button></header>` +\n `<nav class=\"${CLS.TABS}\" role=\"tablist\" aria-label=\"修改器页面\">${tabHtml}</nav>` +\n `<main id=\"md-tabpanel\" class=\"${CLS.BODY}\"><div id=\"md-content\" class=\"md-pages\"></div></main>` +\n `<div class=\"${CLS.CHANGES_BAR}\"><span class=\"md-changes-count\">未提交:${ds.total()}</span>` +\n `<select data-act=\"mode\" aria-label=\"应用方式\"><option value=\"body\">正文</option><option value=\"direct\">直接</option></select>` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"view-changes\">${icon('edit')}查看变更</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_WARN}\" data-act=\"discard\">放弃</button>` +\n `<button type=\"button\" class=\"${CLS.BUTTON} ${CLS.BTN_PRIMARY}\" data-act=\"apply\">${icon('save')}应用</button></div>` +\n `<div class=\"${CLS.LIVE}\" data-kind=\"info\" aria-live=\"polite\">正在加载变量…</div>`;\n\n const body = root.querySelector && root.querySelector('.' + CLS.BODY);\n const tabBar = root.querySelector && root.querySelector('.' + CLS.TABS);\n const contentRoot = root.querySelector && root.querySelector('#md-content');\n\n function mounted() { return !disposed && root.parentNode && doc.getElementById(CLS.ROOT) === root; }\n function historyVisible() { return mounted() && root.style.display !== 'none' && state.activeTab === 'history'; }\n function redrawHistory() { if (historyVisible()) { ensurePage('history', true); updateChrome(); } }\n function cancelHistoryView() { historyRequest++; historyUi.loading = false; historyUi.stale = true; }\nasync function refreshHistoryPage() {\n if (!historyVisible()) return;\n const request = ++historyRequest;\n historyUi.loading = true;\n redrawHistory();\n let result;\n try { result = typeof mod.refreshHistory === 'function' ? await mod.refreshHistory() : { ok: true }; }\n catch (error) { result = { ok: false, reason: error.message }; }\n if (request !== historyRequest || !historyVisible()) return;\n historyUi.loading = false;\n historyUi.stale = false;\n historyUi.result = result || { ok: true };\n redrawHistory();\n }\n\n function viewport() {\n return {\n width: Number(P.innerWidth) || (doc.documentElement && doc.documentElement.clientWidth) || 0,\n height: Number(P.innerHeight) || (doc.documentElement && doc.documentElement.clientHeight) || 0,\n };\n }\n\n function applyTheme() {\n const theme = uiStore.theme();\n root.setAttribute('data-theme-style', theme.family);\n root.setAttribute('data-theme', theme.mode);\n fab.setAttribute('data-theme-style', theme.family);\n fab.setAttribute('data-theme', theme.mode);\n }\n\n function applyGeometry(target) {\n const geometry = uiStore.geometry();\n const isFab = target === fab;\n target.style.right = 'auto';\n target.style.left = geometry.x + 'px';\n target.style.top = geometry.y + 'px';\n if (!isFab) {\n target.style.width = geometry.width + 'px';\n target.style.height = geometry.height + 'px';\n }\n }\n\n function setSharedPosition(x, y, persist, element) {\n const view = viewport();\n const fallback = element === fab ? { width: 52, height: 52 } : uiStore.geometry();\n const rect = element && element.getBoundingClientRect ? element.getBoundingClientRect() : fallback;\n const next = uiStateModule.clampGeometry(Object.assign(uiStore.geometry(), { x, y }), view.width, view.height, rect.width || fallback.width, rect.height || fallback.height);\n uiStore.setGeometry({ x: next.x, y: next.y }, persist);\n for (const node of [root, fab]) {\n node.style.right = 'auto';\n node.style.left = next.x + 'px';\n node.style.top = next.y + 'px';\n }\n return next;\n }\n\n function savePanelBounds() {\n if (root.style.display === 'none' || root.classList.contains('md-fullscreen') || !root.getBoundingClientRect) return;\n const rect = root.getBoundingClientRect();\n setSharedPosition(rect.left, rect.top, false, root);\n const current = uiStore.geometry();\n const next = { width: Math.max(280, rect.width), height: Math.max(260, rect.height), lastOpenState: 'panel' };\n if (Math.abs(current.width - next.width) > 0.5 || Math.abs(current.height - next.height) > 0.5 || current.lastOpenState !== 'panel') {\n uiStore.setGeometry(next, true);\n }\n }\n\n function pageState(tabId = state.activeTab) {\n return uiStore.tab(tabId);\n }\n\n function draftKey(id, tabId = state.activeTab) {\n const page = pages.get(tabId);\n return tabId === 'npc' && id !== 'md-npc-select' && id !== 'md-npc-filter'\n ? encodeKey(page && page.getAttribute('data-entity') || pageState(tabId).selectedEntity || '') + '::' + id : id;\n }\n\n function isViewControl(element) {\n return ['select-npc', 'select-collection', 'select-template'].includes(element.getAttribute('data-act')) ||\n element.matches('[data-option-filter],[data-row-filter],[data-json-filter]');\n }\n\n function capturePage(tabId = state.activeTab) {\n return uiStore.batch(() => capturePageState(tabId));\n }\n\n function capturePageState(tabId) {\n const page = pages.get(tabId);\n const saved = pageState(tabId);\n if (body && tabId === state.activeTab) {\n saved.scrollTop = body.scrollTop || 0;\n if (typeof uiStore.setScroll === 'function') uiStore.setScroll(tabId, saved.scrollTop);\n }\n if (tabBar) {\n state.tabBarScrollLeft = tabBar.scrollLeft || 0;\n if (typeof uiStore.setTabBarScroll === 'function') uiStore.setTabBarScroll(state.tabBarScrollLeft);\n }\n if (!page || !page.querySelectorAll) return;\n page.querySelectorAll('[data-card-id]').forEach((card) => {\n const id = card.getAttribute('data-card-id');\n saved.openCards[id] = !!card.open;\n if (typeof uiStore.setCardOpen === 'function') uiStore.setCardOpen(tabId, id, !!card.open);\n });\n page.querySelectorAll('[data-md-touched]').forEach((element) => {\n if (!element.id || isViewControl(element)) return;\n // 只保存用户实际触碰过的控件;程序重绘得到的当前值不能伪装成草稿。\n saved.drafts[draftKey(element.id, tabId)] = element.type === 'checkbox' ? !!element.checked : element.value;\n });\n page.querySelectorAll('[data-option-filter],[data-row-filter],[data-json-filter]').forEach((filter) => {\n if (filter.id) saved.filters[filter.id] = filter.value;\n });\n // 错误由 setFieldError 同步入仓库,不能在索引迁移后用旧 DOM 的空提示覆盖它。\n const active = doc.activeElement;\n if (active && page.contains && page.contains(active) && active.id) {\n saved.focusedField = { id: active.id, start: active.selectionStart, end: active.selectionEnd };\n }\n }\n\n function applyFilter(filter) {\n if (filter.hasAttribute('data-option-filter')) {\n const select = doc.getElementById(filter.getAttribute('data-option-filter'));\n const query = filter.value.trim().toLocaleLowerCase();\n if (select) Array.from(select.options || []).forEach((option) => { option.hidden = !!query && !option.textContent.toLocaleLowerCase().includes(query); });\n } else if (filter.hasAttribute('data-row-filter')) {\n const page = filter.closest('[data-page]');\n const query = filter.value.trim().toLocaleLowerCase();\n if (page) page.querySelectorAll(filter.getAttribute('data-row-filter')).forEach((row) => { row.hidden = !!query && !(row.getAttribute('data-filter-text') || '').includes(query); });\n } else if (filter.hasAttribute('data-json-filter')) updateJsonFilter(filter);\n }\nfunction restoreContent(page, tabId) {\n const saved = pageState(tabId);\n if (!page || !page.querySelectorAll) return;\n page.querySelectorAll('[data-card-id]').forEach((card) => {\n const id = card.getAttribute('data-card-id');\n if (Object.prototype.hasOwnProperty.call(saved.openCards, id)) card.open = !!saved.openCards[id];\n card._mdOpen = !!card.open;\n const summary = card.querySelector('summary');\n if (summary) summary.setAttribute('aria-expanded', card.open ? 'true' : 'false');\n });\n page.querySelectorAll('input,select,textarea').forEach((element) => {\n const key = draftKey(element.id, tabId);\n if (element.id && !isViewControl(element) && Object.prototype.hasOwnProperty.call(saved.drafts, key)) {\n if (element.type === 'checkbox') element.checked = !!saved.drafts[key];\n else element.value = saved.drafts[key];\n element.setAttribute('data-md-touched', 'true');\n }\n });\n page.querySelectorAll('[data-option-filter],[data-row-filter],[data-json-filter]').forEach((filter) => {\n if (filter.id && Object.prototype.hasOwnProperty.call(saved.filters, filter.id)) filter.value = saved.filters[filter.id];\n applyFilter(filter);\n });\n page.querySelectorAll('[data-error-for]').forEach((error) => {\n const id = error.getAttribute('data-error-for');\n const message = saved.errors[draftKey(id, tabId)] || '';\n error.textContent = message;\n if (message) {\n const input = doc.getElementById(id);\n if (input) input.setAttribute('aria-invalid', 'true');\n }\n });\n }\nfunction materialize(scope, tabId) {\n const view = pageViews.get(tabId);\n if (!scope || !view || !scope.querySelectorAll) return;\n for (const slot of scope.querySelectorAll('[data-md-deferred]')) {\n let parent = slot.parentElement;\n while (parent && parent !== pages.get(tabId)) {\n if (parent.tagName === 'DETAILS' && !parent.open || parent.hasAttribute('data-template-pane') && parent.hidden) break;\n parent = parent.parentElement;\n }\n if (parent !== pages.get(tabId)) continue;\n const id = slot.getAttribute('data-md-deferred');\n const factory = view.deferredViews.get(id);\n if (!factory) continue;\n slot.innerHTML = factory();\n slot.removeAttribute('data-md-deferred');\n view.deferredViews.delete(id);\n controlsChanged = true;\n restoreContent(slot, tabId);\n materialize(slot, tabId);\n }\n }\n\n function restorePage(tabId = state.activeTab, restoreFocus = false) {\n const page = pages.get(tabId);\n if (!page) return;\n const saved = pageState(tabId);\n restoreContent(page, tabId);\n materialize(page, tabId);\n if (body) body.scrollTop = saved.scrollTop || 0;\n if (tabBar) tabBar.scrollLeft = state.tabBarScrollLeft || 0;\n if (restoreFocus && saved.focusedField) {\n const field = doc.getElementById(saved.focusedField.id);\n if (field && field.focus) {\n field.focus();\n if (field.setSelectionRange && Number.isFinite(saved.focusedField.start)) field.setSelectionRange(saved.focusedField.start, saved.focusedField.end);\n }\n }\n }\n\n function createViewState(tabId) {\n const saved = pageState(tabId);\n const theme = uiStore.theme();\n return {\n selectedNpc: saved.selectedEntity,\n selectedTemplate: saved.selectedEntity,\n deferredViews: new Map(),\n loadedJson: new Set(Object.keys(saved.loadedEditors).filter((key) => saved.loadedEditors[key])),\n editorModes: saved.editorModes,\n collectionSelections: saved.collectionSelections,\n historyStatus: historyUi,\n themeFamily: theme.family,\n themeMode: theme.mode,\n settings: uiStore.settings(),\n };\n }\n\n function ensurePage(tabId, force = false) {\n if (!contentRoot || !doc.createElement) return null;\n let page = pages.get(tabId);\n if (page && !force) return page;\n if (page) capturePage(tabId);\n const oldPage = page;\n page = doc.createElement('section');\n page.id = `md-page-${tabId}`;\n page.className = 'md-tab-page';\n page.setAttribute('role', 'tabpanel');\n page.setAttribute('aria-labelledby', `md-tab-${tabId}`);\n page.setAttribute('data-page', tabId);\n const viewState = createViewState(tabId);\n pageViews.set(tabId, viewState);\n page.innerHTML = renderTabContent(tabId, mod.getEditor(), mod.getAuxEditor() || {}, viewState, mod);\n if (tabId === 'npc') page.setAttribute('data-entity', viewState.selectedNpc || '');\n pageState(tabId).selectedEntity = (tabId === 'templates' ? viewState.selectedTemplate : viewState.selectedNpc) || pageState(tabId).selectedEntity;\n if (oldPage && oldPage.parentNode) oldPage.parentNode.replaceChild(page, oldPage);\n else contentRoot.appendChild(page);\n pages.set(tabId, page);\n controlsChanged = true;\n restorePage(tabId);\n return page;\n }\n\n function invalidatePages(clearSession, options = {}) {\n if (clearSession) uiStore.clearEditingSession();\n for (const page of pages.values()) if (page.parentNode) page.parentNode.removeChild(page);\n pages.clear();\n pageViews.clear();\n controlsChanged = true;\n }\n\n function switchTab(tabId, focusTab) {\n if (!tabs.some((tab) => tab.key === tabId)) return;\n const wasHistory = state.activeTab === 'history';\n if (wasHistory && tabId !== 'history') cancelHistoryView();\n uiStore.batch(() => {\n if (state.activeTab !== tabId) capturePage(state.activeTab);\n uiStore.setActiveTab(tabId);\n });\n const page = ensurePage(tabId);\n for (const [key, node] of pages) node.hidden = key !== tabId;\n if (root.querySelectorAll) root.querySelectorAll('[data-tab]').forEach((button) => {\n const active = button.getAttribute('data-tab') === tabId;\n button.classList.toggle(CLS.TAB_ACTIVE, active);\n button.setAttribute('aria-selected', active ? 'true' : 'false');\n button.tabIndex = active ? 0 : -1;\n });\n if (page && body) body.scrollTop = pageState(tabId).scrollTop || 0;\n if (tabBar) tabBar.scrollLeft = state.tabBarScrollLeft || 0;\n if (tabId === 'history' && (!wasHistory || historyUi.stale || !historyUi.result)) refreshHistoryPage();\n if (focusTab) {\n const button = root.querySelector(`[data-tab=\"${tabId}\"]`);\n if (button && button.focus) button.focus();\n }\n updateChrome();\n }\n\n function selectTemplate(key) {\n pageState('templates').selectedEntity = key;\n const page = pages.get('templates');\n if (!page) return;\n page.querySelectorAll('[data-template-pane]').forEach((pane) => { pane.hidden = pane.getAttribute('data-template-pane') !== encodeKey(key); });\n materialize(page, 'templates');\n updateChrome();\n }\nfunction revealField(tabId, pathText) {\n const parts = patch.parsePointer(pathText);\n const auxiliary = ['date', 'calendar', 'calendarStore', 'chat', 'chatVariables'].includes(parts[0]);\n const relative = auxiliary ? pointer(parts.slice(1)) : pathText;\n if (tabId === 'npc' && parts[0] === '关系列表') pageState(tabId).selectedEntity = parts[1];\n if (tabId === 'templates') pageState(tabId).selectedEntity = auxiliary ? parts[1] : parts[0];\n for (let i = 1; i < parts.length; i++) pageState(tabId).collectionSelections[pointer(parts.slice(0, i))] = parts[i];\n // 切换对象属于显式视图更新;保留草稿,避免沿用旧对象的详情。\n const existing = pages.get(tabId);\n if (existing) { capturePage(tabId); existing.remove(); pages.delete(tabId); pageViews.delete(tabId); }\n switchTab(tabId);\n const page = pages.get(tabId);\n const matchesPath = (prefix) => prefix && (relative === prefix || relative.startsWith(prefix + '/'));\n for (let depth = 0; page && depth < 12; depth++) {\n let opened = false;\n page.querySelectorAll('[data-view-path]').forEach((group) => {\n if (!matchesPath(group.getAttribute('data-view-path')) || group.closest('[data-template-pane][hidden]')) return;\n const card = group.tagName === 'DETAILS' ? group : group.querySelector('details');\n if (card && !card.open) { card.open = true; opened = true; }\n });\n materialize(page, tabId);\n if (!opened) break;\n }\n const target = page && Array.from(page.querySelectorAll('[data-bind],[data-structure-bind],[data-aux-path],[data-chat-key]')).find((input) =>\n input.getAttribute('data-bind') === relative || input.getAttribute('data-structure-bind') === relative || input.getAttribute('data-aux-path') === relative ||\n input.hasAttribute('data-chat-key') && decodeKey(input.getAttribute('data-chat-key')) === (auxiliary ? parts[1] : parts[0]));\n if (target) {\n let parent = target.parentElement;\n while (parent && parent !== page) { if (parent.tagName === 'DETAILS') parent.open = true; parent = parent.parentElement; }\n target.scrollIntoView({ block: 'center' }); target.focus();\n }\n updateChrome();\n }\n\n function updateChrome() {\n applyTheme();\n root.setAttribute('aria-busy', state.busyOperation ? 'true' : 'false');\n const live = root.querySelector && root.querySelector('.' + CLS.LIVE);\n if (live) {\n const summary = state.liveKind === 'error' ? playerFailure(state.liveMessage) : state.liveMessage;\n live.innerHTML = `<span>${esc(summary)}</span>` + (summary !== state.liveMessage ? ` <button type=\"button\" class=\"${CLS.BUTTON}\" data-act=\"error-details\">详情</button>` : '');\n live.setAttribute('data-kind', state.liveKind);\n }\n const count = root.querySelector && root.querySelector('.md-changes-count');\n if (count) count.textContent = '未提交:' + ds.total();\n if (root.querySelectorAll) root.querySelectorAll('[data-tab]').forEach((button) => {\n const badge = button.querySelector('.md-badge');\n if (badge) badge.hidden = !ds.isDirty(button.getAttribute('data-tab'));\n });\n const currentSettings = uiStore.settings();\n const isAdvDisabled = currentSettings.syncMvu === false || currentSettings.autoSaveChat === false;\n const bar = root.querySelector && root.querySelector('.' + CLS.CHANGES_BAR);\n let warnBadge = bar && bar.querySelector('.md-adv-warn');\n if (isAdvDisabled && bar) {\n if (!warnBadge) {\n warnBadge = doc.createElement('span');\n warnBadge.className = 'md-badge md-badge-warn md-adv-warn';\n warnBadge.title = '更新通知或自动保存已关闭';\n warnBadge.textContent = '⚠ 非默认';\n bar.insertBefore(warnBadge, bar.firstChild);\n }\n } else if (warnBadge && warnBadge.parentNode) {\n warnBadge.parentNode.removeChild(warnBadge);\n }\n const mode = root.querySelector && root.querySelector('[data-act=\"mode\"]');\n if (mode) mode.value = mod.getApplyMode();\n const discard = root.querySelector && root.querySelector('[data-act=\"discard\"]');\n if (discard) discard.disabled = !mod.hasDrafts() || state.busyOperation;\n const apply = root.querySelector && root.querySelector('[data-act=\"apply\"]');\n if (apply) apply.innerHTML = state.busyOperation ? '处理中…' : icon('save') + '应用';\n if (root.querySelectorAll && (controlsChanged || previousBusy !== state.busyOperation)) root.querySelectorAll('input,select,textarea,button').forEach((element) => {\n if (state.busyOperation) {\n if (!element.disabled) element.setAttribute('data-busy-disabled', 'true');\n element.disabled = true;\n } else if (element.getAttribute && element.getAttribute('data-busy-disabled') === 'true') {\n element.disabled = false;\n element.removeAttribute('data-busy-disabled');\n }\n });\n const invalid = Object.values(state.tabs).some(tab => Object.values(tab.errors || {}).some(Boolean));\n if (apply) apply.disabled = state.busyOperation || !mod.getSnapshot() || invalid;\n if (!mod.getSnapshot() && controlsChanged && root.querySelectorAll) root.querySelectorAll('[data-bind],[data-aux-root],[data-structure-bind],[data-chat-key],[data-json-bind],[data-json-aux-root],[data-json-subset],[data-json-aux-subset]')\n .forEach(element => { element.disabled = true; });\n previousBusy = state.busyOperation;\n controlsChanged = false;\n }\n\n function setLive(message, kind = 'info') {\n state.liveMessage = message;\n state.liveKind = kind;\n updateChrome();\n }\n\n function reportOutcome(result, success, extraWarning = '') {\n const warnings = [result && result.warning, ...(result && Array.isArray(result.warnings) ? result.warnings : []), extraWarning].filter(Boolean);\n const ok = result && result.ok;\n const message = ok ? result.noWrite ? '已保留当前值,没有执行新的写入' : success : result && result.reason || '操作失败';\n setLive(message + (warnings.length ? ';注意:' + warnings.join(';') : ''), ok ? warnings.length ? 'warning' : 'success' : 'error');\n }\n\n function mark(tabId, pathText) {\n if (typeof mod.invalidateChangeSet === 'function') mod.invalidateChangeSet();\n ds.markDirty(tabId, pathText);\n updateChrome();\n }\n\n function setFieldError(id, message) {\n const target = root.querySelector && root.querySelector(`[data-error-for=\"${cssEscape(id)}\"]`);\n if (target) target.textContent = message || '';\n const input = doc.getElementById(id);\n if (input) input.setAttribute('aria-invalid', message ? 'true' : 'false');\n const saved = pageState();\n if (message) saved.errors[draftKey(id)] = message;\n else delete saved.errors[draftKey(id)];\n }\n\n function rerenderActive(options = {}) {\n if (options.clearDrafts) { capturePage(); pageState().drafts = {}; }\n ensurePage(state.activeTab, true);\n for (const [key, node] of pages) node.hidden = key !== state.activeTab;\n updateChrome();\n }\nfunction forgetDrafts(prefix, keepId) {\n const saved = pageState();\n const matches = (id) => id !== keepId && (id === prefix || id.startsWith(prefix + '-'));\n for (const data of [saved.drafts, saved.errors]) {\n for (const id of Object.keys(data)) if (id !== draftKey(keepId || '') && (id === draftKey(prefix) || id.startsWith(draftKey(prefix) + '-'))) delete data[id];\n }\n const page = pages.get(state.activeTab);\n if (page) page.querySelectorAll('input,select,textarea').forEach((input) => {\n if (matches(input.id)) { input.removeAttribute('data-md-touched'); setFieldError(input.id, ''); }\n });\n }\n\n function editorIdFor(element) {\n const card = element.closest('[data-card-id^=\"editor-\"]');\n return card ? card.getAttribute('data-card-id').slice(7) : '';\n }\n\n function moveDrafts(from, to) {\n if (from === to) return;\n const prefix = draftKey(from);\n for (const data of [pageState().drafts, pageState().errors]) {\n for (const key of Object.keys(data)) {\n if (key !== prefix && !key.startsWith(prefix + '-')) continue;\n const suffix = key.slice(prefix.length);\n if (suffix !== '-name') data[draftKey(to) + suffix] = data[key];\n }\n }\n forgetDrafts(from);\n }\n\n function updateJsonFilter(filter) {\n const targetId = filter.getAttribute('data-json-filter');\n const page = pages.get(state.activeTab);\n const output = page && page.querySelector(`[data-json-index-for=\"${cssEscape(targetId)}\"]`);\n const area = doc.getElementById(targetId);\n if (!output || !area) return;\n const query = filter.value.trim().toLocaleLowerCase();\n if (!query) { output.hidden = true; output.innerHTML = ''; return; }\n try {\n if (!Array.isArray(filter._mdTopLevelKeys)) {\n const parsed = JSON.parse(area.value);\n filter._mdTopLevelKeys = parsed && typeof parsed === 'object' ? Object.keys(parsed) : [];\n }\n const matches = filter._mdTopLevelKeys.filter((key) => String(key).toLocaleLowerCase().includes(query));\n output.innerHTML = matches.slice(0, 40).map((key) => `<button type=\"button\" class=\"${CLS.BUTTON}\" data-json-jump=\"${encodeKey(key)}\">${esc(key)}</button>`).join('') || '<small>没有匹配的顶层键。</small>';\n output.hidden = false;\n } catch (error) {\n output.innerHTML = '<small>当前 JSON 尚未通过解析,无法筛选。</small>';\n output.hidden = false;\n }\n }\n\n function handleJsonChange(element) {\n try {\n const parsed = JSON.parse(element.value);\n if (!parsed || typeof parsed !== 'object') throw new Error('此编辑器必须保存 JSON 对象或数组');\n assertSafeTree(parsed);\n if (element.hasAttribute('data-json-bind')) {\n const pathText = element.getAttribute('data-json-bind');\n const original = getPath(mod.getEditor(), pathText);\n setPath(mod.getEditor(), pathText, restoreReadonly(original, parsed));\n mark(element.getAttribute('data-dirty-tab'), pathText);\n } else if (element.hasAttribute('data-json-aux-root')) {\n const rootName = element.getAttribute('data-json-aux-root');\n const pathText = element.getAttribute('data-json-aux-path');\n const original = getPath(mod.getAuxEditor()[rootName], pathText);\n setPath(mod.getAuxEditor()[rootName], pathText, restoreReadonly(original, parsed), `/${rootName}${pathText}`);\n mark(element.getAttribute('data-dirty-tab'), `/${rootName}${pathText}`);\n } else if (element.hasAttribute('data-json-subset')) {\n const pathText = element.getAttribute('data-json-subset');\n const target = getPath(mod.getEditor(), pathText);\n replaceSubset(target, restoreReadonly(target, parsed), (element.getAttribute('data-json-subset-keys') || '').split(',').filter(Boolean));\n mark(element.getAttribute('data-dirty-tab'), pathText);\n } else {\n const rootName = element.getAttribute('data-json-aux-subset');\n const target = mod.getAuxEditor()[rootName];\n const preservedPath = element.getAttribute('data-json-subset-path');\n const preserved = preservedPath ? util.deepClone(getPath(target, preservedPath)) : undefined;\n replaceSubset(target, restoreReadonly(target, parsed), (element.getAttribute('data-json-subset-keys') || '').split(',').filter(Boolean));\n if (preservedPath && preserved !== undefined) setPath(target, preservedPath, preserved);\n mark(element.getAttribute('data-dirty-tab'), '/' + rootName);\n }\n setFieldError(element.id, '');\n const editorId = editorIdFor(element);\n if (editorId) forgetDrafts(editorId, element.id);\n return true;\n } catch (error) { setFieldError(element.id, 'JSON 未保存:' + formatJsonError(element.value, error)); return false; }\n }\n\n function handleBoundChange(element, targetRoot, pathText, dirtyPath) {\n try {\n const value = parseControlValue(element.value, element.getAttribute('data-type'), element);\n setPath(targetRoot, pathText, value, dirtyPath || pathText);\n setFieldError(element.id, '');\n const editorId = editorIdFor(element);\n if (editorId) forgetDrafts(editorId + '-raw');\n mark(element.getAttribute('data-dirty-tab'), dirtyPath || pathText);\n } catch (error) { setFieldError(element.id, error.message); }\n }\n\n function updateWorldTime(element) {\n const group = element.closest('[data-world-time-group]');\n if (!group) return;\n const read = (key) => group.querySelector(`[data-world-time-part=\"${key}\"]`).value.trim();\n const pad = (value) => String(Math.max(0, Number(value) || 0)).padStart(2, '0');\n const result = `${read('era')}纪元${Number(read('year')) || 0}年${Number(read('month')) || 1}月${Number(read('day')) || 1}日 ${read('weekday')} ${pad(read('hour'))}:${pad(read('minute'))}`;\n setPath(mod.getEditor(), '/世界/时间', result);\n mark('world', '/世界/时间');\n }\n\n async function dispatch(action, element) {\n if (state.busyOperation) return;\n if (action === 'refresh-history') {\n await refreshHistoryPage();\n } else if (action === 'refresh') {\n if (mod.hasDrafts() && !await confirmDialog('刷新快照', '刷新会放弃当前未提交修改,包括尚未填完的输入。', '继续刷新', true)) return;\n state.busyOperation = true; updateChrome();\n try {\n const loaded = await mod.load();\n invalidatePages(true);\n ensurePage(state.activeTab);\n setLive(loaded ? '已刷新并重新锁定目标' : '当前回复没有可编辑的角色变量', loaded ? 'success' : 'error');\n } catch (error) { setLive('刷新失败:' + error.message, 'error'); }\n state.busyOperation = false; updateChrome();\n } else if (action === 'close') {\n if (ds.total() && !await confirmDialog('关闭修改器', '关闭会放弃当前未提交修改。', '关闭', true)) return;\n if (ds.total()) { mod.discard(); invalidatePages(true); }\n hideToFloatingButton();\n } else if (action === 'minimize') hideToFloatingButton();\n else if (action === 'fullscreen') toggleFullscreen();\n else if (action === 'view-changes') {\n try { mod.buildChangeSet(); await showDiff(mod.preview(), false, mod.getApplyMode()); }\n catch (error) { setLive(error.message, 'error'); }\n } else if (action === 'discard') {\n if (!await confirmDialog('放弃修改', '确定恢复到本次加载的快照吗?', '放弃', true)) return;\n mod.discard(); invalidatePages(true); ensurePage(state.activeTab); setLive('已放弃未提交修改');\n } else if (action === 'apply') await doApply();\n else if (action === 'add-npc') {\n const input = doc.getElementById('md-npc-new');\n const name = input && input.value.trim();\n if (!name || !util.isSafeKey(name) || name.charAt(0) === '_') return setLive('NPC 名称为空或不可用', 'error');\n const relations = mod.getEditor()['关系列表'] || (mod.getEditor()['关系列表'] = {});\n if (relations[name]) return setLive('已存在同名 NPC', 'error');\n relations[name] = newNpcTemplate(); pageState('npc').selectedEntity = name; mark('npc', pointer(['关系列表', name])); rerenderActive();\n } else if (action === 'delete-npc') {\n const name = pageState('npc').selectedEntity;\n if (editorRegistry.hasReadonly((mod.getEditor()['关系列表'] || {})[name])) return setLive('此 NPC 包含角色卡维护的只读字段,不能整体删除', 'error');\n if (!name || !await confirmDialog('删除 NPC', `确定删除 ${esc(name)} 及其全部角色数据吗?`, '删除', true)) return;\n const relations = mod.getEditor()['关系列表'];\n delete relations[name]; pageState('npc').selectedEntity = Object.keys(relations)[0] || ''; mark('npc', pointer(['关系列表', name])); rerenderActive();\n } else if (action === 'append-signal') {\n const input = doc.getElementById('md-event-signal');\n const val = input && input.value.trim();\n if (!val) return setLive('追加信号内容为空', 'error');\n const additions = val.split(/[,,]/).map((item) => item.trim()).filter(Boolean);\n const existingSignals = (mod.getEditor()['事件'] && mod.getEditor()['事件']['信号']) || [];\n const msg = `当前既有信号:${existingSignals.join(',') || '无'}<br>准备追加新信号:<b>${additions.join(',')}</b><br>信号追加后不可删除或重排,确定追加吗?`;\n if (!await confirmDialog('追加事件信号', msg, '追加', false)) return;\n const result = patch.appendEventSignal(mod.getEditor(), additions);\n if (!result.ok) return setLive(result.reason, 'error');\n mod.getEditor()['事件']['信号'] = result.ops[0].value; mark('world', '/事件/信号');\n if (input) input.value = '';\n setLive('已追加事件信号', 'success'); rerenderActive();\n } else if (action === 'discard-single') {\n const pathText = element.getAttribute('data-path');\n const tab = element.getAttribute('data-tab');\n if (!pathText) return;\n mod.discardSingle(pathText);\n if (tab) ds.removeDirty(tab, pathText);\n setLive(`已撤销 ${pathText} 的修改`, 'success');\n rerenderActive();\n } else if (action === 'jump-field') {\n const pathText = element.getAttribute('data-path');\n const tab = element.getAttribute('data-tab');\n if (tab) revealField(tab, pathText);\n } else if (action === 'generate-patch-instruction') {\n const items = mod.preview();\n const ops = items.filter((item) => !item.path.startsWith('/date') && !item.path.startsWith('/calendar') && !item.path.startsWith('/chat')).map((item) => ({\n op: item.op,\n path: item.path,\n value: item.newValue,\n }));\n const patchText = bodyMerge.buildFullPatchText(ops, true);\n await dialog('生成的正文差异指令', `<div class=\"md-note\">可将以下指令块直接追加到最新 AI 回复正文末尾:</div><textarea class=\"md-json\" readonly style=\"height:220px\">${esc(patchText)}</textarea>`, null, false);\n } else if (action === 'reprocess-mvu') {\n if (!await confirmDialog('重新执行正文指令', '重新执行这条回复中的全部变量指令并保存结果。增减指令可能再次生效;只编辑或删除正文指令后,不一定需要执行此操作。继续吗?', '执行', false)) return;\n state.busyOperation = true; updateChrome();\n let result;\n try { result = await mod.reprocessMvu(); }\n catch (e) { result = { ok: false, reason: e.message || String(e) }; }\n if (!mounted()) return;\n state.busyOperation = false;\n if (result.ok) { invalidatePages(true); ensurePage(state.activeTab); }\n reportOutcome(result, '正文指令已重新执行并保存变量');\n rerenderActive();\n } else if (action === 'export-snapshot') {\n try {\n const json = mod.exportSnapshot();\n if (typeof doc.createElement === 'function' && typeof Blob !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {\n const blob = new Blob([json], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const a = doc.createElement('a');\n a.href = url;\n a.download = `mingding-snapshot-${Date.now()}.json`;\n doc.body.appendChild(a);\n a.click();\n doc.body.removeChild(a);\n URL.revokeObjectURL(url);\n setLive('快照已成功导出为 JSON 文件', 'success');\n } else {\n await dialog('导出快照数据', `<div class=\"md-note\">请复制以下快照 JSON 内容:</div><textarea class=\"md-json\" readonly style=\"height:260px\">${esc(json)}</textarea>`, null, false);\n }\n } catch (e) { setLive('导出失败:' + e.message, 'error'); }\n } else if (action === 'import-snapshot') {\n const inputHtml = `<div class=\"md-note\" style=\"margin-bottom:8px\">请选择本地快照 JSON 文件,或直接在下方粘贴 JSON 文本恢复到当前工作副本:</div>` +\n `<div style=\"display:flex;align-items:center;gap:8px;margin-bottom:10px;flex-wrap:wrap\">` +\n `<label class=\"${CLS.BUTTON} ${CLS.BTN_PRIMARY}\" style=\"display:inline-flex;align-items:center;gap:6px;cursor:pointer;padding:7px 14px;font-size:13px;border-radius:6px;font-weight:600\">` +\n `<span>📁 选择本地快照文件 (.json)...</span>` +\n `<input type=\"file\" id=\"md-import-file\" accept=\".json,application/json\" style=\"display:none\">` +\n `</label>` +\n `<button type=\"button\" class=\"${CLS.BUTTON}\" id=\"md-import-paste-btn\" style=\"display:inline-flex;align-items:center;gap:6px;padding:7px 12px;font-size:13px;border-radius:6px\">` +\n `<span>📋 从剪贴板粘贴</span>` +\n `</button>` +\n `<span id=\"md-import-filename\" style=\"font-size:12px;color:var(--md-tx2,#8f9bb3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px\"></span>` +\n `</div>` +\n `<textarea id=\"md-import-text\" class=\"md-json\" placeholder=\"选择文件或粘贴后,在此预览/编辑快照 JSON...\" style=\"height:200px;width:100%;box-sizing:border-box\"></textarea>`;\n let importedText = '';\n const confirmed = await dialog(\n '导入快照',\n inputHtml,\n '导入到工作区',\n false,\n (overlay) => {\n const fileInput = overlay.querySelector('#md-import-file');\n const textarea = overlay.querySelector('#md-import-text');\n const fileNameSpan = overlay.querySelector('#md-import-filename');\n const pasteBtn = overlay.querySelector('#md-import-paste-btn');\n if (fileInput && textarea) {\n fileInput.addEventListener('change', (ev) => {\n const file = ev.target.files && ev.target.files[0];\n if (file) {\n if (fileNameSpan) fileNameSpan.textContent = `已选择: ${file.name}`;\n const reader = new FileReader();\n reader.onload = (e) => {\n textarea.value = e.target.result || '';\n };\n reader.readAsText(file, 'utf-8');\n }\n });\n }\n if (pasteBtn && textarea) {\n pasteBtn.addEventListener('click', async () => {\n try {\n if (navigator && navigator.clipboard && typeof navigator.clipboard.readText === 'function') {\n const text = await navigator.clipboard.readText();\n if (text) {\n textarea.value = text;\n if (fileNameSpan) fileNameSpan.textContent = '已从剪贴板读取';\n }\n } else {\n textarea.focus();\n }\n } catch (e) {\n textarea.focus();\n }\n });\n }\n },\n (overlay) => {\n const textarea = overlay.querySelector('#md-import-text');\n importedText = textarea ? textarea.value.trim() : '';\n return true;\n }\n );\n if (!confirmed) return;\n const text = importedText;\n if (!text) return setLive('未提供快照内容', 'error');\n const importTarget = mod.getSnapshot();\n state.busyOperation = true; updateChrome();\n try {\n const res = await mod.importSnapshot(text);\n if (!mounted()) return;\n state.busyOperation = false;\n const targetChanged = importTarget !== mod.getSnapshot() || res && res.stale;\n const warning = [targetChanged ? '聊天目标已改变,请先核对原目标草稿;当前页面未被迟到结果替换' : '', importPreviewError ? '导入内容尚未通过检查,可放弃本次导入:' + importPreviewError : ''].filter(Boolean).join(';');\n reportOutcome(res, '快照已导入工作副本,尚未写入聊天;核对后点击“应用”', warning);\n } catch (e) { if (mounted()) { state.busyOperation = false; setLive('导入失败:' + e.message, 'error'); } }\n } else if (['clear-logs', 'clear-injections', 'clear-history'].includes(action)) {\n const [method, label] = { 'clear-logs': ['clearLogs', '操作日志'], 'clear-injections': ['clearInjections', '正文注入记录'], 'clear-history': ['clearHistory', '回滚历史'] }[action];\n if (!await confirmDialog('清空' + label, `确定清空所有${label}吗?此操作不可撤销。`, '清空', true)) return;\n state.busyOperation = true; updateChrome();\n let result;\n try { result = await mod[method](); } catch (error) { result = { ok: false, reason: error.message }; }\n if (!mounted()) return;\n state.busyOperation = false;\n reportOutcome(result || { ok: true }, label + '已清空');\n redrawHistory();\n } else if (action === 'edit-declared-op') {\n const blockIndex = Number(element.getAttribute('data-block-index'));\n const opIndex = Number(element.getAttribute('data-op-index'));\n const snap = mod.getSnapshot();\n const { ops } = bodyMerge.parseUpdateBlocksWithAnalysis(snap ? snap.messageText : '');\n const targetOp = ops.find((o) => o.blockIndex === blockIndex && o.opIndex === opIndex);\n if (!targetOp) return setLive('未找到对应声明', 'error');\n const editHtml = `<div class=\"md-note\">编辑并写回这条指令,写前备份相关指令块,不保存剧情全文;空间不足会先询问。</div>` +\n `<div class=\"${CLS.FIELD}\"><label>操作类型</label><div class=\"md-control\"><select id=\"md-edit-op-type\"><option value=\"replace\" ${targetOp.op === 'replace' ? 'selected' : ''}>replace (修改)</option><option value=\"insert\" ${targetOp.op === 'insert' ? 'selected' : ''}>insert (新增)</option><option value=\"remove\" ${targetOp.op === 'remove' ? 'selected' : ''}>remove (删除)</option></select></div></div>` +\n `<div class=\"${CLS.FIELD}\"><label>路径</label><div class=\"md-control\"><input type=\"text\" id=\"md-edit-op-path\" value=\"${esc(targetOp.path)}\"></div></div>` +\n `<div class=\"${CLS.FIELD}\"><label>值 (JSON 或字符串)</label><div class=\"md-control\"><textarea id=\"md-edit-op-val\" style=\"height:120px\">${esc(targetOp.value !== undefined ? (typeof targetOp.value === 'string' ? targetOp.value : JSON.stringify(targetOp.value, null, 2)) : '')}</textarea></div></div>`;\n try {\n let formResult = null;\n const ok = await dialog(\n '编辑正文变量声明',\n editHtml,\n '保存并写回正文',\n false,\n null,\n (overlay) => {\n const typeEl = overlay.querySelector('#md-edit-op-type');\n const pathEl = overlay.querySelector('#md-edit-op-path');\n const valEl = overlay.querySelector('#md-edit-op-val');\n formResult = {\n newType: typeEl ? typeEl.value : 'replace',\n newPath: pathEl ? pathEl.value.trim() : '',\n rawVal: valEl ? valEl.value.trim() : '',\n };\n return true;\n }\n );\n if (!ok || !formResult) return;\n const { newType, newPath, rawVal } = formResult;\n if (!newPath) return setLive('路径不能为空', 'error');\n let newVal;\n if (newType !== 'remove' && rawVal) {\n try { newVal = JSON.parse(rawVal); }\n catch (e) { newVal = rawVal; }\n }\n state.busyOperation = true; updateChrome();\n const res = await mod.updateDeclaredOp(blockIndex, opIndex, { op: newType, path: newPath, value: newVal });\n if (!mounted()) return;\n state.busyOperation = false;\n if (res && res.ok) {\n invalidatePages(true);\n ensurePage(state.activeTab);\n reportOutcome(res, '正文声明已更新;剧情不变,实际数值未重新处理');\n } else {\n setLive('更新失败:' + ((res && res.reason) || '未知错误'), 'error');\n }\n } catch (e) {\n if (!mounted()) return;\n state.busyOperation = false;\n setLive('更新异常:' + e.message, 'error');\n }\n rerenderActive();\n } else if (action === 'delete-declared-op') {\n const blockIndex = Number(element.getAttribute('data-block-index'));\n const opIndex = Number(element.getAttribute('data-op-index'));\n if (!await confirmDialog('删除正文声明', '确定从当前 AI 回复中删除这条指令吗?写前会尝试备份相关指令;空间不足会先询问,取消就保留原指令。', '删除', true)) return;\n state.busyOperation = true; updateChrome();\n try {\n const res = await mod.removeDeclaredOp(blockIndex, opIndex);\n if (!mounted()) return;\n state.busyOperation = false;\n if (res.ok) { invalidatePages(true); ensurePage(state.activeTab); reportOutcome(res, '已删除正文声明;实际数值保持不变'); }\n else { setLive('删除失败:' + res.reason, 'error'); }\n } catch (e) { if (!mounted()) return; state.busyOperation = false; setLive('删除异常:' + e.message, 'error'); }\n rerenderActive();\n } else if (action === 'reset-settings') {\n if (!await confirmDialog('恢复默认设置', '恢复经典固定深色、直接写入、20 条历史上限、窗口位置尺寸及页面展开滚动设置。开启同步刷新和自动保存;保留业务编辑草稿。继续吗?', '恢复', true)) return;\n const reset = uiStore.resetToDefaults({ preserveDrafts: true });\n const applied = typeof mod.setSettings === 'function' ? await mod.setSettings(reset.persistent.settings) : { ok: true };\n root.classList.remove('md-fullscreen'); normalGeometry = null;\n applyTheme(); applyGeometry(root); applyGeometry(fab); clampPanel();\n // resetToDefaults 已经清除了设置页草稿和所有窗口/卡片视图状态;\n // 这里不能再先 capturePage,否则旧设置会被 restorePage 写回。\n invalidatePages(false);\n ensurePage(state.activeTab);\n switchTab(state.activeTab);\n const resetError = !reset.saved ? '浏览器拒绝保存设置' : applied && applied.ok === false ? applied.reason : '';\n setLive(resetError ? '默认设置未完全生效:' + resetError : '已恢复默认设置(业务编辑草稿已保留)', resetError ? 'error' : 'success');\n } else if (action === 'add-chat-var') {\n const input = doc.getElementById('md-chat-new-key');\n const key = input && input.value.trim();\n if (!key || !util.isSafeKey(key) || key.charAt(0) === '_') return setLive('变量名为空或不可用', 'error');\n if (Object.prototype.hasOwnProperty.call(mod.getAuxEditor().chatVariables, key)) return setLive('已存在同名变量', 'error');\n mod.getAuxEditor().chatVariables[key] = ''; pageState('templates').selectedEntity = key;\n mark('templates', '/chatVariables/' + key); rerenderActive();\n } else if (action === 'delete-chat-var') {\n const key = decodeKey(element.getAttribute('data-key'));\n if (!await confirmDialog('删除模板变量', `确定删除 ${esc(key)} 吗?`, '删除', true)) return;\n delete mod.getAuxEditor().chatVariables[key]; mark('templates', '/chatVariables/' + key); rerenderActive();\n } else if (action === 'editor-mode') {\n const editorId = element.getAttribute('data-editor-id');\n const raw = doc.getElementById(editorId + '-raw');\n if (raw && !handleJsonChange(raw)) return setLive('原始 JSON 尚未保存,请先修正标出的错误后再切换', 'error');\n const card = element.closest('[data-card-id^=\"editor-\"]');\n const errorPrefix = draftKey(editorId);\n const storedError = Object.entries(pageState().errors).some(([key, message]) => message && (key === errorPrefix || key.startsWith(errorPrefix + '-')));\n if (!raw && (storedError || card && card.querySelector('[aria-invalid=\"true\"]'))) return setLive('此卡片有未保存的错误输入,请先修正后再切换', 'error');\n if (raw) forgetDrafts(editorId);\n pageState().editorModes[editorId] = element.getAttribute('data-editor-mode');\n if (state.liveMessage.startsWith('原始 JSON 尚未保存') || state.liveMessage.startsWith('此卡片有未保存')) {\n setLive('已切换编辑方式;修改仍暂存在工作区');\n }\n rerenderActive();\n } else if (action.startsWith('tree-')) {\n const id = element.getAttribute('data-tree-id');\n const pathText = element.getAttribute('data-tree-path');\n const rootName = element.getAttribute('data-tree-aux');\n const target = rootName ? mod.getAuxEditor()[rootName] : mod.getEditor();\n const collection = getPath(target, pathText);\n const operation = action.slice(5);\n const input = doc.getElementById(id + '-name');\n const key = element.getAttribute('data-tree-key');\n const type = doc.getElementById(id + '-type');\n const valueInput = doc.getElementById(element.getAttribute('data-tree-value-input') || '');\n if (operation === 'remove' && !await confirmDialog('删除字段', `确定删除 ${esc(key)} 及其中的内容吗?`, '删除', true)) return;\n try {\n if (target !== (rootName ? mod.getAuxEditor()[rootName] : mod.getEditor())) throw new Error('数据已重新加载,请检查当前字段后再操作');\n if (valueInput && !valueInput.value.trim()) throw new Error('请先填写新增条目');\n capturePage();\n editorRegistry.editTree(target, pathText, operation, key, input && input.value.trim(), type ? type.value : 'text', !rootName);\n if (valueInput) collection[collection.length - 1] = valueInput.value.trim();\n setFieldError(id + '-name', '');\n const scope = element.getAttribute('data-tree-scope');\n if (operation === 'rename') moveDrafts(id, scope + '-key-' + encodeKey(input.value.trim()));\n else {\n forgetDrafts(id);\n if (Array.isArray(collection) && operation === 'remove') {\n for (let i = Number(key) + 1; i <= collection.length; i++) moveDrafts(scope + '-key-' + i, scope + '-key-' + (i - 1));\n }\n }\n const editorId = editorIdFor(element);\n if (editorId) forgetDrafts(editorId + '-raw');\n mark(element.getAttribute('data-dirty-tab'), (rootName ? '/' + rootName : '') + pathText);\n if (state.treeError && state.treeError.id === id && state.liveMessage === state.treeError.message) setLive('本项修改已暂存,应用后写入聊天');\n state.treeError = null;\n rerenderActive();\n } catch (error) {\n state.treeError = { id, message: error.message };\n setFieldError(id + '-name', error.message); setLive(error.message, 'error');\n }\n } else if (action === 'add-collection-item') {\n const editorId = element.getAttribute('data-editor-id');\n const input = doc.getElementById(editorId + '-new');\n const name = input && input.value.trim();\n const pathText = element.getAttribute('data-collection-path');\n if (!name || !util.isSafeKey(name) || name.charAt(0) === '_') return setLive('条目名称为空或不可用', 'error');\n const collection = getPath(mod.getEditor(), pathText);\n if (!collection || typeof collection !== 'object' || Array.isArray(collection)) return setLive('集合结构不可编辑', 'error');\n if (Object.prototype.hasOwnProperty.call(collection, name)) return setLive('已存在同名条目', 'error');\n collection[name] = editorRegistry.createDefault(element.getAttribute('data-collection-kind'));\n pageState().collectionSelections[pathText] = name;\n forgetDrafts(editorId + '-new'); forgetDrafts((editorIdFor(element) || editorId) + '-raw');\n mark(element.closest('[data-page]').getAttribute('data-page'), `${pathText}/${patch.escapeSegment(name)}`); rerenderActive();\n } else if (action === 'delete-collection-item' || action === 'rename-collection-item') {\n const pathText = element.getAttribute('data-collection-path');\n const oldName = decodeKey(element.getAttribute('data-old-name'));\n const collection = getPath(mod.getEditor(), pathText);\n if (!collection || !Object.prototype.hasOwnProperty.call(collection, oldName)) return;\n if (oldName.startsWith('_') || editorRegistry.hasReadonly(collection[oldName])) return setLive('此条目包含只读字段,不能删除或改名', 'error');\n capturePage();\n const editorId = element.getAttribute('data-editor-id') || editorIdFor(element);\n if (action === 'delete-collection-item') {\n if (!await confirmDialog('删除条目', `确定删除 ${esc(oldName)} 吗?`, '删除', true)) return;\n if (collection !== getPath(mod.getEditor(), pathText)) return setLive('数据已重新加载,请检查当前条目后再操作', 'error');\n delete collection[oldName];\n } else {\n const input = doc.getElementById(element.getAttribute('data-name-input'));\n const nextName = input && input.value.trim();\n if (!nextName || !util.isSafeKey(nextName) || nextName.charAt(0) === '_') return setLive('新名称不可用', 'error');\n if (nextName !== oldName && Object.prototype.hasOwnProperty.call(collection, nextName)) return setLive('已存在同名条目', 'error');\n if (nextName !== oldName) {\n collection[nextName] = collection[oldName]; delete collection[oldName];\n pageState().collectionSelections[pathText] = nextName;\n if (editorId) moveDrafts(editorId + '-item-' + encodeKey(oldName), editorId + '-item-' + encodeKey(nextName));\n }\n }\n if (editorId) {\n if (action === 'delete-collection-item') forgetDrafts(editorId + '-item-' + encodeKey(oldName));\n forgetDrafts((editorIdFor(element) || editorId) + '-raw');\n }\n mark(element.closest('[data-page]').getAttribute('data-page'), pathText); rerenderActive();\n } else if (action.indexOf('set-reincarnation') === 0) {\n const input = doc.getElementById('md-reincarnation');\n const raw = input && String(input.value).trim();\n const value = action === 'set-reincarnation-dev' ? 666666 : action === 'set-reincarnation-1000' ? 1000 : Number(raw);\n if (!Number.isInteger(value) || value < 0) return setLive('转生点必须为非负整数', 'error');\n state.busyOperation = true; updateChrome();\n let result;\n try { result = await mod.setReincarnation(value); } catch (error) { result = { ok: false, reason: error.message }; }\n state.busyOperation = false; setLive(result.ok ? `已写入转生点 ${result.value}` : result.reason, result.ok ? 'success' : 'error'); rerenderActive();\n } else if (action === 'rollback' || action === 'restore-full') {\n const entry = mod.historyList()[Number(element.getAttribute('data-index'))];\n if (!entry) return setLive('记录已变化,请重新打开历史页面', 'error');\n const scope = getBackupScope(entry);\n const fullSnapshot = action === 'restore-full';\n if (!await confirmDialog(fullSnapshot ? '完整快照恢复' : '撤销本次操作', `${fullSnapshot ? '会覆盖记录范围内后来发生的变量修改。' : '只撤销这次实际改过的字段及相关正文指令,保留无关修改;同一字段后来又变化时由你选择。'}<br>记录范围:${esc(scope.scopeLabel)}。剧情文字不会恢复或删除;操作前会尝试保存恢复点。${scope.partial ? '<br>' + esc(scope.partialReason) : ''}${mod.hasDrafts() ? '<br>恢复成功后将重新读取数据,当前未提交输入会被替换。' : ''}<br>继续吗?`, fullSnapshot ? '确认完整恢复' : '确认撤销', true)) return;\n state.busyOperation = true; updateChrome();\n let result;\n try { result = await mod.rollback(entry, { fullSnapshot }); } catch (error) { result = { ok: false, reason: error.message }; }\n if (!mounted()) return;\n state.busyOperation = false;\n if (result.ok) invalidatePages(true);\n ensurePage(state.activeTab);\n const warning = (result.reloaded === false ? '回滚已保存,但重新加载失败:' + result.reloadError : '') || (result.partial ? scope.partialReason : '');\n reportOutcome(result, '已恢复记录包含的数据;剧情保持不变', warning);\n } else if (action === 'error-details') {\n await dialog('操作详情', `<pre>${esc(state.liveMessage)}</pre>`, null, false);\n }\n }\n\n async function doApply() {\n capturePage();\n const invalidDraft = Object.values(state.tabs).some((tab) => Object.values(tab.errors || {}).some(Boolean));\n if (invalidDraft || root.querySelector('[aria-invalid=\"true\"]')) return setLive('仍有未保存的错误输入,请先修正对应字段或原始 JSON,再应用', 'error');\n const validation = mod.validateEditor();\n if (!validation.ok) return setLive('字段校验失败:' + validation.errors[0], 'error');\n try { mod.buildChangeSet(); } catch (error) { return setLive(error.message, 'error'); }\n if (!mod.preview().length) return setLive('没有可应用的变更');\n state.busyOperation = true; setLive('正在准备修改预览…');\n const mode = mod.getApplyMode();\n let preflight;\n try { preflight = await mod.preflight({ mode }); } catch (error) { preflight = { ok: false, reason: error.message }; }\n if (!mounted()) return;\n state.busyOperation = false; updateChrome();\n if (!preflight.ok) return setLive(preflight.reason || '修改准备失败', 'error');\n if (!await showDiff(preflight.preview, true, mode)) return setLive('已取消应用');\n state.busyOperation = true; setLive('正在按角色卡规则处理并保存…');\n let result;\n try { result = await mod.apply({ mode }); } catch (error) { result = { ok: false, reason: error.message }; }\n if (!mounted()) return;\n state.busyOperation = false;\n if (result.ok) { invalidatePages(true); ensurePage(state.activeTab); }\n const warning = result.reloaded === false ? '写入已保存,但重新加载失败:' + result.reloadError : '';\n reportOutcome(result, result.historySaved ? '已应用、写后核验并保存回滚点' : '已应用并核验,但没有新增回滚点', warning);\n }\n\n function showDiff(items, confirm, mode = mod.getApplyMode()) {\n if (!items || !items.length) {\n return dialog(confirm ? `确认应用` : '当前变更', `<div class=\"${CLS.EMPTY}\">暂无变更</div>`, confirm ? '确认应用' : null, false);\n }\n\n const opNames = { replace: '修改', delta: '增减', insert: '新增', remove: '删除', move: '移动' };\n\n const groups = new Map();\n for (const item of items) {\n const info = classifyGroup(item.path);\n if (!groups.has(info.name)) groups.set(info.name, { info, list: [] });\n groups.get(info.name).list.push(item);\n }\n\n let bodyHtml = `<p class=\"md-note\">以下是准备提交的修改,尚未更新角色数据。确认后按角色卡规则处理一次;数值上限、升级及统计等联动以处理后的实际结果为准。</p><div class=\"md-diff-groups\">`;\n for (const [groupName, groupData] of groups) {\n bodyHtml += `<div class=\"md-diff-group\">` +\n `<div class=\"md-diff-group-header\">${icon(groupData.info.icon)} <span>${esc(groupName)}</span> <span class=\"md-badge ${groupData.info.badgeCls}\">${groupData.list.length}</span></div>`;\n\n for (const item of groupData.list) {\n const cnName = bodyMerge.pathCN(item.path);\n const isAux = item.path.startsWith('/date') || item.path.startsWith('/calendar') || item.path.startsWith('/chat');\n const destBadge = mode === 'body'\n ? (isAux ? `<span class=\"md-badge md-badge-direct\">更新日历或聊天变量</span>` : `<span class=\"md-badge md-badge-body\">更新数值并记录正文指令</span>`)\n : `<span class=\"md-badge md-badge-direct\">直接更新数值</span>`;\n\n bodyHtml += `<div class=\"md-diff-row\">` +\n `<div><b>${esc(opNames[item.op] || item.op)}</b> <span>${esc(cnName)}</span> ${destBadge}</div>` +\n `<small>修改前</small><pre>${esc(valueSummary(item.oldValue))}</pre>` +\n `<small>准备修改为</small><pre>${esc(valueSummary(item.newValue))}</pre>` +\n (item.actualValue !== undefined ? `<small>按角色卡规则处理后的结果</small><pre>${esc(valueSummary(item.actualValue))}</pre>` : '') +\n `<details class=\"md-log-details\"><summary>查看数据路径</summary><code>${esc(item.path)}</code></details>` +\n `</div>`;\n }\n bodyHtml += `</div>`;\n }\n bodyHtml += `</div>`;\n\n const title = confirm ? `确认${mode === 'body' ? '正文' : '直接'}模式应用 (${items.length} 处变更)` : `当前变更 (${items.length} 处)`;\n return dialog(title, bodyHtml, confirm ? '确认应用' : null, false);\n }\n\n function confirmDialog(title, text, label, danger) { return dialog(title, `<p>${text}</p>`, label, danger); }\n\n if (typeof mod.setInteractions === 'function') mod.setInteractions({\n confirmCapacity: () => confirmDialog('备份空间不足', '继续后将依次尝试:额外淘汰最旧记录、仅保存变量快照;若仍放不下,则本次修改没有持久回滚备份。只在实际需要时降级。取消不会继续修改,也不会删减已有备份。', '接受降级并继续', true),\n resolveConflicts: rows => dialog('这些字段有新的变化',\n '<p>请选择使用你准备的值,还是保留现在的值。数组按整项处理;取消会停止本次操作。</p>' +\n rows.map((row, i) => `<div class=\"md-diff-row\"><div><b>${esc(row.label)} · ${esc(bodyMerge.pathCN(row.path))}</b></div>` +\n [['原值', row.original], ['现在的值', row.current], ['你的修改', row.proposed]].map(([label, state]) =>\n `<small>${label}</small><pre>${esc(state.exists ? JSON.stringify(state.value, null, 2) : '(字段不存在)')}</pre>`).join('') +\n `<label for=\"md-conflict-${i}\">本项处理${row.atomic ? '(移动的来源与去向联动)' : ''}</label><select id=\"md-conflict-${i}\" data-conflict=\"${i}\"><option value=\"overwrite\">按我的修改覆盖</option><option value=\"keep\">保留当前值</option></select></div>`).join(''),\n '确认选择', true, overlay => {\n if (rows.some(row => row.atomic)) overlay.addEventListener('change', event => {\n if (event.target.hasAttribute('data-conflict')) overlay.querySelectorAll('[data-conflict]').forEach(select => { select.value = event.target.value; });\n });\n }, overlay => Array.from(overlay.querySelectorAll('[data-conflict]'), select => select.value)),\n });\n\n function dialog(title, bodyHtml, confirmLabel, danger, onMount, onConfirm) {\n return new Promise((resolve) => {\n const overlay = doc.createElement('div');\n const theme = uiStore.theme();\n overlay.className = 'md-dialog-overlay';\n overlay.setAttribute('data-theme-style', theme.family);\n overlay.setAttribute('data-theme', theme.mode);\n overlay.innerHTML = `<div class=\"${CLS.DIALOG}\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"md-dialog-title\"><h2 id=\"md-dialog-title\">${esc(title)}</h2><div class=\"md-dialog-body\">${bodyHtml}</div><div class=\"md-dialog-actions\"><button type=\"button\" class=\"${CLS.BUTTON}\" data-dialog=\"cancel\">${confirmLabel ? '取消' : '关闭'}</button>${confirmLabel ? `<button type=\"button\" class=\"${CLS.BUTTON} ${danger ? CLS.BTN_WARN : CLS.BTN_PRIMARY}\" data-dialog=\"confirm\">${esc(confirmLabel)}</button>` : ''}</div></div>`;\n doc.body.appendChild(overlay);\n if (typeof onMount === 'function') {\n try { onMount(overlay); } catch (e) { /* ignore */ }\n }\n const previous = doc.activeElement;\n const cancel = overlay.querySelector('[data-dialog=\"cancel\"]');\n const confirmButton = overlay.querySelector('[data-dialog=\"confirm\"]');\n const close = (answer) => {\n openDialogs.delete(close);\n if (overlay.parentNode) overlay.parentNode.removeChild(overlay);\n if (previous && previous.focus) previous.focus();\n resolve(answer);\n };\n openDialogs.add(close);\n cancel.addEventListener('click', () => close(false));\n if (confirmButton) {\n confirmButton.addEventListener('click', async () => {\n let customResult = true;\n if (typeof onConfirm === 'function') {\n try {\n const res = await onConfirm(overlay);\n if (res === false) return;\n if (res !== undefined && res !== true) customResult = res;\n } catch (e) {\n console.error(e);\n return;\n }\n }\n close(customResult);\n });\n }\n overlay.addEventListener('keydown', (event) => {\n if (event.key === 'Escape') close(false);\n if (event.key === 'Tab') {\n const focusable = Array.from(overlay.querySelectorAll('button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex=\"0\"]')).filter((node) => node.getClientRects().length);\n const index = focusable.indexOf(doc.activeElement);\n event.preventDefault(); focusable[(index + (event.shiftKey ? -1 : 1) + focusable.length) % focusable.length].focus();\n }\n });\n (confirmButton || cancel).focus();\n });\n }\n\n function hideToFloatingButton() {\n cancelHistoryView();\n capturePage();\n if (root.classList.contains('md-fullscreen')) toggleFullscreen();\n savePanelBounds();\n uiStore.setGeometry({ lastOpenState: 'fab' }, true);\n root.style.display = 'none'; fab.style.display = 'flex'; applyGeometry(fab);\n if (typeof mod.setPanelOpen === 'function') mod.setPanelOpen(false);\n if (fab.focus) fab.focus();\n }\n\n function showPanel() {\n fab.style.display = 'none'; root.style.display = 'flex'; uiStore.setGeometry({ lastOpenState: 'panel' }, true); applyGeometry(root); clampPanel();\n if (typeof mod.setPanelOpen === 'function') mod.setPanelOpen(true);\n if (!panelOpened) {\n panelOpened = true; state.busyOperation = true; updateChrome();\n (async () => {\n try {\n const loaded = await mod.load();\n state.liveMessage = loaded ? '变量已加载;修改仅暂存在内存中' : '当前回复没有可编辑的角色变量';\n state.liveKind = loaded ? 'success' : 'error';\n } catch (error) { state.liveMessage = '加载失败:' + error.message; state.liveKind = 'error'; }\n if (!mounted()) return;\n state.busyOperation = false; switchTab(state.activeTab);\n })();\n } else {\n const stale = typeof mod.isStale === 'function' && mod.isStale();\n const hasDrafts = typeof mod.hasDrafts === 'function' && mod.hasDrafts();\n // 隐藏期间只标记失效;重新打开时仅在没有草稿的情况下自动加载。\n if (!mod.getSnapshot() || (stale && !hasDrafts)) {\n state.busyOperation = true; updateChrome();\n (async () => {\n try {\n const loaded = await mod.load();\n state.liveMessage = loaded ? '变量已加载;修改仅暂存在内存中' : '当前回复没有可编辑的角色变量';\n state.liveKind = loaded ? 'success' : 'error';\n } catch (error) { state.liveMessage = '加载失败:' + error.message; state.liveKind = 'error'; }\n if (!mounted()) return;\n state.busyOperation = false; switchTab(state.activeTab);\n })();\n } else {\n switchTab(state.activeTab);\n }\n }\n }\n\n function clampPanel() {\n if (root.classList.contains('md-fullscreen') || !root.getBoundingClientRect) return;\n const rect = root.getBoundingClientRect();\n const next = setSharedPosition(rect.left, rect.top, false, root);\n root.style.left = next.x + 'px'; root.style.top = next.y + 'px';\n }\n\n function toggleFullscreen() {\n if (root.classList.contains('md-fullscreen')) {\n root.classList.remove('md-fullscreen');\n if (normalGeometry) uiStore.setGeometry(normalGeometry, false);\n applyGeometry(root); normalGeometry = null; clampPanel(); return;\n }\n savePanelBounds(); normalGeometry = uiStore.geometry(); root.classList.add('md-fullscreen');\n }\n\n function bindDrag() {\n let fabDrag = null;\n let suppressClick = false;\n fab.addEventListener('pointerdown', (event) => {\n if (event.pointerType === 'mouse' && event.button !== 0) return;\n const rect = fab.getBoundingClientRect();\n fabDrag = { id: event.pointerId, x: event.clientX, y: event.clientY, left: rect.left, top: rect.top, moved: false };\n suppressClick = false; if (fab.setPointerCapture) fab.setPointerCapture(event.pointerId); event.preventDefault();\n });\n fab.addEventListener('pointermove', (event) => {\n if (!fabDrag || event.pointerId !== fabDrag.id) return;\n const dx = event.clientX - fabDrag.x; const dy = event.clientY - fabDrag.y;\n if (!fabDrag.moved && Math.abs(dx) < 4 && Math.abs(dy) < 4) return;\n fabDrag.moved = true; setSharedPosition(fabDrag.left + dx, fabDrag.top + dy, false, fab);\n });\n const stopFab = (event) => {\n if (!fabDrag || event.pointerId !== fabDrag.id) return;\n suppressClick = fabDrag.moved;\n const rect = fab.getBoundingClientRect(); setSharedPosition(rect.left, rect.top, true, fab);\n fabDrag = null;\n };\n fab.addEventListener('pointerup', stopFab); fab.addEventListener('pointercancel', stopFab);\n fab.addEventListener('click', (event) => { if (suppressClick) { suppressClick = false; event.preventDefault(); return; } showPanel(); });\n\n const header = root.querySelector && root.querySelector('.' + CLS.HEADER);\n if (!header) return;\n let panelDrag = null;\n header.addEventListener('pointerdown', (event) => {\n if (root.classList.contains('md-fullscreen') || event.button !== 0 || event.target.closest('button,input,select,textarea')) return;\n const rect = root.getBoundingClientRect(); panelDrag = { id: event.pointerId, x: event.clientX, y: event.clientY, left: rect.left, top: rect.top };\n if (header.setPointerCapture) header.setPointerCapture(event.pointerId); event.preventDefault();\n });\n header.addEventListener('pointermove', (event) => {\n if (!panelDrag || event.pointerId !== panelDrag.id) return;\n setSharedPosition(panelDrag.left + event.clientX - panelDrag.x, panelDrag.top + event.clientY - panelDrag.y, false, root);\n });\n const stopPanel = (event) => {\n if (!panelDrag || event.pointerId !== panelDrag.id) return;\n const rect = root.getBoundingClientRect(); setSharedPosition(rect.left, rect.top, true, root); panelDrag = null;\n };\n header.addEventListener('pointerup', stopPanel); header.addEventListener('pointercancel', stopPanel);\n }\n\n function cssEscape(value) { return String(value).replace(/([\\\\\"'])/g, '\\\\$1'); }\n\n if (root.addEventListener) {\n root.addEventListener('input', (event) => {\n const element = event.target;\n if (element.id && !isViewControl(element)) {\n element.setAttribute('data-md-touched', 'true');\n pageState().drafts[draftKey(element.id)] = element.type === 'checkbox' ? !!element.checked : element.value;\n if (typeof mod.setDraftPending === 'function') mod.setDraftPending(true);\n // 键入时只检查当前控件,不扫描整棵变量树,也不把未完成的 JSON 写入工作副本。\n try {\n if (element.matches('[data-json-bind],[data-json-aux-root],[data-json-subset],[data-json-aux-subset],[data-chat-type=\"json\"]')) {\n const value = JSON.parse(element.value);\n if (!element.hasAttribute('data-chat-key') && (!value || typeof value !== 'object')) throw new Error('这里需要对象或数组');\n assertSafeTree(value);\n } else if (element.hasAttribute('data-type') || element.getAttribute('data-chat-type') === 'number') {\n parseControlValue(element.value, element.getAttribute('data-type') || 'number', element);\n }\n setFieldError(element.id, '');\n } catch (error) { setFieldError(element.id, '输入尚未有效:' + error.message); }\n updateChrome();\n }\n if (element.matches && element.matches('[data-option-filter],[data-row-filter],[data-json-filter]')) {\n pageState().filters[element.id] = element.value; applyFilter(element);\n }\n });\n root.addEventListener('change', async (event) => {\n const element = event.target;\n if (element.id && !isViewControl(element)) {\n element.setAttribute('data-md-touched', 'true');\n pageState().drafts[draftKey(element.id)] = element.type === 'checkbox' ? !!element.checked : element.value;\n if (typeof mod.setDraftPending === 'function') mod.setDraftPending(true);\n }\n if (element.hasAttribute('data-bind')) handleBoundChange(element, mod.getEditor(), element.getAttribute('data-bind'));\n else if (element.hasAttribute('data-aux-root')) {\n const rootName = element.getAttribute('data-aux-root');\n handleBoundChange(element, mod.getAuxEditor()[rootName], element.getAttribute('data-aux-path'), `/${rootName}${element.getAttribute('data-aux-path')}`);\n } else if (element.hasAttribute('data-structure-bind')) {\n const rootName = element.getAttribute('data-structure-aux-root');\n const target = rootName ? mod.getAuxEditor()[rootName] : mod.getEditor();\n const pathText = element.getAttribute('data-structure-bind');\n handleBoundChange(element, target, pathText, rootName ? `/${rootName}${pathText}` : pathText);\n } else if (element.hasAttribute('data-world-time-part')) updateWorldTime(element);\n else if (element.matches('[data-json-bind],[data-json-aux-root],[data-json-subset],[data-json-aux-subset]')) handleJsonChange(element);\n else if (element.hasAttribute('data-chat-key')) {\n const key = decodeKey(element.getAttribute('data-chat-key'));\n try {\n const type = element.getAttribute('data-chat-type');\n const value = type === 'json' ? JSON.parse(element.value) : type === 'number' ? parseControlValue(element.value, 'number', element) : type === 'boolean' ? element.value === 'true' : element.value;\n if (value && typeof value === 'object') assertSafeTree(value);\n mod.getAuxEditor().chatVariables[key] = value; setFieldError(element.id, ''); mark('templates', '/chatVariables/' + key);\n } catch (error) { setFieldError(element.id, '内容未保存:' + error.message); }\n } else if (element.getAttribute('data-act') === 'select-npc') {\n pageState('npc').selectedEntity = element.value; rerenderActive();\n } else if (element.getAttribute('data-act') === 'select-collection') {\n pageState().collectionSelections[element.getAttribute('data-collection-path')] = element.value; rerenderActive();\n } else if (element.getAttribute('data-act') === 'select-template') {\n selectTemplate(element.value);\n } else if (element.getAttribute('data-act') === 'mode') {\n mod.setApplyMode(element.value);\n uiStore.setSettings({ applyMode: element.value });\n setLive(`已切换为${element.value === 'body' ? '正文' : '直接'}模式`);\n } else if (element.getAttribute('data-act') === 'theme-family') {\n uiStore.setTheme({ family: element.value }); applyTheme(); setLive('主题样式已切换');\n } else if (element.getAttribute('data-act') === 'theme-mode') {\n uiStore.setTheme({ mode: element.value }); applyTheme(); setLive('主题配色已切换');\n } else if (element.getAttribute('data-act') === 'setting-change') {\n const key = element.getAttribute('data-setting-key');\n let val = element.value;\n if (element.type === 'number') val = Number(val);\n else if (element.value === 'true') val = true;\n else if (element.value === 'false') val = false;\n const runtimeResult = mod && typeof mod.setSettings === 'function' ? await mod.setSettings({ [key]: val }) : { ok: true };\n if (runtimeResult && runtimeResult.ok === false) {\n element.value = String(uiStore.settings()[key]);\n delete pageState().drafts[draftKey(element.id)]; element.removeAttribute('data-md-touched');\n return setLive('设置未更新:' + runtimeResult.reason, 'error');\n }\n uiStore.setSettings({ [key]: val });\n if (key === 'themeFamily' || key === 'themeMode') {\n uiStore.setTheme({ [key === 'themeFamily' ? 'family' : 'mode']: val });\n applyTheme();\n }\n setLive('设置已更新', 'info');\n updateChrome();\n } else if (element.getAttribute('data-act') === 'setting-change-adv') {\n const key = element.getAttribute('data-setting-key');\n const val = element.value === 'true';\n const desc = key === 'syncMvu'\n ? '关闭“应用后同步触发 MVU 刷新”可能导致酒馆助手变量无法立即同步。'\n : '关闭“应用后自动保存聊天”可能导致意外退出时未持久化。';\n (async () => {\n if (!val && !await confirmDialog('修改高级设置', `${desc}<br>确定关闭吗?`, '确定关闭', true)) {\n element.value = 'true';\n return;\n }\n uiStore.setSettings({ [key]: val });\n if (mod && typeof mod.setSettings === 'function') await mod.setSettings({ [key]: val });\n setLive(`高级设置已${val ? '开启' : '关闭'}`, 'info');\n updateChrome();\n })();\n }\n });\n root.addEventListener('click', (event) => {\n const tab = event.target.closest && event.target.closest('[data-tab]');\n if (tab) { switchTab(tab.getAttribute('data-tab')); return; }\n const jump = event.target.closest && event.target.closest('[data-json-jump]');\n if (jump) {\n const filter = jump.closest('.md-section').querySelector('[data-json-filter]');\n const area = doc.getElementById(filter.getAttribute('data-json-filter'));\n const token = JSON.stringify(decodeKey(jump.getAttribute('data-json-jump')));\n const position = area.value.indexOf(token);\n if (position >= 0) area.setSelectionRange(position, position + token.length);\n area.focus(); return;\n }\n const actionNode = event.target.closest && event.target.closest('[data-act]');\n if (!actionNode || ['mode', 'select-npc', 'select-collection', 'select-template', 'theme-family', 'theme-mode', 'setting-change', 'setting-change-adv'].includes(actionNode.getAttribute('data-act'))) return;\n if (actionNode.getAttribute('data-act') === 'format-json') {\n const area = doc.getElementById(actionNode.getAttribute('data-target'));\n try { area.value = JSON.stringify(JSON.parse(area.value), null, 2); setFieldError(area.id, ''); }\n catch (error) { setFieldError(area.id, 'JSON 格式错误:' + formatJsonError(area.value, error)); }\n return;\n }\n dispatch(actionNode.getAttribute('data-act'), actionNode);\n });\n root.addEventListener('keydown', (event) => {\n const button = event.target.closest && event.target.closest('[data-tab]');\n if (!button || !['ArrowLeft', 'ArrowRight'].includes(event.key)) return;\n const index = tabs.findIndex((tab) => tab.key === button.getAttribute('data-tab'));\n const next = tabs[(index + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length];\n event.preventDefault(); switchTab(next.key, true);\n });\n root.addEventListener('toggle', (event) => {\n const card = event.target;\n if (!card.matches || !card.matches('[data-card-id]')) return;\n const page = card.closest('[data-page]');\n const tabId = page && page.getAttribute('data-page');\n if (!page || pages.get(tabId) !== page || card._mdOpen === !!card.open) return;\n card._mdOpen = !!card.open;\n const cardId = card.getAttribute('data-card-id');\n uiStore.setCardOpen(tabId, cardId, !!card.open);\n const summary = card.querySelector('summary'); if (summary) summary.setAttribute('aria-expanded', card.open ? 'true' : 'false');\n if (card.open) { materialize(card, tabId); updateChrome(); }\n const deferred = card.open && card.querySelector('[data-deferred-json]');\n if (deferred && tabId === state.activeTab) { pageState(tabId).loadedEditors[deferred.getAttribute('data-deferred-json')] = true; rerenderActive(); }\n }, true);\n if (body && body.addEventListener) {\n body.addEventListener('scroll', () => {\n const top = body.scrollTop || 0;\n pageState().scrollTop = top;\n if (typeof uiStore.setScroll === 'function') uiStore.setScroll(state.activeTab, top);\n }, { passive: true });\n }\n if (tabBar && tabBar.addEventListener) {\n tabBar.addEventListener('scroll', () => {\n const left = tabBar.scrollLeft || 0;\n state.tabBarScrollLeft = left;\n if (typeof uiStore.setTabBarScroll === 'function') uiStore.setTabBarScroll(left);\n }, { passive: true });\n }\n root.addEventListener('pointerup', () => savePanelBounds());\n }\n\n bindDrag();\n const unsubscribe = mod.subscribe((event) => {\n if (!mounted()) return;\n if (['history', 'logs', 'injections'].includes(event.type)) {\n historyUi.stale = true;\n if (historyVisible() && !historyUi.loading) {\n if (event.result) historyUi.result = event.result;\n redrawHistory();\n }\n return;\n }\n if (event.type === 'imported') {\n importPreviewError = syncImportedDirty(mod, ds);\n if (typeof mod.setDraftPending === 'function') mod.setDraftPending(ds.total() > 0);\n invalidatePages(true);\n if (root.style.display !== 'none') ensurePage(state.activeTab);\n updateChrome();\n return;\n }\n if (event.type === 'loaded') {\n invalidatePages(true);\n if (typeof mod.setDraftPending === 'function') mod.setDraftPending(false);\n state.liveMessage = event.message || '已自动同步最新消息快照';\n state.liveKind = 'success';\n if (root.style.display !== 'none') ensurePage(state.activeTab);\n updateChrome();\n } else if (event.type === 'invalidated') {\n // 外部目标变化时由运行时标记草稿过期;保留用户输入,直到用户明确刷新或放弃。\n // 没有草稿时才清除页面缓存,避免旧控件值覆盖后续自动加载的数据。\n if (!event.preserved) invalidatePages(true);\n if (!event.preserved && typeof mod.setDraftPending === 'function') mod.setDraftPending(false);\n state.liveMessage = event.message;\n state.liveKind = event.kind || 'error';\n if (!event.preserved && root.style.display !== 'none') ensurePage(state.activeTab);\n updateChrome();\n }\n });\n mod.addCleanup(unsubscribe);\n mod.addCleanup(() => {\n disposed = true; cancelHistoryView();\n pages.clear(); pageViews.clear();\n for (const close of openDialogs) close(false);\n if (typeof mod.setInteractions === 'function') mod.setInteractions(null);\n });\n\n applyTheme(); applyGeometry(root); applyGeometry(fab); updateChrome();\n initialSettings.then((result) => {\n if (root.parentNode && result && result.ok === false) setLive('设置未完全应用:' + result.reason, 'error');\n }).catch((error) => { if (root.parentNode) setLive('设置读取失败:' + error.message, 'error'); });\n root.style.display = 'none'; fab.style.display = 'flex';\n if (P && P.addEventListener) {\n const onResize = () => {\n const element = fab.style.display !== 'none' ? fab : root;\n const rect = element.getBoundingClientRect(); setSharedPosition(rect.left, rect.top, true, element);\n };\n P.addEventListener('resize', onResize); mod.addCleanup(() => P.removeEventListener('resize', onResize));\n }\n}\nfunction unmount(P) {\n const doc = P && P.document;\n if (!doc) return;\n const root = doc.getElementById(CLS.ROOT);\n if (root && root.parentNode) root.parentNode.removeChild(root);\n const fab = doc.getElementById(CLS.FAB);\n if (fab && fab.parentNode) fab.parentNode.removeChild(fab);\n if (typeof doc.querySelectorAll === 'function') {\n doc.querySelectorAll('.md-dialog-overlay').forEach((node) => node.parentNode && node.parentNode.removeChild(node));\n }\n}\n\nmodule.exports = {\n mount,\n unmount,\n renderTabContent,\n parseControlValue,\n editableView,\n restoreReadonly,\n setPath,\n newNpcTemplate,\n BUSINESS_TABS,\n TOOL_TABS,\n CLS,\n clampFloatingPosition,\n loadPosition,\n parseWorldTime,\n formatJsonError,\n syncImportedDirty,\n};\n\n});\n\n__define('ui/player-help', function (module, exports, require) {\nconst tabs = Object.freeze({\n protagonist: '调整主角的属性、成长、物品和能力。只读数值由角色卡计算,不能直接改;其他改动先暂存,确认应用后才写入聊天。',\n npc: '选择一名 NPC,查看或修改其资料、好感度、物品与能力。新增和删除也会先暂存;经验与升级记录在“日历”页管理。',\n world: '调整故事中的时间地点、任务、新闻和事件进度。扩展区只展示当前聊天已有的额外内容,不会自动安装或启用 DLC。',\n calendar: '管理 NPC 的经验记录、成长设置、游戏统计和日历标签。这些记录与角色属性分开保存,但可以和其他修改一起应用。',\n start: '用于自定义开局页面的转生点调整。这里点击写入会立即改变开局页面,不走底栏应用,也不加入聊天回滚历史。',\n templates: '管理当前聊天供角色卡或其他脚本引用的变量。改名、删值可能影响引用它的内容;所有修改先暂存,不会输入后立即保存。',\n operations: '检查各页暂存的修改,可逐项撤销或跳回对应字段。直接写入只改当前数值;正文注入还会把改动写入当前回复的变量指令,两种方式都不会请求 AI 续写。',\n interpretation: '查看当前 AI 回复写了哪些变量指令,并可编辑或删除。改正文指令与改当前数值是两回事;“重新处理”会再读本条回复的全部指令,增减类操作可能重复计算,不是每次应用后的必点步骤。',\n history: '这里分别保存备份、操作日志和正文注入记录。默认撤销只还原本次改过的字段和相关指令,保留后来无关的修改;完整快照恢复会覆盖记录范围内后来的变量变化。空间不足而需要额外减少或省略备份时先询问,取消就不继续修改。日志不能代替备份,恢复只能作用于原聊天、回复及候选版本。',\n settings: '调整面板外观和应用习惯,设置保存在当前浏览器。首次使用和恢复默认均为经典主题、固定深色;已选择的其他配色会继续保留。',\n about: '命定之诗修改器用于调整当前聊天的角色与世界数据,不修改角色卡本体,也不替换世界书、正则或角色脚本。',\n});\n\nconst cards = Object.freeze({\n '基础与成长': '种族、等级、经验、属性点和金钱等主角数据。等级与升级条件仍受角色卡规则限制。',\n '基础资料': '当前 NPC 的身份、关系与出场状态。是否在场与是否签订命定契约是不同设置,请分别核对。',\n '五维属性': '调整角色的五项基础属性。它们可能参与角色卡的派生数值计算,应用后以角色卡显示的结果为准。',\n '资源': '当前值表示剩余生命、法力或体力;基础上限由角色卡计算,只读。想提高上限,请修改“额外”,不要把当前值当成上限。',\n '状态效果': '管理角色身上的增益、减益和特殊状态,包括层数、剩余时间及来源。删除条目表示移除这一状态。',\n '背包': '管理携带的物品、数量和物品说明。新增名称应避免与已有物品混淆;数量为零时可能由角色卡清理。',\n '装备': '管理装备的品质、部位和效果。这里修改的是装备记录,不会替你补写装备或卸下的剧情。',\n '技能': '管理技能的品质、消耗、效果和描述。修改消耗说明不等于立即扣除当前资源。',\n '资产': '管理房产、载具等资产及其位置、空间与结算说明。“内部资产”记录其中的物品或子资产。',\n '效果': '按效果条目分别添加、修改或删除,保留各项名称与内容;不确定格式时可查看原始 JSON。',\n '登神长阶': '管理登神进度、要素、权能、法则与神国资料。只读字段仍由角色卡维护,修改资料不代表自动完成剧情条件。',\n '神国': '记录神国名称、描述和当前已有的扩展资料,不会自动生成对应剧情。',\n '扮演资料': '为当前 NPC 记录性格、外貌、着装、心里话和背景;这些是角色资料,不是发送给 AI 的新消息。',\n '时间与地点': '调整故事时间和当前地点;不会改电脑时间,也不会自动模拟这段时间内的事件。',\n '事件': '设置事件是否开启、是否结束以及标题和阶段。信号用于记录已发生的触发事项,只能追加,不能删改已有信号。',\n '任务列表': '编辑任务状态、进展、目标和奖励说明。填写奖励内容不等于直接把奖励加入背包。',\n '新闻三栏': '分别管理快讯、留言板和茶会消息。这里保存的是消息内容,不会自动请求 AI 生成新闻。',\n '事件扩展字段': '展示当前事件已有的额外设置。含义由对应角色卡或扩展作者约定,不明用途时保留原值。',\n '活跃 DLC / 扩展变量': '只列出当前聊天的额外数据。这里修改已有字段,不会替你安装 DLC 或创建完整的扩展内容。',\n 'NPC 等级 / 经验 / 升级所需': '各列依次为等级、累计经验和升级所需经验。这里是日历保存的成长记录,注意与 NPC 页面当前等级核对。',\n '游戏统计': '查看或调整死亡次数、累计命运点数、破产次数和最大负债等记录;改统计不等于获得或失去对应资源。',\n 'NPC 成长规则': '设置 NPC 是否随主角升级、是否需契约才能获得经验等成长条件。具体生效时机由角色卡负责。',\n '日历标签': '日历可识别的事件标签名称,多个标签用逗号分开;添加标签不会自动创建事件。',\n '成长扩展记录': '日历成长记录之外的已有设置。保留不熟悉的字段,以免影响其他脚本。',\n '日历扩展记录': '日历界面的其他存储内容,只展示当前已有数据;建议仅调整明确知道用途的项目。',\n '自定义开局': '先打开角色卡的自定义开局页面;无法找到该页面时按钮不可用。转生点只影响本次开局选择。',\n '操作中心与变更暂存': '原始值是本次加载时的数据,暂存值是准备应用的数据。撤销只移除这一项待应用修改,不会回滚已经保存的内容。',\n '目标正文中的历史变量声明': '这些指令已写在当前回复中。编辑或删除会直接改这条回复的指令;回滚时恢复受影响的指令块,不替换无关剧情。',\n '当前待提交变量操作': '这是业务页尚未应用的改动,与回复中已经存在的指令分开显示。',\n '基础设置': '界面样式决定分层或简洁外观,配色方案决定深浅颜色;“跟随酒馆主题”不代表跟随角色卡状态栏。',\n '高级设置': '默认开启变量更新通知和聊天自动保存。关闭后可能需要手动刷新或保存,不会取消写入前的规则检查。',\n '原始 JSON 编辑': '适合熟悉数据格式的玩家。内容必须符合 JSON 格式;只读字段受保护,格式有误时不能应用。',\n '扩展字段': '当前条目中角色卡或扩展额外保存的内容;未识别不代表可以随意删除。',\n});\n\nconst about = Object.freeze({\n author: '@gunkoishi_50181',\n intro: tabs.about,\n support: '适用于命定之诗 V4.3.2,并保留 V4.3.1 存档的兼容读写;不支持 V4.2.1。需要在酒馆助手中运行。',\n});\n\nmodule.exports = { tabs, cards, about };\n\n});\n\n__define('ui/styles', function (module, exports, require) {\nconst CLS = Object.freeze({\n // ROOT 同时作为面板 id 使用;此前这里写成 md-mod,实际 DOM id 却是\n // md-modifier-root,导致整套 #id 样式完全没有命中,面板失去悬浮布局。\n ROOT: 'md-modifier-root',\n FAB: 'md-modifier-fab',\n PANEL: 'md-panel',\n HEADER: 'md-header',\n TABS: 'md-tabs',\n TAB: 'md-tab',\n TAB_ACTIVE: 'md-tab-active',\n BODY: 'md-body',\n CHANGES_BAR: 'md-changes-bar',\n GRID: 'md-grid',\n FIELD: 'md-field',\n NESTED_LIST: 'md-nested-list',\n TOOL: 'md-tool',\n BUTTON: 'md-btn',\n BTN_PRIMARY: 'md-btn-primary',\n BTN_WARN: 'md-btn-warn',\n DIALOG: 'md-dialog',\n LIVE: 'md-live',\n EMPTY: 'md-empty',\n});\nconst ARIA = Object.freeze({\n TAB_ROLE: 'tab',\n TABPANEL_ROLE: 'tabpanel',\n DIALOG_ROLE: 'dialog',\n});\n\nconst STYLE_ID = 'mingding-modifier-style';\nfunction injectStyle(doc) {\n if (!doc) return;\n const existing = doc.getElementById(STYLE_ID);\n if (existing) {\n // 热更新后沿用同一节点,但不能继续使用上一版控件样式。\n if (existing.textContent !== CSS_TEXT) existing.textContent = CSS_TEXT;\n return;\n }\n const st = doc.createElement('style');\n st.id = STYLE_ID;\n st.textContent = CSS_TEXT;\n (doc.head || doc.documentElement).appendChild(st);\n}\n\n// 集合导航和标签行与普通字段使用同一套控件外观,避免遗漏新增输入入口。\nconst CONTROL_SELECTORS = [\n '.md-field input', '.md-field select', '.md-field textarea',\n '.md-collection-tools input', '.md-collection-tools select', '.md-item-name-row input',\n '.md-collection-nav>input', '.md-collection-nav>select',\n '.md-text-list-row>input', '.md-text-list-add>input', '.md-changes-bar select',\n].map((selector) => '#' + CLS.ROOT + ' ' + selector)\n .concat(['input', 'select', 'textarea'].map(tag => '.md-dialog-overlay ' + tag));\n\nconst CSS_TEXT = [\n '#' + CLS.ROOT + '{position:fixed;--md-bg0:rgba(18,20,26,.97);--md-bg1:#1d212c;--md-bg2:#232a3d;--md-line:#2c3140;--md-line2:#3a3f4e;--md-tx:#d8dce6;--md-tx2:#9aa1b5;--md-accent:#ffd27a;--md-ok:#8fd3a8;--md-danger:#ff8fa8;right:16px;top:16px;z-index:99999;width:420px;max-height:85vh;display:flex;flex-direction:column;border-radius:12px;background:var(--md-bg0,rgba(18,20,26,.97));border:1px solid var(--md-line2,#3a3f4e);box-shadow:0 8px 40px rgba(0,0,0,.6);color:var(--md-tx,#d8dce6);font:13px/1.5 \"Segoe UI\",system-ui,\"Microsoft YaHei\",sans-serif;overflow:hidden;resize:both;min-width:280px;min-height:260px;max-width:calc(100vw - 16px);user-select:none}',\n '#' + CLS.ROOT + '[data-theme=\"dark\"]{--md-bg0:rgba(18,20,26,.97);--md-bg1:#1d212c;--md-bg2:#232a3d;--md-line:#2c3140;--md-line2:#3a3f4e;--md-tx:#d8dce6;--md-tx2:#9aa1b5;--md-accent:#ffd27a;--md-ok:#8fd3a8;--md-danger:#ff8fa8}',\n '#' + CLS.ROOT + '[data-theme=\"light\"]{--md-bg0:#f2f3f7;--md-bg1:#ffffff;--md-bg2:#e9ebf2;--md-line:#d4d8e2;--md-line2:#c4c9d6;--md-tx:#1e2230;--md-tx2:#6b7285;--md-accent:#b8860b;--md-ok:#2e7d32;--md-danger:#c62828}',\n '#' + CLS.ROOT + '[data-theme=\"auto\"]{--md-bg0:var(--SmartThemeBlurTintColor,rgba(18,20,26,.97));--md-bg1:var(--SmartThemeBotMesBlurTintColor,#1d212c);--md-bg2:var(--SmartThemeUserMesBlurTintColor,#232a3d);--md-tx:var(--SmartThemeBodyColor,#e8ebf4);--md-tx2:var(--SmartThemeQuoteColor,#9aa1b5);--md-line:var(--SmartThemeBorderColor,#3a3f4e);--md-line2:var(--SmartThemeBorderColor,#3a3f4e);--md-accent:var(--SmartThemeEmColor,#ffd27a);--md-ok:#8fd3a8;--md-danger:#ff8fa8}',\n '.md-dialog-overlay{position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:100000;display:flex;align-items:center;justify-content:center;padding:12px}',\n '.md-dialog-overlay[data-theme=\"dark\"]{--md-bg0:rgba(18,20,26,.97);--md-bg1:#1d212c;--md-bg2:#232a3d;--md-line:#2c3140;--md-line2:#3a3f4e;--md-tx:#d8dce6;--md-tx2:#9aa1b5;--md-accent:#ffd27a;--md-ok:#8fd3a8;--md-danger:#ff8fa8}',\n '.md-dialog-overlay[data-theme=\"light\"]{--md-bg0:#f2f3f7;--md-bg1:#ffffff;--md-bg2:#e9ebf2;--md-line:#d4d8e2;--md-line2:#c4c9d6;--md-tx:#1e2230;--md-tx2:#6b7285;--md-accent:#b8860b;--md-ok:#2e7d32;--md-danger:#c62828}',\n '.md-dialog-overlay[data-theme=\"auto\"]{--md-bg0:var(--SmartThemeBlurTintColor,rgba(18,20,26,.97));--md-bg1:var(--SmartThemeBotMesBlurTintColor,#1d212c);--md-bg2:var(--SmartThemeUserMesBlurTintColor,#232a3d);--md-tx:var(--SmartThemeBodyColor,#e8ebf4);--md-tx2:var(--SmartThemeQuoteColor,#9aa1b5);--md-line:var(--SmartThemeBorderColor,#3a3f4e);--md-accent:var(--SmartThemeEmColor,#ffd27a)}',\n '#' + CLS.ROOT + ' *{box-sizing:border-box}',\n '#' + CLS.ROOT + ' .md-icon{display:inline-block;flex:0 0 auto;fill:currentColor;vertical-align:-2px}',\n '#' + CLS.ROOT + ' .' + CLS.HEADER + '{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,var(--md-bg2,#1c2333),var(--md-bg2,#232a3d));border-bottom:1px solid var(--md-line2,#3a3f4e);flex:0 0 auto;cursor:move;touch-action:none}',\n '#' + CLS.ROOT + ' .' + CLS.HEADER + ' .md-title{font-weight:600;color:var(--md-accent,#ffd27a);letter-spacing:1px;font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:6px}',\n '#' + CLS.ROOT + ' .md-header-btn{border:0;background:transparent;color:var(--md-tx2,#9aa1b5);border-radius:6px;padding:4px 7px;min-width:30px;min-height:30px;cursor:pointer;font-size:15px;line-height:1;display:inline-flex;align-items:center;justify-content:center}',\n '#' + CLS.ROOT + ' .md-header-btn:hover,#' + CLS.ROOT + ' .md-header-btn:focus-visible{background:#3a4152;color:#fff;outline:none}',\n '#' + CLS.ROOT + ' .' + CLS.TABS + '{display:flex;gap:2px;padding:6px 8px 0;background:var(--md-bg0,#15171f);border-bottom:1px solid var(--md-line,#2c3140);flex:0 0 auto;overflow-x:auto;-webkit-overflow-scrolling:touch}',\n '#' + CLS.ROOT + ' .' + CLS.TAB + '{border:none;background:transparent;color:var(--md-tx2,#9aa1b5);padding:6px 9px;border-radius:8px 8px 0 0;cursor:pointer;font-size:12px;white-space:nowrap;min-height:34px;display:flex;align-items:center;gap:5px;flex-shrink:0}',\n '#' + CLS.ROOT + ' .' + CLS.TAB + ':hover{background:var(--md-bg2,#232838);color:var(--md-tx,#dfe3ee)}',\n '#' + CLS.ROOT + ' .' + CLS.TAB + '.' + CLS.TAB_ACTIVE + '{background:var(--md-bg2,#232a3d);color:var(--md-accent,#ffd27a);box-shadow:inset 0 2px 0 var(--md-accent,#ffd27a)}',\n '#' + CLS.ROOT + ' .' + CLS.BODY + '{flex:1;overflow-y:auto;padding:8px;background:var(--md-bg0,#181b24)}',\n '#' + CLS.ROOT + ' .md-tab-page[hidden]{display:none}',\n '#' + CLS.ROOT + ' .' + CLS.GRID + '{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:6px}',\n '#' + CLS.ROOT + ' .' + CLS.FIELD + '{display:flex;align-items:center;gap:6px;margin-bottom:6px;min-width:0}',\n '#' + CLS.ROOT + ' .' + CLS.FIELD + ' label{width:86px;flex:0 0 86px;color:var(--md-tx2,#9aa1b5);font-size:12px;text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',\n '#' + CLS.ROOT + ' .md-control{flex:1;min-width:0}',\n CONTROL_SELECTORS.join(',') + '{flex:1;min-width:0;min-height:var(--md-control-height,32px);background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#e8ebf4);border-radius:6px;padding:4px 8px;font-family:inherit;font-size:12px;outline:none}',\n '#' + CLS.ROOT + ' .' + CLS.FIELD + ' input,' + '#' + CLS.ROOT + ' .' + CLS.FIELD + ' select{width:100%}',\n CONTROL_SELECTORS.map((selector) => selector + ':focus').join(',') + '{border-color:var(--md-accent,#ffd27a)}',\n '#' + CLS.ROOT + ' textarea{resize:vertical}',\n '#' + CLS.ROOT + ' input[readonly]{opacity:.72;background:var(--md-bg2,#232838)}',\n '#' + CLS.ROOT + ' .' + CLS.CHANGES_BAR + '{display:flex;align-items:center;gap:8px;padding:8px 10px;border-top:1px solid var(--md-line,#2c3140);background:var(--md-bg1,#1d212c);flex:0 0 auto;flex-wrap:wrap}',\n '#' + CLS.ROOT + ' .' + CLS.BUTTON + ',.md-dialog-overlay .' + CLS.BUTTON + '{border:1px solid #3a4152;background:var(--md-bg2,#232838);color:var(--md-tx,#c9d1e5);border-radius:6px;padding:5px 12px;cursor:pointer;font-size:12px;min-height:32px;display:inline-flex;align-items:center;justify-content:center;gap:5px;flex-shrink:0;white-space:nowrap;touch-action:manipulation;transition:border-color .15s,color .15s,background .15s}',\n '#' + CLS.ROOT + ' .' + CLS.BUTTON + ':hover,.md-dialog-overlay .' + CLS.BUTTON + ':hover{border-color:var(--md-accent,#ffd27a);color:var(--md-accent,#ffd27a)}',\n '#' + CLS.ROOT + ' .' + CLS.BUTTON + ':disabled,.md-dialog-overlay .' + CLS.BUTTON + ':disabled{opacity:.5;cursor:not-allowed}',\n '#' + CLS.ROOT + ' .' + CLS.BTN_PRIMARY + ',.md-dialog-overlay .' + CLS.BTN_PRIMARY + '{background:linear-gradient(90deg,#5b6cff,#7d5bff);border:none;color:#fff;font-weight:600;box-shadow:0 2px 8px rgba(91,108,255,.35)}',\n '#' + CLS.ROOT + ' .' + CLS.BTN_PRIMARY + ':hover,.md-dialog-overlay .' + CLS.BTN_PRIMARY + ':hover{background:linear-gradient(90deg,#6c7cff,#8e6eff);color:#fff;box-shadow:0 4px 14px rgba(91,108,255,.5)}',\n '#' + CLS.ROOT + ' .' + CLS.BTN_WARN + ',.md-dialog-overlay .' + CLS.BTN_WARN + '{background:#3a1f2b;border-color:#7a3b52;color:#ff8fa8}',\n '#' + CLS.ROOT + ' .' + CLS.BTN_WARN + ':hover,.md-dialog-overlay .' + CLS.BTN_WARN + ':hover{background:#4a2536;border-color:#9a4865;color:#ffb0c2}',\n '#' + CLS.ROOT + ' .' + CLS.LIVE + '{font-size:11px;color:var(--md-tx2,#9aa1b5);padding:4px 10px;flex:0 0 auto;min-width:0;max-height:110px;overflow:auto;overflow-wrap:anywhere}',\n '#' + CLS.ROOT + ' .' + CLS.LIVE + '[data-kind=\"success\"]{color:var(--md-ok,#8fd3a8)}',\n '#' + CLS.ROOT + ' .' + CLS.LIVE + '[data-kind=\"error\"]{color:var(--md-danger,#ff8fa8)}',\n '#' + CLS.ROOT + ' .' + CLS.LIVE + '[data-kind=\"warning\"]{color:var(--md-accent,#ffd27a)}',\n '#' + CLS.ROOT + ' .' + CLS.EMPTY + '{color:var(--md-tx2,#6b7391);font-size:12px;padding:12px;text-align:center}',\n '#' + CLS.ROOT + ' .' + CLS.NESTED_LIST + '{margin:0;padding-left:16px}',\n '#' + CLS.ROOT + ' .md-section{border:1px solid var(--md-line,#2c3140);border-radius:8px;margin-bottom:8px;background:var(--md-bg1,#1d212c);overflow:hidden}',\n '#' + CLS.ROOT + ' .md-section>summary{cursor:pointer;padding:8px 10px;background:var(--md-bg2,#212636);color:var(--md-tx,#b8c2e0);font-weight:600;font-size:12px;min-height:36px;display:flex;align-items:center;gap:6px;list-style:none;user-select:none}',\n '#' + CLS.ROOT + ' .md-section>summary::-webkit-details-marker{display:none}',\n '#' + CLS.ROOT + ' .md-section>summary:hover{color:var(--md-accent,#ffd27a)}',\n '#' + CLS.ROOT + ' .md-section>summary:after{content:\"▸\";margin-left:auto;color:var(--md-tx2,#9aa1b5);transition:transform .15s}',\n '#' + CLS.ROOT + ' .md-section[open]>summary:after{transform:rotate(90deg)}',\n '#' + CLS.ROOT + ' .md-section>summary .md-icon{color:#7d8bb8}',\n '#' + CLS.ROOT + ' .md-section-body{padding:8px 10px;background:var(--md-bg1,#1d212c);overflow-x:auto}',\n '#' + CLS.ROOT + ' .md-count{margin-left:auto;color:var(--md-tx2,#9aa1b5);font-weight:400;margin-right:6px}',\n '#' + CLS.ROOT + ' .md-json{display:block;width:100%;min-height:150px;background:var(--md-bg0,#0e1017);color:var(--md-ok,#8fd3a8);border:1px solid var(--md-line,#333a4d);border-radius:6px;padding:8px;font:11px/1.5 Consolas,monospace}',\n '#' + CLS.ROOT + ' .md-json-filter{display:block;width:100%;margin:5px 0;background:var(--md-bg0,#181b24);color:var(--md-tx,#e8ebf4);border:1px solid var(--md-line,#333a4d);border-radius:6px;padding:5px 8px;font-size:12px}',\n '#' + CLS.ROOT + ' .md-json-index{display:flex;gap:4px;flex-wrap:wrap;max-height:100px;overflow:auto;padding:4px 0}',\n '#' + CLS.ROOT + ' .md-json-index[hidden]{display:none}#' + CLS.ROOT + ' .md-json-index small{color:var(--md-tx2,#9aa1b5)}',\n '#' + CLS.ROOT + ' .md-calendar-row[hidden]{display:none}',\n '#' + CLS.ROOT + ' .md-json-tools,#' + CLS.ROOT + ' .md-inline-actions,#' + CLS.ROOT + ' .md-dialog-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:6px 0}',\n '#' + CLS.ROOT + ' .md-editor-mode{display:flex;justify-content:flex-end;gap:6px;margin:0 0 8px}',\n '#' + CLS.ROOT + ' .md-collection-tools,#' + CLS.ROOT + ' .md-item-name-row{display:flex;align-items:center;gap:6px;margin:6px 0;flex-wrap:wrap}',\n '#' + CLS.ROOT + ' .md-collection-tools input,#' + CLS.ROOT + ' .md-item-name-row input{min-width:90px}',\n '#' + CLS.ROOT + '{container-type:inline-size}',\n '#' + CLS.ROOT + ' .md-collection-workspace{display:grid;grid-template-columns:minmax(0,1fr);gap:12px}',\n '#' + CLS.ROOT + ' .md-collection-nav{display:flex;flex-direction:column;gap:6px;min-width:0}',\n '#' + CLS.ROOT + ' .md-collection-nav label{font-size:12px;color:var(--md-tx2)}',\n '#' + CLS.ROOT + ' .md-collection-nav>input,#' + CLS.ROOT + ' .md-collection-nav>select{width:100%;flex:none;height:var(--md-control-height,32px)}',\n '#' + CLS.ROOT + ' .md-template-nav{padding:8px 10px;margin:8px 0 10px;border:1px solid var(--md-line);border-radius:6px;background:var(--md-bg1)}',\n '#' + CLS.ROOT + ' .md-template-nav label{font-weight:600;color:var(--md-tx)}',\n '#' + CLS.ROOT + ' [data-template-pane][hidden]{display:none}',\n '#' + CLS.ROOT + ' .md-template-complex{border-top:1px solid var(--md-line);padding-top:8px;margin-top:8px}',\n '#' + CLS.ROOT + ' .md-template-complex>.md-item-name-row{margin-top:0}',\n '#' + CLS.ROOT + ' .md-collection-detail{min-width:0}#' + CLS.ROOT + ' .md-collection-detail>h3{font-size:14px;color:var(--md-accent);margin:0 0 8px;overflow-wrap:anywhere}',\n '#' + CLS.ROOT + ' .md-collection-detail textarea{width:100%;min-height:62px}',\n '#' + CLS.ROOT + ' .md-subgroup{border:0;border-top:1px solid var(--md-line);margin:10px 0;padding:6px 0 0;background:transparent}',\n '#' + CLS.ROOT + ' .md-subgroup>summary{font-size:12px;font-weight:600;color:var(--md-tx2);cursor:pointer;padding:5px 0;overflow-wrap:anywhere}',\n '#' + CLS.ROOT + ' .md-subgroup-body{padding:3px 0 2px 10px;border-left:2px solid var(--md-line2);margin-left:2px}',\n '#' + CLS.ROOT + ' .md-field-group{margin:10px 0;padding:8px 0;border-top:1px solid var(--md-line)}',\n '#' + CLS.ROOT + ' .md-field-group h4{margin:0 0 6px;color:var(--md-accent);font-size:12px;font-weight:600}',\n '#' + CLS.ROOT + ' .md-basic-fields{border-top:0;margin-top:0;padding-top:0}',\n '#' + CLS.ROOT + ' .md-effect-row{padding:6px 0;border-bottom:1px solid var(--md-line)}#' + CLS.ROOT + ' .md-effect-row:last-child{border-bottom:0}',\n '#' + CLS.ROOT + ' .md-tree-field{padding:4px 0}#' + CLS.ROOT + ' .md-text-list-row,#' + CLS.ROOT + ' .md-text-list-add{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px;align-items:start;margin:5px 0}',\n '#' + CLS.ROOT + ' .md-text-list-row>input,#' + CLS.ROOT + ' .md-text-list-add>input{width:100%;height:var(--md-control-height,32px);grid-column:1;grid-row:1}',\n '#' + CLS.ROOT + ' .md-text-list-row>.md-list-actions,#' + CLS.ROOT + ' .md-text-list-add>button{grid-column:2;grid-row:1}',\n '#' + CLS.ROOT + ' .md-text-list-row>.md-field-error,#' + CLS.ROOT + ' .md-text-list-add>.md-field-error{grid-column:1/-1}',\n '#' + CLS.ROOT + ' .md-list-actions{display:flex;align-items:center}#' + CLS.ROOT + ' .md-list-actions button{width:var(--md-control-height,32px);height:var(--md-control-height,32px);padding:5px}',\n '#' + CLS.ROOT + ' .md-json-tools span{flex:1;color:var(--md-tx2,#9aa1b5);font-size:11px}',\n '#' + CLS.ROOT + ' .md-inline-actions input{flex:1;min-width:120px;background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#e8ebf4);border-radius:6px;padding:5px 8px}',\n '#' + CLS.ROOT + ' .md-field-error{min-height:0;color:var(--md-danger,#ff8fa8);font-size:11px}',\n '#' + CLS.ROOT + ' .md-field-error:not(:empty){margin-top:3px}',\n '#' + CLS.ROOT + ' .md-hint,#' + CLS.ROOT + ' .md-note{color:var(--md-tx2,#9aa1b5);font-size:11px;line-height:1.6}',\n '#' + CLS.ROOT + ' .md-readonly-list{padding:6px;border-radius:6px;background:var(--md-bg2,#232838);font-size:11px}',\n '#' + CLS.ROOT + ' .md-calendar-row{display:grid;grid-template-columns:minmax(90px,1fr) 64px 82px 82px;gap:5px;align-items:center;margin-bottom:5px}',\n '#' + CLS.ROOT + ' .md-calendar-row input{min-width:0;width:100%;background:var(--md-bg0,#181b24);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#e8ebf4);border-radius:5px;padding:4px}',\n '#' + CLS.ROOT + ' .md-template-row{display:grid;grid-template-columns:110px 1fr auto;gap:6px;align-items:start;margin:8px 0}',\n '#' + CLS.ROOT + ' .md-template-control textarea{width:100%;min-height:70px;background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#e8ebf4);border-radius:6px;padding:6px}',\n '#' + CLS.ROOT + ' .md-template-control input,#' + CLS.ROOT + ' .md-template-control select{width:100%;background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#333a4d);color:var(--md-tx,#e8ebf4);border-radius:6px;padding:6px}',\n '#' + CLS.ROOT + ' .md-template-row .md-field-error{grid-column:2/4}',\n '#' + CLS.ROOT + ' .md-history-row{display:flex;gap:10px;align-items:flex-start;justify-content:space-between;border-bottom:1px solid var(--md-line,#333a4d);padding:10px 0;flex-wrap:nowrap}',\n '#' + CLS.ROOT + ' .md-history-row>div{flex:1;min-width:0;line-height:1.6}',\n '#' + CLS.ROOT + ' .md-history-row b{font-size:13px;color:var(--md-tx,#e8ebf4);margin-right:6px}',\n '#' + CLS.ROOT + ' .md-history-row small{display:block;color:var(--md-tx2,#9aa1b5);font-size:11px;margin-top:3px;word-break:break-all}',\n '#' + CLS.ROOT + ' .md-history-row button{flex-shrink:0;margin-top:2px}',\n '#' + CLS.ROOT + ' .md-operation-row{border-bottom:1px solid var(--md-line,#333a4d);padding:6px 0}#' + CLS.ROOT + ' .md-operation-row:last-child{border-bottom:0}',\n '#' + CLS.ROOT + ' .md-operation-row code{color:var(--md-accent,#ffd27a);word-break:break-all}#' + CLS.ROOT + ' .md-operation-row small{display:block;color:var(--md-tx2,#9aa1b5)}',\n '#' + CLS.ROOT + ' .md-operation-row pre{white-space:pre-wrap;word-break:break-word;margin:3px 0 0;padding:5px;background:var(--md-bg0,#181b24);border-radius:5px;font-size:11px}',\n '#' + CLS.ROOT + ' .md-warning{color:var(--md-danger,#ff8fa8);padding:8px;border:1px solid var(--md-danger,#ff8fa8);border-radius:6px}',\n '.md-dialog-overlay .' + CLS.DIALOG + '{background:var(--md-bg1,#1d212c);color:var(--md-tx,#e8ebf4);border:1px solid var(--md-accent,#ffd27a);border-radius:10px;width:560px;max-width:96vw;max-height:82vh;overflow:auto;padding:16px;box-shadow:0 8px 40px rgba(0,0,0,.7);font:13px/1.5 \"Segoe UI\",system-ui,\"Microsoft YaHei\",sans-serif}',\n '.md-dialog-overlay h2,.md-dialog-overlay h3{color:var(--md-accent,#ffd27a);margin:0 0 10px;font-size:14px;font-weight:600}',\n '.md-dialog-overlay .md-dialog-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:12px}',\n '.md-dialog-overlay .md-diff-row{border-top:1px solid var(--md-line,#333a4d);padding:8px 0}.md-dialog-overlay .md-diff-row:first-child{border-top:0}',\n '.md-dialog-overlay .md-diff-row>label{display:block;margin:8px 0 4px}.md-dialog-overlay .md-diff-row>select{width:100%}',\n '.md-dialog-overlay pre{white-space:pre-wrap;word-break:break-word;background:var(--md-bg0,#181b24);padding:5px;border-radius:5px;margin:2px 0 6px;font-size:11px}',\n '#' + CLS.ROOT + '.md-fullscreen{left:8px!important;right:8px!important;top:8px!important;width:auto!important;height:calc(100vh - 16px)!important;max-width:none;max-height:none;resize:none}',\n '#' + CLS.ROOT + '.md-minimized{width:230px!important;min-height:0;height:auto!important;resize:none}#' + CLS.ROOT + '.md-minimized nav,#' + CLS.ROOT + '.md-minimized main,#' + CLS.ROOT + '.md-minimized .' + CLS.CHANGES_BAR + ',#' + CLS.ROOT + '.md-minimized .' + CLS.LIVE + '{display:none}',\n '#' + CLS.ROOT + '[data-theme-style=\"classic\"]{border-color:var(--md-line2,#3a3f4e);box-shadow:0 8px 40px rgba(0,0,0,.6)}',\n '#' + CLS.ROOT + '[data-theme-style=\"classic\"] .' + CLS.HEADER + '{background:linear-gradient(90deg,var(--md-bg2,#1c2333),var(--md-bg2,#232a3d))}',\n '#' + CLS.ROOT + '[data-theme-style=\"classic\"] .md-section{overflow:hidden}',\n '#' + CLS.ROOT + '[data-theme-style=\"classic\"] .md-section>summary{background:var(--md-bg2,#212636);color:var(--md-tx,#b8c2e0)}',\n '#' + CLS.ROOT + '[data-theme-style=\"classic\"] .md-section>summary .md-icon{color:#7d8bb8}',\n // 简洁主题只调整视觉层级,不改变字段顺序、控件尺寸或窗口布局。\n '#' + CLS.ROOT + '[data-theme-style=\"simple\"]{border-radius:6px;box-shadow:0 3px 12px rgba(0,0,0,.25)}',\n '#' + CLS.ROOT + '[data-theme-style=\"simple\"] .' + CLS.HEADER + '{background:var(--md-bg0);border-bottom:1px solid var(--md-line)}',\n '#' + CLS.ROOT + '[data-theme-style=\"simple\"] .md-section{background:transparent;border:0;border-bottom:1px solid var(--md-line);border-radius:0}',\n '#' + CLS.ROOT + '[data-theme-style=\"simple\"] .md-section>summary,#' + CLS.ROOT + '[data-theme-style=\"simple\"] .md-section-body{background:transparent}',\n '#' + CLS.ROOT + '[data-theme-style=\"simple\"] .md-section>summary .md-icon{color:var(--md-tx2)}',\n '#' + CLS.ROOT + '[data-theme-style=\"simple\"] .' + CLS.TAB + '{border-radius:0}',\n '#' + CLS.ROOT + '[data-theme-style=\"simple\"] .' + CLS.TAB_ACTIVE + '{background:transparent;box-shadow:inset 0 -2px 0 var(--md-accent)}',\n '#' + CLS.ROOT + '[data-theme-style=\"simple\"] .' + CLS.BUTTON + ',.md-dialog-overlay[data-theme-style=\"simple\"] .' + CLS.BUTTON + '{border-radius:4px;box-shadow:none}',\n '#' + CLS.ROOT + '[data-theme-style=\"simple\"] .' + CLS.BTN_PRIMARY + ',.md-dialog-overlay[data-theme-style=\"simple\"] .' + CLS.BTN_PRIMARY + '{background:var(--md-tx);color:var(--md-bg1);border:1px solid var(--md-tx);box-shadow:none}',\n '#' + CLS.ROOT + '[data-theme-style=\"simple\"] .md-help-box{background:transparent;border:0;border-left:2px solid var(--md-line2);border-radius:0}',\n '.md-dialog-overlay[data-theme-style=\"simple\"] .' + CLS.DIALOG + '{border-color:var(--md-line2,var(--md-line));border-radius:6px;box-shadow:0 3px 12px rgba(0,0,0,.25)}',\n '#' + CLS.ROOT + ' .md-help-box{background:var(--md-bg1,#10131c);border:1px solid #3a4152;border-left:3px solid var(--md-accent,#ffd27a);border-radius:6px;padding:8px 10px;margin-bottom:8px;font-size:12px;line-height:1.8;color:var(--md-tx,#c6cede)}',\n '#' + CLS.ROOT + ' .md-help-box b{color:var(--md-accent,#ffd27a)}',\n '#' + CLS.ROOT + ' .md-badge,.md-dialog-overlay .md-badge{display:inline-flex;align-items:center;gap:2px;padding:2px 7px;border-radius:4px;font-size:11px;font-weight:600;line-height:1.2;vertical-align:middle;flex-shrink:0}',\n '#' + CLS.ROOT + ' .md-badge-replace,.md-dialog-overlay .md-badge-replace{background:#1a2744;color:#79a8ff;border:1px solid #2d4575}',\n '#' + CLS.ROOT + ' .md-badge-insert,.md-dialog-overlay .md-badge-insert{background:#132a1e;color:#52c480;border:1px solid #225a3a}',\n '#' + CLS.ROOT + ' .md-badge-remove,.md-dialog-overlay .md-badge-remove{background:#381a24;color:#ff7597;border:1px solid #6f2639}',\n '#' + CLS.ROOT + ' .md-badge-delta,.md-dialog-overlay .md-badge-delta{background:#332712;color:#ffbe4d;border:1px solid #684f22}',\n '#' + CLS.ROOT + ' .md-badge-protagonist,.md-dialog-overlay .md-badge-protagonist{background:#1a233a;color:#79a8ff;border:1px solid #334c7a}',\n '#' + CLS.ROOT + ' .md-badge-npc,.md-dialog-overlay .md-badge-npc{background:#2a1c38;color:#d88cff;border:1px solid #5a337a}',\n '#' + CLS.ROOT + ' .md-badge-world,.md-dialog-overlay .md-badge-world{background:#152828;color:#5de0d3;border:1px solid #235c56}',\n '#' + CLS.ROOT + ' .md-badge-calendar,.md-dialog-overlay .md-badge-calendar{background:#2e2617;color:#ffbe4d;border:1px solid #665022}',\n '#' + CLS.ROOT + ' .md-badge-templates,.md-dialog-overlay .md-badge-templates{background:#2d1d24;color:#ff7597;border:1px solid #662a3c}',\n '#' + CLS.ROOT + ' .md-badge-direct,.md-dialog-overlay .md-badge-direct{background:#1f2b1c;color:#7ee081;border:1px solid #355e30}',\n '#' + CLS.ROOT + ' .md-badge-body,.md-dialog-overlay .md-badge-body{background:#2b2216;color:#ffa94d;border:1px solid #5c4120}',\n '#' + CLS.ROOT + ' .md-badge-warn,.md-dialog-overlay .md-badge-warn{background:#3a1818;color:#ff6b6b;border:1px solid #6e2020}',\n '#' + CLS.ROOT + ' .md-operations-toolbar,#' + CLS.ROOT + ' .md-interpretation-toolbar{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:6px;margin-bottom:10px}',\n '#' + CLS.ROOT + ' .md-operation-card{background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#333a4d);border-radius:8px;padding:8px 10px;margin-bottom:8px}',\n '#' + CLS.ROOT + ' .md-operation-header{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:6px}',\n '#' + CLS.ROOT + ' .md-op-title{display:flex;gap:6px;align-items:center;flex-wrap:wrap;flex:1;min-width:0;line-height:1.5}',\n '#' + CLS.ROOT + ' .md-op-title b{color:var(--md-accent,#ffd27a);font-size:13px}',\n '#' + CLS.ROOT + ' .md-op-actions{display:flex;gap:4px;flex-shrink:0}',\n '#' + CLS.ROOT + ' .md-op-path{font-size:11px;color:var(--md-tx2,#9aa1b5);margin-bottom:4px;word-break:break-all}',\n '#' + CLS.ROOT + ' .md-op-diff-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:6px;margin-top:6px}',\n '#' + CLS.ROOT + ' .md-diff-box{background:var(--md-bg0,#181b24);border:1px solid var(--md-line,#333a4d);border-radius:4px;padding:4px 6px;font-size:11px}',\n '#' + CLS.ROOT + ' .md-diff-label{font-size:10px;color:var(--md-tx2,#9aa1b5);display:block;margin-bottom:2px}',\n '#' + CLS.ROOT + ' .md-diff-box pre{margin:0;max-height:80px;overflow-y:auto;white-space:pre-wrap;word-break:break-word}',\n '#' + CLS.ROOT + ' .md-history-section{border:0;border-top:1px solid var(--md-line,#333a4d);border-radius:0;padding:12px 0;margin-bottom:8px;background:transparent}',\n '#' + CLS.ROOT + ' .md-history-status{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:8px 0;margin-bottom:10px;border-bottom:1px solid var(--md-line);font-size:12px;overflow-wrap:anywhere}',\n '#' + CLS.ROOT + ' .md-history-status>p,#' + CLS.ROOT + ' .md-history-status>details{flex-basis:100%;margin:0}#' + CLS.ROOT + ' [data-history-state=\"error\"] [data-history-message]{color:var(--md-danger)}',\n '#' + CLS.ROOT + ' .md-log-details{margin:5px 0;min-width:0}#' + CLS.ROOT + ' .md-log-details summary{cursor:pointer;color:var(--md-tx2);font-size:12px}',\n '#' + CLS.ROOT + ' .md-log-details pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:240px;overflow:auto;font-size:11px;line-height:1.5}',\n '#' + CLS.ROOT + ' .md-history-section-title{font-weight:600;color:var(--md-accent,#ffd27a);margin-bottom:6px;display:flex;align-items:center;gap:6px;font-size:13px}',\n '#' + CLS.ROOT + ' .md-log-list,#' + CLS.ROOT + ' .md-injection-list{max-height:180px;overflow-y:auto;background:var(--md-bg0,#181b24);border:1px solid var(--md-line,#333a4d);border-radius:6px;padding:6px;font-size:11px}',\n '#' + CLS.ROOT + ' .md-log-item,#' + CLS.ROOT + ' .md-injection-item{padding:4px 0;border-bottom:1px dashed var(--md-line,#2c3140);display:flex;gap:6px;align-items:center;flex-wrap:wrap}',\n '#' + CLS.ROOT + ' .md-log-item:last-child,#' + CLS.ROOT + ' .md-injection-item:last-child{border-bottom:none}',\n '#' + CLS.ROOT + ' .md-log-time{color:var(--md-tx2,#9aa1b5);font-size:10px;min-width:55px}',\n '#' + CLS.ROOT + ' .md-log-summary{flex:1;word-break:break-word;min-width:140px}',\n '#' + CLS.ROOT + ' .md-interpret-analysis{background:var(--md-bg1,#12141c);border:1px solid var(--md-accent,#ffd27a);border-radius:8px;padding:8px 10px;margin-bottom:10px}',\n '#' + CLS.ROOT + ' .md-interpret-analysis-title{font-weight:600;color:var(--md-accent,#ffd27a);margin-bottom:4px;display:flex;align-items:center;gap:6px;font-size:12px}',\n '#' + CLS.ROOT + ' .md-interpret-analysis-content{font-size:12px;line-height:1.5;color:var(--md-tx,#cdd6ea);white-space:pre-wrap}',\n '#' + CLS.ROOT + ' .md-interpret-op{background:var(--md-bg1,#12141c);border:1px solid var(--md-line,#333a4d);border-radius:6px;padding:8px 10px;margin-bottom:8px;display:flex;flex-direction:column;gap:6px}',\n '#' + CLS.ROOT + ' .md-interpret-op[data-op=\"replace\"]{border-left:3px solid #79a8ff}',\n '#' + CLS.ROOT + ' .md-interpret-op[data-op=\"insert\"]{border-left:3px solid #52c480}',\n '#' + CLS.ROOT + ' .md-interpret-op[data-op=\"remove\"]{border-left:3px solid #ff7597}',\n '#' + CLS.ROOT + ' .md-interpret-op[data-op=\"delta\"]{border-left:3px solid #ffbe4d}',\n '#' + CLS.ROOT + ' .md-interpret-op-header{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}',\n '#' + CLS.ROOT + ' .md-interpret-op-actions{display:flex;gap:4px;flex-shrink:0}',\n '#' + CLS.ROOT + ' .md-interpret-op-val{margin:0;max-height:90px;overflow-y:auto;background:var(--md-bg0,#181b24);padding:4px 6px;border-radius:4px;font-size:11px}',\n '#' + CLS.ROOT + ' .md-diff-group{margin-bottom:12px;border:1px solid var(--md-line,#333a4d);border-radius:6px;overflow:hidden}',\n '#' + CLS.ROOT + ' .md-diff-group-header{background:var(--md-bg2,#232a3d);padding:6px 10px;font-weight:600;color:var(--md-accent,#ffd27a);font-size:12px;display:flex;align-items:center;gap:6px}',\n '.md-dialog-overlay .md-diff-group{margin-bottom:10px;border:1px solid var(--md-line,#333a4d);border-radius:6px;overflow:hidden}',\n '.md-dialog-overlay .md-diff-group-header{background:var(--md-bg2,#232a3d);padding:6px 10px;font-weight:600;color:var(--md-accent,#ffd27a);font-size:12px;display:flex;align-items:center;gap:6px}',\n '#' + CLS.FAB + '{position:fixed;right:16px;top:16px;z-index:99999;width:44px;height:44px;border-radius:50%;border:1px solid var(--md-accent,#ffd27a);background:linear-gradient(135deg,var(--md-bg2,#1c2333),var(--md-bg2,#232a3d));color:var(--md-accent,#ffd27a);cursor:pointer;display:none;align-items:center;justify-content:center;box-shadow:0 4px 20px rgba(0,0,0,.5);touch-action:none;-webkit-user-select:none;user-select:none;transition:transform .15s,box-shadow .15s}',\n '#' + CLS.FAB + ':hover{transform:scale(1.08);box-shadow:0 6px 26px rgba(0,0,0,.65)}',\n '#' + CLS.FAB + ':focus-visible{outline:2px solid var(--md-accent,#ffd27a);outline-offset:3px}',\n // 窄屏固定两行:数量与方式在首行,三个操作在次行,避免下拉被 flex 压到截字。\n '@media (max-width:768px){#' + CLS.ROOT + ' .' + CLS.CHANGES_BAR + '{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(72px,auto);gap:6px}#' + CLS.ROOT + ' .md-changes-count{grid-column:1/3;overflow-wrap:anywhere}#' + CLS.ROOT + ' .' + CLS.CHANGES_BAR + '>select{grid-column:3;width:100%;min-width:72px}}',\n '@media (max-width:768px){#' + CLS.ROOT + '{--md-control-height:38px;width:min(96vw,420px)!important;height:min(92vh,680px)!important;right:8px;top:8px;max-height:92vh;resize:none}#' + CLS.ROOT + ' .' + CLS.GRID + '{grid-template-columns:1fr}#' + CLS.ROOT + ' .md-template-row{grid-template-columns:1fr}#' + CLS.ROOT + ' .md-template-row .md-field-error{grid-column:auto}#' + CLS.ROOT + ' .md-calendar-row{grid-template-columns:1fr 58px 70px 70px}#' + CLS.ROOT + ' .' + CLS.BUTTON + ',#' + CLS.ROOT + ' .md-header-btn,#' + CLS.ROOT + ' .' + CLS.TAB + '{min-height:38px}#' + CLS.FAB + '{right:12px;top:12px;width:48px;height:48px;min-height:44px}}',\n].join('\\n');\n\nmodule.exports = { CLS, ARIA, injectStyle, STYLE_ID, CSS_TEXT };\n\n});\n\n__define('ui/ui-state', function (module, exports, require) {\nconst STORAGE_KEY = 'md_modifier_v2_14_ui_state';\nconst LEGACY_POSITION_KEY = 'md_modifier_v2_14_position';\nconst LEGACY_THEME_KEY = 'md_modifier_v2_14_theme';\nconst LEGACY_CONFIG_KEY = 'md_modifier_v2_cfg';\nconst DEFAULT_THEME = Object.freeze({ family: 'classic', mode: 'dark' });\n\nconst DEFAULT_PERSISTENT = Object.freeze({\n geometry: Object.freeze({ x: 16, y: 16, width: 420, height: 600, lastOpenState: 'fab' }),\n theme: DEFAULT_THEME,\n settings: Object.freeze({\n applyMode: 'direct',\n themeFamily: DEFAULT_THEME.family,\n themeMode: DEFAULT_THEME.mode,\n historyLimit: 20,\n rememberWindow: true,\n syncMvu: true,\n autoSaveChat: true,\n }),\n cardStates: Object.freeze({}),\n scrollStates: Object.freeze({}),\n activeTab: 'protagonist',\n tabBarScrollLeft: 0,\n});\n\nfunction clone(value) {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction finite(value, fallback) {\n // Number(null) 和 Number('') 都会得到 0;配置缺失时不能把它们误当成用户坐标或尺寸。\n if (value === null || value === undefined || value === '') return fallback;\n const number = Number(value);\n return Number.isFinite(number) ? number : fallback;\n}\n\nfunction normalizePersistent(raw) {\n const source = raw && typeof raw === 'object' ? raw : {};\n const geometry = source.geometry && typeof source.geometry === 'object' ? source.geometry : {};\n const theme = source.theme && typeof source.theme === 'object' ? source.theme : {};\n const settings = source.settings && typeof source.settings === 'object' ? source.settings : {};\n const family = (theme.family === 'simple' || settings.themeFamily === 'simple') ? 'simple' : 'classic';\n const modeVal = theme.mode || settings.themeMode;\n const mode = ['auto', 'dark', 'light'].includes(modeVal) ? modeVal : DEFAULT_PERSISTENT.theme.mode;\n const cardStates = source.cardStates && typeof source.cardStates === 'object' && !Array.isArray(source.cardStates) ? clone(source.cardStates) : {};\n const scrollStates = source.scrollStates && typeof source.scrollStates === 'object' && !Array.isArray(source.scrollStates) ? clone(source.scrollStates) : {};\n const activeTab = typeof source.activeTab === 'string' && source.activeTab ? source.activeTab : 'protagonist';\n const tabBarScrollLeft = finite(source.tabBarScrollLeft, 0);\n\n return {\n geometry: {\n x: finite(geometry.x, DEFAULT_PERSISTENT.geometry.x),\n y: finite(geometry.y, DEFAULT_PERSISTENT.geometry.y),\n width: Math.max(280, finite(geometry.width, DEFAULT_PERSISTENT.geometry.width)),\n height: Math.max(260, finite(geometry.height, DEFAULT_PERSISTENT.geometry.height)),\n lastOpenState: geometry.lastOpenState === 'panel' ? 'panel' : 'fab',\n },\n theme: {\n family,\n mode,\n },\n settings: {\n applyMode: settings.applyMode === 'body' ? 'body' : 'direct',\n themeFamily: family,\n themeMode: mode,\n historyLimit: Math.min(100, Math.max(1, finite(settings.historyLimit, DEFAULT_PERSISTENT.settings.historyLimit))),\n rememberWindow: settings.rememberWindow !== false,\n syncMvu: settings.syncMvu !== false,\n autoSaveChat: settings.autoSaveChat !== false,\n },\n cardStates,\n scrollStates,\n activeTab,\n tabBarScrollLeft,\n };\n}\n\nfunction storageFor(P) {\n try {\n if (P && P.localStorage && typeof P.localStorage.getItem === 'function') return P.localStorage;\n } catch (error) { /* 浏览器策略可能拒绝存储访问。 */ }\n return null;\n}\n\nfunction loadPersistent(P) {\n const store = storageFor(P);\n if (!store) return normalizePersistent(null);\n try {\n const current = JSON.parse(store.getItem(STORAGE_KEY) || 'null');\n if (current) {\n const loaded = normalizePersistent(current);\n if (!loaded.settings.rememberWindow) loaded.geometry = clone(DEFAULT_PERSISTENT.geometry);\n return loaded;\n }\n\n // 兼容当前失败版和 v2.13 的坐标、配色设置,仅迁移可确认的字段。\n const legacyPosition = JSON.parse(store.getItem(LEGACY_POSITION_KEY) || 'null');\n const legacyConfig = JSON.parse(store.getItem(LEGACY_CONFIG_KEY) || 'null');\n const legacyTheme = store.getItem(LEGACY_THEME_KEY);\n const migrated = normalizePersistent({\n geometry: {\n x: legacyPosition && legacyPosition.x !== undefined ? legacyPosition.x : legacyConfig && legacyConfig.posX,\n y: legacyPosition && legacyPosition.y !== undefined ? legacyPosition.y : legacyConfig && legacyConfig.posY,\n width: legacyConfig && legacyConfig.width,\n height: legacyConfig && legacyConfig.height,\n },\n theme: { family: DEFAULT_THEME.family, mode: ['auto', 'dark', 'light'].includes(legacyTheme) ? legacyTheme : DEFAULT_THEME.mode },\n });\n if ((!legacyPosition || legacyPosition.x === undefined) && (!legacyConfig || legacyConfig.posX === undefined)) {\n const viewportWidth = Number(P && P.innerWidth) || 0;\n if (viewportWidth) migrated.geometry.x = Math.max(0, viewportWidth - migrated.geometry.width - 16);\n }\n return migrated;\n } catch (error) {\n return normalizePersistent(null);\n }\n}\n\nfunction savePersistent(P, value) {\n const store = storageFor(P);\n if (!store) return false;\n try {\n const stored = normalizePersistent(value);\n if (!stored.settings.rememberWindow) stored.geometry = clone(DEFAULT_PERSISTENT.geometry);\n store.setItem(STORAGE_KEY, JSON.stringify(stored));\n return true;\n } catch (error) {\n return false;\n }\n}\n\nfunction createTabState() {\n return {\n scrollTop: 0,\n openCards: {},\n selectedEntity: '',\n filters: {},\n drafts: {},\n errors: {},\n editorModes: {},\n collectionSelections: {},\n loadedEditors: {},\n focusedField: null,\n };\n}\nfunction clampGeometry(geometry, viewportWidth, viewportHeight, elementWidth, elementHeight) {\n const value = normalizePersistent({ geometry }).geometry;\n const vw = Math.max(0, finite(viewportWidth, 0));\n const vh = Math.max(0, finite(viewportHeight, 0));\n const width = Math.max(0, finite(elementWidth, value.width));\n const height = Math.max(0, finite(elementHeight, value.height));\n return Object.assign({}, value, {\n x: Math.min(Math.max(value.x, 0), Math.max(0, vw - Math.min(width, vw))),\n y: Math.min(Math.max(value.y, 0), Math.max(0, vh - Math.min(height, vh))),\n });\n}\n\nfunction createUiStateStore(P) {\n let persistent = loadPersistent(P);\n let batchDepth = 0;\n let pendingSave = false;\n function saveView() {\n pendingSave = true;\n if (!batchDepth) { pendingSave = false; return savePersistent(P, persistent); }\n return true;\n }\n const state = {\n activeTab: persistent.activeTab || 'protagonist',\n tabBarScrollLeft: persistent.tabBarScrollLeft || 0,\n tabs: {},\n busyOperation: false,\n liveMessage: '正在加载变量…',\n liveKind: 'info',\n };\n\n function tab(id) {\n if (!state.tabs[id]) {\n const ts = createTabState();\n if (persistent.cardStates && persistent.cardStates[id]) {\n ts.openCards = Object.assign({}, persistent.cardStates[id]);\n }\n if (persistent.scrollStates && persistent.scrollStates[id] !== undefined) {\n ts.scrollTop = finite(persistent.scrollStates[id], 0);\n }\n state.tabs[id] = ts;\n }\n return state.tabs[id];\n }\n\n return {\n state,\n tab,\nbatch(update) {\n batchDepth++;\n try { return update(); }\n finally { if (!--batchDepth && pendingSave) saveView(); }\n },\n persistent() { return clone(persistent); },\n geometry() { return Object.assign({}, persistent.geometry); },\n theme() { return Object.assign({}, persistent.theme); },\n settings() { return Object.assign({}, persistent.settings); },\n cardStates() { return clone(persistent.cardStates); },\n scrollStates() { return clone(persistent.scrollStates); },\n setCardOpen(tabId, cardId, open) {\n if (!tabId || !cardId) return;\n const t = tab(tabId);\n t.openCards[cardId] = !!open;\n if (!persistent.cardStates) persistent.cardStates = {};\n if (!persistent.cardStates[tabId]) persistent.cardStates[tabId] = {};\n if (persistent.cardStates[tabId][cardId] === !!open) return;\n persistent.cardStates[tabId][cardId] = !!open;\n saveView();\n },\n setScroll(tabId, scrollTop) {\n if (!tabId) return;\n const t = tab(tabId);\n t.scrollTop = scrollTop;\n if (!persistent.scrollStates) persistent.scrollStates = {};\n if (persistent.scrollStates[tabId] === scrollTop) return;\n persistent.scrollStates[tabId] = scrollTop;\n saveView();\n },\n setActiveTab(activeTab) {\n if (!activeTab) return;\n state.activeTab = activeTab;\n if (persistent.activeTab === activeTab) return;\n persistent.activeTab = activeTab;\n saveView();\n },\n setTabBarScroll(scrollLeft) {\n state.tabBarScrollLeft = scrollLeft;\n if (persistent.tabBarScrollLeft === scrollLeft) return;\n persistent.tabBarScrollLeft = scrollLeft;\n saveView();\n },\n setGeometry(change, persist = true) {\n persistent.geometry = Object.assign({}, persistent.geometry, change || {});\n persistent = normalizePersistent(persistent);\n if (persist) savePersistent(P, persistent);\n return this.geometry();\n },\n setTheme(change, persist = true) {\n persistent.theme = Object.assign({}, persistent.theme, change || {});\n persistent.settings = Object.assign({}, persistent.settings, {\n themeFamily: persistent.theme.family,\n themeMode: persistent.theme.mode,\n });\n persistent = normalizePersistent(persistent);\n if (persist) savePersistent(P, persistent);\n return this.theme();\n },\n setSettings(change, persist = true) {\n persistent.settings = Object.assign({}, persistent.settings, change || {});\n if (change && (change.themeFamily || change.themeMode)) {\n persistent.theme = Object.assign({}, persistent.theme, {\n family: change.themeFamily || persistent.theme.family,\n mode: change.themeMode || persistent.theme.mode,\n });\n }\n persistent = normalizePersistent(persistent);\n if (persist) savePersistent(P, persistent);\n return this.settings();\n },\nresetToDefaults({ preserveDrafts = true } = {}) {\n const drafts = {};\n if (preserveDrafts) {\n for (const [tabId, tabState] of Object.entries(state.tabs)) {\n if (tabId !== 'settings' && tabState && tabState.drafts) drafts[tabId] = clone({\n drafts: tabState.drafts, errors: tabState.errors, selectedEntity: tabState.selectedEntity,\n editorModes: tabState.editorModes, collectionSelections: tabState.collectionSelections,\n });\n }\n }\n persistent = normalizePersistent(DEFAULT_PERSISTENT);\n state.activeTab = persistent.activeTab;\n state.tabBarScrollLeft = persistent.tabBarScrollLeft;\n state.tabs = {};\n for (const [tabId, editing] of Object.entries(drafts)) {\n const next = createTabState();\n Object.assign(next, editing);\n state.tabs[tabId] = next;\n }\n const saved = savePersistent(P, persistent);\n return { saved, persistent: this.persistent() };\n },\n clearEditingSession() {\n state.tabs = {};\n state.tabBarScrollLeft = 0;\n },\n };\n}\n\nmodule.exports = {\n STORAGE_KEY,\n DEFAULT_PERSISTENT,\n normalizePersistent,\n loadPersistent,\n savePersistent,\n createTabState,\n clampGeometry,\n createUiStateStore,\n};\n\n});\n\n__define('util', function (module, exports, require) {\nfunction esc(s) {\n return String(s == null ? '' : s)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\nfunction isSafeKey(n) {\n return !!n && !/^(__proto__|constructor|prototype)$/i.test(String(n).trim());\n}\nfunction deepClone(o) {\n // JSON.stringify(undefined) 返回 undefined 而非 JSON 文本;保留它才能正确表达 remove/insert 预览。\n if (o === undefined || o === null || typeof o !== 'object') return o;\n try { return JSON.parse(JSON.stringify(o)); }\n catch (e) { throw new Error('数据无法安全深拷贝: ' + (e && e.message ? e.message : e)); }\n}\nfunction deepEqual(a, b) {\n if (a === b) return true;\n if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && a.length !== b.length) return false;\n // 对象键顺序不是变量变化;数组顺序和键是否存在仍是有意义的状态。\n try {\n const keys = Object.keys(a);\n return keys.length === Object.keys(b).length && keys.every((key) =>\n Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key]));\n }\n catch (e) { return false; }\n}\nfunction deepFreeze(value) {\n if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;\n Object.keys(value).forEach((key) => deepFreeze(value[key]));\n return Object.freeze(value);\n}\nfunction num(v) {\n const n = parseFloat(v);\n return Number.isFinite(n) ? n : 0;\n}\nfunction int(v) {\n return Math.trunc(num(v));\n}\nfunction sleep(ms) {\n return new Promise((r) => setTimeout(r, ms));\n}\nfunction isPlainObject(v) {\n return !!v && typeof v === 'object' && !Array.isArray(v);\n}\nfunction extend() {\n const out = {};\n for (const o of arguments) {\n if (isPlainObject(o)) Object.keys(o).forEach((k) => { out[k] = o[k]; });\n }\n return out;\n}\n\nmodule.exports = { esc, isSafeKey, deepClone, deepEqual, deepFreeze, num, int, sleep, isPlainObject, extend };\n\n});\n\n__define('version', function (module, exports, require) {\nconst DISPLAY_VERSION = '2.14';\nconst PACKAGE_VERSION = '2.14.0';\nconst MARKER = '__MINGDING_MODIFIER_' + DISPLAY_VERSION.replace(/\\./g, '_') + '__';\n\nmodule.exports = { DISPLAY_VERSION, PACKAGE_VERSION, MARKER };\n\n});\n\n const __boot = () => {\n const entry = __require('', 'main');\n if (typeof entry === 'function') entry();\n else if (entry && typeof entry.boot === 'function') entry.boot();\n };\n __boot();\n})();",
|
|
7
|
+
"info": "v2.14:重构为模块化主线,修复目标解析、MVU 调用、字段模型、补丁语义与回滚范围,统一交互闭环",
|
|
8
|
+
"button": {
|
|
9
|
+
"enabled": false,
|
|
10
|
+
"buttons": []
|
|
11
|
+
},
|
|
12
|
+
"data": {}
|
|
13
|
+
}
|