@zhin.js/adapter-sandbox 5.0.6 → 6.0.1
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 +52 -0
- package/README.md +15 -15
- package/adapters/sandbox.ts +5 -0
- package/lib/endpoint.d.ts +4 -0
- package/lib/endpoint.js +125 -16
- package/lib/protocol.d.ts +15 -4
- package/lib/protocol.js +41 -7
- package/package.json +20 -13
- package/pages/RichTextEditor.tsx +429 -0
- package/pages/SandboxChat.tsx +672 -0
- package/pages/index.tsx +18 -0
- package/pages/sandboxTransport.ts +45 -0
- package/schema.json +27 -7
- package/src/endpoint.ts +140 -15
- package/src/protocol.ts +57 -7
- package/pages/sandbox.tsx +0 -95
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# @zhin.js/adapter-process
|
|
2
2
|
|
|
3
|
+
## 6.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5691aba: 第二轮全量审计修复批(8 面 ~60 bug):
|
|
8
|
+
|
|
9
|
+
- **安全**:email 附件路径穿越修复(basename + downloadPath 约束);lark/telegram/satori webhook 鉴权(缺密钥告警、timingSafeEqual、±5min 时效窗、chat_type 修正);onebot wss/webhook 缺 token 告警;qq webhook 改原始字节验签;renderJsx/JSX 转义注入修复;console runtime token 401 死循环。
|
|
10
|
+
- **P0 功能**:sandbox 多 endpoint 解析 + WS 路径隔离;short-url expand(undici opaqueredirect)改 follow;AI 压缩摘要失败不再静默丢历史(熔断恢复生效);console-ui 实时推送事件名归一化 + IndexedDB schema 对齐;process-monitor 热重载不再误判崩溃。
|
|
11
|
+
- **生命周期**:email IMAP 断线重连 + 在飞锁;onebot11/12 start 失败清理;line replyToken TTL + push 兜底;wechat-mp token 过期重试 + MsgId 去重;weixin-ilink buf 推进/防抖写盘/媒体 TTL/QR abort;satori PONG 看门狗;退避自毁修复。
|
|
12
|
+
- **游戏**:text-adventure 终局 restart 复活 + requires 服务端校验;tic-tac-toe PvP 占用/restart/队列清理/TTL;idiom-chain/word-riddle 闲聊不扣失误;别名中间件不劫持普通聊天。
|
|
13
|
+
- **共享库**:schema falsy 默认值/date/tuple/union 修复;database parseCondition Date/未知操作符、sqlite TEXT 往返、query 分派、belongsToMany 方言、migration dry-run;schedule DST 回拨死循环、重复 id 去重、flush 串行化;game-kit fallback 编号/onboarding 提示/尾缀边界/活引用拷贝。
|
|
14
|
+
- **渲染语音**:fetch 全部超时 + 渲染并发闸;sanitizeHtml form 保文本;STT 扩展名映射 + 删临时文件;TTS 未知 provider 报错;emojiCache LRU 负缓存/fontCache style/clearFonts 恢复;register 错误分类收窄。
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [cdf64e7]
|
|
17
|
+
- Updated dependencies [2d0a159]
|
|
18
|
+
- Updated dependencies [5691aba]
|
|
19
|
+
- Updated dependencies [078e3f7]
|
|
20
|
+
- Updated dependencies [9c997b2]
|
|
21
|
+
- Updated dependencies [09d4f25]
|
|
22
|
+
- Updated dependencies [f0ec5ab]
|
|
23
|
+
- Updated dependencies [fa66c4c]
|
|
24
|
+
- Updated dependencies [fa66c4c]
|
|
25
|
+
- Updated dependencies [6cb6152]
|
|
26
|
+
- @zhin.js/plugin-runtime@1.1.1
|
|
27
|
+
- @zhin.js/host-http@1.0.3
|
|
28
|
+
- @zhin.js/client@2.1.1
|
|
29
|
+
- @zhin.js/adapter@1.1.1
|
|
30
|
+
- @zhin.js/core@1.4.1
|
|
31
|
+
- @zhin.js/page@1.0.3
|
|
32
|
+
|
|
33
|
+
## 6.0.0
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- 7db69c1: 命令前缀改为适配器配置项:`MessageDispatcher` 不再硬编码 `/`,默认按消息所属适配器实例 config 的 `commandPrefix` 解析(默认 `''` 无前缀,任意文本按命令匹配),`endpoints[i].commandPrefix` 逐项覆盖;`ImRuntime({ commandPrefix })` 仍可设全局静态前缀。全部 20 个平台适配器 schema 新增 `commandPrefix` 属性。
|
|
38
|
+
|
|
39
|
+
BREAKING(行为变化):未配置时命令不再需要 `/` 前缀——原 `/zt` 写法不再命中,直接发 `zt` 即可;需要斜杠风格的适配器请在配置里显式设 `commandPrefix: '/'`。
|
|
40
|
+
|
|
41
|
+
- 713445c: 适配器配置格式定稿(不兼容旧格式):`plugins.<adapter>` 顶层仅共享字段 + `commandPrefix`,`endpoints[i]` 携带 endpoint 级字段(`name` + 凭据,各 schema 已类型化),`endpoints` 为必填(icqq 另需顶层 `master`);icqq 新增 `trusted` 列表(顶层/逐项均可)。scaffold-wizard 全部字段式与自定义 configure() 产出改为新格式,examples(full-bot / qq-games-bot)与 20 个适配器 README 同步迁移。
|
|
42
|
+
- Updated dependencies [7db69c1]
|
|
43
|
+
- Updated dependencies [e5c84ed]
|
|
44
|
+
- Updated dependencies [3ea84a0]
|
|
45
|
+
- Updated dependencies [5849336]
|
|
46
|
+
- Updated dependencies [1ddcd70]
|
|
47
|
+
- Updated dependencies [ac9da66]
|
|
48
|
+
- @zhin.js/core@1.4.0
|
|
49
|
+
- @zhin.js/adapter@1.1.0
|
|
50
|
+
- @zhin.js/plugin-runtime@1.1.0
|
|
51
|
+
- @zhin.js/client@2.1.0
|
|
52
|
+
- @zhin.js/host-http@1.0.2
|
|
53
|
+
- @zhin.js/page@1.0.2
|
|
54
|
+
|
|
3
55
|
## 5.0.6
|
|
4
56
|
|
|
5
57
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -23,43 +23,43 @@ pnpm add @zhin.js/adapter-sandbox
|
|
|
23
23
|
- `@zhin.js/adapter` — 约定式 `adapters/sandbox.ts`
|
|
24
24
|
- `@zhin.js/host-http` — Root 提供的 `httpHostToken`(WebSocket `/sandbox` + Console HTTP)
|
|
25
25
|
- `@zhin.js/core` — `messageGatewayToken` / ImRuntime 入站出站
|
|
26
|
-
- `@zhin.js/page` + `pages/
|
|
26
|
+
- `@zhin.js/page` + `pages/index.tsx` — ADR 0046 约定页(`definePage`;路由 `/sandbox`)
|
|
27
27
|
|
|
28
28
|
Root 在 `zhin runtime start` 时装载 `@zhin.js/host-http`、`ConsoleRuntime` 与
|
|
29
29
|
`ClientBuildModuleRuntime`。打开 `http://<host>:<port>/console` 可浏览页面;Sandbox 页
|
|
30
|
-
|
|
30
|
+
(路由 `/sandbox`,与 WebSocket `/sandbox` 同 path:GET 开页、Upgrade 走 WS)内置聊天壳。
|
|
31
31
|
|
|
32
32
|
旧 `client/`(`register(api)` / `pageManager.addEntry`)仅保留给 legacy Host 栈参考,
|
|
33
33
|
**不是** Plugin Runtime 生产入口。
|
|
34
34
|
|
|
35
|
-
### 旧 Host
|
|
35
|
+
### 旧 Host 栈(已删除)
|
|
36
|
+
|
|
37
|
+
原 legacy 插件包 `@zhin.js/host-router`(HTTP 服务)与 `@zhin.js/host-api`(Host 侧 Console API,`addEntry` 注册 Sandbox 扩展)已删除;`zhin dev` 现由 `@zhin.js/cli` 自动装配 Console/HTTP Host(`@zhin.js/host-http` + `@zhin.js/pagemanager`),无需安装任何 Host 插件。
|
|
36
38
|
|
|
37
|
-
- `@zhin.js/host-router` — HTTP 服务(提供 Router 和 WebSocket)
|
|
38
|
-
- `@zhin.js/host-api` — Host 侧 Console API(`addEntry` 注册 Sandbox 扩展)
|
|
39
39
|
- `@zhin.js/client` — Remote Console 客户端 SDK(UI 在 zhin-console 仓库)
|
|
40
40
|
|
|
41
41
|
出站 wire 只做 JSON 封装;旧 `segment-mapper`(canonical segments)归一化上移到 gateway/core 渲染链。
|
|
42
42
|
|
|
43
43
|
## 配置
|
|
44
44
|
|
|
45
|
-
**推荐(与 [minimal-bot](../../../examples/minimal-bot/) 一致)**:`endpoints: []`,在 Remote Console 打开「沙盒」页时经 `/sandbox` WebSocket **自动创建** bot(如 `sandbox-xxxx`),无需在 yaml 里写 `context: sandbox`。
|
|
45
|
+
**推荐(与 [minimal-bot](../../../examples/minimal-bot/) 一致)**:`plugins.sandbox.endpoints: []`,在 Remote Console 打开「沙盒」页时经 `/sandbox` WebSocket **自动创建** bot(如 `sandbox-xxxx`),无需在 yaml 里写 `context: sandbox`。
|
|
46
46
|
|
|
47
47
|
```yaml
|
|
48
|
-
# zhin.config.yml
|
|
49
|
-
endpoints: []
|
|
50
|
-
|
|
48
|
+
# zhin.config.yml(Plugin Runtime)
|
|
51
49
|
plugins:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- "@zhin.js/host-api"
|
|
50
|
+
sandbox:
|
|
51
|
+
endpoints: []
|
|
55
52
|
```
|
|
56
53
|
|
|
57
54
|
可选:若需在启动时即在 bot 列表显示**固定名称**的离线占位 bot,可显式配置:
|
|
58
55
|
|
|
59
56
|
```yaml
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
plugins:
|
|
58
|
+
sandbox:
|
|
59
|
+
endpoints:
|
|
60
|
+
- name: sandbox-bot
|
|
61
|
+
context: sandbox
|
|
62
|
+
owner: sandbox-user
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
## 使用方式
|
package/adapters/sandbox.ts
CHANGED
|
@@ -15,6 +15,11 @@ export type { SandboxEndpointOptions } from '../src/endpoint.js';
|
|
|
15
15
|
|
|
16
16
|
export default defineAdapter<SandboxAdapterConfig>({
|
|
17
17
|
capabilities: ['inbound', 'outbound'],
|
|
18
|
+
// Console UI 直接渲染 base64 内联媒体;交互段由 Console 原生承载。
|
|
19
|
+
segments: {
|
|
20
|
+
outboundMedia: ['base64'],
|
|
21
|
+
interactive: 'native',
|
|
22
|
+
},
|
|
18
23
|
create(context) {
|
|
19
24
|
return new SandboxWsEndpoint({
|
|
20
25
|
id: context.id,
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export interface SandboxEndpointOptions {
|
|
|
9
9
|
readonly http: HttpHost;
|
|
10
10
|
readonly defaults: ResolvedSandboxBot;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Sandbox 是本地开发/测试面,无平台社交图谱(好友/群/频道),
|
|
14
|
+
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
15
|
+
*/
|
|
12
16
|
export declare class SandboxWsEndpoint implements EndpointInstance {
|
|
13
17
|
#private;
|
|
14
18
|
constructor(options: SandboxEndpointOptions);
|
package/lib/endpoint.js
CHANGED
|
@@ -5,10 +5,50 @@ import { randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
6
|
import { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, whenWsOpen, } from './protocol.js';
|
|
7
7
|
const logger = getLogger('sandbox');
|
|
8
|
+
/**
|
|
9
|
+
* 多 sandbox endpoint 共用同一个 HttpHost 时,同 path 的所有 WS listener
|
|
10
|
+
* 都会被回调(入站重复、出站互窜)。按 endpoint 名隔离挂载路径:
|
|
11
|
+
* 首个占用 `/sandbox`(保持 Console 默认兼容),其余退到 `/sandbox/<name>`。
|
|
12
|
+
* 认领记录按 HttpHost 隔离,endpoint stop() 时必须 release。
|
|
13
|
+
*/
|
|
14
|
+
const claimedWsPaths = new WeakMap();
|
|
15
|
+
function claimSandboxWsPath(http, name) {
|
|
16
|
+
let claims = claimedWsPaths.get(http);
|
|
17
|
+
if (!claims) {
|
|
18
|
+
claims = new Map();
|
|
19
|
+
claimedWsPaths.set(http, claims);
|
|
20
|
+
}
|
|
21
|
+
const candidates = ['/sandbox', `/sandbox/${encodeURIComponent(name)}`];
|
|
22
|
+
let path = candidates.find((candidate) => !claims.has(candidate) || claims.get(candidate) === name);
|
|
23
|
+
if (!path) {
|
|
24
|
+
let index = 2;
|
|
25
|
+
path = `/sandbox/${encodeURIComponent(name)}-${index}`;
|
|
26
|
+
while (claims.has(path)) {
|
|
27
|
+
index += 1;
|
|
28
|
+
path = `/sandbox/${encodeURIComponent(name)}-${index}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
claims.set(path, name);
|
|
32
|
+
const claimed = path;
|
|
33
|
+
const registry = claims;
|
|
34
|
+
return {
|
|
35
|
+
path: claimed,
|
|
36
|
+
release: () => {
|
|
37
|
+
if (registry.get(claimed) === name)
|
|
38
|
+
registry.delete(claimed);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sandbox 是本地开发/测试面,无平台社交图谱(好友/群/频道),
|
|
44
|
+
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
45
|
+
*/
|
|
8
46
|
export class SandboxWsEndpoint {
|
|
9
47
|
#options;
|
|
10
48
|
#connections = new Map();
|
|
11
49
|
#wsHandleRelease;
|
|
50
|
+
#wsPathRelease;
|
|
51
|
+
#wsPath = '/sandbox';
|
|
12
52
|
#open = false;
|
|
13
53
|
#started = false;
|
|
14
54
|
constructor(options) {
|
|
@@ -22,7 +62,11 @@ export class SandboxWsEndpoint {
|
|
|
22
62
|
if (this.#started)
|
|
23
63
|
return;
|
|
24
64
|
this.#started = true;
|
|
25
|
-
|
|
65
|
+
// 多 endpoint 同 path 会被全部回调(入站重复、出站互窜),按名隔离。
|
|
66
|
+
const claim = claimSandboxWsPath(this.#options.http, this.#options.defaults.name);
|
|
67
|
+
this.#wsPathRelease = claim.release;
|
|
68
|
+
this.#wsPath = claim.path;
|
|
69
|
+
const handle = this.#options.http.ws(claim.path);
|
|
26
70
|
this.#wsHandleRelease = handle.onConnection((connection) => {
|
|
27
71
|
this.#acceptConnection(connection);
|
|
28
72
|
});
|
|
@@ -31,7 +75,7 @@ export class SandboxWsEndpoint {
|
|
|
31
75
|
}
|
|
32
76
|
logger.info(formatCompact({
|
|
33
77
|
op: 'sandbox_ws_mounted',
|
|
34
|
-
path:
|
|
78
|
+
path: claim.path,
|
|
35
79
|
endpoint: this.#options.defaults.name,
|
|
36
80
|
}));
|
|
37
81
|
}
|
|
@@ -45,8 +89,19 @@ export class SandboxWsEndpoint {
|
|
|
45
89
|
this.#open = false;
|
|
46
90
|
this.#wsHandleRelease?.();
|
|
47
91
|
this.#wsHandleRelease = undefined;
|
|
48
|
-
|
|
92
|
+
this.#wsPathRelease?.();
|
|
93
|
+
this.#wsPathRelease = undefined;
|
|
94
|
+
for (const connection of this.#connections.values()) {
|
|
49
95
|
connection.release();
|
|
96
|
+
if (!connection.placeholder) {
|
|
97
|
+
try {
|
|
98
|
+
connection.socket.close(1001, 'sandbox endpoint stopped');
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* already closed */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
50
105
|
this.#connections.clear();
|
|
51
106
|
this.#started = false;
|
|
52
107
|
logger.debug(formatCompact({ op: 'sandbox_stopped' }));
|
|
@@ -54,7 +109,9 @@ export class SandboxWsEndpoint {
|
|
|
54
109
|
send({ target, payload }) {
|
|
55
110
|
if (!this.#open)
|
|
56
111
|
return undefined;
|
|
57
|
-
|
|
112
|
+
// Reply target is the connection key (bot name / sandbox-uuid), not private:channelId.
|
|
113
|
+
const connection = this.#connections.get(target)
|
|
114
|
+
?? this.#findLiveConnection();
|
|
58
115
|
if (!connection) {
|
|
59
116
|
logger.debug(formatCompact({ op: 'sandbox_send_miss', target }));
|
|
60
117
|
return undefined;
|
|
@@ -63,31 +120,75 @@ export class SandboxWsEndpoint {
|
|
|
63
120
|
logger.debug(formatCompact({ op: 'sandbox_send_placeholder', target }));
|
|
64
121
|
return undefined;
|
|
65
122
|
}
|
|
66
|
-
|
|
67
|
-
|
|
123
|
+
// Console UI filters by type+id; stamp last inbound channel onto outbound wire.
|
|
124
|
+
const channel = connection.lastChannel ?? {
|
|
125
|
+
type: 'private',
|
|
126
|
+
id: connection.owner,
|
|
127
|
+
};
|
|
128
|
+
connection.socket.send(formatSandboxOutbound(payload, {
|
|
129
|
+
type: channel.type,
|
|
130
|
+
id: channel.id,
|
|
131
|
+
bot: this.#options.defaults.name,
|
|
132
|
+
endpoint: connection.target,
|
|
133
|
+
}));
|
|
134
|
+
logger.debug(formatCompact({
|
|
135
|
+
op: 'sandbox_send',
|
|
136
|
+
target: connection.target,
|
|
137
|
+
channelType: channel.type,
|
|
138
|
+
channelId: channel.id,
|
|
139
|
+
}));
|
|
68
140
|
return payload;
|
|
69
141
|
}
|
|
142
|
+
/** Prefer a real (non-placeholder) socket when reply target key is wrong/stale. */
|
|
143
|
+
#findLiveConnection() {
|
|
144
|
+
for (const connection of this.#connections.values()) {
|
|
145
|
+
if (!connection.placeholder)
|
|
146
|
+
return connection;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
70
150
|
#acceptConnection(connection) {
|
|
71
151
|
const target = this.#options.defaults.randomNamePerConnection
|
|
72
152
|
? `sandbox-${randomUUID().slice(0, 8)}`
|
|
73
153
|
: this.#options.defaults.name;
|
|
74
154
|
const owner = this.#options.defaults.owner;
|
|
75
155
|
const socket = connection.socket;
|
|
76
|
-
|
|
77
|
-
|
|
156
|
+
// Fixed-name mode reuses `target`; dropping the prior entry without
|
|
157
|
+
// closing its socket leaves a zombie browser tab that still looks
|
|
158
|
+
// connected but never receives outbound traffic.
|
|
159
|
+
const previous = this.#connections.get(target);
|
|
160
|
+
if (previous) {
|
|
161
|
+
previous.release();
|
|
162
|
+
if (!previous.placeholder) {
|
|
163
|
+
try {
|
|
164
|
+
previous.socket.close(4000, 'replaced by new sandbox client');
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
/* already closed */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
78
170
|
this.#connections.delete(target);
|
|
79
171
|
}
|
|
80
172
|
const release = bindSandboxWsSocket(socket, {
|
|
81
173
|
onMessage: (raw) => {
|
|
82
|
-
if (!this.#open)
|
|
83
|
-
return;
|
|
84
174
|
const parsed = parseSandboxWsPayload(raw);
|
|
175
|
+
const conn = this.#connections.get(target);
|
|
176
|
+
if (conn && !conn.placeholder) {
|
|
177
|
+
conn.lastChannel = {
|
|
178
|
+
type: parsed.type,
|
|
179
|
+
id: parsed.id || owner,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
85
182
|
logger.debug(formatCompact({
|
|
86
183
|
op: 'sandbox_recv',
|
|
87
184
|
target,
|
|
88
185
|
sender: parsed.id || owner,
|
|
186
|
+
channelType: parsed.type,
|
|
187
|
+
channelId: parsed.id || owner,
|
|
89
188
|
text: parsed.text.slice(0, 80),
|
|
90
189
|
}));
|
|
190
|
+
// Don't gate on #open — inbound must always reach the gateway so
|
|
191
|
+
// Command/AI dispatch and outbound replies work.
|
|
91
192
|
void this.#options.gateway.receive({
|
|
92
193
|
adapter: this.#options.id,
|
|
93
194
|
target,
|
|
@@ -95,6 +196,9 @@ export class SandboxWsEndpoint {
|
|
|
95
196
|
sender: parsed.id || owner,
|
|
96
197
|
metadata: Object.freeze({
|
|
97
198
|
type: parsed.type,
|
|
199
|
+
channelType: parsed.type,
|
|
200
|
+
channelId: parsed.id || owner,
|
|
201
|
+
endpoint: target,
|
|
98
202
|
elements: parsed.content,
|
|
99
203
|
timestamp: parsed.timestamp,
|
|
100
204
|
...(parsed.action ? { action: parsed.action } : {}),
|
|
@@ -108,8 +212,13 @@ export class SandboxWsEndpoint {
|
|
|
108
212
|
});
|
|
109
213
|
},
|
|
110
214
|
onClose: () => {
|
|
111
|
-
this
|
|
112
|
-
|
|
215
|
+
// Only drop the map entry if we still own this socket — a replace
|
|
216
|
+
// may have already swapped in a newer connection for the same target.
|
|
217
|
+
const current = this.#connections.get(target);
|
|
218
|
+
if (current && current.socket === socket) {
|
|
219
|
+
this.#connections.delete(target);
|
|
220
|
+
logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
|
|
221
|
+
}
|
|
113
222
|
},
|
|
114
223
|
onError: (err) => {
|
|
115
224
|
logger.warn(formatCompact({
|
|
@@ -119,7 +228,7 @@ export class SandboxWsEndpoint {
|
|
|
119
228
|
}));
|
|
120
229
|
},
|
|
121
230
|
});
|
|
122
|
-
this.#connections.set(target,
|
|
231
|
+
this.#connections.set(target, { target, owner, socket, release });
|
|
123
232
|
logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
124
233
|
if (!this.#options.defaults.randomNamePerConnection) {
|
|
125
234
|
const readyPayload = JSON.stringify({
|
|
@@ -131,7 +240,7 @@ export class SandboxWsEndpoint {
|
|
|
131
240
|
data: {
|
|
132
241
|
text: [
|
|
133
242
|
`已连接 Sandbox「${target}」`,
|
|
134
|
-
|
|
243
|
+
`与 Node Host 控制台沙盒协议一致(${this.#wsPath})`,
|
|
135
244
|
'命令: help · ping · zt · status',
|
|
136
245
|
].join('\n'),
|
|
137
246
|
},
|
|
@@ -144,12 +253,12 @@ export class SandboxWsEndpoint {
|
|
|
144
253
|
#ensurePlaceholder(name, owner) {
|
|
145
254
|
if (this.#connections.has(name))
|
|
146
255
|
return;
|
|
147
|
-
this.#connections.set(name,
|
|
256
|
+
this.#connections.set(name, {
|
|
148
257
|
target: name,
|
|
149
258
|
owner,
|
|
150
259
|
socket: { send: () => undefined, close: () => undefined },
|
|
151
260
|
release: () => undefined,
|
|
152
261
|
placeholder: true,
|
|
153
|
-
})
|
|
262
|
+
});
|
|
154
263
|
}
|
|
155
264
|
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ export type ResolvedSandboxBot = {
|
|
|
19
19
|
readonly randomNamePerConnection: boolean;
|
|
20
20
|
};
|
|
21
21
|
export interface SandboxAdapterConfig {
|
|
22
|
+
/** Runtime expands `endpoints[i]` onto the top level — prefer these. */
|
|
23
|
+
readonly context?: string;
|
|
24
|
+
readonly name?: string;
|
|
25
|
+
readonly owner?: string;
|
|
26
|
+
/** Legacy shape: endpoint entries nested under `endpoints[]`. */
|
|
22
27
|
readonly endpoints?: ReadonlyArray<{
|
|
23
28
|
readonly context?: string;
|
|
24
29
|
readonly name?: string;
|
|
@@ -42,11 +47,17 @@ export declare function parseSandboxWsPayload(raw: string): {
|
|
|
42
47
|
payload: string;
|
|
43
48
|
};
|
|
44
49
|
};
|
|
50
|
+
export type SandboxOutboundChannel = {
|
|
51
|
+
readonly type?: string;
|
|
52
|
+
readonly id?: string;
|
|
53
|
+
readonly bot?: string;
|
|
54
|
+
readonly endpoint?: string;
|
|
55
|
+
readonly messageId?: string;
|
|
56
|
+
};
|
|
45
57
|
/**
|
|
46
58
|
* Wire-encode an already-rendered outbound payload.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* owns that before `endpoint.send`.
|
|
59
|
+
* Stamps `channel` so Console SandboxChat can filter by type+id (otherwise
|
|
60
|
+
* replies look like they disappeared).
|
|
50
61
|
*/
|
|
51
|
-
export declare function formatSandboxOutbound(payload: unknown): string;
|
|
62
|
+
export declare function formatSandboxOutbound(payload: unknown, channel?: SandboxOutboundChannel): string;
|
|
52
63
|
export declare function whenWsOpen(ws: SandboxWsSocket, fn: () => void): void;
|
package/lib/protocol.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/** Sandbox WebSocket wire protocol helpers (no legacy Adapter/Endpoint). */
|
|
2
2
|
export function resolveSandboxEndpoint(appConfig) {
|
|
3
3
|
const entry = appConfig.endpoints?.find((item) => item.context === 'sandbox');
|
|
4
|
-
const fixedName = typeof
|
|
4
|
+
const fixedName = typeof appConfig.name === 'string' && appConfig.name
|
|
5
|
+
? appConfig.name
|
|
6
|
+
: typeof entry?.name === 'string' && entry.name
|
|
7
|
+
? entry.name
|
|
8
|
+
: undefined;
|
|
5
9
|
const name = fixedName || process.env.SANDBOX_BOT_NAME || 'sandbox-bot';
|
|
6
|
-
const owner = (typeof
|
|
10
|
+
const owner = (typeof appConfig.owner === 'string' && appConfig.owner)
|
|
11
|
+
|| (typeof entry?.owner === 'string' && entry.owner)
|
|
7
12
|
|| process.env.SANDBOX_BOT_OWNER
|
|
8
13
|
|| 'sandbox-user';
|
|
9
14
|
return {
|
|
@@ -100,24 +105,53 @@ export function parseSandboxWsPayload(raw) {
|
|
|
100
105
|
}
|
|
101
106
|
/**
|
|
102
107
|
* Wire-encode an already-rendered outbound payload.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* owns that before `endpoint.send`.
|
|
108
|
+
* Stamps `channel` so Console SandboxChat can filter by type+id (otherwise
|
|
109
|
+
* replies look like they disappeared).
|
|
106
110
|
*/
|
|
107
|
-
export function formatSandboxOutbound(payload) {
|
|
111
|
+
export function formatSandboxOutbound(payload, channel = {}) {
|
|
112
|
+
const stamp = {};
|
|
113
|
+
if (channel.type)
|
|
114
|
+
stamp.type = channel.type;
|
|
115
|
+
if (channel.id)
|
|
116
|
+
stamp.id = channel.id;
|
|
117
|
+
if (channel.bot)
|
|
118
|
+
stamp.bot = channel.bot;
|
|
119
|
+
if (channel.endpoint)
|
|
120
|
+
stamp.endpoint = channel.endpoint;
|
|
121
|
+
if (channel.messageId)
|
|
122
|
+
stamp.messageId = channel.messageId;
|
|
108
123
|
if (typeof payload === 'string') {
|
|
109
124
|
return JSON.stringify({
|
|
125
|
+
...stamp,
|
|
110
126
|
content: [{ type: 'text', data: { text: payload } }],
|
|
111
127
|
timestamp: Date.now(),
|
|
112
128
|
});
|
|
113
129
|
}
|
|
114
130
|
if (Array.isArray(payload)) {
|
|
115
131
|
return JSON.stringify({
|
|
132
|
+
...stamp,
|
|
116
133
|
content: payload,
|
|
117
134
|
timestamp: Date.now(),
|
|
118
135
|
});
|
|
119
136
|
}
|
|
120
|
-
|
|
137
|
+
// Already a wire envelope ({ content, type, … }) — pass through so the
|
|
138
|
+
// Console UI can read `content` / `type` without an extra nesting layer.
|
|
139
|
+
// Bare segment objects ({ type: 'text', data: … }) still need wrapping.
|
|
140
|
+
if (payload
|
|
141
|
+
&& typeof payload === 'object'
|
|
142
|
+
&& !Array.isArray(payload)
|
|
143
|
+
&& ('content' in payload
|
|
144
|
+
|| 'type' in payload && 'timestamp' in payload)) {
|
|
145
|
+
const envelope = payload;
|
|
146
|
+
return JSON.stringify({
|
|
147
|
+
...stamp,
|
|
148
|
+
...envelope,
|
|
149
|
+
type: envelope.type ?? stamp.type,
|
|
150
|
+
id: envelope.id ?? stamp.id,
|
|
151
|
+
timestamp: typeof envelope.timestamp === 'number' ? envelope.timestamp : Date.now(),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return JSON.stringify({ ...stamp, content: payload, timestamp: Date.now() });
|
|
121
155
|
}
|
|
122
156
|
/** WebSocket.OPEN 常量值;Node <22 无全局 WebSocket,不能用 WebSocket.OPEN。 */
|
|
123
157
|
const WS_OPEN = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-sandbox",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Zhin.js Sandbox adapter for Plugin Runtime (WebSocket /sandbox)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -46,35 +46,42 @@
|
|
|
46
46
|
"directory": "plugins/adapters/sandbox"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"lucide-react": "^0.525.0",
|
|
49
50
|
"react": "^19.1.0",
|
|
51
|
+
"@zhin.js/adapter": "1.1.1",
|
|
52
|
+
"@zhin.js/client": "2.1.1",
|
|
50
53
|
"@zhin.js/console-contract": "1.0.0",
|
|
51
|
-
"@zhin.js/
|
|
52
|
-
"@zhin.js/
|
|
53
|
-
"@zhin.js/host-http": "1.0.1",
|
|
54
|
+
"@zhin.js/core": "1.4.1",
|
|
55
|
+
"@zhin.js/host-http": "1.0.3",
|
|
54
56
|
"@zhin.js/logger": "1.0.75",
|
|
55
|
-
"@zhin.js/page": "1.0.
|
|
56
|
-
"@zhin.js/plugin-runtime": "1.
|
|
57
|
+
"@zhin.js/page": "1.0.3",
|
|
58
|
+
"@zhin.js/plugin-runtime": "1.1.1"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
61
|
+
"@types/react": "^19.1.8",
|
|
59
62
|
"@types/ws": "^8.18.1",
|
|
60
63
|
"typescript": "^6.0.3",
|
|
61
64
|
"vitest": "^4.1.10",
|
|
62
65
|
"ws": "^8.21.0",
|
|
63
|
-
"@zhin.js/pagemanager": "2.0.
|
|
64
|
-
"@zhin.js/runtime": "1.0.
|
|
66
|
+
"@zhin.js/pagemanager": "2.0.6",
|
|
67
|
+
"@zhin.js/runtime": "1.0.3"
|
|
65
68
|
},
|
|
66
69
|
"peerDependencies": {
|
|
67
70
|
"react": "^19.0.0",
|
|
68
|
-
"@zhin.js/adapter": "1.
|
|
71
|
+
"@zhin.js/adapter": "1.1.1",
|
|
72
|
+
"@zhin.js/client": "2.1.1",
|
|
69
73
|
"@zhin.js/console-contract": "1.0.0",
|
|
70
|
-
"@zhin.js/core": "1.
|
|
71
|
-
"@zhin.js/host-http": "1.0.
|
|
72
|
-
"@zhin.js/page": "1.0.
|
|
73
|
-
"@zhin.js/plugin-runtime": "1.
|
|
74
|
+
"@zhin.js/core": "1.4.1",
|
|
75
|
+
"@zhin.js/host-http": "1.0.3",
|
|
76
|
+
"@zhin.js/page": "1.0.3",
|
|
77
|
+
"@zhin.js/plugin-runtime": "1.1.1"
|
|
74
78
|
},
|
|
75
79
|
"peerDependenciesMeta": {
|
|
76
80
|
"react": {
|
|
77
81
|
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@zhin.js/client": {
|
|
84
|
+
"optional": true
|
|
78
85
|
}
|
|
79
86
|
},
|
|
80
87
|
"publishConfig": {
|