@zhin.js/adapter-sandbox 7.0.12 → 7.0.15
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 +40 -0
- package/adapters/sandbox.js +2 -1
- package/adapters/sandbox.ts +2 -1
- package/lib/endpoint.d.ts +18 -2
- package/lib/endpoint.js +86 -12
- package/lib/protocol.d.ts +3 -0
- package/lib/protocol.js +18 -2
- package/lib/run-config.d.ts +10 -0
- package/lib/run-config.js +30 -0
- package/package.json +16 -16
- package/pages/RichTextEditor.js +23 -8
- package/pages/RichTextEditor.tsx +20 -8
- package/pages/SandboxChat.js +315 -63
- package/pages/SandboxChat.tsx +693 -179
- package/pages/agentTrace.js +559 -0
- package/pages/agentTrace.test.js +235 -0
- package/pages/agentTrace.test.ts +265 -0
- package/pages/agentTrace.ts +646 -0
- package/pages/index.js +2 -2
- package/pages/index.tsx +2 -2
- package/pages/playgroundState.js +126 -0
- package/pages/playgroundState.test.js +92 -0
- package/pages/playgroundState.test.ts +105 -0
- package/pages/playgroundState.ts +172 -0
- package/src/endpoint.ts +98 -14
- package/src/protocol.ts +25 -2
- package/src/run-config.ts +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @zhin.js/adapter-process
|
|
2
2
|
|
|
3
|
+
## 7.0.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bcc4c87: Probe Docker readiness asynchronously so Sandbox WebSocket permissions and messages are never blocked by the informational ready frame.
|
|
8
|
+
|
|
9
|
+
## 7.0.14
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 9e609f4: Stabilize Sandbox WebSocket regressions by waiting for the server-side connection registration and verifying the demo-scope gate independently from the informational ready frame.
|
|
14
|
+
|
|
15
|
+
## 7.0.13
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 974772e: Replace the user-facing `Prompt` vocabulary with the `UserInteraction` authoring surface for input, confirmation, and selection. Commands and handlers now expose `interaction`; IM Runtime exposes `createInteraction`; schema-driven endpoint collection is named `SchemaInteraction`. The old prompt-named interaction types and properties are removed rather than aliased. User interactions render through one canonical Markdown and keyboard/list presentation module shared by commands and Agent `ask_user` turns.
|
|
20
|
+
|
|
21
|
+
Extract the transport-neutral interaction contract into `@zhin.js/interaction`. A discriminated `ask()` API supports text, number, confirmation, single-select, multi-select, and typed lists with structured `title`, `description`, and `tip` content. Typed `sequence()` interactions return one result object keyed by step id, render progress, and retry invalid replies without leaking invalid values to callers.
|
|
22
|
+
|
|
23
|
+
Preserve AI Markdown and card command actions through outbound publishing. QQ delivers Markdown with native command buttons; KOOK, Discord, Telegram, DingTalk, and Lark/Feishu now declare and encode their native Markdown dialects while retaining each adapter's interaction policy. Correct QQ callback button action encoding and button style mapping.
|
|
24
|
+
|
|
25
|
+
- 5969c5b: Add SideEventGateway so adapters forward notice/request/system into HandlerIndex. HandlerContext now exposes only generation-safe capabilities and prompt ports; live Endpoint escape hatches are removed.
|
|
26
|
+
- Updated dependencies [5969c5b]
|
|
27
|
+
- Updated dependencies [d336a3f]
|
|
28
|
+
- Updated dependencies [5969c5b]
|
|
29
|
+
- Updated dependencies [5969c5b]
|
|
30
|
+
- Updated dependencies [974772e]
|
|
31
|
+
- Updated dependencies [5969c5b]
|
|
32
|
+
- Updated dependencies [2f786bd]
|
|
33
|
+
- Updated dependencies [1312ca0]
|
|
34
|
+
- Updated dependencies [985fa22]
|
|
35
|
+
- @zhin.js/im-contract@1.0.4
|
|
36
|
+
- @zhin.js/core@1.5.12
|
|
37
|
+
- @zhin.js/adapter@1.1.11
|
|
38
|
+
- @zhin.js/host-http@1.0.11
|
|
39
|
+
- @zhin.js/client@2.1.10
|
|
40
|
+
- zhin.js@6.0.12
|
|
41
|
+
- @zhin.js/page@1.0.12
|
|
42
|
+
|
|
3
43
|
## 7.0.12
|
|
4
44
|
|
|
5
45
|
### Patch Changes
|
package/adapters/sandbox.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Convention entry: discover `adapters/sandbox.ts` → defineAdapter.
|
|
4
4
|
*/
|
|
5
5
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
-
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
6
|
+
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
7
7
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
8
8
|
import { SandboxWsEndpoint } from "../lib/endpoint.js";
|
|
9
9
|
import { resolveSandboxEndpoint, } from "../lib/protocol.js";
|
|
@@ -20,6 +20,7 @@ export default defineAdapter({
|
|
|
20
20
|
return new SandboxWsEndpoint({
|
|
21
21
|
id: context.id,
|
|
22
22
|
gateway: context.use(messageGatewayToken),
|
|
23
|
+
sideEvents: context.use(sideEventGatewayToken),
|
|
23
24
|
http: context.use(httpHostToken),
|
|
24
25
|
defaults: resolveSandboxEndpoint(context.config),
|
|
25
26
|
});
|
package/adapters/sandbox.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Convention entry: discover `adapters/sandbox.ts` → defineAdapter.
|
|
3
3
|
*/
|
|
4
4
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
-
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
5
|
+
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
6
6
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
7
7
|
import { SandboxWsEndpoint } from '../src/endpoint.js';
|
|
8
8
|
import {
|
|
@@ -25,6 +25,7 @@ export default defineAdapter<SandboxAdapterConfig>({
|
|
|
25
25
|
return new SandboxWsEndpoint({
|
|
26
26
|
id: context.id,
|
|
27
27
|
gateway: context.use(messageGatewayToken),
|
|
28
|
+
sideEvents: context.use(sideEventGatewayToken),
|
|
28
29
|
http: context.use(httpHostToken),
|
|
29
30
|
defaults: resolveSandboxEndpoint(context.config),
|
|
30
31
|
});
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import type { EndpointInstance, EndpointSendRequest } from 'zhin.js/adapter';
|
|
2
|
-
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
2
|
+
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
3
3
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
4
|
import type { CapabilityId } from 'zhin.js';
|
|
5
5
|
import { type ResolvedSandboxBot } from './protocol.js';
|
|
6
|
+
type ShellIsolationStatus = Readonly<{
|
|
7
|
+
available: boolean;
|
|
8
|
+
provider: 'docker';
|
|
9
|
+
message: string;
|
|
10
|
+
}>;
|
|
11
|
+
type ShellIsolationProbe = () => Promise<ShellIsolationStatus>;
|
|
12
|
+
/**
|
|
13
|
+
* Share an in-flight readiness probe across reconnects while still allowing a
|
|
14
|
+
* later connection to refresh the informational status after it settles.
|
|
15
|
+
* Kept internal to this module's package surface; tests import it directly.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createSandboxReadinessGate(probe: ShellIsolationProbe): Readonly<{
|
|
18
|
+
afterProbe: (deliver: (status: ShellIsolationStatus) => void) => void;
|
|
19
|
+
}>;
|
|
6
20
|
export interface SandboxEndpointOptions {
|
|
7
21
|
readonly id: CapabilityId;
|
|
8
22
|
readonly gateway: MessageGateway;
|
|
23
|
+
readonly sideEvents?: SideEventGateway;
|
|
9
24
|
readonly http: HttpHost;
|
|
10
25
|
readonly defaults: ResolvedSandboxBot;
|
|
11
26
|
}
|
|
@@ -22,5 +37,6 @@ export declare class SandboxWsEndpoint implements EndpointInstance {
|
|
|
22
37
|
open(): void;
|
|
23
38
|
close(): void;
|
|
24
39
|
stop(): void;
|
|
25
|
-
send({ conversation, payload }: EndpointSendRequest):
|
|
40
|
+
send({ conversation, payload }: EndpointSendRequest): string;
|
|
26
41
|
}
|
|
42
|
+
export {};
|
package/lib/endpoint.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* SandboxWsEndpoint — WebSocket lifecycle and MessageGateway bridge for /sandbox.
|
|
3
3
|
*/
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
5
6
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
6
7
|
import { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, sandboxInboundConversation, whenWsOpen, } from './protocol.js';
|
|
7
8
|
/**
|
|
@@ -38,6 +39,30 @@ function claimSandboxWsPath(http, name) {
|
|
|
38
39
|
},
|
|
39
40
|
};
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Share an in-flight readiness probe across reconnects while still allowing a
|
|
44
|
+
* later connection to refresh the informational status after it settles.
|
|
45
|
+
* Kept internal to this module's package surface; tests import it directly.
|
|
46
|
+
*/
|
|
47
|
+
export function createSandboxReadinessGate(probe) {
|
|
48
|
+
let inFlight;
|
|
49
|
+
const acquire = () => {
|
|
50
|
+
if (inFlight)
|
|
51
|
+
return inFlight;
|
|
52
|
+
const pending = probe();
|
|
53
|
+
const shared = pending.finally(() => {
|
|
54
|
+
if (inFlight === shared)
|
|
55
|
+
inFlight = undefined;
|
|
56
|
+
});
|
|
57
|
+
inFlight = shared;
|
|
58
|
+
return shared;
|
|
59
|
+
};
|
|
60
|
+
return Object.freeze({
|
|
61
|
+
afterProbe: (deliver) => {
|
|
62
|
+
void acquire().then(deliver);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
41
66
|
/**
|
|
42
67
|
* Sandbox 是本地开发/测试面,无平台社交图谱(好友/群/频道),
|
|
43
68
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
@@ -46,6 +71,7 @@ export class SandboxWsEndpoint {
|
|
|
46
71
|
#logger;
|
|
47
72
|
#options;
|
|
48
73
|
#connections = new Map();
|
|
74
|
+
#readiness = createSandboxReadinessGate(probeShellIsolation);
|
|
49
75
|
#wsHandleRelease;
|
|
50
76
|
#wsPathRelease;
|
|
51
77
|
#wsPath = '/sandbox';
|
|
@@ -105,7 +131,7 @@ export class SandboxWsEndpoint {
|
|
|
105
131
|
}
|
|
106
132
|
send({ conversation, payload }) {
|
|
107
133
|
if (!this.#open)
|
|
108
|
-
|
|
134
|
+
throw new Error('Sandbox Endpoint is not open');
|
|
109
135
|
// Reply targets this endpoint's own live socket (fixed bot name, or the
|
|
110
136
|
// only live random-name connection); conversation carries kind/id stamp.
|
|
111
137
|
const connection = this.#connections.get(this.#options.defaults.id)
|
|
@@ -115,14 +141,14 @@ export class SandboxWsEndpoint {
|
|
|
115
141
|
op: 'sandbox_send_miss',
|
|
116
142
|
target: `${conversation.kind}:${conversation.id}`,
|
|
117
143
|
}));
|
|
118
|
-
|
|
144
|
+
throw new Error('Sandbox Endpoint has no live connection');
|
|
119
145
|
}
|
|
120
146
|
if (connection.placeholder) {
|
|
121
147
|
this.#logger.debug(formatCompact({
|
|
122
148
|
op: 'sandbox_send_placeholder',
|
|
123
149
|
target: `${conversation.kind}:${conversation.id}`,
|
|
124
150
|
}));
|
|
125
|
-
|
|
151
|
+
throw new Error('Sandbox Endpoint has no live connection');
|
|
126
152
|
}
|
|
127
153
|
// Console UI filters by type+id; stamp the conversation onto outbound wire.
|
|
128
154
|
connection.socket.send(formatSandboxOutbound(payload, {
|
|
@@ -137,7 +163,7 @@ export class SandboxWsEndpoint {
|
|
|
137
163
|
channelType: conversation.kind,
|
|
138
164
|
channelId: conversation.id,
|
|
139
165
|
}));
|
|
140
|
-
return
|
|
166
|
+
return randomUUID();
|
|
141
167
|
}
|
|
142
168
|
/** Prefer a real (non-placeholder) socket when reply target key is wrong/stale. */
|
|
143
169
|
#findLiveConnection() {
|
|
@@ -152,6 +178,7 @@ export class SandboxWsEndpoint {
|
|
|
152
178
|
? `sandbox-${randomUUID().slice(0, 8)}`
|
|
153
179
|
: this.#options.defaults.id;
|
|
154
180
|
const owner = this.#options.defaults.owner;
|
|
181
|
+
const canExecute = connection.authScope === 'full';
|
|
155
182
|
const socket = connection.socket;
|
|
156
183
|
// Fixed-name mode reuses `target`; dropping the prior entry without
|
|
157
184
|
// closing its socket leaves a zombie browser tab that still looks
|
|
@@ -171,26 +198,37 @@ export class SandboxWsEndpoint {
|
|
|
171
198
|
}
|
|
172
199
|
const release = bindSandboxWsSocket(socket, {
|
|
173
200
|
onMessage: (raw) => {
|
|
201
|
+
if (!canExecute) {
|
|
202
|
+
socket.send(JSON.stringify({
|
|
203
|
+
type: 'error',
|
|
204
|
+
id: owner,
|
|
205
|
+
endpoint: target,
|
|
206
|
+
content: [{ type: 'text', data: { text: '当前连接是只读演示权限,不能运行 Agent 任务。' } }],
|
|
207
|
+
timestamp: Date.now(),
|
|
208
|
+
}));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
174
211
|
const parsed = parseSandboxWsPayload(raw);
|
|
175
|
-
const
|
|
212
|
+
const sceneId = parsed.id || owner;
|
|
176
213
|
const conversation = sandboxInboundConversation(String(this.#options.id), {
|
|
177
214
|
type: parsed.type,
|
|
178
|
-
id:
|
|
215
|
+
id: sceneId,
|
|
179
216
|
});
|
|
180
217
|
this.#logger.debug(formatCompact({
|
|
181
218
|
op: 'sandbox_recv',
|
|
182
219
|
target,
|
|
183
|
-
sender,
|
|
220
|
+
sender: owner,
|
|
184
221
|
channelType: parsed.type,
|
|
185
|
-
channelId:
|
|
222
|
+
channelId: sceneId,
|
|
186
223
|
text: parsed.text.slice(0, 80),
|
|
187
224
|
}));
|
|
188
225
|
// Don't gate on #open — inbound must always reach the gateway so
|
|
189
226
|
// Command/AI dispatch and outbound replies work.
|
|
190
227
|
void this.#options.gateway.receive({
|
|
191
228
|
conversation,
|
|
229
|
+
...(parsed.messageId ? { message: { conversation, id: parsed.messageId } } : {}),
|
|
192
230
|
content: parsed.text,
|
|
193
|
-
sender: { id:
|
|
231
|
+
sender: { id: owner },
|
|
194
232
|
endpointId: target,
|
|
195
233
|
metadata: Object.freeze({
|
|
196
234
|
type: parsed.type,
|
|
@@ -199,6 +237,7 @@ export class SandboxWsEndpoint {
|
|
|
199
237
|
elements: parsed.content,
|
|
200
238
|
timestamp: parsed.timestamp,
|
|
201
239
|
...(parsed.action ? { action: parsed.action } : {}),
|
|
240
|
+
...(parsed.agentRun ? { sandboxAgentRun: parsed.agentRun } : {}),
|
|
202
241
|
}),
|
|
203
242
|
}).catch((err) => {
|
|
204
243
|
this.#logger.warn(formatCompact({
|
|
@@ -227,25 +266,31 @@ export class SandboxWsEndpoint {
|
|
|
227
266
|
});
|
|
228
267
|
this.#connections.set(target, { target, owner, socket, release });
|
|
229
268
|
this.#logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
230
|
-
|
|
269
|
+
this.#readiness.afterProbe((shellIsolation) => {
|
|
270
|
+
const current = this.#connections.get(target);
|
|
271
|
+
if (!current || current.socket !== socket)
|
|
272
|
+
return;
|
|
231
273
|
const readyPayload = JSON.stringify({
|
|
232
274
|
type: 'ready',
|
|
233
275
|
id: owner,
|
|
234
276
|
endpoint: target,
|
|
277
|
+
workingDirectory: process.cwd(),
|
|
278
|
+
canExecute,
|
|
279
|
+
shellIsolation,
|
|
235
280
|
content: [{
|
|
236
281
|
type: 'text',
|
|
237
282
|
data: {
|
|
238
283
|
text: [
|
|
239
284
|
`已连接 Sandbox「${target}」`,
|
|
240
285
|
`与 Node Host 控制台沙盒协议一致(${this.#wsPath})`,
|
|
241
|
-
'
|
|
286
|
+
'Agent 试验台会话已启用持久化运行配置。',
|
|
242
287
|
].join('\n'),
|
|
243
288
|
},
|
|
244
289
|
}],
|
|
245
290
|
timestamp: Date.now(),
|
|
246
291
|
});
|
|
247
292
|
whenWsOpen(socket, () => socket.send(readyPayload));
|
|
248
|
-
}
|
|
293
|
+
});
|
|
249
294
|
}
|
|
250
295
|
#ensurePlaceholder(name, owner) {
|
|
251
296
|
if (this.#connections.has(name))
|
|
@@ -259,3 +304,32 @@ export class SandboxWsEndpoint {
|
|
|
259
304
|
});
|
|
260
305
|
}
|
|
261
306
|
}
|
|
307
|
+
function probeShellIsolation() {
|
|
308
|
+
return new Promise((resolve) => {
|
|
309
|
+
execFile('docker', ['info', '--format', '{{.ServerVersion}}'], {
|
|
310
|
+
encoding: 'utf8',
|
|
311
|
+
timeout: 500,
|
|
312
|
+
}, (error, stdout) => {
|
|
313
|
+
if (!error) {
|
|
314
|
+
const version = stdout.trim();
|
|
315
|
+
resolve(Object.freeze({
|
|
316
|
+
available: true,
|
|
317
|
+
provider: 'docker',
|
|
318
|
+
message: version ? `Docker ${version}` : 'Docker ready',
|
|
319
|
+
}));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
resolve(Object.freeze({
|
|
323
|
+
available: false,
|
|
324
|
+
provider: 'docker',
|
|
325
|
+
message: error.killed || error.code === 'ETIMEDOUT'
|
|
326
|
+
? 'Docker readiness check timed out'
|
|
327
|
+
: 'Docker daemon is unavailable',
|
|
328
|
+
}));
|
|
329
|
+
});
|
|
330
|
+
}).catch(() => Object.freeze({
|
|
331
|
+
available: false,
|
|
332
|
+
provider: 'docker',
|
|
333
|
+
message: 'Docker is unavailable',
|
|
334
|
+
}));
|
|
335
|
+
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Sandbox WebSocket wire protocol helpers (no legacy Adapter/Endpoint). */
|
|
2
2
|
import type { ConversationRef } from '@zhin.js/im-contract';
|
|
3
|
+
import { type SandboxAgentRunConfig } from './run-config.js';
|
|
3
4
|
export type MessageType = 'private' | 'group' | 'guild' | 'direct' | 'channel';
|
|
4
5
|
export interface MessageElement {
|
|
5
6
|
readonly type: string;
|
|
@@ -40,6 +41,7 @@ export declare function bindSandboxWsSocket(ws: SandboxWsSocket, handlers: {
|
|
|
40
41
|
export declare function parseSandboxWsPayload(raw: string): {
|
|
41
42
|
type: MessageType;
|
|
42
43
|
id: string;
|
|
44
|
+
messageId?: string;
|
|
43
45
|
content: MessageElement[];
|
|
44
46
|
timestamp: number;
|
|
45
47
|
text: string;
|
|
@@ -47,6 +49,7 @@ export declare function parseSandboxWsPayload(raw: string): {
|
|
|
47
49
|
id: string;
|
|
48
50
|
payload: string;
|
|
49
51
|
};
|
|
52
|
+
agentRun?: SandboxAgentRunConfig;
|
|
50
53
|
};
|
|
51
54
|
/**
|
|
52
55
|
* 入站归一化 → ConversationRef。sandbox 无平台社交图谱:
|
package/lib/protocol.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { isMediaRef } from '@zhin.js/core';
|
|
4
4
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
5
|
+
import { normalizeSandboxAgentRunConfig, } from './run-config.js';
|
|
5
6
|
const logger = getLogger('sandbox');
|
|
6
7
|
export function resolveSandboxEndpoint(appConfig) {
|
|
7
8
|
const entry = appConfig.endpoints?.find((item) => item.context === 'sandbox');
|
|
@@ -19,7 +20,10 @@ export function resolveSandboxEndpoint(appConfig) {
|
|
|
19
20
|
context: 'sandbox',
|
|
20
21
|
id,
|
|
21
22
|
owner,
|
|
22
|
-
|
|
23
|
+
// The endpoint id participates in the Agent session key. Keep it stable
|
|
24
|
+
// across browser reconnects and Host restarts so a persisted playground
|
|
25
|
+
// session resumes the same Agent context.
|
|
26
|
+
randomNamePerConnection: false,
|
|
23
27
|
};
|
|
24
28
|
}
|
|
25
29
|
export function bindSandboxWsSocket(ws, handlers) {
|
|
@@ -105,7 +109,19 @@ export function parseSandboxWsPayload(raw) {
|
|
|
105
109
|
? payload.text
|
|
106
110
|
: action?.payload ?? raw;
|
|
107
111
|
}
|
|
108
|
-
|
|
112
|
+
const agentRun = normalizeSandboxAgentRunConfig(payload.agentRun);
|
|
113
|
+
const rawMessageId = typeof payload.messageId === 'string' ? payload.messageId.trim() : '';
|
|
114
|
+
const messageId = /^[A-Za-z0-9._:-]{1,160}$/u.test(rawMessageId) ? rawMessageId : undefined;
|
|
115
|
+
return {
|
|
116
|
+
type,
|
|
117
|
+
id,
|
|
118
|
+
...(messageId ? { messageId } : {}),
|
|
119
|
+
content,
|
|
120
|
+
timestamp: payload.timestamp ?? Date.now(),
|
|
121
|
+
text,
|
|
122
|
+
action,
|
|
123
|
+
...(agentRun ? { agentRun } : {}),
|
|
124
|
+
};
|
|
109
125
|
}
|
|
110
126
|
/**
|
|
111
127
|
* 入站归一化 → ConversationRef。sandbox 无平台社交图谱:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SandboxSafetyMode = 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
2
|
+
export type SandboxApprovalMode = 'ask' | 'deny' | 'allow';
|
|
3
|
+
export interface SandboxAgentRunConfig {
|
|
4
|
+
readonly workingDirectory: string;
|
|
5
|
+
readonly safetyMode: SandboxSafetyMode;
|
|
6
|
+
readonly approvalMode: SandboxApprovalMode;
|
|
7
|
+
readonly networkAccess: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_SANDBOX_AGENT_RUN_CONFIG: SandboxAgentRunConfig;
|
|
10
|
+
export declare function normalizeSandboxAgentRunConfig(value: unknown): SandboxAgentRunConfig | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const DEFAULT_SANDBOX_AGENT_RUN_CONFIG = Object.freeze({
|
|
2
|
+
workingDirectory: '',
|
|
3
|
+
safetyMode: 'workspace-write',
|
|
4
|
+
approvalMode: 'ask',
|
|
5
|
+
networkAccess: false,
|
|
6
|
+
});
|
|
7
|
+
const SAFETY_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
|
|
8
|
+
const APPROVAL_MODES = new Set(['ask', 'deny', 'allow']);
|
|
9
|
+
export function normalizeSandboxAgentRunConfig(value) {
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
+
return undefined;
|
|
12
|
+
const input = value;
|
|
13
|
+
const workingDirectory = typeof input.workingDirectory === 'string'
|
|
14
|
+
? input.workingDirectory.trim().slice(0, 4096)
|
|
15
|
+
: '';
|
|
16
|
+
const safetyMode = SAFETY_MODES.has(input.safetyMode)
|
|
17
|
+
? input.safetyMode
|
|
18
|
+
: DEFAULT_SANDBOX_AGENT_RUN_CONFIG.safetyMode;
|
|
19
|
+
const approvalMode = APPROVAL_MODES.has(input.approvalMode)
|
|
20
|
+
? input.approvalMode
|
|
21
|
+
: DEFAULT_SANDBOX_AGENT_RUN_CONFIG.approvalMode;
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
workingDirectory,
|
|
24
|
+
safetyMode,
|
|
25
|
+
approvalMode,
|
|
26
|
+
// Full host access cannot be combined with a portable network namespace.
|
|
27
|
+
// Keep the contract honest: danger mode includes network authority.
|
|
28
|
+
networkAccess: safetyMode === 'danger-full-access' || input.networkAccess === true,
|
|
29
|
+
});
|
|
30
|
+
}
|
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.15",
|
|
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,14 @@
|
|
|
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.
|
|
51
|
+
"@zhin.js/adapter": "1.1.11",
|
|
52
|
+
"@zhin.js/client": "2.1.10",
|
|
53
53
|
"@zhin.js/console-contract": "1.0.1",
|
|
54
|
-
"@zhin.js/core": "1.5.
|
|
55
|
-
"@zhin.js/host-http": "1.0.
|
|
56
|
-
"@zhin.js/im-contract": "1.0.
|
|
54
|
+
"@zhin.js/core": "1.5.12",
|
|
55
|
+
"@zhin.js/host-http": "1.0.11",
|
|
56
|
+
"@zhin.js/im-contract": "1.0.4",
|
|
57
57
|
"@zhin.js/logger": "1.0.76",
|
|
58
|
-
"@zhin.js/page": "1.0.
|
|
58
|
+
"@zhin.js/page": "1.0.12"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/react": "^19.2.18",
|
|
@@ -63,19 +63,19 @@
|
|
|
63
63
|
"typescript": "^6.0.3",
|
|
64
64
|
"vitest": "^4.1.10",
|
|
65
65
|
"ws": "^8.21.1",
|
|
66
|
-
"@zhin.js/pagemanager": "2.0.
|
|
67
|
-
"@zhin.js/runtime": "1.0.
|
|
68
|
-
"zhin.js": "6.0.
|
|
66
|
+
"@zhin.js/pagemanager": "2.0.19",
|
|
67
|
+
"@zhin.js/runtime": "1.0.13",
|
|
68
|
+
"zhin.js": "6.0.12"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"react": "^19.0.0",
|
|
72
|
-
"@zhin.js/adapter": "1.1.
|
|
73
|
-
"@zhin.js/client": "2.1.
|
|
72
|
+
"@zhin.js/adapter": "1.1.11",
|
|
73
|
+
"@zhin.js/client": "2.1.10",
|
|
74
74
|
"@zhin.js/console-contract": "1.0.1",
|
|
75
|
-
"@zhin.js/core": "1.5.
|
|
76
|
-
"@zhin.js/host-http": "1.0.
|
|
77
|
-
"@zhin.js/page": "1.0.
|
|
78
|
-
"zhin.js": "6.0.
|
|
75
|
+
"@zhin.js/core": "1.5.12",
|
|
76
|
+
"@zhin.js/host-http": "1.0.11",
|
|
77
|
+
"@zhin.js/page": "1.0.12",
|
|
78
|
+
"zhin.js": "6.0.12"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"react": {
|
package/pages/RichTextEditor.js
CHANGED
|
@@ -10,14 +10,22 @@ const RichTextEditor = forwardRef(({ placeholder = '输入消息...', onSend, on
|
|
|
10
10
|
return { text: '', segments: [] };
|
|
11
11
|
let text = '';
|
|
12
12
|
const segments = [];
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const appendText = (value) => {
|
|
14
|
+
if (!value)
|
|
15
|
+
return;
|
|
16
|
+
text += value;
|
|
17
|
+
const previous = segments.at(-1);
|
|
18
|
+
if (previous?.type === 'text') {
|
|
19
|
+
previous.data.text = String(previous.data.text ?? '') + value;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
segments.push({ type: 'text', data: { text: value } });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const visit = (node) => {
|
|
15
26
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
16
27
|
const textContent = node.textContent || '';
|
|
17
|
-
|
|
18
|
-
text += textContent;
|
|
19
|
-
segments.push({ type: 'text', data: { text: textContent } });
|
|
20
|
-
}
|
|
28
|
+
appendText(textContent);
|
|
21
29
|
}
|
|
22
30
|
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
23
31
|
const el = node;
|
|
@@ -51,10 +59,17 @@ const RichTextEditor = forwardRef(({ placeholder = '输入消息...', onSend, on
|
|
|
51
59
|
});
|
|
52
60
|
}
|
|
53
61
|
else if (el.tagName === 'BR') {
|
|
54
|
-
|
|
62
|
+
appendText('\n');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const isBlock = el.tagName === 'DIV' || el.tagName === 'P';
|
|
66
|
+
if (isBlock && text && !text.endsWith('\n'))
|
|
67
|
+
appendText('\n');
|
|
68
|
+
Array.from(el.childNodes).forEach(visit);
|
|
55
69
|
}
|
|
56
70
|
}
|
|
57
|
-
}
|
|
71
|
+
};
|
|
72
|
+
Array.from(editorRef.current.childNodes).forEach(visit);
|
|
58
73
|
return { text, segments };
|
|
59
74
|
};
|
|
60
75
|
// 插入表情
|
package/pages/RichTextEditor.tsx
CHANGED
|
@@ -37,15 +37,22 @@ const RichTextEditor = forwardRef<RichTextEditorRef, RichTextEditorProps>(
|
|
|
37
37
|
|
|
38
38
|
let text = ''
|
|
39
39
|
const segments: MessageSegment[] = []
|
|
40
|
-
const nodes = Array.from(editorRef.current.childNodes)
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
const appendText = (value: string) => {
|
|
42
|
+
if (!value) return
|
|
43
|
+
text += value
|
|
44
|
+
const previous = segments.at(-1)
|
|
45
|
+
if (previous?.type === 'text') {
|
|
46
|
+
previous.data.text = String(previous.data.text ?? '') + value
|
|
47
|
+
} else {
|
|
48
|
+
segments.push({ type: 'text', data: { text: value } })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const visit = (node: Node) => {
|
|
43
53
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
44
54
|
const textContent = node.textContent || ''
|
|
45
|
-
|
|
46
|
-
text += textContent
|
|
47
|
-
segments.push({ type: 'text', data: { text: textContent } })
|
|
48
|
-
}
|
|
55
|
+
appendText(textContent)
|
|
49
56
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
50
57
|
const el = node as HTMLElement
|
|
51
58
|
|
|
@@ -74,11 +81,17 @@ const RichTextEditor = forwardRef<RichTextEditorRef, RichTextEditorProps>(
|
|
|
74
81
|
data: id ? { target: id, name } : { target: name, name },
|
|
75
82
|
})
|
|
76
83
|
} else if (el.tagName === 'BR') {
|
|
77
|
-
|
|
84
|
+
appendText('\n')
|
|
85
|
+
} else {
|
|
86
|
+
const isBlock = el.tagName === 'DIV' || el.tagName === 'P'
|
|
87
|
+
if (isBlock && text && !text.endsWith('\n')) appendText('\n')
|
|
88
|
+
Array.from(el.childNodes).forEach(visit)
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
|
|
93
|
+
Array.from(editorRef.current.childNodes).forEach(visit)
|
|
94
|
+
|
|
82
95
|
return { text, segments }
|
|
83
96
|
}
|
|
84
97
|
|
|
@@ -426,4 +439,3 @@ const RichTextEditor = forwardRef<RichTextEditorRef, RichTextEditorProps>(
|
|
|
426
439
|
RichTextEditor.displayName = 'RichTextEditor'
|
|
427
440
|
|
|
428
441
|
export default RichTextEditor
|
|
429
|
-
|