Add files via upload

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff