previewService.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import { spawn, ChildProcess } from "child_process";
  2. import { execSync } from "child_process";
  3. import path from "path";
  4. import fs from "fs";
  5. import http from "http";
  6. import { createAssetsSymlink, generateAdConfig } from "./configGenerator";
  7. const TEMPLATE_DIR = path.resolve(__dirname, "../../../../templates/coloring");
  8. const PREVIEW_PORT = 5199;
  9. let viteProcess: ChildProcess | null = null;
  10. let currentCreativeId: string | null = null;
  11. /**
  12. * 等待 HTTP 服务就绪
  13. */
  14. function waitForReady(url: string, maxRetries = 15): Promise<void> {
  15. return new Promise((resolve, reject) => {
  16. let tries = 0;
  17. function check() {
  18. http.get(url, (res) => {
  19. if (res.statusCode === 200) resolve();
  20. else retry();
  21. }).on("error", retry);
  22. }
  23. function retry() {
  24. if (++tries >= maxRetries) {
  25. reject(new Error(`Preview server did not start within ${maxRetries}s`));
  26. return;
  27. }
  28. setTimeout(check, 1000);
  29. }
  30. check();
  31. });
  32. }
  33. /**
  34. * 启动实时预览。等待 Vite dev server 就绪后才返回。
  35. */
  36. export async function startPreview(
  37. creativeId: string,
  38. theme: Record<string, string>,
  39. storageDir: string
  40. ): Promise<{ url: string }> {
  41. // 1. 停止旧的预览(如果有)
  42. stopPreview();
  43. // 2. 创建 symlink
  44. createAssetsSymlink(creativeId, storageDir);
  45. // 3. 生成配置
  46. const configContent = generateAdConfig({ creativeId, theme, storageDir });
  47. const configPath = path.join(TEMPLATE_DIR, "src", "filler", "_ad_config_.ts");
  48. fs.writeFileSync(configPath, configContent, "utf-8");
  49. // 4. 启动 Vite dev server(base 路径由 PREVIEW_BASE_PATH 环境变量传入 vite.config.js)
  50. console.log(`[preview] Starting Vite dev server on port ${PREVIEW_PORT}...`);
  51. viteProcess = spawn("npx", [
  52. "vite",
  53. "--port", String(PREVIEW_PORT),
  54. "--strictPort",
  55. ], {
  56. cwd: TEMPLATE_DIR,
  57. env: {
  58. ...process.env,
  59. AD_CONFIG_PATH: "src/filler/_ad_config_.ts",
  60. PREVIEW_BASE_PATH: process.env.PREVIEW_BASE_PATH || "/",
  61. },
  62. stdio: ["ignore", "pipe", "pipe"],
  63. });
  64. viteProcess.stdout?.on("data", (data: Buffer) => {
  65. console.log(`[preview:vite] ${data.toString().trim()}`);
  66. });
  67. viteProcess.stderr?.on("data", (data: Buffer) => {
  68. console.log(`[preview:vite] ${data.toString().trim()}`);
  69. });
  70. viteProcess.on("exit", (code) => {
  71. console.log(`[preview] Vite dev server exited (code ${code})`);
  72. viteProcess = null;
  73. currentCreativeId = null;
  74. });
  75. currentCreativeId = creativeId;
  76. // 5. 等待 Vite 就绪
  77. const localUrl = `http://localhost:${PREVIEW_PORT}`;
  78. console.log("[preview] Waiting for Vite to be ready...");
  79. await waitForReady(localUrl);
  80. console.log("[preview] Vite is ready.");
  81. // 生产环境通过 nginx 代理暴露公网 URL,本地开发直接用 localhost
  82. const publicUrl = process.env.PREVIEW_PUBLIC_URL || localUrl;
  83. return { url: publicUrl };
  84. }
  85. /**
  86. * 更新预览配置(主题变更时调用)。Vite HMR 会自动检测并刷新页面。
  87. */
  88. export function updatePreviewConfig(
  89. creativeId: string,
  90. theme: Record<string, string>,
  91. storageDir: string
  92. ): void {
  93. if (currentCreativeId !== creativeId) return;
  94. const configContent = generateAdConfig({ creativeId, theme, storageDir });
  95. const configPath = path.join(TEMPLATE_DIR, "src", "filler", "_ad_config_.ts");
  96. fs.writeFileSync(configPath, configContent, "utf-8");
  97. console.log(`[preview] Config updated for creative ${creativeId}`);
  98. }
  99. /**
  100. * 停止预览
  101. */
  102. export function stopPreview(): void {
  103. if (viteProcess) {
  104. console.log("[preview] Stopping Vite dev server...");
  105. try {
  106. viteProcess.kill("SIGTERM");
  107. } catch {
  108. // ignore
  109. }
  110. viteProcess = null;
  111. }
  112. currentCreativeId = null;
  113. // 确保端口释放
  114. try {
  115. execSync(`lsof -ti :${PREVIEW_PORT} | xargs kill -9 2>/dev/null`, { stdio: "ignore" });
  116. } catch {}
  117. }
  118. export function getPreviewStatus(): { active: boolean; creativeId: string | null; url: string | null } {
  119. return {
  120. active: viteProcess !== null && currentCreativeId !== null,
  121. creativeId: currentCreativeId,
  122. url: currentCreativeId ? `http://localhost:${PREVIEW_PORT}` : null,
  123. };
  124. }