vite-plugin-aipanel 1.2.0-beta.0

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.
Files changed (98) hide show
  1. package/README.md +72 -0
  2. package/es/client/vue-devtools-bridge.d.ts +58 -0
  3. package/es/client/vue-devtools-bridge.mjs +150 -0
  4. package/es/core/injector.d.ts +2 -0
  5. package/es/core/injector.mjs +15 -0
  6. package/es/core/mcp-chrome.d.ts +56 -0
  7. package/es/core/mcp-chrome.mjs +133 -0
  8. package/es/core/mcp-proxy.d.ts +23 -0
  9. package/es/core/mcp-proxy.mjs +212 -0
  10. package/es/core/mcp-tools.d.ts +15 -0
  11. package/es/core/mcp-tools.mjs +671 -0
  12. package/es/core/provider-loader.d.ts +19 -0
  13. package/es/core/provider-loader.mjs +30 -0
  14. package/es/core/proxy-server.d.ts +12 -0
  15. package/es/core/proxy-server.mjs +148 -0
  16. package/es/core/service.d.ts +39 -0
  17. package/es/core/service.mjs +267 -0
  18. package/es/endpoints/context.d.ts +3 -0
  19. package/es/endpoints/context.mjs +106 -0
  20. package/es/endpoints/index.d.ts +9 -0
  21. package/es/endpoints/index.mjs +30 -0
  22. package/es/endpoints/logs.d.ts +9 -0
  23. package/es/endpoints/logs.mjs +86 -0
  24. package/es/endpoints/mcp.d.ts +6 -0
  25. package/es/endpoints/mcp.mjs +610 -0
  26. package/es/endpoints/sessions.d.ts +3 -0
  27. package/es/endpoints/sessions.mjs +62 -0
  28. package/es/endpoints/sse.d.ts +3 -0
  29. package/es/endpoints/sse.mjs +44 -0
  30. package/es/endpoints/start.d.ts +3 -0
  31. package/es/endpoints/start.mjs +21 -0
  32. package/es/endpoints/types.d.ts +34 -0
  33. package/es/endpoints/types.mjs +0 -0
  34. package/es/endpoints/vue-devtools.d.ts +9 -0
  35. package/es/endpoints/vue-devtools.mjs +107 -0
  36. package/es/endpoints/warmup.d.ts +3 -0
  37. package/es/endpoints/warmup.mjs +42 -0
  38. package/es/endpoints/widget.d.ts +3 -0
  39. package/es/endpoints/widget.mjs +36 -0
  40. package/es/env.d.ts +1 -0
  41. package/es/index.d.ts +22 -0
  42. package/es/index.mjs +342 -0
  43. package/es/utils/format-bridge.d.ts +9 -0
  44. package/es/utils/format-bridge.mjs +80 -0
  45. package/es/utils/paths.d.ts +3 -0
  46. package/es/utils/paths.mjs +45 -0
  47. package/es/utils/system.d.ts +3 -0
  48. package/es/utils/system.mjs +76 -0
  49. package/lib/client/vue-devtools-bridge.cjs +150 -0
  50. package/lib/client/vue-devtools-bridge.d.ts +58 -0
  51. package/lib/client.css +2 -0
  52. package/lib/client.js +6950 -0
  53. package/lib/core/injector.cjs +34 -0
  54. package/lib/core/injector.d.ts +2 -0
  55. package/lib/core/mcp-chrome.cjs +161 -0
  56. package/lib/core/mcp-chrome.d.ts +56 -0
  57. package/lib/core/mcp-proxy.cjs +244 -0
  58. package/lib/core/mcp-proxy.d.ts +23 -0
  59. package/lib/core/mcp-tools.cjs +694 -0
  60. package/lib/core/mcp-tools.d.ts +15 -0
  61. package/lib/core/provider-loader.cjs +53 -0
  62. package/lib/core/provider-loader.d.ts +19 -0
  63. package/lib/core/proxy-server.cjs +181 -0
  64. package/lib/core/proxy-server.d.ts +12 -0
  65. package/lib/core/service.cjs +289 -0
  66. package/lib/core/service.d.ts +39 -0
  67. package/lib/endpoints/context.cjs +129 -0
  68. package/lib/endpoints/context.d.ts +3 -0
  69. package/lib/endpoints/index.cjs +58 -0
  70. package/lib/endpoints/index.d.ts +9 -0
  71. package/lib/endpoints/logs.cjs +107 -0
  72. package/lib/endpoints/logs.d.ts +9 -0
  73. package/lib/endpoints/mcp.cjs +624 -0
  74. package/lib/endpoints/mcp.d.ts +6 -0
  75. package/lib/endpoints/sessions.cjs +85 -0
  76. package/lib/endpoints/sessions.d.ts +3 -0
  77. package/lib/endpoints/sse.cjs +67 -0
  78. package/lib/endpoints/sse.d.ts +3 -0
  79. package/lib/endpoints/start.cjs +44 -0
  80. package/lib/endpoints/start.d.ts +3 -0
  81. package/lib/endpoints/types.cjs +15 -0
  82. package/lib/endpoints/types.d.ts +34 -0
  83. package/lib/endpoints/vue-devtools.cjs +132 -0
  84. package/lib/endpoints/vue-devtools.d.ts +9 -0
  85. package/lib/endpoints/warmup.cjs +65 -0
  86. package/lib/endpoints/warmup.d.ts +3 -0
  87. package/lib/endpoints/widget.cjs +69 -0
  88. package/lib/endpoints/widget.d.ts +3 -0
  89. package/lib/env.d.ts +1 -0
  90. package/lib/index.cjs +361 -0
  91. package/lib/index.d.ts +22 -0
  92. package/lib/utils/format-bridge.cjs +102 -0
  93. package/lib/utils/format-bridge.d.ts +9 -0
  94. package/lib/utils/paths.cjs +80 -0
  95. package/lib/utils/paths.d.ts +3 -0
  96. package/lib/utils/system.cjs +110 -0
  97. package/lib/utils/system.d.ts +3 -0
  98. package/package.json +68 -0
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # vite-plugin-aipanel
2
+
3
+ 在 Vite 开发环境中嵌入 AIPanel AI 助手,边聊天边改代码,HMR 实时预览。
4
+
5
+ ## 快速开始
6
+
7
+ ### 前置条件
8
+
9
+ 安装 [OpenCode CLI](https://opencode.ai)(AI 引擎,必须):
10
+
11
+ ```bash
12
+ curl -fsSL https://opencode.ai/install | bash
13
+ ```
14
+
15
+ ### 1. 安装插件
16
+
17
+ ```bash
18
+ npm install -D vite-plugin-aipanel
19
+ ```
20
+
21
+ ### 2. 配置 Vite
22
+
23
+ ```ts
24
+ // vite.config.ts
25
+ import { defineConfig } from "vite";
26
+ import aipanelAssistant from "vite-plugin-aipanel";
27
+
28
+ export default defineConfig({
29
+ plugins: [aipanelAssistant()],
30
+ });
31
+ ```
32
+
33
+ ### 3. 启动开发服务器
34
+
35
+ ```bash
36
+ npm run dev
37
+ ```
38
+
39
+ Vite 插件会自动启动 AIPanel Web 服务并创建当前项目的 AI 会话。
40
+
41
+ ### 4. 安装浏览器扩展
42
+
43
+ 1. [下载扩展包](https://github.com/code-farmer-i/vite-plugin-aipanel/raw/main/packages/extension/aipanel-assistant.zip)
44
+ 2. 打开 Chrome,地址栏输入 `chrome://extensions/`
45
+ 3. 打开右上角**「开发者模式」**开关
46
+ 4. 解压下载的 `.zip`,点击**「加载已解压的扩展程序」**,选择解压后的文件夹
47
+ 5. 用 Chrome 打开 `localhost` 开发页面,点击工具栏中的 AIPanel 图标即可开始对话
48
+
49
+ > Edge / Arc / Brave 操作步骤相同,入口分别是 `edge://extensions/` / `arc://extensions/` / `brave://extensions/`。
50
+
51
+ ## 工作原理
52
+
53
+ ```
54
+ ┌────────────────┐ ┌────────────────┐ ┌─────────────┐
55
+ │ Vite 插件 │ │ 浏览器扩展 │ │ OpenCode │
56
+ │ 启动 Web 服务 │◀────▶│ 侧边栏面板 │◀────▶│ AI 引擎 │
57
+ │ 代理跨域 │ │ 页面上下文同步 │ │ 会话管理 │
58
+ └────────────────┘ └────────────────┘ └─────────────┘
59
+ ```
60
+
61
+ - Vite 插件在项目启动时自动拉起 AIPanel Web 服务
62
+ - 浏览器扩展检测到 `localhost` 页面后自动连接该服务
63
+ - 实时同步页面 URL、标题等上下文给 AI
64
+ - 多标签页自动切换对应项目的会话
65
+
66
+ ## 文档
67
+
68
+ 完整使用指南请访问 [在线文档](https://code-farmer-i.github.io/vite-plugin-aipanel/)。
69
+
70
+ ## License
71
+
72
+ MIT
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Vue DevTools 桥接脚本
3
+ * 注入到用户页面,初始化 @vue/devtools-kit 并暴露到 window.__aipanel_vue
4
+ * AI 通过 Chrome DevTools evaluate_script 调用此 API
5
+ *
6
+ * API Proxy 自动对读操作做 safeStringify,getInspectorState 额外裁剪 Vue 内部数据
7
+ */
8
+ import { devtools, devtoolsRouter } from "@vue/devtools-kit";
9
+ declare function safeStringify(obj: unknown): string;
10
+ declare const safeApi: {
11
+ getInspectorTree(payload: Pick<import("@vue/devtools-kit").DevToolsV6PluginAPIHookPayloads[import("@vue/devtools-kit").DevToolsV6PluginAPIHookKeys.GET_INSPECTOR_TREE], "inspectorId" | "filter">): Promise<never[]>;
12
+ getInspectorState(payload: Pick<import("@vue/devtools-kit").DevToolsV6PluginAPIHookPayloads[import("@vue/devtools-kit").DevToolsV6PluginAPIHookKeys.GET_INSPECTOR_STATE], "inspectorId" | "nodeId">): Promise<import("@vue/devtools-kit").CustomInspectorState>;
13
+ editInspectorState(payload: import("@vue/devtools-kit").DevToolsV6PluginAPIHookPayloads[import("@vue/devtools-kit").DevToolsV6PluginAPIHookKeys.EDIT_INSPECTOR_STATE]): void;
14
+ sendInspectorState(inspectorId: string): void;
15
+ inspectComponentInspector(): Promise<string>;
16
+ cancelInspectComponentInspector(): void;
17
+ getComponentRenderCode(id: string): any;
18
+ scrollToComponent(id: string): void;
19
+ openInEditor: typeof import("@vue/devtools-kit").openInEditor;
20
+ getVueInspector: typeof import("@vue/devtools-kit").getComponentInspector;
21
+ toggleApp(id: string, options?: {
22
+ inspectingComponent?: boolean;
23
+ }): void;
24
+ inspectDOM(instanceId: string): void;
25
+ updatePluginSettings(pluginId: string, key: string, value: string): void;
26
+ getPluginSettings(pluginId: string): {
27
+ options: Record<string, {
28
+ label: string;
29
+ description?: string;
30
+ } & ({
31
+ type: "boolean";
32
+ defaultValue: boolean;
33
+ } | {
34
+ type: "choice";
35
+ defaultValue: string | number;
36
+ options: {
37
+ value: string | number;
38
+ label: string;
39
+ }[];
40
+ component?: "select" | "button-group";
41
+ } | {
42
+ type: "text";
43
+ defaultValue: string;
44
+ })> | null;
45
+ values: any;
46
+ };
47
+ };
48
+ declare global {
49
+ interface Window {
50
+ __aipanel_vue: {
51
+ api: typeof safeApi;
52
+ router: typeof devtoolsRouter;
53
+ ctx: typeof devtools.ctx;
54
+ safeStringify: typeof safeStringify;
55
+ };
56
+ }
57
+ }
58
+ export {};
@@ -0,0 +1,150 @@
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
+ }
92
+ const READ_METHODS = /* @__PURE__ */ new Set([
93
+ "getInspectorState",
94
+ "getInspectorTree",
95
+ "devtoolsState",
96
+ "getRouterInfo",
97
+ "getComponentRenderCode"
98
+ ]);
99
+ async function isNodeIdValid(nodeId) {
100
+ const targetId = String(nodeId);
101
+ try {
102
+ const tree = await devtools.api.getInspectorTree({
103
+ inspectorId: "components",
104
+ filter: ""
105
+ });
106
+ const find = (nodes) => nodes.some((n) => n.id === targetId || find(n.children ?? []));
107
+ return find(tree);
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+ const safeApi = new Proxy(devtools.api, {
113
+ get(target, prop, receiver) {
114
+ const original = Reflect.get(target, prop, receiver);
115
+ if (typeof original !== "function" || !READ_METHODS.has(prop))
116
+ return original;
117
+ return async (...args) => {
118
+ if (prop === "getInspectorState") {
119
+ const nodeId = args[0]?.nodeId;
120
+ if (!await isNodeIdValid(nodeId)) {
121
+ return safeStringify({
122
+ error: `\u7EC4\u4EF6 ${String(nodeId)} \u5DF2\u5931\u6548\uFF08\u9875\u9762\u53EF\u80FD\u5DF2\u5237\u65B0\u6216\u7EC4\u4EF6\u5DF2\u5378\u8F7D\uFF09\uFF0C\u8BF7\u91CD\u65B0\u8C03\u7528 vue-devtools_get_component_tree \u83B7\u53D6\u6700\u65B0 nodeId \u540E\u518D\u8BD5\u3002`
123
+ });
124
+ }
125
+ }
126
+ try {
127
+ const raw = await original.apply(target, args);
128
+ if (prop === "getInspectorState") {
129
+ return safeStringify({
130
+ state: sanitizeState(
131
+ raw?.state ?? raw
132
+ )
133
+ });
134
+ }
135
+ return safeStringify(raw);
136
+ } catch (error) {
137
+ return safeStringify({
138
+ error: `\u7EC4\u4EF6\u72B6\u6001\u83B7\u53D6\u5931\u8D25\uFF08${error.message}\uFF09\u3002\u9875\u9762\u53EF\u80FD\u5DF2\u5237\u65B0\uFF0C\u8BF7\u91CD\u65B0\u8C03\u7528 vue-devtools_get_component_tree \u83B7\u53D6\u6700\u65B0 nodeId \u540E\u518D\u8BD5\u3002`
139
+ });
140
+ }
141
+ };
142
+ }
143
+ });
144
+ devtools.init();
145
+ window.__aipanel_vue = {
146
+ api: safeApi,
147
+ router: devtoolsRouter,
148
+ ctx: devtools.ctx,
149
+ safeStringify
150
+ };
@@ -0,0 +1,2 @@
1
+ import type { WidgetOptions } from "@aipanel/core";
2
+ export declare function injectWidget(options: WidgetOptions): string;
@@ -0,0 +1,15 @@
1
+ import {
2
+ CONFIG_DATA_ATTR,
3
+ WIDGET_SCRIPT_PATH,
4
+ WIDGET_STYLE_PATH
5
+ } from "@aipanel/core";
6
+ function injectWidget(options) {
7
+ const configBase64 = Buffer.from(JSON.stringify(options)).toString("base64");
8
+ const scriptTag = `<script type="module" src="${WIDGET_SCRIPT_PATH}" ${CONFIG_DATA_ATTR}="${configBase64}"></script>`;
9
+ const styleTag = `<link rel="stylesheet" href="${WIDGET_STYLE_PATH}" />`;
10
+ return `${styleTag}
11
+ ${scriptTag}`;
12
+ }
13
+ export {
14
+ injectWidget
15
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * MCP Chrome 页面相关工具
3
+ *
4
+ * 专注于 Chrome DevTools 页面匹配与解析逻辑
5
+ */
6
+ import type { ViteDevServer } from "vite";
7
+ import type { McpProxy } from "./mcp-proxy";
8
+ /** 从 Vite 服务器解析的所有 URL 中提取项目 origin 列表 */
9
+ export declare function getProjectOrigins(server: ViteDevServer): string[];
10
+ /** 判断页面 URL 是否属于项目的某个 origin */
11
+ export declare function isProjectPage(url: string, origins: string[]): boolean;
12
+ export interface PageInfo {
13
+ pageId: number;
14
+ url: string;
15
+ title: string;
16
+ /** Chrome DevTools 当前选中的页面 */
17
+ selected: boolean;
18
+ }
19
+ /** 解析 list_pages 返回的文本。格式:"56: Title (URL) [selected]" */
20
+ export declare function parseListPages(text: string): PageInfo[];
21
+ /** 从 evaluate_script 返回的格式化文本中提取 JSON 值 */
22
+ export declare function extractEvalValue(text: string | undefined): string | undefined;
23
+ type PageIdResult = {
24
+ ok: true;
25
+ pageId: number;
26
+ } | {
27
+ ok: false;
28
+ error: string;
29
+ };
30
+ /**
31
+ * 校验 pageId 是否属于项目页面
32
+ *
33
+ * 调用 list_pages → 过滤项目页面 → 检查 pageId 是否在范围内。
34
+ * 供 MCP 代理层和 Vue DevTools 端点共用。
35
+ */
36
+ export declare function validatePageId(mcp: McpProxy, pageId: number, projectOrigins: string[]): Promise<{
37
+ valid: true;
38
+ projectPages: PageInfo[];
39
+ } | {
40
+ valid: false;
41
+ error: string;
42
+ projectPages: PageInfo[];
43
+ }>;
44
+ /**
45
+ * 通过 MCP 解析当前页面对应的 Chrome DevTools pageId
46
+ *
47
+ * 策略:
48
+ * 1. list_pages → 解析每行的 "pageId: title (URL)" 格式
49
+ * 2. 仅用 sessionId(_aipanel_pk)匹配,跨导航可靠;不做 URL 降级匹配
50
+ *
51
+ * 失败时返回具体原因,由调用方透传给 Agent。
52
+ */
53
+ export declare function resolveChromePageId(mcp: McpProxy | undefined, url: string, title: string, projectOrigins: string[], sessionId?: string, pages?: PageInfo[],
54
+ /** Chrome 当前选中的 pageId(调用方传入,避免过滤后丢失非项目页面信息) */
55
+ chromeSelectedPageId?: number): Promise<PageIdResult>;
56
+ export {};
@@ -0,0 +1,133 @@
1
+ import { createLogger } from "@aipanel/core/node";
2
+ const log = createLogger("McpChrome");
3
+ function getProjectOrigins(server) {
4
+ const local = server.resolvedUrls?.local ?? [];
5
+ const network = server.resolvedUrls?.network ?? [];
6
+ const origins = [...new Set([...local, ...network].map((u) => new URL(u).origin))];
7
+ log.debug("project origins", { origins });
8
+ return origins;
9
+ }
10
+ function isProjectPage(url, origins) {
11
+ return origins.some((origin) => url.startsWith(origin));
12
+ }
13
+ function parseListPages(text) {
14
+ const pages = [];
15
+ for (const line of text.split("\n")) {
16
+ const match = line.match(/^(\d+):\s*(.+?)\s*\((\S+)\)/);
17
+ if (match) {
18
+ pages.push({
19
+ pageId: parseInt(match[1], 10),
20
+ title: match[2].replace(/\s*\.{3}$/, "").trim(),
21
+ url: match[3],
22
+ selected: /\[selected\]/i.test(line)
23
+ });
24
+ }
25
+ }
26
+ return pages;
27
+ }
28
+ function extractEvalValue(text) {
29
+ if (!text) return void 0;
30
+ const marker = "```json\n";
31
+ const startIdx = text.indexOf(marker);
32
+ if (startIdx < 0) return text.trim();
33
+ const contentStart = startIdx + marker.length;
34
+ const endIdx = text.indexOf("\n```", contentStart);
35
+ const jsonStr = endIdx < 0 ? text.substring(contentStart) : text.substring(contentStart, endIdx);
36
+ try {
37
+ return JSON.parse(jsonStr.trim());
38
+ } catch {
39
+ return jsonStr.trim();
40
+ }
41
+ }
42
+ async function validatePageId(mcp, pageId, projectOrigins) {
43
+ const listResult = await mcp.callChromeDevTool("list_pages", {});
44
+ if (listResult?.error || !listResult?.result) {
45
+ return {
46
+ valid: false,
47
+ error: listResult?.error?.message ?? "\u65E0\u6CD5\u83B7\u53D6 Chrome \u9875\u9762\u5217\u8868\uFF0C\u8BF7\u786E\u8BA4 Chrome DevTools \u5DF2\u8FDE\u63A5",
48
+ projectPages: []
49
+ };
50
+ }
51
+ const text = listResult?.result?.content?.[0]?.text;
52
+ const allPages = text ? parseListPages(text) : [];
53
+ const projectPages = allPages.filter((p) => isProjectPage(p.url, projectOrigins));
54
+ const isValid = projectPages.some((p) => p.pageId === pageId);
55
+ if (!isValid) {
56
+ return {
57
+ valid: false,
58
+ error: `pageId ${pageId} \u65E0\u6548\u6216\u975E\u9879\u76EE\u9875\u9762\uFF0C\u8BF7\u83B7\u53D6\u6709\u6548\u9875\u9762 ID`,
59
+ projectPages
60
+ };
61
+ }
62
+ return { valid: true, projectPages };
63
+ }
64
+ async function resolveChromePageId(mcp, url, title, projectOrigins, sessionId, pages, chromeSelectedPageId) {
65
+ if (!mcp || !mcp.isRunning) {
66
+ const reason = !mcp ? "MCP \u6A21\u5757\u672A\u521D\u59CB\u5316" : "Chrome DevTools MCP \u8FDB\u7A0B\u672A\u542F\u52A8";
67
+ log.debug(`resolveChromePageId: ${reason}`);
68
+ return { ok: false, error: reason };
69
+ }
70
+ if (!url) {
71
+ return { ok: false, error: "\u9875\u9762 URL \u4E3A\u7A7A\uFF0C\u5C1A\u672A\u6536\u5230\u4E0A\u4E0B\u6587\u4FE1\u606F" };
72
+ }
73
+ try {
74
+ if (!pages) {
75
+ const listResult = await mcp.callChromeDevTool("list_pages", {});
76
+ const text = listResult?.result?.content?.[0]?.text;
77
+ if (!text) {
78
+ return { ok: false, error: "\u65E0\u6CD5\u83B7\u53D6 Chrome \u9875\u9762\u5217\u8868\uFF0C\u8BF7\u786E\u8BA4\u5DF2\u6253\u5F00\u76EE\u6807\u9875\u9762" };
79
+ }
80
+ const allPages = parseListPages(text);
81
+ chromeSelectedPageId = allPages.find((p) => p.selected)?.pageId;
82
+ pages = allPages.filter((p) => isProjectPage(p.url, projectOrigins));
83
+ }
84
+ log.debug("resolveChromePageId: list_pages result", {
85
+ pages: pages.map((p) => ({ id: p.pageId, url: p.url, title: p.title.substring(0, 40) })),
86
+ target: { url, title }
87
+ });
88
+ if (sessionId) {
89
+ let matchedPageId = null;
90
+ for (const page of pages) {
91
+ await mcp.callChromeDevTool("select_page", { pageId: page.pageId, bringToFront: false });
92
+ const evalResult = await mcp.callChromeDevTool("evaluate_script", {
93
+ function: "() => sessionStorage.getItem('_aipanel_pk')"
94
+ });
95
+ const rawText = evalResult?.result?.content?.[0]?.text;
96
+ const extracted = extractEvalValue(rawText);
97
+ if (extracted === sessionId) {
98
+ matchedPageId = page.pageId;
99
+ break;
100
+ }
101
+ }
102
+ if (chromeSelectedPageId != null && chromeSelectedPageId !== matchedPageId) {
103
+ await mcp.callChromeDevTool("select_page", {
104
+ pageId: chromeSelectedPageId,
105
+ bringToFront: false
106
+ });
107
+ }
108
+ if (matchedPageId != null) {
109
+ return { ok: true, pageId: matchedPageId };
110
+ }
111
+ return {
112
+ ok: false,
113
+ error: `\u672A\u80FD\u901A\u8FC7\u4F1A\u8BDD\u6807\u8BC6\u627E\u5230\u76EE\u6807\u9875\u9762\uFF0C\u8BF7\u786E\u8BA4\u76EE\u6807\u9875\u9762\u5DF2\u6253\u5F00`
114
+ };
115
+ }
116
+ return {
117
+ ok: false,
118
+ error: `\u4E0A\u4E0B\u6587\u7F3A\u5C11\u4F1A\u8BDD\u6807\u8BC6\uFF08_aipanel_pk\uFF09\uFF0C\u65E0\u6CD5\u5B9A\u4F4D\u9875\u9762\uFF0C\u8BF7\u5237\u65B0\u76EE\u6807\u9875\u9762\u540E\u91CD\u8BD5`
119
+ };
120
+ } catch (err) {
121
+ const msg = `MCP \u8C03\u7528\u5931\u8D25: ${err.message}`;
122
+ log.debug("Failed to resolve pageId via MCP", { error: err.message });
123
+ return { ok: false, error: msg };
124
+ }
125
+ }
126
+ export {
127
+ extractEvalValue,
128
+ getProjectOrigins,
129
+ isProjectPage,
130
+ parseListPages,
131
+ resolveChromePageId,
132
+ validatePageId
133
+ };
@@ -0,0 +1,23 @@
1
+ export interface McpProxyOptions {
2
+ args?: string[];
3
+ idleTimeout?: number;
4
+ }
5
+ export declare class McpProxy {
6
+ #private;
7
+ readonly sessionId: string;
8
+ constructor(options?: McpProxyOptions);
9
+ get isRunning(): boolean;
10
+ start(): Promise<void>;
11
+ /** 转发原始 JSON-RPC 请求,保留客户端 ID */
12
+ forward(rawRequest: string): Promise<string>;
13
+ /** 调用 MCP 工具 */
14
+ call(method: string, params?: Record<string, unknown>): Promise<unknown>;
15
+ /** 直接调用 chrome-devtools-mcp 底层工具(内部使用) */
16
+ callChromeDevTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
17
+ /** 验证 MCP 是否可用 + 预热 CDP 连接 */
18
+ verify(): Promise<{
19
+ ok: boolean;
20
+ error?: string;
21
+ }>;
22
+ stop(): void;
23
+ }