Browse Source

feat(平台适配): Google 平台 ExitApi.exit() CTA 支持,产物统一命名为 index.html

- Google adapter openStore 改用 ExitApi.exit(),构建时注入 exitapi.js
- 所有平台产物统一输出为 dist/<platform>/index.html,移除自定义命名逻辑
- 文档补充 Google/Unity 平台验证入口和步骤
guoziyun 3 weeks ago
parent
commit
42ce781c02
4 changed files with 64 additions and 47 deletions
  1. 36 23
      README.md
  2. 5 5
      agent.md
  3. 6 1
      src/filler/ad-platform/adapters/google.ts
  4. 17 18
      vite.config.js

+ 36 - 23
README.md

@@ -96,13 +96,13 @@ https://gemini.google.com/share/8340c20dd2d1
 
 ## 技术栈
 
-| 层级         | 技术                                                |
-| ------------ | --------------------------------------------------- |
-| 渲染引擎     | WebGL 2(自研,无外部 GL 框架)                     |
-| 构建工具     | Vite 5 + TypeScript 5                               |
-| 打包插件     | vite-plugin-singlefile(输出单文件 HTML)           |
+| 层级         | 技术                                                                 |
+| ------------ | -------------------------------------------------------------------- |
+| 渲染引擎     | WebGL 2(自研,无外部 GL 框架)                                      |
+| 构建工具     | Vite 5 + TypeScript 5                                                |
+| 打包插件     | vite-plugin-singlefile(输出单文件 HTML)                            |
 | 广告平台适配 | AdPlatformAdapter(Applovin / Unity / Playturbo-Mintegral / Google) |
-| 单文件产物   | 默认与各平台 profile 均输出自包含 HTML              |
+| 单文件产物   | 默认与各平台 profile 均输出自包含 HTML                               |
 
 ## 项目结构
 
@@ -121,11 +121,11 @@ assets/
   css/            # 样式
 dist/
   index.html                 # 默认产物
-  applovin/applovin.html     # Applovin 产物
-  unity/unity.html           # Unity 产物
-  playturbo/playturbo.html   # Playturbo 产物
-  mintegral/mintegral.html   # Mintegral 产物
-  google/google.html         # Google 产物
+  applovin/index.html        # Applovin 产物
+  unity/index.html           # Unity 产物
+  playturbo/index.html       # Playturbo 产物
+  mintegral/index.html       # Mintegral 产物
+  google/index.html          # Google 产物
 ```
 
 ## 开发调试
@@ -160,11 +160,11 @@ http://42.193.231.145:5173
 
 ```bash
 npm run build           # dist/index.html
-npm run build:applovin  # dist/applovin/applovin.html
-npm run build:unity     # dist/unity/unity.html
-npm run build:playturbo # dist/playturbo/playturbo.html
-npm run build:mintegral # dist/mintegral/mintegral.html
-npm run build:google    # dist/google/google.html
+npm run build:applovin  # dist/applovin/index.html
+npm run build:unity     # dist/unity/index.html
+npm run build:playturbo # dist/playturbo/index.html
+npm run build:mintegral # dist/mintegral/index.html
+npm run build:google    # dist/google/index.html
 npm run build:all       # 依次输出以上全部产物
 ```
 
@@ -196,7 +196,7 @@ npm run build:all       # 依次输出以上全部产物
 
 **预览工具**:https://p.applov.in/playablePreview?create=1
 
-1. 执行 `npm run build:applovin`,打开预览工具,上传 `dist/applovin/applovin.html`
+1. 执行 `npm run build:applovin`,打开预览工具,上传 `dist/applovin/index.html`
 2. 检查项:
    - [ ] 广告正常加载,WebGL 填色可交互
    - [ ] CTA 按钮可点击(Applovin 通过 `ExitApi.exit()` 关闭广告,落地页由平台后台配置)
@@ -207,27 +207,40 @@ npm run build:all       # 依次输出以上全部产物
 
 **规范文档**:https://docs.unity.com/zh-cn/grow/acquire/creatives/playable/specifications
 
-1. 执行 `npm run build:unity`,在 Unity Creative 后台上传 `dist/unity/unity.html`
-2. 检查项:
+**验证方式**:Unity 没有 Web 端预览工具,需通过 **Ad Testing App**(iOS / Android,最新版 4.0.0)扫码验证。
+
+1. 执行 `npm run build:unity`,将 `dist/unity/index.html` 托管到可访问的 HTTPS URL
+2. 在 Unity Ads Dashboard 上传素材,或在 Ad Testing App 中直接扫码加载
+3. 检查项:
    - [ ] `dapi.gameReady()` 被正确调用(可在控制台确认)
    - [ ] CTA 跳转正常(Unity 使用 MRAID `mraid.open(url)` 跳转)
    - [ ] 广告尺寸符合规范(单文件 HTML,无外部请求)
+   - [ ] 文件大小 ≤ 5 MB
 
 ### Google AdMob / Google Ads
 
 **规范文档**:https://support.google.com/google-ads/answer/9981650?hl=en#_HTML
 
-1. 执行 `npm run build:google`,使用 Google Web Designer 或 HTML5 验证工具检验 `dist/google/google.html`
-2. 检查项:
+**验证工具**:https://h5validator.appspot.com/adwords/asset
+
+Google 的官方验证器要求上传 `.zip` 而非裸 HTML。需要先将产物打包为 zip,上传时勾选 **"Select for App Campaigns"**,然后点击眼睛图标预览。
+
+1. 执行 `npm run build:google`,将 `dist/google/index.html` 打包为 `google-ad.zip`
+2. 打开 h5validator,上传 zip,勾选 "Select for App Campaigns"
+3. 点击眼睛图标预览,测试交互和 CTA 点击
+4. 检查项:
    - [ ] 无外部网络请求(所有资源均已内联)
-   - [ ] 文件大小满足限制(≤5 MB)
-   - [ ] CTA 可正常触发 `window.open(url)`
+   - [ ] 文件大小满足限制(≤5 MB,zip 内 ≤512 个文件
+   - [ ] CTA 可正常触发跳转(Google 平台使用 `ExitApi.exit()`)
    - [ ] 不依赖 `document.write` 或被禁用 API
+   - [ ] 包含方向 meta 标签(`<meta name="ad.orientation" content="portrait">`)
 
 ### Mintergal / Playturbo
 
 **规范文档**:https://www.playturbo.com/review/doc
 
+**预览工具**:https://www.playturbo.com/review
+
 1. 执行 `npm run build:playturbo` 或 `npm run build:mintegral`,按平台文档要求上传对应产物
 2. 检查项:
    - [ ] 资源加载完成后调用 `window.gameReady()`

+ 5 - 5
agent.md

@@ -295,11 +295,11 @@ CSS 回调 (cssAdjustProgress / cssColorDone / cssOnFinish)
 
 ```bash
 npm run build           # dist/index.html
-npm run build:applovin  # dist/applovin/applovin.html
-npm run build:unity     # dist/unity/unity.html
-npm run build:playturbo # dist/playturbo/playturbo.html
-npm run build:mintegral # dist/mintegral/mintegral.html
-npm run build:google    # dist/google/google.html
+npm run build:applovin  # dist/applovin/index.html
+npm run build:unity     # dist/unity/index.html
+npm run build:playturbo # dist/playturbo/index.html
+npm run build:mintegral # dist/mintegral/index.html
+npm run build:google    # dist/google/index.html
 npm run build:all       # 依次输出默认与各平台产物
 ```
 

+ 6 - 1
src/filler/ad-platform/adapters/google.ts

@@ -23,7 +23,12 @@ export function createGoogleAdapter(): AdPlatformAdapter {
     },
 
     openStore() {
-      window.open(getStoreUrl(), "_blank");
+      if (typeof window.ExitApi !== "undefined" && window.ExitApi.exit) {
+        window.ExitApi.exit();
+      } else {
+        // 非 Google 广告环境下的 fallback(如本地调试)
+        window.open(getStoreUrl(), "_blank");
+      }
     },
 
     shouldUseCustomLoading() {

+ 17 - 18
vite.config.js

@@ -11,9 +11,9 @@ const platformBuilds = {
   google: { adapter: "google", output: "google" },
 };
 
-function patchSingleFileHtml(htmlPath) {
+function patchSingleFileHtml(htmlPath, adapter) {
   if (!fs.existsSync(htmlPath)) return;
-  const html = fs
+  let html = fs
     .readFileSync(htmlPath, "utf8")
     .replace(/<script\s+type="module"\s+crossorigin>/g, "<script>")
     .replace(/<script\s+crossorigin\s+type="module">/g, "<script>")
@@ -22,19 +22,24 @@ function patchSingleFileHtml(htmlPath) {
     .replace(/<style\s+rel="stylesheet"\s+crossorigin>/g, "<style>")
     .replace(/<style\s+crossorigin\s+rel="stylesheet">/g, "<style>")
     .replace(/<style\s+crossorigin>/g, "<style>");
+
+  // Google 平台需要引入 exitapi.js,CTA 才能正常跳转
+  if (adapter === "google") {
+    html = html.replace(
+      "</head>",
+      '<script src="https://tpc.googlesyndication.com/pagead/js/exitapi.js"></script></head>',
+    );
+  }
+
   fs.writeFileSync(htmlPath, html);
 }
 
-function finalizeHtmlPlugin(outDir, fileName) {
+function finalizeHtmlPlugin(outDir, adapter) {
   return {
     name: "finalize-html-output",
     closeBundle() {
-      const indexPath = path.resolve(__dirname, outDir, "index.html");
-      const targetPath = path.resolve(__dirname, outDir, fileName);
-      if (fileName !== "index.html" && fs.existsSync(indexPath)) {
-        fs.renameSync(indexPath, targetPath);
-      }
-      patchSingleFileHtml(targetPath);
+      const htmlPath = path.resolve(__dirname, outDir, "index.html");
+      patchSingleFileHtml(htmlPath, adapter);
     },
   };
 }
@@ -44,10 +49,9 @@ module.exports = defineConfig(({ mode }) => {
   const adapter = platformBuild?.adapter || "google";
   const output = platformBuild?.output;
   const outDir = output ? `dist/${output}` : "dist";
-  const htmlFileName = output ? `${output}.html` : "index.html";
 
   return {
-    plugins: [viteSingleFile(), finalizeHtmlPlugin(outDir, htmlFileName)],
+    plugins: [viteSingleFile(), finalizeHtmlPlugin(outDir, adapter)],
     resolve: {
       alias: {
         "./ad-platform/current": path.resolve(
@@ -64,17 +68,12 @@ module.exports = defineConfig(({ mode }) => {
       emptyOutDir: true,
       rollupOptions: {
         input: {
-          [path.basename(htmlFileName, ".html")]: "./index.html",
+          main: "./index.html",
         },
         output: {
           entryFileNames: "assets/[name].js",
           chunkFileNames: "assets/[name].js",
-          assetFileNames: (assetInfo) => {
-            if (assetInfo.name && assetInfo.name.endsWith(".html")) {
-              return htmlFileName;
-            }
-            return "assets/[name][extname]";
-          },
+          assetFileNames: "assets/[name][extname]",
         },
       },
     },