@zhin.js/adapter-sandbox 5.0.5 → 5.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @zhin.js/adapter-process
2
2
 
3
+ ## 5.0.6
4
+
5
+ ### Patch Changes
6
+
7
+ - cc5c94d: 约定式插件运行时迁移(breaking):插件与适配器由 `usePlugin()` / `extends Adapter` 迁移为 `definePlugin` / `defineAdapter` + `plugin.ts` + 约定目录(`adapters/`、`commands/`、`components/`、`tools/` 等)。
8
+
9
+ - 新增约定式运行时包:`@zhin.js/plugin-runtime`、`@zhin.js/adapter`、`@zhin.js/runtime`、`@zhin.js/host-http`(首版 1.0.0 走 init-publish,不在本 changeset 内 bump)。
10
+ - 全部 20 个平台适配器改为约定式 `defineAdapter`,旧 `usePlugin` / `extends Adapter` / `segment-mapper` 生产入口已删除;onebot11 反向 WSS、onebot12 webhook/wss、milky sse/webhook/wss、satori webhook、kook webhook、qq webhook/middleware 等 slice 1 推迟的连接模式已补齐。
11
+ - 游戏 / 工具 / 服务插件同步迁移到约定目录结构。
12
+ - CLI 增加 plugin-runtime host installer(http/database/outbound/schedule/console 等)。
13
+
14
+ 后续加固(同批):
15
+
16
+ - CLI:`zhin runtime start --daemon`(pidfile/崩溃拉起/风暴保护),orphan watchdog 防僵尸进程;legacy `zhin dev` / `zhin start` 已移除(含 `zhin restart`),`zhin stop` 兼容新 daemon。
17
+ - 安全:builtin 工具统一走 `security/policy-facade.ts` 的 `runToolPolicies`(声明式策略表,deny 优先);审计日志 close flush + 背压队列;`splitCompoundCommand` 引号感知、`extractCommandName` 去引号堵绕过。
18
+ - 日志:Logger 双堆栈修复、本地时区、`getLogger` 挂树(`setLevel` 递归生效)、第三方库(log4js/discord)桥接、启动人读总结。
19
+ - 结构:`plugins/games/shared` 迁为 `packages/game-kit`(`@zhin.js/game-kit`);死目录 `plugins/adapters/common` 删除。
20
+ - 脚手架:`create-zhin-app` / `zhin new` / scaffold-wizard 生成物改为 Plugin Runtime 形态(minimal-bot 同构,新配置格式)。
21
+ - Console:endpoint.list 真实名称与 phase、schema:get-all 按 instanceKey 映射、db:\* 接 DatabaseHost。
22
+
23
+ 注:按仓库发布惯例(见 1bb345dd2),本次 breaking 迁移统一使用 patch,避免 zhin.js 5.0 级联。
24
+
25
+ - Updated dependencies [16ec4e8]
26
+ - Updated dependencies [cc5c94d]
27
+ - Updated dependencies [447f3e2]
28
+ - @zhin.js/core@1.3.5
29
+ - @zhin.js/host-http@1.0.1
30
+ - @zhin.js/logger@1.0.75
31
+ - @zhin.js/plugin-runtime@1.0.1
32
+ - @zhin.js/adapter@1.0.1
33
+ - @zhin.js/page@1.0.1
34
+
3
35
  ## 5.0.5
4
36
 
5
37
  ### Patch Changes
package/README.md CHANGED
@@ -18,12 +18,28 @@ pnpm add @zhin.js/adapter-sandbox
18
18
 
19
19
  ## 依赖
20
20
 
21
- Sandbox 适配器需要以下服务插件:
21
+ ### Plugin Runtime(新,`zhin runtime start`)
22
+
23
+ - `@zhin.js/adapter` — 约定式 `adapters/sandbox.ts`
24
+ - `@zhin.js/host-http` — Root 提供的 `httpHostToken`(WebSocket `/sandbox` + Console HTTP)
25
+ - `@zhin.js/core` — `messageGatewayToken` / ImRuntime 入站出站
26
+ - `@zhin.js/page` + `pages/sandbox.tsx` — ADR 0046 约定页(`definePage`)
27
+
28
+ Root 在 `zhin runtime start` 时装载 `@zhin.js/host-http`、`ConsoleRuntime` 与
29
+ `ClientBuildModuleRuntime`。打开 `http://<host>:<port>/console` 可浏览页面;Sandbox 页
30
+ (路由形如 `/…/p-sandbox`)内置 WebSocket `/sandbox` 聊天壳。
31
+
32
+ 旧 `client/`(`register(api)` / `pageManager.addEntry`)仅保留给 legacy Host 栈参考,
33
+ **不是** Plugin Runtime 生产入口。
34
+
35
+ ### 旧 Host 栈(`zhin dev` / full-bot)
22
36
 
23
37
  - `@zhin.js/host-router` — HTTP 服务(提供 Router 和 WebSocket)
24
38
  - `@zhin.js/host-api` — Host 侧 Console API(`addEntry` 注册 Sandbox 扩展)
25
39
  - `@zhin.js/client` — Remote Console 客户端 SDK(UI 在 zhin-console 仓库)
26
40
 
41
+ 出站 wire 只做 JSON 封装;旧 `segment-mapper`(canonical segments)归一化上移到 gateway/core 渲染链。
42
+
27
43
  ## 配置
28
44
 
29
45
  **推荐(与 [minimal-bot](../../../examples/minimal-bot/) 一致)**:`endpoints: []`,在 Remote Console 打开「沙盒」页时经 `/sandbox` WebSocket **自动创建** bot(如 `sandbox-xxxx`),无需在 yaml 里写 `context: sandbox`。
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Convention entry: discover `adapters/sandbox.ts` → defineAdapter.
3
+ */
4
+ import { defineAdapter } from '@zhin.js/adapter';
5
+ import { messageGatewayToken } from '@zhin.js/core/runtime';
6
+ import { httpHostToken } from '@zhin.js/host-http';
7
+ import { SandboxWsEndpoint } from '../src/endpoint.js';
8
+ import {
9
+ resolveSandboxEndpoint,
10
+ type SandboxAdapterConfig,
11
+ } from '../src/protocol.js';
12
+
13
+ export { SandboxWsEndpoint } from '../src/endpoint.js';
14
+ export type { SandboxEndpointOptions } from '../src/endpoint.js';
15
+
16
+ export default defineAdapter<SandboxAdapterConfig>({
17
+ capabilities: ['inbound', 'outbound'],
18
+ create(context) {
19
+ return new SandboxWsEndpoint({
20
+ id: context.id,
21
+ gateway: context.use(messageGatewayToken),
22
+ http: context.use(httpHostToken),
23
+ defaults: resolveSandboxEndpoint(context.config),
24
+ });
25
+ },
26
+ });
@@ -0,0 +1,25 @@
1
+ import type { EndpointInstance } from '@zhin.js/adapter';
2
+ import type { MessageGateway } from '@zhin.js/core/runtime';
3
+ import type { HttpHost } from '@zhin.js/host-http';
4
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
5
+ import { type ResolvedSandboxBot } from './protocol.js';
6
+ export interface SandboxEndpointOptions {
7
+ readonly id: CapabilityId;
8
+ readonly gateway: MessageGateway;
9
+ readonly http: HttpHost;
10
+ readonly defaults: ResolvedSandboxBot;
11
+ }
12
+ export declare class SandboxWsEndpoint implements EndpointInstance {
13
+ #private;
14
+ constructor(options: SandboxEndpointOptions);
15
+ /** Live endpoint name (config `name`) — Console endpoint.list/resolve uses it. */
16
+ get name(): string;
17
+ start(): void;
18
+ open(): void;
19
+ close(): void;
20
+ stop(): void;
21
+ send({ target, payload }: {
22
+ readonly target: string;
23
+ readonly payload: unknown;
24
+ }): unknown;
25
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * SandboxWsEndpoint — WebSocket lifecycle and MessageGateway bridge for /sandbox.
3
+ */
4
+ import { randomUUID } from 'node:crypto';
5
+ import { formatCompact, getLogger } from '@zhin.js/logger';
6
+ import { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, whenWsOpen, } from './protocol.js';
7
+ const logger = getLogger('sandbox');
8
+ export class SandboxWsEndpoint {
9
+ #options;
10
+ #connections = new Map();
11
+ #wsHandleRelease;
12
+ #open = false;
13
+ #started = false;
14
+ constructor(options) {
15
+ this.#options = options;
16
+ }
17
+ /** Live endpoint name (config `name`) — Console endpoint.list/resolve uses it. */
18
+ get name() {
19
+ return this.#options.defaults.name;
20
+ }
21
+ start() {
22
+ if (this.#started)
23
+ return;
24
+ this.#started = true;
25
+ const handle = this.#options.http.ws('/sandbox');
26
+ this.#wsHandleRelease = handle.onConnection((connection) => {
27
+ this.#acceptConnection(connection);
28
+ });
29
+ if (!this.#options.defaults.randomNamePerConnection) {
30
+ this.#ensurePlaceholder(this.#options.defaults.name, this.#options.defaults.owner);
31
+ }
32
+ logger.info(formatCompact({
33
+ op: 'sandbox_ws_mounted',
34
+ path: '/sandbox',
35
+ endpoint: this.#options.defaults.name,
36
+ }));
37
+ }
38
+ open() {
39
+ this.#open = true;
40
+ }
41
+ close() {
42
+ this.#open = false;
43
+ }
44
+ stop() {
45
+ this.#open = false;
46
+ this.#wsHandleRelease?.();
47
+ this.#wsHandleRelease = undefined;
48
+ for (const connection of this.#connections.values())
49
+ connection.release();
50
+ this.#connections.clear();
51
+ this.#started = false;
52
+ logger.debug(formatCompact({ op: 'sandbox_stopped' }));
53
+ }
54
+ send({ target, payload }) {
55
+ if (!this.#open)
56
+ return undefined;
57
+ const connection = this.#connections.get(target);
58
+ if (!connection) {
59
+ logger.debug(formatCompact({ op: 'sandbox_send_miss', target }));
60
+ return undefined;
61
+ }
62
+ if (connection.placeholder) {
63
+ logger.debug(formatCompact({ op: 'sandbox_send_placeholder', target }));
64
+ return undefined;
65
+ }
66
+ connection.socket.send(formatSandboxOutbound(payload));
67
+ logger.debug(formatCompact({ op: 'sandbox_send', target }));
68
+ return payload;
69
+ }
70
+ #acceptConnection(connection) {
71
+ const target = this.#options.defaults.randomNamePerConnection
72
+ ? `sandbox-${randomUUID().slice(0, 8)}`
73
+ : this.#options.defaults.name;
74
+ const owner = this.#options.defaults.owner;
75
+ const socket = connection.socket;
76
+ if (this.#connections.has(target)) {
77
+ this.#connections.get(target)?.release();
78
+ this.#connections.delete(target);
79
+ }
80
+ const release = bindSandboxWsSocket(socket, {
81
+ onMessage: (raw) => {
82
+ if (!this.#open)
83
+ return;
84
+ const parsed = parseSandboxWsPayload(raw);
85
+ logger.debug(formatCompact({
86
+ op: 'sandbox_recv',
87
+ target,
88
+ sender: parsed.id || owner,
89
+ text: parsed.text.slice(0, 80),
90
+ }));
91
+ void this.#options.gateway.receive({
92
+ adapter: this.#options.id,
93
+ target,
94
+ content: parsed.text,
95
+ sender: parsed.id || owner,
96
+ metadata: Object.freeze({
97
+ type: parsed.type,
98
+ elements: parsed.content,
99
+ timestamp: parsed.timestamp,
100
+ ...(parsed.action ? { action: parsed.action } : {}),
101
+ }),
102
+ }).catch((err) => {
103
+ logger.warn(formatCompact({
104
+ op: 'sandbox_gateway_receive_failed',
105
+ target,
106
+ error: err instanceof Error ? err.message : String(err),
107
+ }));
108
+ });
109
+ },
110
+ onClose: () => {
111
+ this.#connections.delete(target);
112
+ logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
113
+ },
114
+ onError: (err) => {
115
+ logger.warn(formatCompact({
116
+ op: 'sandbox_ws_error',
117
+ target,
118
+ error: err instanceof Error ? err.message : String(err),
119
+ }));
120
+ },
121
+ });
122
+ this.#connections.set(target, Object.freeze({ target, owner, socket, release }));
123
+ logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
124
+ if (!this.#options.defaults.randomNamePerConnection) {
125
+ const readyPayload = JSON.stringify({
126
+ type: 'ready',
127
+ id: owner,
128
+ endpoint: target,
129
+ content: [{
130
+ type: 'text',
131
+ data: {
132
+ text: [
133
+ `已连接 Sandbox「${target}」`,
134
+ '与 Node Host 控制台沙盒协议一致(/sandbox)',
135
+ '命令: help · ping · zt · status',
136
+ ].join('\n'),
137
+ },
138
+ }],
139
+ timestamp: Date.now(),
140
+ });
141
+ whenWsOpen(socket, () => socket.send(readyPayload));
142
+ }
143
+ }
144
+ #ensurePlaceholder(name, owner) {
145
+ if (this.#connections.has(name))
146
+ return;
147
+ this.#connections.set(name, Object.freeze({
148
+ target: name,
149
+ owner,
150
+ socket: { send: () => undefined, close: () => undefined },
151
+ release: () => undefined,
152
+ placeholder: true,
153
+ }));
154
+ }
155
+ }
package/lib/index.d.ts CHANGED
@@ -1,22 +1,2 @@
1
- import { usePlugin } from "zhin.js";
2
- import { SandboxWsHostAdapter } from "./sandbox-ws.js";
3
- type SandboxRouter = {
4
- ws: (path: string) => NonNullable<SandboxAdapter["wss"]>;
5
- };
6
- declare module "zhin.js" {
7
- interface Adapters {
8
- sandbox: SandboxAdapter;
9
- }
10
- }
11
- /** Node:`Router.ws` */
12
- export declare class SandboxAdapter extends SandboxWsHostAdapter {
13
- wss?: {
14
- on: (ev: string, fn: (...args: unknown[]) => void) => void;
15
- close: () => void;
16
- };
17
- constructor(plugin: ReturnType<typeof usePlugin>);
18
- start(): Promise<void>;
19
- setupWebSocket(router: SandboxRouter): Promise<void>;
20
- }
21
- export { SandboxWsEndpoint, SandboxWsHostAdapter, resolveSandboxEndpoint, bindSandboxWsSocket, parseSandboxWsPayload, type ResolvedSandboxBot, type SandboxWsConfig, type SandboxWsSocket, } from "./sandbox-ws.js";
22
- //# sourceMappingURL=index.d.ts.map
1
+ export { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, resolveSandboxEndpoint, whenWsOpen, type MessageElement, type MessageType, type ResolvedSandboxBot, type SandboxAdapterConfig, type SandboxWsSocket, } from './protocol.js';
2
+ export { SandboxWsEndpoint, type SandboxEndpointOptions, } from './endpoint.js';
package/lib/index.js CHANGED
@@ -1,67 +1,2 @@
1
- import path from "node:path";
2
- import { usePlugin } from "zhin.js";
3
- import { SandboxWsHostAdapter, resolveSandboxEndpoint, } from "./sandbox-ws.js";
4
- const plugin = usePlugin();
5
- const logger = plugin.logger;
6
- /** Node:`Router.ws` */
7
- export class SandboxAdapter extends SandboxWsHostAdapter {
8
- wss;
9
- constructor(plugin) {
10
- const appConfig = (plugin.inject("config")?.getPrimary() ?? {});
11
- super(plugin, resolveSandboxEndpoint(appConfig));
12
- }
13
- async start() {
14
- await super.start();
15
- this.registerConfiguredPlaceholder();
16
- logger.debug(`Sandbox placeholder: ${this.defaults.name} (offline until /sandbox WS)`);
17
- }
18
- async setupWebSocket(router) {
19
- if (this.wss)
20
- return;
21
- this.wss = router.ws("/sandbox");
22
- this.wss.on("connection", (...args) => {
23
- const ws = args[0];
24
- const req = args[1];
25
- logger.debug(`New sandbox connection from ${req.socket?.remoteAddress ?? "unknown"}`);
26
- const endpoint = this.acceptWebSocket(ws);
27
- ws.on("close", () => {
28
- logger.debug(`Sandbox connection closed: ${endpoint.$config.name}`);
29
- this.endpoints.delete(endpoint.$id);
30
- });
31
- ws.on("error", (error) => {
32
- logger.error(`Sandbox WebSocket error for ${endpoint.$config.name}:`, error);
33
- });
34
- });
35
- logger.debug("Sandbox WebSocket server started at /sandbox (Node Router.ws)");
36
- }
37
- }
38
- const { provide } = usePlugin();
39
- provide({
40
- name: "sandbox",
41
- description: "Sandbox Adapter — Node Router.ws",
42
- mounted: async (p) => {
43
- const adapter = new SandboxAdapter(p);
44
- await adapter.start();
45
- return adapter;
46
- },
47
- dispose: async (adapter) => {
48
- for (const endpoint of adapter.endpoints.values()) {
49
- await endpoint.$disconnect();
50
- }
51
- adapter.wss?.close();
52
- await adapter.stop();
53
- },
54
- });
55
- plugin.useContext("router", 'sandbox', async (router, adapter) => {
56
- await adapter.setupWebSocket(router);
57
- });
58
- plugin.useContext("web", (pageManager) => {
59
- pageManager.addEntry({
60
- id: "sandbox",
61
- development: path.resolve(import.meta.dirname, "../client/index.tsx"),
62
- production: path.resolve(import.meta.dirname, "../dist/index.js"),
63
- meta: { name: "Sandbox" },
64
- });
65
- });
66
- export { SandboxWsEndpoint, SandboxWsHostAdapter, resolveSandboxEndpoint, bindSandboxWsSocket, parseSandboxWsPayload, } from "./sandbox-ws.js";
67
- //# sourceMappingURL=index.js.map
1
+ export { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, resolveSandboxEndpoint, whenWsOpen, } from './protocol.js';
2
+ export { SandboxWsEndpoint, } from './endpoint.js';
@@ -0,0 +1,52 @@
1
+ /** Sandbox WebSocket wire protocol helpers (no legacy Adapter/Endpoint). */
2
+ export type MessageType = 'private' | 'group' | 'guild' | 'direct' | 'channel';
3
+ export interface MessageElement {
4
+ readonly type: string;
5
+ readonly data?: Record<string, unknown>;
6
+ }
7
+ export interface SandboxWsSocket {
8
+ send(data: string): void;
9
+ close(code?: number, reason?: string): void;
10
+ on?(event: 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
11
+ off?(event: 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
12
+ addEventListener?(type: 'message' | 'close' | 'error', listener: (ev: Event | MessageEvent | CloseEvent) => void): void;
13
+ removeEventListener?(type: 'message' | 'close' | 'error', listener: (ev: Event | MessageEvent | CloseEvent) => void): void;
14
+ }
15
+ export type ResolvedSandboxBot = {
16
+ readonly context: 'sandbox';
17
+ readonly name: string;
18
+ readonly owner: string;
19
+ readonly randomNamePerConnection: boolean;
20
+ };
21
+ export interface SandboxAdapterConfig {
22
+ readonly endpoints?: ReadonlyArray<{
23
+ readonly context?: string;
24
+ readonly name?: string;
25
+ readonly owner?: string;
26
+ }>;
27
+ }
28
+ export declare function resolveSandboxEndpoint(appConfig: SandboxAdapterConfig): ResolvedSandboxBot;
29
+ export declare function bindSandboxWsSocket(ws: SandboxWsSocket, handlers: {
30
+ onMessage: (raw: string) => void;
31
+ onClose: () => void;
32
+ onError?: (err: unknown) => void;
33
+ }): () => void;
34
+ export declare function parseSandboxWsPayload(raw: string): {
35
+ type: MessageType;
36
+ id: string;
37
+ content: MessageElement[];
38
+ timestamp: number;
39
+ text: string;
40
+ action?: {
41
+ id: string;
42
+ payload: string;
43
+ };
44
+ };
45
+ /**
46
+ * Wire-encode an already-rendered outbound payload.
47
+ * Canonical segment mapping (old `segment-mapper` re-export of `to/fromCanonicalSegments`
48
+ * from legacy `zhin.js`) is intentionally not done here — the gateway/core render path
49
+ * owns that before `endpoint.send`.
50
+ */
51
+ export declare function formatSandboxOutbound(payload: unknown): string;
52
+ export declare function whenWsOpen(ws: SandboxWsSocket, fn: () => void): void;
@@ -0,0 +1,135 @@
1
+ /** Sandbox WebSocket wire protocol helpers (no legacy Adapter/Endpoint). */
2
+ export function resolveSandboxEndpoint(appConfig) {
3
+ const entry = appConfig.endpoints?.find((item) => item.context === 'sandbox');
4
+ const fixedName = typeof entry?.name === 'string' ? entry.name : undefined;
5
+ const name = fixedName || process.env.SANDBOX_BOT_NAME || 'sandbox-bot';
6
+ const owner = (typeof entry?.owner === 'string' && entry.owner)
7
+ || process.env.SANDBOX_BOT_OWNER
8
+ || 'sandbox-user';
9
+ return {
10
+ context: 'sandbox',
11
+ name,
12
+ owner,
13
+ randomNamePerConnection: !fixedName,
14
+ };
15
+ }
16
+ export function bindSandboxWsSocket(ws, handlers) {
17
+ if (typeof ws.on === 'function') {
18
+ const onMessage = (...args) => {
19
+ const data = args[0];
20
+ const raw = typeof data === 'string'
21
+ ? data
22
+ : data instanceof ArrayBuffer
23
+ ? new TextDecoder().decode(data)
24
+ : Buffer.isBuffer(data)
25
+ ? data.toString()
26
+ : String(data ?? '');
27
+ handlers.onMessage(raw);
28
+ };
29
+ ws.on('message', onMessage);
30
+ ws.on('close', handlers.onClose);
31
+ if (handlers.onError)
32
+ ws.on('error', handlers.onError);
33
+ return () => {
34
+ ws.off?.('message', onMessage);
35
+ ws.off?.('close', handlers.onClose);
36
+ if (handlers.onError)
37
+ ws.off?.('error', handlers.onError);
38
+ };
39
+ }
40
+ const onMessage = (ev) => {
41
+ const data = ev.data;
42
+ handlers.onMessage(typeof data === 'string' ? data : '');
43
+ };
44
+ const onClose = () => handlers.onClose();
45
+ const onError = handlers.onError
46
+ ? () => handlers.onError?.(new Error('WebSocket error'))
47
+ : undefined;
48
+ ws.addEventListener('message', onMessage);
49
+ ws.addEventListener('close', onClose);
50
+ if (onError)
51
+ ws.addEventListener('error', onError);
52
+ return () => {
53
+ ws.removeEventListener('message', onMessage);
54
+ ws.removeEventListener('close', onClose);
55
+ if (onError)
56
+ ws.removeEventListener('error', onError);
57
+ };
58
+ }
59
+ export function parseSandboxWsPayload(raw) {
60
+ let payload;
61
+ try {
62
+ payload = JSON.parse(raw);
63
+ }
64
+ catch {
65
+ payload = { text: raw };
66
+ }
67
+ const type = payload.type ?? 'private';
68
+ const id = payload.id ?? 'sandbox-user';
69
+ const content = typeof payload.content === 'string'
70
+ ? [{ type: 'text', data: { text: payload.content } }]
71
+ : Array.isArray(payload.content)
72
+ ? payload.content
73
+ : [{ type: 'text', data: { text: payload.text ?? raw } }];
74
+ const actionSegment = content.find((segment) => segment.type === 'action');
75
+ let action;
76
+ if (actionSegment?.data) {
77
+ const actionPayload = typeof actionSegment.data.payload === 'string'
78
+ ? actionSegment.data.payload
79
+ : typeof actionSegment.data.id === 'string'
80
+ ? actionSegment.data.id
81
+ : '';
82
+ const actionId = typeof actionSegment.data.id === 'string'
83
+ ? actionSegment.data.id
84
+ : actionPayload;
85
+ if (actionId || actionPayload) {
86
+ action = { id: actionId || actionPayload, payload: actionPayload || actionId };
87
+ }
88
+ }
89
+ let text = content
90
+ .flatMap((segment) => (segment.type === 'text' && typeof segment.data?.text === 'string'
91
+ ? [segment.data.text]
92
+ : []))
93
+ .join('\n');
94
+ if (!text.trim()) {
95
+ text = (typeof payload.text === 'string' && payload.text.trim())
96
+ ? payload.text
97
+ : action?.payload ?? raw;
98
+ }
99
+ return { type, id, content, timestamp: payload.timestamp ?? Date.now(), text, action };
100
+ }
101
+ /**
102
+ * Wire-encode an already-rendered outbound payload.
103
+ * Canonical segment mapping (old `segment-mapper` re-export of `to/fromCanonicalSegments`
104
+ * from legacy `zhin.js`) is intentionally not done here — the gateway/core render path
105
+ * owns that before `endpoint.send`.
106
+ */
107
+ export function formatSandboxOutbound(payload) {
108
+ if (typeof payload === 'string') {
109
+ return JSON.stringify({
110
+ content: [{ type: 'text', data: { text: payload } }],
111
+ timestamp: Date.now(),
112
+ });
113
+ }
114
+ if (Array.isArray(payload)) {
115
+ return JSON.stringify({
116
+ content: payload,
117
+ timestamp: Date.now(),
118
+ });
119
+ }
120
+ return JSON.stringify({ content: payload, timestamp: Date.now() });
121
+ }
122
+ /** WebSocket.OPEN 常量值;Node <22 无全局 WebSocket,不能用 WebSocket.OPEN。 */
123
+ const WS_OPEN = 1;
124
+ export function whenWsOpen(ws, fn) {
125
+ const std = ws;
126
+ if (typeof std.readyState === 'number') {
127
+ if (std.readyState === WS_OPEN) {
128
+ fn();
129
+ return;
130
+ }
131
+ std.addEventListener('open', fn, { once: true });
132
+ return;
133
+ }
134
+ fn();
135
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-sandbox",
3
- "version": "5.0.5",
4
- "description": "Zhin.js adapter for local testing and development",
3
+ "version": "5.0.6",
4
+ "description": "Zhin.js Sandbox adapter for Plugin Runtime (WebSocket /sandbox)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -13,9 +13,12 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
+ "adapters",
17
+ "pages",
18
+ "plugin.ts",
19
+ "schema.json",
16
20
  "src",
17
21
  "lib",
18
- "client",
19
22
  "dist",
20
23
  "agent",
21
24
  "README.md",
@@ -43,35 +46,34 @@
43
46
  "directory": "plugins/adapters/sandbox"
44
47
  },
45
48
  "dependencies": {
46
- "lucide-react": "^1.22.0"
49
+ "react": "^19.1.0",
50
+ "@zhin.js/console-contract": "1.0.0",
51
+ "@zhin.js/adapter": "1.0.1",
52
+ "@zhin.js/core": "1.3.5",
53
+ "@zhin.js/host-http": "1.0.1",
54
+ "@zhin.js/logger": "1.0.75",
55
+ "@zhin.js/page": "1.0.1",
56
+ "@zhin.js/plugin-runtime": "1.0.1"
47
57
  },
48
58
  "devDependencies": {
49
- "@types/react": "^19.2.17",
50
- "@types/react-dom": "^19.2.3",
59
+ "@types/ws": "^8.18.1",
51
60
  "typescript": "^6.0.3",
52
- "@zhin.js/cli": "1.0.93",
53
- "@zhin.js/client": "2.0.5",
54
- "@zhin.js/contract": "1.0.3",
55
- "@zhin.js/core": "1.3.4",
56
- "@zhin.js/host-api": "2.0.5",
57
- "@zhin.js/host-router": "2.0.3",
58
- "zhin.js": "4.1.2"
61
+ "vitest": "^4.1.10",
62
+ "ws": "^8.21.0",
63
+ "@zhin.js/pagemanager": "2.0.4",
64
+ "@zhin.js/runtime": "1.0.1"
59
65
  },
60
66
  "peerDependencies": {
61
- "@zhin.js/core": "1.3.4",
62
- "@zhin.js/client": "2.0.5",
63
- "@zhin.js/host-router": "2.0.3",
64
- "@zhin.js/host-api": "2.0.5",
65
- "@zhin.js/contract": "1.0.3"
67
+ "react": "^19.0.0",
68
+ "@zhin.js/adapter": "1.0.1",
69
+ "@zhin.js/console-contract": "1.0.0",
70
+ "@zhin.js/core": "1.3.5",
71
+ "@zhin.js/host-http": "1.0.1",
72
+ "@zhin.js/page": "1.0.1",
73
+ "@zhin.js/plugin-runtime": "1.0.1"
66
74
  },
67
75
  "peerDependenciesMeta": {
68
- "@zhin.js/host-router": {
69
- "optional": true
70
- },
71
- "@zhin.js/host-api": {
72
- "optional": true
73
- },
74
- "@zhin.js/client": {
76
+ "react": {
75
77
  "optional": true
76
78
  }
77
79
  },
@@ -82,8 +84,27 @@
82
84
  "engines": {
83
85
  "node": "^20.19.0 || >=22.12.0"
84
86
  },
87
+ "zhin": {
88
+ "protocol": 1,
89
+ "type": "plugin",
90
+ "entry": "./plugin.ts",
91
+ "engine": "^1.0.0",
92
+ "runtime": "trusted",
93
+ "features": [
94
+ {
95
+ "package": "@zhin.js/adapter",
96
+ "api": "^1.0.0"
97
+ },
98
+ {
99
+ "package": "@zhin.js/page",
100
+ "api": "^1.0.0"
101
+ }
102
+ ],
103
+ "plugins": []
104
+ },
85
105
  "scripts": {
86
- "build": "zhin build",
87
- "clean": "rimraf lib"
106
+ "build": "tsc",
107
+ "clean": "rimraf lib",
108
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/sandbox/tests"
88
109
  }
89
110
  }