@zhin.js/adapter-sandbox 5.0.6 → 6.0.0

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