@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/client.js
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { gunzipSync } from 'node:zlib';
|
|
2
2
|
import { authHeaders } from './auth.js';
|
|
3
|
+
import { resolveErrorHint } from './error-hints.js';
|
|
3
4
|
function isEnvelope(v) {
|
|
4
5
|
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
5
6
|
return false;
|
|
6
7
|
const o = v;
|
|
7
8
|
return o['status'] === 'success' || o['status'] === 'failed';
|
|
8
9
|
}
|
|
9
|
-
/** 常见错误码的排查提示,直接给到模型,省掉一轮试错。 */
|
|
10
|
-
const ERROR_HINTS = {
|
|
11
|
-
20001: '业务参数不合法,核对参数名、取值范围和必填项(注意批量接口的参数名多为复数,如 dataKeys)',
|
|
12
|
-
23508: '数据服务错误,常见原因是报告不适用该接口版本(如新报告调用了 1.0 接口,或旧报告调用了 2.0 接口),改用对应版本重试',
|
|
13
|
-
24050: '该账号未开通 Open API 权限,请联系 UWA 工作人员开通',
|
|
14
|
-
24052: '请求参数有误,请对照接口文档检查',
|
|
15
|
-
24054: 'AppId 不存在,检查 appId 是否正确、是否用错了环境(sandbox / 线上凭证不通用)',
|
|
16
|
-
24056: '签名错误,检查 appSecret 是否正确',
|
|
17
|
-
24057: '时间戳过期,本机时间与服务端偏差不能超过 20 分钟',
|
|
18
|
-
30001: '服务端错误,常见原因是用错了引擎对应的接口(如对 UE 报告调用了 Unity 专用接口)',
|
|
19
|
-
};
|
|
20
10
|
export class UwaApiError extends Error {
|
|
21
11
|
code;
|
|
22
12
|
rawMessage;
|
|
@@ -68,8 +58,9 @@ export class UwaClient {
|
|
|
68
58
|
if (parsed.status === 'failed' || parsed.error) {
|
|
69
59
|
const code = parsed.error?.code ?? -1;
|
|
70
60
|
const raw = parsed.error?.data?.rawMessage ?? '';
|
|
71
|
-
const
|
|
72
|
-
|
|
61
|
+
const apiMessage = parsed.error?.message ?? '请求失败';
|
|
62
|
+
const hint = resolveErrorHint(code, raw, apiMessage);
|
|
63
|
+
throw new UwaApiError(code, raw, `[${code}] ${apiMessage}${raw ? ` (${raw})` : ''}${hint ? `\n排查建议:${hint}` : ''}`);
|
|
73
64
|
}
|
|
74
65
|
return parsed.data ?? {};
|
|
75
66
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ToolResult } from '../tools.js';
|
|
2
|
+
export declare function isObj(v: unknown): v is Record<string, unknown>;
|
|
3
|
+
export declare function asText(value: unknown): string;
|
|
4
|
+
/** 把任意结果压进字符闸门,超限时保留元信息 + 提示。 */
|
|
5
|
+
export declare function jsonToolResult(data: unknown, maxChars: number): ToolResult;
|
|
6
|
+
export declare function errorResult(err: unknown): ToolResult;
|
|
7
|
+
/** 从 createDate 字符串取出 YYYY-MM-DD,无法解析则返回空。 */
|
|
8
|
+
export declare function dayOf(dateStr: string | undefined | null): string | null;
|
|
9
|
+
export declare function dayGte(day: string, cutoff: string): boolean;
|
|
10
|
+
export declare function dayGt(day: string, cutoff: string): boolean;
|
|
11
|
+
export declare function dayLt(day: string, cutoff: string): boolean;
|
|
12
|
+
export declare function dayLte(day: string, cutoff: string): boolean;
|
|
13
|
+
/** 宽松比较 SDK 版本号(按点分数字)。缺省或无法解析视为不满足。 */
|
|
14
|
+
export declare function sdkAtLeast(sdk: string | undefined | null, min: string): boolean;
|
|
15
|
+
export declare function requireReportKey(args: Record<string, unknown>): {
|
|
16
|
+
dataKey?: string;
|
|
17
|
+
recordId?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function topNOf(args: Record<string, unknown>, fallback?: number): number;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export function isObj(v) {
|
|
2
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
3
|
+
}
|
|
4
|
+
export function asText(value) {
|
|
5
|
+
if (typeof value === 'string')
|
|
6
|
+
return value;
|
|
7
|
+
if (value === undefined || value === null)
|
|
8
|
+
return '(无返回内容)';
|
|
9
|
+
return JSON.stringify(value, null, 2);
|
|
10
|
+
}
|
|
11
|
+
/** 把任意结果压进字符闸门,超限时保留元信息 + 提示。 */
|
|
12
|
+
export function jsonToolResult(data, maxChars) {
|
|
13
|
+
const text = asText(data);
|
|
14
|
+
if (maxChars <= 0 || text.length <= maxChars) {
|
|
15
|
+
return { content: [{ type: 'text', text }] };
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: 'text',
|
|
21
|
+
text: asText({
|
|
22
|
+
_truncated: `返回超过 ${maxChars} 字符上限已截断。请缩小 topN / 查询范围,或启动时调大 --max-chars。`,
|
|
23
|
+
_returnedChars: maxChars,
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
{ type: 'text', text: text.slice(0, maxChars) },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function errorResult(err) {
|
|
31
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32
|
+
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
/** 从 createDate 字符串取出 YYYY-MM-DD,无法解析则返回空。 */
|
|
35
|
+
export function dayOf(dateStr) {
|
|
36
|
+
if (!dateStr)
|
|
37
|
+
return null;
|
|
38
|
+
const m = /(\d{4}-\d{2}-\d{2})/.exec(String(dateStr));
|
|
39
|
+
return m?.[1] ?? null;
|
|
40
|
+
}
|
|
41
|
+
export function dayGte(day, cutoff) {
|
|
42
|
+
return day >= cutoff;
|
|
43
|
+
}
|
|
44
|
+
export function dayGt(day, cutoff) {
|
|
45
|
+
return day > cutoff;
|
|
46
|
+
}
|
|
47
|
+
export function dayLt(day, cutoff) {
|
|
48
|
+
return day < cutoff;
|
|
49
|
+
}
|
|
50
|
+
export function dayLte(day, cutoff) {
|
|
51
|
+
return day <= cutoff;
|
|
52
|
+
}
|
|
53
|
+
/** 宽松比较 SDK 版本号(按点分数字)。缺省或无法解析视为不满足。 */
|
|
54
|
+
export function sdkAtLeast(sdk, min) {
|
|
55
|
+
if (!sdk)
|
|
56
|
+
return false;
|
|
57
|
+
const parse = (s) => s
|
|
58
|
+
.replace(/^v/i, '')
|
|
59
|
+
.split(/[^0-9]+/)
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map((n) => Number.parseInt(n, 10));
|
|
62
|
+
const a = parse(sdk);
|
|
63
|
+
const b = parse(min);
|
|
64
|
+
if (!a.length || a.some((n) => Number.isNaN(n)))
|
|
65
|
+
return false;
|
|
66
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
67
|
+
const x = a[i] ?? 0;
|
|
68
|
+
const y = b[i] ?? 0;
|
|
69
|
+
if (x > y)
|
|
70
|
+
return true;
|
|
71
|
+
if (x < y)
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
export function requireReportKey(args) {
|
|
77
|
+
const dataKey = typeof args['dataKey'] === 'string' && args['dataKey'] ? args['dataKey'] : undefined;
|
|
78
|
+
const recordId = args['recordId'] !== undefined && args['recordId'] !== null && args['recordId'] !== ''
|
|
79
|
+
? String(args['recordId'])
|
|
80
|
+
: undefined;
|
|
81
|
+
if (!dataKey && !recordId)
|
|
82
|
+
throw new Error('必须提供 dataKey 或 recordId');
|
|
83
|
+
return { dataKey, recordId };
|
|
84
|
+
}
|
|
85
|
+
export function topNOf(args, fallback = 20) {
|
|
86
|
+
const n = typeof args['topN'] === 'number' ? args['topN'] : fallback;
|
|
87
|
+
if (!Number.isFinite(n) || n < 1)
|
|
88
|
+
return fallback;
|
|
89
|
+
return Math.min(Math.floor(n), 200);
|
|
90
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { Engine } from '../spec.js';
|
|
3
|
+
import { type NameCase } from '../tools.js';
|
|
4
|
+
import type { UwaClient } from '../client.js';
|
|
5
|
+
import type { CompositeTool } from './types.js';
|
|
6
|
+
/** 全部手写复合工具(不进 uwa-openapi.json)。 */
|
|
7
|
+
export declare const COMPOSITE_TOOLS: readonly CompositeTool[];
|
|
8
|
+
export declare const COMPOSITE_DEFAULT_IDS: readonly string[];
|
|
9
|
+
export interface SelectCompositeOptions {
|
|
10
|
+
tools?: string[];
|
|
11
|
+
engine?: Engine | 'all';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 与原子工具同一套 --tool / --engine 过滤语义。
|
|
15
|
+
* 未指定 --tool 时视为 preset.default(复合工具若声明了该键则选中)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function selectCompositeTools(opts: SelectCompositeOptions): CompositeTool[];
|
|
18
|
+
export declare function registerCompositeTools(server: McpServer, client: UwaClient, opts: {
|
|
19
|
+
tools: string[];
|
|
20
|
+
engine: Engine | 'all';
|
|
21
|
+
nameCase: NameCase;
|
|
22
|
+
namePrefix: string;
|
|
23
|
+
maxChars: number;
|
|
24
|
+
}): number;
|
|
25
|
+
export declare function compositeFilterKeys(): string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { toolName } from '../tools.js';
|
|
2
|
+
import { topResourcesTool } from './top-resources.js';
|
|
3
|
+
import { topFunctionsTool } from './top-functions.js';
|
|
4
|
+
import { reportDiagnosisTool } from './report-diagnosis.js';
|
|
5
|
+
/** 全部手写复合工具(不进 uwa-openapi.json)。 */
|
|
6
|
+
export const COMPOSITE_TOOLS = [
|
|
7
|
+
topResourcesTool,
|
|
8
|
+
topFunctionsTool,
|
|
9
|
+
reportDiagnosisTool,
|
|
10
|
+
];
|
|
11
|
+
export const COMPOSITE_DEFAULT_IDS = COMPOSITE_TOOLS.map((t) => t.id);
|
|
12
|
+
/**
|
|
13
|
+
* 与原子工具同一套 --tool / --engine 过滤语义。
|
|
14
|
+
* 未指定 --tool 时视为 preset.default(复合工具若声明了该键则选中)。
|
|
15
|
+
*/
|
|
16
|
+
export function selectCompositeTools(opts) {
|
|
17
|
+
const wanted = new Set((opts.tools?.length ? opts.tools : ['preset.default']).map((t) => t.trim()).filter(Boolean));
|
|
18
|
+
const engine = opts.engine ?? 'all';
|
|
19
|
+
return COMPOSITE_TOOLS.filter((t) => {
|
|
20
|
+
if (engine !== 'all' && !t.engines.includes(engine))
|
|
21
|
+
return false;
|
|
22
|
+
const keys = new Set([t.id, 'preset.all', 'all', 'composite', ...t.filterKeys]);
|
|
23
|
+
return [...keys].some((k) => wanted.has(k));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function registerCompositeTools(server, client, opts) {
|
|
27
|
+
const selected = selectCompositeTools({ tools: opts.tools, engine: opts.engine });
|
|
28
|
+
const ctx = {
|
|
29
|
+
client,
|
|
30
|
+
maxChars: opts.maxChars,
|
|
31
|
+
nameCase: opts.nameCase,
|
|
32
|
+
namePrefix: opts.namePrefix,
|
|
33
|
+
engine: opts.engine,
|
|
34
|
+
};
|
|
35
|
+
for (const tool of selected) {
|
|
36
|
+
server.registerTool(toolName(tool.id, opts.nameCase, opts.namePrefix), {
|
|
37
|
+
title: tool.title,
|
|
38
|
+
description: tool.description,
|
|
39
|
+
inputSchema: tool.inputSchema,
|
|
40
|
+
}, (async (args) => tool.handler(args ?? {}, ctx)));
|
|
41
|
+
}
|
|
42
|
+
return selected.length;
|
|
43
|
+
}
|
|
44
|
+
export function compositeFilterKeys() {
|
|
45
|
+
return [...new Set(COMPOSITE_TOOLS.flatMap((t) => [t.id, 'composite', ...t.filterKeys]))].sort();
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 按函数名过滤(子串或 /regex/flags),供 top_functions 本地过滤 idmap 结果。 */
|
|
2
|
+
export declare function matchesNamePattern(name: string, pattern?: unknown): boolean;
|
|
3
|
+
export declare function filterRowsByNamePattern<T extends {
|
|
4
|
+
name: string;
|
|
5
|
+
}>(rows: T[], pattern: unknown): {
|
|
6
|
+
rows: T[];
|
|
7
|
+
matchedTotal: number;
|
|
8
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** 按函数名过滤(子串或 /regex/flags),供 top_functions 本地过滤 idmap 结果。 */
|
|
2
|
+
export function matchesNamePattern(name, pattern) {
|
|
3
|
+
if (pattern == null || (typeof pattern === 'string' && !pattern.trim()))
|
|
4
|
+
return true;
|
|
5
|
+
const p = String(pattern).trim();
|
|
6
|
+
if (p.length >= 2 && p.startsWith('/') && p.lastIndexOf('/') > 0) {
|
|
7
|
+
const last = p.lastIndexOf('/');
|
|
8
|
+
const body = p.slice(1, last);
|
|
9
|
+
const flags = p.slice(last + 1);
|
|
10
|
+
try {
|
|
11
|
+
return new RegExp(body, flags).test(name);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return name.toLowerCase().includes(p.toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return name.toLowerCase().includes(p.toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
export function filterRowsByNamePattern(rows, pattern) {
|
|
20
|
+
if (pattern == null || (typeof pattern === 'string' && !pattern.trim())) {
|
|
21
|
+
return { rows, matchedTotal: rows.length };
|
|
22
|
+
}
|
|
23
|
+
const matched = rows.filter((r) => matchesNamePattern(r.name, pattern));
|
|
24
|
+
return { rows: matched, matchedTotal: matched.length };
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overview 统计 v1 / v2 / UE 2.0 返回结构差异很大:
|
|
3
|
+
* - Unity v1:对象形 brief { fps_mean: [49] } + asset_stats
|
|
4
|
+
* - UE 2.0:数组形 brief [{ label, value }] + Resource 段
|
|
5
|
+
* - Unity v2(POST):categories[] + basicInfo{}
|
|
6
|
+
* 这里统一抽成复合工具能用的扁平视图。
|
|
7
|
+
*/
|
|
8
|
+
export interface OverviewView {
|
|
9
|
+
/** 简报指标 key → 单值 */
|
|
10
|
+
brief: Record<string, unknown>;
|
|
11
|
+
/** 报告基本信息 label/key → 单值 */
|
|
12
|
+
summary: Record<string, unknown>;
|
|
13
|
+
/** 函数/卡顿相关条目 */
|
|
14
|
+
functions: {
|
|
15
|
+
name: string;
|
|
16
|
+
value: number;
|
|
17
|
+
unit?: string;
|
|
18
|
+
source: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
}[];
|
|
21
|
+
/** 内存/资源相关条目(类型级) */
|
|
22
|
+
memory: {
|
|
23
|
+
name: string;
|
|
24
|
+
assetType: string;
|
|
25
|
+
memoryBytes: number;
|
|
26
|
+
_source: string;
|
|
27
|
+
}[];
|
|
28
|
+
/** 是否开启资源采集;无法判定时为 null */
|
|
29
|
+
hasResource: boolean | null;
|
|
30
|
+
shape: 'v1' | 'v2' | 'ue-v2' | 'unknown';
|
|
31
|
+
}
|
|
32
|
+
/** 把 unwrap 后的报告或原始 data 统一成 OverviewView。 */
|
|
33
|
+
export declare function viewOverview(data: unknown): OverviewView;
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { isObj } from './helpers.js';
|
|
2
|
+
function firstVal(v) {
|
|
3
|
+
return Array.isArray(v) ? v[0] : v;
|
|
4
|
+
}
|
|
5
|
+
function numOf(v) {
|
|
6
|
+
const x = firstVal(v);
|
|
7
|
+
if (typeof x === 'number' && Number.isFinite(x))
|
|
8
|
+
return x;
|
|
9
|
+
if (typeof x === 'string' && x.trim() && !Number.isNaN(Number(x)))
|
|
10
|
+
return Number(x);
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function toBytes(value, unit) {
|
|
14
|
+
const u = (unit ?? '').toLowerCase();
|
|
15
|
+
if (u === 'b' || u === 'byte' || u === 'bytes')
|
|
16
|
+
return Math.round(value);
|
|
17
|
+
if (u === 'kb')
|
|
18
|
+
return Math.round(value * 1024);
|
|
19
|
+
if (u === 'mb')
|
|
20
|
+
return Math.round(value * 1024 * 1024);
|
|
21
|
+
if (u === 'gb')
|
|
22
|
+
return Math.round(value * 1024 * 1024 * 1024);
|
|
23
|
+
// 无单位时:很大的数当字节,否则当 MB(兼容 v1 brief 的 _mem_max_mb)
|
|
24
|
+
return value > 10_000 ? Math.round(value) : Math.round(value * 1024 * 1024);
|
|
25
|
+
}
|
|
26
|
+
function unitFromLabel(label) {
|
|
27
|
+
const m = /[((]\s*(MB|KB|GB|B|字节)\s*[))]/i.exec(label);
|
|
28
|
+
return m?.[1]?.toLowerCase() === '字节' ? 'b' : m?.[1]?.toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
/** 排除「数量峰值」这类非内存指标。 */
|
|
31
|
+
function looksLikeMemoryMetric(label, key, unit) {
|
|
32
|
+
if (/数量|count|cnt/i.test(label) || /count|cnt/i.test(key))
|
|
33
|
+
return false;
|
|
34
|
+
if (/^(b|byte|bytes|kb|mb|gb)$/i.test(unit))
|
|
35
|
+
return true;
|
|
36
|
+
return /内存|memory|mem|reserved|used|heap|gfx|mono|lua/i.test(label + key);
|
|
37
|
+
}
|
|
38
|
+
function guessAssetType(label, fallback) {
|
|
39
|
+
const rules = [
|
|
40
|
+
[/RenderTexture/i, 'RenderTexture'],
|
|
41
|
+
[/纹理|Texture/i, 'Texture'],
|
|
42
|
+
[/网格|Mesh/i, 'Mesh'],
|
|
43
|
+
[/Shader/i, 'Shader'],
|
|
44
|
+
[/材质|Material/i, 'Material'],
|
|
45
|
+
[/动画|Animation/i, 'AnimationClip'],
|
|
46
|
+
[/音频|Audio/i, 'AudioClip'],
|
|
47
|
+
[/字体|Font/i, 'Font'],
|
|
48
|
+
[/AssetBundle/i, 'AssetBundle'],
|
|
49
|
+
[/TextAsset|文本资源/i, 'TextAsset'],
|
|
50
|
+
[/粒子|Particle/i, 'ParticleSystem'],
|
|
51
|
+
[/Lua/i, 'Lua'],
|
|
52
|
+
[/Mono/i, 'Mono'],
|
|
53
|
+
[/Gfx/i, 'Gfx'],
|
|
54
|
+
];
|
|
55
|
+
for (const [re, t] of rules)
|
|
56
|
+
if (re.test(label))
|
|
57
|
+
return t;
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
/** 把 summary / UE brief 一类 [{label,value}] 展开成 map。 */
|
|
61
|
+
function expandLabelValueRows(rows) {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
if (!isObj(row))
|
|
65
|
+
continue;
|
|
66
|
+
const label = String(row['label'] ?? row['key'] ?? '');
|
|
67
|
+
if (!label)
|
|
68
|
+
continue;
|
|
69
|
+
out[label] = firstVal(row['value'] ?? row['data']);
|
|
70
|
+
const key = row['key'];
|
|
71
|
+
if (typeof key === 'string' && key && !(key in out))
|
|
72
|
+
out[key] = out[label];
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function basicInfoMap(basicInfo) {
|
|
77
|
+
const out = {};
|
|
78
|
+
if (!basicInfo)
|
|
79
|
+
return out;
|
|
80
|
+
const rows = Array.isArray(basicInfo)
|
|
81
|
+
? basicInfo
|
|
82
|
+
: isObj(basicInfo)
|
|
83
|
+
? Object.values(basicInfo)
|
|
84
|
+
: [];
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
if (!isObj(row))
|
|
87
|
+
continue;
|
|
88
|
+
const key = String(row['key'] ?? row['label'] ?? '');
|
|
89
|
+
if (!key)
|
|
90
|
+
continue;
|
|
91
|
+
out[key] = firstVal(row['data'] ?? row['value']);
|
|
92
|
+
const label = row['label'];
|
|
93
|
+
if (typeof label === 'string' && label && !(label in out))
|
|
94
|
+
out[label] = out[key];
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function parseHasResource(summary, brief) {
|
|
99
|
+
for (const src of [brief, summary]) {
|
|
100
|
+
for (const [k, v] of Object.entries(src)) {
|
|
101
|
+
const keyHit = /^(OverviewHasResource|Resource|resourceMemory|resourceManagement|资源内存|资源管理|资源采集)$/i.test(k);
|
|
102
|
+
if (!keyHit)
|
|
103
|
+
continue;
|
|
104
|
+
const n = numOf(v);
|
|
105
|
+
if (n != null)
|
|
106
|
+
return n !== 0;
|
|
107
|
+
const s = String(firstVal(v) ?? '');
|
|
108
|
+
if (s === '1' || s === '2' || /详细|开启|千帧|60|模式/.test(s))
|
|
109
|
+
return true;
|
|
110
|
+
if (s === '0' || /关闭|无|关/.test(s))
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* UE 2.0:从 Resource / resource 段判定资源采集。
|
|
118
|
+
* 「资源内存占用峰值」> 0 → true;段存在但无法确认时返回 null(不误报关闭)。
|
|
119
|
+
*/
|
|
120
|
+
function parseHasResourceFromResourceSection(report) {
|
|
121
|
+
const section = report['Resource'] ?? report['resource'];
|
|
122
|
+
if (!Array.isArray(section) || section.length === 0)
|
|
123
|
+
return null;
|
|
124
|
+
let peak = null;
|
|
125
|
+
let anyMemPositive = false;
|
|
126
|
+
for (const item of section) {
|
|
127
|
+
if (!isObj(item))
|
|
128
|
+
continue;
|
|
129
|
+
const label = String(item['label'] ?? item['key'] ?? '');
|
|
130
|
+
const n = numOf(item['value'] ?? item['data']);
|
|
131
|
+
if (n == null)
|
|
132
|
+
continue;
|
|
133
|
+
if (/^资源内存占用峰值/.test(label) || /资源内存占用峰值(MB)/.test(label))
|
|
134
|
+
peak = n;
|
|
135
|
+
const unit = unitFromLabel(label) ?? '';
|
|
136
|
+
if (looksLikeMemoryMetric(label, '', unit) && n > 0)
|
|
137
|
+
anyMemPositive = true;
|
|
138
|
+
}
|
|
139
|
+
if (peak != null && peak > 0)
|
|
140
|
+
return true;
|
|
141
|
+
if (anyMemPositive)
|
|
142
|
+
return true;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
/** 解析 Resource / resource 数组 → memory[](排除数量峰值)。 */
|
|
146
|
+
function memoryFromResourceSection(report) {
|
|
147
|
+
const section = report['Resource'] ?? report['resource'];
|
|
148
|
+
if (!Array.isArray(section))
|
|
149
|
+
return [];
|
|
150
|
+
const memory = [];
|
|
151
|
+
for (const item of section) {
|
|
152
|
+
if (!isObj(item))
|
|
153
|
+
continue;
|
|
154
|
+
const label = String(item['label'] ?? item['key'] ?? '');
|
|
155
|
+
const num = numOf(item['value'] ?? item['data']);
|
|
156
|
+
if (!label || num == null)
|
|
157
|
+
continue;
|
|
158
|
+
const unit = unitFromLabel(label) ?? 'mb';
|
|
159
|
+
if (!looksLikeMemoryMetric(label, '', unit))
|
|
160
|
+
continue;
|
|
161
|
+
// 总「资源内存占用峰值」不绑具体类型,仍保留便于看总量
|
|
162
|
+
memory.push({
|
|
163
|
+
name: label,
|
|
164
|
+
assetType: guessAssetType(label, 'Resource'),
|
|
165
|
+
memoryBytes: toBytes(num, unit),
|
|
166
|
+
_source: 'overview.Resource',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return memory;
|
|
170
|
+
}
|
|
171
|
+
function extractV2(data) {
|
|
172
|
+
const summary = basicInfoMap(data['basicInfo']);
|
|
173
|
+
const brief = {};
|
|
174
|
+
const functions = [];
|
|
175
|
+
const memory = [];
|
|
176
|
+
const categories = Array.isArray(data['categories']) ? data['categories'] : [];
|
|
177
|
+
for (const cat of categories) {
|
|
178
|
+
if (!isObj(cat))
|
|
179
|
+
continue;
|
|
180
|
+
const catName = String(cat['category'] ?? '');
|
|
181
|
+
const indicators = Array.isArray(cat['indicators']) ? cat['indicators'] : [];
|
|
182
|
+
for (const ind of indicators) {
|
|
183
|
+
if (!isObj(ind))
|
|
184
|
+
continue;
|
|
185
|
+
const label = String(ind['label'] ?? ind['key'] ?? '');
|
|
186
|
+
const key = String(ind['key'] ?? ind['indicatorKey'] ?? label);
|
|
187
|
+
const value = numOf(ind['data'] ?? ind['value']);
|
|
188
|
+
const unitObj = ind['unit'];
|
|
189
|
+
const unit = isObj(unitObj) ? String(unitObj['value'] ?? unitObj['description'] ?? '') : String(unitObj ?? '');
|
|
190
|
+
if (value != null && key)
|
|
191
|
+
brief[key] = value;
|
|
192
|
+
if (value != null && label && !(label in brief))
|
|
193
|
+
brief[label] = value;
|
|
194
|
+
const isFuncCat = /卡顿|重点函数|函数/.test(catName);
|
|
195
|
+
const isMemCat = /内存|资源|LLM/i.test(catName);
|
|
196
|
+
if (isFuncCat && value != null && label) {
|
|
197
|
+
functions.push({
|
|
198
|
+
name: label,
|
|
199
|
+
value,
|
|
200
|
+
unit: unit || undefined,
|
|
201
|
+
source: `overview.v2.${catName}`,
|
|
202
|
+
label: catName,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (isMemCat && value != null && label && looksLikeMemoryMetric(label, key, unit)) {
|
|
206
|
+
memory.push({
|
|
207
|
+
name: label,
|
|
208
|
+
assetType: guessAssetType(label, catName),
|
|
209
|
+
memoryBytes: toBytes(value, unit),
|
|
210
|
+
_source: `overview.v2.${catName}`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
functions.sort((a, b) => b.value - a.value);
|
|
216
|
+
memory.sort((a, b) => b.memoryBytes - a.memoryBytes);
|
|
217
|
+
return {
|
|
218
|
+
brief,
|
|
219
|
+
summary,
|
|
220
|
+
functions,
|
|
221
|
+
memory,
|
|
222
|
+
hasResource: parseHasResource(summary, brief),
|
|
223
|
+
shape: 'v2',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function extractV1OrUe(report) {
|
|
227
|
+
const brief = {};
|
|
228
|
+
const summary = {};
|
|
229
|
+
const functions = [];
|
|
230
|
+
const memory = [];
|
|
231
|
+
const briefIsArray = Array.isArray(report['brief']);
|
|
232
|
+
if (briefIsArray) {
|
|
233
|
+
Object.assign(brief, expandLabelValueRows(report['brief']));
|
|
234
|
+
}
|
|
235
|
+
else if (isObj(report['brief'])) {
|
|
236
|
+
for (const [k, v] of Object.entries(report['brief']))
|
|
237
|
+
brief[k] = firstVal(v);
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(report['summary'])) {
|
|
240
|
+
Object.assign(summary, expandLabelValueRows(report['summary']));
|
|
241
|
+
}
|
|
242
|
+
else if (isObj(report['reportInfo'])) {
|
|
243
|
+
Object.assign(summary, report['reportInfo']);
|
|
244
|
+
}
|
|
245
|
+
if (Array.isArray(report['func'])) {
|
|
246
|
+
for (const item of report['func']) {
|
|
247
|
+
if (!isObj(item))
|
|
248
|
+
continue;
|
|
249
|
+
const name = String(item['label'] ?? item['key'] ?? item['name'] ?? '');
|
|
250
|
+
const value = numOf(item['value'] ?? item['data']);
|
|
251
|
+
if (!name || value == null)
|
|
252
|
+
continue;
|
|
253
|
+
functions.push({ name, value, source: 'overview.func', label: name });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (isObj(report['brief'])) {
|
|
257
|
+
for (const [k, v] of Object.entries(report['brief'])) {
|
|
258
|
+
if (!/_mem_max_mb$/i.test(k) && !/Memory.*max/i.test(k))
|
|
259
|
+
continue;
|
|
260
|
+
const num = numOf(v);
|
|
261
|
+
if (num == null)
|
|
262
|
+
continue;
|
|
263
|
+
memory.push({
|
|
264
|
+
name: k,
|
|
265
|
+
assetType: k.replace(/_mem_max_mb$/i, ''),
|
|
266
|
+
memoryBytes: toBytes(num, 'mb'),
|
|
267
|
+
_source: 'overview.brief',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// UE 数组 brief 里也可能夹带内存类指标(类型级)
|
|
272
|
+
if (briefIsArray) {
|
|
273
|
+
for (const [label, v] of Object.entries(brief)) {
|
|
274
|
+
if (!looksLikeMemoryMetric(label, '', unitFromLabel(label) ?? ''))
|
|
275
|
+
continue;
|
|
276
|
+
const num = numOf(v);
|
|
277
|
+
if (num == null)
|
|
278
|
+
continue;
|
|
279
|
+
if (memory.some((m) => m.name === label))
|
|
280
|
+
continue;
|
|
281
|
+
memory.push({
|
|
282
|
+
name: label,
|
|
283
|
+
assetType: guessAssetType(label, 'brief'),
|
|
284
|
+
memoryBytes: toBytes(num, unitFromLabel(label) ?? 'mb'),
|
|
285
|
+
_source: 'overview.brief',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const row of memoryFromResourceSection(report)) {
|
|
290
|
+
if (memory.some((m) => m.name === row.name))
|
|
291
|
+
continue;
|
|
292
|
+
memory.push(row);
|
|
293
|
+
}
|
|
294
|
+
if (Array.isArray(report['asset_stats'])) {
|
|
295
|
+
for (const item of report['asset_stats']) {
|
|
296
|
+
if (!isObj(item))
|
|
297
|
+
continue;
|
|
298
|
+
const label = String(item['label'] ?? item['key'] ?? '');
|
|
299
|
+
const num = numOf(item['value'] ?? item['data']);
|
|
300
|
+
if (!label || num == null)
|
|
301
|
+
continue;
|
|
302
|
+
if (memory.some((m) => m.name === label))
|
|
303
|
+
continue;
|
|
304
|
+
const looksMb = /mb|内存/i.test(label);
|
|
305
|
+
memory.push({
|
|
306
|
+
name: label,
|
|
307
|
+
assetType: guessAssetType(label, label),
|
|
308
|
+
memoryBytes: looksMb ? toBytes(num, 'mb') : toBytes(num, 'b'),
|
|
309
|
+
_source: 'overview.asset_stats',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(report['LLM'])) {
|
|
314
|
+
for (const item of report['LLM']) {
|
|
315
|
+
if (!isObj(item))
|
|
316
|
+
continue;
|
|
317
|
+
const label = String(item['label'] ?? item['key'] ?? '');
|
|
318
|
+
const num = numOf(item['value'] ?? item['data']);
|
|
319
|
+
if (!label || num == null)
|
|
320
|
+
continue;
|
|
321
|
+
memory.push({ name: label, assetType: 'LLM', memoryBytes: toBytes(num, 'b'), _source: 'overview.LLM' });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
functions.sort((a, b) => b.value - a.value);
|
|
325
|
+
memory.sort((a, b) => b.memoryBytes - a.memoryBytes);
|
|
326
|
+
const fromSection = parseHasResourceFromResourceSection(report);
|
|
327
|
+
const fromFields = parseHasResource(summary, brief);
|
|
328
|
+
return {
|
|
329
|
+
brief,
|
|
330
|
+
summary,
|
|
331
|
+
functions,
|
|
332
|
+
memory,
|
|
333
|
+
hasResource: fromSection ?? fromFields,
|
|
334
|
+
shape: briefIsArray ? 'ue-v2' : 'v1',
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/** 把 unwrap 后的报告或原始 data 统一成 OverviewView。 */
|
|
338
|
+
export function viewOverview(data) {
|
|
339
|
+
if (!data) {
|
|
340
|
+
return { brief: {}, summary: {}, functions: [], memory: [], hasResource: null, shape: 'unknown' };
|
|
341
|
+
}
|
|
342
|
+
// 数组取第一份
|
|
343
|
+
let root = data;
|
|
344
|
+
if (Array.isArray(data) && data.length)
|
|
345
|
+
root = data[0];
|
|
346
|
+
if (!isObj(root)) {
|
|
347
|
+
return { brief: {}, summary: {}, functions: [], memory: [], hasResource: null, shape: 'unknown' };
|
|
348
|
+
}
|
|
349
|
+
if (Array.isArray(root['categories']) || root['basicInfo'] != null)
|
|
350
|
+
return extractV2(root);
|
|
351
|
+
// batch map: { [dataKey]: { categories... } }
|
|
352
|
+
const values = Object.values(root);
|
|
353
|
+
if (values.length === 1 && isObj(values[0]) && (Array.isArray(values[0]['categories']) || values[0]['basicInfo'])) {
|
|
354
|
+
return extractV2(values[0]);
|
|
355
|
+
}
|
|
356
|
+
for (const k of ['reports', 'list', 'content', 'data', 'records']) {
|
|
357
|
+
const v = root[k];
|
|
358
|
+
if (Array.isArray(v) && v.length && isObj(v[0]))
|
|
359
|
+
return viewOverview(v[0]);
|
|
360
|
+
}
|
|
361
|
+
if ('brief' in root ||
|
|
362
|
+
'func' in root ||
|
|
363
|
+
'summary' in root ||
|
|
364
|
+
'asset_stats' in root ||
|
|
365
|
+
'Resource' in root ||
|
|
366
|
+
'resource' in root) {
|
|
367
|
+
return extractV1OrUe(root);
|
|
368
|
+
}
|
|
369
|
+
// 可能整包就是 v2 包在更深一层
|
|
370
|
+
if (values.length && isObj(values[0]) && Array.isArray(values[0]['categories']))
|
|
371
|
+
return extractV2(values[0]);
|
|
372
|
+
return extractV1OrUe(root);
|
|
373
|
+
}
|