@uwa4d/openapi-mcp 0.2.0-beta.5 → 0.2.0-beta.7

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.
@@ -0,0 +1,8 @@
1
+ /** 按函数名过滤(子串或 /regex/flags),供 top_functions 本地过滤 idmap 结果。 */
2
+ export declare function matchesNamePattern(name: string, pattern?: unknown): boolean;
3
+ export declare function filterRowsByNamePattern<T extends {
4
+ name: string;
5
+ }>(rows: T[], pattern: unknown): {
6
+ rows: T[];
7
+ matchedTotal: number;
8
+ };
@@ -0,0 +1,25 @@
1
+ /** 按函数名过滤(子串或 /regex/flags),供 top_functions 本地过滤 idmap 结果。 */
2
+ export function matchesNamePattern(name, pattern) {
3
+ if (pattern == null || (typeof pattern === 'string' && !pattern.trim()))
4
+ return true;
5
+ const p = String(pattern).trim();
6
+ if (p.length >= 2 && p.startsWith('/') && p.lastIndexOf('/') > 0) {
7
+ const last = p.lastIndexOf('/');
8
+ const body = p.slice(1, last);
9
+ const flags = p.slice(last + 1);
10
+ try {
11
+ return new RegExp(body, flags).test(name);
12
+ }
13
+ catch {
14
+ return name.toLowerCase().includes(p.toLowerCase());
15
+ }
16
+ }
17
+ return name.toLowerCase().includes(p.toLowerCase());
18
+ }
19
+ export function filterRowsByNamePattern(rows, pattern) {
20
+ if (pattern == null || (typeof pattern === 'string' && !pattern.trim())) {
21
+ return { rows, matchedTotal: rows.length };
22
+ }
23
+ const matched = rows.filter((r) => matchesNamePattern(r.name, pattern));
24
+ return { rows: matched, matchedTotal: matched.length };
25
+ }
@@ -66,7 +66,8 @@ export const reportDiagnosisTool = {
66
66
  '对单份 GOT Online 报告做一次结构化体检:自动选对 Overview 统计 v1/v2,并串联 Top 资源、Top 函数与场景摘要。',
67
67
  '适用引擎:Unity / UE|复合工具',
68
68
  '调用方不必自己判断 2026-07-09 / 2026-03-25 分界或 SDK 版本——内部按 get_report_detail 路由。',
69
- 'Top 函数来自堆栈树解析合并,不是统计接口里的抽样指标。',
69
+ 'Top 函数:Overview 走 method/idmap/statistic v1.0.2 + stackTestMode;Mono 走堆栈树;逐帧曲线用 method/curve/presign v1.0.2。',
70
+ '查面板统计/曲线前先调 gotonline_overview_indicator_dashboard_keys 核对 indicatorDashboards(勿把 fps_mean 等统计字段当面板名)。',
70
71
  '返回摘要而非原始大包;需要明细时再按 _apisUsed 中的原子工具深挖。',
71
72
  ].join('\n'),
72
73
  inputSchema: {
@@ -1,5 +1,7 @@
1
1
  import type { UwaClient } from '../client.js';
2
2
  import type { ReportIdentity } from './route-overview.js';
3
+ import { type OverviewStackTestMode } from './stack-test-mode.js';
4
+ export type { OverviewStackTestMode, OverviewStackTestModeProfile } from './stack-test-mode.js';
3
5
  export type StackMetric = 'selfTime' | 'totalTime' | 'callCount' | 'selfMemory';
4
6
  export interface StackAggRow {
5
7
  methodId: string;
@@ -18,15 +20,25 @@ export interface StackAggRow {
18
20
  }
19
21
  export interface StackAggResult {
20
22
  testMode: string | null;
23
+ /** 仅 Overview:堆栈采集模式 CPU_ONLY / LUA_MEM_ONLY / CPU_AND_LUA_MEM */
24
+ stackTestMode?: OverviewStackTestMode | null;
25
+ availableMetrics?: StackMetric[];
26
+ availableFields?: string[];
27
+ metricGuide?: string;
21
28
  metric: StackMetric;
22
29
  unit: string;
23
30
  rows: Array<{
24
31
  name: string;
25
32
  methodId: string;
33
+ stackMethodId?: string;
26
34
  value: number;
27
35
  unit: string;
28
- selfTimeMs?: number;
29
- totalTimeMs?: number;
36
+ selfTimeMean?: number | null;
37
+ totalTimeMean?: number | null;
38
+ callCountTotal?: number | null;
39
+ selfMemoryMean?: number | null;
40
+ selfMemoryTotal?: number | null;
41
+ selfMemoryUnit?: string | null;
30
42
  selfMemoryKb?: number;
31
43
  callCount: number;
32
44
  callFrameCount: number;
@@ -48,6 +60,7 @@ interface TreeNode {
48
60
  totalMemoryKb: number;
49
61
  selfMemoryKb: number;
50
62
  }
63
+ export declare function aggregateOverviewStackFromStatistic(client: UwaClient, identity: ReportIdentity, topN: number, metricInput?: StackMetric, threadName?: string, namePattern?: string): Promise<StackAggResult>;
51
64
  /**
52
65
  * Overview:线程堆栈树(主线程)+ 函数 idmap → 按函数名合并节点。
53
66
  * 同名(同一 methodId)节点的 selfTime / callCount 相加,再按指标排序取 Top N。
@@ -62,4 +75,3 @@ export declare function aggregateMonoStack(client: UwaClient, identity: ReportId
62
75
  * 解析无表头 CSV。列数随 testMode 变化;UE 固定 7 列(与 CPU_ONLY 同形)。
63
76
  */
64
77
  export declare function parseStackCsv(csvText: string, testMode: string | null): TreeNode[];
65
- export {};
@@ -1,4 +1,155 @@
1
1
  import { isObj } from './helpers.js';
2
+ import { assertOverviewMetricSupported, inferOverviewStackTestMode, normalizeOverviewStackTestMode, OVERVIEW_STACK_TEST_MODE_PROFILES, resolveOverviewDefaultMetric, } from './stack-test-mode.js';
3
+ import { matchesNamePattern } from './name-pattern.js';
4
+ /**
5
+ * Overview:函数 idmap 统计(v1.0.2)+ Overview stackTestMode → Top N。
6
+ * stackTestMode 仅 Overview 有效;决定哪些统计字段/metric 可用。
7
+ */
8
+ function parseStatNumber(obj) {
9
+ if (!isObj(obj) || obj['value'] == null)
10
+ return null;
11
+ const n = Number(obj['value']);
12
+ return Number.isFinite(n) ? n : null;
13
+ }
14
+ function parseStatUnit(obj) {
15
+ if (!isObj(obj))
16
+ return null;
17
+ const unit = obj['unit'];
18
+ if (!isObj(unit) || unit['value'] == null)
19
+ return null;
20
+ return String(unit['value']);
21
+ }
22
+ async function fetchOverviewStackTestMode(client, identity) {
23
+ if (identity.engine === 'unreal') {
24
+ return { stackTestMode: 'CPU_ONLY', apisUsed: [] };
25
+ }
26
+ const keyQuery = identity.dataKey
27
+ ? { dataKey: identity.dataKey }
28
+ : { recordId: identity.recordId };
29
+ try {
30
+ const meta = (await client.call('GET', '/openapi/v1/data/gotonline/overview/stack/overall/tree/presign', 'v1.0.1', keyQuery));
31
+ return {
32
+ stackTestMode: normalizeOverviewStackTestMode(meta['testMode']),
33
+ apisUsed: ['gotonline_overview_stack_overall_tree_presign'],
34
+ };
35
+ }
36
+ catch {
37
+ return { stackTestMode: null, apisUsed: [] };
38
+ }
39
+ }
40
+ export async function aggregateOverviewStackFromStatistic(client, identity, topN, metricInput, threadName, namePattern) {
41
+ const keyQuery = identity.dataKey
42
+ ? { dataKey: identity.dataKey }
43
+ : { recordId: identity.recordId };
44
+ if (identity.engine === 'unreal') {
45
+ keyQuery['threadName'] = threadName?.trim() || 'GameThread';
46
+ }
47
+ const apisUsed = [];
48
+ const [{ stackTestMode: presignMode, apisUsed: presignApis }, raw] = await Promise.all([
49
+ fetchOverviewStackTestMode(client, identity),
50
+ client.call('GET', '/openapi/v1/data/gotonline/overview/method/idmap/statistic', 'v1.0.2', keyQuery),
51
+ ]);
52
+ apisUsed.push(...presignApis, 'gotonline_overview_method_idmap_statistic');
53
+ const list = Array.isArray(raw) ? raw : [];
54
+ if (!list.length) {
55
+ throw new Error('函数 idmap 统计返回空列表(报告可能未开堆栈或 SDK < 2.5.1)');
56
+ }
57
+ const rows = [];
58
+ for (const item of list) {
59
+ if (!isObj(item))
60
+ continue;
61
+ const methodId = item['stackMethodId'] != null ? String(item['stackMethodId']) : '';
62
+ const name = item['stackMethodName'] != null ? String(item['stackMethodName']) : methodId;
63
+ if (!methodId)
64
+ continue;
65
+ const memMeanObj = item['selfMemoryMean'];
66
+ const memTotalObj = item['selfMemoryTotal'];
67
+ const selfTimeMean = parseStatNumber(item['selfTimeMean']);
68
+ const totalTimeMean = parseStatNumber(item['totalTimeMean']);
69
+ const callCountTotal = parseStatNumber(item['callCountTotal']);
70
+ const selfMemoryMean = parseStatNumber(memMeanObj);
71
+ const selfMemoryTotal = parseStatNumber(memTotalObj);
72
+ const selfMemoryUnit = parseStatUnit(memMeanObj) ?? parseStatUnit(memTotalObj);
73
+ if (selfTimeMean == null &&
74
+ totalTimeMean == null &&
75
+ callCountTotal == null &&
76
+ selfMemoryMean == null &&
77
+ selfMemoryTotal == null) {
78
+ continue;
79
+ }
80
+ rows.push({
81
+ stackMethodId: methodId,
82
+ name,
83
+ selfTimeMean,
84
+ totalTimeMean,
85
+ callCountTotal,
86
+ selfMemoryMean,
87
+ selfMemoryTotal,
88
+ selfMemoryUnit,
89
+ });
90
+ }
91
+ if (!rows.length) {
92
+ throw new Error('函数 idmap 统计无有效数据');
93
+ }
94
+ const hasTime = rows.some((r) => r.selfTimeMean != null || r.totalTimeMean != null);
95
+ const hasMem = rows.some((r) => r.selfMemoryMean != null || r.selfMemoryTotal != null);
96
+ const stackTestMode = presignMode ?? inferOverviewStackTestMode({ hasTime, hasMem });
97
+ if (!stackTestMode) {
98
+ throw new Error('无法判断 Overview stackTestMode');
99
+ }
100
+ const profile = OVERVIEW_STACK_TEST_MODE_PROFILES[stackTestMode];
101
+ const metric = metricInput ?? resolveOverviewDefaultMetric(stackTestMode);
102
+ assertOverviewMetricSupported(metric, profile);
103
+ const metricValue = (r) => {
104
+ if (metric === 'totalTime')
105
+ return r.totalTimeMean != null ? r.totalTimeMean : -1;
106
+ if (metric === 'callCount')
107
+ return r.callCountTotal != null ? r.callCountTotal : -1;
108
+ if (metric === 'selfMemory') {
109
+ if (r.selfMemoryMean != null)
110
+ return r.selfMemoryMean;
111
+ if (r.selfMemoryTotal != null)
112
+ return r.selfMemoryTotal;
113
+ return -1;
114
+ }
115
+ return r.selfTimeMean != null ? r.selfTimeMean : -1;
116
+ };
117
+ const sorted = rows.sort((a, b) => metricValue(b) - metricValue(a));
118
+ const pattern = namePattern?.trim();
119
+ const matched = pattern ? sorted.filter((r) => matchesNamePattern(r.name, pattern)) : sorted;
120
+ const unit = metric === 'callCount' ? '次' : metric === 'selfMemory' ? rows.find((r) => r.selfMemoryUnit)?.selfMemoryUnit ?? 'KB' : 'ms';
121
+ return {
122
+ testMode: stackTestMode,
123
+ stackTestMode,
124
+ availableMetrics: [...profile.availableMetrics],
125
+ availableFields: [...profile.availableFields],
126
+ metricGuide: profile.description,
127
+ metric,
128
+ unit,
129
+ rows: matched.slice(0, topN).map((r) => ({
130
+ name: r.name,
131
+ methodId: r.stackMethodId,
132
+ stackMethodId: r.stackMethodId,
133
+ selfTimeMean: r.selfTimeMean != null ? round2(r.selfTimeMean) : null,
134
+ totalTimeMean: r.totalTimeMean != null ? round2(r.totalTimeMean) : null,
135
+ callCountTotal: r.callCountTotal != null ? Math.round(r.callCountTotal) : null,
136
+ selfMemoryMean: r.selfMemoryMean != null ? round2(r.selfMemoryMean) : null,
137
+ selfMemoryTotal: r.selfMemoryTotal != null ? round2(r.selfMemoryTotal) : null,
138
+ selfMemoryUnit: r.selfMemoryUnit,
139
+ value: metric === 'callCount'
140
+ ? Math.round(metricValue(r) >= 0 ? metricValue(r) : 0)
141
+ : round2(metricValue(r) >= 0 ? metricValue(r) : 0),
142
+ unit,
143
+ callCount: r.callCountTotal != null ? Math.round(r.callCountTotal) : 0,
144
+ callFrameCount: 0,
145
+ nodeCount: 1,
146
+ source: 'overview.method.idmap.statistic',
147
+ })),
148
+ totalNodes: rows.length,
149
+ uniqueFunctions: rows.length,
150
+ apisUsed,
151
+ };
152
+ }
2
153
  /**
3
154
  * Overview:线程堆栈树(主线程)+ 函数 idmap → 按函数名合并节点。
4
155
  * 同名(同一 methodId)节点的 selfTime / callCount 相加,再按指标排序取 Top N。
@@ -231,10 +382,11 @@ function finalizeAgg(nodes, idMap, topN, metric, testMode, apisUsed, source) {
231
382
  return {
232
383
  name: r.name,
233
384
  methodId: r.methodId,
385
+ stackMethodId: r.methodId,
386
+ selfTimeMean: round2(r.selfTimeUs / 1000),
387
+ totalTimeMean: round2(r.totalTimeUs / 1000),
234
388
  value,
235
389
  unit,
236
- selfTimeMs: round2(r.selfTimeUs / 1000),
237
- totalTimeMs: round2(r.totalTimeUs / 1000),
238
390
  selfMemoryKb: round2(r.selfMemoryKb),
239
391
  callCount: r.callCount,
240
392
  callFrameCount: r.callFrameCount,
@@ -0,0 +1,33 @@
1
+ import type { StackMetric } from './stack-agg.js';
2
+ /**
3
+ * Overview 堆栈采集模式(来自 overview/stack/overall/tree/presign 的 testMode)。
4
+ * 仅适用于 Overview 报告;Mono 报告走独立堆栈,不使用本枚举。
5
+ */
6
+ export type OverviewStackTestMode = 'CPU_ONLY' | 'LUA_MEM_ONLY' | 'CPU_AND_LUA_MEM';
7
+ /** @deprecated 使用 OverviewStackTestMode */
8
+ export type StackTestMode = OverviewStackTestMode;
9
+ export interface OverviewStackTestModeProfile {
10
+ stackTestMode: OverviewStackTestMode;
11
+ availableMetrics: StackMetric[];
12
+ availableFields: readonly string[];
13
+ description: string;
14
+ }
15
+ /** @deprecated 使用 OverviewStackTestModeProfile */
16
+ export type StackTestModeProfile = OverviewStackTestModeProfile;
17
+ export declare const OVERVIEW_STACK_TEST_MODE_PROFILES: Record<OverviewStackTestMode, OverviewStackTestModeProfile>;
18
+ export declare const STACK_TEST_MODE_PROFILES: Record<OverviewStackTestMode, OverviewStackTestModeProfile>;
19
+ export declare function normalizeOverviewStackTestMode(raw: unknown): OverviewStackTestMode | null;
20
+ /** @deprecated 使用 normalizeOverviewStackTestMode */
21
+ export declare const normalizeStackTestMode: typeof normalizeOverviewStackTestMode;
22
+ export declare function inferOverviewStackTestMode(flags: {
23
+ hasTime: boolean;
24
+ hasMem: boolean;
25
+ }): OverviewStackTestMode | null;
26
+ /** @deprecated 使用 inferOverviewStackTestMode */
27
+ export declare const inferStackTestMode: typeof inferOverviewStackTestMode;
28
+ export declare function resolveOverviewDefaultMetric(stackTestMode: OverviewStackTestMode): StackMetric;
29
+ /** @deprecated 使用 resolveOverviewDefaultMetric */
30
+ export declare const resolveDefaultMetric: typeof resolveOverviewDefaultMetric;
31
+ export declare function assertOverviewMetricSupported(metric: StackMetric, profile: OverviewStackTestModeProfile): void;
32
+ /** @deprecated 使用 assertOverviewMetricSupported */
33
+ export declare const assertMetricSupported: typeof assertOverviewMetricSupported;
@@ -0,0 +1,60 @@
1
+ export const OVERVIEW_STACK_TEST_MODE_PROFILES = {
2
+ CPU_ONLY: {
3
+ stackTestMode: 'CPU_ONLY',
4
+ availableMetrics: ['selfTime', 'totalTime', 'callCount'],
5
+ availableFields: ['selfTimeMean', 'totalTimeMean', 'callCountTotal'],
6
+ description: 'CPU_ONLY:仅有耗时(selfTimeMean/totalTimeMean,ms)与调用次数(callCountTotal);内存字段恒为 null。',
7
+ },
8
+ LUA_MEM_ONLY: {
9
+ stackTestMode: 'LUA_MEM_ONLY',
10
+ availableMetrics: ['callCount', 'selfMemory'],
11
+ availableFields: ['callCountTotal', 'selfMemoryMean', 'selfMemoryTotal'],
12
+ description: 'LUA_MEM_ONLY:仅有调用次数(callCountTotal)与内存(selfMemoryMean/selfMemoryTotal);耗时字段恒为 null。',
13
+ },
14
+ CPU_AND_LUA_MEM: {
15
+ stackTestMode: 'CPU_AND_LUA_MEM',
16
+ availableMetrics: ['selfTime', 'totalTime', 'callCount', 'selfMemory'],
17
+ availableFields: [
18
+ 'selfTimeMean',
19
+ 'totalTimeMean',
20
+ 'callCountTotal',
21
+ 'selfMemoryMean',
22
+ 'selfMemoryTotal',
23
+ ],
24
+ description: 'CPU_AND_LUA_MEM:耗时、调用次数、内存均可用。',
25
+ },
26
+ };
27
+ export const STACK_TEST_MODE_PROFILES = OVERVIEW_STACK_TEST_MODE_PROFILES;
28
+ export function normalizeOverviewStackTestMode(raw) {
29
+ const tm = String(raw ?? '').toUpperCase();
30
+ if (tm === 'CPU_ONLY' || tm === 'LUA_MEM_ONLY' || tm === 'CPU_AND_LUA_MEM') {
31
+ return tm;
32
+ }
33
+ return null;
34
+ }
35
+ /** @deprecated 使用 normalizeOverviewStackTestMode */
36
+ export const normalizeStackTestMode = normalizeOverviewStackTestMode;
37
+ export function inferOverviewStackTestMode(flags) {
38
+ if (flags.hasTime && flags.hasMem)
39
+ return 'CPU_AND_LUA_MEM';
40
+ if (flags.hasMem && !flags.hasTime)
41
+ return 'LUA_MEM_ONLY';
42
+ if (flags.hasTime)
43
+ return 'CPU_ONLY';
44
+ return null;
45
+ }
46
+ /** @deprecated 使用 inferOverviewStackTestMode */
47
+ export const inferStackTestMode = inferOverviewStackTestMode;
48
+ export function resolveOverviewDefaultMetric(stackTestMode) {
49
+ return stackTestMode === 'LUA_MEM_ONLY' ? 'selfMemory' : 'selfTime';
50
+ }
51
+ /** @deprecated 使用 resolveOverviewDefaultMetric */
52
+ export const resolveDefaultMetric = resolveOverviewDefaultMetric;
53
+ export function assertOverviewMetricSupported(metric, profile) {
54
+ if (!profile.availableMetrics.includes(metric)) {
55
+ throw new Error(`当前 Overview stackTestMode=${profile.stackTestMode} 不支持 metric=${metric}。` +
56
+ `可用:${profile.availableMetrics.join('、')}。${profile.description}`);
57
+ }
58
+ }
59
+ /** @deprecated 使用 assertOverviewMetricSupported */
60
+ export const assertMetricSupported = assertOverviewMetricSupported;
@@ -7,8 +7,13 @@ export interface FunctionRow {
7
7
  source: string;
8
8
  label?: string;
9
9
  methodId?: string;
10
- selfTimeMs?: number;
11
- totalTimeMs?: number;
10
+ stackMethodId?: string;
11
+ selfTimeMean?: number | null;
12
+ totalTimeMean?: number | null;
13
+ callCountTotal?: number | null;
14
+ selfMemoryMean?: number | null;
15
+ selfMemoryTotal?: number | null;
16
+ selfMemoryUnit?: string | null;
12
17
  callCount?: number;
13
18
  nodeCount?: number;
14
19
  }
@@ -1,12 +1,19 @@
1
1
  import { z } from './types.js';
2
2
  import { errorResult, jsonToolResult, requireReportKey, topNOf } from './helpers.js';
3
3
  import { fetchReportIdentity } from './route-overview.js';
4
- import { aggregateMonoStack, aggregateOverviewStack } from './stack-agg.js';
5
- function resolveMetric(raw, mode) {
4
+ import { aggregateMonoStack, aggregateOverviewStackFromStatistic } from './stack-agg.js';
5
+ import { filterRowsByNamePattern } from './name-pattern.js';
6
+ function resolveOverviewMetricInput(raw) {
6
7
  if (raw === 'totalTime' || raw === 'callCount' || raw === 'selfMemory' || raw === 'selfTime') {
7
8
  return raw;
8
9
  }
9
- return mode === 'mono' ? 'selfMemory' : 'selfTime';
10
+ return undefined;
11
+ }
12
+ function resolveMonoMetric(raw) {
13
+ if (raw === 'totalTime' || raw === 'callCount' || raw === 'selfMemory' || raw === 'selfTime') {
14
+ return raw;
15
+ }
16
+ return 'selfMemory';
10
17
  }
11
18
  export async function runTopFunctions(client, args) {
12
19
  const key = requireReportKey(args);
@@ -17,33 +24,42 @@ export async function runTopFunctions(client, args) {
17
24
  const identity = await fetchReportIdentity(client, key);
18
25
  const apisUsed = ['get_report_detail'];
19
26
  const mode = subtype ?? identity.serviceSubtype ?? 'overview';
20
- const metric = resolveMetric(args['metric'], mode);
21
27
  if (mode === 'mono') {
28
+ const metric = resolveMonoMetric(args['metric']);
22
29
  if (identity.engine !== 'unity') {
23
30
  return {
24
31
  engine: identity.engine,
25
32
  dataKey: identity.dataKey,
33
+ serviceSubtype: 'mono',
26
34
  items: [],
27
35
  _apisUsed: apisUsed,
28
- _note: 'Mono 模式仅支持 Unity 报告。',
36
+ _note: 'Mono 模式仅支持 Unity 报告(与 Overview stackTestMode 无关)。',
29
37
  _limitations: ['engine!=unity'],
30
38
  };
31
39
  }
32
40
  try {
33
- const agg = await aggregateMonoStack(client, identity, topN, metric);
41
+ const namePattern = typeof args['namePattern'] === 'string' && args['namePattern'].trim()
42
+ ? String(args['namePattern']).trim()
43
+ : undefined;
44
+ const fetchN = namePattern ? Math.max(topN, 200) : topN;
45
+ const agg = await aggregateMonoStack(client, identity, fetchN, metric);
46
+ const filtered = filterRowsByNamePattern(agg.rows, namePattern);
47
+ const items = filtered.rows.slice(0, topN);
34
48
  return {
35
49
  engine: identity.engine,
36
50
  dataKey: identity.dataKey,
37
51
  serviceSubtype: 'mono',
38
52
  topN,
39
53
  metric: agg.metric,
40
- testMode: agg.testMode,
41
- items: agg.rows,
54
+ namePattern: namePattern ?? null,
55
+ matchedFunctions: namePattern ? filtered.matchedTotal : agg.uniqueFunctions,
56
+ items,
42
57
  totalNodes: agg.totalNodes,
43
58
  uniqueFunctions: agg.uniqueFunctions,
44
59
  _apisUsed: [...apisUsed, ...agg.apisUsed],
45
- _note: `已下载 Mono 正向堆栈树并按函数名合并同名节点(共 ${agg.totalNodes} 个节点 → ${agg.uniqueFunctions} 个函数),` +
46
- `按 ${agg.metric} 降序取 Top ${agg.rows.length}。`,
60
+ _note: `Mono 模式:已下载正向堆栈树合并取 Top ${items.length}(共 ${agg.totalNodes} 节点)。` +
61
+ `Mono 不使用 Overview stackTestMode;默认按 selfMemory 排序。` +
62
+ (namePattern ? ` namePattern「${String(namePattern)}」命中 ${filtered.matchedTotal} 个。` : ''),
47
63
  };
48
64
  }
49
65
  catch (e) {
@@ -58,25 +74,44 @@ export async function runTopFunctions(client, args) {
58
74
  };
59
75
  }
60
76
  }
61
- // Overview:必须以堆栈树 + idmap 为准;统计接口里的「卡顿重点函数 / func / 自定义函数组」都不完整。
77
+ const threadName = typeof args['threadName'] === 'string' && args['threadName'].trim()
78
+ ? String(args['threadName']).trim()
79
+ : undefined;
80
+ const metricInput = resolveOverviewMetricInput(args['metric']);
81
+ const namePattern = typeof args['namePattern'] === 'string' && args['namePattern'].trim()
82
+ ? String(args['namePattern']).trim()
83
+ : undefined;
62
84
  try {
63
- const agg = await aggregateOverviewStack(client, identity, topN, metric);
85
+ const agg = await aggregateOverviewStackFromStatistic(client, identity, topN, metricInput, threadName, namePattern);
86
+ const nameFilterNote = namePattern
87
+ ? agg.rows.length === 0
88
+ ? ` namePattern「${namePattern}」无匹配函数。`
89
+ : ` namePattern「${namePattern}」已过滤后取 Top ${agg.rows.length}。`
90
+ : '';
64
91
  return {
65
92
  engine: identity.engine,
66
93
  dataKey: identity.dataKey,
67
94
  createDate: identity.createDate,
68
95
  sdkVersion: identity.sdkVersion,
69
- serviceSubtype: identity.serviceSubtype,
96
+ serviceSubtype: 'overview',
97
+ stackTestMode: agg.stackTestMode,
98
+ availableMetrics: agg.availableMetrics,
99
+ availableFields: agg.availableFields,
70
100
  topN,
71
101
  metric: agg.metric,
72
- testMode: agg.testMode,
102
+ namePattern: namePattern ?? null,
73
103
  items: agg.rows,
74
104
  totalNodes: agg.totalNodes,
75
105
  uniqueFunctions: agg.uniqueFunctions,
76
106
  _apisUsed: [...apisUsed, ...agg.apisUsed],
77
- _note: `已下载主线程堆栈树并按函数名合并同名节点(共 ${agg.totalNodes} 个节点 → ${agg.uniqueFunctions} 个函数),` +
78
- `按 ${agg.metric}(${agg.unit})降序取 Top ${agg.rows.length}。` +
79
- `默认排序字段为 selfTime(自身耗时),不是统计接口里的抽样指标。`,
107
+ _note: `Overview 模式 stackTestMode=${agg.stackTestMode}。${agg.metricGuide ?? ''} ` +
108
+ `本次按 ${agg.metric} 排序,Top ${agg.rows.length} / 共 ${agg.totalNodes} 函数。` +
109
+ `数据来自 method/idmap/statistic v1.0.2(非堆栈树解析)。` +
110
+ `逐帧曲线:method/curve/presign v1.0.2 + stackMethodId。` +
111
+ (identity.engine === 'unreal'
112
+ ? ` UE 默认 threadName=GameThread${threadName ? `(本次 ${threadName})` : ''};UE 固定 CPU_ONLY。`
113
+ : '') +
114
+ nameFilterNote,
80
115
  };
81
116
  }
82
117
  catch (e) {
@@ -85,12 +120,12 @@ export async function runTopFunctions(client, args) {
85
120
  dataKey: identity.dataKey,
86
121
  createDate: identity.createDate,
87
122
  sdkVersion: identity.sdkVersion,
88
- serviceSubtype: identity.serviceSubtype,
123
+ serviceSubtype: 'overview',
89
124
  items: [],
90
125
  _apisUsed: apisUsed,
91
- _note: `堆栈树聚合失败:${e instanceof Error ? e.message : String(e)}。` +
92
- `Overview 函数 Top 依赖 stack/overall/tree + stack/id/map(通常需 SDK ≥ 2.5.1 且开启 CPU 堆栈)。`,
93
- _limitations: ['overview_stack_unavailable'],
126
+ _note: `Overview 函数 Top 失败:${e instanceof Error ? e.message : String(e)}。` +
127
+ `通常需 SDK ≥ 2.5.1 且报告开启堆栈。`,
128
+ _limitations: ['overview_idmap_statistic_unavailable'],
94
129
  };
95
130
  }
96
131
  }
@@ -100,26 +135,36 @@ export const topFunctionsTool = {
100
135
  engines: ['unity', 'unreal'],
101
136
  filterKeys: ['top_functions', 'composite', 'preset.default', 'overview', 'mono'],
102
137
  description: [
103
- '从堆栈树聚合指定报告的函数 Top N(按自身耗时合并同名节点)。',
138
+ '从函数统计聚合指定报告的函数 Top N',
104
139
  '适用引擎:Unity / UE|复合工具',
105
- 'Overview:下载主线程堆栈树(stack/overall/tree/presign)+ 函数 idmap(stack/id/map),',
106
- ' methodId 还原为函数名,并把树上同一函数的多个节点合并(selfTime/callCount 相加),再取 Top N。',
107
- '不要用统计接口里的「卡顿重点函数」或自定义函数组——那不是全量函数耗时。',
108
- 'Mono(仅 Unity):走 Mono 正向堆栈树 + Mono 函数 idmap,默认按自身内存排序。',
109
- '需要原始树时请直接调对应的 stack/presign 原子工具。',
140
+ 'Overview:method/idmap/statistic v1.0.2;stackTestMode 决定可用字段(仅 Overview):',
141
+ ' CPU_ONLY selfTimeMean/totalTimeMean/callCountTotal;',
142
+ ' LUA_MEM_ONLY → callCountTotal/selfMemoryMean/selfMemoryTotal;',
143
+ ' CPU_AND_LUA_MEM 全部;UE 固定 CPU_ONLY。',
144
+ 'Mono(仅 Unity):独立堆栈树,不使用 stackTestMode,默认 selfMemory。',
145
+ '逐帧曲线:method/curve/presign v1.0.2 + stackMethodId。',
146
+ 'namePattern:可选,按函数名子串或 /正则/ 过滤(Overview 在 idmap 结果上过滤;Mono 在合并后的函数列表上过滤)。',
110
147
  ].join('\n'),
111
148
  inputSchema: {
112
149
  dataKey: z.string().optional().describe('报告 dataKey,与 recordId 二选一'),
113
150
  recordId: z.union([z.string(), z.number()]).optional().describe('报告 recordId,与 dataKey 二选一'),
114
151
  topN: z.number().optional().describe('返回条数,默认 20,最大 200'),
152
+ namePattern: z
153
+ .string()
154
+ .optional()
155
+ .describe('函数名过滤:默认子串匹配(忽略大小写);正则写 /pattern/i,如 /Canvas\\./ 或 Update'),
115
156
  metric: z
116
157
  .enum(['selfTime', 'totalTime', 'callCount', 'selfMemory'])
117
158
  .optional()
118
- .describe('排序指标,默认 Overview selfTime(自身耗时 ms),Mono selfMemory(KB)'),
159
+ .describe('排序指标。Overview 须与 stackTestMode 匹配(CPU_ONLY 无 selfMemory;LUA_MEM_ONLY 无 selfTime/totalTime);未传时 Overview 按模式默认 selfTime selfMemory'),
160
+ threadName: z
161
+ .string()
162
+ .optional()
163
+ .describe('UE Overview 线程名,默认 GameThread;Unity 忽略'),
119
164
  serviceSubtype: z
120
165
  .enum(['overview', 'mono'])
121
166
  .optional()
122
- .describe('报告模式,默认跟随报告详情;mono 仅 Unity'),
167
+ .describe('overview(默认)或 mono;stackTestModeoverview 有效'),
123
168
  },
124
169
  async handler(args, ctx) {
125
170
  try {
@@ -0,0 +1,6 @@
1
+ /** indicatorDashboards 合法面板传参名称(与 got-query OverviewIndicatorDashboardCatalogService 同源)。 */
2
+ export declare const INDICATOR_DASHBOARD_KEYS: readonly string[];
3
+ /** 常见误传:统计维度后缀 / 子指标名,不是面板标识符。 */
4
+ export declare const INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES: readonly ["fps_mean", "fps_maximum", "frametime_min", "frametime_gt_40_pct", "temperature_mean", "drawcall_maximum"];
5
+ export declare function splitIndicatorDashboards(raw: unknown): string[];
6
+ export declare function findUnknownIndicatorDashboards(raw: unknown): string[];
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const keysJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'indicator-dashboard-keys.json'), 'utf8'));
5
+ /** indicatorDashboards 合法面板传参名称(与 got-query OverviewIndicatorDashboardCatalogService 同源)。 */
6
+ export const INDICATOR_DASHBOARD_KEYS = keysJson.keys;
7
+ /** 常见误传:统计维度后缀 / 子指标名,不是面板标识符。 */
8
+ export const INDICATOR_DASHBOARD_NEGATIVE_EXAMPLES = [
9
+ 'fps_mean',
10
+ 'fps_maximum',
11
+ 'frametime_min',
12
+ 'frametime_gt_40_pct',
13
+ 'temperature_mean',
14
+ 'drawcall_maximum',
15
+ ];
16
+ export function splitIndicatorDashboards(raw) {
17
+ if (typeof raw !== 'string' || !raw.trim())
18
+ return [];
19
+ return raw
20
+ .split(',')
21
+ .map((s) => s.trim())
22
+ .filter(Boolean);
23
+ }
24
+ export function findUnknownIndicatorDashboards(raw) {
25
+ const parts = splitIndicatorDashboards(raw);
26
+ if (!parts.length)
27
+ return [];
28
+ const valid = new Set(INDICATOR_DASHBOARD_KEYS);
29
+ return parts.filter((k) => !valid.has(k));
30
+ }