@uwa4d/openapi-mcp 0.2.0-beta.1 → 0.2.0-beta.10
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
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { z } from './types.js';
|
|
2
|
+
import { errorResult, isObj, 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
|
+
import { extractPssPeakCard } from '../annotate-dashboard.js';
|
|
8
|
+
function summarizeScenes(data) {
|
|
9
|
+
if (!data)
|
|
10
|
+
return { sceneCount: 0 };
|
|
11
|
+
if (Array.isArray(data))
|
|
12
|
+
return { sceneCount: data.length, scenes: data.slice(0, 20) };
|
|
13
|
+
if (typeof data !== 'object' || data === null)
|
|
14
|
+
return { rawType: typeof data };
|
|
15
|
+
const obj = data;
|
|
16
|
+
for (const k of ['scenes', 'list', 'content', 'records']) {
|
|
17
|
+
if (Array.isArray(obj[k])) {
|
|
18
|
+
const arr = obj[k];
|
|
19
|
+
return { sceneCount: arr.length, scenes: arr.slice(0, 20) };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { keys: Object.keys(obj).slice(0, 30) };
|
|
23
|
+
}
|
|
24
|
+
/** list_scenes 返回起止帧;归一成 sceneIdx + sceneName + frameStart/End。 */
|
|
25
|
+
function normalizeSceneList(data) {
|
|
26
|
+
const arr = Array.isArray(data)
|
|
27
|
+
? data
|
|
28
|
+
: data && typeof data === 'object' && Array.isArray(data['scenes'])
|
|
29
|
+
? data['scenes']
|
|
30
|
+
: [];
|
|
31
|
+
return arr.slice(0, 50).map((item, idx) => {
|
|
32
|
+
if (!item || typeof item !== 'object')
|
|
33
|
+
return { sceneIdx: idx, raw: item };
|
|
34
|
+
const o = item;
|
|
35
|
+
return {
|
|
36
|
+
sceneIdx: idx,
|
|
37
|
+
sceneName: o['sceneName'] ?? o['name'] ?? null,
|
|
38
|
+
frameStart: o['frameStart'] ?? o['startFrame'] ?? null,
|
|
39
|
+
frameEnd: o['frameEnd'] ?? o['endFrame'] ?? null,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** brief 全量可能很长,体检只保留常见关键指标(含 Unity key 与 UE 中文 label)。 */
|
|
44
|
+
function pickImportantBrief(brief) {
|
|
45
|
+
const preferExact = [
|
|
46
|
+
'fps_mean',
|
|
47
|
+
'fps',
|
|
48
|
+
'FPS均值',
|
|
49
|
+
'FPS均值(帧/秒)',
|
|
50
|
+
'jank_rate',
|
|
51
|
+
'Jank均值',
|
|
52
|
+
'Jank均值(次/分钟)',
|
|
53
|
+
'cpu_freq_mean',
|
|
54
|
+
'Total_Reserved_Memory_Bytes_maximum',
|
|
55
|
+
'Reserved Total峰值',
|
|
56
|
+
'设备内存峰值(MB)',
|
|
57
|
+
'lua_total_memory_maximum',
|
|
58
|
+
];
|
|
59
|
+
const preferLoose = [/fps/i, /jank/i, /设备内存/, /reserved.*memory/i, /卡顿/];
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const k of preferExact)
|
|
62
|
+
if (brief[k] !== undefined)
|
|
63
|
+
out[k] = brief[k];
|
|
64
|
+
for (const [k, v] of Object.entries(brief)) {
|
|
65
|
+
if (k in out)
|
|
66
|
+
continue;
|
|
67
|
+
if (preferLoose.some((re) => re.test(k)))
|
|
68
|
+
out[k] = v;
|
|
69
|
+
}
|
|
70
|
+
let n = 0;
|
|
71
|
+
for (const [k, v] of Object.entries(brief)) {
|
|
72
|
+
if (k in out)
|
|
73
|
+
continue;
|
|
74
|
+
out[k] = v;
|
|
75
|
+
if (++n >= 12)
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function numOf(v) {
|
|
81
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
82
|
+
return v;
|
|
83
|
+
if (Array.isArray(v) && typeof v[0] === 'number')
|
|
84
|
+
return v[0];
|
|
85
|
+
if (typeof v === 'string' && v.trim() && Number.isFinite(Number(v)))
|
|
86
|
+
return Number(v);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/** 从 overview brief/summary 尽量抽出 PSS 峰值答题卡。 */
|
|
90
|
+
function extractPssFromView(brief, summary) {
|
|
91
|
+
const sources = [brief, summary];
|
|
92
|
+
for (const src of sources) {
|
|
93
|
+
const peak = numOf(src['android_memory_pss_maximum']) ??
|
|
94
|
+
numOf(src['android_memory_pss@max']) ??
|
|
95
|
+
numOf(src['PSS峰值']) ??
|
|
96
|
+
numOf(src['设备内存峰值(MB)']);
|
|
97
|
+
const frame = numOf(src['android_memory_pss_maximum_frame']) ??
|
|
98
|
+
numOf(src['android_memory_pss@max_frame']) ??
|
|
99
|
+
null;
|
|
100
|
+
if (peak == null)
|
|
101
|
+
continue;
|
|
102
|
+
// brief 里设备内存峰值多为 MB;android_memory_pss 多为 KB
|
|
103
|
+
const looksMb = peak < 50_000 && (src['设备内存峰值(MB)'] !== undefined || peak < 2048);
|
|
104
|
+
const peakKb = looksMb ? Math.round(peak * 1024) : peak;
|
|
105
|
+
return {
|
|
106
|
+
pss_peak_kb: peakKb,
|
|
107
|
+
pss_peak_mb: Math.round((peakKb / 1024) * 100) / 100,
|
|
108
|
+
pss_peak_frame: frame,
|
|
109
|
+
pss_unit: 'KB',
|
|
110
|
+
_pssHint: 'Android PSS 峰值请优先读 pss_peak_kb/mb/frame;合法面板名 android_memory_pss@max(不是 android_pss_max)。',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/** 优先走 indicator statistic 拿完整 PSS 峰值+帧;失败再回退 overview brief。 */
|
|
116
|
+
async function resolvePssPeakCard(client, identity, brief, summary, apisUsed, limitations) {
|
|
117
|
+
const keyQuery = {
|
|
118
|
+
indicatorDashboards: 'android_memory_pss@max',
|
|
119
|
+
};
|
|
120
|
+
if (identity.dataKey)
|
|
121
|
+
keyQuery['dataKey'] = identity.dataKey;
|
|
122
|
+
else if (identity.recordId != null)
|
|
123
|
+
keyQuery['recordId'] = identity.recordId;
|
|
124
|
+
else
|
|
125
|
+
return extractPssFromView(brief, summary);
|
|
126
|
+
try {
|
|
127
|
+
const raw = await client.call('GET', '/openapi/v1/data/gotonline/overview/indicator/statistic/dashboard', 'v1.0.1', keyQuery);
|
|
128
|
+
apisUsed.push('gotonline_overview_indicator_statistic_dashboard');
|
|
129
|
+
const card = extractPssPeakCard(raw);
|
|
130
|
+
if (card)
|
|
131
|
+
return card;
|
|
132
|
+
limitations.push('indicator PSS 面板无峰值字段,已回退 overview 摘要');
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
limitations.push(`PSS 面板查询失败,已回退 overview 摘要:${e instanceof Error ? e.message : String(e)}`);
|
|
136
|
+
}
|
|
137
|
+
return extractPssFromView(brief, summary);
|
|
138
|
+
}
|
|
139
|
+
function isWaitForVsyncName(name) {
|
|
140
|
+
return /wait\s*for\s*vsync|WaitForVsync|Gfx\.WaitForPresent/i.test(String(name ?? ''));
|
|
141
|
+
}
|
|
142
|
+
function buildBottleneckCandidates(topFunctions, gpuBoundEmpty) {
|
|
143
|
+
const items = topFunctions && Array.isArray(topFunctions['items']) ? topFunctions['items'] : [];
|
|
144
|
+
const out = [];
|
|
145
|
+
for (const raw of items.slice(0, 8)) {
|
|
146
|
+
if (!isObj(raw))
|
|
147
|
+
continue;
|
|
148
|
+
const name = raw['name'] ?? raw['methodName'] ?? raw['stackMethodName'];
|
|
149
|
+
const wait = isWaitForVsyncName(name);
|
|
150
|
+
out.push({
|
|
151
|
+
name,
|
|
152
|
+
selfTimeMean: raw['selfTimeMean'] ?? raw['value'] ?? null,
|
|
153
|
+
source: 'top_functions',
|
|
154
|
+
role: wait ? 'frame_sync_wait' : 'cpu_candidate',
|
|
155
|
+
note: wait
|
|
156
|
+
? 'WaitForVsync/Present 等待是帧同步表现,不能直接当卡顿根因或 GPU Bound 证据'
|
|
157
|
+
: '需结合 stutter 合并树确认是否为卡顿根因',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (gpuBoundEmpty === true) {
|
|
161
|
+
out.push({
|
|
162
|
+
name: 'GPU Bound',
|
|
163
|
+
source: 'gotonline_gpu_bound',
|
|
164
|
+
role: 'not_detected',
|
|
165
|
+
note: 'targetFps=30 下未检出 GPU Bound 区间',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
export const reportDiagnosisTool = {
|
|
171
|
+
id: 'report_diagnosis',
|
|
172
|
+
title: '报告体检',
|
|
173
|
+
engines: ['unity', 'unreal'],
|
|
174
|
+
filterKeys: ['report_diagnosis', 'composite', 'preset.default', 'overview'],
|
|
175
|
+
description: [
|
|
176
|
+
'对单份 GOT Online 报告做一次结构化体检:自动选对 Overview 统计 v1/v2,并串联 Top 资源、Top 函数、场景摘要与 GPU Bound@30。',
|
|
177
|
+
'适用引擎:Unity / UE|复合工具',
|
|
178
|
+
'调用方不必自己判断 2026-07-09 / 2026-03-25 分界或 SDK 版本——内部按 get_report_detail 路由。',
|
|
179
|
+
'返回答题卡:forbidden_claims、bottleneck_candidates(与 topFunctions 分栏)、gpuBound、pss_peak_*、comparability_note。',
|
|
180
|
+
'卡顿根因:优先 stutter_full_tree / stutter_scene_tree;禁止把 WaitForVsync 写成根因或 GPU 主瓶颈。',
|
|
181
|
+
'gpuBound 为空时只能说「未检出 Bound」,禁止断言 GPU 是主要瓶颈。',
|
|
182
|
+
'查面板统计/曲线前先调 gotonline_overview_indicator_dashboard_keys(勿把 fps_mean 等统计字段当面板名)。',
|
|
183
|
+
'scenes 含 list_scenes 起止帧(sceneIdx/sceneName/frameStart/frameEnd)。',
|
|
184
|
+
'返回摘要而非原始大包;需要明细时再按 _apisUsed 中的原子工具深挖。',
|
|
185
|
+
].join('\n'),
|
|
186
|
+
inputSchema: {
|
|
187
|
+
dataKey: z.string().optional().describe('报告 dataKey,与 recordId 二选一'),
|
|
188
|
+
recordId: z.union([z.string(), z.number()]).optional().describe('报告 recordId,与 dataKey 二选一'),
|
|
189
|
+
},
|
|
190
|
+
async handler(args, ctx) {
|
|
191
|
+
try {
|
|
192
|
+
const key = requireReportKey(args);
|
|
193
|
+
const apisUsed = [];
|
|
194
|
+
const limitations = [];
|
|
195
|
+
const identity = await fetchReportIdentity(ctx.client, key);
|
|
196
|
+
apisUsed.push('get_report_detail');
|
|
197
|
+
const { target, data } = await fetchOverviewStatistic(ctx.client, identity);
|
|
198
|
+
apisUsed.push(target.id);
|
|
199
|
+
const view = viewOverview(data);
|
|
200
|
+
let topResources = null;
|
|
201
|
+
try {
|
|
202
|
+
topResources = await runTopResources(ctx.client, { ...key, topN: 10 });
|
|
203
|
+
const used = topResources['_apisUsed'];
|
|
204
|
+
if (Array.isArray(used))
|
|
205
|
+
for (const u of used)
|
|
206
|
+
if (!apisUsed.includes(String(u)))
|
|
207
|
+
apisUsed.push(String(u));
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
limitations.push(`top_resources 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
211
|
+
}
|
|
212
|
+
let topFunctions = null;
|
|
213
|
+
try {
|
|
214
|
+
const subtype = identity.serviceSubtype === 'mono' ? 'mono' : 'overview';
|
|
215
|
+
topFunctions = await runTopFunctions(ctx.client, { ...key, topN: 10, serviceSubtype: subtype });
|
|
216
|
+
const used = topFunctions['_apisUsed'];
|
|
217
|
+
if (Array.isArray(used))
|
|
218
|
+
for (const u of used)
|
|
219
|
+
if (!apisUsed.includes(String(u)))
|
|
220
|
+
apisUsed.push(String(u));
|
|
221
|
+
if (Array.isArray(topFunctions['_limitations'])) {
|
|
222
|
+
for (const lim of topFunctions['_limitations'])
|
|
223
|
+
limitations.push(String(lim));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
limitations.push(`top_functions 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
228
|
+
}
|
|
229
|
+
let scenes = null;
|
|
230
|
+
try {
|
|
231
|
+
const scene = await fetchSceneStatistic(ctx.client, identity);
|
|
232
|
+
if (scene) {
|
|
233
|
+
scenes = summarizeScenes(scene.data);
|
|
234
|
+
apisUsed.push(scene.target.id);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
limitations.push('当前引擎无 Unity 场景统计路由,已跳过场景统计摘要');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
limitations.push(`场景统计失败:${e instanceof Error ? e.message : String(e)}`);
|
|
242
|
+
}
|
|
243
|
+
// list_scenes 补起止帧(场景统计摘要经常只有名称/索引)
|
|
244
|
+
try {
|
|
245
|
+
const keyQuery = {};
|
|
246
|
+
if (identity.dataKey)
|
|
247
|
+
keyQuery['dataKey'] = identity.dataKey;
|
|
248
|
+
else if (identity.recordId)
|
|
249
|
+
keyQuery['recordId'] = identity.recordId;
|
|
250
|
+
const sceneList = await ctx.client.call('GET', '/openapi/v1/data/gotonline/scene/info', 'v1.0.1', keyQuery);
|
|
251
|
+
const list = normalizeSceneList(sceneList);
|
|
252
|
+
if (list.length) {
|
|
253
|
+
scenes = { ...(scenes ?? {}), sceneCount: list.length, scenes: list };
|
|
254
|
+
if (!apisUsed.includes('list_scenes'))
|
|
255
|
+
apisUsed.push('list_scenes');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
limitations.push(`场景列表(list_scenes)失败:${e instanceof Error ? e.message : String(e)}`);
|
|
260
|
+
}
|
|
261
|
+
let gpuBound = null;
|
|
262
|
+
let gpuBoundEmpty = null;
|
|
263
|
+
try {
|
|
264
|
+
const boundQuery = { targetFps: 30 };
|
|
265
|
+
if (identity.dataKey)
|
|
266
|
+
boundQuery['dataKey'] = identity.dataKey;
|
|
267
|
+
else if (identity.recordId)
|
|
268
|
+
boundQuery['recordId'] = identity.recordId;
|
|
269
|
+
gpuBound = await ctx.client.call('GET', '/openapi/v1/data/gotonline/gpu/bound', 'v1.0.1', boundQuery);
|
|
270
|
+
apisUsed.push('gotonline_gpu_bound');
|
|
271
|
+
const intervals = Array.isArray(gpuBound)
|
|
272
|
+
? gpuBound
|
|
273
|
+
: isObj(gpuBound) && Array.isArray(gpuBound['data'])
|
|
274
|
+
? gpuBound['data']
|
|
275
|
+
: null;
|
|
276
|
+
gpuBoundEmpty = Array.isArray(intervals) ? intervals.length === 0 : null;
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
limitations.push(`gpu_bound 失败:${e instanceof Error ? e.message : String(e)}`);
|
|
280
|
+
}
|
|
281
|
+
// 仅在明确未开启时提示;hasResource === null 表示无法判定,不误报关闭
|
|
282
|
+
if (view.hasResource === false) {
|
|
283
|
+
limitations.push('未开启资源采集,逐资源 Top 可能为空');
|
|
284
|
+
}
|
|
285
|
+
const forbidden_claims = [];
|
|
286
|
+
if (gpuBoundEmpty === true) {
|
|
287
|
+
forbidden_claims.push('GPU是主要瓶颈', 'GPU Bound明显', 'GPU主瓶颈');
|
|
288
|
+
}
|
|
289
|
+
forbidden_claims.push('WaitForVsync是卡顿根因');
|
|
290
|
+
const pssCard = await resolvePssPeakCard(ctx.client, identity, view.brief, view.summary, apisUsed, limitations);
|
|
291
|
+
const sceneCount = isObj(scenes) ? Number(scenes['sceneCount'] ?? 0) : 0;
|
|
292
|
+
const comparability_note = [
|
|
293
|
+
'与其它报告对比时:请核对场景名/数量、测试时长、目标帧率、SDK/引擎版本是否一致。',
|
|
294
|
+
sceneCount > 0 ? `本报告场景数=${sceneCount}。` : '',
|
|
295
|
+
'仅比较 FPS 均值不能视为严格同场景对比。',
|
|
296
|
+
]
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.join(' ');
|
|
299
|
+
const result = {
|
|
300
|
+
report: {
|
|
301
|
+
dataKey: identity.dataKey,
|
|
302
|
+
recordId: identity.recordId,
|
|
303
|
+
recordName: identity.recordName,
|
|
304
|
+
engine: identity.engine,
|
|
305
|
+
createDate: identity.createDate,
|
|
306
|
+
sdkVersion: identity.sdkVersion,
|
|
307
|
+
serviceSubtype: identity.serviceSubtype,
|
|
308
|
+
},
|
|
309
|
+
routedOverview: target.id,
|
|
310
|
+
overviewShape: view.shape,
|
|
311
|
+
hasResource: view.hasResource,
|
|
312
|
+
summary: view.summary,
|
|
313
|
+
brief: pickImportantBrief(view.brief),
|
|
314
|
+
...(pssCard ?? {}),
|
|
315
|
+
gpuBound: Array.isArray(gpuBound) ? gpuBound : isObj(gpuBound) ? gpuBound['data'] ?? gpuBound : gpuBound,
|
|
316
|
+
gpuBoundEmpty,
|
|
317
|
+
forbidden_claims,
|
|
318
|
+
bottleneck_candidates: buildBottleneckCandidates(topFunctions, gpuBoundEmpty),
|
|
319
|
+
topResources: topResources
|
|
320
|
+
? {
|
|
321
|
+
items: topResources['items'],
|
|
322
|
+
byType: topResources['byType'],
|
|
323
|
+
note: topResources['_note'],
|
|
324
|
+
}
|
|
325
|
+
: null,
|
|
326
|
+
topFunctions: topFunctions
|
|
327
|
+
? {
|
|
328
|
+
items: topFunctions['items'],
|
|
329
|
+
metric: topFunctions['metric'],
|
|
330
|
+
uniqueFunctions: topFunctions['uniqueFunctions'],
|
|
331
|
+
note: topFunctions['_note'],
|
|
332
|
+
_columnNote: 'topFunctions 是耗时排行,不等于卡顿根因;根因看 stutter 合并树与 bottleneck_candidates。',
|
|
333
|
+
}
|
|
334
|
+
: null,
|
|
335
|
+
scenes,
|
|
336
|
+
comparability_note,
|
|
337
|
+
_apisUsed: apisUsed,
|
|
338
|
+
_limitations: limitations,
|
|
339
|
+
_note: '结构化体检摘要(含答题卡)。GPU Bound 空时禁止 GPU 主瓶颈结论;卡顿根因优先 stutter 树。细节请按 _apisUsed 调用对应原子工具。',
|
|
340
|
+
};
|
|
341
|
+
return jsonToolResult(result, ctx.maxChars);
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
return errorResult(err);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
/** Unity / UE 共用 AT 资源总览列表;解析日 ≥ 2026-06-25 时优先走这条。 */
|
|
60
|
+
export declare function preferAtResourceTable(identity: ReportIdentity): boolean;
|
|
61
|
+
/** 调用已路由的 Overview 统计,返回原始 data + 路由元信息。 */
|
|
62
|
+
export declare function fetchOverviewStatistic(client: UwaClient, identity: ReportIdentity): Promise<{
|
|
63
|
+
target: OverviewTarget;
|
|
64
|
+
data: unknown;
|
|
65
|
+
}>;
|
|
66
|
+
export declare function fetchSceneStatistic(client: UwaClient, identity: ReportIdentity): Promise<{
|
|
67
|
+
target: SceneTarget;
|
|
68
|
+
data: unknown;
|
|
69
|
+
} | null>;
|
|
70
|
+
/** 从 Overview 统计结果里取出「第一份报告」的业务对象(v1/v2 结构略有差异)。 */
|
|
71
|
+
export declare function unwrapOverviewReport(data: unknown): Record<string, unknown> | null;
|
|
72
|
+
export declare function overviewHasResource(report: Record<string, unknown> | null): boolean;
|
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
/** Unity / UE 共用 AT 资源总览列表;解析日 ≥ 2026-06-25 时优先走这条。 */
|
|
140
|
+
export function preferAtResourceTable(identity) {
|
|
141
|
+
return identity.createDate != null && dayGte(identity.createDate, AT_RESOURCE_DAY);
|
|
142
|
+
}
|
|
143
|
+
/** 调用已路由的 Overview 统计,返回原始 data + 路由元信息。 */
|
|
144
|
+
export async function fetchOverviewStatistic(client, identity) {
|
|
145
|
+
const target = resolveOverviewTarget(identity);
|
|
146
|
+
const key = identity.dataKey ?? identity.recordId;
|
|
147
|
+
if (!key)
|
|
148
|
+
throw new Error('报告缺少 dataKey/recordId,无法拉统计');
|
|
149
|
+
let data;
|
|
150
|
+
if (target.method === 'POST') {
|
|
151
|
+
data = await client.call(target.method, target.path, target.apiVersion, {}, { dataKeys: [key] });
|
|
152
|
+
}
|
|
153
|
+
else if (identity.dataKey) {
|
|
154
|
+
data = await client.call(target.method, target.path, target.apiVersion, { dataKeys: identity.dataKey });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
data = await client.call(target.method, target.path, target.apiVersion, {
|
|
158
|
+
recordIds: identity.recordId,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return { target, data };
|
|
162
|
+
}
|
|
163
|
+
export async function fetchSceneStatistic(client, identity) {
|
|
164
|
+
const target = resolveSceneTarget(identity);
|
|
165
|
+
if (!target)
|
|
166
|
+
return null;
|
|
167
|
+
const key = identity.dataKey ?? identity.recordId;
|
|
168
|
+
if (!key)
|
|
169
|
+
return null;
|
|
170
|
+
let data;
|
|
171
|
+
if (target.method === 'POST') {
|
|
172
|
+
data = await client.call(target.method, target.path, target.apiVersion, {}, { dataKey: key });
|
|
173
|
+
}
|
|
174
|
+
else if (identity.dataKey) {
|
|
175
|
+
data = await client.call(target.method, target.path, target.apiVersion, { dataKey: identity.dataKey });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
data = await client.call(target.method, target.path, target.apiVersion, {
|
|
179
|
+
recordId: identity.recordId,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return { target, data };
|
|
183
|
+
}
|
|
184
|
+
/** 从 Overview 统计结果里取出「第一份报告」的业务对象(v1/v2 结构略有差异)。 */
|
|
185
|
+
export function unwrapOverviewReport(data) {
|
|
186
|
+
if (!data)
|
|
187
|
+
return null;
|
|
188
|
+
if (Array.isArray(data) && data.length && isObj(data[0]))
|
|
189
|
+
return data[0];
|
|
190
|
+
if (!isObj(data))
|
|
191
|
+
return null;
|
|
192
|
+
for (const k of ['reports', 'list', 'content', 'data', 'records']) {
|
|
193
|
+
const v = data[k];
|
|
194
|
+
if (Array.isArray(v) && v.length && isObj(v[0]))
|
|
195
|
+
return v[0];
|
|
196
|
+
}
|
|
197
|
+
// v2 batch 有时直接是 { [dataKey]: {...} }
|
|
198
|
+
const values = Object.values(data);
|
|
199
|
+
if (values.length === 1 && isObj(values[0]))
|
|
200
|
+
return values[0];
|
|
201
|
+
if ('brief' in data || 'func' in data || 'summary' in data || 'asset_stats' in data)
|
|
202
|
+
return data;
|
|
203
|
+
return data;
|
|
204
|
+
}
|
|
205
|
+
export function overviewHasResource(report) {
|
|
206
|
+
if (!report)
|
|
207
|
+
return false;
|
|
208
|
+
const brief = report['brief'];
|
|
209
|
+
if (isObj(brief) && brief['OverviewHasResource'] != null) {
|
|
210
|
+
const v = brief['OverviewHasResource'];
|
|
211
|
+
const n = Array.isArray(v) ? Number(v[0]) : Number(v);
|
|
212
|
+
return Number.isFinite(n) && n !== 0;
|
|
213
|
+
}
|
|
214
|
+
// summary 里可能是 label/value 数组
|
|
215
|
+
const summary = report['summary'];
|
|
216
|
+
if (Array.isArray(summary)) {
|
|
217
|
+
for (const row of summary) {
|
|
218
|
+
if (!isObj(row))
|
|
219
|
+
continue;
|
|
220
|
+
const label = String(row['label'] ?? row['key'] ?? '');
|
|
221
|
+
if (/OverviewHasResource/i.test(label) || label === 'Resource') {
|
|
222
|
+
const val = row['value'] ?? row['data'];
|
|
223
|
+
const first = Array.isArray(val) ? val[0] : val;
|
|
224
|
+
const n = Number(first);
|
|
225
|
+
if (Number.isFinite(n))
|
|
226
|
+
return n !== 0;
|
|
227
|
+
if (String(first) === '1' || String(first) === '2')
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|