2025-12-13 14:35:29 +08:00

635 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
// 默认的AgentID
const DEFAULT_ASSISTANT_ID = "513695";
// 版本号
const VERSION_CODE = "5.8.0";
// 平台代码
const PLATFORM_CODE = "7";
// 设备ID
const DEVICE_ID = Math.random() * 999999999999999999 + 7000000000000000000;
// WebID
const WEB_ID = Math.random() * 999999999999999999 + 7000000000000000000;
// 用户ID
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": "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;
/**
* 获取缓存中的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) {
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("; ");
}
/**
* 获取积分信息
*
* @param refreshToken 用于刷新access_token的refresh_token
*/
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: {
// Cookie: 'x-web-secsdk-uid=ef44bd0d-0cf6-448c-b517-fd1b5a7267ba; s_v_web_id=verify_m4b1lhlu_DI8qKRlD_7mJJ_4eqx_9shQ_s8eS2QLAbc4n; passport_csrf_token=86f3619c0c4a9c13f24117f71dc18524; passport_csrf_token_default=86f3619c0c4a9c13f24117f71dc18524; n_mh=9-mIeuD4wZnlYrrOvfzG3MuT6aQmCUtmr8FxV8Kl8xY; sid_guard=a7eb745aec44bb3186dbc2083ea9e1a6%7C1733386629%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT; uid_tt=59a46c7d3f34bda9588b93590cca2e12; uid_tt_ss=59a46c7d3f34bda9588b93590cca2e12; sid_tt=a7eb745aec44bb3186dbc2083ea9e1a6; sessionid=a7eb745aec44bb3186dbc2083ea9e1a6; sessionid_ss=a7eb745aec44bb3186dbc2083ea9e1a6; is_staff_user=false; sid_ucp_v1=1.0.0-KGRiOGY2ODQyNWU1OTk3NzRhYTE2ZmZhYmFjNjdmYjY3NzRmZGRiZTgKHgjToPCw0cwbEIXDxboGGJ-tHyAMMITDxboGOAhAJhoCaGwiIGE3ZWI3NDVhZWM0NGJiMzE4NmRiYzIwODNlYTllMWE2; ssid_ucp_v1=1.0.0-KGRiOGY2ODQyNWU1OTk3NzRhYTE2ZmZhYmFjNjdmYjY3NzRmZGRiZTgKHgjToPCw0cwbEIXDxboGGJ-tHyAMMITDxboGOAhAJhoCaGwiIGE3ZWI3NDVhZWM0NGJiMzE4NmRiYzIwODNlYTllMWE2; store-region=cn-gd; store-region-src=uid; user_spaces_idc={"7444764277623653426":"lf"}; ttwid=1|cxHJViEev1mfkjntdMziir8SwbU8uPNVSaeh9QpEUs8|1733966961|d8d52f5f56607427691be4ac44253f7870a34d25dd05a01b4d89b8a7c5ea82ad; _tea_web_id=7444838473275573797; fpk1=fa6c6a4d9ba074b90003896f36b6960066521c1faec6a60bdcb69ec8ddf85e8360b4c0704412848ec582b2abca73d57a; odin_tt=efe9dc150207879b88509e651a1c4af4e7ffb4cfcb522425a75bd72fbf894eda570bbf7ffb551c8b1de0aa2bfa0bd1be6c4157411ecdcf4464fcaf8dd6657d66',
Referer: "https://jimeng.jianying.com/ai-tool/home/",
// "Device-Time": 1733966964,
// Sign: "f3dbb824b378abea7c03cbb152b3a365"
}
});
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
}
}
/**
* 接收今日积分
*
* @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: "https://jimeng.jianying.com/ai-tool/image/generate"
}
});
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 = {},
host: string= "",
region: string= "",
) {
const token = await acquireToken(refreshToken);
const deviceTime = util.unixTimestamp();
const sign = util.md5(
`9e2c|${uri.slice(-7)}|${PLATFORM_CODE}|${VERSION_CODE}|${deviceTime}||11ac`
);
const response = await axios.request({
method,
url: `${host||'https://jimeng.jianying.com'}${uri}`,
params: {
aid: DEFAULT_ASSISTANT_ID,
device_platform: "web",
region: region||"CN",
web_id: WEB_ID,
...(options.params || {}),
},
headers: {
...FAKE_HEADERS,
Cookie: generateCookie(token),
"Device-Time": deviceTime,
Sign: sign,
"Sign-Ver": "1",
...(options.headers || {}),
},
timeout: 15000,
maxContentLength: 50 * 1000 * 1000,
validateStatus: () => true,
..._.omit(options, "params", "headers"),
});
// 流式响应直接返回response
if (options.responseType == "stream") return response;
return checkResult(response);
}
/**
* 预检查文件URL有效性
*
* @param fileUrl 文件URL
*/
export async function checkFileUrl(fileUrl: string) {
if (util.isBASE64Data(fileUrl)) return;
const result = await axios.head(fileUrl, {
timeout: 15000,
maxContentLength: Infinity,
maxBodyLength: Infinity,
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 fileUrl 文件URL
* @param refreshToken 用于刷新access_token的refresh_token
* @param isVideoImage 是否是用于视频图像
*/
export async function uploadFile(
fileUrl: string,
refreshToken: string,
isVideoImage: boolean = false
) {
// 预检查远程文件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");
}
// 下载文件到内存,如果您的服务器内存很小,建议考虑改造为流直传到下一个接口上,避免停留占用内存
else {
filename = path.basename(fileUrl);
({ data: fileData } = await axios.get(fileUrl, {
responseType: "arraybuffer",
// 100M限制
maxContentLength: FILE_MAX_SIZE,
// 60秒超时
timeout: 60000,
}));
}
// 获取文件的MIME类型
mimeType = mimeType || mime.getType(filename);
// 待开发
}
/**
* 检查请求结果
*
* @param result 结果
*/
export function checkResult(result: AxiosResponse) {
const { ret, errmsg, data } = result.data;
console.log("检查请求结果", { ret, errmsg } );
if (!_.isFinite(Number(ret))) return result.data;
if (ret !== '0') {
logger.error(`[Jimeng API Error] ret=${ret}, errmsg=${errmsg}, data=${JSON.stringify(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) {
return authorization.replace("Bearer ", "").split(",");
}
/**
* 获取Token存活状态
*/
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;
}
}
export function image3Options(
model,
componentId,
prompt,
sampleStrength,
height,
width,
negativePrompt = '',
resolutionType = "1k",
generateCount = 4
){
const min_version = "3.0.2"
const version = "3.0.2"
return {
params: {
babi_param: encodeURIComponent(
JSON.stringify({
scenario: "image_video_generation",
feature_key: "aigc_to_image",
feature_entrance: "to_image",
feature_entrance_detail: "to_image-" + model,
})
),
},
data: {
extend: {
root_model: model,
template_id: "",
},
submit_id: util.uuid(),
metrics_extra: JSON.stringify({
templateId: "",
generateCount: generateCount,
promptSource: "custom",
templateSource: "",
lastRequestId: "",
originRequestId: "",
}),
draft_content: JSON.stringify({
type: "draft",
id: util.uuid(),
min_version: min_version,
is_from_tsn: true,
version: version,
main_component_id: componentId,
component_list: [
{
type: "image_base_component",
id: componentId,
min_version: min_version,
generate_type: "generate",
aigc_mode: "workbench",
abilities: {
type: "",
id: util.uuid(),
generate: {
type: "",
id: util.uuid(),
core_param: {
type: "",
id: util.uuid(),
model,
prompt:prompt,
negative_prompt: negativePrompt,
seed: Math.floor(Math.random() * 100000000) + 2500000000,
sample_strength: sampleStrength,
image_ratio: 1,
large_image_info: {
type: "",
id: util.uuid(),
height,
width,
resolution_type: resolutionType,
},
},
history_option: {
type: "",
id: util.uuid(),
},
},
},
},
],
}),
http_common_info: {
aid: Number(DEFAULT_ASSISTANT_ID),
},
},
}
}
export function image4Options_0302(
model,
componentId,
prompt,
sampleStrength,
height,
width,
negativePrompt = '',
resolutionType = "2k",
generateCount = 4
){
const min_version = "3.0.2"
const version = "3.0.2"
const submit_id = util.uuid()
return {
params: {
babi_param: encodeURIComponent(
JSON.stringify({
scenario: "image_video_generation",
feature_key: "aigc_to_image",
feature_entrance: "to_image",
feature_entrance_detail: "to_image-" + model,
})
),
},
data: {
extend: {
root_model: model,
},
submit_id: submit_id,
metrics_extra: JSON.stringify({
promptSource: "custom",
generateCount: generateCount,
enterFrom: "click",
sceneOptions:JSON.stringify([
{
"type": "image",
"scene": "ImageBasicGenerate",
"modelReqKey": model,
"resolutionType": resolutionType,
"abilityList": [],
"benefitCount": 4,
"reportParams": {
"enterSource": "generate",
"vipSource": "generate",
"extraVipFunctionKey": model+"-"+resolutionType,
"useVipFunctionDetailsReporterHoc": true
}
}
]),
isBoxSelect: false,
isCutout: false,
generateId: submit_id,
isRegenerate: false
}),
draft_content: JSON.stringify({
type: "draft",
id: util.uuid(),
min_version: min_version,
is_from_tsn: true,
version: version,
main_component_id: componentId,
component_list: [
{
type: "image_base_component",
id: componentId,
min_version: min_version,
generate_type: "generate",
aigc_mode: "workbench",
metadata: {
"type": "",
"id": util.uuid(),
"created_platform": 3,
"created_platform_version": "",
"created_time_in_ms": Math.floor(Date.now()/1000),
"created_did": ""
},
abilities: {
type: "",
id: util.uuid(),
generate: {
type: "",
id: util.uuid(),
core_param: {
type: "",
id: util.uuid(),
model,
prompt,
negative_prompt: negativePrompt,
seed: Math.floor(Math.random() * 100000000) + 2500000000,
sample_strength: sampleStrength,
image_ratio: 5,
large_image_info: {
type: "",
id: util.uuid(),
height,
width,
resolution_type: resolutionType,
},
intelligent_ratio: false
},
},
gen_option: {
"type": "",
"id": util.uuid(),
"gen_count": generateCount,
"generate_all": false
}
},
},
],
}),
http_common_info: {
aid: Number(DEFAULT_ASSISTANT_ID),
},
},
}
}
export function image4Options_0337(
model,
componentId,
prompt,
sampleStrength,
height,
width,
negativePrompt = '',
resolutionType = "2k",
generateCount = 4
){
const min_version = "3.0.2"
const version = "3.3.7"
const submit_id = util.uuid()
return {
params: {
da_version: version,
web_component_open_flag:1,
web_version:'7.5.0',
aigc_features:'app_lip_sync',
babi_param: encodeURIComponent(
JSON.stringify({
scenario: "image_video_generation",
feature_key: "aigc_to_image",
feature_entrance: "to_image",
feature_entrance_detail: "to_image-" + model,
})
),
},
data: {
extend: {
root_model: model,
},
submit_id: submit_id,
metrics_extra: JSON.stringify({
promptSource: "custom",
generateCount: generateCount,
enterFrom: "click",
sceneOptions:JSON.stringify([
{
"type": "image",
"scene": "ImageBasicGenerate",
"modelReqKey": model,
"resolutionType": resolutionType,
"abilityList": [],
"benefitCount": 4,
"reportParams": {
"enterSource": "generate",
"vipSource": "generate",
"extraVipFunctionKey": model+"-"+resolutionType,
"useVipFunctionDetailsReporterHoc": true
}
}
]),
isBoxSelect: false,
isCutout: false,
generateId: submit_id,
isRegenerate: false
}),
draft_content: JSON.stringify({
type: "draft",
id: util.uuid(),
min_version: min_version,
is_from_tsn: true,
version: version,
main_component_id: componentId,
component_list: [
{
type: "image_base_component",
id: componentId,
min_version: min_version,
generate_type: "generate",
aigc_mode: "workbench",
metadata: {
"type": "",
"id": util.uuid(),
"created_platform": 3,
"created_platform_version": "",
"created_time_in_ms": Math.floor(Date.now()/1000),
"created_did": ""
},
abilities: {
type: "",
id: util.uuid(),
generate: {
type: "",
id: util.uuid(),
core_param: {
type: "",
id: util.uuid(),
model,
prompt,
negative_prompt: negativePrompt,
seed: Math.floor(Math.random() * 100000000) + 2500000000,
sample_strength: sampleStrength,
image_ratio: 5,
large_image_info: {
type: "",
id: util.uuid(),
height,
width,
resolution_type: resolutionType,
},
intelligent_ratio: false
},
},
gen_option: {
"type": "",
"id": util.uuid(),
"gen_count": generateCount,
"generate_all": false
}
},
},
],
}),
http_common_info: {
aid: Number(DEFAULT_ASSISTANT_ID),
},
},
}
}