@uwa4d/openapi-mcp 0.2.0-beta.0 → 0.2.0-beta.10

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 (46) hide show
  1. package/README.md +105 -9
  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 +17 -0
  39. package/dist/tools.js +399 -18
  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/overrides.json +26 -0
  46. package/spec/uwa-openapi.json +495 -111
package/dist/tools.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { UwaApiError } from './client.js';
3
+ import { INDICATOR_DASHBOARD_PANEL_GUIDE, versionGuideFor } from './version-guide.js';
4
+ import { partitionIndicatorDashboards, INDICATOR_DASHBOARD_KEYS, INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES, } from './indicator-dashboard-keys.js';
5
+ import { annotateStatisticDashboard, annotateOverviewStatisticPss, extractCurveFilterArgs, filterCurveData, hasCurveFilter, isIndicatorCurveDashboard, isIndicatorStatisticDashboard, isOverviewStatisticOp, stutterRootCauseGate, stripOuterXAxisWhenFramesOnly, } from './annotate-dashboard.js';
3
6
  const ENGINE_LABEL = { unity: 'Unity', unreal: 'UE' };
4
7
  /**
5
8
  * 预签名数据默认完整返回,不按条数截断——实测各接口解压后在 10 KB ~ 500 KB 之间,
@@ -36,6 +39,14 @@ function zodFor(param) {
36
39
  */
37
40
  function describe(op) {
38
41
  const lines = [op.summary || op.name];
42
+ // v1/v2 选用规则放在最前,避免模型淹没在长 notes 里选错接口
43
+ const guide = versionGuideFor(op.id);
44
+ if (guide)
45
+ lines.push(guide);
46
+ else if (op.query.some((p) => p.name === 'indicatorDashboards')) {
47
+ // 带 indicatorDashboards 但未单独写 VERSION_GUIDE 的原子工具(兜底)
48
+ lines.push(INDICATOR_DASHBOARD_PANEL_GUIDE);
49
+ }
39
50
  const engines = op.engines.map((e) => ENGINE_LABEL[e]).join(' / ');
40
51
  lines.push(`适用引擎:${engines}|模块:${op.modules.join(',')}|${op.method} ${op.path}`);
41
52
  const sdk = op.variants.find((v) => v.sdkRequirement)?.sdkRequirement;
@@ -76,28 +87,74 @@ function describe(op) {
76
87
  }
77
88
  return lines.join('\n');
78
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
+ }
79
97
  export function buildInputSchema(op) {
80
98
  const shape = {};
81
99
  for (const p of [...op.query, ...op.body]) {
82
- const base = zodFor(p).describe(p.example ? `${p.description}(示例:${p.example})` : p.description);
100
+ let desc = p.description;
101
+ if (p.name === 'indicatorDashboards') {
102
+ const sampleKeys = ['fps_avg', 'drawcall_cnt_max', 'frametime@gt_40_pct', 'temperature_max'];
103
+ desc = [
104
+ desc,
105
+ '传参名为面板标识符:后缀 _max/_avg 等为历史命名,不表示只能取峰值或均值。',
106
+ '每个面板一次返回全部统计量,勿按 mean/max/min 拆成多个面板名。',
107
+ `合法取值共 ${INDICATOR_DASHBOARD_KEYS.length} 个;可先调 gotonline_overview_indicator_dashboard_keys 拉全量列表(命名含 @ 等,不可从示例归纳)。`,
108
+ `常见误传(非面板名):${INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES.join('、')}。`,
109
+ `示例面板名:${sampleKeys.join('、')}。`,
110
+ ].join(' ');
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
+ }
118
+ const base = zodFor(p).describe(p.example ? `${desc}(示例:${p.example})` : desc);
83
119
  shape[p.name] = p.required ? base : base.optional();
84
120
  }
85
121
  if (op.apiVersions.length > 1) {
122
+ const def = defaultApiVersionFor(op);
86
123
  shape['apiVersion'] = z
87
124
  .enum(op.apiVersions)
88
125
  .optional()
89
- .describe(`接口版本,默认 ${op.defaultApiVersion}。不同版本的返回字段有差异,详见文档`);
126
+ .describe(`接口版本,默认 ${def}。不同版本的返回字段有差异,详见文档`);
90
127
  }
91
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');
92
132
  shape['download'] = z
93
133
  .boolean()
94
134
  .optional()
95
- .describe('是否自动下载并解析预签名数据,默认 true。设为 false 时只返回 dataPresignUrl');
135
+ .describe(isHeavyTree
136
+ ? '是否自动下载并解析预签名数据,默认 true。大报告堆栈树建议 download=false,只取 dataPresignUrl 自行处理,避免撑爆上下文'
137
+ : '是否自动下载并解析预签名数据,默认 true。设为 false 时只返回 dataPresignUrl');
96
138
  shape['maxRows'] = z
97
139
  .number()
98
140
  .optional()
99
141
  .describe('可选的条目数上限。默认返回全部数据,只有在明确只需要前 N 条时才传这个参数');
100
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
+ }
101
158
  return shape;
102
159
  }
103
160
  /** 找出对象里最长的那个数组字段,它通常就是主数据列表。 */
@@ -145,13 +202,58 @@ function summarize(payload, maxRows, maxChars) {
145
202
  rowTruncated = maxRows > 0 && lines.length > maxRows;
146
203
  body = rowTruncated ? lines.slice(0, maxRows).join('\n') : (payload.text ?? '');
147
204
  }
148
- let text = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
149
- let charTruncated = false;
150
- if (maxChars > 0 && text.length > maxChars) {
151
- charTruncated = true;
152
- text = text.slice(0, maxChars);
205
+ const fitted = fitToChars(body, maxChars);
206
+ return { rowTruncated, total, ...fitted };
207
+ }
208
+ const serialize = (v) => (typeof v === 'string' ? v : JSON.stringify(v, null, 2));
209
+ /**
210
+ * 把返回内容压到字符上限以内。
211
+ *
212
+ * 直接按字符裁剪 JSON 会切出非法结构,模型没法解析。因此优先按条目缩减
213
+ * 主数据数组——二分找出能放下的最大条数,保留完整的 JSON 结构,
214
+ * 并在元信息里说明丢了多少。只有在没有数组可缩减时才退化为字符串截断。
215
+ */
216
+ function fitToChars(body, maxChars) {
217
+ const full = serialize(body);
218
+ if (maxChars <= 0 || full.length <= maxChars) {
219
+ return { charTruncated: false, chars: full.length, body: full };
220
+ }
221
+ const rebuild = (n) => {
222
+ if (Array.isArray(body))
223
+ return serialize(body.slice(0, n));
224
+ if (body && typeof body === 'object') {
225
+ const obj = body;
226
+ const key = mainArrayKey(obj);
227
+ if (!key)
228
+ return null;
229
+ return serialize({ ...obj, [key]: obj[key].slice(0, n) });
230
+ }
231
+ return null;
232
+ };
233
+ const count = Array.isArray(body)
234
+ ? body.length
235
+ : body && typeof body === 'object'
236
+ ? (() => {
237
+ const key = mainArrayKey(body);
238
+ return key ? body[key].length : 0;
239
+ })()
240
+ : 0;
241
+ if (count > 0 && rebuild(0) !== null) {
242
+ // 二分出能放进上限的最大条数
243
+ let lo = 0;
244
+ let hi = count;
245
+ while (lo < hi) {
246
+ const mid = Math.ceil((lo + hi) / 2);
247
+ const text = rebuild(mid);
248
+ if (text.length <= maxChars)
249
+ lo = mid;
250
+ else
251
+ hi = mid - 1;
252
+ }
253
+ const text = rebuild(lo);
254
+ return { charTruncated: true, kept: lo, chars: text.length, body: text };
153
255
  }
154
- return { rowTruncated, charTruncated, total, chars: text.length, body: text };
256
+ return { charTruncated: true, chars: maxChars, body: full.slice(0, maxChars) };
155
257
  }
156
258
  function isObj(v) {
157
259
  return typeof v === 'object' && v !== null && !Array.isArray(v);
@@ -196,6 +298,160 @@ export function annotateCurveAxes(data) {
196
298
  _axisHint: '每条曲线按 _axisBinding[曲线名].xAxisSource 指定的横轴取值,不要统一使用外层 x_axis',
197
299
  };
198
300
  }
301
+ /**
302
+ * 自定义面板 statistic/curve 返回空 `{}` 时,优先提示面板名传错(静默失败高发)。
303
+ */
304
+ export function annotateDashboardEmptyResult(op, data, args) {
305
+ const isDashboard = op.path.includes('/indicator/statistic/dashboard') ||
306
+ op.path.includes('/indicator/curve/dashboard') ||
307
+ op.path.includes('/custom/dashboard');
308
+ if (!isDashboard || !isObj(data))
309
+ return data;
310
+ const requested = args['indicatorDashboards'];
311
+ const stat = data['statistic'];
312
+ const columns = data['columns'];
313
+ const emptyStat = stat == null || (isObj(stat) && Object.keys(stat).length === 0);
314
+ const emptyColumns = !Array.isArray(columns) || columns.length === 0;
315
+ if (!emptyStat && !emptyColumns)
316
+ return data;
317
+ const { unknown } = partitionIndicatorDashboards(requested);
318
+ const hintParts = [
319
+ 'indicatorDashboards 可能传错,导致 statistic/columns 为空。',
320
+ '面板传参名≠统计维度后缀(如 fps_mean、frametime_min 是返回字段,不是面板名)。',
321
+ '合法示例:fps_avg、drawcall_cnt_max、frametime@gt_40_pct(含 @ 等特殊字符)。',
322
+ `误传示例:${INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES.join('、')}。`,
323
+ '请先调 gotonline_overview_indicator_dashboard_keys 核对全量合法面板名。',
324
+ ];
325
+ if (unknown.length) {
326
+ hintParts.unshift(`未识别的面板名:${unknown.join('、')}。`);
327
+ }
328
+ return {
329
+ ...data,
330
+ _hint: hintParts.join(' '),
331
+ };
332
+ }
333
+ const RECORD_ARRAY_KEYS = /^gotOnline.*Records$/;
334
+ function hasNonEmptyRecords(v) {
335
+ return Array.isArray(v) && v.length > 0;
336
+ }
337
+ /** 各 GOT Online 子模块是否有报告;供模型「无 GPU 停手」。 */
338
+ export function buildModuleAvailable(group) {
339
+ return {
340
+ overview: hasNonEmptyRecords(group['gotOnlineOverviewRecords']),
341
+ gpu: hasNonEmptyRecords(group['gotOnlineGPURecords']),
342
+ mono: hasNonEmptyRecords(group['gotOnlineMonoRecords']),
343
+ lua: hasNonEmptyRecords(group['gotOnlineLuaRecords']),
344
+ resource: hasNonEmptyRecords(group['gotOnlineResourceRecords']),
345
+ };
346
+ }
347
+ export function annotateModuleAvailable(data) {
348
+ if (!isObj(data) || !Array.isArray(data['content']))
349
+ return data;
350
+ const content = data['content'].map((g) => {
351
+ if (!isObj(g))
352
+ return g;
353
+ const module_available = buildModuleAvailable(g);
354
+ const missing = Object.entries(module_available)
355
+ .filter(([, ok]) => !ok)
356
+ .map(([k]) => k);
357
+ return {
358
+ ...g,
359
+ module_available,
360
+ ...(missing.length
361
+ ? {
362
+ _moduleHint: `本项目组缺少模块:${missing.join('、')}。module_available 为 false 的模块请停手,禁止用 Overview 数据冒充分析。`,
363
+ }
364
+ : {}),
365
+ };
366
+ });
367
+ return { ...data, content };
368
+ }
369
+ /**
370
+ * 报告列表接口一次返回、按项目组分组。但上游会把同一份报告挂到每个项目组下
371
+ * (沙箱实测多组两两交集 100%),直接按组统计会成倍放大。
372
+ *
373
+ * 这里就地合并:按 link 里的 project= 判定真实归属,每份报告只保留在正确的组里。
374
+ * 不额外打接口。上游修好后检测不到重复,本函数原样返回。
375
+ */
376
+ export function annotateProjectGroups(data) {
377
+ if (!isObj(data) || !Array.isArray(data['content']))
378
+ return annotateModuleAvailable(data);
379
+ const groups = data['content'].filter(isObj);
380
+ if (groups.length < 2)
381
+ return annotateModuleAvailable(data);
382
+ // dataKey → 出现过的项目组;以及「该报告在哪个数组字段里、原文是什么」
383
+ const groupsOf = new Map();
384
+ const firstSeen = new Map();
385
+ for (const g of groups) {
386
+ const gid = String(g['projectGroupId'] ?? '');
387
+ if (!gid)
388
+ continue;
389
+ for (const [k, v] of Object.entries(g)) {
390
+ if (!RECORD_ARRAY_KEYS.test(k) || !Array.isArray(v))
391
+ continue;
392
+ for (const r of v) {
393
+ if (!isObj(r))
394
+ continue;
395
+ const key = typeof r['dataKey'] === 'string' ? r['dataKey'] : null;
396
+ if (!key)
397
+ continue;
398
+ if (!groupsOf.has(key))
399
+ groupsOf.set(key, new Set());
400
+ groupsOf.get(key).add(gid);
401
+ if (!firstSeen.has(key)) {
402
+ const fromLink = /[?&]project=(\d+)/.exec(String(r['link'] ?? ''))?.[1];
403
+ firstSeen.set(key, { arrayKey: k, record: r, realGid: fromLink ?? gid });
404
+ }
405
+ }
406
+ }
407
+ }
408
+ const duplicated = [...groupsOf.values()].some((gs) => gs.size > 1);
409
+ if (!duplicated)
410
+ return annotateModuleAvailable(data);
411
+ // 已知项目组壳子(保留名称、engine 等元信息),报告重新按真实归属灌入
412
+ const shells = new Map();
413
+ for (const g of groups) {
414
+ const gid = String(g['projectGroupId'] ?? '');
415
+ if (!gid || shells.has(gid))
416
+ continue;
417
+ const shell = { ...g };
418
+ for (const k of Object.keys(shell)) {
419
+ if (RECORD_ARRAY_KEYS.test(k))
420
+ shell[k] = [];
421
+ }
422
+ shells.set(gid, shell);
423
+ }
424
+ let placed = 0;
425
+ let orphaned = 0;
426
+ for (const { arrayKey, record, realGid } of firstSeen.values()) {
427
+ let shell = shells.get(realGid);
428
+ if (!shell) {
429
+ // link 指向的组不在本页 content 里——仍给出一份壳,避免报告丢失
430
+ shell = {
431
+ projectGroupId: Number.isFinite(Number(realGid)) ? Number(realGid) : realGid,
432
+ projectGroupName: `(id=${realGid})`,
433
+ engine: null,
434
+ };
435
+ shells.set(realGid, shell);
436
+ orphaned++;
437
+ }
438
+ if (!Array.isArray(shell[arrayKey]))
439
+ shell[arrayKey] = [];
440
+ shell[arrayKey].push(record);
441
+ placed++;
442
+ }
443
+ // 丢掉合并后一个报告都没有的空组(它们原先只有别人的副本)
444
+ const content = [...shells.values()].filter((g) => Object.entries(g).some(([k, v]) => RECORD_ARRAY_KEYS.test(k) && Array.isArray(v) && v.length > 0));
445
+ return annotateModuleAvailable({
446
+ ...data,
447
+ content,
448
+ _deduplicated: true,
449
+ _uniqueReportCount: placed,
450
+ _dedupeNote: `上游把同一份报告重复挂在多个项目组下,已按 link 中 project= 合并归位,去重后本页 ${placed} 份` +
451
+ (orphaned ? `(其中 ${orphaned} 份所属项目组不在本页,已单独列出)` : '') +
452
+ '。按项目组统计请用本页 _uniqueReportCount / 各组 Records;跨页汇总须自行按 dataKey 去重,勿把各页条数简单相加。',
453
+ });
454
+ }
199
455
  /** MCP 的 content.text 必须是字符串,undefined 会让客户端校验失败。 */
200
456
  function asText(value) {
201
457
  if (typeof value === 'string')
@@ -204,41 +460,166 @@ function asText(value) {
204
460
  return '(接口无返回内容)';
205
461
  return JSON.stringify(value, null, 2);
206
462
  }
463
+ /** 空 Bound / 空日志列表等:补可教模型的语义,避免误判。 */
464
+ export function annotateEmptyStateSemantics(op, data) {
465
+ if (op.id === 'gotonline_gpu_bound') {
466
+ const intervals = Array.isArray(data) ? data : isObj(data) && Array.isArray(data['data']) ? data['data'] : null;
467
+ if (intervals && intervals.length === 0) {
468
+ const base = isObj(data) ? data : { data: intervals };
469
+ return {
470
+ ...base,
471
+ gpuBound: [],
472
+ forbidden_claims: ['GPU是主要瓶颈', 'GPU Bound明显', 'GPU主瓶颈'],
473
+ _hint: 'GPU Bound 为空数组:仅表示在当前 targetFps 下未检出 Bound 区间,不等于 GPU 完全无压力;禁止据此断言 GPU 是主要瓶颈。',
474
+ };
475
+ }
476
+ }
477
+ if (op.id === 'gotonline_overview_log_export') {
478
+ const empty = (Array.isArray(data) && data.length === 0) ||
479
+ (isObj(data) && Array.isArray(data['data']) && data['data'].length === 0);
480
+ if (empty) {
481
+ const base = isObj(data) ? data : { entries: data };
482
+ return {
483
+ ...base,
484
+ _hint: '运行日志 1.0 返回空列表:可能是真无条目,也可能是新报告应改用 gotonline_lg_runtime_log_entries_presign(日志 2.0)。禁止说成「一定没有日志」。',
485
+ };
486
+ }
487
+ }
488
+ if (op.id === 'gotonline_overview_event_lower_memory_frames') {
489
+ const empty = (Array.isArray(data) && data.length === 0) ||
490
+ (isObj(data) && Array.isArray(data['data']) && data['data'].length === 0);
491
+ if (empty) {
492
+ const base = isObj(data) ? data : { frames: data };
493
+ return {
494
+ ...base,
495
+ _hint: '未触发 Low Memory 事件(空数组)。不能推出内存完全没问题,请结合 PSS 峰值等综合判断。',
496
+ };
497
+ }
498
+ }
499
+ return data;
500
+ }
207
501
  export function makeHandler(op, client, defaultMaxRows, maxChars) {
208
502
  return async (args) => {
209
503
  try {
210
- const apiVersion = args['apiVersion'] ?? op.defaultApiVersion;
504
+ const apiVersion = args['apiVersion'] ?? defaultApiVersionFor(op);
211
505
  const wantDownload = args['download'] ?? true;
212
506
  const maxRows = args['maxRows'] ?? defaultMaxRows;
507
+ const { filter: curveFilter, cleaned: argsForApi } = isIndicatorCurveDashboard(op.path)
508
+ ? extractCurveFilterArgs(args)
509
+ : { filter: {}, cleaned: args };
510
+ let acceptedDashboards = [];
511
+ let unknownDashboards = [];
512
+ if (typeof argsForApi['indicatorDashboards'] === 'string') {
513
+ const part = partitionIndicatorDashboards(argsForApi['indicatorDashboards']);
514
+ acceptedDashboards = part.accepted;
515
+ unknownDashboards = part.unknown;
516
+ if (part.all.length && !acceptedDashboards.length) {
517
+ return {
518
+ content: [
519
+ {
520
+ type: 'text',
521
+ text: asText({
522
+ error: 'PARAMETERS_WRONG',
523
+ message: `indicatorDashboards 全部无法识别:${unknownDashboards.join('、')}`,
524
+ _unknownDashboards: unknownDashboards,
525
+ _acceptedDashboards: [],
526
+ _hint: `这些是面板标识符,不是统计维度后缀。` +
527
+ `误传示例:${INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES.join('、')};` +
528
+ `合法示例:fps_avg、frametime@gt_40_pct。` +
529
+ `请调 gotonline_overview_indicator_dashboard_keys 获取全量 ${INDICATOR_DASHBOARD_KEYS.length} 个合法值。`,
530
+ }),
531
+ },
532
+ ],
533
+ isError: true,
534
+ };
535
+ }
536
+ if (acceptedDashboards.length) {
537
+ argsForApi['indicatorDashboards'] = acceptedDashboards.join(',');
538
+ }
539
+ }
213
540
  const query = {};
214
541
  for (const p of op.query)
215
- if (args[p.name] !== undefined)
216
- query[p.name] = args[p.name];
542
+ if (argsForApi[p.name] !== undefined)
543
+ query[p.name] = argsForApi[p.name];
217
544
  let body;
218
545
  if (op.body.length) {
219
546
  body = {};
220
547
  for (const p of op.body)
221
- if (args[p.name] !== undefined)
222
- body[p.name] = args[p.name];
548
+ if (argsForApi[p.name] !== undefined)
549
+ body[p.name] = argsForApi[p.name];
550
+ }
551
+ const raw = await client.call(op.method, op.path, apiVersion, query, body);
552
+ let processed = annotateProjectGroups(annotateCurveAxes(raw));
553
+ if (isIndicatorStatisticDashboard(op.path) && acceptedDashboards.length) {
554
+ processed = annotateStatisticDashboard(processed, acceptedDashboards, unknownDashboards);
555
+ }
556
+ else if (isOverviewStatisticOp(op.id, op.path)) {
557
+ processed = annotateOverviewStatisticPss(processed);
558
+ }
559
+ else if (isIndicatorCurveDashboard(op.path)) {
560
+ if (acceptedDashboards.length || unknownDashboards.length) {
561
+ processed = isObj(processed)
562
+ ? {
563
+ ...processed,
564
+ _acceptedDashboards: acceptedDashboards,
565
+ ...(unknownDashboards.length ? { _unknownDashboards: unknownDashboards } : {}),
566
+ }
567
+ : processed;
568
+ }
569
+ if (hasCurveFilter(curveFilter)) {
570
+ processed = filterCurveData(processed, curveFilter);
571
+ if (curveFilter.returnFramesOnly) {
572
+ processed = stripOuterXAxisWhenFramesOnly(processed, curveFilter);
573
+ }
574
+ else {
575
+ processed = annotateCurveAxes(processed);
576
+ }
577
+ }
578
+ }
579
+ let data = annotateDashboardEmptyResult(op, processed, args);
580
+ data = annotateEmptyStateSemantics(op, data);
581
+ const stutterGate = stutterRootCauseGate(op.id);
582
+ if (stutterGate && isObj(data)) {
583
+ data = { ...data, ...stutterGate };
223
584
  }
224
- const data = annotateCurveAxes(await client.call(op.method, op.path, apiVersion, query, body));
225
585
  const presignUrl = op.returnsPresignUrl && data && typeof data === 'object'
226
586
  ? data['dataPresignUrl']
227
587
  : undefined;
228
588
  if (!presignUrl || !wantDownload) {
229
- return { content: [{ type: 'text', text: asText(data) }] };
589
+ // 非预签名接口同样可能很大(如报告列表 50 条约 56K 字符、
590
+ // 统计接口批量查十几份报告可达 20 万字符),这里必须同样设闸门,
591
+ // 否则会直接撑爆模型上下文。
592
+ const fitted = fitToChars(data, maxChars);
593
+ if (!fitted.charTruncated) {
594
+ return { content: [{ type: 'text', text: asText(fitted.body) }] };
595
+ }
596
+ return {
597
+ content: [
598
+ {
599
+ type: 'text',
600
+ text: asText({
601
+ _truncated: `返回内容超过 ${maxChars} 字符上限,已缩减为 ${fitted.kept ?? 0} 条。` +
602
+ `请缩小查询范围后重试${op.query.some((p) => p.name === 'pageSize') ? '(如调小 pageSize 分页获取)' : '(如减少批量查询的条目数)'},` +
603
+ `或在启动参数里调大 --max-chars。`,
604
+ _returnedChars: fitted.chars,
605
+ }),
606
+ },
607
+ { type: 'text', text: asText(fitted.body) },
608
+ ],
609
+ };
230
610
  }
231
611
  const payload = await client.downloadPresign(presignUrl);
232
- const { rowTruncated, charTruncated, total, chars, body: content } = summarize(payload, maxRows, maxChars);
612
+ const { rowTruncated, charTruncated, total, kept, chars, body: content } = summarize(payload, maxRows, maxChars);
233
613
  const notes = [];
234
614
  if (rowTruncated)
235
615
  notes.push(`按 maxRows=${maxRows} 截断,去掉 maxRows 参数可取全量`);
236
616
  if (charTruncated) {
237
- notes.push(`内容超过 ${maxChars} 字符上限已截断,尾部数据缺失。` +
617
+ notes.push(`内容超过 ${maxChars} 字符上限,已缩减为 ${kept ?? 0} 条。` +
238
618
  `如需完整数据,用 download=false 拿 dataPresignUrl 自行下载,或启动时调大 --max-chars`);
239
619
  }
240
620
  const meta = {
241
621
  ...data,
622
+ ...(stutterRootCauseGate(op.id) ?? {}),
242
623
  _downloaded: true,
243
624
  _format: payload.kind,
244
625
  ...(total !== undefined ? { _totalItems: total } : {}),
@@ -0,0 +1,14 @@
1
+ /** npm 包名(与 package.json 同步)。 */
2
+ export declare const PACKAGE_NAME = "@uwa4d/openapi-mcp";
3
+ export interface UpdateCheckResult {
4
+ current: string;
5
+ latest: string;
6
+ channel: 'beta' | 'latest';
7
+ }
8
+ /** 当前运行版本应跟哪个 dist-tag 比对新版。 */
9
+ export declare function resolveUpdateChannel(version: string): 'beta' | 'latest';
10
+ /** 查询 registry 是否有比 current 更新的同通道版本;失败时返回 null。 */
11
+ export declare function checkForUpdate(current: string): Promise<UpdateCheckResult | null>;
12
+ export declare function formatUpdateNotice(result: UpdateCheckResult): string;
13
+ /** 启动时异步检查;仅写 stderr,不阻塞 MCP 握手。 */
14
+ export declare function scheduleUpdateNotice(current: string): void;
@@ -0,0 +1,94 @@
1
+ /** npm 包名(与 package.json 同步)。 */
2
+ export const PACKAGE_NAME = '@uwa4d/openapi-mcp';
3
+ const REGISTRY_URL = 'https://registry.npmjs.org/@uwa4d%2Fopenapi-mcp';
4
+ const CHECK_TIMEOUT_MS = 8_000;
5
+ /** 当前运行版本应跟哪个 dist-tag 比对新版。 */
6
+ export function resolveUpdateChannel(version) {
7
+ return version.includes('-') ? 'beta' : 'latest';
8
+ }
9
+ /** 简易 semver 比较:a > b 返回 1,相等 0,a < b 返回 -1。 */
10
+ function compareVersions(a, b) {
11
+ if (a === b)
12
+ return 0;
13
+ const parseCore = (v) => v.split('-')[0].split('.').map((n) => Number.parseInt(n, 10) || 0);
14
+ const parsePre = (v) => {
15
+ const idx = v.indexOf('-');
16
+ if (idx < 0)
17
+ return [];
18
+ return v
19
+ .slice(idx + 1)
20
+ .split('.')
21
+ .map((part, i) => (i === 0 ? part : Number.parseInt(part, 10) || part));
22
+ };
23
+ const aCore = parseCore(a);
24
+ const bCore = parseCore(b);
25
+ for (let i = 0; i < Math.max(aCore.length, bCore.length); i++) {
26
+ const av = aCore[i] ?? 0;
27
+ const bv = bCore[i] ?? 0;
28
+ if (av !== bv)
29
+ return av > bv ? 1 : -1;
30
+ }
31
+ const aPre = parsePre(a);
32
+ const bPre = parsePre(b);
33
+ const aHasPre = aPre.length > 0;
34
+ const bHasPre = bPre.length > 0;
35
+ if (!aHasPre && bHasPre)
36
+ return 1;
37
+ if (aHasPre && !bHasPre)
38
+ return -1;
39
+ if (!aHasPre && !bHasPre)
40
+ return 0;
41
+ for (let i = 0; i < Math.max(aPre.length, bPre.length); i++) {
42
+ const av = aPre[i];
43
+ const bv = bPre[i];
44
+ if (av === bv)
45
+ continue;
46
+ if (av === undefined)
47
+ return -1;
48
+ if (bv === undefined)
49
+ return 1;
50
+ if (typeof av === 'number' && typeof bv === 'number')
51
+ return av > bv ? 1 : -1;
52
+ return String(av) > String(bv) ? 1 : -1;
53
+ }
54
+ return 0;
55
+ }
56
+ async function fetchDistTagVersion(channel) {
57
+ try {
58
+ const res = await fetch(REGISTRY_URL, {
59
+ signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
60
+ headers: { Accept: 'application/json' },
61
+ });
62
+ if (!res.ok)
63
+ return null;
64
+ const data = (await res.json());
65
+ const tag = data['dist-tags']?.[channel];
66
+ return typeof tag === 'string' && tag.trim() ? tag.trim() : null;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /** 查询 registry 是否有比 current 更新的同通道版本;失败时返回 null。 */
73
+ export async function checkForUpdate(current) {
74
+ const channel = resolveUpdateChannel(current);
75
+ const latest = await fetchDistTagVersion(channel);
76
+ if (!latest || compareVersions(latest, current) <= 0)
77
+ return null;
78
+ return { current, latest, channel };
79
+ }
80
+ export function formatUpdateNotice(result) {
81
+ const pkgRef = result.channel === 'beta' ? `${PACKAGE_NAME}@beta` : PACKAGE_NAME;
82
+ return (`[uwa-openapi-mcp] 有新版本 ${result.latest}(当前 ${result.current},通道 ${result.channel})。` +
83
+ `若 MCP 配置未锁定具体版本号,请在您使用的 AI 客户端中重启 MCP 服务以升级(重载 MCP 配置或重启客户端均可);` +
84
+ `也可在终端核对:npx --yes --prefer-online ${pkgRef} --version`);
85
+ }
86
+ /** 启动时异步检查;仅写 stderr,不阻塞 MCP 握手。 */
87
+ export function scheduleUpdateNotice(current) {
88
+ if (process.env['UWA_MCP_SKIP_UPDATE_CHECK'] === '1')
89
+ return;
90
+ void checkForUpdate(current).then((result) => {
91
+ if (result)
92
+ console.error(formatUpdateNotice(result));
93
+ });
94
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 版本成对工具的选用规则。
3
+ *
4
+ * 集中手写、挂到 describe() 首段,避免散落在 notes 里被模型漏读。
5
+ * 不塞进 docs/ 生成链路——刷新官方文档镜像不会冲掉这里。
6
+ */
7
+ /**
8
+ * indicatorDashboards 传参名称说明(面板标识符,非统计维度后缀)。
9
+ * 挂到所有带 indicatorDashboards 的工具 description,避免模型见 _max 只读峰值。
10
+ */
11
+ export declare const INDICATOR_DASHBOARD_PANEL_GUIDE: string;
12
+ /**
13
+ * 堆栈 / 卡顿工具路由。卡顿树已是「多帧合并后的根因视图」,
14
+ * 禁止误导成「对每个 >40ms 帧循环拉指定帧树再聚类」。
15
+ */
16
+ export declare const STACK_ANALYSIS_GUIDE: string;
17
+ /** 写入工具 description 的「选用规则」块,key 为 operation id。 */
18
+ export declare const VERSION_GUIDE: Record<string, string>;
19
+ export declare function versionGuideFor(opId: string): string | undefined;