@zhin.js/adapter-onebot11 1.1.3 → 1.1.5

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,42 @@
1
1
  # @zhin.js/adapter-onebot11
2
2
 
3
+ ## 1.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
9
+ 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.
10
+
11
+ - Updated dependencies [b9b3953]
12
+ - Updated dependencies [b9b3953]
13
+ - Updated dependencies [b9b3953]
14
+ - Updated dependencies [b9b3953]
15
+ - Updated dependencies [b9b3953]
16
+ - Updated dependencies [b9b3953]
17
+ - Updated dependencies [b9b3953]
18
+ - Updated dependencies [b9b3953]
19
+ - Updated dependencies [b9b3953]
20
+ - Updated dependencies [b9b3953]
21
+ - Updated dependencies [b9b3953]
22
+ - @zhin.js/adapter@1.1.16
23
+ - @zhin.js/im-contract@1.1.3
24
+ - @zhin.js/core@1.1.40
25
+ - @zhin.js/host-http@1.1.3
26
+ - @zhin.js/tool@1.1.3
27
+ - @zhin.js/skill@1.1.3
28
+ - @zhin.js/agent-feature@1.1.3
29
+
30
+ ## 1.1.4
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [43eda99]
35
+ - Updated dependencies [0de4836]
36
+ - @zhin.js/core@1.1.39
37
+ - @zhin.js/permission@1.1.3
38
+ - @zhin.js/adapter@1.1.15
39
+
3
40
  ## 1.1.3
4
41
 
5
42
  ### Patch Changes
package/README.md CHANGED
@@ -103,3 +103,7 @@ plugins:
103
103
  ## 许可证
104
104
 
105
105
  MIT License
106
+
107
+ ### 高阶消息支持边界
108
+
109
+ OneBot11 标准支持链接分享:canonical share 的 url/title/description/image 分别映射标准 url/title/content/image,保留标题与描述。具体桥是否实现需实机确认;NapCat 的 share 发送限制不能以标准支持推断通过。标准没有 Markdown 与 keyboard 段,直接出站明确 `unsupported_operation / not_sent`;文字交互降级不能算原生按钮支持。参考:[OneBot11 消息段规范](https://github.com/botuniverse/onebot-11/blob/master/message/segment.md)。
package/lib/protocol.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  * Spec: https://github.com/botuniverse/onebot-11
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('onebot11');
9
9
  function normalizeConnection(connection) {
@@ -224,6 +224,20 @@ function oneBotMediaSegment(type, data) {
224
224
  function canonicalToOneBotSegment(segment) {
225
225
  const data = segment.data ?? {};
226
226
  switch (segment.type) {
227
+ case 'markdown':
228
+ case 'keyboard':
229
+ throw new EndpointDeliveryError('unsupported_operation', `OneBot11 standard has no ${segment.type} message segment`, 'not_sent');
230
+ case 'share': {
231
+ if (['audio', 'artist', 'duration', 'config'].some(key => data[key] !== undefined)) {
232
+ throw new EndpointDeliveryError('unsupported_operation', 'onebot11 rich share metadata is not implemented', 'not_sent');
233
+ }
234
+ if (typeof data.url !== 'string' || !/^https?:\/\//.test(data.url) || typeof data.title !== 'string' || !data.title) {
235
+ throw new EndpointDeliveryError('unsupported_operation', 'Invalid OneBot11 share URL or title', 'not_sent');
236
+ }
237
+ return { type: 'share', data: { url: data.url, title: data.title,
238
+ ...(typeof (data.description ?? data.content) === 'string' ? { content: data.description ?? data.content } : {}),
239
+ ...(typeof data.image === 'string' ? { image: data.image } : {}) } };
240
+ }
227
241
  case 'mention': {
228
242
  const target = data.target ?? data.qq ?? data.id;
229
243
  if (target == null)
@@ -1,4 +1,4 @@
1
- import { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
1
+ import { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest, type EndpointTransportState } from 'zhin.js/adapter';
2
2
  import type { CapabilityId } from 'zhin.js';
3
3
  import { type OneBot11WsConfig } from './protocol.js';
4
4
  import { type OneBot11WsCreateOptions, type OneBot11WsSocket } from './ws-types.js';
@@ -15,6 +15,7 @@ export declare class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
15
15
  readonly control: EndpointControl;
16
16
  readonly content: import("@zhin.js/adapter").EndpointContentPort;
17
17
  constructor(options: OneBot11WsEndpointOptions);
18
+ get transportState(): EndpointTransportState;
18
19
  start(): Promise<void>;
19
20
  stop(): Promise<void>;
20
21
  send({ conversation, payload }: EndpointSendRequest): Promise<string>;
@@ -1,3 +1,4 @@
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
1
2
  /**
2
3
  * OneBot11 WS client endpoint — outbound connect to OneBot implementation.
3
4
  */
@@ -43,9 +44,12 @@ export class OneBot11WsEndpoint extends ClientEndpoint {
43
44
  maxIntervalMs: config.reconnect_interval,
44
45
  jitterMs: 0,
45
46
  },
46
- heartbeat: { intervalMs: config.heartbeat_interval },
47
+ heartbeat: { intervalMs: config.heartbeat_interval, watchdogMisses: 2 },
47
48
  });
48
49
  }
50
+ get transportState() {
51
+ return this.#lifecycle.state;
52
+ }
49
53
  async start() {
50
54
  if (this.#lifecycle.started)
51
55
  return;
@@ -77,7 +81,11 @@ export class OneBot11WsEndpoint extends ClientEndpoint {
77
81
  const message = formatOutboundSegments(payload);
78
82
  const { action, params } = buildSendAction(conversation, message);
79
83
  const data = await callOnebot11Client(this.client, action, params);
80
- const messageId = data?.message_id != null ? String(data.message_id) : '';
84
+ const rawId = data?.message_id;
85
+ if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
86
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
87
+ }
88
+ const messageId = String(rawId);
81
89
  this.#logger.debug(formatCompact({
82
90
  op: 'onebot11_send',
83
91
  endpoint: this.#options.config.id,
@@ -92,7 +100,7 @@ export class OneBot11WsEndpoint extends ClientEndpoint {
92
100
  await callOnebot11Client(this.client, 'delete_msg', { message_id: Number(messageId) });
93
101
  }
94
102
  #callApi(action, params = {}) {
95
- return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
103
+ return callOneBot11WsAction(this.#lifecycle.state === 'open' ? this.#ws : undefined, this.#pending, this.#requestId, action, params);
96
104
  }
97
105
  #admitRaw(ev) {
98
106
  if (!isMessageEvent(ev)) {
@@ -138,6 +146,9 @@ export class OneBot11WsEndpoint extends ClientEndpoint {
138
146
  const ws = create(url, { headers });
139
147
  this.#ws = ws;
140
148
  handle.onForceClose(() => {
149
+ if (this.#ws === ws) {
150
+ rejectAllPending(this.#pending);
151
+ }
141
152
  try {
142
153
  ws.close();
143
154
  }
@@ -146,7 +157,7 @@ export class OneBot11WsEndpoint extends ClientEndpoint {
146
157
  }
147
158
  });
148
159
  ws.on('open', () => {
149
- if (settled)
160
+ if (settled || this.#ws !== ws || !this.#lifecycle.started)
150
161
  return;
151
162
  settled = true;
152
163
  if (!this.#options.config.access_token) {
@@ -164,7 +175,14 @@ export class OneBot11WsEndpoint extends ClientEndpoint {
164
175
  this.#lifecycle.startHeartbeat(() => this.#ws?.ping?.());
165
176
  resolve();
166
177
  });
178
+ ws.on('pong', () => {
179
+ if (this.#ws === ws)
180
+ this.#lifecycle.notifyHeartbeatAck();
181
+ });
167
182
  ws.on('message', (data) => {
183
+ if (this.#ws !== ws || ws.readyState !== 1 || !this.#lifecycle.started)
184
+ return;
185
+ this.#lifecycle.notifyHeartbeatAck();
168
186
  handleOneBot11WsMessage(data, {
169
187
  endpointId: this.#options.config.id,
170
188
  pending: this.#pending,
@@ -172,6 +190,10 @@ export class OneBot11WsEndpoint extends ClientEndpoint {
172
190
  });
173
191
  });
174
192
  ws.on('close', (code, reason) => {
193
+ if (this.#ws !== ws)
194
+ return;
195
+ this.#ws = undefined;
196
+ rejectAllPending(this.#pending);
175
197
  const reasonStr = typeof reason === 'string'
176
198
  ? reason
177
199
  : Buffer.isBuffer(reason)
@@ -192,6 +214,8 @@ export class OneBot11WsEndpoint extends ClientEndpoint {
192
214
  handle.notifyClosed(`OneBot11 WS 关闭: ${codeNum} ${reasonStr || 'closed'}${codeHint}`);
193
215
  });
194
216
  ws.on('error', (err) => {
217
+ if (this.#ws !== ws)
218
+ return;
195
219
  const error = err instanceof Error ? err : new Error(String(err));
196
220
  this.#logger.warn(formatCompact({
197
221
  op: 'ws_error',
@@ -1,3 +1,4 @@
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
1
2
  import { clearTimeout, setTimeout } from 'node:timers';
2
3
  import { formatCompact, getLogger } from '@zhin.js/logger';
3
4
  import { WS_OPEN, } from './ws-types.js';
@@ -37,23 +38,30 @@ export function handleOneBot11WsMessage(data, options) {
37
38
  }
38
39
  export function callOneBot11WsAction(ws, pending, requestId, action, params) {
39
40
  if (!ws || ws.readyState !== WS_OPEN) {
40
- return Promise.reject(new Error('WebSocket 未连接'));
41
+ return Promise.reject(new EndpointDeliveryError('endpoint_disconnected', 'WebSocket 未连接', 'not_sent'));
41
42
  }
42
43
  const echo = `ob11_${++requestId.value}`;
43
44
  const req = { action, params, echo };
44
45
  return new Promise((resolve, reject) => {
45
46
  const timeout = setTimeout(() => {
46
47
  pending.delete(echo);
47
- reject(new Error(`OneBot11 动作超时: ${action}`));
48
+ reject(new EndpointDeliveryError('endpoint_timeout', `OneBot11 动作超时: ${action}`, 'unknown'));
48
49
  }, 30_000);
49
50
  pending.set(echo, { resolve, reject, timeout });
50
- ws.send(JSON.stringify(req));
51
+ try {
52
+ ws.send(JSON.stringify(req));
53
+ }
54
+ catch (error) {
55
+ pending.delete(echo);
56
+ clearTimeout(timeout);
57
+ reject(error instanceof Error ? error : new Error(String(error)));
58
+ }
51
59
  });
52
60
  }
53
61
  export function rejectAllPending(pending, message = '连接已关闭') {
54
62
  for (const [, entry] of pending) {
55
63
  clearTimeout(entry.timeout);
56
- entry.reject(new Error(message));
64
+ entry.reject(new EndpointDeliveryError('endpoint_disconnected', message, 'unknown'));
57
65
  }
58
66
  pending.clear();
59
67
  }
package/lib/ws-types.d.ts CHANGED
@@ -4,7 +4,7 @@ export interface OneBot11WsSocket {
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 OneBot11WsCreateOptions {
10
10
  readonly headers?: Record<string, string>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * OneBot11 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
3
3
  */
4
- import { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
4
+ import { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest, type EndpointTransportState } from 'zhin.js/adapter';
5
5
  import type { HttpHost } from '@zhin.js/host-http';
6
6
  import type { CapabilityId } from 'zhin.js';
7
7
  import { type OneBot11WssConfig } from './protocol.js';
@@ -18,6 +18,7 @@ export declare class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client>
18
18
  readonly control: EndpointControl;
19
19
  readonly content: import("@zhin.js/adapter").EndpointContentPort;
20
20
  constructor(options: OneBot11WssEndpointOptions);
21
+ get transportState(): EndpointTransportState;
21
22
  start(): Promise<void>;
22
23
  stop(): Promise<void>;
23
24
  send({ conversation, payload }: EndpointSendRequest): Promise<string>;
@@ -1,3 +1,4 @@
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
1
2
  /**
2
3
  * OneBot11 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
3
4
  */
@@ -24,6 +25,7 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
24
25
  #requestId = { value: 0 };
25
26
  #pending = new Map();
26
27
  #started = false;
28
+ #stopped = false;
27
29
  constructor(options) {
28
30
  super();
29
31
  this.#logger = getAdapterLogger('onebot11', options.config.id);
@@ -31,6 +33,7 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
31
33
  this.#connectionLifecycle = createEndpointLifecycle({
32
34
  name: `${options.config.id}:inbound`,
33
35
  reconnect: false,
36
+ heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
34
37
  });
35
38
  this.client = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
36
39
  const callApi = (action, params) => callOnebot11Client(this.client, action, params);
@@ -41,9 +44,21 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
41
44
  this.#admitRaw(payload);
42
45
  }, (_name, error) => this.#warnPlatformEvent(error));
43
46
  }
47
+ get transportState() {
48
+ if (this.#stopped)
49
+ return 'stopped';
50
+ if (!this.#started)
51
+ return 'idle';
52
+ if (this.#connectionLifecycle.state === 'closed')
53
+ return 'closed';
54
+ if (this.#ws?.readyState === 1)
55
+ return 'open';
56
+ return this.#connectionLifecycle.state === 'connecting' ? 'connecting' : 'idle';
57
+ }
44
58
  async start() {
45
59
  if (this.#started)
46
60
  return;
61
+ this.#stopped = false;
47
62
  if (!this.#options.config.access_token) {
48
63
  // wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
49
64
  this.#logger.warn(formatCompact({
@@ -74,6 +89,7 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
74
89
  }));
75
90
  }
76
91
  async stop() {
92
+ this.#stopped = true;
77
93
  this.close();
78
94
  this.#wsRelease?.();
79
95
  this.#wsRelease = undefined;
@@ -87,7 +103,11 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
87
103
  const message = formatOutboundSegments(payload);
88
104
  const { action, params } = buildSendAction(conversation, message);
89
105
  const data = await callOnebot11Client(this.client, action, params);
90
- return data?.message_id != null ? String(data.message_id) : '';
106
+ const rawId = data?.message_id;
107
+ if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
108
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
109
+ }
110
+ return String(rawId);
91
111
  }
92
112
  async recallMessage(messageId) {
93
113
  if (!messageId)
@@ -95,7 +115,7 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
95
115
  await callOnebot11Client(this.client, 'delete_msg', { message_id: Number(messageId) });
96
116
  }
97
117
  #callApi(action, params = {}) {
98
- return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
118
+ return callOneBot11WsAction(this.#connectionLifecycle.state === 'closed' ? undefined : this.#ws, this.#pending, this.#requestId, action, params);
99
119
  }
100
120
  #admitRaw(ev) {
101
121
  if (!isMessageEvent(ev)) {
@@ -132,6 +152,10 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
132
152
  }));
133
153
  }
134
154
  async #acceptConnection(connection) {
155
+ if (!this.#started || this.#stopped) {
156
+ connection.socket.close();
157
+ return;
158
+ }
135
159
  if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
136
160
  connection.socket.close(4003, 'Unauthorized');
137
161
  return;
@@ -142,6 +166,9 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
142
166
  await this.#connectionLifecycle.start(async (lifecycleHandle) => {
143
167
  this.#ws = socket;
144
168
  lifecycleHandle.onForceClose(() => {
169
+ if (this.#ws === socket) {
170
+ rejectAllPending(this.#pending);
171
+ }
145
172
  try {
146
173
  socket.close();
147
174
  }
@@ -159,8 +186,12 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
159
186
  /* ignore */
160
187
  }
161
188
  }, this.#options.config.heartbeat_interval);
189
+ socket.on('pong', () => {
190
+ if (this.#ws === socket)
191
+ this.#connectionLifecycle.notifyHeartbeatAck();
192
+ });
162
193
  socket.on('message', (data) => {
163
- if (this.#ws !== socket)
194
+ if (this.#ws !== socket || this.#connectionLifecycle.state === 'closed')
164
195
  return;
165
196
  this.#connectionLifecycle.notifyHeartbeatAck();
166
197
  handleOneBot11WsMessage(data, {
@@ -169,7 +200,14 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
169
200
  ingest: (ev) => this.client.ingest(ev),
170
201
  });
171
202
  });
203
+ socket.on('error', (error) => {
204
+ if (this.#ws !== socket)
205
+ return;
206
+ this.#logger.warn(formatCompact({ op: 'ws_error', error: String(error) }));
207
+ });
172
208
  socket.on('close', () => {
209
+ if (this.#ws !== socket)
210
+ return;
173
211
  if (this.#ws === socket) {
174
212
  this.#ws = undefined;
175
213
  rejectAllPending(this.#pending);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-onebot11",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Zhin.js OneBot 11 adapter for Plugin Runtime (WebSocket client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -45,16 +45,16 @@
45
45
  "@imhelper/onebot-v11": "1.0.7",
46
46
  "imhelper": "1.0.7",
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.38",
48
+ "@zhin.js/adapter": "1.1.16",
49
+ "@zhin.js/agent-feature": "1.1.3",
50
+ "@zhin.js/core": "1.1.40",
51
51
  "@zhin.js/feature-kit": "1.1.2",
52
- "@zhin.js/host-http": "1.1.2",
53
- "@zhin.js/im-contract": "1.1.2",
52
+ "@zhin.js/host-http": "1.1.3",
53
+ "@zhin.js/im-contract": "1.1.3",
54
54
  "@zhin.js/logger": "1.1.1",
55
- "@zhin.js/permission": "1.1.2",
56
- "@zhin.js/skill": "1.1.2",
57
- "@zhin.js/tool": "1.1.2"
55
+ "@zhin.js/permission": "1.1.3",
56
+ "@zhin.js/skill": "1.1.3",
57
+ "@zhin.js/tool": "1.1.3"
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.3"
65
+ "@zhin.js/host-http": "1.1.3",
66
+ "zhin.js": "1.1.5"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "zod": "^4.0.0",
70
- "@zhin.js/adapter": "^1.1.15",
71
- "@zhin.js/command": "^1.1.2",
72
- "@zhin.js/core": "^1.1.38",
73
- "zhin.js": "^1.1.3"
70
+ "@zhin.js/adapter": "^1.1.16",
71
+ "@zhin.js/command": "^1.1.4",
72
+ "@zhin.js/core": "^1.1.40",
73
+ "zhin.js": "^1.1.5"
74
74
  },
75
75
  "peerDependenciesMeta": {
76
76
  "@zhin.js/command": {
package/src/protocol.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  * Spec: https://github.com/botuniverse/onebot-11
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('onebot11');
@@ -340,6 +340,21 @@ function oneBotMediaSegment(
340
340
  function canonicalToOneBotSegment(segment: OneBot11WireSegment): OneBot11Segment | null {
341
341
  const data = segment.data ?? {};
342
342
  switch (segment.type) {
343
+ case 'markdown':
344
+ case 'keyboard':
345
+ throw new EndpointDeliveryError('unsupported_operation', `OneBot11 standard has no ${segment.type} message segment`, 'not_sent');
346
+ case 'share': {
347
+ if (['audio', 'artist', 'duration', 'config'].some(key => data[key] !== undefined)) {
348
+ throw new EndpointDeliveryError('unsupported_operation', 'onebot11 rich share metadata is not implemented', 'not_sent');
349
+ }
350
+ if (typeof data.url !== 'string' || !/^https?:\/\//.test(data.url) || typeof data.title !== 'string' || !data.title) {
351
+ throw new EndpointDeliveryError('unsupported_operation', 'Invalid OneBot11 share URL or title', 'not_sent');
352
+ }
353
+ return { type: 'share', data: { url: data.url, title: data.title,
354
+ ...(typeof (data.description ?? data.content) === 'string' ? { content: data.description ?? data.content } : {}),
355
+ ...(typeof data.image === 'string' ? { image: data.image } : {}) } };
356
+ }
357
+
343
358
  case 'mention': {
344
359
  const target = data.target ?? data.qq ?? data.id;
345
360
  if (target == null) return { type: segment.type, data };
@@ -1,3 +1,4 @@
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
1
2
  /**
2
3
  * OneBot11 WS client endpoint — outbound connect to OneBot implementation.
3
4
  */
@@ -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';
@@ -89,10 +91,14 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
89
91
  maxIntervalMs: config.reconnect_interval,
90
92
  jitterMs: 0,
91
93
  },
92
- heartbeat: { intervalMs: config.heartbeat_interval },
94
+ heartbeat: { intervalMs: config.heartbeat_interval, watchdogMisses: 2 },
93
95
  });
94
96
  }
95
97
 
98
+ get transportState(): EndpointTransportState {
99
+ return this.#lifecycle.state;
100
+ }
101
+
96
102
  async start(): Promise<void> {
97
103
  if (this.#lifecycle.started) return;
98
104
  try {
@@ -123,7 +129,11 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
123
129
  const message = formatOutboundSegments(payload);
124
130
  const { action, params } = buildSendAction(conversation, message);
125
131
  const data = await callOnebot11Client<{ message_id?: number | string }>(this.client, action, params);
126
- const messageId = data?.message_id != null ? String(data.message_id) : '';
132
+ const rawId = data?.message_id;
133
+ if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
134
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
135
+ }
136
+ const messageId = String(rawId);
127
137
  this.#logger.debug(formatCompact({
128
138
  op: 'onebot11_send',
129
139
  endpoint: this.#options.config.id,
@@ -142,7 +152,7 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
142
152
  action: string,
143
153
  params: Record<string, unknown> = {},
144
154
  ): ReturnType<typeof callOneBot11WsAction> {
145
- return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
155
+ return callOneBot11WsAction(this.#lifecycle.state === 'open' ? this.#ws : undefined, this.#pending, this.#requestId, action, params);
146
156
  }
147
157
 
148
158
  #admitRaw(ev: OneBot11Event): void {
@@ -199,6 +209,9 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
199
209
  const ws = create(url, { headers });
200
210
  this.#ws = ws;
201
211
  handle.onForceClose(() => {
212
+ if (this.#ws === ws) {
213
+ rejectAllPending(this.#pending);
214
+ }
202
215
  try {
203
216
  ws.close();
204
217
  } catch {
@@ -207,7 +220,7 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
207
220
  });
208
221
 
209
222
  ws.on('open', () => {
210
- if (settled) return;
223
+ if (settled || this.#ws !== ws || !this.#lifecycle.started) return;
211
224
  settled = true;
212
225
  if (!this.#options.config.access_token) {
213
226
  this.#logger.warn(formatCompact({
@@ -225,7 +238,12 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
225
238
  resolve();
226
239
  });
227
240
 
241
+ ws.on('pong', () => {
242
+ if (this.#ws === ws) this.#lifecycle.notifyHeartbeatAck();
243
+ });
228
244
  ws.on('message', (data) => {
245
+ if (this.#ws !== ws || ws.readyState !== 1 || !this.#lifecycle.started) return;
246
+ this.#lifecycle.notifyHeartbeatAck();
229
247
  handleOneBot11WsMessage(data, {
230
248
  endpointId: this.#options.config.id,
231
249
  pending: this.#pending,
@@ -234,6 +252,9 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
234
252
  });
235
253
 
236
254
  ws.on('close', (code, reason) => {
255
+ if (this.#ws !== ws) return;
256
+ this.#ws = undefined;
257
+ rejectAllPending(this.#pending);
237
258
  const reasonStr = typeof reason === 'string'
238
259
  ? reason
239
260
  : Buffer.isBuffer(reason)
@@ -255,6 +276,7 @@ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
255
276
  });
256
277
 
257
278
  ws.on('error', (err) => {
279
+ if (this.#ws !== ws) return;
258
280
  const error = err instanceof Error ? err : new Error(String(err));
259
281
  this.#logger.warn(formatCompact({
260
282
  op: 'ws_error',
@@ -1,3 +1,4 @@
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
1
2
  import { clearTimeout, setTimeout } from 'node:timers';
2
3
  import { formatCompact, getLogger } from '@zhin.js/logger';
3
4
  import {
@@ -59,17 +60,23 @@ export function callOneBot11WsAction(
59
60
  params: Record<string, unknown>,
60
61
  ): Promise<OneBot11ActionResponse> {
61
62
  if (!ws || ws.readyState !== WS_OPEN) {
62
- return Promise.reject(new Error('WebSocket 未连接'));
63
+ return Promise.reject(new EndpointDeliveryError('endpoint_disconnected', 'WebSocket 未连接', 'not_sent'));
63
64
  }
64
65
  const echo = `ob11_${++requestId.value}`;
65
66
  const req: OneBot11ActionRequest = { action, params, echo };
66
67
  return new Promise((resolve, reject) => {
67
68
  const timeout = setTimeout(() => {
68
69
  pending.delete(echo);
69
- reject(new Error(`OneBot11 动作超时: ${action}`));
70
+ reject(new EndpointDeliveryError('endpoint_timeout', `OneBot11 动作超时: ${action}`, 'unknown'));
70
71
  }, 30_000);
71
72
  pending.set(echo, { resolve, reject, timeout });
72
- ws.send(JSON.stringify(req));
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
+ }
73
80
  });
74
81
  }
75
82
 
@@ -79,7 +86,7 @@ export function rejectAllPending(
79
86
  ): void {
80
87
  for (const [, entry] of pending) {
81
88
  clearTimeout(entry.timeout);
82
- entry.reject(new Error(message));
89
+ entry.reject(new EndpointDeliveryError('endpoint_disconnected', message, 'unknown'));
83
90
  }
84
91
  pending.clear();
85
92
  }
package/src/ws-types.ts CHANGED
@@ -4,7 +4,7 @@ export interface OneBot11WsSocket {
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 OneBot11WsCreateOptions {
@@ -1,3 +1,4 @@
1
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
1
2
  /**
2
3
  * OneBot11 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
3
4
  */
@@ -9,6 +10,7 @@ import {
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';
@@ -62,6 +64,7 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
62
64
  #requestId = { value: 0 };
63
65
  #pending = new Map<string, OneBot11PendingAction>();
64
66
  #started = false;
67
+ #stopped = false;
65
68
 
66
69
  constructor(options: OneBot11WssEndpointOptions) {
67
70
  super();
@@ -70,6 +73,7 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
70
73
  this.#connectionLifecycle = createEndpointLifecycle({
71
74
  name: `${options.config.id}:inbound`,
72
75
  reconnect: false,
76
+ heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
73
77
  });
74
78
  this.client = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
75
79
  const callApi = (action: string, params?: Record<string, unknown>) => callOnebot11Client(this.client, action, params);
@@ -84,8 +88,17 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
84
88
  );
85
89
  }
86
90
 
91
+ get transportState(): EndpointTransportState {
92
+ if (this.#stopped) return 'stopped';
93
+ if (!this.#started) return 'idle';
94
+ if (this.#connectionLifecycle.state === 'closed') return 'closed';
95
+ if (this.#ws?.readyState === 1) return 'open';
96
+ return this.#connectionLifecycle.state === 'connecting' ? 'connecting' : 'idle';
97
+ }
98
+
87
99
  async start(): Promise<void> {
88
100
  if (this.#started) return;
101
+ this.#stopped = false;
89
102
  if (!this.#options.config.access_token) {
90
103
  // wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
91
104
  this.#logger.warn(formatCompact({
@@ -117,6 +130,7 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
117
130
  }
118
131
 
119
132
  async stop(): Promise<void> {
133
+ this.#stopped = true;
120
134
  this.close();
121
135
  this.#wsRelease?.();
122
136
  this.#wsRelease = undefined;
@@ -131,7 +145,11 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
131
145
  const message = formatOutboundSegments(payload);
132
146
  const { action, params } = buildSendAction(conversation, message);
133
147
  const data = await callOnebot11Client<{ message_id?: number | string }>(this.client, action, params);
134
- return data?.message_id != null ? String(data.message_id) : '';
148
+ const rawId = data?.message_id;
149
+ if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
150
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
151
+ }
152
+ return String(rawId);
135
153
  }
136
154
 
137
155
  async recallMessage(messageId: string): Promise<void> {
@@ -143,7 +161,7 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
143
161
  action: string,
144
162
  params: Record<string, unknown> = {},
145
163
  ): ReturnType<typeof callOneBot11WsAction> {
146
- return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
164
+ return callOneBot11WsAction(this.#connectionLifecycle.state === 'closed' ? undefined : this.#ws, this.#pending, this.#requestId, action, params);
147
165
  }
148
166
 
149
167
  #admitRaw(ev: OneBot11Event): void {
@@ -189,6 +207,10 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
189
207
  }
190
208
 
191
209
  async #acceptConnection(connection: WsConnection): Promise<void> {
210
+ if (!this.#started || this.#stopped) {
211
+ connection.socket.close();
212
+ return;
213
+ }
192
214
  if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
193
215
  connection.socket.close(4003, 'Unauthorized');
194
216
  return;
@@ -199,6 +221,9 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
199
221
  await this.#connectionLifecycle.start(async (lifecycleHandle) => {
200
222
  this.#ws = socket;
201
223
  lifecycleHandle.onForceClose(() => {
224
+ if (this.#ws === socket) {
225
+ rejectAllPending(this.#pending);
226
+ }
202
227
  try {
203
228
  socket.close();
204
229
  } catch {
@@ -213,8 +238,11 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
213
238
  /* ignore */
214
239
  }
215
240
  }, this.#options.config.heartbeat_interval);
241
+ socket.on('pong', () => {
242
+ if (this.#ws === socket) this.#connectionLifecycle.notifyHeartbeatAck();
243
+ });
216
244
  socket.on('message', (data) => {
217
- if (this.#ws !== socket) return;
245
+ if (this.#ws !== socket || this.#connectionLifecycle.state === 'closed') return;
218
246
  this.#connectionLifecycle.notifyHeartbeatAck();
219
247
  handleOneBot11WsMessage(data, {
220
248
  endpointId: this.#options.config.id,
@@ -222,7 +250,12 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
222
250
  ingest: (ev) => this.client.ingest(ev as Parameters<Onebot11Client['ingest']>[0]),
223
251
  });
224
252
  });
253
+ socket.on('error', (error) => {
254
+ if (this.#ws !== socket) return;
255
+ this.#logger.warn(formatCompact({ op: 'ws_error', error: String(error) }));
256
+ });
225
257
  socket.on('close', () => {
258
+ if (this.#ws !== socket) return;
226
259
  if (this.#ws === socket) {
227
260
  this.#ws = undefined;
228
261
  rejectAllPending(this.#pending);