@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/CHANGELOG.md +45 -0
- package/README.md +22 -1
- package/adapters/sandbox.ts +26 -0
- package/lib/endpoint.d.ts +25 -0
- package/lib/endpoint.js +155 -0
- package/lib/index.d.ts +2 -22
- package/lib/index.js +2 -67
- package/lib/protocol.d.ts +52 -0
- package/lib/protocol.js +135 -0
- package/package.json +49 -28
- package/pages/sandbox.tsx +95 -0
- package/plugin.ts +8 -0
- package/schema.json +20 -0
- package/src/endpoint.ts +188 -0
- package/src/index.ts +12 -101
- package/src/protocol.ts +200 -0
- package/client/RichTextEditor.tsx +0 -429
- package/client/Sandbox.tsx +0 -588
- package/client/index.tsx +0 -11
- package/client/sandboxTransport.ts +0 -28
- package/client/tsconfig.json +0 -7
- package/dist/index.js +0 -45
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/sandbox-ws.d.ts +0 -86
- package/lib/sandbox-ws.d.ts.map +0 -1
- package/lib/sandbox-ws.js +0 -295
- package/lib/sandbox-ws.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/src/sandbox-ws.ts +0 -404
- package/src/segment-mapper.ts +0 -1
- /package/{skills/sandbox/SKILL.md → agent/skills/sandbox.md} +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-sandbox",
|
|
3
|
-
"version": "5.0.
|
|
4
|
-
"description": "Zhin.js adapter for
|
|
3
|
+
"version": "5.0.6",
|
|
4
|
+
"description": "Zhin.js Sandbox adapter for Plugin Runtime (WebSocket /sandbox)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
+
"adapters",
|
|
17
|
+
"pages",
|
|
18
|
+
"plugin.ts",
|
|
19
|
+
"schema.json",
|
|
16
20
|
"src",
|
|
17
21
|
"lib",
|
|
18
|
-
"client",
|
|
19
22
|
"dist",
|
|
20
|
-
"
|
|
23
|
+
"agent",
|
|
21
24
|
"README.md",
|
|
22
25
|
"CHANGELOG.md"
|
|
23
26
|
],
|
|
@@ -43,35 +46,34 @@
|
|
|
43
46
|
"directory": "plugins/adapters/sandbox"
|
|
44
47
|
},
|
|
45
48
|
"dependencies": {
|
|
46
|
-
"
|
|
49
|
+
"react": "^19.1.0",
|
|
50
|
+
"@zhin.js/console-contract": "1.0.0",
|
|
51
|
+
"@zhin.js/adapter": "1.0.1",
|
|
52
|
+
"@zhin.js/core": "1.3.5",
|
|
53
|
+
"@zhin.js/host-http": "1.0.1",
|
|
54
|
+
"@zhin.js/logger": "1.0.75",
|
|
55
|
+
"@zhin.js/page": "1.0.1",
|
|
56
|
+
"@zhin.js/plugin-runtime": "1.0.1"
|
|
47
57
|
},
|
|
48
58
|
"devDependencies": {
|
|
49
|
-
"@types/
|
|
50
|
-
"@types/react-dom": "^19.2.3",
|
|
59
|
+
"@types/ws": "^8.18.1",
|
|
51
60
|
"typescript": "^6.0.3",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"@zhin.js/
|
|
55
|
-
"@zhin.js/
|
|
56
|
-
"@zhin.js/host-api": "2.0.4",
|
|
57
|
-
"@zhin.js/host-router": "2.0.2",
|
|
58
|
-
"zhin.js": "4.1.1"
|
|
61
|
+
"vitest": "^4.1.10",
|
|
62
|
+
"ws": "^8.21.0",
|
|
63
|
+
"@zhin.js/pagemanager": "2.0.4",
|
|
64
|
+
"@zhin.js/runtime": "1.0.1"
|
|
59
65
|
},
|
|
60
66
|
"peerDependencies": {
|
|
61
|
-
"
|
|
62
|
-
"@zhin.js/
|
|
63
|
-
"@zhin.js/
|
|
64
|
-
"@zhin.js/
|
|
65
|
-
"@zhin.js/
|
|
67
|
+
"react": "^19.0.0",
|
|
68
|
+
"@zhin.js/adapter": "1.0.1",
|
|
69
|
+
"@zhin.js/console-contract": "1.0.0",
|
|
70
|
+
"@zhin.js/core": "1.3.5",
|
|
71
|
+
"@zhin.js/host-http": "1.0.1",
|
|
72
|
+
"@zhin.js/page": "1.0.1",
|
|
73
|
+
"@zhin.js/plugin-runtime": "1.0.1"
|
|
66
74
|
},
|
|
67
75
|
"peerDependenciesMeta": {
|
|
68
|
-
"
|
|
69
|
-
"optional": true
|
|
70
|
-
},
|
|
71
|
-
"@zhin.js/host-api": {
|
|
72
|
-
"optional": true
|
|
73
|
-
},
|
|
74
|
-
"@zhin.js/client": {
|
|
76
|
+
"react": {
|
|
75
77
|
"optional": true
|
|
76
78
|
}
|
|
77
79
|
},
|
|
@@ -82,8 +84,27 @@
|
|
|
82
84
|
"engines": {
|
|
83
85
|
"node": "^20.19.0 || >=22.12.0"
|
|
84
86
|
},
|
|
87
|
+
"zhin": {
|
|
88
|
+
"protocol": 1,
|
|
89
|
+
"type": "plugin",
|
|
90
|
+
"entry": "./plugin.ts",
|
|
91
|
+
"engine": "^1.0.0",
|
|
92
|
+
"runtime": "trusted",
|
|
93
|
+
"features": [
|
|
94
|
+
{
|
|
95
|
+
"package": "@zhin.js/adapter",
|
|
96
|
+
"api": "^1.0.0"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"package": "@zhin.js/page",
|
|
100
|
+
"api": "^1.0.0"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"plugins": []
|
|
104
|
+
},
|
|
85
105
|
"scripts": {
|
|
86
|
-
"build": "
|
|
87
|
-
"clean": "rimraf lib"
|
|
106
|
+
"build": "tsc",
|
|
107
|
+
"clean": "rimraf lib",
|
|
108
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/sandbox/tests"
|
|
88
109
|
}
|
|
89
110
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { definePage } from '@zhin.js/console-contract';
|
|
2
|
+
import { useEffect, useRef, useState, type FormEvent } from 'react';
|
|
3
|
+
|
|
4
|
+
export const meta = definePage({
|
|
5
|
+
title: 'Sandbox',
|
|
6
|
+
icon: 'Box',
|
|
7
|
+
order: 10,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convention page (ADR 0046). Plugin Runtime Host also serves a vanilla WS shell
|
|
12
|
+
* for this localName so local smoke does not depend on a CDN React graph.
|
|
13
|
+
*/
|
|
14
|
+
export default function SandboxPage() {
|
|
15
|
+
const [connected, setConnected] = useState(false);
|
|
16
|
+
const [input, setInput] = useState('');
|
|
17
|
+
const [lines, setLines] = useState<readonly { readonly kind: 'in' | 'out'; readonly text: string }[]>([]);
|
|
18
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const url = new URL('/sandbox', window.location.href);
|
|
22
|
+
url.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
23
|
+
const ws = new WebSocket(url);
|
|
24
|
+
wsRef.current = ws;
|
|
25
|
+
ws.addEventListener('open', () => setConnected(true));
|
|
26
|
+
ws.addEventListener('close', () => setConnected(false));
|
|
27
|
+
ws.addEventListener('message', (event) => {
|
|
28
|
+
let text = String(event.data);
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(text) as {
|
|
31
|
+
content?: Array<{ data?: { text?: string } }> | string;
|
|
32
|
+
};
|
|
33
|
+
text = Array.isArray(data.content)
|
|
34
|
+
? data.content.map((segment) => segment?.data?.text ?? '').filter(Boolean).join('\n')
|
|
35
|
+
: typeof data.content === 'string'
|
|
36
|
+
? data.content
|
|
37
|
+
: text;
|
|
38
|
+
} catch {
|
|
39
|
+
/* keep raw */
|
|
40
|
+
}
|
|
41
|
+
setLines((previous) => [...previous, { kind: 'in', text }]);
|
|
42
|
+
});
|
|
43
|
+
return () => {
|
|
44
|
+
ws.close();
|
|
45
|
+
wsRef.current = null;
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const onSubmit = (event: FormEvent) => {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
const text = input.trim();
|
|
52
|
+
const ws = wsRef.current;
|
|
53
|
+
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
54
|
+
ws.send(JSON.stringify({ text, timestamp: Date.now() }));
|
|
55
|
+
setLines((previous) => [...previous, { kind: 'out', text }]);
|
|
56
|
+
setInput('');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 3rem)', maxWidth: 720, margin: '0 auto' }}>
|
|
61
|
+
<p style={{ padding: '0.75rem 1rem', margin: 0, color: '#71717a' }}>
|
|
62
|
+
{connected ? 'WebSocket /sandbox connected' : 'connecting…'}
|
|
63
|
+
</p>
|
|
64
|
+
<div style={{ flex: 1, overflow: 'auto', padding: '1rem', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
65
|
+
{lines.map((line, index) => (
|
|
66
|
+
<div
|
|
67
|
+
key={`${line.kind}-${index}-${line.text.slice(0, 12)}`}
|
|
68
|
+
style={{
|
|
69
|
+
alignSelf: line.kind === 'out' ? 'flex-end' : 'flex-start',
|
|
70
|
+
background: line.kind === 'out' ? '#ccfbf1' : '#fff',
|
|
71
|
+
border: '1px solid #e4e4e7',
|
|
72
|
+
borderRadius: 10,
|
|
73
|
+
padding: '0.6rem 0.8rem',
|
|
74
|
+
maxWidth: '85%',
|
|
75
|
+
whiteSpace: 'pre-wrap',
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{line.text}
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
<form onSubmit={onSubmit} style={{ display: 'flex', gap: 8, padding: 12, borderTop: '1px solid #e4e4e7' }}>
|
|
83
|
+
<input
|
|
84
|
+
value={input}
|
|
85
|
+
onChange={(event) => setInput(event.target.value)}
|
|
86
|
+
placeholder="发送到 /sandbox …"
|
|
87
|
+
style={{ flex: 1, padding: '0.55rem 0.75rem', borderRadius: 8, border: '1px solid #d4d4d8' }}
|
|
88
|
+
/>
|
|
89
|
+
<button type="submit" style={{ padding: '0.55rem 0.9rem', border: 0, borderRadius: 8, background: '#0f766e', color: '#fff' }}>
|
|
90
|
+
发送
|
|
91
|
+
</button>
|
|
92
|
+
</form>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
package/plugin.ts
ADDED
package/schema.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"endpoints": {
|
|
7
|
+
"type": "array",
|
|
8
|
+
"default": [],
|
|
9
|
+
"items": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": false,
|
|
12
|
+
"properties": {
|
|
13
|
+
"context": { "type": "string" },
|
|
14
|
+
"name": { "type": "string" },
|
|
15
|
+
"owner": { "type": "string" }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/endpoint.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SandboxWsEndpoint — WebSocket lifecycle and MessageGateway bridge for /sandbox.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
6
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
|
+
import type { HttpHost, WsConnection } from '@zhin.js/host-http';
|
|
8
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
|
+
import {
|
|
11
|
+
bindSandboxWsSocket,
|
|
12
|
+
formatSandboxOutbound,
|
|
13
|
+
parseSandboxWsPayload,
|
|
14
|
+
whenWsOpen,
|
|
15
|
+
type ResolvedSandboxBot,
|
|
16
|
+
type SandboxWsSocket,
|
|
17
|
+
} from './protocol.js';
|
|
18
|
+
|
|
19
|
+
const logger = getLogger('sandbox');
|
|
20
|
+
|
|
21
|
+
interface SandboxConnection {
|
|
22
|
+
readonly target: string;
|
|
23
|
+
readonly owner: string;
|
|
24
|
+
readonly socket: SandboxWsSocket;
|
|
25
|
+
readonly release: () => void;
|
|
26
|
+
/** true = 占位连接(尚无真实 WS 客户端),send 命中时按 miss 处理。 */
|
|
27
|
+
readonly placeholder?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SandboxEndpointOptions {
|
|
31
|
+
readonly id: CapabilityId;
|
|
32
|
+
readonly gateway: MessageGateway;
|
|
33
|
+
readonly http: HttpHost;
|
|
34
|
+
readonly defaults: ResolvedSandboxBot;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class SandboxWsEndpoint implements EndpointInstance {
|
|
38
|
+
readonly #options: SandboxEndpointOptions;
|
|
39
|
+
readonly #connections = new Map<string, SandboxConnection>();
|
|
40
|
+
#wsHandleRelease?: () => void;
|
|
41
|
+
#open = false;
|
|
42
|
+
#started = false;
|
|
43
|
+
|
|
44
|
+
constructor(options: SandboxEndpointOptions) {
|
|
45
|
+
this.#options = options;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Live endpoint name (config `name`) — Console endpoint.list/resolve uses it. */
|
|
49
|
+
get name(): string {
|
|
50
|
+
return this.#options.defaults.name;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
start(): void {
|
|
54
|
+
if (this.#started) return;
|
|
55
|
+
this.#started = true;
|
|
56
|
+
const handle = this.#options.http.ws('/sandbox');
|
|
57
|
+
this.#wsHandleRelease = handle.onConnection((connection) => {
|
|
58
|
+
this.#acceptConnection(connection);
|
|
59
|
+
});
|
|
60
|
+
if (!this.#options.defaults.randomNamePerConnection) {
|
|
61
|
+
this.#ensurePlaceholder(this.#options.defaults.name, this.#options.defaults.owner);
|
|
62
|
+
}
|
|
63
|
+
logger.info(formatCompact({
|
|
64
|
+
op: 'sandbox_ws_mounted',
|
|
65
|
+
path: '/sandbox',
|
|
66
|
+
endpoint: this.#options.defaults.name,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
open(): void {
|
|
71
|
+
this.#open = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
close(): void {
|
|
75
|
+
this.#open = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stop(): void {
|
|
79
|
+
this.#open = false;
|
|
80
|
+
this.#wsHandleRelease?.();
|
|
81
|
+
this.#wsHandleRelease = undefined;
|
|
82
|
+
for (const connection of this.#connections.values()) connection.release();
|
|
83
|
+
this.#connections.clear();
|
|
84
|
+
this.#started = false;
|
|
85
|
+
logger.debug(formatCompact({ op: 'sandbox_stopped' }));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
send({ target, payload }: { readonly target: string; readonly payload: unknown }): unknown {
|
|
89
|
+
if (!this.#open) return undefined;
|
|
90
|
+
const connection = this.#connections.get(target);
|
|
91
|
+
if (!connection) {
|
|
92
|
+
logger.debug(formatCompact({ op: 'sandbox_send_miss', target }));
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
if (connection.placeholder) {
|
|
96
|
+
logger.debug(formatCompact({ op: 'sandbox_send_placeholder', target }));
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
connection.socket.send(formatSandboxOutbound(payload));
|
|
100
|
+
logger.debug(formatCompact({ op: 'sandbox_send', target }));
|
|
101
|
+
return payload;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#acceptConnection(connection: WsConnection): void {
|
|
105
|
+
const target = this.#options.defaults.randomNamePerConnection
|
|
106
|
+
? `sandbox-${randomUUID().slice(0, 8)}`
|
|
107
|
+
: this.#options.defaults.name;
|
|
108
|
+
const owner = this.#options.defaults.owner;
|
|
109
|
+
const socket = connection.socket as SandboxWsSocket;
|
|
110
|
+
if (this.#connections.has(target)) {
|
|
111
|
+
this.#connections.get(target)?.release();
|
|
112
|
+
this.#connections.delete(target);
|
|
113
|
+
}
|
|
114
|
+
const release = bindSandboxWsSocket(socket, {
|
|
115
|
+
onMessage: (raw) => {
|
|
116
|
+
if (!this.#open) return;
|
|
117
|
+
const parsed = parseSandboxWsPayload(raw);
|
|
118
|
+
logger.debug(formatCompact({
|
|
119
|
+
op: 'sandbox_recv',
|
|
120
|
+
target,
|
|
121
|
+
sender: parsed.id || owner,
|
|
122
|
+
text: parsed.text.slice(0, 80),
|
|
123
|
+
}));
|
|
124
|
+
void this.#options.gateway.receive({
|
|
125
|
+
adapter: this.#options.id,
|
|
126
|
+
target,
|
|
127
|
+
content: parsed.text,
|
|
128
|
+
sender: parsed.id || owner,
|
|
129
|
+
metadata: Object.freeze({
|
|
130
|
+
type: parsed.type,
|
|
131
|
+
elements: parsed.content,
|
|
132
|
+
timestamp: parsed.timestamp,
|
|
133
|
+
...(parsed.action ? { action: parsed.action } : {}),
|
|
134
|
+
}),
|
|
135
|
+
}).catch((err) => {
|
|
136
|
+
logger.warn(formatCompact({
|
|
137
|
+
op: 'sandbox_gateway_receive_failed',
|
|
138
|
+
target,
|
|
139
|
+
error: err instanceof Error ? err.message : String(err),
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
onClose: () => {
|
|
144
|
+
this.#connections.delete(target);
|
|
145
|
+
logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
|
|
146
|
+
},
|
|
147
|
+
onError: (err) => {
|
|
148
|
+
logger.warn(formatCompact({
|
|
149
|
+
op: 'sandbox_ws_error',
|
|
150
|
+
target,
|
|
151
|
+
error: err instanceof Error ? err.message : String(err),
|
|
152
|
+
}));
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
this.#connections.set(target, Object.freeze({ target, owner, socket, release }));
|
|
156
|
+
logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
157
|
+
if (!this.#options.defaults.randomNamePerConnection) {
|
|
158
|
+
const readyPayload = JSON.stringify({
|
|
159
|
+
type: 'ready',
|
|
160
|
+
id: owner,
|
|
161
|
+
endpoint: target,
|
|
162
|
+
content: [{
|
|
163
|
+
type: 'text',
|
|
164
|
+
data: {
|
|
165
|
+
text: [
|
|
166
|
+
`已连接 Sandbox「${target}」`,
|
|
167
|
+
'与 Node Host 控制台沙盒协议一致(/sandbox)',
|
|
168
|
+
'命令: help · ping · zt · status',
|
|
169
|
+
].join('\n'),
|
|
170
|
+
},
|
|
171
|
+
}],
|
|
172
|
+
timestamp: Date.now(),
|
|
173
|
+
});
|
|
174
|
+
whenWsOpen(socket, () => socket.send(readyPayload));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#ensurePlaceholder(name: string, owner: string): void {
|
|
179
|
+
if (this.#connections.has(name)) return;
|
|
180
|
+
this.#connections.set(name, Object.freeze({
|
|
181
|
+
target: name,
|
|
182
|
+
owner,
|
|
183
|
+
socket: { send: () => undefined, close: () => undefined },
|
|
184
|
+
release: () => undefined,
|
|
185
|
+
placeholder: true,
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,106 +1,17 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { usePlugin, type Plugin } from "zhin.js";
|
|
3
|
-
import type { WebSocket } from "ws";
|
|
4
|
-
import {
|
|
5
|
-
SandboxWsHostAdapter,
|
|
6
|
-
resolveSandboxEndpoint,
|
|
7
|
-
type SandboxWsSocket,
|
|
8
|
-
} from "./sandbox-ws.js";
|
|
9
|
-
import { PageManager } from "@zhin.js/host-api";
|
|
10
|
-
|
|
11
|
-
type SandboxRouter = {
|
|
12
|
-
ws: (path: string) => NonNullable<SandboxAdapter["wss"]>;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
declare module "zhin.js" {
|
|
16
|
-
interface Adapters {
|
|
17
|
-
sandbox: SandboxAdapter;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const plugin = usePlugin();
|
|
22
|
-
const logger = plugin.logger;
|
|
23
|
-
|
|
24
|
-
/** Node:`Router.ws` */
|
|
25
|
-
export class SandboxAdapter extends SandboxWsHostAdapter {
|
|
26
|
-
wss?: { on: (ev: string, fn: (...args: unknown[]) => void) => void; close: () => void };
|
|
27
|
-
|
|
28
|
-
constructor(plugin: ReturnType<typeof usePlugin>) {
|
|
29
|
-
const appConfig = (plugin.inject("config")?.getPrimary() ?? {}) as Record<string, unknown>;
|
|
30
|
-
super(plugin, resolveSandboxEndpoint(appConfig));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
override async start(): Promise<void> {
|
|
34
|
-
await super.start();
|
|
35
|
-
this.registerConfiguredPlaceholder();
|
|
36
|
-
logger.debug(
|
|
37
|
-
`Sandbox placeholder: ${this.defaults.name} (offline until /sandbox WS)`,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async setupWebSocket(router: SandboxRouter): Promise<void> {
|
|
42
|
-
if (this.wss) return;
|
|
43
|
-
this.wss = router.ws("/sandbox");
|
|
44
|
-
|
|
45
|
-
this.wss.on("connection", (...args: unknown[]) => {
|
|
46
|
-
const ws = args[0] as WebSocket;
|
|
47
|
-
const req = args[1] as { socket?: { remoteAddress?: string } };
|
|
48
|
-
logger.debug(
|
|
49
|
-
`New sandbox connection from ${req.socket?.remoteAddress ?? "unknown"}`,
|
|
50
|
-
);
|
|
51
|
-
const endpoint = this.acceptWebSocket(ws as SandboxWsSocket);
|
|
52
|
-
ws.on("close", () => {
|
|
53
|
-
logger.debug(`Sandbox connection closed: ${endpoint.$config.name}`);
|
|
54
|
-
this.endpoints.delete(endpoint.$id);
|
|
55
|
-
});
|
|
56
|
-
ws.on("error", (error) => {
|
|
57
|
-
logger.error(`Sandbox WebSocket error for ${endpoint.$config.name}:`, error);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
logger.debug("Sandbox WebSocket server started at /sandbox (Node Router.ws)");
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const { provide } = usePlugin();
|
|
66
|
-
|
|
67
|
-
provide({
|
|
68
|
-
name: "sandbox",
|
|
69
|
-
description: "Sandbox Adapter — Node Router.ws",
|
|
70
|
-
mounted: async (p: Plugin) => {
|
|
71
|
-
const adapter = new SandboxAdapter(p);
|
|
72
|
-
await adapter.start();
|
|
73
|
-
return adapter;
|
|
74
|
-
},
|
|
75
|
-
dispose: async (adapter: SandboxAdapter) => {
|
|
76
|
-
for (const endpoint of adapter.endpoints.values()) {
|
|
77
|
-
await endpoint.$disconnect();
|
|
78
|
-
}
|
|
79
|
-
adapter.wss?.close();
|
|
80
|
-
await adapter.stop();
|
|
81
|
-
},
|
|
82
|
-
} as never);
|
|
83
|
-
|
|
84
|
-
plugin.useContext("router",'sandbox', async (router: SandboxRouter,adapter: SandboxAdapter) => {
|
|
85
|
-
await adapter.setupWebSocket(router);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
plugin.useContext("web", (pageManager) => {
|
|
89
|
-
pageManager.addEntry({
|
|
90
|
-
id: "sandbox",
|
|
91
|
-
development: path.resolve(import.meta.dirname, "../client/index.tsx"),
|
|
92
|
-
production: path.resolve(import.meta.dirname, "../dist/index.js"),
|
|
93
|
-
meta: { name: "Sandbox" },
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
1
|
export {
|
|
98
|
-
SandboxWsEndpoint,
|
|
99
|
-
SandboxWsHostAdapter,
|
|
100
|
-
resolveSandboxEndpoint,
|
|
101
2
|
bindSandboxWsSocket,
|
|
3
|
+
formatSandboxOutbound,
|
|
102
4
|
parseSandboxWsPayload,
|
|
5
|
+
resolveSandboxEndpoint,
|
|
6
|
+
whenWsOpen,
|
|
7
|
+
type MessageElement,
|
|
8
|
+
type MessageType,
|
|
103
9
|
type ResolvedSandboxBot,
|
|
104
|
-
type
|
|
10
|
+
type SandboxAdapterConfig,
|
|
105
11
|
type SandboxWsSocket,
|
|
106
|
-
} from
|
|
12
|
+
} from './protocol.js';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
SandboxWsEndpoint,
|
|
16
|
+
type SandboxEndpointOptions,
|
|
17
|
+
} from './endpoint.js';
|