@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/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 type { EndpointInstance, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
6
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
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 implements EndpointInstance {
44
+ export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
45
+ readonly client: Onebot12Client;
41
46
  readonly #options: OneBot12WebhookEndpointOptions;
42
- readonly management: EndpointManagement = createOneBot12EndpointManagement(this);
43
- readonly content = createOneBot12ContentPort((action, params) => this.callApi(action, params));
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.#open = false;
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.callApi(action, params),
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 this.callApi('send_message', params) as { message_id?: string } | undefined;
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
- /** Public API for management surface / callers;webhook 模式走 api_url。 */
117
- async callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
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
- const resp = await this.#callAction(
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
- admit(ev: OneBot12Event): void {
131
- if (!this.#open) return;
145
+ #admitRaw(ev: OneBot12Event): void {
132
146
  if (!isMessageEvent(ev)) {
133
147
  receiveOneBot12SideEvent(
134
- this.#options.sideEvents,
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.#options.gateway.receive({
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
- const raw = await readRequestBody(request);
189
- let ev: OneBot12Event;
190
- try {
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
- if (this.#open) this.admit(ev);
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
- }
@@ -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 EndpointInstance,
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 implements EndpointInstance {
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 = createOneBot12EndpointManagement(this);
58
- readonly content = createOneBot12ContentPort((action, params) => this.callApi(action, params));
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: unknown) => void;
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.#open = false;
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.callApi(action, params),
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 this.#callAction('send_message', params) as { message_id?: string } | undefined;
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.#callAction('delete_message', { message_id: messageId });
157
+ await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
153
158
  }
154
159
 
155
- /** Public API for management surface / callers. */
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.#options.sideEvents,
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.#options.gateway.receive({
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
- if (resp.status === 'ok') pending.resolve(resp.data);
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.admit(msg as OneBot12Event);
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<unknown> {
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
  }
@@ -1,9 +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
- import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
6
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
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 implements EndpointInstance {
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 = createOneBot12EndpointManagement(this);
44
- readonly content = createOneBot12ContentPort((action, params) => this.callApi(action, params));
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
- #heartbeatTimer?: NodeJS.Timeout;
54
+ readonly #lifecycle: EndpointLifecycle;
48
55
  #requestId = 0;
49
56
  #pending = new Map<string, {
50
- resolve: (value: unknown) => void;
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
- const handle = this.#options.http.ws(this.#options.config.path);
75
- this.#wsRelease = handle.onConnection((connection) => {
76
- 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
+ });
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.#open = false;
96
- this.#wsRelease?.();
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.callApi(action, params),
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 this.#callAction('send_message', params) as { message_id?: string } | undefined;
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.#callAction('delete_message', { message_id: messageId });
148
+ await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
139
149
  }
140
150
 
141
- /** Public API for management surface / callers. */
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.#options.sideEvents,
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.#options.gateway.receive({
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
- if (this.#heartbeatTimer) {
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
- if (resp.status === 'ok') pending.resolve(resp.data);
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.admit(msg as OneBot12Event);
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<unknown> {
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
  }