@zhin.js/adapter-onebot11 1.1.18 → 1.1.20

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +13 -6
  3. package/adapters/onebot11/index.js +3 -26
  4. package/adapters/onebot11/index.ts +2 -32
  5. package/commands/onebot11/endpoint/definition.js +0 -2
  6. package/commands/onebot11/endpoint/definition.ts +0 -2
  7. package/lib/adapter.d.ts +20 -0
  8. package/lib/adapter.js +26 -0
  9. package/lib/client.d.ts +13 -11
  10. package/lib/client.js +35 -12
  11. package/lib/console.d.ts +1 -1
  12. package/lib/console.js +2 -5
  13. package/lib/content-port.d.ts +2 -2
  14. package/lib/content-port.js +2 -2
  15. package/lib/endpoint-management.d.ts +2 -2
  16. package/lib/endpoint-management.js +8 -8
  17. package/lib/identity.d.ts +3 -0
  18. package/lib/identity.js +32 -0
  19. package/lib/index.d.ts +3 -2
  20. package/lib/index.js +3 -2
  21. package/lib/self-id.js +1 -1
  22. package/lib/side-event-dispatch.d.ts +8 -1
  23. package/lib/side-event-dispatch.js +2 -1
  24. package/lib/ws-endpoint.d.ts +5 -12
  25. package/lib/ws-endpoint.js +75 -50
  26. package/lib/ws-transport.d.ts +18 -11
  27. package/lib/ws-transport.js +28 -56
  28. package/lib/ws-types.d.ts +0 -6
  29. package/lib/wss-endpoint.d.ts +5 -12
  30. package/lib/wss-endpoint.js +76 -53
  31. package/package.json +18 -18
  32. package/plugin.js +0 -3
  33. package/src/adapter.ts +32 -0
  34. package/src/client.ts +38 -28
  35. package/src/console.ts +2 -5
  36. package/src/content-port.ts +4 -4
  37. package/src/endpoint-management.ts +15 -12
  38. package/src/identity.ts +30 -0
  39. package/src/index.ts +4 -4
  40. package/src/self-id.ts +1 -1
  41. package/src/side-event-dispatch.ts +9 -7
  42. package/src/ws-endpoint.ts +71 -64
  43. package/src/ws-transport.ts +41 -82
  44. package/src/ws-types.ts +0 -7
  45. package/src/wss-endpoint.ts +73 -68
  46. package/lib/onebot11-runtime-state.d.ts +0 -1
  47. package/lib/onebot11-runtime-state.js +0 -6
  48. package/src/onebot11-runtime-state.ts +0 -7
@@ -4,7 +4,8 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
4
4
  */
5
5
  import WebSocket from 'ws';
6
6
  import {
7
- ClientEndpoint,
7
+ createEndpointSpecification, endpointSpec,
8
+ type Endpoint, type EndpointSpecification,
8
9
  createRecallEndpointControl,
9
10
  createEndpointLifecycle,
10
11
  type EndpointConnectHandle,
@@ -12,6 +13,7 @@ import {
12
13
  type EndpointLifecycle,
13
14
  type EndpointManagement,
14
15
  type EndpointSendRequest,
16
+ type PendingRpcRequest,
15
17
  type EndpointTransportState,
16
18
  } from 'zhin.js/adapter';
17
19
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
@@ -32,20 +34,16 @@ import {
32
34
  } from './protocol.js';
33
35
  import { receiveOneBot11SideEvent } from './side-event-dispatch.js';
34
36
  import { createOneBot11ContentPort } from './content-port.js';
37
+ import { createOneBot11Rpc, type OneBot11Rpc } from './ws-transport.js';
35
38
  import {
36
- callOneBot11WsAction,
37
- handleOneBot11WsMessage,
38
- rejectAllPending,
39
- } from './ws-transport.js';
40
- import {
41
- type OneBot11PendingAction,
42
39
  type OneBot11WsCreateOptions,
43
40
  type OneBot11WsSocket,
44
41
  } from './ws-types.js';
45
42
  import { probeOnebot11SelfId, resolveOnebot11SelfId } from './self-id.js';
46
- import { callOnebot11Client, createOnebot11EndpointClient, forwardOnebot11ClientEvents, type Onebot11Client } from './client.js';
43
+ import { callOnebot11Client, readOnebot11Response, onebot11ClientOptions, forwardOnebot11ClientEvents, Onebot11Client } from './client.js';
47
44
 
48
45
  export interface OneBot11WsEndpointOptions {
46
+ readonly signal?: AbortSignal;
49
47
  readonly id: CapabilityId;
50
48
  /** 连接后 best-effort 调用 get_login_info 上报账号(Adapter 工厂开启;默认关闭以免占用首个 echo)。 */
51
49
  readonly probeLoginInfo?: boolean;
@@ -56,35 +54,50 @@ export interface OneBot11WsEndpointOptions {
56
54
  ) => OneBot11WsSocket;
57
55
  }
58
56
 
59
- export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
60
- readonly client: Onebot11Client;
57
+ export class OneBot11WsEndpoint extends Onebot11Client implements Endpoint {
58
+ readonly [endpointSpec]: EndpointSpecification;
59
+ #open = false;
61
60
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
62
61
 
63
62
  readonly #options: OneBot11WsEndpointOptions;
64
- readonly management: EndpointManagement;
65
- readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
66
- readonly content;
63
+ readonly #management: EndpointManagement;
64
+ readonly #control: EndpointControl = createRecallEndpointControl((id, signal?: AbortSignal) => this.#recallMessage(id, signal));
65
+ readonly #content;
66
+ readonly #releaseClientEvents: () => void;
67
67
  readonly #lifecycle: EndpointLifecycle;
68
68
  #ws?: OneBot11WsSocket;
69
69
  #stopped = false;
70
- #requestId = { value: 0 };
71
- #pending = new Map<string, OneBot11PendingAction>();
70
+ readonly #rpc: OneBot11Rpc;
72
71
 
73
72
  constructor(options: OneBot11WsEndpointOptions) {
74
- super();
73
+ const rpc = createOneBot11Rpc({ endpointId: options.config.id, signal: options.signal,
74
+ socket: () => this.#lifecycle.state === 'open' ? this.#ws : undefined,
75
+ });
76
+ super(onebot11ClientOptions(options.config, (action, params) => rpc.call({ action, params })));
77
+ this.#rpc = rpc;
75
78
  this.#logger = getAdapterLogger('onebot11', options.config.id);
76
79
  this.#options = options;
77
- this.client = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
78
- const callApi = (action: string, params?: Record<string, unknown>) => callOnebot11Client(this.client, action, params);
79
- this.management = createOneBot11EndpointManagement({ callApi });
80
- this.content = createOneBot11ContentPort(callApi);
81
- this.bindClientEvents(
82
- (receive) => forwardOnebot11ClientEvents(this.client, receive),
83
- (name, payload) => {
80
+ const callApi = (request: PendingRpcRequest) => this.#rpc.call(request).then(readOnebot11Response);
81
+ this.#management = createOneBot11EndpointManagement({ callApi });
82
+ this.#content = createOneBot11ContentPort(callApi);
83
+ const readTransportState = () => this.#lifecycle.state;
84
+ this[endpointSpec] = createEndpointSpecification(this, {
85
+ name: options.config.id,
86
+ start: () => this.#start(), open: () => { this.#open = true; },
87
+ close: () => { this.#open = false; }, stop: () => this.#stop(),
88
+ send: request => this.#send(request),
89
+ get transportState() { return readTransportState(); },
90
+ management: this.#management, control: this.#control, content: this.#content,
91
+ });
92
+ this.#releaseClientEvents = forwardOnebot11ClientEvents(this, (name, payload) => {
93
+ void this[endpointSpec].events.run(() => {
94
+ // A native frame can arrive before start resolves. The spec owns candidate
95
+ // buffering and Root/child admission; transport start is not publication.
96
+ if (this.#stopped || (!this.#open && !this.#lifecycle.started)) return;
97
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
84
98
  if (name === 'event') this.#admitRaw(payload as OneBot11Event);
85
- },
86
- (_name, error) => this.#warnPlatformEvent(error),
87
- );
99
+ }).catch(error => this.#warnPlatformEvent(error));
100
+ });
88
101
  const { config } = options;
89
102
  this.#lifecycle = createEndpointLifecycle({
90
103
  name: config.id,
@@ -99,11 +112,11 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
99
112
  });
100
113
  }
101
114
 
102
- get transportState(): EndpointTransportState {
115
+ get #transportState(): EndpointTransportState {
103
116
  return this.#lifecycle.state;
104
117
  }
105
118
 
106
- async start(): Promise<void> {
119
+ async #start(): Promise<void> {
107
120
  if (this.#lifecycle.started) return;
108
121
  this.#stopped = false;
109
122
  try {
@@ -123,20 +136,23 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
123
136
  }
124
137
  }
125
138
 
126
- async stop(): Promise<void> {
139
+ async #stop(): Promise<void> {
127
140
  this.#stopped = true;
128
- this.close();
141
+ this.#open = false;
129
142
  // 基座负责:清重连/心跳定时器、强关 ws、唤醒 stop-during-connect 竞态
130
- await this.#lifecycle.stop();
131
- rejectAllPending(this.#pending);
132
- this.#ws = undefined;
133
- this.reportSelfUserId(undefined);
143
+ try { await this.#lifecycle.stop(); }
144
+ finally {
145
+ this.#rpc.dispose();
146
+ this.#releaseClientEvents();
147
+ this.#ws = undefined;
148
+ this[endpointSpec].reportSelfUserId(undefined);
149
+ }
134
150
  }
135
151
 
136
- async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
152
+ async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
137
153
  const message = formatOutboundSegments(payload);
138
154
  const { action, params } = buildSendAction(conversation, message);
139
- const data = await callOnebot11Client<{ message_id?: number | string }>(this.client, action, params);
155
+ const data = readOnebot11Response<{ message_id?: number | string }>(await this.#rpc.call({ action, params, signal }));
140
156
  const rawId = data?.message_id;
141
157
  if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
142
158
  throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
@@ -151,35 +167,30 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
151
167
  return messageId;
152
168
  }
153
169
 
154
- async recallMessage(messageId: string): Promise<void> {
170
+ async #recallMessage(messageId: string, signal?: AbortSignal): Promise<void> {
155
171
  if (!messageId) return;
156
- await callOnebot11Client(this.client, 'delete_msg', { message_id: Number(messageId) });
172
+ await this.#rpc.call({ action: 'delete_msg', params: { message_id: Number(messageId) }, signal }).then(readOnebot11Response);
157
173
  }
158
174
 
159
- #callApi(
160
- action: string,
161
- params: Record<string, unknown> = {},
162
- ): ReturnType<typeof callOneBot11WsAction> {
163
- return callOneBot11WsAction(this.#lifecycle.state === 'open' ? this.#ws : undefined, this.#pending, this.#requestId, action, params);
164
- }
165
175
 
166
176
  #admitRaw(ev: OneBot11Event): void {
167
177
  const selfId = resolveOnebot11SelfId(ev.self_id);
168
- if (!this.#stopped && selfId && selfId !== this.selfUserId) this.reportSelfUserId(selfId);
178
+ if (!this.#stopped && selfId) this[endpointSpec].reportSelfUserId(selfId);
169
179
  if (!isMessageEvent(ev)) {
170
- receiveOneBot11SideEvent(
171
- (name, payload) => this.emit(name, payload),
172
- this.#options.config.id,
173
- { callApi: (action, params) => callOnebot11Client(this.client, action, params) },
174
- ev,
175
- this.#logger,
176
- );
180
+ receiveOneBot11SideEvent(ev, {
181
+ emit: (name, payload) => this[endpointSpec].events.emit(name, payload),
182
+ endpointKey: this.#options.config.id,
183
+ caller: { callApi: (action, params) => callOnebot11Client({ client: this, action, params }) },
184
+ logger: this.#logger,
185
+ });
177
186
  return;
178
187
  }
179
- const conversation = onebot11InboundConversation(String(this.#options.id), ev);
188
+ const conversation = { ...onebot11InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
180
189
  const content = formatInboundContent(ev);
181
190
  const inbound = formatInboundMetadata(ev, this.#options.config.id);
182
- void this.emit('message.receive', {
191
+ void this[endpointSpec].events.message({
192
+ event: ev,
193
+ ...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
183
194
  conversation,
184
195
  message: { conversation, id: String(ev.message_id) },
185
196
  content,
@@ -203,14 +214,14 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
203
214
 
204
215
  /** Best-effort 登录账号探测;失败仅 debug,不影响连接。 */
205
216
  async #probeSelfUserId(): Promise<void> {
206
- const selfId = await probeOnebot11SelfId(this.client, (error) => {
217
+ const selfId = await probeOnebot11SelfId(this, (error) => {
207
218
  this.#logger.debug(formatCompact({
208
219
  op: 'onebot11_get_login_info_failed',
209
220
  error: error instanceof Error ? error.message : String(error),
210
221
  }));
211
222
  });
212
- if (this.#stopped || this.transportState !== 'open') return;
213
- if (selfId && selfId !== this.selfUserId) this.reportSelfUserId(selfId);
223
+ if (this.#stopped || this.#transportState !== 'open') return;
224
+ if (selfId) this[endpointSpec].reportSelfUserId(selfId);
214
225
  }
215
226
 
216
227
  #warnPlatformEvent(error: unknown): void {
@@ -232,7 +243,7 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
232
243
  this.#ws = ws;
233
244
  handle.onForceClose(() => {
234
245
  if (this.#ws === ws) {
235
- rejectAllPending(this.#pending);
246
+ this.#rpc.disconnect();
236
247
  }
237
248
  try {
238
249
  ws.close();
@@ -266,17 +277,13 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
266
277
  ws.on('message', (data) => {
267
278
  if (this.#ws !== ws || ws.readyState !== 1 || !this.#lifecycle.started) return;
268
279
  this.#lifecycle.notifyHeartbeatAck();
269
- handleOneBot11WsMessage(data, {
270
- endpointId: this.#options.config.id,
271
- pending: this.#pending,
272
- ingest: (ev) => this.client.ingest(ev as Parameters<Onebot11Client['ingest']>[0]),
273
- });
280
+ this.#rpc.receive(data, (ev) => this.ingest(ev as Parameters<Onebot11Client['ingest']>[0]));
274
281
  });
275
282
 
276
283
  ws.on('close', (code, reason) => {
277
284
  if (this.#ws !== ws) return;
278
285
  this.#ws = undefined;
279
- rejectAllPending(this.#pending);
286
+ this.#rpc.disconnect();
280
287
  const reasonStr = typeof reason === 'string'
281
288
  ? reason
282
289
  : Buffer.isBuffer(reason)
@@ -1,92 +1,51 @@
1
- import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
- import { clearTimeout, setTimeout } from 'node:timers';
1
+ import { createPendingRpc, type PendingRpc, type PendingRpcRequest } from 'zhin.js/adapter';
3
2
  import { formatCompact, getLogger } from '@zhin.js/logger';
4
- import {
5
- type OneBot11ActionRequest,
6
- type OneBot11ActionResponse,
7
- type OneBot11Event,
8
- } from './protocol.js';
9
- import {
10
- WS_OPEN,
11
- type OneBot11PendingAction,
12
- type OneBot11WsSocket,
13
- } from './ws-types.js';
3
+ import type { OneBot11ActionResponse, OneBot11Event } from './protocol.js';
4
+ import { WS_OPEN, type OneBot11WsSocket } from './ws-types.js';
14
5
 
15
6
  const logger = getLogger('onebot11');
16
-
17
- export function decodeWsPayload(data: unknown): string {
18
- if (typeof data === 'string') return data;
19
- if (Buffer.isBuffer(data)) return data.toString();
20
- if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
21
- return String(data ?? '');
7
+ export interface OneBot11RpcOptions {
8
+ readonly endpointId: string;
9
+ readonly signal?: AbortSignal;
10
+ /** Reads the lifecycle's selected socket; RPC never opens or reconnects it. */
11
+ readonly socket: () => OneBot11WsSocket | undefined;
22
12
  }
23
-
24
- export function handleOneBot11WsMessage(
25
- data: unknown,
26
- options: {
27
- readonly endpointId: string;
28
- readonly pending: Map<string, OneBot11PendingAction>;
29
- readonly ingest: (ev: OneBot11Event) => void;
30
- },
31
- ): void {
32
- try {
33
- const raw = decodeWsPayload(data);
34
- const msg = JSON.parse(raw) as OneBot11Event | OneBot11ActionResponse;
35
- if ('echo' in msg && typeof (msg as OneBot11ActionResponse).echo === 'string') {
36
- const resp = msg as OneBot11ActionResponse;
37
- const pending = options.pending.get(resp.echo!);
38
- if (pending) {
39
- options.pending.delete(resp.echo!);
40
- clearTimeout(pending.timeout);
41
- pending.resolve(resp);
42
- }
43
- return;
44
- }
45
- options.ingest(msg as OneBot11Event);
46
- } catch (error) {
47
- logger.warn(formatCompact({
48
- op: 'onebot11_parse_failed',
49
- endpoint: options.endpointId,
50
- error: error instanceof Error ? error.message : String(error),
51
- }));
52
- }
13
+ export interface OneBot11Rpc {
14
+ call(request: PendingRpcRequest): Promise<OneBot11ActionResponse>;
15
+ receive(data: unknown, ingest: (event: OneBot11Event) => void): void;
16
+ disconnect: PendingRpc<OneBot11ActionResponse>['disconnect'];
17
+ dispose(): void;
18
+ readonly pendingCount: number;
53
19
  }
54
20
 
55
- export function callOneBot11WsAction(
56
- ws: OneBot11WsSocket | undefined,
57
- pending: Map<string, OneBot11PendingAction>,
58
- requestId: { value: number },
59
- action: string,
60
- params: Record<string, unknown>,
61
- ): Promise<OneBot11ActionResponse> {
62
- if (!ws || ws.readyState !== WS_OPEN) {
63
- return Promise.reject(new EndpointDeliveryError('endpoint_disconnected', 'WebSocket 未连接', 'not_sent'));
64
- }
65
- const echo = `ob11_${++requestId.value}`;
66
- const req: OneBot11ActionRequest = { action, params, echo };
67
- return new Promise((resolve, reject) => {
68
- const timeout = setTimeout(() => {
69
- pending.delete(echo);
70
- reject(new EndpointDeliveryError('endpoint_timeout', `OneBot11 动作超时: ${action}`, 'unknown'));
71
- }, 30_000);
72
- pending.set(echo, { resolve, reject, timeout });
73
- try {
74
- ws.send(JSON.stringify(req));
75
- } catch (error) {
76
- pending.delete(echo);
77
- clearTimeout(timeout);
78
- reject(error instanceof Error ? error : new Error(String(error)));
79
- }
21
+ /** A Client's one request owner serves native SDK actions and framework operations. */
22
+ export function createOneBot11Rpc(options: OneBot11RpcOptions): OneBot11Rpc {
23
+ const pending = createPendingRpc<OneBot11ActionResponse>({
24
+ signal: options.signal, requestPrefix: 'ob11_', timeoutMs: 30_000,
25
+ transport: () => { const socket = options.socket(); return socket?.readyState === WS_OPEN ? socket : undefined; },
26
+ serialize: request => JSON.stringify({ action: request.action, params: request.params ?? {}, echo: request.id }),
27
+ });
28
+ return Object.freeze({
29
+ call: pending.call, disconnect: pending.disconnect, dispose: pending.dispose,
30
+ get pendingCount() { return pending.pendingCount; },
31
+ receive(data: unknown, ingest: (event: OneBot11Event) => void) {
32
+ try {
33
+ const message = JSON.parse(decodeWsPayload(data)) as OneBot11Event | OneBot11ActionResponse;
34
+ if ('echo' in message && typeof message.echo === 'string') {
35
+ pending.accept({ id: message.echo, value: message as OneBot11ActionResponse }); return;
36
+ }
37
+ ingest(message as OneBot11Event);
38
+ } catch (error) {
39
+ logger.warn(formatCompact({ op: 'onebot11_parse_failed', endpoint: options.endpointId,
40
+ error: error instanceof Error ? error.message : String(error) }));
41
+ }
42
+ },
80
43
  });
81
44
  }
82
45
 
83
- export function rejectAllPending(
84
- pending: Map<string, OneBot11PendingAction>,
85
- message = '连接已关闭',
86
- ): void {
87
- for (const [, entry] of pending) {
88
- clearTimeout(entry.timeout);
89
- entry.reject(new EndpointDeliveryError('endpoint_disconnected', message, 'unknown'));
90
- }
91
- pending.clear();
46
+ export function decodeWsPayload(data: unknown): string {
47
+ if (typeof data === 'string') return data;
48
+ if (Buffer.isBuffer(data)) return data.toString();
49
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
50
+ return String(data ?? '');
92
51
  }
package/src/ws-types.ts CHANGED
@@ -12,10 +12,3 @@ export interface OneBot11WsCreateOptions {
12
12
  }
13
13
 
14
14
  export const WS_OPEN = 1;
15
-
16
- export interface OneBot11PendingAction {
17
- resolve: (value: OneBot11ActionResponse) => void;
18
- reject: (err: Error) => void;
19
- timeout: NodeJS.Timeout;
20
- }
21
- import type { OneBot11ActionResponse } from './protocol.js';