@zhin.js/adapter-sandbox 8.0.0 → 8.0.2
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 +31 -0
- package/README.md +14 -1
- package/adapters/sandbox.js +0 -3
- package/adapters/sandbox.ts +0 -3
- package/lib/client.d.ts +27 -0
- package/lib/client.js +27 -0
- package/lib/endpoint.d.ts +5 -5
- package/lib/endpoint.js +34 -12
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +17 -16
- package/src/client.ts +47 -0
- package/src/endpoint.ts +46 -11
- package/src/index.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @zhin.js/adapter-process
|
|
2
2
|
|
|
3
|
+
## 8.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [a5ee497]
|
|
8
|
+
- Updated dependencies [ba7e17a]
|
|
9
|
+
- Updated dependencies [7108d0b]
|
|
10
|
+
- @zhin.js/host-http@1.0.14
|
|
11
|
+
- @zhin.js/core@1.5.15
|
|
12
|
+
- zhin.js@6.0.15
|
|
13
|
+
|
|
14
|
+
## 8.0.1
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- e9c6a73: Preserve WebSocket path ownership when a previous hot-reload generation stops after its replacement starts.
|
|
19
|
+
- 1fc78bc: Unify native platform Client access behind the literal `adapter` discriminant. Handlers infer both native events and Clients, while command, inbound/outbound middleware, and both Agent tool authoring surfaces expose the exact operation-scoped Client through a lazy `$client` getter. Definitions without `adapter` keep `$client` typed as `unknown`, and runtime dispatch rejects adapter mismatches before resolving the Client. Bundled platform tools now use this single path instead of model-provided endpoint ids and adapter-specific dependency wrappers. Every adapter registers one Client/EventMap contract, and protocol adapters including NapCat, Milky, OneBot and Satori now produce transport-independent Client objects rather than letting Endpoint instances impersonate Clients.
|
|
20
|
+
- Updated dependencies [4e8117c]
|
|
21
|
+
- Updated dependencies [54bfd6b]
|
|
22
|
+
- Updated dependencies [12025ee]
|
|
23
|
+
- Updated dependencies [09b14d6]
|
|
24
|
+
- Updated dependencies [1fc78bc]
|
|
25
|
+
- @zhin.js/adapter@1.2.1
|
|
26
|
+
- @zhin.js/core@1.5.14
|
|
27
|
+
- @zhin.js/host-http@1.0.13
|
|
28
|
+
- @zhin.js/logger@1.0.77
|
|
29
|
+
- zhin.js@6.0.14
|
|
30
|
+
- @zhin.js/feature-kit@1.0.13
|
|
31
|
+
- @zhin.js/page@1.0.13
|
|
32
|
+
- @zhin.js/client@2.1.12
|
|
33
|
+
|
|
3
34
|
## 8.0.0
|
|
4
35
|
|
|
5
36
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -16,13 +16,17 @@ Zhin.js Sandbox 适配器,基于 WebSocket 的本地测试适配器;浏览
|
|
|
16
16
|
pnpm add @zhin.js/adapter-sandbox
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## 前置条件
|
|
20
|
+
|
|
21
|
+
Sandbox 不需要外部账号。只需由 `zhin runtime start` 装配 HTTP Host,并确保浏览器能访问启动日志中的 Host 地址。
|
|
22
|
+
|
|
19
23
|
## 依赖
|
|
20
24
|
|
|
21
25
|
### Plugin Runtime(新,`zhin runtime start`)
|
|
22
26
|
|
|
23
27
|
- `@zhin.js/adapter` — 约定式 `adapters/sandbox.ts`
|
|
24
28
|
- `@zhin.js/host-http` — Root 提供的 `httpHostToken`(WebSocket `/sandbox` + Console HTTP)
|
|
25
|
-
- `@zhin.js/core` — `
|
|
29
|
+
- `@zhin.js/core` — `Endpoint.emit(...)` 入站、`outboundMessageToken` 出站
|
|
26
30
|
- `@zhin.js/page` + `pages/index.tsx` — ADR 0046 约定页(`definePage`;路由 `/sandbox`)
|
|
27
31
|
|
|
28
32
|
Root 在 `zhin runtime start` 时装载 `@zhin.js/host-http`、`ConsoleRuntime` 与
|
|
@@ -96,6 +100,15 @@ Sandbox 使用 JSON 消息格式:
|
|
|
96
100
|
技能说明见 `agent/skills/sandbox.md`(本地沙箱调试约束)。
|
|
97
101
|
|
|
98
102
|
|
|
103
|
+
## 故障排查
|
|
104
|
+
|
|
105
|
+
| 现象 | 排查 |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| Console 无法连接 | 以启动日志中的 Host、端口和 token 为准 |
|
|
108
|
+
| Sandbox 页面空白 | 检查 HTTP Host 是否因端口占用软降级,以及鉴权/CORS 错误 |
|
|
109
|
+
| 刷新后没有历史消息 | 核对 Endpoint 与频道,并检查 history RPC 与 recovery gap 日志 |
|
|
110
|
+
| 命令或工具未生效 | 在运行时能力页确认它已进入当前 generation |
|
|
111
|
+
|
|
99
112
|
## 许可证
|
|
100
113
|
|
|
101
114
|
MIT License
|
package/adapters/sandbox.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Convention entry: discover `adapters/sandbox.ts` → defineAdapter.
|
|
4
4
|
*/
|
|
5
5
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
-
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
7
6
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
8
7
|
import { SandboxWsEndpoint } from "../lib/endpoint.js";
|
|
9
8
|
import { resolveSandboxEndpoint, } from "../lib/protocol.js";
|
|
@@ -19,8 +18,6 @@ export default defineAdapter({
|
|
|
19
18
|
create(context) {
|
|
20
19
|
return new SandboxWsEndpoint({
|
|
21
20
|
id: context.id,
|
|
22
|
-
gateway: context.use(messageGatewayToken),
|
|
23
|
-
sideEvents: context.use(sideEventGatewayToken),
|
|
24
21
|
http: context.use(httpHostToken),
|
|
25
22
|
defaults: resolveSandboxEndpoint(context.config),
|
|
26
23
|
});
|
package/adapters/sandbox.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Convention entry: discover `adapters/sandbox.ts` → defineAdapter.
|
|
3
3
|
*/
|
|
4
4
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
-
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
6
5
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
7
6
|
import { SandboxWsEndpoint } from '../src/endpoint.js';
|
|
8
7
|
import {
|
|
@@ -24,8 +23,6 @@ export default defineAdapter<SandboxAdapterConfig>({
|
|
|
24
23
|
create(context) {
|
|
25
24
|
return new SandboxWsEndpoint({
|
|
26
25
|
id: context.id,
|
|
27
|
-
gateway: context.use(messageGatewayToken),
|
|
28
|
-
sideEvents: context.use(sideEventGatewayToken),
|
|
29
26
|
http: context.use(httpHostToken),
|
|
30
27
|
defaults: resolveSandboxEndpoint(context.config),
|
|
31
28
|
});
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SandboxWsSocket } from './protocol.js';
|
|
2
|
+
export interface SandboxClientConnection {
|
|
3
|
+
readonly target: string;
|
|
4
|
+
readonly owner: string;
|
|
5
|
+
readonly socket: SandboxWsSocket;
|
|
6
|
+
readonly placeholder: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Direct view of the live Sandbox protocol clients owned by one Endpoint. */
|
|
9
|
+
export declare class SandboxClient {
|
|
10
|
+
private readonly resolvePath;
|
|
11
|
+
private readonly resolveConnections;
|
|
12
|
+
constructor(resolvePath: () => string, resolveConnections: () => Iterable<SandboxClientConnection>);
|
|
13
|
+
get path(): string;
|
|
14
|
+
connections(): readonly SandboxClientConnection[];
|
|
15
|
+
connection(target: string): SandboxClientConnection | undefined;
|
|
16
|
+
send(target: string, payload: string): void;
|
|
17
|
+
}
|
|
18
|
+
export type SandboxClientEventMap = Record<string, unknown>;
|
|
19
|
+
declare module '@zhin.js/feature-kit' {
|
|
20
|
+
interface AdapterClientRegistry {
|
|
21
|
+
readonly sandbox: {
|
|
22
|
+
readonly client: SandboxClient;
|
|
23
|
+
readonly events: SandboxClientEventMap;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export declare const sandboxClient: import("@zhin.js/adapter").EndpointClientToken<SandboxClient, SandboxClientEventMap>;
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineEndpointClient } from 'zhin.js/adapter';
|
|
2
|
+
/** Direct view of the live Sandbox protocol clients owned by one Endpoint. */
|
|
3
|
+
export class SandboxClient {
|
|
4
|
+
resolvePath;
|
|
5
|
+
resolveConnections;
|
|
6
|
+
constructor(resolvePath, resolveConnections) {
|
|
7
|
+
this.resolvePath = resolvePath;
|
|
8
|
+
this.resolveConnections = resolveConnections;
|
|
9
|
+
}
|
|
10
|
+
get path() {
|
|
11
|
+
return this.resolvePath();
|
|
12
|
+
}
|
|
13
|
+
connections() {
|
|
14
|
+
return Object.freeze([...this.resolveConnections()]);
|
|
15
|
+
}
|
|
16
|
+
connection(target) {
|
|
17
|
+
return this.connections().find((connection) => connection.target === target);
|
|
18
|
+
}
|
|
19
|
+
send(target, payload) {
|
|
20
|
+
const connection = this.connection(target);
|
|
21
|
+
if (!connection || connection.placeholder) {
|
|
22
|
+
throw new Error(`Sandbox client ${target} is not connected`);
|
|
23
|
+
}
|
|
24
|
+
connection.socket.send(payload);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const sandboxClient = defineEndpointClient('sandbox');
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
2
|
+
import { type EndpointSendRequest } from 'zhin.js/adapter';
|
|
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
|
+
import { SandboxClient } from './client.js';
|
|
6
7
|
type ShellIsolationStatus = Readonly<{
|
|
7
8
|
available: boolean;
|
|
8
9
|
provider: 'docker';
|
|
@@ -19,8 +20,6 @@ export declare function createSandboxReadinessGate(probe: ShellIsolationProbe):
|
|
|
19
20
|
}>;
|
|
20
21
|
export interface SandboxEndpointOptions {
|
|
21
22
|
readonly id: CapabilityId;
|
|
22
|
-
readonly gateway: MessageGateway;
|
|
23
|
-
readonly sideEvents?: SideEventGateway;
|
|
24
23
|
readonly http: HttpHost;
|
|
25
24
|
readonly defaults: ResolvedSandboxBot;
|
|
26
25
|
}
|
|
@@ -28,8 +27,9 @@ export interface SandboxEndpointOptions {
|
|
|
28
27
|
* Sandbox 是本地开发/测试面,无平台社交图谱(好友/群/频道),
|
|
29
28
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
30
29
|
*/
|
|
31
|
-
export declare class SandboxWsEndpoint
|
|
30
|
+
export declare class SandboxWsEndpoint extends Endpoint<SandboxClient> {
|
|
32
31
|
#private;
|
|
32
|
+
readonly client: SandboxClient;
|
|
33
33
|
constructor(options: SandboxEndpointOptions);
|
|
34
34
|
/** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
|
|
35
35
|
get name(): string;
|
package/lib/endpoint.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
1
2
|
/**
|
|
2
|
-
* SandboxWsEndpoint — WebSocket lifecycle and
|
|
3
|
+
* SandboxWsEndpoint — WebSocket lifecycle and OutboundMessageService bridge for /sandbox.
|
|
3
4
|
*/
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { execFile } from 'node:child_process';
|
|
6
7
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
7
8
|
import { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, sandboxInboundConversation, whenWsOpen, } from './protocol.js';
|
|
8
|
-
|
|
9
|
-
* 多 sandbox endpoint 共用同一个 HttpHost 时,同 path 的所有 WS listener
|
|
10
|
-
* 都会被回调(入站重复、出站互窜)。按 endpoint 名隔离挂载路径:
|
|
11
|
-
* 首个占用 `/sandbox`(保持 Console 默认兼容),其余退到 `/sandbox/<name>`。
|
|
12
|
-
* 认领记录按 HttpHost 隔离,endpoint stop() 时必须 release。
|
|
13
|
-
*/
|
|
9
|
+
import { SandboxClient } from './client.js';
|
|
14
10
|
const claimedWsPaths = new WeakMap();
|
|
15
11
|
function claimSandboxWsPath(http, name) {
|
|
16
12
|
let claims = claimedWsPaths.get(http);
|
|
@@ -19,7 +15,7 @@ function claimSandboxWsPath(http, name) {
|
|
|
19
15
|
claimedWsPaths.set(http, claims);
|
|
20
16
|
}
|
|
21
17
|
const candidates = ['/sandbox', `/sandbox/${encodeURIComponent(name)}`];
|
|
22
|
-
let path = candidates.find((candidate) => !claims.has(candidate) || claims.get(candidate) === name);
|
|
18
|
+
let path = candidates.find((candidate) => !claims.has(candidate) || claims.get(candidate)?.name === name);
|
|
23
19
|
if (!path) {
|
|
24
20
|
let index = 2;
|
|
25
21
|
path = `/sandbox/${encodeURIComponent(name)}-${index}`;
|
|
@@ -28,14 +24,19 @@ function claimSandboxWsPath(http, name) {
|
|
|
28
24
|
path = `/sandbox/${encodeURIComponent(name)}-${index}`;
|
|
29
25
|
}
|
|
30
26
|
}
|
|
31
|
-
|
|
27
|
+
const owner = Symbol(name);
|
|
28
|
+
const claim = claims.get(path) ?? { name, owners: new Set() };
|
|
29
|
+
claim.owners.add(owner);
|
|
30
|
+
claims.set(path, claim);
|
|
32
31
|
const claimed = path;
|
|
33
32
|
const registry = claims;
|
|
34
33
|
return {
|
|
35
34
|
path: claimed,
|
|
36
35
|
release: () => {
|
|
37
|
-
|
|
36
|
+
claim.owners.delete(owner);
|
|
37
|
+
if (claim.owners.size === 0 && registry.get(claimed) === claim) {
|
|
38
38
|
registry.delete(claimed);
|
|
39
|
+
}
|
|
39
40
|
},
|
|
40
41
|
};
|
|
41
42
|
}
|
|
@@ -67,7 +68,8 @@ export function createSandboxReadinessGate(probe) {
|
|
|
67
68
|
* Sandbox 是本地开发/测试面,无平台社交图谱(好友/群/频道),
|
|
68
69
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
69
70
|
*/
|
|
70
|
-
export class SandboxWsEndpoint {
|
|
71
|
+
export class SandboxWsEndpoint extends Endpoint {
|
|
72
|
+
client;
|
|
71
73
|
#logger;
|
|
72
74
|
#options;
|
|
73
75
|
#connections = new Map();
|
|
@@ -78,8 +80,15 @@ export class SandboxWsEndpoint {
|
|
|
78
80
|
#open = false;
|
|
79
81
|
#started = false;
|
|
80
82
|
constructor(options) {
|
|
83
|
+
super();
|
|
81
84
|
this.#logger = getAdapterLogger('sandbox', options.defaults.id);
|
|
82
85
|
this.#options = options;
|
|
86
|
+
this.client = new SandboxClient(() => this.#wsPath, () => [...this.#connections.values()].map((connection) => ({
|
|
87
|
+
target: connection.target,
|
|
88
|
+
owner: connection.owner,
|
|
89
|
+
socket: connection.socket,
|
|
90
|
+
placeholder: connection.placeholder === true,
|
|
91
|
+
})));
|
|
83
92
|
}
|
|
84
93
|
/** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
|
|
85
94
|
get name() {
|
|
@@ -198,6 +207,7 @@ export class SandboxWsEndpoint {
|
|
|
198
207
|
}
|
|
199
208
|
const release = bindSandboxWsSocket(socket, {
|
|
200
209
|
onMessage: (raw) => {
|
|
210
|
+
void this.#emitPlatformEvent('message', raw);
|
|
201
211
|
if (!canExecute) {
|
|
202
212
|
socket.send(JSON.stringify({
|
|
203
213
|
type: 'error',
|
|
@@ -224,7 +234,7 @@ export class SandboxWsEndpoint {
|
|
|
224
234
|
}));
|
|
225
235
|
// Don't gate on #open — inbound must always reach the gateway so
|
|
226
236
|
// Command/AI dispatch and outbound replies work.
|
|
227
|
-
void this
|
|
237
|
+
void this.emit('message.receive', {
|
|
228
238
|
conversation,
|
|
229
239
|
...(parsed.messageId ? { message: { conversation, id: parsed.messageId } } : {}),
|
|
230
240
|
content: parsed.text,
|
|
@@ -248,6 +258,7 @@ export class SandboxWsEndpoint {
|
|
|
248
258
|
});
|
|
249
259
|
},
|
|
250
260
|
onClose: () => {
|
|
261
|
+
void this.#emitPlatformEvent('connection.close', { target, owner });
|
|
251
262
|
// Only drop the map entry if we still own this socket — a replace
|
|
252
263
|
// may have already swapped in a newer connection for the same target.
|
|
253
264
|
const current = this.#connections.get(target);
|
|
@@ -257,6 +268,7 @@ export class SandboxWsEndpoint {
|
|
|
257
268
|
}
|
|
258
269
|
},
|
|
259
270
|
onError: (err) => {
|
|
271
|
+
void this.#emitPlatformEvent('connection.error', { target, owner, error: err });
|
|
260
272
|
this.#logger.warn(formatCompact({
|
|
261
273
|
op: 'sandbox_ws_error',
|
|
262
274
|
target,
|
|
@@ -265,6 +277,7 @@ export class SandboxWsEndpoint {
|
|
|
265
277
|
},
|
|
266
278
|
});
|
|
267
279
|
this.#connections.set(target, { target, owner, socket, release });
|
|
280
|
+
void this.#emitPlatformEvent('connection.open', { target, owner, socket });
|
|
268
281
|
this.#logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
269
282
|
this.#readiness.afterProbe((shellIsolation) => {
|
|
270
283
|
const current = this.#connections.get(target);
|
|
@@ -303,6 +316,15 @@ export class SandboxWsEndpoint {
|
|
|
303
316
|
placeholder: true,
|
|
304
317
|
});
|
|
305
318
|
}
|
|
319
|
+
async #emitPlatformEvent(name, event) {
|
|
320
|
+
await this.emitPlatform(name, event).catch((error) => {
|
|
321
|
+
this.#logger.warn(formatCompact({
|
|
322
|
+
op: 'sandbox_platform_event_failed',
|
|
323
|
+
event: name,
|
|
324
|
+
error: error instanceof Error ? error.message : String(error),
|
|
325
|
+
}));
|
|
326
|
+
});
|
|
327
|
+
}
|
|
306
328
|
}
|
|
307
329
|
function probeShellIsolation() {
|
|
308
330
|
return new Promise((resolve) => {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { bindSandboxWsSocket, formatSandboxOutbound, normalizeSandboxOutboundSegments, parseSandboxWsPayload, resolveSandboxEndpoint, sandboxInboundConversation, whenWsOpen, type MessageElement, type MessageType, type ResolvedSandboxBot, type SandboxAdapterConfig, type SandboxWsSocket, } from './protocol.js';
|
|
2
|
+
export { SandboxClient, sandboxClient, type SandboxClientEventMap, type SandboxClientConnection, } from './client.js';
|
|
2
3
|
export { SandboxWsEndpoint, type SandboxEndpointOptions, } from './endpoint.js';
|
package/lib/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { bindSandboxWsSocket, formatSandboxOutbound, normalizeSandboxOutboundSegments, parseSandboxWsPayload, resolveSandboxEndpoint, sandboxInboundConversation, whenWsOpen, } from './protocol.js';
|
|
2
|
+
export { SandboxClient, sandboxClient, } from './client.js';
|
|
2
3
|
export { SandboxWsEndpoint, } from './endpoint.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-sandbox",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.2",
|
|
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.2.
|
|
52
|
-
"@zhin.js/client": "2.1.
|
|
51
|
+
"@zhin.js/adapter": "1.2.1",
|
|
52
|
+
"@zhin.js/client": "2.1.12",
|
|
53
53
|
"@zhin.js/console-contract": "1.0.1",
|
|
54
|
-
"@zhin.js/core": "1.5.
|
|
55
|
-
"@zhin.js/
|
|
54
|
+
"@zhin.js/core": "1.5.15",
|
|
55
|
+
"@zhin.js/feature-kit": "1.0.13",
|
|
56
|
+
"@zhin.js/host-http": "1.0.14",
|
|
56
57
|
"@zhin.js/im-contract": "1.0.4",
|
|
57
|
-
"@zhin.js/logger": "1.0.
|
|
58
|
-
"@zhin.js/page": "1.0.
|
|
58
|
+
"@zhin.js/logger": "1.0.77",
|
|
59
|
+
"@zhin.js/page": "1.0.13"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@types/react": "^19.2.18",
|
|
@@ -63,19 +64,19 @@
|
|
|
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.
|
|
68
|
-
"zhin.js": "6.0.
|
|
67
|
+
"@zhin.js/pagemanager": "2.0.21",
|
|
68
|
+
"@zhin.js/runtime": "1.0.15",
|
|
69
|
+
"zhin.js": "6.0.15"
|
|
69
70
|
},
|
|
70
71
|
"peerDependencies": {
|
|
71
72
|
"react": "^19.0.0",
|
|
72
|
-
"@zhin.js/adapter": "1.2.
|
|
73
|
-
"@zhin.js/client": "2.1.
|
|
73
|
+
"@zhin.js/adapter": "1.2.1",
|
|
74
|
+
"@zhin.js/client": "2.1.12",
|
|
74
75
|
"@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.
|
|
76
|
+
"@zhin.js/core": "1.5.15",
|
|
77
|
+
"@zhin.js/host-http": "1.0.14",
|
|
78
|
+
"@zhin.js/page": "1.0.13",
|
|
79
|
+
"zhin.js": "6.0.15"
|
|
79
80
|
},
|
|
80
81
|
"peerDependenciesMeta": {
|
|
81
82
|
"react": {
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SandboxWsSocket } from './protocol.js';
|
|
2
|
+
import { defineEndpointClient } from 'zhin.js/adapter';
|
|
3
|
+
|
|
4
|
+
export interface SandboxClientConnection {
|
|
5
|
+
readonly target: string;
|
|
6
|
+
readonly owner: string;
|
|
7
|
+
readonly socket: SandboxWsSocket;
|
|
8
|
+
readonly placeholder: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Direct view of the live Sandbox protocol clients owned by one Endpoint. */
|
|
12
|
+
export class SandboxClient {
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly resolvePath: () => string,
|
|
15
|
+
private readonly resolveConnections: () => Iterable<SandboxClientConnection>,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
get path(): string {
|
|
19
|
+
return this.resolvePath();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
connections(): readonly SandboxClientConnection[] {
|
|
23
|
+
return Object.freeze([...this.resolveConnections()]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
connection(target: string): SandboxClientConnection | undefined {
|
|
27
|
+
return this.connections().find((connection) => connection.target === target);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
send(target: string, payload: string): void {
|
|
31
|
+
const connection = this.connection(target);
|
|
32
|
+
if (!connection || connection.placeholder) {
|
|
33
|
+
throw new Error(`Sandbox client ${target} is not connected`);
|
|
34
|
+
}
|
|
35
|
+
connection.socket.send(payload);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SandboxClientEventMap = Record<string, unknown>;
|
|
40
|
+
|
|
41
|
+
declare module '@zhin.js/feature-kit' {
|
|
42
|
+
interface AdapterClientRegistry {
|
|
43
|
+
readonly sandbox: { readonly client: SandboxClient; readonly events: SandboxClientEventMap };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const sandboxClient = defineEndpointClient<SandboxClient, SandboxClientEventMap>('sandbox');
|
package/src/endpoint.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
1
2
|
/**
|
|
2
|
-
* SandboxWsEndpoint — WebSocket lifecycle and
|
|
3
|
+
* SandboxWsEndpoint — WebSocket lifecycle and OutboundMessageService bridge for /sandbox.
|
|
3
4
|
*/
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { execFile } from 'node:child_process';
|
|
6
|
-
import
|
|
7
|
-
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
7
|
+
import { type EndpointSendRequest } from 'zhin.js/adapter';
|
|
8
8
|
import type { HttpHost, WsConnection } from '@zhin.js/host-http';
|
|
9
9
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
10
10
|
import type { CapabilityId } from 'zhin.js';
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ResolvedSandboxBot,
|
|
18
18
|
type SandboxWsSocket,
|
|
19
19
|
} from './protocol.js';
|
|
20
|
+
import { SandboxClient, type SandboxClientConnection } from './client.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* 多 sandbox endpoint 共用同一个 HttpHost 时,同 path 的所有 WS listener
|
|
@@ -24,7 +25,12 @@ import {
|
|
|
24
25
|
* 首个占用 `/sandbox`(保持 Console 默认兼容),其余退到 `/sandbox/<name>`。
|
|
25
26
|
* 认领记录按 HttpHost 隔离,endpoint stop() 时必须 release。
|
|
26
27
|
*/
|
|
27
|
-
|
|
28
|
+
interface SandboxWsPathClaim {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly owners: Set<symbol>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const claimedWsPaths = new WeakMap<HttpHost, Map<string, SandboxWsPathClaim>>();
|
|
28
34
|
|
|
29
35
|
function claimSandboxWsPath(
|
|
30
36
|
http: HttpHost,
|
|
@@ -37,7 +43,7 @@ function claimSandboxWsPath(
|
|
|
37
43
|
}
|
|
38
44
|
const candidates = ['/sandbox', `/sandbox/${encodeURIComponent(name)}`];
|
|
39
45
|
let path = candidates.find(
|
|
40
|
-
(candidate) => !claims!.has(candidate) || claims!.get(candidate) === name,
|
|
46
|
+
(candidate) => !claims!.has(candidate) || claims!.get(candidate)?.name === name,
|
|
41
47
|
);
|
|
42
48
|
if (!path) {
|
|
43
49
|
let index = 2;
|
|
@@ -47,13 +53,19 @@ function claimSandboxWsPath(
|
|
|
47
53
|
path = `/sandbox/${encodeURIComponent(name)}-${index}`;
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
|
-
|
|
56
|
+
const owner = Symbol(name);
|
|
57
|
+
const claim = claims.get(path) ?? { name, owners: new Set<symbol>() };
|
|
58
|
+
claim.owners.add(owner);
|
|
59
|
+
claims.set(path, claim);
|
|
51
60
|
const claimed = path;
|
|
52
61
|
const registry = claims;
|
|
53
62
|
return {
|
|
54
63
|
path: claimed,
|
|
55
64
|
release: () => {
|
|
56
|
-
|
|
65
|
+
claim.owners.delete(owner);
|
|
66
|
+
if (claim.owners.size === 0 && registry.get(claimed) === claim) {
|
|
67
|
+
registry.delete(claimed);
|
|
68
|
+
}
|
|
57
69
|
},
|
|
58
70
|
};
|
|
59
71
|
}
|
|
@@ -102,8 +114,6 @@ export function createSandboxReadinessGate(probe: ShellIsolationProbe): Readonly
|
|
|
102
114
|
|
|
103
115
|
export interface SandboxEndpointOptions {
|
|
104
116
|
readonly id: CapabilityId;
|
|
105
|
-
readonly gateway: MessageGateway;
|
|
106
|
-
readonly sideEvents?: SideEventGateway;
|
|
107
117
|
readonly http: HttpHost;
|
|
108
118
|
readonly defaults: ResolvedSandboxBot;
|
|
109
119
|
}
|
|
@@ -112,7 +122,8 @@ export interface SandboxEndpointOptions {
|
|
|
112
122
|
* Sandbox 是本地开发/测试面,无平台社交图谱(好友/群/频道),
|
|
113
123
|
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
114
124
|
*/
|
|
115
|
-
export class SandboxWsEndpoint
|
|
125
|
+
export class SandboxWsEndpoint extends Endpoint<SandboxClient> {
|
|
126
|
+
readonly client: SandboxClient;
|
|
116
127
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
117
128
|
|
|
118
129
|
readonly #options: SandboxEndpointOptions;
|
|
@@ -125,8 +136,18 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
125
136
|
#started = false;
|
|
126
137
|
|
|
127
138
|
constructor(options: SandboxEndpointOptions) {
|
|
139
|
+
super();
|
|
128
140
|
this.#logger = getAdapterLogger('sandbox', options.defaults.id);
|
|
129
141
|
this.#options = options;
|
|
142
|
+
this.client = new SandboxClient(
|
|
143
|
+
() => this.#wsPath,
|
|
144
|
+
() => [...this.#connections.values()].map((connection) => ({
|
|
145
|
+
target: connection.target,
|
|
146
|
+
owner: connection.owner,
|
|
147
|
+
socket: connection.socket,
|
|
148
|
+
placeholder: connection.placeholder === true,
|
|
149
|
+
} satisfies SandboxClientConnection)),
|
|
150
|
+
);
|
|
130
151
|
}
|
|
131
152
|
|
|
132
153
|
/** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
|
|
@@ -248,6 +269,7 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
248
269
|
}
|
|
249
270
|
const release = bindSandboxWsSocket(socket, {
|
|
250
271
|
onMessage: (raw) => {
|
|
272
|
+
void this.#emitPlatformEvent('message', raw);
|
|
251
273
|
if (!canExecute) {
|
|
252
274
|
socket.send(JSON.stringify({
|
|
253
275
|
type: 'error',
|
|
@@ -274,7 +296,7 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
274
296
|
}));
|
|
275
297
|
// Don't gate on #open — inbound must always reach the gateway so
|
|
276
298
|
// Command/AI dispatch and outbound replies work.
|
|
277
|
-
void this
|
|
299
|
+
void this.emit('message.receive', {
|
|
278
300
|
conversation,
|
|
279
301
|
...(parsed.messageId ? { message: { conversation, id: parsed.messageId } } : {}),
|
|
280
302
|
content: parsed.text,
|
|
@@ -298,6 +320,7 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
298
320
|
});
|
|
299
321
|
},
|
|
300
322
|
onClose: () => {
|
|
323
|
+
void this.#emitPlatformEvent('connection.close', { target, owner });
|
|
301
324
|
// Only drop the map entry if we still own this socket — a replace
|
|
302
325
|
// may have already swapped in a newer connection for the same target.
|
|
303
326
|
const current = this.#connections.get(target);
|
|
@@ -307,6 +330,7 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
307
330
|
}
|
|
308
331
|
},
|
|
309
332
|
onError: (err) => {
|
|
333
|
+
void this.#emitPlatformEvent('connection.error', { target, owner, error: err });
|
|
310
334
|
this.#logger.warn(formatCompact({
|
|
311
335
|
op: 'sandbox_ws_error',
|
|
312
336
|
target,
|
|
@@ -315,6 +339,7 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
315
339
|
},
|
|
316
340
|
});
|
|
317
341
|
this.#connections.set(target, { target, owner, socket, release });
|
|
342
|
+
void this.#emitPlatformEvent('connection.open', { target, owner, socket });
|
|
318
343
|
this.#logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
319
344
|
this.#readiness.afterProbe((shellIsolation) => {
|
|
320
345
|
const current = this.#connections.get(target);
|
|
@@ -352,6 +377,16 @@ export class SandboxWsEndpoint implements EndpointInstance {
|
|
|
352
377
|
placeholder: true,
|
|
353
378
|
});
|
|
354
379
|
}
|
|
380
|
+
|
|
381
|
+
async #emitPlatformEvent(name: string, event: unknown): Promise<void> {
|
|
382
|
+
await this.emitPlatform(name, event).catch((error) => {
|
|
383
|
+
this.#logger.warn(formatCompact({
|
|
384
|
+
op: 'sandbox_platform_event_failed',
|
|
385
|
+
event: name,
|
|
386
|
+
error: error instanceof Error ? error.message : String(error),
|
|
387
|
+
}));
|
|
388
|
+
});
|
|
389
|
+
}
|
|
355
390
|
}
|
|
356
391
|
|
|
357
392
|
function probeShellIsolation(): Promise<ShellIsolationStatus> {
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,13 @@ export {
|
|
|
13
13
|
type SandboxWsSocket,
|
|
14
14
|
} from './protocol.js';
|
|
15
15
|
|
|
16
|
+
export {
|
|
17
|
+
SandboxClient,
|
|
18
|
+
sandboxClient,
|
|
19
|
+
type SandboxClientEventMap,
|
|
20
|
+
type SandboxClientConnection,
|
|
21
|
+
} from './client.js';
|
|
22
|
+
|
|
16
23
|
export {
|
|
17
24
|
SandboxWsEndpoint,
|
|
18
25
|
type SandboxEndpointOptions,
|