import { Module, ModuleBuilder } from './Module.js'; import { extension_settings, getContext } from '../../../../../extensions.js'; import { saveSettingsDebounced, saveChat, reloadCurrentChat, eventSource, event_types } from '../../../../../../script.js'; import { registerSlashCommand } from '../../../../../slash-commands.js'; import { extensionName } from '../../utils/settings.js'; const sfigenSettingsKey = 'sfigen_settings'; const defaultSettings = { api_key: '', model: 'Qwen/Qwen-Image', negative_prompt: '模糊, 低分辨率, 水印, 文字', image_size: '1664x928', steps: 50, cfg: 4.0, regex_tag: 'sfigen', prefix_prompt: '' }; const builder = new ModuleBuilder() .name('SfiGen') .view('assets/siliconflow-image-gen.html') .strict(true) .required(['mount']); export default class SfiGenModule extends Module { constructor() { super(builder); this.settings = {}; } async init(ctx = {}) { await super.init(ctx); this._loadSettings(); return this; } _loadSettings() { if (!extension_settings[extensionName]) { extension_settings[extensionName] = {}; } if (!extension_settings[extensionName][sfigenSettingsKey]) { extension_settings[extensionName][sfigenSettingsKey] = { ...defaultSettings }; } this.settings = extension_settings[extensionName][sfigenSettingsKey]; // Ensure all default keys exist for (const key in defaultSettings) { if (!(key in this.settings)) { this.settings[key] = defaultSettings[key]; } } } _saveSettings() { extension_settings[extensionName][sfigenSettingsKey] = this.settings; saveSettingsDebounced(); } async mount() { if (this.el) { this.el.id = 'amily2_sfigen_panel'; this.el.style.display = 'none'; } this._bindUI(); this._registerSlashCommand(); this._bindEvents(); this._bindButtonsGlobal(); } _bindUI() { const $el = $(this.el); // Bind inputs $el.find('#sfigen_api_key').val(this.settings.api_key).on('input', (e) => { this.settings.api_key = $(e.target).val(); this._saveSettings(); }); $el.find('#sfigen_model').val(this.settings.model).on('input', (e) => { this.settings.model = $(e.target).val(); this._saveSettings(); }); $el.find('#sfigen_negative_prompt').val(this.settings.negative_prompt).on('input', (e) => { this.settings.negative_prompt = $(e.target).val(); this._saveSettings(); }); $el.find('#sfigen_image_size').val(this.settings.image_size).on('change', (e) => { this.settings.image_size = $(e.target).val(); this._saveSettings(); }); $el.find('#sfigen_steps').val(this.settings.steps).on('input', (e) => { this.settings.steps = $(e.target).val(); this._saveSettings(); }); $el.find('#sfigen_cfg').val(this.settings.cfg).on('input', (e) => { this.settings.cfg = $(e.target).val(); this._saveSettings(); }); $el.find('#sfigen_regex_tag').val(this.settings.regex_tag).on('input', (e) => { this.settings.regex_tag = $(e.target).val(); this._saveSettings(); }); $el.find('#sfigen_prefix_prompt').val(this.settings.prefix_prompt).on('input', (e) => { this.settings.prefix_prompt = $(e.target).val(); this._saveSettings(); }); // Bind style tags $el.find('.sfigen-style-tag').on('click', (e) => { const promptToAdd = $(e.target).data('prompt'); const textarea = $el.find('#sfigen_prefix_prompt'); let currentVal = textarea.val().trim(); if (currentVal) { if (!currentVal.endsWith(',')) { currentVal += ', '; } else { currentVal += ' '; } textarea.val(currentVal + promptToAdd); } else { textarea.val(promptToAdd); } textarea.trigger('input'); $(e.target).css('opacity', '0.5'); setTimeout(() => $(e.target).css('opacity', '1'), 200); }); // Bind back button $el.find('#amily2_sfigen_back_to_main').on('click', () => { $el.hide(); $('#amily2_chat_optimiser > .plugin-features').show(); }); } async _generateImage(prompt) { let finalPrompt = prompt; if (this.settings.prefix_prompt && this.settings.prefix_prompt.trim() !== '') { finalPrompt = `${this.settings.prefix_prompt.trim()}, ${prompt}`; } console.log(`[SfiGen] 开始生成图片,最终提示词:`, finalPrompt); if (!this.settings.api_key) { console.warn(`[SfiGen] 未配置 API Key`); toastr.error('请先在扩展设置中配置 SiliconFlow API Key'); return null; } const url = 'https://api.siliconflow.cn/v1/images/generations'; const headers = { 'Authorization': `Bearer ${this.settings.api_key}`, 'Content-Type': 'application/json' }; const body = { model: this.settings.model, prompt: finalPrompt, negative_prompt: this.settings.negative_prompt, image_size: this.settings.image_size, seed: Math.floor(Math.random() * 1000000000), num_inference_steps: parseInt(this.settings.steps), cfg: parseFloat(this.settings.cfg) }; try { toastr.info('正在生成图片,请稍候...'); const response = await fetch(url, { method: 'POST', headers: headers, body: JSON.stringify(body) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || `HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.images && data.images.length > 0) { toastr.success('图片生成成功!'); return data.images[0].url; } else { throw new Error('API 返回数据中没有图片 URL'); } } catch (error) { console.error(`[SfiGen] 生成图片失败:`, error); toastr.error(`生成图片失败: ${error.message}`); return null; } } _escapeHtml(unsafe) { return (unsafe || '').replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } _processMessageDOM(messageId) { const messageElement = $(`.mes[mesid="${messageId}"] .mes_text`); if (!messageElement.length) return; // 检查是否已经处理过,如果已经有容器,说明已经处理过了,直接返回 if (messageElement.find('.sfigen-image-container').length > 0) { return; } let html = messageElement.html(); const tag = this.settings.regex_tag || 'sfigen'; let newHtml = html; let hasMatch = false; // 1. 匹配 [tag: prompt] const regexPrompt = new RegExp(`\\[${tag}:\\s*([^\\]]+)\\]`, 'gi'); newHtml = newHtml.replace(regexPrompt, (match, prompt) => { hasMatch = true; const buttonId = `sfigen-btn-${messageId}-${Math.random().toString(36).substr(2, 9)}`; const safePrompt = this._escapeHtml(prompt); const safeMatch = this._escapeHtml(match); return `
`; }); // 2. 匹配 [tag_img: prompt | url1,url2] const regexImg = new RegExp(`\\[${tag}_img:\\s*([^\\]]+)\\]`, 'gi'); newHtml = newHtml.replace(regexImg, (match, content) => { hasMatch = true; let prompt = "未知提示词"; let imageList = []; if (content.includes('|')) { const parts = content.split('|'); prompt = parts[0].trim(); imageList = parts[1].split(',').map(u => u.trim()); } else { imageList = content.split(',').map(u => u.trim()); } const displayUrl = imageList[imageList.length - 1]; const buttonId = `sfigen-btn-${messageId}-${Math.random().toString(36).substr(2, 9)}`; const safePrompt = this._escapeHtml(prompt); const safeMatch = this._escapeHtml(match); let navHtml = ''; if (imageList.length > 1) { navHtml = `
`; imageList.forEach((url, index) => { const isActive = index === imageList.length - 1; navHtml += ``; }); navHtml += `
`; } return `
CG
${navHtml}
`; }); if (hasMatch) { messageElement.html(newHtml); } } _bindEvents() { const handleMessageRender = (messageId) => { setTimeout(() => this._processMessageDOM(messageId), 50); }; eventSource.on(event_types.USER_MESSAGE_RENDERED, handleMessageRender); eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleMessageRender); eventSource.on(event_types.MESSAGE_UPDATED, handleMessageRender); eventSource.on(event_types.MESSAGE_EDITED, handleMessageRender); eventSource.on(event_types.MESSAGE_SWIPED, handleMessageRender); eventSource.on(event_types.CHAT_CHANGED, () => { setTimeout(() => { $('.mes').each((_, el) => { const messageId = $(el).attr('mesid'); if (messageId) { this._processMessageDOM(messageId); } }); }, 500); }); // Initial processing setTimeout(() => { $('.mes').each((_, el) => { const messageId = $(el).attr('mesid'); if (messageId) { this._processMessageDOM(messageId); } }); }, 1000); } _bindButtonsGlobal() { $(document).off('click', '.sfigen-generate-btn'); $(document).on('click', '.sfigen-generate-btn', async (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const btn = $(e.currentTarget); const container = btn.closest('.sfigen-image-container'); const prompt = container.data('prompt'); const messageId = container.data('message-id'); const originalTag = container.data('original-tag'); btn.prop('disabled', true); btn.html(' 生成中...'); const imageUrl = await this._generateImage(prompt); if (imageUrl) { const tag = this.settings.regex_tag || 'sfigen'; let existingUrls = container.data('urls') ? String(container.data('urls')).split(',') : []; existingUrls.push(imageUrl); const urlsString = existingUrls.join(','); const newTag = `[${tag}_img: ${prompt} | ${urlsString}]`; const context = getContext(); const chat = context.chat; if (chat && chat[messageId]) { const message = chat[messageId]; // Fix: Use a more robust replacement strategy // Sometimes originalTag might have been modified by markdown parser // So we replace the whole tag block in the original message const regexPrompt = new RegExp(`\\[${tag}:\\s*([^\\]]+)\\]`, 'gi'); const regexImg = new RegExp(`\\[${tag}_img:\\s*([^\\]]+)\\]`, 'gi'); let replaced = false; // Try exact match first if (message.mes.includes(originalTag)) { message.mes = message.mes.replace(originalTag, newTag); replaced = true; } // If not found, try regex replacement else { message.mes = message.mes.replace(regexImg, (match, content) => { if (content.includes(prompt)) { replaced = true; return newTag; } return match; }); if (!replaced) { message.mes = message.mes.replace(regexPrompt, (match, p) => { if (p.trim() === prompt.trim()) { replaced = true; return newTag; } return match; }); } } if (replaced) { await saveChat(); // 立即在前端替换 DOM,显示生成的图片 let navHtml = ''; if (existingUrls.length > 1) { navHtml = `
`; existingUrls.forEach((url, index) => { const isActive = index === existingUrls.length - 1; navHtml += ``; }); navHtml += `
`; } const newButtonId = `sfigen-btn-${messageId}-${Math.random().toString(36).substr(2, 9)}`; const safePrompt = this._escapeHtml(prompt); const safeNewTag = this._escapeHtml(newTag); const safeUrlsString = this._escapeHtml(urlsString); const safeImageUrl = this._escapeHtml(imageUrl); const finalHtml = `
CG
${navHtml}
`; container.replaceWith(finalHtml); } else { console.warn(`[SfiGen] Could not find tag to replace in message ${messageId}`); toastr.warning('图片已生成,但无法保存到聊天记录中。'); } } } else { btn.prop('disabled', false); btn.html(' 重新生成'); } }); // Image hover and zoom $(document).on('mouseenter', '.sfigen-img-wrapper', function() { $(this).find('.sfigen-img-overlay').css('opacity', '1'); $(this).find('.sfigen-display-img').css('transform', 'scale(1.02)'); }).on('mouseleave', '.sfigen-img-wrapper', function() { $(this).find('.sfigen-img-overlay').css('opacity', '0'); $(this).find('.sfigen-display-img').css('transform', 'scale(1)'); }); $(document).on('click', '.sfigen-img-wrapper', function(e) { e.stopPropagation(); const imgUrl = $(this).find('img').attr('src'); const overlay = $(`
`); $('body').append(overlay); setTimeout(() => { overlay.css('opacity', '1'); overlay.find('img').css('transform', 'scale(1)'); }, 10); overlay.on('click', function() { overlay.css('opacity', '0'); overlay.find('img').css('transform', 'scale(0.9)'); setTimeout(() => overlay.remove(), 300); }); }); // Save image $(document).on('click', '.sfigen-save-btn', async function(e) { e.preventDefault(); e.stopPropagation(); const url = $(this).data('url'); try { const response = await fetch(url); const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = downloadUrl; const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); a.download = `sfigen_${timestamp}.png`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(downloadUrl); document.body.removeChild(a); toastr.success('图片已保存到默认下载目录'); } catch (error) { console.error(`[SfiGen] 保存图片失败:`, error); toastr.error('保存图片失败'); } }); // Nav buttons $(document).on('click', '.sfigen-nav-btn', function(e) { e.preventDefault(); e.stopPropagation(); const btn = $(this); const container = btn.closest('.sfigen-image-container'); const targetUrl = btn.data('url'); container.find('.sfigen-display-img').attr('src', targetUrl); container.find('.sfigen-save-btn').data('url', targetUrl); container.find('.sfigen-nav-btn').css('background-color', 'var(--SmartThemeBorderColor)'); btn.css('background-color', 'var(--SmartThemeQuoteColor)'); }); } _registerSlashCommand() { registerSlashCommand('sfigen', async (args, value) => { if (!value) { toastr.warning('请提供提示词。例如: /sfigen 一个可爱的猫咪'); return; } const imageUrl = await this._generateImage(value); if (imageUrl) { const context = getContext(); const message = `Generated Image`; context.chat.push({ name: 'System', is_user: false, is_system: true, mes: message, send_date: Date.now(), }); await saveChat(); if (typeof window.updateChat === 'function') { window.updateChat(); } else if (typeof window.updateMessageBlock === 'function') { window.updateMessageBlock(context.chat.length - 1, context.chat[context.chat.length - 1]); } else { await reloadCurrentChat(); } } }, [], '使用 SiliconFlow 生成图片', true, true); } }