buildService.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.BuildService = void 0;
  7. const child_process_1 = require("child_process");
  8. const path_1 = __importDefault(require("path"));
  9. const fs_1 = __importDefault(require("fs"));
  10. const archiver_1 = __importDefault(require("archiver"));
  11. const configGenerator_1 = require("./configGenerator");
  12. const storageService_1 = require("./storageService");
  13. const TEMPLATE_DIR = path_1.default.resolve(__dirname, "../../../../templates/coloring");
  14. const BUILD_TIMEOUT_MS = 120_000; // 单次构建超时 120s
  15. class BuildService {
  16. db;
  17. storageDir;
  18. queue = [];
  19. running = false;
  20. constructor(db, storageDir) {
  21. this.db = db;
  22. this.storageDir = storageDir;
  23. }
  24. enqueue(buildId, creativeId, platforms, theme) {
  25. this.queue.push(() => this.build(buildId, creativeId, platforms, theme));
  26. if (!this.running) {
  27. this.processQueue();
  28. }
  29. }
  30. async processQueue() {
  31. this.running = true;
  32. while (this.queue.length > 0) {
  33. const task = this.queue.shift();
  34. try {
  35. await task();
  36. }
  37. catch (err) {
  38. console.error("[build] Queue task failed:", err);
  39. }
  40. }
  41. this.running = false;
  42. }
  43. async build(buildId, creativeId, platforms, theme) {
  44. const startTime = new Date().toISOString();
  45. try {
  46. // 更新状态 → building
  47. this.db
  48. .prepare("UPDATE builds SET status = 'building', started_at = ? WHERE id = ?")
  49. .run(startTime, buildId);
  50. // 1. 创建 symlink
  51. (0, configGenerator_1.createAssetsSymlink)(creativeId, this.storageDir);
  52. // 2. 生成 _ad_config_.ts
  53. const configContent = (0, configGenerator_1.generateAdConfig)({
  54. creativeId,
  55. theme,
  56. storageDir: this.storageDir,
  57. });
  58. const configPath = path_1.default.join(TEMPLATE_DIR, "src", "filler", "_ad_config_.ts");
  59. fs_1.default.writeFileSync(configPath, configContent, "utf-8");
  60. console.log(`[build] Generated _ad_config_.ts for creative ${creativeId}`);
  61. // 3. 构建输出目录
  62. const buildOutputDir = path_1.default.join(this.storageDir, "creatives", creativeId, "builds", buildId);
  63. (0, storageService_1.ensureDir)(buildOutputDir);
  64. // 4. 逐平台构建
  65. const results = [];
  66. for (const platform of platforms) {
  67. console.log(`[build] Building ${platform} for creative ${creativeId}...`);
  68. await this.runViteBuild(platform);
  69. await this.collectOutput(buildOutputDir, platform, results);
  70. }
  71. // 5. 复制预览产物(用第一个平台的输出即可,供真机扫码测试)
  72. const previewSrc = path_1.default.join(TEMPLATE_DIR, "dist", platforms[0], "index.html");
  73. const previewDir = path_1.default.join(this.storageDir, "previews");
  74. (0, storageService_1.ensureDir)(previewDir);
  75. const previewId = buildId.replace(/-/g, "").slice(0, 8);
  76. const previewPath = path_1.default.join(previewDir, `${previewId}.html`);
  77. fs_1.default.copyFileSync(previewSrc, previewPath);
  78. console.log(`[build] Preview file: ${previewPath} (from ${platforms[0]})`);
  79. // 7. 打包 ZIP
  80. await this.createZip(buildOutputDir, results);
  81. // 6. 更新数据库
  82. const finishedAt = new Date().toISOString();
  83. this.db
  84. .prepare(`UPDATE builds SET status = 'completed', results = ?, finished_at = ? WHERE id = ?`)
  85. .run(JSON.stringify(results), finishedAt, buildId);
  86. // 更新创意状态
  87. this.db
  88. .prepare("UPDATE creatives SET status = 'built', updated_at = datetime('now') WHERE id = ?")
  89. .run(creativeId);
  90. console.log(`[build] Build ${buildId} completed: ${results.map((r) => r.platform).join(", ")}`);
  91. }
  92. catch (err) {
  93. console.error(`[build] Build ${buildId} failed:`, err.message);
  94. this.db
  95. .prepare("UPDATE builds SET status = 'failed', error_log = ? WHERE id = ?")
  96. .run(err.message || "Unknown error", buildId);
  97. this.db
  98. .prepare("UPDATE creatives SET status = 'assets_ready', updated_at = datetime('now') WHERE id = ?")
  99. .run(creativeId);
  100. }
  101. finally {
  102. // 清理临时文件
  103. (0, configGenerator_1.cleanupBuildArtifacts)();
  104. }
  105. }
  106. runViteBuild(platform) {
  107. return new Promise((resolve, reject) => {
  108. const cmd = `cd ${TEMPLATE_DIR} && AD_CONFIG_PATH=src/filler/_ad_config_.ts npx vite build --mode ${platform}`;
  109. console.log(`[build] Executing: ${cmd}`);
  110. (0, child_process_1.exec)(cmd, { timeout: BUILD_TIMEOUT_MS }, (error, stdout, stderr) => {
  111. if (stdout)
  112. console.log(`[vite:${platform}]`, stdout.slice(-500));
  113. if (stderr && !stderr.includes("vite"))
  114. console.error(`[vite:${platform}]`, stderr.slice(-500));
  115. if (error) {
  116. reject(new Error(`Vite build failed for ${platform}: ${error.message}`));
  117. }
  118. else {
  119. resolve();
  120. }
  121. });
  122. });
  123. }
  124. async collectOutput(buildOutputDir, platform, results) {
  125. const distPath = path_1.default.join(TEMPLATE_DIR, "dist", platform, "index.html");
  126. const destDir = path_1.default.join(buildOutputDir, platform);
  127. (0, storageService_1.ensureDir)(destDir);
  128. const destPath = path_1.default.join(destDir, "index.html");
  129. if (!fs_1.default.existsSync(distPath)) {
  130. throw new Error(`Build output not found for platform ${platform}: ${distPath}`);
  131. }
  132. fs_1.default.copyFileSync(distPath, destPath);
  133. const stat = fs_1.default.statSync(destPath);
  134. results.push({ platform, fileSize: stat.size });
  135. }
  136. createZip(buildOutputDir, results) {
  137. return new Promise((resolve, reject) => {
  138. const zipPath = path_1.default.join(buildOutputDir, "all.zip");
  139. const output = fs_1.default.createWriteStream(zipPath);
  140. const archive = (0, archiver_1.default)("zip", { zlib: { level: 9 } });
  141. output.on("close", resolve);
  142. archive.on("error", reject);
  143. archive.pipe(output);
  144. for (const r of results) {
  145. const filePath = path_1.default.join(buildOutputDir, r.platform, "index.html");
  146. if (fs_1.default.existsSync(filePath)) {
  147. archive.file(filePath, { name: `${r.platform}/index.html` });
  148. }
  149. }
  150. archive.finalize();
  151. });
  152. }
  153. }
  154. exports.BuildService = BuildService;
  155. //# sourceMappingURL=buildService.js.map