11-jimengManager.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  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. * 支持 action:
  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 = '6pFf6EAdKT';
  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 apigPayInfo = auth && auth.objectId ? await fetchApigPayInfo(auth.objectId).catch(() => null) : null;
  201. const billingAuth = mergeApigPayBalance(auth, apigPayInfo);
  202. const unitPriceCny = Number(
  203. billingAuth && billingAuth.api && billingAuth.api.price
  204. ? billingAuth.api.price
  205. : apigPayInfo && apigPayInfo.price
  206. ? apigPayInfo.price
  207. : DEFAULT_APIG_UNIT_PRICE_CNY
  208. );
  209. const billing = estimateJimengBilling(endpoint, payload, unitPriceCny);
  210. const stableKey = idempotencyKey || `jimeng:${endpoint}:${user.objectId}:${stableJson(sanitizeBillingSnapshot(payload))}`;
  211. const lastBilling = billingAuth && billingAuth.lastBilling && typeof billingAuth.lastBilling === 'object' ? billingAuth.lastBilling : null;
  212. if (lastBilling && lastBilling.idempotencyKey === stableKey) {
  213. if (lastBilling.workId) {
  214. return {
  215. code: 200,
  216. success: true,
  217. data: {
  218. workId: lastBilling.workId,
  219. billing: lastBilling,
  220. reused: true,
  221. },
  222. };
  223. }
  224. if (lastBilling.status === 'reserved') {
  225. const error = new Error('上一条相同即梦生成请求仍在处理中,请稍后再试');
  226. error.status = 409;
  227. error.detail = { idempotencyKey: stableKey, lastBilling };
  228. throw error;
  229. }
  230. }
  231. const reservation = await reserveJimengCredits({
  232. user,
  233. auth: billingAuth,
  234. billing,
  235. endpoint,
  236. payload,
  237. idempotencyKey: stableKey,
  238. sessionToken,
  239. });
  240. try {
  241. const data = await requestJimengWithRetry(endpoint, payload);
  242. const workId = extractJimengWorkId(data);
  243. if (!workId) {
  244. await refundJimengCredits({
  245. user,
  246. auth: billingAuth,
  247. reservation,
  248. reason: 'Jimeng submit returned no workId',
  249. sessionToken,
  250. });
  251. return data;
  252. }
  253. const submittedBilling = {
  254. ...reservation.lastBilling,
  255. status: 'submitted',
  256. workId,
  257. responseSnapshot: sanitizeBillingSnapshot(data),
  258. updatedAt: new Date().toISOString(),
  259. };
  260. await updateApigAuthBilling(billingAuth, { lastBilling: submittedBilling }, sessionToken);
  261. return {
  262. ...data,
  263. billing: {
  264. costCredits: billing.credits,
  265. costCny: billing.costCny,
  266. balanceAfter: reservation.nextCount,
  267. workId,
  268. },
  269. };
  270. } catch (error) {
  271. await refundJimengCredits({
  272. user,
  273. auth: billingAuth,
  274. reservation,
  275. reason: error && error.message ? error.message : 'Jimeng submit failed before workId',
  276. sessionToken,
  277. });
  278. throw error;
  279. }
  280. }
  281. function extractJimengWorkId(data) {
  282. return data && (
  283. data.workId
  284. || data.taskId
  285. || data.objectId
  286. || data.data && (data.data.workId || data.data.taskId || data.data.objectId)
  287. || data.result && (data.result.workId || data.result.taskId || data.result.objectId)
  288. ) || '';
  289. }
  290. async function parseRequest(method, path, body, sessionToken) {
  291. const base = String(PARSE_API_HOST || '').replace(/\/+$/, '').replace(/\/parse$/i, '');
  292. const url = `${base}/parse${path}`;
  293. const headers = {
  294. 'Content-Type': 'application/json',
  295. 'Accept': 'application/json',
  296. 'X-Parse-Application-Id': PARSE_APP_ID,
  297. };
  298. if (sessionToken) headers['X-Parse-Session-Token'] = sessionToken;
  299. const init = { method, headers };
  300. if (body !== undefined) init.body = JSON.stringify(body);
  301. const { status, ok, rawText, data } = await requestJson(method, url, init);
  302. if (!ok) {
  303. const error = new Error(readErrorMessage(data, rawText) || `Parse ${method} ${path} failed`);
  304. error.status = status || 500;
  305. error.detail = data || rawText || '';
  306. throw error;
  307. }
  308. return data;
  309. }
  310. async function verifyBillingUser(sessionToken) {
  311. if (!sessionToken) {
  312. const error = new Error('请先登录后再生成');
  313. error.status = 401;
  314. throw error;
  315. }
  316. return parseRequest('GET', '/users/me?include=company', undefined, sessionToken);
  317. }
  318. async function findVideoWorkflowAuth(userId, sessionToken) {
  319. const where = encodeURIComponent(JSON.stringify({
  320. api: { __type: 'Pointer', className: 'APIG', objectId: VIDEO_WORKFLOW_APIG_ID },
  321. user: { __type: 'Pointer', className: '_User', objectId: userId },
  322. }));
  323. const data = await parseRequest('GET', `/classes/APIGAuth?where=${where}&include=api&limit=1`, undefined, sessionToken);
  324. return Array.isArray(data.results) ? data.results[0] || null : null;
  325. }
  326. async function ensureVideoWorkflowAuth(userId, sessionToken) {
  327. const existing = await findVideoWorkflowAuth(userId, sessionToken);
  328. if (existing) return existing;
  329. const body = {
  330. api: { __type: 'Pointer', className: 'APIG', objectId: VIDEO_WORKFLOW_APIG_ID },
  331. user: { __type: 'Pointer', className: '_User', objectId: userId },
  332. count: 0,
  333. used: 0,
  334. };
  335. const created = await parseRequest('POST', '/classes/APIGAuth', body, sessionToken);
  336. return { ...body, ...created };
  337. }
  338. async function fetchApigPayInfo(authId) {
  339. const base = String(PARSE_API_HOST || '').replace(/\/+$/, '').replace(/\/parse$/i, '');
  340. const { ok, data } = await requestJson('POST', `${base}/api/apig/getApig`, {
  341. headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
  342. body: JSON.stringify({ authid: authId }),
  343. });
  344. if (!ok || !data || Number(data.code) !== 200 || !data.data) return null;
  345. return data.data;
  346. }
  347. function mergeApigPayBalance(auth, apigPayInfo) {
  348. if (!apigPayInfo || typeof apigPayInfo !== 'object') return auth;
  349. const count = apigPayInfo.count !== undefined ? Number(apigPayInfo.count) : Number(auth && auth.count || 0);
  350. const used = apigPayInfo.used !== undefined ? Number(apigPayInfo.used) : Number(auth && auth.used || 0);
  351. const api = {
  352. ...(auth && auth.api && typeof auth.api === 'object' ? auth.api : {}),
  353. objectId: apigPayInfo.objectId || apigPayInfo.api && apigPayInfo.api.objectId || auth && auth.api && auth.api.objectId || VIDEO_WORKFLOW_APIG_ID,
  354. title: apigPayInfo.title || apigPayInfo.api && apigPayInfo.api.title || auth && auth.api && auth.api.title,
  355. price: apigPayInfo.price || apigPayInfo.api && apigPayInfo.api.price || auth && auth.api && auth.api.price,
  356. };
  357. return {
  358. ...(auth || {}),
  359. count: Number.isFinite(count) ? count : Number(auth && auth.count || 0),
  360. used: Number.isFinite(used) ? used : Number(auth && auth.used || 0),
  361. api,
  362. };
  363. }
  364. async function updateApigAuthBilling(auth, patch, sessionToken) {
  365. return parseRequest('PUT', `/classes/APIGAuth/${encodeURIComponent(auth.objectId)}`, patch, sessionToken);
  366. }
  367. async function reserveJimengCredits({ auth, billing, endpoint, payload, idempotencyKey, sessionToken }) {
  368. const oldCount = Number(auth.count || 0);
  369. const oldUsed = Number(auth.used || 0);
  370. if (oldCount < billing.credits) {
  371. const error = new Error(`余额不足:当前 ${oldCount},本次需要 ${billing.credits}`);
  372. error.status = 402;
  373. throw error;
  374. }
  375. const nextCount = oldCount - billing.credits;
  376. const nextUsed = oldUsed + billing.credits;
  377. const lastBilling = {
  378. kind: 'jimeng_consume',
  379. status: 'reserved',
  380. endpoint,
  381. costCredits: billing.credits,
  382. costCny: billing.costCny,
  383. unitPriceCny: billing.unitPriceCny,
  384. quantity: billing.quantity,
  385. unit: billing.unit,
  386. idempotencyKey,
  387. balanceBefore: oldCount,
  388. balanceAfter: nextCount,
  389. usedBefore: oldUsed,
  390. usedAfter: nextUsed,
  391. requestSnapshot: sanitizeBillingSnapshot(payload),
  392. updatedAt: new Date().toISOString(),
  393. };
  394. await updateApigAuthBilling(auth, { count: nextCount, used: nextUsed, lastBilling }, sessionToken);
  395. return { oldCount, oldUsed, nextCount, nextUsed, lastBilling };
  396. }
  397. async function refundJimengCredits({ user, auth, reservation, reason, sessionToken }) {
  398. const latest = await findVideoWorkflowAuth(user.objectId, sessionToken);
  399. const current = latest || auth;
  400. const currentCount = Number(current.count || 0);
  401. const currentUsed = Number(current.used || 0);
  402. const refundCredits = Number(reservation.lastBilling.costCredits || 0);
  403. const nextCount = currentCount + refundCredits;
  404. const nextUsed = Math.max(0, currentUsed - refundCredits);
  405. await updateApigAuthBilling(current, {
  406. count: nextCount,
  407. used: nextUsed,
  408. lastBilling: {
  409. ...reservation.lastBilling,
  410. status: 'refunded',
  411. refundReason: reason,
  412. balanceAfter: nextCount,
  413. usedAfter: nextUsed,
  414. updatedAt: new Date().toISOString(),
  415. },
  416. }, sessionToken);
  417. }
  418. async function requestJimengWithRetry(endpoint, payload) {
  419. let lastError = null;
  420. const url = `${JIMENG_BASE_URL}/${encodeURIComponent(endpoint)}`;
  421. for (let attempt = 1; attempt <= JIMENG_MAX_ATTEMPTS; attempt += 1) {
  422. try {
  423. const { status, ok, rawText, data, transport } = await postJson(url, payload);
  424. const code = Number(data && data.code ? data.code : status || 0);
  425. const retryable = !ok || code === 408 || code === 429 || code >= 500;
  426. if (!retryable || attempt >= JIMENG_MAX_ATTEMPTS) {
  427. if (!ok) {
  428. const error = new Error(readErrorMessage(data, rawText) || `Jimeng HTTP ${status}`);
  429. error.status = status || code || 500;
  430. error.detail = data || rawText || '';
  431. error.upstream = { endpoint, status, transport, attempt, maxAttempts: JIMENG_MAX_ATTEMPTS };
  432. throw error;
  433. }
  434. if (!data) {
  435. return { code: 500, success: false, error: 'Jimeng returned an empty response' };
  436. }
  437. return data;
  438. }
  439. } catch (error) {
  440. lastError = error;
  441. if (attempt >= JIMENG_MAX_ATTEMPTS) break;
  442. }
  443. await sleep(Math.min(12000, 1200 * attempt * attempt));
  444. }
  445. const error = new Error(`Jimeng upstream request failed: ${formatFetchError(lastError)}; endpoint=${endpoint}`);
  446. error.status = (lastError && lastError.status) || 500;
  447. error.detail = (lastError && lastError.detail) || '';
  448. error.upstream = { endpoint, attempt: JIMENG_MAX_ATTEMPTS, maxAttempts: JIMENG_MAX_ATTEMPTS };
  449. throw error;
  450. }
  451. async function getWorkResult(workId) {
  452. const urls = buildParseClassUrls('ImagineWork', workId);
  453. const headers = { 'X-Parse-Application-Id': PARSE_APP_ID };
  454. let lastError = null;
  455. for (let attempt = 1; attempt <= JIMENG_MAX_ATTEMPTS; attempt += 1) {
  456. for (const url of urls) {
  457. try {
  458. const { status, ok, rawText, data } = await getJson(url, headers);
  459. const wrongRoute = status === 404 && /Cannot\s+GET\s+\/api\/parse\/classes/i.test(rawText || '');
  460. const retryable = !ok && !wrongRoute && (status === 408 || status === 429 || status >= 500);
  461. if (ok) {
  462. return { code: 200, success: true, data };
  463. }
  464. if (wrongRoute && urls.length > 1) {
  465. lastError = new Error(`Wrong Parse route: ${url}`);
  466. continue;
  467. }
  468. if (!retryable || attempt >= JIMENG_MAX_ATTEMPTS) {
  469. return {
  470. code: status || 500,
  471. success: false,
  472. error: readErrorMessage(data, rawText) || `Failed to query Jimeng work result from ${maskUrl(url)}`,
  473. };
  474. }
  475. } catch (error) {
  476. lastError = error;
  477. if (attempt >= JIMENG_MAX_ATTEMPTS && url === urls[urls.length - 1]) break;
  478. }
  479. }
  480. await sleep(Math.min(12000, 1200 * attempt * attempt));
  481. }
  482. return {
  483. code: 500,
  484. success: false,
  485. error: `Failed to query Jimeng work result: ${formatFetchError(lastError)}`,
  486. };
  487. }
  488. async function postJson(url, body) {
  489. return requestJson('POST', url, {
  490. headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
  491. body: JSON.stringify(body || {}),
  492. });
  493. }
  494. async function getJson(url, headers) {
  495. return requestJson('GET', url, {
  496. headers: { 'Accept': 'application/json', ...(headers || {}) },
  497. });
  498. }
  499. async function requestJson(method, url, init) {
  500. let lastError = null;
  501. if (typeof fetch === 'function') {
  502. try {
  503. const response = await fetch(url, { method, ...(init || {}) });
  504. return readResponse(response, 'fetch');
  505. } catch (error) {
  506. lastError = error;
  507. }
  508. }
  509. if (typeof require === 'function') {
  510. try {
  511. return await requestJsonWithNodeHttps(method, url, init);
  512. } catch (error) {
  513. lastError = error;
  514. }
  515. }
  516. if (typeof XMLHttpRequest !== 'undefined') {
  517. try {
  518. return await requestJsonWithXhr(method, url, init);
  519. } catch (error) {
  520. lastError = error;
  521. }
  522. }
  523. throw lastError || new Error('No HTTP client is available in this cloud runtime');
  524. }
  525. async function readResponse(response, transport) {
  526. const rawText = await response.text();
  527. let data = null;
  528. try { data = rawText ? JSON.parse(rawText) : null; } catch {}
  529. return {
  530. status: response.status,
  531. ok: !!response.ok,
  532. rawText,
  533. data,
  534. transport,
  535. };
  536. }
  537. function requestJsonWithNodeHttps(method, url, init) {
  538. return new Promise((resolve, reject) => {
  539. try {
  540. const parsed = new URL(url);
  541. const lib = parsed.protocol === 'http:' ? require('http') : require('https');
  542. const headers = init && init.headers ? init.headers : {};
  543. const body = init && init.body ? init.body : '';
  544. const req = lib.request({
  545. method,
  546. protocol: parsed.protocol,
  547. hostname: parsed.hostname,
  548. port: parsed.port,
  549. path: `${parsed.pathname}${parsed.search}`,
  550. headers: body ? { ...headers, 'Content-Length': Buffer.byteLength(body) } : headers,
  551. }, (res) => {
  552. const chunks = [];
  553. res.on('data', (chunk) => chunks.push(chunk));
  554. res.on('end', () => {
  555. const rawText = Buffer.concat(chunks).toString('utf8');
  556. let data = null;
  557. try { data = rawText ? JSON.parse(rawText) : null; } catch {}
  558. resolve({
  559. status: res.statusCode || 0,
  560. ok: res.statusCode >= 200 && res.statusCode < 300,
  561. rawText,
  562. data,
  563. transport: 'node-https',
  564. });
  565. });
  566. });
  567. req.on('error', reject);
  568. if (body) req.write(body);
  569. req.end();
  570. } catch (error) {
  571. reject(error);
  572. }
  573. });
  574. }
  575. function requestJsonWithXhr(method, url, init) {
  576. return new Promise((resolve, reject) => {
  577. const xhr = new XMLHttpRequest();
  578. xhr.open(method, url, true);
  579. const headers = init && init.headers ? init.headers : {};
  580. for (const [key, value] of Object.entries(headers)) {
  581. xhr.setRequestHeader(key, value);
  582. }
  583. xhr.onreadystatechange = function onReadyStateChange() {
  584. if (xhr.readyState !== 4) return;
  585. let data = null;
  586. try { data = xhr.responseText ? JSON.parse(xhr.responseText) : null; } catch {}
  587. resolve({
  588. status: xhr.status,
  589. ok: xhr.status >= 200 && xhr.status < 300,
  590. rawText: xhr.responseText || '',
  591. data,
  592. transport: 'xhr',
  593. });
  594. };
  595. xhr.onerror = function onXhrError() {
  596. reject(new Error('XMLHttpRequest failed'));
  597. };
  598. xhr.send(init && init.body ? init.body : null);
  599. });
  600. }
  601. function pickParam(request, ...names) {
  602. const sources = [request && request.params, request && request.body, request];
  603. for (const src of sources) {
  604. if (!src || typeof src !== 'object') continue;
  605. for (const name of names) {
  606. const value = src[name];
  607. if (value !== undefined && value !== null && value !== '') return value;
  608. }
  609. }
  610. return null;
  611. }
  612. function stripEmpty(value) {
  613. if (!value || typeof value !== 'object') return value;
  614. const out = Array.isArray(value) ? [] : {};
  615. for (const [key, val] of Object.entries(value)) {
  616. if (val === undefined || val === null || val === '') continue;
  617. if (val && typeof val === 'object' && !Array.isArray(val)) {
  618. const nested = stripEmpty(val);
  619. if (Object.keys(nested).length) out[key] = nested;
  620. } else {
  621. out[key] = val;
  622. }
  623. }
  624. return out;
  625. }
  626. function readJimengToken() {
  627. return readEnv('JIMENG_TOKEN')
  628. || readEnv('VOLC_JIMENG_TOKEN')
  629. || readEnv('VOICE_TOKEN')
  630. || readEnv('VOC_TOKEN')
  631. || readEnv('VOLC_TOKEN')
  632. || readEnv('TRANSCRIPTION_VOC_TOKEN')
  633. || JIMENG_TOKEN_FALLBACK
  634. || '';
  635. }
  636. function jimengTokenSource() {
  637. const names = ['JIMENG_TOKEN', 'VOLC_JIMENG_TOKEN', 'VOICE_TOKEN', 'VOC_TOKEN', 'VOLC_TOKEN', 'TRANSCRIPTION_VOC_TOKEN'];
  638. for (const name of names) {
  639. if (readEnv(name)) return name;
  640. }
  641. if (JIMENG_TOKEN_FALLBACK) return 'JIMENG_TOKEN_FALLBACK';
  642. return '';
  643. }
  644. function normalizeBearerToken(token) {
  645. const value = String(token || '').trim();
  646. if (!value) return '';
  647. return /^Bearer\s+/i.test(value) ? value : `Bearer ${value}`;
  648. }
  649. function readErrorMessage(data, fallback) {
  650. if (!data || typeof data !== 'object') return fallback || '';
  651. const nested = data.message && typeof data.message === 'object' ? data.message.errmsg : null;
  652. return nested && nested.message
  653. || data.errmsg && data.errmsg.message
  654. || data.error && data.error.message
  655. || data.message
  656. || data.msg
  657. || data.error
  658. || data.detail
  659. || fallback
  660. || '';
  661. }
  662. function buildParseClassUrls(className, objectId) {
  663. const base = String(PARSE_API_HOST || '').replace(/\/+$/, '');
  664. const primaryPath = /\/parse$/i.test(base)
  665. ? `/classes/${encodeURIComponent(className)}/${encodeURIComponent(objectId)}`
  666. : `/parse/classes/${encodeURIComponent(className)}/${encodeURIComponent(objectId)}`;
  667. const primary = `${base}${primaryPath}`;
  668. const originMatch = base.match(/^(https?:\/\/[^/]+)/i);
  669. const origin = originMatch ? originMatch[1] : '';
  670. const parseDirect = origin
  671. ? `${origin}/parse/classes/${encodeURIComponent(className)}/${encodeURIComponent(objectId)}`
  672. : primary;
  673. return Array.from(new Set([parseDirect, primary]));
  674. }
  675. function normalizeParseApiHost(value) {
  676. return String(value || 'https://server.fmode.cn/parse')
  677. .replace(/\/+$/, '')
  678. .replace(/\/api\/functions$/i, '')
  679. .replace(/\/api$/i, '');
  680. }
  681. function formatFetchError(error) {
  682. if (!error) return 'unknown error';
  683. const message = error.message || String(error);
  684. const cause = error.cause ? `; cause=${error.cause.code || error.cause.message || error.cause}` : '';
  685. return `${message}${cause}`;
  686. }
  687. function maskUrl(value) {
  688. return String(value || '')
  689. .replace(/(token=)[^&]+/ig, '$1***')
  690. .replace(/(Bearer\s+)[^&\s]+/ig, '$1***');
  691. }
  692. function sleep(ms) {
  693. return new Promise((resolve) => setTimeout(resolve, ms));
  694. }
  695. function readEnv(name) {
  696. if (typeof process !== 'undefined' && process.env && process.env[name]) {
  697. return process.env[name];
  698. }
  699. return '';
  700. }