@zhin.js/adapter-sandbox 7.0.1 → 7.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @zhin.js/adapter-process
2
2
 
3
+ ## 7.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [afc0e66]
8
+ - Updated dependencies [2e41ad5]
9
+ - Updated dependencies [9f57124]
10
+ - @zhin.js/core@1.5.2
11
+ - @zhin.js/adapter@1.1.5
12
+ - @zhin.js/im-contract@1.0.1
13
+ - @zhin.js/plugin-runtime@1.1.3
14
+ - @zhin.js/host-http@1.0.6
15
+ - @zhin.js/page@1.0.6
16
+
3
17
  ## 7.0.1
4
18
 
5
19
  ### Patch Changes
package/lib/endpoint.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { EndpointInstance } from '@zhin.js/adapter';
1
+ import type { EndpointInstance, EndpointSendRequest } from '@zhin.js/adapter';
2
2
  import type { MessageGateway } from '@zhin.js/core/runtime';
3
3
  import type { HttpHost } from '@zhin.js/host-http';
4
4
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
@@ -22,8 +22,5 @@ export declare class SandboxWsEndpoint implements EndpointInstance {
22
22
  open(): void;
23
23
  close(): void;
24
24
  stop(): void;
25
- send({ target, payload }: {
26
- readonly target: string;
27
- readonly payload: unknown;
28
- }): unknown;
25
+ send({ conversation, payload }: EndpointSendRequest): unknown;
29
26
  }
package/lib/endpoint.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { formatCompact, getLogger } from '@zhin.js/logger';
6
- import { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, whenWsOpen, } from './protocol.js';
6
+ import { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, sandboxInboundConversation, whenWsOpen, } from './protocol.js';
7
7
  const logger = getLogger('sandbox');
8
8
  /**
9
9
  * 多 sandbox endpoint 共用同一个 HttpHost 时,同 path 的所有 WS listener
@@ -106,36 +106,39 @@ export class SandboxWsEndpoint {
106
106
  this.#started = false;
107
107
  logger.debug(formatCompact({ op: 'sandbox_stopped' }));
108
108
  }
109
- send({ target, payload }) {
109
+ send({ conversation, payload }) {
110
110
  if (!this.#open)
111
111
  return undefined;
112
- // Reply target is the connection key (bot name / sandbox-uuid), not private:channelId.
113
- const connection = this.#connections.get(target)
112
+ // Reply targets this endpoint's own live socket (fixed bot name, or the
113
+ // only live random-name connection); conversation carries kind/id stamp.
114
+ const connection = this.#connections.get(this.#options.defaults.name)
114
115
  ?? this.#findLiveConnection();
115
116
  if (!connection) {
116
- logger.debug(formatCompact({ op: 'sandbox_send_miss', target }));
117
+ logger.debug(formatCompact({
118
+ op: 'sandbox_send_miss',
119
+ target: `${conversation.kind}:${conversation.id}`,
120
+ }));
117
121
  return undefined;
118
122
  }
119
123
  if (connection.placeholder) {
120
- logger.debug(formatCompact({ op: 'sandbox_send_placeholder', target }));
124
+ logger.debug(formatCompact({
125
+ op: 'sandbox_send_placeholder',
126
+ target: `${conversation.kind}:${conversation.id}`,
127
+ }));
121
128
  return undefined;
122
129
  }
123
- // Console UI filters by type+id; stamp last inbound channel onto outbound wire.
124
- const channel = connection.lastChannel ?? {
125
- type: 'private',
126
- id: connection.owner,
127
- };
130
+ // Console UI filters by type+id; stamp the conversation onto outbound wire.
128
131
  connection.socket.send(formatSandboxOutbound(payload, {
129
- type: channel.type,
130
- id: channel.id,
132
+ type: conversation.kind,
133
+ id: conversation.id,
131
134
  bot: this.#options.defaults.name,
132
135
  endpoint: connection.target,
133
136
  }));
134
137
  logger.debug(formatCompact({
135
138
  op: 'sandbox_send',
136
139
  target: connection.target,
137
- channelType: channel.type,
138
- channelId: channel.id,
140
+ channelType: conversation.kind,
141
+ channelId: conversation.id,
139
142
  }));
140
143
  return payload;
141
144
  }
@@ -172,28 +175,25 @@ export class SandboxWsEndpoint {
172
175
  const release = bindSandboxWsSocket(socket, {
173
176
  onMessage: (raw) => {
174
177
  const parsed = parseSandboxWsPayload(raw);
175
- const conn = this.#connections.get(target);
176
- if (conn && !conn.placeholder) {
177
- conn.lastChannel = {
178
- type: parsed.type,
179
- id: parsed.id || owner,
180
- };
181
- }
178
+ const sender = parsed.id || owner;
179
+ const conversation = sandboxInboundConversation(String(this.#options.id), {
180
+ type: parsed.type,
181
+ id: sender,
182
+ });
182
183
  logger.debug(formatCompact({
183
184
  op: 'sandbox_recv',
184
185
  target,
185
- sender: parsed.id || owner,
186
+ sender,
186
187
  channelType: parsed.type,
187
- channelId: parsed.id || owner,
188
+ channelId: sender,
188
189
  text: parsed.text.slice(0, 80),
189
190
  }));
190
191
  // Don't gate on #open — inbound must always reach the gateway so
191
192
  // Command/AI dispatch and outbound replies work.
192
193
  void this.#options.gateway.receive({
193
- adapter: this.#options.id,
194
- target,
194
+ conversation,
195
195
  content: parsed.text,
196
- sender: parsed.id || owner,
196
+ sender,
197
197
  metadata: Object.freeze({
198
198
  type: parsed.type,
199
199
  channelType: parsed.type,
package/lib/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { bindSandboxWsSocket, formatSandboxOutbound, normalizeSandboxOutboundSegments, parseSandboxWsPayload, resolveSandboxEndpoint, whenWsOpen, type MessageElement, type MessageType, type ResolvedSandboxBot, type SandboxAdapterConfig, type SandboxWsSocket, } from './protocol.js';
1
+ export { bindSandboxWsSocket, formatSandboxOutbound, normalizeSandboxOutboundSegments, parseSandboxWsPayload, resolveSandboxEndpoint, sandboxInboundConversation, whenWsOpen, type MessageElement, type MessageType, type ResolvedSandboxBot, type SandboxAdapterConfig, type SandboxWsSocket, } from './protocol.js';
2
2
  export { SandboxWsEndpoint, type SandboxEndpointOptions, } from './endpoint.js';
package/lib/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { bindSandboxWsSocket, formatSandboxOutbound, normalizeSandboxOutboundSegments, parseSandboxWsPayload, resolveSandboxEndpoint, whenWsOpen, } from './protocol.js';
1
+ export { bindSandboxWsSocket, formatSandboxOutbound, normalizeSandboxOutboundSegments, parseSandboxWsPayload, resolveSandboxEndpoint, sandboxInboundConversation, whenWsOpen, } from './protocol.js';
2
2
  export { SandboxWsEndpoint, } from './endpoint.js';
package/lib/protocol.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /** Sandbox WebSocket wire protocol helpers (no legacy Adapter/Endpoint). */
2
+ import type { ConversationRef } from '@zhin.js/im-contract';
2
3
  export type MessageType = 'private' | 'group' | 'guild' | 'direct' | 'channel';
3
4
  export interface MessageElement {
4
5
  readonly type: string;
@@ -47,6 +48,15 @@ export declare function parseSandboxWsPayload(raw: string): {
47
48
  payload: string;
48
49
  };
49
50
  };
51
+ /**
52
+ * 入站归一化 → ConversationRef。sandbox 无平台社交图谱:
53
+ * `private`/`group`/`channel` 直映射;`direct`(私聊)归 'private';
54
+ * `guild`(频道容器语义)归 'channel'。无 guild/temp 容器信息,不产生 parent。
55
+ */
56
+ export declare function sandboxInboundConversation(endpointId: string, msg: {
57
+ readonly type: MessageType;
58
+ readonly id: string;
59
+ }): ConversationRef;
50
60
  export type SandboxOutboundChannel = {
51
61
  readonly type?: string;
52
62
  readonly id?: string;
package/lib/protocol.js CHANGED
@@ -107,6 +107,23 @@ export function parseSandboxWsPayload(raw) {
107
107
  }
108
108
  return { type, id, content, timestamp: payload.timestamp ?? Date.now(), text, action };
109
109
  }
110
+ /**
111
+ * 入站归一化 → ConversationRef。sandbox 无平台社交图谱:
112
+ * `private`/`group`/`channel` 直映射;`direct`(私聊)归 'private';
113
+ * `guild`(频道容器语义)归 'channel'。无 guild/temp 容器信息,不产生 parent。
114
+ */
115
+ export function sandboxInboundConversation(endpointId, msg) {
116
+ const kind = msg.type === 'direct'
117
+ ? 'private'
118
+ : msg.type === 'guild'
119
+ ? 'channel'
120
+ : msg.type;
121
+ return {
122
+ endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
123
+ kind,
124
+ id: msg.id,
125
+ };
126
+ }
110
127
  const MEDIA_SEGMENT_TYPES = new Set(['image', 'audio', 'video', 'file']);
111
128
  /**
112
129
  * base64 内联值:Console UI 的 resolveMediaSrc 只识别 data: / base64://
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-sandbox",
3
- "version": "7.0.1",
3
+ "version": "7.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.1.4",
51
+ "@zhin.js/adapter": "1.1.5",
52
52
  "@zhin.js/client": "2.1.4",
53
53
  "@zhin.js/console-contract": "1.0.0",
54
- "@zhin.js/core": "1.5.1",
55
- "@zhin.js/host-http": "1.0.5",
54
+ "@zhin.js/core": "1.5.2",
55
+ "@zhin.js/host-http": "1.0.6",
56
+ "@zhin.js/im-contract": "1.0.1",
56
57
  "@zhin.js/logger": "1.0.75",
57
- "@zhin.js/page": "1.0.5",
58
- "@zhin.js/plugin-runtime": "1.1.2"
58
+ "@zhin.js/page": "1.0.6",
59
+ "@zhin.js/plugin-runtime": "1.1.3"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@types/react": "^19.2.18",
@@ -63,18 +64,18 @@
63
64
  "typescript": "^6.0.3",
64
65
  "vitest": "^4.1.10",
65
66
  "ws": "^8.21.1",
66
- "@zhin.js/pagemanager": "2.0.11",
67
- "@zhin.js/runtime": "1.0.6"
67
+ "@zhin.js/pagemanager": "2.0.12",
68
+ "@zhin.js/runtime": "1.0.7"
68
69
  },
69
70
  "peerDependencies": {
70
71
  "react": "^19.0.0",
71
- "@zhin.js/adapter": "1.1.4",
72
+ "@zhin.js/adapter": "1.1.5",
72
73
  "@zhin.js/client": "2.1.4",
73
74
  "@zhin.js/console-contract": "1.0.0",
74
- "@zhin.js/core": "1.5.1",
75
- "@zhin.js/host-http": "1.0.5",
76
- "@zhin.js/page": "1.0.5",
77
- "@zhin.js/plugin-runtime": "1.1.2"
75
+ "@zhin.js/core": "1.5.2",
76
+ "@zhin.js/host-http": "1.0.6",
77
+ "@zhin.js/page": "1.0.6",
78
+ "@zhin.js/plugin-runtime": "1.1.3"
78
79
  },
79
80
  "peerDependenciesMeta": {
80
81
  "react": {
package/src/endpoint.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * SandboxWsEndpoint — WebSocket lifecycle and MessageGateway bridge for /sandbox.
3
3
  */
4
4
  import { randomUUID } from 'node:crypto';
5
- import type { EndpointInstance } from '@zhin.js/adapter';
5
+ import type { EndpointInstance, EndpointSendRequest } from '@zhin.js/adapter';
6
6
  import type { MessageGateway } from '@zhin.js/core/runtime';
7
7
  import type { HttpHost, WsConnection } from '@zhin.js/host-http';
8
8
  import { formatCompact, getLogger } from '@zhin.js/logger';
@@ -11,6 +11,7 @@ import {
11
11
  bindSandboxWsSocket,
12
12
  formatSandboxOutbound,
13
13
  parseSandboxWsPayload,
14
+ sandboxInboundConversation,
14
15
  whenWsOpen,
15
16
  type ResolvedSandboxBot,
16
17
  type SandboxWsSocket,
@@ -58,11 +59,6 @@ function claimSandboxWsPath(
58
59
  };
59
60
  }
60
61
 
61
- interface SandboxChannel {
62
- readonly type: string;
63
- readonly id: string;
64
- }
65
-
66
62
  interface SandboxConnection {
67
63
  readonly target: string;
68
64
  readonly owner: string;
@@ -70,11 +66,6 @@ interface SandboxConnection {
70
66
  readonly release: () => void;
71
67
  /** true = 占位连接(尚无真实 WS 客户端),send 命中时按 miss 处理。 */
72
68
  readonly placeholder?: boolean;
73
- /**
74
- * 最近一条入站频道(Console UI 按 type+id 过滤气泡)。
75
- * 出站必须带回,否则回复石沉大海。
76
- */
77
- lastChannel?: SandboxChannel;
78
69
  }
79
70
 
80
71
  export interface SandboxEndpointOptions {
@@ -156,35 +147,38 @@ export class SandboxWsEndpoint implements EndpointInstance {
156
147
  logger.debug(formatCompact({ op: 'sandbox_stopped' }));
157
148
  }
158
149
 
159
- send({ target, payload }: { readonly target: string; readonly payload: unknown }): unknown {
150
+ send({ conversation, payload }: EndpointSendRequest): unknown {
160
151
  if (!this.#open) return undefined;
161
- // Reply target is the connection key (bot name / sandbox-uuid), not private:channelId.
162
- const connection = this.#connections.get(target)
152
+ // Reply targets this endpoint's own live socket (fixed bot name, or the
153
+ // only live random-name connection); conversation carries kind/id stamp.
154
+ const connection = this.#connections.get(this.#options.defaults.name)
163
155
  ?? this.#findLiveConnection();
164
156
  if (!connection) {
165
- logger.debug(formatCompact({ op: 'sandbox_send_miss', target }));
157
+ logger.debug(formatCompact({
158
+ op: 'sandbox_send_miss',
159
+ target: `${conversation.kind}:${conversation.id}`,
160
+ }));
166
161
  return undefined;
167
162
  }
168
163
  if (connection.placeholder) {
169
- logger.debug(formatCompact({ op: 'sandbox_send_placeholder', target }));
164
+ logger.debug(formatCompact({
165
+ op: 'sandbox_send_placeholder',
166
+ target: `${conversation.kind}:${conversation.id}`,
167
+ }));
170
168
  return undefined;
171
169
  }
172
- // Console UI filters by type+id; stamp last inbound channel onto outbound wire.
173
- const channel = connection.lastChannel ?? {
174
- type: 'private',
175
- id: connection.owner,
176
- };
170
+ // Console UI filters by type+id; stamp the conversation onto outbound wire.
177
171
  connection.socket.send(formatSandboxOutbound(payload, {
178
- type: channel.type,
179
- id: channel.id,
172
+ type: conversation.kind,
173
+ id: conversation.id,
180
174
  bot: this.#options.defaults.name,
181
175
  endpoint: connection.target,
182
176
  }));
183
177
  logger.debug(formatCompact({
184
178
  op: 'sandbox_send',
185
179
  target: connection.target,
186
- channelType: channel.type,
187
- channelId: channel.id,
180
+ channelType: conversation.kind,
181
+ channelId: conversation.id,
188
182
  }));
189
183
  return payload;
190
184
  }
@@ -221,28 +215,25 @@ export class SandboxWsEndpoint implements EndpointInstance {
221
215
  const release = bindSandboxWsSocket(socket, {
222
216
  onMessage: (raw) => {
223
217
  const parsed = parseSandboxWsPayload(raw);
224
- const conn = this.#connections.get(target);
225
- if (conn && !conn.placeholder) {
226
- conn.lastChannel = {
227
- type: parsed.type,
228
- id: parsed.id || owner,
229
- };
230
- }
218
+ const sender = parsed.id || owner;
219
+ const conversation = sandboxInboundConversation(String(this.#options.id), {
220
+ type: parsed.type,
221
+ id: sender,
222
+ });
231
223
  logger.debug(formatCompact({
232
224
  op: 'sandbox_recv',
233
225
  target,
234
- sender: parsed.id || owner,
226
+ sender,
235
227
  channelType: parsed.type,
236
- channelId: parsed.id || owner,
228
+ channelId: sender,
237
229
  text: parsed.text.slice(0, 80),
238
230
  }));
239
231
  // Don't gate on #open — inbound must always reach the gateway so
240
232
  // Command/AI dispatch and outbound replies work.
241
233
  void this.#options.gateway.receive({
242
- adapter: this.#options.id,
243
- target,
234
+ conversation,
244
235
  content: parsed.text,
245
- sender: parsed.id || owner,
236
+ sender,
246
237
  metadata: Object.freeze({
247
238
  type: parsed.type,
248
239
  channelType: parsed.type,
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export {
4
4
  normalizeSandboxOutboundSegments,
5
5
  parseSandboxWsPayload,
6
6
  resolveSandboxEndpoint,
7
+ sandboxInboundConversation,
7
8
  whenWsOpen,
8
9
  type MessageElement,
9
10
  type MessageType,
package/src/protocol.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { isMediaRef } from '@zhin.js/core';
5
+ import type { ConversationKind, ConversationRef } from '@zhin.js/im-contract';
5
6
  import { formatCompact, getLogger } from '@zhin.js/logger';
6
7
 
7
8
  const logger = getLogger('sandbox');
@@ -177,6 +178,27 @@ export function parseSandboxWsPayload(raw: string): {
177
178
  return { type, id, content, timestamp: payload.timestamp ?? Date.now(), text, action };
178
179
  }
179
180
 
181
+ /**
182
+ * 入站归一化 → ConversationRef。sandbox 无平台社交图谱:
183
+ * `private`/`group`/`channel` 直映射;`direct`(私聊)归 'private';
184
+ * `guild`(频道容器语义)归 'channel'。无 guild/temp 容器信息,不产生 parent。
185
+ */
186
+ export function sandboxInboundConversation(
187
+ endpointId: string,
188
+ msg: { readonly type: MessageType; readonly id: string },
189
+ ): ConversationRef {
190
+ const kind: ConversationKind = msg.type === 'direct'
191
+ ? 'private'
192
+ : msg.type === 'guild'
193
+ ? 'channel'
194
+ : msg.type;
195
+ return {
196
+ endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
197
+ kind,
198
+ id: msg.id,
199
+ };
200
+ }
201
+
180
202
  export type SandboxOutboundChannel = {
181
203
  readonly type?: string;
182
204
  readonly id?: string;