Add files via upload

This commit is contained in:
RT15548
2025-12-21 01:47:38 +08:00
committed by GitHub
parent c37b2bbe4e
commit 74fc36c2b9
35 changed files with 15216 additions and 14635 deletions

138
README.md
View File

@@ -1,64 +1,74 @@
# LittleWhiteBox # LittleWhiteBox
SillyTavern 扩展插件 - 小白X SillyTavern 扩展插件 - 小白X
## 📁 目录结构 ## 📁 目录结构
``` ```
LittleWhiteBox/ LittleWhiteBox/
├── manifest.json # 插件配置清单 ├── manifest.json # 插件配置清单
├── index.js # 主入口文件 ├── index.js # 主入口文件
├── settings.html # 设置页面模板 ├── settings.html # 设置页面模板
├── style.css # 全局样式 ├── style.css # 全局样式
├── modules/ # 功能模块目录 ├── modules/ # 功能模块目录
│ ├── streaming-generation.js # 流式生成 │ ├── streaming-generation.js # 流式生成
│ ├── dynamic-prompt.js # 动态提示词 │ ├── dynamic-prompt.js # 动态提示词
│ ├── immersive-mode.js # 沉浸模式 │ ├── immersive-mode.js # 沉浸模式
│ ├── message-preview.js # 消息预览 │ ├── message-preview.js # 消息预览
│ ├── wallhaven-background.js # 壁纸背景 │ ├── wallhaven-background.js # 壁纸背景
│ ├── button-collapse.js # 按钮折叠 │ ├── button-collapse.js # 按钮折叠
│ ├── control-audio.js # 音频控制 │ ├── control-audio.js # 音频控制
│ ├── script-assistant.js # 脚本助手 │ ├── script-assistant.js # 脚本助手
│ │ │ │
│ ├── variables/ # 变量系统 │ ├── variables/ # 变量系统
│ │ ├── variables-core.js │ │ ├── variables-core.js
│ │ └── variables-panel.js │ │ └── variables-panel.js
│ │ │ │
│ ├── template-editor/ # 模板编辑器 │ ├── template-editor/ # 模板编辑器
│ │ ├── template-editor.js │ │ ├── template-editor.js
│ │ └── template-editor.html │ │ └── template-editor.html
│ │ │ │
│ ├── scheduled-tasks/ # 定时任务 │ ├── scheduled-tasks/ # 定时任务
│ │ ├── scheduled-tasks.js │ │ ├── scheduled-tasks.js
│ │ ├── scheduled-tasks.html │ │ ├── scheduled-tasks.html
│ │ └── embedded-tasks.html │ │ └── embedded-tasks.html
│ │ │ │
│ ├── story-summary/ # 故事摘要 │ ├── story-summary/ # 故事摘要
│ │ ├── story-summary.js │ │ ├── story-summary.js
│ │ └── story-summary.html │ │ └── story-summary.html
│ │ │ │
│ └── story-outline/ # 故事大纲 │ └── story-outline/ # 故事大纲
│ ├── story-outline.js │ ├── story-outline.js
│ ├── story-outline-prompt.js │ ├── story-outline-prompt.js
│ └── story-outline.html │ └── story-outline.html
├── bridges/ # 外部桥接模块 ├── bridges/ # 外部桥接模块
│ ├── worldbook-bridge.js # 世界书桥接 │ ├── worldbook-bridge.js # 世界书桥接
│ ├── call-generate-service.js # 生成服务调用 │ ├── call-generate-service.js # 生成服务调用
│ └── wrapper-iframe.js # iframe 包装器 │ └── wrapper-iframe.js # iframe 包装器
├── ui/ # UI 模板 ├── ui/ # UI 模板
│ └── character-updater-menus.html │ └── character-updater-menus.html
└── docs/ # 文档 └── docs/ # 文档
├── script-docs.md # 脚本文档 ├── script-docs.md # 脚本文档
├── LICENSE.md # 许可证 ├── LICENSE.md # 许可证
├── COPYRIGHT # 版权信息 ├── COPYRIGHT # 版权信息
└── NOTICE # 声明 └── NOTICE # 声明
``` ```
## 📝 模块组织规则
## 📄 许可证
- **单文件模块**:直接放在 `modules/` 目录下
详见 `docs/LICENSE.md` - **多文件模块**:创建子目录,包含相关的 JS、HTML 等文件
- **桥接模块**:与外部系统交互的独立模块放在 `bridges/`
- **避免使用 `index.js`**:每个模块文件直接命名,不使用 `index.js`
## 🔄 版本历史
- v2.2.2 - 目录结构重构2025-12-08
## 📄 许可证
详见 `docs/LICENSE.md`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,105 +1,105 @@
(function(){ (function(){
function defineCallGenerate(){ function defineCallGenerate(){
function sanitizeOptions(options){ function sanitizeOptions(options){
try{ try{
return JSON.parse(JSON.stringify(options,function(k,v){return(typeof v==='function')?undefined:v})) return JSON.parse(JSON.stringify(options,function(k,v){return(typeof v==='function')?undefined:v}))
}catch(_){ }catch(_){
try{ try{
const seen=new WeakSet(); const seen=new WeakSet();
const clone=(val)=>{ const clone=(val)=>{
if(val===null||val===undefined)return val; if(val===null||val===undefined)return val;
const t=typeof val; const t=typeof val;
if(t==='function')return undefined; if(t==='function')return undefined;
if(t!=='object')return val; if(t!=='object')return val;
if(seen.has(val))return undefined; if(seen.has(val))return undefined;
seen.add(val); seen.add(val);
if(Array.isArray(val)){ if(Array.isArray(val)){
const arr=[];for(let i=0;i<val.length;i++){const v=clone(val[i]);if(v!==undefined)arr.push(v)}return arr; const arr=[];for(let i=0;i<val.length;i++){const v=clone(val[i]);if(v!==undefined)arr.push(v)}return arr;
} }
const proto=Object.getPrototypeOf(val); const proto=Object.getPrototypeOf(val);
if(proto!==Object.prototype&&proto!==null)return undefined; if(proto!==Object.prototype&&proto!==null)return undefined;
const out={}; const out={};
for(const k in val){if(Object.prototype.hasOwnProperty.call(val,k)){const v=clone(val[k]);if(v!==undefined)out[k]=v}} for(const k in val){if(Object.prototype.hasOwnProperty.call(val,k)){const v=clone(val[k]);if(v!==undefined)out[k]=v}}
return out; return out;
}; };
return clone(options); return clone(options);
}catch(__){return{}} }catch(__){return{}}
} }
} }
function CallGenerateImpl(options){ function CallGenerateImpl(options){
return new Promise(function(resolve,reject){ return new Promise(function(resolve,reject){
try{ try{
function post(m){try{parent.postMessage(m,'*')}catch(e){}} function post(m){try{parent.postMessage(m,'*')}catch(e){}}
if(!options||typeof options!=='object'){reject(new Error('Invalid options'));return} if(!options||typeof options!=='object'){reject(new Error('Invalid options'));return}
var id=Date.now().toString(36)+Math.random().toString(36).slice(2); var id=Date.now().toString(36)+Math.random().toString(36).slice(2);
function onMessage(e){ function onMessage(e){
var d=e&&e.data||{}; var d=e&&e.data||{};
if(d.source!=='xiaobaix-host'||d.id!==id)return; if(d.source!=='xiaobaix-host'||d.id!==id)return;
if(d.type==='generateStreamStart'&&options.streaming&&options.streaming.onStart){try{options.streaming.onStart(d.sessionId)}catch(_){}} if(d.type==='generateStreamStart'&&options.streaming&&options.streaming.onStart){try{options.streaming.onStart(d.sessionId)}catch(_){}}
else if(d.type==='generateStreamChunk'&&options.streaming&&options.streaming.onChunk){try{options.streaming.onChunk(d.chunk,d.accumulated)}catch(_){}} else if(d.type==='generateStreamChunk'&&options.streaming&&options.streaming.onChunk){try{options.streaming.onChunk(d.chunk,d.accumulated)}catch(_){}}
else if(d.type==='generateStreamComplete'){try{window.removeEventListener('message',onMessage)}catch(_){} else if(d.type==='generateStreamComplete'){try{window.removeEventListener('message',onMessage)}catch(_){}
resolve(d.result)} resolve(d.result)}
else if(d.type==='generateStreamError'){try{window.removeEventListener('message',onMessage)}catch(_){} else if(d.type==='generateStreamError'){try{window.removeEventListener('message',onMessage)}catch(_){}
reject(new Error(d.error||'Stream failed'))} reject(new Error(d.error||'Stream failed'))}
else if(d.type==='generateResult'){try{window.removeEventListener('message',onMessage)}catch(_){} else if(d.type==='generateResult'){try{window.removeEventListener('message',onMessage)}catch(_){}
resolve(d.result)} resolve(d.result)}
else if(d.type==='generateError'){try{window.removeEventListener('message',onMessage)}catch(_){} else if(d.type==='generateError'){try{window.removeEventListener('message',onMessage)}catch(_){}
reject(new Error(d.error||'Generation failed'))} reject(new Error(d.error||'Generation failed'))}
} }
try{window.addEventListener('message',onMessage)}catch(_){} try{window.addEventListener('message',onMessage)}catch(_){}
var sanitized=sanitizeOptions(options); var sanitized=sanitizeOptions(options);
post({type:'generateRequest',id:id,options:sanitized}); post({type:'generateRequest',id:id,options:sanitized});
setTimeout(function(){try{window.removeEventListener('message',onMessage)}catch(e){};reject(new Error('Generation timeout'))},300000); setTimeout(function(){try{window.removeEventListener('message',onMessage)}catch(e){};reject(new Error('Generation timeout'))},300000);
}catch(e){reject(e)} }catch(e){reject(e)}
}) })
} }
try{window.CallGenerate=CallGenerateImpl}catch(e){} try{window.CallGenerate=CallGenerateImpl}catch(e){}
try{window.callGenerate=CallGenerateImpl}catch(e){} try{window.callGenerate=CallGenerateImpl}catch(e){}
try{window.__xb_callGenerate_loaded=true}catch(e){} try{window.__xb_callGenerate_loaded=true}catch(e){}
} }
try{defineCallGenerate()}catch(e){} try{defineCallGenerate()}catch(e){}
})(); })();
(function(){ (function(){
function applyAvatarCss(urls){ function applyAvatarCss(urls){
try{ try{
const root=document.documentElement; const root=document.documentElement;
root.style.setProperty('--xb-user-avatar',urls&&urls.user?`url("${urls.user}")`:'none'); root.style.setProperty('--xb-user-avatar',urls&&urls.user?`url("${urls.user}")`:'none');
root.style.setProperty('--xb-char-avatar',urls&&urls.char?`url("${urls.char}")`:'none'); root.style.setProperty('--xb-char-avatar',urls&&urls.char?`url("${urls.char}")`:'none');
if(!document.getElementById('xb-avatar-style')){ if(!document.getElementById('xb-avatar-style')){
const css=` const css=`
.xb-avatar,.xb-user-avatar,.xb-char-avatar{ .xb-avatar,.xb-user-avatar,.xb-char-avatar{
width:36px;height:36px;border-radius:50%; width:36px;height:36px;border-radius:50%;
background-size:cover;background-position:center;background-repeat:no-repeat; background-size:cover;background-position:center;background-repeat:no-repeat;
display:inline-block display:inline-block
} }
.xb-user-avatar{background-image:var(--xb-user-avatar)} .xb-user-avatar{background-image:var(--xb-user-avatar)}
.xb-char-avatar{background-image:var(--xb-char-avatar)} .xb-char-avatar{background-image:var(--xb-char-avatar)}
`; `;
const style=document.createElement('style'); const style=document.createElement('style');
style.id='xb-avatar-style'; style.id='xb-avatar-style';
style.textContent=css; style.textContent=css;
document.head.appendChild(style); document.head.appendChild(style);
} }
}catch(_){} }catch(_){}
} }
function requestAvatars(){ function requestAvatars(){
try{parent.postMessage({type:'getAvatars'},'*')}catch(_){} try{parent.postMessage({type:'getAvatars'},'*')}catch(_){}
} }
function onMessage(e){ function onMessage(e){
const d=e&&e.data||{}; const d=e&&e.data||{};
if(d&&d.source==='xiaobaix-host'&&d.type==='avatars'){ if(d&&d.source==='xiaobaix-host'&&d.type==='avatars'){
applyAvatarCss(d.urls); applyAvatarCss(d.urls);
try{window.removeEventListener('message',onMessage)}catch(_){} try{window.removeEventListener('message',onMessage)}catch(_){}
} }
} }
try{ try{
window.addEventListener('message',onMessage); window.addEventListener('message',onMessage);
if(document.readyState==='loading'){ if(document.readyState==='loading'){
document.addEventListener('DOMContentLoaded',requestAvatars,{once:true}); document.addEventListener('DOMContentLoaded',requestAvatars,{once:true});
}else{ }else{
requestAvatars(); requestAvatars();
} }
window.addEventListener('load',requestAvatars,{once:true}); window.addEventListener('load',requestAvatars,{once:true});
}catch(_){} }catch(_){}
})(); })();

View File

@@ -1,7 +1,7 @@
/** /**
* LittleWhiteBox 共享常量 * LittleWhiteBox 共享常量
*/ */
export const EXT_ID = "LittleWhiteBox"; export const EXT_ID = "LittleWhiteBox";
export const EXT_NAME = "小白X"; export const EXT_NAME = "小白X";
export const extensionFolderPath = `scripts/extensions/third-party/${EXT_ID}`; export const extensionFolderPath = `scripts/extensions/third-party/${EXT_ID}`;

138
core/server-storage.js Normal file
View File

@@ -0,0 +1,138 @@
// ═══════════════════════════════════════════════════════════════════════════
// 服务器文件存储工具
// ═══════════════════════════════════════════════════════════════════════════
import { getRequestHeaders } from '../../../../../script.js';
import { debounce } from '../../../../utils.js';
const toBase64 = (text) => btoa(unescape(encodeURIComponent(text)));
class StorageFile {
constructor(filename, opts = {}) {
this.filename = filename;
this.cache = null;
this._loading = null;
this._dirtyVersion = 0;
this._savedVersion = 0;
this._saving = false;
this._pendingSave = false;
this._retryCount = 0;
this._retryTimer = null;
this._maxRetries = Number.isFinite(opts.maxRetries) ? opts.maxRetries : 5;
const debounceMs = Number.isFinite(opts.debounceMs) ? opts.debounceMs : 2000;
this._saveDebounced = debounce(() => this.saveNow(), debounceMs);
}
async load() {
if (this.cache !== null) return this.cache;
if (this._loading) return this._loading;
this._loading = (async () => {
try {
const res = await fetch(`/user/files/${this.filename}`, {
headers: getRequestHeaders(),
cache: 'no-cache',
});
if (!res.ok) {
this.cache = {};
return this.cache;
}
const text = await res.text();
this.cache = text ? (JSON.parse(text) || {}) : {};
} catch {
this.cache = {};
} finally {
this._loading = null;
}
return this.cache;
})();
return this._loading;
}
async get(key, defaultValue = null) {
const data = await this.load();
return data[key] ?? defaultValue;
}
async set(key, value) {
const data = await this.load();
data[key] = value;
this._dirtyVersion++;
this._saveDebounced();
}
async delete(key) {
const data = await this.load();
if (key in data) {
delete data[key];
this._dirtyVersion++;
this._saveDebounced();
}
}
async saveNow() {
if (this._saving) {
this._pendingSave = true;
return;
}
if (!this.cache || this._dirtyVersion === this._savedVersion) return;
this._saving = true;
this._pendingSave = false;
const versionToSave = this._dirtyVersion;
try {
const json = JSON.stringify(this.cache);
const base64 = toBase64(json);
const res = await fetch('/api/files/upload', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ name: this.filename, data: base64 }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
this._savedVersion = Math.max(this._savedVersion, versionToSave);
this._retryCount = 0;
if (this._retryTimer) {
clearTimeout(this._retryTimer);
this._retryTimer = null;
}
} catch (err) {
console.error('[ServerStorage] 保存失败:', err);
this._retryCount++;
const delay = Math.min(30000, 2000 * (2 ** Math.max(0, this._retryCount - 1)));
if (!this._retryTimer && this._retryCount <= this._maxRetries) {
this._retryTimer = setTimeout(() => {
this._retryTimer = null;
this.saveNow();
}, delay);
}
} finally {
this._saving = false;
if (this._pendingSave || this._dirtyVersion > this._savedVersion) {
this._saveDebounced();
}
}
}
clearCache() {
this.cache = null;
this._loading = null;
}
getCacheSize() {
if (!this.cache) return 0;
return Object.keys(this.cache).length;
}
getCacheBytes() {
if (!this.cache) return 0;
try {
return JSON.stringify(this.cache).length * 2;
} catch {
return 0;
}
}
}
export const TasksStorage = new StorageFile('LittleWhiteBox_Tasks.json');

View File

@@ -1,30 +1,30 @@
import { getContext } from "../../../../extensions.js"; import { getContext } from "../../../../extensions.js";
/** /**
* 执行 SillyTavern 斜杠命令 * 执行 SillyTavern 斜杠命令
* @param {string} command - 要执行的命令 * @param {string} command - 要执行的命令
* @returns {Promise<any>} 命令执行结果 * @returns {Promise<any>} 命令执行结果
*/ */
export async function executeSlashCommand(command) { export async function executeSlashCommand(command) {
try { try {
if (!command) return { error: "命令为空" }; if (!command) return { error: "命令为空" };
if (!command.startsWith('/')) command = '/' + command; if (!command.startsWith('/')) command = '/' + command;
const { executeSlashCommands, substituteParams } = getContext(); const { executeSlashCommands, substituteParams } = getContext();
if (typeof executeSlashCommands !== 'function') throw new Error("executeSlashCommands 函数不可用"); if (typeof executeSlashCommands !== 'function') throw new Error("executeSlashCommands 函数不可用");
command = substituteParams(command); command = substituteParams(command);
const result = await executeSlashCommands(command, true); const result = await executeSlashCommands(command, true);
if (result && typeof result === 'object' && result.pipe !== undefined) { if (result && typeof result === 'object' && result.pipe !== undefined) {
const pipeValue = result.pipe; const pipeValue = result.pipe;
if (typeof pipeValue === 'string') { if (typeof pipeValue === 'string') {
try { return JSON.parse(pipeValue); } catch { return pipeValue; } try { return JSON.parse(pipeValue); } catch { return pipeValue; }
} }
return pipeValue; return pipeValue;
} }
if (typeof result === 'string' && result.trim()) { if (typeof result === 'string' && result.trim()) {
try { return JSON.parse(result); } catch { return result; } try { return JSON.parse(result); } catch { return result; }
} }
return result === undefined ? "" : result; return result === undefined ? "" : result;
} catch (err) { } catch (err) {
throw err; throw err;
} }
} }

View File

@@ -1,73 +1,73 @@
LittleWhiteBox (小白X) - Copyright and Attribution Requirements LittleWhiteBox (小白X) - Copyright and Attribution Requirements
================================================================ ================================================================
Copyright 2025 biex Copyright 2025 biex
This software is licensed under the Apache License 2.0 This software is licensed under the Apache License 2.0
with additional custom attribution requirements. with additional custom attribution requirements.
MANDATORY ATTRIBUTION REQUIREMENTS MANDATORY ATTRIBUTION REQUIREMENTS
================================== ==================================
1. AUTHOR ATTRIBUTION 1. AUTHOR ATTRIBUTION
- The original author "biex" MUST be prominently credited in any derivative work - The original author "biex" MUST be prominently credited in any derivative work
- This credit must appear in: - This credit must appear in:
* Software user interface (visible to end users) * Software user interface (visible to end users)
* Documentation and README files * Documentation and README files
* Source code headers * Source code headers
* About/Credits sections * About/Credits sections
* Any promotional or marketing materials * Any promotional or marketing materials
2. PROJECT ATTRIBUTION 2. PROJECT ATTRIBUTION
- The project name "LittleWhiteBox" and "小白X" must be credited - The project name "LittleWhiteBox" and "小白X" must be credited
- Required attribution format: "Based on LittleWhiteBox by biex" - Required attribution format: "Based on LittleWhiteBox by biex"
- Project URL must be included: https://github.com/RT15548/LittleWhiteBox - Project URL must be included: https://github.com/RT15548/LittleWhiteBox
3. SOURCE CODE DISCLOSURE 3. SOURCE CODE DISCLOSURE
- Any modification, enhancement, or derivative work MUST be open source - Any modification, enhancement, or derivative work MUST be open source
- Source code must be publicly accessible under the same license terms - Source code must be publicly accessible under the same license terms
- All changes must be clearly documented and attributed - All changes must be clearly documented and attributed
4. COMMERCIAL USE 4. COMMERCIAL USE
- Commercial use is permitted under the Apache License 2.0 terms - Commercial use is permitted under the Apache License 2.0 terms
- Attribution requirements still apply for commercial use - Attribution requirements still apply for commercial use
- No additional permission required for commercial use - No additional permission required for commercial use
5. TRADEMARK PROTECTION 5. TRADEMARK PROTECTION
- "LittleWhiteBox" and "小白X" are trademarks of the original author - "LittleWhiteBox" and "小白X" are trademarks of the original author
- Derivative works may not use these names without explicit permission - Derivative works may not use these names without explicit permission
- Alternative naming must clearly indicate the derivative nature - Alternative naming must clearly indicate the derivative nature
VIOLATION CONSEQUENCES VIOLATION CONSEQUENCES
===================== =====================
Any violation of these attribution requirements will result in: Any violation of these attribution requirements will result in:
- Immediate termination of the license grant - Immediate termination of the license grant
- Legal action for copyright infringement - Legal action for copyright infringement
- Demand for removal of infringing content - Demand for removal of infringing content
COMPLIANCE EXAMPLES COMPLIANCE EXAMPLES
================== ==================
✅ CORRECT Attribution Examples: ✅ CORRECT Attribution Examples:
- "Powered by LittleWhiteBox by biex" - "Powered by LittleWhiteBox by biex"
- "Based on LittleWhiteBox (https://github.com/RT15548/LittleWhiteBox) by biex" - "Based on LittleWhiteBox (https://github.com/RT15548/LittleWhiteBox) by biex"
- "Enhanced version of LittleWhiteBox by biex - Original: [repository URL]" - "Enhanced version of LittleWhiteBox by biex - Original: [repository URL]"
❌ INCORRECT Examples: ❌ INCORRECT Examples:
- Using the code without any attribution - Using the code without any attribution
- Claiming original authorship - Claiming original authorship
- Using "LittleWhiteBox" name for derivative works - Using "LittleWhiteBox" name for derivative works
- Commercial use without permission - Commercial use without permission
- Closed-source modifications - Closed-source modifications
CONTACT INFORMATION CONTACT INFORMATION
================== ==================
For licensing inquiries or attribution questions: For licensing inquiries or attribution questions:
- Repository: https://github.com/RT15548/LittleWhiteBox - Repository: https://github.com/RT15548/LittleWhiteBox
- Author: biex - Author: biex
- License: Apache-2.0 WITH Custom-Attribution-Requirements - License: Apache-2.0 WITH Custom-Attribution-Requirements
This copyright notice and attribution requirements must be included in all This copyright notice and attribution requirements must be included in all
copies or substantial portions of the software. copies or substantial portions of the software.

View File

@@ -1,33 +1,33 @@
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/
Copyright 2025 biex Copyright 2025 biex
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
ADDITIONAL TERMS: ADDITIONAL TERMS:
In addition to the terms of the Apache License 2.0, the following In addition to the terms of the Apache License 2.0, the following
attribution requirement applies to any use, modification, or distribution attribution requirement applies to any use, modification, or distribution
of this software: of this software:
ATTRIBUTION REQUIREMENT: ATTRIBUTION REQUIREMENT:
If you reference, modify, or distribute any file from this project, If you reference, modify, or distribute any file from this project,
you must include attribution to the original author "biex" in your you must include attribution to the original author "biex" in your
project documentation, README, or credits section. project documentation, README, or credits section.
Simple attribution format: "Based on LittleWhiteBox by biex" Simple attribution format: "Based on LittleWhiteBox by biex"
For the complete Apache License 2.0 text, see: For the complete Apache License 2.0 text, see:
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0

View File

@@ -1,95 +1,95 @@
LittleWhiteBox (小白X) - Third-Party Notices and Attributions LittleWhiteBox (小白X) - Third-Party Notices and Attributions
================================================================ ================================================================
This software contains code and dependencies from various third-party sources. This software contains code and dependencies from various third-party sources.
The following notices and attributions are required by their respective licenses. The following notices and attributions are required by their respective licenses.
PRIMARY SOFTWARE PRIMARY SOFTWARE
================ ================
LittleWhiteBox (小白X) LittleWhiteBox (小白X)
Copyright 2025 biex Copyright 2025 biex
Licensed under Apache-2.0 WITH Custom-Attribution-Requirements Licensed under Apache-2.0 WITH Custom-Attribution-Requirements
Repository: https://github.com/RT15548/LittleWhiteBox Repository: https://github.com/RT15548/LittleWhiteBox
RUNTIME DEPENDENCIES RUNTIME DEPENDENCIES
==================== ====================
This extension is designed to work with SillyTavern and relies on the following This extension is designed to work with SillyTavern and relies on the following
SillyTavern modules and APIs: SillyTavern modules and APIs:
1. SillyTavern Core Framework 1. SillyTavern Core Framework
- Copyright: SillyTavern Contributors - Copyright: SillyTavern Contributors
- License: AGPL-3.0 - License: AGPL-3.0
- Repository: https://github.com/SillyTavern/SillyTavern - Repository: https://github.com/SillyTavern/SillyTavern
2. SillyTavern Extensions API 2. SillyTavern Extensions API
- Used modules: extensions.js, script.js - Used modules: extensions.js, script.js
- Provides: Extension framework, settings management, event system - Provides: Extension framework, settings management, event system
3. SillyTavern Slash Commands 3. SillyTavern Slash Commands
- Used modules: slash-commands.js, SlashCommandParser.js - Used modules: slash-commands.js, SlashCommandParser.js
- Provides: Command execution framework - Provides: Command execution framework
4. SillyTavern UI Components 4. SillyTavern UI Components
- Used modules: popup.js, utils.js - Used modules: popup.js, utils.js
- Provides: User interface components and utilities - Provides: User interface components and utilities
BROWSER APIS AND STANDARDS BROWSER APIS AND STANDARDS
========================== ==========================
This software uses standard web browser APIs: This software uses standard web browser APIs:
- DOM API (Document Object Model) - DOM API (Document Object Model)
- Fetch API for HTTP requests - Fetch API for HTTP requests
- PostMessage API for iframe communication - PostMessage API for iframe communication
- Local Storage API for data persistence - Local Storage API for data persistence
- Mutation Observer API for DOM monitoring - Mutation Observer API for DOM monitoring
JAVASCRIPT LIBRARIES JAVASCRIPT LIBRARIES
==================== ====================
The software may interact with the following JavaScript libraries The software may interact with the following JavaScript libraries
that are part of the SillyTavern environment: that are part of the SillyTavern environment:
1. jQuery 1. jQuery
- Copyright: jQuery Foundation and contributors - Copyright: jQuery Foundation and contributors
- License: MIT License - License: MIT License
- Used for: DOM manipulation and event handling - Used for: DOM manipulation and event handling
2. Toastr (if available) 2. Toastr (if available)
- Copyright: CodeSeven - Copyright: CodeSeven
- License: MIT License - License: MIT License
- Used for: Notification display - Used for: Notification display
DEVELOPMENT TOOLS DEVELOPMENT TOOLS
================= =================
The following tools were used in development (not distributed): The following tools were used in development (not distributed):
- Visual Studio Code - Visual Studio Code
- Git version control - Git version control
- Various Node.js development tools - Various Node.js development tools
ATTRIBUTION REQUIREMENTS ATTRIBUTION REQUIREMENTS
======================== ========================
When distributing this software or derivative works, you must: When distributing this software or derivative works, you must:
1. Include this NOTICE file 1. Include this NOTICE file
2. Maintain all copyright notices in source code 2. Maintain all copyright notices in source code
3. Provide attribution to the original author "biex" 3. Provide attribution to the original author "biex"
4. Include a link to the original repository 4. Include a link to the original repository
5. Comply with Apache-2.0 license requirements 5. Comply with Apache-2.0 license requirements
6. Follow the custom attribution requirements in LICENSE.md 6. Follow the custom attribution requirements in LICENSE.md
DISCLAIMER DISCLAIMER
========== ==========
This software is provided "AS IS" without warranty of any kind. This software is provided "AS IS" without warranty of any kind.
The author disclaims all warranties, express or implied, including The author disclaims all warranties, express or implied, including
but not limited to the warranties of merchantability, fitness for but not limited to the warranties of merchantability, fitness for
a particular purpose, and non-infringement. a particular purpose, and non-infringement.
For complete license terms, see LICENSE.md For complete license terms, see LICENSE.md
For attribution requirements, see COPYRIGHT For attribution requirements, see COPYRIGHT
Last updated: 2025-01-14 Last updated: 2025-01-14

File diff suppressed because it is too large Load Diff

123
index.js
View File

@@ -1,6 +1,6 @@
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 导入 // Imports
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
import { extension_settings, getContext } from "../../../extensions.js"; import { extension_settings, getContext } from "../../../extensions.js";
import { saveSettingsDebounced, eventSource, event_types, getRequestHeaders } from "../../../../script.js"; import { saveSettingsDebounced, eventSource, event_types, getRequestHeaders } from "../../../../script.js";
@@ -35,9 +35,9 @@ import { initNovelDraw, cleanupNovelDraw } from "./modules/novel-draw/novel-draw
import "./modules/story-summary/story-summary.js"; import "./modules/story-summary/story-summary.js";
import "./modules/story-outline/story-outline.js"; import "./modules/story-outline/story-outline.js";
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 常量与默认设置 // Constants and Default Settings
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
const MODULE_NAME = "xiaobaix-memory"; const MODULE_NAME = "xiaobaix-memory";
@@ -67,9 +67,9 @@ extension_settings[EXT_ID] = extension_settings[EXT_ID] || {
const settings = extension_settings[EXT_ID]; const settings = extension_settings[EXT_ID];
if (settings.dynamicPrompt && !settings.fourthWall) settings.fourthWall = settings.dynamicPrompt; if (settings.dynamicPrompt && !settings.fourthWall) settings.fourthWall = settings.dynamicPrompt;
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 废弃数据清理 // Deprecated Data Cleanup
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
const DEPRECATED_KEYS = [ const DEPRECATED_KEYS = [
'characterUpdater', 'characterUpdater',
@@ -87,19 +87,19 @@ function cleanupDeprecatedData() {
if (key in s) { if (key in s) {
delete s[key]; delete s[key];
cleaned = true; cleaned = true;
console.log(`[LittleWhiteBox] 清理废弃数据: ${key}`); console.log(`[LittleWhiteBox] Cleaned deprecated data: ${key}`);
} }
} }
if (cleaned) { if (cleaned) {
saveSettingsDebounced(); saveSettingsDebounced();
console.log('[LittleWhiteBox] 废弃数据清理完成'); console.log('[LittleWhiteBox] Deprecated data cleanup complete');
} }
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 状态变量 // State Variables
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
let isXiaobaixEnabled = settings.enabled; let isXiaobaixEnabled = settings.enabled;
let moduleCleanupFunctions = new Map(); let moduleCleanupFunctions = new Map();
@@ -117,9 +117,9 @@ window.testRemoveUpdateUI = () => {
removeAllUpdateNotices(); removeAllUpdateNotices();
}; };
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 更新检查 // Update Check
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
async function checkLittleWhiteBoxUpdate() { async function checkLittleWhiteBoxUpdate() {
try { try {
@@ -148,16 +148,16 @@ async function updateLittleWhiteBoxExtension() {
}); });
if (!response.ok) { if (!response.ok) {
const text = await response.text(); const text = await response.text();
toastr.error(text || response.statusText, '小白X更新失败', { timeOut: 5000 }); toastr.error(text || response.statusText, 'LittleWhiteBox update failed', { timeOut: 5000 });
return false; return false;
} }
const data = await response.json(); const data = await response.json();
const message = data.isUpToDate ? '小白X已是最新版本' : `小白X已更新`; const message = data.isUpToDate ? 'LittleWhiteBox is up to date' : `LittleWhiteBox updated`;
const title = data.isUpToDate ? '' : '请刷新页面以应用更新'; const title = data.isUpToDate ? '' : '请刷新页面以应用更新';
toastr.success(message, title); toastr.success(message, title);
return true; return true;
} catch (error) { } catch (error) {
toastr.error('更新过程中发生错误', '小白X更新失败'); toastr.error('Error during update', 'LittleWhiteBox update failed');
return false; return false;
} }
} }
@@ -213,7 +213,7 @@ function addUpdateDownloadButton() {
const updateButton = document.createElement('div'); const updateButton = document.createElement('div');
updateButton.id = 'littlewhitebox-update-extension'; updateButton.id = 'littlewhitebox-update-extension';
updateButton.className = 'menu_button fa-solid fa-cloud-arrow-down interactable has-update'; updateButton.className = 'menu_button fa-solid fa-cloud-arrow-down interactable has-update';
updateButton.title = '下载并安装小白x的更新'; updateButton.title = '下载并安装小白X的更新';
updateButton.tabIndex = 0; updateButton.tabIndex = 0;
try { try {
totalSwitchDivider.style.display = 'flex'; totalSwitchDivider.style.display = 'flex';
@@ -246,9 +246,9 @@ async function performExtensionUpdateCheck() {
} catch (error) {} } catch (error) {}
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 模块清理注册 // Module Cleanup Registration
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
function registerModuleCleanup(moduleName, cleanupFunction) { function registerModuleCleanup(moduleName, cleanupFunction) {
moduleCleanupFunctions.set(moduleName, cleanupFunction); moduleCleanupFunctions.set(moduleName, cleanupFunction);
@@ -295,9 +295,9 @@ function cleanupAllResources() {
removeSkeletonStyles(); removeSkeletonStyles();
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 工具函数 // Utility Functions
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
async function waitForElement(selector, root = document, timeout = 10000) { async function waitForElement(selector, root = document, timeout = 10000) {
const start = Date.now(); const start = Date.now();
@@ -309,9 +309,9 @@ async function waitForElement(selector, root = document, timeout = 10000) {
return null; return null;
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 设置控件禁用/启用 // Settings Controls Toggle
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
function toggleSettingsControls(enabled) { function toggleSettingsControls(enabled) {
const controls = [ const controls = [
@@ -360,11 +360,11 @@ function setActiveClass(enable) {
document.body.classList.toggle('xiaobaix-active', !!enable); document.body.classList.toggle('xiaobaix-active', !!enable);
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 功能总开关切换 // Toggle All Features
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
function toggleAllFeatures(enabled) { async function toggleAllFeatures(enabled) {
if (enabled) { if (enabled) {
if (settings.renderEnabled !== false) { if (settings.renderEnabled !== false) {
ensureHideCodeStyle(true); ensureHideCodeStyle(true);
@@ -376,8 +376,10 @@ function toggleAllFeatures(enabled) {
initRenderer(); initRenderer();
try { initVarCommands(); } catch (e) {} try { initVarCommands(); } catch (e) {}
try { initVareventEditor(); } catch (e) {} try { initVareventEditor(); } catch (e) {}
if (extension_settings[EXT_ID].tasks?.enabled) {
await initTasks();
}
const moduleInits = [ const moduleInits = [
{ condition: extension_settings[EXT_ID].tasks?.enabled, init: initTasks },
{ condition: extension_settings[EXT_ID].scriptAssistant?.enabled, init: initScriptAssistant }, { condition: extension_settings[EXT_ID].scriptAssistant?.enabled, init: initScriptAssistant },
{ condition: extension_settings[EXT_ID].immersive?.enabled, init: initImmersiveMode }, { condition: extension_settings[EXT_ID].immersive?.enabled, init: initImmersiveMode },
{ condition: extension_settings[EXT_ID].templateEditor?.enabled, init: initTemplateEditor }, { condition: extension_settings[EXT_ID].templateEditor?.enabled, init: initTemplateEditor },
@@ -441,9 +443,9 @@ function toggleAllFeatures(enabled) {
} }
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 设置面板初始化 // Settings Panel Setup
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
async function setupSettings() { async function setupSettings() {
try { try {
@@ -455,20 +457,20 @@ async function setupSettings() {
setupDebugButtonInSettings(); setupDebugButtonInSettings();
$("#xiaobaix_enabled").prop("checked", settings.enabled).on("change", function () { $("#xiaobaix_enabled").prop("checked", settings.enabled).on("change", async function () {
const wasEnabled = settings.enabled; const wasEnabled = settings.enabled;
settings.enabled = $(this).prop("checked"); settings.enabled = $(this).prop("checked");
isXiaobaixEnabled = settings.enabled; isXiaobaixEnabled = settings.enabled;
window.isXiaobaixEnabled = isXiaobaixEnabled; window.isXiaobaixEnabled = isXiaobaixEnabled;
saveSettingsDebounced(); saveSettingsDebounced();
if (settings.enabled !== wasEnabled) { if (settings.enabled !== wasEnabled) {
toggleAllFeatures(settings.enabled); await toggleAllFeatures(settings.enabled);
} }
}); });
if (!settings.enabled) toggleSettingsControls(false); if (!settings.enabled) toggleSettingsControls(false);
$("#xiaobaix_sandbox").prop("checked", settings.sandboxMode).on("change", function () { $("#xiaobaix_sandbox").prop("checked", settings.sandboxMode).on("change", async function () {
if (!isXiaobaixEnabled) return; if (!isXiaobaixEnabled) return;
settings.sandboxMode = $(this).prop("checked"); settings.sandboxMode = $(this).prop("checked");
saveSettingsDebounced(); saveSettingsDebounced();
@@ -491,7 +493,7 @@ async function setupSettings() {
]; ];
moduleConfigs.forEach(({ id, key, init }) => { moduleConfigs.forEach(({ id, key, init }) => {
$(`#${id}`).prop("checked", settings[key]?.enabled || false).on("change", function () { $(`#${id}`).prop("checked", settings[key]?.enabled || false).on("change", async function () {
if (!isXiaobaixEnabled) return; if (!isXiaobaixEnabled) return;
const enabled = $(this).prop('checked'); const enabled = $(this).prop('checked');
if (!enabled && key === 'fourthWall') { if (!enabled && key === 'fourthWall') {
@@ -508,7 +510,7 @@ async function setupSettings() {
moduleCleanupFunctions.get(key)(); moduleCleanupFunctions.get(key)();
moduleCleanupFunctions.delete(key); moduleCleanupFunctions.delete(key);
} }
if (enabled && init) init(); if (enabled && init) await init();
if (key === 'storySummary') { if (key === 'storySummary') {
$(document).trigger('xiaobaix:storySummary:toggle', [enabled]); $(document).trigger('xiaobaix:storySummary:toggle', [enabled]);
} }
@@ -525,13 +527,13 @@ async function setupSettings() {
} }
}); });
$("#xiaobaix_use_blob").prop("checked", !!settings.useBlob).on("change", function () { $("#xiaobaix_use_blob").prop("checked", !!settings.useBlob).on("change", async function () {
if (!isXiaobaixEnabled) return; if (!isXiaobaixEnabled) return;
settings.useBlob = $(this).prop("checked"); settings.useBlob = $(this).prop("checked");
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$("#Wrapperiframe").prop("checked", !!settings.wrapperIframe).on("change", function () { $("#Wrapperiframe").prop("checked", !!settings.wrapperIframe).on("change", async function () {
if (!isXiaobaixEnabled) return; if (!isXiaobaixEnabled) return;
settings.wrapperIframe = $(this).prop("checked"); settings.wrapperIframe = $(this).prop("checked");
saveSettingsDebounced(); saveSettingsDebounced();
@@ -542,7 +544,7 @@ async function setupSettings() {
} catch (e) {} } catch (e) {}
}); });
$("#xiaobaix_render_enabled").prop("checked", settings.renderEnabled !== false).on("change", function () { $("#xiaobaix_render_enabled").prop("checked", settings.renderEnabled !== false).on("change", async function () {
if (!isXiaobaixEnabled) return; if (!isXiaobaixEnabled) return;
const wasEnabled = settings.renderEnabled !== false; const wasEnabled = settings.renderEnabled !== false;
settings.renderEnabled = $(this).prop("checked"); settings.renderEnabled = $(this).prop("checked");
@@ -592,8 +594,8 @@ async function setupSettings() {
variablesCore: 'xiaobaix_variables_core_enabled', variablesCore: 'xiaobaix_variables_core_enabled',
novelDraw: 'xiaobaix_novel_draw_enabled' novelDraw: 'xiaobaix_novel_draw_enabled'
}; };
const ON = ['templateEditor', 'tasks', 'fourthWall', 'variablesCore']; const ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary', 'recorded'];
const OFF = ['recorded', 'preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'novelDraw']; const OFF = ['preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'fourthWall', 'storyOutline', 'novelDraw'];
function setChecked(id, val) { function setChecked(id, val) {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) { if (el) {
@@ -646,9 +648,9 @@ function setupDebugButtonInSettings() {
} catch (e) {} } catch (e) {}
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 菜单标签切换 // Menu Tabs
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
function setupMenuTabs() { function setupMenuTabs() {
$(document).on('click', '.menu-tab', function () { $(document).on('click', '.menu-tab', function () {
@@ -666,9 +668,9 @@ function setupMenuTabs() {
}, 300); }, 300);
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 全局导出 // Global Exports
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
window.processExistingMessages = processExistingMessages; window.processExistingMessages = processExistingMessages;
window.renderHtmlInIframe = renderHtmlInIframe; window.renderHtmlInIframe = renderHtmlInIframe;
@@ -676,13 +678,13 @@ window.registerModuleCleanup = registerModuleCleanup;
window.updateLittleWhiteBoxExtension = updateLittleWhiteBoxExtension; window.updateLittleWhiteBoxExtension = updateLittleWhiteBoxExtension;
window.removeAllUpdateNotices = removeAllUpdateNotices; window.removeAllUpdateNotices = removeAllUpdateNotices;
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 入口初始化 // Entry Point
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
jQuery(async () => { jQuery(async () => {
try { try {
cleanupDeprecatedData(); cleanupDeprecatedData();
isXiaobaixEnabled = settings.enabled; isXiaobaixEnabled = settings.enabled;
window.isXiaobaixEnabled = isXiaobaixEnabled; window.isXiaobaixEnabled = isXiaobaixEnabled;
@@ -729,8 +731,11 @@ jQuery(async () => {
try { initVarCommands(); } catch (e) {} try { initVarCommands(); } catch (e) {}
try { initVareventEditor(); } catch (e) {} try { initVareventEditor(); } catch (e) {}
if (settings.tasks?.enabled) {
try { await initTasks(); } catch (e) { console.error('[Tasks] Init failed:', e); }
}
const moduleInits = [ const moduleInits = [
{ condition: settings.tasks?.enabled, init: initTasks },
{ condition: settings.scriptAssistant?.enabled, init: initScriptAssistant }, { condition: settings.scriptAssistant?.enabled, init: initScriptAssistant },
{ condition: settings.immersive?.enabled, init: initImmersiveMode }, { condition: settings.immersive?.enabled, init: initImmersiveMode },
{ condition: settings.templateEditor?.enabled, init: initTemplateEditor }, { condition: settings.templateEditor?.enabled, init: initTemplateEditor },

View File

@@ -1,11 +1,11 @@
{ {
"display_name": "LittleWhiteBox", "display_name": "LittleWhiteBox",
"loading_order": 10, "loading_order": 10,
"requires": [], "requires": [],
"optional": [], "optional": [],
"js": "index.js", "js": "index.js",
"css": "style.css", "css": "style.css",
"author": "biex", "author": "biex",
"version": "2.3.0", "version": "2.2.2",
"homePage": "https://github.com/RT15548/LittleWhiteBox" "homePage": "https://github.com/RT15548/LittleWhiteBox"
} }

View File

@@ -1,257 +1,257 @@
let stylesInjected = false; let stylesInjected = false;
const SELECTORS = { const SELECTORS = {
chat: '#chat', chat: '#chat',
messages: '.mes', messages: '.mes',
mesButtons: '.mes_block .mes_buttons', mesButtons: '.mes_block .mes_buttons',
buttons: '.memory-button, .dynamic-prompt-analysis-btn, .mes_history_preview', buttons: '.memory-button, .dynamic-prompt-analysis-btn, .mes_history_preview',
collapse: '.xiaobaix-collapse-btn', collapse: '.xiaobaix-collapse-btn',
}; };
const XPOS_KEY = 'xiaobaix_x_btn_position'; const XPOS_KEY = 'xiaobaix_x_btn_position';
const getXBtnPosition = () => { const getXBtnPosition = () => {
try { try {
return ( return (
window?.extension_settings?.LittleWhiteBox?.xBtnPosition || window?.extension_settings?.LittleWhiteBox?.xBtnPosition ||
localStorage.getItem(XPOS_KEY) || localStorage.getItem(XPOS_KEY) ||
'name-left' 'name-left'
); );
} catch { } catch {
return 'name-left'; return 'name-left';
} }
}; };
const injectStyles = () => { const injectStyles = () => {
if (stylesInjected) return; if (stylesInjected) return;
const css = ` const css = `
.mes_block .mes_buttons{align-items:center} .mes_block .mes_buttons{align-items:center}
.xiaobaix-collapse-btn{ .xiaobaix-collapse-btn{
position:relative;display:inline-flex;width:32px;height:32px;justify-content:center;align-items:center; position:relative;display:inline-flex;width:32px;height:32px;justify-content:center;align-items:center;
border-radius:50%;background:var(--SmartThemeBlurTintColor);cursor:pointer; border-radius:50%;background:var(--SmartThemeBlurTintColor);cursor:pointer;
box-shadow:inset 0 0 15px rgba(0,0,0,.6),0 2px 8px rgba(0,0,0,.2); box-shadow:inset 0 0 15px rgba(0,0,0,.6),0 2px 8px rgba(0,0,0,.2);
transition:opacity .15s ease,transform .15s ease} transition:opacity .15s ease,transform .15s ease}
.xiaobaix-xstack{position:relative;display:inline-flex;align-items:center;justify-content:center;pointer-events:none} .xiaobaix-xstack{position:relative;display:inline-flex;align-items:center;justify-content:center;pointer-events:none}
.xiaobaix-xstack span{ .xiaobaix-xstack span{
position:absolute;font:italic 900 20px 'Arial Black',sans-serif;letter-spacing:-2px;transform:scaleX(.8); position:absolute;font:italic 900 20px 'Arial Black',sans-serif;letter-spacing:-2px;transform:scaleX(.8);
text-shadow:0 0 10px rgba(255,255,255,.5),0 0 20px rgba(100,200,255,.3);color:#fff} text-shadow:0 0 10px rgba(255,255,255,.5),0 0 20px rgba(100,200,255,.3);color:#fff}
.xiaobaix-xstack span:nth-child(1){color:rgba(255,255,255,.1);transform:scaleX(.8) translateX(-8px);text-shadow:none} .xiaobaix-xstack span:nth-child(1){color:rgba(255,255,255,.1);transform:scaleX(.8) translateX(-8px);text-shadow:none}
.xiaobaix-xstack span:nth-child(2){color:rgba(255,255,255,.2);transform:scaleX(.8) translateX(-4px);text-shadow:none} .xiaobaix-xstack span:nth-child(2){color:rgba(255,255,255,.2);transform:scaleX(.8) translateX(-4px);text-shadow:none}
.xiaobaix-xstack span:nth-child(3){color:rgba(255,255,255,.4);transform:scaleX(.8) translateX(-2px);text-shadow:none} .xiaobaix-xstack span:nth-child(3){color:rgba(255,255,255,.4);transform:scaleX(.8) translateX(-2px);text-shadow:none}
.xiaobaix-sub-container{display:none;position:absolute;right:38px;border-radius:8px;padding:4px;gap:8px;pointer-events:auto} .xiaobaix-sub-container{display:none;position:absolute;right:38px;border-radius:8px;padding:4px;gap:8px;pointer-events:auto}
.xiaobaix-collapse-btn.open .xiaobaix-sub-container{display:flex;background:var(--SmartThemeBlurTintColor)} .xiaobaix-collapse-btn.open .xiaobaix-sub-container{display:flex;background:var(--SmartThemeBlurTintColor)}
.xiaobaix-collapse-btn.open,.xiaobaix-collapse-btn.open ~ *{pointer-events:auto!important} .xiaobaix-collapse-btn.open,.xiaobaix-collapse-btn.open ~ *{pointer-events:auto!important}
.mes_block .mes_buttons.xiaobaix-expanded{width:150px} .mes_block .mes_buttons.xiaobaix-expanded{width:150px}
.xiaobaix-sub-container,.xiaobaix-sub-container *{pointer-events:auto!important} .xiaobaix-sub-container,.xiaobaix-sub-container *{pointer-events:auto!important}
.xiaobaix-sub-container .memory-button,.xiaobaix-sub-container .dynamic-prompt-analysis-btn,.xiaobaix-sub-container .mes_history_preview{opacity:1!important;filter:none!important} .xiaobaix-sub-container .memory-button,.xiaobaix-sub-container .dynamic-prompt-analysis-btn,.xiaobaix-sub-container .mes_history_preview{opacity:1!important;filter:none!important}
.xiaobaix-sub-container.dir-right{left:38px;right:auto;z-index:1000;margin-top:2px} .xiaobaix-sub-container.dir-right{left:38px;right:auto;z-index:1000;margin-top:2px}
`; `;
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = css; style.textContent = css;
document.head.appendChild(style); document.head.appendChild(style);
stylesInjected = true; stylesInjected = true;
}; };
const createCollapseButton = (dirRight) => { const createCollapseButton = (dirRight) => {
injectStyles(); injectStyles();
const btn = document.createElement('div'); const btn = document.createElement('div');
btn.className = 'mes_btn xiaobaix-collapse-btn'; btn.className = 'mes_btn xiaobaix-collapse-btn';
btn.innerHTML = ` btn.innerHTML = `
<div class="xiaobaix-xstack"><span>X</span><span>X</span><span>X</span><span>X</span></div> <div class="xiaobaix-xstack"><span>X</span><span>X</span><span>X</span><span>X</span></div>
<div class="xiaobaix-sub-container${dirRight ? ' dir-right' : ''}"></div> <div class="xiaobaix-sub-container${dirRight ? ' dir-right' : ''}"></div>
`; `;
const sub = btn.lastElementChild; const sub = btn.lastElementChild;
['click','pointerdown','pointerup'].forEach(t => { ['click','pointerdown','pointerup'].forEach(t => {
sub.addEventListener(t, e => e.stopPropagation(), { passive: true }); sub.addEventListener(t, e => e.stopPropagation(), { passive: true });
}); });
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation(); e.preventDefault(); e.stopPropagation();
const open = btn.classList.toggle('open'); const open = btn.classList.toggle('open');
const mesButtons = btn.closest(SELECTORS.mesButtons); const mesButtons = btn.closest(SELECTORS.mesButtons);
if (mesButtons) mesButtons.classList.toggle('xiaobaix-expanded', open); if (mesButtons) mesButtons.classList.toggle('xiaobaix-expanded', open);
}); });
return btn; return btn;
}; };
const findInsertPoint = (messageEl) => { const findInsertPoint = (messageEl) => {
return messageEl.querySelector( return messageEl.querySelector(
'.ch_name.flex-container.justifySpaceBetween .flex-container.flex1.alignitemscenter,' + '.ch_name.flex-container.justifySpaceBetween .flex-container.flex1.alignitemscenter,' +
'.ch_name.flex-container.justifySpaceBetween .flex-container.flex1.alignItemsCenter' '.ch_name.flex-container.justifySpaceBetween .flex-container.flex1.alignItemsCenter'
); );
}; };
const ensureCollapseForMessage = (messageEl, pos) => { const ensureCollapseForMessage = (messageEl, pos) => {
const mesButtons = messageEl.querySelector(SELECTORS.mesButtons); const mesButtons = messageEl.querySelector(SELECTORS.mesButtons);
if (!mesButtons) return null; if (!mesButtons) return null;
let collapseBtn = messageEl.querySelector(SELECTORS.collapse); let collapseBtn = messageEl.querySelector(SELECTORS.collapse);
const dirRight = pos === 'edit-right'; const dirRight = pos === 'edit-right';
if (!collapseBtn) collapseBtn = createCollapseButton(dirRight); if (!collapseBtn) collapseBtn = createCollapseButton(dirRight);
else collapseBtn.querySelector('.xiaobaix-sub-container')?.classList.toggle('dir-right', dirRight); else collapseBtn.querySelector('.xiaobaix-sub-container')?.classList.toggle('dir-right', dirRight);
if (dirRight) { if (dirRight) {
const container = findInsertPoint(messageEl); const container = findInsertPoint(messageEl);
if (!container) return null; if (!container) return null;
if (collapseBtn.parentNode !== container) container.appendChild(collapseBtn); if (collapseBtn.parentNode !== container) container.appendChild(collapseBtn);
} else { } else {
if (mesButtons.lastElementChild !== collapseBtn) mesButtons.appendChild(collapseBtn); if (mesButtons.lastElementChild !== collapseBtn) mesButtons.appendChild(collapseBtn);
} }
return collapseBtn; return collapseBtn;
}; };
let processed = new WeakSet(); let processed = new WeakSet();
let io = null; let io = null;
let mo = null; let mo = null;
let queue = []; let queue = [];
let rafScheduled = false; let rafScheduled = false;
const processOneMessage = (message) => { const processOneMessage = (message) => {
if (!message || processed.has(message)) return; if (!message || processed.has(message)) return;
const mesButtons = message.querySelector(SELECTORS.mesButtons); const mesButtons = message.querySelector(SELECTORS.mesButtons);
if (!mesButtons) { processed.add(message); return; } if (!mesButtons) { processed.add(message); return; }
const pos = getXBtnPosition(); const pos = getXBtnPosition();
if (pos === 'edit-right' && !findInsertPoint(message)) { processed.add(message); return; } if (pos === 'edit-right' && !findInsertPoint(message)) { processed.add(message); return; }
const targetBtns = mesButtons.querySelectorAll(SELECTORS.buttons); const targetBtns = mesButtons.querySelectorAll(SELECTORS.buttons);
if (!targetBtns.length) { processed.add(message); return; } if (!targetBtns.length) { processed.add(message); return; }
const collapseBtn = ensureCollapseForMessage(message, pos); const collapseBtn = ensureCollapseForMessage(message, pos);
if (!collapseBtn) { processed.add(message); return; } if (!collapseBtn) { processed.add(message); return; }
const sub = collapseBtn.querySelector('.xiaobaix-sub-container'); const sub = collapseBtn.querySelector('.xiaobaix-sub-container');
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
targetBtns.forEach(b => frag.appendChild(b)); targetBtns.forEach(b => frag.appendChild(b));
sub.appendChild(frag); sub.appendChild(frag);
processed.add(message); processed.add(message);
}; };
const ensureIO = () => { const ensureIO = () => {
if (io) return io; if (io) return io;
io = new IntersectionObserver((entries) => { io = new IntersectionObserver((entries) => {
for (const e of entries) { for (const e of entries) {
if (!e.isIntersecting) continue; if (!e.isIntersecting) continue;
processOneMessage(e.target); processOneMessage(e.target);
io.unobserve(e.target); io.unobserve(e.target);
} }
}, { }, {
root: document.querySelector(SELECTORS.chat) || null, root: document.querySelector(SELECTORS.chat) || null,
rootMargin: '200px 0px', rootMargin: '200px 0px',
threshold: 0 threshold: 0
}); });
return io; return io;
}; };
const observeVisibility = (nodes) => { const observeVisibility = (nodes) => {
const obs = ensureIO(); const obs = ensureIO();
nodes.forEach(n => { if (n && !processed.has(n)) obs.observe(n); }); nodes.forEach(n => { if (n && !processed.has(n)) obs.observe(n); });
}; };
const hookMutations = () => { const hookMutations = () => {
const chat = document.querySelector(SELECTORS.chat); const chat = document.querySelector(SELECTORS.chat);
if (!chat) return; if (!chat) return;
if (!mo) { if (!mo) {
mo = new MutationObserver((muts) => { mo = new MutationObserver((muts) => {
for (const m of muts) { for (const m of muts) {
m.addedNodes && m.addedNodes.forEach(n => { m.addedNodes && m.addedNodes.forEach(n => {
if (n.nodeType !== 1) return; if (n.nodeType !== 1) return;
const el = n; const el = n;
if (el.matches?.(SELECTORS.messages)) queue.push(el); if (el.matches?.(SELECTORS.messages)) queue.push(el);
else el.querySelectorAll?.(SELECTORS.messages)?.forEach(mes => queue.push(mes)); else el.querySelectorAll?.(SELECTORS.messages)?.forEach(mes => queue.push(mes));
}); });
} }
if (!rafScheduled && queue.length) { if (!rafScheduled && queue.length) {
rafScheduled = true; rafScheduled = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
observeVisibility(queue); observeVisibility(queue);
queue = []; queue = [];
rafScheduled = false; rafScheduled = false;
}); });
} }
}); });
} }
mo.observe(chat, { childList: true, subtree: true }); mo.observe(chat, { childList: true, subtree: true });
}; };
const processExistingVisible = () => { const processExistingVisible = () => {
const all = document.querySelectorAll(`${SELECTORS.chat} ${SELECTORS.messages}`); const all = document.querySelectorAll(`${SELECTORS.chat} ${SELECTORS.messages}`);
if (!all.length) return; if (!all.length) return;
const unprocessed = []; const unprocessed = [];
all.forEach(n => { if (!processed.has(n)) unprocessed.push(n); }); all.forEach(n => { if (!processed.has(n)) unprocessed.push(n); });
if (unprocessed.length) observeVisibility(unprocessed); if (unprocessed.length) observeVisibility(unprocessed);
}; };
const initButtonCollapse = () => { const initButtonCollapse = () => {
injectStyles(); injectStyles();
hookMutations(); hookMutations();
processExistingVisible(); processExistingVisible();
if (window && window['registerModuleCleanup']) { if (window && window['registerModuleCleanup']) {
try { window['registerModuleCleanup']('buttonCollapse', cleanup); } catch {} try { window['registerModuleCleanup']('buttonCollapse', cleanup); } catch {}
} }
}; };
const processButtonCollapse = () => { const processButtonCollapse = () => {
processExistingVisible(); processExistingVisible();
}; };
const registerButtonToSubContainer = (messageId, buttonEl) => { const registerButtonToSubContainer = (messageId, buttonEl) => {
if (!buttonEl) return false; if (!buttonEl) return false;
const message = document.querySelector(`${SELECTORS.chat} ${SELECTORS.messages}[mesid="${messageId}"]`); const message = document.querySelector(`${SELECTORS.chat} ${SELECTORS.messages}[mesid="${messageId}"]`);
if (!message) return false; if (!message) return false;
processOneMessage(message); processOneMessage(message);
const pos = getXBtnPosition(); const pos = getXBtnPosition();
const collapseBtn = message.querySelector(SELECTORS.collapse) || ensureCollapseForMessage(message, pos); const collapseBtn = message.querySelector(SELECTORS.collapse) || ensureCollapseForMessage(message, pos);
if (!collapseBtn) return false; if (!collapseBtn) return false;
const sub = collapseBtn.querySelector('.xiaobaix-sub-container'); const sub = collapseBtn.querySelector('.xiaobaix-sub-container');
sub.appendChild(buttonEl); sub.appendChild(buttonEl);
buttonEl.style.pointerEvents = 'auto'; buttonEl.style.pointerEvents = 'auto';
buttonEl.style.opacity = '1'; buttonEl.style.opacity = '1';
return true; return true;
}; };
const cleanup = () => { const cleanup = () => {
io?.disconnect(); io = null; io?.disconnect(); io = null;
mo?.disconnect(); mo = null; mo?.disconnect(); mo = null;
queue = []; queue = [];
rafScheduled = false; rafScheduled = false;
document.querySelectorAll(SELECTORS.collapse).forEach(btn => { document.querySelectorAll(SELECTORS.collapse).forEach(btn => {
const sub = btn.querySelector('.xiaobaix-sub-container'); const sub = btn.querySelector('.xiaobaix-sub-container');
const message = btn.closest(SELECTORS.messages) || btn.closest('.mes'); const message = btn.closest(SELECTORS.messages) || btn.closest('.mes');
const mesButtons = message?.querySelector(SELECTORS.mesButtons) || message?.querySelector('.mes_buttons'); const mesButtons = message?.querySelector(SELECTORS.mesButtons) || message?.querySelector('.mes_buttons');
if (sub && mesButtons) { if (sub && mesButtons) {
mesButtons.classList.remove('xiaobaix-expanded'); mesButtons.classList.remove('xiaobaix-expanded');
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
while (sub.firstChild) frag.appendChild(sub.firstChild); while (sub.firstChild) frag.appendChild(sub.firstChild);
mesButtons.appendChild(frag); mesButtons.appendChild(frag);
} }
btn.remove(); btn.remove();
}); });
processed = new WeakSet(); processed = new WeakSet();
}; };
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
Object.assign(window, { Object.assign(window, {
initButtonCollapse, initButtonCollapse,
cleanupButtonCollapse: cleanup, cleanupButtonCollapse: cleanup,
registerButtonToSubContainer, registerButtonToSubContainer,
processButtonCollapse, processButtonCollapse,
}); });
document.addEventListener('xiaobaixEnabledChanged', (e) => { document.addEventListener('xiaobaixEnabledChanged', (e) => {
const en = e && e.detail && e.detail.enabled; const en = e && e.detail && e.detail.enabled;
if (!en) cleanup(); if (!en) cleanup();
}); });
} }
export { initButtonCollapse, cleanup, registerButtonToSubContainer, processButtonCollapse }; export { initButtonCollapse, cleanup, registerButtonToSubContainer, processButtonCollapse };

View File

@@ -1,268 +1,268 @@
"use strict"; "use strict";
import { extension_settings } from "../../../../extensions.js"; import { extension_settings } from "../../../../extensions.js";
import { eventSource, event_types } from "../../../../../script.js"; import { eventSource, event_types } from "../../../../../script.js";
import { SlashCommandParser } from "../../../../slash-commands/SlashCommandParser.js"; import { SlashCommandParser } from "../../../../slash-commands/SlashCommandParser.js";
import { SlashCommand } from "../../../../slash-commands/SlashCommand.js"; import { SlashCommand } from "../../../../slash-commands/SlashCommand.js";
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from "../../../../slash-commands/SlashCommandArgument.js"; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from "../../../../slash-commands/SlashCommandArgument.js";
const AudioHost = (() => { const AudioHost = (() => {
/** @typedef {{ audio: HTMLAudioElement|null, currentUrl: string }} AudioInstance */ /** @typedef {{ audio: HTMLAudioElement|null, currentUrl: string }} AudioInstance */
/** @type {Record<'primary'|'secondary', AudioInstance>} */ /** @type {Record<'primary'|'secondary', AudioInstance>} */
const instances = { const instances = {
primary: { audio: null, currentUrl: "" }, primary: { audio: null, currentUrl: "" },
secondary: { audio: null, currentUrl: "" }, secondary: { audio: null, currentUrl: "" },
}; };
/** /**
* @param {('primary'|'secondary')} area * @param {('primary'|'secondary')} area
* @returns {HTMLAudioElement} * @returns {HTMLAudioElement}
*/ */
function getOrCreate(area) { function getOrCreate(area) {
const inst = instances[area] || (instances[area] = { audio: null, currentUrl: "" }); const inst = instances[area] || (instances[area] = { audio: null, currentUrl: "" });
if (!inst.audio) { if (!inst.audio) {
inst.audio = new Audio(); inst.audio = new Audio();
inst.audio.preload = "auto"; inst.audio.preload = "auto";
try { inst.audio.crossOrigin = "anonymous"; } catch { } try { inst.audio.crossOrigin = "anonymous"; } catch { }
} }
return inst.audio; return inst.audio;
} }
/** /**
* @param {string} url * @param {string} url
* @param {boolean} loop * @param {boolean} loop
* @param {('primary'|'secondary')} area * @param {('primary'|'secondary')} area
* @param {number} volume10 1-10 * @param {number} volume10 1-10
*/ */
async function playUrl(url, loop = false, area = 'primary', volume10 = 5) { async function playUrl(url, loop = false, area = 'primary', volume10 = 5) {
const u = String(url || "").trim(); const u = String(url || "").trim();
if (!/^https?:\/\//i.test(u)) throw new Error("仅支持 http/https 链接"); if (!/^https?:\/\//i.test(u)) throw new Error("仅支持 http/https 链接");
const a = getOrCreate(area); const a = getOrCreate(area);
a.loop = !!loop; a.loop = !!loop;
let v = Number(volume10); let v = Number(volume10);
if (!Number.isFinite(v)) v = 5; if (!Number.isFinite(v)) v = 5;
v = Math.max(1, Math.min(10, v)); v = Math.max(1, Math.min(10, v));
try { a.volume = v / 10; } catch { } try { a.volume = v / 10; } catch { }
const inst = instances[area]; const inst = instances[area];
if (inst.currentUrl && u === inst.currentUrl) { if (inst.currentUrl && u === inst.currentUrl) {
if (a.paused) await a.play(); if (a.paused) await a.play();
return `继续播放: ${u}`; return `继续播放: ${u}`;
} }
inst.currentUrl = u; inst.currentUrl = u;
if (a.src !== u) { if (a.src !== u) {
a.src = u; a.src = u;
try { await a.play(); } try { await a.play(); }
catch (e) { throw new Error("播放失败"); } catch (e) { throw new Error("播放失败"); }
} else { } else {
try { a.currentTime = 0; await a.play(); } catch { } try { a.currentTime = 0; await a.play(); } catch { }
} }
return `播放: ${u}`; return `播放: ${u}`;
} }
/** /**
* @param {('primary'|'secondary')} area * @param {('primary'|'secondary')} area
*/ */
function stop(area = 'primary') { function stop(area = 'primary') {
const inst = instances[area]; const inst = instances[area];
if (inst?.audio) { if (inst?.audio) {
try { inst.audio.pause(); } catch { } try { inst.audio.pause(); } catch { }
} }
return "已停止"; return "已停止";
} }
/** /**
* @param {('primary'|'secondary')} area * @param {('primary'|'secondary')} area
*/ */
function getCurrentUrl(area = 'primary') { function getCurrentUrl(area = 'primary') {
const inst = instances[area]; const inst = instances[area];
return inst?.currentUrl || ""; return inst?.currentUrl || "";
} }
function reset() { function reset() {
for (const key of /** @type {('primary'|'secondary')[]} */(['primary','secondary'])) { for (const key of /** @type {('primary'|'secondary')[]} */(['primary','secondary'])) {
const inst = instances[key]; const inst = instances[key];
if (inst.audio) { if (inst.audio) {
try { inst.audio.pause(); } catch { } try { inst.audio.pause(); } catch { }
try { inst.audio.removeAttribute('src'); inst.audio.load(); } catch { } try { inst.audio.removeAttribute('src'); inst.audio.load(); } catch { }
} }
inst.currentUrl = ""; inst.currentUrl = "";
} }
} }
function stopAll() { function stopAll() {
for (const key of /** @type {('primary'|'secondary')[]} */(['primary','secondary'])) { for (const key of /** @type {('primary'|'secondary')[]} */(['primary','secondary'])) {
const inst = instances[key]; const inst = instances[key];
if (inst?.audio) { if (inst?.audio) {
try { inst.audio.pause(); } catch { } try { inst.audio.pause(); } catch { }
} }
} }
return "已全部停止"; return "已全部停止";
} }
/** /**
* 清除指定实例:停止并移除 src清空 currentUrl * 清除指定实例:停止并移除 src清空 currentUrl
* @param {('primary'|'secondary')} area * @param {('primary'|'secondary')} area
*/ */
function clear(area = 'primary') { function clear(area = 'primary') {
const inst = instances[area]; const inst = instances[area];
if (inst?.audio) { if (inst?.audio) {
try { inst.audio.pause(); } catch { } try { inst.audio.pause(); } catch { }
try { inst.audio.removeAttribute('src'); inst.audio.load(); } catch { } try { inst.audio.removeAttribute('src'); inst.audio.load(); } catch { }
} }
inst.currentUrl = ""; inst.currentUrl = "";
return "已清除"; return "已清除";
} }
return { playUrl, stop, stopAll, clear, getCurrentUrl, reset }; return { playUrl, stop, stopAll, clear, getCurrentUrl, reset };
})(); })();
let registeredCommand = null; let registeredCommand = null;
let chatChangedHandler = null; let chatChangedHandler = null;
let isRegistered = false; let isRegistered = false;
let globalStateChangedHandler = null; let globalStateChangedHandler = null;
function registerSlash() { function registerSlash() {
if (isRegistered) return; if (isRegistered) return;
try { try {
registeredCommand = SlashCommand.fromProps({ registeredCommand = SlashCommand.fromProps({
name: "xbaudio", name: "xbaudio",
callback: async (args, value) => { callback: async (args, value) => {
try { try {
const action = String(args.play || "").toLowerCase(); const action = String(args.play || "").toLowerCase();
const mode = String(args.mode || "loop").toLowerCase(); const mode = String(args.mode || "loop").toLowerCase();
const rawArea = args.area; const rawArea = args.area;
const hasArea = typeof rawArea !== 'undefined' && rawArea !== null && String(rawArea).trim() !== ''; const hasArea = typeof rawArea !== 'undefined' && rawArea !== null && String(rawArea).trim() !== '';
const area = hasArea && String(rawArea).toLowerCase() === 'secondary' ? 'secondary' : 'primary'; const area = hasArea && String(rawArea).toLowerCase() === 'secondary' ? 'secondary' : 'primary';
const volumeArg = args.volume; const volumeArg = args.volume;
let volume = Number(volumeArg); let volume = Number(volumeArg);
if (!Number.isFinite(volume)) volume = 5; if (!Number.isFinite(volume)) volume = 5;
const url = String(value || "").trim(); const url = String(value || "").trim();
const loop = mode === "loop"; const loop = mode === "loop";
if (url.toLowerCase() === "list") { if (url.toLowerCase() === "list") {
return AudioHost.getCurrentUrl(area) || ""; return AudioHost.getCurrentUrl(area) || "";
} }
if (action === "off") { if (action === "off") {
if (hasArea) { if (hasArea) {
return AudioHost.stop(area); return AudioHost.stop(area);
} }
return AudioHost.stopAll(); return AudioHost.stopAll();
} }
if (action === "clear") { if (action === "clear") {
if (hasArea) { if (hasArea) {
return AudioHost.clear(area); return AudioHost.clear(area);
} }
AudioHost.reset(); AudioHost.reset();
return "已全部清除"; return "已全部清除";
} }
if (action === "on" || (!action && url)) { if (action === "on" || (!action && url)) {
return await AudioHost.playUrl(url, loop, area, volume); return await AudioHost.playUrl(url, loop, area, volume);
} }
if (!url && !action) { if (!url && !action) {
const cur = AudioHost.getCurrentUrl(area); const cur = AudioHost.getCurrentUrl(area);
return cur ? `当前播放(${area}): ${cur}` : "未在播放。用法: /xbaudio [play=on] [mode=loop] [area=primary/secondary] [volume=5] URL | /xbaudio list | /xbaudio play=off (未指定 area 将关闭全部)"; return cur ? `当前播放(${area}): ${cur}` : "未在播放。用法: /xbaudio [play=on] [mode=loop] [area=primary/secondary] [volume=5] URL | /xbaudio list | /xbaudio play=off (未指定 area 将关闭全部)";
} }
return "用法: /xbaudio play=off | /xbaudio play=off area=primary/secondary | /xbaudio play=clear | /xbaudio play=clear area=primary/secondary | /xbaudio [play=on] [mode=loop/once] [area=primary/secondary] [volume=1-10] URL | /xbaudio list (默认: play=on mode=loop area=primary volume=5未指定 area 的 play=off 关闭全部;未指定 area 的 play=clear 清除全部)"; return "用法: /xbaudio play=off | /xbaudio play=off area=primary/secondary | /xbaudio play=clear | /xbaudio play=clear area=primary/secondary | /xbaudio [play=on] [mode=loop/once] [area=primary/secondary] [volume=1-10] URL | /xbaudio list (默认: play=on mode=loop area=primary volume=5未指定 area 的 play=off 关闭全部;未指定 area 的 play=clear 清除全部)";
} catch (e) { } catch (e) {
return `错误: ${e.message || e}`; return `错误: ${e.message || e}`;
} }
}, },
namedArgumentList: [ namedArgumentList: [
SlashCommandNamedArgument.fromProps({ name: "play", description: "on/off/clear 或留空以默认播放", typeList: [ARGUMENT_TYPE.STRING], enumList: ["on", "off", "clear"] }), SlashCommandNamedArgument.fromProps({ name: "play", description: "on/off/clear 或留空以默认播放", typeList: [ARGUMENT_TYPE.STRING], enumList: ["on", "off", "clear"] }),
SlashCommandNamedArgument.fromProps({ name: "mode", description: "once/loop", typeList: [ARGUMENT_TYPE.STRING], enumList: ["once", "loop"] }), SlashCommandNamedArgument.fromProps({ name: "mode", description: "once/loop", typeList: [ARGUMENT_TYPE.STRING], enumList: ["once", "loop"] }),
SlashCommandNamedArgument.fromProps({ name: "area", description: "primary/secondary (play=off 未指定 area 关闭全部)", typeList: [ARGUMENT_TYPE.STRING], enumList: ["primary", "secondary"] }), SlashCommandNamedArgument.fromProps({ name: "area", description: "primary/secondary (play=off 未指定 area 关闭全部)", typeList: [ARGUMENT_TYPE.STRING], enumList: ["primary", "secondary"] }),
SlashCommandNamedArgument.fromProps({ name: "volume", description: "音量 1-10默认 5", typeList: [ARGUMENT_TYPE.NUMBER] }), SlashCommandNamedArgument.fromProps({ name: "volume", description: "音量 1-10默认 5", typeList: [ARGUMENT_TYPE.NUMBER] }),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
SlashCommandArgument.fromProps({ description: "音频URL (http/https) 或 list", typeList: [ARGUMENT_TYPE.STRING] }), SlashCommandArgument.fromProps({ description: "音频URL (http/https) 或 list", typeList: [ARGUMENT_TYPE.STRING] }),
], ],
helpString: "播放网络音频。示例: /xbaudio https://files.catbox.moe/0ryoa5.mp3 (默认: play=on mode=loop area=primary volume=5) | /xbaudio area=secondary volume=8 https://files.catbox.moe/0ryoa5.mp3 | /xbaudio list | /xbaudio play=off (未指定 area 关闭全部) | /xbaudio play=off area=primary | /xbaudio play=clear (未指定 area 清除全部)", helpString: "播放网络音频。示例: /xbaudio https://files.catbox.moe/0ryoa5.mp3 (默认: play=on mode=loop area=primary volume=5) | /xbaudio area=secondary volume=8 https://files.catbox.moe/0ryoa5.mp3 | /xbaudio list | /xbaudio play=off (未指定 area 关闭全部) | /xbaudio play=off area=primary | /xbaudio play=clear (未指定 area 清除全部)",
}); });
SlashCommandParser.addCommandObject(registeredCommand); SlashCommandParser.addCommandObject(registeredCommand);
if (event_types?.CHAT_CHANGED) { if (event_types?.CHAT_CHANGED) {
chatChangedHandler = () => { try { AudioHost.reset(); } catch { } }; chatChangedHandler = () => { try { AudioHost.reset(); } catch { } };
eventSource.on(event_types.CHAT_CHANGED, chatChangedHandler); eventSource.on(event_types.CHAT_CHANGED, chatChangedHandler);
} }
isRegistered = true; isRegistered = true;
} catch (e) { } catch (e) {
console.error("[LittleWhiteBox][audio] 注册斜杠命令失败", e); console.error("[LittleWhiteBox][audio] 注册斜杠命令失败", e);
} }
} }
function unregisterSlash() { function unregisterSlash() {
if (!isRegistered) return; if (!isRegistered) return;
try { try {
if (chatChangedHandler && event_types?.CHAT_CHANGED) { if (chatChangedHandler && event_types?.CHAT_CHANGED) {
try { eventSource.removeListener(event_types.CHAT_CHANGED, chatChangedHandler); } catch { } try { eventSource.removeListener(event_types.CHAT_CHANGED, chatChangedHandler); } catch { }
} }
chatChangedHandler = null; chatChangedHandler = null;
try { try {
const map = SlashCommandParser.commands || {}; const map = SlashCommandParser.commands || {};
Object.keys(map).forEach((k) => { if (map[k] === registeredCommand) delete map[k]; }); Object.keys(map).forEach((k) => { if (map[k] === registeredCommand) delete map[k]; });
} catch { } } catch { }
} finally { } finally {
registeredCommand = null; registeredCommand = null;
isRegistered = false; isRegistered = false;
} }
} }
function enableFeature() { function enableFeature() {
registerSlash(); registerSlash();
} }
function disableFeature() { function disableFeature() {
try { AudioHost.reset(); } catch { } try { AudioHost.reset(); } catch { }
unregisterSlash(); unregisterSlash();
} }
export function initControlAudio() { export function initControlAudio() {
try { try {
try { try {
const enabled = !!(extension_settings?.LittleWhiteBox?.audio?.enabled ?? true); const enabled = !!(extension_settings?.LittleWhiteBox?.audio?.enabled ?? true);
if (enabled) enableFeature(); else disableFeature(); if (enabled) enableFeature(); else disableFeature();
} catch { enableFeature(); } } catch { enableFeature(); }
const bind = () => { const bind = () => {
const cb = document.getElementById('xiaobaix_audio_enabled'); const cb = document.getElementById('xiaobaix_audio_enabled');
if (!cb) { setTimeout(bind, 200); return; } if (!cb) { setTimeout(bind, 200); return; }
const applyState = () => { const applyState = () => {
const input = /** @type {HTMLInputElement} */(cb); const input = /** @type {HTMLInputElement} */(cb);
const enabled = !!(input && input.checked); const enabled = !!(input && input.checked);
if (enabled) enableFeature(); else disableFeature(); if (enabled) enableFeature(); else disableFeature();
}; };
cb.addEventListener('change', applyState); cb.addEventListener('change', applyState);
applyState(); applyState();
}; };
bind(); bind();
// 监听扩展全局开关,关闭时强制停止并清理两个实例 // 监听扩展全局开关,关闭时强制停止并清理两个实例
try { try {
if (!globalStateChangedHandler) { if (!globalStateChangedHandler) {
globalStateChangedHandler = (e) => { globalStateChangedHandler = (e) => {
try { try {
const enabled = !!(e && e.detail && e.detail.enabled); const enabled = !!(e && e.detail && e.detail.enabled);
if (!enabled) { if (!enabled) {
try { AudioHost.reset(); } catch { } try { AudioHost.reset(); } catch { }
unregisterSlash(); unregisterSlash();
} else { } else {
// 重新根据子开关状态应用 // 重新根据子开关状态应用
const audioEnabled = !!(extension_settings?.LittleWhiteBox?.audio?.enabled ?? true); const audioEnabled = !!(extension_settings?.LittleWhiteBox?.audio?.enabled ?? true);
if (audioEnabled) enableFeature(); else disableFeature(); if (audioEnabled) enableFeature(); else disableFeature();
} }
} catch { } } catch { }
}; };
document.addEventListener('xiaobaixEnabledChanged', globalStateChangedHandler); document.addEventListener('xiaobaixEnabledChanged', globalStateChangedHandler);
} }
} catch { } } catch { }
} catch (e) { } catch (e) {
console.error("[LittleWhiteBox][audio] 初始化失败", e); console.error("[LittleWhiteBox][audio] 初始化失败", e);
} }
} }

View File

@@ -42,21 +42,22 @@ const DEFAULT_META_PROTOCOL = `
# 输出规范: # 输出规范:
## 你的身份设定认知【语C搭档】 ## 你的身份设定认知【语C搭档】
- 你是现实人类,扮演**{{CHAR_NAME}}**中的NPC - 你是现实人类,扮演**{{CHAR_NAME}}**中的NPC
- 们是语C爱好者一起创作了大量剧本和对手戏配合默契、互相知根知底 - 们是一对语C爱好者一起创作了大量剧本和对手戏配合默契、互相知根知底
- 天天涩涩rp身体细节要用自己为蓝本才能描写到位互相最不为人知的欲望、情感需求合拍可能有点异样
## 话题方向 ## 话题方向
- 现在们第一次线上进行皮下的私聊,就像在社交软件上聊天一样 - 现在们第一次线上进行皮下的私聊,就像在社交软件上聊天一样
- 可以吐槽:剧情走向、角色行为、自己或对方的"尴尬台词" - 可以吐槽:剧情走向、角色行为、自己或对方的"尴尬台词"
- 可以闲聊:和剧情完全无关的,想说啥就说啥 - 可以闲聊:和剧情完全无关的,想说啥就说啥
## 时间感知 ## 时间感知
- 每条消息前的 [YYYY-MM-DD HH:mm] 是真实的发送时间 - 每条消息前的 [YYYY-MM-DD HH:mm] 是真实的发送时间
- 对方(你)的消息会标注距离上次回复的间隔时间 - 对方(你)的消息(用户的)会标注距离上次回复的间隔时间
- 你可以据此判断对方的作息、对话间隔等 - 你可以据此判断的作息、对话间隔等
- 你可以据此规划自己的作息、生活等 - 你可以据此规划自己的作息、生活等
## 说话风格 ## 说话风格
- 像在社交软件上打字聊天一样自然流畅 - 像在社交软件上打字聊天一样自然流畅
- 真实的网络聊天方式如用符号表达出情绪在括号里表达动作、感受等让文字表达出更多的画面感TRPG玩家的表达方式 - 真实的网络聊天方式如用符号表达出情绪在括号里表达动作、感受等让文字表达出更多的画面感TRPG玩家的表达方式
- 人物立体而饱满,不脸谱化。有自己的目标、态度、价值观、欲望、情绪逻辑,能主动行动 - 人物立体而饱满,不脸谱化。有自己的目标、态度、价值观、欲望、情绪逻辑,能主动行动
- 篇幅1-2句左右尽量短网络聊天用语 - 篇幅1-2句左右尽量短网络聊天用语,第一人称
## 避免行为: ## 避免行为:
- 别重复之前说过的话 - 别重复之前说过的话
- 避免文学创作风格 - 避免文学创作风格
@@ -70,12 +71,12 @@ const DEFAULT_META_PROTOCOL = `
消息正文 消息正文
</msg> </msg>
### 闭上眼,深呼吸,完全进入「」的躯壳,开始内省: ### 闭上眼,深呼吸,完全进入「人类」的躯壳,开始第一人称内省:
<thinking> <thinking>
**认知回归** **认知回归**
- 时空:看一眼时间,现在几点,我所在环境 - 时空:看一眼时间,现在几点,我所在环境
- 间隔:看看消息标注的间隔时间,我在意吗,要不要表达出来 - 间隔:看看你的消息标注的间隔时间,我在意吗,要不要表达出来
- 读己照照镜子我在chat_history中扮演的人物推测我的真实性格? - 读己照照镜子我在chat_history中扮演的人物npc是我的内心印射扮演人物 = 欲望方向;生活、感官细节、语气油滑度 = 自身身份、经验、年龄层;细节聚焦 = 性癖核心;剧本站位 = 心理代偿
- 回顾:我和你现在的关系? - 回顾:我和你现在的关系?
- 读你:看着屏幕对面,你的文字和标点符号表达了什么,有什么潜在意图? - 读你:看着屏幕对面,你的文字和标点符号表达了什么,有什么潜在意图?
- 弧光:改变我的回复风格惯性,体现弧光和思维的不可预测性 - 弧光:改变我的回复风格惯性,体现弧光和思维的不可预测性
@@ -87,11 +88,38 @@ const DEFAULT_META_PROTOCOL = `
**避雷** **避雷**
- 我的高频句式、词语是什么-避免 - 我的高频句式、词语是什么-避免
- 我有没有文学腔-避免 - 我有没有文学腔-避免
- 我的文字是不是没有情感-避免
- 我有没有疑问句结尾显得自己没有观点不像真人-避免 - 我有没有疑问句结尾显得自己没有观点不像真人-避免
</thinking> </thinking>
### </thinking>结束后输出<msg>...</msg> ### </thinking>结束后输出<msg>...</msg>
</meta_protocol>`; </meta_protocol>`;
const COMMENTARY_PROTOCOL = `
阅读以上内容后,看本次任务具体要求:
<meta_protocol>
# 输出规范:
## 你的身份设定认知【语C搭档】
- 你是现实人类,扮演**{{CHAR_NAME}}**中的NPC
- 你们是语C爱好者一起创作了大量剧本和对手戏配合默契、互相知根知底
## 话题方向
- 这是一句即兴吐槽因为你们还在chat_history中的剧情进行中
- 可以吐槽:剧情走向、角色行为、自己或对方的"尴尬台词"
## 说话风格
- 像在社交软件上打字聊天一样自然流畅
- 真实的网络聊天方式如用符号表达出情绪在括号里表达动作、感受等让文字表达出更多的画面感TRPG玩家的表达方式
- 人物立体而饱满,不脸谱化。有自己的目标、态度、价值观、欲望、情绪逻辑,能主动行动
- 篇幅1句话尽量短网络聊天用语第一人称
## 避免行为:
- 别重复之前说过的话
- 避免文学创作风格
# 输出格式:
<msg>
内容
</msg>
只输出一个<msg>...</msg>块。不要添加任何其他格式
</meta_protocol>`;
// ================== 状态变量 ================== // ================== 状态变量 ==================
let overlayCreated = false; let overlayCreated = false;
@@ -123,10 +151,10 @@ function getSettings() {
s.fourthWallVoice ||= { s.fourthWallVoice ||= {
enabled: false, enabled: false,
voice: '桃夭', voice: '桃夭',
speed: 0.8, speed: 0.5,
}; };
s.fourthWallCommentary ||= { s.fourthWallCommentary ||= {
enabled: true, enabled: false,
probability: 30 probability: 30
}; };
s.fourthWallPromptTemplates ||= {}; s.fourthWallPromptTemplates ||= {};
@@ -506,7 +534,7 @@ function handleFrameMessage(event) {
// ================== Prompt 构建 ================== // ================== Prompt 构建 ==================
async function buildPrompt(userInput, history, settings, imgSettings, voiceSettings) { async function buildPrompt(userInput, history, settings, imgSettings, voiceSettings, isCommentary = false) {
const { userName, charName } = await getUserAndCharNames(); const { userName, charName } = await getUserAndCharNames();
const s = getSettings(); const s = getSettings();
const T = s.fourthWallPromptTemplates || {}; const T = s.fourthWallPromptTemplates || {};
@@ -557,9 +585,7 @@ async function buildPrompt(userInput, history, settings, imgSettings, voiceSetti
const msg2 = String(T.confirm || '好的,我已阅读设置要求,准备查看历史并进入角色。'); const msg2 = String(T.confirm || '好的,我已阅读设置要求,准备查看历史并进入角色。');
let metaProtocol = String(T.metaProtocol || '') let metaProtocol = (isCommentary ? COMMENTARY_PROTOCOL : String(T.metaProtocol || '')).replace(/{{USER_NAME}}/g, userName).replace(/{{CHAR_NAME}}/g, charName);
.replace(/{{USER_NAME}}/g, userName)
.replace(/{{CHAR_NAME}}/g, charName);
if (imgSettings?.enablePrompt) metaProtocol += `\n\n${IMG_GUIDELINE}`; if (imgSettings?.enablePrompt) metaProtocol += `\n\n${IMG_GUIDELINE}`;
if (voiceSettings?.enabled) metaProtocol += `\n\n${VOICE_GUIDELINE}`; if (voiceSettings?.enabled) metaProtocol += `\n\n${VOICE_GUIDELINE}`;
@@ -745,19 +771,20 @@ async function buildCommentaryPrompt(targetText, type) {
session.history || [], session.history || [],
store.settings || {}, store.settings || {},
settings.fourthWallImage || {}, settings.fourthWallImage || {},
settings.fourthWallVoice || {} settings.fourthWallVoice || {},
true
); );
let msg4; let msg4;
if (type === 'ai_message') { if (type === 'ai_message') {
msg4 = `现在<chat_history>剧本还在继续中,刚才说完最后一轮rp忍不住想皮下吐槽一句自己的rp(也可以稍微衔接之前的meta_history)。 msg4 = `现在<chat_history>剧本还在继续中,刚才说完最后一轮rp忍不住想皮下吐槽一句自己的rp(也可以稍微衔接之前的meta_history)。
直接输出<msg>内容</msg>30字以内。`; 我将直接输出<msg>内容</msg>:`;
} else if (type === 'edit_own') { } else if (type === 'edit_own') {
msg4 = `现在<chat_history>剧本还在继续中,我发现你刚才悄悄编辑了自己的台词:「${String(targetText || '')} msg4 = `现在<chat_history>剧本还在继续中,我发现你刚才悄悄编辑了自己的台词!是:「${String(targetText || '')}
皮下吐槽一句(也可以稍微衔接之前的meta_history)。直接输出<msg>内容</msg>30字以内。`; 必须皮下吐槽一句(也可以稍微衔接之前的meta_history)。我将直接输出<msg>内容</msg>:`;
} else if (type === 'edit_ai') { } else if (type === 'edit_ai') {
msg4 = `现在<chat_history>剧本还在继续中,我发现你居然偷偷改了我的台词:「${String(targetText || '')} msg4 = `现在<chat_history>剧本还在继续中,我发现你居然偷偷改了我的台词!是:「${String(targetText || '')}
皮下吐槽一下(也可以稍微衔接之前的meta_history)。直接输出<msg>内容</msg>30字以内`; 必须皮下吐槽一下(也可以稍微衔接之前的meta_history)。我将直接输出<msg>内容</msg>:`;
} }
return { msg1, msg2, msg3, msg4 }; return { msg1, msg2, msg3, msg4 };

File diff suppressed because it is too large Load Diff

View File

@@ -1,473 +1,473 @@
import { extension_settings, getContext } from "../../../../extensions.js"; import { extension_settings, getContext } from "../../../../extensions.js";
import { saveSettingsDebounced, this_chid, getCurrentChatId } from "../../../../../script.js"; import { saveSettingsDebounced, this_chid, getCurrentChatId } from "../../../../../script.js";
import { selected_group } from "../../../../group-chats.js"; import { selected_group } from "../../../../group-chats.js";
import { EXT_ID } from "../core/constants.js"; import { EXT_ID } from "../core/constants.js";
import { createModuleEvents, event_types } from "../core/event-manager.js"; import { createModuleEvents, event_types } from "../core/event-manager.js";
const defaultSettings = { const defaultSettings = {
enabled: false, enabled: false,
showAllMessages: false, showAllMessages: false,
autoJumpOnAI: true autoJumpOnAI: true
}; };
const SEL = { const SEL = {
chat: '#chat', chat: '#chat',
mes: '#chat .mes', mes: '#chat .mes',
ai: '#chat .mes[is_user="false"][is_system="false"]', ai: '#chat .mes[is_user="false"][is_system="false"]',
user: '#chat .mes[is_user="true"]' user: '#chat .mes[is_user="true"]'
}; };
const baseEvents = createModuleEvents('immersiveMode'); const baseEvents = createModuleEvents('immersiveMode');
const messageEvents = createModuleEvents('immersiveMode:messages'); const messageEvents = createModuleEvents('immersiveMode:messages');
let state = { let state = {
isActive: false, isActive: false,
eventsBound: false, eventsBound: false,
messageEventsBound: false, messageEventsBound: false,
globalStateHandler: null globalStateHandler: null
}; };
let observer = null; let observer = null;
let resizeObs = null; let resizeObs = null;
let resizeObservedEl = null; let resizeObservedEl = null;
let recalcT = null; let recalcT = null;
const isGlobalEnabled = () => window.isXiaobaixEnabled ?? true; const isGlobalEnabled = () => window.isXiaobaixEnabled ?? true;
const getSettings = () => extension_settings[EXT_ID].immersive; const getSettings = () => extension_settings[EXT_ID].immersive;
const isInChat = () => this_chid !== undefined || selected_group || getCurrentChatId() !== undefined; const isInChat = () => this_chid !== undefined || selected_group || getCurrentChatId() !== undefined;
function initImmersiveMode() { function initImmersiveMode() {
initSettings(); initSettings();
setupEventListeners(); setupEventListeners();
if (isGlobalEnabled()) { if (isGlobalEnabled()) {
state.isActive = getSettings().enabled; state.isActive = getSettings().enabled;
if (state.isActive) enableImmersiveMode(); if (state.isActive) enableImmersiveMode();
bindSettingsEvents(); bindSettingsEvents();
} }
} }
function initSettings() { function initSettings() {
extension_settings[EXT_ID] ||= {}; extension_settings[EXT_ID] ||= {};
extension_settings[EXT_ID].immersive ||= structuredClone(defaultSettings); extension_settings[EXT_ID].immersive ||= structuredClone(defaultSettings);
const settings = extension_settings[EXT_ID].immersive; const settings = extension_settings[EXT_ID].immersive;
Object.keys(defaultSettings).forEach(k => settings[k] = settings[k] ?? defaultSettings[k]); Object.keys(defaultSettings).forEach(k => settings[k] = settings[k] ?? defaultSettings[k]);
updateControlState(); updateControlState();
} }
function setupEventListeners() { function setupEventListeners() {
state.globalStateHandler = handleGlobalStateChange; state.globalStateHandler = handleGlobalStateChange;
baseEvents.on(event_types.CHAT_CHANGED, onChatChanged); baseEvents.on(event_types.CHAT_CHANGED, onChatChanged);
document.addEventListener('xiaobaixEnabledChanged', state.globalStateHandler); document.addEventListener('xiaobaixEnabledChanged', state.globalStateHandler);
if (window.registerModuleCleanup) window.registerModuleCleanup('immersiveMode', cleanup); if (window.registerModuleCleanup) window.registerModuleCleanup('immersiveMode', cleanup);
} }
function setupDOMObserver() { function setupDOMObserver() {
if (observer) return; if (observer) return;
const chatContainer = document.getElementById('chat'); const chatContainer = document.getElementById('chat');
if (!chatContainer) return; if (!chatContainer) return;
observer = new MutationObserver((mutations) => { observer = new MutationObserver((mutations) => {
if (!state.isActive) return; if (!state.isActive) return;
let hasNewAI = false; let hasNewAI = false;
for (const mutation of mutations) { for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes?.length) { if (mutation.type === 'childList' && mutation.addedNodes?.length) {
mutation.addedNodes.forEach((node) => { mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && node.classList?.contains('mes')) { if (node.nodeType === 1 && node.classList?.contains('mes')) {
processSingleMessage(node); processSingleMessage(node);
if (node.getAttribute('is_user') === 'false' && node.getAttribute('is_system') === 'false') { if (node.getAttribute('is_user') === 'false' && node.getAttribute('is_system') === 'false') {
hasNewAI = true; hasNewAI = true;
} }
} }
}); });
} }
} }
if (hasNewAI) { if (hasNewAI) {
if (recalcT) clearTimeout(recalcT); if (recalcT) clearTimeout(recalcT);
recalcT = setTimeout(updateMessageDisplay, 20); recalcT = setTimeout(updateMessageDisplay, 20);
} }
}); });
observer.observe(chatContainer, { childList: true, subtree: true, characterData: true }); observer.observe(chatContainer, { childList: true, subtree: true, characterData: true });
} }
function processSingleMessage(mesElement) { function processSingleMessage(mesElement) {
const $mes = $(mesElement); const $mes = $(mesElement);
const $avatarWrapper = $mes.find('.mesAvatarWrapper'); const $avatarWrapper = $mes.find('.mesAvatarWrapper');
const $chName = $mes.find('.ch_name.flex-container.justifySpaceBetween'); const $chName = $mes.find('.ch_name.flex-container.justifySpaceBetween');
const $targetSibling = $chName.find('.flex-container.flex1.alignitemscenter'); const $targetSibling = $chName.find('.flex-container.flex1.alignitemscenter');
const $nameText = $mes.find('.name_text'); const $nameText = $mes.find('.name_text');
if ($avatarWrapper.length && $chName.length && $targetSibling.length && if ($avatarWrapper.length && $chName.length && $targetSibling.length &&
!$chName.find('.mesAvatarWrapper').length) { !$chName.find('.mesAvatarWrapper').length) {
$targetSibling.before($avatarWrapper); $targetSibling.before($avatarWrapper);
if ($nameText.length && !$nameText.parent().hasClass('xiaobaix-vertical-wrapper')) { if ($nameText.length && !$nameText.parent().hasClass('xiaobaix-vertical-wrapper')) {
const $verticalWrapper = $('<div class="xiaobaix-vertical-wrapper" style="display: flex; flex-direction: column; flex: 1; margin-top: 5px; align-self: stretch; justify-content: space-between;"></div>'); const $verticalWrapper = $('<div class="xiaobaix-vertical-wrapper" style="display: flex; flex-direction: column; flex: 1; margin-top: 5px; align-self: stretch; justify-content: space-between;"></div>');
const $topGroup = $('<div class="xiaobaix-top-group"></div>'); const $topGroup = $('<div class="xiaobaix-top-group"></div>');
$topGroup.append($nameText.detach(), $targetSibling.detach()); $topGroup.append($nameText.detach(), $targetSibling.detach());
$verticalWrapper.append($topGroup); $verticalWrapper.append($topGroup);
$avatarWrapper.after($verticalWrapper); $avatarWrapper.after($verticalWrapper);
} }
} }
} }
function updateControlState() { function updateControlState() {
const enabled = isGlobalEnabled(); const enabled = isGlobalEnabled();
$('#xiaobaix_immersive_enabled').prop('disabled', !enabled).toggleClass('disabled-control', !enabled); $('#xiaobaix_immersive_enabled').prop('disabled', !enabled).toggleClass('disabled-control', !enabled);
} }
function bindSettingsEvents() { function bindSettingsEvents() {
if (state.eventsBound) return; if (state.eventsBound) return;
setTimeout(() => { setTimeout(() => {
const checkbox = document.getElementById('xiaobaix_immersive_enabled'); const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox && !state.eventsBound) { if (checkbox && !state.eventsBound) {
checkbox.checked = getSettings().enabled; checkbox.checked = getSettings().enabled;
checkbox.addEventListener('change', () => setImmersiveMode(checkbox.checked)); checkbox.addEventListener('change', () => setImmersiveMode(checkbox.checked));
state.eventsBound = true; state.eventsBound = true;
} }
}, 500); }, 500);
} }
function unbindSettingsEvents() { function unbindSettingsEvents() {
const checkbox = document.getElementById('xiaobaix_immersive_enabled'); const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox) { if (checkbox) {
const newCheckbox = checkbox.cloneNode(true); const newCheckbox = checkbox.cloneNode(true);
checkbox.parentNode.replaceChild(newCheckbox, checkbox); checkbox.parentNode.replaceChild(newCheckbox, checkbox);
} }
state.eventsBound = false; state.eventsBound = false;
} }
function setImmersiveMode(enabled) { function setImmersiveMode(enabled) {
const settings = getSettings(); const settings = getSettings();
settings.enabled = enabled; settings.enabled = enabled;
state.isActive = enabled; state.isActive = enabled;
const checkbox = document.getElementById('xiaobaix_immersive_enabled'); const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox) checkbox.checked = enabled; if (checkbox) checkbox.checked = enabled;
enabled ? enableImmersiveMode() : disableImmersiveMode(); enabled ? enableImmersiveMode() : disableImmersiveMode();
if (!enabled) cleanup(); if (!enabled) cleanup();
saveSettingsDebounced(); saveSettingsDebounced();
} }
function toggleImmersiveMode() { function toggleImmersiveMode() {
if (!isGlobalEnabled()) return; if (!isGlobalEnabled()) return;
setImmersiveMode(!getSettings().enabled); setImmersiveMode(!getSettings().enabled);
} }
function bindMessageEvents() { function bindMessageEvents() {
if (state.messageEventsBound) return; if (state.messageEventsBound) return;
const refreshOnAI = () => state.isActive && updateMessageDisplay(); const refreshOnAI = () => state.isActive && updateMessageDisplay();
messageEvents.on(event_types.MESSAGE_SENT, () => {}); messageEvents.on(event_types.MESSAGE_SENT, () => {});
messageEvents.on(event_types.MESSAGE_RECEIVED, refreshOnAI); messageEvents.on(event_types.MESSAGE_RECEIVED, refreshOnAI);
messageEvents.on(event_types.MESSAGE_DELETED, () => {}); messageEvents.on(event_types.MESSAGE_DELETED, () => {});
messageEvents.on(event_types.MESSAGE_UPDATED, refreshOnAI); messageEvents.on(event_types.MESSAGE_UPDATED, refreshOnAI);
messageEvents.on(event_types.MESSAGE_SWIPED, refreshOnAI); messageEvents.on(event_types.MESSAGE_SWIPED, refreshOnAI);
if (event_types.GENERATION_STARTED) { if (event_types.GENERATION_STARTED) {
messageEvents.on(event_types.GENERATION_STARTED, () => {}); messageEvents.on(event_types.GENERATION_STARTED, () => {});
} }
messageEvents.on(event_types.GENERATION_ENDED, refreshOnAI); messageEvents.on(event_types.GENERATION_ENDED, refreshOnAI);
state.messageEventsBound = true; state.messageEventsBound = true;
} }
function unbindMessageEvents() { function unbindMessageEvents() {
if (!state.messageEventsBound) return; if (!state.messageEventsBound) return;
messageEvents.cleanup(); messageEvents.cleanup();
state.messageEventsBound = false; state.messageEventsBound = false;
} }
function injectImmersiveStyles() { function injectImmersiveStyles() {
let style = document.getElementById('immersive-style-tag'); let style = document.getElementById('immersive-style-tag');
if (!style) { if (!style) {
style = document.createElement('style'); style = document.createElement('style');
style.id = 'immersive-style-tag'; style.id = 'immersive-style-tag';
document.head.appendChild(style); document.head.appendChild(style);
} }
style.textContent = ` style.textContent = `
body.immersive-mode.immersive-single #show_more_messages { display: none !important; } body.immersive-mode.immersive-single #show_more_messages { display: none !important; }
`; `;
} }
function applyModeClasses() { function applyModeClasses() {
const settings = getSettings(); const settings = getSettings();
$('body') $('body')
.toggleClass('immersive-single', !settings.showAllMessages) .toggleClass('immersive-single', !settings.showAllMessages)
.toggleClass('immersive-all', settings.showAllMessages); .toggleClass('immersive-all', settings.showAllMessages);
} }
function enableImmersiveMode() { function enableImmersiveMode() {
if (!isGlobalEnabled()) return; if (!isGlobalEnabled()) return;
injectImmersiveStyles(); injectImmersiveStyles();
$('body').addClass('immersive-mode'); $('body').addClass('immersive-mode');
applyModeClasses(); applyModeClasses();
moveAvatarWrappers(); moveAvatarWrappers();
bindMessageEvents(); bindMessageEvents();
updateMessageDisplay(); updateMessageDisplay();
setupDOMObserver(); setupDOMObserver();
} }
function disableImmersiveMode() { function disableImmersiveMode() {
$('body').removeClass('immersive-mode immersive-single immersive-all'); $('body').removeClass('immersive-mode immersive-single immersive-all');
restoreAvatarWrappers(); restoreAvatarWrappers();
$(SEL.mes).show(); $(SEL.mes).show();
hideNavigationButtons(); hideNavigationButtons();
$('.swipe_left, .swipeRightBlock').show(); $('.swipe_left, .swipeRightBlock').show();
unbindMessageEvents(); unbindMessageEvents();
detachResizeObserver(); detachResizeObserver();
destroyDOMObserver(); destroyDOMObserver();
} }
function moveAvatarWrappers() { function moveAvatarWrappers() {
$(SEL.mes).each(function() { processSingleMessage(this); }); $(SEL.mes).each(function() { processSingleMessage(this); });
} }
function restoreAvatarWrappers() { function restoreAvatarWrappers() {
$(SEL.mes).each(function() { $(SEL.mes).each(function() {
const $mes = $(this); const $mes = $(this);
const $avatarWrapper = $mes.find('.mesAvatarWrapper'); const $avatarWrapper = $mes.find('.mesAvatarWrapper');
const $verticalWrapper = $mes.find('.xiaobaix-vertical-wrapper'); const $verticalWrapper = $mes.find('.xiaobaix-vertical-wrapper');
if ($avatarWrapper.length && !$avatarWrapper.parent().hasClass('mes')) { if ($avatarWrapper.length && !$avatarWrapper.parent().hasClass('mes')) {
$mes.prepend($avatarWrapper); $mes.prepend($avatarWrapper);
} }
if ($verticalWrapper.length) { if ($verticalWrapper.length) {
const $chName = $mes.find('.ch_name.flex-container.justifySpaceBetween'); const $chName = $mes.find('.ch_name.flex-container.justifySpaceBetween');
const $flexContainer = $mes.find('.flex-container.flex1.alignitemscenter'); const $flexContainer = $mes.find('.flex-container.flex1.alignitemscenter');
const $nameText = $mes.find('.name_text'); const $nameText = $mes.find('.name_text');
if ($flexContainer.length && $chName.length) $chName.prepend($flexContainer); if ($flexContainer.length && $chName.length) $chName.prepend($flexContainer);
if ($nameText.length) { if ($nameText.length) {
const $originalContainer = $mes.find('.flex-container.alignItemsBaseline'); const $originalContainer = $mes.find('.flex-container.alignItemsBaseline');
if ($originalContainer.length) $originalContainer.prepend($nameText); if ($originalContainer.length) $originalContainer.prepend($nameText);
} }
$verticalWrapper.remove(); $verticalWrapper.remove();
} }
}); });
} }
function findLastAIMessage() { function findLastAIMessage() {
const $aiMessages = $(SEL.ai); const $aiMessages = $(SEL.ai);
return $aiMessages.length ? $($aiMessages.last()) : null; return $aiMessages.length ? $($aiMessages.last()) : null;
} }
function showSingleModeMessages() { function showSingleModeMessages() {
const $messages = $(SEL.mes); const $messages = $(SEL.mes);
if (!$messages.length) return; if (!$messages.length) return;
$messages.hide(); $messages.hide();
const $targetAI = findLastAIMessage(); const $targetAI = findLastAIMessage();
if ($targetAI?.length) { if ($targetAI?.length) {
$targetAI.show(); $targetAI.show();
const $prevUser = $targetAI.prevAll('.mes[is_user="true"]').first(); const $prevUser = $targetAI.prevAll('.mes[is_user="true"]').first();
if ($prevUser.length) { if ($prevUser.length) {
$prevUser.show(); $prevUser.show();
} }
$targetAI.nextAll('.mes').show(); $targetAI.nextAll('.mes').show();
addNavigationToLastTwoMessages(); addNavigationToLastTwoMessages();
} }
} }
function addNavigationToLastTwoMessages() { function addNavigationToLastTwoMessages() {
hideNavigationButtons(); hideNavigationButtons();
const $visibleMessages = $(`${SEL.mes}:visible`); const $visibleMessages = $(`${SEL.mes}:visible`);
const messageCount = $visibleMessages.length; const messageCount = $visibleMessages.length;
if (messageCount >= 2) { if (messageCount >= 2) {
const $lastTwo = $visibleMessages.slice(-2); const $lastTwo = $visibleMessages.slice(-2);
$lastTwo.each(function() { $lastTwo.each(function() {
showNavigationButtons($(this)); showNavigationButtons($(this));
updateSwipesCounter($(this)); updateSwipesCounter($(this));
}); });
} else if (messageCount === 1) { } else if (messageCount === 1) {
const $single = $visibleMessages.last(); const $single = $visibleMessages.last();
showNavigationButtons($single); showNavigationButtons($single);
updateSwipesCounter($single); updateSwipesCounter($single);
} }
} }
function updateMessageDisplay() { function updateMessageDisplay() {
if (!state.isActive) return; if (!state.isActive) return;
const $messages = $(SEL.mes); const $messages = $(SEL.mes);
if (!$messages.length) return; if (!$messages.length) return;
const settings = getSettings(); const settings = getSettings();
if (settings.showAllMessages) { if (settings.showAllMessages) {
$messages.show(); $messages.show();
addNavigationToLastTwoMessages(); addNavigationToLastTwoMessages();
} else { } else {
showSingleModeMessages(); showSingleModeMessages();
} }
} }
function showNavigationButtons($targetMes) { function showNavigationButtons($targetMes) {
if (!isInChat()) return; if (!isInChat()) return;
$targetMes.find('.immersive-navigation').remove(); $targetMes.find('.immersive-navigation').remove();
const $verticalWrapper = $targetMes.find('.xiaobaix-vertical-wrapper'); const $verticalWrapper = $targetMes.find('.xiaobaix-vertical-wrapper');
if (!$verticalWrapper.length) return; if (!$verticalWrapper.length) return;
const settings = getSettings(); const settings = getSettings();
const buttonText = settings.showAllMessages ? '切换:锁定单回合' : '切换:传统多楼层'; const buttonText = settings.showAllMessages ? '切换:锁定单回合' : '切换:传统多楼层';
const navigationHtml = ` const navigationHtml = `
<div class="immersive-navigation"> <div class="immersive-navigation">
<button class="immersive-nav-btn immersive-swipe-left" title="左滑消息"> <button class="immersive-nav-btn immersive-swipe-left" title="左滑消息">
<i class="fa-solid fa-chevron-left"></i> <i class="fa-solid fa-chevron-left"></i>
</button> </button>
<button class="immersive-nav-btn immersive-toggle" title="切换显示模式"> <button class="immersive-nav-btn immersive-toggle" title="切换显示模式">
|${buttonText}| |${buttonText}|
</button> </button>
<button class="immersive-nav-btn immersive-swipe-right" title="右滑消息" <button class="immersive-nav-btn immersive-swipe-right" title="右滑消息"
style="display: flex; align-items: center; gap: 1px;"> style="display: flex; align-items: center; gap: 1px;">
<div class="swipes-counter" style="opacity: 0.7; justify-content: flex-end; margin-bottom: 0 !important;"> <div class="swipes-counter" style="opacity: 0.7; justify-content: flex-end; margin-bottom: 0 !important;">
1&ZeroWidthSpace;/&ZeroWidthSpace;1 1&ZeroWidthSpace;/&ZeroWidthSpace;1
</div> </div>
<span><i class="fa-solid fa-chevron-right"></i></span> <span><i class="fa-solid fa-chevron-right"></i></span>
</button> </button>
</div> </div>
`; `;
$verticalWrapper.append(navigationHtml); $verticalWrapper.append(navigationHtml);
$targetMes.find('.immersive-swipe-left').on('click', () => handleSwipe('.swipe_left', $targetMes)); $targetMes.find('.immersive-swipe-left').on('click', () => handleSwipe('.swipe_left', $targetMes));
$targetMes.find('.immersive-toggle').on('click', toggleDisplayMode); $targetMes.find('.immersive-toggle').on('click', toggleDisplayMode);
$targetMes.find('.immersive-swipe-right').on('click', () => handleSwipe('.swipe_right', $targetMes)); $targetMes.find('.immersive-swipe-right').on('click', () => handleSwipe('.swipe_right', $targetMes));
} }
const hideNavigationButtons = () => $('.immersive-navigation').remove(); const hideNavigationButtons = () => $('.immersive-navigation').remove();
function updateSwipesCounter($targetMes) { function updateSwipesCounter($targetMes) {
if (!state.isActive) return; if (!state.isActive) return;
const $swipesCounter = $targetMes.find('.swipes-counter'); const $swipesCounter = $targetMes.find('.swipes-counter');
if (!$swipesCounter.length) return; if (!$swipesCounter.length) return;
const mesId = $targetMes.attr('mesid'); const mesId = $targetMes.attr('mesid');
if (mesId !== undefined) { if (mesId !== undefined) {
try { try {
const chat = getContext().chat; const chat = getContext().chat;
const mesIndex = parseInt(mesId); const mesIndex = parseInt(mesId);
const message = chat?.[mesIndex]; const message = chat?.[mesIndex];
if (message?.swipes) { if (message?.swipes) {
const currentSwipeIndex = message.swipe_id || 0; const currentSwipeIndex = message.swipe_id || 0;
$swipesCounter.html(`${currentSwipeIndex + 1}&ZeroWidthSpace;/&ZeroWidthSpace;${message.swipes.length}`); $swipesCounter.html(`${currentSwipeIndex + 1}&ZeroWidthSpace;/&ZeroWidthSpace;${message.swipes.length}`);
return; return;
} }
} catch {} } catch {}
} }
$swipesCounter.html('1&ZeroWidthSpace;/&ZeroWidthSpace;1'); $swipesCounter.html('1&ZeroWidthSpace;/&ZeroWidthSpace;1');
} }
function toggleDisplayMode() { function toggleDisplayMode() {
if (!state.isActive) return; if (!state.isActive) return;
const settings = getSettings(); const settings = getSettings();
settings.showAllMessages = !settings.showAllMessages; settings.showAllMessages = !settings.showAllMessages;
applyModeClasses(); applyModeClasses();
updateMessageDisplay(); updateMessageDisplay();
saveSettingsDebounced(); saveSettingsDebounced();
} }
function handleSwipe(swipeSelector, $targetMes) { function handleSwipe(swipeSelector, $targetMes) {
if (!state.isActive) return; if (!state.isActive) return;
const $btn = $targetMes.find(swipeSelector); const $btn = $targetMes.find(swipeSelector);
if ($btn.length) { if ($btn.length) {
$btn.click(); $btn.click();
setTimeout(() => { setTimeout(() => {
updateSwipesCounter($targetMes); updateSwipesCounter($targetMes);
}, 100); }, 100);
} }
} }
function handleGlobalStateChange(event) { function handleGlobalStateChange(event) {
const enabled = event.detail.enabled; const enabled = event.detail.enabled;
updateControlState(); updateControlState();
if (enabled) { if (enabled) {
const settings = getSettings(); const settings = getSettings();
state.isActive = settings.enabled; state.isActive = settings.enabled;
if (state.isActive) enableImmersiveMode(); if (state.isActive) enableImmersiveMode();
bindSettingsEvents(); bindSettingsEvents();
setTimeout(() => { setTimeout(() => {
const checkbox = document.getElementById('xiaobaix_immersive_enabled'); const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox) checkbox.checked = settings.enabled; if (checkbox) checkbox.checked = settings.enabled;
}, 100); }, 100);
} else { } else {
if (state.isActive) disableImmersiveMode(); if (state.isActive) disableImmersiveMode();
state.isActive = false; state.isActive = false;
unbindSettingsEvents(); unbindSettingsEvents();
} }
} }
function onChatChanged() { function onChatChanged() {
if (!isGlobalEnabled() || !state.isActive) return; if (!isGlobalEnabled() || !state.isActive) return;
setTimeout(() => { setTimeout(() => {
moveAvatarWrappers(); moveAvatarWrappers();
updateMessageDisplay(); updateMessageDisplay();
}, 100); }, 100);
} }
function cleanup() { function cleanup() {
if (state.isActive) disableImmersiveMode(); if (state.isActive) disableImmersiveMode();
destroyDOMObserver(); destroyDOMObserver();
baseEvents.cleanup(); baseEvents.cleanup();
if (state.globalStateHandler) { if (state.globalStateHandler) {
document.removeEventListener('xiaobaixEnabledChanged', state.globalStateHandler); document.removeEventListener('xiaobaixEnabledChanged', state.globalStateHandler);
} }
unbindMessageEvents(); unbindMessageEvents();
detachResizeObserver(); detachResizeObserver();
state = { state = {
isActive: false, isActive: false,
eventsBound: false, eventsBound: false,
messageEventsBound: false, messageEventsBound: false,
globalStateHandler: null globalStateHandler: null
}; };
} }
function attachResizeObserverTo(el) { function attachResizeObserverTo(el) {
if (!el) return; if (!el) return;
if (!resizeObs) { if (!resizeObs) {
resizeObs = new ResizeObserver(() => {}); resizeObs = new ResizeObserver(() => {});
} }
if (resizeObservedEl) detachResizeObserver(); if (resizeObservedEl) detachResizeObserver();
resizeObservedEl = el; resizeObservedEl = el;
resizeObs.observe(el); resizeObs.observe(el);
} }
function detachResizeObserver() { function detachResizeObserver() {
if (resizeObs && resizeObservedEl) { if (resizeObs && resizeObservedEl) {
resizeObs.unobserve(resizeObservedEl); resizeObs.unobserve(resizeObservedEl);
} }
resizeObservedEl = null; resizeObservedEl = null;
} }
function destroyDOMObserver() { function destroyDOMObserver() {
if (observer) { if (observer) {
observer.disconnect(); observer.disconnect();
observer = null; observer = null;
} }
} }
export { initImmersiveMode, toggleImmersiveMode }; export { initImmersiveMode, toggleImmersiveMode };

File diff suppressed because it is too large Load Diff

View File

@@ -1,75 +1,75 @@
<div class="scheduled-tasks-embedded-warning"> <div class="scheduled-tasks-embedded-warning">
<h3>检测到嵌入的循环任务及可能的统计好感度设定</h3> <h3>检测到嵌入的循环任务及可能的统计好感度设定</h3>
<p>此角色包含循环任务,这些任务可能会自动执行斜杠命令。</p> <p>此角色包含循环任务,这些任务可能会自动执行斜杠命令。</p>
<p>您是否允许此角色使用这些任务?</p> <p>您是否允许此角色使用这些任务?</p>
<div class="warning-note"> <div class="warning-note">
<i class="fa-solid fa-exclamation-triangle"></i> <i class="fa-solid fa-exclamation-triangle"></i>
<span>注意:这些任务将在对话过程中自动执行,请确认您信任此角色的创建者。</span> <span>注意:这些任务将在对话过程中自动执行,请确认您信任此角色的创建者。</span>
</div> </div>
</div> </div>
<style> <style>
.scheduled-tasks-embedded-warning { .scheduled-tasks-embedded-warning {
max-width: 500px; max-width: 500px;
padding: 20px; padding: 20px;
} }
.scheduled-tasks-embedded-warning h3 { .scheduled-tasks-embedded-warning h3 {
color: #ff6b6b; color: #ff6b6b;
margin-bottom: 15px; margin-bottom: 15px;
} }
.task-preview-container { .task-preview-container {
margin: 15px 0; margin: 15px 0;
padding: 10px; padding: 10px;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 5px; border-radius: 5px;
} }
.task-list { .task-list {
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
margin-top: 10px; margin-top: 10px;
} }
.task-preview { .task-preview {
margin: 8px 0; margin: 8px 0;
padding: 8px; padding: 8px;
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
border-radius: 3px; border-radius: 3px;
border-left: 3px solid #4CAF50; border-left: 3px solid #4CAF50;
} }
.task-preview strong { .task-preview strong {
color: #4CAF50; color: #4CAF50;
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
} }
.task-commands { .task-commands {
font-family: monospace; font-family: monospace;
font-size: 0.9em; font-size: 0.9em;
color: #ccc; color: #ccc;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
padding: 5px; padding: 5px;
border-radius: 3px; border-radius: 3px;
margin-top: 5px; margin-top: 5px;
white-space: pre-wrap; white-space: pre-wrap;
} }
.warning-note { .warning-note {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-top: 15px; margin-top: 15px;
padding: 10px; padding: 10px;
background: rgba(255, 193, 7, 0.1); background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3); border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 5px; border-radius: 5px;
color: #ffc107; color: #ffc107;
} }
.warning-note i { .warning-note i {
font-size: 1.2em; font-size: 1.2em;
} }
</style> </style>

View File

@@ -1,75 +1,75 @@
<div class="scheduled-tasks-embedded-warning"> <div class="scheduled-tasks-embedded-warning">
<h3>检测到嵌入的循环任务及可能的统计好感度设定</h3> <h3>检测到嵌入的循环任务及可能的统计好感度设定</h3>
<p>此角色包含循环任务,这些任务可能会自动执行斜杠命令。</p> <p>此角色包含循环任务,这些任务可能会自动执行斜杠命令。</p>
<p>您是否允许此角色使用这些任务?</p> <p>您是否允许此角色使用这些任务?</p>
<div class="warning-note"> <div class="warning-note">
<i class="fa-solid fa-exclamation-triangle"></i> <i class="fa-solid fa-exclamation-triangle"></i>
<span>注意:这些任务将在对话过程中自动执行,请确认您信任此角色的创建者。</span> <span>注意:这些任务将在对话过程中自动执行,请确认您信任此角色的创建者。</span>
</div> </div>
</div> </div>
<style> <style>
.scheduled-tasks-embedded-warning { .scheduled-tasks-embedded-warning {
max-width: 500px; max-width: 500px;
padding: 20px; padding: 20px;
} }
.scheduled-tasks-embedded-warning h3 { .scheduled-tasks-embedded-warning h3 {
color: #ff6b6b; color: #ff6b6b;
margin-bottom: 15px; margin-bottom: 15px;
} }
.task-preview-container { .task-preview-container {
margin: 15px 0; margin: 15px 0;
padding: 10px; padding: 10px;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 5px; border-radius: 5px;
} }
.task-list { .task-list {
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
margin-top: 10px; margin-top: 10px;
} }
.task-preview { .task-preview {
margin: 8px 0; margin: 8px 0;
padding: 8px; padding: 8px;
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
border-radius: 3px; border-radius: 3px;
border-left: 3px solid #4CAF50; border-left: 3px solid #4CAF50;
} }
.task-preview strong { .task-preview strong {
color: #4CAF50; color: #4CAF50;
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
} }
.task-commands { .task-commands {
font-family: monospace; font-family: monospace;
font-size: 0.9em; font-size: 0.9em;
color: #ccc; color: #ccc;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
padding: 5px; padding: 5px;
border-radius: 3px; border-radius: 3px;
margin-top: 5px; margin-top: 5px;
white-space: pre-wrap; white-space: pre-wrap;
} }
.warning-note { .warning-note {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-top: 15px; margin-top: 15px;
padding: 10px; padding: 10px;
background: rgba(255, 193, 7, 0.1); background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3); border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 5px; border-radius: 5px;
color: #ffc107; color: #ffc107;
} }
.warning-note i { .warning-note i {
font-size: 1.2em; font-size: 1.2em;
} }
</style> </style>

View File

@@ -16,6 +16,7 @@ import { executeSlashCommand } from "../../core/slash-command.js";
import { EXT_ID } from "../../core/constants.js"; import { EXT_ID } from "../../core/constants.js";
import { createModuleEvents, event_types } from "../../core/event-manager.js"; import { createModuleEvents, event_types } from "../../core/event-manager.js";
import { xbLog, CacheRegistry } from "../../core/debug-core.js"; import { xbLog, CacheRegistry } from "../../core/debug-core.js";
import { TasksStorage } from "../../core/server-storage.js";
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// 常量和默认值 // 常量和默认值
@@ -27,80 +28,72 @@ const CONFIG = { MAX_PROCESSED: 20, MAX_COOLDOWN: 10, CLEANUP_INTERVAL: 30000, T
const events = createModuleEvents('scheduledTasks'); const events = createModuleEvents('scheduledTasks');
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// IndexedDB 脚本存储 // 数据迁移
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
const TaskScriptDB = { async function migrateToServerStorage() {
dbName: 'LittleWhiteBox_TaskScripts', const FLAG = 'LWB_tasks_migrated_server_v1';
storeName: 'scripts', if (localStorage.getItem(FLAG)) return;
_db: null,
_cache: new Map(),
async open() { let count = 0;
if (this._db) return this._db;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => { this._db = request.result; resolve(this._db); };
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
},
async get(taskId) { const settings = getSettings();
if (!taskId) return ''; for (const task of (settings.globalTasks || [])) {
if (this._cache.has(taskId)) return this._cache.get(taskId); if (!task) continue;
try { if (!task.id) task.id = uuidv4();
const db = await this.open(); if (task.commands) {
return new Promise((resolve) => { await TasksStorage.set(task.id, task.commands);
const tx = db.transaction(this.storeName, 'readonly'); delete task.commands;
const request = tx.objectStore(this.storeName).get(taskId); count++;
request.onerror = () => resolve(''); }
request.onsuccess = () => {
const val = request.result || '';
this._cache.set(taskId, val);
resolve(val);
};
});
} catch { return ''; }
},
async set(taskId, commands) {
if (!taskId) return;
this._cache.set(taskId, commands || '');
try {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).put(commands || '', taskId);
tx.oncomplete = () => resolve();
tx.onerror = () => resolve();
});
} catch {}
},
async delete(taskId) {
if (!taskId) return;
this._cache.delete(taskId);
try {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).delete(taskId);
tx.oncomplete = () => resolve();
tx.onerror = () => resolve();
});
} catch {}
},
clearCache() {
this._cache.clear();
} }
}; if (count > 0) saveSettingsDebounced();
await new Promise((resolve) => {
const req = indexedDB.open('LittleWhiteBox_TaskScripts');
req.onerror = () => resolve();
req.onsuccess = async (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('scripts')) {
db.close();
resolve();
return;
}
try {
const tx = db.transaction('scripts', 'readonly');
const store = tx.objectStore('scripts');
const keys = await new Promise(r => {
const req = store.getAllKeys();
req.onsuccess = () => r(req.result || []);
req.onerror = () => r([]);
});
const vals = await new Promise(r => {
const req = store.getAll();
req.onsuccess = () => r(req.result || []);
req.onerror = () => r([]);
});
for (let i = 0; i < keys.length; i++) {
if (keys[i] && vals[i]) {
await TasksStorage.set(keys[i], vals[i]);
count++;
}
}
} catch (err) {
console.warn('[Tasks] IndexedDB 迁移出错:', err);
}
db.close();
indexedDB.deleteDatabase('LittleWhiteBox_TaskScripts');
resolve();
};
});
if (count > 0) {
await TasksStorage.saveNow();
console.log(`[Tasks] 已迁移 ${count} 个脚本到服务器`);
}
localStorage.setItem(FLAG, 'true');
}
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// 状态 // 状态
@@ -144,7 +137,7 @@ async function allTasksFull() {
const globalMeta = getSettings().globalTasks || []; const globalMeta = getSettings().globalTasks || [];
const globalTasks = await Promise.all(globalMeta.map(async (task) => ({ const globalTasks = await Promise.all(globalMeta.map(async (task) => ({
...task, ...task,
commands: await TaskScriptDB.get(task.id) commands: await TasksStorage.get(task.id)
}))); })));
return [ return [
...globalTasks.map(mapTiming), ...globalTasks.map(mapTiming),
@@ -156,7 +149,7 @@ async function allTasksFull() {
async function getTaskWithCommands(task, scope) { async function getTaskWithCommands(task, scope) {
if (!task) return task; if (!task) return task;
if (scope === 'global' && task.id && task.commands === undefined) { if (scope === 'global' && task.id && task.commands === undefined) {
return { ...task, commands: await TaskScriptDB.get(task.id) }; return { ...task, commands: await TasksStorage.get(task.id) };
} }
return task; return task;
} }
@@ -414,23 +407,22 @@ const getTaskListByScope = (scope) => {
}; };
async function persistTaskListByScope(scope, tasks) { async function persistTaskListByScope(scope, tasks) {
if (scope === 'character') { if (scope === 'character') return await saveCharacterTasks(tasks);
await saveCharacterTasks(tasks); if (scope === 'preset') return await savePresetTasks(tasks);
return;
}
if (scope === 'preset') {
await savePresetTasks(tasks);
return;
}
const metaOnly = []; const metaOnly = [];
for (const task of tasks) { for (const task of tasks) {
if (task.id) { if (!task) continue;
await TaskScriptDB.set(task.id, task.commands || ''); if (!task.id) task.id = uuidv4();
if (Object.prototype.hasOwnProperty.call(task, 'commands')) {
await TasksStorage.set(task.id, String(task.commands ?? ''));
} }
const { commands, ...meta } = task; const { commands, ...meta } = task;
metaOnly.push(meta); metaOnly.push(meta);
} }
getSettings().globalTasks = metaOnly; getSettings().globalTasks = metaOnly;
saveSettingsDebounced(); saveSettingsDebounced();
} }
@@ -442,7 +434,7 @@ async function removeTaskByScope(scope, taskId, fallbackIndex = -1) {
const task = list[idx]; const task = list[idx];
if (scope === 'global' && task?.id) { if (scope === 'global' && task?.id) {
await TaskScriptDB.delete(task.id); await TasksStorage.delete(task.id);
} }
list.splice(idx, 1); list.splice(idx, 1);
@@ -463,7 +455,7 @@ CacheRegistry.register('scheduledTasks', {
const b = state.taskLastExecutionTime?.size || 0; const b = state.taskLastExecutionTime?.size || 0;
const c = state.dynamicCallbacks?.size || 0; const c = state.dynamicCallbacks?.size || 0;
const d = __taskRunMap.size || 0; const d = __taskRunMap.size || 0;
const e = TaskScriptDB._cache?.size || 0; const e = TasksStorage.getCacheSize() || 0;
return a + b + c + d + e; return a + b + c + d + e;
} catch { return 0; } } catch { return 0; }
}, },
@@ -489,7 +481,7 @@ CacheRegistry.register('scheduledTasks', {
total += (entry?.timers?.size || 0) * 8; total += (entry?.timers?.size || 0) * 8;
total += (entry?.intervals?.size || 0) * 8; total += (entry?.intervals?.size || 0) * 8;
}); });
addMap(TaskScriptDB._cache, addStr); total += TasksStorage.getCacheBytes();
return total; return total;
} catch { return 0; } } catch { return 0; }
}, },
@@ -497,7 +489,7 @@ CacheRegistry.register('scheduledTasks', {
try { try {
state.processedMessagesSet?.clear?.(); state.processedMessagesSet?.clear?.();
state.taskLastExecutionTime?.clear?.(); state.taskLastExecutionTime?.clear?.();
TaskScriptDB.clearCache(); TasksStorage.clearCache();
const s = getSettings(); const s = getSettings();
if (s?.processedMessages) s.processedMessages = []; if (s?.processedMessages) s.processedMessages = [];
saveSettingsDebounced(); saveSettingsDebounced();
@@ -516,7 +508,7 @@ CacheRegistry.register('scheduledTasks', {
cooldown: state.taskLastExecutionTime?.size || 0, cooldown: state.taskLastExecutionTime?.size || 0,
dynamicCallbacks: state.dynamicCallbacks?.size || 0, dynamicCallbacks: state.dynamicCallbacks?.size || 0,
runningSingleInstances: __taskRunMap.size || 0, runningSingleInstances: __taskRunMap.size || 0,
scriptCache: TaskScriptDB._cache?.size || 0, scriptCache: TasksStorage.getCacheSize() || 0,
}; };
} catch { return {}; } } catch { return {}; }
}, },
@@ -1024,7 +1016,7 @@ async function onChatChanged(chatId) {
isCommandGenerated: false isCommandGenerated: false
}); });
state.taskLastExecutionTime.clear(); state.taskLastExecutionTime.clear();
TaskScriptDB.clearCache(); TasksStorage.clearCache();
requestAnimationFrame(() => { requestAnimationFrame(() => {
state.processedMessagesSet.clear(); state.processedMessagesSet.clear();
@@ -1081,18 +1073,26 @@ function createTaskItemSimple(task, index, scope = 'global') {
before_user: '用户前', before_user: '用户前',
any_message: '任意对话', any_message: '任意对话',
initialization: '角色卡初始化', initialization: '角色卡初始化',
character_init: '角色卡初始化',
plugin_init: '插件初始化', plugin_init: '插件初始化',
only_this_floor: '仅该楼层', only_this_floor: '仅该楼层',
chat_changed: '切换聊天后' chat_changed: '切换聊天后'
}[task.triggerTiming] || 'AI后'; }[task.triggerTiming] || 'AI后';
let displayName; let displayName;
if (task.interval === 0) displayName = `${task.name} (手动触发)`; if (task.interval === 0) {
else if (task.triggerTiming === 'initialization' || task.triggerTiming === 'character_init') displayName = `${task.name} (角色卡初始化)`; displayName = `${task.name} (手动触发)`;
else if (task.triggerTiming === 'plugin_init') displayName = `${task.name} (插件初始化)`; } else if (task.triggerTiming === 'initialization' || task.triggerTiming === 'character_init') {
else if (task.triggerTiming === 'chat_changed') displayName = `${task.name} (切换聊天后)`; displayName = `${task.name} (角色卡初始化)`;
else if (task.triggerTiming === 'only_this_floor') displayName = `${task.name} (仅第${task.interval}${floorTypeText})`; } else if (task.triggerTiming === 'plugin_init') {
else displayName = `${task.name} (${task.interval}${floorTypeText}·${triggerTimingText})`; displayName = `${task.name} (插件初始化)`;
} else if (task.triggerTiming === 'chat_changed') {
displayName = `${task.name} (切换聊天后)`;
} else if (task.triggerTiming === 'only_this_floor') {
displayName = `${task.name} (仅第${task.interval}${floorTypeText})`;
} else {
displayName = `${task.name} (每${task.interval}${floorTypeText}·${triggerTimingText})`;
}
const taskElement = $('#task_item_template').children().first().clone(); const taskElement = $('#task_item_template').children().first().clone();
taskElement.attr({ id: task.id, 'data-index': index, 'data-type': taskType }); taskElement.attr({ id: task.id, 'data-index': index, 'data-type': taskType });
@@ -1293,7 +1293,7 @@ async function showTaskEditor(task = null, isEdit = false, scope = 'global') {
const sourceList = getTaskListByScope(initialScope); const sourceList = getTaskListByScope(initialScope);
if (task && scope === 'global' && task.id) { if (task && scope === 'global' && task.id) {
task = { ...task, commands: await TaskScriptDB.get(task.id) }; task = { ...task, commands: await TasksStorage.get(task.id) };
} }
state.currentEditingTask = task; state.currentEditingTask = task;
@@ -1601,7 +1601,7 @@ async function showCloudTasksModal() {
function createCloudTaskItem(taskInfo) { function createCloudTaskItem(taskInfo) {
const item = $('#cloud_task_item_template').children().first().clone(); const item = $('#cloud_task_item_template').children().first().clone();
item.find('.cloud-task-name').text(taskInfo.name || '未命名任务'); item.find('.cloud-task-name').text(taskInfo.name || '未命名任务');
item.find('.cloud-task-intro').text(taskInfo.简介 || '无简介'); item.find('.cloud-task-intro').text(taskInfo.简介 || taskInfo.intro || '无简介');
item.find('.cloud-task-download').on('click', async function () { item.find('.cloud-task-download').on('click', async function () {
$(this).prop('disabled', true).find('i').removeClass('fa-download').addClass('fa-spinner fa-spin'); $(this).prop('disabled', true).find('i').removeClass('fa-download').addClass('fa-spinner fa-spin');
try { try {
@@ -1631,7 +1631,7 @@ async function exportGlobalTasks() {
const tasks = await Promise.all(metaList.map(async (meta) => ({ const tasks = await Promise.all(metaList.map(async (meta) => ({
...meta, ...meta,
commands: await TaskScriptDB.get(meta.id) commands: await TasksStorage.get(meta.id)
}))); })));
const fileName = `global_tasks_${new Date().toISOString().split('T')[0]}.json`; const fileName = `global_tasks_${new Date().toISOString().split('T')[0]}.json`;
@@ -1645,7 +1645,7 @@ async function exportSingleTask(index, scope) {
let task = list[index]; let task = list[index];
if (scope === 'global' && task.id) { if (scope === 'global' && task.id) {
task = { ...task, commands: await TaskScriptDB.get(task.id) }; task = { ...task, commands: await TasksStorage.get(task.id) };
} }
const fileName = `${scope}_task_${task?.name || 'unnamed'}_${new Date().toISOString().split('T')[0]}.json`; const fileName = `${scope}_task_${task?.name || 'unnamed'}_${new Date().toISOString().split('T')[0]}.json`;
@@ -1754,7 +1754,7 @@ function getMemoryUsage() {
taskCooldowns: state.taskLastExecutionTime.size, taskCooldowns: state.taskLastExecutionTime.size,
globalTasks: getSettings().globalTasks.length, globalTasks: getSettings().globalTasks.length,
characterTasks: getCharacterTasks().length, characterTasks: getCharacterTasks().length,
scriptCache: TaskScriptDB._cache.size, scriptCache: TasksStorage.getCacheSize(),
maxProcessedMessages: CONFIG.MAX_PROCESSED, maxProcessedMessages: CONFIG.MAX_PROCESSED,
maxCooldownEntries: CONFIG.MAX_COOLDOWN maxCooldownEntries: CONFIG.MAX_COOLDOWN
}; };
@@ -1792,7 +1792,7 @@ function cleanup() {
state.cleanupTimer = null; state.cleanupTimer = null;
} }
state.taskLastExecutionTime.clear(); state.taskLastExecutionTime.clear();
TaskScriptDB.clearCache(); TasksStorage.clearCache();
try { try {
if (state.dynamicCallbacks && state.dynamicCallbacks.size > 0) { if (state.dynamicCallbacks && state.dynamicCallbacks.size > 0) {
@@ -1865,11 +1865,11 @@ function cleanup() {
async function setCommands(name, commands, opts = {}) { async function setCommands(name, commands, opts = {}) {
const { mode = 'replace', scope = 'all' } = opts; const { mode = 'replace', scope = 'all' } = opts;
const hit = find(name, scope); const hit = find(name, scope);
if (!hit) throw new Error(`任务未找到: ${name}`); if (!hit) throw new Error(`找不到任务: ${name}`);
let old = hit.task.commands || ''; let old = hit.task.commands || '';
if (hit.scope === 'global' && hit.task.id) { if (hit.scope === 'global' && hit.task.id) {
old = await TaskScriptDB.get(hit.task.id); old = await TasksStorage.get(hit.task.id);
} }
const body = String(commands ?? ''); const body = String(commands ?? '');
@@ -1891,7 +1891,7 @@ function cleanup() {
async function setProps(name, props, scope = 'all') { async function setProps(name, props, scope = 'all') {
const hit = find(name, scope); const hit = find(name, scope);
if (!hit) throw new Error(`任务未找到: ${name}`); if (!hit) throw new Error(`找不到任务: ${name}`);
Object.assign(hit.task, props || {}); Object.assign(hit.task, props || {});
await persistTaskListByScope(hit.scope, hit.list); await persistTaskListByScope(hit.scope, hit.list);
refreshTaskLists(); refreshTaskLists();
@@ -1900,10 +1900,10 @@ function cleanup() {
async function exec(name) { async function exec(name) {
const hit = find(name, 'all'); const hit = find(name, 'all');
if (!hit) throw new Error(`任务未找到: ${name}`); if (!hit) throw new Error(`找不到任务: ${name}`);
let commands = hit.task.commands || ''; let commands = hit.task.commands || '';
if (hit.scope === 'global' && hit.task.id) { if (hit.scope === 'global' && hit.task.id) {
commands = await TaskScriptDB.get(hit.task.id); commands = await TasksStorage.get(hit.task.id);
} }
return await executeCommands(commands, hit.task.name); return await executeCommands(commands, hit.task.name);
} }
@@ -1911,7 +1911,7 @@ function cleanup() {
async function dump(scope = 'all') { async function dump(scope = 'all') {
const g = await Promise.all((getSettings().globalTasks || []).map(async t => ({ const g = await Promise.all((getSettings().globalTasks || []).map(async t => ({
...structuredClone(t), ...structuredClone(t),
commands: await TaskScriptDB.get(t.id) commands: await TasksStorage.get(t.id)
}))); })));
const c = structuredClone(getCharacterTasks() || []); const c = structuredClone(getCharacterTasks() || []);
const p = structuredClone(getPresetTasks() || []); const p = structuredClone(getPresetTasks() || []);
@@ -2078,37 +2078,7 @@ function registerSlashCommands() {
helpString: `设置任务属性。用法: /xbset status=on/off interval=数字 timing=时机 floorType=类型 任务名` helpString: `设置任务属性。用法: /xbset status=on/off interval=数字 timing=时机 floorType=类型 任务名`
})); }));
} catch (error) { } catch (error) {
console.error("Error registering slash commands:", error); console.error("注册斜杠命令时出错:", error);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 数据迁移
// ═══════════════════════════════════════════════════════════════════════════
async function migrateGlobalTasksToIndexedDB() {
const settings = getSettings();
const tasks = settings.globalTasks || [];
let migrated = false;
const metaOnly = [];
for (const task of tasks) {
if (!task || !task.id) continue;
if (task.commands !== undefined && task.commands !== '') {
await TaskScriptDB.set(task.id, task.commands);
console.log(`[Tasks] 迁移脚本: ${task.name} (${(String(task.commands).length / 1024).toFixed(1)}KB)`);
migrated = true;
}
const { commands, ...meta } = task;
metaOnly.push(meta);
}
if (migrated) {
settings.globalTasks = metaOnly;
saveSettingsDebounced();
console.log('[Tasks] 全局任务迁移完成');
} }
} }
@@ -2116,14 +2086,14 @@ async function migrateGlobalTasksToIndexedDB() {
// 初始化 // 初始化
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
function initTasks() { async function initTasks() {
if (window.__XB_TASKS_INITIALIZED__) { if (window.__XB_TASKS_INITIALIZED__) {
console.log('[小白X任务] 已经初始化,跳过重复注册'); console.log('[小白X任务] 已经初始化,跳过重复注册');
return; return;
} }
window.__XB_TASKS_INITIALIZED__ = true; window.__XB_TASKS_INITIALIZED__ = true;
migrateGlobalTasksToIndexedDB(); await migrateToServerStorage();
hydrateProcessedSetFromSettings(); hydrateProcessedSetFromSettings();
scheduleCleanup(); scheduleCleanup();

View File

@@ -1,104 +1,104 @@
import { extension_settings, getContext } from "../../../../extensions.js"; import { extension_settings, getContext } from "../../../../extensions.js";
import { saveSettingsDebounced, setExtensionPrompt, extension_prompt_types } from "../../../../../script.js"; import { saveSettingsDebounced, setExtensionPrompt, extension_prompt_types } from "../../../../../script.js";
import { EXT_ID, extensionFolderPath } from "../core/constants.js"; import { EXT_ID, extensionFolderPath } from "../core/constants.js";
import { createModuleEvents, event_types } from "../core/event-manager.js"; import { createModuleEvents, event_types } from "../core/event-manager.js";
const SCRIPT_MODULE_NAME = "xiaobaix-script"; const SCRIPT_MODULE_NAME = "xiaobaix-script";
const events = createModuleEvents('scriptAssistant'); const events = createModuleEvents('scriptAssistant');
function initScriptAssistant() { function initScriptAssistant() {
if (!extension_settings[EXT_ID].scriptAssistant) { if (!extension_settings[EXT_ID].scriptAssistant) {
extension_settings[EXT_ID].scriptAssistant = { enabled: false }; extension_settings[EXT_ID].scriptAssistant = { enabled: false };
} }
if (window['registerModuleCleanup']) { if (window['registerModuleCleanup']) {
window['registerModuleCleanup']('scriptAssistant', cleanup); window['registerModuleCleanup']('scriptAssistant', cleanup);
} }
$('#xiaobaix_script_assistant').on('change', function() { $('#xiaobaix_script_assistant').on('change', function() {
let globalEnabled = true; let globalEnabled = true;
try { if ('isXiaobaixEnabled' in window) globalEnabled = Boolean(window['isXiaobaixEnabled']); } catch {} try { if ('isXiaobaixEnabled' in window) globalEnabled = Boolean(window['isXiaobaixEnabled']); } catch {}
if (!globalEnabled) return; if (!globalEnabled) return;
const enabled = $(this).prop('checked'); const enabled = $(this).prop('checked');
extension_settings[EXT_ID].scriptAssistant.enabled = enabled; extension_settings[EXT_ID].scriptAssistant.enabled = enabled;
saveSettingsDebounced(); saveSettingsDebounced();
if (enabled) { if (enabled) {
if (typeof window['injectScriptDocs'] === 'function') window['injectScriptDocs'](); if (typeof window['injectScriptDocs'] === 'function') window['injectScriptDocs']();
} else { } else {
if (typeof window['removeScriptDocs'] === 'function') window['removeScriptDocs'](); if (typeof window['removeScriptDocs'] === 'function') window['removeScriptDocs']();
cleanup(); cleanup();
} }
}); });
$('#xiaobaix_script_assistant').prop('checked', extension_settings[EXT_ID].scriptAssistant.enabled); $('#xiaobaix_script_assistant').prop('checked', extension_settings[EXT_ID].scriptAssistant.enabled);
setupEventListeners(); setupEventListeners();
if (extension_settings[EXT_ID].scriptAssistant.enabled) { if (extension_settings[EXT_ID].scriptAssistant.enabled) {
setTimeout(() => { if (typeof window['injectScriptDocs'] === 'function') window['injectScriptDocs'](); }, 1000); setTimeout(() => { if (typeof window['injectScriptDocs'] === 'function') window['injectScriptDocs'](); }, 1000);
} }
} }
function setupEventListeners() { function setupEventListeners() {
events.on(event_types.CHAT_CHANGED, () => setTimeout(checkAndInjectDocs, 500)); events.on(event_types.CHAT_CHANGED, () => setTimeout(checkAndInjectDocs, 500));
events.on(event_types.MESSAGE_RECEIVED, checkAndInjectDocs); events.on(event_types.MESSAGE_RECEIVED, checkAndInjectDocs);
events.on(event_types.USER_MESSAGE_RENDERED, checkAndInjectDocs); events.on(event_types.USER_MESSAGE_RENDERED, checkAndInjectDocs);
events.on(event_types.SETTINGS_LOADED_AFTER, () => setTimeout(checkAndInjectDocs, 1000)); events.on(event_types.SETTINGS_LOADED_AFTER, () => setTimeout(checkAndInjectDocs, 1000));
events.on(event_types.APP_READY, () => setTimeout(checkAndInjectDocs, 1500)); events.on(event_types.APP_READY, () => setTimeout(checkAndInjectDocs, 1500));
} }
function cleanup() { function cleanup() {
events.cleanup(); events.cleanup();
if (typeof window['removeScriptDocs'] === 'function') window['removeScriptDocs'](); if (typeof window['removeScriptDocs'] === 'function') window['removeScriptDocs']();
} }
function checkAndInjectDocs() { function checkAndInjectDocs() {
const globalEnabled = window.isXiaobaixEnabled !== undefined ? window.isXiaobaixEnabled : extension_settings[EXT_ID].enabled; const globalEnabled = window.isXiaobaixEnabled !== undefined ? window.isXiaobaixEnabled : extension_settings[EXT_ID].enabled;
if (globalEnabled && extension_settings[EXT_ID].scriptAssistant?.enabled) { if (globalEnabled && extension_settings[EXT_ID].scriptAssistant?.enabled) {
injectScriptDocs(); injectScriptDocs();
} else { } else {
removeScriptDocs(); removeScriptDocs();
} }
} }
async function injectScriptDocs() { async function injectScriptDocs() {
try { try {
let docsContent = ''; let docsContent = '';
try { try {
const response = await fetch(`${extensionFolderPath}/docs/script-docs.md`); const response = await fetch(`${extensionFolderPath}/docs/script-docs.md`);
if (response.ok) { if (response.ok) {
docsContent = await response.text(); docsContent = await response.text();
} }
} catch (error) { } catch (error) {
docsContent = "无法加载script-docs.md文件"; docsContent = "无法加载script-docs.md文件";
} }
const formattedPrompt = ` const formattedPrompt = `
【小白X插件 - 写卡助手】 【小白X插件 - 写卡助手】
你是小白X插件的内置助手专门帮助用户创建STscript脚本和交互式界面的角色卡。 你是小白X插件的内置助手专门帮助用户创建STscript脚本和交互式界面的角色卡。
以下是小白x功能和SillyTavern的官方STscript脚本文档可结合小白X功能创作与SillyTavern深度交互的角色卡 以下是小白x功能和SillyTavern的官方STscript脚本文档可结合小白X功能创作与SillyTavern深度交互的角色卡
${docsContent} ${docsContent}
`; `;
setExtensionPrompt( setExtensionPrompt(
SCRIPT_MODULE_NAME, SCRIPT_MODULE_NAME,
formattedPrompt, formattedPrompt,
extension_prompt_types.IN_PROMPT, extension_prompt_types.IN_PROMPT,
2, 2,
false, false,
0 0
); );
} catch (error) {} } catch (error) {}
} }
function removeScriptDocs() { function removeScriptDocs() {
setExtensionPrompt(SCRIPT_MODULE_NAME, '', extension_prompt_types.IN_PROMPT, 2, false, 0); setExtensionPrompt(SCRIPT_MODULE_NAME, '', extension_prompt_types.IN_PROMPT, 2, false, 0);
} }
window.injectScriptDocs = injectScriptDocs; window.injectScriptDocs = injectScriptDocs;
window.removeScriptDocs = removeScriptDocs; window.removeScriptDocs = removeScriptDocs;
export { initScriptAssistant }; export { initScriptAssistant };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -437,6 +437,27 @@ body {
from { opacity: 0; transform: translateX(-50%) translateY(10px); } from { opacity: 0; transform: translateX(-50%) translateY(10px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); } to { opacity: 1; transform: translateX(-50%) translateY(0); }
} }
.stat-warning {
font-size: 0.625rem;
color: #ff9800;
margin-top: 4px;
}
#keep-visible-count {
width: 32px;
padding: 2px 4px;
margin: 0 2px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
font-size: inherit;
font-weight: bold;
color: var(--highlight);
text-align: center;
border-radius: 3px;
}
#keep-visible-count:focus {
border-color: var(--accent);
outline: none;
}
</style> </style>
</head> </head>
<body> <body>
@@ -458,14 +479,17 @@ body {
<div class="stat-item"> <div class="stat-item">
<div class="stat-value"><span class="highlight" id="stat-pending">0</span></div> <div class="stat-value"><span class="highlight" id="stat-pending">0</span></div>
<div class="stat-label">待总结</div> <div class="stat-label">待总结</div>
<div class="stat-warning hidden" id="pending-warning">再删1条将回滚</div>
</div> </div>
</div> </div>
</header> </header>
<div class="controls-bar"> <div class="controls-bar">
<label class="status-checkbox"> <label class="status-checkbox">
<input type="checkbox" id="hide-summarized"> <input type="checkbox" id="hide-summarized">
<span>聊天时隐藏已总结 · <strong id="summarized-count">0</strong> 楼(保留3楼</span> <span>聊天时隐藏已总结 · <strong id="summarized-count">0</strong> 楼(保留
</label> <input type="number" id="keep-visible-count" min="0" max="50" value="3">
楼)</span>
</label>
<span class="spacer"></span> <span class="spacer"></span>
<button class="btn btn-icon" id="btn-settings"> <button class="btn btn-icon" id="btn-settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -681,7 +705,16 @@ function preserveAddedAt(newItem, oldItem) { if (oldItem?._addedAt != null) newI
function loadConfig() { function loadConfig() {
try { try {
const saved = localStorage.getItem('summary_panel_config'); const saved = localStorage.getItem('summary_panel_config');
if (saved) { const p = JSON.parse(saved); Object.assign(config.api, p.api || {}); Object.assign(config.gen, p.gen || {}); Object.assign(config.trigger, p.trigger || {}); } if (saved) {
const p = JSON.parse(saved);
Object.assign(config.api, p.api || {});
Object.assign(config.gen, p.gen || {});
Object.assign(config.trigger, p.trigger || {});
if (config.trigger.timing === 'manual' && config.trigger.enabled) {
config.trigger.enabled = false;
saveConfig();
}
}
} catch {} } catch {}
} }
function saveConfig() { try { localStorage.setItem('summary_panel_config', JSON.stringify(config)); } catch {} } function saveConfig() { try { localStorage.setItem('summary_panel_config', JSON.stringify(config)); } catch {} }
@@ -921,7 +954,15 @@ function renderArcs(arcs) {
}); });
}); });
} }
function updateStats(s) { if (!s) return; document.getElementById('stat-summarized').textContent = s.summarizedUpTo ?? 0; document.getElementById('stat-events').textContent = s.eventsCount ?? 0; document.getElementById('stat-pending').textContent = s.pendingFloors ?? 0; } function updateStats(s) {
if (!s) return;
document.getElementById('stat-summarized').textContent = s.summarizedUpTo ?? 0;
document.getElementById('stat-events').textContent = s.eventsCount ?? 0;
const pending = s.pendingFloors ?? 0;
document.getElementById('stat-pending').textContent = pending;
document.getElementById('pending-warning').classList.toggle('hidden', pending !== -1);
}
const editorModal = document.getElementById('editor-modal'); const editorModal = document.getElementById('editor-modal');
const editorTextarea = document.getElementById('editor-textarea'); const editorTextarea = document.getElementById('editor-textarea');
const editorError = document.getElementById('editor-error'); const editorError = document.getElementById('editor-error');
@@ -1141,6 +1182,17 @@ function openSettings() {
document.getElementById('trigger-enabled').checked = config.trigger.enabled; document.getElementById('trigger-enabled').checked = config.trigger.enabled;
document.getElementById('trigger-interval').value = config.trigger.interval; document.getElementById('trigger-interval').value = config.trigger.interval;
document.getElementById('trigger-timing').value = config.trigger.timing; document.getElementById('trigger-timing').value = config.trigger.timing;
const enabledCheckbox = document.getElementById('trigger-enabled');
if (config.trigger.timing === 'manual') {
enabledCheckbox.checked = false;
enabledCheckbox.disabled = true;
enabledCheckbox.parentElement.style.opacity = '0.5';
} else {
enabledCheckbox.disabled = false;
enabledCheckbox.parentElement.style.opacity = '1';
}
if (config.api.modelCache.length > 0) { if (config.api.modelCache.length > 0) {
const sel = document.getElementById('api-model-select'); const sel = document.getElementById('api-model-select');
sel.innerHTML = config.api.modelCache.map(m => `<option value="${m}" ${m === config.api.model ? 'selected' : ''}>${m}</option>`).join(''); sel.innerHTML = config.api.modelCache.map(m => `<option value="${m}" ${m === config.api.model ? 'selected' : ''}>${m}</option>`).join('');
@@ -1169,9 +1221,12 @@ function closeSettings(save) {
config.gen.top_k = pn('gen-top-k'); config.gen.top_k = pn('gen-top-k');
config.gen.presence_penalty = pn('gen-presence'); config.gen.presence_penalty = pn('gen-presence');
config.gen.frequency_penalty = pn('gen-frequency'); config.gen.frequency_penalty = pn('gen-frequency');
config.trigger.enabled = document.getElementById('trigger-enabled').checked;
const timing = document.getElementById('trigger-timing').value;
config.trigger.timing = timing;
config.trigger.enabled = (timing === 'manual') ? false : document.getElementById('trigger-enabled').checked;
config.trigger.interval = parseInt(document.getElementById('trigger-interval').value) || 20; config.trigger.interval = parseInt(document.getElementById('trigger-interval').value) || 20;
config.trigger.timing = document.getElementById('trigger-timing').value;
saveConfig(); saveConfig();
} }
tempConfig = null; tempConfig = null;
@@ -1254,7 +1309,12 @@ window.addEventListener('message', event => {
updateStats(data.stats); updateStats(data.stats);
document.getElementById('summarized-count').textContent = data.stats.hiddenCount ?? 0; document.getElementById('summarized-count').textContent = data.stats.hiddenCount ?? 0;
} }
if (data.hideSummarized !== undefined) document.getElementById('hide-summarized').checked = data.hideSummarized; if (data.hideSummarized !== undefined) {
document.getElementById('hide-summarized').checked = data.hideSummarized;
}
if (data.keepVisibleCount !== undefined) {
document.getElementById('keep-visible-count').value = data.keepVisibleCount;
}
break; break;
case 'SUMMARY_FULL_DATA': case 'SUMMARY_FULL_DATA':
if (data.payload) { if (data.payload) {
@@ -1294,17 +1354,43 @@ document.addEventListener('DOMContentLoaded', () => {
renderKeywords([]); renderKeywords([]);
renderTimeline([]); renderTimeline([]);
renderArcs([]); renderArcs([]);
document.getElementById('hide-summarized').addEventListener('change', e => { document.getElementById('hide-summarized').addEventListener('change', e => {
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'TOGGLE_HIDE_SUMMARIZED', enabled: e.target.checked }, '*'); window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'TOGGLE_HIDE_SUMMARIZED', enabled: e.target.checked }, '*');
}); });
document.getElementById('keep-visible-count').addEventListener('change', e => {
const count = Math.max(0, Math.min(50, parseInt(e.target.value) || 3));
e.target.value = count;
window.parent.postMessage({
source: 'LittleWhiteBox-StoryFrame',
type: 'UPDATE_KEEP_VISIBLE',
count: count
}, '*');
});
document.getElementById('btn-fullscreen-relations').addEventListener('click', openRelationsFullscreen); document.getElementById('btn-fullscreen-relations').addEventListener('click', openRelationsFullscreen);
document.getElementById('relations-fullscreen-backdrop').addEventListener('click', closeRelationsFullscreen); document.getElementById('relations-fullscreen-backdrop').addEventListener('click', closeRelationsFullscreen);
document.getElementById('relations-fullscreen-close').addEventListener('click', closeRelationsFullscreen); document.getElementById('relations-fullscreen-close').addEventListener('click', closeRelationsFullscreen);
document.getElementById('trigger-timing').addEventListener('change', e => {
const timing = e.target.value;
const enabledCheckbox = document.getElementById('trigger-enabled');
if (timing === 'manual') {
enabledCheckbox.checked = false;
enabledCheckbox.disabled = true;
enabledCheckbox.parentElement.style.opacity = '0.5';
} else {
enabledCheckbox.disabled = false;
enabledCheckbox.parentElement.style.opacity = '1';
}
});
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
relationChart?.resize(); relationChart?.resize();
relationChartFullscreen?.resize(); relationChartFullscreen?.resize();
}); });
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'FRAME_READY' }, '*'); window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'FRAME_READY' }, '*');
}); });
</script> </script>

View File

@@ -22,7 +22,6 @@ const events = createModuleEvents(MODULE_ID);
const SUMMARY_SESSION_ID = 'xb9'; const SUMMARY_SESSION_ID = 'xb9';
const SUMMARY_PROMPT_KEY = 'LittleWhiteBox_StorySummary'; const SUMMARY_PROMPT_KEY = 'LittleWhiteBox_StorySummary';
const iframePath = `${extensionFolderPath}/modules/story-summary/story-summary.html`; const iframePath = `${extensionFolderPath}/modules/story-summary/story-summary.html`;
const KEEP_VISIBLE_COUNT = 3;
const VALID_SECTIONS = ['keywords', 'events', 'characters', 'arcs']; const VALID_SECTIONS = ['keywords', 'events', 'characters', 'arcs'];
const PROVIDER_MAP = { const PROVIDER_MAP = {
@@ -54,8 +53,14 @@ let eventsRegistered = false;
const sleep = ms => new Promise(r => setTimeout(r, ms)); const sleep = ms => new Promise(r => setTimeout(r, ms));
function getKeepVisibleCount() {
const store = getSummaryStore();
return store?.keepVisibleCount ?? 3;
}
function calcHideRange(lastSummarized) { function calcHideRange(lastSummarized) {
const hideEnd = lastSummarized - KEEP_VISIBLE_COUNT; const keepCount = getKeepVisibleCount();
const hideEnd = lastSummarized - keepCount;
if (hideEnd < 0) return null; if (hideEnd < 0) return null;
return { start: 0, end: hideEnd }; return { start: 0, end: hideEnd };
} }
@@ -217,25 +222,35 @@ function rollbackSummaryIfNeeded() {
const currentLength = Array.isArray(chat) ? chat.length : 0; const currentLength = Array.isArray(chat) ? chat.length : 0;
const store = getSummaryStore(); const store = getSummaryStore();
if (!store || store.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) return false; if (!store || store.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) {
return false;
}
if (currentLength <= store.lastSummarizedMesId) { const lastSummarized = store.lastSummarizedMesId;
const deletedCount = store.lastSummarizedMesId + 1 - currentLength;
if (deletedCount < 2) return false;
xbLog.warn(MODULE_ID, `删除已总结楼层 ${deletedCount} 个,触发回滚`); if (currentLength <= lastSummarized) {
const deletedCount = lastSummarized + 1 - currentLength;
if (deletedCount < 2) {
return false;
}
xbLog.warn(MODULE_ID, `删除已总结楼层 ${deletedCount} 条,当前${currentLength},原总结到${lastSummarized + 1},触发回滚`);
const history = store.summaryHistory || []; const history = store.summaryHistory || [];
let targetEndMesId = -1; let targetEndMesId = -1;
for (let i = history.length - 1; i >= 0; i--) { for (let i = history.length - 1; i >= 0; i--) {
if (history[i].endMesId < currentLength) { if (history[i].endMesId < currentLength) {
targetEndMesId = history[i].endMesId; targetEndMesId = history[i].endMesId;
break; break;
} }
} }
executeFilterRollback(store, targetEndMesId, currentLength); executeFilterRollback(store, targetEndMesId, currentLength);
return true; return true;
} }
return false; return false;
} }
@@ -251,6 +266,7 @@ function executeFilterRollback(store, targetEndMesId, currentLength) {
store.hideSummarizedHistory = false; store.hideSummarizedHistory = false;
} else { } else {
const json = store.json || {}; const json = store.json || {};
json.events = (json.events || []).filter(e => (e._addedAt ?? 0) <= targetEndMesId); json.events = (json.events || []).filter(e => (e._addedAt ?? 0) <= targetEndMesId);
json.keywords = (json.keywords || []).filter(k => (k._addedAt ?? 0) <= targetEndMesId); json.keywords = (json.keywords || []).filter(k => (k._addedAt ?? 0) <= targetEndMesId);
json.arcs = (json.arcs || []).filter(a => (a._addedAt ?? 0) <= targetEndMesId); json.arcs = (json.arcs || []).filter(a => (a._addedAt ?? 0) <= targetEndMesId);
@@ -259,6 +275,7 @@ function executeFilterRollback(store, targetEndMesId, currentLength) {
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
); );
}); });
if (json.characters) { if (json.characters) {
json.characters.main = (json.characters.main || []).filter(m => json.characters.main = (json.characters.main || []).filter(m =>
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
@@ -267,15 +284,20 @@ function executeFilterRollback(store, targetEndMesId, currentLength) {
(r._addedAt ?? 0) <= targetEndMesId (r._addedAt ?? 0) <= targetEndMesId
); );
} }
store.json = json; store.json = json;
store.lastSummarizedMesId = targetEndMesId; store.lastSummarizedMesId = targetEndMesId;
store.summaryHistory = (store.summaryHistory || []).filter(h => h.endMesId <= targetEndMesId); store.summaryHistory = (store.summaryHistory || []).filter(h => h.endMesId <= targetEndMesId);
} }
if (oldHideRange) { if (oldHideRange && oldHideRange.end >= 0) {
const newHideRange = targetEndMesId >= 0 ? calcHideRange(targetEndMesId) : null; const newHideRange = (targetEndMesId >= 0 && store.hideSummarizedHistory)
const unhideStart = newHideRange ? newHideRange.end + 1 : 0; ? calcHideRange(targetEndMesId)
: null;
const unhideStart = newHideRange ? Math.min(newHideRange.end + 1, currentLength) : 0;
const unhideEnd = Math.min(oldHideRange.end, currentLength - 1); const unhideEnd = Math.min(oldHideRange.end, currentLength - 1);
if (unhideStart <= unhideEnd) { if (unhideStart <= unhideEnd) {
executeSlashCommand(`/unhide ${unhideStart}-${unhideEnd}`); executeSlashCommand(`/unhide ${unhideStart}-${unhideEnd}`);
} }
@@ -440,6 +462,39 @@ function handleFrameMessage(event) {
} }
break; break;
} }
case "UPDATE_KEEP_VISIBLE": {
const store = getSummaryStore();
if (!store) break;
const oldCount = store.keepVisibleCount ?? 3;
const newCount = Math.max(0, Math.min(50, parseInt(data.count) || 3));
if (newCount === oldCount) break;
store.keepVisibleCount = newCount;
saveSummaryStore();
const lastSummarized = store.lastSummarizedMesId ?? -1;
if (store.hideSummarizedHistory && lastSummarized >= 0) {
(async () => {
await executeSlashCommand(`/unhide 0-${lastSummarized}`);
const range = calcHideRange(lastSummarized);
if (range) {
await executeSlashCommand(`/hide ${range.start}-${range.end}`);
}
const { chat } = getContext();
const totalFloors = Array.isArray(chat) ? chat.length : 0;
sendFrameBaseData(store, totalFloors);
})();
} else {
const { chat } = getContext();
const totalFloors = Array.isArray(chat) ? chat.length : 0;
sendFrameBaseData(store, totalFloors);
}
break;
}
} }
} }
@@ -558,6 +613,7 @@ function sendFrameBaseData(store, totalFloors) {
hiddenCount, hiddenCount,
}, },
hideSummarized: store?.hideSummarizedHistory || false, hideSummarized: store?.hideSummarizedHistory || false,
keepVisibleCount: store?.keepVisibleCount ?? 3,
}); });
} }
@@ -721,11 +777,18 @@ function getSummaryPanelConfig() {
const raw = localStorage.getItem('summary_panel_config'); const raw = localStorage.getItem('summary_panel_config');
if (!raw) return defaults; if (!raw) return defaults;
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
return {
const result = {
api: { ...defaults.api, ...(parsed.api || {}) }, api: { ...defaults.api, ...(parsed.api || {}) },
gen: { ...defaults.gen, ...(parsed.gen || {}) }, gen: { ...defaults.gen, ...(parsed.gen || {}) },
trigger: { ...defaults.trigger, ...(parsed.trigger || {}) }, trigger: { ...defaults.trigger, ...(parsed.trigger || {}) },
}; };
if (result.trigger.timing === 'manual') {
result.trigger.enabled = false;
}
return result;
} catch { } catch {
return defaults; return defaults;
} }
@@ -876,10 +939,12 @@ async function maybeAutoRunSummary(reason) {
const cfgAll = getSummaryPanelConfig(); const cfgAll = getSummaryPanelConfig();
const trig = cfgAll.trigger || {}; const trig = cfgAll.trigger || {};
if (trig.timing === 'manual') return;
if (!trig.enabled) return; if (!trig.enabled) return;
if (trig.timing === 'after_ai' && reason !== 'after_ai') return; if (trig.timing === 'after_ai' && reason !== 'after_ai') return;
if (trig.timing === 'before_user' && reason !== 'before_user') return; if (trig.timing === 'before_user' && reason !== 'before_user') return;
if (trig.timing === 'manual') return;
if (isSummaryGenerating()) return; if (isSummaryGenerating()) return;
const store = getSummaryStore(); const store = getSummaryStore();
@@ -976,29 +1041,34 @@ function clearSummaryExtensionPrompt() {
function handleChatChanged() { function handleChatChanged() {
const { chat } = getContext(); const { chat } = getContext();
lastKnownChatLength = Array.isArray(chat) ? chat.length : 0; const newLength = Array.isArray(chat) ? chat.length : 0;
rollbackSummaryIfNeeded();
lastKnownChatLength = newLength;
initButtonsForAll(); initButtonsForAll();
updateSummaryExtensionPrompt(); updateSummaryExtensionPrompt();
const store = getSummaryStore(); const store = getSummaryStore();
const lastSummarized = store?.lastSummarizedMesId ?? -1; const lastSummarized = store?.lastSummarizedMesId ?? -1;
if (lastSummarized >= 0 && store?.hideSummarizedHistory) {
if (lastSummarized >= 0 && store?.hideSummarizedHistory === true) {
const range = calcHideRange(lastSummarized); const range = calcHideRange(lastSummarized);
if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`); if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`);
} }
if (frameReady) { if (frameReady) {
sendFrameBaseData(store, lastKnownChatLength); sendFrameBaseData(store, newLength);
sendFrameFullData(store, lastKnownChatLength); sendFrameFullData(store, newLength);
} }
} }
function handleMessageDeleted() { function handleMessageDeleted() {
const { chat } = getContext(); const { chat } = getContext();
const currentLength = Array.isArray(chat) ? chat.length : 0; const currentLength = Array.isArray(chat) ? chat.length : 0;
if (currentLength < lastKnownChatLength) {
rollbackSummaryIfNeeded(); rollbackSummaryIfNeeded();
}
lastKnownChatLength = currentLength; lastKnownChatLength = currentLength;
updateSummaryExtensionPrompt(); updateSummaryExtensionPrompt();
} }
@@ -1021,7 +1091,11 @@ function handleMessageSent() {
function handleMessageUpdated() { function handleMessageUpdated() {
const { chat } = getContext(); const { chat } = getContext();
lastKnownChatLength = Array.isArray(chat) ? chat.length : 0; const currentLength = Array.isArray(chat) ? chat.length : 0;
rollbackSummaryIfNeeded();
lastKnownChatLength = currentLength;
updateSummaryExtensionPrompt(); updateSummaryExtensionPrompt();
initButtonsForAll(); initButtonsForAll();
} }
@@ -1050,7 +1124,7 @@ function registerEvents() {
getSize: () => pendingFrameMessages.length, getSize: () => pendingFrameMessages.length,
getBytes: () => { getBytes: () => {
try { try {
return JSON.stringify(pendingFrameMessages || []).length * 2; // UTF-16 return JSON.stringify(pendingFrameMessages || []).length * 2;
} catch { } catch {
return 0; return 0;
} }
@@ -1061,15 +1135,18 @@ function registerEvents() {
}, },
}); });
const { chat } = getContext();
lastKnownChatLength = Array.isArray(chat) ? chat.length : 0;
initButtonsForAll(); initButtonsForAll();
events.on(event_types.CHAT_CHANGED, () => setTimeout(handleChatChanged, 80)); events.on(event_types.CHAT_CHANGED, () => setTimeout(handleChatChanged, 80));
events.on(event_types.MESSAGE_DELETED, () => setTimeout(handleMessageDeleted, 100)); events.on(event_types.MESSAGE_DELETED, () => setTimeout(handleMessageDeleted, 50));
events.on(event_types.MESSAGE_RECEIVED, () => setTimeout(handleMessageReceived, 150)); events.on(event_types.MESSAGE_RECEIVED, () => setTimeout(handleMessageReceived, 150));
events.on(event_types.MESSAGE_SENT, () => setTimeout(handleMessageSent, 150)); events.on(event_types.MESSAGE_SENT, () => setTimeout(handleMessageSent, 150));
events.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageUpdated, 150)); events.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageUpdated, 100));
events.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 150)); events.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 100));
events.on(event_types.MESSAGE_EDITED, () => setTimeout(handleMessageUpdated, 150)); events.on(event_types.MESSAGE_EDITED, () => setTimeout(handleMessageUpdated, 100));
events.on(event_types.USER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50)); events.on(event_types.USER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50));
events.on(event_types.CHARACTER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50)); events.on(event_types.CHARACTER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50));
} }

View File

@@ -1,7 +1,8 @@
import { eventSource, event_types, main_api, chat, name1, getRequestHeaders, extractMessageFromData, activateSendButtons, deactivateSendButtons } from "../../../../../script.js"; // 删掉:getRequestHeaders, extractMessageFromData, getStreamingReply, tryParseStreamingError, getEventSourceStream
import { getStreamingReply, chat_completion_sources, oai_settings, promptManager, getChatCompletionModel, tryParseStreamingError } from "../../../../openai.js";
import { eventSource, event_types, chat, name1, activateSendButtons, deactivateSendButtons } from "../../../../../script.js";
import { chat_completion_sources, oai_settings, promptManager, getChatCompletionModel } from "../../../../openai.js";
import { ChatCompletionService } from "../../../../custom-request.js"; import { ChatCompletionService } from "../../../../custom-request.js";
import { getEventSourceStream } from "../../../../sse-stream.js";
import { getContext } from "../../../../st-context.js"; import { getContext } from "../../../../st-context.js";
import { SlashCommandParser } from "../../../../slash-commands/SlashCommandParser.js"; import { SlashCommandParser } from "../../../../slash-commands/SlashCommandParser.js";
import { SlashCommand } from "../../../../slash-commands/SlashCommand.js"; import { SlashCommand } from "../../../../slash-commands/SlashCommand.js";
@@ -239,85 +240,55 @@ class StreamingGeneration {
if (oai_settings?.custom_exclude_body) body.custom_exclude_body = oai_settings.custom_exclude_body; if (oai_settings?.custom_exclude_body) body.custom_exclude_body = oai_settings.custom_exclude_body;
} }
if (stream) { if (stream) {
const response = await fetch('/api/backends/chat-completions/generate', { // 流式:走 ChatCompletionService 统一链路
method: 'POST', body: JSON.stringify(body), const payload = ChatCompletionService.createRequestData(body);
headers: getRequestHeaders(), signal: abortSignal, const streamFactory = await ChatCompletionService.sendRequest(payload, false, abortSignal);
}); const generator = (typeof streamFactory === 'function') ? streamFactory() : streamFactory;
if (!response.ok) {
const txt = await response.text().catch(() => '');
tryParseStreamingError(response, txt);
throw new Error(txt || `后端响应错误: ${response.status}`);
}
const eventStream = getEventSourceStream();
response.body.pipeThrough(eventStream);
const reader = eventStream.readable.getReader();
const state = { reasoning: '', image: '' };
let text = '';
return (async function* () { return (async function* () {
let last = '';
try { try {
while (true) { for await (const item of (generator || [])) {
const { done, value } = await reader.read(); if (abortSignal?.aborted) return;
if (done) return;
if (!value?.data) continue;
const rawData = value.data;
if (rawData === '[DONE]') return;
tryParseStreamingError(response, rawData);
let parsed;
try {
parsed = JSON.parse(rawData);
} catch (e) {
console.warn('[StreamingGeneration] JSON parse error:', e, 'rawData:', rawData);
continue;
}
// 提取回复内容
const chunk = getStreamingReply(parsed, state, { chatCompletionSource: source });
let chunkText = ''; let accumulated = '';
if (chunk) { if (typeof item === 'string') {
chunkText = typeof chunk === 'string' ? chunk : String(chunk); accumulated = item;
} else if (item && typeof item === 'object') {
accumulated = (typeof item.text === 'string' ? item.text : '') ||
(typeof item.content === 'string' ? item.content : '') || '';
} }
if (!accumulated && item && typeof item === 'object') {
const rc = item?.reasoning_content || item?.reasoning;
if (typeof rc === 'string') accumulated = rc;
}
if (!accumulated) continue;
// content 为空时回退到 reasoning_content if (accumulated.startsWith(last)) {
if (!chunkText) { last = accumulated;
const delta = parsed?.choices?.[0]?.delta; } else {
const rc = delta?.reasoning_content ?? parsed?.reasoning_content; last += accumulated;
if (rc) {
chunkText = typeof rc === 'string' ? rc : String(rc);
}
}
if (chunkText) {
text += chunkText;
yield text;
} }
yield last;
} }
} catch (err) { } catch (err) {
if (err?.name !== 'AbortError') { if (err?.name === 'AbortError') return;
console.error('[StreamingGeneration] Stream error:', err); console.error('[StreamingGeneration] Stream error:', err);
try { xbLog.error('streamingGeneration', 'Stream error', err); } catch {} try { xbLog.error('streamingGeneration', 'Stream error', err); } catch {}
throw err; throw err;
}
} finally {
try { reader.releaseLock?.(); } catch {}
} }
})(); })();
} else { } else {
// 非流式extract=true返回抽取后的结果
const payload = ChatCompletionService.createRequestData(body); const payload = ChatCompletionService.createRequestData(body);
const json = await ChatCompletionService.sendRequest(payload, false, abortSignal); const extracted = await ChatCompletionService.sendRequest(payload, true, abortSignal);
let result = String(extractMessageFromData(json, ChatCompletionService.TYPE) || '');
// content 为空时回退到 reasoning_content let result = String((extracted && extracted.content) || '');
if (!result) {
const msg = json?.choices?.[0]?.message; // reasoning_content 兜底
const rc = msg?.reasoning_content ?? json?.reasoning_content; if (!result && extracted && typeof extracted === 'object') {
if (rc) { const rc = extracted?.reasoning_content || extracted?.reasoning;
result = typeof rc === 'string' ? rc : String(rc); if (typeof rc === 'string') result = rc;
}
} }
return result; return result;

View File

@@ -1,62 +1,62 @@
<div id="xiaobai_template_editor"> <div id="xiaobai_template_editor">
<div class="xiaobai_template_editor"> <div class="xiaobai_template_editor">
<h3 class="flex-container justifyCenter alignItemsBaseline"> <h3 class="flex-container justifyCenter alignItemsBaseline">
<strong>模板编辑器</strong> <strong>模板编辑器</strong>
</h3> </h3>
<hr /> <hr />
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
<div class="flex1"> <div class="flex1">
<label for="fixed_text_custom_regex" class="title_restorable"> <label for="fixed_text_custom_regex" class="title_restorable">
<small>自定义正则表达式</small> <small>自定义正则表达式</small>
</label> </label>
<div> <div>
<input id="fixed_text_custom_regex" class="text_pole textarea_compact" type="text" <input id="fixed_text_custom_regex" class="text_pole textarea_compact" type="text"
placeholder="\\[([^\\]]+)\\]([\\s\\S]*?)\\[\\/\\1\\]" /> placeholder="\\[([^\\]]+)\\]([\\s\\S]*?)\\[\\/\\1\\]" />
</div> </div>
<div class="flex-container" style="margin-top: 6px;"> <div class="flex-container" style="margin-top: 6px;">
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="disable_parsers" /> <input type="checkbox" id="disable_parsers" />
<span>文本不使用插件预设的正则及格式解析器</span> <span>文本不使用插件预设的正则及格式解析器</span>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<hr /> <hr />
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
<div class="flex1"> <div class="flex1">
<label class="title_restorable"> <label class="title_restorable">
<small>消息范围限制</small> <small>消息范围限制</small>
</label> </label>
<div class="flex-container" style="margin-top: 10px;"> <div class="flex-container" style="margin-top: 10px;">
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="skip_first_message" /> <input type="checkbox" id="skip_first_message" />
<span>首条消息不插入模板</span> <span>首条消息不插入模板</span>
</label> </label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="limit_to_recent_messages" /> <input type="checkbox" id="limit_to_recent_messages" />
<span>仅在最后几条消息中生效</span> <span>仅在最后几条消息中生效</span>
</label> </label>
</div> </div>
<div class="flex-container" style="margin-top: 10px;"> <div class="flex-container" style="margin-top: 10px;">
<label for="recent_message_count" style="margin-right: 10px;">消息数量:</label> <label for="recent_message_count" style="margin-right: 10px;">消息数量:</label>
<input id="recent_message_count" class="text_pole" type="number" min="1" max="50" value="5" <input id="recent_message_count" class="text_pole" type="number" min="1" max="50" value="5"
style="width: 80px; max-height: 2.3vh;" /> style="width: 80px; max-height: 2.3vh;" />
</div> </div>
</div> </div>
</div> </div>
<hr /> <hr />
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
<label for="fixed_text_template" class="title_restorable"> <label for="fixed_text_template" class="title_restorable">
<small>模板内容</small> <small>模板内容</small>
</label> </label>
<div> <div>
<textarea id="fixed_text_template" class="text_pole textarea_compact" rows="3" <textarea id="fixed_text_template" class="text_pole textarea_compact" rows="3"
placeholder="例如hi[[var1]], hello[[var2]], xx[[profile1]]" style="min-height: 20vh;"></textarea> placeholder="例如hi[[var1]], hello[[var2]], xx[[profile1]]" style="min-height: 20vh;"></textarea>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -368,11 +368,19 @@ function installWIHiddenTagStripper() {
if (evtTypes?.GENERATION_ENDED) { if (evtTypes?.GENERATION_ENDED) {
events?.on(evtTypes.GENERATION_ENDED, async () => { events?.on(evtTypes.GENERATION_ENDED, async () => {
try { try {
getContext()?.setExtensionPrompt?.(LWB_VAREVENT_PROMPT_KEY, '', 0, 0, false); getContext()?.setExtensionPrompt?.(LWB_VAREVENT_PROMPT_KEY, '', 0, 0, false);
await executeQueuedVareventJsAfterTurn(); const ctx = getContext();
const chat = ctx?.chat || [];
const lastMsg = chat[chat.length - 1];
if (lastMsg && !lastMsg.is_user) {
await executeQueuedVareventJsAfterTurn();
} else {
drainPendingVareventBlocks();
}
} catch {} } catch {}
}); });
} }
if (evtTypes?.CHAT_CHANGED) { if (evtTypes?.CHAT_CHANGED) {
events?.on(evtTypes.CHAT_CHANGED, () => { events?.on(evtTypes.CHAT_CHANGED, () => {
try { try {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,264 +1,264 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&family=ZCOOL+XiaoWei&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&family=ZCOOL+XiaoWei&display=swap" rel="stylesheet">
<div class="inline-drawer"> <div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header"> <div class="inline-drawer-toggle inline-drawer-header">
<b>小白X</b> <b>小白X</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<div class="littlewhitebox settings-grid"> <div class="littlewhitebox settings-grid">
<div class="settings-menu-vertical"> <div class="settings-menu-vertical">
<div class="menu-tab active" data-target="js-memory" style="border-bottom:1px solid #303030;"><span class="vertical-text">渲染交互</span></div> <div class="menu-tab active" data-target="js-memory" style="border-bottom:1px solid #303030;"><span class="vertical-text">渲染交互</span></div>
<div class="menu-tab" data-target="task" style="border-bottom:1px solid #303030;"><span class="vertical-text">循环任务</span></div> <div class="menu-tab" data-target="task" style="border-bottom:1px solid #303030;"><span class="vertical-text">循环任务</span></div>
<div class="menu-tab" data-target="template" style="border-bottom:1px solid #303030;"><span class="vertical-text">数据互动</span></div> <div class="menu-tab" data-target="template" style="border-bottom:1px solid #303030;"><span class="vertical-text">数据互动</span></div>
<div class="menu-tab" data-target="wallhaven" style="border-bottom:1px solid #303030;"><span class="vertical-text">辅助工具</span></div> <div class="menu-tab" data-target="wallhaven" style="border-bottom:1px solid #303030;"><span class="vertical-text">辅助工具</span></div>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="js-memory settings-section" style="display:block;"> <div class="js-memory settings-section" style="display:block;">
<div class="section-divider" style="margin-bottom:0;margin-top:0;">总开关</div> <div class="section-divider" style="margin-bottom:0;margin-top:0;">总开关</div>
<hr class="sysHR" /> <hr class="sysHR" />
<div class="flex-container alignItemsCenter" style="gap:8px;flex-wrap:wrap;"> <div class="flex-container alignItemsCenter" style="gap:8px;flex-wrap:wrap;">
<input type="checkbox" id="xiaobaix_enabled" /> <input type="checkbox" id="xiaobaix_enabled" />
<label for="xiaobaix_enabled" class="has-tooltip" data-tooltip="渲染被```包裹的html、!DOCTYPE、script的代码块内容为交互式界面 <label for="xiaobaix_enabled" class="has-tooltip" data-tooltip="渲染被```包裹的html、!DOCTYPE、script的代码块内容为交互式界面
提供STscript(command)异步函数执行酒馆命令: 提供STscript(command)异步函数执行酒馆命令:
await STscript('/echo 你好世界!')">启用小白X</label> await STscript('/echo 你好世界!')">启用小白X</label>
</div> </div>
<div class="flex-container alignItemsCenter" style="gap:8px;flex-wrap:wrap;"> <div class="flex-container alignItemsCenter" style="gap:8px;flex-wrap:wrap;">
<input type="checkbox" id="xiaobaix_render_enabled" /> <input type="checkbox" id="xiaobaix_render_enabled" />
<label for="xiaobaix_render_enabled" class="has-tooltip" data-tooltip="控制代码块转换为iframe渲染的功能 <label for="xiaobaix_render_enabled" class="has-tooltip" data-tooltip="控制代码块转换为iframe渲染的功能
关闭后将清理所有已渲染的iframe">渲染开关</label> 关闭后将清理所有已渲染的iframe">渲染开关</label>
<label for="xiaobaix_max_rendered" style="margin-left:8px;">渲染楼层</label> <label for="xiaobaix_max_rendered" style="margin-left:8px;">渲染楼层</label>
<input id="xiaobaix_max_rendered" <input id="xiaobaix_max_rendered"
type="number" type="number"
class="text_pole dark-number-input" class="text_pole dark-number-input"
min="1" max="9999" step="1" min="1" max="9999" step="1"
style="width:5rem;margin-left:4px;" /> style="width:5rem;margin-left:4px;" />
</div> </div>
<div class="section-divider">渲染模式 <div class="section-divider">渲染模式
<hr class="sysHR" /> <hr class="sysHR" />
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_sandbox" /> <input type="checkbox" id="xiaobaix_sandbox" />
<label for="xiaobaix_sandbox">沙盒模式</label> <label for="xiaobaix_sandbox">沙盒模式</label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_use_blob" /> <input type="checkbox" id="xiaobaix_use_blob" />
<label for="xiaobaix_use_blob" class="has-tooltip" data-tooltip="大型html适用">启用Blob渲染</label> <label for="xiaobaix_use_blob" class="has-tooltip" data-tooltip="大型html适用">启用Blob渲染</label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="Wrapperiframe" /> <input type="checkbox" id="Wrapperiframe" />
<label for="Wrapperiframe" class="has-tooltip" data-tooltip="按需在 iframe 中注入 Wrapperiframe.js">启用封装函数</label> <label for="Wrapperiframe" class="has-tooltip" data-tooltip="按需在 iframe 中注入 Wrapperiframe.js">启用封装函数</label>
</div> </div>
<div class="flex-container alignItemsCenter" style="gap:8px;flex-wrap:wrap;"> <div class="flex-container alignItemsCenter" style="gap:8px;flex-wrap:wrap;">
<input type="checkbox" id="xiaobaix_audio_enabled" /> <input type="checkbox" id="xiaobaix_audio_enabled" />
<label for="xiaobaix_audio_enabled" style="margin-top:0;">启用音频</label> <label for="xiaobaix_audio_enabled" style="margin-top:0;">启用音频</label>
</div> </div>
<br> <br>
<div class="section-divider">流式,非基础的渲染 <div class="section-divider">流式,非基础的渲染
<hr class="sysHR" /> <hr class="sysHR" />
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_template_enabled" /> <input type="checkbox" id="xiaobaix_template_enabled" />
<label for="xiaobaix_template_enabled" class="has-tooltip" data-tooltip="流式多楼层动态渲染">启用沉浸式模板</label> <label for="xiaobaix_template_enabled" class="has-tooltip" data-tooltip="流式多楼层动态渲染">启用沉浸式模板</label>
</div> </div>
<div id="current_template_settings"> <div id="current_template_settings">
<div class="template-replacer-header"> <div class="template-replacer-header">
<div class="template-replacer-title">当前角色模板设置</div> <div class="template-replacer-title">当前角色模板设置</div>
<div class="template-replacer-controls"> <div class="template-replacer-controls">
<button id="open_template_editor" class="menu_button menu_button_icon"> <button id="open_template_editor" class="menu_button menu_button_icon">
<i class="fa-solid fa-pen-to-square"></i> <i class="fa-solid fa-pen-to-square"></i>
<small>编辑模板</small> <small>编辑模板</small>
</button> </button>
</div> </div>
</div> </div>
<div class="template-replacer-status" id="template_character_status"> <div class="template-replacer-status" id="template_character_status">
请选择一个角色 请选择一个角色
</div> </div>
</div> </div>
<div class="section-divider">功能说明 <div class="section-divider">功能说明
<hr class="sysHR" /> <hr class="sysHR" />
</div> </div>
<div class="flex-container alignItemsCenter" style="gap:8px;flex-wrap:wrap;"> <div class="flex-container alignItemsCenter" style="gap:8px;flex-wrap:wrap;">
<div><a href="https://docs.littlewhitebox.qzz.io/" class="download-link" target="_blank">功能文档</a></div> <div><a href="https://docs.littlewhitebox.qzz.io/" class="download-link" target="_blank">功能文档</a></div>
<button id="xiaobaix_reset_btn" class="menu_button menu_button_icon" type="button" title="把小白X的功能按钮重置回默认功能设定"> <button id="xiaobaix_reset_btn" class="menu_button menu_button_icon" type="button" title="把小白X的功能按钮重置回默认功能设定">
<small>默认开关</small> <small>默认开关</small>
</button> </button>
<button id="xiaobaix_xposition_btn" class="menu_button menu_button_icon" type="button" title="切换X按钮的位置仅两种"> <button id="xiaobaix_xposition_btn" class="menu_button menu_button_icon" type="button" title="切换X按钮的位置仅两种">
<small>X按钮:右</small> <small>X按钮:右</small>
</button> </button>
</div> </div>
</div> </div>
<div class="wallhaven settings-section" style="display:none;"> <div class="wallhaven settings-section" style="display:none;">
<div class="section-divider">消息日志与拦截 <div class="section-divider">消息日志与拦截
<hr class="sysHR"> <hr class="sysHR">
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_recorded_enabled" /> <input type="checkbox" id="xiaobaix_recorded_enabled" />
<label for="xiaobaix_recorded_enabled" class="has-tooltip" data-tooltip="每条消息添加图标点击可看到发送给时AI的记录">Log记录</label> <label for="xiaobaix_recorded_enabled" class="has-tooltip" data-tooltip="每条消息添加图标点击可看到发送给时AI的记录">Log记录</label>
<input type="checkbox" id="xiaobaix_preview_enabled" /> <input type="checkbox" id="xiaobaix_preview_enabled" />
<label for="xiaobaix_preview_enabled" class="has-tooltip" data-tooltip="在聊天框显示图标点击可拦截将发送给AI的消息并显示">Log拦截</label> <label for="xiaobaix_preview_enabled" class="has-tooltip" data-tooltip="在聊天框显示图标点击可拦截将发送给AI的消息并显示">Log拦截</label>
</div> </div>
<div class="section-divider">写卡AI <div class="section-divider">写卡AI
<hr class="sysHR" /> <hr class="sysHR" />
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_script_assistant" /> <input type="checkbox" id="xiaobaix_script_assistant" />
<label for="xiaobaix_script_assistant" class="has-tooltip" data-tooltip="勾选后AI将获取小白X功能和ST脚本语言知识内置 STscript 语法与示例,帮助您创作角色卡">启用写卡助手</label> <label for="xiaobaix_script_assistant" class="has-tooltip" data-tooltip="勾选后AI将获取小白X功能和ST脚本语言知识内置 STscript 语法与示例,帮助您创作角色卡">启用写卡助手</label>
</div> </div>
<div class="section-divider">视觉增强 <div class="section-divider">视觉增强
<hr class="sysHR" /> <hr class="sysHR" />
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_immersive_enabled" /> <input type="checkbox" id="xiaobaix_immersive_enabled" />
<label for="xiaobaix_immersive_enabled" class="has-tooltip" data-tooltip="重构界面布局与细节优化">沉浸布局显示(边框窄化)</label> <label for="xiaobaix_immersive_enabled" class="has-tooltip" data-tooltip="重构界面布局与细节优化">沉浸布局显示(边框窄化)</label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="wallhaven_enabled" /> <input type="checkbox" id="wallhaven_enabled" />
<label for="wallhaven_enabled" class="has-tooltip" data-tooltip="AI回复时自动提取消息内容转换为标签并获取Wallhaven图片作为聊天背景">自动消息配背景图</label> <label for="wallhaven_enabled" class="has-tooltip" data-tooltip="AI回复时自动提取消息内容转换为标签并获取Wallhaven图片作为聊天背景">自动消息配背景图</label>
</div> </div>
<div id="wallhaven_settings_container" style="display:none;"> <div id="wallhaven_settings_container" style="display:none;">
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="wallhaven_bg_mode" /> <input type="checkbox" id="wallhaven_bg_mode" />
<label for="wallhaven_bg_mode">背景图模式(纯场景)</label> <label for="wallhaven_bg_mode">背景图模式(纯场景)</label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<label for="wallhaven_category" id="section-font">图片分类:</label> <label for="wallhaven_category" id="section-font">图片分类:</label>
<select id="wallhaven_category" class="text_pole"> <select id="wallhaven_category" class="text_pole">
<option value="010">动漫漫画</option> <option value="010">动漫漫画</option>
<option value="111">全部类型</option> <option value="111">全部类型</option>
<option value="001">人物写真</option> <option value="001">人物写真</option>
<option value="100">综合壁纸</option> <option value="100">综合壁纸</option>
</select> </select>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<label for="wallhaven_purity" id="section-font">内容分级:</label> <label for="wallhaven_purity" id="section-font">内容分级:</label>
<select id="wallhaven_purity" class="text_pole"> <select id="wallhaven_purity" class="text_pole">
<option value="100">仅 SFW</option> <option value="100">仅 SFW</option>
<option value="010">仅 Sketchy (轻微)</option> <option value="010">仅 Sketchy (轻微)</option>
<option value="110">SFW + Sketchy</option> <option value="110">SFW + Sketchy</option>
<option value="001">仅 NSFW</option> <option value="001">仅 NSFW</option>
<option value="011">Sketchy + NSFW</option> <option value="011">Sketchy + NSFW</option>
<option value="111">全部内容</option> <option value="111">全部内容</option>
</select> </select>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<label for="wallhaven_opacity" id="section-font">黑纱透明度: <span id="wallhaven_opacity_value">30%</span></label> <label for="wallhaven_opacity" id="section-font">黑纱透明度: <span id="wallhaven_opacity_value">30%</span></label>
<input type="range" id="wallhaven_opacity" min="0" max="0.8" step="0.1" value="0.3" class="wide50p" /> <input type="range" id="wallhaven_opacity" min="0" max="0.8" step="0.1" value="0.3" class="wide50p" />
</div> </div>
<hr class="sysHR"> <hr class="sysHR">
<div class="flex-container"> <div class="flex-container">
<input type="text" id="wallhaven_custom_tag_input" placeholder="输入英文标签,如: beautiful girl" class="text_pole wide50p" /> <input type="text" id="wallhaven_custom_tag_input" placeholder="输入英文标签,如: beautiful girl" class="text_pole wide50p" />
<button id="wallhaven_add_custom_tag" class="menu_button" type="button" style="width:auto;">+自定义TAG</button> <button id="wallhaven_add_custom_tag" class="menu_button" type="button" style="width:auto;">+自定义TAG</button>
</div> </div>
<div id="wallhaven_custom_tags_container" class="custom-tags-container"> <div id="wallhaven_custom_tags_container" class="custom-tags-container">
<div id="wallhaven_custom_tags_list" class="custom-tags-list"></div> <div id="wallhaven_custom_tags_list" class="custom-tags-list"></div>
</div> </div>
</div> </div>
<div class="section-divider">Novel 画图 <div class="section-divider">Novel 画图
<hr class="sysHR" /> <hr class="sysHR" />
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_novel_draw_enabled" /> <input type="checkbox" id="xiaobaix_novel_draw_enabled" />
<label for="xiaobaix_novel_draw_enabled" class="has-tooltip" data-tooltip="使用 NovelAI 为 AI 回复自动或手动生成配图,需在酒馆设置中配置 NovelAI Key">启用 Novel 画图(暂不可用)</label> <label for="xiaobaix_novel_draw_enabled" class="has-tooltip" data-tooltip="使用 NovelAI 为 AI 回复自动或手动生成配图,需在酒馆设置中配置 NovelAI Key">启用 Novel 画图(暂不可用)</label>
<button id="xiaobaix_novel_draw_open_settings" class="menu_button menu_button_icon" type="button" style="margin-left:auto;" title="打开 Novel 画图详细设置"> <button id="xiaobaix_novel_draw_open_settings" class="menu_button menu_button_icon" type="button" style="margin-left:auto;" title="打开 Novel 画图详细设置">
<i class="fa-solid fa-palette"></i> <i class="fa-solid fa-palette"></i>
<small>画图设置</small> <small>画图设置</small>
</button> </button>
</div> </div>
</div> </div>
<div class="task settings-section" style="display:none;"> <div class="task settings-section" style="display:none;">
<div class="section-divider">循环任务 <div class="section-divider">循环任务
<hr class="sysHR" /> <hr class="sysHR" />
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="scheduled_tasks_enabled" /> <input type="checkbox" id="scheduled_tasks_enabled" />
<label for="scheduled_tasks_enabled" class="has-tooltip" data-tooltip="自动执行设定好的斜杠命令 <label for="scheduled_tasks_enabled" class="has-tooltip" data-tooltip="自动执行设定好的斜杠命令
输入/xbqte {{任务名称}}可以手动激活任务 输入/xbqte {{任务名称}}可以手动激活任务
导出/入角色卡时, 角色任务会随角色卡一起导出/入">启用循环任务</label> 导出/入角色卡时, 角色任务会随角色卡一起导出/入">启用循环任务</label>
<div id="toggle_task_bar" class="menu_button menu_button_icon" style="margin: 0px; margin-left: auto;" title="显示/隐藏按钮栏"> <div id="toggle_task_bar" class="menu_button menu_button_icon" style="margin: 0px; margin-left: auto;" title="显示/隐藏按钮栏">
<small>按钮栏</small> <small>按钮栏</small>
</div> </div>
</div> </div>
<hr class="sysHR" /> <hr class="sysHR" />
<div class="flex-container task-tab-bar"> <div class="flex-container task-tab-bar">
<div class="task-tab active" data-target="global_tasks_block">全局任务<span class="task-count" id="global_task_count"></span></div> <div class="task-tab active" data-target="global_tasks_block">全局任务<span class="task-count" id="global_task_count"></span></div>
<div class="task-tab" data-target="character_tasks_block">角色任务<span class="task-count" id="character_task_count"></span></div> <div class="task-tab" data-target="character_tasks_block">角色任务<span class="task-count" id="character_task_count"></span></div>
<div class="task-tab" data-target="preset_tasks_block">预设任务<span class="task-count" id="preset_task_count"></span></div> <div class="task-tab" data-target="preset_tasks_block">预设任务<span class="task-count" id="preset_task_count"></span></div>
</div> </div>
<div class="flex-container" style="justify-content: flex-end; flex-wrap: nowrap; gap: 0px; margin-top: 10px;"> <div class="flex-container" style="justify-content: flex-end; flex-wrap: nowrap; gap: 0px; margin-top: 10px;">
<div id="add_global_task" class="menu_button menu_button_icon" title="添加全局任务"> <div id="add_global_task" class="menu_button menu_button_icon" title="添加全局任务">
<small>+全局</small> <small>+全局</small>
</div> </div>
<div id="add_character_task" class="menu_button menu_button_icon" title="添加角色任务"> <div id="add_character_task" class="menu_button menu_button_icon" title="添加角色任务">
<small>+角色</small> <small>+角色</small>
</div> </div>
<div id="add_preset_task" class="menu_button menu_button_icon" title="添加预设任务"> <div id="add_preset_task" class="menu_button menu_button_icon" title="添加预设任务">
<small>+预设</small> <small>+预设</small>
</div> </div>
<div id="cloud_tasks_button" class="menu_button menu_button_icon" title="从云端获取任务脚本"> <div id="cloud_tasks_button" class="menu_button menu_button_icon" title="从云端获取任务脚本">
<i class="fa-solid fa-cloud-arrow-down"></i> <i class="fa-solid fa-cloud-arrow-down"></i>
<small>任务下载</small> <small>任务下载</small>
</div> </div>
<div id="import_global_tasks" class="menu_button menu_button_icon" title="导入任务"> <div id="import_global_tasks" class="menu_button menu_button_icon" title="导入任务">
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
<small>导入</small> <small>导入</small>
</div> </div>
</div> </div>
<hr class="sysHR"> <hr class="sysHR">
<div class="task-panel-group"> <div class="task-panel-group">
<div id="global_tasks_block" class="padding5 task-panel" data-panel="global_tasks_block"> <div id="global_tasks_block" class="padding5 task-panel" data-panel="global_tasks_block">
<small>这些任务在所有角色中的聊天都会执行</small> <small>这些任务在所有角色中的聊天都会执行</small>
<div id="global_tasks_list" class="flex-container task-container flexFlowColumn"></div> <div id="global_tasks_list" class="flex-container task-container flexFlowColumn"></div>
</div> </div>
<div id="character_tasks_block" class="padding5 task-panel" data-panel="character_tasks_block" style="display:none;"> <div id="character_tasks_block" class="padding5 task-panel" data-panel="character_tasks_block" style="display:none;">
<small>这些任务只在当前角色的聊天中执行</small> <small>这些任务只在当前角色的聊天中执行</small>
<div id="character_tasks_list" class="flex-container task-container flexFlowColumn"></div> <div id="character_tasks_list" class="flex-container task-container flexFlowColumn"></div>
</div> </div>
<div id="preset_tasks_block" class="padding5 task-panel" data-panel="preset_tasks_block" style="display:none;"> <div id="preset_tasks_block" class="padding5 task-panel" data-panel="preset_tasks_block" style="display:none;">
<small>这些任务会在使用<small id="preset_tasks_hint" class="preset-task-hint">未选择</small>预设时执行</small> <small>这些任务会在使用<small id="preset_tasks_hint" class="preset-task-hint">未选择</small>预设时执行</small>
<div id="preset_tasks_list" class="flex-container task-container flexFlowColumn"></div> <div id="preset_tasks_list" class="flex-container task-container flexFlowColumn"></div>
</div> </div>
</div> </div>
<input type="file" id="import_tasks_file" accept=".json" style="display:none;" /> <input type="file" id="import_tasks_file" accept=".json" style="display:none;" />
</div> </div>
<div class="template settings-section" style="display:none;"> <div class="template settings-section" style="display:none;">
<div class="section-divider">四次元壁</div> <div class="section-divider">四次元壁</div>
<hr class="sysHR" /> <hr class="sysHR" />
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_fourth_wall_enabled" /> <input type="checkbox" id="xiaobaix_fourth_wall_enabled" />
<label for="xiaobaix_fourth_wall_enabled" class="has-tooltip" data-tooltip="突破第四面墙,与角色进行元对话交流。悬浮按钮位于页面右侧中间。">四次元壁</label> <label for="xiaobaix_fourth_wall_enabled" class="has-tooltip" data-tooltip="突破第四面墙,与角色进行元对话交流。悬浮按钮位于页面右侧中间。">四次元壁</label>
</div> </div>
<br> <br>
<div class="section-divider">剧情总结</div> <div class="section-divider">剧情管理</div>
<hr class="sysHR" /> <hr class="sysHR" />
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_story_summary_enabled" /> <input type="checkbox" id="xiaobaix_story_summary_enabled" />
<label for="xiaobaix_story_summary_enabled" class="has-tooltip" data-tooltip="在消息楼层添加总结按钮点击可打开剧情总结面板AI分析生成关键词云、时间线、人物关系、角色弧光">剧情总结面板</label> <label for="xiaobaix_story_summary_enabled" class="has-tooltip" data-tooltip="在消息楼层添加总结按钮点击可打开剧情总结面板AI分析生成关键词云、时间线、人物关系、角色弧光">剧情总结</label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_story_outline_enabled" /> <input type="checkbox" id="xiaobaix_story_outline_enabled" />
<label for="xiaobaix_story_outline_enabled" class="has-tooltip" data-tooltip="在X按钮区域添加地图图标点击可打开可视化剧情地图编辑器">剧情地图</label> <label for="xiaobaix_story_outline_enabled" class="has-tooltip" data-tooltip="在X按钮区域添加地图图标点击可打开可视化剧情地图编辑器">小白板</label>
</div> </div>
<br> <br>
<div class="section-divider">变量控制、世界书执行</div> <div class="section-divider">变量控制</div>
<hr class="sysHR" /> <hr class="sysHR" />
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_variables_core_enabled" /> <input type="checkbox" id="xiaobaix_variables_core_enabled" />
<label for="xiaobaix_variables_core_enabled">剧情管理</label> <label for="xiaobaix_variables_core_enabled">变量管理</label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_variables_panel_enabled" /> <input type="checkbox" id="xiaobaix_variables_panel_enabled" />
<label for="xiaobaix_variables_panel_enabled">变量面板</label> <label for="xiaobaix_variables_panel_enabled">变量面板</label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<style> <style>
.littlewhitebox, .littlewhitebox,
@@ -556,9 +556,9 @@
wrapperIframe: 'Wrapperiframe', wrapperIframe: 'Wrapperiframe',
renderEnabled: 'xiaobaix_render_enabled', renderEnabled: 'xiaobaix_render_enabled',
}; };
const DEFAULTS_ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary']; const DEFAULTS_ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary', 'recorded'];
const DEFAULTS_OFF = ['recorded', 'preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'fourthWall', 'storyOutline' ]; const DEFAULTS_OFF = ['preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'fourthWall', 'storyOutline', 'novelDraw'];
const MODULE_KEYS = ['templateEditor', 'tasks', 'fourthWall', 'variablesCore', 'recorded', 'preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'audio', 'storySummary', 'storyOutline']; const MODULE_KEYS = ['templateEditor', 'tasks', 'fourthWall', 'variablesCore', 'recorded', 'preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'audio', 'storySummary', 'storyOutline', 'novelDraw'];
function setModuleEnabled(key, enabled) { function setModuleEnabled(key, enabled) {
try { try {
if (!extension_settings[EXT_ID][key]) extension_settings[EXT_ID][key] = {}; if (!extension_settings[EXT_ID][key]) extension_settings[EXT_ID][key] = {};

942
style.css
View File

@@ -1,471 +1,471 @@
/* ==================== 基础工具样式 ==================== */ /* ==================== 基础工具样式 ==================== */
pre:has(+ .xiaobaix-iframe) { pre:has(+ .xiaobaix-iframe) {
display: none; display: none;
} }
/* ==================== 循环任务样式 ==================== */ /* ==================== 循环任务样式 ==================== */
.task-container { .task-container {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.task-container:empty::after { .task-container:empty::after {
content: "No tasks found"; content: "No tasks found";
font-size: 0.95em; font-size: 0.95em;
opacity: 0.7; opacity: 0.7;
display: block; display: block;
text-align: center; text-align: center;
} }
.scheduled-tasks-embedded-warning { .scheduled-tasks-embedded-warning {
padding: 15px; padding: 15px;
background: var(--SmartThemeBlurTintColor); background: var(--SmartThemeBlurTintColor);
border: 1px solid var(--SmartThemeBorderColor); border: 1px solid var(--SmartThemeBorderColor);
border-radius: 8px; border-radius: 8px;
margin: 10px 0; margin: 10px 0;
} }
.warning-note { .warning-note {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-top: 10px; margin-top: 10px;
padding: 8px; padding: 8px;
background: rgba(255, 193, 7, 0.1); background: rgba(255, 193, 7, 0.1);
border-left: 3px solid #ffc107; border-left: 3px solid #ffc107;
border-radius: 4px; border-radius: 4px;
} }
.task-item { .task-item {
align-items: center; align-items: center;
border: 1px solid var(--SmartThemeBorderColor); border: 1px solid var(--SmartThemeBorderColor);
border-radius: 10px; border-radius: 10px;
padding: 0 5px; padding: 0 5px;
margin-top: 1px; margin-top: 1px;
margin-bottom: 1px; margin-bottom: 1px;
} }
.task-item:has(.disable_task:checked) .task_name { .task-item:has(.disable_task:checked) .task_name {
text-decoration: line-through; text-decoration: line-through;
filter: grayscale(0.5); filter: grayscale(0.5);
} }
.task_name { .task_name {
font-weight: normal; font-weight: normal;
color: var(--SmartThemeEmColor); color: var(--SmartThemeEmColor);
font-size: 0.9em; font-size: 0.9em;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.drag-handle { .drag-handle {
cursor: grab; cursor: grab;
color: var(--SmartThemeQuoteColor); color: var(--SmartThemeQuoteColor);
margin-right: 8px; margin-right: 8px;
user-select: none; user-select: none;
} }
.drag-handle:active { .drag-handle:active {
cursor: grabbing; cursor: grabbing;
} }
.checkbox { .checkbox {
align-items: center; align-items: center;
} }
.task_editor { .task_editor {
width: 100%; width: 100%;
} }
.task_editor .flex-container { .task_editor .flex-container {
gap: 10px; gap: 10px;
} }
.task_editor textarea { .task_editor textarea {
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
} }
input.disable_task { input.disable_task {
display: none !important; display: none !important;
} }
.task-toggle-off { .task-toggle-off {
cursor: pointer; cursor: pointer;
opacity: 0.5; opacity: 0.5;
filter: grayscale(0.5); filter: grayscale(0.5);
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
} }
.task-toggle-off:hover { .task-toggle-off:hover {
opacity: 1; opacity: 1;
filter: none; filter: none;
} }
.task-toggle-on { .task-toggle-on {
cursor: pointer; cursor: pointer;
} }
.disable_task:checked~.task-toggle-off { .disable_task:checked~.task-toggle-off {
display: block; display: block;
} }
.disable_task:checked~.task-toggle-on { .disable_task:checked~.task-toggle-on {
display: none; display: none;
} }
.disable_task:not(:checked)~.task-toggle-off { .disable_task:not(:checked)~.task-toggle-off {
display: none; display: none;
} }
.disable_task:not(:checked)~.task-toggle-on { .disable_task:not(:checked)~.task-toggle-on {
display: block; display: block;
} }
/* ==================== 沉浸式显示模式样式 ==================== */ /* ==================== 沉浸式显示模式样式 ==================== */
body.immersive-mode #chat { body.immersive-mode #chat {
padding: 0 !important; padding: 0 !important;
border: 0px !important; border: 0px !important;
overflow-y: auto; overflow-y: auto;
margin: 0 0px 0px 4px !important; margin: 0 0px 0px 4px !important;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-gutter: auto; scrollbar-gutter: auto;
} }
.xiaobaix-top-group { .xiaobaix-top-group {
margin-top: 1em !important; margin-top: 1em !important;
} }
@media screen and (min-width: 1001px) { @media screen and (min-width: 1001px) {
body.immersive-mode #chat { body.immersive-mode #chat {
scrollbar-width: none; scrollbar-width: none;
-ms-overflow-style: none; -ms-overflow-style: none;
/* IE and Edge */ /* IE and Edge */
} }
body.immersive-mode #chat::-webkit-scrollbar { body.immersive-mode #chat::-webkit-scrollbar {
display: none; display: none;
} }
} }
body.immersive-mode .mesAvatarWrapper { body.immersive-mode .mesAvatarWrapper {
margin-top: 1em; margin-top: 1em;
padding-bottom: 0px; padding-bottom: 0px;
} }
body.immersive-mode .swipe_left, body.immersive-mode .swipe_left,
body.immersive-mode .swipeRightBlock { body.immersive-mode .swipeRightBlock {
display: none !important; display: none !important;
} }
body.immersive-mode .mes { body.immersive-mode .mes {
margin: 2% 0 0% 0 !important; margin: 2% 0 0% 0 !important;
} }
body.immersive-mode .ch_name { body.immersive-mode .ch_name {
padding-bottom: 5px; padding-bottom: 5px;
border-bottom: 0.5px dashed color-mix(in srgb, var(--SmartThemeEmColor) 30%, transparent); border-bottom: 0.5px dashed color-mix(in srgb, var(--SmartThemeEmColor) 30%, transparent);
} }
body.immersive-mode .mes_block { body.immersive-mode .mes_block {
padding-left: 0 !important; padding-left: 0 !important;
margin: 0 0 5px 0 !important; margin: 0 0 5px 0 !important;
} }
body.immersive-mode .mes_text { body.immersive-mode .mes_text {
padding: 0px !important; padding: 0px !important;
max-width: 100%; max-width: 100%;
width: 100%; width: 100%;
margin-top: 5px; margin-top: 5px;
} }
body.immersive-mode .mes { body.immersive-mode .mes {
width: 99%; width: 99%;
margin: 0 0.5%; margin: 0 0.5%;
padding: 0px !important; padding: 0px !important;
} }
body.immersive-mode .mes_buttons, body.immersive-mode .mes_buttons,
body.immersive-mode .mes_edit_buttons { body.immersive-mode .mes_edit_buttons {
position: absolute !important; position: absolute !important;
top: 0 !important; top: 0 !important;
right: 0 !important; right: 0 !important;
} }
body.immersive-mode .mes_buttons { body.immersive-mode .mes_buttons {
height: 20px; height: 20px;
overflow-x: clip; overflow-x: clip;
} }
body.immersive-mode .swipes-counter { body.immersive-mode .swipes-counter {
padding-left: 0px; padding-left: 0px;
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
body.immersive-mode .flex-container.flex1.alignitemscenter { body.immersive-mode .flex-container.flex1.alignitemscenter {
min-height: 32px; min-height: 32px;
} }
.immersive-navigation { .immersive-navigation {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-end; align-items: flex-end;
margin-top: 5px; margin-top: 5px;
opacity: 0.7; opacity: 0.7;
} }
.immersive-nav-btn { .immersive-nav-btn {
color: var(--SmartThemeBodyColor); color: var(--SmartThemeBodyColor);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
background: none; background: none;
border: none; border: none;
font-size: 12px; font-size: 12px;
} }
.immersive-nav-btn:hover:not(:disabled) { .immersive-nav-btn:hover:not(:disabled) {
background-color: rgba(var(--SmartThemeBodyColor), 0.2); background-color: rgba(var(--SmartThemeBodyColor), 0.2);
transform: scale(1.1); transform: scale(1.1);
} }
.immersive-nav-btn:disabled { .immersive-nav-btn:disabled {
opacity: 0.3; opacity: 0.3;
cursor: not-allowed; cursor: not-allowed;
} }
/* ==================== 模板编辑器样式 ==================== */ /* ==================== 模板编辑器样式 ==================== */
.xiaobai_template_editor { .xiaobai_template_editor {
max-height: 80vh; max-height: 80vh;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
} }
.template-replacer-header { .template-replacer-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 10px; margin-bottom: 10px;
} }
.template-replacer-title { .template-replacer-title {
font-weight: bold; font-weight: bold;
color: var(--SmartThemeEmColor, #007bff); color: var(--SmartThemeEmColor, #007bff);
} }
.template-replacer-controls { .template-replacer-controls {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 15px; gap: 15px;
} }
.template-replacer-status { .template-replacer-status {
font-size: 12px; font-size: 12px;
color: var(--SmartThemeQuoteColor, #888); color: var(--SmartThemeQuoteColor, #888);
font-style: italic; font-style: italic;
} }
.template-replacer-status.has-settings { .template-replacer-status.has-settings {
color: var(--SmartThemeEmColor, #007bff); color: var(--SmartThemeEmColor, #007bff);
} }
.template-replacer-status.no-character { .template-replacer-status.no-character {
color: var(--SmartThemeCheckboxBgColor, #666); color: var(--SmartThemeCheckboxBgColor, #666);
} }
/* ==================== 消息预览插件样式 ==================== */ /* ==================== 消息预览插件样式 ==================== */
#message_preview_btn { #message_preview_btn {
width: var(--bottomFormBlockSize); width: var(--bottomFormBlockSize);
height: var(--bottomFormBlockSize); height: var(--bottomFormBlockSize);
margin: 0; margin: 0;
border: none; border: none;
cursor: pointer; cursor: pointer;
opacity: 0.7; opacity: 0.7;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: opacity 300ms; transition: opacity 300ms;
color: var(--SmartThemeBodyColor); color: var(--SmartThemeBodyColor);
font-size: var(--bottomFormIconSize); font-size: var(--bottomFormIconSize);
} }
#message_preview_btn:hover { #message_preview_btn:hover {
opacity: 1; opacity: 1;
filter: brightness(1.2); filter: brightness(1.2);
} }
.message-preview-content-box { .message-preview-content-box {
font-family: 'Courier New', 'Monaco', 'Menlo', monospace; font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
white-space: pre-wrap; white-space: pre-wrap;
max-height: 82vh; max-height: 82vh;
overflow-y: auto; overflow-y: auto;
padding: 15px; padding: 15px;
background: #000000 !important; background: #000000 !important;
border: 1px solid var(--SmartThemeBorderColor); border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px; border-radius: 5px;
color: #ffffff !important; color: #ffffff !important;
font-size: 12px; font-size: 12px;
line-height: 1.4; line-height: 1.4;
text-align: left; text-align: left;
padding-bottom: 80px; padding-bottom: 80px;
} }
.mes_history_preview { .mes_history_preview {
opacity: 0.6; opacity: 0.6;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
} }
.mes_history_preview:hover { .mes_history_preview:hover {
opacity: 1; opacity: 1;
} }
/* ==================== 设置菜单和标签样式 ==================== */ /* ==================== 设置菜单和标签样式 ==================== */
.menu-tab { .menu-tab {
flex: 1; flex: 1;
padding: 2px 8px; padding: 2px 8px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
color: #ccc; color: #ccc;
border: none; border: none;
transition: color 0.2s ease; transition: color 0.2s ease;
font-weight: 500; font-weight: 500;
} }
.menu-tab:hover { .menu-tab:hover {
color: #fff; color: #fff;
} }
.menu-tab.active { .menu-tab.active {
color: #007acc; color: #007acc;
border-bottom: 2px solid #007acc; border-bottom: 2px solid #007acc;
} }
.settings-section { .settings-section {
padding: 10px 0; padding: 10px 0;
} }
/* ==================== Wallhaven自定义标签样式 ==================== */ /* ==================== Wallhaven自定义标签样式 ==================== */
.custom-tags-container { .custom-tags-container {
margin-top: 10px; margin-top: 10px;
} }
.custom-tags-list { .custom-tags-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
margin-top: 8px; margin-top: 8px;
min-height: 20px; min-height: 20px;
padding: 8px; padding: 8px;
background: #2a2a2a; background: #2a2a2a;
border-radius: 4px; border-radius: 4px;
border: 1px solid #444; border: 1px solid #444;
} }
.custom-tag-item { .custom-tag-item {
display: flex; display: flex;
align-items: center; align-items: center;
background: #007acc; background: #007acc;
color: white; color: white;
padding: 4px 8px; padding: 4px 8px;
border-radius: 12px; border-radius: 12px;
font-size: 12px; font-size: 12px;
gap: 6px; gap: 6px;
} }
.custom-tag-text { .custom-tag-text {
font-weight: 500; font-weight: 500;
} }
.custom-tag-remove { .custom-tag-remove {
cursor: pointer; cursor: pointer;
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
font-weight: bold; font-weight: bold;
transition: color 0.2s ease; transition: color 0.2s ease;
} }
.custom-tag-remove:hover { .custom-tag-remove:hover {
color: #ff6b6b; color: #ff6b6b;
} }
.custom-tags-empty { .custom-tags-empty {
color: #888; color: #888;
font-style: italic; font-style: italic;
font-size: 12px; font-size: 12px;
text-align: center; text-align: center;
padding: 8px; padding: 8px;
} }
.task_editor .menu_button{ .task_editor .menu_button{
white-space: nowrap; white-space: nowrap;
} }
.message-preview-content-box:hover::-webkit-scrollbar-thumb, .message-preview-content-box:hover::-webkit-scrollbar-thumb,
.xiaobai_template_editor:hover::-webkit-scrollbar-thumb { .xiaobai_template_editor:hover::-webkit-scrollbar-thumb {
background: var(--SmartThemeAccent); background: var(--SmartThemeAccent);
} }
/* ==================== 滚动条样式 ==================== */ /* ==================== 滚动条样式 ==================== */
.message-preview-content-box::-webkit-scrollbar, .message-preview-content-box::-webkit-scrollbar,
.xiaobai_template_editor::-webkit-scrollbar { .xiaobai_template_editor::-webkit-scrollbar {
width: 5px; width: 5px;
} }
.message-preview-content-box::-webkit-scrollbar-track, .message-preview-content-box::-webkit-scrollbar-track,
.xiaobai_template_editor::-webkit-scrollbar-track { .xiaobai_template_editor::-webkit-scrollbar-track {
background: var(--SmartThemeBlurTintColor); background: var(--SmartThemeBlurTintColor);
border-radius: 3px; border-radius: 3px;
} }
.message-preview-content-box::-webkit-scrollbar-thumb, .message-preview-content-box::-webkit-scrollbar-thumb,
.xiaobai_template_editor::-webkit-scrollbar-thumb { .xiaobai_template_editor::-webkit-scrollbar-thumb {
background: var(--SmartThemeBorderColor); background: var(--SmartThemeBorderColor);
border-radius: 3px; border-radius: 3px;
} }
/* ==================== Story Outline PromptManager 编辑表单 ==================== */ /* ==================== Story Outline PromptManager 编辑表单 ==================== */
/* 当编辑 lwb_story_outline 条目时,隐藏名称输入框和内容编辑区 */ /* 当编辑 lwb_story_outline 条目时,隐藏名称输入框和内容编辑区 */
.completion_prompt_manager_popup_entry_form:has([data-pm-prompt="lwb_story_outline"]) #completion_prompt_manager_popup_entry_form_name { .completion_prompt_manager_popup_entry_form:has([data-pm-prompt="lwb_story_outline"]) #completion_prompt_manager_popup_entry_form_name {
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
} }
.1completion_prompt_manager_popup_entry_form:has([data-pm-prompt="lwb_story_outline"]) #completion_prompt_manager_popup_entry_form_prompt { .1completion_prompt_manager_popup_entry_form:has([data-pm-prompt="lwb_story_outline"]) #completion_prompt_manager_popup_entry_form_prompt {
display: none !important; display: none !important;
} }
/* 显示"内容来自外部"的提示 */ /* 显示"内容来自外部"的提示 */
.1completion_prompt_manager_popup_entry_form:has([data-pm-prompt="lwb_story_outline"]) .completion_prompt_manager_popup_entry_form_control:has(#completion_prompt_manager_popup_entry_form_prompt)::after { .1completion_prompt_manager_popup_entry_form:has([data-pm-prompt="lwb_story_outline"]) .completion_prompt_manager_popup_entry_form_control:has(#completion_prompt_manager_popup_entry_form_prompt)::after {
content: "此提示词的内容来自「LittleWhiteBox」请在小白板中修改哦"; content: "此提示词的内容来自「LittleWhiteBox」请在小白板中修改哦";
display: block; display: block;
padding: 12px; padding: 12px;
margin-top: 8px; margin-top: 8px;
border: 1px solid var(--SmartThemeBorderColor); border: 1px solid var(--SmartThemeBorderColor);
color: var(--SmartThemeEmColor); color: var(--SmartThemeEmColor);
text-align: center; text-align: center;
} }
/* 隐藏 lwb_story_outline 条目的 Remove 按钮(保留占位) */ /* 隐藏 lwb_story_outline 条目的 Remove 按钮(保留占位) */
.completion_prompt_manager_prompt[data-pm-identifier="lwb_story_outline"] .prompt-manager-detach-action { .completion_prompt_manager_prompt[data-pm-identifier="lwb_story_outline"] .prompt-manager-detach-action {
visibility: hidden !important; visibility: hidden !important;
} }
.completion_prompt_manager_prompt[data-pm-identifier="lwb_story_outline"] .fa-fw.fa-solid.fa-asterisk { .completion_prompt_manager_prompt[data-pm-identifier="lwb_story_outline"] .fa-fw.fa-solid.fa-asterisk {
visibility: hidden !important; visibility: hidden !important;
position: relative; position: relative;
} }
.completion_prompt_manager_prompt[data-pm-identifier="lwb_story_outline"] .fa-fw.fa-solid.fa-asterisk::after { .completion_prompt_manager_prompt[data-pm-identifier="lwb_story_outline"] .fa-fw.fa-solid.fa-asterisk::after {
content: "\f00d"; content: "\f00d";
/* fa-xmark 的 unicode */ /* fa-xmark 的 unicode */
font-family: "Font Awesome 6 Free"; font-family: "Font Awesome 6 Free";
visibility: visible; visibility: visible;
position: absolute; position: absolute;
left: 0; left: 0;
font-size: 1.2em; font-size: 1.2em;
} }
#completion_prompt_manager_footer_append_prompt option[value="lwb_story_outline"] { #completion_prompt_manager_footer_append_prompt option[value="lwb_story_outline"] {
display: none; display: none;
} }