| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778 |
- /**
- * 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 '';
- }
|