调整-局部剧情-流式开关-寻找NPC按钮-编辑模板形式。
This commit is contained in:
@@ -277,9 +277,9 @@ const DEFAULT_JSON_TEMPLATES = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"side_story": {
|
"side_story": {
|
||||||
"surface": "{{user}}刚进入时看到的画面或听到的话语,充满生活感。",
|
"Incident": "触发。描写打破环境平衡的瞬间。它是一个‘钩子’,负责强行吸引玩家注意力并建立临场感(如:突发的争吵、破碎声、人群的异动)。",
|
||||||
"inner": "如果{{user}}稍微多停留或互动,可以发现的细节(例如 NPC 的小秘密、店家的用心布置)。",
|
"Facade": "表现。交代明面上的剧情逻辑。不需过多渲染,只需叙述‘看起来是怎么回事’。重点在于冲突的表面原因、人物的公开说辞或围观者眼中的剧本。这是玩家不需要深入调查就能获得的信息。",
|
||||||
"Introduce": "接着玩家之前经历,填写引入这段故事的文字(纯叙述文本)。不要包含 /send、/sendas、/sys 或类似 'as name=\"...\"' 的前缀。"
|
"Undercurrent": "暗流。背后的秘密或真实动机。它是驱动事件发生的‘真实引擎’。它不一定是反转,但必须是‘隐藏在表面下的信息’(如:某种苦衷、被误导的真相、或是玩家探究后才能发现的关联)。它是对Facade的深化,为玩家的后续介入提供价值。"
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
.btn:hover{border-color:var(--c);background:var(--bg3)}
|
.btn:hover{border-color:var(--c);background:var(--bg3)}
|
||||||
.btn:disabled{opacity:.5;cursor:not-allowed}
|
.btn:disabled{opacity:.5;cursor:not-allowed}
|
||||||
.btn-p{background:var(--c);color:#fff;border-color:var(--c)}
|
.btn-p{background:var(--c);color:#fff;border-color:var(--c)}
|
||||||
|
.btn-due{background:#ffe1e1!important;border-color:#ff9b9b!important;color:#7a1f1f!important}
|
||||||
|
.btn-due:hover{background:#ffd1d1!important}
|
||||||
.btn-s{padding:6px 12px;font-size:12px}
|
.btn-s{padding:6px 12px;font-size:12px}
|
||||||
.btn-c{width:28px;height:28px;padding:0;background:#888;border-color:#777;color:#fff;border-radius:50%}
|
.btn-c{width:28px;height:28px;padding:0;background:#888;border-color:#777;color:#fff;border-radius:50%}
|
||||||
.btn-add{width:32px;height:32px;padding:0;border-radius:50%;flex-shrink:0}
|
.btn-add{width:32px;height:32px;padding:0;border-radius:50%;flex-shrink:0}
|
||||||
@@ -176,7 +178,7 @@
|
|||||||
.modal-by{flex:1;overflow-y:auto;padding:18px}
|
.modal-by{flex:1;overflow-y:auto;padding:18px}
|
||||||
|
|
||||||
/* 编辑器 */
|
/* 编辑器 */
|
||||||
.ed-ta{width:100%;min-height:200px;padding:12px;background:var(--bg);border:none;border-top:1px solid var(--bd);font-family:'SF Mono',Monaco,Consolas,monospace;font-size:11px;line-height:1.5;color:var(--c);resize:none;outline:none}
|
.ed-ta{width:100%;min-height:100px;padding:12px;background:var(--bg);border:none;border-top:1px solid var(--bd);font-family:'SF Mono',Monaco,Consolas,monospace;font-size:11px;line-height:1.5;color:var(--c);resize:vertical;outline:none;overflow:auto}
|
||||||
.ed-preview{margin-top:10px;padding:10px;background:var(--bg3);border:1px solid var(--bd);border-radius:4px;font-size:11px;font-family:'SF Mono',Monaco,Consolas,monospace;white-space:pre-wrap;display:none;color:var(--c2)}
|
.ed-preview{margin-top:10px;padding:10px;background:var(--bg3);border:1px solid var(--bd);border-radius:4px;font-size:11px;font-family:'SF Mono',Monaco,Consolas,monospace;white-space:pre-wrap;display:none;color:var(--c2)}
|
||||||
.ed-err{padding:10px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;color:#b91c1c;font-size:12px;margin-top:10px;display:none}
|
.ed-err{padding:10px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;color:#b91c1c;font-size:12px;margin-top:10px;display:none}
|
||||||
.ed-err.vis{display:block}
|
.ed-err.vis{display:block}
|
||||||
@@ -308,7 +310,7 @@
|
|||||||
<div class="side-menu-btn" id="btn-side-menu-toggle" title="快捷操作"><i class="fa-solid fa-ellipsis"></i></div>
|
<div class="side-menu-btn" id="btn-side-menu-toggle" title="快捷操作"><i class="fa-solid fa-ellipsis"></i></div>
|
||||||
<div class="side-menu-panel" id="side-menu-panel">
|
<div class="side-menu-panel" id="side-menu-panel">
|
||||||
<button class="btn btn-s fc g4" id="btn-gen-local-map"><i class="fa-solid fa-plus"></i>局部地图</button>
|
<button class="btn btn-s fc g4" id="btn-gen-local-map"><i class="fa-solid fa-plus"></i>局部地图</button>
|
||||||
<button class="btn btn-s fc g4" id="btn-simulate"><i class="fa-solid fa-rotate"></i>推演</button>
|
<button class="btn btn-s fc g4" id="btn-simulate"><i class="fa-solid fa-rotate"></i>世界推演</button>
|
||||||
<button class="btn btn-s fc g4" id="btn-gen-local-scene"><i class="fa-solid fa-feather-pointed"></i>局部剧情</button>
|
<button class="btn btn-s fc g4" id="btn-gen-local-scene"><i class="fa-solid fa-feather-pointed"></i>局部剧情</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -370,7 +372,7 @@
|
|||||||
<div class="comm-tab act" data-t="stranger">陌路人</div>
|
<div class="comm-tab act" data-t="stranger">陌路人</div>
|
||||||
<div class="comm-tab" data-t="contact">联络人</div>
|
<div class="comm-tab" data-t="contact">联络人</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-add fcc" id="btn-refresh-strangers" title="摇一摇"><i class="fa-solid fa-rotate"></i></button>
|
<button class="btn btn-add fcc" id="btn-refresh-strangers" title="微信摇一摇,寻找遇过的陌生人"><i class="fa-solid fa-street-view"></i></button>
|
||||||
<button class="btn btn-add fcc" id="btn-add-ct"><i class="fa-solid fa-plus"></i></button>
|
<button class="btn btn-add fcc" id="btn-add-ct"><i class="fa-solid fa-plus"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="sec-stranger" class="comm-sec act"></div>
|
<div id="sec-stranger" class="comm-sec act"></div>
|
||||||
@@ -435,8 +437,7 @@
|
|||||||
<div class="form-g" style="flex:1"><label class="form-l">偏离分数</label><input type="number" class="form-in" id="set-deviation" min="0" max="100" value="0"><div class="set-hint">玩家行为对世界的影响</div></div>
|
<div class="form-g" style="flex:1"><label class="form-l">偏离分数</label><input type="number" class="form-in" id="set-deviation" min="0" max="100" value="0"><div class="set-hint">玩家行为对世界的影响</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="set-row" style="gap:20px">
|
<div class="set-row" style="gap:20px">
|
||||||
<div class="form-g" style="flex:1"><label class="form-l">推演进度</label><input type="number" class="form-in" id="set-sim-progress" min="0" value="0"><div class="set-hint">当前累计推演点数</div></div>
|
<div class="form-g" style="flex:1"><label class="form-l">推演倒计时</label><input type="number" class="form-in" id="set-sim-target" value="5"><div class="set-hint">局部地图/场景切换/局部剧情每次 -1;≤0 时提醒</div></div>
|
||||||
<div class="form-g" style="flex:1"><label class="form-l">推演目标</label><input type="number" class="form-in" id="set-sim-target" min="1" value="5"><div class="set-hint">达到目标自动推演</div></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="set-sec">
|
<div class="set-sec">
|
||||||
@@ -457,7 +458,7 @@
|
|||||||
<div class="set-row"><input type="text" class="form-in" id="set-model" placeholder="输入模型名称"><button class="btn btn-s" id="btn-fetch-models">获取</button><button class="btn btn-s btn-p" id="btn-test-conn">测试连接</button></div>
|
<div class="set-row"><input type="text" class="form-in" id="set-model" placeholder="输入模型名称"><button class="btn btn-s" id="btn-fetch-models">获取</button><button class="btn btn-s btn-p" id="btn-test-conn">测试连接</button></div>
|
||||||
<select class="form-in" id="set-model-list" style="display:none;margin-top:8px"></select>
|
<select class="form-in" id="set-model-list" style="display:none;margin-top:8px"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="set-test"><label class="form-l">聊天历史楼层数</label><input type="number" class="form-in" id="set-history-count" min="0" max="200" value="50" style="width:100px"><div class="set-test-res" id="test-res"></div></div>
|
<div class="set-test"><label class="form-l">聊天历史楼层数</label><input type="number" class="form-in" id="set-history-count" min="0" max="200" value="50" style="width:100px"><label class="fc g4 fs12 c2 usn" title="启用后使用流式请求"><input type="checkbox" id="set-use-stream">流式</label><div class="set-test-res" id="test-res"></div></div>
|
||||||
<div class="set-sec-t" style="margin-top:16px">NPC 世界书条目</div>
|
<div class="set-sec-t" style="margin-top:16px">NPC 世界书条目</div>
|
||||||
<div class="set-row" style="gap:20px">
|
<div class="set-row" style="gap:20px">
|
||||||
<div class="form-g" style="flex:1"><label class="form-l">插入位置</label>
|
<div class="form-g" style="flex:1"><label class="form-l">插入位置</label>
|
||||||
@@ -466,18 +467,115 @@
|
|||||||
<option value="2">↑AN 作者注释前</option><option value="3">↓AN 作者注释后</option>
|
<option value="2">↑AN 作者注释前</option><option value="3">↓AN 作者注释后</option>
|
||||||
<option value="5">↑EM 增强定义前</option><option value="6">↓EM 增强定义后</option>
|
<option value="5">↑EM 增强定义前</option><option value="6">↓EM 增强定义后</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="set-hint">生成的NPC条目插入位置</div>
|
<div class="set-hint">陌路人-生成的NPC条目插入位置</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-g" style="flex:1"><label class="form-l">条目顺序</label><input type="number" class="form-in" id="set-npc-order" min="0" max="1000" value="100"><div class="set-hint">数值越小越靠前</div></div>
|
<div class="form-g" style="flex:1"><label class="form-l">条目顺序</label><input type="number" class="form-in" id="set-npc-order" min="0" max="1000" value="100"><div class="set-hint">数值越小越靠前</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="set-sec"><div class="set-sec-t">预设 Story Outline 数据</div><div class="set-hint" style="margin-bottom:12px">勾选的条目将写入预设</div><div id="data-list"></div></div>
|
<div class="set-sec"><div class="set-sec-t">预设 Story Outline 数据</div><div class="set-hint" style="margin-bottom:12px">勾选的条目将写入预设</div><div id="data-list"></div></div>
|
||||||
<div class="set-sec"><div class="set-sec-t">高级设置 · 自定义提示词</div><div class="set-hint" style="margin-bottom:12px">UAUA四段 + JSON 模板</div><div id="prompt-list"></div></div>
|
<div class="set-sec">
|
||||||
|
<div class="set-sec-t">提示词/JSON 模板 <button class="btn btn-s" id="btn-adv-prompts"><i class="fa-solid fa-pen"></i> 编辑模板</button></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-ft fc"><button class="btn btn-s m-cancel">取消</button><button class="btn btn-s btn-p" id="set-save">保存</button></div>
|
<div class="modal-ft fc"><button class="btn btn-s m-cancel">取消</button><button class="btn btn-s btn-p" id="set-save">保存</button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 提示词/JSON 模板编辑弹窗 -->
|
||||||
|
<div class="modal" id="m-adv-prompts">
|
||||||
|
<div class="modal-bd"></div>
|
||||||
|
<div class="modal-p lg">
|
||||||
|
<div class="modal-hd fc"><h2>高级设置 · 模板编辑</h2><button class="modal-x fcc">✕</button></div>
|
||||||
|
<div class="modal-by">
|
||||||
|
<div class="form-g">
|
||||||
|
<label class="form-l">选择模板</label>
|
||||||
|
<select class="form-in" id="adv-key"></select>
|
||||||
|
<div class="set-hint">可编辑并保存;也可一键重置回 `story-outline-prompt.js` 预设</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-sec-t" style="margin-top:12px">UAUA 提示词(JS function 字符串)</div>
|
||||||
|
<div class="form-g"><label class="form-l">u1</label><textarea class="ed-ta" id="adv-u1" style="height:85px;"></textarea></div>
|
||||||
|
<div class="form-g"><label class="form-l">a1</label><textarea class="ed-ta" id="adv-a1" style="height:85px;"></textarea></div>
|
||||||
|
<div class="form-g"><label class="form-l">u2</label><textarea class="ed-ta" id="adv-u2" style="height:85px;"></textarea></div>
|
||||||
|
<div class="form-g"><label class="form-l">a2</label><textarea class="ed-ta" id="adv-a2" style="height:85px;"></textarea></div>
|
||||||
|
|
||||||
|
<div class="set-sec-t" style="margin-top:12px">JSON 模板</div>
|
||||||
|
<div class="form-g" id="adv-json-wrap">
|
||||||
|
<label class="form-l">模板字符串</label>
|
||||||
|
<textarea class="ed-ta" id="adv-json" style="min-height:140px"></textarea>
|
||||||
|
<div class="set-hint" id="adv-json-hint"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg3 bd r6 p12" style="margin-top:12px">
|
||||||
|
<div class="fs12 fw6 c2">变量说明(写进模板里会被替换)</div>
|
||||||
|
|
||||||
|
<div class="set-hint" style="margin-top:8px;line-height:1.65">
|
||||||
|
<div class="fw6 c2" style="margin-bottom:4px">ST 宏(发送前自动替换)</div>
|
||||||
|
<div><code>{{user}}</code>:你的名字/称呼</div>
|
||||||
|
<div><code>{{persona}}</code>:你的 Persona(用户设定)</div>
|
||||||
|
<div><code>{{description}}</code>:当前角色描述(角色卡 description)</div>
|
||||||
|
<div><code>{$worldInfo}</code>:世界书(World Info)注入内容</div>
|
||||||
|
<div><code>{$historyN}</code>:最近 N 条聊天历史(例:<code>{$history50}</code>)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-hint" style="margin-top:10px;line-height:1.65">
|
||||||
|
<div class="fw6 c2" style="margin-bottom:4px">模板参数(在函数里用 <code>${v.xxx}</code> 取值)</div>
|
||||||
|
<div class="c2">通用</div>
|
||||||
|
<div><code>v.storyOutline</code>:剧情大纲文本</div>
|
||||||
|
<div><code>v.historyCount</code>:历史条数(配合 <code>{$historyN}</code>)</div>
|
||||||
|
<div><code>v.mode</code>:模式(assist/story)</div>
|
||||||
|
|
||||||
|
<div class="c2" style="margin-top:6px">短信/邀请</div>
|
||||||
|
<div><code>v.contactName</code>:联系人/NPC 名字</div>
|
||||||
|
<div><code>v.userName</code>:用户名字(部分模板可能用到)</div>
|
||||||
|
<div><code>v.smsHistoryContent</code>:已整理的短信历史块</div>
|
||||||
|
<div><code>v.userMessage</code>:用户发来的新短信文本</div>
|
||||||
|
<div><code>v.characterContent</code>:人物设定文本(可选)</div>
|
||||||
|
<div><code>v.targetLocation</code>:邀请要去的地点</div>
|
||||||
|
|
||||||
|
<div class="c2" style="margin-top:6px">总结/压缩</div>
|
||||||
|
<div><code>v.existingSummaryContent</code>:已有摘要内容</div>
|
||||||
|
<div><code>v.conversationText</code>:需要总结的对话文本</div>
|
||||||
|
|
||||||
|
<div class="c2" style="margin-top:6px">NPC/陌路人提取</div>
|
||||||
|
<div><code>v.strangerName</code>:新 NPC 名字</div>
|
||||||
|
<div><code>v.strangerInfo</code>:新 NPC 描述</div>
|
||||||
|
<div><code>v.existingContacts</code>:已有联络人列表(避免重复)</div>
|
||||||
|
<div><code>v.existingStrangers</code>:已有陌路人列表(避免重复)</div>
|
||||||
|
|
||||||
|
<div class="c2" style="margin-top:6px">场景切换/推进</div>
|
||||||
|
<div><code>v.prevLocationName</code>:上一地点名</div>
|
||||||
|
<div><code>v.prevLocationInfo</code>:上一地点描述(可选)</div>
|
||||||
|
<div><code>v.targetLocationName</code>:目标地点名</div>
|
||||||
|
<div><code>v.targetLocationType</code>:目标地点类型(home/sub 等)</div>
|
||||||
|
<div><code>v.targetLocationInfo</code>:目标地点描述(可选)</div>
|
||||||
|
<div><code>v.playerAction</code>:玩家行动/意图(可选)</div>
|
||||||
|
<div><code>v.stage</code>:当前阶段/轮次</div>
|
||||||
|
<div><code>v.currentTimeline</code>:当前时间线对象(可选)</div>
|
||||||
|
|
||||||
|
<div class="c2" style="margin-top:6px">局部地图/局部剧情</div>
|
||||||
|
<div><code>v.outdoorDescription</code>:大地图/户外地图描述(可选)</div>
|
||||||
|
<div><code>v.locationName</code>:当前地点名</div>
|
||||||
|
<div><code>v.locationInfo</code>:当前地点信息(可选)</div>
|
||||||
|
<div><code>v.playerLocation</code>:玩家当前地点名(可选)</div>
|
||||||
|
<div><code>v.currentLocalMap</code>:当前局部地图 JSON(可选)</div>
|
||||||
|
|
||||||
|
<div class="c2" style="margin-top:6px">世界生成/推演</div>
|
||||||
|
<div><code>v.playerRequests</code>:世界生成需求文本</div>
|
||||||
|
<div><code>v.step1Data</code>:世界生成 Step 1 数据(meta)</div>
|
||||||
|
<div><code>v.currentWorldData</code>:当前世界状态 JSON(字符串)</div>
|
||||||
|
<div><code>v.deviationScore</code>:干扰评分/偏差值</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-ft fc">
|
||||||
|
<button class="btn btn-s" id="adv-reset"><i class="fa-solid fa-rotate-left"></i> 重置为预设</button>
|
||||||
|
<button class="btn btn-s m-cancel">取消</button>
|
||||||
|
<button class="btn btn-s btn-p" id="adv-save">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 数据编辑弹窗 -->
|
<!-- 数据编辑弹窗 -->
|
||||||
<div class="modal" id="m-data-edit">
|
<div class="modal" id="m-data-edit">
|
||||||
<div class="modal-bd"></div>
|
<div class="modal-bd"></div>
|
||||||
@@ -585,7 +683,7 @@
|
|||||||
<script>
|
<script>
|
||||||
// ================== 数据 ==================
|
// ================== 数据 ==================
|
||||||
const D = {
|
const D = {
|
||||||
stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: 5,
|
stage: 0, deviationScore: 0, simulationTarget: 5,
|
||||||
meta: { truth: null, onion_layers: null, timeline: null, user_guide: null },
|
meta: { truth: null, onion_layers: null, timeline: null, user_guide: null },
|
||||||
world: {}, maps: { outdoor: { nodes: [] }, indoor: null }, sceneSetup: null,
|
world: {}, maps: { outdoor: { nodes: [] }, indoor: null }, sceneSetup: null,
|
||||||
contacts: { strangers: [], contacts: [{ name: '{{characterName}}', avatar: '', color: '#555', location: '在线', info: '角色卡联络人', online: true, worldbookUid: '__CHARACTER_CARD__', messages: [], summarizedCount: 0 }] }
|
contacts: { strangers: [], contacts: [{ name: '{{characterName}}', avatar: '', color: '#555', location: '在线', info: '角色卡联络人', online: true, worldbookUid: '__CHARACTER_CARD__', messages: [], summarizedCount: 0 }] }
|
||||||
@@ -602,6 +700,12 @@ const stripXml = s => s ? s.replace(/<(\w+)[^>]*>[\s\S]*?<\/\1>/g, '').replace(/
|
|||||||
const parseLinks = t => t.replace(/\*\*([^*]+)\*\*/g, '<span class="loc-lk" data-loc="$1">$1</span>');
|
const parseLinks = t => t.replace(/\*\*([^*]+)\*\*/g, '<span class="loc-lk" data-loc="$1">$1</span>');
|
||||||
const post = (type, data = {}) => parent.postMessage({ source: 'LittleWhiteBox-OutlineFrame', type, ...data }, '*');
|
const post = (type, data = {}) => parent.postMessage({ source: 'LittleWhiteBox-OutlineFrame', type, ...data }, '*');
|
||||||
|
|
||||||
|
const syncSimDueUI = () => {
|
||||||
|
const due = (Number(D.simulationTarget) || 0) <= 0;
|
||||||
|
$('btn-simulate')?.classList.toggle('btn-due', due);
|
||||||
|
$('world-sim-ok')?.classList.toggle('btn-due', due);
|
||||||
|
};
|
||||||
|
|
||||||
const BtnState = {
|
const BtnState = {
|
||||||
load: (btn, t) => { btn.disabled = true; btn._o = btn.innerHTML; btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> ${t}` },
|
load: (btn, t) => { btn.disabled = true; btn._o = btn.innerHTML; btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> ${t}` },
|
||||||
reset: (btn, t) => { btn.disabled = false; btn.innerHTML = t || btn._o }
|
reset: (btn, t) => { btn.disabled = false; btn.innerHTML = t || btn._o }
|
||||||
@@ -966,8 +1070,7 @@ const saveAll = () => post('SAVE_ALL_DATA', { allData: { meta: D.meta, world: D.
|
|||||||
|
|
||||||
// ================== 设置相关 ==================
|
// ================== 设置相关 ==================
|
||||||
const dataKeys = [['meta', '大纲', '核心真相、洋葱结构、时间线、用户指南', () => D.meta, v => D.meta = v], ['world', '世界资讯', '世界新闻等信息', () => D.world, v => D.world = v], ['outdoor', '大地图', '室外区域的地点和路线', () => D.maps.outdoor, v => D.maps.outdoor = v], ['indoor', '局部地图', '隐藏的室内/局部场景地图', () => D.maps.indoor, v => D.maps.indoor = v], ['sceneSetup', '区域剧情', '当前区域的 Side Story', () => D.sceneSetup, v => D.sceneSetup = v], ['characterContactSms', '角色卡短信', '角色卡联络人的短信记录', () => ({ messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, summaries: charSmsHistory?.summaries || {} }), v => { if (v && typeof v === 'object') charSmsHistory = { messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, ...(v || {}) }; }], ['strangers', '陌路人', '已遇见但未建立联系的角色', () => D.contacts.strangers, v => D.contacts.strangers = v], ['contacts', '联络人', '已添加的联系人', () => contactsForSave(), v => { const keep = (D.contacts.contacts || []).find(isCharCardContact); D.contacts.contacts = (keep ? [keep] : []).concat(Array.isArray(v) ? v : []); }]];
|
const dataKeys = [['meta', '大纲', '核心真相、洋葱结构、时间线、用户指南', () => D.meta, v => D.meta = v], ['world', '世界资讯', '世界新闻等信息', () => D.world, v => D.world = v], ['outdoor', '大地图', '室外区域的地点和路线', () => D.maps.outdoor, v => D.maps.outdoor = v], ['indoor', '局部地图', '隐藏的室内/局部场景地图', () => D.maps.indoor, v => D.maps.indoor = v], ['sceneSetup', '区域剧情', '当前区域的 Side Story', () => D.sceneSetup, v => D.sceneSetup = v], ['characterContactSms', '角色卡短信', '角色卡联络人的短信记录', () => ({ messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, summaries: charSmsHistory?.summaries || {} }), v => { if (v && typeof v === 'object') charSmsHistory = { messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, ...(v || {}) }; }], ['strangers', '陌路人', '已遇见但未建立联系的角色', () => D.contacts.strangers, v => D.contacts.strangers = v], ['contacts', '联络人', '已添加的联系人', () => contactsForSave(), v => { const keep = (D.contacts.contacts || []).find(isCharCardContact); D.contacts.contacts = (keep ? [keep] : []).concat(Array.isArray(v) ? v : []); }]];
|
||||||
const promptKeys = [['jsonTemplates', 'JSON 模板', 'JSON 输出模板合集', 'templates'], ['sms', '短信回复', 'UAUA 短信模拟', 'prompt'], ['summary', '总结压缩', '新增剧情要素提取', 'prompt'], ['invite', '邀请回复', '短信邀请场景', 'prompt'], ['npc', 'NPC 生成', '陌路人扩写为 NPC', 'prompt'], ['stranger', '提取陌路人', '从剧情中提取 NPC', 'prompt'], ['worldGen', '世界生成(故事模式)', '初始世界构建', 'prompt'], ['worldSim', '世界推演(故事模式)', '根据历史演化世界', 'prompt'], ['sceneSwitch', '场景切换(故事模式)', '结算上一地点 + 新场景', 'prompt'], ['worldGenAssist', '世界生成(辅助模式)', '仅生成地图/新闻', 'prompt'], ['worldSimAssist', '世界推演(辅助模式)', '仅更新地图/新闻', 'prompt'], ['sceneSwitchAssist', '场景切换(辅助模式)', '生成轻松小剧情', 'prompt'], ['localMapGen', '局部地图生成', '生成室内/局部场景', 'prompt']];
|
let gSet = { apiUrl: '', apiKey: '', model: '', mode: 'assist' }, dataCk = {}, editCtx = null, commSet = { historyCount: 50, npcPosition: 0, npcOrder: 100, stream: false }, promptSources = {}, promptTemplates = {}, promptDefaults = { jsonTemplates: {}, promptSources: {} };
|
||||||
let gSet = { apiUrl: '', apiKey: '', model: '', mode: 'assist' }, dataCk = {}, editCtx = null, commSet = { historyCount: 50, npcPosition: 0, npcOrder: 100 }, promptSources = {}, promptTemplates = {}, promptDefaults = { jsonTemplates: {}, promptSources: {} };
|
|
||||||
|
|
||||||
const reqSet = () => post('GET_SETTINGS');
|
const reqSet = () => post('GET_SETTINGS');
|
||||||
|
|
||||||
@@ -977,13 +1080,171 @@ const renderDataList = () => {
|
|||||||
$$('#data-list .data-edit').forEach(b => b.onclick = e => { e.stopPropagation(); openDataEdit(b.dataset.k); });
|
$$('#data-list .data-edit').forEach(b => b.onclick = e => { e.stopPropagation(); openDataEdit(b.dataset.k); });
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPromptList = () => {
|
// ================== 高级设置:提示词/JSON 模板 ==================
|
||||||
$('prompt-list').innerHTML = promptKeys.map(([k, t, d, tp]) => `<div class="data-item" data-k="${k}" data-t="${tp}"><div class="data-ck"><i class="fa-solid fa-pen"></i></div><div class="data-info"><div class="data-nm">${t}</div><div class="data-desc">${d}</div></div><button class="data-edit" data-k="${k}" data-t="${tp}" title="编辑"><i class="fa-solid fa-pen"></i></button></div>`).join('');
|
const ADV_PROMPT_ITEMS = [
|
||||||
$$('#prompt-list .data-item').forEach(i => i.onclick = e => { const k = i.dataset.k, tp = i.dataset.t; if (e.target.closest('.data-edit')) { e.stopPropagation(); openPromptEdit(k, tp); return; } openPromptEdit(k, tp); });
|
['sms', '短信回复'],
|
||||||
$$('#prompt-list .data-edit').forEach(b => b.onclick = e => { e.stopPropagation(); openPromptEdit(b.dataset.k, b.dataset.t); });
|
['invite', '邀请回复'],
|
||||||
|
['npc', 'NPC 生成'],
|
||||||
|
['stranger', '提取陌路人'],
|
||||||
|
['worldGenStep1', '世界生成 Step 1(故事模式)'],
|
||||||
|
['worldGenStep2', '世界生成 Step 2(故事模式)'],
|
||||||
|
['worldSim', '世界推演(故事模式)'],
|
||||||
|
['sceneSwitch', '场景切换(故事模式)'],
|
||||||
|
['worldGenAssist', '世界生成(辅助模式)'],
|
||||||
|
['worldSimAssist', '世界推演(辅助模式)'],
|
||||||
|
['sceneSwitchAssist', '场景切换(辅助模式)'],
|
||||||
|
['localMapGen', '局部地图生成'],
|
||||||
|
['localMapRefresh', '局部地图刷新'],
|
||||||
|
['localSceneGen', '局部剧情生成'],
|
||||||
|
['summary', '总结压缩(无独立 JSON 模板)'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const advHasJsonTemplate = (key) => {
|
||||||
|
const defs = promptDefaults?.jsonTemplates || {};
|
||||||
|
return Object.prototype.hasOwnProperty.call(defs, key);
|
||||||
};
|
};
|
||||||
|
|
||||||
const unescapePromptStr = s => String(s || '').replace(/\\\\/g, '\\').replace(/\\t/g, ' ').replace(/\\n/g, '\n');
|
const advGetPromptObj = (key, useDefaults = false) => {
|
||||||
|
const defs = promptDefaults?.promptSources || {};
|
||||||
|
const cur = promptSources || {};
|
||||||
|
const v = (useDefaults ? defs[key] : (cur[key] || defs[key])) || {};
|
||||||
|
return {
|
||||||
|
u1: typeof v.u1 === 'string' ? v.u1 : '',
|
||||||
|
a1: typeof v.a1 === 'string' ? v.a1 : '',
|
||||||
|
u2: typeof v.u2 === 'string' ? v.u2 : '',
|
||||||
|
a2: typeof v.a2 === 'string' ? v.a2 : '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const advSplitFn = (s) => {
|
||||||
|
const str = String(s ?? '');
|
||||||
|
const first = str.indexOf('`');
|
||||||
|
const last = str.lastIndexOf('`');
|
||||||
|
if (first === -1 || last === -1 || last <= first) return null;
|
||||||
|
return { prefix: str.slice(0, first), body: str.slice(first + 1, last), suffix: str.slice(last + 1) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const advDecodeTemplateEscapes = (body) => {
|
||||||
|
const s = String(body ?? '');
|
||||||
|
let out = '';
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
const c = s[i];
|
||||||
|
if (c !== '\\') { out += c; continue; }
|
||||||
|
const n = s[i + 1];
|
||||||
|
if (n === undefined) { out += '\\'; continue; }
|
||||||
|
if (n === 'n') { out += '\n'; i++; continue; }
|
||||||
|
if (n === 't') { out += ' '; i++; continue; }
|
||||||
|
if (n === '`') { out += '`'; i++; continue; }
|
||||||
|
if (n === '\\') { out += '\\'; i++; continue; }
|
||||||
|
out += '\\' + n;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const advEncodeTemplateEscapes = (pretty) => {
|
||||||
|
const s = String(pretty ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
let out = '';
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
const c = s[i];
|
||||||
|
if (c === '\n') out += '\\n';
|
||||||
|
else if (c === '\t') out += '\\t';
|
||||||
|
else if (c === '`') out += '\\`';
|
||||||
|
else out += c;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const advGetJsonTemplate = (key, useDefaults = false) => {
|
||||||
|
const defs = promptDefaults?.jsonTemplates || {};
|
||||||
|
const cur = promptTemplates || {};
|
||||||
|
if (!advHasJsonTemplate(key) && !Object.prototype.hasOwnProperty.call(cur, key)) return '';
|
||||||
|
const v = useDefaults ? defs[key] : (Object.prototype.hasOwnProperty.call(cur, key) ? cur[key] : defs[key]);
|
||||||
|
return typeof v === 'string' ? v : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const advApplyToUI = (key, useDefaults = false) => {
|
||||||
|
const p = advGetPromptObj(key, useDefaults);
|
||||||
|
const applyPart = (id, raw) => {
|
||||||
|
const el = $(id);
|
||||||
|
if (!el) return;
|
||||||
|
const rawStr = String(raw ?? '');
|
||||||
|
const split = advSplitFn(rawStr);
|
||||||
|
const pretty = split ? advDecodeTemplateEscapes(split.body) : rawStr;
|
||||||
|
el._meta = { raw: rawStr, split, pretty };
|
||||||
|
el.value = pretty;
|
||||||
|
};
|
||||||
|
applyPart('adv-u1', p.u1);
|
||||||
|
applyPart('adv-a1', p.a1);
|
||||||
|
applyPart('adv-u2', p.u2);
|
||||||
|
applyPart('adv-a2', p.a2);
|
||||||
|
|
||||||
|
const hasJson = advHasJsonTemplate(key) || Object.prototype.hasOwnProperty.call(promptTemplates || {}, key);
|
||||||
|
$('adv-json-wrap').style.display = hasJson ? '' : 'none';
|
||||||
|
const jRaw = hasJson ? advGetJsonTemplate(key, useDefaults) : '';
|
||||||
|
const jEl = $('adv-json');
|
||||||
|
if (jEl) jEl.value = hasJson ? String(jRaw ?? '') : '';
|
||||||
|
$('adv-json-hint').textContent = hasJson ? '' : '该模板没有独立 JSON 模板。';
|
||||||
|
};
|
||||||
|
|
||||||
|
const advCommitEdits = (key) => {
|
||||||
|
const build = (id) => {
|
||||||
|
const el = $(id);
|
||||||
|
const meta = el?._meta;
|
||||||
|
const prettyNow = String(el?.value ?? '');
|
||||||
|
if (meta && prettyNow === meta.pretty) return meta.raw;
|
||||||
|
if (meta?.split) return `${meta.split.prefix}\`${advEncodeTemplateEscapes(prettyNow)}\`${meta.split.suffix}`;
|
||||||
|
return meta?.raw ?? prettyNow;
|
||||||
|
};
|
||||||
|
|
||||||
|
promptSources[key] = {
|
||||||
|
u1: build('adv-u1'),
|
||||||
|
a1: build('adv-a1'),
|
||||||
|
u2: build('adv-u2'),
|
||||||
|
a2: build('adv-a2'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (advHasJsonTemplate(key) || Object.prototype.hasOwnProperty.call(promptTemplates || {}, key)) {
|
||||||
|
const jEl = $('adv-json');
|
||||||
|
const jt = String(jEl?.value ?? '');
|
||||||
|
promptTemplates[key] = jt;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const advInit = () => {
|
||||||
|
const sel = $('adv-key');
|
||||||
|
if (!sel || sel._inited) return;
|
||||||
|
sel._inited = true;
|
||||||
|
sel.innerHTML = ADV_PROMPT_ITEMS.map(([k, t]) => `<option value="${k}">${t} (${k})</option>`).join('');
|
||||||
|
sel.onchange = () => advApplyToUI(sel.value, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const advOpen = () => {
|
||||||
|
advInit();
|
||||||
|
const sel = $('adv-key');
|
||||||
|
const key = sel?.value || ADV_PROMPT_ITEMS[0]?.[0];
|
||||||
|
if (key) advApplyToUI(key, false);
|
||||||
|
openM('m-adv-prompts');
|
||||||
|
};
|
||||||
|
|
||||||
|
const advSave = () => {
|
||||||
|
advInit();
|
||||||
|
const key = $('adv-key')?.value;
|
||||||
|
if (!key) return;
|
||||||
|
advCommitEdits(key);
|
||||||
|
post('SAVE_PROMPTS', { promptConfig: { jsonTemplates: promptTemplates, promptSources } });
|
||||||
|
closeM('m-adv-prompts');
|
||||||
|
};
|
||||||
|
|
||||||
|
const advReset = () => {
|
||||||
|
advInit();
|
||||||
|
const key = $('adv-key')?.value;
|
||||||
|
if (!key) return;
|
||||||
|
advApplyToUI(key, true);
|
||||||
|
advCommitEdits(key);
|
||||||
|
post('SAVE_PROMPTS', { promptConfig: { jsonTemplates: promptTemplates, promptSources } });
|
||||||
|
closeM('m-adv-prompts');
|
||||||
|
};
|
||||||
const parseJsonLoose = (input) => {
|
const parseJsonLoose = (input) => {
|
||||||
const str = String(input ?? '').trim();
|
const str = String(input ?? '').trim();
|
||||||
if (!str) throw new Error('空内容');
|
if (!str) throw new Error('空内容');
|
||||||
@@ -1009,20 +1270,13 @@ const parseJsonLoose = (input) => {
|
|||||||
};
|
};
|
||||||
const updateEditPreview = () => {
|
const updateEditPreview = () => {
|
||||||
const p = $('data-edit-preview');
|
const p = $('data-edit-preview');
|
||||||
if (!p || editCtx?.type !== 'prompt') { p.style.display = 'none'; p.textContent = ''; return; }
|
if (!p) return;
|
||||||
p.style.display = 'block';
|
p.style.display = 'none';
|
||||||
const raw = $('data-edit-ta').value || '';
|
p.textContent = '';
|
||||||
let txt = raw;
|
|
||||||
try {
|
|
||||||
const obj = parseJsonLoose(raw);
|
|
||||||
if (obj && typeof obj === 'object') txt = Object.entries(obj).map(([k, v]) => `${k}: ${typeof v === 'string' ? unescapePromptStr(v) : typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v)}`).join('\n\n');
|
|
||||||
} catch { txt = unescapePromptStr(raw); }
|
|
||||||
p.textContent = txt;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setEditContent = (title, val) => { $('data-edit-title').textContent = title; $('data-edit-ta').value = val; $('data-edit-err').classList.remove('vis'); updateEditPreview(); openM('m-data-edit'); };
|
const setEditContent = (title, val) => { $('data-edit-title').textContent = title; $('data-edit-ta').value = val; $('data-edit-err').classList.remove('vis'); updateEditPreview(); openM('m-data-edit'); };
|
||||||
const openDataEdit = k => { const i = dataKeys.find(([x]) => x === k); if (!i) return; editCtx = { type: k === 'characterContactSms' ? 'charSms' : 'data', key: k }; setEditContent(`编辑 - ${i[1]}`, JSON.stringify(i[3](), null, 2)); };
|
const openDataEdit = k => { const i = dataKeys.find(([x]) => x === k); if (!i) return; editCtx = { type: k === 'characterContactSms' ? 'charSms' : 'data', key: k }; setEditContent(`编辑 - ${i[1]}`, JSON.stringify(i[3](), null, 2)); };
|
||||||
const openPromptEdit = (k, tp) => { const i = promptKeys.find(([x]) => x === k); editCtx = { type: 'prompt', key: k }; const val = tp === 'templates' ? (promptTemplates || promptDefaults.jsonTemplates || {}) : (promptSources[k] || promptDefaults.promptSources[k] || { u1: '', a1: '', u2: '', a2: '' }); setEditContent(`编辑 - ${i?.[1] || k}`, JSON.stringify(val, null, 2)); };
|
|
||||||
|
|
||||||
$('data-edit-save').onclick = () => {
|
$('data-edit-save').onclick = () => {
|
||||||
if (!editCtx) return;
|
if (!editCtx) return;
|
||||||
@@ -1039,18 +1293,6 @@ $('data-edit-save').onclick = () => {
|
|||||||
if (!sums || typeof sums !== 'object' || Array.isArray(sums)) throw new Error('需要 summaries 对象');
|
if (!sums || typeof sums !== 'object' || Array.isArray(sums)) throw new Error('需要 summaries 对象');
|
||||||
charSmsHistory.summaries = sums;
|
charSmsHistory.summaries = sums;
|
||||||
post('SAVE_CHAR_SMS_HISTORY', { summaries: sums });
|
post('SAVE_CHAR_SMS_HISTORY', { summaries: sums });
|
||||||
} else if (editCtx.type === 'prompt') {
|
|
||||||
if (editCtx.key === 'jsonTemplates') {
|
|
||||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('JSON 模板需要是对象');
|
|
||||||
promptTemplates = parsed;
|
|
||||||
} else {
|
|
||||||
if (!parsed || typeof parsed !== 'object') throw new Error('需要包含 u1/a1/u2/a2 字符串');
|
|
||||||
const miss = ['u1', 'a1', 'u2', 'a2'].some(k => typeof parsed?.[k] !== 'string');
|
|
||||||
if (miss) throw new Error('需要包含 u1/a1/u2/a2 字符串');
|
|
||||||
promptSources[editCtx.key] = parsed;
|
|
||||||
}
|
|
||||||
renderPromptList();
|
|
||||||
post('SAVE_PROMPTS', { promptConfig: { jsonTemplates: promptTemplates, promptSources } });
|
|
||||||
}
|
}
|
||||||
closeM('m-data-edit');
|
closeM('m-data-edit');
|
||||||
editCtx = null;
|
editCtx = null;
|
||||||
@@ -1082,17 +1324,21 @@ $('btn-settings').onclick = () => {
|
|||||||
$('test-res').className = 'set-test-res';
|
$('test-res').className = 'set-test-res';
|
||||||
$('set-stage').value = D.stage || 0;
|
$('set-stage').value = D.stage || 0;
|
||||||
$('set-deviation').value = D.deviationScore || 0;
|
$('set-deviation').value = D.deviationScore || 0;
|
||||||
$('set-sim-progress').value = D.simulationProgress || 0;
|
$('set-sim-target').value = (D.simulationTarget ?? 5);
|
||||||
$('set-sim-target').value = D.simulationTarget || 5;
|
|
||||||
$('set-mode').value = gSet.mode || 'story';
|
$('set-mode').value = gSet.mode || 'story';
|
||||||
$('set-history-count').value = commSet.historyCount || 50;
|
$('set-history-count').value = commSet.historyCount || 50;
|
||||||
|
$('set-use-stream').checked = !!commSet.stream;
|
||||||
$('set-npc-position').value = commSet.npcPosition || 0;
|
$('set-npc-position').value = commSet.npcPosition || 0;
|
||||||
$('set-npc-order').value = commSet.npcOrder || 100;
|
$('set-npc-order').value = commSet.npcOrder || 100;
|
||||||
renderDataList();
|
renderDataList();
|
||||||
renderPromptList();
|
syncSimDueUI();
|
||||||
openM('m-settings');
|
openM('m-settings');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$('btn-adv-prompts').onclick = () => advOpen();
|
||||||
|
$('adv-save').onclick = () => advSave();
|
||||||
|
$('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-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() }); };
|
$('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() }); };
|
||||||
|
|
||||||
@@ -1100,12 +1346,13 @@ $('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 || 'assist' };
|
gSet = { apiUrl: $('set-api-url').value.trim(), apiKey: $('set-api-key').value.trim(), model: $('set-model').value.trim(), mode: $('set-mode').value || 'assist' };
|
||||||
D.stage = Math.max(0, Math.min(10, parseInt($('set-stage').value, 10) || 0));
|
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.deviationScore = Math.max(0, Math.min(100, parseInt($('set-deviation').value, 10) || 0));
|
||||||
D.simulationProgress = Math.max(0, parseInt($('set-sim-progress').value, 10) || 0);
|
D.simulationTarget = parseInt($('set-sim-target').value, 10);
|
||||||
D.simulationTarget = Math.max(1, parseInt($('set-sim-target').value, 10) || 5);
|
if (Number.isNaN(D.simulationTarget)) D.simulationTarget = 5;
|
||||||
commSet = { historyCount: Math.max(0, Math.min(200, parseInt($('set-history-count').value, 10) || 50)), npcPosition: parseInt($('set-npc-position').value, 10) || 0, npcOrder: Math.max(0, Math.min(1000, parseInt($('set-npc-order').value, 10) || 100)) };
|
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(1000, parseInt($('set-npc-order').value, 10) || 100)) };
|
||||||
const od = {};
|
const od = {};
|
||||||
dataKeys.forEach(([k, , , get]) => { if (dataCk[k]) od[k] = get(); });
|
dataKeys.forEach(([k, , , get]) => { if (dataCk[k]) od[k] = get(); });
|
||||||
post('SAVE_SETTINGS', { globalSettings: gSet, commSettings: commSet, stage: D.stage, deviationScore: D.deviationScore, simulationProgress: D.simulationProgress, simulationTarget: D.simulationTarget, playerLocation, dataChecked: dataCk, outlineData: od, allData: { meta: D.meta, timeline: D.timeline, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, strangers: D.contacts.strangers, contacts: contactsForSave() } });
|
syncSimDueUI();
|
||||||
|
post('SAVE_SETTINGS', { globalSettings: gSet, commSettings: commSet, stage: D.stage, deviationScore: D.deviationScore, simulationTarget: D.simulationTarget, playerLocation, dataChecked: dataCk, outlineData: od, 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');
|
closeM('m-settings');
|
||||||
};
|
};
|
||||||
$('btn-close').onclick = () => post('CLOSE_PANEL');
|
$('btn-close').onclick = () => post('CLOSE_PANEL');
|
||||||
@@ -1119,10 +1366,9 @@ window.addEventListener('message', e => {
|
|||||||
if (d.globalSettings) gSet = d.globalSettings;
|
if (d.globalSettings) gSet = d.globalSettings;
|
||||||
if (d.stage !== undefined) D.stage = d.stage;
|
if (d.stage !== undefined) D.stage = d.stage;
|
||||||
if (d.deviationScore !== undefined) D.deviationScore = d.deviationScore;
|
if (d.deviationScore !== undefined) D.deviationScore = d.deviationScore;
|
||||||
if (d.simulationProgress !== undefined) D.simulationProgress = d.simulationProgress;
|
|
||||||
if (d.simulationTarget !== undefined) D.simulationTarget = d.simulationTarget;
|
if (d.simulationTarget !== undefined) D.simulationTarget = d.simulationTarget;
|
||||||
if (d.playerLocation) playerLocation = d.playerLocation;
|
if (d.playerLocation) playerLocation = d.playerLocation;
|
||||||
if (d.commSettings) commSet = { historyCount: d.commSettings.historyCount ?? 50, npcPosition: d.commSettings.npcPosition ?? 0, npcOrder: d.commSettings.npcOrder ?? 100 };
|
if (d.commSettings) commSet = { historyCount: d.commSettings.historyCount ?? 50, npcPosition: d.commSettings.npcPosition ?? 0, npcOrder: d.commSettings.npcOrder ?? 100, stream: !!d.commSettings.stream };
|
||||||
if (d.dataChecked) dataCk = d.dataChecked;
|
if (d.dataChecked) dataCk = d.dataChecked;
|
||||||
if (d.promptConfig) { promptTemplates = d.promptConfig.current?.jsonTemplates || {}; promptSources = d.promptConfig.current?.promptSources || {}; promptDefaults = d.promptConfig.defaults || promptDefaults; }
|
if (d.promptConfig) { promptTemplates = d.promptConfig.current?.jsonTemplates || {}; promptSources = d.promptConfig.current?.promptSources || {}; promptDefaults = d.promptConfig.defaults || promptDefaults; }
|
||||||
if (d.outlineData) {
|
if (d.outlineData) {
|
||||||
@@ -1159,23 +1405,31 @@ window.addEventListener('message', e => {
|
|||||||
charContact.avatar = (d.characterCardName || '')[0] || charContact.avatar || '';
|
charContact.avatar = (d.characterCardName || '')[0] || charContact.avatar || '';
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
|
syncSimDueUI();
|
||||||
if ($('m-settings').classList.contains('act')) {
|
if ($('m-settings').classList.contains('act')) {
|
||||||
$('set-api-url').value = gSet.apiUrl || '';
|
$('set-api-url').value = gSet.apiUrl || '';
|
||||||
$('set-api-key').value = gSet.apiKey || '';
|
$('set-api-key').value = gSet.apiKey || '';
|
||||||
$('set-model').value = gSet.model || '';
|
$('set-model').value = gSet.model || '';
|
||||||
$('set-stage').value = D.stage;
|
$('set-stage').value = D.stage;
|
||||||
$('set-deviation').value = D.deviationScore;
|
$('set-deviation').value = D.deviationScore;
|
||||||
$('set-sim-progress').value = D.simulationProgress || 0;
|
$('set-sim-target').value = (D.simulationTarget ?? 5);
|
||||||
$('set-sim-target').value = D.simulationTarget || 5;
|
|
||||||
$('set-mode').value = gSet.mode || 'story';
|
$('set-mode').value = gSet.mode || 'story';
|
||||||
$('set-history-count').value = commSet.historyCount;
|
$('set-history-count').value = commSet.historyCount;
|
||||||
|
$('set-use-stream').checked = !!commSet.stream;
|
||||||
$('set-npc-position').value = commSet.npcPosition;
|
$('set-npc-position').value = commSet.npcPosition;
|
||||||
$('set-npc-order').value = commSet.npcOrder;
|
$('set-npc-order').value = commSet.npcOrder;
|
||||||
renderDataList();
|
renderDataList();
|
||||||
renderPromptList();
|
|
||||||
}
|
}
|
||||||
} else if (t === 'PROMPT_CONFIG_UPDATED') {
|
} else if (t === 'PROMPT_CONFIG_UPDATED') {
|
||||||
if (d.promptConfig) { promptTemplates = d.promptConfig.current?.jsonTemplates || {}; promptSources = d.promptConfig.current?.promptSources || {}; promptDefaults = d.promptConfig.defaults || promptDefaults; if ($('m-settings').classList.contains('act')) renderPromptList(); }
|
if (d.promptConfig) {
|
||||||
|
promptTemplates = d.promptConfig.current?.jsonTemplates || {};
|
||||||
|
promptSources = d.promptConfig.current?.promptSources || {};
|
||||||
|
promptDefaults = d.promptConfig.defaults || promptDefaults;
|
||||||
|
if ($('m-adv-prompts').classList.contains('act')) {
|
||||||
|
const key = $('adv-key')?.value;
|
||||||
|
if (key) advApplyToUI(key, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (t === 'FETCH_MODELS_RESULT') {
|
} else if (t === 'FETCH_MODELS_RESULT') {
|
||||||
BtnState.reset($('btn-fetch-models'), '获取');
|
BtnState.reset($('btn-fetch-models'), '获取');
|
||||||
const s = $('set-model-list');
|
const s = $('set-model-list');
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { getContext } from "../../../../../st-context.js";
|
|||||||
import { streamingGeneration } from "../streaming-generation.js";
|
import { streamingGeneration } from "../streaming-generation.js";
|
||||||
import { EXT_ID, extensionFolderPath } from "../../core/constants.js";
|
import { EXT_ID, extensionFolderPath } from "../../core/constants.js";
|
||||||
import { createModuleEvents, event_types } from "../../core/event-manager.js";
|
import { createModuleEvents, event_types } from "../../core/event-manager.js";
|
||||||
|
import { StoryOutlinePromptStorage, StoryOutlineSettingsStorage } from "../../core/server-storage.js";
|
||||||
import { promptManager } from "../../../../../openai.js";
|
import { promptManager } from "../../../../../openai.js";
|
||||||
import {
|
import {
|
||||||
buildSmsMessages, buildSummaryMessages, buildSmsHistoryContent, buildExistingSummaryContent,
|
buildSmsMessages, buildSummaryMessages, buildSmsHistoryContent, buildExistingSummaryContent,
|
||||||
@@ -220,7 +221,7 @@ function getOutlineStore() {
|
|||||||
if (!chat_metadata) return null;
|
if (!chat_metadata) return null;
|
||||||
const ext = chat_metadata.extensions ||= {}, lwb = ext[EXT_ID] ||= {};
|
const ext = chat_metadata.extensions ||= {}, lwb = ext[EXT_ID] ||= {};
|
||||||
return lwb.storyOutline ||= {
|
return lwb.storyOutline ||= {
|
||||||
mapData: null, stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: 5, playerLocation: '家',
|
mapData: null, stage: 0, deviationScore: 0, simulationTarget: 5, playerLocation: '家',
|
||||||
outlineData: { meta: null, world: null, outdoor: null, indoor: null, sceneSetup: null, strangers: null, contacts: null },
|
outlineData: { meta: null, world: null, outdoor: null, indoor: null, sceneSetup: null, strangers: null, contacts: null },
|
||||||
dataChecked: { meta: true, world: true, outdoor: true, indoor: true, sceneSetup: true, strangers: false, contacts: false, characterContactSms: false }
|
dataChecked: { meta: true, world: true, outdoor: true, indoor: true, sceneSetup: true, strangers: false, contacts: false, characterContactSms: false }
|
||||||
};
|
};
|
||||||
@@ -229,7 +230,7 @@ function getOutlineStore() {
|
|||||||
/** 全局/通讯设置读写 */
|
/** 全局/通讯设置读写 */
|
||||||
const getGlobalSettings = () => getStore(STORAGE_KEYS.global, { apiUrl: '', apiKey: '', model: '', mode: 'assist' });
|
const getGlobalSettings = () => getStore(STORAGE_KEYS.global, { apiUrl: '', apiKey: '', model: '', mode: 'assist' });
|
||||||
const saveGlobalSettings = s => setStore(STORAGE_KEYS.global, s);
|
const saveGlobalSettings = s => setStore(STORAGE_KEYS.global, s);
|
||||||
const getCommSettings = () => ({ historyCount: 50, npcPosition: 0, npcOrder: 100, ...getStore(STORAGE_KEYS.comm, {}) });
|
const getCommSettings = () => ({ historyCount: 50, npcPosition: 0, npcOrder: 100, stream: false, ...getStore(STORAGE_KEYS.comm, {}) });
|
||||||
const saveCommSettings = s => setStore(STORAGE_KEYS.comm, s);
|
const saveCommSettings = s => setStore(STORAGE_KEYS.comm, s);
|
||||||
|
|
||||||
/** 获取角色卡信息 */
|
/** 获取角色卡信息 */
|
||||||
@@ -252,10 +253,44 @@ function getCharSmsHistory() {
|
|||||||
|
|
||||||
// ==================== 5. LLM调用 ====================
|
// ==================== 5. LLM调用 ====================
|
||||||
|
|
||||||
|
const STREAM_DONE_EVT = 'xiaobaix_streaming_completed';
|
||||||
|
let streamLlmQueue = Promise.resolve();
|
||||||
|
|
||||||
|
function createStreamingWaiter(sessionId, timeoutMs = 180000) {
|
||||||
|
let done = false;
|
||||||
|
let timer = null;
|
||||||
|
let handler = null;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
try { if (timer) clearTimeout(timer); } catch {}
|
||||||
|
try { eventSource.removeListener?.(STREAM_DONE_EVT, handler); } catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
handler = (payload) => {
|
||||||
|
if (!payload || payload.sessionId !== sessionId) return;
|
||||||
|
cleanup();
|
||||||
|
resolve(String(payload.finalText ?? ''));
|
||||||
|
};
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error('Streaming timeout'));
|
||||||
|
}, timeoutMs);
|
||||||
|
try { eventSource.on?.(STREAM_DONE_EVT, handler); } catch (e) {
|
||||||
|
cleanup();
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { promise, cleanup };
|
||||||
|
}
|
||||||
|
|
||||||
/** 调用LLM */
|
/** 调用LLM */
|
||||||
async function callLLM(promptOrMsgs, useRaw = false) {
|
async function callLLM(promptOrMsgs, useRaw = false) {
|
||||||
const { apiUrl, apiKey, model } = getGlobalSettings();
|
const { apiUrl, apiKey, model } = getGlobalSettings();
|
||||||
|
const useStream = !!getCommSettings()?.stream;
|
||||||
|
|
||||||
const normalize = r => {
|
const normalize = r => {
|
||||||
if (r == null) return '';
|
if (r == null) return '';
|
||||||
@@ -271,17 +306,18 @@ async function callLLM(promptOrMsgs, useRaw = false) {
|
|||||||
return String(r);
|
return String(r);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 构建基础选项
|
const baseOpts = { lock: 'on' };
|
||||||
const opts = { nonstream: 'true', lock: 'on' };
|
if (!useStream) baseOpts.nonstream = 'true';
|
||||||
if (apiUrl?.trim()) Object.assign(opts, { api: 'openai', apiurl: apiUrl.trim(), ...(apiKey && { apipassword: apiKey }), ...(model && { model }) });
|
if (apiUrl?.trim()) Object.assign(baseOpts, { api: 'openai', apiurl: apiUrl.trim(), ...(apiKey && { apipassword: apiKey }), ...(model && { model }) });
|
||||||
|
|
||||||
|
if (!useStream) {
|
||||||
|
const opts = { ...baseOpts };
|
||||||
|
|
||||||
if (useRaw) {
|
if (useRaw) {
|
||||||
const messages = Array.isArray(promptOrMsgs)
|
const messages = Array.isArray(promptOrMsgs)
|
||||||
? promptOrMsgs
|
? promptOrMsgs
|
||||||
: [{ role: 'user', content: String(promptOrMsgs || '').trim() }];
|
: [{ role: 'user', content: String(promptOrMsgs || '').trim() }];
|
||||||
|
|
||||||
// 直接把消息转成 top 参数格式,不做预处理
|
|
||||||
// {$worldInfo} 和 {$historyN} 由 xbgenrawCommand 内部处理
|
|
||||||
const roleMap = { user: 'user', assistant: 'assistant', system: 'sys' };
|
const roleMap = { user: 'user', assistant: 'assistant', system: 'sys' };
|
||||||
const topParts = messages
|
const topParts = messages
|
||||||
.filter(m => m?.role && typeof m.content === 'string' && m.content.trim())
|
.filter(m => m?.role && typeof m.content === 'string' && m.content.trim())
|
||||||
@@ -290,9 +326,7 @@ async function callLLM(promptOrMsgs, useRaw = false) {
|
|||||||
return `${role}={${m.content}}`;
|
return `${role}={${m.content}}`;
|
||||||
});
|
});
|
||||||
const topParam = topParts.join(';');
|
const topParam = topParts.join(';');
|
||||||
|
|
||||||
opts.top = topParam;
|
opts.top = topParam;
|
||||||
// 不设置 addon,让 xbgenrawCommand 自己处理 {$worldInfo} 占位符替换
|
|
||||||
|
|
||||||
const raw = await streamingGeneration.xbgenrawCommand(opts, '');
|
const raw = await streamingGeneration.xbgenrawCommand(opts, '');
|
||||||
const text = normalize(raw).trim();
|
const text = normalize(raw).trim();
|
||||||
@@ -314,6 +348,41 @@ async function callLLM(promptOrMsgs, useRaw = false) {
|
|||||||
return normalize(await streamingGeneration.xbgenCommand(opts, promptOrMsgs)).trim();
|
return normalize(await streamingGeneration.xbgenCommand(opts, promptOrMsgs)).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runStreaming = async () => {
|
||||||
|
const sessionId = 'xb10';
|
||||||
|
const waiter = createStreamingWaiter(sessionId);
|
||||||
|
const opts = { ...baseOpts, id: sessionId };
|
||||||
|
try {
|
||||||
|
if (useRaw) {
|
||||||
|
const messages = Array.isArray(promptOrMsgs)
|
||||||
|
? promptOrMsgs
|
||||||
|
: [{ role: 'user', content: String(promptOrMsgs || '').trim() }];
|
||||||
|
|
||||||
|
const roleMap = { user: 'user', assistant: 'assistant', system: 'sys' };
|
||||||
|
const topParts = messages
|
||||||
|
.filter(m => m?.role && typeof m.content === 'string' && m.content.trim())
|
||||||
|
.map(m => {
|
||||||
|
const role = roleMap[m.role] || m.role;
|
||||||
|
return `${role}={${m.content}}`;
|
||||||
|
});
|
||||||
|
opts.top = topParts.join(';');
|
||||||
|
await streamingGeneration.xbgenrawCommand(opts, '');
|
||||||
|
return (await waiter.promise).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.as = 'user';
|
||||||
|
opts.position = 'history';
|
||||||
|
await streamingGeneration.xbgenCommand(opts, promptOrMsgs);
|
||||||
|
return (await waiter.promise).trim();
|
||||||
|
} finally {
|
||||||
|
waiter.cleanup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
streamLlmQueue = streamLlmQueue.then(runStreaming, runStreaming);
|
||||||
|
return streamLlmQueue;
|
||||||
|
}
|
||||||
|
|
||||||
/** 调用LLM并解析JSON */
|
/** 调用LLM并解析JSON */
|
||||||
async function callLLMJson({ messages, useRaw = true, isArray = false, validate }) {
|
async function callLLMJson({ messages, useRaw = true, isArray = false, validate }) {
|
||||||
try {
|
try {
|
||||||
@@ -447,7 +516,13 @@ function formatOutlinePrompt() {
|
|||||||
// 当前剧情
|
// 当前剧情
|
||||||
if (c?.sceneSetup && d.sceneSetup) {
|
if (c?.sceneSetup && d.sceneSetup) {
|
||||||
const ss = d.sceneSetup.sideStory || d.sceneSetup.side_story || d.sceneSetup;
|
const ss = d.sceneSetup.sideStory || d.sceneSetup.side_story || d.sceneSetup;
|
||||||
if (ss && (ss.surface || ss.inner)) { has = true; text += "### 当前剧情 (Current Scene)\n"; if (ss.surface) text += `* 表象: ${ss.surface}\n`; if (ss.inner) text += `* 里层 (潜台词): ${ss.inner}\n`; text += "\n"; }
|
if (ss && (ss.Facade || ss.Undercurrent)) {
|
||||||
|
has = true;
|
||||||
|
text += "### 当前剧情 (Current Scene)\n";
|
||||||
|
if (ss.Facade) text += `* 表现: ${ss.Facade}\n`;
|
||||||
|
if (ss.Undercurrent) text += `* 暗流: ${ss.Undercurrent}\n`;
|
||||||
|
text += "\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 角色卡短信
|
// 角色卡短信
|
||||||
@@ -539,7 +614,7 @@ function sendSettings() {
|
|||||||
const store = getOutlineStore(), { name: charName, desc: charDesc } = getCharInfo();
|
const store = getOutlineStore(), { name: charName, desc: charDesc } = getCharInfo();
|
||||||
postFrame({
|
postFrame({
|
||||||
type: "LOAD_SETTINGS", globalSettings: getGlobalSettings(), commSettings: getCommSettings(),
|
type: "LOAD_SETTINGS", globalSettings: getGlobalSettings(), commSettings: getCommSettings(),
|
||||||
stage: store?.stage ?? 0, deviationScore: store?.deviationScore ?? 0, simulationProgress: store?.simulationProgress ?? 0,
|
stage: store?.stage ?? 0, deviationScore: store?.deviationScore ?? 0,
|
||||||
simulationTarget: store?.simulationTarget ?? 5, playerLocation: store?.playerLocation ?? '家',
|
simulationTarget: store?.simulationTarget ?? 5, playerLocation: store?.playerLocation ?? '家',
|
||||||
dataChecked: store?.dataChecked || {}, outlineData: store?.outlineData || {}, promptConfig: getPromptConfigPayload?.(),
|
dataChecked: store?.dataChecked || {}, outlineData: store?.outlineData || {}, promptConfig: getPromptConfigPayload?.(),
|
||||||
characterCardName: charName, characterCardDescription: charDesc,
|
characterCardName: charName, characterCardDescription: charDesc,
|
||||||
@@ -549,6 +624,18 @@ function sendSettings() {
|
|||||||
|
|
||||||
const loadAndSend = () => { const s = getOutlineStore(); if (s?.mapData) postFrame({ type: "LOAD_MAP_DATA", mapData: s.mapData }); sendSettings(); };
|
const loadAndSend = () => { const s = getOutlineStore(); if (s?.mapData) postFrame({ type: "LOAD_MAP_DATA", mapData: s.mapData }); sendSettings(); };
|
||||||
|
|
||||||
|
function sendSimStateOnly() {
|
||||||
|
const store = getOutlineStore();
|
||||||
|
postFrame({
|
||||||
|
type: "LOAD_SETTINGS",
|
||||||
|
commSettings: getCommSettings(),
|
||||||
|
stage: store?.stage ?? 0,
|
||||||
|
deviationScore: store?.deviationScore ?? 0,
|
||||||
|
simulationTarget: store?.simulationTarget ?? 5,
|
||||||
|
playerLocation: store?.playerLocation ?? '家',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 9. 请求处理器 ====================
|
// ==================== 9. 请求处理器 ====================
|
||||||
|
|
||||||
const reply = (type, reqId, data) => postFrame({ type, requestId: reqId, ...data });
|
const reply = (type, reqId, data) => postFrame({ type, requestId: reqId, ...data });
|
||||||
@@ -578,19 +665,26 @@ function mergeSimData(orig, upd) {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 检查自动推演 */
|
function tickSimCountdown(store) {
|
||||||
async function checkAutoSim(reqId) {
|
if (!store) return;
|
||||||
const store = getOutlineStore();
|
const prevRaw = Number(store.simulationTarget);
|
||||||
if (!store || (store.simulationProgress || 0) < (store.simulationTarget ?? 5)) return;
|
const prev = Number.isFinite(prevRaw) ? prevRaw : 5;
|
||||||
const data = { meta: store.outlineData?.meta || {}, world: store.outlineData?.world || null, maps: { outdoor: store.outlineData?.outdoor || null, indoor: store.outlineData?.indoor || null } };
|
const next = prev - 1;
|
||||||
await handleSimWorld({ requestId: `wsim_auto_${Date.now()}`, currentData: JSON.stringify(data), isAuto: true });
|
store.simulationTarget = next;
|
||||||
|
store.updatedAt = Date.now();
|
||||||
|
saveMetadataDebounced?.();
|
||||||
|
sendSimStateOnly();
|
||||||
|
if (prev > 0 && next <= 0) {
|
||||||
|
try { processCommands?.('/echo 该进行世界推演啦!'); } catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证器
|
// 验证器
|
||||||
const V = {
|
const V = {
|
||||||
sum: o => o?.summary, npc: o => o?.name && o?.aliases, arr: o => Array.isArray(o),
|
sum: o => o?.summary, npc: o => o?.name && o?.aliases, arr: o => Array.isArray(o),
|
||||||
scene: o => !!o?.review?.deviation && !!(o?.local_map || o?.scene_setup?.local_map),
|
scene: o => !!o?.review?.deviation && !!(o?.local_map || o?.scene_setup?.local_map),
|
||||||
lscene: o => !!o?.side_story, inv: o => typeof o?.invite === 'boolean' && o?.reply,
|
lscene: o => !!(o?.side_story?.Incident && o?.side_story?.Facade && o?.side_story?.Undercurrent),
|
||||||
|
inv: o => typeof o?.invite === 'boolean' && o?.reply,
|
||||||
sms: o => typeof o?.reply === 'string' && o.reply.length > 0,
|
sms: o => typeof o?.reply === 'string' && o.reply.length > 0,
|
||||||
wg1: d => !!d && typeof d === 'object', // 只要是对象就行,后续会 normalize
|
wg1: d => !!d && typeof d === 'object', // 只要是对象就行,后续会 normalize
|
||||||
wg2: d => !!(d?.world && (d?.maps || d?.world?.maps)?.outdoor),
|
wg2: d => !!(d?.world && (d?.maps || d?.world?.maps)?.outdoor),
|
||||||
@@ -783,10 +877,9 @@ async function handleSceneSwitch({ requestId, prevLocationName, prevLocationInfo
|
|||||||
const data = await callLLMJson({ messages: msgs, validate: V.scene });
|
const data = await callLLMJson({ messages: msgs, validate: V.scene });
|
||||||
if (!data || !V.scene(data)) return replyErr('SCENE_SWITCH_RESULT', requestId, '场景生成失败:无法解析 JSON 数据');
|
if (!data || !V.scene(data)) return replyErr('SCENE_SWITCH_RESULT', requestId, '场景生成失败:无法解析 JSON 数据');
|
||||||
const delta = data.review?.deviation?.score_delta || 0, old = store?.deviationScore || 0, newS = Math.min(100, Math.max(0, old + delta));
|
const delta = data.review?.deviation?.score_delta || 0, old = store?.deviationScore || 0, newS = Math.min(100, Math.max(0, old + delta));
|
||||||
if (store) { store.deviationScore = newS; if (targetLocationType !== 'home') store.simulationProgress = (store.simulationProgress || 0) + 1; saveMetadataDebounced?.(); }
|
if (store) { store.deviationScore = newS; tickSimCountdown(store); }
|
||||||
const lm = data.local_map || data.scene_setup?.local_map || null;
|
const lm = data.local_map || data.scene_setup?.local_map || null;
|
||||||
reply('SCENE_SWITCH_RESULT', requestId, { success: true, sceneData: { review: data.review, localMap: lm, strangers: [], scoreDelta: delta, newScore: newS } });
|
reply('SCENE_SWITCH_RESULT', requestId, { success: true, sceneData: { review: data.review, localMap: lm, strangers: [], scoreDelta: delta, newScore: newS } });
|
||||||
checkAutoSim(requestId);
|
|
||||||
} catch (e) { replyErr('SCENE_SWITCH_RESULT', requestId, `场景切换失败: ${e.message}`); }
|
} catch (e) { replyErr('SCENE_SWITCH_RESULT', requestId, `场景切换失败: ${e.message}`); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,6 +909,7 @@ async function handleGenLocalMap({ requestId, outdoorDescription }) {
|
|||||||
const msgs = buildLocalMapGenMessages({ storyOutline: formatOutlinePrompt(), outdoorDescription: outdoorDescription || '', historyCount: getCommSettings().historyCount || 50 });
|
const msgs = buildLocalMapGenMessages({ storyOutline: formatOutlinePrompt(), outdoorDescription: outdoorDescription || '', historyCount: getCommSettings().historyCount || 50 });
|
||||||
const data = await callLLMJson({ messages: msgs, validate: V.lm });
|
const data = await callLLMJson({ messages: msgs, validate: V.lm });
|
||||||
if (!data?.inside) return replyErr('GENERATE_LOCAL_MAP_RESULT', requestId, '局部地图生成失败:无法解析 JSON 数据');
|
if (!data?.inside) return replyErr('GENERATE_LOCAL_MAP_RESULT', requestId, '局部地图生成失败:无法解析 JSON 数据');
|
||||||
|
tickSimCountdown(getOutlineStore());
|
||||||
reply('GENERATE_LOCAL_MAP_RESULT', requestId, { success: true, localMapData: data.inside });
|
reply('GENERATE_LOCAL_MAP_RESULT', requestId, { success: true, localMapData: data.inside });
|
||||||
} catch (e) { replyErr('GENERATE_LOCAL_MAP_RESULT', requestId, `局部地图生成失败: ${e.message}`); }
|
} catch (e) { replyErr('GENERATE_LOCAL_MAP_RESULT', requestId, `局部地图生成失败: ${e.message}`); }
|
||||||
}
|
}
|
||||||
@@ -826,6 +920,7 @@ async function handleRefreshLocalMap({ requestId, locationName, currentLocalMap,
|
|||||||
const msgs = buildLocalMapRefreshMessages({ storyOutline: formatOutlinePrompt(), locationName: locationName || store?.playerLocation || '未知地点', locationInfo: currentLocalMap?.description || '', currentLocalMap: currentLocalMap || null, outdoorDescription: outdoorDescription || '', historyCount: comm.historyCount || 50, playerLocation: store?.playerLocation });
|
const msgs = buildLocalMapRefreshMessages({ storyOutline: formatOutlinePrompt(), locationName: locationName || store?.playerLocation || '未知地点', locationInfo: currentLocalMap?.description || '', currentLocalMap: currentLocalMap || null, outdoorDescription: outdoorDescription || '', historyCount: comm.historyCount || 50, playerLocation: store?.playerLocation });
|
||||||
const data = await callLLMJson({ messages: msgs, validate: V.lm });
|
const data = await callLLMJson({ messages: msgs, validate: V.lm });
|
||||||
if (!data?.inside) return replyErr('REFRESH_LOCAL_MAP_RESULT', requestId, '局部地图刷新失败:无法解析 JSON 数据');
|
if (!data?.inside) return replyErr('REFRESH_LOCAL_MAP_RESULT', requestId, '局部地图刷新失败:无法解析 JSON 数据');
|
||||||
|
tickSimCountdown(store);
|
||||||
reply('REFRESH_LOCAL_MAP_RESULT', requestId, { success: true, localMapData: data.inside });
|
reply('REFRESH_LOCAL_MAP_RESULT', requestId, { success: true, localMapData: data.inside });
|
||||||
} catch (e) { replyErr('REFRESH_LOCAL_MAP_RESULT', requestId, `局部地图刷新失败: ${e.message}`); }
|
} catch (e) { replyErr('REFRESH_LOCAL_MAP_RESULT', requestId, `局部地图刷新失败: ${e.message}`); }
|
||||||
}
|
}
|
||||||
@@ -836,11 +931,11 @@ async function handleGenLocalScene({ requestId, locationName, locationInfo }) {
|
|||||||
const msgs = buildLocalSceneGenMessages({ storyOutline: formatOutlinePrompt(), locationName: locationName || store?.playerLocation || '未知地点', locationInfo: locationInfo || '', stage: store?.stage || 0, currentAtmosphere: getAtmosphere(store), historyCount: comm.historyCount || 50, mode, playerLocation: store?.playerLocation });
|
const msgs = buildLocalSceneGenMessages({ storyOutline: formatOutlinePrompt(), locationName: locationName || store?.playerLocation || '未知地点', locationInfo: locationInfo || '', stage: store?.stage || 0, currentAtmosphere: getAtmosphere(store), historyCount: comm.historyCount || 50, mode, playerLocation: store?.playerLocation });
|
||||||
const data = await callLLMJson({ messages: msgs, validate: V.lscene });
|
const data = await callLLMJson({ messages: msgs, validate: V.lscene });
|
||||||
if (!data || !V.lscene(data)) return replyErr('GENERATE_LOCAL_SCENE_RESULT', requestId, '局部剧情生成失败:无法解析 JSON 数据');
|
if (!data || !V.lscene(data)) return replyErr('GENERATE_LOCAL_SCENE_RESULT', requestId, '局部剧情生成失败:无法解析 JSON 数据');
|
||||||
if (store) { store.simulationProgress = (store.simulationProgress || 0) + 1; saveMetadataDebounced?.(); }
|
tickSimCountdown(store);
|
||||||
const ssf = data.side_story || null, intro = ssf?.Introduce || ssf?.introduce || '';
|
const ssf = data.side_story || null;
|
||||||
const ss = ssf ? (() => { const { Introduce, introduce: i2, story, ...rest } = ssf; return rest; })() : null;
|
const intro = (ssf?.Incident || '').trim();
|
||||||
|
const ss = ssf ? { Facade: ssf.Facade || '', Undercurrent: ssf.Undercurrent || '' } : null;
|
||||||
reply('GENERATE_LOCAL_SCENE_RESULT', requestId, { success: true, sceneSetup: { sideStory: ss, review: data.review || null }, introduce: intro, loc: locationName });
|
reply('GENERATE_LOCAL_SCENE_RESULT', requestId, { success: true, sceneSetup: { sideStory: ss, review: data.review || null }, introduce: intro, loc: locationName });
|
||||||
checkAutoSim(requestId);
|
|
||||||
} catch (e) { replyErr('GENERATE_LOCAL_SCENE_RESULT', requestId, `局部剧情生成失败: ${e.message}`); }
|
} catch (e) { replyErr('GENERATE_LOCAL_SCENE_RESULT', requestId, `局部剧情生成失败: ${e.message}`); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -898,7 +993,7 @@ async function handleGenWorld({ requestId, playerRequests }) {
|
|||||||
const msgs = buildWorldGenStep2Messages({ historyCount: comm.historyCount || 50, playerRequests, mode: 'assist' });
|
const msgs = buildWorldGenStep2Messages({ historyCount: comm.historyCount || 50, playerRequests, mode: 'assist' });
|
||||||
const wd = await callLLMJson({ messages: msgs, validate: V.wga });
|
const wd = await callLLMJson({ messages: msgs, validate: V.wga });
|
||||||
if (!wd?.maps?.outdoor || !Array.isArray(wd.maps.outdoor.nodes)) return replyErr('GENERATE_WORLD_RESULT', requestId, '生成失败:返回数据缺少地图节点');
|
if (!wd?.maps?.outdoor || !Array.isArray(wd.maps.outdoor.nodes)) return replyErr('GENERATE_WORLD_RESULT', requestId, '生成失败:返回数据缺少地图节点');
|
||||||
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = { ...wd }; saveMetadataDebounced?.(); }
|
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationTarget: randRange(3, 7) }); store.outlineData = { ...wd }; saveMetadataDebounced?.(); sendSimStateOnly(); }
|
||||||
return reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: wd });
|
return reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: wd });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -925,7 +1020,7 @@ async function handleGenWorld({ requestId, playerRequests }) {
|
|||||||
|
|
||||||
const final = { meta: s1d.meta, world: s2d.world, maps: s2d.maps, playerLocation: s2d.playerLocation };
|
const final = { meta: s1d.meta, world: s2d.world, maps: s2d.maps, playerLocation: s2d.playerLocation };
|
||||||
step1Cache = null;
|
step1Cache = null;
|
||||||
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); }
|
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); sendSimStateOnly(); }
|
||||||
reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: final });
|
reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: final });
|
||||||
} catch (e) { replyErr('GENERATE_WORLD_RESULT', requestId, `生成失败: ${e.message}`); }
|
} catch (e) { replyErr('GENERATE_WORLD_RESULT', requestId, `生成失败: ${e.message}`); }
|
||||||
}
|
}
|
||||||
@@ -946,7 +1041,7 @@ async function handleRetryStep2({ requestId }) {
|
|||||||
|
|
||||||
const final = { meta: s1d.meta, world: s2d.world, maps: s2d.maps, playerLocation: s2d.playerLocation };
|
const final = { meta: s1d.meta, world: s2d.world, maps: s2d.maps, playerLocation: s2d.playerLocation };
|
||||||
step1Cache = null;
|
step1Cache = null;
|
||||||
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); }
|
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); sendSimStateOnly(); }
|
||||||
reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: final });
|
reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: final });
|
||||||
} catch (e) { replyErr('GENERATE_WORLD_RESULT', requestId, `Step 2 重试失败: ${e.message}`); }
|
} catch (e) { replyErr('GENERATE_WORLD_RESULT', requestId, `Step 2 重试失败: ${e.message}`); }
|
||||||
}
|
}
|
||||||
@@ -958,7 +1053,7 @@ async function handleSimWorld({ requestId, currentData, isAuto }) {
|
|||||||
const data = await callLLMJson({ messages: msgs, validate: V.w });
|
const data = await callLLMJson({ messages: msgs, validate: V.w });
|
||||||
if (!data || !V.w(data)) return replyErr('SIMULATE_WORLD_RESULT', requestId, mode === 'assist' ? '世界推演失败:无法解析 JSON 数据(需包含 world 或 maps 字段)' : '世界推演失败:无法解析 JSON 数据');
|
if (!data || !V.w(data)) return replyErr('SIMULATE_WORLD_RESULT', requestId, mode === 'assist' ? '世界推演失败:无法解析 JSON 数据(需包含 world 或 maps 字段)' : '世界推演失败:无法解析 JSON 数据');
|
||||||
const orig = safe(() => JSON.parse(currentData)) || {}, merged = mergeSimData(orig, data);
|
const orig = safe(() => JSON.parse(currentData)) || {}, merged = mergeSimData(orig, data);
|
||||||
if (store) { store.stage = (store.stage || 0) + 1; store.simulationProgress = 0; store.simulationTarget = randRange(3, 7); saveMetadataDebounced?.(); }
|
if (store) { store.stage = (store.stage || 0) + 1; store.simulationTarget = randRange(3, 7); saveMetadataDebounced?.(); sendSimStateOnly(); }
|
||||||
reply('SIMULATE_WORLD_RESULT', requestId, { success: true, simData: merged, isAuto: !!isAuto });
|
reply('SIMULATE_WORLD_RESULT', requestId, { success: true, simData: merged, isAuto: !!isAuto });
|
||||||
} catch (e) { replyErr('SIMULATE_WORLD_RESULT', requestId, `推演失败: ${e.message}`); }
|
} catch (e) { replyErr('SIMULATE_WORLD_RESULT', requestId, `推演失败: ${e.message}`); }
|
||||||
}
|
}
|
||||||
@@ -968,18 +1063,25 @@ function handleSaveSettings(d) {
|
|||||||
if (d.commSettings) saveCommSettings(d.commSettings);
|
if (d.commSettings) saveCommSettings(d.commSettings);
|
||||||
const store = getOutlineStore();
|
const store = getOutlineStore();
|
||||||
if (store) {
|
if (store) {
|
||||||
['stage', 'deviationScore', 'simulationProgress', 'simulationTarget', 'playerLocation'].forEach(k => { if (d[k] !== undefined) store[k] = d[k]; });
|
['stage', 'deviationScore', 'simulationTarget', 'playerLocation'].forEach(k => { if (d[k] !== undefined) store[k] = d[k]; });
|
||||||
if (d.dataChecked) store.dataChecked = d.dataChecked;
|
if (d.dataChecked) store.dataChecked = d.dataChecked;
|
||||||
if (d.allData) store.outlineData = d.allData;
|
if (d.allData) store.outlineData = d.allData;
|
||||||
store.updatedAt = Date.now();
|
store.updatedAt = Date.now();
|
||||||
saveMetadataDebounced?.();
|
saveMetadataDebounced?.();
|
||||||
}
|
}
|
||||||
injectOutline();
|
injectOutline();
|
||||||
|
try {
|
||||||
|
StoryOutlineSettingsStorage?.set?.('settings', {
|
||||||
|
globalSettings: getGlobalSettings(),
|
||||||
|
commSettings: getCommSettings(),
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSavePrompts(d) {
|
function handleSavePrompts(d) {
|
||||||
if (!d?.promptConfig) return;
|
if (!d?.promptConfig) return;
|
||||||
setPromptConfig?.(d.promptConfig, true);
|
const payload = setPromptConfig?.(d.promptConfig, true);
|
||||||
|
try { StoryOutlinePromptStorage?.set?.('promptConfig', payload || d.promptConfig); } catch {}
|
||||||
postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() });
|
postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1194,8 +1296,28 @@ document.addEventListener('xiaobaixEnabledChanged', e => {
|
|||||||
|
|
||||||
// ==================== 初始化 ====================
|
// ==================== 初始化 ====================
|
||||||
|
|
||||||
|
async function initPromptConfigFromServer() {
|
||||||
|
try {
|
||||||
|
const cfg = await StoryOutlinePromptStorage?.get?.('promptConfig', null);
|
||||||
|
if (!cfg) return;
|
||||||
|
setPromptConfig?.(cfg, true);
|
||||||
|
postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() });
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initSettingsFromServer() {
|
||||||
|
try {
|
||||||
|
const s = await StoryOutlineSettingsStorage?.get?.('settings', null);
|
||||||
|
if (!s || typeof s !== 'object') return;
|
||||||
|
if (s.globalSettings) saveGlobalSettings(s.globalSettings);
|
||||||
|
if (s.commSettings) saveCommSettings(s.commSettings);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
jQuery(() => {
|
jQuery(() => {
|
||||||
if (!getSettings().storyOutline?.enabled) return;
|
if (!getSettings().storyOutline?.enabled) return;
|
||||||
|
initSettingsFromServer();
|
||||||
|
initPromptConfigFromServer();
|
||||||
registerEvents();
|
registerEvents();
|
||||||
setTimeout(injectOutline, 200);
|
setTimeout(injectOutline, 200);
|
||||||
window.registerModuleCleanup?.('storyOutline', cleanup);
|
window.registerModuleCleanup?.('storyOutline', cleanup);
|
||||||
|
|||||||
Reference in New Issue
Block a user