@uwa4d/openapi-mcp 0.2.0-beta.1 → 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 +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 +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.js +5 -0
- package/dist/version-guide.d.ts +9 -0
- package/dist/version-guide.js +87 -0
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { z } from './types.js';
|
|
2
|
+
import { errorResult, jsonToolResult, requireReportKey } from './helpers.js';
|
|
3
|
+
import { fetchOverviewStatistic, fetchReportIdentity, fetchSceneStatistic, } from './route-overview.js';
|
|
4
|
+
import { viewOverview } from './overview-view.js';
|
|
5
|
+
import { runTopFunctions } from './top-functions.js';
|
|
6
|
+
import { runTopResources } from './top-resources.js';
|
|
7
|
+
function summarizeScenes(data) {
|
|
8
|
+
if (!data)
|
|
9
|
+
return { sceneCount: 0 };
|
|
10
|
+
if (Array.isArray(data))
|
|
11
|
+
return { sceneCount: data.length, scenes: data.slice(0, 20) };
|
|
12
|
+
if (typeof data !== 'object' || data === null)
|
|
13
|
+
return { rawType: typeof data };
|
|
14
|
+
const obj = data;
|
|
15
|
+
for (const k of ['scenes', 'list', 'content', 'records']) {
|
|
16
|
+
if (Array.isArray(obj[k])) {
|
|
17
|
+
const arr = obj[k];
|
|
18
|
+
return { sceneCount: arr.length, scenes: arr.slice(0, 20) };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { keys: Object.keys(obj).slice(0, 30) };
|
|
22
|
+
}
|
|
23
|
+
/** brief 全量可能很长,体检只保留常见关键指标。 */
|
|
24
|
+
function pickImportantBrief(brief) {
|
|
25
|
+
const prefer = [
|
|
26
|
+
'fps_mean',
|
|
27
|
+
'fps',
|
|
28
|
+
'FPS均值',
|
|
29
|
+
'jank_rate',
|
|
30
|
+
'cpu_freq_mean',
|
|
31
|
+
'Total_Reserved_Memory_Bytes_maximum',
|
|
32
|
+
'Reserved Total峰值',
|
|
33
|
+
'lua_total_memory_maximum',
|
|
34
|
+
];
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const k of prefer)
|
|
37
|
+
if (brief[k] !== undefined)
|
|
38
|
+
out[k] = brief[k];
|
|
39
|
+
let n = 0;
|
|
40
|
+
for (const [k, v] of Object.entries(brief)) {
|
|
41
|
+
if (k in out)
|
|
42
|
+
continue;
|
|
43
|
+
out[k] = v;
|
|
44
|
+
if (++n >= 12)
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
export const reportDiagnosisTool = {
|
|
50
|
+
id: 'report_diagnosis',
|
|
51
|
+
title: '报告体检',
|
|
52
|
+
engines: ['unity', 'unreal'],
|
|
53
|
+
filterKeys: ['report_diagnosis', 'composite', 'preset.default', 'overview'],
|
|
54
|
+
description: [
|
|
55
|
+
'对单份 GOT Online 报告做一次结构化体检:自动选对 Overview 统计 v1/v2,并串联 Top 资源、Top 函数与场景摘要。',
|
|
56
|
+
'适用引擎:Unity / UE|复合工具',
|
|
57
|
+
'调用方不必自己判断 2026-07-09 / 2026-03-25 分界或 SDK 版本——内部按 get_report_detail 路由。',
|
|
58
|
+
'Top 函数来自堆栈树解析合并,不是统计接口里的抽样指标。',
|
|
59
|
+
'返回摘要而非原始大包;需要明细时再按 _apisUsed 中的原子工具深挖。',
|
|
60
|
+
].join('\n'),
|
|
61
|
+
inputSchema: {
|
|
62
|
+
dataKey: z.string().optional().describe('报告 dataKey,与 recordId 二选一'),
|
|
63
|
+
recordId: z.union([z.string(), z.number()]).optional().describe('报告 recordId,与 dataKey 二选一'),
|
|
64
|
+
},
|
|
65
|
+
async handler(args, ctx) {
|
|
66
|
+
try {
|
|
67
|
+
const key = requireReportKey(args);
|
|
68
|
+
const apisUsed = [];
|
|
69
|
+
const limitations = [];
|
|
70
|
+
const identity = await fetchReportIdentity(ctx.client, key);
|
|
71
|
+
apisUsed.push('get_report_detail');
|
|
72
|
+
const { target, data } = await fetchOverviewStatistic(ctx.client, identity);
|
|
73
|
+
apisUsed.push(target.id);
|
|
74
|
+
const view = viewOverview(data);
|
|
75
|
+
const hasRes = view.hasResource === true;
|
|
76
|
+
let topResources = null;
|
|
77
|
+
try {
|
|
78
|
+
topResources = await runTopResources(ctx.client, { ...key, topN: 10 });
|
|
79
|
+
const used = topResources['_apisUsed'];
|
|
80
|
+
if (Array.isArray(used))
|
|
81
|
+
for (const u of used)
|
|
82
|
+
if (!apisUsed.includes(String(u)))
|
|
83
|
+
apisUsed.push(String(u));
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
limitations.push(`top_resources 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
87
|
+
}
|
|
88
|
+
let topFunctions = null;
|
|
89
|
+
try {
|
|
90
|
+
const subtype = identity.serviceSubtype === 'mono' ? 'mono' : 'overview';
|
|
91
|
+
topFunctions = await runTopFunctions(ctx.client, { ...key, topN: 10, serviceSubtype: subtype });
|
|
92
|
+
const used = topFunctions['_apisUsed'];
|
|
93
|
+
if (Array.isArray(used))
|
|
94
|
+
for (const u of used)
|
|
95
|
+
if (!apisUsed.includes(String(u)))
|
|
96
|
+
apisUsed.push(String(u));
|
|
97
|
+
if (Array.isArray(topFunctions['_limitations'])) {
|
|
98
|
+
for (const lim of topFunctions['_limitations'])
|
|
99
|
+
limitations.push(String(lim));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
limitations.push(`top_functions 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
104
|
+
}
|
|
105
|
+
let scenes = null;
|
|
106
|
+
try {
|
|
107
|
+
const scene = await fetchSceneStatistic(ctx.client, identity);
|
|
108
|
+
if (scene) {
|
|
109
|
+
scenes = summarizeScenes(scene.data);
|
|
110
|
+
apisUsed.push(scene.target.id);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
limitations.push('当前引擎无 Unity 场景统计路由,已跳过场景摘要');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
limitations.push(`场景统计失败:${e instanceof Error ? e.message : String(e)}`);
|
|
118
|
+
}
|
|
119
|
+
if (!hasRes)
|
|
120
|
+
limitations.push('未开启资源采集或无法从 brief 判定,逐资源 Top 可能为空');
|
|
121
|
+
const result = {
|
|
122
|
+
report: {
|
|
123
|
+
dataKey: identity.dataKey,
|
|
124
|
+
recordId: identity.recordId,
|
|
125
|
+
recordName: identity.recordName,
|
|
126
|
+
engine: identity.engine,
|
|
127
|
+
createDate: identity.createDate,
|
|
128
|
+
sdkVersion: identity.sdkVersion,
|
|
129
|
+
serviceSubtype: identity.serviceSubtype,
|
|
130
|
+
},
|
|
131
|
+
routedOverview: target.id,
|
|
132
|
+
overviewShape: view.shape,
|
|
133
|
+
summary: view.summary,
|
|
134
|
+
brief: pickImportantBrief(view.brief),
|
|
135
|
+
topResources: topResources
|
|
136
|
+
? { items: topResources['items'], note: topResources['_note'] }
|
|
137
|
+
: null,
|
|
138
|
+
topFunctions: topFunctions
|
|
139
|
+
? {
|
|
140
|
+
items: topFunctions['items'],
|
|
141
|
+
metric: topFunctions['metric'],
|
|
142
|
+
uniqueFunctions: topFunctions['uniqueFunctions'],
|
|
143
|
+
note: topFunctions['_note'],
|
|
144
|
+
}
|
|
145
|
+
: null,
|
|
146
|
+
scenes,
|
|
147
|
+
_apisUsed: apisUsed,
|
|
148
|
+
_limitations: limitations,
|
|
149
|
+
_note: '结构化体检摘要。细节请按 _apisUsed 调用对应原子工具。',
|
|
150
|
+
};
|
|
151
|
+
return jsonToolResult(result, ctx.maxChars);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return errorResult(err);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { UwaClient } from '../client.js';
|
|
2
|
+
import type { Engine } from '../spec.js';
|
|
3
|
+
/** Unity Overview / 场景统计 2.0 分界 */
|
|
4
|
+
export declare const UNITY_V2_DAY = "2026-07-09";
|
|
5
|
+
/** UE Overview 2.0 分界(v1 含当日及以前,v2 为严格之后) */
|
|
6
|
+
export declare const UE_V2_DAY = "2026-03-25";
|
|
7
|
+
/** AT 资源总览列表可用分界 */
|
|
8
|
+
export declare const AT_RESOURCE_DAY = "2026-06-25";
|
|
9
|
+
export declare const UNITY_V2_SDK = "2.5.1";
|
|
10
|
+
export interface ReportIdentity {
|
|
11
|
+
dataKey?: string;
|
|
12
|
+
recordId?: string;
|
|
13
|
+
engine: Engine;
|
|
14
|
+
createDate: string | null;
|
|
15
|
+
sdkVersion: string | null;
|
|
16
|
+
serviceSubtype: string | null;
|
|
17
|
+
recordName: string | null;
|
|
18
|
+
raw: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export type OverviewTarget = {
|
|
21
|
+
id: 'get_overview_statistic_v1';
|
|
22
|
+
method: 'GET';
|
|
23
|
+
path: '/openapi/v1/data/gotonline/overview/report';
|
|
24
|
+
apiVersion: 'v1.0.1';
|
|
25
|
+
} | {
|
|
26
|
+
id: 'get_overview_statistic_v2';
|
|
27
|
+
method: 'POST';
|
|
28
|
+
path: '/openapi/v1/data/gotonline/overview/statistic/batch';
|
|
29
|
+
apiVersion: 'v1.0.1';
|
|
30
|
+
} | {
|
|
31
|
+
id: 'get_ue_overview_statistic_v2';
|
|
32
|
+
method: 'GET';
|
|
33
|
+
path: '/openapi/v1/data/gotonline/ue/overview/report';
|
|
34
|
+
apiVersion: 'v1.0.1';
|
|
35
|
+
};
|
|
36
|
+
export type SceneTarget = {
|
|
37
|
+
id: 'get_scene_statistic_v1';
|
|
38
|
+
method: 'GET';
|
|
39
|
+
path: '/openapi/v1/data/gotonline/overview/scene';
|
|
40
|
+
apiVersion: 'v1.0.1';
|
|
41
|
+
} | {
|
|
42
|
+
id: 'get_scene_statistic_v2';
|
|
43
|
+
method: 'POST';
|
|
44
|
+
path: '/openapi/v1/data/gotonline/overview/report/scene/statistic/batch';
|
|
45
|
+
apiVersion: 'v1.0.1';
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* 拉取单份报告详情并规范化身份字段。
|
|
49
|
+
* get_report_detail 的 dataKeys 为逗号分隔;返回可能是数组或带 records 的对象。
|
|
50
|
+
*/
|
|
51
|
+
export declare function fetchReportIdentity(client: UwaClient, key: {
|
|
52
|
+
dataKey?: string;
|
|
53
|
+
recordId?: string;
|
|
54
|
+
}): Promise<ReportIdentity>;
|
|
55
|
+
/** 按引擎 + 日期 + SDK 选出 Overview 统计接口。 */
|
|
56
|
+
export declare function resolveOverviewTarget(identity: ReportIdentity): OverviewTarget;
|
|
57
|
+
/** Unity 场景统计路由;UE 无独立 scene v1/v2 对,返回 null。 */
|
|
58
|
+
export declare function resolveSceneTarget(identity: ReportIdentity): SceneTarget | null;
|
|
59
|
+
export declare function preferAtResourceTable(identity: ReportIdentity): boolean;
|
|
60
|
+
/** 调用已路由的 Overview 统计,返回原始 data + 路由元信息。 */
|
|
61
|
+
export declare function fetchOverviewStatistic(client: UwaClient, identity: ReportIdentity): Promise<{
|
|
62
|
+
target: OverviewTarget;
|
|
63
|
+
data: unknown;
|
|
64
|
+
}>;
|
|
65
|
+
export declare function fetchSceneStatistic(client: UwaClient, identity: ReportIdentity): Promise<{
|
|
66
|
+
target: SceneTarget;
|
|
67
|
+
data: unknown;
|
|
68
|
+
} | null>;
|
|
69
|
+
/** 从 Overview 统计结果里取出「第一份报告」的业务对象(v1/v2 结构略有差异)。 */
|
|
70
|
+
export declare function unwrapOverviewReport(data: unknown): Record<string, unknown> | null;
|
|
71
|
+
export declare function overviewHasResource(report: Record<string, unknown> | null): boolean;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { dayGte, dayLt, dayLte, dayOf, isObj, sdkAtLeast } from './helpers.js';
|
|
2
|
+
/** Unity Overview / 场景统计 2.0 分界 */
|
|
3
|
+
export const UNITY_V2_DAY = '2026-07-09';
|
|
4
|
+
/** UE Overview 2.0 分界(v1 含当日及以前,v2 为严格之后) */
|
|
5
|
+
export const UE_V2_DAY = '2026-03-25';
|
|
6
|
+
/** AT 资源总览列表可用分界 */
|
|
7
|
+
export const AT_RESOURCE_DAY = '2026-06-25';
|
|
8
|
+
export const UNITY_V2_SDK = '2.5.1';
|
|
9
|
+
function normalizeEngine(v) {
|
|
10
|
+
const s = String(v ?? '').toLowerCase();
|
|
11
|
+
if (s === 'unity' || s === '1')
|
|
12
|
+
return 'unity';
|
|
13
|
+
if (s === 'unreal' || s === 'ue' || s === '2' || s === '3')
|
|
14
|
+
return 'unreal';
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 拉取单份报告详情并规范化身份字段。
|
|
19
|
+
* get_report_detail 的 dataKeys 为逗号分隔;返回可能是数组或带 records 的对象。
|
|
20
|
+
*/
|
|
21
|
+
export async function fetchReportIdentity(client, key) {
|
|
22
|
+
if (!key.dataKey && !key.recordId)
|
|
23
|
+
throw new Error('必须提供 dataKey 或 recordId');
|
|
24
|
+
// 详情接口以 dataKeys 为主;只有 recordId 时先用 recordId 拼进查询(部分环境也认)
|
|
25
|
+
const query = {};
|
|
26
|
+
if (key.dataKey)
|
|
27
|
+
query['dataKeys'] = key.dataKey;
|
|
28
|
+
else
|
|
29
|
+
query['dataKeys'] = key.recordId;
|
|
30
|
+
const data = await client.call('GET', '/openapi/v1/data/gotonline/records/detail', 'v1.0.1', query);
|
|
31
|
+
const record = pickFirstRecord(data);
|
|
32
|
+
if (!record)
|
|
33
|
+
throw new Error('get_report_detail 未返回报告详情,请确认 dataKey/recordId 是否正确');
|
|
34
|
+
const engine = normalizeEngine(record['engine']) ??
|
|
35
|
+
normalizeEngine(isObj(data) ? data['engine'] : null) ??
|
|
36
|
+
inferEngineFromLink(String(record['link'] ?? ''));
|
|
37
|
+
if (!engine)
|
|
38
|
+
throw new Error('无法从报告详情判断引擎(unity / unreal),请检查返回字段');
|
|
39
|
+
return {
|
|
40
|
+
dataKey: typeof record['dataKey'] === 'string' ? record['dataKey'] : key.dataKey,
|
|
41
|
+
recordId: record['recordId'] !== undefined && record['recordId'] !== null
|
|
42
|
+
? String(record['recordId'])
|
|
43
|
+
: key.recordId,
|
|
44
|
+
engine,
|
|
45
|
+
createDate: dayOf(String(record['createDate'] ?? record['date'] ?? '')),
|
|
46
|
+
sdkVersion: record['sdkVersion'] != null ? String(record['sdkVersion']) : null,
|
|
47
|
+
serviceSubtype: record['serviceSubtype'] != null ? String(record['serviceSubtype']) : null,
|
|
48
|
+
recordName: record['recordName'] != null ? String(record['recordName']) : null,
|
|
49
|
+
raw: record,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function pickFirstRecord(data) {
|
|
53
|
+
if (Array.isArray(data) && data.length && isObj(data[0]))
|
|
54
|
+
return data[0];
|
|
55
|
+
if (!isObj(data))
|
|
56
|
+
return null;
|
|
57
|
+
for (const k of ['records', 'list', 'content', 'data']) {
|
|
58
|
+
const v = data[k];
|
|
59
|
+
if (Array.isArray(v) && v.length && isObj(v[0]))
|
|
60
|
+
return v[0];
|
|
61
|
+
}
|
|
62
|
+
// 单条对象直接返回
|
|
63
|
+
if ('dataKey' in data || 'recordId' in data || 'serviceSubtype' in data)
|
|
64
|
+
return data;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function inferEngineFromLink(link) {
|
|
68
|
+
const m = /[?&]engine=(\d+)/.exec(link);
|
|
69
|
+
if (!m)
|
|
70
|
+
return null;
|
|
71
|
+
return normalizeEngine(m[1]);
|
|
72
|
+
}
|
|
73
|
+
/** 按引擎 + 日期 + SDK 选出 Overview 统计接口。 */
|
|
74
|
+
export function resolveOverviewTarget(identity) {
|
|
75
|
+
const day = identity.createDate;
|
|
76
|
+
if (identity.engine === 'unreal') {
|
|
77
|
+
if (day && dayLte(day, UE_V2_DAY)) {
|
|
78
|
+
return {
|
|
79
|
+
id: 'get_overview_statistic_v1',
|
|
80
|
+
method: 'GET',
|
|
81
|
+
path: '/openapi/v1/data/gotonline/overview/report',
|
|
82
|
+
apiVersion: 'v1.0.1',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
id: 'get_ue_overview_statistic_v2',
|
|
87
|
+
method: 'GET',
|
|
88
|
+
path: '/openapi/v1/data/gotonline/ue/overview/report',
|
|
89
|
+
apiVersion: 'v1.0.1',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Unity
|
|
93
|
+
const canV2 = day != null && dayGte(day, UNITY_V2_DAY) && sdkAtLeast(identity.sdkVersion, UNITY_V2_SDK);
|
|
94
|
+
if (canV2) {
|
|
95
|
+
return {
|
|
96
|
+
id: 'get_overview_statistic_v2',
|
|
97
|
+
method: 'POST',
|
|
98
|
+
path: '/openapi/v1/data/gotonline/overview/statistic/batch',
|
|
99
|
+
apiVersion: 'v1.0.1',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
id: 'get_overview_statistic_v1',
|
|
104
|
+
method: 'GET',
|
|
105
|
+
path: '/openapi/v1/data/gotonline/overview/report',
|
|
106
|
+
apiVersion: 'v1.0.1',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Unity 场景统计路由;UE 无独立 scene v1/v2 对,返回 null。 */
|
|
110
|
+
export function resolveSceneTarget(identity) {
|
|
111
|
+
if (identity.engine !== 'unity')
|
|
112
|
+
return null;
|
|
113
|
+
const day = identity.createDate;
|
|
114
|
+
const canV2 = day != null && dayGte(day, UNITY_V2_DAY) && sdkAtLeast(identity.sdkVersion, UNITY_V2_SDK);
|
|
115
|
+
if (canV2) {
|
|
116
|
+
return {
|
|
117
|
+
id: 'get_scene_statistic_v2',
|
|
118
|
+
method: 'POST',
|
|
119
|
+
path: '/openapi/v1/data/gotonline/overview/report/scene/statistic/batch',
|
|
120
|
+
apiVersion: 'v1.0.1',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (day != null && dayLt(day, UNITY_V2_DAY)) {
|
|
124
|
+
return {
|
|
125
|
+
id: 'get_scene_statistic_v1',
|
|
126
|
+
method: 'GET',
|
|
127
|
+
path: '/openapi/v1/data/gotonline/overview/scene',
|
|
128
|
+
apiVersion: 'v1.0.1',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// 日期未知时偏保守走 v1
|
|
132
|
+
return {
|
|
133
|
+
id: 'get_scene_statistic_v1',
|
|
134
|
+
method: 'GET',
|
|
135
|
+
path: '/openapi/v1/data/gotonline/overview/scene',
|
|
136
|
+
apiVersion: 'v1.0.1',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function preferAtResourceTable(identity) {
|
|
140
|
+
return identity.engine === 'unity' && identity.createDate != null && dayGte(identity.createDate, AT_RESOURCE_DAY);
|
|
141
|
+
}
|
|
142
|
+
/** 调用已路由的 Overview 统计,返回原始 data + 路由元信息。 */
|
|
143
|
+
export async function fetchOverviewStatistic(client, identity) {
|
|
144
|
+
const target = resolveOverviewTarget(identity);
|
|
145
|
+
const key = identity.dataKey ?? identity.recordId;
|
|
146
|
+
if (!key)
|
|
147
|
+
throw new Error('报告缺少 dataKey/recordId,无法拉统计');
|
|
148
|
+
let data;
|
|
149
|
+
if (target.method === 'POST') {
|
|
150
|
+
data = await client.call(target.method, target.path, target.apiVersion, {}, { dataKeys: [key] });
|
|
151
|
+
}
|
|
152
|
+
else if (identity.dataKey) {
|
|
153
|
+
data = await client.call(target.method, target.path, target.apiVersion, { dataKeys: identity.dataKey });
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
data = await client.call(target.method, target.path, target.apiVersion, {
|
|
157
|
+
recordIds: identity.recordId,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return { target, data };
|
|
161
|
+
}
|
|
162
|
+
export async function fetchSceneStatistic(client, identity) {
|
|
163
|
+
const target = resolveSceneTarget(identity);
|
|
164
|
+
if (!target)
|
|
165
|
+
return null;
|
|
166
|
+
const key = identity.dataKey ?? identity.recordId;
|
|
167
|
+
if (!key)
|
|
168
|
+
return null;
|
|
169
|
+
let data;
|
|
170
|
+
if (target.method === 'POST') {
|
|
171
|
+
data = await client.call(target.method, target.path, target.apiVersion, {}, { dataKey: key });
|
|
172
|
+
}
|
|
173
|
+
else if (identity.dataKey) {
|
|
174
|
+
data = await client.call(target.method, target.path, target.apiVersion, { dataKey: identity.dataKey });
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
data = await client.call(target.method, target.path, target.apiVersion, {
|
|
178
|
+
recordId: identity.recordId,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return { target, data };
|
|
182
|
+
}
|
|
183
|
+
/** 从 Overview 统计结果里取出「第一份报告」的业务对象(v1/v2 结构略有差异)。 */
|
|
184
|
+
export function unwrapOverviewReport(data) {
|
|
185
|
+
if (!data)
|
|
186
|
+
return null;
|
|
187
|
+
if (Array.isArray(data) && data.length && isObj(data[0]))
|
|
188
|
+
return data[0];
|
|
189
|
+
if (!isObj(data))
|
|
190
|
+
return null;
|
|
191
|
+
for (const k of ['reports', 'list', 'content', 'data', 'records']) {
|
|
192
|
+
const v = data[k];
|
|
193
|
+
if (Array.isArray(v) && v.length && isObj(v[0]))
|
|
194
|
+
return v[0];
|
|
195
|
+
}
|
|
196
|
+
// v2 batch 有时直接是 { [dataKey]: {...} }
|
|
197
|
+
const values = Object.values(data);
|
|
198
|
+
if (values.length === 1 && isObj(values[0]))
|
|
199
|
+
return values[0];
|
|
200
|
+
if ('brief' in data || 'func' in data || 'summary' in data || 'asset_stats' in data)
|
|
201
|
+
return data;
|
|
202
|
+
return data;
|
|
203
|
+
}
|
|
204
|
+
export function overviewHasResource(report) {
|
|
205
|
+
if (!report)
|
|
206
|
+
return false;
|
|
207
|
+
const brief = report['brief'];
|
|
208
|
+
if (isObj(brief) && brief['OverviewHasResource'] != null) {
|
|
209
|
+
const v = brief['OverviewHasResource'];
|
|
210
|
+
const n = Array.isArray(v) ? Number(v[0]) : Number(v);
|
|
211
|
+
return Number.isFinite(n) && n !== 0;
|
|
212
|
+
}
|
|
213
|
+
// summary 里可能是 label/value 数组
|
|
214
|
+
const summary = report['summary'];
|
|
215
|
+
if (Array.isArray(summary)) {
|
|
216
|
+
for (const row of summary) {
|
|
217
|
+
if (!isObj(row))
|
|
218
|
+
continue;
|
|
219
|
+
const label = String(row['label'] ?? row['key'] ?? '');
|
|
220
|
+
if (/OverviewHasResource/i.test(label) || label === 'Resource') {
|
|
221
|
+
const val = row['value'] ?? row['data'];
|
|
222
|
+
const first = Array.isArray(val) ? val[0] : val;
|
|
223
|
+
const n = Number(first);
|
|
224
|
+
if (Number.isFinite(n))
|
|
225
|
+
return n !== 0;
|
|
226
|
+
if (String(first) === '1' || String(first) === '2')
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { UwaClient } from '../client.js';
|
|
2
|
+
import type { ReportIdentity } from './route-overview.js';
|
|
3
|
+
export type StackMetric = 'selfTime' | 'totalTime' | 'callCount' | 'selfMemory';
|
|
4
|
+
export interface StackAggRow {
|
|
5
|
+
methodId: string;
|
|
6
|
+
name: string;
|
|
7
|
+
/** 自身耗时合计(微秒);CPU 类模式 */
|
|
8
|
+
selfTimeUs: number;
|
|
9
|
+
/** 总耗时合计(微秒);仅供参考,同名合并时可能有父子重叠语义问题 */
|
|
10
|
+
totalTimeUs: number;
|
|
11
|
+
callCount: number;
|
|
12
|
+
callFrameCount: number;
|
|
13
|
+
/** 自身内存合计(KB);Lua 内存类模式 */
|
|
14
|
+
selfMemoryKb: number;
|
|
15
|
+
totalMemoryKb: number;
|
|
16
|
+
/** 该函数在树中出现的节点数(合并前) */
|
|
17
|
+
nodeCount: number;
|
|
18
|
+
}
|
|
19
|
+
export interface StackAggResult {
|
|
20
|
+
testMode: string | null;
|
|
21
|
+
metric: StackMetric;
|
|
22
|
+
unit: string;
|
|
23
|
+
rows: Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
methodId: string;
|
|
26
|
+
value: number;
|
|
27
|
+
unit: string;
|
|
28
|
+
selfTimeMs?: number;
|
|
29
|
+
totalTimeMs?: number;
|
|
30
|
+
selfMemoryKb?: number;
|
|
31
|
+
callCount: number;
|
|
32
|
+
callFrameCount: number;
|
|
33
|
+
nodeCount: number;
|
|
34
|
+
source: string;
|
|
35
|
+
}>;
|
|
36
|
+
totalNodes: number;
|
|
37
|
+
uniqueFunctions: number;
|
|
38
|
+
apisUsed: string[];
|
|
39
|
+
}
|
|
40
|
+
interface TreeNode {
|
|
41
|
+
depth: number;
|
|
42
|
+
sampleId: string;
|
|
43
|
+
methodId: string;
|
|
44
|
+
totalTimeUs: number;
|
|
45
|
+
selfTimeUs: number;
|
|
46
|
+
callCount: number;
|
|
47
|
+
callFrameCount: number;
|
|
48
|
+
totalMemoryKb: number;
|
|
49
|
+
selfMemoryKb: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Overview:线程堆栈树(主线程)+ 函数 idmap → 按函数名合并节点。
|
|
53
|
+
* 同名(同一 methodId)节点的 selfTime / callCount 相加,再按指标排序取 Top N。
|
|
54
|
+
*/
|
|
55
|
+
export declare function aggregateOverviewStack(client: UwaClient, identity: ReportIdentity, topN: number, metric?: StackMetric): Promise<StackAggResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Mono:正向堆栈树 + Mono 函数 idmap。
|
|
58
|
+
* 文本格式与 Overview CSV 不同时走宽松解析;失败则抛错由上层处理。
|
|
59
|
+
*/
|
|
60
|
+
export declare function aggregateMonoStack(client: UwaClient, identity: ReportIdentity, topN: number, metric?: StackMetric): Promise<StackAggResult>;
|
|
61
|
+
/**
|
|
62
|
+
* 解析无表头 CSV。列数随 testMode 变化;UE 固定 7 列(与 CPU_ONLY 同形)。
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseStackCsv(csvText: string, testMode: string | null): TreeNode[];
|
|
65
|
+
export {};
|