507 lines
19 KiB
JavaScript
507 lines
19 KiB
JavaScript
const axios = require('axios');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
require('dotenv').config();
|
||
|
||
// 配置
|
||
const config = {
|
||
apiKey: process.env.API_KEY,
|
||
apiBaseUrl: process.env.API_BASE_URL || 'https://api.wuyinkeji.com',
|
||
generateApi: process.env.GENERATE_API || '/api/img/nanoBanana-pro',
|
||
// 强制使用正确的查询接口路径,避免配置错误
|
||
queryApi: (process.env.QUERY_API && process.env.QUERY_API.includes('drawDetail'))
|
||
? process.env.QUERY_API
|
||
: '/api/img/drawDetail',
|
||
sourceImageUrl: process.env.SOURCE_IMAGE_URL,
|
||
imageSize: process.env.IMAGE_SIZE || '2K',
|
||
aspectRatio: process.env.ASPECT_RATIO || '1:1',
|
||
queryInterval: parseInt(process.env.QUERY_INTERVAL) || 5000,
|
||
maxQueryCount: parseInt(process.env.MAX_QUERY_COUNT) || 60,
|
||
outputDir: path.join(__dirname, 'img_2')
|
||
};
|
||
|
||
// 验证查询接口路径
|
||
if (!config.queryApi.includes('drawDetail')) {
|
||
console.warn(`⚠️ 警告: 查询接口路径可能不正确: ${config.queryApi}`);
|
||
console.warn(` 已自动修正为: /api/img/drawDetail`);
|
||
config.queryApi = '/api/img/drawDetail';
|
||
}
|
||
|
||
// 确保输出目录存在
|
||
if (!fs.existsSync(config.outputDir)) {
|
||
fs.mkdirSync(config.outputDir, { recursive: true });
|
||
}
|
||
|
||
// 亚马逊主图prompt模板
|
||
const MAIN_IMAGE_PROMPTS = [
|
||
{
|
||
name: 'main_image_white_bg',
|
||
prompt: 'Create a professional Amazon product main image with pure white background. The product should be centered, well-lit, showing all key features clearly. High quality commercial photography style, clean and minimalist.',
|
||
aspectRatio: '1:1'
|
||
},
|
||
{
|
||
name: 'main_image_lifestyle',
|
||
prompt: 'Create a lifestyle Amazon product main image showing the product in a natural, appealing setting. Professional product photography, bright and inviting atmosphere.',
|
||
aspectRatio: '1:1'
|
||
}
|
||
];
|
||
|
||
// 亚马逊产品图prompt模板
|
||
const PRODUCT_IMAGE_PROMPTS = [
|
||
{
|
||
name: 'product_detail_1',
|
||
prompt: 'Create a detailed Amazon product image showing close-up details and features. Professional product photography, white background, high resolution.',
|
||
aspectRatio: '1:1'
|
||
},
|
||
{
|
||
name: 'product_detail_2',
|
||
prompt: 'Create an Amazon product image showing different angles and perspectives. Clean white background, professional lighting, showcasing product quality.',
|
||
aspectRatio: '1:1'
|
||
},
|
||
{
|
||
name: 'product_in_use',
|
||
prompt: 'Create an Amazon product image showing the product in use scenario. Natural setting, professional photography, highlighting product benefits.',
|
||
aspectRatio: '4:3'
|
||
},
|
||
{
|
||
name: 'product_features',
|
||
prompt: 'Create an Amazon product image highlighting key features and specifications. Clean layout, professional design, easy to understand.',
|
||
aspectRatio: '16:9'
|
||
}
|
||
];
|
||
|
||
/**
|
||
* 调用生图API
|
||
*/
|
||
async function generateImage(prompt, imageConfig) {
|
||
try {
|
||
const url = `${config.apiBaseUrl}${config.generateApi}`;
|
||
const requestData = {
|
||
prompt: prompt.prompt,
|
||
aspectRatio: imageConfig.aspectRatio || prompt.aspectRatio || config.aspectRatio,
|
||
imageSize: imageConfig.imageSize || config.imageSize
|
||
};
|
||
|
||
// 如果提供了源图片URL,添加到请求中
|
||
if (config.sourceImageUrl) {
|
||
requestData.img_url = config.sourceImageUrl;
|
||
}
|
||
|
||
console.log(`\n正在生成图片: ${prompt.name}`);
|
||
console.log(`Prompt: ${prompt.prompt.substring(0, 100)}...`);
|
||
console.log(`请求URL: ${url}`);
|
||
|
||
const response = await axios.post(url, requestData, {
|
||
headers: {
|
||
'Content-Type': 'application/json;charset:utf-8;',
|
||
'Authorization': config.apiKey
|
||
},
|
||
timeout: 30000
|
||
});
|
||
|
||
if (response.data && response.data.code === 200) {
|
||
// 根据API文档,查询接口需要的是 data.id(图片ID),不是 task_id
|
||
// 优先使用 data.id,这是查询接口需要的ID
|
||
const taskId = response.data.data?.id ||
|
||
response.data.data?.taskId ||
|
||
response.data.taskId ||
|
||
response.data.id;
|
||
|
||
// 如果获取到的是 task_id(字符串格式),需要找到对应的 id
|
||
// 因为查询接口需要的是数字 id,而不是 task_id
|
||
if (!taskId) {
|
||
console.error(`❌ 无法获取任务ID,完整响应:`, JSON.stringify(response.data, null, 2));
|
||
return { success: false, error: '无法获取任务ID' };
|
||
}
|
||
|
||
console.log(`✅ 任务提交成功`);
|
||
console.log(`📋 完整响应数据:`, JSON.stringify(response.data, null, 2));
|
||
console.log(`📝 任务ID (用于查询): ${taskId} (类型: ${typeof taskId})`);
|
||
if (response.data.data?.task_id) {
|
||
console.log(`📝 任务task_id: ${response.data.data.task_id}`);
|
||
}
|
||
console.log(`⚠️ 注意: 查询接口需要使用 data.id (${response.data.data?.id || '未找到'}),而不是 task_id`);
|
||
|
||
// 确保使用 data.id 作为查询ID
|
||
const queryId = response.data.data?.id || taskId;
|
||
return { success: true, taskId: queryId, name: prompt.name };
|
||
} else {
|
||
console.error(`❌ 任务提交失败:`, JSON.stringify(response.data, null, 2));
|
||
return { success: false, error: response.data };
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ 生成图片时出错:`, error.message);
|
||
if (error.response) {
|
||
console.error(`响应数据:`, error.response.data);
|
||
}
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询图片生成结果
|
||
*/
|
||
async function queryImageResult(taskId, imageName) {
|
||
let queryCount = 0;
|
||
let queryInterval = null;
|
||
|
||
// 执行单次查询的函数
|
||
const performQuery = async () => {
|
||
queryCount++;
|
||
|
||
if (queryCount > config.maxQueryCount) {
|
||
if (queryInterval) {
|
||
clearInterval(queryInterval);
|
||
}
|
||
throw new Error(`查询超时,已查询${queryCount}次`);
|
||
}
|
||
|
||
try {
|
||
// 根据API文档: https://api.wuyinkeji.com/doc/9
|
||
// 接口地址: https://api.wuyinkeji.com/api/img/drawDetail
|
||
// 请求方式: GET
|
||
// 请求参数: id (必填, int类型) - 图片ID(注意:是 data.id,不是 task_id)
|
||
// 返回格式: data.status (0:排队中,1:生成中,2:成功,3:失败)
|
||
// data.image_url (生成的图片地址)
|
||
|
||
// 确保查询接口路径正确
|
||
const queryApiPath = config.queryApi || '/api/img/drawDetail';
|
||
if (!queryApiPath.includes('drawDetail')) {
|
||
console.error(`❌ 警告: 查询接口路径可能不正确: ${queryApiPath}`);
|
||
console.error(` 正确的路径应该是: /api/img/drawDetail`);
|
||
}
|
||
|
||
// 确保ID是数字类型(查询接口要求int类型)
|
||
const queryId = typeof taskId === 'string' && /^\d+$/.test(taskId)
|
||
? parseInt(taskId)
|
||
: taskId;
|
||
|
||
const queryUrl = `${config.apiBaseUrl}${queryApiPath}?id=${queryId}`;
|
||
|
||
console.log(`[${queryCount}/${config.maxQueryCount}] 查询任务状态: ID=${queryId} (原始: ${taskId}, 类型: ${typeof queryId})`);
|
||
console.log(`🔗 查询URL: ${queryUrl}`);
|
||
if (queryCount === 1) {
|
||
console.log(`🔑 使用API密钥: ${config.apiKey ? config.apiKey.substring(0, 10) + '...' : '未设置'}`);
|
||
}
|
||
|
||
const response = await axios.get(queryUrl, {
|
||
headers: {
|
||
'Content-Type': 'application/json;charset:utf-8;',
|
||
'Authorization': config.apiKey
|
||
},
|
||
timeout: 10000
|
||
});
|
||
|
||
const data = response.data;
|
||
|
||
// 首次查询或每10次查询打印完整响应(用于调试)
|
||
if (queryCount === 1 || queryCount % 10 === 0) {
|
||
console.log(`📋 完整API响应 (第${queryCount}次查询):`, JSON.stringify(data, null, 2));
|
||
}
|
||
|
||
// 根据API文档响应格式:
|
||
// code: 状态码
|
||
// msg: 状态信息
|
||
// data.status: 0:排队中,1:生成中,2:成功,3:失败
|
||
// data.image_url: 生成的图片地址
|
||
if (data.code === 200 && data.data) {
|
||
// 处理 status 可能是数字或字符串的情况
|
||
const status = parseInt(data.data.status);
|
||
const imageUrl = data.data.image_url;
|
||
|
||
// 添加详细日志
|
||
const statusText = status === 0 ? '排队中' : status === 1 ? '生成中' : status === 2 ? '成功' : status === 3 ? '失败' : `未知(${status})`;
|
||
console.log(`📊 查询结果详情:`, {
|
||
status: status,
|
||
statusType: typeof data.data.status,
|
||
statusText: statusText,
|
||
hasImageUrl: !!imageUrl,
|
||
imageUrl: imageUrl ? (imageUrl.length > 100 ? imageUrl.substring(0, 100) + '...' : imageUrl) : '无',
|
||
imageUrlLength: imageUrl ? imageUrl.length : 0,
|
||
prompt: data.data.prompt ? (data.data.prompt.substring(0, 50) + '...') : '无',
|
||
createdAt: data.data.created_at || '无',
|
||
updatedAt: data.data.updated_at || '无'
|
||
});
|
||
|
||
// status: 2 表示成功(使用宽松比较,支持数字2和字符串"2")
|
||
if (status === 2 || data.data.status === '2' || data.data.status === 2) {
|
||
// 检查 imageUrl 是否有效(不能为空字符串、null、undefined)
|
||
const validImageUrl = imageUrl && typeof imageUrl === 'string' && imageUrl.trim().length > 0;
|
||
|
||
if (validImageUrl) {
|
||
if (queryInterval) {
|
||
clearInterval(queryInterval);
|
||
}
|
||
const finalImageUrl = imageUrl.trim();
|
||
console.log(`✅ 图片生成成功: ${imageName}`);
|
||
console.log(`图片URL: ${finalImageUrl}`);
|
||
console.log(`图片URL长度: ${finalImageUrl.length}`);
|
||
return { imageUrl: finalImageUrl, taskId, imageName, success: true };
|
||
} else {
|
||
console.log(`⚠️ 状态为成功但URL无效,继续等待...`);
|
||
console.log(` imageUrl值: ${JSON.stringify(imageUrl)}`);
|
||
console.log(` imageUrl类型: ${typeof imageUrl}`);
|
||
// 如果状态是2但URL无效,继续等待
|
||
return { success: false, continue: true };
|
||
}
|
||
}
|
||
// status: 3 表示失败
|
||
else if (status === 3 || data.data.status === '3') {
|
||
if (queryInterval) {
|
||
clearInterval(queryInterval);
|
||
}
|
||
const errorMsg = data.msg || data.data.msg || '未知错误';
|
||
console.error(`❌ 图片生成失败: ${errorMsg}`);
|
||
throw new Error(`图片生成失败: ${errorMsg}`);
|
||
}
|
||
// status: 0 排队中, 1 生成中
|
||
else {
|
||
console.log(`⏳ ${statusText}... (已等待 ${Math.round(queryCount * config.queryInterval / 1000)} 秒)`);
|
||
return { success: false, continue: true };
|
||
}
|
||
} else if (data.code !== 200) {
|
||
// API返回错误
|
||
console.error(`⚠️ 查询返回错误: ${data.msg || '未知错误'} (code: ${data.code})`);
|
||
console.error(`完整错误响应:`, JSON.stringify(data, null, 2));
|
||
|
||
// 如果是认证错误或参数错误,立即失败
|
||
if (data.code === 401 || data.code === 403 || data.code === 400) {
|
||
if (queryInterval) {
|
||
clearInterval(queryInterval);
|
||
}
|
||
throw new Error(`API错误: ${data.msg} (code: ${data.code})`);
|
||
}
|
||
// 其他错误继续重试
|
||
return { success: false, continue: true };
|
||
} else {
|
||
// data为空或格式不对
|
||
console.warn(`⚠️ 响应格式异常:`, JSON.stringify(data, null, 2));
|
||
console.log(`⏳ 等待中... (响应: ${data.msg || '处理中'})`);
|
||
return { success: false, continue: true };
|
||
}
|
||
} catch (error) {
|
||
if (error.response) {
|
||
// HTTP错误响应
|
||
const statusCode = error.response.status;
|
||
const errorData = error.response.data;
|
||
|
||
if (statusCode === 404) {
|
||
// 任务可能还在处理中,或者任务ID不存在
|
||
console.error(`❌ 任务未找到(404): ID=${taskId} (类型: ${typeof taskId})`);
|
||
console.error(` 可能的原因:`);
|
||
console.error(` 1. 任务ID类型错误 - 查询接口需要数字ID,不是task_id字符串`);
|
||
console.error(` 2. 任务还在处理中,稍后再试`);
|
||
console.error(` 3. 任务ID不存在或已过期`);
|
||
console.error(` 提示: 请检查生图接口返回的 data.id 字段(应该是数字)`);
|
||
|
||
// 如果是字符串类型的ID,尝试转换为数字
|
||
if (typeof taskId === 'string' && /^\d+$/.test(taskId)) {
|
||
console.log(` 尝试将字符串ID转换为数字: ${parseInt(taskId)}`);
|
||
// 不立即失败,继续重试(可能任务还在处理)
|
||
}
|
||
} else if (statusCode === 401 || statusCode === 403) {
|
||
// 认证错误,立即失败
|
||
if (queryInterval) {
|
||
clearInterval(queryInterval);
|
||
}
|
||
console.error(`❌ 认证失败 (${statusCode}):`, errorData);
|
||
throw new Error(`认证失败: ${errorData?.msg || error.message}`);
|
||
} else {
|
||
console.error(`❌ HTTP错误 (${statusCode}):`, errorData || error.message);
|
||
}
|
||
} else if (error.code === 'ECONNABORTED') {
|
||
console.error(`❌ 请求超时:`, error.message);
|
||
} else if (error.message && error.message.includes('查询超时')) {
|
||
// 这是我们的超时错误,直接抛出
|
||
throw error;
|
||
} else {
|
||
console.error(`❌ 查询时出错:`, error.message);
|
||
if (error.stack) {
|
||
console.error(`错误堆栈:`, error.stack);
|
||
}
|
||
}
|
||
// 网络错误等继续重试,不立即失败
|
||
return { success: false, continue: true };
|
||
}
|
||
};
|
||
|
||
return new Promise(async (resolve, reject) => {
|
||
try {
|
||
// 立即执行第一次查询,不等待间隔
|
||
const firstResult = await performQuery();
|
||
if (firstResult.success) {
|
||
resolve(firstResult);
|
||
return;
|
||
}
|
||
|
||
// 如果第一次查询未成功,设置定时器继续查询
|
||
queryInterval = setInterval(async () => {
|
||
try {
|
||
const result = await performQuery();
|
||
if (result.success) {
|
||
if (queryInterval) {
|
||
clearInterval(queryInterval);
|
||
}
|
||
resolve(result);
|
||
}
|
||
// 如果 continue 为 true,继续下一次查询
|
||
} catch (error) {
|
||
if (queryInterval) {
|
||
clearInterval(queryInterval);
|
||
}
|
||
reject(error);
|
||
}
|
||
}, config.queryInterval);
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 下载并保存图片
|
||
*/
|
||
async function downloadAndSaveImage(imageUrl, imageName) {
|
||
try {
|
||
console.log(`\n开始下载图片: ${imageUrl}`);
|
||
|
||
const response = await axios({
|
||
url: imageUrl,
|
||
method: 'GET',
|
||
responseType: 'stream',
|
||
timeout: 60000
|
||
});
|
||
|
||
// 确定文件扩展名
|
||
const contentType = response.headers['content-type'];
|
||
let extension = '.jpg';
|
||
if (contentType) {
|
||
if (contentType.includes('png')) extension = '.png';
|
||
else if (contentType.includes('webp')) extension = '.webp';
|
||
}
|
||
|
||
const filename = `${imageName}${extension}`;
|
||
const filepath = path.join(config.outputDir, filename);
|
||
|
||
const writer = fs.createWriteStream(filepath);
|
||
response.data.pipe(writer);
|
||
|
||
return new Promise((resolve, reject) => {
|
||
writer.on('finish', () => {
|
||
console.log(`✅ 图片已保存: ${filepath}`);
|
||
resolve(filepath);
|
||
});
|
||
writer.on('error', (error) => {
|
||
console.error(`❌ 保存图片失败:`, error);
|
||
reject(error);
|
||
});
|
||
});
|
||
} catch (error) {
|
||
console.error(`❌ 下载图片失败:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理单个图片生成任务
|
||
*/
|
||
async function processImageGeneration(prompt) {
|
||
try {
|
||
// 1. 提交生成任务
|
||
const generateResult = await generateImage(prompt, {
|
||
aspectRatio: prompt.aspectRatio,
|
||
imageSize: config.imageSize
|
||
});
|
||
|
||
if (!generateResult.success) {
|
||
throw new Error('任务提交失败');
|
||
}
|
||
|
||
// 2. 查询生成结果
|
||
const queryResult = await queryImageResult(generateResult.taskId, prompt.name);
|
||
|
||
// 3. 下载并保存图片
|
||
await downloadAndSaveImage(queryResult.imageUrl, queryResult.imageName);
|
||
|
||
return { success: true, name: prompt.name };
|
||
} catch (error) {
|
||
console.error(`处理图片 ${prompt.name} 时出错:`, error.message);
|
||
return { success: false, name: prompt.name, error: error.message };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 主工作流
|
||
*/
|
||
async function main() {
|
||
console.log('🚀 开始亚马逊产品图生成工作流...\n');
|
||
console.log('配置信息:');
|
||
console.log(`- API地址: ${config.apiBaseUrl}`);
|
||
console.log(`- 生图接口: ${config.generateApi}`);
|
||
console.log(`- 查询接口: ${config.queryApi}`);
|
||
console.log(`- 源图片: ${config.sourceImageUrl || '使用本地图片'}`);
|
||
console.log(`- 输出目录: ${config.outputDir}`);
|
||
console.log(`- 图片尺寸: ${config.imageSize}`);
|
||
console.log(`- 查询间隔: ${config.queryInterval}ms`);
|
||
console.log(`- 最大查询次数: ${config.maxQueryCount}次\n`);
|
||
|
||
if (!config.apiKey) {
|
||
console.error('❌ 错误: 未设置API_KEY,请在.env文件中配置');
|
||
process.exit(1);
|
||
}
|
||
|
||
if (!config.sourceImageUrl) {
|
||
console.warn('⚠️ 警告: 未设置SOURCE_IMAGE_URL');
|
||
console.warn('提示: 请将P1191464.JPG上传到图床服务,然后在.env中设置SOURCE_IMAGE_URL');
|
||
console.warn('或者修改代码以支持直接上传本地图片\n');
|
||
}
|
||
|
||
const allPrompts = [...MAIN_IMAGE_PROMPTS, ...PRODUCT_IMAGE_PROMPTS];
|
||
const results = [];
|
||
|
||
// 顺序处理每个图片生成任务
|
||
for (const prompt of allPrompts) {
|
||
const result = await processImageGeneration(prompt);
|
||
results.push(result);
|
||
|
||
// 在任务之间稍作延迟,避免请求过快
|
||
if (allPrompts.indexOf(prompt) < allPrompts.length - 1) {
|
||
console.log('\n等待3秒后处理下一个任务...\n');
|
||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
}
|
||
}
|
||
|
||
// 输出总结
|
||
console.log('\n' + '='.repeat(50));
|
||
console.log('📊 工作流执行总结:');
|
||
console.log('='.repeat(50));
|
||
const successCount = results.filter(r => r.success).length;
|
||
const failCount = results.filter(r => !r.success).length;
|
||
console.log(`✅ 成功: ${successCount}/${results.length}`);
|
||
console.log(`❌ 失败: ${failCount}/${results.length}`);
|
||
|
||
if (failCount > 0) {
|
||
console.log('\n失败的任务:');
|
||
results.filter(r => !r.success).forEach(r => {
|
||
console.log(` - ${r.name}: ${r.error}`);
|
||
});
|
||
}
|
||
console.log('='.repeat(50));
|
||
}
|
||
|
||
// 运行主工作流
|
||
if (require.main === module) {
|
||
main().catch(error => {
|
||
console.error('❌ 工作流执行失败:', error);
|
||
process.exit(1);
|
||
});
|
||
}
|
||
|
||
module.exports = {
|
||
generateImage,
|
||
queryImageResult,
|
||
downloadAndSaveImage,
|
||
processImageGeneration
|
||
};
|
||
|