Pārlūkot izejas kodu

feat: Express proxy middleware for Vite preview

Instead of fighting with Vite's base config (which doesn't work reliably
in dev mode for HTML injection AND module imports), use an Express proxy
middleware that:
- Forwards /preview/* requests from nginx/ads/ to Vite on localhost:5199
- Rewrites HTML responses: all root-absolute paths get /ads/preview/ prefix
- Non-HTML responses are piped through unchanged

Flow: browser → /ads/preview/ → nginx → Express → Vite → HTML rewrite → browser

Changes:
- New middleware/platform/server/src/middleware/previewProxy.ts
- index.ts: mount previewProxy at /preview
- previewService.ts: removed base-related code, Vite now serves at /
- vite.config.js: removed base (back to original)
- ecosystem.config.js: PREVIEW_PUBLIC_URL now /ads/preview/
guoziyun 3 nedēļas atpakaļ
vecāks
revīzija
4eb417e069

+ 1 - 1
ecosystem.config.js

@@ -7,7 +7,7 @@ module.exports = {
       env: {
         NODE_ENV: "production",
         PORT: 3001,
-        PREVIEW_PUBLIC_URL: "https://color2.jccytech.cn/ads-preview/",
+        PREVIEW_PUBLIC_URL: "https://color2.jccytech.cn/ads/preview/",
         PREVIEW_BASE_PATH: "/ads-preview/",
       },
       // 构建服务可能需要较多内存

+ 3 - 0
platform/server/dist/index.js

@@ -15,6 +15,7 @@ const builds_1 = require("./routes/builds");
 const preview_1 = require("./routes/preview");
 const preview_2 = require("./routes/preview");
 const errorHandler_1 = require("./middleware/errorHandler");
+const previewProxy_1 = require("./middleware/previewProxy");
 const PORT = process.env.PORT || 3001;
 const STORAGE_DIR = path_1.default.resolve(__dirname, "../../../storage");
 const CLIENT_DIST = path_1.default.resolve(__dirname, "../../client/dist");
@@ -32,6 +33,8 @@ async function main() {
     app.use("/api/v1", (0, assets_1.assetsRouter)(db, STORAGE_DIR));
     app.use("/api/v1", (0, builds_1.buildsRouter)(db, STORAGE_DIR, preview_2.onThemeSaved));
     app.use("/api/v1", (0, preview_1.previewRouter)(db, STORAGE_DIR));
+    // Vite 预览代理:将 /preview/* 请求转发到 Vite dev server 并重写 HTML 路径
+    app.use("/preview", previewProxy_1.previewProxy);
     // 生产环境:serve React 静态文件
     app.use(express_1.default.static(CLIENT_DIST));
     app.get("*", (_req, res) => {

+ 1 - 1
platform/server/dist/index.js.map

@@ -1 +1 @@
-{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,sDAA8B;AAC9B,gDAAwB;AACxB,gDAAwB;AACxB,4CAA6C;AAC7C,oCAA0C;AAC1C,kDAAqD;AACrD,kDAAqD;AACrD,4CAA+C;AAC/C,4CAA+C;AAC/C,8CAAiD;AACjD,8CAAgD;AAChD,4DAAyD;AAEzD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AACtC,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;AAChE,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;AAEjE,KAAK,UAAU,IAAI;IACjB,SAAS;IACT,MAAM,EAAE,GAAG,IAAA,uBAAY,EAAC,WAAW,CAAC,CAAC;IACrC,IAAA,oBAAa,EAAC,EAAE,CAAC,CAAC;IAElB,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IAEtB,MAAM;IACN,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,GAAE,CAAC,CAAC;IAChB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAExB,SAAS;IACT,GAAG,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAA,2BAAe,EAAC,EAAE,CAAC,CAAC,CAAC;IAClD,GAAG,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAA,2BAAe,EAAC,EAAE,EAAE,WAAW,EAAE,sBAAY,CAAC,CAAC,CAAC;IAC7E,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,IAAA,qBAAY,EAAC,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC;IAClD,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,IAAA,qBAAY,EAAC,EAAE,EAAE,WAAW,EAAE,sBAAY,CAAC,CAAC,CAAC;IAChE,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,IAAA,uBAAa,EAAC,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC;IAEnD,wBAAwB;IACxB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;IACrC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACzB,GAAG,CAAC,QAAQ,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,OAAO;IACP,GAAG,CAAC,GAAG,CAAC,2BAAY,CAAC,CAAC;IAEtB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QACpB,OAAO,CAAC,GAAG,CAAC,iDAAiD,IAAI,EAAE,CAAC,CAAC;QACrE,OAAO,CAAC,GAAG,CAAC,uBAAuB,WAAW,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
+{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,sDAA8B;AAC9B,gDAAwB;AACxB,gDAAwB;AACxB,4CAA6C;AAC7C,oCAA0C;AAC1C,kDAAqD;AACrD,kDAAqD;AACrD,4CAA+C;AAC/C,4CAA+C;AAC/C,8CAAiD;AACjD,8CAAgD;AAChD,4DAAyD;AACzD,4DAAyD;AAEzD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AACtC,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;AAChE,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;AAEjE,KAAK,UAAU,IAAI;IACjB,SAAS;IACT,MAAM,EAAE,GAAG,IAAA,uBAAY,EAAC,WAAW,CAAC,CAAC;IACrC,IAAA,oBAAa,EAAC,EAAE,CAAC,CAAC;IAElB,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IAEtB,MAAM;IACN,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,GAAE,CAAC,CAAC;IAChB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAExB,SAAS;IACT,GAAG,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAA,2BAAe,EAAC,EAAE,CAAC,CAAC,CAAC;IAClD,GAAG,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAA,2BAAe,EAAC,EAAE,EAAE,WAAW,EAAE,sBAAY,CAAC,CAAC,CAAC;IAC7E,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,IAAA,qBAAY,EAAC,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC;IAClD,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,IAAA,qBAAY,EAAC,EAAE,EAAE,WAAW,EAAE,sBAAY,CAAC,CAAC,CAAC;IAChE,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,IAAA,uBAAa,EAAC,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC;IAEnD,2DAA2D;IAC3D,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,2BAAY,CAAC,CAAC;IAElC,wBAAwB;IACxB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;IACrC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACzB,GAAG,CAAC,QAAQ,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,OAAO;IACP,GAAG,CAAC,GAAG,CAAC,2BAAY,CAAC,CAAC;IAEtB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QACpB,OAAO,CAAC,GAAG,CAAC,iDAAiD,IAAI,EAAE,CAAC,CAAC;QACrE,OAAO,CAAC,GAAG,CAAC,uBAAuB,WAAW,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}

+ 8 - 0
platform/server/dist/middleware/previewProxy.d.ts

@@ -0,0 +1,8 @@
+import { Request, Response } from "express";
+/**
+ * 将 /preview/* 请求代理到 Vite dev server。
+ * 对 HTML 响应自动重写路径,给所有根绝对路径加上 /ads/preview/ 前缀,
+ * 确保浏览器通过 nginx 正确加载资源。
+ */
+export declare function previewProxy(req: Request, res: Response): void;
+//# sourceMappingURL=previewProxy.d.ts.map

+ 1 - 0
platform/server/dist/middleware/previewProxy.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"previewProxy.d.ts","sourceRoot":"","sources":["../../src/middleware/previewProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAmB5C;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CA6E9D"}

+ 96 - 0
platform/server/dist/middleware/previewProxy.js

@@ -0,0 +1,96 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.previewProxy = previewProxy;
+const http_1 = __importDefault(require("http"));
+const VITE_HOST = "127.0.0.1";
+const VITE_PORT = 5199;
+// 这些 header 需要从代理响应中剔除(hop-by-hop)
+const HOP_BY_HOP = new Set([
+    "transfer-encoding",
+    "connection",
+    "keep-alive",
+    "proxy-connection",
+    "proxy-authenticate",
+    "proxy-authorization",
+    "te",
+    "trailers",
+    "upgrade",
+]);
+/**
+ * 将 /preview/* 请求代理到 Vite dev server。
+ * 对 HTML 响应自动重写路径,给所有根绝对路径加上 /ads/preview/ 前缀,
+ * 确保浏览器通过 nginx 正确加载资源。
+ */
+function previewProxy(req, res) {
+    // Express mount 会剥离 /preview 前缀,req.url 即 Vite 需要的路径
+    const targetPath = req.url || "/";
+    // 过滤请求 headers(去掉 hop-by-hop,设置正确的 host)
+    const reqHeaders = {};
+    for (const [key, value] of Object.entries(req.headers)) {
+        if (value === undefined)
+            continue;
+        const lk = key.toLowerCase();
+        if (lk === "host")
+            continue;
+        if (lk === "connection" || lk === "keep-alive" || lk === "upgrade")
+            continue;
+        reqHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
+    }
+    reqHeaders["host"] = `${VITE_HOST}:${VITE_PORT}`;
+    const proxyReq = http_1.default.request({
+        hostname: VITE_HOST,
+        port: VITE_PORT,
+        path: targetPath,
+        method: req.method,
+        headers: reqHeaders,
+    }, (proxyRes) => {
+        const contentType = (proxyRes.headers["content-type"] || "");
+        const isHtml = contentType.includes("text/html");
+        // 复制响应 headers(过滤 hop-by-hop)
+        const resHeaders = {};
+        for (const [key, value] of Object.entries(proxyRes.headers)) {
+            if (HOP_BY_HOP.has(key.toLowerCase()))
+                continue;
+            if (value !== undefined && value !== null) {
+                resHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
+            }
+        }
+        res.status(proxyRes.statusCode || 200);
+        if (isHtml) {
+            const chunks = [];
+            proxyRes.on("data", (chunk) => chunks.push(chunk));
+            proxyRes.on("end", () => {
+                let html = Buffer.concat(chunks).toString("utf-8");
+                // 将 src="/..." 和 href="/..." 重写为 /ads/preview/...
+                // 匹配 root-absolute 路径:以 / 开头,后跟非 / 字符(避免匹配 //)
+                html = html.replace(/(src|href)="\/([^/][^"]*)"/g, '$1="/ads/preview/$2"');
+                const body = Buffer.from(html, "utf-8");
+                resHeaders["content-type"] = "text/html; charset=utf-8";
+                resHeaders["content-length"] = String(body.length);
+                res.set(resHeaders);
+                res.send(body);
+            });
+        }
+        else {
+            res.set(resHeaders);
+            proxyRes.pipe(res);
+        }
+    });
+    proxyReq.on("error", (err) => {
+        console.error(`[preview:proxy] ${err.message}`);
+        if (!res.headersSent) {
+            res.status(502).json({ error: "Preview server not available" });
+        }
+    });
+    // POST/PUT/PATCH 请求需要转发 body
+    if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
+        req.pipe(proxyReq);
+    }
+    else {
+        proxyReq.end();
+    }
+}
+//# sourceMappingURL=previewProxy.js.map

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
platform/server/dist/middleware/previewProxy.js.map


+ 1 - 1
platform/server/dist/services/previewService.d.ts.map

@@ -1 +1 @@
-{"version":3,"file":"previewService.d.ts","sourceRoot":"","sources":["../../src/services/previewService.ts"],"names":[],"mappings":"AAoCA;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAyD1B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,UAAU,EAAE,MAAM,GACjB,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAgBlC;AAED,wBAAgB,gBAAgB,IAAI;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAMrG"}
+{"version":3,"file":"previewService.d.ts","sourceRoot":"","sources":["../../src/services/previewService.ts"],"names":[],"mappings":"AAoCA;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAkD1B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,UAAU,EAAE,MAAM,GACjB,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAgBlC;AAED,wBAAgB,gBAAgB,IAAI;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAMrG"}

+ 5 - 11
platform/server/dist/services/previewService.js

@@ -53,11 +53,8 @@ async function startPreview(creativeId, theme, storageDir) {
     const configContent = (0, configGenerator_1.generateAdConfig)({ creativeId, theme, storageDir });
     const configPath = path_1.default.join(TEMPLATE_DIR, "src", "filler", "_ad_config_.ts");
     fs_1.default.writeFileSync(configPath, configContent, "utf-8");
-    // 4. 启动 Vite dev server
-    //    base 路径通过 PREVIEW_BASE_PATH 环境变量传入 vite.config.js
-    //    注意:nginx proxy_pass 不能有尾部斜杠,以保留 /ads-preview/ 前缀
-    const previewBase = process.env.PREVIEW_BASE_PATH || "/";
-    console.log(`[preview] Starting Vite dev server on port ${PREVIEW_PORT} (base: ${previewBase})...`);
+    // 4. 启动 Vite dev server(无 base,Express 代理会负责路径重写)
+    console.log(`[preview] Starting Vite dev server on port ${PREVIEW_PORT}...`);
     viteProcess = (0, child_process_1.spawn)(path_1.default.join(TEMPLATE_DIR, "node_modules", ".bin", "vite"), [
         "--port", String(PREVIEW_PORT),
         "--strictPort",
@@ -81,12 +78,9 @@ async function startPreview(creativeId, theme, storageDir) {
         currentCreativeId = null;
     });
     currentCreativeId = creativeId;
-    // 5. 等待 Vite 就绪
-    //    Vite 有 base 路径时,根路径可能 404,需要用带 prefix 的 URL 检查
-    const checkUrl = previewBase !== "/"
-        ? `http://localhost:${PREVIEW_PORT}${previewBase}`
-        : `http://localhost:${PREVIEW_PORT}`;
-    console.log(`[preview] Waiting for Vite to be ready (checking ${checkUrl})...`);
+    // 5. 等待 Vite 就绪(Vite 无 base,直接检查 localhost)
+    const checkUrl = `http://localhost:${PREVIEW_PORT}`;
+    console.log(`[preview] Waiting for Vite to be ready...`);
     await waitForReady(checkUrl);
     console.log("[preview] Vite is ready.");
     // 生产环境通过 nginx 代理暴露公网 URL,本地开发直接用 localhost

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
platform/server/dist/services/previewService.js.map


+ 4 - 0
platform/server/src/index.ts

@@ -10,6 +10,7 @@ import { buildsRouter } from "./routes/builds";
 import { previewRouter } from "./routes/preview";
 import { onThemeSaved } from "./routes/preview";
 import { errorHandler } from "./middleware/errorHandler";
+import { previewProxy } from "./middleware/previewProxy";
 
 const PORT = process.env.PORT || 3001;
 const STORAGE_DIR = path.resolve(__dirname, "../../../storage");
@@ -33,6 +34,9 @@ async function main() {
   app.use("/api/v1", buildsRouter(db, STORAGE_DIR, onThemeSaved));
   app.use("/api/v1", previewRouter(db, STORAGE_DIR));
 
+  // Vite 预览代理:将 /preview/* 请求转发到 Vite dev server 并重写 HTML 路径
+  app.use("/preview", previewProxy);
+
   // 生产环境:serve React 静态文件
   app.use(express.static(CLIENT_DIST));
   app.get("*", (_req, res) => {

+ 102 - 0
platform/server/src/middleware/previewProxy.ts

@@ -0,0 +1,102 @@
+import { Request, Response } from "express";
+import http from "http";
+
+const VITE_HOST = "127.0.0.1";
+const VITE_PORT = 5199;
+
+// 这些 header 需要从代理响应中剔除(hop-by-hop)
+const HOP_BY_HOP = new Set([
+  "transfer-encoding",
+  "connection",
+  "keep-alive",
+  "proxy-connection",
+  "proxy-authenticate",
+  "proxy-authorization",
+  "te",
+  "trailers",
+  "upgrade",
+]);
+
+/**
+ * 将 /preview/* 请求代理到 Vite dev server。
+ * 对 HTML 响应自动重写路径,给所有根绝对路径加上 /ads/preview/ 前缀,
+ * 确保浏览器通过 nginx 正确加载资源。
+ */
+export function previewProxy(req: Request, res: Response): void {
+  // Express mount 会剥离 /preview 前缀,req.url 即 Vite 需要的路径
+  const targetPath = req.url || "/";
+
+  // 过滤请求 headers(去掉 hop-by-hop,设置正确的 host)
+  const reqHeaders: Record<string, string> = {};
+  for (const [key, value] of Object.entries(req.headers)) {
+    if (value === undefined) continue;
+    const lk = key.toLowerCase();
+    if (lk === "host") continue;
+    if (lk === "connection" || lk === "keep-alive" || lk === "upgrade") continue;
+    reqHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
+  }
+  reqHeaders["host"] = `${VITE_HOST}:${VITE_PORT}`;
+
+  const proxyReq = http.request(
+    {
+      hostname: VITE_HOST,
+      port: VITE_PORT,
+      path: targetPath,
+      method: req.method,
+      headers: reqHeaders,
+    },
+    (proxyRes) => {
+      const contentType = (proxyRes.headers["content-type"] || "") as string;
+      const isHtml = contentType.includes("text/html");
+
+      // 复制响应 headers(过滤 hop-by-hop)
+      const resHeaders: Record<string, string> = {};
+      for (const [key, value] of Object.entries(proxyRes.headers)) {
+        if (HOP_BY_HOP.has(key.toLowerCase())) continue;
+        if (value !== undefined && value !== null) {
+          resHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
+        }
+      }
+
+      res.status(proxyRes.statusCode || 200);
+
+      if (isHtml) {
+        const chunks: Buffer[] = [];
+        proxyRes.on("data", (chunk: Buffer) => chunks.push(chunk));
+        proxyRes.on("end", () => {
+          let html = Buffer.concat(chunks).toString("utf-8");
+
+          // 将 src="/..." 和 href="/..." 重写为 /ads/preview/...
+          // 匹配 root-absolute 路径:以 / 开头,后跟非 / 字符(避免匹配 //)
+          html = html.replace(
+            /(src|href)="\/([^/][^"]*)"/g,
+            '$1="/ads/preview/$2"',
+          );
+
+          const body = Buffer.from(html, "utf-8");
+          resHeaders["content-type"] = "text/html; charset=utf-8";
+          resHeaders["content-length"] = String(body.length);
+          res.set(resHeaders);
+          res.send(body);
+        });
+      } else {
+        res.set(resHeaders);
+        proxyRes.pipe(res);
+      }
+    },
+  );
+
+  proxyReq.on("error", (err) => {
+    console.error(`[preview:proxy] ${err.message}`);
+    if (!res.headersSent) {
+      res.status(502).json({ error: "Preview server not available" });
+    }
+  });
+
+  // POST/PUT/PATCH 请求需要转发 body
+  if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
+    req.pipe(proxyReq);
+  } else {
+    proxyReq.end();
+  }
+}

+ 5 - 12
platform/server/src/services/previewService.ts

@@ -53,12 +53,8 @@ export async function startPreview(
   const configPath = path.join(TEMPLATE_DIR, "src", "filler", "_ad_config_.ts");
   fs.writeFileSync(configPath, configContent, "utf-8");
 
-  // 4. 启动 Vite dev server
-  //    base 路径通过 PREVIEW_BASE_PATH 环境变量传入 vite.config.js
-  //    注意:nginx proxy_pass 不能有尾部斜杠,以保留 /ads-preview/ 前缀
-  const previewBase = process.env.PREVIEW_BASE_PATH || "/";
-
-  console.log(`[preview] Starting Vite dev server on port ${PREVIEW_PORT} (base: ${previewBase})...`);
+  // 4. 启动 Vite dev server(无 base,Express 代理会负责路径重写)
+  console.log(`[preview] Starting Vite dev server on port ${PREVIEW_PORT}...`);
   viteProcess = spawn(path.join(TEMPLATE_DIR, "node_modules", ".bin", "vite"), [
     "--port", String(PREVIEW_PORT),
     "--strictPort",
@@ -85,12 +81,9 @@ export async function startPreview(
 
   currentCreativeId = creativeId;
 
-  // 5. 等待 Vite 就绪
-  //    Vite 有 base 路径时,根路径可能 404,需要用带 prefix 的 URL 检查
-  const checkUrl = previewBase !== "/"
-    ? `http://localhost:${PREVIEW_PORT}${previewBase}`
-    : `http://localhost:${PREVIEW_PORT}`;
-  console.log(`[preview] Waiting for Vite to be ready (checking ${checkUrl})...`);
+  // 5. 等待 Vite 就绪(Vite 无 base,直接检查 localhost)
+  const checkUrl = `http://localhost:${PREVIEW_PORT}`;
+  console.log(`[preview] Waiting for Vite to be ready...`);
   await waitForReady(checkUrl);
   console.log("[preview] Vite is ready.");
 

+ 0 - 6
templates/coloring/vite.config.js

@@ -42,13 +42,7 @@ module.exports = defineConfig(({ mode }) => {
   const output = platformBuild?.output;
   const outDir = output ? `dist/${output}` : "dist";
 
-  // 预览模式下通过 PREVIEW_BASE_PATH 设置 base,如 /ads-preview/
-  // 这会控制 Vite 注入的 @vite/client 路径 + 所有 JS import 模块路径的前缀
-  const base = process.env.PREVIEW_BASE_PATH || undefined;
-  if (base) console.log(`[vite:config] base = ${base}`);
-
   return {
-    base,
     plugins: [viteSingleFile(), finalizeHtmlPlugin(outDir, adapter)],
     server: {
       allowedHosts: ["color2.jccytech.cn", "localhost", ".jccytech.cn"],

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels