ai-queue.js 13 KB

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