From 9b41710edf809bf997bfde312fc8cf907135289c Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:31:55 +0800 Subject: [PATCH 01/22] Strip everything before and including (handles unclosed think blocks) --- modules/ena-planner/ena-planner.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ena-planner/ena-planner.js b/modules/ena-planner/ena-planner.js index 590521e..fcc6bd9 100644 --- a/modules/ena-planner/ena-planner.js +++ b/modules/ena-planner/ena-planner.js @@ -307,7 +307,8 @@ function formatCharCardBlock(charObj) { function cleanAiMessageText(text) { let out = String(text ?? ''); - // 1) Strip properly wrapped / blocks only. + // 1) Strip everything before and including (handles unclosed think blocks) + out = out.replace(/^[\s\S]*?<\/think>/i, ''); out = out.replace(/]*>[\s\S]*?<\/think>/gi, ''); out = out.replace(/]*>[\s\S]*?<\/thinking>/gi, ''); From e38278e5c675cdb5bd5310cad16eb5c0dc67d3ff Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:46:12 +0800 Subject: [PATCH 02/22] =?UTF-8?q?Log=20=E6=A0=B7=E5=BC=8F=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ena-planner/ena-planner.css | 46 ++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/modules/ena-planner/ena-planner.css b/modules/ena-planner/ena-planner.css index e819880..52dec9c 100644 --- a/modules/ena-planner/ena-planner.css +++ b/modules/ena-planner/ena-planner.css @@ -618,6 +618,50 @@ textarea.input { font-size: .8125rem; } +/* Message cards inside log */ +.msg-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 8px; +} + +.msg-card { + border-radius: var(--radius); + border-left: 3px solid var(--bdr); + background: var(--code-bg); + padding: 8px 12px; +} + +.msg-card.msg-system { border-left-color: #6b8afd; } +.msg-card.msg-user { border-left-color: #4ecdc4; } +.msg-card.msg-assistant { border-left-color: #f7a046; } + +.msg-role { + font-size: .6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .04em; + margin-bottom: 4px; + color: var(--txt3); +} + +.msg-system .msg-role { color: #6b8afd; } +.msg-user .msg-role { color: #4ecdc4; } +.msg-assistant .msg-role { color: #f7a046; } + +.msg-content { + font-family: 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace; + font-size: .6875rem; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; + color: var(--code-txt); + margin: 0; + max-height: 300px; + overflow-y: auto; +} + details { margin-bottom: 6px; } @@ -841,4 +885,4 @@ details summary:hover { details summary { padding: 8px 0; } -} \ No newline at end of file +} From 08d70946ab4d1947701b1769eed30c21765bbbbd Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:49:10 +0800 Subject: [PATCH 03/22] =?UTF-8?q?Log=E6=A0=B7=E5=BC=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ena-planner/ena-planner.html | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/modules/ena-planner/ena-planner.html b/modules/ena-planner/ena-planner.html index e27e2e8..12e9f4b 100644 --- a/modules/ena-planner/ena-planner.html +++ b/modules/ena-planner/ena-planner.html @@ -636,6 +636,23 @@

EnaPlanner

const time = item.time ? new Date(item.time).toLocaleString() : '-'; const cls = item.ok ? 'success' : 'error'; const label = item.ok ? '成功' : '失败'; + + // Format request messages: one card per message with role label + let msgHtml = ''; + if (Array.isArray(item.requestMessages) && item.requestMessages.length) { + msgHtml = item.requestMessages.map((m, i) => { + const role = escapeHtml(m.role || 'unknown'); + const roleClass = role === 'system' ? 'msg-system' : role === 'user' ? 'msg-user' : 'msg-assistant'; + const content = escapeHtml(m.content || ''); + return `
+
[${i + 1}] ${role}
+
${content}
+
`; + }).join(''); + } else { + msgHtml = '
无消息
'; + } + return `
@@ -643,8 +660,8 @@

EnaPlanner

${escapeHtml(item.model || '-')}
${item.error ? `
${escapeHtml(item.error)}
` : ''} -
请求消息 -
${escapeHtml(JSON.stringify(item.requestMessages || [], null, 2))}
+
请求消息 (${(item.requestMessages || []).length} 条) +
${msgHtml}
原始回复
${escapeHtml(item.rawReply || '')}
From 24ca51051b0677063c2a177886ed041a75314022 Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:07:36 +0800 Subject: [PATCH 04/22] =?UTF-8?q?=E5=B0=8F=E7=99=BD=E6=9D=BF=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E6=9B=9D=E9=9C=B2=E7=BB=99ena-planner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/story-outline/story-outline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/story-outline/story-outline.js b/modules/story-outline/story-outline.js index aa01a92..3a0a08c 100644 --- a/modules/story-outline/story-outline.js +++ b/modules/story-outline/story-outline.js @@ -1395,4 +1395,4 @@ jQuery(() => { window.registerModuleCleanup?.('storyOutline', cleanup); }); -export { cleanup }; +export { cleanup, formatOutlinePrompt }; From f5eacbaebe00beea4291235d26afc1aa10de747d Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:10:44 +0800 Subject: [PATCH 05/22] =?UTF-8?q?=E5=B0=8F=E7=99=BD=E6=9D=BF=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E6=9B=9D=E9=9C=B2=E7=BB=99ena-planner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ena-planner/ena-planner.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/ena-planner/ena-planner.js b/modules/ena-planner/ena-planner.js index fcc6bd9..db12f4f 100644 --- a/modules/ena-planner/ena-planner.js +++ b/modules/ena-planner/ena-planner.js @@ -7,6 +7,7 @@ import { extensionFolderPath } from '../../core/constants.js'; import { EnaPlannerStorage } from '../../core/server-storage.js'; import { postToIframe, isTrustedIframeEvent } from '../../core/iframe-messaging.js'; import { DEFAULT_PROMPT_BLOCKS, BUILTIN_TEMPLATES } from './ena-planner-presets.js'; +import { formatOutlinePrompt } from '../story-outline/story-outline.js'; const EXT_NAME = 'ena-planner'; const OVERLAY_ID = 'xiaobaix-ena-planner-overlay'; @@ -1135,6 +1136,7 @@ async function buildPlannerMessages(rawUserInput) { const scanText = [charBlockRaw, cachedSummary, recentChatRaw, vectorRaw, plotsRaw, rawUserInput].join('\n\n'); const worldbookRaw = await buildWorldbookBlock(scanText); + const outlineRaw = typeof formatOutlinePrompt === 'function' ? (formatOutlinePrompt() || '') : ''; // Render templates/macros const charBlock = await renderTemplateAll(charBlockRaw, env, messageVars); @@ -1144,6 +1146,7 @@ async function buildPlannerMessages(rawUserInput) { const storySummary = cachedSummary.trim().length > 30 ? await renderTemplateAll(cachedSummary, env, messageVars) : ''; const worldbook = await renderTemplateAll(worldbookRaw, env, messageVars); const userInput = await renderTemplateAll(rawUserInput, env, messageVars); + const storyOutline = outlineRaw.trim().length > 10 ? await renderTemplateAll(outlineRaw, env, messageVars) : ''; const messages = []; @@ -1159,6 +1162,11 @@ async function buildPlannerMessages(rawUserInput) { // 3) Worldbook if (String(worldbook).trim()) messages.push({ role: 'system', content: worldbook }); + // 3.5) Story Outline / 剧情地图(小白板世界架构) + if (storyOutline.trim()) { + messages.push({ role: 'system', content: `\n${storyOutline}\n` }); + } + // 4) Chat history (last 2 AI responses — floors N-1 & N-3) if (String(recentChat).trim()) messages.push({ role: 'system', content: recentChat }); From ce32861ca75c29c82126af0bc8837eb281019bc3 Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:57:53 +0800 Subject: [PATCH 06/22] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=B8=96=E7=95=8C?= =?UTF-8?q?=E4=B9=A6=E5=AE=8F=E8=AF=BB=E5=8F=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ena-planner/ena-planner.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/modules/ena-planner/ena-planner.js b/modules/ena-planner/ena-planner.js index db12f4f..57ec8db 100644 --- a/modules/ena-planner/ena-planner.js +++ b/modules/ena-planner/ena-planner.js @@ -8,6 +8,7 @@ import { EnaPlannerStorage } from '../../core/server-storage.js'; import { postToIframe, isTrustedIframeEvent } from '../../core/iframe-messaging.js'; import { DEFAULT_PROMPT_BLOCKS, BUILTIN_TEMPLATES } from './ena-planner-presets.js'; import { formatOutlinePrompt } from '../story-outline/story-outline.js'; +import jsyaml from '../../libs/js-yaml.mjs'; const EXT_NAME = 'ena-planner'; const OVERLAY_ID = 'xiaobaix-ena-planner-overlay'; @@ -551,6 +552,7 @@ function matchSelective(entry, scanText) { const keys2 = Array.isArray(entry?.keysecondary) ? entry.keysecondary.filter(Boolean) : []; const total = keys.length; + if (total === 0) return false; const hit = keys.reduce((acc, kw) => acc + (keywordPresent(scanText, kw) ? 1 : 0), 0); let ok = false; @@ -838,6 +840,17 @@ function resolveGetMessageVariableMacros(text, messageVars) { }); } +function resolveFormatMessageVariableMacros(text, messageVars) { + return text.replace(/{{\s*format_message_variable::([^}]+)\s*}}/g, (_, rawPath) => { + const path = String(rawPath || '').trim(); + if (!path) return ''; + const val = deepGet(messageVars, path); + if (val == null) return ''; + if (typeof val === 'string') return val; + try { return jsyaml.dump(val, { lineWidth: -1, noRefs: true }); } catch { return safeStringify(val); } + }); +} + function getLatestMessageVarTable() { try { if (window.Mvu?.getMvuData) { @@ -858,6 +871,7 @@ async function renderTemplateAll(text, env, messageVars) { out = await evalEjsIfPossible(out, env); out = substituteMacrosViaST(out); out = resolveGetMessageVariableMacros(out, messageVars); + out = resolveFormatMessageVariableMacros(out, messageVars); return out; } From fe9736a1b425d09c9dfe665d1432882defcdcf35 Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:51:11 +0800 Subject: [PATCH 07/22] =?UTF-8?q?=E4=BF=AE=E6=AD=A3summary=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E7=BB=BF=E7=81=AF=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ena-planner/ena-planner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ena-planner/ena-planner.js b/modules/ena-planner/ena-planner.js index 57ec8db..c4db7cc 100644 --- a/modules/ena-planner/ena-planner.js +++ b/modules/ena-planner/ena-planner.js @@ -1147,7 +1147,7 @@ async function buildPlannerMessages(rawUserInput) { const vectorRaw = ''; // Build scanText for worldbook keyword activation - const scanText = [charBlockRaw, cachedSummary, recentChatRaw, vectorRaw, plotsRaw, rawUserInput].join('\n\n'); + const scanText = [charBlockRaw, recentChatRaw, vectorRaw, plotsRaw, rawUserInput].join('\n\n'); const worldbookRaw = await buildWorldbookBlock(scanText); const outlineRaw = typeof formatOutlinePrompt === 'function' ? (formatOutlinePrompt() || '') : ''; From d7f6d1f22bef2b6704d5f5a457d36922241dde23 Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:43:13 +0800 Subject: [PATCH 08/22] =?UTF-8?q?=E5=90=91=E9=87=8F=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E5=88=B0ST=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../story-summary/vector/storage/vector-io.js | 327 ++++++++++++++++++ 1 file changed, 327 insertions(+) diff --git a/modules/story-summary/vector/storage/vector-io.js b/modules/story-summary/vector/storage/vector-io.js index 99899a7..f293dfd 100644 --- a/modules/story-summary/vector/storage/vector-io.js +++ b/modules/story-summary/vector/storage/vector-io.js @@ -5,6 +5,7 @@ import { zipSync, unzipSync, strToU8, strFromU8 } from '../../../../libs/fflate.mjs'; import { getContext } from '../../../../../../../extensions.js'; +import { getRequestHeaders } from '../../../../../../../../script.js'; import { xbLog } from '../../../../core/debug-core.js'; import { getMeta, @@ -72,6 +73,30 @@ function downloadBlob(blob, filename) { document.body.removeChild(a); URL.revokeObjectURL(url); } +// 二进制 Uint8Array → base64(分块处理,避免 btoa 栈溢出) +function uint8ToBase64(uint8) { + const CHUNK = 0x8000; + let result = ''; + for (let i = 0; i < uint8.length; i += CHUNK) { + result += String.fromCharCode.apply(null, uint8.subarray(i, i + CHUNK)); + } + return btoa(result); +} + +// base64 → Uint8Array +function base64ToUint8(base64) { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +// 服务器备份文件名 +function getBackupFilename(chatId) { + return `LWB_VectorBackup_${chatId}.zip`; +} // ═══════════════════════════════════════════════════════════════════════════ // 导出 @@ -383,3 +408,305 @@ export async function importVectors(file, onProgress) { fingerprintMismatch, }; } +// ═══════════════════════════════════════════════════════════════════════════ +// 备份到服务器 +// ═══════════════════════════════════════════════════════════════════════════ + +export async function backupToServer(onProgress) { + const { chatId } = getContext(); + if (!chatId) { + throw new Error('未打开聊天'); + } + + onProgress?.('读取数据...'); + + const meta = await getMeta(chatId); + const chunks = await getAllChunks(chatId); + const chunkVectors = await getAllChunkVectors(chatId); + const eventVectors = await getAllEventVectors(chatId); + const stateAtoms = getStateAtoms(); + const stateVectors = await getAllStateVectors(chatId); + + if (chunkVectors.length === 0 && eventVectors.length === 0 && stateVectors.length === 0) { + throw new Error('没有可备份的向量数据'); + } + + const dims = chunkVectors[0]?.vector?.length + || eventVectors[0]?.vector?.length + || stateVectors[0]?.vector?.length + || 0; + if (dims === 0) { + throw new Error('无法确定向量维度'); + } + + onProgress?.('构建索引...'); + + const sortedChunks = [...chunks].sort((a, b) => a.chunkId.localeCompare(b.chunkId)); + const chunkVectorMap = new Map(chunkVectors.map(cv => [cv.chunkId, cv.vector])); + + const chunksJsonl = sortedChunks.map(c => JSON.stringify({ + chunkId: c.chunkId, + floor: c.floor, + chunkIdx: c.chunkIdx, + speaker: c.speaker, + isUser: c.isUser, + text: c.text, + textHash: c.textHash, + })).join('\n'); + + const chunkVectorsOrdered = sortedChunks.map(c => chunkVectorMap.get(c.chunkId) || new Array(dims).fill(0)); + + onProgress?.('压缩向量...'); + + const sortedEventVectors = [...eventVectors].sort((a, b) => a.eventId.localeCompare(b.eventId)); + const eventsJsonl = sortedEventVectors.map(ev => JSON.stringify({ + eventId: ev.eventId, + })).join('\n'); + const eventVectorsOrdered = sortedEventVectors.map(ev => ev.vector); + + const sortedStateVectors = [...stateVectors].sort((a, b) => String(a.atomId).localeCompare(String(b.atomId))); + const stateVectorsOrdered = sortedStateVectors.map(v => v.vector); + const rDims = sortedStateVectors.find(v => v.rVector?.length)?.rVector?.length || dims; + const stateRVectorsOrdered = sortedStateVectors.map(v => + v.rVector?.length ? v.rVector : new Array(rDims).fill(0) + ); + const stateVectorsJsonl = sortedStateVectors.map(v => JSON.stringify({ + atomId: v.atomId, + floor: v.floor, + hasRVector: !!(v.rVector?.length), + rDims: v.rVector?.length || 0, + })).join('\n'); + + const manifest = { + version: EXPORT_VERSION, + exportedAt: Date.now(), + chatId, + fingerprint: meta.fingerprint || '', + dims, + chunkCount: sortedChunks.length, + chunkVectorCount: chunkVectors.length, + eventCount: sortedEventVectors.length, + stateAtomCount: stateAtoms.length, + stateVectorCount: stateVectors.length, + stateRVectorCount: sortedStateVectors.filter(v => v.rVector?.length).length, + rDims, + lastChunkFloor: meta.lastChunkFloor ?? -1, + }; + + onProgress?.('打包文件...'); + + const zipData = zipSync({ + 'manifest.json': strToU8(JSON.stringify(manifest, null, 2)), + 'chunks.jsonl': strToU8(chunksJsonl), + 'chunk_vectors.bin': float32ToBytes(chunkVectorsOrdered, dims), + 'events.jsonl': strToU8(eventsJsonl), + 'event_vectors.bin': float32ToBytes(eventVectorsOrdered, dims), + 'state_atoms.json': strToU8(JSON.stringify(stateAtoms)), + 'state_vectors.jsonl': strToU8(stateVectorsJsonl), + 'state_vectors.bin': stateVectorsOrdered.length + ? float32ToBytes(stateVectorsOrdered, dims) + : new Uint8Array(0), + 'state_r_vectors.bin': stateRVectorsOrdered.length + ? float32ToBytes(stateRVectorsOrdered, rDims) + : new Uint8Array(0), + }, { level: 1 }); + + onProgress?.('上传到服务器...'); + + const base64 = uint8ToBase64(zipData); + const filename = getBackupFilename(chatId); + + const res = await fetch('/api/files/upload', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ name: filename, data: base64 }), + }); + if (!res.ok) { + throw new Error(`服务器返回 ${res.status}`); + } + + const sizeMB = (zipData.byteLength / 1024 / 1024).toFixed(2); + xbLog.info(MODULE_ID, `备份完成: ${filename} (${sizeMB}MB)`); + + return { + filename, + size: zipData.byteLength, + chunkCount: sortedChunks.length, + eventCount: sortedEventVectors.length, + }; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 从服务器恢复 +// ═══════════════════════════════════════════════════════════════════════════ + +export async function restoreFromServer(onProgress) { + const { chatId } = getContext(); + if (!chatId) { + throw new Error('未打开聊天'); + } + + onProgress?.('从服务器下载...'); + + const filename = getBackupFilename(chatId); + const res = await fetch(`/user/files/${filename}`, { + headers: getRequestHeaders(), + cache: 'no-cache', + }); + + if (!res.ok) { + if (res.status === 404) { + throw new Error('服务器上没有找到此聊天的备份'); + } + throw new Error(`服务器返回 ${res.status}`); + } + + const text = await res.text(); + if (!text) { + throw new Error('服务器上没有找到此聊天的备份'); + } + + onProgress?.('解压文件...'); + + const zipData = base64ToUint8(text); + + let unzipped; + try { + unzipped = unzipSync(zipData); + } catch (e) { + throw new Error('备份文件格式错误,无法解压'); + } + + if (!unzipped['manifest.json']) { + throw new Error('缺少 manifest.json'); + } + + const manifest = JSON.parse(strFromU8(unzipped['manifest.json'])); + + if (![1, 2].includes(manifest.version)) { + throw new Error(`不支持的版本: ${manifest.version}`); + } + + onProgress?.('校验数据...'); + + const vectorCfg = getVectorConfig(); + const currentFingerprint = vectorCfg ? getEngineFingerprint(vectorCfg) : ''; + const fingerprintMismatch = manifest.fingerprint && currentFingerprint && manifest.fingerprint !== currentFingerprint; + const chatIdMismatch = manifest.chatId !== chatId; + + const warnings = []; + if (fingerprintMismatch) { + warnings.push(`向量引擎不匹配(文件: ${manifest.fingerprint}, 当前: ${currentFingerprint}),导入后需重新生成`); + } + if (chatIdMismatch) { + warnings.push(`聊天ID不匹配(文件: ${manifest.chatId}, 当前: ${chatId})`); + } + + onProgress?.('解析数据...'); + + const chunksJsonl = unzipped['chunks.jsonl'] ? strFromU8(unzipped['chunks.jsonl']) : ''; + const chunkMetas = chunksJsonl.split('\n').filter(Boolean).map(line => JSON.parse(line)); + + const chunkVectorsBytes = unzipped['chunk_vectors.bin']; + const chunkVectors = chunkVectorsBytes ? bytesToFloat32(chunkVectorsBytes, manifest.dims) : []; + + const eventsJsonl = unzipped['events.jsonl'] ? strFromU8(unzipped['events.jsonl']) : ''; + const eventMetas = eventsJsonl.split('\n').filter(Boolean).map(line => JSON.parse(line)); + + const eventVectorsBytes = unzipped['event_vectors.bin']; + const eventVectors = eventVectorsBytes ? bytesToFloat32(eventVectorsBytes, manifest.dims) : []; + + const stateAtoms = unzipped['state_atoms.json'] + ? JSON.parse(strFromU8(unzipped['state_atoms.json'])) + : []; + + const stateVectorsJsonl = unzipped['state_vectors.jsonl'] ? strFromU8(unzipped['state_vectors.jsonl']) : ''; + const stateVectorMetas = stateVectorsJsonl.split('\n').filter(Boolean).map(line => JSON.parse(line)); + + const stateVectorsBytes = unzipped['state_vectors.bin']; + const stateVectors = (stateVectorsBytes && stateVectorMetas.length) + ? bytesToFloat32(stateVectorsBytes, manifest.dims) + : []; + const stateRVectorsBytes = unzipped['state_r_vectors.bin']; + const stateRVectors = (stateRVectorsBytes && stateVectorMetas.length) + ? bytesToFloat32(stateRVectorsBytes, manifest.rDims || manifest.dims) + : []; + const hasRVectorMeta = stateVectorMetas.some(m => typeof m.hasRVector === 'boolean'); + + if (chunkMetas.length !== chunkVectors.length) { + throw new Error(`chunk 数量不匹配: 元数据 ${chunkMetas.length}, 向量 ${chunkVectors.length}`); + } + if (eventMetas.length !== eventVectors.length) { + throw new Error(`event 数量不匹配: 元数据 ${eventMetas.length}, 向量 ${eventVectors.length}`); + } + if (stateVectorMetas.length !== stateVectors.length) { + throw new Error(`state 向量数量不匹配: 元数据 ${stateVectorMetas.length}, 向量 ${stateVectors.length}`); + } + if (stateRVectors.length > 0 && stateVectorMetas.length !== stateRVectors.length) { + throw new Error(`state r-vector count mismatch: meta=${stateVectorMetas.length}, vectors=${stateRVectors.length}`); + } + + onProgress?.('清空旧数据...'); + + await clearAllChunks(chatId); + await clearEventVectors(chatId); + await clearStateVectors(chatId); + clearStateAtoms(); + + onProgress?.('写入数据...'); + + if (chunkMetas.length > 0) { + const chunksToSave = chunkMetas.map(meta => ({ + chunkId: meta.chunkId, + floor: meta.floor, + chunkIdx: meta.chunkIdx, + speaker: meta.speaker, + isUser: meta.isUser, + text: meta.text, + textHash: meta.textHash, + })); + await saveChunks(chatId, chunksToSave); + + const chunkVectorItems = chunkMetas.map((meta, idx) => ({ + chunkId: meta.chunkId, + vector: chunkVectors[idx], + })); + await saveChunkVectors(chatId, chunkVectorItems, manifest.fingerprint); + } + + if (eventMetas.length > 0) { + const eventVectorItems = eventMetas.map((meta, idx) => ({ + eventId: meta.eventId, + vector: eventVectors[idx], + })); + await saveEventVectors(chatId, eventVectorItems, manifest.fingerprint); + } + + if (stateAtoms.length > 0) { + saveStateAtoms(stateAtoms); + } + + if (stateVectorMetas.length > 0) { + const stateVectorItems = stateVectorMetas.map((meta, idx) => ({ + atomId: meta.atomId, + floor: meta.floor, + vector: stateVectors[idx], + rVector: (stateRVectors[idx] && (!hasRVectorMeta || meta.hasRVector)) ? stateRVectors[idx] : null, + })); + await saveStateVectors(chatId, stateVectorItems, manifest.fingerprint); + } + + await updateMeta(chatId, { + fingerprint: manifest.fingerprint, + lastChunkFloor: manifest.lastChunkFloor, + }); + + xbLog.info(MODULE_ID, `从服务器恢复完成: ${chunkMetas.length} chunks, ${eventMetas.length} events, ${stateAtoms.length} state atoms`); + + return { + chunkCount: chunkMetas.length, + eventCount: eventMetas.length, + warnings, + fingerprintMismatch, + }; +} From ea940be35d0e76124957a0b6d8651679f708e903 Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:45:46 +0800 Subject: [PATCH 09/22] =?UTF-8?q?=E5=90=91=E9=87=8F=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E5=88=B0ST=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/story-summary/story-summary.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html index bef07e8..bff0d77 100644 --- a/modules/story-summary/story-summary.html +++ b/modules/story-summary/story-summary.html @@ -561,6 +561,13 @@

编辑

style="flex:1">导入向量数据
+
+ + +
+
From 554966fde908df240a90ff21c8acb8c43fb55958 Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:49:23 +0800 Subject: [PATCH 10/22] =?UTF-8?q?=E5=90=91=E9=87=8F=E5=88=B0ST=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/story-summary/story-summary-ui.js | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js index 65142ea..b329031 100644 --- a/modules/story-summary/story-summary-ui.js +++ b/modules/story-summary/story-summary-ui.js @@ -424,6 +424,17 @@ $('vector-io-status').textContent = '导入中...'; postMsg('VECTOR_IMPORT_PICK'); }; + $('btn-backup-server').onclick = () => { + $('btn-backup-server').disabled = true; + $('server-io-status').textContent = '备份中...'; + postMsg('VECTOR_BACKUP_SERVER'); + }; + + $('btn-restore-server').onclick = () => { + $('btn-restore-server').disabled = true; + $('server-io-status').textContent = '恢复中...'; + postMsg('VECTOR_RESTORE_SERVER'); + }; initAnchorUI(); postMsg('REQUEST_ANCHOR_STATS'); @@ -1500,6 +1511,28 @@ $('vector-io-status').textContent = '导入失败: ' + (d.error || '未知错误'); } break; + case 'VECTOR_BACKUP_RESULT': + $('btn-backup-server').disabled = false; + if (d.success) { + $('server-io-status').textContent = `☁️ 备份成功: ${(d.size / 1024 / 1024).toFixed(2)}MB (${d.chunkCount} 片段, ${d.eventCount} 事件)`; + } else { + $('server-io-status').textContent = '备份失败: ' + (d.error || '未知错误'); + } + break; + + case 'VECTOR_RESTORE_RESULT': + $('btn-restore-server').disabled = false; + if (d.success) { + let msg = `☁️ 恢复成功: ${d.chunkCount} 片段, ${d.eventCount} 事件`; + if (d.warnings?.length) { + msg += '\n⚠️ ' + d.warnings.join('\n⚠️ '); + } + $('server-io-status').textContent = msg; + postMsg('REQUEST_VECTOR_STATS'); + } else { + $('server-io-status').textContent = '恢复失败: ' + (d.error || '未知错误'); + } + break; case 'RECALL_LOG': setRecallLog(d.text || ''); From 46d92fc5a975d6086d129093228dba80a1cae5c0 Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:51:56 +0800 Subject: [PATCH 11/22] =?UTF-8?q?=E5=90=91=E9=87=8F=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E5=88=B0ST=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/story-summary/story-summary.js | 41 +++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 40d1c01..877f702 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -89,7 +89,7 @@ import { } from "./vector/storage/state-store.js"; // vector io -import { exportVectors, importVectors } from "./vector/storage/vector-io.js"; +import { exportVectors, importVectors, backupToServer, restoreFromServer } from "./vector/storage/vector-io.js"; import { invalidateLexicalIndex, warmupIndex, addDocumentsForFloor, removeDocumentsByFloor, addEventDocuments } from "./vector/retrieval/lexical-index.js"; @@ -1459,6 +1459,45 @@ async function handleFrameMessage(event) { input.click(); })(); break; + case "VECTOR_BACKUP_SERVER": + (async () => { + try { + const result = await backupToServer((status) => { + postToFrame({ type: "VECTOR_IO_STATUS", status }); + }); + postToFrame({ + type: "VECTOR_BACKUP_RESULT", + success: true, + size: result.size, + chunkCount: result.chunkCount, + eventCount: result.eventCount, + }); + } catch (e) { + postToFrame({ type: "VECTOR_BACKUP_RESULT", success: false, error: e.message }); + } + })(); + break; + + case "VECTOR_RESTORE_SERVER": + (async () => { + try { + const result = await restoreFromServer((status) => { + postToFrame({ type: "VECTOR_IO_STATUS", status }); + }); + postToFrame({ + type: "VECTOR_RESTORE_RESULT", + success: true, + chunkCount: result.chunkCount, + eventCount: result.eventCount, + warnings: result.warnings, + fingerprintMismatch: result.fingerprintMismatch, + }); + await sendVectorStatsToFrame(); + } catch (e) { + postToFrame({ type: "VECTOR_RESTORE_RESULT", success: false, error: e.message }); + } + })(); + break; case "REQUEST_VECTOR_STATS": sendVectorStatsToFrame(); From d6c3f4564035891e33fe79a6789fd2fa4a5eb174 Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:02:55 +0800 Subject: [PATCH 12/22] =?UTF-8?q?backup=20file=E5=90=8D=E7=A7=B0=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/story-summary/vector/storage/vector-io.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/story-summary/vector/storage/vector-io.js b/modules/story-summary/vector/storage/vector-io.js index f293dfd..d15ab18 100644 --- a/modules/story-summary/vector/storage/vector-io.js +++ b/modules/story-summary/vector/storage/vector-io.js @@ -95,7 +95,14 @@ function base64ToUint8(base64) { // 服务器备份文件名 function getBackupFilename(chatId) { - return `LWB_VectorBackup_${chatId}.zip`; + // chatId 可能含中文/特殊字符,ST 只接受 [a-zA-Z0-9_-] + // 用简单 hash 生成安全文件名 + let hash = 0; + for (let i = 0; i < chatId.length; i++) { + hash = ((hash << 5) - hash + chatId.charCodeAt(i)) | 0; + } + const safe = (hash >>> 0).toString(36); + return `LWB_VectorBackup_${safe}.zip`; } // ═══════════════════════════════════════════════════════════════════════════ From 7275692067ae226f27877bb6d7ce045e781a983c Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:11:13 +0800 Subject: [PATCH 13/22] =?UTF-8?q?=E5=AD=98=E5=8F=96=E5=90=91=E9=87=8F?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/story-summary/vector/storage/vector-io.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/story-summary/vector/storage/vector-io.js b/modules/story-summary/vector/storage/vector-io.js index d15ab18..7033cc1 100644 --- a/modules/story-summary/vector/storage/vector-io.js +++ b/modules/story-summary/vector/storage/vector-io.js @@ -568,14 +568,14 @@ export async function restoreFromServer(onProgress) { throw new Error(`服务器返回 ${res.status}`); } - const text = await res.text(); - if (!text) { + const arrayBuffer = await res.arrayBuffer(); + if (!arrayBuffer || arrayBuffer.byteLength === 0) { throw new Error('服务器上没有找到此聊天的备份'); } onProgress?.('解压文件...'); - const zipData = base64ToUint8(text); + const zipData = new Uint8Array(arrayBuffer); let unzipped; try { From 1a2eb509e4971678fedde28ab893366852bebf9f Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:35:42 +0800 Subject: [PATCH 14/22] =?UTF-8?q?=E5=88=87=E8=81=8A=E5=A4=A9=E6=97=B6?= =?UTF-8?q?=E6=B8=85=E6=8E=89=E6=97=A7=20summary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/story-summary/story-summary.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 877f702..54fa799 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -1639,6 +1639,7 @@ async function handleManualGenerate(mesId, config) { async function handleChatChanged() { if (!events) return; + _lastBuiltPromptText = ""; // ← 加这一行,切聊天时清掉旧 summary const { chat } = getContext(); activeChatId = getContext().chatId || null; const newLength = Array.isArray(chat) ? chat.length : 0; From ae45853277cb2897db389689e37005515e807b4a Mon Sep 17 00:00:00 2001 From: LittleWhiteBox Dev Date: Tue, 17 Mar 2026 00:04:02 +0800 Subject: [PATCH 15/22] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=91=E9=87=8F?= =?UTF-8?q?=E5=A4=87=E4=BB=BD=E7=AE=A1=E7=90=86=20UI=EF=BC=88=E6=B8=85?= =?UTF-8?q?=E5=8D=95=20+=20Modal=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vector-io.js:新增 fetchManifest / upsertManifestEntry / deleteServerBackup 等清单管理函数;backupToServer 成功后自动写入 LWB_BackupManifest.json - story-summary.html:在服务器 IO 区域新增「管理」按钮及独立 Modal 弹窗 - story-summary-ui.js:新增备份列表渲染、删除确认、只读模式降级逻辑 - story-summary.js:新增 VECTOR_LIST_BACKUPS / VECTOR_DELETE_BACKUP 消息处理 Co-Authored-By: Claude Sonnet 4.6 --- modules/story-summary/story-summary-ui.js | 91 ++++++++++ modules/story-summary/story-summary.html | 27 +++ modules/story-summary/story-summary.js | 56 ++++++- .../story-summary/vector/storage/vector-io.js | 157 ++++++++++++++++++ 4 files changed, 330 insertions(+), 1 deletion(-) diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js index b329031..f530c7a 100644 --- a/modules/story-summary/story-summary-ui.js +++ b/modules/story-summary/story-summary-ui.js @@ -104,6 +104,9 @@ let allLinks = []; let activeRelationTooltip = null; let lastRecallLogText = ''; + let backupDeleteSupported = true; + let backupDeleteUnsupportedReason = ''; + let lastLoadedBackupFiles = []; // ═══════════════════════════════════════════════════════════════════════════ // Messaging @@ -436,6 +439,19 @@ postMsg('VECTOR_RESTORE_SERVER'); }; + $('btn-manage-backups').onclick = () => { + $('backup-manager-modal').style.display = 'flex'; + $('backup-manager-status').textContent = '加载中...'; + postMsg('VECTOR_LIST_BACKUPS'); + }; + $('btn-close-backup-modal').onclick = () => { + $('backup-manager-modal').style.display = 'none'; + }; + $('btn-refresh-backups').onclick = () => { + $('backup-manager-status').textContent = '加载中...'; + postMsg('VECTOR_LIST_BACKUPS'); + }; + initAnchorUI(); postMsg('REQUEST_ANCHOR_STATS'); } @@ -1534,6 +1550,27 @@ } break; + case 'VECTOR_LIST_RESULT': + backupDeleteSupported = d.deleteSupported !== false; + backupDeleteUnsupportedReason = d.deleteUnsupportedReason || ''; + renderBackupList(d.files); + $('backup-manager-status').textContent = ''; + break; + + case 'VECTOR_DELETE_BACKUP_RESULT': + if (!d.success) { + $('backup-manager-status').textContent = '删除失败: ' + (d.error || '未知'); + if (d.deleteSupported === false) { + backupDeleteSupported = false; + backupDeleteUnsupportedReason = d.deleteUnsupportedReason || '宿主不支持删除'; + renderBackupList(lastLoadedBackupFiles); + } + } else { + $('backup-manager-status').textContent = '已删除'; + postMsg('VECTOR_LIST_BACKUPS'); + } + break; + case 'RECALL_LOG': setRecallLog(d.text || ''); break; @@ -1810,4 +1847,58 @@ setHtml(container, html); } + + // ═══════════════════════════════════════════════════════════════════════════ + // 备份管理弹窗 + // ═══════════════════════════════════════════════════════════════════════════ + + function renderBackupList(files) { + lastLoadedBackupFiles = files || []; + const el = $('backup-list-content'); + $('backup-count-badge').textContent = `(${lastLoadedBackupFiles.length})`; + if (!lastLoadedBackupFiles.length) { + el.innerHTML = '
暂无备份记录
'; + return; + } + const sorted = [...lastLoadedBackupFiles].sort( + (a, b) => new Date(b.backupTime) - new Date(a.backupTime) + ); + const rows = sorted.map(f => { + const label = h(f.chatId || f.filename); + const title = h(f.chatId || f.filename); + const size = f.size ? (f.size / 1024 / 1024).toFixed(2) + 'MB' : '?'; + const time = f.backupTime ? new Date(f.backupTime).toLocaleString() : '?'; + const disabled = backupDeleteSupported ? '' : 'disabled'; + const btnTitle = backupDeleteSupported ? '' : h(backupDeleteUnsupportedReason); + const dataFile = h(f.filename); + const dataPath = h(f.serverPath || ''); + return `
+ ${label} + ${size} + ${time} + +
`; + }).join(''); + el.innerHTML = rows; + if (!backupDeleteSupported) { + $('backup-manager-status').textContent = '⚠️ 只读模式:' + backupDeleteUnsupportedReason; + } + el.querySelectorAll('[data-file]').forEach(btn => { + if (btn.disabled) return; + btn.onclick = () => { + if (!confirm(`确认删除此备份?\n${btn.dataset.file}`)) return; + $('backup-manager-status').textContent = '删除中...'; + btn.disabled = true; + postMsg('VECTOR_DELETE_BACKUP', { + filename: btn.dataset.file, + serverPath: btn.dataset.path, + }); + }; + }); + } })(); diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html index bff0d77..515bf5c 100644 --- a/modules/story-summary/story-summary.html +++ b/modules/story-summary/story-summary.html @@ -568,6 +568,11 @@

编辑

style="flex:1">☁️ 从服务器恢复向量
+
+ +
@@ -861,6 +866,28 @@

确认操作

+ + + diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 54fa799..07db4e3 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -89,7 +89,7 @@ import { } from "./vector/storage/state-store.js"; // vector io -import { exportVectors, importVectors, backupToServer, restoreFromServer } from "./vector/storage/vector-io.js"; +import { exportVectors, importVectors, backupToServer, restoreFromServer, fetchManifest, deleteServerBackup, isDeleteUnsupportedError } from "./vector/storage/vector-io.js"; import { invalidateLexicalIndex, warmupIndex, addDocumentsForFloor, removeDocumentsByFloor, addEventDocuments } from "./vector/retrieval/lexical-index.js"; @@ -182,6 +182,8 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // 向量提醒节流 let lastVectorWarningAt = 0; const VECTOR_WARNING_COOLDOWN_MS = 120000; // 2分钟内不重复提醒 +let backupDeleteSupported = true; +let backupDeleteUnsupportedReason = ''; const EXT_PROMPT_KEY = "LittleWhiteBox_StorySummary"; const MIN_INJECTION_DEPTH = 2; @@ -1499,6 +1501,58 @@ async function handleFrameMessage(event) { })(); break; + case "VECTOR_LIST_BACKUPS": + (async () => { + try { + const files = await fetchManifest(); + postToFrame({ + type: "VECTOR_LIST_RESULT", + files, + deleteSupported: backupDeleteSupported, + deleteUnsupportedReason: backupDeleteUnsupportedReason, + }); + } catch (e) { + postToFrame({ + type: "VECTOR_LIST_RESULT", + files: [], + deleteSupported: backupDeleteSupported, + deleteUnsupportedReason: backupDeleteUnsupportedReason, + }); + } + })(); + break; + + case "VECTOR_DELETE_BACKUP": + (async () => { + if (!backupDeleteSupported) { + postToFrame({ + type: "VECTOR_DELETE_BACKUP_RESULT", + success: false, + error: backupDeleteUnsupportedReason, + deleteSupported: false, + deleteUnsupportedReason: backupDeleteUnsupportedReason, + }); + return; + } + try { + await deleteServerBackup(data.filename, data.serverPath); + postToFrame({ type: "VECTOR_DELETE_BACKUP_RESULT", success: true }); + } catch (e) { + if (isDeleteUnsupportedError(e)) { + backupDeleteSupported = false; + backupDeleteUnsupportedReason = e.message || '宿主不支持删除接口'; + } + postToFrame({ + type: "VECTOR_DELETE_BACKUP_RESULT", + success: false, + error: e.message, + deleteSupported: backupDeleteSupported, + deleteUnsupportedReason: backupDeleteUnsupportedReason, + }); + } + })(); + break; + case "REQUEST_VECTOR_STATS": sendVectorStatsToFrame(); maybePreloadTokenizer(); diff --git a/modules/story-summary/vector/storage/vector-io.js b/modules/story-summary/vector/storage/vector-io.js index 7033cc1..446edb2 100644 --- a/modules/story-summary/vector/storage/vector-io.js +++ b/modules/story-summary/vector/storage/vector-io.js @@ -532,6 +532,26 @@ export async function backupToServer(onProgress) { throw new Error(`服务器返回 ${res.status}`); } + // 新增:安全读取 path 字段 + let uploadedPath = null; + try { + const resJson = await res.json(); + if (typeof resJson?.path === 'string') uploadedPath = resJson.path; + } catch (_) { /* JSON 解析失败时 uploadedPath 保持 null */ } + + // 新增:写清单(独立 try/catch,失败不影响原有备份返回) + try { + await upsertManifestEntry({ + filename, + serverPath: uploadedPath, + size: zipData.byteLength, + chatId, + backupTime: new Date().toISOString(), + }); + } catch (e) { + xbLog.warn(MODULE_ID, `清单写入失败(不影响备份结果): ${e.message}`); + } + const sizeMB = (zipData.byteLength / 1024 / 1024).toFixed(2); xbLog.info(MODULE_ID, `备份完成: ${filename} (${sizeMB}MB)`); @@ -717,3 +737,140 @@ export async function restoreFromServer(onProgress) { fingerprintMismatch, }; } + +// ═══════════════════════════════════════════════════════════════════════════ +// 备份清单管理 +// ═══════════════════════════════════════════════════════════════════════════ + +const BACKUP_MANIFEST = 'LWB_BackupManifest.json'; + +// 宽容解析:非数组/JSON 失败/字段异常时清洗,不抛错 +async function fetchManifest() { + try { + const res = await fetch(`/user/files/${BACKUP_MANIFEST}`, { + headers: getRequestHeaders(), + cache: 'no-cache', + }); + if (!res.ok) return []; + const raw = await res.json(); + if (!Array.isArray(raw)) return []; + return raw.map(normalizeManifestEntry).filter(Boolean); + } catch (_) { + return []; + } +} + +// 标准化单条条目字段,非法 filename 直接丢弃,其余字段降级 +function normalizeManifestEntry(raw) { + if (!raw || typeof raw !== 'object') return null; + const filename = typeof raw.filename === 'string' ? raw.filename : null; + if (!filename || !/^LWB_VectorBackup_[a-z0-9]+\.zip$/.test(filename)) return null; + return { + filename, + serverPath: typeof raw.serverPath === 'string' ? raw.serverPath : null, + size: typeof raw.size === 'number' ? raw.size : null, + chatId: typeof raw.chatId === 'string' ? raw.chatId : null, + backupTime: typeof raw.backupTime === 'string' ? raw.backupTime : null, + }; +} + +// 安全推导/校验 serverPath:缺失时推导,与 filename 不一致时拒绝 +function buildSafeServerPath(filename, serverPath) { + const expected = `user/files/${filename}`; + if (!serverPath) return expected; + if (serverPath !== expected) { + throw new Error(`serverPath 不安全: ${serverPath}`); + } + return serverPath; +} + +// 读-改(upsert by filename)-写回-验证,失败最多重试 2 次 +async function upsertManifestEntry({ filename, serverPath, size, chatId, backupTime }) { + const MAX_RETRIES = 3; + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + // 读取现有清单 + const existing = await fetchManifest(); + + // upsert by filename + const idx = existing.findIndex(e => e.filename === filename); + const entry = { filename, serverPath, size, chatId, backupTime }; + if (idx >= 0) { + existing[idx] = entry; + } else { + existing.push(entry); + } + + // 上传清单 + const json = JSON.stringify(existing, null, 2); + const base64 = uint8ToBase64(new TextEncoder().encode(json)); + const res = await fetch('/api/files/upload', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ name: BACKUP_MANIFEST, data: base64 }), + }); + if (!res.ok) throw new Error(`清单上传失败: ${res.status}`); + + // 写后立即重读验证 + const verified = await fetchManifest(); + if (verified.some(e => e.filename === filename)) return; + + // 最后一次仍失败才抛出 + if (attempt === MAX_RETRIES - 1) { + throw new Error('清单写入后验证失败,重试已耗尽'); + } + } +} + +// 删除前校验 + POST /api/files/delete + 更新清单 +async function deleteServerBackup(filename, serverPath) { + // 安全校验 + if (!/^LWB_VectorBackup_[a-z0-9]+\.zip$/.test(filename)) { + throw new Error(`非法文件名: ${filename}`); + } + const safePath = buildSafeServerPath(filename, serverPath || null); + + // 物理删除 + const res = await fetch('/api/files/delete', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ path: safePath }), + }); + if (!res.ok) { + const err = new Error(`删除失败: ${res.status}`); + err.status = res.status; + err.method = 'DELETE'; + throw err; + } + + // 更新清单(删除条目) + try { + const existing = await fetchManifest(); + const filtered = existing.filter(e => e.filename !== filename); + const json = JSON.stringify(filtered, null, 2); + const base64 = uint8ToBase64(new TextEncoder().encode(json)); + const upRes = await fetch('/api/files/upload', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ name: BACKUP_MANIFEST, data: base64 }), + }); + if (!upRes.ok) { + throw new Error('zip 已删除,但清单更新失败,请手动刷新'); + } + } catch (e) { + // zip 删成功但清单更新失败 → 抛"部分成功"错误 + const partialErr = new Error(e.message || 'zip 已删除,清单同步失败'); + partialErr.partial = true; + throw partialErr; + } +} + +// 集中判断 404/405/method not allowed/unsupported +function isDeleteUnsupportedError(err) { + if (!err) return false; + const status = err.status; + if (status === 404 || status === 405) return true; + const msg = String(err.message || '').toLowerCase(); + return msg.includes('method not allowed') || msg.includes('unsupported') || msg.includes('not found'); +} + +export { fetchManifest, deleteServerBackup, isDeleteUnsupportedError }; From 0613fedd5e090e16c0d6f9c119632ea493d598b0 Mon Sep 17 00:00:00 2001 From: LittleWhiteBox Dev Date: Tue, 17 Mar 2026 00:17:16 +0800 Subject: [PATCH 16/22] =?UTF-8?q?=E5=A4=87=E4=BB=BD=E7=AE=A1=E7=90=86=20Mo?= =?UTF-8?q?dal=20=E7=A7=BB=E8=87=B3=E7=88=B6=E7=AA=97=E5=8F=A3=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B1=82=E7=BA=A7=E4=B8=8E=E9=85=8D=E8=89=B2?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modal 从 iframe 移到父窗口 DOM(z-index:100000),不再被 settings modal 遮挡 - 改为白底深色文字,配色清晰可读 - 删除逻辑直接在父窗口调用,无需跨帧消息 - 简化 story-summary-ui.js,移除 modal 相关代码 Co-Authored-By: Claude Sonnet 4.6 --- modules/story-summary/story-summary-ui.js | 90 +--------- modules/story-summary/story-summary.html | 21 --- modules/story-summary/story-summary.js | 194 +++++++++++++++++----- 3 files changed, 152 insertions(+), 153 deletions(-) diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js index f530c7a..9faefce 100644 --- a/modules/story-summary/story-summary-ui.js +++ b/modules/story-summary/story-summary-ui.js @@ -104,9 +104,6 @@ let allLinks = []; let activeRelationTooltip = null; let lastRecallLogText = ''; - let backupDeleteSupported = true; - let backupDeleteUnsupportedReason = ''; - let lastLoadedBackupFiles = []; // ═══════════════════════════════════════════════════════════════════════════ // Messaging @@ -439,18 +436,7 @@ postMsg('VECTOR_RESTORE_SERVER'); }; - $('btn-manage-backups').onclick = () => { - $('backup-manager-modal').style.display = 'flex'; - $('backup-manager-status').textContent = '加载中...'; - postMsg('VECTOR_LIST_BACKUPS'); - }; - $('btn-close-backup-modal').onclick = () => { - $('backup-manager-modal').style.display = 'none'; - }; - $('btn-refresh-backups').onclick = () => { - $('backup-manager-status').textContent = '加载中...'; - postMsg('VECTOR_LIST_BACKUPS'); - }; + $('btn-manage-backups').onclick = () => postMsg('VECTOR_LIST_BACKUPS'); initAnchorUI(); postMsg('REQUEST_ANCHOR_STATS'); @@ -1550,27 +1536,6 @@ } break; - case 'VECTOR_LIST_RESULT': - backupDeleteSupported = d.deleteSupported !== false; - backupDeleteUnsupportedReason = d.deleteUnsupportedReason || ''; - renderBackupList(d.files); - $('backup-manager-status').textContent = ''; - break; - - case 'VECTOR_DELETE_BACKUP_RESULT': - if (!d.success) { - $('backup-manager-status').textContent = '删除失败: ' + (d.error || '未知'); - if (d.deleteSupported === false) { - backupDeleteSupported = false; - backupDeleteUnsupportedReason = d.deleteUnsupportedReason || '宿主不支持删除'; - renderBackupList(lastLoadedBackupFiles); - } - } else { - $('backup-manager-status').textContent = '已删除'; - postMsg('VECTOR_LIST_BACKUPS'); - } - break; - case 'RECALL_LOG': setRecallLog(d.text || ''); break; @@ -1848,57 +1813,4 @@ setHtml(container, html); } - // ═══════════════════════════════════════════════════════════════════════════ - // 备份管理弹窗 - // ═══════════════════════════════════════════════════════════════════════════ - - function renderBackupList(files) { - lastLoadedBackupFiles = files || []; - const el = $('backup-list-content'); - $('backup-count-badge').textContent = `(${lastLoadedBackupFiles.length})`; - if (!lastLoadedBackupFiles.length) { - el.innerHTML = '
暂无备份记录
'; - return; - } - const sorted = [...lastLoadedBackupFiles].sort( - (a, b) => new Date(b.backupTime) - new Date(a.backupTime) - ); - const rows = sorted.map(f => { - const label = h(f.chatId || f.filename); - const title = h(f.chatId || f.filename); - const size = f.size ? (f.size / 1024 / 1024).toFixed(2) + 'MB' : '?'; - const time = f.backupTime ? new Date(f.backupTime).toLocaleString() : '?'; - const disabled = backupDeleteSupported ? '' : 'disabled'; - const btnTitle = backupDeleteSupported ? '' : h(backupDeleteUnsupportedReason); - const dataFile = h(f.filename); - const dataPath = h(f.serverPath || ''); - return `
- ${label} - ${size} - ${time} - -
`; - }).join(''); - el.innerHTML = rows; - if (!backupDeleteSupported) { - $('backup-manager-status').textContent = '⚠️ 只读模式:' + backupDeleteUnsupportedReason; - } - el.querySelectorAll('[data-file]').forEach(btn => { - if (btn.disabled) return; - btn.onclick = () => { - if (!confirm(`确认删除此备份?\n${btn.dataset.file}`)) return; - $('backup-manager-status').textContent = '删除中...'; - btn.disabled = true; - postMsg('VECTOR_DELETE_BACKUP', { - filename: btn.dataset.file, - serverPath: btn.dataset.path, - }); - }; - }); - } })(); diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html index 515bf5c..a336d0c 100644 --- a/modules/story-summary/story-summary.html +++ b/modules/story-summary/story-summary.html @@ -867,27 +867,6 @@

确认操作

- - diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 07db4e3..61a6bc9 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -1505,50 +1505,9 @@ async function handleFrameMessage(event) { (async () => { try { const files = await fetchManifest(); - postToFrame({ - type: "VECTOR_LIST_RESULT", - files, - deleteSupported: backupDeleteSupported, - deleteUnsupportedReason: backupDeleteUnsupportedReason, - }); - } catch (e) { - postToFrame({ - type: "VECTOR_LIST_RESULT", - files: [], - deleteSupported: backupDeleteSupported, - deleteUnsupportedReason: backupDeleteUnsupportedReason, - }); - } - })(); - break; - - case "VECTOR_DELETE_BACKUP": - (async () => { - if (!backupDeleteSupported) { - postToFrame({ - type: "VECTOR_DELETE_BACKUP_RESULT", - success: false, - error: backupDeleteUnsupportedReason, - deleteSupported: false, - deleteUnsupportedReason: backupDeleteUnsupportedReason, - }); - return; - } - try { - await deleteServerBackup(data.filename, data.serverPath); - postToFrame({ type: "VECTOR_DELETE_BACKUP_RESULT", success: true }); + showBackupManagerModal(files); } catch (e) { - if (isDeleteUnsupportedError(e)) { - backupDeleteSupported = false; - backupDeleteUnsupportedReason = e.message || '宿主不支持删除接口'; - } - postToFrame({ - type: "VECTOR_DELETE_BACKUP_RESULT", - success: false, - error: e.message, - deleteSupported: backupDeleteSupported, - deleteUnsupportedReason: backupDeleteUnsupportedReason, - }); + showBackupManagerModal([]); } })(); break; @@ -2009,6 +1968,155 @@ function unregisterEvents() { document.removeEventListener("keydown", onSendKeydown, true); } +// ═══════════════════════════════════════════════════════════════════════════ +// 备份管理 Modal(渲染在父窗口,确保层级在 settings modal 之上) +// ═══════════════════════════════════════════════════════════════════════════ + +function showBackupManagerModal(initialFiles) { + document.getElementById('lwb-backup-manager-modal')?.remove(); + + const overlay = document.createElement('div'); + overlay.id = 'lwb-backup-manager-modal'; + overlay.style.cssText = [ + 'position:fixed', 'inset:0', 'background:rgba(0,0,0,.55)', + 'z-index:100000', 'display:flex', 'align-items:center', 'justify-content:center', + ].join(';'); + + const box = document.createElement('div'); + box.style.cssText = [ + 'background:#fff', 'color:#222', 'border-radius:8px', + 'width:min(520px,92vw)', 'padding:18px', + 'max-height:80vh', 'display:flex', 'flex-direction:column', + 'box-shadow:0 8px 32px rgba(0,0,0,.35)', 'font-size:14px', + ].join(';'); + + // Header + const header = document.createElement('div'); + header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:10px'; + const title = document.createElement('span'); + title.style.cssText = 'font-weight:700;font-size:15px'; + title.textContent = '服务器向量备份'; + const badge = document.createElement('span'); + badge.id = 'lwb-backup-badge'; + badge.style.cssText = 'opacity:0.5;font-size:0.85em;margin-left:4px'; + title.appendChild(badge); + + const btnRow = document.createElement('div'); + btnRow.style.cssText = 'display:flex;gap:6px'; + + const btnRefresh = document.createElement('button'); + btnRefresh.className = 'btn btn-sm'; + btnRefresh.textContent = '刷新'; + + const btnClose = document.createElement('button'); + btnClose.className = 'btn btn-sm'; + btnClose.textContent = '✕'; + btnClose.onclick = () => overlay.remove(); + + btnRow.append(btnRefresh, btnClose); + header.append(title, btnRow); + + // List area + const listEl = document.createElement('div'); + listEl.id = 'lwb-backup-list'; + listEl.style.cssText = 'overflow-y:auto;flex:1;min-height:60px'; + + // Status bar + const statusEl = document.createElement('div'); + statusEl.id = 'lwb-backup-status'; + statusEl.style.cssText = 'margin-top:8px;font-size:0.82em;color:#666;min-height:1em'; + + box.append(header, listEl, statusEl); + overlay.appendChild(box); + document.body.appendChild(overlay); + + // Close on backdrop click + overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); + + function setStatus(text, isError) { + statusEl.textContent = text; + statusEl.style.color = isError ? '#c00' : '#666'; + } + + function renderList(files) { + badge.textContent = `(${files.length})`; + if (!files.length) { + listEl.innerHTML = '
暂无备份记录
'; + return; + } + const sorted = [...files].sort((a, b) => new Date(b.backupTime) - new Date(a.backupTime)); + listEl.replaceChildren(); + sorted.forEach(f => { + const row = document.createElement('div'); + row.style.cssText = [ + 'display:flex', 'gap:8px', 'align-items:center', 'padding:6px 2px', + 'border-bottom:1px solid #e8e8e8', 'font-size:0.82em', + ].join(';'); + + const label = document.createElement('span'); + label.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#333'; + label.title = f.chatId || f.filename; + label.textContent = f.chatId || f.filename; + + const size = document.createElement('span'); + size.style.cssText = 'white-space:nowrap;color:#555'; + size.textContent = f.size ? (f.size / 1024 / 1024).toFixed(2) + 'MB' : '?'; + + const time = document.createElement('span'); + time.style.cssText = 'white-space:nowrap;color:#888'; + time.textContent = f.backupTime ? new Date(f.backupTime).toLocaleString() : '?'; + + const btnDel = document.createElement('button'); + btnDel.className = 'btn btn-sm'; + btnDel.style.cssText = 'padding:1px 10px;flex-shrink:0;color:#c00;border-color:#c00'; + btnDel.textContent = '删'; + btnDel.onclick = async () => { + if (!confirm(`确认删除此备份?\n${f.filename}`)) return; + setStatus('删除中...'); + btnDel.disabled = true; + try { + await deleteServerBackup(f.filename, f.serverPath); + setStatus('已删除'); + const updated = await fetchManifest(); + renderList(updated); + } catch (e) { + if (isDeleteUnsupportedError(e)) { + backupDeleteSupported = false; + backupDeleteUnsupportedReason = e.message || '宿主不支持删除接口'; + setStatus('⚠️ 只读模式:' + backupDeleteUnsupportedReason, true); + // 禁用所有删除按钮 + listEl.querySelectorAll('button').forEach(b => { b.disabled = true; }); + } else { + setStatus('删除失败: ' + (e.message || '未知'), true); + btnDel.disabled = false; + } + } + }; + + row.append(label, size, time, btnDel); + listEl.appendChild(row); + }); + + if (!backupDeleteSupported) { + setStatus('⚠️ 只读模式:' + backupDeleteUnsupportedReason, true); + listEl.querySelectorAll('button').forEach(b => { b.disabled = true; }); + } + } + + btnRefresh.onclick = async () => { + setStatus('加载中...'); + try { + const files = await fetchManifest(); + renderList(files); + setStatus(''); + } catch (e) { + setStatus('加载失败: ' + e.message, true); + } + }; + + renderList(initialFiles); +} + // ═══════════════════════════════════════════════════════════════════════════ // Toggle 监听 // ═══════════════════════════════════════════════════════════════════════════ From 13d79e411aaa2d26c6df77e7b801a52452fcd145 Mon Sep 17 00:00:00 2001 From: LittleWhiteBox Dev Date: Tue, 17 Mar 2026 00:28:04 +0800 Subject: [PATCH 17/22] =?UTF-8?q?=E5=88=A0=E9=99=A4=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E6=97=B6=E8=87=AA=E5=8A=A8=E6=B8=85=E7=90=86=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E5=90=91=E9=87=8F=E5=A4=87=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vector-io.js:导出 getBackupFilename - story-summary.js:监听 CHAT_DELETED / GROUP_CHAT_DELETED,静默删除对应 zip 和清单条目 Co-Authored-By: Claude Sonnet 4.6 --- modules/story-summary/story-summary.js | 20 ++++++++++++++++++- .../story-summary/vector/storage/vector-io.js | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 61a6bc9..00f6755 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -89,7 +89,7 @@ import { } from "./vector/storage/state-store.js"; // vector io -import { exportVectors, importVectors, backupToServer, restoreFromServer, fetchManifest, deleteServerBackup, isDeleteUnsupportedError } from "./vector/storage/vector-io.js"; +import { exportVectors, importVectors, backupToServer, restoreFromServer, fetchManifest, deleteServerBackup, isDeleteUnsupportedError, getBackupFilename } from "./vector/storage/vector-io.js"; import { invalidateLexicalIndex, warmupIndex, addDocumentsForFloor, removeDocumentsByFloor, addEventDocuments } from "./vector/retrieval/lexical-index.js"; @@ -1948,6 +1948,10 @@ function registerEvents() { events.on(event_types.GENERATION_STARTED, handleGenerationStarted); events.on(event_types.GENERATION_STOPPED, clearExtensionPrompt); events.on(event_types.GENERATION_ENDED, clearExtensionPrompt); + + // 聊天删除时清理对应的服务器向量备份 + events.on(event_types.CHAT_DELETED, handleChatDeleted); + events.on(event_types.GROUP_CHAT_DELETED, handleChatDeleted); } function unregisterEvents() { @@ -1968,6 +1972,20 @@ function unregisterEvents() { document.removeEventListener("keydown", onSendKeydown, true); } +// ═══════════════════════════════════════════════════════════════════════════ +// 聊天删除时自动清理服务器向量备份 +// ═══════════════════════════════════════════════════════════════════════════ + +async function handleChatDeleted(chatId) { + try { + const filename = getBackupFilename(chatId); + await deleteServerBackup(filename, null); + xbLog.info(MODULE_ID, `聊天删除,已清理服务器备份: ${filename}`); + } catch (_) { + // 文件不存在或宿主不支持删除,静默处理 + } +} + // ═══════════════════════════════════════════════════════════════════════════ // 备份管理 Modal(渲染在父窗口,确保层级在 settings modal 之上) // ═══════════════════════════════════════════════════════════════════════════ diff --git a/modules/story-summary/vector/storage/vector-io.js b/modules/story-summary/vector/storage/vector-io.js index 446edb2..27eab69 100644 --- a/modules/story-summary/vector/storage/vector-io.js +++ b/modules/story-summary/vector/storage/vector-io.js @@ -873,4 +873,4 @@ function isDeleteUnsupportedError(err) { return msg.includes('method not allowed') || msg.includes('unsupported') || msg.includes('not found'); } -export { fetchManifest, deleteServerBackup, isDeleteUnsupportedError }; +export { fetchManifest, deleteServerBackup, isDeleteUnsupportedError, getBackupFilename }; From d9d475ba72076387bf87109c095a091060c76c69 Mon Sep 17 00:00:00 2001 From: LittleWhiteBox Dev Date: Tue, 17 Mar 2026 00:31:40 +0800 Subject: [PATCH 18/22] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20serverPath=20?= =?UTF-8?q?=E5=90=AB=E5=89=8D=E5=AF=BC=E6=96=9C=E6=9D=A0=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildSafeServerPath 比较前 strip 前导 /,upsertManifestEntry 写入前同样 normalize, 确保清单和校验逻辑使用统一格式 Co-Authored-By: Claude Sonnet 4.6 --- modules/story-summary/vector/storage/vector-io.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/story-summary/vector/storage/vector-io.js b/modules/story-summary/vector/storage/vector-io.js index 27eab69..0c5d8eb 100644 --- a/modules/story-summary/vector/storage/vector-io.js +++ b/modules/story-summary/vector/storage/vector-io.js @@ -778,14 +778,16 @@ function normalizeManifestEntry(raw) { function buildSafeServerPath(filename, serverPath) { const expected = `user/files/${filename}`; if (!serverPath) return expected; - if (serverPath !== expected) { + const normalized = serverPath.replace(/^\/+/, ''); + if (normalized !== expected) { throw new Error(`serverPath 不安全: ${serverPath}`); } - return serverPath; + return normalized; } // 读-改(upsert by filename)-写回-验证,失败最多重试 2 次 async function upsertManifestEntry({ filename, serverPath, size, chatId, backupTime }) { + if (typeof serverPath === 'string') serverPath = serverPath.replace(/^\/+/, ''); const MAX_RETRIES = 3; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { // 读取现有清单 From 5b2d65bdf2aecda701fc0d023a9103f2f570e11a Mon Sep 17 00:00:00 2001 From: LittleWhiteBox Dev Date: Tue, 17 Mar 2026 00:35:59 +0800 Subject: [PATCH 19/22] =?UTF-8?q?normalizeManifestEntry=20=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E6=97=B6=E5=90=8C=E6=AD=A5=20strip=20serverPath=20?= =?UTF-8?q?=E5=89=8D=E5=AF=BC=E6=96=9C=E6=9D=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补全斜杠 normalize 的覆盖点:写入(upsertManifestEntry)、校验(buildSafeServerPath)、 读取(normalizeManifestEntry)三处统一,旧清单条目自动修正 Co-Authored-By: Claude Sonnet 4.6 --- modules/story-summary/vector/storage/vector-io.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/story-summary/vector/storage/vector-io.js b/modules/story-summary/vector/storage/vector-io.js index 0c5d8eb..90327ee 100644 --- a/modules/story-summary/vector/storage/vector-io.js +++ b/modules/story-summary/vector/storage/vector-io.js @@ -765,9 +765,10 @@ function normalizeManifestEntry(raw) { if (!raw || typeof raw !== 'object') return null; const filename = typeof raw.filename === 'string' ? raw.filename : null; if (!filename || !/^LWB_VectorBackup_[a-z0-9]+\.zip$/.test(filename)) return null; + const rawPath = typeof raw.serverPath === 'string' ? raw.serverPath.replace(/^\/+/, '') : null; return { filename, - serverPath: typeof raw.serverPath === 'string' ? raw.serverPath : null, + serverPath: rawPath, size: typeof raw.size === 'number' ? raw.size : null, chatId: typeof raw.chatId === 'string' ? raw.chatId : null, backupTime: typeof raw.backupTime === 'string' ? raw.backupTime : null, From 072992c6e58c10e0ab3de404bee9445790daa1b4 Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:34:43 +0800 Subject: [PATCH 20/22] =?UTF-8?q?=E9=87=8D=E8=A6=81NPC=E7=94=9F=E6=88=90?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=EF=BC=9A=E6=8B=86=E5=88=86=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=8C=89=E9=92=AE=20+=20=E5=AE=8C=E6=95=B4=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E6=A1=A3=E6=A1=88=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 陌路人卡片"添加"按钮拆为"重要"(importantNpc)和"背景板"(npc)两个 - 新增 importantNpc 生成路径,传递 npcType 贯穿 genAddCt → CHECK_STRANGER_WORLDBOOK_RESULT → GENERATE_NPC_RESULT - 新增 importantNpc JSON 模板:白描外貌、世界观适配、性格调色盘+衍生、台词示例、结构化二次解释 - 新增 importantNpc UAUA 提示词:内嵌白描规则+正反示范、调色盘衍生写法指导 Co-Authored-By: Claude Sonnet 4.6 --- modules/story-outline/story-outline-prompt.js | 72 +++++++++++++++++++ modules/story-outline/story-outline.html | 5 +- modules/story-outline/story-outline.js | 9 ++- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/modules/story-outline/story-outline-prompt.js b/modules/story-outline/story-outline-prompt.js index 56e0810..ffd81d1 100644 --- a/modules/story-outline/story-outline-prompt.js +++ b/modules/story-outline/story-outline-prompt.js @@ -54,6 +54,52 @@ const DEFAULT_JSON_TEMPLATES = { "stance": "核心态度·具体表现。例如:'中立·唯利是图'、'友善·盲目崇拜' 或 '敌对·疯狂'", "secret": "该角色掌握的一个关键信息、道具或秘密。必须结合'剧情大纲'生成,作为一个潜在的剧情钩子。" } +}`, + importantNpc: `{ + "name": "角色全名", + "aliases": ["别名1", "别名2", "英文名/拼音"], + "intro": "白描一句话:外貌+身份。仅用名词和动词,禁止形容词和比喻。例:'黑色长直发过腰,左眼下泪痣,着灰色风衣的赏金猎人。'", + "appearance": { + "build": "体型白描(如:比{{user}}高一个头。宽肩,窄腰。)", + "face": "面部白描(如:颧骨高,下颌线锐利。左眉尾有一道旧疤。)", + "hair_and_eyes": "发型发色、瞳色", + "marks": "显著标记——疤痕、痣、纹身等,无则写'无'", + "attire": "当前穿着" + }, + "background": "角色来历与当前处境。必须交代因果链:什么过去→塑造了什么性格→为什么出现在当前场景。200字左右。", + "world_adaptation": {}, + "personality_palette": { + "base_color": "底色——驱动一切行为的最底层核心性格(如:恐惧、控制欲、孤独)", + "main_colors": ["主色调1", "主色调2——日常最常表现出的性格"], + "accents": ["点缀——不常见但在特定情境下浮现的性格"], + "derivatives": [ + "[主色调1]衍生一:(写具体场景+具体行为,不是定义。错误:'她很温柔';正确:'会在{{user}}加班时默默端一杯温水放在桌上,不说话,放下就走')", + "[主色调1]衍生二:(另一个场景的表现,衍生之间可以互相矛盾——这才是真实的人)", + "[主色调2]衍生一:...", + "[底色]衍生一:(底色通常不轻易暴露,写什么条件下会泄漏出来)", + "[点缀]衍生一:..." + ] + }, + "speaking": { + "style": "语气、语速、口癖、惯用词", + "samples": ["台词示例1——展现主色调", "台词示例2——展现底色泄漏", "台词示例3——展现对{{user}}的态度"], + "attitude_to_user": "对{{user}}的态度及其原因" + }, + "understanding": [ + { + "about": "某个性格特质或行为模式", + "clarification": "这个特质的真正含义是……不是……在什么情况下会……常见误读是……正确理解是……" + }, + { + "about": "另一个容易被AI误读的特质", + "clarification": "解释动机而非重复描述。预判AI可能的补全方向并提前纠正。" + } + ], + "game_data": { + "stance": "核心态度·具体表现(如:'中立·唯利是图'、'友善·盲目崇拜'、'敌对·疯狂')", + "secret": "角色掌握的一个关键秘密/信息/道具。必须结合剧情大纲生成,作为剧情钩子。", + "motivation": "核心驱动力与行动优先级准则" + } }`, stranger: `[{ "name": "角色名", "location": "当前地点", "info": "一句话简介" }]`, worldGenStep1: `{ @@ -258,6 +304,31 @@ const DEFAULT_PROMPTS = { u2: v => `${worldInfo}\n\n${history(v.historyCount)}\n\n剧情秘密大纲(*从这里提取线索赋予角色秘密*):\n${wrap('story_outline', v.storyOutline) || '\n(无)\n'}\n\n需要生成:【${v.strangerName} - ${v.strangerInfo}】\n\n输出要求:\n1. 必须是合法 JSON\n2. 使用标准 JSON 语法:所有键名和字符串都使用半角双引号 "\n3. 文本字段(intro/background/persona/game_data 等)中,如需表示引号,请使用单引号或中文引号「」或"",不要使用半角双引号 "\n4. aliases须含简称或绰号\n\n模板:${JSON_TEMPLATES.npc}`, a2: () => `了解,开始生成JSON:` }, + importantNpc: { + u1: v => `你是TRPG重要角色档案生成器。将陌生人【${v.strangerName} - ${v.strangerInfo}】扩充为剧情核心角色的完整档案。 + +核心写作原则: +1. **基础信息用绝对零度白描**:只写客观事实,不用形容词/比喻/模糊词(似乎、仿佛、宛如)。用名词和动词直接呈现。 + × "她有一头好看柔顺的黑色长发" → √ "黑色长直发,过腰。瞳色黑。" + × "他身材魁梧,给人压迫感" → √ "身高比{{user}}高一个头。宽肩,厚背。" + +2. **性格用调色盘+衍生展开**:人的性格像调色盘,底色是最深层驱动力,主色调是日常表现,点缀是偶尔闪现的侧面。每种性格必须通过"衍生"展开为具体场景行为——不是贴标签,是写"在什么情况下会做什么"。衍生之间可以互相矛盾,这才是真实的人。 + × "温柔衍生:她很温柔,对人很好。"(标签重复) + √ "温柔衍生:生气时——真正生气基本都和{{user}}有关。当有人欺负{{user}},她会一把拉住{{user}}让其靠自己,然后用冰冷目光看对方。" + +3. **台词示例**:3句具体台词,分别展现主色调、底色泄漏、对{{user}}的态度。 + +4. **二次解释(understanding数组)**:逐条针对角色最容易被误读的性格特质,写结构化纠偏。每条包含about(哪个特质)和clarification(真正含义、不是什么、在什么情况下怎样)。至少2条。这不是重复调色盘,是解释动机和预判误读。 + × "关于温柔:她很温柔,对人好。"(重复调色盘) + √ "关于乐观的双重性:和{{user}}在一起时是真实的,和其他人相处时是维持人设的假象。脆弱时只会在{{user}}面前表现。" + +5. **世界观适配(world_adaptation对象)**:根据故事世界观动态生成键值对。修仙世界→灵根、境界、功法等字段;赛博世界→义体部位、型号、副作用等字段;现代世界→可留空对象。不预设固定字段,由你根据世界观判断需要什么。 + +基于世界观、剧情大纲和现有角色关系,输出严格JSON。`, + a1: () => `明白。我将严格遵循白描原则和调色盘衍生写法,按JSON模板输出完整角色档案,不含多余文本。`, + u2: v => `${worldInfo}\n\n${history(v.historyCount)}\n\n剧情秘密大纲(*从这里提取线索赋予角色秘密和动机*):\n${wrap('story_outline', v.storyOutline) || '\n(无)\n'}\n\n需要生成:【${v.strangerName} - ${v.strangerInfo}】\n\n输出要求:\n1. 必须是合法 JSON\n2. 使用标准 JSON 语法:所有键名和字符串都使用半角双引号 "\n3. 文本字段中如需表示引号,请使用单引号或中文引号「」或"",不要使用半角双引号 "\n4. aliases须含简称或绰号\n5. personality_palette.derivatives 至少5条,每条都是具体场景+具体行为\n6. speaking.samples 须3句具体台词\n7. understanding数组至少2条,每条须含about和clarification\n8. world_adaptation根据世界观动态生成键值对,无特殊体系则输出空对象{}\n9. 总输出约800-1500字\n\n模板:${JSON_TEMPLATES.importantNpc}`, + a2: () => `了解,开始以白描+调色盘衍生法生成重要角色档案JSON:` + }, stranger: { u1: v => `你是TRPG数据整理助手。从剧情文本中提取{{user}}遇到的陌生人/NPC,整理为JSON数组。`, a1: () => `明白。请提供【世界观】和【剧情经历】,我将提取角色并以JSON数组输出。`, @@ -585,6 +656,7 @@ export const buildSmsMessages = v => build('sms', v); export const buildSummaryMessages = v => build('summary', v); export const buildInviteMessages = v => build('invite', v); export const buildNpcGenerationMessages = v => build('npc', v); +export const buildImportantNpcGenerationMessages = v => build('importantNpc', v); export const buildExtractStrangersMessages = v => build('stranger', v); export const buildWorldGenStep1Messages = v => build('worldGenStep1', v); export const buildWorldGenStep2Messages = v => build('worldGenStep2', v); diff --git a/modules/story-outline/story-outline.html b/modules/story-outline/story-outline.html index bfeb786..d64c941 100644 --- a/modules/story-outline/story-outline.html +++ b/modules/story-outline/story-outline.html @@ -995,7 +995,8 @@ .ct-acts { display: flex; - gap: 10px + flex-wrap: wrap; + gap: 6px } .ct-acts .btn { @@ -2870,7 +2871,7 @@

操作结果

(() => { const t = getStoredTheme(); "dark" === t && setTheme("dark") })(); $("btn-theme")?.addEventListener("click", toggleTheme); - const inds = popup.querySelectorAll(".pop-h-ind span"), setPopH = t => { const e = snaps(); popH = Math.max(e[0], Math.min(.85 * innerHeight, t)), popup.style.height = popH + "px", popLv = e.map(((t, e) => [e, Math.abs(popH - t)])).sort(((t, e) => t[1] - e[1]))[0][0], inds.forEach(((t, e) => t.classList.toggle("act", e === popLv))) }, snapTo = t => { popLv = Math.max(0, Math.min(2, t)), setPopH(snaps()[popLv]) }, openPop = (t = 1) => { popup.classList.add("act"), snapTo(t) }; $("side-pop-handle").onclick = () => sidePop.classList.toggle("act"), $("pop-hd").onmousedown = t => { popDrag = !0, popSY = t.clientY, popSH = popH || snaps()[1], popup.classList.add("drag"), t.preventDefault() }, $("pop-hd").ontouchstart = t => { t.preventDefault(), popDrag = !0, popSY = t.touches[0].clientY, popSH = popH || snaps()[1], popup.classList.add("drag") }, document.onmousemove = t => { popDrag && setPopH(popSH + popSY - t.clientY) }, document.ontouchmove = t => { popDrag && t.touches.length && (t.preventDefault(), setPopH(popSH + popSY - t.touches[0].clientY)) }; const endDrag = () => { popDrag && (popDrag = !1, popup.classList.remove("drag"), snapTo(popLv)) }; document.onmouseup = endDrag, document.ontouchend = endDrag, document.ontouchcancel = endDrag; const bindLinks = t => t.querySelectorAll(".loc-lk").forEach((t => t.onclick = e => { e.stopPropagation(); const s = t.dataset.loc, o = nodes.find((t => t.name === s)); if (o) return panTo(o), void showInfo(o); const a = getCurInside(), n = a?.nodes?.find((t => t.name === s)); n && ($("info-t").textContent = n.name, $("info-c").textContent = n.info || "暂无信息...", $("tip").classList.add("show"), $("btn-goto").classList.remove("show"), isMob() && ($("mob-info-t").textContent = n.name, $("mob-info-c").textContent = n.info || "暂无信息...", popup.classList.contains("act") || openPop(1))) })), bindFold = t => t.querySelector(".fold-h").onclick = () => t.classList.toggle("exp"); $$(".modal-bd,.modal-x,.m-cancel").forEach((t => t.onclick = () => t.closest(".modal").classList.remove("act"))); const openChat = t => { chatTgt = t, $("chat-av").textContent = t.avatar, $("chat-av").style.background = t.color, $("chat-nm").textContent = t.name, $("chat-st").textContent = t.online ? "● 在线" : t.location, !t.worldbookUid || t.messages && t.messages.length ? renderMsgs() : post("LOAD_SMS_HISTORY", { worldbookUid: t.worldbookUid }), chat.classList.add("act"), $("chat-in").focus() }, closeChat = () => { chat.classList.remove("act"), chatTgt = null, smsGen = !1 }, renderMsgs = () => { if (!chatTgt) return; const t = chatTgt.messages || [], e = chatTgt.summarizedCount || 0; let s = ""; t.length ? t.forEach(((t, o) => { e > 0 && o === e && (s += '
—— 以上为已总结消息 ——
'), s += `
${escHtml(stripXml(t.text))}
` })) : s = '
暂无消息,开始聊天吧
', $("chat-msgs").innerHTML = s, $("chat-msgs").scrollTop = $("chat-msgs").scrollHeight, $("chat-compress").disabled = t.length - e < 2 || smsGen, $("chat-back").disabled = !t.length || smsGen }, sendMsg = () => { const t = $("chat-in").value.trim(); if (!t || !chatTgt || smsGen) return; chatTgt.messages = chatTgt.messages || [], chatTgt.messages.push({ type: "sent", text: t }), $("chat-in").value = "", renderMsgs(), smsGen = !0, $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !0, chatTgt.messages.push({ type: "received", text: "正在输入...", typing: !0 }), renderMsgs(); const e = Req.create("sms"); Req.set("sms", { tgt: chatTgt }), post("SEND_SMS", { requestId: e, contactName: chatTgt.name, worldbookUid: chatTgt.worldbookUid, userMessage: t, chatHistory: chatTgt.messages.filter((t => !t.typing)).slice(-20), summarizedCount: chatTgt.summarizedCount || 0 }) }, isCharCardContact = t => "__CHARACTER_CARD__" === t?.worldbookUid, contactsForSave = () => (D.contacts.contacts || []).filter((t => !isCharCardContact(t))), saveCt = () => post("SAVE_CONTACTS", { contacts: contactsForSave(), strangers: D.contacts.strangers }), saveChat = t => post("SAVE_SMS_HISTORY", { worldbookUid: t.worldbookUid, contactName: t.name, messages: t.messages.filter((t => !t.typing)), summarizedCount: t.summarizedCount || 0 }); $("chat-x").onclick = closeChat, $("chat-back").onclick = () => { if (!chatTgt || smsGen) return; const t = chatTgt.messages || []; if (t.length) { for (t.pop(); t[t.length - 1]?.typing;)t.pop(); chatTgt.summarizedCount = Math.min(chatTgt.summarizedCount || 0, t.length), renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt) } }, $("chat-clr").onclick = () => { chatTgt && confirm(`确定要清空与 ${chatTgt.name} 的所有聊天记录吗?`) && (chatTgt.messages = [], chatTgt.summarizedCount = 0, renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt)) }, $("chat-compress").onclick = () => { if (!chatTgt || smsGen) return; const t = chatTgt.messages || [], e = chatTgt.summarizedCount || 0, s = t.slice(e); if (s.length < 2) return void alert("至少需要2条未总结的消息才能压缩"); if (!confirm(`确定要压缩总结 ${s.length} 条消息吗?`)) return; smsGen = !0, $("chat-compress").disabled = $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !0; const o = Req.create("compress"); Req.set("compress", { tgt: chatTgt }), post("COMPRESS_SMS", { requestId: o, contactName: chatTgt.name, worldbookUid: chatTgt.worldbookUid, messages: t.filter((t => !t.typing)), summarizedCount: e }) }, $("chat-send").onclick = sendMsg; const chatIn = $("chat-in");["keydown", "keypress", "keyup"].forEach((t => chatIn.addEventListener(t, (t => t.stopPropagation())))), chatIn.addEventListener("keydown", (t => { "Enter" !== t.key || t.shiftKey || (t.preventDefault(), sendMsg()) })); const openInv = t => { invTgt = t, selLoc = null, $("inv-t").textContent = `邀请:${t.name}`, $("loc-list").innerHTML = D.maps.outdoor.nodes.map((t => `
${h(t.name)}
${h(t.info || "")}
`)).join(""), $$("#loc-list .loc-i").forEach((t => t.onclick = () => { $$("#loc-list .loc-i").forEach((t => t.classList.remove("sel"))), t.classList.add("sel"), selLoc = t.dataset.n })), openM("m-invite") }; $("inv-ok").onclick = () => { if (!selLoc || !invTgt) return; const t = $("inv-ok"); BtnState.load(t, "询问中..."); const e = Req.create("invite"), s = canonicalLoc(selLoc); Req.set("invite", { contact: invTgt, loc: s, btn: t }), post("SEND_INVITE", { requestId: e, contactName: invTgt.name, contactUid: invTgt.worldbookUid, targetLocation: s, smsHistory: (invTgt.messages || []).map((t => "sent" === t.type ? `{{user}}: ${t.text}` : `${invTgt.name}: ${t.text}`)).join("\n") }) }; let addCtState = { uid: "", name: "", keys: [] }; const resetAddCt = () => { addCtState = { uid: "", name: "", keys: [] }, $("add-uid").value = "", $("add-name").value = "", $("add-name").innerHTML = '', $("name-select-group").style.display = "none", $("uid-check-err").classList.remove("vis"), $("add-ct-ok").disabled = !0, BtnState.reset($("btn-check-uid"), ' 检查') }, showUidErr = t => { $("uid-check-err").textContent = t, $("uid-check-err").classList.add("vis") }; $("btn-check-uid").onclick = () => { const t = $("add-uid").value.trim(); t ? ($("uid-check-err").classList.remove("vis"), BtnState.load($("btn-check-uid"), "检查中"), $("name-select-group").style.display = "none", $("add-ct-ok").disabled = !0, addCtState.uid = t, post("CHECK_WORLDBOOK_UID", { uid: t, requestId: Req.create("uidck") })) : showUidErr("请输入UID") }, $("btn-add-ct").onclick = () => { resetAddCt(), openM("m-add-ct") }, $("add-ct-ok").onclick = () => { const t = addCtState.uid || $("add-uid").value.trim(), e = addCtState.name || $("add-name").value.trim(); t && e && (D.contacts.contacts.some((e => e.worldbookUid === t)) ? showUidErr("该联络人已存在") : (D.contacts.contacts.push({ name: e, avatar: e[0], color: "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: "未知", worldbookUid: t, messages: [] }), saveCt(), closeM("m-add-ct"), render())) }; const genAddCt = (t, e, s) => { BtnState.load(s, "检查中"); const o = Req.create("stgwb"); Req.set("stgwb", { name: t, info: e, btn: s }), post("CHECK_STRANGER_WORLDBOOK", { requestId: o, strangerName: t }) }; function canonicalLoc(t) { return String(t || "").trim().replace(/^\u90ae\u8f6e/, "") } $("btn-refresh-strangers").onclick = () => { const t = $("btn-refresh-strangers"); BtnState.load(t, ""); const e = Req.create("extract"); Req.set("extract", { btn: t }), post("EXTRACT_STRANGERS", { requestId: e, existingContacts: D.contacts.contacts, existingStrangers: D.contacts.strangers }) }, $("world-gen-ok").onclick = () => { const t = $("world-gen-ok"), e = $("world-gen-status"); BtnState.load(t, "生成中"), e.style.display = "block", e.textContent = "正在生成世界数据,请稍候...", post("GENERATE_WORLD", { requestId: Req.create("wgen"), playerRequests: $("world-gen-req").value.trim() }) }, $("world-sim-ok").onclick = () => { if (!D.meta && !D.timeline && !D.maps?.outdoor) return void alert("请先生成世界数据,再进行推演"); const t = $("world-sim-ok"), e = $("world-sim-status"); BtnState.load(t, "推演中"), e.style.display = "block", e.style.color = "var(--ok)", e.textContent = "正在分析玩家行为并推演世界变化...", post("SIMULATE_WORLD", { requestId: Req.create("wsim"), currentData: JSON.stringify({ meta: D.meta, timeline: D.timeline, world: D.world, maps: D.maps }, null, 2) }) }, $("btn-deduce").onclick = () => openM("m-world-gen"), $("btn-simulate").onclick = () => openM("m-world-sim"), $("btn-side-menu-toggle").onclick = () => { const t = $("side-menu-panel"), e = $("btn-side-menu-toggle"); t.classList.toggle("show"), e.classList.toggle("act", t.classList.contains("show")) }, document.addEventListener("click", (t => { t.target.closest(".side-menu") || ($("side-menu-panel")?.classList.remove("show"), $("btn-side-menu-toggle")?.classList.remove("act")) })); const getWaitingContacts = t => { const e = canonicalLoc(t); return e ? (D.contacts.contacts || []).filter((t => t?.waitingAt && canonicalLoc(t.waitingAt) === e)) : [] }, finishTravel = (t, e) => { playerLocation = t, selectedMapValue = "current"; const s = getWaitingContacts(t); s.forEach((t => delete t.waitingAt)), saveAll(), render(), hideInfo(); const o = $("goto-task")?.value; let a = `{{user}}离开了${e || "上一地点"},来到${t}。${o ? "意图:" + o : ""}`; s.length && (a += ` ${s.map((t => t.name)).join("、")}已经在这里等你了。`), post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${a}` }) }; $("goto-ok").onclick = () => { if (!curNode) return; const t = $("goto-ok"), e = D.maps?.outdoor?.nodes?.find((t => t.name === playerLocation)), s = { name: playerLocation, info: e?.info || "" }, o = curNode.name, a = D.maps?.indoor?.[o]; if (a?.description) return BtnState.reset(t, "确认前往"), closeM("m-goto"), finishTravel(o, s.name), void switchMapView("current"); let n = "main" === curNode.type ? "main" : "home" === curNode.type ? "home" : "sub"; const r = Req.create("scene"); Req.set("scene", { node: curNode, prev: s.name }), BtnState.load(t, "生成中"), post("SCENE_SWITCH", { requestId: r, prevLocationName: s.name, prevLocationInfo: s.info, targetLocationName: curNode.name, targetLocationType: n, targetLocationInfo: curNode.data?.info || "", playerAction: $("goto-task").value || "" }) }, $("btn-gen-local-map").onclick = () => { const t = $("btn-gen-local-map"); BtnState.load(t, "生成中"); const e = Req.create("localmap"); Req.set("localmap", { btn: t }), post("GENERATE_LOCAL_MAP", { requestId: e, outdoorDescription: D.maps?.outdoor?.description || "" }) }, $("btn-refresh-local-map").onclick = () => { if (!playerLocation) return void showResultModal("提示", "请先生成世界数据", !0); const t = $("btn-refresh-local-map"), e = D.maps?.indoor?.[playerLocation], s = e; if (!s) return void showResultModal("提示", "当前区域没有局部地图,请先生成", !0); BtnState.load(t, "刷新中"); const o = Req.create("localmaprf"); Req.set("localmaprf", { btn: t, loc: playerLocation }), post("REFRESH_LOCAL_MAP", { requestId: o, locationName: playerLocation, currentLocalMap: s, outdoorDescription: D.maps?.outdoor?.description || "" }) }, $("btn-gen-local-scene").onclick = () => { if (!playerLocation || "未知" === playerLocation) return void showResultModal("提示", "请先生成世界数据", !0); const t = $("btn-gen-local-scene"); BtnState.load(t, "生成中"); const e = D.maps?.outdoor?.nodes?.find((t => t.name === playerLocation)), s = D.maps?.indoor?.[playerLocation], o = s?.description || e?.info || e?.data?.info || "", a = Req.create("localscene"); Req.set("localscene", { btn: t, loc: playerLocation }), post("GENERATE_LOCAL_SCENE", { requestId: a, locationName: playerLocation, locationInfo: o }) }, $("btn-refresh-world-news").onclick = () => { const t = $("btn-refresh-world-news"); if (!t) return; t.disabled = !0, t._o = t._o ?? t.innerHTML, t.innerHTML = ''; const e = Req.create("newsrf"); Req.set("newsrf", { btn: t }), post("REFRESH_WORLD_NEWS", { requestId: e }) }; const saveAll = () => post("SAVE_ALL_DATA", { allData: { meta: D.meta, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, sceneSetup: D.sceneSetup, strangers: D.contacts.strangers, contacts: contactsForSave() }, playerLocation }), dataKeys = [["meta", "大纲", "核心真相、洋葱结构、时间线、用户指南", () => D.meta, t => D.meta = t], ["world", "世界资讯", "世界新闻等信息", () => D.world, t => D.world = t], ["outdoor", "大地图", "室外区域的地点和路线", () => D.maps.outdoor, t => D.maps.outdoor = t], ["indoor", "局部地图", "隐藏的室内/局部场景地图", () => D.maps.indoor, t => D.maps.indoor = t], ["sceneSetup", "区域剧情", "当前区域的 Side Story", () => D.sceneSetup, t => D.sceneSetup = t], ["characterContactSms", "角色卡短信", "角色卡联络人的短信记录", () => ({ messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, summaries: charSmsHistory?.summaries || {} }), t => { t && "object" == typeof t && (charSmsHistory = { messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, ...t || {} }) }], ["strangers", "陌路人", "已遇见但未建立联系的角色", () => D.contacts.strangers, t => D.contacts.strangers = t], ["contacts", "联络人", "已添加的联系人", () => contactsForSave(), t => { const e = (D.contacts.contacts || []).find(isCharCardContact); D.contacts.contacts = (e ? [e] : []).concat(Array.isArray(t) ? t : []) }]]; let gSet = { apiUrl: "", apiKey: "", model: "", mode: "assist" }, dataCk = {}, editCtx = null, commSet = { historyCount: 50, npcPosition: 0, npcOrder: 100, stream: !1 }, promptDefaults = { jsonTemplates: {}, promptSources: {} }, promptStores = { global: { jsonTemplates: {}, promptSources: {} }, character: { jsonTemplates: {}, promptSources: {} } }; const reqSet = () => post("GET_SETTINGS"), renderDataList = () => { $("data-list").innerHTML = dataKeys.map((([t, e, s]) => `
${e}
${s}
`)).join(""), $$("#data-list .data-item").forEach((t => t.onclick = e => { if (e.target.closest(".data-edit")) return; const s = t.dataset.k; dataCk[s] = !dataCk[s], t.classList.toggle("sel", dataCk[s]) })), $$("#data-list .data-edit").forEach((t => t.onclick = e => { e.stopPropagation(), openDataEdit(t.dataset.k) })) }, ADV_PROMPT_ITEMS = [["sms", "短信回复"], ["invite", "邀请回复"], ["npc", "NPC 生成"], ["stranger", "提取陌路人"], ["worldGenStep1", "大纲生成"], ["worldGenStep2", "世界生成"], ["worldSim", "世界推演(故事模式)"], ["worldSimAssist", "世界推演(辅助模式)"], ["worldNewsRefresh", "世界新闻刷新"], ["sceneSwitch", "场景切换"], ["localMapGen", "局部地图生成"], ["localMapRefresh", "局部地图刷新"], ["localSceneGen", "局部剧情生成"], ["summary", "总结压缩"]], advHasJsonTemplate = t => { const e = promptDefaults?.jsonTemplates || {}; return Object.prototype.hasOwnProperty.call(e, t) }, advGetScope = () => "global" === $("adv-scope")?.value ? "global" : "character", advStoreHasKey = (t, e) => { const s = ("global" === t ? promptStores.global : promptStores.character) || {}; return !(!Object.prototype.hasOwnProperty.call(s?.promptSources || {}, e) && !Object.prototype.hasOwnProperty.call(s?.jsonTemplates || {}, e)) }, advGetPromptObj = (t, e = "character") => { const s = promptDefaults?.promptSources || {}, o = promptStores?.global?.promptSources || {}, a = promptStores?.character?.promptSources || {}, n = ("global" === e ? o[t] || s[t] : a[t] || o[t] || s[t]) || {}; return { u1: "string" == typeof n.u1 ? n.u1 : "", a1: "string" == typeof n.a1 ? n.a1 : "", u2: "string" == typeof n.u2 ? n.u2 : "", a2: "string" == typeof n.a2 ? n.a2 : "" } }, advNormalizeDisplayText = t => { let e = String(t ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"); return !e.includes("\n") && e.includes("\\n") && (e = e.replaceAll("\\n", "\n")), e.includes("\\t") && (e = e.replaceAll("\\t", "\t")), e.includes("\\`") && (e = e.replaceAll("\\`", "`")), e }, advNormalizeSaveText = t => String(t ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"), advGetJsonTemplate = (t, e = "character") => { const s = promptDefaults?.jsonTemplates || {}, o = promptStores?.global?.jsonTemplates || {}, a = promptStores?.character?.jsonTemplates || {}; if (!(advHasJsonTemplate(t) || Object.prototype.hasOwnProperty.call(o, t) || Object.prototype.hasOwnProperty.call(a, t))) return ""; const n = "global" === e ? Object.prototype.hasOwnProperty.call(o, t) ? o[t] : s[t] : Object.prototype.hasOwnProperty.call(a, t) ? a[t] : Object.prototype.hasOwnProperty.call(o, t) ? o[t] : s[t]; return "string" == typeof n ? n : "" }, advApplyToUI = (t, e = "character") => { const s = advGetPromptObj(t, e), o = (t, e) => { const s = $(t); s && (s.value = advNormalizeDisplayText(e)) }; o("adv-u1", s.u1), o("adv-a1", s.a1), o("adv-u2", s.u2), o("adv-a2", s.a2), $("adv-json-wrap").style.display = ""; const a = advGetJsonTemplate(t, e), n = $("adv-json"); n && (n.value = String(a ?? "")), advUpdateVarHelp(t) }, advUpdateVarHelp = t => { $$(".adv-vars-group").forEach((e => { const s = String(e.dataset.advFor || "").split(",").map((t => t.trim())).filter(Boolean); e.style.display = !s.length || s.includes(t) ? "" : "none" })) }, advBuildEdits = () => { const t = t => advNormalizeSaveText($(t)?.value ?? ""); return { prompt: { u1: t("adv-u1"), a1: t("adv-a1"), u2: t("adv-u2"), a2: t("adv-a2") }, jsonTemplate: advNormalizeSaveText($("adv-json")?.value ?? "") } }, advInit = () => { const t = $("adv-key"); if (!t || t._inited) return; t._inited = !0, t.innerHTML = ADV_PROMPT_ITEMS.map((([t, e]) => ``)).join(""), t.onchange = () => advApplyToUI(t.value, advGetScope()); const e = $("adv-scope"); e && !e._inited && (e._inited = !0, e.onchange = () => { const t = $("adv-key")?.value; t && advApplyToUI(t, advGetScope()) }) }, advOpen = () => { advInit(); const t = $("adv-key"), e = t?.value || ADV_PROMPT_ITEMS[0]?.[0]; if (e) { const t = $("adv-scope"); t && (t.value = advStoreHasKey("character", e) ? "character" : "global"), advApplyToUI(e, advGetScope()) } openM("m-adv-prompts") }, advSaveTo = t => { advInit(); const e = $("adv-key")?.value; if (!e) return; const { prompt: s, jsonTemplate: o } = advBuildEdits(); post("SAVE_PROMPTS", { scope: t, key: e, prompt: s, jsonTemplate: o }), closeM("m-adv-prompts") }, advReset = () => { advInit(); const t = $("adv-key")?.value; t && (post("SAVE_PROMPTS", { scope: advGetScope(), key: t, reset: !0 }), closeM("m-adv-prompts")) }, parseJsonLoose = t => { const e = String(t ?? "").trim(); if (!e) throw new Error("空内容"); try { return JSON.parse(e) } catch { } const s = e.match(/```[^\n]*\n([\s\S]*?)\n```/); if (s?.[1]) { const t = s[1].trim(); try { return JSON.parse(t) } catch { } } const o = (t, s) => { const o = e.indexOf(t), a = e.lastIndexOf(s); return -1 === o || -1 === a || a <= o ? null : e.slice(o, a + 1) }, a = o("{", "}") ?? o("[", "]"); return a ? JSON.parse(a) : JSON.parse(e) }, updateEditPreview = () => { const t = $("data-edit-preview"); t && (t.style.display = "none", t.textContent = "") }, setEditContent = (t, e) => { $("data-edit-title").textContent = t, $("data-edit-ta").value = e, $("data-edit-err").classList.remove("vis"), updateEditPreview(), openM("m-data-edit") }, openDataEdit = t => { const e = dataKeys.find((([e]) => e === t)); e && (editCtx = { type: "characterContactSms" === t ? "charSms" : "data", key: t }, setEditContent(`编辑 - ${e[1]}`, JSON.stringify(e[3](), null, 2))) }; $("data-edit-save").onclick = () => { if (editCtx) try { const t = parseJsonLoose($("data-edit-ta").value); if ("data" === editCtx.type) { const e = dataKeys.find((([t]) => t === editCtx.key)); if (!e) return; e[4](t), render(), saveAll() } else if ("charSms" === editCtx.type) { const e = t?.summaries ?? t; if (!e || "object" != typeof e || Array.isArray(e)) throw new Error("需要 summaries 对象"); charSmsHistory.summaries = e, post("SAVE_CHAR_SMS_HISTORY", { summaries: e }) } closeM("m-data-edit"), editCtx = null } catch (t) { $("data-edit-err").textContent = `JSON错误: ${t.message}`, $("data-edit-err").classList.add("vis") } }, $("data-edit-ta").addEventListener("input", updateEditPreview); const showTestRes = (t, e) => { const s = $("test-res"); s.textContent = e, s.className = "set-test-res " + (t ? "ok" : "err") }, showResultModal = (t, e, s = !1, o = null) => { $("res-title").textContent = t, $("res-title").style.color = s ? "var(--err)" : "", $("res-msg").textContent = e; const a = $("res-record-box"), n = $("res-record"); o ? (a.style.display = "block", n.textContent = "object" == typeof o ? JSON.stringify(o, null, 2) : String(o)) : (a.style.display = "none", n.textContent = ""); const r = $("res-action"); r.style.display = "none", r.textContent = "", r.onclick = null, openM("m-result") }; function render() { const t = D.world?.news || []; $("news-list").innerHTML = t.length ? t.map((t => `
${h(t.title)}
${h(t.time || "")}

${h(t.content)}

`)).join("") : '
暂无新闻
', $$("#news-list .fold").forEach(bindFold); const e = D.meta?.user_guide; e && ($("ug-state").textContent = e.current_state || "未知状态", $("ug-actions").innerHTML = (e.guides || []).map(((t, e) => `
${e + 1}. ${h(t)}
`)).join("") || '
暂无行动指南
'); const s = (t, e) => (t || []).length ? t.map((t => `
${h(t.avatar || "")}
${h(t.name || "")}
${t.online ? "● 在线" : h(t.location)}
${t.info ? `
${h(t.info)}
` : ""}
${e ? `` : ``}
`)).join("") : '
暂无
'; if ($("sec-stranger").innerHTML = s(D.contacts.strangers, !0), $("sec-contact").innerHTML = s(D.contacts.contacts, !1), $$(".comm-sec .fold").forEach(bindFold), $$(".add-btn").forEach((t => t.onclick = e => { e.stopPropagation(), genAddCt(t.dataset.name, t.dataset.info || "", t) })), $$(".ignore-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.strangers.findIndex((e => e.name === t.dataset.name)); s > -1 && (D.contacts.strangers.splice(s, 1), saveCt(), render()) })), $$(".msg-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.contacts.find((e => e.worldbookUid === t.dataset.uid)); s && openChat(s) })), $$(".inv-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.contacts.find((e => e.worldbookUid === t.dataset.uid)); s && openInv(s) })), "current" === selectedMapValue) { const t = getCurInside(); $("side-desc").innerHTML = t?.description ? `
📍 ${h(playerLocation)}
` + parseLinks(t.description) : parseLinks(D.maps?.outdoor?.description || "") } else $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""); bindLinks($("side-desc")), $("mob-desc").innerHTML = "", renderMapSelector(), renderMap() } function renderMap() { const t = D.maps?.outdoor; if (seed = 123456789, inner.querySelectorAll(".item").forEach((t => t.remove())), svg.innerHTML = "", nodes = [], lines = [], !t?.nodes?.length) return; t.nodes.forEach(((t, e) => { const s = dirMap[t.position] || [0, 0], o = Math.hypot(s[0], s[1]) || 1, a = 120 * (t.distant || 1); nodes.push({ id: "n" + e, name: t.name, type: t.type || "sub", pos: t.position, distant: t.distant || 1, x: s[0] / o * a, y: s[1] / o * a, data: t }) })); const e = {}; nodes.forEach((t => (e[t.pos] = e[t.pos] || []).push(t))); for (let t in e) { const s = e[t]; if (s.length <= 1) continue; const o = dirMap[t] || [0, 0], a = Math.hypot(o[0], o[1]) || 1, n = -o[1] / a, r = o[0] / a, i = (s.length - 1) / 2; s.sort(((t, e) => t.distant - e.distant)).forEach(((t, e) => { t.x += n * (e - i) * 50, t.y += r * (e - i) * 50 })) } for (let t = 0; t < 15; t++)for (let t = 0; t < nodes.length; t++)for (let e = t + 1; e < nodes.length; e++) { const s = nodes[t], o = nodes[e], a = s.x - o.x, n = s.y - o.y, r = Math.hypot(a, n); if (r < 80 && r > 0) { const t = (80 - r) / r * .5; s.x += a * t, s.y += n * t, o.x -= a * t, o.y -= n * t } } nodes.forEach((t => { const e = document.createElement("div"); e.className = `item node-${t.type}`, e.id = t.id, e.textContent = "home" === t.type ? "🏠 " + t.name : t.name, e.style.cssText = `left:${t.x + 2e3}px;top:${t.y + 2e3}px`, e.onclick = e => { e.stopPropagation(), curNode?.id === t.id ? hideInfo() : showInfo(t) }, inner.appendChild(e) })); for (let t in e) { const s = e[t].sort(((t, e) => t.distant - e.distant)); for (let t = 0; t < s.length - 1; t++)lines.push([s[t], s[t + 1]]) } const s = Object.values(e).map((t => t.sort(((t, e) => t.distant - e.distant))[0])).sort(((t, e) => Math.atan2(t.y, t.x) - Math.atan2(e.y, e.x))); s.forEach(((t, e) => lines.push([t, s[(e + 1) % s.length]]))); const o = (t, e) => lines.some((([s, o]) => s === t && o === e || s === e && o === t)); for (let t = 0, e = 0; e < Math.floor(nodes.length / 5) && t < 200; t++) { const t = nodes[Math.floor(rand() * nodes.length)], s = nodes[Math.floor(rand() * nodes.length)]; t === s || o(t, s) || (lines.push([t, s]), e++) } drawLines() } function drawLines() { svg.innerHTML = ""; const t = mapWrap.getBoundingClientRect(); lines.forEach((([e, s]) => { const o = $(e.id), a = $(s.id); if (!o || !a) return; const n = o.getBoundingClientRect(), r = a.getBoundingClientRect(), i = document.createElementNS("http://www.w3.org/2000/svg", "line"); i.setAttribute("x1", (n.left + n.width / 2 - t.left) / scale - offX / scale), i.setAttribute("y1", (n.top + n.height / 2 - t.top) / scale - offY / scale), i.setAttribute("x2", (r.left + r.width / 2 - t.left) / scale - offX / scale), i.setAttribute("y2", (r.top + r.height / 2 - t.top) / scale - offY / scale); const c = "main" === e.type && "main" === s.type || "home" === e.type || "home" === s.type; i.setAttribute("stroke", c ? "var(--c)" : "var(--c4)"), i.setAttribute("stroke-width", c ? "2" : "1"), c || i.setAttribute("stroke-dasharray", "4 3"), svg.appendChild(i) })) } $("btn-settings").onclick = () => { reqSet(), $("set-api-url").value = gSet.apiUrl || "", $("set-api-key").value = gSet.apiKey || "", $("set-model").value = gSet.model || "", $("set-model-list").style.display = "none", $("test-res").className = "set-test-res", $("set-stage").value = D.stage || 0, $("set-deviation").value = D.deviationScore || 0, $("set-sim-target").value = D.simulationTarget ?? 5, $("set-mode").value = gSet.mode || "story", $("set-history-count").value = commSet.historyCount || 50, $("set-use-stream").checked = !!commSet.stream, $("set-npc-position").value = commSet.npcPosition || 0, $("set-npc-order").value = commSet.npcOrder || 100, renderDataList(), syncSimDueUI(), openM("m-settings") }, $("btn-adv-prompts").onclick = () => advOpen(), $("adv-save-global").onclick = () => advSaveTo("global"), $("adv-save-char").onclick = () => advSaveTo("character"), $("adv-reset").onclick = () => advReset(), $("btn-fetch-models").onclick = () => { BtnState.load($("btn-fetch-models"), "加载"), post("FETCH_MODELS", { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim() }) }, $("btn-test-conn").onclick = () => { $("test-res").className = "set-test-res", BtnState.load($("btn-test-conn"), "测试"), post("TEST_CONNECTION", { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim(), model: $("set-model").value.trim() }) }, $("set-save").onclick = () => { gSet = { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim(), model: $("set-model").value.trim(), mode: $("set-mode").value || "story" }, D.stage = Math.max(0, Math.min(10, parseInt($("set-stage").value, 10) || 0)), D.deviationScore = Math.max(0, Math.min(100, parseInt($("set-deviation").value, 10) || 0)), D.simulationTarget = parseInt($("set-sim-target").value, 10), Number.isNaN(D.simulationTarget) && (D.simulationTarget = 5), commSet = { historyCount: Math.max(0, Math.min(200, parseInt($("set-history-count").value, 10) || 50)), stream: !!$("set-use-stream").checked, npcPosition: parseInt($("set-npc-position").value, 10) || 0, npcOrder: Math.max(0, Math.min(1e3, parseInt($("set-npc-order").value, 10) || 100)) }; const t = {}; dataKeys.forEach((([e, , , s]) => { dataCk[e] && (t[e] = s()) })), syncSimDueUI(), post("SAVE_SETTINGS", { globalSettings: gSet, commSettings: commSet, stage: D.stage, deviationScore: D.deviationScore, simulationTarget: D.simulationTarget, playerLocation, dataChecked: dataCk, outlineData: t, allData: { meta: D.meta, timeline: D.timeline, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, strangers: D.contacts.strangers, contacts: contactsForSave() } }), closeM("m-settings") }, $("btn-close").onclick = () => post("CLOSE_PANEL"), window.addEventListener("message", (t => { if (t.origin !== PARENT_ORIGIN || t.source !== parent) return; if ("LittleWhiteBox" !== t.data?.source) return; const e = t.data, s = e.type; if ("LOAD_SETTINGS" === s) { if (e.globalSettings && (gSet = e.globalSettings), void 0 !== e.stage && (D.stage = e.stage), void 0 !== e.deviationScore && (D.deviationScore = e.deviationScore), void 0 !== e.simulationTarget && (D.simulationTarget = e.simulationTarget), e.playerLocation && (playerLocation = e.playerLocation), e.commSettings && (commSet = { historyCount: e.commSettings.historyCount ?? 50, npcPosition: e.commSettings.npcPosition ?? 0, npcOrder: e.commSettings.npcOrder ?? 100, stream: !!e.commSettings.stream }), e.dataChecked && (dataCk = e.dataChecked), e.promptConfig && (promptDefaults = e.promptConfig.defaults || promptDefaults, e.promptConfig.stores ? (promptStores.global = e.promptConfig.stores.global || { jsonTemplates: {}, promptSources: {} }, promptStores.character = e.promptConfig.stores.character || { jsonTemplates: {}, promptSources: {} }) : (promptStores.global = e.promptConfig.current || { jsonTemplates: {}, promptSources: {} }, promptStores.character = promptStores.character || { jsonTemplates: {}, promptSources: {} })), e.outlineData) { const t = e.outlineData; t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.outdoor && (D.maps.outdoor = t.outdoor), t.indoor && (D.maps.indoor = t.indoor), t.sceneSetup && (D.sceneSetup = t.sceneSetup), t.strangers && (D.contacts.strangers = t.strangers), t.contacts && (D.contacts.contacts = t.contacts) } { const t = e.characterContactSmsHistory || {}; charSmsHistory = { messages: Array.isArray(t.messages) ? t.messages : [], summarizedCount: t.summarizedCount || 0, summaries: t.summaries || {} } } let t = D.contacts.contacts.find((t => "__CHARACTER_CARD__" === t.worldbookUid)); t || (t = D.contacts.contacts.find((t => !t.worldbookUid && "炒饭智能" === t.name)), t ? (t.worldbookUid = "__CHARACTER_CARD__", t.info = "角色卡联络人", t.location = "在线", t.online = !0) : (D.contacts.contacts.unshift({ name: e.characterCardName || "{{characterName}}", avatar: "", color: "#555", location: "在线", info: "角色卡联络人", online: !0, worldbookUid: "__CHARACTER_CARD__", messages: [], summarizedCount: 0 }), t = D.contacts.contacts[0])), t && e.characterCardName && (t.name = e.characterCardName, t.avatar = (e.characterCardName || "")[0] || t.avatar || ""), render(), syncSimDueUI(), $("m-settings").classList.contains("act") && ($("set-api-url").value = gSet.apiUrl || "", $("set-api-key").value = gSet.apiKey || "", $("set-model").value = gSet.model || "", $("set-stage").value = D.stage, $("set-deviation").value = D.deviationScore, $("set-sim-target").value = D.simulationTarget ?? 5, $("set-mode").value = gSet.mode || "story", $("set-history-count").value = commSet.historyCount, $("set-use-stream").checked = !!commSet.stream, $("set-npc-position").value = commSet.npcPosition, $("set-npc-order").value = commSet.npcOrder, renderDataList()) } else if ("PROMPT_CONFIG_UPDATED" === s) { if (e.promptConfig && (promptDefaults = e.promptConfig.defaults || promptDefaults, e.promptConfig.stores ? (promptStores.global = e.promptConfig.stores.global || { jsonTemplates: {}, promptSources: {} }, promptStores.character = e.promptConfig.stores.character || { jsonTemplates: {}, promptSources: {} }) : (promptStores.global = e.promptConfig.current || { jsonTemplates: {}, promptSources: {} }, promptStores.character = promptStores.character || { jsonTemplates: {}, promptSources: {} }), $("m-adv-prompts").classList.contains("act"))) { const t = $("adv-key")?.value; t && advApplyToUI(t, advGetScope()) } } else if ("FETCH_MODELS_RESULT" === s) { BtnState.reset($("btn-fetch-models"), "获取"); const t = $("set-model-list"); if (e.error) return t.style.display = "none", void showTestRes(!1, "获取模型失败: " + e.error); if (!e.models?.length) return t.style.display = "none", void showTestRes(!1, "未找到可用模型"); t.innerHTML = '' + e.models.map((t => ``)).join(""), t.style.display = "block", t.onchange = () => { t.value && ($("set-model").value = t.value) }, showTestRes(!0, `找到 ${e.models.length} 个模型`) } else if ("TEST_CONN_RESULT" === s) BtnState.reset($("btn-test-conn"), "测试连接"), showTestRes(e.success, e.message); else if ("CHECK_WORLDBOOK_UID_RESULT" === s) { if (BtnState.reset($("btn-check-uid"), ' 检查'), !Req.match(e.requestId)) return; if (e.error) return void showUidErr(e.error); if (!e.primaryKeys?.length) return void showUidErr("该条目没有主要关键字"); addCtState.keys = e.primaryKeys; const t = $("add-name"); t.innerHTML = '' + e.primaryKeys.map((t => ``)).join(""), t.onchange = () => { addCtState.name = t.value, $("add-ct-ok").disabled = !t.value }, $("name-select-group").style.display = "block", 1 === e.primaryKeys.length && (addCtState.name = e.primaryKeys[0], t.value = addCtState.name, $("add-ct-ok").disabled = !1) } else if ("SMS_RESULT" === s) { const t = Req.get("sms"); if (!t || t.id !== e.requestId) return; if (Req.clear("sms"), smsGen = !1, $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !1, !chatTgt) return; chatTgt.messages = chatTgt.messages.filter((t => !t.typing)), e.error ? chatTgt.messages.push({ type: "received", text: `[错误] ${e.error}` }) : e.reply && chatTgt.messages.push({ type: "received", text: stripXml(e.reply) }), renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt) } else if ("SMS_STREAM" === s) { const t = Req.get("sms"); if (!t || t.id !== e.requestId || !chatTgt) return; const s = chatTgt.messages.find((t => t.typing)); s && e.text && (s.text = e.text, renderMsgs()) } else if ("LOAD_SMS_HISTORY_RESULT" === s) { if (!chatTgt || chatTgt.worldbookUid !== e.worldbookUid) return; e.messages?.length && (chatTgt.messages = e.messages, chatTgt.summarizedCount = e.summarizedCount || 0, saveCt()), renderMsgs() } else if ("COMPRESS_SMS_RESULT" === s) { const t = Req.get("compress"); if (!t || t.id !== e.requestId) return; if (Req.clear("compress"), smsGen = !1, $("chat-compress").disabled = $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !1, !chatTgt) return; if (e.error) return void alert(`压缩失败: ${e.error}`); void 0 !== e.newSummarizedCount && (chatTgt.summarizedCount = e.newSummarizedCount, renderMsgs(), saveCt()) } else if ("CHECK_STRANGER_WORLDBOOK_RESULT" === s) { const t = Req.get("stgwb"); if (!t || t.id !== e.requestId) return; const { name: s, info: o, btn: a } = t; if (Req.clear("stgwb"), e.found && e.worldbookUid) { BtnState.reset(a, ' 添加'); const t = D.contacts.strangers.findIndex((t => t.name === s)); if (t > -1) { const s = D.contacts.strangers.splice(t, 1)[0]; D.contacts.contacts.push({ name: s.name, avatar: s.avatar || s.name[0], color: s.color || "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: s.location || "未知", info: s.info || "", worldbookUid: e.worldbookUid, messages: [] }), saveCt(), render() } } else { BtnState.load(a, "生成中"); const t = Req.create("npcgen"); Req.set("npcgen", { name: s, info: o, btn: a }), post("GENERATE_NPC", { requestId: t, strangerName: s, strangerInfo: o }) } } else if ("GENERATE_NPC_RESULT" === s) { const t = Req.get("npcgen"); if (!t || t.id !== e.requestId) return; const { name: s, btn: o } = t; if (Req.clear("npcgen"), BtnState.reset(o, ' 添加'), e.error) return void showResultModal("生成角色失败", "生成 NPC 失败", !0, e.error); if (e.success && e.worldbookUid) { const t = D.contacts.strangers.findIndex((t => t.name === s)); if (t > -1) { const o = D.contacts.strangers.splice(t, 1)[0], a = e.npcData || {}; D.contacts.contacts.push({ name: a.name || o.name, avatar: (a.name || o.name)[0], color: o.color || "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: o.location || "未知", info: a.intro || o.info || "", worldbookUid: e.worldbookUid, messages: [] }), saveCt(), render(), showResultModal("生成成功", `NPC ${s} 已生成并添加到联络人`, !1, e.npcData) } } } else if ("EXTRACT_STRANGERS_RESULT" === s) { const t = Req.get("extract"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("extract"), BtnState.reset(s, ''), e.error) return void showResultModal("提取失败", "提取陌路人失败", !0, e.error); if (e.success && Array.isArray(e.strangers)) { if (!e.strangers.length) return void showResultModal("提取结果", "没有发现新的陌路人"); const t = [...D.contacts.contacts.map((t => t.name)), ...D.contacts.strangers.map((t => t.name))], s = e.strangers.filter((e => !t.includes(e.name))); if (!s.length) return void showResultModal("提取结果", "提取到的角色都已存在"); D.contacts.strangers = D.contacts.strangers.concat(s), saveCt(), render(), showResultModal("提取成功", `成功提取 ${s.length} 个新陌路人`, !1, s) } } else if ("GENERATE_WORLD_STATUS" === s) { if (!Req.match(e.requestId)) return; const t = $("world-gen-status"); t.style.display = "block", t.style.color = "var(--ok)", t.textContent = e.message } else if ("GENERATE_WORLD_RESULT" === s) { if (!Req.match(e.requestId)) return; Req.clear("wgen"); const t = $("world-gen-ok"), s = $("world-gen-status"); if (BtnState.reset(t, ' 开始生成'), e.error) { if (s.style.display = "none", showResultModal("生成失败", "世界生成失败", !0, e.error), String(e.error || "").includes("Step 2")) { const e = $("res-action"); e.style.display = "inline-block", e.textContent = "重试 Step2", e.onclick = () => { closeM("m-result"); const e = Req.create("wgen"); BtnState.load(t, "重试中"), s.style.display = "block", s.style.color = "var(--ok)", s.textContent = "准备重试 Step 2/2...", post("RETRY_WORLD_GEN_STEP2", { requestId: e }) } } return } if (e.success && e.worldData) { s.style.color = "var(--ok)", s.textContent = "生成成功!正在应用数据..."; const t = e.worldData; if (t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.maps?.outdoor && (D.maps.outdoor = t.maps.outdoor), D.stage = 0, D.deviationScore = 0, t.playerLocation) playerLocation = t.playerLocation; else { const t = D.maps?.outdoor?.nodes?.find((t => "home" === t.type)); playerLocation = t?.name || D.maps?.outdoor?.nodes?.[0]?.name || "未知" } t.maps?.inside && playerLocation && (D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[playerLocation] = t.maps.inside), selectedMapValue = "current", saveAll(), render(), setTimeout((() => { closeM("m-world-gen"), s.style.display = "none", $("world-gen-req").value = "", showResultModal("生成成功", "世界数据生成完成!Stage 和 Deviation 已重置为 0", !1, e.worldData) }), 500) } } else if ("SIMULATE_WORLD_RESULT" === s) { if (!e.isAuto && !Req.match(e.requestId)) return; if (!e.isAuto) { Req.clear("wsim"); const t = $("world-sim-ok"), s = $("world-sim-status"); if (BtnState.reset(t, ' 开始推演'), e.error) return s.style.display = "none", void showResultModal("推演失败", "世界推演失败", !0, e.error) } if (e.success && e.simData) { if (!e.isAuto) { const t = $("world-sim-status"); t.style.color = "var(--ok)", t.textContent = "推演成功!正在应用数据..." } const t = e.simData; t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.maps?.outdoor && (D.maps.outdoor = t.maps.outdoor), D.stage = (D.stage || 0) + 1, saveAll(), render(), e.isAuto || setTimeout((() => { closeM("m-world-sim"), $("world-sim-status").style.display = "none", showResultModal("推演成功", `世界推演完成!Stage 已推进到 ${D.stage}`, !1, e.simData) }), 500) } } else if ("REFRESH_WORLD_NEWS_RESULT" === s) { const t = Req.get("newsrf"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("newsrf"), s && (s.disabled = !1, s.innerHTML = s._o ?? ''), e.error) return void showResultModal("刷新失败", "世界新闻刷新失败", !0, e.error); e.success && Array.isArray(e.news) && (D.world = D.world || {}, D.world.news = e.news, saveAll(), render(), showResultModal("刷新成功", `已更新世界新闻(${e.news.length} 条)`, !1, e.news)) } else if ("SCENE_SWITCH_RESULT" === s) { const t = Req.get("scene"); if (!t || t.id !== e.requestId) return; const { node: s, prev: o } = t; if (Req.clear("scene"), BtnState.reset($("goto-ok"), "确认前往"), closeM("m-goto"), e.error) return void showResultModal("切换失败", "场景切换失败", !0, e.error); if (e.success && e.sceneData) { const t = e.sceneData; if ("number" == typeof t.newScore && (D.deviationScore = t.newScore), t.localMap && (D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s.name] = t.localMap), t.strangers?.length) { const e = new Set((D.contacts.strangers || []).map((t => t.name))), s = t.strangers.filter((t => !e.has(t.name))); D.contacts.strangers = [...D.contacts.strangers || [], ...s] } finishTravel(s.name, o), 0 !== t.scoreDelta && showResultModal("切换成功", `场景切换完成!\n偏差值变化: ${t.scoreDelta > 0 ? "+" : ""}${t.scoreDelta} (当前: ${t.newScore})`, !1, e.sceneData) } } else if ("REFRESH_LOCAL_MAP_RESULT" === s) { const t = Req.get("localmaprf"); if (!t || t.id !== e.requestId) return; const { btn: s, loc: o } = t; if (Req.clear("localmaprf"), BtnState.reset(s, '刷新'), e.error) return void showResultModal("刷新失败", "刷新局部地图失败", !0, e.error); if (e.success && e.localMapData) { const t = e.localMapData, s = t.name || o || playerLocation || "当前位置"; D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s] = t, playerLocation = s, selectedMapValue = "current", saveAll(), render(), t.description && ($("side-desc").innerHTML = `
📍 ${h(s)}
` + parseLinks(t.description), bindLinks($("side-desc"))), showResultModal("刷新成功", `局部地图已刷新!当前位置: ${s}`, !1, e.localMapData) } } else if ("GENERATE_LOCAL_SCENE_RESULT" === s) { const t = Req.get("localscene"); if (!t || t.id !== e.requestId) return; const { btn: s, loc: o } = t; if (Req.clear("localscene"), BtnState.reset(s, '局部剧情'), e.error) return void showResultModal("生成失败", "局部剧情生成失败", !0, e.error); if (e.success && e.sceneSetup) { D.sceneSetup = { ...D.sceneSetup || {}, ...e.sceneSetup || {} }, saveAll(), render(); const t = String(e.introduce || "").replace(/^\s*(?:\/?\s*(?:sendas|as)\s+name\s*=\s*(?:"[^"]*"|'[^']*'|\S+)\s+)/i, "").replace(/\s+/g, " ").trim(); t && post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${t}` }), showResultModal("生成成功", `局部剧情已生成:${o || playerLocation}`, !1, e.sceneSetup) } } else if ("SEND_INVITE_RESULT" === s) { const t = Req.get("invite"); if (!t || t.id !== e.requestId) return; const { contact: s, loc: o, btn: a } = t; if (Req.clear("invite"), BtnState.reset(a, "发送邀请"), e.error) return void showResultModal("邀请失败", "邀请发送失败", !0, e.error); if (e.success && e.inviteData) { const t = e.inviteData, a = canonicalLoc(t.targetLocation || o || ""), n = D.contacts.contacts.find((t => t && s && t.worldbookUid && s.worldbookUid && t.worldbookUid === s.worldbookUid)) || D.contacts.contacts.find((t => t && s && t.name === s.name)) || s; if (n.messages = n.messages || [], n.messages.push({ type: "sent", text: `我邀请你前往「${a}」` }), n.messages.push({ type: "received", text: t.reply }), t.accepted) { const e = canonicalLoc(playerLocation); n.location = a, e && a && e === a ? (delete n.waitingAt, post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${n.name}过来了。` })) : n.waitingAt = a, showResultModal("邀请成功", `${n.name} 接受了邀请!\n回复: ${t.reply}`, !1, t) } else showResultModal("邀请被拒", `${n.name} 拒绝了邀请。\n回复: ${t.reply}`, !1, t); saveAll(), n.worldbookUid && saveChat(n), closeM("m-invite"), render(), openChat(n) } } else if ("GENERATE_LOCAL_MAP_RESULT" === s) { const t = Req.get("localmap"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("localmap"), BtnState.reset(s, '局部地图'), e.error) return void showResultModal("生成失败", "局部地图生成失败", !0, e.error); if (e.success && e.localMapData) { const t = e.localMapData, s = t.name || "当前位置"; D.sceneSetup = null, D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s] = t, playerLocation = s, selectedMapValue = "current", saveAll(), render(), t.description && ($("side-desc").innerHTML = `
📍 ${h(s)}
` + parseLinks(t.description), bindLinks($("side-desc"))), showResultModal("生成成功", `局部地图生成完成!当前位置: ${s}`, !1, t) } } })); const updateTf = () => { inner.style.transform = `translate(${offX}px,${offY}px) scale(${scale})`, $("zoom-ind").textContent = Math.round(100 * scale) + "%", requestAnimationFrame(drawLines) }, initPos = () => { offX = mapWrap.clientWidth / 2 - 2e3, offY = mapWrap.clientHeight / 2 - 2e3, scale = 1, updateTf() }; function panTo(t, e = 350) { if (!t || anim) return; if (!$(t.id)) return; const s = -(t.x + 2e3) * scale + mapWrap.clientWidth / 2, o = -(t.y + 2e3) * scale + .25 * mapWrap.clientHeight, a = offX, n = offY, r = performance.now(); anim = !0, function t(i) { const c = Math.min((i - r) / e, 1), l = 1 - Math.pow(1 - c, 3); offX = a + (s - a) * l, offY = n + (o - n) * l, updateTf(), c < 1 ? requestAnimationFrame(t) : anim = !1 }(r) } function showInfo(t) { if (!t?.data) return; curNode = t, inner.querySelectorAll(".item").forEach((t => t.classList.remove("hl"))), $(t.id)?.classList.add("hl"); const e = t.name === playerLocation; $("btn-goto").classList.toggle("show", !e), e || ($("goto-t").textContent = `前往 ${t.name}`); const s = D.maps?.indoor?.[t.name]; e && s?.description ? ($("side-desc").innerHTML = `
📍 ${h(t.name)}
` + parseLinks(s.description), bindLinks($("side-desc"))) : ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc"))), isMob() ? ($("mob-info-t").textContent = t.name, $("mob-info-c").textContent = e ? s?.description || t.data.info || "暂无信息..." : t.data.info || "暂无信息...", popup.classList.contains("act") || openPop(1)) : ($("info-t").textContent = t.name, $("info-c").textContent = t.data.info || "暂无信息...", $("tip").classList.add("show")) } const hideInfo = () => { curNode = null, inner.querySelectorAll(".item").forEach((t => t.classList.remove("hl"))), $("btn-goto").classList.remove("show"), $("tip").classList.remove("show") }; function renderMapSelector() { const t = $("map-lbl-select"); t.innerHTML = ''; const e = D.maps?.outdoor?.nodes?.findIndex((t => t.name === playerLocation)), s = D.maps?.indoor && D.maps.indoor[playerLocation]; if ((e >= 0 || s) && (t.innerHTML += ``), t.innerHTML += "", D.maps?.outdoor?.nodes?.length && D.maps.outdoor.nodes.forEach(((e, s) => { e.name !== playerLocation && (t.innerHTML += ``) })), D.maps?.indoor) { const e = Object.keys(D.maps.indoor).filter((t => t !== playerLocation && !D.maps?.outdoor?.nodes?.some((e => e.name === t)))); e.length && (t.innerHTML += "", e.forEach((e => t.innerHTML += ``))) } t.value = selectedMapValue, updateMapLabel() } function updateMapLabel() { const t = $("map-lbl-select").value; if ("overview" === t) $("map-lbl-t").textContent = "大地图"; else if ("current" === t) $("map-lbl-t").textContent = playerLocation + "(你)"; else if (t.startsWith("node:")) { const e = parseInt(t.split(":")[1]); $("map-lbl-t").textContent = D.maps?.outdoor?.nodes?.[e]?.name || "未知" } else t.startsWith("indoor:") && ($("map-lbl-t").textContent = t.replace("indoor:", "")) } function switchMapView(t) { if (selectedMapValue = t, hideInfo(), "overview" === t) $("btn-goto").classList.remove("show"), $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")), initPos(); else if ("current" === t) { $("btn-goto").classList.remove("show"); const t = getCurInside(); t?.description ? ($("side-desc").innerHTML = `
📍 ${h(playerLocation)}
` + parseLinks(t.description), bindLinks($("side-desc"))) : ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc"))); const e = nodes.find((t => t.name === playerLocation)); e && (panTo(e), showInfo(e)) } else if (t.startsWith("node:")) { const e = parseInt(t.split(":")[1]), s = D.maps?.outdoor?.nodes?.[e]; $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")); const o = nodes.find((t => t.name === s?.name)); o && (panTo(o), showInfo(o)) } else if (t.startsWith("indoor:")) { const e = t.replace("indoor:", ""), s = D.maps?.indoor?.[e]; e !== playerLocation ? ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")), curNode = { name: e, type: "sub", data: { info: stripXml(s?.description || "") }, isIndoor: !0 }, $("btn-goto").classList.add("show"), $("goto-t").textContent = `前往 ${e}`) : ($("btn-goto").classList.remove("show"), s?.description && ($("side-desc").innerHTML = `
🏠 ${h(e)}
` + parseLinks(s.description), bindLinks($("side-desc")))) } updateMapLabel() } $("info-bk").onclick = hideInfo, $("mob-info-bk").onclick = () => popup.classList.remove("act"), $("map-lbl-select").onchange = t => switchMapView(t.target.value); let sx, sy, lastDist = 0, lastCX = 0, lastCY = 0; mapWrap.onmousedown = t => { anim || t.target.closest(".map-act,.map-lbl") || (t.target.classList.contains("item") || hideInfo(), drag = !0, sx = t.clientX, sy = t.clientY, mapWrap.style.cursor = "grabbing") }, mapWrap.onmousemove = t => { drag && (offX += t.clientX - sx, offY += t.clientY - sy, sx = t.clientX, sy = t.clientY, updateTf()) }, mapWrap.onmouseup = mapWrap.onmouseleave = () => { drag = !1, mapWrap.style.cursor = "grab" }, mapWrap.onwheel = t => { if (anim) return; t.preventDefault(); const e = Math.max(.3, Math.min(3, scale + (t.deltaY > 0 ? -.1 : .1))), s = mapWrap.getBoundingClientRect(), o = t.clientX - s.left, a = t.clientY - s.top, n = e / scale; offX = o - (o - offX) * n, offY = a - (a - offY) * n, scale = e, updateTf() }, mapWrap.ontouchstart = t => { if (anim || t.target.closest(".map-act,.map-lbl")) return; const e = t.target.closest(".item"); e ? e._ts = { x: t.touches[0].clientX, y: t.touches[0].clientY, t: Date.now() } : (hideInfo(), 1 === t.touches.length ? (drag = !0, sx = t.touches[0].clientX, sy = t.touches[0].clientY) : 2 === t.touches.length && (drag = !1, lastDist = Math.hypot(t.touches[0].clientX - t.touches[1].clientX, t.touches[0].clientY - t.touches[1].clientY), lastCX = (t.touches[0].clientX + t.touches[1].clientX) / 2, lastCY = (t.touches[0].clientY + t.touches[1].clientY) / 2)) }, mapWrap.ontouchmove = t => { const e = t.target.closest(".item"); if (e && e._ts) Math.hypot(t.touches[0].clientX - e._ts.x, t.touches[0].clientY - e._ts.y) > 10 && (delete e._ts, drag = !0, sx = t.touches[0].clientX, sy = t.touches[0].clientY); else if (1 === t.touches.length && drag) t.preventDefault(), offX += t.touches[0].clientX - sx, offY += t.touches[0].clientY - sy, sx = t.touches[0].clientX, sy = t.touches[0].clientY, updateTf(); else if (2 === t.touches.length) { t.preventDefault(); const e = Math.hypot(t.touches[0].clientX - t.touches[1].clientX, t.touches[0].clientY - t.touches[1].clientY), s = (t.touches[0].clientX + t.touches[1].clientX) / 2, o = (t.touches[0].clientY + t.touches[1].clientY) / 2, a = Math.max(.3, Math.min(3, scale * (e / lastDist))), n = mapWrap.getBoundingClientRect(), r = s - n.left, i = o - n.top, c = a / scale; offX = r - (r - offX) * c, offY = i - (i - offY) * c, offX += s - lastCX, offY += o - lastCY, scale = a, lastDist = e, lastCX = s, lastCY = o, updateTf() } }, mapWrap.ontouchend = t => { const e = t.target.closest(".item"); if (e && e._ts) { const s = Date.now() - e._ts.t; if (delete e._ts, s < 300) { const s = nodes.find((t => t.id === e.id)); s && (t.preventDefault(), curNode?.id === s.id ? hideInfo() : showInfo(s)) } } drag = !1 }, $$(".nav-i").forEach((t => t.onclick = () => { $$(".nav-i").forEach((t => t.classList.remove("act"))), $$(".page").forEach((t => t.classList.remove("act"))), t.classList.add("act"), $(`page-${t.dataset.p}`).classList.add("act"); const e = "map" === t.dataset.p; sidePop.classList.toggle("show", e), isMob() && (e ? openPop(1) : popup.classList.remove("act")), e && setTimeout((() => { initPos(), drawLines() }), 50) })), $$(".comm-tab").forEach((t => t.onclick = () => { $$(".comm-tab").forEach((t => t.classList.remove("act"))), $$(".comm-sec").forEach((t => t.classList.remove("act"))), t.classList.add("act"), $(`sec-${t.dataset.t}`).classList.add("act") })), $("btn-goto").onclick = t => { t.stopPropagation(), curNode && ($("goto-d").textContent = `目的地:${curNode.name}`, $("goto-task").value = "", openM("m-goto")) }, addEventListener("resize", (() => requestAnimationFrame(drawLines))), window.clickTab = (t, e) => { const s = t.closest(".settings-modal"); if (!s) return; s.querySelectorAll(".set-nav-item").forEach((t => t.classList.remove("act"))), t.classList.add("act"), s.querySelectorAll(".set-tab-page").forEach((t => t.classList.remove("act"))); const o = document.getElementById(e); o && (o.classList.add("act"), o.style.animation = "none", o.offsetHeight, o.style.animation = null) }, document.addEventListener("DOMContentLoaded", (() => { render(), initPos(), sidePop.classList.add("show"), sidePop.classList.add("act"), isMob() && openPop(1), post("FRAME_READY"), setTimeout((() => { "current" === selectedMapValue && switchMapView("current") }), 100) })); + const inds = popup.querySelectorAll(".pop-h-ind span"), setPopH = t => { const e = snaps(); popH = Math.max(e[0], Math.min(.85 * innerHeight, t)), popup.style.height = popH + "px", popLv = e.map(((t, e) => [e, Math.abs(popH - t)])).sort(((t, e) => t[1] - e[1]))[0][0], inds.forEach(((t, e) => t.classList.toggle("act", e === popLv))) }, snapTo = t => { popLv = Math.max(0, Math.min(2, t)), setPopH(snaps()[popLv]) }, openPop = (t = 1) => { popup.classList.add("act"), snapTo(t) }; $("side-pop-handle").onclick = () => sidePop.classList.toggle("act"), $("pop-hd").onmousedown = t => { popDrag = !0, popSY = t.clientY, popSH = popH || snaps()[1], popup.classList.add("drag"), t.preventDefault() }, $("pop-hd").ontouchstart = t => { t.preventDefault(), popDrag = !0, popSY = t.touches[0].clientY, popSH = popH || snaps()[1], popup.classList.add("drag") }, document.onmousemove = t => { popDrag && setPopH(popSH + popSY - t.clientY) }, document.ontouchmove = t => { popDrag && t.touches.length && (t.preventDefault(), setPopH(popSH + popSY - t.touches[0].clientY)) }; const endDrag = () => { popDrag && (popDrag = !1, popup.classList.remove("drag"), snapTo(popLv)) }; document.onmouseup = endDrag, document.ontouchend = endDrag, document.ontouchcancel = endDrag; const bindLinks = t => t.querySelectorAll(".loc-lk").forEach((t => t.onclick = e => { e.stopPropagation(); const s = t.dataset.loc, o = nodes.find((t => t.name === s)); if (o) return panTo(o), void showInfo(o); const a = getCurInside(), n = a?.nodes?.find((t => t.name === s)); n && ($("info-t").textContent = n.name, $("info-c").textContent = n.info || "暂无信息...", $("tip").classList.add("show"), $("btn-goto").classList.remove("show"), isMob() && ($("mob-info-t").textContent = n.name, $("mob-info-c").textContent = n.info || "暂无信息...", popup.classList.contains("act") || openPop(1))) })), bindFold = t => t.querySelector(".fold-h").onclick = () => t.classList.toggle("exp"); $$(".modal-bd,.modal-x,.m-cancel").forEach((t => t.onclick = () => t.closest(".modal").classList.remove("act"))); const openChat = t => { chatTgt = t, $("chat-av").textContent = t.avatar, $("chat-av").style.background = t.color, $("chat-nm").textContent = t.name, $("chat-st").textContent = t.online ? "● 在线" : t.location, !t.worldbookUid || t.messages && t.messages.length ? renderMsgs() : post("LOAD_SMS_HISTORY", { worldbookUid: t.worldbookUid }), chat.classList.add("act"), $("chat-in").focus() }, closeChat = () => { chat.classList.remove("act"), chatTgt = null, smsGen = !1 }, renderMsgs = () => { if (!chatTgt) return; const t = chatTgt.messages || [], e = chatTgt.summarizedCount || 0; let s = ""; t.length ? t.forEach(((t, o) => { e > 0 && o === e && (s += '
—— 以上为已总结消息 ——
'), s += `
${escHtml(stripXml(t.text))}
` })) : s = '
暂无消息,开始聊天吧
', $("chat-msgs").innerHTML = s, $("chat-msgs").scrollTop = $("chat-msgs").scrollHeight, $("chat-compress").disabled = t.length - e < 2 || smsGen, $("chat-back").disabled = !t.length || smsGen }, sendMsg = () => { const t = $("chat-in").value.trim(); if (!t || !chatTgt || smsGen) return; chatTgt.messages = chatTgt.messages || [], chatTgt.messages.push({ type: "sent", text: t }), $("chat-in").value = "", renderMsgs(), smsGen = !0, $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !0, chatTgt.messages.push({ type: "received", text: "正在输入...", typing: !0 }), renderMsgs(); const e = Req.create("sms"); Req.set("sms", { tgt: chatTgt }), post("SEND_SMS", { requestId: e, contactName: chatTgt.name, worldbookUid: chatTgt.worldbookUid, userMessage: t, chatHistory: chatTgt.messages.filter((t => !t.typing)).slice(-20), summarizedCount: chatTgt.summarizedCount || 0 }) }, isCharCardContact = t => "__CHARACTER_CARD__" === t?.worldbookUid, contactsForSave = () => (D.contacts.contacts || []).filter((t => !isCharCardContact(t))), saveCt = () => post("SAVE_CONTACTS", { contacts: contactsForSave(), strangers: D.contacts.strangers }), saveChat = t => post("SAVE_SMS_HISTORY", { worldbookUid: t.worldbookUid, contactName: t.name, messages: t.messages.filter((t => !t.typing)), summarizedCount: t.summarizedCount || 0 }); $("chat-x").onclick = closeChat, $("chat-back").onclick = () => { if (!chatTgt || smsGen) return; const t = chatTgt.messages || []; if (t.length) { for (t.pop(); t[t.length - 1]?.typing;)t.pop(); chatTgt.summarizedCount = Math.min(chatTgt.summarizedCount || 0, t.length), renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt) } }, $("chat-clr").onclick = () => { chatTgt && confirm(`确定要清空与 ${chatTgt.name} 的所有聊天记录吗?`) && (chatTgt.messages = [], chatTgt.summarizedCount = 0, renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt)) }, $("chat-compress").onclick = () => { if (!chatTgt || smsGen) return; const t = chatTgt.messages || [], e = chatTgt.summarizedCount || 0, s = t.slice(e); if (s.length < 2) return void alert("至少需要2条未总结的消息才能压缩"); if (!confirm(`确定要压缩总结 ${s.length} 条消息吗?`)) return; smsGen = !0, $("chat-compress").disabled = $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !0; const o = Req.create("compress"); Req.set("compress", { tgt: chatTgt }), post("COMPRESS_SMS", { requestId: o, contactName: chatTgt.name, worldbookUid: chatTgt.worldbookUid, messages: t.filter((t => !t.typing)), summarizedCount: e }) }, $("chat-send").onclick = sendMsg; const chatIn = $("chat-in");["keydown", "keypress", "keyup"].forEach((t => chatIn.addEventListener(t, (t => t.stopPropagation())))), chatIn.addEventListener("keydown", (t => { "Enter" !== t.key || t.shiftKey || (t.preventDefault(), sendMsg()) })); const openInv = t => { invTgt = t, selLoc = null, $("inv-t").textContent = `邀请:${t.name}`, $("loc-list").innerHTML = D.maps.outdoor.nodes.map((t => `
${h(t.name)}
${h(t.info || "")}
`)).join(""), $$("#loc-list .loc-i").forEach((t => t.onclick = () => { $$("#loc-list .loc-i").forEach((t => t.classList.remove("sel"))), t.classList.add("sel"), selLoc = t.dataset.n })), openM("m-invite") }; $("inv-ok").onclick = () => { if (!selLoc || !invTgt) return; const t = $("inv-ok"); BtnState.load(t, "询问中..."); const e = Req.create("invite"), s = canonicalLoc(selLoc); Req.set("invite", { contact: invTgt, loc: s, btn: t }), post("SEND_INVITE", { requestId: e, contactName: invTgt.name, contactUid: invTgt.worldbookUid, targetLocation: s, smsHistory: (invTgt.messages || []).map((t => "sent" === t.type ? `{{user}}: ${t.text}` : `${invTgt.name}: ${t.text}`)).join("\n") }) }; let addCtState = { uid: "", name: "", keys: [] }; const resetAddCt = () => { addCtState = { uid: "", name: "", keys: [] }, $("add-uid").value = "", $("add-name").value = "", $("add-name").innerHTML = '', $("name-select-group").style.display = "none", $("uid-check-err").classList.remove("vis"), $("add-ct-ok").disabled = !0, BtnState.reset($("btn-check-uid"), ' 检查') }, showUidErr = t => { $("uid-check-err").textContent = t, $("uid-check-err").classList.add("vis") }; $("btn-check-uid").onclick = () => { const t = $("add-uid").value.trim(); t ? ($("uid-check-err").classList.remove("vis"), BtnState.load($("btn-check-uid"), "检查中"), $("name-select-group").style.display = "none", $("add-ct-ok").disabled = !0, addCtState.uid = t, post("CHECK_WORLDBOOK_UID", { uid: t, requestId: Req.create("uidck") })) : showUidErr("请输入UID") }, $("btn-add-ct").onclick = () => { resetAddCt(), openM("m-add-ct") }, $("add-ct-ok").onclick = () => { const t = addCtState.uid || $("add-uid").value.trim(), e = addCtState.name || $("add-name").value.trim(); t && e && (D.contacts.contacts.some((e => e.worldbookUid === t)) ? showUidErr("该联络人已存在") : (D.contacts.contacts.push({ name: e, avatar: e[0], color: "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: "未知", worldbookUid: t, messages: [] }), saveCt(), closeM("m-add-ct"), render())) }; const genAddCt = (t, e, s, npcType = "npc") => { BtnState.load(s, "检查中"); const o = Req.create("stgwb"); Req.set("stgwb", { name: t, info: e, btn: s, npcType }), post("CHECK_STRANGER_WORLDBOOK", { requestId: o, strangerName: t }) }; function canonicalLoc(t) { return String(t || "").trim().replace(/^\u90ae\u8f6e/, "") } $("btn-refresh-strangers").onclick = () => { const t = $("btn-refresh-strangers"); BtnState.load(t, ""); const e = Req.create("extract"); Req.set("extract", { btn: t }), post("EXTRACT_STRANGERS", { requestId: e, existingContacts: D.contacts.contacts, existingStrangers: D.contacts.strangers }) }, $("world-gen-ok").onclick = () => { const t = $("world-gen-ok"), e = $("world-gen-status"); BtnState.load(t, "生成中"), e.style.display = "block", e.textContent = "正在生成世界数据,请稍候...", post("GENERATE_WORLD", { requestId: Req.create("wgen"), playerRequests: $("world-gen-req").value.trim() }) }, $("world-sim-ok").onclick = () => { if (!D.meta && !D.timeline && !D.maps?.outdoor) return void alert("请先生成世界数据,再进行推演"); const t = $("world-sim-ok"), e = $("world-sim-status"); BtnState.load(t, "推演中"), e.style.display = "block", e.style.color = "var(--ok)", e.textContent = "正在分析玩家行为并推演世界变化...", post("SIMULATE_WORLD", { requestId: Req.create("wsim"), currentData: JSON.stringify({ meta: D.meta, timeline: D.timeline, world: D.world, maps: D.maps }, null, 2) }) }, $("btn-deduce").onclick = () => openM("m-world-gen"), $("btn-simulate").onclick = () => openM("m-world-sim"), $("btn-side-menu-toggle").onclick = () => { const t = $("side-menu-panel"), e = $("btn-side-menu-toggle"); t.classList.toggle("show"), e.classList.toggle("act", t.classList.contains("show")) }, document.addEventListener("click", (t => { t.target.closest(".side-menu") || ($("side-menu-panel")?.classList.remove("show"), $("btn-side-menu-toggle")?.classList.remove("act")) })); const getWaitingContacts = t => { const e = canonicalLoc(t); return e ? (D.contacts.contacts || []).filter((t => t?.waitingAt && canonicalLoc(t.waitingAt) === e)) : [] }, finishTravel = (t, e) => { playerLocation = t, selectedMapValue = "current"; const s = getWaitingContacts(t); s.forEach((t => delete t.waitingAt)), saveAll(), render(), hideInfo(); const o = $("goto-task")?.value; let a = `{{user}}离开了${e || "上一地点"},来到${t}。${o ? "意图:" + o : ""}`; s.length && (a += ` ${s.map((t => t.name)).join("、")}已经在这里等你了。`), post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${a}` }) }; $("goto-ok").onclick = () => { if (!curNode) return; const t = $("goto-ok"), e = D.maps?.outdoor?.nodes?.find((t => t.name === playerLocation)), s = { name: playerLocation, info: e?.info || "" }, o = curNode.name, a = D.maps?.indoor?.[o]; if (a?.description) return BtnState.reset(t, "确认前往"), closeM("m-goto"), finishTravel(o, s.name), void switchMapView("current"); let n = "main" === curNode.type ? "main" : "home" === curNode.type ? "home" : "sub"; const r = Req.create("scene"); Req.set("scene", { node: curNode, prev: s.name }), BtnState.load(t, "生成中"), post("SCENE_SWITCH", { requestId: r, prevLocationName: s.name, prevLocationInfo: s.info, targetLocationName: curNode.name, targetLocationType: n, targetLocationInfo: curNode.data?.info || "", playerAction: $("goto-task").value || "" }) }, $("btn-gen-local-map").onclick = () => { const t = $("btn-gen-local-map"); BtnState.load(t, "生成中"); const e = Req.create("localmap"); Req.set("localmap", { btn: t }), post("GENERATE_LOCAL_MAP", { requestId: e, outdoorDescription: D.maps?.outdoor?.description || "" }) }, $("btn-refresh-local-map").onclick = () => { if (!playerLocation) return void showResultModal("提示", "请先生成世界数据", !0); const t = $("btn-refresh-local-map"), e = D.maps?.indoor?.[playerLocation], s = e; if (!s) return void showResultModal("提示", "当前区域没有局部地图,请先生成", !0); BtnState.load(t, "刷新中"); const o = Req.create("localmaprf"); Req.set("localmaprf", { btn: t, loc: playerLocation }), post("REFRESH_LOCAL_MAP", { requestId: o, locationName: playerLocation, currentLocalMap: s, outdoorDescription: D.maps?.outdoor?.description || "" }) }, $("btn-gen-local-scene").onclick = () => { if (!playerLocation || "未知" === playerLocation) return void showResultModal("提示", "请先生成世界数据", !0); const t = $("btn-gen-local-scene"); BtnState.load(t, "生成中"); const e = D.maps?.outdoor?.nodes?.find((t => t.name === playerLocation)), s = D.maps?.indoor?.[playerLocation], o = s?.description || e?.info || e?.data?.info || "", a = Req.create("localscene"); Req.set("localscene", { btn: t, loc: playerLocation }), post("GENERATE_LOCAL_SCENE", { requestId: a, locationName: playerLocation, locationInfo: o }) }, $("btn-refresh-world-news").onclick = () => { const t = $("btn-refresh-world-news"); if (!t) return; t.disabled = !0, t._o = t._o ?? t.innerHTML, t.innerHTML = ''; const e = Req.create("newsrf"); Req.set("newsrf", { btn: t }), post("REFRESH_WORLD_NEWS", { requestId: e }) }; const saveAll = () => post("SAVE_ALL_DATA", { allData: { meta: D.meta, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, sceneSetup: D.sceneSetup, strangers: D.contacts.strangers, contacts: contactsForSave() }, playerLocation }), dataKeys = [["meta", "大纲", "核心真相、洋葱结构、时间线、用户指南", () => D.meta, t => D.meta = t], ["world", "世界资讯", "世界新闻等信息", () => D.world, t => D.world = t], ["outdoor", "大地图", "室外区域的地点和路线", () => D.maps.outdoor, t => D.maps.outdoor = t], ["indoor", "局部地图", "隐藏的室内/局部场景地图", () => D.maps.indoor, t => D.maps.indoor = t], ["sceneSetup", "区域剧情", "当前区域的 Side Story", () => D.sceneSetup, t => D.sceneSetup = t], ["characterContactSms", "角色卡短信", "角色卡联络人的短信记录", () => ({ messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, summaries: charSmsHistory?.summaries || {} }), t => { t && "object" == typeof t && (charSmsHistory = { messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, ...t || {} }) }], ["strangers", "陌路人", "已遇见但未建立联系的角色", () => D.contacts.strangers, t => D.contacts.strangers = t], ["contacts", "联络人", "已添加的联系人", () => contactsForSave(), t => { const e = (D.contacts.contacts || []).find(isCharCardContact); D.contacts.contacts = (e ? [e] : []).concat(Array.isArray(t) ? t : []) }]]; let gSet = { apiUrl: "", apiKey: "", model: "", mode: "assist" }, dataCk = {}, editCtx = null, commSet = { historyCount: 50, npcPosition: 0, npcOrder: 100, stream: !1 }, promptDefaults = { jsonTemplates: {}, promptSources: {} }, promptStores = { global: { jsonTemplates: {}, promptSources: {} }, character: { jsonTemplates: {}, promptSources: {} } }; const reqSet = () => post("GET_SETTINGS"), renderDataList = () => { $("data-list").innerHTML = dataKeys.map((([t, e, s]) => `
${e}
${s}
`)).join(""), $$("#data-list .data-item").forEach((t => t.onclick = e => { if (e.target.closest(".data-edit")) return; const s = t.dataset.k; dataCk[s] = !dataCk[s], t.classList.toggle("sel", dataCk[s]) })), $$("#data-list .data-edit").forEach((t => t.onclick = e => { e.stopPropagation(), openDataEdit(t.dataset.k) })) }, ADV_PROMPT_ITEMS = [["sms", "短信回复"], ["invite", "邀请回复"], ["npc", "NPC 生成"], ["importantNpc", "重要NPC生成"], ["stranger", "提取陌路人"], ["worldGenStep1", "大纲生成"], ["worldGenStep2", "世界生成"], ["worldSim", "世界推演(故事模式)"], ["worldSimAssist", "世界推演(辅助模式)"], ["worldNewsRefresh", "世界新闻刷新"], ["sceneSwitch", "场景切换"], ["localMapGen", "局部地图生成"], ["localMapRefresh", "局部地图刷新"], ["localSceneGen", "局部剧情生成"], ["summary", "总结压缩"]], advHasJsonTemplate = t => { const e = promptDefaults?.jsonTemplates || {}; return Object.prototype.hasOwnProperty.call(e, t) }, advGetScope = () => "global" === $("adv-scope")?.value ? "global" : "character", advStoreHasKey = (t, e) => { const s = ("global" === t ? promptStores.global : promptStores.character) || {}; return !(!Object.prototype.hasOwnProperty.call(s?.promptSources || {}, e) && !Object.prototype.hasOwnProperty.call(s?.jsonTemplates || {}, e)) }, advGetPromptObj = (t, e = "character") => { const s = promptDefaults?.promptSources || {}, o = promptStores?.global?.promptSources || {}, a = promptStores?.character?.promptSources || {}, n = ("global" === e ? o[t] || s[t] : a[t] || o[t] || s[t]) || {}; return { u1: "string" == typeof n.u1 ? n.u1 : "", a1: "string" == typeof n.a1 ? n.a1 : "", u2: "string" == typeof n.u2 ? n.u2 : "", a2: "string" == typeof n.a2 ? n.a2 : "" } }, advNormalizeDisplayText = t => { let e = String(t ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"); return !e.includes("\n") && e.includes("\\n") && (e = e.replaceAll("\\n", "\n")), e.includes("\\t") && (e = e.replaceAll("\\t", "\t")), e.includes("\\`") && (e = e.replaceAll("\\`", "`")), e }, advNormalizeSaveText = t => String(t ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"), advGetJsonTemplate = (t, e = "character") => { const s = promptDefaults?.jsonTemplates || {}, o = promptStores?.global?.jsonTemplates || {}, a = promptStores?.character?.jsonTemplates || {}; if (!(advHasJsonTemplate(t) || Object.prototype.hasOwnProperty.call(o, t) || Object.prototype.hasOwnProperty.call(a, t))) return ""; const n = "global" === e ? Object.prototype.hasOwnProperty.call(o, t) ? o[t] : s[t] : Object.prototype.hasOwnProperty.call(a, t) ? a[t] : Object.prototype.hasOwnProperty.call(o, t) ? o[t] : s[t]; return "string" == typeof n ? n : "" }, advApplyToUI = (t, e = "character") => { const s = advGetPromptObj(t, e), o = (t, e) => { const s = $(t); s && (s.value = advNormalizeDisplayText(e)) }; o("adv-u1", s.u1), o("adv-a1", s.a1), o("adv-u2", s.u2), o("adv-a2", s.a2), $("adv-json-wrap").style.display = ""; const a = advGetJsonTemplate(t, e), n = $("adv-json"); n && (n.value = String(a ?? "")), advUpdateVarHelp(t) }, advUpdateVarHelp = t => { $$(".adv-vars-group").forEach((e => { const s = String(e.dataset.advFor || "").split(",").map((t => t.trim())).filter(Boolean); e.style.display = !s.length || s.includes(t) ? "" : "none" })) }, advBuildEdits = () => { const t = t => advNormalizeSaveText($(t)?.value ?? ""); return { prompt: { u1: t("adv-u1"), a1: t("adv-a1"), u2: t("adv-u2"), a2: t("adv-a2") }, jsonTemplate: advNormalizeSaveText($("adv-json")?.value ?? "") } }, advInit = () => { const t = $("adv-key"); if (!t || t._inited) return; t._inited = !0, t.innerHTML = ADV_PROMPT_ITEMS.map((([t, e]) => ``)).join(""), t.onchange = () => advApplyToUI(t.value, advGetScope()); const e = $("adv-scope"); e && !e._inited && (e._inited = !0, e.onchange = () => { const t = $("adv-key")?.value; t && advApplyToUI(t, advGetScope()) }) }, advOpen = () => { advInit(); const t = $("adv-key"), e = t?.value || ADV_PROMPT_ITEMS[0]?.[0]; if (e) { const t = $("adv-scope"); t && (t.value = advStoreHasKey("character", e) ? "character" : "global"), advApplyToUI(e, advGetScope()) } openM("m-adv-prompts") }, advSaveTo = t => { advInit(); const e = $("adv-key")?.value; if (!e) return; const { prompt: s, jsonTemplate: o } = advBuildEdits(); post("SAVE_PROMPTS", { scope: t, key: e, prompt: s, jsonTemplate: o }), closeM("m-adv-prompts") }, advReset = () => { advInit(); const t = $("adv-key")?.value; t && (post("SAVE_PROMPTS", { scope: advGetScope(), key: t, reset: !0 }), closeM("m-adv-prompts")) }, parseJsonLoose = t => { const e = String(t ?? "").trim(); if (!e) throw new Error("空内容"); try { return JSON.parse(e) } catch { } const s = e.match(/```[^\n]*\n([\s\S]*?)\n```/); if (s?.[1]) { const t = s[1].trim(); try { return JSON.parse(t) } catch { } } const o = (t, s) => { const o = e.indexOf(t), a = e.lastIndexOf(s); return -1 === o || -1 === a || a <= o ? null : e.slice(o, a + 1) }, a = o("{", "}") ?? o("[", "]"); return a ? JSON.parse(a) : JSON.parse(e) }, updateEditPreview = () => { const t = $("data-edit-preview"); t && (t.style.display = "none", t.textContent = "") }, setEditContent = (t, e) => { $("data-edit-title").textContent = t, $("data-edit-ta").value = e, $("data-edit-err").classList.remove("vis"), updateEditPreview(), openM("m-data-edit") }, openDataEdit = t => { const e = dataKeys.find((([e]) => e === t)); e && (editCtx = { type: "characterContactSms" === t ? "charSms" : "data", key: t }, setEditContent(`编辑 - ${e[1]}`, JSON.stringify(e[3](), null, 2))) }; $("data-edit-save").onclick = () => { if (editCtx) try { const t = parseJsonLoose($("data-edit-ta").value); if ("data" === editCtx.type) { const e = dataKeys.find((([t]) => t === editCtx.key)); if (!e) return; e[4](t), render(), saveAll() } else if ("charSms" === editCtx.type) { const e = t?.summaries ?? t; if (!e || "object" != typeof e || Array.isArray(e)) throw new Error("需要 summaries 对象"); charSmsHistory.summaries = e, post("SAVE_CHAR_SMS_HISTORY", { summaries: e }) } closeM("m-data-edit"), editCtx = null } catch (t) { $("data-edit-err").textContent = `JSON错误: ${t.message}`, $("data-edit-err").classList.add("vis") } }, $("data-edit-ta").addEventListener("input", updateEditPreview); const showTestRes = (t, e) => { const s = $("test-res"); s.textContent = e, s.className = "set-test-res " + (t ? "ok" : "err") }, showResultModal = (t, e, s = !1, o = null) => { $("res-title").textContent = t, $("res-title").style.color = s ? "var(--err)" : "", $("res-msg").textContent = e; const a = $("res-record-box"), n = $("res-record"); o ? (a.style.display = "block", n.textContent = "object" == typeof o ? JSON.stringify(o, null, 2) : String(o)) : (a.style.display = "none", n.textContent = ""); const r = $("res-action"); r.style.display = "none", r.textContent = "", r.onclick = null, openM("m-result") }; function render() { const t = D.world?.news || []; $("news-list").innerHTML = t.length ? t.map((t => `
${h(t.title)}
${h(t.time || "")}

${h(t.content)}

`)).join("") : '
暂无新闻
', $$("#news-list .fold").forEach(bindFold); const e = D.meta?.user_guide; e && ($("ug-state").textContent = e.current_state || "未知状态", $("ug-actions").innerHTML = (e.guides || []).map(((t, e) => `
${e + 1}. ${h(t)}
`)).join("") || '
暂无行动指南
'); const s = (t, e) => (t || []).length ? t.map((t => `
${h(t.avatar || "")}
${h(t.name || "")}
${t.online ? "● 在线" : h(t.location)}
${t.info ? `
${h(t.info)}
` : ""}
${e ? `` : ``}
`)).join("") : '
暂无
'; if ($("sec-stranger").innerHTML = s(D.contacts.strangers, !0), $("sec-contact").innerHTML = s(D.contacts.contacts, !1), $$(".comm-sec .fold").forEach(bindFold), $$(".add-btn").forEach((t => t.onclick = e => { e.stopPropagation(), genAddCt(t.dataset.name, t.dataset.info || "", t, t.dataset.npctype || "npc") })), $$(".ignore-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.strangers.findIndex((e => e.name === t.dataset.name)); s > -1 && (D.contacts.strangers.splice(s, 1), saveCt(), render()) })), $$(".msg-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.contacts.find((e => e.worldbookUid === t.dataset.uid)); s && openChat(s) })), $$(".inv-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.contacts.find((e => e.worldbookUid === t.dataset.uid)); s && openInv(s) })), "current" === selectedMapValue) { const t = getCurInside(); $("side-desc").innerHTML = t?.description ? `
📍 ${h(playerLocation)}
` + parseLinks(t.description) : parseLinks(D.maps?.outdoor?.description || "") } else $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""); bindLinks($("side-desc")), $("mob-desc").innerHTML = "", renderMapSelector(), renderMap() } function renderMap() { const t = D.maps?.outdoor; if (seed = 123456789, inner.querySelectorAll(".item").forEach((t => t.remove())), svg.innerHTML = "", nodes = [], lines = [], !t?.nodes?.length) return; t.nodes.forEach(((t, e) => { const s = dirMap[t.position] || [0, 0], o = Math.hypot(s[0], s[1]) || 1, a = 120 * (t.distant || 1); nodes.push({ id: "n" + e, name: t.name, type: t.type || "sub", pos: t.position, distant: t.distant || 1, x: s[0] / o * a, y: s[1] / o * a, data: t }) })); const e = {}; nodes.forEach((t => (e[t.pos] = e[t.pos] || []).push(t))); for (let t in e) { const s = e[t]; if (s.length <= 1) continue; const o = dirMap[t] || [0, 0], a = Math.hypot(o[0], o[1]) || 1, n = -o[1] / a, r = o[0] / a, i = (s.length - 1) / 2; s.sort(((t, e) => t.distant - e.distant)).forEach(((t, e) => { t.x += n * (e - i) * 50, t.y += r * (e - i) * 50 })) } for (let t = 0; t < 15; t++)for (let t = 0; t < nodes.length; t++)for (let e = t + 1; e < nodes.length; e++) { const s = nodes[t], o = nodes[e], a = s.x - o.x, n = s.y - o.y, r = Math.hypot(a, n); if (r < 80 && r > 0) { const t = (80 - r) / r * .5; s.x += a * t, s.y += n * t, o.x -= a * t, o.y -= n * t } } nodes.forEach((t => { const e = document.createElement("div"); e.className = `item node-${t.type}`, e.id = t.id, e.textContent = "home" === t.type ? "🏠 " + t.name : t.name, e.style.cssText = `left:${t.x + 2e3}px;top:${t.y + 2e3}px`, e.onclick = e => { e.stopPropagation(), curNode?.id === t.id ? hideInfo() : showInfo(t) }, inner.appendChild(e) })); for (let t in e) { const s = e[t].sort(((t, e) => t.distant - e.distant)); for (let t = 0; t < s.length - 1; t++)lines.push([s[t], s[t + 1]]) } const s = Object.values(e).map((t => t.sort(((t, e) => t.distant - e.distant))[0])).sort(((t, e) => Math.atan2(t.y, t.x) - Math.atan2(e.y, e.x))); s.forEach(((t, e) => lines.push([t, s[(e + 1) % s.length]]))); const o = (t, e) => lines.some((([s, o]) => s === t && o === e || s === e && o === t)); for (let t = 0, e = 0; e < Math.floor(nodes.length / 5) && t < 200; t++) { const t = nodes[Math.floor(rand() * nodes.length)], s = nodes[Math.floor(rand() * nodes.length)]; t === s || o(t, s) || (lines.push([t, s]), e++) } drawLines() } function drawLines() { svg.innerHTML = ""; const t = mapWrap.getBoundingClientRect(); lines.forEach((([e, s]) => { const o = $(e.id), a = $(s.id); if (!o || !a) return; const n = o.getBoundingClientRect(), r = a.getBoundingClientRect(), i = document.createElementNS("http://www.w3.org/2000/svg", "line"); i.setAttribute("x1", (n.left + n.width / 2 - t.left) / scale - offX / scale), i.setAttribute("y1", (n.top + n.height / 2 - t.top) / scale - offY / scale), i.setAttribute("x2", (r.left + r.width / 2 - t.left) / scale - offX / scale), i.setAttribute("y2", (r.top + r.height / 2 - t.top) / scale - offY / scale); const c = "main" === e.type && "main" === s.type || "home" === e.type || "home" === s.type; i.setAttribute("stroke", c ? "var(--c)" : "var(--c4)"), i.setAttribute("stroke-width", c ? "2" : "1"), c || i.setAttribute("stroke-dasharray", "4 3"), svg.appendChild(i) })) } $("btn-settings").onclick = () => { reqSet(), $("set-api-url").value = gSet.apiUrl || "", $("set-api-key").value = gSet.apiKey || "", $("set-model").value = gSet.model || "", $("set-model-list").style.display = "none", $("test-res").className = "set-test-res", $("set-stage").value = D.stage || 0, $("set-deviation").value = D.deviationScore || 0, $("set-sim-target").value = D.simulationTarget ?? 5, $("set-mode").value = gSet.mode || "story", $("set-history-count").value = commSet.historyCount || 50, $("set-use-stream").checked = !!commSet.stream, $("set-npc-position").value = commSet.npcPosition || 0, $("set-npc-order").value = commSet.npcOrder || 100, renderDataList(), syncSimDueUI(), openM("m-settings") }, $("btn-adv-prompts").onclick = () => advOpen(), $("adv-save-global").onclick = () => advSaveTo("global"), $("adv-save-char").onclick = () => advSaveTo("character"), $("adv-reset").onclick = () => advReset(), $("btn-fetch-models").onclick = () => { BtnState.load($("btn-fetch-models"), "加载"), post("FETCH_MODELS", { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim() }) }, $("btn-test-conn").onclick = () => { $("test-res").className = "set-test-res", BtnState.load($("btn-test-conn"), "测试"), post("TEST_CONNECTION", { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim(), model: $("set-model").value.trim() }) }, $("set-save").onclick = () => { gSet = { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim(), model: $("set-model").value.trim(), mode: $("set-mode").value || "story" }, D.stage = Math.max(0, Math.min(10, parseInt($("set-stage").value, 10) || 0)), D.deviationScore = Math.max(0, Math.min(100, parseInt($("set-deviation").value, 10) || 0)), D.simulationTarget = parseInt($("set-sim-target").value, 10), Number.isNaN(D.simulationTarget) && (D.simulationTarget = 5), commSet = { historyCount: Math.max(0, Math.min(200, parseInt($("set-history-count").value, 10) || 50)), stream: !!$("set-use-stream").checked, npcPosition: parseInt($("set-npc-position").value, 10) || 0, npcOrder: Math.max(0, Math.min(1e3, parseInt($("set-npc-order").value, 10) || 100)) }; const t = {}; dataKeys.forEach((([e, , , s]) => { dataCk[e] && (t[e] = s()) })), syncSimDueUI(), post("SAVE_SETTINGS", { globalSettings: gSet, commSettings: commSet, stage: D.stage, deviationScore: D.deviationScore, simulationTarget: D.simulationTarget, playerLocation, dataChecked: dataCk, outlineData: t, allData: { meta: D.meta, timeline: D.timeline, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, strangers: D.contacts.strangers, contacts: contactsForSave() } }), closeM("m-settings") }, $("btn-close").onclick = () => post("CLOSE_PANEL"), window.addEventListener("message", (t => { if (t.origin !== PARENT_ORIGIN || t.source !== parent) return; if ("LittleWhiteBox" !== t.data?.source) return; const e = t.data, s = e.type; if ("LOAD_SETTINGS" === s) { if (e.globalSettings && (gSet = e.globalSettings), void 0 !== e.stage && (D.stage = e.stage), void 0 !== e.deviationScore && (D.deviationScore = e.deviationScore), void 0 !== e.simulationTarget && (D.simulationTarget = e.simulationTarget), e.playerLocation && (playerLocation = e.playerLocation), e.commSettings && (commSet = { historyCount: e.commSettings.historyCount ?? 50, npcPosition: e.commSettings.npcPosition ?? 0, npcOrder: e.commSettings.npcOrder ?? 100, stream: !!e.commSettings.stream }), e.dataChecked && (dataCk = e.dataChecked), e.promptConfig && (promptDefaults = e.promptConfig.defaults || promptDefaults, e.promptConfig.stores ? (promptStores.global = e.promptConfig.stores.global || { jsonTemplates: {}, promptSources: {} }, promptStores.character = e.promptConfig.stores.character || { jsonTemplates: {}, promptSources: {} }) : (promptStores.global = e.promptConfig.current || { jsonTemplates: {}, promptSources: {} }, promptStores.character = promptStores.character || { jsonTemplates: {}, promptSources: {} })), e.outlineData) { const t = e.outlineData; t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.outdoor && (D.maps.outdoor = t.outdoor), t.indoor && (D.maps.indoor = t.indoor), t.sceneSetup && (D.sceneSetup = t.sceneSetup), t.strangers && (D.contacts.strangers = t.strangers), t.contacts && (D.contacts.contacts = t.contacts) } { const t = e.characterContactSmsHistory || {}; charSmsHistory = { messages: Array.isArray(t.messages) ? t.messages : [], summarizedCount: t.summarizedCount || 0, summaries: t.summaries || {} } } let t = D.contacts.contacts.find((t => "__CHARACTER_CARD__" === t.worldbookUid)); t || (t = D.contacts.contacts.find((t => !t.worldbookUid && "炒饭智能" === t.name)), t ? (t.worldbookUid = "__CHARACTER_CARD__", t.info = "角色卡联络人", t.location = "在线", t.online = !0) : (D.contacts.contacts.unshift({ name: e.characterCardName || "{{characterName}}", avatar: "", color: "#555", location: "在线", info: "角色卡联络人", online: !0, worldbookUid: "__CHARACTER_CARD__", messages: [], summarizedCount: 0 }), t = D.contacts.contacts[0])), t && e.characterCardName && (t.name = e.characterCardName, t.avatar = (e.characterCardName || "")[0] || t.avatar || ""), render(), syncSimDueUI(), $("m-settings").classList.contains("act") && ($("set-api-url").value = gSet.apiUrl || "", $("set-api-key").value = gSet.apiKey || "", $("set-model").value = gSet.model || "", $("set-stage").value = D.stage, $("set-deviation").value = D.deviationScore, $("set-sim-target").value = D.simulationTarget ?? 5, $("set-mode").value = gSet.mode || "story", $("set-history-count").value = commSet.historyCount, $("set-use-stream").checked = !!commSet.stream, $("set-npc-position").value = commSet.npcPosition, $("set-npc-order").value = commSet.npcOrder, renderDataList()) } else if ("PROMPT_CONFIG_UPDATED" === s) { if (e.promptConfig && (promptDefaults = e.promptConfig.defaults || promptDefaults, e.promptConfig.stores ? (promptStores.global = e.promptConfig.stores.global || { jsonTemplates: {}, promptSources: {} }, promptStores.character = e.promptConfig.stores.character || { jsonTemplates: {}, promptSources: {} }) : (promptStores.global = e.promptConfig.current || { jsonTemplates: {}, promptSources: {} }, promptStores.character = promptStores.character || { jsonTemplates: {}, promptSources: {} }), $("m-adv-prompts").classList.contains("act"))) { const t = $("adv-key")?.value; t && advApplyToUI(t, advGetScope()) } } else if ("FETCH_MODELS_RESULT" === s) { BtnState.reset($("btn-fetch-models"), "获取"); const t = $("set-model-list"); if (e.error) return t.style.display = "none", void showTestRes(!1, "获取模型失败: " + e.error); if (!e.models?.length) return t.style.display = "none", void showTestRes(!1, "未找到可用模型"); t.innerHTML = '' + e.models.map((t => ``)).join(""), t.style.display = "block", t.onchange = () => { t.value && ($("set-model").value = t.value) }, showTestRes(!0, `找到 ${e.models.length} 个模型`) } else if ("TEST_CONN_RESULT" === s) BtnState.reset($("btn-test-conn"), "测试连接"), showTestRes(e.success, e.message); else if ("CHECK_WORLDBOOK_UID_RESULT" === s) { if (BtnState.reset($("btn-check-uid"), ' 检查'), !Req.match(e.requestId)) return; if (e.error) return void showUidErr(e.error); if (!e.primaryKeys?.length) return void showUidErr("该条目没有主要关键字"); addCtState.keys = e.primaryKeys; const t = $("add-name"); t.innerHTML = '' + e.primaryKeys.map((t => ``)).join(""), t.onchange = () => { addCtState.name = t.value, $("add-ct-ok").disabled = !t.value }, $("name-select-group").style.display = "block", 1 === e.primaryKeys.length && (addCtState.name = e.primaryKeys[0], t.value = addCtState.name, $("add-ct-ok").disabled = !1) } else if ("SMS_RESULT" === s) { const t = Req.get("sms"); if (!t || t.id !== e.requestId) return; if (Req.clear("sms"), smsGen = !1, $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !1, !chatTgt) return; chatTgt.messages = chatTgt.messages.filter((t => !t.typing)), e.error ? chatTgt.messages.push({ type: "received", text: `[错误] ${e.error}` }) : e.reply && chatTgt.messages.push({ type: "received", text: stripXml(e.reply) }), renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt) } else if ("SMS_STREAM" === s) { const t = Req.get("sms"); if (!t || t.id !== e.requestId || !chatTgt) return; const s = chatTgt.messages.find((t => t.typing)); s && e.text && (s.text = e.text, renderMsgs()) } else if ("LOAD_SMS_HISTORY_RESULT" === s) { if (!chatTgt || chatTgt.worldbookUid !== e.worldbookUid) return; e.messages?.length && (chatTgt.messages = e.messages, chatTgt.summarizedCount = e.summarizedCount || 0, saveCt()), renderMsgs() } else if ("COMPRESS_SMS_RESULT" === s) { const t = Req.get("compress"); if (!t || t.id !== e.requestId) return; if (Req.clear("compress"), smsGen = !1, $("chat-compress").disabled = $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !1, !chatTgt) return; if (e.error) return void alert(`压缩失败: ${e.error}`); void 0 !== e.newSummarizedCount && (chatTgt.summarizedCount = e.newSummarizedCount, renderMsgs(), saveCt()) } else if ("CHECK_STRANGER_WORLDBOOK_RESULT" === s) { const t = Req.get("stgwb"); if (!t || t.id !== e.requestId) return; const { name: s, info: o, btn: a, npcType: nt } = t; if (Req.clear("stgwb"), e.found && e.worldbookUid) { BtnState.reset(a, nt === "importantNpc" ? ' 重要' : ' 背景板'); const t = D.contacts.strangers.findIndex((t => t.name === s)); if (t > -1) { const s = D.contacts.strangers.splice(t, 1)[0]; D.contacts.contacts.push({ name: s.name, avatar: s.avatar || s.name[0], color: s.color || "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: s.location || "未知", info: s.info || "", worldbookUid: e.worldbookUid, messages: [] }), saveCt(), render() } } else { BtnState.load(a, "生成中"); const t = Req.create("npcgen"); Req.set("npcgen", { name: s, info: o, btn: a, npcType: nt }), post("GENERATE_NPC", { requestId: t, strangerName: s, strangerInfo: o, npcType: nt }) } } else if ("GENERATE_NPC_RESULT" === s) { const t = Req.get("npcgen"); if (!t || t.id !== e.requestId) return; const { name: s, btn: o, npcType: nt } = t; const _resetLbl = nt === "importantNpc" ? ' 重要' : ' 背景板'; if (Req.clear("npcgen"), BtnState.reset(o, _resetLbl), e.error) return void showResultModal("生成角色失败", "生成 NPC 失败", !0, e.error); if (e.success && e.worldbookUid) { const t = D.contacts.strangers.findIndex((t => t.name === s)); if (t > -1) { const o = D.contacts.strangers.splice(t, 1)[0], a = e.npcData || {}; D.contacts.contacts.push({ name: a.name || o.name, avatar: (a.name || o.name)[0], color: o.color || "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: o.location || "未知", info: a.intro || o.info || "", worldbookUid: e.worldbookUid, messages: [] }), saveCt(), render(), showResultModal("生成成功", `NPC ${s} 已生成并添加到联络人`, !1, e.npcData) } } } else if ("EXTRACT_STRANGERS_RESULT" === s) { const t = Req.get("extract"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("extract"), BtnState.reset(s, ''), e.error) return void showResultModal("提取失败", "提取陌路人失败", !0, e.error); if (e.success && Array.isArray(e.strangers)) { if (!e.strangers.length) return void showResultModal("提取结果", "没有发现新的陌路人"); const t = [...D.contacts.contacts.map((t => t.name)), ...D.contacts.strangers.map((t => t.name))], s = e.strangers.filter((e => !t.includes(e.name))); if (!s.length) return void showResultModal("提取结果", "提取到的角色都已存在"); D.contacts.strangers = D.contacts.strangers.concat(s), saveCt(), render(), showResultModal("提取成功", `成功提取 ${s.length} 个新陌路人`, !1, s) } } else if ("GENERATE_WORLD_STATUS" === s) { if (!Req.match(e.requestId)) return; const t = $("world-gen-status"); t.style.display = "block", t.style.color = "var(--ok)", t.textContent = e.message } else if ("GENERATE_WORLD_RESULT" === s) { if (!Req.match(e.requestId)) return; Req.clear("wgen"); const t = $("world-gen-ok"), s = $("world-gen-status"); if (BtnState.reset(t, ' 开始生成'), e.error) { if (s.style.display = "none", showResultModal("生成失败", "世界生成失败", !0, e.error), String(e.error || "").includes("Step 2")) { const e = $("res-action"); e.style.display = "inline-block", e.textContent = "重试 Step2", e.onclick = () => { closeM("m-result"); const e = Req.create("wgen"); BtnState.load(t, "重试中"), s.style.display = "block", s.style.color = "var(--ok)", s.textContent = "准备重试 Step 2/2...", post("RETRY_WORLD_GEN_STEP2", { requestId: e }) } } return } if (e.success && e.worldData) { s.style.color = "var(--ok)", s.textContent = "生成成功!正在应用数据..."; const t = e.worldData; if (t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.maps?.outdoor && (D.maps.outdoor = t.maps.outdoor), D.stage = 0, D.deviationScore = 0, t.playerLocation) playerLocation = t.playerLocation; else { const t = D.maps?.outdoor?.nodes?.find((t => "home" === t.type)); playerLocation = t?.name || D.maps?.outdoor?.nodes?.[0]?.name || "未知" } t.maps?.inside && playerLocation && (D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[playerLocation] = t.maps.inside), selectedMapValue = "current", saveAll(), render(), setTimeout((() => { closeM("m-world-gen"), s.style.display = "none", $("world-gen-req").value = "", showResultModal("生成成功", "世界数据生成完成!Stage 和 Deviation 已重置为 0", !1, e.worldData) }), 500) } } else if ("SIMULATE_WORLD_RESULT" === s) { if (!e.isAuto && !Req.match(e.requestId)) return; if (!e.isAuto) { Req.clear("wsim"); const t = $("world-sim-ok"), s = $("world-sim-status"); if (BtnState.reset(t, ' 开始推演'), e.error) return s.style.display = "none", void showResultModal("推演失败", "世界推演失败", !0, e.error) } if (e.success && e.simData) { if (!e.isAuto) { const t = $("world-sim-status"); t.style.color = "var(--ok)", t.textContent = "推演成功!正在应用数据..." } const t = e.simData; t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.maps?.outdoor && (D.maps.outdoor = t.maps.outdoor), D.stage = (D.stage || 0) + 1, saveAll(), render(), e.isAuto || setTimeout((() => { closeM("m-world-sim"), $("world-sim-status").style.display = "none", showResultModal("推演成功", `世界推演完成!Stage 已推进到 ${D.stage}`, !1, e.simData) }), 500) } } else if ("REFRESH_WORLD_NEWS_RESULT" === s) { const t = Req.get("newsrf"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("newsrf"), s && (s.disabled = !1, s.innerHTML = s._o ?? ''), e.error) return void showResultModal("刷新失败", "世界新闻刷新失败", !0, e.error); e.success && Array.isArray(e.news) && (D.world = D.world || {}, D.world.news = e.news, saveAll(), render(), showResultModal("刷新成功", `已更新世界新闻(${e.news.length} 条)`, !1, e.news)) } else if ("SCENE_SWITCH_RESULT" === s) { const t = Req.get("scene"); if (!t || t.id !== e.requestId) return; const { node: s, prev: o } = t; if (Req.clear("scene"), BtnState.reset($("goto-ok"), "确认前往"), closeM("m-goto"), e.error) return void showResultModal("切换失败", "场景切换失败", !0, e.error); if (e.success && e.sceneData) { const t = e.sceneData; if ("number" == typeof t.newScore && (D.deviationScore = t.newScore), t.localMap && (D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s.name] = t.localMap), t.strangers?.length) { const e = new Set((D.contacts.strangers || []).map((t => t.name))), s = t.strangers.filter((t => !e.has(t.name))); D.contacts.strangers = [...D.contacts.strangers || [], ...s] } finishTravel(s.name, o), 0 !== t.scoreDelta && showResultModal("切换成功", `场景切换完成!\n偏差值变化: ${t.scoreDelta > 0 ? "+" : ""}${t.scoreDelta} (当前: ${t.newScore})`, !1, e.sceneData) } } else if ("REFRESH_LOCAL_MAP_RESULT" === s) { const t = Req.get("localmaprf"); if (!t || t.id !== e.requestId) return; const { btn: s, loc: o } = t; if (Req.clear("localmaprf"), BtnState.reset(s, '刷新'), e.error) return void showResultModal("刷新失败", "刷新局部地图失败", !0, e.error); if (e.success && e.localMapData) { const t = e.localMapData, s = t.name || o || playerLocation || "当前位置"; D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s] = t, playerLocation = s, selectedMapValue = "current", saveAll(), render(), t.description && ($("side-desc").innerHTML = `
📍 ${h(s)}
` + parseLinks(t.description), bindLinks($("side-desc"))), showResultModal("刷新成功", `局部地图已刷新!当前位置: ${s}`, !1, e.localMapData) } } else if ("GENERATE_LOCAL_SCENE_RESULT" === s) { const t = Req.get("localscene"); if (!t || t.id !== e.requestId) return; const { btn: s, loc: o } = t; if (Req.clear("localscene"), BtnState.reset(s, '局部剧情'), e.error) return void showResultModal("生成失败", "局部剧情生成失败", !0, e.error); if (e.success && e.sceneSetup) { D.sceneSetup = { ...D.sceneSetup || {}, ...e.sceneSetup || {} }, saveAll(), render(); const t = String(e.introduce || "").replace(/^\s*(?:\/?\s*(?:sendas|as)\s+name\s*=\s*(?:"[^"]*"|'[^']*'|\S+)\s+)/i, "").replace(/\s+/g, " ").trim(); t && post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${t}` }), showResultModal("生成成功", `局部剧情已生成:${o || playerLocation}`, !1, e.sceneSetup) } } else if ("SEND_INVITE_RESULT" === s) { const t = Req.get("invite"); if (!t || t.id !== e.requestId) return; const { contact: s, loc: o, btn: a } = t; if (Req.clear("invite"), BtnState.reset(a, "发送邀请"), e.error) return void showResultModal("邀请失败", "邀请发送失败", !0, e.error); if (e.success && e.inviteData) { const t = e.inviteData, a = canonicalLoc(t.targetLocation || o || ""), n = D.contacts.contacts.find((t => t && s && t.worldbookUid && s.worldbookUid && t.worldbookUid === s.worldbookUid)) || D.contacts.contacts.find((t => t && s && t.name === s.name)) || s; if (n.messages = n.messages || [], n.messages.push({ type: "sent", text: `我邀请你前往「${a}」` }), n.messages.push({ type: "received", text: t.reply }), t.accepted) { const e = canonicalLoc(playerLocation); n.location = a, e && a && e === a ? (delete n.waitingAt, post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${n.name}过来了。` })) : n.waitingAt = a, showResultModal("邀请成功", `${n.name} 接受了邀请!\n回复: ${t.reply}`, !1, t) } else showResultModal("邀请被拒", `${n.name} 拒绝了邀请。\n回复: ${t.reply}`, !1, t); saveAll(), n.worldbookUid && saveChat(n), closeM("m-invite"), render(), openChat(n) } } else if ("GENERATE_LOCAL_MAP_RESULT" === s) { const t = Req.get("localmap"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("localmap"), BtnState.reset(s, '局部地图'), e.error) return void showResultModal("生成失败", "局部地图生成失败", !0, e.error); if (e.success && e.localMapData) { const t = e.localMapData, s = t.name || "当前位置"; D.sceneSetup = null, D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s] = t, playerLocation = s, selectedMapValue = "current", saveAll(), render(), t.description && ($("side-desc").innerHTML = `
📍 ${h(s)}
` + parseLinks(t.description), bindLinks($("side-desc"))), showResultModal("生成成功", `局部地图生成完成!当前位置: ${s}`, !1, t) } } })); const updateTf = () => { inner.style.transform = `translate(${offX}px,${offY}px) scale(${scale})`, $("zoom-ind").textContent = Math.round(100 * scale) + "%", requestAnimationFrame(drawLines) }, initPos = () => { offX = mapWrap.clientWidth / 2 - 2e3, offY = mapWrap.clientHeight / 2 - 2e3, scale = 1, updateTf() }; function panTo(t, e = 350) { if (!t || anim) return; if (!$(t.id)) return; const s = -(t.x + 2e3) * scale + mapWrap.clientWidth / 2, o = -(t.y + 2e3) * scale + .25 * mapWrap.clientHeight, a = offX, n = offY, r = performance.now(); anim = !0, function t(i) { const c = Math.min((i - r) / e, 1), l = 1 - Math.pow(1 - c, 3); offX = a + (s - a) * l, offY = n + (o - n) * l, updateTf(), c < 1 ? requestAnimationFrame(t) : anim = !1 }(r) } function showInfo(t) { if (!t?.data) return; curNode = t, inner.querySelectorAll(".item").forEach((t => t.classList.remove("hl"))), $(t.id)?.classList.add("hl"); const e = t.name === playerLocation; $("btn-goto").classList.toggle("show", !e), e || ($("goto-t").textContent = `前往 ${t.name}`); const s = D.maps?.indoor?.[t.name]; e && s?.description ? ($("side-desc").innerHTML = `
📍 ${h(t.name)}
` + parseLinks(s.description), bindLinks($("side-desc"))) : ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc"))), isMob() ? ($("mob-info-t").textContent = t.name, $("mob-info-c").textContent = e ? s?.description || t.data.info || "暂无信息..." : t.data.info || "暂无信息...", popup.classList.contains("act") || openPop(1)) : ($("info-t").textContent = t.name, $("info-c").textContent = t.data.info || "暂无信息...", $("tip").classList.add("show")) } const hideInfo = () => { curNode = null, inner.querySelectorAll(".item").forEach((t => t.classList.remove("hl"))), $("btn-goto").classList.remove("show"), $("tip").classList.remove("show") }; function renderMapSelector() { const t = $("map-lbl-select"); t.innerHTML = ''; const e = D.maps?.outdoor?.nodes?.findIndex((t => t.name === playerLocation)), s = D.maps?.indoor && D.maps.indoor[playerLocation]; if ((e >= 0 || s) && (t.innerHTML += ``), t.innerHTML += "", D.maps?.outdoor?.nodes?.length && D.maps.outdoor.nodes.forEach(((e, s) => { e.name !== playerLocation && (t.innerHTML += ``) })), D.maps?.indoor) { const e = Object.keys(D.maps.indoor).filter((t => t !== playerLocation && !D.maps?.outdoor?.nodes?.some((e => e.name === t)))); e.length && (t.innerHTML += "", e.forEach((e => t.innerHTML += ``))) } t.value = selectedMapValue, updateMapLabel() } function updateMapLabel() { const t = $("map-lbl-select").value; if ("overview" === t) $("map-lbl-t").textContent = "大地图"; else if ("current" === t) $("map-lbl-t").textContent = playerLocation + "(你)"; else if (t.startsWith("node:")) { const e = parseInt(t.split(":")[1]); $("map-lbl-t").textContent = D.maps?.outdoor?.nodes?.[e]?.name || "未知" } else t.startsWith("indoor:") && ($("map-lbl-t").textContent = t.replace("indoor:", "")) } function switchMapView(t) { if (selectedMapValue = t, hideInfo(), "overview" === t) $("btn-goto").classList.remove("show"), $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")), initPos(); else if ("current" === t) { $("btn-goto").classList.remove("show"); const t = getCurInside(); t?.description ? ($("side-desc").innerHTML = `
📍 ${h(playerLocation)}
` + parseLinks(t.description), bindLinks($("side-desc"))) : ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc"))); const e = nodes.find((t => t.name === playerLocation)); e && (panTo(e), showInfo(e)) } else if (t.startsWith("node:")) { const e = parseInt(t.split(":")[1]), s = D.maps?.outdoor?.nodes?.[e]; $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")); const o = nodes.find((t => t.name === s?.name)); o && (panTo(o), showInfo(o)) } else if (t.startsWith("indoor:")) { const e = t.replace("indoor:", ""), s = D.maps?.indoor?.[e]; e !== playerLocation ? ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")), curNode = { name: e, type: "sub", data: { info: stripXml(s?.description || "") }, isIndoor: !0 }, $("btn-goto").classList.add("show"), $("goto-t").textContent = `前往 ${e}`) : ($("btn-goto").classList.remove("show"), s?.description && ($("side-desc").innerHTML = `
🏠 ${h(e)}
` + parseLinks(s.description), bindLinks($("side-desc")))) } updateMapLabel() } $("info-bk").onclick = hideInfo, $("mob-info-bk").onclick = () => popup.classList.remove("act"), $("map-lbl-select").onchange = t => switchMapView(t.target.value); let sx, sy, lastDist = 0, lastCX = 0, lastCY = 0; mapWrap.onmousedown = t => { anim || t.target.closest(".map-act,.map-lbl") || (t.target.classList.contains("item") || hideInfo(), drag = !0, sx = t.clientX, sy = t.clientY, mapWrap.style.cursor = "grabbing") }, mapWrap.onmousemove = t => { drag && (offX += t.clientX - sx, offY += t.clientY - sy, sx = t.clientX, sy = t.clientY, updateTf()) }, mapWrap.onmouseup = mapWrap.onmouseleave = () => { drag = !1, mapWrap.style.cursor = "grab" }, mapWrap.onwheel = t => { if (anim) return; t.preventDefault(); const e = Math.max(.3, Math.min(3, scale + (t.deltaY > 0 ? -.1 : .1))), s = mapWrap.getBoundingClientRect(), o = t.clientX - s.left, a = t.clientY - s.top, n = e / scale; offX = o - (o - offX) * n, offY = a - (a - offY) * n, scale = e, updateTf() }, mapWrap.ontouchstart = t => { if (anim || t.target.closest(".map-act,.map-lbl")) return; const e = t.target.closest(".item"); e ? e._ts = { x: t.touches[0].clientX, y: t.touches[0].clientY, t: Date.now() } : (hideInfo(), 1 === t.touches.length ? (drag = !0, sx = t.touches[0].clientX, sy = t.touches[0].clientY) : 2 === t.touches.length && (drag = !1, lastDist = Math.hypot(t.touches[0].clientX - t.touches[1].clientX, t.touches[0].clientY - t.touches[1].clientY), lastCX = (t.touches[0].clientX + t.touches[1].clientX) / 2, lastCY = (t.touches[0].clientY + t.touches[1].clientY) / 2)) }, mapWrap.ontouchmove = t => { const e = t.target.closest(".item"); if (e && e._ts) Math.hypot(t.touches[0].clientX - e._ts.x, t.touches[0].clientY - e._ts.y) > 10 && (delete e._ts, drag = !0, sx = t.touches[0].clientX, sy = t.touches[0].clientY); else if (1 === t.touches.length && drag) t.preventDefault(), offX += t.touches[0].clientX - sx, offY += t.touches[0].clientY - sy, sx = t.touches[0].clientX, sy = t.touches[0].clientY, updateTf(); else if (2 === t.touches.length) { t.preventDefault(); const e = Math.hypot(t.touches[0].clientX - t.touches[1].clientX, t.touches[0].clientY - t.touches[1].clientY), s = (t.touches[0].clientX + t.touches[1].clientX) / 2, o = (t.touches[0].clientY + t.touches[1].clientY) / 2, a = Math.max(.3, Math.min(3, scale * (e / lastDist))), n = mapWrap.getBoundingClientRect(), r = s - n.left, i = o - n.top, c = a / scale; offX = r - (r - offX) * c, offY = i - (i - offY) * c, offX += s - lastCX, offY += o - lastCY, scale = a, lastDist = e, lastCX = s, lastCY = o, updateTf() } }, mapWrap.ontouchend = t => { const e = t.target.closest(".item"); if (e && e._ts) { const s = Date.now() - e._ts.t; if (delete e._ts, s < 300) { const s = nodes.find((t => t.id === e.id)); s && (t.preventDefault(), curNode?.id === s.id ? hideInfo() : showInfo(s)) } } drag = !1 }, $$(".nav-i").forEach((t => t.onclick = () => { $$(".nav-i").forEach((t => t.classList.remove("act"))), $$(".page").forEach((t => t.classList.remove("act"))), t.classList.add("act"), $(`page-${t.dataset.p}`).classList.add("act"); const e = "map" === t.dataset.p; sidePop.classList.toggle("show", e), isMob() && (e ? openPop(1) : popup.classList.remove("act")), e && setTimeout((() => { initPos(), drawLines() }), 50) })), $$(".comm-tab").forEach((t => t.onclick = () => { $$(".comm-tab").forEach((t => t.classList.remove("act"))), $$(".comm-sec").forEach((t => t.classList.remove("act"))), t.classList.add("act"), $(`sec-${t.dataset.t}`).classList.add("act") })), $("btn-goto").onclick = t => { t.stopPropagation(), curNode && ($("goto-d").textContent = `目的地:${curNode.name}`, $("goto-task").value = "", openM("m-goto")) }, addEventListener("resize", (() => requestAnimationFrame(drawLines))), window.clickTab = (t, e) => { const s = t.closest(".settings-modal"); if (!s) return; s.querySelectorAll(".set-nav-item").forEach((t => t.classList.remove("act"))), t.classList.add("act"), s.querySelectorAll(".set-tab-page").forEach((t => t.classList.remove("act"))); const o = document.getElementById(e); o && (o.classList.add("act"), o.style.animation = "none", o.offsetHeight, o.style.animation = null) }, document.addEventListener("DOMContentLoaded", (() => { render(), initPos(), sidePop.classList.add("show"), sidePop.classList.add("act"), isMob() && openPop(1), post("FRAME_READY"), setTimeout((() => { "current" === selectedMapValue && switchMapView("current") }), 100) })); diff --git a/modules/story-outline/story-outline.js b/modules/story-outline/story-outline.js index 3a0a08c..c9b74bf 100644 --- a/modules/story-outline/story-outline.js +++ b/modules/story-outline/story-outline.js @@ -32,7 +32,7 @@ import { StoryOutlineStorage } from "../../core/server-storage.js"; import { promptManager } from "../../../../../openai.js"; import { buildSmsMessages, buildSummaryMessages, buildSmsHistoryContent, buildExistingSummaryContent, - buildNpcGenerationMessages, formatNpcToWorldbookContent, buildExtractStrangersMessages, + buildNpcGenerationMessages, buildImportantNpcGenerationMessages, formatNpcToWorldbookContent, buildExtractStrangersMessages, buildWorldGenStep1Messages, buildWorldGenStep2Messages, buildWorldSimMessages, buildSceneSwitchMessages, buildInviteMessages, buildLocalMapGenMessages, buildLocalMapRefreshMessages, buildLocalSceneGenMessages, buildOverlayHtml, MOBILE_LAYOUT_STYLE, DESKTOP_LAYOUT_STYLE, getPromptConfigPayload, setPromptConfig @@ -874,14 +874,17 @@ async function handleCheckStrangerWb({ requestId, strangerName }) { postFrame({ type: 'CHECK_STRANGER_WORLDBOOK_RESULT', requestId, found: !!r, ...(r && { worldbookUid: r.uid, worldbook: r.bookName, entryName: r.entry.comment || r.entry.key?.[0] || strangerName }) }); } -async function handleGenNpc({ requestId, strangerName, strangerInfo }) { +async function handleGenNpc({ requestId, strangerName, strangerInfo, npcType = 'npc' }) { try { const comm = getCommSettings(); const ctx = getContext(), char = ctx.characters?.[ctx.characterId]; if (!char) return replyErr('GENERATE_NPC_RESULT', requestId, '未找到当前角色卡'); const primary = char.data?.extensions?.world; if (!primary || !world_names?.includes(primary)) return replyErr('GENERATE_NPC_RESULT', requestId, '角色卡未绑定世界书,请先绑定世界书'); - const msgs = buildNpcGenerationMessages(getCommonPromptVars({ strangerName, strangerInfo: strangerInfo || '(无描述)' })); + const vars = getCommonPromptVars({ strangerName, strangerInfo: strangerInfo || '(无描述)' }); + const msgs = npcType === 'importantNpc' + ? buildImportantNpcGenerationMessages(vars) + : buildNpcGenerationMessages(vars); const npc = await callLLMJson({ messages: msgs, validate: V.npc }); if (!npc?.name) return replyErr('GENERATE_NPC_RESULT', requestId, 'NPC 生成失败:无法解析 JSON 数据'); const wd = await loadWorldInfo(primary); if (!wd) return replyErr('GENERATE_NPC_RESULT', requestId, `无法加载世界书: ${primary}`); From 63461d78af3659508f7af00fbc2adb55c2de192e Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:18:47 +0800 Subject: [PATCH 21/22] =?UTF-8?q?=E9=AB=98=E7=BA=A7=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E7=BC=96=E8=BE=91=E5=99=A8=E5=8A=A0=E6=B3=A8?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- modules/story-outline/story-outline.html | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/story-outline/story-outline.html b/modules/story-outline/story-outline.html index d64c941..b21046e 100644 --- a/modules/story-outline/story-outline.html +++ b/modules/story-outline/story-outline.html @@ -2678,6 +2678,7 @@

高级设置 · 模板编辑

UAUA 提示词 (JS Function String)
+
原调色盘提示词经由三明月佬授权二创使用,请勿未经允许散播
From f8cea6a18d7c06a953fda911948b08b51645c9c8 Mon Sep 17 00:00:00 2001 From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:21:32 +0800 Subject: [PATCH 22/22] =?UTF-8?q?=E6=8E=88=E6=9D=83=E5=A3=B0=E6=98=8E?= =?UTF-8?q?=E4=BB=85=E5=9C=A8=E9=87=8D=E8=A6=81NPC=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E4=B8=8B=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- modules/story-outline/story-outline.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/story-outline/story-outline.html b/modules/story-outline/story-outline.html index b21046e..9fc4e8f 100644 --- a/modules/story-outline/story-outline.html +++ b/modules/story-outline/story-outline.html @@ -2678,7 +2678,7 @@

高级设置 · 模板编辑

UAUA 提示词 (JS Function String)
-
原调色盘提示词经由三明月佬授权二创使用,请勿未经允许散播
+
@@ -2872,7 +2872,7 @@

操作结果

(() => { const t = getStoredTheme(); "dark" === t && setTheme("dark") })(); $("btn-theme")?.addEventListener("click", toggleTheme); - const inds = popup.querySelectorAll(".pop-h-ind span"), setPopH = t => { const e = snaps(); popH = Math.max(e[0], Math.min(.85 * innerHeight, t)), popup.style.height = popH + "px", popLv = e.map(((t, e) => [e, Math.abs(popH - t)])).sort(((t, e) => t[1] - e[1]))[0][0], inds.forEach(((t, e) => t.classList.toggle("act", e === popLv))) }, snapTo = t => { popLv = Math.max(0, Math.min(2, t)), setPopH(snaps()[popLv]) }, openPop = (t = 1) => { popup.classList.add("act"), snapTo(t) }; $("side-pop-handle").onclick = () => sidePop.classList.toggle("act"), $("pop-hd").onmousedown = t => { popDrag = !0, popSY = t.clientY, popSH = popH || snaps()[1], popup.classList.add("drag"), t.preventDefault() }, $("pop-hd").ontouchstart = t => { t.preventDefault(), popDrag = !0, popSY = t.touches[0].clientY, popSH = popH || snaps()[1], popup.classList.add("drag") }, document.onmousemove = t => { popDrag && setPopH(popSH + popSY - t.clientY) }, document.ontouchmove = t => { popDrag && t.touches.length && (t.preventDefault(), setPopH(popSH + popSY - t.touches[0].clientY)) }; const endDrag = () => { popDrag && (popDrag = !1, popup.classList.remove("drag"), snapTo(popLv)) }; document.onmouseup = endDrag, document.ontouchend = endDrag, document.ontouchcancel = endDrag; const bindLinks = t => t.querySelectorAll(".loc-lk").forEach((t => t.onclick = e => { e.stopPropagation(); const s = t.dataset.loc, o = nodes.find((t => t.name === s)); if (o) return panTo(o), void showInfo(o); const a = getCurInside(), n = a?.nodes?.find((t => t.name === s)); n && ($("info-t").textContent = n.name, $("info-c").textContent = n.info || "暂无信息...", $("tip").classList.add("show"), $("btn-goto").classList.remove("show"), isMob() && ($("mob-info-t").textContent = n.name, $("mob-info-c").textContent = n.info || "暂无信息...", popup.classList.contains("act") || openPop(1))) })), bindFold = t => t.querySelector(".fold-h").onclick = () => t.classList.toggle("exp"); $$(".modal-bd,.modal-x,.m-cancel").forEach((t => t.onclick = () => t.closest(".modal").classList.remove("act"))); const openChat = t => { chatTgt = t, $("chat-av").textContent = t.avatar, $("chat-av").style.background = t.color, $("chat-nm").textContent = t.name, $("chat-st").textContent = t.online ? "● 在线" : t.location, !t.worldbookUid || t.messages && t.messages.length ? renderMsgs() : post("LOAD_SMS_HISTORY", { worldbookUid: t.worldbookUid }), chat.classList.add("act"), $("chat-in").focus() }, closeChat = () => { chat.classList.remove("act"), chatTgt = null, smsGen = !1 }, renderMsgs = () => { if (!chatTgt) return; const t = chatTgt.messages || [], e = chatTgt.summarizedCount || 0; let s = ""; t.length ? t.forEach(((t, o) => { e > 0 && o === e && (s += '
—— 以上为已总结消息 ——
'), s += `
${escHtml(stripXml(t.text))}
` })) : s = '
暂无消息,开始聊天吧
', $("chat-msgs").innerHTML = s, $("chat-msgs").scrollTop = $("chat-msgs").scrollHeight, $("chat-compress").disabled = t.length - e < 2 || smsGen, $("chat-back").disabled = !t.length || smsGen }, sendMsg = () => { const t = $("chat-in").value.trim(); if (!t || !chatTgt || smsGen) return; chatTgt.messages = chatTgt.messages || [], chatTgt.messages.push({ type: "sent", text: t }), $("chat-in").value = "", renderMsgs(), smsGen = !0, $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !0, chatTgt.messages.push({ type: "received", text: "正在输入...", typing: !0 }), renderMsgs(); const e = Req.create("sms"); Req.set("sms", { tgt: chatTgt }), post("SEND_SMS", { requestId: e, contactName: chatTgt.name, worldbookUid: chatTgt.worldbookUid, userMessage: t, chatHistory: chatTgt.messages.filter((t => !t.typing)).slice(-20), summarizedCount: chatTgt.summarizedCount || 0 }) }, isCharCardContact = t => "__CHARACTER_CARD__" === t?.worldbookUid, contactsForSave = () => (D.contacts.contacts || []).filter((t => !isCharCardContact(t))), saveCt = () => post("SAVE_CONTACTS", { contacts: contactsForSave(), strangers: D.contacts.strangers }), saveChat = t => post("SAVE_SMS_HISTORY", { worldbookUid: t.worldbookUid, contactName: t.name, messages: t.messages.filter((t => !t.typing)), summarizedCount: t.summarizedCount || 0 }); $("chat-x").onclick = closeChat, $("chat-back").onclick = () => { if (!chatTgt || smsGen) return; const t = chatTgt.messages || []; if (t.length) { for (t.pop(); t[t.length - 1]?.typing;)t.pop(); chatTgt.summarizedCount = Math.min(chatTgt.summarizedCount || 0, t.length), renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt) } }, $("chat-clr").onclick = () => { chatTgt && confirm(`确定要清空与 ${chatTgt.name} 的所有聊天记录吗?`) && (chatTgt.messages = [], chatTgt.summarizedCount = 0, renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt)) }, $("chat-compress").onclick = () => { if (!chatTgt || smsGen) return; const t = chatTgt.messages || [], e = chatTgt.summarizedCount || 0, s = t.slice(e); if (s.length < 2) return void alert("至少需要2条未总结的消息才能压缩"); if (!confirm(`确定要压缩总结 ${s.length} 条消息吗?`)) return; smsGen = !0, $("chat-compress").disabled = $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !0; const o = Req.create("compress"); Req.set("compress", { tgt: chatTgt }), post("COMPRESS_SMS", { requestId: o, contactName: chatTgt.name, worldbookUid: chatTgt.worldbookUid, messages: t.filter((t => !t.typing)), summarizedCount: e }) }, $("chat-send").onclick = sendMsg; const chatIn = $("chat-in");["keydown", "keypress", "keyup"].forEach((t => chatIn.addEventListener(t, (t => t.stopPropagation())))), chatIn.addEventListener("keydown", (t => { "Enter" !== t.key || t.shiftKey || (t.preventDefault(), sendMsg()) })); const openInv = t => { invTgt = t, selLoc = null, $("inv-t").textContent = `邀请:${t.name}`, $("loc-list").innerHTML = D.maps.outdoor.nodes.map((t => `
${h(t.name)}
${h(t.info || "")}
`)).join(""), $$("#loc-list .loc-i").forEach((t => t.onclick = () => { $$("#loc-list .loc-i").forEach((t => t.classList.remove("sel"))), t.classList.add("sel"), selLoc = t.dataset.n })), openM("m-invite") }; $("inv-ok").onclick = () => { if (!selLoc || !invTgt) return; const t = $("inv-ok"); BtnState.load(t, "询问中..."); const e = Req.create("invite"), s = canonicalLoc(selLoc); Req.set("invite", { contact: invTgt, loc: s, btn: t }), post("SEND_INVITE", { requestId: e, contactName: invTgt.name, contactUid: invTgt.worldbookUid, targetLocation: s, smsHistory: (invTgt.messages || []).map((t => "sent" === t.type ? `{{user}}: ${t.text}` : `${invTgt.name}: ${t.text}`)).join("\n") }) }; let addCtState = { uid: "", name: "", keys: [] }; const resetAddCt = () => { addCtState = { uid: "", name: "", keys: [] }, $("add-uid").value = "", $("add-name").value = "", $("add-name").innerHTML = '', $("name-select-group").style.display = "none", $("uid-check-err").classList.remove("vis"), $("add-ct-ok").disabled = !0, BtnState.reset($("btn-check-uid"), ' 检查') }, showUidErr = t => { $("uid-check-err").textContent = t, $("uid-check-err").classList.add("vis") }; $("btn-check-uid").onclick = () => { const t = $("add-uid").value.trim(); t ? ($("uid-check-err").classList.remove("vis"), BtnState.load($("btn-check-uid"), "检查中"), $("name-select-group").style.display = "none", $("add-ct-ok").disabled = !0, addCtState.uid = t, post("CHECK_WORLDBOOK_UID", { uid: t, requestId: Req.create("uidck") })) : showUidErr("请输入UID") }, $("btn-add-ct").onclick = () => { resetAddCt(), openM("m-add-ct") }, $("add-ct-ok").onclick = () => { const t = addCtState.uid || $("add-uid").value.trim(), e = addCtState.name || $("add-name").value.trim(); t && e && (D.contacts.contacts.some((e => e.worldbookUid === t)) ? showUidErr("该联络人已存在") : (D.contacts.contacts.push({ name: e, avatar: e[0], color: "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: "未知", worldbookUid: t, messages: [] }), saveCt(), closeM("m-add-ct"), render())) }; const genAddCt = (t, e, s, npcType = "npc") => { BtnState.load(s, "检查中"); const o = Req.create("stgwb"); Req.set("stgwb", { name: t, info: e, btn: s, npcType }), post("CHECK_STRANGER_WORLDBOOK", { requestId: o, strangerName: t }) }; function canonicalLoc(t) { return String(t || "").trim().replace(/^\u90ae\u8f6e/, "") } $("btn-refresh-strangers").onclick = () => { const t = $("btn-refresh-strangers"); BtnState.load(t, ""); const e = Req.create("extract"); Req.set("extract", { btn: t }), post("EXTRACT_STRANGERS", { requestId: e, existingContacts: D.contacts.contacts, existingStrangers: D.contacts.strangers }) }, $("world-gen-ok").onclick = () => { const t = $("world-gen-ok"), e = $("world-gen-status"); BtnState.load(t, "生成中"), e.style.display = "block", e.textContent = "正在生成世界数据,请稍候...", post("GENERATE_WORLD", { requestId: Req.create("wgen"), playerRequests: $("world-gen-req").value.trim() }) }, $("world-sim-ok").onclick = () => { if (!D.meta && !D.timeline && !D.maps?.outdoor) return void alert("请先生成世界数据,再进行推演"); const t = $("world-sim-ok"), e = $("world-sim-status"); BtnState.load(t, "推演中"), e.style.display = "block", e.style.color = "var(--ok)", e.textContent = "正在分析玩家行为并推演世界变化...", post("SIMULATE_WORLD", { requestId: Req.create("wsim"), currentData: JSON.stringify({ meta: D.meta, timeline: D.timeline, world: D.world, maps: D.maps }, null, 2) }) }, $("btn-deduce").onclick = () => openM("m-world-gen"), $("btn-simulate").onclick = () => openM("m-world-sim"), $("btn-side-menu-toggle").onclick = () => { const t = $("side-menu-panel"), e = $("btn-side-menu-toggle"); t.classList.toggle("show"), e.classList.toggle("act", t.classList.contains("show")) }, document.addEventListener("click", (t => { t.target.closest(".side-menu") || ($("side-menu-panel")?.classList.remove("show"), $("btn-side-menu-toggle")?.classList.remove("act")) })); const getWaitingContacts = t => { const e = canonicalLoc(t); return e ? (D.contacts.contacts || []).filter((t => t?.waitingAt && canonicalLoc(t.waitingAt) === e)) : [] }, finishTravel = (t, e) => { playerLocation = t, selectedMapValue = "current"; const s = getWaitingContacts(t); s.forEach((t => delete t.waitingAt)), saveAll(), render(), hideInfo(); const o = $("goto-task")?.value; let a = `{{user}}离开了${e || "上一地点"},来到${t}。${o ? "意图:" + o : ""}`; s.length && (a += ` ${s.map((t => t.name)).join("、")}已经在这里等你了。`), post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${a}` }) }; $("goto-ok").onclick = () => { if (!curNode) return; const t = $("goto-ok"), e = D.maps?.outdoor?.nodes?.find((t => t.name === playerLocation)), s = { name: playerLocation, info: e?.info || "" }, o = curNode.name, a = D.maps?.indoor?.[o]; if (a?.description) return BtnState.reset(t, "确认前往"), closeM("m-goto"), finishTravel(o, s.name), void switchMapView("current"); let n = "main" === curNode.type ? "main" : "home" === curNode.type ? "home" : "sub"; const r = Req.create("scene"); Req.set("scene", { node: curNode, prev: s.name }), BtnState.load(t, "生成中"), post("SCENE_SWITCH", { requestId: r, prevLocationName: s.name, prevLocationInfo: s.info, targetLocationName: curNode.name, targetLocationType: n, targetLocationInfo: curNode.data?.info || "", playerAction: $("goto-task").value || "" }) }, $("btn-gen-local-map").onclick = () => { const t = $("btn-gen-local-map"); BtnState.load(t, "生成中"); const e = Req.create("localmap"); Req.set("localmap", { btn: t }), post("GENERATE_LOCAL_MAP", { requestId: e, outdoorDescription: D.maps?.outdoor?.description || "" }) }, $("btn-refresh-local-map").onclick = () => { if (!playerLocation) return void showResultModal("提示", "请先生成世界数据", !0); const t = $("btn-refresh-local-map"), e = D.maps?.indoor?.[playerLocation], s = e; if (!s) return void showResultModal("提示", "当前区域没有局部地图,请先生成", !0); BtnState.load(t, "刷新中"); const o = Req.create("localmaprf"); Req.set("localmaprf", { btn: t, loc: playerLocation }), post("REFRESH_LOCAL_MAP", { requestId: o, locationName: playerLocation, currentLocalMap: s, outdoorDescription: D.maps?.outdoor?.description || "" }) }, $("btn-gen-local-scene").onclick = () => { if (!playerLocation || "未知" === playerLocation) return void showResultModal("提示", "请先生成世界数据", !0); const t = $("btn-gen-local-scene"); BtnState.load(t, "生成中"); const e = D.maps?.outdoor?.nodes?.find((t => t.name === playerLocation)), s = D.maps?.indoor?.[playerLocation], o = s?.description || e?.info || e?.data?.info || "", a = Req.create("localscene"); Req.set("localscene", { btn: t, loc: playerLocation }), post("GENERATE_LOCAL_SCENE", { requestId: a, locationName: playerLocation, locationInfo: o }) }, $("btn-refresh-world-news").onclick = () => { const t = $("btn-refresh-world-news"); if (!t) return; t.disabled = !0, t._o = t._o ?? t.innerHTML, t.innerHTML = ''; const e = Req.create("newsrf"); Req.set("newsrf", { btn: t }), post("REFRESH_WORLD_NEWS", { requestId: e }) }; const saveAll = () => post("SAVE_ALL_DATA", { allData: { meta: D.meta, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, sceneSetup: D.sceneSetup, strangers: D.contacts.strangers, contacts: contactsForSave() }, playerLocation }), dataKeys = [["meta", "大纲", "核心真相、洋葱结构、时间线、用户指南", () => D.meta, t => D.meta = t], ["world", "世界资讯", "世界新闻等信息", () => D.world, t => D.world = t], ["outdoor", "大地图", "室外区域的地点和路线", () => D.maps.outdoor, t => D.maps.outdoor = t], ["indoor", "局部地图", "隐藏的室内/局部场景地图", () => D.maps.indoor, t => D.maps.indoor = t], ["sceneSetup", "区域剧情", "当前区域的 Side Story", () => D.sceneSetup, t => D.sceneSetup = t], ["characterContactSms", "角色卡短信", "角色卡联络人的短信记录", () => ({ messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, summaries: charSmsHistory?.summaries || {} }), t => { t && "object" == typeof t && (charSmsHistory = { messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, ...t || {} }) }], ["strangers", "陌路人", "已遇见但未建立联系的角色", () => D.contacts.strangers, t => D.contacts.strangers = t], ["contacts", "联络人", "已添加的联系人", () => contactsForSave(), t => { const e = (D.contacts.contacts || []).find(isCharCardContact); D.contacts.contacts = (e ? [e] : []).concat(Array.isArray(t) ? t : []) }]]; let gSet = { apiUrl: "", apiKey: "", model: "", mode: "assist" }, dataCk = {}, editCtx = null, commSet = { historyCount: 50, npcPosition: 0, npcOrder: 100, stream: !1 }, promptDefaults = { jsonTemplates: {}, promptSources: {} }, promptStores = { global: { jsonTemplates: {}, promptSources: {} }, character: { jsonTemplates: {}, promptSources: {} } }; const reqSet = () => post("GET_SETTINGS"), renderDataList = () => { $("data-list").innerHTML = dataKeys.map((([t, e, s]) => `
${e}
${s}
`)).join(""), $$("#data-list .data-item").forEach((t => t.onclick = e => { if (e.target.closest(".data-edit")) return; const s = t.dataset.k; dataCk[s] = !dataCk[s], t.classList.toggle("sel", dataCk[s]) })), $$("#data-list .data-edit").forEach((t => t.onclick = e => { e.stopPropagation(), openDataEdit(t.dataset.k) })) }, ADV_PROMPT_ITEMS = [["sms", "短信回复"], ["invite", "邀请回复"], ["npc", "NPC 生成"], ["importantNpc", "重要NPC生成"], ["stranger", "提取陌路人"], ["worldGenStep1", "大纲生成"], ["worldGenStep2", "世界生成"], ["worldSim", "世界推演(故事模式)"], ["worldSimAssist", "世界推演(辅助模式)"], ["worldNewsRefresh", "世界新闻刷新"], ["sceneSwitch", "场景切换"], ["localMapGen", "局部地图生成"], ["localMapRefresh", "局部地图刷新"], ["localSceneGen", "局部剧情生成"], ["summary", "总结压缩"]], advHasJsonTemplate = t => { const e = promptDefaults?.jsonTemplates || {}; return Object.prototype.hasOwnProperty.call(e, t) }, advGetScope = () => "global" === $("adv-scope")?.value ? "global" : "character", advStoreHasKey = (t, e) => { const s = ("global" === t ? promptStores.global : promptStores.character) || {}; return !(!Object.prototype.hasOwnProperty.call(s?.promptSources || {}, e) && !Object.prototype.hasOwnProperty.call(s?.jsonTemplates || {}, e)) }, advGetPromptObj = (t, e = "character") => { const s = promptDefaults?.promptSources || {}, o = promptStores?.global?.promptSources || {}, a = promptStores?.character?.promptSources || {}, n = ("global" === e ? o[t] || s[t] : a[t] || o[t] || s[t]) || {}; return { u1: "string" == typeof n.u1 ? n.u1 : "", a1: "string" == typeof n.a1 ? n.a1 : "", u2: "string" == typeof n.u2 ? n.u2 : "", a2: "string" == typeof n.a2 ? n.a2 : "" } }, advNormalizeDisplayText = t => { let e = String(t ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"); return !e.includes("\n") && e.includes("\\n") && (e = e.replaceAll("\\n", "\n")), e.includes("\\t") && (e = e.replaceAll("\\t", "\t")), e.includes("\\`") && (e = e.replaceAll("\\`", "`")), e }, advNormalizeSaveText = t => String(t ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"), advGetJsonTemplate = (t, e = "character") => { const s = promptDefaults?.jsonTemplates || {}, o = promptStores?.global?.jsonTemplates || {}, a = promptStores?.character?.jsonTemplates || {}; if (!(advHasJsonTemplate(t) || Object.prototype.hasOwnProperty.call(o, t) || Object.prototype.hasOwnProperty.call(a, t))) return ""; const n = "global" === e ? Object.prototype.hasOwnProperty.call(o, t) ? o[t] : s[t] : Object.prototype.hasOwnProperty.call(a, t) ? a[t] : Object.prototype.hasOwnProperty.call(o, t) ? o[t] : s[t]; return "string" == typeof n ? n : "" }, advApplyToUI = (t, e = "character") => { const s = advGetPromptObj(t, e), o = (t, e) => { const s = $(t); s && (s.value = advNormalizeDisplayText(e)) }; o("adv-u1", s.u1), o("adv-a1", s.a1), o("adv-u2", s.u2), o("adv-a2", s.a2), $("adv-json-wrap").style.display = ""; const a = advGetJsonTemplate(t, e), n = $("adv-json"); n && (n.value = String(a ?? "")), advUpdateVarHelp(t) }, advUpdateVarHelp = t => { $$(".adv-vars-group").forEach((e => { const s = String(e.dataset.advFor || "").split(",").map((t => t.trim())).filter(Boolean); e.style.display = !s.length || s.includes(t) ? "" : "none" })) }, advBuildEdits = () => { const t = t => advNormalizeSaveText($(t)?.value ?? ""); return { prompt: { u1: t("adv-u1"), a1: t("adv-a1"), u2: t("adv-u2"), a2: t("adv-a2") }, jsonTemplate: advNormalizeSaveText($("adv-json")?.value ?? "") } }, advInit = () => { const t = $("adv-key"); if (!t || t._inited) return; t._inited = !0, t.innerHTML = ADV_PROMPT_ITEMS.map((([t, e]) => ``)).join(""), t.onchange = () => advApplyToUI(t.value, advGetScope()); const e = $("adv-scope"); e && !e._inited && (e._inited = !0, e.onchange = () => { const t = $("adv-key")?.value; t && advApplyToUI(t, advGetScope()) }) }, advOpen = () => { advInit(); const t = $("adv-key"), e = t?.value || ADV_PROMPT_ITEMS[0]?.[0]; if (e) { const t = $("adv-scope"); t && (t.value = advStoreHasKey("character", e) ? "character" : "global"), advApplyToUI(e, advGetScope()) } openM("m-adv-prompts") }, advSaveTo = t => { advInit(); const e = $("adv-key")?.value; if (!e) return; const { prompt: s, jsonTemplate: o } = advBuildEdits(); post("SAVE_PROMPTS", { scope: t, key: e, prompt: s, jsonTemplate: o }), closeM("m-adv-prompts") }, advReset = () => { advInit(); const t = $("adv-key")?.value; t && (post("SAVE_PROMPTS", { scope: advGetScope(), key: t, reset: !0 }), closeM("m-adv-prompts")) }, parseJsonLoose = t => { const e = String(t ?? "").trim(); if (!e) throw new Error("空内容"); try { return JSON.parse(e) } catch { } const s = e.match(/```[^\n]*\n([\s\S]*?)\n```/); if (s?.[1]) { const t = s[1].trim(); try { return JSON.parse(t) } catch { } } const o = (t, s) => { const o = e.indexOf(t), a = e.lastIndexOf(s); return -1 === o || -1 === a || a <= o ? null : e.slice(o, a + 1) }, a = o("{", "}") ?? o("[", "]"); return a ? JSON.parse(a) : JSON.parse(e) }, updateEditPreview = () => { const t = $("data-edit-preview"); t && (t.style.display = "none", t.textContent = "") }, setEditContent = (t, e) => { $("data-edit-title").textContent = t, $("data-edit-ta").value = e, $("data-edit-err").classList.remove("vis"), updateEditPreview(), openM("m-data-edit") }, openDataEdit = t => { const e = dataKeys.find((([e]) => e === t)); e && (editCtx = { type: "characterContactSms" === t ? "charSms" : "data", key: t }, setEditContent(`编辑 - ${e[1]}`, JSON.stringify(e[3](), null, 2))) }; $("data-edit-save").onclick = () => { if (editCtx) try { const t = parseJsonLoose($("data-edit-ta").value); if ("data" === editCtx.type) { const e = dataKeys.find((([t]) => t === editCtx.key)); if (!e) return; e[4](t), render(), saveAll() } else if ("charSms" === editCtx.type) { const e = t?.summaries ?? t; if (!e || "object" != typeof e || Array.isArray(e)) throw new Error("需要 summaries 对象"); charSmsHistory.summaries = e, post("SAVE_CHAR_SMS_HISTORY", { summaries: e }) } closeM("m-data-edit"), editCtx = null } catch (t) { $("data-edit-err").textContent = `JSON错误: ${t.message}`, $("data-edit-err").classList.add("vis") } }, $("data-edit-ta").addEventListener("input", updateEditPreview); const showTestRes = (t, e) => { const s = $("test-res"); s.textContent = e, s.className = "set-test-res " + (t ? "ok" : "err") }, showResultModal = (t, e, s = !1, o = null) => { $("res-title").textContent = t, $("res-title").style.color = s ? "var(--err)" : "", $("res-msg").textContent = e; const a = $("res-record-box"), n = $("res-record"); o ? (a.style.display = "block", n.textContent = "object" == typeof o ? JSON.stringify(o, null, 2) : String(o)) : (a.style.display = "none", n.textContent = ""); const r = $("res-action"); r.style.display = "none", r.textContent = "", r.onclick = null, openM("m-result") }; function render() { const t = D.world?.news || []; $("news-list").innerHTML = t.length ? t.map((t => `
${h(t.title)}
${h(t.time || "")}

${h(t.content)}

`)).join("") : '
暂无新闻
', $$("#news-list .fold").forEach(bindFold); const e = D.meta?.user_guide; e && ($("ug-state").textContent = e.current_state || "未知状态", $("ug-actions").innerHTML = (e.guides || []).map(((t, e) => `
${e + 1}. ${h(t)}
`)).join("") || '
暂无行动指南
'); const s = (t, e) => (t || []).length ? t.map((t => `
${h(t.avatar || "")}
${h(t.name || "")}
${t.online ? "● 在线" : h(t.location)}
${t.info ? `
${h(t.info)}
` : ""}
${e ? `` : ``}
`)).join("") : '
暂无
'; if ($("sec-stranger").innerHTML = s(D.contacts.strangers, !0), $("sec-contact").innerHTML = s(D.contacts.contacts, !1), $$(".comm-sec .fold").forEach(bindFold), $$(".add-btn").forEach((t => t.onclick = e => { e.stopPropagation(), genAddCt(t.dataset.name, t.dataset.info || "", t, t.dataset.npctype || "npc") })), $$(".ignore-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.strangers.findIndex((e => e.name === t.dataset.name)); s > -1 && (D.contacts.strangers.splice(s, 1), saveCt(), render()) })), $$(".msg-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.contacts.find((e => e.worldbookUid === t.dataset.uid)); s && openChat(s) })), $$(".inv-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.contacts.find((e => e.worldbookUid === t.dataset.uid)); s && openInv(s) })), "current" === selectedMapValue) { const t = getCurInside(); $("side-desc").innerHTML = t?.description ? `
📍 ${h(playerLocation)}
` + parseLinks(t.description) : parseLinks(D.maps?.outdoor?.description || "") } else $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""); bindLinks($("side-desc")), $("mob-desc").innerHTML = "", renderMapSelector(), renderMap() } function renderMap() { const t = D.maps?.outdoor; if (seed = 123456789, inner.querySelectorAll(".item").forEach((t => t.remove())), svg.innerHTML = "", nodes = [], lines = [], !t?.nodes?.length) return; t.nodes.forEach(((t, e) => { const s = dirMap[t.position] || [0, 0], o = Math.hypot(s[0], s[1]) || 1, a = 120 * (t.distant || 1); nodes.push({ id: "n" + e, name: t.name, type: t.type || "sub", pos: t.position, distant: t.distant || 1, x: s[0] / o * a, y: s[1] / o * a, data: t }) })); const e = {}; nodes.forEach((t => (e[t.pos] = e[t.pos] || []).push(t))); for (let t in e) { const s = e[t]; if (s.length <= 1) continue; const o = dirMap[t] || [0, 0], a = Math.hypot(o[0], o[1]) || 1, n = -o[1] / a, r = o[0] / a, i = (s.length - 1) / 2; s.sort(((t, e) => t.distant - e.distant)).forEach(((t, e) => { t.x += n * (e - i) * 50, t.y += r * (e - i) * 50 })) } for (let t = 0; t < 15; t++)for (let t = 0; t < nodes.length; t++)for (let e = t + 1; e < nodes.length; e++) { const s = nodes[t], o = nodes[e], a = s.x - o.x, n = s.y - o.y, r = Math.hypot(a, n); if (r < 80 && r > 0) { const t = (80 - r) / r * .5; s.x += a * t, s.y += n * t, o.x -= a * t, o.y -= n * t } } nodes.forEach((t => { const e = document.createElement("div"); e.className = `item node-${t.type}`, e.id = t.id, e.textContent = "home" === t.type ? "🏠 " + t.name : t.name, e.style.cssText = `left:${t.x + 2e3}px;top:${t.y + 2e3}px`, e.onclick = e => { e.stopPropagation(), curNode?.id === t.id ? hideInfo() : showInfo(t) }, inner.appendChild(e) })); for (let t in e) { const s = e[t].sort(((t, e) => t.distant - e.distant)); for (let t = 0; t < s.length - 1; t++)lines.push([s[t], s[t + 1]]) } const s = Object.values(e).map((t => t.sort(((t, e) => t.distant - e.distant))[0])).sort(((t, e) => Math.atan2(t.y, t.x) - Math.atan2(e.y, e.x))); s.forEach(((t, e) => lines.push([t, s[(e + 1) % s.length]]))); const o = (t, e) => lines.some((([s, o]) => s === t && o === e || s === e && o === t)); for (let t = 0, e = 0; e < Math.floor(nodes.length / 5) && t < 200; t++) { const t = nodes[Math.floor(rand() * nodes.length)], s = nodes[Math.floor(rand() * nodes.length)]; t === s || o(t, s) || (lines.push([t, s]), e++) } drawLines() } function drawLines() { svg.innerHTML = ""; const t = mapWrap.getBoundingClientRect(); lines.forEach((([e, s]) => { const o = $(e.id), a = $(s.id); if (!o || !a) return; const n = o.getBoundingClientRect(), r = a.getBoundingClientRect(), i = document.createElementNS("http://www.w3.org/2000/svg", "line"); i.setAttribute("x1", (n.left + n.width / 2 - t.left) / scale - offX / scale), i.setAttribute("y1", (n.top + n.height / 2 - t.top) / scale - offY / scale), i.setAttribute("x2", (r.left + r.width / 2 - t.left) / scale - offX / scale), i.setAttribute("y2", (r.top + r.height / 2 - t.top) / scale - offY / scale); const c = "main" === e.type && "main" === s.type || "home" === e.type || "home" === s.type; i.setAttribute("stroke", c ? "var(--c)" : "var(--c4)"), i.setAttribute("stroke-width", c ? "2" : "1"), c || i.setAttribute("stroke-dasharray", "4 3"), svg.appendChild(i) })) } $("btn-settings").onclick = () => { reqSet(), $("set-api-url").value = gSet.apiUrl || "", $("set-api-key").value = gSet.apiKey || "", $("set-model").value = gSet.model || "", $("set-model-list").style.display = "none", $("test-res").className = "set-test-res", $("set-stage").value = D.stage || 0, $("set-deviation").value = D.deviationScore || 0, $("set-sim-target").value = D.simulationTarget ?? 5, $("set-mode").value = gSet.mode || "story", $("set-history-count").value = commSet.historyCount || 50, $("set-use-stream").checked = !!commSet.stream, $("set-npc-position").value = commSet.npcPosition || 0, $("set-npc-order").value = commSet.npcOrder || 100, renderDataList(), syncSimDueUI(), openM("m-settings") }, $("btn-adv-prompts").onclick = () => advOpen(), $("adv-save-global").onclick = () => advSaveTo("global"), $("adv-save-char").onclick = () => advSaveTo("character"), $("adv-reset").onclick = () => advReset(), $("btn-fetch-models").onclick = () => { BtnState.load($("btn-fetch-models"), "加载"), post("FETCH_MODELS", { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim() }) }, $("btn-test-conn").onclick = () => { $("test-res").className = "set-test-res", BtnState.load($("btn-test-conn"), "测试"), post("TEST_CONNECTION", { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim(), model: $("set-model").value.trim() }) }, $("set-save").onclick = () => { gSet = { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim(), model: $("set-model").value.trim(), mode: $("set-mode").value || "story" }, D.stage = Math.max(0, Math.min(10, parseInt($("set-stage").value, 10) || 0)), D.deviationScore = Math.max(0, Math.min(100, parseInt($("set-deviation").value, 10) || 0)), D.simulationTarget = parseInt($("set-sim-target").value, 10), Number.isNaN(D.simulationTarget) && (D.simulationTarget = 5), commSet = { historyCount: Math.max(0, Math.min(200, parseInt($("set-history-count").value, 10) || 50)), stream: !!$("set-use-stream").checked, npcPosition: parseInt($("set-npc-position").value, 10) || 0, npcOrder: Math.max(0, Math.min(1e3, parseInt($("set-npc-order").value, 10) || 100)) }; const t = {}; dataKeys.forEach((([e, , , s]) => { dataCk[e] && (t[e] = s()) })), syncSimDueUI(), post("SAVE_SETTINGS", { globalSettings: gSet, commSettings: commSet, stage: D.stage, deviationScore: D.deviationScore, simulationTarget: D.simulationTarget, playerLocation, dataChecked: dataCk, outlineData: t, allData: { meta: D.meta, timeline: D.timeline, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, strangers: D.contacts.strangers, contacts: contactsForSave() } }), closeM("m-settings") }, $("btn-close").onclick = () => post("CLOSE_PANEL"), window.addEventListener("message", (t => { if (t.origin !== PARENT_ORIGIN || t.source !== parent) return; if ("LittleWhiteBox" !== t.data?.source) return; const e = t.data, s = e.type; if ("LOAD_SETTINGS" === s) { if (e.globalSettings && (gSet = e.globalSettings), void 0 !== e.stage && (D.stage = e.stage), void 0 !== e.deviationScore && (D.deviationScore = e.deviationScore), void 0 !== e.simulationTarget && (D.simulationTarget = e.simulationTarget), e.playerLocation && (playerLocation = e.playerLocation), e.commSettings && (commSet = { historyCount: e.commSettings.historyCount ?? 50, npcPosition: e.commSettings.npcPosition ?? 0, npcOrder: e.commSettings.npcOrder ?? 100, stream: !!e.commSettings.stream }), e.dataChecked && (dataCk = e.dataChecked), e.promptConfig && (promptDefaults = e.promptConfig.defaults || promptDefaults, e.promptConfig.stores ? (promptStores.global = e.promptConfig.stores.global || { jsonTemplates: {}, promptSources: {} }, promptStores.character = e.promptConfig.stores.character || { jsonTemplates: {}, promptSources: {} }) : (promptStores.global = e.promptConfig.current || { jsonTemplates: {}, promptSources: {} }, promptStores.character = promptStores.character || { jsonTemplates: {}, promptSources: {} })), e.outlineData) { const t = e.outlineData; t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.outdoor && (D.maps.outdoor = t.outdoor), t.indoor && (D.maps.indoor = t.indoor), t.sceneSetup && (D.sceneSetup = t.sceneSetup), t.strangers && (D.contacts.strangers = t.strangers), t.contacts && (D.contacts.contacts = t.contacts) } { const t = e.characterContactSmsHistory || {}; charSmsHistory = { messages: Array.isArray(t.messages) ? t.messages : [], summarizedCount: t.summarizedCount || 0, summaries: t.summaries || {} } } let t = D.contacts.contacts.find((t => "__CHARACTER_CARD__" === t.worldbookUid)); t || (t = D.contacts.contacts.find((t => !t.worldbookUid && "炒饭智能" === t.name)), t ? (t.worldbookUid = "__CHARACTER_CARD__", t.info = "角色卡联络人", t.location = "在线", t.online = !0) : (D.contacts.contacts.unshift({ name: e.characterCardName || "{{characterName}}", avatar: "", color: "#555", location: "在线", info: "角色卡联络人", online: !0, worldbookUid: "__CHARACTER_CARD__", messages: [], summarizedCount: 0 }), t = D.contacts.contacts[0])), t && e.characterCardName && (t.name = e.characterCardName, t.avatar = (e.characterCardName || "")[0] || t.avatar || ""), render(), syncSimDueUI(), $("m-settings").classList.contains("act") && ($("set-api-url").value = gSet.apiUrl || "", $("set-api-key").value = gSet.apiKey || "", $("set-model").value = gSet.model || "", $("set-stage").value = D.stage, $("set-deviation").value = D.deviationScore, $("set-sim-target").value = D.simulationTarget ?? 5, $("set-mode").value = gSet.mode || "story", $("set-history-count").value = commSet.historyCount, $("set-use-stream").checked = !!commSet.stream, $("set-npc-position").value = commSet.npcPosition, $("set-npc-order").value = commSet.npcOrder, renderDataList()) } else if ("PROMPT_CONFIG_UPDATED" === s) { if (e.promptConfig && (promptDefaults = e.promptConfig.defaults || promptDefaults, e.promptConfig.stores ? (promptStores.global = e.promptConfig.stores.global || { jsonTemplates: {}, promptSources: {} }, promptStores.character = e.promptConfig.stores.character || { jsonTemplates: {}, promptSources: {} }) : (promptStores.global = e.promptConfig.current || { jsonTemplates: {}, promptSources: {} }, promptStores.character = promptStores.character || { jsonTemplates: {}, promptSources: {} }), $("m-adv-prompts").classList.contains("act"))) { const t = $("adv-key")?.value; t && advApplyToUI(t, advGetScope()) } } else if ("FETCH_MODELS_RESULT" === s) { BtnState.reset($("btn-fetch-models"), "获取"); const t = $("set-model-list"); if (e.error) return t.style.display = "none", void showTestRes(!1, "获取模型失败: " + e.error); if (!e.models?.length) return t.style.display = "none", void showTestRes(!1, "未找到可用模型"); t.innerHTML = '' + e.models.map((t => ``)).join(""), t.style.display = "block", t.onchange = () => { t.value && ($("set-model").value = t.value) }, showTestRes(!0, `找到 ${e.models.length} 个模型`) } else if ("TEST_CONN_RESULT" === s) BtnState.reset($("btn-test-conn"), "测试连接"), showTestRes(e.success, e.message); else if ("CHECK_WORLDBOOK_UID_RESULT" === s) { if (BtnState.reset($("btn-check-uid"), ' 检查'), !Req.match(e.requestId)) return; if (e.error) return void showUidErr(e.error); if (!e.primaryKeys?.length) return void showUidErr("该条目没有主要关键字"); addCtState.keys = e.primaryKeys; const t = $("add-name"); t.innerHTML = '' + e.primaryKeys.map((t => ``)).join(""), t.onchange = () => { addCtState.name = t.value, $("add-ct-ok").disabled = !t.value }, $("name-select-group").style.display = "block", 1 === e.primaryKeys.length && (addCtState.name = e.primaryKeys[0], t.value = addCtState.name, $("add-ct-ok").disabled = !1) } else if ("SMS_RESULT" === s) { const t = Req.get("sms"); if (!t || t.id !== e.requestId) return; if (Req.clear("sms"), smsGen = !1, $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !1, !chatTgt) return; chatTgt.messages = chatTgt.messages.filter((t => !t.typing)), e.error ? chatTgt.messages.push({ type: "received", text: `[错误] ${e.error}` }) : e.reply && chatTgt.messages.push({ type: "received", text: stripXml(e.reply) }), renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt) } else if ("SMS_STREAM" === s) { const t = Req.get("sms"); if (!t || t.id !== e.requestId || !chatTgt) return; const s = chatTgt.messages.find((t => t.typing)); s && e.text && (s.text = e.text, renderMsgs()) } else if ("LOAD_SMS_HISTORY_RESULT" === s) { if (!chatTgt || chatTgt.worldbookUid !== e.worldbookUid) return; e.messages?.length && (chatTgt.messages = e.messages, chatTgt.summarizedCount = e.summarizedCount || 0, saveCt()), renderMsgs() } else if ("COMPRESS_SMS_RESULT" === s) { const t = Req.get("compress"); if (!t || t.id !== e.requestId) return; if (Req.clear("compress"), smsGen = !1, $("chat-compress").disabled = $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !1, !chatTgt) return; if (e.error) return void alert(`压缩失败: ${e.error}`); void 0 !== e.newSummarizedCount && (chatTgt.summarizedCount = e.newSummarizedCount, renderMsgs(), saveCt()) } else if ("CHECK_STRANGER_WORLDBOOK_RESULT" === s) { const t = Req.get("stgwb"); if (!t || t.id !== e.requestId) return; const { name: s, info: o, btn: a, npcType: nt } = t; if (Req.clear("stgwb"), e.found && e.worldbookUid) { BtnState.reset(a, nt === "importantNpc" ? ' 重要' : ' 背景板'); const t = D.contacts.strangers.findIndex((t => t.name === s)); if (t > -1) { const s = D.contacts.strangers.splice(t, 1)[0]; D.contacts.contacts.push({ name: s.name, avatar: s.avatar || s.name[0], color: s.color || "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: s.location || "未知", info: s.info || "", worldbookUid: e.worldbookUid, messages: [] }), saveCt(), render() } } else { BtnState.load(a, "生成中"); const t = Req.create("npcgen"); Req.set("npcgen", { name: s, info: o, btn: a, npcType: nt }), post("GENERATE_NPC", { requestId: t, strangerName: s, strangerInfo: o, npcType: nt }) } } else if ("GENERATE_NPC_RESULT" === s) { const t = Req.get("npcgen"); if (!t || t.id !== e.requestId) return; const { name: s, btn: o, npcType: nt } = t; const _resetLbl = nt === "importantNpc" ? ' 重要' : ' 背景板'; if (Req.clear("npcgen"), BtnState.reset(o, _resetLbl), e.error) return void showResultModal("生成角色失败", "生成 NPC 失败", !0, e.error); if (e.success && e.worldbookUid) { const t = D.contacts.strangers.findIndex((t => t.name === s)); if (t > -1) { const o = D.contacts.strangers.splice(t, 1)[0], a = e.npcData || {}; D.contacts.contacts.push({ name: a.name || o.name, avatar: (a.name || o.name)[0], color: o.color || "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: o.location || "未知", info: a.intro || o.info || "", worldbookUid: e.worldbookUid, messages: [] }), saveCt(), render(), showResultModal("生成成功", `NPC ${s} 已生成并添加到联络人`, !1, e.npcData) } } } else if ("EXTRACT_STRANGERS_RESULT" === s) { const t = Req.get("extract"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("extract"), BtnState.reset(s, ''), e.error) return void showResultModal("提取失败", "提取陌路人失败", !0, e.error); if (e.success && Array.isArray(e.strangers)) { if (!e.strangers.length) return void showResultModal("提取结果", "没有发现新的陌路人"); const t = [...D.contacts.contacts.map((t => t.name)), ...D.contacts.strangers.map((t => t.name))], s = e.strangers.filter((e => !t.includes(e.name))); if (!s.length) return void showResultModal("提取结果", "提取到的角色都已存在"); D.contacts.strangers = D.contacts.strangers.concat(s), saveCt(), render(), showResultModal("提取成功", `成功提取 ${s.length} 个新陌路人`, !1, s) } } else if ("GENERATE_WORLD_STATUS" === s) { if (!Req.match(e.requestId)) return; const t = $("world-gen-status"); t.style.display = "block", t.style.color = "var(--ok)", t.textContent = e.message } else if ("GENERATE_WORLD_RESULT" === s) { if (!Req.match(e.requestId)) return; Req.clear("wgen"); const t = $("world-gen-ok"), s = $("world-gen-status"); if (BtnState.reset(t, ' 开始生成'), e.error) { if (s.style.display = "none", showResultModal("生成失败", "世界生成失败", !0, e.error), String(e.error || "").includes("Step 2")) { const e = $("res-action"); e.style.display = "inline-block", e.textContent = "重试 Step2", e.onclick = () => { closeM("m-result"); const e = Req.create("wgen"); BtnState.load(t, "重试中"), s.style.display = "block", s.style.color = "var(--ok)", s.textContent = "准备重试 Step 2/2...", post("RETRY_WORLD_GEN_STEP2", { requestId: e }) } } return } if (e.success && e.worldData) { s.style.color = "var(--ok)", s.textContent = "生成成功!正在应用数据..."; const t = e.worldData; if (t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.maps?.outdoor && (D.maps.outdoor = t.maps.outdoor), D.stage = 0, D.deviationScore = 0, t.playerLocation) playerLocation = t.playerLocation; else { const t = D.maps?.outdoor?.nodes?.find((t => "home" === t.type)); playerLocation = t?.name || D.maps?.outdoor?.nodes?.[0]?.name || "未知" } t.maps?.inside && playerLocation && (D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[playerLocation] = t.maps.inside), selectedMapValue = "current", saveAll(), render(), setTimeout((() => { closeM("m-world-gen"), s.style.display = "none", $("world-gen-req").value = "", showResultModal("生成成功", "世界数据生成完成!Stage 和 Deviation 已重置为 0", !1, e.worldData) }), 500) } } else if ("SIMULATE_WORLD_RESULT" === s) { if (!e.isAuto && !Req.match(e.requestId)) return; if (!e.isAuto) { Req.clear("wsim"); const t = $("world-sim-ok"), s = $("world-sim-status"); if (BtnState.reset(t, ' 开始推演'), e.error) return s.style.display = "none", void showResultModal("推演失败", "世界推演失败", !0, e.error) } if (e.success && e.simData) { if (!e.isAuto) { const t = $("world-sim-status"); t.style.color = "var(--ok)", t.textContent = "推演成功!正在应用数据..." } const t = e.simData; t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.maps?.outdoor && (D.maps.outdoor = t.maps.outdoor), D.stage = (D.stage || 0) + 1, saveAll(), render(), e.isAuto || setTimeout((() => { closeM("m-world-sim"), $("world-sim-status").style.display = "none", showResultModal("推演成功", `世界推演完成!Stage 已推进到 ${D.stage}`, !1, e.simData) }), 500) } } else if ("REFRESH_WORLD_NEWS_RESULT" === s) { const t = Req.get("newsrf"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("newsrf"), s && (s.disabled = !1, s.innerHTML = s._o ?? ''), e.error) return void showResultModal("刷新失败", "世界新闻刷新失败", !0, e.error); e.success && Array.isArray(e.news) && (D.world = D.world || {}, D.world.news = e.news, saveAll(), render(), showResultModal("刷新成功", `已更新世界新闻(${e.news.length} 条)`, !1, e.news)) } else if ("SCENE_SWITCH_RESULT" === s) { const t = Req.get("scene"); if (!t || t.id !== e.requestId) return; const { node: s, prev: o } = t; if (Req.clear("scene"), BtnState.reset($("goto-ok"), "确认前往"), closeM("m-goto"), e.error) return void showResultModal("切换失败", "场景切换失败", !0, e.error); if (e.success && e.sceneData) { const t = e.sceneData; if ("number" == typeof t.newScore && (D.deviationScore = t.newScore), t.localMap && (D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s.name] = t.localMap), t.strangers?.length) { const e = new Set((D.contacts.strangers || []).map((t => t.name))), s = t.strangers.filter((t => !e.has(t.name))); D.contacts.strangers = [...D.contacts.strangers || [], ...s] } finishTravel(s.name, o), 0 !== t.scoreDelta && showResultModal("切换成功", `场景切换完成!\n偏差值变化: ${t.scoreDelta > 0 ? "+" : ""}${t.scoreDelta} (当前: ${t.newScore})`, !1, e.sceneData) } } else if ("REFRESH_LOCAL_MAP_RESULT" === s) { const t = Req.get("localmaprf"); if (!t || t.id !== e.requestId) return; const { btn: s, loc: o } = t; if (Req.clear("localmaprf"), BtnState.reset(s, '刷新'), e.error) return void showResultModal("刷新失败", "刷新局部地图失败", !0, e.error); if (e.success && e.localMapData) { const t = e.localMapData, s = t.name || o || playerLocation || "当前位置"; D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s] = t, playerLocation = s, selectedMapValue = "current", saveAll(), render(), t.description && ($("side-desc").innerHTML = `
📍 ${h(s)}
` + parseLinks(t.description), bindLinks($("side-desc"))), showResultModal("刷新成功", `局部地图已刷新!当前位置: ${s}`, !1, e.localMapData) } } else if ("GENERATE_LOCAL_SCENE_RESULT" === s) { const t = Req.get("localscene"); if (!t || t.id !== e.requestId) return; const { btn: s, loc: o } = t; if (Req.clear("localscene"), BtnState.reset(s, '局部剧情'), e.error) return void showResultModal("生成失败", "局部剧情生成失败", !0, e.error); if (e.success && e.sceneSetup) { D.sceneSetup = { ...D.sceneSetup || {}, ...e.sceneSetup || {} }, saveAll(), render(); const t = String(e.introduce || "").replace(/^\s*(?:\/?\s*(?:sendas|as)\s+name\s*=\s*(?:"[^"]*"|'[^']*'|\S+)\s+)/i, "").replace(/\s+/g, " ").trim(); t && post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${t}` }), showResultModal("生成成功", `局部剧情已生成:${o || playerLocation}`, !1, e.sceneSetup) } } else if ("SEND_INVITE_RESULT" === s) { const t = Req.get("invite"); if (!t || t.id !== e.requestId) return; const { contact: s, loc: o, btn: a } = t; if (Req.clear("invite"), BtnState.reset(a, "发送邀请"), e.error) return void showResultModal("邀请失败", "邀请发送失败", !0, e.error); if (e.success && e.inviteData) { const t = e.inviteData, a = canonicalLoc(t.targetLocation || o || ""), n = D.contacts.contacts.find((t => t && s && t.worldbookUid && s.worldbookUid && t.worldbookUid === s.worldbookUid)) || D.contacts.contacts.find((t => t && s && t.name === s.name)) || s; if (n.messages = n.messages || [], n.messages.push({ type: "sent", text: `我邀请你前往「${a}」` }), n.messages.push({ type: "received", text: t.reply }), t.accepted) { const e = canonicalLoc(playerLocation); n.location = a, e && a && e === a ? (delete n.waitingAt, post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${n.name}过来了。` })) : n.waitingAt = a, showResultModal("邀请成功", `${n.name} 接受了邀请!\n回复: ${t.reply}`, !1, t) } else showResultModal("邀请被拒", `${n.name} 拒绝了邀请。\n回复: ${t.reply}`, !1, t); saveAll(), n.worldbookUid && saveChat(n), closeM("m-invite"), render(), openChat(n) } } else if ("GENERATE_LOCAL_MAP_RESULT" === s) { const t = Req.get("localmap"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("localmap"), BtnState.reset(s, '局部地图'), e.error) return void showResultModal("生成失败", "局部地图生成失败", !0, e.error); if (e.success && e.localMapData) { const t = e.localMapData, s = t.name || "当前位置"; D.sceneSetup = null, D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s] = t, playerLocation = s, selectedMapValue = "current", saveAll(), render(), t.description && ($("side-desc").innerHTML = `
📍 ${h(s)}
` + parseLinks(t.description), bindLinks($("side-desc"))), showResultModal("生成成功", `局部地图生成完成!当前位置: ${s}`, !1, t) } } })); const updateTf = () => { inner.style.transform = `translate(${offX}px,${offY}px) scale(${scale})`, $("zoom-ind").textContent = Math.round(100 * scale) + "%", requestAnimationFrame(drawLines) }, initPos = () => { offX = mapWrap.clientWidth / 2 - 2e3, offY = mapWrap.clientHeight / 2 - 2e3, scale = 1, updateTf() }; function panTo(t, e = 350) { if (!t || anim) return; if (!$(t.id)) return; const s = -(t.x + 2e3) * scale + mapWrap.clientWidth / 2, o = -(t.y + 2e3) * scale + .25 * mapWrap.clientHeight, a = offX, n = offY, r = performance.now(); anim = !0, function t(i) { const c = Math.min((i - r) / e, 1), l = 1 - Math.pow(1 - c, 3); offX = a + (s - a) * l, offY = n + (o - n) * l, updateTf(), c < 1 ? requestAnimationFrame(t) : anim = !1 }(r) } function showInfo(t) { if (!t?.data) return; curNode = t, inner.querySelectorAll(".item").forEach((t => t.classList.remove("hl"))), $(t.id)?.classList.add("hl"); const e = t.name === playerLocation; $("btn-goto").classList.toggle("show", !e), e || ($("goto-t").textContent = `前往 ${t.name}`); const s = D.maps?.indoor?.[t.name]; e && s?.description ? ($("side-desc").innerHTML = `
📍 ${h(t.name)}
` + parseLinks(s.description), bindLinks($("side-desc"))) : ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc"))), isMob() ? ($("mob-info-t").textContent = t.name, $("mob-info-c").textContent = e ? s?.description || t.data.info || "暂无信息..." : t.data.info || "暂无信息...", popup.classList.contains("act") || openPop(1)) : ($("info-t").textContent = t.name, $("info-c").textContent = t.data.info || "暂无信息...", $("tip").classList.add("show")) } const hideInfo = () => { curNode = null, inner.querySelectorAll(".item").forEach((t => t.classList.remove("hl"))), $("btn-goto").classList.remove("show"), $("tip").classList.remove("show") }; function renderMapSelector() { const t = $("map-lbl-select"); t.innerHTML = ''; const e = D.maps?.outdoor?.nodes?.findIndex((t => t.name === playerLocation)), s = D.maps?.indoor && D.maps.indoor[playerLocation]; if ((e >= 0 || s) && (t.innerHTML += ``), t.innerHTML += "", D.maps?.outdoor?.nodes?.length && D.maps.outdoor.nodes.forEach(((e, s) => { e.name !== playerLocation && (t.innerHTML += ``) })), D.maps?.indoor) { const e = Object.keys(D.maps.indoor).filter((t => t !== playerLocation && !D.maps?.outdoor?.nodes?.some((e => e.name === t)))); e.length && (t.innerHTML += "", e.forEach((e => t.innerHTML += ``))) } t.value = selectedMapValue, updateMapLabel() } function updateMapLabel() { const t = $("map-lbl-select").value; if ("overview" === t) $("map-lbl-t").textContent = "大地图"; else if ("current" === t) $("map-lbl-t").textContent = playerLocation + "(你)"; else if (t.startsWith("node:")) { const e = parseInt(t.split(":")[1]); $("map-lbl-t").textContent = D.maps?.outdoor?.nodes?.[e]?.name || "未知" } else t.startsWith("indoor:") && ($("map-lbl-t").textContent = t.replace("indoor:", "")) } function switchMapView(t) { if (selectedMapValue = t, hideInfo(), "overview" === t) $("btn-goto").classList.remove("show"), $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")), initPos(); else if ("current" === t) { $("btn-goto").classList.remove("show"); const t = getCurInside(); t?.description ? ($("side-desc").innerHTML = `
📍 ${h(playerLocation)}
` + parseLinks(t.description), bindLinks($("side-desc"))) : ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc"))); const e = nodes.find((t => t.name === playerLocation)); e && (panTo(e), showInfo(e)) } else if (t.startsWith("node:")) { const e = parseInt(t.split(":")[1]), s = D.maps?.outdoor?.nodes?.[e]; $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")); const o = nodes.find((t => t.name === s?.name)); o && (panTo(o), showInfo(o)) } else if (t.startsWith("indoor:")) { const e = t.replace("indoor:", ""), s = D.maps?.indoor?.[e]; e !== playerLocation ? ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")), curNode = { name: e, type: "sub", data: { info: stripXml(s?.description || "") }, isIndoor: !0 }, $("btn-goto").classList.add("show"), $("goto-t").textContent = `前往 ${e}`) : ($("btn-goto").classList.remove("show"), s?.description && ($("side-desc").innerHTML = `
🏠 ${h(e)}
` + parseLinks(s.description), bindLinks($("side-desc")))) } updateMapLabel() } $("info-bk").onclick = hideInfo, $("mob-info-bk").onclick = () => popup.classList.remove("act"), $("map-lbl-select").onchange = t => switchMapView(t.target.value); let sx, sy, lastDist = 0, lastCX = 0, lastCY = 0; mapWrap.onmousedown = t => { anim || t.target.closest(".map-act,.map-lbl") || (t.target.classList.contains("item") || hideInfo(), drag = !0, sx = t.clientX, sy = t.clientY, mapWrap.style.cursor = "grabbing") }, mapWrap.onmousemove = t => { drag && (offX += t.clientX - sx, offY += t.clientY - sy, sx = t.clientX, sy = t.clientY, updateTf()) }, mapWrap.onmouseup = mapWrap.onmouseleave = () => { drag = !1, mapWrap.style.cursor = "grab" }, mapWrap.onwheel = t => { if (anim) return; t.preventDefault(); const e = Math.max(.3, Math.min(3, scale + (t.deltaY > 0 ? -.1 : .1))), s = mapWrap.getBoundingClientRect(), o = t.clientX - s.left, a = t.clientY - s.top, n = e / scale; offX = o - (o - offX) * n, offY = a - (a - offY) * n, scale = e, updateTf() }, mapWrap.ontouchstart = t => { if (anim || t.target.closest(".map-act,.map-lbl")) return; const e = t.target.closest(".item"); e ? e._ts = { x: t.touches[0].clientX, y: t.touches[0].clientY, t: Date.now() } : (hideInfo(), 1 === t.touches.length ? (drag = !0, sx = t.touches[0].clientX, sy = t.touches[0].clientY) : 2 === t.touches.length && (drag = !1, lastDist = Math.hypot(t.touches[0].clientX - t.touches[1].clientX, t.touches[0].clientY - t.touches[1].clientY), lastCX = (t.touches[0].clientX + t.touches[1].clientX) / 2, lastCY = (t.touches[0].clientY + t.touches[1].clientY) / 2)) }, mapWrap.ontouchmove = t => { const e = t.target.closest(".item"); if (e && e._ts) Math.hypot(t.touches[0].clientX - e._ts.x, t.touches[0].clientY - e._ts.y) > 10 && (delete e._ts, drag = !0, sx = t.touches[0].clientX, sy = t.touches[0].clientY); else if (1 === t.touches.length && drag) t.preventDefault(), offX += t.touches[0].clientX - sx, offY += t.touches[0].clientY - sy, sx = t.touches[0].clientX, sy = t.touches[0].clientY, updateTf(); else if (2 === t.touches.length) { t.preventDefault(); const e = Math.hypot(t.touches[0].clientX - t.touches[1].clientX, t.touches[0].clientY - t.touches[1].clientY), s = (t.touches[0].clientX + t.touches[1].clientX) / 2, o = (t.touches[0].clientY + t.touches[1].clientY) / 2, a = Math.max(.3, Math.min(3, scale * (e / lastDist))), n = mapWrap.getBoundingClientRect(), r = s - n.left, i = o - n.top, c = a / scale; offX = r - (r - offX) * c, offY = i - (i - offY) * c, offX += s - lastCX, offY += o - lastCY, scale = a, lastDist = e, lastCX = s, lastCY = o, updateTf() } }, mapWrap.ontouchend = t => { const e = t.target.closest(".item"); if (e && e._ts) { const s = Date.now() - e._ts.t; if (delete e._ts, s < 300) { const s = nodes.find((t => t.id === e.id)); s && (t.preventDefault(), curNode?.id === s.id ? hideInfo() : showInfo(s)) } } drag = !1 }, $$(".nav-i").forEach((t => t.onclick = () => { $$(".nav-i").forEach((t => t.classList.remove("act"))), $$(".page").forEach((t => t.classList.remove("act"))), t.classList.add("act"), $(`page-${t.dataset.p}`).classList.add("act"); const e = "map" === t.dataset.p; sidePop.classList.toggle("show", e), isMob() && (e ? openPop(1) : popup.classList.remove("act")), e && setTimeout((() => { initPos(), drawLines() }), 50) })), $$(".comm-tab").forEach((t => t.onclick = () => { $$(".comm-tab").forEach((t => t.classList.remove("act"))), $$(".comm-sec").forEach((t => t.classList.remove("act"))), t.classList.add("act"), $(`sec-${t.dataset.t}`).classList.add("act") })), $("btn-goto").onclick = t => { t.stopPropagation(), curNode && ($("goto-d").textContent = `目的地:${curNode.name}`, $("goto-task").value = "", openM("m-goto")) }, addEventListener("resize", (() => requestAnimationFrame(drawLines))), window.clickTab = (t, e) => { const s = t.closest(".settings-modal"); if (!s) return; s.querySelectorAll(".set-nav-item").forEach((t => t.classList.remove("act"))), t.classList.add("act"), s.querySelectorAll(".set-tab-page").forEach((t => t.classList.remove("act"))); const o = document.getElementById(e); o && (o.classList.add("act"), o.style.animation = "none", o.offsetHeight, o.style.animation = null) }, document.addEventListener("DOMContentLoaded", (() => { render(), initPos(), sidePop.classList.add("show"), sidePop.classList.add("act"), isMob() && openPop(1), post("FRAME_READY"), setTimeout((() => { "current" === selectedMapValue && switchMapView("current") }), 100) })); + const inds = popup.querySelectorAll(".pop-h-ind span"), setPopH = t => { const e = snaps(); popH = Math.max(e[0], Math.min(.85 * innerHeight, t)), popup.style.height = popH + "px", popLv = e.map(((t, e) => [e, Math.abs(popH - t)])).sort(((t, e) => t[1] - e[1]))[0][0], inds.forEach(((t, e) => t.classList.toggle("act", e === popLv))) }, snapTo = t => { popLv = Math.max(0, Math.min(2, t)), setPopH(snaps()[popLv]) }, openPop = (t = 1) => { popup.classList.add("act"), snapTo(t) }; $("side-pop-handle").onclick = () => sidePop.classList.toggle("act"), $("pop-hd").onmousedown = t => { popDrag = !0, popSY = t.clientY, popSH = popH || snaps()[1], popup.classList.add("drag"), t.preventDefault() }, $("pop-hd").ontouchstart = t => { t.preventDefault(), popDrag = !0, popSY = t.touches[0].clientY, popSH = popH || snaps()[1], popup.classList.add("drag") }, document.onmousemove = t => { popDrag && setPopH(popSH + popSY - t.clientY) }, document.ontouchmove = t => { popDrag && t.touches.length && (t.preventDefault(), setPopH(popSH + popSY - t.touches[0].clientY)) }; const endDrag = () => { popDrag && (popDrag = !1, popup.classList.remove("drag"), snapTo(popLv)) }; document.onmouseup = endDrag, document.ontouchend = endDrag, document.ontouchcancel = endDrag; const bindLinks = t => t.querySelectorAll(".loc-lk").forEach((t => t.onclick = e => { e.stopPropagation(); const s = t.dataset.loc, o = nodes.find((t => t.name === s)); if (o) return panTo(o), void showInfo(o); const a = getCurInside(), n = a?.nodes?.find((t => t.name === s)); n && ($("info-t").textContent = n.name, $("info-c").textContent = n.info || "暂无信息...", $("tip").classList.add("show"), $("btn-goto").classList.remove("show"), isMob() && ($("mob-info-t").textContent = n.name, $("mob-info-c").textContent = n.info || "暂无信息...", popup.classList.contains("act") || openPop(1))) })), bindFold = t => t.querySelector(".fold-h").onclick = () => t.classList.toggle("exp"); $$(".modal-bd,.modal-x,.m-cancel").forEach((t => t.onclick = () => t.closest(".modal").classList.remove("act"))); const openChat = t => { chatTgt = t, $("chat-av").textContent = t.avatar, $("chat-av").style.background = t.color, $("chat-nm").textContent = t.name, $("chat-st").textContent = t.online ? "● 在线" : t.location, !t.worldbookUid || t.messages && t.messages.length ? renderMsgs() : post("LOAD_SMS_HISTORY", { worldbookUid: t.worldbookUid }), chat.classList.add("act"), $("chat-in").focus() }, closeChat = () => { chat.classList.remove("act"), chatTgt = null, smsGen = !1 }, renderMsgs = () => { if (!chatTgt) return; const t = chatTgt.messages || [], e = chatTgt.summarizedCount || 0; let s = ""; t.length ? t.forEach(((t, o) => { e > 0 && o === e && (s += '
—— 以上为已总结消息 ——
'), s += `
${escHtml(stripXml(t.text))}
` })) : s = '
暂无消息,开始聊天吧
', $("chat-msgs").innerHTML = s, $("chat-msgs").scrollTop = $("chat-msgs").scrollHeight, $("chat-compress").disabled = t.length - e < 2 || smsGen, $("chat-back").disabled = !t.length || smsGen }, sendMsg = () => { const t = $("chat-in").value.trim(); if (!t || !chatTgt || smsGen) return; chatTgt.messages = chatTgt.messages || [], chatTgt.messages.push({ type: "sent", text: t }), $("chat-in").value = "", renderMsgs(), smsGen = !0, $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !0, chatTgt.messages.push({ type: "received", text: "正在输入...", typing: !0 }), renderMsgs(); const e = Req.create("sms"); Req.set("sms", { tgt: chatTgt }), post("SEND_SMS", { requestId: e, contactName: chatTgt.name, worldbookUid: chatTgt.worldbookUid, userMessage: t, chatHistory: chatTgt.messages.filter((t => !t.typing)).slice(-20), summarizedCount: chatTgt.summarizedCount || 0 }) }, isCharCardContact = t => "__CHARACTER_CARD__" === t?.worldbookUid, contactsForSave = () => (D.contacts.contacts || []).filter((t => !isCharCardContact(t))), saveCt = () => post("SAVE_CONTACTS", { contacts: contactsForSave(), strangers: D.contacts.strangers }), saveChat = t => post("SAVE_SMS_HISTORY", { worldbookUid: t.worldbookUid, contactName: t.name, messages: t.messages.filter((t => !t.typing)), summarizedCount: t.summarizedCount || 0 }); $("chat-x").onclick = closeChat, $("chat-back").onclick = () => { if (!chatTgt || smsGen) return; const t = chatTgt.messages || []; if (t.length) { for (t.pop(); t[t.length - 1]?.typing;)t.pop(); chatTgt.summarizedCount = Math.min(chatTgt.summarizedCount || 0, t.length), renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt) } }, $("chat-clr").onclick = () => { chatTgt && confirm(`确定要清空与 ${chatTgt.name} 的所有聊天记录吗?`) && (chatTgt.messages = [], chatTgt.summarizedCount = 0, renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt)) }, $("chat-compress").onclick = () => { if (!chatTgt || smsGen) return; const t = chatTgt.messages || [], e = chatTgt.summarizedCount || 0, s = t.slice(e); if (s.length < 2) return void alert("至少需要2条未总结的消息才能压缩"); if (!confirm(`确定要压缩总结 ${s.length} 条消息吗?`)) return; smsGen = !0, $("chat-compress").disabled = $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !0; const o = Req.create("compress"); Req.set("compress", { tgt: chatTgt }), post("COMPRESS_SMS", { requestId: o, contactName: chatTgt.name, worldbookUid: chatTgt.worldbookUid, messages: t.filter((t => !t.typing)), summarizedCount: e }) }, $("chat-send").onclick = sendMsg; const chatIn = $("chat-in");["keydown", "keypress", "keyup"].forEach((t => chatIn.addEventListener(t, (t => t.stopPropagation())))), chatIn.addEventListener("keydown", (t => { "Enter" !== t.key || t.shiftKey || (t.preventDefault(), sendMsg()) })); const openInv = t => { invTgt = t, selLoc = null, $("inv-t").textContent = `邀请:${t.name}`, $("loc-list").innerHTML = D.maps.outdoor.nodes.map((t => `
${h(t.name)}
${h(t.info || "")}
`)).join(""), $$("#loc-list .loc-i").forEach((t => t.onclick = () => { $$("#loc-list .loc-i").forEach((t => t.classList.remove("sel"))), t.classList.add("sel"), selLoc = t.dataset.n })), openM("m-invite") }; $("inv-ok").onclick = () => { if (!selLoc || !invTgt) return; const t = $("inv-ok"); BtnState.load(t, "询问中..."); const e = Req.create("invite"), s = canonicalLoc(selLoc); Req.set("invite", { contact: invTgt, loc: s, btn: t }), post("SEND_INVITE", { requestId: e, contactName: invTgt.name, contactUid: invTgt.worldbookUid, targetLocation: s, smsHistory: (invTgt.messages || []).map((t => "sent" === t.type ? `{{user}}: ${t.text}` : `${invTgt.name}: ${t.text}`)).join("\n") }) }; let addCtState = { uid: "", name: "", keys: [] }; const resetAddCt = () => { addCtState = { uid: "", name: "", keys: [] }, $("add-uid").value = "", $("add-name").value = "", $("add-name").innerHTML = '', $("name-select-group").style.display = "none", $("uid-check-err").classList.remove("vis"), $("add-ct-ok").disabled = !0, BtnState.reset($("btn-check-uid"), ' 检查') }, showUidErr = t => { $("uid-check-err").textContent = t, $("uid-check-err").classList.add("vis") }; $("btn-check-uid").onclick = () => { const t = $("add-uid").value.trim(); t ? ($("uid-check-err").classList.remove("vis"), BtnState.load($("btn-check-uid"), "检查中"), $("name-select-group").style.display = "none", $("add-ct-ok").disabled = !0, addCtState.uid = t, post("CHECK_WORLDBOOK_UID", { uid: t, requestId: Req.create("uidck") })) : showUidErr("请输入UID") }, $("btn-add-ct").onclick = () => { resetAddCt(), openM("m-add-ct") }, $("add-ct-ok").onclick = () => { const t = addCtState.uid || $("add-uid").value.trim(), e = addCtState.name || $("add-name").value.trim(); t && e && (D.contacts.contacts.some((e => e.worldbookUid === t)) ? showUidErr("该联络人已存在") : (D.contacts.contacts.push({ name: e, avatar: e[0], color: "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: "未知", worldbookUid: t, messages: [] }), saveCt(), closeM("m-add-ct"), render())) }; const genAddCt = (t, e, s, npcType = "npc") => { BtnState.load(s, "检查中"); const o = Req.create("stgwb"); Req.set("stgwb", { name: t, info: e, btn: s, npcType }), post("CHECK_STRANGER_WORLDBOOK", { requestId: o, strangerName: t }) }; function canonicalLoc(t) { return String(t || "").trim().replace(/^\u90ae\u8f6e/, "") } $("btn-refresh-strangers").onclick = () => { const t = $("btn-refresh-strangers"); BtnState.load(t, ""); const e = Req.create("extract"); Req.set("extract", { btn: t }), post("EXTRACT_STRANGERS", { requestId: e, existingContacts: D.contacts.contacts, existingStrangers: D.contacts.strangers }) }, $("world-gen-ok").onclick = () => { const t = $("world-gen-ok"), e = $("world-gen-status"); BtnState.load(t, "生成中"), e.style.display = "block", e.textContent = "正在生成世界数据,请稍候...", post("GENERATE_WORLD", { requestId: Req.create("wgen"), playerRequests: $("world-gen-req").value.trim() }) }, $("world-sim-ok").onclick = () => { if (!D.meta && !D.timeline && !D.maps?.outdoor) return void alert("请先生成世界数据,再进行推演"); const t = $("world-sim-ok"), e = $("world-sim-status"); BtnState.load(t, "推演中"), e.style.display = "block", e.style.color = "var(--ok)", e.textContent = "正在分析玩家行为并推演世界变化...", post("SIMULATE_WORLD", { requestId: Req.create("wsim"), currentData: JSON.stringify({ meta: D.meta, timeline: D.timeline, world: D.world, maps: D.maps }, null, 2) }) }, $("btn-deduce").onclick = () => openM("m-world-gen"), $("btn-simulate").onclick = () => openM("m-world-sim"), $("btn-side-menu-toggle").onclick = () => { const t = $("side-menu-panel"), e = $("btn-side-menu-toggle"); t.classList.toggle("show"), e.classList.toggle("act", t.classList.contains("show")) }, document.addEventListener("click", (t => { t.target.closest(".side-menu") || ($("side-menu-panel")?.classList.remove("show"), $("btn-side-menu-toggle")?.classList.remove("act")) })); const getWaitingContacts = t => { const e = canonicalLoc(t); return e ? (D.contacts.contacts || []).filter((t => t?.waitingAt && canonicalLoc(t.waitingAt) === e)) : [] }, finishTravel = (t, e) => { playerLocation = t, selectedMapValue = "current"; const s = getWaitingContacts(t); s.forEach((t => delete t.waitingAt)), saveAll(), render(), hideInfo(); const o = $("goto-task")?.value; let a = `{{user}}离开了${e || "上一地点"},来到${t}。${o ? "意图:" + o : ""}`; s.length && (a += ` ${s.map((t => t.name)).join("、")}已经在这里等你了。`), post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${a}` }) }; $("goto-ok").onclick = () => { if (!curNode) return; const t = $("goto-ok"), e = D.maps?.outdoor?.nodes?.find((t => t.name === playerLocation)), s = { name: playerLocation, info: e?.info || "" }, o = curNode.name, a = D.maps?.indoor?.[o]; if (a?.description) return BtnState.reset(t, "确认前往"), closeM("m-goto"), finishTravel(o, s.name), void switchMapView("current"); let n = "main" === curNode.type ? "main" : "home" === curNode.type ? "home" : "sub"; const r = Req.create("scene"); Req.set("scene", { node: curNode, prev: s.name }), BtnState.load(t, "生成中"), post("SCENE_SWITCH", { requestId: r, prevLocationName: s.name, prevLocationInfo: s.info, targetLocationName: curNode.name, targetLocationType: n, targetLocationInfo: curNode.data?.info || "", playerAction: $("goto-task").value || "" }) }, $("btn-gen-local-map").onclick = () => { const t = $("btn-gen-local-map"); BtnState.load(t, "生成中"); const e = Req.create("localmap"); Req.set("localmap", { btn: t }), post("GENERATE_LOCAL_MAP", { requestId: e, outdoorDescription: D.maps?.outdoor?.description || "" }) }, $("btn-refresh-local-map").onclick = () => { if (!playerLocation) return void showResultModal("提示", "请先生成世界数据", !0); const t = $("btn-refresh-local-map"), e = D.maps?.indoor?.[playerLocation], s = e; if (!s) return void showResultModal("提示", "当前区域没有局部地图,请先生成", !0); BtnState.load(t, "刷新中"); const o = Req.create("localmaprf"); Req.set("localmaprf", { btn: t, loc: playerLocation }), post("REFRESH_LOCAL_MAP", { requestId: o, locationName: playerLocation, currentLocalMap: s, outdoorDescription: D.maps?.outdoor?.description || "" }) }, $("btn-gen-local-scene").onclick = () => { if (!playerLocation || "未知" === playerLocation) return void showResultModal("提示", "请先生成世界数据", !0); const t = $("btn-gen-local-scene"); BtnState.load(t, "生成中"); const e = D.maps?.outdoor?.nodes?.find((t => t.name === playerLocation)), s = D.maps?.indoor?.[playerLocation], o = s?.description || e?.info || e?.data?.info || "", a = Req.create("localscene"); Req.set("localscene", { btn: t, loc: playerLocation }), post("GENERATE_LOCAL_SCENE", { requestId: a, locationName: playerLocation, locationInfo: o }) }, $("btn-refresh-world-news").onclick = () => { const t = $("btn-refresh-world-news"); if (!t) return; t.disabled = !0, t._o = t._o ?? t.innerHTML, t.innerHTML = ''; const e = Req.create("newsrf"); Req.set("newsrf", { btn: t }), post("REFRESH_WORLD_NEWS", { requestId: e }) }; const saveAll = () => post("SAVE_ALL_DATA", { allData: { meta: D.meta, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, sceneSetup: D.sceneSetup, strangers: D.contacts.strangers, contacts: contactsForSave() }, playerLocation }), dataKeys = [["meta", "大纲", "核心真相、洋葱结构、时间线、用户指南", () => D.meta, t => D.meta = t], ["world", "世界资讯", "世界新闻等信息", () => D.world, t => D.world = t], ["outdoor", "大地图", "室外区域的地点和路线", () => D.maps.outdoor, t => D.maps.outdoor = t], ["indoor", "局部地图", "隐藏的室内/局部场景地图", () => D.maps.indoor, t => D.maps.indoor = t], ["sceneSetup", "区域剧情", "当前区域的 Side Story", () => D.sceneSetup, t => D.sceneSetup = t], ["characterContactSms", "角色卡短信", "角色卡联络人的短信记录", () => ({ messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, summaries: charSmsHistory?.summaries || {} }), t => { t && "object" == typeof t && (charSmsHistory = { messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, ...t || {} }) }], ["strangers", "陌路人", "已遇见但未建立联系的角色", () => D.contacts.strangers, t => D.contacts.strangers = t], ["contacts", "联络人", "已添加的联系人", () => contactsForSave(), t => { const e = (D.contacts.contacts || []).find(isCharCardContact); D.contacts.contacts = (e ? [e] : []).concat(Array.isArray(t) ? t : []) }]]; let gSet = { apiUrl: "", apiKey: "", model: "", mode: "assist" }, dataCk = {}, editCtx = null, commSet = { historyCount: 50, npcPosition: 0, npcOrder: 100, stream: !1 }, promptDefaults = { jsonTemplates: {}, promptSources: {} }, promptStores = { global: { jsonTemplates: {}, promptSources: {} }, character: { jsonTemplates: {}, promptSources: {} } }; const reqSet = () => post("GET_SETTINGS"), renderDataList = () => { $("data-list").innerHTML = dataKeys.map((([t, e, s]) => `
${e}
${s}
`)).join(""), $$("#data-list .data-item").forEach((t => t.onclick = e => { if (e.target.closest(".data-edit")) return; const s = t.dataset.k; dataCk[s] = !dataCk[s], t.classList.toggle("sel", dataCk[s]) })), $$("#data-list .data-edit").forEach((t => t.onclick = e => { e.stopPropagation(), openDataEdit(t.dataset.k) })) }, ADV_PROMPT_ITEMS = [["sms", "短信回复"], ["invite", "邀请回复"], ["npc", "NPC 生成"], ["importantNpc", "重要NPC生成"], ["stranger", "提取陌路人"], ["worldGenStep1", "大纲生成"], ["worldGenStep2", "世界生成"], ["worldSim", "世界推演(故事模式)"], ["worldSimAssist", "世界推演(辅助模式)"], ["worldNewsRefresh", "世界新闻刷新"], ["sceneSwitch", "场景切换"], ["localMapGen", "局部地图生成"], ["localMapRefresh", "局部地图刷新"], ["localSceneGen", "局部剧情生成"], ["summary", "总结压缩"]], advHasJsonTemplate = t => { const e = promptDefaults?.jsonTemplates || {}; return Object.prototype.hasOwnProperty.call(e, t) }, advGetScope = () => "global" === $("adv-scope")?.value ? "global" : "character", advStoreHasKey = (t, e) => { const s = ("global" === t ? promptStores.global : promptStores.character) || {}; return !(!Object.prototype.hasOwnProperty.call(s?.promptSources || {}, e) && !Object.prototype.hasOwnProperty.call(s?.jsonTemplates || {}, e)) }, advGetPromptObj = (t, e = "character") => { const s = promptDefaults?.promptSources || {}, o = promptStores?.global?.promptSources || {}, a = promptStores?.character?.promptSources || {}, n = ("global" === e ? o[t] || s[t] : a[t] || o[t] || s[t]) || {}; return { u1: "string" == typeof n.u1 ? n.u1 : "", a1: "string" == typeof n.a1 ? n.a1 : "", u2: "string" == typeof n.u2 ? n.u2 : "", a2: "string" == typeof n.a2 ? n.a2 : "" } }, advNormalizeDisplayText = t => { let e = String(t ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"); return !e.includes("\n") && e.includes("\\n") && (e = e.replaceAll("\\n", "\n")), e.includes("\\t") && (e = e.replaceAll("\\t", "\t")), e.includes("\\`") && (e = e.replaceAll("\\`", "`")), e }, advNormalizeSaveText = t => String(t ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"), advGetJsonTemplate = (t, e = "character") => { const s = promptDefaults?.jsonTemplates || {}, o = promptStores?.global?.jsonTemplates || {}, a = promptStores?.character?.jsonTemplates || {}; if (!(advHasJsonTemplate(t) || Object.prototype.hasOwnProperty.call(o, t) || Object.prototype.hasOwnProperty.call(a, t))) return ""; const n = "global" === e ? Object.prototype.hasOwnProperty.call(o, t) ? o[t] : s[t] : Object.prototype.hasOwnProperty.call(a, t) ? a[t] : Object.prototype.hasOwnProperty.call(o, t) ? o[t] : s[t]; return "string" == typeof n ? n : "" }, advApplyToUI = (t, e = "character") => { const s = advGetPromptObj(t, e), o = (t, e) => { const s = $(t); s && (s.value = advNormalizeDisplayText(e)) }; o("adv-u1", s.u1), o("adv-a1", s.a1), o("adv-u2", s.u2), o("adv-a2", s.a2), $("adv-json-wrap").style.display = ""; const a = advGetJsonTemplate(t, e), n = $("adv-json"); n && (n.value = String(a ?? "")), advUpdateVarHelp(t); const _n = $("adv-attr-notice"); _n && (_n.style.display = t === "importantNpc" ? "" : "none") }, advUpdateVarHelp = t => { $$(".adv-vars-group").forEach((e => { const s = String(e.dataset.advFor || "").split(",").map((t => t.trim())).filter(Boolean); e.style.display = !s.length || s.includes(t) ? "" : "none" })) }, advBuildEdits = () => { const t = t => advNormalizeSaveText($(t)?.value ?? ""); return { prompt: { u1: t("adv-u1"), a1: t("adv-a1"), u2: t("adv-u2"), a2: t("adv-a2") }, jsonTemplate: advNormalizeSaveText($("adv-json")?.value ?? "") } }, advInit = () => { const t = $("adv-key"); if (!t || t._inited) return; t._inited = !0, t.innerHTML = ADV_PROMPT_ITEMS.map((([t, e]) => ``)).join(""), t.onchange = () => advApplyToUI(t.value, advGetScope()); const e = $("adv-scope"); e && !e._inited && (e._inited = !0, e.onchange = () => { const t = $("adv-key")?.value; t && advApplyToUI(t, advGetScope()) }) }, advOpen = () => { advInit(); const t = $("adv-key"), e = t?.value || ADV_PROMPT_ITEMS[0]?.[0]; if (e) { const t = $("adv-scope"); t && (t.value = advStoreHasKey("character", e) ? "character" : "global"), advApplyToUI(e, advGetScope()) } openM("m-adv-prompts") }, advSaveTo = t => { advInit(); const e = $("adv-key")?.value; if (!e) return; const { prompt: s, jsonTemplate: o } = advBuildEdits(); post("SAVE_PROMPTS", { scope: t, key: e, prompt: s, jsonTemplate: o }), closeM("m-adv-prompts") }, advReset = () => { advInit(); const t = $("adv-key")?.value; t && (post("SAVE_PROMPTS", { scope: advGetScope(), key: t, reset: !0 }), closeM("m-adv-prompts")) }, parseJsonLoose = t => { const e = String(t ?? "").trim(); if (!e) throw new Error("空内容"); try { return JSON.parse(e) } catch { } const s = e.match(/```[^\n]*\n([\s\S]*?)\n```/); if (s?.[1]) { const t = s[1].trim(); try { return JSON.parse(t) } catch { } } const o = (t, s) => { const o = e.indexOf(t), a = e.lastIndexOf(s); return -1 === o || -1 === a || a <= o ? null : e.slice(o, a + 1) }, a = o("{", "}") ?? o("[", "]"); return a ? JSON.parse(a) : JSON.parse(e) }, updateEditPreview = () => { const t = $("data-edit-preview"); t && (t.style.display = "none", t.textContent = "") }, setEditContent = (t, e) => { $("data-edit-title").textContent = t, $("data-edit-ta").value = e, $("data-edit-err").classList.remove("vis"), updateEditPreview(), openM("m-data-edit") }, openDataEdit = t => { const e = dataKeys.find((([e]) => e === t)); e && (editCtx = { type: "characterContactSms" === t ? "charSms" : "data", key: t }, setEditContent(`编辑 - ${e[1]}`, JSON.stringify(e[3](), null, 2))) }; $("data-edit-save").onclick = () => { if (editCtx) try { const t = parseJsonLoose($("data-edit-ta").value); if ("data" === editCtx.type) { const e = dataKeys.find((([t]) => t === editCtx.key)); if (!e) return; e[4](t), render(), saveAll() } else if ("charSms" === editCtx.type) { const e = t?.summaries ?? t; if (!e || "object" != typeof e || Array.isArray(e)) throw new Error("需要 summaries 对象"); charSmsHistory.summaries = e, post("SAVE_CHAR_SMS_HISTORY", { summaries: e }) } closeM("m-data-edit"), editCtx = null } catch (t) { $("data-edit-err").textContent = `JSON错误: ${t.message}`, $("data-edit-err").classList.add("vis") } }, $("data-edit-ta").addEventListener("input", updateEditPreview); const showTestRes = (t, e) => { const s = $("test-res"); s.textContent = e, s.className = "set-test-res " + (t ? "ok" : "err") }, showResultModal = (t, e, s = !1, o = null) => { $("res-title").textContent = t, $("res-title").style.color = s ? "var(--err)" : "", $("res-msg").textContent = e; const a = $("res-record-box"), n = $("res-record"); o ? (a.style.display = "block", n.textContent = "object" == typeof o ? JSON.stringify(o, null, 2) : String(o)) : (a.style.display = "none", n.textContent = ""); const r = $("res-action"); r.style.display = "none", r.textContent = "", r.onclick = null, openM("m-result") }; function render() { const t = D.world?.news || []; $("news-list").innerHTML = t.length ? t.map((t => `
${h(t.title)}
${h(t.time || "")}

${h(t.content)}

`)).join("") : '
暂无新闻
', $$("#news-list .fold").forEach(bindFold); const e = D.meta?.user_guide; e && ($("ug-state").textContent = e.current_state || "未知状态", $("ug-actions").innerHTML = (e.guides || []).map(((t, e) => `
${e + 1}. ${h(t)}
`)).join("") || '
暂无行动指南
'); const s = (t, e) => (t || []).length ? t.map((t => `
${h(t.avatar || "")}
${h(t.name || "")}
${t.online ? "● 在线" : h(t.location)}
${t.info ? `
${h(t.info)}
` : ""}
${e ? `` : ``}
`)).join("") : '
暂无
'; if ($("sec-stranger").innerHTML = s(D.contacts.strangers, !0), $("sec-contact").innerHTML = s(D.contacts.contacts, !1), $$(".comm-sec .fold").forEach(bindFold), $$(".add-btn").forEach((t => t.onclick = e => { e.stopPropagation(), genAddCt(t.dataset.name, t.dataset.info || "", t, t.dataset.npctype || "npc") })), $$(".ignore-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.strangers.findIndex((e => e.name === t.dataset.name)); s > -1 && (D.contacts.strangers.splice(s, 1), saveCt(), render()) })), $$(".msg-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.contacts.find((e => e.worldbookUid === t.dataset.uid)); s && openChat(s) })), $$(".inv-btn").forEach((t => t.onclick = e => { e.stopPropagation(); const s = D.contacts.contacts.find((e => e.worldbookUid === t.dataset.uid)); s && openInv(s) })), "current" === selectedMapValue) { const t = getCurInside(); $("side-desc").innerHTML = t?.description ? `
📍 ${h(playerLocation)}
` + parseLinks(t.description) : parseLinks(D.maps?.outdoor?.description || "") } else $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""); bindLinks($("side-desc")), $("mob-desc").innerHTML = "", renderMapSelector(), renderMap() } function renderMap() { const t = D.maps?.outdoor; if (seed = 123456789, inner.querySelectorAll(".item").forEach((t => t.remove())), svg.innerHTML = "", nodes = [], lines = [], !t?.nodes?.length) return; t.nodes.forEach(((t, e) => { const s = dirMap[t.position] || [0, 0], o = Math.hypot(s[0], s[1]) || 1, a = 120 * (t.distant || 1); nodes.push({ id: "n" + e, name: t.name, type: t.type || "sub", pos: t.position, distant: t.distant || 1, x: s[0] / o * a, y: s[1] / o * a, data: t }) })); const e = {}; nodes.forEach((t => (e[t.pos] = e[t.pos] || []).push(t))); for (let t in e) { const s = e[t]; if (s.length <= 1) continue; const o = dirMap[t] || [0, 0], a = Math.hypot(o[0], o[1]) || 1, n = -o[1] / a, r = o[0] / a, i = (s.length - 1) / 2; s.sort(((t, e) => t.distant - e.distant)).forEach(((t, e) => { t.x += n * (e - i) * 50, t.y += r * (e - i) * 50 })) } for (let t = 0; t < 15; t++)for (let t = 0; t < nodes.length; t++)for (let e = t + 1; e < nodes.length; e++) { const s = nodes[t], o = nodes[e], a = s.x - o.x, n = s.y - o.y, r = Math.hypot(a, n); if (r < 80 && r > 0) { const t = (80 - r) / r * .5; s.x += a * t, s.y += n * t, o.x -= a * t, o.y -= n * t } } nodes.forEach((t => { const e = document.createElement("div"); e.className = `item node-${t.type}`, e.id = t.id, e.textContent = "home" === t.type ? "🏠 " + t.name : t.name, e.style.cssText = `left:${t.x + 2e3}px;top:${t.y + 2e3}px`, e.onclick = e => { e.stopPropagation(), curNode?.id === t.id ? hideInfo() : showInfo(t) }, inner.appendChild(e) })); for (let t in e) { const s = e[t].sort(((t, e) => t.distant - e.distant)); for (let t = 0; t < s.length - 1; t++)lines.push([s[t], s[t + 1]]) } const s = Object.values(e).map((t => t.sort(((t, e) => t.distant - e.distant))[0])).sort(((t, e) => Math.atan2(t.y, t.x) - Math.atan2(e.y, e.x))); s.forEach(((t, e) => lines.push([t, s[(e + 1) % s.length]]))); const o = (t, e) => lines.some((([s, o]) => s === t && o === e || s === e && o === t)); for (let t = 0, e = 0; e < Math.floor(nodes.length / 5) && t < 200; t++) { const t = nodes[Math.floor(rand() * nodes.length)], s = nodes[Math.floor(rand() * nodes.length)]; t === s || o(t, s) || (lines.push([t, s]), e++) } drawLines() } function drawLines() { svg.innerHTML = ""; const t = mapWrap.getBoundingClientRect(); lines.forEach((([e, s]) => { const o = $(e.id), a = $(s.id); if (!o || !a) return; const n = o.getBoundingClientRect(), r = a.getBoundingClientRect(), i = document.createElementNS("http://www.w3.org/2000/svg", "line"); i.setAttribute("x1", (n.left + n.width / 2 - t.left) / scale - offX / scale), i.setAttribute("y1", (n.top + n.height / 2 - t.top) / scale - offY / scale), i.setAttribute("x2", (r.left + r.width / 2 - t.left) / scale - offX / scale), i.setAttribute("y2", (r.top + r.height / 2 - t.top) / scale - offY / scale); const c = "main" === e.type && "main" === s.type || "home" === e.type || "home" === s.type; i.setAttribute("stroke", c ? "var(--c)" : "var(--c4)"), i.setAttribute("stroke-width", c ? "2" : "1"), c || i.setAttribute("stroke-dasharray", "4 3"), svg.appendChild(i) })) } $("btn-settings").onclick = () => { reqSet(), $("set-api-url").value = gSet.apiUrl || "", $("set-api-key").value = gSet.apiKey || "", $("set-model").value = gSet.model || "", $("set-model-list").style.display = "none", $("test-res").className = "set-test-res", $("set-stage").value = D.stage || 0, $("set-deviation").value = D.deviationScore || 0, $("set-sim-target").value = D.simulationTarget ?? 5, $("set-mode").value = gSet.mode || "story", $("set-history-count").value = commSet.historyCount || 50, $("set-use-stream").checked = !!commSet.stream, $("set-npc-position").value = commSet.npcPosition || 0, $("set-npc-order").value = commSet.npcOrder || 100, renderDataList(), syncSimDueUI(), openM("m-settings") }, $("btn-adv-prompts").onclick = () => advOpen(), $("adv-save-global").onclick = () => advSaveTo("global"), $("adv-save-char").onclick = () => advSaveTo("character"), $("adv-reset").onclick = () => advReset(), $("btn-fetch-models").onclick = () => { BtnState.load($("btn-fetch-models"), "加载"), post("FETCH_MODELS", { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim() }) }, $("btn-test-conn").onclick = () => { $("test-res").className = "set-test-res", BtnState.load($("btn-test-conn"), "测试"), post("TEST_CONNECTION", { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim(), model: $("set-model").value.trim() }) }, $("set-save").onclick = () => { gSet = { apiUrl: $("set-api-url").value.trim(), apiKey: $("set-api-key").value.trim(), model: $("set-model").value.trim(), mode: $("set-mode").value || "story" }, D.stage = Math.max(0, Math.min(10, parseInt($("set-stage").value, 10) || 0)), D.deviationScore = Math.max(0, Math.min(100, parseInt($("set-deviation").value, 10) || 0)), D.simulationTarget = parseInt($("set-sim-target").value, 10), Number.isNaN(D.simulationTarget) && (D.simulationTarget = 5), commSet = { historyCount: Math.max(0, Math.min(200, parseInt($("set-history-count").value, 10) || 50)), stream: !!$("set-use-stream").checked, npcPosition: parseInt($("set-npc-position").value, 10) || 0, npcOrder: Math.max(0, Math.min(1e3, parseInt($("set-npc-order").value, 10) || 100)) }; const t = {}; dataKeys.forEach((([e, , , s]) => { dataCk[e] && (t[e] = s()) })), syncSimDueUI(), post("SAVE_SETTINGS", { globalSettings: gSet, commSettings: commSet, stage: D.stage, deviationScore: D.deviationScore, simulationTarget: D.simulationTarget, playerLocation, dataChecked: dataCk, outlineData: t, allData: { meta: D.meta, timeline: D.timeline, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, strangers: D.contacts.strangers, contacts: contactsForSave() } }), closeM("m-settings") }, $("btn-close").onclick = () => post("CLOSE_PANEL"), window.addEventListener("message", (t => { if (t.origin !== PARENT_ORIGIN || t.source !== parent) return; if ("LittleWhiteBox" !== t.data?.source) return; const e = t.data, s = e.type; if ("LOAD_SETTINGS" === s) { if (e.globalSettings && (gSet = e.globalSettings), void 0 !== e.stage && (D.stage = e.stage), void 0 !== e.deviationScore && (D.deviationScore = e.deviationScore), void 0 !== e.simulationTarget && (D.simulationTarget = e.simulationTarget), e.playerLocation && (playerLocation = e.playerLocation), e.commSettings && (commSet = { historyCount: e.commSettings.historyCount ?? 50, npcPosition: e.commSettings.npcPosition ?? 0, npcOrder: e.commSettings.npcOrder ?? 100, stream: !!e.commSettings.stream }), e.dataChecked && (dataCk = e.dataChecked), e.promptConfig && (promptDefaults = e.promptConfig.defaults || promptDefaults, e.promptConfig.stores ? (promptStores.global = e.promptConfig.stores.global || { jsonTemplates: {}, promptSources: {} }, promptStores.character = e.promptConfig.stores.character || { jsonTemplates: {}, promptSources: {} }) : (promptStores.global = e.promptConfig.current || { jsonTemplates: {}, promptSources: {} }, promptStores.character = promptStores.character || { jsonTemplates: {}, promptSources: {} })), e.outlineData) { const t = e.outlineData; t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.outdoor && (D.maps.outdoor = t.outdoor), t.indoor && (D.maps.indoor = t.indoor), t.sceneSetup && (D.sceneSetup = t.sceneSetup), t.strangers && (D.contacts.strangers = t.strangers), t.contacts && (D.contacts.contacts = t.contacts) } { const t = e.characterContactSmsHistory || {}; charSmsHistory = { messages: Array.isArray(t.messages) ? t.messages : [], summarizedCount: t.summarizedCount || 0, summaries: t.summaries || {} } } let t = D.contacts.contacts.find((t => "__CHARACTER_CARD__" === t.worldbookUid)); t || (t = D.contacts.contacts.find((t => !t.worldbookUid && "炒饭智能" === t.name)), t ? (t.worldbookUid = "__CHARACTER_CARD__", t.info = "角色卡联络人", t.location = "在线", t.online = !0) : (D.contacts.contacts.unshift({ name: e.characterCardName || "{{characterName}}", avatar: "", color: "#555", location: "在线", info: "角色卡联络人", online: !0, worldbookUid: "__CHARACTER_CARD__", messages: [], summarizedCount: 0 }), t = D.contacts.contacts[0])), t && e.characterCardName && (t.name = e.characterCardName, t.avatar = (e.characterCardName || "")[0] || t.avatar || ""), render(), syncSimDueUI(), $("m-settings").classList.contains("act") && ($("set-api-url").value = gSet.apiUrl || "", $("set-api-key").value = gSet.apiKey || "", $("set-model").value = gSet.model || "", $("set-stage").value = D.stage, $("set-deviation").value = D.deviationScore, $("set-sim-target").value = D.simulationTarget ?? 5, $("set-mode").value = gSet.mode || "story", $("set-history-count").value = commSet.historyCount, $("set-use-stream").checked = !!commSet.stream, $("set-npc-position").value = commSet.npcPosition, $("set-npc-order").value = commSet.npcOrder, renderDataList()) } else if ("PROMPT_CONFIG_UPDATED" === s) { if (e.promptConfig && (promptDefaults = e.promptConfig.defaults || promptDefaults, e.promptConfig.stores ? (promptStores.global = e.promptConfig.stores.global || { jsonTemplates: {}, promptSources: {} }, promptStores.character = e.promptConfig.stores.character || { jsonTemplates: {}, promptSources: {} }) : (promptStores.global = e.promptConfig.current || { jsonTemplates: {}, promptSources: {} }, promptStores.character = promptStores.character || { jsonTemplates: {}, promptSources: {} }), $("m-adv-prompts").classList.contains("act"))) { const t = $("adv-key")?.value; t && advApplyToUI(t, advGetScope()) } } else if ("FETCH_MODELS_RESULT" === s) { BtnState.reset($("btn-fetch-models"), "获取"); const t = $("set-model-list"); if (e.error) return t.style.display = "none", void showTestRes(!1, "获取模型失败: " + e.error); if (!e.models?.length) return t.style.display = "none", void showTestRes(!1, "未找到可用模型"); t.innerHTML = '' + e.models.map((t => ``)).join(""), t.style.display = "block", t.onchange = () => { t.value && ($("set-model").value = t.value) }, showTestRes(!0, `找到 ${e.models.length} 个模型`) } else if ("TEST_CONN_RESULT" === s) BtnState.reset($("btn-test-conn"), "测试连接"), showTestRes(e.success, e.message); else if ("CHECK_WORLDBOOK_UID_RESULT" === s) { if (BtnState.reset($("btn-check-uid"), ' 检查'), !Req.match(e.requestId)) return; if (e.error) return void showUidErr(e.error); if (!e.primaryKeys?.length) return void showUidErr("该条目没有主要关键字"); addCtState.keys = e.primaryKeys; const t = $("add-name"); t.innerHTML = '' + e.primaryKeys.map((t => ``)).join(""), t.onchange = () => { addCtState.name = t.value, $("add-ct-ok").disabled = !t.value }, $("name-select-group").style.display = "block", 1 === e.primaryKeys.length && (addCtState.name = e.primaryKeys[0], t.value = addCtState.name, $("add-ct-ok").disabled = !1) } else if ("SMS_RESULT" === s) { const t = Req.get("sms"); if (!t || t.id !== e.requestId) return; if (Req.clear("sms"), smsGen = !1, $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !1, !chatTgt) return; chatTgt.messages = chatTgt.messages.filter((t => !t.typing)), e.error ? chatTgt.messages.push({ type: "received", text: `[错误] ${e.error}` }) : e.reply && chatTgt.messages.push({ type: "received", text: stripXml(e.reply) }), renderMsgs(), saveCt(), chatTgt.worldbookUid && saveChat(chatTgt) } else if ("SMS_STREAM" === s) { const t = Req.get("sms"); if (!t || t.id !== e.requestId || !chatTgt) return; const s = chatTgt.messages.find((t => t.typing)); s && e.text && (s.text = e.text, renderMsgs()) } else if ("LOAD_SMS_HISTORY_RESULT" === s) { if (!chatTgt || chatTgt.worldbookUid !== e.worldbookUid) return; e.messages?.length && (chatTgt.messages = e.messages, chatTgt.summarizedCount = e.summarizedCount || 0, saveCt()), renderMsgs() } else if ("COMPRESS_SMS_RESULT" === s) { const t = Req.get("compress"); if (!t || t.id !== e.requestId) return; if (Req.clear("compress"), smsGen = !1, $("chat-compress").disabled = $("chat-in").disabled = $("chat-send").disabled = $("chat-back").disabled = !1, !chatTgt) return; if (e.error) return void alert(`压缩失败: ${e.error}`); void 0 !== e.newSummarizedCount && (chatTgt.summarizedCount = e.newSummarizedCount, renderMsgs(), saveCt()) } else if ("CHECK_STRANGER_WORLDBOOK_RESULT" === s) { const t = Req.get("stgwb"); if (!t || t.id !== e.requestId) return; const { name: s, info: o, btn: a, npcType: nt } = t; if (Req.clear("stgwb"), e.found && e.worldbookUid) { BtnState.reset(a, nt === "importantNpc" ? ' 重要' : ' 背景板'); const t = D.contacts.strangers.findIndex((t => t.name === s)); if (t > -1) { const s = D.contacts.strangers.splice(t, 1)[0]; D.contacts.contacts.push({ name: s.name, avatar: s.avatar || s.name[0], color: s.color || "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: s.location || "未知", info: s.info || "", worldbookUid: e.worldbookUid, messages: [] }), saveCt(), render() } } else { BtnState.load(a, "生成中"); const t = Req.create("npcgen"); Req.set("npcgen", { name: s, info: o, btn: a, npcType: nt }), post("GENERATE_NPC", { requestId: t, strangerName: s, strangerInfo: o, npcType: nt }) } } else if ("GENERATE_NPC_RESULT" === s) { const t = Req.get("npcgen"); if (!t || t.id !== e.requestId) return; const { name: s, btn: o, npcType: nt } = t; const _resetLbl = nt === "importantNpc" ? ' 重要' : ' 背景板'; if (Req.clear("npcgen"), BtnState.reset(o, _resetLbl), e.error) return void showResultModal("生成角色失败", "生成 NPC 失败", !0, e.error); if (e.success && e.worldbookUid) { const t = D.contacts.strangers.findIndex((t => t.name === s)); if (t > -1) { const o = D.contacts.strangers.splice(t, 1)[0], a = e.npcData || {}; D.contacts.contacts.push({ name: a.name || o.name, avatar: (a.name || o.name)[0], color: o.color || "#" + Math.floor(16777215 * Math.random()).toString(16).padStart(6, "0"), location: o.location || "未知", info: a.intro || o.info || "", worldbookUid: e.worldbookUid, messages: [] }), saveCt(), render(), showResultModal("生成成功", `NPC ${s} 已生成并添加到联络人`, !1, e.npcData) } } } else if ("EXTRACT_STRANGERS_RESULT" === s) { const t = Req.get("extract"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("extract"), BtnState.reset(s, ''), e.error) return void showResultModal("提取失败", "提取陌路人失败", !0, e.error); if (e.success && Array.isArray(e.strangers)) { if (!e.strangers.length) return void showResultModal("提取结果", "没有发现新的陌路人"); const t = [...D.contacts.contacts.map((t => t.name)), ...D.contacts.strangers.map((t => t.name))], s = e.strangers.filter((e => !t.includes(e.name))); if (!s.length) return void showResultModal("提取结果", "提取到的角色都已存在"); D.contacts.strangers = D.contacts.strangers.concat(s), saveCt(), render(), showResultModal("提取成功", `成功提取 ${s.length} 个新陌路人`, !1, s) } } else if ("GENERATE_WORLD_STATUS" === s) { if (!Req.match(e.requestId)) return; const t = $("world-gen-status"); t.style.display = "block", t.style.color = "var(--ok)", t.textContent = e.message } else if ("GENERATE_WORLD_RESULT" === s) { if (!Req.match(e.requestId)) return; Req.clear("wgen"); const t = $("world-gen-ok"), s = $("world-gen-status"); if (BtnState.reset(t, ' 开始生成'), e.error) { if (s.style.display = "none", showResultModal("生成失败", "世界生成失败", !0, e.error), String(e.error || "").includes("Step 2")) { const e = $("res-action"); e.style.display = "inline-block", e.textContent = "重试 Step2", e.onclick = () => { closeM("m-result"); const e = Req.create("wgen"); BtnState.load(t, "重试中"), s.style.display = "block", s.style.color = "var(--ok)", s.textContent = "准备重试 Step 2/2...", post("RETRY_WORLD_GEN_STEP2", { requestId: e }) } } return } if (e.success && e.worldData) { s.style.color = "var(--ok)", s.textContent = "生成成功!正在应用数据..."; const t = e.worldData; if (t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.maps?.outdoor && (D.maps.outdoor = t.maps.outdoor), D.stage = 0, D.deviationScore = 0, t.playerLocation) playerLocation = t.playerLocation; else { const t = D.maps?.outdoor?.nodes?.find((t => "home" === t.type)); playerLocation = t?.name || D.maps?.outdoor?.nodes?.[0]?.name || "未知" } t.maps?.inside && playerLocation && (D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[playerLocation] = t.maps.inside), selectedMapValue = "current", saveAll(), render(), setTimeout((() => { closeM("m-world-gen"), s.style.display = "none", $("world-gen-req").value = "", showResultModal("生成成功", "世界数据生成完成!Stage 和 Deviation 已重置为 0", !1, e.worldData) }), 500) } } else if ("SIMULATE_WORLD_RESULT" === s) { if (!e.isAuto && !Req.match(e.requestId)) return; if (!e.isAuto) { Req.clear("wsim"); const t = $("world-sim-ok"), s = $("world-sim-status"); if (BtnState.reset(t, ' 开始推演'), e.error) return s.style.display = "none", void showResultModal("推演失败", "世界推演失败", !0, e.error) } if (e.success && e.simData) { if (!e.isAuto) { const t = $("world-sim-status"); t.style.color = "var(--ok)", t.textContent = "推演成功!正在应用数据..." } const t = e.simData; t.meta && (D.meta = t.meta), t.world && (D.world = t.world), t.maps?.outdoor && (D.maps.outdoor = t.maps.outdoor), D.stage = (D.stage || 0) + 1, saveAll(), render(), e.isAuto || setTimeout((() => { closeM("m-world-sim"), $("world-sim-status").style.display = "none", showResultModal("推演成功", `世界推演完成!Stage 已推进到 ${D.stage}`, !1, e.simData) }), 500) } } else if ("REFRESH_WORLD_NEWS_RESULT" === s) { const t = Req.get("newsrf"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("newsrf"), s && (s.disabled = !1, s.innerHTML = s._o ?? ''), e.error) return void showResultModal("刷新失败", "世界新闻刷新失败", !0, e.error); e.success && Array.isArray(e.news) && (D.world = D.world || {}, D.world.news = e.news, saveAll(), render(), showResultModal("刷新成功", `已更新世界新闻(${e.news.length} 条)`, !1, e.news)) } else if ("SCENE_SWITCH_RESULT" === s) { const t = Req.get("scene"); if (!t || t.id !== e.requestId) return; const { node: s, prev: o } = t; if (Req.clear("scene"), BtnState.reset($("goto-ok"), "确认前往"), closeM("m-goto"), e.error) return void showResultModal("切换失败", "场景切换失败", !0, e.error); if (e.success && e.sceneData) { const t = e.sceneData; if ("number" == typeof t.newScore && (D.deviationScore = t.newScore), t.localMap && (D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s.name] = t.localMap), t.strangers?.length) { const e = new Set((D.contacts.strangers || []).map((t => t.name))), s = t.strangers.filter((t => !e.has(t.name))); D.contacts.strangers = [...D.contacts.strangers || [], ...s] } finishTravel(s.name, o), 0 !== t.scoreDelta && showResultModal("切换成功", `场景切换完成!\n偏差值变化: ${t.scoreDelta > 0 ? "+" : ""}${t.scoreDelta} (当前: ${t.newScore})`, !1, e.sceneData) } } else if ("REFRESH_LOCAL_MAP_RESULT" === s) { const t = Req.get("localmaprf"); if (!t || t.id !== e.requestId) return; const { btn: s, loc: o } = t; if (Req.clear("localmaprf"), BtnState.reset(s, '刷新'), e.error) return void showResultModal("刷新失败", "刷新局部地图失败", !0, e.error); if (e.success && e.localMapData) { const t = e.localMapData, s = t.name || o || playerLocation || "当前位置"; D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s] = t, playerLocation = s, selectedMapValue = "current", saveAll(), render(), t.description && ($("side-desc").innerHTML = `
📍 ${h(s)}
` + parseLinks(t.description), bindLinks($("side-desc"))), showResultModal("刷新成功", `局部地图已刷新!当前位置: ${s}`, !1, e.localMapData) } } else if ("GENERATE_LOCAL_SCENE_RESULT" === s) { const t = Req.get("localscene"); if (!t || t.id !== e.requestId) return; const { btn: s, loc: o } = t; if (Req.clear("localscene"), BtnState.reset(s, '局部剧情'), e.error) return void showResultModal("生成失败", "局部剧情生成失败", !0, e.error); if (e.success && e.sceneSetup) { D.sceneSetup = { ...D.sceneSetup || {}, ...e.sceneSetup || {} }, saveAll(), render(); const t = String(e.introduce || "").replace(/^\s*(?:\/?\s*(?:sendas|as)\s+name\s*=\s*(?:"[^"]*"|'[^']*'|\S+)\s+)/i, "").replace(/\s+/g, " ").trim(); t && post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${t}` }), showResultModal("生成成功", `局部剧情已生成:${o || playerLocation}`, !1, e.sceneSetup) } } else if ("SEND_INVITE_RESULT" === s) { const t = Req.get("invite"); if (!t || t.id !== e.requestId) return; const { contact: s, loc: o, btn: a } = t; if (Req.clear("invite"), BtnState.reset(a, "发送邀请"), e.error) return void showResultModal("邀请失败", "邀请发送失败", !0, e.error); if (e.success && e.inviteData) { const t = e.inviteData, a = canonicalLoc(t.targetLocation || o || ""), n = D.contacts.contacts.find((t => t && s && t.worldbookUid && s.worldbookUid && t.worldbookUid === s.worldbookUid)) || D.contacts.contacts.find((t => t && s && t.name === s.name)) || s; if (n.messages = n.messages || [], n.messages.push({ type: "sent", text: `我邀请你前往「${a}」` }), n.messages.push({ type: "received", text: t.reply }), t.accepted) { const e = canonicalLoc(playerLocation); n.location = a, e && a && e === a ? (delete n.waitingAt, post("EXECUTE_SLASH_COMMAND", { command: `/sendas name="剧情任务" ${n.name}过来了。` })) : n.waitingAt = a, showResultModal("邀请成功", `${n.name} 接受了邀请!\n回复: ${t.reply}`, !1, t) } else showResultModal("邀请被拒", `${n.name} 拒绝了邀请。\n回复: ${t.reply}`, !1, t); saveAll(), n.worldbookUid && saveChat(n), closeM("m-invite"), render(), openChat(n) } } else if ("GENERATE_LOCAL_MAP_RESULT" === s) { const t = Req.get("localmap"); if (!t || t.id !== e.requestId) return; const { btn: s } = t; if (Req.clear("localmap"), BtnState.reset(s, '局部地图'), e.error) return void showResultModal("生成失败", "局部地图生成失败", !0, e.error); if (e.success && e.localMapData) { const t = e.localMapData, s = t.name || "当前位置"; D.sceneSetup = null, D.maps.indoor = D.maps.indoor || {}, D.maps.indoor[s] = t, playerLocation = s, selectedMapValue = "current", saveAll(), render(), t.description && ($("side-desc").innerHTML = `
📍 ${h(s)}
` + parseLinks(t.description), bindLinks($("side-desc"))), showResultModal("生成成功", `局部地图生成完成!当前位置: ${s}`, !1, t) } } })); const updateTf = () => { inner.style.transform = `translate(${offX}px,${offY}px) scale(${scale})`, $("zoom-ind").textContent = Math.round(100 * scale) + "%", requestAnimationFrame(drawLines) }, initPos = () => { offX = mapWrap.clientWidth / 2 - 2e3, offY = mapWrap.clientHeight / 2 - 2e3, scale = 1, updateTf() }; function panTo(t, e = 350) { if (!t || anim) return; if (!$(t.id)) return; const s = -(t.x + 2e3) * scale + mapWrap.clientWidth / 2, o = -(t.y + 2e3) * scale + .25 * mapWrap.clientHeight, a = offX, n = offY, r = performance.now(); anim = !0, function t(i) { const c = Math.min((i - r) / e, 1), l = 1 - Math.pow(1 - c, 3); offX = a + (s - a) * l, offY = n + (o - n) * l, updateTf(), c < 1 ? requestAnimationFrame(t) : anim = !1 }(r) } function showInfo(t) { if (!t?.data) return; curNode = t, inner.querySelectorAll(".item").forEach((t => t.classList.remove("hl"))), $(t.id)?.classList.add("hl"); const e = t.name === playerLocation; $("btn-goto").classList.toggle("show", !e), e || ($("goto-t").textContent = `前往 ${t.name}`); const s = D.maps?.indoor?.[t.name]; e && s?.description ? ($("side-desc").innerHTML = `
📍 ${h(t.name)}
` + parseLinks(s.description), bindLinks($("side-desc"))) : ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc"))), isMob() ? ($("mob-info-t").textContent = t.name, $("mob-info-c").textContent = e ? s?.description || t.data.info || "暂无信息..." : t.data.info || "暂无信息...", popup.classList.contains("act") || openPop(1)) : ($("info-t").textContent = t.name, $("info-c").textContent = t.data.info || "暂无信息...", $("tip").classList.add("show")) } const hideInfo = () => { curNode = null, inner.querySelectorAll(".item").forEach((t => t.classList.remove("hl"))), $("btn-goto").classList.remove("show"), $("tip").classList.remove("show") }; function renderMapSelector() { const t = $("map-lbl-select"); t.innerHTML = ''; const e = D.maps?.outdoor?.nodes?.findIndex((t => t.name === playerLocation)), s = D.maps?.indoor && D.maps.indoor[playerLocation]; if ((e >= 0 || s) && (t.innerHTML += ``), t.innerHTML += "", D.maps?.outdoor?.nodes?.length && D.maps.outdoor.nodes.forEach(((e, s) => { e.name !== playerLocation && (t.innerHTML += ``) })), D.maps?.indoor) { const e = Object.keys(D.maps.indoor).filter((t => t !== playerLocation && !D.maps?.outdoor?.nodes?.some((e => e.name === t)))); e.length && (t.innerHTML += "", e.forEach((e => t.innerHTML += ``))) } t.value = selectedMapValue, updateMapLabel() } function updateMapLabel() { const t = $("map-lbl-select").value; if ("overview" === t) $("map-lbl-t").textContent = "大地图"; else if ("current" === t) $("map-lbl-t").textContent = playerLocation + "(你)"; else if (t.startsWith("node:")) { const e = parseInt(t.split(":")[1]); $("map-lbl-t").textContent = D.maps?.outdoor?.nodes?.[e]?.name || "未知" } else t.startsWith("indoor:") && ($("map-lbl-t").textContent = t.replace("indoor:", "")) } function switchMapView(t) { if (selectedMapValue = t, hideInfo(), "overview" === t) $("btn-goto").classList.remove("show"), $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")), initPos(); else if ("current" === t) { $("btn-goto").classList.remove("show"); const t = getCurInside(); t?.description ? ($("side-desc").innerHTML = `
📍 ${h(playerLocation)}
` + parseLinks(t.description), bindLinks($("side-desc"))) : ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc"))); const e = nodes.find((t => t.name === playerLocation)); e && (panTo(e), showInfo(e)) } else if (t.startsWith("node:")) { const e = parseInt(t.split(":")[1]), s = D.maps?.outdoor?.nodes?.[e]; $("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")); const o = nodes.find((t => t.name === s?.name)); o && (panTo(o), showInfo(o)) } else if (t.startsWith("indoor:")) { const e = t.replace("indoor:", ""), s = D.maps?.indoor?.[e]; e !== playerLocation ? ($("side-desc").innerHTML = parseLinks(D.maps?.outdoor?.description || ""), bindLinks($("side-desc")), curNode = { name: e, type: "sub", data: { info: stripXml(s?.description || "") }, isIndoor: !0 }, $("btn-goto").classList.add("show"), $("goto-t").textContent = `前往 ${e}`) : ($("btn-goto").classList.remove("show"), s?.description && ($("side-desc").innerHTML = `
🏠 ${h(e)}
` + parseLinks(s.description), bindLinks($("side-desc")))) } updateMapLabel() } $("info-bk").onclick = hideInfo, $("mob-info-bk").onclick = () => popup.classList.remove("act"), $("map-lbl-select").onchange = t => switchMapView(t.target.value); let sx, sy, lastDist = 0, lastCX = 0, lastCY = 0; mapWrap.onmousedown = t => { anim || t.target.closest(".map-act,.map-lbl") || (t.target.classList.contains("item") || hideInfo(), drag = !0, sx = t.clientX, sy = t.clientY, mapWrap.style.cursor = "grabbing") }, mapWrap.onmousemove = t => { drag && (offX += t.clientX - sx, offY += t.clientY - sy, sx = t.clientX, sy = t.clientY, updateTf()) }, mapWrap.onmouseup = mapWrap.onmouseleave = () => { drag = !1, mapWrap.style.cursor = "grab" }, mapWrap.onwheel = t => { if (anim) return; t.preventDefault(); const e = Math.max(.3, Math.min(3, scale + (t.deltaY > 0 ? -.1 : .1))), s = mapWrap.getBoundingClientRect(), o = t.clientX - s.left, a = t.clientY - s.top, n = e / scale; offX = o - (o - offX) * n, offY = a - (a - offY) * n, scale = e, updateTf() }, mapWrap.ontouchstart = t => { if (anim || t.target.closest(".map-act,.map-lbl")) return; const e = t.target.closest(".item"); e ? e._ts = { x: t.touches[0].clientX, y: t.touches[0].clientY, t: Date.now() } : (hideInfo(), 1 === t.touches.length ? (drag = !0, sx = t.touches[0].clientX, sy = t.touches[0].clientY) : 2 === t.touches.length && (drag = !1, lastDist = Math.hypot(t.touches[0].clientX - t.touches[1].clientX, t.touches[0].clientY - t.touches[1].clientY), lastCX = (t.touches[0].clientX + t.touches[1].clientX) / 2, lastCY = (t.touches[0].clientY + t.touches[1].clientY) / 2)) }, mapWrap.ontouchmove = t => { const e = t.target.closest(".item"); if (e && e._ts) Math.hypot(t.touches[0].clientX - e._ts.x, t.touches[0].clientY - e._ts.y) > 10 && (delete e._ts, drag = !0, sx = t.touches[0].clientX, sy = t.touches[0].clientY); else if (1 === t.touches.length && drag) t.preventDefault(), offX += t.touches[0].clientX - sx, offY += t.touches[0].clientY - sy, sx = t.touches[0].clientX, sy = t.touches[0].clientY, updateTf(); else if (2 === t.touches.length) { t.preventDefault(); const e = Math.hypot(t.touches[0].clientX - t.touches[1].clientX, t.touches[0].clientY - t.touches[1].clientY), s = (t.touches[0].clientX + t.touches[1].clientX) / 2, o = (t.touches[0].clientY + t.touches[1].clientY) / 2, a = Math.max(.3, Math.min(3, scale * (e / lastDist))), n = mapWrap.getBoundingClientRect(), r = s - n.left, i = o - n.top, c = a / scale; offX = r - (r - offX) * c, offY = i - (i - offY) * c, offX += s - lastCX, offY += o - lastCY, scale = a, lastDist = e, lastCX = s, lastCY = o, updateTf() } }, mapWrap.ontouchend = t => { const e = t.target.closest(".item"); if (e && e._ts) { const s = Date.now() - e._ts.t; if (delete e._ts, s < 300) { const s = nodes.find((t => t.id === e.id)); s && (t.preventDefault(), curNode?.id === s.id ? hideInfo() : showInfo(s)) } } drag = !1 }, $$(".nav-i").forEach((t => t.onclick = () => { $$(".nav-i").forEach((t => t.classList.remove("act"))), $$(".page").forEach((t => t.classList.remove("act"))), t.classList.add("act"), $(`page-${t.dataset.p}`).classList.add("act"); const e = "map" === t.dataset.p; sidePop.classList.toggle("show", e), isMob() && (e ? openPop(1) : popup.classList.remove("act")), e && setTimeout((() => { initPos(), drawLines() }), 50) })), $$(".comm-tab").forEach((t => t.onclick = () => { $$(".comm-tab").forEach((t => t.classList.remove("act"))), $$(".comm-sec").forEach((t => t.classList.remove("act"))), t.classList.add("act"), $(`sec-${t.dataset.t}`).classList.add("act") })), $("btn-goto").onclick = t => { t.stopPropagation(), curNode && ($("goto-d").textContent = `目的地:${curNode.name}`, $("goto-task").value = "", openM("m-goto")) }, addEventListener("resize", (() => requestAnimationFrame(drawLines))), window.clickTab = (t, e) => { const s = t.closest(".settings-modal"); if (!s) return; s.querySelectorAll(".set-nav-item").forEach((t => t.classList.remove("act"))), t.classList.add("act"), s.querySelectorAll(".set-tab-page").forEach((t => t.classList.remove("act"))); const o = document.getElementById(e); o && (o.classList.add("act"), o.style.animation = "none", o.offsetHeight, o.style.animation = null) }, document.addEventListener("DOMContentLoaded", (() => { render(), initPos(), sidePop.classList.add("show"), sidePop.classList.add("act"), isMob() && openPop(1), post("FRAME_READY"), setTimeout((() => { "current" === selectedMapValue && switchMapView("current") }), 100) }));