import { ICloudbase } from '@cloudbase/types' import { ICloudbaseComponent } from '@cloudbase/types/component' import { ICallFunctionOptions, ICustomReqOpts } from '@cloudbase/types/functions' import { constants, utils, helpers } from '@cloudbase/utilities' import { requestContainer } from '@cloudbase/cloudrun' declare const cloudbase: ICloudbase const { getSdkName, ERRORS, COMMUNITY_SITE_URL } = constants const { execCallback, maybeThrow } = utils const { catchErrorsDecorator } = helpers const COMPONENT_NAME = 'functions' const GATEWAY_ERROR_MESSAGE_MAX = 1024 function truncateGatewayErrorMessage(text: string): string { if (text.length <= GATEWAY_ERROR_MESSAGE_MAX) { return text } return `${text.slice(0, GATEWAY_ERROR_MESSAGE_MAX)}…` } function fromJsonGatewayError(data: any, requestId?: string) { return { code: data?.code || ERRORS.NETWORK_ERROR, message: data?.message || data?.msg || '', requestId, } } /** GATEWAY 失败 body:JSON 对象 / JSON 字符串 / HTML / 纯文本 → { code, message, requestId } */ function normalizeGatewayError(data: any, requestId?: string) { if (data && typeof data === 'object' && !Array.isArray(data)) { return fromJsonGatewayError(data, requestId) } if (typeof data === 'string') { try { const parsed = JSON.parse(data) if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { return fromJsonGatewayError(parsed, requestId) } } catch (e) { // HTML / nginx 502 / 纯文本 } return { code: ERRORS.NETWORK_ERROR, message: truncateGatewayErrorMessage(data), requestId, } } return { code: ERRORS.NETWORK_ERROR, message: truncateGatewayErrorMessage(data === null || data === undefined ? '' : String(data)), requestId, } } function isGatewayBodyParseError(err: any): boolean { if (!err) { return false } if (err instanceof SyntaxError || err.name === 'SyntaxError') { return true } const msg = String(err.message || '') return /not valid JSON|Unexpected token|JSON\.parse|Failed to execute 'json'/i.test(msg) } function isWedaFunc(functionName: string): boolean { // 微搭运行态链路改造 // https://iwiki.woa.com/pages/viewpage.action?pageId=1315543040 // lcap-common-service(合并前,代理除了数据源之外的所有请求) // lcap-data-* (合并前,数据源的云函数,每个数据源两个,包括预览和发布) // lcap-datasource-test (合并前,数据源测试用的云函数) // lowcode-datasource* (合并后,前面三个的合体,共有两个,预览和发布) return ( functionName === 'lcap-common-service' || functionName === 'lcap-datasource-test' || functionName.startsWith('lcap-data') || functionName.startsWith('lowcode-datasource') || functionName === 'cloudbase_module' ) } function isCloudBackendFunc(functionName: string): boolean { // 云后台运行态链路 // 包含工作流的调用 return functionName === 'lowcode-wx-datasource' } function shouldUseGateway(config: any, functionName: string): boolean { return config.endPointMode === 'GATEWAY' && !isWedaFunc(functionName) && !isCloudBackendFunc(functionName) } class CloudbaseFunctions { /** 云函数 2.0 / 云托管:`type: 'cloudrun'` */ private static async invokeCloudrun(app: any, options: ICallFunctionOptions, jsonData: string) { const { name, path = '', method, header = {} } = options const requestId = utils.generateRequestId() const result = await requestContainer.call(app, { name, data: jsonData as any, path, method, header: { ...header, 'X-Request-Id': requestId }, }) return { result, requestId, } } /** GATEWAY:`POST /v1/functions/:name`,按 statusCode 判断成败 */ private static async invokeGateway( app: any, options: ICallFunctionOptions, callback?: Function, customReqOpts?: ICustomReqOpts, ) { const { request, config } = app let response: any try { response = await request.gateWay({ ...options, path: 'functions', // 非 2xx 也要拿到 statusCode,由本层按 throw 开关处理 shouldThrowOnError: false, }, customReqOpts) } catch (e) { const err: any = e // content-type 标成 JSON 的 HTML 等,body 解析抛错 if (isGatewayBodyParseError(err)) { return maybeThrow(config, normalizeGatewayError(err.message, err.requestId)) } return execCallback(callback, err) } if (Number(response?.statusCode) >= 400) { return maybeThrow(config, normalizeGatewayError(response?.data, response?.requestId)) } return execCallback(callback, null, { requestId: response?.requestId, result: response?.data, }) } /** CLOUD_API:`functions.invokeFunction` */ private static async invokeCloudApi( app: any, options: ICallFunctionOptions, jsonData: string, callback?: Function, customReqOpts?: ICustomReqOpts, ) { const { name, data, query, parse, search } = options const res = await app.request.send( 'functions.invokeFunction', { inQuery: query, parse, search, function_name: name, request_data: jsonData, }, { endPointMode: 'CLOUD_API', defaultQuery: data?.params?.action ? { action: data?.params?.action } : {}, }, customReqOpts, ) let result = res.data?.response_data if (result === undefined) { return execCallback(callback, null, res) } // parse 为 true 时服务端解析 JSON,SDK 不再次解析 if (parse) { return execCallback(callback, null, { result, requestId: res.requestId, }) } try { result = JSON.parse(result) return execCallback(callback, null, { result, requestId: res.requestId, }) } catch (e) { return execCallback( callback, new Error(`[${getSdkName()}][${ERRORS.INVALID_PARAMS}][${COMPONENT_NAME}.callFunction] response data must be json`), ) } } @catchErrorsDecorator({ customInfo: { className: 'Cloudbase', methodName: 'callFunction', }, title: '函数调用失败', messages: [ '请确认以下各项:', ' 1 - 调用 callFunction() 的语法或参数是否正确', ' 2 - 当前环境下是否存在此函数', ' 3 - 函数安全规则是否限制了当前登录状态访问', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async callFunction(options: ICallFunctionOptions, callback?: Function, customReqOpts?: ICustomReqOpts) { const { name, data, type } = options // @ts-ignore — 组件模式下 this 为 cloudbase 实例,运行时才具备 config/request const { config } = this if (!name) { return maybeThrow(config, { code: ERRORS.INVALID_PARAMS, message: `[${COMPONENT_NAME}.callFunction] invalid function name`, }) } let jsonData try { jsonData = data ? JSON.stringify(data) : '' } catch (e) { return maybeThrow(config, { code: ERRORS.INVALID_PARAMS, message: `[${COMPONENT_NAME}.callFunction] invalid data`, }) } if (type === 'cloudrun') { return CloudbaseFunctions.invokeCloudrun(this, options, jsonData) } try { if (shouldUseGateway(config, name)) { return await CloudbaseFunctions.invokeGateway(this, options, callback, customReqOpts) } return await CloudbaseFunctions.invokeCloudApi(this, options, jsonData, callback, customReqOpts) } catch (e) { execCallback(callback, e) } } } const cloudbaseFunctions = new CloudbaseFunctions() const component: ICloudbaseComponent = { name: COMPONENT_NAME, entity: { callFunction: cloudbaseFunctions.callFunction, }, } try { cloudbase.registerComponent(component) } catch (e) {} export function registerFunctions(app: Pick) { try { app.registerComponent(component) } catch (e) { console.warn(e) } }