Files
Amazon_img/index.js
2026-01-06 23:45:28 +08:00

507 lines
19 KiB
JavaScript
Raw Permalink 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.

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