11-jimengManager.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. /**
  2. * Cloud function: jimengManager
  3. *
  4. * Secure proxy for Jimeng generation endpoints. The browser calls this cloud
  5. * function with { action: 'call', endpoint, payload }, and the function injects
  6. * the server-side token before forwarding to JIMENG_BASE_URL.
  7. *
  8. * actions:
  9. * call -> proxy a whitelisted Jimeng endpoint
  10. * getWorkResult -> read Parse ImagineWork by workId/objectId
  11. * diagnose -> non-secret deployment diagnostics
  12. */
  13. const JIMENG_ALLOWED_ENDPOINTS = new Set([
  14. 'getClothesV2',
  15. 'getImgByImg',
  16. 'getImgV4',
  17. 'getText2ImgV3',
  18. 'getText2ImgV31',
  19. 'getImg2ImgV3',
  20. 'getImgV4_pod',
  21. 'getImgV4_goods',
  22. 'getInpaint',
  23. 'getSuperResolution',
  24. 'getVideoV3_720p',
  25. 'getVideoV3_1080p',
  26. 'getVideoV3_Pro',
  27. 'getActor',
  28. 'getActorV2',
  29. 'getDataByTask02',
  30. 'getOhIdentifyMain',
  31. 'getOhDateByTask',
  32. 'getOhDetectMain',
  33. 'getOmniHuman',
  34. ]);
  35. const JIMENG_BASE_URL = (readEnv('JIMENG_BASE_URL') || 'https://server.fmode.cn/api/volcengine/jimeng').replace(/\/+$/, '');
  36. const PARSE_API_HOST = normalizeParseApiHost(readEnv('PARSE_API_HOST') || readEnv('PARSE_BASE_URL') || 'https://server.fmode.cn/api');
  37. const PARSE_APP_ID = readEnv('PARSE_APP_ID') || readEnv('PARSE_APPLICATION_ID') || readEnv('X_PARSE_APPLICATION_ID') || 'ncloudmaster';
  38. const JIMENG_MAX_ATTEMPTS = Math.max(1, Number(readEnv('JIMENG_MAX_ATTEMPTS') || readEnv('JIMENG_LOCAL_MAX_ATTEMPTS') || 3));
  39. const JIMENG_TOKEN_FALLBACK = 'Bearer r:f0333969e312a40e4703e8fe4ed1c600';
  40. const VIDEO_WORKFLOW_APIG_ID = '6pF6EAdKT';
  41. const DEFAULT_APIG_UNIT_PRICE_CNY = 0.1;
  42. const JIMENG_PRICE_CNY = {
  43. getImgV4: { unit: 'image', cny: 0.22 },
  44. getText2ImgV3: { unit: 'call', cny: 0.2 },
  45. getText2ImgV31: { unit: 'call', cny: 0.2 },
  46. getImg2ImgV3: { unit: 'call', cny: 0.2 },
  47. getImgV4_pod: { unit: 'image', cny: 0.22 },
  48. getImgV4_goods: { unit: 'image', cny: 0.22 },
  49. getInpaint: { unit: 'call', cny: 0.2 },
  50. getSuperResolution: { unit: 'call', cny: 0.4 },
  51. getVideoV3_720p: { unit: 'second', cny: 0.28 },
  52. getVideoV3_1080p: { unit: 'second', cny: 0.63 },
  53. getVideoV3_Pro: { unit: 'second', cny: 1 },
  54. getActor: { unit: 'second', cny: 0.5 },
  55. getActorV2: { unit: 'second', cny: 0.4 },
  56. getOmniHuman: { unit: 'second', cny: 1 },
  57. };
  58. const JIMENG_QUERY_ENDPOINTS = new Set(['getDataByTask02', 'getOhDateByTask', 'getWorkResult']);
  59. async function handler(request, response) {
  60. try {
  61. const action = String(pickParam(request, 'action') || 'call').trim();
  62. if (action === 'diagnose') {
  63. return response.json({
  64. code: 200,
  65. success: true,
  66. data: {
  67. baseUrl: maskUrl(JIMENG_BASE_URL),
  68. parseApiHost: maskUrl(PARSE_API_HOST),
  69. parseAppIdConfigured: !!PARSE_APP_ID,
  70. tokenConfigured: !!readJimengToken(),
  71. tokenSource: jimengTokenSource(),
  72. allowedEndpoints: Array.from(JIMENG_ALLOWED_ENDPOINTS).sort(),
  73. maxAttempts: JIMENG_MAX_ATTEMPTS,
  74. },
  75. });
  76. }
  77. if (action === 'getWorkResult' || action === 'work') {
  78. const workId = String(pickParam(request, 'workId', 'objectId', 'id') || '').trim();
  79. if (!workId) {
  80. return response.json({ code: 400, success: false, error: 'Missing workId' });
  81. }
  82. const result = await getWorkResult(workId);
  83. return response.json(result);
  84. }
  85. if (action !== 'call') {
  86. return response.json({ code: 400, success: false, error: `Unknown action: ${action}` });
  87. }
  88. const endpoint = String(pickParam(request, 'endpoint', 'routerName', 'route') || '').trim();
  89. if (!JIMENG_ALLOWED_ENDPOINTS.has(endpoint)) {
  90. return response.json({ code: 400, success: false, error: `Unsupported Jimeng endpoint: ${endpoint || '(empty)'}` });
  91. }
  92. const token = normalizeBearerToken(pickParam(request, 'token') || readJimengToken());
  93. if (!token) {
  94. return response.json({
  95. code: 400,
  96. success: false,
  97. error: 'Jimeng token is not configured. Set JIMENG_TOKEN, VOLC_JIMENG_TOKEN, VOICE_TOKEN, VOC_TOKEN, or VOLC_TOKEN, or update JIMENG_TOKEN_FALLBACK.',
  98. });
  99. }
  100. const rawPayload = pickParam(request, 'payload', 'data') || request.body || {};
  101. const payload = normalizeJimengPayload(endpoint, rawPayload, token);
  102. const sessionToken = String(pickParam(request, 'sessionToken') || '').trim();
  103. const idempotencyKey = String(pickParam(request, 'idempotencyKey') || rawPayload.idempotencyKey || rawPayload.generationTaskId || '').trim();
  104. const data = await requestJimengWithBilling({ endpoint, payload, sessionToken, idempotencyKey });
  105. return response.json(data);
  106. } catch (error) {
  107. console.error('jimengManager failed:', error && error.message ? error.message : error);
  108. return response.json({
  109. code: error && error.status ? error.status : 500,
  110. success: false,
  111. error: error && error.message ? error.message : 'Jimeng service call failed',
  112. detail: error && error.detail ? error.detail : '',
  113. upstream: error && error.upstream ? error.upstream : undefined,
  114. });
  115. }
  116. }
  117. function normalizeJimengPayload(endpoint, input, token) {
  118. const payload = { ...(input || {}) };
  119. delete payload.action;
  120. delete payload.endpoint;
  121. delete payload.route;
  122. delete payload.data;
  123. delete payload.payload;
  124. if (endpoint === 'getDataByTask02' || endpoint === 'getOhDateByTask') {
  125. payload.routerName = payload.routerName || payload.endpoint || payload.routeName;
  126. }
  127. if (endpoint === 'getDataByTask02' && !payload.routerName) {
  128. payload.routerName = 'getImgV4';
  129. }
  130. if (endpoint === 'getOhDateByTask' && !payload.routerName) {
  131. payload.routerName = 'getOmniHuman';
  132. }
  133. if (payload.task_id && !payload.taskId) payload.taskId = payload.task_id;
  134. if (payload.objectId && !payload.workId) payload.workId = payload.objectId;
  135. if (endpoint === 'getVideoV3_Pro' && Array.isArray(payload.image_urls) && payload.image_urls[0] && !payload.image_url) {
  136. payload.image_url = payload.image_urls[0];
  137. delete payload.image_urls;
  138. }
  139. payload.token = token;
  140. return stripEmpty(payload);
  141. }
  142. function billingFramesToSeconds(frames) {
  143. const raw = Number(frames || 121);
  144. return raw >= 241 ? 10 : 5;
  145. }
  146. function billingMediaSeconds(endpoint, payload) {
  147. if (endpoint === 'getActor' || endpoint === 'getActorV2') {
  148. return Math.max(1, Math.ceil(Number(payload.durationSeconds || payload.duration || 5)));
  149. }
  150. if (endpoint === 'getOmniHuman') {
  151. return Math.max(1, Math.ceil(Number(payload.durationSeconds || payload.audioDurationSeconds || payload.duration || 5)));
  152. }
  153. return billingFramesToSeconds(payload.frames);
  154. }
  155. function billingImageQuantity(payload) {
  156. if (payload.force_single === false || payload.forceSingle === false) return 2;
  157. return Math.max(1, Math.ceil(Number(payload.quantity || payload.count || 1)));
  158. }
  159. function estimateJimengBilling(endpoint, payload, unitPriceCny) {
  160. if (JIMENG_QUERY_ENDPOINTS.has(endpoint)) return null;
  161. const rule = JIMENG_PRICE_CNY[endpoint];
  162. if (!rule) return null;
  163. const price = Math.max(0.01, Number(unitPriceCny || DEFAULT_APIG_UNIT_PRICE_CNY));
  164. const body = payload || {};
  165. const quantity = rule.unit === 'second'
  166. ? billingMediaSeconds(endpoint, body)
  167. : rule.unit === 'image'
  168. ? billingImageQuantity(body)
  169. : 1;
  170. const costCny = Math.round(rule.cny * quantity * 100) / 100;
  171. return {
  172. endpoint,
  173. unit: rule.unit,
  174. quantity,
  175. costCny,
  176. unitPriceCny: price,
  177. credits: Math.max(1, Math.ceil(costCny / price)),
  178. };
  179. }
  180. function sanitizeBillingSnapshot(payload) {
  181. const copy = { ...(payload || {}) };
  182. delete copy.token;
  183. delete copy.apiKey;
  184. delete copy.apiSecret;
  185. delete copy.binary_data_base64;
  186. return copy;
  187. }
  188. function stableJson(value) {
  189. if (value === null || typeof value !== 'object') return JSON.stringify(value);
  190. if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
  191. return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
  192. }
  193. async function requestJimengWithBilling({ endpoint, payload, sessionToken, idempotencyKey }) {
  194. const initialBilling = estimateJimengBilling(endpoint, payload, DEFAULT_APIG_UNIT_PRICE_CNY);
  195. if (!initialBilling) {
  196. return requestJimengWithRetry(endpoint, payload);
  197. }
  198. const user = await verifyBillingUser(sessionToken);
  199. const auth = await ensureVideoWorkflowAuth(user.objectId, sessionToken);
  200. const unitPriceCny = Number(auth && auth.api && auth.api.price ? auth.api.price : DEFAULT_APIG_UNIT_PRICE_CNY);
  201. const billing = estimateJimengBilling(endpoint, payload, unitPriceCny);
  202. const stableKey = idempotencyKey || `jimeng:${endpoint}:${user.objectId}:${stableJson(sanitizeBillingSnapshot(payload))}`;
  203. const lastBilling = auth && auth.lastBilling && typeof auth.lastBilling === 'object' ? auth.lastBilling : null;
  204. if (lastBilling && lastBilling.idempotencyKey === stableKey) {
  205. if (lastBilling.workId) {
  206. return {
  207. code: 200,
  208. success: true,
  209. data: {
  210. workId: lastBilling.workId,
  211. billing: lastBilling,
  212. reused: true,
  213. },
  214. };
  215. }
  216. if (lastBilling.status === 'reserved') {
  217. const error = new Error('上一条相同即梦生成请求仍在处理中,请稍后再试');
  218. error.status = 409;
  219. error.detail = { idempotencyKey: stableKey, lastBilling };
  220. throw error;
  221. }
  222. }
  223. const reservation = await reserveJimengCredits({
  224. user,
  225. auth,
  226. billing,
  227. endpoint,
  228. payload,
  229. idempotencyKey: stableKey,
  230. sessionToken,
  231. });
  232. try {
  233. const data = await requestJimengWithRetry(endpoint, payload);
  234. const workId = extractJimengWorkId(data);
  235. if (!workId) {
  236. await refundJimengCredits({
  237. user,
  238. auth,
  239. reservation,
  240. reason: 'Jimeng submit returned no workId',
  241. sessionToken,
  242. });
  243. return data;
  244. }
  245. const submittedBilling = {
  246. ...reservation.lastBilling,
  247. status: 'submitted',
  248. workId,
  249. responseSnapshot: sanitizeBillingSnapshot(data),
  250. updatedAt: new Date().toISOString(),
  251. };
  252. await updateApigAuthBilling(auth, { lastBilling: submittedBilling }, sessionToken);
  253. return {
  254. ...data,
  255. billing: {
  256. costCredits: billing.credits,
  257. costCny: billing.costCny,
  258. balanceAfter: reservation.nextCount,
  259. workId,
  260. },
  261. };
  262. } catch (error) {
  263. await refundJimengCredits({
  264. user,
  265. auth,
  266. reservation,
  267. reason: error && error.message ? error.message : 'Jimeng submit failed before workId',
  268. sessionToken,
  269. });
  270. throw error;
  271. }
  272. }
  273. function extractJimengWorkId(data) {
  274. return data && (
  275. data.workId
  276. || data.taskId
  277. || data.objectId
  278. || data.data && (data.data.workId || data.data.taskId || data.data.objectId)
  279. || data.result && (data.result.workId || data.result.taskId || data.result.objectId)
  280. ) || '';
  281. }
  282. async function parseRequest(method, path, body, sessionToken) {
  283. const base = String(PARSE_API_HOST || '').replace(/\/+$/, '').replace(/\/parse$/i, '');
  284. const url = `${base}/parse${path}`;
  285. const headers = {
  286. 'Content-Type': 'application/json',
  287. 'Accept': 'application/json',
  288. 'X-Parse-Application-Id': PARSE_APP_ID,
  289. };
  290. if (sessionToken) headers['X-Parse-Session-Token'] = sessionToken;
  291. const init = { method, headers };
  292. if (body !== undefined) init.body = JSON.stringify(body);
  293. const { status, ok, rawText, data } = await requestJson(method, url, init);
  294. if (!ok) {
  295. const error = new Error(readErrorMessage(data, rawText) || `Parse ${method} ${path} failed`);
  296. error.status = status || 500;
  297. error.detail = data || rawText || '';
  298. throw error;
  299. }
  300. return data;
  301. }
  302. async function verifyBillingUser(sessionToken) {
  303. if (!sessionToken) {
  304. const error = new Error('请先登录后再生成');
  305. error.status = 401;
  306. throw error;
  307. }
  308. return parseRequest('GET', '/users/me?include=company', undefined, sessionToken);
  309. }
  310. async function findVideoWorkflowAuth(userId, sessionToken) {
  311. const where = encodeURIComponent(JSON.stringify({
  312. api: { __type: 'Pointer', className: 'APIG', objectId: VIDEO_WORKFLOW_APIG_ID },
  313. user: { __type: 'Pointer', className: '_User', objectId: userId },
  314. }));
  315. const data = await parseRequest('GET', `/classes/APIGAuth?where=${where}&include=api&limit=1`, undefined, sessionToken);
  316. return Array.isArray(data.results) ? data.results[0] || null : null;
  317. }
  318. async function ensureVideoWorkflowAuth(userId, sessionToken) {
  319. const existing = await findVideoWorkflowAuth(userId, sessionToken);
  320. if (existing) return existing;
  321. const body = {
  322. api: { __type: 'Pointer', className: 'APIG', objectId: VIDEO_WORKFLOW_APIG_ID },
  323. user: { __type: 'Pointer', className: '_User', objectId: userId },
  324. count: 0,
  325. used: 0,
  326. };
  327. const created = await parseRequest('POST', '/classes/APIGAuth', body, sessionToken);
  328. return { ...body, ...created };
  329. }
  330. async function updateApigAuthBilling(auth, patch, sessionToken) {
  331. return parseRequest('PUT', `/classes/APIGAuth/${encodeURIComponent(auth.objectId)}`, patch, sessionToken);
  332. }
  333. async function reserveJimengCredits({ auth, billing, endpoint, payload, idempotencyKey, sessionToken }) {
  334. const oldCount = Number(auth.count || 0);
  335. const oldUsed = Number(auth.used || 0);
  336. if (oldCount < billing.credits) {
  337. const error = new Error(`余额不足:当前 ${oldCount},本次需要 ${billing.credits}`);
  338. error.status = 402;
  339. throw error;
  340. }
  341. const nextCount = oldCount - billing.credits;
  342. const nextUsed = oldUsed + billing.credits;
  343. const lastBilling = {
  344. kind: 'jimeng_consume',
  345. status: 'reserved',
  346. endpoint,
  347. costCredits: billing.credits,
  348. costCny: billing.costCny,
  349. unitPriceCny: billing.unitPriceCny,
  350. quantity: billing.quantity,
  351. unit: billing.unit,
  352. idempotencyKey,
  353. balanceBefore: oldCount,
  354. balanceAfter: nextCount,
  355. usedBefore: oldUsed,
  356. usedAfter: nextUsed,
  357. requestSnapshot: sanitizeBillingSnapshot(payload),
  358. updatedAt: new Date().toISOString(),
  359. };
  360. await updateApigAuthBilling(auth, { count: nextCount, used: nextUsed, lastBilling }, sessionToken);
  361. return { oldCount, oldUsed, nextCount, nextUsed, lastBilling };
  362. }
  363. async function refundJimengCredits({ user, auth, reservation, reason, sessionToken }) {
  364. const latest = await findVideoWorkflowAuth(user.objectId, sessionToken);
  365. const current = latest || auth;
  366. const currentCount = Number(current.count || 0);
  367. const currentUsed = Number(current.used || 0);
  368. const refundCredits = Number(reservation.lastBilling.costCredits || 0);
  369. const nextCount = currentCount + refundCredits;
  370. const nextUsed = Math.max(0, currentUsed - refundCredits);
  371. await updateApigAuthBilling(current, {
  372. count: nextCount,
  373. used: nextUsed,
  374. lastBilling: {
  375. ...reservation.lastBilling,
  376. status: 'refunded',
  377. refundReason: reason,
  378. balanceAfter: nextCount,
  379. usedAfter: nextUsed,
  380. updatedAt: new Date().toISOString(),
  381. },
  382. }, sessionToken);
  383. }
  384. async function requestJimengWithRetry(endpoint, payload) {
  385. let lastError = null;
  386. const url = `${JIMENG_BASE_URL}/${encodeURIComponent(endpoint)}`;
  387. for (let attempt = 1; attempt <= JIMENG_MAX_ATTEMPTS; attempt += 1) {
  388. try {
  389. const { status, ok, rawText, data, transport } = await postJson(url, payload);
  390. const code = Number(data && data.code ? data.code : status || 0);
  391. const retryable = !ok || code === 408 || code === 429 || code >= 500;
  392. if (!retryable || attempt >= JIMENG_MAX_ATTEMPTS) {
  393. if (!ok) {
  394. const error = new Error(readErrorMessage(data, rawText) || `Jimeng HTTP ${status}`);
  395. error.status = status || code || 500;
  396. error.detail = data || rawText || '';
  397. error.upstream = { endpoint, status, transport, attempt, maxAttempts: JIMENG_MAX_ATTEMPTS };
  398. throw error;
  399. }
  400. if (!data) {
  401. return { code: 500, success: false, error: 'Jimeng returned an empty response' };
  402. }
  403. return data;
  404. }
  405. } catch (error) {
  406. lastError = error;
  407. if (attempt >= JIMENG_MAX_ATTEMPTS) break;
  408. }
  409. await sleep(Math.min(12000, 1200 * attempt * attempt));
  410. }
  411. const error = new Error(`Jimeng upstream request failed: ${formatFetchError(lastError)}; endpoint=${endpoint}`);
  412. error.status = (lastError && lastError.status) || 500;
  413. error.detail = (lastError && lastError.detail) || '';
  414. error.upstream = { endpoint, attempt: JIMENG_MAX_ATTEMPTS, maxAttempts: JIMENG_MAX_ATTEMPTS };
  415. throw error;
  416. }
  417. async function getWorkResult(workId) {
  418. const urls = buildParseClassUrls('ImagineWork', workId);
  419. const headers = { 'X-Parse-Application-Id': PARSE_APP_ID };
  420. let lastError = null;
  421. for (let attempt = 1; attempt <= JIMENG_MAX_ATTEMPTS; attempt += 1) {
  422. for (const url of urls) {
  423. try {
  424. const { status, ok, rawText, data } = await getJson(url, headers);
  425. const wrongRoute = status === 404 && /Cannot\s+GET\s+\/api\/parse\/classes/i.test(rawText || '');
  426. const retryable = !ok && !wrongRoute && (status === 408 || status === 429 || status >= 500);
  427. if (ok) {
  428. return { code: 200, success: true, data };
  429. }
  430. if (wrongRoute && urls.length > 1) {
  431. lastError = new Error(`Wrong Parse route: ${url}`);
  432. continue;
  433. }
  434. if (!retryable || attempt >= JIMENG_MAX_ATTEMPTS) {
  435. return {
  436. code: status || 500,
  437. success: false,
  438. error: readErrorMessage(data, rawText) || `Failed to query Jimeng work result from ${maskUrl(url)}`,
  439. };
  440. }
  441. } catch (error) {
  442. lastError = error;
  443. if (attempt >= JIMENG_MAX_ATTEMPTS && url === urls[urls.length - 1]) break;
  444. }
  445. }
  446. await sleep(Math.min(12000, 1200 * attempt * attempt));
  447. }
  448. return {
  449. code: 500,
  450. success: false,
  451. error: `Failed to query Jimeng work result: ${formatFetchError(lastError)}`,
  452. };
  453. }
  454. async function postJson(url, body) {
  455. return requestJson('POST', url, {
  456. headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
  457. body: JSON.stringify(body || {}),
  458. });
  459. }
  460. async function getJson(url, headers) {
  461. return requestJson('GET', url, {
  462. headers: { 'Accept': 'application/json', ...(headers || {}) },
  463. });
  464. }
  465. async function requestJson(method, url, init) {
  466. let lastError = null;
  467. if (typeof fetch === 'function') {
  468. try {
  469. const response = await fetch(url, { method, ...(init || {}) });
  470. return readResponse(response, 'fetch');
  471. } catch (error) {
  472. lastError = error;
  473. }
  474. }
  475. if (typeof require === 'function') {
  476. try {
  477. return await requestJsonWithNodeHttps(method, url, init);
  478. } catch (error) {
  479. lastError = error;
  480. }
  481. }
  482. if (typeof XMLHttpRequest !== 'undefined') {
  483. try {
  484. return await requestJsonWithXhr(method, url, init);
  485. } catch (error) {
  486. lastError = error;
  487. }
  488. }
  489. throw lastError || new Error('No HTTP client is available in this cloud runtime');
  490. }
  491. async function readResponse(response, transport) {
  492. const rawText = await response.text();
  493. let data = null;
  494. try { data = rawText ? JSON.parse(rawText) : null; } catch {}
  495. return {
  496. status: response.status,
  497. ok: !!response.ok,
  498. rawText,
  499. data,
  500. transport,
  501. };
  502. }
  503. function requestJsonWithNodeHttps(method, url, init) {
  504. return new Promise((resolve, reject) => {
  505. try {
  506. const parsed = new URL(url);
  507. const lib = parsed.protocol === 'http:' ? require('http') : require('https');
  508. const headers = init && init.headers ? init.headers : {};
  509. const body = init && init.body ? init.body : '';
  510. const req = lib.request({
  511. method,
  512. protocol: parsed.protocol,
  513. hostname: parsed.hostname,
  514. port: parsed.port,
  515. path: `${parsed.pathname}${parsed.search}`,
  516. headers: body ? { ...headers, 'Content-Length': Buffer.byteLength(body) } : headers,
  517. }, (res) => {
  518. const chunks = [];
  519. res.on('data', (chunk) => chunks.push(chunk));
  520. res.on('end', () => {
  521. const rawText = Buffer.concat(chunks).toString('utf8');
  522. let data = null;
  523. try { data = rawText ? JSON.parse(rawText) : null; } catch {}
  524. resolve({
  525. status: res.statusCode || 0,
  526. ok: res.statusCode >= 200 && res.statusCode < 300,
  527. rawText,
  528. data,
  529. transport: 'node-https',
  530. });
  531. });
  532. });
  533. req.on('error', reject);
  534. if (body) req.write(body);
  535. req.end();
  536. } catch (error) {
  537. reject(error);
  538. }
  539. });
  540. }
  541. function requestJsonWithXhr(method, url, init) {
  542. return new Promise((resolve, reject) => {
  543. const xhr = new XMLHttpRequest();
  544. xhr.open(method, url, true);
  545. const headers = init && init.headers ? init.headers : {};
  546. for (const [key, value] of Object.entries(headers)) {
  547. xhr.setRequestHeader(key, value);
  548. }
  549. xhr.onreadystatechange = function onReadyStateChange() {
  550. if (xhr.readyState !== 4) return;
  551. let data = null;
  552. try { data = xhr.responseText ? JSON.parse(xhr.responseText) : null; } catch {}
  553. resolve({
  554. status: xhr.status,
  555. ok: xhr.status >= 200 && xhr.status < 300,
  556. rawText: xhr.responseText || '',
  557. data,
  558. transport: 'xhr',
  559. });
  560. };
  561. xhr.onerror = function onXhrError() {
  562. reject(new Error('XMLHttpRequest failed'));
  563. };
  564. xhr.send(init && init.body ? init.body : null);
  565. });
  566. }
  567. function pickParam(request, ...names) {
  568. const sources = [request && request.params, request && request.body, request];
  569. for (const src of sources) {
  570. if (!src || typeof src !== 'object') continue;
  571. for (const name of names) {
  572. const value = src[name];
  573. if (value !== undefined && value !== null && value !== '') return value;
  574. }
  575. }
  576. return null;
  577. }
  578. function stripEmpty(value) {
  579. if (!value || typeof value !== 'object') return value;
  580. const out = Array.isArray(value) ? [] : {};
  581. for (const [key, val] of Object.entries(value)) {
  582. if (val === undefined || val === null || val === '') continue;
  583. if (val && typeof val === 'object' && !Array.isArray(val)) {
  584. const nested = stripEmpty(val);
  585. if (Object.keys(nested).length) out[key] = nested;
  586. } else {
  587. out[key] = val;
  588. }
  589. }
  590. return out;
  591. }
  592. function readJimengToken() {
  593. return readEnv('JIMENG_TOKEN')
  594. || readEnv('VOLC_JIMENG_TOKEN')
  595. || readEnv('VOICE_TOKEN')
  596. || readEnv('VOC_TOKEN')
  597. || readEnv('VOLC_TOKEN')
  598. || readEnv('TRANSCRIPTION_VOC_TOKEN')
  599. || JIMENG_TOKEN_FALLBACK
  600. || '';
  601. }
  602. function jimengTokenSource() {
  603. const names = ['JIMENG_TOKEN', 'VOLC_JIMENG_TOKEN', 'VOICE_TOKEN', 'VOC_TOKEN', 'VOLC_TOKEN', 'TRANSCRIPTION_VOC_TOKEN'];
  604. for (const name of names) {
  605. if (readEnv(name)) return name;
  606. }
  607. if (JIMENG_TOKEN_FALLBACK) return 'JIMENG_TOKEN_FALLBACK';
  608. return '';
  609. }
  610. function normalizeBearerToken(token) {
  611. const value = String(token || '').trim();
  612. if (!value) return '';
  613. return /^Bearer\s+/i.test(value) ? value : `Bearer ${value}`;
  614. }
  615. function readErrorMessage(data, fallback) {
  616. if (!data || typeof data !== 'object') return fallback || '';
  617. const nested = data.message && typeof data.message === 'object' ? data.message.errmsg : null;
  618. return nested && nested.message
  619. || data.errmsg && data.errmsg.message
  620. || data.error && data.error.message
  621. || data.message
  622. || data.msg
  623. || data.error
  624. || data.detail
  625. || fallback
  626. || '';
  627. }
  628. function buildParseClassUrls(className, objectId) {
  629. const base = String(PARSE_API_HOST || '').replace(/\/+$/, '');
  630. const primaryPath = /\/parse$/i.test(base)
  631. ? `/classes/${encodeURIComponent(className)}/${encodeURIComponent(objectId)}`
  632. : `/parse/classes/${encodeURIComponent(className)}/${encodeURIComponent(objectId)}`;
  633. const primary = `${base}${primaryPath}`;
  634. const originMatch = base.match(/^(https?:\/\/[^/]+)/i);
  635. const origin = originMatch ? originMatch[1] : '';
  636. const parseDirect = origin
  637. ? `${origin}/parse/classes/${encodeURIComponent(className)}/${encodeURIComponent(objectId)}`
  638. : primary;
  639. return Array.from(new Set([parseDirect, primary]));
  640. }
  641. function normalizeParseApiHost(value) {
  642. return String(value || 'https://server.fmode.cn/parse')
  643. .replace(/\/+$/, '')
  644. .replace(/\/api\/functions$/i, '')
  645. .replace(/\/api$/i, '');
  646. }
  647. function formatFetchError(error) {
  648. if (!error) return 'unknown error';
  649. const message = error.message || String(error);
  650. const cause = error.cause ? `; cause=${error.cause.code || error.cause.message || error.cause}` : '';
  651. return `${message}${cause}`;
  652. }
  653. function maskUrl(value) {
  654. return String(value || '')
  655. .replace(/(token=)[^&]+/ig, '$1***')
  656. .replace(/(Bearer\s+)[^&\s]+/ig, '$1***');
  657. }
  658. function sleep(ms) {
  659. return new Promise((resolve) => setTimeout(resolve, ms));
  660. }
  661. function readEnv(name) {
  662. if (typeof process !== 'undefined' && process.env && process.env[name]) {
  663. return process.env[name];
  664. }
  665. return '';
  666. }