feat: add custom image API extension
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"Image Auto Generation": "自动生成图片",
|
||||
"Insert In Current Message": "插入当前消息",
|
||||
"Create New Message": "创建新消息",
|
||||
"Image Insert Type": "图片插入类型",
|
||||
"Disable": "禁用",
|
||||
"Enable Custom Image API": "启用自定义图片 API",
|
||||
"Custom Image API Base URL": "自定义图片 API Base URL",
|
||||
"Custom Image Model ID": "自定义图片模型 ID",
|
||||
"Custom Image API Key": "图片 API Key",
|
||||
"Clear Custom Image API Key": "清除图片 API Key",
|
||||
"Custom Image API Key Hint": "留空时使用酒馆的通用 API Key。插件 Key 只保存在当前浏览器,不会打包进插件。",
|
||||
"Image Size": "图片尺寸",
|
||||
"Response Format": "返回格式",
|
||||
"Auto": "自动",
|
||||
"URL": "URL",
|
||||
"Base64 JSON": "Base64 JSON",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"Enable Prompt Injection": "启用提示注入",
|
||||
"Prompt Template": "提示模板",
|
||||
"Regex": "正则表达式",
|
||||
"Inline Replace Mode": "行内替换模式",
|
||||
"Position": "注入位置",
|
||||
"Depth": "深度",
|
||||
"at Depth System": "深度系统消息",
|
||||
"at Depth User": "深度用户消息",
|
||||
"at Depth AI": "深度助手消息"
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
// The main script for the extension
|
||||
// The following are examples of some basic extension functionality
|
||||
|
||||
//You'll likely need to import extension_settings, getContext, and loadExtensionSettings from extensions.js
|
||||
import { extension_settings, getContext } from '../../../extensions.js';
|
||||
//You'll likely need to import some other functions from the main script
|
||||
import {
|
||||
saveSettingsDebounced,
|
||||
eventSource,
|
||||
event_types,
|
||||
updateMessageBlock,
|
||||
ensureMessageMediaIsArray,
|
||||
} from '../../../../script.js';
|
||||
import { appendMediaToMessage } from '../../../../script.js';
|
||||
import { regexFromString } from '../../../utils.js';
|
||||
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
|
||||
import { findSecret, SECRET_KEYS } from '../../../secrets.js';
|
||||
|
||||
// 扩展名称和路径
|
||||
const extensionName = 'st-image-auto-generation';
|
||||
// /scripts/extensions/third-party
|
||||
const extensionFolderPath = `/scripts/extensions/third-party/${extensionName}`;
|
||||
const apiKeyStorageKey = `${extensionName}.apiKey`;
|
||||
|
||||
// 插入类型常量
|
||||
const INSERT_TYPE = {
|
||||
DISABLED: 'disabled',
|
||||
INLINE: 'inline',
|
||||
NEW_MESSAGE: 'new',
|
||||
REPLACE: 'replace',
|
||||
};
|
||||
|
||||
/**
|
||||
* Escapes characters for safe inclusion inside HTML attribute values.
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeHtmlAttribute(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// 默认设置
|
||||
const defaultSettings = {
|
||||
insertType: INSERT_TYPE.DISABLED,
|
||||
customApi: {
|
||||
enabled: true,
|
||||
baseUrl: 'https://freeapi.dgbmc.top/v1',
|
||||
model: 'grok-imagine-image',
|
||||
size: '1024x1024',
|
||||
responseFormat: 'auto',
|
||||
},
|
||||
promptInjection: {
|
||||
enabled: true,
|
||||
prompt: `<image_generation>
|
||||
You must insert a <pic prompt="example prompt"> at end of the reply. Prompts are used for stable diffusion image generation, based on the plot and character to output appropriate prompts to generate captivating images.
|
||||
</image_generation>`,
|
||||
regex: '/<pic[^>]*\\sprompt="([^"]*)"[^>]*?>/g',
|
||||
position: 'deep_system', // deep_system, deep_user, deep_assistant
|
||||
depth: 0, // 0表示添加到末尾,>0表示从末尾往前数第几个位置
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads the API key stored for this extension in the current browser.
|
||||
* It is deliberately kept out of extension_settings so shared settings
|
||||
* exports and plugin packages do not contain the key.
|
||||
* @returns {string}
|
||||
*/
|
||||
function getStoredApiKey() {
|
||||
try {
|
||||
return window.localStorage.getItem(apiKeyStorageKey) || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores or clears the API key for this extension in the current browser.
|
||||
* @param {string} value
|
||||
*/
|
||||
function setStoredApiKey(value) {
|
||||
try {
|
||||
const normalizedValue = String(value || '').trim();
|
||||
if (normalizedValue) {
|
||||
window.localStorage.setItem(apiKeyStorageKey, normalizedValue);
|
||||
} else {
|
||||
window.localStorage.removeItem(apiKeyStorageKey);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[${extensionName}] 无法保存图片 API Key:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 从设置更新UI
|
||||
function updateUI() {
|
||||
const settings = extension_settings[extensionName];
|
||||
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据insertType设置开关状态
|
||||
$('#auto_generation').toggleClass(
|
||||
'selected',
|
||||
settings.insertType !== INSERT_TYPE.DISABLED,
|
||||
);
|
||||
|
||||
// 只在表单元素存在时更新它们
|
||||
if ($('#image_generation_insert_type').length) {
|
||||
$('#image_generation_insert_type').val(
|
||||
settings.insertType,
|
||||
);
|
||||
$('#custom_image_api_enabled').prop('checked', settings.customApi.enabled);
|
||||
$('#custom_image_api_base_url').val(settings.customApi.baseUrl);
|
||||
$('#custom_image_api_model').val(settings.customApi.model);
|
||||
$('#custom_image_api_size').val(settings.customApi.size);
|
||||
$('#custom_image_api_response_format').val(settings.customApi.responseFormat);
|
||||
$('#custom_image_api_key').val(getStoredApiKey());
|
||||
$('#custom_image_api_settings').toggle(settings.customApi.enabled);
|
||||
$('#prompt_injection_enabled').prop(
|
||||
'checked',
|
||||
settings.promptInjection.enabled,
|
||||
);
|
||||
$('#prompt_injection_text').val(
|
||||
settings.promptInjection.prompt,
|
||||
);
|
||||
$('#prompt_injection_regex').val(
|
||||
settings.promptInjection.regex,
|
||||
);
|
||||
$('#prompt_injection_position').val(
|
||||
settings.promptInjection.position,
|
||||
);
|
||||
$('#prompt_injection_depth').val(
|
||||
settings.promptInjection.depth,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
async function loadSettings() {
|
||||
extension_settings[extensionName] = extension_settings[extensionName] || {};
|
||||
const settings = extension_settings[extensionName];
|
||||
|
||||
if (settings.insertType === undefined) {
|
||||
settings.insertType = defaultSettings.insertType;
|
||||
}
|
||||
|
||||
settings.customApi = {
|
||||
...defaultSettings.customApi,
|
||||
...(settings.customApi || {}),
|
||||
};
|
||||
settings.customApi.enabled = settings.customApi.enabled !== false;
|
||||
settings.customApi.baseUrl = String(
|
||||
settings.customApi.baseUrl || defaultSettings.customApi.baseUrl,
|
||||
).trim();
|
||||
settings.customApi.model = String(
|
||||
settings.customApi.model || defaultSettings.customApi.model,
|
||||
).trim();
|
||||
settings.customApi.size = String(
|
||||
settings.customApi.size || defaultSettings.customApi.size,
|
||||
).trim();
|
||||
if (!['auto', 'url', 'b64_json'].includes(settings.customApi.responseFormat)) {
|
||||
settings.customApi.responseFormat = defaultSettings.customApi.responseFormat;
|
||||
}
|
||||
|
||||
settings.promptInjection = {
|
||||
...defaultSettings.promptInjection,
|
||||
...(settings.promptInjection || {}),
|
||||
};
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// 创建设置页面
|
||||
async function createSettings(settingsHtml) {
|
||||
// 创建一个容器来存放设置,确保其正确显示在扩展设置面板中
|
||||
if (!$('#image_auto_generation_container').length) {
|
||||
$('#extensions_settings2').append(
|
||||
'<div id="image_auto_generation_container" class="extension_container"></div>',
|
||||
);
|
||||
}
|
||||
|
||||
// 使用传入的settingsHtml而不是重新获取
|
||||
$('#image_auto_generation_container').empty().append(settingsHtml);
|
||||
|
||||
// 添加设置变更事件处理
|
||||
$('#image_generation_insert_type').on('change', function () {
|
||||
const newValue = $(this).val();
|
||||
extension_settings[extensionName].insertType = newValue;
|
||||
updateUI();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#custom_image_api_enabled').on('change', function () {
|
||||
extension_settings[extensionName].customApi.enabled = $(this).prop('checked');
|
||||
updateUI();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#custom_image_api_base_url').on('input', function () {
|
||||
extension_settings[extensionName].customApi.baseUrl = String($(this).val()).trim();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#custom_image_api_model').on('input', function () {
|
||||
extension_settings[extensionName].customApi.model = String($(this).val()).trim();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#custom_image_api_size').on('change', function () {
|
||||
extension_settings[extensionName].customApi.size = String($(this).val()).trim();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#custom_image_api_response_format').on('change', function () {
|
||||
extension_settings[extensionName].customApi.responseFormat = $(this).val();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#custom_image_api_key').on('input', function () {
|
||||
setStoredApiKey($(this).val());
|
||||
});
|
||||
|
||||
$('#custom_image_api_key_clear').on('click', function () {
|
||||
setStoredApiKey('');
|
||||
$('#custom_image_api_key').val('');
|
||||
toastr.success('图片 API Key 已清除');
|
||||
});
|
||||
|
||||
// 添加提示词注入设置的事件处理
|
||||
$('#prompt_injection_enabled').on('change', function () {
|
||||
extension_settings[extensionName].promptInjection.enabled =
|
||||
$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#prompt_injection_text').on('input', function () {
|
||||
extension_settings[extensionName].promptInjection.prompt =
|
||||
$(this).val();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#prompt_injection_regex').on('input', function () {
|
||||
extension_settings[extensionName].promptInjection.regex = $(this).val();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#prompt_injection_position').on('change', function () {
|
||||
extension_settings[extensionName].promptInjection.position =
|
||||
$(this).val();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
// 深度设置事件处理
|
||||
$('#prompt_injection_depth').on('input', function () {
|
||||
const value = parseInt(String($(this).val()));
|
||||
extension_settings[extensionName].promptInjection.depth = isNaN(value)
|
||||
? 0
|
||||
: value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
// 初始化设置值
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// 设置变更处理函数
|
||||
function onExtensionButtonClick() {
|
||||
// 直接访问扩展设置面板
|
||||
const extensionsDrawer = $('#extensions-settings-button .drawer-toggle');
|
||||
|
||||
// 如果抽屉是关闭的,点击打开它
|
||||
if ($('#rm_extensions_block').hasClass('closedDrawer')) {
|
||||
extensionsDrawer.trigger('click');
|
||||
}
|
||||
|
||||
// 等待抽屉打开后滚动到我们的设置容器
|
||||
setTimeout(() => {
|
||||
// 找到我们的设置容器
|
||||
const container = $('#image_auto_generation_container');
|
||||
if (container.length) {
|
||||
// 滚动到设置面板位置
|
||||
$('#rm_extensions_block').animate(
|
||||
{
|
||||
scrollTop:
|
||||
container.offset().top -
|
||||
$('#rm_extensions_block').offset().top +
|
||||
$('#rm_extensions_block').scrollTop(),
|
||||
},
|
||||
500,
|
||||
);
|
||||
|
||||
// 使用SillyTavern原生的抽屉展开方式
|
||||
// 检查抽屉内容是否可见
|
||||
const drawerContent = container.find('.inline-drawer-content');
|
||||
const drawerHeader = container.find('.inline-drawer-header');
|
||||
|
||||
// 只有当内容被隐藏时才触发展开
|
||||
if (drawerContent.is(':hidden') && drawerHeader.length) {
|
||||
// 直接使用原生点击事件触发,而不做任何内部处理
|
||||
drawerHeader.trigger('click');
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 初始化扩展
|
||||
$(function () {
|
||||
(async function () {
|
||||
// 获取设置HTML (只获取一次)
|
||||
const settingsHtml = await $.get(
|
||||
`${extensionFolderPath}/settings.html`,
|
||||
);
|
||||
|
||||
// 添加扩展到菜单
|
||||
$('#extensionsMenu')
|
||||
.append(`<div id="auto_generation" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-robot"></div>
|
||||
<span data-i18n="Image Auto Generation">Image Auto Generation</span>
|
||||
</div>`);
|
||||
|
||||
// 修改点击事件,打开设置面板而不是切换状态
|
||||
$('#auto_generation').off('click').on('click', onExtensionButtonClick);
|
||||
|
||||
await loadSettings();
|
||||
|
||||
// 创建设置 - 将获取的HTML传递给createSettings
|
||||
await createSettings(settingsHtml);
|
||||
|
||||
// 确保设置面板可见时,设置值是正确的
|
||||
$('#extensions-settings-button').on('click', function () {
|
||||
setTimeout(() => {
|
||||
updateUI();
|
||||
}, 200);
|
||||
});
|
||||
})();
|
||||
});
|
||||
// 获取消息角色
|
||||
function getMesRole() {
|
||||
// 确保对象路径存在
|
||||
if (
|
||||
!extension_settings[extensionName] ||
|
||||
!extension_settings[extensionName].promptInjection ||
|
||||
!extension_settings[extensionName].promptInjection.position
|
||||
) {
|
||||
return 'system'; // 默认返回system角色
|
||||
}
|
||||
|
||||
switch (extension_settings[extensionName].promptInjection.position) {
|
||||
case 'deep_system':
|
||||
return 'system';
|
||||
case 'deep_user':
|
||||
return 'user';
|
||||
case 'deep_assistant':
|
||||
return 'assistant';
|
||||
default:
|
||||
return 'system';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an OpenAI-compatible image generation endpoint from a base URL.
|
||||
* A full /images/generations URL is also accepted for convenience.
|
||||
* @param {string} baseUrl
|
||||
* @returns {string}
|
||||
*/
|
||||
function getImageGenerationUrl(baseUrl) {
|
||||
const normalizedBaseUrl = String(baseUrl || '').trim().replace(/\/+$/, '');
|
||||
if (!normalizedBaseUrl) {
|
||||
throw new Error('自定义图片 API 地址不能为空');
|
||||
}
|
||||
|
||||
return /\/images\/generations$/i.test(normalizedBaseUrl)
|
||||
? normalizedBaseUrl
|
||||
: `${normalizedBaseUrl}/images/generations`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an error body without leaking the API key into the error message.
|
||||
* @param {Response} response
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getImageApiError(response) {
|
||||
const text = await response.text();
|
||||
if (!text) {
|
||||
return `${response.status} ${response.statusText}`.trim();
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(text);
|
||||
return (
|
||||
data?.error?.message ||
|
||||
(typeof data?.error === 'string' ? data.error : '') ||
|
||||
data?.message ||
|
||||
text
|
||||
);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a browser-loadable image URL from an OpenAI-compatible response.
|
||||
* @param {any} data
|
||||
* @returns {string}
|
||||
*/
|
||||
function extractImageUrl(data) {
|
||||
const item = Array.isArray(data?.data)
|
||||
? data.data[0]
|
||||
: Array.isArray(data?.images)
|
||||
? data.images[0]
|
||||
: data?.data;
|
||||
|
||||
if (typeof item === 'string' && item.trim()) {
|
||||
return item.trim();
|
||||
}
|
||||
|
||||
const imageUrl = item?.url || item?.image_url || data?.url;
|
||||
if (typeof imageUrl === 'string' && imageUrl.trim()) {
|
||||
return imageUrl.trim();
|
||||
}
|
||||
|
||||
const base64 = item?.b64_json || item?.base64 || data?.b64_json;
|
||||
if (typeof base64 === 'string' && base64.trim()) {
|
||||
if (base64.startsWith('data:')) {
|
||||
return base64;
|
||||
}
|
||||
|
||||
const format = String(item?.format || data?.format || 'png')
|
||||
.replace(/[^a-z0-9.+-]/gi, '')
|
||||
.toLowerCase() || 'png';
|
||||
return `data:image/${format};base64,${base64}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an image through a user-configured OpenAI-compatible endpoint.
|
||||
* @param {string} prompt
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function generateCustomImage(prompt) {
|
||||
const customApi = extension_settings[extensionName].customApi;
|
||||
const apiKey = getStoredApiKey() || await findSecret(SECRET_KEYS.GENERIC);
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const model = String(customApi.model || '').trim();
|
||||
const size = String(customApi.size || '').trim();
|
||||
|
||||
if (!model) {
|
||||
throw new Error('自定义图片模型 ID 不能为空');
|
||||
}
|
||||
|
||||
if (!size) {
|
||||
throw new Error('图片尺寸不能为空');
|
||||
}
|
||||
|
||||
if (apiKey) {
|
||||
const normalizedKey = String(apiKey).trim();
|
||||
headers.Authorization = /^bearer\s/i.test(normalizedKey)
|
||||
? normalizedKey
|
||||
: `Bearer ${normalizedKey}`;
|
||||
}
|
||||
|
||||
const body = {
|
||||
prompt: prompt.trim(),
|
||||
model,
|
||||
size,
|
||||
n: 1,
|
||||
};
|
||||
|
||||
if (customApi.responseFormat !== 'auto') {
|
||||
body.response_format = customApi.responseFormat;
|
||||
}
|
||||
|
||||
const response = await fetch(getImageGenerationUrl(customApi.baseUrl), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await getImageApiError(response));
|
||||
}
|
||||
|
||||
const imageUrl = extractImageUrl(await response.json());
|
||||
if (!imageUrl) {
|
||||
throw new Error('图片 API 返回中没有找到 url 或 b64_json');
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an image using either the custom endpoint or SillyTavern's native
|
||||
* stable-diffusion command.
|
||||
* @param {string} prompt
|
||||
* @param {string} insertType
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function generateImage(prompt, insertType) {
|
||||
const settings = extension_settings[extensionName];
|
||||
if (settings.customApi.enabled) {
|
||||
return await generateCustomImage(prompt);
|
||||
}
|
||||
|
||||
const command = SlashCommandParser.commands['sd'];
|
||||
if (!command?.callback) {
|
||||
throw new Error('酒馆图片生成命令尚未加载,请刷新页面后重试');
|
||||
}
|
||||
|
||||
return await command.callback(
|
||||
{
|
||||
quiet: insertType === INSERT_TYPE.NEW_MESSAGE ? 'false' : 'true',
|
||||
},
|
||||
prompt,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom-API result as a new assistant message.
|
||||
* @param {any} context
|
||||
* @param {string} prompt
|
||||
* @param {string} imageUrl
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function addImageMessage(context, prompt, imageUrl) {
|
||||
const message = {
|
||||
name: context.name2 || 'Image Generation',
|
||||
is_user: false,
|
||||
is_system: false,
|
||||
send_date: new Date().toISOString(),
|
||||
mes: '',
|
||||
extra: {
|
||||
media: [
|
||||
{
|
||||
type: 'image',
|
||||
url: imageUrl,
|
||||
title: prompt,
|
||||
source: 'generated',
|
||||
},
|
||||
],
|
||||
media_display: 'gallery',
|
||||
media_index: 0,
|
||||
inline_image: false,
|
||||
},
|
||||
};
|
||||
|
||||
context.chat.push(message);
|
||||
const messageId = context.chat.length - 1;
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId, 'extension');
|
||||
context.addOneMessage(message);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension');
|
||||
await context.saveChat();
|
||||
}
|
||||
|
||||
// 监听CHAT_COMPLETION_PROMPT_READY事件以注入提示词
|
||||
eventSource.on(
|
||||
event_types.CHAT_COMPLETION_PROMPT_READY,
|
||||
async function (eventData) {
|
||||
try {
|
||||
// 确保设置对象和promptInjection对象都存在
|
||||
if (
|
||||
!extension_settings[extensionName] ||
|
||||
!extension_settings[extensionName].promptInjection ||
|
||||
!extension_settings[extensionName].promptInjection.enabled ||
|
||||
extension_settings[extensionName].insertType ===
|
||||
INSERT_TYPE.DISABLED
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt =
|
||||
extension_settings[extensionName].promptInjection.prompt;
|
||||
const depth =
|
||||
extension_settings[extensionName].promptInjection.depth || 0;
|
||||
const role = getMesRole();
|
||||
|
||||
console.log(
|
||||
`[${extensionName}] 准备注入提示词: 角色=${role}, 深度=${depth}`,
|
||||
);
|
||||
console.log(
|
||||
`[${extensionName}] 提示词内容: ${prompt.substring(0, 50)}...`,
|
||||
);
|
||||
|
||||
// 根据depth参数决定插入位置
|
||||
if (depth === 0) {
|
||||
// 添加到末尾
|
||||
eventData.chat.push({ role: role, content: prompt });
|
||||
console.log(`[${extensionName}] 提示词已添加到聊天末尾`);
|
||||
} else {
|
||||
// 从末尾向前插入
|
||||
eventData.chat.splice(-depth, 0, {
|
||||
role: role,
|
||||
content: prompt,
|
||||
});
|
||||
console.log(
|
||||
`[${extensionName}] 提示词已插入到聊天中,从末尾往前第 ${depth} 个位置`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[${extensionName}] 提示词注入错误:`, error);
|
||||
toastr.error(`提示词注入错误: ${error}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听消息接收事件
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, handleIncomingMessage);
|
||||
async function handleIncomingMessage() {
|
||||
// 确保设置对象存在
|
||||
if (
|
||||
!extension_settings[extensionName] ||
|
||||
extension_settings[extensionName].insertType === INSERT_TYPE.DISABLED
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
const message = context.chat[context.chat.length - 1];
|
||||
|
||||
// 检查是否是AI消息
|
||||
if (!message || message.is_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保promptInjection对象和regex属性存在
|
||||
if (
|
||||
!extension_settings[extensionName].promptInjection ||
|
||||
!extension_settings[extensionName].promptInjection.regex ||
|
||||
typeof message.mes !== 'string'
|
||||
) {
|
||||
console.error('Prompt injection settings not properly initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用正则表达式search
|
||||
const imgTagRegex = regexFromString(
|
||||
extension_settings[extensionName].promptInjection.regex,
|
||||
);
|
||||
// const testRegex = regexFromString(extension_settings[extensionName].promptInjection.regex);
|
||||
let matches;
|
||||
if (imgTagRegex.global) {
|
||||
matches = [...message.mes.matchAll(imgTagRegex)];
|
||||
} else {
|
||||
const singleMatch = message.mes.match(imgTagRegex);
|
||||
matches = singleMatch ? [singleMatch] : [];
|
||||
}
|
||||
console.log(imgTagRegex, matches);
|
||||
if (matches.length > 0) {
|
||||
// 延迟执行图片生成,确保消息首先显示出来
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
toastr.info(`Generating ${matches.length} images...`);
|
||||
const insertType = extension_settings[extensionName].insertType;
|
||||
|
||||
const messageId = context.chat.indexOf(message);
|
||||
if (messageId < 0 || context.chat[messageId] !== message) {
|
||||
throw new Error('当前聊天已发生变化,已取消图片插入');
|
||||
}
|
||||
|
||||
// 在当前消息中插入图片
|
||||
if (!message.extra) {
|
||||
message.extra = {};
|
||||
}
|
||||
ensureMessageMediaIsArray(message);
|
||||
if (!Array.isArray(message.extra.media)) {
|
||||
message.extra.media = [];
|
||||
}
|
||||
|
||||
// 获取消息元素用于稍后更新
|
||||
const messageElement = $(`.mes[mesid="${messageId}"]`);
|
||||
|
||||
// 处理每个匹配的图片标签
|
||||
for (const match of matches) {
|
||||
const prompt =
|
||||
typeof match?.[1] === 'string' ? match[1] : '';
|
||||
if (!prompt.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const imageUrl = await generateImage(prompt, insertType);
|
||||
if (typeof imageUrl !== 'string' || !imageUrl.trim()) {
|
||||
throw new Error('图片生成接口返回了空结果');
|
||||
}
|
||||
|
||||
const customApiEnabled = extension_settings[extensionName].customApi.enabled;
|
||||
|
||||
if (insertType === INSERT_TYPE.NEW_MESSAGE) {
|
||||
// Native /sd already creates the new message when quiet=false.
|
||||
if (customApiEnabled) {
|
||||
await addImageMessage(context, prompt, imageUrl);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 统一插入到extra里
|
||||
if (insertType === INSERT_TYPE.INLINE) {
|
||||
message.extra.media.push({
|
||||
type: 'image',
|
||||
url: imageUrl,
|
||||
title: prompt,
|
||||
source: 'generated',
|
||||
});
|
||||
message.extra.media_display = 'gallery';
|
||||
message.extra.media_index = message.extra.media.length - 1;
|
||||
message.extra.inline_image = true;
|
||||
|
||||
// 更新UI
|
||||
appendMediaToMessage(message, messageElement);
|
||||
|
||||
// 保存聊天记录
|
||||
await context.saveChat();
|
||||
} else if (insertType === INSERT_TYPE.REPLACE) {
|
||||
// Find the original image tag in the message
|
||||
const originalTag =
|
||||
typeof match?.[0] === 'string' ? match[0] : '';
|
||||
if (!originalTag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace it with an actual image tag
|
||||
const escapedUrl = escapeHtmlAttribute(imageUrl);
|
||||
const escapedPrompt = escapeHtmlAttribute(prompt);
|
||||
const newImageTag = `<img src="${escapedUrl}" title="${escapedPrompt}" alt="${escapedPrompt}">`;
|
||||
message.mes = message.mes.replace(originalTag, newImageTag);
|
||||
|
||||
// Update the message display using updateMessageBlock
|
||||
updateMessageBlock(messageId, message);
|
||||
await eventSource.emit(event_types.MESSAGE_UPDATED, messageId);
|
||||
|
||||
// Save the chat
|
||||
await context.saveChat();
|
||||
}
|
||||
}
|
||||
toastr.success(
|
||||
`${matches.length} images generated successfully`,
|
||||
);
|
||||
} catch (error) {
|
||||
toastr.error(`Image generation error: ${error}`);
|
||||
console.error('Image generation error:', error);
|
||||
}
|
||||
}, 0); //防阻塞UI渲染
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"display_name": "Image Auto Generation - Custom API",
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"author": "wickedcode01",
|
||||
"version": "1.1.0",
|
||||
"homePage": "https://github.com/wickedcode01/st-image-auto-generation",
|
||||
"auto_update": false,
|
||||
"i18n": {
|
||||
"zh-cn": "i18n/zh-cn.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b data-i18n="Image Auto Generation">Image Auto Generation</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<div class="flex-container flexnowrap">
|
||||
<label for="image_generation_insert_type" class="flex1" data-i18n="Image Insert Type">Image Insert Type</label>
|
||||
<select id="image_generation_insert_type" class="text_pole widthNatural margin0">
|
||||
<option data-i18n="Insert In Current Message" value="inline">Insert In Current Message</option>
|
||||
<option data-i18n="Inline Replace Mode" value="replace">Inline Replace Mode</option>
|
||||
<option data-i18n="Create New Message" value="new">Create New Message</option>
|
||||
<option data-i18n="Disable" value="disabled">Disable</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-container flexnowrap">
|
||||
<label class="checkbox_label" for="custom_image_api_enabled">
|
||||
<span data-i18n="Enable Custom Image API">Enable Custom Image API</span>
|
||||
<input type="checkbox" id="custom_image_api_enabled" class="checkbox">
|
||||
</label>
|
||||
</div>
|
||||
<div id="custom_image_api_settings">
|
||||
<div>
|
||||
<label for="custom_image_api_base_url" class="flex1" data-i18n="Custom Image API Base URL">Custom Image API Base URL</label>
|
||||
<input id="custom_image_api_base_url" class="text_pole" type="url" placeholder="https://example.com/v1">
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom_image_api_model" class="flex1" data-i18n="Custom Image Model ID">Custom Image Model ID</label>
|
||||
<input id="custom_image_api_model" class="text_pole" type="text" placeholder="grok-imagine-image">
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom_image_api_key" class="flex1" data-i18n="Custom Image API Key">Custom Image API Key</label>
|
||||
<div class="flex-container flexnowrap">
|
||||
<input id="custom_image_api_key" class="text_pole flex1" type="password" autocomplete="off" placeholder="sk-...">
|
||||
<button id="custom_image_api_key_clear" type="button" class="menu_button menu_button_icon" data-i18n="[title]Clear Custom Image API Key" title="Clear Custom Image API Key" aria-label="Clear Custom Image API Key">
|
||||
<i class="fa-solid fa-eraser"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small data-i18n="Custom Image API Key Hint">留空时使用酒馆的通用 API Key。插件 Key 只保存在当前浏览器,不会打包进插件。</small>
|
||||
</div>
|
||||
<div class="flex-container flexnowrap">
|
||||
<label for="custom_image_api_size" class="flex1" data-i18n="Image Size">Image Size</label>
|
||||
<input id="custom_image_api_size" class="text_pole widthUnset" type="text" placeholder="1024x1024">
|
||||
</div>
|
||||
<div class="flex-container flexnowrap">
|
||||
<label for="custom_image_api_response_format" class="flex1" data-i18n="Response Format">Response Format</label>
|
||||
<select id="custom_image_api_response_format" class="text_pole widthNatural margin0">
|
||||
<option value="auto" data-i18n="Auto">Auto</option>
|
||||
<option value="url" data-i18n="URL">URL</option>
|
||||
<option value="b64_json" data-i18n="Base64 JSON">Base64 JSON</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container flexnowrap">
|
||||
<label class="checkbox_label" for="prompt_injection_enabled">
|
||||
<span data-i18n="Enable Prompt Injection">Enable Prompt Injection</span>
|
||||
<input type="checkbox" id="prompt_injection_enabled" class="checkbox">
|
||||
</label>
|
||||
</div>
|
||||
<div class="">
|
||||
<div><label for="prompt_injection_text" class="flex1" data-i18n="Prompt Template">Prompt Template</label></div>
|
||||
<textarea id="prompt_injection_text" class="text_pole textarea_compact" rows="3" placeholder="Prompt that instruct AI to generate image tag"></textarea>
|
||||
</div>
|
||||
<div class="">
|
||||
<div><label for="prompt_injection_regex" class="flex1" data-i18n="Regex">Regex</label></div>
|
||||
<textarea id="prompt_injection_regex" class="text_pole textarea_compact" rows="2" placeholder="Your regex here"></textarea>
|
||||
</div>
|
||||
<div class="flex-container flexnowrap">
|
||||
<label for="prompt_injection_position" class="flex1" data-i18n="Position">Position</label>
|
||||
<select id="prompt_injection_position" class="text_pole widthNatural margin0">
|
||||
<option value="deep_system" data-i18n="at Depth System">Deep System</option>
|
||||
<option value="deep_user" data-i18n="at Depth User">Deep User</option>
|
||||
<option value="deep_assistant" data-i18n="at Depth AI">Deep Assistant</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-container flexnowrap">
|
||||
<label for="prompt_injection_depth" class="flex1" data-i18n="Depth">Depth</label>
|
||||
<input id="prompt_injection_depth" class="text_pole widthUnset" type="number" min="0" max="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user