@uwa4d/openapi-mcp 0.2.0-beta.0 → 0.2.0-beta.2
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 +36 -3
- 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 +32 -0
- package/dist/composite/overview-view.js +252 -0
- package/dist/composite/report-diagnosis.d.ts +2 -0
- package/dist/composite/report-diagnosis.js +157 -0
- package/dist/composite/route-overview.d.ts +71 -0
- package/dist/composite/route-overview.js +232 -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 +184 -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 +5 -1
- package/dist/server.js +15 -4
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +168 -10
- package/dist/version-guide.d.ts +9 -0
- package/dist/version-guide.js +87 -0
- package/package.json +1 -1
- package/spec/overrides.json +26 -0
- package/spec/uwa-openapi.json +6 -6
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(Unity 逐资源合并多类型;UE 为类型级峰值) |
|
|
132
|
+
| `top_functions` | 函数 Top N:下载堆栈树 + idmap,按函数名合并同名节点后再排序 |
|
|
133
|
+
|
|
134
|
+
不确定该调统计 1.0 还是 2.0 时,优先用这三个,不必自己判断日期和 SDK。
|
|
135
|
+
|
|
121
136
|
## 大数据接口的返回方式
|
|
122
137
|
|
|
123
138
|
堆栈树、资源列表、卡顿帧等接口不直接返回数据,而是返回一个有效期 600 秒的预签名下载地址。
|
|
@@ -128,7 +143,15 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
|
128
143
|
- `_totalItems` — 主数据列表的条目总数
|
|
129
144
|
- `_returnedChars` — 实际返回的字符数
|
|
130
145
|
|
|
131
|
-
只在明确只要前 N 条时才传 `maxRows
|
|
146
|
+
只在明确只要前 N 条时才传 `maxRows`。
|
|
147
|
+
|
|
148
|
+
## 返回体积控制
|
|
149
|
+
|
|
150
|
+
`--max-chars`(默认 50 万字符)对**所有**工具生效,不只是上面这类下载型接口。批量统计、报告列表这类接口一次查十几份报告就可能产生二十万字符以上的返回,容易撑爆模型上下文。
|
|
151
|
+
|
|
152
|
+
超限时不会粗暴截断字符串——那样会切出无法解析的 JSON——而是按条目缩减,保证返回始终是完整合法的结构,并在 `_truncated` 里说明缩减到了多少条、该怎么拿到剩下的(分页或缩小批量)。
|
|
153
|
+
|
|
154
|
+
如果你的客户端对单次工具输出有更严格的限制,把 `--max-chars` 调小即可。
|
|
132
155
|
|
|
133
156
|
## 曲线接口的双横轴
|
|
134
157
|
|
|
@@ -181,8 +204,18 @@ npx -y @uwa4d/openapi-mcp list-tools -t overview -e unity
|
|
|
181
204
|
**提示凭证错误**
|
|
182
205
|
用 `check` 命令在终端单独验证一次,排除是客户端配置传参的问题。注意测试环境和生产环境的凭证不通用。
|
|
183
206
|
|
|
184
|
-
|
|
185
|
-
|
|
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`,内部会按报告详情自动路由。
|
|
186
219
|
|
|
187
220
|
## 许可
|
|
188
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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overview 统计 v1 / v2 返回结构差异很大:
|
|
3
|
+
* - v1:flat brief / func / asset_stats / summary
|
|
4
|
+
* - v2(Unity POST):categories[] + basicInfo{}
|
|
5
|
+
* 这里统一抽成复合工具能用的扁平视图。
|
|
6
|
+
*/
|
|
7
|
+
export interface OverviewView {
|
|
8
|
+
/** 简报指标 key → 单值 */
|
|
9
|
+
brief: Record<string, unknown>;
|
|
10
|
+
/** 报告基本信息 label/key → 单值 */
|
|
11
|
+
summary: Record<string, unknown>;
|
|
12
|
+
/** 函数/卡顿相关条目 */
|
|
13
|
+
functions: {
|
|
14
|
+
name: string;
|
|
15
|
+
value: number;
|
|
16
|
+
unit?: string;
|
|
17
|
+
source: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
}[];
|
|
20
|
+
/** 内存/资源相关条目(类型级) */
|
|
21
|
+
memory: {
|
|
22
|
+
name: string;
|
|
23
|
+
assetType: string;
|
|
24
|
+
memoryBytes: number;
|
|
25
|
+
_source: string;
|
|
26
|
+
}[];
|
|
27
|
+
/** 是否开启资源采集;无法判定时为 null */
|
|
28
|
+
hasResource: boolean | null;
|
|
29
|
+
shape: 'v1' | 'v2' | 'unknown';
|
|
30
|
+
}
|
|
31
|
+
/** 把 unwrap 后的报告或原始 data 统一成 OverviewView。 */
|
|
32
|
+
export declare function viewOverview(data: unknown): OverviewView;
|
|
@@ -0,0 +1,252 @@
|
|
|
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
|
+
/** 排除「数量峰值」这类非内存指标。 */
|
|
27
|
+
function looksLikeMemoryMetric(label, key, unit) {
|
|
28
|
+
if (/数量|count|cnt/i.test(label) || /count|cnt/i.test(key))
|
|
29
|
+
return false;
|
|
30
|
+
if (/^(b|byte|bytes|kb|mb|gb)$/i.test(unit))
|
|
31
|
+
return true;
|
|
32
|
+
return /内存|memory|mem|reserved|used|heap|gfx|mono|lua/i.test(label + key);
|
|
33
|
+
}
|
|
34
|
+
function guessAssetType(label, fallback) {
|
|
35
|
+
const m = /(Texture|Mesh|Shader|Material|Animation|Audio|Font|RenderTexture|AssetBundle|TextAsset|Lua|Mono|Gfx|GC)/i.exec(label);
|
|
36
|
+
return m?.[1] ?? fallback;
|
|
37
|
+
}
|
|
38
|
+
function basicInfoMap(basicInfo) {
|
|
39
|
+
const out = {};
|
|
40
|
+
if (!basicInfo)
|
|
41
|
+
return out;
|
|
42
|
+
const rows = Array.isArray(basicInfo)
|
|
43
|
+
? basicInfo
|
|
44
|
+
: isObj(basicInfo)
|
|
45
|
+
? Object.values(basicInfo)
|
|
46
|
+
: [];
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
if (!isObj(row))
|
|
49
|
+
continue;
|
|
50
|
+
const key = String(row['key'] ?? row['label'] ?? '');
|
|
51
|
+
if (!key)
|
|
52
|
+
continue;
|
|
53
|
+
out[key] = firstVal(row['data'] ?? row['value']);
|
|
54
|
+
const label = row['label'];
|
|
55
|
+
if (typeof label === 'string' && label && !(label in out))
|
|
56
|
+
out[label] = out[key];
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function parseHasResource(summary, brief) {
|
|
61
|
+
for (const src of [brief, summary]) {
|
|
62
|
+
for (const [k, v] of Object.entries(src)) {
|
|
63
|
+
const keyHit = /^(OverviewHasResource|Resource|resourceMemory|resourceManagement|资源内存|资源管理|资源采集)$/i.test(k);
|
|
64
|
+
if (!keyHit)
|
|
65
|
+
continue;
|
|
66
|
+
const n = numOf(v);
|
|
67
|
+
if (n != null)
|
|
68
|
+
return n !== 0;
|
|
69
|
+
const s = String(firstVal(v) ?? '');
|
|
70
|
+
if (s === '1' || s === '2' || /详细|开启|千帧|60|模式/.test(s))
|
|
71
|
+
return true;
|
|
72
|
+
if (s === '0' || /关闭|无|关/.test(s))
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function extractV2(data) {
|
|
79
|
+
const summary = basicInfoMap(data['basicInfo']);
|
|
80
|
+
const brief = {};
|
|
81
|
+
const functions = [];
|
|
82
|
+
const memory = [];
|
|
83
|
+
const categories = Array.isArray(data['categories']) ? data['categories'] : [];
|
|
84
|
+
for (const cat of categories) {
|
|
85
|
+
if (!isObj(cat))
|
|
86
|
+
continue;
|
|
87
|
+
const catName = String(cat['category'] ?? '');
|
|
88
|
+
const indicators = Array.isArray(cat['indicators']) ? cat['indicators'] : [];
|
|
89
|
+
for (const ind of indicators) {
|
|
90
|
+
if (!isObj(ind))
|
|
91
|
+
continue;
|
|
92
|
+
const label = String(ind['label'] ?? ind['key'] ?? '');
|
|
93
|
+
const key = String(ind['key'] ?? ind['indicatorKey'] ?? label);
|
|
94
|
+
const value = numOf(ind['data'] ?? ind['value']);
|
|
95
|
+
const unitObj = ind['unit'];
|
|
96
|
+
const unit = isObj(unitObj) ? String(unitObj['value'] ?? unitObj['description'] ?? '') : String(unitObj ?? '');
|
|
97
|
+
if (value != null && key)
|
|
98
|
+
brief[key] = value;
|
|
99
|
+
if (value != null && label && !(label in brief))
|
|
100
|
+
brief[label] = value;
|
|
101
|
+
const isFuncCat = /卡顿|重点函数|函数/.test(catName);
|
|
102
|
+
const isMemCat = /内存|资源|LLM/i.test(catName);
|
|
103
|
+
if (isFuncCat && value != null && label) {
|
|
104
|
+
functions.push({
|
|
105
|
+
name: label,
|
|
106
|
+
value,
|
|
107
|
+
unit: unit || undefined,
|
|
108
|
+
source: `overview.v2.${catName}`,
|
|
109
|
+
label: catName,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (isMemCat && value != null && label && looksLikeMemoryMetric(label, key, unit)) {
|
|
113
|
+
memory.push({
|
|
114
|
+
name: label,
|
|
115
|
+
assetType: guessAssetType(label, catName),
|
|
116
|
+
memoryBytes: toBytes(value, unit),
|
|
117
|
+
_source: `overview.v2.${catName}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
functions.sort((a, b) => b.value - a.value);
|
|
123
|
+
memory.sort((a, b) => b.memoryBytes - a.memoryBytes);
|
|
124
|
+
return {
|
|
125
|
+
brief,
|
|
126
|
+
summary,
|
|
127
|
+
functions,
|
|
128
|
+
memory,
|
|
129
|
+
hasResource: parseHasResource(summary, brief),
|
|
130
|
+
shape: 'v2',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function extractV1(report) {
|
|
134
|
+
const brief = {};
|
|
135
|
+
const summary = {};
|
|
136
|
+
const functions = [];
|
|
137
|
+
const memory = [];
|
|
138
|
+
if (isObj(report['brief'])) {
|
|
139
|
+
for (const [k, v] of Object.entries(report['brief']))
|
|
140
|
+
brief[k] = firstVal(v);
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(report['summary'])) {
|
|
143
|
+
for (const row of report['summary']) {
|
|
144
|
+
if (!isObj(row))
|
|
145
|
+
continue;
|
|
146
|
+
const label = String(row['label'] ?? row['key'] ?? '');
|
|
147
|
+
if (!label)
|
|
148
|
+
continue;
|
|
149
|
+
summary[label] = firstVal(row['value'] ?? row['data']);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (isObj(report['reportInfo'])) {
|
|
153
|
+
Object.assign(summary, report['reportInfo']);
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(report['func'])) {
|
|
156
|
+
for (const item of report['func']) {
|
|
157
|
+
if (!isObj(item))
|
|
158
|
+
continue;
|
|
159
|
+
const name = String(item['label'] ?? item['key'] ?? item['name'] ?? '');
|
|
160
|
+
const value = numOf(item['value'] ?? item['data']);
|
|
161
|
+
if (!name || value == null)
|
|
162
|
+
continue;
|
|
163
|
+
functions.push({ name, value, source: 'overview.func', label: name });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (isObj(report['brief'])) {
|
|
167
|
+
for (const [k, v] of Object.entries(report['brief'])) {
|
|
168
|
+
if (!/_mem_max_mb$/i.test(k) && !/Memory.*max/i.test(k))
|
|
169
|
+
continue;
|
|
170
|
+
const num = numOf(v);
|
|
171
|
+
if (num == null)
|
|
172
|
+
continue;
|
|
173
|
+
memory.push({
|
|
174
|
+
name: k,
|
|
175
|
+
assetType: k.replace(/_mem_max_mb$/i, ''),
|
|
176
|
+
memoryBytes: toBytes(num, 'mb'),
|
|
177
|
+
_source: 'overview.brief',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(report['asset_stats'])) {
|
|
182
|
+
for (const item of report['asset_stats']) {
|
|
183
|
+
if (!isObj(item))
|
|
184
|
+
continue;
|
|
185
|
+
const label = String(item['label'] ?? item['key'] ?? '');
|
|
186
|
+
const num = numOf(item['value'] ?? item['data']);
|
|
187
|
+
if (!label || num == null)
|
|
188
|
+
continue;
|
|
189
|
+
if (memory.some((m) => m.name === label))
|
|
190
|
+
continue;
|
|
191
|
+
const looksMb = /mb|内存/i.test(label);
|
|
192
|
+
memory.push({
|
|
193
|
+
name: label,
|
|
194
|
+
assetType: label,
|
|
195
|
+
memoryBytes: looksMb ? toBytes(num, 'mb') : toBytes(num, 'b'),
|
|
196
|
+
_source: 'overview.asset_stats',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (Array.isArray(report['LLM'])) {
|
|
201
|
+
for (const item of report['LLM']) {
|
|
202
|
+
if (!isObj(item))
|
|
203
|
+
continue;
|
|
204
|
+
const label = String(item['label'] ?? item['key'] ?? '');
|
|
205
|
+
const num = numOf(item['value'] ?? item['data']);
|
|
206
|
+
if (!label || num == null)
|
|
207
|
+
continue;
|
|
208
|
+
memory.push({ name: label, assetType: 'LLM', memoryBytes: toBytes(num, 'b'), _source: 'overview.LLM' });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
functions.sort((a, b) => b.value - a.value);
|
|
212
|
+
memory.sort((a, b) => b.memoryBytes - a.memoryBytes);
|
|
213
|
+
return {
|
|
214
|
+
brief,
|
|
215
|
+
summary,
|
|
216
|
+
functions,
|
|
217
|
+
memory,
|
|
218
|
+
hasResource: parseHasResource(summary, brief),
|
|
219
|
+
shape: 'v1',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/** 把 unwrap 后的报告或原始 data 统一成 OverviewView。 */
|
|
223
|
+
export function viewOverview(data) {
|
|
224
|
+
if (!data) {
|
|
225
|
+
return { brief: {}, summary: {}, functions: [], memory: [], hasResource: null, shape: 'unknown' };
|
|
226
|
+
}
|
|
227
|
+
// 数组取第一份
|
|
228
|
+
let root = data;
|
|
229
|
+
if (Array.isArray(data) && data.length)
|
|
230
|
+
root = data[0];
|
|
231
|
+
if (!isObj(root)) {
|
|
232
|
+
return { brief: {}, summary: {}, functions: [], memory: [], hasResource: null, shape: 'unknown' };
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(root['categories']) || root['basicInfo'] != null)
|
|
235
|
+
return extractV2(root);
|
|
236
|
+
// batch map: { [dataKey]: { categories... } }
|
|
237
|
+
const values = Object.values(root);
|
|
238
|
+
if (values.length === 1 && isObj(values[0]) && (Array.isArray(values[0]['categories']) || values[0]['basicInfo'])) {
|
|
239
|
+
return extractV2(values[0]);
|
|
240
|
+
}
|
|
241
|
+
for (const k of ['reports', 'list', 'content', 'data', 'records']) {
|
|
242
|
+
const v = root[k];
|
|
243
|
+
if (Array.isArray(v) && v.length && isObj(v[0]))
|
|
244
|
+
return viewOverview(v[0]);
|
|
245
|
+
}
|
|
246
|
+
if ('brief' in root || 'func' in root || 'summary' in root || 'asset_stats' in root)
|
|
247
|
+
return extractV1(root);
|
|
248
|
+
// 可能整包就是 v2 包在更深一层
|
|
249
|
+
if (values.length && isObj(values[0]) && Array.isArray(values[0]['categories']))
|
|
250
|
+
return extractV2(values[0]);
|
|
251
|
+
return extractV1(root);
|
|
252
|
+
}
|