| import path from "path"; |
| import _ from "lodash"; |
| import mime from "mime"; |
| import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; |
| import { HttpsProxyAgent } from "https-proxy-agent"; |
| import { SocksProxyAgent } from "socks-proxy-agent"; |
|
|
| import APIException from "@/lib/exceptions/APIException.ts"; |
| import EX from "@/api/consts/exceptions.ts"; |
| import logger from "@/lib/logger.ts"; |
| import util from "@/lib/util.ts"; |
| import { JimengErrorHandler, JimengErrorResponse } from "@/lib/error-handler.ts"; |
| import { BASE_URL_DREAMINA_US, BASE_URL_DREAMINA_HK, DA_VERSION, WEB_VERSION } from "@/api/consts/dreamina.ts"; |
|
|
| import { |
| BASE_URL_CN, |
| BASE_URL_US_COMMERCE, |
| BASE_URL_HK_COMMERCE, |
| BASE_URL_HK, |
| DEFAULT_ASSISTANT_ID_CN, |
| DEFAULT_ASSISTANT_ID_US, |
| DEFAULT_ASSISTANT_ID_HK, |
| DEFAULT_ASSISTANT_ID_JP, |
| DEFAULT_ASSISTANT_ID_SG, |
| PLATFORM_CODE, |
| REGION_CN, |
| REGION_US, |
| REGION_HK, |
| REGION_JP, |
| REGION_SG, |
| VERSION_CODE, |
| RETRY_CONFIG |
| } from "@/api/consts/common.ts"; |
|
|
| |
| const MODEL_NAME = "jimeng"; |
| |
| const DEVICE_ID = Math.random() * 999999999999999999 + 7000000000000000000; |
| |
| const WEB_ID = Math.random() * 999999999999999999 + 7000000000000000000; |
| |
| const USER_ID = util.uuid(false); |
| |
| 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", |
| Appvr: VERSION_CODE, |
| Pragma: "no-cache", |
| Priority: "u=1, i", |
| 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 interface RegionInfo { |
| isUS: boolean; |
| isHK: boolean; |
| isJP: boolean; |
| isSG: boolean; |
| isInternational: boolean; |
| isCN: boolean; |
| } |
|
|
| export interface TokenWithProxy { |
| token: string; |
| proxyUrl: string | null; |
| } |
|
|
| export function parseProxyFromToken(rawToken: string): TokenWithProxy { |
| const tokenValue = rawToken.trim(); |
| const proxyPattern = /^(https?|socks(?:4|5)?):\/\//i; |
| if (!proxyPattern.test(tokenValue)) return { token: tokenValue, proxyUrl: null }; |
|
|
| const lastAtIndex = tokenValue.lastIndexOf("@"); |
| if (lastAtIndex <= 0 || lastAtIndex === tokenValue.length - 1) |
| return { token: tokenValue, proxyUrl: null }; |
|
|
| const proxyUrl = tokenValue.slice(0, lastAtIndex); |
| const token = tokenValue.slice(lastAtIndex + 1); |
| if (!proxyUrl || !token) return { token: tokenValue, proxyUrl: null }; |
|
|
| return { token, proxyUrl }; |
| } |
|
|
| export function parseRegionFromToken(refreshToken: string): RegionInfo { |
| const { token: parsedToken } = parseProxyFromToken(refreshToken); |
| const token = parsedToken.toLowerCase(); |
| const isUS = token.startsWith('us-'); |
| const isHK = token.startsWith('hk-'); |
| const isJP = token.startsWith('jp-'); |
| const isSG = token.startsWith('sg-'); |
| const isInternational = isUS || isHK || isJP || isSG; |
|
|
| return { |
| isUS, |
| isHK, |
| isJP, |
| isSG, |
| isInternational, |
| isCN: !isInternational |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function getRefererByRegion(refreshToken: string, cnPath: string): string { |
| const { isInternational } = parseRegionFromToken(refreshToken); |
| return isInternational |
| ? "https://dreamina.capcut.com/" |
| : `https://jimeng.jianying.com${cnPath}`; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function getAssistantId(regionInfo: RegionInfo): number { |
| if (regionInfo.isUS) return DEFAULT_ASSISTANT_ID_US; |
| if (regionInfo.isJP) return DEFAULT_ASSISTANT_ID_JP; |
| if (regionInfo.isSG) return DEFAULT_ASSISTANT_ID_SG; |
| if (regionInfo.isHK) return DEFAULT_ASSISTANT_ID_HK; |
| return DEFAULT_ASSISTANT_ID_CN; |
| } |
|
|
| |
| |
| |
| export function generateCookie(refreshToken: string) { |
| const { token: tokenWithRegion } = parseProxyFromToken(refreshToken); |
| const { isUS, isHK, isJP, isSG } = parseRegionFromToken(tokenWithRegion); |
| const token = (isUS || isHK || isJP || isSG) |
| ? tokenWithRegion.substring(3) |
| : tokenWithRegion; |
|
|
| return [ |
| `_tea_web_id=${WEB_ID}`, |
| `is_staff_user=false`, |
| `sid_guard=${token}%7C${util.unixTimestamp()}%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT`, |
| `uid_tt=${USER_ID}`, |
| `uid_tt_ss=${USER_ID}`, |
| `sid_tt=${token}`, |
| `sessionid=${token}`, |
| `sessionid_ss=${token}`, |
| ].join("; "); |
| } |
|
|
| |
| |
| |
| |
| |
| export async function getCredit(refreshToken: string) { |
| const referer = getRefererByRegion(refreshToken, "/ai-tool/image/generate"); |
|
|
| const { |
| credit: { gift_credit, purchase_credit, vip_credit } |
| } = await request("POST", "/commerce/v1/benefits/user_credit", refreshToken, { |
| data: {}, |
| headers: { |
| Referer: referer, |
| }, |
| noDefaultParams: true |
| }); |
| 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 referer = getRefererByRegion(refreshToken, "/ai-tool/home"); |
|
|
| const { receive_quota } = await request("POST", "/commerce/v1/benefits/credit_receive", refreshToken, { |
| data: { |
| time_zone: "Asia/Shanghai" |
| }, |
| headers: { |
| Referer: referer |
| } |
| }); |
| logger.info(`今日${receive_quota}积分收取成功`); |
| return receive_quota; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export async function request( |
| method: string, |
| uri: string, |
| refreshToken: string, |
| options: AxiosRequestConfig & { noDefaultParams?: boolean } = {} |
| ) { |
| const { token: tokenWithRegion, proxyUrl } = parseProxyFromToken(refreshToken); |
| const regionInfo = parseRegionFromToken(tokenWithRegion); |
| const { isUS, isHK, isJP, isSG } = regionInfo; |
| await acquireToken(regionInfo.isInternational ? tokenWithRegion.substring(3) : tokenWithRegion); |
| const deviceTime = util.unixTimestamp(); |
| const sign = util.md5( |
| `9e2c|${uri.slice(-7)}|${PLATFORM_CODE}|${VERSION_CODE}|${deviceTime}||11ac` |
| ); |
|
|
| let baseUrl: string; |
| let aid: number; |
| let region: string; |
|
|
| if (isUS) { |
| if (uri.startsWith("/commerce/")) { |
| baseUrl = BASE_URL_US_COMMERCE; |
| } else { |
| baseUrl = BASE_URL_DREAMINA_US; |
| } |
| aid = DEFAULT_ASSISTANT_ID_US; |
| region = REGION_US; |
| } else if (isHK || isJP || isSG) { |
| |
| if (uri.startsWith("/commerce/")) { |
| baseUrl = BASE_URL_HK_COMMERCE; |
| } else { |
| baseUrl = BASE_URL_DREAMINA_HK; |
| } |
| if (isJP) { |
| aid = DEFAULT_ASSISTANT_ID_JP; |
| region = REGION_JP; |
| } else if (isSG) { |
| aid = DEFAULT_ASSISTANT_ID_SG; |
| region = REGION_SG; |
| } else { |
| aid = DEFAULT_ASSISTANT_ID_HK; |
| region = REGION_HK; |
| } |
| } else { |
| |
| baseUrl = BASE_URL_CN; |
| aid = DEFAULT_ASSISTANT_ID_CN; |
| region = REGION_CN; |
| } |
|
|
| const origin = new URL(baseUrl).origin; |
|
|
| const fullUrl = `${baseUrl}${uri}`; |
| const requestParams = options.noDefaultParams ? (options.params || {}) : { |
| aid: aid, |
| device_platform: "web", |
| region: region, |
| ...(isUS || isHK || isJP || isSG ? {} : { webId: WEB_ID }), |
| da_version: DA_VERSION, |
| os: "windows", |
| web_component_open_flag: 1, |
| web_version: WEB_VERSION, |
| aigc_features: "app_lip_sync", |
| ...(options.params || {}), |
| }; |
|
|
| const headers = { |
| ...FAKE_HEADERS, |
| Origin: origin, |
| Referer: origin, |
| "App-Sdk-Version": "48.0.0", |
| Appid: aid, |
| Cookie: generateCookie(tokenWithRegion), |
| "Device-Time": deviceTime, |
| Lan: isUS ? "en" : isJP ? "ja" : (isHK || isSG) ? "en" : "zh-Hans", |
| Loc: isUS ? "us" : isJP ? "jp" : isHK ? "hk" : isSG ? "sg" : "cn", |
| Sign: sign, |
| "Sign-Ver": "1", |
| Tdid: "", |
| ...(options.headers || {}), |
| }; |
|
|
| logger.info(`发送请求: ${method.toUpperCase()} ${fullUrl}`); |
| if (proxyUrl) { |
| const maskedProxyUrl = proxyUrl.replace(/\/\/([^@/]+)@/i, "//***@"); |
| logger.info(`使用代理: ${maskedProxyUrl}`); |
| } |
| logger.info(`请求参数: ${JSON.stringify(requestParams)}`); |
| logger.info(`请求数据: ${JSON.stringify(options.data || {})}`); |
|
|
| const proxyAgent = proxyUrl |
| ? (proxyUrl.toLowerCase().startsWith("socks") |
| ? new SocksProxyAgent(proxyUrl) |
| : new HttpsProxyAgent(proxyUrl)) |
| : undefined; |
|
|
| |
| let retries = 0; |
| const maxRetries = RETRY_CONFIG.MAX_RETRY_COUNT; |
| let lastError = null; |
|
|
| while (retries <= maxRetries) { |
| try { |
| if (retries > 0) { |
| logger.info(`第 ${retries} 次重试请求: ${method.toUpperCase()} ${fullUrl}`); |
| |
| await new Promise(resolve => setTimeout(resolve, RETRY_CONFIG.RETRY_DELAY)); |
| } |
|
|
| const response = await axios.request({ |
| method, |
| url: fullUrl, |
| params: requestParams, |
| headers: headers, |
| timeout: 45000, |
| validateStatus: () => true, |
| ..._.omit(options, "params", "headers"), |
| ...(proxyAgent ? { httpAgent: proxyAgent, httpsAgent: proxyAgent, proxy: false } : {}), |
| }); |
|
|
| |
| 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}`); |
|
|
| |
| |
| |
| const retryableErrorCodes = [ |
| 'ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', |
| 'ECONNREFUSED', 'EAI_AGAIN', 'EPIPE', 'ENETUNREACH', 'EHOSTUNREACH' |
| ]; |
| const isRetryableError = retryableErrorCodes.includes(error.code) || |
| error.message.includes('timeout') || |
| error.message.includes('network') || |
| error.message.includes('ECONNRESET') || |
| error.message.includes('socket hang up') || |
| error.message.includes('Proxy connection'); |
|
|
| if (isRetryableError && retries < maxRetries) { |
| retries++; |
| continue; |
| } |
|
|
| |
| break; |
| } |
| } |
|
|
| |
| if (lastError) { |
| logger.error(`请求失败,已重试 ${retries} 次: ${lastError.message}`); |
| if (lastError.response) { |
| logger.error(`响应状态: ${lastError.response.status}`); |
| logger.error(`响应数据: ${JSON.stringify(lastError.response.data)}`); |
| } |
| throw lastError; |
| } else { |
| |
| const error = new Error(`请求失败,已重试 ${retries} 次,但没有具体错误信息`); |
| logger.error(error.message); |
| throw error; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export async function checkImageContent( |
| imageUri: string, |
| refreshToken: string, |
| regionInfo: RegionInfo |
| ): Promise<void> { |
| |
| if (regionInfo.isInternational) return; |
|
|
| const babiParam = JSON.stringify({ |
| scenario: "image_video_generation", |
| feature_key: "aigc_to_image", |
| feature_entrance: "to-generate", |
| feature_entrance_detail: "to-generate-algo_proxy", |
| }); |
|
|
| logger.info(`开始图片内容安全检测: ${imageUri}`); |
|
|
| try { |
| await request("post", "/mweb/v1/algo_proxy", refreshToken, { |
| params: { |
| babi_param: babiParam, |
| }, |
| data: { |
| scene: "image_face_ip", |
| options: { ip_check: true }, |
| req_key: "benchmark_test_user_upload_image_input", |
| file_list: [{ file_uri: imageUri }], |
| req_params: {}, |
| }, |
| }); |
| logger.info(`图片内容安全检测通过: ${imageUri}`); |
| } catch (error: any) { |
| |
| const isContentViolation = error.message && ( |
| error.message.includes('2003') || |
| error.message.includes('risk not pass') || |
| error.message.includes('detected risk') |
| ); |
| if (isContentViolation) { |
| logger.error(`图片内容安全检测未通过: ${imageUri}, ${error.message}`); |
| throw new APIException( |
| EX.API_REQUEST_FAILED, |
| `图片内容检测未通过,该图片可能包含违规内容` |
| ); |
| } |
| |
| logger.warn(`图片内容安全检测服务异常(不阻塞): ${imageUri}, ${error.message}`); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| 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; |
|
|
| |
| JimengErrorHandler.handleApiResponse(result.data as JimengErrorResponse, { |
| context: '即梦API请求', |
| operation: '请求' |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| export function tokenSplit(authorization: string) { |
| return authorization.replace("Bearer ", "").split(","); |
| } |
|
|
| |
| |
| |
| export async function getTokenLiveStatus(refreshToken: string) { |
| try { |
| const result = await request( |
| "POST", |
| "/passport/account/info/v2", |
| refreshToken, |
| { |
| params: { |
| account_sdk_source: "web", |
| }, |
| } |
| ); |
| |
| return !!result?.user_id; |
| } catch (err) { |
| return false; |
| } |
| } |
|
|