ci: auto build & obfuscate [2026-04-06 19:02:37] (Jenkins #10)

This commit is contained in:
Jenkins CI
2026-04-06 19:02:37 +08:00
parent 0421e44e0f
commit 0c5ac2c70b
13 changed files with 694 additions and 82 deletions

542
SL/module/SfiGenModule.js Normal file
View File

@@ -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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
_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 `<div class="sfigen-image-container" data-message-id="${messageId}" data-prompt="${safePrompt}" data-original-tag="${safeMatch}" style="width: 96%; max-width: 600px; background: var(--SmartThemeBlurTintColor); border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden; margin: 20px auto; padding: 15px; text-align: center; position: relative; z-index: 10;"><button id="${buttonId}" class="sfigen-generate-btn" style="background-color: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); border: 1px solid var(--SmartThemeBorderColor); padding: 8px 20px; border-radius: 8px; cursor: pointer; pointer-events: auto; display: inline-block; font-weight: bold; transition: all 0.2s;"><i class="fa-solid fa-image"></i> 生成图片</button></div>`;
});
// 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 = `<div style="display: flex; justify-content: center; gap: 10px; margin-top: 10px;">`;
imageList.forEach((url, index) => {
const isActive = index === imageList.length - 1;
navHtml += `<button class="sfigen-nav-btn" data-url="${this._escapeHtml(url)}" style="width: 12px; height: 12px; border-radius: 50%; border: none; background-color: ${isActive ? 'var(--SmartThemeQuoteColor)' : 'var(--SmartThemeBorderColor)'}; cursor: pointer; padding: 0;"></button>`;
});
navHtml += `</div>`;
}
return `<div class="sfigen-image-container" data-message-id="${messageId}" data-prompt="${safePrompt}" data-original-tag="${safeMatch}" data-urls="${this._escapeHtml(imageList.join(','))}" style="width: 96%; max-width: 600px; background: var(--SmartThemeBlurTintColor); border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden; margin: 20px auto; padding: 15px; text-align: center; position: relative; z-index: 10;">
<div style="width: calc(100% - 4px); margin: 2px auto 15px auto; border: 2px solid rgba(0,0,0,0.15); border-radius: 8px; overflow: hidden; position: relative; cursor: pointer;" class="sfigen-img-wrapper">
<img src="${this._escapeHtml(displayUrl)}" class="sfigen-display-img" style="width: 100%; display: block; transition: transform 0.3s;" alt="CG" title="点击放大">
<div class="sfigen-img-overlay" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); display: flex; justify-content: center; align-items: center; opacity: 0; transition: opacity 0.2s;">
<i class="fa-solid fa-magnifying-glass-plus" style="color: white; font-size: 2em;"></i>
</div>
</div>
${navHtml}
<div style="display: flex; justify-content: center; gap: 10px; margin-top: 15px;">
<button id="${buttonId}" class="sfigen-generate-btn" style="background-color: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); border: 1px solid var(--SmartThemeBorderColor); padding: 8px 20px; border-radius: 8px; cursor: pointer; pointer-events: auto; display: inline-block; font-weight: bold; transition: all 0.2s;"><i class="fa-solid fa-rotate-right"></i> 再次生成</button>
<button class="sfigen-save-btn" data-url="${this._escapeHtml(displayUrl)}" style="background-color: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); border: 1px solid var(--SmartThemeBorderColor); padding: 8px 20px; border-radius: 8px; cursor: pointer; pointer-events: auto; display: inline-block; font-weight: bold; transition: all 0.2s;"><i class="fa-solid fa-download"></i> 保存图片</button>
</div>
</div>`;
});
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('<i class="fa-solid fa-spinner fa-spin"></i> 生成中...');
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 = `<div style="display: flex; justify-content: center; gap: 10px; margin-top: 10px;">`;
existingUrls.forEach((url, index) => {
const isActive = index === existingUrls.length - 1;
navHtml += `<button class="sfigen-nav-btn" data-url="${this._escapeHtml(url)}" style="width: 12px; height: 12px; border-radius: 50%; border: none; background-color: ${isActive ? 'var(--SmartThemeQuoteColor)' : 'var(--SmartThemeBorderColor)'}; cursor: pointer; padding: 0;"></button>`;
});
navHtml += `</div>`;
}
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 = `<div class="sfigen-image-container" data-message-id="${messageId}" data-prompt="${safePrompt}" data-original-tag="${safeNewTag}" data-urls="${safeUrlsString}" style="width: 96%; max-width: 600px; background: var(--SmartThemeBlurTintColor); border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden; margin: 20px auto; padding: 15px; text-align: center; position: relative; z-index: 10;">
<div style="width: calc(100% - 4px); margin: 2px auto 15px auto; border: 2px solid rgba(0,0,0,0.15); border-radius: 8px; overflow: hidden; position: relative; cursor: pointer;" class="sfigen-img-wrapper">
<img src="${safeImageUrl}" class="sfigen-display-img" style="width: 100%; display: block; transition: transform 0.3s;" alt="CG" title="点击放大">
<div class="sfigen-img-overlay" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); display: flex; justify-content: center; align-items: center; opacity: 0; transition: opacity 0.2s;">
<i class="fa-solid fa-magnifying-glass-plus" style="color: white; font-size: 2em;"></i>
</div>
</div>
${navHtml}
<div style="display: flex; justify-content: center; gap: 10px; margin-top: 15px;">
<button id="${newButtonId}" class="sfigen-generate-btn" style="background-color: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); border: 1px solid var(--SmartThemeBorderColor); padding: 8px 20px; border-radius: 8px; cursor: pointer; pointer-events: auto; display: inline-block; font-weight: bold; transition: all 0.2s;"><i class="fa-solid fa-rotate-right"></i> 再次生成</button>
<button class="sfigen-save-btn" data-url="${safeImageUrl}" style="background-color: var(--SmartThemeQuoteColor); color: var(--SmartThemeBodyColor); border: 1px solid var(--SmartThemeBorderColor); padding: 8px 20px; border-radius: 8px; cursor: pointer; pointer-events: auto; display: inline-block; font-weight: bold; transition: all 0.2s;"><i class="fa-solid fa-download"></i> 保存图片</button>
</div>
</div>`;
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('<i class="fa-solid fa-image"></i> 重新生成');
}
});
// 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 = $(`
<div id="sfigen-zoom-overlay" style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.9); z-index: 9999; display: flex; justify-content: center; align-items: center; cursor: zoom-out; opacity: 0; transition: opacity 0.3s;">
<img src="${imgUrl}" style="max-width: 95%; max-height: 95%; object-fit: contain; border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.5); transform: scale(0.9); transition: transform 0.3s;">
<div style="position: absolute; top: 20px; right: 20px; color: white; font-size: 24px; cursor: pointer;"><i class="fa-solid fa-xmark"></i></div>
</div>
`);
$('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 = `<img src="${imageUrl}" alt="Generated Image" style="max-width: 100%; border-radius: 8px;" />`;
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);
}
}

View File

@@ -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());
}