@uwa4d/openapi-mcp 0.2.0-beta.1 → 0.2.0-beta.3
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 +27 -2
- package/dist/cli.js +18 -6
- 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/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 +174 -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 +65 -0
- package/dist/composite/stack-agg.js +257 -0
- package/dist/composite/top-functions.d.ts +16 -0
- package/dist/composite/top-functions.js +133 -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/presets.js +4 -0
- package/dist/server.d.ts +6 -1
- package/dist/server.js +29 -4
- package/dist/tools.js +5 -0
- package/dist/version-guide.d.ts +9 -0
- package/dist/version-guide.js +90 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,9 @@ npx -y @uwa4d/openapi-mcp check -a <app_id> -s <app_secret>
|
|
|
107
107
|
# 组合:默认集 + 所有预签名类接口 + 单个指定工具
|
|
108
108
|
-t preset.default,presign,gotonline_overview_stack_id_map
|
|
109
109
|
|
|
110
|
+
# 只要复合工具(报告体检 / Top 资源 / Top 函数)
|
|
111
|
+
-t composite
|
|
112
|
+
|
|
110
113
|
# 只做 Unity 项目,砍掉 UE 专用工具
|
|
111
114
|
-t preset.default -e unity
|
|
112
115
|
```
|
|
@@ -118,6 +121,18 @@ npx -y @uwa4d/openapi-mcp list-tools
|
|
|
118
121
|
npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
119
122
|
```
|
|
120
123
|
|
|
124
|
+
## 复合工具
|
|
125
|
+
|
|
126
|
+
默认 preset 里除了原子接口,还有三个**复合工具**——内部会串多个接口并聚合,适合对话式提问:
|
|
127
|
+
|
|
128
|
+
| 工具 | 做什么 |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `report_diagnosis` | 单份报告体检:自动选对统计 v1/v2,附带 Top 资源/函数与场景摘要 |
|
|
131
|
+
| `top_resources` | 资源内存 Top N(默认 11 种类型;`groupBy=merged` 全局 Top / `assetType` 按类型各 Top) |
|
|
132
|
+
| `top_functions` | 函数 Top N:下载堆栈树 + idmap,按函数名合并同名节点后再排序 |
|
|
133
|
+
|
|
134
|
+
不确定该调统计 1.0 还是 2.0 时,优先用这三个,不必自己判断日期和 SDK。
|
|
135
|
+
|
|
121
136
|
## 大数据接口的返回方式
|
|
122
137
|
|
|
123
138
|
堆栈树、资源列表、卡顿帧等接口不直接返回数据,而是返回一个有效期 600 秒的预签名下载地址。
|
|
@@ -189,8 +204,18 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
|
189
204
|
**提示凭证错误**
|
|
190
205
|
用 `check` 命令在终端单独验证一次,排除是客户端配置传参的问题。注意测试环境和生产环境的凭证不通用。
|
|
191
206
|
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
**返回「数据服务错误」或不确定该用 v1 还是 v2**
|
|
208
|
+
|
|
209
|
+
部分接口按报告解析/提交日期和引擎区分版本:
|
|
210
|
+
|
|
211
|
+
| 场景 | 用哪个 |
|
|
212
|
+
| --- | --- |
|
|
213
|
+
| Unity Overview,解析日 **< 2026-07-09** | `get_overview_statistic_v1` |
|
|
214
|
+
| Unity Overview,解析日 **≥ 2026-07-09** 且 SDK ≥ 2.5.1 | `get_overview_statistic_v2` |
|
|
215
|
+
| UE Overview,提交日 **≤ 2026-03-25** | `get_overview_statistic_v1` |
|
|
216
|
+
| UE Overview,提交日 **> 2026-03-25** | `get_ue_overview_statistic_v2` |
|
|
217
|
+
|
|
218
|
+
场景统计、运行日志、快照帧等也有类似 1.0/2.0 成对工具,描述开头有「选用规则」。不确定时直接用复合工具 `report_diagnosis` / `top_functions`,内部会按报告详情自动路由。
|
|
194
219
|
|
|
195
220
|
## 许可
|
|
196
221
|
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command, Option } from 'commander';
|
|
3
3
|
import { UwaClient } from './client.js';
|
|
4
4
|
import { loadSpec } from './spec.js';
|
|
5
|
+
import { compositeFilterKeys, selectCompositeTools, COMPOSITE_TOOLS } from './composite/index.js';
|
|
5
6
|
import { filterKeys, selectOperations } from './presets.js';
|
|
6
7
|
import { DEFAULT_MAX_CHARS, DEFAULT_MAX_ROWS, toolName } from './tools.js';
|
|
7
8
|
import { PACKAGE_VERSION, startStdio } from './server.js';
|
|
@@ -70,10 +71,10 @@ program
|
|
|
70
71
|
.option('--spec <path>', '自定义 spec 文件路径')
|
|
71
72
|
.action((opts) => {
|
|
72
73
|
const spec = loadSpec(opts.spec);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}).map((
|
|
74
|
+
const toolFilter = opts.tool ?? ['all'];
|
|
75
|
+
const engine = opts.engine;
|
|
76
|
+
const selected = new Set(selectOperations(spec.operations, { tools: toolFilter, engine }).map((o) => o.id));
|
|
77
|
+
const selectedComposite = new Set(selectCompositeTools({ tools: toolFilter, engine }).map((t) => t.id));
|
|
77
78
|
const byModule = new Map();
|
|
78
79
|
for (const op of spec.operations) {
|
|
79
80
|
const key = op.modules.join(',');
|
|
@@ -89,8 +90,19 @@ program
|
|
|
89
90
|
console.log(`${mark} ${toolName(op.id, 'snake', '').padEnd(52)} ${op.method.padEnd(4)} ${engines.padEnd(7)} ${op.name}`);
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
console.log(
|
|
93
|
-
|
|
93
|
+
console.log('\n## composite(手写复合工具)');
|
|
94
|
+
for (const t of COMPOSITE_TOOLS) {
|
|
95
|
+
const mark = selectedComposite.has(t.id) ? '*' : ' ';
|
|
96
|
+
const engines = t.engines.map((e) => (e === 'unity' ? 'U3D' : 'UE')).join('+');
|
|
97
|
+
console.log(`${mark} ${toolName(t.id, 'snake', '').padEnd(52)} CMP ${engines.padEnd(7)} ${t.title}`);
|
|
98
|
+
}
|
|
99
|
+
const total = spec.operations.length + COMPOSITE_TOOLS.length;
|
|
100
|
+
const selectedCount = selected.size + selectedComposite.size;
|
|
101
|
+
console.log(`\n共 ${total} 个工具(原子 ${spec.operations.length} + 复合 ${COMPOSITE_TOOLS.length}),当前条件选中 ${selectedCount} 个(* 标记)`);
|
|
102
|
+
const keys = [
|
|
103
|
+
...new Set([...spec.operations.flatMap(filterKeys), ...compositeFilterKeys()]),
|
|
104
|
+
].sort();
|
|
105
|
+
console.log(`可用过滤键:${keys.join(', ')}`);
|
|
94
106
|
});
|
|
95
107
|
program
|
|
96
108
|
.command('check')
|
|
@@ -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,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
|
+
}
|