Initial commit
This commit is contained in:
280
modules/fourth-wall/fw-image.js
Normal file
280
modules/fourth-wall/fw-image.js
Normal file
@@ -0,0 +1,280 @@
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 图片模块 - 缓存与生成(带队列)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const DB_NAME = 'xb_fourth_wall_images';
|
||||
const DB_STORE = 'images';
|
||||
const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
// 队列配置
|
||||
const QUEUE_DELAY_MIN = 5000;
|
||||
const QUEUE_DELAY_MAX = 10000;
|
||||
|
||||
let db = null;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 生成队列(全局共享)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const generateQueue = [];
|
||||
let isQueueProcessing = false;
|
||||
|
||||
function getRandomDelay() {
|
||||
return QUEUE_DELAY_MIN + Math.random() * (QUEUE_DELAY_MAX - QUEUE_DELAY_MIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将生成任务加入队列
|
||||
* @returns {Promise<string>} base64 图片
|
||||
*/
|
||||
function enqueueGeneration(tags, onProgress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const position = generateQueue.length + 1;
|
||||
onProgress?.('queued', position);
|
||||
|
||||
generateQueue.push({ tags, resolve, reject, onProgress });
|
||||
processQueue();
|
||||
});
|
||||
}
|
||||
|
||||
async function processQueue() {
|
||||
if (isQueueProcessing || generateQueue.length === 0) return;
|
||||
|
||||
isQueueProcessing = true;
|
||||
|
||||
while (generateQueue.length > 0) {
|
||||
const { tags, resolve, reject, onProgress } = generateQueue.shift();
|
||||
|
||||
// 通知:开始生成
|
||||
onProgress?.('generating', generateQueue.length);
|
||||
|
||||
try {
|
||||
const base64 = await doGenerateImage(tags);
|
||||
resolve(base64);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
// 如果还有待处理的,等待冷却
|
||||
if (generateQueue.length > 0) {
|
||||
const delay = getRandomDelay();
|
||||
|
||||
// 通知所有排队中的任务
|
||||
generateQueue.forEach((item, idx) => {
|
||||
item.onProgress?.('waiting', idx + 1, delay);
|
||||
});
|
||||
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
}
|
||||
}
|
||||
|
||||
isQueueProcessing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列状态
|
||||
*/
|
||||
export function getQueueStatus() {
|
||||
return {
|
||||
pending: generateQueue.length,
|
||||
isProcessing: isQueueProcessing
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空队列
|
||||
*/
|
||||
export function clearQueue() {
|
||||
while (generateQueue.length > 0) {
|
||||
const { reject } = generateQueue.shift();
|
||||
reject(new Error('队列已清空'));
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// IndexedDB 操作(保持不变)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async function openDB() {
|
||||
if (db) return db;
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, 1);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => { db = request.result; resolve(db); };
|
||||
request.onupgradeneeded = (e) => {
|
||||
const database = e.target.result;
|
||||
if (!database.objectStoreNames.contains(DB_STORE)) {
|
||||
database.createObjectStore(DB_STORE, { keyPath: 'hash' });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function hashTags(tags) {
|
||||
let hash = 0;
|
||||
const str = String(tags || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
return 'fw_' + Math.abs(hash).toString(36);
|
||||
}
|
||||
|
||||
async function getFromCache(tags) {
|
||||
try {
|
||||
const database = await openDB();
|
||||
const hash = hashTags(tags);
|
||||
return new Promise((resolve) => {
|
||||
const tx = database.transaction(DB_STORE, 'readonly');
|
||||
const req = tx.objectStore(DB_STORE).get(hash);
|
||||
req.onsuccess = () => {
|
||||
const result = req.result;
|
||||
resolve(result && Date.now() - result.timestamp < CACHE_TTL ? result.base64 : null);
|
||||
};
|
||||
req.onerror = () => resolve(null);
|
||||
});
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function saveToCache(tags, base64) {
|
||||
try {
|
||||
const database = await openDB();
|
||||
const tx = database.transaction(DB_STORE, 'readwrite');
|
||||
tx.objectStore(DB_STORE).put({
|
||||
hash: hashTags(tags),
|
||||
tags,
|
||||
base64,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export async function clearExpiredCache() {
|
||||
try {
|
||||
const database = await openDB();
|
||||
const cutoff = Date.now() - CACHE_TTL;
|
||||
const tx = database.transaction(DB_STORE, 'readwrite');
|
||||
const store = tx.objectStore(DB_STORE);
|
||||
store.openCursor().onsuccess = (e) => {
|
||||
const cursor = e.target.result;
|
||||
if (cursor) {
|
||||
if (cursor.value.timestamp < cutoff) cursor.delete();
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 图片生成(内部函数,直接调用 NovelDraw)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async function doGenerateImage(tags) {
|
||||
const novelDraw = window.xiaobaixNovelDraw;
|
||||
if (!novelDraw) {
|
||||
throw new Error('NovelDraw 模块未启用');
|
||||
}
|
||||
|
||||
const settings = novelDraw.getSettings();
|
||||
const paramsPreset = settings.paramsPresets?.find(p => p.id === settings.selectedParamsPresetId)
|
||||
|| settings.paramsPresets?.[0];
|
||||
|
||||
if (!paramsPreset) {
|
||||
throw new Error('无可用的参数预设');
|
||||
}
|
||||
|
||||
const scene = [paramsPreset.positivePrefix, tags].filter(Boolean).join(', ');
|
||||
|
||||
const base64 = await novelDraw.generateNovelImage({
|
||||
scene,
|
||||
characterPrompts: [],
|
||||
negativePrompt: paramsPreset.negativePrefix || '',
|
||||
params: paramsPreset.params || {}
|
||||
});
|
||||
|
||||
await saveToCache(tags, base64);
|
||||
return base64;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 公开接口
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 检查缓存
|
||||
*/
|
||||
export async function checkImageCache(tags) {
|
||||
return await getFromCache(tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图片(自动排队)
|
||||
* @param {string} tags - 图片标签
|
||||
* @param {Function} [onProgress] - 进度回调 (status, position, delay?)
|
||||
* @returns {Promise<string>} base64 图片
|
||||
*/
|
||||
export async function generateImage(tags, onProgress) {
|
||||
// 先检查缓存
|
||||
const cached = await getFromCache(tags);
|
||||
if (cached) return cached;
|
||||
|
||||
// 加入队列生成
|
||||
return enqueueGeneration(tags, onProgress);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// postMessage 接口(用于 iframe)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export async function handleCheckCache(data, postToFrame) {
|
||||
const { requestId, tags } = data;
|
||||
|
||||
if (!tags?.trim()) {
|
||||
postToFrame({ type: 'CACHE_MISS', requestId, tags: '' });
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = await getFromCache(tags);
|
||||
|
||||
if (cached) {
|
||||
postToFrame({ type: 'IMAGE_RESULT', requestId, base64: cached, fromCache: true });
|
||||
} else {
|
||||
postToFrame({ type: 'CACHE_MISS', requestId, tags });
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleGenerate(data, postToFrame) {
|
||||
const { requestId, tags } = data;
|
||||
|
||||
if (!tags?.trim()) {
|
||||
postToFrame({ type: 'IMAGE_RESULT', requestId, error: '无效的图片标签' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用队列生成,发送进度更新
|
||||
const base64 = await generateImage(tags, (status, position, delay) => {
|
||||
postToFrame({
|
||||
type: 'IMAGE_PROGRESS',
|
||||
requestId,
|
||||
status,
|
||||
position,
|
||||
delay: delay ? Math.round(delay / 1000) : undefined
|
||||
});
|
||||
});
|
||||
|
||||
postToFrame({ type: 'IMAGE_RESULT', requestId, base64 });
|
||||
|
||||
} catch (e) {
|
||||
postToFrame({ type: 'IMAGE_RESULT', requestId, error: e?.message || '生成失败' });
|
||||
}
|
||||
}
|
||||
|
||||
export const IMG_GUIDELINE = `## 模拟图片
|
||||
如果需要发图、照片给对方时,可以在聊天文本中穿插以下格式行,进行图片模拟:
|
||||
[img: Subject, Appearance, Background, Atmosphere, Extra descriptors]
|
||||
- tag必须为英文,用逗号分隔,使用Danbooru风格的tag,5-15个tag
|
||||
- 第一个tag须固定为人物数量标签,如: 1girl, 1boy, 2girls, solo, etc.
|
||||
- 可以多张照片: 每行一张 [img: ...]
|
||||
- 当需要发送的内容尺度较大时加上nsfw相关tag
|
||||
- image部分也需要在<msg>内`;
|
||||
Reference in New Issue
Block a user