ai.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. const config = require('./config');
  2. const REQUEST_TIMEOUT_MS = 120000;
  3. const REQUEST_TIMEOUT_IMG_MS = 180000;
  4. module.exports.REQUEST_TIMEOUT_MS = REQUEST_TIMEOUT_MS;
  5. module.exports.REQUEST_TIMEOUT_IMG_MS = REQUEST_TIMEOUT_IMG_MS;
  6. /**
  7. * run(action, ...args, options?)
  8. * options: { model?: string, timeoutMs?: number } 可选;未传 model 时各 request 使用 config 默认。
  9. * 例:run('text2text', '你好', { model: 'gpt-4o-mini' })
  10. * run('img2text', prompt, imageUrl, { model: 'gpt-4o', timeoutMs: 60000 })
  11. * run('img2text', prompt, [urlScreen, urlTpl], { model: 'gpt-5.4' }) // 多图,如 img-center ROI
  12. */
  13. function request(path, body, timeoutMs) {
  14. const baseUrl = (config.BASE_URL || '').replace(/\/$/, '');
  15. const url = path.startsWith('http') ? path : `${baseUrl}/${path.replace(/^\//, '')}`;
  16. const controller = new AbortController();
  17. const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  18. return fetch(url, {
  19. method: 'POST',
  20. headers: {
  21. 'Content-Type': 'application/json',
  22. 'Authorization': `Bearer ${config.API_KEY || ''}`
  23. },
  24. body: JSON.stringify(body),
  25. signal: controller.signal
  26. })
  27. .then((res) => {
  28. clearTimeout(timeoutId);
  29. return res.json().catch(() => ({})).then((data) => {
  30. if (!res.ok) throw new Error(data.error?.message || data.error || `HTTP ${res.status}`);
  31. return data;
  32. });
  33. })
  34. .catch((e) => {
  35. clearTimeout(timeoutId);
  36. if (e.name === 'AbortError') throw new Error(`请求超时 (${timeoutMs / 1000} 秒)`);
  37. throw e;
  38. });
  39. }
  40. /** images/edits 等:multipart,勿设 Content-Type(由 fetch 带 boundary) */
  41. function requestMultipart (subPath, formData, timeoutMs) {
  42. const baseUrl = (config.BASE_URL || '').replace(/\/$/, '');
  43. const url = subPath.startsWith('http') ? subPath : `${baseUrl}/${String(subPath).replace(/^\//, '')}`;
  44. const controller = new AbortController();
  45. const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  46. return fetch(url, {
  47. method: 'POST',
  48. headers: {
  49. Authorization: `Bearer ${config.API_KEY || ''}`,
  50. },
  51. body: formData,
  52. signal: controller.signal,
  53. })
  54. .then((res) => {
  55. clearTimeout(timeoutId);
  56. return res.json().catch(() => ({})).then((data) => {
  57. if (!res.ok) throw new Error(data.error?.message || data.error || `HTTP ${res.status}`);
  58. return data;
  59. });
  60. })
  61. .catch((e) => {
  62. clearTimeout(timeoutId);
  63. if (e.name === 'AbortError') throw new Error(`请求超时 (${timeoutMs / 1000} 秒)`);
  64. throw e;
  65. });
  66. }
  67. function doubaoRequest(path, body, timeoutMs) {
  68. const baseUrl = (config.DOUBAO_BASE_URL || '').replace(/\/$/, '');
  69. const url = path.startsWith('http') ? path : `${baseUrl}/${path.replace(/^\//, '')}`;
  70. const controller = new AbortController();
  71. const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  72. return fetch(url, {
  73. method: 'POST',
  74. headers: {
  75. 'Content-Type': 'application/json',
  76. 'Authorization': `Bearer ${config.DOUBAO_API_KEY || ''}`
  77. },
  78. body: JSON.stringify(body),
  79. signal: controller.signal
  80. })
  81. .then((res) => {
  82. clearTimeout(timeoutId);
  83. return res.json().catch(() => ({})).then((data) => {
  84. if (!res.ok) throw new Error(data.error?.message || data.error || `HTTP ${res.status}`);
  85. return data;
  86. });
  87. })
  88. .catch((e) => {
  89. clearTimeout(timeoutId);
  90. if (e.name === 'AbortError') throw new Error(`请求超时 (${timeoutMs / 1000} 秒)`);
  91. throw e;
  92. });
  93. }
  94. const text2text = require('./request/text2text');
  95. const img2text = require('./request/img2text');
  96. const text2img = require('./request/text2img');
  97. const img2img = require('./request/img2img');
  98. const REQ = { text2text, img2text, text2img, img2img };
  99. /** 最后一个参数可为 { model?: string, timeoutMs?: number },未传 model 则用各 request 内默认 */
  100. function popRunOptions (args) {
  101. const last = args[args.length - 1];
  102. if (
  103. last != null &&
  104. typeof last === 'object' &&
  105. !Array.isArray(last) &&
  106. (Object.prototype.hasOwnProperty.call(last, 'model') ||
  107. Object.prototype.hasOwnProperty.call(last, 'timeoutMs'))
  108. ) {
  109. return { options: args.pop(), rest: args };
  110. }
  111. return { options: {}, rest: args };
  112. }
  113. async function run (action, ...args) {
  114. const { options, rest } = popRunOptions(args);
  115. const callArgs = rest;
  116. if (action.startsWith('doubao_')) {
  117. return await doRequest(action.substring(7), true, callArgs, options);
  118. }
  119. return await doRequest(action, false, callArgs, options);
  120. }
  121. /** 从 request 模块取参数,在 ai.js 内发请求 */
  122. async function doRequest (action, isDoubao, args, options = {}) {
  123. const req = REQ[action];
  124. if (!req) return { success: false, error: 'Unknown action: ' + action };
  125. if (isDoubao && (!config.DOUBAO_MODEL || !config.DOUBAO_MODEL.trim())) {
  126. return { success: false, error: '豆包未配置:请在 nodejs/ai/config.js 中设置 DOUBAO_MODEL 为你在火山引擎控制台创建的模型接入点 ID(endpoint ID),或设置环境变量 DOUBAO_MODEL' };
  127. }
  128. const path = req.path;
  129. const modelOverride =
  130. options.model != null && String(options.model).trim() !== ''
  131. ? String(options.model).trim()
  132. : undefined;
  133. const timeoutMs =
  134. options.timeoutMs != null && Number.isFinite(Number(options.timeoutMs))
  135. ? Number(options.timeoutMs)
  136. : req.timeoutMs;
  137. try {
  138. if (action === 'img2img' && !isDoubao && typeof req.buildFormData === 'function') {
  139. const form = req.buildFormData(args[0], args[1], modelOverride);
  140. const data = await requestMultipart(path, form, timeoutMs);
  141. return { success: true, data };
  142. }
  143. let body;
  144. if (action === 'text2text') {
  145. body = isDoubao
  146. ? req.getDoubaoBody(args[0], modelOverride)
  147. : req.getBody(args[0], modelOverride);
  148. } else if (action === 'text2img') {
  149. body = isDoubao
  150. ? req.getDoubaoBody(args[0], args[1], modelOverride)
  151. : req.getBody(args[0], args[1], modelOverride);
  152. } else {
  153. body = isDoubao
  154. ? req.getDoubaoBody(args[0], args[1], modelOverride)
  155. : req.getBody(args[0], args[1], modelOverride);
  156. }
  157. const data = isDoubao
  158. ? await doubaoRequest(path, body, timeoutMs)
  159. : await request(path, body, timeoutMs);
  160. return { success: true, data };
  161. } catch (e) {
  162. const msg = e && (e.message || String(e));
  163. const cause = e && e.cause && (e.cause.message || String(e.cause));
  164. const err = cause ? `${msg} (${cause})` : msg;
  165. return { success: false, error: err || '网络请求失败' };
  166. }
  167. }
  168. module.exports.run = run;
  169. module.exports.request = request;
  170. module.exports.REQUEST_TIMEOUT_MS = REQUEST_TIMEOUT_MS;
  171. module.exports.REQUEST_TIMEOUT_IMG_MS = REQUEST_TIMEOUT_IMG_MS;