Compare commits

...

2 Commits

Author SHA1 Message Date
RT15548
0e7213f006 Resolve conflicts in favor of local version 2026-02-16 17:12:19 +08:00
RT15548
14276b51b7 Upload LittleWhiteBox extension 2026-02-16 17:11:25 +08:00
11 changed files with 3829 additions and 3454 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

257
modules/button-collapse.js Normal file
View File

@@ -0,0 +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 };

View File

@@ -3,6 +3,7 @@
// ═══════════════════════════════════════════════════════════════════════════
import { xbLog } from '../../../../core/debug-core.js';
import { getVectorConfig } from '../../data/config.js';
import { getApiKey } from './siliconflow.js';
const MODULE_ID = 'vector-llm-service';
const SILICONFLOW_API_URL = 'https://api.siliconflow.cn/v1';
@@ -40,8 +41,7 @@ export async function callLLM(messages, options = {}) {
const mod = getStreamingModule();
if (!mod) throw new Error('Streaming module not ready');
const cfg = getVectorConfig();
const apiKey = cfg?.online?.key || '';
const apiKey = getApiKey() || '';
if (!apiKey) {
throw new Error('L0 requires siliconflow API key');
}

View File

@@ -1,21 +1,63 @@
// ═══════════════════════════════════════════════════════════════════════════
// siliconflow.js - 仅保留 Embedding
// siliconflow.js - Embedding + 多 Key 轮询
//
// 在 API Key 输入框中用逗号、分号、竖线或换行分隔多个 Key例如
// sk-aaa,sk-bbb,sk-ccc
// 每次调用自动轮询到下一个 Key并发请求会均匀分布到所有 Key 上。
// ═══════════════════════════════════════════════════════════════════════════
const BASE_URL = 'https://api.siliconflow.cn';
const EMBEDDING_MODEL = 'BAAI/bge-m3';
export function getApiKey() {
// ★ 多 Key 轮询状态
let _keyIndex = 0;
/**
* 从 localStorage 解析所有 Key支持逗号、分号、竖线、换行分隔
*/
function parseKeys() {
try {
const raw = localStorage.getItem('summary_panel_config');
if (raw) {
const parsed = JSON.parse(raw);
return parsed.vector?.online?.key || null;
const keyStr = parsed.vector?.online?.key || '';
return keyStr
.split(/[,;|\n]+/)
.map(k => k.trim())
.filter(k => k.length > 0);
}
} catch { }
return null;
return [];
}
/**
* 获取下一个可用的 API Key轮询
* 每次调用返回不同的 Key自动循环
*/
export function getApiKey() {
const keys = parseKeys();
if (!keys.length) return null;
if (keys.length === 1) return keys[0];
const idx = _keyIndex % keys.length;
const key = keys[idx];
_keyIndex = (_keyIndex + 1) % keys.length;
const masked = key.length > 10 ? key.slice(0, 6) + '***' + key.slice(-4) : '***';
console.log(`[SiliconFlow] 使用 Key ${idx + 1}/${keys.length}: ${masked}`);
return key;
}
/**
* 获取当前配置的 Key 数量(供外部模块动态调整并发用)
*/
export function getKeyCount() {
return Math.max(1, parseKeys().length);
}
// ═══════════════════════════════════════════════════════════════════════════
// Embedding
// ═══════════════════════════════════════════════════════════════════════════
export async function embed(texts, options = {}) {
if (!texts?.length) return [];

View File

@@ -181,14 +181,83 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
// ★ Phase 1: 收集所有新提取的 atoms不向量化
const allNewAtoms = [];
// ★ 30 并发批次处理
// 并发池处理(保持固定并发度)
// ★ 限流检测:连续失败 N 次后暂停并降速
let consecutiveFailures = 0;
let rateLimited = false;
const RATE_LIMIT_THRESHOLD = 3; // 连续失败多少次触发限流保护
const RATE_LIMIT_WAIT_MS = 60000; // 限流后等待时间60 秒)
const RETRY_INTERVAL_MS = 1000; // 降速模式下每次请求间隔1 秒)
const RETRY_CONCURRENCY = 1; // ★ 降速模式下的并发数默认1建议不要超过5
// ★ 通用处理单个 pair 的逻辑(复用于正常模式和降速模式)
const processPair = async (pair, idx, workerId) => {
const floor = pair.aiFloor;
const prev = getL0FloorStatus(floor);
active++;
if (active > peakActive) peakActive = active;
if (DEBUG_CONCURRENCY && (idx % 10 === 0)) {
xbLog.info(MODULE_ID, `L0 pool start idx=${idx} active=${active} peak=${peakActive} worker=${workerId}`);
}
try {
const atoms = await extractAtomsForRound(pair.userMsg, pair.aiMsg, floor, { timeout: 20000 });
if (extractionCancelled) return;
if (atoms == null) {
throw new Error('llm_failed');
}
// ★ 成功:重置连续失败计数
consecutiveFailures = 0;
if (!atoms.length) {
setL0FloorStatus(floor, { status: 'empty', reason: 'llm_empty', atoms: 0 });
} else {
atoms.forEach(a => a.chatId = chatId);
saveStateAtoms(atoms);
allNewAtoms.push(...atoms);
setL0FloorStatus(floor, { status: 'ok', atoms: atoms.length });
builtAtoms += atoms.length;
}
} catch (e) {
if (extractionCancelled) return;
setL0FloorStatus(floor, {
status: 'fail',
attempts: (prev?.attempts || 0) + 1,
reason: String(e?.message || e).replace(/\s+/g, ' ').slice(0, 120),
});
failed++;
// ★ 限流检测:连续失败累加
consecutiveFailures++;
if (consecutiveFailures >= RATE_LIMIT_THRESHOLD && !rateLimited) {
rateLimited = true;
xbLog.warn(MODULE_ID, `连续失败 ${consecutiveFailures} 次,疑似触发 API 限流,将暂停所有并发`);
}
} finally {
active--;
if (!extractionCancelled) {
completed++;
onProgress?.(`提取: ${completed}/${total}`, completed, total);
}
if (DEBUG_CONCURRENCY && (completed % 25 === 0 || completed === total)) {
const elapsed = Math.max(1, Math.round(performance.now() - tStart));
xbLog.info(MODULE_ID, `L0 pool progress=${completed}/${total} active=${active} peak=${peakActive} elapsedMs=${elapsed}`);
}
}
};
// ★ 并发池处理(保持固定并发度)
const poolSize = Math.min(CONCURRENCY, pendingPairs.length);
let nextIndex = 0;
let started = 0;
const runWorker = async (workerId) => {
while (true) {
if (extractionCancelled) return;
if (extractionCancelled || rateLimited) return;
const idx = nextIndex++;
if (idx >= pendingPairs.length) return;
@@ -198,57 +267,9 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
await new Promise(r => setTimeout(r, stagger * STAGGER_DELAY));
}
if (extractionCancelled) return;
if (extractionCancelled || rateLimited) return;
const floor = pair.aiFloor;
const prev = getL0FloorStatus(floor);
active++;
if (active > peakActive) peakActive = active;
if (DEBUG_CONCURRENCY && (idx % 10 === 0)) {
xbLog.info(MODULE_ID, `L0 pool start idx=${idx} active=${active} peak=${peakActive} worker=${workerId}`);
}
try {
const atoms = await extractAtomsForRound(pair.userMsg, pair.aiMsg, floor, { timeout: 20000 });
if (extractionCancelled) return;
if (atoms == null) {
throw new Error('llm_failed');
}
if (!atoms.length) {
setL0FloorStatus(floor, { status: 'empty', reason: 'llm_empty', atoms: 0 });
} else {
atoms.forEach(a => a.chatId = chatId);
saveStateAtoms(atoms);
// Phase 1: 只收集,不向量化
allNewAtoms.push(...atoms);
setL0FloorStatus(floor, { status: 'ok', atoms: atoms.length });
builtAtoms += atoms.length;
}
} catch (e) {
if (extractionCancelled) return;
setL0FloorStatus(floor, {
status: 'fail',
attempts: (prev?.attempts || 0) + 1,
reason: String(e?.message || e).replace(/\s+/g, ' ').slice(0, 120),
});
failed++;
} finally {
active--;
if (!extractionCancelled) {
completed++;
onProgress?.(`提取: ${completed}/${total}`, completed, total);
}
if (DEBUG_CONCURRENCY && (completed % 25 === 0 || completed === total)) {
const elapsed = Math.max(1, Math.round(performance.now() - tStart));
xbLog.info(MODULE_ID, `L0 pool progress=${completed}/${total} active=${active} peak=${peakActive} elapsedMs=${elapsed}`);
}
}
await processPair(pair, idx, workerId);
}
};
@@ -258,6 +279,61 @@ export async function incrementalExtractAtoms(chatId, chat, onProgress, options
xbLog.info(MODULE_ID, `L0 pool done completed=${completed}/${total} failed=${failed} peakActive=${peakActive} elapsedMs=${elapsed}`);
}
// ═════════════════════════════════════════════════════════════════════
// ★ 限流恢复:重置进度,从头开始以限速模式慢慢跑
// ═════════════════════════════════════════════════════════════════════
if (rateLimited && !extractionCancelled) {
const waitSec = RATE_LIMIT_WAIT_MS / 1000;
xbLog.info(MODULE_ID, `限流保护:将重置进度并从头开始降速重来(并发=${RETRY_CONCURRENCY}, 间隔=${RETRY_INTERVAL_MS}ms`);
onProgress?.(`疑似限流,${waitSec}s 后降速重头开始...`, completed, total);
await new Promise(r => setTimeout(r, RATE_LIMIT_WAIT_MS));
if (!extractionCancelled) {
// ★ 核心逻辑:重置计数器,让 UI 从 0 开始跑,给用户“重头开始”的反馈
rateLimited = false;
consecutiveFailures = 0;
completed = 0;
failed = 0;
let retryNextIdx = 0;
xbLog.info(MODULE_ID, `限流恢复:开始降速模式扫描 ${pendingPairs.length} 个楼层`);
const retryWorkers = Math.min(RETRY_CONCURRENCY, pendingPairs.length);
const runRetryWorker = async (wid) => {
while (true) {
if (extractionCancelled) return;
const idx = retryNextIdx++;
if (idx >= pendingPairs.length) return;
const pair = pendingPairs[idx];
const floor = pair.aiFloor;
// ★ 检查该楼层状态
const st = getL0FloorStatus(floor);
if (st?.status === 'ok' || st?.status === 'empty') {
// 刚才已经成功了,直接跳过(仅增加进度计数)
completed++;
onProgress?.(`提取: ${completed}/${total} (跳过已完成)`, completed, total);
continue;
}
// ★ 没做过的,用 slow 模式处理
await processPair(pair, idx, `retry-${wid}`);
// 每个请求后休息,避免再次触发限流
if (idx < pendingPairs.length - 1 && RETRY_INTERVAL_MS > 0) {
await new Promise(r => setTimeout(r, RETRY_INTERVAL_MS));
}
}
};
await Promise.all(Array.from({ length: retryWorkers }, (_, i) => runRetryWorker(i)));
xbLog.info(MODULE_ID, `降速重头开始阶段结束`);
}
}
try {
saveMetadataDebounced?.();
} catch { }

File diff suppressed because it is too large Load Diff

942
style.css
View File

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