@zhin.js/adapter-sandbox 5.0.5 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +27 -11
- package/adapters/sandbox.ts +26 -0
- package/lib/endpoint.d.ts +25 -0
- package/lib/endpoint.js +218 -0
- package/lib/index.d.ts +2 -22
- package/lib/index.js +2 -67
- package/lib/protocol.d.ts +58 -0
- package/lib/protocol.js +164 -0
- package/package.json +52 -24
- package/{client/Sandbox.tsx → pages/SandboxChat.tsx} +96 -12
- package/pages/sandbox.tsx +17 -0
- package/pages/sandboxTransport.ts +45 -0
- package/plugin.ts +8 -0
- package/schema.json +40 -0
- package/src/endpoint.ts +261 -0
- package/src/index.ts +12 -101
- package/src/protocol.ts +240 -0
- package/client/index.tsx +0 -11
- package/client/sandboxTransport.ts +0 -28
- package/client/tsconfig.json +0 -7
- package/dist/index.js +0 -45
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/sandbox-ws.d.ts +0 -86
- package/lib/sandbox-ws.d.ts.map +0 -1
- package/lib/sandbox-ws.js +0 -295
- package/lib/sandbox-ws.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/src/sandbox-ws.ts +0 -404
- package/src/segment-mapper.ts +0 -1
- /package/{client → pages}/RichTextEditor.tsx +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# @zhin.js/adapter-process
|
|
2
2
|
|
|
3
|
+
## 6.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7db69c1: 命令前缀改为适配器配置项:`MessageDispatcher` 不再硬编码 `/`,默认按消息所属适配器实例 config 的 `commandPrefix` 解析(默认 `''` 无前缀,任意文本按命令匹配),`endpoints[i].commandPrefix` 逐项覆盖;`ImRuntime({ commandPrefix })` 仍可设全局静态前缀。全部 20 个平台适配器 schema 新增 `commandPrefix` 属性。
|
|
8
|
+
|
|
9
|
+
BREAKING(行为变化):未配置时命令不再需要 `/` 前缀——原 `/zt` 写法不再命中,直接发 `zt` 即可;需要斜杠风格的适配器请在配置里显式设 `commandPrefix: '/'`。
|
|
10
|
+
|
|
11
|
+
- 713445c: 适配器配置格式定稿(不兼容旧格式):`plugins.<adapter>` 顶层仅共享字段 + `commandPrefix`,`endpoints[i]` 携带 endpoint 级字段(`name` + 凭据,各 schema 已类型化),`endpoints` 为必填(icqq 另需顶层 `master`);icqq 新增 `trusted` 列表(顶层/逐项均可)。scaffold-wizard 全部字段式与自定义 configure() 产出改为新格式,examples(full-bot / qq-games-bot)与 20 个适配器 README 同步迁移。
|
|
12
|
+
- Updated dependencies [7db69c1]
|
|
13
|
+
- Updated dependencies [e5c84ed]
|
|
14
|
+
- Updated dependencies [3ea84a0]
|
|
15
|
+
- Updated dependencies [5849336]
|
|
16
|
+
- Updated dependencies [1ddcd70]
|
|
17
|
+
- Updated dependencies [ac9da66]
|
|
18
|
+
- @zhin.js/core@1.4.0
|
|
19
|
+
- @zhin.js/adapter@1.1.0
|
|
20
|
+
- @zhin.js/plugin-runtime@1.1.0
|
|
21
|
+
- @zhin.js/client@2.1.0
|
|
22
|
+
- @zhin.js/host-http@1.0.2
|
|
23
|
+
- @zhin.js/page@1.0.2
|
|
24
|
+
|
|
25
|
+
## 5.0.6
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- cc5c94d: 约定式插件运行时迁移(breaking):插件与适配器由 `usePlugin()` / `extends Adapter` 迁移为 `definePlugin` / `defineAdapter` + `plugin.ts` + 约定目录(`adapters/`、`commands/`、`components/`、`tools/` 等)。
|
|
30
|
+
|
|
31
|
+
- 新增约定式运行时包:`@zhin.js/plugin-runtime`、`@zhin.js/adapter`、`@zhin.js/runtime`、`@zhin.js/host-http`(首版 1.0.0 走 init-publish,不在本 changeset 内 bump)。
|
|
32
|
+
- 全部 20 个平台适配器改为约定式 `defineAdapter`,旧 `usePlugin` / `extends Adapter` / `segment-mapper` 生产入口已删除;onebot11 反向 WSS、onebot12 webhook/wss、milky sse/webhook/wss、satori webhook、kook webhook、qq webhook/middleware 等 slice 1 推迟的连接模式已补齐。
|
|
33
|
+
- 游戏 / 工具 / 服务插件同步迁移到约定目录结构。
|
|
34
|
+
- CLI 增加 plugin-runtime host installer(http/database/outbound/schedule/console 等)。
|
|
35
|
+
|
|
36
|
+
后续加固(同批):
|
|
37
|
+
|
|
38
|
+
- CLI:`zhin runtime start --daemon`(pidfile/崩溃拉起/风暴保护),orphan watchdog 防僵尸进程;legacy `zhin dev` / `zhin start` 已移除(含 `zhin restart`),`zhin stop` 兼容新 daemon。
|
|
39
|
+
- 安全:builtin 工具统一走 `security/policy-facade.ts` 的 `runToolPolicies`(声明式策略表,deny 优先);审计日志 close flush + 背压队列;`splitCompoundCommand` 引号感知、`extractCommandName` 去引号堵绕过。
|
|
40
|
+
- 日志:Logger 双堆栈修复、本地时区、`getLogger` 挂树(`setLevel` 递归生效)、第三方库(log4js/discord)桥接、启动人读总结。
|
|
41
|
+
- 结构:`plugins/games/shared` 迁为 `packages/game-kit`(`@zhin.js/game-kit`);死目录 `plugins/adapters/common` 删除。
|
|
42
|
+
- 脚手架:`create-zhin-app` / `zhin new` / scaffold-wizard 生成物改为 Plugin Runtime 形态(minimal-bot 同构,新配置格式)。
|
|
43
|
+
- Console:endpoint.list 真实名称与 phase、schema:get-all 按 instanceKey 映射、db:\* 接 DatabaseHost。
|
|
44
|
+
|
|
45
|
+
注:按仓库发布惯例(见 1bb345dd2),本次 breaking 迁移统一使用 patch,避免 zhin.js 5.0 级联。
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [16ec4e8]
|
|
48
|
+
- Updated dependencies [cc5c94d]
|
|
49
|
+
- Updated dependencies [447f3e2]
|
|
50
|
+
- @zhin.js/core@1.3.5
|
|
51
|
+
- @zhin.js/host-http@1.0.1
|
|
52
|
+
- @zhin.js/logger@1.0.75
|
|
53
|
+
- @zhin.js/plugin-runtime@1.0.1
|
|
54
|
+
- @zhin.js/adapter@1.0.1
|
|
55
|
+
- @zhin.js/page@1.0.1
|
|
56
|
+
|
|
3
57
|
## 5.0.5
|
|
4
58
|
|
|
5
59
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -18,32 +18,48 @@ pnpm add @zhin.js/adapter-sandbox
|
|
|
18
18
|
|
|
19
19
|
## 依赖
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### Plugin Runtime(新,`zhin runtime start`)
|
|
22
|
+
|
|
23
|
+
- `@zhin.js/adapter` — 约定式 `adapters/sandbox.ts`
|
|
24
|
+
- `@zhin.js/host-http` — Root 提供的 `httpHostToken`(WebSocket `/sandbox` + Console HTTP)
|
|
25
|
+
- `@zhin.js/core` — `messageGatewayToken` / ImRuntime 入站出站
|
|
26
|
+
- `@zhin.js/page` + `pages/sandbox.tsx` — ADR 0046 约定页(`definePage`)
|
|
27
|
+
|
|
28
|
+
Root 在 `zhin runtime start` 时装载 `@zhin.js/host-http`、`ConsoleRuntime` 与
|
|
29
|
+
`ClientBuildModuleRuntime`。打开 `http://<host>:<port>/console` 可浏览页面;Sandbox 页
|
|
30
|
+
(路由形如 `/…/p-sandbox`)内置 WebSocket `/sandbox` 聊天壳。
|
|
31
|
+
|
|
32
|
+
旧 `client/`(`register(api)` / `pageManager.addEntry`)仅保留给 legacy Host 栈参考,
|
|
33
|
+
**不是** Plugin Runtime 生产入口。
|
|
34
|
+
|
|
35
|
+
### 旧 Host 栈(`zhin dev` / full-bot)
|
|
22
36
|
|
|
23
37
|
- `@zhin.js/host-router` — HTTP 服务(提供 Router 和 WebSocket)
|
|
24
38
|
- `@zhin.js/host-api` — Host 侧 Console API(`addEntry` 注册 Sandbox 扩展)
|
|
25
39
|
- `@zhin.js/client` — Remote Console 客户端 SDK(UI 在 zhin-console 仓库)
|
|
26
40
|
|
|
41
|
+
出站 wire 只做 JSON 封装;旧 `segment-mapper`(canonical segments)归一化上移到 gateway/core 渲染链。
|
|
42
|
+
|
|
27
43
|
## 配置
|
|
28
44
|
|
|
29
|
-
**推荐(与 [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`。
|
|
30
46
|
|
|
31
47
|
```yaml
|
|
32
|
-
# zhin.config.yml
|
|
33
|
-
endpoints: []
|
|
34
|
-
|
|
48
|
+
# zhin.config.yml(Plugin Runtime)
|
|
35
49
|
plugins:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- "@zhin.js/host-api"
|
|
50
|
+
sandbox:
|
|
51
|
+
endpoints: []
|
|
39
52
|
```
|
|
40
53
|
|
|
41
54
|
可选:若需在启动时即在 bot 列表显示**固定名称**的离线占位 bot,可显式配置:
|
|
42
55
|
|
|
43
56
|
```yaml
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
plugins:
|
|
58
|
+
sandbox:
|
|
59
|
+
endpoints:
|
|
60
|
+
- name: sandbox-bot
|
|
61
|
+
context: sandbox
|
|
62
|
+
owner: sandbox-user
|
|
47
63
|
```
|
|
48
64
|
|
|
49
65
|
## 使用方式
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convention entry: discover `adapters/sandbox.ts` → defineAdapter.
|
|
3
|
+
*/
|
|
4
|
+
import { defineAdapter } from '@zhin.js/adapter';
|
|
5
|
+
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
6
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
7
|
+
import { SandboxWsEndpoint } from '../src/endpoint.js';
|
|
8
|
+
import {
|
|
9
|
+
resolveSandboxEndpoint,
|
|
10
|
+
type SandboxAdapterConfig,
|
|
11
|
+
} from '../src/protocol.js';
|
|
12
|
+
|
|
13
|
+
export { SandboxWsEndpoint } from '../src/endpoint.js';
|
|
14
|
+
export type { SandboxEndpointOptions } from '../src/endpoint.js';
|
|
15
|
+
|
|
16
|
+
export default defineAdapter<SandboxAdapterConfig>({
|
|
17
|
+
capabilities: ['inbound', 'outbound'],
|
|
18
|
+
create(context) {
|
|
19
|
+
return new SandboxWsEndpoint({
|
|
20
|
+
id: context.id,
|
|
21
|
+
gateway: context.use(messageGatewayToken),
|
|
22
|
+
http: context.use(httpHostToken),
|
|
23
|
+
defaults: resolveSandboxEndpoint(context.config),
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
2
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
|
+
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
5
|
+
import { type ResolvedSandboxBot } from './protocol.js';
|
|
6
|
+
export interface SandboxEndpointOptions {
|
|
7
|
+
readonly id: CapabilityId;
|
|
8
|
+
readonly gateway: MessageGateway;
|
|
9
|
+
readonly http: HttpHost;
|
|
10
|
+
readonly defaults: ResolvedSandboxBot;
|
|
11
|
+
}
|
|
12
|
+
export declare class SandboxWsEndpoint implements EndpointInstance {
|
|
13
|
+
#private;
|
|
14
|
+
constructor(options: SandboxEndpointOptions);
|
|
15
|
+
/** Live endpoint name (config `name`) — Console endpoint.list/resolve uses it. */
|
|
16
|
+
get name(): string;
|
|
17
|
+
start(): void;
|
|
18
|
+
open(): void;
|
|
19
|
+
close(): void;
|
|
20
|
+
stop(): void;
|
|
21
|
+
send({ target, payload }: {
|
|
22
|
+
readonly target: string;
|
|
23
|
+
readonly payload: unknown;
|
|
24
|
+
}): unknown;
|
|
25
|
+
}
|
package/lib/endpoint.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SandboxWsEndpoint — WebSocket lifecycle and MessageGateway bridge for /sandbox.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
|
+
import { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, whenWsOpen, } from './protocol.js';
|
|
7
|
+
const logger = getLogger('sandbox');
|
|
8
|
+
export class SandboxWsEndpoint {
|
|
9
|
+
#options;
|
|
10
|
+
#connections = new Map();
|
|
11
|
+
#wsHandleRelease;
|
|
12
|
+
#open = false;
|
|
13
|
+
#started = false;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.#options = options;
|
|
16
|
+
}
|
|
17
|
+
/** Live endpoint name (config `name`) — Console endpoint.list/resolve uses it. */
|
|
18
|
+
get name() {
|
|
19
|
+
return this.#options.defaults.name;
|
|
20
|
+
}
|
|
21
|
+
start() {
|
|
22
|
+
if (this.#started)
|
|
23
|
+
return;
|
|
24
|
+
this.#started = true;
|
|
25
|
+
const handle = this.#options.http.ws('/sandbox');
|
|
26
|
+
this.#wsHandleRelease = handle.onConnection((connection) => {
|
|
27
|
+
this.#acceptConnection(connection);
|
|
28
|
+
});
|
|
29
|
+
if (!this.#options.defaults.randomNamePerConnection) {
|
|
30
|
+
this.#ensurePlaceholder(this.#options.defaults.name, this.#options.defaults.owner);
|
|
31
|
+
}
|
|
32
|
+
logger.info(formatCompact({
|
|
33
|
+
op: 'sandbox_ws_mounted',
|
|
34
|
+
path: '/sandbox',
|
|
35
|
+
endpoint: this.#options.defaults.name,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
open() {
|
|
39
|
+
this.#open = true;
|
|
40
|
+
}
|
|
41
|
+
close() {
|
|
42
|
+
this.#open = false;
|
|
43
|
+
}
|
|
44
|
+
stop() {
|
|
45
|
+
this.#open = false;
|
|
46
|
+
this.#wsHandleRelease?.();
|
|
47
|
+
this.#wsHandleRelease = undefined;
|
|
48
|
+
for (const connection of this.#connections.values()) {
|
|
49
|
+
connection.release();
|
|
50
|
+
if (!connection.placeholder) {
|
|
51
|
+
try {
|
|
52
|
+
connection.socket.close(1001, 'sandbox endpoint stopped');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* already closed */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this.#connections.clear();
|
|
60
|
+
this.#started = false;
|
|
61
|
+
logger.debug(formatCompact({ op: 'sandbox_stopped' }));
|
|
62
|
+
}
|
|
63
|
+
send({ target, payload }) {
|
|
64
|
+
if (!this.#open)
|
|
65
|
+
return undefined;
|
|
66
|
+
// Reply target is the connection key (bot name / sandbox-uuid), not private:channelId.
|
|
67
|
+
const connection = this.#connections.get(target)
|
|
68
|
+
?? this.#findLiveConnection();
|
|
69
|
+
if (!connection) {
|
|
70
|
+
logger.debug(formatCompact({ op: 'sandbox_send_miss', target }));
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (connection.placeholder) {
|
|
74
|
+
logger.debug(formatCompact({ op: 'sandbox_send_placeholder', target }));
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
// Console UI filters by type+id; stamp last inbound channel onto outbound wire.
|
|
78
|
+
const channel = connection.lastChannel ?? {
|
|
79
|
+
type: 'private',
|
|
80
|
+
id: connection.owner,
|
|
81
|
+
};
|
|
82
|
+
connection.socket.send(formatSandboxOutbound(payload, {
|
|
83
|
+
type: channel.type,
|
|
84
|
+
id: channel.id,
|
|
85
|
+
bot: this.#options.defaults.name,
|
|
86
|
+
endpoint: connection.target,
|
|
87
|
+
}));
|
|
88
|
+
logger.debug(formatCompact({
|
|
89
|
+
op: 'sandbox_send',
|
|
90
|
+
target: connection.target,
|
|
91
|
+
channelType: channel.type,
|
|
92
|
+
channelId: channel.id,
|
|
93
|
+
}));
|
|
94
|
+
return payload;
|
|
95
|
+
}
|
|
96
|
+
/** Prefer a real (non-placeholder) socket when reply target key is wrong/stale. */
|
|
97
|
+
#findLiveConnection() {
|
|
98
|
+
for (const connection of this.#connections.values()) {
|
|
99
|
+
if (!connection.placeholder)
|
|
100
|
+
return connection;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
#acceptConnection(connection) {
|
|
105
|
+
const target = this.#options.defaults.randomNamePerConnection
|
|
106
|
+
? `sandbox-${randomUUID().slice(0, 8)}`
|
|
107
|
+
: this.#options.defaults.name;
|
|
108
|
+
const owner = this.#options.defaults.owner;
|
|
109
|
+
const socket = connection.socket;
|
|
110
|
+
// Fixed-name mode reuses `target`; dropping the prior entry without
|
|
111
|
+
// closing its socket leaves a zombie browser tab that still looks
|
|
112
|
+
// connected but never receives outbound traffic.
|
|
113
|
+
const previous = this.#connections.get(target);
|
|
114
|
+
if (previous) {
|
|
115
|
+
previous.release();
|
|
116
|
+
if (!previous.placeholder) {
|
|
117
|
+
try {
|
|
118
|
+
previous.socket.close(4000, 'replaced by new sandbox client');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* already closed */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
this.#connections.delete(target);
|
|
125
|
+
}
|
|
126
|
+
const release = bindSandboxWsSocket(socket, {
|
|
127
|
+
onMessage: (raw) => {
|
|
128
|
+
const parsed = parseSandboxWsPayload(raw);
|
|
129
|
+
const conn = this.#connections.get(target);
|
|
130
|
+
if (conn && !conn.placeholder) {
|
|
131
|
+
conn.lastChannel = {
|
|
132
|
+
type: parsed.type,
|
|
133
|
+
id: parsed.id || owner,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
logger.debug(formatCompact({
|
|
137
|
+
op: 'sandbox_recv',
|
|
138
|
+
target,
|
|
139
|
+
sender: parsed.id || owner,
|
|
140
|
+
channelType: parsed.type,
|
|
141
|
+
channelId: parsed.id || owner,
|
|
142
|
+
text: parsed.text.slice(0, 80),
|
|
143
|
+
}));
|
|
144
|
+
// Don't gate on #open — inbound must always reach the gateway so
|
|
145
|
+
// Command/AI dispatch and outbound replies work.
|
|
146
|
+
void this.#options.gateway.receive({
|
|
147
|
+
adapter: this.#options.id,
|
|
148
|
+
target,
|
|
149
|
+
content: parsed.text,
|
|
150
|
+
sender: parsed.id || owner,
|
|
151
|
+
metadata: Object.freeze({
|
|
152
|
+
type: parsed.type,
|
|
153
|
+
channelType: parsed.type,
|
|
154
|
+
channelId: parsed.id || owner,
|
|
155
|
+
endpoint: target,
|
|
156
|
+
elements: parsed.content,
|
|
157
|
+
timestamp: parsed.timestamp,
|
|
158
|
+
...(parsed.action ? { action: parsed.action } : {}),
|
|
159
|
+
}),
|
|
160
|
+
}).catch((err) => {
|
|
161
|
+
logger.warn(formatCompact({
|
|
162
|
+
op: 'sandbox_gateway_receive_failed',
|
|
163
|
+
target,
|
|
164
|
+
error: err instanceof Error ? err.message : String(err),
|
|
165
|
+
}));
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
onClose: () => {
|
|
169
|
+
// Only drop the map entry if we still own this socket — a replace
|
|
170
|
+
// may have already swapped in a newer connection for the same target.
|
|
171
|
+
const current = this.#connections.get(target);
|
|
172
|
+
if (current && current.socket === socket) {
|
|
173
|
+
this.#connections.delete(target);
|
|
174
|
+
logger.debug(formatCompact({ op: 'sandbox_ws_closed', target }));
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
onError: (err) => {
|
|
178
|
+
logger.warn(formatCompact({
|
|
179
|
+
op: 'sandbox_ws_error',
|
|
180
|
+
target,
|
|
181
|
+
error: err instanceof Error ? err.message : String(err),
|
|
182
|
+
}));
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
this.#connections.set(target, { target, owner, socket, release });
|
|
186
|
+
logger.debug(formatCompact({ op: 'sandbox_ws_connected', target, owner }));
|
|
187
|
+
if (!this.#options.defaults.randomNamePerConnection) {
|
|
188
|
+
const readyPayload = JSON.stringify({
|
|
189
|
+
type: 'ready',
|
|
190
|
+
id: owner,
|
|
191
|
+
endpoint: target,
|
|
192
|
+
content: [{
|
|
193
|
+
type: 'text',
|
|
194
|
+
data: {
|
|
195
|
+
text: [
|
|
196
|
+
`已连接 Sandbox「${target}」`,
|
|
197
|
+
'与 Node Host 控制台沙盒协议一致(/sandbox)',
|
|
198
|
+
'命令: help · ping · zt · status',
|
|
199
|
+
].join('\n'),
|
|
200
|
+
},
|
|
201
|
+
}],
|
|
202
|
+
timestamp: Date.now(),
|
|
203
|
+
});
|
|
204
|
+
whenWsOpen(socket, () => socket.send(readyPayload));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
#ensurePlaceholder(name, owner) {
|
|
208
|
+
if (this.#connections.has(name))
|
|
209
|
+
return;
|
|
210
|
+
this.#connections.set(name, {
|
|
211
|
+
target: name,
|
|
212
|
+
owner,
|
|
213
|
+
socket: { send: () => undefined, close: () => undefined },
|
|
214
|
+
release: () => undefined,
|
|
215
|
+
placeholder: true,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,22 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
type SandboxRouter = {
|
|
4
|
-
ws: (path: string) => NonNullable<SandboxAdapter["wss"]>;
|
|
5
|
-
};
|
|
6
|
-
declare module "zhin.js" {
|
|
7
|
-
interface Adapters {
|
|
8
|
-
sandbox: SandboxAdapter;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
/** Node:`Router.ws` */
|
|
12
|
-
export declare class SandboxAdapter extends SandboxWsHostAdapter {
|
|
13
|
-
wss?: {
|
|
14
|
-
on: (ev: string, fn: (...args: unknown[]) => void) => void;
|
|
15
|
-
close: () => void;
|
|
16
|
-
};
|
|
17
|
-
constructor(plugin: ReturnType<typeof usePlugin>);
|
|
18
|
-
start(): Promise<void>;
|
|
19
|
-
setupWebSocket(router: SandboxRouter): Promise<void>;
|
|
20
|
-
}
|
|
21
|
-
export { SandboxWsEndpoint, SandboxWsHostAdapter, resolveSandboxEndpoint, bindSandboxWsSocket, parseSandboxWsPayload, type ResolvedSandboxBot, type SandboxWsConfig, type SandboxWsSocket, } from "./sandbox-ws.js";
|
|
22
|
-
//# sourceMappingURL=index.d.ts.map
|
|
1
|
+
export { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, resolveSandboxEndpoint, whenWsOpen, type MessageElement, type MessageType, type ResolvedSandboxBot, type SandboxAdapterConfig, type SandboxWsSocket, } from './protocol.js';
|
|
2
|
+
export { SandboxWsEndpoint, type SandboxEndpointOptions, } from './endpoint.js';
|
package/lib/index.js
CHANGED
|
@@ -1,67 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { SandboxWsHostAdapter, resolveSandboxEndpoint, } from "./sandbox-ws.js";
|
|
4
|
-
const plugin = usePlugin();
|
|
5
|
-
const logger = plugin.logger;
|
|
6
|
-
/** Node:`Router.ws` */
|
|
7
|
-
export class SandboxAdapter extends SandboxWsHostAdapter {
|
|
8
|
-
wss;
|
|
9
|
-
constructor(plugin) {
|
|
10
|
-
const appConfig = (plugin.inject("config")?.getPrimary() ?? {});
|
|
11
|
-
super(plugin, resolveSandboxEndpoint(appConfig));
|
|
12
|
-
}
|
|
13
|
-
async start() {
|
|
14
|
-
await super.start();
|
|
15
|
-
this.registerConfiguredPlaceholder();
|
|
16
|
-
logger.debug(`Sandbox placeholder: ${this.defaults.name} (offline until /sandbox WS)`);
|
|
17
|
-
}
|
|
18
|
-
async setupWebSocket(router) {
|
|
19
|
-
if (this.wss)
|
|
20
|
-
return;
|
|
21
|
-
this.wss = router.ws("/sandbox");
|
|
22
|
-
this.wss.on("connection", (...args) => {
|
|
23
|
-
const ws = args[0];
|
|
24
|
-
const req = args[1];
|
|
25
|
-
logger.debug(`New sandbox connection from ${req.socket?.remoteAddress ?? "unknown"}`);
|
|
26
|
-
const endpoint = this.acceptWebSocket(ws);
|
|
27
|
-
ws.on("close", () => {
|
|
28
|
-
logger.debug(`Sandbox connection closed: ${endpoint.$config.name}`);
|
|
29
|
-
this.endpoints.delete(endpoint.$id);
|
|
30
|
-
});
|
|
31
|
-
ws.on("error", (error) => {
|
|
32
|
-
logger.error(`Sandbox WebSocket error for ${endpoint.$config.name}:`, error);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
logger.debug("Sandbox WebSocket server started at /sandbox (Node Router.ws)");
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
const { provide } = usePlugin();
|
|
39
|
-
provide({
|
|
40
|
-
name: "sandbox",
|
|
41
|
-
description: "Sandbox Adapter — Node Router.ws",
|
|
42
|
-
mounted: async (p) => {
|
|
43
|
-
const adapter = new SandboxAdapter(p);
|
|
44
|
-
await adapter.start();
|
|
45
|
-
return adapter;
|
|
46
|
-
},
|
|
47
|
-
dispose: async (adapter) => {
|
|
48
|
-
for (const endpoint of adapter.endpoints.values()) {
|
|
49
|
-
await endpoint.$disconnect();
|
|
50
|
-
}
|
|
51
|
-
adapter.wss?.close();
|
|
52
|
-
await adapter.stop();
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
plugin.useContext("router", 'sandbox', async (router, adapter) => {
|
|
56
|
-
await adapter.setupWebSocket(router);
|
|
57
|
-
});
|
|
58
|
-
plugin.useContext("web", (pageManager) => {
|
|
59
|
-
pageManager.addEntry({
|
|
60
|
-
id: "sandbox",
|
|
61
|
-
development: path.resolve(import.meta.dirname, "../client/index.tsx"),
|
|
62
|
-
production: path.resolve(import.meta.dirname, "../dist/index.js"),
|
|
63
|
-
meta: { name: "Sandbox" },
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
export { SandboxWsEndpoint, SandboxWsHostAdapter, resolveSandboxEndpoint, bindSandboxWsSocket, parseSandboxWsPayload, } from "./sandbox-ws.js";
|
|
67
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
export { bindSandboxWsSocket, formatSandboxOutbound, parseSandboxWsPayload, resolveSandboxEndpoint, whenWsOpen, } from './protocol.js';
|
|
2
|
+
export { SandboxWsEndpoint, } from './endpoint.js';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Sandbox WebSocket wire protocol helpers (no legacy Adapter/Endpoint). */
|
|
2
|
+
export type MessageType = 'private' | 'group' | 'guild' | 'direct' | 'channel';
|
|
3
|
+
export interface MessageElement {
|
|
4
|
+
readonly type: string;
|
|
5
|
+
readonly data?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface SandboxWsSocket {
|
|
8
|
+
send(data: string): void;
|
|
9
|
+
close(code?: number, reason?: string): void;
|
|
10
|
+
on?(event: 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
|
|
11
|
+
off?(event: 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
|
|
12
|
+
addEventListener?(type: 'message' | 'close' | 'error', listener: (ev: Event | MessageEvent | CloseEvent) => void): void;
|
|
13
|
+
removeEventListener?(type: 'message' | 'close' | 'error', listener: (ev: Event | MessageEvent | CloseEvent) => void): void;
|
|
14
|
+
}
|
|
15
|
+
export type ResolvedSandboxBot = {
|
|
16
|
+
readonly context: 'sandbox';
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly owner: string;
|
|
19
|
+
readonly randomNamePerConnection: boolean;
|
|
20
|
+
};
|
|
21
|
+
export interface SandboxAdapterConfig {
|
|
22
|
+
readonly endpoints?: ReadonlyArray<{
|
|
23
|
+
readonly context?: string;
|
|
24
|
+
readonly name?: string;
|
|
25
|
+
readonly owner?: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
export declare function resolveSandboxEndpoint(appConfig: SandboxAdapterConfig): ResolvedSandboxBot;
|
|
29
|
+
export declare function bindSandboxWsSocket(ws: SandboxWsSocket, handlers: {
|
|
30
|
+
onMessage: (raw: string) => void;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
onError?: (err: unknown) => void;
|
|
33
|
+
}): () => void;
|
|
34
|
+
export declare function parseSandboxWsPayload(raw: string): {
|
|
35
|
+
type: MessageType;
|
|
36
|
+
id: string;
|
|
37
|
+
content: MessageElement[];
|
|
38
|
+
timestamp: number;
|
|
39
|
+
text: string;
|
|
40
|
+
action?: {
|
|
41
|
+
id: string;
|
|
42
|
+
payload: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
export type SandboxOutboundChannel = {
|
|
46
|
+
readonly type?: string;
|
|
47
|
+
readonly id?: string;
|
|
48
|
+
readonly bot?: string;
|
|
49
|
+
readonly endpoint?: string;
|
|
50
|
+
readonly messageId?: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Wire-encode an already-rendered outbound payload.
|
|
54
|
+
* Stamps `channel` so Console SandboxChat can filter by type+id (otherwise
|
|
55
|
+
* replies look like they disappeared).
|
|
56
|
+
*/
|
|
57
|
+
export declare function formatSandboxOutbound(payload: unknown, channel?: SandboxOutboundChannel): string;
|
|
58
|
+
export declare function whenWsOpen(ws: SandboxWsSocket, fn: () => void): void;
|