@zhin.js/adapter-sandbox 5.0.4 → 5.0.6

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