@zhin.js/adapter-sandbox 7.0.2 → 7.0.4

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,44 @@
1
1
  # @zhin.js/adapter-process
2
2
 
3
+ ## 7.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [0de46a8]
8
+ - Updated dependencies [c106ecc]
9
+ - Updated dependencies [b0f37ae]
10
+ - Updated dependencies [daffd4c]
11
+ - Updated dependencies [36c7400]
12
+ - Updated dependencies [162fa34]
13
+ - Updated dependencies [e40b048]
14
+ - Updated dependencies [f1708c3]
15
+ - Updated dependencies [e53444f]
16
+ - Updated dependencies [92b0dd7]
17
+ - Updated dependencies [a7df753]
18
+ - @zhin.js/host-http@1.0.8
19
+ - @zhin.js/im-contract@1.0.3
20
+ - @zhin.js/adapter@1.1.7
21
+ - @zhin.js/plugin-runtime@1.1.5
22
+ - @zhin.js/core@1.5.4
23
+ - @zhin.js/page@1.0.8
24
+ - @zhin.js/client@2.1.6
25
+
26
+ ## 7.0.3
27
+
28
+ ### Patch Changes
29
+
30
+ - f8c7a54: fix: im
31
+ - Updated dependencies [f8c7a54]
32
+ - @zhin.js/logger@1.0.76
33
+ - @zhin.js/client@2.1.5
34
+ - @zhin.js/page@1.0.7
35
+ - @zhin.js/console-contract@1.0.1
36
+ - @zhin.js/host-http@1.0.7
37
+ - @zhin.js/adapter@1.1.6
38
+ - @zhin.js/core@1.5.3
39
+ - @zhin.js/im-contract@1.0.2
40
+ - @zhin.js/plugin-runtime@1.1.4
41
+
3
42
  ## 7.0.2
4
43
 
5
44
  ### Patch Changes
package/lib/endpoint.d.ts CHANGED
@@ -16,7 +16,7 @@ export interface SandboxEndpointOptions {
16
16
  export declare class SandboxWsEndpoint implements EndpointInstance {
17
17
  #private;
18
18
  constructor(options: SandboxEndpointOptions);
19
- /** Live endpoint name (config `name`) — Console endpoint.list/resolve uses it. */
19
+ /** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
20
20
  get name(): string;
21
21
  start(): void;
22
22
  open(): void;
package/lib/endpoint.js CHANGED
@@ -2,9 +2,8 @@
2
2
  * SandboxWsEndpoint — WebSocket lifecycle and MessageGateway bridge for /sandbox.
3
3
  */
4
4
  import { randomUUID } from 'node:crypto';
5
- import { formatCompact, getLogger } from '@zhin.js/logger';
5
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
6
6
  import { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, sandboxInboundConversation, whenWsOpen, } from './protocol.js';
7
- const logger = getLogger('sandbox');
8
7
  /**
9
8
  * 多 sandbox endpoint 共用同一个 HttpHost 时,同 path 的所有 WS listener
10
9
  * 都会被回调(入站重复、出站互窜)。按 endpoint 名隔离挂载路径:
@@ -44,6 +43,7 @@ function claimSandboxWsPath(http, name) {
44
43
  * 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
45
44
  */
46
45
  export class SandboxWsEndpoint {
46
+ #logger;
47
47
  #options;
48
48
  #connections = new Map();
49
49
  #wsHandleRelease;
@@ -52,18 +52,19 @@ export class SandboxWsEndpoint {
52
52
  #open = false;
53
53
  #started = false;
54
54
  constructor(options) {
55
+ this.#logger = getAdapterLogger('sandbox', options.defaults.id);
55
56
  this.#options = options;
56
57
  }
57
- /** Live endpoint name (config `name`) — Console endpoint.list/resolve uses it. */
58
+ /** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
58
59
  get name() {
59
- return this.#options.defaults.name;
60
+ return this.#options.defaults.id;
60
61
  }
61
62
  start() {
62
63
  if (this.#started)
63
64
  return;
64
65
  this.#started = true;
65
66
  // 多 endpoint 同 path 会被全部回调(入站重复、出站互窜),按名隔离。
66
- const claim = claimSandboxWsPath(this.#options.http, this.#options.defaults.name);
67
+ const claim = claimSandboxWsPath(this.#options.http, this.#options.defaults.id);
67
68
  this.#wsPathRelease = claim.release;
68
69
  this.#wsPath = claim.path;
69
70
  const handle = this.#options.http.ws(claim.path);
@@ -71,13 +72,9 @@ export class SandboxWsEndpoint {
71
72
  this.#acceptConnection(connection);
72
73
  });
73
74
  if (!this.#options.defaults.randomNamePerConnection) {
74
- this.#ensurePlaceholder(this.#options.defaults.name, this.#options.defaults.owner);
75
+ this.#ensurePlaceholder(this.#options.defaults.id, this.#options.defaults.owner);
75
76
  }
76
- logger.info(formatCompact({
77
- op: 'sandbox_ws_mounted',
78
- path: claim.path,
79
- endpoint: this.#options.defaults.name,
80
- }));
77
+ this.#logger.info(`ws mounted ${claim.path}`);
81
78
  }
82
79
  open() {
83
80
  this.#open = true;
@@ -104,24 +101,24 @@ export class SandboxWsEndpoint {
104
101
  }
105
102
  this.#connections.clear();
106
103
  this.#started = false;
107
- logger.debug(formatCompact({ op: 'sandbox_stopped' }));
104
+ this.#logger.debug(formatCompact({ op: 'sandbox_stopped' }));
108
105
  }
109
106
  send({ conversation, payload }) {
110
107
  if (!this.#open)
111
108
  return undefined;
112
109
  // Reply targets this endpoint's own live socket (fixed bot name, or the
113
110
  // only live random-name connection); conversation carries kind/id stamp.
114
- const connection = this.#connections.get(this.#options.defaults.name)
111
+ const connection = this.#connections.get(this.#options.defaults.id)
115
112
  ?? this.#findLiveConnection();
116
113
  if (!connection) {
117
- logger.debug(formatCompact({
114
+ this.#logger.debug(formatCompact({
118
115
  op: 'sandbox_send_miss',
119
116
  target: `${conversation.kind}:${conversation.id}`,
120
117
  }));
121
118
  return undefined;
122
119
  }
123
120
  if (connection.placeholder) {
124
- logger.debug(formatCompact({
121
+ this.#logger.debug(formatCompact({
125
122
  op: 'sandbox_send_placeholder',
126
123
  target: `${conversation.kind}:${conversation.id}`,
127
124
  }));
@@ -131,10 +128,10 @@ export class SandboxWsEndpoint {
131
128
  connection.socket.send(formatSandboxOutbound(payload, {
132
129
  type: conversation.kind,
133
130
  id: conversation.id,
134
- bot: this.#options.defaults.name,
131
+ bot: this.#options.defaults.id,
135
132
  endpoint: connection.target,
136
133
  }));
137
- logger.debug(formatCompact({
134
+ this.#logger.debug(formatCompact({
138
135
  op: 'sandbox_send',
139
136
  target: connection.target,
140
137
  channelType: conversation.kind,
@@ -153,7 +150,7 @@ export class SandboxWsEndpoint {
153
150
  #acceptConnection(connection) {
154
151
  const target = this.#options.defaults.randomNamePerConnection
155
152
  ? `sandbox-${randomUUID().slice(0, 8)}`
156
- : this.#options.defaults.name;
153
+ : this.#options.defaults.id;
157
154
  const owner = this.#options.defaults.owner;
158
155
  const socket = connection.socket;
159
156
  // Fixed-name mode reuses `target`; dropping the prior entry without
@@ -180,7 +177,7 @@ export class SandboxWsEndpoint {
180
177
  type: parsed.type,
181
178
  id: sender,
182
179
  });
183
- logger.debug(formatCompact({
180
+ this.#logger.debug(formatCompact({
184
181
  op: 'sandbox_recv',
185
182
  target,
186
183
  sender,
@@ -193,18 +190,18 @@ export class SandboxWsEndpoint {
193
190
  void this.#options.gateway.receive({
194
191
  conversation,
195
192
  content: parsed.text,
196
- sender,
193
+ sender: { id: sender },
194
+ endpointId: target,
197
195
  metadata: Object.freeze({
198
196
  type: parsed.type,
199
197
  channelType: parsed.type,
200
198
  channelId: parsed.id || owner,
201
- endpoint: target,
202
199
  elements: parsed.content,
203
200
  timestamp: parsed.timestamp,
204
201
  ...(parsed.action ? { action: parsed.action } : {}),
205
202
  }),
206
203
  }).catch((err) => {
207
- logger.warn(formatCompact({
204
+ this.#logger.warn(formatCompact({
208
205
  op: 'sandbox_gateway_receive_failed',
209
206
  target,
210
207
  error: err instanceof Error ? err.message : String(err),
@@ -217,11 +214,11 @@ export class SandboxWsEndpoint {
217
214
  const current = this.#connections.get(target);
218
215
  if (current && current.socket === socket) {
219
216
  this.#connections.delete(target);
220
- logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
217
+ this.#logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
221
218
  }
222
219
  },
223
220
  onError: (err) => {
224
- logger.warn(formatCompact({
221
+ this.#logger.warn(formatCompact({
225
222
  op: 'sandbox_ws_error',
226
223
  target,
227
224
  error: err instanceof Error ? err.message : String(err),
@@ -229,7 +226,7 @@ export class SandboxWsEndpoint {
229
226
  },
230
227
  });
231
228
  this.#connections.set(target, { target, owner, socket, release });
232
- logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
229
+ this.#logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
233
230
  if (!this.#options.defaults.randomNamePerConnection) {
234
231
  const readyPayload = JSON.stringify({
235
232
  type: 'ready',
package/lib/protocol.d.ts CHANGED
@@ -15,19 +15,19 @@ export interface SandboxWsSocket {
15
15
  }
16
16
  export type ResolvedSandboxBot = {
17
17
  readonly context: 'sandbox';
18
- readonly name: string;
18
+ readonly id: string;
19
19
  readonly owner: string;
20
20
  readonly randomNamePerConnection: boolean;
21
21
  };
22
22
  export interface SandboxAdapterConfig {
23
23
  /** Runtime expands `endpoints[i]` onto the top level — prefer these. */
24
24
  readonly context?: string;
25
- readonly name?: string;
25
+ readonly id?: string;
26
26
  readonly owner?: string;
27
27
  /** Legacy shape: endpoint entries nested under `endpoints[]`. */
28
28
  readonly endpoints?: ReadonlyArray<{
29
29
  readonly context?: string;
30
- readonly name?: string;
30
+ readonly id?: string;
31
31
  readonly owner?: string;
32
32
  }>;
33
33
  }
@@ -53,7 +53,7 @@ export declare function parseSandboxWsPayload(raw: string): {
53
53
  * `private`/`group`/`channel` 直映射;`direct`(私聊)归 'private';
54
54
  * `guild`(频道容器语义)归 'channel'。无 guild/temp 容器信息,不产生 parent。
55
55
  */
56
- export declare function sandboxInboundConversation(endpointId: string, msg: {
56
+ export declare function sandboxInboundConversation(endpointKey: string, msg: {
57
57
  readonly type: MessageType;
58
58
  readonly id: string;
59
59
  }): ConversationRef;
package/lib/protocol.js CHANGED
@@ -5,19 +5,19 @@ import { formatCompact, getLogger } from '@zhin.js/logger';
5
5
  const logger = getLogger('sandbox');
6
6
  export function resolveSandboxEndpoint(appConfig) {
7
7
  const entry = appConfig.endpoints?.find((item) => item.context === 'sandbox');
8
- const fixedName = typeof appConfig.name === 'string' && appConfig.name
9
- ? appConfig.name
10
- : typeof entry?.name === 'string' && entry.name
11
- ? entry.name
8
+ const fixedName = typeof appConfig.id === 'string' && appConfig.id
9
+ ? appConfig.id
10
+ : typeof entry?.id === 'string' && entry.id
11
+ ? entry.id
12
12
  : undefined;
13
- const name = fixedName || process.env.SANDBOX_BOT_NAME || 'sandbox-bot';
13
+ const id = fixedName || process.env.SANDBOX_BOT_NAME || 'sandbox-bot';
14
14
  const owner = (typeof appConfig.owner === 'string' && appConfig.owner)
15
15
  || (typeof entry?.owner === 'string' && entry.owner)
16
16
  || process.env.SANDBOX_BOT_OWNER
17
17
  || 'sandbox-user';
18
18
  return {
19
19
  context: 'sandbox',
20
- name,
20
+ id,
21
21
  owner,
22
22
  randomNamePerConnection: !fixedName,
23
23
  };
@@ -112,14 +112,14 @@ export function parseSandboxWsPayload(raw) {
112
112
  * `private`/`group`/`channel` 直映射;`direct`(私聊)归 'private';
113
113
  * `guild`(频道容器语义)归 'channel'。无 guild/temp 容器信息,不产生 parent。
114
114
  */
115
- export function sandboxInboundConversation(endpointId, msg) {
115
+ export function sandboxInboundConversation(endpointKey, msg) {
116
116
  const kind = msg.type === 'direct'
117
117
  ? 'private'
118
118
  : msg.type === 'guild'
119
119
  ? 'channel'
120
120
  : msg.type;
121
121
  return {
122
- endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
122
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
123
123
  kind,
124
124
  id: msg.id,
125
125
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-sandbox",
3
- "version": "7.0.2",
3
+ "version": "7.0.4",
4
4
  "description": "Zhin.js Sandbox adapter for Plugin Runtime (WebSocket /sandbox)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -48,15 +48,15 @@
48
48
  "dependencies": {
49
49
  "lucide-react": "^0.525.0",
50
50
  "react": "^19.2.8",
51
- "@zhin.js/adapter": "1.1.5",
52
- "@zhin.js/client": "2.1.4",
53
- "@zhin.js/console-contract": "1.0.0",
54
- "@zhin.js/core": "1.5.2",
55
- "@zhin.js/host-http": "1.0.6",
56
- "@zhin.js/im-contract": "1.0.1",
57
- "@zhin.js/logger": "1.0.75",
58
- "@zhin.js/page": "1.0.6",
59
- "@zhin.js/plugin-runtime": "1.1.3"
51
+ "@zhin.js/adapter": "1.1.7",
52
+ "@zhin.js/client": "2.1.6",
53
+ "@zhin.js/console-contract": "1.0.1",
54
+ "@zhin.js/core": "1.5.4",
55
+ "@zhin.js/host-http": "1.0.8",
56
+ "@zhin.js/im-contract": "1.0.3",
57
+ "@zhin.js/logger": "1.0.76",
58
+ "@zhin.js/page": "1.0.8",
59
+ "@zhin.js/plugin-runtime": "1.1.5"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/react": "^19.2.18",
@@ -64,18 +64,18 @@
64
64
  "typescript": "^6.0.3",
65
65
  "vitest": "^4.1.10",
66
66
  "ws": "^8.21.1",
67
- "@zhin.js/pagemanager": "2.0.12",
68
- "@zhin.js/runtime": "1.0.7"
67
+ "@zhin.js/pagemanager": "2.0.14",
68
+ "@zhin.js/runtime": "1.0.9"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "^19.0.0",
72
- "@zhin.js/adapter": "1.1.5",
73
- "@zhin.js/client": "2.1.4",
74
- "@zhin.js/console-contract": "1.0.0",
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"
72
+ "@zhin.js/adapter": "1.1.7",
73
+ "@zhin.js/client": "2.1.6",
74
+ "@zhin.js/console-contract": "1.0.1",
75
+ "@zhin.js/core": "1.5.4",
76
+ "@zhin.js/host-http": "1.0.8",
77
+ "@zhin.js/page": "1.0.8",
78
+ "@zhin.js/plugin-runtime": "1.1.5"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "react": {
@@ -15,7 +15,7 @@ export default function Sandbox() {
15
15
  const [faceList, setFaceList] = useState([]);
16
16
  const [activeChannel, setActiveChannel] = useState(channels[0]);
17
17
  const [inputText, setInputText] = useState('');
18
- const [endpointName, setBotName] = useState('ProcessEndpoint');
18
+ const [endpointId, setBotName] = useState('ProcessEndpoint');
19
19
  const [connected, setConnected] = useState(false);
20
20
  const [showFacePicker, setShowFacePicker] = useState(false);
21
21
  /** 输入区:插入图片 / 视频 / 音频 URL */
@@ -59,7 +59,7 @@ export default function Sandbox() {
59
59
  ? parseTextToSegments(data.content)
60
60
  : Array.isArray(data.content) ? data.content : parseTextToSegments(String(data.content ?? ''));
61
61
  const channelName = data.type === 'private'
62
- ? `私聊-${data.bot || endpointName}`
62
+ ? `私聊-${data.bot || endpointId}`
63
63
  : data.type === 'group'
64
64
  ? `群组-${data.id}`
65
65
  : `频道-${data.id}`;
@@ -74,7 +74,7 @@ export default function Sandbox() {
74
74
  setMessages((prev) => [...prev, {
75
75
  id: data.messageId ?? `bot_${data.timestamp}`, type: 'received', channelType,
76
76
  channelId: data.id, channelName, senderId: 'endpoint',
77
- senderName: data.bot || endpointName, content, timestamp: data.timestamp,
77
+ senderName: data.bot || endpointId, content, timestamp: data.timestamp,
78
78
  }]);
79
79
  };
80
80
  const sendInteractiveAction = (payload) => {
@@ -355,7 +355,7 @@ export default function Sandbox() {
355
355
  return (_jsxs("div", { className: cn("menu-item", isActive && "active"), onClick: () => switchChannel(channel), children: [_jsx("span", { className: "shrink-0", children: getChannelIcon(channel.type) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium truncate", children: channel.name }), _jsx("div", { className: "text-xs text-muted-foreground", children: channel.type === 'private' ? '私聊' : channel.type === 'group' ? '群聊' : '频道' })] }), channel.unread > 0 && _jsx("span", { className: "inline-flex items-center justify-center h-5 min-w-5 rounded-full bg-destructive text-destructive-foreground text-[10px] font-medium px-1", children: channel.unread })] }, channel.id));
356
356
  }), _jsxs("div", { className: "pt-2 mt-2 border-t space-y-1", children: [_jsxs("div", { className: cn("menu-item", viewMode === 'requests' && "active"), onClick: () => { setViewMode('requests'); if (window.innerWidth < 768)
357
357
  setShowChannelList(false); }, children: [_jsx(UserPlus, { size: 16, className: "shrink-0" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium", children: "\u8BF7\u6C42" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "\u597D\u53CB/\u7FA4\u9080\u8BF7\u7B49" })] })] }), _jsxs("div", { className: cn("menu-item", viewMode === 'notices' && "active"), onClick: () => { setViewMode('notices'); if (window.innerWidth < 768)
358
- setShowChannelList(false); }, children: [_jsx(Bell, { size: 16, className: "shrink-0" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium", children: "\u901A\u77E5" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "\u7FA4\u7BA1/\u64A4\u56DE\u7B49" })] })] })] })] }), _jsx("div", { className: "p-2 border-t", children: _jsx("button", { className: "w-full py-2 px-3 rounded-md border border-dashed text-sm text-muted-foreground hover:bg-accent transition-colors", onClick: addChannel, children: "+ \u6DFB\u52A0\u9891\u9053" }) })] }), showChannelList && _jsx("div", { className: "channel-overlay md:hidden", onClick: () => setShowChannelList(false) }), _jsxs("div", { className: "chat-area", children: [viewMode === 'requests' && (_jsxs("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden", children: [_jsx("div", { className: "p-3 border-b flex-shrink-0", children: _jsxs("h2", { className: "text-lg font-bold flex items-center gap-2", children: [_jsx(UserPlus, { size: 20 }), " \u8BF7\u6C42"] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center", children: [_jsx(UserPlus, { size: 48, className: "opacity-30" }), _jsx("span", { children: "\u6C99\u76D2\u4E3A\u6A21\u62DF\u73AF\u5883\uFF0C\u6682\u65E0\u8BF7\u6C42\u6570\u636E" }), _jsxs("span", { className: "text-sm", children: ["\u5B9E\u9645\u597D\u53CB/\u7FA4\u9080\u8BF7\u7B49\u8BF7\u6C42\u8BF7\u5230\u4FA7\u8FB9\u680F ", _jsx("strong", { children: "\u673A\u5668\u4EBA" }), " \u9875\u9762\u8FDB\u5165\u5BF9\u5E94\u673A\u5668\u4EBA\u7BA1\u7406\u67E5\u770B"] })] })] })), viewMode === 'notices' && (_jsxs("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden", children: [_jsx("div", { className: "p-3 border-b flex-shrink-0", children: _jsxs("h2", { className: "text-lg font-bold flex items-center gap-2", children: [_jsx(Bell, { size: 20 }), " \u901A\u77E5"] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center", children: [_jsx(Bell, { size: 48, className: "opacity-30" }), _jsx("span", { children: "\u6C99\u76D2\u4E3A\u6A21\u62DF\u73AF\u5883\uFF0C\u6682\u65E0\u901A\u77E5\u6570\u636E" }), _jsxs("span", { className: "text-sm", children: ["\u5B9E\u9645\u7FA4\u7BA1\u3001\u64A4\u56DE\u7B49\u901A\u77E5\u8BF7\u5230\u4FA7\u8FB9\u680F ", _jsx("strong", { children: "\u673A\u5668\u4EBA" }), " \u9875\u9762\u8FDB\u5165\u5BF9\u5E94\u673A\u5668\u4EBA\u7BA1\u7406\u67E5\u770B"] })] })] })), viewMode === 'chat' && (_jsxs(_Fragment, { children: [_jsx("div", { className: "rounded-lg border bg-card p-3 flex-shrink-0", children: _jsxs("div", { className: "flex justify-between items-center flex-wrap gap-2", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "p-2 rounded-lg bg-secondary", children: getChannelIcon(activeChannel.type) }), _jsxs("div", { children: [_jsx("h2", { className: "text-lg font-bold", children: activeChannel.name }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { children: activeChannel.id }), _jsx("span", { className: "inline-flex items-center px-1.5 py-0.5 rounded border text-[10px]", children: channelMessages.length }), _jsx("span", { children: "\u6761\u6D88\u606F" })] })] }), _jsx("span", { className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-secondary text-secondary-foreground", children: activeChannel.type === 'private' ? '私聊' : activeChannel.type === 'group' ? '群聊' : '频道' })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { value: endpointName, onChange: (e) => setBotName(e.target.value), placeholder: "\u673A\u5668\u4EBA\u540D\u79F0", className: "h-8 w-28 rounded-md border bg-transparent px-2 text-sm" }), _jsxs("button", { className: "inline-flex items-center gap-1 h-8 px-3 rounded-md bg-secondary text-secondary-foreground text-sm hover:bg-secondary/80", onClick: clearMessages, children: [_jsx(Trash2, { size: 14 }), " \u6E05\u7A7A"] })] })] }) }), _jsx("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0", children: _jsx("div", { className: "flex-1 overflow-y-auto p-4", children: channelMessages.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center h-full gap-3", children: [_jsx(MessageSquare, { size: 64, className: "text-muted-foreground/20" }), _jsx("span", { className: "text-muted-foreground", children: "\u6682\u65E0\u6D88\u606F\uFF0C\u5F00\u59CB\u5BF9\u8BDD\u5427\uFF01" })] })) : (_jsxs("div", { className: "space-y-2", children: [channelMessages.map((msg) => (_jsx("div", { className: cn("flex", msg.type === 'sent' ? "justify-end" : "justify-start"), children: _jsxs("div", { className: cn("max-w-[70%] p-3 rounded-2xl", msg.type === 'sent' ? "bg-primary text-primary-foreground" : "bg-muted"), children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [msg.type === 'received' && _jsx(Bot, { size: 14 }), msg.type === 'sent' && _jsx(User, { size: 14 }), _jsx("span", { className: "text-xs font-medium opacity-90", children: msg.senderName }), _jsx("span", { className: "text-xs opacity-70", children: new Date(msg.timestamp).toLocaleTimeString() })] }), _jsx("div", { className: "text-sm space-y-1", children: renderMessageSegments(msg.content, msg.type === 'sent') })] }) }, msg.id))), _jsx("div", { ref: messagesEndRef })] })) }) }), _jsxs("div", { className: "rounded-lg border bg-card p-3 flex-shrink-0 space-y-3", children: [_jsxs("div", { className: "flex gap-2 items-center flex-wrap", children: [_jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", showFacePicker ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setShowFacePicker(!showFacePicker); setMediaPanel(null); }, title: "\u63D2\u5165\u8868\u60C5", children: _jsx(Smile, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'image' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'image' ? null : 'image')); setShowFacePicker(false); }, title: "\u63D2\u5165\u56FE\u7247 URL", children: _jsx(Image, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'video' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'video' ? null : 'video')); setShowFacePicker(false); }, title: "\u63D2\u5165\u89C6\u9891 URL", children: _jsx(Video, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'audio' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'audio' ? null : 'audio')); setShowFacePicker(false); }, title: "\u63D2\u5165\u97F3\u9891 URL", children: _jsx(Music, { size: 16 }) }), _jsx("div", { className: "flex-1 min-w-[1rem]" }), inputText && (_jsx("button", { className: "h-8 w-8 rounded-md flex items-center justify-center hover:bg-accent transition-colors", onClick: () => { setInputText(''); setPreviewSegments([]); }, children: _jsx(X, { size: 16 }) }))] }), showFacePicker && (_jsxs("div", { className: "p-3 rounded-md border bg-muted/30 max-h-64 overflow-y-auto space-y-2", children: [_jsx("input", { value: faceSearchQuery, onChange: (e) => setFaceSearchQuery(e.target.value), placeholder: "\u641C\u7D22\u8868\u60C5...", className: "w-full h-8 rounded-md border bg-transparent px-2 text-sm" }), _jsx("div", { className: "grid grid-cols-8 gap-1", children: filteredFaces.slice(0, 80).map((face) => (_jsx("button", { onClick: () => insertFace(face.id), title: face.name, className: "w-10 h-10 rounded-md border flex items-center justify-center hover:bg-accent transition-colors", children: _jsx("img", { src: `https://face.viki.moe/apng/${face.id}.png`, alt: face.name, className: "w-8 h-8" }) }, face.id))) }), filteredFaces.length === 0 && (_jsxs("div", { className: "flex flex-col items-center gap-2 py-4", children: [_jsx(Search, { size: 32, className: "text-muted-foreground/30" }), _jsx("span", { className: "text-sm text-muted-foreground", children: "\u672A\u627E\u5230\u5339\u914D\u7684\u8868\u60C5" })] }))] })), mediaPanel && (_jsxs("div", { className: "p-3 rounded-md border bg-muted/30 space-y-2", children: [_jsxs("p", { className: "text-xs text-muted-foreground", children: [mediaPanel === 'image' && '支持 http(s) 图片链接或 data URL', mediaPanel === 'video' && '支持浏览器可解码的视频直链(如 .mp4、.webm)', mediaPanel === 'audio' && '支持 .mp3、.ogg、.wav 等音频直链'] }), _jsx("input", { value: mediaUrl, onChange: (e) => setMediaUrl(e.target.value), placeholder: mediaPanel === 'image' ? '图片 URL…' : mediaPanel === 'video' ? '视频 URL…' : '音频 URL…', className: "w-full h-8 rounded-md border border-input bg-background px-2 text-sm", onKeyDown: (e) => { if (e.key === 'Enter') {
358
+ setShowChannelList(false); }, children: [_jsx(Bell, { size: 16, className: "shrink-0" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium", children: "\u901A\u77E5" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "\u7FA4\u7BA1/\u64A4\u56DE\u7B49" })] })] })] })] }), _jsx("div", { className: "p-2 border-t", children: _jsx("button", { className: "w-full py-2 px-3 rounded-md border border-dashed text-sm text-muted-foreground hover:bg-accent transition-colors", onClick: addChannel, children: "+ \u6DFB\u52A0\u9891\u9053" }) })] }), showChannelList && _jsx("div", { className: "channel-overlay md:hidden", onClick: () => setShowChannelList(false) }), _jsxs("div", { className: "chat-area", children: [viewMode === 'requests' && (_jsxs("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden", children: [_jsx("div", { className: "p-3 border-b flex-shrink-0", children: _jsxs("h2", { className: "text-lg font-bold flex items-center gap-2", children: [_jsx(UserPlus, { size: 20 }), " \u8BF7\u6C42"] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center", children: [_jsx(UserPlus, { size: 48, className: "opacity-30" }), _jsx("span", { children: "\u6C99\u76D2\u4E3A\u6A21\u62DF\u73AF\u5883\uFF0C\u6682\u65E0\u8BF7\u6C42\u6570\u636E" }), _jsxs("span", { className: "text-sm", children: ["\u5B9E\u9645\u597D\u53CB/\u7FA4\u9080\u8BF7\u7B49\u8BF7\u6C42\u8BF7\u5230\u4FA7\u8FB9\u680F ", _jsx("strong", { children: "\u673A\u5668\u4EBA" }), " \u9875\u9762\u8FDB\u5165\u5BF9\u5E94\u673A\u5668\u4EBA\u7BA1\u7406\u67E5\u770B"] })] })] })), viewMode === 'notices' && (_jsxs("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden", children: [_jsx("div", { className: "p-3 border-b flex-shrink-0", children: _jsxs("h2", { className: "text-lg font-bold flex items-center gap-2", children: [_jsx(Bell, { size: 20 }), " \u901A\u77E5"] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center", children: [_jsx(Bell, { size: 48, className: "opacity-30" }), _jsx("span", { children: "\u6C99\u76D2\u4E3A\u6A21\u62DF\u73AF\u5883\uFF0C\u6682\u65E0\u901A\u77E5\u6570\u636E" }), _jsxs("span", { className: "text-sm", children: ["\u5B9E\u9645\u7FA4\u7BA1\u3001\u64A4\u56DE\u7B49\u901A\u77E5\u8BF7\u5230\u4FA7\u8FB9\u680F ", _jsx("strong", { children: "\u673A\u5668\u4EBA" }), " \u9875\u9762\u8FDB\u5165\u5BF9\u5E94\u673A\u5668\u4EBA\u7BA1\u7406\u67E5\u770B"] })] })] })), viewMode === 'chat' && (_jsxs(_Fragment, { children: [_jsx("div", { className: "rounded-lg border bg-card p-3 flex-shrink-0", children: _jsxs("div", { className: "flex justify-between items-center flex-wrap gap-2", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "p-2 rounded-lg bg-secondary", children: getChannelIcon(activeChannel.type) }), _jsxs("div", { children: [_jsx("h2", { className: "text-lg font-bold", children: activeChannel.name }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { children: activeChannel.id }), _jsx("span", { className: "inline-flex items-center px-1.5 py-0.5 rounded border text-[10px]", children: channelMessages.length }), _jsx("span", { children: "\u6761\u6D88\u606F" })] })] }), _jsx("span", { className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-secondary text-secondary-foreground", children: activeChannel.type === 'private' ? '私聊' : activeChannel.type === 'group' ? '群聊' : '频道' })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { value: endpointId, onChange: (e) => setBotName(e.target.value), placeholder: "\u673A\u5668\u4EBA\u540D\u79F0", className: "h-8 w-28 rounded-md border bg-transparent px-2 text-sm" }), _jsxs("button", { className: "inline-flex items-center gap-1 h-8 px-3 rounded-md bg-secondary text-secondary-foreground text-sm hover:bg-secondary/80", onClick: clearMessages, children: [_jsx(Trash2, { size: 14 }), " \u6E05\u7A7A"] })] })] }) }), _jsx("div", { className: "rounded-lg border bg-card flex-1 flex flex-col min-h-0", children: _jsx("div", { className: "flex-1 overflow-y-auto p-4", children: channelMessages.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center h-full gap-3", children: [_jsx(MessageSquare, { size: 64, className: "text-muted-foreground/20" }), _jsx("span", { className: "text-muted-foreground", children: "\u6682\u65E0\u6D88\u606F\uFF0C\u5F00\u59CB\u5BF9\u8BDD\u5427\uFF01" })] })) : (_jsxs("div", { className: "space-y-2", children: [channelMessages.map((msg) => (_jsx("div", { className: cn("flex", msg.type === 'sent' ? "justify-end" : "justify-start"), children: _jsxs("div", { className: cn("max-w-[70%] p-3 rounded-2xl", msg.type === 'sent' ? "bg-primary text-primary-foreground" : "bg-muted"), children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [msg.type === 'received' && _jsx(Bot, { size: 14 }), msg.type === 'sent' && _jsx(User, { size: 14 }), _jsx("span", { className: "text-xs font-medium opacity-90", children: msg.senderName }), _jsx("span", { className: "text-xs opacity-70", children: new Date(msg.timestamp).toLocaleTimeString() })] }), _jsx("div", { className: "text-sm space-y-1", children: renderMessageSegments(msg.content, msg.type === 'sent') })] }) }, msg.id))), _jsx("div", { ref: messagesEndRef })] })) }) }), _jsxs("div", { className: "rounded-lg border bg-card p-3 flex-shrink-0 space-y-3", children: [_jsxs("div", { className: "flex gap-2 items-center flex-wrap", children: [_jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", showFacePicker ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setShowFacePicker(!showFacePicker); setMediaPanel(null); }, title: "\u63D2\u5165\u8868\u60C5", children: _jsx(Smile, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'image' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'image' ? null : 'image')); setShowFacePicker(false); }, title: "\u63D2\u5165\u56FE\u7247 URL", children: _jsx(Image, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'video' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'video' ? null : 'video')); setShowFacePicker(false); }, title: "\u63D2\u5165\u89C6\u9891 URL", children: _jsx(Video, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'audio' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'audio' ? null : 'audio')); setShowFacePicker(false); }, title: "\u63D2\u5165\u97F3\u9891 URL", children: _jsx(Music, { size: 16 }) }), _jsx("div", { className: "flex-1 min-w-[1rem]" }), inputText && (_jsx("button", { className: "h-8 w-8 rounded-md flex items-center justify-center hover:bg-accent transition-colors", onClick: () => { setInputText(''); setPreviewSegments([]); }, children: _jsx(X, { size: 16 }) }))] }), showFacePicker && (_jsxs("div", { className: "p-3 rounded-md border bg-muted/30 max-h-64 overflow-y-auto space-y-2", children: [_jsx("input", { value: faceSearchQuery, onChange: (e) => setFaceSearchQuery(e.target.value), placeholder: "\u641C\u7D22\u8868\u60C5...", className: "w-full h-8 rounded-md border bg-transparent px-2 text-sm" }), _jsx("div", { className: "grid grid-cols-8 gap-1", children: filteredFaces.slice(0, 80).map((face) => (_jsx("button", { onClick: () => insertFace(face.id), title: face.name, className: "w-10 h-10 rounded-md border flex items-center justify-center hover:bg-accent transition-colors", children: _jsx("img", { src: `https://face.viki.moe/apng/${face.id}.png`, alt: face.name, className: "w-8 h-8" }) }, face.id))) }), filteredFaces.length === 0 && (_jsxs("div", { className: "flex flex-col items-center gap-2 py-4", children: [_jsx(Search, { size: 32, className: "text-muted-foreground/30" }), _jsx("span", { className: "text-sm text-muted-foreground", children: "\u672A\u627E\u5230\u5339\u914D\u7684\u8868\u60C5" })] }))] })), mediaPanel && (_jsxs("div", { className: "p-3 rounded-md border bg-muted/30 space-y-2", children: [_jsxs("p", { className: "text-xs text-muted-foreground", children: [mediaPanel === 'image' && '支持 http(s) 图片链接或 data URL', mediaPanel === 'video' && '支持浏览器可解码的视频直链(如 .mp4、.webm)', mediaPanel === 'audio' && '支持 .mp3、.ogg、.wav 等音频直链'] }), _jsx("input", { value: mediaUrl, onChange: (e) => setMediaUrl(e.target.value), placeholder: mediaPanel === 'image' ? '图片 URL…' : mediaPanel === 'video' ? '视频 URL…' : '音频 URL…', className: "w-full h-8 rounded-md border border-input bg-background px-2 text-sm", onKeyDown: (e) => { if (e.key === 'Enter') {
359
359
  e.preventDefault();
360
360
  commitMediaUrl();
361
361
  } } }), _jsxs("button", { type: "button", className: "inline-flex items-center gap-1 h-8 px-3 rounded-md bg-primary text-primary-foreground text-sm disabled:opacity-50", onClick: commitMediaUrl, disabled: !mediaUrl.trim(), children: [_jsx(Check, { size: 14 }), " \u63D2\u5165\u5230\u8F93\u5165\u6846"] })] })), _jsxs("div", { className: "flex gap-2 items-start", children: [_jsxs("div", { className: "flex-1 relative", children: [_jsx(RichTextEditor, { ref: editorRef, placeholder: `向 ${activeChannel.name} 发送消息...`, onSend: handleSendMessage, onChange: handleEditorChange, onAtTrigger: handleAtTrigger, minHeight: "44px", maxHeight: "200px" }), atPopoverPosition && (_jsx("div", { className: "absolute z-50 rounded-lg border bg-popover shadow-md min-w-60 max-h-72 overflow-y-auto p-1", style: { top: `${atPopoverPosition.top}px`, left: `${atPopoverPosition.left}px` }, children: filteredAtSuggestions.length > 0 ? filteredAtSuggestions.map((user) => (_jsxs("div", { className: "flex items-center gap-2 p-2 rounded-md cursor-pointer hover:bg-accent transition-colors", onClick: () => selectAtUser(user), children: [_jsx(User, { size: 16, className: "text-muted-foreground" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-sm font-medium", children: user.name }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["ID: ", user.id] })] })] }, user.id))) : (_jsxs("div", { className: "flex flex-col items-center gap-2 p-4", children: [_jsx(Search, { size: 20, className: "text-muted-foreground/50" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "\u672A\u627E\u5230\u5339\u914D\u7684\u7528\u6237" })] })) }))] }), _jsxs("button", { className: "inline-flex items-center gap-1.5 h-10 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50 transition-colors hover:bg-primary/90", onClick: () => { const c = editorRef.current?.getContent(); if (c)
@@ -32,7 +32,7 @@ export default function Sandbox() {
32
32
  const [faceList, setFaceList] = useState<Face[]>([])
33
33
  const [activeChannel, setActiveChannel] = useState<Channel>(channels[0])
34
34
  const [inputText, setInputText] = useState('')
35
- const [endpointName, setBotName] = useState('ProcessEndpoint')
35
+ const [endpointId, setBotName] = useState('ProcessEndpoint')
36
36
  const [connected, setConnected] = useState(false)
37
37
  const [showFacePicker, setShowFacePicker] = useState(false)
38
38
  /** 输入区:插入图片 / 视频 / 音频 URL */
@@ -79,7 +79,7 @@ export default function Sandbox() {
79
79
  : Array.isArray(data.content) ? data.content as MessageSegment[] : parseTextToSegments(String(data.content ?? ''))
80
80
 
81
81
  const channelName = data.type === 'private'
82
- ? `私聊-${data.bot || endpointName}`
82
+ ? `私聊-${data.bot || endpointId}`
83
83
  : data.type === 'group'
84
84
  ? `群组-${data.id}`
85
85
  : `频道-${data.id}`
@@ -95,7 +95,7 @@ export default function Sandbox() {
95
95
  setMessages((prev) => [...prev, {
96
96
  id: data.messageId ?? `bot_${data.timestamp}`, type: 'received', channelType,
97
97
  channelId: data.id, channelName, senderId: 'endpoint',
98
- senderName: data.bot || endpointName, content, timestamp: data.timestamp,
98
+ senderName: data.bot || endpointId, content, timestamp: data.timestamp,
99
99
  }])
100
100
  }
101
101
 
@@ -509,7 +509,7 @@ export default function Sandbox() {
509
509
  </span>
510
510
  </div>
511
511
  <div className="flex items-center gap-2">
512
- <input value={endpointName} onChange={(e) => setBotName(e.target.value)} placeholder="机器人名称"
512
+ <input value={endpointId} onChange={(e) => setBotName(e.target.value)} placeholder="机器人名称"
513
513
  className="h-8 w-28 rounded-md border bg-transparent px-2 text-sm" />
514
514
  <button className="inline-flex items-center gap-1 h-8 px-3 rounded-md bg-secondary text-secondary-foreground text-sm hover:bg-secondary/80" onClick={clearMessages}>
515
515
  <Trash2 size={14} /> 清空
package/schema.json CHANGED
@@ -3,16 +3,48 @@
3
3
  "type": "object",
4
4
  "additionalProperties": false,
5
5
  "properties": {
6
+ "master": {
7
+ "type": [
8
+ "string",
9
+ "number"
10
+ ],
11
+ "description": "框架 master(sandbox client id (distinct from endpoints[].owner);AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
12
+ },
13
+ "trusted": {
14
+ "type": "array",
15
+ "items": {
16
+ "type": [
17
+ "string",
18
+ "number"
19
+ ],
20
+ "description": "Trusted sandbox client id (distinct from endpoints[].owner)"
21
+ },
22
+ "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
23
+ },
6
24
  "endpoints": {
7
25
  "type": "array",
8
- "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(name 必填,其余覆盖顶层)",
26
+ "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
9
27
  "items": {
10
28
  "type": "object",
11
29
  "additionalProperties": true,
12
30
  "properties": {
13
- "name": {
14
- "type": "string",
15
- "description": "Sandbox bot name"
31
+ "master": {
32
+ "type": [
33
+ "string",
34
+ "number"
35
+ ],
36
+ "description": "本 endpoint 的框架 master(sandbox client id (distinct from endpoints[].owner));覆盖顶层 master"
37
+ },
38
+ "trusted": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": [
42
+ "string",
43
+ "number"
44
+ ],
45
+ "description": "Trusted sandbox client id (distinct from endpoints[].owner)"
46
+ },
47
+ "description": "本 endpoint 的 trusted 列表"
16
48
  },
17
49
  "context": {
18
50
  "type": "string",
@@ -21,10 +53,14 @@
21
53
  "owner": {
22
54
  "type": "string",
23
55
  "description": "Sandbox owner user ID"
56
+ },
57
+ "id": {
58
+ "type": "string",
59
+ "description": "Sandbox bot name"
24
60
  }
25
61
  },
26
62
  "required": [
27
- "name"
63
+ "id"
28
64
  ]
29
65
  }
30
66
  },
package/src/endpoint.ts CHANGED
@@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto';
5
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
- import { formatCompact, getLogger } from '@zhin.js/logger';
8
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
9
9
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
10
10
  import {
11
11
  bindSandboxWsSocket,
@@ -17,8 +17,6 @@ import {
17
17
  type SandboxWsSocket,
18
18
  } from './protocol.js';
19
19
 
20
- const logger = getLogger('sandbox');
21
-
22
20
  /**
23
21
  * 多 sandbox endpoint 共用同一个 HttpHost 时,同 path 的所有 WS listener
24
22
  * 都会被回调(入站重复、出站互窜)。按 endpoint 名隔离挂载路径:
@@ -80,6 +78,8 @@ export interface SandboxEndpointOptions {
80
78
  * 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
81
79
  */
82
80
  export class SandboxWsEndpoint implements EndpointInstance {
81
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
82
+
83
83
  readonly #options: SandboxEndpointOptions;
84
84
  readonly #connections = new Map<string, SandboxConnection>();
85
85
  #wsHandleRelease?: () => void;
@@ -89,19 +89,20 @@ export class SandboxWsEndpoint implements EndpointInstance {
89
89
  #started = false;
90
90
 
91
91
  constructor(options: SandboxEndpointOptions) {
92
+ this.#logger = getAdapterLogger('sandbox', options.defaults.id);
92
93
  this.#options = options;
93
94
  }
94
95
 
95
- /** Live endpoint name (config `name`) — Console endpoint.list/resolve uses it. */
96
+ /** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
96
97
  get name(): string {
97
- return this.#options.defaults.name;
98
+ return this.#options.defaults.id;
98
99
  }
99
100
 
100
101
  start(): void {
101
102
  if (this.#started) return;
102
103
  this.#started = true;
103
104
  // 多 endpoint 同 path 会被全部回调(入站重复、出站互窜),按名隔离。
104
- const claim = claimSandboxWsPath(this.#options.http, this.#options.defaults.name);
105
+ const claim = claimSandboxWsPath(this.#options.http, this.#options.defaults.id);
105
106
  this.#wsPathRelease = claim.release;
106
107
  this.#wsPath = claim.path;
107
108
  const handle = this.#options.http.ws(claim.path);
@@ -109,13 +110,9 @@ export class SandboxWsEndpoint implements EndpointInstance {
109
110
  this.#acceptConnection(connection);
110
111
  });
111
112
  if (!this.#options.defaults.randomNamePerConnection) {
112
- this.#ensurePlaceholder(this.#options.defaults.name, this.#options.defaults.owner);
113
+ this.#ensurePlaceholder(this.#options.defaults.id, this.#options.defaults.owner);
113
114
  }
114
- logger.info(formatCompact({
115
- op: 'sandbox_ws_mounted',
116
- path: claim.path,
117
- endpoint: this.#options.defaults.name,
118
- }));
115
+ this.#logger.info(`ws mounted ${claim.path}`);
119
116
  }
120
117
 
121
118
  open(): void {
@@ -144,24 +141,24 @@ export class SandboxWsEndpoint implements EndpointInstance {
144
141
  }
145
142
  this.#connections.clear();
146
143
  this.#started = false;
147
- logger.debug(formatCompact({ op: 'sandbox_stopped' }));
144
+ this.#logger.debug(formatCompact({ op: 'sandbox_stopped' }));
148
145
  }
149
146
 
150
147
  send({ conversation, payload }: EndpointSendRequest): unknown {
151
148
  if (!this.#open) return undefined;
152
149
  // Reply targets this endpoint's own live socket (fixed bot name, or the
153
150
  // only live random-name connection); conversation carries kind/id stamp.
154
- const connection = this.#connections.get(this.#options.defaults.name)
151
+ const connection = this.#connections.get(this.#options.defaults.id)
155
152
  ?? this.#findLiveConnection();
156
153
  if (!connection) {
157
- logger.debug(formatCompact({
154
+ this.#logger.debug(formatCompact({
158
155
  op: 'sandbox_send_miss',
159
156
  target: `${conversation.kind}:${conversation.id}`,
160
157
  }));
161
158
  return undefined;
162
159
  }
163
160
  if (connection.placeholder) {
164
- logger.debug(formatCompact({
161
+ this.#logger.debug(formatCompact({
165
162
  op: 'sandbox_send_placeholder',
166
163
  target: `${conversation.kind}:${conversation.id}`,
167
164
  }));
@@ -171,10 +168,10 @@ export class SandboxWsEndpoint implements EndpointInstance {
171
168
  connection.socket.send(formatSandboxOutbound(payload, {
172
169
  type: conversation.kind,
173
170
  id: conversation.id,
174
- bot: this.#options.defaults.name,
171
+ bot: this.#options.defaults.id,
175
172
  endpoint: connection.target,
176
173
  }));
177
- logger.debug(formatCompact({
174
+ this.#logger.debug(formatCompact({
178
175
  op: 'sandbox_send',
179
176
  target: connection.target,
180
177
  channelType: conversation.kind,
@@ -194,7 +191,7 @@ export class SandboxWsEndpoint implements EndpointInstance {
194
191
  #acceptConnection(connection: WsConnection): void {
195
192
  const target = this.#options.defaults.randomNamePerConnection
196
193
  ? `sandbox-${randomUUID().slice(0, 8)}`
197
- : this.#options.defaults.name;
194
+ : this.#options.defaults.id;
198
195
  const owner = this.#options.defaults.owner;
199
196
  const socket = connection.socket as SandboxWsSocket;
200
197
  // Fixed-name mode reuses `target`; dropping the prior entry without
@@ -220,7 +217,7 @@ export class SandboxWsEndpoint implements EndpointInstance {
220
217
  type: parsed.type,
221
218
  id: sender,
222
219
  });
223
- logger.debug(formatCompact({
220
+ this.#logger.debug(formatCompact({
224
221
  op: 'sandbox_recv',
225
222
  target,
226
223
  sender,
@@ -233,18 +230,18 @@ export class SandboxWsEndpoint implements EndpointInstance {
233
230
  void this.#options.gateway.receive({
234
231
  conversation,
235
232
  content: parsed.text,
236
- sender,
233
+ sender: { id: sender },
234
+ endpointId: target,
237
235
  metadata: Object.freeze({
238
236
  type: parsed.type,
239
237
  channelType: parsed.type,
240
238
  channelId: parsed.id || owner,
241
- endpoint: target,
242
239
  elements: parsed.content,
243
240
  timestamp: parsed.timestamp,
244
241
  ...(parsed.action ? { action: parsed.action } : {}),
245
242
  }),
246
243
  }).catch((err) => {
247
- logger.warn(formatCompact({
244
+ this.#logger.warn(formatCompact({
248
245
  op: 'sandbox_gateway_receive_failed',
249
246
  target,
250
247
  error: err instanceof Error ? err.message : String(err),
@@ -257,11 +254,11 @@ export class SandboxWsEndpoint implements EndpointInstance {
257
254
  const current = this.#connections.get(target);
258
255
  if (current && current.socket === socket) {
259
256
  this.#connections.delete(target);
260
- logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
257
+ this.#logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
261
258
  }
262
259
  },
263
260
  onError: (err) => {
264
- logger.warn(formatCompact({
261
+ this.#logger.warn(formatCompact({
265
262
  op: 'sandbox_ws_error',
266
263
  target,
267
264
  error: err instanceof Error ? err.message : String(err),
@@ -269,7 +266,7 @@ export class SandboxWsEndpoint implements EndpointInstance {
269
266
  },
270
267
  });
271
268
  this.#connections.set(target, { target, owner, socket, release });
272
- logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
269
+ this.#logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
273
270
  if (!this.#options.defaults.randomNamePerConnection) {
274
271
  const readyPayload = JSON.stringify({
275
272
  type: 'ready',
package/src/protocol.ts CHANGED
@@ -34,7 +34,7 @@ export interface SandboxWsSocket {
34
34
 
35
35
  export type ResolvedSandboxBot = {
36
36
  readonly context: 'sandbox';
37
- readonly name: string;
37
+ readonly id: string;
38
38
  readonly owner: string;
39
39
  readonly randomNamePerConnection: boolean;
40
40
  };
@@ -42,12 +42,12 @@ export type ResolvedSandboxBot = {
42
42
  export interface SandboxAdapterConfig {
43
43
  /** Runtime expands `endpoints[i]` onto the top level — prefer these. */
44
44
  readonly context?: string;
45
- readonly name?: string;
45
+ readonly id?: string;
46
46
  readonly owner?: string;
47
47
  /** Legacy shape: endpoint entries nested under `endpoints[]`. */
48
48
  readonly endpoints?: ReadonlyArray<{
49
49
  readonly context?: string;
50
- readonly name?: string;
50
+ readonly id?: string;
51
51
  readonly owner?: string;
52
52
  }>;
53
53
  }
@@ -56,19 +56,19 @@ export function resolveSandboxEndpoint(
56
56
  appConfig: SandboxAdapterConfig,
57
57
  ): ResolvedSandboxBot {
58
58
  const entry = appConfig.endpoints?.find((item) => item.context === 'sandbox');
59
- const fixedName = typeof appConfig.name === 'string' && appConfig.name
60
- ? appConfig.name
61
- : typeof entry?.name === 'string' && entry.name
62
- ? entry.name
59
+ const fixedName = typeof appConfig.id === 'string' && appConfig.id
60
+ ? appConfig.id
61
+ : typeof entry?.id === 'string' && entry.id
62
+ ? entry.id
63
63
  : undefined;
64
- const name = fixedName || process.env.SANDBOX_BOT_NAME || 'sandbox-bot';
64
+ const id = fixedName || process.env.SANDBOX_BOT_NAME || 'sandbox-bot';
65
65
  const owner = (typeof appConfig.owner === 'string' && appConfig.owner)
66
66
  || (typeof entry?.owner === 'string' && entry.owner)
67
67
  || process.env.SANDBOX_BOT_OWNER
68
68
  || 'sandbox-user';
69
69
  return {
70
70
  context: 'sandbox',
71
- name,
71
+ id,
72
72
  owner,
73
73
  randomNamePerConnection: !fixedName,
74
74
  };
@@ -184,7 +184,7 @@ export function parseSandboxWsPayload(raw: string): {
184
184
  * `guild`(频道容器语义)归 'channel'。无 guild/temp 容器信息,不产生 parent。
185
185
  */
186
186
  export function sandboxInboundConversation(
187
- endpointId: string,
187
+ endpointKey: string,
188
188
  msg: { readonly type: MessageType; readonly id: string },
189
189
  ): ConversationRef {
190
190
  const kind: ConversationKind = msg.type === 'direct'
@@ -193,7 +193,7 @@ export function sandboxInboundConversation(
193
193
  ? 'channel'
194
194
  : msg.type;
195
195
  return {
196
- endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
196
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
197
197
  kind,
198
198
  id: msg.id,
199
199
  };