> 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 产物，以及将这些产物重新部署为 Web 项目并用 granite 打包。

```
┌─────────────────────────────────────────────────────────────────────┐
│                        入口点                                 │
│  菜单：构建与打包  │  构建窗口  │  服务器启动/重启    │
│  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 |
| --------------------- | ---------- | ----------- | ---------- |
| `构建并打包`               | `true`     | `true`      | `false`    |
| `构建与打包（清理）`           | `true`     | `true`      | `true`     |
| `Deploy (Test)`       | `true`     | `true`      | `false`    |
| `Deploy (Production)` | `true`     | `true`      | `true`     |
| `开发服务器启动`             | `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` | 硬编码                                                    |
| Linker Target       | `Wasm`                | 硬编码                                                    |
| 脚本后端                | `IL2CPP`              | 硬编码                                                    |
| 内存大小                | 256\~1536MB           | 按 Unity 版本的默认值（用户可覆盖）                                  |
| 压缩                  | `Brotli`              | 默认值。 `decompressionFallback`已开启，因此可在所有 Unity 版本中使用     |
| 线程                  | `false`               | 默认值（移动浏览器兼容性）                                          |
| 数据缓存                | `false`               | 默认值                                                    |
| `nameFilesAsHashes` | 用户设置（默认 `true`)       | 仅在 Unity 2021.x 中强制 `false` — `true`否则会出现 Bee 构建循环 bug |
| 引擎代码剥离              | 用户设置                  | —                                                      |
| 托管剥离                | `High`                | 默认值                                                    |
| IL2CPP Config       | 用户设置                  | —                                                      |

默认值的单一来源是 `AITEditorScriptObject`中 `GetDefault*` 静态方法。只有 Dev Server 配置文件会将压缩 `Disabled`降到较低级别——配置文件之间的差异 [构建配置文件](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` 通过完整性验证，就会跳过 install。

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

Kill switch 是环境变量 `AIT_DISABLE_INSTALL_SKIP`。 `1`/`true`如果为真则关闭跳过；如果值无法解析，则先输出警告，再同样关闭跳过——以 fail-safe 方式运行，避免因拼写错误而让 kill switch 失效。

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

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

`ValidateNodeModulesIntegrity()`的判定顺序：

1. `node_modules/`如果没有则有效（新安装）
2. `node_modules/.pnpm/` 如果目录不存在则无效（stale modules）
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` Disabled                   | `*.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 不会预加载。若 Unity 加载器通过 `<script>` 标签加载 framework.js， `as="fetch"` 预加载与缓存键不一致，可能导致重复下载。这会增加内存压力，并提高偶发初始化失败（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` 枚举。值 `7`是之前的 `WEBGL_BUILD_INCOMPLETE`，但随着细分为 `10`\~`13`而被移除了。

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

要显示给用户的完整消息和简短标签都由 `AITExportErrorCatalog`拥有。

```
发生构建错误
  ↓
ShowComplexDialog("构建失败", errorMessage, ...)
  ├── "确认" → 退出
  └── "Issue 报告" → AITErrorReporter.OpenIssueInBrowser()
                     → 打开自动填充了内容的 GitHub Issues 链接
```

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

#### 错误对话框

| 条件           | 标题     | 内容              |
| ------------ | ------ | --------------- |
| 缺少 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 版本不匹配                 | 预期 vs 实际版本显示  |
| `node_modules/.pnpm` 无              | 过期模块          |
| 旧版本模板升级                             | 自定义修改手动重新应用指南 |
| 缺少加载画面文件                            | 使用了空白加载画面     |
| 生成构建标记失败                            | 仅警告（构建继续）     |
| 无构建标记 / Unity 版本不匹配                 | 自动清理构建        |
| `AIT_DISABLE_INSTALL_SKIP` 无法解析值    | 按跳过禁用处理       |

#### 控制台错误

| 条件                        | 结果                                |
| ------------------------- | --------------------------------- |
| `webgl/Build/` 文件夹不存在     | `BUILD_FOLDER_MISSING`            |
| 缺少必需的 WebGL 文件            | `REQUIRED_FILE_MISSING`           |
| `index.html` 无            | `INDEX_HTML_MISSING`              |
| 致命占位符未替换                  | `PLACEHOLDER_SUBSTITUTION_FAILED` |
| 检测到空路径模式                  | `PLACEHOLDER_SUBSTITUTION_FAILED` |
| 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/
├── Start Server              → StartServer() → DoExport(dev) + granite dev
├── Stop Server               → StopServer()
├── Restart Server            → RestartServer(serverOnly: false)
└── Restart Server (server-only) → 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) — 标记区域契约、网页入口编辑
* [加载画面自定义](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.
