@zhin.js/adapter-sandbox 5.0.6 → 6.0.0
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 +22 -0
- package/README.md +10 -10
- package/lib/endpoint.js +76 -13
- package/lib/protocol.d.ts +10 -4
- package/lib/protocol.js +34 -5
- package/package.json +20 -13
- package/pages/RichTextEditor.tsx +429 -0
- package/pages/SandboxChat.tsx +672 -0
- package/pages/sandbox.tsx +6 -84
- package/pages/sandboxTransport.ts +45 -0
- package/schema.json +27 -7
- package/src/endpoint.ts +85 -12
- package/src/protocol.ts +45 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @zhin.js/adapter-process
|
|
2
2
|
|
|
3
|
+
## 6.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7db69c1: 命令前缀改为适配器配置项:`MessageDispatcher` 不再硬编码 `/`,默认按消息所属适配器实例 config 的 `commandPrefix` 解析(默认 `''` 无前缀,任意文本按命令匹配),`endpoints[i].commandPrefix` 逐项覆盖;`ImRuntime({ commandPrefix })` 仍可设全局静态前缀。全部 20 个平台适配器 schema 新增 `commandPrefix` 属性。
|
|
8
|
+
|
|
9
|
+
BREAKING(行为变化):未配置时命令不再需要 `/` 前缀——原 `/zt` 写法不再命中,直接发 `zt` 即可;需要斜杠风格的适配器请在配置里显式设 `commandPrefix: '/'`。
|
|
10
|
+
|
|
11
|
+
- 713445c: 适配器配置格式定稿(不兼容旧格式):`plugins.<adapter>` 顶层仅共享字段 + `commandPrefix`,`endpoints[i]` 携带 endpoint 级字段(`name` + 凭据,各 schema 已类型化),`endpoints` 为必填(icqq 另需顶层 `master`);icqq 新增 `trusted` 列表(顶层/逐项均可)。scaffold-wizard 全部字段式与自定义 configure() 产出改为新格式,examples(full-bot / qq-games-bot)与 20 个适配器 README 同步迁移。
|
|
12
|
+
- Updated dependencies [7db69c1]
|
|
13
|
+
- Updated dependencies [e5c84ed]
|
|
14
|
+
- Updated dependencies [3ea84a0]
|
|
15
|
+
- Updated dependencies [5849336]
|
|
16
|
+
- Updated dependencies [1ddcd70]
|
|
17
|
+
- Updated dependencies [ac9da66]
|
|
18
|
+
- @zhin.js/core@1.4.0
|
|
19
|
+
- @zhin.js/adapter@1.1.0
|
|
20
|
+
- @zhin.js/plugin-runtime@1.1.0
|
|
21
|
+
- @zhin.js/client@2.1.0
|
|
22
|
+
- @zhin.js/host-http@1.0.2
|
|
23
|
+
- @zhin.js/page@1.0.2
|
|
24
|
+
|
|
3
25
|
## 5.0.6
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -42,24 +42,24 @@ Root 在 `zhin runtime start` 时装载 `@zhin.js/host-http`、`ConsoleRuntime`
|
|
|
42
42
|
|
|
43
43
|
## 配置
|
|
44
44
|
|
|
45
|
-
**推荐(与 [minimal-bot](../../../examples/minimal-bot/) 一致)**:`endpoints: []`,在 Remote Console 打开「沙盒」页时经 `/sandbox` WebSocket **自动创建** bot(如 `sandbox-xxxx`),无需在 yaml 里写 `context: sandbox`。
|
|
45
|
+
**推荐(与 [minimal-bot](../../../examples/minimal-bot/) 一致)**:`plugins.sandbox.endpoints: []`,在 Remote Console 打开「沙盒」页时经 `/sandbox` WebSocket **自动创建** bot(如 `sandbox-xxxx`),无需在 yaml 里写 `context: sandbox`。
|
|
46
46
|
|
|
47
47
|
```yaml
|
|
48
|
-
# zhin.config.yml
|
|
49
|
-
endpoints: []
|
|
50
|
-
|
|
48
|
+
# zhin.config.yml(Plugin Runtime)
|
|
51
49
|
plugins:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- "@zhin.js/host-api"
|
|
50
|
+
sandbox:
|
|
51
|
+
endpoints: []
|
|
55
52
|
```
|
|
56
53
|
|
|
57
54
|
可选:若需在启动时即在 bot 列表显示**固定名称**的离线占位 bot,可显式配置:
|
|
58
55
|
|
|
59
56
|
```yaml
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
plugins:
|
|
58
|
+
sandbox:
|
|
59
|
+
endpoints:
|
|
60
|
+
- name: sandbox-bot
|
|
61
|
+
context: sandbox
|
|
62
|
+
owner: sandbox-user
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
## 使用方式
|
package/lib/endpoint.js
CHANGED
|
@@ -45,8 +45,17 @@ export class SandboxWsEndpoint {
|
|
|
45
45
|
this.#open = false;
|
|
46
46
|
this.#wsHandleRelease?.();
|
|
47
47
|
this.#wsHandleRelease = undefined;
|
|
48
|
-
for (const connection of this.#connections.values())
|
|
48
|
+
for (const connection of this.#connections.values()) {
|
|
49
49
|
connection.release();
|
|
50
|
+
if (!connection.placeholder) {
|
|
51
|
+
try {
|
|
52
|
+
connection.socket.close(1001, 'sandbox endpoint stopped');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* already closed */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
50
59
|
this.#connections.clear();
|
|
51
60
|
this.#started = false;
|
|
52
61
|
logger.debug(formatCompact({ op: 'sandbox_stopped' }));
|
|
@@ -54,7 +63,9 @@ export class SandboxWsEndpoint {
|
|
|
54
63
|
send({ target, payload }) {
|
|
55
64
|
if (!this.#open)
|
|
56
65
|
return undefined;
|
|
57
|
-
|
|
66
|
+
// Reply target is the connection key (bot name / sandbox-uuid), not private:channelId.
|
|
67
|
+
const connection = this.#connections.get(target)
|
|
68
|
+
?? this.#findLiveConnection();
|
|
58
69
|
if (!connection) {
|
|
59
70
|
logger.debug(formatCompact({ op: 'sandbox_send_miss', target }));
|
|
60
71
|
return undefined;
|
|
@@ -63,31 +74,75 @@ export class SandboxWsEndpoint {
|
|
|
63
74
|
logger.debug(formatCompact({ op: 'sandbox_send_placeholder', target }));
|
|
64
75
|
return undefined;
|
|
65
76
|
}
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
// Console UI filters by type+id; stamp last inbound channel onto outbound wire.
|
|
78
|
+
const channel = connection.lastChannel ?? {
|
|
79
|
+
type: 'private',
|
|
80
|
+
id: connection.owner,
|
|
81
|
+
};
|
|
82
|
+
connection.socket.send(formatSandboxOutbound(payload, {
|
|
83
|
+
type: channel.type,
|
|
84
|
+
id: channel.id,
|
|
85
|
+
bot: this.#options.defaults.name,
|
|
86
|
+
endpoint: connection.target,
|
|
87
|
+
}));
|
|
88
|
+
logger.debug(formatCompact({
|
|
89
|
+
op: 'sandbox_send',
|
|
90
|
+
target: connection.target,
|
|
91
|
+
channelType: channel.type,
|
|
92
|
+
channelId: channel.id,
|
|
93
|
+
}));
|
|
68
94
|
return payload;
|
|
69
95
|
}
|
|
96
|
+
/** Prefer a real (non-placeholder) socket when reply target key is wrong/stale. */
|
|
97
|
+
#findLiveConnection() {
|
|
98
|
+
for (const connection of this.#connections.values()) {
|
|
99
|
+
if (!connection.placeholder)
|
|
100
|
+
return connection;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
70
104
|
#acceptConnection(connection) {
|
|
71
105
|
const target = this.#options.defaults.randomNamePerConnection
|
|
72
106
|
? `sandbox-${randomUUID().slice(0, 8)}`
|
|
73
107
|
: this.#options.defaults.name;
|
|
74
108
|
const owner = this.#options.defaults.owner;
|
|
75
109
|
const socket = connection.socket;
|
|
76
|
-
|
|
77
|
-
|
|
110
|
+
// Fixed-name mode reuses `target`; dropping the prior entry without
|
|
111
|
+
// closing its socket leaves a zombie browser tab that still looks
|
|
112
|
+
// connected but never receives outbound traffic.
|
|
113
|
+
const previous = this.#connections.get(target);
|
|
114
|
+
if (previous) {
|
|
115
|
+
previous.release();
|
|
116
|
+
if (!previous.placeholder) {
|
|
117
|
+
try {
|
|
118
|
+
previous.socket.close(4000, 'replaced by new sandbox client');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* already closed */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
78
124
|
this.#connections.delete(target);
|
|
79
125
|
}
|
|
80
126
|
const release = bindSandboxWsSocket(socket, {
|
|
81
127
|
onMessage: (raw) => {
|
|
82
|
-
if (!this.#open)
|
|
83
|
-
return;
|
|
84
128
|
const parsed = parseSandboxWsPayload(raw);
|
|
129
|
+
const conn = this.#connections.get(target);
|
|
130
|
+
if (conn && !conn.placeholder) {
|
|
131
|
+
conn.lastChannel = {
|
|
132
|
+
type: parsed.type,
|
|
133
|
+
id: parsed.id || owner,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
85
136
|
logger.debug(formatCompact({
|
|
86
137
|
op: 'sandbox_recv',
|
|
87
138
|
target,
|
|
88
139
|
sender: parsed.id || owner,
|
|
140
|
+
channelType: parsed.type,
|
|
141
|
+
channelId: parsed.id || owner,
|
|
89
142
|
text: parsed.text.slice(0, 80),
|
|
90
143
|
}));
|
|
144
|
+
// Don't gate on #open — inbound must always reach the gateway so
|
|
145
|
+
// Command/AI dispatch and outbound replies work.
|
|
91
146
|
void this.#options.gateway.receive({
|
|
92
147
|
adapter: this.#options.id,
|
|
93
148
|
target,
|
|
@@ -95,6 +150,9 @@ export class SandboxWsEndpoint {
|
|
|
95
150
|
sender: parsed.id || owner,
|
|
96
151
|
metadata: Object.freeze({
|
|
97
152
|
type: parsed.type,
|
|
153
|
+
channelType: parsed.type,
|
|
154
|
+
channelId: parsed.id || owner,
|
|
155
|
+
endpoint: target,
|
|
98
156
|
elements: parsed.content,
|
|
99
157
|
timestamp: parsed.timestamp,
|
|
100
158
|
...(parsed.action ? { action: parsed.action } : {}),
|
|
@@ -108,8 +166,13 @@ export class SandboxWsEndpoint {
|
|
|
108
166
|
});
|
|
109
167
|
},
|
|
110
168
|
onClose: () => {
|
|
111
|
-
this
|
|
112
|
-
|
|
169
|
+
// Only drop the map entry if we still own this socket — a replace
|
|
170
|
+
// may have already swapped in a newer connection for the same target.
|
|
171
|
+
const current = this.#connections.get(target);
|
|
172
|
+
if (current && current.socket === socket) {
|
|
173
|
+
this.#connections.delete(target);
|
|
174
|
+
logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
|
|
175
|
+
}
|
|
113
176
|
},
|
|
114
177
|
onError: (err) => {
|
|
115
178
|
logger.warn(formatCompact({
|
|
@@ -119,7 +182,7 @@ export class SandboxWsEndpoint {
|
|
|
119
182
|
}));
|
|
120
183
|
},
|
|
121
184
|
});
|
|
122
|
-
this.#connections.set(target,
|
|
185
|
+
this.#connections.set(target, { target, owner, socket, release });
|
|
123
186
|
logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
124
187
|
if (!this.#options.defaults.randomNamePerConnection) {
|
|
125
188
|
const readyPayload = JSON.stringify({
|
|
@@ -144,12 +207,12 @@ export class SandboxWsEndpoint {
|
|
|
144
207
|
#ensurePlaceholder(name, owner) {
|
|
145
208
|
if (this.#connections.has(name))
|
|
146
209
|
return;
|
|
147
|
-
this.#connections.set(name,
|
|
210
|
+
this.#connections.set(name, {
|
|
148
211
|
target: name,
|
|
149
212
|
owner,
|
|
150
213
|
socket: { send: () => undefined, close: () => undefined },
|
|
151
214
|
release: () => undefined,
|
|
152
215
|
placeholder: true,
|
|
153
|
-
})
|
|
216
|
+
});
|
|
154
217
|
}
|
|
155
218
|
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -42,11 +42,17 @@ export declare function parseSandboxWsPayload(raw: string): {
|
|
|
42
42
|
payload: string;
|
|
43
43
|
};
|
|
44
44
|
};
|
|
45
|
+
export type SandboxOutboundChannel = {
|
|
46
|
+
readonly type?: string;
|
|
47
|
+
readonly id?: string;
|
|
48
|
+
readonly bot?: string;
|
|
49
|
+
readonly endpoint?: string;
|
|
50
|
+
readonly messageId?: string;
|
|
51
|
+
};
|
|
45
52
|
/**
|
|
46
53
|
* Wire-encode an already-rendered outbound payload.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* owns that before `endpoint.send`.
|
|
54
|
+
* Stamps `channel` so Console SandboxChat can filter by type+id (otherwise
|
|
55
|
+
* replies look like they disappeared).
|
|
50
56
|
*/
|
|
51
|
-
export declare function formatSandboxOutbound(payload: unknown): string;
|
|
57
|
+
export declare function formatSandboxOutbound(payload: unknown, channel?: SandboxOutboundChannel): string;
|
|
52
58
|
export declare function whenWsOpen(ws: SandboxWsSocket, fn: () => void): void;
|
package/lib/protocol.js
CHANGED
|
@@ -100,24 +100,53 @@ export function parseSandboxWsPayload(raw) {
|
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* Wire-encode an already-rendered outbound payload.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* owns that before `endpoint.send`.
|
|
103
|
+
* Stamps `channel` so Console SandboxChat can filter by type+id (otherwise
|
|
104
|
+
* replies look like they disappeared).
|
|
106
105
|
*/
|
|
107
|
-
export function formatSandboxOutbound(payload) {
|
|
106
|
+
export function formatSandboxOutbound(payload, channel = {}) {
|
|
107
|
+
const stamp = {};
|
|
108
|
+
if (channel.type)
|
|
109
|
+
stamp.type = channel.type;
|
|
110
|
+
if (channel.id)
|
|
111
|
+
stamp.id = channel.id;
|
|
112
|
+
if (channel.bot)
|
|
113
|
+
stamp.bot = channel.bot;
|
|
114
|
+
if (channel.endpoint)
|
|
115
|
+
stamp.endpoint = channel.endpoint;
|
|
116
|
+
if (channel.messageId)
|
|
117
|
+
stamp.messageId = channel.messageId;
|
|
108
118
|
if (typeof payload === 'string') {
|
|
109
119
|
return JSON.stringify({
|
|
120
|
+
...stamp,
|
|
110
121
|
content: [{ type: 'text', data: { text: payload } }],
|
|
111
122
|
timestamp: Date.now(),
|
|
112
123
|
});
|
|
113
124
|
}
|
|
114
125
|
if (Array.isArray(payload)) {
|
|
115
126
|
return JSON.stringify({
|
|
127
|
+
...stamp,
|
|
116
128
|
content: payload,
|
|
117
129
|
timestamp: Date.now(),
|
|
118
130
|
});
|
|
119
131
|
}
|
|
120
|
-
|
|
132
|
+
// Already a wire envelope ({ content, type, … }) — pass through so the
|
|
133
|
+
// Console UI can read `content` / `type` without an extra nesting layer.
|
|
134
|
+
// Bare segment objects ({ type: 'text', data: … }) still need wrapping.
|
|
135
|
+
if (payload
|
|
136
|
+
&& typeof payload === 'object'
|
|
137
|
+
&& !Array.isArray(payload)
|
|
138
|
+
&& ('content' in payload
|
|
139
|
+
|| 'type' in payload && 'timestamp' in payload)) {
|
|
140
|
+
const envelope = payload;
|
|
141
|
+
return JSON.stringify({
|
|
142
|
+
...stamp,
|
|
143
|
+
...envelope,
|
|
144
|
+
type: envelope.type ?? stamp.type,
|
|
145
|
+
id: envelope.id ?? stamp.id,
|
|
146
|
+
timestamp: typeof envelope.timestamp === 'number' ? envelope.timestamp : Date.now(),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return JSON.stringify({ ...stamp, content: payload, timestamp: Date.now() });
|
|
121
150
|
}
|
|
122
151
|
/** WebSocket.OPEN 常量值;Node <22 无全局 WebSocket,不能用 WebSocket.OPEN。 */
|
|
123
152
|
const WS_OPEN = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-sandbox",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Zhin.js Sandbox adapter for Plugin Runtime (WebSocket /sandbox)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -46,35 +46,42 @@
|
|
|
46
46
|
"directory": "plugins/adapters/sandbox"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"lucide-react": "^0.525.0",
|
|
49
50
|
"react": "^19.1.0",
|
|
51
|
+
"@zhin.js/client": "2.1.0",
|
|
50
52
|
"@zhin.js/console-contract": "1.0.0",
|
|
51
|
-
"@zhin.js/adapter": "1.0
|
|
52
|
-
"@zhin.js/core": "1.
|
|
53
|
-
"@zhin.js/host-http": "1.0.
|
|
53
|
+
"@zhin.js/adapter": "1.1.0",
|
|
54
|
+
"@zhin.js/core": "1.4.0",
|
|
55
|
+
"@zhin.js/host-http": "1.0.2",
|
|
54
56
|
"@zhin.js/logger": "1.0.75",
|
|
55
|
-
"@zhin.js/page": "1.0.
|
|
56
|
-
"@zhin.js/plugin-runtime": "1.0
|
|
57
|
+
"@zhin.js/page": "1.0.2",
|
|
58
|
+
"@zhin.js/plugin-runtime": "1.1.0"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
61
|
+
"@types/react": "^19.1.8",
|
|
59
62
|
"@types/ws": "^8.18.1",
|
|
60
63
|
"typescript": "^6.0.3",
|
|
61
64
|
"vitest": "^4.1.10",
|
|
62
65
|
"ws": "^8.21.0",
|
|
63
|
-
"@zhin.js/pagemanager": "2.0.
|
|
64
|
-
"@zhin.js/runtime": "1.0.
|
|
66
|
+
"@zhin.js/pagemanager": "2.0.5",
|
|
67
|
+
"@zhin.js/runtime": "1.0.2"
|
|
65
68
|
},
|
|
66
69
|
"peerDependencies": {
|
|
67
70
|
"react": "^19.0.0",
|
|
68
|
-
"@zhin.js/adapter": "1.0
|
|
71
|
+
"@zhin.js/adapter": "1.1.0",
|
|
72
|
+
"@zhin.js/client": "2.1.0",
|
|
69
73
|
"@zhin.js/console-contract": "1.0.0",
|
|
70
|
-
"@zhin.js/core": "1.
|
|
71
|
-
"@zhin.js/host-http": "1.0.
|
|
72
|
-
"@zhin.js/page": "1.0.
|
|
73
|
-
"@zhin.js/plugin-runtime": "1.0
|
|
74
|
+
"@zhin.js/core": "1.4.0",
|
|
75
|
+
"@zhin.js/host-http": "1.0.2",
|
|
76
|
+
"@zhin.js/page": "1.0.2",
|
|
77
|
+
"@zhin.js/plugin-runtime": "1.1.0"
|
|
74
78
|
},
|
|
75
79
|
"peerDependenciesMeta": {
|
|
76
80
|
"react": {
|
|
77
81
|
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@zhin.js/client": {
|
|
84
|
+
"optional": true
|
|
78
85
|
}
|
|
79
86
|
},
|
|
80
87
|
"publishConfig": {
|