@cloudbase/manager-node 5.8.9-beta.1 → 5.9.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.
- package/.codebuddy/rules/error-code-contract.mdc +13 -0
- package/.coding-ci-publish-beta.yaml +19 -12
- package/CHANGELOG.md +8 -0
- package/changelog_content_en.md +16 -0
- package/changelog_content_zh.md +16 -0
- package/lib/agent/index.js +6 -5
- package/lib/aiModel/index.js +21 -9
- package/lib/cloudApp/index.js +4 -4
- package/lib/cloudrun/index.js +16 -15
- package/lib/common/index.js +1 -1
- package/lib/database/index.js +23 -23
- package/lib/deploy/DatabaseDeployer.js +19 -24
- package/lib/deploy/DeployOrchestrator.js +2 -2
- package/lib/deploy/FunctionDeployer.js +19 -1
- package/lib/deploy/GatewayDeployer.js +5 -5
- package/lib/deploy/StaticDeployer.js +2 -2
- package/lib/deploy/framework.js +2 -1
- package/lib/deploy/function/config-guard.js +23 -3
- package/lib/deploy/function/enterprise-tcr.js +18 -2
- package/lib/docs/index.js +4 -3
- package/lib/env/index.js +16 -16
- package/lib/environment.js +4 -4
- package/lib/environmentManager.js +5 -1
- package/lib/error-docs.js +385 -0
- package/lib/error-types.js +21 -0
- package/lib/error.js +239 -2
- package/lib/function/index.js +454 -90
- package/lib/function/packer.js +1 -1
- package/lib/hosting/index.js +1 -1
- package/lib/index.js +2 -1
- package/lib/integration/crypto.js +4 -3
- package/lib/integration/index.js +1 -1
- package/lib/integration/secretKey.js +4 -4
- package/lib/monitor/index.js +1 -1
- package/lib/mysql/index.js +2 -1
- package/lib/permission/index.js +56 -49
- package/lib/platform/index.js +2 -2
- package/lib/sandbox/index.js +68 -61
- package/lib/storage/index.js +164 -162
- package/lib/user/index.js +32 -31
- package/lib/utils/cloud-api-request.js +13 -5
- package/lib/utils/cloudbase-request.js +21 -0
- package/lib/utils/fs.js +3 -3
- package/lib/utils/http-request.js +2 -2
- package/lib/utils/index.js +4 -3
- package/lib/utils/storage-config.js +3 -3
- package/package.json +2 -2
- package/types/error-docs.d.ts +57 -0
- package/types/error-types.d.ts +32 -0
- package/types/error.d.ts +148 -2
- package/types/function/index.d.ts +166 -8
- package/types/utils/cloudbase-request.d.ts +22 -0
package/lib/function/index.js
CHANGED
|
@@ -10,14 +10,24 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.FunctionService = exports.ENTERPRISE_IMAGE_REGION_GUIDANCE = void 0;
|
|
13
|
+
exports.formatImageDeploymentErrorMessage = formatImageDeploymentErrorMessage;
|
|
13
14
|
const fs_1 = __importDefault(require("fs"));
|
|
14
15
|
const path_1 = __importDefault(require("path"));
|
|
15
16
|
const lodash_1 = __importDefault(require("lodash"));
|
|
16
17
|
const cos_nodejs_sdk_v5_1 = __importDefault(require("cos-nodejs-sdk-v5"));
|
|
17
18
|
const packer_1 = require("./packer");
|
|
18
19
|
const error_1 = require("../error");
|
|
20
|
+
const error_types_1 = require("../error-types");
|
|
19
21
|
const utils_1 = require("../utils");
|
|
20
22
|
const constant_1 = require("../constant");
|
|
23
|
+
/**
|
|
24
|
+
* CBF(云开发云函数,`Functions[0].Backend === 'cbf'`)环境支持的运行环境。
|
|
25
|
+
*
|
|
26
|
+
* 服务端目前仅提供 Node.js 24.11;SDK 在 CBF 环境下也以它作为 `createFunction` 的默认 runtime。
|
|
27
|
+
*
|
|
28
|
+
* 仅供 SDK 内部使用:CBF 的字段支持范围仍在联调中,暂不作为公开 API 导出。
|
|
29
|
+
*/
|
|
30
|
+
const CBF_ALLOWED_RUNTIMES = ['Nodejs24.11'];
|
|
21
31
|
/**
|
|
22
32
|
* 解析环境变量为布尔值
|
|
23
33
|
* 视 'true'/'1'/'yes'/'on'(大小写不敏感)为 true,
|
|
@@ -115,28 +125,51 @@ function getFieldIgnoreCase(obj, fieldName) {
|
|
|
115
125
|
}
|
|
116
126
|
/** 企业版 TCR 镜像对 SCF 不可见时的排查指引 */
|
|
117
127
|
exports.ENTERPRISE_IMAGE_REGION_GUIDANCE = '请确认 TCR 企业版实例已在当前函数地域提供可访问的实例或复制实例,并检查 RegistryId、镜像地址及拉取权限。';
|
|
128
|
+
/**
|
|
129
|
+
* 幂等缓存:错误对象 → 已增强的文本。
|
|
130
|
+
*
|
|
131
|
+
* 为什么不用「给错误对象挂标记位 / 写回 message」:
|
|
132
|
+
* 1) **不改入参** ⇒ 无副作用,不污染调用方传入的数据源;
|
|
133
|
+
* 2) 冻结 / 只读 / 不可扩展对象同样有效(此前要靠 try/catch 兜底);
|
|
134
|
+
* 3) WeakMap 为弱引用 ⇒ 不阻碍错误对象被回收。
|
|
135
|
+
*/
|
|
136
|
+
const ENHANCED_MESSAGES = new WeakMap();
|
|
137
|
+
/** 取出可安全取属性的错误对象(null / 原始值 ⇒ undefined) */
|
|
138
|
+
function errorCarrierOf(error) {
|
|
139
|
+
return typeof error === 'object' && error !== null ? error : undefined;
|
|
140
|
+
}
|
|
118
141
|
/**
|
|
119
142
|
* 为企业版镜像的典型不可见错误补充排查指引,同时保留原始错误和 requestId。
|
|
120
143
|
* 仅匹配 SCF 已进入 enterprise 镜像查询阶段的错误,避免给普通网络或鉴权错误误导提示。
|
|
144
|
+
*
|
|
145
|
+
* 纯函数:只读入参,**不修改**传进来的错误对象;幂等由 WeakMap 缓存保证。
|
|
121
146
|
*/
|
|
122
147
|
function formatImageDeploymentErrorMessage(error, imageConfig) {
|
|
123
|
-
const
|
|
124
|
-
const
|
|
148
|
+
const carrier = errorCarrierOf(error);
|
|
149
|
+
const message = typeof (carrier === null || carrier === void 0 ? void 0 : carrier.message) === 'string' ? carrier.message : String(error);
|
|
150
|
+
const code = typeof (carrier === null || carrier === void 0 ? void 0 : carrier.code) === 'string' ? carrier.code : '';
|
|
125
151
|
const haystack = `${code} ${message}`.toLowerCase();
|
|
126
152
|
const isEnterprise = (imageConfig === null || imageConfig === void 0 ? void 0 : imageConfig.imageType) === 'enterprise';
|
|
127
153
|
const isEnterpriseImageNotFound = haystack.includes('resourcenotfound.image') ||
|
|
128
154
|
haystack.includes('describe images enterprise resource not found');
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
// 幂等只看缓存,不看文本(文本被中间层改动也不影响判定)
|
|
156
|
+
const applied = carrier ? ENHANCED_MESSAGES.get(carrier) : undefined;
|
|
157
|
+
if (applied !== undefined) {
|
|
158
|
+
return applied;
|
|
159
|
+
}
|
|
160
|
+
if (!isEnterprise || !isEnterpriseImageNotFound) {
|
|
132
161
|
return message;
|
|
133
162
|
}
|
|
134
|
-
|
|
163
|
+
const enhanced = `${message}\n提示:${exports.ENTERPRISE_IMAGE_REGION_GUIDANCE}`;
|
|
164
|
+
if (carrier) {
|
|
165
|
+
ENHANCED_MESSAGES.set(carrier, enhanced);
|
|
166
|
+
}
|
|
167
|
+
return enhanced;
|
|
135
168
|
}
|
|
136
169
|
// 解析函数配置,换成请求参数(用于 CreateFunction)
|
|
137
170
|
function configToParams(options) {
|
|
138
171
|
var _a, _b, _c;
|
|
139
|
-
const { func, codeSecret, baseParams = {} } = options;
|
|
172
|
+
const { func, codeSecret, baseParams = {}, isCbf = false } = options;
|
|
140
173
|
// 白名单:只有这些字段会被透传到 API(大小写不敏感)
|
|
141
174
|
// 参考 SCF API 文档:https://cloud.tencent.com/document/product/583/18586
|
|
142
175
|
// key: 小写用于匹配,value: PascalCase 用于输出
|
|
@@ -177,10 +210,12 @@ function configToParams(options) {
|
|
|
177
210
|
params.InstallDependency = toBooleanString(func.installDependency);
|
|
178
211
|
}
|
|
179
212
|
// 2. L5 配置 - 当不存在时不修改,否则根据 true/false 进行修改
|
|
213
|
+
// CBF 不接受 SDK 注入的兜底字段:未显式配置时不提交;
|
|
214
|
+
// 用户显式配置的仍原样下发,由服务端校验并透传报错
|
|
180
215
|
if (func.l5 !== undefined) {
|
|
181
216
|
params.L5Enable = toBooleanString(func.l5);
|
|
182
217
|
}
|
|
183
|
-
else {
|
|
218
|
+
else if (!isCbf) {
|
|
184
219
|
params.L5Enable = null;
|
|
185
220
|
}
|
|
186
221
|
// 3. 需要 'TRUE'/'FALSE' 字符串的布尔字段统一处理(大小写不敏感)
|
|
@@ -267,6 +302,63 @@ function configToParams(options) {
|
|
|
267
302
|
const runtime = params.Runtime || 'Nodejs20.19';
|
|
268
303
|
return Object.assign({ Handler: func.handler || 'index.main', Runtime: 'Nodejs20.19', MemorySize: 256, InstallDependency: isNodeFunction(runtime) ? 'TRUE' : 'FALSE' }, params);
|
|
269
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* 判断错误是否**可重试**(AIP-194:只重试 `UNAVAILABLE` 类瞬时错误)。
|
|
307
|
+
*
|
|
308
|
+
* ⚠️ 默认**不可重试**(default-deny):权限不足、参数非法、资源冲突等**确定性失败**立即抛出 ——
|
|
309
|
+
* 对它们重试只会浪费时间,更会把「必然失败」伪装成「偶发失败」,掩盖真实原因。
|
|
310
|
+
*/
|
|
311
|
+
/** 安全取字符串字段(不依赖 `as any` 断言 —— 代码评审:类型安全) */
|
|
312
|
+
function pickStringField(value, key) {
|
|
313
|
+
if (typeof value !== 'object' || value === null) {
|
|
314
|
+
return '';
|
|
315
|
+
}
|
|
316
|
+
const field = value[key];
|
|
317
|
+
return typeof field === 'string' ? field : '';
|
|
318
|
+
}
|
|
319
|
+
/** 可重试的**精确错误码**(小写比较;AIP-194 的 UNAVAILABLE 家族 + 本层网络 errno) */
|
|
320
|
+
const RETRYABLE_ERROR_CODES = new Set([
|
|
321
|
+
'resourceunavailable',
|
|
322
|
+
'serviceunavailable',
|
|
323
|
+
'internalerror',
|
|
324
|
+
'requestlimitexceeded',
|
|
325
|
+
'etimedout',
|
|
326
|
+
'econnreset',
|
|
327
|
+
'econnrefused'
|
|
328
|
+
]);
|
|
329
|
+
/** 仅在 ① 未命中时用于**文案**回退的瞬时信号(不含 `not found` 等语义模糊项) */
|
|
330
|
+
const RETRYABLE_ERROR_TEXT_HINTS = [
|
|
331
|
+
'unavailable',
|
|
332
|
+
'internal error',
|
|
333
|
+
'timeout',
|
|
334
|
+
'connection reset',
|
|
335
|
+
'network error'
|
|
336
|
+
];
|
|
337
|
+
function isRetryableError(e) {
|
|
338
|
+
const code = pickStringField(e, 'code').toLowerCase();
|
|
339
|
+
const message = pickStringField(e, 'message').toLowerCase();
|
|
340
|
+
// ① 精确码优先(含 `ResourceUnavailable.xxx` 这类带子码的形态)
|
|
341
|
+
if (RETRYABLE_ERROR_CODES.has(code) || RETRYABLE_ERROR_CODES.has(code.split('.').pop() || '')) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
// ② 退一步只看瞬时信号文案
|
|
345
|
+
return RETRYABLE_ERROR_TEXT_HINTS.some(hint => message.includes(hint));
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* 判断错误是否为**创建/更新期的「资源尚不可见」**(NOT_FOUND 家族)。
|
|
349
|
+
*
|
|
350
|
+
* 场景:`CreateFunction` 已提交成功,但 `GetFunction` 短时间内仍返回 NOT_FOUND。
|
|
351
|
+
* 这类错误应**容忍并继续轮询**,而不是把正常的「创建中」误报为失败。
|
|
352
|
+
*
|
|
353
|
+
* ⚠️ 与 `isRetryableError` 分工不同:NOT_FOUND 的**竞态容忍**只在这里(轮询场景),
|
|
354
|
+
* 不进「可重试」白名单 —— 避免把「资源 ID 写错」这类确定性失败也当成可重试。
|
|
355
|
+
*/
|
|
356
|
+
function isNotYetVisibleError(e) {
|
|
357
|
+
const code = pickStringField(e, 'code').toLowerCase();
|
|
358
|
+
const message = pickStringField(e, 'message').toLowerCase();
|
|
359
|
+
const haystack = `${code} ${message}`;
|
|
360
|
+
return haystack.includes('notfound') || haystack.includes('not_found') || haystack.includes('not found');
|
|
361
|
+
}
|
|
270
362
|
class FunctionService {
|
|
271
363
|
constructor(environment) {
|
|
272
364
|
this.environment = environment;
|
|
@@ -307,7 +399,7 @@ class FunctionService {
|
|
|
307
399
|
await packer.build();
|
|
308
400
|
base64 = await packer.getBase64Code();
|
|
309
401
|
if (!base64) {
|
|
310
|
-
|
|
402
|
+
(0, error_1.throwNotFound)('函数不存在!');
|
|
311
403
|
}
|
|
312
404
|
params.AddFiles = base64;
|
|
313
405
|
}
|
|
@@ -315,6 +407,14 @@ class FunctionService {
|
|
|
315
407
|
}
|
|
316
408
|
/**
|
|
317
409
|
* 创建云函数
|
|
410
|
+
*
|
|
411
|
+
* CBF(云开发云函数,`Functions[0].Backend === 'cbf'`)环境差异:
|
|
412
|
+
* - 默认 runtime 为 `Nodejs24.11`(见 `CBF_ALLOWED_RUNTIMES`)
|
|
413
|
+
* - 不再注入 `Stamp` / `ClsTopicId` / `ClsLogsetId` 与默认 `Role`
|
|
414
|
+
* - 不支持定时触发器,内部跳过触发器创建
|
|
415
|
+
*
|
|
416
|
+
* 用户显式配置的字段一律原样下发,不支持的字段由服务端报错透传。
|
|
417
|
+
*
|
|
318
418
|
* @param {ICreateFunctionParam} funcParam
|
|
319
419
|
* @returns {(Promise<IResponseInfo | ICreateFunctionRes>)}
|
|
320
420
|
*/
|
|
@@ -323,25 +423,34 @@ class FunctionService {
|
|
|
323
423
|
const { namespace, backend } = this.getFunctionConfig();
|
|
324
424
|
const { func, functionRootPath, force = false, base64Code, codeSecret, functionPath, deployMode, code } = funcParam;
|
|
325
425
|
const funcName = func.name;
|
|
326
|
-
const
|
|
426
|
+
const isCbf = backend === 'cbf';
|
|
427
|
+
// EnvId 为两端共用;Stamp / ClsTopicId / ClsLogsetId 与默认 Role 都是 SDK 侧注入的字段,
|
|
428
|
+
// CBF(云开发云函数)不接受这些参数,一旦下发 CreateFunction 会直接失败(CBF does not
|
|
429
|
+
// support parameter xxx),故 CBF 下不再注入。用户显式配置的 role 等字段仍经
|
|
430
|
+
// configToParams 的字段映射原样下发,由服务端校验并透传报错。
|
|
327
431
|
const baseParams = {
|
|
328
|
-
EnvId: namespace
|
|
329
|
-
Stamp: 'MINI_QCBASE',
|
|
330
|
-
ClsTopicId: TopicId,
|
|
331
|
-
ClsLogsetId: LogsetId
|
|
432
|
+
EnvId: namespace
|
|
332
433
|
};
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
baseParams.
|
|
434
|
+
if (!isCbf) {
|
|
435
|
+
const { TopicId, LogsetId } = this.getClsServiceConfig();
|
|
436
|
+
baseParams.Stamp = 'MINI_QCBASE';
|
|
437
|
+
baseParams.ClsTopicId = TopicId;
|
|
438
|
+
baseParams.ClsLogsetId = LogsetId;
|
|
439
|
+
// 默认注入 TCB_QcsRole,DISABLE_DEFAULT_TCB_ROLE 为真时跳过(由 func.role 决定或留空)
|
|
440
|
+
if (!parseEnvBoolean(process.env.DISABLE_DEFAULT_TCB_ROLE)) {
|
|
441
|
+
baseParams.Role = 'TCB_QcsRole';
|
|
442
|
+
}
|
|
336
443
|
}
|
|
337
444
|
const params = configToParams({
|
|
338
445
|
func,
|
|
339
446
|
codeSecret,
|
|
340
|
-
baseParams
|
|
447
|
+
baseParams,
|
|
448
|
+
isCbf
|
|
341
449
|
});
|
|
342
450
|
// 统一走 tcb;默认 runtime 按环境函数后端类型设置
|
|
451
|
+
// CBF(云开发云函数)目前仅提供 Nodejs24.11(见 CBF_ALLOWED_RUNTIMES)
|
|
343
452
|
if (!func.runtime) {
|
|
344
|
-
params.Runtime =
|
|
453
|
+
params.Runtime = isCbf ? CBF_ALLOWED_RUNTIMES[0] : 'Nodejs20.19';
|
|
345
454
|
}
|
|
346
455
|
const customCode = code;
|
|
347
456
|
const customImageCode = customCode === null || customCode === void 0 ? void 0 : customCode.ImageConfig;
|
|
@@ -357,7 +466,7 @@ class FunctionService {
|
|
|
357
466
|
params.Code = customCode;
|
|
358
467
|
}
|
|
359
468
|
if (!((_c = (_b = params.Code) === null || _b === void 0 ? void 0 : _b.ImageConfig) === null || _c === void 0 ? void 0 : _c.ImageUri)) {
|
|
360
|
-
|
|
469
|
+
(0, error_1.throwInvalidParam)('镜像部署需要配置 imageConfig.imageUri');
|
|
361
470
|
}
|
|
362
471
|
// 镜像函数不需要 Handler / InstallDependency
|
|
363
472
|
delete params.Handler;
|
|
@@ -384,8 +493,10 @@ class FunctionService {
|
|
|
384
493
|
const res = await this.tcbService.request('CreateFunction', params);
|
|
385
494
|
// 等待函数状态正常
|
|
386
495
|
await this.waitFunctionActive(funcName, codeSecret);
|
|
387
|
-
//
|
|
388
|
-
|
|
496
|
+
// 创建函数触发器、失败自动重试(CBF 不支持定时触发器,跳过)
|
|
497
|
+
if (!isCbf) {
|
|
498
|
+
await this.retryCreateTrigger(funcName, func.triggers);
|
|
499
|
+
}
|
|
389
500
|
// 设置路径,创建云接入路径
|
|
390
501
|
if (func.path) {
|
|
391
502
|
await this.createAccessPath(funcName, func.path);
|
|
@@ -432,8 +543,10 @@ class FunctionService {
|
|
|
432
543
|
const configRes = await this.updateFunctionConfig(func);
|
|
433
544
|
// 等待函数状态正常
|
|
434
545
|
await this.waitFunctionActive(funcName, codeSecret);
|
|
435
|
-
// 3.
|
|
436
|
-
const triggerRes =
|
|
546
|
+
// 3. 创建函数触发器(CBF 不支持定时触发器,跳过)
|
|
547
|
+
const triggerRes = isCbf
|
|
548
|
+
? null
|
|
549
|
+
: await this.retryCreateTrigger(funcName, func.triggers);
|
|
437
550
|
// 设置路径,创建云接入路径
|
|
438
551
|
if (func.path) {
|
|
439
552
|
await this.createAccessPath(funcName, func.path);
|
|
@@ -544,7 +657,7 @@ class FunctionService {
|
|
|
544
657
|
currentOffset += pageSize;
|
|
545
658
|
}
|
|
546
659
|
catch (error) {
|
|
547
|
-
|
|
660
|
+
(0, error_1.throwApiError)(`获取函数列表失败: ${error.message}`);
|
|
548
661
|
}
|
|
549
662
|
}
|
|
550
663
|
// 格式化数据
|
|
@@ -601,25 +714,39 @@ class FunctionService {
|
|
|
601
714
|
(0, utils_1.successLog)(`[${name}] 函数删除成功!`);
|
|
602
715
|
}
|
|
603
716
|
catch (e) {
|
|
604
|
-
|
|
717
|
+
(0, error_1.throwApiError)(e.message);
|
|
605
718
|
}
|
|
606
719
|
})());
|
|
607
720
|
await Promise.all(promises);
|
|
608
721
|
}
|
|
609
722
|
/**
|
|
610
723
|
* 获取云函数详细信息
|
|
724
|
+
*
|
|
725
|
+
* CBF(云开发云函数,`Functions[0].Backend === 'cbf'`)环境差异:
|
|
726
|
+
* - 不再注入 `ShowCode`(CBF 不接受该参数),因此也拿不到函数代码内容,
|
|
727
|
+
* 需要代码请使用 `downloadFunction`
|
|
728
|
+
* - 不转发 `codeSecret`:CBF 不支持代码保护,若在此下发会掩盖
|
|
729
|
+
* `CreateFunction` / `UpdateFunctionCode` 的真实报错
|
|
730
|
+
*
|
|
611
731
|
* @param {string} name 云函数名称
|
|
612
732
|
* @returns {Promise<Record<string, string>>}
|
|
613
733
|
*/
|
|
614
734
|
async getFunctionDetail(name, codeSecret) {
|
|
615
|
-
const { namespace } = this.getFunctionConfig();
|
|
735
|
+
const { namespace, backend } = this.getFunctionConfig();
|
|
736
|
+
const isCbf = backend === 'cbf';
|
|
737
|
+
// GetFunction 只传 EnvId 即可(SCF 与 CBF 均实测支持),Namespace 属冗余参数,不再下发
|
|
616
738
|
const params = {
|
|
617
739
|
FunctionName: name,
|
|
618
|
-
|
|
619
|
-
EnvId: namespace,
|
|
620
|
-
Namespace: namespace
|
|
740
|
+
EnvId: namespace
|
|
621
741
|
};
|
|
622
|
-
|
|
742
|
+
// ShowCode 是 SDK 侧注入的字段:CBF(云开发云函数)不接受该参数,故 CBF 下不再注入
|
|
743
|
+
if (!isCbf) {
|
|
744
|
+
params.ShowCode = 'TRUE';
|
|
745
|
+
}
|
|
746
|
+
// CodeSecret(代码保护)在 CBF 下不被支持。该参数由调用方透传进来,
|
|
747
|
+
// 若在这里下发,用户会先看到指向 GetFunction 的报错,掩盖真正不支持代码保护的
|
|
748
|
+
// CreateFunction / UpdateFunctionCode,故 CBF 下不转发
|
|
749
|
+
if (codeSecret && !isCbf) {
|
|
623
750
|
params.CodeSecret = codeSecret;
|
|
624
751
|
}
|
|
625
752
|
const data = await this.tcbService.request('GetFunction', params);
|
|
@@ -664,7 +791,7 @@ class FunctionService {
|
|
|
664
791
|
data.push(info);
|
|
665
792
|
}
|
|
666
793
|
catch (e) {
|
|
667
|
-
|
|
794
|
+
(0, error_1.throwApiError)(`${name} 获取信息失败:${e.message}`);
|
|
668
795
|
}
|
|
669
796
|
})());
|
|
670
797
|
await Promise.all(promises);
|
|
@@ -686,7 +813,7 @@ class FunctionService {
|
|
|
686
813
|
* @returns {Promise<IFunctionLogRes>}
|
|
687
814
|
*/
|
|
688
815
|
async getFunctionLogs(_options) {
|
|
689
|
-
|
|
816
|
+
(0, error_1.throwApiError)('getFunctionLogs 已废弃:底层 GetFunctionLogs 接口已下线,请使用 env.getLogService().searchClsLog() 查询云函数日志');
|
|
690
817
|
}
|
|
691
818
|
/**
|
|
692
819
|
* 获取函数日志 ID 列表
|
|
@@ -703,7 +830,7 @@ class FunctionService {
|
|
|
703
830
|
* @returns {Promise<IFunctionLogResV2>}
|
|
704
831
|
*/
|
|
705
832
|
async getFunctionLogsV2(_options) {
|
|
706
|
-
|
|
833
|
+
(0, error_1.throwApiError)('getFunctionLogsV2 已废弃:底层 GetFunctionLogs 接口已下线,请使用 env.getLogService().searchClsLog() 查询云函数日志');
|
|
707
834
|
}
|
|
708
835
|
/**
|
|
709
836
|
* 根据函数日志 ID 查询日志详情
|
|
@@ -716,7 +843,7 @@ class FunctionService {
|
|
|
716
843
|
* @returns {Promise<IFunctionLogDetailRes>}
|
|
717
844
|
*/
|
|
718
845
|
async getFunctionLogDetail(_options) {
|
|
719
|
-
|
|
846
|
+
(0, error_1.throwApiError)('getFunctionLogDetail 已废弃:底层 GetFunctionLogDetail 接口已下线,请使用 env.getLogService().searchClsLog() 查询云函数日志');
|
|
720
847
|
}
|
|
721
848
|
/**
|
|
722
849
|
* 获取函数的完整调用日志
|
|
@@ -726,16 +853,25 @@ class FunctionService {
|
|
|
726
853
|
* @returns {Promise<IFunctionLogDetailRes[]>} 返回包含完整日志详情的数组
|
|
727
854
|
*/
|
|
728
855
|
async getCompleteFunctionLogs(_options) {
|
|
729
|
-
|
|
856
|
+
(0, error_1.throwApiError)('getCompleteFunctionLogs 已废弃:底层 GetFunctionLogs/GetFunctionLogDetail 接口已下线,请使用 env.getLogService().searchClsLog() 查询云函数日志');
|
|
730
857
|
}
|
|
731
858
|
/**
|
|
732
859
|
* 更新云函数配置
|
|
860
|
+
*
|
|
861
|
+
* CBF(云开发云函数,`Functions[0].Backend === 'cbf'`)环境差异:
|
|
862
|
+
* - 不再注入 `ClsTopicId` / `ClsLogsetId`,也不转发 `InstallDependency`
|
|
863
|
+
* - 环境变量:不传不接受 Key 为空的占位项,清空需提交空数组(传 `envVariables: {}` 即清空)
|
|
864
|
+
* - 不支持定时触发器:配置里的 `triggers` 会被跳过
|
|
865
|
+
*
|
|
866
|
+
* 用户显式配置的字段一律原样下发,不支持的字段由服务端报错透传。
|
|
867
|
+
*
|
|
733
868
|
* @param {ICloudFunction} func 云函数配置
|
|
734
869
|
* @returns {Promise<IResponseInfo>}
|
|
735
870
|
*/
|
|
736
871
|
async updateFunctionConfig(func) {
|
|
737
872
|
var _a, _b, _c;
|
|
738
|
-
const { namespace } = this.getFunctionConfig();
|
|
873
|
+
const { namespace, backend } = this.getFunctionConfig();
|
|
874
|
+
const isCbf = backend === 'cbf';
|
|
739
875
|
// UpdateFunctionConfiguration API 白名单(大小写不敏感)
|
|
740
876
|
// 参考:https://cloud.tencent.com/document/product/583/18580
|
|
741
877
|
// 注意:Runtime, Handler, Code, Type, ProtocolType 只能在 CreateFunction 时指定
|
|
@@ -758,14 +894,19 @@ class FunctionService {
|
|
|
758
894
|
// 注意:dnsCache 需要 'TRUE'/'FALSE' 字符串,在特殊处理阶段处理
|
|
759
895
|
intranetconfig: 'IntranetConfig'
|
|
760
896
|
};
|
|
761
|
-
const { TopicId, LogsetId } = this.getClsServiceConfig();
|
|
762
897
|
// 构建参数
|
|
898
|
+
// ClsTopicId / ClsLogsetId 为 SDK 侧注入的字段:CBF(云开发云函数)不接受这两个参数,
|
|
899
|
+
// 且 UpdateFunctionConfiguration 按请求 JSON 中出现过的 key 生成 FieldMask,
|
|
900
|
+
// 一旦下发会直接报 FieldMask contains an unsupported field,故 CBF 下不再注入。
|
|
763
901
|
const params = {
|
|
764
902
|
EnvId: namespace,
|
|
765
|
-
FunctionName: func.name
|
|
766
|
-
ClsTopicId: TopicId,
|
|
767
|
-
ClsLogsetId: LogsetId
|
|
903
|
+
FunctionName: func.name
|
|
768
904
|
};
|
|
905
|
+
if (!isCbf) {
|
|
906
|
+
const { TopicId, LogsetId } = this.getClsServiceConfig();
|
|
907
|
+
params.ClsTopicId = TopicId;
|
|
908
|
+
params.ClsLogsetId = LogsetId;
|
|
909
|
+
}
|
|
769
910
|
// 白名单字段自动转换(大小写不敏感,统一输出 PascalCase)
|
|
770
911
|
for (const key of Object.keys(func)) {
|
|
771
912
|
const lowerKey = key.toLowerCase();
|
|
@@ -775,7 +916,11 @@ class FunctionService {
|
|
|
775
916
|
params[pascalKey] = toPascalCaseKeys(func[key]);
|
|
776
917
|
}
|
|
777
918
|
// 特殊处理:安装依赖标志(仅透传用户显式指定的值)
|
|
778
|
-
|
|
919
|
+
// CBF(云开发云函数)的 UpdateFunctionConfiguration 不接受该参数
|
|
920
|
+
// (CBF does not support parameter InstallDependency)。它属于「创建 / 更新代码」语义:
|
|
921
|
+
// SDK 在 createFunction(force=true) 覆盖部署时会把整个 func 透传给本接口,
|
|
922
|
+
// 若原样下发会让常规覆盖部署直接失败,故 CBF 下不下发
|
|
923
|
+
if (func.installDependency !== undefined && !isCbf) {
|
|
779
924
|
params.InstallDependency = toBooleanString(func.installDependency);
|
|
780
925
|
}
|
|
781
926
|
// 特殊处理:L5 配置(支持 boolean | string)
|
|
@@ -795,13 +940,25 @@ class FunctionService {
|
|
|
795
940
|
params.Publish = toBooleanString(publishValue);
|
|
796
941
|
}
|
|
797
942
|
// 特殊处理:环境变量
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
943
|
+
// - 有变量:原样下发
|
|
944
|
+
// - 未传 envVariables:不提交,保持云端现有变量不变
|
|
945
|
+
// - 显式传空对象:CBF(云开发云函数)清空环境变量需提交空数组
|
|
946
|
+
// (CBF 不接受 SCF 那套 Key 为空的占位项,会报 Invalid environment variable)
|
|
947
|
+
if (func.envVariables) {
|
|
948
|
+
const envKeys = Object.keys(func.envVariables);
|
|
949
|
+
if (envKeys.length > 0) {
|
|
950
|
+
params.Environment = {
|
|
951
|
+
Variables: envKeys.map(key => ({
|
|
952
|
+
Key: key,
|
|
953
|
+
Value: func.envVariables[key]
|
|
954
|
+
}))
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
else if (isCbf) {
|
|
958
|
+
params.Environment = {
|
|
959
|
+
Variables: []
|
|
960
|
+
};
|
|
961
|
+
}
|
|
805
962
|
}
|
|
806
963
|
// 特殊处理:VPC 配置
|
|
807
964
|
if (((_a = func === null || func === void 0 ? void 0 : func.vpc) === null || _a === void 0 ? void 0 : _a.subnetId) !== undefined && ((_b = func === null || func === void 0 ? void 0 : func.vpc) === null || _b === void 0 ? void 0 : _b.vpcId) !== undefined) {
|
|
@@ -846,7 +1003,8 @@ class FunctionService {
|
|
|
846
1003
|
}
|
|
847
1004
|
try {
|
|
848
1005
|
// 如果函数配置中包含触发器,则更新触发器
|
|
849
|
-
|
|
1006
|
+
// CBF(云开发云函数)不支持定时触发器,内部静默跳过(避免无意义的失败告警)
|
|
1007
|
+
if (func.triggers && func.triggers.length > 0 && !isCbf) {
|
|
850
1008
|
try {
|
|
851
1009
|
await this.createFunctionTriggers(func.name, func.triggers);
|
|
852
1010
|
}
|
|
@@ -865,6 +1023,11 @@ class FunctionService {
|
|
|
865
1023
|
}
|
|
866
1024
|
}
|
|
867
1025
|
/**
|
|
1026
|
+
* 更新云函数代码
|
|
1027
|
+
*
|
|
1028
|
+
* `UpdateFunctionCode` 只传 `EnvId` 即可(SCF 与 CBF 均实测支持),不再下发冗余的 `Namespace`。
|
|
1029
|
+
*
|
|
1030
|
+
* 注意:CBF(云开发云函数)不支持 `codeSecret`(代码保护),传入后会由服务端报错。
|
|
868
1031
|
*
|
|
869
1032
|
* @param {IUpdateFunctionCodeParam} funcParam
|
|
870
1033
|
* @returns {Promise<IResponseInfo>}
|
|
@@ -879,13 +1042,12 @@ class FunctionService {
|
|
|
879
1042
|
const params = {
|
|
880
1043
|
FunctionName: funcName,
|
|
881
1044
|
EnvId: namespace,
|
|
882
|
-
Namespace: namespace,
|
|
883
1045
|
Code: {
|
|
884
1046
|
ImageConfig: buildImageConfig(func.imageConfig)
|
|
885
1047
|
}
|
|
886
1048
|
};
|
|
887
1049
|
if (!params.Code.ImageConfig.ImageUri) {
|
|
888
|
-
|
|
1050
|
+
(0, error_1.throwInvalidParam)('镜像部署需要配置 imageConfig.imageUri');
|
|
889
1051
|
}
|
|
890
1052
|
try {
|
|
891
1053
|
// 等待函数状态正常
|
|
@@ -927,7 +1089,6 @@ class FunctionService {
|
|
|
927
1089
|
const params = {
|
|
928
1090
|
FunctionName: funcName,
|
|
929
1091
|
EnvId: namespace,
|
|
930
|
-
Namespace: namespace,
|
|
931
1092
|
Handler: func.handler || 'index.main',
|
|
932
1093
|
InstallDependency: installDependency,
|
|
933
1094
|
Code: codeParams
|
|
@@ -1023,7 +1184,7 @@ class FunctionService {
|
|
|
1023
1184
|
return Object.assign({ RequestId }, Result);
|
|
1024
1185
|
}
|
|
1025
1186
|
catch (e) {
|
|
1026
|
-
throw new error_1.CloudBaseError(`[${name}] 调用失败:${e.message}`, {
|
|
1187
|
+
throw new error_1.CloudBaseError(`[${name}] 调用失败:${e.message}`, { code: 'API_ERROR', source: 'manager-node',
|
|
1027
1188
|
requestId: e.requestId
|
|
1028
1189
|
});
|
|
1029
1190
|
}
|
|
@@ -1045,7 +1206,7 @@ class FunctionService {
|
|
|
1045
1206
|
return result;
|
|
1046
1207
|
}
|
|
1047
1208
|
catch (e) {
|
|
1048
|
-
|
|
1209
|
+
(0, error_1.throwApiError)(`${func.name} 函数调用失败:${e.message}`);
|
|
1049
1210
|
}
|
|
1050
1211
|
})());
|
|
1051
1212
|
return Promise.all(promises);
|
|
@@ -1062,7 +1223,7 @@ class FunctionService {
|
|
|
1062
1223
|
async copyFunction(name, newFunctionName, targetEnvId, force = false) {
|
|
1063
1224
|
const { namespace } = this.getFunctionConfig();
|
|
1064
1225
|
if (!namespace || !name || !newFunctionName) {
|
|
1065
|
-
|
|
1226
|
+
(0, error_1.throwInvalidParam)('参数缺失');
|
|
1066
1227
|
}
|
|
1067
1228
|
return this.scfService.request('CopyFunction', {
|
|
1068
1229
|
FunctionName: name,
|
|
@@ -1074,6 +1235,11 @@ class FunctionService {
|
|
|
1074
1235
|
}
|
|
1075
1236
|
/**
|
|
1076
1237
|
* 创建云函数触发器
|
|
1238
|
+
*
|
|
1239
|
+
* CBF(云开发云函数,`Functions[0].Backend === 'cbf'`)环境不支持定时触发器,
|
|
1240
|
+
* 且该接口走 SCF 后端,对 CBF 的函数会返回「未找到指定的函数」这类误导性报错,
|
|
1241
|
+
* 故 CBF 下直接抛错。内部调用点(`createFunction` / `updateFunctionConfig`)会跳过本方法。
|
|
1242
|
+
*
|
|
1077
1243
|
* @param {string} name 云函数名称
|
|
1078
1244
|
* @param {ICloudFunctionTrigger[]} triggers 云函数触发器配置
|
|
1079
1245
|
* @returns {Promise<IResponseInfo>}
|
|
@@ -1081,10 +1247,15 @@ class FunctionService {
|
|
|
1081
1247
|
async createFunctionTriggers(name, triggers = []) {
|
|
1082
1248
|
if (!triggers || !triggers.length)
|
|
1083
1249
|
return null;
|
|
1084
|
-
const { namespace } = this.getFunctionConfig();
|
|
1250
|
+
const { namespace, backend } = this.getFunctionConfig();
|
|
1251
|
+
// CBF(云开发云函数)不支持定时触发器:不下发到 SCF 后端
|
|
1252
|
+
// (否则会报「未找到指定的函数」,误导用户以为是函数不存在)
|
|
1253
|
+
if (backend === 'cbf') {
|
|
1254
|
+
(0, error_1.throwInvalidParam)(`[${name}] CBF(云开发云函数)环境不支持定时触发器`);
|
|
1255
|
+
}
|
|
1085
1256
|
const parsedTriggers = triggers.map(item => {
|
|
1086
1257
|
if (item.type !== 'timer') {
|
|
1087
|
-
|
|
1258
|
+
(0, error_1.throwInvalidParam)(`不支持的触发器类型 [${item.type}],目前仅支持定时触发器(timer)!`);
|
|
1088
1259
|
}
|
|
1089
1260
|
return {
|
|
1090
1261
|
TriggerName: item.name,
|
|
@@ -1117,19 +1288,29 @@ class FunctionService {
|
|
|
1117
1288
|
(0, utils_1.successLog)(`[${func.name}] 创建云函数触发器成功!`);
|
|
1118
1289
|
}
|
|
1119
1290
|
catch (e) {
|
|
1120
|
-
|
|
1291
|
+
(0, error_1.throwApiError)(e.message);
|
|
1121
1292
|
}
|
|
1122
1293
|
})());
|
|
1123
1294
|
await Promise.all(promises);
|
|
1124
1295
|
}
|
|
1125
1296
|
/**
|
|
1126
1297
|
* 删除云函数触发器
|
|
1298
|
+
*
|
|
1299
|
+
* CBF(云开发云函数,`Functions[0].Backend === 'cbf'`)环境不支持定时触发器,
|
|
1300
|
+
* 且该接口走 SCF 后端,对 CBF 的函数会返回「未找到指定的函数」这类误导性报错,
|
|
1301
|
+
* 故 CBF 下直接抛错。
|
|
1302
|
+
*
|
|
1127
1303
|
* @param {string} name 云函数名称
|
|
1128
1304
|
* @param {string} triggerName 云函数触发器名称
|
|
1129
1305
|
* @returns {Promise<IResponseInfo>}
|
|
1130
1306
|
*/
|
|
1131
1307
|
async deleteFunctionTrigger(name, triggerName) {
|
|
1132
|
-
const { namespace } = this.getFunctionConfig();
|
|
1308
|
+
const { namespace, backend } = this.getFunctionConfig();
|
|
1309
|
+
// CBF(云开发云函数)不支持定时触发器:不下发到 SCF 后端
|
|
1310
|
+
// (否则会报「未找到指定的函数」,误导用户以为是函数不存在)
|
|
1311
|
+
if (backend === 'cbf') {
|
|
1312
|
+
(0, error_1.throwInvalidParam)(`[${name}] CBF(云开发云函数)环境不支持定时触发器`);
|
|
1313
|
+
}
|
|
1133
1314
|
try {
|
|
1134
1315
|
return await this.scfService.request('DeleteTrigger', {
|
|
1135
1316
|
FunctionName: name,
|
|
@@ -1139,7 +1320,7 @@ class FunctionService {
|
|
|
1139
1320
|
});
|
|
1140
1321
|
}
|
|
1141
1322
|
catch (e) {
|
|
1142
|
-
|
|
1323
|
+
(0, error_1.throwApiError)(`[${name}] 删除触发器失败:${e.message}`);
|
|
1143
1324
|
}
|
|
1144
1325
|
}
|
|
1145
1326
|
async batchDeleteTriggers(options) {
|
|
@@ -1150,7 +1331,7 @@ class FunctionService {
|
|
|
1150
1331
|
(0, utils_1.successLog)(`[${func.name}] 删除云函数触发器成功!`);
|
|
1151
1332
|
}
|
|
1152
1333
|
catch (e) {
|
|
1153
|
-
|
|
1334
|
+
(0, error_1.throwApiError)(e.message);
|
|
1154
1335
|
}
|
|
1155
1336
|
})());
|
|
1156
1337
|
await Promise.all(promises);
|
|
@@ -1202,7 +1383,7 @@ class FunctionService {
|
|
|
1202
1383
|
return { Url, RequestId, CodeSha256 };
|
|
1203
1384
|
}
|
|
1204
1385
|
catch (e) {
|
|
1205
|
-
throw new error_1.CloudBaseError(`[${functionName}] 获取函数代码下载链接失败:${e.message}`, {
|
|
1386
|
+
throw new error_1.CloudBaseError(`[${functionName}] 获取函数代码下载链接失败:${e.message}`, { code: 'API_ERROR', source: 'manager-node',
|
|
1206
1387
|
requestId: e.requestId
|
|
1207
1388
|
});
|
|
1208
1389
|
}
|
|
@@ -1231,7 +1412,7 @@ class FunctionService {
|
|
|
1231
1412
|
Layers = Layers.map(item => lodash_1.default.pick(item, ['LayerName', 'LayerVersion']));
|
|
1232
1413
|
const index = Layers.findIndex(item => item.LayerName === layerName && item.LayerVersion === layerVersion);
|
|
1233
1414
|
if (index === -1) {
|
|
1234
|
-
|
|
1415
|
+
(0, error_1.throwNotFound)('层不存在');
|
|
1235
1416
|
}
|
|
1236
1417
|
// 删除指定的层
|
|
1237
1418
|
Layers.splice(index, 1);
|
|
@@ -1268,7 +1449,7 @@ class FunctionService {
|
|
|
1268
1449
|
const url = res === null || res === void 0 ? void 0 : res.Location;
|
|
1269
1450
|
const zipPath = path_1.default.join(destPath, `${name}-${version}.zip`);
|
|
1270
1451
|
if ((0, utils_1.checkFullAccess)(zipPath)) {
|
|
1271
|
-
|
|
1452
|
+
(0, error_1.throwInvalidParam)(`文件已存在:${zipPath}`);
|
|
1272
1453
|
}
|
|
1273
1454
|
// 下载文件
|
|
1274
1455
|
return (0, utils_1.downloadAndExtractRemoteZip)(url, destPath);
|
|
@@ -1301,7 +1482,7 @@ class FunctionService {
|
|
|
1301
1482
|
else {
|
|
1302
1483
|
const fileType = path_1.default.extname(contentPath);
|
|
1303
1484
|
if (fileType !== '.zip') {
|
|
1304
|
-
|
|
1485
|
+
(0, error_1.throwInvalidParam)('文件类型不正确,目前只支持 ZIP 文件!');
|
|
1305
1486
|
}
|
|
1306
1487
|
const fileBuffer = await fs_1.default.promises.readFile(contentPath);
|
|
1307
1488
|
base64 = fileBuffer.toString('base64');
|
|
@@ -1370,7 +1551,9 @@ class FunctionService {
|
|
|
1370
1551
|
timer = setTimeout(() => {
|
|
1371
1552
|
clearInterval(ticker);
|
|
1372
1553
|
if (!resolved) {
|
|
1373
|
-
reject(
|
|
1554
|
+
// ⚠️ 不能写 reject(throwNetError(...)):该 helper 必抛 ⇒ 异常抛在定时器回调里、
|
|
1555
|
+
// Promise 永不 settle ⇒ 调用方挂死。**值位置**必须构造错误对象。
|
|
1556
|
+
reject(new error_1.CloudBaseError('函数状态异常,检查超时', { type: error_types_1.ERROR_TYPES.netError }));
|
|
1374
1557
|
}
|
|
1375
1558
|
}, 300000);
|
|
1376
1559
|
ticker = setInterval(async () => {
|
|
@@ -1384,7 +1567,7 @@ class FunctionService {
|
|
|
1384
1567
|
const errorDetails = (StatusReasons === null || StatusReasons === void 0 ? void 0 : StatusReasons.map(item => `[${item.ErrorCode}] ${item.ErrorMessage}`).join('\n')) || '';
|
|
1385
1568
|
const errorMsg = `云函数创建失败${StatusDesc ? `\n状态描述: ${StatusDesc}` : ''}${errorDetails ? `\n失败信息: ${errorDetails}` : ''}`;
|
|
1386
1569
|
// 注意:这里不传递 RequestId,因为这是 GetFunction 的 RequestId,不是导致失败的 CreateFunction/UpdateFunctionCode 的 RequestId
|
|
1387
|
-
|
|
1570
|
+
(0, error_1.throwApiError)(errorMsg);
|
|
1388
1571
|
}
|
|
1389
1572
|
// 函数状态正常
|
|
1390
1573
|
clearInterval(ticker);
|
|
@@ -1392,6 +1575,10 @@ class FunctionService {
|
|
|
1392
1575
|
resolve();
|
|
1393
1576
|
}
|
|
1394
1577
|
catch (e) {
|
|
1578
|
+
// 容忍创建/更新期的「资源尚不可见」:`GetFunction` 在函数刚提交后会短暂返回 NOT_FOUND,
|
|
1579
|
+
// 此时继续轮询即可(超时仍由 5 分钟定时器兜底)—— 否则会把正常的「创建中」误报为失败。
|
|
1580
|
+
if (isNotYetVisibleError(e))
|
|
1581
|
+
return;
|
|
1395
1582
|
clearInterval(ticker);
|
|
1396
1583
|
clearTimeout(timer);
|
|
1397
1584
|
reject(e);
|
|
@@ -1458,10 +1645,10 @@ class FunctionService {
|
|
|
1458
1645
|
async publishVersion(publishParams) {
|
|
1459
1646
|
const { namespace } = this.getFunctionConfig();
|
|
1460
1647
|
const { functionName: FunctionName, description: Description } = publishParams;
|
|
1461
|
-
return this.
|
|
1648
|
+
return this.tcbService.request('PublishVersion', {
|
|
1462
1649
|
FunctionName,
|
|
1463
1650
|
Description,
|
|
1464
|
-
|
|
1651
|
+
EnvId: namespace
|
|
1465
1652
|
});
|
|
1466
1653
|
}
|
|
1467
1654
|
/**
|
|
@@ -1472,10 +1659,11 @@ class FunctionService {
|
|
|
1472
1659
|
*/
|
|
1473
1660
|
async listVersionByFunction(listVersionParams) {
|
|
1474
1661
|
const { namespace } = this.getFunctionConfig();
|
|
1475
|
-
const { functionName: FunctionName, offset: Offset, limit: Limit, order: Order, orderBy: OrderBy } = listVersionParams;
|
|
1476
|
-
return this.
|
|
1662
|
+
const { functionName: FunctionName, searchKey: SearchKey, offset: Offset, limit: Limit, order: Order, orderBy: OrderBy } = listVersionParams;
|
|
1663
|
+
return this.tcbService.request('ListVersionByFunction', {
|
|
1477
1664
|
FunctionName,
|
|
1478
|
-
|
|
1665
|
+
EnvId: namespace,
|
|
1666
|
+
SearchKey,
|
|
1479
1667
|
Offset,
|
|
1480
1668
|
Limit,
|
|
1481
1669
|
Order,
|
|
@@ -1483,7 +1671,23 @@ class FunctionService {
|
|
|
1483
1671
|
});
|
|
1484
1672
|
}
|
|
1485
1673
|
/**
|
|
1486
|
-
*
|
|
1674
|
+
* 删除函数指定版本
|
|
1675
|
+
* @param {IDeleteFunctionVersionParams} params
|
|
1676
|
+
* @returns {Promise<IResponseInfo>}
|
|
1677
|
+
* @memberof FunctionService
|
|
1678
|
+
*/
|
|
1679
|
+
async deleteFunctionVersion(params) {
|
|
1680
|
+
const { namespace } = this.getFunctionConfig();
|
|
1681
|
+
const { functionName: FunctionName, qualifier: Qualifier } = params;
|
|
1682
|
+
return this.tcbService.request('DeleteFunctionVersion', {
|
|
1683
|
+
FunctionName,
|
|
1684
|
+
EnvId: namespace,
|
|
1685
|
+
Qualifier
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* 更新函数流量配置(旧版 SCF 协议)
|
|
1690
|
+
* @deprecated 请使用 updateFunctionAliasConfigV2
|
|
1487
1691
|
* @param {IUpdateFunctionAliasConfig} updateVersionConfigParams
|
|
1488
1692
|
* @returns {Promise<IResponseInfo>}
|
|
1489
1693
|
* @memberof FunctionService
|
|
@@ -1501,7 +1705,24 @@ class FunctionService {
|
|
|
1501
1705
|
});
|
|
1502
1706
|
}
|
|
1503
1707
|
/**
|
|
1504
|
-
*
|
|
1708
|
+
* 更新函数流量策略(V2,TCB Traffic 协议)
|
|
1709
|
+
* @param {IUpdateFunctionAliasConfigV2} updateVersionConfigParams
|
|
1710
|
+
* @returns {Promise<IResponseInfo>}
|
|
1711
|
+
* @memberof FunctionService
|
|
1712
|
+
*/
|
|
1713
|
+
async updateFunctionAliasConfigV2(updateVersionConfigParams) {
|
|
1714
|
+
const { namespace } = this.getFunctionConfig();
|
|
1715
|
+
const { functionName: FunctionName, name: Name, traffic: Traffic } = updateVersionConfigParams;
|
|
1716
|
+
return this.tcbService.request('UpdateAlias', {
|
|
1717
|
+
FunctionName,
|
|
1718
|
+
Name,
|
|
1719
|
+
EnvId: namespace,
|
|
1720
|
+
Traffic
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* 查询函数别名详情(旧版 SCF 协议)
|
|
1725
|
+
* @deprecated 请使用 getFunctionAliasV2
|
|
1505
1726
|
* @param {IGetFunctionAlias} params
|
|
1506
1727
|
* @returns {Promise<IGetFunctionAliasRes>}
|
|
1507
1728
|
* @memberof FunctionService
|
|
@@ -1516,14 +1737,37 @@ class FunctionService {
|
|
|
1516
1737
|
});
|
|
1517
1738
|
}
|
|
1518
1739
|
/**
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1740
|
+
* 查询函数别名详情(V2,TCB Traffic 协议)
|
|
1741
|
+
* @param {IGetFunctionAliasV2} params
|
|
1742
|
+
* @returns {Promise<IGetFunctionAliasResV2>}
|
|
1743
|
+
* @memberof FunctionService
|
|
1744
|
+
*/
|
|
1745
|
+
async getFunctionAliasV2(params) {
|
|
1746
|
+
const { namespace } = this.getFunctionConfig();
|
|
1747
|
+
const { functionName: FunctionName, name: Name } = params;
|
|
1748
|
+
return this.tcbService.request('GetAlias', {
|
|
1749
|
+
FunctionName,
|
|
1750
|
+
Name,
|
|
1751
|
+
EnvId: namespace
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* COS 上传方式上传函数代码包,返回 { Key }
|
|
1756
|
+
* - CBF(backend=cbf):tcb.GetTempCosInfo 获取 PutUrl 直传,Key = 服务端生成的 ObjectKey
|
|
1757
|
+
* - SCF(backend=scf,默认):scf.GetTempCosInfo + Sign + COS SDK 上传
|
|
1758
|
+
* Key 由 getCodeParams 格式化为 TempCosObjectName 用于创建/更新函数
|
|
1521
1759
|
*/
|
|
1522
1760
|
async uploadFunctionZipToCos(options, installDependency) {
|
|
1761
|
+
const { backend } = this.getFunctionConfig();
|
|
1762
|
+
// CBF 环境:tcb.GetTempCosInfo + PutUrl 直传
|
|
1763
|
+
if (backend === 'cbf') {
|
|
1764
|
+
return this.uploadFunctionZipViaPutUrl(options, installDependency);
|
|
1765
|
+
}
|
|
1766
|
+
// SCF 环境:保留原链路(scf.GetTempCosInfo + Sign + COS SDK)
|
|
1523
1767
|
const { func, functionPath, functionRootPath } = options;
|
|
1524
1768
|
const { env, appId } = this.getFunctionConfig();
|
|
1525
1769
|
if (!appId) {
|
|
1526
|
-
|
|
1770
|
+
(0, error_1.throwApiError)('无法获取 AppId,请确认环境初始化完成或已开通存储服务');
|
|
1527
1771
|
}
|
|
1528
1772
|
const objectPath = `${appId}/${env}/${func.name}.zip`;
|
|
1529
1773
|
// 1. 生成存放函数包的临时 Cos 目录
|
|
@@ -1565,7 +1809,8 @@ class FunctionService {
|
|
|
1565
1809
|
// 添加超时保护
|
|
1566
1810
|
const timeout = setTimeout(() => {
|
|
1567
1811
|
packer.clean();
|
|
1568
|
-
|
|
1812
|
+
// 同上:值位置不能调用必抛 helper
|
|
1813
|
+
reject(new error_1.CloudBaseError(`[${func.name}] COS 上传超时(60秒)`, { type: error_types_1.ERROR_TYPES.netError }));
|
|
1569
1814
|
}, 60000);
|
|
1570
1815
|
cos.sliceUploadFile(uploadParams, async (err, data) => {
|
|
1571
1816
|
clearTimeout(timeout);
|
|
@@ -1640,7 +1885,7 @@ class FunctionService {
|
|
|
1640
1885
|
catch (e) {
|
|
1641
1886
|
// 忽略响应体解析错误
|
|
1642
1887
|
}
|
|
1643
|
-
|
|
1888
|
+
(0, error_1.throwApiError)(`上传失败: ${response.status} ${response.statusText}${errorDetail}`);
|
|
1644
1889
|
}
|
|
1645
1890
|
// 清理临时文件
|
|
1646
1891
|
await packer.clean();
|
|
@@ -1655,7 +1900,7 @@ class FunctionService {
|
|
|
1655
1900
|
const MAX_ZIP_SIZE = 1.5 * 1024 * 1024; // 1.5MB
|
|
1656
1901
|
const MAX_BASE64_LENGTH = Math.floor((MAX_ZIP_SIZE * 4) / 3); // ≈ 2097152
|
|
1657
1902
|
if ((base64Code === null || base64Code === void 0 ? void 0 : base64Code.length) > MAX_BASE64_LENGTH) {
|
|
1658
|
-
|
|
1903
|
+
(0, error_1.throwInvalidParam)('ZipFile 上传不能大于 1.5MB,请使用 COS 上传方式');
|
|
1659
1904
|
}
|
|
1660
1905
|
if (base64Code === null || base64Code === void 0 ? void 0 : base64Code.length) {
|
|
1661
1906
|
return {
|
|
@@ -1680,26 +1925,128 @@ class FunctionService {
|
|
|
1680
1925
|
// 判断是否超过 ZipFile 上传大小限制(1.5MB)
|
|
1681
1926
|
const reachMax = await packer.isReachMaxSize();
|
|
1682
1927
|
if (reachMax) {
|
|
1683
|
-
|
|
1928
|
+
(0, error_1.throwInvalidParam)('ZipFile 上传不能大于 1.5MB,请使用 COS 上传方式(deployMode: "cos")');
|
|
1684
1929
|
}
|
|
1685
1930
|
const base64 = await packer.getBase64Code();
|
|
1686
1931
|
if (!(base64 === null || base64 === void 0 ? void 0 : base64.length)) {
|
|
1687
|
-
|
|
1932
|
+
(0, error_1.throwInvalidParam)('文件不能为空');
|
|
1688
1933
|
}
|
|
1689
1934
|
console.log(`[${func.name}] 部署方式: ZIP base64 上传`);
|
|
1690
1935
|
return {
|
|
1691
1936
|
ZipFile: base64
|
|
1692
1937
|
};
|
|
1693
1938
|
}
|
|
1694
|
-
// 默认走 COS 上传(GetTempCosInfo + COS SDK
|
|
1939
|
+
// 默认走 COS 上传(SCF: scf.GetTempCosInfo + COS SDK;CBF: tcb.GetTempCosInfo + PutUrl 直传)
|
|
1695
1940
|
console.log(`[${func.name}] 部署方式: COS 上传`);
|
|
1941
|
+
const { backend } = this.getFunctionConfig();
|
|
1696
1942
|
const region = this.environment.cloudBaseContext.region || constant_1.SCF_TEMP_COS.DEFAULT_REGION;
|
|
1697
1943
|
const result = await this.uploadFunctionZipToCos(options, installDependency);
|
|
1944
|
+
// CBF:ObjectKey 由服务端生成,原样填入(无前导斜杠);SCF:保持原有 /{Key} 格式
|
|
1945
|
+
if (backend === 'cbf') {
|
|
1946
|
+
return {
|
|
1947
|
+
CosBucketRegion: region,
|
|
1948
|
+
TempCosObjectName: result.Key
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1698
1951
|
return {
|
|
1699
1952
|
CosBucketRegion: region,
|
|
1700
1953
|
TempCosObjectName: `/${result.Key}`
|
|
1701
1954
|
};
|
|
1702
1955
|
}
|
|
1956
|
+
/**
|
|
1957
|
+
* CBF 环境的临时代码包上传(tcb.GetTempCosInfo + PutUrl 直传)
|
|
1958
|
+
* - 先压缩再取 PutUrl,避免压缩耗时消耗 PutUrl 约 300 秒的有效期
|
|
1959
|
+
* - PutUrl 过期(403)/ 5xx / 网络错误时重新调用 GetTempCosInfo 刷新地址,不复用旧地址(初始 + 重试共 3 次)
|
|
1960
|
+
* - ObjectKey 由服务端生成,多次调用得到不同上传路径,上传成功后原样返回
|
|
1961
|
+
* @private
|
|
1962
|
+
*/
|
|
1963
|
+
async uploadFunctionZipViaPutUrl(options, installDependency) {
|
|
1964
|
+
const { func, functionPath, functionRootPath } = options;
|
|
1965
|
+
const { env } = this.getFunctionConfig();
|
|
1966
|
+
// FileName 格式为 `{FunctionName}.zip`,最大 64 字符
|
|
1967
|
+
const fileName = `${func.name}.zip`;
|
|
1968
|
+
if (fileName.length > 64) {
|
|
1969
|
+
(0, error_1.throwInvalidParam)(`[${func.name}] 代码包文件名 ${fileName} 超过 64 字符上限`);
|
|
1970
|
+
}
|
|
1971
|
+
// 1. 本地压缩(云端安装依赖时自动忽略 node_modules 目录,与 SCF 链路一致)
|
|
1972
|
+
const ignore = installDependency === 'TRUE'
|
|
1973
|
+
? ['node_modules/**/*', 'node_modules', ...(func.ignore || [])]
|
|
1974
|
+
: [...(func.ignore || [])];
|
|
1975
|
+
const packer = new packer_1.FunctionPacker({
|
|
1976
|
+
ignore,
|
|
1977
|
+
codeType: packer_1.CodeType.File,
|
|
1978
|
+
functionPath,
|
|
1979
|
+
name: func.name,
|
|
1980
|
+
root: functionRootPath
|
|
1981
|
+
});
|
|
1982
|
+
const zipFilePath = await packer.compressFiles();
|
|
1983
|
+
try {
|
|
1984
|
+
const maxAttempts = 3;
|
|
1985
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1986
|
+
// 2. 获取上传信息(每次调用服务端生成新的 PutUrl / ObjectKey)
|
|
1987
|
+
const { PutUrl, ObjectKey } = await this.tcbService.request('GetTempCosInfo', {
|
|
1988
|
+
EnvId: env,
|
|
1989
|
+
FileName: fileName
|
|
1990
|
+
});
|
|
1991
|
+
if (!PutUrl || !ObjectKey) {
|
|
1992
|
+
(0, error_1.throwApiError)(`[${func.name}] GetTempCosInfo 返回数据异常:缺少 PutUrl 或 ObjectKey`);
|
|
1993
|
+
}
|
|
1994
|
+
// 3. 对 PutUrl 直接 HTTP PUT 上传 zip,无需拼接桶域名或 Authorization
|
|
1995
|
+
// 网络错误/超时返回 null,由外层刷新 PutUrl 重试
|
|
1996
|
+
const response = await this.putZipFileToUrl(PutUrl, zipFilePath).catch(e => {
|
|
1997
|
+
if (attempt === maxAttempts) {
|
|
1998
|
+
// R3:继承上游错误码(FetchError 自带 ECONNREFUSED / ETIMEDOUT 等)
|
|
1999
|
+
const errorCode = e.code || e.statusCode || '';
|
|
2000
|
+
throw new error_1.CloudBaseError(`[${func.name}] COS 上传失败: ${e.message}`, {
|
|
2001
|
+
code: errorCode,
|
|
2002
|
+
original: e
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
return null;
|
|
2006
|
+
});
|
|
2007
|
+
if (!response) {
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
if (response.ok) {
|
|
2011
|
+
return { Key: ObjectKey };
|
|
2012
|
+
}
|
|
2013
|
+
let errorDetail = '';
|
|
2014
|
+
try {
|
|
2015
|
+
errorDetail = await response.text();
|
|
2016
|
+
}
|
|
2017
|
+
catch (e) {
|
|
2018
|
+
// 忽略响应体读取失败
|
|
2019
|
+
}
|
|
2020
|
+
const message = `COS 上传失败: ${response.status} ${response.statusText}${errorDetail ? ` - ${errorDetail}` : ''}`;
|
|
2021
|
+
// 403(PutUrl 过期)/ 5xx 可刷新地址重试;其余 4xx 直接失败
|
|
2022
|
+
const retryable = response.status === 403 || response.status >= 500;
|
|
2023
|
+
if (!retryable || attempt === maxAttempts) {
|
|
2024
|
+
// HTTP 状态码属平台返回,不作为本层自有码,保留在 message 中
|
|
2025
|
+
(0, error_1.throwApiError)(message);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
/* istanbul ignore next */
|
|
2029
|
+
(0, error_1.throwApiError)(`[${func.name}] COS 上传失败`);
|
|
2030
|
+
}
|
|
2031
|
+
finally {
|
|
2032
|
+
// 4. 清理临时 zip
|
|
2033
|
+
await packer.clean();
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* 对 PutUrl 发起 HTTP PUT 上传 zip 文件(超时 120 秒,支持 http_proxy 代理)
|
|
2038
|
+
* @private
|
|
2039
|
+
*/
|
|
2040
|
+
async putZipFileToUrl(putUrl, zipFilePath) {
|
|
2041
|
+
return (0, utils_1.fetchStream)(putUrl, {
|
|
2042
|
+
method: 'PUT',
|
|
2043
|
+
body: fs_1.default.createReadStream(zipFilePath),
|
|
2044
|
+
headers: {
|
|
2045
|
+
'Content-Type': 'application/zip'
|
|
2046
|
+
},
|
|
2047
|
+
timeout: 120000
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
1703
2050
|
async createAccessPath(name, path) {
|
|
1704
2051
|
const access = this.environment.getAccessService();
|
|
1705
2052
|
try {
|
|
@@ -1736,20 +2083,25 @@ class FunctionService {
|
|
|
1736
2083
|
ObjectPath: `${appId}/${env}/${name}.zip"`
|
|
1737
2084
|
});
|
|
1738
2085
|
}
|
|
2086
|
+
/**
|
|
2087
|
+
* 创建触发器(带重试)。
|
|
2088
|
+
*
|
|
2089
|
+
* ⚠️ AIP-194:**只重试可重试错误**。旧实现是 catch-all 盲重试 —— 权限不足 / 参数非法这类
|
|
2090
|
+
* **必然失败**的错误也会被重试 3 次,既浪费时间,又把确定性失败伪装成偶发失败、掩盖真实原因。
|
|
2091
|
+
*/
|
|
1739
2092
|
async retryCreateTrigger(name, triggers, count = 0) {
|
|
1740
2093
|
try {
|
|
1741
2094
|
const res = await this.createFunctionTriggers(name, triggers);
|
|
1742
2095
|
return res;
|
|
1743
2096
|
}
|
|
1744
2097
|
catch (e) {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
const res = await this.retryCreateTrigger(name, triggers, count + 1);
|
|
1748
|
-
return res;
|
|
1749
|
-
}
|
|
1750
|
-
else {
|
|
2098
|
+
// 不可重试(权限 / 参数 / 资源冲突等)或已达上限 → 立即抛出真实错误
|
|
2099
|
+
if (!isRetryableError(e) || count >= 3) {
|
|
1751
2100
|
throw e;
|
|
1752
2101
|
}
|
|
2102
|
+
console.warn(`[${name}] 创建触发器遇到可重试错误(第 ${count + 1}/3 次),500ms 后重试:${pickStringField(e, 'message') || String(e)}`);
|
|
2103
|
+
await (0, utils_1.sleep)(500);
|
|
2104
|
+
return this.retryCreateTrigger(name, triggers, count + 1);
|
|
1753
2105
|
}
|
|
1754
2106
|
}
|
|
1755
2107
|
/**
|
|
@@ -1924,12 +2276,21 @@ __decorate([
|
|
|
1924
2276
|
__decorate([
|
|
1925
2277
|
(0, utils_1.preLazy)()
|
|
1926
2278
|
], FunctionService.prototype, "listVersionByFunction", null);
|
|
2279
|
+
__decorate([
|
|
2280
|
+
(0, utils_1.preLazy)()
|
|
2281
|
+
], FunctionService.prototype, "deleteFunctionVersion", null);
|
|
1927
2282
|
__decorate([
|
|
1928
2283
|
(0, utils_1.preLazy)()
|
|
1929
2284
|
], FunctionService.prototype, "updateFunctionAliasConfig", null);
|
|
2285
|
+
__decorate([
|
|
2286
|
+
(0, utils_1.preLazy)()
|
|
2287
|
+
], FunctionService.prototype, "updateFunctionAliasConfigV2", null);
|
|
1930
2288
|
__decorate([
|
|
1931
2289
|
(0, utils_1.preLazy)()
|
|
1932
2290
|
], FunctionService.prototype, "getFunctionAlias", null);
|
|
2291
|
+
__decorate([
|
|
2292
|
+
(0, utils_1.preLazy)()
|
|
2293
|
+
], FunctionService.prototype, "getFunctionAliasV2", null);
|
|
1933
2294
|
__decorate([
|
|
1934
2295
|
(0, utils_1.preLazy)()
|
|
1935
2296
|
], FunctionService.prototype, "uploadFunctionZipToCos", null);
|
|
@@ -1939,6 +2300,9 @@ __decorate([
|
|
|
1939
2300
|
__decorate([
|
|
1940
2301
|
(0, utils_1.preLazy)()
|
|
1941
2302
|
], FunctionService.prototype, "getCodeParams", null);
|
|
2303
|
+
__decorate([
|
|
2304
|
+
(0, utils_1.preLazy)()
|
|
2305
|
+
], FunctionService.prototype, "uploadFunctionZipViaPutUrl", null);
|
|
1942
2306
|
__decorate([
|
|
1943
2307
|
(0, utils_1.preLazy)()
|
|
1944
2308
|
], FunctionService.prototype, "createAccessPath", null);
|