@uwa4d/openapi-mcp 0.2.0-beta.1 → 0.2.0-beta.11

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.
Files changed (45) hide show
  1. package/README.md +96 -8
  2. package/dist/annotate-dashboard.d.ts +48 -0
  3. package/dist/annotate-dashboard.js +413 -0
  4. package/dist/cli.js +29 -6
  5. package/dist/client.js +4 -13
  6. package/dist/composite/helpers.d.ts +19 -0
  7. package/dist/composite/helpers.js +90 -0
  8. package/dist/composite/index.d.ts +25 -0
  9. package/dist/composite/index.js +46 -0
  10. package/dist/composite/name-pattern.d.ts +8 -0
  11. package/dist/composite/name-pattern.js +25 -0
  12. package/dist/composite/overview-view.d.ts +33 -0
  13. package/dist/composite/overview-view.js +373 -0
  14. package/dist/composite/report-diagnosis.d.ts +2 -0
  15. package/dist/composite/report-diagnosis.js +347 -0
  16. package/dist/composite/route-overview.d.ts +72 -0
  17. package/dist/composite/route-overview.js +233 -0
  18. package/dist/composite/stack-agg.d.ts +77 -0
  19. package/dist/composite/stack-agg.js +409 -0
  20. package/dist/composite/stack-test-mode.d.ts +33 -0
  21. package/dist/composite/stack-test-mode.js +60 -0
  22. package/dist/composite/top-functions.d.ts +21 -0
  23. package/dist/composite/top-functions.js +178 -0
  24. package/dist/composite/top-resources.d.ts +12 -0
  25. package/dist/composite/top-resources.js +263 -0
  26. package/dist/composite/types.d.ts +25 -0
  27. package/dist/composite/types.js +2 -0
  28. package/dist/error-hints.d.ts +23 -0
  29. package/dist/error-hints.js +80 -0
  30. package/dist/indicator-dashboard-keys.d.ts +12 -0
  31. package/dist/indicator-dashboard-keys.js +50 -0
  32. package/dist/indicator-dashboard-keys.json +338 -0
  33. package/dist/presets.js +9 -0
  34. package/dist/server-instructions.d.ts +5 -0
  35. package/dist/server-instructions.js +17 -0
  36. package/dist/server.d.ts +6 -1
  37. package/dist/server.js +33 -5
  38. package/dist/tools.d.ts +9 -0
  39. package/dist/tools.js +244 -16
  40. package/dist/version-check.d.ts +14 -0
  41. package/dist/version-check.js +94 -0
  42. package/dist/version-guide.d.ts +19 -0
  43. package/dist/version-guide.js +223 -0
  44. package/package.json +2 -2
  45. package/spec/uwa-openapi.json +490 -106
package/README.md CHANGED
@@ -40,7 +40,7 @@ UWA 开放平台 MCP Server。把 UWA Open API 暴露为 MCP 工具,让 Cursor
40
40
  | Claude Desktop | `claude_desktop_config.json` |
41
41
  | Trae | 设置面板中的 MCP 配置 |
42
42
 
43
- 改完重启客户端即可。
43
+ 改完重启客户端即可。多数客户端(Claude Desktop、Trae,以及 PATH 正常的环境)按上面配置即可,**一般不用写 node / npx 绝对路径**。
44
44
 
45
45
  凭证也可以走命令行参数,适配不支持 `env` 字段的客户端:
46
46
 
@@ -57,6 +57,73 @@ UWA 开放平台 MCP Server。把 UWA Open API 暴露为 MCP 工具,让 Cursor
57
57
 
58
58
  > 配置文件里含有凭证,注意不要提交到公开代码仓库。
59
59
 
60
+ ### Cursor 连不上时(按系统)
61
+
62
+ 先在**本机终端**跑通 `npx -y @uwa4d/openapi-mcp check ...`。终端正常、仅 Cursor 失败时,多半是 Cursor 启动 MCP 的环境问题(**不是本包或凭证坏了**)。按日志选对应写法,仍用 `npx`,不必绑死本机 Node 安装路径。
63
+
64
+ | 系统 | 常见日志 | 推荐改法 |
65
+ | --- | --- | --- |
66
+ | **macOS** | `ENOENT .../Cursor.app/.../resources/lib` | 用登录壳启动(见下「macOS」) |
67
+ | **Windows** | `spawn npx ENOENT` | 用 `cmd /c` 包一层(见下「Windows」) |
68
+ | **Linux** | 找不到 `npx` / 类似 PATH 问题 | 用 `bash -lc`(见下「Linux」) |
69
+
70
+ **macOS(推荐)**
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "uwa-openapi": {
76
+ "command": "/bin/zsh",
77
+ "args": ["-lic", "npx -y @uwa4d/openapi-mcp mcp"],
78
+ "env": {
79
+ "UWA_MCP_APP_ID": "<your_app_id>",
80
+ "UWA_MCP_APP_SECRET": "<your_app_secret>"
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ 说明:Cursor 可能把自带 Node 插进 `PATH`,导致 `npx` 找错运行时。`-lic` 走登录壳,加载你平时终端里的 Node(含 nvm 等),不写死版本路径。额外参数写进同一条命令字符串即可。
88
+
89
+ **Windows**
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "uwa-openapi": {
95
+ "command": "cmd",
96
+ "args": ["/c", "npx", "-y", "@uwa4d/openapi-mcp", "mcp"],
97
+ "env": {
98
+ "UWA_MCP_APP_ID": "<your_app_id>",
99
+ "UWA_MCP_APP_SECRET": "<your_app_secret>"
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ 说明:Windows 上 `npx` 实际是 `npx.cmd`,Cursor 直接 `spawn('npx')` 常会 `ENOENT`。经 `cmd /c` 启动即可。请先确认系统已安装 Node 18+,且在 **CMD** 里执行 `npx -v` 成功(若只用 nvm-windows / fnm,需保证其 bin 已进系统 PATH,或改用该工具提供的 `exec` 方式)。
107
+
108
+ **Linux**
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "uwa-openapi": {
114
+ "command": "/bin/bash",
115
+ "args": ["-lc", "npx -y @uwa4d/openapi-mcp mcp"],
116
+ "env": {
117
+ "UWA_MCP_APP_ID": "<your_app_id>",
118
+ "UWA_MCP_APP_SECRET": "<your_app_secret>"
119
+ }
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ 改完后在 Cursor 中 **Restart MCP**(或重启客户端)。若仍失败,把 MCP 输出日志里的完整报错留给支持同学。
126
+
60
127
  ### 先验证一下
61
128
 
62
129
  配置前可以在终端确认凭证和网络是否通:
@@ -78,7 +145,6 @@ npx -y @uwa4d/openapi-mcp check -a <app_id> -s <app_secret>
78
145
  | `--app-id` | `-a` | AppId | `UWA_MCP_APP_ID` |
79
146
  | `--app-secret` | `-s` | AppSecret | `UWA_MCP_APP_SECRET` |
80
147
  | `--base-url` | `-b` | 接口地址,默认 `https://secure-api.uwa4d.com` | `UWA_MCP_API_BASE_URL` |
81
- | `--sandbox` | | 切到测试环境 `https://sandbox-api.uwa4d.com` | |
82
148
  | `--tool` | `-t` | 加载哪些工具,逗号分隔,默认 `preset.default` | `UWA_MCP_TOOL` |
83
149
  | `--engine` | `-e` | 只加载适用于 `unity` / `unreal` 的工具,默认 `all` | `UWA_MCP_ENGINE` |
84
150
  | `--tool-name-case` | `-c` | 工具命名风格 `snake` / `camel`,默认 `snake` | |
@@ -107,6 +173,9 @@ npx -y @uwa4d/openapi-mcp check -a <app_id> -s <app_secret>
107
173
  # 组合:默认集 + 所有预签名类接口 + 单个指定工具
108
174
  -t preset.default,presign,gotonline_overview_stack_id_map
109
175
 
176
+ # 只要复合工具(报告体检 / Top 资源 / Top 函数)
177
+ -t composite
178
+
110
179
  # 只做 Unity 项目,砍掉 UE 专用工具
111
180
  -t preset.default -e unity
112
181
  ```
@@ -118,6 +187,18 @@ npx -y @uwa4d/openapi-mcp list-tools
118
187
  npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
119
188
  ```
120
189
 
190
+ ## 复合工具
191
+
192
+ 默认 preset 里除了原子接口,还有三个**复合工具**——内部会串多个接口并聚合,适合对话式提问:
193
+
194
+ | 工具 | 做什么 |
195
+ | --- | --- |
196
+ | `report_diagnosis` | 单份报告体检:自动选对统计 v1/v2,附带 Top 资源/函数与场景摘要 |
197
+ | `top_resources` | 资源内存 Top N(默认 11 种类型;`groupBy=merged` 全局 Top / `assetType` 按类型各 Top) |
198
+ | `top_functions` | 函数 Top N:下载堆栈树 + idmap,按函数名合并同名节点后再排序 |
199
+
200
+ 不确定该调统计 1.0 还是 2.0 时,优先用这三个,不必自己判断日期和 SDK。
201
+
121
202
  ## 大数据接口的返回方式
122
203
 
123
204
  堆栈树、资源列表、卡顿帧等接口不直接返回数据,而是返回一个有效期 600 秒的预签名下载地址。
@@ -177,9 +258,6 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
177
258
  | --- | --- | --- |
178
259
  | `@uwa4d/openapi-mcp` | 跟随最新稳定版,自动升级 | 默认,大多数用户 |
179
260
  | `@uwa4d/openapi-mcp@0.1.0` | 锁死某个版本 | 生产流水线,要求完全可复现 |
180
- | `@uwa4d/openapi-mcp@beta` | 跟随测试版 | 配合 UWA 验证新接口 |
181
-
182
- 测试版只发布在 `beta` 标签下,默认写法不会拉到测试版。
183
261
 
184
262
  ## 常见问题
185
263
 
@@ -187,10 +265,20 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
187
265
  检查 Node 版本是否 ≥ 18,以及配置文件 JSON 格式是否正确(多一个逗号就会整个失效)。改完配置需要重启客户端。
188
266
 
189
267
  **提示凭证错误**
190
- 用 `check` 命令在终端单独验证一次,排除是客户端配置传参的问题。注意测试环境和生产环境的凭证不通用。
268
+ 用 `check` 命令在终端单独验证一次,排除是客户端配置传参的问题。确认 AppId / AppSecret 无误且未过期。
269
+
270
+ **返回「数据服务错误」或不确定该用 v1 还是 v2**
271
+
272
+ 部分接口按报告解析/提交日期和引擎区分版本:
273
+
274
+ | 场景 | 用哪个 |
275
+ | --- | --- |
276
+ | Unity Overview,解析日 **&lt; 2026-07-09** | `get_overview_statistic_v1` |
277
+ | Unity Overview,解析日 **≥ 2026-07-09** 且 SDK ≥ 2.5.1 | `get_overview_statistic_v2` |
278
+ | UE Overview,提交日 **≤ 2026-03-25** | `get_overview_statistic_v1` |
279
+ | UE Overview,提交日 **&gt; 2026-03-25** | `get_ue_overview_statistic_v2` |
191
280
 
192
- **返回「数据服务错误」**
193
- 部分接口区分报告生成时间和引擎类型,例如 Unity 2026-07-09 之前的报告用 `get_overview_statistic_v1`,之后的用 `get_overview_statistic_v2`。工具描述里注明了适用范围。
281
+ 场景统计、运行日志、快照帧等也有类似 1.0/2.0 成对工具,描述开头有「选用规则」。不确定时直接用复合工具 `report_diagnosis` / `top_functions`,内部会按报告详情自动路由。
194
282
 
195
283
  ## 许可
196
284
 
@@ -0,0 +1,48 @@
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
+ /** returnFramesOnly 时去掉顶层无用 x_axis,避免慢帧列表撑爆上下文。 */
32
+ export declare function stripOuterXAxisWhenFramesOnly(data: unknown, filter: CurveFilterOptions): unknown;
33
+ export declare function isIndicatorStatisticDashboard(opPath: string): boolean;
34
+ export declare function isIndicatorCurveDashboard(opPath: string): boolean;
35
+ /** 从 android_memory_pss 统计抽出弱模型友好的峰值字段(单位 KB)。 */
36
+ export declare function extractPssPeakCard(data: unknown): Record<string, unknown> | null;
37
+ /**
38
+ * 从 Overview 统计 v2(categories[])抽出 PSS 峰值答题卡。
39
+ * v2 通常只有 maximum、没有 peak frame —— frame 为 null 时提示改用 indicator 面板。
40
+ */
41
+ export declare function extractPssPeakFromOverviewStatistic(data: unknown): Record<string, unknown> | null;
42
+ /** Overview 统计(v1/v2)成功返回后附加 PSS 答题卡。 */
43
+ export declare function annotateOverviewStatisticPss(data: unknown): unknown;
44
+ export declare function isOverviewStatisticOp(opId: string, _opPath?: string): boolean;
45
+ /** 卡顿合并树:返回层硬门禁,防止 WaitForVsync→根因 / 空 Bound 仍说 GPU 主瓶颈。 */
46
+ export declare function stutterRootCauseGate(opId: string): Record<string, unknown> | null;
47
+ /** 统计面板成功返回后的统一标注管线。 */
48
+ export declare function annotateStatisticDashboard(data: unknown, accepted: string[], unknown: string[]): unknown;
@@ -0,0 +1,413 @@
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
+ /** returnFramesOnly 时去掉顶层无用 x_axis,避免慢帧列表撑爆上下文。 */
258
+ export function stripOuterXAxisWhenFramesOnly(data, filter) {
259
+ if (!filter.returnFramesOnly || !isObj(data))
260
+ return data;
261
+ const { x_axis: _drop, ...rest } = data;
262
+ return {
263
+ ...rest,
264
+ _framesOnlyNote: 'returnFramesOnly=true 已省略顶层 x_axis;帧号在各曲线 frames 字段。',
265
+ };
266
+ }
267
+ export function isIndicatorStatisticDashboard(opPath) {
268
+ return opPath.includes('/indicator/statistic/dashboard') || opPath.includes('/custom/dashboard');
269
+ }
270
+ export function isIndicatorCurveDashboard(opPath) {
271
+ return opPath.includes('/indicator/curve/dashboard') || opPath.includes('/gpu/curve');
272
+ }
273
+ function numFromUnitOrData(v) {
274
+ if (typeof v === 'number' && Number.isFinite(v))
275
+ return v;
276
+ if (Array.isArray(v) && typeof v[0] === 'number' && Number.isFinite(v[0]))
277
+ return v[0];
278
+ if (typeof v === 'object' && v !== null) {
279
+ const o = v;
280
+ if (typeof o.value === 'number' && Number.isFinite(o.value))
281
+ return o.value;
282
+ if (Array.isArray(o.data) && typeof o.data[0] === 'number')
283
+ return o.data[0];
284
+ }
285
+ return null;
286
+ }
287
+ function buildPssPeakCard(peakKb, frame) {
288
+ return {
289
+ pss_peak_kb: peakKb,
290
+ pss_peak_mb: Math.round((peakKb / 1024) * 100) / 100,
291
+ pss_peak_frame: frame,
292
+ pss_unit: 'KB',
293
+ _pssHint: 'Android PSS 峰值请优先读本对象顶层 pss_peak_kb / pss_peak_mb / pss_peak_frame;' +
294
+ '合法面板名是 android_memory_pss@max(不是 android_pss_max)。单位默认 KB。',
295
+ };
296
+ }
297
+ /** 从 android_memory_pss 统计抽出弱模型友好的峰值字段(单位 KB)。 */
298
+ export function extractPssPeakCard(data) {
299
+ if (!isObj(data))
300
+ return null;
301
+ const statistic = isObj(data['statistic']) ? data['statistic'] : null;
302
+ const nested = statistic && isObj(statistic['android_memory_pss'])
303
+ ? statistic['android_memory_pss']
304
+ : null;
305
+ const peakKb = (nested ? numFromUnitOrData(nested['maximum']) : null) ??
306
+ (statistic ? numFromUnitOrData(statistic['android_memory_pss_maximum']) : null) ??
307
+ numFromUnitOrData(data['android_memory_pss_maximum']);
308
+ const frame = (nested ? numFromUnitOrData(nested['maximum_frame']) : null) ??
309
+ (statistic ? numFromUnitOrData(statistic['android_memory_pss_maximum_frame']) : null) ??
310
+ numFromUnitOrData(data['android_memory_pss_maximum_frame']);
311
+ if (peakKb == null)
312
+ return null;
313
+ return buildPssPeakCard(peakKb, frame);
314
+ }
315
+ /**
316
+ * 从 Overview 统计 v2(categories[])抽出 PSS 峰值答题卡。
317
+ * v2 通常只有 maximum、没有 peak frame —— frame 为 null 时提示改用 indicator 面板。
318
+ */
319
+ export function extractPssPeakFromOverviewStatistic(data) {
320
+ if (!isObj(data))
321
+ return null;
322
+ const categories = Array.isArray(data['categories'])
323
+ ? data['categories']
324
+ : isObj(data['data']) && Array.isArray(data['data']['categories'])
325
+ ? data['data']['categories']
326
+ : null;
327
+ if (!categories)
328
+ return null;
329
+ let peakKb = null;
330
+ let frame = null;
331
+ for (const cat of categories) {
332
+ if (!isObj(cat) || !Array.isArray(cat['indicators']))
333
+ continue;
334
+ for (const ind of cat['indicators']) {
335
+ if (!isObj(ind))
336
+ continue;
337
+ const key = String(ind['key'] ?? ind['indicatorKey'] ?? '');
338
+ const n = numFromUnitOrData(ind['data'] ?? ind['value']);
339
+ if (n == null)
340
+ continue;
341
+ if (key === 'android_memory_pss_maximum' || key === 'android_memory_pss@max')
342
+ peakKb = n;
343
+ if (key === 'android_memory_pss_maximum_frame' || key === 'android_memory_pss@max_frame')
344
+ frame = n;
345
+ }
346
+ }
347
+ if (peakKb == null)
348
+ return null;
349
+ const card = buildPssPeakCard(peakKb, frame);
350
+ if (frame == null) {
351
+ card['_pssFrameNote'] =
352
+ '本接口未返回峰值帧;需要 pss_peak_frame 时请再调 gotonline_overview_indicator_statistic_dashboard,面板名 android_memory_pss@max。';
353
+ }
354
+ return card;
355
+ }
356
+ /** Overview 统计(v1/v2)成功返回后附加 PSS 答题卡。 */
357
+ export function annotateOverviewStatisticPss(data) {
358
+ if (Array.isArray(data)) {
359
+ return data.map((item) => annotateOverviewStatisticPss(item));
360
+ }
361
+ if (!isObj(data))
362
+ return data;
363
+ // batch map: { [dataKey]: { categories... } }
364
+ const keys = Object.keys(data);
365
+ if (!data['categories'] &&
366
+ !data['statistic'] &&
367
+ !data['brief'] &&
368
+ keys.length > 0 &&
369
+ keys.every((k) => isObj(data[k]))) {
370
+ const next = { ...data };
371
+ for (const k of keys)
372
+ next[k] = annotateOverviewStatisticPss(data[k]);
373
+ return next;
374
+ }
375
+ if (data['pss_peak_kb'] != null)
376
+ return data;
377
+ const fromDash = extractPssPeakCard(data);
378
+ const fromV2 = fromDash ?? extractPssPeakFromOverviewStatistic(data);
379
+ if (!fromV2)
380
+ return data;
381
+ return { ...data, ...fromV2 };
382
+ }
383
+ export function isOverviewStatisticOp(opId, _opPath) {
384
+ return (opId === 'get_overview_statistic_v1' ||
385
+ opId === 'get_overview_statistic_v2' ||
386
+ opId === 'get_ue_overview_statistic_v2');
387
+ }
388
+ /** 卡顿合并树:返回层硬门禁,防止 WaitForVsync→根因 / 空 Bound 仍说 GPU 主瓶颈。 */
389
+ export function stutterRootCauseGate(opId) {
390
+ if (!/stack_stutter_(full|scene)_tree/.test(opId))
391
+ return null;
392
+ return {
393
+ forbidden_claims: ['WaitForVsync是卡顿根因', 'GPU是主要瓶颈', 'GPU Bound明显', 'GPU主瓶颈'],
394
+ _stutterRootCauseHint: '本树是卡顿根因主入口。请按耗时归因到具体业务/引擎节点;' +
395
+ '禁止把 WaitForVsync / Gfx.WaitForPresent 写成根因或 GPU Bound 证据。' +
396
+ '若要说 GPU 主瓶颈,必须另查 gotonline_gpu_bound;空数组只能说「未检出 Bound」。',
397
+ };
398
+ }
399
+ /** 统计面板成功返回后的统一标注管线。 */
400
+ export function annotateStatisticDashboard(data, accepted, unknown) {
401
+ let out = annotateSceneUnits(data);
402
+ out = annotateDuplicateFlatAliases(out);
403
+ out = annotateParamFieldMap(out);
404
+ if (!isObj(out))
405
+ return out;
406
+ const pssCard = extractPssPeakCard(out);
407
+ return {
408
+ ...out,
409
+ ...(pssCard ?? {}),
410
+ _acceptedDashboards: accepted,
411
+ ...(unknown.length ? { _unknownDashboards: unknown } : {}),
412
+ };
413
+ }
package/dist/cli.js CHANGED
@@ -2,9 +2,11 @@
2
2
  import { Command, Option } from 'commander';
3
3
  import { UwaClient } from './client.js';
4
4
  import { loadSpec } from './spec.js';
5
+ import { compositeFilterKeys, selectCompositeTools, COMPOSITE_TOOLS } from './composite/index.js';
5
6
  import { filterKeys, selectOperations } from './presets.js';
6
7
  import { DEFAULT_MAX_CHARS, DEFAULT_MAX_ROWS, toolName } from './tools.js';
7
8
  import { PACKAGE_VERSION, startStdio } from './server.js';
9
+ import { checkForUpdate, formatUpdateNotice, resolveUpdateChannel } from './version-check.js';
8
10
  const DEFAULT_BASE_URL = 'https://secure-api.uwa4d.com';
9
11
  const SANDBOX_BASE_URL = 'https://sandbox-api.uwa4d.com';
10
12
  function splitList(value) {
@@ -70,10 +72,10 @@ program
70
72
  .option('--spec <path>', '自定义 spec 文件路径')
71
73
  .action((opts) => {
72
74
  const spec = loadSpec(opts.spec);
73
- const selected = new Set(selectOperations(spec.operations, {
74
- tools: opts.tool ?? ['all'],
75
- engine: opts.engine,
76
- }).map((o) => o.id));
75
+ const toolFilter = opts.tool ?? ['all'];
76
+ const engine = opts.engine;
77
+ const selected = new Set(selectOperations(spec.operations, { tools: toolFilter, engine }).map((o) => o.id));
78
+ const selectedComposite = new Set(selectCompositeTools({ tools: toolFilter, engine }).map((t) => t.id));
77
79
  const byModule = new Map();
78
80
  for (const op of spec.operations) {
79
81
  const key = op.modules.join(',');
@@ -89,8 +91,19 @@ program
89
91
  console.log(`${mark} ${toolName(op.id, 'snake', '').padEnd(52)} ${op.method.padEnd(4)} ${engines.padEnd(7)} ${op.name}`);
90
92
  }
91
93
  }
92
- console.log(`\n ${spec.operations.length} 个工具,当前条件选中 ${selected.size} 个(* 标记)`);
93
- console.log(`可用过滤键:${[...new Set(spec.operations.flatMap(filterKeys))].sort().join(', ')}`);
94
+ console.log('\n## composite(手写复合工具)');
95
+ for (const t of COMPOSITE_TOOLS) {
96
+ const mark = selectedComposite.has(t.id) ? '*' : ' ';
97
+ const engines = t.engines.map((e) => (e === 'unity' ? 'U3D' : 'UE')).join('+');
98
+ console.log(`${mark} ${toolName(t.id, 'snake', '').padEnd(52)} CMP ${engines.padEnd(7)} ${t.title}`);
99
+ }
100
+ const total = spec.operations.length + COMPOSITE_TOOLS.length;
101
+ const selectedCount = selected.size + selectedComposite.size;
102
+ console.log(`\n共 ${total} 个工具(原子 ${spec.operations.length} + 复合 ${COMPOSITE_TOOLS.length}),当前条件选中 ${selectedCount} 个(* 标记)`);
103
+ const keys = [
104
+ ...new Set([...spec.operations.flatMap(filterKeys), ...compositeFilterKeys()]),
105
+ ].sort();
106
+ console.log(`可用过滤键:${keys.join(', ')}`);
94
107
  });
95
108
  program
96
109
  .command('check')
@@ -121,6 +134,16 @@ program
121
134
  console.log('\n凭证有效,接口连通。');
122
135
  if (typeof total === 'number')
123
136
  console.log(`最近 7 天有 ${total} 份报告。`);
137
+ console.log(`\nMCP 包版本:${PACKAGE_VERSION}(${resolveUpdateChannel(PACKAGE_VERSION)} 通道)`);
138
+ if (process.env['UWA_MCP_SKIP_UPDATE_CHECK'] !== '1') {
139
+ const update = await checkForUpdate(PACKAGE_VERSION);
140
+ if (update) {
141
+ console.log(formatUpdateNotice(update));
142
+ }
143
+ else {
144
+ console.log('已是最新同通道版本(或暂时无法连接 npm registry)。');
145
+ }
146
+ }
124
147
  console.log('\n可以按 README 配置 MCP 客户端了。');
125
148
  }
126
149
  catch (err) {