@zhin.js/adapter-sandbox 7.0.1 → 7.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 +53 -0
- package/lib/endpoint.d.ts +3 -6
- package/lib/endpoint.js +45 -48
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +13 -3
- package/lib/protocol.js +23 -6
- package/package.json +19 -18
- package/pages/SandboxChat.js +4 -4
- package/pages/SandboxChat.tsx +4 -4
- package/schema.json +41 -5
- package/src/endpoint.ts +47 -59
- package/src/index.ts +1 -0
- package/src/protocol.ts +31 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# @zhin.js/adapter-process
|
|
2
2
|
|
|
3
|
+
## 7.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [0de46a8]
|
|
8
|
+
- Updated dependencies [c106ecc]
|
|
9
|
+
- Updated dependencies [b0f37ae]
|
|
10
|
+
- Updated dependencies [daffd4c]
|
|
11
|
+
- Updated dependencies [36c7400]
|
|
12
|
+
- Updated dependencies [162fa34]
|
|
13
|
+
- Updated dependencies [e40b048]
|
|
14
|
+
- Updated dependencies [f1708c3]
|
|
15
|
+
- Updated dependencies [e53444f]
|
|
16
|
+
- Updated dependencies [92b0dd7]
|
|
17
|
+
- Updated dependencies [a7df753]
|
|
18
|
+
- @zhin.js/host-http@1.0.8
|
|
19
|
+
- @zhin.js/im-contract@1.0.3
|
|
20
|
+
- @zhin.js/adapter@1.1.7
|
|
21
|
+
- @zhin.js/plugin-runtime@1.1.5
|
|
22
|
+
- @zhin.js/core@1.5.4
|
|
23
|
+
- @zhin.js/page@1.0.8
|
|
24
|
+
- @zhin.js/client@2.1.6
|
|
25
|
+
|
|
26
|
+
## 7.0.3
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- f8c7a54: fix: im
|
|
31
|
+
- Updated dependencies [f8c7a54]
|
|
32
|
+
- @zhin.js/logger@1.0.76
|
|
33
|
+
- @zhin.js/client@2.1.5
|
|
34
|
+
- @zhin.js/page@1.0.7
|
|
35
|
+
- @zhin.js/console-contract@1.0.1
|
|
36
|
+
- @zhin.js/host-http@1.0.7
|
|
37
|
+
- @zhin.js/adapter@1.1.6
|
|
38
|
+
- @zhin.js/core@1.5.3
|
|
39
|
+
- @zhin.js/im-contract@1.0.2
|
|
40
|
+
- @zhin.js/plugin-runtime@1.1.4
|
|
41
|
+
|
|
42
|
+
## 7.0.2
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- Updated dependencies [afc0e66]
|
|
47
|
+
- Updated dependencies [2e41ad5]
|
|
48
|
+
- Updated dependencies [9f57124]
|
|
49
|
+
- @zhin.js/core@1.5.2
|
|
50
|
+
- @zhin.js/adapter@1.1.5
|
|
51
|
+
- @zhin.js/im-contract@1.0.1
|
|
52
|
+
- @zhin.js/plugin-runtime@1.1.3
|
|
53
|
+
- @zhin.js/host-http@1.0.6
|
|
54
|
+
- @zhin.js/page@1.0.6
|
|
55
|
+
|
|
3
56
|
## 7.0.1
|
|
4
57
|
|
|
5
58
|
### Patch Changes
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
1
|
+
import type { EndpointInstance, EndpointSendRequest } from '@zhin.js/adapter';
|
|
2
2
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
3
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
4
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
@@ -16,14 +16,11 @@ export interface SandboxEndpointOptions {
|
|
|
16
16
|
export declare class SandboxWsEndpoint implements EndpointInstance {
|
|
17
17
|
#private;
|
|
18
18
|
constructor(options: SandboxEndpointOptions);
|
|
19
|
-
/** Live endpoint
|
|
19
|
+
/** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
|
|
20
20
|
get name(): string;
|
|
21
21
|
start(): void;
|
|
22
22
|
open(): void;
|
|
23
23
|
close(): void;
|
|
24
24
|
stop(): void;
|
|
25
|
-
send({
|
|
26
|
-
readonly target: string;
|
|
27
|
-
readonly payload: unknown;
|
|
28
|
-
}): unknown;
|
|
25
|
+
send({ conversation, payload }: EndpointSendRequest): unknown;
|
|
29
26
|
}
|
package/lib/endpoint.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* SandboxWsEndpoint — WebSocket lifecycle and MessageGateway bridge for /sandbox.
|
|
3
3
|
*/
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { formatCompact,
|
|
6
|
-
import { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, whenWsOpen, } from './protocol.js';
|
|
7
|
-
const logger = getLogger('sandbox');
|
|
5
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
6
|
+
import { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, sandboxInboundConversation, whenWsOpen, } from './protocol.js';
|
|
8
7
|
/**
|
|
9
8
|
* 多 sandbox endpoint 共用同一个 HttpHost 时,同 path 的所有 WS listener
|
|
10
9
|
* 都会被回调(入站重复、出站互窜)。按 endpoint 名隔离挂载路径:
|
|
@@ -44,6 +43,7 @@ function claimSandboxWsPath(http, name) {
|
|
|
44
43
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
45
44
|
*/
|
|
46
45
|
export class SandboxWsEndpoint {
|
|
46
|
+
#logger;
|
|
47
47
|
#options;
|
|
48
48
|
#connections = new Map();
|
|
49
49
|
#wsHandleRelease;
|
|
@@ -52,18 +52,19 @@ export class SandboxWsEndpoint {
|
|
|
52
52
|
#open = false;
|
|
53
53
|
#started = false;
|
|
54
54
|
constructor(options) {
|
|
55
|
+
this.#logger = getAdapterLogger('sandbox', options.defaults.id);
|
|
55
56
|
this.#options = options;
|
|
56
57
|
}
|
|
57
|
-
/** Live endpoint
|
|
58
|
+
/** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
|
|
58
59
|
get name() {
|
|
59
|
-
return this.#options.defaults.
|
|
60
|
+
return this.#options.defaults.id;
|
|
60
61
|
}
|
|
61
62
|
start() {
|
|
62
63
|
if (this.#started)
|
|
63
64
|
return;
|
|
64
65
|
this.#started = true;
|
|
65
66
|
// 多 endpoint 同 path 会被全部回调(入站重复、出站互窜),按名隔离。
|
|
66
|
-
const claim = claimSandboxWsPath(this.#options.http, this.#options.defaults.
|
|
67
|
+
const claim = claimSandboxWsPath(this.#options.http, this.#options.defaults.id);
|
|
67
68
|
this.#wsPathRelease = claim.release;
|
|
68
69
|
this.#wsPath = claim.path;
|
|
69
70
|
const handle = this.#options.http.ws(claim.path);
|
|
@@ -71,13 +72,9 @@ export class SandboxWsEndpoint {
|
|
|
71
72
|
this.#acceptConnection(connection);
|
|
72
73
|
});
|
|
73
74
|
if (!this.#options.defaults.randomNamePerConnection) {
|
|
74
|
-
this.#ensurePlaceholder(this.#options.defaults.
|
|
75
|
+
this.#ensurePlaceholder(this.#options.defaults.id, this.#options.defaults.owner);
|
|
75
76
|
}
|
|
76
|
-
logger.info(
|
|
77
|
-
op: 'sandbox_ws_mounted',
|
|
78
|
-
path: claim.path,
|
|
79
|
-
endpoint: this.#options.defaults.name,
|
|
80
|
-
}));
|
|
77
|
+
this.#logger.info(`ws mounted ${claim.path}`);
|
|
81
78
|
}
|
|
82
79
|
open() {
|
|
83
80
|
this.#open = true;
|
|
@@ -104,38 +101,41 @@ export class SandboxWsEndpoint {
|
|
|
104
101
|
}
|
|
105
102
|
this.#connections.clear();
|
|
106
103
|
this.#started = false;
|
|
107
|
-
logger.debug(formatCompact({ op: 'sandbox_stopped' }));
|
|
104
|
+
this.#logger.debug(formatCompact({ op: 'sandbox_stopped' }));
|
|
108
105
|
}
|
|
109
|
-
send({
|
|
106
|
+
send({ conversation, payload }) {
|
|
110
107
|
if (!this.#open)
|
|
111
108
|
return undefined;
|
|
112
|
-
// Reply
|
|
113
|
-
|
|
109
|
+
// Reply targets this endpoint's own live socket (fixed bot name, or the
|
|
110
|
+
// only live random-name connection); conversation carries kind/id stamp.
|
|
111
|
+
const connection = this.#connections.get(this.#options.defaults.id)
|
|
114
112
|
?? this.#findLiveConnection();
|
|
115
113
|
if (!connection) {
|
|
116
|
-
logger.debug(formatCompact({
|
|
114
|
+
this.#logger.debug(formatCompact({
|
|
115
|
+
op: 'sandbox_send_miss',
|
|
116
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
117
|
+
}));
|
|
117
118
|
return undefined;
|
|
118
119
|
}
|
|
119
120
|
if (connection.placeholder) {
|
|
120
|
-
logger.debug(formatCompact({
|
|
121
|
+
this.#logger.debug(formatCompact({
|
|
122
|
+
op: 'sandbox_send_placeholder',
|
|
123
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
124
|
+
}));
|
|
121
125
|
return undefined;
|
|
122
126
|
}
|
|
123
|
-
// Console UI filters by type+id; stamp
|
|
124
|
-
const channel = connection.lastChannel ?? {
|
|
125
|
-
type: 'private',
|
|
126
|
-
id: connection.owner,
|
|
127
|
-
};
|
|
127
|
+
// Console UI filters by type+id; stamp the conversation onto outbound wire.
|
|
128
128
|
connection.socket.send(formatSandboxOutbound(payload, {
|
|
129
|
-
type:
|
|
130
|
-
id:
|
|
131
|
-
bot: this.#options.defaults.
|
|
129
|
+
type: conversation.kind,
|
|
130
|
+
id: conversation.id,
|
|
131
|
+
bot: this.#options.defaults.id,
|
|
132
132
|
endpoint: connection.target,
|
|
133
133
|
}));
|
|
134
|
-
logger.debug(formatCompact({
|
|
134
|
+
this.#logger.debug(formatCompact({
|
|
135
135
|
op: 'sandbox_send',
|
|
136
136
|
target: connection.target,
|
|
137
|
-
channelType:
|
|
138
|
-
channelId:
|
|
137
|
+
channelType: conversation.kind,
|
|
138
|
+
channelId: conversation.id,
|
|
139
139
|
}));
|
|
140
140
|
return payload;
|
|
141
141
|
}
|
|
@@ -150,7 +150,7 @@ export class SandboxWsEndpoint {
|
|
|
150
150
|
#acceptConnection(connection) {
|
|
151
151
|
const target = this.#options.defaults.randomNamePerConnection
|
|
152
152
|
? `sandbox-${randomUUID().slice(0, 8)}`
|
|
153
|
-
: this.#options.defaults.
|
|
153
|
+
: this.#options.defaults.id;
|
|
154
154
|
const owner = this.#options.defaults.owner;
|
|
155
155
|
const socket = connection.socket;
|
|
156
156
|
// Fixed-name mode reuses `target`; dropping the prior entry without
|
|
@@ -172,39 +172,36 @@ export class SandboxWsEndpoint {
|
|
|
172
172
|
const release = bindSandboxWsSocket(socket, {
|
|
173
173
|
onMessage: (raw) => {
|
|
174
174
|
const parsed = parseSandboxWsPayload(raw);
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
logger.debug(formatCompact({
|
|
175
|
+
const sender = parsed.id || owner;
|
|
176
|
+
const conversation = sandboxInboundConversation(String(this.#options.id), {
|
|
177
|
+
type: parsed.type,
|
|
178
|
+
id: sender,
|
|
179
|
+
});
|
|
180
|
+
this.#logger.debug(formatCompact({
|
|
183
181
|
op: 'sandbox_recv',
|
|
184
182
|
target,
|
|
185
|
-
sender
|
|
183
|
+
sender,
|
|
186
184
|
channelType: parsed.type,
|
|
187
|
-
channelId:
|
|
185
|
+
channelId: sender,
|
|
188
186
|
text: parsed.text.slice(0, 80),
|
|
189
187
|
}));
|
|
190
188
|
// Don't gate on #open — inbound must always reach the gateway so
|
|
191
189
|
// Command/AI dispatch and outbound replies work.
|
|
192
190
|
void this.#options.gateway.receive({
|
|
193
|
-
|
|
194
|
-
target,
|
|
191
|
+
conversation,
|
|
195
192
|
content: parsed.text,
|
|
196
|
-
sender:
|
|
193
|
+
sender: { id: sender },
|
|
194
|
+
endpointId: target,
|
|
197
195
|
metadata: Object.freeze({
|
|
198
196
|
type: parsed.type,
|
|
199
197
|
channelType: parsed.type,
|
|
200
198
|
channelId: parsed.id || owner,
|
|
201
|
-
endpoint: target,
|
|
202
199
|
elements: parsed.content,
|
|
203
200
|
timestamp: parsed.timestamp,
|
|
204
201
|
...(parsed.action ? { action: parsed.action } : {}),
|
|
205
202
|
}),
|
|
206
203
|
}).catch((err) => {
|
|
207
|
-
logger.warn(formatCompact({
|
|
204
|
+
this.#logger.warn(formatCompact({
|
|
208
205
|
op: 'sandbox_gateway_receive_failed',
|
|
209
206
|
target,
|
|
210
207
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -217,11 +214,11 @@ export class SandboxWsEndpoint {
|
|
|
217
214
|
const current = this.#connections.get(target);
|
|
218
215
|
if (current && current.socket === socket) {
|
|
219
216
|
this.#connections.delete(target);
|
|
220
|
-
logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
|
|
217
|
+
this.#logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
|
|
221
218
|
}
|
|
222
219
|
},
|
|
223
220
|
onError: (err) => {
|
|
224
|
-
logger.warn(formatCompact({
|
|
221
|
+
this.#logger.warn(formatCompact({
|
|
225
222
|
op: 'sandbox_ws_error',
|
|
226
223
|
target,
|
|
227
224
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -229,7 +226,7 @@ export class SandboxWsEndpoint {
|
|
|
229
226
|
},
|
|
230
227
|
});
|
|
231
228
|
this.#connections.set(target, { target, owner, socket, release });
|
|
232
|
-
logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
229
|
+
this.#logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
233
230
|
if (!this.#options.defaults.randomNamePerConnection) {
|
|
234
231
|
const readyPayload = JSON.stringify({
|
|
235
232
|
type: 'ready',
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { bindSandboxWsSocket, formatSandboxOutbound, normalizeSandboxOutboundSegments, parseSandboxWsPayload, resolveSandboxEndpoint, whenWsOpen, type MessageElement, type MessageType, type ResolvedSandboxBot, type SandboxAdapterConfig, type SandboxWsSocket, } from './protocol.js';
|
|
1
|
+
export { bindSandboxWsSocket, formatSandboxOutbound, normalizeSandboxOutboundSegments, parseSandboxWsPayload, resolveSandboxEndpoint, sandboxInboundConversation, whenWsOpen, type MessageElement, type MessageType, type ResolvedSandboxBot, type SandboxAdapterConfig, type SandboxWsSocket, } from './protocol.js';
|
|
2
2
|
export { SandboxWsEndpoint, type SandboxEndpointOptions, } from './endpoint.js';
|
package/lib/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { bindSandboxWsSocket, formatSandboxOutbound, normalizeSandboxOutboundSegments, parseSandboxWsPayload, resolveSandboxEndpoint, whenWsOpen, } from './protocol.js';
|
|
1
|
+
export { bindSandboxWsSocket, formatSandboxOutbound, normalizeSandboxOutboundSegments, parseSandboxWsPayload, resolveSandboxEndpoint, sandboxInboundConversation, whenWsOpen, } from './protocol.js';
|
|
2
2
|
export { SandboxWsEndpoint, } from './endpoint.js';
|
package/lib/protocol.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Sandbox WebSocket wire protocol helpers (no legacy Adapter/Endpoint). */
|
|
2
|
+
import type { ConversationRef } from '@zhin.js/im-contract';
|
|
2
3
|
export type MessageType = 'private' | 'group' | 'guild' | 'direct' | 'channel';
|
|
3
4
|
export interface MessageElement {
|
|
4
5
|
readonly type: string;
|
|
@@ -14,19 +15,19 @@ export interface SandboxWsSocket {
|
|
|
14
15
|
}
|
|
15
16
|
export type ResolvedSandboxBot = {
|
|
16
17
|
readonly context: 'sandbox';
|
|
17
|
-
readonly
|
|
18
|
+
readonly id: string;
|
|
18
19
|
readonly owner: string;
|
|
19
20
|
readonly randomNamePerConnection: boolean;
|
|
20
21
|
};
|
|
21
22
|
export interface SandboxAdapterConfig {
|
|
22
23
|
/** Runtime expands `endpoints[i]` onto the top level — prefer these. */
|
|
23
24
|
readonly context?: string;
|
|
24
|
-
readonly
|
|
25
|
+
readonly id?: string;
|
|
25
26
|
readonly owner?: string;
|
|
26
27
|
/** Legacy shape: endpoint entries nested under `endpoints[]`. */
|
|
27
28
|
readonly endpoints?: ReadonlyArray<{
|
|
28
29
|
readonly context?: string;
|
|
29
|
-
readonly
|
|
30
|
+
readonly id?: string;
|
|
30
31
|
readonly owner?: string;
|
|
31
32
|
}>;
|
|
32
33
|
}
|
|
@@ -47,6 +48,15 @@ export declare function parseSandboxWsPayload(raw: string): {
|
|
|
47
48
|
payload: string;
|
|
48
49
|
};
|
|
49
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* 入站归一化 → ConversationRef。sandbox 无平台社交图谱:
|
|
53
|
+
* `private`/`group`/`channel` 直映射;`direct`(私聊)归 'private';
|
|
54
|
+
* `guild`(频道容器语义)归 'channel'。无 guild/temp 容器信息,不产生 parent。
|
|
55
|
+
*/
|
|
56
|
+
export declare function sandboxInboundConversation(endpointKey: string, msg: {
|
|
57
|
+
readonly type: MessageType;
|
|
58
|
+
readonly id: string;
|
|
59
|
+
}): ConversationRef;
|
|
50
60
|
export type SandboxOutboundChannel = {
|
|
51
61
|
readonly type?: string;
|
|
52
62
|
readonly id?: string;
|
package/lib/protocol.js
CHANGED
|
@@ -5,19 +5,19 @@ import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
|
5
5
|
const logger = getLogger('sandbox');
|
|
6
6
|
export function resolveSandboxEndpoint(appConfig) {
|
|
7
7
|
const entry = appConfig.endpoints?.find((item) => item.context === 'sandbox');
|
|
8
|
-
const fixedName = typeof appConfig.
|
|
9
|
-
? appConfig.
|
|
10
|
-
: typeof entry?.
|
|
11
|
-
? entry.
|
|
8
|
+
const fixedName = typeof appConfig.id === 'string' && appConfig.id
|
|
9
|
+
? appConfig.id
|
|
10
|
+
: typeof entry?.id === 'string' && entry.id
|
|
11
|
+
? entry.id
|
|
12
12
|
: undefined;
|
|
13
|
-
const
|
|
13
|
+
const id = fixedName || process.env.SANDBOX_BOT_NAME || 'sandbox-bot';
|
|
14
14
|
const owner = (typeof appConfig.owner === 'string' && appConfig.owner)
|
|
15
15
|
|| (typeof entry?.owner === 'string' && entry.owner)
|
|
16
16
|
|| process.env.SANDBOX_BOT_OWNER
|
|
17
17
|
|| 'sandbox-user';
|
|
18
18
|
return {
|
|
19
19
|
context: 'sandbox',
|
|
20
|
-
|
|
20
|
+
id,
|
|
21
21
|
owner,
|
|
22
22
|
randomNamePerConnection: !fixedName,
|
|
23
23
|
};
|
|
@@ -107,6 +107,23 @@ export function parseSandboxWsPayload(raw) {
|
|
|
107
107
|
}
|
|
108
108
|
return { type, id, content, timestamp: payload.timestamp ?? Date.now(), text, action };
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* 入站归一化 → ConversationRef。sandbox 无平台社交图谱:
|
|
112
|
+
* `private`/`group`/`channel` 直映射;`direct`(私聊)归 'private';
|
|
113
|
+
* `guild`(频道容器语义)归 'channel'。无 guild/temp 容器信息,不产生 parent。
|
|
114
|
+
*/
|
|
115
|
+
export function sandboxInboundConversation(endpointKey, msg) {
|
|
116
|
+
const kind = msg.type === 'direct'
|
|
117
|
+
? 'private'
|
|
118
|
+
: msg.type === 'guild'
|
|
119
|
+
? 'channel'
|
|
120
|
+
: msg.type;
|
|
121
|
+
return {
|
|
122
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
123
|
+
kind,
|
|
124
|
+
id: msg.id,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
110
127
|
const MEDIA_SEGMENT_TYPES = new Set(['image', 'audio', 'video', 'file']);
|
|
111
128
|
/**
|
|
112
129
|
* base64 内联值:Console UI 的 resolveMediaSrc 只识别 data: / base64://
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-sandbox",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.4",
|
|
4
4
|
"description": "Zhin.js Sandbox adapter for Plugin Runtime (WebSocket /sandbox)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -48,14 +48,15 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"lucide-react": "^0.525.0",
|
|
50
50
|
"react": "^19.2.8",
|
|
51
|
-
"@zhin.js/adapter": "1.1.
|
|
52
|
-
"@zhin.js/client": "2.1.
|
|
53
|
-
"@zhin.js/console-contract": "1.0.
|
|
54
|
-
"@zhin.js/core": "1.5.
|
|
55
|
-
"@zhin.js/host-http": "1.0.
|
|
56
|
-
"@zhin.js/
|
|
57
|
-
"@zhin.js/
|
|
58
|
-
"@zhin.js/
|
|
51
|
+
"@zhin.js/adapter": "1.1.7",
|
|
52
|
+
"@zhin.js/client": "2.1.6",
|
|
53
|
+
"@zhin.js/console-contract": "1.0.1",
|
|
54
|
+
"@zhin.js/core": "1.5.4",
|
|
55
|
+
"@zhin.js/host-http": "1.0.8",
|
|
56
|
+
"@zhin.js/im-contract": "1.0.3",
|
|
57
|
+
"@zhin.js/logger": "1.0.76",
|
|
58
|
+
"@zhin.js/page": "1.0.8",
|
|
59
|
+
"@zhin.js/plugin-runtime": "1.1.5"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@types/react": "^19.2.18",
|
|
@@ -63,18 +64,18 @@
|
|
|
63
64
|
"typescript": "^6.0.3",
|
|
64
65
|
"vitest": "^4.1.10",
|
|
65
66
|
"ws": "^8.21.1",
|
|
66
|
-
"@zhin.js/pagemanager": "2.0.
|
|
67
|
-
"@zhin.js/runtime": "1.0.
|
|
67
|
+
"@zhin.js/pagemanager": "2.0.14",
|
|
68
|
+
"@zhin.js/runtime": "1.0.9"
|
|
68
69
|
},
|
|
69
70
|
"peerDependencies": {
|
|
70
71
|
"react": "^19.0.0",
|
|
71
|
-
"@zhin.js/adapter": "1.1.
|
|
72
|
-
"@zhin.js/client": "2.1.
|
|
73
|
-
"@zhin.js/console-contract": "1.0.
|
|
74
|
-
"@zhin.js/core": "1.5.
|
|
75
|
-
"@zhin.js/host-http": "1.0.
|
|
76
|
-
"@zhin.js/page": "1.0.
|
|
77
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
72
|
+
"@zhin.js/adapter": "1.1.7",
|
|
73
|
+
"@zhin.js/client": "2.1.6",
|
|
74
|
+
"@zhin.js/console-contract": "1.0.1",
|
|
75
|
+
"@zhin.js/core": "1.5.4",
|
|
76
|
+
"@zhin.js/host-http": "1.0.8",
|
|
77
|
+
"@zhin.js/page": "1.0.8",
|
|
78
|
+
"@zhin.js/plugin-runtime": "1.1.5"
|
|
78
79
|
},
|
|
79
80
|
"peerDependenciesMeta": {
|
|
80
81
|
"react": {
|
package/pages/SandboxChat.js
CHANGED
|
@@ -15,7 +15,7 @@ export default function Sandbox() {
|
|
|
15
15
|
const [faceList, setFaceList] = useState([]);
|
|
16
16
|
const [activeChannel, setActiveChannel] = useState(channels[0]);
|
|
17
17
|
const [inputText, setInputText] = useState('');
|
|
18
|
-
const [
|
|
18
|
+
const [endpointId, setBotName] = useState('ProcessEndpoint');
|
|
19
19
|
const [connected, setConnected] = useState(false);
|
|
20
20
|
const [showFacePicker, setShowFacePicker] = useState(false);
|
|
21
21
|
/** 输入区:插入图片 / 视频 / 音频 URL */
|
|
@@ -59,7 +59,7 @@ export default function Sandbox() {
|
|
|
59
59
|
? parseTextToSegments(data.content)
|
|
60
60
|
: Array.isArray(data.content) ? data.content : parseTextToSegments(String(data.content ?? ''));
|
|
61
61
|
const channelName = data.type === 'private'
|
|
62
|
-
? `私聊-${data.bot ||
|
|
62
|
+
? `私聊-${data.bot || endpointId}`
|
|
63
63
|
: data.type === 'group'
|
|
64
64
|
? `群组-${data.id}`
|
|
65
65
|
: `频道-${data.id}`;
|
|
@@ -74,7 +74,7 @@ export default function Sandbox() {
|
|
|
74
74
|
setMessages((prev) => [...prev, {
|
|
75
75
|
id: data.messageId ?? `bot_${data.timestamp}`, type: 'received', channelType,
|
|
76
76
|
channelId: data.id, channelName, senderId: 'endpoint',
|
|
77
|
-
senderName: data.bot ||
|
|
77
|
+
senderName: data.bot || endpointId, content, timestamp: data.timestamp,
|
|
78
78
|
}]);
|
|
79
79
|
};
|
|
80
80
|
const sendInteractiveAction = (payload) => {
|
|
@@ -355,7 +355,7 @@ export default function Sandbox() {
|
|
|
355
355
|
return (_jsxs("div", { className: cn("menu-item", isActive && "active"), onClick: () => switchChannel(channel), children: [_jsx("span", { className: "shrink-0", children: getChannelIcon(channel.type) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium truncate", children: channel.name }), _jsx("div", { className: "text-xs text-muted-foreground", children: channel.type === 'private' ? '私聊' : channel.type === 'group' ? '群聊' : '频道' })] }), channel.unread > 0 && _jsx("span", { className: "inline-flex items-center justify-center h-5 min-w-5 rounded-full bg-destructive text-destructive-foreground text-[10px] font-medium px-1", children: channel.unread })] }, channel.id));
|
|
356
356
|
}), _jsxs("div", { className: "pt-2 mt-2 border-t space-y-1", children: [_jsxs("div", { className: cn("menu-item", viewMode === 'requests' && "active"), onClick: () => { setViewMode('requests'); if (window.innerWidth < 768)
|
|
357
357
|
setShowChannelList(false); }, children: [_jsx(UserPlus, { size: 16, className: "shrink-0" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium", children: "\u8BF7\u6C42" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "\u597D\u53CB/\u7FA4\u9080\u8BF7\u7B49" })] })] }), _jsxs("div", { className: cn("menu-item", viewMode === 'notices' && "active"), onClick: () => { setViewMode('notices'); if (window.innerWidth < 768)
|
|
358
|
-
setShowChannelList(false); }, children: [_jsx(Bell, { size: 16, className: "shrink-0" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium", children: "\u901A\u77E5" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "\u7FA4\u7BA1/\u64A4\u56DE\u7B49" })] })] })] })] }), _jsx("div", { className: "p-2 border-t", children: _jsx("button", { className: "w-full py-2 px-3 rounded-md border border-dashed text-sm text-muted-foreground hover:bg-accent transition-colors", onClick: addChannel, children: "+ \u6DFB\u52A0\u9891\u9053" }) })] }), showChannelList && _jsx("div", { className: "channel-overlay md:hidden", onClick: () => setShowChannelList(false) }), _jsxs("div", { className: "chat-area", children: [viewMode === 'requests' && (_jsxs("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden", children: [_jsx("div", { className: "p-3 border-b flex-shrink-0", children: _jsxs("h2", { className: "text-lg font-bold flex items-center gap-2", children: [_jsx(UserPlus, { size: 20 }), " \u8BF7\u6C42"] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center", children: [_jsx(UserPlus, { size: 48, className: "opacity-30" }), _jsx("span", { children: "\u6C99\u76D2\u4E3A\u6A21\u62DF\u73AF\u5883\uFF0C\u6682\u65E0\u8BF7\u6C42\u6570\u636E" }), _jsxs("span", { className: "text-sm", children: ["\u5B9E\u9645\u597D\u53CB/\u7FA4\u9080\u8BF7\u7B49\u8BF7\u6C42\u8BF7\u5230\u4FA7\u8FB9\u680F ", _jsx("strong", { children: "\u673A\u5668\u4EBA" }), " \u9875\u9762\u8FDB\u5165\u5BF9\u5E94\u673A\u5668\u4EBA\u7BA1\u7406\u67E5\u770B"] })] })] })), viewMode === 'notices' && (_jsxs("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden", children: [_jsx("div", { className: "p-3 border-b flex-shrink-0", children: _jsxs("h2", { className: "text-lg font-bold flex items-center gap-2", children: [_jsx(Bell, { size: 20 }), " \u901A\u77E5"] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center", children: [_jsx(Bell, { size: 48, className: "opacity-30" }), _jsx("span", { children: "\u6C99\u76D2\u4E3A\u6A21\u62DF\u73AF\u5883\uFF0C\u6682\u65E0\u901A\u77E5\u6570\u636E" }), _jsxs("span", { className: "text-sm", children: ["\u5B9E\u9645\u7FA4\u7BA1\u3001\u64A4\u56DE\u7B49\u901A\u77E5\u8BF7\u5230\u4FA7\u8FB9\u680F ", _jsx("strong", { children: "\u673A\u5668\u4EBA" }), " \u9875\u9762\u8FDB\u5165\u5BF9\u5E94\u673A\u5668\u4EBA\u7BA1\u7406\u67E5\u770B"] })] })] })), viewMode === 'chat' && (_jsxs(_Fragment, { children: [_jsx("div", { className: "rounded-lg border bg-card p-3 flex-shrink-0", children: _jsxs("div", { className: "flex justify-between items-center flex-wrap gap-2", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "p-2 rounded-lg bg-secondary", children: getChannelIcon(activeChannel.type) }), _jsxs("div", { children: [_jsx("h2", { className: "text-lg font-bold", children: activeChannel.name }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { children: activeChannel.id }), _jsx("span", { className: "inline-flex items-center px-1.5 py-0.5 rounded border text-[10px]", children: channelMessages.length }), _jsx("span", { children: "\u6761\u6D88\u606F" })] })] }), _jsx("span", { className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-secondary text-secondary-foreground", children: activeChannel.type === 'private' ? '私聊' : activeChannel.type === 'group' ? '群聊' : '频道' })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { value:
|
|
358
|
+
setShowChannelList(false); }, children: [_jsx(Bell, { size: 16, className: "shrink-0" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium", children: "\u901A\u77E5" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "\u7FA4\u7BA1/\u64A4\u56DE\u7B49" })] })] })] })] }), _jsx("div", { className: "p-2 border-t", children: _jsx("button", { className: "w-full py-2 px-3 rounded-md border border-dashed text-sm text-muted-foreground hover:bg-accent transition-colors", onClick: addChannel, children: "+ \u6DFB\u52A0\u9891\u9053" }) })] }), showChannelList && _jsx("div", { className: "channel-overlay md:hidden", onClick: () => setShowChannelList(false) }), _jsxs("div", { className: "chat-area", children: [viewMode === 'requests' && (_jsxs("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden", children: [_jsx("div", { className: "p-3 border-b flex-shrink-0", children: _jsxs("h2", { className: "text-lg font-bold flex items-center gap-2", children: [_jsx(UserPlus, { size: 20 }), " \u8BF7\u6C42"] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center", children: [_jsx(UserPlus, { size: 48, className: "opacity-30" }), _jsx("span", { children: "\u6C99\u76D2\u4E3A\u6A21\u62DF\u73AF\u5883\uFF0C\u6682\u65E0\u8BF7\u6C42\u6570\u636E" }), _jsxs("span", { className: "text-sm", children: ["\u5B9E\u9645\u597D\u53CB/\u7FA4\u9080\u8BF7\u7B49\u8BF7\u6C42\u8BF7\u5230\u4FA7\u8FB9\u680F ", _jsx("strong", { children: "\u673A\u5668\u4EBA" }), " \u9875\u9762\u8FDB\u5165\u5BF9\u5E94\u673A\u5668\u4EBA\u7BA1\u7406\u67E5\u770B"] })] })] })), viewMode === 'notices' && (_jsxs("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden", children: [_jsx("div", { className: "p-3 border-b flex-shrink-0", children: _jsxs("h2", { className: "text-lg font-bold flex items-center gap-2", children: [_jsx(Bell, { size: 20 }), " \u901A\u77E5"] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center", children: [_jsx(Bell, { size: 48, className: "opacity-30" }), _jsx("span", { children: "\u6C99\u76D2\u4E3A\u6A21\u62DF\u73AF\u5883\uFF0C\u6682\u65E0\u901A\u77E5\u6570\u636E" }), _jsxs("span", { className: "text-sm", children: ["\u5B9E\u9645\u7FA4\u7BA1\u3001\u64A4\u56DE\u7B49\u901A\u77E5\u8BF7\u5230\u4FA7\u8FB9\u680F ", _jsx("strong", { children: "\u673A\u5668\u4EBA" }), " \u9875\u9762\u8FDB\u5165\u5BF9\u5E94\u673A\u5668\u4EBA\u7BA1\u7406\u67E5\u770B"] })] })] })), viewMode === 'chat' && (_jsxs(_Fragment, { children: [_jsx("div", { className: "rounded-lg border bg-card p-3 flex-shrink-0", children: _jsxs("div", { className: "flex justify-between items-center flex-wrap gap-2", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "p-2 rounded-lg bg-secondary", children: getChannelIcon(activeChannel.type) }), _jsxs("div", { children: [_jsx("h2", { className: "text-lg font-bold", children: activeChannel.name }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { children: activeChannel.id }), _jsx("span", { className: "inline-flex items-center px-1.5 py-0.5 rounded border text-[10px]", children: channelMessages.length }), _jsx("span", { children: "\u6761\u6D88\u606F" })] })] }), _jsx("span", { className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-secondary text-secondary-foreground", children: activeChannel.type === 'private' ? '私聊' : activeChannel.type === 'group' ? '群聊' : '频道' })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { value: endpointId, onChange: (e) => setBotName(e.target.value), placeholder: "\u673A\u5668\u4EBA\u540D\u79F0", className: "h-8 w-28 rounded-md border bg-transparent px-2 text-sm" }), _jsxs("button", { className: "inline-flex items-center gap-1 h-8 px-3 rounded-md bg-secondary text-secondary-foreground text-sm hover:bg-secondary/80", onClick: clearMessages, children: [_jsx(Trash2, { size: 14 }), " \u6E05\u7A7A"] })] })] }) }), _jsx("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0", children: _jsx("div", { className: "flex-1 overflow-y-auto p-4", children: channelMessages.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center h-full gap-3", children: [_jsx(MessageSquare, { size: 64, className: "text-muted-foreground/20" }), _jsx("span", { className: "text-muted-foreground", children: "\u6682\u65E0\u6D88\u606F\uFF0C\u5F00\u59CB\u5BF9\u8BDD\u5427\uFF01" })] })) : (_jsxs("div", { className: "space-y-2", children: [channelMessages.map((msg) => (_jsx("div", { className: cn("flex", msg.type === 'sent' ? "justify-end" : "justify-start"), children: _jsxs("div", { className: cn("max-w-[70%] p-3 rounded-2xl", msg.type === 'sent' ? "bg-primary text-primary-foreground" : "bg-muted"), children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [msg.type === 'received' && _jsx(Bot, { size: 14 }), msg.type === 'sent' && _jsx(User, { size: 14 }), _jsx("span", { className: "text-xs font-medium opacity-90", children: msg.senderName }), _jsx("span", { className: "text-xs opacity-70", children: new Date(msg.timestamp).toLocaleTimeString() })] }), _jsx("div", { className: "text-sm space-y-1", children: renderMessageSegments(msg.content, msg.type === 'sent') })] }) }, msg.id))), _jsx("div", { ref: messagesEndRef })] })) }) }), _jsxs("div", { className: "rounded-lg border bg-card p-3 flex-shrink-0 space-y-3", children: [_jsxs("div", { className: "flex gap-2 items-center flex-wrap", children: [_jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", showFacePicker ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setShowFacePicker(!showFacePicker); setMediaPanel(null); }, title: "\u63D2\u5165\u8868\u60C5", children: _jsx(Smile, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'image' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'image' ? null : 'image')); setShowFacePicker(false); }, title: "\u63D2\u5165\u56FE\u7247 URL", children: _jsx(Image, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'video' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'video' ? null : 'video')); setShowFacePicker(false); }, title: "\u63D2\u5165\u89C6\u9891 URL", children: _jsx(Video, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'audio' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'audio' ? null : 'audio')); setShowFacePicker(false); }, title: "\u63D2\u5165\u97F3\u9891 URL", children: _jsx(Music, { size: 16 }) }), _jsx("div", { className: "flex-1 min-w-[1rem]" }), inputText && (_jsx("button", { className: "h-8 w-8 rounded-md flex items-center justify-center hover:bg-accent transition-colors", onClick: () => { setInputText(''); setPreviewSegments([]); }, children: _jsx(X, { size: 16 }) }))] }), showFacePicker && (_jsxs("div", { className: "p-3 rounded-md border bg-muted/30 max-h-64 overflow-y-auto space-y-2", children: [_jsx("input", { value: faceSearchQuery, onChange: (e) => setFaceSearchQuery(e.target.value), placeholder: "\u641C\u7D22\u8868\u60C5...", className: "w-full h-8 rounded-md border bg-transparent px-2 text-sm" }), _jsx("div", { className: "grid grid-cols-8 gap-1", children: filteredFaces.slice(0, 80).map((face) => (_jsx("button", { onClick: () => insertFace(face.id), title: face.name, className: "w-10 h-10 rounded-md border flex items-center justify-center hover:bg-accent transition-colors", children: _jsx("img", { src: `https://face.viki.moe/apng/${face.id}.png`, alt: face.name, className: "w-8 h-8" }) }, face.id))) }), filteredFaces.length === 0 && (_jsxs("div", { className: "flex flex-col items-center gap-2 py-4", children: [_jsx(Search, { size: 32, className: "text-muted-foreground/30" }), _jsx("span", { className: "text-sm text-muted-foreground", children: "\u672A\u627E\u5230\u5339\u914D\u7684\u8868\u60C5" })] }))] })), mediaPanel && (_jsxs("div", { className: "p-3 rounded-md border bg-muted/30 space-y-2", children: [_jsxs("p", { className: "text-xs text-muted-foreground", children: [mediaPanel === 'image' && '支持 http(s) 图片链接或 data URL', mediaPanel === 'video' && '支持浏览器可解码的视频直链(如 .mp4、.webm)', mediaPanel === 'audio' && '支持 .mp3、.ogg、.wav 等音频直链'] }), _jsx("input", { value: mediaUrl, onChange: (e) => setMediaUrl(e.target.value), placeholder: mediaPanel === 'image' ? '图片 URL…' : mediaPanel === 'video' ? '视频 URL…' : '音频 URL…', className: "w-full h-8 rounded-md border border-input bg-background px-2 text-sm", onKeyDown: (e) => { if (e.key === 'Enter') {
|
|
359
359
|
e.preventDefault();
|
|
360
360
|
commitMediaUrl();
|
|
361
361
|
} } }), _jsxs("button", { type: "button", className: "inline-flex items-center gap-1 h-8 px-3 rounded-md bg-primary text-primary-foreground text-sm disabled:opacity-50", onClick: commitMediaUrl, disabled: !mediaUrl.trim(), children: [_jsx(Check, { size: 14 }), " \u63D2\u5165\u5230\u8F93\u5165\u6846"] })] })), _jsxs("div", { className: "flex gap-2 items-start", children: [_jsxs("div", { className: "flex-1 relative", children: [_jsx(RichTextEditor, { ref: editorRef, placeholder: `向 ${activeChannel.name} 发送消息...`, onSend: handleSendMessage, onChange: handleEditorChange, onAtTrigger: handleAtTrigger, minHeight: "44px", maxHeight: "200px" }), atPopoverPosition && (_jsx("div", { className: "absolute z-50 rounded-lg border bg-popover shadow-md min-w-60 max-h-72 overflow-y-auto p-1", style: { top: `${atPopoverPosition.top}px`, left: `${atPopoverPosition.left}px` }, children: filteredAtSuggestions.length > 0 ? filteredAtSuggestions.map((user) => (_jsxs("div", { className: "flex items-center gap-2 p-2 rounded-md cursor-pointer hover:bg-accent transition-colors", onClick: () => selectAtUser(user), children: [_jsx(User, { size: 16, className: "text-muted-foreground" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-sm font-medium", children: user.name }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["ID: ", user.id] })] })] }, user.id))) : (_jsxs("div", { className: "flex flex-col items-center gap-2 p-4", children: [_jsx(Search, { size: 20, className: "text-muted-foreground/50" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "\u672A\u627E\u5230\u5339\u914D\u7684\u7528\u6237" })] })) }))] }), _jsxs("button", { className: "inline-flex items-center gap-1.5 h-10 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50 transition-colors hover:bg-primary/90", onClick: () => { const c = editorRef.current?.getContent(); if (c)
|
package/pages/SandboxChat.tsx
CHANGED
|
@@ -32,7 +32,7 @@ export default function Sandbox() {
|
|
|
32
32
|
const [faceList, setFaceList] = useState<Face[]>([])
|
|
33
33
|
const [activeChannel, setActiveChannel] = useState<Channel>(channels[0])
|
|
34
34
|
const [inputText, setInputText] = useState('')
|
|
35
|
-
const [
|
|
35
|
+
const [endpointId, setBotName] = useState('ProcessEndpoint')
|
|
36
36
|
const [connected, setConnected] = useState(false)
|
|
37
37
|
const [showFacePicker, setShowFacePicker] = useState(false)
|
|
38
38
|
/** 输入区:插入图片 / 视频 / 音频 URL */
|
|
@@ -79,7 +79,7 @@ export default function Sandbox() {
|
|
|
79
79
|
: Array.isArray(data.content) ? data.content as MessageSegment[] : parseTextToSegments(String(data.content ?? ''))
|
|
80
80
|
|
|
81
81
|
const channelName = data.type === 'private'
|
|
82
|
-
? `私聊-${data.bot ||
|
|
82
|
+
? `私聊-${data.bot || endpointId}`
|
|
83
83
|
: data.type === 'group'
|
|
84
84
|
? `群组-${data.id}`
|
|
85
85
|
: `频道-${data.id}`
|
|
@@ -95,7 +95,7 @@ export default function Sandbox() {
|
|
|
95
95
|
setMessages((prev) => [...prev, {
|
|
96
96
|
id: data.messageId ?? `bot_${data.timestamp}`, type: 'received', channelType,
|
|
97
97
|
channelId: data.id, channelName, senderId: 'endpoint',
|
|
98
|
-
senderName: data.bot ||
|
|
98
|
+
senderName: data.bot || endpointId, content, timestamp: data.timestamp,
|
|
99
99
|
}])
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -509,7 +509,7 @@ export default function Sandbox() {
|
|
|
509
509
|
</span>
|
|
510
510
|
</div>
|
|
511
511
|
<div className="flex items-center gap-2">
|
|
512
|
-
<input value={
|
|
512
|
+
<input value={endpointId} onChange={(e) => setBotName(e.target.value)} placeholder="机器人名称"
|
|
513
513
|
className="h-8 w-28 rounded-md border bg-transparent px-2 text-sm" />
|
|
514
514
|
<button className="inline-flex items-center gap-1 h-8 px-3 rounded-md bg-secondary text-secondary-foreground text-sm hover:bg-secondary/80" onClick={clearMessages}>
|
|
515
515
|
<Trash2 size={14} /> 清空
|
package/schema.json
CHANGED
|
@@ -3,16 +3,48 @@
|
|
|
3
3
|
"type": "object",
|
|
4
4
|
"additionalProperties": false,
|
|
5
5
|
"properties": {
|
|
6
|
+
"master": {
|
|
7
|
+
"type": [
|
|
8
|
+
"string",
|
|
9
|
+
"number"
|
|
10
|
+
],
|
|
11
|
+
"description": "框架 master(sandbox client id (distinct from endpoints[].owner);AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
|
|
12
|
+
},
|
|
13
|
+
"trusted": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": {
|
|
16
|
+
"type": [
|
|
17
|
+
"string",
|
|
18
|
+
"number"
|
|
19
|
+
],
|
|
20
|
+
"description": "Trusted sandbox client id (distinct from endpoints[].owner)"
|
|
21
|
+
},
|
|
22
|
+
"description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
|
|
23
|
+
},
|
|
6
24
|
"endpoints": {
|
|
7
25
|
"type": "array",
|
|
8
|
-
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(
|
|
26
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
9
27
|
"items": {
|
|
10
28
|
"type": "object",
|
|
11
29
|
"additionalProperties": true,
|
|
12
30
|
"properties": {
|
|
13
|
-
"
|
|
14
|
-
"type":
|
|
15
|
-
|
|
31
|
+
"master": {
|
|
32
|
+
"type": [
|
|
33
|
+
"string",
|
|
34
|
+
"number"
|
|
35
|
+
],
|
|
36
|
+
"description": "本 endpoint 的框架 master(sandbox client id (distinct from endpoints[].owner));覆盖顶层 master"
|
|
37
|
+
},
|
|
38
|
+
"trusted": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {
|
|
41
|
+
"type": [
|
|
42
|
+
"string",
|
|
43
|
+
"number"
|
|
44
|
+
],
|
|
45
|
+
"description": "Trusted sandbox client id (distinct from endpoints[].owner)"
|
|
46
|
+
},
|
|
47
|
+
"description": "本 endpoint 的 trusted 列表"
|
|
16
48
|
},
|
|
17
49
|
"context": {
|
|
18
50
|
"type": "string",
|
|
@@ -21,10 +53,14 @@
|
|
|
21
53
|
"owner": {
|
|
22
54
|
"type": "string",
|
|
23
55
|
"description": "Sandbox owner user ID"
|
|
56
|
+
},
|
|
57
|
+
"id": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Sandbox bot name"
|
|
24
60
|
}
|
|
25
61
|
},
|
|
26
62
|
"required": [
|
|
27
|
-
"
|
|
63
|
+
"id"
|
|
28
64
|
]
|
|
29
65
|
}
|
|
30
66
|
},
|
package/src/endpoint.ts
CHANGED
|
@@ -2,22 +2,21 @@
|
|
|
2
2
|
* SandboxWsEndpoint — WebSocket lifecycle and MessageGateway bridge for /sandbox.
|
|
3
3
|
*/
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
5
|
+
import type { EndpointInstance, EndpointSendRequest } from '@zhin.js/adapter';
|
|
6
6
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
7
|
import type { HttpHost, WsConnection } from '@zhin.js/host-http';
|
|
8
|
-
import { formatCompact,
|
|
8
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
9
9
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
10
|
import {
|
|
11
11
|
bindSandboxWsSocket,
|
|
12
12
|
formatSandboxOutbound,
|
|
13
13
|
parseSandboxWsPayload,
|
|
14
|
+
sandboxInboundConversation,
|
|
14
15
|
whenWsOpen,
|
|
15
16
|
type ResolvedSandboxBot,
|
|
16
17
|
type SandboxWsSocket,
|
|
17
18
|
} from './protocol.js';
|
|
18
19
|
|
|
19
|
-
const logger = getLogger('sandbox');
|
|
20
|
-
|
|
21
20
|
/**
|
|
22
21
|
* 多 sandbox endpoint 共用同一个 HttpHost 时,同 path 的所有 WS listener
|
|
23
22
|
* 都会被回调(入站重复、出站互窜)。按 endpoint 名隔离挂载路径:
|
|
@@ -58,11 +57,6 @@ function claimSandboxWsPath(
|
|
|
58
57
|
};
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
interface SandboxChannel {
|
|
62
|
-
readonly type: string;
|
|
63
|
-
readonly id: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
60
|
interface SandboxConnection {
|
|
67
61
|
readonly target: string;
|
|
68
62
|
readonly owner: string;
|
|
@@ -70,11 +64,6 @@ interface SandboxConnection {
|
|
|
70
64
|
readonly release: () => void;
|
|
71
65
|
/** true = 占位连接(尚无真实 WS 客户端),send 命中时按 miss 处理。 */
|
|
72
66
|
readonly placeholder?: boolean;
|
|
73
|
-
/**
|
|
74
|
-
* 最近一条入站频道(Console UI 按 type+id 过滤气泡)。
|
|
75
|
-
* 出站必须带回,否则回复石沉大海。
|
|
76
|
-
*/
|
|
77
|
-
lastChannel?: SandboxChannel;
|
|
78
67
|
}
|
|
79
68
|
|
|
80
69
|
export interface SandboxEndpointOptions {
|
|
@@ -89,6 +78,8 @@ export interface SandboxEndpointOptions {
|
|
|
89
78
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
90
79
|
*/
|
|
91
80
|
export class SandboxWsEndpoint implements EndpointInstance {
|
|
81
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
82
|
+
|
|
92
83
|
readonly #options: SandboxEndpointOptions;
|
|
93
84
|
readonly #connections = new Map<string, SandboxConnection>();
|
|
94
85
|
#wsHandleRelease?: () => void;
|
|
@@ -98,19 +89,20 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
98
89
|
#started = false;
|
|
99
90
|
|
|
100
91
|
constructor(options: SandboxEndpointOptions) {
|
|
92
|
+
this.#logger = getAdapterLogger('sandbox', options.defaults.id);
|
|
101
93
|
this.#options = options;
|
|
102
94
|
}
|
|
103
95
|
|
|
104
|
-
/** Live endpoint
|
|
96
|
+
/** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
|
|
105
97
|
get name(): string {
|
|
106
|
-
return this.#options.defaults.
|
|
98
|
+
return this.#options.defaults.id;
|
|
107
99
|
}
|
|
108
100
|
|
|
109
101
|
start(): void {
|
|
110
102
|
if (this.#started) return;
|
|
111
103
|
this.#started = true;
|
|
112
104
|
// 多 endpoint 同 path 会被全部回调(入站重复、出站互窜),按名隔离。
|
|
113
|
-
const claim = claimSandboxWsPath(this.#options.http, this.#options.defaults.
|
|
105
|
+
const claim = claimSandboxWsPath(this.#options.http, this.#options.defaults.id);
|
|
114
106
|
this.#wsPathRelease = claim.release;
|
|
115
107
|
this.#wsPath = claim.path;
|
|
116
108
|
const handle = this.#options.http.ws(claim.path);
|
|
@@ -118,13 +110,9 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
118
110
|
this.#acceptConnection(connection);
|
|
119
111
|
});
|
|
120
112
|
if (!this.#options.defaults.randomNamePerConnection) {
|
|
121
|
-
this.#ensurePlaceholder(this.#options.defaults.
|
|
113
|
+
this.#ensurePlaceholder(this.#options.defaults.id, this.#options.defaults.owner);
|
|
122
114
|
}
|
|
123
|
-
logger.info(
|
|
124
|
-
op: 'sandbox_ws_mounted',
|
|
125
|
-
path: claim.path,
|
|
126
|
-
endpoint: this.#options.defaults.name,
|
|
127
|
-
}));
|
|
115
|
+
this.#logger.info(`ws mounted ${claim.path}`);
|
|
128
116
|
}
|
|
129
117
|
|
|
130
118
|
open(): void {
|
|
@@ -153,38 +141,41 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
153
141
|
}
|
|
154
142
|
this.#connections.clear();
|
|
155
143
|
this.#started = false;
|
|
156
|
-
logger.debug(formatCompact({ op: 'sandbox_stopped' }));
|
|
144
|
+
this.#logger.debug(formatCompact({ op: 'sandbox_stopped' }));
|
|
157
145
|
}
|
|
158
146
|
|
|
159
|
-
send({
|
|
147
|
+
send({ conversation, payload }: EndpointSendRequest): unknown {
|
|
160
148
|
if (!this.#open) return undefined;
|
|
161
|
-
// Reply
|
|
162
|
-
|
|
149
|
+
// Reply targets this endpoint's own live socket (fixed bot name, or the
|
|
150
|
+
// only live random-name connection); conversation carries kind/id stamp.
|
|
151
|
+
const connection = this.#connections.get(this.#options.defaults.id)
|
|
163
152
|
?? this.#findLiveConnection();
|
|
164
153
|
if (!connection) {
|
|
165
|
-
logger.debug(formatCompact({
|
|
154
|
+
this.#logger.debug(formatCompact({
|
|
155
|
+
op: 'sandbox_send_miss',
|
|
156
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
157
|
+
}));
|
|
166
158
|
return undefined;
|
|
167
159
|
}
|
|
168
160
|
if (connection.placeholder) {
|
|
169
|
-
logger.debug(formatCompact({
|
|
161
|
+
this.#logger.debug(formatCompact({
|
|
162
|
+
op: 'sandbox_send_placeholder',
|
|
163
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
164
|
+
}));
|
|
170
165
|
return undefined;
|
|
171
166
|
}
|
|
172
|
-
// Console UI filters by type+id; stamp
|
|
173
|
-
const channel = connection.lastChannel ?? {
|
|
174
|
-
type: 'private',
|
|
175
|
-
id: connection.owner,
|
|
176
|
-
};
|
|
167
|
+
// Console UI filters by type+id; stamp the conversation onto outbound wire.
|
|
177
168
|
connection.socket.send(formatSandboxOutbound(payload, {
|
|
178
|
-
type:
|
|
179
|
-
id:
|
|
180
|
-
bot: this.#options.defaults.
|
|
169
|
+
type: conversation.kind,
|
|
170
|
+
id: conversation.id,
|
|
171
|
+
bot: this.#options.defaults.id,
|
|
181
172
|
endpoint: connection.target,
|
|
182
173
|
}));
|
|
183
|
-
logger.debug(formatCompact({
|
|
174
|
+
this.#logger.debug(formatCompact({
|
|
184
175
|
op: 'sandbox_send',
|
|
185
176
|
target: connection.target,
|
|
186
|
-
channelType:
|
|
187
|
-
channelId:
|
|
177
|
+
channelType: conversation.kind,
|
|
178
|
+
channelId: conversation.id,
|
|
188
179
|
}));
|
|
189
180
|
return payload;
|
|
190
181
|
}
|
|
@@ -200,7 +191,7 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
200
191
|
#acceptConnection(connection: WsConnection): void {
|
|
201
192
|
const target = this.#options.defaults.randomNamePerConnection
|
|
202
193
|
? `sandbox-${randomUUID().slice(0, 8)}`
|
|
203
|
-
: this.#options.defaults.
|
|
194
|
+
: this.#options.defaults.id;
|
|
204
195
|
const owner = this.#options.defaults.owner;
|
|
205
196
|
const socket = connection.socket as SandboxWsSocket;
|
|
206
197
|
// Fixed-name mode reuses `target`; dropping the prior entry without
|
|
@@ -221,39 +212,36 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
221
212
|
const release = bindSandboxWsSocket(socket, {
|
|
222
213
|
onMessage: (raw) => {
|
|
223
214
|
const parsed = parseSandboxWsPayload(raw);
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
logger.debug(formatCompact({
|
|
215
|
+
const sender = parsed.id || owner;
|
|
216
|
+
const conversation = sandboxInboundConversation(String(this.#options.id), {
|
|
217
|
+
type: parsed.type,
|
|
218
|
+
id: sender,
|
|
219
|
+
});
|
|
220
|
+
this.#logger.debug(formatCompact({
|
|
232
221
|
op: 'sandbox_recv',
|
|
233
222
|
target,
|
|
234
|
-
sender
|
|
223
|
+
sender,
|
|
235
224
|
channelType: parsed.type,
|
|
236
|
-
channelId:
|
|
225
|
+
channelId: sender,
|
|
237
226
|
text: parsed.text.slice(0, 80),
|
|
238
227
|
}));
|
|
239
228
|
// Don't gate on #open — inbound must always reach the gateway so
|
|
240
229
|
// Command/AI dispatch and outbound replies work.
|
|
241
230
|
void this.#options.gateway.receive({
|
|
242
|
-
|
|
243
|
-
target,
|
|
231
|
+
conversation,
|
|
244
232
|
content: parsed.text,
|
|
245
|
-
sender:
|
|
233
|
+
sender: { id: sender },
|
|
234
|
+
endpointId: target,
|
|
246
235
|
metadata: Object.freeze({
|
|
247
236
|
type: parsed.type,
|
|
248
237
|
channelType: parsed.type,
|
|
249
238
|
channelId: parsed.id || owner,
|
|
250
|
-
endpoint: target,
|
|
251
239
|
elements: parsed.content,
|
|
252
240
|
timestamp: parsed.timestamp,
|
|
253
241
|
...(parsed.action ? { action: parsed.action } : {}),
|
|
254
242
|
}),
|
|
255
243
|
}).catch((err) => {
|
|
256
|
-
logger.warn(formatCompact({
|
|
244
|
+
this.#logger.warn(formatCompact({
|
|
257
245
|
op: 'sandbox_gateway_receive_failed',
|
|
258
246
|
target,
|
|
259
247
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -266,11 +254,11 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
266
254
|
const current = this.#connections.get(target);
|
|
267
255
|
if (current && current.socket === socket) {
|
|
268
256
|
this.#connections.delete(target);
|
|
269
|
-
logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
|
|
257
|
+
this.#logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
|
|
270
258
|
}
|
|
271
259
|
},
|
|
272
260
|
onError: (err) => {
|
|
273
|
-
logger.warn(formatCompact({
|
|
261
|
+
this.#logger.warn(formatCompact({
|
|
274
262
|
op: 'sandbox_ws_error',
|
|
275
263
|
target,
|
|
276
264
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -278,7 +266,7 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
278
266
|
},
|
|
279
267
|
});
|
|
280
268
|
this.#connections.set(target, { target, owner, socket, release });
|
|
281
|
-
logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
269
|
+
this.#logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
282
270
|
if (!this.#options.defaults.randomNamePerConnection) {
|
|
283
271
|
const readyPayload = JSON.stringify({
|
|
284
272
|
type: 'ready',
|
package/src/index.ts
CHANGED
package/src/protocol.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { isMediaRef } from '@zhin.js/core';
|
|
5
|
+
import type { ConversationKind, ConversationRef } from '@zhin.js/im-contract';
|
|
5
6
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
7
|
|
|
7
8
|
const logger = getLogger('sandbox');
|
|
@@ -33,7 +34,7 @@ export interface SandboxWsSocket {
|
|
|
33
34
|
|
|
34
35
|
export type ResolvedSandboxBot = {
|
|
35
36
|
readonly context: 'sandbox';
|
|
36
|
-
readonly
|
|
37
|
+
readonly id: string;
|
|
37
38
|
readonly owner: string;
|
|
38
39
|
readonly randomNamePerConnection: boolean;
|
|
39
40
|
};
|
|
@@ -41,12 +42,12 @@ export type ResolvedSandboxBot = {
|
|
|
41
42
|
export interface SandboxAdapterConfig {
|
|
42
43
|
/** Runtime expands `endpoints[i]` onto the top level — prefer these. */
|
|
43
44
|
readonly context?: string;
|
|
44
|
-
readonly
|
|
45
|
+
readonly id?: string;
|
|
45
46
|
readonly owner?: string;
|
|
46
47
|
/** Legacy shape: endpoint entries nested under `endpoints[]`. */
|
|
47
48
|
readonly endpoints?: ReadonlyArray<{
|
|
48
49
|
readonly context?: string;
|
|
49
|
-
readonly
|
|
50
|
+
readonly id?: string;
|
|
50
51
|
readonly owner?: string;
|
|
51
52
|
}>;
|
|
52
53
|
}
|
|
@@ -55,19 +56,19 @@ export function resolveSandboxEndpoint(
|
|
|
55
56
|
appConfig: SandboxAdapterConfig,
|
|
56
57
|
): ResolvedSandboxBot {
|
|
57
58
|
const entry = appConfig.endpoints?.find((item) => item.context === 'sandbox');
|
|
58
|
-
const fixedName = typeof appConfig.
|
|
59
|
-
? appConfig.
|
|
60
|
-
: typeof entry?.
|
|
61
|
-
? entry.
|
|
59
|
+
const fixedName = typeof appConfig.id === 'string' && appConfig.id
|
|
60
|
+
? appConfig.id
|
|
61
|
+
: typeof entry?.id === 'string' && entry.id
|
|
62
|
+
? entry.id
|
|
62
63
|
: undefined;
|
|
63
|
-
const
|
|
64
|
+
const id = fixedName || process.env.SANDBOX_BOT_NAME || 'sandbox-bot';
|
|
64
65
|
const owner = (typeof appConfig.owner === 'string' && appConfig.owner)
|
|
65
66
|
|| (typeof entry?.owner === 'string' && entry.owner)
|
|
66
67
|
|| process.env.SANDBOX_BOT_OWNER
|
|
67
68
|
|| 'sandbox-user';
|
|
68
69
|
return {
|
|
69
70
|
context: 'sandbox',
|
|
70
|
-
|
|
71
|
+
id,
|
|
71
72
|
owner,
|
|
72
73
|
randomNamePerConnection: !fixedName,
|
|
73
74
|
};
|
|
@@ -177,6 +178,27 @@ export function parseSandboxWsPayload(raw: string): {
|
|
|
177
178
|
return { type, id, content, timestamp: payload.timestamp ?? Date.now(), text, action };
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
/**
|
|
182
|
+
* 入站归一化 → ConversationRef。sandbox 无平台社交图谱:
|
|
183
|
+
* `private`/`group`/`channel` 直映射;`direct`(私聊)归 'private';
|
|
184
|
+
* `guild`(频道容器语义)归 'channel'。无 guild/temp 容器信息,不产生 parent。
|
|
185
|
+
*/
|
|
186
|
+
export function sandboxInboundConversation(
|
|
187
|
+
endpointKey: string,
|
|
188
|
+
msg: { readonly type: MessageType; readonly id: string },
|
|
189
|
+
): ConversationRef {
|
|
190
|
+
const kind: ConversationKind = msg.type === 'direct'
|
|
191
|
+
? 'private'
|
|
192
|
+
: msg.type === 'guild'
|
|
193
|
+
? 'channel'
|
|
194
|
+
: msg.type;
|
|
195
|
+
return {
|
|
196
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
197
|
+
kind,
|
|
198
|
+
id: msg.id,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
180
202
|
export type SandboxOutboundChannel = {
|
|
181
203
|
readonly type?: string;
|
|
182
204
|
readonly id?: string;
|