@cloudbase/manager-node 5.5.6 → 5.5.7-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -192,7 +192,9 @@ function configToParams(options) {
192
192
  if ((_c = func === null || func === void 0 ? void 0 : func.layers) === null || _c === void 0 ? void 0 : _c.length) {
193
193
  params.Layers = func.layers.map(item => ({
194
194
  LayerName: item.name,
195
- LayerVersion: item.version
195
+ // item.version 可能是字符串(如 JSON 配置文件中的 "1" 或模板变量替换结果),
196
+ // SCF API 要求 LayerVersion 为 int64,需显式转为 number
197
+ LayerVersion: Number(item.version)
196
198
  }));
197
199
  }
198
200
  // 7. 代码保护
@@ -818,7 +820,9 @@ class FunctionService {
818
820
  if ((_c = func === null || func === void 0 ? void 0 : func.layers) === null || _c === void 0 ? void 0 : _c.length) {
819
821
  params.Layers = func.layers.map(item => ({
820
822
  LayerName: item.name,
821
- LayerVersion: item.version
823
+ // item.version 可能是字符串(如 JSON 配置文件中的 "1" 或模板变量替换结果),
824
+ // SCF API 要求 LayerVersion 为 int64,需显式转为 number
825
+ LayerVersion: Number(item.version)
822
826
  }));
823
827
  }
824
828
  // 特殊处理:HTTP 函数多并发配置(大小写不敏感)
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ /* eslint-disable @typescript-eslint/member-ordering */
2
3
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
4
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
5
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -241,27 +242,30 @@ class StorageService {
241
242
  getMetadataController.loadTasks(tasks);
242
243
  const files = await getMetadataController.run();
243
244
  // 对文件上传进行处理
244
- const cos = this.getCos(parallel);
245
- const uploadFiles = util_1.default.promisify(cos.uploadFiles).bind(cos);
246
- const params = {
247
- files,
248
- SliceSize: BIG_FILE_SIZE,
245
+ // 构造 files 数组(格式:{ localPath, cloudPath }),传给 uploadFilesCustom
246
+ const filesForUpload = fileStatsList
247
+ .filter(stats => !stats.isDir)
248
+ .map(stats => ({
249
+ localPath: stats.filePath,
250
+ cloudPath: stats.cloudFileKey
251
+ }));
252
+ // 调用 uploadFilesCustom(智能混合模式:并行→串行)
253
+ // 不再直接调用 uploadFilesWithRetry(纯并行重试,容易 socket hang up)
254
+ return this.uploadFilesCustom({
255
+ files: filesForUpload,
256
+ bucket,
257
+ region,
258
+ ignore: [],
249
259
  onProgress,
250
- onFileFinish
251
- };
252
- return this.uploadFilesWithRetry({
253
- uploadFiles,
254
- options: params,
255
- times: retryCount,
256
- interval: retryInterval,
257
- failedFiles: []
260
+ onFileFinish,
261
+ fileId,
262
+ parallel,
263
+ retryCount,
264
+ retryInterval
258
265
  });
259
266
  }
260
- /**
261
- * 批量上传文件
262
- * @param options
263
- */
264
267
  async uploadFilesCustom(options) {
268
+ var _a;
265
269
  const { files, bucket, region, ignore, onProgress, onFileFinish, fileId = true, parallel = 20, retryCount = 0, retryInterval = 500 } = options;
266
270
  if (!files || !files.length) {
267
271
  return;
@@ -275,6 +279,146 @@ class StorageService {
275
279
  };
276
280
  })
277
281
  .filter(item => ((ignore === null || ignore === void 0 ? void 0 : ignore.length) ? !micromatch_1.default.isMatch(item.filePath, ignore) : true));
282
+ const totalFiles = fileList.length;
283
+ console.log(`[uploadFiles] 准备上传 ${totalFiles} 个文件`);
284
+ // 模式 1:完全串行模式(最稳定)
285
+ if (process.env.COS_UPLOAD_SERIAL === 'true') {
286
+ console.log(`[uploadFiles] 串行安全模式已启用`);
287
+ const serialResult = await this.uploadFilesSerial({
288
+ fileList,
289
+ bucket,
290
+ region,
291
+ fileId,
292
+ retryCount,
293
+ retryInterval,
294
+ onFileFinish
295
+ });
296
+ if (serialResult.failedFiles.length > 0) {
297
+ throw new error_1.CloudBaseError(`[uploadFiles] 串行上传仍失败 (${serialResult.failedFiles.length} 个):\n` +
298
+ serialResult.failedFiles.map(f => `- ${f.cloudFileKey}: ${f.error}`).join('\n'), { code: 'HOSTING_DEPLOY_FAILED' });
299
+ }
300
+ return {
301
+ successCount: serialResult.successCount,
302
+ totalFiles,
303
+ failedFiles: []
304
+ };
305
+ }
306
+ // 模式 2:智能混合模式(默认)
307
+ // - 先并行快速上传
308
+ // - 失败的文件自动走串行重试
309
+ console.log(`[uploadFiles] 智能混合模式:先并行快速上传,失败文件自动串行重试`);
310
+ // 第一步:并行上传(分批)
311
+ const parallelFailedFiles = await this.uploadFilesParallel({
312
+ fileList,
313
+ bucket,
314
+ region,
315
+ fileId,
316
+ parallel,
317
+ retryCount: 0, // 并行阶段不重试,失败后交给串行阶段
318
+ retryInterval,
319
+ onFileFinish
320
+ });
321
+ // 第二步:串行重试失败的文件
322
+ if (parallelFailedFiles && parallelFailedFiles.length > 0) {
323
+ console.warn(`[uploadFiles] 并行阶段 ${parallelFailedFiles.length} 个文件失败,自动切换串行重试...`);
324
+ const serialResult = await this.uploadFilesSerial({
325
+ fileList: parallelFailedFiles,
326
+ bucket,
327
+ region,
328
+ fileId: false, // 串行重试不需要再获取 fileId
329
+ retryCount,
330
+ retryInterval,
331
+ onFileFinish
332
+ });
333
+ // 检查是否还有失败
334
+ if (((_a = serialResult === null || serialResult === void 0 ? void 0 : serialResult.failedFiles) === null || _a === void 0 ? void 0 : _a.length) > 0) {
335
+ throw new error_1.CloudBaseError(`[uploadFiles] 串行重试后仍失败 (${serialResult.failedFiles.length} 个):\n` +
336
+ serialResult.failedFiles.map(f => `- ${f.cloudFileKey}: ${f.error}`).join('\n'), { code: 'HOSTING_DEPLOY_FAILED' });
337
+ }
338
+ }
339
+ console.log(`[uploadFiles] 全部完成!共处理 ${totalFiles} 个文件`);
340
+ return {
341
+ successCount: totalFiles,
342
+ totalFiles,
343
+ failedFiles: []
344
+ };
345
+ }
346
+ /**
347
+ * 串行安全模式:逐文件上传(类似自定义脚本 deploy.js 的方式)
348
+ * 特点:
349
+ * - 每个文件独立 HTTP 连接,不会出现 socket hang up
350
+ * - 单线程串行,稳定性最高
351
+ * - 适合大规模目录(>1000 文件)或有网络问题的场景
352
+ */
353
+ // eslint-disable-next-line @typescript-eslint/member-ordering
354
+ async uploadFilesSerial(options) {
355
+ const { fileList, bucket, region, fileId, retryCount, retryInterval, onFileFinish } = options;
356
+ let uploadedCount = 0;
357
+ let failedFiles = [];
358
+ for (let i = 0; i < fileList.length; i++) {
359
+ const file = fileList[i];
360
+ let success = false;
361
+ // 重试机制
362
+ for (let attempt = 0; attempt <= retryCount; attempt++) {
363
+ try {
364
+ // 每个文件创建新的 COS 实例(确保连接隔离)
365
+ const cos = this.getCos(1); // 严格单并发
366
+ const putObject = util_1.default.promisify(cos.putObject).bind(cos);
367
+ await putObject({
368
+ Bucket: bucket,
369
+ Region: region,
370
+ Key: file.cloudFileKey,
371
+ Body: fs_1.default.createReadStream(file.filePath),
372
+ ContentLength: fs_1.default.statSync(file.filePath).size,
373
+ });
374
+ uploadedCount++;
375
+ // 显示进度(每 100 个文件或最后 5 个)
376
+ if (uploadedCount % 100 === 0 || fileList.length - i <= 5) {
377
+ console.log(`[serial-upload] [${uploadedCount}/${fileList.length}] ${file.cloudFileKey}`);
378
+ }
379
+ success = true;
380
+ // 回调成功
381
+ if (onFileFinish) {
382
+ onFileFinish(null, {}, file);
383
+ }
384
+ break;
385
+ }
386
+ catch (error) {
387
+ const isLastAttempt = attempt === retryCount;
388
+ if (isLastAttempt) {
389
+ console.error(`[serial-upload] [${i + 1}/${fileList.length}] ${file.cloudFileKey}: ${error.message || error.code}`);
390
+ // 记录失败的文件
391
+ failedFiles.push(Object.assign(Object.assign({}, file), { error: error.message || String(error) }));
392
+ // 回调失败
393
+ if (onFileFinish) {
394
+ onFileFinish(error, null, file);
395
+ }
396
+ }
397
+ else {
398
+ // 等待后重试
399
+ await new Promise(resolve => { setTimeout(resolve, retryInterval); });
400
+ }
401
+ }
402
+ }
403
+ }
404
+ console.log(`[serial-upload] 完成:成功 ${uploadedCount}/${fileList.length},失败 ${failedFiles.length}`);
405
+ return {
406
+ successCount: uploadedCount,
407
+ failedFiles
408
+ };
409
+ }
410
+ /**
411
+ * 并发模式:使用 cos.uploadFiles() 批量上传
412
+ * 特点:
413
+ * - 速度快,利用 SDK 内部并行能力
414
+ * - 分批上传 + 自动刷新凭证
415
+ * - 适合小规模目录或网络良好的场景
416
+ *
417
+ * @returns 失败的文件列表(空的表示全部成功)
418
+ */
419
+ // eslint-disable-next-line @typescript-eslint/member-ordering
420
+ async uploadFilesParallel(options) {
421
+ const { fileList, bucket, region, fileId, parallel, retryCount, retryInterval, onProgress, onFileFinish } = options;
278
422
  // 生成上传文件属性
279
423
  const tasks = fileList.map(stats => async () => {
280
424
  let cosFileId;
@@ -290,31 +434,89 @@ class StorageService {
290
434
  'x-cos-meta-fileid': cosFileId
291
435
  };
292
436
  });
293
- // 控制请求并发
437
+ // 控制请求并发(获取 metadata 阶段)
294
438
  const asyncTaskController = new parallel_1.AsyncTaskParallelController(parallel, 50);
295
439
  asyncTaskController.loadTasks(tasks);
296
- fileList = await asyncTaskController.run();
297
- const cos = this.getCos(parallel);
298
- const uploadFiles = util_1.default.promisify(cos.uploadFiles).bind(cos);
299
- const params = {
300
- files: fileList,
301
- SliceSize: BIG_FILE_SIZE,
302
- onProgress,
303
- onFileFinish
304
- };
305
- // return uploadFiles({
306
- // onProgress,
307
- // onFileFinish,
308
- // files: fileList,
309
- // SliceSize: BIG_FILE_SIZE
310
- // })
311
- return this.uploadFilesWithRetry({
312
- uploadFiles,
313
- options: params,
314
- times: retryCount,
315
- interval: retryInterval,
316
- failedFiles: []
317
- });
440
+ const processedFileList = await asyncTaskController.run();
441
+ // 分批上传,避免单次上传时间过长导致 STS 临时凭证过期
442
+ // 默认每批 500 个文件,可通过环境变量 COS_UPLOAD_BATCH_SIZE 自定义
443
+ const BATCH_SIZE = process.env.COS_UPLOAD_BATCH_SIZE
444
+ ? parseInt(process.env.COS_UPLOAD_BATCH_SIZE, 10)
445
+ : 500;
446
+ // 收集所有批次失败的文件
447
+ let allFailedFiles = [];
448
+ for (let batchStart = 0; batchStart < processedFileList.length; batchStart += BATCH_SIZE) {
449
+ const batchEnd = Math.min(batchStart + BATCH_SIZE, processedFileList.length);
450
+ const batchFiles = processedFileList.slice(batchStart, batchEnd);
451
+ console.log(`[parallel-upload] 正在上传第 ${Math.floor(batchStart / BATCH_SIZE) + 1} 批次 (文件 ${batchStart + 1}-${batchEnd}/${processedFileList.length})`);
452
+ // 每批次重新获取 COS 实例(刷新 STS 临时凭证)
453
+ const cos = this.getCos(parallel);
454
+ const uploadFiles = util_1.default.promisify(cos.uploadFiles).bind(cos);
455
+ // 跟踪本批次中真正失败的文件(通过 onFileFinish 回调)
456
+ const batchFailedFiles = [];
457
+ const batchSuccessFiles = new Set();
458
+ const params = {
459
+ files: batchFiles,
460
+ SliceSize: BIG_FILE_SIZE,
461
+ onProgress,
462
+ onFileFinish: (error, result, file) => {
463
+ // 空值检查:防止 file 为 null/undefined
464
+ if (!file) {
465
+ console.warn('[parallel-upload] onFileFinish received null/undefined file');
466
+ return;
467
+ }
468
+ const fileKey = file.Key || file.cloudFileKey;
469
+ if (error) {
470
+ batchFailedFiles.push({
471
+ filePath: file.FilePath || file.filePath,
472
+ cloudFileKey: fileKey,
473
+ error: error.message || String(error)
474
+ });
475
+ }
476
+ else {
477
+ batchSuccessFiles.add(fileKey);
478
+ }
479
+ // 继续调用用户传入的回调(转换参数格式)
480
+ // 注意:cloudFileKey 和 filePath 是 CLI 期望的格式
481
+ onFileFinish === null || onFileFinish === void 0 ? void 0 : onFileFinish(error, result, Object.assign(Object.assign({}, file), { cloudFileKey: fileKey, filePath: file.FilePath || file.filePath }));
482
+ }
483
+ };
484
+ try {
485
+ await this.uploadFilesWithRetry({
486
+ uploadFiles,
487
+ options: params,
488
+ times: retryCount,
489
+ interval: retryInterval,
490
+ failedFiles: []
491
+ });
492
+ console.log(`[parallel-upload] 第 ${Math.floor(batchStart / BATCH_SIZE) + 1} 批次上传成功`);
493
+ }
494
+ catch (error) {
495
+ console.error(`[parallel-upload] 第 ${Math.floor(batchStart / BATCH_SIZE) + 1} 批次上传失败:`, error.message || error.code || error);
496
+ // 只记录真正失败的文件(通过 onFileFinish 回调跟踪)
497
+ // 如果 onFileFinish 没有被调用(如 socket hang up),则标记整个批次为失败
498
+ if (batchFailedFiles.length === 0 && batchSuccessFiles.size === 0) {
499
+ // onFileFinish 未被调用,整个批次失败
500
+ allFailedFiles.push(...batchFiles.map(f => ({
501
+ filePath: f.FilePath || f.filePath,
502
+ cloudFileKey: f.Key || f.cloudFileKey,
503
+ error: error.message || String(error)
504
+ })));
505
+ }
506
+ else {
507
+ // onFileFinish 已被调用,只记录失败的文件
508
+ allFailedFiles.push(...batchFailedFiles);
509
+ }
510
+ }
511
+ }
512
+ // 返回失败文件列表(不抛异常,让调用方决定是否走串行重试)
513
+ if (allFailedFiles.length > 0) {
514
+ console.warn(`[parallel-upload] 共 ${allFailedFiles.length}/${processedFileList.length} 个文件并行上传失败,等待串行重试...`);
515
+ }
516
+ else {
517
+ console.log(`[parallel-upload] 全部 ${processedFileList.length} 个文件并行上传成功`);
518
+ }
519
+ return allFailedFiles;
318
520
  }
319
521
  /**
320
522
  * 创建一个空的文件夹
@@ -2506,6 +2708,7 @@ class StorageService {
2506
2708
  }
2507
2709
  /**
2508
2710
  * 获取 COS 配置
2711
+ * @param parallel 并发数量
2509
2712
  */
2510
2713
  getCos(parallel = 20) {
2511
2714
  const internalEndpoint = this.environment.cloudBaseContext.isInternalEndpoint();
@@ -2751,6 +2954,16 @@ __decorate([
2751
2954
  ], StorageService.prototype, "uploadDirectoryCustom", null);
2752
2955
  __decorate([
2753
2956
  (0, utils_1.preLazy)()
2957
+ /**
2958
+ * 上传文件(支持精准重试失败文件)
2959
+ *
2960
+ * 智能混合模式(默认):
2961
+ * 1. 先并行快速上传(速度快)
2962
+ * 2. 失败的文件自动走串行重试(稳定性高)
2963
+ *
2964
+ * 可选模式:
2965
+ * - COS_UPLOAD_SERIAL=true:完全串行模式(最稳定但慢)
2966
+ */
2754
2967
  ], StorageService.prototype, "uploadFilesCustom", null);
2755
2968
  __decorate([
2756
2969
  (0, utils_1.preLazy)()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/manager-node",
3
- "version": "5.5.6",
3
+ "version": "5.5.7-beta.1",
4
4
  "description": "The node manage service api for cloudbase.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -42,9 +42,17 @@ function getLastTag() {
42
42
 
43
43
  const currentTag = `v${VERSION}`
44
44
  for (const tag of tags) {
45
- // 跳过当前版本,找到第一个有效的 semver tag
46
- if (tag !== currentTag && /^v?\d+\.\d+\.\d+/.test(tag)) {
47
- return tag
45
+ // 跳过当前版本,找到第一个有效的 semver tag(排除 pre-release,如 v3.5.8-beta.0)
46
+ // 正则必须以数字结尾(不含 "-"),确保只匹配正式版本 tag
47
+ if (tag !== currentTag && /^v\d+\.\d+\.\d+$/.test(tag)) {
48
+ // 确保该 tag 的 commit 是当前分支的祖先(排除 release 分支等未合并到当前分支的 tag)
49
+ try {
50
+ execSync(`git merge-base --is-ancestor ${tag} HEAD`, { encoding: 'utf8' })
51
+ return tag
52
+ } catch {
53
+ // tag 不是当前分支的祖先,跳过(如 release 分支的 tag 未合并到 master)
54
+ continue
55
+ }
48
56
  }
49
57
  }
50
58
  return ''
@@ -141,6 +149,9 @@ function shouldFilterEarly(subject, branch) {
141
149
  // 3. 分支名就是纯版本号的,如 "2.9.4", "v3.0.1"
142
150
  if (/^v?\d+\.\d+(\.\d+)?(-\w+)?$/.test(branch)) return true
143
151
 
152
+ // 4. release 分支的 MR 是发版自动化,不包含用户可见变更
153
+ if (/^release\//.test(branch)) return true
154
+
144
155
  return false
145
156
  }
146
157
 
@@ -191,17 +202,15 @@ function parseMR(mergeCommit) {
191
202
  // 早期过滤:基于 subject 和分支名
192
203
  if (shouldFilterEarly(subject, branch)) return null
193
204
 
194
- // 优先从 body 中找 conventional commit 描述
205
+ // body 解析描述和类型
195
206
  let type = 'other'
196
207
  let scope = ''
197
208
  let description = ''
198
209
  let breaking = false
199
210
 
200
- // body 可能有多行,逐行找第一个有意义的 conventional commit 格式
211
+ // 先尝试从 body 中找 conventional commit
201
212
  const bodyLines = body.split('\n').map((l) => l.trim()).filter(Boolean)
202
213
  for (const line of bodyLines) {
203
- // 跳过分支名本身的重复
204
- if (line === branch) continue
205
214
  const parsed = parseConventional(line)
206
215
  if (parsed) {
207
216
  type = parsed.type
@@ -210,28 +219,28 @@ function parseMR(mergeCommit) {
210
219
  breaking = parsed.breaking
211
220
  break
212
221
  }
213
- // 如果不是 conventional 格式但也不是分支名,用第一个有意义的行作为描述
214
- if (!description && line.length > 3) {
215
- description = line
222
+ }
223
+
224
+ // 如果 conventional commit 的描述无效(如 "update v"),回退到分支名生成描述
225
+ const isGenericDescription = /^(update|udpate)\s*v?|^bump\s*(version|v)?|^\s*$|^v?\d+\.\d+(\.\d+)?$/i.test(description)
226
+ if (isGenericDescription) {
227
+ // conventional commit 描述是无效的发版消息,使用分支名生成描述
228
+ description = branch ? getDescriptionFromBranch(branch) : ''
229
+ // 如果分支名也无法提供有效描述,用 subject
230
+ if (!description) {
231
+ description = subject.replace(/\(merge request !\d+\)$/, '').trim()
216
232
  }
217
233
  }
218
234
 
219
- // 如果 body 没有提供有效信息,从分支名推断
235
+ // 如果 body 中没有 conventional commit,从分支名推断类型
220
236
  if (type === 'other' && branch) {
221
237
  const inferredType = inferTypeFromBranch(branch)
222
238
  if (inferredType) type = inferredType
223
239
  }
224
240
 
225
- if (!description && branch) {
226
- description = getDescriptionFromBranch(branch)
227
- }
228
-
229
- if (!description) {
230
- description = subject
231
- }
232
-
233
241
  // 后期过滤:基于解析后的描述内容(过滤 "update v" 等发版 MR)
234
- if (shouldFilterByDescription(description, body)) return null
242
+ // 但如果是 conventional commit 格式(type 不为 'other'),说明是有效 MR,不过滤
243
+ if (type === 'other' && shouldFilterByDescription(description, body)) return null
235
244
 
236
245
  // 更智能的分类:描述内容优先于分支名推断
237
246
  // 例如 feature/ 分支但描述是"文档内容"的,应归类为 docs
@@ -317,7 +326,8 @@ function generateChangelog(categories) {
317
326
 
318
327
  // 如果完全没有 MR
319
328
  if (!hasContent) {
320
- lines.push('- 版本发布')
329
+ // 不生成默认条目,保持 changelog 干净
330
+ lines.push('')
321
331
  }
322
332
 
323
333
  lines.push('')
@@ -195,11 +195,25 @@ export declare class StorageService {
195
195
  * @returns {Promise<void>}
196
196
  */
197
197
  uploadDirectoryCustom(options: IFileOptions & ICustomOptions): Promise<void>;
198
+ uploadFilesCustom(options: IFilesOptions & ICustomOptions): Promise<any>;
198
199
  /**
199
- * 批量上传文件
200
- * @param options
200
+ * 串行安全模式:逐文件上传(类似自定义脚本 deploy.js 的方式)
201
+ * 特点:
202
+ * - 每个文件独立 HTTP 连接,不会出现 socket hang up
203
+ * - 单线程串行,稳定性最高
204
+ * - 适合大规模目录(>1000 文件)或有网络问题的场景
201
205
  */
202
- uploadFilesCustom(options: IFilesOptions & ICustomOptions): Promise<any>;
206
+ private uploadFilesSerial;
207
+ /**
208
+ * 并发模式:使用 cos.uploadFiles() 批量上传
209
+ * 特点:
210
+ * - 速度快,利用 SDK 内部并行能力
211
+ * - 分批上传 + 自动刷新凭证
212
+ * - 适合小规模目录或网络良好的场景
213
+ *
214
+ * @returns 失败的文件列表(空的表示全部成功)
215
+ */
216
+ private uploadFilesParallel;
203
217
  /**
204
218
  * 创建一个空的文件夹
205
219
  * @param {string} cloudPath
@@ -531,6 +545,7 @@ export declare class StorageService {
531
545
  getObjectInfoPublic(options: IGetObjectInfoPublicHttpOptions): Promise<IGetObjectInfoHttpResult>;
532
546
  /**
533
547
  * 获取 COS 配置
548
+ * @param parallel 并发数量
534
549
  */
535
550
  private getCos;
536
551
  /**