@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/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 implements EndpointInstance {
44
+ export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
45
+ readonly client: Onebot12Client;
47
46
  readonly #options: OneBot12WebhookEndpointOptions;
48
- readonly management: EndpointManagement = createOneBot12EndpointManagement(this);
47
+ readonly management: EndpointManagement;
49
48
  readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
50
- readonly content = createOneBot12ContentPort((action, params) => this.callApi(action, params));
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.#open = false;
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.callApi(action, params),
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 this.callApi('send_message', params) as { message_id?: string } | undefined;
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.callApi('delete_message', { message_id: messageId });
127
+ await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
126
128
  }
127
129
 
128
- /** Public API for management surface / callers;webhook 模式走 api_url。 */
129
- async callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
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
- const resp = await this.#callAction(
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
- admit(ev: OneBot12Event): void {
143
- if (!this.#open) return;
145
+ #admitRaw(ev: OneBot12Event): void {
144
146
  if (!isMessageEvent(ev)) {
145
147
  receiveOneBot12SideEvent(
146
- this.#options.sideEvents,
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.#options.gateway.receive({
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
- const raw = await readRequestBody(request);
201
- let ev: OneBot12Event;
202
- try {
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
- if (this.#open) this.admit(ev);
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
- }
@@ -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 implements EndpointInstance {
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 = createOneBot12EndpointManagement(this);
58
+ readonly management: EndpointManagement;
60
59
  readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
61
- readonly content = createOneBot12ContentPort((action, params) => this.callApi(action, params));
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: unknown) => void;
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.#open = false;
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.callApi(action, params),
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 this.#callAction('send_message', params) as { message_id?: string } | undefined;
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.#callAction('delete_message', { message_id: messageId });
157
+ await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
156
158
  }
157
159
 
158
- /** Public API for management surface / callers. */
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.#options.sideEvents,
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.#options.gateway.receive({
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
- if (resp.status === 'ok') pending.resolve(resp.data);
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.admit(msg as OneBot12Event);
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<unknown> {
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
  }
@@ -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 implements EndpointInstance {
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 = createOneBot12EndpointManagement(this);
49
+ readonly management: EndpointManagement;
50
50
  readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
51
- readonly content = createOneBot12ContentPort((action, params) => this.callApi(action, params));
51
+ readonly content;
52
52
  #ws?: OneBot12WsSocket;
53
53
  #wsRelease?: () => void;
54
- #heartbeatTimer?: NodeJS.Timeout;
54
+ readonly #lifecycle: EndpointLifecycle;
55
55
  #requestId = 0;
56
56
  #pending = new Map<string, {
57
- resolve: (value: unknown) => void;
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
- const handle = this.#options.http.ws(this.#options.config.path);
82
- this.#wsRelease = handle.onConnection((connection) => {
83
- this.#acceptConnection(connection);
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.#open = false;
103
- this.#wsRelease?.();
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.callApi(action, params),
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 this.#callAction('send_message', params) as { message_id?: string } | undefined;
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.#callAction('delete_message', { message_id: messageId });
148
+ await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
146
149
  }
147
150
 
148
- /** Public API for management surface / callers. */
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.#options.sideEvents,
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.#options.gateway.receive({
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
- if (this.#heartbeatTimer) {
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
- if (resp.status === 'ok') pending.resolve(resp.data);
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.admit(msg as OneBot12Event);
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<unknown> {
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
  }