@zhin.js/adapter-onebot12 3.0.2 → 4.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 +460 -0
- package/README.md +40 -74
- 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 +66 -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,287 @@
|
|
|
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 type { EndpointInstance } from '@zhin.js/adapter';
|
|
7
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
8
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
|
+
import {
|
|
11
|
+
buildSendMessageParams,
|
|
12
|
+
buildWsConnectOptions,
|
|
13
|
+
formatInboundContent,
|
|
14
|
+
formatInboundTarget,
|
|
15
|
+
formatOutboundSegments,
|
|
16
|
+
isBotMentioned,
|
|
17
|
+
isMessageEvent,
|
|
18
|
+
senderNickname,
|
|
19
|
+
senderUserId,
|
|
20
|
+
type OneBot12ActionRequest,
|
|
21
|
+
type OneBot12ActionResponse,
|
|
22
|
+
type OneBot12Event,
|
|
23
|
+
type OneBot12WsConfig,
|
|
24
|
+
} from './protocol.js';
|
|
25
|
+
import {
|
|
26
|
+
type OneBot12WsCreateOptions,
|
|
27
|
+
type OneBot12WsSocket,
|
|
28
|
+
WS_OPEN,
|
|
29
|
+
} from './ws-types.js';
|
|
30
|
+
|
|
31
|
+
const logger = getLogger('onebot12');
|
|
32
|
+
|
|
33
|
+
export interface OneBot12WsEndpointOptions {
|
|
34
|
+
readonly id: CapabilityId;
|
|
35
|
+
readonly gateway: MessageGateway;
|
|
36
|
+
readonly config: OneBot12WsConfig;
|
|
37
|
+
readonly createWebSocket?: (
|
|
38
|
+
url: string,
|
|
39
|
+
options: OneBot12WsCreateOptions,
|
|
40
|
+
) => OneBot12WsSocket;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class OneBot12WsEndpoint implements EndpointInstance {
|
|
44
|
+
readonly #options: OneBot12WsEndpointOptions;
|
|
45
|
+
#ws?: OneBot12WsSocket;
|
|
46
|
+
#reconnectTimer?: NodeJS.Timeout;
|
|
47
|
+
#heartbeatTimer?: NodeJS.Timeout;
|
|
48
|
+
#requestId = 0;
|
|
49
|
+
#pending = new Map<string, {
|
|
50
|
+
resolve: (value: unknown) => void;
|
|
51
|
+
reject: (err: Error) => void;
|
|
52
|
+
timeout: NodeJS.Timeout;
|
|
53
|
+
}>();
|
|
54
|
+
#open = false;
|
|
55
|
+
#started = false;
|
|
56
|
+
#stopping = false;
|
|
57
|
+
|
|
58
|
+
constructor(options: OneBot12WsEndpointOptions) {
|
|
59
|
+
this.#options = options;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async start(): Promise<void> {
|
|
63
|
+
if (this.#started) return;
|
|
64
|
+
this.#started = true;
|
|
65
|
+
this.#stopping = false;
|
|
66
|
+
await this.#connect();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
open(): void {
|
|
70
|
+
this.#open = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
close(): void {
|
|
74
|
+
this.#open = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async stop(): Promise<void> {
|
|
78
|
+
this.#open = false;
|
|
79
|
+
this.#stopping = true;
|
|
80
|
+
this.#started = false;
|
|
81
|
+
if (this.#reconnectTimer) {
|
|
82
|
+
clearTimeout(this.#reconnectTimer);
|
|
83
|
+
this.#reconnectTimer = undefined;
|
|
84
|
+
}
|
|
85
|
+
if (this.#heartbeatTimer) {
|
|
86
|
+
clearInterval(this.#heartbeatTimer);
|
|
87
|
+
this.#heartbeatTimer = undefined;
|
|
88
|
+
}
|
|
89
|
+
for (const [, pending] of this.#pending) {
|
|
90
|
+
clearTimeout(pending.timeout);
|
|
91
|
+
pending.reject(new Error('连接已关闭'));
|
|
92
|
+
}
|
|
93
|
+
this.#pending.clear();
|
|
94
|
+
if (this.#ws) {
|
|
95
|
+
try {
|
|
96
|
+
this.#ws.close();
|
|
97
|
+
} catch {
|
|
98
|
+
/* ignore */
|
|
99
|
+
}
|
|
100
|
+
this.#ws = undefined;
|
|
101
|
+
}
|
|
102
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
106
|
+
const message = formatOutboundSegments(payload);
|
|
107
|
+
const params = buildSendMessageParams(target, message);
|
|
108
|
+
const data = await this.#callAction('send_message', params) as { message_id?: string } | undefined;
|
|
109
|
+
const messageId = data?.message_id ?? '';
|
|
110
|
+
logger.debug(formatCompact({
|
|
111
|
+
op: 'onebot12_send',
|
|
112
|
+
endpoint: this.#options.config.name,
|
|
113
|
+
target,
|
|
114
|
+
messageId,
|
|
115
|
+
}));
|
|
116
|
+
return messageId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Test / internal: admit a parsed event when the endpoint is open. */
|
|
120
|
+
admit(ev: OneBot12Event): void {
|
|
121
|
+
if (!this.#open || !isMessageEvent(ev)) return;
|
|
122
|
+
const target = formatInboundTarget(ev);
|
|
123
|
+
const content = formatInboundContent(ev);
|
|
124
|
+
const nickname = senderNickname(ev);
|
|
125
|
+
const mentioned = isBotMentioned(ev);
|
|
126
|
+
void this.#options.gateway.receive({
|
|
127
|
+
adapter: this.#options.id,
|
|
128
|
+
target,
|
|
129
|
+
content,
|
|
130
|
+
sender: senderUserId(ev),
|
|
131
|
+
id: ev.message_id,
|
|
132
|
+
metadata: Object.freeze({
|
|
133
|
+
detail_type: ev.detail_type,
|
|
134
|
+
user_id: ev.user_id,
|
|
135
|
+
group_id: ev.group_id,
|
|
136
|
+
channel_id: ev.channel_id,
|
|
137
|
+
guild_id: ev.guild_id,
|
|
138
|
+
endpoint: this.#options.config.name,
|
|
139
|
+
time: ev.time,
|
|
140
|
+
...(nickname ? { nickname } : {}),
|
|
141
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
142
|
+
}),
|
|
143
|
+
}).catch((err) => {
|
|
144
|
+
logger.warn(formatCompact({
|
|
145
|
+
op: 'onebot12_gateway_receive_failed',
|
|
146
|
+
target,
|
|
147
|
+
error: err instanceof Error ? err.message : String(err),
|
|
148
|
+
}));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async #connect(): Promise<void> {
|
|
153
|
+
const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
|
|
154
|
+
const create = this.#options.createWebSocket
|
|
155
|
+
?? ((connectUrl: string, options: OneBot12WsCreateOptions) =>
|
|
156
|
+
new WebSocket(connectUrl, { headers: options.headers }) as unknown as OneBot12WsSocket);
|
|
157
|
+
|
|
158
|
+
await new Promise<void>((resolve, reject) => {
|
|
159
|
+
let settled = false;
|
|
160
|
+
const ws = create(url, { headers });
|
|
161
|
+
this.#ws = ws;
|
|
162
|
+
|
|
163
|
+
ws.on('open', () => {
|
|
164
|
+
if (settled) return;
|
|
165
|
+
settled = true;
|
|
166
|
+
logger.debug(formatCompact({
|
|
167
|
+
endpoint: this.#options.config.name,
|
|
168
|
+
mode: 'ws',
|
|
169
|
+
url: safeUrl,
|
|
170
|
+
}));
|
|
171
|
+
this.#startHeartbeat();
|
|
172
|
+
resolve();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
ws.on('message', (data) => {
|
|
176
|
+
this.#onMessage(data);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
ws.on('close', (code, reason) => {
|
|
180
|
+
const reasonStr = typeof reason === 'string'
|
|
181
|
+
? reason
|
|
182
|
+
: Buffer.isBuffer(reason)
|
|
183
|
+
? reason.toString()
|
|
184
|
+
: String(reason ?? '');
|
|
185
|
+
const codeNum = typeof code === 'number' ? code : Number(code ?? 0);
|
|
186
|
+
logger.warn(formatCompact({
|
|
187
|
+
op: 'disconnect',
|
|
188
|
+
endpoint: this.#options.config.name,
|
|
189
|
+
code: codeNum,
|
|
190
|
+
error: reasonStr || 'closed',
|
|
191
|
+
reconnect_ms: this.#options.config.reconnect_interval,
|
|
192
|
+
}));
|
|
193
|
+
if (!settled) {
|
|
194
|
+
settled = true;
|
|
195
|
+
reject(new Error(`OneBot12 WS 关闭: ${codeNum} ${reasonStr}`));
|
|
196
|
+
}
|
|
197
|
+
this.#scheduleReconnect();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
ws.on('error', (err) => {
|
|
201
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
202
|
+
logger.warn(formatCompact({
|
|
203
|
+
op: 'ws_error',
|
|
204
|
+
endpoint: this.#options.config.name,
|
|
205
|
+
ok: false,
|
|
206
|
+
error: error.message,
|
|
207
|
+
}));
|
|
208
|
+
if (!settled) {
|
|
209
|
+
settled = true;
|
|
210
|
+
reject(error);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#onMessage(data: unknown): void {
|
|
217
|
+
try {
|
|
218
|
+
const raw = typeof data === 'string'
|
|
219
|
+
? data
|
|
220
|
+
: Buffer.isBuffer(data)
|
|
221
|
+
? data.toString()
|
|
222
|
+
: data instanceof ArrayBuffer
|
|
223
|
+
? new TextDecoder().decode(data)
|
|
224
|
+
: String(data ?? '');
|
|
225
|
+
const msg = JSON.parse(raw) as OneBot12Event | OneBot12ActionResponse;
|
|
226
|
+
if ('echo' in msg && typeof (msg as OneBot12ActionResponse).echo === 'string') {
|
|
227
|
+
const resp = msg as OneBot12ActionResponse;
|
|
228
|
+
const pending = this.#pending.get(resp.echo!);
|
|
229
|
+
if (pending) {
|
|
230
|
+
this.#pending.delete(resp.echo!);
|
|
231
|
+
clearTimeout(pending.timeout);
|
|
232
|
+
if (resp.status === 'ok') pending.resolve(resp.data);
|
|
233
|
+
else pending.reject(new Error(`OneBot12 retcode=${resp.retcode}: ${resp.message}`));
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.admit(msg as OneBot12Event);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
logger.warn(formatCompact({
|
|
240
|
+
op: 'onebot12_parse_failed',
|
|
241
|
+
endpoint: this.#options.config.name,
|
|
242
|
+
error: error instanceof Error ? error.message : String(error),
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#callAction(action: string, params: Record<string, unknown>): Promise<unknown> {
|
|
248
|
+
if (!this.#ws || this.#ws.readyState !== WS_OPEN) {
|
|
249
|
+
return Promise.reject(new Error('WebSocket 未连接'));
|
|
250
|
+
}
|
|
251
|
+
const echo = `ob12_${++this.#requestId}`;
|
|
252
|
+
const req: OneBot12ActionRequest = { action, params, echo };
|
|
253
|
+
return new Promise((resolve, reject) => {
|
|
254
|
+
const timeout = setTimeout(() => {
|
|
255
|
+
this.#pending.delete(echo);
|
|
256
|
+
reject(new Error(`OneBot12 动作超时: ${action}`));
|
|
257
|
+
}, 30_000);
|
|
258
|
+
this.#pending.set(echo, { resolve, reject, timeout });
|
|
259
|
+
this.#ws!.send(JSON.stringify(req));
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#startHeartbeat(): void {
|
|
264
|
+
if (this.#heartbeatTimer) {
|
|
265
|
+
clearInterval(this.#heartbeatTimer);
|
|
266
|
+
}
|
|
267
|
+
this.#heartbeatTimer = setInterval(() => {
|
|
268
|
+
this.#callAction('get_status', {}).catch(() => {});
|
|
269
|
+
}, this.#options.config.heartbeat_interval);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#scheduleReconnect(): void {
|
|
273
|
+
if (this.#stopping || !this.#started || this.#reconnectTimer) return;
|
|
274
|
+
const delay = this.#options.config.reconnect_interval;
|
|
275
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
276
|
+
this.#reconnectTimer = undefined;
|
|
277
|
+
void this.#connect().catch((err) => {
|
|
278
|
+
logger.warn(formatCompact({
|
|
279
|
+
op: 'reconnect',
|
|
280
|
+
endpoint: this.#options.config.name,
|
|
281
|
+
ok: false,
|
|
282
|
+
error: err instanceof Error ? err.message : String(err),
|
|
283
|
+
}));
|
|
284
|
+
});
|
|
285
|
+
}, delay);
|
|
286
|
+
}
|
|
287
|
+
}
|
package/src/ws-types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
|
|
9
|
+
export interface OneBot12WsCreateOptions {
|
|
10
|
+
readonly headers?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const WS_OPEN = 1;
|
|
14
|
+
|
|
15
|
+
export interface OneBot12PendingAction {
|
|
16
|
+
resolve: (value: unknown) => void;
|
|
17
|
+
reject: (err: Error) => void;
|
|
18
|
+
timeout: NodeJS.Timeout;
|
|
19
|
+
}
|
package/src/wss-auth.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
|
|
3
|
+
export function verifyOneBotAccessToken(
|
|
4
|
+
accessToken: string | undefined,
|
|
5
|
+
request: IncomingMessage,
|
|
6
|
+
): boolean {
|
|
7
|
+
if (!accessToken) return true;
|
|
8
|
+
const auth = request.headers.authorization ?? '';
|
|
9
|
+
if (auth === `Bearer ${accessToken}`) return true;
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
12
|
+
if (url.searchParams.get('access_token') === accessToken) return true;
|
|
13
|
+
} catch {
|
|
14
|
+
/* ignore */
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot12 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
|
|
3
|
+
*/
|
|
4
|
+
import { clearInterval } from 'node:timers';
|
|
5
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
6
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
|
+
import type { HttpHost, WsConnection } from '@zhin.js/host-http';
|
|
8
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
|
+
import {
|
|
11
|
+
buildSendMessageParams,
|
|
12
|
+
formatInboundContent,
|
|
13
|
+
formatInboundTarget,
|
|
14
|
+
formatOutboundSegments,
|
|
15
|
+
isBotMentioned,
|
|
16
|
+
isMessageEvent,
|
|
17
|
+
senderNickname,
|
|
18
|
+
senderUserId,
|
|
19
|
+
type OneBot12ActionRequest,
|
|
20
|
+
type OneBot12ActionResponse,
|
|
21
|
+
type OneBot12Event,
|
|
22
|
+
type OneBot12WssConfig,
|
|
23
|
+
} from './protocol.js';
|
|
24
|
+
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
25
|
+
import { type OneBot12WsSocket, WS_OPEN } from './ws-types.js';
|
|
26
|
+
|
|
27
|
+
const logger = getLogger('onebot12');
|
|
28
|
+
|
|
29
|
+
export interface OneBot12WssEndpointOptions {
|
|
30
|
+
readonly id: CapabilityId;
|
|
31
|
+
readonly gateway: MessageGateway;
|
|
32
|
+
readonly http: HttpHost;
|
|
33
|
+
readonly config: OneBot12WssConfig;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class OneBot12WssEndpoint implements EndpointInstance {
|
|
37
|
+
readonly #options: OneBot12WssEndpointOptions;
|
|
38
|
+
#ws?: OneBot12WsSocket;
|
|
39
|
+
#wsRelease?: () => void;
|
|
40
|
+
#heartbeatTimer?: NodeJS.Timeout;
|
|
41
|
+
#requestId = 0;
|
|
42
|
+
#pending = new Map<string, {
|
|
43
|
+
resolve: (value: unknown) => void;
|
|
44
|
+
reject: (err: Error) => void;
|
|
45
|
+
timeout: NodeJS.Timeout;
|
|
46
|
+
}>();
|
|
47
|
+
#open = false;
|
|
48
|
+
#started = false;
|
|
49
|
+
|
|
50
|
+
constructor(options: OneBot12WssEndpointOptions) {
|
|
51
|
+
this.#options = options;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async start(): Promise<void> {
|
|
55
|
+
if (this.#started) return;
|
|
56
|
+
this.#started = true;
|
|
57
|
+
const handle = this.#options.http.ws(this.#options.config.path);
|
|
58
|
+
this.#wsRelease = handle.onConnection((connection) => {
|
|
59
|
+
this.#acceptConnection(connection);
|
|
60
|
+
});
|
|
61
|
+
logger.info(formatCompact({
|
|
62
|
+
op: 'listen',
|
|
63
|
+
endpoint: this.#options.config.name,
|
|
64
|
+
mode: 'wss',
|
|
65
|
+
path: this.#options.config.path,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
open(): void {
|
|
70
|
+
this.#open = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
close(): void {
|
|
74
|
+
this.#open = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async stop(): Promise<void> {
|
|
78
|
+
this.#open = false;
|
|
79
|
+
this.#wsRelease?.();
|
|
80
|
+
this.#wsRelease = undefined;
|
|
81
|
+
if (this.#heartbeatTimer) {
|
|
82
|
+
clearInterval(this.#heartbeatTimer);
|
|
83
|
+
this.#heartbeatTimer = undefined;
|
|
84
|
+
}
|
|
85
|
+
for (const [, pending] of this.#pending) {
|
|
86
|
+
clearTimeout(pending.timeout);
|
|
87
|
+
pending.reject(new Error('连接已关闭'));
|
|
88
|
+
}
|
|
89
|
+
this.#pending.clear();
|
|
90
|
+
if (this.#ws) {
|
|
91
|
+
try {
|
|
92
|
+
this.#ws.close();
|
|
93
|
+
} catch {
|
|
94
|
+
/* ignore */
|
|
95
|
+
}
|
|
96
|
+
this.#ws = undefined;
|
|
97
|
+
}
|
|
98
|
+
this.#started = false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
102
|
+
const message = formatOutboundSegments(payload);
|
|
103
|
+
const params = buildSendMessageParams(target, message);
|
|
104
|
+
const data = await this.#callAction('send_message', params) as { message_id?: string } | undefined;
|
|
105
|
+
return data?.message_id ?? '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
admit(ev: OneBot12Event): void {
|
|
109
|
+
if (!this.#open || !isMessageEvent(ev)) return;
|
|
110
|
+
const target = formatInboundTarget(ev);
|
|
111
|
+
const nickname = senderNickname(ev);
|
|
112
|
+
const mentioned = isBotMentioned(ev);
|
|
113
|
+
void this.#options.gateway.receive({
|
|
114
|
+
adapter: this.#options.id,
|
|
115
|
+
target,
|
|
116
|
+
content: formatInboundContent(ev),
|
|
117
|
+
sender: senderUserId(ev),
|
|
118
|
+
id: ev.message_id,
|
|
119
|
+
metadata: Object.freeze({
|
|
120
|
+
detail_type: ev.detail_type,
|
|
121
|
+
user_id: ev.user_id,
|
|
122
|
+
group_id: ev.group_id,
|
|
123
|
+
channel_id: ev.channel_id,
|
|
124
|
+
guild_id: ev.guild_id,
|
|
125
|
+
endpoint: this.#options.config.name,
|
|
126
|
+
time: ev.time,
|
|
127
|
+
...(nickname ? { nickname } : {}),
|
|
128
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
129
|
+
}),
|
|
130
|
+
}).catch((err) => {
|
|
131
|
+
logger.warn(formatCompact({
|
|
132
|
+
op: 'onebot12_gateway_receive_failed',
|
|
133
|
+
target,
|
|
134
|
+
error: err instanceof Error ? err.message : String(err),
|
|
135
|
+
}));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#acceptConnection(connection: WsConnection): void {
|
|
140
|
+
if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
|
|
141
|
+
connection.socket.close(4003, 'Unauthorized');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const socket = connection.socket as unknown as OneBot12WsSocket;
|
|
145
|
+
if (this.#ws) {
|
|
146
|
+
try {
|
|
147
|
+
this.#ws.close();
|
|
148
|
+
} catch {
|
|
149
|
+
/* ignore */
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
this.#ws = socket;
|
|
153
|
+
this.#startHeartbeat();
|
|
154
|
+
socket.on('message', (data) => {
|
|
155
|
+
this.#onMessage(data);
|
|
156
|
+
});
|
|
157
|
+
socket.on('close', () => {
|
|
158
|
+
if (this.#ws === socket) {
|
|
159
|
+
this.#ws = undefined;
|
|
160
|
+
if (this.#heartbeatTimer) {
|
|
161
|
+
clearInterval(this.#heartbeatTimer);
|
|
162
|
+
this.#heartbeatTimer = undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
logger.debug(formatCompact({
|
|
167
|
+
endpoint: this.#options.config.name,
|
|
168
|
+
mode: 'wss',
|
|
169
|
+
peer: connection.request.socket.remoteAddress,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#onMessage(data: unknown): void {
|
|
174
|
+
try {
|
|
175
|
+
const raw = typeof data === 'string'
|
|
176
|
+
? data
|
|
177
|
+
: Buffer.isBuffer(data)
|
|
178
|
+
? data.toString()
|
|
179
|
+
: data instanceof ArrayBuffer
|
|
180
|
+
? new TextDecoder().decode(data)
|
|
181
|
+
: String(data ?? '');
|
|
182
|
+
const msg = JSON.parse(raw) as OneBot12Event | OneBot12ActionResponse;
|
|
183
|
+
if ('echo' in msg && typeof (msg as OneBot12ActionResponse).echo === 'string') {
|
|
184
|
+
const resp = msg as OneBot12ActionResponse;
|
|
185
|
+
const pending = this.#pending.get(resp.echo!);
|
|
186
|
+
if (pending) {
|
|
187
|
+
this.#pending.delete(resp.echo!);
|
|
188
|
+
clearTimeout(pending.timeout);
|
|
189
|
+
if (resp.status === 'ok') pending.resolve(resp.data);
|
|
190
|
+
else pending.reject(new Error(`OneBot12 retcode=${resp.retcode}: ${resp.message}`));
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.admit(msg as OneBot12Event);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.warn(formatCompact({
|
|
197
|
+
op: 'onebot12_parse_failed',
|
|
198
|
+
endpoint: this.#options.config.name,
|
|
199
|
+
error: error instanceof Error ? error.message : String(error),
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#callAction(action: string, params: Record<string, unknown>): Promise<unknown> {
|
|
205
|
+
if (!this.#ws || this.#ws.readyState !== WS_OPEN) {
|
|
206
|
+
return Promise.reject(new Error('WebSocket 未连接'));
|
|
207
|
+
}
|
|
208
|
+
const echo = `ob12_${++this.#requestId}`;
|
|
209
|
+
const req: OneBot12ActionRequest = { action, params, echo };
|
|
210
|
+
return new Promise((resolve, reject) => {
|
|
211
|
+
const timeout = setTimeout(() => {
|
|
212
|
+
this.#pending.delete(echo);
|
|
213
|
+
reject(new Error(`OneBot12 动作超时: ${action}`));
|
|
214
|
+
}, 30_000);
|
|
215
|
+
this.#pending.set(echo, { resolve, reject, timeout });
|
|
216
|
+
this.#ws!.send(JSON.stringify(req));
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#startHeartbeat(): void {
|
|
221
|
+
if (this.#heartbeatTimer) clearInterval(this.#heartbeatTimer);
|
|
222
|
+
this.#heartbeatTimer = setInterval(() => {
|
|
223
|
+
this.#callAction('get_status', {}).catch(() => {});
|
|
224
|
+
}, this.#options.config.heartbeat_interval);
|
|
225
|
+
}
|
|
226
|
+
}
|
package/lib/adapter.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OneBot 12 适配器:单一适配器支持正向 WS / Webhook / 反向 WS,由 config.connection 区分
|
|
3
|
-
* 协议文档:https://12.onebot.dev/
|
|
4
|
-
*/
|
|
5
|
-
import { Adapter, Plugin } from 'zhin.js';
|
|
6
|
-
import { OneBot12WsClient } from './endpoint-ws.js';
|
|
7
|
-
import { OneBot12WebhookEndpoint } from './endpoint-webhook.js';
|
|
8
|
-
import { OneBot12WssServer } from './endpoint-wss.js';
|
|
9
|
-
import type { OneBot12EndpointConfig } from './types.js';
|
|
10
|
-
export type OneBot12Bot = OneBot12WsClient | OneBot12WebhookEndpoint | OneBot12WssServer;
|
|
11
|
-
export declare class OneBot12Adapter extends Adapter<OneBot12Bot> {
|
|
12
|
-
#private;
|
|
13
|
-
static readonly capabilities: readonly ["inbound", "outbound"];
|
|
14
|
-
static outboundRichSegmentPolicy: import("zhin.js").OutboundRichSegmentPolicy;
|
|
15
|
-
static interactivePolicy: "text";
|
|
16
|
-
constructor(plugin: Plugin);
|
|
17
|
-
createEndpoint(config: OneBot12EndpointConfig): OneBot12Bot;
|
|
18
|
-
start(): Promise<void>;
|
|
19
|
-
}
|
|
20
|
-
//# sourceMappingURL=adapter.d.ts.map
|
package/lib/adapter.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAiB,OAAO,EAAE,MAAM,EAAwC,MAAM,SAAS,CAAC;AAE/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EACV,sBAAsB,EAIvB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,WAAW,GAAG,gBAAgB,GAAG,uBAAuB,GAAG,iBAAiB,CAAC;AAEzF,qBAAa,eAAgB,SAAQ,OAAO,CAAC,WAAW,CAAC;;IACvD,gBAAyB,YAAY,mCAAoC;IACzE,OAAgB,yBAAyB,8CAAwC;IACjF,OAAgB,iBAAiB,EAAG,MAAM,CAAU;gBAIxC,MAAM,EAAE,MAAM;IAI1B,cAAc,CAAC,MAAM,EAAE,sBAAsB,GAAG,WAAW;IAmBrD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAO7B"}
|
package/lib/adapter.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OneBot 12 适配器:单一适配器支持正向 WS / Webhook / 反向 WS,由 config.connection 区分
|
|
3
|
-
* 协议文档:https://12.onebot.dev/
|
|
4
|
-
*/
|
|
5
|
-
import { Adapter, OUTBOUND_RICH_SEGMENT_POLICY_IM_FULL } from 'zhin.js';
|
|
6
|
-
import { OneBot12WsClient } from './endpoint-ws.js';
|
|
7
|
-
import { OneBot12WebhookEndpoint } from './endpoint-webhook.js';
|
|
8
|
-
import { OneBot12WssServer } from './endpoint-wss.js';
|
|
9
|
-
export class OneBot12Adapter extends Adapter {
|
|
10
|
-
static capabilities = ['inbound', 'outbound'];
|
|
11
|
-
static outboundRichSegmentPolicy = OUTBOUND_RICH_SEGMENT_POLICY_IM_FULL;
|
|
12
|
-
static interactivePolicy = 'text';
|
|
13
|
-
#router;
|
|
14
|
-
constructor(plugin) {
|
|
15
|
-
super(plugin, 'onebot12', []);
|
|
16
|
-
}
|
|
17
|
-
createEndpoint(config) {
|
|
18
|
-
switch (config.connection) {
|
|
19
|
-
case 'ws':
|
|
20
|
-
return new OneBot12WsClient(this, config);
|
|
21
|
-
case 'webhook':
|
|
22
|
-
if (!this.#router) {
|
|
23
|
-
throw new Error('OneBot12 connection: webhook 需要 router,请安装并在配置中启用 @zhin.js/host-router');
|
|
24
|
-
}
|
|
25
|
-
return new OneBot12WebhookEndpoint(this, this.#router, config);
|
|
26
|
-
case 'wss':
|
|
27
|
-
if (!this.#router) {
|
|
28
|
-
throw new Error('OneBot12 connection: wss 需要 router,请安装并在配置中启用 @zhin.js/host-router');
|
|
29
|
-
}
|
|
30
|
-
return new OneBot12WssServer(this, this.#router, config);
|
|
31
|
-
default:
|
|
32
|
-
throw new Error(`Unknown OneBot12 connection: ${config.connection}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
async start() {
|
|
36
|
-
this.#router = this.plugin.inject('router');
|
|
37
|
-
this.plugin.useContext('router', (router) => {
|
|
38
|
-
this.#router = router;
|
|
39
|
-
});
|
|
40
|
-
await super.start();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
//# sourceMappingURL=adapter.js.map
|
package/lib/adapter.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAiB,OAAO,EAAU,oCAAoC,EAAE,MAAM,SAAS,CAAC;AAE/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAUtD,MAAM,OAAO,eAAgB,SAAQ,OAAoB;IACvD,MAAM,CAAmB,YAAY,GAAG,CAAC,SAAS,EAAE,UAAU,CAAU,CAAC;IACzE,MAAM,CAAU,yBAAyB,GAAG,oCAAoC,CAAC;IACjF,MAAM,CAAU,iBAAiB,GAAG,MAAe,CAAC;IAEpD,OAAO,CAAU;IAEjB,YAAY,MAAc;QACxB,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;IAChC,CAAC;IAED,cAAc,CAAC,MAA8B;QAC3C,QAAQ,MAAM,CAAC,UAAU,EAAE,CAAC;YAC1B,KAAK,IAAI;gBACP,OAAO,IAAI,gBAAgB,CAAC,IAAI,EAAE,MAA0B,CAAC,CAAC;YAChE,KAAK,SAAS;gBACZ,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;oBAClB,MAAM,IAAI,KAAK,CAAC,wEAAwE,CAAC,CAAC;gBAC5F,CAAC;gBACD,OAAO,IAAI,uBAAuB,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,MAA+B,CAAC,CAAC;YAC1F,KAAK,KAAK;gBACR,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;oBAClB,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;gBACxF,CAAC;gBACD,OAAO,IAAI,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,MAA2B,CAAC,CAAC;YAChF;gBACE,MAAM,IAAI,KAAK,CAAC,gCAAiC,MAAiC,CAAC,UAAU,EAAE,CAAC,CAAC;QACrG,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,OAAO,GAAI,IAAI,CAAC,MAAM,CAAC,MAA8C,CAAC,QAAQ,CAAC,CAAC;QACpF,IAAI,CAAC,MAAM,CAAC,UAAkE,CAAC,QAAQ,EAAE,CAAC,MAAc,EAAE,EAAE;YAC3G,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACxB,CAAC,CAAC,CAAC;QACH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC"}
|
package/lib/api.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OneBot 12 HTTP 动作调用:POST 动作请求到实现的 HTTP 端点,返回动作响应
|
|
3
|
-
* 参考 https://12.onebot.dev/connect/communication/http/
|
|
4
|
-
*/
|
|
5
|
-
import type { OneBot12ActionResponse } from './types.js';
|
|
6
|
-
export interface OneBot12HttpOptions {
|
|
7
|
-
url: string;
|
|
8
|
-
access_token?: string;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应
|
|
12
|
-
*/
|
|
13
|
-
export declare function callOneBot12Action(options: OneBot12HttpOptions, action: string, params?: Record<string, unknown>, echo?: string): Promise<OneBot12ActionResponse>;
|
|
14
|
-
//# sourceMappingURL=api.d.ts.map
|
package/lib/api.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAyB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEhF,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,mBAAmB,EAC5B,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,sBAAsB,CAAC,CA4BjC"}
|
package/lib/api.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应
|
|
3
|
-
*/
|
|
4
|
-
export async function callOneBot12Action(options, action, params = {}, echo) {
|
|
5
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
6
|
-
if (options.access_token) {
|
|
7
|
-
headers['Authorization'] = `Bearer ${options.access_token}`;
|
|
8
|
-
}
|
|
9
|
-
const body = { action, params };
|
|
10
|
-
if (echo)
|
|
11
|
-
body.echo = echo;
|
|
12
|
-
const res = await fetch(options.url, {
|
|
13
|
-
method: 'POST',
|
|
14
|
-
headers,
|
|
15
|
-
body: JSON.stringify(body),
|
|
16
|
-
});
|
|
17
|
-
const text = await res.text();
|
|
18
|
-
if (res.status === 401)
|
|
19
|
-
throw new Error(`OneBot12 鉴权失败: ${text}`);
|
|
20
|
-
if (res.status !== 200)
|
|
21
|
-
throw new Error(`OneBot12 HTTP ${res.status}: ${text}`);
|
|
22
|
-
let data;
|
|
23
|
-
try {
|
|
24
|
-
data = JSON.parse(text);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
throw new Error(`OneBot12 无效响应: ${text.slice(0, 200)}`);
|
|
28
|
-
}
|
|
29
|
-
if (data.status === 'failed' && data.retcode !== 0) {
|
|
30
|
-
throw new Error(`OneBot12 动作失败 retcode=${data.retcode}: ${data.message}`);
|
|
31
|
-
}
|
|
32
|
-
return data;
|
|
33
|
-
}
|
|
34
|
-
//# sourceMappingURL=api.js.map
|
package/lib/api.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAWA;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAA4B,EAC5B,MAAc,EACd,SAAkC,EAAE,EACpC,IAAa;IAEb,MAAM,OAAO,GAA2B,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;IAC/E,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;QACzB,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,OAAO,CAAC,YAAY,EAAE,CAAC;IAC9D,CAAC;IACD,MAAM,IAAI,GAA0B,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACvD,IAAI,IAAI;QAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAE3B,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QACnC,MAAM,EAAE,MAAM;QACd,OAAO;QACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC;IAClE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC;IAEhF,IAAI,IAA4B,CAAC;IACjC,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAA2B,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,kBAAkB,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|