queue.service.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. "use strict";
  2. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
  3. if (k2 === undefined) k2 = k;
  4. var desc = Object.getOwnPropertyDescriptor(m, k);
  5. if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
  6. desc = { enumerable: true, get: function() { return m[k]; } };
  7. }
  8. Object.defineProperty(o, k2, desc);
  9. }) : (function(o, m, k, k2) {
  10. if (k2 === undefined) k2 = k;
  11. o[k2] = m[k];
  12. }));
  13. var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
  14. Object.defineProperty(o, "default", { enumerable: true, value: v });
  15. }) : function(o, v) {
  16. o["default"] = v;
  17. });
  18. var __importStar = (this && this.__importStar) || (function () {
  19. var ownKeys = function(o) {
  20. ownKeys = Object.getOwnPropertyNames || function (o) {
  21. var ar = [];
  22. for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
  23. return ar;
  24. };
  25. return ownKeys(o);
  26. };
  27. return function (mod) {
  28. if (mod && mod.__esModule) return mod;
  29. var result = {};
  30. if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
  31. __setModuleDefault(result, mod);
  32. return result;
  33. };
  34. })();
  35. Object.defineProperty(exports, "__esModule", { value: true });
  36. exports.queueService = void 0;
  37. const models_1 = require("../../models");
  38. const TtsService = __importStar(require("../tts/tts.service"));
  39. const MAX_CONCURRENCY = 2;
  40. const POLL_INTERVAL_MS = 1000;
  41. class QueueService {
  42. constructor() {
  43. this.isRunning = false;
  44. this.activeCount = 0;
  45. this.timer = null;
  46. }
  47. isRunning = false;
  48. activeCount = 0;
  49. timer = null;
  50. /**
  51. * 添加任务到队列
  52. */
  53. async addTask(userId, text, voiceId, voiceParams) {
  54. if (!text || !text.trim()) {
  55. throw new Error('请输入要转换的文本');
  56. }
  57. if (!voiceId) {
  58. throw new Error('请选择音色');
  59. }
  60. const existing = await models_1.prisma.audioRecord.findFirst({
  61. where: {
  62. userId: userId || null,
  63. status: 'pending',
  64. },
  65. });
  66. if (existing) {
  67. throw new Error('已有待处理任务,请等待完成后再添加');
  68. }
  69. const params = {
  70. speed: voiceParams?.speed || 1.0,
  71. pitch: voiceParams?.pitch || 0,
  72. volume: voiceParams?.volume || 50,
  73. };
  74. const record = await models_1.prisma.audioRecord.create({
  75. data: {
  76. userId: userId || null,
  77. audioId: `queue_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
  78. title: text.slice(0, 50),
  79. text,
  80. wordCount: text.length,
  81. voiceId,
  82. voiceParams: JSON.stringify(params),
  83. status: 'pending',
  84. },
  85. });
  86. if (this.isRunning) {
  87. this._poll();
  88. }
  89. return { id: record.id, audioId: record.audioId, status: record.status };
  90. }
  91. /**
  92. * 启动队列
  93. */
  94. start() {
  95. if (this.isRunning) {
  96. return { running: true, message: '队列已在运行中' };
  97. }
  98. this.isRunning = true;
  99. console.log('[QueueService] 队列已启动 (并发=' + MAX_CONCURRENCY + ')');
  100. this._poll();
  101. return { running: true, message: '队列已启动' };
  102. }
  103. /**
  104. * 停止队列
  105. */
  106. stop() {
  107. if (!this.isRunning) {
  108. return { running: false, message: '队列已停止' };
  109. }
  110. this.isRunning = false;
  111. if (this.timer) {
  112. clearTimeout(this.timer);
  113. this.timer = null;
  114. }
  115. console.log('[QueueService] 队列已停止 (活跃任务=' + this.activeCount + ')');
  116. return { running: false, message: '队列已停止,正在处理的任务会继续完成' };
  117. }
  118. /**
  119. * 获取队列状态
  120. */
  121. async getStatus() {
  122. const [pending, processing, completed, failed] = await Promise.all([
  123. models_1.prisma.audioRecord.count({ where: { status: 'pending' } }),
  124. models_1.prisma.audioRecord.count({ where: { status: 'processing' } }),
  125. models_1.prisma.audioRecord.count({ where: { status: 'completed' } }),
  126. models_1.prisma.audioRecord.count({ where: { status: 'failed' } }),
  127. ]);
  128. return {
  129. running: this.isRunning,
  130. activeCount: this.activeCount,
  131. maxConcurrency: MAX_CONCURRENCY,
  132. pending,
  133. processing,
  134. completed,
  135. failed,
  136. };
  137. }
  138. /**
  139. * 获取任务列表
  140. */
  141. async getTasks(status, page = 1, pageSize = 20) {
  142. const where = {};
  143. if (status) {
  144. where.status = status;
  145. }
  146. const [tasks, total] = await Promise.all([
  147. models_1.prisma.audioRecord.findMany({
  148. where,
  149. orderBy: { createdAt: 'desc' },
  150. skip: (page - 1) * pageSize,
  151. take: pageSize,
  152. select: {
  153. id: true,
  154. audioId: true,
  155. title: true,
  156. wordCount: true,
  157. voiceId: true,
  158. voiceParams: true,
  159. audioUrl: true,
  160. audioDuration: true,
  161. audioSize: true,
  162. status: true,
  163. errorMsg: true,
  164. createdAt: true,
  165. updatedAt: true,
  166. },
  167. }),
  168. models_1.prisma.audioRecord.count({ where }),
  169. ]);
  170. return { list: tasks, total, page, pageSize };
  171. }
  172. /**
  173. * 删除任务
  174. */
  175. async deleteTask(audioId) {
  176. const record = await models_1.prisma.audioRecord.findUnique({
  177. where: { audioId },
  178. });
  179. if (!record) {
  180. throw new Error('任务不存在');
  181. }
  182. if (record.status === 'processing') {
  183. throw new Error('任务正在处理中,无法删除');
  184. }
  185. await models_1.prisma.audioRecord.delete({
  186. where: { audioId },
  187. });
  188. return { deleted: true, audioId };
  189. }
  190. /**
  191. * 清空所有待处理任务
  192. */
  193. async clearPendingTasks() {
  194. const result = await models_1.prisma.audioRecord.deleteMany({
  195. where: { status: 'pending' },
  196. });
  197. return { deleted: result.count };
  198. }
  199. /**
  200. * 内部:轮询处理队列
  201. */
  202. _poll() {
  203. if (this.timer) {
  204. clearTimeout(this.timer);
  205. }
  206. this.timer = setTimeout(() => this._pollTick(), POLL_INTERVAL_MS);
  207. }
  208. async _pollTick() {
  209. if (!this.isRunning) return;
  210. try {
  211. while (this.isRunning && this.activeCount < MAX_CONCURRENCY) {
  212. const task = await this._claimNextTask();
  213. if (!task) break;
  214. this.activeCount++;
  215. this._processTask(task).finally(() => {
  216. this.activeCount--;
  217. });
  218. }
  219. } catch (err) {
  220. console.error('[QueueService] 轮询异常:', err.message);
  221. }
  222. if (this.isRunning) {
  223. this._poll();
  224. }
  225. }
  226. async _claimNextTask() {
  227. return models_1.prisma.$transaction(async (tx) => {
  228. const task = await tx.audioRecord.findFirst({
  229. where: { status: 'pending' },
  230. orderBy: { createdAt: 'asc' },
  231. });
  232. if (!task) return null;
  233. await tx.audioRecord.update({
  234. where: { id: task.id },
  235. data: { status: 'processing' },
  236. });
  237. return task;
  238. });
  239. }
  240. async _processTask(task) {
  241. const logPrefix = '[QueueService] 任务 #' + task.id;
  242. console.log(logPrefix + ' 开始处理: ' + task.audioId + ' 文本长度=' + task.wordCount);
  243. try {
  244. const voiceParams = task.voiceParams ? JSON.parse(task.voiceParams) : {};
  245. await TtsService.generateAudio(
  246. task.userId,
  247. task.text,
  248. task.voiceId,
  249. voiceParams,
  250. async (audioUrl, duration) => {
  251. try {
  252. await models_1.prisma.audioRecord.update({
  253. where: { id: task.id },
  254. data: {
  255. status: 'completed',
  256. audioUrl,
  257. audioDuration: duration || 0,
  258. },
  259. });
  260. console.log(logPrefix + ' 完成 → ' + audioUrl);
  261. } catch (err) {
  262. console.error(logPrefix + ' 更新完成状态失败:', err.message);
  263. }
  264. }
  265. );
  266. await new Promise((resolve) => {
  267. const checkComplete = async () => {
  268. const record = await models_1.prisma.audioRecord.findUnique({
  269. where: { id: task.id },
  270. });
  271. if (record && (record.status === 'completed' || record.status === 'failed')) {
  272. resolve();
  273. return;
  274. }
  275. setTimeout(checkComplete, 2000);
  276. };
  277. setTimeout(checkComplete, 5000);
  278. });
  279. } catch (err) {
  280. console.error(logPrefix + ' 处理异常:', err.message);
  281. await models_1.prisma.audioRecord.update({
  282. where: { id: task.id },
  283. data: {
  284. status: 'failed',
  285. errorMsg: err.message || '未知错误',
  286. },
  287. }).catch(() => {});
  288. }
  289. }
  290. }
  291. exports.queueService = new QueueService();