@uwa4d/openapi-mcp 0.2.0-beta.2 → 0.2.0-beta.3

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 CHANGED
@@ -128,7 +128,7 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
128
128
  | 工具 | 做什么 |
129
129
  | --- | --- |
130
130
  | `report_diagnosis` | 单份报告体检:自动选对统计 v1/v2,附带 Top 资源/函数与场景摘要 |
131
- | `top_resources` | 资源内存 Top N(Unity 逐资源合并多类型;UE 为类型级峰值) |
131
+ | `top_resources` | 资源内存 Top N(默认 11 种类型;`groupBy=merged` 全局 Top / `assetType` 按类型各 Top) |
132
132
  | `top_functions` | 函数 Top N:下载堆栈树 + idmap,按函数名合并同名节点后再排序 |
133
133
 
134
134
  不确定该调统计 1.0 还是 2.0 时,优先用这三个,不必自己判断日期和 SDK。
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Overview 统计 v1 / v2 返回结构差异很大:
3
- * - v1:flat brief / func / asset_stats / summary
4
- * - v2(Unity POST):categories[] + basicInfo{}
2
+ * Overview 统计 v1 / v2 / UE 2.0 返回结构差异很大:
3
+ * - Unity v1:对象形 brief { fps_mean: [49] } + asset_stats
4
+ * - UE 2.0:数组形 brief [{ label, value }] + Resource 段
5
+ * - Unity v2(POST):categories[] + basicInfo{}
5
6
  * 这里统一抽成复合工具能用的扁平视图。
6
7
  */
7
8
  export interface OverviewView {
@@ -26,7 +27,7 @@ export interface OverviewView {
26
27
  }[];
27
28
  /** 是否开启资源采集;无法判定时为 null */
28
29
  hasResource: boolean | null;
29
- shape: 'v1' | 'v2' | 'unknown';
30
+ shape: 'v1' | 'v2' | 'ue-v2' | 'unknown';
30
31
  }
31
32
  /** 把 unwrap 后的报告或原始 data 统一成 OverviewView。 */
32
33
  export declare function viewOverview(data: unknown): OverviewView;
@@ -23,6 +23,10 @@ function toBytes(value, unit) {
23
23
  // 无单位时:很大的数当字节,否则当 MB(兼容 v1 brief 的 _mem_max_mb)
24
24
  return value > 10_000 ? Math.round(value) : Math.round(value * 1024 * 1024);
25
25
  }
26
+ function unitFromLabel(label) {
27
+ const m = /[((]\s*(MB|KB|GB|B|字节)\s*[))]/i.exec(label);
28
+ return m?.[1]?.toLowerCase() === '字节' ? 'b' : m?.[1]?.toLowerCase();
29
+ }
26
30
  /** 排除「数量峰值」这类非内存指标。 */
27
31
  function looksLikeMemoryMetric(label, key, unit) {
28
32
  if (/数量|count|cnt/i.test(label) || /count|cnt/i.test(key))
@@ -32,8 +36,42 @@ function looksLikeMemoryMetric(label, key, unit) {
32
36
  return /内存|memory|mem|reserved|used|heap|gfx|mono|lua/i.test(label + key);
33
37
  }
34
38
  function guessAssetType(label, fallback) {
35
- const m = /(Texture|Mesh|Shader|Material|Animation|Audio|Font|RenderTexture|AssetBundle|TextAsset|Lua|Mono|Gfx|GC)/i.exec(label);
36
- return m?.[1] ?? fallback;
39
+ const rules = [
40
+ [/RenderTexture/i, 'RenderTexture'],
41
+ [/纹理|Texture/i, 'Texture'],
42
+ [/网格|Mesh/i, 'Mesh'],
43
+ [/Shader/i, 'Shader'],
44
+ [/材质|Material/i, 'Material'],
45
+ [/动画|Animation/i, 'AnimationClip'],
46
+ [/音频|Audio/i, 'AudioClip'],
47
+ [/字体|Font/i, 'Font'],
48
+ [/AssetBundle/i, 'AssetBundle'],
49
+ [/TextAsset|文本资源/i, 'TextAsset'],
50
+ [/粒子|Particle/i, 'ParticleSystem'],
51
+ [/Lua/i, 'Lua'],
52
+ [/Mono/i, 'Mono'],
53
+ [/Gfx/i, 'Gfx'],
54
+ ];
55
+ for (const [re, t] of rules)
56
+ if (re.test(label))
57
+ return t;
58
+ return fallback;
59
+ }
60
+ /** 把 summary / UE brief 一类 [{label,value}] 展开成 map。 */
61
+ function expandLabelValueRows(rows) {
62
+ const out = {};
63
+ for (const row of rows) {
64
+ if (!isObj(row))
65
+ continue;
66
+ const label = String(row['label'] ?? row['key'] ?? '');
67
+ if (!label)
68
+ continue;
69
+ out[label] = firstVal(row['value'] ?? row['data']);
70
+ const key = row['key'];
71
+ if (typeof key === 'string' && key && !(key in out))
72
+ out[key] = out[label];
73
+ }
74
+ return out;
37
75
  }
38
76
  function basicInfoMap(basicInfo) {
39
77
  const out = {};
@@ -75,6 +113,61 @@ function parseHasResource(summary, brief) {
75
113
  }
76
114
  return null;
77
115
  }
116
+ /**
117
+ * UE 2.0:从 Resource / resource 段判定资源采集。
118
+ * 「资源内存占用峰值」> 0 → true;段存在但无法确认时返回 null(不误报关闭)。
119
+ */
120
+ function parseHasResourceFromResourceSection(report) {
121
+ const section = report['Resource'] ?? report['resource'];
122
+ if (!Array.isArray(section) || section.length === 0)
123
+ return null;
124
+ let peak = null;
125
+ let anyMemPositive = false;
126
+ for (const item of section) {
127
+ if (!isObj(item))
128
+ continue;
129
+ const label = String(item['label'] ?? item['key'] ?? '');
130
+ const n = numOf(item['value'] ?? item['data']);
131
+ if (n == null)
132
+ continue;
133
+ if (/^资源内存占用峰值/.test(label) || /资源内存占用峰值(MB)/.test(label))
134
+ peak = n;
135
+ const unit = unitFromLabel(label) ?? '';
136
+ if (looksLikeMemoryMetric(label, '', unit) && n > 0)
137
+ anyMemPositive = true;
138
+ }
139
+ if (peak != null && peak > 0)
140
+ return true;
141
+ if (anyMemPositive)
142
+ return true;
143
+ return null;
144
+ }
145
+ /** 解析 Resource / resource 数组 → memory[](排除数量峰值)。 */
146
+ function memoryFromResourceSection(report) {
147
+ const section = report['Resource'] ?? report['resource'];
148
+ if (!Array.isArray(section))
149
+ return [];
150
+ const memory = [];
151
+ for (const item of section) {
152
+ if (!isObj(item))
153
+ continue;
154
+ const label = String(item['label'] ?? item['key'] ?? '');
155
+ const num = numOf(item['value'] ?? item['data']);
156
+ if (!label || num == null)
157
+ continue;
158
+ const unit = unitFromLabel(label) ?? 'mb';
159
+ if (!looksLikeMemoryMetric(label, '', unit))
160
+ continue;
161
+ // 总「资源内存占用峰值」不绑具体类型,仍保留便于看总量
162
+ memory.push({
163
+ name: label,
164
+ assetType: guessAssetType(label, 'Resource'),
165
+ memoryBytes: toBytes(num, unit),
166
+ _source: 'overview.Resource',
167
+ });
168
+ }
169
+ return memory;
170
+ }
78
171
  function extractV2(data) {
79
172
  const summary = basicInfoMap(data['basicInfo']);
80
173
  const brief = {};
@@ -130,24 +223,21 @@ function extractV2(data) {
130
223
  shape: 'v2',
131
224
  };
132
225
  }
133
- function extractV1(report) {
226
+ function extractV1OrUe(report) {
134
227
  const brief = {};
135
228
  const summary = {};
136
229
  const functions = [];
137
230
  const memory = [];
138
- if (isObj(report['brief'])) {
231
+ const briefIsArray = Array.isArray(report['brief']);
232
+ if (briefIsArray) {
233
+ Object.assign(brief, expandLabelValueRows(report['brief']));
234
+ }
235
+ else if (isObj(report['brief'])) {
139
236
  for (const [k, v] of Object.entries(report['brief']))
140
237
  brief[k] = firstVal(v);
141
238
  }
142
239
  if (Array.isArray(report['summary'])) {
143
- for (const row of report['summary']) {
144
- if (!isObj(row))
145
- continue;
146
- const label = String(row['label'] ?? row['key'] ?? '');
147
- if (!label)
148
- continue;
149
- summary[label] = firstVal(row['value'] ?? row['data']);
150
- }
240
+ Object.assign(summary, expandLabelValueRows(report['summary']));
151
241
  }
152
242
  else if (isObj(report['reportInfo'])) {
153
243
  Object.assign(summary, report['reportInfo']);
@@ -178,6 +268,29 @@ function extractV1(report) {
178
268
  });
179
269
  }
180
270
  }
271
+ // UE 数组 brief 里也可能夹带内存类指标(类型级)
272
+ if (briefIsArray) {
273
+ for (const [label, v] of Object.entries(brief)) {
274
+ if (!looksLikeMemoryMetric(label, '', unitFromLabel(label) ?? ''))
275
+ continue;
276
+ const num = numOf(v);
277
+ if (num == null)
278
+ continue;
279
+ if (memory.some((m) => m.name === label))
280
+ continue;
281
+ memory.push({
282
+ name: label,
283
+ assetType: guessAssetType(label, 'brief'),
284
+ memoryBytes: toBytes(num, unitFromLabel(label) ?? 'mb'),
285
+ _source: 'overview.brief',
286
+ });
287
+ }
288
+ }
289
+ for (const row of memoryFromResourceSection(report)) {
290
+ if (memory.some((m) => m.name === row.name))
291
+ continue;
292
+ memory.push(row);
293
+ }
181
294
  if (Array.isArray(report['asset_stats'])) {
182
295
  for (const item of report['asset_stats']) {
183
296
  if (!isObj(item))
@@ -191,7 +304,7 @@ function extractV1(report) {
191
304
  const looksMb = /mb|内存/i.test(label);
192
305
  memory.push({
193
306
  name: label,
194
- assetType: label,
307
+ assetType: guessAssetType(label, label),
195
308
  memoryBytes: looksMb ? toBytes(num, 'mb') : toBytes(num, 'b'),
196
309
  _source: 'overview.asset_stats',
197
310
  });
@@ -210,13 +323,15 @@ function extractV1(report) {
210
323
  }
211
324
  functions.sort((a, b) => b.value - a.value);
212
325
  memory.sort((a, b) => b.memoryBytes - a.memoryBytes);
326
+ const fromSection = parseHasResourceFromResourceSection(report);
327
+ const fromFields = parseHasResource(summary, brief);
213
328
  return {
214
329
  brief,
215
330
  summary,
216
331
  functions,
217
332
  memory,
218
- hasResource: parseHasResource(summary, brief),
219
- shape: 'v1',
333
+ hasResource: fromSection ?? fromFields,
334
+ shape: briefIsArray ? 'ue-v2' : 'v1',
220
335
  };
221
336
  }
222
337
  /** 把 unwrap 后的报告或原始 data 统一成 OverviewView。 */
@@ -243,10 +358,16 @@ export function viewOverview(data) {
243
358
  if (Array.isArray(v) && v.length && isObj(v[0]))
244
359
  return viewOverview(v[0]);
245
360
  }
246
- if ('brief' in root || 'func' in root || 'summary' in root || 'asset_stats' in root)
247
- return extractV1(root);
361
+ if ('brief' in root ||
362
+ 'func' in root ||
363
+ 'summary' in root ||
364
+ 'asset_stats' in root ||
365
+ 'Resource' in root ||
366
+ 'resource' in root) {
367
+ return extractV1OrUe(root);
368
+ }
248
369
  // 可能整包就是 v2 包在更深一层
249
370
  if (values.length && isObj(values[0]) && Array.isArray(values[0]['categories']))
250
371
  return extractV2(values[0]);
251
- return extractV1(root);
372
+ return extractV1OrUe(root);
252
373
  }
@@ -20,22 +20,33 @@ function summarizeScenes(data) {
20
20
  }
21
21
  return { keys: Object.keys(obj).slice(0, 30) };
22
22
  }
23
- /** brief 全量可能很长,体检只保留常见关键指标。 */
23
+ /** brief 全量可能很长,体检只保留常见关键指标(含 Unity key 与 UE 中文 label)。 */
24
24
  function pickImportantBrief(brief) {
25
- const prefer = [
25
+ const preferExact = [
26
26
  'fps_mean',
27
27
  'fps',
28
28
  'FPS均值',
29
+ 'FPS均值(帧/秒)',
29
30
  'jank_rate',
31
+ 'Jank均值',
32
+ 'Jank均值(次/分钟)',
30
33
  'cpu_freq_mean',
31
34
  'Total_Reserved_Memory_Bytes_maximum',
32
35
  'Reserved Total峰值',
36
+ '设备内存峰值(MB)',
33
37
  'lua_total_memory_maximum',
34
38
  ];
39
+ const preferLoose = [/fps/i, /jank/i, /设备内存/, /reserved.*memory/i, /卡顿/];
35
40
  const out = {};
36
- for (const k of prefer)
41
+ for (const k of preferExact)
37
42
  if (brief[k] !== undefined)
38
43
  out[k] = brief[k];
44
+ for (const [k, v] of Object.entries(brief)) {
45
+ if (k in out)
46
+ continue;
47
+ if (preferLoose.some((re) => re.test(k)))
48
+ out[k] = v;
49
+ }
39
50
  let n = 0;
40
51
  for (const [k, v] of Object.entries(brief)) {
41
52
  if (k in out)
@@ -72,7 +83,6 @@ export const reportDiagnosisTool = {
72
83
  const { target, data } = await fetchOverviewStatistic(ctx.client, identity);
73
84
  apisUsed.push(target.id);
74
85
  const view = viewOverview(data);
75
- const hasRes = view.hasResource === true;
76
86
  let topResources = null;
77
87
  try {
78
88
  topResources = await runTopResources(ctx.client, { ...key, topN: 10 });
@@ -116,8 +126,10 @@ export const reportDiagnosisTool = {
116
126
  catch (e) {
117
127
  limitations.push(`场景统计失败:${e instanceof Error ? e.message : String(e)}`);
118
128
  }
119
- if (!hasRes)
120
- limitations.push('未开启资源采集或无法从 brief 判定,逐资源 Top 可能为空');
129
+ // 仅在明确未开启时提示;hasResource === null 表示无法判定,不误报关闭
130
+ if (view.hasResource === false) {
131
+ limitations.push('未开启资源采集,逐资源 Top 可能为空');
132
+ }
121
133
  const result = {
122
134
  report: {
123
135
  dataKey: identity.dataKey,
@@ -130,10 +142,15 @@ export const reportDiagnosisTool = {
130
142
  },
131
143
  routedOverview: target.id,
132
144
  overviewShape: view.shape,
145
+ hasResource: view.hasResource,
133
146
  summary: view.summary,
134
147
  brief: pickImportantBrief(view.brief),
135
148
  topResources: topResources
136
- ? { items: topResources['items'], note: topResources['_note'] }
149
+ ? {
150
+ items: topResources['items'],
151
+ byType: topResources['byType'],
152
+ note: topResources['_note'],
153
+ }
137
154
  : null,
138
155
  topFunctions: topFunctions
139
156
  ? {
@@ -56,6 +56,7 @@ export declare function fetchReportIdentity(client: UwaClient, key: {
56
56
  export declare function resolveOverviewTarget(identity: ReportIdentity): OverviewTarget;
57
57
  /** Unity 场景统计路由;UE 无独立 scene v1/v2 对,返回 null。 */
58
58
  export declare function resolveSceneTarget(identity: ReportIdentity): SceneTarget | null;
59
+ /** Unity / UE 共用 AT 资源总览列表;解析日 ≥ 2026-06-25 时优先走这条。 */
59
60
  export declare function preferAtResourceTable(identity: ReportIdentity): boolean;
60
61
  /** 调用已路由的 Overview 统计,返回原始 data + 路由元信息。 */
61
62
  export declare function fetchOverviewStatistic(client: UwaClient, identity: ReportIdentity): Promise<{
@@ -136,8 +136,9 @@ export function resolveSceneTarget(identity) {
136
136
  apiVersion: 'v1.0.1',
137
137
  };
138
138
  }
139
+ /** Unity / UE 共用 AT 资源总览列表;解析日 ≥ 2026-06-25 时优先走这条。 */
139
140
  export function preferAtResourceTable(identity) {
140
- return identity.engine === 'unity' && identity.createDate != null && dayGte(identity.createDate, AT_RESOURCE_DAY);
141
+ return identity.createDate != null && dayGte(identity.createDate, AT_RESOURCE_DAY);
141
142
  }
142
143
  /** 调用已路由的 Overview 统计,返回原始 data + 路由元信息。 */
143
144
  export async function fetchOverviewStatistic(client, identity) {
@@ -2,28 +2,45 @@ import { z } from './types.js';
2
2
  import { errorResult, isObj, jsonToolResult, requireReportKey, topNOf, } from './helpers.js';
3
3
  import { fetchOverviewStatistic, fetchReportIdentity, preferAtResourceTable, } from './route-overview.js';
4
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) {
5
+ /** OpenAPI memory/manage、AT overall 文档枚举对齐(11 种)。 */
6
+ const DEFAULT_ASSET_TYPES = [
7
+ 'Texture',
8
+ 'Mesh',
9
+ 'AnimationClip',
10
+ 'AudioClip',
11
+ 'Material',
12
+ 'Shader',
13
+ 'Font',
14
+ 'RenderTexture',
15
+ 'ParticleSystem',
16
+ 'AssetBundle',
17
+ 'TextAsset',
18
+ ];
19
+ /** AT 资源总览(Unity / UE 实测均可用)。 */
20
+ async function fetchAtAssetType(client, identity, assetType) {
21
+ const keyQuery = identity.dataKey
22
+ ? { dataKey: identity.dataKey }
23
+ : { recordId: identity.recordId };
24
+ const meta = (await client.call('GET', '/openapi/v1/data/gotonline/overview/at/resource/overall/table/presign', 'v1.0.1', { ...keyQuery, assetType }));
25
+ const url = typeof meta['dataPresignUrl'] === 'string' ? meta['dataPresignUrl'] : null;
26
+ if (!url)
27
+ return [];
28
+ const payload = await client.downloadPresign(url);
29
+ const resources = extractAtResources(payload.json);
30
+ return resources.map((r) => ({
31
+ name: String(r['name'] ?? r['id'] ?? ''),
32
+ assetType,
33
+ memoryBytes: Number(r['memMaximum'] ?? 0) || 0,
34
+ count: r['countMaximum'] != null ? Number(r['countMaximum']) : undefined,
35
+ extras: pickExtras(r, ['id', 'typeName', 'tags', 'lifecycle', 'properties']),
36
+ _source: 'at/resource/overall/table/presign',
37
+ }));
38
+ }
39
+ /** Unity 旧路径:memory/manage(仅 Unity,且 AT 不可用时回退)。 */
40
+ async function fetchMemoryManageAssetType(client, identity, assetType) {
7
41
  const keyQuery = identity.dataKey
8
42
  ? { dataKey: identity.dataKey }
9
43
  : { 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
44
  const data = (await client.call('GET', '/openapi/v1/data/gotonline/overview/memory/manage/data/report', 'v1.0.1', { ...keyQuery, assetType }));
28
45
  const dict = isObj(data['Asset_dict']) ? data['Asset_dict'] : {};
29
46
  return Object.values(dict)
@@ -46,7 +63,6 @@ function extractAtResources(json) {
46
63
  return [];
47
64
  if (Array.isArray(json['resources']))
48
65
  return json['resources'].filter(isObj);
49
- // 偶发整包就是一张表
50
66
  for (const v of Object.values(json)) {
51
67
  if (Array.isArray(v) && v.length && isObj(v[0]) && ('memMaximum' in v[0] || 'name' in v[0])) {
52
68
  return v.filter(isObj);
@@ -61,7 +77,6 @@ function pickExtras(r, keys) {
61
77
  out[k] = r[k];
62
78
  return Object.keys(out).length ? out : undefined;
63
79
  }
64
- /** 从 Overview 统计抽出类型级内存峰值(UE / 无逐资源列表时用)。 */
65
80
  function typeLevelFromOverview(view) {
66
81
  return view.memory.map((m) => ({
67
82
  name: m.name,
@@ -70,84 +85,143 @@ function typeLevelFromOverview(view) {
70
85
  _source: m._source,
71
86
  }));
72
87
  }
88
+ async function fetchPerAssetRows(client, identity, assetTypes, useAt) {
89
+ const apisUsed = [];
90
+ const errors = [];
91
+ const items = [];
92
+ const settled = await Promise.allSettled(assetTypes.map((t) => useAt ? fetchAtAssetType(client, identity, t) : fetchMemoryManageAssetType(client, identity, t)));
93
+ for (let i = 0; i < settled.length; i++) {
94
+ const r = settled[i];
95
+ const t = assetTypes[i];
96
+ if (r.status === 'fulfilled') {
97
+ items.push(...r.value);
98
+ apisUsed.push(useAt ? `at_resource_overall:${t}` : `memory_manage:${t}`);
99
+ }
100
+ else {
101
+ errors.push(`${t}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
102
+ }
103
+ }
104
+ return {
105
+ items,
106
+ apisUsed,
107
+ errors,
108
+ sourceKind: useAt ? 'at_resource_overall_presign' : 'memory_manage',
109
+ };
110
+ }
111
+ function groupByAssetType(items, assetTypes, topN) {
112
+ const byType = {};
113
+ for (const t of assetTypes) {
114
+ byType[t] = items
115
+ .filter((r) => r.assetType === t)
116
+ .sort((a, b) => b.memoryBytes - a.memoryBytes)
117
+ .slice(0, topN);
118
+ }
119
+ return byType;
120
+ }
121
+ function normalizeGroupBy(raw) {
122
+ return raw === 'assetType' ? 'assetType' : 'merged';
123
+ }
73
124
  export async function runTopResources(client, args) {
74
125
  const key = requireReportKey(args);
75
126
  const topN = topNOf(args, 20);
76
127
  const assetTypes = normalizeAssetTypes(args['assetTypes']);
128
+ const groupBy = normalizeGroupBy(args['groupBy']);
77
129
  const identity = await fetchReportIdentity(client, key);
78
130
  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
131
+ // 先拉 Overview,用于资源开关判定与最终回退
95
132
  const { target, data } = await fetchOverviewStatistic(client, identity);
96
133
  apisUsed.push(target.id);
97
134
  const view = viewOverview(data);
98
- const hasRes = view.hasResource === true;
99
- if (!hasRes) {
100
- const items = typeLevelFromOverview(view).slice(0, topN);
135
+ const canAt = preferAtResourceTable(identity);
136
+ // Unity 旧报告可回退 memory/manage;UE 没有这条,只能 AT 或类型级峰值
137
+ const useAt = canAt || identity.engine === 'unreal';
138
+ const useMemoryManage = !useAt && identity.engine === 'unity';
139
+ const packResult = (items, extra) => {
140
+ const sorted = [...items].sort((a, b) => b.memoryBytes - a.memoryBytes);
141
+ if (groupBy === 'assetType') {
142
+ const byType = groupByAssetType(sorted, assetTypes, topN);
143
+ const flat = Object.values(byType).flat();
144
+ return {
145
+ engine: identity.engine,
146
+ dataKey: identity.dataKey,
147
+ createDate: identity.createDate,
148
+ sdkVersion: identity.sdkVersion,
149
+ assetTypes,
150
+ topN,
151
+ groupBy,
152
+ byType,
153
+ items: flat,
154
+ _overviewShape: view.shape,
155
+ ...extra,
156
+ };
157
+ }
101
158
  return {
102
159
  engine: identity.engine,
103
160
  dataKey: identity.dataKey,
161
+ createDate: identity.createDate,
162
+ sdkVersion: identity.sdkVersion,
163
+ assetTypes,
104
164
  topN,
105
- items,
106
- _note: view.hasResource === false
107
- ? '报告未开启资源采集(OverviewHasResource=0),无法拉逐资源列表;已回退为 Overview 类型级峰值。'
108
- : '无法从统计结果判定是否开启资源采集;已回退为 Overview 类型级峰值。',
109
- _apisUsed: apisUsed,
110
- _limitations: [view.hasResource === false ? '未开启资源采集' : '资源采集状态未知'],
165
+ groupBy,
166
+ items: sorted.slice(0, topN),
111
167
  _overviewShape: view.shape,
168
+ ...extra,
112
169
  };
170
+ };
171
+ if (!useAt && !useMemoryManage) {
172
+ return packResult(typeLevelFromOverview(view), {
173
+ _note: '无法使用 AT 资源列表,已回退为 Overview 类型级峰值。',
174
+ _apisUsed: apisUsed,
175
+ _limitations: ['at_resource_unavailable'],
176
+ });
113
177
  }
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
- }
178
+ // Unity:明确未开资源采集时不必打 AT;UE 的 AT 实测可直接用,不依赖 OverviewHasResource 字段
179
+ if (identity.engine === 'unity' && view.hasResource === false && !canAt) {
180
+ return packResult(typeLevelFromOverview(view), {
181
+ _note: '报告未开启资源采集,无法拉逐资源列表;已回退为 Overview 类型级峰值。',
182
+ _apisUsed: apisUsed,
183
+ _limitations: ['未开启资源采集'],
184
+ });
128
185
  }
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',
186
+ let fetched = await fetchPerAssetRows(client, identity, assetTypes, useAt);
187
+ // AT 全部失败时,Unity 还可回退 memory/manage
188
+ if (useAt && fetched.items.length === 0 && identity.engine === 'unity') {
189
+ const fallback = await fetchPerAssetRows(client, identity, assetTypes, false);
190
+ if (fallback.items.length > 0 || fallback.errors.length === 0)
191
+ fetched = fallback;
192
+ }
193
+ apisUsed.push(...fetched.apisUsed);
194
+ if (fetched.items.length === 0) {
195
+ const fallbackItems = typeLevelFromOverview(view);
196
+ return packResult(fallbackItems, {
197
+ _sourceKind: fetched.sourceKind,
198
+ _apisUsed: apisUsed,
199
+ ...(fetched.errors.length ? { _partialErrors: fetched.errors } : {}),
200
+ _note: fallbackItems.length > 0
201
+ ? '逐资源列表为空,已回退为 Overview 类型级峰值。'
202
+ : '逐资源列表与类型级峰值均为空(报告可能未采集资源内存)。',
203
+ _limitations: ['per_asset_empty'],
204
+ });
205
+ }
206
+ const note = groupBy === 'assetType'
207
+ ? `已按类型各取 Top ${topN}(${fetched.sourceKind},共 ${assetTypes.length} 种)。`
208
+ : `已按内存峰值合并 ${assetTypes.length} 种资源类型(${fetched.sourceKind}),返回全局 Top ${topN}。`;
209
+ return packResult(fetched.items, {
210
+ totalCandidates: fetched.items.length,
211
+ _sourceKind: fetched.sourceKind,
141
212
  _apisUsed: apisUsed,
142
- ...(errors.length ? { _partialErrors: errors } : {}),
143
- _note: `已按内存峰值合并 ${assetTypes.length} 种资源类型,返回 Top ${top.length}。`,
144
- };
213
+ ...(fetched.errors.length ? { _partialErrors: fetched.errors } : {}),
214
+ _note: note,
215
+ });
145
216
  }
146
217
  function normalizeAssetTypes(raw) {
147
218
  if (Array.isArray(raw) && raw.length)
148
219
  return raw.map(String);
149
220
  if (typeof raw === 'string' && raw.trim()) {
150
- return raw.split(/[,,]/).map((s) => s.trim()).filter(Boolean);
221
+ return raw
222
+ .split(/[,,]/)
223
+ .map((s) => s.trim())
224
+ .filter(Boolean);
151
225
  }
152
226
  return [...DEFAULT_ASSET_TYPES];
153
227
  }
@@ -157,10 +231,11 @@ export const topResourcesTool = {
157
231
  engines: ['unity', 'unreal'],
158
232
  filterKeys: ['top_resources', 'composite', 'preset.default', 'overview'],
159
233
  description: [
160
- '聚合指定报告的资源内存占用 Top N',
161
- '适用引擎:Unity / UE|复合工具(内部串多个接口,调用方不必自己选 v1/v2)',
162
- 'Unity:按 assetType 并行拉取资源列表(解析日 2026-06-25 优先 AT 总览预签名;否则 memory/manage),合并后按内存峰值排序。',
163
- 'UE:无逐资源列表,返回 Overview 统计中的类型级峰值,并在 _note 中标明。',
234
+ '聚合指定报告的资源内存占用 Top N(逐资源)。',
235
+ '适用引擎:Unity / UE|复合工具',
236
+ 'Unity / UE 均优先走 AT 资源总览预签名(at/resource/overall/table/presign),按 assetType 并行下载后合并排序。',
237
+ 'groupBy=merged(默认):跨类型合并后取全局 Top N;groupBy=assetType:每种类型各取 Top N,返回 byType。',
238
+ 'Unity 旧报告(解析日 < 2026-06-25)回退 memory/manage;拉不到逐资源时再回退 Overview 类型级峰值。',
164
239
  '不确定报告版本时直接用本工具;需要原始全量列表时再调对应原子工具。',
165
240
  ].join('\n'),
166
241
  inputSchema: {
@@ -169,8 +244,12 @@ export const topResourcesTool = {
169
244
  assetTypes: z
170
245
  .array(z.string())
171
246
  .optional()
172
- .describe(`资源类型列表,默认 ${DEFAULT_ASSET_TYPES.join(',')}。Unity 每次内部按类型各查一次再合并`),
173
- topN: z.number().optional().describe('返回条数,默认 20,最大 200'),
247
+ .describe(`资源类型列表,默认全部 11 种:${DEFAULT_ASSET_TYPES.join(',')}。每次内部按类型各查一次`),
248
+ groupBy: z
249
+ .enum(['merged', 'assetType'])
250
+ .optional()
251
+ .describe("分组模式:merged=跨类型全局 Top N(默认);assetType=每种类型各 Top N,返回 byType"),
252
+ topN: z.number().optional().describe('返回条数,默认 20,最大 200;groupBy=assetType 时为每类型条数'),
174
253
  },
175
254
  async handler(args, ctx) {
176
255
  try {
package/dist/server.d.ts CHANGED
@@ -17,7 +17,8 @@ export interface ServerOptions {
17
17
  timeoutMs: number;
18
18
  specPath?: string;
19
19
  }
20
- export declare const PACKAGE_VERSION = "0.2.0";
20
+ /** package.json 同步,避免 MCP 握手版本和 npm 包不一致。 */
21
+ export declare const PACKAGE_VERSION: string;
21
22
  export declare function createServer(opts: ServerOptions): {
22
23
  server: McpServer;
23
24
  toolCount: number;
package/dist/server.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
1
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
6
  import { UwaClient } from './client.js';
@@ -5,7 +8,18 @@ import { registerCompositeTools, COMPOSITE_TOOLS, selectCompositeTools } from '.
5
8
  import { selectOperations } from './presets.js';
6
9
  import { loadSpec } from './spec.js';
7
10
  import { makeHandler, toolConfig, toolName } from './tools.js';
8
- export const PACKAGE_VERSION = '0.2.0';
11
+ function readPackageVersion() {
12
+ try {
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
15
+ return pkg.version ?? '0.0.0';
16
+ }
17
+ catch {
18
+ return '0.0.0';
19
+ }
20
+ }
21
+ /** 与 package.json 同步,避免 MCP 握手版本和 npm 包不一致。 */
22
+ export const PACKAGE_VERSION = readPackageVersion();
9
23
  export function createServer(opts) {
10
24
  const spec = loadSpec(opts.specPath);
11
25
  const operations = selectOperations(spec.operations, { tools: opts.tools, engine: opts.engine });
@@ -50,6 +50,9 @@ export const VERSION_GUIDE = {
50
50
  '【选用规则——务必先看】',
51
51
  '- 自定义面板帧数据 2.0。需 SDK ≥ 2.5.1,最多 3 个面板;不支持内存类面板。',
52
52
  '- 要统计值(含场景维度)用 gotonline_overview_indicator_statistic_dashboard。',
53
+ '- 【横轴——勿混用】外层 x_axis 为逐帧共享横轴(1、2、3…);y_axis.{指标名}.x_axis 为该曲线专属横轴(常见步长 30:0、30、60…)。',
54
+ '- 读取某条曲线时:若存在专属 x_axis 必须用专属,否则才用外层共享 x_axis。把共享横轴套到 FPS 等专属曲线会错位。',
55
+ '- 响应另含 _axisBinding / _axisHint,按其中 xAxisSource 取值即可。',
53
56
  ].join('\n'),
54
57
  gotonline_overview_memory_usage_snapshot: [
55
58
  '【选用规则——务必先看】',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwa4d/openapi-mcp",
3
- "version": "0.2.0-beta.2",
3
+ "version": "0.2.0-beta.3",
4
4
  "description": "UWA 开放平台 MCP Server,将 UWA Open API 暴露为 MCP 工具供 AI 助手调用",
5
5
  "type": "module",
6
6
  "bin": {