11-jimengManager.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /**
  2. * 云函数:jimengManager
  3. * 代理即梦图片、视频、数字人、动作迁移等任务接口,避免前端暴露上游地址和 token。
  4. *
  5. * 部署后把函数 objectId 填入 src/app/services/cloud-functions.ts 的 jimeng。
  6. */
  7. const JIMENG_TOKEN = readEnv('JIMENG_TOKEN') || 'Bearer r:f0333969e312a40e4703e8fe4ed1c600';
  8. const JIMENG_BASE_URL = readEnv('JIMENG_BASE_URL') || 'https://server.fmode.cn/api/volcengine/jimeng';
  9. const PARSE_BASE_URL = readEnv('PARSE_BASE_URL') || 'https://server.fmode.cn/parse';
  10. const PARSE_APP_ID = readEnv('PARSE_APP_ID') || 'ncloudmaster';
  11. const REQUEST_TIMEOUT_MS = Number(readEnv('JIMENG_REQUEST_TIMEOUT_MS') || 60000);
  12. const MAX_ATTEMPTS = Math.max(1, Number(readEnv('JIMENG_MAX_ATTEMPTS') || 3));
  13. const ALLOWED_ENDPOINTS = new Set([
  14. 'getVideoV3_720p',
  15. 'getVideoV3_1080p',
  16. 'getVideoV3_Pro',
  17. 'getText2ImgV31',
  18. 'getImgV4',
  19. 'getDataByTask02',
  20. 'getOhIdentifyMain',
  21. 'getOhDateByTask',
  22. 'getOmniHuman',
  23. 'getActorV2',
  24. ]);
  25. async function handler(request, response) {
  26. try {
  27. const action = pickParam(request, 'action') || 'call';
  28. if (action === 'call') {
  29. const endpoint = String(pickParam(request, 'endpoint') || '').trim();
  30. const payload = pickParam(request, 'payload', 'data') || {};
  31. if (!ALLOWED_ENDPOINTS.has(endpoint)) {
  32. return response.json({ code: 400, success: false, error: '不支持的生成接口' });
  33. }
  34. const upstreamPayload = stripEmpty({ ...payload, token: JIMENG_TOKEN });
  35. const data = await postJson(`${JIMENG_BASE_URL}/${endpoint}`, upstreamPayload);
  36. response.json(data);
  37. return;
  38. }
  39. if (action === 'getWorkResult') {
  40. const workId = String(pickParam(request, 'workId', 'objectId') || '').trim();
  41. if (!workId) {
  42. return response.json({ code: 400, success: false, error: '缺少作品 ID' });
  43. }
  44. const data = await getJson(`${PARSE_BASE_URL}/classes/ImagineWork/${encodeURIComponent(workId)}`, {
  45. 'X-Parse-Application-Id': PARSE_APP_ID,
  46. });
  47. response.json({ code: 200, success: true, data });
  48. return;
  49. }
  50. response.json({ code: 400, success: false, error: `未知 action: ${action}` });
  51. } catch (error) {
  52. console.error('jimengManager failed:', error && error.message ? error.message : error);
  53. response.json({ code: 500, success: false, error: error && error.message ? error.message : '生成服务调用失败' });
  54. }
  55. }
  56. function pickParam(request, ...names) {
  57. const sources = [request.params, request.body, request];
  58. for (const src of sources) {
  59. if (!src || typeof src !== 'object') continue;
  60. for (const n of names) {
  61. const v = src[n];
  62. if (v !== undefined && v !== null && v !== '') return v;
  63. }
  64. }
  65. return null;
  66. }
  67. function stripEmpty(value) {
  68. if (!value || typeof value !== 'object') return value;
  69. const out = Array.isArray(value) ? [] : {};
  70. for (const [key, val] of Object.entries(value)) {
  71. if (val === undefined || val === null || val === '') continue;
  72. if (val && typeof val === 'object' && !Array.isArray(val)) {
  73. const nested = stripEmpty(val);
  74. if (Object.keys(nested).length) out[key] = nested;
  75. } else {
  76. out[key] = val;
  77. }
  78. }
  79. return out;
  80. }
  81. async function postJson(url, body) {
  82. return requestJson(url, {
  83. method: 'POST',
  84. headers: { 'Content-Type': 'application/json' },
  85. body: JSON.stringify(body),
  86. });
  87. }
  88. async function getJson(url, headers) {
  89. const data = await requestJson(url, { method: 'GET', headers });
  90. if (data && data.success === false) throw new Error(data.error || '查询结果失败');
  91. return data;
  92. }
  93. async function requestJson(url, options) {
  94. let lastError = null;
  95. for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
  96. try {
  97. const r = await fetchWithTimeout(url, options, REQUEST_TIMEOUT_MS);
  98. const data = await readResponse(r, url, attempt);
  99. if (data && data.success === false && isRetryableStatus(data.code) && attempt < MAX_ATTEMPTS) {
  100. await sleep(backoffMs(attempt));
  101. continue;
  102. }
  103. return data;
  104. } catch (error) {
  105. lastError = error;
  106. if (!isRetryableNetworkError(error) || attempt >= MAX_ATTEMPTS) break;
  107. await sleep(backoffMs(attempt));
  108. }
  109. }
  110. const message = lastError && lastError.message ? lastError.message : 'fetch failed';
  111. throw new Error(`即梦上游网络请求失败:${message};url=${maskUrl(url)};attempts=${MAX_ATTEMPTS}`);
  112. }
  113. async function fetchWithTimeout(url, options, timeoutMs) {
  114. if (typeof AbortController === 'undefined') {
  115. return fetch(url, options);
  116. }
  117. const controller = new AbortController();
  118. const timer = setTimeout(() => controller.abort(), timeoutMs);
  119. try {
  120. return await fetch(url, { ...options, signal: controller.signal });
  121. } finally {
  122. clearTimeout(timer);
  123. }
  124. }
  125. async function readResponse(r, url, attempt) {
  126. const text = await r.text();
  127. let data = null;
  128. try { data = text ? JSON.parse(text) : null; } catch {}
  129. if (!r.ok) {
  130. return {
  131. code: r.status,
  132. success: false,
  133. error: readError(data, text) || `HTTP ${r.status}`,
  134. upstream: {
  135. status: r.status,
  136. url: maskUrl(url),
  137. attempt,
  138. maxAttempts: MAX_ATTEMPTS,
  139. },
  140. };
  141. }
  142. return data || { code: 500, success: false, error: '服务返回异常' };
  143. }
  144. function isRetryableStatus(status) {
  145. const code = Number(status || 0);
  146. return code === 408 || code === 429 || code >= 500;
  147. }
  148. function isRetryableNetworkError(error) {
  149. const message = error && error.message ? error.message : String(error || '');
  150. return /fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|network|abort|timeout/i.test(message);
  151. }
  152. function backoffMs(attempt) {
  153. return Math.min(12000, 1200 * attempt * attempt);
  154. }
  155. function sleep(ms) {
  156. return new Promise(resolve => setTimeout(resolve, ms));
  157. }
  158. function maskUrl(url) {
  159. try {
  160. const u = new URL(url);
  161. return `${u.origin}${u.pathname}`;
  162. } catch {
  163. return String(url || '').split('?')[0];
  164. }
  165. }
  166. function readError(data, fallback) {
  167. return data?.error?.message || data?.error || data?.message || data?.msg || fallback || '';
  168. }
  169. function readEnv(name) {
  170. if (typeof process !== 'undefined' && process.env && process.env[name]) {
  171. return process.env[name];
  172. }
  173. return '';
  174. }