@zhin.js/adapter-sandbox 7.0.2 → 7.0.5
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 +46 -0
- package/lib/endpoint.d.ts +1 -1
- package/lib/endpoint.js +22 -25
- package/lib/protocol.d.ts +4 -4
- package/lib/protocol.js +8 -8
- package/package.json +19 -19
- package/pages/SandboxChat.js +4 -4
- package/pages/SandboxChat.tsx +4 -4
- package/schema.json +41 -5
- package/src/endpoint.ts +23 -26
- package/src/protocol.ts +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# @zhin.js/adapter-process
|
|
2
2
|
|
|
3
|
+
## 7.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @zhin.js/adapter@1.1.7
|
|
8
|
+
- @zhin.js/core@1.5.5
|
|
9
|
+
|
|
10
|
+
## 7.0.4
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [0de46a8]
|
|
15
|
+
- Updated dependencies [c106ecc]
|
|
16
|
+
- Updated dependencies [b0f37ae]
|
|
17
|
+
- Updated dependencies [daffd4c]
|
|
18
|
+
- Updated dependencies [36c7400]
|
|
19
|
+
- Updated dependencies [162fa34]
|
|
20
|
+
- Updated dependencies [e40b048]
|
|
21
|
+
- Updated dependencies [f1708c3]
|
|
22
|
+
- Updated dependencies [e53444f]
|
|
23
|
+
- Updated dependencies [92b0dd7]
|
|
24
|
+
- Updated dependencies [a7df753]
|
|
25
|
+
- @zhin.js/host-http@1.0.8
|
|
26
|
+
- @zhin.js/im-contract@1.0.3
|
|
27
|
+
- @zhin.js/adapter@1.1.7
|
|
28
|
+
- @zhin.js/plugin-runtime@1.1.5
|
|
29
|
+
- @zhin.js/core@1.5.4
|
|
30
|
+
- @zhin.js/page@1.0.8
|
|
31
|
+
- @zhin.js/client@2.1.6
|
|
32
|
+
|
|
33
|
+
## 7.0.3
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- f8c7a54: fix: im
|
|
38
|
+
- Updated dependencies [f8c7a54]
|
|
39
|
+
- @zhin.js/logger@1.0.76
|
|
40
|
+
- @zhin.js/client@2.1.5
|
|
41
|
+
- @zhin.js/page@1.0.7
|
|
42
|
+
- @zhin.js/console-contract@1.0.1
|
|
43
|
+
- @zhin.js/host-http@1.0.7
|
|
44
|
+
- @zhin.js/adapter@1.1.6
|
|
45
|
+
- @zhin.js/core@1.5.3
|
|
46
|
+
- @zhin.js/im-contract@1.0.2
|
|
47
|
+
- @zhin.js/plugin-runtime@1.1.4
|
|
48
|
+
|
|
3
49
|
## 7.0.2
|
|
4
50
|
|
|
5
51
|
### 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
|
|
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,
|
|
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
|
|
58
|
+
/** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
|
|
58
59
|
get name() {
|
|
59
|
-
return this.#options.defaults.
|
|
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.
|
|
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.
|
|
75
|
+
this.#ensurePlaceholder(this.#options.defaults.id, this.#options.defaults.owner);
|
|
75
76
|
}
|
|
76
|
-
logger.info(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
9
|
-
? appConfig.
|
|
10
|
-
: typeof entry?.
|
|
11
|
-
? entry.
|
|
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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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.
|
|
3
|
+
"version": "7.0.5",
|
|
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.
|
|
52
|
-
"@zhin.js/client": "2.1.
|
|
53
|
-
"@zhin.js/console-contract": "1.0.
|
|
54
|
-
"@zhin.js/core": "1.5.
|
|
55
|
-
"@zhin.js/host-http": "1.0.
|
|
56
|
-
"@zhin.js/im-contract": "1.0.
|
|
57
|
-
"@zhin.js/logger": "1.0.
|
|
58
|
-
"@zhin.js/page": "1.0.
|
|
59
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
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.5",
|
|
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.
|
|
68
|
-
"@zhin.js/runtime": "1.0.
|
|
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.
|
|
73
|
-
"@zhin.js/client": "2.1.
|
|
74
|
-
"@zhin.js/console-contract": "1.0.
|
|
75
|
-
"@zhin.js/core": "1.5.
|
|
76
|
-
"@zhin.js/host-http": "1.0.
|
|
77
|
-
"@zhin.js/page": "1.0.
|
|
78
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
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.5",
|
|
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": {
|
package/pages/SandboxChat.js
CHANGED
|
@@ -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 [
|
|
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 ||
|
|
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 ||
|
|
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:
|
|
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)
|
package/pages/SandboxChat.tsx
CHANGED
|
@@ -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 [
|
|
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 ||
|
|
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 ||
|
|
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={
|
|
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。每项与顶层字段同构(
|
|
26
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
9
27
|
"items": {
|
|
10
28
|
"type": "object",
|
|
11
29
|
"additionalProperties": true,
|
|
12
30
|
"properties": {
|
|
13
|
-
"
|
|
14
|
-
"type":
|
|
15
|
-
|
|
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
|
-
"
|
|
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,
|
|
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
|
|
96
|
+
/** Live endpoint id (config `id`) — Console endpoint.list/resolve uses it. */
|
|
96
97
|
get name(): string {
|
|
97
|
-
return this.#options.defaults.
|
|
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.
|
|
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.
|
|
113
|
+
this.#ensurePlaceholder(this.#options.defaults.id, this.#options.defaults.owner);
|
|
113
114
|
}
|
|
114
|
-
logger.info(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
60
|
-
? appConfig.
|
|
61
|
-
: typeof entry?.
|
|
62
|
-
? entry.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
196
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
197
197
|
kind,
|
|
198
198
|
id: msg.id,
|
|
199
199
|
};
|