@zhin.js/adapter-sandbox 3.0.1 → 3.0.3

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.
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Sandbox 传输 — Node WebSocket;Edge HTTP+SSE(见 sandbox-sse-hub / fetch-sse)。
3
+ */
4
+ import { EventEmitter } from "node:events";
5
+ import { broadcastSandboxSse, closeSandboxSseSession } from "./sandbox-sse-hub.js";
6
+ import { Adapter, Message, segment, } from "zhin.js";
7
+ /** 无 WS 时的占位 socket(仅用于 bot:list,不可收发) */
8
+ export function createOfflineSandboxWs() {
9
+ return { send: () => { }, close: () => { } };
10
+ }
11
+ export function createSandboxSseTransport(sessionId, onClose) {
12
+ return {
13
+ sessionId,
14
+ send(data) {
15
+ broadcastSandboxSse(sessionId, data);
16
+ },
17
+ close() {
18
+ closeSandboxSseSession(sessionId);
19
+ onClose?.();
20
+ },
21
+ };
22
+ }
23
+ function envVar(key) {
24
+ const g = globalThis;
25
+ return g.Deno?.env.get(key) ?? g.process?.env[key];
26
+ }
27
+ export function resolveSandboxBot(appConfig) {
28
+ const bots = appConfig.bots;
29
+ const entry = bots?.find((b) => b.context === "sandbox");
30
+ const fixedName = typeof entry?.name === "string" ? entry.name : undefined;
31
+ const name = fixedName ||
32
+ envVar("SANDBOX_BOT_NAME") ||
33
+ "sandbox-bot";
34
+ const owner = (typeof entry?.owner === "string" && entry.owner) ||
35
+ envVar("SANDBOX_BOT_OWNER") ||
36
+ "sandbox-user";
37
+ const transportRaw = (typeof entry?.transport === "string" && entry.transport) ||
38
+ envVar("SANDBOX_TRANSPORT") ||
39
+ "websocket";
40
+ const transport = transportRaw === "http-sse" || transportRaw === "sse" ? "http-sse" : "websocket";
41
+ return {
42
+ context: "sandbox",
43
+ name,
44
+ owner,
45
+ randomNamePerConnection: !fixedName,
46
+ transport,
47
+ };
48
+ }
49
+ /** 标准 WebSocket 在 upgrade 后可能尚未 OPEN;Node `ws` 在 connection 回调里通常已可 send */
50
+ function whenWsOpen(ws, fn) {
51
+ const std = ws;
52
+ if (typeof std.readyState === "number") {
53
+ if (std.readyState === WebSocket.OPEN) {
54
+ fn();
55
+ return;
56
+ }
57
+ std.addEventListener("open", fn, { once: true });
58
+ return;
59
+ }
60
+ fn();
61
+ }
62
+ export function bindSandboxWsSocket(ws, handlers) {
63
+ if (typeof ws.on === "function") {
64
+ const onMessage = (...args) => {
65
+ const data = args[0];
66
+ const raw = typeof data === "string"
67
+ ? data
68
+ : data instanceof ArrayBuffer
69
+ ? new TextDecoder().decode(data)
70
+ : Buffer.isBuffer(data)
71
+ ? data.toString()
72
+ : String(data ?? "");
73
+ handlers.onMessage(raw);
74
+ };
75
+ ws.on("message", onMessage);
76
+ ws.on("close", handlers.onClose);
77
+ if (handlers.onError)
78
+ ws.on("error", handlers.onError);
79
+ return () => {
80
+ ws.off?.("message", onMessage);
81
+ ws.off?.("close", handlers.onClose);
82
+ if (handlers.onError)
83
+ ws.off?.("error", handlers.onError);
84
+ };
85
+ }
86
+ const onMessage = (ev) => {
87
+ const data = ev.data;
88
+ handlers.onMessage(typeof data === "string" ? data : "");
89
+ };
90
+ const onClose = () => handlers.onClose();
91
+ const onError = handlers.onError
92
+ ? () => handlers.onError?.(new Error("WebSocket error"))
93
+ : undefined;
94
+ ws.addEventListener("message", onMessage);
95
+ ws.addEventListener("close", onClose);
96
+ if (onError)
97
+ ws.addEventListener("error", onError);
98
+ return () => {
99
+ ws.removeEventListener("message", onMessage);
100
+ ws.removeEventListener("close", onClose);
101
+ if (onError)
102
+ ws.removeEventListener("error", onError);
103
+ };
104
+ }
105
+ export function parseSandboxWsPayload(raw) {
106
+ let payload;
107
+ try {
108
+ payload = JSON.parse(raw);
109
+ }
110
+ catch {
111
+ payload = { text: raw };
112
+ }
113
+ const type = payload.type ?? "private";
114
+ const id = payload.id ?? "sandbox-user";
115
+ const content = typeof payload.content === "string"
116
+ ? [{ type: "text", data: { text: payload.content } }]
117
+ : Array.isArray(payload.content)
118
+ ? payload.content
119
+ : [{ type: "text", data: { text: payload.text ?? raw } }];
120
+ return { type, id, content, timestamp: payload.timestamp ?? Date.now() };
121
+ }
122
+ export class SandboxWsBot extends EventEmitter {
123
+ adapter;
124
+ $config;
125
+ $connected = false;
126
+ #unbind = null;
127
+ get $id() {
128
+ return this.$config.name;
129
+ }
130
+ constructor(adapter, $config) {
131
+ super();
132
+ this.adapter = adapter;
133
+ this.$config = $config;
134
+ }
135
+ async $connect() {
136
+ if (this.$config.offline || !this.$config.ws)
137
+ return;
138
+ const ws = this.$config.ws;
139
+ if (typeof ws.on !== "function" && typeof ws.addEventListener !== "function") {
140
+ this.$connected = true;
141
+ return;
142
+ }
143
+ this.#unbind = bindSandboxWsSocket(ws, {
144
+ onMessage: (raw) => {
145
+ const { type, id, content, timestamp } = parseSandboxWsPayload(raw);
146
+ this.adapter.emit("message.receive", this.$formatMessage({ content, type, id, ts: timestamp }));
147
+ },
148
+ onClose: () => {
149
+ this.$connected = false;
150
+ if (!this.$config.offline) {
151
+ this.adapter.bots.delete(this.$id);
152
+ }
153
+ },
154
+ onError: (err) => {
155
+ this.adapter.logger.warn(`sandbox ws error (${this.$config.name}): ${err instanceof Error ? err.message : String(err)}`);
156
+ },
157
+ });
158
+ this.$connected = true;
159
+ }
160
+ async $disconnect() {
161
+ this.#unbind?.();
162
+ this.#unbind = null;
163
+ this.$config.ws?.close();
164
+ this.$connected = false;
165
+ }
166
+ $formatMessage({ content, type, id, ts }) {
167
+ if (!this.$config.owner)
168
+ this.$config.owner = id;
169
+ const message = Message.from({ content, type, id, ts }, {
170
+ $id: `${ts}`,
171
+ $adapter: "sandbox",
172
+ $bot: this.$config.name,
173
+ $sender: { id, name: "mock" },
174
+ $channel: { id, type },
175
+ $content: content,
176
+ $raw: segment.raw(content),
177
+ $timestamp: ts,
178
+ $recall: async () => { },
179
+ $reply: async (replyContent, quote) => {
180
+ const normalized = Array.isArray(replyContent) ? replyContent : [replyContent];
181
+ if (quote) {
182
+ normalized.unshift({
183
+ type: "reply",
184
+ data: {
185
+ id: typeof quote === "boolean" ? message.$id : quote,
186
+ },
187
+ });
188
+ }
189
+ return await this.adapter.sendMessage({
190
+ context: "sandbox",
191
+ bot: this.$config.name,
192
+ content: normalized,
193
+ id,
194
+ type,
195
+ });
196
+ },
197
+ });
198
+ return message;
199
+ }
200
+ async $sendMessage(options) {
201
+ if (!this.$connected)
202
+ return "";
203
+ const ws = this.$config.ws;
204
+ if (!ws)
205
+ return "";
206
+ ws.send(JSON.stringify({
207
+ ...options,
208
+ content: options.content,
209
+ timestamp: Date.now(),
210
+ }));
211
+ return "";
212
+ }
213
+ async $recallMessage(_id) { }
214
+ }
215
+ export class SandboxWsHostAdapter extends Adapter {
216
+ defaults;
217
+ #sseSessions = new Map();
218
+ constructor(plugin, defaults) {
219
+ super(plugin, "sandbox", []);
220
+ this.defaults = defaults;
221
+ }
222
+ get transport() {
223
+ return this.defaults.transport;
224
+ }
225
+ hasSseSession(sessionId) {
226
+ return this.#sseSessions.has(sessionId);
227
+ }
228
+ getBotBySseSession(sessionId) {
229
+ const name = this.#sseSessions.get(sessionId);
230
+ return name ? this.bots.get(name) : undefined;
231
+ }
232
+ createBot(config) {
233
+ const bot = new SandboxWsBot(this, config);
234
+ this.bots.set(bot.$id, bot);
235
+ return bot;
236
+ }
237
+ /** `zhin.config.yml` 中 `context: sandbox` + 固定 `name` 时,启动即出现在 bot:list(离线) */
238
+ registerConfiguredPlaceholder() {
239
+ if (this.defaults.randomNamePerConnection)
240
+ return;
241
+ if (this.bots.has(this.defaults.name))
242
+ return;
243
+ this.createBot({
244
+ context: "sandbox",
245
+ name: this.defaults.name,
246
+ owner: this.defaults.owner,
247
+ ws: createOfflineSandboxWs(),
248
+ offline: true,
249
+ });
250
+ }
251
+ /** Edge / 外部 upgrade:注入已建立的 WebSocket */
252
+ acceptWebSocket(ws, overrides) {
253
+ const name = overrides?.name ??
254
+ (this.defaults.randomNamePerConnection
255
+ ? `sandbox-${crypto.randomUUID().slice(0, 8)}`
256
+ : this.defaults.name);
257
+ const owner = overrides?.owner ?? this.defaults.owner;
258
+ const existing = this.bots.get(name);
259
+ if (existing) {
260
+ void existing.$disconnect();
261
+ this.bots.delete(name);
262
+ }
263
+ const bot = this.createBot({ context: "sandbox", ws, name, owner, offline: false });
264
+ void bot.$connect();
265
+ if (!this.defaults.randomNamePerConnection) {
266
+ const readyPayload = JSON.stringify({
267
+ type: "ready",
268
+ id: owner,
269
+ bot: name,
270
+ content: [
271
+ {
272
+ type: "text",
273
+ data: {
274
+ text: [
275
+ `已连接 Sandbox「${name}」`,
276
+ "与 Node Host 控制台沙盒协议一致(/sandbox)",
277
+ "命令: help · ping · zt · status",
278
+ ].join("\n"),
279
+ },
280
+ },
281
+ ],
282
+ timestamp: Date.now(),
283
+ });
284
+ whenWsOpen(ws, () => ws.send(readyPayload));
285
+ }
286
+ return bot;
287
+ }
288
+ /** Edge SSE:建立会话并绑定固定 bot */
289
+ acceptSseSession(sessionId, overrides) {
290
+ const name = overrides?.name ?? this.defaults.name;
291
+ const owner = overrides?.owner ?? this.defaults.owner;
292
+ const existing = this.bots.get(name);
293
+ if (existing) {
294
+ void existing.$disconnect();
295
+ this.bots.delete(name);
296
+ }
297
+ const ws = createSandboxSseTransport(sessionId, () => {
298
+ this.#sseSessions.delete(sessionId);
299
+ this.bots.delete(name);
300
+ });
301
+ const bot = this.createBot({ context: "sandbox", ws, name, owner, offline: false });
302
+ this.#sseSessions.set(sessionId, name);
303
+ void bot.$connect();
304
+ if (!this.defaults.randomNamePerConnection) {
305
+ const readyPayload = JSON.stringify({
306
+ type: "ready",
307
+ id: owner,
308
+ bot: name,
309
+ content: [
310
+ {
311
+ type: "text",
312
+ data: {
313
+ text: [
314
+ `已连接 Sandbox「${name}」`,
315
+ "传输: HTTP + SSE(Edge)",
316
+ "命令: help · ping · zt · status",
317
+ ].join("\n"),
318
+ },
319
+ },
320
+ ],
321
+ timestamp: Date.now(),
322
+ });
323
+ ws.send(readyPayload);
324
+ }
325
+ return bot;
326
+ }
327
+ /** POST /sandbox/message — 与 WebSocket 帧同格式的 JSON 字符串 */
328
+ ingestSseClientMessage(sessionId, raw) {
329
+ const bot = this.getBotBySseSession(sessionId);
330
+ if (!bot || !bot.$connected) {
331
+ throw new Error(`sandbox session not connected: ${sessionId}`);
332
+ }
333
+ const { type, id, content, timestamp } = parseSandboxWsPayload(raw);
334
+ this.emit("message.receive", bot.$formatMessage({ content, type, id, ts: timestamp }));
335
+ }
336
+ }
337
+ //# sourceMappingURL=sandbox-ws.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-ws.js","sourceRoot":"","sources":["../src/sandbox-ws.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EACL,OAAO,EAEP,OAAO,EACP,OAAO,GAMR,MAAM,SAAS,CAAC;AAWjB,0CAA0C;AAC1C,MAAM,UAAU,sBAAsB;IACpC,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,SAAiB,EACjB,OAAoB;IAEpB,OAAO;QACL,SAAS;QACT,IAAI,CAAC,IAAY;YACf,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACvC,CAAC;QACD,KAAK;YACH,sBAAsB,CAAC,SAAS,CAAC,CAAC;YAClC,OAAO,EAAE,EAAE,CAAC;QACd,CAAC;KACF,CAAC;AACJ,CAAC;AAsCD,SAAS,MAAM,CAAC,GAAW;IACzB,MAAM,CAAC,GAAG,UAGT,CAAC;IACF,OAAO,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,SAAkC;IAElC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAkD,CAAC;IAC1E,MAAM,KAAK,GAAG,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,OAAO,KAAK,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3E,MAAM,IAAI,GACR,SAAS;QACT,MAAM,CAAC,kBAAkB,CAAC;QAC1B,aAAa,CAAC;IAChB,MAAM,KAAK,GACT,CAAC,OAAO,KAAK,EAAE,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC;QACjD,MAAM,CAAC,mBAAmB,CAAC;QAC3B,cAAc,CAAC;IACjB,MAAM,YAAY,GAChB,CAAC,OAAO,KAAK,EAAE,SAAS,KAAK,QAAQ,IAAI,KAAK,CAAC,SAAS,CAAC;QACzD,MAAM,CAAC,mBAAmB,CAAC;QAC3B,WAAW,CAAC;IACd,MAAM,SAAS,GACb,YAAY,KAAK,UAAU,IAAI,YAAY,KAAK,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC;IACnF,OAAO;QACL,OAAO,EAAE,SAAS;QAClB,IAAI;QACJ,KAAK;QACL,uBAAuB,EAAE,CAAC,SAAS;QACnC,SAAS;KACV,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,SAAS,UAAU,CAAC,EAAmB,EAAE,EAAc;IACrD,MAAM,GAAG,GAAG,EAAe,CAAC;IAC5B,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;QACvC,IAAI,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;YACtC,EAAE,EAAE,CAAC;YACL,OAAO;QACT,CAAC;QACD,GAAG,CAAC,gBAAgB,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,OAAO;IACT,CAAC;IACD,EAAE,EAAE,CAAC;AACP,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,EAAmB,EACnB,QAIC;IAED,IAAI,OAAO,EAAE,CAAC,EAAE,KAAK,UAAU,EAAE,CAAC;QAChC,MAAM,SAAS,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE;YACvC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YACrB,MAAM,GAAG,GAAG,OAAO,IAAI,KAAK,QAAQ;gBAClC,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,IAAI,YAAY,WAAW;oBAC7B,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;oBAChC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;wBACvB,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE;wBACjB,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;YACvB,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC,CAAC;QACF,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC5B,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QACjC,IAAI,QAAQ,CAAC,OAAO;YAAE,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QACvD,OAAO,GAAG,EAAE;YACV,EAAE,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YAC/B,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;YACpC,IAAI,QAAQ,CAAC,OAAO;gBAAE,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC5D,CAAC,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,CAAC,EAAS,EAAE,EAAE;QAC9B,MAAM,IAAI,GAAI,EAAmB,CAAC,IAAI,CAAC;QACvC,QAAQ,CAAC,SAAS,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC3D,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO;QAC9B,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;QACxD,CAAC,CAAC,SAAS,CAAC;IACd,EAAE,CAAC,gBAAiB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC3C,EAAE,CAAC,gBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACvC,IAAI,OAAO;QAAE,EAAE,CAAC,gBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACpD,OAAO,GAAG,EAAE;QACV,EAAE,CAAC,mBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC9C,EAAE,CAAC,mBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC1C,IAAI,OAAO;YAAE,EAAE,CAAC,mBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAM/C,IAAI,OAMH,CAAC;IACF,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAmB,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;IAC1B,CAAC;IACD,MAAM,IAAI,GAAI,OAAO,CAAC,IAAoB,IAAI,SAAS,CAAC;IACxD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,IAAI,cAAc,CAAC;IACxC,MAAM,OAAO,GAAqB,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ;QACnE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QACrD,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;YAChC,CAAC,CAAC,OAAO,CAAC,OAAO;YACjB,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IAC5D,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;AAC3E,CAAC;AASD,MAAM,OAAO,YAAa,SAAQ,YAAY;IASnC;IACA;IATT,UAAU,GAAG,KAAK,CAAC;IACnB,OAAO,GAAwB,IAAI,CAAC;IAEpC,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED,YACS,OAA6B,EAC7B,OAAwB;QAE/B,KAAK,EAAE,CAAC;QAHD,YAAO,GAAP,OAAO,CAAsB;QAC7B,YAAO,GAAP,OAAO,CAAiB;IAGjC,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,OAAO;QACrD,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,IAAI,OAAO,EAAE,CAAC,EAAE,KAAK,UAAU,IAAI,OAAO,EAAE,CAAC,gBAAgB,KAAK,UAAU,EAAE,CAAC;YAC7E,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACvB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,mBAAmB,CAAC,EAAE,EAAE;YACrC,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE;gBACjB,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC;gBACpE,IAAI,CAAC,OAAO,CAAC,IAAI,CACf,iBAAiB,EACjB,IAAI,CAAC,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAC1D,CAAC;YACJ,CAAC;YACD,OAAO,EAAE,GAAG,EAAE;gBACZ,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;gBACxB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;oBAC1B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACrC,CAAC;YACH,CAAC;YACD,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBACf,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CACtB,qBAAqB,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC/F,CAAC;YACJ,CAAC;SACF,CAAC,CAAC;QACH,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,WAAW;QACf,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;IAC1B,CAAC;IAED,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAY;QAChD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK;YAAE,IAAI,CAAC,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC;QACjD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAC1B,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,EACzB;YACE,GAAG,EAAE,GAAG,EAAE,EAAE;YACZ,QAAQ,EAAE,SAAkB;YAC5B,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI;YACvB,OAAO,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;YAC7B,QAAQ,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;YACtB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAC1B,UAAU,EAAE,EAAE;YACd,OAAO,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;YACvB,MAAM,EAAE,KAAK,EAAE,YAAyB,EAAE,KAAwB,EAAE,EAAE;gBACpE,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;gBAC/E,IAAI,KAAK,EAAE,CAAC;oBACV,UAAU,CAAC,OAAO,CAAC;wBACjB,IAAI,EAAE,OAAO;wBACb,IAAI,EAAE;4BACJ,EAAE,EAAE,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK;yBACrD;qBACF,CAAC,CAAC;gBACL,CAAC;gBACD,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC;oBACpC,OAAO,EAAE,SAAS;oBAClB,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI;oBACtB,OAAO,EAAE,UAAU;oBACnB,EAAE;oBACF,IAAI;iBACL,CAAC,CAAC;YACL,CAAC;SACF,CACF,CAAC;QACF,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAAoB;QACrC,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO,EAAE,CAAC;QAChC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,IAAI,CAAC,EAAE;YAAE,OAAO,EAAE,CAAC;QACnB,EAAE,CAAC,IAAI,CACL,IAAI,CAAC,SAAS,CAAC;YACb,GAAG,OAAO;YACV,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC,CACH,CAAC;QACF,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,GAAW,IAAkB,CAAC;CACpD;AAED,MAAM,OAAO,oBAAqB,SAAQ,OAAqB;IAKxC;IAJZ,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAElD,YACE,MAAc,EACK,QAA4B;QAE/C,KAAK,CAAC,MAAM,EAAE,SAAkC,EAAE,EAAE,CAAC,CAAC;QAFnC,aAAQ,GAAR,QAAQ,CAAoB;IAGjD,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;IACjC,CAAC;IAED,aAAa,CAAC,SAAiB;QAC7B,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC1C,CAAC;IAED,kBAAkB,CAAC,SAAiB;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAChD,CAAC;IAED,SAAS,CAAC,MAAuB;QAC/B,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC5B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,+EAA+E;IAC/E,6BAA6B;QAC3B,IAAI,IAAI,CAAC,QAAQ,CAAC,uBAAuB;YAAE,OAAO;QAClD,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO;QAC9C,IAAI,CAAC,SAAS,CAAC;YACb,OAAO,EAAE,SAAS;YAClB,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;YACxB,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK;YAC1B,EAAE,EAAE,sBAAsB,EAAE;YAC5B,OAAO,EAAE,IAAI;SACd,CAAC,CAAC;IACL,CAAC;IAED,yCAAyC;IACzC,eAAe,CACb,EAAmB,EACnB,SAA4D;QAE5D,MAAM,IAAI,GAAG,SAAS,EAAE,IAAI;YAC1B,CAAC,IAAI,CAAC,QAAQ,CAAC,uBAAuB;gBACpC,CAAC,CAAC,WAAW,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE;gBAC9C,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,KAAK,GAAG,SAAS,EAAE,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,QAAQ,EAAE,CAAC;YACb,KAAK,QAAQ,CAAC,WAAW,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACpF,KAAK,GAAG,CAAC,QAAQ,EAAE,CAAC;QACpB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,uBAAuB,EAAE,CAAC;YAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;gBAClC,IAAI,EAAE,OAAO;gBACb,EAAE,EAAE,KAAK;gBACT,GAAG,EAAE,IAAI;gBACT,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE;4BACJ,IAAI,EAAE;gCACJ,eAAe,IAAI,GAAG;gCACtB,iCAAiC;gCACjC,+BAA+B;6BAChC,CAAC,IAAI,CAAC,IAAI,CAAC;yBACb;qBACF;iBACF;gBACD,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;aACtB,CAAC,CAAC;YACH,UAAU,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,6BAA6B;IAC7B,gBAAgB,CACd,SAAiB,EACjB,SAA4D;QAE5D,MAAM,IAAI,GAAG,SAAS,EAAE,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QACnD,MAAM,KAAK,GAAG,SAAS,EAAE,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,QAAQ,EAAE,CAAC;YACb,KAAK,QAAQ,CAAC,WAAW,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QACD,MAAM,EAAE,GAAG,yBAAyB,CAAC,SAAS,EAAE,GAAG,EAAE;YACnD,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACpC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACpF,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACvC,KAAK,GAAG,CAAC,QAAQ,EAAE,CAAC;QACpB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,uBAAuB,EAAE,CAAC;YAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;gBAClC,IAAI,EAAE,OAAO;gBACb,EAAE,EAAE,KAAK;gBACT,GAAG,EAAE,IAAI;gBACT,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE;4BACJ,IAAI,EAAE;gCACJ,eAAe,IAAI,GAAG;gCACtB,sBAAsB;gCACtB,+BAA+B;6BAChC,CAAC,IAAI,CAAC,IAAI,CAAC;yBACb;qBACF;iBACF;gBACD,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;aACtB,CAAC,CAAC;YACH,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACxB,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,yDAAyD;IACzD,sBAAsB,CAAC,SAAiB,EAAE,GAAW;QACnD,MAAM,GAAG,GAAG,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;QAC/C,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,kCAAkC,SAAS,EAAE,CAAC,CAAC;QACjE,CAAC;QACD,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,CACP,iBAAiB,EACjB,GAAG,CAAC,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CACzD,CAAC;IACJ,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-sandbox",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "Zhin.js adapter for local testing and development",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -43,25 +43,30 @@
43
43
  "directory": "plugins/adapters/sandbox"
44
44
  },
45
45
  "devDependencies": {
46
- "@types/react": "^19.2.14",
46
+ "@types/react": "^19.2.15",
47
47
  "@types/react-dom": "^19.2.3",
48
48
  "radix-ui": "^1.4.3",
49
49
  "lucide-react": "^1.16.0",
50
50
  "typescript": "^6.0.3",
51
- "@zhin.js/cli": "1.0.78",
52
- "zhin.js": "1.0.82"
51
+ "@zhin.js/http-host": "0.1.3",
52
+ "@zhin.js/cli": "1.0.80",
53
+ "zhin.js": "1.0.84"
53
54
  },
54
55
  "peerDependencies": {
55
- "@zhin.js/core": "1.1.24",
56
- "@zhin.js/client": "1.1.1",
57
- "@zhin.js/http": "1.0.75",
58
- "@zhin.js/console": "3.0.1",
59
- "zhin.js": "1.0.82"
56
+ "@zhin.js/core": "1.1.26",
57
+ "@zhin.js/client": "1.1.3",
58
+ "@zhin.js/http": "1.0.77",
59
+ "@zhin.js/http-host": "0.1.3",
60
+ "@zhin.js/console": "3.0.3",
61
+ "zhin.js": "1.0.84"
60
62
  },
61
63
  "peerDependenciesMeta": {
62
64
  "@zhin.js/http": {
63
65
  "optional": true
64
66
  },
67
+ "@zhin.js/http-host": {
68
+ "optional": true
69
+ },
65
70
  "@zhin.js/console": {
66
71
  "optional": true
67
72
  },
@@ -0,0 +1,87 @@
1
+ import { registerFetchRoute, type RouteTable } from "@zhin.js/http-host/edge";
2
+ import type { RouterContext } from "@zhin.js/http-host/edge";
3
+ import type { SandboxWsHostAdapter } from "./sandbox-ws.js";
4
+ import { subscribeSandboxSse } from "./sandbox-sse-hub.js";
5
+
6
+ export type RegisterSandboxSseOptions = {
7
+ eventsPath?: string;
8
+ messagePath?: string;
9
+ };
10
+
11
+ function resolveSessionId(ctx: RouterContext): string | undefined {
12
+ const q = ctx.query.session?.trim();
13
+ if (q) return q;
14
+ const h = ctx.get("x-sandbox-session")?.trim();
15
+ if (h) return h;
16
+ const body = ctx.request.body as { session?: string } | undefined;
17
+ if (body && typeof body.session === "string" && body.session.trim()) {
18
+ return body.session.trim();
19
+ }
20
+ return undefined;
21
+ }
22
+
23
+ /**
24
+ * Edge / Vercel 等:Sandbox 使用 HTTP POST 上行 + SSE 下行(无 WebSocket upgrade)。
25
+ */
26
+ export function registerSandboxSseRoutes(
27
+ table: RouteTable,
28
+ getAdapter: () => SandboxWsHostAdapter,
29
+ options: RegisterSandboxSseOptions = {},
30
+ ): void {
31
+ const eventsPath = options.eventsPath ?? "/sandbox/events";
32
+ const messagePath = options.messagePath ?? "/sandbox/message";
33
+
34
+ registerFetchRoute(table, "GET", eventsPath, (ctx: RouterContext) => {
35
+ const sessionId = resolveSessionId(ctx);
36
+ if (!sessionId) {
37
+ ctx.status = 400;
38
+ ctx.body = { success: false, error: "Missing session (query ?session= or X-Sandbox-Session)" };
39
+ return;
40
+ }
41
+ const adapter = getAdapter();
42
+ if (!adapter.hasSseSession(sessionId)) {
43
+ adapter.acceptSseSession(sessionId);
44
+ }
45
+ const lastEventId = ctx.query["last-event-id"] ?? ctx.query.lastEventId;
46
+ const stream = subscribeSandboxSse(sessionId, lastEventId);
47
+ ctx.status = 200;
48
+ ctx.set("Content-Type", "text/event-stream; charset=utf-8");
49
+ ctx.set("Cache-Control", "no-cache, no-transform");
50
+ ctx.set("Connection", "keep-alive");
51
+ ctx.set("X-Accel-Buffering", "no");
52
+ ctx.body = stream;
53
+ });
54
+
55
+ registerFetchRoute(table, "POST", messagePath, async (ctx: RouterContext) => {
56
+ const sessionId = resolveSessionId(ctx);
57
+ if (!sessionId) {
58
+ ctx.status = 400;
59
+ ctx.body = { success: false, error: "Missing session (X-Sandbox-Session or body.session)" };
60
+ return;
61
+ }
62
+ const adapter = getAdapter();
63
+ if (!adapter.hasSseSession(sessionId)) {
64
+ ctx.status = 404;
65
+ ctx.body = { success: false, error: "No sandbox session; open GET /sandbox/events first" };
66
+ return;
67
+ }
68
+ const body = ctx.request.body;
69
+ const raw = typeof body === "string"
70
+ ? body
71
+ : body === undefined || body === null
72
+ ? ""
73
+ : JSON.stringify(body);
74
+ if (!raw) {
75
+ ctx.status = 400;
76
+ ctx.body = { success: false, error: "Empty message body" };
77
+ return;
78
+ }
79
+ try {
80
+ adapter.ingestSseClientMessage(sessionId, raw);
81
+ ctx.body = { success: true };
82
+ } catch (err) {
83
+ ctx.status = 500;
84
+ ctx.body = { success: false, error: (err as Error).message };
85
+ }
86
+ });
87
+ }
@@ -0,0 +1,23 @@
1
+ import { registerWebSocketRoute, type RouteTable } from "@zhin.js/http-host/edge";
2
+ import type { SandboxWsHostAdapter, SandboxWsSocket } from "./sandbox-ws.js";
3
+
4
+ export type RegisterSandboxWsOptions = {
5
+ /** 兼容旧路径,默认 `/ws` */
6
+ legacyPaths?: string[];
7
+ };
8
+
9
+ /**
10
+ * Deno / Edge:在 {@link RouteTable} 上注册 `/sandbox` WebSocket(http-host Fetch upgrade)。
11
+ */
12
+ export function registerSandboxWebSocketRoutes(
13
+ table: RouteTable,
14
+ getAdapter: () => SandboxWsHostAdapter,
15
+ options: RegisterSandboxWsOptions = {},
16
+ ): void {
17
+ const paths = ["/sandbox", ...(options.legacyPaths ?? ["/ws"])];
18
+ for (const path of paths) {
19
+ registerWebSocketRoute(table, path, (ws: unknown) => {
20
+ getAdapter().acceptWebSocket(ws as SandboxWsSocket);
21
+ });
22
+ }
23
+ }