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