@zhin.js/adapter-napcat 1.1.4 → 1.1.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @zhin.js/adapter-napcat
2
2
 
3
+ ## 1.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [badcc1f]
8
+ - @zhin.js/core@1.1.41
9
+ - @zhin.js/feature-kit@1.1.3
10
+ - @zhin.js/adapter@1.1.17
11
+ - @zhin.js/agent-feature@1.1.4
12
+ - @zhin.js/skill@1.1.4
13
+ - @zhin.js/tool@1.1.4
14
+ - @zhin.js/host-http@1.1.4
15
+ - @zhin.js/permission@1.1.4
16
+
17
+ ## 1.1.5
18
+
19
+ ### Patch Changes
20
+
21
+ - b9b3953: Fix adapter connection races, transport health reporting, candidate rollback and delivery acknowledgement boundaries. Separate generation admission from locally observed connectivity, reject unconfirmed message receipts, and mark uncertain delivery results without recommending automatic replay.
22
+
23
+ Class and compact endpoints should expose a `transportState` getter to report physical connection or local listener health. Unobserved endpoints now report an unknown transport and remain offline in diagnostics. Platform stability tiers remain unchanged until real account acceptance evidence is available.
24
+
25
+ - Updated dependencies [b9b3953]
26
+ - Updated dependencies [b9b3953]
27
+ - Updated dependencies [b9b3953]
28
+ - Updated dependencies [b9b3953]
29
+ - Updated dependencies [b9b3953]
30
+ - Updated dependencies [b9b3953]
31
+ - Updated dependencies [b9b3953]
32
+ - Updated dependencies [b9b3953]
33
+ - Updated dependencies [b9b3953]
34
+ - Updated dependencies [b9b3953]
35
+ - Updated dependencies [b9b3953]
36
+ - @zhin.js/adapter@1.1.16
37
+ - @zhin.js/im-contract@1.1.3
38
+ - @zhin.js/core@1.1.40
39
+ - @zhin.js/host-http@1.1.3
40
+ - @zhin.js/tool@1.1.3
41
+ - @zhin.js/skill@1.1.3
42
+ - @zhin.js/agent-feature@1.1.3
43
+
3
44
  ## 1.1.4
4
45
 
5
46
  ### Patch Changes
package/README.md CHANGED
@@ -97,3 +97,7 @@ endpoint 配置,不会从环境变量推断 endpoint id。
97
97
  ## 许可证
98
98
 
99
99
  MIT License
100
+
101
+ ### 高阶消息支持边界
102
+
103
+ NapCat 官方消息兼容表标明 share 仅接收,不支持发送;Markdown 仅可在双层合并转发内发送,不支持直接消息。Canonical `markdown`、`share`、`keyboard` 直接出站明确返回 `unsupported_operation / not_sent`,不会将占位段或原始协议猜测报告成功。当前未实现合并转发 Markdown,亦无原生按钮回调闭环。参考:[官方兼容表](https://napneko.github.io/develop/msg)。
@@ -1,5 +1,4 @@
1
- import { Endpoint } from 'zhin.js/adapter';
2
- import { type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
1
+ import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest, type EndpointTransportState } from 'zhin.js/adapter';
3
2
  import type { HttpHost } from '@zhin.js/host-http';
4
3
  import type { CapabilityId } from 'zhin.js';
5
4
  import { callNapCatHttpAction, type NapCatEvent, type NapCatHttpConfig } from './protocol.js';
@@ -17,6 +16,7 @@ export declare class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
17
16
  readonly control: EndpointControl;
18
17
  readonly content: import("@zhin.js/adapter").EndpointContentPort;
19
18
  constructor(options: NapCatHttpEndpointOptions);
19
+ get transportState(): EndpointTransportState;
20
20
  start(): Promise<void>;
21
21
  open(): void;
22
22
  close(): void;
@@ -1,5 +1,5 @@
1
- import { Endpoint } from 'zhin.js/adapter';
2
- import { createRecallEndpointControl, } from 'zhin.js/adapter';
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
+ import { Endpoint, createRecallEndpointControl, } from 'zhin.js/adapter';
3
3
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
4
4
  import { createNapCatEndpointManagement } from './endpoint-management.js';
5
5
  import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
@@ -21,15 +21,20 @@ export class NapCatHttpEndpoint extends Endpoint {
21
21
  #routeReleases = [];
22
22
  #open = false;
23
23
  #started = false;
24
+ #stopped = false;
24
25
  constructor(options) {
25
26
  super();
26
27
  this.#logger = getAdapterLogger('napcat', options.config.id);
27
28
  this.#options = options;
28
29
  this.#callHttpAction = options.callHttpAction ?? callNapCatHttpAction;
29
30
  }
31
+ get transportState() {
32
+ return this.#stopped ? 'stopped' : this.#started ? 'open' : 'idle';
33
+ }
30
34
  async start() {
31
35
  if (this.#started)
32
36
  return;
37
+ this.#stopped = false;
33
38
  this.#started = true;
34
39
  this.#setupRoutes();
35
40
  this.#logger.info(formatCompact({
@@ -46,6 +51,7 @@ export class NapCatHttpEndpoint extends Endpoint {
46
51
  this.#open = false;
47
52
  }
48
53
  async stop() {
54
+ this.#stopped = true;
49
55
  this.#open = false;
50
56
  for (const release of this.#routeReleases.splice(0))
51
57
  release();
@@ -61,7 +67,11 @@ export class NapCatHttpEndpoint extends Endpoint {
61
67
  access_token: this.#options.config.access_token,
62
68
  }, action, params);
63
69
  const data = resp.data;
64
- const messageId = data?.message_id != null ? String(data.message_id) : '';
70
+ const rawId = data?.message_id;
71
+ if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
72
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
73
+ }
74
+ const messageId = String(rawId);
65
75
  this.#logger.debug(formatCompact({
66
76
  op: 'napcat_send',
67
77
  endpoint: this.#options.config.id,
package/lib/protocol.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * No legacy Adapter/Endpoint / segment-mapper.
4
4
  * Canonicalization is owned by gateway/core before endpoint.send.
5
5
  */
6
- import { isMediaRef } from '@zhin.js/im-contract';
6
+ import { EndpointDeliveryError, isMediaRef } from '@zhin.js/im-contract';
7
7
  import { formatCompact, getLogger } from '@zhin.js/logger';
8
8
  const logger = getLogger('napcat');
9
9
  function normalizeConnection(connection) {
@@ -189,6 +189,10 @@ function oneBotMediaSegment(type, data) {
189
189
  function canonicalToOneBotSegment(segment) {
190
190
  const data = segment.data ?? {};
191
191
  switch (segment.type) {
192
+ case 'markdown':
193
+ case 'share':
194
+ case 'keyboard':
195
+ throw new EndpointDeliveryError('unsupported_operation', `NapCat direct ${segment.type} sending is not supported`, 'not_sent');
192
196
  case 'mention': {
193
197
  const target = data.target ?? data.qq ?? data.id;
194
198
  if (target == null)
@@ -295,15 +299,16 @@ export async function callNapCatHttpAction(options, action, params = {}) {
295
299
  headers.Authorization = `Bearer ${options.access_token}`;
296
300
  }
297
301
  const res = await fetch(url, {
302
+ signal: AbortSignal.timeout(30_000),
298
303
  method: 'POST',
299
304
  headers,
300
305
  body: JSON.stringify(params),
301
306
  });
302
307
  const text = await res.text();
303
308
  if (res.status === 401)
304
- throw new Error(`NapCat HTTP auth failed: ${text}`);
309
+ throw new EndpointDeliveryError('platform_rejected', `NapCat HTTP auth failed: ${text}`, 'rejected');
305
310
  if (res.status !== 200)
306
- throw new Error(`NapCat HTTP ${res.status}: ${text}`);
311
+ throw new EndpointDeliveryError(res.status === 429 || res.status === 403 ? 'platform_rejected' : 'delivery_unconfirmed', `NapCat HTTP ${res.status}: ${text}`, res.status === 429 || res.status === 403 ? 'rejected' : 'unknown');
307
312
  let data;
308
313
  try {
309
314
  data = JSON.parse(text);
@@ -312,7 +317,7 @@ export async function callNapCatHttpAction(options, action, params = {}) {
312
317
  throw new Error(`NapCat HTTP invalid response: ${text.slice(0, 200)}`);
313
318
  }
314
319
  if (data.status !== 'ok' && data.retcode !== 0) {
315
- throw new Error(`NapCat HTTP action failed [${data.retcode}]: ${data.message || data.wording || 'unknown'}`);
320
+ throw new EndpointDeliveryError('platform_rejected', `NapCat HTTP action failed [${data.retcode}]: ${data.message || data.wording || 'unknown'}`, 'rejected');
316
321
  }
317
322
  return data;
318
323
  }
@@ -1,5 +1,4 @@
1
- import { Endpoint } from 'zhin.js/adapter';
2
- import { type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
1
+ import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest, type EndpointTransportState } from 'zhin.js/adapter';
3
2
  import type { CapabilityId } from 'zhin.js';
4
3
  import { type NapCatEvent, type NapCatWsConfig } from './protocol.js';
5
4
  import { type NapCatWsCreateOptions, type NapCatWsSocket } from './ws-types.js';
@@ -16,6 +15,7 @@ export declare class NapCatWsEndpoint extends Endpoint<NapcatClient> {
16
15
  readonly control: EndpointControl;
17
16
  readonly content: import("@zhin.js/adapter").EndpointContentPort;
18
17
  constructor(options: NapCatWsEndpointOptions);
18
+ get transportState(): EndpointTransportState;
19
19
  start(): Promise<void>;
20
20
  open(): void;
21
21
  close(): void;
@@ -1,9 +1,9 @@
1
- import { Endpoint } from 'zhin.js/adapter';
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
2
  /**
3
3
  * NapCat WS client endpoint — outbound connect to NapCat.
4
4
  */
5
5
  import WebSocket from 'ws';
6
- import { createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
6
+ import { Endpoint, 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';
@@ -31,6 +31,7 @@ export class NapCatWsEndpoint extends Endpoint {
31
31
  this.#options = options;
32
32
  this.#lifecycle = createEndpointLifecycle({
33
33
  name: options.config.id,
34
+ heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
34
35
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
35
36
  reconnect: {
36
37
  initialIntervalMs: options.config.reconnect_interval,
@@ -40,6 +41,9 @@ export class NapCatWsEndpoint extends Endpoint {
40
41
  },
41
42
  });
42
43
  }
44
+ get transportState() {
45
+ return this.#lifecycle.state;
46
+ }
43
47
  async start() {
44
48
  if (this.#lifecycle.started)
45
49
  return;
@@ -70,7 +74,11 @@ export class NapCatWsEndpoint extends Endpoint {
70
74
  const message = formatOutboundSegments(payload);
71
75
  const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
72
76
  const data = await this.client.callApi(action, params);
73
- const messageId = data?.message_id != null ? String(data.message_id) : '';
77
+ const rawId = data?.message_id;
78
+ if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
79
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
80
+ }
81
+ const messageId = String(rawId);
74
82
  this.#logger.debug(formatCompact({
75
83
  op: 'napcat_send',
76
84
  endpoint: this.#options.config.id,
@@ -85,7 +93,7 @@ export class NapCatWsEndpoint extends Endpoint {
85
93
  await this.client.callApi('delete_msg', { message_id: Number(messageId) });
86
94
  }
87
95
  #callApi(action, params = {}) {
88
- return callNapCatWsAction(this.#ws, this.#pending, this.#requestId, action, params);
96
+ return callNapCatWsAction(this.#lifecycle.state === 'open' ? this.#ws : undefined, this.#pending, this.#requestId, action, params);
89
97
  }
90
98
  /** Test / internal: admit a parsed event when the endpoint is open. */
91
99
  admit(ev) {
@@ -150,6 +158,9 @@ export class NapCatWsEndpoint extends Endpoint {
150
158
  const ws = create(url, { headers });
151
159
  this.#ws = ws;
152
160
  handle.onForceClose(() => {
161
+ if (this.#ws === ws) {
162
+ rejectAllPending(this.#pending);
163
+ }
153
164
  try {
154
165
  ws.close();
155
166
  }
@@ -158,7 +169,7 @@ export class NapCatWsEndpoint extends Endpoint {
158
169
  }
159
170
  });
160
171
  ws.on('open', () => {
161
- if (settled)
172
+ if (settled || this.#ws !== ws || !this.#lifecycle.started)
162
173
  return;
163
174
  settled = true;
164
175
  if (!this.#options.config.access_token) {
@@ -186,7 +197,14 @@ export class NapCatWsEndpoint extends Endpoint {
186
197
  }
187
198
  resolve();
188
199
  });
200
+ ws.on('pong', () => {
201
+ if (this.#ws === ws)
202
+ this.#lifecycle.notifyHeartbeatAck();
203
+ });
189
204
  ws.on('message', (data) => {
205
+ if (this.#ws !== ws || ws.readyState !== 1 || !this.#lifecycle.started)
206
+ return;
207
+ this.#lifecycle.notifyHeartbeatAck();
190
208
  handleNapCatWsMessage(data, {
191
209
  endpointId: this.#options.config.id,
192
210
  pending: this.#pending,
@@ -194,6 +212,10 @@ export class NapCatWsEndpoint extends Endpoint {
194
212
  });
195
213
  });
196
214
  ws.on('close', (code, reason) => {
215
+ if (this.#ws !== ws)
216
+ return;
217
+ this.#ws = undefined;
218
+ rejectAllPending(this.#pending);
197
219
  const reasonStr = typeof reason === 'string'
198
220
  ? reason
199
221
  : Buffer.isBuffer(reason)
@@ -219,6 +241,8 @@ export class NapCatWsEndpoint extends Endpoint {
219
241
  }
220
242
  });
221
243
  ws.on('error', (err) => {
244
+ if (this.#ws !== ws)
245
+ return;
222
246
  const error = err instanceof Error ? err : new Error(String(err));
223
247
  this.#logger.warn(formatCompact({
224
248
  op: 'ws_error',
@@ -1,3 +1,4 @@
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
1
2
  import { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';
2
3
  import { formatCompact, getLogger } from '@zhin.js/logger';
3
4
  import { WS_OPEN, } from './ws-types.js';
@@ -24,7 +25,7 @@ export function handleNapCatWsMessage(data, options) {
24
25
  if (resp.status === 'ok')
25
26
  pending.resolve(resp.data);
26
27
  else {
27
- pending.reject(new Error(`API error [${resp.retcode}]: ${resp.message || resp.wording || 'unknown'}`));
28
+ pending.reject(new EndpointDeliveryError('platform_rejected', `API error [${resp.retcode}]: ${resp.message || resp.wording || 'unknown'}`, 'rejected'));
28
29
  }
29
30
  }
30
31
  return;
@@ -41,17 +42,24 @@ export function handleNapCatWsMessage(data, options) {
41
42
  }
42
43
  export function callNapCatWsAction(ws, pending, requestId, action, params) {
43
44
  if (!ws || ws.readyState !== WS_OPEN) {
44
- return Promise.reject(new Error('WebSocket is not connected'));
45
+ return Promise.reject(new EndpointDeliveryError('endpoint_disconnected', 'WebSocket is not connected', 'not_sent'));
45
46
  }
46
47
  const echo = `req_${++requestId.value}`;
47
48
  const req = { action, params, echo };
48
49
  return new Promise((resolve, reject) => {
49
50
  const timeout = setTimeout(() => {
50
51
  pending.delete(echo);
51
- reject(new Error(`API call timeout: ${action}`));
52
+ reject(new EndpointDeliveryError('endpoint_timeout', `API call timeout: ${action}`, 'unknown'));
52
53
  }, 30_000);
53
54
  pending.set(echo, { resolve, reject, timeout });
54
- ws.send(JSON.stringify(req));
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
+ }
55
63
  });
56
64
  }
57
65
  export function startNapCatHeartbeat(ws, intervalMs, existingTimer) {
@@ -69,7 +77,7 @@ export function startNapCatHeartbeat(ws, intervalMs, existingTimer) {
69
77
  export function rejectAllPending(pending, message = 'Connection closed') {
70
78
  for (const [, entry] of pending) {
71
79
  clearTimeout(entry.timeout);
72
- entry.reject(new Error(message));
80
+ entry.reject(new EndpointDeliveryError('endpoint_disconnected', message, 'unknown'));
73
81
  }
74
82
  pending.clear();
75
83
  }
package/lib/ws-types.d.ts CHANGED
@@ -4,7 +4,7 @@ export interface NapCatWsSocket {
4
4
  send(data: string): void;
5
5
  close(code?: number, reason?: string): void;
6
6
  ping?(): void;
7
- on(event: 'open' | 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
7
+ on(event: 'open' | 'message' | 'close' | 'error' | 'pong', listener: (...args: unknown[]) => void): void;
8
8
  }
9
9
  export interface NapCatWsCreateOptions {
10
10
  readonly headers?: Record<string, string>;
@@ -1,8 +1,7 @@
1
- import { Endpoint } from 'zhin.js/adapter';
2
1
  /**
3
2
  * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
4
3
  */
5
- import { type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
4
+ import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest, type EndpointTransportState } from 'zhin.js/adapter';
6
5
  import type { HttpHost } from '@zhin.js/host-http';
7
6
  import type { CapabilityId } from 'zhin.js';
8
7
  import { type NapCatEvent, type NapCatWssConfig } from './protocol.js';
@@ -19,6 +18,7 @@ export declare class NapCatWssEndpoint extends Endpoint<NapcatClient> {
19
18
  readonly control: EndpointControl;
20
19
  readonly content: import("@zhin.js/adapter").EndpointContentPort;
21
20
  constructor(options: NapCatWssEndpointOptions);
21
+ get transportState(): EndpointTransportState;
22
22
  start(): Promise<void>;
23
23
  open(): void;
24
24
  close(): void;
@@ -1,8 +1,8 @@
1
- import { Endpoint } from 'zhin.js/adapter';
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
2
  /**
3
3
  * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
4
4
  */
5
- import { createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
5
+ import { Endpoint, 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';
@@ -28,6 +28,7 @@ export class NapCatWssEndpoint extends Endpoint {
28
28
  #pending = new Map();
29
29
  #open = false;
30
30
  #started = false;
31
+ #stopped = false;
31
32
  constructor(options) {
32
33
  super();
33
34
  this.#logger = getAdapterLogger('napcat', options.config.id);
@@ -35,11 +36,24 @@ export class NapCatWssEndpoint extends Endpoint {
35
36
  this.#connectionLifecycle = createEndpointLifecycle({
36
37
  name: `${options.config.id}:inbound`,
37
38
  reconnect: false,
39
+ heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
38
40
  });
39
41
  }
42
+ get transportState() {
43
+ if (this.#stopped)
44
+ return 'stopped';
45
+ if (!this.#started)
46
+ return 'idle';
47
+ if (this.#connectionLifecycle.state === 'closed')
48
+ return 'closed';
49
+ if (this.#ws?.readyState === 1)
50
+ return 'open';
51
+ return this.#connectionLifecycle.state === 'connecting' ? 'connecting' : 'idle';
52
+ }
40
53
  async start() {
41
54
  if (this.#started)
42
55
  return;
56
+ this.#stopped = false;
43
57
  this.#started = true;
44
58
  const handle = this.#options.http.ws(this.#options.config.path);
45
59
  this.#wsRelease = handle.onConnection((connection) => {
@@ -67,6 +81,7 @@ export class NapCatWssEndpoint extends Endpoint {
67
81
  this.#open = false;
68
82
  }
69
83
  async stop() {
84
+ this.#stopped = true;
70
85
  this.#open = false;
71
86
  this.#wsRelease?.();
72
87
  this.#wsRelease = undefined;
@@ -81,7 +96,11 @@ export class NapCatWssEndpoint extends Endpoint {
81
96
  const message = formatOutboundSegments(payload);
82
97
  const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
83
98
  const data = await this.client.callApi(action, params);
84
- return data?.message_id != null ? String(data.message_id) : '';
99
+ const rawId = data?.message_id;
100
+ if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
101
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
102
+ }
103
+ return String(rawId);
85
104
  }
86
105
  async recallMessage(messageId) {
87
106
  if (!messageId)
@@ -89,7 +108,7 @@ export class NapCatWssEndpoint extends Endpoint {
89
108
  await this.client.callApi('delete_msg', { message_id: Number(messageId) });
90
109
  }
91
110
  #callApi(action, params = {}) {
92
- return callNapCatWsAction(this.#ws, this.#pending, this.#requestId, action, params);
111
+ return callNapCatWsAction(this.#connectionLifecycle.state === 'closed' ? undefined : this.#ws, this.#pending, this.#requestId, action, params);
93
112
  }
94
113
  admit(ev) {
95
114
  if (!this.#open)
@@ -144,15 +163,23 @@ export class NapCatWssEndpoint extends Endpoint {
144
163
  });
145
164
  }
146
165
  async #acceptConnection(connection) {
166
+ if (!this.#started || this.#stopped) {
167
+ connection.socket.close();
168
+ return;
169
+ }
147
170
  if (!verifyNapCatAccessToken(this.#options.config.access_token, connection.request)) {
148
171
  connection.socket.close(4003, 'Unauthorized');
149
172
  return;
150
173
  }
151
174
  const socket = connection.socket;
152
175
  await this.#connectionLifecycle.stop();
176
+ rejectAllPending(this.#pending, 'Connection replaced');
153
177
  await this.#connectionLifecycle.start(async (lifecycleHandle) => {
154
178
  this.#ws = socket;
155
179
  lifecycleHandle.onForceClose(() => {
180
+ if (this.#ws === socket) {
181
+ rejectAllPending(this.#pending);
182
+ }
156
183
  try {
157
184
  socket.close();
158
185
  }
@@ -168,7 +195,13 @@ export class NapCatWssEndpoint extends Endpoint {
168
195
  /* ignore */
169
196
  }
170
197
  }, this.#options.config.heartbeat_interval);
198
+ socket.on('pong', () => {
199
+ if (this.#ws === socket)
200
+ this.#connectionLifecycle.notifyHeartbeatAck();
201
+ });
171
202
  socket.on('message', (data) => {
203
+ if (this.#ws !== socket || this.#connectionLifecycle.state === 'closed')
204
+ return;
172
205
  this.#connectionLifecycle.notifyHeartbeatAck();
173
206
  handleNapCatWsMessage(data, {
174
207
  endpointId: this.#options.config.id,
@@ -176,9 +209,16 @@ export class NapCatWssEndpoint extends Endpoint {
176
209
  admit: (event) => this.admit(event),
177
210
  });
178
211
  });
212
+ socket.on('error', (error) => {
213
+ if (this.#ws !== socket)
214
+ return;
215
+ this.#logger.warn(formatCompact({ op: 'ws_error', error: String(error) }));
216
+ });
179
217
  socket.on('close', () => {
180
- if (this.#ws === socket)
181
- this.#ws = undefined;
218
+ if (this.#ws !== socket)
219
+ return;
220
+ this.#ws = undefined;
221
+ rejectAllPending(this.#pending);
182
222
  lifecycleHandle.notifyClosed(new Error('NapCat reverse WebSocket closed'));
183
223
  });
184
224
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-napcat",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Zhin.js NapCat adapter for Plugin Runtime (WebSocket client, OneBot11 + NapCat extensions)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -45,16 +45,16 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "ws": "^8.21.1",
48
- "@zhin.js/adapter": "1.1.15",
49
- "@zhin.js/agent-feature": "1.1.2",
50
- "@zhin.js/core": "1.1.39",
51
- "@zhin.js/feature-kit": "1.1.2",
52
- "@zhin.js/host-http": "1.1.2",
53
- "@zhin.js/im-contract": "1.1.2",
48
+ "@zhin.js/adapter": "1.1.17",
49
+ "@zhin.js/agent-feature": "1.1.4",
50
+ "@zhin.js/core": "1.1.41",
51
+ "@zhin.js/feature-kit": "1.1.3",
52
+ "@zhin.js/host-http": "1.1.4",
53
+ "@zhin.js/im-contract": "1.1.3",
54
54
  "@zhin.js/logger": "1.1.1",
55
- "@zhin.js/permission": "1.1.3",
56
- "@zhin.js/skill": "1.1.2",
57
- "@zhin.js/tool": "1.1.2"
55
+ "@zhin.js/permission": "1.1.4",
56
+ "@zhin.js/skill": "1.1.4",
57
+ "@zhin.js/tool": "1.1.4"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^26.1.2",
@@ -62,15 +62,15 @@
62
62
  "typescript": "^6.0.3",
63
63
  "vitest": "^4.1.10",
64
64
  "zod": "^4.4.3",
65
- "@zhin.js/host-http": "1.1.2",
66
- "zhin.js": "1.1.4"
65
+ "@zhin.js/host-http": "1.1.4",
66
+ "zhin.js": "1.1.6"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "zod": "^4.0.0",
70
- "@zhin.js/adapter": "^1.1.15",
71
- "@zhin.js/command": "^1.1.3",
72
- "@zhin.js/core": "^1.1.39",
73
- "zhin.js": "^1.1.4"
70
+ "@zhin.js/adapter": "^1.1.17",
71
+ "@zhin.js/command": "^1.1.5",
72
+ "@zhin.js/core": "^1.1.41",
73
+ "zhin.js": "^1.1.6"
74
74
  },
75
75
  "peerDependenciesMeta": {
76
76
  "@zhin.js/command": {
@@ -1,13 +1,15 @@
1
- import { Endpoint } from 'zhin.js/adapter';
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
2
  /**
3
3
  * NapCat HTTP endpoint — POST inbound events + HTTP API outbound.
4
4
  */
5
5
  import type { IncomingMessage, ServerResponse } from 'node:http';
6
6
  import {
7
+ Endpoint,
7
8
  createRecallEndpointControl,
8
9
  type EndpointControl,
9
10
  type EndpointManagement,
10
11
  type EndpointSendRequest,
12
+ type EndpointTransportState,
11
13
  } from 'zhin.js/adapter';
12
14
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
13
15
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
@@ -59,6 +61,7 @@ export class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
59
61
  #routeReleases: HttpRouteRegistration[] = [];
60
62
  #open = false;
61
63
  #started = false;
64
+ #stopped = false;
62
65
 
63
66
  constructor(options: NapCatHttpEndpointOptions) {
64
67
  super();
@@ -67,8 +70,13 @@ export class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
67
70
  this.#callHttpAction = options.callHttpAction ?? callNapCatHttpAction;
68
71
  }
69
72
 
73
+ get transportState(): EndpointTransportState {
74
+ return this.#stopped ? 'stopped' : this.#started ? 'open' : 'idle';
75
+ }
76
+
70
77
  async start(): Promise<void> {
71
78
  if (this.#started) return;
79
+ this.#stopped = false;
72
80
  this.#started = true;
73
81
  this.#setupRoutes();
74
82
  this.#logger.info(formatCompact({
@@ -88,6 +96,7 @@ export class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
88
96
  }
89
97
 
90
98
  async stop(): Promise<void> {
99
+ this.#stopped = true;
91
100
  this.#open = false;
92
101
  for (const release of this.#routeReleases.splice(0)) release();
93
102
  this.#inboundDeduper.clear();
@@ -107,7 +116,11 @@ export class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
107
116
  params,
108
117
  );
109
118
  const data = resp.data as { message_id?: number | string } | undefined;
110
- const messageId = data?.message_id != null ? String(data.message_id) : '';
119
+ const rawId = data?.message_id;
120
+ if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
121
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
122
+ }
123
+ const messageId = String(rawId);
111
124
  this.#logger.debug(formatCompact({
112
125
  op: 'napcat_send',
113
126
  endpoint: this.#options.config.id,
package/src/protocol.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * No legacy Adapter/Endpoint / segment-mapper.
4
4
  * Canonicalization is owned by gateway/core before endpoint.send.
5
5
  */
6
- import { isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
6
+ import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
7
7
  import { formatCompact, getLogger } from '@zhin.js/logger';
8
8
 
9
9
  const logger = getLogger('napcat');
@@ -318,6 +318,11 @@ function oneBotMediaSegment(
318
318
  function canonicalToOneBotSegment(segment: NapCatWireSegment): MessageSegment | null {
319
319
  const data = segment.data ?? {};
320
320
  switch (segment.type) {
321
+ case 'markdown':
322
+ case 'share':
323
+ case 'keyboard':
324
+ throw new EndpointDeliveryError('unsupported_operation', `NapCat direct ${segment.type} sending is not supported`, 'not_sent');
325
+
321
326
  case 'mention': {
322
327
  const target = data.target ?? data.qq ?? data.id;
323
328
  if (target == null) return { type: segment.type, data };
@@ -443,13 +448,14 @@ export async function callNapCatHttpAction(
443
448
  headers.Authorization = `Bearer ${options.access_token}`;
444
449
  }
445
450
  const res = await fetch(url, {
451
+ signal: AbortSignal.timeout(30_000),
446
452
  method: 'POST',
447
453
  headers,
448
454
  body: JSON.stringify(params),
449
455
  });
450
456
  const text = await res.text();
451
- if (res.status === 401) throw new Error(`NapCat HTTP auth failed: ${text}`);
452
- if (res.status !== 200) throw new Error(`NapCat HTTP ${res.status}: ${text}`);
457
+ if (res.status === 401) throw new EndpointDeliveryError('platform_rejected', `NapCat HTTP auth failed: ${text}`, 'rejected');
458
+ if (res.status !== 200) throw new EndpointDeliveryError(res.status === 429 || res.status === 403 ? 'platform_rejected' : 'delivery_unconfirmed', `NapCat HTTP ${res.status}: ${text}`, res.status === 429 || res.status === 403 ? 'rejected' : 'unknown');
453
459
  let data: NapCatActionResponse;
454
460
  try {
455
461
  data = JSON.parse(text) as NapCatActionResponse;
@@ -457,8 +463,8 @@ export async function callNapCatHttpAction(
457
463
  throw new Error(`NapCat HTTP invalid response: ${text.slice(0, 200)}`);
458
464
  }
459
465
  if (data.status !== 'ok' && data.retcode !== 0) {
460
- throw new Error(
461
- `NapCat HTTP action failed [${data.retcode}]: ${data.message || data.wording || 'unknown'}`,
466
+ throw new EndpointDeliveryError(
467
+ 'platform_rejected', `NapCat HTTP action failed [${data.retcode}]: ${data.message || data.wording || 'unknown'}`, 'rejected',
462
468
  );
463
469
  }
464
470
  return data;
@@ -1,9 +1,10 @@
1
- import { Endpoint } from 'zhin.js/adapter';
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
2
  /**
3
3
  * NapCat WS client endpoint — outbound connect to NapCat.
4
4
  */
5
5
  import WebSocket from 'ws';
6
6
  import {
7
+ Endpoint,
7
8
  createRecallEndpointControl,
8
9
  createEndpointLifecycle,
9
10
  type EndpointConnectHandle,
@@ -11,6 +12,7 @@ import {
11
12
  type EndpointLifecycle,
12
13
  type EndpointManagement,
13
14
  type EndpointSendRequest,
15
+ type EndpointTransportState,
14
16
  } from 'zhin.js/adapter';
15
17
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
16
18
  import type { CapabilityId } from 'zhin.js';
@@ -78,6 +80,7 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
78
80
  this.#options = options;
79
81
  this.#lifecycle = createEndpointLifecycle({
80
82
  name: options.config.id,
83
+ heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
81
84
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
82
85
  reconnect: {
83
86
  initialIntervalMs: options.config.reconnect_interval,
@@ -88,6 +91,10 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
88
91
  });
89
92
  }
90
93
 
94
+ get transportState(): EndpointTransportState {
95
+ return this.#lifecycle.state;
96
+ }
97
+
91
98
  async start(): Promise<void> {
92
99
  if (this.#lifecycle.started) return;
93
100
  await this.#lifecycle.start((handle) => this.#connect(handle));
@@ -120,7 +127,11 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
120
127
  const message = formatOutboundSegments(payload);
121
128
  const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
122
129
  const data = await this.client.callApi(action, params) as { message_id?: number | string } | undefined;
123
- const messageId = data?.message_id != null ? String(data.message_id) : '';
130
+ const rawId = data?.message_id;
131
+ if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
132
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
133
+ }
134
+ const messageId = String(rawId);
124
135
  this.#logger.debug(formatCompact({
125
136
  op: 'napcat_send',
126
137
  endpoint: this.#options.config.id,
@@ -136,7 +147,7 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
136
147
  }
137
148
 
138
149
  #callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
139
- return callNapCatWsAction(this.#ws, this.#pending, this.#requestId, action, params);
150
+ return callNapCatWsAction(this.#lifecycle.state === 'open' ? this.#ws : undefined, this.#pending, this.#requestId, action, params);
140
151
  }
141
152
 
142
153
  /** Test / internal: admit a parsed event when the endpoint is open. */
@@ -208,6 +219,9 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
208
219
  const ws = create(url, { headers });
209
220
  this.#ws = ws;
210
221
  handle.onForceClose(() => {
222
+ if (this.#ws === ws) {
223
+ rejectAllPending(this.#pending);
224
+ }
211
225
  try {
212
226
  ws.close();
213
227
  } catch {
@@ -216,7 +230,7 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
216
230
  });
217
231
 
218
232
  ws.on('open', () => {
219
- if (settled) return;
233
+ if (settled || this.#ws !== ws || !this.#lifecycle.started) return;
220
234
  settled = true;
221
235
  if (!this.#options.config.access_token) {
222
236
  this.#logger.warn(formatCompact({
@@ -243,7 +257,12 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
243
257
  resolve();
244
258
  });
245
259
 
260
+ ws.on('pong', () => {
261
+ if (this.#ws === ws) this.#lifecycle.notifyHeartbeatAck();
262
+ });
246
263
  ws.on('message', (data) => {
264
+ if (this.#ws !== ws || ws.readyState !== 1 || !this.#lifecycle.started) return;
265
+ this.#lifecycle.notifyHeartbeatAck();
247
266
  handleNapCatWsMessage(data, {
248
267
  endpointId: this.#options.config.id,
249
268
  pending: this.#pending,
@@ -252,6 +271,9 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
252
271
  });
253
272
 
254
273
  ws.on('close', (code, reason) => {
274
+ if (this.#ws !== ws) return;
275
+ this.#ws = undefined;
276
+ rejectAllPending(this.#pending);
255
277
  const reasonStr = typeof reason === 'string'
256
278
  ? reason
257
279
  : Buffer.isBuffer(reason)
@@ -278,6 +300,7 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
278
300
  });
279
301
 
280
302
  ws.on('error', (err) => {
303
+ if (this.#ws !== ws) return;
281
304
  const error = err instanceof Error ? err : new Error(String(err));
282
305
  this.#logger.warn(formatCompact({
283
306
  op: 'ws_error',
@@ -1,3 +1,4 @@
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
1
2
  import { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';
2
3
  import { formatCompact, getLogger } from '@zhin.js/logger';
3
4
  import {
@@ -39,8 +40,8 @@ export function handleNapCatWsMessage(
39
40
  clearTimeout(pending.timeout);
40
41
  if (resp.status === 'ok') pending.resolve(resp.data);
41
42
  else {
42
- pending.reject(new Error(
43
- `API error [${resp.retcode}]: ${resp.message || resp.wording || 'unknown'}`,
43
+ pending.reject(new EndpointDeliveryError(
44
+ 'platform_rejected', `API error [${resp.retcode}]: ${resp.message || resp.wording || 'unknown'}`, 'rejected',
44
45
  ));
45
46
  }
46
47
  }
@@ -64,17 +65,23 @@ export function callNapCatWsAction(
64
65
  params: Record<string, unknown>,
65
66
  ): Promise<unknown> {
66
67
  if (!ws || ws.readyState !== WS_OPEN) {
67
- return Promise.reject(new Error('WebSocket is not connected'));
68
+ return Promise.reject(new EndpointDeliveryError('endpoint_disconnected', 'WebSocket is not connected', 'not_sent'));
68
69
  }
69
70
  const echo = `req_${++requestId.value}`;
70
71
  const req: NapCatActionRequest = { action, params, echo };
71
72
  return new Promise((resolve, reject) => {
72
73
  const timeout = setTimeout(() => {
73
74
  pending.delete(echo);
74
- reject(new Error(`API call timeout: ${action}`));
75
+ reject(new EndpointDeliveryError('endpoint_timeout', `API call timeout: ${action}`, 'unknown'));
75
76
  }, 30_000);
76
77
  pending.set(echo, { resolve, reject, timeout });
77
- ws.send(JSON.stringify(req));
78
+ try {
79
+ ws.send(JSON.stringify(req));
80
+ } catch (error) {
81
+ pending.delete(echo);
82
+ clearTimeout(timeout);
83
+ reject(error instanceof Error ? error : new Error(String(error)));
84
+ }
78
85
  });
79
86
  }
80
87
 
@@ -99,7 +106,7 @@ export function rejectAllPending(
99
106
  ): void {
100
107
  for (const [, entry] of pending) {
101
108
  clearTimeout(entry.timeout);
102
- entry.reject(new Error(message));
109
+ entry.reject(new EndpointDeliveryError('endpoint_disconnected', message, 'unknown'));
103
110
  }
104
111
  pending.clear();
105
112
  }
package/src/ws-types.ts CHANGED
@@ -4,7 +4,7 @@ export interface NapCatWsSocket {
4
4
  send(data: string): void;
5
5
  close(code?: number, reason?: string): void;
6
6
  ping?(): void;
7
- on(event: 'open' | 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
7
+ on(event: 'open' | 'message' | 'close' | 'error' | 'pong', listener: (...args: unknown[]) => void): void;
8
8
  }
9
9
 
10
10
  export interface NapCatWsCreateOptions {
@@ -1,14 +1,16 @@
1
- import { Endpoint } from 'zhin.js/adapter';
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
2
  /**
3
3
  * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
4
4
  */
5
5
  import {
6
+ Endpoint,
6
7
  createEndpointLifecycle,
7
8
  createRecallEndpointControl,
8
9
  type EndpointLifecycle,
9
10
  type EndpointControl,
10
11
  type EndpointManagement,
11
12
  type EndpointSendRequest,
13
+ type EndpointTransportState,
12
14
  } from 'zhin.js/adapter';
13
15
  import type { HttpHost, WsConnection } from '@zhin.js/host-http';
14
16
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
@@ -70,6 +72,7 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
70
72
  #pending = new Map<string, NapCatPendingAction>();
71
73
  #open = false;
72
74
  #started = false;
75
+ #stopped = false;
73
76
 
74
77
  constructor(options: NapCatWssEndpointOptions) {
75
78
  super();
@@ -78,11 +81,21 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
78
81
  this.#connectionLifecycle = createEndpointLifecycle({
79
82
  name: `${options.config.id}:inbound`,
80
83
  reconnect: false,
84
+ heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
81
85
  });
82
86
  }
83
87
 
88
+ get transportState(): EndpointTransportState {
89
+ if (this.#stopped) return 'stopped';
90
+ if (!this.#started) return 'idle';
91
+ if (this.#connectionLifecycle.state === 'closed') return 'closed';
92
+ if (this.#ws?.readyState === 1) return 'open';
93
+ return this.#connectionLifecycle.state === 'connecting' ? 'connecting' : 'idle';
94
+ }
95
+
84
96
  async start(): Promise<void> {
85
97
  if (this.#started) return;
98
+ this.#stopped = false;
86
99
  this.#started = true;
87
100
  const handle = this.#options.http.ws(this.#options.config.path);
88
101
  this.#wsRelease = handle.onConnection((connection) => {
@@ -113,6 +126,7 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
113
126
  }
114
127
 
115
128
  async stop(): Promise<void> {
129
+ this.#stopped = true;
116
130
  this.#open = false;
117
131
  this.#wsRelease?.();
118
132
  this.#wsRelease = undefined;
@@ -128,7 +142,11 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
128
142
  const message = formatOutboundSegments(payload);
129
143
  const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
130
144
  const data = await this.client.callApi(action, params) as { message_id?: number | string } | undefined;
131
- return data?.message_id != null ? String(data.message_id) : '';
145
+ const rawId = data?.message_id;
146
+ if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
147
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
148
+ }
149
+ return String(rawId);
132
150
  }
133
151
 
134
152
  async recallMessage(messageId: string): Promise<void> {
@@ -137,7 +155,7 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
137
155
  }
138
156
 
139
157
  #callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
140
- return callNapCatWsAction(this.#ws, this.#pending, this.#requestId, action, params);
158
+ return callNapCatWsAction(this.#connectionLifecycle.state === 'closed' ? undefined : this.#ws, this.#pending, this.#requestId, action, params);
141
159
  }
142
160
 
143
161
  admit(ev: NapCatEvent): void {
@@ -197,15 +215,23 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
197
215
  }
198
216
 
199
217
  async #acceptConnection(connection: WsConnection): Promise<void> {
218
+ if (!this.#started || this.#stopped) {
219
+ connection.socket.close();
220
+ return;
221
+ }
200
222
  if (!verifyNapCatAccessToken(this.#options.config.access_token, connection.request)) {
201
223
  connection.socket.close(4003, 'Unauthorized');
202
224
  return;
203
225
  }
204
226
  const socket = connection.socket as unknown as NapCatWsSocket;
205
227
  await this.#connectionLifecycle.stop();
228
+ rejectAllPending(this.#pending, 'Connection replaced');
206
229
  await this.#connectionLifecycle.start(async (lifecycleHandle) => {
207
230
  this.#ws = socket;
208
231
  lifecycleHandle.onForceClose(() => {
232
+ if (this.#ws === socket) {
233
+ rejectAllPending(this.#pending);
234
+ }
209
235
  try {
210
236
  socket.close();
211
237
  } catch {
@@ -219,7 +245,11 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
219
245
  /* ignore */
220
246
  }
221
247
  }, this.#options.config.heartbeat_interval);
248
+ socket.on('pong', () => {
249
+ if (this.#ws === socket) this.#connectionLifecycle.notifyHeartbeatAck();
250
+ });
222
251
  socket.on('message', (data) => {
252
+ if (this.#ws !== socket || this.#connectionLifecycle.state === 'closed') return;
223
253
  this.#connectionLifecycle.notifyHeartbeatAck();
224
254
  handleNapCatWsMessage(data, {
225
255
  endpointId: this.#options.config.id,
@@ -227,8 +257,14 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
227
257
  admit: (event) => this.admit(event),
228
258
  });
229
259
  });
260
+ socket.on('error', (error) => {
261
+ if (this.#ws !== socket) return;
262
+ this.#logger.warn(formatCompact({ op: 'ws_error', error: String(error) }));
263
+ });
230
264
  socket.on('close', () => {
231
- if (this.#ws === socket) this.#ws = undefined;
265
+ if (this.#ws !== socket) return;
266
+ this.#ws = undefined;
267
+ rejectAllPending(this.#pending);
232
268
  lifecycleHandle.notifyClosed(new Error('NapCat reverse WebSocket closed'));
233
269
  });
234
270
  });