/** * Cloud function: jimengManager * * Secure proxy for Jimeng generation endpoints. The browser calls this cloud * function with { action: 'call', endpoint, payload }, and the function injects * the server-side token before forwarding to JIMENG_BASE_URL. * * 支持 action: * call -> proxy a whitelisted Jimeng endpoint * getWorkResult -> read Parse ImagineWork by workId/objectId * diagnose -> non-secret deployment diagnostics */ const JIMENG_ALLOWED_ENDPOINTS = new Set([ 'getClothesV2', 'getImgByImg', 'getImgV4', 'getText2ImgV3', 'getText2ImgV31', 'getImg2ImgV3', 'getImgV4_pod', 'getImgV4_goods', 'getInpaint', 'getSuperResolution', 'getVideoV3_720p', 'getVideoV3_1080p', 'getVideoV3_Pro', 'getActor', 'getActorV2', 'getDataByTask02', 'getOhIdentifyMain', 'getOhDateByTask', 'getOhDetectMain', 'getOmniHuman', ]); const JIMENG_BASE_URL = (readEnv('JIMENG_BASE_URL') || 'https://server.fmode.cn/api/volcengine/jimeng').replace(/\/+$/, ''); const PARSE_API_HOST = normalizeParseApiHost(readEnv('PARSE_API_HOST') || readEnv('PARSE_BASE_URL') || 'https://server.fmode.cn/api'); const PARSE_APP_ID = readEnv('PARSE_APP_ID') || readEnv('PARSE_APPLICATION_ID') || readEnv('X_PARSE_APPLICATION_ID') || 'ncloudmaster'; const JIMENG_MAX_ATTEMPTS = Math.max(1, Number(readEnv('JIMENG_MAX_ATTEMPTS') || readEnv('JIMENG_LOCAL_MAX_ATTEMPTS') || 3)); const JIMENG_TOKEN_FALLBACK = 'Bearer r:f0333969e312a40e4703e8fe4ed1c600'; const VIDEO_WORKFLOW_APIG_ID = '6pFf6EAdKT'; const DEFAULT_APIG_UNIT_PRICE_CNY = 0.1; const JIMENG_PRICE_CNY = { getImgV4: { unit: 'image', cny: 0.22 }, getText2ImgV3: { unit: 'call', cny: 0.2 }, getText2ImgV31: { unit: 'call', cny: 0.2 }, getImg2ImgV3: { unit: 'call', cny: 0.2 }, getImgV4_pod: { unit: 'image', cny: 0.22 }, getImgV4_goods: { unit: 'image', cny: 0.22 }, getInpaint: { unit: 'call', cny: 0.2 }, getSuperResolution: { unit: 'call', cny: 0.4 }, getVideoV3_720p: { unit: 'second', cny: 0.28 }, getVideoV3_1080p: { unit: 'second', cny: 0.63 }, getVideoV3_Pro: { unit: 'second', cny: 1 }, getActor: { unit: 'second', cny: 0.5 }, getActorV2: { unit: 'second', cny: 0.4 }, getOmniHuman: { unit: 'second', cny: 1 }, }; const JIMENG_QUERY_ENDPOINTS = new Set(['getDataByTask02', 'getOhDateByTask', 'getWorkResult']); async function handler(request, response) { try { const action = String(pickParam(request, 'action') || 'call').trim(); if (action === 'diagnose') { return response.json({ code: 200, success: true, data: { baseUrl: maskUrl(JIMENG_BASE_URL), parseApiHost: maskUrl(PARSE_API_HOST), parseAppIdConfigured: !!PARSE_APP_ID, tokenConfigured: !!readJimengToken(), tokenSource: jimengTokenSource(), allowedEndpoints: Array.from(JIMENG_ALLOWED_ENDPOINTS).sort(), maxAttempts: JIMENG_MAX_ATTEMPTS, }, }); } if (action === 'getWorkResult' || action === 'work') { const workId = String(pickParam(request, 'workId', 'objectId', 'id') || '').trim(); if (!workId) { return response.json({ code: 400, success: false, error: 'Missing workId' }); } const result = await getWorkResult(workId); return response.json(result); } if (action !== 'call') { return response.json({ code: 400, success: false, error: `Unknown action: ${action}` }); } const endpoint = String(pickParam(request, 'endpoint', 'routerName', 'route') || '').trim(); if (!JIMENG_ALLOWED_ENDPOINTS.has(endpoint)) { return response.json({ code: 400, success: false, error: `Unsupported Jimeng endpoint: ${endpoint || '(empty)'}` }); } const token = normalizeBearerToken(pickParam(request, 'token') || readJimengToken()); if (!token) { return response.json({ code: 400, success: false, error: 'Jimeng token is not configured. Set JIMENG_TOKEN, VOLC_JIMENG_TOKEN, VOICE_TOKEN, VOC_TOKEN, or VOLC_TOKEN, or update JIMENG_TOKEN_FALLBACK.', }); } const rawPayload = pickParam(request, 'payload', 'data') || request.body || {}; const payload = normalizeJimengPayload(endpoint, rawPayload, token); const sessionToken = String(pickParam(request, 'sessionToken') || '').trim(); const idempotencyKey = String(pickParam(request, 'idempotencyKey') || rawPayload.idempotencyKey || rawPayload.generationTaskId || '').trim(); const data = await requestJimengWithBilling({ endpoint, payload, sessionToken, idempotencyKey }); return response.json(data); } catch (error) { console.error('jimengManager failed:', error && error.message ? error.message : error); return response.json({ code: error && error.status ? error.status : 500, success: false, error: error && error.message ? error.message : 'Jimeng service call failed', detail: error && error.detail ? error.detail : '', upstream: error && error.upstream ? error.upstream : undefined, }); } } function normalizeJimengPayload(endpoint, input, token) { const payload = { ...(input || {}) }; delete payload.action; delete payload.endpoint; delete payload.route; delete payload.data; delete payload.payload; if (endpoint === 'getDataByTask02' || endpoint === 'getOhDateByTask') { payload.routerName = payload.routerName || payload.endpoint || payload.routeName; } if (endpoint === 'getDataByTask02' && !payload.routerName) { payload.routerName = 'getImgV4'; } if (endpoint === 'getOhDateByTask' && !payload.routerName) { payload.routerName = 'getOmniHuman'; } if (payload.task_id && !payload.taskId) payload.taskId = payload.task_id; if (payload.objectId && !payload.workId) payload.workId = payload.objectId; if (endpoint === 'getVideoV3_Pro' && Array.isArray(payload.image_urls) && payload.image_urls[0] && !payload.image_url) { payload.image_url = payload.image_urls[0]; delete payload.image_urls; } payload.token = token; return stripEmpty(payload); } function billingFramesToSeconds(frames) { const raw = Number(frames || 121); return raw >= 241 ? 10 : 5; } function billingMediaSeconds(endpoint, payload) { if (endpoint === 'getActor' || endpoint === 'getActorV2') { return Math.max(1, Math.ceil(Number(payload.durationSeconds || payload.duration || 5))); } if (endpoint === 'getOmniHuman') { return Math.max(1, Math.ceil(Number(payload.durationSeconds || payload.audioDurationSeconds || payload.duration || 5))); } return billingFramesToSeconds(payload.frames); } function billingImageQuantity(payload) { if (payload.force_single === false || payload.forceSingle === false) return 2; return Math.max(1, Math.ceil(Number(payload.quantity || payload.count || 1))); } function estimateJimengBilling(endpoint, payload, unitPriceCny) { if (JIMENG_QUERY_ENDPOINTS.has(endpoint)) return null; const rule = JIMENG_PRICE_CNY[endpoint]; if (!rule) return null; const price = Math.max(0.01, Number(unitPriceCny || DEFAULT_APIG_UNIT_PRICE_CNY)); const body = payload || {}; const quantity = rule.unit === 'second' ? billingMediaSeconds(endpoint, body) : rule.unit === 'image' ? billingImageQuantity(body) : 1; const costCny = Math.round(rule.cny * quantity * 100) / 100; return { endpoint, unit: rule.unit, quantity, costCny, unitPriceCny: price, credits: Math.max(1, Math.ceil(costCny / price)), }; } function sanitizeBillingSnapshot(payload) { const copy = { ...(payload || {}) }; delete copy.token; delete copy.apiKey; delete copy.apiSecret; delete copy.binary_data_base64; return copy; } function stableJson(value) { if (value === null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`; return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`; } async function requestJimengWithBilling({ endpoint, payload, sessionToken, idempotencyKey }) { const initialBilling = estimateJimengBilling(endpoint, payload, DEFAULT_APIG_UNIT_PRICE_CNY); if (!initialBilling) { return requestJimengWithRetry(endpoint, payload); } const user = await verifyBillingUser(sessionToken); const auth = await ensureVideoWorkflowAuth(user.objectId, sessionToken); const apigPayInfo = auth && auth.objectId ? await fetchApigPayInfo(auth.objectId).catch(() => null) : null; const billingAuth = mergeApigPayBalance(auth, apigPayInfo); const unitPriceCny = Number( billingAuth && billingAuth.api && billingAuth.api.price ? billingAuth.api.price : apigPayInfo && apigPayInfo.price ? apigPayInfo.price : DEFAULT_APIG_UNIT_PRICE_CNY ); const billing = estimateJimengBilling(endpoint, payload, unitPriceCny); const stableKey = idempotencyKey || `jimeng:${endpoint}:${user.objectId}:${stableJson(sanitizeBillingSnapshot(payload))}`; const lastBilling = billingAuth && billingAuth.lastBilling && typeof billingAuth.lastBilling === 'object' ? billingAuth.lastBilling : null; if (lastBilling && lastBilling.idempotencyKey === stableKey) { if (lastBilling.workId) { return { code: 200, success: true, data: { workId: lastBilling.workId, billing: lastBilling, reused: true, }, }; } if (lastBilling.status === 'reserved') { const error = new Error('上一条相同即梦生成请求仍在处理中,请稍后再试'); error.status = 409; error.detail = { idempotencyKey: stableKey, lastBilling }; throw error; } } const reservation = await reserveJimengCredits({ user, auth: billingAuth, billing, endpoint, payload, idempotencyKey: stableKey, sessionToken, }); try { const data = await requestJimengWithRetry(endpoint, payload); const workId = extractJimengWorkId(data); if (!workId) { await refundJimengCredits({ user, auth: billingAuth, reservation, reason: 'Jimeng submit returned no workId', sessionToken, }); return data; } const submittedBilling = { ...reservation.lastBilling, status: 'submitted', workId, responseSnapshot: sanitizeBillingSnapshot(data), updatedAt: new Date().toISOString(), }; await updateApigAuthBilling(billingAuth, { lastBilling: submittedBilling }, sessionToken); return { ...data, billing: { costCredits: billing.credits, costCny: billing.costCny, balanceAfter: reservation.nextCount, workId, }, }; } catch (error) { await refundJimengCredits({ user, auth: billingAuth, reservation, reason: error && error.message ? error.message : 'Jimeng submit failed before workId', sessionToken, }); throw error; } } function extractJimengWorkId(data) { return data && ( data.workId || data.taskId || data.objectId || data.data && (data.data.workId || data.data.taskId || data.data.objectId) || data.result && (data.result.workId || data.result.taskId || data.result.objectId) ) || ''; } async function parseRequest(method, path, body, sessionToken) { const base = String(PARSE_API_HOST || '').replace(/\/+$/, '').replace(/\/parse$/i, ''); const url = `${base}/parse${path}`; const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Parse-Application-Id': PARSE_APP_ID, }; if (sessionToken) headers['X-Parse-Session-Token'] = sessionToken; const init = { method, headers }; if (body !== undefined) init.body = JSON.stringify(body); const { status, ok, rawText, data } = await requestJson(method, url, init); if (!ok) { const error = new Error(readErrorMessage(data, rawText) || `Parse ${method} ${path} failed`); error.status = status || 500; error.detail = data || rawText || ''; throw error; } return data; } async function verifyBillingUser(sessionToken) { if (!sessionToken) { const error = new Error('请先登录后再生成'); error.status = 401; throw error; } return parseRequest('GET', '/users/me?include=company', undefined, sessionToken); } async function findVideoWorkflowAuth(userId, sessionToken) { const where = encodeURIComponent(JSON.stringify({ api: { __type: 'Pointer', className: 'APIG', objectId: VIDEO_WORKFLOW_APIG_ID }, user: { __type: 'Pointer', className: '_User', objectId: userId }, })); const data = await parseRequest('GET', `/classes/APIGAuth?where=${where}&include=api&limit=1`, undefined, sessionToken); return Array.isArray(data.results) ? data.results[0] || null : null; } async function ensureVideoWorkflowAuth(userId, sessionToken) { const existing = await findVideoWorkflowAuth(userId, sessionToken); if (existing) return existing; const body = { api: { __type: 'Pointer', className: 'APIG', objectId: VIDEO_WORKFLOW_APIG_ID }, user: { __type: 'Pointer', className: '_User', objectId: userId }, count: 0, used: 0, }; const created = await parseRequest('POST', '/classes/APIGAuth', body, sessionToken); return { ...body, ...created }; } async function fetchApigPayInfo(authId) { const base = String(PARSE_API_HOST || '').replace(/\/+$/, '').replace(/\/parse$/i, ''); const { ok, data } = await requestJson('POST', `${base}/api/apig/getApig`, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ authid: authId }), }); if (!ok || !data || Number(data.code) !== 200 || !data.data) return null; return data.data; } function mergeApigPayBalance(auth, apigPayInfo) { if (!apigPayInfo || typeof apigPayInfo !== 'object') return auth; const count = apigPayInfo.count !== undefined ? Number(apigPayInfo.count) : Number(auth && auth.count || 0); const used = apigPayInfo.used !== undefined ? Number(apigPayInfo.used) : Number(auth && auth.used || 0); const api = { ...(auth && auth.api && typeof auth.api === 'object' ? auth.api : {}), objectId: apigPayInfo.objectId || apigPayInfo.api && apigPayInfo.api.objectId || auth && auth.api && auth.api.objectId || VIDEO_WORKFLOW_APIG_ID, title: apigPayInfo.title || apigPayInfo.api && apigPayInfo.api.title || auth && auth.api && auth.api.title, price: apigPayInfo.price || apigPayInfo.api && apigPayInfo.api.price || auth && auth.api && auth.api.price, }; return { ...(auth || {}), count: Number.isFinite(count) ? count : Number(auth && auth.count || 0), used: Number.isFinite(used) ? used : Number(auth && auth.used || 0), api, }; } async function updateApigAuthBilling(auth, patch, sessionToken) { return parseRequest('PUT', `/classes/APIGAuth/${encodeURIComponent(auth.objectId)}`, patch, sessionToken); } async function reserveJimengCredits({ auth, billing, endpoint, payload, idempotencyKey, sessionToken }) { const oldCount = Number(auth.count || 0); const oldUsed = Number(auth.used || 0); if (oldCount < billing.credits) { const error = new Error(`余额不足:当前 ${oldCount},本次需要 ${billing.credits}`); error.status = 402; throw error; } const nextCount = oldCount - billing.credits; const nextUsed = oldUsed + billing.credits; const lastBilling = { kind: 'jimeng_consume', status: 'reserved', endpoint, costCredits: billing.credits, costCny: billing.costCny, unitPriceCny: billing.unitPriceCny, quantity: billing.quantity, unit: billing.unit, idempotencyKey, balanceBefore: oldCount, balanceAfter: nextCount, usedBefore: oldUsed, usedAfter: nextUsed, requestSnapshot: sanitizeBillingSnapshot(payload), updatedAt: new Date().toISOString(), }; await updateApigAuthBilling(auth, { count: nextCount, used: nextUsed, lastBilling }, sessionToken); return { oldCount, oldUsed, nextCount, nextUsed, lastBilling }; } async function refundJimengCredits({ user, auth, reservation, reason, sessionToken }) { const latest = await findVideoWorkflowAuth(user.objectId, sessionToken); const current = latest || auth; const currentCount = Number(current.count || 0); const currentUsed = Number(current.used || 0); const refundCredits = Number(reservation.lastBilling.costCredits || 0); const nextCount = currentCount + refundCredits; const nextUsed = Math.max(0, currentUsed - refundCredits); await updateApigAuthBilling(current, { count: nextCount, used: nextUsed, lastBilling: { ...reservation.lastBilling, status: 'refunded', refundReason: reason, balanceAfter: nextCount, usedAfter: nextUsed, updatedAt: new Date().toISOString(), }, }, sessionToken); } async function requestJimengWithRetry(endpoint, payload) { let lastError = null; const url = `${JIMENG_BASE_URL}/${encodeURIComponent(endpoint)}`; for (let attempt = 1; attempt <= JIMENG_MAX_ATTEMPTS; attempt += 1) { try { const { status, ok, rawText, data, transport } = await postJson(url, payload); const code = Number(data && data.code ? data.code : status || 0); const retryable = !ok || code === 408 || code === 429 || code >= 500; if (!retryable || attempt >= JIMENG_MAX_ATTEMPTS) { if (!ok) { const error = new Error(readErrorMessage(data, rawText) || `Jimeng HTTP ${status}`); error.status = status || code || 500; error.detail = data || rawText || ''; error.upstream = { endpoint, status, transport, attempt, maxAttempts: JIMENG_MAX_ATTEMPTS }; throw error; } if (!data) { return { code: 500, success: false, error: 'Jimeng returned an empty response' }; } return data; } } catch (error) { lastError = error; if (attempt >= JIMENG_MAX_ATTEMPTS) break; } await sleep(Math.min(12000, 1200 * attempt * attempt)); } const error = new Error(`Jimeng upstream request failed: ${formatFetchError(lastError)}; endpoint=${endpoint}`); error.status = (lastError && lastError.status) || 500; error.detail = (lastError && lastError.detail) || ''; error.upstream = { endpoint, attempt: JIMENG_MAX_ATTEMPTS, maxAttempts: JIMENG_MAX_ATTEMPTS }; throw error; } async function getWorkResult(workId) { const urls = buildParseClassUrls('ImagineWork', workId); const headers = { 'X-Parse-Application-Id': PARSE_APP_ID }; let lastError = null; for (let attempt = 1; attempt <= JIMENG_MAX_ATTEMPTS; attempt += 1) { for (const url of urls) { try { const { status, ok, rawText, data } = await getJson(url, headers); const wrongRoute = status === 404 && /Cannot\s+GET\s+\/api\/parse\/classes/i.test(rawText || ''); const retryable = !ok && !wrongRoute && (status === 408 || status === 429 || status >= 500); if (ok) { return { code: 200, success: true, data }; } if (wrongRoute && urls.length > 1) { lastError = new Error(`Wrong Parse route: ${url}`); continue; } if (!retryable || attempt >= JIMENG_MAX_ATTEMPTS) { return { code: status || 500, success: false, error: readErrorMessage(data, rawText) || `Failed to query Jimeng work result from ${maskUrl(url)}`, }; } } catch (error) { lastError = error; if (attempt >= JIMENG_MAX_ATTEMPTS && url === urls[urls.length - 1]) break; } } await sleep(Math.min(12000, 1200 * attempt * attempt)); } return { code: 500, success: false, error: `Failed to query Jimeng work result: ${formatFetchError(lastError)}`, }; } async function postJson(url, body) { return requestJson('POST', url, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(body || {}), }); } async function getJson(url, headers) { return requestJson('GET', url, { headers: { 'Accept': 'application/json', ...(headers || {}) }, }); } async function requestJson(method, url, init) { let lastError = null; if (typeof fetch === 'function') { try { const response = await fetch(url, { method, ...(init || {}) }); return readResponse(response, 'fetch'); } catch (error) { lastError = error; } } if (typeof require === 'function') { try { return await requestJsonWithNodeHttps(method, url, init); } catch (error) { lastError = error; } } if (typeof XMLHttpRequest !== 'undefined') { try { return await requestJsonWithXhr(method, url, init); } catch (error) { lastError = error; } } throw lastError || new Error('No HTTP client is available in this cloud runtime'); } async function readResponse(response, transport) { const rawText = await response.text(); let data = null; try { data = rawText ? JSON.parse(rawText) : null; } catch {} return { status: response.status, ok: !!response.ok, rawText, data, transport, }; } function requestJsonWithNodeHttps(method, url, init) { return new Promise((resolve, reject) => { try { const parsed = new URL(url); const lib = parsed.protocol === 'http:' ? require('http') : require('https'); const headers = init && init.headers ? init.headers : {}; const body = init && init.body ? init.body : ''; const req = lib.request({ method, protocol: parsed.protocol, hostname: parsed.hostname, port: parsed.port, path: `${parsed.pathname}${parsed.search}`, headers: body ? { ...headers, 'Content-Length': Buffer.byteLength(body) } : headers, }, (res) => { const chunks = []; res.on('data', (chunk) => chunks.push(chunk)); res.on('end', () => { const rawText = Buffer.concat(chunks).toString('utf8'); let data = null; try { data = rawText ? JSON.parse(rawText) : null; } catch {} resolve({ status: res.statusCode || 0, ok: res.statusCode >= 200 && res.statusCode < 300, rawText, data, transport: 'node-https', }); }); }); req.on('error', reject); if (body) req.write(body); req.end(); } catch (error) { reject(error); } }); } function requestJsonWithXhr(method, url, init) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(method, url, true); const headers = init && init.headers ? init.headers : {}; for (const [key, value] of Object.entries(headers)) { xhr.setRequestHeader(key, value); } xhr.onreadystatechange = function onReadyStateChange() { if (xhr.readyState !== 4) return; let data = null; try { data = xhr.responseText ? JSON.parse(xhr.responseText) : null; } catch {} resolve({ status: xhr.status, ok: xhr.status >= 200 && xhr.status < 300, rawText: xhr.responseText || '', data, transport: 'xhr', }); }; xhr.onerror = function onXhrError() { reject(new Error('XMLHttpRequest failed')); }; xhr.send(init && init.body ? init.body : null); }); } function pickParam(request, ...names) { const sources = [request && request.params, request && request.body, request]; for (const src of sources) { if (!src || typeof src !== 'object') continue; for (const name of names) { const value = src[name]; if (value !== undefined && value !== null && value !== '') return value; } } return null; } function stripEmpty(value) { if (!value || typeof value !== 'object') return value; const out = Array.isArray(value) ? [] : {}; for (const [key, val] of Object.entries(value)) { if (val === undefined || val === null || val === '') continue; if (val && typeof val === 'object' && !Array.isArray(val)) { const nested = stripEmpty(val); if (Object.keys(nested).length) out[key] = nested; } else { out[key] = val; } } return out; } function readJimengToken() { return readEnv('JIMENG_TOKEN') || readEnv('VOLC_JIMENG_TOKEN') || readEnv('VOICE_TOKEN') || readEnv('VOC_TOKEN') || readEnv('VOLC_TOKEN') || readEnv('TRANSCRIPTION_VOC_TOKEN') || JIMENG_TOKEN_FALLBACK || ''; } function jimengTokenSource() { const names = ['JIMENG_TOKEN', 'VOLC_JIMENG_TOKEN', 'VOICE_TOKEN', 'VOC_TOKEN', 'VOLC_TOKEN', 'TRANSCRIPTION_VOC_TOKEN']; for (const name of names) { if (readEnv(name)) return name; } if (JIMENG_TOKEN_FALLBACK) return 'JIMENG_TOKEN_FALLBACK'; return ''; } function normalizeBearerToken(token) { const value = String(token || '').trim(); if (!value) return ''; return /^Bearer\s+/i.test(value) ? value : `Bearer ${value}`; } function readErrorMessage(data, fallback) { if (!data || typeof data !== 'object') return fallback || ''; const nested = data.message && typeof data.message === 'object' ? data.message.errmsg : null; return nested && nested.message || data.errmsg && data.errmsg.message || data.error && data.error.message || data.message || data.msg || data.error || data.detail || fallback || ''; } function buildParseClassUrls(className, objectId) { const base = String(PARSE_API_HOST || '').replace(/\/+$/, ''); const primaryPath = /\/parse$/i.test(base) ? `/classes/${encodeURIComponent(className)}/${encodeURIComponent(objectId)}` : `/parse/classes/${encodeURIComponent(className)}/${encodeURIComponent(objectId)}`; const primary = `${base}${primaryPath}`; const originMatch = base.match(/^(https?:\/\/[^/]+)/i); const origin = originMatch ? originMatch[1] : ''; const parseDirect = origin ? `${origin}/parse/classes/${encodeURIComponent(className)}/${encodeURIComponent(objectId)}` : primary; return Array.from(new Set([parseDirect, primary])); } function normalizeParseApiHost(value) { return String(value || 'https://server.fmode.cn/parse') .replace(/\/+$/, '') .replace(/\/api\/functions$/i, '') .replace(/\/api$/i, ''); } function formatFetchError(error) { if (!error) return 'unknown error'; const message = error.message || String(error); const cause = error.cause ? `; cause=${error.cause.code || error.cause.message || error.cause}` : ''; return `${message}${cause}`; } function maskUrl(value) { return String(value || '') .replace(/(token=)[^&]+/ig, '$1***') .replace(/(Bearer\s+)[^&\s]+/ig, '$1***'); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function readEnv(name) { if (typeof process !== 'undefined' && process.env && process.env[name]) { return process.env[name]; } return ''; }