previewService.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  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,由 nginx 独立端口代理,所有路径走 /)
  50. console.log(`[preview] Starting Vite dev server on port ${PREVIEW_PORT}...`);
  51. viteProcess = spawn(path.join(TEMPLATE_DIR, "node_modules", ".bin", "vite"), [
  52. "--port", String(PREVIEW_PORT),
  53. "--strictPort",
  54. ], {
  55. cwd: TEMPLATE_DIR,
  56. env: {
  57. ...process.env,
  58. AD_CONFIG_PATH: "src/filler/_ad_config_.ts",
  59. },
  60. stdio: ["ignore", "pipe", "pipe"],
  61. });
  62. viteProcess.stdout?.on("data", (data: Buffer) => {
  63. console.log(`[preview:vite] ${data.toString().trim()}`);
  64. });
  65. viteProcess.stderr?.on("data", (data: Buffer) => {
  66. console.log(`[preview:vite] ${data.toString().trim()}`);
  67. });
  68. viteProcess.on("exit", (code) => {
  69. console.log(`[preview] Vite dev server exited (code ${code})`);
  70. viteProcess = null;
  71. currentCreativeId = null;
  72. });
  73. currentCreativeId = creativeId;
  74. // 5. 等待 Vite 就绪(Vite 无 base,直接检查 localhost)
  75. const checkUrl = `http://localhost:${PREVIEW_PORT}`;
  76. console.log(`[preview] Waiting for Vite to be ready...`);
  77. await waitForReady(checkUrl);
  78. console.log("[preview] Vite is ready.");
  79. // 生产环境通过 nginx 代理暴露公网 URL,本地开发直接用 localhost
  80. const localUrl = `http://localhost:${PREVIEW_PORT}`;
  81. const publicUrl = process.env.PREVIEW_PUBLIC_URL || localUrl;
  82. return { url: publicUrl };
  83. }
  84. /**
  85. * 更新预览配置(主题变更时调用)。Vite HMR 会自动检测并刷新页面。
  86. */
  87. export function updatePreviewConfig(
  88. creativeId: string,
  89. theme: Record<string, string>,
  90. storageDir: string
  91. ): void {
  92. if (currentCreativeId !== creativeId) return;
  93. const configContent = generateAdConfig({ creativeId, theme, storageDir });
  94. const configPath = path.join(TEMPLATE_DIR, "src", "filler", "_ad_config_.ts");
  95. fs.writeFileSync(configPath, configContent, "utf-8");
  96. console.log(`[preview] Config updated for creative ${creativeId}`);
  97. }
  98. /**
  99. * 停止预览
  100. */
  101. export function stopPreview(): void {
  102. if (viteProcess) {
  103. console.log("[preview] Stopping Vite dev server...");
  104. try {
  105. viteProcess.kill("SIGTERM");
  106. } catch {
  107. // ignore
  108. }
  109. viteProcess = null;
  110. }
  111. currentCreativeId = null;
  112. // 确保端口释放
  113. try {
  114. execSync(`lsof -ti :${PREVIEW_PORT} | xargs kill -9 2>/dev/null`, { stdio: "ignore" });
  115. } catch {}
  116. }
  117. export function getPreviewStatus(): { active: boolean; creativeId: string | null; url: string | null } {
  118. return {
  119. active: viteProcess !== null && currentCreativeId !== null,
  120. creativeId: currentCreativeId,
  121. url: currentCreativeId ? `http://localhost:${PREVIEW_PORT}` : null,
  122. };
  123. }