import _ from "lodash"; import crypto from "crypto"; import APIException from "@/lib/exceptions/APIException.ts"; import EX from "@/api/consts/exceptions.ts"; import util from "@/lib/util.ts"; import { getCredit, receiveCredit, request } from "./core.ts"; import logger from "@/lib/logger.ts"; import { getModelConfig } from "@/lib/configs/model-config.ts"; const DEFAULT_ASSISTANT_ID = 513695; export const DEFAULT_MODEL = "jimeng-4.5"; const DRAFT_VERSION = "3.3.4"; const DRAFT_MIN_VERSION = "3.0.2"; // 支持的图片比例和分辨率配置 const RESOLUTION_OPTIONS: { [resolution: string]: { [ratio: string]: { width: number; height: number; ratio: number }; }; } = { "1k": { "1:1": { width: 1024, height: 1024, ratio: 1 }, "4:3": { width: 768, height: 1024, ratio: 4 }, "3:4": { width: 1024, height: 768, ratio: 2 }, "16:9": { width: 1024, height: 576, ratio: 3 }, "9:16": { width: 576, height: 1024, ratio: 5 }, "3:2": { width: 1024, height: 682, ratio: 7 }, "2:3": { width: 682, height: 1024, ratio: 6 }, "21:9": { width: 1195, height: 512, ratio: 8 }, }, "2k": { "1:1": { width: 2048, height: 2048, ratio: 1 }, "4:3": { width: 2304, height: 1728, ratio: 4 }, "3:4": { width: 1728, height: 2304, ratio: 2 }, "16:9": { width: 2560, height: 1440, ratio: 3 }, "9:16": { width: 1440, height: 2560, ratio: 5 }, "3:2": { width: 2496, height: 1664, ratio: 7 }, "2:3": { width: 1664, height: 2496, ratio: 6 }, "21:9": { width: 3024, height: 1296, ratio: 8 }, }, "4k": { "1:1": { width: 4096, height: 4096, ratio: 101 }, "4:3": { width: 4608, height: 3456, ratio: 104 }, "3:4": { width: 3456, height: 4608, ratio: 102 }, "16:9": { width: 5120, height: 2880, ratio: 103 }, "9:16": { width: 2880, height: 5120, ratio: 105 }, "3:2": { width: 4992, height: 3328, ratio: 107 }, "2:3": { width: 3328, height: 4992, ratio: 106 }, "21:9": { width: 6048, height: 2592, ratio: 108 }, }, }; // 解析分辨率参数 function resolveResolution( resolution: string = "2k", ratio: string = "1:1" ): { width: number; height: number; imageRatio: number; resolutionType: string } { const resolutionGroup = RESOLUTION_OPTIONS[resolution]; if (!resolutionGroup) { const supportedResolutions = Object.keys(RESOLUTION_OPTIONS).join(", "); throw new Error(`不支持的分辨率 "${resolution}"。支持的分辨率: ${supportedResolutions}`); } const ratioConfig = resolutionGroup[ratio]; if (!ratioConfig) { const supportedRatios = Object.keys(resolutionGroup).join(", "); throw new Error(`在 "${resolution}" 分辨率下,不支持的比例 "${ratio}"。支持的比例: ${supportedRatios}`); } return { width: ratioConfig.width, height: ratioConfig.height, imageRatio: ratioConfig.ratio, resolutionType: resolution, }; } // 模型特定的版本配置 const MODEL_DRAFT_VERSIONS: { [key: string]: string } = { "jimeng-4.5": "3.3.4", "jimeng-4.1": "3.3.4", "jimeng-4.0": "3.3.4", "jimeng-3.1": "3.0.2", "jimeng-3.0": "3.0.2", "jimeng-2.1": "3.0.2", "jimeng-2.0-pro": "3.0.2", "jimeng-2.0": "3.0.2", "jimeng-1.4": "3.0.2", "jimeng-xl-pro": "3.0.2", }; // 获取模型对应的draft版本 function getDraftVersion(model: string): string { try { const config = getModelConfig(model); return config.draftVersion; } catch (e) { // 如果配置中没有,使用旧的映射 return MODEL_DRAFT_VERSIONS[model] || DRAFT_VERSION; } } const MODEL_MAP = { "jimeng-4.5": "high_aes_general_v40l", "jimeng-4.1": "high_aes_general_v41", "jimeng-4.0": "high_aes_general_v40", "jimeng-3.1": "high_aes_general_v30l_art_fangzhou:general_v3.0_18b", "jimeng-3.0": "high_aes_general_v30l:general_v3.0_18b", "jimeng-2.1": "high_aes_general_v21_L:general_v2.1_L", "jimeng-2.0-pro": "high_aes_general_v20_L:general_v2.0_L", "jimeng-2.0": "high_aes_general_v20:general_v2.0", "jimeng-1.4": "high_aes_general_v14:general_v1.4", "jimeng-xl-pro": "text2img_xl_sft", }; // 向后兼容的函数 export function getModel(model: string) { try { const config = getModelConfig(model); return config.internalModel; } catch (e) { // 如果配置中没有,使用旧的映射 return MODEL_MAP[model] || MODEL_MAP[DEFAULT_MODEL]; } } // AWS4-HMAC-SHA256 签名生成函数 function createSignature( method: string, url: string, headers: { [key: string]: string }, accessKeyId: string, secretAccessKey: string, sessionToken?: string, payload: string = '' ) { const urlObj = new URL(url); const pathname = urlObj.pathname || '/'; const search = urlObj.search; // 创建规范请求 const timestamp = headers['x-amz-date']; const date = timestamp.substr(0, 8); const region = 'cn-north-1'; const service = 'imagex'; // 规范化查询参数 - 手动处理以确保正确的顺序 const queryParams: Array<[string, string]> = []; const searchParams = new URLSearchParams(search); searchParams.forEach((value, key) => { queryParams.push([key, value]); }); // 按键名排序 - 大小写敏感,先大写字母,后小写字母 queryParams.sort(([a], [b]) => { // AWS要求大小写敏感的ASCII排序 if (a < b) return -1; if (a > b) return 1; return 0; }); // 构建规范查询字符串(不进行额外编码,因为URL中已经编码) const canonicalQueryString = queryParams .map(([key, value]) => `${key}=${value}`) .join('&'); // 规范化头部 - 只包含必要的头部 const headersToSign: { [key: string]: string } = { 'x-amz-date': timestamp }; // 添加 session token if (sessionToken) { headersToSign['x-amz-security-token'] = sessionToken; } // 如果是POST请求且包含payload,添加content-sha256头 let payloadHash = crypto.createHash('sha256').update('').digest('hex'); // 默认空payload if (method.toUpperCase() === 'POST' && payload) { payloadHash = crypto.createHash('sha256').update(payload, 'utf8').digest('hex'); headersToSign['x-amz-content-sha256'] = payloadHash; } const signedHeaders = Object.keys(headersToSign) .map(key => key.toLowerCase()) .sort() .join(';'); const canonicalHeaders = Object.keys(headersToSign) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) .map(key => `${key.toLowerCase()}:${headersToSign[key].trim()}\n`) .join(''); // 创建规范请求 const canonicalRequest = [ method.toUpperCase(), pathname, canonicalQueryString, canonicalHeaders, signedHeaders, payloadHash ].join('\n'); // 调试输出 logger.debug(`规范请求: Method: ${method.toUpperCase()} Path: ${pathname} Query: ${canonicalQueryString} Headers: ${canonicalHeaders} SignedHeaders: ${signedHeaders} PayloadHash: ${payloadHash} ---完整规范请求--- ${canonicalRequest} ---结束---`); // 创建待签名字符串 const credentialScope = `${date}/${region}/${service}/aws4_request`; const stringToSign = [ 'AWS4-HMAC-SHA256', timestamp, credentialScope, crypto.createHash('sha256').update(canonicalRequest, 'utf8').digest('hex') ].join('\n'); logger.debug(`待签名字符串: ${stringToSign}`); // 生成签名 const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(date).digest(); const kRegion = crypto.createHmac('sha256', kDate).update(region).digest(); const kService = crypto.createHmac('sha256', kRegion).update(service).digest(); const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest(); const signature = crypto.createHmac('sha256', kSigning).update(stringToSign, 'utf8').digest('hex'); return `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`; } // 计算文件的CRC32值 function calculateCRC32(buffer: ArrayBuffer): string { const crcTable = []; for (let i = 0; i < 256; i++) { let crc = i; for (let j = 0; j < 8; j++) { crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1); } crcTable[i] = crc; } let crc = 0 ^ (-1); const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.length; i++) { crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF]; } return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0'); } // 图片上传功能:将外部图片URL上传到即梦系统 async function uploadImageFromUrl(imageUrl: string, refreshToken: string): Promise { try { logger.info(`开始上传图片: ${imageUrl}`); // 第一步:获取上传令牌 const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, { data: { scene: 2, // AIGC 图片上传场景 }, }); const { access_key_id, secret_access_key, session_token, service_id } = tokenResult; if (!access_key_id || !secret_access_key || !session_token) { throw new Error("获取上传令牌失败"); } // 使用固定的service_id const actualServiceId = service_id || "tb4s082cfz"; logger.info(`获取上传令牌成功: service_id=${actualServiceId}`); // 下载图片数据 const imageResponse = await fetch(imageUrl); if (!imageResponse.ok) { throw new Error(`下载图片失败: ${imageResponse.status}`); } const imageBuffer = await imageResponse.arrayBuffer(); const fileSize = imageBuffer.byteLength; const crc32 = calculateCRC32(imageBuffer); logger.info(`图片下载完成: 大小=${fileSize}字节, CRC32=${crc32}`); // 第二步:申请图片上传权限 // 使用UTC时间格式 YYYYMMDD'T'HHMMSS'Z' const now = new Date(); const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z'); // 生成随机字符串作为签名参数 const randomStr = Math.random().toString(36).substring(2, 12); // 保持原始的参数顺序(这是API期望的顺序) const applyUrl = `https://imagex.bytedanceapi.com/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}`; logger.debug(`原始URL: ${applyUrl}`); // 构建AWS签名所需的头部 const requestHeaders = { 'x-amz-date': timestamp, 'x-amz-security-token': session_token }; // 生成AWS签名 const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token); // 调试日志 logger.info(`AWS签名调试信息: URL: ${applyUrl} AccessKeyId: ${access_key_id} SessionToken: ${session_token ? '存在' : '不存在'} Timestamp: ${timestamp} Authorization: ${authorization} `); const applyResponse = await fetch(applyUrl, { method: 'GET', headers: { 'accept': '*/*', 'accept-language': 'zh-CN,zh;q=0.9', 'authorization': authorization, 'origin': 'https://jimeng.jianying.com', 'referer': 'https://jimeng.jianying.com/ai-tool/generate', 'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'cross-site', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36', 'x-amz-date': timestamp, 'x-amz-security-token': session_token, }, }); if (!applyResponse.ok) { const errorText = await applyResponse.text(); throw new Error(`申请上传权限失败: ${applyResponse.status} - ${errorText}`); } const applyResult = await applyResponse.json(); // 检查是否有错误 if (applyResult?.ResponseMetadata?.Error) { throw new Error(`申请上传权限失败: ${JSON.stringify(applyResult.ResponseMetadata.Error)}`); } logger.info(`申请上传权限成功`); // 解析上传信息 const uploadAddress = applyResult?.Result?.UploadAddress; if (!uploadAddress || !uploadAddress.StoreInfos || !uploadAddress.UploadHosts) { throw new Error(`获取上传地址失败: ${JSON.stringify(applyResult)}`); } const storeInfo = uploadAddress.StoreInfos[0]; const uploadHost = uploadAddress.UploadHosts[0]; const auth = storeInfo.Auth; // 构建上传URL const uploadUrl = `https://${uploadHost}/upload/v1/${storeInfo.StoreUri}`; // 提取图片ID (StoreUri最后一个斜杠后的部分) const imageId = storeInfo.StoreUri.split('/').pop(); logger.info(`准备上传图片: imageId=${imageId}, uploadUrl=${uploadUrl}`); // 第三步:上传图片文件 const uploadResponse = await fetch(uploadUrl, { method: 'POST', headers: { 'Accept': '*/*', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Authorization': auth, 'Connection': 'keep-alive', 'Content-CRC32': crc32, 'Content-Disposition': 'attachment; filename="undefined"', 'Content-Type': 'application/octet-stream', 'Origin': 'https://jimeng.jianying.com', 'Referer': 'https://jimeng.jianying.com/ai-tool/generate', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'cross-site', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36', 'X-Storage-U': '704135154117550', // 用户ID,可以从token或其他地方获取 }, body: imageBuffer, }); if (!uploadResponse.ok) { const errorText = await uploadResponse.text(); throw new Error(`图片上传失败: ${uploadResponse.status} - ${errorText}`); } logger.info(`图片文件上传成功`); // 第四步:提交上传 const commitUrl = `https://imagex.bytedanceapi.com/?Action=CommitImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}`; const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z'); const commitPayload = JSON.stringify({ SessionKey: uploadAddress.SessionKey, SuccessActionStatus: "200" }); // 计算payload的SHA256哈希值 const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex'); // 构建AWS签名所需的头部 const commitRequestHeaders = { 'x-amz-date': commitTimestamp, 'x-amz-security-token': session_token, 'x-amz-content-sha256': payloadHash }; // 生成AWS签名 const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload); const commitResponse = await fetch(commitUrl, { method: 'POST', headers: { 'accept': '*/*', 'accept-language': 'zh-CN,zh;q=0.9', 'authorization': commitAuthorization, 'content-type': 'application/json', 'origin': 'https://jimeng.jianying.com', 'referer': 'https://jimeng.jianying.com/ai-tool/generate', 'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'cross-site', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36', 'x-amz-date': commitTimestamp, 'x-amz-security-token': session_token, 'x-amz-content-sha256': payloadHash, }, body: commitPayload, }); if (!commitResponse.ok) { const errorText = await commitResponse.text(); throw new Error(`提交上传失败: ${commitResponse.status} - ${errorText}`); } const commitResult = await commitResponse.json(); // 检查提交结果 if (commitResult?.ResponseMetadata?.Error) { throw new Error(`提交上传失败: ${JSON.stringify(commitResult.ResponseMetadata.Error)}`); } if (!commitResult?.Result?.Results || commitResult.Result.Results.length === 0) { throw new Error(`提交上传响应缺少结果: ${JSON.stringify(commitResult)}`); } const uploadResult = commitResult.Result.Results[0]; if (uploadResult.UriStatus !== 2000) { throw new Error(`图片上传状态异常: UriStatus=${uploadResult.UriStatus}`); } // 获取完整的URI(包含前缀) const fullImageUri = uploadResult.Uri; // 如: "tos-cn-i-tb4s082cfz/bab623359bd9410da0c1f07897b16fec" // 验证图片信息 const pluginResult = commitResult.Result?.PluginResult?.[0]; if (pluginResult) { logger.info(`图片上传成功详情:`, { imageUri: pluginResult.ImageUri, sourceUri: pluginResult.SourceUri, size: `${pluginResult.ImageWidth}x${pluginResult.ImageHeight}`, format: pluginResult.ImageFormat, fileSize: pluginResult.ImageSize, md5: pluginResult.ImageMd5 }); // 优先使用PluginResult中的ImageUri,因为它可能是最准确的 if (pluginResult.ImageUri) { logger.info(`图片上传完成: ${pluginResult.ImageUri}`); return pluginResult.ImageUri; // 返回完整的URI } } logger.info(`图片上传完成: ${fullImageUri}`); return fullImageUri; // 返回完整的URI } catch (error) { logger.error(`图片上传失败: ${error.message}`); throw error; } } // 从Buffer上传图片 async function uploadImageBuffer(buffer: Buffer, refreshToken: string): Promise { try { logger.info(`开始从Buffer上传图片,大小: ${buffer.length}字节`); // 获取上传凭证 const proofResult = await request( 'POST', '/mweb/v1/get_upload_image_proof', refreshToken, { data: { scene: 'aigc_image', file_name: `${util.uuid()}.jpg`, file_size: buffer.length, } } ); if (!proofResult || !proofResult.proof_info) { logger.error(`获取上传凭证失败: ${JSON.stringify(proofResult)}`); throw new APIException(EX.API_REQUEST_FAILED, '获取上传凭证失败'); } logger.info(`获取上传凭证成功`); // 上传文件 const { proof_info } = proofResult; const uploadProofUrl = 'https://imagex.bytedanceapi.com/'; const formData = new FormData(); const blob = new Blob([buffer], { type: 'image/jpeg' }); formData.append('file', blob, `${util.uuid()}.jpg`); const uploadResult = await fetch(uploadProofUrl + '?' + new URLSearchParams(proof_info.query_params).toString(), { method: 'POST', headers: proof_info.headers, body: formData, }); if (!uploadResult.ok) { logger.error(`上传文件失败: 状态码 ${uploadResult.status}`); throw new APIException(EX.API_REQUEST_FAILED, `上传文件失败: 状态码 ${uploadResult.status}`); } // 验证 proof_info.image_uri 是否存在 if (!proof_info.image_uri) { logger.error(`上传凭证中缺少 image_uri: ${JSON.stringify(proof_info)}`); throw new APIException(EX.API_REQUEST_FAILED, '上传凭证中缺少 image_uri'); } logger.info(`Buffer图片上传成功: ${proof_info.image_uri}`); return proof_info.image_uri; } catch (error) { logger.error(`Buffer图片上传失败: ${error.message}`); throw error; } } // 图片合成功能:先上传图片,然后进行图生图 export async function generateImageComposition( _model: string, prompt: string, imageUrls: (string | Buffer)[], { ratio = "1:1", resolution = "2k", sampleStrength = 0.5, negativePrompt = "", intelligentRatio = false, }: { ratio?: string; resolution?: string; sampleStrength?: number; negativePrompt?: string; intelligentRatio?: boolean; }, refreshToken: string ) { const model = getModel(_model); const draftVersion = getDraftVersion(_model); const imageCount = imageUrls.length; // 解析分辨率 const resolutionResult = resolveResolution(resolution, ratio); const { width, height, imageRatio, resolutionType } = resolutionResult; logger.info(`使用模型: ${_model} 映射模型: ${model} 图生图功能 ${imageCount}张图片 ${width}x${height} (${ratio}@${resolution}) 精细度: ${sampleStrength}`); const { totalCredit } = await getCredit(refreshToken); if (totalCredit <= 0) await receiveCredit(refreshToken); // 上传所有输入图片 const uploadedImageIds: string[] = []; for (let i = 0; i < imageUrls.length; i++) { try { const image = imageUrls[i]; let imageId: string; if (typeof image === 'string') { logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (URL)...`); imageId = await uploadImageFromUrl(image, refreshToken); } else { logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (Buffer)...`); imageId = await uploadImageBuffer(image, refreshToken); } uploadedImageIds.push(imageId); logger.info(`图片 ${i + 1}/${imageCount} 上传成功: ${imageId}`); } catch (error) { logger.error(`图片 ${i + 1}/${imageCount} 上传失败: ${error.message}`); throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `图片上传失败: ${error.message}`); } } logger.info(`所有图片上传完成,开始图生图: ${uploadedImageIds.join(', ')}`); const componentId = util.uuid(); const submitId = util.uuid(); // 构建图生图的 sceneOptions(不包含 benefitCount 以避免扣积分) // 注意:sceneOptions 需要是对象,在 metrics_extra 中会被 JSON.stringify const sceneOption = { type: "image", scene: "ImageBasicGenerate", modelReqKey: _model, resolutionType, abilityList: uploadedImageIds.map(() => ({ abilityName: "byte_edit", strength: sampleStrength, source: { imageUrl: `blob:https://jimeng.jianying.com/${util.uuid()}` } })), reportParams: { enterSource: "generate", vipSource: "generate", extraVipFunctionKey: `${_model}-${resolutionType}`, useVipFunctionDetailsReporterHoc: true, }, }; const { aigc_data } = await request( "post", "/mweb/v1/aigc_draft/generate", refreshToken, { data: { extend: { root_model: model, }, submit_id: submitId, metrics_extra: JSON.stringify({ promptSource: "custom", generateCount: 1, enterFrom: "click", sceneOptions: JSON.stringify([sceneOption]), generateId: submitId, isRegenerate: false }), draft_content: JSON.stringify({ type: "draft", id: util.uuid(), min_version: "3.2.9", min_features: [], is_from_tsn: true, version: "3.2.9", main_component_id: componentId, component_list: [ { type: "image_base_component", id: componentId, min_version: "3.0.2", aigc_mode: "workbench", metadata: { type: "", id: util.uuid(), created_platform: 3, created_platform_version: "", created_time_in_ms: Date.now().toString(), created_did: "", }, generate_type: "blend", abilities: { type: "", id: util.uuid(), blend: { type: "", id: util.uuid(), min_version: "3.2.9", min_features: [], core_param: { type: "", id: util.uuid(), model, prompt: `${'#'.repeat(imageCount * 2)}${prompt}`, sample_strength: sampleStrength, image_ratio: imageRatio, large_image_info: { type: "", id: util.uuid(), height, width, resolution_type: resolutionType }, intelligent_ratio: intelligentRatio, }, ability_list: uploadedImageIds.map((imageId) => ({ type: "", id: util.uuid(), name: "byte_edit", image_uri_list: [imageId], image_list: [{ type: "image", id: util.uuid(), source_from: "upload", platform_type: 1, name: "", image_uri: imageId, width: 0, height: 0, format: "", uri: imageId }], strength: 0.5 })), prompt_placeholder_info_list: uploadedImageIds.map((_, index) => ({ type: "", id: util.uuid(), ability_index: index })), postedit_param: { type: "", id: util.uuid(), generate_type: 0 } }, }, }, ], }), http_common_info: { aid: DEFAULT_ASSISTANT_ID, }, }, } ); const historyId = aigc_data?.history_record_id; if (!historyId) throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在"); logger.info(`图生图任务已提交,history_id: ${historyId},等待生成完成...`); let status = 20, failCode, item_list = []; let pollCount = 0; const maxPollCount = 600; // 最多轮询10分钟 while (pollCount < maxPollCount) { await new Promise((resolve) => setTimeout(resolve, 1000)); pollCount++; if (pollCount % 30 === 0) { logger.info(`图生图进度: 第 ${pollCount} 次轮询 (history_id: ${historyId}),当前状态: ${status},已生成: ${item_list.length} 张图片...`); } const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, { data: { history_ids: [historyId], image_info: { width: 2048, height: 2048, format: "webp", image_scene_list: [ { scene: "smart_crop", width: 360, height: 360, uniq_key: "smart_crop-w:360-h:360", format: "webp", }, { scene: "smart_crop", width: 480, height: 480, uniq_key: "smart_crop-w:480-h:480", format: "webp", }, { scene: "smart_crop", width: 720, height: 720, uniq_key: "smart_crop-w:720-h:720", format: "webp", }, { scene: "smart_crop", width: 720, height: 480, uniq_key: "smart_crop-w:720-h:480", format: "webp", }, { scene: "normal", width: 2400, height: 2400, uniq_key: "2400", format: "webp", }, { scene: "normal", width: 1080, height: 1080, uniq_key: "1080", format: "webp", }, { scene: "normal", width: 720, height: 720, uniq_key: "720", format: "webp", }, { scene: "normal", width: 480, height: 480, uniq_key: "480", format: "webp", }, { scene: "normal", width: 360, height: 360, uniq_key: "360", format: "webp", }, ], }, http_common_info: { aid: DEFAULT_ASSISTANT_ID, }, }, }); if (!result[historyId]) throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在"); status = result[historyId].status; failCode = result[historyId].fail_code; item_list = result[historyId].item_list || []; // 检查是否已生成图片 if (item_list.length > 0) { logger.info(`图生图完成: 状态=${status}, 已生成 ${item_list.length} 张图片`); break; } // 记录详细状态 if (pollCount % 60 === 0) { logger.info(`图生图详细状态: status=${status}, item_list.length=${item_list.length}, failCode=${failCode || 'none'}`); } // 如果状态是完成但图片数量为0,记录并继续等待 if (status === 10 && item_list.length === 0 && pollCount % 30 === 0) { logger.info(`图生图状态已完成但无图片生成: 状态=${status}, 继续等待...`); } } if (pollCount >= maxPollCount) { logger.warn(`图生图超时: 轮询了 ${pollCount} 次,当前状态: ${status},已生成图片数: ${item_list.length}`); } if (status === 30) { if (failCode === '2038') throw new APIException(EX.API_CONTENT_FILTERED); else throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `图生图失败,错误代码: ${failCode}`); } const resultImageUrls = item_list.map((item) => { if(!item?.image?.large_images?.[0]?.image_url) return item?.common_attr?.cover_url || null; return item.image.large_images[0].image_url; }).filter(url => url !== null); logger.info(`图生图结果: 成功生成 ${resultImageUrls.length} 张图片`); return resultImageUrls; } // 多图生成函数(支持jimeng-4.0及以上版本) async function generateMultiImages( _model: string, prompt: string, { ratio = "1:1", resolution = "2k", sampleStrength = 0.5, negativePrompt = "", intelligentRatio = false, }: { ratio?: string; resolution?: string; sampleStrength?: number; negativePrompt?: string; intelligentRatio?: boolean; }, refreshToken: string ) { const model = getModel(_model); // 解析分辨率 const resolutionResult = resolveResolution(resolution, ratio); const { width, height, imageRatio, resolutionType } = resolutionResult; // 从prompt中提取图片数量,默认为4张 const targetImageCount = prompt.match(/(\d+)张/) ? parseInt(prompt.match(/(\d+)张/)[1]) : 4; logger.info(`使用 ${_model} 多图生成: ${targetImageCount}张图片 ${width}x${height} (${ratio}@${resolution}) 精细度: ${sampleStrength}`); const componentId = util.uuid(); const submitId = util.uuid(); // 构建多图模式的 sceneOptions(不包含 benefitCount 以避免扣积分) const sceneOption = { type: "image", scene: "ImageMultiGenerate", modelReqKey: _model, resolutionType, abilityList: [], reportParams: { enterSource: "generate", vipSource: "generate", extraVipFunctionKey: `${_model}-${resolutionType}`, useVipFunctionDetailsReporterHoc: true, }, }; const { aigc_data } = await request( "post", "/mweb/v1/aigc_draft/generate", refreshToken, { data: { extend: { root_model: model, }, submit_id: submitId, metrics_extra: JSON.stringify({ promptSource: "custom", generateCount: 1, enterFrom: "click", sceneOptions: JSON.stringify([sceneOption]), generateId: submitId, isRegenerate: false, templateId: "", templateSource: "", lastRequestId: "", originRequestId: "", }), draft_content: JSON.stringify({ type: "draft", id: util.uuid(), min_version: DRAFT_MIN_VERSION, min_features: [], is_from_tsn: true, version: DRAFT_VERSION, main_component_id: componentId, component_list: [ { type: "image_base_component", id: componentId, min_version: DRAFT_MIN_VERSION, aigc_mode: "workbench", metadata: { type: "", id: util.uuid(), created_platform: 3, created_platform_version: "", created_time_in_ms: Date.now().toString(), created_did: "", }, generate_type: "generate", abilities: { type: "", id: util.uuid(), generate: { type: "", id: util.uuid(), core_param: { type: "", id: util.uuid(), model, prompt, negative_prompt: negativePrompt, seed: Math.floor(Math.random() * 100000000) + 2500000000, sample_strength: sampleStrength, image_ratio: imageRatio, large_image_info: { type: "", id: util.uuid(), min_version: DRAFT_MIN_VERSION, height, width, resolution_type: resolutionType, }, intelligent_ratio: intelligentRatio, }, gen_option: { type: "", id: util.uuid(), generate_all: false, }, }, }, }, ], }), http_common_info: { aid: DEFAULT_ASSISTANT_ID, }, }, } ); const historyId = aigc_data?.history_record_id; if (!historyId) throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在"); logger.info(`多图生成任务已提交,submit_id: ${submitId}, history_id: ${historyId},等待生成 ${targetImageCount} 张图片...`); // 直接使用 history_id 轮询生成结果(增加轮询时间) let status = 20, failCode, item_list = []; let pollCount = 0; const maxPollCount = 600; // 最多轮询10分钟(600次 * 1秒) while (pollCount < maxPollCount) { await new Promise((resolve) => setTimeout(resolve, 1000)); // 每1秒轮询一次 pollCount++; if (pollCount % 30 === 0) { logger.info(`多图生成进度: 第 ${pollCount} 次轮询 (history_id: ${historyId}),当前状态: ${status},已生成: ${item_list.length}/${targetImageCount} 张图片...`); } const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, { data: { history_ids: [historyId], image_info: { width: 2048, height: 2048, format: "webp", image_scene_list: [ { scene: "smart_crop", width: 360, height: 360, uniq_key: "smart_crop-w:360-h:360", format: "webp", }, { scene: "smart_crop", width: 480, height: 480, uniq_key: "smart_crop-w:480-h:480", format: "webp", }, { scene: "smart_crop", width: 720, height: 720, uniq_key: "smart_crop-w:720-h:720", format: "webp", }, { scene: "smart_crop", width: 720, height: 480, uniq_key: "smart_crop-w:720-h:480", format: "webp", }, { scene: "normal", width: 2400, height: 2400, uniq_key: "2400", format: "webp", }, { scene: "normal", width: 1080, height: 1080, uniq_key: "1080", format: "webp", }, { scene: "normal", width: 720, height: 720, uniq_key: "720", format: "webp", }, { scene: "normal", width: 480, height: 480, uniq_key: "480", format: "webp", }, { scene: "normal", width: 360, height: 360, uniq_key: "360", format: "webp", }, ], }, http_common_info: { aid: DEFAULT_ASSISTANT_ID, }, }, }); if (!result[historyId]) throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在"); status = result[historyId].status; failCode = result[historyId].fail_code; item_list = result[historyId].item_list || []; // 检查是否已生成足够的图片 if (item_list.length >= targetImageCount) { logger.info(`多图生成完成: 状态=${status}, 已生成 ${item_list.length} 张图片`); break; } // 记录详细状态 if (pollCount % 60 === 0) { logger.info(`jimeng-4.0 详细状态: status=${status}, item_list.length=${item_list.length}, failCode=${failCode || 'none'}`); } // 如果状态是完成但图片数量不够,记录并继续等待 if (status === 10 && item_list.length < targetImageCount && pollCount % 30 === 0) { logger.info(`jimeng-4.0 状态已完成但图片数量不足: 状态=${status}, 已生成 ${item_list.length}/${targetImageCount} 张图片,继续等待...`); } } if (pollCount >= maxPollCount) { logger.warn(`多图生成超时: 轮询了 ${pollCount} 次,当前状态: ${status},已生成图片数: ${item_list.length}`); } if (status === 30) { if (failCode === '2038') throw new APIException(EX.API_CONTENT_FILTERED); else throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `生成失败,错误代码: ${failCode}`); } const imageUrls = item_list.map((item) => { if(!item?.image?.large_images?.[0]?.image_url) return item?.common_attr?.cover_url || null; return item.image.large_images[0].image_url; }).filter(url => url !== null); logger.info(`多图生成结果: 成功生成 ${imageUrls.length} 张图片`); return imageUrls; } export async function generateImages( _model: string, prompt: string, { ratio = "1:1", resolution = "2k", sampleStrength = 0.5, negativePrompt = "", intelligentRatio = false, }: { ratio?: string; resolution?: string; sampleStrength?: number; negativePrompt?: string; intelligentRatio?: boolean; }, refreshToken: string ) { const model = getModel(_model); // 解析分辨率 const resolutionResult = resolveResolution(resolution, ratio); const { width, height, imageRatio, resolutionType } = resolutionResult; logger.info(`使用模型: ${_model} 映射模型: ${model} ${width}x${height} (${ratio}@${resolution}) 精细度: ${sampleStrength}`); const { totalCredit } = await getCredit(refreshToken); if (totalCredit <= 0) await receiveCredit(refreshToken); // 检测是否为多图生成请求 const isMultiImageRequest = (/jimeng-4\.[0-9]+/.test(_model)) && ( prompt.includes("连续") || prompt.includes("绘本") || prompt.includes("故事") || /\d+张/.test(prompt) ); // 如果是多图请求,使用专门的处理逻辑 if (isMultiImageRequest) { return await generateMultiImages(_model, prompt, { ratio, resolution, sampleStrength, negativePrompt, intelligentRatio }, refreshToken); } const componentId = util.uuid(); const submitId = util.uuid(); // 构建 sceneOptions 用于 metrics_extra(不包含 benefitCount 以避免扣积分) const sceneOption = { type: "image", scene: "ImageBasicGenerate", modelReqKey: _model, resolutionType, abilityList: [], reportParams: { enterSource: "generate", vipSource: "generate", extraVipFunctionKey: `${_model}-${resolutionType}`, useVipFunctionDetailsReporterHoc: true, }, }; const { aigc_data } = await request( "post", "/mweb/v1/aigc_draft/generate", refreshToken, { data: { extend: { root_model: model, }, submit_id: submitId, metrics_extra: JSON.stringify({ promptSource: "custom", generateCount: 1, enterFrom: "click", sceneOptions: JSON.stringify([sceneOption]), generateId: submitId, isRegenerate: false, }), draft_content: JSON.stringify({ type: "draft", id: util.uuid(), min_version: DRAFT_MIN_VERSION, min_features: [], is_from_tsn: true, version: DRAFT_VERSION, main_component_id: componentId, component_list: [ { type: "image_base_component", id: componentId, min_version: DRAFT_MIN_VERSION, aigc_mode: "workbench", metadata: { type: "", id: util.uuid(), created_platform: 3, created_platform_version: "", created_time_in_ms: Date.now().toString(), created_did: "", }, generate_type: "generate", abilities: { type: "", id: util.uuid(), generate: { type: "", id: util.uuid(), core_param: { type: "", id: util.uuid(), model, prompt, negative_prompt: negativePrompt, seed: Math.floor(Math.random() * 100000000) + 2500000000, sample_strength: sampleStrength, image_ratio: imageRatio, large_image_info: { type: "", id: util.uuid(), min_version: DRAFT_MIN_VERSION, height, width, resolution_type: resolutionType, }, intelligent_ratio: intelligentRatio, }, gen_option: { type: "", id: util.uuid(), generate_all: false, }, }, }, }, ], }), http_common_info: { aid: DEFAULT_ASSISTANT_ID, }, }, } ); const historyId = aigc_data.history_record_id; if (!historyId) throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在"); logger.info(`文生图任务已提交,submit_id: ${submitId}, history_id: ${historyId},等待生成完成...`); let status = 20, failCode, item_list = []; let pollCount = 0; const maxPollCount = 600; // 最多轮询10分钟 while (pollCount < maxPollCount) { await new Promise((resolve) => setTimeout(resolve, 1000)); pollCount++; if (pollCount % 30 === 0) { logger.info(`文生图进度: 第 ${pollCount} 次轮询 (history_id: ${historyId}),当前状态: ${status},已生成: ${item_list.length} 张图片...`); } const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, { data: { history_ids: [historyId], image_info: { width: 2048, height: 2048, format: "webp", image_scene_list: [ { scene: "smart_crop", width: 360, height: 360, uniq_key: "smart_crop-w:360-h:360", format: "webp", }, { scene: "smart_crop", width: 480, height: 480, uniq_key: "smart_crop-w:480-h:480", format: "webp", }, { scene: "smart_crop", width: 720, height: 720, uniq_key: "smart_crop-w:720-h:720", format: "webp", }, { scene: "smart_crop", width: 720, height: 480, uniq_key: "smart_crop-w:720-h:480", format: "webp", }, { scene: "smart_crop", width: 360, height: 240, uniq_key: "smart_crop-w:360-h:240", format: "webp", }, { scene: "smart_crop", width: 240, height: 320, uniq_key: "smart_crop-w:240-h:320", format: "webp", }, { scene: "smart_crop", width: 480, height: 640, uniq_key: "smart_crop-w:480-h:640", format: "webp", }, { scene: "normal", width: 2400, height: 2400, uniq_key: "2400", format: "webp", }, { scene: "normal", width: 1080, height: 1080, uniq_key: "1080", format: "webp", }, { scene: "normal", width: 720, height: 720, uniq_key: "720", format: "webp", }, { scene: "normal", width: 480, height: 480, uniq_key: "480", format: "webp", }, { scene: "normal", width: 360, height: 360, uniq_key: "360", format: "webp", }, ], }, http_common_info: { aid: DEFAULT_ASSISTANT_ID, }, }, }); if (!result[historyId]) throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在"); status = result[historyId].status; failCode = result[historyId].fail_code; item_list = result[historyId].item_list || []; // 检查是否已生成图片 if (item_list.length > 0) { logger.info(`文生图完成: 状态=${status}, 已生成 ${item_list.length} 张图片`); break; } // 记录详细状态 if (pollCount % 60 === 0) { logger.info(`文生图详细状态: status=${status}, item_list.length=${item_list.length}, failCode=${failCode || 'none'}`); } // 如果状态是完成但图片数量为0,记录并继续等待 if (status === 10 && item_list.length === 0 && pollCount % 30 === 0) { logger.info(`文生图状态已完成但无图片生成: 状态=${status}, 继续等待...`); } } if (pollCount >= maxPollCount) { logger.warn(`文生图超时: 轮询了 ${pollCount} 次,当前状态: ${status},已生成图片数: ${item_list.length}`); } if (status === 30) { if (failCode === '2038') throw new APIException(EX.API_CONTENT_FILTERED); else throw new APIException(EX.API_IMAGE_GENERATION_FAILED); } const imageUrls = item_list.map((item) => { if(!item?.image?.large_images?.[0]?.image_url) return item?.common_attr?.cover_url || null; return item.image.large_images[0].image_url; }).filter(url => url !== null); logger.info(`文生图结果: 成功生成 ${imageUrls.length} 张图片`); return imageUrls; } export default { generateImages, generateImageComposition, };