vite-plugin-aipanel 1.2.28 → 1.3.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.
@@ -0,0 +1,617 @@
1
+ import {
2
+ VUE_DEVTOOLS_TIMELINE_DEFAULTS,
3
+ VUE_DEVTOOLS_TIMELINE_INCLUDE,
4
+ VUE_DEVTOOLS_TIMELINE_INCLUDES,
5
+ VUE_DEVTOOLS_TIMELINE_LAYERS
6
+ } from "@aipanel/core";
7
+ import { sanitizeValue } from "./vue-devtools-sanitize.mjs";
8
+ const DEVTOOLS_HOOK_EVENTS = {
9
+ appInit: "app:init",
10
+ appUnmount: "app:unmount",
11
+ componentAdded: "component:added",
12
+ componentUpdated: "component:updated",
13
+ componentRemoved: "component:removed",
14
+ componentEmit: "component:emit",
15
+ performanceStart: "perf:start",
16
+ performanceEnd: "perf:end"
17
+ };
18
+ const MAX_COMPONENTS = 200;
19
+ const MAX_MARKS = 20;
20
+ const PENDING_TTL_MS = 5e3;
21
+ const PENDING_PRUNE_THRESHOLD = 64;
22
+ const MAX_PENDING = 5e3;
23
+ const MAX_PAYLOAD_BYTES = 64 * 1024;
24
+ const TREE_CACHE_TTL_MS = 2e3;
25
+ function round2(value) {
26
+ return Math.round(value * 100) / 100;
27
+ }
28
+ function clampNumber(value, min, max, fallback) {
29
+ const num = typeof value === "number" && Number.isFinite(value) ? value : NaN;
30
+ if (Number.isNaN(num)) return fallback;
31
+ return Math.min(max, Math.max(min, num));
32
+ }
33
+ function readName(instance) {
34
+ const type = instance?.type;
35
+ const name = type?.name ?? type?.__name;
36
+ return typeof name === "string" && name ? name : "(anonymous)";
37
+ }
38
+ function readUid(instance) {
39
+ const uid = instance?.uid;
40
+ return typeof uid === "number" ? uid : void 0;
41
+ }
42
+ function readRoutePath(route) {
43
+ const value = route;
44
+ if (typeof value?.fullPath === "string") return value.fullPath;
45
+ if (typeof value?.path === "string") return value.path;
46
+ return String(route ?? "");
47
+ }
48
+ function readEventName(data) {
49
+ const event = data?.event;
50
+ return typeof event === "string" ? event : "(unknown)";
51
+ }
52
+ function collectFiles(nodes, files) {
53
+ if (!Array.isArray(nodes)) return;
54
+ for (const node of nodes) {
55
+ const { id, file, children } = node ?? {};
56
+ if (typeof id === "string" && typeof file === "string" && file) files.set(id, file);
57
+ collectChildren(children, files);
58
+ }
59
+ }
60
+ function collectChildren(children, files) {
61
+ collectFiles(children, files);
62
+ }
63
+ function safely(handler) {
64
+ return (...args) => {
65
+ try {
66
+ handler(...args);
67
+ } catch {
68
+ }
69
+ };
70
+ }
71
+ function createTimelineRecorder(options = {}) {
72
+ const capacity = options.capacity ?? VUE_DEVTOOLS_TIMELINE_DEFAULTS.capacity;
73
+ const now = options.now ?? (() => performance.now());
74
+ const getAppRecords = options.getAppRecords ?? (() => []);
75
+ const resolveRouter = options.resolveRouter ?? (() => void 0);
76
+ const hook = options.hook;
77
+ function readNow() {
78
+ try {
79
+ const value = now();
80
+ return typeof value === "number" && Number.isFinite(value) ? value : Date.now();
81
+ } catch {
82
+ return Date.now();
83
+ }
84
+ }
85
+ const bootId = Math.random().toString(36).slice(2, 10);
86
+ let startedAt = readNow();
87
+ let records = [];
88
+ let marks = [];
89
+ const pending = /* @__PURE__ */ new Map();
90
+ const components = /* @__PURE__ */ new Map();
91
+ const appRouters = /* @__PURE__ */ new WeakMap();
92
+ const routerSubscriptions = /* @__PURE__ */ new Map();
93
+ const unsubscribers = [];
94
+ const lifecycleTotals = { added: 0, updated: 0, removed: 0 };
95
+ let componentEvictions = 0;
96
+ let dropped = 0;
97
+ let droppedAt = null;
98
+ let unpaired = 0;
99
+ let prunedStarts = 0;
100
+ let sequence = 0;
101
+ let markSequence = 0;
102
+ let routerCount = 0;
103
+ let treeCache = null;
104
+ function appIdOf(app, instance) {
105
+ const appLike = app;
106
+ const fromApp = appLike?.__VUE_DEVTOOLS_NEXT_APP_RECORD_ID__ ?? appLike?.__VUE_DEVTOOLS_NEXT_APP_RECORD__?.id;
107
+ if (typeof fromApp === "string" && fromApp) return fromApp;
108
+ const fromInstance = instance?.appContext?.app;
109
+ const fromInstanceId = fromInstance?.__VUE_DEVTOOLS_NEXT_APP_RECORD_ID__ ?? fromInstance?.__VUE_DEVTOOLS_NEXT_APP_RECORD__?.id;
110
+ if (typeof fromInstanceId === "string" && fromInstanceId) return fromInstanceId;
111
+ return getAppRecords().find((record) => record.app === app)?.id ?? "?";
112
+ }
113
+ function nodeIdOf(app, uid, instance) {
114
+ const instanceLike = instance;
115
+ if (typeof instanceLike?.__VUE_DEVTOOLS_NEXT_UID__ === "string") {
116
+ return instanceLike.__VUE_DEVTOOLS_NEXT_UID__;
117
+ }
118
+ const appId = appIdOf(app, instance);
119
+ if (appId === "?") return void 0;
120
+ const isRoot = instanceLike != null && instanceLike.root === instance || app?._instance?.uid === uid;
121
+ return `${appId}:${isRoot ? "root" : uid}`;
122
+ }
123
+ function push(record) {
124
+ records.push({ ...record, seq: ++sequence });
125
+ const overflow = records.length - capacity;
126
+ if (overflow > 0) {
127
+ const evicted = records.splice(0, overflow);
128
+ const newest = evicted[evicted.length - 1];
129
+ if (newest) droppedAt = Math.max(droppedAt ?? newest.at, newest.at);
130
+ dropped += overflow;
131
+ }
132
+ }
133
+ function prunePending(nowAt) {
134
+ if (pending.size > PENDING_PRUNE_THRESHOLD) {
135
+ for (const [key, operation] of pending) {
136
+ if (nowAt - operation.at > PENDING_TTL_MS) {
137
+ pending.delete(key);
138
+ prunedStarts += 1;
139
+ }
140
+ }
141
+ }
142
+ while (pending.size > MAX_PENDING) {
143
+ const oldest = pending.keys().next().value;
144
+ if (oldest === void 0) break;
145
+ pending.delete(oldest);
146
+ prunedStarts += 1;
147
+ }
148
+ }
149
+ function touchComponent(app, uid, instance, field) {
150
+ const key = `${appIdOf(app, instance)}:${uid}`;
151
+ const nodeId = nodeIdOf(app, uid, instance);
152
+ const existing = components.get(key);
153
+ const stat = existing ?? {
154
+ name: readName(instance),
155
+ ...nodeId === void 0 ? {} : { nodeId },
156
+ added: 0,
157
+ updated: 0,
158
+ removed: 0,
159
+ lastAt: 0
160
+ };
161
+ if (existing && stat.nodeId === void 0 && nodeId !== void 0) stat.nodeId = nodeId;
162
+ stat[field] += 1;
163
+ stat.lastAt = readNow();
164
+ lifecycleTotals[field] += 1;
165
+ if (existing) components.delete(key);
166
+ components.set(key, stat);
167
+ while (components.size > MAX_COMPONENTS) {
168
+ const oldest = components.keys().next().value;
169
+ if (oldest === void 0) break;
170
+ components.delete(oldest);
171
+ componentEvictions += 1;
172
+ }
173
+ }
174
+ function onPerformanceStart(app, uid, vm, type, time) {
175
+ if (typeof uid !== "number" || typeof type !== "string") return;
176
+ prunePending(time);
177
+ const nodeId = nodeIdOf(app, uid, vm);
178
+ pending.set(`${appIdOf(app, vm)}:${uid}:${type}`, {
179
+ name: readName(vm),
180
+ ...nodeId === void 0 ? {} : { nodeId },
181
+ at: typeof time === "number" ? time : readNow()
182
+ });
183
+ }
184
+ function onPerformanceEnd(app, uid, vm, type, time) {
185
+ if (typeof uid !== "number" || typeof type !== "string") return;
186
+ const key = `${appIdOf(app, vm)}:${uid}:${type}`;
187
+ const started = pending.get(key);
188
+ pending.delete(key);
189
+ if (!started) {
190
+ unpaired += 1;
191
+ return;
192
+ }
193
+ const endAt = typeof time === "number" ? time : readNow();
194
+ push({
195
+ at: endAt,
196
+ layer: "perf",
197
+ kind: type,
198
+ name: started.name || readName(vm),
199
+ nodeId: started.nodeId,
200
+ dur: round2(Math.max(0, endAt - started.at))
201
+ });
202
+ }
203
+ function onComponentAdded(app, uid, _parentUid, instance) {
204
+ if (typeof uid !== "number") return;
205
+ touchComponent(app, uid, instance, "added");
206
+ }
207
+ function onComponentUpdated(app, uid, _parentUid, instance) {
208
+ if (typeof uid !== "number") return;
209
+ touchComponent(app, uid, instance, "updated");
210
+ }
211
+ function onComponentRemoved(app, uid, _parentUid, instance) {
212
+ if (typeof uid !== "number") return;
213
+ touchComponent(app, uid, instance, "removed");
214
+ }
215
+ function onComponentEmit(app, instance, event, params) {
216
+ const uid = readUid(instance);
217
+ const nodeId = uid === void 0 ? void 0 : nodeIdOf(app, uid, instance);
218
+ push({
219
+ at: readNow(),
220
+ layer: "emit",
221
+ kind: "emit",
222
+ name: readName(instance),
223
+ ...nodeId === void 0 ? {} : { nodeId },
224
+ data: { event: typeof event === "string" ? event : String(event), params: sanitizeValue(params) }
225
+ });
226
+ }
227
+ function onNavigated(to, from, failure) {
228
+ const toPath = readRoutePath(to);
229
+ const failureDetail = failure ? sanitizeValue(failure?.message ?? String(failure)) : void 0;
230
+ push({
231
+ at: readNow(),
232
+ layer: "navigate",
233
+ kind: "navigation",
234
+ name: toPath,
235
+ level: failure ? "warning" : "default",
236
+ data: {
237
+ from: readRoutePath(from),
238
+ to: toPath,
239
+ ...failureDetail === void 0 || failureDetail === null ? {} : { failure: failureDetail }
240
+ }
241
+ });
242
+ }
243
+ function subscribeRouter(app) {
244
+ const router = resolveRouter(app);
245
+ if (!router || typeof router.afterEach !== "function") return;
246
+ const appKey = typeof app === "object" && app !== null ? app : void 0;
247
+ if (appKey) {
248
+ const previous = appRouters.get(appKey);
249
+ if (previous === router && routerSubscriptions.has(router)) return;
250
+ if (previous && previous !== router) releaseRouter(previous);
251
+ appRouters.set(appKey, router);
252
+ } else if (routerSubscriptions.has(router)) {
253
+ return;
254
+ }
255
+ const existing = routerSubscriptions.get(router);
256
+ if (existing) {
257
+ existing.apps += 1;
258
+ routerCount += 1;
259
+ return;
260
+ }
261
+ const off = router.afterEach(safely(onNavigated));
262
+ if (typeof off !== "function") return;
263
+ routerSubscriptions.set(router, { off, apps: 1 });
264
+ routerCount += 1;
265
+ }
266
+ function releaseRouter(router) {
267
+ const entry = routerSubscriptions.get(router);
268
+ if (!entry) return;
269
+ entry.apps = Math.max(0, entry.apps - 1);
270
+ routerCount = Math.max(0, routerCount - 1);
271
+ if (entry.apps > 0) return;
272
+ safely(entry.off)();
273
+ routerSubscriptions.delete(router);
274
+ }
275
+ function unsubscribeRouter(app) {
276
+ const appKey = typeof app === "object" && app !== null ? app : void 0;
277
+ if (!appKey) return;
278
+ const router = appRouters.get(appKey);
279
+ if (!router) return;
280
+ appRouters.delete(appKey);
281
+ releaseRouter(router);
282
+ }
283
+ function onAppInit(app) {
284
+ subscribeRouter(app);
285
+ }
286
+ function onAppUnmount(app) {
287
+ unsubscribeRouter(app);
288
+ }
289
+ function attachRouters() {
290
+ let records2;
291
+ try {
292
+ records2 = getAppRecords();
293
+ } catch {
294
+ return;
295
+ }
296
+ for (const record of records2) {
297
+ try {
298
+ subscribeRouter(record.app);
299
+ } catch {
300
+ }
301
+ }
302
+ }
303
+ try {
304
+ if (hook && typeof hook.on === "function") {
305
+ const subscribe = (event, handler) => {
306
+ try {
307
+ const off = hook.on(event, safely(handler));
308
+ if (typeof off === "function") unsubscribers.push(off);
309
+ } catch {
310
+ }
311
+ };
312
+ subscribe(DEVTOOLS_HOOK_EVENTS.performanceStart, onPerformanceStart);
313
+ subscribe(DEVTOOLS_HOOK_EVENTS.performanceEnd, onPerformanceEnd);
314
+ subscribe(DEVTOOLS_HOOK_EVENTS.componentAdded, onComponentAdded);
315
+ subscribe(DEVTOOLS_HOOK_EVENTS.componentUpdated, onComponentUpdated);
316
+ subscribe(DEVTOOLS_HOOK_EVENTS.componentRemoved, onComponentRemoved);
317
+ subscribe(DEVTOOLS_HOOK_EVENTS.componentEmit, onComponentEmit);
318
+ subscribe(DEVTOOLS_HOOK_EVENTS.appInit, onAppInit);
319
+ subscribe(DEVTOOLS_HOOK_EVENTS.appUnmount, onAppUnmount);
320
+ }
321
+ } catch {
322
+ }
323
+ async function fileMap(nowAt) {
324
+ if (treeCache && nowAt - treeCache.at < TREE_CACHE_TTL_MS) return treeCache.files;
325
+ const files = /* @__PURE__ */ new Map();
326
+ try {
327
+ const tree = await options.getInspectorTree?.();
328
+ collectFiles(tree, files);
329
+ } catch {
330
+ }
331
+ treeCache = { at: nowAt, files };
332
+ return files;
333
+ }
334
+ function mark(label) {
335
+ const at = readNow();
336
+ const markId = `m${++markSequence}`;
337
+ const name = typeof label === "string" && label ? label : "mark";
338
+ marks.push({ id: markId, label: name, at });
339
+ while (marks.length > MAX_MARKS) marks.shift();
340
+ push({ at, layer: "agent", kind: "mark", name, data: { markId } });
341
+ return { markId, buffered: records.length };
342
+ }
343
+ function clear() {
344
+ const cleared = records.length;
345
+ const clearedMarks = marks.length;
346
+ records = [];
347
+ marks = [];
348
+ pending.clear();
349
+ components.clear();
350
+ lifecycleTotals.added = 0;
351
+ lifecycleTotals.updated = 0;
352
+ lifecycleTotals.removed = 0;
353
+ componentEvictions = 0;
354
+ dropped = 0;
355
+ droppedAt = null;
356
+ unpaired = 0;
357
+ prunedStarts = 0;
358
+ treeCache = null;
359
+ startedAt = readNow();
360
+ return { cleared, marks: clearedMarks };
361
+ }
362
+ function dispose() {
363
+ for (const off of unsubscribers) safely(off)();
364
+ unsubscribers.length = 0;
365
+ for (const { off } of routerSubscriptions.values()) safely(off)();
366
+ routerSubscriptions.clear();
367
+ routerCount = 0;
368
+ }
369
+ async function get(query = {}) {
370
+ const nowAt = readNow();
371
+ const include = typeof query.include === "string" && VUE_DEVTOOLS_TIMELINE_INCLUDES.includes(query.include) ? query.include : VUE_DEVTOOLS_TIMELINE_DEFAULTS.include;
372
+ const windowMs = clampNumber(query.windowMs, 0, 6e5, VUE_DEVTOOLS_TIMELINE_DEFAULTS.windowMs);
373
+ const minDurationMs = clampNumber(
374
+ query.minDurationMs,
375
+ 0,
376
+ 6e4,
377
+ VUE_DEVTOOLS_TIMELINE_DEFAULTS.minDurationMs
378
+ );
379
+ const limit = Math.floor(
380
+ clampNumber(query.limit, 1, VUE_DEVTOOLS_TIMELINE_DEFAULTS.maxLimit, VUE_DEVTOOLS_TIMELINE_DEFAULTS.limit)
381
+ );
382
+ const layers = normalizeLayers(query.layers);
383
+ const componentFilter = typeof query.component === "string" ? query.component.toLowerCase() : "";
384
+ const notes = [];
385
+ prunePending(nowAt);
386
+ let from = nowAt - windowMs;
387
+ if (query.sinceMark) {
388
+ const target = findMark(query.sinceMark);
389
+ if (target) {
390
+ from = target.at;
391
+ } else {
392
+ notes.push(
393
+ `\u672A\u627E\u5230 mark\u300C${query.sinceMark}\u300D\uFF08\u9875\u9762\u53EF\u80FD\u5DF2\u6574\u9875\u5237\u65B0\uFF0C\u6216\u6807\u8BB0\u5DF2\u88AB\u4E22\u5F03\uFF09\uFF1B\u5DF2\u9000\u56DE windowMs=${windowMs} \u7684\u7A97\u53E3\u3002`
394
+ );
395
+ }
396
+ }
397
+ const inWindow = records.filter((record) => record.at >= from && layers.has(record.layer));
398
+ const scoped = inWindow.filter((record) => {
399
+ if (!componentFilter) return true;
400
+ if (record.layer === "navigate" || record.layer === "agent") return true;
401
+ return record.name.toLowerCase().includes(componentFilter);
402
+ });
403
+ const windowTruncated = droppedAt !== null && droppedAt >= from;
404
+ if (scoped.length === 0) {
405
+ notes.push(
406
+ "\u7A97\u53E3\u5185\u65E0\u4E8B\u4EF6\u3002\u5E38\u89C1\u539F\u56E0\uFF1A\u7A97\u53E3\u5185\u6CA1\u6709\u4EA4\u4E92\u6216\u6E32\u67D3\uFF1B\u6216\u9875\u9762\u521A\u6574\u9875\u5237\u65B0\u2014\u2014SPA \u8DEF\u7531\u5207\u6362\u4FDD\u7559\u8BB0\u5F55\uFF0C\u6574\u9875\u5237\u65B0\u4F1A\u6E05\u7A7A\u7F13\u51B2\u3002"
407
+ );
408
+ }
409
+ if (!hook) {
410
+ notes.push("\u672A\u53D6\u5F97 Vue DevTools hook\uFF1A\u9875\u9762\u53EF\u80FD\u4E0D\u662F Vue \u9879\u76EE\uFF0C\u6216\u6865\u672A\u521D\u59CB\u5316\u3002");
411
+ }
412
+ if (windowTruncated) {
413
+ notes.push(
414
+ `\u7A97\u53E3\u5185\u6570\u636E\u4E0D\u5B8C\u6574\uFF1A\u7F13\u51B2\u5BB9\u91CF ${capacity} \u6761\uFF0C\u6700\u65E9\u4FDD\u7559\u8BB0\u5F55\u8D77\u4E8E -${round2(nowAt - (records[0]?.at ?? nowAt))}ms\uFF0C\u7A97\u53E3\u8D77\u70B9\u66F4\u65E9\u7684\u90E8\u5206\u5DF2\u88AB\u4E22\u5F03\uFF08\u7D2F\u8BA1\u4E22\u5F03 ${dropped} \u6761\uFF09\u3002`
415
+ );
416
+ }
417
+ if (unpaired > 0 || prunedStarts > 0) {
418
+ const parts = [
419
+ unpaired > 0 ? `${unpaired} \u6B21\u53EA\u6709 end \u6CA1\u6709 start` : "",
420
+ prunedStarts > 0 ? `${prunedStarts} \u6B21 start \u672A\u7B49\u5230 end` : ""
421
+ ].filter(Boolean);
422
+ notes.push(
423
+ `\u6709 ${parts.join("\u3001")}\uFF1A\u8FD9\u4E9B\u6E32\u67D3\u7684\u8017\u65F6\u662F"\u7F3A\u5931"\uFF0C\u4E0D\u662F 0ms\uFF08start \u88AB\u6E05\u7406\u540E\u624D\u6536\u5230 end \u65F6\uFF0C\u540C\u4E00\u6B21\u6E32\u67D3\u4F1A\u540C\u65F6\u8BA1\u5165\u4E24\u5904\uFF09\u3002`
424
+ );
425
+ }
426
+ const omitted = { perfBelowThreshold: 0, byLimit: 0, byPayload: 0 };
427
+ let picked;
428
+ let eligible;
429
+ if (include === VUE_DEVTOOLS_TIMELINE_INCLUDE.SUMMARY) {
430
+ picked = [];
431
+ eligible = 0;
432
+ } else if (include === VUE_DEVTOOLS_TIMELINE_INCLUDE.ALL) {
433
+ picked = scoped.slice(-limit);
434
+ eligible = scoped.length;
435
+ } else {
436
+ const perf = scoped.filter((record) => record.layer === "perf");
437
+ omitted.perfBelowThreshold = perf.filter((record) => (record.dur ?? 0) < minDurationMs).length;
438
+ const slow = perf.filter((record) => (record.dur ?? 0) >= minDurationMs).sort((a, b) => (b.dur ?? 0) - (a.dur ?? 0)).slice(0, limit);
439
+ const rest = scoped.filter((record) => record.layer !== "perf").slice(-limit);
440
+ picked = [...slow, ...rest];
441
+ eligible = perf.length - omitted.perfBelowThreshold + scoped.filter((r) => r.layer !== "perf").length;
442
+ }
443
+ omitted.byLimit = Math.max(0, eligible - picked.length);
444
+ picked.sort((a, b) => a.at - b.at);
445
+ const detail = picked.map((record) => ({
446
+ seq: record.seq,
447
+ t: round2(record.at - nowAt),
448
+ layer: record.layer,
449
+ kind: record.kind,
450
+ name: record.name,
451
+ ...record.dur === void 0 ? {} : { dur: record.dur },
452
+ ...record.nodeId ? { nodeId: record.nodeId } : {},
453
+ ...record.level ? { level: record.level } : {},
454
+ ...record.data === void 0 ? {} : { data: record.data }
455
+ }));
456
+ if (query.enrich !== false && detail.length > 0 && options.getInspectorTree) {
457
+ const files = await fileMap(nowAt);
458
+ for (const record of detail) {
459
+ if (!record.nodeId) continue;
460
+ const file = files.get(record.nodeId);
461
+ if (file) record.file = file;
462
+ }
463
+ }
464
+ const summary = buildSummary(scoped, layers, componentFilter, minDurationMs, nowAt);
465
+ const result = {
466
+ // ms 报实际窗口跨度(sinceMark 会覆盖 windowMs,不能谎报成请求值)
467
+ window: {
468
+ from: round2(from - nowAt),
469
+ to: 0,
470
+ ms: round2(nowAt - from),
471
+ ...query.sinceMark ? { sinceMark: query.sinceMark } : {}
472
+ },
473
+ buffer: {
474
+ capacity,
475
+ size: records.length,
476
+ dropped,
477
+ ...droppedAt === null ? {} : { droppedAtMsAgo: round2(droppedAt - nowAt) },
478
+ windowTruncated,
479
+ unpaired,
480
+ prunedStarts,
481
+ marks: marks.length,
482
+ startedAtMsAgo: round2(nowAt - startedAt),
483
+ bootId,
484
+ capture: { hook: Boolean(hook), routers: routerCount }
485
+ },
486
+ summary,
487
+ records: detail,
488
+ truncated: false,
489
+ detailComplete: true,
490
+ omitted,
491
+ notes
492
+ };
493
+ while (result.records.length > 0 && jsonSize(result) > MAX_PAYLOAD_BYTES) {
494
+ result.records.shift();
495
+ omitted.byPayload += 1;
496
+ }
497
+ result.truncated = omitted.byLimit > 0 || omitted.byPayload > 0;
498
+ result.detailComplete = omitted.perfBelowThreshold === 0 && omitted.byLimit === 0 && omitted.byPayload === 0;
499
+ if (omitted.perfBelowThreshold > 0 || omitted.byLimit > 0 || omitted.byPayload > 0) {
500
+ notes.push(
501
+ `\u660E\u7EC6\u975E\u5168\u91CF\uFF08\u4F4E\u4E8E ${minDurationMs}ms ${omitted.perfBelowThreshold} \u6761\u3001\u8D85\u51FA limit ${omitted.byLimit} \u6761\u3001\u8D85\u4F53\u79EF ${omitted.byPayload} \u6761\uFF09\uFF1B\u6B21\u6570\u4E0E\u8017\u65F6\u4EE5 summary \u4E3A\u51C6\u3002`
502
+ );
503
+ }
504
+ return result;
505
+ function findMark(idOrLabel) {
506
+ for (let index = marks.length - 1; index >= 0; index -= 1) {
507
+ const candidate = marks[index];
508
+ if (candidate && (candidate.id === idOrLabel || candidate.label === idOrLabel)) return candidate;
509
+ }
510
+ return void 0;
511
+ }
512
+ function buildSummary(windowRecords, activeLayers, nameFilter, threshold, reference) {
513
+ const summary2 = {};
514
+ if (activeLayers.has("perf")) {
515
+ const componentsByKey = /* @__PURE__ */ new Map();
516
+ let totalMs = 0;
517
+ const perf = windowRecords.filter((record) => record.layer === "perf");
518
+ for (const record of perf) {
519
+ const dur = record.dur ?? 0;
520
+ totalMs += dur;
521
+ const key = record.nodeId ?? record.name;
522
+ const entry = componentsByKey.get(key) ?? {
523
+ name: record.name,
524
+ ...record.nodeId ? { nodeId: record.nodeId } : {},
525
+ count: 0,
526
+ totalMs: 0,
527
+ maxMs: 0
528
+ };
529
+ entry.count += 1;
530
+ entry.totalMs = round2(entry.totalMs + dur);
531
+ entry.maxMs = Math.max(entry.maxMs, dur);
532
+ componentsByKey.set(key, entry);
533
+ }
534
+ const byComponent = [...componentsByKey.values()].sort((a, b) => b.totalMs - a.totalMs).slice(0, 10);
535
+ const slowest = [...perf].sort((a, b) => (b.dur ?? 0) - (a.dur ?? 0)).slice(0, 5).map((record) => ({
536
+ name: record.name,
537
+ kind: record.kind,
538
+ dur: record.dur ?? 0,
539
+ at: round2(record.at - reference),
540
+ ...record.nodeId ? { nodeId: record.nodeId } : {}
541
+ }));
542
+ summary2.perf = {
543
+ operations: perf.length,
544
+ totalMs: round2(totalMs),
545
+ thresholdMs: threshold,
546
+ byComponent,
547
+ slowest
548
+ };
549
+ }
550
+ if (activeLayers.has("lifecycle")) {
551
+ const stats = [...components.values()].filter(
552
+ (stat) => !nameFilter || stat.name.toLowerCase().includes(nameFilter)
553
+ );
554
+ summary2.lifecycle = {
555
+ sinceMs: round2(reference - startedAt),
556
+ note: "\u7D2F\u8BA1\u8BA1\u6570\uFF08\u81EA\u9875\u9762\u52A0\u8F7D/\u4E0A\u6B21 clear \u8D77\uFF09\uFF0C\u4E0D\u53D7 windowMs \u5F71\u54CD\uFF1Badded/updated/removed \u4E3A\u5168\u9875\u7CBE\u786E\u7D2F\u8BA1\uFF0Ccomponent \u8FC7\u6EE4\u53EA\u4F5C\u7528\u4E8E byComponent",
557
+ // 用精确累计而不是逐组件求和:逐组件表有上限,淘汰后求和会静默少报
558
+ added: lifecycleTotals.added,
559
+ updated: lifecycleTotals.updated,
560
+ removed: lifecycleTotals.removed,
561
+ trackedComponents: components.size,
562
+ // >0 表示组件数超过逐组件表上限,byComponent 只是其中一部分
563
+ componentEvictions,
564
+ byComponent: stats.sort((a, b) => b.updated - a.updated).slice(0, 10).map((stat) => ({
565
+ name: stat.name,
566
+ nodeId: stat.nodeId,
567
+ added: stat.added,
568
+ updated: stat.updated,
569
+ removed: stat.removed
570
+ }))
571
+ };
572
+ }
573
+ if (activeLayers.has("emit")) {
574
+ const emits = /* @__PURE__ */ new Map();
575
+ for (const record of windowRecords.filter((record2) => record2.layer === "emit")) {
576
+ const event = readEventName(record.data);
577
+ const key = `${record.name}|${event}`;
578
+ const entry = emits.get(key) ?? { name: record.name, event, count: 0, at: record.at };
579
+ entry.count += 1;
580
+ entry.at = record.at;
581
+ emits.set(key, entry);
582
+ }
583
+ summary2.emit = [...emits.values()].sort((a, b) => b.count - a.count).slice(0, 10).map((entry) => ({
584
+ name: entry.name,
585
+ event: entry.event,
586
+ count: entry.count,
587
+ at: round2(entry.at - reference)
588
+ }));
589
+ }
590
+ if (activeLayers.has("navigate")) {
591
+ summary2.navigate = windowRecords.filter((record) => record.layer === "navigate").slice(-10).map((record) => ({ ...record.data, at: round2(record.at - reference) }));
592
+ }
593
+ if (activeLayers.has("agent")) {
594
+ summary2.agent = windowRecords.filter((record) => record.layer === "agent").slice(-10).map((record) => ({ label: record.name, at: round2(record.at - reference) }));
595
+ }
596
+ return summary2;
597
+ }
598
+ }
599
+ return { get, mark, clear, attachRouters, dispose };
600
+ }
601
+ function normalizeLayers(input) {
602
+ const allowed = new Set(VUE_DEVTOOLS_TIMELINE_LAYERS);
603
+ if (!Array.isArray(input) || input.length === 0) return allowed;
604
+ const picked = input.filter((layer) => allowed.has(layer));
605
+ return new Set(picked.length > 0 ? picked : allowed);
606
+ }
607
+ function jsonSize(value) {
608
+ try {
609
+ return JSON.stringify(value)?.length ?? 0;
610
+ } catch {
611
+ return Number.POSITIVE_INFINITY;
612
+ }
613
+ }
614
+ export {
615
+ DEVTOOLS_HOOK_EVENTS,
616
+ createTimelineRecorder
617
+ };
@@ -56,6 +56,13 @@ export declare const CUSTOM_TOOLS: CustomTool[];
56
56
  * 页面桥接工具:描述与输入 schema(模型可见)+ 后端 action(调用分发)单一来源。
57
57
  * tools/list 与 tools/call 都取自本定义,避免"清单"与"路由"两份来源各自漂移。
58
58
  * 只在注入 Vue DevTools 桥的项目(Vue 项目)暴露。
59
+ *
60
+ * 描述写法(description 是模型唯一能看到的说明书,会随每次请求注入,所以既要准也要省):
61
+ * 1. 第一行一句话说清"给什么";
62
+ * 2. **何时使用**:正向场景 + 与相邻工具的分工;
63
+ * 3. **返回**:字段与形态(够推理即可,不重复 inputSchema 的参数说明);
64
+ * 4. **注意**:会误导判断的边界必须写明"缺失 ≠ 没有 / ≠ 0";
65
+ * 5. 只承诺实现里真有的能力——被裁剪/丢弃的数据要显式说明,不能让模型按描述去查一个永远为空的东西。
59
66
  */
60
67
  export interface VueDevtoolsTool extends CustomTool {
61
68
  /** 页面桥后端 action(endpoints/vue-devtools.ts executeAction) */