@zhin.js/adapter-onebot11 1.0.98 → 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 (88) hide show
  1. package/CHANGELOG.md +745 -3
  2. package/PERMITS.md +14 -0
  3. package/README.md +59 -367
  4. package/adapters/onebot11/index.js +40 -0
  5. package/adapters/onebot11/index.ts +53 -0
  6. package/agents/onebot11/agent.json +20 -0
  7. package/agents/onebot11/boundaries.md +3 -0
  8. package/agents/onebot11/conventions.md +3 -0
  9. package/agents/onebot11/skills/onebot11/SKILL.md +15 -0
  10. package/agents/onebot11/skills/onebot11/tools/set_title/index.js +25 -0
  11. package/agents/onebot11/skills/onebot11/tools/set_title/index.ts +24 -0
  12. package/agents/onebot11/system.md +3 -0
  13. package/commands/onebot11/endpoint/add/[id]/index.js +3 -0
  14. package/commands/onebot11/endpoint/add/[id]/index.ts +3 -0
  15. package/commands/onebot11/endpoint/definition.js +25 -0
  16. package/commands/onebot11/endpoint/definition.ts +23 -0
  17. package/commands/onebot11/endpoint/list/index.js +3 -0
  18. package/commands/onebot11/endpoint/list/index.ts +3 -0
  19. package/commands/onebot11/endpoint/remove/[id]/index.js +3 -0
  20. package/commands/onebot11/endpoint/remove/[id]/index.ts +3 -0
  21. package/lib/client.d.ts +20 -0
  22. package/lib/client.js +30 -0
  23. package/lib/content-port.d.ts +4 -0
  24. package/lib/content-port.js +63 -0
  25. package/lib/endpoint-management.d.ts +15 -0
  26. package/lib/endpoint-management.js +47 -0
  27. package/lib/index.d.ts +10 -16
  28. package/lib/index.js +7 -53
  29. package/lib/onebot11-runtime-state.d.ts +1 -0
  30. package/lib/onebot11-runtime-state.js +6 -0
  31. package/lib/protocol.d.ts +143 -0
  32. package/lib/protocol.js +327 -0
  33. package/lib/side-event-dispatch.d.ts +7 -0
  34. package/lib/side-event-dispatch.js +36 -0
  35. package/lib/ws-endpoint.d.ts +22 -0
  36. package/lib/ws-endpoint.js +209 -0
  37. package/lib/ws-transport.d.ts +12 -0
  38. package/lib/ws-transport.js +59 -0
  39. package/lib/ws-types.d.ts +18 -0
  40. package/lib/ws-types.js +1 -0
  41. package/lib/wss-auth.d.ts +2 -0
  42. package/lib/wss-auth.js +16 -0
  43. package/lib/wss-endpoint.d.ts +25 -0
  44. package/lib/wss-endpoint.js +186 -0
  45. package/package.json +82 -23
  46. package/plugin.js +18 -0
  47. package/schema.json +114 -0
  48. package/src/client.ts +78 -0
  49. package/src/content-port.ts +66 -0
  50. package/src/endpoint-management.ts +72 -0
  51. package/src/index.ts +60 -65
  52. package/src/onebot11-runtime-state.ts +7 -0
  53. package/src/protocol.ts +452 -0
  54. package/src/side-event-dispatch.ts +48 -0
  55. package/src/ws-endpoint.ts +272 -0
  56. package/src/ws-transport.ts +85 -0
  57. package/src/ws-types.ts +21 -0
  58. package/src/wss-auth.ts +17 -0
  59. package/src/wss-endpoint.ts +239 -0
  60. package/lib/adapter.d.ts +0 -32
  61. package/lib/adapter.d.ts.map +0 -1
  62. package/lib/adapter.js +0 -92
  63. package/lib/adapter.js.map +0 -1
  64. package/lib/bot-ws-client.d.ts +0 -40
  65. package/lib/bot-ws-client.d.ts.map +0 -1
  66. package/lib/bot-ws-client.js +0 -397
  67. package/lib/bot-ws-client.js.map +0 -1
  68. package/lib/bot-ws-server.d.ts +0 -42
  69. package/lib/bot-ws-server.d.ts.map +0 -1
  70. package/lib/bot-ws-server.js +0 -344
  71. package/lib/bot-ws-server.js.map +0 -1
  72. package/lib/index.d.ts.map +0 -1
  73. package/lib/index.js.map +0 -1
  74. package/lib/onebot-get-msg.d.ts +0 -3
  75. package/lib/onebot-get-msg.d.ts.map +0 -1
  76. package/lib/onebot-get-msg.js +0 -27
  77. package/lib/onebot-get-msg.js.map +0 -1
  78. package/lib/types.d.ts +0 -54
  79. package/lib/types.d.ts.map +0 -1
  80. package/lib/types.js +0 -5
  81. package/lib/types.js.map +0 -1
  82. package/plugin.yml +0 -3
  83. package/skills/onebot11/SKILL.md +0 -80
  84. package/src/adapter.ts +0 -104
  85. package/src/bot-ws-client.ts +0 -421
  86. package/src/bot-ws-server.ts +0 -382
  87. package/src/onebot-get-msg.ts +0 -33
  88. package/src/types.ts +0 -58
@@ -0,0 +1,48 @@
1
+ import { receiveOneBotLikeSideEvent } from '@zhin.js/core';
2
+ import type { EndpointEventEmitter } from 'zhin.js/adapter';
3
+ import { formatCompact, type getAdapterLogger } from '@zhin.js/logger';
4
+ import type { OneBot11Event } from './protocol.js';
5
+
6
+ export interface OneBot11SideEventCaller {
7
+ callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
8
+ }
9
+
10
+ export function receiveOneBot11SideEvent(
11
+ emit: EndpointEventEmitter,
12
+ endpointKey: string,
13
+ caller: OneBot11SideEventCaller,
14
+ raw: OneBot11Event,
15
+ logger: ReturnType<typeof getAdapterLogger>,
16
+ ): void {
17
+ if (!emit) return;
18
+ const record = raw as Record<string, unknown>;
19
+ const postType = String(record.post_type ?? '');
20
+ const requestType = String(record.request_type ?? '');
21
+ const isRequest = postType === 'request' || postType.startsWith('request.');
22
+ const isFriend = requestType === 'friend' || postType.includes('friend');
23
+ void receiveOneBotLikeSideEvent(emit, {
24
+ adapter: 'onebot11',
25
+ endpointKey,
26
+ platform: 'onebot',
27
+ raw: record,
28
+ ...(isRequest ? {
29
+ approve: async (flag, remark) => {
30
+ await (isFriend
31
+ ? caller.callApi('set_friend_add_request', { flag, approve: true, remark })
32
+ : caller.callApi('set_group_add_request', { flag, approve: true, reason: remark }));
33
+ },
34
+ reject: async (flag, reason) => {
35
+ await (isFriend
36
+ ? caller.callApi('set_friend_add_request', { flag, approve: false })
37
+ : caller.callApi('set_group_add_request', { flag, approve: false, reason }));
38
+ },
39
+ } : {}),
40
+ }).catch((err) => {
41
+ logger.warn(formatCompact({
42
+ op: 'onebot11_side_event_failed',
43
+ endpoint: endpointKey,
44
+ post_type: postType || undefined,
45
+ error: err instanceof Error ? err.message : String(err),
46
+ }));
47
+ });
48
+ }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * OneBot11 WS client endpoint — outbound connect to OneBot implementation.
3
+ */
4
+ import WebSocket from 'ws';
5
+ import {
6
+ ClientEndpoint,
7
+ createRecallEndpointControl,
8
+ createEndpointLifecycle,
9
+ type EndpointConnectHandle,
10
+ type EndpointControl,
11
+ type EndpointLifecycle,
12
+ type EndpointManagement,
13
+ type EndpointSendRequest,
14
+ } from 'zhin.js/adapter';
15
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
16
+ import type { CapabilityId } from 'zhin.js';
17
+ import { createOneBot11EndpointManagement } from './endpoint-management.js';
18
+ import {
19
+ buildSendAction,
20
+ buildWsConnectOptions,
21
+ formatInboundContent,
22
+ formatInboundMetadata,
23
+ formatOutboundSegments,
24
+ isMessageEvent,
25
+ onebot11InboundConversation,
26
+ senderDisplayName,
27
+ senderUserId,
28
+ type OneBot11Event,
29
+ type OneBot11WsConfig,
30
+ } from './protocol.js';
31
+ import { receiveOneBot11SideEvent } from './side-event-dispatch.js';
32
+ import { createOneBot11ContentPort } from './content-port.js';
33
+ import {
34
+ callOneBot11WsAction,
35
+ handleOneBot11WsMessage,
36
+ rejectAllPending,
37
+ } from './ws-transport.js';
38
+ import {
39
+ type OneBot11PendingAction,
40
+ type OneBot11WsCreateOptions,
41
+ type OneBot11WsSocket,
42
+ } from './ws-types.js';
43
+ import { callOnebot11Client, createOnebot11EndpointClient, forwardOnebot11ClientEvents, type Onebot11Client } from './client.js';
44
+
45
+ export interface OneBot11WsEndpointOptions {
46
+ readonly id: CapabilityId;
47
+ readonly config: OneBot11WsConfig;
48
+ readonly createWebSocket?: (
49
+ url: string,
50
+ options: OneBot11WsCreateOptions,
51
+ ) => OneBot11WsSocket;
52
+ }
53
+
54
+ export class OneBot11WsEndpoint extends ClientEndpoint<Onebot11Client> {
55
+ readonly client: Onebot11Client;
56
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
57
+
58
+ readonly #options: OneBot11WsEndpointOptions;
59
+ readonly management: EndpointManagement;
60
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
61
+ readonly content;
62
+ readonly #lifecycle: EndpointLifecycle;
63
+ #ws?: OneBot11WsSocket;
64
+ #requestId = { value: 0 };
65
+ #pending = new Map<string, OneBot11PendingAction>();
66
+
67
+ constructor(options: OneBot11WsEndpointOptions) {
68
+ super();
69
+ this.#logger = getAdapterLogger('onebot11', options.config.id);
70
+ this.#options = options;
71
+ this.client = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
72
+ const callApi = (action: string, params?: Record<string, unknown>) => callOnebot11Client(this.client, action, params);
73
+ this.management = createOneBot11EndpointManagement({ callApi });
74
+ this.content = createOneBot11ContentPort(callApi);
75
+ this.bindClientEvents(
76
+ (receive) => forwardOnebot11ClientEvents(this.client, receive),
77
+ (name, payload) => {
78
+ if (name === 'event') this.#admitRaw(payload as OneBot11Event);
79
+ },
80
+ (_name, error) => this.#warnPlatformEvent(error),
81
+ );
82
+ const { config } = options;
83
+ this.#lifecycle = createEndpointLifecycle({
84
+ name: config.id,
85
+ reconnect: {
86
+ initialIntervalMs: config.reconnect_interval,
87
+ // 固定间隔(multiplier 1、无抖动),对齐旧 reconnect_interval 语义
88
+ multiplier: 1,
89
+ maxIntervalMs: config.reconnect_interval,
90
+ jitterMs: 0,
91
+ },
92
+ heartbeat: { intervalMs: config.heartbeat_interval },
93
+ });
94
+ }
95
+
96
+ async start(): Promise<void> {
97
+ if (this.#lifecycle.started) return;
98
+ try {
99
+ await this.#lifecycle.start((handle) => this.#connect(handle));
100
+ } catch (err) {
101
+ // start 失败必须清理现场(状态复位由基座保证)
102
+ if (this.#ws) {
103
+ try {
104
+ this.#ws.close();
105
+ } catch {
106
+ /* ignore */
107
+ }
108
+ this.#ws = undefined;
109
+ }
110
+ throw err;
111
+ }
112
+ }
113
+
114
+ async stop(): Promise<void> {
115
+ this.close();
116
+ // 基座负责:清重连/心跳定时器、强关 ws、唤醒 stop-during-connect 竞态
117
+ await this.#lifecycle.stop();
118
+ rejectAllPending(this.#pending);
119
+ this.#ws = undefined;
120
+ }
121
+
122
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
123
+ const message = formatOutboundSegments(payload);
124
+ const { action, params } = buildSendAction(conversation, message);
125
+ const data = await callOnebot11Client<{ message_id?: number | string }>(this.client, action, params);
126
+ const messageId = data?.message_id != null ? String(data.message_id) : '';
127
+ this.#logger.debug(formatCompact({
128
+ op: 'onebot11_send',
129
+ endpoint: this.#options.config.id,
130
+ target: `${conversation.kind}:${conversation.id}`,
131
+ messageId,
132
+ }));
133
+ return messageId;
134
+ }
135
+
136
+ async recallMessage(messageId: string): Promise<void> {
137
+ if (!messageId) return;
138
+ await callOnebot11Client(this.client, 'delete_msg', { message_id: Number(messageId) });
139
+ }
140
+
141
+ #callApi(
142
+ action: string,
143
+ params: Record<string, unknown> = {},
144
+ ): ReturnType<typeof callOneBot11WsAction> {
145
+ return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
146
+ }
147
+
148
+ #admitRaw(ev: OneBot11Event): void {
149
+ if (!isMessageEvent(ev)) {
150
+ receiveOneBot11SideEvent(
151
+ (name, payload) => this.emit(name, payload),
152
+ this.#options.config.id,
153
+ { callApi: (action, params) => callOnebot11Client(this.client, action, params) },
154
+ ev,
155
+ this.#logger,
156
+ );
157
+ return;
158
+ }
159
+ const conversation = onebot11InboundConversation(String(this.#options.id), ev);
160
+ const content = formatInboundContent(ev);
161
+ const inbound = formatInboundMetadata(ev, this.#options.config.id);
162
+ void this.emit('message.receive', {
163
+ conversation,
164
+ message: { conversation, id: String(ev.message_id) },
165
+ content,
166
+ sender: {
167
+ id: senderUserId(ev),
168
+ name: senderDisplayName(ev) || undefined,
169
+ ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
170
+ },
171
+ endpointId: inbound.endpointId,
172
+ ...(inbound.mentioned ? { mentioned: true } : {}),
173
+ ...(inbound.replyTo ? { replyTo: inbound.replyTo } : {}),
174
+ metadata: inbound.metadata,
175
+ }).catch((err) => {
176
+ this.#logger.warn(formatCompact({
177
+ op: 'onebot11_gateway_receive_failed',
178
+ target: `${conversation.kind}:${conversation.id}`,
179
+ error: err instanceof Error ? err.message : String(err),
180
+ }));
181
+ });
182
+ }
183
+
184
+ #warnPlatformEvent(error: unknown): void {
185
+ this.#logger.warn(formatCompact({
186
+ op: 'onebot11_platform_event_failed',
187
+ error: error instanceof Error ? error.message : String(error),
188
+ }));
189
+ }
190
+
191
+ async #connect(handle: EndpointConnectHandle): Promise<void> {
192
+ const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
193
+ const create = this.#options.createWebSocket
194
+ ?? ((connectUrl: string, options: OneBot11WsCreateOptions) =>
195
+ new WebSocket(connectUrl, { headers: options.headers }) as unknown as OneBot11WsSocket);
196
+
197
+ await new Promise<void>((resolve, reject) => {
198
+ let settled = false;
199
+ const ws = create(url, { headers });
200
+ this.#ws = ws;
201
+ handle.onForceClose(() => {
202
+ try {
203
+ ws.close();
204
+ } catch {
205
+ /* ignore */
206
+ }
207
+ });
208
+
209
+ ws.on('open', () => {
210
+ if (settled) return;
211
+ settled = true;
212
+ if (!this.#options.config.access_token) {
213
+ this.#logger.warn(formatCompact({
214
+ endpoint: this.#options.config.id,
215
+ ok: false,
216
+ error: 'missing access_token',
217
+ }));
218
+ }
219
+ this.#logger.debug(formatCompact({
220
+ endpoint: this.#options.config.id,
221
+ mode: 'ws',
222
+ url: safeUrl,
223
+ }));
224
+ this.#lifecycle.startHeartbeat(() => this.#ws?.ping?.());
225
+ resolve();
226
+ });
227
+
228
+ ws.on('message', (data) => {
229
+ handleOneBot11WsMessage(data, {
230
+ endpointId: this.#options.config.id,
231
+ pending: this.#pending,
232
+ ingest: (ev) => this.client.ingest(ev as Parameters<Onebot11Client['ingest']>[0]),
233
+ });
234
+ });
235
+
236
+ ws.on('close', (code, reason) => {
237
+ const reasonStr = typeof reason === 'string'
238
+ ? reason
239
+ : Buffer.isBuffer(reason)
240
+ ? reason.toString()
241
+ : String(reason ?? '');
242
+ const codeNum = typeof code === 'number' ? code : Number(code ?? 0);
243
+ const codeHint = codeNum === 1005
244
+ ? ' [无状态,多为服务端/代理未发 close 帧即断开]'
245
+ : codeNum === 1006
246
+ ? ' [异常关闭]'
247
+ : '';
248
+ if (!settled) {
249
+ settled = true;
250
+ reject(new Error(`OneBot11 WS 关闭: ${codeNum} ${reasonStr}`));
251
+ }
252
+ // 断开日志与重连武装均由基座负责;仅曾 open 的连接才会武装重连,
253
+ // 初始连接失败由 start() 的拒绝路径复位,不产生僵尸重连。
254
+ handle.notifyClosed(`OneBot11 WS 关闭: ${codeNum} ${reasonStr || 'closed'}${codeHint}`);
255
+ });
256
+
257
+ ws.on('error', (err) => {
258
+ const error = err instanceof Error ? err : new Error(String(err));
259
+ this.#logger.warn(formatCompact({
260
+ op: 'ws_error',
261
+ endpoint: this.#options.config.id,
262
+ ok: false,
263
+ error: error.message,
264
+ }));
265
+ if (!settled) {
266
+ settled = true;
267
+ reject(error);
268
+ }
269
+ });
270
+ });
271
+ }
272
+ }
@@ -0,0 +1,85 @@
1
+ import { clearTimeout, setTimeout } from 'node:timers';
2
+ import { formatCompact, getLogger } from '@zhin.js/logger';
3
+ import {
4
+ type OneBot11ActionRequest,
5
+ type OneBot11ActionResponse,
6
+ type OneBot11Event,
7
+ } from './protocol.js';
8
+ import {
9
+ WS_OPEN,
10
+ type OneBot11PendingAction,
11
+ type OneBot11WsSocket,
12
+ } from './ws-types.js';
13
+
14
+ const logger = getLogger('onebot11');
15
+
16
+ export function decodeWsPayload(data: unknown): string {
17
+ if (typeof data === 'string') return data;
18
+ if (Buffer.isBuffer(data)) return data.toString();
19
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
20
+ return String(data ?? '');
21
+ }
22
+
23
+ export function handleOneBot11WsMessage(
24
+ data: unknown,
25
+ options: {
26
+ readonly endpointId: string;
27
+ readonly pending: Map<string, OneBot11PendingAction>;
28
+ readonly ingest: (ev: OneBot11Event) => void;
29
+ },
30
+ ): void {
31
+ try {
32
+ const raw = decodeWsPayload(data);
33
+ const msg = JSON.parse(raw) as OneBot11Event | OneBot11ActionResponse;
34
+ if ('echo' in msg && typeof (msg as OneBot11ActionResponse).echo === 'string') {
35
+ const resp = msg as OneBot11ActionResponse;
36
+ const pending = options.pending.get(resp.echo!);
37
+ if (pending) {
38
+ options.pending.delete(resp.echo!);
39
+ clearTimeout(pending.timeout);
40
+ pending.resolve(resp);
41
+ }
42
+ return;
43
+ }
44
+ options.ingest(msg as OneBot11Event);
45
+ } catch (error) {
46
+ logger.warn(formatCompact({
47
+ op: 'onebot11_parse_failed',
48
+ endpoint: options.endpointId,
49
+ error: error instanceof Error ? error.message : String(error),
50
+ }));
51
+ }
52
+ }
53
+
54
+ export function callOneBot11WsAction(
55
+ ws: OneBot11WsSocket | undefined,
56
+ pending: Map<string, OneBot11PendingAction>,
57
+ requestId: { value: number },
58
+ action: string,
59
+ params: Record<string, unknown>,
60
+ ): Promise<OneBot11ActionResponse> {
61
+ if (!ws || ws.readyState !== WS_OPEN) {
62
+ return Promise.reject(new Error('WebSocket 未连接'));
63
+ }
64
+ const echo = `ob11_${++requestId.value}`;
65
+ const req: OneBot11ActionRequest = { action, params, echo };
66
+ return new Promise((resolve, reject) => {
67
+ const timeout = setTimeout(() => {
68
+ pending.delete(echo);
69
+ reject(new Error(`OneBot11 动作超时: ${action}`));
70
+ }, 30_000);
71
+ pending.set(echo, { resolve, reject, timeout });
72
+ ws.send(JSON.stringify(req));
73
+ });
74
+ }
75
+
76
+ export function rejectAllPending(
77
+ pending: Map<string, OneBot11PendingAction>,
78
+ message = '连接已关闭',
79
+ ): void {
80
+ for (const [, entry] of pending) {
81
+ clearTimeout(entry.timeout);
82
+ entry.reject(new Error(message));
83
+ }
84
+ pending.clear();
85
+ }
@@ -0,0 +1,21 @@
1
+ /** Minimal WS surface used by the endpoint (real `ws` or test mock). */
2
+ export interface OneBot11WsSocket {
3
+ readonly readyState: number;
4
+ send(data: string): void;
5
+ close(code?: number, reason?: string): void;
6
+ ping?(): void;
7
+ on(event: 'open' | 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
8
+ }
9
+
10
+ export interface OneBot11WsCreateOptions {
11
+ readonly headers?: Record<string, string>;
12
+ }
13
+
14
+ export const WS_OPEN = 1;
15
+
16
+ export interface OneBot11PendingAction {
17
+ resolve: (value: OneBot11ActionResponse) => void;
18
+ reject: (err: Error) => void;
19
+ timeout: NodeJS.Timeout;
20
+ }
21
+ import type { OneBot11ActionResponse } from './protocol.js';
@@ -0,0 +1,17 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+
3
+ export function verifyOneBotAccessToken(
4
+ accessToken: string | undefined,
5
+ request: IncomingMessage,
6
+ ): boolean {
7
+ if (!accessToken) return true;
8
+ const auth = request.headers.authorization ?? '';
9
+ if (auth === `Bearer ${accessToken}`) return true;
10
+ try {
11
+ const url = new URL(request.url ?? '/', 'http://localhost');
12
+ if (url.searchParams.get('access_token') === accessToken) return true;
13
+ } catch {
14
+ /* ignore */
15
+ }
16
+ return false;
17
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * OneBot11 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
3
+ */
4
+ import {
5
+ ClientEndpoint,
6
+ createEndpointLifecycle,
7
+ createRecallEndpointControl,
8
+ type EndpointLifecycle,
9
+ type EndpointControl,
10
+ type EndpointManagement,
11
+ type EndpointSendRequest,
12
+ } from 'zhin.js/adapter';
13
+ import type { HttpHost, WsConnection } from '@zhin.js/host-http';
14
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
15
+ import type { CapabilityId } from 'zhin.js';
16
+ import { createOneBot11EndpointManagement } from './endpoint-management.js';
17
+ import {
18
+ buildSendAction,
19
+ formatInboundContent,
20
+ formatInboundMetadata,
21
+ formatOutboundSegments,
22
+ isMessageEvent,
23
+ onebot11InboundConversation,
24
+ senderDisplayName,
25
+ senderUserId,
26
+ type OneBot11Event,
27
+ type OneBot11WssConfig,
28
+ } from './protocol.js';
29
+ import { receiveOneBot11SideEvent } from './side-event-dispatch.js';
30
+ import { createOneBot11ContentPort } from './content-port.js';
31
+ import { verifyOneBotAccessToken } from './wss-auth.js';
32
+ import {
33
+ callOneBot11WsAction,
34
+ handleOneBot11WsMessage,
35
+ rejectAllPending,
36
+ } from './ws-transport.js';
37
+ import { OneBot11WsEndpoint } from './ws-endpoint.js';
38
+ import {
39
+ type OneBot11PendingAction,
40
+ type OneBot11WsSocket,
41
+ } from './ws-types.js';
42
+ import { callOnebot11Client, createOnebot11EndpointClient, forwardOnebot11ClientEvents, type Onebot11Client } from './client.js';
43
+
44
+ export interface OneBot11WssEndpointOptions {
45
+ readonly id: CapabilityId;
46
+ readonly http: HttpHost;
47
+ readonly config: OneBot11WssConfig;
48
+ }
49
+
50
+ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
51
+ readonly client: Onebot11Client;
52
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
53
+
54
+ readonly #options: OneBot11WssEndpointOptions;
55
+ readonly management: EndpointManagement;
56
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
57
+ readonly content;
58
+ #ws?: OneBot11WsSocket;
59
+ #wsRelease?: () => void;
60
+ readonly #connectionLifecycle: EndpointLifecycle;
61
+ #connectionTask = Promise.resolve();
62
+ #requestId = { value: 0 };
63
+ #pending = new Map<string, OneBot11PendingAction>();
64
+ #started = false;
65
+
66
+ constructor(options: OneBot11WssEndpointOptions) {
67
+ super();
68
+ this.#logger = getAdapterLogger('onebot11', options.config.id);
69
+ this.#options = options;
70
+ this.#connectionLifecycle = createEndpointLifecycle({
71
+ name: `${options.config.id}:inbound`,
72
+ reconnect: false,
73
+ });
74
+ this.client = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
75
+ const callApi = (action: string, params?: Record<string, unknown>) => callOnebot11Client(this.client, action, params);
76
+ this.management = createOneBot11EndpointManagement({ callApi });
77
+ this.content = createOneBot11ContentPort(callApi);
78
+ this.bindClientEvents(
79
+ (receive) => forwardOnebot11ClientEvents(this.client, receive),
80
+ (name, payload) => {
81
+ if (name === 'event') this.#admitRaw(payload as OneBot11Event);
82
+ },
83
+ (_name, error) => this.#warnPlatformEvent(error),
84
+ );
85
+ }
86
+
87
+ async start(): Promise<void> {
88
+ if (this.#started) return;
89
+ if (!this.#options.config.access_token) {
90
+ // wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
91
+ this.#logger.warn(formatCompact({
92
+ endpoint: this.#options.config.id,
93
+ mode: 'wss',
94
+ ok: false,
95
+ error: 'missing access_token',
96
+ }));
97
+ }
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
+ });
109
+ });
110
+ this.#started = true;
111
+ this.#logger.info(formatCompact({
112
+ op: 'listen',
113
+ endpoint: this.#options.config.id,
114
+ mode: 'wss',
115
+ path: this.#options.config.path,
116
+ }));
117
+ }
118
+
119
+ async stop(): Promise<void> {
120
+ this.close();
121
+ this.#wsRelease?.();
122
+ this.#wsRelease = undefined;
123
+ await this.#connectionTask;
124
+ await this.#connectionLifecycle.stop();
125
+ rejectAllPending(this.#pending);
126
+ this.#ws = undefined;
127
+ this.#started = false;
128
+ }
129
+
130
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
131
+ const message = formatOutboundSegments(payload);
132
+ const { action, params } = buildSendAction(conversation, message);
133
+ const data = await callOnebot11Client<{ message_id?: number | string }>(this.client, action, params);
134
+ return data?.message_id != null ? String(data.message_id) : '';
135
+ }
136
+
137
+ async recallMessage(messageId: string): Promise<void> {
138
+ if (!messageId) return;
139
+ await callOnebot11Client(this.client, 'delete_msg', { message_id: Number(messageId) });
140
+ }
141
+
142
+ #callApi(
143
+ action: string,
144
+ params: Record<string, unknown> = {},
145
+ ): ReturnType<typeof callOneBot11WsAction> {
146
+ return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
147
+ }
148
+
149
+ #admitRaw(ev: OneBot11Event): void {
150
+ if (!isMessageEvent(ev)) {
151
+ receiveOneBot11SideEvent(
152
+ (name, payload) => this.emit(name, payload),
153
+ this.#options.config.id,
154
+ { callApi: (action, params) => callOnebot11Client(this.client, action, params) },
155
+ ev,
156
+ this.#logger,
157
+ );
158
+ return;
159
+ }
160
+ const conversation = onebot11InboundConversation(String(this.#options.id), ev);
161
+ const inbound = formatInboundMetadata(ev, this.#options.config.id);
162
+ void this.emit('message.receive', {
163
+ conversation,
164
+ message: { conversation, id: String(ev.message_id) },
165
+ content: formatInboundContent(ev),
166
+ sender: {
167
+ id: senderUserId(ev),
168
+ name: senderDisplayName(ev) || undefined,
169
+ ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
170
+ },
171
+ endpointId: inbound.endpointId,
172
+ ...(inbound.mentioned ? { mentioned: true } : {}),
173
+ ...(inbound.replyTo ? { replyTo: inbound.replyTo } : {}),
174
+ metadata: inbound.metadata,
175
+ }).catch((err) => {
176
+ this.#logger.warn(formatCompact({
177
+ op: 'onebot11_gateway_receive_failed',
178
+ target: `${conversation.kind}:${conversation.id}`,
179
+ error: err instanceof Error ? err.message : String(err),
180
+ }));
181
+ });
182
+ }
183
+
184
+ #warnPlatformEvent(error: unknown): void {
185
+ this.#logger.warn(formatCompact({
186
+ op: 'onebot11_platform_event_failed',
187
+ error: error instanceof Error ? error.message : String(error),
188
+ }));
189
+ }
190
+
191
+ async #acceptConnection(connection: WsConnection): Promise<void> {
192
+ if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
193
+ connection.socket.close(4003, 'Unauthorized');
194
+ return;
195
+ }
196
+ const socket = connection.socket as unknown as OneBot11WsSocket;
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'));
231
+ });
232
+ });
233
+ this.#logger.debug(formatCompact({
234
+ endpoint: this.#options.config.id,
235
+ mode: 'wss',
236
+ peer: connection.request.socket.remoteAddress,
237
+ }));
238
+ }
239
+ }