xt8 commited on
Commit
942ff10
·
verified ·
1 Parent(s): 9867f1b

Upload 48 files

Browse files
docker-compose.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ jimeng-api:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ image: jimeng
7
+ container_name: jimeng-api
8
+ restart: unless-stopped
9
+ ports:
10
+ - "5100:5100"
11
+ #volumes:
12
+ # # 挂载日志目录(确保权限正确)
13
+ # - ./logs:/app/logs
14
+ # # 挂载临时目录
15
+ # - ./tmp:/app/tmp
16
+ healthcheck:
17
+ test: ["CMD", "wget", "-q", "--spider", "http://localhost:5100/ping"]
18
+ interval: 15s
19
+ timeout: 5s
20
+ retries: 3
21
+ start_period: 20s
22
+ # 日志配置
23
+ logging:
24
+ driver: "json-file"
25
+ options:
26
+ max-size: "10m"
27
+ max-file: "3"
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
src/api/consts/common.ts CHANGED
@@ -6,14 +6,22 @@
6
  export const BASE_URL_CN = "https://jimeng.jianying.com";
7
 
8
  export const BASE_URL_US_COMMERCE = "https://commerce.us.capcut.com";
 
 
9
 
10
  // 默认助手ID
11
  export const DEFAULT_ASSISTANT_ID_CN = 513695;
12
  export const DEFAULT_ASSISTANT_ID_US = 513641;
 
 
 
13
 
14
  // 地区
15
  export const REGION_CN = "cn";
16
  export const REGION_US = "US";
 
 
 
17
 
18
  // 平台代码
19
  export const PLATFORM_CODE = "7";
@@ -51,6 +59,7 @@ export const IMAGE_MODEL_MAP_US = {
51
  export const VIDEO_MODEL_MAP = {
52
  "jimeng-video-3.0-pro": "dreamina_ic_generate_video_model_vgfm_3.0_pro",
53
  "jimeng-video-3.0": "dreamina_ic_generate_video_model_vgfm_3.0",
 
54
  "jimeng-video-2.0": "dreamina_ic_generate_video_model_vgfm_lite",
55
  "jimeng-video-2.0-pro": "dreamina_ic_generate_video_model_vgfm1.0"
56
  };
 
6
  export const BASE_URL_CN = "https://jimeng.jianying.com";
7
 
8
  export const BASE_URL_US_COMMERCE = "https://commerce.us.capcut.com";
9
+ export const BASE_URL_HK_COMMERCE = "https://commerce-api-sg.capcut.com";
10
+ export const BASE_URL_HK = "https://mweb-api-sg.capcut.com";
11
 
12
  // 默认助手ID
13
  export const DEFAULT_ASSISTANT_ID_CN = 513695;
14
  export const DEFAULT_ASSISTANT_ID_US = 513641;
15
+ export const DEFAULT_ASSISTANT_ID_HK = 513641;
16
+ export const DEFAULT_ASSISTANT_ID_JP = 513641;
17
+ export const DEFAULT_ASSISTANT_ID_SG = 513641;
18
 
19
  // 地区
20
  export const REGION_CN = "cn";
21
  export const REGION_US = "US";
22
+ export const REGION_HK = "HK";
23
+ export const REGION_JP = "JP";
24
+ export const REGION_SG = "SG";
25
 
26
  // 平台代码
27
  export const PLATFORM_CODE = "7";
 
59
  export const VIDEO_MODEL_MAP = {
60
  "jimeng-video-3.0-pro": "dreamina_ic_generate_video_model_vgfm_3.0_pro",
61
  "jimeng-video-3.0": "dreamina_ic_generate_video_model_vgfm_3.0",
62
+ "jimeng-video-3.0-fast": "dreamina_ic_generate_video_model_vgfm_3.0_fast",
63
  "jimeng-video-2.0": "dreamina_ic_generate_video_model_vgfm_lite",
64
  "jimeng-video-2.0-pro": "dreamina_ic_generate_video_model_vgfm1.0"
65
  };
src/api/consts/dreamina.ts CHANGED
@@ -3,6 +3,9 @@
3
  export const BASE_URL_DREAMINA_US = "https://dreamina-api.us.capcut.com";
4
  export const BASE_URL_IMAGEX_US = "https://imagex16-normal-us-ttp.capcutapi.us";
5
 
 
 
 
6
 
7
  export const WEB_VERSION = "7.5.0";
8
  export const DA_VERSION = "3.3.2";
 
3
  export const BASE_URL_DREAMINA_US = "https://dreamina-api.us.capcut.com";
4
  export const BASE_URL_IMAGEX_US = "https://imagex16-normal-us-ttp.capcutapi.us";
5
 
6
+ export const BASE_URL_DREAMINA_HK = "https://mweb-api-sg.capcut.com";
7
+ export const BASE_URL_IMAGEX_HK = "https://imagex16-normal-sg-ttp.capcutapi.sg";
8
+
9
 
10
  export const WEB_VERSION = "7.5.0";
11
  export const DA_VERSION = "3.3.2";
src/api/controllers/core.ts CHANGED
@@ -10,15 +10,23 @@ import { createParser } from "eventsource-parser";
10
  import logger from "@/lib/logger.ts";
11
  import util from "@/lib/util.ts";
12
  import { JimengErrorHandler, JimengErrorResponse } from "@/lib/error-handler.ts";
13
- import { BASE_URL_DREAMINA_US } from "@/api/consts/dreamina.ts";
14
- import {
15
  BASE_URL_CN,
16
  BASE_URL_US_COMMERCE,
 
 
17
  DEFAULT_ASSISTANT_ID_CN,
18
  DEFAULT_ASSISTANT_ID_US,
 
 
 
19
  PLATFORM_CODE,
20
  REGION_CN,
21
  REGION_US,
 
 
 
22
  VERSION_CODE,
23
  RETRY_CONFIG
24
  } from "@/api/consts/common.ts";
@@ -70,11 +78,21 @@ export async function acquireToken(refreshToken: string): Promise<string> {
70
  */
71
  export function generateCookie(refreshToken: string) {
72
  const isUS = refreshToken.toLowerCase().startsWith('us-');
73
- const token = isUS ? refreshToken.substring(3) : refreshToken;
 
 
 
 
 
 
 
 
 
 
74
  return [
75
  `_tea_web_id=${WEB_ID}`,
76
  `is_staff_user=false`,
77
- `store-region=${isUS ? 'us' : 'cn-gd'}`,
78
  `store-region-src=uid`,
79
  `sid_guard=${token}%7C${util.unixTimestamp()}%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT`,
80
  `uid_tt=${USER_ID}`,
@@ -137,8 +155,6 @@ export async function receiveCredit(refreshToken: string) {
137
  * @param params 请求参数
138
  * @param headers 请求头
139
  */
140
- import { BASE_URL_DREAMINA_US } from "@/api/consts/dreamina.ts";
141
-
142
  export async function request(
143
  method: string,
144
  uri: string,
@@ -146,14 +162,17 @@ export async function request(
146
  options: AxiosRequestConfig & { noDefaultParams?: boolean } = {}
147
  ) {
148
  const isUS = refreshToken.toLowerCase().startsWith('us-');
149
- const token = await acquireToken(isUS ? refreshToken.substring(3) : refreshToken);
 
 
 
150
  const deviceTime = util.unixTimestamp();
151
  const sign = util.md5(
152
  `9e2c|${uri.slice(-7)}|${PLATFORM_CODE}|${VERSION_CODE}|${deviceTime}||11ac`
153
  );
154
 
155
  let baseUrl: string;
156
- let aid: string;
157
  let region: string;
158
 
159
  if (isUS) {
@@ -164,7 +183,25 @@ export async function request(
164
  }
165
  aid = DEFAULT_ASSISTANT_ID_US;
166
  region = REGION_US;
167
- } else { // 'jimeng' (CN)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  baseUrl = BASE_URL_CN;
169
  aid = DEFAULT_ASSISTANT_ID_CN;
170
  region = REGION_CN;
@@ -177,7 +214,7 @@ export async function request(
177
  aid: aid,
178
  device_platform: "web",
179
  region: region,
180
- ...(isUS ? {} : { webId: WEB_ID }),
181
  da_version: "3.3.2",
182
  web_component_open_flag: 1,
183
  web_version: "7.5.0",
 
10
  import logger from "@/lib/logger.ts";
11
  import util from "@/lib/util.ts";
12
  import { JimengErrorHandler, JimengErrorResponse } from "@/lib/error-handler.ts";
13
+ import { BASE_URL_DREAMINA_US, BASE_URL_DREAMINA_HK } from "@/api/consts/dreamina.ts";
14
+ import {
15
  BASE_URL_CN,
16
  BASE_URL_US_COMMERCE,
17
+ BASE_URL_HK_COMMERCE,
18
+ BASE_URL_HK,
19
  DEFAULT_ASSISTANT_ID_CN,
20
  DEFAULT_ASSISTANT_ID_US,
21
+ DEFAULT_ASSISTANT_ID_HK,
22
+ DEFAULT_ASSISTANT_ID_JP,
23
+ DEFAULT_ASSISTANT_ID_SG,
24
  PLATFORM_CODE,
25
  REGION_CN,
26
  REGION_US,
27
+ REGION_HK,
28
+ REGION_JP,
29
+ REGION_SG,
30
  VERSION_CODE,
31
  RETRY_CONFIG
32
  } from "@/api/consts/common.ts";
 
78
  */
79
  export function generateCookie(refreshToken: string) {
80
  const isUS = refreshToken.toLowerCase().startsWith('us-');
81
+ const isHK = refreshToken.toLowerCase().startsWith('hk-');
82
+ const isJP = refreshToken.toLowerCase().startsWith('jp-');
83
+ const isSG = refreshToken.toLowerCase().startsWith('sg-');
84
+ const token = (isUS || isHK || isJP || isSG) ? refreshToken.substring(3) : refreshToken;
85
+
86
+ let storeRegion = 'cn-gd';
87
+ if (isUS) storeRegion = 'us';
88
+ else if (isHK) storeRegion = 'hk';
89
+ else if (isJP) storeRegion = 'hk'; // JP uses HK store region
90
+ else if (isSG) storeRegion = 'hk'; // SG uses HK store region
91
+
92
  return [
93
  `_tea_web_id=${WEB_ID}`,
94
  `is_staff_user=false`,
95
+ `store-region=${storeRegion}`,
96
  `store-region-src=uid`,
97
  `sid_guard=${token}%7C${util.unixTimestamp()}%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT`,
98
  `uid_tt=${USER_ID}`,
 
155
  * @param params 请求参数
156
  * @param headers 请求头
157
  */
 
 
158
  export async function request(
159
  method: string,
160
  uri: string,
 
162
  options: AxiosRequestConfig & { noDefaultParams?: boolean } = {}
163
  ) {
164
  const isUS = refreshToken.toLowerCase().startsWith('us-');
165
+ const isHK = refreshToken.toLowerCase().startsWith('hk-');
166
+ const isJP = refreshToken.toLowerCase().startsWith('jp-');
167
+ const isSG = refreshToken.toLowerCase().startsWith('sg-');
168
+ const token = await acquireToken((isUS || isHK || isJP || isSG) ? refreshToken.substring(3) : refreshToken);
169
  const deviceTime = util.unixTimestamp();
170
  const sign = util.md5(
171
  `9e2c|${uri.slice(-7)}|${PLATFORM_CODE}|${VERSION_CODE}|${deviceTime}||11ac`
172
  );
173
 
174
  let baseUrl: string;
175
+ let aid: number;
176
  let region: string;
177
 
178
  if (isUS) {
 
183
  }
184
  aid = DEFAULT_ASSISTANT_ID_US;
185
  region = REGION_US;
186
+ } else if (isHK || isJP || isSG) {
187
+ // HK, JP and SG regions use the same SG base URL
188
+ if (uri.startsWith("/commerce/")) {
189
+ baseUrl = BASE_URL_HK_COMMERCE;
190
+ } else {
191
+ baseUrl = BASE_URL_DREAMINA_HK;
192
+ }
193
+ if (isJP) {
194
+ aid = DEFAULT_ASSISTANT_ID_JP;
195
+ region = REGION_JP;
196
+ } else if (isSG) {
197
+ aid = DEFAULT_ASSISTANT_ID_SG;
198
+ region = REGION_SG;
199
+ } else {
200
+ aid = DEFAULT_ASSISTANT_ID_HK;
201
+ region = REGION_HK;
202
+ }
203
+ } else {
204
+ // CN region
205
  baseUrl = BASE_URL_CN;
206
  aid = DEFAULT_ASSISTANT_ID_CN;
207
  region = REGION_CN;
 
214
  aid: aid,
215
  device_platform: "web",
216
  region: region,
217
+ ...(isUS || isHK || isJP || isSG ? {} : { webId: WEB_ID }),
218
  da_version: "3.3.2",
219
  web_component_open_flag: 1,
220
  web_version: "7.5.0",
src/api/controllers/images.ts CHANGED
@@ -7,8 +7,8 @@ import util from "@/lib/util.ts";
7
  import { getCredit, receiveCredit, request } from "./core.ts";
8
  import logger from "@/lib/logger.ts";
9
  import { SmartPoller, PollingStatus } from "@/lib/smart-poller.ts";
10
- import { DEFAULT_ASSISTANT_ID_CN, DEFAULT_ASSISTANT_ID_US, DEFAULT_IMAGE_MODEL, DRAFT_VERSION, DRAFT_MIN_VERSION, IMAGE_MODEL_MAP, IMAGE_MODEL_MAP_US, RESOLUTION_OPTIONS } from "@/api/consts/common.ts";
11
- import { BASE_URL_DREAMINA_US, BASE_URL_IMAGEX_US, WEB_VERSION as DREAMINA_WEB_VERSION, DA_VERSION as DREAMINA_DA_VERSION, AIGC_FEATURES as DREAMINA_AIGC_FEATURES } from "@/api/consts/dreamina.ts";
12
  import { createSignature } from "@/lib/aws-signature.ts";
13
 
14
  export const DEFAULT_MODEL = DEFAULT_IMAGE_MODEL;
@@ -33,9 +33,9 @@ function getResolutionParams(resolution: string = '2k', ratio: string = '1:1'):
33
  resolution_type: resolution,
34
  };
35
  }
36
- export function getModel(model: string, isUS: boolean) {
37
- const modelMap = isUS ? IMAGE_MODEL_MAP_US : IMAGE_MODEL_MAP;
38
- if (isUS && !modelMap[model]) {
39
  const supportedModels = Object.keys(modelMap).join(', ');
40
  throw new Error(`国际版不支持模型 "${model}"。支持的模型: ${supportedModels}`);
41
  }
@@ -60,9 +60,9 @@ function calculateCRC32(buffer: ArrayBuffer): string {
60
  return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
61
  }
62
 
63
- async function uploadImageFromUrl(imageUrl: string, refreshToken: string, isUS: boolean): Promise<string> {
64
  try {
65
- logger.info(`开始上传图片: ${imageUrl} (isUS: ${isUS})`);
66
 
67
  const imageResponse = await fetch(imageUrl);
68
  if (!imageResponse.ok) {
@@ -70,7 +70,7 @@ async function uploadImageFromUrl(imageUrl: string, refreshToken: string, isUS:
70
  }
71
  const imageBuffer = await imageResponse.arrayBuffer();
72
 
73
- return await uploadImageFromBuffer(Buffer.from(imageBuffer), refreshToken, isUS);
74
 
75
  } catch (error) {
76
  logger.error(`图片上传失败: ${error.message}`);
@@ -78,16 +78,21 @@ async function uploadImageFromUrl(imageUrl: string, refreshToken: string, isUS:
78
  }
79
  }
80
 
81
- async function uploadImageFromBuffer(imageBuffer: Buffer, refreshToken: string, isUS: boolean): Promise<string> {
82
  try {
83
- logger.info(`开始通过Buffer上传图片... (isUS: ${isUS})`);
 
 
 
 
 
84
 
85
  const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
86
  data: {
87
  scene: 2,
88
  },
89
- params: isUS ? {
90
- aid: DEFAULT_ASSISTANT_ID_US,
91
  web_version: DREAMINA_WEB_VERSION,
92
  da_version: DREAMINA_DA_VERSION,
93
  aigc_features: DREAMINA_AIGC_FEATURES,
@@ -96,13 +101,13 @@ async function uploadImageFromBuffer(imageBuffer: Buffer, refreshToken: string,
96
  },
97
  });
98
  const { access_key_id, secret_access_key, session_token } = tokenResult;
99
- const service_id = isUS ? tokenResult.space_name : tokenResult.service_id;
100
 
101
  if (!access_key_id || !secret_access_key || !session_token) {
102
  throw new Error("获取上传令牌失败");
103
  }
104
 
105
- const actualServiceId = service_id || (isUS ? "wopfjsm1ax" : "tb4s082cfz");
106
 
107
  logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
108
 
@@ -114,9 +119,9 @@ async function uploadImageFromBuffer(imageBuffer: Buffer, refreshToken: string,
114
  const now = new Date();
115
  const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
116
  const randomStr = Math.random().toString(36).substring(2, 12);
117
-
118
- const applyUrlHost = isUS ? BASE_URL_IMAGEX_US : 'https://imagex.bytedanceapi.com';
119
- const applyUrl = `${applyUrlHost}/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}${isUS ? '&device_platform=web' : ''}`;
120
 
121
  const requestHeaders = {
122
  'x-amz-date': timestamp,
@@ -125,7 +130,7 @@ async function uploadImageFromBuffer(imageBuffer: Buffer, refreshToken: string,
125
 
126
  const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token);
127
 
128
- const origin = isUS ? new URL(BASE_URL_DREAMINA_US).origin : 'https://jimeng.jianying.com';
129
 
130
  const applyResponse = await fetch(applyUrl, {
131
  method: 'GET',
@@ -247,7 +252,11 @@ export async function generateImageComposition(
247
  refreshToken: string
248
  ) {
249
  const isUS = refreshToken.toLowerCase().startsWith('us-');
250
- const model = getModel(_model, isUS);
 
 
 
 
251
 
252
  let width, height, image_ratio, resolution_type;
253
 
@@ -283,10 +292,10 @@ export async function generateImageComposition(
283
  let imageId: string;
284
  if (typeof image === 'string') {
285
  logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (URL)...`);
286
- imageId = await uploadImageFromUrl(image, refreshToken, isUS);
287
  } else {
288
  logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (Buffer)...`);
289
- imageId = await uploadImageFromBuffer(image, refreshToken, isUS);
290
  }
291
  uploadedImageIds.push(imageId);
292
  logger.info(`图片 ${i + 1}/${imageCount} 上传成功: ${imageId}`);
@@ -403,7 +412,7 @@ export async function generateImageComposition(
403
  ],
404
  }),
405
  http_common_info: {
406
- aid: isUS ? DEFAULT_ASSISTANT_ID_US : DEFAULT_ASSISTANT_ID_CN
407
  }
408
  },
409
  }
@@ -521,7 +530,11 @@ export async function generateImages(
521
  refreshToken: string
522
  ) {
523
  const isUS = refreshToken.toLowerCase().startsWith('us-');
524
- const model = getModel(_model, isUS);
 
 
 
 
525
  logger.info(`使用模型: ${_model} 映射模型: ${model} 分辨率: ${resolution} 比例: ${ratio} 精细度: ${sampleStrength}`);
526
 
527
  return await generateImagesInternal(_model, prompt, { ratio, resolution, sampleStrength, negativePrompt }, refreshToken);
@@ -544,7 +557,11 @@ async function generateImagesInternal(
544
  refreshToken: string
545
  ) {
546
  const isUS = refreshToken.toLowerCase().startsWith('us-');
547
- const model = getModel(_model, isUS);
 
 
 
 
548
 
549
  let width, height, image_ratio, resolution_type;
550
 
@@ -655,7 +672,7 @@ async function generateImagesInternal(
655
  ],
656
  }),
657
  http_common_info: {
658
- aid: isUS ? DEFAULT_ASSISTANT_ID_US : DEFAULT_ASSISTANT_ID_CN
659
  }
660
  },
661
  }
@@ -775,7 +792,11 @@ async function generateJimeng40MultiImages(
775
  refreshToken: string
776
  ) {
777
  const isUS = refreshToken.toLowerCase().startsWith('us-');
778
- const model = getModel(_model, isUS);
 
 
 
 
779
  const { width, height, image_ratio, resolution_type } = getResolutionParams(resolution, ratio);
780
 
781
  const targetImageCount = prompt.match(/(\d+)张/) ? parseInt(prompt.match(/(\d+)张/)[1]) : 4;
@@ -858,7 +879,7 @@ async function generateJimeng40MultiImages(
858
  ],
859
  }),
860
  http_common_info: {
861
- aid: isUS ? DEFAULT_ASSISTANT_ID_US : DEFAULT_ASSISTANT_ID_CN
862
  }
863
  },
864
  }
 
7
  import { getCredit, receiveCredit, request } from "./core.ts";
8
  import logger from "@/lib/logger.ts";
9
  import { SmartPoller, PollingStatus } from "@/lib/smart-poller.ts";
10
+ import { DEFAULT_ASSISTANT_ID_CN, DEFAULT_ASSISTANT_ID_US, DEFAULT_ASSISTANT_ID_HK, DEFAULT_ASSISTANT_ID_JP, DEFAULT_ASSISTANT_ID_SG, DEFAULT_IMAGE_MODEL, DRAFT_VERSION, DRAFT_MIN_VERSION, IMAGE_MODEL_MAP, IMAGE_MODEL_MAP_US, RESOLUTION_OPTIONS } from "@/api/consts/common.ts";
11
+ import { BASE_URL_DREAMINA_US, BASE_URL_DREAMINA_HK, BASE_URL_IMAGEX_US, BASE_URL_IMAGEX_HK, WEB_VERSION as DREAMINA_WEB_VERSION, DA_VERSION as DREAMINA_DA_VERSION, AIGC_FEATURES as DREAMINA_AIGC_FEATURES } from "@/api/consts/dreamina.ts";
12
  import { createSignature } from "@/lib/aws-signature.ts";
13
 
14
  export const DEFAULT_MODEL = DEFAULT_IMAGE_MODEL;
 
33
  resolution_type: resolution,
34
  };
35
  }
36
+ export function getModel(model: string, isInternational: boolean) {
37
+ const modelMap = isInternational ? IMAGE_MODEL_MAP_US : IMAGE_MODEL_MAP;
38
+ if (isInternational && !modelMap[model]) {
39
  const supportedModels = Object.keys(modelMap).join(', ');
40
  throw new Error(`国际版不支持模型 "${model}"。支持的模型: ${supportedModels}`);
41
  }
 
60
  return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
61
  }
62
 
63
+ async function uploadImageFromUrl(imageUrl: string, refreshToken: string, isInternational: boolean): Promise<string> {
64
  try {
65
+ logger.info(`开始上传图片: ${imageUrl} (isInternational: ${isInternational})`);
66
 
67
  const imageResponse = await fetch(imageUrl);
68
  if (!imageResponse.ok) {
 
70
  }
71
  const imageBuffer = await imageResponse.arrayBuffer();
72
 
73
+ return await uploadImageFromBuffer(Buffer.from(imageBuffer), refreshToken, isInternational);
74
 
75
  } catch (error) {
76
  logger.error(`图片上传失败: ${error.message}`);
 
78
  }
79
  }
80
 
81
+ async function uploadImageFromBuffer(imageBuffer: Buffer, refreshToken: string, isInternational: boolean): Promise<string> {
82
  try {
83
+ logger.info(`开始通过Buffer上传图片... (isInternational: ${isInternational})`);
84
+
85
+ const isUS = refreshToken.toLowerCase().startsWith('us-');
86
+ const isHK = refreshToken.toLowerCase().startsWith('hk-');
87
+ const isJP = refreshToken.toLowerCase().startsWith('jp-');
88
+ const isSG = refreshToken.toLowerCase().startsWith('sg-');
89
 
90
  const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
91
  data: {
92
  scene: 2,
93
  },
94
+ params: isInternational ? {
95
+ aid: isUS ? DEFAULT_ASSISTANT_ID_US : (isJP ? DEFAULT_ASSISTANT_ID_JP : (isSG ? DEFAULT_ASSISTANT_ID_SG : DEFAULT_ASSISTANT_ID_HK)),
96
  web_version: DREAMINA_WEB_VERSION,
97
  da_version: DREAMINA_DA_VERSION,
98
  aigc_features: DREAMINA_AIGC_FEATURES,
 
101
  },
102
  });
103
  const { access_key_id, secret_access_key, session_token } = tokenResult;
104
+ const service_id = isInternational ? tokenResult.space_name : tokenResult.service_id;
105
 
106
  if (!access_key_id || !secret_access_key || !session_token) {
107
  throw new Error("获取上传令牌失败");
108
  }
109
 
110
+ const actualServiceId = service_id || (isUS ? "wopfjsm1ax" : (isHK || isJP || isSG) ? "wopfjsm1ax" : "tb4s082cfz");
111
 
112
  logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
113
 
 
119
  const now = new Date();
120
  const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
121
  const randomStr = Math.random().toString(36).substring(2, 12);
122
+
123
+ const applyUrlHost = isUS ? BASE_URL_IMAGEX_US : (isHK || isJP || isSG) ? BASE_URL_IMAGEX_HK : 'https://imagex.bytedanceapi.com';
124
+ const applyUrl = `${applyUrlHost}/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}${isInternational ? '&device_platform=web' : ''}`;
125
 
126
  const requestHeaders = {
127
  'x-amz-date': timestamp,
 
130
 
131
  const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token);
132
 
133
+ const origin = isUS ? new URL(BASE_URL_DREAMINA_US).origin : (isHK || isJP || isSG) ? new URL(BASE_URL_DREAMINA_HK).origin : 'https://jimeng.jianying.com';
134
 
135
  const applyResponse = await fetch(applyUrl, {
136
  method: 'GET',
 
252
  refreshToken: string
253
  ) {
254
  const isUS = refreshToken.toLowerCase().startsWith('us-');
255
+ const isHK = refreshToken.toLowerCase().startsWith('hk-');
256
+ const isJP = refreshToken.toLowerCase().startsWith('jp-');
257
+ const isSG = refreshToken.toLowerCase().startsWith('sg-');
258
+ const isInternational = isUS || isHK || isJP || isSG;
259
+ const model = getModel(_model, isInternational);
260
 
261
  let width, height, image_ratio, resolution_type;
262
 
 
292
  let imageId: string;
293
  if (typeof image === 'string') {
294
  logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (URL)...`);
295
+ imageId = await uploadImageFromUrl(image, refreshToken, isInternational);
296
  } else {
297
  logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (Buffer)...`);
298
+ imageId = await uploadImageFromBuffer(image, refreshToken, isInternational);
299
  }
300
  uploadedImageIds.push(imageId);
301
  logger.info(`图片 ${i + 1}/${imageCount} 上传成功: ${imageId}`);
 
412
  ],
413
  }),
414
  http_common_info: {
415
+ aid: isInternational ? (isUS ? DEFAULT_ASSISTANT_ID_US : (isJP ? DEFAULT_ASSISTANT_ID_JP : (isSG ? DEFAULT_ASSISTANT_ID_SG : DEFAULT_ASSISTANT_ID_HK))) : DEFAULT_ASSISTANT_ID_CN
416
  }
417
  },
418
  }
 
530
  refreshToken: string
531
  ) {
532
  const isUS = refreshToken.toLowerCase().startsWith('us-');
533
+ const isHK = refreshToken.toLowerCase().startsWith('hk-');
534
+ const isJP = refreshToken.toLowerCase().startsWith('jp-');
535
+ const isSG = refreshToken.toLowerCase().startsWith('sg-');
536
+ const isInternational = isUS || isHK || isJP || isSG;
537
+ const model = getModel(_model, isInternational);
538
  logger.info(`使用模型: ${_model} 映射模型: ${model} 分辨率: ${resolution} 比例: ${ratio} 精细度: ${sampleStrength}`);
539
 
540
  return await generateImagesInternal(_model, prompt, { ratio, resolution, sampleStrength, negativePrompt }, refreshToken);
 
557
  refreshToken: string
558
  ) {
559
  const isUS = refreshToken.toLowerCase().startsWith('us-');
560
+ const isHK = refreshToken.toLowerCase().startsWith('hk-');
561
+ const isJP = refreshToken.toLowerCase().startsWith('jp-');
562
+ const isSG = refreshToken.toLowerCase().startsWith('sg-');
563
+ const isInternational = isUS || isHK || isJP || isSG;
564
+ const model = getModel(_model, isInternational);
565
 
566
  let width, height, image_ratio, resolution_type;
567
 
 
672
  ],
673
  }),
674
  http_common_info: {
675
+ aid: isInternational ? (isUS ? DEFAULT_ASSISTANT_ID_US : (isJP ? DEFAULT_ASSISTANT_ID_JP : (isSG ? DEFAULT_ASSISTANT_ID_SG : DEFAULT_ASSISTANT_ID_HK))) : DEFAULT_ASSISTANT_ID_CN
676
  }
677
  },
678
  }
 
792
  refreshToken: string
793
  ) {
794
  const isUS = refreshToken.toLowerCase().startsWith('us-');
795
+ const isHK = refreshToken.toLowerCase().startsWith('hk-');
796
+ const isJP = refreshToken.toLowerCase().startsWith('jp-');
797
+ const isSG = refreshToken.toLowerCase().startsWith('sg-');
798
+ const isInternational = isUS || isHK || isJP || isSG;
799
+ const model = getModel(_model, isInternational);
800
  const { width, height, image_ratio, resolution_type } = getResolutionParams(resolution, ratio);
801
 
802
  const targetImageCount = prompt.match(/(\d+)张/) ? parseInt(prompt.match(/(\d+)张/)[1]) : 4;
 
879
  ],
880
  }),
881
  http_common_info: {
882
+ aid: isInternational ? (isUS ? DEFAULT_ASSISTANT_ID_US : (isJP ? DEFAULT_ASSISTANT_ID_JP : (isSG ? DEFAULT_ASSISTANT_ID_SG : DEFAULT_ASSISTANT_ID_HK))) : DEFAULT_ASSISTANT_ID_CN
883
  }
884
  },
885
  }
src/api/controllers/videos.ts CHANGED
@@ -8,7 +8,8 @@ import util from "@/lib/util.ts";
8
  import { getCredit, receiveCredit, request } from "./core.ts";
9
  import logger from "@/lib/logger.ts";
10
  import { SmartPoller, PollingStatus } from "@/lib/smart-poller.ts";
11
- import { DEFAULT_ASSISTANT_ID_CN, DEFAULT_VIDEO_MODEL, DRAFT_VERSION, VIDEO_MODEL_MAP } from "@/api/consts/common.ts";
 
12
 
13
  export const DEFAULT_MODEL = DEFAULT_VIDEO_MODEL;
14
 
@@ -24,16 +25,16 @@ function createSignature(
24
  accessKeyId: string,
25
  secretAccessKey: string,
26
  sessionToken?: string,
27
- payload: string = ''
 
28
  ) {
29
  const urlObj = new URL(url);
30
  const pathname = urlObj.pathname || '/';
31
  const search = urlObj.search;
32
-
33
  // 创建规范请求
34
  const timestamp = headers['x-amz-date'];
35
  const date = timestamp.substr(0, 8);
36
- const region = 'cn-north-1';
37
  const service = 'imagex';
38
 
39
  // 规范化查询参数
@@ -128,40 +129,54 @@ function calculateCRC32(buffer: ArrayBuffer): string {
128
 
129
  // 核心上传逻辑:上传二进制buffer到ImageX
130
  async function _uploadImageBuffer(imageBuffer: ArrayBuffer, refreshToken: string): Promise<string> {
 
 
 
 
 
 
 
 
 
131
  // 第一步:获取上传令牌
132
  const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
133
  data: {
134
  scene: 2, // AIGC 图片上传场景
135
  },
136
  });
137
-
138
  const { access_key_id, secret_access_key, session_token, service_id } = tokenResult;
139
  if (!access_key_id || !secret_access_key || !session_token) {
140
  throw new Error("获取上传令牌失败");
141
  }
142
-
143
- const actualServiceId = service_id || "tb4s082cfz";
144
  logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
145
 
146
  const fileSize = imageBuffer.byteLength;
147
  const crc32 = calculateCRC32(imageBuffer);
148
 
149
  logger.info(`图片Buffer准备完成: 大小=${fileSize}字节, CRC32=${crc32}`);
150
-
151
  // 第二步:申请图片上传权限
152
  const now = new Date();
153
  const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
154
-
155
  const randomStr = Math.random().toString(36).substring(2, 12);
156
- const applyUrl = `https://imagex.bytedanceapi.com/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}`;
157
-
 
 
 
158
  const requestHeaders = {
159
  'x-amz-date': timestamp,
160
  'x-amz-security-token': session_token
161
  };
162
-
163
- const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token);
164
-
 
 
165
  logger.info(`申请上传权限: ${applyUrl}`);
166
 
167
  const applyResponse = await fetch(applyUrl, {
@@ -170,8 +185,8 @@ async function _uploadImageBuffer(imageBuffer: ArrayBuffer, refreshToken: string
170
  'accept': '*/*',
171
  'accept-language': 'zh-CN,zh;q=0.9',
172
  'authorization': authorization,
173
- 'origin': 'https://jimeng.jianying.com',
174
- 'referer': 'https://jimeng.jianying.com/ai-tool/video/generate',
175
  'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
176
  'sec-ch-ua-mobile': '?0',
177
  'sec-ch-ua-platform': '"Windows"',
@@ -240,26 +255,26 @@ async function _uploadImageBuffer(imageBuffer: ArrayBuffer, refreshToken: string
240
  }
241
 
242
  logger.info(`图片文件上传成功`);
243
-
244
  // 第四步:提交上传
245
- const commitUrl = `https://imagex.bytedanceapi.com/?Action=CommitImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}`;
246
-
247
  const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
248
  const commitPayload = JSON.stringify({
249
  SessionKey: uploadAddress.SessionKey,
250
  SuccessActionStatus: "200"
251
  });
252
-
253
  const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex');
254
-
255
  const commitRequestHeaders = {
256
  'x-amz-date': commitTimestamp,
257
  'x-amz-security-token': session_token,
258
  'x-amz-content-sha256': payloadHash
259
  };
260
-
261
- const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload);
262
-
263
  const commitResponse = await fetch(commitUrl, {
264
  method: 'POST',
265
  headers: {
@@ -267,8 +282,8 @@ async function _uploadImageBuffer(imageBuffer: ArrayBuffer, refreshToken: string
267
  'accept-language': 'zh-CN,zh;q=0.9',
268
  'authorization': commitAuthorization,
269
  'content-type': 'application/json',
270
- 'origin': 'https://jimeng.jianying.com',
271
- 'referer': 'https://jimeng.jianying.com/ai-tool/video/generate',
272
  'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
273
  'sec-ch-ua-mobile': '?0',
274
  'sec-ch-ua-platform': '"Windows"',
@@ -372,6 +387,15 @@ export async function generateVideo(
372
  },
373
  refreshToken: string
374
  ) {
 
 
 
 
 
 
 
 
 
375
  const model = getModel(_model);
376
 
377
  // 将秒转换为毫秒,只支持5秒和10秒
@@ -513,12 +537,12 @@ export async function generateVideo(
513
  {
514
  params: {
515
  aigc_features: "app_lip_sync",
516
- web_version: "6.6.0",
517
  da_version: DRAFT_VERSION,
518
  },
519
  data: {
520
  "extend": {
521
- "root_model": end_frame_image ? MODEL_MAP['jimeng-video-3.0'] : model,
522
  "m_video_commerce_info": {
523
  benefit_type: "basic_video_operation_vgfm_v_three",
524
  resource_id: "generate_video",
@@ -590,7 +614,9 @@ export async function generateVideo(
590
  }],
591
  }),
592
  http_common_info: {
593
- aid: Number(DEFAULT_ASSISTANT_ID_CN),
 
 
594
  },
595
  },
596
  }
@@ -600,158 +626,103 @@ export async function generateVideo(
600
  if (!historyId)
601
  throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
602
 
603
- // 轮询获取结果
604
- let status = 20, failCode, item_list = [];
605
- let retryCount = 0;
606
- const maxRetries = 60; // 增加重试次数,支持约20分钟的总重试时间
607
-
608
- // 首次查询前等待更长时间,让服务器有时间处理请求
609
  await new Promise((resolve) => setTimeout(resolve, 5000));
610
-
611
- logger.info(`开始轮询视频生成结果,历史ID: ${historyId},最大重试次数: ${maxRetries}`);
612
- logger.info(`即梦官网API地址: https://jimeng.jianying.com/mweb/v1/get_history_by_ids`);
613
- logger.info(`视频生成请求已发送,请同时在即梦官网查看: https://jimeng.jianying.com/ai-tool/video/generate`);
614
-
615
- while (status === 20 && retryCount < maxRetries) {
616
- try {
617
- // 构建请求URL和参数
618
- const requestUrl = "/mweb/v1/get_history_by_ids";
619
- const requestData = {
 
 
 
 
 
 
 
 
 
620
  history_ids: [historyId],
621
- };
622
-
623
- // 尝试两种不同的API请求方式
624
- let result;
625
- let useAlternativeApi = retryCount > 10 && retryCount % 2 === 0; // 在重试10次后,每隔一次尝试备用API
626
-
627
- if (useAlternativeApi) {
628
- // 备用API请求方式
629
- logger.info(`尝试备用API请求方式,URL: ${requestUrl}, 历史ID: ${historyId}, 重试次数: ${retryCount + 1}/${maxRetries}`);
630
- const alternativeRequestData = {
631
- history_record_ids: [historyId],
632
- };
633
- result = await request("post", "/mweb/v1/get_history_records", refreshToken, {
634
- data: alternativeRequestData,
635
- });
636
- logger.info(`备用API响应: ${JSON.stringify(result)}`);
637
-
638
- // 尝试直接从响应中提取视频URL
639
- const responseStr = JSON.stringify(result);
640
- const videoUrlMatch = responseStr.match(/https:\/\/v[0-9]+-artist\.vlabvod\.com\/[^"\s]+/);
641
- if (videoUrlMatch && videoUrlMatch[0]) {
642
- logger.info(`从备用API响应中直接提取到视频URL: ${videoUrlMatch[0]}`);
643
- // 提前返回找到的URL
644
- return videoUrlMatch[0];
645
- }
646
- } else {
647
- // 标准API请求方式
648
- logger.info(`发送请求获取视频生成结果,URL: ${requestUrl}, 历史ID: ${historyId}, 重试次数: ${retryCount + 1}/${maxRetries}`);
649
- result = await request("post", requestUrl, refreshToken, {
650
- data: requestData,
651
- });
652
- const responseStr = JSON.stringify(result);
653
- logger.info(`标准API响应摘要: ${responseStr.substring(0, 300)}...`);
654
-
655
- // 尝试直接从响应中提取视频URL
656
- const videoUrlMatch = responseStr.match(/https:\/\/v[0-9]+-artist\.vlabvod\.com\/[^"\s]+/);
657
- if (videoUrlMatch && videoUrlMatch[0]) {
658
- logger.info(`从标准API响应中直接提取到视频URL: ${videoUrlMatch[0]}`);
659
- // 提前返回找到的URL
660
- return videoUrlMatch[0];
661
  }
662
- }
663
-
664
-
665
- // 检查结果是否有
666
- let historyData;
667
-
668
- if (useAlternativeApi && result.history_records && result.history_records.length > 0) {
669
- // 处理备用API返回的数据格式
670
- historyData = result.history_records[0];
671
- logger.info(`从备用API获取到历史记录`);
672
- } else if (result.history_list && result.history_list.length > 0) {
673
- // 处理标准API返回的数据格式
674
- historyData = result.history_list[0];
675
- logger.info(`从标准API获取到历史记录`);
676
- } else {
677
- // 两种API都没有返回有效数据
678
- logger.warn(`历史记录不存在,重试中 (${retryCount + 1}/${maxRetries})... 历史ID: ${historyId}`);
679
- logger.info(`请同时在即梦官网检查视频是否已生成: https://jimeng.jianying.com/ai-tool/video/generate`);
680
-
681
- retryCount++;
682
- // 增加重试间隔时间,但设置上限为30秒
683
- const waitTime = Math.min(2000 * (retryCount + 1), 30000);
684
- logger.info(`等待 ${waitTime}ms 后进行第 ${retryCount + 1} 次重试`);
685
- await new Promise((resolve) => setTimeout(resolve, waitTime));
686
- continue;
687
- }
688
-
689
- // 记录获取到的结果详情
690
- logger.info(`获取到历史记录结果: ${JSON.stringify(historyData)}`);
691
-
692
-
693
- // 从历史数据中提取状态和结果
694
- status = historyData.status;
695
- failCode = historyData.fail_code;
696
- item_list = historyData.item_list || [];
697
-
698
- logger.info(`视频生成状态: ${status}, 失败代码: ${failCode || '无'}, 项目列表长度: ${item_list.length}`);
699
-
700
- // 如果有视频URL,提前记录
701
- let tempVideoUrl = item_list?.[0]?.video?.transcoded_video?.origin?.video_url;
702
- if (!tempVideoUrl) {
703
- // 尝试从其他可能的路径获取
704
- tempVideoUrl = item_list?.[0]?.video?.play_url ||
705
- item_list?.[0]?.video?.download_url ||
706
- item_list?.[0]?.video?.url;
707
- }
708
-
709
  if (tempVideoUrl) {
710
  logger.info(`检测到视频URL: ${tempVideoUrl}`);
711
  }
712
-
713
- if (status === 30) {
714
- let error;
715
- if (failCode === 2038 || failCode === '2038') {
716
- error = new APIException(EX.API_CONTENT_FILTERED, "内容被过滤");
717
- } else if (failCode === 1001 || failCode === '1001') {
718
- error = new APIException(EX.API_IMAGE_GENERATION_FAILED, `生成失败,错误码: ${failCode}。可能原因:文本或图片中包含敏感内容`);
719
- } else {
720
- error = new APIException(EX.API_IMAGE_GENERATION_FAILED, `生成失败,错误码: ${failCode}`);
721
- }
722
- // 添加历史ID到错误对象,以便在chat.ts中显示
723
- error.historyId = historyId;
724
- throw error;
725
- }
726
-
727
- // 如果状态仍在处理中,等待后继续
728
- if (status === 20) {
729
- const waitTime = 2000 * (Math.min(retryCount + 1, 5)); // 随着重试次数增加等待时间,但最多10秒
730
- logger.info(`视频生成中,状态码: ${status},等待 ${waitTime}ms 后继续查询`);
731
- await new Promise((resolve) => setTimeout(resolve, waitTime));
732
- }
733
- } catch (error) {
734
- logger.error(`轮询视频生成结果出错: ${error.message}`);
735
- retryCount++;
736
- await new Promise((resolve) => setTimeout(resolve, 2000 * (retryCount + 1)));
737
  }
738
- }
739
-
740
- // 如果达到最大重试次数仍未成功
741
- if (retryCount >= maxRetries && status === 20) {
742
- logger.error(`视频生成超时,已尝试 ${retryCount} 次,总耗时约 ${Math.floor(retryCount * 2000 / 1000 / 60)} 分钟`);
743
- const error = new APIException(EX.API_IMAGE_GENERATION_FAILED, "获取视频生成结果超时,请稍后在即梦官网查看您的视频");
744
- // 添加历史ID到错误对象,以便在chat.ts中显示
745
- error.historyId = historyId;
746
- throw error;
747
- }
 
 
 
 
748
 
749
  // 提取视频URL
750
  let videoUrl = item_list?.[0]?.video?.transcoded_video?.origin?.video_url;
751
-
752
  // 如果通过常规路径无法获取视频URL,尝试其他可能的路径
753
  if (!videoUrl) {
754
- // 尝试从item_list中的其他可能位置获取
755
  if (item_list?.[0]?.video?.play_url) {
756
  videoUrl = item_list[0].video.play_url;
757
  logger.info(`从play_url获取到视频URL: ${videoUrl}`);
@@ -762,15 +733,13 @@ export async function generateVideo(
762
  videoUrl = item_list[0].video.url;
763
  logger.info(`从url获取到视频URL: ${videoUrl}`);
764
  } else {
765
- // 如果仍然找不到,记录错误并抛出异常
766
  logger.error(`未能获取视频URL,item_list: ${JSON.stringify(item_list)}`);
767
- const error = new APIException(EX.API_IMAGE_GENERATION_FAILED, "未能获取视频URL,请稍后在即梦官网查看");
768
- // 添加历史ID到错误对象,以便在chat.ts中显示
769
  error.historyId = historyId;
770
  throw error;
771
  }
772
  }
773
 
774
- logger.info(`视频生成成功,URL: ${videoUrl}`);
775
  return videoUrl;
776
  }
 
8
  import { getCredit, receiveCredit, request } from "./core.ts";
9
  import logger from "@/lib/logger.ts";
10
  import { SmartPoller, PollingStatus } from "@/lib/smart-poller.ts";
11
+ import { DEFAULT_ASSISTANT_ID_CN, DEFAULT_ASSISTANT_ID_US, DEFAULT_ASSISTANT_ID_HK, DEFAULT_ASSISTANT_ID_JP, DEFAULT_ASSISTANT_ID_SG, DEFAULT_VIDEO_MODEL, DRAFT_VERSION, VIDEO_MODEL_MAP } from "@/api/consts/common.ts";
12
+ import { BASE_URL_DREAMINA_US, BASE_URL_DREAMINA_HK, BASE_URL_IMAGEX_US, BASE_URL_IMAGEX_HK } from "@/api/consts/dreamina.ts";
13
 
14
  export const DEFAULT_MODEL = DEFAULT_VIDEO_MODEL;
15
 
 
25
  accessKeyId: string,
26
  secretAccessKey: string,
27
  sessionToken?: string,
28
+ payload: string = '',
29
+ region: string = 'cn-north-1'
30
  ) {
31
  const urlObj = new URL(url);
32
  const pathname = urlObj.pathname || '/';
33
  const search = urlObj.search;
34
+
35
  // 创建规范请求
36
  const timestamp = headers['x-amz-date'];
37
  const date = timestamp.substr(0, 8);
 
38
  const service = 'imagex';
39
 
40
  // 规范化查询参数
 
129
 
130
  // 核心上传逻辑:上传二进制buffer到ImageX
131
  async function _uploadImageBuffer(imageBuffer: ArrayBuffer, refreshToken: string): Promise<string> {
132
+ // 检测区域
133
+ const isUS = refreshToken.toLowerCase().startsWith('us-');
134
+ const isHK = refreshToken.toLowerCase().startsWith('hk-');
135
+ const isJP = refreshToken.toLowerCase().startsWith('jp-');
136
+ const isSG = refreshToken.toLowerCase().startsWith('sg-');
137
+ const isInternational = isUS || isHK || isJP || isSG;
138
+
139
+ logger.info(`开始上传视频图片... (isInternational: ${isInternational}, isUS: ${isUS}, isHK: ${isHK}, isJP: ${isJP}, isSG: ${isSG})`);
140
+
141
  // 第一步:获取上传令牌
142
  const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
143
  data: {
144
  scene: 2, // AIGC 图片上传场景
145
  },
146
  });
147
+
148
  const { access_key_id, secret_access_key, session_token, service_id } = tokenResult;
149
  if (!access_key_id || !secret_access_key || !session_token) {
150
  throw new Error("获取上传令牌失败");
151
  }
152
+
153
+ const actualServiceId = service_id || (isUS ? "wopfjsm1ax" : (isHK || isJP || isSG) ? "wopfjsm1ax" : "tb4s082cfz");
154
  logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
155
 
156
  const fileSize = imageBuffer.byteLength;
157
  const crc32 = calculateCRC32(imageBuffer);
158
 
159
  logger.info(`图片Buffer准备完成: 大小=${fileSize}字节, CRC32=${crc32}`);
160
+
161
  // 第二步:申请图片上传权限
162
  const now = new Date();
163
  const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
164
+
165
  const randomStr = Math.random().toString(36).substring(2, 12);
166
+ const applyUrlHost = isUS ? BASE_URL_IMAGEX_US : (isHK || isJP || isSG) ? BASE_URL_IMAGEX_HK : 'https://imagex.bytedanceapi.com';
167
+ const applyUrl = `${applyUrlHost}/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}${isInternational ? '&device_platform=web' : ''}`;
168
+
169
+ const region = isUS ? 'us-east-1' : (isHK || isJP || isSG) ? 'ap-southeast-1' : 'cn-north-1';
170
+
171
  const requestHeaders = {
172
  'x-amz-date': timestamp,
173
  'x-amz-security-token': session_token
174
  };
175
+
176
+ const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token, '', region);
177
+
178
+ const origin = isUS ? new URL(BASE_URL_DREAMINA_US).origin : (isHK || isJP || isSG) ? new URL(BASE_URL_DREAMINA_HK).origin : 'https://jimeng.jianying.com';
179
+
180
  logger.info(`申请上传权限: ${applyUrl}`);
181
 
182
  const applyResponse = await fetch(applyUrl, {
 
185
  'accept': '*/*',
186
  'accept-language': 'zh-CN,zh;q=0.9',
187
  'authorization': authorization,
188
+ 'origin': origin,
189
+ 'referer': `${origin}/ai-tool/video/generate`,
190
  'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
191
  'sec-ch-ua-mobile': '?0',
192
  'sec-ch-ua-platform': '"Windows"',
 
255
  }
256
 
257
  logger.info(`图片文件上传成功`);
258
+
259
  // 第四步:提交上传
260
+ const commitUrl = `${applyUrlHost}/?Action=CommitImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}`;
261
+
262
  const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
263
  const commitPayload = JSON.stringify({
264
  SessionKey: uploadAddress.SessionKey,
265
  SuccessActionStatus: "200"
266
  });
267
+
268
  const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex');
269
+
270
  const commitRequestHeaders = {
271
  'x-amz-date': commitTimestamp,
272
  'x-amz-security-token': session_token,
273
  'x-amz-content-sha256': payloadHash
274
  };
275
+
276
+ const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload, region);
277
+
278
  const commitResponse = await fetch(commitUrl, {
279
  method: 'POST',
280
  headers: {
 
282
  'accept-language': 'zh-CN,zh;q=0.9',
283
  'authorization': commitAuthorization,
284
  'content-type': 'application/json',
285
+ 'origin': origin,
286
+ 'referer': `${origin}/ai-tool/video/generate`,
287
  'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
288
  'sec-ch-ua-mobile': '?0',
289
  'sec-ch-ua-platform': '"Windows"',
 
387
  },
388
  refreshToken: string
389
  ) {
390
+ // 检测区域
391
+ const isUS = refreshToken.toLowerCase().startsWith('us-');
392
+ const isHK = refreshToken.toLowerCase().startsWith('hk-');
393
+ const isJP = refreshToken.toLowerCase().startsWith('jp-');
394
+ const isSG = refreshToken.toLowerCase().startsWith('sg-');
395
+ const isInternational = isUS || isHK || isJP || isSG;
396
+
397
+ logger.info(`视频生成区域检测: isUS=${isUS}, isHK=${isHK}, isJP=${isJP}, isSG=${isSG}, isInternational=${isInternational}`);
398
+
399
  const model = getModel(_model);
400
 
401
  // 将秒转换为毫秒,只支持5秒和10秒
 
537
  {
538
  params: {
539
  aigc_features: "app_lip_sync",
540
+ web_version: "7.5.0",
541
  da_version: DRAFT_VERSION,
542
  },
543
  data: {
544
  "extend": {
545
+ "root_model": end_frame_image ? VIDEO_MODEL_MAP['jimeng-video-3.0'] : model,
546
  "m_video_commerce_info": {
547
  benefit_type: "basic_video_operation_vgfm_v_three",
548
  resource_id: "generate_video",
 
614
  }],
615
  }),
616
  http_common_info: {
617
+ aid: isInternational
618
+ ? (isUS ? DEFAULT_ASSISTANT_ID_US : (isJP ? DEFAULT_ASSISTANT_ID_JP : (isSG ? DEFAULT_ASSISTANT_ID_SG : DEFAULT_ASSISTANT_ID_HK)))
619
+ : DEFAULT_ASSISTANT_ID_CN
620
  },
621
  },
622
  }
 
626
  if (!historyId)
627
  throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
628
 
629
+ logger.info(`视频生成任务已提交,history_id: ${historyId},等待生成完成...`);
630
+
631
+ // 首次查询前等待,让服务器有时间处理请求
 
 
 
632
  await new Promise((resolve) => setTimeout(resolve, 5000));
633
+
634
+ // 使用 SmartPoller 进行智能轮询
635
+ const maxPollCount = 900; // 增加轮询次数,支持更长的生成时间
636
+ let pollAttempts = 0;
637
+
638
+ const poller = new SmartPoller({
639
+ maxPollCount,
640
+ pollInterval: 2000, // 2秒基础间隔
641
+ expectedItemCount: 1,
642
+ type: 'video',
643
+ timeoutSeconds: 1200 // 20分钟超时
644
+ });
645
+
646
+ const { result: pollingResult, data: finalHistoryData } = await poller.poll(async () => {
647
+ pollAttempts++;
648
+
649
+ // 使用标准API请求方式
650
+ const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
651
+ data: {
652
  history_ids: [historyId],
653
+ },
654
+ });
655
+
656
+ // 尝试直接从响应中提取视频URL
657
+ const responseStr = JSON.stringify(result);
658
+ const videoUrlMatch = responseStr.match(/https:\/\/v[0-9]+-artist\.vlabvod\.com\/[^"\s]+/);
659
+ if (videoUrlMatch && videoUrlMatch[0]) {
660
+ logger.info(`从API响应中直接提取到视频URL: ${videoUrlMatch[0]}`);
661
+ // 构造成功状态并返回
662
+ return {
663
+ status: {
664
+ status: 10,
665
+ itemCount: 1,
666
+ historyId
667
+ } as PollingStatus,
668
+ data: {
669
+ status: 10,
670
+ item_list: [{
671
+ video: {
672
+ transcoded_video: {
673
+ origin: {
674
+ video_url: videoUrlMatch[0]
675
+ }
676
+ }
677
+ }
678
+ }]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
  }
680
+ };
681
+ }
682
+
683
+ // 检查响应中是否有该 history_id 的数据
684
+ if (!result[historyId]) {
685
+ logger.warn(`API未返回历史记录,historyId: ${historyId}`);
686
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在");
687
+ }
688
+
689
+ const historyData = result[historyId];
690
+
691
+ const currentStatus = historyData.status;
692
+ const currentFailCode = historyData.fail_code;
693
+ const currentItemList = historyData.item_list || [];
694
+ const finishTime = historyData.task?.finish_time || 0;
695
+
696
+ // 记录详细信息
697
+ if (currentItemList.length > 0) {
698
+ const tempVideoUrl = currentItemList[0]?.video?.transcoded_video?.origin?.video_url ||
699
+ currentItemList[0]?.video?.play_url ||
700
+ currentItemList[0]?.video?.download_url ||
701
+ currentItemList[0]?.video?.url;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  if (tempVideoUrl) {
703
  logger.info(`检测到视频URL: ${tempVideoUrl}`);
704
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705
  }
706
+
707
+ return {
708
+ status: {
709
+ status: currentStatus,
710
+ failCode: currentFailCode,
711
+ itemCount: currentItemList.length,
712
+ finishTime,
713
+ historyId
714
+ } as PollingStatus,
715
+ data: historyData
716
+ };
717
+ }, historyId);
718
+
719
+ const item_list = finalHistoryData.item_list || [];
720
 
721
  // 提取视频URL
722
  let videoUrl = item_list?.[0]?.video?.transcoded_video?.origin?.video_url;
723
+
724
  // 如果通过常规路径无法获取视频URL,尝试其他可能的路径
725
  if (!videoUrl) {
 
726
  if (item_list?.[0]?.video?.play_url) {
727
  videoUrl = item_list[0].video.play_url;
728
  logger.info(`从play_url获取到视频URL: ${videoUrl}`);
 
733
  videoUrl = item_list[0].video.url;
734
  logger.info(`从url获取到视频URL: ${videoUrl}`);
735
  } else {
 
736
  logger.error(`未能获取视频URL,item_list: ${JSON.stringify(item_list)}`);
737
+ const error = new APIException(EX.API_IMAGE_GENERATION_FAILED, "未能获取视频URL,请稍后查看");
 
738
  error.historyId = historyId;
739
  throw error;
740
  }
741
  }
742
 
743
+ logger.info(`视频生成成功,URL: ${videoUrl},总耗时: ${pollingResult.elapsedTime}秒`);
744
  return videoUrl;
745
  }