vite-plugin-aipanel 1.2.7 → 1.2.9
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.
- package/es/core/mcp-proxy.d.ts +8 -1
- package/es/core/mcp-proxy.mjs +44 -12
- package/es/core/proxy-server.d.ts +7 -0
- package/es/core/proxy-server.mjs +22 -13
- package/es/core/service.d.ts +8 -1
- package/es/core/service.mjs +30 -12
- package/es/endpoints/host-events.d.ts +8 -0
- package/es/endpoints/host-events.mjs +77 -0
- package/es/endpoints/index.mjs +2 -0
- package/es/endpoints/sessions.mjs +3 -1
- package/es/endpoints/start.mjs +1 -1
- package/es/endpoints/types.d.ts +7 -2
- package/es/index.mjs +17 -5
- package/es/utils/format-bridge.mjs +11 -4
- package/lib/client.js +882 -883
- package/lib/core/mcp-proxy.cjs +47 -13
- package/lib/core/mcp-proxy.d.ts +8 -1
- package/lib/core/proxy-server.cjs +22 -13
- package/lib/core/proxy-server.d.ts +7 -0
- package/lib/core/service.cjs +30 -12
- package/lib/core/service.d.ts +8 -1
- package/lib/endpoints/host-events.cjs +100 -0
- package/lib/endpoints/host-events.d.ts +8 -0
- package/lib/endpoints/index.cjs +2 -0
- package/lib/endpoints/sessions.cjs +3 -1
- package/lib/endpoints/start.cjs +1 -1
- package/lib/endpoints/types.d.ts +7 -2
- package/lib/index.cjs +16 -5
- package/lib/utils/format-bridge.cjs +5 -4
- package/package.json +5 -5
package/es/core/mcp-proxy.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
/** chrome-devtools-mcp 的核心受保护参数(用户不可覆盖;始终最后注入确保生效) */
|
|
2
|
+
export declare const CORE_MCP_ARGS: readonly ["--auto-connect", "--no-usage-statistics", "--no-performance-crux", "--no-page-id-routing"];
|
|
3
|
+
/** 过滤用户透传参数:剔除与核心受保护 flag 冲突的项,其余原样保留 */
|
|
4
|
+
export declare function filterUserMcpArgs(userArgs: readonly string[]): string[];
|
|
1
5
|
export interface McpProxyOptions {
|
|
2
|
-
|
|
6
|
+
/** 用户透传的额外 CLI 参数(追加;与核心受保护项冲突的会被剔除) */
|
|
7
|
+
userArgs?: string[];
|
|
8
|
+
/** 用户透传的额外环境变量(合并到 process.env 之上) */
|
|
9
|
+
env?: Record<string, string>;
|
|
3
10
|
idleTimeout?: number;
|
|
4
11
|
}
|
|
5
12
|
export declare class McpProxy {
|
package/es/core/mcp-proxy.mjs
CHANGED
|
@@ -17,13 +17,41 @@ var __privateWrapper = (obj, member, setter, getter) => ({
|
|
|
17
17
|
return __privateGet(obj, member, getter);
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
|
-
var _proc, _rl, _messageId, _internalIdBase, _pending, _args, _startPromise, _lastExit, _stderrTail, _idleTimer, _idleTimeout, _McpProxy_instances, doStart_fn, formatNotAvailable_fn, resetIdleTimer_fn;
|
|
20
|
+
var _proc, _rl, _messageId, _internalIdBase, _pending, _args, _env, _startPromise, _lastExit, _stderrTail, _idleTimer, _idleTimeout, _McpProxy_instances, doStart_fn, formatNotAvailable_fn, resetIdleTimer_fn;
|
|
21
21
|
import { spawn } from "node:child_process";
|
|
22
22
|
import { createInterface } from "node:readline";
|
|
23
23
|
import crypto from "node:crypto";
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import { createLogger, createPackageRequire, resolvePackageDir } from "@aipanel/core/node";
|
|
26
26
|
const log = createLogger("McpProxy");
|
|
27
|
+
const CORE_MCP_ARGS = [
|
|
28
|
+
"--auto-connect",
|
|
29
|
+
"--no-usage-statistics",
|
|
30
|
+
"--no-performance-crux",
|
|
31
|
+
// chrome-devtools-mcp >=1.8.0 默认开启 pageIdRouting(要求每个页面级工具都传 pageId)。
|
|
32
|
+
// 代理层已自行校验 pageId 并用 select_page 选中目标页面后再转发,故显式关闭,
|
|
33
|
+
// 避免底层工具 schema 强制必填 pageId 导致转发时的参数校验失败。
|
|
34
|
+
"--no-page-id-routing"
|
|
35
|
+
];
|
|
36
|
+
const PROTECTED_FLAGS = /* @__PURE__ */ new Set([
|
|
37
|
+
"auto-connect",
|
|
38
|
+
"usage-statistics",
|
|
39
|
+
"performance-crux",
|
|
40
|
+
"page-id-routing"
|
|
41
|
+
]);
|
|
42
|
+
function canonicalFlagName(arg) {
|
|
43
|
+
let flag = arg.trim();
|
|
44
|
+
if (!flag.startsWith("-")) return null;
|
|
45
|
+
flag = flag.replace(/^--?/, "").split("=")[0] ?? "";
|
|
46
|
+
if (flag.startsWith("no-")) flag = flag.slice(3);
|
|
47
|
+
return flag || null;
|
|
48
|
+
}
|
|
49
|
+
function filterUserMcpArgs(userArgs) {
|
|
50
|
+
return userArgs.filter((arg) => {
|
|
51
|
+
const name = canonicalFlagName(arg);
|
|
52
|
+
return name === null || !PROTECTED_FLAGS.has(name);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
27
55
|
function resolveChromeDevToolsMcpBin() {
|
|
28
56
|
const pluginDir = resolvePackageDir("vite-plugin-aipanel");
|
|
29
57
|
const require2 = createPackageRequire(pluginDir);
|
|
@@ -43,6 +71,7 @@ class McpProxy {
|
|
|
43
71
|
__privateAdd(this, _internalIdBase, 1e6);
|
|
44
72
|
__privateAdd(this, _pending, /* @__PURE__ */ new Map());
|
|
45
73
|
__privateAdd(this, _args);
|
|
74
|
+
__privateAdd(this, _env);
|
|
46
75
|
__privateAdd(this, _startPromise, null);
|
|
47
76
|
/** 最近一次进程退出信息,用于生成精确错误(无退出记录时为 null) */
|
|
48
77
|
__privateAdd(this, _lastExit, null);
|
|
@@ -51,15 +80,14 @@ class McpProxy {
|
|
|
51
80
|
__privateAdd(this, _idleTimer, null);
|
|
52
81
|
__privateAdd(this, _idleTimeout);
|
|
53
82
|
__publicField(this, "sessionId");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
]);
|
|
83
|
+
const userArgs = options.userArgs ?? [];
|
|
84
|
+
const filtered = filterUserMcpArgs(userArgs);
|
|
85
|
+
if (filtered.length !== userArgs.length) {
|
|
86
|
+
const dropped = userArgs.filter((a) => !filtered.includes(a));
|
|
87
|
+
log.warn("chrome MCP \u7528\u6237\u53C2\u6570\u4E0E\u6838\u5FC3\u53D7\u4FDD\u62A4\u9879\u51B2\u7A81\uFF0C\u5DF2\u5FFD\u7565", { dropped });
|
|
88
|
+
}
|
|
89
|
+
__privateSet(this, _args, [...filtered, ...CORE_MCP_ARGS]);
|
|
90
|
+
__privateSet(this, _env, options.env);
|
|
63
91
|
__privateSet(this, _idleTimeout, options.idleTimeout ?? 0);
|
|
64
92
|
this.sessionId = crypto.randomUUID();
|
|
65
93
|
}
|
|
@@ -153,6 +181,7 @@ _messageId = new WeakMap();
|
|
|
153
181
|
_internalIdBase = new WeakMap();
|
|
154
182
|
_pending = new WeakMap();
|
|
155
183
|
_args = new WeakMap();
|
|
184
|
+
_env = new WeakMap();
|
|
156
185
|
_startPromise = new WeakMap();
|
|
157
186
|
_lastExit = new WeakMap();
|
|
158
187
|
_stderrTail = new WeakMap();
|
|
@@ -166,7 +195,8 @@ doStart_fn = async function() {
|
|
|
166
195
|
__privateSet(this, _lastExit, null);
|
|
167
196
|
__privateSet(this, _stderrTail, []);
|
|
168
197
|
__privateSet(this, _proc, spawn(process.execPath, [binPath, ...__privateGet(this, _args)], {
|
|
169
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
198
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
199
|
+
env: { ...process.env, ...__privateGet(this, _env) }
|
|
170
200
|
}));
|
|
171
201
|
log.debug("Using local chrome-devtools-mcp");
|
|
172
202
|
} catch {
|
|
@@ -255,5 +285,7 @@ resetIdleTimer_fn = function() {
|
|
|
255
285
|
}, __privateGet(this, _idleTimeout)));
|
|
256
286
|
};
|
|
257
287
|
export {
|
|
258
|
-
|
|
288
|
+
CORE_MCP_ARGS,
|
|
289
|
+
McpProxy,
|
|
290
|
+
filterUserMcpArgs
|
|
259
291
|
};
|
|
@@ -4,6 +4,13 @@ export interface ProxyServerOptions {
|
|
|
4
4
|
bridgeScript?: string;
|
|
5
5
|
/** 绑定地址,需与端口检查使用的地址族一致,避免 IPv4/IPv6 不匹配 */
|
|
6
6
|
hostname?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Provider Web 服务的 browser-session 认证 Cookie(dsh 0.1.2+ 需要)。
|
|
9
|
+
* 代理在转发每个请求(含 WebSocket 升级)时把该 Cookie 注入到上游请求头,
|
|
10
|
+
* 使浏览器免于亲自换取/携带会话 Cookie(避免跨端口/跨站 SameSite 限制),
|
|
11
|
+
* 同时通过上游 /api 的 browser-auth 门禁。
|
|
12
|
+
*/
|
|
13
|
+
webAuthCookie?: string;
|
|
7
14
|
}
|
|
8
15
|
export interface ProxyServerResult {
|
|
9
16
|
server: http.Server;
|
package/es/core/proxy-server.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import http from "http";
|
|
2
|
-
import {
|
|
2
|
+
import { BRIDGE_SCRIPT_PATH } from "@aipanel/core";
|
|
3
|
+
import { createLogger } from "@aipanel/core/node";
|
|
3
4
|
const log = createLogger("ProxyServer");
|
|
4
5
|
function startProxyServer(targetUrl, port, options = {}) {
|
|
5
6
|
return new Promise((resolve, reject) => {
|
|
6
7
|
const target = new URL(targetUrl);
|
|
7
8
|
const bridgeScript = options.bridgeScript ?? "";
|
|
9
|
+
const webAuthCookie = options.webAuthCookie ?? "";
|
|
8
10
|
const agent = new http.Agent({
|
|
9
11
|
keepAlive: true,
|
|
10
12
|
keepAliveMsecs: 3e4,
|
|
@@ -32,6 +34,9 @@ function startProxyServer(targetUrl, port, options = {}) {
|
|
|
32
34
|
headers: {
|
|
33
35
|
...req.headers,
|
|
34
36
|
host: target.host,
|
|
37
|
+
// 注入 Provider Web 服务的 browser-session 认证 Cookie:代理作为已认证客户端
|
|
38
|
+
// 替 iframe 里的浏览器完成 dsh 0.1.2+ 的会话认证(浏览器无需换取/携带该 Cookie)。
|
|
39
|
+
...webAuthCookie ? { cookie: webAuthCookie } : {},
|
|
35
40
|
// 上游(如 dsh 的 trust fence)校验 Origin 必须匹配自身 origin;
|
|
36
41
|
// 页面经代理访问时浏览器发的 Origin 是代理端口,须改写为目标 origin 否则 403
|
|
37
42
|
origin: `http://${target.host}`,
|
|
@@ -49,18 +54,20 @@ function startProxyServer(targetUrl, port, options = {}) {
|
|
|
49
54
|
});
|
|
50
55
|
proxyRes.on("end", () => {
|
|
51
56
|
let body = Buffer.concat(chunks).toString("utf-8");
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
if (bridgeScript.trim().length > 0) {
|
|
58
|
+
if (body.match(/<\/head>/i)) {
|
|
59
|
+
body = body.replace(
|
|
60
|
+
/<\/head>/i,
|
|
61
|
+
`<script src="${BRIDGE_SCRIPT_PATH}"></script></head>`
|
|
62
|
+
);
|
|
63
|
+
} else if (body.match(/<\/body>/i)) {
|
|
64
|
+
body = body.replace(
|
|
65
|
+
/<\/body>/i,
|
|
66
|
+
`<script src="${BRIDGE_SCRIPT_PATH}"></script></body>`
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
body += `<script src="${BRIDGE_SCRIPT_PATH}"></script>`;
|
|
70
|
+
}
|
|
64
71
|
}
|
|
65
72
|
const headers = {};
|
|
66
73
|
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
@@ -104,6 +111,8 @@ function startProxyServer(targetUrl, port, options = {}) {
|
|
|
104
111
|
headers: {
|
|
105
112
|
...req.headers,
|
|
106
113
|
host: target.host,
|
|
114
|
+
// 与 HTTP 分支一致:注入认证 Cookie,通过 dsh 0.1.2+ 的 browser-auth 门禁
|
|
115
|
+
...webAuthCookie ? { cookie: webAuthCookie } : {},
|
|
107
116
|
// 与 HTTP 分支一致:改写 Origin 为目标 origin,通过 dsh 的 trust fence
|
|
108
117
|
origin: `http://${target.host}`
|
|
109
118
|
}
|
package/es/core/service.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResultPromise } from "execa";
|
|
2
2
|
import type http from "http";
|
|
3
|
-
import type { PluginOptions, ServiceStartupTask, WebProvider } from "@aipanel/core";
|
|
3
|
+
import type { PluginOptions, ProviderEvent, ServiceStartupTask, WebProvider } from "@aipanel/core";
|
|
4
4
|
import { ChromeMcpWarmupErrorType } from "@aipanel/core";
|
|
5
5
|
import type { McpProxy } from "./mcp-proxy";
|
|
6
6
|
export declare class AIPanelService {
|
|
@@ -25,11 +25,18 @@ export declare class AIPanelService {
|
|
|
25
25
|
private mcp;
|
|
26
26
|
private provider;
|
|
27
27
|
private unsubscribeEvents;
|
|
28
|
+
/**
|
|
29
|
+
* 宿主事件推送令牌(每轮 start 随机生成)。Provider 侧 Host 插件(dsh-plugin)推送
|
|
30
|
+
* ProviderEvent 时须携带该令牌,core 校验通过后按 SESSION_EVENT 广播给 SSE 客户端。
|
|
31
|
+
*/
|
|
32
|
+
eventsToken: string | null;
|
|
28
33
|
constructor(config: Required<PluginOptions>, sseClients: Set<http.ServerResponse>, onPortAllocated: (port: number) => void, onProxyPortAllocated: (port: number) => void);
|
|
29
34
|
/** 设置 Provider(由编排层动态加载后调用) */
|
|
30
35
|
setProvider(provider: WebProvider): void;
|
|
31
36
|
private sendTaskUpdate;
|
|
32
37
|
start(vitePort: number, corsOrigins: string[], contextApiUrl: string, logsApiUrl: string, viteOrigin: string, mcp: McpProxy, vueDevtoolsApiUrl?: string): Promise<void>;
|
|
38
|
+
/** 把一条 ProviderEvent 广播给所有 SSE 客户端(SESSION_EVENT 载荷) */
|
|
39
|
+
pushProviderEvent(event: ProviderEvent): void;
|
|
33
40
|
retryWarmupChromeMcp(): Promise<{
|
|
34
41
|
success: boolean;
|
|
35
42
|
errorType?: string;
|
package/es/core/service.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { DEFAULT_PROXY_PORT, SERVER_START_TIMEOUT, ChromeMcpWarmupErrorType } from "@aipanel/core";
|
|
5
6
|
import { createLogger, findAvailablePort } from "@aipanel/core/node";
|
|
6
7
|
import { findGitRoot, waitForServer } from "../utils/system.mjs";
|
|
@@ -26,6 +27,11 @@ class AIPanelService {
|
|
|
26
27
|
__publicField(this, "mcp", null);
|
|
27
28
|
__publicField(this, "provider", null);
|
|
28
29
|
__publicField(this, "unsubscribeEvents", null);
|
|
30
|
+
/**
|
|
31
|
+
* 宿主事件推送令牌(每轮 start 随机生成)。Provider 侧 Host 插件(dsh-plugin)推送
|
|
32
|
+
* ProviderEvent 时须携带该令牌,core 校验通过后按 SESSION_EVENT 广播给 SSE 客户端。
|
|
33
|
+
*/
|
|
34
|
+
__publicField(this, "eventsToken", null);
|
|
29
35
|
this.actualWebPort = config.webPort;
|
|
30
36
|
this.actualProxyPort = config.proxyPort ?? DEFAULT_PROXY_PORT;
|
|
31
37
|
}
|
|
@@ -60,6 +66,7 @@ class AIPanelService {
|
|
|
60
66
|
throw new Error("Provider \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 setProvider");
|
|
61
67
|
}
|
|
62
68
|
this.startPromise = (async () => {
|
|
69
|
+
this.eventsToken = randomUUID();
|
|
63
70
|
const timer = log.timer("startServices", {
|
|
64
71
|
corsOrigins,
|
|
65
72
|
contextApiUrl,
|
|
@@ -96,6 +103,7 @@ class AIPanelService {
|
|
|
96
103
|
this.sendTaskUpdate("preparing_runtime");
|
|
97
104
|
this.sendTaskUpdate("starting_web");
|
|
98
105
|
let webUrl;
|
|
106
|
+
let webAuthCookie;
|
|
99
107
|
try {
|
|
100
108
|
const startResult = await provider.start({
|
|
101
109
|
port: this.actualWebPort,
|
|
@@ -106,11 +114,14 @@ class AIPanelService {
|
|
|
106
114
|
contextApiUrl,
|
|
107
115
|
logsApiUrl,
|
|
108
116
|
verbose: this.config.verbose,
|
|
109
|
-
vueDevtoolsApiUrl
|
|
117
|
+
vueDevtoolsApiUrl,
|
|
118
|
+
// 宿主事件推送令牌:provider 将其注入 Host 插件(dsh-plugin)配置,用于回推事件鉴权
|
|
119
|
+
eventsToken: this.eventsToken ?? void 0
|
|
110
120
|
});
|
|
111
121
|
this.webProcess = startResult.processHandle ?? null;
|
|
112
122
|
timer.checkpoint("Web process started");
|
|
113
123
|
webUrl = startResult.url;
|
|
124
|
+
webAuthCookie = startResult.webAuthCookie;
|
|
114
125
|
log.debug(`Waiting for Web UI to become ready at ${webUrl}...`);
|
|
115
126
|
this.sendTaskUpdate("waiting_web_ready");
|
|
116
127
|
await waitForServer(webUrl, SERVER_START_TIMEOUT, this.webProcess ?? void 0);
|
|
@@ -147,7 +158,8 @@ class AIPanelService {
|
|
|
147
158
|
try {
|
|
148
159
|
const result = await startProxyServer(webUrl, this.actualProxyPort, {
|
|
149
160
|
bridgeScript: provider.bridgeScript,
|
|
150
|
-
hostname: this.config.hostname
|
|
161
|
+
hostname: this.config.hostname,
|
|
162
|
+
webAuthCookie
|
|
151
163
|
});
|
|
152
164
|
this.proxyServer = result.server;
|
|
153
165
|
if (result.actualPort !== this.actualProxyPort) {
|
|
@@ -164,7 +176,8 @@ class AIPanelService {
|
|
|
164
176
|
const nextPort = await findAvailablePort(this.actualProxyPort + 1, this.config.hostname);
|
|
165
177
|
const result = await startProxyServer(webUrl, nextPort, {
|
|
166
178
|
bridgeScript: provider.bridgeScript,
|
|
167
|
-
hostname: this.config.hostname
|
|
179
|
+
hostname: this.config.hostname,
|
|
180
|
+
webAuthCookie
|
|
168
181
|
});
|
|
169
182
|
this.proxyServer = result.server;
|
|
170
183
|
this.actualProxyPort = result.actualPort;
|
|
@@ -199,15 +212,7 @@ class AIPanelService {
|
|
|
199
212
|
this.isStarted = true;
|
|
200
213
|
this.unsubscribeEvents?.();
|
|
201
214
|
this.unsubscribeEvents = provider.subscribeEvents((event) => {
|
|
202
|
-
this.
|
|
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
|
-
});
|
|
215
|
+
this.pushProviderEvent(event);
|
|
211
216
|
});
|
|
212
217
|
if (warmupFailed) {
|
|
213
218
|
this.sendTaskUpdate("chrome_mcp_failed", {
|
|
@@ -221,6 +226,18 @@ class AIPanelService {
|
|
|
221
226
|
})();
|
|
222
227
|
return this.startPromise;
|
|
223
228
|
}
|
|
229
|
+
/** 把一条 ProviderEvent 广播给所有 SSE 客户端(SESSION_EVENT 载荷) */
|
|
230
|
+
pushProviderEvent(event) {
|
|
231
|
+
this.sseClients.forEach((client) => {
|
|
232
|
+
try {
|
|
233
|
+
client.write(`data: ${JSON.stringify({ type: "SESSION_EVENT", event })}
|
|
234
|
+
|
|
235
|
+
`);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
log.debug("Failed to send SESSION_EVENT", { error: e });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
224
241
|
async retryWarmupChromeMcp() {
|
|
225
242
|
if (!this.mcp) {
|
|
226
243
|
return { success: false, errorType: "UNKNOWN", errorMessage: "MCP not initialized" };
|
|
@@ -250,6 +267,7 @@ class AIPanelService {
|
|
|
250
267
|
log.info("Stopping Web UI services...");
|
|
251
268
|
this.unsubscribeEvents?.();
|
|
252
269
|
this.unsubscribeEvents = null;
|
|
270
|
+
this.eventsToken = null;
|
|
253
271
|
if (this.proxyServer) {
|
|
254
272
|
log.debug("Closing proxy server");
|
|
255
273
|
this.proxyServer.close();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ViteDevServer } from "vite";
|
|
2
|
+
import type { EndpointContext } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* 宿主侧事件推送端点(dsh-plugin 等 Host 插件 → core → SSE 广播)。
|
|
5
|
+
* 只允许无浏览器 Origin 的服务端请求:携带每轮启动随机令牌(x-aipanel-token 头或 body.token),
|
|
6
|
+
* 通过校验后按 SESSION_EVENT 广播给全部 SSE 客户端(与 provider.subscribeEvents 同一通道)。
|
|
7
|
+
*/
|
|
8
|
+
export declare function setupHostEventsEndpoint(server: ViteDevServer, ctx: EndpointContext): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { HOST_EVENTS_API_PATH } from "@aipanel/core";
|
|
2
|
+
import { RequestContext, createLogger } from "@aipanel/core/node";
|
|
3
|
+
const log = createLogger("Endpoints:HostEvents");
|
|
4
|
+
function setupHostEventsEndpoint(server, ctx) {
|
|
5
|
+
server.middlewares.use(HOST_EVENTS_API_PATH, async (req, res) => {
|
|
6
|
+
const reqCtx = new RequestContext(req.method || "GET", HOST_EVENTS_API_PATH);
|
|
7
|
+
res.setHeader("Content-Type", "application/json");
|
|
8
|
+
if (req.headers.origin) {
|
|
9
|
+
res.writeHead(403);
|
|
10
|
+
res.end(JSON.stringify({ error: "forbidden" }));
|
|
11
|
+
reqCtx.end(403);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (req.method === "OPTIONS") {
|
|
15
|
+
res.writeHead(204);
|
|
16
|
+
res.end();
|
|
17
|
+
reqCtx.end(204);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (req.method !== "POST") {
|
|
21
|
+
res.writeHead(405);
|
|
22
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
23
|
+
reqCtx.end(405);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let body = "";
|
|
27
|
+
req.on("data", (chunk) => body += chunk.toString());
|
|
28
|
+
req.on("error", () => {
|
|
29
|
+
res.writeHead(400);
|
|
30
|
+
res.end(JSON.stringify({ error: "request error" }));
|
|
31
|
+
reqCtx.end(400);
|
|
32
|
+
});
|
|
33
|
+
req.on("end", () => {
|
|
34
|
+
try {
|
|
35
|
+
const data = JSON.parse(body);
|
|
36
|
+
const expected = ctx.eventsToken;
|
|
37
|
+
if (!expected || data.token !== expected) {
|
|
38
|
+
res.writeHead(403);
|
|
39
|
+
res.end(JSON.stringify({ error: "invalid token" }));
|
|
40
|
+
reqCtx.end(403);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const event = data.event;
|
|
44
|
+
if (!isProviderEvent(event)) {
|
|
45
|
+
res.writeHead(400);
|
|
46
|
+
res.end(JSON.stringify({ error: "invalid event" }));
|
|
47
|
+
reqCtx.end(400);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
ctx.pushProviderEvent(event);
|
|
51
|
+
res.writeHead(200);
|
|
52
|
+
res.end(JSON.stringify({ ok: true }));
|
|
53
|
+
reqCtx.end(200);
|
|
54
|
+
} catch {
|
|
55
|
+
res.writeHead(400);
|
|
56
|
+
res.end(JSON.stringify({ error: "invalid json" }));
|
|
57
|
+
reqCtx.end(400);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function isProviderEvent(value) {
|
|
63
|
+
if (typeof value !== "object" || value === null) return false;
|
|
64
|
+
const v = value;
|
|
65
|
+
if (v.type === "session.status" || v.type === "thinking") {
|
|
66
|
+
return typeof v.sessionId === "string" && v.sessionId.length > 0;
|
|
67
|
+
}
|
|
68
|
+
if (v.type === "session.updated") {
|
|
69
|
+
const s = v.session;
|
|
70
|
+
return typeof s === "object" && s !== null && typeof s.id === "string";
|
|
71
|
+
}
|
|
72
|
+
if (v.type === "connected") return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
setupHostEventsEndpoint
|
|
77
|
+
};
|
package/es/endpoints/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { setupWidgetEndpoints } from "./widget.mjs";
|
|
|
2
2
|
import { setupContextEndpoint } from "./context.mjs";
|
|
3
3
|
import { setupStartEndpoint } from "./start.mjs";
|
|
4
4
|
import { setupSseEndpoint } from "./sse.mjs";
|
|
5
|
+
import { setupHostEventsEndpoint } from "./host-events.mjs";
|
|
5
6
|
import { setupSessionsEndpoint } from "./sessions.mjs";
|
|
6
7
|
import { setupWarmupEndpoint } from "./warmup.mjs";
|
|
7
8
|
import { setupLogsEndpoint } from "./logs.mjs";
|
|
@@ -14,6 +15,7 @@ function setupMiddlewares(server, ctx, mcp, logFiles) {
|
|
|
14
15
|
setupContextEndpoint(server, ctx);
|
|
15
16
|
setupStartEndpoint(server, ctx);
|
|
16
17
|
setupSseEndpoint(server, ctx);
|
|
18
|
+
setupHostEventsEndpoint(server, ctx);
|
|
17
19
|
setupSessionsEndpoint(server, ctx);
|
|
18
20
|
setupWarmupEndpoint(server, ctx);
|
|
19
21
|
setupLogsEndpoint(server);
|
|
@@ -17,8 +17,10 @@ function setupSessionsEndpoint(server, ctx) {
|
|
|
17
17
|
try {
|
|
18
18
|
if (req.method === "GET") {
|
|
19
19
|
reqCtx.checkpoint("Fetching sessions");
|
|
20
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
21
|
+
const activeSessionId = url.searchParams.get("current") || void 0;
|
|
20
22
|
const [sessions, capabilities] = await Promise.all([
|
|
21
|
-
ctx.getSessions(),
|
|
23
|
+
ctx.getSessions(activeSessionId),
|
|
22
24
|
ctx.getCapabilities()
|
|
23
25
|
]);
|
|
24
26
|
res.writeHead(200);
|
package/es/endpoints/start.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { START_API_PATH } from "@aipanel/core";
|
|
|
2
2
|
import { RequestContext } from "@aipanel/core/node";
|
|
3
3
|
function setupStartEndpoint(server, ctx) {
|
|
4
4
|
server.middlewares.use(START_API_PATH, async (_req, res) => {
|
|
5
|
-
const reqCtx = new RequestContext("GET", START_API_PATH);
|
|
5
|
+
const reqCtx = new RequestContext("GET", START_API_PATH, { quiet: true });
|
|
6
6
|
res.setHeader("Content-Type", "application/json");
|
|
7
7
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
8
8
|
res.writeHead(200);
|
package/es/endpoints/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatSession, PageContext, ProviderCapabilities, ServiceStartupTask } from "@aipanel/core";
|
|
1
|
+
import type { ChatSession, PageContext, ProviderCapabilities, ProviderEvent, ServiceStartupTask } from "@aipanel/core";
|
|
2
2
|
import type http from "http";
|
|
3
3
|
export interface EndpointContext {
|
|
4
4
|
get webUrl(): string | null;
|
|
@@ -19,11 +19,16 @@ export interface EndpointContext {
|
|
|
19
19
|
get actualProxyPort(): number;
|
|
20
20
|
get actualWebPort(): number;
|
|
21
21
|
get serviceInstanceId(): string;
|
|
22
|
-
|
|
22
|
+
/** 获取会话列表;activeSessionId(当前选中会话)由客户端传入,供 Provider 对齐 UI 可见性规则 */
|
|
23
|
+
getSessions: (activeSessionId?: string) => Promise<ChatSession[]>;
|
|
23
24
|
createSession: () => Promise<ChatSession>;
|
|
24
25
|
deleteSession: (id: string) => Promise<void>;
|
|
25
26
|
/** 获取当前 Provider 能力描述(客户端自适应行为依据) */
|
|
26
27
|
getCapabilities: () => ProviderCapabilities;
|
|
28
|
+
/** 宿主事件推送令牌(Host 插件回推 ProviderEvent 时鉴权用;未启动时 null) */
|
|
29
|
+
get eventsToken(): string | null;
|
|
30
|
+
/** 广播一条 ProviderEvent 给所有 SSE 客户端(SESSION_EVENT 载荷;Host 插件回推与 Provider 订阅共用) */
|
|
31
|
+
pushProviderEvent: (event: ProviderEvent) => void;
|
|
27
32
|
resolveWidgetPath: () => string;
|
|
28
33
|
resolveWidgetStylePath: () => string;
|
|
29
34
|
retryWarmupChromeMcp: () => Promise<{
|
package/es/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CONTEXT_API_PATH,
|
|
6
6
|
DEFAULT_PROXY_PORT,
|
|
7
7
|
MCP_API_PATH,
|
|
8
|
+
SESSION_ID_KEY,
|
|
8
9
|
resolvePluginConfig,
|
|
9
10
|
setVerbose
|
|
10
11
|
} from "@aipanel/core";
|
|
@@ -35,7 +36,7 @@ const SILENT_CONTEXT_SCRIPT = `<script>
|
|
|
35
36
|
if (!force && key === last) return;
|
|
36
37
|
last = key;
|
|
37
38
|
var sessionId = "";
|
|
38
|
-
try { sessionId = sessionStorage.getItem("
|
|
39
|
+
try { sessionId = sessionStorage.getItem("${SESSION_ID_KEY}") || ""; } catch (e) {}
|
|
39
40
|
fetch(API, {
|
|
40
41
|
method: "POST",
|
|
41
42
|
headers: { "Content-Type": "application/json" },
|
|
@@ -91,7 +92,12 @@ function createAIPanelPlugin(options = {}) {
|
|
|
91
92
|
let activeTabId = DEFAULT_TAB;
|
|
92
93
|
const serviceInstanceId = crypto.randomUUID();
|
|
93
94
|
const sseClients = /* @__PURE__ */ new Set();
|
|
94
|
-
const mcpProxy = new McpProxy({
|
|
95
|
+
const mcpProxy = new McpProxy({
|
|
96
|
+
idleTimeout: 5 * 60 * 1e3,
|
|
97
|
+
// 用户 chrome MCP 透传:追加 args/env(核心受保护参数不可覆盖,由 McpProxy 过滤)
|
|
98
|
+
userArgs: config.chromeMcp?.args,
|
|
99
|
+
env: config.chromeMcp?.env
|
|
100
|
+
});
|
|
95
101
|
let provider = null;
|
|
96
102
|
const service = new AIPanelService(
|
|
97
103
|
config,
|
|
@@ -158,9 +164,15 @@ function createAIPanelPlugin(options = {}) {
|
|
|
158
164
|
get serviceInstanceId() {
|
|
159
165
|
return serviceInstanceId;
|
|
160
166
|
},
|
|
161
|
-
|
|
167
|
+
get eventsToken() {
|
|
168
|
+
return service.eventsToken;
|
|
169
|
+
},
|
|
170
|
+
pushProviderEvent(event) {
|
|
171
|
+
service.pushProviderEvent(event);
|
|
172
|
+
},
|
|
173
|
+
getSessions: (activeSessionId) => {
|
|
162
174
|
if (!provider) return Promise.reject(new Error("Provider \u672A\u521D\u59CB\u5316"));
|
|
163
|
-
return provider.listSessions(service.workspaceRoot);
|
|
175
|
+
return provider.listSessions(service.workspaceRoot, activeSessionId);
|
|
164
176
|
},
|
|
165
177
|
createSession: () => {
|
|
166
178
|
if (!provider) return Promise.reject(new Error("Provider \u672A\u521D\u59CB\u5316"));
|
|
@@ -301,7 +313,7 @@ function createAIPanelPlugin(options = {}) {
|
|
|
301
313
|
];
|
|
302
314
|
const titleInject = `<script>
|
|
303
315
|
(function () {
|
|
304
|
-
var KEY = "
|
|
316
|
+
var KEY = "${SESSION_ID_KEY}";
|
|
305
317
|
if (!sessionStorage.getItem(KEY)) {
|
|
306
318
|
sessionStorage.setItem(KEY, "[" + Math.random().toString(36).slice(2, 10) + "]");
|
|
307
319
|
}
|
|
@@ -2,20 +2,27 @@
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import http from "http";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_HOSTNAME,
|
|
7
|
+
ENV_VSCODE_PORT,
|
|
8
|
+
VSCODE_EXTENSION_PORT,
|
|
9
|
+
VSCODE_ROUTE_FORMAT,
|
|
10
|
+
VSCODE_ROUTE_HEALTH
|
|
11
|
+
} from "@aipanel/core";
|
|
5
12
|
const filePath = process.argv[2];
|
|
6
13
|
if (!filePath) {
|
|
7
14
|
console.error("Usage: node format-bridge.cjs <filePath>");
|
|
8
15
|
process.exit(1);
|
|
9
16
|
}
|
|
10
17
|
const absPath = path.resolve(filePath);
|
|
11
|
-
const PORT = parseInt(process.env
|
|
18
|
+
const PORT = parseInt(process.env[ENV_VSCODE_PORT] || "", 10) || VSCODE_EXTENSION_PORT;
|
|
12
19
|
function touchFile() {
|
|
13
20
|
const now = /* @__PURE__ */ new Date();
|
|
14
21
|
fs.utimesSync(absPath, now, now);
|
|
15
22
|
}
|
|
16
23
|
function probeHealth() {
|
|
17
24
|
return new Promise((resolve) => {
|
|
18
|
-
const req = http.get(`http
|
|
25
|
+
const req = http.get(`http://${DEFAULT_HOSTNAME}:${PORT}${VSCODE_ROUTE_HEALTH}`, (res) => {
|
|
19
26
|
res.resume();
|
|
20
27
|
resolve(res.statusCode === 200);
|
|
21
28
|
});
|
|
@@ -31,9 +38,9 @@ function formatViaExtension() {
|
|
|
31
38
|
const body = JSON.stringify({ filePath: absPath });
|
|
32
39
|
const req = http.request(
|
|
33
40
|
{
|
|
34
|
-
hostname:
|
|
41
|
+
hostname: DEFAULT_HOSTNAME,
|
|
35
42
|
port: PORT,
|
|
36
|
-
path:
|
|
43
|
+
path: VSCODE_ROUTE_FORMAT,
|
|
37
44
|
method: "POST",
|
|
38
45
|
headers: { "Content-Type": "application/json" },
|
|
39
46
|
timeout: 5e3
|