@zhin.js/adapter-napcat 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 (57) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +8 -5
  3. package/adapters/napcat/index.js +2 -29
  4. package/adapters/napcat/index.ts +3 -35
  5. package/agents/napcat/skills/napcat-account/tools/set_profile/index.js +1 -1
  6. package/agents/napcat/skills/napcat-account/tools/set_profile/index.ts +1 -1
  7. package/agents/napcat/skills/napcat-group-files-history/tools/upload_group_file/index.js +1 -1
  8. package/agents/napcat/skills/napcat-group-files-history/tools/upload_group_file/index.ts +1 -1
  9. package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.js +1 -1
  10. package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.ts +1 -1
  11. package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.js +1 -1
  12. package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.ts +1 -1
  13. package/commands/napcat/endpoint/definition.js +0 -2
  14. package/commands/napcat/endpoint/definition.ts +0 -2
  15. package/lib/adapter.d.ts +23 -0
  16. package/lib/adapter.js +42 -0
  17. package/lib/client.d.ts +53 -16
  18. package/lib/client.js +53 -10
  19. package/lib/console.d.ts +1 -1
  20. package/lib/console.js +2 -5
  21. package/lib/endpoint-management.d.ts +2 -2
  22. package/lib/endpoint-management.js +13 -13
  23. package/lib/http-endpoint.d.ts +5 -15
  24. package/lib/http-endpoint.js +44 -36
  25. package/lib/identity.d.ts +3 -0
  26. package/lib/identity.js +32 -0
  27. package/lib/index.d.ts +3 -2
  28. package/lib/index.js +2 -2
  29. package/lib/onebot-get-msg.d.ts +2 -2
  30. package/lib/onebot-get-msg.js +2 -2
  31. package/lib/protocol.d.ts +5 -0
  32. package/lib/protocol.js +23 -11
  33. package/lib/ws-endpoint.d.ts +6 -16
  34. package/lib/ws-endpoint.js +57 -48
  35. package/lib/ws-transport.d.ts +18 -12
  36. package/lib/ws-transport.js +28 -72
  37. package/lib/ws-types.d.ts +0 -5
  38. package/lib/wss-endpoint.d.ts +7 -15
  39. package/lib/wss-endpoint.js +56 -47
  40. package/package.json +18 -18
  41. package/plugin.js +0 -3
  42. package/src/adapter.ts +45 -0
  43. package/src/client.ts +91 -19
  44. package/src/console.ts +2 -5
  45. package/src/endpoint-management.ts +19 -15
  46. package/src/http-endpoint.ts +47 -45
  47. package/src/identity.ts +29 -0
  48. package/src/index.ts +9 -5
  49. package/src/onebot-get-msg.ts +4 -4
  50. package/src/protocol.ts +26 -14
  51. package/src/ws-endpoint.ts +61 -54
  52. package/src/ws-transport.ts +40 -101
  53. package/src/ws-types.ts +0 -6
  54. package/src/wss-endpoint.ts +60 -54
  55. package/lib/napcat-runtime-state.d.ts +0 -1
  56. package/lib/napcat-runtime-state.js +0 -6
  57. package/src/napcat-runtime-state.ts +0 -7
@@ -3,32 +3,42 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
3
3
  * NapCat WS client endpoint — outbound connect to NapCat.
4
4
  */
5
5
  import WebSocket from 'ws';
6
- import { Endpoint, createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
6
+ import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
7
7
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
8
8
  import { createNapCatEndpointManagement } from './endpoint-management.js';
9
9
  import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
10
10
  import { buildSendAction, buildWsConnectOptions, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, senderUserId, } from './protocol.js';
11
11
  import { receiveNapCatSideEvent } from './side-event-dispatch.js';
12
- import { callNapCatWsAction, handleNapCatWsMessage, rejectAllPending, } from './ws-transport.js';
13
12
  import { createNapCatContentPort } from './onebot-get-msg.js';
14
13
  import { NapcatClient } from './client.js';
15
- export class NapCatWsEndpoint extends Endpoint {
16
- client = new NapcatClient((action, params) => this.#callApi(action, params));
14
+ export class NapCatWsEndpoint extends NapcatClient {
15
+ [endpointSpec];
17
16
  #logger;
18
17
  #options;
19
18
  #inboundDeduper = new InboundMessageDeduper();
20
- management = createNapCatEndpointManagement(this.client);
21
- control = createRecallEndpointControl((id) => this.recallMessage(id));
22
- content = createNapCatContentPort((action, params) => this.client.callApi(action, params));
19
+ #management = createNapCatEndpointManagement(this);
20
+ #control = createRecallEndpointControl((id, signal) => this.#recallMessage(id, signal));
21
+ #content = createNapCatContentPort(request => this.requestApi(request));
23
22
  #lifecycle;
24
- #ws;
25
- #requestId = { value: 0 };
26
- #pending = new Map();
27
23
  #open = false;
28
24
  constructor(options) {
29
- super();
25
+ super({ config: options.config, signal: options.signal });
30
26
  this.#logger = getAdapterLogger('napcat', options.config.id);
31
27
  this.#options = options;
28
+ const readTransportState = () => this.#transportState;
29
+ this[endpointSpec] = createEndpointSpecification(this, {
30
+ name: options.config.id,
31
+ start: () => this.#start(), open: () => this.#openEndpoint(),
32
+ close: () => this.#close(), stop: () => this.#stop(),
33
+ send: request => this.#send(request),
34
+ management: this.#management, control: this.#control, content: this.#content,
35
+ get transportState() { return readTransportState(); },
36
+ });
37
+ this.on('event', event => {
38
+ void this[endpointSpec].events.run(() => this.#admit(event)).catch((error) => {
39
+ this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
40
+ });
41
+ });
32
42
  this.#lifecycle = createEndpointLifecycle({
33
43
  name: options.config.id,
34
44
  heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
@@ -41,39 +51,42 @@ export class NapCatWsEndpoint extends Endpoint {
41
51
  },
42
52
  });
43
53
  }
44
- get transportState() {
54
+ get #transportState() {
45
55
  return this.#lifecycle.state;
46
56
  }
47
- async start() {
57
+ get actionSocket() {
58
+ return this.#lifecycle.state === 'open' ? this.transportSocket : undefined;
59
+ }
60
+ async #start() {
48
61
  if (this.#lifecycle.started)
49
62
  return;
50
63
  await this.#lifecycle.start((handle) => this.#connect(handle));
51
64
  }
52
- open() {
65
+ #openEndpoint() {
53
66
  this.#open = true;
54
67
  }
55
- close() {
68
+ #close() {
56
69
  this.#open = false;
57
70
  }
58
- async stop() {
71
+ async #stop() {
59
72
  this.#open = false;
60
73
  await this.#lifecycle.stop();
61
- rejectAllPending(this.#pending);
74
+ this.rpc.disconnect();
62
75
  this.#inboundDeduper.clear();
63
- if (this.#ws) {
76
+ if (this.transportSocket) {
64
77
  try {
65
- this.#ws.close();
78
+ this.transportSocket.close();
66
79
  }
67
80
  catch {
68
81
  /* ignore */
69
82
  }
70
- this.#ws = undefined;
83
+ this.transportSocket = undefined;
71
84
  }
72
85
  }
73
- async send({ conversation, payload }) {
86
+ async #send({ conversation, payload, signal }) {
74
87
  const message = formatOutboundSegments(payload);
75
88
  const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
76
- const data = await this.client.callApi(action, params);
89
+ const data = await this.requestApi({ action, params, signal });
77
90
  const rawId = data?.message_id;
78
91
  if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
79
92
  throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
@@ -87,26 +100,24 @@ export class NapCatWsEndpoint extends Endpoint {
87
100
  }));
88
101
  return messageId;
89
102
  }
90
- async recallMessage(messageId) {
103
+ async #recallMessage(messageId, signal) {
91
104
  if (!messageId)
92
105
  return;
93
- await this.client.callApi('delete_msg', { message_id: Number(messageId) });
94
- }
95
- #callApi(action, params = {}) {
96
- return callNapCatWsAction(this.#lifecycle.state === 'open' ? this.#ws : undefined, this.#pending, this.#requestId, action, params);
106
+ await this.requestApi({ action: 'delete_msg', params: { message_id: Number(messageId) }, signal });
97
107
  }
98
108
  /** Test / internal: admit a parsed event when the endpoint is open. */
99
- admit(ev) {
100
- if (!this.#open)
109
+ #admit(ev) {
110
+ const nativeEvent = ev;
111
+ if (!this.#open && !this.#lifecycle.started)
101
112
  return;
102
- void this.emitPlatform(napCatPlatformEventName(ev), ev).catch((error) => {
113
+ void this[endpointSpec].events.platform(napCatPlatformEventName(ev), ev).catch((error) => {
103
114
  this.#logger.warn(formatCompact({
104
115
  op: 'napcat_platform_event_failed',
105
116
  error: error instanceof Error ? error.message : String(error),
106
117
  }));
107
118
  });
108
119
  if (!isMessageEvent(ev)) {
109
- receiveNapCatSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, this.client, ev, this.#logger);
120
+ receiveNapCatSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), this.#options.config.id, this, ev, this.#logger);
110
121
  return;
111
122
  }
112
123
  if (isSelfMessage(ev))
@@ -117,11 +128,13 @@ export class NapCatWsEndpoint extends Endpoint {
117
128
  if (Array.isArray(ev.message) || typeof ev.message === 'string') {
118
129
  ev = { ...ev, message: normalizeMessage(ev.message) };
119
130
  }
120
- const conversation = napcatInboundConversation(String(this.#options.id), ev);
131
+ const conversation = { ...napcatInboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
121
132
  const content = formatInboundContent(ev);
122
133
  const nickname = senderNickname(ev);
123
134
  const mentioned = isNapCatBotMentioned(ev);
124
- void this.emit('message.receive', {
135
+ void this[endpointSpec].events.message({
136
+ event: nativeEvent,
137
+ ...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
125
138
  conversation,
126
139
  message: { conversation, id: msgId },
127
140
  content,
@@ -156,10 +169,10 @@ export class NapCatWsEndpoint extends Endpoint {
156
169
  await new Promise((resolve, reject) => {
157
170
  let settled = false;
158
171
  const ws = create(url, { headers });
159
- this.#ws = ws;
172
+ this.transportSocket = ws;
160
173
  handle.onForceClose(() => {
161
- if (this.#ws === ws) {
162
- rejectAllPending(this.#pending);
174
+ if (this.transportSocket === ws) {
175
+ this.rpc.disconnect();
163
176
  }
164
177
  try {
165
178
  ws.close();
@@ -169,7 +182,7 @@ export class NapCatWsEndpoint extends Endpoint {
169
182
  }
170
183
  });
171
184
  ws.on('open', () => {
172
- if (settled || this.#ws !== ws || !this.#lifecycle.started)
185
+ if (settled || this.transportSocket !== ws || !this.#lifecycle.started)
173
186
  return;
174
187
  settled = true;
175
188
  if (!this.#options.config.access_token) {
@@ -198,24 +211,20 @@ export class NapCatWsEndpoint extends Endpoint {
198
211
  resolve();
199
212
  });
200
213
  ws.on('pong', () => {
201
- if (this.#ws === ws)
214
+ if (this.transportSocket === ws)
202
215
  this.#lifecycle.notifyHeartbeatAck();
203
216
  });
204
217
  ws.on('message', (data) => {
205
- if (this.#ws !== ws || ws.readyState !== 1 || !this.#lifecycle.started)
218
+ if (this.transportSocket !== ws || ws.readyState !== 1 || !this.#lifecycle.started)
206
219
  return;
207
220
  this.#lifecycle.notifyHeartbeatAck();
208
- handleNapCatWsMessage(data, {
209
- endpointId: this.#options.config.id,
210
- pending: this.#pending,
211
- admit: (event) => this.admit(event),
212
- });
221
+ this.rpc.receive(data, event => this.ingest(event));
213
222
  });
214
223
  ws.on('close', (code, reason) => {
215
- if (this.#ws !== ws)
224
+ if (this.transportSocket !== ws)
216
225
  return;
217
- this.#ws = undefined;
218
- rejectAllPending(this.#pending);
226
+ this.transportSocket = undefined;
227
+ this.rpc.disconnect();
219
228
  const reasonStr = typeof reason === 'string'
220
229
  ? reason
221
230
  : Buffer.isBuffer(reason)
@@ -241,7 +250,7 @@ export class NapCatWsEndpoint extends Endpoint {
241
250
  }
242
251
  });
243
252
  ws.on('error', (err) => {
244
- if (this.#ws !== ws)
253
+ if (this.transportSocket !== ws)
245
254
  return;
246
255
  const error = err instanceof Error ? err : new Error(String(err));
247
256
  this.#logger.warn(formatCompact({
@@ -1,13 +1,19 @@
1
- import { type NapCatEvent } from './protocol.js';
2
- import { type NapCatPendingAction, type NapCatWsSocket } from './ws-types.js';
3
- export declare function decodeWsPayload(data: unknown): string;
4
- export declare function handleNapCatWsMessage(data: unknown, options: {
1
+ import { type PendingRpc, type PendingRpcRequest } from 'zhin.js/adapter';
2
+ import type { NapCatActionResponse, NapCatEvent } from './protocol.js';
3
+ import { type NapCatWsSocket } from './ws-types.js';
4
+ export interface NapCatRpcOptions {
5
5
  readonly endpointId: string;
6
- readonly pending: Map<string, NapCatPendingAction>;
7
- readonly admit: (ev: NapCatEvent) => void;
8
- }): void;
9
- export declare function callNapCatWsAction(ws: NapCatWsSocket | undefined, pending: Map<string, NapCatPendingAction>, requestId: {
10
- value: number;
11
- }, action: string, params: Record<string, unknown>): Promise<unknown>;
12
- export declare function startNapCatHeartbeat(ws: NapCatWsSocket | undefined, intervalMs: number, existingTimer?: NodeJS.Timeout): NodeJS.Timeout;
13
- export declare function rejectAllPending(pending: Map<string, NapCatPendingAction>, message?: string): void;
6
+ readonly signal?: AbortSignal;
7
+ /** Reads the lifecycle-selected socket; RPC does not open or reconnect it. */
8
+ readonly socket: () => NapCatWsSocket | undefined;
9
+ }
10
+ export interface NapCatRpc {
11
+ call(request: PendingRpcRequest): Promise<NapCatActionResponse>;
12
+ receive(data: unknown, ingest: (event: NapCatEvent) => void): void;
13
+ disconnect: PendingRpc<NapCatActionResponse>['disconnect'];
14
+ dispose(): void;
15
+ readonly pendingCount: number;
16
+ }
17
+ /** Native SDK and semantic ports share this Client's correlation and cancellation owner. */
18
+ export declare function createNapCatRpc(options: NapCatRpcOptions): NapCatRpc;
19
+ export declare function decodeWsPayload(data: unknown): string;
@@ -1,8 +1,33 @@
1
- import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
- import { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';
1
+ import { createPendingRpc } from 'zhin.js/adapter';
3
2
  import { formatCompact, getLogger } from '@zhin.js/logger';
4
- import { WS_OPEN, } from './ws-types.js';
3
+ import { WS_OPEN } from './ws-types.js';
5
4
  const logger = getLogger('napcat');
5
+ /** Native SDK and semantic ports share this Client's correlation and cancellation owner. */
6
+ export function createNapCatRpc(options) {
7
+ const pending = createPendingRpc({
8
+ signal: options.signal, requestPrefix: 'req_', timeoutMs: 30_000,
9
+ transport: () => { const socket = options.socket(); return socket?.readyState === WS_OPEN ? socket : undefined; },
10
+ serialize: request => JSON.stringify({ action: request.action, params: request.params ?? {}, echo: request.id }),
11
+ });
12
+ return Object.freeze({
13
+ call: pending.call, disconnect: pending.disconnect, dispose: pending.dispose,
14
+ get pendingCount() { return pending.pendingCount; },
15
+ receive(data, ingest) {
16
+ try {
17
+ const message = JSON.parse(decodeWsPayload(data));
18
+ if ('echo' in message && typeof message.echo === 'string') {
19
+ pending.accept({ id: message.echo, value: message });
20
+ return;
21
+ }
22
+ ingest(message);
23
+ }
24
+ catch (error) {
25
+ logger.warn(formatCompact({ op: 'napcat_parse_failed', endpoint: options.endpointId,
26
+ error: error instanceof Error ? error.message : String(error) }));
27
+ }
28
+ },
29
+ });
30
+ }
6
31
  export function decodeWsPayload(data) {
7
32
  if (typeof data === 'string')
8
33
  return data;
@@ -12,72 +37,3 @@ export function decodeWsPayload(data) {
12
37
  return new TextDecoder().decode(data);
13
38
  return String(data ?? '');
14
39
  }
15
- export function handleNapCatWsMessage(data, options) {
16
- try {
17
- const raw = decodeWsPayload(data);
18
- const msg = JSON.parse(raw);
19
- if ('echo' in msg && typeof msg.echo === 'string') {
20
- const resp = msg;
21
- const pending = options.pending.get(resp.echo);
22
- if (pending) {
23
- options.pending.delete(resp.echo);
24
- clearTimeout(pending.timeout);
25
- if (resp.status === 'ok')
26
- pending.resolve(resp.data);
27
- else {
28
- pending.reject(new EndpointDeliveryError('platform_rejected', `API error [${resp.retcode}]: ${resp.message || resp.wording || 'unknown'}`, 'rejected'));
29
- }
30
- }
31
- return;
32
- }
33
- options.admit(msg);
34
- }
35
- catch (error) {
36
- logger.warn(formatCompact({
37
- op: 'napcat_parse_failed',
38
- endpoint: options.endpointId,
39
- error: error instanceof Error ? error.message : String(error),
40
- }));
41
- }
42
- }
43
- export function callNapCatWsAction(ws, pending, requestId, action, params) {
44
- if (!ws || ws.readyState !== WS_OPEN) {
45
- return Promise.reject(new EndpointDeliveryError('endpoint_disconnected', 'WebSocket is not connected', 'not_sent'));
46
- }
47
- const echo = `req_${++requestId.value}`;
48
- const req = { action, params, echo };
49
- return new Promise((resolve, reject) => {
50
- const timeout = setTimeout(() => {
51
- pending.delete(echo);
52
- reject(new EndpointDeliveryError('endpoint_timeout', `API call timeout: ${action}`, 'unknown'));
53
- }, 30_000);
54
- pending.set(echo, { resolve, reject, timeout });
55
- try {
56
- ws.send(JSON.stringify(req));
57
- }
58
- catch (error) {
59
- pending.delete(echo);
60
- clearTimeout(timeout);
61
- reject(error instanceof Error ? error : new Error(String(error)));
62
- }
63
- });
64
- }
65
- export function startNapCatHeartbeat(ws, intervalMs, existingTimer) {
66
- if (existingTimer)
67
- clearInterval(existingTimer);
68
- return setInterval(() => {
69
- try {
70
- ws?.ping?.();
71
- }
72
- catch {
73
- /* ignore */
74
- }
75
- }, intervalMs);
76
- }
77
- export function rejectAllPending(pending, message = 'Connection closed') {
78
- for (const [, entry] of pending) {
79
- clearTimeout(entry.timeout);
80
- entry.reject(new EndpointDeliveryError('endpoint_disconnected', message, 'unknown'));
81
- }
82
- pending.clear();
83
- }
package/lib/ws-types.d.ts CHANGED
@@ -10,8 +10,3 @@ export interface NapCatWsCreateOptions {
10
10
  readonly headers?: Record<string, string>;
11
11
  }
12
12
  export declare const WS_OPEN = 1;
13
- export interface NapCatPendingAction {
14
- resolve: (value: unknown) => void;
15
- reject: (err: Error) => void;
16
- timeout: NodeJS.Timeout;
17
- }
@@ -1,29 +1,21 @@
1
1
  /**
2
2
  * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
3
3
  */
4
- import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest, type EndpointTransportState } from 'zhin.js/adapter';
4
+ import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
5
5
  import type { HttpHost } from '@zhin.js/host-http';
6
6
  import type { CapabilityId } from 'zhin.js';
7
- import { type NapCatEvent, type NapCatWssConfig } from './protocol.js';
7
+ import { type NapCatWssConfig } from './protocol.js';
8
8
  import { NapcatClient } from './client.js';
9
+ import { type NapCatWsSocket } from './ws-types.js';
9
10
  export interface NapCatWssEndpointOptions {
10
11
  readonly id: CapabilityId;
12
+ readonly signal?: AbortSignal;
11
13
  readonly http: HttpHost;
12
14
  readonly config: NapCatWssConfig;
13
15
  }
14
- export declare class NapCatWssEndpoint extends Endpoint<NapcatClient> {
16
+ export declare class NapCatWssEndpoint extends NapcatClient implements Endpoint {
15
17
  #private;
16
- readonly client: NapcatClient;
17
- readonly management: EndpointManagement;
18
- readonly control: EndpointControl;
19
- readonly content: import("@zhin.js/adapter").EndpointContentPort;
18
+ readonly [endpointSpec]: EndpointSpecification;
20
19
  constructor(options: NapCatWssEndpointOptions);
21
- get transportState(): EndpointTransportState;
22
- start(): Promise<void>;
23
- open(): void;
24
- close(): void;
25
- stop(): Promise<void>;
26
- send({ conversation, payload }: EndpointSendRequest): Promise<string>;
27
- recallMessage(messageId: string): Promise<void>;
28
- admit(ev: NapCatEvent): void;
20
+ protected get actionSocket(): NapCatWsSocket | undefined;
29
21
  }
@@ -2,7 +2,7 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
2
  /**
3
3
  * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
4
4
  */
5
- import { Endpoint, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
5
+ import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
6
6
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
7
7
  import { createNapCatEndpointManagement } from './endpoint-management.js';
8
8
  import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
@@ -10,47 +10,60 @@ import { buildSendAction, formatInboundContent, formatOutboundSegments, isMessag
10
10
  import { receiveNapCatSideEvent } from './side-event-dispatch.js';
11
11
  import { createNapCatContentPort } from './onebot-get-msg.js';
12
12
  import { NapcatClient } from './client.js';
13
- import { callNapCatWsAction, handleNapCatWsMessage, rejectAllPending, } from './ws-transport.js';
14
13
  import { verifyNapCatAccessToken } from './wss-auth.js';
15
- export class NapCatWssEndpoint extends Endpoint {
16
- client = new NapcatClient((action, params) => this.#callApi(action, params));
14
+ export class NapCatWssEndpoint extends NapcatClient {
15
+ [endpointSpec];
17
16
  #logger;
18
17
  #options;
19
18
  #inboundDeduper = new InboundMessageDeduper();
20
- management = createNapCatEndpointManagement(this.client);
21
- control = createRecallEndpointControl((id) => this.recallMessage(id));
22
- content = createNapCatContentPort((action, params) => this.client.callApi(action, params));
19
+ #management = createNapCatEndpointManagement(this);
20
+ #control = createRecallEndpointControl((id, signal) => this.#recallMessage(id, signal));
21
+ #content = createNapCatContentPort(request => this.requestApi(request));
23
22
  #connectionLifecycle;
24
- #ws;
25
23
  #wsRelease;
26
24
  #connectionTask = Promise.resolve();
27
- #requestId = { value: 0 };
28
- #pending = new Map();
29
25
  #open = false;
30
26
  #started = false;
31
27
  #stopped = false;
32
28
  constructor(options) {
33
- super();
29
+ super({ config: options.config, signal: options.signal });
34
30
  this.#logger = getAdapterLogger('napcat', options.config.id);
35
31
  this.#options = options;
32
+ const readTransportState = () => this.#transportState;
33
+ this[endpointSpec] = createEndpointSpecification(this, {
34
+ name: options.config.id,
35
+ start: () => this.#start(), open: () => this.#openEndpoint(),
36
+ close: () => this.#close(), stop: () => this.#stop(),
37
+ send: request => this.#send(request),
38
+ management: this.#management, control: this.#control, content: this.#content,
39
+ get transportState() { return readTransportState(); },
40
+ });
41
+ this.on('event', event => {
42
+ void this[endpointSpec].events.run(() => this.#admit(event)).catch((error) => {
43
+ this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
44
+ });
45
+ });
36
46
  this.#connectionLifecycle = createEndpointLifecycle({
37
47
  name: `${options.config.id}:inbound`,
38
48
  reconnect: false,
39
49
  heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
40
50
  });
41
51
  }
42
- get transportState() {
52
+ get #transportState() {
43
53
  if (this.#stopped)
44
54
  return 'stopped';
45
55
  if (!this.#started)
46
56
  return 'idle';
47
57
  if (this.#connectionLifecycle.state === 'closed')
48
58
  return 'closed';
49
- if (this.#ws?.readyState === 1)
59
+ if (this.transportSocket?.readyState === 1)
50
60
  return 'open';
51
61
  return this.#connectionLifecycle.state === 'connecting' ? 'connecting' : 'idle';
52
62
  }
53
- async start() {
63
+ get actionSocket() {
64
+ return this.#connectionLifecycle.state === 'closed' ? undefined : this.transportSocket;
65
+ }
66
+ async #start() {
54
67
  if (this.#started)
55
68
  return;
56
69
  this.#stopped = false;
@@ -74,53 +87,51 @@ export class NapCatWssEndpoint extends Endpoint {
74
87
  path: this.#options.config.path,
75
88
  }));
76
89
  }
77
- open() {
90
+ #openEndpoint() {
78
91
  this.#open = true;
79
92
  }
80
- close() {
93
+ #close() {
81
94
  this.#open = false;
82
95
  }
83
- async stop() {
96
+ async #stop() {
84
97
  this.#stopped = true;
85
98
  this.#open = false;
86
99
  this.#wsRelease?.();
87
100
  this.#wsRelease = undefined;
88
101
  await this.#connectionTask;
89
102
  await this.#connectionLifecycle.stop();
90
- rejectAllPending(this.#pending);
103
+ this.rpc.disconnect();
91
104
  this.#inboundDeduper.clear();
92
- this.#ws = undefined;
105
+ this.transportSocket = undefined;
93
106
  this.#started = false;
94
107
  }
95
- async send({ conversation, payload }) {
108
+ async #send({ conversation, payload, signal }) {
96
109
  const message = formatOutboundSegments(payload);
97
110
  const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
98
- const data = await this.client.callApi(action, params);
111
+ const data = await this.requestApi({ action, params, signal });
99
112
  const rawId = data?.message_id;
100
113
  if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
101
114
  throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
102
115
  }
103
116
  return String(rawId);
104
117
  }
105
- async recallMessage(messageId) {
118
+ async #recallMessage(messageId, signal) {
106
119
  if (!messageId)
107
120
  return;
108
- await this.client.callApi('delete_msg', { message_id: Number(messageId) });
109
- }
110
- #callApi(action, params = {}) {
111
- return callNapCatWsAction(this.#connectionLifecycle.state === 'closed' ? undefined : this.#ws, this.#pending, this.#requestId, action, params);
121
+ await this.requestApi({ action: 'delete_msg', params: { message_id: Number(messageId) }, signal });
112
122
  }
113
- admit(ev) {
114
- if (!this.#open)
123
+ #admit(ev) {
124
+ const nativeEvent = ev;
125
+ if (!this.#open && !this.#started)
115
126
  return;
116
- void this.emitPlatform(napCatPlatformEventName(ev), ev).catch((error) => {
127
+ void this[endpointSpec].events.platform(napCatPlatformEventName(ev), ev).catch((error) => {
117
128
  this.#logger.warn(formatCompact({
118
129
  op: 'napcat_platform_event_failed',
119
130
  error: error instanceof Error ? error.message : String(error),
120
131
  }));
121
132
  });
122
133
  if (!isMessageEvent(ev)) {
123
- receiveNapCatSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, this.client, ev, this.#logger);
134
+ receiveNapCatSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), this.#options.config.id, this, ev, this.#logger);
124
135
  return;
125
136
  }
126
137
  if (isSelfMessage(ev))
@@ -131,10 +142,12 @@ export class NapCatWssEndpoint extends Endpoint {
131
142
  if (Array.isArray(ev.message) || typeof ev.message === 'string') {
132
143
  ev = { ...ev, message: normalizeMessage(ev.message) };
133
144
  }
134
- const conversation = napcatInboundConversation(String(this.#options.id), ev);
145
+ const conversation = { ...napcatInboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
135
146
  const nickname = senderNickname(ev);
136
147
  const mentioned = isNapCatBotMentioned(ev);
137
- void this.emit('message.receive', {
148
+ void this[endpointSpec].events.message({
149
+ event: nativeEvent,
150
+ ...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
138
151
  conversation,
139
152
  message: { conversation, id: msgId },
140
153
  content: formatInboundContent(ev),
@@ -173,12 +186,12 @@ export class NapCatWssEndpoint extends Endpoint {
173
186
  }
174
187
  const socket = connection.socket;
175
188
  await this.#connectionLifecycle.stop();
176
- rejectAllPending(this.#pending, 'Connection replaced');
189
+ this.rpc.disconnect('Connection replaced');
177
190
  await this.#connectionLifecycle.start(async (lifecycleHandle) => {
178
- this.#ws = socket;
191
+ this.transportSocket = socket;
179
192
  lifecycleHandle.onForceClose(() => {
180
- if (this.#ws === socket) {
181
- rejectAllPending(this.#pending);
193
+ if (this.transportSocket === socket) {
194
+ this.rpc.disconnect();
182
195
  }
183
196
  try {
184
197
  socket.close();
@@ -196,29 +209,25 @@ export class NapCatWssEndpoint extends Endpoint {
196
209
  }
197
210
  }, this.#options.config.heartbeat_interval);
198
211
  socket.on('pong', () => {
199
- if (this.#ws === socket)
212
+ if (this.transportSocket === socket)
200
213
  this.#connectionLifecycle.notifyHeartbeatAck();
201
214
  });
202
215
  socket.on('message', (data) => {
203
- if (this.#ws !== socket || this.#connectionLifecycle.state === 'closed')
216
+ if (this.transportSocket !== socket || this.#connectionLifecycle.state === 'closed')
204
217
  return;
205
218
  this.#connectionLifecycle.notifyHeartbeatAck();
206
- handleNapCatWsMessage(data, {
207
- endpointId: this.#options.config.id,
208
- pending: this.#pending,
209
- admit: (event) => this.admit(event),
210
- });
219
+ this.rpc.receive(data, event => this.ingest(event));
211
220
  });
212
221
  socket.on('error', (error) => {
213
- if (this.#ws !== socket)
222
+ if (this.transportSocket !== socket)
214
223
  return;
215
224
  this.#logger.warn(formatCompact({ op: 'ws_error', error: String(error) }));
216
225
  });
217
226
  socket.on('close', () => {
218
- if (this.#ws !== socket)
227
+ if (this.transportSocket !== socket)
219
228
  return;
220
- this.#ws = undefined;
221
- rejectAllPending(this.#pending);
229
+ this.transportSocket = undefined;
230
+ this.rpc.disconnect();
222
231
  lifecycleHandle.notifyClosed(new Error('NapCat reverse WebSocket closed'));
223
232
  });
224
233
  });