ai-queue.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. // AI生图队列管理器
  2. const fs = require('fs');
  3. const path = require('path');
  4. const ReplaceCharacterHandler = require('./replace-character');
  5. const AI_HISTORY_FILE = path.join(__dirname, 'ai-history.json');
  6. const QUEUE_FILE = path.join(__dirname, 'ai-queue.json');
  7. // 队列状态
  8. let queue = [];
  9. let isProcessing = false;
  10. // 初始化:加载队列
  11. function initQueue() {
  12. try {
  13. if (fs.existsSync(QUEUE_FILE)) {
  14. const data = fs.readFileSync(QUEUE_FILE, 'utf-8');
  15. queue = JSON.parse(data);
  16. }
  17. } catch (error) {
  18. console.error('[AIQueue] 加载队列失败:', error);
  19. queue = [];
  20. }
  21. }
  22. // 保存队列
  23. function saveQueue() {
  24. try {
  25. fs.writeFileSync(QUEUE_FILE, JSON.stringify(queue, null, 2), 'utf-8');
  26. } catch (error) {
  27. console.error('[AIQueue] 保存队列失败:', error);
  28. }
  29. }
  30. // 加载AI历史
  31. function loadAIHistory() {
  32. try {
  33. if (fs.existsSync(AI_HISTORY_FILE)) {
  34. const data = fs.readFileSync(AI_HISTORY_FILE, 'utf-8');
  35. return JSON.parse(data);
  36. }
  37. } catch (error) {
  38. console.error('[AIQueue] 加载历史失败:', error);
  39. }
  40. return {};
  41. }
  42. // 保存AI历史
  43. function saveAIHistory(history) {
  44. try {
  45. fs.writeFileSync(AI_HISTORY_FILE, JSON.stringify(history, null, 2), 'utf-8');
  46. } catch (error) {
  47. console.error('[AIQueue] 保存历史失败:', error);
  48. }
  49. }
  50. // 添加任务到队列
  51. function addToQueue(username, taskData) {
  52. const taskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  53. const task = {
  54. id: taskId,
  55. username: username.toLowerCase(),
  56. status: queue.length === 0 && !isProcessing ? 'rendering' : 'queued',
  57. createdAt: new Date().toISOString(),
  58. ...taskData
  59. };
  60. queue.push(task);
  61. saveQueue();
  62. // 保存原始图片预览(用于模糊显示)
  63. let previewUrl = null;
  64. if (taskData.image1) {
  65. previewUrl = savePreviewImage(username, taskId, taskData.image1);
  66. }
  67. // 添加到历史记录
  68. const history = loadAIHistory();
  69. if (!history[task.username]) {
  70. history[task.username] = [];
  71. }
  72. history[task.username].unshift({
  73. id: taskId,
  74. status: task.status,
  75. createdAt: task.createdAt,
  76. imageUrl: null,
  77. previewUrl: previewUrl // 原始图片预览
  78. });
  79. saveAIHistory(history);
  80. // 如果队列为空且没有正在处理的任务,立即开始处理
  81. if (queue.length === 1 && !isProcessing) {
  82. processQueue();
  83. }
  84. return taskId;
  85. }
  86. // 保存预览图片(原始texture的缩略图)
  87. function savePreviewImage(username, taskId, imageBase64) {
  88. const usersDir = path.join(__dirname, 'users');
  89. const userDir = path.join(usersDir, username.toLowerCase());
  90. const aiDir = path.join(userDir, 'ai-images');
  91. // 确保目录存在
  92. if (!fs.existsSync(aiDir)) {
  93. fs.mkdirSync(aiDir, { recursive: true });
  94. }
  95. const imagePath = path.join(aiDir, `${taskId}_preview.png`);
  96. const imageBuffer = Buffer.from(imageBase64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
  97. fs.writeFileSync(imagePath, imageBuffer);
  98. return `/api/ai/preview?username=${encodeURIComponent(username)}&id=${encodeURIComponent(taskId)}`;
  99. }
  100. // 处理队列
  101. async function processQueue() {
  102. if (isProcessing || queue.length === 0) {
  103. return;
  104. }
  105. isProcessing = true;
  106. while (queue.length > 0) {
  107. const task = queue[0];
  108. // 更新状态为rendering
  109. if (task.status === 'queued') {
  110. task.status = 'rendering';
  111. updateTaskStatus(task.id, 'rendering');
  112. }
  113. try {
  114. console.log(`[AIQueue] 开始处理任务: ${task.id}`);
  115. // 调用Gemini API
  116. const result = await callGeminiAPIWithPromise(
  117. task.image1,
  118. task.image2,
  119. task.image1Width,
  120. task.image1Height,
  121. task.additionalPrompt || ''
  122. );
  123. if (result.success && result.imageData) {
  124. // 保存图片到用户目录
  125. const imageUrl = await saveAIImage(task.username, task.id, result.imageData);
  126. // 更新任务状态
  127. task.status = 'completed';
  128. task.imageUrl = imageUrl;
  129. task.completedAt = new Date().toISOString();
  130. updateTaskStatus(task.id, 'completed', imageUrl);
  131. console.log(`[AIQueue] 任务完成: ${task.id}`);
  132. } else {
  133. // Gemini API 明确返回失败
  134. const apiError = new Error(result.error || '生成失败');
  135. apiError.isApiError = true;
  136. throw apiError;
  137. }
  138. } catch (error) {
  139. console.error(`[AIQueue] 任务失败: ${task.id}`, error);
  140. // 只有 Gemini API 明确返回失败时才标记为 failed
  141. if (error.isApiError) {
  142. task.status = 'failed';
  143. task.error = error.message;
  144. task.completedAt = new Date().toISOString();
  145. // 保存原始任务数据用于重试(不包含图片数据以节省空间,重试时从预览图重新加载)
  146. updateTaskStatus(task.id, 'failed', null, error.message, {
  147. image1Width: task.image1Width,
  148. image1Height: task.image1Height,
  149. additionalPrompt: task.additionalPrompt
  150. });
  151. } else {
  152. // 其他错误(代码错误、网络错误等)自动重试
  153. console.log(`[AIQueue] 非API错误,将任务重新加入队列末尾: ${task.id}`);
  154. task.status = 'queued';
  155. task.retryCount = (task.retryCount || 0) + 1;
  156. // 最多重试3次
  157. if (task.retryCount <= 3) {
  158. queue.push({ ...task });
  159. updateTaskStatus(task.id, 'queued');
  160. } else {
  161. console.error(`[AIQueue] 任务重试次数超过限制,标记为失败: ${task.id}`);
  162. task.status = 'failed';
  163. task.error = '多次重试失败:' + error.message;
  164. task.completedAt = new Date().toISOString();
  165. updateTaskStatus(task.id, 'failed', null, task.error, {
  166. image1Width: task.image1Width,
  167. image1Height: task.image1Height,
  168. additionalPrompt: task.additionalPrompt
  169. });
  170. }
  171. }
  172. }
  173. // 从队列中移除
  174. queue.shift();
  175. saveQueue();
  176. }
  177. isProcessing = false;
  178. }
  179. // 调用Gemini API(Promise版本)
  180. function callGeminiAPIWithPromise(image1Base64, image2Base64, image1Width, image1Height, additionalPrompt) {
  181. return new Promise((resolve, reject) => {
  182. const https = require('https');
  183. // 移除 data:image/png;base64, 前缀(如果有)
  184. const cleanImage1 = image1Base64.replace(/^data:image\/\w+;base64,/, '');
  185. const cleanImage2 = image2Base64.replace(/^data:image\/\w+;base64,/, '');
  186. // 构建请求内容
  187. const content = [
  188. {
  189. type: "image_url",
  190. image_url: {
  191. url: `data:image/png;base64,${cleanImage1}`
  192. }
  193. },
  194. {
  195. type: "image_url",
  196. image_url: {
  197. url: `data:image/png;base64,${cleanImage2}`
  198. }
  199. },
  200. {
  201. type: "text",
  202. text: ReplaceCharacterHandler.buildPromptText(image1Width, image1Height, additionalPrompt)
  203. }
  204. ];
  205. const requestData = JSON.stringify({
  206. model: "gemini-3-pro-image-preview",
  207. messages: [{ role: "user", content: content }]
  208. });
  209. const options = {
  210. hostname: 'api.chatanywhere.tech',
  211. port: 443,
  212. path: '/v1/chat/completions',
  213. method: 'POST',
  214. headers: {
  215. 'Authorization': 'Bearer sk-j32LgDixK6pfESYGfJtgc2Tzlmszx5NZhSH0sOzpLQkYuKek',
  216. 'Content-Type': 'application/json',
  217. 'Content-Length': Buffer.byteLength(requestData)
  218. },
  219. timeout: 300000 // 5分钟超时
  220. };
  221. console.log('[AIQueue] 正在调用 Gemini API...');
  222. const geminiReq = https.request(options, (geminiRes) => {
  223. let responseData = '';
  224. geminiRes.on('data', (chunk) => {
  225. responseData += chunk;
  226. });
  227. geminiRes.on('end', () => {
  228. try {
  229. if (geminiRes.statusCode !== 200) {
  230. console.error('[AIQueue] Gemini API 返回错误:', geminiRes.statusCode, responseData);
  231. reject(new Error(`Gemini API error: ${geminiRes.statusCode}`));
  232. return;
  233. }
  234. const response = JSON.parse(responseData);
  235. // 解析响应,提取图片
  236. const imageData = ReplaceCharacterHandler.extractImageFromResponse(response);
  237. if (!imageData) {
  238. console.error('[AIQueue] 无法从响应中提取图片');
  239. reject(new Error('Failed to extract image from response'));
  240. return;
  241. }
  242. resolve({
  243. success: true,
  244. imageData: imageData
  245. });
  246. console.log('[AIQueue] ✓ 成功处理请求');
  247. } catch (error) {
  248. console.error('[AIQueue] 解析响应失败:', error);
  249. reject(error);
  250. }
  251. });
  252. });
  253. geminiReq.on('error', (error) => {
  254. console.error('[AIQueue] 请求错误:', error);
  255. reject(error);
  256. });
  257. geminiReq.on('timeout', () => {
  258. console.error('[AIQueue] 请求超时');
  259. geminiReq.destroy();
  260. reject(new Error('Request timeout'));
  261. });
  262. geminiReq.write(requestData);
  263. geminiReq.end();
  264. });
  265. }
  266. // 保存AI生成的图片
  267. function saveAIImage(username, taskId, imageBase64) {
  268. const usersDir = path.join(__dirname, 'users');
  269. const userDir = path.join(usersDir, username.toLowerCase());
  270. const aiDir = path.join(userDir, 'ai-images');
  271. // 确保目录存在
  272. if (!fs.existsSync(aiDir)) {
  273. fs.mkdirSync(aiDir, { recursive: true });
  274. }
  275. const imagePath = path.join(aiDir, `${taskId}.png`);
  276. const imageBuffer = Buffer.from(imageBase64, 'base64');
  277. fs.writeFileSync(imagePath, imageBuffer);
  278. return `/api/ai/image?username=${encodeURIComponent(username)}&id=${encodeURIComponent(taskId)}`;
  279. }
  280. // 更新任务状态
  281. function updateTaskStatus(taskId, status, imageUrl = null, error = null, retryData = null) {
  282. const history = loadAIHistory();
  283. for (const username in history) {
  284. const userHistory = history[username];
  285. const task = userHistory.find(t => t.id === taskId);
  286. if (task) {
  287. task.status = status;
  288. if (imageUrl) {
  289. task.imageUrl = imageUrl;
  290. }
  291. if (error) {
  292. task.error = error;
  293. }
  294. if (retryData) {
  295. task.retryData = retryData; // 保存重试所需数据
  296. }
  297. if (status === 'completed' || status === 'failed') {
  298. task.completedAt = new Date().toISOString();
  299. }
  300. break;
  301. }
  302. }
  303. saveAIHistory(history);
  304. }
  305. // 获取用户AI历史
  306. function getUserAIHistory(username) {
  307. const history = loadAIHistory();
  308. const normalizedUsername = username.toLowerCase();
  309. return history[normalizedUsername] || [];
  310. }
  311. // 处理AI生图请求(队列版本)
  312. function handleAIRequest(req, res) {
  313. if (req.method !== 'POST') {
  314. res.writeHead(405, { 'Content-Type': 'application/json' });
  315. res.end(JSON.stringify({ error: 'Method not allowed' }));
  316. return;
  317. }
  318. let body = '';
  319. req.on('data', (chunk) => {
  320. body += chunk.toString();
  321. });
  322. req.on('end', () => {
  323. try {
  324. const data = JSON.parse(body);
  325. const { username, image1, image2, image1Width, image1Height, additionalPrompt } = data;
  326. if (!username) {
  327. res.writeHead(400, { 'Content-Type': 'application/json' });
  328. res.end(JSON.stringify({ success: false, error: '缺少用户名参数' }));
  329. return;
  330. }
  331. if (!image1 || !image2) {
  332. res.writeHead(400, { 'Content-Type': 'application/json' });
  333. res.end(JSON.stringify({ success: false, error: 'Missing required fields: image1, image2' }));
  334. return;
  335. }
  336. if (!image1Width || !image1Height) {
  337. res.writeHead(400, { 'Content-Type': 'application/json' });
  338. res.end(JSON.stringify({ success: false, error: 'Missing required fields: image1Width, image1Height' }));
  339. return;
  340. }
  341. // 添加到队列
  342. const taskId = addToQueue(username, {
  343. image1,
  344. image2,
  345. image1Width,
  346. image1Height,
  347. additionalPrompt: additionalPrompt || ''
  348. });
  349. // 立即返回任务ID
  350. res.writeHead(200, { 'Content-Type': 'application/json' });
  351. res.end(JSON.stringify({
  352. success: true,
  353. taskId: taskId,
  354. message: '请求生图成功,正在处理中...'
  355. }));
  356. // 异步处理队列
  357. processQueue();
  358. } catch (error) {
  359. console.error('[AIQueue] 处理请求失败:', error);
  360. res.writeHead(500, { 'Content-Type': 'application/json' });
  361. res.end(JSON.stringify({ success: false, error: '处理失败', details: error.message }));
  362. }
  363. });
  364. req.on('error', (error) => {
  365. console.error('[AIQueue] 请求错误:', error);
  366. res.writeHead(500, { 'Content-Type': 'application/json' });
  367. res.end(JSON.stringify({ success: false, error: 'Request error', details: error.message }));
  368. });
  369. }
  370. // 重试失败的任务(免费)
  371. function retryTask(taskId, username) {
  372. const history = loadAIHistory();
  373. const userHistory = history[username.toLowerCase()];
  374. if (!userHistory) {
  375. return { success: false, error: '用户历史不存在' };
  376. }
  377. const task = userHistory.find(t => t.id === taskId);
  378. if (!task) {
  379. return { success: false, error: '任务不存在' };
  380. }
  381. if (task.status !== 'failed') {
  382. return { success: false, error: '只能重试失败的任务' };
  383. }
  384. // 读取预览图作为 image1
  385. const previewPath = path.join(__dirname, 'users', username.toLowerCase(), 'ai-images', `${taskId}_preview.png`);
  386. if (!fs.existsSync(previewPath)) {
  387. return { success: false, error: '预览图不存在,无法重试' };
  388. }
  389. const previewData = fs.readFileSync(previewPath);
  390. const image1Base64 = previewData.toString('base64');
  391. // 生成新任务ID
  392. const newTaskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  393. // 获取重试数据
  394. const retryData = task.retryData || {};
  395. // 创建新任务
  396. const newTask = {
  397. id: newTaskId,
  398. username: username.toLowerCase(),
  399. status: queue.length === 0 && !isProcessing ? 'rendering' : 'queued',
  400. createdAt: new Date().toISOString(),
  401. image1: image1Base64,
  402. image2: image1Base64, // 重试时使用同一张图
  403. image1Width: retryData.image1Width || 512,
  404. image1Height: retryData.image1Height || 512,
  405. additionalPrompt: retryData.additionalPrompt || '',
  406. isRetry: true,
  407. originalTaskId: taskId
  408. };
  409. queue.push(newTask);
  410. saveQueue();
  411. // 保存预览图到新任务
  412. const newPreviewPath = path.join(__dirname, 'users', username.toLowerCase(), 'ai-images', `${newTaskId}_preview.png`);
  413. fs.copyFileSync(previewPath, newPreviewPath);
  414. // 添加到历史记录
  415. userHistory.unshift({
  416. id: newTaskId,
  417. status: newTask.status,
  418. createdAt: newTask.createdAt,
  419. imageUrl: null,
  420. previewUrl: `/api/ai/preview?username=${encodeURIComponent(username)}&id=${encodeURIComponent(newTaskId)}`,
  421. isRetry: true
  422. });
  423. // 将原任务标记为已重试
  424. task.retried = true;
  425. task.retriedTaskId = newTaskId;
  426. saveAIHistory(history);
  427. // 开始处理队列
  428. if (queue.length === 1 && !isProcessing) {
  429. processQueue();
  430. }
  431. return { success: true, taskId: newTaskId };
  432. }
  433. // 处理重试请求
  434. function handleRetryRequest(req, res) {
  435. let body = '';
  436. req.on('data', chunk => {
  437. body += chunk.toString();
  438. });
  439. req.on('end', () => {
  440. try {
  441. const { taskId, username } = JSON.parse(body);
  442. if (!taskId || !username) {
  443. res.writeHead(400, { 'Content-Type': 'application/json' });
  444. res.end(JSON.stringify({ success: false, error: '缺少必要参数' }));
  445. return;
  446. }
  447. const result = retryTask(taskId, username);
  448. res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
  449. res.end(JSON.stringify(result));
  450. } catch (error) {
  451. console.error('[AIQueue] 重试请求失败:', error);
  452. res.writeHead(500, { 'Content-Type': 'application/json' });
  453. res.end(JSON.stringify({ success: false, error: '处理失败', details: error.message }));
  454. }
  455. });
  456. }
  457. // 初始化
  458. initQueue();
  459. module.exports = {
  460. handleAIRequest,
  461. handleRetryRequest,
  462. getUserAIHistory,
  463. processQueue
  464. };