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