Update local plugin changes

This commit is contained in:
henrryyes
2026-01-18 01:48:30 +08:00
parent b9b02d48ae
commit 0bd3cc57c5
7 changed files with 2272 additions and 1409 deletions

View File

@@ -43,7 +43,7 @@ const CONFIG_VERSION = 4;
const MAX_SEED = 0xFFFFFFFF;
const API_TEST_TIMEOUT = 15000;
const PLACEHOLDER_REGEX = /\[image:([a-z0-9\-_]+)\]/gi;
const INITIAL_RENDER_MESSAGE_LIMIT = 10;
const INITIAL_RENDER_MESSAGE_LIMIT = 1;
const events = createModuleEvents(MODULE_KEY);
@@ -103,6 +103,7 @@ let settingsCache = null;
let settingsLoaded = false;
let generationAbortController = null;
let messageObserver = null;
let ensureNovelDrawPanelRef = null;
// ═══════════════════════════════════════════════════════════════════════════
// 样式
@@ -177,6 +178,13 @@ function ensureStyles() {
.xb-nd-edit-input:focus{border-color:rgba(212,165,116,0.5);outline:none}
.xb-nd-edit-input.scene{border-color:rgba(212,165,116,0.3)}
.xb-nd-edit-input.char{border-color:rgba(147,197,253,0.3)}
.xb-nd-live-btn{position:absolute;bottom:10px;right:10px;z-index:5;padding:4px 8px;background:rgba(0,0,0,0.75);border:none;border-radius:12px;color:rgba(255,255,255,0.7);font-size:10px;font-weight:700;letter-spacing:0.5px;cursor:pointer;opacity:0.7;transition:all 0.2s;user-select:none}
.xb-nd-live-btn:hover{opacity:1;background:rgba(0,0,0,0.85)}
.xb-nd-live-btn.active{background:rgba(62,207,142,0.9);color:#fff;opacity:1;box-shadow:0 0 10px rgba(62,207,142,0.5)}
.xb-nd-live-btn.loading{pointer-events:none;opacity:0.5}
.xb-nd-img.mode-live .xb-nd-img-wrap>img{opacity:0!important;pointer-events:none}
.xb-nd-live-canvas{border-radius:10px;overflow:hidden}
.xb-nd-live-canvas canvas{display:block;border-radius:10px}
`;
document.head.appendChild(style);
}
@@ -770,6 +778,7 @@ function buildImageHtml({ slotId, imgId, url, tags, positive, messageId, state =
<span class="xb-nd-nav-text">${displayVersion} / ${historyCount}</span>
<button class="xb-nd-nav-arrow" data-action="nav-next" title="${currentIndex === 0 ? '重新生成' : '下一版本'}"></button>
</div>`;
const liveBtn = `<button class="xb-nd-live-btn" data-action="toggle-live" title="Live Photo">LIVE</button>`;
const menuBusy = isBusy ? ' busy' : '';
const menuHtml = `<div class="xb-nd-menu-wrap${menuBusy}">
@@ -787,6 +796,7 @@ ${indicator}
<div class="xb-nd-img-wrap" data-total="${historyCount}">
<img src="${url}" style="max-width:100%;width:auto;height:auto;border-radius:10px;cursor:pointer;box-shadow:0 3px 15px rgba(0,0,0,0.25);${isBusy ? 'opacity:0.5;' : ''}" data-action="open-gallery" ${lazyAttr}>
${navPill}
${liveBtn}
</div>
${menuHtml}
<div class="xb-nd-edit" style="display:none;position:absolute;bottom:8px;left:8px;right:8px;background:rgba(0,0,0,0.9);border-radius:10px;padding:10px;text-align:left;z-index:15;">
@@ -856,6 +866,12 @@ function setImageState(container, state) {
// ═══════════════════════════════════════════════════════════════════════════
async function navigateToImage(container, targetIndex) {
try {
const { destroyLiveEffect } = await import('./image-live-effect.js');
destroyLiveEffect(container);
container.querySelector('.xb-nd-live-btn')?.classList.remove('active');
} catch {}
const slotId = container.dataset.slotId;
const historyCount = parseInt(container.dataset.historyCount) || 1;
const currentIndex = parseInt(container.dataset.currentIndex) || 0;
@@ -966,6 +982,23 @@ function handleTouchEnd(e) {
// 事件委托与图片操作
// ═══════════════════════════════════════════════════════════════════════════
async function handleLiveToggle(container) {
const btn = container.querySelector('.xb-nd-live-btn');
if (!btn || btn.classList.contains('loading')) return;
btn.classList.add('loading');
try {
const { toggleLiveEffect } = await import('./image-live-effect.js');
const isActive = await toggleLiveEffect(container);
btn.classList.remove('loading');
btn.classList.toggle('active', isActive);
} catch (e) {
console.error('[NovelDraw] Live effect failed:', e);
btn.classList.remove('loading');
}
}
function setupEventDelegation() {
if (window._xbNovelEventsBound) return;
window._xbNovelEventsBound = true;
@@ -1045,6 +1078,10 @@ function setupEventDelegation() {
else await refreshSingleImage(container);
break;
}
case 'toggle-live': {
handleLiveToggle(container);
break;
}
}
}, { capture: true });
@@ -1268,6 +1305,12 @@ async function refreshSingleImage(container) {
if (!tags || currentState === ImageState.SAVING || currentState === ImageState.REFRESHING || !slotId) return;
try {
const { destroyLiveEffect } = await import('./image-live-effect.js');
destroyLiveEffect(container);
container.querySelector('.xb-nd-live-btn')?.classList.remove('active');
} catch {}
toggleEditPanel(container, false);
setImageState(container, ImageState.REFRESHING);
@@ -1892,36 +1935,65 @@ async function generateAndInsertImages({ messageId, onStateChange }) {
async function autoGenerateForLastAI() {
const s = getSettings();
if (!isModuleEnabled() || s.mode !== 'auto' || autoBusy) return;
const ctx = getContext();
const chat = ctx.chat || [];
const lastIdx = chat.length - 1;
if (lastIdx < 0) return;
const lastMessage = chat[lastIdx];
if (!lastMessage || lastMessage.is_user) return;
const content = String(lastMessage.mes || '').replace(PLACEHOLDER_REGEX, '').trim();
if (content.length < 50) return;
lastMessage.extra ||= {};
if (lastMessage.extra.xb_novel_auto_done) return;
autoBusy = true;
try {
const { setState, FloatState } = await import('./floating-panel.js');
const { setStateForMessage, FloatState, ensureNovelDrawPanel } = await import('./floating-panel.js');
// 确保面板存在
const messageEl = document.querySelector(`.mes[mesid="${lastIdx}"]`);
if (messageEl) {
ensureNovelDrawPanel(messageEl, lastIdx, { force: true });
}
await generateAndInsertImages({
messageId: lastIdx,
onStateChange: (state, data) => {
switch (state) {
case 'llm': setState(FloatState.LLM); break;
case 'gen': setState(FloatState.GEN, data); break;
case 'progress': setState(FloatState.GEN, data); break;
case 'cooldown': setState(FloatState.COOLDOWN, data); break;
case 'success': setState(data.success === data.total ? FloatState.SUCCESS : FloatState.PARTIAL, data); break;
case 'llm':
setStateForMessage(lastIdx, FloatState.LLM);
break;
case 'gen':
case 'progress':
setStateForMessage(lastIdx, FloatState.GEN, data);
break;
case 'cooldown':
setStateForMessage(lastIdx, FloatState.COOLDOWN, data);
break;
case 'success':
setStateForMessage(
lastIdx,
data.success === data.total ? FloatState.SUCCESS : FloatState.PARTIAL,
data
);
break;
}
}
});
lastMessage.extra.xb_novel_auto_done = true;
} catch (e) {
console.error('[NovelDraw] 自动配图失败:', e);
const { setState, FloatState } = await import('./floating-panel.js');
setState(FloatState.ERROR, { error: classifyError(e) });
try {
const { setStateForMessage, FloatState } = await import('./floating-panel.js');
setStateForMessage(lastIdx, FloatState.ERROR, { error: classifyError(e) });
} catch {}
} finally {
autoBusy = false;
}
@@ -2363,6 +2435,22 @@ export async function openNovelDrawSettings() {
showOverlay();
}
// eslint-disable-next-line no-unused-vars
function renderExistingPanels() {
if (typeof ensureNovelDrawPanelRef !== 'function') return;
const context = getContext();
const chat = context.chat || [];
chat.forEach((message, messageId) => {
if (!message || message.is_user) return; // 跳过用户消息
const messageEl = document.querySelector(`.mes[mesid="${messageId}"]`);
if (!messageEl) return;
ensureNovelDrawPanelRef(messageEl, messageId);
});
}
export async function initNovelDraw() {
if (window?.isXiaobaixEnabled === false) return;
@@ -2374,10 +2462,51 @@ export async function initNovelDraw() {
setupEventDelegation();
setupGenerateInterceptor();
openDB().then(() => { const s = getSettings(); clearExpiredCache(s.cacheDays || 3); });
openDB().then(() => {
const s = getSettings();
clearExpiredCache(s.cacheDays || 3);
});
const { createFloatingPanel } = await import('./floating-panel.js');
createFloatingPanel();
// ════════════════════════════════════════════════════════════════════
// 动态导入 floating-panel(避免循环依赖)
// ════════════════════════════════════════════════════════════════════
const { ensureNovelDrawPanel: ensureNovelDrawPanelFn } = await import('./floating-panel.js');
ensureNovelDrawPanelRef = ensureNovelDrawPanelFn;
// 为现有消息创建画图面板
const renderExistingPanels = () => {
const context = getContext();
const chat = context.chat || [];
chat.forEach((message, messageId) => {
if (!message || message.is_user) return;
const messageEl = document.querySelector(`.mes[mesid="${messageId}"]`);
if (!messageEl) return;
ensureNovelDrawPanelRef?.(messageEl, messageId);
});
};
// ════════════════════════════════════════════════════════════════════
// 事件监听
// ════════════════════════════════════════════════════════════════════
// AI 消息渲染时创建画图按钮
events.on(event_types.CHARACTER_MESSAGE_RENDERED, (data) => {
const messageId = typeof data === 'number' ? data : data?.messageId ?? data?.mesId;
if (messageId === undefined) return;
const messageEl = document.querySelector(`.mes[mesid="${messageId}"]`);
if (!messageEl) return;
const context = getContext();
const message = context.chat?.[messageId];
if (message?.is_user) return;
ensureNovelDrawPanelRef?.(messageEl, messageId);
});
events.on(event_types.CHARACTER_MESSAGE_RENDERED, handleMessageRendered);
events.on(event_types.USER_MESSAGE_RENDERED, handleMessageRendered);
@@ -2385,7 +2514,28 @@ export async function initNovelDraw() {
events.on(event_types.MESSAGE_EDITED, handleMessageModified);
events.on(event_types.MESSAGE_UPDATED, handleMessageModified);
events.on(event_types.MESSAGE_SWIPED, handleMessageModified);
events.on(event_types.GENERATION_ENDED, async () => { try { await autoGenerateForLastAI(); } catch (e) { console.error('[NovelDraw]', e); } });
events.on(event_types.GENERATION_ENDED, async () => {
try {
await autoGenerateForLastAI();
} catch (e) {
console.error('[NovelDraw]', e);
}
});
// 聊天切换时重新创建面板
events.on(event_types.CHAT_CHANGED, () => {
setTimeout(renderExistingPanels, 200);
});
// ════════════════════════════════════════════════════════════════════
// 初始渲染
// ════════════════════════════════════════════════════════════════════
renderExistingPanels();
// ════════════════════════════════════════════════════════════════════
// 全局 API
// ════════════════════════════════════════════════════════════════════
window.xiaobaixNovelDraw = {
getSettings,
@@ -2437,8 +2587,16 @@ export async function cleanupNovelDraw() {
window.removeEventListener('message', handleFrameMessage);
document.getElementById('xiaobaix-novel-draw-overlay')?.remove();
const { destroyFloatingPanel } = await import('./floating-panel.js');
destroyFloatingPanel();
// 动态导入并清理
try {
const { destroyFloatingPanel } = await import('./floating-panel.js');
destroyFloatingPanel();
} catch {}
try {
const { destroyAllLiveEffects } = await import('./image-live-effect.js');
destroyAllLiveEffects();
} catch {}
delete window.xiaobaixNovelDraw;
delete window._xbNovelEventsBound;