@zhin.js/adapter-onebot12 6.0.0 → 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 +20 -0
- package/README.md +21 -2
- package/adapters/onebot12.js +0 -9
- package/adapters/onebot12.ts +0 -9
- package/lib/client.d.ts +20 -0
- package/lib/client.js +33 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +2 -0
- package/lib/onebot12-endpoint-commands.d.ts +1 -1
- package/lib/protocol.d.ts +2 -2
- package/lib/protocol.js +1 -1
- package/lib/side-event-dispatch.d.ts +2 -2
- package/lib/side-event-dispatch.js +3 -3
- package/lib/webhook.d.ts +5 -11
- package/lib/webhook.js +34 -50
- package/lib/ws-endpoint.d.ts +5 -12
- package/lib/ws-endpoint.js +30 -30
- package/lib/wss-endpoint.d.ts +8 -11
- package/lib/wss-endpoint.js +58 -66
- package/package.json +14 -11
- package/src/client.ts +81 -0
- package/src/index.ts +16 -0
- package/src/protocol.ts +2 -2
- package/src/side-event-dispatch.ts +4 -4
- package/src/webhook.ts +42 -54
- package/src/ws-endpoint.ts +37 -36
- package/src/wss-endpoint.ts +66 -69
package/src/webhook.ts
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
5
|
import {
|
|
6
|
+
ClientEndpoint,
|
|
6
7
|
createRecallEndpointControl,
|
|
7
8
|
type EndpointControl,
|
|
8
|
-
type EndpointInstance,
|
|
9
9
|
type EndpointManagement,
|
|
10
10
|
type EndpointSendRequest,
|
|
11
11
|
} from 'zhin.js/adapter';
|
|
12
|
-
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
13
12
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
14
13
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
15
14
|
import type { CapabilityId } from 'zhin.js';
|
|
@@ -30,32 +29,43 @@ import {
|
|
|
30
29
|
} from './protocol.js';
|
|
31
30
|
import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
|
|
32
31
|
import { createOneBot12ContentPort } from './content-port.js';
|
|
32
|
+
import { callOnebot12Client, createOnebot12EndpointClient, forwardOnebot12ClientEvents, type Onebot12Client } from './client.js';
|
|
33
33
|
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
34
34
|
|
|
35
35
|
const logger = getLogger('onebot12');
|
|
36
36
|
|
|
37
37
|
export interface OneBot12WebhookEndpointOptions {
|
|
38
38
|
readonly id: CapabilityId;
|
|
39
|
-
readonly gateway: MessageGateway;
|
|
40
|
-
readonly sideEvents?: SideEventGateway;
|
|
41
39
|
readonly http: HttpHost;
|
|
42
40
|
readonly config: OneBot12WebhookConfig;
|
|
43
41
|
readonly callAction?: typeof callOneBot12Action;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
export class OneBot12WebhookEndpoint
|
|
44
|
+
export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
45
|
+
readonly client: Onebot12Client;
|
|
47
46
|
readonly #options: OneBot12WebhookEndpointOptions;
|
|
48
|
-
readonly management: EndpointManagement
|
|
47
|
+
readonly management: EndpointManagement;
|
|
49
48
|
readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
50
|
-
readonly content
|
|
49
|
+
readonly content;
|
|
51
50
|
readonly #callAction: typeof callOneBot12Action;
|
|
52
51
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
53
|
-
#open = false;
|
|
54
52
|
#started = false;
|
|
55
53
|
|
|
56
54
|
constructor(options: OneBot12WebhookEndpointOptions) {
|
|
55
|
+
super();
|
|
57
56
|
this.#options = options;
|
|
58
57
|
this.#callAction = options.callAction ?? callOneBot12Action;
|
|
58
|
+
this.client = createOnebot12EndpointClient(options.config, (action, params) => this.#callApi(action, params));
|
|
59
|
+
const callApi = (action: string, params?: Record<string, unknown>) => callOnebot12Client(this.client, action, params);
|
|
60
|
+
this.management = createOneBot12EndpointManagement({ callApi });
|
|
61
|
+
this.content = createOneBot12ContentPort(callApi);
|
|
62
|
+
this.bindClientEvents(
|
|
63
|
+
(receive) => forwardOnebot12ClientEvents(this.client, receive),
|
|
64
|
+
(name, payload) => {
|
|
65
|
+
if (name === 'event') this.#admitRaw(payload as OneBot12Event);
|
|
66
|
+
},
|
|
67
|
+
(_name, error) => this.#warnPlatformEvent(error),
|
|
68
|
+
);
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
async start(): Promise<void> {
|
|
@@ -79,16 +89,8 @@ export class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
|
79
89
|
}));
|
|
80
90
|
}
|
|
81
91
|
|
|
82
|
-
open(): void {
|
|
83
|
-
this.#open = true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
close(): void {
|
|
87
|
-
this.#open = false;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
92
|
async stop(): Promise<void> {
|
|
91
|
-
this
|
|
93
|
+
this.close();
|
|
92
94
|
for (const release of this.#routeReleases.splice(0)) release();
|
|
93
95
|
this.#started = false;
|
|
94
96
|
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.id }));
|
|
@@ -97,7 +99,7 @@ export class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
|
97
99
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
98
100
|
const materialized = await uploadOneBot12MediaSegments(
|
|
99
101
|
payload,
|
|
100
|
-
(action, params) => this.
|
|
102
|
+
(action, params) => callOnebot12Client(this.client, action, params),
|
|
101
103
|
(error) => {
|
|
102
104
|
logger.warn(formatCompact({
|
|
103
105
|
op: 'onebot12_upload_failed',
|
|
@@ -108,7 +110,7 @@ export class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
|
108
110
|
);
|
|
109
111
|
const message = formatOutboundSegments(materialized);
|
|
110
112
|
const params = buildSendMessageParams(conversation, message);
|
|
111
|
-
const data = await
|
|
113
|
+
const data = await callOnebot12Client<{ message_id?: string }>(this.client, 'send_message', params);
|
|
112
114
|
const messageId = data?.message_id ?? '';
|
|
113
115
|
logger.debug(formatCompact({
|
|
114
116
|
op: 'onebot12_send',
|
|
@@ -122,30 +124,30 @@ export class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
|
122
124
|
|
|
123
125
|
async recallMessage(messageId: string): Promise<void> {
|
|
124
126
|
if (!messageId) return;
|
|
125
|
-
await this.
|
|
127
|
+
await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
async #callApi(
|
|
131
|
+
action: string,
|
|
132
|
+
params: Record<string, unknown> = {},
|
|
133
|
+
): Promise<import('@imhelper/onebot-v12').OneBotV12Response> {
|
|
130
134
|
const apiUrl = this.#options.config.api_url;
|
|
131
135
|
if (!apiUrl) {
|
|
132
136
|
throw new Error('OneBot12 connection:webhook requires api_url for outbound api');
|
|
133
137
|
}
|
|
134
|
-
|
|
138
|
+
return this.#callAction(
|
|
135
139
|
{ url: apiUrl, access_token: this.#options.config.access_token },
|
|
136
140
|
action,
|
|
137
141
|
params,
|
|
138
142
|
);
|
|
139
|
-
return resp.data;
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
if (!this.#open) return;
|
|
145
|
+
#admitRaw(ev: OneBot12Event): void {
|
|
144
146
|
if (!isMessageEvent(ev)) {
|
|
145
147
|
receiveOneBot12SideEvent(
|
|
146
|
-
this
|
|
148
|
+
(name, payload) => this.emit(name, payload),
|
|
147
149
|
this.#options.config.id,
|
|
148
|
-
this,
|
|
150
|
+
{ callApi: (action, params) => callOnebot12Client(this.client, action, params) },
|
|
149
151
|
ev,
|
|
150
152
|
logger,
|
|
151
153
|
);
|
|
@@ -155,7 +157,7 @@ export class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
|
155
157
|
const content = formatInboundContent(ev);
|
|
156
158
|
const nickname = senderNickname(ev);
|
|
157
159
|
const mentioned = isBotMentioned(ev);
|
|
158
|
-
void this
|
|
160
|
+
void this.emit('message.receive', {
|
|
159
161
|
conversation,
|
|
160
162
|
message: { conversation, id: ev.message_id },
|
|
161
163
|
content,
|
|
@@ -181,6 +183,13 @@ export class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
|
181
183
|
});
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
#warnPlatformEvent(error: unknown): void {
|
|
187
|
+
logger.warn(formatCompact({
|
|
188
|
+
op: 'onebot12_platform_event_failed',
|
|
189
|
+
error: error instanceof Error ? error.message : String(error),
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
|
|
184
193
|
#setupRoutes(): void {
|
|
185
194
|
const path = this.#options.config.path;
|
|
186
195
|
this.#routeReleases.push(
|
|
@@ -197,18 +206,12 @@ export class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
|
197
206
|
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
198
207
|
return;
|
|
199
208
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
ev = JSON.parse(raw) as OneBot12Event;
|
|
204
|
-
} catch {
|
|
205
|
-
response.writeHead(400, { 'Content-Type': 'application/json' });
|
|
206
|
-
response.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
209
|
+
if (!this.clientEventsOpen) {
|
|
210
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
211
|
+
response.end(JSON.stringify({ status: 'ok' }));
|
|
207
212
|
return;
|
|
208
213
|
}
|
|
209
|
-
|
|
210
|
-
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
211
|
-
response.end(JSON.stringify({ status: 'ok' }));
|
|
214
|
+
await this.client.acceptHttp(request, response);
|
|
212
215
|
} catch (error) {
|
|
213
216
|
logger.error('OneBot12 webhook error:', error);
|
|
214
217
|
if (!response.headersSent) {
|
|
@@ -218,18 +221,3 @@ export class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
|
218
221
|
}
|
|
219
222
|
}
|
|
220
223
|
}
|
|
221
|
-
|
|
222
|
-
async function readRequestBody(request: IncomingMessage): Promise<string> {
|
|
223
|
-
const chunks: Buffer[] = [];
|
|
224
|
-
let size = 0;
|
|
225
|
-
for await (const chunk of request) {
|
|
226
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
227
|
-
size += buffer.length;
|
|
228
|
-
if (size > 1_048_576) {
|
|
229
|
-
request.destroy();
|
|
230
|
-
throw new Error('Request body exceeds 1MB');
|
|
231
|
-
}
|
|
232
|
-
chunks.push(buffer);
|
|
233
|
-
}
|
|
234
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
235
|
-
}
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -4,16 +4,15 @@
|
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
5
|
import { clearTimeout } from 'node:timers';
|
|
6
6
|
import {
|
|
7
|
+
ClientEndpoint,
|
|
7
8
|
createRecallEndpointControl,
|
|
8
9
|
createEndpointLifecycle,
|
|
9
10
|
type EndpointConnectHandle,
|
|
10
11
|
type EndpointControl,
|
|
11
|
-
type EndpointInstance,
|
|
12
12
|
type EndpointLifecycle,
|
|
13
13
|
type EndpointManagement,
|
|
14
14
|
type EndpointSendRequest,
|
|
15
15
|
} from 'zhin.js/adapter';
|
|
16
|
-
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
17
16
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
18
17
|
import type { CapabilityId } from 'zhin.js';
|
|
19
18
|
import { createOneBot12EndpointManagement } from './endpoint-management.js';
|
|
@@ -35,6 +34,7 @@ import {
|
|
|
35
34
|
} from './protocol.js';
|
|
36
35
|
import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
|
|
37
36
|
import { createOneBot12ContentPort } from './content-port.js';
|
|
37
|
+
import { callOnebot12Client, createOnebot12EndpointClient, forwardOnebot12ClientEvents, type Onebot12Client } from './client.js';
|
|
38
38
|
import {
|
|
39
39
|
type OneBot12WsCreateOptions,
|
|
40
40
|
type OneBot12WsSocket,
|
|
@@ -43,8 +43,6 @@ import {
|
|
|
43
43
|
|
|
44
44
|
export interface OneBot12WsEndpointOptions {
|
|
45
45
|
readonly id: CapabilityId;
|
|
46
|
-
readonly gateway: MessageGateway;
|
|
47
|
-
readonly sideEvents?: SideEventGateway;
|
|
48
46
|
readonly config: OneBot12WsConfig;
|
|
49
47
|
readonly createWebSocket?: (
|
|
50
48
|
url: string,
|
|
@@ -52,26 +50,38 @@ export interface OneBot12WsEndpointOptions {
|
|
|
52
50
|
) => OneBot12WsSocket;
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
export class OneBot12WsEndpoint
|
|
53
|
+
export class OneBot12WsEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
54
|
+
readonly client: Onebot12Client;
|
|
56
55
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
57
56
|
|
|
58
57
|
readonly #options: OneBot12WsEndpointOptions;
|
|
59
|
-
readonly management: EndpointManagement
|
|
58
|
+
readonly management: EndpointManagement;
|
|
60
59
|
readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
61
|
-
readonly content
|
|
60
|
+
readonly content;
|
|
62
61
|
readonly #lifecycle: EndpointLifecycle;
|
|
63
62
|
#ws?: OneBot12WsSocket;
|
|
64
63
|
#requestId = 0;
|
|
65
64
|
#pending = new Map<string, {
|
|
66
|
-
resolve: (value:
|
|
65
|
+
resolve: (value: OneBot12ActionResponse) => void;
|
|
67
66
|
reject: (err: Error) => void;
|
|
68
67
|
timeout: NodeJS.Timeout;
|
|
69
68
|
}>();
|
|
70
|
-
#open = false;
|
|
71
69
|
|
|
72
70
|
constructor(options: OneBot12WsEndpointOptions) {
|
|
71
|
+
super();
|
|
73
72
|
this.#logger = getAdapterLogger('onebot12', options.config.id);
|
|
74
73
|
this.#options = options;
|
|
74
|
+
this.client = createOnebot12EndpointClient(options.config, (action, params) => this.#callAction(action, params ?? {}));
|
|
75
|
+
const callApi = (action: string, params?: Record<string, unknown>) => callOnebot12Client(this.client, action, params);
|
|
76
|
+
this.management = createOneBot12EndpointManagement({ callApi });
|
|
77
|
+
this.content = createOneBot12ContentPort(callApi);
|
|
78
|
+
this.bindClientEvents(
|
|
79
|
+
(receive) => forwardOnebot12ClientEvents(this.client, receive),
|
|
80
|
+
(name, payload) => {
|
|
81
|
+
if (name === 'event') this.#admitRaw(payload as OneBot12Event);
|
|
82
|
+
},
|
|
83
|
+
(_name, error) => this.#warnPlatformEvent(error),
|
|
84
|
+
);
|
|
75
85
|
const { config } = options;
|
|
76
86
|
this.#lifecycle = createEndpointLifecycle({
|
|
77
87
|
name: config.id,
|
|
@@ -104,16 +114,8 @@ export class OneBot12WsEndpoint implements EndpointInstance {
|
|
|
104
114
|
}
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
open(): void {
|
|
108
|
-
this.#open = true;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
close(): void {
|
|
112
|
-
this.#open = false;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
117
|
async stop(): Promise<void> {
|
|
116
|
-
this
|
|
118
|
+
this.close();
|
|
117
119
|
// 基座负责:清重连/心跳定时器、强关 ws、唤醒 stop-during-connect 竞态
|
|
118
120
|
await this.#lifecycle.stop();
|
|
119
121
|
for (const [, pending] of this.#pending) {
|
|
@@ -127,7 +129,7 @@ export class OneBot12WsEndpoint implements EndpointInstance {
|
|
|
127
129
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
128
130
|
const materialized = await uploadOneBot12MediaSegments(
|
|
129
131
|
payload,
|
|
130
|
-
(action, params) => this.
|
|
132
|
+
(action, params) => callOnebot12Client(this.client, action, params),
|
|
131
133
|
(error) => {
|
|
132
134
|
this.#logger.warn(formatCompact({
|
|
133
135
|
op: 'onebot12_upload_failed',
|
|
@@ -138,7 +140,7 @@ export class OneBot12WsEndpoint implements EndpointInstance {
|
|
|
138
140
|
);
|
|
139
141
|
const message = formatOutboundSegments(materialized);
|
|
140
142
|
const params = buildSendMessageParams(conversation, message);
|
|
141
|
-
const data = await
|
|
143
|
+
const data = await callOnebot12Client<{ message_id?: string }>(this.client, 'send_message', params);
|
|
142
144
|
const messageId = data?.message_id ?? '';
|
|
143
145
|
this.#logger.debug(formatCompact({
|
|
144
146
|
op: 'onebot12_send',
|
|
@@ -152,22 +154,15 @@ export class OneBot12WsEndpoint implements EndpointInstance {
|
|
|
152
154
|
|
|
153
155
|
async recallMessage(messageId: string): Promise<void> {
|
|
154
156
|
if (!messageId) return;
|
|
155
|
-
await this
|
|
157
|
+
await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
|
|
159
|
-
callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
|
|
160
|
-
return this.#callAction(action, params);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Test / internal: admit a parsed event when the endpoint is open. */
|
|
164
|
-
admit(ev: OneBot12Event): void {
|
|
165
|
-
if (!this.#open) return;
|
|
160
|
+
#admitRaw(ev: OneBot12Event): void {
|
|
166
161
|
if (!isMessageEvent(ev)) {
|
|
167
162
|
receiveOneBot12SideEvent(
|
|
168
|
-
this
|
|
163
|
+
(name, payload) => this.emit(name, payload),
|
|
169
164
|
this.#options.config.id,
|
|
170
|
-
this,
|
|
165
|
+
{ callApi: (action, params) => callOnebot12Client(this.client, action, params) },
|
|
171
166
|
ev,
|
|
172
167
|
this.#logger,
|
|
173
168
|
);
|
|
@@ -177,7 +172,7 @@ export class OneBot12WsEndpoint implements EndpointInstance {
|
|
|
177
172
|
const content = formatInboundContent(ev);
|
|
178
173
|
const nickname = senderNickname(ev);
|
|
179
174
|
const mentioned = isBotMentioned(ev);
|
|
180
|
-
void this
|
|
175
|
+
void this.emit('message.receive', {
|
|
181
176
|
conversation,
|
|
182
177
|
message: { conversation, id: ev.message_id },
|
|
183
178
|
content,
|
|
@@ -203,6 +198,13 @@ export class OneBot12WsEndpoint implements EndpointInstance {
|
|
|
203
198
|
});
|
|
204
199
|
}
|
|
205
200
|
|
|
201
|
+
#warnPlatformEvent(error: unknown): void {
|
|
202
|
+
this.#logger.warn(formatCompact({
|
|
203
|
+
op: 'onebot12_platform_event_failed',
|
|
204
|
+
error: error instanceof Error ? error.message : String(error),
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
|
|
206
208
|
async #connect(handle: EndpointConnectHandle): Promise<void> {
|
|
207
209
|
const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
|
|
208
210
|
const create = this.#options.createWebSocket
|
|
@@ -287,12 +289,11 @@ export class OneBot12WsEndpoint implements EndpointInstance {
|
|
|
287
289
|
if (pending) {
|
|
288
290
|
this.#pending.delete(resp.echo!);
|
|
289
291
|
clearTimeout(pending.timeout);
|
|
290
|
-
|
|
291
|
-
else pending.reject(new Error(`OneBot12 retcode=${resp.retcode}: ${resp.message}`));
|
|
292
|
+
pending.resolve(resp);
|
|
292
293
|
}
|
|
293
294
|
return;
|
|
294
295
|
}
|
|
295
|
-
this.
|
|
296
|
+
this.client.ingest(msg as Parameters<Onebot12Client['ingest']>[0]);
|
|
296
297
|
} catch (error) {
|
|
297
298
|
this.#logger.warn(formatCompact({
|
|
298
299
|
op: 'onebot12_parse_failed',
|
|
@@ -302,7 +303,7 @@ export class OneBot12WsEndpoint implements EndpointInstance {
|
|
|
302
303
|
}
|
|
303
304
|
}
|
|
304
305
|
|
|
305
|
-
#callAction(action: string, params: Record<string, unknown>): Promise<
|
|
306
|
+
#callAction(action: string, params: Record<string, unknown>): Promise<OneBot12ActionResponse> {
|
|
306
307
|
if (!this.#ws || this.#ws.readyState !== WS_OPEN) {
|
|
307
308
|
return Promise.reject(new Error('WebSocket 未连接'));
|
|
308
309
|
}
|
package/src/wss-endpoint.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OneBot12 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
|
|
3
3
|
*/
|
|
4
|
-
import { clearInterval } from 'node:timers';
|
|
5
4
|
import {
|
|
5
|
+
ClientEndpoint,
|
|
6
|
+
createEndpointLifecycle,
|
|
6
7
|
createRecallEndpointControl,
|
|
8
|
+
type EndpointLifecycle,
|
|
7
9
|
type EndpointControl,
|
|
8
|
-
type EndpointInstance,
|
|
9
10
|
type EndpointManagement,
|
|
10
11
|
type EndpointSendRequest,
|
|
11
12
|
} from 'zhin.js/adapter';
|
|
12
|
-
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
13
13
|
import type { HttpHost, WsConnection } from '@zhin.js/host-http';
|
|
14
14
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
15
15
|
import type { CapabilityId } from 'zhin.js';
|
|
@@ -31,44 +31,57 @@ import {
|
|
|
31
31
|
} from './protocol.js';
|
|
32
32
|
import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
|
|
33
33
|
import { createOneBot12ContentPort } from './content-port.js';
|
|
34
|
+
import { callOnebot12Client, createOnebot12EndpointClient, forwardOnebot12ClientEvents, type Onebot12Client } from './client.js';
|
|
34
35
|
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
35
36
|
import { type OneBot12WsSocket, WS_OPEN } from './ws-types.js';
|
|
36
37
|
|
|
37
38
|
export interface OneBot12WssEndpointOptions {
|
|
38
39
|
readonly id: CapabilityId;
|
|
39
|
-
readonly gateway: MessageGateway;
|
|
40
|
-
readonly sideEvents?: SideEventGateway;
|
|
41
40
|
readonly http: HttpHost;
|
|
42
41
|
readonly config: OneBot12WssConfig;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
export class OneBot12WssEndpoint
|
|
44
|
+
export class OneBot12WssEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
45
|
+
readonly client: Onebot12Client;
|
|
46
46
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
47
47
|
|
|
48
48
|
readonly #options: OneBot12WssEndpointOptions;
|
|
49
|
-
readonly management: EndpointManagement
|
|
49
|
+
readonly management: EndpointManagement;
|
|
50
50
|
readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
51
|
-
readonly content
|
|
51
|
+
readonly content;
|
|
52
52
|
#ws?: OneBot12WsSocket;
|
|
53
53
|
#wsRelease?: () => void;
|
|
54
|
-
#
|
|
54
|
+
readonly #lifecycle: EndpointLifecycle;
|
|
55
55
|
#requestId = 0;
|
|
56
56
|
#pending = new Map<string, {
|
|
57
|
-
resolve: (value:
|
|
57
|
+
resolve: (value: OneBot12ActionResponse) => void;
|
|
58
58
|
reject: (err: Error) => void;
|
|
59
59
|
timeout: NodeJS.Timeout;
|
|
60
60
|
}>();
|
|
61
|
-
#open = false;
|
|
62
|
-
#started = false;
|
|
63
61
|
|
|
64
62
|
constructor(options: OneBot12WssEndpointOptions) {
|
|
63
|
+
super();
|
|
65
64
|
this.#logger = getAdapterLogger('onebot12', options.config.id);
|
|
66
65
|
this.#options = options;
|
|
66
|
+
this.#lifecycle = createEndpointLifecycle({
|
|
67
|
+
name: options.config.id,
|
|
68
|
+
reconnect: false,
|
|
69
|
+
heartbeat: { intervalMs: options.config.heartbeat_interval },
|
|
70
|
+
});
|
|
71
|
+
this.client = createOnebot12EndpointClient(options.config, (action, params) => this.#callAction(action, params ?? {}));
|
|
72
|
+
const callApi = (action: string, params?: Record<string, unknown>) => callOnebot12Client(this.client, action, params);
|
|
73
|
+
this.management = createOneBot12EndpointManagement({ callApi });
|
|
74
|
+
this.content = createOneBot12ContentPort(callApi);
|
|
75
|
+
this.bindClientEvents(
|
|
76
|
+
(receive) => forwardOnebot12ClientEvents(this.client, receive),
|
|
77
|
+
(name, payload) => {
|
|
78
|
+
if (name === 'event') this.#admitRaw(payload as OneBot12Event);
|
|
79
|
+
},
|
|
80
|
+
(_name, error) => this.#warnPlatformEvent(error),
|
|
81
|
+
);
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
async start(): Promise<void> {
|
|
70
|
-
if (this.#started) return;
|
|
71
|
-
this.#started = true;
|
|
72
85
|
if (!this.#options.config.access_token) {
|
|
73
86
|
// wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
|
|
74
87
|
this.#logger.warn(formatCompact({
|
|
@@ -78,9 +91,21 @@ export class OneBot12WssEndpoint implements EndpointInstance {
|
|
|
78
91
|
error: 'missing access_token',
|
|
79
92
|
}));
|
|
80
93
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.#
|
|
94
|
+
await this.#lifecycle.start(async (lifecycleHandle) => {
|
|
95
|
+
const handle = this.#options.http.ws(this.#options.config.path);
|
|
96
|
+
this.#wsRelease = handle.onConnection((connection) => {
|
|
97
|
+
this.#acceptConnection(connection);
|
|
98
|
+
});
|
|
99
|
+
lifecycleHandle.onForceClose(() => {
|
|
100
|
+
this.#wsRelease?.();
|
|
101
|
+
this.#wsRelease = undefined;
|
|
102
|
+
try {
|
|
103
|
+
this.#ws?.close();
|
|
104
|
+
} catch {
|
|
105
|
+
/* ignore */
|
|
106
|
+
}
|
|
107
|
+
this.#ws = undefined;
|
|
108
|
+
});
|
|
84
109
|
});
|
|
85
110
|
this.#logger.info(formatCompact({
|
|
86
111
|
op: 'listen',
|
|
@@ -90,42 +115,20 @@ export class OneBot12WssEndpoint implements EndpointInstance {
|
|
|
90
115
|
}));
|
|
91
116
|
}
|
|
92
117
|
|
|
93
|
-
open(): void {
|
|
94
|
-
this.#open = true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
close(): void {
|
|
98
|
-
this.#open = false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
118
|
async stop(): Promise<void> {
|
|
102
|
-
this
|
|
103
|
-
this.#
|
|
104
|
-
this.#wsRelease = undefined;
|
|
105
|
-
if (this.#heartbeatTimer) {
|
|
106
|
-
clearInterval(this.#heartbeatTimer);
|
|
107
|
-
this.#heartbeatTimer = undefined;
|
|
108
|
-
}
|
|
119
|
+
this.close();
|
|
120
|
+
await this.#lifecycle.stop();
|
|
109
121
|
for (const [, pending] of this.#pending) {
|
|
110
122
|
clearTimeout(pending.timeout);
|
|
111
123
|
pending.reject(new Error('连接已关闭'));
|
|
112
124
|
}
|
|
113
125
|
this.#pending.clear();
|
|
114
|
-
if (this.#ws) {
|
|
115
|
-
try {
|
|
116
|
-
this.#ws.close();
|
|
117
|
-
} catch {
|
|
118
|
-
/* ignore */
|
|
119
|
-
}
|
|
120
|
-
this.#ws = undefined;
|
|
121
|
-
}
|
|
122
|
-
this.#started = false;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
126
129
|
const materialized = await uploadOneBot12MediaSegments(
|
|
127
130
|
payload,
|
|
128
|
-
(action, params) => this.
|
|
131
|
+
(action, params) => callOnebot12Client(this.client, action, params),
|
|
129
132
|
(error) => {
|
|
130
133
|
this.#logger.warn(formatCompact({
|
|
131
134
|
op: 'onebot12_upload_failed',
|
|
@@ -136,27 +139,21 @@ export class OneBot12WssEndpoint implements EndpointInstance {
|
|
|
136
139
|
);
|
|
137
140
|
const message = formatOutboundSegments(materialized);
|
|
138
141
|
const params = buildSendMessageParams(conversation, message);
|
|
139
|
-
const data = await
|
|
142
|
+
const data = await callOnebot12Client<{ message_id?: string }>(this.client, 'send_message', params);
|
|
140
143
|
return data?.message_id ?? '';
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
async recallMessage(messageId: string): Promise<void> {
|
|
144
147
|
if (!messageId) return;
|
|
145
|
-
await this
|
|
148
|
+
await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
|
|
149
|
-
callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
|
|
150
|
-
return this.#callAction(action, params);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
admit(ev: OneBot12Event): void {
|
|
154
|
-
if (!this.#open) return;
|
|
151
|
+
#admitRaw(ev: OneBot12Event): void {
|
|
155
152
|
if (!isMessageEvent(ev)) {
|
|
156
153
|
receiveOneBot12SideEvent(
|
|
157
|
-
this
|
|
154
|
+
(name, payload) => this.emit(name, payload),
|
|
158
155
|
this.#options.config.id,
|
|
159
|
-
this,
|
|
156
|
+
{ callApi: (action, params) => callOnebot12Client(this.client, action, params) },
|
|
160
157
|
ev,
|
|
161
158
|
this.#logger,
|
|
162
159
|
);
|
|
@@ -165,7 +162,7 @@ export class OneBot12WssEndpoint implements EndpointInstance {
|
|
|
165
162
|
const conversation = onebot12InboundConversation(String(this.#options.id), ev);
|
|
166
163
|
const nickname = senderNickname(ev);
|
|
167
164
|
const mentioned = isBotMentioned(ev);
|
|
168
|
-
void this
|
|
165
|
+
void this.emit('message.receive', {
|
|
169
166
|
conversation,
|
|
170
167
|
message: { conversation, id: ev.message_id },
|
|
171
168
|
content: formatInboundContent(ev),
|
|
@@ -191,6 +188,13 @@ export class OneBot12WssEndpoint implements EndpointInstance {
|
|
|
191
188
|
});
|
|
192
189
|
}
|
|
193
190
|
|
|
191
|
+
#warnPlatformEvent(error: unknown): void {
|
|
192
|
+
this.#logger.warn(formatCompact({
|
|
193
|
+
op: 'onebot12_platform_event_failed',
|
|
194
|
+
error: error instanceof Error ? error.message : String(error),
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
194
198
|
#acceptConnection(connection: WsConnection): void {
|
|
195
199
|
if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
|
|
196
200
|
connection.socket.close(4003, 'Unauthorized');
|
|
@@ -205,17 +209,17 @@ export class OneBot12WssEndpoint implements EndpointInstance {
|
|
|
205
209
|
}
|
|
206
210
|
}
|
|
207
211
|
this.#ws = socket;
|
|
208
|
-
this.#startHeartbeat()
|
|
212
|
+
this.#lifecycle.startHeartbeat(() => {
|
|
213
|
+
this.#callAction('get_status', {}).catch(() => {});
|
|
214
|
+
});
|
|
209
215
|
socket.on('message', (data) => {
|
|
216
|
+
this.#lifecycle.notifyHeartbeatAck();
|
|
210
217
|
this.#onMessage(data);
|
|
211
218
|
});
|
|
212
219
|
socket.on('close', () => {
|
|
213
220
|
if (this.#ws === socket) {
|
|
214
221
|
this.#ws = undefined;
|
|
215
|
-
|
|
216
|
-
clearInterval(this.#heartbeatTimer);
|
|
217
|
-
this.#heartbeatTimer = undefined;
|
|
218
|
-
}
|
|
222
|
+
this.#lifecycle.stopHeartbeat();
|
|
219
223
|
}
|
|
220
224
|
});
|
|
221
225
|
this.#logger.debug(formatCompact({
|
|
@@ -241,12 +245,11 @@ export class OneBot12WssEndpoint implements EndpointInstance {
|
|
|
241
245
|
if (pending) {
|
|
242
246
|
this.#pending.delete(resp.echo!);
|
|
243
247
|
clearTimeout(pending.timeout);
|
|
244
|
-
|
|
245
|
-
else pending.reject(new Error(`OneBot12 retcode=${resp.retcode}: ${resp.message}`));
|
|
248
|
+
pending.resolve(resp);
|
|
246
249
|
}
|
|
247
250
|
return;
|
|
248
251
|
}
|
|
249
|
-
this.
|
|
252
|
+
this.client.ingest(msg as Parameters<Onebot12Client['ingest']>[0]);
|
|
250
253
|
} catch (error) {
|
|
251
254
|
this.#logger.warn(formatCompact({
|
|
252
255
|
op: 'onebot12_parse_failed',
|
|
@@ -256,7 +259,7 @@ export class OneBot12WssEndpoint implements EndpointInstance {
|
|
|
256
259
|
}
|
|
257
260
|
}
|
|
258
261
|
|
|
259
|
-
#callAction(action: string, params: Record<string, unknown>): Promise<
|
|
262
|
+
#callAction(action: string, params: Record<string, unknown>): Promise<OneBot12ActionResponse> {
|
|
260
263
|
if (!this.#ws || this.#ws.readyState !== WS_OPEN) {
|
|
261
264
|
return Promise.reject(new Error('WebSocket 未连接'));
|
|
262
265
|
}
|
|
@@ -272,10 +275,4 @@ export class OneBot12WssEndpoint implements EndpointInstance {
|
|
|
272
275
|
});
|
|
273
276
|
}
|
|
274
277
|
|
|
275
|
-
#startHeartbeat(): void {
|
|
276
|
-
if (this.#heartbeatTimer) clearInterval(this.#heartbeatTimer);
|
|
277
|
-
this.#heartbeatTimer = setInterval(() => {
|
|
278
|
-
this.#callAction('get_status', {}).catch(() => {});
|
|
279
|
-
}, this.#options.config.heartbeat_interval);
|
|
280
|
-
}
|
|
281
278
|
}
|