diff --git a/SL/module/SfiGenModule.js b/SL/module/SfiGenModule.js new file mode 100644 index 0000000..dd8347c --- /dev/null +++ b/SL/module/SfiGenModule.js @@ -0,0 +1,542 @@ +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'; + +const extensionName = 'ST-Amily2-Chat-Optimisation-Dev'; // Use main extension name for settings +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); + } +} diff --git a/SL/module/register-all.js b/SL/module/register-all.js index 5a8a695..0dadda4 100644 --- a/SL/module/register-all.js +++ b/SL/module/register-all.js @@ -20,6 +20,7 @@ import GlossaryModule from './GlossaryModule.js'; import RendererModule from './RendererModule.js'; import SuperMemoryModule from './SuperMemoryModule.js'; import ApiConfigModule from './ApiConfigModule.js'; +import SfiGenModule from './SfiGenModule.js'; export function registerAllModules() { registry.register('AdditionalFeatures', () => new AdditionalFeaturesModule()); @@ -33,4 +34,5 @@ export function registerAllModules() { registry.register('Renderer', () => new RendererModule()); registry.register('SuperMemory', () => new SuperMemoryModule()); registry.register('ApiConfig', () => new ApiConfigModule()); + registry.register('SfiGen', () => new SfiGenModule()); } diff --git a/TODO.md b/TODO.md index 83b3134..6e07454 100644 --- a/TODO.md +++ b/TODO.md @@ -64,6 +64,11 @@ - **分步填表上下文丢失修复**:修复了 `core/table-system/secondary-filler.js` 中 `getHistoryContext` 函数的切片索引错误(Off-by-one error),确保紧挨着目标楼层的那条关键历史消息能够被正确提取并发送给 AI,提供完整的上下文因果关系。 以下为更新内容: +- **硅基生图模块集成**: + - 在“附加功能”面板中新增“硅基生图”入口,与“前端渲染”按钮平行排列。 + - 支持在聊天消息中通过 `[sfigen: 提示词]` 标签一键生成图片,并支持多张图片切换、放大预览和保存到本地。 + - 修复了编辑消息后生图 UI 重复渲染或消失的问题,确保 DOM 更新的稳定性。 + - 修复了图片 URL 无法正确保存到聊天记录的问题。 - **自动构建器优化**: - **多会话管理**:支持创建、切换和删除多个独立的构建会话,方便用户同时进行多个角色的构建任务。 - **状态持久化**:动态规则、聊天记录和任务状态现在会保存在本地存储中,刷新页面或关闭窗口后不会丢失。 diff --git a/assets/amily2-modal.html b/assets/amily2-modal.html index 680e40c..7f16552 100644 --- a/assets/amily2-modal.html +++ b/assets/amily2-modal.html @@ -223,7 +223,10 @@ + +
+
diff --git a/assets/api-config-panel.html b/assets/api-config-panel.html index 735a25f..8f1d1be 100644 --- a/assets/api-config-panel.html +++ b/assets/api-config-panel.html @@ -74,15 +74,14 @@ - -