xt8 commited on
Commit
e097ca3
·
verified ·
1 Parent(s): a5a9842

Upload 52 files

Browse files
Dockerfile CHANGED
@@ -49,17 +49,17 @@ RUN mkdir -p /app/logs /app/tmp && \
49
  chown -R jimeng:nodejs /app/logs /app/tmp
50
 
51
  # 设置环境变量
52
- ENV SERVER_PORT=7860
53
 
54
  # 切换到非root用户
55
  USER jimeng
56
 
57
  # 暴露端口
58
- EXPOSE 7860
59
 
60
  # 健康检查
61
  HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=3 \
62
- CMD wget -q --spider http://localhost:7860/ping
63
 
64
  # 启动应用
65
  CMD ["yarn", "start"]
 
49
  chown -R jimeng:nodejs /app/logs /app/tmp
50
 
51
  # 设置环境变量
52
+ ENV SERVER_PORT=5100
53
 
54
  # 切换到非root用户
55
  USER jimeng
56
 
57
  # 暴露端口
58
+ EXPOSE 5100
59
 
60
  # 健康检查
61
  HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=3 \
62
+ CMD wget -q --spider http://localhost:5100/ping
63
 
64
  # 启动应用
65
  CMD ["yarn", "start"]
src/api/consts/dreamina.ts CHANGED
@@ -4,7 +4,7 @@ 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";
 
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://imagex-normal-sg.capcutapi.com";
8
 
9
 
10
  export const WEB_VERSION = "7.5.0";
src/api/controllers/core.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { PassThrough } from "stream";
2
  import path from "path";
3
  import _ from "lodash";
4
  import mime from "mime";
@@ -6,7 +5,6 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
6
 
7
  import APIException from "@/lib/exceptions/APIException.ts";
8
  import EX from "@/api/consts/exceptions.ts";
9
- 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";
@@ -73,14 +71,72 @@ export async function acquireToken(refreshToken: string): Promise<string> {
73
  return refreshToken;
74
  }
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  /**
77
  * 生成cookie
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';
@@ -110,12 +166,14 @@ export function generateCookie(refreshToken: string) {
110
  * @param refreshToken 用于刷新access_token的refresh_token
111
  */
112
  export async function getCredit(refreshToken: string) {
 
 
113
  const {
114
  credit: { gift_credit, purchase_credit, vip_credit }
115
  } = await request("POST", "/commerce/v1/benefits/user_credit", refreshToken, {
116
  data: {},
117
  headers: {
118
- Referer: "https://jimeng.jianying.com/ai-tool/image/generate",
119
  },
120
  noDefaultParams: true
121
  });
@@ -135,12 +193,14 @@ export async function getCredit(refreshToken: string) {
135
  */
136
  export async function receiveCredit(refreshToken: string) {
137
  logger.info("正在收取今日积分...")
 
 
138
  const { cur_total_credits, receive_quota } = await request("POST", "/commerce/v1/benefits/credit_receive", refreshToken, {
139
  data: {
140
  time_zone: "Asia/Shanghai"
141
  },
142
  headers: {
143
- Referer: "https://jimeng.jianying.com/ai-tool/image/generate"
144
  }
145
  });
146
  logger.info(`\n今日${receive_quota}积分收取成功\n剩余积分: ${cur_total_credits}`);
@@ -161,11 +221,9 @@ export async function request(
161
  refreshToken: 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`
 
 
1
  import path from "path";
2
  import _ from "lodash";
3
  import mime from "mime";
 
5
 
6
  import APIException from "@/lib/exceptions/APIException.ts";
7
  import EX from "@/api/consts/exceptions.ts";
 
8
  import logger from "@/lib/logger.ts";
9
  import util from "@/lib/util.ts";
10
  import { JimengErrorHandler, JimengErrorResponse } from "@/lib/error-handler.ts";
 
71
  return refreshToken;
72
  }
73
 
74
+ /**
75
+ * 解析 token 中的地区信息
76
+ *
77
+ * @param refreshToken 刷新令牌
78
+ * @returns 地区信息对象
79
+ */
80
+ export interface RegionInfo {
81
+ isUS: boolean;
82
+ isHK: boolean;
83
+ isJP: boolean;
84
+ isSG: boolean;
85
+ isInternational: boolean;
86
+ isCN: boolean;
87
+ }
88
+
89
+ export function parseRegionFromToken(refreshToken: string): RegionInfo {
90
+ const token = refreshToken.toLowerCase();
91
+ const isUS = token.startsWith('us-');
92
+ const isHK = token.startsWith('hk-');
93
+ const isJP = token.startsWith('jp-');
94
+ const isSG = token.startsWith('sg-');
95
+ const isInternational = isUS || isHK || isJP || isSG;
96
+
97
+ return {
98
+ isUS,
99
+ isHK,
100
+ isJP,
101
+ isSG,
102
+ isInternational,
103
+ isCN: !isInternational
104
+ };
105
+ }
106
+
107
+ /**
108
+ * 根据地区获取 Referer
109
+ *
110
+ * @param refreshToken 刷新令牌
111
+ * @param cnPath 国内站路径
112
+ * @returns Referer URL
113
+ */
114
+ export function getRefererByRegion(refreshToken: string, cnPath: string): string {
115
+ const { isInternational } = parseRegionFromToken(refreshToken);
116
+ return isInternational
117
+ ? "https://dreamina.capcut.com/"
118
+ : `https://jimeng.jianying.com${cnPath}`;
119
+ }
120
+
121
+ /**
122
+ * 根据地区获取 AssistantID
123
+ *
124
+ * @param regionInfo 地区信息
125
+ * @returns AssistantID
126
+ */
127
+ export function getAssistantId(regionInfo: RegionInfo): number {
128
+ if (regionInfo.isUS) return DEFAULT_ASSISTANT_ID_US;
129
+ if (regionInfo.isJP) return DEFAULT_ASSISTANT_ID_JP;
130
+ if (regionInfo.isSG) return DEFAULT_ASSISTANT_ID_SG;
131
+ if (regionInfo.isHK) return DEFAULT_ASSISTANT_ID_HK;
132
+ return DEFAULT_ASSISTANT_ID_CN;
133
+ }
134
+
135
  /**
136
  * 生成cookie
137
  */
138
  export function generateCookie(refreshToken: string) {
139
+ const { isUS, isHK, isJP, isSG } = parseRegionFromToken(refreshToken);
 
 
 
140
  const token = (isUS || isHK || isJP || isSG) ? refreshToken.substring(3) : refreshToken;
141
 
142
  let storeRegion = 'cn-gd';
 
166
  * @param refreshToken 用于刷新access_token的refresh_token
167
  */
168
  export async function getCredit(refreshToken: string) {
169
+ const referer = getRefererByRegion(refreshToken, "/ai-tool/image/generate");
170
+
171
  const {
172
  credit: { gift_credit, purchase_credit, vip_credit }
173
  } = await request("POST", "/commerce/v1/benefits/user_credit", refreshToken, {
174
  data: {},
175
  headers: {
176
+ Referer: referer,
177
  },
178
  noDefaultParams: true
179
  });
 
193
  */
194
  export async function receiveCredit(refreshToken: string) {
195
  logger.info("正在收取今日积分...")
196
+ const referer = getRefererByRegion(refreshToken, "/ai-tool/home");
197
+
198
  const { cur_total_credits, receive_quota } = await request("POST", "/commerce/v1/benefits/credit_receive", refreshToken, {
199
  data: {
200
  time_zone: "Asia/Shanghai"
201
  },
202
  headers: {
203
+ Referer: referer
204
  }
205
  });
206
  logger.info(`\n今日${receive_quota}积分收取成功\n剩余积分: ${cur_total_credits}`);
 
221
  refreshToken: string,
222
  options: AxiosRequestConfig & { noDefaultParams?: boolean } = {}
223
  ) {
224
+ const regionInfo = parseRegionFromToken(refreshToken);
225
+ const { isUS, isHK, isJP, isSG } = regionInfo;
226
+ const token = await acquireToken(regionInfo.isInternational ? refreshToken.substring(3) : refreshToken);
 
 
227
  const deviceTime = util.unixTimestamp();
228
  const sign = util.md5(
229
  `9e2c|${uri.slice(-7)}|${PLATFORM_CODE}|${VERSION_CODE}|${deviceTime}||11ac`
src/api/controllers/images.ts CHANGED
@@ -1,15 +1,15 @@
1
  import _ from "lodash";
2
- import crypto from "crypto";
3
 
4
  import APIException from "@/lib/exceptions/APIException.ts";
5
  import EX from "@/api/consts/exceptions.ts";
6
  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_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;
15
 
@@ -42,198 +42,6 @@ export function getModel(model: string, isInternational: boolean) {
42
  return modelMap[model] || modelMap[DEFAULT_MODEL];
43
  }
44
 
45
- function calculateCRC32(buffer: ArrayBuffer): string {
46
- const crcTable = [];
47
- for (let i = 0; i < 256; i++) {
48
- let crc = i;
49
- for (let j = 0; j < 8; j++) {
50
- crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
51
- }
52
- crcTable[i] = crc;
53
- }
54
-
55
- let crc = 0 ^ (-1);
56
- const bytes = new Uint8Array(buffer);
57
- for (let i = 0; i < bytes.length; i++) {
58
- crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
59
- }
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) {
69
- throw new Error(`下载图片失败: ${imageResponse.status}`);
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}`);
77
- throw error;
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,
99
- } : {
100
- aid: DEFAULT_ASSISTANT_ID_CN.toString(),
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
-
114
- const fileSize = imageBuffer.byteLength;
115
- const crc32 = calculateCRC32(imageBuffer);
116
-
117
- logger.info(`图片Buffer: 大小=${fileSize}字节, CRC32=${crc32}`);
118
-
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,
128
- 'x-amz-security-token': session_token
129
- };
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',
137
- headers: {
138
- 'accept': '*/*',
139
- 'authorization': authorization,
140
- 'origin': origin,
141
- 'referer': `${origin}/ai-tool/generate`,
142
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
143
- 'x-amz-date': timestamp,
144
- 'x-amz-security-token': session_token,
145
- },
146
- });
147
-
148
- if (!applyResponse.ok) {
149
- const errorText = await applyResponse.text();
150
- throw new Error(`申请上传权限失败: ${applyResponse.status} - ${errorText}`);
151
- }
152
-
153
- const applyResult = await applyResponse.json();
154
-
155
- if (applyResult?.ResponseMetadata?.Error) {
156
- throw new Error(`申请上传权限失败: ${JSON.stringify(applyResult.ResponseMetadata.Error)}`);
157
- }
158
-
159
- const uploadAddress = applyResult?.Result?.UploadAddress;
160
- if (!uploadAddress || !uploadAddress.StoreInfos || !uploadAddress.UploadHosts) {
161
- throw new Error(`获取上传地址失败: ${JSON.stringify(applyResult)}`);
162
- }
163
-
164
- const storeInfo = uploadAddress.StoreInfos[0];
165
- const uploadHost = uploadAddress.UploadHosts[0];
166
- const auth = storeInfo.Auth;
167
- const uploadUrl = `https://${uploadHost}/upload/v1/${storeInfo.StoreUri}`;
168
-
169
- const uploadResponse = await fetch(uploadUrl, {
170
- method: 'POST',
171
- headers: {
172
- 'Authorization': auth,
173
- 'Content-CRC32': crc32,
174
- 'Content-Disposition': 'attachment; filename="undefined"',
175
- 'Content-Type': 'application/octet-stream',
176
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
177
- },
178
- body: imageBuffer,
179
- });
180
-
181
- if (!uploadResponse.ok) {
182
- const errorText = await uploadResponse.text();
183
- throw new Error(`图片上传失败: ${uploadResponse.status} - ${errorText}`);
184
- }
185
-
186
- const commitUrl = `${applyUrlHost}/?Action=CommitImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}`;
187
- const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
188
- const commitPayload = JSON.stringify({
189
- SessionKey: uploadAddress.SessionKey
190
- });
191
- const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex');
192
- const commitRequestHeaders = {
193
- 'x-amz-date': commitTimestamp,
194
- 'x-amz-security-token': session_token,
195
- 'x-amz-content-sha256': payloadHash
196
- };
197
- const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload);
198
-
199
- const commitResponse = await fetch(commitUrl, {
200
- method: 'POST',
201
- headers: {
202
- 'authorization': commitAuthorization,
203
- 'content-type': 'application/json',
204
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
205
- 'x-amz-date': commitTimestamp,
206
- 'x-amz-security-token': session_token,
207
- 'x-amz-content-sha256': payloadHash,
208
- },
209
- body: commitPayload,
210
- });
211
-
212
- if (!commitResponse.ok) {
213
- const errorText = await commitResponse.text();
214
- throw new Error(`提交上传失败: ${commitResponse.status} - ${errorText}`);
215
- }
216
-
217
- const commitResult = await commitResponse.json();
218
- if (commitResult?.ResponseMetadata?.Error) {
219
- throw new Error(`提交上传失败: ${JSON.stringify(commitResult.ResponseMetadata.Error)}`);
220
- }
221
- if (!commitResult?.Result?.Results || commitResult.Result.Results.length === 0) {
222
- throw new Error(`提交上传响应缺少结果: ${JSON.stringify(commitResult)}`);
223
- }
224
- const uploadResult = commitResult.Result.Results[0];
225
- if (uploadResult.UriStatus !== 2000) {
226
- throw new Error(`图片上传状态异常: UriStatus=${uploadResult.UriStatus}`);
227
- }
228
- const fullImageUri = uploadResult.Uri;
229
- logger.info(`图片Buffer上传完成: ${fullImageUri}`);
230
- return fullImageUri;
231
- } catch (error) {
232
- logger.error(`图片Buffer上传失败: ${error.message}`);
233
- throw error;
234
- }
235
- }
236
-
237
  export async function generateImageComposition(
238
  _model: string,
239
  prompt: string,
@@ -251,11 +59,8 @@ export async function generateImageComposition(
251
  },
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;
@@ -292,10 +97,10 @@ export async function generateImageComposition(
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,7 +217,7 @@ export async function generateImageComposition(
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
  }
@@ -479,28 +284,7 @@ export async function generateImageComposition(
479
  }, historyId);
480
 
481
  const item_list = finalTaskInfo.item_list || [];
482
-
483
- const resultImageUrls = item_list.map((item: any, index: number) => {
484
- let imageUrl: string | null = null;
485
-
486
- if (item?.image?.large_images?.[0]?.image_url) {
487
- imageUrl = item.image.large_images[0].image_url;
488
- logger.debug(`图片 ${index + 1}: 使用 large_images URL`);
489
- } else if (item?.common_attr?.cover_url) {
490
- imageUrl = item.common_attr.cover_url;
491
- logger.debug(`图片 ${index + 1}: 使用 cover_url`);
492
- } else if (item?.image_url) {
493
- imageUrl = item.image_url;
494
- logger.debug(`图片 ${index + 1}: 使用 image_url`);
495
- } else if (item?.url) {
496
- imageUrl = item.url;
497
- logger.debug(`图片 ${index + 1}: 使用 url`);
498
- } else {
499
- logger.warn(`图片 ${index + 1}: 无法提取URL,item结构: ${JSON.stringify(item, null, 2)}`);
500
- }
501
-
502
- return imageUrl;
503
- }).filter((url: string | null) => url !== null) as string[];
504
 
505
  logger.info(`图生图结果: 成功生成 ${resultImageUrls.length} 张图片,总耗时 ${pollingResult.elapsedTime} 秒,最终状态: ${pollingResult.status}`);
506
 
@@ -529,12 +313,8 @@ export async function generateImages(
529
  },
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);
@@ -556,12 +336,8 @@ async function generateImagesInternal(
556
  },
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,7 +448,7 @@ async function generateImagesInternal(
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
  }
@@ -744,27 +520,7 @@ async function generateImagesInternal(
744
  .map((x: any) => String(x).trim())
745
  .filter(Boolean);
746
 
747
- const imageUrls = item_list.map((item: any, index: number) => {
748
- let imageUrl: string | null = null;
749
-
750
- if (item?.image?.large_images?.[0]?.image_url) {
751
- imageUrl = item.image.large_images[0].image_url;
752
- logger.debug(`图片 ${index + 1}: 使用 large_images URL`);
753
- } else if (item?.common_attr?.cover_url) {
754
- imageUrl = item.common_attr.cover_url;
755
- logger.debug(`图片 ${index + 1}: 使用 cover_url`);
756
- } else if (item?.image_url) {
757
- imageUrl = item.image_url;
758
- logger.debug(`图片 ${index + 1}: 使用 image_url`);
759
- } else if (item?.url) {
760
- imageUrl = item.url;
761
- logger.debug(`图片 ${index + 1}: 使用 url`);
762
- } else {
763
- logger.warn(`图片 ${index + 1}: 无法提取URL,item结构: ${JSON.stringify(item, null, 2)}`);
764
- }
765
-
766
- return imageUrl;
767
- }).filter((url: string | null) => url !== null) as string[];
768
 
769
  logger.info(`图像生成完成: 成功生成 ${imageUrls.length} 张图片,总耗时 ${pollingResult.elapsedTime} 秒,最终状态: ${pollingResult.status}`);
770
 
@@ -791,12 +547,8 @@ async function generateJimeng40MultiImages(
791
  },
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,7 +631,7 @@ async function generateJimeng40MultiImages(
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
  }
@@ -891,18 +643,15 @@ async function generateJimeng40MultiImages(
891
 
892
  logger.info(`多图生成任务已提交,submit_id: ${submitId}, history_id: ${historyId},等待生成 ${targetImageCount} 张图片...`);
893
 
894
- let status = 20, failCode: string | undefined, item_list: any[] = [];
895
- let pollCount = 0;
896
  const maxPollCount = 600;
897
 
898
- while (pollCount < maxPollCount) {
899
- await new Promise((resolve) => setTimeout(resolve, 1000));
900
- pollCount++;
901
-
902
- if (pollCount % 30 === 0) {
903
- logger.info(`多图生成进度: 第 ${pollCount} 次轮询 (history_id: ${historyId}),当前状态: ${status},已生成: ${item_list.length}/${targetImageCount} 张图片...`);
904
- }
905
 
 
906
  const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
907
  data: {
908
  history_ids: [historyId],
@@ -928,44 +677,28 @@ async function generateJimeng40MultiImages(
928
  if (!result[historyId])
929
  throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在");
930
 
931
- status = result[historyId].status;
932
- failCode = result[historyId].fail_code;
933
- item_list = result[historyId].item_list || [];
934
-
935
- if (item_list.length >= targetImageCount) {
936
- logger.info(`多图生成完成: 状态=${status}, 已生成 ${item_list.length} 张图片`);
937
- break;
938
- }
939
-
940
- if (pollCount % 60 === 0) {
941
- logger.info(`详细状态: status=${status}, item_list.length=${item_list.length}, failCode=${failCode || 'none'}`);
942
- }
943
-
944
- if (status === 10 && item_list.length < targetImageCount && pollCount % 30 === 0) {
945
- logger.info(`状态已完成但图片数量不足: 状态=${status}, 已生成 ${item_list.length}/${targetImageCount} 张图片,继续等待...`);
946
- }
947
- }
948
-
949
- if (pollCount >= maxPollCount) {
950
- logger.warn(`多图生成超时: 轮询了 ${pollCount} 次,当前状态: ${status},已生成图片数: ${item_list.length}`);
951
- }
952
 
953
- if (status === 30) {
954
- if (failCode === '2038')
955
- throw new APIException(EX.API_CONTENT_FILTERED);
956
- else if (failCode === '1001' || failCode === 1001)
957
- throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `生成失败,错误代码: ${failCode}。可能原因:文本或图片中包含敏感内容`);
958
- else
959
- throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `生成失败,错误代码: ${failCode}`);
960
- }
 
 
 
961
 
962
- const imageUrls = item_list.map((item) => {
963
- if(!item?.image?.large_images?.[0]?.image_url)
964
- return item?.common_attr?.cover_url || null;
965
- return item.image.large_images[0].image_url;
966
- }).filter(url => url !== null);
967
 
968
- logger.info(`多图生成结果: 成功生成 ${imageUrls.length} 张图片`);
969
  return imageUrls;
970
  }
971
 
 
1
  import _ from "lodash";
 
2
 
3
  import APIException from "@/lib/exceptions/APIException.ts";
4
  import EX from "@/api/consts/exceptions.ts";
5
  import util from "@/lib/util.ts";
6
+ import { getCredit, receiveCredit, request, parseRegionFromToken, getAssistantId, RegionInfo } from "./core.ts";
7
  import logger from "@/lib/logger.ts";
8
  import { SmartPoller, PollingStatus } from "@/lib/smart-poller.ts";
9
  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";
10
+ import { WEB_VERSION as DREAMINA_WEB_VERSION, DA_VERSION as DREAMINA_DA_VERSION, AIGC_FEATURES as DREAMINA_AIGC_FEATURES } from "@/api/consts/dreamina.ts";
11
+ import { uploadImageFromUrl, uploadImageBuffer } from "@/lib/image-uploader.ts";
12
+ import { extractImageUrls } from "@/lib/image-utils.ts";
13
 
14
  export const DEFAULT_MODEL = DEFAULT_IMAGE_MODEL;
15
 
 
42
  return modelMap[model] || modelMap[DEFAULT_MODEL];
43
  }
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  export async function generateImageComposition(
46
  _model: string,
47
  prompt: string,
 
59
  },
60
  refreshToken: string
61
  ) {
62
+ const regionInfo = parseRegionFromToken(refreshToken);
63
+ const { isInternational } = regionInfo;
 
 
 
64
  const model = getModel(_model, isInternational);
65
 
66
  let width, height, image_ratio, resolution_type;
 
97
  let imageId: string;
98
  if (typeof image === 'string') {
99
  logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (URL)...`);
100
+ imageId = await uploadImageFromUrl(image, refreshToken, regionInfo);
101
  } else {
102
  logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (Buffer)...`);
103
+ imageId = await uploadImageBuffer(image, refreshToken, regionInfo);
104
  }
105
  uploadedImageIds.push(imageId);
106
  logger.info(`图片 ${i + 1}/${imageCount} 上传成功: ${imageId}`);
 
217
  ],
218
  }),
219
  http_common_info: {
220
+ aid: getAssistantId(regionInfo)
221
  }
222
  },
223
  }
 
284
  }, historyId);
285
 
286
  const item_list = finalTaskInfo.item_list || [];
287
+ const resultImageUrls = extractImageUrls(item_list);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
  logger.info(`图生图结果: 成功生成 ${resultImageUrls.length} 张图片,总耗时 ${pollingResult.elapsedTime} 秒,最终状态: ${pollingResult.status}`);
290
 
 
313
  },
314
  refreshToken: string
315
  ) {
316
+ const regionInfo = parseRegionFromToken(refreshToken);
317
+ const model = getModel(_model, regionInfo.isInternational);
 
 
 
 
318
  logger.info(`使用模型: ${_model} 映射模型: ${model} 分辨率: ${resolution} 比例: ${ratio} 精细度: ${sampleStrength}`);
319
 
320
  return await generateImagesInternal(_model, prompt, { ratio, resolution, sampleStrength, negativePrompt }, refreshToken);
 
336
  },
337
  refreshToken: string
338
  ) {
339
+ const regionInfo = parseRegionFromToken(refreshToken);
340
+ const model = getModel(_model, regionInfo.isInternational);
 
 
 
 
341
 
342
  let width, height, image_ratio, resolution_type;
343
 
 
448
  ],
449
  }),
450
  http_common_info: {
451
+ aid: getAssistantId(regionInfo)
452
  }
453
  },
454
  }
 
520
  .map((x: any) => String(x).trim())
521
  .filter(Boolean);
522
 
523
+ const imageUrls = extractImageUrls(item_list);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
 
525
  logger.info(`图像生成完成: 成功生成 ${imageUrls.length} 张图片,总耗时 ${pollingResult.elapsedTime} 秒,最终状态: ${pollingResult.status}`);
526
 
 
547
  },
548
  refreshToken: string
549
  ) {
550
+ const regionInfo = parseRegionFromToken(refreshToken);
551
+ const model = getModel(_model, regionInfo.isInternational);
 
 
 
 
552
  const { width, height, image_ratio, resolution_type } = getResolutionParams(resolution, ratio);
553
 
554
  const targetImageCount = prompt.match(/(\d+)张/) ? parseInt(prompt.match(/(\d+)张/)[1]) : 4;
 
631
  ],
632
  }),
633
  http_common_info: {
634
+ aid: getAssistantId(regionInfo)
635
  }
636
  },
637
  }
 
643
 
644
  logger.info(`多图生成任务已提交,submit_id: ${submitId}, history_id: ${historyId},等待生成 ${targetImageCount} 张图片...`);
645
 
 
 
646
  const maxPollCount = 600;
647
 
648
+ const poller = new SmartPoller({
649
+ maxPollCount,
650
+ expectedItemCount: targetImageCount,
651
+ type: 'image'
652
+ });
 
 
653
 
654
+ const { result: pollingResult, data: finalTaskInfo } = await poller.poll(async () => {
655
  const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
656
  data: {
657
  history_ids: [historyId],
 
677
  if (!result[historyId])
678
  throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在");
679
 
680
+ const taskInfo = result[historyId];
681
+ const currentStatus = taskInfo.status;
682
+ const currentFailCode = taskInfo.fail_code;
683
+ const currentItemList = taskInfo.item_list || [];
684
+ const finishTime = taskInfo.task?.finish_time || 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
 
686
+ return {
687
+ status: {
688
+ status: currentStatus,
689
+ failCode: currentFailCode,
690
+ itemCount: currentItemList.length,
691
+ finishTime,
692
+ historyId
693
+ } as PollingStatus,
694
+ data: taskInfo
695
+ };
696
+ }, historyId);
697
 
698
+ const item_list = finalTaskInfo.item_list || [];
699
+ const imageUrls = extractImageUrls(item_list);
 
 
 
700
 
701
+ logger.info(`多图生成结果: 成功生成 ${imageUrls.length} 张图片,总耗时 ${pollingResult.elapsedTime} 秒,最终状态: ${pollingResult.status}`);
702
  return imageUrls;
703
  }
704
 
src/api/controllers/videos.ts CHANGED
@@ -1,15 +1,15 @@
1
  import _ from "lodash";
2
- import crypto from "crypto";
3
  import fs from "fs-extra";
4
 
5
  import APIException from "@/lib/exceptions/APIException.ts";
6
  import EX from "@/api/consts/exceptions.ts";
7
  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_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
 
@@ -17,322 +17,20 @@ export function getModel(model: string) {
17
  return VIDEO_MODEL_MAP[model] || VIDEO_MODEL_MAP[DEFAULT_MODEL];
18
  }
19
 
20
- // AWS4-HMAC-SHA256 签名生成函数(从 images.ts 复制)
21
- function createSignature(
22
- method: string,
23
- url: string,
24
- headers: { [key: string]: string },
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
- // 规范化查询参数
41
- const queryParams: Array<[string, string]> = [];
42
- const searchParams = new URLSearchParams(search);
43
- searchParams.forEach((value, key) => {
44
- queryParams.push([key, value]);
45
- });
46
-
47
- // 按键名排序
48
- queryParams.sort(([a], [b]) => {
49
- if (a < b) return -1;
50
- if (a > b) return 1;
51
- return 0;
52
- });
53
-
54
- const canonicalQueryString = queryParams
55
- .map(([key, value]) => `${key}=${value}`)
56
- .join('&');
57
-
58
- // 规范化头部
59
- const headersToSign: { [key: string]: string } = {
60
- 'x-amz-date': timestamp
61
- };
62
-
63
- if (sessionToken) {
64
- headersToSign['x-amz-security-token'] = sessionToken;
65
- }
66
-
67
- let payloadHash = crypto.createHash('sha256').update('').digest('hex');
68
- if (method.toUpperCase() === 'POST' && payload) {
69
- payloadHash = crypto.createHash('sha256').update(payload, 'utf8').digest('hex');
70
- headersToSign['x-amz-content-sha256'] = payloadHash;
71
- }
72
-
73
- const signedHeaders = Object.keys(headersToSign)
74
- .map(key => key.toLowerCase())
75
- .sort()
76
- .join(';');
77
-
78
- const canonicalHeaders = Object.keys(headersToSign)
79
- .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
80
- .map(key => `${key.toLowerCase()}:${headersToSign[key].trim()}\n`)
81
- .join('');
82
-
83
- const canonicalRequest = [
84
- method.toUpperCase(),
85
- pathname,
86
- canonicalQueryString,
87
- canonicalHeaders,
88
- signedHeaders,
89
- payloadHash
90
- ].join('\n');
91
-
92
- // 创建待签名字符串
93
- const credentialScope = `${date}/${region}/${service}/aws4_request`;
94
- const stringToSign = [
95
- 'AWS4-HMAC-SHA256',
96
- timestamp,
97
- credentialScope,
98
- crypto.createHash('sha256').update(canonicalRequest, 'utf8').digest('hex')
99
- ].join('\n');
100
-
101
- // 生成签名
102
- const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(date).digest();
103
- const kRegion = crypto.createHmac('sha256', kDate).update(region).digest();
104
- const kService = crypto.createHmac('sha256', kRegion).update(service).digest();
105
- const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest();
106
- const signature = crypto.createHmac('sha256', kSigning).update(stringToSign, 'utf8').digest('hex');
107
-
108
- return `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
109
- }
110
-
111
- // 计算文件的CRC32值(从 images.ts 复制)
112
- function calculateCRC32(buffer: ArrayBuffer): string {
113
- const crcTable = [];
114
- for (let i = 0; i < 256; i++) {
115
- let crc = i;
116
- for (let j = 0; j < 8; j++) {
117
- crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
118
- }
119
- crcTable[i] = crc;
120
- }
121
-
122
- let crc = 0 ^ (-1);
123
- const bytes = new Uint8Array(buffer);
124
- for (let i = 0; i < bytes.length; i++) {
125
- crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
126
  }
127
- return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
128
- }
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, {
183
- method: 'GET',
184
- headers: {
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"',
193
- 'sec-fetch-dest': 'empty',
194
- 'sec-fetch-mode': 'cors',
195
- 'sec-fetch-site': 'cross-site',
196
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
197
- 'x-amz-date': timestamp,
198
- 'x-amz-security-token': session_token,
199
- },
200
- });
201
-
202
- if (!applyResponse.ok) {
203
- const errorText = await applyResponse.text();
204
- throw new Error(`申请上传权限失败: ${applyResponse.status} - ${errorText}`);
205
- }
206
-
207
- const applyResult = await applyResponse.json();
208
-
209
- if (applyResult?.ResponseMetadata?.Error) {
210
- throw new Error(`申请上传权限失败: ${JSON.stringify(applyResult.ResponseMetadata.Error)}`);
211
- }
212
-
213
- logger.info(`申请上传权限成功`);
214
-
215
- // 解析上传信息
216
- const uploadAddress = applyResult?.Result?.UploadAddress;
217
- if (!uploadAddress || !uploadAddress.StoreInfos || !uploadAddress.UploadHosts) {
218
- throw new Error(`获取上传地址失败: ${JSON.stringify(applyResult)}`);
219
- }
220
-
221
- const storeInfo = uploadAddress.StoreInfos[0];
222
- const uploadHost = uploadAddress.UploadHosts[0];
223
- const auth = storeInfo.Auth;
224
-
225
- const uploadUrl = `https://${uploadHost}/upload/v1/${storeInfo.StoreUri}`;
226
- const imageId = storeInfo.StoreUri.split('/').pop();
227
-
228
- logger.info(`准备上传图片: imageId=${imageId}, uploadUrl=${uploadUrl}`);
229
-
230
- // 第三步:上传图片文件
231
- const uploadResponse = await fetch(uploadUrl, {
232
- method: 'POST',
233
- headers: {
234
- 'Accept': '*/*',
235
- 'Accept-Language': 'zh-CN,zh;q=0.9',
236
- 'Authorization': auth,
237
- 'Connection': 'keep-alive',
238
- 'Content-CRC32': crc32,
239
- 'Content-Disposition': 'attachment; filename="undefined"',
240
- 'Content-Type': 'application/octet-stream',
241
- 'Origin': 'https://jimeng.jianying.com',
242
- 'Referer': 'https://jimeng.jianying.com/ai-tool/video/generate',
243
- 'Sec-Fetch-Dest': 'empty',
244
- 'Sec-Fetch-Mode': 'cors',
245
- 'Sec-Fetch-Site': 'cross-site',
246
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
247
- 'X-Storage-U': '704135154117550',
248
- },
249
- body: imageBuffer,
250
- });
251
-
252
- if (!uploadResponse.ok) {
253
- const errorText = await uploadResponse.text();
254
- throw new Error(`图片上传失败: ${uploadResponse.status} - ${errorText}`);
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: {
281
- 'accept': '*/*',
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"',
290
- 'sec-fetch-dest': 'empty',
291
- 'sec-fetch-mode': 'cors',
292
- 'sec-fetch-site': 'cross-site',
293
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
294
- 'x-amz-date': commitTimestamp,
295
- 'x-amz-security-token': session_token,
296
- 'x-amz-content-sha256': payloadHash,
297
- },
298
- body: commitPayload,
299
- });
300
-
301
- if (!commitResponse.ok) {
302
- const errorText = await commitResponse.text();
303
- throw new Error(`提交上传失败: ${commitResponse.status} - ${errorText}`);
304
- }
305
-
306
- const commitResult = await commitResponse.json();
307
-
308
- if (commitResult?.ResponseMetadata?.Error) {
309
- throw new Error(`提交上传失败: ${JSON.stringify(commitResult.ResponseMetadata.Error)}`);
310
- }
311
-
312
- if (!commitResult?.Result?.Results || commitResult.Result.Results.length === 0) {
313
- throw new Error(`提交上传响应缺少结果: ${JSON.stringify(commitResult)}`);
314
- }
315
-
316
- const uploadResult = commitResult.Result.Results[0];
317
- if (uploadResult.UriStatus !== 2000) {
318
- throw new Error(`图片上传状态异常: UriStatus=${uploadResult.UriStatus}`);
319
- }
320
-
321
- const fullImageUri = uploadResult.Uri;
322
-
323
- // 验证图片信息
324
- const pluginResult = commitResult.Result?.PluginResult?.[0];
325
- if (pluginResult && pluginResult.ImageUri) {
326
- logger.info(`视频图片上传完成: ${pluginResult.ImageUri}`);
327
- return pluginResult.ImageUri;
328
- }
329
-
330
- logger.info(`视频图片上传完成: ${fullImageUri}`);
331
- return fullImageUri;
332
  }
333
 
334
  // 处理来自URL的图片
335
- async function uploadImageFromUrl(imageUrl: string, refreshToken: string): Promise<string> {
336
  try {
337
  logger.info(`开始从URL下载并上传视频图片: ${imageUrl}`);
338
  const imageResponse = await fetch(imageUrl);
@@ -340,25 +38,13 @@ async function uploadImageFromUrl(imageUrl: string, refreshToken: string): Promi
340
  throw new Error(`下载图片失败: ${imageResponse.status}`);
341
  }
342
  const imageBuffer = await imageResponse.arrayBuffer();
343
- return await _uploadImageBuffer(imageBuffer, refreshToken);
344
- } catch (error) {
345
  logger.error(`从URL上传视频图片失败: ${error.message}`);
346
  throw error;
347
  }
348
  }
349
 
350
- // 处理本地上传的文件
351
- async function uploadImageFromFile(file: any, refreshToken: string): Promise<string> {
352
- try {
353
- logger.info(`开始从本地文件上传视频图片: ${file.originalFilename} (路径: ${file.filepath})`);
354
- const imageBuffer = await fs.readFile(file.filepath);
355
- return await _uploadImageBuffer(imageBuffer, refreshToken);
356
- } catch (error) {
357
- logger.error(`从本地文件上传视频图片失败: ${error.message}`);
358
- throw error;
359
- }
360
- }
361
-
362
 
363
  /**
364
  * 生成视频
@@ -388,13 +74,10 @@ export async function generateVideo(
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
 
@@ -422,14 +105,14 @@ export async function generateVideo(
422
  if (!file) continue;
423
  try {
424
  logger.info(`开始上传第 ${i + 1} 张本地图片: ${file.originalFilename}`);
425
- const imageUri = await uploadImageFromFile(file, refreshToken);
426
  if (imageUri) {
427
  uploadIDs.push(imageUri);
428
  logger.info(`第 ${i + 1} 张本地图片上传成功: ${imageUri}`);
429
  } else {
430
  logger.error(`第 ${i + 1} 张本地图片上传失败: 未获取到 image_uri`);
431
  }
432
- } catch (error) {
433
  logger.error(`第 ${i + 1} 张本地图片上传失败: ${error.message}`);
434
  if (i === 0) {
435
  throw new APIException(EX.API_REQUEST_FAILED, `首帧图片上传失败: ${error.message}`);
@@ -448,14 +131,14 @@ export async function generateVideo(
448
  }
449
  try {
450
  logger.info(`开始上传第 ${i + 1} 个URL图片: ${filePath}`);
451
- const imageUri = await uploadImageFromUrl(filePath, refreshToken);
452
  if (imageUri) {
453
  uploadIDs.push(imageUri);
454
  logger.info(`第 ${i + 1} 个URL图片上传成功: ${imageUri}`);
455
  } else {
456
  logger.error(`第 ${i + 1} 个URL图片上传失败: 未获取到 image_uri`);
457
  }
458
- } catch (error) {
459
  logger.error(`第 ${i + 1} 个URL图片上传失败: ${error.message}`);
460
  if (i === 0) {
461
  throw new APIException(EX.API_REQUEST_FAILED, `首帧图片上传失败: ${error.message}`);
@@ -614,9 +297,7 @@ export async function generateVideo(
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
  }
@@ -719,25 +400,12 @@ export async function generateVideo(
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}`);
729
- } else if (item_list?.[0]?.video?.download_url) {
730
- videoUrl = item_list[0].video.download_url;
731
- logger.info(`从download_url获取到视频URL: ${videoUrl}`);
732
- } else if (item_list?.[0]?.video?.url) {
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}秒`);
 
1
  import _ from "lodash";
 
2
  import fs from "fs-extra";
3
 
4
  import APIException from "@/lib/exceptions/APIException.ts";
5
  import EX from "@/api/consts/exceptions.ts";
6
  import util from "@/lib/util.ts";
7
+ import { getCredit, receiveCredit, request, parseRegionFromToken, getAssistantId, RegionInfo } 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_VIDEO_MODEL, DRAFT_VERSION, VIDEO_MODEL_MAP } from "@/api/consts/common.ts";
11
+ import { uploadImageBuffer } from "@/lib/image-uploader.ts";
12
+ import { extractVideoUrl } from "@/lib/image-utils.ts";
13
 
14
  export const DEFAULT_MODEL = DEFAULT_VIDEO_MODEL;
15
 
 
17
  return VIDEO_MODEL_MAP[model] || VIDEO_MODEL_MAP[DEFAULT_MODEL];
18
  }
19
 
20
+ // 处理本地上传的文件
21
+ async function uploadImageFromFile(file: any, refreshToken: string, regionInfo: RegionInfo): Promise<string> {
22
+ try {
23
+ logger.info(`开始从本地文件上传视频图片: ${file.originalFilename} (路径: ${file.filepath})`);
24
+ const imageBuffer = await fs.readFile(file.filepath);
25
+ return await uploadImageBuffer(imageBuffer, refreshToken, regionInfo);
26
+ } catch (error: any) {
27
+ logger.error(`从本地文件上传视频图片失败: ${error.message}`);
28
+ throw error;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
 
32
  // 处理来自URL的图片
33
+ async function uploadImageFromUrl(imageUrl: string, refreshToken: string, regionInfo: RegionInfo): Promise<string> {
34
  try {
35
  logger.info(`开始从URL下载并上传视频图片: ${imageUrl}`);
36
  const imageResponse = await fetch(imageUrl);
 
38
  throw new Error(`下载图片失败: ${imageResponse.status}`);
39
  }
40
  const imageBuffer = await imageResponse.arrayBuffer();
41
+ return await uploadImageBuffer(imageBuffer, refreshToken, regionInfo);
42
+ } catch (error: any) {
43
  logger.error(`从URL上传视频图片失败: ${error.message}`);
44
  throw error;
45
  }
46
  }
47
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  /**
50
  * 生成视频
 
74
  refreshToken: string
75
  ) {
76
  // 检测区域
77
+ const regionInfo = parseRegionFromToken(refreshToken);
78
+ const { isInternational } = regionInfo;
 
 
 
79
 
80
+ logger.info(`视频生成区域检测: isInternational=${isInternational}`);
81
 
82
  const model = getModel(_model);
83
 
 
105
  if (!file) continue;
106
  try {
107
  logger.info(`开始上传第 ${i + 1} 张本地图片: ${file.originalFilename}`);
108
+ const imageUri = await uploadImageFromFile(file, refreshToken, regionInfo);
109
  if (imageUri) {
110
  uploadIDs.push(imageUri);
111
  logger.info(`第 ${i + 1} 张本地图片上传成功: ${imageUri}`);
112
  } else {
113
  logger.error(`第 ${i + 1} 张本地图片上传失败: 未获取到 image_uri`);
114
  }
115
+ } catch (error: any) {
116
  logger.error(`第 ${i + 1} 张本地图片上传失败: ${error.message}`);
117
  if (i === 0) {
118
  throw new APIException(EX.API_REQUEST_FAILED, `首帧图片上传失败: ${error.message}`);
 
131
  }
132
  try {
133
  logger.info(`开始上传第 ${i + 1} 个URL图片: ${filePath}`);
134
+ const imageUri = await uploadImageFromUrl(filePath, refreshToken, regionInfo);
135
  if (imageUri) {
136
  uploadIDs.push(imageUri);
137
  logger.info(`第 ${i + 1} 个URL图片上传成功: ${imageUri}`);
138
  } else {
139
  logger.error(`第 ${i + 1} 个URL图片上传失败: 未获取到 image_uri`);
140
  }
141
+ } catch (error: any) {
142
  logger.error(`第 ${i + 1} 个URL图片上传失败: ${error.message}`);
143
  if (i === 0) {
144
  throw new APIException(EX.API_REQUEST_FAILED, `首帧图片上传失败: ${error.message}`);
 
297
  }],
298
  }),
299
  http_common_info: {
300
+ aid: getAssistantId(regionInfo)
 
 
301
  },
302
  },
303
  }
 
400
  const item_list = finalHistoryData.item_list || [];
401
 
402
  // 提取视频URL
403
+ let videoUrl = item_list?.[0] ? extractVideoUrl(item_list[0]) : null;
404
 
405
+ // 如果无法获取视频URL,抛出异常
406
  if (!videoUrl) {
407
+ logger.error(`未能获取视频URL,item_list: ${JSON.stringify(item_list)}`);
408
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "未能获取视频URL,请稍后查看");
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  }
410
 
411
  logger.info(`视频生成成功,URL: ${videoUrl},总耗时: ${pollingResult.elapsedTime}秒`);
src/lib/aws-signature.ts CHANGED
@@ -11,16 +11,16 @@ export function createSignature(
11
  accessKeyId: string,
12
  secretAccessKey: string,
13
  sessionToken?: string,
14
- payload: string = ''
 
15
  ) {
16
  const urlObj = new URL(url);
17
  const pathname = urlObj.pathname || '/';
18
  const search = urlObj.search;
19
-
20
  // 创建规范请求
21
  const timestamp = headers['x-amz-date'];
22
  const date = timestamp.substr(0, 8);
23
- const region = 'cn-north-1';
24
  const service = 'imagex';
25
 
26
  // 规范化查询参数
 
11
  accessKeyId: string,
12
  secretAccessKey: string,
13
  sessionToken?: string,
14
+ payload: string = '',
15
+ region: string = 'cn-north-1'
16
  ) {
17
  const urlObj = new URL(url);
18
  const pathname = urlObj.pathname || '/';
19
  const search = urlObj.search;
20
+
21
  // 创建规范请求
22
  const timestamp = headers['x-amz-date'];
23
  const date = timestamp.substr(0, 8);
 
24
  const service = 'imagex';
25
 
26
  // 规范化查询参数
src/lib/image-uploader.ts ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import crypto from "crypto";
2
+ import { RegionInfo, request } from "@/api/controllers/core.ts";
3
+ import { RegionUtils } from "@/lib/region-utils.ts";
4
+ import { createSignature } from "@/lib/aws-signature.ts";
5
+ import logger from "@/lib/logger.ts";
6
+ import util from "@/lib/util.ts";
7
+
8
+ /**
9
+ * 统一的图片上传模块
10
+ * 整合了images.ts和videos.ts中重复的上传逻辑
11
+ */
12
+
13
+ /**
14
+ * 上传图片Buffer到ImageX
15
+ * @param imageBuffer 图片数据
16
+ * @param refreshToken 刷新令牌
17
+ * @param regionInfo 区域信息
18
+ * @returns 图片URI
19
+ */
20
+ export async function uploadImageBuffer(
21
+ imageBuffer: ArrayBuffer | Buffer,
22
+ refreshToken: string,
23
+ regionInfo: RegionInfo
24
+ ): Promise<string> {
25
+ try {
26
+ logger.info(`开始上传图片Buffer... (isInternational: ${regionInfo.isInternational})`);
27
+
28
+ // 第一步:获取上传令牌
29
+ const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
30
+ data: {
31
+ scene: 2, // AIGC 图片上传场景
32
+ },
33
+ });
34
+
35
+ const { access_key_id, secret_access_key, session_token } = tokenResult;
36
+ const service_id = regionInfo.isInternational ? tokenResult.space_name : tokenResult.service_id;
37
+
38
+ if (!access_key_id || !secret_access_key || !session_token) {
39
+ throw new Error("获取上传令牌失败");
40
+ }
41
+
42
+ const actualServiceId = RegionUtils.getServiceId(regionInfo, service_id);
43
+ logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
44
+
45
+ // 准备文件信息
46
+ const fileSize = imageBuffer.byteLength;
47
+ const crc32 = util.calculateCRC32(imageBuffer);
48
+ logger.info(`图片Buffer: 大小=${fileSize}字节, CRC32=${crc32}`);
49
+
50
+ // 第二步:申请图片上传权限
51
+ const now = new Date();
52
+ const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
53
+ const randomStr = Math.random().toString(36).substring(2, 12);
54
+
55
+ const applyUrlHost = RegionUtils.getImageXUrl(regionInfo);
56
+ const applyUrl = `${applyUrlHost}/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}${regionInfo.isInternational ? '&device_platform=web' : ''}`;
57
+
58
+ const awsRegion = RegionUtils.getAWSRegion(regionInfo);
59
+ const origin = RegionUtils.getOrigin(regionInfo);
60
+
61
+ const requestHeaders = {
62
+ 'x-amz-date': timestamp,
63
+ 'x-amz-security-token': session_token
64
+ };
65
+
66
+ const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token, '', awsRegion);
67
+
68
+ logger.info(`申请上传权限: ${applyUrl}`);
69
+
70
+ let applyResponse;
71
+ try {
72
+ applyResponse = await fetch(applyUrl, {
73
+ method: 'GET',
74
+ headers: {
75
+ 'accept': '*/*',
76
+ 'accept-language': 'zh-CN,zh;q=0.9',
77
+ 'authorization': authorization,
78
+ 'origin': origin,
79
+ 'referer': `${origin}/ai-tool/generate`,
80
+ 'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
81
+ 'sec-ch-ua-mobile': '?0',
82
+ 'sec-ch-ua-platform': '"Windows"',
83
+ 'sec-fetch-dest': 'empty',
84
+ 'sec-fetch-mode': 'cors',
85
+ 'sec-fetch-site': 'cross-site',
86
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
87
+ 'x-amz-date': timestamp,
88
+ 'x-amz-security-token': session_token,
89
+ },
90
+ });
91
+ } catch (fetchError: any) {
92
+ logger.error(`Fetch请求失败,目标URL: ${applyUrl}`);
93
+ logger.error(`错误详情: ${fetchError.message}`);
94
+ throw new Error(`网络请求失败 (${applyUrlHost}): ${fetchError.message}. 请检查: 1) 网络连接是否正常 2) 是否需要配置代理 3) DNS是否能解析该域名`);
95
+ }
96
+
97
+ if (!applyResponse.ok) {
98
+ const errorText = await applyResponse.text();
99
+ throw new Error(`申请上传权限失败: ${applyResponse.status} - ${errorText}`);
100
+ }
101
+
102
+ const applyResult = await applyResponse.json();
103
+
104
+ if (applyResult?.ResponseMetadata?.Error) {
105
+ throw new Error(`申请上传权限失败: ${JSON.stringify(applyResult.ResponseMetadata.Error)}`);
106
+ }
107
+
108
+ logger.info(`申请上传权限成功`);
109
+
110
+ // 解析上传信息
111
+ const uploadAddress = applyResult?.Result?.UploadAddress;
112
+ if (!uploadAddress || !uploadAddress.StoreInfos || !uploadAddress.UploadHosts) {
113
+ throw new Error(`获取上传地址失败: ${JSON.stringify(applyResult)}`);
114
+ }
115
+
116
+ const storeInfo = uploadAddress.StoreInfos[0];
117
+ const uploadHost = uploadAddress.UploadHosts[0];
118
+ const auth = storeInfo.Auth;
119
+ const uploadUrl = `https://${uploadHost}/upload/v1/${storeInfo.StoreUri}`;
120
+
121
+ logger.info(`准备上传图片: uploadUrl=${uploadUrl}`);
122
+
123
+ // 第三步:上传图片文件
124
+ let uploadResponse;
125
+ try {
126
+ uploadResponse = await fetch(uploadUrl, {
127
+ method: 'POST',
128
+ headers: {
129
+ 'Accept': '*/*',
130
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
131
+ 'Authorization': auth,
132
+ 'Connection': 'keep-alive',
133
+ 'Content-CRC32': crc32,
134
+ 'Content-Disposition': 'attachment; filename="undefined"',
135
+ 'Content-Type': 'application/octet-stream',
136
+ 'Origin': origin,
137
+ 'Referer': RegionUtils.getRefererPath(regionInfo),
138
+ 'Sec-Fetch-Dest': 'empty',
139
+ 'Sec-Fetch-Mode': 'cors',
140
+ 'Sec-Fetch-Site': 'cross-site',
141
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
142
+ },
143
+ body: imageBuffer,
144
+ });
145
+ } catch (fetchError: any) {
146
+ logger.error(`图片文件上传fetch请求失败,目标URL: ${uploadUrl}`);
147
+ logger.error(`错误详情: ${fetchError.message}`);
148
+ throw new Error(`图片上传网络请求失败 (${uploadHost}): ${fetchError.message}. 请检查网络连接`);
149
+ }
150
+
151
+ if (!uploadResponse.ok) {
152
+ const errorText = await uploadResponse.text();
153
+ throw new Error(`图片上传失败: ${uploadResponse.status} - ${errorText}`);
154
+ }
155
+
156
+ logger.info(`图片文件上传成功`);
157
+
158
+ // 第四步:提交上传
159
+ const commitUrl = `${applyUrlHost}/?Action=CommitImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}`;
160
+ const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
161
+ const commitPayload = JSON.stringify({
162
+ SessionKey: uploadAddress.SessionKey
163
+ });
164
+
165
+ const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex');
166
+
167
+ const commitRequestHeaders = {
168
+ 'x-amz-date': commitTimestamp,
169
+ 'x-amz-security-token': session_token,
170
+ 'x-amz-content-sha256': payloadHash
171
+ };
172
+
173
+ const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload, awsRegion);
174
+
175
+ let commitResponse;
176
+ try {
177
+ commitResponse = await fetch(commitUrl, {
178
+ method: 'POST',
179
+ headers: {
180
+ 'accept': '*/*',
181
+ 'accept-language': 'zh-CN,zh;q=0.9',
182
+ 'authorization': commitAuthorization,
183
+ 'content-type': 'application/json',
184
+ 'origin': origin,
185
+ 'referer': RegionUtils.getRefererPath(regionInfo),
186
+ 'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
187
+ 'sec-ch-ua-mobile': '?0',
188
+ 'sec-ch-ua-platform': '"Windows"',
189
+ 'sec-fetch-dest': 'empty',
190
+ 'sec-fetch-mode': 'cors',
191
+ 'sec-fetch-site': 'cross-site',
192
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
193
+ 'x-amz-date': commitTimestamp,
194
+ 'x-amz-security-token': session_token,
195
+ 'x-amz-content-sha256': payloadHash,
196
+ },
197
+ body: commitPayload,
198
+ });
199
+ } catch (fetchError: any) {
200
+ logger.error(`提交上传fetch请求失败,目标URL: ${commitUrl}`);
201
+ logger.error(`错误详情: ${fetchError.message}`);
202
+ throw new Error(`提交上传网络请求失败 (${applyUrlHost}): ${fetchError.message}. 请检查网络连接`);
203
+ }
204
+
205
+ if (!commitResponse.ok) {
206
+ const errorText = await commitResponse.text();
207
+ throw new Error(`提交上传失败: ${commitResponse.status} - ${errorText}`);
208
+ }
209
+
210
+ const commitResult = await commitResponse.json();
211
+
212
+ if (commitResult?.ResponseMetadata?.Error) {
213
+ throw new Error(`提交上传失败: ${JSON.stringify(commitResult.ResponseMetadata.Error)}`);
214
+ }
215
+
216
+ if (!commitResult?.Result?.Results || commitResult.Result.Results.length === 0) {
217
+ throw new Error(`提交上传响应缺少结果: ${JSON.stringify(commitResult)}`);
218
+ }
219
+
220
+ const uploadResult = commitResult.Result.Results[0];
221
+ if (uploadResult.UriStatus !== 2000) {
222
+ throw new Error(`图片上传状态异常: UriStatus=${uploadResult.UriStatus}`);
223
+ }
224
+
225
+ const fullImageUri = uploadResult.Uri;
226
+ logger.info(`图片上传完成: ${fullImageUri}`);
227
+
228
+ return fullImageUri;
229
+ } catch (error: any) {
230
+ logger.error(`图片Buffer上传失败: ${error.message}`);
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * 从URL下载并上传图片
237
+ * @param imageUrl 图片URL
238
+ * @param refreshToken 刷新令牌
239
+ * @param regionInfo 区域信息
240
+ * @returns 图片URI
241
+ */
242
+ export async function uploadImageFromUrl(
243
+ imageUrl: string,
244
+ refreshToken: string,
245
+ regionInfo: RegionInfo
246
+ ): Promise<string> {
247
+ try {
248
+ logger.info(`开始从URL下载并上传图片: ${imageUrl}`);
249
+
250
+ const imageResponse = await fetch(imageUrl);
251
+ if (!imageResponse.ok) {
252
+ throw new Error(`下载图片失败: ${imageResponse.status}`);
253
+ }
254
+
255
+ const imageBuffer = await imageResponse.arrayBuffer();
256
+ return await uploadImageBuffer(imageBuffer, refreshToken, regionInfo);
257
+ } catch (error: any) {
258
+ logger.error(`从URL上传图片失败: ${error.message}`);
259
+ throw error;
260
+ }
261
+ }
src/lib/image-utils.ts ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logger from "@/lib/logger.ts";
2
+
3
+ /**
4
+ * 图片URL提取工具
5
+ * 统一从不同格式的响应中提取图片URL
6
+ */
7
+
8
+ /**
9
+ * 从API响应项中提取图片URL
10
+ * @param item API响应中的单个项目
11
+ * @param index 项目索引(用于日志)
12
+ * @returns 图片URL或null
13
+ */
14
+ export function extractImageUrl(item: any, index?: number): string | null {
15
+ const logPrefix = index !== undefined ? `图片 ${index + 1}` : '图片';
16
+
17
+ let imageUrl: string | null = null;
18
+
19
+ // 优先尝试 large_images
20
+ if (item?.image?.large_images?.[0]?.image_url) {
21
+ imageUrl = item.image.large_images[0].image_url;
22
+ logger.debug(`${logPrefix}: 使用 large_images URL`);
23
+ }
24
+ // 其次尝试 cover_url
25
+ else if (item?.common_attr?.cover_url) {
26
+ imageUrl = item.common_attr.cover_url;
27
+ logger.debug(`${logPrefix}: 使用 cover_url`);
28
+ }
29
+ // 再尝试 image_url
30
+ else if (item?.image_url) {
31
+ imageUrl = item.image_url;
32
+ logger.debug(`${logPrefix}: 使用 image_url`);
33
+ }
34
+ // 最后尝试 url
35
+ else if (item?.url) {
36
+ imageUrl = item.url;
37
+ logger.debug(`${logPrefix}: 使用 url`);
38
+ }
39
+ // 无法提取URL
40
+ else {
41
+ logger.warn(`${logPrefix}: 无法提取URL,item结构: ${JSON.stringify(item, null, 2)}`);
42
+ }
43
+
44
+ return imageUrl;
45
+ }
46
+
47
+ /**
48
+ * 从项目列表中批量提取图片URLs
49
+ * @param itemList 项目列表
50
+ * @returns 图片URL数组
51
+ */
52
+ export function extractImageUrls(itemList: any[]): string[] {
53
+ return itemList
54
+ .map((item, index) => extractImageUrl(item, index))
55
+ .filter((url): url is string => url !== null);
56
+ }
57
+
58
+ /**
59
+ * 从视频响应项中提取视频URL
60
+ * @param item 视频响应项
61
+ * @returns 视频URL或null
62
+ */
63
+ export function extractVideoUrl(item: any): string | null {
64
+ // 优先尝试 transcoded_video.origin.video_url
65
+ if (item?.video?.transcoded_video?.origin?.video_url) {
66
+ return item.video.transcoded_video.origin.video_url;
67
+ }
68
+ // 尝试 play_url
69
+ if (item?.video?.play_url) {
70
+ return item.video.play_url;
71
+ }
72
+ // 尝试 download_url
73
+ if (item?.video?.download_url) {
74
+ return item.video.download_url;
75
+ }
76
+ // 尝试 url
77
+ if (item?.video?.url) {
78
+ return item.video.url;
79
+ }
80
+
81
+ return null;
82
+ }
src/lib/region-utils.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RegionInfo } from "@/api/controllers/core.ts";
2
+ import { BASE_URL_DREAMINA_US, BASE_URL_DREAMINA_HK, BASE_URL_IMAGEX_US, BASE_URL_IMAGEX_HK } from "@/api/consts/dreamina.ts";
3
+
4
+ /**
5
+ * 区域配置工具类
6
+ * 统一管理不同区域的配置信息
7
+ */
8
+ export class RegionUtils {
9
+ /**
10
+ * 获取ServiceId
11
+ */
12
+ static getServiceId(regionInfo: RegionInfo, providedServiceId?: string): string {
13
+ if (providedServiceId) {
14
+ return providedServiceId;
15
+ }
16
+
17
+ // US/HK/JP/SG 使用相同的 service_id
18
+ if (regionInfo.isUS || regionInfo.isHK || regionInfo.isJP || regionInfo.isSG) {
19
+ return "wopfjsm1ax";
20
+ }
21
+
22
+ // CN 使用默认的 service_id
23
+ return "tb4s082cfz";
24
+ }
25
+
26
+ /**
27
+ * 获取ImageX URL
28
+ */
29
+ static getImageXUrl(regionInfo: RegionInfo): string {
30
+ if (regionInfo.isUS) {
31
+ return BASE_URL_IMAGEX_US;
32
+ }
33
+
34
+ if (regionInfo.isHK || regionInfo.isJP || regionInfo.isSG) {
35
+ return BASE_URL_IMAGEX_HK;
36
+ }
37
+
38
+ return 'https://imagex.bytedanceapi.com';
39
+ }
40
+
41
+ /**
42
+ * 获取Origin
43
+ */
44
+ static getOrigin(regionInfo: RegionInfo): string {
45
+ if (regionInfo.isUS) {
46
+ return new URL(BASE_URL_DREAMINA_US).origin;
47
+ }
48
+
49
+ if (regionInfo.isHK || regionInfo.isJP || regionInfo.isSG) {
50
+ return new URL(BASE_URL_DREAMINA_HK).origin;
51
+ }
52
+
53
+ return 'https://jimeng.jianying.com';
54
+ }
55
+
56
+ /**
57
+ * 获取AWS区域
58
+ */
59
+ static getAWSRegion(regionInfo: RegionInfo): string {
60
+ if (regionInfo.isUS) {
61
+ return 'us-east-1';
62
+ }
63
+
64
+ if (regionInfo.isHK || regionInfo.isJP || regionInfo.isSG) {
65
+ return 'ap-southeast-1';
66
+ }
67
+
68
+ return 'cn-north-1';
69
+ }
70
+
71
+ /**
72
+ * 获取Referer路径
73
+ */
74
+ static getRefererPath(regionInfo: RegionInfo, path: string = '/ai-tool/generate'): string {
75
+ const origin = this.getOrigin(regionInfo);
76
+ return `${origin}${path}`;
77
+ }
78
+ }
src/lib/smart-poller.ts CHANGED
@@ -122,8 +122,8 @@ export class SmartPoller {
122
  return { shouldExit: true, reason: '任务失败' };
123
  }
124
 
125
- // 3. 已获得期望数量的结果
126
- if (itemCount >= this.options.expectedItemCount) {
127
  return { shouldExit: true, reason: `已获得完整结果集(${itemCount}/${this.options.expectedItemCount})` };
128
  }
129
 
 
122
  return { shouldExit: true, reason: '任务失败' };
123
  }
124
 
125
+ // 3. 已获得期望数量的结果(但必须状态已完成)
126
+ if (itemCount >= this.options.expectedItemCount && (status === 10 || status === 50)) {
127
  return { shouldExit: true, reason: `已获得完整结果集(${itemCount}/${this.options.expectedItemCount})` };
128
  }
129
 
src/lib/util.ts CHANGED
@@ -302,6 +302,29 @@ const util = {
302
  });
303
  return result.data.toString("base64");
304
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  };
306
 
307
  export default util;
 
302
  });
303
  return result.data.toString("base64");
304
  },
305
+
306
+ /**
307
+ * 计算 ArrayBuffer 的 CRC32 值
308
+ * @param buffer ArrayBuffer 数据
309
+ * @returns CRC32 十六进制字符串
310
+ */
311
+ calculateCRC32(buffer: ArrayBuffer): string {
312
+ const crcTable = [];
313
+ for (let i = 0; i < 256; i++) {
314
+ let crc = i;
315
+ for (let j = 0; j < 8; j++) {
316
+ crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
317
+ }
318
+ crcTable[i] = crc;
319
+ }
320
+
321
+ let crc = 0 ^ (-1);
322
+ const bytes = new Uint8Array(buffer);
323
+ for (let i = 0; i < bytes.length; i++) {
324
+ crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
325
+ }
326
+ return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
327
+ },
328
  };
329
 
330
  export default util;
test-sg-network.cjs ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 国际站SG区域网络连接诊断脚本
5
+ * 用于测试能否访问新加坡ImageX服务
6
+ */
7
+
8
+ const dns = require('dns').promises;
9
+ const https = require('https');
10
+
11
+ const SG_DOMAINS = [
12
+ 'mweb-api-sg.capcut.com',
13
+ 'imagex16-normal-sg-ttp.capcutapi.sg'
14
+ ];
15
+
16
+ console.log('=== 国际站SG区域网络诊断 ===\n');
17
+
18
+ async function testDNS(domain) {
19
+ console.log(`[DNS测试] 正在解析: ${domain}`);
20
+ try {
21
+ const addresses = await dns.resolve4(domain);
22
+ console.log(`✓ DNS解析成功: ${domain} -> ${addresses.join(', ')}`);
23
+ return true;
24
+ } catch (error) {
25
+ console.error(`✗ DNS解析失败: ${domain}`);
26
+ console.error(` 错误: ${error.message}`);
27
+ return false;
28
+ }
29
+ }
30
+
31
+ async function testHTTPS(domain) {
32
+ console.log(`\n[HTTPS测试] 正在测试连接: https://${domain}`);
33
+ return new Promise((resolve) => {
34
+ const startTime = Date.now();
35
+ const req = https.get(`https://${domain}`, {
36
+ timeout: 10000,
37
+ headers: {
38
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
39
+ }
40
+ }, (res) => {
41
+ const duration = Date.now() - startTime;
42
+ console.log(`✓ HTTPS连接成功: ${domain}`);
43
+ console.log(` 状态码: ${res.statusCode}`);
44
+ console.log(` 响应时间: ${duration}ms`);
45
+ res.resume();
46
+ resolve(true);
47
+ });
48
+
49
+ req.on('error', (error) => {
50
+ const duration = Date.now() - startTime;
51
+ console.error(`✗ HTTPS连接失败: ${domain}`);
52
+ console.error(` 错误类型: ${error.code || error.constructor.name}`);
53
+ console.error(` 错误信息: ${error.message}`);
54
+ console.error(` 耗时: ${duration}ms`);
55
+
56
+ if (error.code === 'ENOTFOUND') {
57
+ console.error(` 建议: DNS无法解析该域名,请检查DNS设置或网络连接`);
58
+ } else if (error.code === 'ETIMEDOUT' || error.code === 'ECONNREFUSED') {
59
+ console.error(` 建议: 无法连接到服务器,可能需要配置代理或检查防火墙`);
60
+ } else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
61
+ console.error(` 建议: SSL证书问题,可能是系统时间不正确或证书链不完整`);
62
+ }
63
+
64
+ resolve(false);
65
+ });
66
+
67
+ req.on('timeout', () => {
68
+ req.destroy();
69
+ console.error(`✗ HTTPS连接超时: ${domain}`);
70
+ console.error(` 建议: 网络延迟过高或无法访问,请检查网络连接或配置代理`);
71
+ resolve(false);
72
+ });
73
+ });
74
+ }
75
+
76
+ async function testImageXAPI() {
77
+ console.log(`\n[API测试] 测试ImageX API端点`);
78
+ const testUrl = 'https://imagex16-normal-sg-ttp.capcutapi.sg/?Action=GetImageServiceSubscriptions&Version=2018-08-01';
79
+
80
+ return new Promise((resolve) => {
81
+ const startTime = Date.now();
82
+ https.get(testUrl, {
83
+ timeout: 10000,
84
+ headers: {
85
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
86
+ 'Accept': '*/*'
87
+ }
88
+ }, (res) => {
89
+ const duration = Date.now() - startTime;
90
+ console.log(`✓ ImageX API端点可访问`);
91
+ console.log(` 状态码: ${res.statusCode}`);
92
+ console.log(` 响应时间: ${duration}ms`);
93
+
94
+ let data = '';
95
+ res.on('data', chunk => data += chunk);
96
+ res.on('end', () => {
97
+ try {
98
+ const json = JSON.parse(data);
99
+ console.log(` 响应示例: ${data.substring(0, 200)}...`);
100
+ } catch (e) {
101
+ console.log(` 响应: ${data.substring(0, 200)}...`);
102
+ }
103
+ resolve(true);
104
+ });
105
+ }).on('error', (error) => {
106
+ console.error(`✗ ImageX API端点访问失败`);
107
+ console.error(` 错误: ${error.message}`);
108
+ resolve(false);
109
+ });
110
+ });
111
+ }
112
+
113
+ async function main() {
114
+ let allSuccess = true;
115
+
116
+ // 测试DNS解析
117
+ console.log('步骤 1: DNS解析测试');
118
+ console.log('─'.repeat(50));
119
+ for (const domain of SG_DOMAINS) {
120
+ const success = await testDNS(domain);
121
+ if (!success) allSuccess = false;
122
+ }
123
+
124
+ // 测试HTTPS连接
125
+ console.log('\n步骤 2: HTTPS连接测试');
126
+ console.log('─'.repeat(50));
127
+ for (const domain of SG_DOMAINS) {
128
+ const success = await testHTTPS(domain);
129
+ if (!success) allSuccess = false;
130
+ }
131
+
132
+ // 测试API端点
133
+ console.log('\n步骤 3: API端点测试');
134
+ console.log('─'.repeat(50));
135
+ const apiSuccess = await testImageXAPI();
136
+ if (!apiSuccess) allSuccess = false;
137
+
138
+ // 总结
139
+ console.log('\n' + '='.repeat(50));
140
+ if (allSuccess) {
141
+ console.log('✓ 所有测试通过!网络连接正常');
142
+ } else {
143
+ console.log('✗ 部分测试失败,请检查上述错误信息');
144
+ console.log('\n常见解决方案:');
145
+ console.log('1. 检查是否需要配置代理 (HTTP_PROXY/HTTPS_PROXY环境变量)');
146
+ console.log('2. 检查DNS设置是否正确');
147
+ console.log('3. 检查防火墙是否阻止了访问');
148
+ console.log('4. 尝试使用VPN连接');
149
+ console.log('5. 检查系��时间是否正确 (影响SSL证书验证)');
150
+ }
151
+ console.log('='.repeat(50));
152
+ }
153
+
154
+ main().catch(console.error);