@uwa4d/openapi-mcp 0.2.0-beta.7 → 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 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
+ }
@@ -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 = [
@@ -68,6 +87,8 @@ export const reportDiagnosisTool = {
68
87
  '调用方不必自己判断 2026-07-09 / 2026-03-25 分界或 SDK 版本——内部按 get_report_detail 路由。',
69
88
  'Top 函数:Overview 走 method/idmap/statistic v1.0.2 + stackTestMode;Mono 走堆栈树;逐帧曲线用 method/curve/presign v1.0.2。',
70
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)。',
71
92
  '返回摘要而非原始大包;需要明细时再按 _apisUsed 中的原子工具深挖。',
72
93
  ].join('\n'),
73
94
  inputSchema: {
@@ -121,12 +142,30 @@ export const reportDiagnosisTool = {
121
142
  apisUsed.push(scene.target.id);
122
143
  }
123
144
  else {
124
- limitations.push('当前引擎无 Unity 场景统计路由,已跳过场景摘要');
145
+ limitations.push('当前引擎无 Unity 场景统计路由,已跳过场景统计摘要');
125
146
  }
126
147
  }
127
148
  catch (e) {
128
149
  limitations.push(`场景统计失败:${e instanceof Error ? e.message : String(e)}`);
129
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
+ }
130
169
  // 仅在明确未开启时提示;hasResource === null 表示无法判定,不误报关闭
131
170
  if (view.hasResource === false) {
132
171
  limitations.push('未开启资源采集,逐资源 Top 可能为空');
@@ -4,3 +4,9 @@ export declare const INDICATOR_DASHBOARD_KEYS: readonly string[];
4
4
  export declare const INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES: readonly ["fps_mean", "fps_maximum", "frametime_min", "frametime_gt_40_pct", "temperature_mean", "drawcall_maximum"];
5
5
  export declare function splitIndicatorDashboards(raw: unknown): string[];
6
6
  export declare function findUnknownIndicatorDashboards(raw: unknown): string[];
7
+ /** 拆成已接受 / 未识别面板名(保持请求顺序,已接受去重)。 */
8
+ export declare function partitionIndicatorDashboards(raw: unknown): {
9
+ accepted: string[];
10
+ unknown: string[];
11
+ all: string[];
12
+ };
@@ -28,3 +28,22 @@ export function findUnknownIndicatorDashboards(raw) {
28
28
  const valid = new Set(INDICATOR_DASHBOARD_KEYS);
29
29
  return parts.filter((k) => !valid.has(k));
30
30
  }
31
+ /** 拆成已接受 / 未识别面板名(保持请求顺序,已接受去重)。 */
32
+ export function partitionIndicatorDashboards(raw) {
33
+ const all = splitIndicatorDashboards(raw);
34
+ const valid = new Set(INDICATOR_DASHBOARD_KEYS);
35
+ const accepted = [];
36
+ const unknown = [];
37
+ const seen = new Set();
38
+ for (const k of all) {
39
+ if (!valid.has(k)) {
40
+ unknown.push(k);
41
+ continue;
42
+ }
43
+ if (seen.has(k))
44
+ continue;
45
+ seen.add(k);
46
+ accepted.push(k);
47
+ }
48
+ return { accepted, unknown, all };
49
+ }
package/dist/presets.js CHANGED
@@ -22,9 +22,14 @@ export const PRESET_DEFAULT = [
22
22
  'list_optimization_tasks',
23
23
  'gotonline_overview_indicator_statistic_dashboard',
24
24
  'gotonline_overview_indicator_curve_dashboard',
25
+ 'gotonline_overview_indicator_dashboard_keys',
25
26
  'gotonline_overview_memory_manage_data_report',
26
27
  'gotonline_overview_method_group_statistic',
28
+ 'gotonline_overview_stack_id_map',
27
29
  'gotonline_overview_stack_stutter_full_presign',
30
+ 'gotonline_overview_stack_stutter_full_tree_presign',
31
+ 'gotonline_overview_stack_stutter_scene_tree_presign',
32
+ 'gotonline_overview_stack_tree_frame_presign',
28
33
  // 复合工具(手写,见 src/composite/)
29
34
  'top_resources',
30
35
  'top_functions',
package/dist/tools.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { UwaApiError } from './client.js';
3
3
  import { INDICATOR_DASHBOARD_PANEL_GUIDE, versionGuideFor } from './version-guide.js';
4
- import { findUnknownIndicatorDashboards, INDICATOR_DASHBOARD_KEYS, INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES, } from './indicator-dashboard-keys.js';
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
6
  const ENGINE_LABEL = { unity: 'Unity', unreal: 'UE' };
6
7
  /**
7
8
  * 预签名数据默认完整返回,不按条数截断——实测各接口解压后在 10 KB ~ 500 KB 之间,
@@ -86,6 +87,13 @@ function describe(op) {
86
87
  }
87
88
  return lines.join('\n');
88
89
  }
90
+ const SERVICE_META_TYPE_ENUM = 'PA(真人真机)、GOT_ONLINE(GOT Online)、LT(本地资源检测)、AB(AssetBundle 检测)、ATP(自动化功能测试)、BR(Build 分析)';
91
+ /** 个别接口默认 apiVersion 与上游文档「推荐值」不一致时,在 MCP 侧纠偏。 */
92
+ function defaultApiVersionFor(op) {
93
+ if (op.id === 'gotonline_overview_method_group_statistic')
94
+ return 'v1.0.2';
95
+ return op.defaultApiVersion;
96
+ }
89
97
  export function buildInputSchema(op) {
90
98
  const shape = {};
91
99
  for (const p of [...op.query, ...op.body]) {
@@ -95,30 +103,58 @@ export function buildInputSchema(op) {
95
103
  desc = [
96
104
  desc,
97
105
  '传参名为面板标识符:后缀 _max/_avg 等为历史命名,不表示只能取峰值或均值。',
98
- `合法取值共 ${INDICATOR_DASHBOARD_KEYS.length} 个;可先调 gotonline_overview_indicator_dashboard_keys 拉全量列表。`,
106
+ '每个面板一次返回全部统计量,勿按 mean/max/min 拆成多个面板名。',
107
+ `合法取值共 ${INDICATOR_DASHBOARD_KEYS.length} 个;可先调 gotonline_overview_indicator_dashboard_keys 拉全量列表(命名含 @ 等,不可从示例归纳)。`,
99
108
  `常见误传(非面板名):${INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES.join('、')}。`,
100
109
  `示例面板名:${sampleKeys.join('、')}。`,
101
110
  ].join(' ');
102
111
  }
112
+ if (p.name === 'serviceMetaTypeList') {
113
+ desc = `${desc.replace(/见下方枚举值/, '枚举如下').replace(/,$/, '')}。合法值:${SERVICE_META_TYPE_ENUM}。`;
114
+ }
115
+ if (p.name === 'stackMethodName') {
116
+ desc = `${desc} 响应含 _resolvedMethod 时请核对实际命中的 stackMethodId(同名会自动取耗时最大者)。`;
117
+ }
103
118
  const base = zodFor(p).describe(p.example ? `${desc}(示例:${p.example})` : desc);
104
119
  shape[p.name] = p.required ? base : base.optional();
105
120
  }
106
121
  if (op.apiVersions.length > 1) {
122
+ const def = defaultApiVersionFor(op);
107
123
  shape['apiVersion'] = z
108
124
  .enum(op.apiVersions)
109
125
  .optional()
110
- .describe(`接口版本,默认 ${op.defaultApiVersion}。不同版本的返回字段有差异,详见文档`);
126
+ .describe(`接口版本,默认 ${def}。不同版本的返回字段有差异,详见文档`);
111
127
  }
112
128
  if (op.returnsPresignUrl) {
129
+ const isHeavyTree = op.id.includes('stack_overall_tree') ||
130
+ op.id.includes('stack_stutter') ||
131
+ op.id.includes('stack_tree');
113
132
  shape['download'] = z
114
133
  .boolean()
115
134
  .optional()
116
- .describe('是否自动下载并解析预签名数据,默认 true。设为 false 时只返回 dataPresignUrl');
135
+ .describe(isHeavyTree
136
+ ? '是否自动下载并解析预签名数据,默认 true。大报告堆栈树建议 download=false,只取 dataPresignUrl 自行处理,避免撑爆上下文'
137
+ : '是否自动下载并解析预签名数据,默认 true。设为 false 时只返回 dataPresignUrl');
117
138
  shape['maxRows'] = z
118
139
  .number()
119
140
  .optional()
120
141
  .describe('可选的条目数上限。默认返回全部数据,只有在明确只需要前 N 条时才传这个参数');
121
142
  }
143
+ if (isIndicatorCurveDashboard(op.path)) {
144
+ shape['valueGt'] = z
145
+ .number()
146
+ .optional()
147
+ .describe('MCP 侧过滤:只保留数值 > 该阈值的曲线点(例:帧耗时慢帧传 40,单位与曲线一致多为 ms)');
148
+ shape['valueLt'] = z.number().optional().describe('MCP 侧过滤:只保留数值 < 该阈值的曲线点');
149
+ shape['topN'] = z
150
+ .number()
151
+ .optional()
152
+ .describe('MCP 侧过滤:在阈值过滤后按数值降序只保留最大的 N 个点');
153
+ shape['returnFramesOnly'] = z
154
+ .boolean()
155
+ .optional()
156
+ .describe('MCP 侧:为 true 时每条曲线只返回 frames[] + values[],进一步压缩体积');
157
+ }
122
158
  return shape;
123
159
  }
124
160
  /** 找出对象里最长的那个数组字段,它通常就是主数据列表。 */
@@ -278,7 +314,7 @@ export function annotateDashboardEmptyResult(op, data, args) {
278
314
  const emptyColumns = !Array.isArray(columns) || columns.length === 0;
279
315
  if (!emptyStat && !emptyColumns)
280
316
  return data;
281
- const unknown = findUnknownIndicatorDashboards(requested);
317
+ const { unknown } = partitionIndicatorDashboards(requested);
282
318
  const hintParts = [
283
319
  'indicatorDashboards 可能传错,导致 statistic/columns 为空。',
284
320
  '面板传参名≠统计维度后缀(如 fps_mean、frametime_min 是返回字段,不是面板名)。',
@@ -376,9 +412,9 @@ export function annotateProjectGroups(data) {
376
412
  content,
377
413
  _deduplicated: true,
378
414
  _uniqueReportCount: placed,
379
- _dedupeNote: `上游把同一份报告重复挂在多个项目组下,已按 link 中 project= 合并归位,去重后 ${placed} 份` +
415
+ _dedupeNote: `上游把同一份报告重复挂在多个项目组下,已按 link 中 project= 合并归位,去重后本页 ${placed} 份` +
380
416
  (orphaned ? `(其中 ${orphaned} 份所属项目组不在本页,已单独列出)` : '') +
381
- '。可直接按项目组统计。',
417
+ '。按项目组统计请用本页 _uniqueReportCount / 各组 Records;跨页汇总须自行按 dataKey 去重,勿把各页条数简单相加。',
382
418
  };
383
419
  }
384
420
  /** MCP 的 content.text 必须是字符串,undefined 会让客户端校验失败。 */
@@ -392,41 +428,74 @@ function asText(value) {
392
428
  export function makeHandler(op, client, defaultMaxRows, maxChars) {
393
429
  return async (args) => {
394
430
  try {
395
- const apiVersion = args['apiVersion'] ?? op.defaultApiVersion;
431
+ const apiVersion = args['apiVersion'] ?? defaultApiVersionFor(op);
396
432
  const wantDownload = args['download'] ?? true;
397
433
  const maxRows = args['maxRows'] ?? defaultMaxRows;
434
+ const { filter: curveFilter, cleaned: argsForApi } = isIndicatorCurveDashboard(op.path)
435
+ ? extractCurveFilterArgs(args)
436
+ : { filter: {}, cleaned: args };
437
+ let acceptedDashboards = [];
438
+ let unknownDashboards = [];
439
+ if (typeof argsForApi['indicatorDashboards'] === 'string') {
440
+ const part = partitionIndicatorDashboards(argsForApi['indicatorDashboards']);
441
+ acceptedDashboards = part.accepted;
442
+ unknownDashboards = part.unknown;
443
+ if (part.all.length && !acceptedDashboards.length) {
444
+ return {
445
+ content: [
446
+ {
447
+ type: 'text',
448
+ text: asText({
449
+ error: 'PARAMETERS_WRONG',
450
+ message: `indicatorDashboards 全部无法识别:${unknownDashboards.join('、')}`,
451
+ _unknownDashboards: unknownDashboards,
452
+ _acceptedDashboards: [],
453
+ _hint: `这些是面板标识符,不是统计维度后缀。` +
454
+ `误传示例:${INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES.join('、')};` +
455
+ `合法示例:fps_avg、frametime@gt_40_pct。` +
456
+ `请调 gotonline_overview_indicator_dashboard_keys 获取全量 ${INDICATOR_DASHBOARD_KEYS.length} 个合法值。`,
457
+ }),
458
+ },
459
+ ],
460
+ isError: true,
461
+ };
462
+ }
463
+ if (acceptedDashboards.length) {
464
+ argsForApi['indicatorDashboards'] = acceptedDashboards.join(',');
465
+ }
466
+ }
398
467
  const query = {};
399
468
  for (const p of op.query)
400
- if (args[p.name] !== undefined)
401
- query[p.name] = args[p.name];
469
+ if (argsForApi[p.name] !== undefined)
470
+ query[p.name] = argsForApi[p.name];
402
471
  let body;
403
472
  if (op.body.length) {
404
473
  body = {};
405
474
  for (const p of op.body)
406
- if (args[p.name] !== undefined)
407
- body[p.name] = args[p.name];
475
+ if (argsForApi[p.name] !== undefined)
476
+ body[p.name] = argsForApi[p.name];
408
477
  }
409
478
  const raw = await client.call(op.method, op.path, apiVersion, query, body);
410
- const unknownDashboards = findUnknownIndicatorDashboards(args['indicatorDashboards']);
411
- if (unknownDashboards.length) {
412
- return {
413
- content: [
414
- {
415
- type: 'text',
416
- text: asText({
417
- error: 'PARAMETERS_WRONG',
418
- message: `indicatorDashboards 含未知面板名:${unknownDashboards.join('、')}`,
419
- _hint: `这些是面板标识符,不是统计维度后缀。` +
420
- `误传示例:${INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES.join('、')};` +
421
- `合法示例:fps_avg、frametime@gt_40_pct。` +
422
- `请调 gotonline_overview_indicator_dashboard_keys 获取全量 ${INDICATOR_DASHBOARD_KEYS.length} 个合法值。`,
423
- }),
424
- },
425
- ],
426
- isError: true,
427
- };
479
+ let processed = annotateProjectGroups(annotateCurveAxes(raw));
480
+ if (isIndicatorStatisticDashboard(op.path) && acceptedDashboards.length) {
481
+ processed = annotateStatisticDashboard(processed, acceptedDashboards, unknownDashboards);
482
+ }
483
+ else if (isIndicatorCurveDashboard(op.path)) {
484
+ if (acceptedDashboards.length || unknownDashboards.length) {
485
+ processed = isObj(processed)
486
+ ? {
487
+ ...processed,
488
+ _acceptedDashboards: acceptedDashboards,
489
+ ...(unknownDashboards.length ? { _unknownDashboards: unknownDashboards } : {}),
490
+ }
491
+ : processed;
492
+ }
493
+ if (hasCurveFilter(curveFilter)) {
494
+ processed = filterCurveData(processed, curveFilter);
495
+ processed = annotateCurveAxes(processed);
496
+ }
428
497
  }
429
- const data = annotateDashboardEmptyResult(op, annotateProjectGroups(annotateCurveAxes(raw)), args);
498
+ const data = annotateDashboardEmptyResult(op, processed, args);
430
499
  const presignUrl = op.returnsPresignUrl && data && typeof data === 'object'
431
500
  ? data['dataPresignUrl']
432
501
  : undefined;
@@ -9,6 +9,11 @@
9
9
  * 挂到所有带 indicatorDashboards 的工具 description,避免模型见 _max 只读峰值。
10
10
  */
11
11
  export declare const INDICATOR_DASHBOARD_PANEL_GUIDE: string;
12
+ /**
13
+ * 堆栈 / 卡顿工具路由。卡顿树已是「多帧合并后的根因视图」,
14
+ * 禁止误导成「对每个 >40ms 帧循环拉指定帧树再聚类」。
15
+ */
16
+ export declare const STACK_ANALYSIS_GUIDE: string;
12
17
  /** 写入工具 description 的「选用规则」块,key 为 operation id。 */
13
18
  export declare const VERSION_GUIDE: Record<string, string>;
14
19
  export declare function versionGuideFor(opId: string): string | undefined;
@@ -11,14 +11,34 @@
11
11
  export const INDICATOR_DASHBOARD_PANEL_GUIDE = [
12
12
  '【indicatorDashboards 传参名称——勿按后缀猜统计维度】',
13
13
  '- 传参名(如 temperature_max、fps_avg、drawcall_cnt_max)是**面板标识符**,_max / _avg / @max 等为历史命名,**不限制**返回 mean / maximum / min 等统计类型。',
14
+ '- 【返回粒度】每个面板一次返回其下所有子指标的全部统计量(mean / min / maximum / maximum_frame / 专属指标,以及 scene_*),',
15
+ ' 无需也无法按统计量拆成多个面板名(勿传 fps_mean、fps_max、fps_min)。面板→子指标展开见返回体 group;入参→出参映射见 _paramFieldMap。',
14
16
  '- 例:temperature_max 面板含子指标 temperature、android_temper_cpu、android_temper_gpu、android_temper_battery;',
15
17
  ' · 曲线接口(indicator_curve / custom_dashboard)→ 各子指标逐帧曲线;',
16
18
  ' · 统计 2.0(indicator_statistic_dashboard)→ statistic 下 *_mean / *_maximum / *_min 及 scene_* 等全量统计。',
17
19
  '- 查温度曲线/均值/峰值:Unity Overview 且 SDK ≥ 2.5.1 时传 indicatorDashboards=temperature_max,再读 y_axis 或 statistic;勿仅看 get_overview_statistic 的汇总段。',
18
- '- 合法面板名先调 gotonline_overview_indicator_dashboard_keys;勿传 fps_mean、frametime_min 等统计字段名。',
19
- '- 误传示例:fps_mean、frametime_gt_40_pct(缺 @)、frametime_min;合法示例:fps_avg、frametime@gt_40_pct',
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
22
  '- UE Overview 当前 Open API 未开放 custom/indicator dashboard 路径(会 24052),UE 温度汇总见 get_ue_overview_statistic_v2 的 temperature 段。',
21
23
  ].join('\n');
24
+ /**
25
+ * 堆栈 / 卡顿工具路由。卡顿树已是「多帧合并后的根因视图」,
26
+ * 禁止误导成「对每个 >40ms 帧循环拉指定帧树再聚类」。
27
+ */
28
+ export const STACK_ANALYSIS_GUIDE = [
29
+ '【堆栈/卡顿选用——务必先看】',
30
+ '- 卡顿根因主路径:用**卡顿聚合树**(已合并全部/场景内卡顿帧),不要对每个慢帧循环拉指定帧树。',
31
+ '- 工具分工:',
32
+ ' · gotonline_overview_stack_stutter_full_presign → 卡顿帧列表(哪些帧卡、类型耗时拆分)',
33
+ ' · gotonline_overview_stack_stutter_full_tree_presign → **全部卡顿帧合并后的堆栈树**(全报告卡顿共性根因)',
34
+ ' · gotonline_overview_stack_stutter_scene_tree_presign → **指定场景内卡顿帧合并树**(主玩法段)',
35
+ ' · gotonline_overview_stack_tree_frame_presign → **单帧**完整树(仅尖峰深挖,如已知 frameId=8439)',
36
+ ' · gotonline_overview_stack_overall_tree_presign → 整线程全量堆栈(非卡顿专用;看持续税/全局热点)',
37
+ ' · gotonline_overview_stack_sample_frame_presign → 树结点逐帧曲线',
38
+ '- 慢帧占比/挑代表帧:indicator 面板 frametime@gt_40_pct(统计或 curve+valueGt);根因归因仍走卡顿聚合树。',
39
+ '- 读任何堆栈树前必须配合 gotonline_overview_stack_id_map(树里是 methodId)。',
40
+ '- 错误假设:❌「没有批量帧树就无法做卡顿根因」——总体/场景卡顿树就是合并结果。',
41
+ ].join('\n');
22
42
  /** 写入工具 description 的「选用规则」块,key 为 operation id。 */
23
43
  export const VERSION_GUIDE = {
24
44
  get_overview_statistic_v1: [
@@ -61,7 +81,8 @@ export const VERSION_GUIDE = {
61
81
  '- 自定义面板统计 2.0。需 SDK ≥ 2.5.1,支持 GPU 指标,最多 15 个面板。',
62
82
  '- 旧接口(抽帧、无 GPU)见 gotonline_overview_custom_dashboard。',
63
83
  '- 只要逐帧曲线不要统计值时用 gotonline_overview_indicator_curve_dashboard。',
64
- '- 返回 statistic 为空时优先怀疑 indicatorDashboards 传错;响应可能含 _hint。',
84
+ '- 返回 statistic 为空时优先怀疑 indicatorDashboards 传错;响应可能含 _hint / _unknownDashboards。',
85
+ '- 响应可能含 _paramFieldMap(入参面板→出参字段)、_duplicateFlatAliases、_sceneUnitNote(scene_* 补单位标注)。',
65
86
  INDICATOR_DASHBOARD_PANEL_GUIDE,
66
87
  ].join('\n'),
67
88
  gotonline_overview_indicator_curve_dashboard: [
@@ -71,6 +92,8 @@ export const VERSION_GUIDE = {
71
92
  '- 【横轴——勿混用】外层 x_axis 为逐帧共享横轴(1、2、3…);y_axis.{指标名}.x_axis 为该曲线专属横轴(常见步长 30:0、30、60…)。',
72
93
  '- 读取某条曲线时:若存在专属 x_axis 必须用专属,否则才用外层共享 x_axis。把共享横轴套到 FPS 等专属曲线会错位。',
73
94
  '- 响应另含 _axisBinding / _axisHint,按其中 xAxisSource 取值即可。',
95
+ '- 【过滤——MCP 侧】可选 valueGt / valueLt / topN / returnFramesOnly:只返回超阈值或 TopN 点,适合慢帧列表(例:frametime@gt_40_pct + valueGt=40)。',
96
+ '- 慢帧列表 ≠ 卡顿根因:根因请用 stutter_full_tree / stutter_scene_tree(卡顿帧已合并),不要对 valueGt 结果逐帧拉 stack_tree_frame。',
74
97
  INDICATOR_DASHBOARD_PANEL_GUIDE,
75
98
  ].join('\n'),
76
99
  gotonline_gpu_curve: [
@@ -105,8 +128,10 @@ export const VERSION_GUIDE = {
105
128
  ].join('\n'),
106
129
  gotonline_overview_method_group_statistic: [
107
130
  '【选用规则——务必先看】',
108
- '- 自定义函数组统计(Unity SDK ≥ 2.5.1;UE 提交日 > 2026-03-25,且 api 版本须 v1.0.2)。',
131
+ '- 自定义函数组统计(Unity SDK ≥ 2.5.1;UE 提交日 > 2026-03-25)。',
132
+ '- **MCP 默认 apiVersion=v1.0.2**(勿用 v1.0.1,会参数错误)。需要旧行为时显式传 apiVersion=v1.0.1。',
109
133
  '- UE 旧报告请用 gotonline_overview_group_export。',
134
+ '- 未配置自定义函数组时 data=[];Overview 热点函数曲线优先用 method_idmap_statistic + method_curve_presign,不必依赖函数组。',
110
135
  ].join('\n'),
111
136
  gotonline_overview_method_idmap_statistic: [
112
137
  '【选用规则——务必先看】',
@@ -139,6 +164,47 @@ export const VERSION_GUIDE = {
139
164
  '- 合法示例:fps_avg、drawcall_cnt_max、frametime@gt_40_pct、temperature_max。',
140
165
  INDICATOR_DASHBOARD_PANEL_GUIDE,
141
166
  ].join('\n'),
167
+ gotonline_overview_stack_id_map: [
168
+ '【选用规则——务必先看】',
169
+ '- 函数 ID ↔ 名称映射。解析任何 stack_*_tree / stutter_*_tree 前先调本工具。',
170
+ STACK_ANALYSIS_GUIDE,
171
+ ].join('\n'),
172
+ gotonline_overview_stack_overall_tree_presign: [
173
+ '【选用规则——务必先看】',
174
+ '- 指定线程的**全量**调用堆栈树(含非卡顿帧),适合持续税/全局热点。',
175
+ '- 只要卡顿根因:改用 stutter_full_tree / stutter_scene_tree,勿用本接口代替卡顿聚合。',
176
+ STACK_ANALYSIS_GUIDE,
177
+ ].join('\n'),
178
+ gotonline_overview_stack_stutter_full_presign: [
179
+ '【选用规则——务必先看】',
180
+ '- 卡顿**帧列表**(帧号 + 卡顿类型耗时拆分),不是堆栈树。',
181
+ '- 要卡顿根因堆栈:接着调 stutter_full_tree_presign(或场景版 stutter_scene_tree)。',
182
+ STACK_ANALYSIS_GUIDE,
183
+ ].join('\n'),
184
+ gotonline_overview_stack_stutter_full_tree_presign: [
185
+ '【选用规则——务必先看】',
186
+ '- **全部卡顿帧已合并**的调用堆栈树 = 卡顿根因主入口(服务端聚合,无需客户端逐帧拉树再聚类)。',
187
+ '- 只要某一场景:用 stutter_scene_tree;只要某一尖峰帧细节:再用 stack_tree_frame。',
188
+ STACK_ANALYSIS_GUIDE,
189
+ ].join('\n'),
190
+ gotonline_overview_stack_stutter_scene_tree_presign: [
191
+ '【选用规则——务必先看】',
192
+ '- **指定场景内卡顿帧已合并**的堆栈树;sceneIndex 来自 list_scenes 下标。',
193
+ '- 全报告卡顿用 stutter_full_tree;单帧深挖用 stack_tree_frame。',
194
+ STACK_ANALYSIS_GUIDE,
195
+ ].join('\n'),
196
+ gotonline_overview_stack_tree_frame_presign: [
197
+ '【选用规则——务必先看】',
198
+ '- **单帧**完整堆栈树(frameId 必填)。用于已知尖峰帧深挖,不是卡顿分析的第一步。',
199
+ '- 卡顿共性根因请先用 stutter_full_tree / stutter_scene_tree(已是多帧合并结果)。',
200
+ '- ❌ 不要对 frametime valueGt 筛出的每个慢帧循环调用本工具来「聚类」。',
201
+ STACK_ANALYSIS_GUIDE,
202
+ ].join('\n'),
203
+ gotonline_overview_stack_sample_frame_presign: [
204
+ '【选用规则——务必先看】',
205
+ '- 堆栈树某个结点的逐帧曲线;结点 id 来自树文件。',
206
+ STACK_ANALYSIS_GUIDE,
207
+ ].join('\n'),
142
208
  };
143
209
  export function versionGuideFor(opId) {
144
210
  return VERSION_GUIDE[opId];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwa4d/openapi-mcp",
3
- "version": "0.2.0-beta.7",
3
+ "version": "0.2.0-beta.8",
4
4
  "description": "UWA 开放平台 MCP Server,将 UWA Open API 暴露为 MCP 工具供 AI 助手调用",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3631,7 +3631,7 @@
3631
3631
  "v1.0.1",
3632
3632
  "v1.0.2"
3633
3633
  ],
3634
- "defaultApiVersion": "v1.0.1",
3634
+ "defaultApiVersion": "v1.0.2",
3635
3635
  "query": [
3636
3636
  {
3637
3637
  "name": "recordId",