crystelf-plugin/lib/ai/imageProcessor.js

542 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axios from 'axios';
class ImageProcessor {
constructor() {
this.isInitialized = false;
this.config = null;
}
init(config) {
try {
this.config = config;
this.isInitialized = true;
} catch (error) {
logger.error(`[crystelf-ai] 图像处理器初始化失败: ${error.message}`);
throw error;
}
}
/**
* 生成或编辑图像
* @param {string} prompt - 图像描述
* @param {boolean} editMode - 是否为编辑模式
* @param {string|null} sourceImageArr - 源图像URL数组
* @param {Object} config - 配置对象
* @returns {Promise<Object>} 处理结果
*/
async generateOrEditImage(prompt, editMode = false, sourceImageArr = [], config = this.config) {
if (!this.isInitialized && !config) {
return {
success: false,
error: '图像处理器未初始化'
};
}
try {
const mergedConfig = this.mergeImageConfig(config || this.config);
if (editMode && sourceImageArr) {
return await this.editImage(prompt, sourceImageArr, mergedConfig);
} else {
return await this.generateImage(prompt, mergedConfig);
}
} catch (error) {
logger.error(`[crystelf-ai] 图像处理失败: ${error.message}`);
return {
success: false,
error: error.message
};
}
}
/**
* 生成图像 - 根据imageMode选择不同的调用方式
* @param {string} prompt - 图像描述
* @param {Object} config - 配置对象
* @returns {Promise<Object>} 生成结果
*/
async generateImage(prompt, config) {
try {
if (config.imageMode === 'chat') {
return await this.generateImageByChat(prompt, config);
} else {
return await this.generateImageByOpenAI(prompt, config);
}
} catch (error) {
logger.error(`[crystelf-ai] 图像生成失败: ${error.message}`);
return {
success: false,
error: `图像生成失败: ${error.message}`
};
}
}
/**
* 使用OpenAI标准接口生成图像
* @param {string} prompt - 图像描述
* @param {Object} config - 配置对象
* @returns {Promise<Object>} 生成结果
*/
async generateImageByOpenAI(prompt, config) {
try {
logger.info(`[crystelf-ai] 使用OpenAI接口生成图像: ${prompt}`);
const requestBody = {
prompt: prompt,
model: config.model || 'dall-e-3',
n: config.n || 1,
size: config.size || '1024x1024',
quality: config.quality || 'standard',
style: config.style || 'vivid',
response_format: config.responseFormat || 'url',
};
const response = await axios.post(
`${config.baseApi}/v1/images/generations`,
requestBody,
{
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json'
},
timeout: config.timeout || 60000
}
);
if (response.data && response.data.data && response.data.data.length > 0) {
const imageData = response.data.data[0];
const imageUrl = imageData.url || imageData.b64_json;
logger.info(`[crystelf-ai] OpenAI接口图像生成成功: ${imageUrl ? 'URL' : 'Base64数据'}`);
return {
success: true,
imageUrl: imageUrl,
revisedPrompt: imageData.revised_prompt,
description: prompt,
model: config.model || 'Qwen/Qwen-Image',
rawResponse: response.data
};
} else {
logger.error(`[crystelf-ai] 无效的API响应格式: ${JSON.stringify(response.data)}`);
return {
success: false,
error: '无效的API响应格式'
};
}
} catch (error) {
logger.error(`[crystelf-ai] OpenAI接口图像生成失败: ${error.message}`);
return {
success: false,
error: `OpenAI接口图像生成失败: ${error.message}`
};
}
}
/**
* 使用对话式接口生成图像如gemini-3-pro-image-preview
* @param {string} prompt - 图像描述
* @param {Object} config - 配置对象
* @returns {Promise<Object>} 生成结果
*/
async generateImageByChat(prompt, config) {
try {
logger.info(`[crystelf-ai] 使用对话接口生成图像: ${prompt}`);
const messages = [
{
role: 'system',
content: [
{type: 'text', text: '请你根据用户的描述生成高质量且准确的图像,条件允许的情况下,请先思考用户的意图再生成图像,请直接返回图像url,不要任何其他内容'}
]
},
{
role: 'user',
content: [
{type: 'text', text: prompt}
]
}
];
const requestBody = {
model: config.model || 'google/gemini-3-pro-image-preview',
messages: messages,
temperature: config.temperature || 0.7,
modalities: config.modalities || ['text', 'image'],
};
const response = await axios.post(
`${config.baseApi}/v1/chat/completions`,
requestBody,
{
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json'
},
timeout: config.timeout || 60000
}
);
if (response.data && response.data.choices && response.data.choices.length > 0) {
const choice = response.data.choices[0];
if (choice.message && choice.message.images && choice.message.images.length > 0) {
const imageData = choice.message.images[0];
const imageUrl = imageData.image_url ? imageData.image_url.url : null;
if (imageUrl) {
logger.info(`[crystelf-ai] 对话接口图像生成成功: ${imageUrl.substring(0, 50)}...`);
return {
success: true,
imageUrl: imageUrl,
description: prompt,
model: config.model || 'google/gemini-3-pro-image-preview',
rawResponse: response.data
};
}
}
if (choice.message && choice.message.content) {
const imageUrl = this.extractImageUrl(choice.message.content);
if (imageUrl) {
logger.info(`[crystelf-ai] 从响应内容中提取到图像URL: ${imageUrl}`);
return {
success: true,
imageUrl: imageUrl,
description: prompt,
model: config.model || 'google/gemini-3-pro-image-preview',
rawResponse: response.data
};
} else {
logger.info(`[crystelf-ai] 收到文本响应: ${choice.message.content}`);
return {
success: true,
response: choice.message.content,
description: prompt,
model: config.model || 'google/gemini-3-pro-image-preview',
rawResponse: response.data
};
}
}
} else {
logger.error(`[crystelf-ai] 无效的API响应格式: ${JSON.stringify(response.data)}`);
return {
success: false,
error: '无效的API响应格式'
};
}
} catch (error) {
logger.error(`[crystelf-ai] 对话接口图像生成失败: ${error.message}`);
return {
success: false,
error: `对话接口图像生成失败: ${error.message}`
};
}
}
async editImage(prompt, sourceImageArr, config){
if(config.imageMode==='openai'){
return await this.editImageByOpenAI(prompt, sourceImageArr, config);
} else if(config.imageMode==='chat'){
return await this.editImageByChat(prompt, sourceImageArr, config);
} else {
return await this.editImageByChat(prompt, sourceImageArr, config);
}
}
/**
* 使用对话式接口编辑图像如gemini-3-pro-image-preview
* @param {string} prompt - 编辑描述
* @param {string} sourceImageArr - 源图像URL数组
* @param {Object} config - 配置对象
* @returns {Promise<Object>} 编辑结果
*/
async editImageByChat(prompt, sourceImageArr, config) {
try{
logger.info(`[crystelf-ai] 开始编辑图像: ${prompt}, 源图像数量: ${sourceImageArr.length}`);
if(!sourceImageArr||sourceImageArr.length===0){
return {
success: false,
error: '编辑图像需要提供源图像'
};
}
let messages = [];
messages.push({
role: 'system',
content: [
{type: 'text', text: '请你根据用户的描述编辑图像,条件允许的情况下,请先思考用户的意图再编辑图像,请直接返回图像url,不要任何其他内容'}
]
});
let userContent = [];
userContent.push({type: 'text', text: prompt});
sourceImageArr.forEach((img) => {
userContent.push({type: 'image_url', image_url: {url: img}});
});
messages.push({
role: 'user',
content: userContent
});
const requestBody = {
model: config.model || 'google/gemini-3-pro-image-preview',
messages: messages,
temperature: config.temperature || 0.7,
modalities: config.modalities || ['text', 'image'],
};
const response = await axios.post(
`${config.baseApi}/v1/chat/completions`,
requestBody,
{
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json'
},
timeout: config.timeout || 60000
}
);
if (response.data && response.data.choices && response.data.choices.length > 0) {
const choice = response.data.choices[0];
if (choice.message && choice.message.images && choice.message.images.length > 0) {
const imageData = choice.message.images[0];
const imageUrl = imageData.image_url ? imageData.image_url.url : null;
if (imageUrl) {
logger.info(`[crystelf-ai] 对话接口图像生成成功: ${imageUrl.substring(0, 50)}...`);
return {
success: true,
imageUrl: imageUrl,
description: prompt,
model: config.model || 'google/gemini-3-pro-image-preview',
rawResponse: response.data
};
}
}
if (choice.message && choice.message.content) {
const imageUrl = this.extractImageUrl(choice.message.content);
if (imageUrl) {
logger.info(`[crystelf-ai] 从响应内容中提取到图像URL: ${imageUrl}`);
return {
success: true,
imageUrl: imageUrl,
description: prompt,
model: config.model || 'google/gemini-3-pro-image-preview',
rawResponse: response.data
};
} else {
logger.info(`[crystelf-ai] 收到文本响应: ${choice.message.content}`);
return {
success: true,
response: choice.message.content,
description: prompt,
model: config.model || 'google/gemini-3-pro-image-preview',
rawResponse: response.data
};
}
}
}
} catch (err){
logger.error(`[crystelf-ai] 图像编辑失败: ${error.message}`);
return {
success: false,
error: `图像编辑失败: ${error.message}`
};
}
}
/**
* 编辑图像 - 使用OpenAI标准接口
* @param {string} prompt - 编辑描述
* @param {string} sourceImageArr - 源图像URL数组
* @param {Object} config - 配置对象
* @returns {Promise<Object>} 编辑结果
*/
async editImageByOpenAI(prompt, sourceImageArr, config) {
try {
logger.info(`[crystelf-ai] 开始编辑图像: ${prompt}, 源图像数量: ${sourceImageArr.length}`);
if (!sourceImageArr || sourceImageArr.length === 0) {
return {
success: false,
error: '编辑图像需要提供源图像'
};
}
const sourceImage = sourceImageArr[0];
let imageData = sourceImage;
if (sourceImage.startsWith('http')) {
try {
const imageResponse = await axios.get(sourceImage, {
responseType: 'arraybuffer',
timeout: 30000
});
const base64 = Buffer.from(imageResponse.data).toString('base64');
imageData = `data:image/png;base64,${base64}`;
} catch (error) {
logger.error(`[crystelf-ai] 下载源图像失败: ${error.message}`);
return {
success: false,
error: `下载源图像失败: ${error.message}`
};
}
}
const requestBody = {
image: imageData,
prompt: prompt,
model: config.model || 'gemini-3-pro-image-preview',
n: config.n || 1,
size: config.size || '1024x1024',
response_format: config.responseFormat || 'url',
user: config.user || undefined
};
const response = await axios.post(
`${config.baseApi}/v1/images/edits`,
requestBody,
{
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json'
},
timeout: config.timeout || 60000
}
);
if (response.data && response.data.data && response.data.data.length > 0) {
const imageData = response.data.data[0];
const imageUrl = imageData.url || imageData.b64_json;
logger.info(`[crystelf-ai] 图像编辑成功: ${imageUrl ? 'URL' : 'Base64数据'}`);
return {
success: true,
imageUrl: imageUrl,
description: prompt,
model: config.model || 'gemini-3-pro-image-preview',
rawResponse: response.data
};
} else {
logger.error(`[crystelf-ai] 无效的API响应格式: ${JSON.stringify(response.data)}`);
return {
success: false,
error: '无效的API响应格式'
};
}
} catch (error) {
logger.error(`[crystelf-ai] 图像编辑失败: ${error.message}`);
return {
success: false,
error: `图像编辑失败: ${error.message}`
};
}
}
/**
* 从响应内容中提取图像URL
* @param {string} content - 响应内容
* @returns {string|null} 图像URL
*/
extractImageUrl(content) {
if (!content) return null;
const urlPatterns = [
/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp)/i,
/!\[.*?\]\((https?:\/\/[^\s]+)\)/i,
/\[.*?\]\((https?:\/\/[^\s]+)\)/i
];
for (const pattern of urlPatterns) {
const match = content.match(pattern);
if (match) {
return match[1] || match[0];
}
}
if (content.startsWith('http')) {
return content.trim();
}
return null;
}
/**
* 合并图像配置
* @param {Object} userConfig - 用户配置
* @returns {Object} 合并后的配置
*/
mergeImageConfig(userConfig) {
const defaultImageConfig = {
enabled: true,
model: 'gemini-3-pro-image-preview',
baseApi: 'https://api.openai.com',
apiKey: '',
maxTokens: 4000,
temperature: 0.7,
size: '1024x1024',
responseFormat: 'url',
modalities: ['text', 'image'],
timeout: 30000,
quality: 'standard',
style: 'vivid'
};
if (userConfig?.imageConfig) {
return {
...defaultImageConfig,
...userConfig.imageConfig
};
}
const imageRelatedKeys = [
'model', 'baseApi', 'apiKey', 'maxTokens', 'temperature',
'size', 'responseFormat', 'modalities', 'timeout', 'quality', 'style'
];
const mergedConfig = { ...defaultImageConfig };
for (const key of imageRelatedKeys) {
if (userConfig[key] !== undefined) {
mergedConfig[key] = userConfig[key];
}
}
return mergedConfig;
}
/**
* 验证图像配置
* @param {Object} config - 配置对象
* @returns {Object} 验证结果
*/
validateImageConfig(config) {
const errors = [];
if (!config.apiKey) {
errors.push('API密钥不能为空');
}
if (!config.baseApi) {
errors.push('API基础地址不能为空');
}
if (!config.model) {
errors.push('模型名称不能为空');
}
const validSizes = ['256x256', '512x512', '1024x1024', '1792x1024', '1024x1792'];
if (config.size && !validSizes.includes(config.size)) {
errors.push(`图像尺寸必须是以下之一: ${validSizes.join(', ')}`);
}
const validQualities = ['standard', 'hd'];
if (config.quality && !validQualities.includes(config.quality)) {
errors.push(`图像质量必须是以下之一: ${validQualities.join(', ')}`);
}
const validStyles = ['vivid', 'natural'];
if (config.style && !validStyles.includes(config.style)) {
errors.push(`图像风格必须是以下之一: ${validStyles.join(', ')}`);
}
return {
isValid: errors.length === 0,
errors: errors
};
}
}
const imageProcessor = new ImageProcessor();
export { imageProcessor, ImageProcessor };