回退
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,11 @@
|
|||||||
<title>小白板</title>
|
<title>小白板</title>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<style>
|
<style>
|
||||||
/* ================== 基础重置 ================== */
|
|
||||||
*{margin:0;padding:0;box-sizing:border-box}
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
:root{--bg:#f7f7f7;--bg2:#fff;--bg3:#f0f0f0;--c:#222;--c2:#666;--c3:#999;--bd:#ddd}
|
:root{--bg:#f7f7f7;--bg2:#fff;--bg3:#f0f0f0;--c:#222;--c2:#666;--c3:#999;--bd:#ddd}
|
||||||
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--c)}
|
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--c)}
|
||||||
|
|
||||||
/* ================== 工具类 ================== */
|
/* 工具类 */
|
||||||
.fc{display:flex;align-items:center}
|
.fc{display:flex;align-items:center}
|
||||||
.fcc{display:flex;align-items:center;justify-content:center}
|
.fcc{display:flex;align-items:center;justify-content:center}
|
||||||
.col{flex-direction:column}
|
.col{flex-direction:column}
|
||||||
@@ -27,7 +26,7 @@
|
|||||||
.ofy{overflow-y:auto}.usn{user-select:none}
|
.ofy{overflow-y:auto}.usn{user-select:none}
|
||||||
.trans{transition:all .15s}
|
.trans{transition:all .15s}
|
||||||
|
|
||||||
/* ================== 按钮 ================== */
|
/* 按钮 */
|
||||||
.btn{padding:8px 16px;background:var(--bg2);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s;border:1px solid var(--bd);border-radius:4px;color:var(--c)}
|
.btn{padding:8px 16px;background:var(--bg2);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s;border:1px solid var(--bd);border-radius:4px;color:var(--c)}
|
||||||
.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}
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
.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}
|
||||||
|
|
||||||
/* ================== 折叠面板 ================== */
|
/* 折叠面板 */
|
||||||
.fold{background:var(--bg2);border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;overflow:hidden}
|
.fold{background:var(--bg2);border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;overflow:hidden}
|
||||||
.fold-h{padding:12px 14px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}
|
.fold-h{padding:12px 14px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}
|
||||||
.fold-h:hover{background:var(--bg3)}
|
.fold-h:hover{background:var(--bg3)}
|
||||||
@@ -45,7 +44,7 @@
|
|||||||
.fold-b{max-height:0;overflow:hidden;transition:all .25s}
|
.fold-b{max-height:0;overflow:hidden;transition:all .25s}
|
||||||
.fold.exp .fold-b{max-height:300px}
|
.fold.exp .fold-b{max-height:300px}
|
||||||
|
|
||||||
/* ================== 侧边导航 ================== */
|
/* 侧边导航 */
|
||||||
.side-nav-wrap{position:fixed;left:10px;top:50%;transform:translateY(-50%);z-index:500;display:flex;flex-direction:column;gap:6px}
|
.side-nav-wrap{position:fixed;left:10px;top:50%;transform:translateY(-50%);z-index:500;display:flex;flex-direction:column;gap:6px}
|
||||||
.side-glass{background:rgba(255,255,255,.3);backdrop-filter:blur(4px);box-shadow:0 2px 12px rgba(0,0,0,.05);border:1px solid rgba(221,221,221,.3);opacity:.4;transition:opacity .2s,background .2s}
|
.side-glass{background:rgba(255,255,255,.3);backdrop-filter:blur(4px);box-shadow:0 2px 12px rgba(0,0,0,.05);border:1px solid rgba(221,221,221,.3);opacity:.4;transition:opacity .2s,background .2s}
|
||||||
.side-glass:hover{opacity:1;background:rgba(255,255,255,.85);backdrop-filter:blur(8px)}
|
.side-glass:hover{opacity:1;background:rgba(255,255,255,.85);backdrop-filter:blur(8px)}
|
||||||
@@ -62,34 +61,34 @@
|
|||||||
.side-menu-panel.show{display:flex}
|
.side-menu-panel.show{display:flex}
|
||||||
.side-menu-panel .btn{font-size:11px;padding:5px 10px}
|
.side-menu-panel .btn{font-size:11px;padding:5px 10px}
|
||||||
|
|
||||||
/* ================== 工具栏 ================== */
|
/* 工具栏 */
|
||||||
.toolbar{height:44px;background:var(--bg2);border-bottom:1px solid var(--bd);padding:0 12px 0 56px;position:relative;z-index:200}
|
.toolbar{height:44px;background:var(--bg2);border-bottom:1px solid var(--bd);padding:0 12px 0 56px;position:relative;z-index:200}
|
||||||
.toolbar-t{font-size:14px;font-weight:600;margin-right:auto}
|
.toolbar-t{font-size:14px;font-weight:600;margin-right:auto}
|
||||||
.toolbar-t span{font-weight:400;color:var(--c3);margin-left:8px;font-size:12px}
|
.toolbar-t span{font-weight:400;color:var(--c3);margin-left:8px;font-size:12px}
|
||||||
|
|
||||||
/* ================== 页面容器 ================== */
|
/* 页面容器 */
|
||||||
.main-wrap{width:100%;height:calc(100% - 44px);position:relative}
|
.main-wrap{width:100%;height:calc(100% - 44px);position:relative}
|
||||||
.page{position:absolute;inset:0;background:var(--bg);display:none;overflow:hidden}
|
.page{position:absolute;inset:0;background:var(--bg);display:none;overflow:hidden}
|
||||||
.page.act{display:block}
|
.page.act{display:block}
|
||||||
.page-pad{padding:20px 20px 20px 56px;overflow-y:auto;height:100%}
|
.page-pad{padding:20px 20px 20px 56px;overflow-y:auto;height:100%}
|
||||||
|
|
||||||
/* ================== 横幅 ================== */
|
/* 横幅 */
|
||||||
.banner{width:100%;height:160px;border-radius:8px;overflow:hidden;margin-bottom:20px;position:relative;background:var(--bg3)}
|
.banner{width:100%;height:160px;border-radius:8px;overflow:hidden;margin-bottom:20px;position:relative;background:var(--bg3)}
|
||||||
.banner img{width:100%;height:100%;object-fit:cover;filter:grayscale(30%)}
|
.banner img{width:100%;height:100%;object-fit:cover;filter:grayscale(30%)}
|
||||||
.banner-ov{position:absolute;inset:0;background:linear-gradient(transparent 40%,rgba(0,0,0,.5));display:flex;align-items:flex-end;padding:16px}
|
.banner-ov{position:absolute;inset:0;background:linear-gradient(transparent 40%,rgba(0,0,0,.5));display:flex;align-items:flex-end;padding:16px}
|
||||||
.banner-ov div{color:#fff;font-size:13px}
|
.banner-ov div{color:#fff;font-size:13px}
|
||||||
|
|
||||||
/* ================== 章节标题 ================== */
|
/* 章节标题 */
|
||||||
.sec-t{font-size:12px;color:var(--c3);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px}
|
.sec-t{font-size:12px;color:var(--c3);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px}
|
||||||
|
|
||||||
/* ================== 新闻 ================== */
|
/* 新闻 */
|
||||||
.news-sec{margin-bottom:24px}
|
.news-sec{margin-bottom:24px}
|
||||||
.news-t{font-size:13px;font-weight:500}
|
.news-t{font-size:13px;font-weight:500}
|
||||||
.news-time{font-size:11px;color:var(--c3)}
|
.news-time{font-size:11px;color:var(--c3)}
|
||||||
.fold.exp .news-b{padding:0 14px 14px}
|
.fold.exp .news-b{padding:0 14px 14px}
|
||||||
.news-b p{font-size:12px;color:var(--c2);line-height:1.7}
|
.news-b p{font-size:12px;color:var(--c2);line-height:1.7}
|
||||||
|
|
||||||
/* ================== 用户指南 ================== */
|
/* 用户指南 */
|
||||||
.user-guide,.prog{padding:14px;background:var(--bg2);border-radius:6px;border:1px solid var(--bd)}
|
.user-guide,.prog{padding:14px;background:var(--bg2);border-radius:6px;border:1px solid var(--bd)}
|
||||||
.user-guide{margin-bottom:20px}
|
.user-guide{margin-bottom:20px}
|
||||||
.user-guide-state{font-size:13px;font-weight:500;margin-bottom:8px}
|
.user-guide-state{font-size:13px;font-weight:500;margin-bottom:8px}
|
||||||
@@ -97,12 +96,12 @@
|
|||||||
.user-guide-action{padding:8px 12px;background:var(--bg);border:1px solid var(--bd);border-radius:4px;font-size:12px;color:var(--c2);cursor:pointer;transition:all .15s}
|
.user-guide-action{padding:8px 12px;background:var(--bg);border:1px solid var(--bd);border-radius:4px;font-size:12px;color:var(--c2);cursor:pointer;transition:all .15s}
|
||||||
.user-guide-action:hover{border-color:var(--c);background:var(--bg3)}
|
.user-guide-action:hover{border-color:var(--c);background:var(--bg3)}
|
||||||
|
|
||||||
/* ================== 进度条 ================== */
|
/* 进度条 */
|
||||||
.prog-bar{height:6px;background:var(--bg3);border-radius:3px;overflow:hidden}
|
.prog-bar{height:6px;background:var(--bg3);border-radius:3px;overflow:hidden}
|
||||||
.prog-fill{height:100%;background:var(--c);border-radius:3px}
|
.prog-fill{height:100%;background:var(--c);border-radius:3px}
|
||||||
.prog-txt{font-size:11px;color:var(--c3);margin-top:8px;text-align:right}
|
.prog-txt{font-size:11px;color:var(--c3);margin-top:8px;text-align:right}
|
||||||
|
|
||||||
/* ================== 地图 ================== */
|
/* 地图 */
|
||||||
#mapWrap{width:100%;height:100%;background:var(--bg);cursor:grab;overflow:hidden;position:relative;touch-action:none}
|
#mapWrap{width:100%;height:100%;background:var(--bg);cursor:grab;overflow:hidden;position:relative;touch-action:none}
|
||||||
#inner{position:absolute;width:4000px;height:4000px;transform-origin:0 0}
|
#inner{position:absolute;width:4000px;height:4000px;transform-origin:0 0}
|
||||||
#lines{position:absolute;width:4000px;height:4000px;pointer-events:none}
|
#lines{position:absolute;width:4000px;height:4000px;pointer-events:none}
|
||||||
@@ -113,7 +112,7 @@
|
|||||||
.item.node-sub{background:var(--bg2)}
|
.item.node-sub{background:var(--bg2)}
|
||||||
.item.hl{border-color:#666;box-shadow:0 0 0 3px rgba(0,0,0,.2);z-index:20}
|
.item.hl{border-color:#666;box-shadow:0 0 0 3px rgba(0,0,0,.2);z-index:20}
|
||||||
|
|
||||||
/* ================== 地图操作 ================== */
|
/* 地图操作 */
|
||||||
.map-act{position:absolute;top:10px;right:10px;z-index:100}
|
.map-act{position:absolute;top:10px;right:10px;z-index:100}
|
||||||
#btn-goto{display:none}
|
#btn-goto{display:none}
|
||||||
#btn-goto.show{display:flex}
|
#btn-goto.show{display:flex}
|
||||||
@@ -122,7 +121,7 @@
|
|||||||
.map-lbl i:first-child{margin-right:6px}
|
.map-lbl i:first-child{margin-right:6px}
|
||||||
.map-lbl-sel{position:absolute;opacity:0;cursor:pointer;inset:0;border:none;background:transparent;-webkit-appearance:none;appearance:none}
|
.map-lbl-sel{position:absolute;opacity:0;cursor:pointer;inset:0;border:none;background:transparent;-webkit-appearance:none;appearance:none}
|
||||||
|
|
||||||
/* ================== 面板 ================== */
|
/* 面板 */
|
||||||
.panel{position:fixed;background:var(--bg2);padding:6px 12px;border-radius:4px;font-size:11px;color:var(--c3);border:1px solid var(--bd)}
|
.panel{position:fixed;background:var(--bg2);padding:6px 12px;border-radius:4px;font-size:11px;color:var(--c3);border:1px solid var(--bd)}
|
||||||
#zoom-ind{bottom:12px;right:12px}
|
#zoom-ind{bottom:12px;right:12px}
|
||||||
#tip{bottom:12px;left:56px;max-width:240px;padding:12px 14px;line-height:1.6;max-height:180px;overflow-y:auto;display:none}
|
#tip{bottom:12px;left:56px;max-width:240px;padding:12px 14px;line-height:1.6;max-height:180px;overflow-y:auto;display:none}
|
||||||
@@ -138,7 +137,7 @@
|
|||||||
.info-bk:hover{background:var(--bg3)}
|
.info-bk:hover{background:var(--bg3)}
|
||||||
.info-c{color:var(--c2);font-size:12px;line-height:1.6}
|
.info-c{color:var(--c2);font-size:12px;line-height:1.6}
|
||||||
|
|
||||||
/* ================== 通讯录 ================== */
|
/* 通讯录 */
|
||||||
.comm-hd{display:flex;align-items:center;gap:10px;margin-bottom:16px}
|
.comm-hd{display:flex;align-items:center;gap:10px;margin-bottom:16px}
|
||||||
.comm-tabs{display:flex;flex:1}
|
.comm-tabs{display:flex;flex:1}
|
||||||
.comm-tab{flex:1;padding:10px;text-align:center;background:var(--bg2);border:1px solid var(--bd);cursor:pointer;font-size:12px;font-weight:500;transition:all .15s}
|
.comm-tab{flex:1;padding:10px;text-align:center;background:var(--bg2);border:1px solid var(--bd);cursor:pointer;font-size:12px;font-weight:500;transition:all .15s}
|
||||||
@@ -149,7 +148,7 @@
|
|||||||
.comm-sec{display:none}
|
.comm-sec{display:none}
|
||||||
.comm-sec.act{display:block}
|
.comm-sec.act{display:block}
|
||||||
|
|
||||||
/* ================== 联系人卡片 ================== */
|
/* 联系人卡片 */
|
||||||
.ct-hd{gap:10px}
|
.ct-hd{gap:10px}
|
||||||
.ct-av,.chat-av{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-size:13px;flex-shrink:0}
|
.ct-av,.chat-av{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-size:13px;flex-shrink:0}
|
||||||
.ct-info{flex:1}
|
.ct-info{flex:1}
|
||||||
@@ -161,14 +160,13 @@
|
|||||||
.ct-acts .btn{flex:1;justify-content:center;font-size:12px}
|
.ct-acts .btn{flex:1;justify-content:center;font-size:12px}
|
||||||
.empty{text-align:center;padding:40px 20px;color:var(--c3);font-size:13px}
|
.empty{text-align:center;padding:40px 20px;color:var(--c3);font-size:13px}
|
||||||
|
|
||||||
/* ================== 弹窗 ================== */
|
/* 弹窗 */
|
||||||
.modal{position:fixed;inset:0;z-index:10000;display:none;justify-content:center;align-items:center}
|
.modal{position:fixed;inset:0;z-index:10000;display:none;justify-content:center;align-items:center}
|
||||||
.modal.act{display:flex}
|
.modal.act{display:flex}
|
||||||
.modal-bd{position:absolute;inset:0;background:rgba(0,0,0,.4);backdrop-filter:blur(2px)}
|
.modal-bd{position:absolute;inset:0;background:rgba(0,0,0,.4);backdrop-filter:blur(2px)}
|
||||||
.modal-p{position:relative;width:90%;max-width:700px;max-height:85vh;background:var(--bg2);border:1px solid var(--bd);border-radius:8px;overflow:hidden;display:flex;flex-direction:column}
|
.modal-p{position:relative;width:90%;max-width:700px;max-height:85vh;background:var(--bg2);border:1px solid var(--bd);border-radius:8px;overflow:hidden;display:flex;flex-direction:column}
|
||||||
.modal-p.sm{max-width:360px}
|
.modal-p.sm{max-width:360px}
|
||||||
.modal-p.lg{max-width:560px}
|
.modal-p.lg{max-width:560px}
|
||||||
.modal-p.xl{max-width:800px}
|
|
||||||
.modal-hd,.modal-ft{padding:14px 18px}
|
.modal-hd,.modal-ft{padding:14px 18px}
|
||||||
.modal-hd{justify-content:space-between;border-bottom:1px solid var(--bd)}
|
.modal-hd{justify-content:space-between;border-bottom:1px solid var(--bd)}
|
||||||
.modal-hd h2{font-size:14px;font-weight:600}
|
.modal-hd h2{font-size:14px;font-weight:600}
|
||||||
@@ -177,20 +175,20 @@
|
|||||||
.modal-x:hover{background:var(--bg3)}
|
.modal-x:hover{background:var(--bg3)}
|
||||||
.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: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-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}
|
||||||
|
|
||||||
/* ================== 表单 ================== */
|
/* 表单 */
|
||||||
.form-g{margin-bottom:14px}
|
.form-g{margin-bottom:14px}
|
||||||
.form-l{display:block;font-size:12px;font-weight:500;margin-bottom:6px;color:var(--c2)}
|
.form-l{display:block;font-size:12px;font-weight:500;margin-bottom:6px;color:var(--c2)}
|
||||||
.form-in{width:100%;padding:10px 12px;border:1px solid var(--bd);border-radius:4px;font-size:13px;outline:none;background:var(--bg2)}
|
.form-in{width:100%;padding:10px 12px;border:1px solid var(--bd);border-radius:4px;font-size:13px;outline:none;background:var(--bg2)}
|
||||||
.form-ta{min-height:80px;resize:vertical;font-family:inherit}
|
.form-ta{min-height:80px;resize:vertical;font-family:inherit}
|
||||||
.goto-d{font-size:13px;color:var(--c);font-weight:500;padding:10px 14px;background:var(--bg3);border-radius:4px;margin-bottom:14px}
|
.goto-d{font-size:13px;color:var(--c);font-weight:500;padding:10px 14px;background:var(--bg3);border-radius:4px;margin-bottom:14px}
|
||||||
|
|
||||||
/* ================== 聊天 ================== */
|
/* 聊天 */
|
||||||
.chat{position:fixed;top:0;right:-400px;width:300px;height:100%;background:var(--bg2);border-left:1px solid var(--bd);z-index:600;display:flex;flex-direction:column;transition:right .3s}
|
.chat{position:fixed;top:0;right:-400px;width:300px;height:100%;background:var(--bg2);border-left:1px solid var(--bd);z-index:600;display:flex;flex-direction:column;transition:right .3s}
|
||||||
.chat.act{right:0}
|
.chat.act{right:0}
|
||||||
.chat-hd{padding:14px 18px;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:12px;flex-shrink:0}
|
.chat-hd{padding:14px 18px;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:12px;flex-shrink:0}
|
||||||
@@ -220,7 +218,7 @@
|
|||||||
.chat-send:hover{opacity:.9}
|
.chat-send:hover{opacity:.9}
|
||||||
.chat-emp{text-align:center;color:var(--c3);font-size:12px;padding:40px 20px}
|
.chat-emp{text-align:center;color:var(--c3);font-size:12px;padding:40px 20px}
|
||||||
|
|
||||||
/* ================== 地点列表 ================== */
|
/* 地点列表 */
|
||||||
.loc-list{max-height:300px;overflow-y:auto}
|
.loc-list{max-height:300px;overflow-y:auto}
|
||||||
.loc-i{padding:12px 14px;border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;cursor:pointer;transition:all .15s}
|
.loc-i{padding:12px 14px;border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;cursor:pointer;transition:all .15s}
|
||||||
.loc-i:hover{border-color:var(--c);background:var(--bg3)}
|
.loc-i:hover{border-color:var(--c);background:var(--bg3)}
|
||||||
@@ -228,7 +226,7 @@
|
|||||||
.loc-i-nm{font-size:13px;font-weight:500}
|
.loc-i-nm{font-size:13px;font-weight:500}
|
||||||
.loc-i-info{font-size:11px;opacity:.7;margin-top:2px}
|
.loc-i-info{font-size:11px;opacity:.7;margin-top:2px}
|
||||||
|
|
||||||
/* ================== 底部弹窗 ================== */
|
/* 底部弹窗 */
|
||||||
.mob-pop{position:fixed;bottom:0;left:0;right:0;background:var(--bg2);border-top:1px solid var(--bd);border-radius:12px 12px 0 0;box-shadow:0 -2px 16px rgba(0,0,0,.1);z-index:101;display:none;flex-direction:column}
|
.mob-pop{position:fixed;bottom:0;left:0;right:0;background:var(--bg2);border-top:1px solid var(--bd);border-radius:12px 12px 0 0;box-shadow:0 -2px 16px rgba(0,0,0,.1);z-index:101;display:none;flex-direction:column}
|
||||||
.mob-pop.act{display:flex}
|
.mob-pop.act{display:flex}
|
||||||
.mob-pop.drag{transition:none!important}
|
.mob-pop.drag{transition:none!important}
|
||||||
@@ -246,7 +244,7 @@
|
|||||||
.pop-h-ind span{width:3px;height:8px;background:var(--bd);border-radius:1px}
|
.pop-h-ind span{width:3px;height:8px;background:var(--bd);border-radius:1px}
|
||||||
.pop-h-ind span.act{background:var(--c)}
|
.pop-h-ind span.act{background:var(--c)}
|
||||||
|
|
||||||
/* ================== 右侧面板 ================== */
|
/* 右侧面板 */
|
||||||
.side-pop{position:fixed;top:44px;right:0;bottom:0;background:var(--bg2);border-left:1px solid var(--bd);z-index:90;display:none;width:8px}
|
.side-pop{position:fixed;top:44px;right:0;bottom:0;background:var(--bg2);border-left:1px solid var(--bd);z-index:90;display:none;width:8px}
|
||||||
.side-pop.show{display:flex}
|
.side-pop.show{display:flex}
|
||||||
.side-pop:not(.drag){transition:width .2s ease-out}
|
.side-pop:not(.drag){transition:width .2s ease-out}
|
||||||
@@ -258,7 +256,7 @@
|
|||||||
.side-pop-hd{font-size:11px;color:var(--c3);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px}
|
.side-pop-hd{font-size:11px;color:var(--c3);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px}
|
||||||
.side-pop-desc{font-size:13px;color:var(--c2);line-height:1.7}
|
.side-pop-desc{font-size:13px;color:var(--c2);line-height:1.7}
|
||||||
|
|
||||||
/* ================== 设置 ================== */
|
/* 设置 */
|
||||||
.set-sec{margin-bottom:16px}
|
.set-sec{margin-bottom:16px}
|
||||||
.set-sec-t{font-size:13px;font-weight:600;color:var(--c);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--bd)}
|
.set-sec-t{font-size:13px;font-weight:600;color:var(--c);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--bd)}
|
||||||
.set-row{display:flex;align-items:center;gap:8px;margin-bottom:10px}
|
.set-row{display:flex;align-items:center;gap:8px;margin-bottom:10px}
|
||||||
@@ -270,7 +268,7 @@
|
|||||||
.set-test-res.ok{display:block;background:#dcfce7;color:#166534;border:1px solid #86efac}
|
.set-test-res.ok{display:block;background:#dcfce7;color:#166534;border:1px solid #86efac}
|
||||||
.set-test-res.err{display:block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca}
|
.set-test-res.err{display:block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca}
|
||||||
|
|
||||||
/* ================== 数据项 ================== */
|
/* 数据项 */
|
||||||
.data-item{display:flex;align-items:flex-start;gap:10px;padding:12px;background:var(--bg);border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;cursor:pointer;transition:all .15s}
|
.data-item{display:flex;align-items:flex-start;gap:10px;padding:12px;background:var(--bg);border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;cursor:pointer;transition:all .15s}
|
||||||
.data-item:hover{border-color:var(--c2);background:var(--bg3)}
|
.data-item:hover{border-color:var(--c2);background:var(--bg3)}
|
||||||
.data-item.sel{border-color:var(--c);background:rgba(34,34,34,.05)}
|
.data-item.sel{border-color:var(--c);background:rgba(34,34,34,.05)}
|
||||||
@@ -284,23 +282,7 @@
|
|||||||
.data-edit{width:28px;height:28px;border:1px solid var(--bd);border-radius:4px;background:var(--bg2);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s;color:var(--c3)}
|
.data-edit{width:28px;height:28px;border:1px solid var(--bd);border-radius:4px;background:var(--bg2);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s;color:var(--c3)}
|
||||||
.data-edit:hover{border-color:var(--c);color:var(--c);background:var(--bg3)}
|
.data-edit:hover{border-color:var(--c);color:var(--c);background:var(--bg3)}
|
||||||
|
|
||||||
/* ================== 提示词编辑器 ================== */
|
/* 响应式 */
|
||||||
.prompt-sec{margin-bottom:16px}
|
|
||||||
.prompt-lbl{font-size:12px;font-weight:600;color:var(--c2);margin-bottom:6px;display:flex;align-items:center;gap:8px}
|
|
||||||
.prompt-lbl::before{content:'';display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--c3)}
|
|
||||||
.prompt-lbl.u::before{background:#3b82f6}
|
|
||||||
.prompt-lbl.a::before{background:#10b981}
|
|
||||||
.prompt-ta{width:100%;padding:12px;background:var(--bg);border:1px solid var(--bd);border-radius:6px;font-size:12px;line-height:1.7;color:var(--c);resize:vertical;font-family:inherit;min-height:80px}
|
|
||||||
.prompt-ta:focus{outline:none;border-color:var(--c)}
|
|
||||||
.prompt-ta.mono{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:11px;line-height:1.5}
|
|
||||||
.prompt-acts{display:flex;gap:8px;margin-top:12px}
|
|
||||||
.prompt-help{margin-top:16px;padding:14px;background:var(--bg3);border-radius:8px;border:1px solid var(--bd)}
|
|
||||||
.prompt-help-t{font-size:12px;font-weight:600;color:var(--c2);margin-bottom:10px}
|
|
||||||
.prompt-help-c{font-size:11px;color:var(--c3);line-height:2}
|
|
||||||
.prompt-help-c b{color:var(--c2);font-weight:500}
|
|
||||||
.prompt-help-c code{background:var(--bg2);padding:2px 6px;border-radius:3px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:10px}
|
|
||||||
|
|
||||||
/* ================== 响应式 ================== */
|
|
||||||
@media(max-width:550px){
|
@media(max-width:550px){
|
||||||
.chat{width:100%;right:-100%}
|
.chat{width:100%;right:-100%}
|
||||||
.side-pop{bottom:0}
|
.side-pop{bottom:0}
|
||||||
@@ -320,7 +302,7 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- ================== 侧边导航 ================== -->
|
<!-- 侧边导航 -->
|
||||||
<div class="side-nav-wrap">
|
<div class="side-nav-wrap">
|
||||||
<div class="side-menu side-glass">
|
<div class="side-menu side-glass">
|
||||||
<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>
|
||||||
@@ -331,13 +313,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav class="side-nav side-glass">
|
<nav class="side-nav side-glass">
|
||||||
<div class="nav-i" data-p="world"><i class="fa-solid fa-earth-asia"></i></div>
|
<div class="nav-i" data-p="world"><i class="fa-solid fa-earth-americas"></i></div>
|
||||||
<div class="nav-i act" data-p="map"><i class="fa-solid fa-map"></i></div>
|
<div class="nav-i act" data-p="map"><i class="fa-solid fa-map"></i></div>
|
||||||
<div class="nav-i" data-p="comm"><i class="fa-solid fa-phone"></i></div>
|
<div class="nav-i" data-p="comm"><i class="fa-solid fa-phone"></i></div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 工具栏 ================== -->
|
<!-- 工具栏 -->
|
||||||
<div class="toolbar fc g10 usn">
|
<div class="toolbar fc g10 usn">
|
||||||
<div class="toolbar-t">小白板<span>预测试</span></div>
|
<div class="toolbar-t">小白板<span>预测试</span></div>
|
||||||
<button class="btn btn-s fc g6" id="btn-deduce"><i class="fa-solid fa-wand-magic-sparkles"></i>生成</button>
|
<button class="btn btn-s fc g6" id="btn-deduce"><i class="fa-solid fa-wand-magic-sparkles"></i>生成</button>
|
||||||
@@ -345,8 +327,9 @@
|
|||||||
<button class="btn btn-c fcc" id="btn-close">✕</button>
|
<button class="btn btn-c fcc" id="btn-close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 主内容区 ================== -->
|
<!-- 主内容区 -->
|
||||||
<div class="main-wrap">
|
<div class="main-wrap">
|
||||||
|
<!-- 世界页 -->
|
||||||
<div class="page page-pad" id="page-world">
|
<div class="page page-pad" id="page-world">
|
||||||
<div class="banner"><img src="https://picsum.photos/800/300" alt=""><div class="banner-ov"><div>探索未知的世界...</div></div></div>
|
<div class="banner"><img src="https://picsum.photos/800/300" alt=""><div class="banner-ov"><div>探索未知的世界...</div></div></div>
|
||||||
<div class="news-sec"><h3 class="sec-t">最新消息</h3><div id="news-list"></div></div>
|
<div class="news-sec"><h3 class="sec-t">最新消息</h3><div id="news-list"></div></div>
|
||||||
@@ -358,6 +341,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 地图页 -->
|
||||||
<div class="page act" id="page-map">
|
<div class="page act" id="page-map">
|
||||||
<div id="mapWrap">
|
<div id="mapWrap">
|
||||||
<div class="map-lbl" id="map-lbl">
|
<div class="map-lbl" id="map-lbl">
|
||||||
@@ -379,13 +363,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 通讯录页 -->
|
||||||
<div class="page page-pad" id="page-comm">
|
<div class="page page-pad" id="page-comm">
|
||||||
<div class="comm-hd">
|
<div class="comm-hd">
|
||||||
<div class="comm-tabs">
|
<div class="comm-tabs">
|
||||||
<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-globe"></i></button>
|
<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-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>
|
||||||
@@ -393,7 +378,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 聊天面板 ================== -->
|
<!-- 聊天面板 -->
|
||||||
<div class="chat" id="chat">
|
<div class="chat" id="chat">
|
||||||
<div class="chat-hd">
|
<div class="chat-hd">
|
||||||
<div class="chat-av" id="chat-av"></div>
|
<div class="chat-av" id="chat-av"></div>
|
||||||
@@ -412,7 +397,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 右侧描述面板 ================== -->
|
<!-- 右侧描述面板 -->
|
||||||
<div class="side-pop" id="side-pop">
|
<div class="side-pop" id="side-pop">
|
||||||
<div class="side-pop-handle" id="side-pop-handle"><div class="side-pop-bar"></div></div>
|
<div class="side-pop-handle" id="side-pop-handle"><div class="side-pop-bar"></div></div>
|
||||||
<div class="side-pop-ct">
|
<div class="side-pop-ct">
|
||||||
@@ -424,7 +409,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 移动端底部弹窗 ================== -->
|
<!-- 移动端底部弹窗 -->
|
||||||
<div class="mob-pop" id="mob-pop">
|
<div class="mob-pop" id="mob-pop">
|
||||||
<div class="pop-h-ind"><span data-l="2"></span><span data-l="1"></span><span data-l="0"></span></div>
|
<div class="pop-h-ind"><span data-l="2"></span><span data-l="1"></span><span data-l="0"></span></div>
|
||||||
<div class="pop-hd" id="pop-hd"><div class="pop-handle"></div></div>
|
<div class="pop-hd" id="pop-hd"><div class="pop-handle"></div></div>
|
||||||
@@ -437,10 +422,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 设置弹窗 ================== -->
|
<!-- 设置弹窗 -->
|
||||||
<div class="modal" id="m-settings">
|
<div class="modal" id="m-settings">
|
||||||
<div class="modal-bd"></div>
|
<div class="modal-bd"></div>
|
||||||
<div class="modal-p xl">
|
<div class="modal-p lg">
|
||||||
<div class="modal-hd fc"><h2>设置</h2><button class="modal-x fcc">✕</button></div>
|
<div class="modal-hd fc"><h2>设置</h2><button class="modal-x fcc">✕</button></div>
|
||||||
<div class="modal-by">
|
<div class="modal-by">
|
||||||
<div class="set-sec">
|
<div class="set-sec">
|
||||||
@@ -487,103 +472,13 @@
|
|||||||
</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">模板编辑器</div>
|
|
||||||
|
|
||||||
<div class="form-g">
|
|
||||||
<label class="form-l">选择模板</label>
|
|
||||||
<select class="form-in" id="template-type-select">
|
|
||||||
<optgroup label="短信功能">
|
|
||||||
<option value="sms">短信回复</option>
|
|
||||||
<option value="summary">总结压缩</option>
|
|
||||||
<option value="invite">邀请回复</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="NPC管理">
|
|
||||||
<option value="npc">NPC 生成</option>
|
|
||||||
<option value="stranger">提取陌路人</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="世界生成 (故事模式)">
|
|
||||||
<option value="worldGenStep1">世界大纲 Step1</option>
|
|
||||||
<option value="worldGenStep2">世界细节 Step2</option>
|
|
||||||
<option value="worldSim">世界推演</option>
|
|
||||||
<option value="sceneSwitch">场景切换</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="世界生成 (辅助模式)">
|
|
||||||
<option value="worldGenAssist">世界生成 (辅助)</option>
|
|
||||||
<option value="worldSimAssist">世界推演 (辅助)</option>
|
|
||||||
<option value="sceneSwitchAssist">场景切换 (辅助)</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="局部地图">
|
|
||||||
<option value="localMapGen">局部地图生成</option>
|
|
||||||
<option value="localMapRefresh">局部地图刷新</option>
|
|
||||||
<option value="localSceneGen">局部剧情生成</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="template-editor-wrap">
|
|
||||||
<div class="prompt-sec">
|
|
||||||
<div class="prompt-lbl u">USER</div>
|
|
||||||
<textarea class="prompt-ta" id="tpl-u1" rows="6"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="prompt-sec">
|
|
||||||
<div class="prompt-lbl a">ASSISTANT</div>
|
|
||||||
<textarea class="prompt-ta" id="tpl-a1" rows="2"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="prompt-sec">
|
|
||||||
<div class="prompt-lbl u">USER</div>
|
|
||||||
<textarea class="prompt-ta" id="tpl-u2" rows="6"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="prompt-sec">
|
|
||||||
<div class="prompt-lbl a">ASSISTANT</div>
|
|
||||||
<textarea class="prompt-ta" id="tpl-a2" rows="2"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="prompt-sec" style="margin-top:16px">
|
|
||||||
<div class="prompt-lbl" style="color:var(--c2)"><i class="fa-solid fa-code"></i> JSON 输出格式</div>
|
|
||||||
<textarea class="prompt-ta mono" id="tpl-json" rows="10"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="prompt-acts">
|
|
||||||
<button class="btn btn-s" id="tpl-restore"><i class="fa-solid fa-rotate-left"></i> 恢复默认</button>
|
|
||||||
<button class="btn btn-s btn-p" id="tpl-save"><i class="fa-solid fa-check"></i> 保存</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="prompt-help">
|
|
||||||
<div class="prompt-help-t"><i class="fa-solid fa-circle-info"></i> 占位符说明</div>
|
|
||||||
<div class="prompt-help-c">
|
|
||||||
<div style="margin-bottom:8px"><b>角色变量</b></div>
|
|
||||||
<code>{{user}}</code> 玩家名称<br>
|
|
||||||
<code>{{char}}</code> 角色卡名称<br><br>
|
|
||||||
|
|
||||||
<div style="margin-bottom:8px"><b>场景变量</b></div>
|
|
||||||
<code>{{CONTACT_NAME}}</code> 当前聊天对象名称<br>
|
|
||||||
<code>{{USER_MESSAGE}}</code> 玩家发送的短信内容<br>
|
|
||||||
<code>{{TARGET_LOCATION}}</code> 目标地点名称<br>
|
|
||||||
<code>{{STRANGER_NAME}}</code> 陌生人名称<br>
|
|
||||||
<code>{{PLAYER_REQUESTS}}</code> 玩家的特殊需求文本<br><br>
|
|
||||||
|
|
||||||
<div style="margin-bottom:8px"><b>内容块</b></div>
|
|
||||||
<code>{{WORLD_INFO}}</code> 世界设定(角色描述+世界书+人格)<br>
|
|
||||||
<code>{{HISTORY}}</code> 最近N条聊天记录<br>
|
|
||||||
<code>{{HISTORY_50}}</code> 指定获取最近50条记录<br>
|
|
||||||
<code>{{STORY_OUTLINE}}</code> 剧情大纲(仅故事模式)<br>
|
|
||||||
<code>{{SMS_HISTORY}}</code> 短信聊天记录<br>
|
|
||||||
<code>{{CHARACTER_CONTENT}}</code> 联系人的世界书人设<br><br>
|
|
||||||
|
|
||||||
<div style="margin-bottom:8px"><b>JSON模板引用</b></div>
|
|
||||||
<code>{{JSON:sms}}</code> 引用当前模板的JSON格式定义
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
|
||||||
<!-- ================== 数据编辑弹窗 ================== -->
|
<!-- 数据编辑弹窗 -->
|
||||||
<div class="modal" id="m-data-edit">
|
<div class="modal" id="m-data-edit">
|
||||||
<div class="modal-bd"></div>
|
<div class="modal-bd"></div>
|
||||||
<div class="modal-p">
|
<div class="modal-p">
|
||||||
@@ -597,7 +492,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 前往确认弹窗 ================== -->
|
<!-- 前往确认弹窗 -->
|
||||||
<div class="modal" id="m-goto">
|
<div class="modal" id="m-goto">
|
||||||
<div class="modal-bd"></div>
|
<div class="modal-bd"></div>
|
||||||
<div class="modal-p sm">
|
<div class="modal-p sm">
|
||||||
@@ -610,7 +505,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 邀请弹窗 ================== -->
|
<!-- 邀请弹窗 -->
|
||||||
<div class="modal" id="m-invite">
|
<div class="modal" id="m-invite">
|
||||||
<div class="modal-bd"></div>
|
<div class="modal-bd"></div>
|
||||||
<div class="modal-p sm">
|
<div class="modal-p sm">
|
||||||
@@ -623,7 +518,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 世界生成弹窗 ================== -->
|
<!-- 世界生成弹窗 -->
|
||||||
<div class="modal" id="m-world-gen">
|
<div class="modal" id="m-world-gen">
|
||||||
<div class="modal-bd"></div>
|
<div class="modal-bd"></div>
|
||||||
<div class="modal-p">
|
<div class="modal-p">
|
||||||
@@ -636,7 +531,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 世界推演弹窗 ================== -->
|
<!-- 世界推演弹窗 -->
|
||||||
<div class="modal" id="m-world-sim">
|
<div class="modal" id="m-world-sim">
|
||||||
<div class="modal-bd"></div>
|
<div class="modal-bd"></div>
|
||||||
<div class="modal-p">
|
<div class="modal-p">
|
||||||
@@ -654,7 +549,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 添加联络人弹窗 ================== -->
|
<!-- 添加联络人弹窗 -->
|
||||||
<div class="modal" id="m-add-ct">
|
<div class="modal" id="m-add-ct">
|
||||||
<div class="modal-bd"></div>
|
<div class="modal-bd"></div>
|
||||||
<div class="modal-p sm">
|
<div class="modal-p sm">
|
||||||
@@ -671,7 +566,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================== 结果弹窗 ================== -->
|
<!-- 结果弹窗 -->
|
||||||
<div class="modal" id="m-result">
|
<div class="modal" id="m-result">
|
||||||
<div class="modal-bd"></div>
|
<div class="modal-bd"></div>
|
||||||
<div class="modal-p sm">
|
<div class="modal-p sm">
|
||||||
@@ -688,7 +583,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* ================== 数据状态 ================== */
|
// ================== 数据 ==================
|
||||||
const D = {
|
const D = {
|
||||||
stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: 5,
|
stage: 0, deviationScore: 0, simulationProgress: 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 },
|
||||||
@@ -698,7 +593,7 @@ const D = {
|
|||||||
|
|
||||||
let charSmsHistory = { messages: [], summarizedCount: 0, summaries: {} };
|
let charSmsHistory = { messages: [], summarizedCount: 0, summaries: {} };
|
||||||
|
|
||||||
/* ================== 工具函数 ================== */
|
// ================== 工具函数 ==================
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
const $$ = s => document.querySelectorAll(s);
|
const $$ = s => document.querySelectorAll(s);
|
||||||
const isMob = () => innerWidth <= 550;
|
const isMob = () => innerWidth <= 550;
|
||||||
@@ -724,62 +619,7 @@ const Req = {
|
|||||||
const openM = id => $(id).classList.add('act');
|
const openM = id => $(id).classList.add('act');
|
||||||
const closeM = id => $(id).classList.remove('act');
|
const closeM = id => $(id).classList.remove('act');
|
||||||
|
|
||||||
/* ================== 模板编辑器状态 ================== */
|
// ================== 地图状态 ==================
|
||||||
let templateState = {
|
|
||||||
currentType: 'sms',
|
|
||||||
prompts: {},
|
|
||||||
jsonTemplates: {},
|
|
||||||
defaults: { prompts: {}, jsonTemplates: {} }
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadTemplate(type) {
|
|
||||||
templateState.currentType = type;
|
|
||||||
const p = templateState.prompts[type] || templateState.defaults.prompts[type] || {};
|
|
||||||
$('tpl-u1').value = p.u1 || '';
|
|
||||||
$('tpl-a1').value = p.a1 || '';
|
|
||||||
$('tpl-u2').value = p.u2 || '';
|
|
||||||
$('tpl-a2').value = p.a2 || '';
|
|
||||||
const j = templateState.jsonTemplates[type] || templateState.defaults.jsonTemplates[type] || '';
|
|
||||||
$('tpl-json').value = j;
|
|
||||||
autoResizeAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveCurrentTemplate() {
|
|
||||||
const type = templateState.currentType;
|
|
||||||
templateState.prompts[type] = {
|
|
||||||
u1: $('tpl-u1').value,
|
|
||||||
a1: $('tpl-a1').value,
|
|
||||||
u2: $('tpl-u2').value,
|
|
||||||
a2: $('tpl-a2').value
|
|
||||||
};
|
|
||||||
templateState.jsonTemplates[type] = $('tpl-json').value;
|
|
||||||
post('SAVE_PROMPTS', { promptConfig: { prompts: templateState.prompts, jsonTemplates: templateState.jsonTemplates } });
|
|
||||||
const btn = $('tpl-save');
|
|
||||||
const orig = btn.innerHTML;
|
|
||||||
btn.innerHTML = '<i class="fa-solid fa-check"></i> 已保存';
|
|
||||||
btn.disabled = true;
|
|
||||||
setTimeout(() => { btn.innerHTML = orig; btn.disabled = false; }, 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreCurrentTemplate() {
|
|
||||||
const type = templateState.currentType;
|
|
||||||
if (!confirm(`确定要恢复「${type}」为默认模板吗?`)) return;
|
|
||||||
delete templateState.prompts[type];
|
|
||||||
delete templateState.jsonTemplates[type];
|
|
||||||
loadTemplate(type);
|
|
||||||
post('SAVE_PROMPTS', { promptConfig: { prompts: templateState.prompts, jsonTemplates: templateState.jsonTemplates } });
|
|
||||||
}
|
|
||||||
|
|
||||||
function autoResizeAll() {
|
|
||||||
['tpl-u1', 'tpl-a1', 'tpl-u2', 'tpl-a2'].forEach(id => {
|
|
||||||
const ta = $(id);
|
|
||||||
if (!ta) return;
|
|
||||||
ta.style.height = 'auto';
|
|
||||||
ta.style.height = Math.max(ta.scrollHeight, 60) + 'px';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================== 地图状态 ================== */
|
|
||||||
const dirMap = { north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0], northeast: [1, -1], northwest: [-1, -1], southeast: [1, 1], southwest: [-1, 1] };
|
const dirMap = { north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0], northeast: [1, -1], northwest: [-1, -1], southeast: [1, 1], southwest: [-1, 1] };
|
||||||
let playerLocation = '未知', curNode = null, drag = false, scale = 1, offX = 0, offY = 0, seed = 123456789, nodes = [], lines = [], anim = false;
|
let playerLocation = '未知', curNode = null, drag = false, scale = 1, offX = 0, offY = 0, seed = 123456789, nodes = [], lines = [], anim = false;
|
||||||
let chatTgt = null, invTgt = null, selLoc = null, smsGen = false, selectedMapValue = 'current';
|
let chatTgt = null, invTgt = null, selLoc = null, smsGen = false, selectedMapValue = 'current';
|
||||||
@@ -787,9 +627,12 @@ let chatTgt = null, invTgt = null, selLoc = null, smsGen = false, selectedMapVal
|
|||||||
const inner = $("inner"), svg = $("lines"), mapWrap = $("mapWrap"), popup = $('mob-pop'), chat = $('chat'), sidePop = $('side-pop');
|
const inner = $("inner"), svg = $("lines"), mapWrap = $("mapWrap"), popup = $('mob-pop'), chat = $('chat'), sidePop = $('side-pop');
|
||||||
const rand = () => (seed = (seed * 9301 + 49297) % 233280) / 233280;
|
const rand = () => (seed = (seed * 9301 + 49297) % 233280) / 233280;
|
||||||
|
|
||||||
const getCurInside = () => D.maps?.indoor?.[playerLocation] || null;
|
// 获取当前位置的 inside 数据
|
||||||
|
const getCurInside = () => {
|
||||||
|
return D.maps?.indoor?.[playerLocation] || null;
|
||||||
|
};
|
||||||
|
|
||||||
/* ================== 弹窗拖拽 ================== */
|
// ================== 弹窗拖拽 ==================
|
||||||
const snaps = () => [($('pop-hd')?.offsetHeight || 0), innerHeight * .30, innerHeight * .65];
|
const snaps = () => [($('pop-hd')?.offsetHeight || 0), innerHeight * .30, innerHeight * .65];
|
||||||
let popH = 0, popDrag = false, popSY = 0, popSH = 0, popLv = 1;
|
let popH = 0, popDrag = false, popSY = 0, popSH = 0, popLv = 1;
|
||||||
const inds = popup.querySelectorAll('.pop-h-ind span');
|
const inds = popup.querySelectorAll('.pop-h-ind span');
|
||||||
@@ -804,10 +647,12 @@ const setPopH = h => {
|
|||||||
const snapTo = l => { popLv = Math.max(0, Math.min(2, l)); setPopH(snaps()[popLv]); };
|
const snapTo = l => { popLv = Math.max(0, Math.min(2, l)); setPopH(snaps()[popLv]); };
|
||||||
const openPop = (l = 1) => { popup.classList.add('act'); snapTo(l); };
|
const openPop = (l = 1) => { popup.classList.add('act'); snapTo(l); };
|
||||||
|
|
||||||
|
// 右侧面板
|
||||||
const sideMinW = 8, sideMaxW = () => Math.floor(innerWidth * (isMob() ? 0.8 : 1 / 3));
|
const sideMinW = 8, sideMaxW = () => Math.floor(innerWidth * (isMob() ? 0.8 : 1 / 3));
|
||||||
let sideW = sideMinW, sideDrag = false, sideSX = 0, sideSW = 0;
|
let sideW = sideMinW, sideDrag = false, sideSX = 0, sideSW = 0;
|
||||||
const setSideW = w => { sideW = Math.max(sideMinW, Math.min(sideMaxW(), w)); sidePop.style.width = sideW + 'px'; };
|
const setSideW = w => { sideW = Math.max(sideMinW, Math.min(sideMaxW(), w)); sidePop.style.width = sideW + 'px'; };
|
||||||
|
|
||||||
|
// 拖拽事件
|
||||||
$('pop-hd').onmousedown = e => { popDrag = true; popSY = e.clientY; popSH = popH || snaps()[1]; popup.classList.add('drag'); e.preventDefault(); };
|
$('pop-hd').onmousedown = e => { popDrag = true; popSY = e.clientY; popSH = popH || snaps()[1]; popup.classList.add('drag'); e.preventDefault(); };
|
||||||
$('pop-hd').ontouchstart = e => { e.preventDefault(); popDrag = true; popSY = e.touches[0].clientY; popSH = popH || snaps()[1]; popup.classList.add('drag'); };
|
$('pop-hd').ontouchstart = e => { e.preventDefault(); popDrag = true; popSY = e.touches[0].clientY; popSH = popH || snaps()[1]; popup.classList.add('drag'); };
|
||||||
$('side-pop-handle').onmousedown = e => { sideDrag = true; sideSX = e.clientX; sideSW = sideW; sidePop.classList.add('drag'); e.preventDefault(); };
|
$('side-pop-handle').onmousedown = e => { sideDrag = true; sideSX = e.clientX; sideSW = sideW; sidePop.classList.add('drag'); e.preventDefault(); };
|
||||||
@@ -827,7 +672,7 @@ document.onmouseup = endDrag;
|
|||||||
document.ontouchend = endDrag;
|
document.ontouchend = endDrag;
|
||||||
document.ontouchcancel = endDrag;
|
document.ontouchcancel = endDrag;
|
||||||
|
|
||||||
/* ================== 链接绑定 ================== */
|
// ================== 链接绑定 ==================
|
||||||
const bindLinks = el => el.querySelectorAll('.loc-lk').forEach(l => l.onclick = e => {
|
const bindLinks = el => el.querySelectorAll('.loc-lk').forEach(l => l.onclick = e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const locName = l.dataset.loc;
|
const locName = l.dataset.loc;
|
||||||
@@ -845,9 +690,11 @@ const bindLinks = el => el.querySelectorAll('.loc-lk').forEach(l => l.onclick =
|
|||||||
});
|
});
|
||||||
|
|
||||||
const bindFold = el => el.querySelector('.fold-h').onclick = () => el.classList.toggle('exp');
|
const bindFold = el => el.querySelector('.fold-h').onclick = () => el.classList.toggle('exp');
|
||||||
|
|
||||||
|
// ================== 弹窗通用关闭 ==================
|
||||||
$$('.modal-bd,.modal-x,.m-cancel').forEach(el => el.onclick = () => el.closest('.modal').classList.remove('act'));
|
$$('.modal-bd,.modal-x,.m-cancel').forEach(el => el.onclick = () => el.closest('.modal').classList.remove('act'));
|
||||||
|
|
||||||
/* ================== 聊天功能 ================== */
|
// ================== 聊天功能 ==================
|
||||||
const openChat = c => {
|
const openChat = c => {
|
||||||
chatTgt = c;
|
chatTgt = c;
|
||||||
$('chat-av').textContent = c.avatar;
|
$('chat-av').textContent = c.avatar;
|
||||||
@@ -935,7 +782,7 @@ const chatIn = $('chat-in');
|
|||||||
['keydown', 'keypress', 'keyup'].forEach(e => chatIn.addEventListener(e, ev => ev.stopPropagation()));
|
['keydown', 'keypress', 'keyup'].forEach(e => chatIn.addEventListener(e, ev => ev.stopPropagation()));
|
||||||
chatIn.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); } });
|
chatIn.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); } });
|
||||||
|
|
||||||
/* ================== 邀请功能 ================== */
|
// ================== 邀请功能 ==================
|
||||||
const openInv = c => {
|
const openInv = c => {
|
||||||
invTgt = c; selLoc = null;
|
invTgt = c; selLoc = null;
|
||||||
$('inv-t').textContent = `邀请:${c.name}`;
|
$('inv-t').textContent = `邀请:${c.name}`;
|
||||||
@@ -954,7 +801,7 @@ $('inv-ok').onclick = () => {
|
|||||||
post('SEND_INVITE', { requestId: id, contactName: invTgt.name, contactUid: invTgt.worldbookUid, targetLocation: loc, smsHistory: (invTgt.messages || []).map(m => m.type === 'sent' ? `{{user}}: ${m.text}` : `${invTgt.name}: ${m.text}`).join('\n') });
|
post('SEND_INVITE', { requestId: id, contactName: invTgt.name, contactUid: invTgt.worldbookUid, targetLocation: loc, smsHistory: (invTgt.messages || []).map(m => m.type === 'sent' ? `{{user}}: ${m.text}` : `${invTgt.name}: ${m.text}`).join('\n') });
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ================== 添加联络人 ================== */
|
// ================== 添加联络人 ==================
|
||||||
let addCtState = { uid: '', name: '', keys: [] };
|
let addCtState = { uid: '', name: '', keys: [] };
|
||||||
const resetAddCt = () => {
|
const resetAddCt = () => {
|
||||||
addCtState = { uid: '', name: '', keys: [] };
|
addCtState = { uid: '', name: '', keys: [] };
|
||||||
@@ -989,7 +836,7 @@ $('add-ct-ok').onclick = () => {
|
|||||||
render();
|
render();
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ================== 陌路人生成NPC ================== */
|
// ================== 陌路人生成NPC ==================
|
||||||
const genAddCt = (name, info, btn) => {
|
const genAddCt = (name, info, btn) => {
|
||||||
BtnState.load(btn, '检查中');
|
BtnState.load(btn, '检查中');
|
||||||
const id = Req.create('stgwb');
|
const id = Req.create('stgwb');
|
||||||
@@ -1004,7 +851,7 @@ $('btn-refresh-strangers').onclick = () => {
|
|||||||
post('EXTRACT_STRANGERS', { requestId: id, existingContacts: D.contacts.contacts, existingStrangers: D.contacts.strangers });
|
post('EXTRACT_STRANGERS', { requestId: id, existingContacts: D.contacts.contacts, existingStrangers: D.contacts.strangers });
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ================== 世界生成与推演 ================== */
|
// ================== 世界生成与推演 ==================
|
||||||
$('world-gen-ok').onclick = () => {
|
$('world-gen-ok').onclick = () => {
|
||||||
const btn = $('world-gen-ok'), st = $('world-gen-status');
|
const btn = $('world-gen-ok'), st = $('world-gen-status');
|
||||||
BtnState.load(btn, '生成中');
|
BtnState.load(btn, '生成中');
|
||||||
@@ -1024,7 +871,7 @@ $('world-sim-ok').onclick = () => {
|
|||||||
$('btn-deduce').onclick = () => openM('m-world-gen');
|
$('btn-deduce').onclick = () => openM('m-world-gen');
|
||||||
$('btn-simulate').onclick = () => openM('m-world-sim');
|
$('btn-simulate').onclick = () => openM('m-world-sim');
|
||||||
|
|
||||||
/* ================== 侧边菜单 ================== */
|
// ================== 侧边菜单 ==================
|
||||||
$('btn-side-menu-toggle').onclick = () => {
|
$('btn-side-menu-toggle').onclick = () => {
|
||||||
const p = $('side-menu-panel'), btn = $('btn-side-menu-toggle');
|
const p = $('side-menu-panel'), btn = $('btn-side-menu-toggle');
|
||||||
p.classList.toggle('show');
|
p.classList.toggle('show');
|
||||||
@@ -1036,7 +883,7 @@ document.addEventListener('click', e => {
|
|||||||
$('btn-side-menu-toggle')?.classList.remove('act');
|
$('btn-side-menu-toggle')?.classList.remove('act');
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ================== 场景切换 ================== */
|
// ================== 场景切换 ==================
|
||||||
function canonicalLoc(s) { return String(s || '').trim().replace(/^\u90ae\u8f6e/, ''); }
|
function canonicalLoc(s) { return String(s || '').trim().replace(/^\u90ae\u8f6e/, ''); }
|
||||||
const getWaitingContacts = loc => {
|
const getWaitingContacts = loc => {
|
||||||
const target = canonicalLoc(loc);
|
const target = canonicalLoc(loc);
|
||||||
@@ -1081,7 +928,7 @@ $('goto-ok').onclick = () => {
|
|||||||
post('SCENE_SWITCH', { requestId: id, prevLocationName: prev.name, prevLocationInfo: prev.info, targetLocationName: curNode.name, targetLocationType: tt, targetLocationInfo: curNode.data?.info || '', playerAction: $('goto-task').value || '' });
|
post('SCENE_SWITCH', { requestId: id, prevLocationName: prev.name, prevLocationInfo: prev.info, targetLocationName: curNode.name, targetLocationType: tt, targetLocationInfo: curNode.data?.info || '', playerAction: $('goto-task').value || '' });
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ================== 局部地图生成/刷新 ================== */
|
// ================== 局部地图生成/刷新 ==================
|
||||||
$('btn-gen-local-map').onclick = () => {
|
$('btn-gen-local-map').onclick = () => {
|
||||||
const btn = $('btn-gen-local-map');
|
const btn = $('btn-gen-local-map');
|
||||||
BtnState.load(btn, '生成中');
|
BtnState.load(btn, '生成中');
|
||||||
@@ -1114,12 +961,13 @@ $('btn-gen-local-scene').onclick = () => {
|
|||||||
post('GENERATE_LOCAL_SCENE', { requestId: id, locationName: playerLocation, locationInfo });
|
post('GENERATE_LOCAL_SCENE', { requestId: id, locationName: playerLocation, locationInfo });
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ================== 保存数据 ================== */
|
// ================== 保存数据 ==================
|
||||||
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 });
|
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 });
|
||||||
|
|
||||||
/* ================== 设置相关 ================== */
|
// ================== 设置相关 ==================
|
||||||
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 : []); }]];
|
||||||
let gSet = { apiUrl: '', apiKey: '', model: '', mode: 'assist' }, dataCk = {}, editCtx = null, commSet = { historyCount: 50, npcPosition: 0, npcOrder: 100 };
|
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 }, promptSources = {}, promptTemplates = {}, promptDefaults = { jsonTemplates: {}, promptSources: {} };
|
||||||
|
|
||||||
const reqSet = () => post('GET_SETTINGS');
|
const reqSet = () => post('GET_SETTINGS');
|
||||||
|
|
||||||
@@ -1129,20 +977,52 @@ 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 = () => {
|
||||||
|
$('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('');
|
||||||
|
$$('#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); });
|
||||||
|
$$('#prompt-list .data-edit').forEach(b => b.onclick = e => { e.stopPropagation(); openPromptEdit(b.dataset.k, b.dataset.t); });
|
||||||
|
};
|
||||||
|
|
||||||
|
const unescapePromptStr = s => String(s || '').replace(/\\\\/g, '\\').replace(/\\t/g, ' ').replace(/\\n/g, '\n');
|
||||||
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('空内容');
|
||||||
try { return JSON.parse(str); } catch { }
|
try { return JSON.parse(str); } catch { }
|
||||||
|
|
||||||
const fenced = str.match(/```[^\n]*\n([\s\S]*?)\n```/);
|
const fenced = str.match(/```[^\n]*\n([\s\S]*?)\n```/);
|
||||||
if (fenced?.[1]) { try { return JSON.parse(fenced[1].trim()); } catch { } }
|
if (fenced?.[1]) {
|
||||||
const sliceBetween = (open, close) => { const s = str.indexOf(open); const e = str.lastIndexOf(close); if (s === -1 || e === -1 || e <= s) return null; return str.slice(s, e + 1); };
|
const inner = fenced[1].trim();
|
||||||
const objStr = sliceBetween('{', '}') ?? sliceBetween('[', ']');
|
try { return JSON.parse(inner); } catch { }
|
||||||
if (objStr) return JSON.parse(objStr);
|
}
|
||||||
return JSON.parse(str);
|
|
||||||
|
const sliceBetween = (open, close) => {
|
||||||
|
const s = str.indexOf(open);
|
||||||
|
const e = str.lastIndexOf(close);
|
||||||
|
if (s === -1 || e === -1 || e <= s) return null;
|
||||||
|
return str.slice(s, e + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setEditContent = (title, val) => { $('data-edit-title').textContent = title; $('data-edit-ta').value = val; $('data-edit-err').classList.remove('vis'); openM('m-data-edit'); };
|
const objStr = sliceBetween('{', '}') ?? sliceBetween('[', ']');
|
||||||
|
if (objStr) return JSON.parse(objStr);
|
||||||
|
|
||||||
|
return JSON.parse(str);
|
||||||
|
};
|
||||||
|
const updateEditPreview = () => {
|
||||||
|
const p = $('data-edit-preview');
|
||||||
|
if (!p || editCtx?.type !== 'prompt') { p.style.display = 'none'; p.textContent = ''; return; }
|
||||||
|
p.style.display = 'block';
|
||||||
|
const raw = $('data-edit-ta').value || '';
|
||||||
|
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 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;
|
||||||
@@ -1159,11 +1039,24 @@ $('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;
|
||||||
} catch (e) { $('data-edit-err').textContent = `JSON错误: ${e.message}`; $('data-edit-err').classList.add('vis'); }
|
} catch (e) { $('data-edit-err').textContent = `JSON错误: ${e.message}`; $('data-edit-err').classList.add('vis'); }
|
||||||
};
|
};
|
||||||
|
$('data-edit-ta').addEventListener('input', updateEditPreview);
|
||||||
|
|
||||||
const showTestRes = (ok, m) => { const r = $('test-res'); r.textContent = m; r.className = 'set-test-res ' + (ok ? 'ok' : 'err'); };
|
const showTestRes = (ok, m) => { const r = $('test-res'); r.textContent = m; r.className = 'set-test-res ' + (ok ? 'ok' : 'err'); };
|
||||||
const showResultModal = (title, msg, isError = false, record = null) => {
|
const showResultModal = (title, msg, isError = false, record = null) => {
|
||||||
@@ -1196,7 +1089,7 @@ $('btn-settings').onclick = () => {
|
|||||||
$('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();
|
||||||
loadTemplate(templateState.currentType);
|
renderPromptList();
|
||||||
openM('m-settings');
|
openM('m-settings');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1204,15 +1097,6 @@ $('btn-fetch-models').onclick = () => { BtnState.load($('btn-fetch-models'), '
|
|||||||
$('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() }); };
|
||||||
|
|
||||||
$('set-save').onclick = () => {
|
$('set-save').onclick = () => {
|
||||||
const type = templateState.currentType;
|
|
||||||
templateState.prompts[type] = {
|
|
||||||
u1: $('tpl-u1').value,
|
|
||||||
a1: $('tpl-a1').value,
|
|
||||||
u2: $('tpl-u2').value,
|
|
||||||
a2: $('tpl-a2').value
|
|
||||||
};
|
|
||||||
templateState.jsonTemplates[type] = $('tpl-json').value;
|
|
||||||
post('SAVE_PROMPTS', { promptConfig: { prompts: templateState.prompts, jsonTemplates: templateState.jsonTemplates } });
|
|
||||||
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));
|
||||||
@@ -1226,7 +1110,7 @@ $('set-save').onclick = () => {
|
|||||||
};
|
};
|
||||||
$('btn-close').onclick = () => post('CLOSE_PANEL');
|
$('btn-close').onclick = () => post('CLOSE_PANEL');
|
||||||
|
|
||||||
/* ================== 消息处理 ================== */
|
// ================== 消息处理 ==================
|
||||||
window.addEventListener('message', e => {
|
window.addEventListener('message', e => {
|
||||||
if (e.data?.source !== 'LittleWhiteBox') return;
|
if (e.data?.source !== 'LittleWhiteBox') return;
|
||||||
const d = e.data, t = d.type;
|
const d = e.data, t = d.type;
|
||||||
@@ -1240,11 +1124,7 @@ window.addEventListener('message', e => {
|
|||||||
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 };
|
||||||
if (d.dataChecked) dataCk = d.dataChecked;
|
if (d.dataChecked) dataCk = d.dataChecked;
|
||||||
if (d.promptConfig) {
|
if (d.promptConfig) { promptTemplates = d.promptConfig.current?.jsonTemplates || {}; promptSources = d.promptConfig.current?.promptSources || {}; promptDefaults = d.promptConfig.defaults || promptDefaults; }
|
||||||
templateState.prompts = d.promptConfig.current?.prompts || {};
|
|
||||||
templateState.jsonTemplates = d.promptConfig.current?.jsonTemplates || {};
|
|
||||||
templateState.defaults = d.promptConfig.defaults || { prompts: {}, jsonTemplates: {} };
|
|
||||||
}
|
|
||||||
if (d.outlineData) {
|
if (d.outlineData) {
|
||||||
const o = d.outlineData;
|
const o = d.outlineData;
|
||||||
if (o.meta) D.meta = o.meta;
|
if (o.meta) D.meta = o.meta;
|
||||||
@@ -1255,10 +1135,12 @@ window.addEventListener('message', e => {
|
|||||||
if (o.strangers) D.contacts.strangers = o.strangers;
|
if (o.strangers) D.contacts.strangers = o.strangers;
|
||||||
if (o.contacts) D.contacts.contacts = o.contacts;
|
if (o.contacts) D.contacts.contacts = o.contacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const h = d.characterContactSmsHistory || {};
|
const h = d.characterContactSmsHistory || {};
|
||||||
charSmsHistory = { messages: Array.isArray(h.messages) ? h.messages : [], summarizedCount: h.summarizedCount || 0, summaries: h.summaries || {} };
|
charSmsHistory = { messages: Array.isArray(h.messages) ? h.messages : [], summarizedCount: h.summarizedCount || 0, summaries: h.summaries || {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
let charContact = D.contacts.contacts.find(c => c.worldbookUid === '__CHARACTER_CARD__');
|
let charContact = D.contacts.contacts.find(c => c.worldbookUid === '__CHARACTER_CARD__');
|
||||||
if (!charContact) {
|
if (!charContact) {
|
||||||
charContact = D.contacts.contacts.find(c => !c.worldbookUid && c.name === '炒饭智能');
|
charContact = D.contacts.contacts.find(c => !c.worldbookUid && c.name === '炒饭智能');
|
||||||
@@ -1290,17 +1172,10 @@ window.addEventListener('message', e => {
|
|||||||
$('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();
|
||||||
loadTemplate(templateState.currentType);
|
renderPromptList();
|
||||||
}
|
}
|
||||||
} else if (t === 'PROMPT_CONFIG_UPDATED') {
|
} else if (t === 'PROMPT_CONFIG_UPDATED') {
|
||||||
if (d.promptConfig) {
|
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(); }
|
||||||
templateState.prompts = d.promptConfig.current?.prompts || {};
|
|
||||||
templateState.jsonTemplates = d.promptConfig.current?.jsonTemplates || {};
|
|
||||||
templateState.defaults = d.promptConfig.defaults || templateState.defaults;
|
|
||||||
if ($('m-settings').classList.contains('act')) {
|
|
||||||
loadTemplate(templateState.currentType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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');
|
||||||
@@ -1489,7 +1364,10 @@ window.addEventListener('message', e => {
|
|||||||
if (d.success && d.sceneData) {
|
if (d.success && d.sceneData) {
|
||||||
const sc = d.sceneData;
|
const sc = d.sceneData;
|
||||||
if (typeof sc.newScore === 'number') D.deviationScore = sc.newScore;
|
if (typeof sc.newScore === 'number') D.deviationScore = sc.newScore;
|
||||||
if (sc.localMap) { D.maps.indoor = D.maps.indoor || {}; D.maps.indoor[node.name] = sc.localMap; }
|
if (sc.localMap) {
|
||||||
|
D.maps.indoor = D.maps.indoor || {};
|
||||||
|
D.maps.indoor[node.name] = sc.localMap;
|
||||||
|
}
|
||||||
if (sc.strangers?.length) {
|
if (sc.strangers?.length) {
|
||||||
const ex = new Set((D.contacts.strangers || []).map(s => s.name));
|
const ex = new Set((D.contacts.strangers || []).map(s => s.name));
|
||||||
const nw = sc.strangers.filter(s => !ex.has(s.name));
|
const nw = sc.strangers.filter(s => !ex.has(s.name));
|
||||||
@@ -1587,18 +1465,21 @@ window.addEventListener('message', e => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ================== 渲染 ================== */
|
// ================== 渲染 ==================
|
||||||
function render() {
|
function render() {
|
||||||
|
// 新闻
|
||||||
const news = D.world?.news || [];
|
const news = D.world?.news || [];
|
||||||
$('news-list').innerHTML = news.length ? news.map(n => `<div class="fold"><div class="fold-h"><div><div class="news-t">${n.title}</div><div class="news-time">${n.time || ''}</div></div><i class="fa-solid fa-chevron-down fold-a"></i></div><div class="fold-b news-b"><p>${n.content}</p></div></div>`).join('') : '<div class="empty">暂无新闻</div>';
|
$('news-list').innerHTML = news.length ? news.map(n => `<div class="fold"><div class="fold-h"><div><div class="news-t">${n.title}</div><div class="news-time">${n.time || ''}</div></div><i class="fa-solid fa-chevron-down fold-a"></i></div><div class="fold-b news-b"><p>${n.content}</p></div></div>`).join('') : '<div class="empty">暂无新闻</div>';
|
||||||
$$('#news-list .fold').forEach(bindFold);
|
$$('#news-list .fold').forEach(bindFold);
|
||||||
|
|
||||||
|
// 用户指南
|
||||||
const ug = D.meta?.user_guide;
|
const ug = D.meta?.user_guide;
|
||||||
if (ug) {
|
if (ug) {
|
||||||
$('ug-state').textContent = ug.current_state || '未知状态';
|
$('ug-state').textContent = ug.current_state || '未知状态';
|
||||||
$('ug-actions').innerHTML = (ug.guides || []).map((g, i) => `<div class="user-guide-action" data-idx="${i}">${i + 1}. ${g}</div>`).join('') || '<div class="user-guide-action">暂无行动指南</div>';
|
$('ug-actions').innerHTML = (ug.guides || []).map((g, i) => `<div class="user-guide-action" data-idx="${i}">${i + 1}. ${g}</div>`).join('') || '<div class="user-guide-action">暂无行动指南</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 联系人
|
||||||
const renderCt = (list, isS) => (list || []).length ? list.map(p => `<div class="fold" data-name="${p.name || ''}" data-info="${(p.info || '').replace(/"/g, '"')}" data-uid="${p.worldbookUid || ''}"><div class="fold-h ct-hd fc"><div class="ct-av" style="background:${p.color}">${p.avatar}</div><div class="ct-info"><div class="ct-name">${p.name}</div><div class="ct-st">${p.online ? '● 在线' : p.location}</div></div><i class="fa-solid fa-chevron-down fold-a"></i></div><div class="fold-b"><div class="ct-det">${p.info ? `<div class="ct-info-text">${p.info}</div>` : ''}<div class="ct-acts">${isS ? `<button class="btn btn-s btn-p fc add-btn" data-name="${p.name || ''}" data-info="${(p.info || '').replace(/"/g, '"')}"><i class="fa-solid fa-user-plus"></i> 添加</button><button class="btn btn-s fc ignore-btn" data-name="${p.name || ''}"><i class="fa-solid fa-eye-slash"></i> 忽略</button>` : `<button class="btn btn-s fc msg-btn" data-uid="${p.worldbookUid || ''}"><i class="fa-solid fa-message"></i> 短信</button><button class="btn btn-s btn-p fc inv-btn" data-uid="${p.worldbookUid || ''}"><i class="fa-solid fa-paper-plane"></i> 邀请</button>`}</div></div></div></div>`).join('') : '<div class="empty">暂无</div>';
|
const renderCt = (list, isS) => (list || []).length ? list.map(p => `<div class="fold" data-name="${p.name || ''}" data-info="${(p.info || '').replace(/"/g, '"')}" data-uid="${p.worldbookUid || ''}"><div class="fold-h ct-hd fc"><div class="ct-av" style="background:${p.color}">${p.avatar}</div><div class="ct-info"><div class="ct-name">${p.name}</div><div class="ct-st">${p.online ? '● 在线' : p.location}</div></div><i class="fa-solid fa-chevron-down fold-a"></i></div><div class="fold-b"><div class="ct-det">${p.info ? `<div class="ct-info-text">${p.info}</div>` : ''}<div class="ct-acts">${isS ? `<button class="btn btn-s btn-p fc add-btn" data-name="${p.name || ''}" data-info="${(p.info || '').replace(/"/g, '"')}"><i class="fa-solid fa-user-plus"></i> 添加</button><button class="btn btn-s fc ignore-btn" data-name="${p.name || ''}"><i class="fa-solid fa-eye-slash"></i> 忽略</button>` : `<button class="btn btn-s fc msg-btn" data-uid="${p.worldbookUid || ''}"><i class="fa-solid fa-message"></i> 短信</button><button class="btn btn-s btn-p fc inv-btn" data-uid="${p.worldbookUid || ''}"><i class="fa-solid fa-paper-plane"></i> 邀请</button>`}</div></div></div></div>`).join('') : '<div class="empty">暂无</div>';
|
||||||
$('sec-stranger').innerHTML = renderCt(D.contacts.strangers, true);
|
$('sec-stranger').innerHTML = renderCt(D.contacts.strangers, true);
|
||||||
$('sec-contact').innerHTML = renderCt(D.contacts.contacts, false);
|
$('sec-contact').innerHTML = renderCt(D.contacts.contacts, false);
|
||||||
@@ -1607,7 +1488,7 @@ function render() {
|
|||||||
$$('.ignore-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const i = D.contacts.strangers.findIndex(s => s.name === b.dataset.name); if (i > -1) { D.contacts.strangers.splice(i, 1); saveCt(); render(); } });
|
$$('.ignore-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const i = D.contacts.strangers.findIndex(s => s.name === b.dataset.name); if (i > -1) { D.contacts.strangers.splice(i, 1); saveCt(); render(); } });
|
||||||
$$('.msg-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const c = D.contacts.contacts.find(x => x.worldbookUid === b.dataset.uid); if (c) openChat(c); });
|
$$('.msg-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const c = D.contacts.contacts.find(x => x.worldbookUid === b.dataset.uid); if (c) openChat(c); });
|
||||||
$$('.inv-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const c = D.contacts.contacts.find(x => x.worldbookUid === b.dataset.uid); if (c) openInv(c); });
|
$$('.inv-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const c = D.contacts.contacts.find(x => x.worldbookUid === b.dataset.uid); if (c) openInv(c); });
|
||||||
|
// 更新右侧描述面板 - 根据当前视图选择展示内容
|
||||||
if (selectedMapValue === 'current') {
|
if (selectedMapValue === 'current') {
|
||||||
const inside = getCurInside();
|
const inside = getCurInside();
|
||||||
if (inside?.description) {
|
if (inside?.description) {
|
||||||
@@ -1750,6 +1631,7 @@ const hideInfo = () => { curNode = null; inner.querySelectorAll('.item').forEach
|
|||||||
$('info-bk').onclick = hideInfo;
|
$('info-bk').onclick = hideInfo;
|
||||||
$('mob-info-bk').onclick = () => popup.classList.remove('act');
|
$('mob-info-bk').onclick = () => popup.classList.remove('act');
|
||||||
|
|
||||||
|
// 地图选单
|
||||||
function renderMapSelector() {
|
function renderMapSelector() {
|
||||||
const sel = $('map-lbl-select');
|
const sel = $('map-lbl-select');
|
||||||
sel.innerHTML = '<option value="overview">🗺️ 大地图</option>';
|
sel.innerHTML = '<option value="overview">🗺️ 大地图</option>';
|
||||||
@@ -1825,7 +1707,7 @@ function switchMapView(value) {
|
|||||||
}
|
}
|
||||||
$('map-lbl-select').onchange = e => switchMapView(e.target.value);
|
$('map-lbl-select').onchange = e => switchMapView(e.target.value);
|
||||||
|
|
||||||
/* ================== 地图交互 ================== */
|
// 地图交互
|
||||||
let sx, sy, lastDist = 0, lastCX = 0, lastCY = 0;
|
let sx, sy, lastDist = 0, lastCX = 0, lastCY = 0;
|
||||||
mapWrap.onmousedown = e => { if (anim || e.target.closest('.map-act,.map-lbl')) return; if (!e.target.classList.contains('item')) hideInfo(); drag = true; sx = e.clientX; sy = e.clientY; mapWrap.style.cursor = 'grabbing'; };
|
mapWrap.onmousedown = e => { if (anim || e.target.closest('.map-act,.map-lbl')) return; if (!e.target.classList.contains('item')) hideInfo(); drag = true; sx = e.clientX; sy = e.clientY; mapWrap.style.cursor = 'grabbing'; };
|
||||||
mapWrap.onmousemove = e => { if (!drag) return; offX += e.clientX - sx; offY += e.clientY - sy; sx = e.clientX; sy = e.clientY; updateTf(); };
|
mapWrap.onmousemove = e => { if (!drag) return; offX += e.clientX - sx; offY += e.clientY - sy; sx = e.clientX; sy = e.clientY; updateTf(); };
|
||||||
@@ -1859,7 +1741,7 @@ mapWrap.ontouchend = e => {
|
|||||||
drag = false;
|
drag = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ================== 导航 ================== */
|
// 导航
|
||||||
$$('.nav-i').forEach(i => i.onclick = () => {
|
$$('.nav-i').forEach(i => i.onclick = () => {
|
||||||
$$('.nav-i').forEach(n => n.classList.remove('act'));
|
$$('.nav-i').forEach(n => n.classList.remove('act'));
|
||||||
$$('.page').forEach(p => p.classList.remove('act'));
|
$$('.page').forEach(p => p.classList.remove('act'));
|
||||||
@@ -1880,23 +1762,12 @@ $$('.comm-tab').forEach(t => t.onclick = () => {
|
|||||||
$('btn-goto').onclick = e => { e.stopPropagation(); if (curNode) { $('goto-d').textContent = `目的地:${curNode.name}`; $('goto-task').value = ''; openM('m-goto'); } };
|
$('btn-goto').onclick = e => { e.stopPropagation(); if (curNode) { $('goto-d').textContent = `目的地:${curNode.name}`; $('goto-task').value = ''; openM('m-goto'); } };
|
||||||
addEventListener('resize', () => requestAnimationFrame(drawLines));
|
addEventListener('resize', () => requestAnimationFrame(drawLines));
|
||||||
|
|
||||||
/* ================== 初始化 ================== */
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
render();
|
render();
|
||||||
initPos();
|
initPos();
|
||||||
sidePop.classList.add('show');
|
sidePop.classList.add('show');
|
||||||
setSideW(sideMaxW());
|
setSideW(sideMaxW());
|
||||||
if (isMob()) openPop(1);
|
if (isMob()) openPop(1);
|
||||||
|
|
||||||
$('template-type-select').onchange = e => loadTemplate(e.target.value);
|
|
||||||
$('tpl-save').onclick = saveCurrentTemplate;
|
|
||||||
$('tpl-restore').onclick = restoreCurrentTemplate;
|
|
||||||
|
|
||||||
['tpl-u1', 'tpl-a1', 'tpl-u2', 'tpl-a2'].forEach(id => {
|
|
||||||
const ta = $(id);
|
|
||||||
if (ta) ta.oninput = function() { this.style.height = 'auto'; this.style.height = Math.max(this.scrollHeight, 60) + 'px'; };
|
|
||||||
});
|
|
||||||
|
|
||||||
post('FRAME_READY');
|
post('FRAME_READY');
|
||||||
setTimeout(() => { if (selectedMapValue === 'current') switchMapView('current'); }, 100);
|
setTimeout(() => { if (selectedMapValue === 'current') switchMapView('current'); }, 100);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,10 +2,24 @@
|
|||||||
* ============================================================================
|
* ============================================================================
|
||||||
* Story Outline 模块 - 小白板
|
* Story Outline 模块 - 小白板
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
|
* 功能:生成和管理RPG式剧情世界,提供地图导航、NPC管理、短信系统、世界推演
|
||||||
|
*
|
||||||
|
* 分区:
|
||||||
|
* 1. 导入与常量
|
||||||
|
* 2. 通用工具
|
||||||
|
* 3. JSON解析
|
||||||
|
* 4. 存储管理
|
||||||
|
* 5. LLM调用
|
||||||
|
* 6. 世界书操作
|
||||||
|
* 7. 剧情注入
|
||||||
|
* 8. iframe通讯
|
||||||
|
* 9. 请求处理器
|
||||||
|
* 10. UI管理
|
||||||
|
* 11. 事件与初始化
|
||||||
|
* ============================================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ==================== 1. 导入与常量 ====================
|
// ==================== 1. 导入与常量 ====================
|
||||||
|
|
||||||
import { extension_settings, saveMetadataDebounced } from "../../../../../extensions.js";
|
import { extension_settings, saveMetadataDebounced } from "../../../../../extensions.js";
|
||||||
import { chat_metadata, name1, processCommands, eventSource, event_types as st_event_types } from "../../../../../../script.js";
|
import { chat_metadata, name1, processCommands, eventSource, event_types as st_event_types } from "../../../../../../script.js";
|
||||||
import { loadWorldInfo, saveWorldInfo, world_names, world_info } from "../../../../../world-info.js";
|
import { loadWorldInfo, saveWorldInfo, world_names, world_info } from "../../../../../world-info.js";
|
||||||
@@ -25,59 +39,53 @@ import {
|
|||||||
const events = createModuleEvents('storyOutline');
|
const events = createModuleEvents('storyOutline');
|
||||||
const IFRAME_PATH = `${extensionFolderPath}/modules/story-outline/story-outline.html`;
|
const IFRAME_PATH = `${extensionFolderPath}/modules/story-outline/story-outline.html`;
|
||||||
const STORAGE_KEYS = { global: 'LittleWhiteBox_StoryOutline_GlobalSettings', comm: 'LittleWhiteBox_StoryOutline_CommSettings' };
|
const STORAGE_KEYS = { global: 'LittleWhiteBox_StoryOutline_GlobalSettings', comm: 'LittleWhiteBox_StoryOutline_CommSettings' };
|
||||||
const SIZE_STORAGE_KEY = 'LittleWhiteBox_StoryOutline_Size';
|
|
||||||
const STORY_OUTLINE_ID = 'lwb_story_outline';
|
const STORY_OUTLINE_ID = 'lwb_story_outline';
|
||||||
const CHAR_CARD_UID = '__CHARACTER_CARD__';
|
const CHAR_CARD_UID = '__CHARACTER_CARD__';
|
||||||
const DEBUG_KEY = 'LittleWhiteBox_StoryOutline_Debug';
|
const DEBUG_KEY = 'LittleWhiteBox_StoryOutline_Debug';
|
||||||
|
|
||||||
let overlayCreated = false, frameReady = false, currentMesId = null, pendingMsgs = [], presetCleanup = null, step1Cache = null;
|
let overlayCreated = false, frameReady = false, currentMesId = null, pendingMsgs = [], presetCleanup = null, step1Cache = null;
|
||||||
let iframeLoaded = false;
|
|
||||||
|
|
||||||
// ==================== 2. 通用工具 ====================
|
// ==================== 2. 通用工具 ====================
|
||||||
|
|
||||||
|
/** 移动端检测 */
|
||||||
const isMobile = () => window.innerWidth < 550;
|
const isMobile = () => window.innerWidth < 550;
|
||||||
|
|
||||||
|
/** 安全执行函数 */
|
||||||
const safe = fn => { try { return fn(); } catch { return null; } };
|
const safe = fn => { try { return fn(); } catch { return null; } };
|
||||||
const isDebug = () => { try { return localStorage.getItem(DEBUG_KEY) === '1'; } catch { return false; } };
|
const isDebug = () => {
|
||||||
|
try { return localStorage.getItem(DEBUG_KEY) === '1'; } catch { return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
/** localStorage读写 */
|
||||||
const getStore = (k, def) => safe(() => JSON.parse(localStorage.getItem(k))) || def;
|
const getStore = (k, def) => safe(() => JSON.parse(localStorage.getItem(k))) || def;
|
||||||
const setStore = (k, v) => safe(() => localStorage.setItem(k, JSON.stringify(v)));
|
const setStore = (k, v) => safe(() => localStorage.setItem(k, JSON.stringify(v)));
|
||||||
|
|
||||||
|
/** 随机范围 */
|
||||||
const randRange = (a, b) => Math.floor(Math.random() * (b - a + 1)) + a;
|
const randRange = (a, b) => Math.floor(Math.random() * (b - a + 1)) + a;
|
||||||
|
|
||||||
const getStoredSize = (isMob) => {
|
/**
|
||||||
try {
|
* 修复单个 JSON 字符串的语法问题
|
||||||
const data = JSON.parse(localStorage.getItem(SIZE_STORAGE_KEY) || '{}');
|
* 仅在已提取的候选上调用,不做全局破坏性操作
|
||||||
return isMob ? data.mobile : data.desktop;
|
*/
|
||||||
} catch { return null; }
|
|
||||||
};
|
|
||||||
|
|
||||||
const setStoredSize = (isMob, size) => {
|
|
||||||
try {
|
|
||||||
if (!size) return;
|
|
||||||
const data = JSON.parse(localStorage.getItem(SIZE_STORAGE_KEY) || '{}');
|
|
||||||
if (isMob) {
|
|
||||||
if (Number.isFinite(size.height) && size.height > 44) {
|
|
||||||
data.mobile = { height: size.height };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data.desktop = {};
|
|
||||||
if (Number.isFinite(size.width) && size.width > 300) data.desktop.width = size.width;
|
|
||||||
if (Number.isFinite(size.height) && size.height > 200) data.desktop.height = size.height;
|
|
||||||
}
|
|
||||||
localStorage.setItem(SIZE_STORAGE_KEY, JSON.stringify(data));
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 3. JSON解析 ====================
|
|
||||||
|
|
||||||
function fixJson(s) {
|
function fixJson(s) {
|
||||||
if (!s || typeof s !== 'string') return s;
|
if (!s || typeof s !== 'string') return s;
|
||||||
|
|
||||||
let r = s.trim()
|
let r = s.trim()
|
||||||
|
// 统一引号:只转换弯引号
|
||||||
.replace(/[""]/g, '"').replace(/['']/g, "'")
|
.replace(/[""]/g, '"').replace(/['']/g, "'")
|
||||||
|
// 修复键名后的错误引号:如 "key': → "key":
|
||||||
.replace(/"([^"']+)'[\s]*:/g, '"$1":')
|
.replace(/"([^"']+)'[\s]*:/g, '"$1":')
|
||||||
.replace(/'([^"']+)"[\s]*:/g, '"$1":')
|
.replace(/'([^"']+)"[\s]*:/g, '"$1":')
|
||||||
|
// 修复单引号包裹的完整值:: 'value' → : "value"
|
||||||
.replace(/:[\s]*'([^']*)'[\s]*([,}\]])/g, ':"$1"$2')
|
.replace(/:[\s]*'([^']*)'[\s]*([,}\]])/g, ':"$1"$2')
|
||||||
|
// 修复无引号的键名
|
||||||
.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
|
.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
|
||||||
|
// 移除尾随逗号
|
||||||
.replace(/,[\s\n]*([}\]])/g, '$1')
|
.replace(/,[\s\n]*([}\]])/g, '$1')
|
||||||
|
// 修复 undefined 和 NaN
|
||||||
.replace(/:\s*undefined\b/g, ': null').replace(/:\s*NaN\b/g, ': null');
|
.replace(/:\s*undefined\b/g, ': null').replace(/:\s*NaN\b/g, ': null');
|
||||||
|
|
||||||
|
// 补全未闭合的括号
|
||||||
let braces = 0, brackets = 0, inStr = false, esc = false;
|
let braces = 0, brackets = 0, inStr = false, esc = false;
|
||||||
for (const c of r) {
|
for (const c of r) {
|
||||||
if (esc) { esc = false; continue; }
|
if (esc) { esc = false; continue; }
|
||||||
@@ -93,8 +101,17 @@ function fixJson(s) {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从输入中提取 JSON(非破坏性扫描版)
|
||||||
|
* 策略:
|
||||||
|
* 1. 直接在原始字符串中扫描所有 {...} 结构
|
||||||
|
* 2. 对每个候选单独清洗和解析
|
||||||
|
* 3. 按有效属性评分,返回最佳结果
|
||||||
|
*/
|
||||||
function extractJson(input, isArray = false) {
|
function extractJson(input, isArray = false) {
|
||||||
if (!input) return null;
|
if (!input) return null;
|
||||||
|
|
||||||
|
// 处理已经是对象的输入
|
||||||
if (typeof input === 'object' && input !== null) {
|
if (typeof input === 'object' && input !== null) {
|
||||||
if (isArray && Array.isArray(input)) return input;
|
if (isArray && Array.isArray(input)) return input;
|
||||||
if (!isArray && !Array.isArray(input)) {
|
if (!isArray && !Array.isArray(input)) {
|
||||||
@@ -106,21 +123,33 @@ function extractJson(input, isArray = false) {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预处理:只做最基本的清理
|
||||||
const str = String(input).trim()
|
const str = String(input).trim()
|
||||||
.replace(/^\uFEFF/, '')
|
.replace(/^\uFEFF/, '')
|
||||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
||||||
.replace(/\r\n?/g, '\n');
|
.replace(/\r\n?/g, '\n');
|
||||||
if (!str) return null;
|
if (!str) return null;
|
||||||
|
|
||||||
const tryParse = s => { try { return JSON.parse(s); } catch { return null; } };
|
const tryParse = s => { try { return JSON.parse(s); } catch { return null; } };
|
||||||
const ok = (o, arr) => o != null && (arr ? Array.isArray(o) : typeof o === 'object' && !Array.isArray(o));
|
const ok = (o, arr) => o != null && (arr ? Array.isArray(o) : typeof o === 'object' && !Array.isArray(o));
|
||||||
|
|
||||||
|
// 评分函数:meta=10, world/maps=5, 其他=3
|
||||||
const score = o => (o?.meta ? 10 : 0) + (o?.world ? 5 : 0) + (o?.maps ? 5 : 0) +
|
const score = o => (o?.meta ? 10 : 0) + (o?.world ? 5 : 0) + (o?.maps ? 5 : 0) +
|
||||||
(o?.truth ? 3 : 0) + (o?.onion_layers ? 3 : 0) + (o?.atmosphere ? 3 : 0) + (o?.trajectory ? 3 : 0) + (o?.user_guide ? 3 : 0);
|
(o?.truth ? 3 : 0) + (o?.onion_layers ? 3 : 0) + (o?.atmosphere ? 3 : 0) + (o?.trajectory ? 3 : 0) + (o?.user_guide ? 3 : 0);
|
||||||
|
|
||||||
|
// 1. 直接尝试解析(最理想情况)
|
||||||
let r = tryParse(str);
|
let r = tryParse(str);
|
||||||
if (ok(r, isArray) && score(r) > 0) return r;
|
if (ok(r, isArray) && score(r) > 0) return r;
|
||||||
|
|
||||||
|
// 2. 扫描所有 {...} 或 [...] 结构
|
||||||
const open = isArray ? '[' : '{';
|
const open = isArray ? '[' : '{';
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
if (str[i] !== open) continue;
|
if (str[i] !== open) continue;
|
||||||
|
|
||||||
|
// 括号匹配找闭合位置
|
||||||
let depth = 0, inStr = false, esc = false;
|
let depth = 0, inStr = false, esc = false;
|
||||||
for (let j = i; j < str.length; j++) {
|
for (let j = i; j < str.length; j++) {
|
||||||
const c = str[j];
|
const c = str[j];
|
||||||
@@ -132,21 +161,29 @@ function extractJson(input, isArray = false) {
|
|||||||
else if (c === '}' || c === ']') depth--;
|
else if (c === '}' || c === ']') depth--;
|
||||||
if (depth === 0) {
|
if (depth === 0) {
|
||||||
candidates.push({ start: i, end: j, text: str.slice(i, j + 1) });
|
candidates.push({ start: i, end: j, text: str.slice(i, j + 1) });
|
||||||
i = j;
|
i = j; // 跳过已处理的部分
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 按长度排序(大的优先,更可能是完整对象)
|
||||||
candidates.sort((a, b) => b.text.length - a.text.length);
|
candidates.sort((a, b) => b.text.length - a.text.length);
|
||||||
|
|
||||||
|
// 4. 尝试解析每个候选,记录最佳结果
|
||||||
let best = null, bestScore = -1;
|
let best = null, bestScore = -1;
|
||||||
|
|
||||||
for (const { text } of candidates) {
|
for (const { text } of candidates) {
|
||||||
|
// 直接解析
|
||||||
r = tryParse(text);
|
r = tryParse(text);
|
||||||
if (ok(r, isArray)) {
|
if (ok(r, isArray)) {
|
||||||
const s = score(r);
|
const s = score(r);
|
||||||
if (s > bestScore) { best = r; bestScore = s; }
|
if (s > bestScore) { best = r; bestScore = s; }
|
||||||
if (s >= 10) return r;
|
if (s >= 10) return r; // 有 meta 就直接返回
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修复后解析
|
||||||
const fixed = fixJson(text);
|
const fixed = fixJson(text);
|
||||||
r = tryParse(fixed);
|
r = tryParse(fixed);
|
||||||
if (ok(r, isArray)) {
|
if (ok(r, isArray)) {
|
||||||
@@ -155,7 +192,11 @@ function extractJson(input, isArray = false) {
|
|||||||
if (s >= 10) return r;
|
if (s >= 10) return r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. 返回最佳结果
|
||||||
if (best) return best;
|
if (best) return best;
|
||||||
|
|
||||||
|
// 6. 最后尝试:取第一个 { 到最后一个 } 之间的内容
|
||||||
const firstBrace = str.indexOf('{');
|
const firstBrace = str.indexOf('{');
|
||||||
const lastBrace = str.lastIndexOf('}');
|
const lastBrace = str.lastIndexOf('}');
|
||||||
if (!isArray && firstBrace !== -1 && lastBrace > firstBrace) {
|
if (!isArray && firstBrace !== -1 && lastBrace > firstBrace) {
|
||||||
@@ -163,6 +204,7 @@ function extractJson(input, isArray = false) {
|
|||||||
r = tryParse(chunk) || tryParse(fixJson(chunk));
|
r = tryParse(chunk) || tryParse(fixJson(chunk));
|
||||||
if (ok(r, isArray)) return r;
|
if (ok(r, isArray)) return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,8 +212,10 @@ export { extractJson };
|
|||||||
|
|
||||||
// ==================== 4. 存储管理 ====================
|
// ==================== 4. 存储管理 ====================
|
||||||
|
|
||||||
|
/** 获取扩展设置 */
|
||||||
const getSettings = () => { const e = extension_settings[EXT_ID] ||= {}; e.storyOutline ||= { enabled: true }; return e; };
|
const getSettings = () => { const e = extension_settings[EXT_ID] ||= {}; e.storyOutline ||= { enabled: true }; return e; };
|
||||||
|
|
||||||
|
/** 获取剧情大纲存储 */
|
||||||
function getOutlineStore() {
|
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] ||= {};
|
||||||
@@ -182,11 +226,13 @@ 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, ...getStore(STORAGE_KEYS.comm, {}) });
|
||||||
const saveCommSettings = s => setStore(STORAGE_KEYS.comm, s);
|
const saveCommSettings = s => setStore(STORAGE_KEYS.comm, s);
|
||||||
|
|
||||||
|
/** 获取角色卡信息 */
|
||||||
function getCharInfo() {
|
function getCharInfo() {
|
||||||
const ctx = getContext(), char = ctx.characters?.[ctx.characterId];
|
const ctx = getContext(), char = ctx.characters?.[ctx.characterId];
|
||||||
return {
|
return {
|
||||||
@@ -195,6 +241,7 @@ function getCharInfo() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取角色卡短信历史 */
|
||||||
function getCharSmsHistory() {
|
function getCharSmsHistory() {
|
||||||
if (!chat_metadata) return null;
|
if (!chat_metadata) return null;
|
||||||
const root = chat_metadata.LittleWhiteBox_StoryOutline ||= {};
|
const root = chat_metadata.LittleWhiteBox_StoryOutline ||= {};
|
||||||
@@ -205,8 +252,11 @@ function getCharSmsHistory() {
|
|||||||
|
|
||||||
// ==================== 5. LLM调用 ====================
|
// ==================== 5. 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 normalize = r => {
|
const normalize = r => {
|
||||||
if (r == null) return '';
|
if (r == null) return '';
|
||||||
if (typeof r === 'string') return r;
|
if (typeof r === 'string') return r;
|
||||||
@@ -220,12 +270,18 @@ async function callLLM(promptOrMsgs, useRaw = false) {
|
|||||||
}
|
}
|
||||||
return String(r);
|
return String(r);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 构建基础选项
|
||||||
const opts = { nonstream: 'true', lock: 'on' };
|
const opts = { nonstream: 'true', lock: 'on' };
|
||||||
if (apiUrl?.trim()) Object.assign(opts, { api: 'openai', apiurl: apiUrl.trim(), ...(apiKey && { apipassword: apiKey }), ...(model && { model }) });
|
if (apiUrl?.trim()) Object.assign(opts, { api: 'openai', apiurl: apiUrl.trim(), ...(apiKey && { apipassword: apiKey }), ...(model && { model }) });
|
||||||
|
|
||||||
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())
|
||||||
@@ -234,9 +290,13 @@ 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();
|
||||||
|
|
||||||
if (isDebug()) {
|
if (isDebug()) {
|
||||||
try {
|
try {
|
||||||
console.groupCollapsed('[StoryOutline] callLLM(useRaw via xbgenrawCommand)');
|
console.groupCollapsed('[StoryOutline] callLLM(useRaw via xbgenrawCommand)');
|
||||||
@@ -248,11 +308,13 @@ async function callLLM(promptOrMsgs, useRaw = false) {
|
|||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.as = 'user';
|
opts.as = 'user';
|
||||||
opts.position = 'history';
|
opts.position = 'history';
|
||||||
return normalize(await streamingGeneration.xbgenCommand(opts, promptOrMsgs)).trim();
|
return normalize(await streamingGeneration.xbgenCommand(opts, promptOrMsgs)).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 调用LLM并解析JSON */
|
||||||
async function callLLMJson({ messages, useRaw = true, isArray = false, validate }) {
|
async function callLLMJson({ messages, useRaw = true, isArray = false, validate }) {
|
||||||
try {
|
try {
|
||||||
const result = await callLLM(messages, useRaw);
|
const result = await callLLM(messages, useRaw);
|
||||||
@@ -282,6 +344,7 @@ async function callLLMJson({ messages, useRaw = true, isArray = false, validate
|
|||||||
|
|
||||||
// ==================== 6. 世界书操作 ====================
|
// ==================== 6. 世界书操作 ====================
|
||||||
|
|
||||||
|
/** 获取角色卡绑定的世界书 */
|
||||||
async function getCharWorldbooks() {
|
async function getCharWorldbooks() {
|
||||||
const ctx = getContext(), char = ctx.characters?.[ctx.characterId];
|
const ctx = getContext(), char = ctx.characters?.[ctx.characterId];
|
||||||
if (!char) return [];
|
if (!char) return [];
|
||||||
@@ -293,6 +356,7 @@ async function getCharWorldbooks() {
|
|||||||
return books;
|
return books;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 根据UID查找条目 */
|
||||||
async function findEntry(uid) {
|
async function findEntry(uid) {
|
||||||
const uidNum = parseInt(uid, 10);
|
const uidNum = parseInt(uid, 10);
|
||||||
if (isNaN(uidNum)) return null;
|
if (isNaN(uidNum)) return null;
|
||||||
@@ -303,6 +367,7 @@ async function findEntry(uid) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 根据名称搜索条目 */
|
||||||
async function searchEntry(name) {
|
async function searchEntry(name) {
|
||||||
const nl = (name || '').toLowerCase().trim();
|
const nl = (name || '').toLowerCase().trim();
|
||||||
for (const book of await getCharWorldbooks()) {
|
for (const book of await getCharWorldbooks()) {
|
||||||
@@ -319,24 +384,32 @@ async function searchEntry(name) {
|
|||||||
|
|
||||||
// ==================== 7. 剧情注入 ====================
|
// ==================== 7. 剧情注入 ====================
|
||||||
|
|
||||||
|
/** 获取可见洋葱层级 */
|
||||||
const getVisibleLayers = stage => ['L1_The_Veil', 'L2_The_Distortion', 'L3_The_Law', 'L4_The_Agent', 'L5_The_Axiom'].slice(0, Math.min(Math.max(0, stage), 3) + 2);
|
const getVisibleLayers = stage => ['L1_The_Veil', 'L2_The_Distortion', 'L3_The_Law', 'L4_The_Agent', 'L5_The_Axiom'].slice(0, Math.min(Math.max(0, stage), 3) + 2);
|
||||||
|
|
||||||
|
/** 格式化剧情数据为提示词 */
|
||||||
function formatOutlinePrompt() {
|
function formatOutlinePrompt() {
|
||||||
const store = getOutlineStore();
|
const store = getOutlineStore();
|
||||||
if (!store?.outlineData) return "";
|
if (!store?.outlineData) return "";
|
||||||
|
|
||||||
const { outlineData: d, dataChecked: c, playerLocation } = store, stage = store.stage ?? 0;
|
const { outlineData: d, dataChecked: c, playerLocation } = store, stage = store.stage ?? 0;
|
||||||
let text = "## Story Outline (剧情数据)\n\n", has = false;
|
let text = "## Story Outline (剧情数据)\n\n", has = false;
|
||||||
|
|
||||||
|
// 世界真相
|
||||||
if (c?.meta && d.meta?.truth) {
|
if (c?.meta && d.meta?.truth) {
|
||||||
has = true;
|
has = true;
|
||||||
text += "### 世界真相 (World Truth)\n> 注意:以下信息仅供生成逻辑参考,不可告知玩家。\n";
|
text += "### 世界真相 (World Truth)\n> 注意:以下信息仅供生成逻辑参考,不可告知玩家。\n";
|
||||||
if (d.meta.truth.background) text += `* 背景真相: ${d.meta.truth.background}\n`;
|
if (d.meta.truth.background) text += `* 背景真相: ${d.meta.truth.background}\n`;
|
||||||
const dr = d.meta.truth.driver;
|
const dr = d.meta.truth.driver;
|
||||||
if (dr) { if (dr.source) text += `* 驱动: ${dr.source}\n`; if (dr.target_end) text += `* 目的: ${dr.target_end}\n`; if (dr.tactic) text += `* 当前手段: ${dr.tactic}\n`; }
|
if (dr) { if (dr.source) text += `* 驱动: ${dr.source}\n`; if (dr.target_end) text += `* 目的: ${dr.target_end}\n`; if (dr.tactic) text += `* 当前手段: ${dr.tactic}\n`; }
|
||||||
|
|
||||||
|
// 当前气氛
|
||||||
const atm = d.meta.atmosphere?.current;
|
const atm = d.meta.atmosphere?.current;
|
||||||
if (atm) {
|
if (atm) {
|
||||||
if (atm.environmental) text += `* 当前气氛: ${atm.environmental}\n`;
|
if (atm.environmental) text += `* 当前气氛: ${atm.environmental}\n`;
|
||||||
if (atm.npc_attitudes) text += `* NPC态度: ${atm.npc_attitudes}\n`;
|
if (atm.npc_attitudes) text += `* NPC态度: ${atm.npc_attitudes}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onion = d.meta.onion_layers || d.meta.truth.onion_layers;
|
const onion = d.meta.onion_layers || d.meta.truth.onion_layers;
|
||||||
if (onion) {
|
if (onion) {
|
||||||
text += "* 当前可见层级:\n";
|
text += "* 当前可见层级:\n";
|
||||||
@@ -348,7 +421,11 @@ function formatOutlinePrompt() {
|
|||||||
}
|
}
|
||||||
text += "\n";
|
text += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 世界资讯
|
||||||
if (c?.world && d.world?.news?.length) { has = true; text += "### 世界资讯 (News)\n"; d.world.news.forEach(n => { text += `* ${n.title}: ${n.content}\n`; }); text += "\n"; }
|
if (c?.world && d.world?.news?.length) { has = true; text += "### 世界资讯 (News)\n"; d.world.news.forEach(n => { text += `* ${n.title}: ${n.content}\n`; }); text += "\n"; }
|
||||||
|
|
||||||
|
// 环境信息
|
||||||
let mapC = "", locNode = null;
|
let mapC = "", locNode = null;
|
||||||
if (c?.outdoor && d.outdoor) {
|
if (c?.outdoor && d.outdoor) {
|
||||||
if (d.outdoor.description) mapC += `> 大地图环境: ${d.outdoor.description}\n`;
|
if (d.outdoor.description) mapC += `> 大地图环境: ${d.outdoor.description}\n`;
|
||||||
@@ -360,14 +437,20 @@ function formatOutlinePrompt() {
|
|||||||
if (playerLocation && locText) mapC += `\n> 当前地点 (${playerLocation}):\n${locText}\n`;
|
if (playerLocation && locText) mapC += `\n> 当前地点 (${playerLocation}):\n${locText}\n`;
|
||||||
if (c?.indoor && d.indoor && !locNode && !indoorMap && d.indoor.description) { mapC += d.indoor.name ? `\n> 当前地点: ${d.indoor.name}\n` : "\n> 局部区域:\n"; mapC += `${d.indoor.description}\n`; }
|
if (c?.indoor && d.indoor && !locNode && !indoorMap && d.indoor.description) { mapC += d.indoor.name ? `\n> 当前地点: ${d.indoor.name}\n` : "\n> 局部区域:\n"; mapC += `${d.indoor.description}\n`; }
|
||||||
if (mapC) { has = true; text += `### 环境信息 (Environment)\n${mapC}\n`; }
|
if (mapC) { has = true; text += `### 环境信息 (Environment)\n${mapC}\n`; }
|
||||||
|
|
||||||
|
// 周边人物
|
||||||
let charC = "";
|
let charC = "";
|
||||||
if (c?.contacts && d.contacts?.length) { charC += "* 联络人:\n"; d.contacts.forEach(p => charC += ` - ${p.name}${p.location ? ` @ ${p.location}` : ''}: ${p.info || ''}\n`); }
|
if (c?.contacts && d.contacts?.length) { charC += "* 联络人:\n"; d.contacts.forEach(p => charC += ` - ${p.name}${p.location ? ` @ ${p.location}` : ''}: ${p.info || ''}\n`); }
|
||||||
if (c?.strangers && d.strangers?.length) { charC += "* 陌路人:\n"; d.strangers.forEach(p => charC += ` - ${p.name}${p.location ? ` @ ${p.location}` : ''}: ${p.info || ''}\n`); }
|
if (c?.strangers && d.strangers?.length) { charC += "* 陌路人:\n"; d.strangers.forEach(p => charC += ` - ${p.name}${p.location ? ` @ ${p.location}` : ''}: ${p.info || ''}\n`); }
|
||||||
if (charC) { has = true; text += `### 周边人物 (Characters)\n${charC}\n`; }
|
if (charC) { has = true; text += `### 周边人物 (Characters)\n${charC}\n`; }
|
||||||
|
|
||||||
|
// 当前剧情
|
||||||
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.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 (c?.characterContactSms) {
|
if (c?.characterContactSms) {
|
||||||
const { name: charName } = getCharInfo(), hist = getCharSmsHistory();
|
const { name: charName } = getCharInfo(), hist = getCharSmsHistory();
|
||||||
const sums = hist?.summaries || {}, sumKeys = Object.keys(sums).filter(k => k !== '_count').sort((a, b) => a - b);
|
const sums = hist?.summaries || {}, sumKeys = Object.keys(sums).filter(k => k !== '_count').sort((a, b) => a - b);
|
||||||
@@ -379,9 +462,11 @@ function formatOutlinePrompt() {
|
|||||||
text += "\n";
|
text += "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return has ? text.trim() : "";
|
return has ? text.trim() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 确保剧情大纲Prompt存在 */
|
||||||
function ensurePrompt() {
|
function ensurePrompt() {
|
||||||
if (!promptManager) return false;
|
if (!promptManager) return false;
|
||||||
let prompt = promptManager.getPromptById(STORY_OUTLINE_ID);
|
let prompt = promptManager.getPromptById(STORY_OUTLINE_ID);
|
||||||
@@ -399,6 +484,7 @@ function ensurePrompt() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 更新剧情大纲Prompt内容 */
|
||||||
function updatePromptContent() {
|
function updatePromptContent() {
|
||||||
if (!promptManager) return;
|
if (!promptManager) return;
|
||||||
if (!getSettings().storyOutline?.enabled) { removePrompt(); return; }
|
if (!getSettings().storyOutline?.enabled) { removePrompt(); return; }
|
||||||
@@ -411,6 +497,7 @@ function updatePromptContent() {
|
|||||||
promptManager.render?.(false);
|
promptManager.render?.(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 移除剧情大纲Prompt */
|
||||||
function removePrompt() {
|
function removePrompt() {
|
||||||
if (!promptManager) return;
|
if (!promptManager) return;
|
||||||
const prompts = promptManager.serviceSettings?.prompts;
|
const prompts = promptManager.serviceSettings?.prompts;
|
||||||
@@ -420,6 +507,7 @@ function removePrompt() {
|
|||||||
promptManager.render?.(false);
|
promptManager.render?.(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 设置ST预设事件监听 */
|
||||||
function setupSTEvents() {
|
function setupSTEvents() {
|
||||||
if (presetCleanup) return;
|
if (presetCleanup) return;
|
||||||
const onChanged = () => { if (getSettings().storyOutline?.enabled) setTimeout(() => { ensurePrompt(); updatePromptContent(); }, 100); };
|
const onChanged = () => { if (getSettings().storyOutline?.enabled) setTimeout(() => { ensurePrompt(); updatePromptContent(); }, 100); };
|
||||||
@@ -437,6 +525,7 @@ const injectOutline = () => updatePromptContent();
|
|||||||
|
|
||||||
// ==================== 8. iframe通讯 ====================
|
// ==================== 8. iframe通讯 ====================
|
||||||
|
|
||||||
|
/** 发送消息到iframe */
|
||||||
function postFrame(payload) {
|
function postFrame(payload) {
|
||||||
const iframe = document.getElementById("xiaobaix-story-outline-iframe");
|
const iframe = document.getElementById("xiaobaix-story-outline-iframe");
|
||||||
if (!iframe?.contentWindow || !frameReady) { pendingMsgs.push(payload); return; }
|
if (!iframe?.contentWindow || !frameReady) { pendingMsgs.push(payload); return; }
|
||||||
@@ -445,6 +534,7 @@ function postFrame(payload) {
|
|||||||
|
|
||||||
const flushPending = () => { if (!frameReady) return; const f = document.getElementById("xiaobaix-story-outline-iframe"); pendingMsgs.forEach(p => f?.contentWindow?.postMessage({ source: "LittleWhiteBox", ...p }, "*")); pendingMsgs = []; };
|
const flushPending = () => { if (!frameReady) return; const f = document.getElementById("xiaobaix-story-outline-iframe"); pendingMsgs.forEach(p => f?.contentWindow?.postMessage({ source: "LittleWhiteBox", ...p }, "*")); pendingMsgs = []; };
|
||||||
|
|
||||||
|
/** 发送设置到iframe */
|
||||||
function sendSettings() {
|
function sendSettings() {
|
||||||
const store = getOutlineStore(), { name: charName, desc: charDesc } = getCharInfo();
|
const store = getOutlineStore(), { name: charName, desc: charDesc } = getCharInfo();
|
||||||
postFrame({
|
postFrame({
|
||||||
@@ -464,10 +554,12 @@ const loadAndSend = () => { const s = getOutlineStore(); if (s?.mapData) postFra
|
|||||||
const reply = (type, reqId, data) => postFrame({ type, requestId: reqId, ...data });
|
const reply = (type, reqId, data) => postFrame({ type, requestId: reqId, ...data });
|
||||||
const replyErr = (type, reqId, err) => reply(type, reqId, { error: err });
|
const replyErr = (type, reqId, err) => reply(type, reqId, { error: err });
|
||||||
|
|
||||||
|
/** 获取当前气氛 */
|
||||||
function getAtmosphere(store) {
|
function getAtmosphere(store) {
|
||||||
return store?.outlineData?.meta?.atmosphere?.current || null;
|
return store?.outlineData?.meta?.atmosphere?.current || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 合并世界推演数据 */
|
||||||
function mergeSimData(orig, upd) {
|
function mergeSimData(orig, upd) {
|
||||||
if (!upd) return orig;
|
if (!upd) return orig;
|
||||||
const r = JSON.parse(JSON.stringify(orig || {}));
|
const r = JSON.parse(JSON.stringify(orig || {}));
|
||||||
@@ -477,13 +569,16 @@ function mergeSimData(orig, upd) {
|
|||||||
if (ut?.driver?.tactic) r.meta.truth.driver = { ...r.meta.truth.driver, tactic: ut.driver.tactic };
|
if (ut?.driver?.tactic) r.meta.truth.driver = { ...r.meta.truth.driver, tactic: ut.driver.tactic };
|
||||||
if (uo) { ['L1_The_Veil', 'L2_The_Distortion', 'L3_The_Law', 'L4_The_Agent', 'L5_The_Axiom'].forEach(l => { const v = uo[l]; if (Array.isArray(v) && v.length) { r.meta.onion_layers = r.meta.onion_layers || {}; r.meta.onion_layers[l] = v; } }); if (r.meta.truth?.onion_layers) delete r.meta.truth.onion_layers; }
|
if (uo) { ['L1_The_Veil', 'L2_The_Distortion', 'L3_The_Law', 'L4_The_Agent', 'L5_The_Axiom'].forEach(l => { const v = uo[l]; if (Array.isArray(v) && v.length) { r.meta.onion_layers = r.meta.onion_layers || {}; r.meta.onion_layers[l] = v; } }); if (r.meta.truth?.onion_layers) delete r.meta.truth.onion_layers; }
|
||||||
if (um.user_guide || upd?.user_guide) r.meta.user_guide = um.user_guide || upd.user_guide;
|
if (um.user_guide || upd?.user_guide) r.meta.user_guide = um.user_guide || upd.user_guide;
|
||||||
|
// 更新 atmosphere
|
||||||
if (ua) { r.meta.atmosphere = ua; }
|
if (ua) { r.meta.atmosphere = ua; }
|
||||||
|
// 更新 trajectory
|
||||||
if (utr) { r.meta.trajectory = utr; }
|
if (utr) { r.meta.trajectory = utr; }
|
||||||
if (upd?.world) r.world = upd.world;
|
if (upd?.world) r.world = upd.world;
|
||||||
if (upd?.maps?.outdoor) { r.maps = r.maps || {}; r.maps.outdoor = r.maps.outdoor || {}; if (upd.maps.outdoor.description) r.maps.outdoor.description = upd.maps.outdoor.description; if (Array.isArray(upd.maps.outdoor.nodes)) { const on = r.maps.outdoor.nodes || []; upd.maps.outdoor.nodes.forEach(n => { const i = on.findIndex(x => x.name === n.name); if (i >= 0) on[i] = { ...n }; else on.push(n); }); r.maps.outdoor.nodes = on; } }
|
if (upd?.maps?.outdoor) { r.maps = r.maps || {}; r.maps.outdoor = r.maps.outdoor || {}; if (upd.maps.outdoor.description) r.maps.outdoor.description = upd.maps.outdoor.description; if (Array.isArray(upd.maps.outdoor.nodes)) { const on = r.maps.outdoor.nodes || []; upd.maps.outdoor.nodes.forEach(n => { const i = on.findIndex(x => x.name === n.name); if (i >= 0) on[i] = { ...n }; else on.push(n); }); r.maps.outdoor.nodes = on; } }
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 检查自动推演 */
|
||||||
async function checkAutoSim(reqId) {
|
async function checkAutoSim(reqId) {
|
||||||
const store = getOutlineStore();
|
const store = getOutlineStore();
|
||||||
if (!store || (store.simulationProgress || 0) < (store.simulationTarget ?? 5)) return;
|
if (!store || (store.simulationProgress || 0) < (store.simulationTarget ?? 5)) return;
|
||||||
@@ -491,17 +586,20 @@ async function checkAutoSim(reqId) {
|
|||||||
await handleSimWorld({ requestId: `wsim_auto_${Date.now()}`, currentData: JSON.stringify(data), isAuto: true });
|
await handleSimWorld({ requestId: `wsim_auto_${Date.now()}`, currentData: JSON.stringify(data), isAuto: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证器
|
||||||
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, 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',
|
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),
|
||||||
wga: d => !!(d?.world && d?.maps?.outdoor), ws: d => !!d, w: o => !!o && typeof o === 'object',
|
wga: d => !!(d?.world && d?.maps?.outdoor), ws: d => !!d, w: o => !!o && typeof o === 'object',
|
||||||
lm: o => !!o?.inside?.name && !!o?.inside?.description
|
lm: o => !!o?.inside?.name && !!o?.inside?.description
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- 处理器 ---
|
||||||
|
|
||||||
async function handleFetchModels({ apiUrl, apiKey }) {
|
async function handleFetchModels({ apiUrl, apiKey }) {
|
||||||
try {
|
try {
|
||||||
let models = [];
|
let models = [];
|
||||||
@@ -550,6 +648,7 @@ async function handleSendSms({ requestId, contactName, worldbookUid, userMessage
|
|||||||
try {
|
try {
|
||||||
const ctx = getContext(), userName = name1 || ctx.name1 || '用户';
|
const ctx = getContext(), userName = name1 || ctx.name1 || '用户';
|
||||||
let charContent = '', existSum = {}, sc = summarizedCount || 0;
|
let charContent = '', existSum = {}, sc = summarizedCount || 0;
|
||||||
|
|
||||||
if (worldbookUid === CHAR_CARD_UID) {
|
if (worldbookUid === CHAR_CARD_UID) {
|
||||||
charContent = getCharInfo().desc;
|
charContent = getCharInfo().desc;
|
||||||
const h = getCharSmsHistory(); existSum = h?.summaries || {}; sc = summarizedCount ?? h?.summarizedCount ?? 0;
|
const h = getCharSmsHistory(); existSum = h?.summaries || {}; sc = summarizedCount ?? h?.summarizedCount ?? 0;
|
||||||
@@ -562,10 +661,12 @@ async function handleSendSms({ requestId, contactName, worldbookUid, userMessage
|
|||||||
if (s !== -1 && ed !== -1) { const p = safe(() => JSON.parse(c.substring(s + 19, ed).trim())); const si = p?.find?.(i => typeof i === 'string' && i.startsWith('SMS_summary:')); if (si) existSum = safe(() => JSON.parse(si.substring(12))) || {}; }
|
if (s !== -1 && ed !== -1) { const p = safe(() => JSON.parse(c.substring(s + 19, ed).trim())); const si = p?.find?.(i => typeof i === 'string' && i.startsWith('SMS_summary:')); if (si) existSum = safe(() => JSON.parse(si.substring(12))) || {}; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let histText = '';
|
let histText = '';
|
||||||
const sumKeys = Object.keys(existSum).filter(k => k !== '_count').sort((a, b) => a - b);
|
const sumKeys = Object.keys(existSum).filter(k => k !== '_count').sort((a, b) => a - b);
|
||||||
if (sumKeys.length) histText = `[之前的对话摘要] ${sumKeys.map(k => existSum[k]).join(';')}\n\n`;
|
if (sumKeys.length) histText = `[之前的对话摘要] ${sumKeys.map(k => existSum[k]).join(';')}\n\n`;
|
||||||
if (chatHistory?.length > 1) { const msgs = chatHistory.slice(sc, -1); if (msgs.length) histText += msgs.map(m => `${m.type === 'sent' ? userName : contactName}:${m.text}`).join('\n'); }
|
if (chatHistory?.length > 1) { const msgs = chatHistory.slice(sc, -1); if (msgs.length) histText += msgs.map(m => `${m.type === 'sent' ? userName : contactName}:${m.text}`).join('\n'); }
|
||||||
|
|
||||||
const msgs = buildSmsMessages({ contactName, userName, storyOutline: formatOutlinePrompt(), historyCount: getCommSettings().historyCount || 50, smsHistoryContent: buildSmsHistoryContent(histText), userMessage, characterContent: charContent });
|
const msgs = buildSmsMessages({ contactName, userName, storyOutline: formatOutlinePrompt(), historyCount: getCommSettings().historyCount || 50, smsHistoryContent: buildSmsHistoryContent(histText), userMessage, characterContent: charContent });
|
||||||
const parsed = await callLLMJson({ messages: msgs, validate: V.sms });
|
const parsed = await callLLMJson({ messages: msgs, validate: V.sms });
|
||||||
reply('SMS_RESULT', requestId, parsed?.reply ? { reply: parsed.reply } : { error: '生成回复失败,请调整重试' });
|
reply('SMS_RESULT', requestId, parsed?.reply ? { reply: parsed.reply } : { error: '生成回复失败,请调整重试' });
|
||||||
@@ -596,6 +697,7 @@ async function handleCompressSms({ requestId, worldbookUid, messages, contactNam
|
|||||||
try {
|
try {
|
||||||
const ctx = getContext(), userName = name1 || ctx.name1 || '用户';
|
const ctx = getContext(), userName = name1 || ctx.name1 || '用户';
|
||||||
let e = null, existSum = {};
|
let e = null, existSum = {};
|
||||||
|
|
||||||
if (worldbookUid === CHAR_CARD_UID) {
|
if (worldbookUid === CHAR_CARD_UID) {
|
||||||
const h = getCharSmsHistory(); existSum = h?.summaries || {};
|
const h = getCharSmsHistory(); existSum = h?.summaries || {};
|
||||||
const keep = 4, toEnd = Math.max(sc, (messages?.length || 0) - keep);
|
const keep = 4, toEnd = Math.max(sc, (messages?.length || 0) - keep);
|
||||||
@@ -611,8 +713,10 @@ async function handleCompressSms({ requestId, worldbookUid, messages, contactNam
|
|||||||
if (h) { h.messages = Array.isArray(messages) ? messages : (h.messages || []); h.summarizedCount = toEnd; h.summaries = existSum; saveMetadataDebounced?.(); }
|
if (h) { h.messages = Array.isArray(messages) ? messages : (h.messages || []); h.summarizedCount = toEnd; h.summaries = existSum; saveMetadataDebounced?.(); }
|
||||||
return reply('COMPRESS_SMS_RESULT', requestId, { summary: sum, newSummarizedCount: toEnd });
|
return reply('COMPRESS_SMS_RESULT', requestId, { summary: sum, newSummarizedCount: toEnd });
|
||||||
}
|
}
|
||||||
|
|
||||||
e = await findEntry(worldbookUid);
|
e = await findEntry(worldbookUid);
|
||||||
if (e?.entry) { const c = e.entry.content || '', [s, ed] = [c.indexOf('[SMS_HISTORY_START]'), c.indexOf('[SMS_HISTORY_END]')]; if (s !== -1 && ed !== -1) { const p = safe(() => JSON.parse(c.substring(s + 19, ed).trim())); const si = p?.find?.(i => typeof i === 'string' && i.startsWith('SMS_summary:')); if (si) existSum = safe(() => JSON.parse(si.substring(12))) || {}; } }
|
if (e?.entry) { const c = e.entry.content || '', [s, ed] = [c.indexOf('[SMS_HISTORY_START]'), c.indexOf('[SMS_HISTORY_END]')]; if (s !== -1 && ed !== -1) { const p = safe(() => JSON.parse(c.substring(s + 19, ed).trim())); const si = p?.find?.(i => typeof i === 'string' && i.startsWith('SMS_summary:')); if (si) existSum = safe(() => JSON.parse(si.substring(12))) || {}; } }
|
||||||
|
|
||||||
const keep = 4, toEnd = Math.max(sc, messages.length - keep);
|
const keep = 4, toEnd = Math.max(sc, messages.length - keep);
|
||||||
if (toEnd <= sc) return replyErr('COMPRESS_SMS_RESULT', requestId, '没有足够的新消息需要总结');
|
if (toEnd <= sc) return replyErr('COMPRESS_SMS_RESULT', requestId, '没有足够的新消息需要总结');
|
||||||
const toSum = messages.slice(sc, toEnd); if (toSum.length < 2) return replyErr('COMPRESS_SMS_RESULT', requestId, '需要至少2条消息才能进行总结');
|
const toSum = messages.slice(sc, toEnd); if (toSum.length < 2) return replyErr('COMPRESS_SMS_RESULT', requestId, '需要至少2条消息才能进行总结');
|
||||||
@@ -622,6 +726,7 @@ async function handleCompressSms({ requestId, worldbookUid, messages, contactNam
|
|||||||
const parsed = await callLLMJson({ messages: buildSummaryMessages({ existingSummaryContent: buildExistingSummaryContent(existText), conversationText: convText }), validate: V.sum });
|
const parsed = await callLLMJson({ messages: buildSummaryMessages({ existingSummaryContent: buildExistingSummaryContent(existText), conversationText: convText }), validate: V.sum });
|
||||||
const sum = parsed?.summary?.trim?.(); if (!sum) return replyErr('COMPRESS_SMS_RESULT', requestId, 'ECHO:总结生成出错,请重试');
|
const sum = parsed?.summary?.trim?.(); if (!sum) return replyErr('COMPRESS_SMS_RESULT', requestId, 'ECHO:总结生成出错,请重试');
|
||||||
const newSc = toEnd;
|
const newSc = toEnd;
|
||||||
|
|
||||||
if (e) {
|
if (e) {
|
||||||
const { bookName, entry: en, worldData } = e; let c = en.content || ''; const cn = contactName || en.key?.[0] || '角色';
|
const { bookName, entry: en, worldData } = e; let c = en.content || ''; const cn = contactName || en.key?.[0] || '角色';
|
||||||
const [s, ed] = [c.indexOf('[SMS_HISTORY_START]'), c.indexOf('[SMS_HISTORY_END]')]; if (s !== -1 && ed !== -1) c = c.substring(0, s).trimEnd() + c.substring(ed + 17);
|
const [s, ed] = [c.indexOf('[SMS_HISTORY_START]'), c.indexOf('[SMS_HISTORY_END]')]; if (s !== -1 && ed !== -1) c = c.substring(0, s).trimEnd() + c.substring(ed + 17);
|
||||||
@@ -742,6 +847,8 @@ async function handleGenLocalScene({ requestId, locationName, locationInfo }) {
|
|||||||
async function handleGenWorld({ requestId, playerRequests }) {
|
async function handleGenWorld({ requestId, playerRequests }) {
|
||||||
try {
|
try {
|
||||||
const comm = getCommSettings(), mode = getGlobalSettings().mode || 'story', store = getOutlineStore();
|
const comm = getCommSettings(), mode = getGlobalSettings().mode || 'story', store = getOutlineStore();
|
||||||
|
|
||||||
|
// 递归查找函数 - 在任意层级找到目标键
|
||||||
const deepFind = (obj, key) => {
|
const deepFind = (obj, key) => {
|
||||||
if (!obj || typeof obj !== 'object') return null;
|
if (!obj || typeof obj !== 'object') return null;
|
||||||
if (obj[key] !== undefined) return obj[key];
|
if (obj[key] !== undefined) return obj[key];
|
||||||
@@ -751,24 +858,42 @@ async function handleGenWorld({ requestId, playerRequests }) {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeStep1Data = (data) => {
|
const normalizeStep1Data = (data) => {
|
||||||
if (!data || typeof data !== 'object') return null;
|
if (!data || typeof data !== 'object') return null;
|
||||||
|
|
||||||
|
// 构建标准化结构,从任意位置提取数据
|
||||||
const result = { meta: {} };
|
const result = { meta: {} };
|
||||||
|
|
||||||
|
// 提取 truth(可能在 meta.truth, data.truth, 或者 data 本身就是 truth)
|
||||||
result.meta.truth = deepFind(data, 'truth')
|
result.meta.truth = deepFind(data, 'truth')
|
||||||
|| (data.background && data.driver ? data : null)
|
|| (data.background && data.driver ? data : null)
|
||||||
|| { background: deepFind(data, 'background'), driver: deepFind(data, 'driver') };
|
|| { background: deepFind(data, 'background'), driver: deepFind(data, 'driver') };
|
||||||
|
|
||||||
|
// 提取 onion_layers
|
||||||
result.meta.onion_layers = deepFind(data, 'onion_layers') || {};
|
result.meta.onion_layers = deepFind(data, 'onion_layers') || {};
|
||||||
|
|
||||||
|
// 统一洋葱层级为数组格式
|
||||||
['L1_The_Veil', 'L2_The_Distortion', 'L3_The_Law', 'L4_The_Agent', 'L5_The_Axiom'].forEach(k => {
|
['L1_The_Veil', 'L2_The_Distortion', 'L3_The_Law', 'L4_The_Agent', 'L5_The_Axiom'].forEach(k => {
|
||||||
const v = result.meta.onion_layers[k];
|
const v = result.meta.onion_layers[k];
|
||||||
if (v && !Array.isArray(v) && typeof v === 'object') {
|
if (v && !Array.isArray(v) && typeof v === 'object') {
|
||||||
result.meta.onion_layers[k] = [v];
|
result.meta.onion_layers[k] = [v];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 提取 atmosphere
|
||||||
result.meta.atmosphere = deepFind(data, 'atmosphere') || { reasoning: '', current: { environmental: '', npc_attitudes: '' } };
|
result.meta.atmosphere = deepFind(data, 'atmosphere') || { reasoning: '', current: { environmental: '', npc_attitudes: '' } };
|
||||||
|
|
||||||
|
// 提取 trajectory
|
||||||
result.meta.trajectory = deepFind(data, 'trajectory') || { reasoning: '', ending: '' };
|
result.meta.trajectory = deepFind(data, 'trajectory') || { reasoning: '', ending: '' };
|
||||||
|
|
||||||
|
// 提取 user_guide
|
||||||
result.meta.user_guide = deepFind(data, 'user_guide') || { current_state: '', guides: [] };
|
result.meta.user_guide = deepFind(data, 'user_guide') || { current_state: '', guides: [] };
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 辅助模式
|
||||||
if (mode === 'assist') {
|
if (mode === 'assist') {
|
||||||
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 });
|
||||||
@@ -776,20 +901,28 @@ async function handleGenWorld({ requestId, playerRequests }) {
|
|||||||
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, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = { ...wd }; saveMetadataDebounced?.(); }
|
||||||
return reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: wd });
|
return reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: wd });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 1
|
||||||
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '正在构思世界大纲 (Step 1/2)...' });
|
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '正在构思世界大纲 (Step 1/2)...' });
|
||||||
const s1m = buildWorldGenStep1Messages({ historyCount: comm.historyCount || 50, playerRequests });
|
const s1m = buildWorldGenStep1Messages({ historyCount: comm.historyCount || 50, playerRequests });
|
||||||
const s1d = normalizeStep1Data(await callLLMJson({ messages: s1m, validate: V.wg1 }));
|
const s1d = normalizeStep1Data(await callLLMJson({ messages: s1m, validate: V.wg1 }));
|
||||||
|
|
||||||
|
// 简化验证 - 只要有基本数据就行
|
||||||
if (!s1d?.meta) {
|
if (!s1d?.meta) {
|
||||||
return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 1 失败:无法解析大纲数据,请重试');
|
return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 1 失败:无法解析大纲数据,请重试');
|
||||||
}
|
}
|
||||||
step1Cache = { step1Data: s1d, playerRequests: playerRequests || '' };
|
step1Cache = { step1Data: s1d, playerRequests: playerRequests || '' };
|
||||||
|
|
||||||
|
// Step 2
|
||||||
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: 'Step 1 完成,1 秒后开始构建世界细节 (Step 2/2)...' });
|
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: 'Step 1 完成,1 秒后开始构建世界细节 (Step 2/2)...' });
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '正在构建世界细节 (Step 2/2)...' });
|
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '正在构建世界细节 (Step 2/2)...' });
|
||||||
|
|
||||||
const s2m = buildWorldGenStep2Messages({ historyCount: comm.historyCount || 50, playerRequests, step1Data: s1d });
|
const s2m = buildWorldGenStep2Messages({ historyCount: comm.historyCount || 50, playerRequests, step1Data: s1d });
|
||||||
const s2d = await callLLMJson({ messages: s2m, validate: V.wg2 });
|
const s2d = await callLLMJson({ messages: s2m, validate: V.wg2 });
|
||||||
if (s2d?.world?.maps && !s2d?.maps) { s2d.maps = s2d.world.maps; delete s2d.world.maps; }
|
if (s2d?.world?.maps && !s2d?.maps) { s2d.maps = s2d.world.maps; delete s2d.world.maps; }
|
||||||
if (!s2d?.world || !s2d?.maps) return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 2 失败:无法生成有效的地图');
|
if (!s2d?.world || !s2d?.maps) return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 2 失败:无法生成有效的地图');
|
||||||
|
|
||||||
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, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); }
|
||||||
@@ -801,13 +934,16 @@ async function handleRetryStep2({ requestId }) {
|
|||||||
try {
|
try {
|
||||||
if (!step1Cache?.step1Data?.meta) return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 2 重试失败:缺少 Step 1 数据,请重新开始生成');
|
if (!step1Cache?.step1Data?.meta) return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 2 重试失败:缺少 Step 1 数据,请重新开始生成');
|
||||||
const comm = getCommSettings(), store = getOutlineStore(), s1d = step1Cache.step1Data, pr = step1Cache.playerRequests || '';
|
const comm = getCommSettings(), store = getOutlineStore(), s1d = step1Cache.step1Data, pr = step1Cache.playerRequests || '';
|
||||||
|
|
||||||
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '1 秒后重试构建世界细节 (Step 2/2)...' });
|
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '1 秒后重试构建世界细节 (Step 2/2)...' });
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '正在重试构建世界细节 (Step 2/2)...' });
|
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '正在重试构建世界细节 (Step 2/2)...' });
|
||||||
|
|
||||||
const s2m = buildWorldGenStep2Messages({ historyCount: comm.historyCount || 50, playerRequests: pr, step1Data: s1d });
|
const s2m = buildWorldGenStep2Messages({ historyCount: comm.historyCount || 50, playerRequests: pr, step1Data: s1d });
|
||||||
const s2d = await callLLMJson({ messages: s2m, validate: V.wg2 });
|
const s2d = await callLLMJson({ messages: s2m, validate: V.wg2 });
|
||||||
if (s2d?.world?.maps && !s2d?.maps) { s2d.maps = s2d.world.maps; delete s2d.world.maps; }
|
if (s2d?.world?.maps && !s2d?.maps) { s2d.maps = s2d.world.maps; delete s2d.world.maps; }
|
||||||
if (!s2d?.world || !s2d?.maps) return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 2 失败:无法生成有效的地图');
|
if (!s2d?.world || !s2d?.maps) return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 2 失败:无法生成有效的地图');
|
||||||
|
|
||||||
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, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); }
|
||||||
@@ -844,10 +980,7 @@ function handleSaveSettings(d) {
|
|||||||
function handleSavePrompts(d) {
|
function handleSavePrompts(d) {
|
||||||
if (!d?.promptConfig) return;
|
if (!d?.promptConfig) return;
|
||||||
setPromptConfig?.(d.promptConfig, true);
|
setPromptConfig?.(d.promptConfig, true);
|
||||||
postFrame({
|
postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() });
|
||||||
type: "PROMPT_CONFIG_UPDATED",
|
|
||||||
promptConfig: getPromptConfigPayload?.()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSaveContacts(d) {
|
function handleSaveContacts(d) {
|
||||||
@@ -881,6 +1014,7 @@ function handleSaveCharSmsHistory(d) {
|
|||||||
injectOutline();
|
injectOutline();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理器映射
|
||||||
const handlers = {
|
const handlers = {
|
||||||
FRAME_READY: () => { frameReady = true; flushPending(); loadAndSend(); },
|
FRAME_READY: () => { frameReady = true; flushPending(); loadAndSend(); },
|
||||||
CLOSE_PANEL: hideOverlay,
|
CLOSE_PANEL: hideOverlay,
|
||||||
@@ -916,6 +1050,7 @@ const handleMsg = ({ data }) => { if (data?.source === "LittleWhiteBox-OutlineFr
|
|||||||
|
|
||||||
// ==================== 10. UI管理 ====================
|
// ==================== 10. UI管理 ====================
|
||||||
|
|
||||||
|
/** 指针拖拽 */
|
||||||
function setupDrag(el, { onStart, onMove, onEnd, shouldHandle }) {
|
function setupDrag(el, { onStart, onMove, onEnd, shouldHandle }) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
let state = null;
|
let state = null;
|
||||||
@@ -925,6 +1060,7 @@ function setupDrag(el, { onStart, onMove, onEnd, shouldHandle }) {
|
|||||||
['pointerup', 'pointercancel', 'lostpointercapture'].forEach(ev => el.addEventListener(ev, end));
|
['pointerup', 'pointercancel', 'lostpointercapture'].forEach(ev => el.addEventListener(ev, end));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 创建Overlay */
|
||||||
function createOverlay() {
|
function createOverlay() {
|
||||||
if (overlayCreated) return;
|
if (overlayCreated) return;
|
||||||
overlayCreated = true;
|
overlayCreated = true;
|
||||||
@@ -932,6 +1068,7 @@ function createOverlay() {
|
|||||||
const overlay = document.getElementById("xiaobaix-story-outline-overlay"), wrap = overlay.querySelector(".xb-so-frame-wrap"), iframe = overlay.querySelector("iframe");
|
const overlay = document.getElementById("xiaobaix-story-outline-overlay"), wrap = overlay.querySelector(".xb-so-frame-wrap"), iframe = overlay.querySelector("iframe");
|
||||||
const setPtr = v => iframe && (iframe.style.pointerEvents = v);
|
const setPtr = v => iframe && (iframe.style.pointerEvents = v);
|
||||||
|
|
||||||
|
// 拖拽
|
||||||
setupDrag(overlay.querySelector(".xb-so-drag-handle"), {
|
setupDrag(overlay.querySelector(".xb-so-drag-handle"), {
|
||||||
shouldHandle: () => !isMobile(),
|
shouldHandle: () => !isMobile(),
|
||||||
onStart(e) { const r = wrap.getBoundingClientRect(), ro = overlay.getBoundingClientRect(); wrap.style.left = (r.left - ro.left) + 'px'; wrap.style.top = (r.top - ro.top) + 'px'; wrap.style.transform = ''; setPtr('none'); return { sx: e.clientX, sy: e.clientY, sl: parseFloat(wrap.style.left), st: parseFloat(wrap.style.top) }; },
|
onStart(e) { const r = wrap.getBoundingClientRect(), ro = overlay.getBoundingClientRect(); wrap.style.left = (r.left - ro.left) + 'px'; wrap.style.top = (r.top - ro.top) + 'px'; wrap.style.transform = ''; setPtr('none'); return { sx: e.clientX, sy: e.clientY, sl: parseFloat(wrap.style.left), st: parseFloat(wrap.style.top) }; },
|
||||||
@@ -939,18 +1076,20 @@ function createOverlay() {
|
|||||||
onEnd: () => setPtr('')
|
onEnd: () => setPtr('')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 缩放
|
||||||
setupDrag(overlay.querySelector(".xb-so-resize-handle"), {
|
setupDrag(overlay.querySelector(".xb-so-resize-handle"), {
|
||||||
shouldHandle: () => !isMobile(),
|
shouldHandle: () => !isMobile(),
|
||||||
onStart(e) { const r = wrap.getBoundingClientRect(), ro = overlay.getBoundingClientRect(); wrap.style.left = (r.left - ro.left) + 'px'; wrap.style.top = (r.top - ro.top) + 'px'; wrap.style.transform = ''; setPtr('none'); return { sx: e.clientX, sy: e.clientY, sw: wrap.offsetWidth, sh: wrap.offsetHeight, ratio: wrap.offsetWidth / wrap.offsetHeight }; },
|
onStart(e) { const r = wrap.getBoundingClientRect(), ro = overlay.getBoundingClientRect(); wrap.style.left = (r.left - ro.left) + 'px'; wrap.style.top = (r.top - ro.top) + 'px'; wrap.style.transform = ''; setPtr('none'); return { sx: e.clientX, sy: e.clientY, sw: wrap.offsetWidth, sh: wrap.offsetHeight, ratio: wrap.offsetWidth / wrap.offsetHeight }; },
|
||||||
onMove(e, s) { const dx = e.clientX - s.sx, dy = e.clientY - s.sy, delta = Math.abs(dx) > Math.abs(dy) ? dx : dy * s.ratio; let w = Math.max(400, Math.min(window.innerWidth * 0.95, s.sw + delta)), h = w / s.ratio; if (h > window.innerHeight * 0.9) { h = window.innerHeight * 0.9; w = h * s.ratio; } if (h < 300) { h = 300; w = h * s.ratio; } wrap.style.width = w + 'px'; wrap.style.height = h + 'px'; },
|
onMove(e, s) { const dx = e.clientX - s.sx, dy = e.clientY - s.sy, delta = Math.abs(dx) > Math.abs(dy) ? dx : dy * s.ratio; let w = Math.max(400, Math.min(window.innerWidth * 0.95, s.sw + delta)), h = w / s.ratio; if (h > window.innerHeight * 0.9) { h = window.innerHeight * 0.9; w = h * s.ratio; } if (h < 300) { h = 300; w = h * s.ratio; } wrap.style.width = w + 'px'; wrap.style.height = h + 'px'; },
|
||||||
onEnd: () => { setPtr(''); setStoredSize(false, { width: wrap.offsetWidth, height: wrap.offsetHeight }); }
|
onEnd: () => setPtr('')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 移动端
|
||||||
setupDrag(overlay.querySelector(".xb-so-resize-mobile"), {
|
setupDrag(overlay.querySelector(".xb-so-resize-mobile"), {
|
||||||
shouldHandle: () => isMobile(),
|
shouldHandle: () => isMobile(),
|
||||||
onStart(e) { setPtr('none'); return { sy: e.clientY, sh: wrap.offsetHeight }; },
|
onStart(e) { setPtr('none'); return { sy: e.clientY, sh: wrap.offsetHeight }; },
|
||||||
onMove(e, s) { wrap.style.height = Math.max(44, Math.min(window.innerHeight * 0.9, s.sh + e.clientY - s.sy)) + 'px'; },
|
onMove(e, s) { wrap.style.height = Math.max(44, Math.min(window.innerHeight * 0.9, s.sh + e.clientY - s.sy)) + 'px'; },
|
||||||
onEnd: () => { setPtr(''); setStoredSize(true, { height: wrap.offsetHeight }); }
|
onEnd: () => setPtr('')
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("message", handleMsg);
|
window.addEventListener("message", handleMsg);
|
||||||
@@ -959,53 +1098,17 @@ function createOverlay() {
|
|||||||
function updateLayout() {
|
function updateLayout() {
|
||||||
const wrap = document.querySelector(".xb-so-frame-wrap"); if (!wrap) return;
|
const wrap = document.querySelector(".xb-so-frame-wrap"); if (!wrap) return;
|
||||||
const drag = document.querySelector(".xb-so-drag-handle"), resize = document.querySelector(".xb-so-resize-handle"), mobile = document.querySelector(".xb-so-resize-mobile");
|
const drag = document.querySelector(".xb-so-drag-handle"), resize = document.querySelector(".xb-so-resize-handle"), mobile = document.querySelector(".xb-so-resize-mobile");
|
||||||
if (isMobile()) {
|
if (isMobile()) { if (drag) drag.style.display = 'none'; if (resize) resize.style.display = 'none'; if (mobile) mobile.style.display = 'flex'; wrap.style.cssText = MOBILE_LAYOUT_STYLE; const fixedHeight = window.innerHeight * 0.4; wrap.style.height = Math.max(44, fixedHeight) + 'px'; wrap.style.top = '0px'; }
|
||||||
if (drag) drag.style.display = 'none';
|
else { if (drag) drag.style.display = 'block'; if (resize) resize.style.display = 'block'; if (mobile) mobile.style.display = 'none'; wrap.style.cssText = DESKTOP_LAYOUT_STYLE; }
|
||||||
if (resize) resize.style.display = 'none';
|
|
||||||
if (mobile) mobile.style.display = 'flex';
|
|
||||||
wrap.style.cssText = MOBILE_LAYOUT_STYLE;
|
|
||||||
const maxHeight = window.innerHeight * 1;
|
|
||||||
const stored = getStoredSize(true);
|
|
||||||
const height = stored?.height ? Math.min(stored.height, maxHeight) : maxHeight;
|
|
||||||
wrap.style.height = Math.max(44, height) + 'px';
|
|
||||||
wrap.style.top = '0px';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (drag) drag.style.display = 'block';
|
|
||||||
if (resize) resize.style.display = 'block';
|
|
||||||
if (mobile) mobile.style.display = 'none';
|
|
||||||
wrap.style.cssText = DESKTOP_LAYOUT_STYLE;
|
|
||||||
const stored = getStoredSize(false);
|
|
||||||
if (stored) {
|
|
||||||
const maxW = window.innerWidth * 0.95;
|
|
||||||
const maxH = window.innerHeight * 0.9;
|
|
||||||
if (stored.width) wrap.style.width = Math.max(400, Math.min(stored.width, maxW)) + 'px';
|
|
||||||
if (stored.height) wrap.style.height = Math.max(300, Math.min(stored.height, maxH)) + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOverlay() {
|
function showOverlay() { if (!overlayCreated) createOverlay(); frameReady = false; const f = document.getElementById("xiaobaix-story-outline-iframe"); if (f) f.src = IFRAME_PATH; updateLayout(); $("#xiaobaix-story-outline-overlay").show(); }
|
||||||
if (!overlayCreated) createOverlay();
|
function hideOverlay() { $("#xiaobaix-story-outline-overlay").hide(); }
|
||||||
|
|
||||||
if (!iframeLoaded) {
|
|
||||||
frameReady = false;
|
|
||||||
const f = document.getElementById("xiaobaix-story-outline-iframe");
|
|
||||||
if (f) f.src = IFRAME_PATH;
|
|
||||||
iframeLoaded = true;
|
|
||||||
updateLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#xiaobaix-story-outline-overlay").show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideOverlay() {
|
|
||||||
$("#xiaobaix-story-outline-overlay").hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastIsMobile = isMobile();
|
let lastIsMobile = isMobile();
|
||||||
window.addEventListener('resize', () => { const nowIsMobile = isMobile(); if (nowIsMobile !== lastIsMobile) { lastIsMobile = nowIsMobile; updateLayout(); } });
|
window.addEventListener('resize', () => { const nowIsMobile = isMobile(); if (nowIsMobile !== lastIsMobile) { lastIsMobile = nowIsMobile; updateLayout(); } });
|
||||||
|
|
||||||
|
|
||||||
// ==================== 11. 事件与初始化 ====================
|
// ==================== 11. 事件与初始化 ====================
|
||||||
|
|
||||||
let eventsRegistered = false;
|
let eventsRegistered = false;
|
||||||
@@ -1032,13 +1135,17 @@ function initBtns() {
|
|||||||
function registerEvents() {
|
function registerEvents() {
|
||||||
if (eventsRegistered) return;
|
if (eventsRegistered) return;
|
||||||
eventsRegistered = true;
|
eventsRegistered = true;
|
||||||
|
|
||||||
initBtns();
|
initBtns();
|
||||||
|
|
||||||
events.on(event_types.CHAT_CHANGED, () => { setTimeout(initBtns, 80); setTimeout(injectOutline, 100); });
|
events.on(event_types.CHAT_CHANGED, () => { setTimeout(initBtns, 80); setTimeout(injectOutline, 100); });
|
||||||
events.on(event_types.GENERATION_STARTED, injectOutline);
|
events.on(event_types.GENERATION_STARTED, injectOutline);
|
||||||
|
|
||||||
const handler = d => setTimeout(() => {
|
const handler = d => setTimeout(() => {
|
||||||
const id = d?.element ? $(d.element).attr("mesid") : d?.messageId;
|
const id = d?.element ? $(d.element).attr("mesid") : d?.messageId;
|
||||||
id == null ? initBtns() : addBtnToMsg(id);
|
id == null ? initBtns() : addBtnToMsg(id);
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
events.onMany([
|
events.onMany([
|
||||||
event_types.USER_MESSAGE_RENDERED,
|
event_types.USER_MESSAGE_RENDERED,
|
||||||
event_types.CHARACTER_MESSAGE_RENDERED,
|
event_types.CHARACTER_MESSAGE_RENDERED,
|
||||||
@@ -1047,6 +1154,7 @@ function registerEvents() {
|
|||||||
event_types.MESSAGE_SWIPED,
|
event_types.MESSAGE_SWIPED,
|
||||||
event_types.MESSAGE_EDITED
|
event_types.MESSAGE_EDITED
|
||||||
], handler);
|
], handler);
|
||||||
|
|
||||||
setupSTEvents();
|
setupSTEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1056,13 +1164,14 @@ function cleanup() {
|
|||||||
$(".xiaobaix-story-outline-btn").remove();
|
$(".xiaobaix-story-outline-btn").remove();
|
||||||
hideOverlay();
|
hideOverlay();
|
||||||
overlayCreated = false; frameReady = false; pendingMsgs = [];
|
overlayCreated = false; frameReady = false; pendingMsgs = [];
|
||||||
iframeLoaded = false;
|
|
||||||
window.removeEventListener("message", handleMsg);
|
window.removeEventListener("message", handleMsg);
|
||||||
document.getElementById("xiaobaix-story-outline-overlay")?.remove();
|
document.getElementById("xiaobaix-story-outline-overlay")?.remove();
|
||||||
removePrompt();
|
removePrompt();
|
||||||
if (presetCleanup) { presetCleanup(); presetCleanup = null; }
|
if (presetCleanup) { presetCleanup(); presetCleanup = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Toggle 监听(始终注册)====================
|
||||||
|
|
||||||
$(document).on("xiaobaix:storyOutline:toggle", (_e, enabled) => {
|
$(document).on("xiaobaix:storyOutline:toggle", (_e, enabled) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
registerEvents();
|
registerEvents();
|
||||||
@@ -1083,6 +1192,8 @@ document.addEventListener('xiaobaixEnabledChanged', e => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== 初始化 ====================
|
||||||
|
|
||||||
jQuery(() => {
|
jQuery(() => {
|
||||||
if (!getSettings().storyOutline?.enabled) return;
|
if (!getSettings().storyOutline?.enabled) return;
|
||||||
registerEvents();
|
registerEvents();
|
||||||
|
|||||||
Reference in New Issue
Block a user