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 };