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"; import provider from "@/lib/upstream-provider.ts"; import { findDreaminaCookieHeaderBySessionid, getDreaminaApiKey, getDreaminaSessionPool } from "@/lib/intl-account-pool.ts"; // 模型名称 const MODEL_NAME = provider.modelName; // 默认的AgentID export const DEFAULT_ASSISTANT_ID = 513695; // 版本号 const VERSION_CODE = "8.4.0"; // 平台代码 const PLATFORM_CODE = "7"; // 设备ID const DEVICE_ID = Math.random() * 999999999999999999 + 7000000000000000000; // WebID export const WEB_ID = Math.random() * 999999999999999999 + 7000000000000000000; // 用户ID export 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": provider.acceptLanguage, "App-Sdk-Version": "48.0.0", "Cache-control": "no-cache", Appid: provider.assistantId, Appvr: VERSION_CODE, Lan: provider.lan, Loc: provider.loc, Origin: provider.origin, Pragma: "no-cache", Priority: "u=1, i", Referer: provider.referer, Pf: PLATFORM_CODE, "Sec-Ch-Ua": '"Google Chrome";v="132", "Chromium";v="132", "Not_A Brand";v="8"', "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/132.0.0.0 Safari/537.36", }; // 文件最大大小 const FILE_MAX_SIZE = 100 * 1024 * 1024; function resolveBaseUrlByPath(uri: string) { if (uri.startsWith("/commerce/")) return provider.commerceApiBaseUrl; if (uri.startsWith("/mweb/")) return provider.mwebApiBaseUrl; if (uri.startsWith("/lv/") || uri.startsWith("/user/")) return provider.webApiBaseUrl; if (uri.startsWith("/passport/")) return provider.pageBaseUrl; return provider.baseUrl; } function shouldUseGlobalQueryParams(uri: string) { if (uri.startsWith("/passport/")) return false; if (uri.startsWith("/commerce/")) return false; return true; } function isPassportRequest(uri: string) { return uri.startsWith("/passport/") || uri.startsWith("/user/"); } function isDreaminaPageApiRequest(uri: string) { return provider.name === "dreamina-intl" && uri.startsWith("/mweb/"); } function cookieNameFromKey(name: string) { const mapped: Record = { store_idc: "store-idc", cc_target_idc: "cc-target-idc", tt_target_idc_sign: "tt-target-idc-sign", store_country_sign: "store-country-sign", }; return mapped[name] || name; } function resolveAccountCookies(refreshToken: string) { const cookieHeader = provider.name === "dreamina-intl" ? findDreaminaCookieHeaderBySessionid(refreshToken)?.header : null; if (!cookieHeader) return null; const result: Record = {}; for (const chunk of cookieHeader.split(";")) { const trimmed = chunk.trim(); if (!trimmed) continue; const idx = trimmed.indexOf("="); if (idx === -1) continue; const key = trimmed.slice(0, idx).trim(); const value = trimmed.slice(idx + 1).trim(); result[key.replace(/-/g, "_")] = value; } return result; } /** * 获取缓存中的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) { const account = resolveAccountCookies(refreshToken); if (provider.name === "dreamina-intl" && account) { return Object.entries(account) .map(([name, value]) => `${cookieNameFromKey(name)}=${value}`) .join("; "); } const extraCookies = account || provider.extraCookies || {}; const teaWebId = extraCookies?.tea_web_id || String(WEB_ID); const uidTt = extraCookies?.uid_tt || USER_ID; const uidTtSs = extraCookies?.uid_tt_ss || USER_ID; const sidGuard = extraCookies?.sid_guard || `${refreshToken}%7C${util.unixTimestamp()}%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT`; const cookies = [ `_tea_web_id=${teaWebId}`, `is_staff_user=false`, `${provider.storeRegionKey}=${provider.storeRegionValue}`, `${provider.storeRegionSrcKey}=${provider.storeRegionSrcValue}`, `sid_guard=${sidGuard}`, `uid_tt=${uidTt}`, `uid_tt_ss=${uidTtSs}`, `sid_tt=${refreshToken}`, `sessionid=${refreshToken}`, `sessionid_ss=${refreshToken}`, `sid_tt=${refreshToken}` ]; for (const [name, value] of Object.entries(extraCookies || {})) { if (value) cookies.push(`${cookieNameFromKey(name)}=${value}`); } return cookies.join("; "); } /** * 获取浏览器格式的cookie数组(用于Playwright context.addCookies) */ export function getCookiesForBrowser(refreshToken: string) { const domain = provider.cookieDomain; const account = resolveAccountCookies(refreshToken); if (provider.name === "dreamina-intl" && account) { return Object.entries(account).map(([name, value]) => ({ name: cookieNameFromKey(name), value, domain, path: "/", })); } const extraCookies = account || provider.extraCookies || {}; const teaWebId = extraCookies?.tea_web_id || String(WEB_ID); const uidTt = extraCookies?.uid_tt || USER_ID; const uidTtSs = extraCookies?.uid_tt_ss || USER_ID; const sidGuard = extraCookies?.sid_guard || `${refreshToken}%7C${util.unixTimestamp()}%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT`; const cookies = [ { name: "_tea_web_id", value: teaWebId, domain, path: "/" }, { name: "is_staff_user", value: "false", domain, path: "/" }, { name: provider.storeRegionKey, value: provider.storeRegionValue, domain, path: "/" }, { name: provider.storeRegionSrcKey, value: provider.storeRegionSrcValue, domain, path: "/" }, { name: "sid_guard", value: sidGuard, domain, path: "/" }, { name: "uid_tt", value: uidTt, domain, path: "/" }, { name: "uid_tt_ss", value: uidTtSs, domain, path: "/" }, { name: "sid_tt", value: refreshToken, domain, path: "/" }, { name: "sessionid", value: refreshToken, domain, path: "/" }, { name: "sessionid_ss", value: refreshToken, domain, path: "/" }, ]; for (const [name, value] of Object.entries(extraCookies || {})) { if (value) cookies.push({ name: cookieNameFromKey(name), value, domain, path: "/" }); } return cookies; } /** * 获取积分信息 * * @param refreshToken 用于刷新access_token的refresh_token */ export async function getCredit(refreshToken: string) { const candidateRequests = [ { path: provider.creditPath || "/commerce/v1/benefits/user_credit", method: provider.creditMethod || "POST", params: {} }, ...(provider.creditFallbackPaths || []), ]; let lastResult: any = null; for (const candidate of candidateRequests) { try { lastResult = await request(candidate.method, candidate.path, refreshToken, { params: candidate.params || {}, data: candidate.method === "POST" ? {} : undefined, headers: { Referer: provider.pageBaseUrl + "/", } }); if (typeof lastResult === "string" && / ${(err as Error).message}`); } } throw new APIException(EX.API_REQUEST_FAILED, `获取积分失败: ${JSON.stringify(lastResult)}`); } /** * 接收今日积分 * * @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: provider.imageReferer } }); 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 = `${resolveBaseUrlByPath(uri)}${uri}`; const requestParams = shouldUseGlobalQueryParams(uri) ? { aid: provider.assistantId, device_platform: "web", region: provider.region, webId: provider.extraCookies?.tea_web_id || WEB_ID, da_version: "3.3.12", web_component_open_flag: 1, web_version: "7.5.0", aigc_features: "app_lip_sync", os: "windows", commerce_with_input_video: 1, msToken: provider.extraCookies?.msToken, ...(options.params || {}), } : { ...(options.params || {}), }; const headers = isPassportRequest(uri) ? { Accept: "application/json, text/plain, */*", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-language": provider.acceptLanguage, Origin: provider.origin, Referer: provider.generateImageReferer, "User-Agent": FAKE_HEADERS["User-Agent"], Cookie: generateCookie(token), "x-tt-passport-csrf-token": provider.extraCookies?.passport_csrf_token || "", ...(options.headers || {}), } : isDreaminaPageApiRequest(uri) ? { Accept: "application/json, text/plain, */*", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-language": provider.acceptLanguage, Origin: provider.origin, Referer: provider.generateImageReferer, "User-Agent": FAKE_HEADERS["User-Agent"], Cookie: generateCookie(token), "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", ...(options.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) { const token = authorization.replace("Bearer ", "").trim(); const apiKey = getDreaminaApiKey(); if (provider.name === "dreamina-intl" && apiKey && token === apiKey) { const pool = getDreaminaSessionPool(); if (pool.length === 0) throw new APIException(EX.API_REQUEST_FAILED, "未配置国际版账号池 DREAMINA_COOKIE_POOL 或 DREAMINA_SESSIONIDS"); return pool; } return token.split(","); } /** * 获取Token存活状态 */ export async function getTokenLiveStatus(refreshToken: string) { const candidateRequests = [ { path: provider.userInfoPath, method: provider.userInfoMethod, params: provider.userInfoParams || {} }, ...(provider.userInfoFallbackPaths || []), ]; for (const candidate of candidateRequests) { try { const result = await request(candidate.method, candidate.path, refreshToken, { params: provider.name === "dreamina-intl" ? { aid: provider.assistantId, account_sdk_source: "web", sdk_version: "2.1.10-tiktok", language: provider.lan, verifyFp: provider.extraCookies?.s_v_web_id || undefined, ...(candidate.params || {}), } : (candidate.params || {}), }); const user = result?.data || result?.userInfo || result?.user_info || result; if (user?.user_id || user?.uid || user?.id || user?.user_id_str || user?.sec_user_id) { return true; } } catch (err) { logger.warn(`用户信息接口尝试失败: ${candidate.method} ${candidate.path} -> ${(err as Error).message}`); } } return false; }