github-actions[bot]
Deploy from GitHub Actions
8e02bdb
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<string, string> = {
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<string, string> = {};
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<string> {
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" && /<html|<!doctype html/i.test(lastResult)) {
throw new Error("积分接口返回 HTML 页面");
}
const benefitMap = lastResult?.benefit_map || lastResult?.benefits || lastResult?.data?.benefits || null;
const strategy = lastResult?.strategy || lastResult?.data?.strategy || null;
const responseData = typeof lastResult?.response === "string"
? JSON.parse(lastResult.response)
: lastResult?.response;
logger.info(`国际版积分返回: ${JSON.stringify(lastResult).slice(0, 800)}`);
const credit = lastResult?.data?.credit || responseData?.credit || lastResult?.credit || lastResult?.points || strategy || benefitMap || lastResult;
const gift_credit = credit?.gift_credit ?? credit?.giftCredit ?? credit?.gift ?? credit?.gift_balance ?? credit?.free_credit ?? 0;
const purchase_credit = credit?.purchase_credit ?? credit?.purchaseCredit ?? credit?.purchase ?? credit?.purchase_balance ?? credit?.paid_credit ?? 0;
const vip_credit = credit?.vip_credit ?? credit?.vipCredit ?? credit?.vip ?? credit?.vip_balance ?? 0;
if (gift_credit === 0 && purchase_credit === 0 && vip_credit === 0 && !benefitMap && !strategy && !credit) {
throw new Error(`未识别的积分响应结构: ${JSON.stringify(lastResult).slice(0, 300)}`);
}
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,
};
} catch (err) {
logger.warn(`积分接口尝试失败: ${candidate.method} ${candidate.path} -> ${(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;
}