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.
@@ -3,10 +3,12 @@
3
3
  * 注入到用户页面,初始化 @vue/devtools-kit 并暴露到 window.__aipanel_vue
4
4
  * AI 通过 Chrome DevTools evaluate_script 调用此 API
5
5
  *
6
- * API Proxy 自动对读操作做 safeStringify,getInspectorState 额外裁剪 Vue 内部数据
6
+ * - api Proxy:对读操作自动 safeStringify,getInspectorState 额外裁剪 Vue 内部数据;
7
+ * - timeline:页面侧时间线采集器(常驻采集,见 vue-devtools-timeline.ts)。
7
8
  */
8
9
  import { devtools, devtoolsRouter } from "@vue/devtools-kit";
9
- declare function safeStringify(obj: unknown): string;
10
+ import { safeStringify } from "./vue-devtools-sanitize";
11
+ import { type TimelineRecorder } from "./vue-devtools-timeline";
10
12
  declare const safeApi: {
11
13
  getInspectorTree(payload: Pick<import("@vue/devtools-kit").DevToolsV6PluginAPIHookPayloads[import("@vue/devtools-kit").DevToolsV6PluginAPIHookKeys.GET_INSPECTOR_TREE], "inspectorId" | "filter">): Promise<never[]>;
12
14
  getInspectorState(payload: Pick<import("@vue/devtools-kit").DevToolsV6PluginAPIHookPayloads[import("@vue/devtools-kit").DevToolsV6PluginAPIHookKeys.GET_INSPECTOR_STATE], "inspectorId" | "nodeId">): Promise<import("@vue/devtools-kit").CustomInspectorState>;
@@ -52,6 +54,7 @@ declare global {
52
54
  router: typeof devtoolsRouter;
53
55
  ctx: typeof devtools.ctx;
54
56
  safeStringify: typeof safeStringify;
57
+ timeline: TimelineRecorder;
55
58
  };
56
59
  }
57
60
  }
@@ -1,94 +1,8 @@
1
1
  import { devtools, devtoolsRouter } from "@vue/devtools-kit";
2
- const MAX_STRING_LENGTH = 150;
3
- const MAX_DEPTH = 3;
4
- const MAX_KEYS = 20;
5
- const VUE_INTERNAL_KEYS = /* @__PURE__ */ new Set([
6
- "dep",
7
- "subs",
8
- "subsHead",
9
- "deps",
10
- "depsTail",
11
- "activeLink",
12
- "prevActiveLink",
13
- "nextDep",
14
- "prevDep",
15
- "flags",
16
- "globalVersion",
17
- "sc",
18
- "isSSR",
19
- "__v_isRef",
20
- "__v_isReadonly",
21
- "__v_skip",
22
- "computed",
23
- "effect",
24
- "setter",
25
- "fn"
26
- ]);
27
- const SKIP_STATE_TYPES = /* @__PURE__ */ new Set([
28
- "provided",
29
- "injected",
30
- "event listeners",
31
- "template refs"
32
- ]);
33
- function isVueInternalObject(value) {
34
- if (!value || typeof value !== "object") return false;
35
- if (value.__isVue === true) return true;
36
- const keys = Object.keys(value);
37
- const internalCount = keys.filter((k) => VUE_INTERNAL_KEYS.has(k)).length;
38
- return internalCount > 0 && internalCount >= keys.length * 0.5;
39
- }
40
- function sanitizeValue(value, depth = 0) {
41
- if (depth > MAX_DEPTH) return "<max depth>";
42
- if (value === null || value === "__undefined__") return null;
43
- if (typeof value === "string") {
44
- if (value.startsWith("data:") || value.length > MAX_STRING_LENGTH)
45
- return `<${value.slice(0, 50)}... (${value.length} chars)>`;
46
- if (value === "[Circular Reference]" || value === "[Function]") return null;
47
- return value;
48
- }
49
- if (typeof value !== "object") return value;
50
- if (Array.isArray(value)) {
51
- return value.slice(0, MAX_KEYS).map((v) => sanitizeValue(v, depth + 1));
52
- }
53
- if (isVueInternalObject(value)) return null;
54
- const result = {};
55
- const keys = Object.keys(value).filter((k) => !k.startsWith("Symbol(")).slice(0, MAX_KEYS);
56
- for (const key of keys) {
57
- if (VUE_INTERNAL_KEYS.has(key)) continue;
58
- const v = sanitizeValue(value[key], depth + 1);
59
- if (v !== null) result[key] = v;
60
- }
61
- return Object.keys(result).length > 0 ? result : null;
62
- }
63
- function sanitizeState(state) {
64
- const result = {};
65
- for (const item of state) {
66
- if (SKIP_STATE_TYPES.has(item.type)) continue;
67
- if (!result[item.type]) result[item.type] = {};
68
- const value = sanitizeValue(item.value);
69
- if (value !== null) {
70
- result[item.type][item.key] = {
71
- value,
72
- ...item.stateType ? { type: item.stateType } : {}
73
- };
74
- }
75
- }
76
- return result;
77
- }
78
- function safeStringify(obj) {
79
- const seen = /* @__PURE__ */ new WeakSet();
80
- return JSON.stringify(obj, (_key, value) => {
81
- if (typeof value === "object" && value !== null) {
82
- if (seen.has(value)) return "[Circular Reference]";
83
- seen.add(value);
84
- }
85
- if (typeof value === "function") return "[Function]";
86
- if (typeof value === "symbol") return value.toString();
87
- if (typeof value === "bigint") return `${value}n`;
88
- if (value === void 0) return "__undefined__";
89
- return value;
90
- });
91
- }
2
+ import { safeStringify, sanitizeState } from "./vue-devtools-sanitize.mjs";
3
+ import {
4
+ createTimelineRecorder
5
+ } from "./vue-devtools-timeline.mjs";
92
6
  const READ_METHODS = /* @__PURE__ */ new Set([
93
7
  "getInspectorState",
94
8
  "getInspectorTree",
@@ -141,10 +55,30 @@ const safeApi = new Proxy(devtools.api, {
141
55
  };
142
56
  }
143
57
  });
58
+ async function getInspectorTree() {
59
+ return devtools.api.getInspectorTree({ inspectorId: "components", filter: "" });
60
+ }
61
+ function asRouter(value) {
62
+ const router = value;
63
+ return router && typeof router.afterEach === "function" ? router : void 0;
64
+ }
65
+ function resolveRouter(app) {
66
+ const fromApp = asRouter(app?.config?.globalProperties?.$router);
67
+ return fromApp ?? asRouter(devtoolsRouter.value);
68
+ }
144
69
  devtools.init();
70
+ const globalHook = globalThis.__VUE_DEVTOOLS_GLOBAL_HOOK__;
71
+ const timeline = createTimelineRecorder({
72
+ hook: globalHook,
73
+ getAppRecords: () => devtools.ctx.state.appRecords,
74
+ getInspectorTree,
75
+ resolveRouter
76
+ });
77
+ timeline.attachRouters();
145
78
  window.__aipanel_vue = {
146
79
  api: safeApi,
147
80
  router: devtoolsRouter,
148
81
  ctx: devtools.ctx,
149
- safeStringify
82
+ safeStringify,
83
+ timeline
150
84
  };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 注入页面的 Vue DevTools 数据裁剪能力。
3
+ *
4
+ * 桥(vue-devtools-bridge)与时间线采集器(vue-devtools-timeline)共用同一套裁剪规则,
5
+ * 保证任何返回给 agent 的 Vue 数据都经过同样的截断/去内部字段处理。
6
+ */
7
+ import type { InspectorState } from "@vue/devtools-kit";
8
+ /**
9
+ * 对 agent 无意义、被刻意裁掉的状态分类(省 token)。
10
+ * 导出给工具描述做一致性守卫:描述必须显式告诉模型这些分类永远查不到,
11
+ * 否则模型会按描述去查、再把"查不到"读成"不存在"。
12
+ */
13
+ export declare const SKIP_STATE_TYPES: ReadonlySet<string>;
14
+ /**
15
+ * 裁剪后出现的**值级占位记号**(导出给工具描述做一致性守卫)。
16
+ * 不解释它们,模型会把占位读成真实值 —— 例如 `"__undefined__"` 被当成一个字符串 prop。
17
+ * 长字符串另有一种形态:`<前缀... (N chars)>`(见 sanitizeValue)。
18
+ */
19
+ export declare const SANITIZE_PLACEHOLDERS: readonly ["__undefined__", "[Function]", "<max depth>"];
20
+ export declare function sanitizeValue(value: unknown, depth?: number): unknown;
21
+ /** 裁剪 InspectorState[],去掉 Vue 内部数据和噪音类型 */
22
+ export declare function sanitizeState(state: InspectorState[]): Record<string, unknown>;
23
+ /**
24
+ * JSON 序列化:函数/符号/bigint/undefined 换成可读占位,并处理循环引用。
25
+ *
26
+ * 判环用**当前路径上的祖先链**,而不是"见过的所有对象":
27
+ * 同一对象被多处引用是合法结构(DAG)——例如路由记录会被顶层数组与嵌套 children 同时引用
28
+ * (实测 `routes[0].meta === routes[6].children[0].meta` 且不是环)。seen-anywhere 会把它误标成
29
+ * `[Circular Reference]`,agent 读到假值、还以为数据真有环。真环仍标该占位;共享对象**重复输出**而不是被吞掉。
30
+ */
31
+ export declare function safeStringify(obj: unknown): string;
@@ -0,0 +1,103 @@
1
+ const MAX_STRING_LENGTH = 150;
2
+ const MAX_DEPTH = 3;
3
+ const MAX_KEYS = 20;
4
+ const VUE_INTERNAL_KEYS = /* @__PURE__ */ new Set([
5
+ "dep",
6
+ "subs",
7
+ "subsHead",
8
+ "deps",
9
+ "depsTail",
10
+ "activeLink",
11
+ "prevActiveLink",
12
+ "nextDep",
13
+ "prevDep",
14
+ "flags",
15
+ "globalVersion",
16
+ "sc",
17
+ "isSSR",
18
+ "__v_isRef",
19
+ "__v_isReadonly",
20
+ "__v_skip",
21
+ "computed",
22
+ "effect",
23
+ "setter",
24
+ "fn"
25
+ ]);
26
+ const SKIP_STATE_TYPES = /* @__PURE__ */ new Set([
27
+ "provided",
28
+ "injected",
29
+ "event listeners",
30
+ "template refs"
31
+ ]);
32
+ const SANITIZE_PLACEHOLDERS = ["__undefined__", "[Function]", "<max depth>"];
33
+ function isVueInternalObject(value) {
34
+ if (!value || typeof value !== "object") return false;
35
+ if (value.__isVue === true) return true;
36
+ const keys = Object.keys(value);
37
+ const internalCount = keys.filter((k) => VUE_INTERNAL_KEYS.has(k)).length;
38
+ return internalCount > 0 && internalCount >= keys.length * 0.5;
39
+ }
40
+ function sanitizeValue(value, depth = 0) {
41
+ if (depth > MAX_DEPTH) return "<max depth>";
42
+ if (value === null || value === "__undefined__") return null;
43
+ if (typeof value === "string") {
44
+ if (value.startsWith("data:") || value.length > MAX_STRING_LENGTH)
45
+ return `<${value.slice(0, 50)}... (${value.length} chars)>`;
46
+ if (value === "[Circular Reference]" || value === "[Function]") return null;
47
+ return value;
48
+ }
49
+ if (typeof value === "bigint") return `${value}n`;
50
+ if (typeof value === "symbol") return value.toString();
51
+ if (typeof value === "function") return "[Function]";
52
+ if (typeof value !== "object") return value;
53
+ if (Array.isArray(value)) {
54
+ return value.slice(0, MAX_KEYS).map((v) => sanitizeValue(v, depth + 1));
55
+ }
56
+ if (isVueInternalObject(value)) return null;
57
+ const result = {};
58
+ const keys = Object.keys(value).filter((k) => !k.startsWith("Symbol(")).slice(0, MAX_KEYS);
59
+ for (const key of keys) {
60
+ if (VUE_INTERNAL_KEYS.has(key)) continue;
61
+ const v = sanitizeValue(value[key], depth + 1);
62
+ if (v !== null) result[key] = v;
63
+ }
64
+ return Object.keys(result).length > 0 ? result : null;
65
+ }
66
+ function sanitizeState(state) {
67
+ const result = {};
68
+ for (const item of state) {
69
+ if (SKIP_STATE_TYPES.has(item.type)) continue;
70
+ if (!result[item.type]) result[item.type] = {};
71
+ const value = sanitizeValue(item.value);
72
+ if (value !== null) {
73
+ result[item.type][item.key] = {
74
+ value,
75
+ ...item.stateType ? { type: item.stateType } : {}
76
+ };
77
+ }
78
+ }
79
+ return result;
80
+ }
81
+ function safeStringify(obj) {
82
+ const ancestors = [];
83
+ return JSON.stringify(obj, function replacer(_key, value) {
84
+ if (typeof value === "object" && value !== null) {
85
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) ancestors.pop();
86
+ if (ancestors.includes(value)) return "[Circular Reference]";
87
+ ancestors.push(value);
88
+ return value;
89
+ }
90
+ if (typeof value === "function") return "[Function]";
91
+ if (typeof value === "symbol") return value.toString();
92
+ if (typeof value === "bigint") return `${value}n`;
93
+ if (value === void 0) return "__undefined__";
94
+ return value;
95
+ });
96
+ }
97
+ export {
98
+ SANITIZE_PLACEHOLDERS,
99
+ SKIP_STATE_TYPES,
100
+ safeStringify,
101
+ sanitizeState,
102
+ sanitizeValue
103
+ };
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Vue DevTools 时间线采集器(页面侧,供 agent 调试)。
3
+ *
4
+ * 为什么自己实现存储:devtools-kit 的 timeline 事件只转发给已连接的 DevTools 客户端,不落盘
5
+ * (没有 clientConnected 时事件即发即弃),且内置层受 recordingState/highPerfModeEnabled 门控。
6
+ * 本模块直接订阅 `__VUE_DEVTOOLS_GLOBAL_HOOK__` 的原始 Vue 事件(实测不受上述两个门控影响),
7
+ * 在页面内存里维护一个有界环形缓冲,供 `vue-devtools_get_timeline` 按窗口查询。
8
+ *
9
+ * 采集约定:
10
+ * - 常驻采集(无 start/stop 状态):`clear()` 等价于"从现在开始录",`mark()` 只插边界不清历史;
11
+ * - 捕获期就把数据降级为 primitive(绝不持有 app/vm/instance 引用),生命周期事件只累计计数;
12
+ * - 所有 hook 回调都包在 try/catch 里:devtools hook 的 emit 是同步遍历,回调抛错会影响页面渲染。
13
+ */
14
+ import { type VueDevtoolsTimelineInclude, type VueDevtoolsTimelineLayer } from "@aipanel/core";
15
+ /**
16
+ * 原始 devtools hook 的事件名(唯一来源)。
17
+ * @vue/devtools-kit 的 DevToolsHooks 枚举只有 d.ts 声明、运行时未导出(import 值为 undefined),
18
+ * 故无法 import;这里集中定义一次,值对齐 @vue/devtools-kit@8.2.1 的 DevToolsHooks
19
+ * (dist/index.d.ts:718-733),升级该依赖时需同步核对(vue-devtools-timeline.test.ts 锁住配合行为)。
20
+ */
21
+ export declare const DEVTOOLS_HOOK_EVENTS: {
22
+ readonly appInit: "app:init";
23
+ readonly appUnmount: "app:unmount";
24
+ readonly componentAdded: "component:added";
25
+ readonly componentUpdated: "component:updated";
26
+ readonly componentRemoved: "component:removed";
27
+ readonly componentEmit: "component:emit";
28
+ readonly performanceStart: "perf:start";
29
+ readonly performanceEnd: "perf:end";
30
+ };
31
+ /**
32
+ * devtools hook 的监听器签名随事件而异,注册时按动态签名处理(devtools-kit 自己用的是 `Function`)。
33
+ * `never[]` 是下界:任何声明了具体参数的监听器都能注册,但取出来调用时需按事件自行收窄。
34
+ */
35
+ export type HookListener = (...args: never[]) => void;
36
+ /**
37
+ * 原始 devtools hook 的最小结构。
38
+ * 不用 devtools-kit 的 DevToolsHook 类型:其 on() 的事件参数是未导出的枚举类型。
39
+ */
40
+ export interface TimelineHook {
41
+ on(event: string, listener: HookListener): () => void;
42
+ }
43
+ /** Vue 应用实例上与时间线相关的最小形状(避免依赖 Vue 类型) */
44
+ export interface TimelineAppLike {
45
+ _instance?: {
46
+ uid?: number;
47
+ };
48
+ /** devtools-kit 写在 app 上的记录标识;按引用比对 appRecords 会漏(实测出现 ?:uid) */
49
+ __VUE_DEVTOOLS_NEXT_APP_RECORD_ID__?: string;
50
+ __VUE_DEVTOOLS_NEXT_APP_RECORD__?: {
51
+ id?: string;
52
+ };
53
+ }
54
+ /** Vue 组件实例上与 nodeId 相关的最小形状 */
55
+ export interface TimelineInstanceLike {
56
+ uid?: number;
57
+ root?: unknown;
58
+ type?: {
59
+ name?: string;
60
+ __name?: string;
61
+ };
62
+ /** devtools-kit 写在实例上的完整 nodeId(与组件树 id 同源) */
63
+ __VUE_DEVTOOLS_NEXT_UID__?: string;
64
+ /** 挂载早期 app 上还没有记录标识时,用它兜底解析 appId */
65
+ appContext?: {
66
+ app?: TimelineAppLike;
67
+ };
68
+ }
69
+ /** vue-router 实例上与时间线相关的最小形状 */
70
+ export interface TimelineRouterLike {
71
+ afterEach: (handler: (to: unknown, from: unknown, failure?: unknown) => void) => () => void;
72
+ }
73
+ export interface TimelineAppRecord {
74
+ id: string;
75
+ app: unknown;
76
+ }
77
+ /** 时间线明细记录(t 为相对查询时刻的毫秒数,负数表示过去) */
78
+ export interface TimelineRecord {
79
+ seq: number;
80
+ t: number;
81
+ layer: VueDevtoolsTimelineLayer;
82
+ kind: string;
83
+ name: string;
84
+ dur?: number;
85
+ nodeId?: string;
86
+ file?: string;
87
+ level?: "default" | "warning" | "error";
88
+ data?: unknown;
89
+ }
90
+ /** 时间线查询参数(来自 MCP 工具入参,全部可省略) */
91
+ export interface TimelineQuery {
92
+ windowMs?: number;
93
+ sinceMark?: string;
94
+ layers?: string[];
95
+ minDurationMs?: number;
96
+ component?: string;
97
+ include?: VueDevtoolsTimelineInclude;
98
+ limit?: number;
99
+ /** 是否用最新组件树补全 file(默认开,2s 缓存) */
100
+ enrich?: boolean;
101
+ }
102
+ export interface TimelineQueryResult {
103
+ window: {
104
+ from: number;
105
+ to: number;
106
+ ms: number;
107
+ sinceMark?: string;
108
+ };
109
+ buffer: {
110
+ capacity: number;
111
+ size: number;
112
+ dropped: number;
113
+ /**
114
+ * 已丢弃记录里最新一条的时间(相对现在的 ms)。
115
+ * 用它判断"窗口内数据是否被丢弃过":只有当它落在窗口内才说明窗口不完整。
116
+ */
117
+ droppedAtMsAgo?: number;
118
+ /** 窗口起点早于被丢弃的记录 ⇒ 窗口内数据不完整(与 droppedAtMsAgo 配套,避免误报) */
119
+ windowTruncated: boolean;
120
+ /** 只有 end 没有 start 的渲染次数(耗时缺失,不等于 0ms) */
121
+ unpaired: number;
122
+ /** 因长时间没有 end 而被清掉的 start 次数(同样属于耗时缺失) */
123
+ prunedStarts: number;
124
+ marks: number;
125
+ startedAtMsAgo: number;
126
+ bootId: string;
127
+ capture: {
128
+ hook: boolean;
129
+ routers: number;
130
+ };
131
+ };
132
+ summary: Record<string, unknown>;
133
+ records: TimelineRecord[];
134
+ /** 明细被 limit / 体积上限裁剪过(阈值裁剪见 omitted.perfBelowThreshold)——摘要计数不受影响 */
135
+ truncated: boolean;
136
+ /**
137
+ * 明细是否全量:omitted 三项全为 0 才为 true。判断"有没有被裁"只看这一个字段。
138
+ * include=summary 时 records 按请求为空、本字段恒 true(按请求口径,不代表窗口内没有事件)。
139
+ */
140
+ detailComplete: boolean;
141
+ /** 明细裁剪的去向,保证"没看到的不是没发生" */
142
+ omitted: {
143
+ perfBelowThreshold: number;
144
+ byLimit: number;
145
+ byPayload: number;
146
+ };
147
+ notes: string[];
148
+ }
149
+ export interface TimelineRecorderOptions {
150
+ hook?: TimelineHook;
151
+ getAppRecords?: () => TimelineAppRecord[];
152
+ /** 组件树(用于把 nodeId 补成源码文件路径) */
153
+ getInspectorTree?: () => Promise<unknown>;
154
+ /** 由桥决定如何从 Vue 应用取 router(采集器不感知 Vue 内部结构) */
155
+ resolveRouter?: (app: unknown) => TimelineRouterLike | undefined;
156
+ now?: () => number;
157
+ capacity?: number;
158
+ }
159
+ export interface TimelineRecorder {
160
+ get(query?: TimelineQuery): Promise<TimelineQueryResult>;
161
+ mark(label: string): {
162
+ markId: string;
163
+ buffered: number;
164
+ };
165
+ clear(): {
166
+ cleared: number;
167
+ marks: number;
168
+ };
169
+ /** 为已注册的应用补订阅 router(桥注入晚于 app 挂载时的兜底;正常路径由 app:init 覆盖) */
170
+ attachRouters(): void;
171
+ dispose(): void;
172
+ }
173
+ export declare function createTimelineRecorder(options?: TimelineRecorderOptions): TimelineRecorder;