vite-plugin-aipanel 1.2.6 → 1.2.8

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.
@@ -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;
@@ -5,6 +5,7 @@ function startProxyServer(targetUrl, port, options = {}) {
5
5
  return new Promise((resolve, reject) => {
6
6
  const target = new URL(targetUrl);
7
7
  const bridgeScript = options.bridgeScript ?? "";
8
+ const webAuthCookie = options.webAuthCookie ?? "";
8
9
  const agent = new http.Agent({
9
10
  keepAlive: true,
10
11
  keepAliveMsecs: 3e4,
@@ -32,6 +33,9 @@ function startProxyServer(targetUrl, port, options = {}) {
32
33
  headers: {
33
34
  ...req.headers,
34
35
  host: target.host,
36
+ // 注入 Provider Web 服务的 browser-session 认证 Cookie:代理作为已认证客户端
37
+ // 替 iframe 里的浏览器完成 dsh 0.1.2+ 的会话认证(浏览器无需换取/携带该 Cookie)。
38
+ ...webAuthCookie ? { cookie: webAuthCookie } : {},
35
39
  // 上游(如 dsh 的 trust fence)校验 Origin 必须匹配自身 origin;
36
40
  // 页面经代理访问时浏览器发的 Origin 是代理端口,须改写为目标 origin 否则 403
37
41
  origin: `http://${target.host}`,
@@ -104,6 +108,8 @@ function startProxyServer(targetUrl, port, options = {}) {
104
108
  headers: {
105
109
  ...req.headers,
106
110
  host: target.host,
111
+ // 与 HTTP 分支一致:注入认证 Cookie,通过 dsh 0.1.2+ 的 browser-auth 门禁
112
+ ...webAuthCookie ? { cookie: webAuthCookie } : {},
107
113
  // 与 HTTP 分支一致:改写 Origin 为目标 origin,通过 dsh 的 trust fence
108
114
  origin: `http://${target.host}`
109
115
  }
@@ -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;
@@ -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.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
- });
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
+ };
@@ -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);
@@ -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);
@@ -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
- getSessions: () => Promise<ChatSession[]>;
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
@@ -158,9 +158,15 @@ function createAIPanelPlugin(options = {}) {
158
158
  get serviceInstanceId() {
159
159
  return serviceInstanceId;
160
160
  },
161
- getSessions: () => {
161
+ get eventsToken() {
162
+ return service.eventsToken;
163
+ },
164
+ pushProviderEvent(event) {
165
+ service.pushProviderEvent(event);
166
+ },
167
+ getSessions: (activeSessionId) => {
162
168
  if (!provider) return Promise.reject(new Error("Provider \u672A\u521D\u59CB\u5316"));
163
- return provider.listSessions(service.workspaceRoot);
169
+ return provider.listSessions(service.workspaceRoot, activeSessionId);
164
170
  },
165
171
  createSession: () => {
166
172
  if (!provider) return Promise.reject(new Error("Provider \u672A\u521D\u59CB\u5316"));