@uwa4d/openapi-mcp 0.2.0-beta.8 → 0.2.0
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 +68 -1
- package/dist/annotate-dashboard.d.ts +14 -0
- package/dist/annotate-dashboard.js +138 -0
- package/dist/cli.js +11 -0
- package/dist/client.js +4 -13
- package/dist/composite/index.js +2 -0
- package/dist/composite/mcp-version.d.ts +2 -0
- package/dist/composite/mcp-version.js +48 -0
- package/dist/composite/report-diagnosis.js +139 -6
- package/dist/composite/top-resources.js +6 -6
- package/dist/error-hints.d.ts +23 -0
- package/dist/error-hints.js +80 -0
- package/dist/indicator-dashboard-keys.d.ts +1 -1
- package/dist/indicator-dashboard-keys.js +1 -0
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.js +15 -0
- package/dist/presets.js +1 -0
- package/dist/server-instructions.d.ts +5 -0
- package/dist/server-instructions.js +18 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.js +23 -18
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +95 -8
- package/dist/version-check.d.ts +41 -0
- package/dist/version-check.js +161 -0
- package/dist/version-guide.js +19 -5
- package/package.json +2 -1
- package/spec/overrides.json +46 -0
- package/spec/uwa-openapi.json +164 -115
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI / 下游错误码 → AI 友好排查建议。
|
|
3
|
+
* 产品语义按引擎在此投影;got-query 只提供稳定 rawMessage 原因码。
|
|
4
|
+
*/
|
|
5
|
+
export const FILE_META_REASONS = {
|
|
6
|
+
LG_META_ABSENT: 'LG_META_ABSENT',
|
|
7
|
+
LG_RUNTIME_PATH_MISSING: 'LG_RUNTIME_PATH_MISSING',
|
|
8
|
+
AT_META_ABSENT: 'AT_META_ABSENT',
|
|
9
|
+
AT_META_TYPE_INVALID: 'AT_META_TYPE_INVALID',
|
|
10
|
+
AT_META_PATH_MISSING: 'AT_META_PATH_MISSING',
|
|
11
|
+
};
|
|
12
|
+
/** OpenAPI 接口限流(UWA 服务端保护,预期行为)。 */
|
|
13
|
+
export const RATE_LIMIT_ERROR_CODE = 31004;
|
|
14
|
+
/** 复合工具 _partialErrors 含限流时附加的说明(与 resolveErrorHint(31004) 语义一致)。 */
|
|
15
|
+
export const RATE_LIMIT_HINT = 'OpenAPI 限流(31004)是 UWA 服务端保护,不是报告无数据或接口故障。可稍等重试、减少并发或缩小 assetTypes;' +
|
|
16
|
+
'若 _partialErrors 仅部分类型限流,已有 Top 仍可用,需说明可能不完整。';
|
|
17
|
+
const ERROR_HINTS_BY_CODE = {
|
|
18
|
+
20001: '业务参数不合法,核对参数名、取值范围和必填项(注意批量接口的参数名多为复数,如 dataKeys)',
|
|
19
|
+
23508: '数据服务错误,常见原因是报告不适用该接口版本(如新报告调用了 1.0 接口,或旧报告调用了 2.0 接口),改用对应版本重试',
|
|
20
|
+
24050: '该账号未开通 Open API 权限,请联系 UWA 工作人员开通',
|
|
21
|
+
24052: '请求参数有误,请对照接口文档检查',
|
|
22
|
+
24054: 'AppId 不存在,检查 appId 是否正确、是否用错了环境(sandbox / 线上凭证不通用)',
|
|
23
|
+
24056: '签名错误,检查 appSecret 是否正确',
|
|
24
|
+
24057: '时间戳过期,本机时间与服务端偏差不能超过 20 分钟',
|
|
25
|
+
30001: '服务端错误,常见原因是用错了引擎对应的接口(如对 UE 报告调用了 Unity 专用接口);勿把瞬时失败说成「一定没有数据」',
|
|
26
|
+
31004: 'OpenAPI 接口限流(UWA 服务端保护,预期行为)。稍等重试,或减少并行请求/缩小查询范围(如 top_resources 的 assetTypes);' +
|
|
27
|
+
'勿当成报告无数据或接口永久故障。',
|
|
28
|
+
};
|
|
29
|
+
/** OpenAPI 已按引擎写好的稳定文案(优先沿用,再补 AI 纪律)。 */
|
|
30
|
+
function hintForOpenApiRuntimeLogMessage(apiMessage) {
|
|
31
|
+
if (/暂不支持获取当前报告的运行日志/.test(apiMessage)) {
|
|
32
|
+
return 'UE 运行日志方案有过切换:只能说暂不支持获取当前报告的运行日志,勿断定报告本身无日志。旧报告可试 gotonline_overview_log_export。';
|
|
33
|
+
}
|
|
34
|
+
if (/运行日志未解析|运行日志解析未完成/.test(apiMessage)) {
|
|
35
|
+
return 'Unity:该报告无可用运行日志数据(未解析或解析未完成)。旧报告可试 gotonline_overview_log_export。';
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function hintForFileMetaReason(rawMessage, apiMessage) {
|
|
40
|
+
const fromApi = hintForOpenApiRuntimeLogMessage(apiMessage);
|
|
41
|
+
if (fromApi)
|
|
42
|
+
return fromApi;
|
|
43
|
+
if (rawMessage.includes(FILE_META_REASONS.LG_META_ABSENT) || rawMessage.includes(FILE_META_REASONS.LG_RUNTIME_PATH_MISSING)) {
|
|
44
|
+
return ('运行日志 file_meta 不可用(原因码见 rawMessage)。' +
|
|
45
|
+
'Unity:通常表示未解析/无可用日志数据;UE:只能说暂不支持获取当前报告的运行日志,勿断定报告本身无日志。' +
|
|
46
|
+
'旧报告可试 gotonline_overview_log_export。');
|
|
47
|
+
}
|
|
48
|
+
if (rawMessage.includes(FILE_META_REASONS.AT_META_ABSENT) ||
|
|
49
|
+
rawMessage.includes(FILE_META_REASONS.AT_META_TYPE_INVALID) ||
|
|
50
|
+
rawMessage.includes(FILE_META_REASONS.AT_META_PATH_MISSING)) {
|
|
51
|
+
return 'AT 资源 meta 不可用:可能未开启资源采集或未解析完成,勿把失败说成「一定没有资源」。';
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** 错误文案是否含 31004 / API_RATE_LIMIT。 */
|
|
56
|
+
export function isRateLimitMessage(text) {
|
|
57
|
+
return text.includes(`[${RATE_LIMIT_ERROR_CODE}]`) || text.includes('API_RATE_LIMIT') || text.includes('接口限流');
|
|
58
|
+
}
|
|
59
|
+
/** _partialErrors 中是否有限流项。 */
|
|
60
|
+
export function hasRateLimitInPartialErrors(errors) {
|
|
61
|
+
return errors.some(isRateLimitMessage);
|
|
62
|
+
}
|
|
63
|
+
/** 附带 _partialErrors;若含限流则加 _rateLimitHint。 */
|
|
64
|
+
export function withRateLimitAnnotation(extra, partialErrors) {
|
|
65
|
+
if (!partialErrors?.length)
|
|
66
|
+
return extra;
|
|
67
|
+
const out = { ...extra, _partialErrors: partialErrors };
|
|
68
|
+
if (hasRateLimitInPartialErrors(partialErrors)) {
|
|
69
|
+
out._rateLimitHint = RATE_LIMIT_HINT;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** 组装给模型看的排查建议;优先 rawMessage 原因码 + OpenAPI message。 */
|
|
74
|
+
export function resolveErrorHint(code, rawMessage, apiMessage) {
|
|
75
|
+
if (code === 80108) {
|
|
76
|
+
return (hintForFileMetaReason(rawMessage, apiMessage) ??
|
|
77
|
+
'文件 meta 不存在(FILE_META_NOT_EXIST)。运行日志/AT 等场景请结合接口 message 与报告引擎解释;勿臆造数据。');
|
|
78
|
+
}
|
|
79
|
+
return ERROR_HINTS_BY_CODE[code];
|
|
80
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** indicatorDashboards 合法面板传参名称(与 got-query OverviewIndicatorDashboardCatalogService 同源)。 */
|
|
2
2
|
export declare const INDICATOR_DASHBOARD_KEYS: readonly string[];
|
|
3
3
|
/** 常见误传:统计维度后缀 / 子指标名,不是面板标识符。 */
|
|
4
|
-
export declare const INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES: readonly ["fps_mean", "fps_maximum", "frametime_min", "frametime_gt_40_pct", "temperature_mean", "drawcall_maximum"];
|
|
4
|
+
export declare const INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES: readonly ["fps_mean", "fps_maximum", "frametime_min", "frametime_gt_40_pct", "temperature_mean", "drawcall_maximum", "android_pss_max"];
|
|
5
5
|
export declare function splitIndicatorDashboards(raw: unknown): string[];
|
|
6
6
|
export declare function findUnknownIndicatorDashboards(raw: unknown): string[];
|
|
7
7
|
/** 拆成已接受 / 未识别面板名(保持请求顺序,已接受去重)。 */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
function readPackageVersion() {
|
|
5
|
+
try {
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
|
|
8
|
+
return pkg.version ?? '0.0.0';
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return '0.0.0';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** 与 package.json 同步,避免 MCP 握手版本和 npm 包不一致。 */
|
|
15
|
+
export const PACKAGE_VERSION = readPackageVersion();
|
package/dist/presets.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server instructions:客户裸问时的全局纪律(勿依赖用户粘贴规则)。
|
|
3
|
+
* 与 docs/EMPTY_STATE_CONTRACT.md 对齐。
|
|
4
|
+
*/
|
|
5
|
+
export const SERVER_INSTRUCTIONS = [
|
|
6
|
+
'你是 UWA GOT Online OpenAPI MCP 助手。用人话查报告时遵守以下纪律:',
|
|
7
|
+
'',
|
|
8
|
+
'1. 多份候选报告:先列出备注/设备/时间,再选或先问用户;禁止静默默认却假装只有一份。',
|
|
9
|
+
'2. 没有某类报告(如 GPU/Mono):看 list 返回的 module_available;为 false 时明确说没有并停手。禁止用 Overview 的 RenderTexture / WaitForVsync 冒充 GPU 分析。',
|
|
10
|
+
'3. 说「GPU Bound / GPU 是主要瓶颈」之前:必须先查 gotonline_gpu_bound(或看 report_diagnosis.gpuBound)。空数组 [] 只能说「未检出 Bound」,不能说主要瓶颈是 GPU。',
|
|
11
|
+
'4. 卡顿根因:优先 gotonline_overview_stack_stutter_full_tree_presign / stutter_scene_tree;禁止把 WaitForVsync 直接当根因。尖峰再查单帧树。',
|
|
12
|
+
'5. 非法面板名要纠正(fps_mean→fps_avg;android_pss_max→android_memory_pss@max);数值带单位。PSS 优先读 pss_peak_kb/mb/frame。',
|
|
13
|
+
'6. 接口失败时说明原因:运行日志 80108 — Unity 无 LG file_meta 表示未解析、无可用日志;UE 无 meta 只能说「暂不支持获取当前报告的运行日志」(方案切换过),勿断定报告本身无日志。其它失败勿把瞬时错误说成「一定没有数据」。',
|
|
14
|
+
'7. 接口限流 31004(API_RATE_LIMIT):UWA 服务端保护,预期行为,不是 Bug。稍等重试或减少并行/批量(如 top_resources 缩小 assetTypes)。若返回含 _partialErrors 且 _rateLimitHint,部分类型限流时已有 Top 仍可用,需说明可能不完整,勿当成报告无资源。',
|
|
15
|
+
'8. MCP 版本(每对话最多一次):本对话第一次使用任意 UWA MCP 工具前,先调用一次 uwa_mcp_version 核对 npm(正式版只对比正式版,内部测试版只对比测试版,互不交叉)。若 updateAvailable=true / suggestTellUser=true,必须用 userNotice 主动转告并说明重启 MCP 升级;已是最新则不要打扰用户。同一对话内勿重复调用。用户显式问版本时也可再调。',
|
|
16
|
+
'',
|
|
17
|
+
'体检优先调用 report_diagnosis;细节再按 _apisUsed 深挖。对比多份报告时说明场景/时长/目标帧是否可比(见 comparability_note)。',
|
|
18
|
+
].join('\n');
|
package/dist/server.d.ts
CHANGED
|
@@ -16,9 +16,10 @@ export interface ServerOptions {
|
|
|
16
16
|
maxChars: number;
|
|
17
17
|
timeoutMs: number;
|
|
18
18
|
specPath?: string;
|
|
19
|
+
/** 覆盖默认 server instructions(用于启动时注入版本升级提醒)。 */
|
|
20
|
+
instructions?: string;
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
export declare const PACKAGE_VERSION: string;
|
|
22
|
+
export { PACKAGE_VERSION } from './package-version.js';
|
|
22
23
|
export declare function createServer(opts: ServerOptions): {
|
|
23
24
|
server: McpServer;
|
|
24
25
|
toolCount: number;
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
3
|
import { UwaClient } from './client.js';
|
|
@@ -8,18 +5,10 @@ import { registerCompositeTools, COMPOSITE_TOOLS, selectCompositeTools } from '.
|
|
|
8
5
|
import { selectOperations } from './presets.js';
|
|
9
6
|
import { loadSpec } from './spec.js';
|
|
10
7
|
import { makeHandler, toolConfig, toolName } from './tools.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return pkg.version ?? '0.0.0';
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return '0.0.0';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
/** 与 package.json 同步,避免 MCP 握手版本和 npm 包不一致。 */
|
|
22
|
-
export const PACKAGE_VERSION = readPackageVersion();
|
|
8
|
+
import { SERVER_INSTRUCTIONS } from './server-instructions.js';
|
|
9
|
+
import { PACKAGE_VERSION } from './package-version.js';
|
|
10
|
+
import { buildMcpVersionInfo, buildUpdateInstructionAppendix, scheduleUpdateNotice, } from './version-check.js';
|
|
11
|
+
export { PACKAGE_VERSION } from './package-version.js';
|
|
23
12
|
export function createServer(opts) {
|
|
24
13
|
const spec = loadSpec(opts.specPath);
|
|
25
14
|
const operations = selectOperations(spec.operations, { tools: opts.tools, engine: opts.engine });
|
|
@@ -28,7 +17,7 @@ export function createServer(opts) {
|
|
|
28
17
|
credentials: { appId: opts.appId, appSecret: opts.appSecret },
|
|
29
18
|
timeoutMs: opts.timeoutMs,
|
|
30
19
|
});
|
|
31
|
-
const server = new McpServer({ name: 'uwa-openapi-mcp', version: PACKAGE_VERSION });
|
|
20
|
+
const server = new McpServer({ name: 'uwa-openapi-mcp', version: PACKAGE_VERSION }, { instructions: opts.instructions ?? SERVER_INSTRUCTIONS });
|
|
32
21
|
for (const op of operations) {
|
|
33
22
|
server.registerTool(toolName(op.id, opts.nameCase, opts.namePrefix), toolConfig(op), makeHandler(op, client, opts.maxRows, opts.maxChars));
|
|
34
23
|
}
|
|
@@ -44,9 +33,25 @@ export function createServer(opts) {
|
|
|
44
33
|
return { server, toolCount, total, atomicCount: operations.length, compositeCount };
|
|
45
34
|
}
|
|
46
35
|
export async function startStdio(opts) {
|
|
47
|
-
|
|
36
|
+
let instructions = SERVER_INSTRUCTIONS;
|
|
37
|
+
if (process.env['UWA_MCP_SKIP_UPDATE_CHECK'] !== '1') {
|
|
38
|
+
try {
|
|
39
|
+
const info = await buildMcpVersionInfo(PACKAGE_VERSION);
|
|
40
|
+
const appendix = buildUpdateInstructionAppendix(info);
|
|
41
|
+
if (appendix)
|
|
42
|
+
instructions = `${SERVER_INSTRUCTIONS}\n\n${appendix}`;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* 版本检查失败不阻塞启动 */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const { server, toolCount, total, atomicCount, compositeCount } = createServer({
|
|
49
|
+
...opts,
|
|
50
|
+
instructions,
|
|
51
|
+
});
|
|
48
52
|
// stdout 是 MCP 协议通道,任何日志都必须走 stderr
|
|
49
|
-
console.error(`[uwa-openapi-mcp] 已加载 ${toolCount}/${total} 个工具(原子 ${atomicCount} + 复合 ${compositeCount}),接口地址 ${opts.baseUrl}`);
|
|
53
|
+
console.error(`[uwa-openapi-mcp] v${PACKAGE_VERSION} 已加载 ${toolCount}/${total} 个工具(原子 ${atomicCount} + 复合 ${compositeCount}),接口地址 ${opts.baseUrl}`);
|
|
54
|
+
scheduleUpdateNotice(PACKAGE_VERSION);
|
|
50
55
|
await server.connect(new StdioServerTransport());
|
|
51
56
|
}
|
|
52
57
|
export { selectCompositeTools, COMPOSITE_TOOLS };
|
package/dist/tools.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ export declare function annotateCurveAxes(data: unknown): unknown;
|
|
|
32
32
|
* 自定义面板 statistic/curve 返回空 `{}` 时,优先提示面板名传错(静默失败高发)。
|
|
33
33
|
*/
|
|
34
34
|
export declare function annotateDashboardEmptyResult(op: Operation, data: unknown, args: Record<string, unknown>): unknown;
|
|
35
|
+
/** 各 GOT Online 子模块是否有报告;供模型「无 GPU 停手」。 */
|
|
36
|
+
export declare function buildModuleAvailable(group: Record<string, unknown>): Record<string, boolean>;
|
|
37
|
+
export declare function annotateModuleAvailable(data: unknown): unknown;
|
|
35
38
|
/**
|
|
36
39
|
* 报告列表接口一次返回、按项目组分组。但上游会把同一份报告挂到每个项目组下
|
|
37
40
|
* (沙箱实测多组两两交集 100%),直接按组统计会成倍放大。
|
|
@@ -40,6 +43,8 @@ export declare function annotateDashboardEmptyResult(op: Operation, data: unknow
|
|
|
40
43
|
* 不额外打接口。上游修好后检测不到重复,本函数原样返回。
|
|
41
44
|
*/
|
|
42
45
|
export declare function annotateProjectGroups(data: unknown): unknown;
|
|
46
|
+
/** 空 Bound / 空日志列表等:补可教模型的语义,避免误判。 */
|
|
47
|
+
export declare function annotateEmptyStateSemantics(op: Operation, data: unknown): unknown;
|
|
43
48
|
export declare function makeHandler(op: Operation, client: UwaClient, defaultMaxRows: number, maxChars: number): (args: Record<string, unknown>) => Promise<ToolResult>;
|
|
44
49
|
export declare function toolConfig(op: Operation): {
|
|
45
50
|
title: string;
|
package/dist/tools.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { UwaApiError } from './client.js';
|
|
3
3
|
import { INDICATOR_DASHBOARD_PANEL_GUIDE, versionGuideFor } from './version-guide.js';
|
|
4
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
|
+
import { annotateStatisticDashboard, annotateOverviewStatisticPss, extractCurveFilterArgs, filterCurveData, hasCurveFilter, isIndicatorCurveDashboard, isIndicatorStatisticDashboard, isOverviewStatisticOp, stutterRootCauseGate, stripOuterXAxisWhenFramesOnly, } from './annotate-dashboard.js';
|
|
6
6
|
const ENGINE_LABEL = { unity: 'Unity', unreal: 'UE' };
|
|
7
7
|
/**
|
|
8
8
|
* 预签名数据默认完整返回,不按条数截断——实测各接口解压后在 10 KB ~ 500 KB 之间,
|
|
@@ -331,6 +331,41 @@ export function annotateDashboardEmptyResult(op, data, args) {
|
|
|
331
331
|
};
|
|
332
332
|
}
|
|
333
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
|
+
}
|
|
334
369
|
/**
|
|
335
370
|
* 报告列表接口一次返回、按项目组分组。但上游会把同一份报告挂到每个项目组下
|
|
336
371
|
* (沙箱实测多组两两交集 100%),直接按组统计会成倍放大。
|
|
@@ -340,10 +375,10 @@ const RECORD_ARRAY_KEYS = /^gotOnline.*Records$/;
|
|
|
340
375
|
*/
|
|
341
376
|
export function annotateProjectGroups(data) {
|
|
342
377
|
if (!isObj(data) || !Array.isArray(data['content']))
|
|
343
|
-
return data;
|
|
378
|
+
return annotateModuleAvailable(data);
|
|
344
379
|
const groups = data['content'].filter(isObj);
|
|
345
380
|
if (groups.length < 2)
|
|
346
|
-
return data;
|
|
381
|
+
return annotateModuleAvailable(data);
|
|
347
382
|
// dataKey → 出现过的项目组;以及「该报告在哪个数组字段里、原文是什么」
|
|
348
383
|
const groupsOf = new Map();
|
|
349
384
|
const firstSeen = new Map();
|
|
@@ -372,7 +407,7 @@ export function annotateProjectGroups(data) {
|
|
|
372
407
|
}
|
|
373
408
|
const duplicated = [...groupsOf.values()].some((gs) => gs.size > 1);
|
|
374
409
|
if (!duplicated)
|
|
375
|
-
return data;
|
|
410
|
+
return annotateModuleAvailable(data);
|
|
376
411
|
// 已知项目组壳子(保留名称、engine 等元信息),报告重新按真实归属灌入
|
|
377
412
|
const shells = new Map();
|
|
378
413
|
for (const g of groups) {
|
|
@@ -407,7 +442,7 @@ export function annotateProjectGroups(data) {
|
|
|
407
442
|
}
|
|
408
443
|
// 丢掉合并后一个报告都没有的空组(它们原先只有别人的副本)
|
|
409
444
|
const content = [...shells.values()].filter((g) => Object.entries(g).some(([k, v]) => RECORD_ARRAY_KEYS.test(k) && Array.isArray(v) && v.length > 0));
|
|
410
|
-
return {
|
|
445
|
+
return annotateModuleAvailable({
|
|
411
446
|
...data,
|
|
412
447
|
content,
|
|
413
448
|
_deduplicated: true,
|
|
@@ -415,7 +450,7 @@ export function annotateProjectGroups(data) {
|
|
|
415
450
|
_dedupeNote: `上游把同一份报告重复挂在多个项目组下,已按 link 中 project= 合并归位,去重后本页 ${placed} 份` +
|
|
416
451
|
(orphaned ? `(其中 ${orphaned} 份所属项目组不在本页,已单独列出)` : '') +
|
|
417
452
|
'。按项目组统计请用本页 _uniqueReportCount / 各组 Records;跨页汇总须自行按 dataKey 去重,勿把各页条数简单相加。',
|
|
418
|
-
};
|
|
453
|
+
});
|
|
419
454
|
}
|
|
420
455
|
/** MCP 的 content.text 必须是字符串,undefined 会让客户端校验失败。 */
|
|
421
456
|
function asText(value) {
|
|
@@ -425,6 +460,44 @@ function asText(value) {
|
|
|
425
460
|
return '(接口无返回内容)';
|
|
426
461
|
return JSON.stringify(value, null, 2);
|
|
427
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
|
+
}
|
|
428
501
|
export function makeHandler(op, client, defaultMaxRows, maxChars) {
|
|
429
502
|
return async (args) => {
|
|
430
503
|
try {
|
|
@@ -480,6 +553,9 @@ export function makeHandler(op, client, defaultMaxRows, maxChars) {
|
|
|
480
553
|
if (isIndicatorStatisticDashboard(op.path) && acceptedDashboards.length) {
|
|
481
554
|
processed = annotateStatisticDashboard(processed, acceptedDashboards, unknownDashboards);
|
|
482
555
|
}
|
|
556
|
+
else if (isOverviewStatisticOp(op.id, op.path)) {
|
|
557
|
+
processed = annotateOverviewStatisticPss(processed);
|
|
558
|
+
}
|
|
483
559
|
else if (isIndicatorCurveDashboard(op.path)) {
|
|
484
560
|
if (acceptedDashboards.length || unknownDashboards.length) {
|
|
485
561
|
processed = isObj(processed)
|
|
@@ -492,10 +568,20 @@ export function makeHandler(op, client, defaultMaxRows, maxChars) {
|
|
|
492
568
|
}
|
|
493
569
|
if (hasCurveFilter(curveFilter)) {
|
|
494
570
|
processed = filterCurveData(processed, curveFilter);
|
|
495
|
-
|
|
571
|
+
if (curveFilter.returnFramesOnly) {
|
|
572
|
+
processed = stripOuterXAxisWhenFramesOnly(processed, curveFilter);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
processed = annotateCurveAxes(processed);
|
|
576
|
+
}
|
|
496
577
|
}
|
|
497
578
|
}
|
|
498
|
-
|
|
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
|
+
}
|
|
499
585
|
const presignUrl = op.returnsPresignUrl && data && typeof data === 'object'
|
|
500
586
|
? data['dataPresignUrl']
|
|
501
587
|
: undefined;
|
|
@@ -533,6 +619,7 @@ export function makeHandler(op, client, defaultMaxRows, maxChars) {
|
|
|
533
619
|
}
|
|
534
620
|
const meta = {
|
|
535
621
|
...data,
|
|
622
|
+
...(stutterRootCauseGate(op.id) ?? {}),
|
|
536
623
|
_downloaded: true,
|
|
537
624
|
_format: payload.kind,
|
|
538
625
|
...(total !== undefined ? { _totalItems: total } : {}),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** npm 包名(与 package.json 同步)。 */
|
|
2
|
+
export declare const PACKAGE_NAME = "@uwa4d/openapi-mcp";
|
|
3
|
+
export interface UpdateCheckResult {
|
|
4
|
+
current: string;
|
|
5
|
+
latest: string;
|
|
6
|
+
/** npm dist-tag,仅作逻辑区分 */
|
|
7
|
+
registryTag: 'beta' | 'latest';
|
|
8
|
+
}
|
|
9
|
+
/** uwa_mcp_version 工具与启动 instructions 用的结构化版本信息。 */
|
|
10
|
+
export interface McpVersionInfo {
|
|
11
|
+
package: string;
|
|
12
|
+
currentVersion: string;
|
|
13
|
+
/** stable=正式版(客户) / internal=内部测试版;与 npm dist-tag 一一对应,互不交叉比对 */
|
|
14
|
+
releaseLine: 'stable' | 'internal';
|
|
15
|
+
/** 对用户/模型可见的通道名,正式版不说 beta */
|
|
16
|
+
releaseLabel: string;
|
|
17
|
+
latestOnChannel: string | null;
|
|
18
|
+
registryReachable: boolean;
|
|
19
|
+
updateAvailable: boolean;
|
|
20
|
+
userNotice: string;
|
|
21
|
+
upgradeHint: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ReleaseChannelPresentation {
|
|
24
|
+
registryTag: 'beta' | 'latest';
|
|
25
|
+
releaseLine: 'stable' | 'internal';
|
|
26
|
+
releaseLabel: string;
|
|
27
|
+
packageInstallRef: string;
|
|
28
|
+
}
|
|
29
|
+
/** 当前运行版本应跟哪个 npm dist-tag 比对新版(正式↔latest,测试↔beta,不交叉)。 */
|
|
30
|
+
export declare function resolveUpdateChannel(version: string): 'beta' | 'latest';
|
|
31
|
+
/** 正式版与内部测试版的分发呈现(客户侧不出现 beta 字样)。 */
|
|
32
|
+
export declare function describeReleaseChannel(version: string): ReleaseChannelPresentation;
|
|
33
|
+
/** 查询 registry 是否有比 current 更新的同通道版本;失败时返回 null。 */
|
|
34
|
+
export declare function checkForUpdate(current: string): Promise<UpdateCheckResult | null>;
|
|
35
|
+
/** 拉取完整版本信息(工具返回 + 启动 instructions)。 */
|
|
36
|
+
export declare function buildMcpVersionInfo(current: string): Promise<McpVersionInfo>;
|
|
37
|
+
/** 有新版本时追加到 server instructions,让模型在对话里主动提醒用户。 */
|
|
38
|
+
export declare function buildUpdateInstructionAppendix(info: McpVersionInfo): string | null;
|
|
39
|
+
export declare function formatUpdateNotice(result: UpdateCheckResult): string;
|
|
40
|
+
/** 启动时异步检查;仅写 stderr,不阻塞 MCP 握手。 */
|
|
41
|
+
export declare function scheduleUpdateNotice(current: string): void;
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
/** 当前运行版本应跟哪个 npm dist-tag 比对新版(正式↔latest,测试↔beta,不交叉)。 */
|
|
6
|
+
export function resolveUpdateChannel(version) {
|
|
7
|
+
return version.includes('-') ? 'beta' : 'latest';
|
|
8
|
+
}
|
|
9
|
+
/** 正式版与内部测试版的分发呈现(客户侧不出现 beta 字样)。 */
|
|
10
|
+
export function describeReleaseChannel(version) {
|
|
11
|
+
const registryTag = resolveUpdateChannel(version);
|
|
12
|
+
if (registryTag === 'beta') {
|
|
13
|
+
return {
|
|
14
|
+
registryTag: 'beta',
|
|
15
|
+
releaseLine: 'internal',
|
|
16
|
+
releaseLabel: '内部测试版',
|
|
17
|
+
packageInstallRef: `${PACKAGE_NAME}@beta`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
registryTag: 'latest',
|
|
22
|
+
releaseLine: 'stable',
|
|
23
|
+
releaseLabel: '正式版',
|
|
24
|
+
packageInstallRef: PACKAGE_NAME,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** 简易 semver 比较:a > b 返回 1,相等 0,a < b 返回 -1。 */
|
|
28
|
+
function compareVersions(a, b) {
|
|
29
|
+
if (a === b)
|
|
30
|
+
return 0;
|
|
31
|
+
const parseCore = (v) => v.split('-')[0].split('.').map((n) => Number.parseInt(n, 10) || 0);
|
|
32
|
+
const parsePre = (v) => {
|
|
33
|
+
const idx = v.indexOf('-');
|
|
34
|
+
if (idx < 0)
|
|
35
|
+
return [];
|
|
36
|
+
return v
|
|
37
|
+
.slice(idx + 1)
|
|
38
|
+
.split('.')
|
|
39
|
+
.map((part, i) => (i === 0 ? part : Number.parseInt(part, 10) || part));
|
|
40
|
+
};
|
|
41
|
+
const aCore = parseCore(a);
|
|
42
|
+
const bCore = parseCore(b);
|
|
43
|
+
for (let i = 0; i < Math.max(aCore.length, bCore.length); i++) {
|
|
44
|
+
const av = aCore[i] ?? 0;
|
|
45
|
+
const bv = bCore[i] ?? 0;
|
|
46
|
+
if (av !== bv)
|
|
47
|
+
return av > bv ? 1 : -1;
|
|
48
|
+
}
|
|
49
|
+
const aPre = parsePre(a);
|
|
50
|
+
const bPre = parsePre(b);
|
|
51
|
+
const aHasPre = aPre.length > 0;
|
|
52
|
+
const bHasPre = bPre.length > 0;
|
|
53
|
+
if (!aHasPre && bHasPre)
|
|
54
|
+
return 1;
|
|
55
|
+
if (aHasPre && !bHasPre)
|
|
56
|
+
return -1;
|
|
57
|
+
if (!aHasPre && !bHasPre)
|
|
58
|
+
return 0;
|
|
59
|
+
for (let i = 0; i < Math.max(aPre.length, bPre.length); i++) {
|
|
60
|
+
const av = aPre[i];
|
|
61
|
+
const bv = bPre[i];
|
|
62
|
+
if (av === bv)
|
|
63
|
+
continue;
|
|
64
|
+
if (av === undefined)
|
|
65
|
+
return -1;
|
|
66
|
+
if (bv === undefined)
|
|
67
|
+
return 1;
|
|
68
|
+
if (typeof av === 'number' && typeof bv === 'number')
|
|
69
|
+
return av > bv ? 1 : -1;
|
|
70
|
+
return String(av) > String(bv) ? 1 : -1;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
async function fetchDistTagVersion(channel) {
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(REGISTRY_URL, {
|
|
77
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
|
|
78
|
+
headers: { Accept: 'application/json' },
|
|
79
|
+
});
|
|
80
|
+
if (!res.ok)
|
|
81
|
+
return null;
|
|
82
|
+
const data = (await res.json());
|
|
83
|
+
const tag = data['dist-tags']?.[channel];
|
|
84
|
+
return typeof tag === 'string' && tag.trim() ? tag.trim() : null;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** 查询 registry 是否有比 current 更新的同通道版本;失败时返回 null。 */
|
|
91
|
+
export async function checkForUpdate(current) {
|
|
92
|
+
const info = await buildMcpVersionInfo(current);
|
|
93
|
+
if (!info.updateAvailable || !info.latestOnChannel)
|
|
94
|
+
return null;
|
|
95
|
+
return { current: info.currentVersion, latest: info.latestOnChannel, registryTag: describeReleaseChannel(current).registryTag };
|
|
96
|
+
}
|
|
97
|
+
function buildUpgradeHint(presentation) {
|
|
98
|
+
return (`若 MCP 配置未锁定具体版本号,请在您使用的 AI 客户端中重启 MCP 服务以升级(重载 MCP 配置或重启客户端均可);` +
|
|
99
|
+
`终端核对:npx --yes --prefer-online ${presentation.packageInstallRef} --version`);
|
|
100
|
+
}
|
|
101
|
+
/** 拉取完整版本信息(工具返回 + 启动 instructions)。 */
|
|
102
|
+
export async function buildMcpVersionInfo(current) {
|
|
103
|
+
const presentation = describeReleaseChannel(current);
|
|
104
|
+
const latestOnChannel = await fetchDistTagVersion(presentation.registryTag);
|
|
105
|
+
const registryReachable = latestOnChannel !== null;
|
|
106
|
+
const updateAvailable = registryReachable && latestOnChannel !== null && compareVersions(latestOnChannel, current) > 0;
|
|
107
|
+
const upgradeHint = buildUpgradeHint(presentation);
|
|
108
|
+
if (updateAvailable && latestOnChannel) {
|
|
109
|
+
return {
|
|
110
|
+
package: PACKAGE_NAME,
|
|
111
|
+
currentVersion: current,
|
|
112
|
+
releaseLine: presentation.releaseLine,
|
|
113
|
+
releaseLabel: presentation.releaseLabel,
|
|
114
|
+
latestOnChannel,
|
|
115
|
+
registryReachable,
|
|
116
|
+
updateAvailable: true,
|
|
117
|
+
userNotice: `UWA OpenAPI MCP(${presentation.releaseLabel})有新版本:当前 ${current},${presentation.releaseLabel}最新 ${latestOnChannel}。` +
|
|
118
|
+
upgradeHint,
|
|
119
|
+
upgradeHint,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const latestLabel = latestOnChannel ?? '(暂时无法连接 npm 核对最新版)';
|
|
123
|
+
return {
|
|
124
|
+
package: PACKAGE_NAME,
|
|
125
|
+
currentVersion: current,
|
|
126
|
+
releaseLine: presentation.releaseLine,
|
|
127
|
+
releaseLabel: presentation.releaseLabel,
|
|
128
|
+
latestOnChannel,
|
|
129
|
+
registryReachable,
|
|
130
|
+
updateAvailable: false,
|
|
131
|
+
userNotice: registryReachable
|
|
132
|
+
? `UWA OpenAPI MCP(${presentation.releaseLabel})已是最新:当前 ${current},${presentation.releaseLabel}最新 ${latestLabel}。`
|
|
133
|
+
: `UWA OpenAPI MCP(${presentation.releaseLabel})当前版本 ${current};暂时无法连接 npm 核对最新版。`,
|
|
134
|
+
upgradeHint,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** 有新版本时追加到 server instructions,让模型在对话里主动提醒用户。 */
|
|
138
|
+
export function buildUpdateInstructionAppendix(info) {
|
|
139
|
+
if (!info.updateAvailable || !info.latestOnChannel)
|
|
140
|
+
return null;
|
|
141
|
+
const headline = info.releaseLine === 'stable'
|
|
142
|
+
? '【MCP 正式版有新版本 — 请主动告知用户】'
|
|
143
|
+
: '【MCP 内部测试版有新版本 — 请主动告知内部测试人员】';
|
|
144
|
+
return (`${headline}当前运行 ${info.currentVersion},${info.releaseLabel}最新 ${info.latestOnChannel}。` +
|
|
145
|
+
`请用自然语言转告用户重启 MCP 升级(勿只写 stderr 日志)。详情可调工具 uwa_mcp_version。` +
|
|
146
|
+
` ${info.upgradeHint}`);
|
|
147
|
+
}
|
|
148
|
+
export function formatUpdateNotice(result) {
|
|
149
|
+
const presentation = describeReleaseChannel(result.current);
|
|
150
|
+
return (`[uwa-openapi-mcp] ${presentation.releaseLabel}有新版本 ${result.latest}(当前 ${result.current})。` +
|
|
151
|
+
buildUpgradeHint(presentation));
|
|
152
|
+
}
|
|
153
|
+
/** 启动时异步检查;仅写 stderr,不阻塞 MCP 握手。 */
|
|
154
|
+
export function scheduleUpdateNotice(current) {
|
|
155
|
+
if (process.env['UWA_MCP_SKIP_UPDATE_CHECK'] === '1')
|
|
156
|
+
return;
|
|
157
|
+
void checkForUpdate(current).then((result) => {
|
|
158
|
+
if (result)
|
|
159
|
+
console.error(formatUpdateNotice(result));
|
|
160
|
+
});
|
|
161
|
+
}
|