@uwa4d/openapi-mcp 0.2.0-beta.5 → 0.2.0-beta.8
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 +1 -5
- package/dist/annotate-dashboard.d.ts +34 -0
- package/dist/annotate-dashboard.js +275 -0
- package/dist/composite/name-pattern.d.ts +8 -0
- package/dist/composite/name-pattern.js +25 -0
- package/dist/composite/report-diagnosis.js +42 -2
- package/dist/composite/stack-agg.d.ts +15 -3
- package/dist/composite/stack-agg.js +154 -2
- 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 +7 -2
- package/dist/composite/top-functions.js +74 -29
- package/dist/indicator-dashboard-keys.d.ts +12 -0
- package/dist/indicator-dashboard-keys.js +49 -0
- package/dist/indicator-dashboard-keys.json +338 -0
- package/dist/presets.js +5 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +135 -11
- package/dist/version-guide.d.ts +5 -0
- package/dist/version-guide.js +101 -1
- package/package.json +2 -2
- package/spec/uwa-openapi.json +490 -106
package/README.md
CHANGED
|
@@ -78,7 +78,6 @@ npx -y @uwa4d/openapi-mcp check -a <app_id> -s <app_secret>
|
|
|
78
78
|
| `--app-id` | `-a` | AppId | `UWA_MCP_APP_ID` |
|
|
79
79
|
| `--app-secret` | `-s` | AppSecret | `UWA_MCP_APP_SECRET` |
|
|
80
80
|
| `--base-url` | `-b` | 接口地址,默认 `https://secure-api.uwa4d.com` | `UWA_MCP_API_BASE_URL` |
|
|
81
|
-
| `--sandbox` | | 切到测试环境 `https://sandbox-api.uwa4d.com` | |
|
|
82
81
|
| `--tool` | `-t` | 加载哪些工具,逗号分隔,默认 `preset.default` | `UWA_MCP_TOOL` |
|
|
83
82
|
| `--engine` | `-e` | 只加载适用于 `unity` / `unreal` 的工具,默认 `all` | `UWA_MCP_ENGINE` |
|
|
84
83
|
| `--tool-name-case` | `-c` | 工具命名风格 `snake` / `camel`,默认 `snake` | |
|
|
@@ -192,9 +191,6 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
|
192
191
|
| --- | --- | --- |
|
|
193
192
|
| `@uwa4d/openapi-mcp` | 跟随最新稳定版,自动升级 | 默认,大多数用户 |
|
|
194
193
|
| `@uwa4d/openapi-mcp@0.1.0` | 锁死某个版本 | 生产流水线,要求完全可复现 |
|
|
195
|
-
| `@uwa4d/openapi-mcp@beta` | 跟随测试版 | 配合 UWA 验证新接口 |
|
|
196
|
-
|
|
197
|
-
测试版只发布在 `beta` 标签下,默认写法不会拉到测试版。
|
|
198
194
|
|
|
199
195
|
## 常见问题
|
|
200
196
|
|
|
@@ -202,7 +198,7 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
|
202
198
|
检查 Node 版本是否 ≥ 18,以及配置文件 JSON 格式是否正确(多一个逗号就会整个失效)。改完配置需要重启客户端。
|
|
203
199
|
|
|
204
200
|
**提示凭证错误**
|
|
205
|
-
用 `check`
|
|
201
|
+
用 `check` 命令在终端单独验证一次,排除是客户端配置传参的问题。确认 AppId / AppSecret 无误且未过期。
|
|
206
202
|
|
|
207
203
|
**返回「数据服务错误」或不确定该用 v1 还是 v2**
|
|
208
204
|
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
export declare function isIndicatorStatisticDashboard(opPath: string): boolean;
|
|
32
|
+
export declare function isIndicatorCurveDashboard(opPath: string): boolean;
|
|
33
|
+
/** 统计面板成功返回后的统一标注管线。 */
|
|
34
|
+
export declare function annotateStatisticDashboard(data: unknown, accepted: string[], unknown: string[]): unknown;
|
|
@@ -0,0 +1,275 @@
|
|
|
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
|
+
export function isIndicatorStatisticDashboard(opPath) {
|
|
258
|
+
return opPath.includes('/indicator/statistic/dashboard') || opPath.includes('/custom/dashboard');
|
|
259
|
+
}
|
|
260
|
+
export function isIndicatorCurveDashboard(opPath) {
|
|
261
|
+
return opPath.includes('/indicator/curve/dashboard') || opPath.includes('/gpu/curve');
|
|
262
|
+
}
|
|
263
|
+
/** 统计面板成功返回后的统一标注管线。 */
|
|
264
|
+
export function annotateStatisticDashboard(data, accepted, unknown) {
|
|
265
|
+
let out = annotateSceneUnits(data);
|
|
266
|
+
out = annotateDuplicateFlatAliases(out);
|
|
267
|
+
out = annotateParamFieldMap(out);
|
|
268
|
+
if (!isObj(out))
|
|
269
|
+
return out;
|
|
270
|
+
return {
|
|
271
|
+
...out,
|
|
272
|
+
_acceptedDashboards: accepted,
|
|
273
|
+
...(unknown.length ? { _unknownDashboards: unknown } : {}),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 按函数名过滤(子串或 /regex/flags),供 top_functions 本地过滤 idmap 结果。 */
|
|
2
|
+
export declare function matchesNamePattern(name: string, pattern?: unknown): boolean;
|
|
3
|
+
export declare function filterRowsByNamePattern<T extends {
|
|
4
|
+
name: string;
|
|
5
|
+
}>(rows: T[], pattern: unknown): {
|
|
6
|
+
rows: T[];
|
|
7
|
+
matchedTotal: number;
|
|
8
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** 按函数名过滤(子串或 /regex/flags),供 top_functions 本地过滤 idmap 结果。 */
|
|
2
|
+
export function matchesNamePattern(name, pattern) {
|
|
3
|
+
if (pattern == null || (typeof pattern === 'string' && !pattern.trim()))
|
|
4
|
+
return true;
|
|
5
|
+
const p = String(pattern).trim();
|
|
6
|
+
if (p.length >= 2 && p.startsWith('/') && p.lastIndexOf('/') > 0) {
|
|
7
|
+
const last = p.lastIndexOf('/');
|
|
8
|
+
const body = p.slice(1, last);
|
|
9
|
+
const flags = p.slice(last + 1);
|
|
10
|
+
try {
|
|
11
|
+
return new RegExp(body, flags).test(name);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return name.toLowerCase().includes(p.toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return name.toLowerCase().includes(p.toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
export function filterRowsByNamePattern(rows, pattern) {
|
|
20
|
+
if (pattern == null || (typeof pattern === 'string' && !pattern.trim())) {
|
|
21
|
+
return { rows, matchedTotal: rows.length };
|
|
22
|
+
}
|
|
23
|
+
const matched = rows.filter((r) => matchesNamePattern(r.name, pattern));
|
|
24
|
+
return { rows: matched, matchedTotal: matched.length };
|
|
25
|
+
}
|
|
@@ -20,6 +20,25 @@ function summarizeScenes(data) {
|
|
|
20
20
|
}
|
|
21
21
|
return { keys: Object.keys(obj).slice(0, 30) };
|
|
22
22
|
}
|
|
23
|
+
/** list_scenes 返回起止帧;归一成 sceneIdx + sceneName + frameStart/End。 */
|
|
24
|
+
function normalizeSceneList(data) {
|
|
25
|
+
const arr = Array.isArray(data)
|
|
26
|
+
? data
|
|
27
|
+
: data && typeof data === 'object' && Array.isArray(data['scenes'])
|
|
28
|
+
? data['scenes']
|
|
29
|
+
: [];
|
|
30
|
+
return arr.slice(0, 50).map((item, idx) => {
|
|
31
|
+
if (!item || typeof item !== 'object')
|
|
32
|
+
return { sceneIdx: idx, raw: item };
|
|
33
|
+
const o = item;
|
|
34
|
+
return {
|
|
35
|
+
sceneIdx: idx,
|
|
36
|
+
sceneName: o['sceneName'] ?? o['name'] ?? null,
|
|
37
|
+
frameStart: o['frameStart'] ?? o['startFrame'] ?? null,
|
|
38
|
+
frameEnd: o['frameEnd'] ?? o['endFrame'] ?? null,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
23
42
|
/** brief 全量可能很长,体检只保留常见关键指标(含 Unity key 与 UE 中文 label)。 */
|
|
24
43
|
function pickImportantBrief(brief) {
|
|
25
44
|
const preferExact = [
|
|
@@ -66,7 +85,10 @@ export const reportDiagnosisTool = {
|
|
|
66
85
|
'对单份 GOT Online 报告做一次结构化体检:自动选对 Overview 统计 v1/v2,并串联 Top 资源、Top 函数与场景摘要。',
|
|
67
86
|
'适用引擎:Unity / UE|复合工具',
|
|
68
87
|
'调用方不必自己判断 2026-07-09 / 2026-03-25 分界或 SDK 版本——内部按 get_report_detail 路由。',
|
|
69
|
-
'Top
|
|
88
|
+
'Top 函数:Overview 走 method/idmap/statistic v1.0.2 + stackTestMode;Mono 走堆栈树;逐帧曲线用 method/curve/presign v1.0.2。',
|
|
89
|
+
'查面板统计/曲线前先调 gotonline_overview_indicator_dashboard_keys 核对 indicatorDashboards(勿把 fps_mean 等统计字段当面板名)。',
|
|
90
|
+
'卡顿根因:stutter_full_tree / stutter_scene_tree(卡顿帧已合并的树);尖峰再 stack_tree_frame;勿对每个慢帧循环拉帧树。',
|
|
91
|
+
'scenes 含 list_scenes 起止帧(sceneIdx/sceneName/frameStart/frameEnd)。',
|
|
70
92
|
'返回摘要而非原始大包;需要明细时再按 _apisUsed 中的原子工具深挖。',
|
|
71
93
|
].join('\n'),
|
|
72
94
|
inputSchema: {
|
|
@@ -120,12 +142,30 @@ export const reportDiagnosisTool = {
|
|
|
120
142
|
apisUsed.push(scene.target.id);
|
|
121
143
|
}
|
|
122
144
|
else {
|
|
123
|
-
limitations.push('当前引擎无 Unity
|
|
145
|
+
limitations.push('当前引擎无 Unity 场景统计路由,已跳过场景统计摘要');
|
|
124
146
|
}
|
|
125
147
|
}
|
|
126
148
|
catch (e) {
|
|
127
149
|
limitations.push(`场景统计失败:${e instanceof Error ? e.message : String(e)}`);
|
|
128
150
|
}
|
|
151
|
+
// list_scenes 补起止帧(场景统计摘要经常只有名称/索引)
|
|
152
|
+
try {
|
|
153
|
+
const keyQuery = {};
|
|
154
|
+
if (identity.dataKey)
|
|
155
|
+
keyQuery['dataKey'] = identity.dataKey;
|
|
156
|
+
else if (identity.recordId)
|
|
157
|
+
keyQuery['recordId'] = identity.recordId;
|
|
158
|
+
const sceneList = await ctx.client.call('GET', '/openapi/v1/data/gotonline/scene/info', 'v1.0.1', keyQuery);
|
|
159
|
+
const list = normalizeSceneList(sceneList);
|
|
160
|
+
if (list.length) {
|
|
161
|
+
scenes = { ...(scenes ?? {}), sceneCount: list.length, scenes: list };
|
|
162
|
+
if (!apisUsed.includes('list_scenes'))
|
|
163
|
+
apisUsed.push('list_scenes');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
limitations.push(`场景列表(list_scenes)失败:${e instanceof Error ? e.message : String(e)}`);
|
|
168
|
+
}
|
|
129
169
|
// 仅在明确未开启时提示;hasResource === null 表示无法判定,不误报关闭
|
|
130
170
|
if (view.hasResource === false) {
|
|
131
171
|
limitations.push('未开启资源采集,逐资源 Top 可能为空');
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { UwaClient } from '../client.js';
|
|
2
2
|
import type { ReportIdentity } from './route-overview.js';
|
|
3
|
+
import { type OverviewStackTestMode } from './stack-test-mode.js';
|
|
4
|
+
export type { OverviewStackTestMode, OverviewStackTestModeProfile } from './stack-test-mode.js';
|
|
3
5
|
export type StackMetric = 'selfTime' | 'totalTime' | 'callCount' | 'selfMemory';
|
|
4
6
|
export interface StackAggRow {
|
|
5
7
|
methodId: string;
|
|
@@ -18,15 +20,25 @@ export interface StackAggRow {
|
|
|
18
20
|
}
|
|
19
21
|
export interface StackAggResult {
|
|
20
22
|
testMode: string | null;
|
|
23
|
+
/** 仅 Overview:堆栈采集模式 CPU_ONLY / LUA_MEM_ONLY / CPU_AND_LUA_MEM */
|
|
24
|
+
stackTestMode?: OverviewStackTestMode | null;
|
|
25
|
+
availableMetrics?: StackMetric[];
|
|
26
|
+
availableFields?: string[];
|
|
27
|
+
metricGuide?: string;
|
|
21
28
|
metric: StackMetric;
|
|
22
29
|
unit: string;
|
|
23
30
|
rows: Array<{
|
|
24
31
|
name: string;
|
|
25
32
|
methodId: string;
|
|
33
|
+
stackMethodId?: string;
|
|
26
34
|
value: number;
|
|
27
35
|
unit: string;
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
selfTimeMean?: number | null;
|
|
37
|
+
totalTimeMean?: number | null;
|
|
38
|
+
callCountTotal?: number | null;
|
|
39
|
+
selfMemoryMean?: number | null;
|
|
40
|
+
selfMemoryTotal?: number | null;
|
|
41
|
+
selfMemoryUnit?: string | null;
|
|
30
42
|
selfMemoryKb?: number;
|
|
31
43
|
callCount: number;
|
|
32
44
|
callFrameCount: number;
|
|
@@ -48,6 +60,7 @@ interface TreeNode {
|
|
|
48
60
|
totalMemoryKb: number;
|
|
49
61
|
selfMemoryKb: number;
|
|
50
62
|
}
|
|
63
|
+
export declare function aggregateOverviewStackFromStatistic(client: UwaClient, identity: ReportIdentity, topN: number, metricInput?: StackMetric, threadName?: string, namePattern?: string): Promise<StackAggResult>;
|
|
51
64
|
/**
|
|
52
65
|
* Overview:线程堆栈树(主线程)+ 函数 idmap → 按函数名合并节点。
|
|
53
66
|
* 同名(同一 methodId)节点的 selfTime / callCount 相加,再按指标排序取 Top N。
|
|
@@ -62,4 +75,3 @@ export declare function aggregateMonoStack(client: UwaClient, identity: ReportId
|
|
|
62
75
|
* 解析无表头 CSV。列数随 testMode 变化;UE 固定 7 列(与 CPU_ONLY 同形)。
|
|
63
76
|
*/
|
|
64
77
|
export declare function parseStackCsv(csvText: string, testMode: string | null): TreeNode[];
|
|
65
|
-
export {};
|
|
@@ -1,4 +1,155 @@
|
|
|
1
1
|
import { isObj } from './helpers.js';
|
|
2
|
+
import { assertOverviewMetricSupported, inferOverviewStackTestMode, normalizeOverviewStackTestMode, OVERVIEW_STACK_TEST_MODE_PROFILES, resolveOverviewDefaultMetric, } from './stack-test-mode.js';
|
|
3
|
+
import { matchesNamePattern } from './name-pattern.js';
|
|
4
|
+
/**
|
|
5
|
+
* Overview:函数 idmap 统计(v1.0.2)+ Overview stackTestMode → Top N。
|
|
6
|
+
* stackTestMode 仅 Overview 有效;决定哪些统计字段/metric 可用。
|
|
7
|
+
*/
|
|
8
|
+
function parseStatNumber(obj) {
|
|
9
|
+
if (!isObj(obj) || obj['value'] == null)
|
|
10
|
+
return null;
|
|
11
|
+
const n = Number(obj['value']);
|
|
12
|
+
return Number.isFinite(n) ? n : null;
|
|
13
|
+
}
|
|
14
|
+
function parseStatUnit(obj) {
|
|
15
|
+
if (!isObj(obj))
|
|
16
|
+
return null;
|
|
17
|
+
const unit = obj['unit'];
|
|
18
|
+
if (!isObj(unit) || unit['value'] == null)
|
|
19
|
+
return null;
|
|
20
|
+
return String(unit['value']);
|
|
21
|
+
}
|
|
22
|
+
async function fetchOverviewStackTestMode(client, identity) {
|
|
23
|
+
if (identity.engine === 'unreal') {
|
|
24
|
+
return { stackTestMode: 'CPU_ONLY', apisUsed: [] };
|
|
25
|
+
}
|
|
26
|
+
const keyQuery = identity.dataKey
|
|
27
|
+
? { dataKey: identity.dataKey }
|
|
28
|
+
: { recordId: identity.recordId };
|
|
29
|
+
try {
|
|
30
|
+
const meta = (await client.call('GET', '/openapi/v1/data/gotonline/overview/stack/overall/tree/presign', 'v1.0.1', keyQuery));
|
|
31
|
+
return {
|
|
32
|
+
stackTestMode: normalizeOverviewStackTestMode(meta['testMode']),
|
|
33
|
+
apisUsed: ['gotonline_overview_stack_overall_tree_presign'],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { stackTestMode: null, apisUsed: [] };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function aggregateOverviewStackFromStatistic(client, identity, topN, metricInput, threadName, namePattern) {
|
|
41
|
+
const keyQuery = identity.dataKey
|
|
42
|
+
? { dataKey: identity.dataKey }
|
|
43
|
+
: { recordId: identity.recordId };
|
|
44
|
+
if (identity.engine === 'unreal') {
|
|
45
|
+
keyQuery['threadName'] = threadName?.trim() || 'GameThread';
|
|
46
|
+
}
|
|
47
|
+
const apisUsed = [];
|
|
48
|
+
const [{ stackTestMode: presignMode, apisUsed: presignApis }, raw] = await Promise.all([
|
|
49
|
+
fetchOverviewStackTestMode(client, identity),
|
|
50
|
+
client.call('GET', '/openapi/v1/data/gotonline/overview/method/idmap/statistic', 'v1.0.2', keyQuery),
|
|
51
|
+
]);
|
|
52
|
+
apisUsed.push(...presignApis, 'gotonline_overview_method_idmap_statistic');
|
|
53
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
54
|
+
if (!list.length) {
|
|
55
|
+
throw new Error('函数 idmap 统计返回空列表(报告可能未开堆栈或 SDK < 2.5.1)');
|
|
56
|
+
}
|
|
57
|
+
const rows = [];
|
|
58
|
+
for (const item of list) {
|
|
59
|
+
if (!isObj(item))
|
|
60
|
+
continue;
|
|
61
|
+
const methodId = item['stackMethodId'] != null ? String(item['stackMethodId']) : '';
|
|
62
|
+
const name = item['stackMethodName'] != null ? String(item['stackMethodName']) : methodId;
|
|
63
|
+
if (!methodId)
|
|
64
|
+
continue;
|
|
65
|
+
const memMeanObj = item['selfMemoryMean'];
|
|
66
|
+
const memTotalObj = item['selfMemoryTotal'];
|
|
67
|
+
const selfTimeMean = parseStatNumber(item['selfTimeMean']);
|
|
68
|
+
const totalTimeMean = parseStatNumber(item['totalTimeMean']);
|
|
69
|
+
const callCountTotal = parseStatNumber(item['callCountTotal']);
|
|
70
|
+
const selfMemoryMean = parseStatNumber(memMeanObj);
|
|
71
|
+
const selfMemoryTotal = parseStatNumber(memTotalObj);
|
|
72
|
+
const selfMemoryUnit = parseStatUnit(memMeanObj) ?? parseStatUnit(memTotalObj);
|
|
73
|
+
if (selfTimeMean == null &&
|
|
74
|
+
totalTimeMean == null &&
|
|
75
|
+
callCountTotal == null &&
|
|
76
|
+
selfMemoryMean == null &&
|
|
77
|
+
selfMemoryTotal == null) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
rows.push({
|
|
81
|
+
stackMethodId: methodId,
|
|
82
|
+
name,
|
|
83
|
+
selfTimeMean,
|
|
84
|
+
totalTimeMean,
|
|
85
|
+
callCountTotal,
|
|
86
|
+
selfMemoryMean,
|
|
87
|
+
selfMemoryTotal,
|
|
88
|
+
selfMemoryUnit,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (!rows.length) {
|
|
92
|
+
throw new Error('函数 idmap 统计无有效数据');
|
|
93
|
+
}
|
|
94
|
+
const hasTime = rows.some((r) => r.selfTimeMean != null || r.totalTimeMean != null);
|
|
95
|
+
const hasMem = rows.some((r) => r.selfMemoryMean != null || r.selfMemoryTotal != null);
|
|
96
|
+
const stackTestMode = presignMode ?? inferOverviewStackTestMode({ hasTime, hasMem });
|
|
97
|
+
if (!stackTestMode) {
|
|
98
|
+
throw new Error('无法判断 Overview stackTestMode');
|
|
99
|
+
}
|
|
100
|
+
const profile = OVERVIEW_STACK_TEST_MODE_PROFILES[stackTestMode];
|
|
101
|
+
const metric = metricInput ?? resolveOverviewDefaultMetric(stackTestMode);
|
|
102
|
+
assertOverviewMetricSupported(metric, profile);
|
|
103
|
+
const metricValue = (r) => {
|
|
104
|
+
if (metric === 'totalTime')
|
|
105
|
+
return r.totalTimeMean != null ? r.totalTimeMean : -1;
|
|
106
|
+
if (metric === 'callCount')
|
|
107
|
+
return r.callCountTotal != null ? r.callCountTotal : -1;
|
|
108
|
+
if (metric === 'selfMemory') {
|
|
109
|
+
if (r.selfMemoryMean != null)
|
|
110
|
+
return r.selfMemoryMean;
|
|
111
|
+
if (r.selfMemoryTotal != null)
|
|
112
|
+
return r.selfMemoryTotal;
|
|
113
|
+
return -1;
|
|
114
|
+
}
|
|
115
|
+
return r.selfTimeMean != null ? r.selfTimeMean : -1;
|
|
116
|
+
};
|
|
117
|
+
const sorted = rows.sort((a, b) => metricValue(b) - metricValue(a));
|
|
118
|
+
const pattern = namePattern?.trim();
|
|
119
|
+
const matched = pattern ? sorted.filter((r) => matchesNamePattern(r.name, pattern)) : sorted;
|
|
120
|
+
const unit = metric === 'callCount' ? '次' : metric === 'selfMemory' ? rows.find((r) => r.selfMemoryUnit)?.selfMemoryUnit ?? 'KB' : 'ms';
|
|
121
|
+
return {
|
|
122
|
+
testMode: stackTestMode,
|
|
123
|
+
stackTestMode,
|
|
124
|
+
availableMetrics: [...profile.availableMetrics],
|
|
125
|
+
availableFields: [...profile.availableFields],
|
|
126
|
+
metricGuide: profile.description,
|
|
127
|
+
metric,
|
|
128
|
+
unit,
|
|
129
|
+
rows: matched.slice(0, topN).map((r) => ({
|
|
130
|
+
name: r.name,
|
|
131
|
+
methodId: r.stackMethodId,
|
|
132
|
+
stackMethodId: r.stackMethodId,
|
|
133
|
+
selfTimeMean: r.selfTimeMean != null ? round2(r.selfTimeMean) : null,
|
|
134
|
+
totalTimeMean: r.totalTimeMean != null ? round2(r.totalTimeMean) : null,
|
|
135
|
+
callCountTotal: r.callCountTotal != null ? Math.round(r.callCountTotal) : null,
|
|
136
|
+
selfMemoryMean: r.selfMemoryMean != null ? round2(r.selfMemoryMean) : null,
|
|
137
|
+
selfMemoryTotal: r.selfMemoryTotal != null ? round2(r.selfMemoryTotal) : null,
|
|
138
|
+
selfMemoryUnit: r.selfMemoryUnit,
|
|
139
|
+
value: metric === 'callCount'
|
|
140
|
+
? Math.round(metricValue(r) >= 0 ? metricValue(r) : 0)
|
|
141
|
+
: round2(metricValue(r) >= 0 ? metricValue(r) : 0),
|
|
142
|
+
unit,
|
|
143
|
+
callCount: r.callCountTotal != null ? Math.round(r.callCountTotal) : 0,
|
|
144
|
+
callFrameCount: 0,
|
|
145
|
+
nodeCount: 1,
|
|
146
|
+
source: 'overview.method.idmap.statistic',
|
|
147
|
+
})),
|
|
148
|
+
totalNodes: rows.length,
|
|
149
|
+
uniqueFunctions: rows.length,
|
|
150
|
+
apisUsed,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
2
153
|
/**
|
|
3
154
|
* Overview:线程堆栈树(主线程)+ 函数 idmap → 按函数名合并节点。
|
|
4
155
|
* 同名(同一 methodId)节点的 selfTime / callCount 相加,再按指标排序取 Top N。
|
|
@@ -231,10 +382,11 @@ function finalizeAgg(nodes, idMap, topN, metric, testMode, apisUsed, source) {
|
|
|
231
382
|
return {
|
|
232
383
|
name: r.name,
|
|
233
384
|
methodId: r.methodId,
|
|
385
|
+
stackMethodId: r.methodId,
|
|
386
|
+
selfTimeMean: round2(r.selfTimeUs / 1000),
|
|
387
|
+
totalTimeMean: round2(r.totalTimeUs / 1000),
|
|
234
388
|
value,
|
|
235
389
|
unit,
|
|
236
|
-
selfTimeMs: round2(r.selfTimeUs / 1000),
|
|
237
|
-
totalTimeMs: round2(r.totalTimeUs / 1000),
|
|
238
390
|
selfMemoryKb: round2(r.selfMemoryKb),
|
|
239
391
|
callCount: r.callCount,
|
|
240
392
|
callFrameCount: r.callFrameCount,
|