632 lines
19 KiB
TypeScript
632 lines
19 KiB
TypeScript
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') 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),
|
||
},
|
||
},
|
||
}
|
||
} |