@uwa4d/openapi-mcp 0.2.0-beta.0 → 0.2.0-beta.2
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 +36 -3
- package/dist/cli.js +18 -6
- 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/overview-view.d.ts +32 -0
- package/dist/composite/overview-view.js +252 -0
- package/dist/composite/report-diagnosis.d.ts +2 -0
- package/dist/composite/report-diagnosis.js +157 -0
- package/dist/composite/route-overview.d.ts +71 -0
- package/dist/composite/route-overview.js +232 -0
- package/dist/composite/stack-agg.d.ts +65 -0
- package/dist/composite/stack-agg.js +257 -0
- package/dist/composite/top-functions.d.ts +16 -0
- package/dist/composite/top-functions.js +133 -0
- package/dist/composite/top-resources.d.ts +12 -0
- package/dist/composite/top-resources.js +184 -0
- package/dist/composite/types.d.ts +25 -0
- package/dist/composite/types.js +2 -0
- package/dist/presets.js +4 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +15 -4
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +168 -10
- package/dist/version-guide.d.ts +9 -0
- package/dist/version-guide.js +87 -0
- package/package.json +1 -1
- package/spec/overrides.json +26 -0
- package/spec/uwa-openapi.json +6 -6
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { isObj } from './helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Overview:线程堆栈树(主线程)+ 函数 idmap → 按函数名合并节点。
|
|
4
|
+
* 同名(同一 methodId)节点的 selfTime / callCount 相加,再按指标排序取 Top N。
|
|
5
|
+
*/
|
|
6
|
+
export async function aggregateOverviewStack(client, identity, topN, metric = 'selfTime') {
|
|
7
|
+
const keyQuery = identity.dataKey
|
|
8
|
+
? { dataKey: identity.dataKey }
|
|
9
|
+
: { recordId: identity.recordId };
|
|
10
|
+
const apisUsed = [];
|
|
11
|
+
const [treeMeta, idMap] = await Promise.all([
|
|
12
|
+
client.call('GET', '/openapi/v1/data/gotonline/overview/stack/overall/tree/presign', 'v1.0.1', keyQuery),
|
|
13
|
+
fetchFunctionIdMap(client, keyQuery),
|
|
14
|
+
]);
|
|
15
|
+
apisUsed.push('gotonline_overview_stack_overall_tree_presign', 'gotonline_overview_stack_id_map');
|
|
16
|
+
const url = typeof treeMeta['dataPresignUrl'] === 'string' ? treeMeta['dataPresignUrl'] : null;
|
|
17
|
+
if (!url)
|
|
18
|
+
throw new Error('线程堆栈树未返回 dataPresignUrl(报告可能未开 CPU 堆栈或 SDK < 2.5.1)');
|
|
19
|
+
const testMode = treeMeta['testMode'] != null ? String(treeMeta['testMode']) : null;
|
|
20
|
+
const payload = await client.downloadPresign(url);
|
|
21
|
+
const csvText = payload.kind === 'text' ? (payload.text ?? '') : JSON.stringify(payload.json ?? '');
|
|
22
|
+
const nodes = parseStackCsv(csvText, testMode);
|
|
23
|
+
return finalizeAgg(nodes, idMap, topN, metric, testMode, apisUsed, 'overview.stack.overall');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Mono:正向堆栈树 + Mono 函数 idmap。
|
|
27
|
+
* 文本格式与 Overview CSV 不同时走宽松解析;失败则抛错由上层处理。
|
|
28
|
+
*/
|
|
29
|
+
export async function aggregateMonoStack(client, identity, topN, metric = 'selfMemory') {
|
|
30
|
+
const keyQuery = identity.dataKey
|
|
31
|
+
? { dataKey: identity.dataKey }
|
|
32
|
+
: { recordId: identity.recordId };
|
|
33
|
+
const apisUsed = [];
|
|
34
|
+
const [treeMeta, idMapRaw] = await Promise.all([
|
|
35
|
+
client.call('GET', '/openapi/v1/data/gotonline/mono/stack/tree/overall/presign', 'v1.0.1', keyQuery),
|
|
36
|
+
client.call('GET', '/openapi/v1/data/gotonline/mono/stack/method/idmap/presign', 'v1.0.1', keyQuery),
|
|
37
|
+
]);
|
|
38
|
+
apisUsed.push('gotonline_mono_stack_tree_overall_presign', 'gotonline_mono_stack_method_idmap_presign');
|
|
39
|
+
const treeUrl = typeof treeMeta['dataPresignUrl'] === 'string' ? treeMeta['dataPresignUrl'] : null;
|
|
40
|
+
const mapUrl = typeof idMapRaw['dataPresignUrl'] === 'string' ? idMapRaw['dataPresignUrl'] : null;
|
|
41
|
+
if (!treeUrl)
|
|
42
|
+
throw new Error('Mono 堆栈树未返回 dataPresignUrl');
|
|
43
|
+
if (!mapUrl)
|
|
44
|
+
throw new Error('Mono 函数 idmap 未返回 dataPresignUrl');
|
|
45
|
+
const [treePayload, mapPayload] = await Promise.all([
|
|
46
|
+
client.downloadPresign(treeUrl),
|
|
47
|
+
client.downloadPresign(mapUrl),
|
|
48
|
+
]);
|
|
49
|
+
const idMap = parseIdMapPayload(mapPayload.json ?? mapPayload.text);
|
|
50
|
+
const csvText = treePayload.kind === 'text' ? (treePayload.text ?? '') : JSON.stringify(treePayload.json ?? '');
|
|
51
|
+
// Mono 正向树多为文本/CSV;按 CPU_ONLY/内存列尝试解析
|
|
52
|
+
const nodes = parseStackCsv(csvText, 'LUA_MEM_ONLY');
|
|
53
|
+
const useMetric = metric === 'selfTime' || metric === 'totalTime' ? 'selfMemory' : metric;
|
|
54
|
+
return finalizeAgg(nodes, idMap, topN, useMetric, 'LUA_MEM_ONLY', apisUsed, 'mono.stack.overall');
|
|
55
|
+
}
|
|
56
|
+
async function fetchFunctionIdMap(client, keyQuery) {
|
|
57
|
+
const raw = await client.call('GET', '/openapi/v1/data/gotonline/overview/stack/id/map', 'v1.0.1', keyQuery);
|
|
58
|
+
return parseIdMapPayload(raw);
|
|
59
|
+
}
|
|
60
|
+
function parseIdMapPayload(raw) {
|
|
61
|
+
const map = new Map();
|
|
62
|
+
let list = null;
|
|
63
|
+
if (isObj(raw)) {
|
|
64
|
+
list = raw['id_map'] ?? raw['stats_id_map'] ?? raw['idMap'];
|
|
65
|
+
}
|
|
66
|
+
if (!list && typeof raw === 'string') {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (isObj(parsed))
|
|
70
|
+
list = parsed['id_map'] ?? parsed['stats_id_map'];
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* ignore */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(list))
|
|
77
|
+
return map;
|
|
78
|
+
for (const item of list) {
|
|
79
|
+
if (!isObj(item))
|
|
80
|
+
continue;
|
|
81
|
+
const id = item['id'] != null ? String(item['id']) : '';
|
|
82
|
+
const name = item['name'] != null ? String(item['name']) : '';
|
|
83
|
+
if (id)
|
|
84
|
+
map.set(id, name || id);
|
|
85
|
+
}
|
|
86
|
+
return map;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 解析无表头 CSV。列数随 testMode 变化;UE 固定 7 列(与 CPU_ONLY 同形)。
|
|
90
|
+
*/
|
|
91
|
+
export function parseStackCsv(csvText, testMode) {
|
|
92
|
+
const lines = csvText
|
|
93
|
+
.replace(/^\uFEFF/, '')
|
|
94
|
+
.split(/\r?\n/)
|
|
95
|
+
.map((l) => l.trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
const nodes = [];
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const cols = splitCsvLine(line);
|
|
100
|
+
if (cols.length < 6)
|
|
101
|
+
continue;
|
|
102
|
+
const depth = Number(cols[0]);
|
|
103
|
+
const sampleId = String(cols[1] ?? '');
|
|
104
|
+
const methodId = String(cols[2] ?? '');
|
|
105
|
+
if (!methodId || Number.isNaN(depth))
|
|
106
|
+
continue;
|
|
107
|
+
const mode = (testMode ?? inferMode(cols.length)).toUpperCase();
|
|
108
|
+
let totalTimeUs = 0;
|
|
109
|
+
let selfTimeUs = 0;
|
|
110
|
+
let callCount = 0;
|
|
111
|
+
let callFrameCount = 0;
|
|
112
|
+
let totalMemoryKb = 0;
|
|
113
|
+
let selfMemoryKb = 0;
|
|
114
|
+
if (mode === 'LUA_MEM_ONLY' || (cols.length === 7 && mode.includes('LUA') && !mode.includes('CPU'))) {
|
|
115
|
+
totalMemoryKb = num(cols[3]);
|
|
116
|
+
selfMemoryKb = num(cols[4]);
|
|
117
|
+
callCount = num(cols[5]);
|
|
118
|
+
callFrameCount = num(cols[6]);
|
|
119
|
+
}
|
|
120
|
+
else if (mode === 'CPU_AND_LUA_MEM' || cols.length >= 9) {
|
|
121
|
+
totalTimeUs = num(cols[3]);
|
|
122
|
+
selfTimeUs = num(cols[4]);
|
|
123
|
+
callCount = num(cols[5]);
|
|
124
|
+
totalMemoryKb = num(cols[6]);
|
|
125
|
+
selfMemoryKb = num(cols[7]);
|
|
126
|
+
callFrameCount = num(cols[8]);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// CPU_ONLY / UE 7 列
|
|
130
|
+
totalTimeUs = num(cols[3]);
|
|
131
|
+
selfTimeUs = num(cols[4]);
|
|
132
|
+
callCount = num(cols[5]);
|
|
133
|
+
callFrameCount = num(cols[6]);
|
|
134
|
+
}
|
|
135
|
+
nodes.push({
|
|
136
|
+
depth,
|
|
137
|
+
sampleId,
|
|
138
|
+
methodId,
|
|
139
|
+
totalTimeUs,
|
|
140
|
+
selfTimeUs,
|
|
141
|
+
callCount,
|
|
142
|
+
callFrameCount,
|
|
143
|
+
totalMemoryKb,
|
|
144
|
+
selfMemoryKb,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return nodes;
|
|
148
|
+
}
|
|
149
|
+
function inferMode(colCount) {
|
|
150
|
+
if (colCount >= 9)
|
|
151
|
+
return 'CPU_AND_LUA_MEM';
|
|
152
|
+
return 'CPU_ONLY';
|
|
153
|
+
}
|
|
154
|
+
function num(v) {
|
|
155
|
+
if (v == null || v === '')
|
|
156
|
+
return 0;
|
|
157
|
+
const n = Number(v);
|
|
158
|
+
return Number.isFinite(n) ? n : 0;
|
|
159
|
+
}
|
|
160
|
+
/** 简单 CSV 分割:堆栈树字段不含引号逗号,按逗号切即可;兼容制表符。 */
|
|
161
|
+
function splitCsvLine(line) {
|
|
162
|
+
if (line.includes('\t') && !line.includes(','))
|
|
163
|
+
return line.split('\t');
|
|
164
|
+
return line.split(',');
|
|
165
|
+
}
|
|
166
|
+
function finalizeAgg(nodes, idMap, topN, metric, testMode, apisUsed, source) {
|
|
167
|
+
const byId = new Map();
|
|
168
|
+
for (const n of nodes) {
|
|
169
|
+
let row = byId.get(n.methodId);
|
|
170
|
+
if (!row) {
|
|
171
|
+
row = {
|
|
172
|
+
methodId: n.methodId,
|
|
173
|
+
name: idMap.get(n.methodId) ?? n.methodId,
|
|
174
|
+
selfTimeUs: 0,
|
|
175
|
+
totalTimeUs: 0,
|
|
176
|
+
callCount: 0,
|
|
177
|
+
callFrameCount: 0,
|
|
178
|
+
selfMemoryKb: 0,
|
|
179
|
+
totalMemoryKb: 0,
|
|
180
|
+
nodeCount: 0,
|
|
181
|
+
};
|
|
182
|
+
byId.set(n.methodId, row);
|
|
183
|
+
}
|
|
184
|
+
row.selfTimeUs += n.selfTimeUs;
|
|
185
|
+
row.totalTimeUs += n.totalTimeUs;
|
|
186
|
+
row.callCount += n.callCount;
|
|
187
|
+
row.callFrameCount += n.callFrameCount;
|
|
188
|
+
row.selfMemoryKb += n.selfMemoryKb;
|
|
189
|
+
row.totalMemoryKb += n.totalMemoryKb;
|
|
190
|
+
row.nodeCount += 1;
|
|
191
|
+
}
|
|
192
|
+
// 同名不同 methodId 再按 name 合并一次(文档要求「同一个函数的节点还需要合并」)
|
|
193
|
+
const byName = new Map();
|
|
194
|
+
for (const row of byId.values()) {
|
|
195
|
+
const key = row.name || row.methodId;
|
|
196
|
+
const exist = byName.get(key);
|
|
197
|
+
if (!exist) {
|
|
198
|
+
byName.set(key, { ...row });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
exist.selfTimeUs += row.selfTimeUs;
|
|
202
|
+
exist.totalTimeUs += row.totalTimeUs;
|
|
203
|
+
exist.callCount += row.callCount;
|
|
204
|
+
exist.callFrameCount += row.callFrameCount;
|
|
205
|
+
exist.selfMemoryKb += row.selfMemoryKb;
|
|
206
|
+
exist.totalMemoryKb += row.totalMemoryKb;
|
|
207
|
+
exist.nodeCount += row.nodeCount;
|
|
208
|
+
// 保留首次 methodId
|
|
209
|
+
}
|
|
210
|
+
const metricOf = (r) => {
|
|
211
|
+
switch (metric) {
|
|
212
|
+
case 'totalTime':
|
|
213
|
+
return r.totalTimeUs;
|
|
214
|
+
case 'callCount':
|
|
215
|
+
return r.callCount;
|
|
216
|
+
case 'selfMemory':
|
|
217
|
+
return r.selfMemoryKb;
|
|
218
|
+
case 'selfTime':
|
|
219
|
+
default:
|
|
220
|
+
return r.selfTimeUs;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const unit = metric === 'selfMemory' ? 'KB' : metric === 'callCount' ? '次' : 'ms';
|
|
224
|
+
const sorted = [...byName.values()].sort((a, b) => metricOf(b) - metricOf(a));
|
|
225
|
+
const rows = sorted.slice(0, topN).map((r) => {
|
|
226
|
+
const value = metric === 'callCount'
|
|
227
|
+
? r.callCount
|
|
228
|
+
: metric === 'selfMemory'
|
|
229
|
+
? round2(r.selfMemoryKb)
|
|
230
|
+
: round2(metricOf(r) / 1000); // μs → ms
|
|
231
|
+
return {
|
|
232
|
+
name: r.name,
|
|
233
|
+
methodId: r.methodId,
|
|
234
|
+
value,
|
|
235
|
+
unit,
|
|
236
|
+
selfTimeMs: round2(r.selfTimeUs / 1000),
|
|
237
|
+
totalTimeMs: round2(r.totalTimeUs / 1000),
|
|
238
|
+
selfMemoryKb: round2(r.selfMemoryKb),
|
|
239
|
+
callCount: r.callCount,
|
|
240
|
+
callFrameCount: r.callFrameCount,
|
|
241
|
+
nodeCount: r.nodeCount,
|
|
242
|
+
source,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
testMode,
|
|
247
|
+
metric,
|
|
248
|
+
unit,
|
|
249
|
+
rows,
|
|
250
|
+
totalNodes: nodes.length,
|
|
251
|
+
uniqueFunctions: byName.size,
|
|
252
|
+
apisUsed,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function round2(n) {
|
|
256
|
+
return Math.round(n * 100) / 100;
|
|
257
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { UwaClient } from '../client.js';
|
|
2
|
+
import type { CompositeTool } from './types.js';
|
|
3
|
+
export interface FunctionRow {
|
|
4
|
+
name: string;
|
|
5
|
+
value: number;
|
|
6
|
+
unit?: string;
|
|
7
|
+
source: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
methodId?: string;
|
|
10
|
+
selfTimeMs?: number;
|
|
11
|
+
totalTimeMs?: number;
|
|
12
|
+
callCount?: number;
|
|
13
|
+
nodeCount?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function runTopFunctions(client: UwaClient, args: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
16
|
+
export declare const topFunctionsTool: CompositeTool;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { z } from './types.js';
|
|
2
|
+
import { errorResult, jsonToolResult, requireReportKey, topNOf } from './helpers.js';
|
|
3
|
+
import { fetchReportIdentity } from './route-overview.js';
|
|
4
|
+
import { aggregateMonoStack, aggregateOverviewStack } from './stack-agg.js';
|
|
5
|
+
function resolveMetric(raw, mode) {
|
|
6
|
+
if (raw === 'totalTime' || raw === 'callCount' || raw === 'selfMemory' || raw === 'selfTime') {
|
|
7
|
+
return raw;
|
|
8
|
+
}
|
|
9
|
+
return mode === 'mono' ? 'selfMemory' : 'selfTime';
|
|
10
|
+
}
|
|
11
|
+
export async function runTopFunctions(client, args) {
|
|
12
|
+
const key = requireReportKey(args);
|
|
13
|
+
const topN = topNOf(args, 20);
|
|
14
|
+
const subtype = typeof args['serviceSubtype'] === 'string' && args['serviceSubtype']
|
|
15
|
+
? String(args['serviceSubtype']).toLowerCase()
|
|
16
|
+
: undefined;
|
|
17
|
+
const identity = await fetchReportIdentity(client, key);
|
|
18
|
+
const apisUsed = ['get_report_detail'];
|
|
19
|
+
const mode = subtype ?? identity.serviceSubtype ?? 'overview';
|
|
20
|
+
const metric = resolveMetric(args['metric'], mode);
|
|
21
|
+
if (mode === 'mono') {
|
|
22
|
+
if (identity.engine !== 'unity') {
|
|
23
|
+
return {
|
|
24
|
+
engine: identity.engine,
|
|
25
|
+
dataKey: identity.dataKey,
|
|
26
|
+
items: [],
|
|
27
|
+
_apisUsed: apisUsed,
|
|
28
|
+
_note: 'Mono 模式仅支持 Unity 报告。',
|
|
29
|
+
_limitations: ['engine!=unity'],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const agg = await aggregateMonoStack(client, identity, topN, metric);
|
|
34
|
+
return {
|
|
35
|
+
engine: identity.engine,
|
|
36
|
+
dataKey: identity.dataKey,
|
|
37
|
+
serviceSubtype: 'mono',
|
|
38
|
+
topN,
|
|
39
|
+
metric: agg.metric,
|
|
40
|
+
testMode: agg.testMode,
|
|
41
|
+
items: agg.rows,
|
|
42
|
+
totalNodes: agg.totalNodes,
|
|
43
|
+
uniqueFunctions: agg.uniqueFunctions,
|
|
44
|
+
_apisUsed: [...apisUsed, ...agg.apisUsed],
|
|
45
|
+
_note: `已下载 Mono 正向堆栈树并按函数名合并同名节点(共 ${agg.totalNodes} 个节点 → ${agg.uniqueFunctions} 个函数),` +
|
|
46
|
+
`按 ${agg.metric} 降序取 Top ${agg.rows.length}。`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return {
|
|
51
|
+
engine: identity.engine,
|
|
52
|
+
dataKey: identity.dataKey,
|
|
53
|
+
serviceSubtype: 'mono',
|
|
54
|
+
items: [],
|
|
55
|
+
_apisUsed: apisUsed,
|
|
56
|
+
_note: `Mono 堆栈树聚合失败:${e instanceof Error ? e.message : String(e)}`,
|
|
57
|
+
_limitations: ['mono_stack_unavailable'],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Overview:必须以堆栈树 + idmap 为准;统计接口里的「卡顿重点函数 / func / 自定义函数组」都不完整。
|
|
62
|
+
try {
|
|
63
|
+
const agg = await aggregateOverviewStack(client, identity, topN, metric);
|
|
64
|
+
return {
|
|
65
|
+
engine: identity.engine,
|
|
66
|
+
dataKey: identity.dataKey,
|
|
67
|
+
createDate: identity.createDate,
|
|
68
|
+
sdkVersion: identity.sdkVersion,
|
|
69
|
+
serviceSubtype: identity.serviceSubtype,
|
|
70
|
+
topN,
|
|
71
|
+
metric: agg.metric,
|
|
72
|
+
testMode: agg.testMode,
|
|
73
|
+
items: agg.rows,
|
|
74
|
+
totalNodes: agg.totalNodes,
|
|
75
|
+
uniqueFunctions: agg.uniqueFunctions,
|
|
76
|
+
_apisUsed: [...apisUsed, ...agg.apisUsed],
|
|
77
|
+
_note: `已下载主线程堆栈树并按函数名合并同名节点(共 ${agg.totalNodes} 个节点 → ${agg.uniqueFunctions} 个函数),` +
|
|
78
|
+
`按 ${agg.metric}(${agg.unit})降序取 Top ${agg.rows.length}。` +
|
|
79
|
+
`默认排序字段为 selfTime(自身耗时),不是统计接口里的抽样指标。`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
return {
|
|
84
|
+
engine: identity.engine,
|
|
85
|
+
dataKey: identity.dataKey,
|
|
86
|
+
createDate: identity.createDate,
|
|
87
|
+
sdkVersion: identity.sdkVersion,
|
|
88
|
+
serviceSubtype: identity.serviceSubtype,
|
|
89
|
+
items: [],
|
|
90
|
+
_apisUsed: apisUsed,
|
|
91
|
+
_note: `堆栈树聚合失败:${e instanceof Error ? e.message : String(e)}。` +
|
|
92
|
+
`Overview 函数 Top 依赖 stack/overall/tree + stack/id/map(通常需 SDK ≥ 2.5.1 且开启 CPU 堆栈)。`,
|
|
93
|
+
_limitations: ['overview_stack_unavailable'],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export const topFunctionsTool = {
|
|
98
|
+
id: 'top_functions',
|
|
99
|
+
title: '函数耗时 Top N',
|
|
100
|
+
engines: ['unity', 'unreal'],
|
|
101
|
+
filterKeys: ['top_functions', 'composite', 'preset.default', 'overview', 'mono'],
|
|
102
|
+
description: [
|
|
103
|
+
'从堆栈树聚合指定报告的函数 Top N(按自身耗时合并同名节点)。',
|
|
104
|
+
'适用引擎:Unity / UE|复合工具',
|
|
105
|
+
'Overview:下载主线程堆栈树(stack/overall/tree/presign)+ 函数 idmap(stack/id/map),',
|
|
106
|
+
'将 methodId 还原为函数名,并把树上同一函数的多个节点合并(selfTime/callCount 相加),再取 Top N。',
|
|
107
|
+
'不要用统计接口里的「卡顿重点函数」或自定义函数组——那不是全量函数耗时。',
|
|
108
|
+
'Mono(仅 Unity):走 Mono 正向堆栈树 + Mono 函数 idmap,默认按自身内存排序。',
|
|
109
|
+
'需要原始树时请直接调对应的 stack/presign 原子工具。',
|
|
110
|
+
].join('\n'),
|
|
111
|
+
inputSchema: {
|
|
112
|
+
dataKey: z.string().optional().describe('报告 dataKey,与 recordId 二选一'),
|
|
113
|
+
recordId: z.union([z.string(), z.number()]).optional().describe('报告 recordId,与 dataKey 二选一'),
|
|
114
|
+
topN: z.number().optional().describe('返回条数,默认 20,最大 200'),
|
|
115
|
+
metric: z
|
|
116
|
+
.enum(['selfTime', 'totalTime', 'callCount', 'selfMemory'])
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('排序指标,默认 Overview 用 selfTime(自身耗时 ms),Mono 用 selfMemory(KB)'),
|
|
119
|
+
serviceSubtype: z
|
|
120
|
+
.enum(['overview', 'mono'])
|
|
121
|
+
.optional()
|
|
122
|
+
.describe('报告模式,默认跟随报告详情;mono 仅 Unity'),
|
|
123
|
+
},
|
|
124
|
+
async handler(args, ctx) {
|
|
125
|
+
try {
|
|
126
|
+
const result = await runTopFunctions(ctx.client, args);
|
|
127
|
+
return jsonToolResult(result, ctx.maxChars);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
return errorResult(err);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UwaClient } from '../client.js';
|
|
2
|
+
import type { CompositeTool } from './types.js';
|
|
3
|
+
export interface ResourceRow {
|
|
4
|
+
name: string;
|
|
5
|
+
assetType: string;
|
|
6
|
+
memoryBytes: number;
|
|
7
|
+
count?: number;
|
|
8
|
+
extras?: Record<string, unknown>;
|
|
9
|
+
_source: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runTopResources(client: UwaClient, args: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
12
|
+
export declare const topResourcesTool: CompositeTool;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { z } from './types.js';
|
|
2
|
+
import { errorResult, isObj, jsonToolResult, requireReportKey, topNOf, } from './helpers.js';
|
|
3
|
+
import { fetchOverviewStatistic, fetchReportIdentity, preferAtResourceTable, } from './route-overview.js';
|
|
4
|
+
import { viewOverview } from './overview-view.js';
|
|
5
|
+
const DEFAULT_ASSET_TYPES = ['Texture', 'Mesh', 'Shader', 'Material', 'AnimationClip'];
|
|
6
|
+
async function fetchUnityAssetType(client, identity, assetType, useAtTable) {
|
|
7
|
+
const keyQuery = identity.dataKey
|
|
8
|
+
? { dataKey: identity.dataKey }
|
|
9
|
+
: { recordId: identity.recordId };
|
|
10
|
+
if (useAtTable) {
|
|
11
|
+
const meta = (await client.call('GET', '/openapi/v1/data/gotonline/overview/at/resource/overall/table/presign', 'v1.0.1', { ...keyQuery, assetType }));
|
|
12
|
+
const url = typeof meta['dataPresignUrl'] === 'string' ? meta['dataPresignUrl'] : null;
|
|
13
|
+
if (!url)
|
|
14
|
+
return [];
|
|
15
|
+
const payload = await client.downloadPresign(url);
|
|
16
|
+
const json = payload.json;
|
|
17
|
+
const resources = extractAtResources(json);
|
|
18
|
+
return resources.map((r) => ({
|
|
19
|
+
name: String(r['name'] ?? r['id'] ?? ''),
|
|
20
|
+
assetType,
|
|
21
|
+
memoryBytes: Number(r['memMaximum'] ?? 0) || 0,
|
|
22
|
+
count: r['countMaximum'] != null ? Number(r['countMaximum']) : undefined,
|
|
23
|
+
extras: pickExtras(r, ['id', 'typeName', 'tags', 'lifecycle', 'properties']),
|
|
24
|
+
_source: 'at/resource/overall/table/presign',
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
const data = (await client.call('GET', '/openapi/v1/data/gotonline/overview/memory/manage/data/report', 'v1.0.1', { ...keyQuery, assetType }));
|
|
28
|
+
const dict = isObj(data['Asset_dict']) ? data['Asset_dict'] : {};
|
|
29
|
+
return Object.values(dict)
|
|
30
|
+
.filter(isObj)
|
|
31
|
+
.map((r) => ({
|
|
32
|
+
name: String(r['name'] ?? ''),
|
|
33
|
+
assetType,
|
|
34
|
+
memoryBytes: Number(r['max_memory'] ?? 0) || 0,
|
|
35
|
+
count: r['max_count'] != null ? Number(r['max_count']) : undefined,
|
|
36
|
+
extras: pickExtras(r, ['resident_frames', 'last_zero_frame', 'has_rw', 'width', 'height', 'format']),
|
|
37
|
+
_source: 'memory/manage/data/report',
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
function extractAtResources(json) {
|
|
41
|
+
if (!json)
|
|
42
|
+
return [];
|
|
43
|
+
if (Array.isArray(json))
|
|
44
|
+
return json.filter(isObj);
|
|
45
|
+
if (!isObj(json))
|
|
46
|
+
return [];
|
|
47
|
+
if (Array.isArray(json['resources']))
|
|
48
|
+
return json['resources'].filter(isObj);
|
|
49
|
+
// 偶发整包就是一张表
|
|
50
|
+
for (const v of Object.values(json)) {
|
|
51
|
+
if (Array.isArray(v) && v.length && isObj(v[0]) && ('memMaximum' in v[0] || 'name' in v[0])) {
|
|
52
|
+
return v.filter(isObj);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
function pickExtras(r, keys) {
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const k of keys)
|
|
60
|
+
if (r[k] !== undefined)
|
|
61
|
+
out[k] = r[k];
|
|
62
|
+
return Object.keys(out).length ? out : undefined;
|
|
63
|
+
}
|
|
64
|
+
/** 从 Overview 统计抽出类型级内存峰值(UE / 无逐资源列表时用)。 */
|
|
65
|
+
function typeLevelFromOverview(view) {
|
|
66
|
+
return view.memory.map((m) => ({
|
|
67
|
+
name: m.name,
|
|
68
|
+
assetType: m.assetType,
|
|
69
|
+
memoryBytes: m.memoryBytes,
|
|
70
|
+
_source: m._source,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
export async function runTopResources(client, args) {
|
|
74
|
+
const key = requireReportKey(args);
|
|
75
|
+
const topN = topNOf(args, 20);
|
|
76
|
+
const assetTypes = normalizeAssetTypes(args['assetTypes']);
|
|
77
|
+
const identity = await fetchReportIdentity(client, key);
|
|
78
|
+
const apisUsed = ['get_report_detail'];
|
|
79
|
+
if (identity.engine === 'unreal') {
|
|
80
|
+
const { target, data } = await fetchOverviewStatistic(client, identity);
|
|
81
|
+
apisUsed.push(target.id);
|
|
82
|
+
const view = viewOverview(data);
|
|
83
|
+
const items = typeLevelFromOverview(view).slice(0, topN);
|
|
84
|
+
return {
|
|
85
|
+
engine: identity.engine,
|
|
86
|
+
dataKey: identity.dataKey,
|
|
87
|
+
topN,
|
|
88
|
+
items,
|
|
89
|
+
_note: 'UE 无逐资源内存列表接口,此处返回 Overview 统计中的类型级峰值(非单个资源)。Unity 报告可得到逐资源 Top N。',
|
|
90
|
+
_apisUsed: apisUsed,
|
|
91
|
+
_overviewShape: view.shape,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Unity
|
|
95
|
+
const { target, data } = await fetchOverviewStatistic(client, identity);
|
|
96
|
+
apisUsed.push(target.id);
|
|
97
|
+
const view = viewOverview(data);
|
|
98
|
+
const hasRes = view.hasResource === true;
|
|
99
|
+
if (!hasRes) {
|
|
100
|
+
const items = typeLevelFromOverview(view).slice(0, topN);
|
|
101
|
+
return {
|
|
102
|
+
engine: identity.engine,
|
|
103
|
+
dataKey: identity.dataKey,
|
|
104
|
+
topN,
|
|
105
|
+
items,
|
|
106
|
+
_note: view.hasResource === false
|
|
107
|
+
? '报告未开启资源采集(OverviewHasResource=0),无法拉逐资源列表;已回退为 Overview 类型级峰值。'
|
|
108
|
+
: '无法从统计结果判定是否开启资源采集;已回退为 Overview 类型级峰值。',
|
|
109
|
+
_apisUsed: apisUsed,
|
|
110
|
+
_limitations: [view.hasResource === false ? '未开启资源采集' : '资源采集状态未知'],
|
|
111
|
+
_overviewShape: view.shape,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const useAt = preferAtResourceTable(identity);
|
|
115
|
+
const settled = await Promise.allSettled(assetTypes.map((t) => fetchUnityAssetType(client, identity, t, useAt)));
|
|
116
|
+
const items = [];
|
|
117
|
+
const errors = [];
|
|
118
|
+
for (let i = 0; i < settled.length; i++) {
|
|
119
|
+
const r = settled[i];
|
|
120
|
+
const t = assetTypes[i];
|
|
121
|
+
if (r.status === 'fulfilled') {
|
|
122
|
+
items.push(...r.value);
|
|
123
|
+
apisUsed.push(useAt ? `at_resource_overall:${t}` : `memory_manage:${t}`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
errors.push(`${t}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
items.sort((a, b) => b.memoryBytes - a.memoryBytes);
|
|
130
|
+
const top = items.slice(0, topN);
|
|
131
|
+
return {
|
|
132
|
+
engine: identity.engine,
|
|
133
|
+
dataKey: identity.dataKey,
|
|
134
|
+
createDate: identity.createDate,
|
|
135
|
+
sdkVersion: identity.sdkVersion,
|
|
136
|
+
assetTypes,
|
|
137
|
+
topN,
|
|
138
|
+
totalCandidates: items.length,
|
|
139
|
+
items: top,
|
|
140
|
+
_sourceKind: useAt ? 'at_resource_overall_presign' : 'memory_manage',
|
|
141
|
+
_apisUsed: apisUsed,
|
|
142
|
+
...(errors.length ? { _partialErrors: errors } : {}),
|
|
143
|
+
_note: `已按内存峰值合并 ${assetTypes.length} 种资源类型,返回 Top ${top.length}。`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function normalizeAssetTypes(raw) {
|
|
147
|
+
if (Array.isArray(raw) && raw.length)
|
|
148
|
+
return raw.map(String);
|
|
149
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
150
|
+
return raw.split(/[,,]/).map((s) => s.trim()).filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
return [...DEFAULT_ASSET_TYPES];
|
|
153
|
+
}
|
|
154
|
+
export const topResourcesTool = {
|
|
155
|
+
id: 'top_resources',
|
|
156
|
+
title: '资源内存 Top N',
|
|
157
|
+
engines: ['unity', 'unreal'],
|
|
158
|
+
filterKeys: ['top_resources', 'composite', 'preset.default', 'overview'],
|
|
159
|
+
description: [
|
|
160
|
+
'聚合指定报告的资源内存占用 Top N。',
|
|
161
|
+
'适用引擎:Unity / UE|复合工具(内部串多个接口,调用方不必自己选 v1/v2)',
|
|
162
|
+
'Unity:按 assetType 并行拉取资源列表(解析日 ≥ 2026-06-25 优先 AT 总览预签名;否则 memory/manage),合并后按内存峰值排序。',
|
|
163
|
+
'UE:无逐资源列表,返回 Overview 统计中的类型级峰值,并在 _note 中标明。',
|
|
164
|
+
'不确定报告版本时直接用本工具;需要原始全量列表时再调对应原子工具。',
|
|
165
|
+
].join('\n'),
|
|
166
|
+
inputSchema: {
|
|
167
|
+
dataKey: z.string().optional().describe('报告 dataKey,与 recordId 二选一'),
|
|
168
|
+
recordId: z.union([z.string(), z.number()]).optional().describe('报告 recordId,与 dataKey 二选一'),
|
|
169
|
+
assetTypes: z
|
|
170
|
+
.array(z.string())
|
|
171
|
+
.optional()
|
|
172
|
+
.describe(`资源类型列表,默认 ${DEFAULT_ASSET_TYPES.join(',')}。Unity 每次内部按类型各查一次再合并`),
|
|
173
|
+
topN: z.number().optional().describe('返回条数,默认 20,最大 200'),
|
|
174
|
+
},
|
|
175
|
+
async handler(args, ctx) {
|
|
176
|
+
try {
|
|
177
|
+
const result = await runTopResources(ctx.client, args);
|
|
178
|
+
return jsonToolResult(result, ctx.maxChars);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
return errorResult(err);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z, type ZodTypeAny } from 'zod';
|
|
2
|
+
import type { Engine } from '../spec.js';
|
|
3
|
+
import type { UwaClient } from '../client.js';
|
|
4
|
+
import type { NameCase } from '../tools.js';
|
|
5
|
+
import type { ToolResult } from '../tools.js';
|
|
6
|
+
export interface CompositeContext {
|
|
7
|
+
client: UwaClient;
|
|
8
|
+
maxChars: number;
|
|
9
|
+
nameCase: NameCase;
|
|
10
|
+
namePrefix: string;
|
|
11
|
+
/** --engine 过滤,'all' 表示不过滤 */
|
|
12
|
+
engine: Engine | 'all';
|
|
13
|
+
}
|
|
14
|
+
export interface CompositeTool {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
description: string;
|
|
18
|
+
/** 适用引擎;过滤 --engine 时用 */
|
|
19
|
+
engines: Engine[];
|
|
20
|
+
/** --tool 过滤键,除 id 外还可含 composite / preset.default 等 */
|
|
21
|
+
filterKeys: string[];
|
|
22
|
+
inputSchema: Record<string, ZodTypeAny>;
|
|
23
|
+
handler: (args: Record<string, unknown>, ctx: CompositeContext) => Promise<ToolResult>;
|
|
24
|
+
}
|
|
25
|
+
export { z };
|
package/dist/presets.js
CHANGED
|
@@ -25,6 +25,10 @@ export const PRESET_DEFAULT = [
|
|
|
25
25
|
'gotonline_overview_memory_manage_data_report',
|
|
26
26
|
'gotonline_overview_method_group_statistic',
|
|
27
27
|
'gotonline_overview_stack_stutter_full_presign',
|
|
28
|
+
// 复合工具(手写,见 src/composite/)
|
|
29
|
+
'top_resources',
|
|
30
|
+
'top_functions',
|
|
31
|
+
'report_diagnosis',
|
|
28
32
|
];
|
|
29
33
|
/**
|
|
30
34
|
* 每个接口对应的一组过滤键,用户在 --tool 里写任意一个即可命中。
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { COMPOSITE_TOOLS, selectCompositeTools } from './composite/index.js';
|
|
2
3
|
import { type Engine } from './spec.js';
|
|
3
4
|
import { type NameCase } from './tools.js';
|
|
4
5
|
export interface ServerOptions {
|
|
@@ -16,10 +17,13 @@ export interface ServerOptions {
|
|
|
16
17
|
timeoutMs: number;
|
|
17
18
|
specPath?: string;
|
|
18
19
|
}
|
|
19
|
-
export declare const PACKAGE_VERSION = "0.
|
|
20
|
+
export declare const PACKAGE_VERSION = "0.2.0";
|
|
20
21
|
export declare function createServer(opts: ServerOptions): {
|
|
21
22
|
server: McpServer;
|
|
22
23
|
toolCount: number;
|
|
23
24
|
total: number;
|
|
25
|
+
atomicCount: number;
|
|
26
|
+
compositeCount: number;
|
|
24
27
|
};
|
|
25
28
|
export declare function startStdio(opts: ServerOptions): Promise<void>;
|
|
29
|
+
export { selectCompositeTools, COMPOSITE_TOOLS };
|