> For the complete documentation index, see [llms.txt](https://developers-apps-in-toss.toss.im/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers-apps-in-toss.toss.im/documentation/api-and-sdk-zh/unity/build/build-process.md).

# 构建流水线

说明 Unity 项目如何变成可部署的 `.ait` 包之前，说明 SDK 在内部做了什么。

> **对象**: SDK 贡献者。如果你的目标是用 SDK 构建游戏， [构建配置文件](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles)和 [构建自定义](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-customization)这是所需文档。

### 两阶段流水线结构

构建分为 Unity 生成 WebGL 产物的阶段，以及将该产物重新部署为网页项目并用 granite 打包的阶段。

```
┌─────────────────────────────────────────────────────────────────────┐
│                        入口点                                 │
│  菜单: Build & Package  │  Build 窗口  │  服务器启动/重启    │
│  AppsInTossMenu.cs      │  AppsInTossBuildWindow.cs                 │
└─────────────┬───────────────────────────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────────────────────────┐
│  AITConvertCore.DoExport(buildWebGL, doPackaging, cleanBuild,       │
│                          profile, profileName)                      │
│  或 DoExportAsync(...)                                            │
└─────────────┬───────────────────────────────────────────────────────┘
              │
     ┌────────┴────────────────────────────────┐
     ▼                                         ▼
┌──────────────────────┐          ┌────────────────────────────────┐
│  第 1 阶段: WebGL 构建 │          │  第 2 阶段: 打包               │
│  BuildWebGL()        │          │  GenerateMiniAppPackage()      │
│                      │          │  → AITPackageBuilder           │
│  - Init()            │          │    .PackageWebGLBuild()        │
│  - BuildPipeline     │          │                                │
│  - .ait-build-info   │          │  2a. 复制 BuildConfig          │
│                      │          │  2b. WebGL→public 复制         │
│  产物: webgl/      │          │  2c. 占位符替换         │
│                      │          │  2d. 插入加载画面            │
│                      │          │  2e. pnpm install              │
│                      │          │  2f. granite build             │
│                      │          │                                │
│                      │          │  产物: ait-build/dist/       │
└──────────────────────┘          └────────────────────────────────┘
```

#### 调用矩阵

| 入口点                       | buildWebGL | doPackaging | cleanBuild |
| ------------------------- | ---------- | ----------- | ---------- |
| `Build & Package`         | `true`     | `true`      | `false`    |
| `Build & Package (clean)` | `true`     | `true`      | `true`     |
| `Deploy (Test)`           | `true`     | `true`      | `false`    |
| `Deploy (Production)`     | `true`     | `true`      | `true`     |
| `Dev Server 启动`           | `true`     | `true`      | `false`    |
| `重启服务器`                   | `true`     | `true`      | `false`    |
| `重启（仅服务器）`                | —          | —           | —          |

> **参考**: `重启（仅服务器）`是 `DoExport`而只是重启 granite 进程。

### 第 0 阶段初始化

#### 模板同步

`AITTemplateManager.EnsureWebGLTemplatesExist`会在构建前将 SDK 的 WebGL 模板复制到项目中。

SDK 模板搜索顺序：

1. `Packages/im.toss.apps-in-toss-unity-sdk/WebGLTemplates/`
2. `Packages/com.appsintoss.miniapp/WebGLTemplates/`
3. 基于 Assembly 路径（`typeof(AITConvertCore).Assembly.Location` 上层）

项目中 `Assets/WebGLTemplates/AITTemplate/`如果没有，则复制全部；如果有，则基于标记进行更新以保留用户自定义区域。如下 **模板合并时机** 部分。

#### 构建设置

`AITBuildInitializer.Init`会自动配置 Unity PlayerSettings。

| 设置                    | 值                     | 备注                                                   |
| --------------------- | --------------------- | ---------------------------------------------------- |
| WebGL Template        | `PROJECT:AITTemplate` | 硬编码                                                  |
| 链接器目标                 | `Wasm`                | 硬编码                                                  |
| 脚本后端                  | `IL2CPP`              | 硬编码                                                  |
| 内存大小                  | 256\~1536MB           | 按 Unity 版本的默认值（可由用户覆盖）                               |
| Compression           | `Brotli`              | 默认值。 `decompressionFallback`已开启，因此可在所有 Unity 版本中使用   |
| Threading             | `false`               | 默认值（移动浏览器兼容性）                                        |
| Data Caching          | `false`               | 默认值                                                  |
| `nameFilesAsHashes`   | 用户设置（默认 `true`)       | 仅在 Unity 2021.x 中强制 `false` — `true`否则会引发 Bee 构建循环错误 |
| Engine Code Stripping | 用户设置                  | —                                                    |
| Managed Stripping     | `高`                   | 默认值                                                  |
| IL2CPP Config         | 用户设置                  | —                                                    |

默认值的唯一来源是 `AITEditorScriptObject`的 `GetDefault*` 静态方法。仅 Dev Server 配置会将压缩 `禁用`降到 — 各配置文件之间的差异是 [构建配置文件](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles)中有整理。

按版本的默认内存：

* Unity 2021.3: 256MB
* Unity 2022.3: 512MB
* Unity 6 (2023.3+): 1024MB
* Unity 2024.2+: 1536MB

应用于配置文件的环境变量覆盖由 `AITBuildInitializer.ApplyEnvironmentVariableOverrides`处理。变量列表和数值是 [构建配置文件](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles)为准。

#### Config 验证

`DoExport`在启动时 `UnityUtil.GetEditorConf()`读取设置资源，若连资源本身也找不到，则 `INVALID_APP_CONFIG`。

App ID 为空或图标 URL 为空不会阻止构建。App ID 只是 Configuration 窗口中构建按钮失效的条件（`AITEditorScriptObject.IsAppNameValid`）；图标 URL 仅在输入时检查格式。也就是说，直接用空值构建时， `%AIT_ICON_URL%` 等会被替换为空字符串，生成相应包。

### 第 1 阶段 WebGL 构建

`AITConvertCore.BuildWebGL()`

#### 执行流程

```
1. AITBuildInitializer.Init(profile)
   ├── PlayerSettings 自动配置
   ├── 应用环境变量覆盖
   └── 输出构建配置日志

2. 如果是 cleanBuild：
   └── 删除 webgl/ 目录

3. BuildPipeline.BuildPlayer()
   ├── scenes: EditorBuildSettings.scenes（仅勾选的）
   ├── locationPathName: "{projectPath}/webgl"
   ├── target: WebGL
   └── options: BuildOptions.None（若 cleanBuild 则追加 BuildOptions.CleanBuildCache）

4. BuildReport 检查
   ├── 成功 → 写入 .ait-build-info.json
   └── 失败 → AITErrorReporter.SetBuildReport(report) + 返回错误

5. 写入构建标记：webgl/.ait-build-info.json
```

#### 构建标记

在成功的 WebGL 构建后 `webgl/.ait-build-info.json`会将元数据写入。模式为 `AITConvertCore.cs`的 `AITBuildInfo` 类。

```json
{
    "sdkVersion": "1.7.0",
    "buildTime": "2024-03-01T12:00:00.0000000Z",
    "compressionFormat": 2,
    "decompressionFallback": true,
    "profileName": "Production",
    "unityVersion": "6000.2.15f1"
}
```

| 字段                      | 说明                                                                           |
| ----------------------- | ---------------------------------------------------------------------------- |
| `sdkVersion`            | SDK 包版本                                                                      |
| `buildTime`             | UTC ISO 8601 构建时间                                                            |
| `compressionFormat`     | `PlayerSettings.WebGL.compressionFormat` int 值（0=Disabled, 1=Gzip, 2=Brotli） |
| `decompressionFallback` | `PlayerSettings.WebGL.decompressionFallback`。开启后产物扩展名将 `.unityweb`变为         |
| `profileName`           | "Development" 或 "Production"                                                 |
| `unityVersion`          | `Application.unityVersion`                                                   |

`ReadBuildMarker(webglPath)`读取标记并 `AITBuildInfo`返回；如果文件不存在或解析失败，则 `null`。

标记的用途：

* **构建缓存验证** (`ShouldForceCleanBuild`）—— 若 Unity 版本不一致或没有标记，则自动 clean build
* **压缩格式检测** (`CopyWebGLToPublic`）— `compressionFormat`和 `decompressionFallback`以精确决定扩展名

#### 构建缓存有效性验证

`ShouldForceCleanBuild(outputPath, cleanBuild)`会在 WebGL 构建前验证现有缓存。

```
1. cleanBuild=true → 强制 clean build
2. webgl/ 文件夹不存在 → 新建构建（无需 clean）
3. 无构建标记 → clean build（旧 SDK 版本或损坏）
4. Unity 版本不一致 → clean build（保证构建结果兼容）
5. Build/*.loader.js 不存在 → clean build（缺少必需文件）
6. 全部通过 → 增量构建
```

Unity 的 `BuildPipeline`默认会执行增量构建，因此， `webgl/`若保留着，它只会重新生成变更过的资源。 `cleanBuild=true`如果是 `webgl/`会删除 `BuildOptions.CleanBuildCache`并进行完整构建。

### 第 2 阶段 打包

`AITPackageBuilder.PackageWebGLBuild()` （同步）或 `PackageWebGLBuildAsync()` （异步）。两条路径共享 `PreparePackaging()`的通用准备逻辑。

```
PreparePackaging() ← 同步/异步通用
├── 等待 Node.js/pnpm 安装
├── 创建 ait-build/ 目录
├── CopyBuildConfigFromTemplate()
│   ├── 将 SDK BuildConfig~/ 复制到 ait-build/
│   └── 复制 pnpm-lock.yaml
├── CopyWebGLToPublic()
│   ├── 检查 webgl/Build/（按压缩格式检测文件）
│   ├── Build → 复制到 ait-build/public/Build/
│   ├── TemplateData → 复制到 ait-build/public/TemplateData/
│   ├── Runtime → 复制到 ait-build/public/Runtime/
│   ├── index.html 占位符替换 → ait-build/index.html
│   ├── 插入加载画面
│   └── 占位符验证
├── 检查 pnpm 路径
└── ValidateNodeModulesIntegrity()

同步路径：RunPnpmInstallSync() → RunGraniteBuildSync()
异步路径：RunPnpmInstallAsync() → RunGraniteBuildAsync()

产物：ait-build/dist/
```

#### 复制 BuildConfig

`CopyBuildConfigFromTemplate`会将此 SDK 的 `WebGLTemplates/AITTemplate/BuildConfig~/`将 `ait-build/`复制到。

| 文件                       | 处理                                      |
| ------------------------ | --------------------------------------- |
| `package.json`           | 合并 dependencies                         |
| `tsconfig.json`          | 合并 compilerOptions                      |
| `vite.config.ts`         | `%AIT_VITE_HOST%`, `%AIT_VITE_PORT%` 替换 |
| `granite.config.ts`      | 替换 13 个占位符                              |
| `apps-in-toss.config.ts` | 3.x 配置文件。 `granite.config.ts`以及类似的占位符集合 |
| `pnpm-lock.yaml`         | 存在时复制                                   |

实际的合并规则位于 `Package/BuildConfigMerger.cs`中有说明。

#### 将 WebGL 复制到 public

`CopyWebGLToPublic`如果是 `webgl/` 产物 `ait-build/` 重新部署为如下结构。

```
webgl/
├── Build/
│   ├── webgl.loader.js          → ait-build/public/Build/
│   ├── webgl.data               → ait-build/public/Build/
│   ├── webgl.framework.js       → ait-build/public/Build/
│   └── webgl.wasm               → ait-build/public/Build/
├── TemplateData/                → ait-build/public/TemplateData/
├── Runtime/                     → ait-build/public/Runtime/
└── index.html                   → ait-build/index.html（替换后）
```

在这一过程中，占位符替换和加载画面的插入会同时发生。替换规则在下面的 **占位符替换** 一节中。加载画面本身的行为和自定义方式在 [加载画面自定义](https://developers-apps-in-toss.toss.im/documentation/unity/build/loading-screen-customization)为准。

#### pnpm install

**安装跳过判定。** 为避免每次构建都重新执行 install，在成功 install 之后立即 `Package/PnpmInstallStateMarker.cs`为 `package.json`·`pnpm-lock.yaml`的内容哈希和 pnpm 版本 `ait-build/node_modules/.ait-install-state.json`记录到 `node_modules` 中。下一次构建中，只要这些值全部一致并且

把标记 `node_modules` **放在** 里的原因是 `NodeModulesValidator.CleanNodeModules`为 `node_modules`如果把整个 node\_modules 删除，标记也会一并失效——这会与重试策略的 clean 步骤自动保持一致。任何无法判定的情况，例如没有标记或解析失败，都按 fail-closed 处理为“不可跳过”。因为错误跳过（导致构建失败）的代价高于不必要的重装（浪费时间）。

开关是环境变量 `AIT_DISABLE_INSTALL_SKIP`。 `1`/`true`，会关闭跳过；若值无法解析，会记录警告并同样关闭跳过——为避免因拼写错误而使关闭开关失效，按 fail-safe 方式运行。

**第 3 阶段重试。** 不跳过时 `PnpmInstallStages` 按数组中定义的顺序进行。

```
┌──────────────────────────────────────┐
│  ValidateNodeModulesIntegrity()      │
│  web-framework 版本不一致？          │
│  → 删除 node_modules 后重新安装       │
└─────────────┬────────────────────────┘
              │
              ▼
┌──────────────────────────────────────┐
│  第一轮：pnpm install --frozen-lockfile │  ← 最快（lockfile 无变更）
│  成功？→ 完成                        │
│  失败？↓                             │
├──────────────────────────────────────┤
│  第二轮：pnpm install                   │  ← 允许更新 lockfile
│       --no-frozen-lockfile           │
│  成功？→ 完成                        │
│  失败？↓                             │
├──────────────────────────────────────┤
│  第三轮：CleanNodeModules()             │  ← 删除 node_modules + .npm-cache
│       + pnpm install                 │
│         --no-frozen-lockfile         │
│  成功？→ 完成                        │
│  失败？→ FAIL_NPM_BUILD 错误         │
└──────────────────────────────────────┘
```

`ValidateNodeModulesIntegrity()`的判定顺序：

1. `node_modules/`若不存在则有效（新安装）
2. `node_modules/.pnpm/` 目录不存在则无效（陈旧模块）
3. `package.json`在 `@apps-in-toss/web-framework` 版本提取
4. `node_modules/.pnpm/@apps-in-toss+web-framework@{version}*/` 存在性检查——版本不一致或包不存在时会先警告再判为无效

#### granite build

```bash
pnpm run build   # → 执行 granite build
```

如果失败， `CleanNodeModules()` → `pnpm install --no-frozen-lockfile` → `pnpm run build`再重试一次；如果还是失败，则 `FAIL_NPM_BUILD`。

产物位于 `ait-build/dist/`，如果这里没有 `.ait` 文件 `DIST_FOLDER_MISSING` 或 `AIT_FILE_MISSING`则会继续导致

### 文件签名检测与验证

`AITBuildValidator`会验证 WebGL 产物的存在性和完整性。

#### 按压缩格式的搜索模式

`GetFilePatterns(compressionFormat, decompressionFallback)`根据此构建标记值确定搜索模式。

| 条件                             | data 模式           | framework 模式              | wasm 模式           |
| ------------------------------ | ----------------- | ------------------------- | ----------------- |
| `decompressionFallback = true` | `*.data.unityweb` | `*.framework.js.unityweb` | `*.wasm.unityweb` |
| `0` 禁用                         | `*.data`          | `*.framework.js`          | `*.wasm`          |
| `1` Gzip                       | `*.data.gz`       | `*.framework.js.gz`       | `*.wasm.gz`       |
| `2` Brotli                     | `*.data.br`       | `*.framework.js.br`       | `*.wasm.br`       |
| 其他（回退）                         | `*.data*`         | `*.framework.js*`         | `*.wasm*`         |

`decompressionFallback`如果启用，则优先于压缩格式。由于 loader 不属于压缩对象，因此始终 `*.loader.js`。

#### 文件检测

`FindFileInBuild(buildPath, pattern, isRequired)`使用 glob 模式查找文件。 `*.data*` 相同的尾部通配符也会 `*.data.meta`匹配，因此 `.meta`会从结果中排除——如果不排除， `LastWriteTime` 按 `.meta`进行排序时会选出最新项，导致返回错误的文件名。

| 模式                | 必填 | 说明              |
| ----------------- | -- | --------------- |
| `*.loader.js`     | 是  | Unity WebGL 加载器 |
| `*.data*`         | 是  | 游戏数据            |
| `*.framework.js*` | 是  | Unity 框架        |
| `*.wasm*`         | 是  | WebAssembly 二进制 |
| `*.symbols.json*` | 否  | 调试符号            |

**在重复匹配时自动清理。** 当一个模式匹配到多个文件时， `LastWriteTime` 按降序（同分时按文件名降序）排序，只保留最新的一个，其余的 `.meta`会一并删除。仅保留警告日志的方式会在每次构建中重复出现，累积 Sentry 噪音，因此改为删除。如果有删除失败的文件，会输出建议执行 Clean Build 的信息日志。

**必需文件缺失。** `isRequired=true`如果找不到某个模式，只将第一行发送到 Sentry（以便按模式稳定聚合 fingerprint），其余诊断行仅保留在控制台。诊断内容包括搜索路径、以下字符串，以及 Build 文件夹中的实际文件列表（如果为空也会说明）。

```
如果没有此文件，运行时会发生 'createUnityInstance is not defined' 错误。
```

返回值为空字符串，并在调用处 `REQUIRED_FILE_MISSING`则会继续导致

#### 占位符替换验证

`ValidatePlaceholderSubstitution(content, filePath)`该正则表达式 `%[A-Z_]+%`查找未替换的占位符。

严重（错误 + 构建失败）：

* `%UNITY_WEBGL_LOADER_URL%`
* `%UNITY_WEBGL_DATA_URL%`
* `%UNITY_WEBGL_FRAMEWORK_URL%`
* `%UNITY_WEBGL_CODE_URL%`

其他 `%...%` 模式只会记警告。以下空路径模式也会被视为严重错误。

```html
src="Build/"     ← 表示缺少 loader.js
"Build/"         ← 表示缺少 data 文件
Build/",         ← 分隔符后文件名为空
```

`apps-in-toss.config.ts`在这种情况下，SDK\_GENERATED 区域中的未替换项是硬错误，但 USER\_CONFIG 区域中残留的 SDK 占位符或从 3.x 移过来的键只会是警告——由于合并时优先使用 SDK 值，构建结果仍然正常。

#### 构建完成报告

`PrintBuildReport(buildProjectPath, distPath)`为 `ait-build/public/Build/`扫描并输出 4 个必需模式和 1 个可选模式是否存在，以及文件大小。若缺少必需模式， `Debug.LogError`做 `[缺失！]`进行标记，而可选模式仅在存在时显示。

### 占位符替换

#### index.html

`AITPackageBuilder.CopyWebGLToPublic()`中执行。

**Unity 标准**

| 占位符                       | 来源                                      |
| ------------------------- | --------------------------------------- |
| `%UNITY_WEB_NAME%`        | `PlayerSettings.productName`            |
| `%UNITY_WIDTH%`           | `PlayerSettings.defaultWebScreenWidth`  |
| `%UNITY_HEIGHT%`          | `PlayerSettings.defaultWebScreenHeight` |
| `%UNITY_COMPANY_NAME%`    | `PlayerSettings.companyName`            |
| `%UNITY_PRODUCT_NAME%`    | `PlayerSettings.productName`            |
| `%UNITY_PRODUCT_VERSION%` | `PlayerSettings.bundleVersion`          |

**Unity WebGL URL** — 如果未替换，构建将失败。

| 占位符                           | 替换值                           |
| ----------------------------- | ----------------------------- |
| `%UNITY_WEBGL_LOADER_URL%`    | `Build/{loaderFile}`          |
| `%UNITY_WEBGL_DATA_URL%`      | `Build/{dataFile}`            |
| `%UNITY_WEBGL_FRAMEWORK_URL%` | `Build/{frameworkFile}`       |
| `%UNITY_WEBGL_CODE_URL%`      | `Build/{wasmFile}`            |
| `%UNITY_WEBGL_SYMBOLS_URL%`   | `Build/{symbolsFile}` （或空字符串） |

**旧版文件名** — 用于向后兼容。仅替换文件名，不含路径。

| 占位符                                | 替换值               |
| ---------------------------------- | ----------------- |
| `%UNITY_WEBGL_LOADER_FILENAME%`    | `{loaderFile}`    |
| `%UNITY_WEBGL_DATA_FILENAME%`      | `{dataFile}`      |
| `%UNITY_WEBGL_FRAMEWORK_FILENAME%` | `{frameworkFile}` |
| `%UNITY_WEBGL_CODE_FILENAME%`      | `{wasmFile}`      |
| `%UNITY_WEBGL_SYMBOLS_FILENAME%`   | `{symbolsFile}`   |

**AIT 自定义**

| 占位符                          | 替换值                  | 说明                                                                                                                      |
| ---------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `%AIT_ENABLE_DEBUG_CONSOLE%` | `"true"` / `"false"` | 启用调试控制台                                                                                                                 |
| `%AIT_DEVICE_PIXEL_RATIO%`   | 数字                   | 设备像素比                                                                                                                   |
| `%AIT_ICON_URL%`             | URL 字符串              | 应用图标 URL                                                                                                                |
| `%AIT_DISPLAY_NAME%`         | 字符串                  | 应用显示名称                                                                                                                  |
| `%AIT_PRIMARY_COLOR%`        | 颜色代码                 | 品牌颜色（默认： `#3182f6`)                                                                                                     |
| `%AIT_PRELOAD_TAGS%`         | HTML 标签              | `<link rel="preload">` 标签                                                                                               |
| `%AIT_LOADING_SCREEN%`       | HTML 字符串             | 加载画面的全部内容。 [加载画面自定义](https://developers-apps-in-toss.toss.im/documentation/unity/build/loading-screen-customization) 参考 |

#### Preload 标签

`GeneratePreloadTags(dataFile, wasmFile, frameworkFile)`生成。

```html
<link rel="preload" href="Build/webgl.data" as="fetch">
<link rel="preload" href="Build/webgl.wasm" as="fetch">
```

> **重要**：framework.js 不会被 preload。如果 Unity 加载器通过 `<script>` 标签加载 framework.js， `as="fetch"` preload 与缓存键不一致，可能会导致重复下载。这会增加内存压力，从而提高偶发初始化失败（ASM\_CONSTS 错误）的概率。

#### granite.config.ts

`Package.BuildConfigMerger.UpdateGraniteConfig()`中替换 13 项。

| 占位符                                         | 来源                                       |
| ------------------------------------------- | ---------------------------------------- |
| `%AIT_APP_NAME%`                            | `config.appName`                         |
| `%AIT_DISPLAY_NAME%`                        | `config.displayName`                     |
| `%AIT_PRIMARY_COLOR%`                       | `config.primaryColor`                    |
| `%AIT_ICON_URL%`                            | `config.iconUrl`                         |
| `%AIT_BRIDGE_COLOR_MODE%`                   | `config.GetBridgeColorModeString()`      |
| `%AIT_WEBVIEW_TYPE%`                        | `config.GetWebViewTypeString()`          |
| `%AIT_NAVIGATION_BAR%`                      | `config.GetNavigationBarJson()`          |
| `%AIT_ALLOWS_INLINE_MEDIA_PLAYBACK%`        | `config.allowsInlineMediaPlayback`       |
| `%AIT_MEDIA_PLAYBACK_REQUIRES_USER_ACTION%` | `config.mediaPlaybackRequiresUserAction` |
| `%AIT_VITE_HOST%`                           | `config.viteHost`                        |
| `%AIT_VITE_PORT%`                           | `config.vitePort`                        |
| `%AIT_PERMISSIONS%`                         | `config.GetPermissionsJson()`            |
| `%AIT_OUTDIR%`                              | `config.outdir`                          |

#### vite.config.ts

`%AIT_VITE_HOST%` → `config.viteHost`, `%AIT_VITE_PORT%` → `config.vitePort`.

### 模板合并时机

`AITTemplateManager`通过基于标记的方式合并 SDK 模板和项目自定义区域。标记语法和用户可编辑区域是 [构建自定义](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-customization)权威定义。这里只说明合并 **何时、对什么** 发生。

合并发生在 Phase 0 的 `EnsureWebGLTemplatesExist`中，也就是在 Unity WebGL 构建开始之前。

```
SDK 更新时：
  ├── index.html:
  │   ├── 无标记（旧版本）→ 替换为 SDK 模板 + 警告
  │   └── 有标记 → 保留 USER_HEAD、USER_BODY_END 区域，更新其余部分
  ├── vite.config.ts, granite.config.ts, apps-in-toss.config.ts:
  │   └── 保留 USER_CONFIG 区域，更新 SDK_GENERATED 区域
  ├── Runtime/ → 始终覆盖为 SDK 版本（如调试控制台等）
  └── TemplateData/ → 始终覆盖为 SDK 版本
```

发现没有标记的旧版 index.html 时，会先输出以下警告，然后替换为 SDK 模板。

```
[AIT] 模板更新：将旧版本模板替换为新的基于标记的模板。
⚠️ 如果旧的 index.html 中有自定义修改，请手动重新应用到 USER_* 标记区域。
```

### Node.js 和 pnpm 管理

SDK 会独立下载并使用自己的 Node.js，不依赖系统安装。版本的单一来源是 `AITNodeJSDownloader.cs`的 `NODE_VERSION`和 `AITPackageManagerHelper.cs`的 `PNPM_VERSION`。

`PNPM_VERSION`是 `package.json`, `sdk-runtime-generator~/package.json`, `WebGLTemplates/AITTemplate/BuildConfig~/package.json` 三处的 `packageManager` 字段必须始终一致。如果值不一致，客户端使用的 pnpm 与更新 lockfile 的 pnpm 会不同，从而产生 specifier drift。

安装路径是 `~/.ait-unity-sdk/nodejs/v{NODE_VERSION}/{platform}/`。

下载镜像按顺序回退。

1. `https://nodejs.org/dist/` （官方）
2. `https://cdn.npmmirror.com/binaries/node/`
3. `https://repo.huaweicloud.com/nodejs/`

```
1. 确认安装路径 → 如果已存在则跳过
2. 尝试镜像 1：
   ├── 下载 .tar.gz（macOS/Linux）或 .zip（Windows）
   ├── 验证 SHA256 校验和 ← 失败时删除下载文件 + 切换到下一个镜像
   └── 解压 → 临时文件夹
3. 镜像 2/3 回退（相同流程）
4. 临时文件夹 → 最终路径的原子移动
5. 安装 pnpm：corepack enable + corepack prepare
```

各平台的 SHA256 哈希硬编码在 `AITNodeJSDownloader.cs`中。Node.js 和 pnpm 的执行路径解析及进程管理由 `AITPackageManagerHelper`负责。

### 错误代码

`AITConvertCore.AITExportError` 是一个 enum。值 `7`曾是旧的 `WEBGL_BUILD_INCOMPLETE`但 `10`\~`13`在细分后被移除了。

| 代码                      | 值  | 简短标签          |
| ----------------------- | -- | ------------- |
| `成功`                    | 0  | 成功            |
| `未找到 Node.js`           | 1  | 未找到 Node.js   |
| `WebGL 构建失败`            | 2  | WebGL 构建错误    |
| `INVALID_APP_CONFIG`    | 3  | 应用设置错误        |
| `NETWORK_ERROR`         | 4  | 网络错误          |
| `已取消`                   | 5  | 用户取消          |
| `FAIL_NPM_BUILD`        | 6  | pnpm 构建错误     |
| `缺少 Build 文件夹`          | 10 | 缺少 Build 文件夹  |
| `REQUIRED_FILE_MISSING` | 11 | 必需文件缺失        |
| `缺少 index.html`         | 12 | 缺少 index.html |
| `占位符替换失败`               | 13 | 未替换占位符        |
| `DIST_FOLDER_MISSING`   | 14 | 缺少 dist 文件夹   |
| `AIT_FILE_MISSING`      | 15 | 缺少 .ait 文件    |

要展示给用户的完整消息和简短标签都由 `AITExportErrorCatalog`负责。

```
发生构建错误
  ↓
ShowComplexDialog("构建失败", errorMessage, ...)
  ├── "确定" → 退出
  └── "Issue 신고" → AITErrorReporter.OpenIssueInBrowser()
                     → 打开自动填充到 GitHub Issues 的 issue URL
```

### 用户警告与对话框条件

#### 错误对话框

| 条件           | 标题     | 内容              |
| ------------ | ------ | --------------- |
| 缺少 SDK 加载模板  | "错误"   | 找不到 SDK 加载画面模板  |
| 缺少 Build 文件夹 | "错误"   | 找不到 WebGL 构建文件夹 |
| 应用名称未设置      | "错误"   | 应用名称未设置         |
| 分发密钥未设置      | "错误"   | 分发密钥未设置         |
| pnpm 安装失败    | "构建失败" | pnpm 安装失败       |
| 构建取消         | "已取消"  | 构建已取消           |
| 构建成功         | "成功"   | 构建和打包已完成        |
| Clean 完成     | "完成"   | Clean 已完成       |
| 分发超时         | "超时"   | 分发超时            |
| 端口冲突         | "端口冲突" | 该端口已在使用中        |

#### 确认对话框

| 条件          | 行为                                    |
| ----------- | ------------------------------------- |
| `AIT/Clean` | "要删除 webgl/、ait-build/ 文件夹吗？"         |
| 发布确认        | "应用名称：X，版本：Y — 要发布吗？"（显示自动生成的 memo 值） |
| 重置设置        | "要重置设置吗？"                             |
| 重置加载画面      | "要将加载画面重置为默认模板吗？"                     |

#### 三方对话框

| 条件   | 选项                |
| ---- | ----------------- |
| 构建失败 | "确定" / "Issue 신고" |
| 发布失败 | "确定" / "Issue 신고" |

#### 控制台警告

| 条件                                  | 消息摘要           |
| ----------------------------------- | -------------- |
| 非严重的占位符未替换                          | 显示对应占位符名称      |
| USER\_CONFIG 中残留 SDK 管理设置           | 建议移除（构建正常）     |
| `pnpm install --frozen-lockfile` 失败 | 进入下一次重试步骤      |
| web-framework 版本不一致                 | 显示预期版本与实际版本    |
| `node_modules/.pnpm` 无              | 过期模块           |
| 旧版本模板升级                             | 自定义修改手动重新应用说明  |
| 无加载画面文件                             | 使用了空白加载画面      |
| 写入构建标记失败                            | 仅警告（构建继续）      |
| 缺少构建标记 / Unity版本不匹配                 | 自动 clean build |
| `AIT_DISABLE_INSTALL_SKIP` 无法解析值    | 按未启用跳过处理       |

#### 控制台错误

| 条件                        | 结果                      |
| ------------------------- | ----------------------- |
| `webgl/Build/` 文件夹不存在     | `缺少 Build 文件夹`          |
| 缺少必需的 WebGL 文件            | `REQUIRED_FILE_MISSING` |
| `index.html` 无            | `缺少 index.html`         |
| 致命占位符未替换                  | `占位符替换失败`               |
| 检测到空路径模式                  | `占位符替换失败`               |
| granite build 后没有 dist    | `DIST_FOLDER_MISSING`   |
| 在 dist 中 `.ait` 无         | `AIT_FILE_MISSING`      |
| pnpm install 最终失败         | 构建中止                    |
| 缺少 SDK BuildConfig 文件夹    | 构建中止                    |
| 缺少 SDK WebGLTemplates 文件夹 | 构建中止                    |

### 服务器生命周期

本地服务器只有 Dev Server 一个（过去存在的 Production Server 自 3.0.0 起因无法与沙盒应用联动而被移除——若要在真机上确认生产配置，请 [入门中的 Deploy (Test)](https://developers-apps-in-toss.toss.im/documentation/unity/first-steps/getting-started)使用）。

| 方法                          | 说明                                       |
| --------------------------- | ---------------------------------------- |
| `StartServer()`             | 构建 + 启动服务器                               |
| `StopServer()`              | 终止服务器进程                                  |
| `RestartServer(serverOnly)` | `serverOnly=false`则构建+服务器， `true`则仅重启服务器 |

```
AIT/Dev Server/
├── 启动服务器              → StartServer() → DoExport(dev) + granite dev
├── 停止服务器               → StopServer()
├── 重启服务器            → RestartServer(serverOnly: false)
└── 重启服务器（仅服务器） → RestartServer(serverOnly: true)
```

如果目标端口已被占用，则会弹出“端口冲突”对话框，用户需要更改端口或终止占用该端口的进程。

### 错误报告

`AITErrorReporter`为 `[InitializeOnLoad]`在编辑器启动时 `Application.logMessageReceived`订阅以将所有控制台日志捕获到循环缓冲区中。

| 缓冲区           | 最大大小 | 捕获对象                                 |
| ------------- | ---- | ------------------------------------ |
| `errorLogs`   | 50条  | `LogType.Error`, `LogType.Exception` |
| `warningLogs` | 30条  | `LogType.Warning`                    |
| `infoLogs`    | 20条  | `LogType.Log`, `LogType.Assert`      |

`OpenIssueInBrowser(errorCode, profileName)`会根据此缓冲区自动构建 GitHub Issue URL。标题为 `[构建错误] {errorCode}`，正文包含 SDK/Unity/OS 版本、配置文件名称、错误代码和消息、应用设置、 `BuildReport` 错误（如有）以及最近的控制台日志。若 URL 超过 2000 个字符 `infoLogs` → `warningLogs` → `errorLogs` 则按顺序逐步截断。

发送到 Sentry 的日志范围与噪声抑制策略为 [Sentry集成](https://developers-apps-in-toss.toss.im/documentation/unity/add-features/sentry-integration)中有说明。

### 相关文档

* [构建配置文件](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles) — 各配置文件的设置差异、环境变量覆盖
* [构建自定义](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-customization) — 标记区域契约、Web 入口点编辑
* [加载画面自定义](https://developers-apps-in-toss.toss.im/documentation/unity/build/loading-screen-customization) — 加载画面替换与 `AITLoading` API
* [Sentry集成](https://developers-apps-in-toss.toss.im/documentation/unity/add-features/sentry-integration) — 错误收集与上下文注入
* [问题排查](https://developers-apps-in-toss.toss.im/documentation/unity/first-steps/faq) — 构建受阻时


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developers-apps-in-toss.toss.im/documentation/api-and-sdk-zh/unity/build/build-process.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
