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 { 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), }, }, } }