| 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"; |
| |
| const DEFAULT_ASSISTANT_ID = "513695"; |
| |
| const VERSION_CODE = "5.8.0"; |
| |
| const PLATFORM_CODE = "7"; |
| |
| const DEVICE_ID = Math.random() * 999999999999999999 + 7000000000000000000; |
| |
| const WEB_ID = Math.random() * 999999999999999999 + 7000000000000000000; |
| |
| const USER_ID = util.uuid(false); |
| |
| const MAX_RETRY_COUNT = 3; |
| |
| const RETRY_DELAY = 5000; |
| |
| 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", |
| "Last-event-id": "undefined", |
| 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="131", "Chromium";v="131", "Not_A Brand";v="24"', |
| "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/131.0.0.0 Safari/537.36", |
| }; |
| |
| const FILE_MAX_SIZE = 100 * 1024 * 1024; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function acquireToken(refreshToken: string): Promise<string> { |
| return refreshToken; |
| } |
|
|
| |
| |
| |
| 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("; "); |
| } |
|
|
| |
| |
| |
| |
| |
| 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: { |
| |
| Referer: "https://jimeng.jianying.com/ai-tool/image/generate", |
| |
| |
| } |
| }); |
| 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 |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| 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", |
| web_id: WEB_ID, |
| ...(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, |
| validateStatus: () => true, |
| ..._.omit(options, "params", "headers"), |
| }); |
| |
| |
| logger.info(`响应状态: ${response.status} ${response.statusText}`); |
| |
| |
| if (options.responseType == "stream") return response; |
| |
| |
| const responseDataSummary = JSON.stringify(response.data).substring(0, 500) + |
| (JSON.stringify(response.data).length > 500 ? "..." : ""); |
| logger.info(`响应数据摘要: ${responseDataSummary}`); |
| |
| |
| 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; |
| } |
| |
| |
| |
| |
| |
| |
| 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` |
| ); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export async function uploadFile( |
| refreshToken: string, |
| fileUrl: string, |
| isVideoImage: boolean = false |
| ) { |
| try { |
| logger.info(`开始上传文件: ${fileUrl}, 视频图像模式: ${isVideoImage}`); |
| |
| |
| await checkFileUrl(fileUrl); |
|
|
| let filename, fileData, mimeType; |
| |
| 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", |
| |
| maxContentLength: FILE_MAX_SIZE, |
| |
| timeout: 60000, |
| })); |
| logger.info(`文件下载完成,文件名: ${filename}, 大小: ${fileData.length}字节`); |
| } |
|
|
| |
| mimeType = mimeType || mime.getType(filename); |
| logger.info(`文件MIME类型: ${mimeType}`); |
| |
| |
| 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}`); |
| } |
| |
| |
| 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; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| 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}`); |
| } |
|
|
| |
| |
| |
| |
| |
| export function tokenSplit(authorization: string) { |
| return authorization.replace("Bearer ", "").split(","); |
| } |
|
|
| |
| |
| |
| 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; |
| } |
| } |