@skillbuilder/miniapp-simulator 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/docs/assets/audit-auto-stop-config-2026-10-21.jpg +0 -0
  2. package/docs/assets/audit-balanced-panel-2026-10-16.jpg +0 -0
  3. package/docs/assets/audit-lcp-input-2026-10-20.jpg +0 -0
  4. package/docs/assets/audit-paint-game3d-2026-10-16.jpg +0 -0
  5. package/docs/assets/audit-paint-webgl-2026-10-16.jpg +0 -0
  6. package/docs/assets/audit-paint-webgl-apm-excerpt-2026-10-16.json +397 -0
  7. package/docs/assets/audit-poetry-2026-10-16.jpg +0 -0
  8. package/docs/assets/audit-remaining-results-2026-10-16.json +2749 -0
  9. package/docs/assets/miniapp-audit-risk-review-2026-10-13.png +0 -0
  10. package/docs/assets/miniapp-audit-runtime-hung-2026-10-13.png +0 -0
  11. package/docs/audit-data-collection.codex.md +422 -0
  12. package/docs/audit-local-validation.codex.md +165 -0
  13. package/docs/audit-score-design.codex.md +221 -0
  14. package/docs/audit-score-design.md +885 -0
  15. package/docs/audit-score-tech-plan.md +448 -0
  16. package/docs/audit-tech-design.codex.md +468 -0
  17. package/docs/electron-review.md +223 -12
  18. package/docs/quality-evaluation-redoc.xml +163 -0
  19. package/lib/client-BDt3WDu_.mjs +1118 -0
  20. package/lib/{index-D5AYVv8s.mjs → index-7MeYFlk_.mjs} +210 -182
  21. package/lib/index.js +1254 -1115
  22. package/lib/remote-client.js +2 -2
  23. package/lib/types/host/shadow-host.d.ts +10 -0
  24. package/lib/types/injection/registry.d.ts +5 -0
  25. package/lib/types/remote/audit-controller.d.ts +56 -0
  26. package/lib/types/remote/audit-leave-monitor.d.ts +12 -0
  27. package/lib/types/remote/audit-telemetry.d.ts +23 -0
  28. package/lib/types/remote/audit-watchdog.d.ts +14 -0
  29. package/lib/types/remote/client.d.ts +25 -1
  30. package/lib/types/remote/load-timing.d.ts +5 -0
  31. package/lib/types/remote/protocol.d.ts +151 -1
  32. package/lib/types/remote/runtime.d.ts +2 -0
  33. package/lib/types/remote-client.d.ts +2 -2
  34. package/lib/types/sw/service-worker-host.d.ts +15 -0
  35. package/lib/types/tracker/default-tracker.d.ts +8 -0
  36. package/lib/types/types/options.d.ts +24 -0
  37. package/lib/workbench.js +1 -1
  38. package/package.json +1 -1
  39. package/lib/client-_vNHVHSm.mjs +0 -775
@@ -0,0 +1,885 @@
1
+ # 小工具体验评分与性能管控方案
2
+
3
+ ## 1. 背景与目标
4
+
5
+ 本文覆盖一期远程模拟器评测及其 1.1 期游戏增强。1.1 期的识别模型、专项指标、审核流程和验收要求见 §15,实施状态为待开发、待验证;当前已实现的采集与默认策略见 [一期评分与采集设计](./audit-score-design.codex.md)。
6
+
7
+ 小工具形态跨度极大:传统工具类(算命、计算器)、2D/3D 游戏、视频类并存,产物由开发者自己的
8
+ 构建链路产出(webpack / vite / Unity / Cocos),不在平台控制范围内。线上频繁出现 Android / iOS
9
+ 侧 OOM,问题直接暴露给用户。
10
+
11
+ 需要在模拟器侧建立基础管控能力,让审核在上线前拦住低质小工具。
12
+
13
+ 目标:
14
+
15
+ - 在**远程模拟器**链路上提供按需触发的体验评测能力,产出评分与优化建议。
16
+ - 优先解决 OOM:建立内存、显存、泄漏斜率等此前完全没有覆盖的指标。
17
+ - 阈值与准入规则全部由 Apollo 下发,先采集、后调控,调整阈值与开启卡口都不发版。
18
+ - 采集、打分、上报失败时静默降级,不阻塞、不抛错、不改变小工具本身的运行结果。
19
+
20
+ 不在本期范围:Electron 审核模拟器(`apps/mini-app-review-simulator`)、创作者侧自查面板。
21
+
22
+ ## 2. 使用形态与整体架构
23
+
24
+ ### 2.1 按需进入的评测模式
25
+
26
+ 评测按需进入,不跟车常驻:
27
+
28
+ ```text
29
+ 审核台(Mars)打开小工具
30
+ └─ 正常预览(不加载评测插件,零开销)
31
+ └─ 审核员点「开始评测」
32
+ ├─ runtime 按业务线拉取评测插件
33
+ ├─ 注册探针到 InjectionRegistry
34
+ ├─ reload 小工具(带探针重跑)
35
+ ├─ 审核员操作 / auto-pilot,持续采集
36
+ └─ 「结束评测」→ 出报告 →「重新检测」/「导出 JSON」
37
+ ```
38
+
39
+ **评测必须触发一次 reload。** 探针需在 document-start、早于业务 bundle 注入才能捕获启动期分配,
40
+ 对已运行的实例插桩必然漏数据。该行为需在交互上让审核员有预期。
41
+
42
+ `BusinessLine` 已有 `mars`(审核)取值,`creator` / `skillDesignable` 业务线默认不加载插件。
43
+
44
+ ### 2.2 插件形态
45
+
46
+ 评测能力以**独立插件**接入,不编译进模拟器核心:
47
+
48
+ ```text
49
+ @skillbuilder/miniapp-audit-plugin
50
+ ├─ guest/ 探针源码(构建为字符串,安全 hook)
51
+ ├─ runtime/ 注册探针 + 收集时序 + 调规则引擎 + 出报告
52
+ └─ panel/ Vue 评分面板(defineCustomElement),见 §10
53
+ ```
54
+
55
+ 三段全部运行在 **runtime iframe 内**,宿主页不参与,详见 §10.1。
56
+
57
+ ### 2.3 装载链路
58
+
59
+ `InjectionRegistry`(`src/injection/registry.ts`)本身已是干净的扩展点:介质无关、`register()`
60
+ 支持按 id 覆盖、已有 `unregister()` 与稳定顺序数组。**不需要新造插件机制**,只需把注册口从构造
61
+ 函数里写死的内置列表(`src/core/simulator.ts` 中 8 个内置模块)开放出来:
62
+
63
+ ```ts
64
+ registerInjection(module: InjectionModule): void
65
+ unregisterInjection(id: string): boolean
66
+ ```
67
+
68
+ 约束:评测模块必须排在 `securityInjection` 之前(seal IIFE 最后会冻结 `navigator`),且尽量靠前。
69
+
70
+ 远程链路下宿主页无法直接传入插件——`RemoteSimulatorSerializableOptions` omit 了全部函数型 option,
71
+ 插件只能由 runtime 侧装载。服务端插件目录照抄 `runtime-manifest-catalog` 的形状:
72
+
73
+ ```ts
74
+ interface SimulatorPluginCatalog {
75
+ activePluginsByBusinessLine: Record<string, string[]>; // { "mars": ["audit@3"] }
76
+ releases: Record<string, {
77
+ buildId: string;
78
+ protocolVersion: number; // 与 runtime 协议版本严格校验
79
+ pluginUrl: string; // 同 origin,托管在 simulator-server
80
+ integrity?: string;
81
+ }>;
82
+ }
83
+ ```
84
+
85
+ 加载用动态 `import(pluginUrl)`,**不用 eval**:插件产物与 runtime 同源,ESM 动态导入无需放开
86
+ `script-src unsafe-eval`。
87
+
88
+ guest 侧探针代码以**字符串**形式由 `InjectionModule.buildHeadScriptSource()` 返回,装配器拼进带
89
+ nonce 的内联 script,**guest 的 CSP 无需任何改动**。
90
+
91
+ ### 2.4 注入时机
92
+
93
+ `src/sw/worker/serve.ts` 的 `injectHead()` 有三级兜底:
94
+
95
+ ```text
96
+ 有 <head> → 插在 <head> 开标签紧后
97
+ 无 head 但有 <html> → 造 <head> 插入
98
+ 两者都无 → 整体前置到 HTML 最前
99
+ ```
100
+
101
+ 三种情况下注入片段都是文档中第一个执行的脚本,业务 bundle 不可能先于探针运行,**hook 安装在
102
+ t=0 完成,不会漏任何调用**。
103
+
104
+ ## 3. 打标与探针注入策略
105
+
106
+ ### 3.1 统一标签接口
107
+
108
+ ```ts
109
+ interface MiniAppFeatureTags {
110
+ source: "declared" | "scanned" | "merged";
111
+ appType?: "tool" | "game2d" | "game3d" | "video";
112
+ features: string[]; // ["webgl","wasm","worker","canvas2d","audio",...]
113
+ evidence?: Record<string, string>; // 扫到的证据(文件:特征串),仅审计用
114
+ }
115
+ ```
116
+
117
+ 一期 `source: "scanned"`(包解析阶段字符串扫描),未来上游直接下发即为 `"declared"`,
118
+ **下游消费方无需改动**,扫描器可直接下线。
119
+
120
+ 打标时机插在 `parseRawZip` 之后、`buildSwInjectionPayload` 之前,天然早于注入决策。
121
+
122
+ ### 3.2 注入门控按「常驻成本」划分,不按标签划分
123
+
124
+ 被动 hook 若从未被调用,成本仅为启动时一次属性赋值,运行期开销精确为 0。条件注入省不到实际
125
+ 开销,却会因漏标造成**数据永久丢失且无法事后补测**。因此:
126
+
127
+ | 探针类型 | 不使用时的常驻成本 | 注入策略 |
128
+ | --- | --- | --- |
129
+ | 被动 hook:`texImage2D` / `bufferData` / `getContext` / `new Worker` / `WebAssembly.Memory.grow` / `Image.load` | 0 | **全量注入,不看标签** |
130
+ | 主动轮询:heap 采样 `setInterval`、rAF FPS 循环、`PerformanceObserver`、DOM 扫描 | 持续消耗 | 按 Apollo 策略/标签开关 |
131
+
132
+ 判据是「这段代码不用时会不会持续消耗 CPU」,而非「这个能力用不用得上」。
133
+
134
+ **禁止在探针内部做 `if (hasWebGL)` 之类的能力分支判断。** 能力差异通过注入清单与评分项适用性
135
+ 表达,探针本身只负责采集。
136
+
137
+ ### 3.3 标签的主职责:评分项适用性与预算档位
138
+
139
+ ```text
140
+ 包解析 (RawParseResult)
141
+ └─ 打标器 ──→ FeatureTags ─┬─→ [Apollo] featureProbes → 主动轮询类探针开关
142
+ (扫描 / 上游声明) └─→ [Apollo] featureMetrics → 适用评分项集合 → 覆盖度与归一化
143
+ ```
144
+
145
+ 同一份标签同时决定「采什么」与「评什么」,保证两者同源,避免出现「注了探针无人评分」或
146
+ 「评分项拿不到数据」的错位。
147
+
148
+ ### 3.4 漏标自愈
149
+
150
+ 运行期观测补齐渲染和资源能力,1.1 期按以下规则处理:
151
+
152
+ ```text
153
+ getContext("webgl" / "webgl2") 成功或观测到 GL 资源调用
154
+ → 补齐 webgl 能力与 renderModes
155
+ → 纳入图形资源相关指标
156
+ → 单独展示业务形态、引擎证据和识别置信等级
157
+ ```
158
+
159
+ WebGL、Canvas、WASM 或某个引擎的存在,只证明技术能力。业务形态由申报信息、运行证据与审核员确认共同确定;WebGL 不直接推出 3D 游戏,Three.js 不直接推出游戏,2D 游戏也可使用 WebGL。
160
+
161
+ 证据冲突时保留冲突原因和各自来源,提示审核员确认。形态未确定时标为 `unknown`,仍展示已观测到的资源和事故风险;没有识别到引擎不能让这些风险消失。识别的详细模型与预算选择见 §15.2。
162
+
163
+ ### 3.5 一期扫描规则
164
+
165
+ 1.1 期扫描 `rawFiles` 中的 JS / HTML 文本和资源路径,复用已有解包结果。扫描采用文件数、字节数和耗时预算;达到上限时标记扫描不完整。资源后缀和单个字符串属于弱证据,具体识别规则见 §15.2。
166
+
167
+ | 标签 | 特征串(命中任一即打标) |
168
+ | --- | --- |
169
+ | webgl | `getContext` 邻近的 `webgl` / `webgl2` / `experimental-webgl`、`THREE.`、`PIXI.`、`BABYLON`、`.glsl` |
170
+ | wasm | `WebAssembly`、包内存在 `.wasm` |
171
+ | worker | `new Worker`、`.worker.js` |
172
+ | canvas2d | `getContext` 邻近的 `"2d"` |
173
+ | audio | `AudioContext`、`new Audio`、包内音频文件 |
174
+ | video | `<video`、包内视频文件 |
175
+
176
+ ## 4. 评分项清单
177
+
178
+ 采集层标记:【包】静态包分析 ·【hook】guest 安全 hook ·【scan】guest DOM/CSSOM 扫描 ·
179
+ 【obs】PerformanceObserver / rAF ·【runtime】runtime 侧观测。
180
+
181
+ 「Canvas 类适用」列用于覆盖度计算:✅ 适用 · ⚠️ 部分适用 · ❌ 不适用。
182
+
183
+ ### 4.1 性能(30)
184
+
185
+ | 指标 | 判定标准 | 权重 | 采集层 | Canvas 类 |
186
+ | --- | --- | --- | --- | --- |
187
+ | 首屏时间 | FCP ≤ 5s | 5 | 【obs】已有 `installPaintReporter` | ✅ |
188
+ | 主内容绘制 | LCP ≤ 2.5s | 4 | 【obs】 | ✅ |
189
+ | 长任务阻塞 | 单次 longtask ≤ 1s,TBT ≤ 600ms | 5 | 【obs】`longtask` | ✅ |
190
+ | 渲染节奏 | 页面 rAF 节拍、业务 rAF 与绘制提交分别标注来源;主循环内统计间隔与卡顿,见 §15.4 | 0,完成校准后配置 | 【obs】+【hook】 | ⚠️ |
191
+ | DOM 节点数 | < 1500 节点 / 深度 < 30 / 单节点子节点 < 60 | 3 | 【scan】 | ❌ |
192
+ | 网络请求耗时 | ≤ 1s | 3 | 【hook】fetch/XHR + Resource Timing | ⚠️ |
193
+ | 网络请求并发 | >300ms 的并发 ≤ 10 | 2 | 【hook】 | ⚠️ |
194
+ | 网络请求缓存 | 3min 内同 url 不出现两次 >128KB 相同回包 | 3 | 【hook】 | ⚠️ |
195
+
196
+ ### 4.2 内存与稳定性(35)
197
+
198
+ OOM 的正面战场,本方案单独建类并给最高权重。
199
+
200
+ | 指标 | 判定标准 | 权重 | 采集层 | Canvas 类 |
201
+ | --- | --- | --- | --- | --- |
202
+ | 峰值 JS 堆 | P95 采样 − 会话基线 ≤ profile 预算,见 §5.4 | 6 | 【obs】`performance.memory` 采样 | ✅ |
203
+ | 堆增长斜率 | 稳定期 ≤ 8MB/min(泄漏检测,不需基线校正) | 7 | 时序线性回归 | ✅ |
204
+ | 估算显存 | 纹理 + buffer 累计 ≤ profile 预算 | 6 | 【hook】`texImage2D` / `texStorage2D` / `bufferData` / `renderbufferStorage` | ✅ |
205
+ | Canvas 后备缓冲 | 总和 ≤ profile 预算 | 3 | 【hook】canvas width/height + `getContext` | ✅ |
206
+ | 解码图片字节 | 总和 ≤ profile 预算 | 4 | 【hook】`load` 事件算 `naturalW × naturalH × 4` | ⚠️ |
207
+ | WebGL 上下文丢失 | 记录丢失、恢复与恢复后操作;作为稳定性风险,不能单独归因为 OOM | 风险项,由 enforce 控制 | 【obs】上下文事件 | ✅ |
208
+ | WASM 内存 | 峰值 ≤ profile 预算 | 3 | 【hook】`WebAssembly.Memory.prototype.grow` | ✅ |
209
+ | Worker 数量 | 同时存活 ≤ 4 | 2 | 【hook】`new Worker` / `terminate` 配对 | ✅ |
210
+ | JS 异常 | 零异常 | 4 | 已有 js-api UMD `installErrorMonitor` | ✅ |
211
+ | 网络请求异常 | 已授权请求全部正常返回 | 3 | 【hook】 | ⚠️ |
212
+ | 崩溃 / 白屏 | 不得发生 | 硬卡口 | 【runtime】失联检测 + 现有 `mini-app:resource-limit` | ✅ |
213
+
214
+ ### 4.3 体验(25)
215
+
216
+ 面向交互与视觉体验的检查项,合计 25 分。
217
+
218
+ | 指标 | 判定标准 | 权重 | 采集层 | Canvas 类 |
219
+ | --- | --- | --- | --- | --- |
220
+ | 惯性滚动 | `overflow:scroll` 元素在 iOS 下有 `-webkit-overflow-scrolling:touch` | 3 | 【scan】CSSOM | ❌ |
221
+ | 避免 `:active` 点击态 | 样式表无 `:active` selector | 3 | 【scan】 | ❌ |
222
+ | 图片比例失真 | 显示高/宽与原图高/宽偏差 ≤ 15% | 4 | 【scan】 | ❌ |
223
+ | 可点击元素响应区域 | 绑定事件元素宽高 ≥ 20px | 4 | 【scan】`getBoundingClientRect` | ❌ |
224
+ | 安全区适配 | `position:fixed` 且高度 <68px 的可交互元素在安全区内 | 3 | 【scan】 | ⚠️ |
225
+ | 不合理导航调用 | 启动 500ms 内调用 `redirectTo` 等路由 API | 3 | 【hook】jsapi bridge | ✅ |
226
+ | 触摸响应延迟 | 点击到视觉反馈 ≤ 100ms | 3 | 【hook】事件 + rAF | ✅ |
227
+ | 颜色对比度 | 大字 ≥3、常规 ≥4.5(W3C) | **0** | 【scan】 | ❌ |
228
+ | 尺寸适配 | 存在 `@media` 或视口自适应逻辑 | 2 | 【scan】 | ⚠️ |
229
+
230
+ 颜色对比度:算法不稳定,权重 0,仅作提醒项不计分。
231
+
232
+ ### 4.4 包与最佳实践(10 + 提醒项)
233
+
234
+ | 指标 | 判定标准 | 权重 | 采集层 |
235
+ | --- | --- | --- | --- |
236
+ | 解压后总体积 | 按 profile 分档线性扣分,见 §9.3 | 4 | 【包】`RawParseResult.files` 求和 |
237
+ | 单图资源体积 | ≤ 200KB,超限建议走 CDN | 3 | 【包】 |
238
+ | 未压缩 JS | 单个 JS 未压缩且 >500KB | 3 | 【包】特征检测 |
239
+ | 使用 HTTPS | 全部请求 https | 0 | 【hook】 |
240
+ | CSS 使用率 | 单个样式表未使用部分 ≤ 2KB | 0 | 【scan】`document.styleSheets` |
241
+ | 定时器回收 | 卸载后无存活 timer | 0 | 【hook】`setTimeout` / `setInterval` |
242
+ | 冗余文件 | 包内存在未被引用的资源 | 0 | 【包】+ 运行期资源访问记录取差集 |
243
+ | sourcemap 残留 | 不应包含 `.map` | 0 | 【包】 |
244
+
245
+ ### 4.5 权重汇总
246
+
247
+ | 类别 | 权重 | 说明 |
248
+ | --- | --- | --- |
249
+ | 性能 | 30 | 运行时类指标改用 Web 原生口径重建 |
250
+ | 内存与稳定性 | **35** | OOM 是主要矛盾,本方案新增 8 项 |
251
+ | 体验 | 25 | 交互与视觉类检查 |
252
+ | 包与最佳实践 | 10 | 产物不可控,静态分析权重下调但不为 0 |
253
+ | 硬卡口 | 二值 | 上下文丢失 / 崩溃 / 泄漏斜率,不参与打分 |
254
+
255
+ ## 5. 计分模型
256
+
257
+ ### 5.1 按适用项归一化 + 覆盖度闸门
258
+
259
+ ```text
260
+ 总分 = Σ(适用项得分 × 权重) / Σ(适用项权重)
261
+ 覆盖度 = Σ(适用项权重) / Σ(全部权重)
262
+
263
+ if (覆盖度 < minCoverage) → 报告标记「不可评」,不出分
264
+ ```
265
+
266
+ **不适用项既不给分也不计入分母**,而非默认满分。纯 Canvas 类小工具约 40% 的检查项不适用,
267
+ 沿用默认满分会让最危险的形态自动获得虚高分。覆盖度闸门作为第二道兜底。
268
+
269
+ ### 5.2 双层判定
270
+
271
+ 加权总分不作为准入依据,仅用于优化建议与运营分档。
272
+
273
+ | 层 | 内容 | 行为 |
274
+ | --- | --- | --- |
275
+ | 硬卡口(二值) | 峰值堆 / 显存 / 解码图片超档位预算、堆增长斜率超限、WebGL 上下文丢失、崩溃或 JS fatal、包体积超限 | 不通过直接拒绝 |
276
+ | 体验评分(100 分制) | 上述四大类加权 | 呈现给审核员与开发者,不阻断 |
277
+
278
+ ### 5.3 分档预算
279
+
280
+ 一期当前实现按 `appType` 选择 profile。1.1 期通过业务形态、玩法维度、渲染能力和审核确认选择 `evaluationProfile`(§15.2),预算数值由 Apollo 下发;单独检测到 WebGL 不触发 `game3d` 档位。
281
+
282
+ ### 5.4 内存口径:会话基线校正
283
+
284
+ `performance.memory` 读的是所在 V8 堆的总量,而非单个 iframe 独占。远程链路下 guest iframe 与
285
+ runtime iframe 同源,默认共用同一个堆,读数天然包含模拟器自身开销。
286
+
287
+ **不通过进程隔离解决,改为把底噪从判定值中减去。**
288
+
289
+ 底噪不是常数。`src/sw/service-worker-host.ts` 的 `mount()` 在把 rawFiles transfer 给 SW 之前,
290
+ 会先 `cloneMountInput()` 做一份完整深拷贝(`cloneRawFiles` 即 `bytes.slice(0)`)并常驻
291
+ `mountSnapshots`——用于 SW 进程被浏览器回收后恢复挂载。因此 **runtime 侧长期持有一份完整的解压
292
+ 后包体,底噪随包体积线性变化**,不能用固定常数近似。
293
+
294
+ 取值方式:在 **SW mount 完成之后、guest 开始加载之前**读一次 `performance.memory`。该时刻 ZIP 已
295
+ 解析、快照已落地、小工具尚未执行任何代码,读数即为本次会话的真实底噪,自动包含包体积影响。
296
+
297
+ ```text
298
+ 会话基线 baseline = mount 完成后、guest 加载前的 usedJSHeapSize
299
+ 峰值堆判定 = P95(采样序列) - baseline vs profile.peakHeapMB
300
+ 堆增长斜率 = 稳定期采样序列线性回归斜率
301
+ ```
302
+
303
+ 两点说明:
304
+
305
+ - **峰值取 P95 或上包络,不取单次 max**。`performance.memory` 呈 GC 锯齿,单次最大值受 GC 时机
306
+ 影响,会导致同一个包两次评测结果漂移,审核申诉时无法自证。
307
+ - **堆增长斜率不需要基线校正**。斜率是变化率,任何常数偏移求导后为 0;runtime 与 SW 在 guest
308
+ 运行期不做持续分配,对斜率无贡献。§4.2 中价值最高的泄漏检测项因此天然免疫该问题。
309
+
310
+ ## 6. Apollo 配置
311
+
312
+ 服务端已有通用作用域配置框架 `policy-scope.ts`:`scope { appIds, userIds, businessLines }` +
313
+ `strategy Record<string, unknown>`。体验评测策略使用独立 priority:每条 `strategy` 都是完整策略,命中多条时选择 priority 最大的一整份,不做字段合并。Server 只校验外层 JSON、scope 和 priority 结构,不感知评分字段语义。
314
+
315
+ 评测规则**新增独立 Apollo key**,复用 `parseScopedConfigPolicies`,聚合进 `POST /simulator/runtime-config`
316
+ (该接口定位即「一次获取当前 app / user / 接入平台的全部动态配置」),**不新增请求,不改动任何
317
+ 既有配置项**。
318
+
319
+ ```ts
320
+ interface AuditPolicyRule {
321
+ priority: number;
322
+ scope: {
323
+ appIds?: string[];
324
+ userIds?: string[];
325
+ businessLines?: string[];
326
+ };
327
+ strategy: AuditPolicy;
328
+ }
329
+ ```
330
+
331
+ 当前完整 Apollo 示例见 [一期评分与采集设计](./audit-score-design.codex.md#策略配置)。
332
+
333
+ ### 6.1 代码与配置的边界
334
+
335
+ | 进 Apollo(改配置,不发版) | 留在规则包(改代码,发版 + 回归) |
336
+ | --- | --- |
337
+ | 阈值数值、分档 profile | 指标如何采集、如何计算 |
338
+ | 权重、准入分线、覆盖度闸门 | 打分公式、报告结构 |
339
+ | `enforce` / `collect` 开关、指标启停 | 硬卡口判定逻辑 |
340
+ | 标签 → 探针 / 评分项映射 | 新增或删除指标项 |
341
+ | 单 app 豁免 | — |
342
+
343
+ **不得将公式做成 Apollo 中的表达式字符串**,否则等同于线上不可测试的动态代码,一次配置错误即可
344
+ 造成全量审核误杀。参数可调、逻辑不可调。
345
+
346
+ ### 6.2 已知约束
347
+
348
+ - `matchesPolicyScope` 是 **OR** 语义(三个维度任一命中即生效),无法表达「mars 业务线且某 app」。
349
+ app 级策略必须提供完整 `AuditPolicy`,并设置更大的 priority 让其整份生效。
350
+ - scope 不含「小工具类型」维度,分档只能通过 `strategy.profiles` 表达,由 runtime 按标签选取。
351
+
352
+ ## 7. 规则引擎
353
+
354
+ ```text
355
+ @skillbuilder/miniapp-audit-rules 纯函数包,零依赖
356
+ score(timeline: MetricTimeline, policy: AuditPolicy): AuditReport
357
+ ```
358
+
359
+ - **在线**:runtime 内直接调用,即时出分,无服务端往返。
360
+ - **离线**:从 APM 导出的 `miniapp_audit_sample` 时序在 Node 中用同一包的不同版本重算,用于阈值定标。
361
+
362
+ 报告必须同时落 `policyVersion` 与 `rulesVersion`,否则线上调整阈值后历史报告无法解释,申诉时
363
+ 无从对账。
364
+
365
+ ## 8. 数据采集与上报
366
+
367
+ ### 8.1 统一走 APM 事件流
368
+
369
+ `src/tracker/default-tracker.ts` 继续使用 `measurement_name = miniapp_simulator_event`,按 10 条或 10 秒
370
+ 批量发送。评测数据以扁平点位上报:
371
+
372
+ | `event_name` | 内容 | 用途 |
373
+ | --- | --- | --- |
374
+ | `miniapp_audit_session` | 会话阶段、刷新、中断和里程碑 | 队列流程与完整性 |
375
+ | `miniapp_audit_sample` | 1 Hz 原始指标快照 | 时序重建与阈值回放 |
376
+ | `miniapp_audit_paint` | FCP、LCP 候选与收口 | 首屏体验分析与评分校准 |
377
+ | `miniapp_audit_incident` | context lost、JS fatal、熔断和失联 | 硬风险分析 |
378
+ | `miniapp_audit_package` | 包体与静态能力聚合值 | 静态指标校准 |
379
+ | `miniapp_audit_result` | 三态、得分、覆盖度和派生指标 | 大盘与结果分析 |
380
+
381
+ 所有点位使用 `audit_session_id + attempt_no` 关联。timeline 不作为数组塞入单条 APM 数据,每个采样点
382
+ 独立上报;宿主 tracker 负责批量发送,Runtime 卡死后仍能冲刷已经收到的样本。完整事件契约见
383
+ [audit-data-collection.codex.md](./audit-data-collection.codex.md)。
384
+
385
+ 六类事件均由模拟器内部采集和上报,不依赖 Mars 提供人工审核结论。指标分布、风险命中和采集完整性用于筛选候选阈值;误报和漏报通过有明确复测结论的内部样本验证。
386
+
387
+ ## 9. 体积分析
388
+
389
+ ### 9.1 两把尺子:安全阀与质量线是嵌套关系,不是并列关系
390
+
391
+ `src/sw/parse-raw.ts` 中的包限制是**安全阀**而非质量度量:
392
+
393
+ ```ts
394
+ maxZipBytes → 压缩后字节,默认 50MB
395
+ maxSingleFileBytes → 解压后字节,默认 15MB
396
+ maxScriptBytes → 解压后字节,默认 15MB
397
+ // 超限行为:failLimit() → throw MiniAppSimulatorError("PACKAGE_LIMIT_EXCEEDED")
398
+ ```
399
+
400
+ 两者的定位、量级与行为完全不同:
401
+
402
+ | | package-limits(安全阀) | 评分体积线(质量度量) |
403
+ | --- | --- | --- |
404
+ | 语义 | 能不能跑 | 跑得好不好 |
405
+ | 量级 | 50MB 压缩包 / 15MB 单文件 | 4MB 解压后即开始扣分(tool 档) |
406
+ | 口径 | `maxZipBytes` 比**压缩后** | 比**解压后总体积** |
407
+ | 触发条件 | 必须离谱到威胁浏览器才拦 | 常规偏大即扣分 |
408
+ | 行为 | 二值,抛错,包加载失败 | 分档线性扣分,包正常运行 |
409
+ | 配置位置 | 既有 `package-limits` Apollo key | 新增 `auditPolicy` Apollo key |
410
+
411
+ 一个解压后 4MB 的包压缩后约 1.5MB,距 50MB 的阀门还差三十倍——**评分线远在安全阀之内,两者
412
+ 天然嵌套,正常情况下评分扣分早已发生而安全阀从未触发**。
413
+
414
+ **评分阈值不得写入 package-limits**:一旦超限包加载失败,审核员连报告都看不到,等同于评测流程
415
+ 自毁。两套配置必须完全独立。
416
+
417
+ ### 9.2 数据源可复用
418
+
419
+ `RawParseResult` 已返回 `files: Array<{ path: string; size: number }>`(路径 + 解压后字节),
420
+ 体积分析所需的全部信息(解压后总体积、类型分布、大图清单、未压缩 JS 检测)均可由此计算,
421
+ **零额外解包开销**。
422
+
423
+ 注意 `maxZipBytes` 比较的是压缩后体积,而质量口径应看解压后总体积(与内存和加载相关),两者
424
+ 语义不同,需各自独立取数。
425
+
426
+ ### 9.3 体积评分采用分档线性扣分
427
+
428
+ 小工具形态跨度过大,二值判定会把 3D 游戏一刀切死,因此改为按 profile 分档线性扣分:
429
+
430
+ ```text
431
+ size <= idealMB → 100 分
432
+ size <= acceptMB → 100 → 60 线性插值
433
+ size <= maxMB → 60 → 0 线性插值
434
+ size > maxMB → 0 分
435
+ ```
436
+
437
+ | profile | idealMB | acceptMB | maxMB |
438
+ | --- | --- | --- | --- |
439
+ | tool | 4 | 10 | 20 |
440
+ | game2d | 10 | 25 | 40 |
441
+ | game3d | 20 | 50 | 80 |
442
+
443
+ 上述数值为一期占位值,`enforce: false` 期间仅参与出分不参与拒绝,两周后按真实分布校准(§13)。
444
+ 即便是 game3d 档的 maxMB(80MB 解压后),压缩后通常仍远低于 package-limits 的 50MB 压缩包阀门。
445
+
446
+ ## 10. 评分面板 UI
447
+
448
+ ### 10.1 运行位置:全部在 runtime iframe 内
449
+
450
+ 远程模式下的实际层级(`src/remote/runtime.ts:1181-1203` + `src/host/shadow-host.ts`):
451
+
452
+ ```text
453
+ 宿主页(Mars / creator)
454
+ │ 只有薄 SDK remote-client.js:建 iframe + MessageChannel,不创建任何可视元素
455
+ └─ iframe#1 runtime(simulator-server origin)
456
+ html, body { width:100%; height:100%; margin:0; overflow:clip }
457
+ └ <main id="miniapp-simulator-runtime-root"> position:fixed; inset:0
458
+ └ div.miniapp-simulator-host position:absolute; inset:0
459
+ └ #shadow-root
460
+ └ div.dms-bounds absolute; inset:0; overflow:hidden
461
+ └ div.dms-stage stageFit 在此 translate + scale
462
+ └ 手机壳 / 状态栏 / rednote 壳 / 半屏 sheet
463
+ └ iframe#2 guest(SW 虚拟 URL,同 origin)
464
+ └ 小工具本体
465
+ ```
466
+
467
+ 手机壳、状态栏、rednote 壳层、toast、权限弹窗、devtools 面板**全部是 runtime iframe 内的 DOM**,
468
+ 宿主页一个像素都不画。评分面板同样放在这一层,因此宿主页既不会被样式污染,也无需任何接入动作。
469
+
470
+ ### 10.2 布局:手机左、面板右,一个整体
471
+
472
+ 面板不能放进 `bounds`——`bounds` 是 stageFit 的居中与缩放参照系,往里加同级元素会改变手机的基准。
473
+ 正确位置是 `bounds` 之外、`shadowRoot` 之内新增一层 flex:
474
+
475
+ ```text
476
+ #shadow-root
477
+ └ div.dms-layout display:flex
478
+ ├ div.dms-bounds flex:1; position:relative; overflow:hidden
479
+ │ └ div.dms-stage ← 手机 stageFit 自动缩放装入剩余宽度
480
+ └ 评分面板 flex:none; width:380px
481
+ ```
482
+
483
+ `src/core/options.ts:210-214` 说明 **stageFit 默认开启且为「100% 自适应撑满 mount 区域」**,
484
+ 因此缩小 `bounds` 后手机会自动缩放适配,`stageFit` 的居中与缩放逻辑一行都不用改。
485
+
486
+ 面板不存在时 flex 容器只有 `bounds` 一个子项,布局效果与现状的 `inset:0` 等价,不维护两套分支。
487
+
488
+ ### 10.3 宿主零接入
489
+
490
+ 消费方只需升级 npm 包,**没有 option、没有挂载点、没有配置项**。模拟器与评分面板是一个整体,
491
+ 由本包完整管理。
492
+
493
+ 默认行为:
494
+
495
+ | 场景 | 行为 |
496
+ | --- | --- |
497
+ | 非 mars 业务线 | 不加载插件,面板代码与 Vue 均不下载,行为与升级前完全一致 |
498
+ | mars 业务线、未开始评测 | 面板折叠(宽度 0),手机占满容器 |
499
+ | 点击「开始评测」 | 面板展开 380px,手机同步平滑缩放;与 reload 是同一个动作 |
500
+
501
+ 面板宽度固定 380px,不做拖拽——拖拽需要持久化状态与边界处理,收益不足。
502
+
503
+ 面板与手机共享 runtime iframe 的宽度,由接入方在外层挂载点自行调整样式即可,本包不感知也不需要
504
+ 接入方改动代码。
505
+
506
+ ### 10.4 技术选型:Vue defineCustomElement
507
+
508
+ 面板用 Vue 3 SFC + `defineCustomElement` 实现,**Vue 只进插件产物,不进 core**。
509
+
510
+ 选型依据(对照本包现有手写 Web Component 的实测行数):
511
+
512
+ | 现有文件 | 行数 | 做的事 |
513
+ | --- | --- | --- |
514
+ | `native-ui/devtools/LogsView.ts` | 146 | 工具条 + **一层扁平列表** + 增量追加 |
515
+ | `native-ui/devtools/CapabilitiesView.ts` | 264 | 能力勾选列表 + 应用按钮 |
516
+
517
+ 评分面板需要:4 种顶层状态(空闲/评测中/报告/不可评)、5 个分数环、约 30 个检查项的
518
+ **三层嵌套折叠**、以及评测期间每秒更新的 live 指标。手写估 900~1300 行,Vue SFC 估 400~600 行。
519
+
520
+ 手写方案有两个必然踩到的坑:
521
+
522
+ - **每秒刷新与折叠状态冲突**。`LogsView` 之所以简单是因为它只追加不更新;评分面板实时态要更新
523
+ 数十个字段,全量 `innerHTML` 重刷会丢失折叠状态与滚动位置,手动增量更新等同于自写劣质 diff。
524
+ - **三层嵌套数据 + 三层展开状态**。`report.categories[].items[].detail` 靠 `data-*` 属性做事件
525
+ 委托反查,改一次结构要同步改模板拼接、事件委托、状态存储三处。
526
+
527
+ 构建约束:
528
+
529
+ - 必须 **runtime-only(预编译 SFC)**,不得使用运行时模板编译——后者依赖 `new Function`,会撞
530
+ CSP 的 `unsafe-eval`。
531
+ - `@vitejs/plugin-vue` 开启 `features: { customElement: true }`(或 `*.ce.vue` 命名约定),
532
+ SFC 的 `<style>` 会自动注入 shadow root。
533
+ - `customElements.define` 需做幂等 guard(重复 define 会抛错),且自定义元素一经注册无法撤销。
534
+ - 报告对象经 **property** 传入(`el.report = reportObject`),attribute 只能传字符串。
535
+
536
+ core 的 `vite.config.ts` 无需改动,插件是独立包、独立构建配置、自带 Vue 依赖。Vue 3 runtime-only
537
+ 约 34KB gzip,仅在 mars 业务线点击评测时加载一次。
538
+
539
+ ### 10.5 面板结构
540
+
541
+ ```text
542
+ ┌─ 体验评分 ────────────────── 重新检测 · 导出 JSON ─┐
543
+ │ profile: game3d 覆盖度 78% 采集 3:24 │
544
+ │ policy 2026-09-29.1 · rules 1.0.2 ●0-59 ●60-89 ●90-100
545
+ │ │
546
+ │ ( 72 ) ( 65 ) ( 58 ) ( 88 ) ( 90 ) │
547
+ │ 总分 性能 内存稳定 体验 包 │
548
+ │ │
549
+ │ ⛔ 硬卡口未通过:堆增长 14MB/min(上限 8) │
550
+ │ │
551
+ │ 内存与稳定性 58 │
552
+ │ 没通过 2 项 ▾ │
553
+ │ 1 堆持续增长,疑似泄漏 14MB/min,上限 8 │
554
+ │ ▸ 说明与优化建议 │
555
+ │ 2 估算显存超预算 412MB,上限 384 │
556
+ │ 已通过 6 项 ▸ │
557
+ │ │
558
+ │ 不适用 4 项(未纳入计分)▸ │
559
+ └─────────────────────────────────────────────────────┘
560
+ ```
561
+
562
+ 两处刻意偏离常见做法:
563
+
564
+ - **硬卡口单独置顶**,不混入分数。总分 72 但硬卡口未过即为拒绝(§5.2)。
565
+ - **「不适用 N 项」必须显式列出**并配合覆盖度百分比。若把不适用项按默认满分处理并隐藏,审核员
566
+ 就无从知晓评测范围;本方案要让「这个工具有 4 项没法评」对人可见,否则覆盖度闸门是黑盒。
567
+
568
+ 评测进行中:分数环位置改为进度条 + live 堆占用 / FPS 数字。
569
+
570
+ **报告文字必须可选中、可复制**(项名、实测值、上限值)——「把超限数值贴进拒绝理由」是审核高频
571
+ 操作。这一点与模拟器现有的 PC 交互屏蔽(`utils/simulator-pc-guard.ts`,禁用文本圈选以贴近真机
572
+ 体感)冲突,需将该屏蔽的作用域限定在手机区域,实现见技术改造方案 §4.2。
573
+
574
+ ### 10.6 后续演进:native-ui 迁移 Vue(不在本期)
575
+
576
+ `src/native-ui/` 现为手写 Web Component,合计约 4700 行,其中三个壳层占 2212 行
577
+ (`xhs-note-detail-shell` 932 / `xhs-note-half-sheet` 661 / `xhs-profile-shell` 619)。迁移到
578
+ Vue 可观预期收益(估 900~1100 行),方向已认可,但**不并入本期**,原因:
579
+
580
+ - 会把 Vue 从「插件可选依赖」变成 **core 硬依赖**——`remote` 构建是 `inlineDynamicImports` 单文件,
581
+ Vue 将进入 `runtime.js`,所有会话(含 creator)都需下载。
582
+ - 本地模式(`createMiniAppSimulator`)下 native-ui 直接运行在消费方页面内,消费方包括 React 应用
583
+ `apps/mini-app-review-simulator`。互操作本身没有问题(自定义元素标准),但影响面需单独评估。
584
+ - `half-sheet-drag.ts`(290 行手势)与 `chrome-tone.ts`(114 行状态栏色调)是精调过的存量代码,
585
+ 重写易在细节上静默退化。
586
+
587
+ 本期评分面板即为试点,完成后可验证三件事,再据此决定迁移:Shadow DOM 下的样式注入是否干净、
588
+ 预编译 SFC 在 runtime iframe 的 CSP 下是否可用、打包后的实际体积增量。构建配置届时可直接复用。
589
+
590
+ ## 11. 协议变更
591
+
592
+ 远程协议由 v8 升至 v9(单一来源 `src/remote/protocol-version.json`)。
593
+
594
+ ```ts
595
+ // capability
596
+ REMOTE_SIMULATOR_CAPABILITIES += "audit"
597
+
598
+ // method
599
+ "audit.start" // { autoPilot?: boolean; durationMs?: number } —— 触发 reload
600
+ "audit.stop"
601
+ "audit.getReport"
602
+ "audit.exportRaw" // 导出原始指标时序 JSON
603
+
604
+ // event
605
+ "auditProgress" // { phase, elapsedMs, live: { heapMB, fps, ... } }
606
+ "auditReport" // 完整报告
607
+ ```
608
+
609
+ `RemoteSimulatorSerializableOptions` 无需改动:插件选择由 connect 消息的 `businessLine` 结合服务端
610
+ catalog 决定,不经宿主传递函数。
611
+
612
+ ## 12. 安全 hook 规范
613
+
614
+ 所有 hook 必须保持行为透明,不得改变被测小工具的任何可观察行为。
615
+
616
+ ```ts
617
+ function safeHook(target, key, onCall) {
618
+ const original = target[key];
619
+ if (!original || original.__probed) return; // 幂等,防重复注入叠加
620
+ const wrapped = function (...args) {
621
+ try { onCall(this, args); } catch { /* 统计绝不影响业务 */ }
622
+ return original.apply(this, args); // 原样透传 this / 参数 / 返回值 / 异常
623
+ };
624
+ Object.defineProperty(wrapped, "name", { value: original.name });
625
+ Object.defineProperty(wrapped, "length", { value: original.length });
626
+ wrapped.toString = () => original.toString();
627
+ wrapped.__probed = true;
628
+ target[key] = wrapped; // 保持原描述符
629
+ }
630
+ ```
631
+
632
+ 强制要求:
633
+
634
+ - **异常必须原样抛出**,不得吞掉。一旦吞异常,被测行为即被改变,测量结果失去意义,更可能把本应
635
+ 失败的 case 测成通过。
636
+ - **保留 `name` / `length` / `toString`**。Three.js、Pixi 等库存在能力嗅探,签名改变会导致其走入
637
+ 不同分支,测到的不是真实路径。
638
+ - **统计逻辑必须 O(1) 且零分配**。`texImage2D` 在 3D 游戏中每帧可达上千次调用,逐次记录会使探针
639
+ 自身成为 OOM 元凶——测内存的组件把内存吃爆,在审核链路上是致命的。只做累加计数器 + 定时快照。
640
+ - **避免二次开销放大**。`naturalWidth` 等高频 getter 不做 hook,改为在 `load` 事件中计算一次。
641
+ - **注入顺序固定**:document-start,早于业务 bundle,且排在 seal IIFE 之前。
642
+ - **可关闭**:生产端默认不注入,仅在评测模式与灰度校准包中开启;开关需支持人群灰度,不得编译期
643
+ 写死。
644
+
645
+ ## 13. 分期
646
+
647
+ ```text
648
+ ① 新增 Apollo key + parseAuditPolicy + 聚合进 runtime-config ← 与探针解耦,可先行
649
+ ② APM 评测事件流 + 会话关联字段 ← 采集底座,必须早于出分
650
+ ③ 打标器 + 探针(安全 hook)+ 体积分析(消费 RawParseResult.files)
651
+ ④ 规则引擎(纯函数包)
652
+ ⑤ shadow-host 分栏改造 + Vue 评分面板(§10)
653
+ ⑥ enforce=false 上线,报告仅供审核员人工参考,累积两周数据
654
+ ⑦ 拉取分布、对齐线上已 OOM 样本定阈值 → 调整 Apollo → enforce=true ← 全程不发版
655
+ ```
656
+
657
+ ①② 是「先采集」的实质,顺序不可颠倒:先出分后建采集等同于把无数据支撑的阈值先行固化。
658
+
659
+ ⑤ 的 `shadow-host.ts` 分栏改造影响所有消费方的布局路径,建议与评测功能解耦、单独提交并回归
660
+ 现有三个接入方(creator IDE、playground、mini-app-review-simulator)。
661
+
662
+ ⑦ 之后开启卡口、调整阈值、回滚均为 Apollo 配置操作,秒级生效。
663
+
664
+ 一期不做自动拒绝,只出分并提示,在保持审核口径不变的前提下顺带完成定标数据积累。
665
+
666
+ ## 14. 待验证风险
667
+
668
+ ### 14.1 auto-pilot 覆盖率
669
+
670
+ 随机点击难以进入游戏主循环(多数 3D 游戏需先通过开始菜单)。一期依赖审核员手动操作 + 持续采集
671
+ 兜底;后续可能需要开发者提供压测脚本或录制交互序列,会增加接入成本。
672
+
673
+ ### 14.2 审核确定性
674
+
675
+ 同一包两次评测得分不同会引发申诉。需固定机型档位、网络节流参数与 auto-pilot 随机种子,内存峰值
676
+ 按 §5.4 取 P95 而非单次最大值。
677
+
678
+ ### 14.3 版本兼容矩阵
679
+
680
+ 插件动态加载引入「runtime 版本 × 插件版本」矩阵,`protocolVersion` 校验需与 runtime-manifest
681
+ 保持同等严格度。
682
+
683
+ ### 14.4 阈值全部为占位值
684
+
685
+ §6 profiles 中的内存、显存、体积数值均为一期占位,无线上数据支撑。`enforce: false` 期间仅参与
686
+ 出分,不得据此拒绝任何包。真实阈值须按 §13 第 ⑦ 步由采样分布校准后再写入 Apollo。
687
+
688
+ ## 15. 1.1 期:游戏识别与评测增强
689
+
690
+ ### 15.1 交付范围与状态
691
+
692
+ 1.1 期沿用 Mars 远程模拟器、document-start 探针、浏览器内规则计算、常驻审核面板和 APM 事件流。目标是让报告回答四个问题:内容属于什么形态、审核员体验了什么玩法、哪个阶段出现了风险、哪些证据足以支持判断。
693
+
694
+ | 能力 | 当前实现 | 1.1 期交付 |
695
+ | --- | --- | --- |
696
+ | 识别 | 文件名引擎指纹、`tool/game2d/game3d/video`、运行期资源补标 | 业务形态、玩法维度、渲染能力、引擎、证据与置信等级分别记录 |
697
+ | 使用证据 | 有效时长、样本数、操作数与首次操作时间 | 游戏阶段、有效玩法时长、连续/离散操作、主要玩法体验记录 |
698
+ | 流畅度 | 探针独立 rAF 节拍、最长 Long Task | 业务循环/绘制提交节奏、间隔分位数、长间隔比例、主循环疑似冻结 |
699
+ | 资源 | 图形资源水位、Canvas、图片、WASM、Worker | 绘制调用和上传强度、分阶段水位、重复玩法后资源变化 |
700
+ | 稳定性 | JS 异常、context lost、失联、卡死与熔断 | 事故发生阶段、context restored、恢复后提交和人工操作结果 |
701
+ | 数据 | APM 会话/采样/首屏/事故/包/结果 | 识别与玩法字段、采集来源、专项质量状态及校准所需分层信息 |
702
+
703
+ 本节为待实施方案,表中增强能力需按 §15.8 验收。新增指标默认观察、权重为 0、`enforce: false`;现有已采到的异常和硬风险继续完整展示。
704
+
705
+ ### 15.2 识别模型与证据
706
+
707
+ 建议的数据模型:
708
+
709
+ ```ts
710
+ interface GameIdentification {
711
+ experienceType: "tool" | "game" | "video" | "interactive-content" | "unknown";
712
+ gameplayDimension: "2d" | "3d" | "mixed" | "unknown";
713
+ renderModes: Array<"dom" | "canvas2d" | "webgl1" | "webgl2" | "webgpu">;
714
+ engineCandidates: Array<{
715
+ name: "pixi" | "phaser" | "cocos" | "laya" | "three" | "babylon" | "playcanvas" | "unity" | "unknown";
716
+ version?: string;
717
+ confidence: "high" | "medium" | "low";
718
+ evidenceCodes: string[];
719
+ }>;
720
+ identificationVersion: string;
721
+ scanComplete: boolean;
722
+ reviewerConfirmed: boolean;
723
+ evaluationProfile: "tool" | "game2d" | "game3d" | "video" | "unconfirmed";
724
+ }
725
+ ```
726
+
727
+ 置信等级表示规则证据强弱,不是统计概率。多引擎包保留候选,不用最后一个命中覆盖所有结果。无法可靠取得的版本留空,不从任意数字猜版本。
728
+
729
+ | 来源 | 用法与边界 |
730
+ | --- | --- |
731
+ | 申报和人工确认 | 提供业务形态、2D/3D 玩法、主要玩法体验与是否需要持续渲染;与技术证据冲突时保留双方来源 |
732
+ | 文件名和资源 | Unity loader/framework/data、`.wasm`、`.glb/.gltf/.ktx2/.basis/.dds/.pvr`、shader 等作为候选证据;资源存在不证明它已加载或属于游戏 |
733
+ | Bundle 内容 | 识别引擎标识、版本模式及组合签名,支持 hash 文件名;普通 `pixi`、`three` 或 `game` 子串不直接定类 |
734
+ | 运行观测 | 成功创建的 context 类型、绘制与资源调用确认实际使用的能力;访问已有数据属性时避免触发 getter,不执行包内代码识别引擎 |
735
+
736
+ 扫描种子预算建议为总计 8MiB 文本、单文件 1MiB、最多 128 个候选文件,采用有界分段读取并记录截断情况;预算需实测后确定。命中规则保留证据码与局部定位,APM 只上报有界证据码和聚合结果,不传 bundle 原文或任意文件清单。
737
+
738
+ 预算选择规则:已确认的 2D 游戏使用 `game2d`,已确认的 3D 游戏使用 `game3d`;混合玩法记录确认依据。WebGL 工具仍保持工具形态,并启用已观测能力对应的资源指标。未确认形态不出游戏档位通过结论,保留通用指标、专项 N/A 原因和风险提示。
739
+
740
+ 必须包含四类识别对照:使用 WebGL 的 2D 游戏、使用 Three.js 的非游戏、原生 Canvas2D 游戏、引擎完全打入 hash bundle 的游戏。`魂斗罗-性能`、`倒车模拟器`、`生化危机`、`像素旅行游戏` 可作为现有候选样本;最终标签由人工核验,不以包名定类。
741
+
742
+ ### 15.3 游戏审核 SOP 与有效性
743
+
744
+ 审核员平均约一分钟处理一个队列项。游戏评测随审核操作采集,由系统自动累积证据,审核员只确认关键阶段:
745
+
746
+ ```text
747
+ 进入队列项,完整重载模拟器
748
+ → 开始审核,探针准备后加载内容
749
+ → loading / menu:完成加载与进入玩法
750
+ → playing:确认进入主循环,实际移动、攻击、选择或完成核心任务
751
+ → transition:条件允许时完成换关、重试或返回后再次进入
752
+ → playing:继续主要玩法
753
+ → 检查主要玩法的操作响应,停止评测
754
+ → 查看风险与专项证据,保存结论,完整重载下一包
755
+ ```
756
+
757
+ 阶段集合为 `loading/menu/playing/paused/transition/finished/unknown`。人工确认是业务语义证据,运行观测提供旁证;主循环不能仅根据“开始按钮被点击”自动判定。阶段回切全部保留在同一时间线,事故附带当时阶段。
758
+
759
+ 游戏有效性种子条件为:通用采样和覆盖门槛满足、核心玩法已确认、前台 `playing` 累计至少 30 秒、主要玩法已实际体验。持续动作类还需持续渲染证据;回合制/棋类/谜题按离散操作和人工完成证据判定,等待状态不要求持续出帧。
760
+
761
+ 操作模型分为 `continuous` 和 `discrete`。离散操作可先观察至少 5 次的门槛;长按移动、拖动和摇杆按持续操作时长与操作类型记录,不能要求每次长按产生多个点击。短流程、单操作玩法和特殊游戏通过完整策略配置适合自己的门槛。数据只保留类型、次数和时长,不保存键值、输入内容和坐标。
762
+
763
+ 换关或再次进入用于采资源变化,仅在玩法支持时执行;不是每包的强制出分条件。复杂或高风险包继续采到会话上限,默认最长总时钟 120 秒包含加载;到时仍不满足游戏证据的报告为 `unscorable`。需要更长操作的样本保留触发原因进入二期复测。
764
+
765
+ ### 15.4 游戏专项指标与准确口径
766
+
767
+ | 指标 | 口径 | 首批用途 |
768
+ | --- | --- | --- |
769
+ | 首次绘制提交 / 核心玩法到达时间 | 从内容开始加载到首次已观测提交、到人工确认主循环;两者分别记录 | 定位加载和玩法进入成本 |
770
+ | 页面节拍 | 探针自身 rAF 回调频率 | 页面调度旁证,权重 0 |
771
+ | 业务 rAF 节奏 | 同一帧时间戳的业务回调去重,排除探针循环 | 业务循环节奏,不能等同画面帧率 |
772
+ | 绘制提交节奏 | 已确认 playing 阶段,按可识别帧窗口聚合 WebGL/Canvas 提交;记录来源及覆盖范围 | 提交频率、间隔 P50/P95/P99 和大于 50/100ms 的间隔比例 |
773
+ | 疑似冻结 | 仅在前台、持续动作玩法、预期继续渲染时记录最长无提交间隔 | 结合长任务、事故和人工操作结果复核 |
774
+ | 主线程阻塞 | Long Task 次数、最长时长、阻塞时长总和;支持时增加 LoAF | 分阶段定位卡顿,不支持时为 N/A |
775
+ | 图形负载 | draw 调用/s、每已识别帧 draw 调用、纹理和 Buffer 逻辑上传字节/s | 解释渲染负担,不以单一 draw 数拒绝 |
776
+ | 资源变化 | 纹理/Buffer/Canvas 水位按阶段分组,重复进入同一玩法后的稳定窗口差值 | 增长风险线索,不直接判定泄漏 |
777
+ | 交互提交间隔 | guest 输入到随后第一次观测绘制提交的间隔 | 流程旁证,不代表视觉反馈延迟或 input-to-photon |
778
+ | 上下文恢复 | 丢失/恢复次数、间隔、恢复后绘制是否继续、人工操作是否成功 | 区分恢复成功、恢复失败和证据不足 |
779
+ | 加载错误 | fetch/XHR 未完成数、请求失败、资源加载错误、超时及取消原因 | 定位玩法不可达和缺资源;取消与失败分开 |
780
+
781
+ **帧指标只描述观测到的业务调度或绘制提交。** JavaScript 侧无法仅凭 rAF 或 draw 调用证明屏幕实际呈现了新帧。rAF 回调属于浏览器动画帧调度,同帧可有多个回调,需要去重。[HTML 动画帧规范](https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#animation-frames)
782
+
783
+ `frameSource` 必填,可取 `raf-heartbeat/business-raf/draw-submit/engine-adapter/unavailable`。WebGL/Canvas 调用不是一调用一帧;采用适配器或有界帧窗口归组,无法可靠归组时只报调用/s,帧项为 N/A。一个游戏多个 canvas、多个 rAF 循环不能重复累计成虚高 FPS。
784
+
785
+ 页面隐藏、暂停、加载和场景切换不进入玩法帧分布;场景切换耗时单独展示。50/100ms 是长间隔观察分桶,并非统一拒绝线;30 FPS 目标游戏和 60/120Hz 环境必须分开分析,未确认目标时不生成掉帧率或达标结论。
786
+
787
+ LoAF 通过浏览器能力检测后订阅,用于补充长渲染周期及阻塞证据,不取代不支持环境下的 Long Task 和提交间隔观测。[W3C Long Animation Frames](https://w3c.github.io/long-animation-frames/)
788
+
789
+ 资源计数和上传字节是逻辑估算,不等于驱动显存或真实 GPU 耗时。资源未回落也可能是合理缓存,需重复路径与健康样本对照。context lost 保留稳定性风险,但不能单独认定 OOM;context restored 后资源需要由业务重建。探针只监听,不调用 `preventDefault()`、不触发强制丢失。[WebGL 上下文丢失与恢复规范](https://registry.khronos.org/webgl/specs/latest/1.0/)
790
+
791
+ Worker/OffscreenCanvas 内调用、WebGPU 和未覆盖扩展必须标记覆盖边界。未覆盖项不能写成 0、正常或无风险;主线程上的 Worker 对象计数不意味着 Worker 内绘制已被采到。
792
+
793
+ ### 15.5 采集实现与报告
794
+
795
+ 实现沿用 `packages/miniapp-audit-plugin`:`collector/package-scan.ts` 接收有界文本证据,`probe/probe.impl.ts` 采集 context 类型、循环/提交与分阶段计数,`collector/session.ts` 保存玩法阶段与使用证据,`rules/` 派生专项指标,`panel/` 展示证据。跨 Runtime/宿主的序列化契约及 `remote/audit-telemetry.ts` 同步扩展,发布时校验实际能力和采集版本。
796
+
797
+ 高频绘制 hook 只做有界计数和更新时间戳,聚合后按 1 Hz 发快照;不逐 draw 发消息,不调用同步 GPU 查询或读取像素。rAF 包装保持回调实参、`this`、取消 ID 和异常语义,探针专用循环使用原生引用并从业务统计排除。所有统计失败均隔离,探针状态在会话结束或整页重载时释放。
798
+
799
+ fetch/XHR 的 pending 和失败状态需真正接线;字段未接线时明确为 N/A。避免为了观测改变 Promise 的拒绝处理或业务异常流。资源错误、HTTP 失败和脚本异常分开归类,同一事故去重,不用一个失败同时扣多项分。
800
+
801
+ 报告继续采用 `scored/unscorable/loadFailed` 三态,增加 `gameQuality` 与各指标的 `available/partial/unavailable` 采集状态。`gameQuality` 至少包括有效玩法时长、操作证据、阶段覆盖、渲染覆盖、主要玩法人工确认和不足原因。玩法有效性是游戏整体出分条件;个别专项数据缺失则该项 N/A,并按适用权重计算覆盖,不能补满分。
802
+
803
+ 面板按“识别与依据 → 是否有效体验 → 硬风险 → 玩法流畅度 → 图形资源 → 不可测范围”呈现。事故显示发生在菜单、战斗、切关或恢复等阶段;总分不能隐藏风险。场景名称只来自有界里程碑或人工选择,不抓取页面任意文本。
804
+
805
+ 报告示例结构(以下为展示示例,非实际测量):
806
+
807
+ ```text
808
+ 形态:2D 游戏;引擎:已确认候选;渲染:WebGL2
809
+ 使用:前台玩法 46 秒;离散操作 15 次;主要玩法已体验
810
+ 采集:draw-submit;主线程覆盖;Worker 内渲染未覆盖
811
+ 流畅度:提交间隔 P95 42ms;>100ms 间隔比例 3%
812
+ 资源:再次进入同一玩法后纹理稳定水位 +12MB,待复核
813
+ 稳定性:playing 阶段 context lost;未观察到恢复
814
+ 结论:需复核;附分项来源与质量原因
815
+ ```
816
+
817
+ ### 15.6 APM 与 Apollo
818
+
819
+ 复用 §8 的 APM 六类事件。所有事件关联 `audit_session_id + attempt_no`,增加 `audit_feature_version` 与 `identification_version`;不同指标来源、浏览器环境、策略版本、玩法和目标刷新率分组分析。
820
+
821
+ | 事件 | 新增字段 |
822
+ | --- | --- |
823
+ | package / result | `experience_type`、`gameplay_dimension`、`engine`、可确认的 `engine_version`、`engine_confidence`、`identification_evidence_codes`、`scan_complete`、`evaluation_profile` |
824
+ | session | `game_phase`、`phase_source`、`milestone_code`、`interaction_mode`、主要玩法确认时间 |
825
+ | sample | `game_phase`、`frame_source`、`frame_sample_count`、`draw_calls`、`texture_upload_bytes`、`buffer_upload_bytes`、`long_task_blocking_ms`、`net_pending` 与各采集能力标记 |
826
+ | incident | `game_phase`、`context_id`、`incident_code`、恢复间隔及事故归属来源 |
827
+ | result | `active_play_ms`、`gameplay_interaction_count`、`hold_interaction_ms`、`core_flow_completed`、提交间隔分位数、长间隔数与分母、疑似冻结、场景资源变化及 N/A 原因码 |
828
+
829
+ APM 的 `measurement_data` 只放数字,包括布尔 0/1;阶段、来源、引擎与原因码进入字符串维度。快照只传扁平标量,长间隔和总间隔数量都上报,便于重算比例;不传逐帧数组、代码、输入文本或坐标。重复玩法差值来自同源同场景锚点,不同场景不强行计算。
830
+
831
+ Apollo 沿用 `miniappSimulatorAuditPolicies`,每条命中策略整份覆盖本地默认。下面展示完整 `strategy` 中拟新增的 `gameEnhancement` 段;发布配置时应把它与通用字段一起组成完整策略。字段为方案设计,需完成代码接线后生效。
832
+
833
+ ```json
834
+ "gameEnhancement": {
835
+ "enabled": false,
836
+ "mode": "observe",
837
+ "minActivePlayMs": 30000,
838
+ "minDiscreteInteractions": 5,
839
+ "minContinuousInteractionMs": 5000,
840
+ "requireCoreFlowConfirmed": true,
841
+ "requireSceneTransition": false,
842
+ "collectDrawCalls": true,
843
+ "collectUploadBytes": true,
844
+ "collectLongAnimationFrames": true,
845
+ "collectInputToSubmit": false,
846
+ "longGapBucketsMs": [50, 100],
847
+ "freezeObservationMs": 1000,
848
+ "scoreWeights": {
849
+ "gameFramePacing": 0,
850
+ "gameUploadPressure": 0,
851
+ "gameSceneRetention": 0,
852
+ "gameInputToSubmit": 0
853
+ }
854
+ }
855
+ ```
856
+
857
+ 30 秒、5 次、5 秒持续操作、1000ms 疑似冻结是首轮观察参数,需用真实玩法和评测会话耗时校准,不作为已确定的准入标准。阈值、权重、游戏证据门槛和采集开关可配置;识别算法、指标公式、帧归组规则和事件含义由代码版本固定。新增专项评分先经样本回放验证,不自动给总分增加权重。
858
+
859
+ ### 15.7 实施优先级
860
+
861
+ | 批次 | 内容 | 验收结果 |
862
+ | --- | --- | --- |
863
+ | P0 | 业务形态与渲染能力拆分、有界 bundle 指纹、识别来源;玩法阶段和有效使用证据 | 2D WebGL 不被推断成 3D,非游戏不因引擎误判;菜单空等待不构成游戏有效体验 |
864
+ | P1 | 主循环提交节奏、长间隔/阻塞、context 恢复、实际网络状态;APM/面板/Apollo | 同一个玩法阶段能串联操作、卡顿、资源和事故,缺失项如实 N/A |
865
+ | P2 | 绘制/上传强度、重复玩法资源变化、可选输入提交间隔与引擎适配器 | 健康/风险样本具有判别力,探针开销和评分权重通过校准 |
866
+
867
+ 每批均通过真实模拟器加载、操作、停止、完整刷新、再加载下一包验证。二期 Electron 提供受控重复操作和 GC 后内存复测;移动端 OOM、进程内存、GPU 真实耗时与设备压力仍需要相应运行环境的数据。
868
+
869
+ ### 15.8 验收与校准
870
+
871
+ - 识别集覆盖 hash bundle、自定义 Canvas2D、2D WebGL、3D 非游戏、纯 DOM 游戏、混合/未知引擎;人工标注后统计误分类和未知率。现有 20 包可复用,缺少的类别补专门 fixture。
872
+ - 在真实远程模拟器验证 30/60 FPS 控制样本、多 rAF、多 canvas、暂停、后台、回合等待、场景切换、Worker 绘制;每个来源的计数与控制样本对应,无法支持的路径显示 N/A。
873
+ - 在主循环注入已知主线程阻塞、重复资源分配/释放、正常缓存、JS 错误和受控 context loss/restored;能显示发生阶段与恢复结果,受控失去上下文不被归因为已证实 OOM。
874
+ - 所有游戏完成可见核心操作再评测;同包同流程至少复跑 3 次,形成健康/风险对照。连续 20 次队列项切换无串包、迟到事故或前一包报告污染。
875
+ - 同样本对比探针开/关,测量新增 hook 的 CPU、提交节奏和堆开销;首轮以稳定控制样本开销不超过 5% 为工程观察目标,达不到时降低采集强度并重新验证。
876
+ - 抽取实际 APM 请求验证字段、0/1 布尔值、事故去重、阶段关联、缺失原因与分布重算;评分回放只消费本次真实可用的指标。
877
+ - 记录评测会话耗时 P50/P90、游戏有效评测率、玩法覆盖、分数分布和风险命中;已知风险召回与健康样本误报通过内部受控复测验证。这些数据用于校准证据门槛与指标权重。
878
+
879
+ ## 16. 参考
880
+
881
+ - 《代码质量分析》(`6a4a836d339aec68bf7659c037b2b775`)
882
+ - 《体验评分方案》(`2a3362798832619e983ffa655662e2fe`)
883
+ - 《模拟器插件方案》(`c47714b1d578c67184852598a9889dfe`)
884
+ - 《模拟器 V2 架构方案》(`cef726f3f96e50d206647721f678c61d`)
885
+ - 本仓库:`docs/remote-architecture.md`、`docs/remote-load-observability.md`、`docs/tracker.md`