import { PassThrough } from "stream"; import path from "path"; import _ from "lodash"; import mime from "mime"; import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import APIException from "@/lib/exceptions/APIException.ts"; import EX from "@/api/consts/exceptions.ts"; import { createParser } from "eventsource-parser"; import logger from "@/lib/logger.ts"; import util from "@/lib/util.ts"; // 模型名称 const MODEL_NAME = "jimeng"; // 默认的AgentID const DEFAULT_ASSISTANT_ID = 513695; // 版本号 const VERSION_CODE = "5.8.0"; // 平台代码 const PLATFORM_CODE = "7"; // 设备ID const DEVICE_ID = Math.random() * 999999999999999999 + 7000000000000000000; // WebID const WEB_ID = Math.random() * 999999999999999999 + 7000000000000000000; // 用户ID const USER_ID = util.uuid(false); // 最大重试次数 const MAX_RETRY_COUNT = 3; // 重试延迟 const RETRY_DELAY = 5000; // 伪装headers const FAKE_HEADERS = { Accept: "application/json, text/plain, */*", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-language": "zh-CN,zh;q=0.9", "Cache-control": "no-cache", Appid: DEFAULT_ASSISTANT_ID, Appvr: VERSION_CODE, Origin: "https://jimeng.jianying.com", Pragma: "no-cache", Priority: "u=1, i", Referer: "https://jimeng.jianying.com", Pf: PLATFORM_CODE, "Sec-Ch-Ua": '"Google Chrome";v="142", "Chromium";v="142", "Not_A Brand";v="99"', "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": '"Windows"', "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36", }; // 文件最大大小 const FILE_MAX_SIZE = 100 * 1024 * 1024; /** * 获取缓存中的access_token * * 目前jimeng的access_token是固定的,暂无刷新功能 * * @param refreshToken 用于刷新access_token的refresh_token */ export async function acquireToken(refreshToken: string): Promise { return refreshToken; } /** * 生成cookie */ export function generateCookie(refreshToken: string) { return [ `_tea_web_id=${WEB_ID}`, `is_staff_user=false`, `store-region=cn-gd`, `store-region-src=uid`, `sid_guard=${refreshToken}%7C${util.unixTimestamp()}%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT`, `uid_tt=${USER_ID}`, `uid_tt_ss=${USER_ID}`, `sid_tt=${refreshToken}`, `sessionid=${refreshToken}`, `sessionid_ss=${refreshToken}`, `sid_tt=${refreshToken}` ].join("; "); } /** * 获取积分信息 * * @param refreshToken 用于刷新access_token的refresh_token */ export async function getCredit(refreshToken: string) { const { credit: { gift_credit, purchase_credit, vip_credit } } = await request("POST", "/commerce/v1/benefits/user_credit", refreshToken, { data: {}, headers: { // Cookie: 'x-web-secsdk-uid=ef44bd0d-0cf6-448c-b517-fd1b5a7267ba; s_v_web_id=verify_m4b1lhlu_DI8qKRlD_7mJJ_4eqx_9shQ_s8eS2QLAbc4n; passport_csrf_token=86f3619c0c4a9c13f24117f71dc18524; passport_csrf_token_default=86f3619c0c4a9c13f24117f71dc18524; n_mh=9-mIeuD4wZnlYrrOvfzG3MuT6aQmCUtmr8FxV8Kl8xY; sid_guard=a7eb745aec44bb3186dbc2083ea9e1a6%7C1733386629%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT; uid_tt=59a46c7d3f34bda9588b93590cca2e12; uid_tt_ss=59a46c7d3f34bda9588b93590cca2e12; sid_tt=a7eb745aec44bb3186dbc2083ea9e1a6; sessionid=a7eb745aec44bb3186dbc2083ea9e1a6; sessionid_ss=a7eb745aec44bb3186dbc2083ea9e1a6; is_staff_user=false; sid_ucp_v1=1.0.0-KGRiOGY2ODQyNWU1OTk3NzRhYTE2ZmZhYmFjNjdmYjY3NzRmZGRiZTgKHgjToPCw0cwbEIXDxboGGJ-tHyAMMITDxboGOAhAJhoCaGwiIGE3ZWI3NDVhZWM0NGJiMzE4NmRiYzIwODNlYTllMWE2; ssid_ucp_v1=1.0.0-KGRiOGY2ODQyNWU1OTk3NzRhYTE2ZmZhYmFjNjdmYjY3NzRmZGRiZTgKHgjToPCw0cwbEIXDxboGGJ-tHyAMMITDxboGOAhAJhoCaGwiIGE3ZWI3NDVhZWM0NGJiMzE4NmRiYzIwODNlYTllMWE2; store-region=cn-gd; store-region-src=uid; user_spaces_idc={"7444764277623653426":"lf"}; ttwid=1|cxHJViEev1mfkjntdMziir8SwbU8uPNVSaeh9QpEUs8|1733966961|d8d52f5f56607427691be4ac44253f7870a34d25dd05a01b4d89b8a7c5ea82ad; _tea_web_id=7444838473275573797; fpk1=fa6c6a4d9ba074b90003896f36b6960066521c1faec6a60bdcb69ec8ddf85e8360b4c0704412848ec582b2abca73d57a; odin_tt=efe9dc150207879b88509e651a1c4af4e7ffb4cfcb522425a75bd72fbf894eda570bbf7ffb551c8b1de0aa2bfa0bd1be6c4157411ecdcf4464fcaf8dd6657d66', Referer: "https://jimeng.jianying.com/ai-tool/image/generate", // "Device-Time": 1733966964, // Sign: "f3dbb824b378abea7c03cbb152b3a365" } }); logger.info(`\n积分信息: \n赠送积分: ${gift_credit}, 购买积分: ${purchase_credit}, VIP积分: ${vip_credit}`); return { giftCredit: gift_credit, purchaseCredit: purchase_credit, vipCredit: vip_credit, totalCredit: gift_credit + purchase_credit + vip_credit } } /** * 接收今日积分 * * @param refreshToken 用于刷新access_token的refresh_token */ export async function receiveCredit(refreshToken: string) { logger.info("正在收取今日积分...") const { cur_total_credits, receive_quota } = await request("POST", "/commerce/v1/benefits/credit_receive", refreshToken, { data: { time_zone: "Asia/Shanghai" }, headers: { Referer: "https://jimeng.jianying.com/ai-tool/image/generate" } }); logger.info(`\n今日${receive_quota}积分收取成功\n剩余积分: ${cur_total_credits}`); return cur_total_credits; } /** * 请求jimeng * * @param method 请求方法 * @param uri 请求路径 * @param params 请求参数 * @param headers 请求头 */ export async function request( method: string, uri: string, refreshToken: string, options: AxiosRequestConfig = {} ) { const token = await acquireToken(refreshToken); const deviceTime = util.unixTimestamp(); const sign = util.md5( `9e2c|${uri.slice(-7)}|${PLATFORM_CODE}|${VERSION_CODE}|${deviceTime}||11ac` ); const fullUrl = `https://jimeng.jianying.com${uri}`; const requestParams = { aid: DEFAULT_ASSISTANT_ID, device_platform: "web", region: "CN", webId: WEB_ID, da_version: "3.3.2", web_component_open_flag: 1, web_version: "7.5.0", aigc_features: "app_lip_sync", ...(options.params || {}), }; const headers = { ...FAKE_HEADERS, Cookie: generateCookie(token), "Device-Time": deviceTime, Sign: sign, "Sign-Ver": "1", ...(options.headers || {}), }; logger.info(`发送请求: ${method.toUpperCase()} ${fullUrl}`); logger.info(`请求参数: ${JSON.stringify(requestParams)}`); logger.info(`请求数据: ${JSON.stringify(options.data || {})}`); // 添加重试逻辑 let retries = 0; const maxRetries = 3; // 最大重试次数 let lastError = null; while (retries <= maxRetries) { try { if (retries > 0) { logger.info(`第 ${retries} 次重试请求: ${method.toUpperCase()} ${fullUrl}`); // 重试前等待一段时间 await new Promise(resolve => setTimeout(resolve, 1000 * retries)); } const response = await axios.request({ method, url: fullUrl, params: requestParams, headers: headers, timeout: 45000, // 增加超时时间到45秒 validateStatus: () => true, // 允许任何状态码 ..._.omit(options, "params", "headers"), }); // 记录响应状态和头信息 logger.info(`响应状态: ${response.status} ${response.statusText}`); // 流式响应直接返回response if (options.responseType == "stream") return response; // 记录响应数据摘要 const responseDataSummary = JSON.stringify(response.data).substring(0, 500) + (JSON.stringify(response.data).length > 500 ? "..." : ""); logger.info(`响应数据摘要: ${responseDataSummary}`); // 检查HTTP状态码 if (response.status >= 400) { logger.warn(`HTTP错误: ${response.status} ${response.statusText}`); if (retries < maxRetries) { retries++; continue; } } return checkResult(response); } catch (error) { lastError = error; logger.error(`请求失败 (尝试 ${retries + 1}/${maxRetries + 1}): ${error.message}`); // 如果是网络错误或超时,尝试重试 if ((error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' || error.message.includes('timeout') || error.message.includes('network')) && retries < maxRetries) { retries++; continue; } // 其他错误直接抛出 break; } } // 所有重试都失败了,抛出最后一个错误 logger.error(`请求失败,已重试 ${retries} 次: ${lastError.message}`); if (lastError.response) { logger.error(`响应状态: ${lastError.response.status}`); logger.error(`响应数据: ${JSON.stringify(lastError.response.data)}`); } throw lastError; } /** * 预检查文件URL有效性 * * @param fileUrl 文件URL */ export async function checkFileUrl(fileUrl: string) { if (util.isBASE64Data(fileUrl)) return; const result = await axios.head(fileUrl, { timeout: 15000, validateStatus: () => true, }); if (result.status >= 400) throw new APIException( EX.API_FILE_URL_INVALID, `File ${fileUrl} is not valid: [${result.status}] ${result.statusText}` ); // 检查文件大小 if (result.headers && result.headers["content-length"]) { const fileSize = parseInt(result.headers["content-length"], 10); if (fileSize > FILE_MAX_SIZE) throw new APIException( EX.API_FILE_EXECEEDS_SIZE, `File ${fileUrl} is not valid` ); } } /** * 上传文件 * * @param refreshToken 用于刷新access_token的refresh_token * @param fileUrl 文件URL或BASE64数据 * @param isVideoImage 是否是用于视频图像 * @returns 上传结果,包含image_uri */ export async function uploadFile( refreshToken: string, fileUrl: string, isVideoImage: boolean = false ) { try { logger.info(`开始上传文件: ${fileUrl}, 视频图像模式: ${isVideoImage}`); // 预检查远程文件URL可用性 await checkFileUrl(fileUrl); let filename, fileData, mimeType; // 如果是BASE64数据则直接转换为Buffer if (util.isBASE64Data(fileUrl)) { mimeType = util.extractBASE64DataFormat(fileUrl); const ext = mime.getExtension(mimeType); filename = `${util.uuid()}.${ext}`; fileData = Buffer.from(util.removeBASE64DataHeader(fileUrl), "base64"); logger.info(`处理BASE64数据,文件名: ${filename}, 类型: ${mimeType}, 大小: ${fileData.length}字节`); } // 下载文件到内存,如果您的服务器内存很小,建议考虑改造为流直传到下一个接口上,避免停留占用内存 else { filename = path.basename(fileUrl); logger.info(`开始下载远程文件: ${fileUrl}`); ({ data: fileData } = await axios.get(fileUrl, { responseType: "arraybuffer", // 100M限制 maxContentLength: FILE_MAX_SIZE, // 60秒超时 timeout: 60000, })); logger.info(`文件下载完成,文件名: ${filename}, 大小: ${fileData.length}字节`); } // 获取文件的MIME类型 mimeType = mimeType || mime.getType(filename); logger.info(`文件MIME类型: ${mimeType}`); // 构建FormData const formData = new FormData(); const blob = new Blob([fileData], { type: mimeType }); formData.append('file', blob, filename); // 获取上传凭证 logger.info(`请求上传凭证,场景: ${isVideoImage ? 'video_cover' : 'aigc_image'}`); const uploadProofUrl = 'https://imagex.bytedanceapi.com/'; const proofResult = await request( 'POST', '/mweb/v1/get_upload_image_proof', refreshToken, { data: { scene: isVideoImage ? 'video_cover' : 'aigc_image', file_name: filename, file_size: fileData.length, } } ); if (!proofResult || !proofResult.proof_info) { logger.error(`获取上传凭证失败: ${JSON.stringify(proofResult)}`); throw new APIException(EX.API_REQUEST_FAILED, '获取上传凭证失败'); } logger.info(`获取上传凭证成功`); // 上传文件 const { proof_info } = proofResult; logger.info(`开始上传文件到: ${uploadProofUrl}`); const uploadResult = await axios.post( uploadProofUrl, formData, { headers: { ...proof_info.headers, 'Content-Type': 'multipart/form-data', }, params: proof_info.query_params, timeout: 60000, validateStatus: () => true, // 允许任何状态码以便详细处理 } ); logger.info(`上传响应状态: ${uploadResult.status}`); if (!uploadResult || uploadResult.status !== 200) { logger.error(`上传文件失败: 状态码 ${uploadResult?.status}, 响应: ${JSON.stringify(uploadResult?.data)}`); 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(`文件上传成功: ${proof_info.image_uri}`); // 返回上传结果 return { image_uri: proof_info.image_uri, uri: proof_info.image_uri, } } catch (error) { logger.error(`文件上传过程中发生错误: ${error.message}`); throw error; } } /** * 检查请求结果 * * @param result 结果 */ export function checkResult(result: AxiosResponse) { const { ret, errmsg, data } = result.data; if (!_.isFinite(Number(ret))) return result.data; if (ret === '0') return data; if (ret === '5000') throw new APIException(EX.API_IMAGE_GENERATION_INSUFFICIENT_POINTS, `[无法生成图像]: 即梦积分可能不足,${errmsg}`); throw new APIException(EX.API_REQUEST_FAILED, `[请求jimeng失败]: ${errmsg}`); } /** * Token切分 * * @param authorization 认证字符串 */ export function tokenSplit(authorization: string) { return authorization.replace("Bearer ", "").split(","); } /** * 获取Token存活状态 */ export async function getTokenLiveStatus(refreshToken: string) { const result = await request( "POST", "/passport/account/info/v2", refreshToken, { params: { account_sdk_source: "web", }, } ); try { const { user_id } = checkResult(result); return !!user_id; } catch (err) { return false; } }