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
@@ -0,0 +1,30 @@
1
+ const PROVIDER_PACKAGES = {
2
+ default: "@aipanel/provider-opencode",
3
+ opencode: "@aipanel/provider-opencode",
4
+ deepseek: "@aipanel/provider-deepseek"
5
+ };
6
+ async function loadProvider(id, ctx) {
7
+ const pkg = PROVIDER_PACKAGES[id];
8
+ if (!pkg) {
9
+ throw new Error(
10
+ `\u672A\u77E5\u7684 Web Provider: "${id}"\u3002\u53EF\u7528 Provider: ${Object.keys(PROVIDER_PACKAGES).join(", ") || "(\u65E0)"}`
11
+ );
12
+ }
13
+ let mod;
14
+ try {
15
+ mod = await import(pkg);
16
+ } catch (e) {
17
+ const error = new Error(
18
+ `\u52A0\u8F7D Web Provider \u5305 "${pkg}" \u5931\u8D25\uFF08\u8BF7\u786E\u8BA4\u5DF2\u6B63\u786E\u5B89\u88C5\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}`
19
+ );
20
+ error.cause = e;
21
+ throw error;
22
+ }
23
+ if (typeof mod.createProvider !== "function") {
24
+ throw new Error(`Provider \u5305 "${pkg}" \u672A\u5BFC\u51FA createProvider \u5DE5\u5382`);
25
+ }
26
+ return mod.createProvider(ctx);
27
+ }
28
+ export {
29
+ loadProvider
30
+ };
@@ -0,0 +1,12 @@
1
+ import http from "http";
2
+ export interface ProxyServerOptions {
3
+ /** 注入到 HTML 的 Provider 侧桥接脚本(Provider 资产) */
4
+ bridgeScript?: string;
5
+ /** 绑定地址,需与端口检查使用的地址族一致,避免 IPv4/IPv6 不匹配 */
6
+ hostname?: string;
7
+ }
8
+ export interface ProxyServerResult {
9
+ server: http.Server;
10
+ actualPort: number;
11
+ }
12
+ export declare function startProxyServer(targetUrl: string, port: number, options?: ProxyServerOptions): Promise<ProxyServerResult>;
@@ -0,0 +1,148 @@
1
+ import http from "http";
2
+ import { createLogger, BRIDGE_SCRIPT_PATH } from "@aipanel/core";
3
+ const log = createLogger("ProxyServer");
4
+ function startProxyServer(targetUrl, port, options = {}) {
5
+ return new Promise((resolve, reject) => {
6
+ const target = new URL(targetUrl);
7
+ const bridgeScript = options.bridgeScript ?? "";
8
+ const agent = new http.Agent({
9
+ keepAlive: true,
10
+ keepAliveMsecs: 3e4,
11
+ maxSockets: 50,
12
+ maxFreeSockets: 10,
13
+ timeout: 0
14
+ });
15
+ const server = http.createServer((req, res) => {
16
+ if (req.url === BRIDGE_SCRIPT_PATH) {
17
+ const body = bridgeScript;
18
+ res.writeHead(200, {
19
+ "content-type": "application/javascript; charset=utf-8",
20
+ "cache-control": "no-store",
21
+ "content-length": Buffer.byteLength(body)
22
+ });
23
+ res.end(body);
24
+ return;
25
+ }
26
+ const requestOptions = {
27
+ agent,
28
+ hostname: target.hostname,
29
+ port: target.port,
30
+ path: req.url,
31
+ method: req.method,
32
+ headers: {
33
+ ...req.headers,
34
+ host: target.host,
35
+ // 上游(如 dsh 的 trust fence)校验 Origin 必须匹配自身 origin;
36
+ // 页面经代理访问时浏览器发的 Origin 是代理端口,须改写为目标 origin 否则 403
37
+ origin: `http://${target.host}`,
38
+ "accept-encoding": "identity"
39
+ },
40
+ timeout: 0
41
+ };
42
+ const proxyReq = http.request(requestOptions, (proxyRes) => {
43
+ const rawContentType = proxyRes.headers["content-type"];
44
+ const contentType = Array.isArray(rawContentType) ? rawContentType[0] ?? "" : rawContentType ?? "";
45
+ if (contentType.includes("text/html")) {
46
+ const chunks = [];
47
+ proxyRes.on("data", (chunk) => {
48
+ chunks.push(chunk);
49
+ });
50
+ proxyRes.on("end", () => {
51
+ let body = Buffer.concat(chunks).toString("utf-8");
52
+ if (body.match(/<\/head>/i)) {
53
+ body = body.replace(
54
+ /<\/head>/i,
55
+ `<script src="${BRIDGE_SCRIPT_PATH}"></script></head>`
56
+ );
57
+ } else if (body.match(/<\/body>/i)) {
58
+ body = body.replace(
59
+ /<\/body>/i,
60
+ `<script src="${BRIDGE_SCRIPT_PATH}"></script></body>`
61
+ );
62
+ } else {
63
+ body += `<script src="${BRIDGE_SCRIPT_PATH}"></script>`;
64
+ }
65
+ const headers = {};
66
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
67
+ if (value !== void 0 && key !== "content-encoding" && key !== "transfer-encoding" && key !== "content-length") {
68
+ headers[key] = value;
69
+ }
70
+ }
71
+ headers["content-length"] = Buffer.byteLength(body);
72
+ res.writeHead(proxyRes.statusCode || 200, headers);
73
+ res.end(body);
74
+ });
75
+ } else {
76
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
77
+ proxyRes.pipe(res);
78
+ }
79
+ });
80
+ proxyReq.on("error", (err) => {
81
+ log.error("Proxy error", { error: err.message, url: req.url });
82
+ res.writeHead(502);
83
+ res.end("Proxy error");
84
+ });
85
+ proxyReq.on("socket", (socket) => {
86
+ socket.setTimeout(0);
87
+ });
88
+ req.on("socket", (socket) => {
89
+ socket.setTimeout(0);
90
+ });
91
+ req.pipe(proxyReq);
92
+ });
93
+ server.on("error", (err) => {
94
+ reject(err);
95
+ });
96
+ server.on("upgrade", (req, clientSocket, head) => {
97
+ const upgradeOptions = {
98
+ hostname: target.hostname,
99
+ port: target.port,
100
+ path: req.url,
101
+ method: req.method,
102
+ // upgrade 不复用 keep-alive 连接池
103
+ agent: false,
104
+ headers: {
105
+ ...req.headers,
106
+ host: target.host,
107
+ // 与 HTTP 分支一致:改写 Origin 为目标 origin,通过 dsh 的 trust fence
108
+ origin: `http://${target.host}`
109
+ }
110
+ };
111
+ const proxyReq = http.request(upgradeOptions);
112
+ proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => {
113
+ proxySocket.removeAllListeners("data");
114
+ proxySocket.setTimeout(0);
115
+ clientSocket.setTimeout(0);
116
+ const headBuf = [
117
+ `HTTP/1.1 ${proxyRes.statusCode} ${proxyRes.statusMessage}`,
118
+ ...Object.entries(proxyRes.headers).map(([k, v]) => `${k}: ${v}`),
119
+ "",
120
+ ""
121
+ ].join("\r\n");
122
+ clientSocket.write(headBuf);
123
+ if (head?.length) proxySocket.write(head);
124
+ if (proxyHead?.length) clientSocket.write(proxyHead);
125
+ proxySocket.pipe(clientSocket);
126
+ clientSocket.pipe(proxySocket);
127
+ proxySocket.on("close", () => clientSocket.destroy());
128
+ clientSocket.on("close", () => proxySocket.destroy());
129
+ });
130
+ proxyReq.on("error", (err) => {
131
+ log.warn("Proxy websocket upgrade failed", { error: err.message, url: req.url });
132
+ clientSocket.destroy();
133
+ });
134
+ proxyReq.end();
135
+ });
136
+ server.timeout = 0;
137
+ server.keepAliveTimeout = 0;
138
+ server.listen(port, options.hostname || void 0, () => {
139
+ const address = server.address();
140
+ const actualPort = typeof address === "object" && address ? address.port : port;
141
+ log.debug(`Proxy server started on port ${actualPort} -> ${targetUrl}`);
142
+ resolve({ server, actualPort });
143
+ });
144
+ });
145
+ }
146
+ export {
147
+ startProxyServer
148
+ };
@@ -0,0 +1,39 @@
1
+ import type { ResultPromise } from "execa";
2
+ import type http from "http";
3
+ import type { PluginOptions, ServiceStartupTask, WebProvider } from "@aipanel/core";
4
+ import { ChromeMcpWarmupErrorType } from "@aipanel/core";
5
+ import type { McpProxy } from "./mcp-proxy";
6
+ export declare class AIPanelService {
7
+ private config;
8
+ private sseClients;
9
+ private onPortAllocated;
10
+ private onProxyPortAllocated;
11
+ webProcess: ResultPromise | null;
12
+ actualWebPort: number;
13
+ actualProxyPort: number;
14
+ isStarted: boolean;
15
+ private startPromise;
16
+ private proxyServer;
17
+ chromeMcpWarmupFailed: boolean;
18
+ chromeMcpWarmupErrorType: ChromeMcpWarmupErrorType | null;
19
+ chromeMcpWarmupErrorMessage: string | null;
20
+ currentTask: {
21
+ task: ServiceStartupTask;
22
+ data?: Record<string, unknown>;
23
+ } | null;
24
+ workspaceRoot: string | null;
25
+ private mcp;
26
+ private provider;
27
+ private unsubscribeEvents;
28
+ constructor(config: Required<PluginOptions>, sseClients: Set<http.ServerResponse>, onPortAllocated: (port: number) => void, onProxyPortAllocated: (port: number) => void);
29
+ /** 设置 Provider(由编排层动态加载后调用) */
30
+ setProvider(provider: WebProvider): void;
31
+ private sendTaskUpdate;
32
+ start(vitePort: number, corsOrigins: string[], contextApiUrl: string, logsApiUrl: string, viteOrigin: string, mcp: McpProxy, vueDevtoolsApiUrl?: string): Promise<void>;
33
+ retryWarmupChromeMcp(): Promise<{
34
+ success: boolean;
35
+ errorType?: string;
36
+ errorMessage?: string;
37
+ }>;
38
+ stop(): Promise<void>;
39
+ }
@@ -0,0 +1,267 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { DEFAULT_PROXY_PORT, SERVER_START_TIMEOUT, ChromeMcpWarmupErrorType } from "@aipanel/core";
5
+ import { createLogger, findAvailablePort } from "@aipanel/core/node";
6
+ import { findGitRoot, waitForServer } from "../utils/system.mjs";
7
+ import { startProxyServer } from "./proxy-server.mjs";
8
+ const log = createLogger("Service");
9
+ class AIPanelService {
10
+ constructor(config, sseClients, onPortAllocated, onProxyPortAllocated) {
11
+ __publicField(this, "config", config);
12
+ __publicField(this, "sseClients", sseClients);
13
+ __publicField(this, "onPortAllocated", onPortAllocated);
14
+ __publicField(this, "onProxyPortAllocated", onProxyPortAllocated);
15
+ __publicField(this, "webProcess", null);
16
+ __publicField(this, "actualWebPort");
17
+ __publicField(this, "actualProxyPort");
18
+ __publicField(this, "isStarted", false);
19
+ __publicField(this, "startPromise", null);
20
+ __publicField(this, "proxyServer", null);
21
+ __publicField(this, "chromeMcpWarmupFailed", false);
22
+ __publicField(this, "chromeMcpWarmupErrorType", null);
23
+ __publicField(this, "chromeMcpWarmupErrorMessage", null);
24
+ __publicField(this, "currentTask", null);
25
+ __publicField(this, "workspaceRoot", null);
26
+ __publicField(this, "mcp", null);
27
+ __publicField(this, "provider", null);
28
+ __publicField(this, "unsubscribeEvents", null);
29
+ this.actualWebPort = config.webPort;
30
+ this.actualProxyPort = config.proxyPort ?? DEFAULT_PROXY_PORT;
31
+ }
32
+ /** 设置 Provider(由编排层动态加载后调用) */
33
+ setProvider(provider) {
34
+ this.provider = provider;
35
+ }
36
+ sendTaskUpdate(task, data) {
37
+ this.currentTask = { task, ...data };
38
+ this.sseClients.forEach((client) => {
39
+ try {
40
+ client.write(`data: ${JSON.stringify({ type: "TASK_UPDATE", task, ...data })}
41
+
42
+ `);
43
+ } catch (e) {
44
+ log.debug("Failed to send TASK_UPDATE event", { error: e });
45
+ }
46
+ });
47
+ }
48
+ async start(vitePort, corsOrigins, contextApiUrl, logsApiUrl, viteOrigin, mcp, vueDevtoolsApiUrl) {
49
+ if (this.isStarted && this.webProcess) {
50
+ log.debug("Services already started, skipping");
51
+ return;
52
+ }
53
+ if (this.startPromise) {
54
+ log.debug("Waiting for existing start promise");
55
+ return this.startPromise;
56
+ }
57
+ this.mcp = mcp;
58
+ const provider = this.provider;
59
+ if (!provider) {
60
+ throw new Error("Provider \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 setProvider");
61
+ }
62
+ this.startPromise = (async () => {
63
+ const timer = log.timer("startServices", {
64
+ corsOrigins,
65
+ contextApiUrl,
66
+ logsApiUrl,
67
+ viteOrigin
68
+ });
69
+ log.info("Starting Web UI services...");
70
+ const orphanCount = await provider.killOrphans?.() ?? 0;
71
+ if (orphanCount > 0) {
72
+ log.debug(`Killed ${orphanCount} orphan Web UI process(es)`);
73
+ }
74
+ this.sendTaskUpdate("checking_provider");
75
+ const env = await provider.checkEnvironment();
76
+ if (!env.ok) {
77
+ log.error(env.message ?? "Provider environment check failed");
78
+ this.sendTaskUpdate("provider_not_installed");
79
+ this.startPromise = null;
80
+ timer.end("\u274C Provider environment check failed");
81
+ return;
82
+ }
83
+ const providerVersion = env.version;
84
+ timer.checkpoint("Provider environment verified");
85
+ this.sendTaskUpdate("allocating_port");
86
+ this.actualWebPort = await findAvailablePort(this.config.webPort, this.config.hostname);
87
+ this.onPortAllocated(this.actualWebPort);
88
+ if (this.actualWebPort !== this.config.webPort) {
89
+ log.info(`Port ${this.config.webPort} is in use, using ${this.actualWebPort} instead`);
90
+ } else {
91
+ log.debug(`Using port ${this.actualWebPort}`);
92
+ }
93
+ timer.checkpoint("Port allocated");
94
+ this.workspaceRoot = findGitRoot(process.cwd());
95
+ log.debug(`Using workspace root: ${this.workspaceRoot}`);
96
+ this.sendTaskUpdate("preparing_runtime");
97
+ this.sendTaskUpdate("starting_web");
98
+ let webUrl;
99
+ try {
100
+ const startResult = await provider.start({
101
+ port: this.actualWebPort,
102
+ hostname: this.config.hostname,
103
+ cwd: this.workspaceRoot,
104
+ corsOrigins,
105
+ vitePort,
106
+ contextApiUrl,
107
+ logsApiUrl,
108
+ verbose: this.config.verbose,
109
+ vueDevtoolsApiUrl
110
+ });
111
+ this.webProcess = startResult.processHandle ?? null;
112
+ timer.checkpoint("Web process started");
113
+ webUrl = startResult.url;
114
+ log.debug(`Waiting for Web UI to become ready at ${webUrl}...`);
115
+ this.sendTaskUpdate("waiting_web_ready");
116
+ await waitForServer(webUrl, SERVER_START_TIMEOUT, this.webProcess ?? void 0);
117
+ if (this.webProcess?.exitCode !== null && this.webProcess?.exitCode !== void 0) {
118
+ throw new Error(`Web process exited with code ${this.webProcess.exitCode}`);
119
+ }
120
+ log.info(
121
+ `Web UI started at ${webUrl}${providerVersion ? ` (${provider.displayName} ${providerVersion})` : ""}`
122
+ );
123
+ } catch (e) {
124
+ log.error("Web UI failed to start", { error: e });
125
+ this.sendTaskUpdate("web_start_timeout");
126
+ await provider.stop();
127
+ this.webProcess = null;
128
+ this.startPromise = null;
129
+ timer.end("\u274C Web start timeout");
130
+ return;
131
+ }
132
+ this.sendTaskUpdate("starting_proxy");
133
+ let proxyStartPort = this.config.proxyPort ?? DEFAULT_PROXY_PORT;
134
+ if (proxyStartPort === this.actualWebPort) {
135
+ proxyStartPort = this.actualWebPort + 1;
136
+ log.debug(`Proxy start port conflicts with web port, using ${proxyStartPort} instead`);
137
+ }
138
+ this.actualProxyPort = await findAvailablePort(proxyStartPort, this.config.hostname);
139
+ this.onProxyPortAllocated(this.actualProxyPort);
140
+ if (this.actualProxyPort !== (this.config.proxyPort ?? DEFAULT_PROXY_PORT)) {
141
+ log.info(
142
+ `Proxy port ${this.config.proxyPort ?? DEFAULT_PROXY_PORT} is in use, using ${this.actualProxyPort} instead`
143
+ );
144
+ } else {
145
+ log.debug(`Using proxy port ${this.actualProxyPort}`);
146
+ }
147
+ try {
148
+ const result = await startProxyServer(webUrl, this.actualProxyPort, {
149
+ bridgeScript: provider.bridgeScript,
150
+ hostname: this.config.hostname
151
+ });
152
+ this.proxyServer = result.server;
153
+ if (result.actualPort !== this.actualProxyPort) {
154
+ log.info(
155
+ `Proxy port ${this.actualProxyPort} was taken, using ${result.actualPort} instead`
156
+ );
157
+ this.actualProxyPort = result.actualPort;
158
+ this.onProxyPortAllocated(this.actualProxyPort);
159
+ }
160
+ } catch (err) {
161
+ const nodeErr = err;
162
+ if (nodeErr.code === "EADDRINUSE") {
163
+ log.debug(`Proxy port ${this.actualProxyPort} became unavailable, trying next port...`);
164
+ const nextPort = await findAvailablePort(this.actualProxyPort + 1, this.config.hostname);
165
+ const result = await startProxyServer(webUrl, nextPort, {
166
+ bridgeScript: provider.bridgeScript,
167
+ hostname: this.config.hostname
168
+ });
169
+ this.proxyServer = result.server;
170
+ this.actualProxyPort = result.actualPort;
171
+ this.onProxyPortAllocated(this.actualProxyPort);
172
+ log.debug(`Proxy server started on fallback port ${this.actualProxyPort}`);
173
+ } else {
174
+ log.error("Proxy server failed to start", { error: nodeErr });
175
+ this.sendTaskUpdate("proxy_start_failed", {
176
+ errorMessage: nodeErr.message
177
+ });
178
+ await provider.stop();
179
+ this.webProcess = null;
180
+ this.startPromise = null;
181
+ return;
182
+ }
183
+ }
184
+ timer.checkpoint("Proxy server started");
185
+ this.sendTaskUpdate("warming_up_chrome");
186
+ let warmupFailed = false;
187
+ try {
188
+ const result = await mcp.verify();
189
+ if (!result.ok) throw new Error(result.error ?? "Chrome MCP not available");
190
+ timer.checkpoint("Chrome MCP warmup complete");
191
+ } catch (e) {
192
+ log.warn("Chrome MCP warmup failed", { error: e });
193
+ this.chromeMcpWarmupFailed = true;
194
+ warmupFailed = true;
195
+ this.chromeMcpWarmupErrorType = ChromeMcpWarmupErrorType.UNKNOWN;
196
+ this.chromeMcpWarmupErrorMessage = e instanceof Error ? e.message : String(e);
197
+ }
198
+ this.sendTaskUpdate("creating_session");
199
+ this.isStarted = true;
200
+ this.unsubscribeEvents?.();
201
+ this.unsubscribeEvents = provider.subscribeEvents((event) => {
202
+ this.sseClients.forEach((client) => {
203
+ try {
204
+ client.write(`data: ${JSON.stringify({ type: "SESSION_EVENT", event })}
205
+
206
+ `);
207
+ } catch (e) {
208
+ log.debug("Failed to send SESSION_EVENT", { error: e });
209
+ }
210
+ });
211
+ });
212
+ if (warmupFailed) {
213
+ this.sendTaskUpdate("chrome_mcp_failed", {
214
+ errorType: this.chromeMcpWarmupErrorType,
215
+ errorMessage: this.chromeMcpWarmupErrorMessage
216
+ });
217
+ } else {
218
+ this.sendTaskUpdate("ready");
219
+ }
220
+ timer.end("\u2713 Services started successfully");
221
+ })();
222
+ return this.startPromise;
223
+ }
224
+ async retryWarmupChromeMcp() {
225
+ if (!this.mcp) {
226
+ return { success: false, errorType: "UNKNOWN", errorMessage: "MCP not initialized" };
227
+ }
228
+ try {
229
+ const result = await this.mcp.verify();
230
+ if (result.ok) {
231
+ this.chromeMcpWarmupFailed = false;
232
+ this.sendTaskUpdate("ready");
233
+ return { success: true };
234
+ }
235
+ return {
236
+ success: false,
237
+ errorType: "CHROME_NOT_CONNECTED",
238
+ errorMessage: result.error ?? "Chrome MCP not available"
239
+ };
240
+ } catch (e) {
241
+ return {
242
+ success: false,
243
+ errorType: "UNKNOWN",
244
+ errorMessage: e instanceof Error ? e.message : String(e)
245
+ };
246
+ }
247
+ }
248
+ async stop() {
249
+ const timer = log.timer("stopServices");
250
+ log.info("Stopping Web UI services...");
251
+ this.unsubscribeEvents?.();
252
+ this.unsubscribeEvents = null;
253
+ if (this.proxyServer) {
254
+ log.debug("Closing proxy server");
255
+ this.proxyServer.close();
256
+ this.proxyServer = null;
257
+ }
258
+ await this.provider?.stop();
259
+ this.webProcess = null;
260
+ this.isStarted = false;
261
+ this.startPromise = null;
262
+ timer.end("\u2713 Services stopped");
263
+ }
264
+ }
265
+ export {
266
+ AIPanelService
267
+ };
@@ -0,0 +1,3 @@
1
+ import type { ViteDevServer } from "vite";
2
+ import type { EndpointContext } from "./types";
3
+ export declare function setupContextEndpoint(server: ViteDevServer, ctx: EndpointContext): void;
@@ -0,0 +1,106 @@
1
+ import { CONTEXT_API_PATH } from "@aipanel/core";
2
+ import { RequestContext, createLogger } from "@aipanel/core/node";
3
+ const log = createLogger("Endpoints:Context");
4
+ function setupContextEndpoint(server, ctx) {
5
+ server.middlewares.use(CONTEXT_API_PATH, async (req, res) => {
6
+ const reqCtx = new RequestContext(req.method || "GET", CONTEXT_API_PATH);
7
+ res.setHeader("Content-Type", "application/json");
8
+ res.setHeader("Access-Control-Allow-Origin", "*");
9
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
10
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
11
+ if (req.method === "OPTIONS") {
12
+ res.writeHead(200);
13
+ res.end();
14
+ reqCtx.end(200);
15
+ return;
16
+ }
17
+ if (req.method === "GET") {
18
+ const pc = ctx.getPageContext();
19
+ res.writeHead(200);
20
+ res.end(JSON.stringify(pc));
21
+ reqCtx.end(200);
22
+ return;
23
+ }
24
+ if (req.method === "DELETE") {
25
+ ctx.clearSelectedElements();
26
+ log.debug("Selected elements cleared", { sseClients: ctx.sseClients.size });
27
+ let sentCount = 0;
28
+ ctx.sseClients.forEach((client) => {
29
+ try {
30
+ client.write(`data: ${JSON.stringify({ type: "CLEAR_ELEMENTS" })}
31
+
32
+ `);
33
+ sentCount++;
34
+ } catch (e) {
35
+ log.debug("Failed to send SSE message", { error: e });
36
+ }
37
+ });
38
+ log.debug("SSE messages sent", {
39
+ count: sentCount,
40
+ totalClients: ctx.sseClients.size
41
+ });
42
+ res.writeHead(200);
43
+ res.end(JSON.stringify({ success: true }));
44
+ reqCtx.end(200);
45
+ return;
46
+ }
47
+ if (req.method === "POST") {
48
+ let body = "";
49
+ req.on("data", (chunk) => body += chunk.toString());
50
+ req.on("error", () => {
51
+ res.writeHead(400);
52
+ res.end(JSON.stringify({ error: "Request error" }));
53
+ });
54
+ req.on("end", () => {
55
+ try {
56
+ const data = JSON.parse(body);
57
+ const tabId = data.tabId != null ? String(data.tabId) : "default";
58
+ const existing = ctx.getPageContext();
59
+ const newCtx = {
60
+ url: data.url || "",
61
+ title: data.title || "",
62
+ sessionId: data.sessionId,
63
+ tabId: data.tabId ?? existing.tabId,
64
+ tabIndex: data.tabIndex ?? existing.tabIndex,
65
+ selectedElements: data.selectedElements || []
66
+ };
67
+ ctx.setPageContext(tabId, newCtx);
68
+ if (data.active) {
69
+ ctx.setActiveTabId(tabId);
70
+ }
71
+ log.debug("Context updated", {
72
+ tabId,
73
+ url: newCtx.url,
74
+ title: newCtx.title,
75
+ sessionId: newCtx.sessionId,
76
+ selectedElementsCount: newCtx.selectedElements?.length || 0
77
+ });
78
+ if (newCtx.selectedElements && newCtx.selectedElements.length > 0) {
79
+ log.debug("Selected elements details", {
80
+ elements: newCtx.selectedElements.map((el) => ({
81
+ filePath: el.filePath,
82
+ line: el.line,
83
+ text: el.innerText?.substring(0, 50)
84
+ }))
85
+ });
86
+ }
87
+ res.writeHead(200);
88
+ res.end(JSON.stringify({ success: true }));
89
+ reqCtx.end(200);
90
+ } catch (e) {
91
+ log.debug("Invalid JSON in request body", { error: e });
92
+ res.writeHead(400);
93
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
94
+ reqCtx.error(e);
95
+ }
96
+ });
97
+ return;
98
+ }
99
+ res.writeHead(405);
100
+ res.end(JSON.stringify({ error: "Method not allowed" }));
101
+ reqCtx.end(405);
102
+ });
103
+ }
104
+ export {
105
+ setupContextEndpoint
106
+ };
@@ -0,0 +1,9 @@
1
+ import type { ViteDevServer } from "vite";
2
+ import type { EndpointContext } from "./types";
3
+ import { MCP_API_PATH } from "./mcp";
4
+ import { VUE_DEVTOOLS_API_PATH } from "./vue-devtools";
5
+ import type { McpProxy } from "../core/mcp-proxy";
6
+ import { LOGS_API_PATH, type LogFileConfig } from "@aipanel/core";
7
+ export * from "./types";
8
+ export { LOGS_API_PATH, MCP_API_PATH, VUE_DEVTOOLS_API_PATH };
9
+ export declare function setupMiddlewares(server: ViteDevServer, ctx: EndpointContext, mcp?: McpProxy, logFiles?: LogFileConfig[]): void;
@@ -0,0 +1,30 @@
1
+ import { setupWidgetEndpoints } from "./widget.mjs";
2
+ import { setupContextEndpoint } from "./context.mjs";
3
+ import { setupStartEndpoint } from "./start.mjs";
4
+ import { setupSseEndpoint } from "./sse.mjs";
5
+ import { setupSessionsEndpoint } from "./sessions.mjs";
6
+ import { setupWarmupEndpoint } from "./warmup.mjs";
7
+ import { setupLogsEndpoint } from "./logs.mjs";
8
+ import { setupMcpEndpoint, MCP_API_PATH } from "./mcp.mjs";
9
+ import { setupVueDevtoolsEndpoint, VUE_DEVTOOLS_API_PATH } from "./vue-devtools.mjs";
10
+ import { LOGS_API_PATH } from "@aipanel/core";
11
+ export * from "./types.mjs";
12
+ function setupMiddlewares(server, ctx, mcp, logFiles) {
13
+ setupWidgetEndpoints(server, ctx);
14
+ setupContextEndpoint(server, ctx);
15
+ setupStartEndpoint(server, ctx);
16
+ setupSseEndpoint(server, ctx);
17
+ setupSessionsEndpoint(server, ctx);
18
+ setupWarmupEndpoint(server, ctx);
19
+ setupLogsEndpoint(server);
20
+ if (mcp) {
21
+ setupMcpEndpoint(server, mcp, () => ctx.getPageContext(), logFiles ?? []);
22
+ setupVueDevtoolsEndpoint(server, mcp);
23
+ }
24
+ }
25
+ export {
26
+ LOGS_API_PATH,
27
+ MCP_API_PATH,
28
+ VUE_DEVTOOLS_API_PATH,
29
+ setupMiddlewares
30
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview 进程日志端点
3
+ * @description 提供 HTTP API 获取 Vite 进程日志缓冲区内容
4
+ */
5
+ import type { ViteDevServer } from "vite";
6
+ /**
7
+ * 设置日志端点
8
+ */
9
+ export declare function setupLogsEndpoint(server: ViteDevServer): void;