import { utils } from '@cloudbase/utilities' import { ReadableStream } from 'web-streams-polyfill' import { ERROR, checkCustomUserIdRegex } from './constants' import { lookupAppId } from './metadata' /** 从 @cloudbase/utilities 中提取的表单和查询字符串工具 */ export const { isFormData, toQueryString } = utils as { isFormData: (obj: unknown) => boolean toQueryString: (data: Record) => string } /** * 跨端安全获取环境变量 * Web 端、小游戏端等没有 process 全局对象,直接访问 process.env 会报错 */ export function getEnv(): Record export function getEnv(key: string): string | undefined export function getEnv(key?: string): string | undefined | Record { if (typeof process === 'undefined' || !process.env) { return key !== undefined ? undefined : {} } return key !== undefined ? process.env[key] : process.env } /** 判断当前是否在 SCF(云函数)运行环境中 */ export const checkIsInScf = () => getEnv('TENCENTCLOUD_RUNENV') === 'SCF' export function checkIsInCBR() { // CBR = CLOUDBASE_RUN return !!getEnv('CBR_ENV_ID') } const kSumeruEnvSet = new Set(['formal', 'pre', 'test']) export function checkIsInSumeru() { // SUMERU_ENV=formal | test | pre return kSumeruEnvSet.has(getEnv('SUMERU_ENV') || '') } export function isNonEmptyString(str: string) { return typeof str === 'string' && str !== '' } export async function checkIsInTencentCloud(): Promise { if (getEnv('TENCENTCLOUD') === 'true') { return true } return isNonEmptyString(await lookupAppId()) } export const getCurrRunEnvTag = async (): Promise => { if (checkIsInScf()) { return 'scf' } if (checkIsInCBR()) { return 'cbr' } if (checkIsInSumeru()) { return 'sumeru' } if (await checkIsInTencentCloud()) { return 'tencentcloud' } return 'unknown' } /** * 校验自定义用户 ID 格式 * @throws 格式不合法时抛出 INVALID_PARAM 错误 */ export const validateUid = (uid: string): void => { if (typeof uid !== 'string') { throw { ...ERROR.INVALID_PARAM, message: 'uid must be a string' } } if (!checkCustomUserIdRegex.test(uid)) { throw { ...ERROR.INVALID_PARAM, message: `Invalid uid: "${uid}"` } } } /** * 安全的 JSON 解析,解析失败时返回原始文本而非抛异常 * @param text - 待解析的字符串 * @returns 解析后的对象,空字符串返回 null,解析失败返回原始文本 */ export function safeParseJson(text: string): any { if (!text || !text.trim()) { return null } try { return JSON.parse(text) } catch (e) { console.warn('catch an error', { e, text }) return text } } /** * 将对象的所有值转为字符串类型 * 用于将 headers 等对象统一为 Record */ export function obj2StrRecord(obj: object): Record { return Object.entries(obj).reduce>((acc, cur: [string, unknown]) => { const [key, value] = cur acc[key] = String(value) return acc }, {}) } /** * 将 HeadersInit 转为可索引的普通对象 * 兼容 Headers 实例和普通对象两种形式 */ export function headersInit2Indexable(h: HeadersInit) { if (isHeaders(h)) { const ret: Record = {} h.forEach((val, key) => { ret[key] = val }) return ret } return h function isHeaders(h: HeadersInit): h is Headers { try { // Node 低版本可能没有全局 Headers return h instanceof Headers } catch (_) { return false } } } /** * 手动解析查询字符串,避免 URLSearchParams 的兼容性问题 * 支持重复 key 自动转为数组 * @param search - 查询字符串,可带前导 ? 或 # */ export function parseQueryString(search: string): Record { const params: any = {} const cleanSearch = search.replace(/^[?#]/, '') if (!cleanSearch) { return params } const pairs = cleanSearch.split('&') pairs.forEach((item) => { let [key, value] = item.split('=') key = decodeURIComponent(key) value = decodeURIComponent(value || '') if (key) { if (params[key]) { // 已存在同名 key,转为数组 if (Array.isArray(params[key])) { params[key].push(value) } else { params[key] = [params[key], value] } } else { params[key] = value } } }) return params } /** * 将 Node ReadableStream 转换为 Web ReadableStream (polyfill) * 用于 fetch 流式响应在 Node 环境下的适配 */ export function createWebStreamFromNodeReadableStream(stream: NodeJS.ReadableStream): ReadableStream { const asyncIterator = stream[Symbol.asyncIterator]() return new ReadableStream({ async pull(controller) { const next = await asyncIterator.next() if (next.done) { controller.close() } else { controller.enqueue(next.value) } }, }) }