@zhin.js/adapter-onebot11 6.0.14 → 7.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.
@@ -1,14 +1,19 @@
1
1
  /**
2
2
  * OneBot11 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';
10
16
  import { createOneBot11EndpointManagement } from './endpoint-management.js';
11
- import { registerOnebot11AgentEndpoint } from './onebot11-agent-deps.js';
12
17
  import {
13
18
  buildSendAction,
14
19
  formatInboundContent,
@@ -28,45 +33,57 @@ import {
28
33
  callOneBot11WsAction,
29
34
  handleOneBot11WsMessage,
30
35
  rejectAllPending,
31
- startOneBot11Heartbeat,
32
36
  } from './ws-transport.js';
33
37
  import { OneBot11WsEndpoint } from './ws-endpoint.js';
34
38
  import {
35
39
  type OneBot11PendingAction,
36
40
  type OneBot11WsSocket,
37
41
  } from './ws-types.js';
42
+ import { callOnebot11Client, createOnebot11EndpointClient, forwardOnebot11ClientEvents, type Onebot11Client } from './client.js';
38
43
 
39
44
  export interface OneBot11WssEndpointOptions {
40
45
  readonly id: CapabilityId;
41
- readonly gateway: MessageGateway;
42
- readonly sideEvents?: SideEventGateway;
43
46
  readonly http: HttpHost;
44
47
  readonly config: OneBot11WssConfig;
45
48
  }
46
49
 
47
- export class OneBot11WssEndpoint implements EndpointInstance {
50
+ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
51
+ readonly client: Onebot11Client;
48
52
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
49
53
 
50
54
  readonly #options: OneBot11WssEndpointOptions;
51
- readonly management: EndpointManagement = createOneBot11EndpointManagement(this);
52
- readonly content = createOneBot11ContentPort((action, params) => this.callApi(action, params));
55
+ readonly management: EndpointManagement;
56
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
57
+ readonly content;
53
58
  #ws?: OneBot11WsSocket;
54
59
  #wsRelease?: () => void;
55
- #heartbeatTimer?: NodeJS.Timeout;
60
+ readonly #lifecycle: EndpointLifecycle;
56
61
  #requestId = { value: 0 };
57
62
  #pending = new Map<string, OneBot11PendingAction>();
58
- #open = false;
59
- #started = false;
60
- #unregisterAgent?: () => void;
61
63
 
62
64
  constructor(options: OneBot11WssEndpointOptions) {
65
+ super();
63
66
  this.#logger = getAdapterLogger('onebot11', options.config.id);
64
67
  this.#options = options;
68
+ this.#lifecycle = createEndpointLifecycle({
69
+ name: options.config.id,
70
+ reconnect: false,
71
+ heartbeat: { intervalMs: options.config.heartbeat_interval },
72
+ });
73
+ this.client = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
74
+ const callApi = (action: string, params?: Record<string, unknown>) => callOnebot11Client(this.client, action, params);
75
+ this.management = createOneBot11EndpointManagement({ callApi });
76
+ this.content = createOneBot11ContentPort(callApi);
77
+ this.bindClientEvents(
78
+ (receive) => forwardOnebot11ClientEvents(this.client, receive),
79
+ (name, payload) => {
80
+ if (name === 'event') this.#admitRaw(payload as OneBot11Event);
81
+ },
82
+ (_name, error) => this.#warnPlatformEvent(error),
83
+ );
65
84
  }
66
85
 
67
86
  async start(): Promise<void> {
68
- if (this.#started) return;
69
- this.#started = true;
70
87
  if (!this.#options.config.access_token) {
71
88
  // wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
72
89
  this.#logger.warn(formatCompact({
@@ -76,13 +93,21 @@ export class OneBot11WssEndpoint implements EndpointInstance {
76
93
  error: 'missing access_token',
77
94
  }));
78
95
  }
79
- this.#unregisterAgent = registerOnebot11AgentEndpoint(
80
- this.#options.config.id,
81
- this as unknown as OneBot11WsEndpoint,
82
- );
83
- const handle = this.#options.http.ws(this.#options.config.path);
84
- this.#wsRelease = handle.onConnection((connection) => {
85
- this.#acceptConnection(connection);
96
+ await this.#lifecycle.start(async (lifecycleHandle) => {
97
+ const handle = this.#options.http.ws(this.#options.config.path);
98
+ this.#wsRelease = handle.onConnection((connection) => {
99
+ this.#acceptConnection(connection);
100
+ });
101
+ lifecycleHandle.onForceClose(() => {
102
+ this.#wsRelease?.();
103
+ this.#wsRelease = undefined;
104
+ try {
105
+ this.#ws?.close();
106
+ } catch {
107
+ /* ignore */
108
+ }
109
+ this.#ws = undefined;
110
+ });
86
111
  });
87
112
  this.#logger.info(formatCompact({
88
113
  op: 'listen',
@@ -92,69 +117,37 @@ export class OneBot11WssEndpoint implements EndpointInstance {
92
117
  }));
93
118
  }
94
119
 
95
- open(): void {
96
- this.#open = true;
97
- }
98
-
99
- close(): void {
100
- this.#open = false;
101
- }
102
-
103
120
  async stop(): Promise<void> {
104
- this.#open = false;
105
- this.#unregisterAgent?.();
106
- this.#unregisterAgent = undefined;
107
- this.#wsRelease?.();
108
- this.#wsRelease = undefined;
109
- if (this.#heartbeatTimer) {
110
- clearInterval(this.#heartbeatTimer);
111
- this.#heartbeatTimer = undefined;
112
- }
121
+ this.close();
122
+ await this.#lifecycle.stop();
113
123
  rejectAllPending(this.#pending);
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
124
  }
124
125
 
125
126
  async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
126
127
  const message = formatOutboundSegments(payload);
127
128
  const { action, params } = buildSendAction(conversation, message);
128
- const data = await this.callApi(action, params) as { message_id?: number | string } | undefined;
129
+ const data = await callOnebot11Client<{ message_id?: number | string }>(this.client, action, params);
129
130
  return data?.message_id != null ? String(data.message_id) : '';
130
131
  }
131
132
 
132
133
  async recallMessage(messageId: string): Promise<void> {
133
134
  if (!messageId) return;
134
- await this.callApi('delete_msg', { message_id: Number(messageId) });
135
+ await callOnebot11Client(this.client, 'delete_msg', { message_id: Number(messageId) });
135
136
  }
136
137
 
137
- callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
138
+ #callApi(
139
+ action: string,
140
+ params: Record<string, unknown> = {},
141
+ ): ReturnType<typeof callOneBot11WsAction> {
138
142
  return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
139
143
  }
140
144
 
141
- async setTitle(groupId: number, userId: number, title: string, duration = -1): Promise<boolean> {
142
- await this.callApi('set_group_special_title', {
143
- group_id: groupId,
144
- user_id: userId,
145
- special_title: title,
146
- duration,
147
- });
148
- return true;
149
- }
150
-
151
- admit(ev: OneBot11Event): void {
152
- if (!this.#open) return;
145
+ #admitRaw(ev: OneBot11Event): void {
153
146
  if (!isMessageEvent(ev)) {
154
147
  receiveOneBot11SideEvent(
155
- this.#options.sideEvents,
148
+ (name, payload) => this.emit(name, payload),
156
149
  this.#options.config.id,
157
- this,
150
+ { callApi: (action, params) => callOnebot11Client(this.client, action, params) },
158
151
  ev,
159
152
  this.#logger,
160
153
  );
@@ -162,7 +155,7 @@ export class OneBot11WssEndpoint implements EndpointInstance {
162
155
  }
163
156
  const conversation = onebot11InboundConversation(String(this.#options.id), ev);
164
157
  const inbound = formatInboundMetadata(ev, this.#options.config.id);
165
- void this.#options.gateway.receive({
158
+ void this.emit('message.receive', {
166
159
  conversation,
167
160
  message: { conversation, id: String(ev.message_id) },
168
161
  content: formatInboundContent(ev),
@@ -184,6 +177,13 @@ export class OneBot11WssEndpoint implements EndpointInstance {
184
177
  });
185
178
  }
186
179
 
180
+ #warnPlatformEvent(error: unknown): void {
181
+ this.#logger.warn(formatCompact({
182
+ op: 'onebot11_platform_event_failed',
183
+ error: error instanceof Error ? error.message : String(error),
184
+ }));
185
+ }
186
+
187
187
  #acceptConnection(connection: WsConnection): void {
188
188
  if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
189
189
  connection.socket.close(4003, 'Unauthorized');
@@ -198,25 +198,25 @@ export class OneBot11WssEndpoint implements EndpointInstance {
198
198
  }
199
199
  }
200
200
  this.#ws = socket;
201
- this.#heartbeatTimer = startOneBot11Heartbeat(
202
- this.#ws,
203
- this.#options.config.heartbeat_interval,
204
- this.#heartbeatTimer,
205
- );
201
+ this.#lifecycle.startHeartbeat(() => {
202
+ try {
203
+ this.#ws?.ping?.();
204
+ } catch {
205
+ /* ignore */
206
+ }
207
+ });
206
208
  socket.on('message', (data) => {
209
+ this.#lifecycle.notifyHeartbeatAck();
207
210
  handleOneBot11WsMessage(data, {
208
211
  endpointId: this.#options.config.id,
209
212
  pending: this.#pending,
210
- admit: (ev) => this.admit(ev),
213
+ ingest: (ev) => this.client.ingest(ev as Parameters<Onebot11Client['ingest']>[0]),
211
214
  });
212
215
  });
213
216
  socket.on('close', () => {
214
217
  if (this.#ws === socket) {
215
218
  this.#ws = undefined;
216
- if (this.#heartbeatTimer) {
217
- clearInterval(this.#heartbeatTimer);
218
- this.#heartbeatTimer = undefined;
219
- }
219
+ this.#lifecycle.stopHeartbeat();
220
220
  }
221
221
  });
222
222
  this.#logger.debug(formatCompact({
@@ -1,15 +0,0 @@
1
- /**
2
- * Agent tool deps for onebot11 (set_title etc.).
3
- * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
- */
5
- export interface Onebot11AgentEndpoint {
6
- setTitle(groupId: number, userId: number, title: string, duration?: number): Promise<boolean>;
7
- callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
8
- }
9
- export interface Onebot11AgentDeps {
10
- getEndpoint: (endpointKey: string) => Onebot11AgentEndpoint;
11
- }
12
- export declare function registerOnebot11AgentEndpoint(endpointKey: string, endpoint: Onebot11AgentEndpoint): () => void;
13
- /** Optional override used by tests / transitional callers. */
14
- export declare function setOnebot11AgentDeps(deps: Onebot11AgentDeps): void;
15
- export declare function getOnebot11AgentDeps(): Onebot11AgentDeps;
@@ -1,30 +0,0 @@
1
- /**
2
- * Agent tool deps for onebot11 (set_title etc.).
3
- * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
- */
5
- const endpoints = new Map();
6
- let override = null;
7
- export function registerOnebot11AgentEndpoint(endpointKey, endpoint) {
8
- endpoints.set(endpointKey, endpoint);
9
- return () => {
10
- if (endpoints.get(endpointKey) === endpoint) {
11
- endpoints.delete(endpointKey);
12
- }
13
- };
14
- }
15
- /** Optional override used by tests / transitional callers. */
16
- export function setOnebot11AgentDeps(deps) {
17
- override = deps;
18
- }
19
- export function getOnebot11AgentDeps() {
20
- if (override)
21
- return override;
22
- return {
23
- getEndpoint(endpointKey) {
24
- const registered = endpoints.get(endpointKey);
25
- if (!registered)
26
- throw new Error(`Endpoint ${endpointKey} 不存在`);
27
- return registered;
28
- },
29
- };
30
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * Agent tool deps for onebot11 (set_title etc.).
3
- * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
- */
5
-
6
- export interface Onebot11AgentEndpoint {
7
- setTitle(
8
- groupId: number,
9
- userId: number,
10
- title: string,
11
- duration?: number,
12
- ): Promise<boolean>;
13
- callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
14
- }
15
-
16
- export interface Onebot11AgentDeps {
17
- getEndpoint: (endpointKey: string) => Onebot11AgentEndpoint;
18
- }
19
-
20
- const endpoints = new Map<string, Onebot11AgentEndpoint>();
21
- let override: Onebot11AgentDeps | null = null;
22
-
23
- export function registerOnebot11AgentEndpoint(
24
- endpointKey: string,
25
- endpoint: Onebot11AgentEndpoint,
26
- ): () => void {
27
- endpoints.set(endpointKey, endpoint);
28
- return () => {
29
- if (endpoints.get(endpointKey) === endpoint) {
30
- endpoints.delete(endpointKey);
31
- }
32
- };
33
- }
34
-
35
- /** Optional override used by tests / transitional callers. */
36
- export function setOnebot11AgentDeps(deps: Onebot11AgentDeps): void {
37
- override = deps;
38
- }
39
-
40
- export function getOnebot11AgentDeps(): Onebot11AgentDeps {
41
- if (override) return override;
42
- return {
43
- getEndpoint(endpointKey: string): Onebot11AgentEndpoint {
44
- const registered = endpoints.get(endpointKey);
45
- if (!registered) throw new Error(`Endpoint ${endpointKey} 不存在`);
46
- return registered;
47
- },
48
- };
49
- }