@uwa4d/openapi-mcp 0.2.0-beta.0 → 0.2.0-beta.10
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.
- package/README.md +105 -9
- package/dist/annotate-dashboard.d.ts +48 -0
- package/dist/annotate-dashboard.js +413 -0
- package/dist/cli.js +29 -6
- package/dist/client.js +4 -13
- package/dist/composite/helpers.d.ts +19 -0
- package/dist/composite/helpers.js +90 -0
- package/dist/composite/index.d.ts +25 -0
- package/dist/composite/index.js +46 -0
- package/dist/composite/name-pattern.d.ts +8 -0
- package/dist/composite/name-pattern.js +25 -0
- package/dist/composite/overview-view.d.ts +33 -0
- package/dist/composite/overview-view.js +373 -0
- package/dist/composite/report-diagnosis.d.ts +2 -0
- package/dist/composite/report-diagnosis.js +347 -0
- package/dist/composite/route-overview.d.ts +72 -0
- package/dist/composite/route-overview.js +233 -0
- package/dist/composite/stack-agg.d.ts +77 -0
- package/dist/composite/stack-agg.js +409 -0
- package/dist/composite/stack-test-mode.d.ts +33 -0
- package/dist/composite/stack-test-mode.js +60 -0
- package/dist/composite/top-functions.d.ts +21 -0
- package/dist/composite/top-functions.js +178 -0
- package/dist/composite/top-resources.d.ts +12 -0
- package/dist/composite/top-resources.js +263 -0
- package/dist/composite/types.d.ts +25 -0
- package/dist/composite/types.js +2 -0
- package/dist/error-hints.d.ts +23 -0
- package/dist/error-hints.js +80 -0
- package/dist/indicator-dashboard-keys.d.ts +12 -0
- package/dist/indicator-dashboard-keys.js +50 -0
- package/dist/indicator-dashboard-keys.json +338 -0
- package/dist/presets.js +9 -0
- package/dist/server-instructions.d.ts +5 -0
- package/dist/server-instructions.js +17 -0
- package/dist/server.d.ts +6 -1
- package/dist/server.js +33 -5
- package/dist/tools.d.ts +17 -0
- package/dist/tools.js +399 -18
- package/dist/version-check.d.ts +14 -0
- package/dist/version-check.js +94 -0
- package/dist/version-guide.d.ts +19 -0
- package/dist/version-guide.js +223 -0
- package/package.json +2 -2
- package/spec/overrides.json +26 -0
- package/spec/uwa-openapi.json +495 -111
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ UWA 开放平台 MCP Server。把 UWA Open API 暴露为 MCP 工具,让 Cursor
|
|
|
40
40
|
| Claude Desktop | `claude_desktop_config.json` |
|
|
41
41
|
| Trae | 设置面板中的 MCP 配置 |
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
改完重启客户端即可。多数客户端(Claude Desktop、Trae,以及 PATH 正常的环境)按上面配置即可,**一般不用写 node / npx 绝对路径**。
|
|
44
44
|
|
|
45
45
|
凭证也可以走命令行参数,适配不支持 `env` 字段的客户端:
|
|
46
46
|
|
|
@@ -57,6 +57,73 @@ UWA 开放平台 MCP Server。把 UWA Open API 暴露为 MCP 工具,让 Cursor
|
|
|
57
57
|
|
|
58
58
|
> 配置文件里含有凭证,注意不要提交到公开代码仓库。
|
|
59
59
|
|
|
60
|
+
### Cursor 连不上时(按系统)
|
|
61
|
+
|
|
62
|
+
先在**本机终端**跑通 `npx -y @uwa4d/openapi-mcp check ...`。终端正常、仅 Cursor 失败时,多半是 Cursor 启动 MCP 的环境问题(**不是本包或凭证坏了**)。按日志选对应写法,仍用 `npx`,不必绑死本机 Node 安装路径。
|
|
63
|
+
|
|
64
|
+
| 系统 | 常见日志 | 推荐改法 |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| **macOS** | `ENOENT .../Cursor.app/.../resources/lib` | 用登录壳启动(见下「macOS」) |
|
|
67
|
+
| **Windows** | `spawn npx ENOENT` | 用 `cmd /c` 包一层(见下「Windows」) |
|
|
68
|
+
| **Linux** | 找不到 `npx` / 类似 PATH 问题 | 用 `bash -lc`(见下「Linux」) |
|
|
69
|
+
|
|
70
|
+
**macOS(推荐)**
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"uwa-openapi": {
|
|
76
|
+
"command": "/bin/zsh",
|
|
77
|
+
"args": ["-lic", "npx -y @uwa4d/openapi-mcp mcp"],
|
|
78
|
+
"env": {
|
|
79
|
+
"UWA_MCP_APP_ID": "<your_app_id>",
|
|
80
|
+
"UWA_MCP_APP_SECRET": "<your_app_secret>"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
说明:Cursor 可能把自带 Node 插进 `PATH`,导致 `npx` 找错运行时。`-lic` 走登录壳,加载你平时终端里的 Node(含 nvm 等),不写死版本路径。额外参数写进同一条命令字符串即可。
|
|
88
|
+
|
|
89
|
+
**Windows**
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"uwa-openapi": {
|
|
95
|
+
"command": "cmd",
|
|
96
|
+
"args": ["/c", "npx", "-y", "@uwa4d/openapi-mcp", "mcp"],
|
|
97
|
+
"env": {
|
|
98
|
+
"UWA_MCP_APP_ID": "<your_app_id>",
|
|
99
|
+
"UWA_MCP_APP_SECRET": "<your_app_secret>"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
说明:Windows 上 `npx` 实际是 `npx.cmd`,Cursor 直接 `spawn('npx')` 常会 `ENOENT`。经 `cmd /c` 启动即可。请先确认系统已安装 Node 18+,且在 **CMD** 里执行 `npx -v` 成功(若只用 nvm-windows / fnm,需保证其 bin 已进系统 PATH,或改用该工具提供的 `exec` 方式)。
|
|
107
|
+
|
|
108
|
+
**Linux**
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"uwa-openapi": {
|
|
114
|
+
"command": "/bin/bash",
|
|
115
|
+
"args": ["-lc", "npx -y @uwa4d/openapi-mcp mcp"],
|
|
116
|
+
"env": {
|
|
117
|
+
"UWA_MCP_APP_ID": "<your_app_id>",
|
|
118
|
+
"UWA_MCP_APP_SECRET": "<your_app_secret>"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
改完后在 Cursor 中 **Restart MCP**(或重启客户端)。若仍失败,把 MCP 输出日志里的完整报错留给支持同学。
|
|
126
|
+
|
|
60
127
|
### 先验证一下
|
|
61
128
|
|
|
62
129
|
配置前可以在终端确认凭证和网络是否通:
|
|
@@ -78,7 +145,6 @@ npx -y @uwa4d/openapi-mcp check -a <app_id> -s <app_secret>
|
|
|
78
145
|
| `--app-id` | `-a` | AppId | `UWA_MCP_APP_ID` |
|
|
79
146
|
| `--app-secret` | `-s` | AppSecret | `UWA_MCP_APP_SECRET` |
|
|
80
147
|
| `--base-url` | `-b` | 接口地址,默认 `https://secure-api.uwa4d.com` | `UWA_MCP_API_BASE_URL` |
|
|
81
|
-
| `--sandbox` | | 切到测试环境 `https://sandbox-api.uwa4d.com` | |
|
|
82
148
|
| `--tool` | `-t` | 加载哪些工具,逗号分隔,默认 `preset.default` | `UWA_MCP_TOOL` |
|
|
83
149
|
| `--engine` | `-e` | 只加载适用于 `unity` / `unreal` 的工具,默认 `all` | `UWA_MCP_ENGINE` |
|
|
84
150
|
| `--tool-name-case` | `-c` | 工具命名风格 `snake` / `camel`,默认 `snake` | |
|
|
@@ -107,6 +173,9 @@ npx -y @uwa4d/openapi-mcp check -a <app_id> -s <app_secret>
|
|
|
107
173
|
# 组合:默认集 + 所有预签名类接口 + 单个指定工具
|
|
108
174
|
-t preset.default,presign,gotonline_overview_stack_id_map
|
|
109
175
|
|
|
176
|
+
# 只要复合工具(报告体检 / Top 资源 / Top 函数)
|
|
177
|
+
-t composite
|
|
178
|
+
|
|
110
179
|
# 只做 Unity 项目,砍掉 UE 专用工具
|
|
111
180
|
-t preset.default -e unity
|
|
112
181
|
```
|
|
@@ -118,6 +187,18 @@ npx -y @uwa4d/openapi-mcp list-tools
|
|
|
118
187
|
npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
119
188
|
```
|
|
120
189
|
|
|
190
|
+
## 复合工具
|
|
191
|
+
|
|
192
|
+
默认 preset 里除了原子接口,还有三个**复合工具**——内部会串多个接口并聚合,适合对话式提问:
|
|
193
|
+
|
|
194
|
+
| 工具 | 做什么 |
|
|
195
|
+
| --- | --- |
|
|
196
|
+
| `report_diagnosis` | 单份报告体检:自动选对统计 v1/v2,附带 Top 资源/函数与场景摘要 |
|
|
197
|
+
| `top_resources` | 资源内存 Top N(默认 11 种类型;`groupBy=merged` 全局 Top / `assetType` 按类型各 Top) |
|
|
198
|
+
| `top_functions` | 函数 Top N:下载堆栈树 + idmap,按函数名合并同名节点后再排序 |
|
|
199
|
+
|
|
200
|
+
不确定该调统计 1.0 还是 2.0 时,优先用这三个,不必自己判断日期和 SDK。
|
|
201
|
+
|
|
121
202
|
## 大数据接口的返回方式
|
|
122
203
|
|
|
123
204
|
堆栈树、资源列表、卡顿帧等接口不直接返回数据,而是返回一个有效期 600 秒的预签名下载地址。
|
|
@@ -128,7 +209,15 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
|
128
209
|
- `_totalItems` — 主数据列表的条目总数
|
|
129
210
|
- `_returnedChars` — 实际返回的字符数
|
|
130
211
|
|
|
131
|
-
只在明确只要前 N 条时才传 `maxRows
|
|
212
|
+
只在明确只要前 N 条时才传 `maxRows`。
|
|
213
|
+
|
|
214
|
+
## 返回体积控制
|
|
215
|
+
|
|
216
|
+
`--max-chars`(默认 50 万字符)对**所有**工具生效,不只是上面这类下载型接口。批量统计、报告列表这类接口一次查十几份报告就可能产生二十万字符以上的返回,容易撑爆模型上下文。
|
|
217
|
+
|
|
218
|
+
超限时不会粗暴截断字符串——那样会切出无法解析的 JSON——而是按条目缩减,保证返回始终是完整合法的结构,并在 `_truncated` 里说明缩减到了多少条、该怎么拿到剩下的(分页或缩小批量)。
|
|
219
|
+
|
|
220
|
+
如果你的客户端对单次工具输出有更严格的限制,把 `--max-chars` 调小即可。
|
|
132
221
|
|
|
133
222
|
## 曲线接口的双横轴
|
|
134
223
|
|
|
@@ -169,9 +258,6 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
|
169
258
|
| --- | --- | --- |
|
|
170
259
|
| `@uwa4d/openapi-mcp` | 跟随最新稳定版,自动升级 | 默认,大多数用户 |
|
|
171
260
|
| `@uwa4d/openapi-mcp@0.1.0` | 锁死某个版本 | 生产流水线,要求完全可复现 |
|
|
172
|
-
| `@uwa4d/openapi-mcp@beta` | 跟随测试版 | 配合 UWA 验证新接口 |
|
|
173
|
-
|
|
174
|
-
测试版只发布在 `beta` 标签下,默认写法不会拉到测试版。
|
|
175
261
|
|
|
176
262
|
## 常见问题
|
|
177
263
|
|
|
@@ -179,10 +265,20 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
|
179
265
|
检查 Node 版本是否 ≥ 18,以及配置文件 JSON 格式是否正确(多一个逗号就会整个失效)。改完配置需要重启客户端。
|
|
180
266
|
|
|
181
267
|
**提示凭证错误**
|
|
182
|
-
用 `check`
|
|
268
|
+
用 `check` 命令在终端单独验证一次,排除是客户端配置传参的问题。确认 AppId / AppSecret 无误且未过期。
|
|
269
|
+
|
|
270
|
+
**返回「数据服务错误」或不确定该用 v1 还是 v2**
|
|
271
|
+
|
|
272
|
+
部分接口按报告解析/提交日期和引擎区分版本:
|
|
273
|
+
|
|
274
|
+
| 场景 | 用哪个 |
|
|
275
|
+
| --- | --- |
|
|
276
|
+
| Unity Overview,解析日 **< 2026-07-09** | `get_overview_statistic_v1` |
|
|
277
|
+
| Unity Overview,解析日 **≥ 2026-07-09** 且 SDK ≥ 2.5.1 | `get_overview_statistic_v2` |
|
|
278
|
+
| UE Overview,提交日 **≤ 2026-03-25** | `get_overview_statistic_v1` |
|
|
279
|
+
| UE Overview,提交日 **> 2026-03-25** | `get_ue_overview_statistic_v2` |
|
|
183
280
|
|
|
184
|
-
|
|
185
|
-
部分接口区分报告生成时间和引擎类型,例如 Unity 2026-07-09 之前的报告用 `get_overview_statistic_v1`,之后的用 `get_overview_statistic_v2`。工具描述里注明了适用范围。
|
|
281
|
+
场景统计、运行日志、快照帧等也有类似 1.0/2.0 成对工具,描述开头有「选用规则」。不确定时直接用复合工具 `report_diagnosis` / `top_functions`,内部会按报告详情自动路由。
|
|
186
282
|
|
|
187
283
|
## 许可
|
|
188
284
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自定义面板 statistic / curve 返回体的 Agent 友好标注与过滤。
|
|
3
|
+
* 不改上游 Open API,只在 MCP wrapper 层消歧、降噪、提效。
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 上游嵌套 mean/maximum/min 与扁平别名已统一为同一单位(如 frametime=ms)。
|
|
7
|
+
* 这里只给仍无单位标注的 scene_* 裸数组补 unit/data,便于模型直接读,不改数值。
|
|
8
|
+
*/
|
|
9
|
+
export declare function annotateSceneUnits(data: unknown): unknown;
|
|
10
|
+
/**
|
|
11
|
+
* 标明扁平别名与嵌套 scene_* 的重复关系,便于模型只读一份。
|
|
12
|
+
*/
|
|
13
|
+
export declare function annotateDuplicateFlatAliases(data: unknown): unknown;
|
|
14
|
+
/** 从 columns / group 生成入参面板名 → 出参字段映射,便于模型自检。 */
|
|
15
|
+
export declare function annotateParamFieldMap(data: unknown): unknown;
|
|
16
|
+
export interface CurveFilterOptions {
|
|
17
|
+
valueGt?: number;
|
|
18
|
+
valueLt?: number;
|
|
19
|
+
topN?: number;
|
|
20
|
+
returnFramesOnly?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function extractCurveFilterArgs(args: Record<string, unknown>): {
|
|
23
|
+
filter: CurveFilterOptions;
|
|
24
|
+
cleaned: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
export declare function hasCurveFilter(filter: CurveFilterOptions): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* 对 y_axis 曲线做客户端过滤,解决「慢帧列表必须全量进上下文」的问题。
|
|
29
|
+
*/
|
|
30
|
+
export declare function filterCurveData(data: unknown, filter: CurveFilterOptions): unknown;
|
|
31
|
+
/** returnFramesOnly 时去掉顶层无用 x_axis,避免慢帧列表撑爆上下文。 */
|
|
32
|
+
export declare function stripOuterXAxisWhenFramesOnly(data: unknown, filter: CurveFilterOptions): unknown;
|
|
33
|
+
export declare function isIndicatorStatisticDashboard(opPath: string): boolean;
|
|
34
|
+
export declare function isIndicatorCurveDashboard(opPath: string): boolean;
|
|
35
|
+
/** 从 android_memory_pss 统计抽出弱模型友好的峰值字段(单位 KB)。 */
|
|
36
|
+
export declare function extractPssPeakCard(data: unknown): Record<string, unknown> | null;
|
|
37
|
+
/**
|
|
38
|
+
* 从 Overview 统计 v2(categories[])抽出 PSS 峰值答题卡。
|
|
39
|
+
* v2 通常只有 maximum、没有 peak frame —— frame 为 null 时提示改用 indicator 面板。
|
|
40
|
+
*/
|
|
41
|
+
export declare function extractPssPeakFromOverviewStatistic(data: unknown): Record<string, unknown> | null;
|
|
42
|
+
/** Overview 统计(v1/v2)成功返回后附加 PSS 答题卡。 */
|
|
43
|
+
export declare function annotateOverviewStatisticPss(data: unknown): unknown;
|
|
44
|
+
export declare function isOverviewStatisticOp(opId: string, _opPath?: string): boolean;
|
|
45
|
+
/** 卡顿合并树:返回层硬门禁,防止 WaitForVsync→根因 / 空 Bound 仍说 GPU 主瓶颈。 */
|
|
46
|
+
export declare function stutterRootCauseGate(opId: string): Record<string, unknown> | null;
|
|
47
|
+
/** 统计面板成功返回后的统一标注管线。 */
|
|
48
|
+
export declare function annotateStatisticDashboard(data: unknown, accepted: string[], unknown: string[]): unknown;
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自定义面板 statistic / curve 返回体的 Agent 友好标注与过滤。
|
|
3
|
+
* 不改上游 Open API,只在 MCP wrapper 层消歧、降噪、提效。
|
|
4
|
+
*/
|
|
5
|
+
function isObj(v) {
|
|
6
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
7
|
+
}
|
|
8
|
+
function isUnitObj(v) {
|
|
9
|
+
return isObj(v) && ('unit' in v || 'value' in v || 'data' in v);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 上游嵌套 mean/maximum/min 与扁平别名已统一为同一单位(如 frametime=ms)。
|
|
13
|
+
* 这里只给仍无单位标注的 scene_* 裸数组补 unit/data,便于模型直接读,不改数值。
|
|
14
|
+
*/
|
|
15
|
+
export function annotateSceneUnits(data) {
|
|
16
|
+
if (!isObj(data) || !isObj(data['statistic']))
|
|
17
|
+
return data;
|
|
18
|
+
const statistic = { ...data['statistic'] };
|
|
19
|
+
const notes = [];
|
|
20
|
+
for (const [key, raw] of Object.entries(statistic)) {
|
|
21
|
+
if (!isObj(raw))
|
|
22
|
+
continue;
|
|
23
|
+
// 扁平别名 { unit, data } 跳过;只处理嵌套子指标对象
|
|
24
|
+
if (Array.isArray(raw['data']) && isObj(raw['unit']) && raw['mean'] === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
const metric = { ...raw };
|
|
27
|
+
let changed = false;
|
|
28
|
+
const sceneUnit = isUnitObj(metric['mean']) && isObj(metric['mean'].unit) ? metric['mean'].unit : null;
|
|
29
|
+
for (const [sk, sv] of Object.entries(metric)) {
|
|
30
|
+
if (!sk.startsWith('scene_') || !Array.isArray(sv))
|
|
31
|
+
continue;
|
|
32
|
+
if (isObj(sv[0]) && 'unit' in sv[0])
|
|
33
|
+
continue;
|
|
34
|
+
if (sk.endsWith('_frame') || sk.includes('max_frame')) {
|
|
35
|
+
metric[sk] = { unit: { description: '帧', value: 'frame' }, data: sv };
|
|
36
|
+
}
|
|
37
|
+
else if (sk.includes('pct') || sk.includes('rate')) {
|
|
38
|
+
metric[sk] = { unit: { description: '百分比', value: 'percentage' }, data: sv };
|
|
39
|
+
}
|
|
40
|
+
else if (sk.includes('per_min')) {
|
|
41
|
+
metric[sk] = { unit: { description: '次/分钟', value: 'times/min' }, data: sv };
|
|
42
|
+
}
|
|
43
|
+
else if (sceneUnit) {
|
|
44
|
+
metric[sk] = { unit: sceneUnit, data: sv };
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
changed = true;
|
|
50
|
+
}
|
|
51
|
+
if (changed) {
|
|
52
|
+
statistic[key] = metric;
|
|
53
|
+
notes.push(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!notes.length)
|
|
57
|
+
return data;
|
|
58
|
+
return {
|
|
59
|
+
...data,
|
|
60
|
+
statistic,
|
|
61
|
+
_sceneUnitNote: `已为子指标 [${notes.join(', ')}] 的 scene_* 裸数组补上 unit(与嵌套 mean.unit 对齐);数值未改动。`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 标明扁平别名与嵌套 scene_* 的重复关系,便于模型只读一份。
|
|
66
|
+
*/
|
|
67
|
+
export function annotateDuplicateFlatAliases(data) {
|
|
68
|
+
if (!isObj(data) || !isObj(data['statistic']))
|
|
69
|
+
return data;
|
|
70
|
+
const statistic = data['statistic'];
|
|
71
|
+
const dups = [];
|
|
72
|
+
for (const [flatKey, flatVal] of Object.entries(statistic)) {
|
|
73
|
+
if (!isObj(flatVal) || !Array.isArray(flatVal['data']))
|
|
74
|
+
continue;
|
|
75
|
+
const m = /^(.+)_(mean|min|maximum|maximum_frame|gt_40_ms_frame_pct|jank_time_pct|jank_frames_per_min|bigjank_frames_per_min)$/.exec(flatKey);
|
|
76
|
+
if (!m)
|
|
77
|
+
continue;
|
|
78
|
+
const indicator = m[1];
|
|
79
|
+
const kind = m[2];
|
|
80
|
+
const nested = statistic[indicator];
|
|
81
|
+
if (!isObj(nested))
|
|
82
|
+
continue;
|
|
83
|
+
const sceneKey = kind === 'mean'
|
|
84
|
+
? 'scene_mean'
|
|
85
|
+
: kind === 'min'
|
|
86
|
+
? 'scene_min'
|
|
87
|
+
: kind === 'maximum'
|
|
88
|
+
? 'scene_max'
|
|
89
|
+
: kind === 'maximum_frame'
|
|
90
|
+
? 'scene_max_frame'
|
|
91
|
+
: `scene_${kind}`;
|
|
92
|
+
const sceneVal = nested[sceneKey];
|
|
93
|
+
const sceneArr = Array.isArray(sceneVal)
|
|
94
|
+
? sceneVal
|
|
95
|
+
: isObj(sceneVal) && Array.isArray(sceneVal['data'])
|
|
96
|
+
? sceneVal['data']
|
|
97
|
+
: null;
|
|
98
|
+
if (!sceneArr)
|
|
99
|
+
continue;
|
|
100
|
+
const flatArr = flatVal['data'];
|
|
101
|
+
if (flatArr.length === sceneArr.length && flatArr.every((v, i) => v === sceneArr[i])) {
|
|
102
|
+
dups.push(`${flatKey} ≡ ${indicator}.${sceneKey}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!dups.length)
|
|
106
|
+
return data;
|
|
107
|
+
return {
|
|
108
|
+
...data,
|
|
109
|
+
_duplicateFlatAliases: dups,
|
|
110
|
+
_duplicateNote: '扁平别名与嵌套 scene_* 数值相同,读嵌套结构即可,扁平 key 为兼容别名。',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** 从 columns / group 生成入参面板名 → 出参字段映射,便于模型自检。 */
|
|
114
|
+
export function annotateParamFieldMap(data) {
|
|
115
|
+
if (!isObj(data))
|
|
116
|
+
return data;
|
|
117
|
+
const columns = data['columns'];
|
|
118
|
+
const group = data['group'];
|
|
119
|
+
if (!Array.isArray(columns) && !Array.isArray(group))
|
|
120
|
+
return data;
|
|
121
|
+
const map = {};
|
|
122
|
+
if (Array.isArray(group)) {
|
|
123
|
+
for (const row of group) {
|
|
124
|
+
if (!Array.isArray(row) || row.length < 2)
|
|
125
|
+
continue;
|
|
126
|
+
const panel = String(row[0]);
|
|
127
|
+
const inds = Array.isArray(row[1]) ? row[1].map(String) : [];
|
|
128
|
+
if (!map[panel])
|
|
129
|
+
map[panel] = { indicators: [], flatKeys: [], nestedHint: [] };
|
|
130
|
+
map[panel].indicators = [...new Set([...map[panel].indicators, ...inds])];
|
|
131
|
+
for (const ind of inds) {
|
|
132
|
+
map[panel].nestedHint.push(`statistic.${ind}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(columns)) {
|
|
137
|
+
for (const col of columns) {
|
|
138
|
+
if (!isObj(col))
|
|
139
|
+
continue;
|
|
140
|
+
const panel = String(col['firstLevel'] ?? '');
|
|
141
|
+
const seconds = Array.isArray(col['secondLevel']) ? col['secondLevel'].map(String) : [];
|
|
142
|
+
if (!panel)
|
|
143
|
+
continue;
|
|
144
|
+
if (!map[panel])
|
|
145
|
+
map[panel] = { indicators: [], flatKeys: [], nestedHint: [] };
|
|
146
|
+
map[panel].flatKeys = [...new Set([...map[panel].flatKeys, ...seconds])];
|
|
147
|
+
for (const k of seconds) {
|
|
148
|
+
map[panel].nestedHint.push(`statistic.${k}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// frametime 专属:入参与出参字段名不一致
|
|
153
|
+
for (const [panel, info] of Object.entries(map)) {
|
|
154
|
+
if (info.indicators.includes('frametime') || panel.includes('frametime') || /jank/i.test(panel)) {
|
|
155
|
+
info.nestedHint.push('statistic.frametime.gt_40_ms_frame_pct', 'statistic.frametime.jank_time_pct', 'statistic.frametime.jank_frames_per_min', 'statistic.frametime.bigjank_frames_per_min');
|
|
156
|
+
}
|
|
157
|
+
info.nestedHint = [...new Set(info.nestedHint)];
|
|
158
|
+
}
|
|
159
|
+
if (!Object.keys(map).length)
|
|
160
|
+
return data;
|
|
161
|
+
return { ...data, _paramFieldMap: map };
|
|
162
|
+
}
|
|
163
|
+
function parseNum(v) {
|
|
164
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
165
|
+
return v;
|
|
166
|
+
if (typeof v === 'string' && v.trim() && Number.isFinite(Number(v)))
|
|
167
|
+
return Number(v);
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
export function extractCurveFilterArgs(args) {
|
|
171
|
+
const filter = {
|
|
172
|
+
valueGt: parseNum(args['valueGt']),
|
|
173
|
+
valueLt: parseNum(args['valueLt']),
|
|
174
|
+
topN: parseNum(args['topN']),
|
|
175
|
+
returnFramesOnly: args['returnFramesOnly'] === true,
|
|
176
|
+
};
|
|
177
|
+
const cleaned = { ...args };
|
|
178
|
+
delete cleaned['valueGt'];
|
|
179
|
+
delete cleaned['valueLt'];
|
|
180
|
+
delete cleaned['topN'];
|
|
181
|
+
delete cleaned['returnFramesOnly'];
|
|
182
|
+
return { filter, cleaned };
|
|
183
|
+
}
|
|
184
|
+
export function hasCurveFilter(filter) {
|
|
185
|
+
return (filter.valueGt !== undefined ||
|
|
186
|
+
filter.valueLt !== undefined ||
|
|
187
|
+
filter.topN !== undefined ||
|
|
188
|
+
filter.returnFramesOnly === true);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 对 y_axis 曲线做客户端过滤,解决「慢帧列表必须全量进上下文」的问题。
|
|
192
|
+
*/
|
|
193
|
+
export function filterCurveData(data, filter) {
|
|
194
|
+
if (!hasCurveFilter(filter) || !isObj(data) || !isObj(data['y_axis']))
|
|
195
|
+
return data;
|
|
196
|
+
const yAxis = data['y_axis'];
|
|
197
|
+
const sharedX = isObj(data['x_axis']) && Array.isArray(data['x_axis']['data']) ? data['x_axis']['data'] : null;
|
|
198
|
+
const nextY = {};
|
|
199
|
+
const filterMeta = {};
|
|
200
|
+
for (const [name, curve] of Object.entries(yAxis)) {
|
|
201
|
+
if (!isObj(curve) || !Array.isArray(curve['data'])) {
|
|
202
|
+
nextY[name] = curve;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const values = curve['data'];
|
|
206
|
+
const ownX = isObj(curve['x_axis']) && Array.isArray(curve['x_axis']['data'])
|
|
207
|
+
? curve['x_axis']['data']
|
|
208
|
+
: sharedX;
|
|
209
|
+
const pairs = [];
|
|
210
|
+
for (let i = 0; i < values.length; i++) {
|
|
211
|
+
const v = values[i];
|
|
212
|
+
if (typeof v !== 'number' || !Number.isFinite(v))
|
|
213
|
+
continue;
|
|
214
|
+
if (filter.valueGt !== undefined && !(v > filter.valueGt))
|
|
215
|
+
continue;
|
|
216
|
+
if (filter.valueLt !== undefined && !(v < filter.valueLt))
|
|
217
|
+
continue;
|
|
218
|
+
pairs.push({ frame: ownX?.[i] ?? i + 1, value: v, idx: i });
|
|
219
|
+
}
|
|
220
|
+
let kept = pairs;
|
|
221
|
+
if (filter.topN !== undefined && filter.topN > 0 && kept.length > filter.topN) {
|
|
222
|
+
kept = [...kept].sort((a, b) => b.value - a.value).slice(0, filter.topN);
|
|
223
|
+
kept.sort((a, b) => Number(a.frame) - Number(b.frame));
|
|
224
|
+
}
|
|
225
|
+
filterMeta[name] = {
|
|
226
|
+
originalPoints: values.length,
|
|
227
|
+
keptPoints: kept.length,
|
|
228
|
+
valueGt: filter.valueGt ?? null,
|
|
229
|
+
valueLt: filter.valueLt ?? null,
|
|
230
|
+
topN: filter.topN ?? null,
|
|
231
|
+
};
|
|
232
|
+
if (filter.returnFramesOnly) {
|
|
233
|
+
nextY[name] = {
|
|
234
|
+
unit: { description: '帧', value: 'frame' },
|
|
235
|
+
frames: kept.map((p) => p.frame),
|
|
236
|
+
values: kept.map((p) => p.value),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
nextY[name] = {
|
|
241
|
+
...curve,
|
|
242
|
+
data: kept.map((p) => p.value),
|
|
243
|
+
x_axis: {
|
|
244
|
+
unit: isObj(curve['x_axis']) ? curve['x_axis']['unit'] : { description: '帧', value: 'frame' },
|
|
245
|
+
data: kept.map((p) => p.frame),
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
...data,
|
|
252
|
+
y_axis: nextY,
|
|
253
|
+
_curveFilter: filterMeta,
|
|
254
|
+
_curveFilterHint: '已按 valueGt/valueLt/topN/returnFramesOnly 在 MCP 侧过滤曲线点;完整曲线去掉这些参数即可。',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/** returnFramesOnly 时去掉顶层无用 x_axis,避免慢帧列表撑爆上下文。 */
|
|
258
|
+
export function stripOuterXAxisWhenFramesOnly(data, filter) {
|
|
259
|
+
if (!filter.returnFramesOnly || !isObj(data))
|
|
260
|
+
return data;
|
|
261
|
+
const { x_axis: _drop, ...rest } = data;
|
|
262
|
+
return {
|
|
263
|
+
...rest,
|
|
264
|
+
_framesOnlyNote: 'returnFramesOnly=true 已省略顶层 x_axis;帧号在各曲线 frames 字段。',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
export function isIndicatorStatisticDashboard(opPath) {
|
|
268
|
+
return opPath.includes('/indicator/statistic/dashboard') || opPath.includes('/custom/dashboard');
|
|
269
|
+
}
|
|
270
|
+
export function isIndicatorCurveDashboard(opPath) {
|
|
271
|
+
return opPath.includes('/indicator/curve/dashboard') || opPath.includes('/gpu/curve');
|
|
272
|
+
}
|
|
273
|
+
function numFromUnitOrData(v) {
|
|
274
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
275
|
+
return v;
|
|
276
|
+
if (Array.isArray(v) && typeof v[0] === 'number' && Number.isFinite(v[0]))
|
|
277
|
+
return v[0];
|
|
278
|
+
if (typeof v === 'object' && v !== null) {
|
|
279
|
+
const o = v;
|
|
280
|
+
if (typeof o.value === 'number' && Number.isFinite(o.value))
|
|
281
|
+
return o.value;
|
|
282
|
+
if (Array.isArray(o.data) && typeof o.data[0] === 'number')
|
|
283
|
+
return o.data[0];
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
function buildPssPeakCard(peakKb, frame) {
|
|
288
|
+
return {
|
|
289
|
+
pss_peak_kb: peakKb,
|
|
290
|
+
pss_peak_mb: Math.round((peakKb / 1024) * 100) / 100,
|
|
291
|
+
pss_peak_frame: frame,
|
|
292
|
+
pss_unit: 'KB',
|
|
293
|
+
_pssHint: 'Android PSS 峰值请优先读本对象顶层 pss_peak_kb / pss_peak_mb / pss_peak_frame;' +
|
|
294
|
+
'合法面板名是 android_memory_pss@max(不是 android_pss_max)。单位默认 KB。',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/** 从 android_memory_pss 统计抽出弱模型友好的峰值字段(单位 KB)。 */
|
|
298
|
+
export function extractPssPeakCard(data) {
|
|
299
|
+
if (!isObj(data))
|
|
300
|
+
return null;
|
|
301
|
+
const statistic = isObj(data['statistic']) ? data['statistic'] : null;
|
|
302
|
+
const nested = statistic && isObj(statistic['android_memory_pss'])
|
|
303
|
+
? statistic['android_memory_pss']
|
|
304
|
+
: null;
|
|
305
|
+
const peakKb = (nested ? numFromUnitOrData(nested['maximum']) : null) ??
|
|
306
|
+
(statistic ? numFromUnitOrData(statistic['android_memory_pss_maximum']) : null) ??
|
|
307
|
+
numFromUnitOrData(data['android_memory_pss_maximum']);
|
|
308
|
+
const frame = (nested ? numFromUnitOrData(nested['maximum_frame']) : null) ??
|
|
309
|
+
(statistic ? numFromUnitOrData(statistic['android_memory_pss_maximum_frame']) : null) ??
|
|
310
|
+
numFromUnitOrData(data['android_memory_pss_maximum_frame']);
|
|
311
|
+
if (peakKb == null)
|
|
312
|
+
return null;
|
|
313
|
+
return buildPssPeakCard(peakKb, frame);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 从 Overview 统计 v2(categories[])抽出 PSS 峰值答题卡。
|
|
317
|
+
* v2 通常只有 maximum、没有 peak frame —— frame 为 null 时提示改用 indicator 面板。
|
|
318
|
+
*/
|
|
319
|
+
export function extractPssPeakFromOverviewStatistic(data) {
|
|
320
|
+
if (!isObj(data))
|
|
321
|
+
return null;
|
|
322
|
+
const categories = Array.isArray(data['categories'])
|
|
323
|
+
? data['categories']
|
|
324
|
+
: isObj(data['data']) && Array.isArray(data['data']['categories'])
|
|
325
|
+
? data['data']['categories']
|
|
326
|
+
: null;
|
|
327
|
+
if (!categories)
|
|
328
|
+
return null;
|
|
329
|
+
let peakKb = null;
|
|
330
|
+
let frame = null;
|
|
331
|
+
for (const cat of categories) {
|
|
332
|
+
if (!isObj(cat) || !Array.isArray(cat['indicators']))
|
|
333
|
+
continue;
|
|
334
|
+
for (const ind of cat['indicators']) {
|
|
335
|
+
if (!isObj(ind))
|
|
336
|
+
continue;
|
|
337
|
+
const key = String(ind['key'] ?? ind['indicatorKey'] ?? '');
|
|
338
|
+
const n = numFromUnitOrData(ind['data'] ?? ind['value']);
|
|
339
|
+
if (n == null)
|
|
340
|
+
continue;
|
|
341
|
+
if (key === 'android_memory_pss_maximum' || key === 'android_memory_pss@max')
|
|
342
|
+
peakKb = n;
|
|
343
|
+
if (key === 'android_memory_pss_maximum_frame' || key === 'android_memory_pss@max_frame')
|
|
344
|
+
frame = n;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (peakKb == null)
|
|
348
|
+
return null;
|
|
349
|
+
const card = buildPssPeakCard(peakKb, frame);
|
|
350
|
+
if (frame == null) {
|
|
351
|
+
card['_pssFrameNote'] =
|
|
352
|
+
'本接口未返回峰值帧;需要 pss_peak_frame 时请再调 gotonline_overview_indicator_statistic_dashboard,面板名 android_memory_pss@max。';
|
|
353
|
+
}
|
|
354
|
+
return card;
|
|
355
|
+
}
|
|
356
|
+
/** Overview 统计(v1/v2)成功返回后附加 PSS 答题卡。 */
|
|
357
|
+
export function annotateOverviewStatisticPss(data) {
|
|
358
|
+
if (Array.isArray(data)) {
|
|
359
|
+
return data.map((item) => annotateOverviewStatisticPss(item));
|
|
360
|
+
}
|
|
361
|
+
if (!isObj(data))
|
|
362
|
+
return data;
|
|
363
|
+
// batch map: { [dataKey]: { categories... } }
|
|
364
|
+
const keys = Object.keys(data);
|
|
365
|
+
if (!data['categories'] &&
|
|
366
|
+
!data['statistic'] &&
|
|
367
|
+
!data['brief'] &&
|
|
368
|
+
keys.length > 0 &&
|
|
369
|
+
keys.every((k) => isObj(data[k]))) {
|
|
370
|
+
const next = { ...data };
|
|
371
|
+
for (const k of keys)
|
|
372
|
+
next[k] = annotateOverviewStatisticPss(data[k]);
|
|
373
|
+
return next;
|
|
374
|
+
}
|
|
375
|
+
if (data['pss_peak_kb'] != null)
|
|
376
|
+
return data;
|
|
377
|
+
const fromDash = extractPssPeakCard(data);
|
|
378
|
+
const fromV2 = fromDash ?? extractPssPeakFromOverviewStatistic(data);
|
|
379
|
+
if (!fromV2)
|
|
380
|
+
return data;
|
|
381
|
+
return { ...data, ...fromV2 };
|
|
382
|
+
}
|
|
383
|
+
export function isOverviewStatisticOp(opId, _opPath) {
|
|
384
|
+
return (opId === 'get_overview_statistic_v1' ||
|
|
385
|
+
opId === 'get_overview_statistic_v2' ||
|
|
386
|
+
opId === 'get_ue_overview_statistic_v2');
|
|
387
|
+
}
|
|
388
|
+
/** 卡顿合并树:返回层硬门禁,防止 WaitForVsync→根因 / 空 Bound 仍说 GPU 主瓶颈。 */
|
|
389
|
+
export function stutterRootCauseGate(opId) {
|
|
390
|
+
if (!/stack_stutter_(full|scene)_tree/.test(opId))
|
|
391
|
+
return null;
|
|
392
|
+
return {
|
|
393
|
+
forbidden_claims: ['WaitForVsync是卡顿根因', 'GPU是主要瓶颈', 'GPU Bound明显', 'GPU主瓶颈'],
|
|
394
|
+
_stutterRootCauseHint: '本树是卡顿根因主入口。请按耗时归因到具体业务/引擎节点;' +
|
|
395
|
+
'禁止把 WaitForVsync / Gfx.WaitForPresent 写成根因或 GPU Bound 证据。' +
|
|
396
|
+
'若要说 GPU 主瓶颈,必须另查 gotonline_gpu_bound;空数组只能说「未检出 Bound」。',
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/** 统计面板成功返回后的统一标注管线。 */
|
|
400
|
+
export function annotateStatisticDashboard(data, accepted, unknown) {
|
|
401
|
+
let out = annotateSceneUnits(data);
|
|
402
|
+
out = annotateDuplicateFlatAliases(out);
|
|
403
|
+
out = annotateParamFieldMap(out);
|
|
404
|
+
if (!isObj(out))
|
|
405
|
+
return out;
|
|
406
|
+
const pssCard = extractPssPeakCard(out);
|
|
407
|
+
return {
|
|
408
|
+
...out,
|
|
409
|
+
...(pssCard ?? {}),
|
|
410
|
+
_acceptedDashboards: accepted,
|
|
411
|
+
...(unknown.length ? { _unknownDashboards: unknown } : {}),
|
|
412
|
+
};
|
|
413
|
+
}
|