@zhin.js/adapter-onebot11 1.1.0 → 1.1.2

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 (40) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +8 -6
  3. package/adapters/{onebot11.js → onebot11/index.js} +8 -8
  4. package/adapters/{onebot11.ts → onebot11/index.ts} +14 -14
  5. package/agents/onebot11/agent.json +20 -0
  6. package/agents/onebot11/boundaries.md +3 -0
  7. package/agents/onebot11/conventions.md +3 -0
  8. package/agents/onebot11/skills/onebot11/SKILL.md +15 -0
  9. package/agents/onebot11/skills/onebot11/tools/set_title/index.js +25 -0
  10. package/{agent/tools/set_title.ts → agents/onebot11/skills/onebot11/tools/set_title/index.ts} +1 -1
  11. package/agents/onebot11/system.md +3 -0
  12. package/commands/{endpoint/add/[id].js → onebot11/endpoint/add/[id]/index.js} +1 -1
  13. package/commands/onebot11/endpoint/add/[id]/index.ts +3 -0
  14. package/{lib/onebot11-endpoint-commands.js → commands/onebot11/endpoint/definition.js} +4 -3
  15. package/{src/onebot11-endpoint-commands.ts → commands/onebot11/endpoint/definition.ts} +3 -3
  16. package/commands/{endpoint/list.js → onebot11/endpoint/list/index.js} +1 -1
  17. package/commands/onebot11/endpoint/list/index.ts +3 -0
  18. package/commands/{endpoint/remove/[id].js → onebot11/endpoint/remove/[id]/index.js} +1 -1
  19. package/commands/onebot11/endpoint/remove/[id]/index.ts +3 -0
  20. package/lib/index.d.ts +2 -2
  21. package/lib/index.js +1 -1
  22. package/lib/onebot11-runtime-state.js +1 -1
  23. package/lib/protocol.d.ts +5 -22
  24. package/lib/protocol.js +37 -26
  25. package/lib/ws-transport.d.ts +0 -1
  26. package/lib/ws-transport.js +1 -13
  27. package/lib/wss-endpoint.js +63 -50
  28. package/package.json +33 -21
  29. package/schema.json +18 -1
  30. package/src/index.ts +0 -2
  31. package/src/onebot11-runtime-state.ts +1 -1
  32. package/src/protocol.ts +43 -50
  33. package/src/ws-transport.ts +1 -16
  34. package/src/wss-endpoint.ts +59 -48
  35. package/agent/skills/onebot11.md +0 -80
  36. package/commands/endpoint/add/[id].ts +0 -3
  37. package/commands/endpoint/list.ts +0 -3
  38. package/commands/endpoint/remove/[id].ts +0 -3
  39. package/lib/onebot11-endpoint-commands.d.ts +0 -1
  40. /package/{agent/PERMITS.md → PERMITS.md} +0 -0
@@ -19,17 +19,18 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
19
19
  content;
20
20
  #ws;
21
21
  #wsRelease;
22
- #lifecycle;
22
+ #connectionLifecycle;
23
+ #connectionTask = Promise.resolve();
23
24
  #requestId = { value: 0 };
24
25
  #pending = new Map();
26
+ #started = false;
25
27
  constructor(options) {
26
28
  super();
27
29
  this.#logger = getAdapterLogger('onebot11', options.config.id);
28
30
  this.#options = options;
29
- this.#lifecycle = createEndpointLifecycle({
30
- name: options.config.id,
31
+ this.#connectionLifecycle = createEndpointLifecycle({
32
+ name: `${options.config.id}:inbound`,
31
33
  reconnect: false,
32
- heartbeat: { intervalMs: options.config.heartbeat_interval },
33
34
  });
34
35
  this.client = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
35
36
  const callApi = (action, params) => callOnebot11Client(this.client, action, params);
@@ -41,6 +42,8 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
41
42
  }, (_name, error) => this.#warnPlatformEvent(error));
42
43
  }
43
44
  async start() {
45
+ if (this.#started)
46
+ return;
44
47
  if (!this.#options.config.access_token) {
45
48
  // wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
46
49
  this.#logger.warn(formatCompact({
@@ -50,23 +53,19 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
50
53
  error: 'missing access_token',
51
54
  }));
52
55
  }
53
- await this.#lifecycle.start(async (lifecycleHandle) => {
54
- const handle = this.#options.http.ws(this.#options.config.path);
55
- this.#wsRelease = handle.onConnection((connection) => {
56
- this.#acceptConnection(connection);
57
- });
58
- lifecycleHandle.onForceClose(() => {
59
- this.#wsRelease?.();
60
- this.#wsRelease = undefined;
61
- try {
62
- this.#ws?.close();
63
- }
64
- catch {
65
- /* ignore */
66
- }
67
- this.#ws = undefined;
56
+ const handle = this.#options.http.ws(this.#options.config.path);
57
+ this.#wsRelease = handle.onConnection((connection) => {
58
+ this.#connectionTask = this.#connectionTask
59
+ .then(() => this.#acceptConnection(connection))
60
+ .catch((error) => {
61
+ this.#logger.warn(formatCompact({
62
+ op: 'wss_connection_failed',
63
+ endpoint: this.#options.config.id,
64
+ error: error instanceof Error ? error.message : String(error),
65
+ }));
68
66
  });
69
67
  });
68
+ this.#started = true;
70
69
  this.#logger.info(formatCompact({
71
70
  op: 'listen',
72
71
  endpoint: this.#options.config.id,
@@ -76,8 +75,13 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
76
75
  }
77
76
  async stop() {
78
77
  this.close();
79
- await this.#lifecycle.stop();
78
+ this.#wsRelease?.();
79
+ this.#wsRelease = undefined;
80
+ await this.#connectionTask;
81
+ await this.#connectionLifecycle.stop();
80
82
  rejectAllPending(this.#pending);
83
+ this.#ws = undefined;
84
+ this.#started = false;
81
85
  }
82
86
  async send({ conversation, payload }) {
83
87
  const message = formatOutboundSegments(payload);
@@ -127,42 +131,51 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
127
131
  error: error instanceof Error ? error.message : String(error),
128
132
  }));
129
133
  }
130
- #acceptConnection(connection) {
134
+ async #acceptConnection(connection) {
131
135
  if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
132
136
  connection.socket.close(4003, 'Unauthorized');
133
137
  return;
134
138
  }
135
139
  const socket = connection.socket;
136
- if (this.#ws) {
137
- try {
138
- this.#ws.close();
139
- }
140
- catch {
141
- /* ignore */
142
- }
143
- }
144
- this.#ws = socket;
145
- this.#lifecycle.startHeartbeat(() => {
146
- try {
147
- this.#ws?.ping?.();
148
- }
149
- catch {
150
- /* ignore */
151
- }
152
- });
153
- socket.on('message', (data) => {
154
- this.#lifecycle.notifyHeartbeatAck();
155
- handleOneBot11WsMessage(data, {
156
- endpointId: this.#options.config.id,
157
- pending: this.#pending,
158
- ingest: (ev) => this.client.ingest(ev),
140
+ await this.#connectionLifecycle.stop();
141
+ rejectAllPending(this.#pending, '连接已替换');
142
+ await this.#connectionLifecycle.start(async (lifecycleHandle) => {
143
+ this.#ws = socket;
144
+ lifecycleHandle.onForceClose(() => {
145
+ try {
146
+ socket.close();
147
+ }
148
+ catch {
149
+ /* ignore */
150
+ }
151
+ if (this.#ws === socket)
152
+ this.#ws = undefined;
153
+ });
154
+ this.#connectionLifecycle.startHeartbeat(() => {
155
+ try {
156
+ socket.ping?.();
157
+ }
158
+ catch {
159
+ /* ignore */
160
+ }
161
+ }, this.#options.config.heartbeat_interval);
162
+ socket.on('message', (data) => {
163
+ if (this.#ws !== socket)
164
+ return;
165
+ this.#connectionLifecycle.notifyHeartbeatAck();
166
+ handleOneBot11WsMessage(data, {
167
+ endpointId: this.#options.config.id,
168
+ pending: this.#pending,
169
+ ingest: (ev) => this.client.ingest(ev),
170
+ });
171
+ });
172
+ socket.on('close', () => {
173
+ if (this.#ws === socket) {
174
+ this.#ws = undefined;
175
+ rejectAllPending(this.#pending);
176
+ }
177
+ lifecycleHandle.notifyClosed(new Error('OneBot11 reverse WebSocket closed'));
159
178
  });
160
- });
161
- socket.on('close', () => {
162
- if (this.#ws === socket) {
163
- this.#ws = undefined;
164
- this.#lifecycle.stopHeartbeat();
165
- }
166
179
  });
167
180
  this.#logger.debug(formatCompact({
168
181
  endpoint: this.#options.config.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-onebot11",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Zhin.js OneBot 11 adapter for Plugin Runtime (WebSocket client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -19,9 +19,11 @@
19
19
  "schema.json",
20
20
  "src",
21
21
  "lib",
22
- "agent",
22
+ "tools",
23
+ "agents",
23
24
  "README.md",
24
- "CHANGELOG.md"
25
+ "CHANGELOG.md",
26
+ "PERMITS.md"
25
27
  ],
26
28
  "keywords": [
27
29
  "zhin",
@@ -43,13 +45,16 @@
43
45
  "@imhelper/onebot-v11": "1.0.7",
44
46
  "imhelper": "1.0.7",
45
47
  "ws": "^8.21.1",
46
- "@zhin.js/adapter": "1.1.12",
47
- "@zhin.js/core": "1.1.35",
48
- "@zhin.js/feature-kit": "1.1.0",
49
- "@zhin.js/host-http": "1.1.0",
50
- "@zhin.js/im-contract": "1.1.0",
51
- "@zhin.js/logger": "1.1.0",
52
- "@zhin.js/permission": "1.1.0"
48
+ "@zhin.js/adapter": "1.1.14",
49
+ "@zhin.js/agent-feature": "1.1.1",
50
+ "@zhin.js/core": "1.1.37",
51
+ "@zhin.js/feature-kit": "1.1.1",
52
+ "@zhin.js/host-http": "1.1.1",
53
+ "@zhin.js/im-contract": "1.1.1",
54
+ "@zhin.js/logger": "1.1.1",
55
+ "@zhin.js/permission": "1.1.1",
56
+ "@zhin.js/skill": "1.1.1",
57
+ "@zhin.js/tool": "1.1.1"
53
58
  },
54
59
  "devDependencies": {
55
60
  "@types/node": "^26.1.2",
@@ -57,22 +62,17 @@
57
62
  "typescript": "^6.0.3",
58
63
  "vitest": "^4.1.10",
59
64
  "zod": "^4.4.3",
60
- "@zhin.js/agent": "1.1.23",
61
- "@zhin.js/host-http": "1.1.0",
62
- "zhin.js": "1.1.0"
65
+ "@zhin.js/host-http": "1.1.1",
66
+ "zhin.js": "1.1.2"
63
67
  },
64
68
  "peerDependencies": {
65
69
  "zod": "^4.0.0",
66
- "@zhin.js/adapter": "^1.1.12",
67
- "@zhin.js/agent": "^1.1.23",
68
- "@zhin.js/command": "^1.1.0",
69
- "@zhin.js/core": "^1.1.35",
70
- "zhin.js": "^1.1.0"
70
+ "@zhin.js/adapter": "^1.1.14",
71
+ "@zhin.js/command": "^1.1.1",
72
+ "@zhin.js/core": "^1.1.37",
73
+ "zhin.js": "^1.1.2"
71
74
  },
72
75
  "peerDependenciesMeta": {
73
- "@zhin.js/agent": {
74
- "optional": true
75
- },
76
76
  "@zhin.js/command": {
77
77
  "optional": true
78
78
  },
@@ -109,6 +109,18 @@
109
109
  {
110
110
  "package": "@zhin.js/command",
111
111
  "api": "^1.0.0"
112
+ },
113
+ {
114
+ "package": "@zhin.js/tool",
115
+ "api": "^1.0.0"
116
+ },
117
+ {
118
+ "package": "@zhin.js/skill",
119
+ "api": "^1.0.0"
120
+ },
121
+ {
122
+ "package": "@zhin.js/agent-feature",
123
+ "api": "^1.0.0"
112
124
  }
113
125
  ],
114
126
  "plugins": []
package/schema.json CHANGED
@@ -40,10 +40,11 @@
40
40
  },
41
41
  "endpoints": {
42
42
  "type": "array",
43
+ "minItems": 1,
43
44
  "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
44
45
  "items": {
45
46
  "type": "object",
46
- "additionalProperties": true,
47
+ "additionalProperties": false,
47
48
  "properties": {
48
49
  "master": {
49
50
  "type": [
@@ -63,6 +64,13 @@
63
64
  },
64
65
  "description": "本 endpoint 的 trusted 列表"
65
66
  },
67
+ "connection": {
68
+ "type": "string",
69
+ "enum": [
70
+ "ws",
71
+ "wss"
72
+ ]
73
+ },
66
74
  "url": {
67
75
  "type": "string",
68
76
  "description": "OneBot implementation WebSocket URL (required for connection: ws)"
@@ -75,6 +83,15 @@
75
83
  "type": "string",
76
84
  "description": "OneBot access token"
77
85
  },
86
+ "reconnect_interval": {
87
+ "type": "number"
88
+ },
89
+ "heartbeat_interval": {
90
+ "type": "number"
91
+ },
92
+ "commandPrefix": {
93
+ "type": "string"
94
+ },
78
95
  "id": {
79
96
  "type": "string",
80
97
  "description": "OneBot11 bot name"
package/src/index.ts CHANGED
@@ -16,7 +16,6 @@ export {
16
16
  senderUserId,
17
17
  type OneBot11ActionRequest,
18
18
  type OneBot11ActionResponse,
19
- type OneBot11AdapterConfig,
20
19
  type OneBot11ConfigBase,
21
20
  type OneBot11EndpointConfig,
22
21
  type OneBot11Event,
@@ -45,7 +44,6 @@ export {
45
44
  decodeWsPayload,
46
45
  handleOneBot11WsMessage,
47
46
  rejectAllPending,
48
- startOneBot11Heartbeat,
49
47
  } from './ws-transport.js';
50
48
 
51
49
  export { verifyOneBotAccessToken } from './wss-auth.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * OneBot11 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `onebot11.endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `onebot11 endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
6
 
package/src/protocol.ts CHANGED
@@ -3,37 +3,20 @@
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 } from '@zhin.js/core';
7
- import type { ConversationRef } from '@zhin.js/im-contract';
6
+ import { isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
8
7
  import { formatCompact, getLogger } from '@zhin.js/logger';
9
8
 
10
9
  const logger = getLogger('onebot11');
11
10
 
12
- /** Transitional legacy endpoint row (`endpoints[]` with `context: onebot11`). */
13
- export interface OneBot11LegacyEndpointRow {
14
- readonly context?: string;
11
+ /** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
12
+ export interface OneBot11EndpointConfig {
15
13
  readonly connection?: 'ws' | 'wss';
16
- /** Legacy alias: `type: 'ws' | 'ws_reverse'` */
17
- readonly type?: string;
18
- readonly id?: string;
19
- readonly access_token?: string;
20
- readonly url?: string;
21
- readonly path?: string;
22
- readonly reconnect_interval?: number;
23
- readonly heartbeat_interval?: number;
24
- }
25
-
26
- /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
27
- export interface OneBot11AdapterConfig {
28
- readonly connection?: 'ws' | 'wss';
29
- readonly id?: string;
14
+ readonly id: string;
30
15
  readonly access_token?: string;
31
16
  readonly url?: string;
32
17
  readonly path?: string;
33
18
  readonly reconnect_interval?: number;
34
19
  readonly heartbeat_interval?: number;
35
- /** Transitional: legacy root `endpoints[]` with `context: onebot11`. */
36
- readonly endpoints?: ReadonlyArray<OneBot11LegacyEndpointRow>;
37
20
  }
38
21
 
39
22
  export interface OneBot11ConfigBase {
@@ -58,7 +41,6 @@ export interface OneBot11WssConfig extends OneBot11ConfigBase {
58
41
  }
59
42
 
60
43
  export type ResolvedOneBot11Config = OneBot11WsConfig | OneBot11WssConfig;
61
- export type OneBot11EndpointConfig = ResolvedOneBot11Config;
62
44
 
63
45
  export interface OneBot11Sender {
64
46
  readonly role?: string;
@@ -110,62 +92,59 @@ export interface OneBot11WireSegment {
110
92
 
111
93
  function normalizeConnection(
112
94
  connection: string | undefined,
113
- legacyType: string | undefined,
114
95
  ): 'ws' | 'wss' {
115
96
  if (connection === 'ws' || connection === 'wss') return connection;
116
- if (legacyType === 'ws_reverse' || legacyType === 'wss') return 'wss';
117
- if (legacyType === 'ws') return 'ws';
118
97
  return 'ws';
119
98
  }
120
99
 
121
- export function resolveOneBot11Config(config: OneBot11AdapterConfig = {}): ResolvedOneBot11Config {
122
- const entry = config.endpoints?.find((item) => item.context === 'onebot11');
123
- const connection = normalizeConnection(
124
- config.connection ?? entry?.connection,
125
- entry?.type,
126
- );
127
- const id = (typeof config.id === 'string' && config.id)
128
- || (typeof entry?.id === 'string' && entry.id)
129
- || process.env.ONEBOT11_BOT_NAME
130
- || 'onebot11-bot';
131
- const access_token = config.access_token ?? entry?.access_token;
100
+ export function resolveOneBot11Config(config: OneBot11EndpointConfig): ResolvedOneBot11Config {
101
+ const connection = normalizeConnection(config.connection);
102
+ const id = requiredEndpointField(config.id, 'id');
103
+ const access_token = optionalEndpointField(config.access_token);
132
104
 
133
105
  if (connection === 'ws') {
134
- const url = config.url ?? entry?.url;
135
- if (!url) {
136
- throw new TypeError(
137
- 'OneBot11 connection:ws requires url (plugins.<key>.url or endpoints with context: onebot11)',
138
- );
139
- }
106
+ const url = requiredEndpointField(config.url, 'url');
140
107
  return {
141
108
  context: 'onebot11',
142
109
  connection: 'ws',
143
110
  id,
144
111
  access_token,
145
112
  url,
146
- reconnect_interval: config.reconnect_interval ?? entry?.reconnect_interval ?? 5000,
147
- heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
113
+ reconnect_interval: config.reconnect_interval ?? 5000,
114
+ heartbeat_interval: config.heartbeat_interval ?? 30_000,
148
115
  };
149
116
  }
150
117
 
151
118
  if (connection === 'wss') {
152
- const path = config.path ?? entry?.path;
153
- if (!path) {
154
- throw new TypeError('OneBot11 connection:wss requires path');
155
- }
119
+ const path = requiredEndpointField(config.path, 'path');
156
120
  return {
157
121
  context: 'onebot11',
158
122
  connection: 'wss',
159
123
  id,
160
124
  access_token,
161
125
  path,
162
- heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
126
+ heartbeat_interval: config.heartbeat_interval ?? 30_000,
163
127
  };
164
128
  }
165
129
 
166
130
  throw new TypeError(`Unknown OneBot11 connection: ${String(connection)}`);
167
131
  }
168
132
 
133
+ function requiredEndpointField(
134
+ value: unknown,
135
+ field: 'id' | 'url' | 'path',
136
+ ): string {
137
+ const resolved = optionalEndpointField(value);
138
+ if (!resolved) {
139
+ throw new TypeError(`OneBot11 endpoint requires a non-empty ${field}`);
140
+ }
141
+ return resolved;
142
+ }
143
+
144
+ function optionalEndpointField(value: unknown): string | undefined {
145
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
146
+ }
147
+
169
148
  /** 判断是否为消息事件(post_type=message) */
170
149
  export function isMessageEvent(
171
150
  ev: OneBot11Event,
@@ -205,10 +184,24 @@ export function formatInboundContent(ev: OneBot11Event): string {
205
184
  .join('');
206
185
  }
207
186
  return typeof ev.raw_message === 'string'
208
- ? ev.raw_message.replace(/\[CQ:[^\]]+\]/g, '').trim()
187
+ ? stripCqCodes(ev.raw_message).trim()
209
188
  : '';
210
189
  }
211
190
 
191
+ function stripCqCodes(input: string): string {
192
+ let result = '';
193
+ let cursor = 0;
194
+ while (cursor < input.length) {
195
+ const start = input.indexOf('[CQ:', cursor);
196
+ if (start < 0) return result + input.slice(cursor);
197
+ result += input.slice(cursor, start);
198
+ const end = input.indexOf(']', start + 4);
199
+ if (end < 0) return result + input.slice(start);
200
+ cursor = end + 1;
201
+ }
202
+ return result;
203
+ }
204
+
212
205
  export function senderDisplayName(ev: OneBot11Event): string {
213
206
  const name = ev.sender?.card || ev.sender?.nickname;
214
207
  if (typeof name === 'string' && name) return name;
@@ -1,4 +1,4 @@
1
- import { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';
1
+ import { clearTimeout, setTimeout } from 'node:timers';
2
2
  import { formatCompact, getLogger } from '@zhin.js/logger';
3
3
  import {
4
4
  type OneBot11ActionRequest,
@@ -73,21 +73,6 @@ export function callOneBot11WsAction(
73
73
  });
74
74
  }
75
75
 
76
- export function startOneBot11Heartbeat(
77
- ws: OneBot11WsSocket | undefined,
78
- intervalMs: number,
79
- existingTimer?: NodeJS.Timeout,
80
- ): NodeJS.Timeout {
81
- if (existingTimer) clearInterval(existingTimer);
82
- return setInterval(() => {
83
- try {
84
- ws?.ping?.();
85
- } catch {
86
- /* ignore */
87
- }
88
- }, intervalMs);
89
- }
90
-
91
76
  export function rejectAllPending(
92
77
  pending: Map<string, OneBot11PendingAction>,
93
78
  message = '连接已关闭',
@@ -57,18 +57,19 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
57
57
  readonly content;
58
58
  #ws?: OneBot11WsSocket;
59
59
  #wsRelease?: () => void;
60
- readonly #lifecycle: EndpointLifecycle;
60
+ readonly #connectionLifecycle: EndpointLifecycle;
61
+ #connectionTask = Promise.resolve();
61
62
  #requestId = { value: 0 };
62
63
  #pending = new Map<string, OneBot11PendingAction>();
64
+ #started = false;
63
65
 
64
66
  constructor(options: OneBot11WssEndpointOptions) {
65
67
  super();
66
68
  this.#logger = getAdapterLogger('onebot11', options.config.id);
67
69
  this.#options = options;
68
- this.#lifecycle = createEndpointLifecycle({
69
- name: options.config.id,
70
+ this.#connectionLifecycle = createEndpointLifecycle({
71
+ name: `${options.config.id}:inbound`,
70
72
  reconnect: false,
71
- heartbeat: { intervalMs: options.config.heartbeat_interval },
72
73
  });
73
74
  this.client = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
74
75
  const callApi = (action: string, params?: Record<string, unknown>) => callOnebot11Client(this.client, action, params);
@@ -84,6 +85,7 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
84
85
  }
85
86
 
86
87
  async start(): Promise<void> {
88
+ if (this.#started) return;
87
89
  if (!this.#options.config.access_token) {
88
90
  // wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
89
91
  this.#logger.warn(formatCompact({
@@ -93,22 +95,19 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
93
95
  error: 'missing access_token',
94
96
  }));
95
97
  }
96
- await this.#lifecycle.start(async (lifecycleHandle) => {
97
- const handle = this.#options.http.ws(this.#options.config.path);
98
- this.#wsRelease = handle.onConnection((connection) => {
99
- this.#acceptConnection(connection);
100
- });
101
- lifecycleHandle.onForceClose(() => {
102
- this.#wsRelease?.();
103
- this.#wsRelease = undefined;
104
- try {
105
- this.#ws?.close();
106
- } catch {
107
- /* ignore */
108
- }
109
- this.#ws = undefined;
110
- });
98
+ const handle = this.#options.http.ws(this.#options.config.path);
99
+ this.#wsRelease = handle.onConnection((connection) => {
100
+ this.#connectionTask = this.#connectionTask
101
+ .then(() => this.#acceptConnection(connection))
102
+ .catch((error) => {
103
+ this.#logger.warn(formatCompact({
104
+ op: 'wss_connection_failed',
105
+ endpoint: this.#options.config.id,
106
+ error: error instanceof Error ? error.message : String(error),
107
+ }));
108
+ });
111
109
  });
110
+ this.#started = true;
112
111
  this.#logger.info(formatCompact({
113
112
  op: 'listen',
114
113
  endpoint: this.#options.config.id,
@@ -119,8 +118,13 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
119
118
 
120
119
  async stop(): Promise<void> {
121
120
  this.close();
122
- await this.#lifecycle.stop();
121
+ this.#wsRelease?.();
122
+ this.#wsRelease = undefined;
123
+ await this.#connectionTask;
124
+ await this.#connectionLifecycle.stop();
123
125
  rejectAllPending(this.#pending);
126
+ this.#ws = undefined;
127
+ this.#started = false;
124
128
  }
125
129
 
126
130
  async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
@@ -184,40 +188,47 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
184
188
  }));
185
189
  }
186
190
 
187
- #acceptConnection(connection: WsConnection): void {
191
+ async #acceptConnection(connection: WsConnection): Promise<void> {
188
192
  if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
189
193
  connection.socket.close(4003, 'Unauthorized');
190
194
  return;
191
195
  }
192
196
  const socket = connection.socket as unknown as OneBot11WsSocket;
193
- if (this.#ws) {
194
- try {
195
- this.#ws.close();
196
- } catch {
197
- /* ignore */
198
- }
199
- }
200
- this.#ws = socket;
201
- this.#lifecycle.startHeartbeat(() => {
202
- try {
203
- this.#ws?.ping?.();
204
- } catch {
205
- /* ignore */
206
- }
207
- });
208
- socket.on('message', (data) => {
209
- this.#lifecycle.notifyHeartbeatAck();
210
- handleOneBot11WsMessage(data, {
211
- endpointId: this.#options.config.id,
212
- pending: this.#pending,
213
- ingest: (ev) => this.client.ingest(ev as Parameters<Onebot11Client['ingest']>[0]),
197
+ await this.#connectionLifecycle.stop();
198
+ rejectAllPending(this.#pending, '连接已替换');
199
+ await this.#connectionLifecycle.start(async (lifecycleHandle) => {
200
+ this.#ws = socket;
201
+ lifecycleHandle.onForceClose(() => {
202
+ try {
203
+ socket.close();
204
+ } catch {
205
+ /* ignore */
206
+ }
207
+ if (this.#ws === socket) this.#ws = undefined;
208
+ });
209
+ this.#connectionLifecycle.startHeartbeat(() => {
210
+ try {
211
+ socket.ping?.();
212
+ } catch {
213
+ /* ignore */
214
+ }
215
+ }, this.#options.config.heartbeat_interval);
216
+ socket.on('message', (data) => {
217
+ if (this.#ws !== socket) return;
218
+ this.#connectionLifecycle.notifyHeartbeatAck();
219
+ handleOneBot11WsMessage(data, {
220
+ endpointId: this.#options.config.id,
221
+ pending: this.#pending,
222
+ ingest: (ev) => this.client.ingest(ev as Parameters<Onebot11Client['ingest']>[0]),
223
+ });
224
+ });
225
+ socket.on('close', () => {
226
+ if (this.#ws === socket) {
227
+ this.#ws = undefined;
228
+ rejectAllPending(this.#pending);
229
+ }
230
+ lifecycleHandle.notifyClosed(new Error('OneBot11 reverse WebSocket closed'));
214
231
  });
215
- });
216
- socket.on('close', () => {
217
- if (this.#ws === socket) {
218
- this.#ws = undefined;
219
- this.#lifecycle.stopHeartbeat();
220
- }
221
232
  });
222
233
  this.#logger.debug(formatCompact({
223
234
  endpoint: this.#options.config.id,