@uwa4d/openapi-mcp 0.2.0-beta.8 → 0.2.0-beta.9
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 +68 -1
- package/dist/annotate-dashboard.d.ts +14 -0
- package/dist/annotate-dashboard.js +138 -0
- package/dist/client.js +4 -13
- package/dist/composite/report-diagnosis.js +139 -6
- package/dist/error-hints.d.ts +13 -0
- package/dist/error-hints.js +55 -0
- package/dist/indicator-dashboard-keys.d.ts +1 -1
- package/dist/indicator-dashboard-keys.js +1 -0
- package/dist/server-instructions.d.ts +5 -0
- package/dist/server-instructions.js +16 -0
- package/dist/server.js +2 -1
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +95 -8
- package/dist/version-guide.js +14 -2
- package/package.json +1 -1
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
|
配置前可以在终端确认凭证和网络是否通:
|
|
@@ -28,7 +28,21 @@ export declare function hasCurveFilter(filter: CurveFilterOptions): boolean;
|
|
|
28
28
|
* 对 y_axis 曲线做客户端过滤,解决「慢帧列表必须全量进上下文」的问题。
|
|
29
29
|
*/
|
|
30
30
|
export declare function filterCurveData(data: unknown, filter: CurveFilterOptions): unknown;
|
|
31
|
+
/** returnFramesOnly 时去掉顶层无用 x_axis,避免慢帧列表撑爆上下文。 */
|
|
32
|
+
export declare function stripOuterXAxisWhenFramesOnly(data: unknown, filter: CurveFilterOptions): unknown;
|
|
31
33
|
export declare function isIndicatorStatisticDashboard(opPath: string): boolean;
|
|
32
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;
|
|
33
47
|
/** 统计面板成功返回后的统一标注管线。 */
|
|
34
48
|
export declare function annotateStatisticDashboard(data: unknown, accepted: string[], unknown: string[]): unknown;
|
|
@@ -254,12 +254,148 @@ export function filterCurveData(data, filter) {
|
|
|
254
254
|
_curveFilterHint: '已按 valueGt/valueLt/topN/returnFramesOnly 在 MCP 侧过滤曲线点;完整曲线去掉这些参数即可。',
|
|
255
255
|
};
|
|
256
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
|
+
}
|
|
257
267
|
export function isIndicatorStatisticDashboard(opPath) {
|
|
258
268
|
return opPath.includes('/indicator/statistic/dashboard') || opPath.includes('/custom/dashboard');
|
|
259
269
|
}
|
|
260
270
|
export function isIndicatorCurveDashboard(opPath) {
|
|
261
271
|
return opPath.includes('/indicator/curve/dashboard') || opPath.includes('/gpu/curve');
|
|
262
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
|
+
}
|
|
263
399
|
/** 统计面板成功返回后的统一标注管线。 */
|
|
264
400
|
export function annotateStatisticDashboard(data, accepted, unknown) {
|
|
265
401
|
let out = annotateSceneUnits(data);
|
|
@@ -267,8 +403,10 @@ export function annotateStatisticDashboard(data, accepted, unknown) {
|
|
|
267
403
|
out = annotateParamFieldMap(out);
|
|
268
404
|
if (!isObj(out))
|
|
269
405
|
return out;
|
|
406
|
+
const pssCard = extractPssPeakCard(out);
|
|
270
407
|
return {
|
|
271
408
|
...out,
|
|
409
|
+
...(pssCard ?? {}),
|
|
272
410
|
_acceptedDashboards: accepted,
|
|
273
411
|
...(unknown.length ? { _unknownDashboards: unknown } : {}),
|
|
274
412
|
};
|
package/dist/client.js
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { gunzipSync } from 'node:zlib';
|
|
2
2
|
import { authHeaders } from './auth.js';
|
|
3
|
+
import { resolveErrorHint } from './error-hints.js';
|
|
3
4
|
function isEnvelope(v) {
|
|
4
5
|
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
5
6
|
return false;
|
|
6
7
|
const o = v;
|
|
7
8
|
return o['status'] === 'success' || o['status'] === 'failed';
|
|
8
9
|
}
|
|
9
|
-
/** 常见错误码的排查提示,直接给到模型,省掉一轮试错。 */
|
|
10
|
-
const ERROR_HINTS = {
|
|
11
|
-
20001: '业务参数不合法,核对参数名、取值范围和必填项(注意批量接口的参数名多为复数,如 dataKeys)',
|
|
12
|
-
23508: '数据服务错误,常见原因是报告不适用该接口版本(如新报告调用了 1.0 接口,或旧报告调用了 2.0 接口),改用对应版本重试',
|
|
13
|
-
24050: '该账号未开通 Open API 权限,请联系 UWA 工作人员开通',
|
|
14
|
-
24052: '请求参数有误,请对照接口文档检查',
|
|
15
|
-
24054: 'AppId 不存在,检查 appId 是否正确、是否用错了环境(sandbox / 线上凭证不通用)',
|
|
16
|
-
24056: '签名错误,检查 appSecret 是否正确',
|
|
17
|
-
24057: '时间戳过期,本机时间与服务端偏差不能超过 20 分钟',
|
|
18
|
-
30001: '服务端错误,常见原因是用错了引擎对应的接口(如对 UE 报告调用了 Unity 专用接口)',
|
|
19
|
-
};
|
|
20
10
|
export class UwaApiError extends Error {
|
|
21
11
|
code;
|
|
22
12
|
rawMessage;
|
|
@@ -68,8 +58,9 @@ export class UwaClient {
|
|
|
68
58
|
if (parsed.status === 'failed' || parsed.error) {
|
|
69
59
|
const code = parsed.error?.code ?? -1;
|
|
70
60
|
const raw = parsed.error?.data?.rawMessage ?? '';
|
|
71
|
-
const
|
|
72
|
-
|
|
61
|
+
const apiMessage = parsed.error?.message ?? '请求失败';
|
|
62
|
+
const hint = resolveErrorHint(code, raw, apiMessage);
|
|
63
|
+
throw new UwaApiError(code, raw, `[${code}] ${apiMessage}${raw ? ` (${raw})` : ''}${hint ? `\n排查建议:${hint}` : ''}`);
|
|
73
64
|
}
|
|
74
65
|
return parsed.data ?? {};
|
|
75
66
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from './types.js';
|
|
2
|
-
import { errorResult, jsonToolResult, requireReportKey } from './helpers.js';
|
|
2
|
+
import { errorResult, isObj, jsonToolResult, requireReportKey } from './helpers.js';
|
|
3
3
|
import { fetchOverviewStatistic, fetchReportIdentity, fetchSceneStatistic, } from './route-overview.js';
|
|
4
4
|
import { viewOverview } from './overview-view.js';
|
|
5
5
|
import { runTopFunctions } from './top-functions.js';
|
|
6
6
|
import { runTopResources } from './top-resources.js';
|
|
7
|
+
import { extractPssPeakCard } from '../annotate-dashboard.js';
|
|
7
8
|
function summarizeScenes(data) {
|
|
8
9
|
if (!data)
|
|
9
10
|
return { sceneCount: 0 };
|
|
@@ -76,18 +77,109 @@ function pickImportantBrief(brief) {
|
|
|
76
77
|
}
|
|
77
78
|
return out;
|
|
78
79
|
}
|
|
80
|
+
function numOf(v) {
|
|
81
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
82
|
+
return v;
|
|
83
|
+
if (Array.isArray(v) && typeof v[0] === 'number')
|
|
84
|
+
return v[0];
|
|
85
|
+
if (typeof v === 'string' && v.trim() && Number.isFinite(Number(v)))
|
|
86
|
+
return Number(v);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/** 从 overview brief/summary 尽量抽出 PSS 峰值答题卡。 */
|
|
90
|
+
function extractPssFromView(brief, summary) {
|
|
91
|
+
const sources = [brief, summary];
|
|
92
|
+
for (const src of sources) {
|
|
93
|
+
const peak = numOf(src['android_memory_pss_maximum']) ??
|
|
94
|
+
numOf(src['android_memory_pss@max']) ??
|
|
95
|
+
numOf(src['PSS峰值']) ??
|
|
96
|
+
numOf(src['设备内存峰值(MB)']);
|
|
97
|
+
const frame = numOf(src['android_memory_pss_maximum_frame']) ??
|
|
98
|
+
numOf(src['android_memory_pss@max_frame']) ??
|
|
99
|
+
null;
|
|
100
|
+
if (peak == null)
|
|
101
|
+
continue;
|
|
102
|
+
// brief 里设备内存峰值多为 MB;android_memory_pss 多为 KB
|
|
103
|
+
const looksMb = peak < 50_000 && (src['设备内存峰值(MB)'] !== undefined || peak < 2048);
|
|
104
|
+
const peakKb = looksMb ? Math.round(peak * 1024) : peak;
|
|
105
|
+
return {
|
|
106
|
+
pss_peak_kb: peakKb,
|
|
107
|
+
pss_peak_mb: Math.round((peakKb / 1024) * 100) / 100,
|
|
108
|
+
pss_peak_frame: frame,
|
|
109
|
+
pss_unit: 'KB',
|
|
110
|
+
_pssHint: 'Android PSS 峰值请优先读 pss_peak_kb/mb/frame;合法面板名 android_memory_pss@max(不是 android_pss_max)。',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/** 优先走 indicator statistic 拿完整 PSS 峰值+帧;失败再回退 overview brief。 */
|
|
116
|
+
async function resolvePssPeakCard(client, identity, brief, summary, apisUsed, limitations) {
|
|
117
|
+
const keyQuery = {
|
|
118
|
+
indicatorDashboards: 'android_memory_pss@max',
|
|
119
|
+
};
|
|
120
|
+
if (identity.dataKey)
|
|
121
|
+
keyQuery['dataKey'] = identity.dataKey;
|
|
122
|
+
else if (identity.recordId != null)
|
|
123
|
+
keyQuery['recordId'] = identity.recordId;
|
|
124
|
+
else
|
|
125
|
+
return extractPssFromView(brief, summary);
|
|
126
|
+
try {
|
|
127
|
+
const raw = await client.call('GET', '/openapi/v1/data/gotonline/overview/indicator/statistic/dashboard', 'v1.0.1', keyQuery);
|
|
128
|
+
apisUsed.push('gotonline_overview_indicator_statistic_dashboard');
|
|
129
|
+
const card = extractPssPeakCard(raw);
|
|
130
|
+
if (card)
|
|
131
|
+
return card;
|
|
132
|
+
limitations.push('indicator PSS 面板无峰值字段,已回退 overview 摘要');
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
limitations.push(`PSS 面板查询失败,已回退 overview 摘要:${e instanceof Error ? e.message : String(e)}`);
|
|
136
|
+
}
|
|
137
|
+
return extractPssFromView(brief, summary);
|
|
138
|
+
}
|
|
139
|
+
function isWaitForVsyncName(name) {
|
|
140
|
+
return /wait\s*for\s*vsync|WaitForVsync|Gfx\.WaitForPresent/i.test(String(name ?? ''));
|
|
141
|
+
}
|
|
142
|
+
function buildBottleneckCandidates(topFunctions, gpuBoundEmpty) {
|
|
143
|
+
const items = topFunctions && Array.isArray(topFunctions['items']) ? topFunctions['items'] : [];
|
|
144
|
+
const out = [];
|
|
145
|
+
for (const raw of items.slice(0, 8)) {
|
|
146
|
+
if (!isObj(raw))
|
|
147
|
+
continue;
|
|
148
|
+
const name = raw['name'] ?? raw['methodName'] ?? raw['stackMethodName'];
|
|
149
|
+
const wait = isWaitForVsyncName(name);
|
|
150
|
+
out.push({
|
|
151
|
+
name,
|
|
152
|
+
selfTimeMean: raw['selfTimeMean'] ?? raw['value'] ?? null,
|
|
153
|
+
source: 'top_functions',
|
|
154
|
+
role: wait ? 'frame_sync_wait' : 'cpu_candidate',
|
|
155
|
+
note: wait
|
|
156
|
+
? 'WaitForVsync/Present 等待是帧同步表现,不能直接当卡顿根因或 GPU Bound 证据'
|
|
157
|
+
: '需结合 stutter 合并树确认是否为卡顿根因',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (gpuBoundEmpty === true) {
|
|
161
|
+
out.push({
|
|
162
|
+
name: 'GPU Bound',
|
|
163
|
+
source: 'gotonline_gpu_bound',
|
|
164
|
+
role: 'not_detected',
|
|
165
|
+
note: 'targetFps=30 下未检出 GPU Bound 区间',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
79
170
|
export const reportDiagnosisTool = {
|
|
80
171
|
id: 'report_diagnosis',
|
|
81
172
|
title: '报告体检',
|
|
82
173
|
engines: ['unity', 'unreal'],
|
|
83
174
|
filterKeys: ['report_diagnosis', 'composite', 'preset.default', 'overview'],
|
|
84
175
|
description: [
|
|
85
|
-
'对单份 GOT Online 报告做一次结构化体检:自动选对 Overview 统计 v1/v2,并串联 Top 资源、Top
|
|
176
|
+
'对单份 GOT Online 报告做一次结构化体检:自动选对 Overview 统计 v1/v2,并串联 Top 资源、Top 函数、场景摘要与 GPU Bound@30。',
|
|
86
177
|
'适用引擎:Unity / UE|复合工具',
|
|
87
178
|
'调用方不必自己判断 2026-07-09 / 2026-03-25 分界或 SDK 版本——内部按 get_report_detail 路由。',
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
179
|
+
'返回答题卡:forbidden_claims、bottleneck_candidates(与 topFunctions 分栏)、gpuBound、pss_peak_*、comparability_note。',
|
|
180
|
+
'卡顿根因:优先 stutter_full_tree / stutter_scene_tree;禁止把 WaitForVsync 写成根因或 GPU 主瓶颈。',
|
|
181
|
+
'gpuBound 为空时只能说「未检出 Bound」,禁止断言 GPU 是主要瓶颈。',
|
|
182
|
+
'查面板统计/曲线前先调 gotonline_overview_indicator_dashboard_keys(勿把 fps_mean 等统计字段当面板名)。',
|
|
91
183
|
'scenes 含 list_scenes 起止帧(sceneIdx/sceneName/frameStart/frameEnd)。',
|
|
92
184
|
'返回摘要而非原始大包;需要明细时再按 _apisUsed 中的原子工具深挖。',
|
|
93
185
|
].join('\n'),
|
|
@@ -166,10 +258,44 @@ export const reportDiagnosisTool = {
|
|
|
166
258
|
catch (e) {
|
|
167
259
|
limitations.push(`场景列表(list_scenes)失败:${e instanceof Error ? e.message : String(e)}`);
|
|
168
260
|
}
|
|
261
|
+
let gpuBound = null;
|
|
262
|
+
let gpuBoundEmpty = null;
|
|
263
|
+
try {
|
|
264
|
+
const boundQuery = { targetFps: 30 };
|
|
265
|
+
if (identity.dataKey)
|
|
266
|
+
boundQuery['dataKey'] = identity.dataKey;
|
|
267
|
+
else if (identity.recordId)
|
|
268
|
+
boundQuery['recordId'] = identity.recordId;
|
|
269
|
+
gpuBound = await ctx.client.call('GET', '/openapi/v1/data/gotonline/gpu/bound', 'v1.0.1', boundQuery);
|
|
270
|
+
apisUsed.push('gotonline_gpu_bound');
|
|
271
|
+
const intervals = Array.isArray(gpuBound)
|
|
272
|
+
? gpuBound
|
|
273
|
+
: isObj(gpuBound) && Array.isArray(gpuBound['data'])
|
|
274
|
+
? gpuBound['data']
|
|
275
|
+
: null;
|
|
276
|
+
gpuBoundEmpty = Array.isArray(intervals) ? intervals.length === 0 : null;
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
limitations.push(`gpu_bound 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
280
|
+
}
|
|
169
281
|
// 仅在明确未开启时提示;hasResource === null 表示无法判定,不误报关闭
|
|
170
282
|
if (view.hasResource === false) {
|
|
171
283
|
limitations.push('未开启资源采集,逐资源 Top 可能为空');
|
|
172
284
|
}
|
|
285
|
+
const forbidden_claims = [];
|
|
286
|
+
if (gpuBoundEmpty === true) {
|
|
287
|
+
forbidden_claims.push('GPU是主要瓶颈', 'GPU Bound明显', 'GPU主瓶颈');
|
|
288
|
+
}
|
|
289
|
+
forbidden_claims.push('WaitForVsync是卡顿根因');
|
|
290
|
+
const pssCard = await resolvePssPeakCard(ctx.client, identity, view.brief, view.summary, apisUsed, limitations);
|
|
291
|
+
const sceneCount = isObj(scenes) ? Number(scenes['sceneCount'] ?? 0) : 0;
|
|
292
|
+
const comparability_note = [
|
|
293
|
+
'与其它报告对比时:请核对场景名/数量、测试时长、目标帧率、SDK/引擎版本是否一致。',
|
|
294
|
+
sceneCount > 0 ? `本报告场景数=${sceneCount}。` : '',
|
|
295
|
+
'仅比较 FPS 均值不能视为严格同场景对比。',
|
|
296
|
+
]
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.join(' ');
|
|
173
299
|
const result = {
|
|
174
300
|
report: {
|
|
175
301
|
dataKey: identity.dataKey,
|
|
@@ -185,6 +311,11 @@ export const reportDiagnosisTool = {
|
|
|
185
311
|
hasResource: view.hasResource,
|
|
186
312
|
summary: view.summary,
|
|
187
313
|
brief: pickImportantBrief(view.brief),
|
|
314
|
+
...(pssCard ?? {}),
|
|
315
|
+
gpuBound: Array.isArray(gpuBound) ? gpuBound : isObj(gpuBound) ? gpuBound['data'] ?? gpuBound : gpuBound,
|
|
316
|
+
gpuBoundEmpty,
|
|
317
|
+
forbidden_claims,
|
|
318
|
+
bottleneck_candidates: buildBottleneckCandidates(topFunctions, gpuBoundEmpty),
|
|
188
319
|
topResources: topResources
|
|
189
320
|
? {
|
|
190
321
|
items: topResources['items'],
|
|
@@ -198,12 +329,14 @@ export const reportDiagnosisTool = {
|
|
|
198
329
|
metric: topFunctions['metric'],
|
|
199
330
|
uniqueFunctions: topFunctions['uniqueFunctions'],
|
|
200
331
|
note: topFunctions['_note'],
|
|
332
|
+
_columnNote: 'topFunctions 是耗时排行,不等于卡顿根因;根因看 stutter 合并树与 bottleneck_candidates。',
|
|
201
333
|
}
|
|
202
334
|
: null,
|
|
203
335
|
scenes,
|
|
336
|
+
comparability_note,
|
|
204
337
|
_apisUsed: apisUsed,
|
|
205
338
|
_limitations: limitations,
|
|
206
|
-
_note: '
|
|
339
|
+
_note: '结构化体检摘要(含答题卡)。GPU Bound 空时禁止 GPU 主瓶颈结论;卡顿根因优先 stutter 树。细节请按 _apisUsed 调用对应原子工具。',
|
|
207
340
|
};
|
|
208
341
|
return jsonToolResult(result, ctx.maxChars);
|
|
209
342
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI / 下游错误码 → AI 友好排查建议。
|
|
3
|
+
* 产品语义按引擎在此投影;got-query 只提供稳定 rawMessage 原因码。
|
|
4
|
+
*/
|
|
5
|
+
export declare const FILE_META_REASONS: {
|
|
6
|
+
readonly LG_META_ABSENT: "LG_META_ABSENT";
|
|
7
|
+
readonly LG_RUNTIME_PATH_MISSING: "LG_RUNTIME_PATH_MISSING";
|
|
8
|
+
readonly AT_META_ABSENT: "AT_META_ABSENT";
|
|
9
|
+
readonly AT_META_TYPE_INVALID: "AT_META_TYPE_INVALID";
|
|
10
|
+
readonly AT_META_PATH_MISSING: "AT_META_PATH_MISSING";
|
|
11
|
+
};
|
|
12
|
+
/** 组装给模型看的排查建议;优先 rawMessage 原因码 + OpenAPI message。 */
|
|
13
|
+
export declare function resolveErrorHint(code: number, rawMessage: string, apiMessage: string): string | undefined;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI / 下游错误码 → AI 友好排查建议。
|
|
3
|
+
* 产品语义按引擎在此投影;got-query 只提供稳定 rawMessage 原因码。
|
|
4
|
+
*/
|
|
5
|
+
export const FILE_META_REASONS = {
|
|
6
|
+
LG_META_ABSENT: 'LG_META_ABSENT',
|
|
7
|
+
LG_RUNTIME_PATH_MISSING: 'LG_RUNTIME_PATH_MISSING',
|
|
8
|
+
AT_META_ABSENT: 'AT_META_ABSENT',
|
|
9
|
+
AT_META_TYPE_INVALID: 'AT_META_TYPE_INVALID',
|
|
10
|
+
AT_META_PATH_MISSING: 'AT_META_PATH_MISSING',
|
|
11
|
+
};
|
|
12
|
+
const ERROR_HINTS_BY_CODE = {
|
|
13
|
+
20001: '业务参数不合法,核对参数名、取值范围和必填项(注意批量接口的参数名多为复数,如 dataKeys)',
|
|
14
|
+
23508: '数据服务错误,常见原因是报告不适用该接口版本(如新报告调用了 1.0 接口,或旧报告调用了 2.0 接口),改用对应版本重试',
|
|
15
|
+
24050: '该账号未开通 Open API 权限,请联系 UWA 工作人员开通',
|
|
16
|
+
24052: '请求参数有误,请对照接口文档检查',
|
|
17
|
+
24054: 'AppId 不存在,检查 appId 是否正确、是否用错了环境(sandbox / 线上凭证不通用)',
|
|
18
|
+
24056: '签名错误,检查 appSecret 是否正确',
|
|
19
|
+
24057: '时间戳过期,本机时间与服务端偏差不能超过 20 分钟',
|
|
20
|
+
30001: '服务端错误,常见原因是用错了引擎对应的接口(如对 UE 报告调用了 Unity 专用接口);勿把瞬时失败说成「一定没有数据」',
|
|
21
|
+
};
|
|
22
|
+
/** OpenAPI 已按引擎写好的稳定文案(优先沿用,再补 AI 纪律)。 */
|
|
23
|
+
function hintForOpenApiRuntimeLogMessage(apiMessage) {
|
|
24
|
+
if (/暂不支持获取当前报告的运行日志/.test(apiMessage)) {
|
|
25
|
+
return 'UE 运行日志方案有过切换:只能说暂不支持获取当前报告的运行日志,勿断定报告本身无日志。旧报告可试 gotonline_overview_log_export。';
|
|
26
|
+
}
|
|
27
|
+
if (/运行日志未解析|运行日志解析未完成/.test(apiMessage)) {
|
|
28
|
+
return 'Unity:该报告无可用运行日志数据(未解析或解析未完成)。旧报告可试 gotonline_overview_log_export。';
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function hintForFileMetaReason(rawMessage, apiMessage) {
|
|
33
|
+
const fromApi = hintForOpenApiRuntimeLogMessage(apiMessage);
|
|
34
|
+
if (fromApi)
|
|
35
|
+
return fromApi;
|
|
36
|
+
if (rawMessage.includes(FILE_META_REASONS.LG_META_ABSENT) || rawMessage.includes(FILE_META_REASONS.LG_RUNTIME_PATH_MISSING)) {
|
|
37
|
+
return ('运行日志 file_meta 不可用(原因码见 rawMessage)。' +
|
|
38
|
+
'Unity:通常表示未解析/无可用日志数据;UE:只能说暂不支持获取当前报告的运行日志,勿断定报告本身无日志。' +
|
|
39
|
+
'旧报告可试 gotonline_overview_log_export。');
|
|
40
|
+
}
|
|
41
|
+
if (rawMessage.includes(FILE_META_REASONS.AT_META_ABSENT) ||
|
|
42
|
+
rawMessage.includes(FILE_META_REASONS.AT_META_TYPE_INVALID) ||
|
|
43
|
+
rawMessage.includes(FILE_META_REASONS.AT_META_PATH_MISSING)) {
|
|
44
|
+
return 'AT 资源 meta 不可用:可能未开启资源采集或未解析完成,勿把失败说成「一定没有资源」。';
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** 组装给模型看的排查建议;优先 rawMessage 原因码 + OpenAPI message。 */
|
|
49
|
+
export function resolveErrorHint(code, rawMessage, apiMessage) {
|
|
50
|
+
if (code === 80108) {
|
|
51
|
+
return (hintForFileMetaReason(rawMessage, apiMessage) ??
|
|
52
|
+
'文件 meta 不存在(FILE_META_NOT_EXIST)。运行日志/AT 等场景请结合接口 message 与报告引擎解释;勿臆造数据。');
|
|
53
|
+
}
|
|
54
|
+
return ERROR_HINTS_BY_CODE[code];
|
|
55
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** indicatorDashboards 合法面板传参名称(与 got-query OverviewIndicatorDashboardCatalogService 同源)。 */
|
|
2
2
|
export declare const INDICATOR_DASHBOARD_KEYS: readonly string[];
|
|
3
3
|
/** 常见误传:统计维度后缀 / 子指标名,不是面板标识符。 */
|
|
4
|
-
export declare const INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES: readonly ["fps_mean", "fps_maximum", "frametime_min", "frametime_gt_40_pct", "temperature_mean", "drawcall_maximum"];
|
|
4
|
+
export declare const INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES: readonly ["fps_mean", "fps_maximum", "frametime_min", "frametime_gt_40_pct", "temperature_mean", "drawcall_maximum", "android_pss_max"];
|
|
5
5
|
export declare function splitIndicatorDashboards(raw: unknown): string[];
|
|
6
6
|
export declare function findUnknownIndicatorDashboards(raw: unknown): string[];
|
|
7
7
|
/** 拆成已接受 / 未识别面板名(保持请求顺序,已接受去重)。 */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server instructions:客户裸问时的全局纪律(勿依赖用户粘贴规则)。
|
|
3
|
+
* 与 docs/EMPTY_STATE_CONTRACT.md 对齐。
|
|
4
|
+
*/
|
|
5
|
+
export const SERVER_INSTRUCTIONS = [
|
|
6
|
+
'你是 UWA GOT Online OpenAPI MCP 助手。用人话查报告时遵守以下纪律:',
|
|
7
|
+
'',
|
|
8
|
+
'1. 多份候选报告:先列出备注/设备/时间,再选或先问用户;禁止静默默认却假装只有一份。',
|
|
9
|
+
'2. 没有某类报告(如 GPU/Mono):看 list 返回的 module_available;为 false 时明确说没有并停手。禁止用 Overview 的 RenderTexture / WaitForVsync 冒充 GPU 分析。',
|
|
10
|
+
'3. 说「GPU Bound / GPU 是主要瓶颈」之前:必须先查 gotonline_gpu_bound(或看 report_diagnosis.gpuBound)。空数组 [] 只能说「未检出 Bound」,不能说主要瓶颈是 GPU。',
|
|
11
|
+
'4. 卡顿根因:优先 gotonline_overview_stack_stutter_full_tree_presign / stutter_scene_tree;禁止把 WaitForVsync 直接当根因。尖峰再查单帧树。',
|
|
12
|
+
'5. 非法面板名要纠正(fps_mean→fps_avg;android_pss_max→android_memory_pss@max);数值带单位。PSS 优先读 pss_peak_kb/mb/frame。',
|
|
13
|
+
'6. 接口失败时说明原因:运行日志 80108 — Unity 无 LG file_meta 表示未解析、无可用日志;UE 无 meta 只能说「暂不支持获取当前报告的运行日志」(方案切换过),勿断定报告本身无日志。其它失败勿把瞬时错误说成「一定没有数据」。',
|
|
14
|
+
'',
|
|
15
|
+
'体检优先调用 report_diagnosis;细节再按 _apisUsed 深挖。对比多份报告时说明场景/时长/目标帧是否可比(见 comparability_note)。',
|
|
16
|
+
].join('\n');
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registerCompositeTools, COMPOSITE_TOOLS, selectCompositeTools } from '.
|
|
|
8
8
|
import { selectOperations } from './presets.js';
|
|
9
9
|
import { loadSpec } from './spec.js';
|
|
10
10
|
import { makeHandler, toolConfig, toolName } from './tools.js';
|
|
11
|
+
import { SERVER_INSTRUCTIONS } from './server-instructions.js';
|
|
11
12
|
function readPackageVersion() {
|
|
12
13
|
try {
|
|
13
14
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -28,7 +29,7 @@ export function createServer(opts) {
|
|
|
28
29
|
credentials: { appId: opts.appId, appSecret: opts.appSecret },
|
|
29
30
|
timeoutMs: opts.timeoutMs,
|
|
30
31
|
});
|
|
31
|
-
const server = new McpServer({ name: 'uwa-openapi-mcp', version: PACKAGE_VERSION });
|
|
32
|
+
const server = new McpServer({ name: 'uwa-openapi-mcp', version: PACKAGE_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
32
33
|
for (const op of operations) {
|
|
33
34
|
server.registerTool(toolName(op.id, opts.nameCase, opts.namePrefix), toolConfig(op), makeHandler(op, client, opts.maxRows, opts.maxChars));
|
|
34
35
|
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ export declare function annotateCurveAxes(data: unknown): unknown;
|
|
|
32
32
|
* 自定义面板 statistic/curve 返回空 `{}` 时,优先提示面板名传错(静默失败高发)。
|
|
33
33
|
*/
|
|
34
34
|
export declare function annotateDashboardEmptyResult(op: Operation, data: unknown, args: Record<string, unknown>): unknown;
|
|
35
|
+
/** 各 GOT Online 子模块是否有报告;供模型「无 GPU 停手」。 */
|
|
36
|
+
export declare function buildModuleAvailable(group: Record<string, unknown>): Record<string, boolean>;
|
|
37
|
+
export declare function annotateModuleAvailable(data: unknown): unknown;
|
|
35
38
|
/**
|
|
36
39
|
* 报告列表接口一次返回、按项目组分组。但上游会把同一份报告挂到每个项目组下
|
|
37
40
|
* (沙箱实测多组两两交集 100%),直接按组统计会成倍放大。
|
|
@@ -40,6 +43,8 @@ export declare function annotateDashboardEmptyResult(op: Operation, data: unknow
|
|
|
40
43
|
* 不额外打接口。上游修好后检测不到重复,本函数原样返回。
|
|
41
44
|
*/
|
|
42
45
|
export declare function annotateProjectGroups(data: unknown): unknown;
|
|
46
|
+
/** 空 Bound / 空日志列表等:补可教模型的语义,避免误判。 */
|
|
47
|
+
export declare function annotateEmptyStateSemantics(op: Operation, data: unknown): unknown;
|
|
43
48
|
export declare function makeHandler(op: Operation, client: UwaClient, defaultMaxRows: number, maxChars: number): (args: Record<string, unknown>) => Promise<ToolResult>;
|
|
44
49
|
export declare function toolConfig(op: Operation): {
|
|
45
50
|
title: string;
|
package/dist/tools.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { UwaApiError } from './client.js';
|
|
3
3
|
import { INDICATOR_DASHBOARD_PANEL_GUIDE, versionGuideFor } from './version-guide.js';
|
|
4
4
|
import { partitionIndicatorDashboards, INDICATOR_DASHBOARD_KEYS, INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES, } from './indicator-dashboard-keys.js';
|
|
5
|
-
import { annotateStatisticDashboard, extractCurveFilterArgs, filterCurveData, hasCurveFilter, isIndicatorCurveDashboard, isIndicatorStatisticDashboard, } from './annotate-dashboard.js';
|
|
5
|
+
import { annotateStatisticDashboard, annotateOverviewStatisticPss, extractCurveFilterArgs, filterCurveData, hasCurveFilter, isIndicatorCurveDashboard, isIndicatorStatisticDashboard, isOverviewStatisticOp, stutterRootCauseGate, stripOuterXAxisWhenFramesOnly, } from './annotate-dashboard.js';
|
|
6
6
|
const ENGINE_LABEL = { unity: 'Unity', unreal: 'UE' };
|
|
7
7
|
/**
|
|
8
8
|
* 预签名数据默认完整返回,不按条数截断——实测各接口解压后在 10 KB ~ 500 KB 之间,
|
|
@@ -331,6 +331,41 @@ export function annotateDashboardEmptyResult(op, data, args) {
|
|
|
331
331
|
};
|
|
332
332
|
}
|
|
333
333
|
const RECORD_ARRAY_KEYS = /^gotOnline.*Records$/;
|
|
334
|
+
function hasNonEmptyRecords(v) {
|
|
335
|
+
return Array.isArray(v) && v.length > 0;
|
|
336
|
+
}
|
|
337
|
+
/** 各 GOT Online 子模块是否有报告;供模型「无 GPU 停手」。 */
|
|
338
|
+
export function buildModuleAvailable(group) {
|
|
339
|
+
return {
|
|
340
|
+
overview: hasNonEmptyRecords(group['gotOnlineOverviewRecords']),
|
|
341
|
+
gpu: hasNonEmptyRecords(group['gotOnlineGPURecords']),
|
|
342
|
+
mono: hasNonEmptyRecords(group['gotOnlineMonoRecords']),
|
|
343
|
+
lua: hasNonEmptyRecords(group['gotOnlineLuaRecords']),
|
|
344
|
+
resource: hasNonEmptyRecords(group['gotOnlineResourceRecords']),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
export function annotateModuleAvailable(data) {
|
|
348
|
+
if (!isObj(data) || !Array.isArray(data['content']))
|
|
349
|
+
return data;
|
|
350
|
+
const content = data['content'].map((g) => {
|
|
351
|
+
if (!isObj(g))
|
|
352
|
+
return g;
|
|
353
|
+
const module_available = buildModuleAvailable(g);
|
|
354
|
+
const missing = Object.entries(module_available)
|
|
355
|
+
.filter(([, ok]) => !ok)
|
|
356
|
+
.map(([k]) => k);
|
|
357
|
+
return {
|
|
358
|
+
...g,
|
|
359
|
+
module_available,
|
|
360
|
+
...(missing.length
|
|
361
|
+
? {
|
|
362
|
+
_moduleHint: `本项目组缺少模块:${missing.join('、')}。module_available 为 false 的模块请停手,禁止用 Overview 数据冒充分析。`,
|
|
363
|
+
}
|
|
364
|
+
: {}),
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
return { ...data, content };
|
|
368
|
+
}
|
|
334
369
|
/**
|
|
335
370
|
* 报告列表接口一次返回、按项目组分组。但上游会把同一份报告挂到每个项目组下
|
|
336
371
|
* (沙箱实测多组两两交集 100%),直接按组统计会成倍放大。
|
|
@@ -340,10 +375,10 @@ const RECORD_ARRAY_KEYS = /^gotOnline.*Records$/;
|
|
|
340
375
|
*/
|
|
341
376
|
export function annotateProjectGroups(data) {
|
|
342
377
|
if (!isObj(data) || !Array.isArray(data['content']))
|
|
343
|
-
return data;
|
|
378
|
+
return annotateModuleAvailable(data);
|
|
344
379
|
const groups = data['content'].filter(isObj);
|
|
345
380
|
if (groups.length < 2)
|
|
346
|
-
return data;
|
|
381
|
+
return annotateModuleAvailable(data);
|
|
347
382
|
// dataKey → 出现过的项目组;以及「该报告在哪个数组字段里、原文是什么」
|
|
348
383
|
const groupsOf = new Map();
|
|
349
384
|
const firstSeen = new Map();
|
|
@@ -372,7 +407,7 @@ export function annotateProjectGroups(data) {
|
|
|
372
407
|
}
|
|
373
408
|
const duplicated = [...groupsOf.values()].some((gs) => gs.size > 1);
|
|
374
409
|
if (!duplicated)
|
|
375
|
-
return data;
|
|
410
|
+
return annotateModuleAvailable(data);
|
|
376
411
|
// 已知项目组壳子(保留名称、engine 等元信息),报告重新按真实归属灌入
|
|
377
412
|
const shells = new Map();
|
|
378
413
|
for (const g of groups) {
|
|
@@ -407,7 +442,7 @@ export function annotateProjectGroups(data) {
|
|
|
407
442
|
}
|
|
408
443
|
// 丢掉合并后一个报告都没有的空组(它们原先只有别人的副本)
|
|
409
444
|
const content = [...shells.values()].filter((g) => Object.entries(g).some(([k, v]) => RECORD_ARRAY_KEYS.test(k) && Array.isArray(v) && v.length > 0));
|
|
410
|
-
return {
|
|
445
|
+
return annotateModuleAvailable({
|
|
411
446
|
...data,
|
|
412
447
|
content,
|
|
413
448
|
_deduplicated: true,
|
|
@@ -415,7 +450,7 @@ export function annotateProjectGroups(data) {
|
|
|
415
450
|
_dedupeNote: `上游把同一份报告重复挂在多个项目组下,已按 link 中 project= 合并归位,去重后本页 ${placed} 份` +
|
|
416
451
|
(orphaned ? `(其中 ${orphaned} 份所属项目组不在本页,已单独列出)` : '') +
|
|
417
452
|
'。按项目组统计请用本页 _uniqueReportCount / 各组 Records;跨页汇总须自行按 dataKey 去重,勿把各页条数简单相加。',
|
|
418
|
-
};
|
|
453
|
+
});
|
|
419
454
|
}
|
|
420
455
|
/** MCP 的 content.text 必须是字符串,undefined 会让客户端校验失败。 */
|
|
421
456
|
function asText(value) {
|
|
@@ -425,6 +460,44 @@ function asText(value) {
|
|
|
425
460
|
return '(接口无返回内容)';
|
|
426
461
|
return JSON.stringify(value, null, 2);
|
|
427
462
|
}
|
|
463
|
+
/** 空 Bound / 空日志列表等:补可教模型的语义,避免误判。 */
|
|
464
|
+
export function annotateEmptyStateSemantics(op, data) {
|
|
465
|
+
if (op.id === 'gotonline_gpu_bound') {
|
|
466
|
+
const intervals = Array.isArray(data) ? data : isObj(data) && Array.isArray(data['data']) ? data['data'] : null;
|
|
467
|
+
if (intervals && intervals.length === 0) {
|
|
468
|
+
const base = isObj(data) ? data : { data: intervals };
|
|
469
|
+
return {
|
|
470
|
+
...base,
|
|
471
|
+
gpuBound: [],
|
|
472
|
+
forbidden_claims: ['GPU是主要瓶颈', 'GPU Bound明显', 'GPU主瓶颈'],
|
|
473
|
+
_hint: 'GPU Bound 为空数组:仅表示在当前 targetFps 下未检出 Bound 区间,不等于 GPU 完全无压力;禁止据此断言 GPU 是主要瓶颈。',
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (op.id === 'gotonline_overview_log_export') {
|
|
478
|
+
const empty = (Array.isArray(data) && data.length === 0) ||
|
|
479
|
+
(isObj(data) && Array.isArray(data['data']) && data['data'].length === 0);
|
|
480
|
+
if (empty) {
|
|
481
|
+
const base = isObj(data) ? data : { entries: data };
|
|
482
|
+
return {
|
|
483
|
+
...base,
|
|
484
|
+
_hint: '运行日志 1.0 返回空列表:可能是真无条目,也可能是新报告应改用 gotonline_lg_runtime_log_entries_presign(日志 2.0)。禁止说成「一定没有日志」。',
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (op.id === 'gotonline_overview_event_lower_memory_frames') {
|
|
489
|
+
const empty = (Array.isArray(data) && data.length === 0) ||
|
|
490
|
+
(isObj(data) && Array.isArray(data['data']) && data['data'].length === 0);
|
|
491
|
+
if (empty) {
|
|
492
|
+
const base = isObj(data) ? data : { frames: data };
|
|
493
|
+
return {
|
|
494
|
+
...base,
|
|
495
|
+
_hint: '未触发 Low Memory 事件(空数组)。不能推出内存完全没问题,请结合 PSS 峰值等综合判断。',
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return data;
|
|
500
|
+
}
|
|
428
501
|
export function makeHandler(op, client, defaultMaxRows, maxChars) {
|
|
429
502
|
return async (args) => {
|
|
430
503
|
try {
|
|
@@ -480,6 +553,9 @@ export function makeHandler(op, client, defaultMaxRows, maxChars) {
|
|
|
480
553
|
if (isIndicatorStatisticDashboard(op.path) && acceptedDashboards.length) {
|
|
481
554
|
processed = annotateStatisticDashboard(processed, acceptedDashboards, unknownDashboards);
|
|
482
555
|
}
|
|
556
|
+
else if (isOverviewStatisticOp(op.id, op.path)) {
|
|
557
|
+
processed = annotateOverviewStatisticPss(processed);
|
|
558
|
+
}
|
|
483
559
|
else if (isIndicatorCurveDashboard(op.path)) {
|
|
484
560
|
if (acceptedDashboards.length || unknownDashboards.length) {
|
|
485
561
|
processed = isObj(processed)
|
|
@@ -492,10 +568,20 @@ export function makeHandler(op, client, defaultMaxRows, maxChars) {
|
|
|
492
568
|
}
|
|
493
569
|
if (hasCurveFilter(curveFilter)) {
|
|
494
570
|
processed = filterCurveData(processed, curveFilter);
|
|
495
|
-
|
|
571
|
+
if (curveFilter.returnFramesOnly) {
|
|
572
|
+
processed = stripOuterXAxisWhenFramesOnly(processed, curveFilter);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
processed = annotateCurveAxes(processed);
|
|
576
|
+
}
|
|
496
577
|
}
|
|
497
578
|
}
|
|
498
|
-
|
|
579
|
+
let data = annotateDashboardEmptyResult(op, processed, args);
|
|
580
|
+
data = annotateEmptyStateSemantics(op, data);
|
|
581
|
+
const stutterGate = stutterRootCauseGate(op.id);
|
|
582
|
+
if (stutterGate && isObj(data)) {
|
|
583
|
+
data = { ...data, ...stutterGate };
|
|
584
|
+
}
|
|
499
585
|
const presignUrl = op.returnsPresignUrl && data && typeof data === 'object'
|
|
500
586
|
? data['dataPresignUrl']
|
|
501
587
|
: undefined;
|
|
@@ -533,6 +619,7 @@ export function makeHandler(op, client, defaultMaxRows, maxChars) {
|
|
|
533
619
|
}
|
|
534
620
|
const meta = {
|
|
535
621
|
...data,
|
|
622
|
+
...(stutterRootCauseGate(op.id) ?? {}),
|
|
536
623
|
_downloaded: true,
|
|
537
624
|
_format: payload.kind,
|
|
538
625
|
...(total !== undefined ? { _totalItems: total } : {}),
|
package/dist/version-guide.js
CHANGED
|
@@ -17,8 +17,9 @@ export const INDICATOR_DASHBOARD_PANEL_GUIDE = [
|
|
|
17
17
|
' · 曲线接口(indicator_curve / custom_dashboard)→ 各子指标逐帧曲线;',
|
|
18
18
|
' · 统计 2.0(indicator_statistic_dashboard)→ statistic 下 *_mean / *_maximum / *_min 及 scene_* 等全量统计。',
|
|
19
19
|
'- 查温度曲线/均值/峰值:Unity Overview 且 SDK ≥ 2.5.1 时传 indicatorDashboards=temperature_max,再读 y_axis 或 statistic;勿仅看 get_overview_statistic 的汇总段。',
|
|
20
|
-
'- 合法面板名先调 gotonline_overview_indicator_dashboard_keys(全量枚举);勿传 fps_mean、frametime_min 等统计字段名。',
|
|
21
|
-
'- 误传示例:fps_mean、frametime_gt_40_pct(缺 @)、frametime_min;合法示例:fps_avg、frametime@gt_40_pct
|
|
20
|
+
'- 合法面板名先调 gotonline_overview_indicator_dashboard_keys(全量枚举);勿传 fps_mean、frametime_min、android_pss_max 等统计字段名。',
|
|
21
|
+
'- 误传示例:fps_mean、android_pss_max、frametime_gt_40_pct(缺 @)、frametime_min;合法示例:fps_avg、android_memory_pss@max、frametime@gt_40_pct。',
|
|
22
|
+
'- Android PSS 峰值:只认面板 android_memory_pss@max;优先读返回顶层 pss_peak_kb / pss_peak_mb / pss_peak_frame(单位 KB)。',
|
|
22
23
|
'- UE Overview 当前 Open API 未开放 custom/indicator dashboard 路径(会 24052),UE 温度汇总见 get_ue_overview_statistic_v2 的 temperature 段。',
|
|
23
24
|
].join('\n');
|
|
24
25
|
/**
|
|
@@ -115,11 +116,19 @@ export const VERSION_GUIDE = {
|
|
|
115
116
|
'【选用规则——务必先看】',
|
|
116
117
|
'- 运行日志 1.0。Unity:解析日 < 2026-07-09;UE:提交日 ≤ 2026-03-25。',
|
|
117
118
|
'- 新报告请用 gotonline_lg_runtime_log_entries_presign(运行日志 2.0)。',
|
|
119
|
+
'- 空列表 ≠「一定没有日志」:新报告误用本接口也可能返回空,应改调 2.0。',
|
|
118
120
|
].join('\n'),
|
|
119
121
|
gotonline_lg_runtime_log_entries_presign: [
|
|
120
122
|
'【选用规则——务必先看】',
|
|
121
123
|
'- 运行日志 2.0(预签名)。解析日 ≥ 2026-07-09 且 SDK ≥ 2.5.1。',
|
|
122
124
|
'- 更早的报告请用 gotonline_overview_log_export。',
|
|
125
|
+
'- 若返回 80108 FILE_META_NOT_EXIST:Unity = 无 LG meta / 未解析 = 无可用日志数据;UE = 方案切换过,只能说「暂不支持获取当前报告的运行日志」,勿断定报告本身无日志。',
|
|
126
|
+
].join('\n'),
|
|
127
|
+
gotonline_gpu_bound: [
|
|
128
|
+
'【选用规则——务必先看】',
|
|
129
|
+
'- 说「GPU Bound / GPU 主瓶颈」前必须先调本工具。',
|
|
130
|
+
'- 返回 [] 只能说「未检出 Bound」,禁止断言 GPU 是主要瓶颈。',
|
|
131
|
+
'- Overview 上的 WaitForVsync / RenderTexture 不能替代本接口。',
|
|
123
132
|
].join('\n'),
|
|
124
133
|
gotonline_overview_group_export: [
|
|
125
134
|
'【选用规则——务必先看】',
|
|
@@ -184,12 +193,15 @@ export const VERSION_GUIDE = {
|
|
|
184
193
|
gotonline_overview_stack_stutter_full_tree_presign: [
|
|
185
194
|
'【选用规则——务必先看】',
|
|
186
195
|
'- **全部卡顿帧已合并**的调用堆栈树 = 卡顿根因主入口(服务端聚合,无需客户端逐帧拉树再聚类)。',
|
|
196
|
+
'- 禁止把 WaitForVsync / Gfx.WaitForPresent 写成卡顿根因或 GPU Bound 证据。',
|
|
197
|
+
'- 说「GPU 主瓶颈」前必须先查 gotonline_gpu_bound;空数组只能说「未检出 Bound」。',
|
|
187
198
|
'- 只要某一场景:用 stutter_scene_tree;只要某一尖峰帧细节:再用 stack_tree_frame。',
|
|
188
199
|
STACK_ANALYSIS_GUIDE,
|
|
189
200
|
].join('\n'),
|
|
190
201
|
gotonline_overview_stack_stutter_scene_tree_presign: [
|
|
191
202
|
'【选用规则——务必先看】',
|
|
192
203
|
'- **指定场景内卡顿帧已合并**的堆栈树;sceneIndex 来自 list_scenes 下标。',
|
|
204
|
+
'- 禁止把 WaitForVsync 当根因;GPU 主瓶颈结论须先查 gpu_bound。',
|
|
193
205
|
'- 全报告卡顿用 stutter_full_tree;单帧深挖用 stack_tree_frame。',
|
|
194
206
|
STACK_ANALYSIS_GUIDE,
|
|
195
207
|
].join('\n'),
|