Add files via upload
This commit is contained in:
138
README.md
138
README.md
@@ -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
@@ -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(_){}
|
||||
})();
|
||||
@@ -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
138
core/server-storage.js
Normal 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');
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
146
docs/COPYRIGHT
146
docs/COPYRIGHT
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
190
docs/NOTICE
190
docs/NOTICE
@@ -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
|
||||
|
||||
3434
docs/script-docs.md
3434
docs/script-docs.md
File diff suppressed because it is too large
Load Diff
123
index.js
123
index.js
@@ -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 },
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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​/​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}​/​${message.swipes.length}`);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
$swipesCounter.html('1​/​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​/​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}​/​${message.swipes.length}`);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
$swipesCounter.html('1​/​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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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
522
settings.html
522
settings.html
@@ -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
942
style.css
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user