@zhin.js/adapter-sandbox 3.0.2 → 3.0.4
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/CHANGELOG.md +25 -0
- package/README.md +16 -2
- package/client/Sandbox.tsx +111 -31
- package/client/sandboxTransport.ts +61 -0
- package/dist/index.js +7 -7
- package/lib/fetch-sse.d.ts +11 -0
- package/lib/fetch-sse.d.ts.map +1 -0
- package/lib/fetch-sse.js +76 -0
- package/lib/fetch-sse.js.map +1 -0
- package/lib/fetch-ws.d.ts +11 -0
- package/lib/fetch-ws.d.ts.map +1 -0
- package/lib/fetch-ws.js +13 -0
- package/lib/fetch-ws.js.map +1 -0
- package/lib/index.d.ts +16 -48
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +25 -132
- package/lib/index.js.map +1 -1
- package/lib/sandbox-sse-hub.d.ts +10 -0
- package/lib/sandbox-sse-hub.d.ts.map +1 -0
- package/lib/sandbox-sse-hub.js +101 -0
- package/lib/sandbox-sse-hub.js.map +1 -0
- package/lib/sandbox-ws.d.ts +91 -0
- package/lib/sandbox-ws.d.ts.map +1 -0
- package/lib/sandbox-ws.js +337 -0
- package/lib/sandbox-ws.js.map +1 -0
- package/package.json +14 -9
- package/src/fetch-sse.ts +87 -0
- package/src/fetch-ws.ts +23 -0
- package/src/index.ts +57 -181
- package/src/sandbox-sse-hub.ts +118 -0
- package/src/sandbox-ws.ts +462 -0
|
@@ -0,0 +1,462 @@
|
|
|
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 {
|
|
7
|
+
Adapter,
|
|
8
|
+
Bot,
|
|
9
|
+
Message,
|
|
10
|
+
segment,
|
|
11
|
+
type MessageElement,
|
|
12
|
+
type MessageType,
|
|
13
|
+
type Plugin,
|
|
14
|
+
type SendContent,
|
|
15
|
+
type SendOptions,
|
|
16
|
+
} from "zhin.js";
|
|
17
|
+
|
|
18
|
+
export interface SandboxWsConfig {
|
|
19
|
+
context: "sandbox";
|
|
20
|
+
ws?: SandboxWsSocket;
|
|
21
|
+
name: string;
|
|
22
|
+
owner?: string;
|
|
23
|
+
/** yaml 预置名:启动时占位,WS 连接前在 bot:list 显示为离线 */
|
|
24
|
+
offline?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 无 WS 时的占位 socket(仅用于 bot:list,不可收发) */
|
|
28
|
+
export function createOfflineSandboxWs(): SandboxWsSocket {
|
|
29
|
+
return { send: () => {}, close: () => {} };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createSandboxSseTransport(
|
|
33
|
+
sessionId: string,
|
|
34
|
+
onClose?: () => void,
|
|
35
|
+
): SandboxWsSocket & { sessionId: string } {
|
|
36
|
+
return {
|
|
37
|
+
sessionId,
|
|
38
|
+
send(data: string) {
|
|
39
|
+
broadcastSandboxSse(sessionId, data);
|
|
40
|
+
},
|
|
41
|
+
close() {
|
|
42
|
+
closeSandboxSseSession(sessionId);
|
|
43
|
+
onClose?.();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 兼容 `ws` 包与标准 WebSocket */
|
|
49
|
+
export type SandboxWsSocket = {
|
|
50
|
+
send(data: string): void;
|
|
51
|
+
close(code?: number, reason?: string): void;
|
|
52
|
+
on?(event: "message" | "close" | "error", listener: (...args: unknown[]) => void): void;
|
|
53
|
+
off?(
|
|
54
|
+
event: "message" | "close" | "error",
|
|
55
|
+
listener: (...args: unknown[]) => void,
|
|
56
|
+
): void;
|
|
57
|
+
addEventListener?(
|
|
58
|
+
type: "message" | "close" | "error",
|
|
59
|
+
listener: (ev: Event | MessageEvent | CloseEvent) => void,
|
|
60
|
+
): void;
|
|
61
|
+
removeEventListener?(
|
|
62
|
+
type: "message" | "close" | "error",
|
|
63
|
+
listener: (ev: Event | MessageEvent | CloseEvent) => void,
|
|
64
|
+
): void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type SandboxBotDefaults = {
|
|
68
|
+
name: string;
|
|
69
|
+
owner: string;
|
|
70
|
+
/** true:每连接随机 bot 名(Node 本地默认);false:固定 name(Edge 单实例) */
|
|
71
|
+
randomNamePerConnection?: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type SandboxTransport = "websocket" | "http-sse";
|
|
75
|
+
|
|
76
|
+
export type ResolvedSandboxBot = {
|
|
77
|
+
context: "sandbox";
|
|
78
|
+
name: string;
|
|
79
|
+
owner: string;
|
|
80
|
+
randomNamePerConnection: boolean;
|
|
81
|
+
transport: SandboxTransport;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function envVar(key: string): string | undefined {
|
|
85
|
+
const g = globalThis as {
|
|
86
|
+
Deno?: { env: { get(k: string): string | undefined } };
|
|
87
|
+
process?: { env: Record<string, string | undefined> };
|
|
88
|
+
};
|
|
89
|
+
return g.Deno?.env.get(key) ?? g.process?.env[key];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveSandboxBot(
|
|
93
|
+
appConfig: Record<string, unknown>,
|
|
94
|
+
): ResolvedSandboxBot {
|
|
95
|
+
const bots = appConfig.bots as Array<Record<string, unknown>> | undefined;
|
|
96
|
+
const entry = bots?.find((b) => b.context === "sandbox");
|
|
97
|
+
const fixedName = typeof entry?.name === "string" ? entry.name : undefined;
|
|
98
|
+
const name =
|
|
99
|
+
fixedName ||
|
|
100
|
+
envVar("SANDBOX_BOT_NAME") ||
|
|
101
|
+
"sandbox-bot";
|
|
102
|
+
const owner =
|
|
103
|
+
(typeof entry?.owner === "string" && entry.owner) ||
|
|
104
|
+
envVar("SANDBOX_BOT_OWNER") ||
|
|
105
|
+
"sandbox-user";
|
|
106
|
+
const transportRaw =
|
|
107
|
+
(typeof entry?.transport === "string" && entry.transport) ||
|
|
108
|
+
envVar("SANDBOX_TRANSPORT") ||
|
|
109
|
+
"websocket";
|
|
110
|
+
const transport: SandboxTransport =
|
|
111
|
+
transportRaw === "http-sse" || transportRaw === "sse" ? "http-sse" : "websocket";
|
|
112
|
+
return {
|
|
113
|
+
context: "sandbox",
|
|
114
|
+
name,
|
|
115
|
+
owner,
|
|
116
|
+
randomNamePerConnection: !fixedName,
|
|
117
|
+
transport,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 标准 WebSocket 在 upgrade 后可能尚未 OPEN;Node `ws` 在 connection 回调里通常已可 send */
|
|
122
|
+
function whenWsOpen(ws: SandboxWsSocket, fn: () => void): void {
|
|
123
|
+
const std = ws as WebSocket;
|
|
124
|
+
if (typeof std.readyState === "number") {
|
|
125
|
+
if (std.readyState === WebSocket.OPEN) {
|
|
126
|
+
fn();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
std.addEventListener("open", fn, { once: true });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
fn();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function bindSandboxWsSocket(
|
|
136
|
+
ws: SandboxWsSocket,
|
|
137
|
+
handlers: {
|
|
138
|
+
onMessage: (raw: string) => void;
|
|
139
|
+
onClose: () => void;
|
|
140
|
+
onError?: (err: unknown) => void;
|
|
141
|
+
},
|
|
142
|
+
): () => void {
|
|
143
|
+
if (typeof ws.on === "function") {
|
|
144
|
+
const onMessage = (...args: unknown[]) => {
|
|
145
|
+
const data = args[0];
|
|
146
|
+
const raw = typeof data === "string"
|
|
147
|
+
? data
|
|
148
|
+
: data instanceof ArrayBuffer
|
|
149
|
+
? new TextDecoder().decode(data)
|
|
150
|
+
: Buffer.isBuffer(data)
|
|
151
|
+
? data.toString()
|
|
152
|
+
: String(data ?? "");
|
|
153
|
+
handlers.onMessage(raw);
|
|
154
|
+
};
|
|
155
|
+
ws.on("message", onMessage);
|
|
156
|
+
ws.on("close", handlers.onClose);
|
|
157
|
+
if (handlers.onError) ws.on("error", handlers.onError);
|
|
158
|
+
return () => {
|
|
159
|
+
ws.off?.("message", onMessage);
|
|
160
|
+
ws.off?.("close", handlers.onClose);
|
|
161
|
+
if (handlers.onError) ws.off?.("error", handlers.onError);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const onMessage = (ev: Event) => {
|
|
165
|
+
const data = (ev as MessageEvent).data;
|
|
166
|
+
handlers.onMessage(typeof data === "string" ? data : "");
|
|
167
|
+
};
|
|
168
|
+
const onClose = () => handlers.onClose();
|
|
169
|
+
const onError = handlers.onError
|
|
170
|
+
? () => handlers.onError?.(new Error("WebSocket error"))
|
|
171
|
+
: undefined;
|
|
172
|
+
ws.addEventListener!("message", onMessage);
|
|
173
|
+
ws.addEventListener!("close", onClose);
|
|
174
|
+
if (onError) ws.addEventListener!("error", onError);
|
|
175
|
+
return () => {
|
|
176
|
+
ws.removeEventListener!("message", onMessage);
|
|
177
|
+
ws.removeEventListener!("close", onClose);
|
|
178
|
+
if (onError) ws.removeEventListener!("error", onError);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function parseSandboxWsPayload(raw: string): {
|
|
183
|
+
type: MessageType;
|
|
184
|
+
id: string;
|
|
185
|
+
content: MessageElement[];
|
|
186
|
+
timestamp: number;
|
|
187
|
+
} {
|
|
188
|
+
let payload: {
|
|
189
|
+
type?: MessageType;
|
|
190
|
+
id?: string;
|
|
191
|
+
content?: MessageElement[] | string;
|
|
192
|
+
text?: string;
|
|
193
|
+
timestamp?: number;
|
|
194
|
+
};
|
|
195
|
+
try {
|
|
196
|
+
payload = JSON.parse(raw) as typeof payload;
|
|
197
|
+
} catch {
|
|
198
|
+
payload = { text: raw };
|
|
199
|
+
}
|
|
200
|
+
const type = (payload.type as MessageType) ?? "private";
|
|
201
|
+
const id = payload.id ?? "sandbox-user";
|
|
202
|
+
const content: MessageElement[] = typeof payload.content === "string"
|
|
203
|
+
? [{ type: "text", data: { text: payload.content } }]
|
|
204
|
+
: Array.isArray(payload.content)
|
|
205
|
+
? payload.content
|
|
206
|
+
: [{ type: "text", data: { text: payload.text ?? raw } }];
|
|
207
|
+
return { type, id, content, timestamp: payload.timestamp ?? Date.now() };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type BotEvent = {
|
|
211
|
+
content: MessageElement[];
|
|
212
|
+
type: MessageType;
|
|
213
|
+
id: string;
|
|
214
|
+
ts: number;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export class SandboxWsBot extends EventEmitter implements Bot<SandboxWsConfig, BotEvent> {
|
|
218
|
+
$connected = false;
|
|
219
|
+
#unbind: (() => void) | null = null;
|
|
220
|
+
|
|
221
|
+
get $id() {
|
|
222
|
+
return this.$config.name;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
constructor(
|
|
226
|
+
public adapter: SandboxWsHostAdapter,
|
|
227
|
+
public $config: SandboxWsConfig,
|
|
228
|
+
) {
|
|
229
|
+
super();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async $connect(): Promise<void> {
|
|
233
|
+
if (this.$config.offline || !this.$config.ws) return;
|
|
234
|
+
const ws = this.$config.ws;
|
|
235
|
+
if (typeof ws.on !== "function" && typeof ws.addEventListener !== "function") {
|
|
236
|
+
this.$connected = true;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.#unbind = bindSandboxWsSocket(ws, {
|
|
240
|
+
onMessage: (raw) => {
|
|
241
|
+
const { type, id, content, timestamp } = parseSandboxWsPayload(raw);
|
|
242
|
+
this.adapter.emit(
|
|
243
|
+
"message.receive",
|
|
244
|
+
this.$formatMessage({ content, type, id, ts: timestamp }),
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
onClose: () => {
|
|
248
|
+
this.$connected = false;
|
|
249
|
+
if (!this.$config.offline) {
|
|
250
|
+
this.adapter.bots.delete(this.$id);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
onError: (err) => {
|
|
254
|
+
this.adapter.logger.warn(
|
|
255
|
+
`sandbox ws error (${this.$config.name}): ${err instanceof Error ? err.message : String(err)}`,
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
this.$connected = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async $disconnect(): Promise<void> {
|
|
263
|
+
this.#unbind?.();
|
|
264
|
+
this.#unbind = null;
|
|
265
|
+
this.$config.ws?.close();
|
|
266
|
+
this.$connected = false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
$formatMessage({ content, type, id, ts }: BotEvent) {
|
|
270
|
+
if (!this.$config.owner) this.$config.owner = id;
|
|
271
|
+
const message = Message.from<BotEvent>(
|
|
272
|
+
{ content, type, id, ts },
|
|
273
|
+
{
|
|
274
|
+
$id: `${ts}`,
|
|
275
|
+
$adapter: "sandbox" as const,
|
|
276
|
+
$bot: this.$config.name,
|
|
277
|
+
$sender: { id, name: "mock" },
|
|
278
|
+
$channel: { id, type },
|
|
279
|
+
$content: content,
|
|
280
|
+
$raw: segment.raw(content),
|
|
281
|
+
$timestamp: ts,
|
|
282
|
+
$recall: async () => {},
|
|
283
|
+
$reply: async (replyContent: SendContent, quote?: boolean | string) => {
|
|
284
|
+
const normalized = Array.isArray(replyContent) ? replyContent : [replyContent];
|
|
285
|
+
if (quote) {
|
|
286
|
+
normalized.unshift({
|
|
287
|
+
type: "reply",
|
|
288
|
+
data: {
|
|
289
|
+
id: typeof quote === "boolean" ? message.$id : quote,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return await this.adapter.sendMessage({
|
|
294
|
+
context: "sandbox",
|
|
295
|
+
bot: this.$config.name,
|
|
296
|
+
content: normalized,
|
|
297
|
+
id,
|
|
298
|
+
type,
|
|
299
|
+
});
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
return message;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async $sendMessage(options: SendOptions): Promise<string> {
|
|
307
|
+
if (!this.$connected) return "";
|
|
308
|
+
const ws = this.$config.ws;
|
|
309
|
+
if (!ws) return "";
|
|
310
|
+
ws.send(
|
|
311
|
+
JSON.stringify({
|
|
312
|
+
...options,
|
|
313
|
+
content: options.content,
|
|
314
|
+
timestamp: Date.now(),
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async $recallMessage(_id: string): Promise<void> {}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export class SandboxWsHostAdapter extends Adapter<SandboxWsBot> {
|
|
324
|
+
readonly #sseSessions = new Map<string, string>();
|
|
325
|
+
|
|
326
|
+
constructor(
|
|
327
|
+
plugin: Plugin,
|
|
328
|
+
protected readonly defaults: ResolvedSandboxBot,
|
|
329
|
+
) {
|
|
330
|
+
super(plugin, "sandbox" as keyof Plugin.Contexts, []);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
get transport(): SandboxTransport {
|
|
334
|
+
return this.defaults.transport;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
hasSseSession(sessionId: string): boolean {
|
|
338
|
+
return this.#sseSessions.has(sessionId);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
getBotBySseSession(sessionId: string): SandboxWsBot | undefined {
|
|
342
|
+
const name = this.#sseSessions.get(sessionId);
|
|
343
|
+
return name ? this.bots.get(name) : undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
createBot(config: SandboxWsConfig): SandboxWsBot {
|
|
347
|
+
const bot = new SandboxWsBot(this, config);
|
|
348
|
+
this.bots.set(bot.$id, bot);
|
|
349
|
+
return bot;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** `zhin.config.yml` 中 `context: sandbox` + 固定 `name` 时,启动即出现在 bot:list(离线) */
|
|
353
|
+
registerConfiguredPlaceholder(): void {
|
|
354
|
+
if (this.defaults.randomNamePerConnection) return;
|
|
355
|
+
if (this.bots.has(this.defaults.name)) return;
|
|
356
|
+
this.createBot({
|
|
357
|
+
context: "sandbox",
|
|
358
|
+
name: this.defaults.name,
|
|
359
|
+
owner: this.defaults.owner,
|
|
360
|
+
ws: createOfflineSandboxWs(),
|
|
361
|
+
offline: true,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Edge / 外部 upgrade:注入已建立的 WebSocket */
|
|
366
|
+
acceptWebSocket(
|
|
367
|
+
ws: SandboxWsSocket,
|
|
368
|
+
overrides?: Partial<Pick<SandboxWsConfig, "name" | "owner">>,
|
|
369
|
+
): SandboxWsBot {
|
|
370
|
+
const name = overrides?.name ??
|
|
371
|
+
(this.defaults.randomNamePerConnection
|
|
372
|
+
? `sandbox-${crypto.randomUUID().slice(0, 8)}`
|
|
373
|
+
: this.defaults.name);
|
|
374
|
+
const owner = overrides?.owner ?? this.defaults.owner;
|
|
375
|
+
const existing = this.bots.get(name);
|
|
376
|
+
if (existing) {
|
|
377
|
+
void existing.$disconnect();
|
|
378
|
+
this.bots.delete(name);
|
|
379
|
+
}
|
|
380
|
+
const bot = this.createBot({ context: "sandbox", ws, name, owner, offline: false });
|
|
381
|
+
void bot.$connect();
|
|
382
|
+
if (!this.defaults.randomNamePerConnection) {
|
|
383
|
+
const readyPayload = JSON.stringify({
|
|
384
|
+
type: "ready",
|
|
385
|
+
id: owner,
|
|
386
|
+
bot: name,
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
389
|
+
type: "text",
|
|
390
|
+
data: {
|
|
391
|
+
text: [
|
|
392
|
+
`已连接 Sandbox「${name}」`,
|
|
393
|
+
"与 Node Host 控制台沙盒协议一致(/sandbox)",
|
|
394
|
+
"命令: help · ping · zt · status",
|
|
395
|
+
].join("\n"),
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
timestamp: Date.now(),
|
|
400
|
+
});
|
|
401
|
+
whenWsOpen(ws, () => ws.send(readyPayload));
|
|
402
|
+
}
|
|
403
|
+
return bot;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Edge SSE:建立会话并绑定固定 bot */
|
|
407
|
+
acceptSseSession(
|
|
408
|
+
sessionId: string,
|
|
409
|
+
overrides?: Partial<Pick<SandboxWsConfig, "name" | "owner">>,
|
|
410
|
+
): SandboxWsBot {
|
|
411
|
+
const name = overrides?.name ?? this.defaults.name;
|
|
412
|
+
const owner = overrides?.owner ?? this.defaults.owner;
|
|
413
|
+
const existing = this.bots.get(name);
|
|
414
|
+
if (existing) {
|
|
415
|
+
void existing.$disconnect();
|
|
416
|
+
this.bots.delete(name);
|
|
417
|
+
}
|
|
418
|
+
const ws = createSandboxSseTransport(sessionId, () => {
|
|
419
|
+
this.#sseSessions.delete(sessionId);
|
|
420
|
+
this.bots.delete(name);
|
|
421
|
+
});
|
|
422
|
+
const bot = this.createBot({ context: "sandbox", ws, name, owner, offline: false });
|
|
423
|
+
this.#sseSessions.set(sessionId, name);
|
|
424
|
+
void bot.$connect();
|
|
425
|
+
if (!this.defaults.randomNamePerConnection) {
|
|
426
|
+
const readyPayload = JSON.stringify({
|
|
427
|
+
type: "ready",
|
|
428
|
+
id: owner,
|
|
429
|
+
bot: name,
|
|
430
|
+
content: [
|
|
431
|
+
{
|
|
432
|
+
type: "text",
|
|
433
|
+
data: {
|
|
434
|
+
text: [
|
|
435
|
+
`已连接 Sandbox「${name}」`,
|
|
436
|
+
"传输: HTTP + SSE(Edge)",
|
|
437
|
+
"命令: help · ping · zt · status",
|
|
438
|
+
].join("\n"),
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
timestamp: Date.now(),
|
|
443
|
+
});
|
|
444
|
+
ws.send(readyPayload);
|
|
445
|
+
}
|
|
446
|
+
return bot;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** POST /sandbox/message — 与 WebSocket 帧同格式的 JSON 字符串 */
|
|
450
|
+
ingestSseClientMessage(sessionId: string, raw: string): void {
|
|
451
|
+
const bot = this.getBotBySseSession(sessionId);
|
|
452
|
+
if (!bot || !bot.$connected) {
|
|
453
|
+
throw new Error(`sandbox session not connected: ${sessionId}`);
|
|
454
|
+
}
|
|
455
|
+
const { type, id, content, timestamp } = parseSandboxWsPayload(raw);
|
|
456
|
+
this.emit(
|
|
457
|
+
"message.receive",
|
|
458
|
+
bot.$formatMessage({ content, type, id, ts: timestamp }),
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|