@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.
- package/README.md +96 -8
- package/dist/annotate-dashboard.d.ts +48 -0
- package/dist/annotate-dashboard.js +413 -0
- package/dist/cli.js +29 -6
- package/dist/client.js +4 -13
- package/dist/composite/helpers.d.ts +19 -0
- package/dist/composite/helpers.js +90 -0
- package/dist/composite/index.d.ts +25 -0
- package/dist/composite/index.js +46 -0
- package/dist/composite/name-pattern.d.ts +8 -0
- package/dist/composite/name-pattern.js +25 -0
- package/dist/composite/overview-view.d.ts +33 -0
- package/dist/composite/overview-view.js +373 -0
- package/dist/composite/report-diagnosis.d.ts +2 -0
- package/dist/composite/report-diagnosis.js +347 -0
- package/dist/composite/route-overview.d.ts +72 -0
- package/dist/composite/route-overview.js +233 -0
- package/dist/composite/stack-agg.d.ts +77 -0
- package/dist/composite/stack-agg.js +409 -0
- package/dist/composite/stack-test-mode.d.ts +33 -0
- package/dist/composite/stack-test-mode.js +60 -0
- package/dist/composite/top-functions.d.ts +21 -0
- package/dist/composite/top-functions.js +178 -0
- package/dist/composite/top-resources.d.ts +12 -0
- package/dist/composite/top-resources.js +263 -0
- package/dist/composite/types.d.ts +25 -0
- package/dist/composite/types.js +2 -0
- package/dist/error-hints.d.ts +23 -0
- package/dist/error-hints.js +80 -0
- package/dist/indicator-dashboard-keys.d.ts +12 -0
- package/dist/indicator-dashboard-keys.js +50 -0
- package/dist/indicator-dashboard-keys.json +338 -0
- package/dist/presets.js +9 -0
- package/dist/server-instructions.d.ts +5 -0
- package/dist/server-instructions.js +17 -0
- package/dist/server.d.ts +6 -1
- package/dist/server.js +33 -5
- package/dist/tools.d.ts +9 -0
- package/dist/tools.js +244 -16
- package/dist/version-check.d.ts +14 -0
- package/dist/version-check.js +94 -0
- package/dist/version-guide.d.ts +19 -0
- package/dist/version-guide.js +223 -0
- package/package.json +2 -2
- package/spec/uwa-openapi.json +490 -106
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
|
-
|
|
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(`接口版本,默认 ${
|
|
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(
|
|
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
|
/** 找出对象里最长的那个数组字段,它通常就是主数据列表。 */
|
|
@@ -241,7 +298,74 @@ export function annotateCurveAxes(data) {
|
|
|
241
298
|
_axisHint: '每条曲线按 _axisBinding[曲线名].xAxisSource 指定的横轴取值,不要统一使用外层 x_axis',
|
|
242
299
|
};
|
|
243
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
|
+
}
|
|
244
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
|
+
}
|
|
245
369
|
/**
|
|
246
370
|
* 报告列表接口一次返回、按项目组分组。但上游会把同一份报告挂到每个项目组下
|
|
247
371
|
* (沙箱实测多组两两交集 100%),直接按组统计会成倍放大。
|
|
@@ -251,10 +375,10 @@ const RECORD_ARRAY_KEYS = /^gotOnline.*Records$/;
|
|
|
251
375
|
*/
|
|
252
376
|
export function annotateProjectGroups(data) {
|
|
253
377
|
if (!isObj(data) || !Array.isArray(data['content']))
|
|
254
|
-
return data;
|
|
378
|
+
return annotateModuleAvailable(data);
|
|
255
379
|
const groups = data['content'].filter(isObj);
|
|
256
380
|
if (groups.length < 2)
|
|
257
|
-
return data;
|
|
381
|
+
return annotateModuleAvailable(data);
|
|
258
382
|
// dataKey → 出现过的项目组;以及「该报告在哪个数组字段里、原文是什么」
|
|
259
383
|
const groupsOf = new Map();
|
|
260
384
|
const firstSeen = new Map();
|
|
@@ -283,7 +407,7 @@ export function annotateProjectGroups(data) {
|
|
|
283
407
|
}
|
|
284
408
|
const duplicated = [...groupsOf.values()].some((gs) => gs.size > 1);
|
|
285
409
|
if (!duplicated)
|
|
286
|
-
return data;
|
|
410
|
+
return annotateModuleAvailable(data);
|
|
287
411
|
// 已知项目组壳子(保留名称、engine 等元信息),报告重新按真实归属灌入
|
|
288
412
|
const shells = new Map();
|
|
289
413
|
for (const g of groups) {
|
|
@@ -318,15 +442,15 @@ export function annotateProjectGroups(data) {
|
|
|
318
442
|
}
|
|
319
443
|
// 丢掉合并后一个报告都没有的空组(它们原先只有别人的副本)
|
|
320
444
|
const content = [...shells.values()].filter((g) => Object.entries(g).some(([k, v]) => RECORD_ARRAY_KEYS.test(k) && Array.isArray(v) && v.length > 0));
|
|
321
|
-
return {
|
|
445
|
+
return annotateModuleAvailable({
|
|
322
446
|
...data,
|
|
323
447
|
content,
|
|
324
448
|
_deduplicated: true,
|
|
325
449
|
_uniqueReportCount: placed,
|
|
326
|
-
_dedupeNote: `上游把同一份报告重复挂在多个项目组下,已按 link 中 project=
|
|
450
|
+
_dedupeNote: `上游把同一份报告重复挂在多个项目组下,已按 link 中 project= 合并归位,去重后本页 ${placed} 份` +
|
|
327
451
|
(orphaned ? `(其中 ${orphaned} 份所属项目组不在本页,已单独列出)` : '') +
|
|
328
|
-
'
|
|
329
|
-
};
|
|
452
|
+
'。按项目组统计请用本页 _uniqueReportCount / 各组 Records;跨页汇总须自行按 dataKey 去重,勿把各页条数简单相加。',
|
|
453
|
+
});
|
|
330
454
|
}
|
|
331
455
|
/** MCP 的 content.text 必须是字符串,undefined 会让客户端校验失败。 */
|
|
332
456
|
function asText(value) {
|
|
@@ -336,25 +460,128 @@ function asText(value) {
|
|
|
336
460
|
return '(接口无返回内容)';
|
|
337
461
|
return JSON.stringify(value, null, 2);
|
|
338
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
|
+
}
|
|
339
501
|
export function makeHandler(op, client, defaultMaxRows, maxChars) {
|
|
340
502
|
return async (args) => {
|
|
341
503
|
try {
|
|
342
|
-
const apiVersion = args['apiVersion'] ?? op
|
|
504
|
+
const apiVersion = args['apiVersion'] ?? defaultApiVersionFor(op);
|
|
343
505
|
const wantDownload = args['download'] ?? true;
|
|
344
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
|
+
}
|
|
345
540
|
const query = {};
|
|
346
541
|
for (const p of op.query)
|
|
347
|
-
if (
|
|
348
|
-
query[p.name] =
|
|
542
|
+
if (argsForApi[p.name] !== undefined)
|
|
543
|
+
query[p.name] = argsForApi[p.name];
|
|
349
544
|
let body;
|
|
350
545
|
if (op.body.length) {
|
|
351
546
|
body = {};
|
|
352
547
|
for (const p of op.body)
|
|
353
|
-
if (
|
|
354
|
-
body[p.name] =
|
|
548
|
+
if (argsForApi[p.name] !== undefined)
|
|
549
|
+
body[p.name] = argsForApi[p.name];
|
|
355
550
|
}
|
|
356
551
|
const raw = await client.call(op.method, op.path, apiVersion, query, body);
|
|
357
|
-
|
|
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 };
|
|
584
|
+
}
|
|
358
585
|
const presignUrl = op.returnsPresignUrl && data && typeof data === 'object'
|
|
359
586
|
? data['dataPresignUrl']
|
|
360
587
|
: undefined;
|
|
@@ -392,6 +619,7 @@ export function makeHandler(op, client, defaultMaxRows, maxChars) {
|
|
|
392
619
|
}
|
|
393
620
|
const meta = {
|
|
394
621
|
...data,
|
|
622
|
+
...(stutterRootCauseGate(op.id) ?? {}),
|
|
395
623
|
_downloaded: true,
|
|
396
624
|
_format: payload.kind,
|
|
397
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;
|