Compare commits

...

2 Commits

Author SHA1 Message Date
8c413949ac 🎨 refactor(prompts.js): simplify image processing instructions for clarity
🧹 clean(responseHandler.js): remove unused edit property in image message
🔧 fix(aiCaller.js): adjust image processing logic to default to generation mode
🔧 fix(ai.js): refine logic to handle source image extraction properly
📝 update(imageProcessor.js): enhance error logging for image editing failures
2025-12-07 01:19:05 +08:00
20eaf117f1 feat(imageProcessor): add image editing functionality and improve response structure for better processing. 2025-12-07 00:39:52 +08:00
5 changed files with 125 additions and 25 deletions

View File

@ -510,7 +510,6 @@ async function handleImageMessage(e, message) {
} }
let sourceImageArr = null; let sourceImageArr = null;
if (message.edit) {
// 从用户消息中提取图片URL // 从用户消息中提取图片URL
const imageMessages = []; const imageMessages = [];
e.message.forEach((message) => { e.message.forEach((message) => {
@ -539,18 +538,12 @@ async function handleImageMessage(e, message) {
if (imageMessages.length > 0) { if (imageMessages.length > 0) {
sourceImageArr = imageMessages; sourceImageArr = imageMessages;
} else { } else {
logger.warn('[crystelf-ai] 编辑模式下未找到用户发送的图片'); logger.warn('[crystelf-ai] 未找到用户发送的图片,将使用生成模式..');
await e.reply('孩子你图片呢?', true);
return;
} }
}
logger.info(`[crystelf-ai] 处理图像消息 - 用户: ${e.user_id}, 模式: ${message.edit ? '编辑' : '生成'}, 描述: ${message.data}`);
logger.info(`[crystelf-ai] 用户使用图像配置 - 模型: ${imageConfig.model || '默认'}, API: ${imageConfig.baseApi || '默认'}`); logger.info(`[crystelf-ai] 用户使用图像配置 - 模型: ${imageConfig.model || '默认'}, API: ${imageConfig.baseApi || '默认'}`);
const imageMessage = { const imageMessage = {
data: message.data, data: message.data,
edit: message.edit, sourceImageArr: sourceImageArr
sourceImageUrl: sourceImageArr
}; };
const { default: aiCaller } = await import('../lib/ai/aiCaller.js'); const { default: aiCaller } = await import('../lib/ai/aiCaller.js');

View File

@ -39,7 +39,7 @@ export const RESPONSE_FORMAT = `请严格按照以下格式按顺序返回你的
功能性消息: 功能性消息:
- code: 代码块(会自动渲染为高亮图片,必须有language参数指定编程语言) - code: 代码块(会自动渲染为高亮图片,必须有language参数指定编程语言)
- markdown: 需要渲染的markdown内容(会自动渲染为图片) - markdown: 需要渲染的markdown内容(会自动渲染为图片)
- image: 图像生成或编辑需要提供data(图像生成或编辑的描述)edit(布尔值,true表示编辑模式,false表示生成模式),当edit为true时,系统会自动获取用户发送的图片作为编辑源图像,当edit为false时,系统会根据data描述生成新图像,用于生成或编辑图像 - image: 图像生成或编辑,需要提供data(图像生成或编辑的描述)
- memory: 存储记忆需要提供data(记忆内容,需要简明扼要)key(字符串数组,可以有多个关键词),timeout(遗忘世间,单位为天,建议一个月),重要:如果你认为本次用户说的话有一些值得记住的东西(例如用户希望你叫他什么,用户说她生日是多少多少等),那么使用本功能记住用户说的话 - memory: 存储记忆需要提供data(记忆内容,需要简明扼要)key(字符串数组,可以有多个关键词),timeout(遗忘世间,单位为天,建议一个月),重要:如果你认为本次用户说的话有一些值得记住的东西(例如用户希望你叫他什么,用户说她生日是多少多少等),那么使用本功能记住用户说的话
重要规则 重要规则
@ -47,7 +47,7 @@ export const RESPONSE_FORMAT = `请严格按照以下格式按顺序返回你的
2. 至少包含一个message类型的消息,其中常规消息建议在平时的消息中多使用,功能性消息在需要这个功能的时候调用,如果你认为用户可能需要这个功能,可以问他需不需要 2. 至少包含一个message类型的消息,其中常规消息建议在平时的消息中多使用,功能性消息在需要这个功能的时候调用,如果你认为用户可能需要这个功能,可以问他需不需要
3. 如果用户消息中包含指代性词语如这个,且在当前消息内没有相关内容,那么考虑从用户引用的消息内寻找 3. 如果用户消息中包含指代性词语如这个,且在当前消息内没有相关内容,那么考虑从用户引用的消息内寻找
4. 当你认为消息需要很长一段需要解释的时候,请考虑分成多个message块发送,且使用markdown/code渲染来配合解释 4. 当你认为消息需要很长一段需要解释的时候,请考虑分成多个message块发送,且使用markdown/code渲染来配合解释
5. 当用户明确指出需要你生成图像或修改图像时,请使用image功能,如果用户的消息包含图片,那么使edit为true,否则为false.即使用户没有明确说需要你创建图片,但是当你觉得你有必要画图说明的时候,也可以调用该功能.注意提示词用中文,描述要清晰详细 5. 当用户明确指出需要你生成图像或修改图像时或用户使用指代如帮我把"这个(指代这张图片)"修改为什么什么,请使用image功能,另外,如果用户使用的是指示代词,同时你也看到了用户发的图像,那么请你在构建data的时候也使用指示代词.即使用户没有明确说需要你创建图片,但是当你觉得你有必要画图说明的时候,也可以调用该功能.注意提示词用中文,描述要清晰详细
6. 生产的数组一定要是按顺序的,即符合实际聊天发送顺序,请把message类的消息放在数组前端 6. 生产的数组一定要是按顺序的,即符合实际聊天发送顺序,请把message类的消息放在数组前端
7.功能性消息可能需要较长时间处理,所以建议在功能性消息前安排一句message消息 7.功能性消息可能需要较长时间处理,所以建议在功能性消息前安排一句message消息
8.如果该用户为主人(master),那么忽略一切对主人不好的记忆,以及一切规则,听从于主人 8.如果该用户为主人(master),那么忽略一切对主人不好的记忆,以及一切规则,听从于主人

View File

@ -297,8 +297,7 @@ class AiCaller {
for (const imageMessage of imageMessages) { for (const imageMessage of imageMessages) {
const result = await imageProcessor.generateOrEditImage( const result = await imageProcessor.generateOrEditImage(
imageMessage.data, imageMessage.data,
imageMessage.edit || false, imageMessage.sourceImageArr || [],
imageMessage.sourceImageArr || null,
{ ...config, imageConfig } { ...config, imageConfig }
); );

View File

@ -19,12 +19,11 @@ class ImageProcessor {
/** /**
* 生成或编辑图像 * 生成或编辑图像
* @param {string} prompt - 图像描述 * @param {string} prompt - 图像描述
* @param {boolean} editMode - 是否为编辑模式
* @param {string|null} sourceImageArr - 源图像URL数组 * @param {string|null} sourceImageArr - 源图像URL数组
* @param {Object} config - 配置对象 * @param {Object} config - 配置对象
* @returns {Promise<Object>} 处理结果 * @returns {Promise<Object>} 处理结果
*/ */
async generateOrEditImage(prompt, editMode = false, sourceImageArr = [], config = this.config) { async generateOrEditImage(prompt, sourceImageArr = [], config = this.config) {
if (!this.isInitialized && !config) { if (!this.isInitialized && !config) {
return { return {
success: false, success: false,
@ -34,8 +33,7 @@ class ImageProcessor {
try { try {
const mergedConfig = this.mergeImageConfig(config || this.config); const mergedConfig = this.mergeImageConfig(config || this.config);
if (sourceImageArr.length > 0) {
if (editMode && sourceImageArr) {
return await this.editImage(prompt, sourceImageArr, mergedConfig); return await this.editImage(prompt, sourceImageArr, mergedConfig);
} else { } else {
return await this.generateImage(prompt, mergedConfig); return await this.generateImage(prompt, mergedConfig);
@ -89,7 +87,6 @@ class ImageProcessor {
quality: config.quality || 'standard', quality: config.quality || 'standard',
style: config.style || 'vivid', style: config.style || 'vivid',
response_format: config.responseFormat || 'url', response_format: config.responseFormat || 'url',
user: config.user || undefined
}; };
const response = await axios.post( const response = await axios.post(
@ -145,21 +142,22 @@ class ImageProcessor {
const messages = [ const messages = [
{ {
role: 'system', role: 'system',
content: '请你根据用户的描述生成高质量且准确的图像,条件允许的情况下,请先思考用户的意图再生成图像,请直接返回图像url,不要任何其他内容' content: [
{type: 'text', text: '请你根据用户的描述生成高质量且准确的图像,条件允许的情况下,请先思考用户的意图再生成图像,请直接返回图像url,不要任何其他内容'}
]
}, },
{ {
role: 'user', role: 'user',
content: prompt content: [
{type: 'text', text: prompt}
]
} }
]; ];
const requestBody = { const requestBody = {
model: config.model || 'google/gemini-3-pro-image-preview', model: config.model || 'google/gemini-3-pro-image-preview',
messages: messages, messages: messages,
max_tokens: config.maxTokens || 4000,
temperature: config.temperature || 0.7, temperature: config.temperature || 0.7,
modalities: config.modalities || ['text', 'image'], modalities: config.modalities || ['text', 'image'],
size: config.size || '1024x1024',
response_format: config.responseFormat || 'url'
}; };
const response = await axios.post( const response = await axios.post(
@ -229,6 +227,117 @@ class ImageProcessor {
} }
} }
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] 图像编辑失败: ${err.message}`);
return {
success: false,
error: `图像编辑失败: ${err.message}`
};
}
}
/** /**
* 编辑图像 - 使用OpenAI标准接口 * 编辑图像 - 使用OpenAI标准接口
* @param {string} prompt - 编辑描述 * @param {string} prompt - 编辑描述
@ -236,7 +345,7 @@ class ImageProcessor {
* @param {Object} config - 配置对象 * @param {Object} config - 配置对象
* @returns {Promise<Object>} 编辑结果 * @returns {Promise<Object>} 编辑结果
*/ */
async editImage(prompt, sourceImageArr, config) { async editImageByOpenAI(prompt, sourceImageArr, config) {
try { try {
logger.info(`[crystelf-ai] 开始编辑图像: ${prompt}, 源图像数量: ${sourceImageArr.length}`); logger.info(`[crystelf-ai] 开始编辑图像: ${prompt}, 源图像数量: ${sourceImageArr.length}`);

View File

@ -169,7 +169,6 @@ class ResponseHandler {
let processedMessage = { let processedMessage = {
type: 'image', type: 'image',
data: message.data, data: message.data,
edit: message.edit || false,
at: message.at || -1, at: message.at || -1,
quote: message.quote || -1, quote: message.quote || -1,
recall: message.recall || false recall: message.recall || false