@zhin.js/adapter-onebot12 3.0.2 → 3.0.3
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 +440 -0
- package/README.md +36 -71
- package/adapters/onebot12.ts +50 -0
- package/lib/index.d.ts +6 -19
- package/lib/index.js +5 -27
- package/lib/protocol.d.ts +151 -0
- package/lib/protocol.js +242 -0
- package/lib/webhook.d.ts +25 -0
- package/lib/webhook.js +140 -0
- package/lib/ws-endpoint.d.ts +25 -0
- package/lib/ws-endpoint.js +240 -0
- package/lib/ws-types.d.ts +16 -0
- package/lib/ws-types.js +1 -0
- package/lib/wss-auth.d.ts +2 -0
- package/lib/wss-auth.js +16 -0
- package/lib/wss-endpoint.d.ts +24 -0
- package/lib/wss-endpoint.js +193 -0
- package/package.json +41 -15
- package/plugin.ts +8 -0
- package/schema.json +40 -0
- package/src/index.ts +46 -37
- package/src/protocol.ts +387 -0
- package/src/webhook.ts +180 -0
- package/src/ws-endpoint.ts +287 -0
- package/src/ws-types.ts +19 -0
- package/src/wss-auth.ts +17 -0
- package/src/wss-endpoint.ts +226 -0
- package/lib/adapter.d.ts +0 -20
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -43
- package/lib/adapter.js.map +0 -1
- package/lib/api.d.ts +0 -14
- package/lib/api.d.ts.map +0 -1
- package/lib/api.js +0 -34
- package/lib/api.js.map +0 -1
- package/lib/endpoint-webhook.d.ts +0 -25
- package/lib/endpoint-webhook.d.ts.map +0 -1
- package/lib/endpoint-webhook.js +0 -122
- package/lib/endpoint-webhook.js.map +0 -1
- package/lib/endpoint-ws.d.ts +0 -26
- package/lib/endpoint-ws.d.ts.map +0 -1
- package/lib/endpoint-ws.js +0 -214
- package/lib/endpoint-ws.js.map +0 -1
- package/lib/endpoint-wss.d.ts +0 -26
- package/lib/endpoint-wss.d.ts.map +0 -1
- package/lib/endpoint-wss.js +0 -191
- package/lib/endpoint-wss.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.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/lib/types.d.ts +0 -77
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -5
- package/lib/types.js.map +0 -1
- package/lib/utils.d.ts +0 -45
- package/lib/utils.d.ts.map +0 -1
- package/lib/utils.js +0 -62
- package/lib/utils.js.map +0 -1
- package/src/adapter.ts +0 -56
- package/src/api.ts +0 -48
- package/src/endpoint-webhook.ts +0 -136
- package/src/endpoint-ws.ts +0 -228
- package/src/endpoint-wss.ts +0 -208
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -86
- package/src/utils.ts +0 -89
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot12 WS client endpoint — outbound connect to OneBot implementation.
|
|
3
|
+
*/
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import { clearInterval, clearTimeout } from 'node:timers';
|
|
6
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
|
+
import { buildSendMessageParams, buildWsConnectOptions, formatInboundContent, formatInboundTarget, formatOutboundSegments, isBotMentioned, isMessageEvent, senderNickname, senderUserId, } from './protocol.js';
|
|
8
|
+
import { WS_OPEN, } from './ws-types.js';
|
|
9
|
+
const logger = getLogger('onebot12');
|
|
10
|
+
export class OneBot12WsEndpoint {
|
|
11
|
+
#options;
|
|
12
|
+
#ws;
|
|
13
|
+
#reconnectTimer;
|
|
14
|
+
#heartbeatTimer;
|
|
15
|
+
#requestId = 0;
|
|
16
|
+
#pending = new Map();
|
|
17
|
+
#open = false;
|
|
18
|
+
#started = false;
|
|
19
|
+
#stopping = false;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.#options = options;
|
|
22
|
+
}
|
|
23
|
+
async start() {
|
|
24
|
+
if (this.#started)
|
|
25
|
+
return;
|
|
26
|
+
this.#started = true;
|
|
27
|
+
this.#stopping = false;
|
|
28
|
+
await this.#connect();
|
|
29
|
+
}
|
|
30
|
+
open() {
|
|
31
|
+
this.#open = true;
|
|
32
|
+
}
|
|
33
|
+
close() {
|
|
34
|
+
this.#open = false;
|
|
35
|
+
}
|
|
36
|
+
async stop() {
|
|
37
|
+
this.#open = false;
|
|
38
|
+
this.#stopping = true;
|
|
39
|
+
this.#started = false;
|
|
40
|
+
if (this.#reconnectTimer) {
|
|
41
|
+
clearTimeout(this.#reconnectTimer);
|
|
42
|
+
this.#reconnectTimer = undefined;
|
|
43
|
+
}
|
|
44
|
+
if (this.#heartbeatTimer) {
|
|
45
|
+
clearInterval(this.#heartbeatTimer);
|
|
46
|
+
this.#heartbeatTimer = undefined;
|
|
47
|
+
}
|
|
48
|
+
for (const [, pending] of this.#pending) {
|
|
49
|
+
clearTimeout(pending.timeout);
|
|
50
|
+
pending.reject(new Error('连接已关闭'));
|
|
51
|
+
}
|
|
52
|
+
this.#pending.clear();
|
|
53
|
+
if (this.#ws) {
|
|
54
|
+
try {
|
|
55
|
+
this.#ws.close();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* ignore */
|
|
59
|
+
}
|
|
60
|
+
this.#ws = undefined;
|
|
61
|
+
}
|
|
62
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
63
|
+
}
|
|
64
|
+
async send({ target, payload }) {
|
|
65
|
+
const message = formatOutboundSegments(payload);
|
|
66
|
+
const params = buildSendMessageParams(target, message);
|
|
67
|
+
const data = await this.#callAction('send_message', params);
|
|
68
|
+
const messageId = data?.message_id ?? '';
|
|
69
|
+
logger.debug(formatCompact({
|
|
70
|
+
op: 'onebot12_send',
|
|
71
|
+
endpoint: this.#options.config.name,
|
|
72
|
+
target,
|
|
73
|
+
messageId,
|
|
74
|
+
}));
|
|
75
|
+
return messageId;
|
|
76
|
+
}
|
|
77
|
+
/** Test / internal: admit a parsed event when the endpoint is open. */
|
|
78
|
+
admit(ev) {
|
|
79
|
+
if (!this.#open || !isMessageEvent(ev))
|
|
80
|
+
return;
|
|
81
|
+
const target = formatInboundTarget(ev);
|
|
82
|
+
const content = formatInboundContent(ev);
|
|
83
|
+
const nickname = senderNickname(ev);
|
|
84
|
+
const mentioned = isBotMentioned(ev);
|
|
85
|
+
void this.#options.gateway.receive({
|
|
86
|
+
adapter: this.#options.id,
|
|
87
|
+
target,
|
|
88
|
+
content,
|
|
89
|
+
sender: senderUserId(ev),
|
|
90
|
+
id: ev.message_id,
|
|
91
|
+
metadata: Object.freeze({
|
|
92
|
+
detail_type: ev.detail_type,
|
|
93
|
+
user_id: ev.user_id,
|
|
94
|
+
group_id: ev.group_id,
|
|
95
|
+
channel_id: ev.channel_id,
|
|
96
|
+
guild_id: ev.guild_id,
|
|
97
|
+
endpoint: this.#options.config.name,
|
|
98
|
+
time: ev.time,
|
|
99
|
+
...(nickname ? { nickname } : {}),
|
|
100
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
101
|
+
}),
|
|
102
|
+
}).catch((err) => {
|
|
103
|
+
logger.warn(formatCompact({
|
|
104
|
+
op: 'onebot12_gateway_receive_failed',
|
|
105
|
+
target,
|
|
106
|
+
error: err instanceof Error ? err.message : String(err),
|
|
107
|
+
}));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async #connect() {
|
|
111
|
+
const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
|
|
112
|
+
const create = this.#options.createWebSocket
|
|
113
|
+
?? ((connectUrl, options) => new WebSocket(connectUrl, { headers: options.headers }));
|
|
114
|
+
await new Promise((resolve, reject) => {
|
|
115
|
+
let settled = false;
|
|
116
|
+
const ws = create(url, { headers });
|
|
117
|
+
this.#ws = ws;
|
|
118
|
+
ws.on('open', () => {
|
|
119
|
+
if (settled)
|
|
120
|
+
return;
|
|
121
|
+
settled = true;
|
|
122
|
+
logger.debug(formatCompact({
|
|
123
|
+
endpoint: this.#options.config.name,
|
|
124
|
+
mode: 'ws',
|
|
125
|
+
url: safeUrl,
|
|
126
|
+
}));
|
|
127
|
+
this.#startHeartbeat();
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
ws.on('message', (data) => {
|
|
131
|
+
this.#onMessage(data);
|
|
132
|
+
});
|
|
133
|
+
ws.on('close', (code, reason) => {
|
|
134
|
+
const reasonStr = typeof reason === 'string'
|
|
135
|
+
? reason
|
|
136
|
+
: Buffer.isBuffer(reason)
|
|
137
|
+
? reason.toString()
|
|
138
|
+
: String(reason ?? '');
|
|
139
|
+
const codeNum = typeof code === 'number' ? code : Number(code ?? 0);
|
|
140
|
+
logger.warn(formatCompact({
|
|
141
|
+
op: 'disconnect',
|
|
142
|
+
endpoint: this.#options.config.name,
|
|
143
|
+
code: codeNum,
|
|
144
|
+
error: reasonStr || 'closed',
|
|
145
|
+
reconnect_ms: this.#options.config.reconnect_interval,
|
|
146
|
+
}));
|
|
147
|
+
if (!settled) {
|
|
148
|
+
settled = true;
|
|
149
|
+
reject(new Error(`OneBot12 WS 关闭: ${codeNum} ${reasonStr}`));
|
|
150
|
+
}
|
|
151
|
+
this.#scheduleReconnect();
|
|
152
|
+
});
|
|
153
|
+
ws.on('error', (err) => {
|
|
154
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
155
|
+
logger.warn(formatCompact({
|
|
156
|
+
op: 'ws_error',
|
|
157
|
+
endpoint: this.#options.config.name,
|
|
158
|
+
ok: false,
|
|
159
|
+
error: error.message,
|
|
160
|
+
}));
|
|
161
|
+
if (!settled) {
|
|
162
|
+
settled = true;
|
|
163
|
+
reject(error);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
#onMessage(data) {
|
|
169
|
+
try {
|
|
170
|
+
const raw = typeof data === 'string'
|
|
171
|
+
? data
|
|
172
|
+
: Buffer.isBuffer(data)
|
|
173
|
+
? data.toString()
|
|
174
|
+
: data instanceof ArrayBuffer
|
|
175
|
+
? new TextDecoder().decode(data)
|
|
176
|
+
: String(data ?? '');
|
|
177
|
+
const msg = JSON.parse(raw);
|
|
178
|
+
if ('echo' in msg && typeof msg.echo === 'string') {
|
|
179
|
+
const resp = msg;
|
|
180
|
+
const pending = this.#pending.get(resp.echo);
|
|
181
|
+
if (pending) {
|
|
182
|
+
this.#pending.delete(resp.echo);
|
|
183
|
+
clearTimeout(pending.timeout);
|
|
184
|
+
if (resp.status === 'ok')
|
|
185
|
+
pending.resolve(resp.data);
|
|
186
|
+
else
|
|
187
|
+
pending.reject(new Error(`OneBot12 retcode=${resp.retcode}: ${resp.message}`));
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.admit(msg);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
logger.warn(formatCompact({
|
|
195
|
+
op: 'onebot12_parse_failed',
|
|
196
|
+
endpoint: this.#options.config.name,
|
|
197
|
+
error: error instanceof Error ? error.message : String(error),
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
#callAction(action, params) {
|
|
202
|
+
if (!this.#ws || this.#ws.readyState !== WS_OPEN) {
|
|
203
|
+
return Promise.reject(new Error('WebSocket 未连接'));
|
|
204
|
+
}
|
|
205
|
+
const echo = `ob12_${++this.#requestId}`;
|
|
206
|
+
const req = { action, params, echo };
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const timeout = setTimeout(() => {
|
|
209
|
+
this.#pending.delete(echo);
|
|
210
|
+
reject(new Error(`OneBot12 动作超时: ${action}`));
|
|
211
|
+
}, 30_000);
|
|
212
|
+
this.#pending.set(echo, { resolve, reject, timeout });
|
|
213
|
+
this.#ws.send(JSON.stringify(req));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
#startHeartbeat() {
|
|
217
|
+
if (this.#heartbeatTimer) {
|
|
218
|
+
clearInterval(this.#heartbeatTimer);
|
|
219
|
+
}
|
|
220
|
+
this.#heartbeatTimer = setInterval(() => {
|
|
221
|
+
this.#callAction('get_status', {}).catch(() => { });
|
|
222
|
+
}, this.#options.config.heartbeat_interval);
|
|
223
|
+
}
|
|
224
|
+
#scheduleReconnect() {
|
|
225
|
+
if (this.#stopping || !this.#started || this.#reconnectTimer)
|
|
226
|
+
return;
|
|
227
|
+
const delay = this.#options.config.reconnect_interval;
|
|
228
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
229
|
+
this.#reconnectTimer = undefined;
|
|
230
|
+
void this.#connect().catch((err) => {
|
|
231
|
+
logger.warn(formatCompact({
|
|
232
|
+
op: 'reconnect',
|
|
233
|
+
endpoint: this.#options.config.name,
|
|
234
|
+
ok: false,
|
|
235
|
+
error: err instanceof Error ? err.message : String(err),
|
|
236
|
+
}));
|
|
237
|
+
});
|
|
238
|
+
}, delay);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Minimal WS surface used by the endpoint (real `ws` or test mock). */
|
|
2
|
+
export interface OneBot12WsSocket {
|
|
3
|
+
readonly readyState: number;
|
|
4
|
+
send(data: string): void;
|
|
5
|
+
close(code?: number, reason?: string): void;
|
|
6
|
+
on(event: 'open' | 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
|
|
7
|
+
}
|
|
8
|
+
export interface OneBot12WsCreateOptions {
|
|
9
|
+
readonly headers?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export declare const WS_OPEN = 1;
|
|
12
|
+
export interface OneBot12PendingAction {
|
|
13
|
+
resolve: (value: unknown) => void;
|
|
14
|
+
reject: (err: Error) => void;
|
|
15
|
+
timeout: NodeJS.Timeout;
|
|
16
|
+
}
|
package/lib/ws-types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const WS_OPEN = 1;
|
package/lib/wss-auth.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function verifyOneBotAccessToken(accessToken, request) {
|
|
2
|
+
if (!accessToken)
|
|
3
|
+
return true;
|
|
4
|
+
const auth = request.headers.authorization ?? '';
|
|
5
|
+
if (auth === `Bearer ${accessToken}`)
|
|
6
|
+
return true;
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
9
|
+
if (url.searchParams.get('access_token') === accessToken)
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
/* ignore */
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 OneBot12Event, type OneBot12WssConfig } from './protocol.js';
|
|
6
|
+
export interface OneBot12WssEndpointOptions {
|
|
7
|
+
readonly id: CapabilityId;
|
|
8
|
+
readonly gateway: MessageGateway;
|
|
9
|
+
readonly http: HttpHost;
|
|
10
|
+
readonly config: OneBot12WssConfig;
|
|
11
|
+
}
|
|
12
|
+
export declare class OneBot12WssEndpoint implements EndpointInstance {
|
|
13
|
+
#private;
|
|
14
|
+
constructor(options: OneBot12WssEndpointOptions);
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
open(): void;
|
|
17
|
+
close(): void;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
send({ target, payload }: {
|
|
20
|
+
readonly target: string;
|
|
21
|
+
readonly payload: unknown;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
admit(ev: OneBot12Event): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot12 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
|
|
3
|
+
*/
|
|
4
|
+
import { clearInterval } from 'node:timers';
|
|
5
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
|
+
import { buildSendMessageParams, formatInboundContent, formatInboundTarget, formatOutboundSegments, isBotMentioned, isMessageEvent, senderNickname, senderUserId, } from './protocol.js';
|
|
7
|
+
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
8
|
+
import { WS_OPEN } from './ws-types.js';
|
|
9
|
+
const logger = getLogger('onebot12');
|
|
10
|
+
export class OneBot12WssEndpoint {
|
|
11
|
+
#options;
|
|
12
|
+
#ws;
|
|
13
|
+
#wsRelease;
|
|
14
|
+
#heartbeatTimer;
|
|
15
|
+
#requestId = 0;
|
|
16
|
+
#pending = new Map();
|
|
17
|
+
#open = false;
|
|
18
|
+
#started = false;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.#options = options;
|
|
21
|
+
}
|
|
22
|
+
async start() {
|
|
23
|
+
if (this.#started)
|
|
24
|
+
return;
|
|
25
|
+
this.#started = true;
|
|
26
|
+
const handle = this.#options.http.ws(this.#options.config.path);
|
|
27
|
+
this.#wsRelease = handle.onConnection((connection) => {
|
|
28
|
+
this.#acceptConnection(connection);
|
|
29
|
+
});
|
|
30
|
+
logger.info(formatCompact({
|
|
31
|
+
op: 'listen',
|
|
32
|
+
endpoint: this.#options.config.name,
|
|
33
|
+
mode: 'wss',
|
|
34
|
+
path: this.#options.config.path,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
open() {
|
|
38
|
+
this.#open = true;
|
|
39
|
+
}
|
|
40
|
+
close() {
|
|
41
|
+
this.#open = false;
|
|
42
|
+
}
|
|
43
|
+
async stop() {
|
|
44
|
+
this.#open = false;
|
|
45
|
+
this.#wsRelease?.();
|
|
46
|
+
this.#wsRelease = undefined;
|
|
47
|
+
if (this.#heartbeatTimer) {
|
|
48
|
+
clearInterval(this.#heartbeatTimer);
|
|
49
|
+
this.#heartbeatTimer = undefined;
|
|
50
|
+
}
|
|
51
|
+
for (const [, pending] of this.#pending) {
|
|
52
|
+
clearTimeout(pending.timeout);
|
|
53
|
+
pending.reject(new Error('连接已关闭'));
|
|
54
|
+
}
|
|
55
|
+
this.#pending.clear();
|
|
56
|
+
if (this.#ws) {
|
|
57
|
+
try {
|
|
58
|
+
this.#ws.close();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* ignore */
|
|
62
|
+
}
|
|
63
|
+
this.#ws = undefined;
|
|
64
|
+
}
|
|
65
|
+
this.#started = false;
|
|
66
|
+
}
|
|
67
|
+
async send({ target, payload }) {
|
|
68
|
+
const message = formatOutboundSegments(payload);
|
|
69
|
+
const params = buildSendMessageParams(target, message);
|
|
70
|
+
const data = await this.#callAction('send_message', params);
|
|
71
|
+
return data?.message_id ?? '';
|
|
72
|
+
}
|
|
73
|
+
admit(ev) {
|
|
74
|
+
if (!this.#open || !isMessageEvent(ev))
|
|
75
|
+
return;
|
|
76
|
+
const target = formatInboundTarget(ev);
|
|
77
|
+
const nickname = senderNickname(ev);
|
|
78
|
+
const mentioned = isBotMentioned(ev);
|
|
79
|
+
void this.#options.gateway.receive({
|
|
80
|
+
adapter: this.#options.id,
|
|
81
|
+
target,
|
|
82
|
+
content: formatInboundContent(ev),
|
|
83
|
+
sender: senderUserId(ev),
|
|
84
|
+
id: ev.message_id,
|
|
85
|
+
metadata: Object.freeze({
|
|
86
|
+
detail_type: ev.detail_type,
|
|
87
|
+
user_id: ev.user_id,
|
|
88
|
+
group_id: ev.group_id,
|
|
89
|
+
channel_id: ev.channel_id,
|
|
90
|
+
guild_id: ev.guild_id,
|
|
91
|
+
endpoint: this.#options.config.name,
|
|
92
|
+
time: ev.time,
|
|
93
|
+
...(nickname ? { nickname } : {}),
|
|
94
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
95
|
+
}),
|
|
96
|
+
}).catch((err) => {
|
|
97
|
+
logger.warn(formatCompact({
|
|
98
|
+
op: 'onebot12_gateway_receive_failed',
|
|
99
|
+
target,
|
|
100
|
+
error: err instanceof Error ? err.message : String(err),
|
|
101
|
+
}));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
#acceptConnection(connection) {
|
|
105
|
+
if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
|
|
106
|
+
connection.socket.close(4003, 'Unauthorized');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const socket = connection.socket;
|
|
110
|
+
if (this.#ws) {
|
|
111
|
+
try {
|
|
112
|
+
this.#ws.close();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* ignore */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.#ws = socket;
|
|
119
|
+
this.#startHeartbeat();
|
|
120
|
+
socket.on('message', (data) => {
|
|
121
|
+
this.#onMessage(data);
|
|
122
|
+
});
|
|
123
|
+
socket.on('close', () => {
|
|
124
|
+
if (this.#ws === socket) {
|
|
125
|
+
this.#ws = undefined;
|
|
126
|
+
if (this.#heartbeatTimer) {
|
|
127
|
+
clearInterval(this.#heartbeatTimer);
|
|
128
|
+
this.#heartbeatTimer = undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
logger.debug(formatCompact({
|
|
133
|
+
endpoint: this.#options.config.name,
|
|
134
|
+
mode: 'wss',
|
|
135
|
+
peer: connection.request.socket.remoteAddress,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
#onMessage(data) {
|
|
139
|
+
try {
|
|
140
|
+
const raw = typeof data === 'string'
|
|
141
|
+
? data
|
|
142
|
+
: Buffer.isBuffer(data)
|
|
143
|
+
? data.toString()
|
|
144
|
+
: data instanceof ArrayBuffer
|
|
145
|
+
? new TextDecoder().decode(data)
|
|
146
|
+
: String(data ?? '');
|
|
147
|
+
const msg = JSON.parse(raw);
|
|
148
|
+
if ('echo' in msg && typeof msg.echo === 'string') {
|
|
149
|
+
const resp = msg;
|
|
150
|
+
const pending = this.#pending.get(resp.echo);
|
|
151
|
+
if (pending) {
|
|
152
|
+
this.#pending.delete(resp.echo);
|
|
153
|
+
clearTimeout(pending.timeout);
|
|
154
|
+
if (resp.status === 'ok')
|
|
155
|
+
pending.resolve(resp.data);
|
|
156
|
+
else
|
|
157
|
+
pending.reject(new Error(`OneBot12 retcode=${resp.retcode}: ${resp.message}`));
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.admit(msg);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
logger.warn(formatCompact({
|
|
165
|
+
op: 'onebot12_parse_failed',
|
|
166
|
+
endpoint: this.#options.config.name,
|
|
167
|
+
error: error instanceof Error ? error.message : String(error),
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
#callAction(action, params) {
|
|
172
|
+
if (!this.#ws || this.#ws.readyState !== WS_OPEN) {
|
|
173
|
+
return Promise.reject(new Error('WebSocket 未连接'));
|
|
174
|
+
}
|
|
175
|
+
const echo = `ob12_${++this.#requestId}`;
|
|
176
|
+
const req = { action, params, echo };
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const timeout = setTimeout(() => {
|
|
179
|
+
this.#pending.delete(echo);
|
|
180
|
+
reject(new Error(`OneBot12 动作超时: ${action}`));
|
|
181
|
+
}, 30_000);
|
|
182
|
+
this.#pending.set(echo, { resolve, reject, timeout });
|
|
183
|
+
this.#ws.send(JSON.stringify(req));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
#startHeartbeat() {
|
|
187
|
+
if (this.#heartbeatTimer)
|
|
188
|
+
clearInterval(this.#heartbeatTimer);
|
|
189
|
+
this.#heartbeatTimer = setInterval(() => {
|
|
190
|
+
this.#callAction('get_status', {}).catch(() => { });
|
|
191
|
+
}, this.#options.config.heartbeat_interval);
|
|
192
|
+
}
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-onebot12",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "Zhin.js adapter for
|
|
3
|
+
"version": "3.0.3",
|
|
4
|
+
"description": "Zhin.js OneBot 12 adapter for Plugin Runtime (WebSocket client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
+
"adapters",
|
|
17
|
+
"plugin.ts",
|
|
18
|
+
"schema.json",
|
|
16
19
|
"src",
|
|
17
20
|
"lib",
|
|
18
|
-
"client",
|
|
19
|
-
"dist",
|
|
20
21
|
"agent",
|
|
21
|
-
"README.md"
|
|
22
|
+
"README.md",
|
|
23
|
+
"CHANGELOG.md"
|
|
22
24
|
],
|
|
23
25
|
"keywords": [
|
|
24
26
|
"zhin",
|
|
@@ -36,23 +38,28 @@
|
|
|
36
38
|
"directory": "plugins/adapters/onebot12"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"ws": "^8.21.0"
|
|
41
|
+
"ws": "^8.21.0",
|
|
42
|
+
"@zhin.js/adapter": "1.0.1",
|
|
43
|
+
"@zhin.js/core": "1.3.5",
|
|
44
|
+
"@zhin.js/host-http": "1.0.1",
|
|
45
|
+
"@zhin.js/logger": "1.0.75",
|
|
46
|
+
"@zhin.js/plugin-runtime": "1.0.1"
|
|
40
47
|
},
|
|
41
48
|
"devDependencies": {
|
|
42
49
|
"@types/node": "^26.1.0",
|
|
43
50
|
"@types/ws": "^8.18.1",
|
|
44
51
|
"typescript": "^6.0.3",
|
|
45
|
-
"
|
|
46
|
-
"@zhin.js/
|
|
47
|
-
"@zhin.js/host-router": "2.0.3",
|
|
48
|
-
"zhin.js": "4.1.2"
|
|
52
|
+
"vitest": "^4.1.10",
|
|
53
|
+
"@zhin.js/host-http": "1.0.1"
|
|
49
54
|
},
|
|
50
55
|
"peerDependencies": {
|
|
51
|
-
"zhin.js": "
|
|
52
|
-
"@zhin.js/
|
|
56
|
+
"@zhin.js/adapter": "1.0.1",
|
|
57
|
+
"@zhin.js/core": "1.3.5",
|
|
58
|
+
"@zhin.js/plugin-runtime": "1.0.1",
|
|
59
|
+
"zhin.js": "4.1.3"
|
|
53
60
|
},
|
|
54
61
|
"peerDependenciesMeta": {
|
|
55
|
-
"
|
|
62
|
+
"zhin.js": {
|
|
56
63
|
"optional": true
|
|
57
64
|
}
|
|
58
65
|
},
|
|
@@ -61,11 +68,30 @@
|
|
|
61
68
|
"email": "admin@liucl.cn",
|
|
62
69
|
"url": "https://github.com/lc-cn"
|
|
63
70
|
},
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public",
|
|
73
|
+
"registry": "https://registry.npmjs.org"
|
|
74
|
+
},
|
|
64
75
|
"engines": {
|
|
65
76
|
"node": "^20.19.0 || >=22.12.0"
|
|
66
77
|
},
|
|
78
|
+
"zhin": {
|
|
79
|
+
"protocol": 1,
|
|
80
|
+
"type": "plugin",
|
|
81
|
+
"entry": "./plugin.ts",
|
|
82
|
+
"engine": "^1.0.0",
|
|
83
|
+
"runtime": "trusted",
|
|
84
|
+
"features": [
|
|
85
|
+
{
|
|
86
|
+
"package": "@zhin.js/adapter",
|
|
87
|
+
"api": "^1.0.0"
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"plugins": []
|
|
91
|
+
},
|
|
67
92
|
"scripts": {
|
|
68
|
-
"build": "
|
|
69
|
-
"clean": "rimraf lib"
|
|
93
|
+
"build": "tsc",
|
|
94
|
+
"clean": "rimraf lib",
|
|
95
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/onebot12/tests"
|
|
70
96
|
}
|
|
71
97
|
}
|
package/plugin.ts
ADDED
package/schema.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"connection": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"enum": ["ws", "webhook", "wss"],
|
|
9
|
+
"default": "ws",
|
|
10
|
+
"description": "ws (default), webhook (httpHostToken POST), or wss (reverse WS via httpHostToken)"
|
|
11
|
+
},
|
|
12
|
+
"name": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"default": "onebot12-bot"
|
|
15
|
+
},
|
|
16
|
+
"url": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "OneBot implementation WebSocket URL (required for connection: ws)"
|
|
19
|
+
},
|
|
20
|
+
"path": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "HTTP/WS path for webhook or reverse-wss"
|
|
23
|
+
},
|
|
24
|
+
"api_url": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "HTTP action endpoint for webhook outbound (required for connection: webhook send)"
|
|
27
|
+
},
|
|
28
|
+
"access_token": {
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"reconnect_interval": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"default": 5000
|
|
34
|
+
},
|
|
35
|
+
"heartbeat_interval": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"default": 30000
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|