const config = require('./config'); const REQUEST_TIMEOUT_MS = 120000; const REQUEST_TIMEOUT_IMG_MS = 180000; module.exports.REQUEST_TIMEOUT_MS = REQUEST_TIMEOUT_MS; module.exports.REQUEST_TIMEOUT_IMG_MS = REQUEST_TIMEOUT_IMG_MS; /** * run(action, ...args, options?) * options: { model?: string, timeoutMs?: number } 可选;未传 model 时各 request 使用 config 默认。 * 例:run('text2text', '你好', { model: 'gpt-4o-mini' }) * run('img2text', prompt, imageUrl, { model: 'gpt-4o', timeoutMs: 60000 }) * run('img2text', prompt, [urlScreen, urlTpl], { model: 'gpt-5.4' }) // 多图,如 img-center ROI */ function request(path, body, timeoutMs) { const baseUrl = (config.BASE_URL || '').replace(/\/$/, ''); const url = path.startsWith('http') ? path : `${baseUrl}/${path.replace(/^\//, '')}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.API_KEY || ''}` }, body: JSON.stringify(body), signal: controller.signal }) .then((res) => { clearTimeout(timeoutId); return res.json().catch(() => ({})).then((data) => { if (!res.ok) throw new Error(data.error?.message || data.error || `HTTP ${res.status}`); return data; }); }) .catch((e) => { clearTimeout(timeoutId); if (e.name === 'AbortError') throw new Error(`请求超时 (${timeoutMs / 1000} 秒)`); throw e; }); } /** images/edits 等:multipart,勿设 Content-Type(由 fetch 带 boundary) */ function requestMultipart (subPath, formData, timeoutMs) { const baseUrl = (config.BASE_URL || '').replace(/\/$/, ''); const url = subPath.startsWith('http') ? subPath : `${baseUrl}/${String(subPath).replace(/^\//, '')}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); return fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${config.API_KEY || ''}`, }, body: formData, signal: controller.signal, }) .then((res) => { clearTimeout(timeoutId); return res.json().catch(() => ({})).then((data) => { if (!res.ok) throw new Error(data.error?.message || data.error || `HTTP ${res.status}`); return data; }); }) .catch((e) => { clearTimeout(timeoutId); if (e.name === 'AbortError') throw new Error(`请求超时 (${timeoutMs / 1000} 秒)`); throw e; }); } function doubaoRequest(path, body, timeoutMs) { const baseUrl = (config.DOUBAO_BASE_URL || '').replace(/\/$/, ''); const url = path.startsWith('http') ? path : `${baseUrl}/${path.replace(/^\//, '')}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.DOUBAO_API_KEY || ''}` }, body: JSON.stringify(body), signal: controller.signal }) .then((res) => { clearTimeout(timeoutId); return res.json().catch(() => ({})).then((data) => { if (!res.ok) throw new Error(data.error?.message || data.error || `HTTP ${res.status}`); return data; }); }) .catch((e) => { clearTimeout(timeoutId); if (e.name === 'AbortError') throw new Error(`请求超时 (${timeoutMs / 1000} 秒)`); throw e; }); } const text2text = require('./request/text2text'); const img2text = require('./request/img2text'); const text2img = require('./request/text2img'); const img2img = require('./request/img2img'); const REQ = { text2text, img2text, text2img, img2img }; /** 最后一个参数可为 { model?: string, timeoutMs?: number },未传 model 则用各 request 内默认 */ function popRunOptions (args) { const last = args[args.length - 1]; if ( last != null && typeof last === 'object' && !Array.isArray(last) && (Object.prototype.hasOwnProperty.call(last, 'model') || Object.prototype.hasOwnProperty.call(last, 'timeoutMs')) ) { return { options: args.pop(), rest: args }; } return { options: {}, rest: args }; } async function run (action, ...args) { const { options, rest } = popRunOptions(args); const callArgs = rest; if (action.startsWith('doubao_')) { return await doRequest(action.substring(7), true, callArgs, options); } return await doRequest(action, false, callArgs, options); } /** 从 request 模块取参数,在 ai.js 内发请求 */ async function doRequest (action, isDoubao, args, options = {}) { const req = REQ[action]; if (!req) return { success: false, error: 'Unknown action: ' + action }; if (isDoubao && (!config.DOUBAO_MODEL || !config.DOUBAO_MODEL.trim())) { return { success: false, error: '豆包未配置:请在 nodejs/ai/config.js 中设置 DOUBAO_MODEL 为你在火山引擎控制台创建的模型接入点 ID(endpoint ID),或设置环境变量 DOUBAO_MODEL' }; } const path = req.path; const modelOverride = options.model != null && String(options.model).trim() !== '' ? String(options.model).trim() : undefined; const timeoutMs = options.timeoutMs != null && Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : req.timeoutMs; try { if (action === 'img2img' && !isDoubao && typeof req.buildFormData === 'function') { const form = req.buildFormData(args[0], args[1], modelOverride); const data = await requestMultipart(path, form, timeoutMs); return { success: true, data }; } let body; if (action === 'text2text') { body = isDoubao ? req.getDoubaoBody(args[0], modelOverride) : req.getBody(args[0], modelOverride); } else if (action === 'text2img') { body = isDoubao ? req.getDoubaoBody(args[0], args[1], modelOverride) : req.getBody(args[0], args[1], modelOverride); } else { body = isDoubao ? req.getDoubaoBody(args[0], args[1], modelOverride) : req.getBody(args[0], args[1], modelOverride); } const data = isDoubao ? await doubaoRequest(path, body, timeoutMs) : await request(path, body, timeoutMs); return { success: true, data }; } catch (e) { const msg = e && (e.message || String(e)); const cause = e && e.cause && (e.cause.message || String(e.cause)); const err = cause ? `${msg} (${cause})` : msg; return { success: false, error: err || '网络请求失败' }; } } module.exports.run = run; module.exports.request = request; module.exports.REQUEST_TIMEOUT_MS = REQUEST_TIMEOUT_MS; module.exports.REQUEST_TIMEOUT_IMG_MS = REQUEST_TIMEOUT_IMG_MS;