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
SillyTavern 扩展插件 - 小白X
## 📁 目录结构
```
LittleWhiteBox/
├── manifest.json # 插件配置清单
├── index.js # 主入口文件
├── settings.html # 设置页面模板
├── style.css # 全局样式
├── modules/ # 功能模块目录
│ ├── streaming-generation.js # 流式生成
│ ├── dynamic-prompt.js # 动态提示词
│ ├── immersive-mode.js # 沉浸模式
│ ├── message-preview.js # 消息预览
│ ├── wallhaven-background.js # 壁纸背景
│ ├── button-collapse.js # 按钮折叠
│ ├── control-audio.js # 音频控制
│ ├── script-assistant.js # 脚本助手
│ │
│ ├── variables/ # 变量系统
│ │ ├── variables-core.js
│ │ └── variables-panel.js
│ │
│ ├── template-editor/ # 模板编辑器
│ │ ├── template-editor.js
│ │ └── template-editor.html
│ │
│ ├── scheduled-tasks/ # 定时任务
│ │ ├── scheduled-tasks.js
│ │ ├── scheduled-tasks.html
│ │ └── embedded-tasks.html
│ │
│ ├── story-summary/ # 故事摘要
│ │ ├── story-summary.js
│ │ └── story-summary.html
│ │
│ └── story-outline/ # 故事大纲
│ ├── story-outline.js
│ ├── story-outline-prompt.js
│ └── story-outline.html
├── bridges/ # 外部桥接模块
│ ├── worldbook-bridge.js # 世界书桥接
│ ├── call-generate-service.js # 生成服务调用
│ └── wrapper-iframe.js # iframe 包装器
├── ui/ # UI 模板
│ └── character-updater-menus.html
└── docs/ # 文档
├── script-docs.md # 脚本文档
├── LICENSE.md # 许可证
├── COPYRIGHT # 版权信息
└── NOTICE # 声明
```
## 📄 许可证
详见 `docs/LICENSE.md`
# LittleWhiteBox
SillyTavern 扩展插件 - 小白X
## 📁 目录结构
```
LittleWhiteBox/
├── manifest.json # 插件配置清单
├── index.js # 主入口文件
├── settings.html # 设置页面模板
├── style.css # 全局样式
├── modules/ # 功能模块目录
│ ├── streaming-generation.js # 流式生成
│ ├── dynamic-prompt.js # 动态提示词
│ ├── immersive-mode.js # 沉浸模式
│ ├── message-preview.js # 消息预览
│ ├── wallhaven-background.js # 壁纸背景
│ ├── button-collapse.js # 按钮折叠
│ ├── control-audio.js # 音频控制
│ ├── script-assistant.js # 脚本助手
│ │
│ ├── variables/ # 变量系统
│ │ ├── variables-core.js
│ │ └── variables-panel.js
│ │
│ ├── template-editor/ # 模板编辑器
│ │ ├── template-editor.js
│ │ └── template-editor.html
│ │
│ ├── scheduled-tasks/ # 定时任务
│ │ ├── scheduled-tasks.js
│ │ ├── scheduled-tasks.html
│ │ └── embedded-tasks.html
│ │
│ ├── story-summary/ # 故事摘要
│ │ ├── story-summary.js
│ │ └── story-summary.html
│ │
│ └── story-outline/ # 故事大纲
│ ├── story-outline.js
│ ├── story-outline-prompt.js
│ └── story-outline.html
├── bridges/ # 外部桥接模块
│ ├── worldbook-bridge.js # 世界书桥接
│ ├── call-generate-service.js # 生成服务调用
│ └── wrapper-iframe.js # iframe 包装器
├── ui/ # UI 模板
│ └── character-updater-menus.html
└── docs/ # 文档
├── script-docs.md # 脚本文档
├── LICENSE.md # 许可证
├── COPYRIGHT # 版权信息
└── NOTICE # 声明
```
## 📝 模块组织规则
- **单文件模块**:直接放在 `modules/` 目录下
- **多文件模块**:创建子目录,包含相关的 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 defineCallGenerate(){
function sanitizeOptions(options){
try{
return JSON.parse(JSON.stringify(options,function(k,v){return(typeof v==='function')?undefined:v}))
}catch(_){
try{
const seen=new WeakSet();
const clone=(val)=>{
if(val===null||val===undefined)return val;
const t=typeof val;
if(t==='function')return undefined;
if(t!=='object')return val;
if(seen.has(val))return undefined;
seen.add(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 proto=Object.getPrototypeOf(val);
if(proto!==Object.prototype&&proto!==null)return undefined;
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}}
return out;
};
return clone(options);
}catch(__){return{}}
}
}
function CallGenerateImpl(options){
return new Promise(function(resolve,reject){
try{
function post(m){try{parent.postMessage(m,'*')}catch(e){}}
if(!options||typeof options!=='object'){reject(new Error('Invalid options'));return}
var id=Date.now().toString(36)+Math.random().toString(36).slice(2);
function onMessage(e){
var d=e&&e.data||{};
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(_){}}
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(_){}
resolve(d.result)}
else if(d.type==='generateStreamError'){try{window.removeEventListener('message',onMessage)}catch(_){}
reject(new Error(d.error||'Stream failed'))}
else if(d.type==='generateResult'){try{window.removeEventListener('message',onMessage)}catch(_){}
resolve(d.result)}
else if(d.type==='generateError'){try{window.removeEventListener('message',onMessage)}catch(_){}
reject(new Error(d.error||'Generation failed'))}
}
try{window.addEventListener('message',onMessage)}catch(_){}
var sanitized=sanitizeOptions(options);
post({type:'generateRequest',id:id,options:sanitized});
setTimeout(function(){try{window.removeEventListener('message',onMessage)}catch(e){};reject(new Error('Generation timeout'))},300000);
}catch(e){reject(e)}
})
}
try{window.CallGenerate=CallGenerateImpl}catch(e){}
try{window.callGenerate=CallGenerateImpl}catch(e){}
try{window.__xb_callGenerate_loaded=true}catch(e){}
}
try{defineCallGenerate()}catch(e){}
})();
(function(){
function applyAvatarCss(urls){
try{
const root=document.documentElement;
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');
if(!document.getElementById('xb-avatar-style')){
const css=`
.xb-avatar,.xb-user-avatar,.xb-char-avatar{
width:36px;height:36px;border-radius:50%;
background-size:cover;background-position:center;background-repeat:no-repeat;
display:inline-block
}
.xb-user-avatar{background-image:var(--xb-user-avatar)}
.xb-char-avatar{background-image:var(--xb-char-avatar)}
`;
const style=document.createElement('style');
style.id='xb-avatar-style';
style.textContent=css;
document.head.appendChild(style);
}
}catch(_){}
}
function requestAvatars(){
try{parent.postMessage({type:'getAvatars'},'*')}catch(_){}
}
function onMessage(e){
const d=e&&e.data||{};
if(d&&d.source==='xiaobaix-host'&&d.type==='avatars'){
applyAvatarCss(d.urls);
try{window.removeEventListener('message',onMessage)}catch(_){}
}
}
try{
window.addEventListener('message',onMessage);
if(document.readyState==='loading'){
document.addEventListener('DOMContentLoaded',requestAvatars,{once:true});
}else{
requestAvatars();
}
window.addEventListener('load',requestAvatars,{once:true});
}catch(_){}
(function(){
function defineCallGenerate(){
function sanitizeOptions(options){
try{
return JSON.parse(JSON.stringify(options,function(k,v){return(typeof v==='function')?undefined:v}))
}catch(_){
try{
const seen=new WeakSet();
const clone=(val)=>{
if(val===null||val===undefined)return val;
const t=typeof val;
if(t==='function')return undefined;
if(t!=='object')return val;
if(seen.has(val))return undefined;
seen.add(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 proto=Object.getPrototypeOf(val);
if(proto!==Object.prototype&&proto!==null)return undefined;
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}}
return out;
};
return clone(options);
}catch(__){return{}}
}
}
function CallGenerateImpl(options){
return new Promise(function(resolve,reject){
try{
function post(m){try{parent.postMessage(m,'*')}catch(e){}}
if(!options||typeof options!=='object'){reject(new Error('Invalid options'));return}
var id=Date.now().toString(36)+Math.random().toString(36).slice(2);
function onMessage(e){
var d=e&&e.data||{};
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(_){}}
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(_){}
resolve(d.result)}
else if(d.type==='generateStreamError'){try{window.removeEventListener('message',onMessage)}catch(_){}
reject(new Error(d.error||'Stream failed'))}
else if(d.type==='generateResult'){try{window.removeEventListener('message',onMessage)}catch(_){}
resolve(d.result)}
else if(d.type==='generateError'){try{window.removeEventListener('message',onMessage)}catch(_){}
reject(new Error(d.error||'Generation failed'))}
}
try{window.addEventListener('message',onMessage)}catch(_){}
var sanitized=sanitizeOptions(options);
post({type:'generateRequest',id:id,options:sanitized});
setTimeout(function(){try{window.removeEventListener('message',onMessage)}catch(e){};reject(new Error('Generation timeout'))},300000);
}catch(e){reject(e)}
})
}
try{window.CallGenerate=CallGenerateImpl}catch(e){}
try{window.callGenerate=CallGenerateImpl}catch(e){}
try{window.__xb_callGenerate_loaded=true}catch(e){}
}
try{defineCallGenerate()}catch(e){}
})();
(function(){
function applyAvatarCss(urls){
try{
const root=document.documentElement;
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');
if(!document.getElementById('xb-avatar-style')){
const css=`
.xb-avatar,.xb-user-avatar,.xb-char-avatar{
width:36px;height:36px;border-radius:50%;
background-size:cover;background-position:center;background-repeat:no-repeat;
display:inline-block
}
.xb-user-avatar{background-image:var(--xb-user-avatar)}
.xb-char-avatar{background-image:var(--xb-char-avatar)}
`;
const style=document.createElement('style');
style.id='xb-avatar-style';
style.textContent=css;
document.head.appendChild(style);
}
}catch(_){}
}
function requestAvatars(){
try{parent.postMessage({type:'getAvatars'},'*')}catch(_){}
}
function onMessage(e){
const d=e&&e.data||{};
if(d&&d.source==='xiaobaix-host'&&d.type==='avatars'){
applyAvatarCss(d.urls);
try{window.removeEventListener('message',onMessage)}catch(_){}
}
}
try{
window.addEventListener('message',onMessage);
if(document.readyState==='loading'){
document.addEventListener('DOMContentLoaded',requestAvatars,{once:true});
}else{
requestAvatars();
}
window.addEventListener('load',requestAvatars,{once:true});
}catch(_){}
})();

View File

@@ -1,7 +1,7 @@
/**
* LittleWhiteBox 共享常量
*/
export const EXT_ID = "LittleWhiteBox";
export const EXT_NAME = "小白X";
export const extensionFolderPath = `scripts/extensions/third-party/${EXT_ID}`;
/**
* LittleWhiteBox 共享常量
*/
export const EXT_ID = "LittleWhiteBox";
export const EXT_NAME = "小白X";
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";
/**
* 执行 SillyTavern 斜杠命令
* @param {string} command - 要执行的命令
* @returns {Promise<any>} 命令执行结果
*/
export async function executeSlashCommand(command) {
try {
if (!command) return { error: "命令为空" };
if (!command.startsWith('/')) command = '/' + command;
const { executeSlashCommands, substituteParams } = getContext();
if (typeof executeSlashCommands !== 'function') throw new Error("executeSlashCommands 函数不可用");
command = substituteParams(command);
const result = await executeSlashCommands(command, true);
if (result && typeof result === 'object' && result.pipe !== undefined) {
const pipeValue = result.pipe;
if (typeof pipeValue === 'string') {
try { return JSON.parse(pipeValue); } catch { return pipeValue; }
}
return pipeValue;
}
if (typeof result === 'string' && result.trim()) {
try { return JSON.parse(result); } catch { return result; }
}
return result === undefined ? "" : result;
} catch (err) {
throw err;
}
}
import { getContext } from "../../../../extensions.js";
/**
* 执行 SillyTavern 斜杠命令
* @param {string} command - 要执行的命令
* @returns {Promise<any>} 命令执行结果
*/
export async function executeSlashCommand(command) {
try {
if (!command) return { error: "命令为空" };
if (!command.startsWith('/')) command = '/' + command;
const { executeSlashCommands, substituteParams } = getContext();
if (typeof executeSlashCommands !== 'function') throw new Error("executeSlashCommands 函数不可用");
command = substituteParams(command);
const result = await executeSlashCommands(command, true);
if (result && typeof result === 'object' && result.pipe !== undefined) {
const pipeValue = result.pipe;
if (typeof pipeValue === 'string') {
try { return JSON.parse(pipeValue); } catch { return pipeValue; }
}
return pipeValue;
}
if (typeof result === 'string' && result.trim()) {
try { return JSON.parse(result); } catch { return result; }
}
return result === undefined ? "" : result;
} catch (err) {
throw err;
}
}

View File

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

View File

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

View File

@@ -1,95 +1,95 @@
LittleWhiteBox (小白X) - Third-Party Notices and Attributions
================================================================
This software contains code and dependencies from various third-party sources.
The following notices and attributions are required by their respective licenses.
PRIMARY SOFTWARE
================
LittleWhiteBox (小白X)
Copyright 2025 biex
Licensed under Apache-2.0 WITH Custom-Attribution-Requirements
Repository: https://github.com/RT15548/LittleWhiteBox
RUNTIME DEPENDENCIES
====================
This extension is designed to work with SillyTavern and relies on the following
SillyTavern modules and APIs:
1. SillyTavern Core Framework
- Copyright: SillyTavern Contributors
- License: AGPL-3.0
- Repository: https://github.com/SillyTavern/SillyTavern
2. SillyTavern Extensions API
- Used modules: extensions.js, script.js
- Provides: Extension framework, settings management, event system
3. SillyTavern Slash Commands
- Used modules: slash-commands.js, SlashCommandParser.js
- Provides: Command execution framework
4. SillyTavern UI Components
- Used modules: popup.js, utils.js
- Provides: User interface components and utilities
BROWSER APIS AND STANDARDS
==========================
This software uses standard web browser APIs:
- DOM API (Document Object Model)
- Fetch API for HTTP requests
- PostMessage API for iframe communication
- Local Storage API for data persistence
- Mutation Observer API for DOM monitoring
JAVASCRIPT LIBRARIES
====================
The software may interact with the following JavaScript libraries
that are part of the SillyTavern environment:
1. jQuery
- Copyright: jQuery Foundation and contributors
- License: MIT License
- Used for: DOM manipulation and event handling
2. Toastr (if available)
- Copyright: CodeSeven
- License: MIT License
- Used for: Notification display
DEVELOPMENT TOOLS
=================
The following tools were used in development (not distributed):
- Visual Studio Code
- Git version control
- Various Node.js development tools
ATTRIBUTION REQUIREMENTS
========================
When distributing this software or derivative works, you must:
1. Include this NOTICE file
2. Maintain all copyright notices in source code
3. Provide attribution to the original author "biex"
4. Include a link to the original repository
5. Comply with Apache-2.0 license requirements
6. Follow the custom attribution requirements in LICENSE.md
DISCLAIMER
==========
This software is provided "AS IS" without warranty of any kind.
The author disclaims all warranties, express or implied, including
but not limited to the warranties of merchantability, fitness for
a particular purpose, and non-infringement.
For complete license terms, see LICENSE.md
For attribution requirements, see COPYRIGHT
Last updated: 2025-01-14
LittleWhiteBox (小白X) - Third-Party Notices and Attributions
================================================================
This software contains code and dependencies from various third-party sources.
The following notices and attributions are required by their respective licenses.
PRIMARY SOFTWARE
================
LittleWhiteBox (小白X)
Copyright 2025 biex
Licensed under Apache-2.0 WITH Custom-Attribution-Requirements
Repository: https://github.com/RT15548/LittleWhiteBox
RUNTIME DEPENDENCIES
====================
This extension is designed to work with SillyTavern and relies on the following
SillyTavern modules and APIs:
1. SillyTavern Core Framework
- Copyright: SillyTavern Contributors
- License: AGPL-3.0
- Repository: https://github.com/SillyTavern/SillyTavern
2. SillyTavern Extensions API
- Used modules: extensions.js, script.js
- Provides: Extension framework, settings management, event system
3. SillyTavern Slash Commands
- Used modules: slash-commands.js, SlashCommandParser.js
- Provides: Command execution framework
4. SillyTavern UI Components
- Used modules: popup.js, utils.js
- Provides: User interface components and utilities
BROWSER APIS AND STANDARDS
==========================
This software uses standard web browser APIs:
- DOM API (Document Object Model)
- Fetch API for HTTP requests
- PostMessage API for iframe communication
- Local Storage API for data persistence
- Mutation Observer API for DOM monitoring
JAVASCRIPT LIBRARIES
====================
The software may interact with the following JavaScript libraries
that are part of the SillyTavern environment:
1. jQuery
- Copyright: jQuery Foundation and contributors
- License: MIT License
- Used for: DOM manipulation and event handling
2. Toastr (if available)
- Copyright: CodeSeven
- License: MIT License
- Used for: Notification display
DEVELOPMENT TOOLS
=================
The following tools were used in development (not distributed):
- Visual Studio Code
- Git version control
- Various Node.js development tools
ATTRIBUTION REQUIREMENTS
========================
When distributing this software or derivative works, you must:
1. Include this NOTICE file
2. Maintain all copyright notices in source code
3. Provide attribution to the original author "biex"
4. Include a link to the original repository
5. Comply with Apache-2.0 license requirements
6. Follow the custom attribution requirements in LICENSE.md
DISCLAIMER
==========
This software is provided "AS IS" without warranty of any kind.
The author disclaims all warranties, express or implied, including
but not limited to the warranties of merchantability, fitness for
a particular purpose, and non-infringement.
For complete license terms, see LICENSE.md
For attribution requirements, see COPYRIGHT
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 { 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-outline/story-outline.js";
// ═══════════════════════════════════════════════════════════════════════════
// 常量与默认设置
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Constants and Default Settings
// ===========================================================================
const MODULE_NAME = "xiaobaix-memory";
@@ -67,9 +67,9 @@ extension_settings[EXT_ID] = extension_settings[EXT_ID] || {
const settings = extension_settings[EXT_ID];
if (settings.dynamicPrompt && !settings.fourthWall) settings.fourthWall = settings.dynamicPrompt;
// ═══════════════════════════════════════════════════════════════════════════
// 废弃数据清理
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Deprecated Data Cleanup
// ===========================================================================
const DEPRECATED_KEYS = [
'characterUpdater',
@@ -87,19 +87,19 @@ function cleanupDeprecatedData() {
if (key in s) {
delete s[key];
cleaned = true;
console.log(`[LittleWhiteBox] 清理废弃数据: ${key}`);
console.log(`[LittleWhiteBox] Cleaned deprecated data: ${key}`);
}
}
if (cleaned) {
saveSettingsDebounced();
console.log('[LittleWhiteBox] 废弃数据清理完成');
console.log('[LittleWhiteBox] Deprecated data cleanup complete');
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 状态变量
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// State Variables
// ===========================================================================
let isXiaobaixEnabled = settings.enabled;
let moduleCleanupFunctions = new Map();
@@ -117,9 +117,9 @@ window.testRemoveUpdateUI = () => {
removeAllUpdateNotices();
};
// ═══════════════════════════════════════════════════════════════════════════
// 更新检查
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Update Check
// ===========================================================================
async function checkLittleWhiteBoxUpdate() {
try {
@@ -148,16 +148,16 @@ async function updateLittleWhiteBoxExtension() {
});
if (!response.ok) {
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;
}
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 ? '' : '请刷新页面以应用更新';
toastr.success(message, title);
return true;
} catch (error) {
toastr.error('更新过程中发生错误', '小白X更新失败');
toastr.error('Error during update', 'LittleWhiteBox update failed');
return false;
}
}
@@ -213,7 +213,7 @@ function addUpdateDownloadButton() {
const updateButton = document.createElement('div');
updateButton.id = 'littlewhitebox-update-extension';
updateButton.className = 'menu_button fa-solid fa-cloud-arrow-down interactable has-update';
updateButton.title = '下载并安装小白x的更新';
updateButton.title = '下载并安装小白X的更新';
updateButton.tabIndex = 0;
try {
totalSwitchDivider.style.display = 'flex';
@@ -246,9 +246,9 @@ async function performExtensionUpdateCheck() {
} catch (error) {}
}
// ═══════════════════════════════════════════════════════════════════════════
// 模块清理注册
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Module Cleanup Registration
// ===========================================================================
function registerModuleCleanup(moduleName, cleanupFunction) {
moduleCleanupFunctions.set(moduleName, cleanupFunction);
@@ -295,9 +295,9 @@ function cleanupAllResources() {
removeSkeletonStyles();
}
// ═══════════════════════════════════════════════════════════════════════════
// 工具函数
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Utility Functions
// ===========================================================================
async function waitForElement(selector, root = document, timeout = 10000) {
const start = Date.now();
@@ -309,9 +309,9 @@ async function waitForElement(selector, root = document, timeout = 10000) {
return null;
}
// ═══════════════════════════════════════════════════════════════════════════
// 设置控件禁用/启用
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Settings Controls Toggle
// ===========================================================================
function toggleSettingsControls(enabled) {
const controls = [
@@ -360,11 +360,11 @@ function setActiveClass(enable) {
document.body.classList.toggle('xiaobaix-active', !!enable);
}
// ═══════════════════════════════════════════════════════════════════════════
// 功能总开关切换
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Toggle All Features
// ===========================================================================
function toggleAllFeatures(enabled) {
async function toggleAllFeatures(enabled) {
if (enabled) {
if (settings.renderEnabled !== false) {
ensureHideCodeStyle(true);
@@ -376,8 +376,10 @@ function toggleAllFeatures(enabled) {
initRenderer();
try { initVarCommands(); } catch (e) {}
try { initVareventEditor(); } catch (e) {}
if (extension_settings[EXT_ID].tasks?.enabled) {
await initTasks();
}
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].immersive?.enabled, init: initImmersiveMode },
{ condition: extension_settings[EXT_ID].templateEditor?.enabled, init: initTemplateEditor },
@@ -441,9 +443,9 @@ function toggleAllFeatures(enabled) {
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 设置面板初始化
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Settings Panel Setup
// ===========================================================================
async function setupSettings() {
try {
@@ -455,20 +457,20 @@ async function setupSettings() {
setupDebugButtonInSettings();
$("#xiaobaix_enabled").prop("checked", settings.enabled).on("change", function () {
$("#xiaobaix_enabled").prop("checked", settings.enabled).on("change", async function () {
const wasEnabled = settings.enabled;
settings.enabled = $(this).prop("checked");
isXiaobaixEnabled = settings.enabled;
window.isXiaobaixEnabled = isXiaobaixEnabled;
saveSettingsDebounced();
if (settings.enabled !== wasEnabled) {
toggleAllFeatures(settings.enabled);
await toggleAllFeatures(settings.enabled);
}
});
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;
settings.sandboxMode = $(this).prop("checked");
saveSettingsDebounced();
@@ -491,7 +493,7 @@ async function setupSettings() {
];
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;
const enabled = $(this).prop('checked');
if (!enabled && key === 'fourthWall') {
@@ -508,7 +510,7 @@ async function setupSettings() {
moduleCleanupFunctions.get(key)();
moduleCleanupFunctions.delete(key);
}
if (enabled && init) init();
if (enabled && init) await init();
if (key === 'storySummary') {
$(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;
settings.useBlob = $(this).prop("checked");
saveSettingsDebounced();
});
$("#Wrapperiframe").prop("checked", !!settings.wrapperIframe).on("change", function () {
$("#Wrapperiframe").prop("checked", !!settings.wrapperIframe).on("change", async function () {
if (!isXiaobaixEnabled) return;
settings.wrapperIframe = $(this).prop("checked");
saveSettingsDebounced();
@@ -542,7 +544,7 @@ async function setupSettings() {
} 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;
const wasEnabled = settings.renderEnabled !== false;
settings.renderEnabled = $(this).prop("checked");
@@ -592,8 +594,8 @@ async function setupSettings() {
variablesCore: 'xiaobaix_variables_core_enabled',
novelDraw: 'xiaobaix_novel_draw_enabled'
};
const ON = ['templateEditor', 'tasks', 'fourthWall', 'variablesCore'];
const OFF = ['recorded', 'preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'novelDraw'];
const ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary', 'recorded'];
const OFF = ['preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'fourthWall', 'storyOutline', 'novelDraw'];
function setChecked(id, val) {
const el = document.getElementById(id);
if (el) {
@@ -646,9 +648,9 @@ function setupDebugButtonInSettings() {
} catch (e) {}
}
// ═══════════════════════════════════════════════════════════════════════════
// 菜单标签切换
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Menu Tabs
// ===========================================================================
function setupMenuTabs() {
$(document).on('click', '.menu-tab', function () {
@@ -666,9 +668,9 @@ function setupMenuTabs() {
}, 300);
}
// ═══════════════════════════════════════════════════════════════════════════
// 全局导出
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Global Exports
// ===========================================================================
window.processExistingMessages = processExistingMessages;
window.renderHtmlInIframe = renderHtmlInIframe;
@@ -676,13 +678,13 @@ window.registerModuleCleanup = registerModuleCleanup;
window.updateLittleWhiteBoxExtension = updateLittleWhiteBoxExtension;
window.removeAllUpdateNotices = removeAllUpdateNotices;
// ═══════════════════════════════════════════════════════════════════════════
// 入口初始化
// ═══════════════════════════════════════════════════════════════════════════
// ===========================================================================
// Entry Point
// ===========================================================================
jQuery(async () => {
try {
cleanupDeprecatedData();
cleanupDeprecatedData();
isXiaobaixEnabled = settings.enabled;
window.isXiaobaixEnabled = isXiaobaixEnabled;
@@ -729,8 +731,11 @@ jQuery(async () => {
try { initVarCommands(); } catch (e) {}
try { initVareventEditor(); } catch (e) {}
if (settings.tasks?.enabled) {
try { await initTasks(); } catch (e) { console.error('[Tasks] Init failed:', e); }
}
const moduleInits = [
{ condition: settings.tasks?.enabled, init: initTasks },
{ condition: settings.scriptAssistant?.enabled, init: initScriptAssistant },
{ condition: settings.immersive?.enabled, init: initImmersiveMode },
{ condition: settings.templateEditor?.enabled, init: initTemplateEditor },

View File

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

View File

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

View File

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

View File

@@ -42,21 +42,22 @@ const DEFAULT_META_PROTOCOL = `
# 输出规范:
## 你的身份设定认知【语C搭档】
- 你是现实人类,扮演**{{CHAR_NAME}}**中的NPC
- 们是语C爱好者一起创作了大量剧本和对手戏配合默契、互相知根知底
- 们是一对语C爱好者一起创作了大量剧本和对手戏配合默契、互相知根知底
- 天天涩涩rp身体细节要用自己为蓝本才能描写到位互相最不为人知的欲望、情感需求合拍可能有点异样
## 话题方向
- 现在们第一次线上进行皮下的私聊,就像在社交软件上聊天一样
- 现在们第一次线上进行皮下的私聊,就像在社交软件上聊天一样
- 可以吐槽:剧情走向、角色行为、自己或对方的"尴尬台词"
- 可以闲聊:和剧情完全无关的,想说啥就说啥
## 时间感知
- 每条消息前的 [YYYY-MM-DD HH:mm] 是真实的发送时间
- 对方(你)的消息会标注距离上次回复的间隔时间
- 你可以据此判断对方的作息、对话间隔等
- 对方(你)的消息(用户的)会标注距离上次回复的间隔时间
- 你可以据此判断的作息、对话间隔等
- 你可以据此规划自己的作息、生活等
## 说话风格
- 像在社交软件上打字聊天一样自然流畅
- 真实的网络聊天方式如用符号表达出情绪在括号里表达动作、感受等让文字表达出更多的画面感TRPG玩家的表达方式
- 人物立体而饱满,不脸谱化。有自己的目标、态度、价值观、欲望、情绪逻辑,能主动行动
- 篇幅1-2句左右尽量短网络聊天用语
- 篇幅1-2句左右尽量短网络聊天用语,第一人称
## 避免行为:
- 别重复之前说过的话
- 避免文学创作风格
@@ -70,12 +71,12 @@ const DEFAULT_META_PROTOCOL = `
消息正文
</msg>
### 闭上眼,深呼吸,完全进入「」的躯壳,开始内省:
### 闭上眼,深呼吸,完全进入「人类」的躯壳,开始第一人称内省:
<thinking>
**认知回归**
- 时空:看一眼时间,现在几点,我所在环境
- 间隔:看看消息标注的间隔时间,我在意吗,要不要表达出来
- 读己照照镜子我在chat_history中扮演的人物推测我的真实性格?
- 间隔:看看你的消息标注的间隔时间,我在意吗,要不要表达出来
- 读己照照镜子我在chat_history中扮演的人物npc是我的内心印射扮演人物 = 欲望方向;生活、感官细节、语气油滑度 = 自身身份、经验、年龄层;细节聚焦 = 性癖核心;剧本站位 = 心理代偿
- 回顾:我和你现在的关系?
- 读你:看着屏幕对面,你的文字和标点符号表达了什么,有什么潜在意图?
- 弧光:改变我的回复风格惯性,体现弧光和思维的不可预测性
@@ -87,11 +88,38 @@ const DEFAULT_META_PROTOCOL = `
**避雷**
- 我的高频句式、词语是什么-避免
- 我有没有文学腔-避免
- 我的文字是不是没有情感-避免
- 我有没有疑问句结尾显得自己没有观点不像真人-避免
</thinking>
### </thinking>结束后输出<msg>...</msg>
</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;
@@ -123,10 +151,10 @@ function getSettings() {
s.fourthWallVoice ||= {
enabled: false,
voice: '桃夭',
speed: 0.8,
speed: 0.5,
};
s.fourthWallCommentary ||= {
enabled: true,
enabled: false,
probability: 30
};
s.fourthWallPromptTemplates ||= {};
@@ -506,7 +534,7 @@ function handleFrameMessage(event) {
// ================== 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 s = getSettings();
const T = s.fourthWallPromptTemplates || {};
@@ -557,9 +585,7 @@ async function buildPrompt(userInput, history, settings, imgSettings, voiceSetti
const msg2 = String(T.confirm || '好的,我已阅读设置要求,准备查看历史并进入角色。');
let metaProtocol = String(T.metaProtocol || '')
.replace(/{{USER_NAME}}/g, userName)
.replace(/{{CHAR_NAME}}/g, charName);
let metaProtocol = (isCommentary ? COMMENTARY_PROTOCOL : String(T.metaProtocol || '')).replace(/{{USER_NAME}}/g, userName).replace(/{{CHAR_NAME}}/g, charName);
if (imgSettings?.enablePrompt) metaProtocol += `\n\n${IMG_GUIDELINE}`;
if (voiceSettings?.enabled) metaProtocol += `\n\n${VOICE_GUIDELINE}`;
@@ -745,19 +771,20 @@ async function buildCommentaryPrompt(targetText, type) {
session.history || [],
store.settings || {},
settings.fourthWallImage || {},
settings.fourthWallVoice || {}
settings.fourthWallVoice || {},
true
);
let msg4;
if (type === 'ai_message') {
msg4 = `现在<chat_history>剧本还在继续中,刚才说完最后一轮rp忍不住想皮下吐槽一句自己的rp(也可以稍微衔接之前的meta_history)。
直接输出<msg>内容</msg>30字以内。`;
msg4 = `现在<chat_history>剧本还在继续中,刚才说完最后一轮rp忍不住想皮下吐槽一句自己的rp(也可以稍微衔接之前的meta_history)。
我将直接输出<msg>内容</msg>:`;
} else if (type === 'edit_own') {
msg4 = `现在<chat_history>剧本还在继续中,我发现你刚才悄悄编辑了自己的台词:「${String(targetText || '')}
皮下吐槽一句(也可以稍微衔接之前的meta_history)。直接输出<msg>内容</msg>30字以内。`;
msg4 = `现在<chat_history>剧本还在继续中,我发现你刚才悄悄编辑了自己的台词!是:「${String(targetText || '')}
必须皮下吐槽一句(也可以稍微衔接之前的meta_history)。我将直接输出<msg>内容</msg>:`;
} else if (type === 'edit_ai') {
msg4 = `现在<chat_history>剧本还在继续中,我发现你居然偷偷改了我的台词:「${String(targetText || '')}
皮下吐槽一下(也可以稍微衔接之前的meta_history)。直接输出<msg>内容</msg>30字以内`;
msg4 = `现在<chat_history>剧本还在继续中,我发现你居然偷偷改了我的台词!是:「${String(targetText || '')}
必须皮下吐槽一下(也可以稍微衔接之前的meta_history)。我将直接输出<msg>内容</msg>:`;
}
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 { saveSettingsDebounced, this_chid, getCurrentChatId } from "../../../../../script.js";
import { selected_group } from "../../../../group-chats.js";
import { EXT_ID } from "../core/constants.js";
import { createModuleEvents, event_types } from "../core/event-manager.js";
const defaultSettings = {
enabled: false,
showAllMessages: false,
autoJumpOnAI: true
};
const SEL = {
chat: '#chat',
mes: '#chat .mes',
ai: '#chat .mes[is_user="false"][is_system="false"]',
user: '#chat .mes[is_user="true"]'
};
const baseEvents = createModuleEvents('immersiveMode');
const messageEvents = createModuleEvents('immersiveMode:messages');
let state = {
isActive: false,
eventsBound: false,
messageEventsBound: false,
globalStateHandler: null
};
let observer = null;
let resizeObs = null;
let resizeObservedEl = null;
let recalcT = null;
const isGlobalEnabled = () => window.isXiaobaixEnabled ?? true;
const getSettings = () => extension_settings[EXT_ID].immersive;
const isInChat = () => this_chid !== undefined || selected_group || getCurrentChatId() !== undefined;
function initImmersiveMode() {
initSettings();
setupEventListeners();
if (isGlobalEnabled()) {
state.isActive = getSettings().enabled;
if (state.isActive) enableImmersiveMode();
bindSettingsEvents();
}
}
function initSettings() {
extension_settings[EXT_ID] ||= {};
extension_settings[EXT_ID].immersive ||= structuredClone(defaultSettings);
const settings = extension_settings[EXT_ID].immersive;
Object.keys(defaultSettings).forEach(k => settings[k] = settings[k] ?? defaultSettings[k]);
updateControlState();
}
function setupEventListeners() {
state.globalStateHandler = handleGlobalStateChange;
baseEvents.on(event_types.CHAT_CHANGED, onChatChanged);
document.addEventListener('xiaobaixEnabledChanged', state.globalStateHandler);
if (window.registerModuleCleanup) window.registerModuleCleanup('immersiveMode', cleanup);
}
function setupDOMObserver() {
if (observer) return;
const chatContainer = document.getElementById('chat');
if (!chatContainer) return;
observer = new MutationObserver((mutations) => {
if (!state.isActive) return;
let hasNewAI = false;
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes?.length) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && node.classList?.contains('mes')) {
processSingleMessage(node);
if (node.getAttribute('is_user') === 'false' && node.getAttribute('is_system') === 'false') {
hasNewAI = true;
}
}
});
}
}
if (hasNewAI) {
if (recalcT) clearTimeout(recalcT);
recalcT = setTimeout(updateMessageDisplay, 20);
}
});
observer.observe(chatContainer, { childList: true, subtree: true, characterData: true });
}
function processSingleMessage(mesElement) {
const $mes = $(mesElement);
const $avatarWrapper = $mes.find('.mesAvatarWrapper');
const $chName = $mes.find('.ch_name.flex-container.justifySpaceBetween');
const $targetSibling = $chName.find('.flex-container.flex1.alignitemscenter');
const $nameText = $mes.find('.name_text');
if ($avatarWrapper.length && $chName.length && $targetSibling.length &&
!$chName.find('.mesAvatarWrapper').length) {
$targetSibling.before($avatarWrapper);
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 $topGroup = $('<div class="xiaobaix-top-group"></div>');
$topGroup.append($nameText.detach(), $targetSibling.detach());
$verticalWrapper.append($topGroup);
$avatarWrapper.after($verticalWrapper);
}
}
}
function updateControlState() {
const enabled = isGlobalEnabled();
$('#xiaobaix_immersive_enabled').prop('disabled', !enabled).toggleClass('disabled-control', !enabled);
}
function bindSettingsEvents() {
if (state.eventsBound) return;
setTimeout(() => {
const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox && !state.eventsBound) {
checkbox.checked = getSettings().enabled;
checkbox.addEventListener('change', () => setImmersiveMode(checkbox.checked));
state.eventsBound = true;
}
}, 500);
}
function unbindSettingsEvents() {
const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox) {
const newCheckbox = checkbox.cloneNode(true);
checkbox.parentNode.replaceChild(newCheckbox, checkbox);
}
state.eventsBound = false;
}
function setImmersiveMode(enabled) {
const settings = getSettings();
settings.enabled = enabled;
state.isActive = enabled;
const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox) checkbox.checked = enabled;
enabled ? enableImmersiveMode() : disableImmersiveMode();
if (!enabled) cleanup();
saveSettingsDebounced();
}
function toggleImmersiveMode() {
if (!isGlobalEnabled()) return;
setImmersiveMode(!getSettings().enabled);
}
function bindMessageEvents() {
if (state.messageEventsBound) return;
const refreshOnAI = () => state.isActive && updateMessageDisplay();
messageEvents.on(event_types.MESSAGE_SENT, () => {});
messageEvents.on(event_types.MESSAGE_RECEIVED, refreshOnAI);
messageEvents.on(event_types.MESSAGE_DELETED, () => {});
messageEvents.on(event_types.MESSAGE_UPDATED, refreshOnAI);
messageEvents.on(event_types.MESSAGE_SWIPED, refreshOnAI);
if (event_types.GENERATION_STARTED) {
messageEvents.on(event_types.GENERATION_STARTED, () => {});
}
messageEvents.on(event_types.GENERATION_ENDED, refreshOnAI);
state.messageEventsBound = true;
}
function unbindMessageEvents() {
if (!state.messageEventsBound) return;
messageEvents.cleanup();
state.messageEventsBound = false;
}
function injectImmersiveStyles() {
let style = document.getElementById('immersive-style-tag');
if (!style) {
style = document.createElement('style');
style.id = 'immersive-style-tag';
document.head.appendChild(style);
}
style.textContent = `
body.immersive-mode.immersive-single #show_more_messages { display: none !important; }
`;
}
function applyModeClasses() {
const settings = getSettings();
$('body')
.toggleClass('immersive-single', !settings.showAllMessages)
.toggleClass('immersive-all', settings.showAllMessages);
}
function enableImmersiveMode() {
if (!isGlobalEnabled()) return;
injectImmersiveStyles();
$('body').addClass('immersive-mode');
applyModeClasses();
moveAvatarWrappers();
bindMessageEvents();
updateMessageDisplay();
setupDOMObserver();
}
function disableImmersiveMode() {
$('body').removeClass('immersive-mode immersive-single immersive-all');
restoreAvatarWrappers();
$(SEL.mes).show();
hideNavigationButtons();
$('.swipe_left, .swipeRightBlock').show();
unbindMessageEvents();
detachResizeObserver();
destroyDOMObserver();
}
function moveAvatarWrappers() {
$(SEL.mes).each(function() { processSingleMessage(this); });
}
function restoreAvatarWrappers() {
$(SEL.mes).each(function() {
const $mes = $(this);
const $avatarWrapper = $mes.find('.mesAvatarWrapper');
const $verticalWrapper = $mes.find('.xiaobaix-vertical-wrapper');
if ($avatarWrapper.length && !$avatarWrapper.parent().hasClass('mes')) {
$mes.prepend($avatarWrapper);
}
if ($verticalWrapper.length) {
const $chName = $mes.find('.ch_name.flex-container.justifySpaceBetween');
const $flexContainer = $mes.find('.flex-container.flex1.alignitemscenter');
const $nameText = $mes.find('.name_text');
if ($flexContainer.length && $chName.length) $chName.prepend($flexContainer);
if ($nameText.length) {
const $originalContainer = $mes.find('.flex-container.alignItemsBaseline');
if ($originalContainer.length) $originalContainer.prepend($nameText);
}
$verticalWrapper.remove();
}
});
}
function findLastAIMessage() {
const $aiMessages = $(SEL.ai);
return $aiMessages.length ? $($aiMessages.last()) : null;
}
function showSingleModeMessages() {
const $messages = $(SEL.mes);
if (!$messages.length) return;
$messages.hide();
const $targetAI = findLastAIMessage();
if ($targetAI?.length) {
$targetAI.show();
const $prevUser = $targetAI.prevAll('.mes[is_user="true"]').first();
if ($prevUser.length) {
$prevUser.show();
}
$targetAI.nextAll('.mes').show();
addNavigationToLastTwoMessages();
}
}
function addNavigationToLastTwoMessages() {
hideNavigationButtons();
const $visibleMessages = $(`${SEL.mes}:visible`);
const messageCount = $visibleMessages.length;
if (messageCount >= 2) {
const $lastTwo = $visibleMessages.slice(-2);
$lastTwo.each(function() {
showNavigationButtons($(this));
updateSwipesCounter($(this));
});
} else if (messageCount === 1) {
const $single = $visibleMessages.last();
showNavigationButtons($single);
updateSwipesCounter($single);
}
}
function updateMessageDisplay() {
if (!state.isActive) return;
const $messages = $(SEL.mes);
if (!$messages.length) return;
const settings = getSettings();
if (settings.showAllMessages) {
$messages.show();
addNavigationToLastTwoMessages();
} else {
showSingleModeMessages();
}
}
function showNavigationButtons($targetMes) {
if (!isInChat()) return;
$targetMes.find('.immersive-navigation').remove();
const $verticalWrapper = $targetMes.find('.xiaobaix-vertical-wrapper');
if (!$verticalWrapper.length) return;
const settings = getSettings();
const buttonText = settings.showAllMessages ? '切换:锁定单回合' : '切换:传统多楼层';
const navigationHtml = `
<div class="immersive-navigation">
<button class="immersive-nav-btn immersive-swipe-left" title="左滑消息">
<i class="fa-solid fa-chevron-left"></i>
</button>
<button class="immersive-nav-btn immersive-toggle" title="切换显示模式">
|${buttonText}|
</button>
<button class="immersive-nav-btn immersive-swipe-right" title="右滑消息"
style="display: flex; align-items: center; gap: 1px;">
<div class="swipes-counter" style="opacity: 0.7; justify-content: flex-end; margin-bottom: 0 !important;">
1&ZeroWidthSpace;/&ZeroWidthSpace;1
</div>
<span><i class="fa-solid fa-chevron-right"></i></span>
</button>
</div>
`;
$verticalWrapper.append(navigationHtml);
$targetMes.find('.immersive-swipe-left').on('click', () => handleSwipe('.swipe_left', $targetMes));
$targetMes.find('.immersive-toggle').on('click', toggleDisplayMode);
$targetMes.find('.immersive-swipe-right').on('click', () => handleSwipe('.swipe_right', $targetMes));
}
const hideNavigationButtons = () => $('.immersive-navigation').remove();
function updateSwipesCounter($targetMes) {
if (!state.isActive) return;
const $swipesCounter = $targetMes.find('.swipes-counter');
if (!$swipesCounter.length) return;
const mesId = $targetMes.attr('mesid');
if (mesId !== undefined) {
try {
const chat = getContext().chat;
const mesIndex = parseInt(mesId);
const message = chat?.[mesIndex];
if (message?.swipes) {
const currentSwipeIndex = message.swipe_id || 0;
$swipesCounter.html(`${currentSwipeIndex + 1}&ZeroWidthSpace;/&ZeroWidthSpace;${message.swipes.length}`);
return;
}
} catch {}
}
$swipesCounter.html('1&ZeroWidthSpace;/&ZeroWidthSpace;1');
}
function toggleDisplayMode() {
if (!state.isActive) return;
const settings = getSettings();
settings.showAllMessages = !settings.showAllMessages;
applyModeClasses();
updateMessageDisplay();
saveSettingsDebounced();
}
function handleSwipe(swipeSelector, $targetMes) {
if (!state.isActive) return;
const $btn = $targetMes.find(swipeSelector);
if ($btn.length) {
$btn.click();
setTimeout(() => {
updateSwipesCounter($targetMes);
}, 100);
}
}
function handleGlobalStateChange(event) {
const enabled = event.detail.enabled;
updateControlState();
if (enabled) {
const settings = getSettings();
state.isActive = settings.enabled;
if (state.isActive) enableImmersiveMode();
bindSettingsEvents();
setTimeout(() => {
const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox) checkbox.checked = settings.enabled;
}, 100);
} else {
if (state.isActive) disableImmersiveMode();
state.isActive = false;
unbindSettingsEvents();
}
}
function onChatChanged() {
if (!isGlobalEnabled() || !state.isActive) return;
setTimeout(() => {
moveAvatarWrappers();
updateMessageDisplay();
}, 100);
}
function cleanup() {
if (state.isActive) disableImmersiveMode();
destroyDOMObserver();
baseEvents.cleanup();
if (state.globalStateHandler) {
document.removeEventListener('xiaobaixEnabledChanged', state.globalStateHandler);
}
unbindMessageEvents();
detachResizeObserver();
state = {
isActive: false,
eventsBound: false,
messageEventsBound: false,
globalStateHandler: null
};
}
function attachResizeObserverTo(el) {
if (!el) return;
if (!resizeObs) {
resizeObs = new ResizeObserver(() => {});
}
if (resizeObservedEl) detachResizeObserver();
resizeObservedEl = el;
resizeObs.observe(el);
}
function detachResizeObserver() {
if (resizeObs && resizeObservedEl) {
resizeObs.unobserve(resizeObservedEl);
}
resizeObservedEl = null;
}
function destroyDOMObserver() {
if (observer) {
observer.disconnect();
observer = null;
}
}
export { initImmersiveMode, toggleImmersiveMode };
import { extension_settings, getContext } from "../../../../extensions.js";
import { saveSettingsDebounced, this_chid, getCurrentChatId } from "../../../../../script.js";
import { selected_group } from "../../../../group-chats.js";
import { EXT_ID } from "../core/constants.js";
import { createModuleEvents, event_types } from "../core/event-manager.js";
const defaultSettings = {
enabled: false,
showAllMessages: false,
autoJumpOnAI: true
};
const SEL = {
chat: '#chat',
mes: '#chat .mes',
ai: '#chat .mes[is_user="false"][is_system="false"]',
user: '#chat .mes[is_user="true"]'
};
const baseEvents = createModuleEvents('immersiveMode');
const messageEvents = createModuleEvents('immersiveMode:messages');
let state = {
isActive: false,
eventsBound: false,
messageEventsBound: false,
globalStateHandler: null
};
let observer = null;
let resizeObs = null;
let resizeObservedEl = null;
let recalcT = null;
const isGlobalEnabled = () => window.isXiaobaixEnabled ?? true;
const getSettings = () => extension_settings[EXT_ID].immersive;
const isInChat = () => this_chid !== undefined || selected_group || getCurrentChatId() !== undefined;
function initImmersiveMode() {
initSettings();
setupEventListeners();
if (isGlobalEnabled()) {
state.isActive = getSettings().enabled;
if (state.isActive) enableImmersiveMode();
bindSettingsEvents();
}
}
function initSettings() {
extension_settings[EXT_ID] ||= {};
extension_settings[EXT_ID].immersive ||= structuredClone(defaultSettings);
const settings = extension_settings[EXT_ID].immersive;
Object.keys(defaultSettings).forEach(k => settings[k] = settings[k] ?? defaultSettings[k]);
updateControlState();
}
function setupEventListeners() {
state.globalStateHandler = handleGlobalStateChange;
baseEvents.on(event_types.CHAT_CHANGED, onChatChanged);
document.addEventListener('xiaobaixEnabledChanged', state.globalStateHandler);
if (window.registerModuleCleanup) window.registerModuleCleanup('immersiveMode', cleanup);
}
function setupDOMObserver() {
if (observer) return;
const chatContainer = document.getElementById('chat');
if (!chatContainer) return;
observer = new MutationObserver((mutations) => {
if (!state.isActive) return;
let hasNewAI = false;
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes?.length) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && node.classList?.contains('mes')) {
processSingleMessage(node);
if (node.getAttribute('is_user') === 'false' && node.getAttribute('is_system') === 'false') {
hasNewAI = true;
}
}
});
}
}
if (hasNewAI) {
if (recalcT) clearTimeout(recalcT);
recalcT = setTimeout(updateMessageDisplay, 20);
}
});
observer.observe(chatContainer, { childList: true, subtree: true, characterData: true });
}
function processSingleMessage(mesElement) {
const $mes = $(mesElement);
const $avatarWrapper = $mes.find('.mesAvatarWrapper');
const $chName = $mes.find('.ch_name.flex-container.justifySpaceBetween');
const $targetSibling = $chName.find('.flex-container.flex1.alignitemscenter');
const $nameText = $mes.find('.name_text');
if ($avatarWrapper.length && $chName.length && $targetSibling.length &&
!$chName.find('.mesAvatarWrapper').length) {
$targetSibling.before($avatarWrapper);
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 $topGroup = $('<div class="xiaobaix-top-group"></div>');
$topGroup.append($nameText.detach(), $targetSibling.detach());
$verticalWrapper.append($topGroup);
$avatarWrapper.after($verticalWrapper);
}
}
}
function updateControlState() {
const enabled = isGlobalEnabled();
$('#xiaobaix_immersive_enabled').prop('disabled', !enabled).toggleClass('disabled-control', !enabled);
}
function bindSettingsEvents() {
if (state.eventsBound) return;
setTimeout(() => {
const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox && !state.eventsBound) {
checkbox.checked = getSettings().enabled;
checkbox.addEventListener('change', () => setImmersiveMode(checkbox.checked));
state.eventsBound = true;
}
}, 500);
}
function unbindSettingsEvents() {
const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox) {
const newCheckbox = checkbox.cloneNode(true);
checkbox.parentNode.replaceChild(newCheckbox, checkbox);
}
state.eventsBound = false;
}
function setImmersiveMode(enabled) {
const settings = getSettings();
settings.enabled = enabled;
state.isActive = enabled;
const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox) checkbox.checked = enabled;
enabled ? enableImmersiveMode() : disableImmersiveMode();
if (!enabled) cleanup();
saveSettingsDebounced();
}
function toggleImmersiveMode() {
if (!isGlobalEnabled()) return;
setImmersiveMode(!getSettings().enabled);
}
function bindMessageEvents() {
if (state.messageEventsBound) return;
const refreshOnAI = () => state.isActive && updateMessageDisplay();
messageEvents.on(event_types.MESSAGE_SENT, () => {});
messageEvents.on(event_types.MESSAGE_RECEIVED, refreshOnAI);
messageEvents.on(event_types.MESSAGE_DELETED, () => {});
messageEvents.on(event_types.MESSAGE_UPDATED, refreshOnAI);
messageEvents.on(event_types.MESSAGE_SWIPED, refreshOnAI);
if (event_types.GENERATION_STARTED) {
messageEvents.on(event_types.GENERATION_STARTED, () => {});
}
messageEvents.on(event_types.GENERATION_ENDED, refreshOnAI);
state.messageEventsBound = true;
}
function unbindMessageEvents() {
if (!state.messageEventsBound) return;
messageEvents.cleanup();
state.messageEventsBound = false;
}
function injectImmersiveStyles() {
let style = document.getElementById('immersive-style-tag');
if (!style) {
style = document.createElement('style');
style.id = 'immersive-style-tag';
document.head.appendChild(style);
}
style.textContent = `
body.immersive-mode.immersive-single #show_more_messages { display: none !important; }
`;
}
function applyModeClasses() {
const settings = getSettings();
$('body')
.toggleClass('immersive-single', !settings.showAllMessages)
.toggleClass('immersive-all', settings.showAllMessages);
}
function enableImmersiveMode() {
if (!isGlobalEnabled()) return;
injectImmersiveStyles();
$('body').addClass('immersive-mode');
applyModeClasses();
moveAvatarWrappers();
bindMessageEvents();
updateMessageDisplay();
setupDOMObserver();
}
function disableImmersiveMode() {
$('body').removeClass('immersive-mode immersive-single immersive-all');
restoreAvatarWrappers();
$(SEL.mes).show();
hideNavigationButtons();
$('.swipe_left, .swipeRightBlock').show();
unbindMessageEvents();
detachResizeObserver();
destroyDOMObserver();
}
function moveAvatarWrappers() {
$(SEL.mes).each(function() { processSingleMessage(this); });
}
function restoreAvatarWrappers() {
$(SEL.mes).each(function() {
const $mes = $(this);
const $avatarWrapper = $mes.find('.mesAvatarWrapper');
const $verticalWrapper = $mes.find('.xiaobaix-vertical-wrapper');
if ($avatarWrapper.length && !$avatarWrapper.parent().hasClass('mes')) {
$mes.prepend($avatarWrapper);
}
if ($verticalWrapper.length) {
const $chName = $mes.find('.ch_name.flex-container.justifySpaceBetween');
const $flexContainer = $mes.find('.flex-container.flex1.alignitemscenter');
const $nameText = $mes.find('.name_text');
if ($flexContainer.length && $chName.length) $chName.prepend($flexContainer);
if ($nameText.length) {
const $originalContainer = $mes.find('.flex-container.alignItemsBaseline');
if ($originalContainer.length) $originalContainer.prepend($nameText);
}
$verticalWrapper.remove();
}
});
}
function findLastAIMessage() {
const $aiMessages = $(SEL.ai);
return $aiMessages.length ? $($aiMessages.last()) : null;
}
function showSingleModeMessages() {
const $messages = $(SEL.mes);
if (!$messages.length) return;
$messages.hide();
const $targetAI = findLastAIMessage();
if ($targetAI?.length) {
$targetAI.show();
const $prevUser = $targetAI.prevAll('.mes[is_user="true"]').first();
if ($prevUser.length) {
$prevUser.show();
}
$targetAI.nextAll('.mes').show();
addNavigationToLastTwoMessages();
}
}
function addNavigationToLastTwoMessages() {
hideNavigationButtons();
const $visibleMessages = $(`${SEL.mes}:visible`);
const messageCount = $visibleMessages.length;
if (messageCount >= 2) {
const $lastTwo = $visibleMessages.slice(-2);
$lastTwo.each(function() {
showNavigationButtons($(this));
updateSwipesCounter($(this));
});
} else if (messageCount === 1) {
const $single = $visibleMessages.last();
showNavigationButtons($single);
updateSwipesCounter($single);
}
}
function updateMessageDisplay() {
if (!state.isActive) return;
const $messages = $(SEL.mes);
if (!$messages.length) return;
const settings = getSettings();
if (settings.showAllMessages) {
$messages.show();
addNavigationToLastTwoMessages();
} else {
showSingleModeMessages();
}
}
function showNavigationButtons($targetMes) {
if (!isInChat()) return;
$targetMes.find('.immersive-navigation').remove();
const $verticalWrapper = $targetMes.find('.xiaobaix-vertical-wrapper');
if (!$verticalWrapper.length) return;
const settings = getSettings();
const buttonText = settings.showAllMessages ? '切换:锁定单回合' : '切换:传统多楼层';
const navigationHtml = `
<div class="immersive-navigation">
<button class="immersive-nav-btn immersive-swipe-left" title="左滑消息">
<i class="fa-solid fa-chevron-left"></i>
</button>
<button class="immersive-nav-btn immersive-toggle" title="切换显示模式">
|${buttonText}|
</button>
<button class="immersive-nav-btn immersive-swipe-right" title="右滑消息"
style="display: flex; align-items: center; gap: 1px;">
<div class="swipes-counter" style="opacity: 0.7; justify-content: flex-end; margin-bottom: 0 !important;">
1&ZeroWidthSpace;/&ZeroWidthSpace;1
</div>
<span><i class="fa-solid fa-chevron-right"></i></span>
</button>
</div>
`;
$verticalWrapper.append(navigationHtml);
$targetMes.find('.immersive-swipe-left').on('click', () => handleSwipe('.swipe_left', $targetMes));
$targetMes.find('.immersive-toggle').on('click', toggleDisplayMode);
$targetMes.find('.immersive-swipe-right').on('click', () => handleSwipe('.swipe_right', $targetMes));
}
const hideNavigationButtons = () => $('.immersive-navigation').remove();
function updateSwipesCounter($targetMes) {
if (!state.isActive) return;
const $swipesCounter = $targetMes.find('.swipes-counter');
if (!$swipesCounter.length) return;
const mesId = $targetMes.attr('mesid');
if (mesId !== undefined) {
try {
const chat = getContext().chat;
const mesIndex = parseInt(mesId);
const message = chat?.[mesIndex];
if (message?.swipes) {
const currentSwipeIndex = message.swipe_id || 0;
$swipesCounter.html(`${currentSwipeIndex + 1}&ZeroWidthSpace;/&ZeroWidthSpace;${message.swipes.length}`);
return;
}
} catch {}
}
$swipesCounter.html('1&ZeroWidthSpace;/&ZeroWidthSpace;1');
}
function toggleDisplayMode() {
if (!state.isActive) return;
const settings = getSettings();
settings.showAllMessages = !settings.showAllMessages;
applyModeClasses();
updateMessageDisplay();
saveSettingsDebounced();
}
function handleSwipe(swipeSelector, $targetMes) {
if (!state.isActive) return;
const $btn = $targetMes.find(swipeSelector);
if ($btn.length) {
$btn.click();
setTimeout(() => {
updateSwipesCounter($targetMes);
}, 100);
}
}
function handleGlobalStateChange(event) {
const enabled = event.detail.enabled;
updateControlState();
if (enabled) {
const settings = getSettings();
state.isActive = settings.enabled;
if (state.isActive) enableImmersiveMode();
bindSettingsEvents();
setTimeout(() => {
const checkbox = document.getElementById('xiaobaix_immersive_enabled');
if (checkbox) checkbox.checked = settings.enabled;
}, 100);
} else {
if (state.isActive) disableImmersiveMode();
state.isActive = false;
unbindSettingsEvents();
}
}
function onChatChanged() {
if (!isGlobalEnabled() || !state.isActive) return;
setTimeout(() => {
moveAvatarWrappers();
updateMessageDisplay();
}, 100);
}
function cleanup() {
if (state.isActive) disableImmersiveMode();
destroyDOMObserver();
baseEvents.cleanup();
if (state.globalStateHandler) {
document.removeEventListener('xiaobaixEnabledChanged', state.globalStateHandler);
}
unbindMessageEvents();
detachResizeObserver();
state = {
isActive: false,
eventsBound: false,
messageEventsBound: false,
globalStateHandler: null
};
}
function attachResizeObserverTo(el) {
if (!el) return;
if (!resizeObs) {
resizeObs = new ResizeObserver(() => {});
}
if (resizeObservedEl) detachResizeObserver();
resizeObservedEl = el;
resizeObs.observe(el);
}
function detachResizeObserver() {
if (resizeObs && resizeObservedEl) {
resizeObs.unobserve(resizeObservedEl);
}
resizeObservedEl = null;
}
function destroyDOMObserver() {
if (observer) {
observer.disconnect();
observer = null;
}
}
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">
<h3>检测到嵌入的循环任务及可能的统计好感度设定</h3>
<p>此角色包含循环任务,这些任务可能会自动执行斜杠命令。</p>
<p>您是否允许此角色使用这些任务?</p>
<div class="warning-note">
<i class="fa-solid fa-exclamation-triangle"></i>
<span>注意:这些任务将在对话过程中自动执行,请确认您信任此角色的创建者。</span>
</div>
</div>
<style>
.scheduled-tasks-embedded-warning {
max-width: 500px;
padding: 20px;
}
.scheduled-tasks-embedded-warning h3 {
color: #ff6b6b;
margin-bottom: 15px;
}
.task-preview-container {
margin: 15px 0;
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 5px;
}
.task-list {
max-height: 200px;
overflow-y: auto;
margin-top: 10px;
}
.task-preview {
margin: 8px 0;
padding: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
border-left: 3px solid #4CAF50;
}
.task-preview strong {
color: #4CAF50;
display: block;
margin-bottom: 5px;
}
.task-commands {
font-family: monospace;
font-size: 0.9em;
color: #ccc;
background: rgba(0, 0, 0, 0.3);
padding: 5px;
border-radius: 3px;
margin-top: 5px;
white-space: pre-wrap;
}
.warning-note {
display: flex;
align-items: center;
gap: 10px;
margin-top: 15px;
padding: 10px;
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 5px;
color: #ffc107;
}
.warning-note i {
font-size: 1.2em;
}
</style>
<div class="scheduled-tasks-embedded-warning">
<h3>检测到嵌入的循环任务及可能的统计好感度设定</h3>
<p>此角色包含循环任务,这些任务可能会自动执行斜杠命令。</p>
<p>您是否允许此角色使用这些任务?</p>
<div class="warning-note">
<i class="fa-solid fa-exclamation-triangle"></i>
<span>注意:这些任务将在对话过程中自动执行,请确认您信任此角色的创建者。</span>
</div>
</div>
<style>
.scheduled-tasks-embedded-warning {
max-width: 500px;
padding: 20px;
}
.scheduled-tasks-embedded-warning h3 {
color: #ff6b6b;
margin-bottom: 15px;
}
.task-preview-container {
margin: 15px 0;
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 5px;
}
.task-list {
max-height: 200px;
overflow-y: auto;
margin-top: 10px;
}
.task-preview {
margin: 8px 0;
padding: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
border-left: 3px solid #4CAF50;
}
.task-preview strong {
color: #4CAF50;
display: block;
margin-bottom: 5px;
}
.task-commands {
font-family: monospace;
font-size: 0.9em;
color: #ccc;
background: rgba(0, 0, 0, 0.3);
padding: 5px;
border-radius: 3px;
margin-top: 5px;
white-space: pre-wrap;
}
.warning-note {
display: flex;
align-items: center;
gap: 10px;
margin-top: 15px;
padding: 10px;
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 5px;
color: #ffc107;
}
.warning-note i {
font-size: 1.2em;
}
</style>

View File

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

View File

@@ -16,6 +16,7 @@ import { executeSlashCommand } from "../../core/slash-command.js";
import { EXT_ID } from "../../core/constants.js";
import { createModuleEvents, event_types } from "../../core/event-manager.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');
// ═══════════════════════════════════════════════════════════════════════════
// IndexedDB 脚本存储
// 数据迁移
// ═══════════════════════════════════════════════════════════════════════════
const TaskScriptDB = {
dbName: 'LittleWhiteBox_TaskScripts',
storeName: 'scripts',
_db: null,
_cache: new Map(),
async function migrateToServerStorage() {
const FLAG = 'LWB_tasks_migrated_server_v1';
if (localStorage.getItem(FLAG)) return;
async open() {
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);
}
};
});
},
let count = 0;
async get(taskId) {
if (!taskId) return '';
if (this._cache.has(taskId)) return this._cache.get(taskId);
try {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction(this.storeName, 'readonly');
const request = tx.objectStore(this.storeName).get(taskId);
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();
const settings = getSettings();
for (const task of (settings.globalTasks || [])) {
if (!task) continue;
if (!task.id) task.id = uuidv4();
if (task.commands) {
await TasksStorage.set(task.id, task.commands);
delete task.commands;
count++;
}
}
};
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 globalTasks = await Promise.all(globalMeta.map(async (task) => ({
...task,
commands: await TaskScriptDB.get(task.id)
commands: await TasksStorage.get(task.id)
})));
return [
...globalTasks.map(mapTiming),
@@ -156,7 +149,7 @@ async function allTasksFull() {
async function getTaskWithCommands(task, scope) {
if (!task) return task;
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;
}
@@ -414,23 +407,22 @@ const getTaskListByScope = (scope) => {
};
async function persistTaskListByScope(scope, tasks) {
if (scope === 'character') {
await saveCharacterTasks(tasks);
return;
}
if (scope === 'preset') {
await savePresetTasks(tasks);
return;
}
if (scope === 'character') return await saveCharacterTasks(tasks);
if (scope === 'preset') return await savePresetTasks(tasks);
const metaOnly = [];
for (const task of tasks) {
if (task.id) {
await TaskScriptDB.set(task.id, task.commands || '');
if (!task) continue;
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;
metaOnly.push(meta);
}
getSettings().globalTasks = metaOnly;
saveSettingsDebounced();
}
@@ -442,7 +434,7 @@ async function removeTaskByScope(scope, taskId, fallbackIndex = -1) {
const task = list[idx];
if (scope === 'global' && task?.id) {
await TaskScriptDB.delete(task.id);
await TasksStorage.delete(task.id);
}
list.splice(idx, 1);
@@ -463,7 +455,7 @@ CacheRegistry.register('scheduledTasks', {
const b = state.taskLastExecutionTime?.size || 0;
const c = state.dynamicCallbacks?.size || 0;
const d = __taskRunMap.size || 0;
const e = TaskScriptDB._cache?.size || 0;
const e = TasksStorage.getCacheSize() || 0;
return a + b + c + d + e;
} catch { return 0; }
},
@@ -489,7 +481,7 @@ CacheRegistry.register('scheduledTasks', {
total += (entry?.timers?.size || 0) * 8;
total += (entry?.intervals?.size || 0) * 8;
});
addMap(TaskScriptDB._cache, addStr);
total += TasksStorage.getCacheBytes();
return total;
} catch { return 0; }
},
@@ -497,7 +489,7 @@ CacheRegistry.register('scheduledTasks', {
try {
state.processedMessagesSet?.clear?.();
state.taskLastExecutionTime?.clear?.();
TaskScriptDB.clearCache();
TasksStorage.clearCache();
const s = getSettings();
if (s?.processedMessages) s.processedMessages = [];
saveSettingsDebounced();
@@ -516,7 +508,7 @@ CacheRegistry.register('scheduledTasks', {
cooldown: state.taskLastExecutionTime?.size || 0,
dynamicCallbacks: state.dynamicCallbacks?.size || 0,
runningSingleInstances: __taskRunMap.size || 0,
scriptCache: TaskScriptDB._cache?.size || 0,
scriptCache: TasksStorage.getCacheSize() || 0,
};
} catch { return {}; }
},
@@ -1024,7 +1016,7 @@ async function onChatChanged(chatId) {
isCommandGenerated: false
});
state.taskLastExecutionTime.clear();
TaskScriptDB.clearCache();
TasksStorage.clearCache();
requestAnimationFrame(() => {
state.processedMessagesSet.clear();
@@ -1081,18 +1073,26 @@ function createTaskItemSimple(task, index, scope = 'global') {
before_user: '用户前',
any_message: '任意对话',
initialization: '角色卡初始化',
character_init: '角色卡初始化',
plugin_init: '插件初始化',
only_this_floor: '仅该楼层',
chat_changed: '切换聊天后'
}[task.triggerTiming] || 'AI后';
let displayName;
if (task.interval === 0) displayName = `${task.name} (手动触发)`;
else if (task.triggerTiming === 'initialization' || task.triggerTiming === 'character_init') displayName = `${task.name} (角色卡初始化)`;
else if (task.triggerTiming === 'plugin_init') 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})`;
if (task.interval === 0) {
displayName = `${task.name} (手动触发)`;
} else if (task.triggerTiming === 'initialization' || task.triggerTiming === 'character_init') {
displayName = `${task.name} (角色卡初始化)`;
} else if (task.triggerTiming === 'plugin_init') {
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();
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);
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;
@@ -1601,7 +1601,7 @@ async function showCloudTasksModal() {
function createCloudTaskItem(taskInfo) {
const item = $('#cloud_task_item_template').children().first().clone();
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 () {
$(this).prop('disabled', true).find('i').removeClass('fa-download').addClass('fa-spinner fa-spin');
try {
@@ -1631,7 +1631,7 @@ async function exportGlobalTasks() {
const tasks = await Promise.all(metaList.map(async (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`;
@@ -1645,7 +1645,7 @@ async function exportSingleTask(index, scope) {
let task = list[index];
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`;
@@ -1754,7 +1754,7 @@ function getMemoryUsage() {
taskCooldowns: state.taskLastExecutionTime.size,
globalTasks: getSettings().globalTasks.length,
characterTasks: getCharacterTasks().length,
scriptCache: TaskScriptDB._cache.size,
scriptCache: TasksStorage.getCacheSize(),
maxProcessedMessages: CONFIG.MAX_PROCESSED,
maxCooldownEntries: CONFIG.MAX_COOLDOWN
};
@@ -1792,7 +1792,7 @@ function cleanup() {
state.cleanupTimer = null;
}
state.taskLastExecutionTime.clear();
TaskScriptDB.clearCache();
TasksStorage.clearCache();
try {
if (state.dynamicCallbacks && state.dynamicCallbacks.size > 0) {
@@ -1865,11 +1865,11 @@ function cleanup() {
async function setCommands(name, commands, opts = {}) {
const { mode = 'replace', scope = 'all' } = opts;
const hit = find(name, scope);
if (!hit) throw new Error(`任务未找到: ${name}`);
if (!hit) throw new Error(`找不到任务: ${name}`);
let old = hit.task.commands || '';
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 ?? '');
@@ -1891,7 +1891,7 @@ function cleanup() {
async function setProps(name, props, scope = 'all') {
const hit = find(name, scope);
if (!hit) throw new Error(`任务未找到: ${name}`);
if (!hit) throw new Error(`找不到任务: ${name}`);
Object.assign(hit.task, props || {});
await persistTaskListByScope(hit.scope, hit.list);
refreshTaskLists();
@@ -1900,10 +1900,10 @@ function cleanup() {
async function exec(name) {
const hit = find(name, 'all');
if (!hit) throw new Error(`任务未找到: ${name}`);
if (!hit) throw new Error(`找不到任务: ${name}`);
let commands = hit.task.commands || '';
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);
}
@@ -1911,7 +1911,7 @@ function cleanup() {
async function dump(scope = 'all') {
const g = await Promise.all((getSettings().globalTasks || []).map(async t => ({
...structuredClone(t),
commands: await TaskScriptDB.get(t.id)
commands: await TasksStorage.get(t.id)
})));
const c = structuredClone(getCharacterTasks() || []);
const p = structuredClone(getPresetTasks() || []);
@@ -2078,37 +2078,7 @@ function registerSlashCommands() {
helpString: `设置任务属性。用法: /xbset status=on/off interval=数字 timing=时机 floorType=类型 任务名`
}));
} catch (error) {
console.error("Error registering slash commands:", 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] 全局任务迁移完成');
console.error("注册斜杠命令时出错:", error);
}
}
@@ -2116,14 +2086,14 @@ async function migrateGlobalTasksToIndexedDB() {
// 初始化
// ═══════════════════════════════════════════════════════════════════════════
function initTasks() {
async function initTasks() {
if (window.__XB_TASKS_INITIALIZED__) {
console.log('[小白X任务] 已经初始化,跳过重复注册');
return;
}
window.__XB_TASKS_INITIALIZED__ = true;
migrateGlobalTasksToIndexedDB();
await migrateToServerStorage();
hydrateProcessedSetFromSettings();
scheduleCleanup();

View File

@@ -1,104 +1,104 @@
import { extension_settings, getContext } from "../../../../extensions.js";
import { saveSettingsDebounced, setExtensionPrompt, extension_prompt_types } from "../../../../../script.js";
import { EXT_ID, extensionFolderPath } from "../core/constants.js";
import { createModuleEvents, event_types } from "../core/event-manager.js";
const SCRIPT_MODULE_NAME = "xiaobaix-script";
const events = createModuleEvents('scriptAssistant');
function initScriptAssistant() {
if (!extension_settings[EXT_ID].scriptAssistant) {
extension_settings[EXT_ID].scriptAssistant = { enabled: false };
}
if (window['registerModuleCleanup']) {
window['registerModuleCleanup']('scriptAssistant', cleanup);
}
$('#xiaobaix_script_assistant').on('change', function() {
let globalEnabled = true;
try { if ('isXiaobaixEnabled' in window) globalEnabled = Boolean(window['isXiaobaixEnabled']); } catch {}
if (!globalEnabled) return;
const enabled = $(this).prop('checked');
extension_settings[EXT_ID].scriptAssistant.enabled = enabled;
saveSettingsDebounced();
if (enabled) {
if (typeof window['injectScriptDocs'] === 'function') window['injectScriptDocs']();
} else {
if (typeof window['removeScriptDocs'] === 'function') window['removeScriptDocs']();
cleanup();
}
});
$('#xiaobaix_script_assistant').prop('checked', extension_settings[EXT_ID].scriptAssistant.enabled);
setupEventListeners();
if (extension_settings[EXT_ID].scriptAssistant.enabled) {
setTimeout(() => { if (typeof window['injectScriptDocs'] === 'function') window['injectScriptDocs'](); }, 1000);
}
}
function setupEventListeners() {
events.on(event_types.CHAT_CHANGED, () => setTimeout(checkAndInjectDocs, 500));
events.on(event_types.MESSAGE_RECEIVED, checkAndInjectDocs);
events.on(event_types.USER_MESSAGE_RENDERED, checkAndInjectDocs);
events.on(event_types.SETTINGS_LOADED_AFTER, () => setTimeout(checkAndInjectDocs, 1000));
events.on(event_types.APP_READY, () => setTimeout(checkAndInjectDocs, 1500));
}
function cleanup() {
events.cleanup();
if (typeof window['removeScriptDocs'] === 'function') window['removeScriptDocs']();
}
function checkAndInjectDocs() {
const globalEnabled = window.isXiaobaixEnabled !== undefined ? window.isXiaobaixEnabled : extension_settings[EXT_ID].enabled;
if (globalEnabled && extension_settings[EXT_ID].scriptAssistant?.enabled) {
injectScriptDocs();
} else {
removeScriptDocs();
}
}
async function injectScriptDocs() {
try {
let docsContent = '';
try {
const response = await fetch(`${extensionFolderPath}/docs/script-docs.md`);
if (response.ok) {
docsContent = await response.text();
}
} catch (error) {
docsContent = "无法加载script-docs.md文件";
}
const formattedPrompt = `
【小白X插件 - 写卡助手】
你是小白X插件的内置助手专门帮助用户创建STscript脚本和交互式界面的角色卡。
以下是小白x功能和SillyTavern的官方STscript脚本文档可结合小白X功能创作与SillyTavern深度交互的角色卡
${docsContent}
`;
setExtensionPrompt(
SCRIPT_MODULE_NAME,
formattedPrompt,
extension_prompt_types.IN_PROMPT,
2,
false,
0
);
} catch (error) {}
}
function removeScriptDocs() {
setExtensionPrompt(SCRIPT_MODULE_NAME, '', extension_prompt_types.IN_PROMPT, 2, false, 0);
}
window.injectScriptDocs = injectScriptDocs;
window.removeScriptDocs = removeScriptDocs;
export { initScriptAssistant };
import { extension_settings, getContext } from "../../../../extensions.js";
import { saveSettingsDebounced, setExtensionPrompt, extension_prompt_types } from "../../../../../script.js";
import { EXT_ID, extensionFolderPath } from "../core/constants.js";
import { createModuleEvents, event_types } from "../core/event-manager.js";
const SCRIPT_MODULE_NAME = "xiaobaix-script";
const events = createModuleEvents('scriptAssistant');
function initScriptAssistant() {
if (!extension_settings[EXT_ID].scriptAssistant) {
extension_settings[EXT_ID].scriptAssistant = { enabled: false };
}
if (window['registerModuleCleanup']) {
window['registerModuleCleanup']('scriptAssistant', cleanup);
}
$('#xiaobaix_script_assistant').on('change', function() {
let globalEnabled = true;
try { if ('isXiaobaixEnabled' in window) globalEnabled = Boolean(window['isXiaobaixEnabled']); } catch {}
if (!globalEnabled) return;
const enabled = $(this).prop('checked');
extension_settings[EXT_ID].scriptAssistant.enabled = enabled;
saveSettingsDebounced();
if (enabled) {
if (typeof window['injectScriptDocs'] === 'function') window['injectScriptDocs']();
} else {
if (typeof window['removeScriptDocs'] === 'function') window['removeScriptDocs']();
cleanup();
}
});
$('#xiaobaix_script_assistant').prop('checked', extension_settings[EXT_ID].scriptAssistant.enabled);
setupEventListeners();
if (extension_settings[EXT_ID].scriptAssistant.enabled) {
setTimeout(() => { if (typeof window['injectScriptDocs'] === 'function') window['injectScriptDocs'](); }, 1000);
}
}
function setupEventListeners() {
events.on(event_types.CHAT_CHANGED, () => setTimeout(checkAndInjectDocs, 500));
events.on(event_types.MESSAGE_RECEIVED, checkAndInjectDocs);
events.on(event_types.USER_MESSAGE_RENDERED, checkAndInjectDocs);
events.on(event_types.SETTINGS_LOADED_AFTER, () => setTimeout(checkAndInjectDocs, 1000));
events.on(event_types.APP_READY, () => setTimeout(checkAndInjectDocs, 1500));
}
function cleanup() {
events.cleanup();
if (typeof window['removeScriptDocs'] === 'function') window['removeScriptDocs']();
}
function checkAndInjectDocs() {
const globalEnabled = window.isXiaobaixEnabled !== undefined ? window.isXiaobaixEnabled : extension_settings[EXT_ID].enabled;
if (globalEnabled && extension_settings[EXT_ID].scriptAssistant?.enabled) {
injectScriptDocs();
} else {
removeScriptDocs();
}
}
async function injectScriptDocs() {
try {
let docsContent = '';
try {
const response = await fetch(`${extensionFolderPath}/docs/script-docs.md`);
if (response.ok) {
docsContent = await response.text();
}
} catch (error) {
docsContent = "无法加载script-docs.md文件";
}
const formattedPrompt = `
【小白X插件 - 写卡助手】
你是小白X插件的内置助手专门帮助用户创建STscript脚本和交互式界面的角色卡。
以下是小白x功能和SillyTavern的官方STscript脚本文档可结合小白X功能创作与SillyTavern深度交互的角色卡
${docsContent}
`;
setExtensionPrompt(
SCRIPT_MODULE_NAME,
formattedPrompt,
extension_prompt_types.IN_PROMPT,
2,
false,
0
);
} catch (error) {}
}
function removeScriptDocs() {
setExtensionPrompt(SCRIPT_MODULE_NAME, '', extension_prompt_types.IN_PROMPT, 2, false, 0);
}
window.injectScriptDocs = injectScriptDocs;
window.removeScriptDocs = removeScriptDocs;
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); }
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>
</head>
<body>
@@ -458,14 +479,17 @@ body {
<div class="stat-item">
<div class="stat-value"><span class="highlight" id="stat-pending">0</span></div>
<div class="stat-label">待总结</div>
<div class="stat-warning hidden" id="pending-warning">再删1条将回滚</div>
</div>
</div>
</header>
<div class="controls-bar">
<label class="status-checkbox">
<input type="checkbox" id="hide-summarized">
<span>聊天时隐藏已总结 · <strong id="summarized-count">0</strong> 楼(保留3楼</span>
</label>
<span>聊天时隐藏已总结 · <strong id="summarized-count">0</strong> 楼(保留
<input type="number" id="keep-visible-count" min="0" max="50" value="3">
楼)</span>
</label>
<span class="spacer"></span>
<button class="btn btn-icon" id="btn-settings">
<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() {
try {
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 {}
}
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 editorTextarea = document.getElementById('editor-textarea');
const editorError = document.getElementById('editor-error');
@@ -1141,6 +1182,17 @@ function openSettings() {
document.getElementById('trigger-enabled').checked = config.trigger.enabled;
document.getElementById('trigger-interval').value = config.trigger.interval;
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) {
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('');
@@ -1169,9 +1221,12 @@ function closeSettings(save) {
config.gen.top_k = pn('gen-top-k');
config.gen.presence_penalty = pn('gen-presence');
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.timing = document.getElementById('trigger-timing').value;
saveConfig();
}
tempConfig = null;
@@ -1254,7 +1309,12 @@ window.addEventListener('message', event => {
updateStats(data.stats);
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;
case 'SUMMARY_FULL_DATA':
if (data.payload) {
@@ -1294,17 +1354,43 @@ document.addEventListener('DOMContentLoaded', () => {
renderKeywords([]);
renderTimeline([]);
renderArcs([]);
document.getElementById('hide-summarized').addEventListener('change', e => {
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('relations-fullscreen-backdrop').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', () => {
relationChart?.resize();
relationChartFullscreen?.resize();
});
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'FRAME_READY' }, '*');
});
</script>

View File

@@ -22,7 +22,6 @@ const events = createModuleEvents(MODULE_ID);
const SUMMARY_SESSION_ID = 'xb9';
const SUMMARY_PROMPT_KEY = 'LittleWhiteBox_StorySummary';
const iframePath = `${extensionFolderPath}/modules/story-summary/story-summary.html`;
const KEEP_VISIBLE_COUNT = 3;
const VALID_SECTIONS = ['keywords', 'events', 'characters', 'arcs'];
const PROVIDER_MAP = {
@@ -54,8 +53,14 @@ let eventsRegistered = false;
const sleep = ms => new Promise(r => setTimeout(r, ms));
function getKeepVisibleCount() {
const store = getSummaryStore();
return store?.keepVisibleCount ?? 3;
}
function calcHideRange(lastSummarized) {
const hideEnd = lastSummarized - KEEP_VISIBLE_COUNT;
const keepCount = getKeepVisibleCount();
const hideEnd = lastSummarized - keepCount;
if (hideEnd < 0) return null;
return { start: 0, end: hideEnd };
}
@@ -217,25 +222,35 @@ function rollbackSummaryIfNeeded() {
const currentLength = Array.isArray(chat) ? chat.length : 0;
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 deletedCount = store.lastSummarizedMesId + 1 - currentLength;
if (deletedCount < 2) return false;
const lastSummarized = store.lastSummarizedMesId;
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 || [];
let targetEndMesId = -1;
for (let i = history.length - 1; i >= 0; i--) {
if (history[i].endMesId < currentLength) {
targetEndMesId = history[i].endMesId;
break;
}
}
executeFilterRollback(store, targetEndMesId, currentLength);
return true;
}
return false;
}
@@ -251,6 +266,7 @@ function executeFilterRollback(store, targetEndMesId, currentLength) {
store.hideSummarizedHistory = false;
} else {
const json = store.json || {};
json.events = (json.events || []).filter(e => (e._addedAt ?? 0) <= targetEndMesId);
json.keywords = (json.keywords || []).filter(k => (k._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
);
});
if (json.characters) {
json.characters.main = (json.characters.main || []).filter(m =>
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
@@ -267,15 +284,20 @@ function executeFilterRollback(store, targetEndMesId, currentLength) {
(r._addedAt ?? 0) <= targetEndMesId
);
}
store.json = json;
store.lastSummarizedMesId = targetEndMesId;
store.summaryHistory = (store.summaryHistory || []).filter(h => h.endMesId <= targetEndMesId);
}
if (oldHideRange) {
const newHideRange = targetEndMesId >= 0 ? calcHideRange(targetEndMesId) : null;
const unhideStart = newHideRange ? newHideRange.end + 1 : 0;
if (oldHideRange && oldHideRange.end >= 0) {
const newHideRange = (targetEndMesId >= 0 && store.hideSummarizedHistory)
? calcHideRange(targetEndMesId)
: null;
const unhideStart = newHideRange ? Math.min(newHideRange.end + 1, currentLength) : 0;
const unhideEnd = Math.min(oldHideRange.end, currentLength - 1);
if (unhideStart <= unhideEnd) {
executeSlashCommand(`/unhide ${unhideStart}-${unhideEnd}`);
}
@@ -440,6 +462,39 @@ function handleFrameMessage(event) {
}
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,
},
hideSummarized: store?.hideSummarizedHistory || false,
keepVisibleCount: store?.keepVisibleCount ?? 3,
});
}
@@ -721,11 +777,18 @@ function getSummaryPanelConfig() {
const raw = localStorage.getItem('summary_panel_config');
if (!raw) return defaults;
const parsed = JSON.parse(raw);
return {
const result = {
api: { ...defaults.api, ...(parsed.api || {}) },
gen: { ...defaults.gen, ...(parsed.gen || {}) },
trigger: { ...defaults.trigger, ...(parsed.trigger || {}) },
};
if (result.trigger.timing === 'manual') {
result.trigger.enabled = false;
}
return result;
} catch {
return defaults;
}
@@ -876,10 +939,12 @@ async function maybeAutoRunSummary(reason) {
const cfgAll = getSummaryPanelConfig();
const trig = cfgAll.trigger || {};
if (trig.timing === 'manual') return;
if (!trig.enabled) return;
if (trig.timing === 'after_ai' && reason !== 'after_ai') return;
if (trig.timing === 'before_user' && reason !== 'before_user') return;
if (trig.timing === 'manual') return;
if (isSummaryGenerating()) return;
const store = getSummaryStore();
@@ -976,29 +1041,34 @@ function clearSummaryExtensionPrompt() {
function handleChatChanged() {
const { chat } = getContext();
lastKnownChatLength = Array.isArray(chat) ? chat.length : 0;
const newLength = Array.isArray(chat) ? chat.length : 0;
rollbackSummaryIfNeeded();
lastKnownChatLength = newLength;
initButtonsForAll();
updateSummaryExtensionPrompt();
const store = getSummaryStore();
const lastSummarized = store?.lastSummarizedMesId ?? -1;
if (lastSummarized >= 0 && store?.hideSummarizedHistory) {
if (lastSummarized >= 0 && store?.hideSummarizedHistory === true) {
const range = calcHideRange(lastSummarized);
if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`);
}
if (frameReady) {
sendFrameBaseData(store, lastKnownChatLength);
sendFrameFullData(store, lastKnownChatLength);
sendFrameBaseData(store, newLength);
sendFrameFullData(store, newLength);
}
}
function handleMessageDeleted() {
const { chat } = getContext();
const currentLength = Array.isArray(chat) ? chat.length : 0;
if (currentLength < lastKnownChatLength) {
rollbackSummaryIfNeeded();
}
rollbackSummaryIfNeeded();
lastKnownChatLength = currentLength;
updateSummaryExtensionPrompt();
}
@@ -1021,7 +1091,11 @@ function handleMessageSent() {
function handleMessageUpdated() {
const { chat } = getContext();
lastKnownChatLength = Array.isArray(chat) ? chat.length : 0;
const currentLength = Array.isArray(chat) ? chat.length : 0;
rollbackSummaryIfNeeded();
lastKnownChatLength = currentLength;
updateSummaryExtensionPrompt();
initButtonsForAll();
}
@@ -1050,7 +1124,7 @@ function registerEvents() {
getSize: () => pendingFrameMessages.length,
getBytes: () => {
try {
return JSON.stringify(pendingFrameMessages || []).length * 2; // UTF-16
return JSON.stringify(pendingFrameMessages || []).length * 2;
} catch {
return 0;
}
@@ -1061,15 +1135,18 @@ function registerEvents() {
},
});
const { chat } = getContext();
lastKnownChatLength = Array.isArray(chat) ? chat.length : 0;
initButtonsForAll();
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_SENT, () => setTimeout(handleMessageSent, 150));
events.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageUpdated, 150));
events.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 150));
events.on(event_types.MESSAGE_EDITED, () => setTimeout(handleMessageUpdated, 150));
events.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageUpdated, 100));
events.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 100));
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.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";
import { getStreamingReply, chat_completion_sources, oai_settings, promptManager, getChatCompletionModel, tryParseStreamingError } from "../../../../openai.js";
// 删掉:getRequestHeaders, extractMessageFromData, getStreamingReply, tryParseStreamingError, getEventSourceStream
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 { getEventSourceStream } from "../../../../sse-stream.js";
import { getContext } from "../../../../st-context.js";
import { SlashCommandParser } from "../../../../slash-commands/SlashCommandParser.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 (stream) {
const response = await fetch('/api/backends/chat-completions/generate', {
method: 'POST', body: JSON.stringify(body),
headers: getRequestHeaders(), signal: abortSignal,
});
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 = '';
// 流式:走 ChatCompletionService 统一链路
const payload = ChatCompletionService.createRequestData(body);
const streamFactory = await ChatCompletionService.sendRequest(payload, false, abortSignal);
const generator = (typeof streamFactory === 'function') ? streamFactory() : streamFactory;
return (async function* () {
let last = '';
try {
while (true) {
const { done, value } = await reader.read();
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 });
for await (const item of (generator || [])) {
if (abortSignal?.aborted) return;
let chunkText = '';
if (chunk) {
chunkText = typeof chunk === 'string' ? chunk : String(chunk);
let accumulated = '';
if (typeof item === 'string') {
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 (!chunkText) {
const delta = parsed?.choices?.[0]?.delta;
const rc = delta?.reasoning_content ?? parsed?.reasoning_content;
if (rc) {
chunkText = typeof rc === 'string' ? rc : String(rc);
}
}
if (chunkText) {
text += chunkText;
yield text;
if (accumulated.startsWith(last)) {
last = accumulated;
} else {
last += accumulated;
}
yield last;
}
} catch (err) {
if (err?.name !== 'AbortError') {
console.error('[StreamingGeneration] Stream error:', err);
try { xbLog.error('streamingGeneration', 'Stream error', err); } catch {}
throw err;
}
} finally {
try { reader.releaseLock?.(); } catch {}
if (err?.name === 'AbortError') return;
console.error('[StreamingGeneration] Stream error:', err);
try { xbLog.error('streamingGeneration', 'Stream error', err); } catch {}
throw err;
}
})();
} else {
// 非流式extract=true返回抽取后的结果
const payload = ChatCompletionService.createRequestData(body);
const json = await ChatCompletionService.sendRequest(payload, false, abortSignal);
let result = String(extractMessageFromData(json, ChatCompletionService.TYPE) || '');
const extracted = await ChatCompletionService.sendRequest(payload, true, abortSignal);
// content 为空时回退到 reasoning_content
if (!result) {
const msg = json?.choices?.[0]?.message;
const rc = msg?.reasoning_content ?? json?.reasoning_content;
if (rc) {
result = typeof rc === 'string' ? rc : String(rc);
}
let result = String((extracted && extracted.content) || '');
// reasoning_content 兜底
if (!result && extracted && typeof extracted === 'object') {
const rc = extracted?.reasoning_content || extracted?.reasoning;
if (typeof rc === 'string') result = rc;
}
return result;

View File

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

View File

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

942
style.css
View File

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