@zhin.js/adapter-onebot11 1.0.97 → 1.1.0
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 +654 -0
- package/README.md +58 -368
- package/adapters/onebot11.js +40 -0
- package/adapters/onebot11.ts +53 -0
- package/agent/PERMITS.md +14 -0
- package/{skills/onebot11/SKILL.md → agent/skills/onebot11.md} +4 -4
- package/agent/tools/set_title.ts +24 -0
- package/commands/endpoint/add/[id].js +3 -0
- package/commands/endpoint/add/[id].ts +3 -0
- package/commands/endpoint/list.js +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[id].js +3 -0
- package/commands/endpoint/remove/[id].ts +3 -0
- package/lib/client.d.ts +20 -0
- package/lib/client.js +30 -0
- package/lib/content-port.d.ts +4 -0
- package/lib/content-port.js +63 -0
- package/lib/endpoint-management.d.ts +15 -0
- package/lib/endpoint-management.js +47 -0
- package/lib/index.d.ts +10 -16
- package/lib/index.js +7 -53
- package/lib/onebot11-endpoint-commands.d.ts +1 -0
- package/lib/onebot11-endpoint-commands.js +24 -0
- package/lib/onebot11-runtime-state.d.ts +1 -0
- package/lib/onebot11-runtime-state.js +6 -0
- package/lib/protocol.d.ts +160 -0
- package/lib/protocol.js +316 -0
- package/lib/side-event-dispatch.d.ts +7 -0
- package/lib/side-event-dispatch.js +36 -0
- package/lib/ws-endpoint.d.ts +22 -0
- package/lib/ws-endpoint.js +209 -0
- package/lib/ws-transport.d.ts +13 -0
- package/lib/ws-transport.js +71 -0
- package/lib/ws-types.d.ts +18 -0
- package/lib/ws-types.js +1 -0
- package/lib/wss-auth.d.ts +2 -0
- package/lib/wss-auth.js +16 -0
- package/lib/wss-endpoint.d.ts +25 -0
- package/lib/wss-endpoint.js +173 -0
- package/package.json +69 -22
- package/plugin.js +18 -0
- package/schema.json +97 -0
- package/src/client.ts +78 -0
- package/src/content-port.ts +66 -0
- package/src/endpoint-management.ts +72 -0
- package/src/index.ts +62 -65
- package/src/onebot11-endpoint-commands.ts +23 -0
- package/src/onebot11-runtime-state.ts +7 -0
- package/src/protocol.ts +459 -0
- package/src/side-event-dispatch.ts +48 -0
- package/src/ws-endpoint.ts +272 -0
- package/src/ws-transport.ts +100 -0
- package/src/ws-types.ts +21 -0
- package/src/wss-auth.ts +17 -0
- package/src/wss-endpoint.ts +228 -0
- package/lib/adapter.d.ts +0 -32
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -92
- package/lib/adapter.js.map +0 -1
- package/lib/bot-ws-client.d.ts +0 -40
- package/lib/bot-ws-client.d.ts.map +0 -1
- package/lib/bot-ws-client.js +0 -395
- package/lib/bot-ws-client.js.map +0 -1
- package/lib/bot-ws-server.d.ts +0 -42
- package/lib/bot-ws-server.d.ts.map +0 -1
- package/lib/bot-ws-server.js +0 -344
- package/lib/bot-ws-server.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/onebot-get-msg.d.ts +0 -3
- package/lib/onebot-get-msg.d.ts.map +0 -1
- package/lib/onebot-get-msg.js +0 -27
- package/lib/onebot-get-msg.js.map +0 -1
- package/lib/types.d.ts +0 -54
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -5
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -104
- package/src/bot-ws-client.ts +0 -420
- package/src/bot-ws-server.ts +0 -382
- package/src/onebot-get-msg.ts +0 -33
- 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,100 @@
|
|
|
1
|
+
import { clearInterval, clearTimeout, setInterval, 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 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
|
+
export function rejectAllPending(
|
|
92
|
+
pending: Map<string, OneBot11PendingAction>,
|
|
93
|
+
message = '连接已关闭',
|
|
94
|
+
): void {
|
|
95
|
+
for (const [, entry] of pending) {
|
|
96
|
+
clearTimeout(entry.timeout);
|
|
97
|
+
entry.reject(new Error(message));
|
|
98
|
+
}
|
|
99
|
+
pending.clear();
|
|
100
|
+
}
|
package/src/ws-types.ts
ADDED
|
@@ -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';
|
package/src/wss-auth.ts
ADDED
|
@@ -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,228 @@
|
|
|
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 #lifecycle: EndpointLifecycle;
|
|
61
|
+
#requestId = { value: 0 };
|
|
62
|
+
#pending = new Map<string, OneBot11PendingAction>();
|
|
63
|
+
|
|
64
|
+
constructor(options: OneBot11WssEndpointOptions) {
|
|
65
|
+
super();
|
|
66
|
+
this.#logger = getAdapterLogger('onebot11', options.config.id);
|
|
67
|
+
this.#options = options;
|
|
68
|
+
this.#lifecycle = createEndpointLifecycle({
|
|
69
|
+
name: options.config.id,
|
|
70
|
+
reconnect: false,
|
|
71
|
+
heartbeat: { intervalMs: options.config.heartbeat_interval },
|
|
72
|
+
});
|
|
73
|
+
this.client = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
|
|
74
|
+
const callApi = (action: string, params?: Record<string, unknown>) => callOnebot11Client(this.client, action, params);
|
|
75
|
+
this.management = createOneBot11EndpointManagement({ callApi });
|
|
76
|
+
this.content = createOneBot11ContentPort(callApi);
|
|
77
|
+
this.bindClientEvents(
|
|
78
|
+
(receive) => forwardOnebot11ClientEvents(this.client, receive),
|
|
79
|
+
(name, payload) => {
|
|
80
|
+
if (name === 'event') this.#admitRaw(payload as OneBot11Event);
|
|
81
|
+
},
|
|
82
|
+
(_name, error) => this.#warnPlatformEvent(error),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async start(): Promise<void> {
|
|
87
|
+
if (!this.#options.config.access_token) {
|
|
88
|
+
// wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
|
|
89
|
+
this.#logger.warn(formatCompact({
|
|
90
|
+
endpoint: this.#options.config.id,
|
|
91
|
+
mode: 'wss',
|
|
92
|
+
ok: false,
|
|
93
|
+
error: 'missing access_token',
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
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
|
+
});
|
|
111
|
+
});
|
|
112
|
+
this.#logger.info(formatCompact({
|
|
113
|
+
op: 'listen',
|
|
114
|
+
endpoint: this.#options.config.id,
|
|
115
|
+
mode: 'wss',
|
|
116
|
+
path: this.#options.config.path,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async stop(): Promise<void> {
|
|
121
|
+
this.close();
|
|
122
|
+
await this.#lifecycle.stop();
|
|
123
|
+
rejectAllPending(this.#pending);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
127
|
+
const message = formatOutboundSegments(payload);
|
|
128
|
+
const { action, params } = buildSendAction(conversation, message);
|
|
129
|
+
const data = await callOnebot11Client<{ message_id?: number | string }>(this.client, action, params);
|
|
130
|
+
return data?.message_id != null ? String(data.message_id) : '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async recallMessage(messageId: string): Promise<void> {
|
|
134
|
+
if (!messageId) return;
|
|
135
|
+
await callOnebot11Client(this.client, 'delete_msg', { message_id: Number(messageId) });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#callApi(
|
|
139
|
+
action: string,
|
|
140
|
+
params: Record<string, unknown> = {},
|
|
141
|
+
): ReturnType<typeof callOneBot11WsAction> {
|
|
142
|
+
return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#admitRaw(ev: OneBot11Event): void {
|
|
146
|
+
if (!isMessageEvent(ev)) {
|
|
147
|
+
receiveOneBot11SideEvent(
|
|
148
|
+
(name, payload) => this.emit(name, payload),
|
|
149
|
+
this.#options.config.id,
|
|
150
|
+
{ callApi: (action, params) => callOnebot11Client(this.client, action, params) },
|
|
151
|
+
ev,
|
|
152
|
+
this.#logger,
|
|
153
|
+
);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const conversation = onebot11InboundConversation(String(this.#options.id), ev);
|
|
157
|
+
const inbound = formatInboundMetadata(ev, this.#options.config.id);
|
|
158
|
+
void this.emit('message.receive', {
|
|
159
|
+
conversation,
|
|
160
|
+
message: { conversation, id: String(ev.message_id) },
|
|
161
|
+
content: formatInboundContent(ev),
|
|
162
|
+
sender: {
|
|
163
|
+
id: senderUserId(ev),
|
|
164
|
+
name: senderDisplayName(ev) || undefined,
|
|
165
|
+
...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
|
|
166
|
+
},
|
|
167
|
+
endpointId: inbound.endpointId,
|
|
168
|
+
...(inbound.mentioned ? { mentioned: true } : {}),
|
|
169
|
+
...(inbound.replyTo ? { replyTo: inbound.replyTo } : {}),
|
|
170
|
+
metadata: inbound.metadata,
|
|
171
|
+
}).catch((err) => {
|
|
172
|
+
this.#logger.warn(formatCompact({
|
|
173
|
+
op: 'onebot11_gateway_receive_failed',
|
|
174
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
175
|
+
error: err instanceof Error ? err.message : String(err),
|
|
176
|
+
}));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#warnPlatformEvent(error: unknown): void {
|
|
181
|
+
this.#logger.warn(formatCompact({
|
|
182
|
+
op: 'onebot11_platform_event_failed',
|
|
183
|
+
error: error instanceof Error ? error.message : String(error),
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#acceptConnection(connection: WsConnection): void {
|
|
188
|
+
if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
|
|
189
|
+
connection.socket.close(4003, 'Unauthorized');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
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]),
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
socket.on('close', () => {
|
|
217
|
+
if (this.#ws === socket) {
|
|
218
|
+
this.#ws = undefined;
|
|
219
|
+
this.#lifecycle.stopHeartbeat();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
this.#logger.debug(formatCompact({
|
|
223
|
+
endpoint: this.#options.config.id,
|
|
224
|
+
mode: 'wss',
|
|
225
|
+
peer: connection.request.socket.remoteAddress,
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
}
|