ai-queue.js 17 KB

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