/** * 云函数:jimengManager * 代理即梦图片、视频、数字人、动作迁移等任务接口,避免前端暴露上游地址和 token。 * * 部署后把函数 objectId 填入 src/app/services/cloud-functions.ts 的 jimeng。 */ const JIMENG_TOKEN = readEnv('JIMENG_TOKEN') || 'Bearer r:f0333969e312a40e4703e8fe4ed1c600'; const JIMENG_BASE_URL = readEnv('JIMENG_BASE_URL') || 'https://server.fmode.cn/api/volcengine/jimeng'; const PARSE_BASE_URL = readEnv('PARSE_BASE_URL') || 'https://server.fmode.cn/parse'; const PARSE_APP_ID = readEnv('PARSE_APP_ID') || 'ncloudmaster'; const REQUEST_TIMEOUT_MS = Number(readEnv('JIMENG_REQUEST_TIMEOUT_MS') || 60000); const MAX_ATTEMPTS = Math.max(1, Number(readEnv('JIMENG_MAX_ATTEMPTS') || 3)); const ALLOWED_ENDPOINTS = new Set([ 'getVideoV3_720p', 'getVideoV3_1080p', 'getVideoV3_Pro', 'getText2ImgV31', 'getImgV4', 'getDataByTask02', 'getOhIdentifyMain', 'getOhDateByTask', 'getOmniHuman', 'getActorV2', ]); async function handler(request, response) { try { const action = pickParam(request, 'action') || 'call'; if (action === 'call') { const endpoint = String(pickParam(request, 'endpoint') || '').trim(); const payload = pickParam(request, 'payload', 'data') || {}; if (!ALLOWED_ENDPOINTS.has(endpoint)) { return response.json({ code: 400, success: false, error: '不支持的生成接口' }); } const upstreamPayload = stripEmpty({ ...payload, token: JIMENG_TOKEN }); const data = await postJson(`${JIMENG_BASE_URL}/${endpoint}`, upstreamPayload); response.json(data); return; } if (action === 'getWorkResult') { const workId = String(pickParam(request, 'workId', 'objectId') || '').trim(); if (!workId) { return response.json({ code: 400, success: false, error: '缺少作品 ID' }); } const data = await getJson(`${PARSE_BASE_URL}/classes/ImagineWork/${encodeURIComponent(workId)}`, { 'X-Parse-Application-Id': PARSE_APP_ID, }); response.json({ code: 200, success: true, data }); return; } response.json({ code: 400, success: false, error: `未知 action: ${action}` }); } catch (error) { console.error('jimengManager failed:', error && error.message ? error.message : error); response.json({ code: 500, success: false, error: error && error.message ? error.message : '生成服务调用失败' }); } } function pickParam(request, ...names) { const sources = [request.params, request.body, request]; for (const src of sources) { if (!src || typeof src !== 'object') continue; for (const n of names) { const v = src[n]; if (v !== undefined && v !== null && v !== '') return v; } } 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; } async function postJson(url, body) { return requestJson(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); } async function getJson(url, headers) { const data = await requestJson(url, { method: 'GET', headers }); if (data && data.success === false) throw new Error(data.error || '查询结果失败'); return data; } async function requestJson(url, options) { let lastError = null; for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { try { const r = await fetchWithTimeout(url, options, REQUEST_TIMEOUT_MS); const data = await readResponse(r, url, attempt); if (data && data.success === false && isRetryableStatus(data.code) && attempt < MAX_ATTEMPTS) { await sleep(backoffMs(attempt)); continue; } return data; } catch (error) { lastError = error; if (!isRetryableNetworkError(error) || attempt >= MAX_ATTEMPTS) break; await sleep(backoffMs(attempt)); } } const message = lastError && lastError.message ? lastError.message : 'fetch failed'; throw new Error(`即梦上游网络请求失败:${message};url=${maskUrl(url)};attempts=${MAX_ATTEMPTS}`); } async function fetchWithTimeout(url, options, timeoutMs) { if (typeof AbortController === 'undefined') { return fetch(url, options); } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(url, { ...options, signal: controller.signal }); } finally { clearTimeout(timer); } } async function readResponse(r, url, attempt) { const text = await r.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch {} if (!r.ok) { return { code: r.status, success: false, error: readError(data, text) || `HTTP ${r.status}`, upstream: { status: r.status, url: maskUrl(url), attempt, maxAttempts: MAX_ATTEMPTS, }, }; } return data || { code: 500, success: false, error: '服务返回异常' }; } function isRetryableStatus(status) { const code = Number(status || 0); return code === 408 || code === 429 || code >= 500; } function isRetryableNetworkError(error) { const message = error && error.message ? error.message : String(error || ''); return /fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|network|abort|timeout/i.test(message); } function backoffMs(attempt) { return Math.min(12000, 1200 * attempt * attempt); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function maskUrl(url) { try { const u = new URL(url); return `${u.origin}${u.pathname}`; } catch { return String(url || '').split('?')[0]; } } function readError(data, fallback) { return data?.error?.message || data?.error || data?.message || data?.msg || fallback || ''; } function readEnv(name) { if (typeof process !== 'undefined' && process.env && process.env[name]) { return process.env[name]; } return ''; }