@zhin.js/adapter-onebot12 3.0.1 → 3.0.3

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 (70) hide show
  1. package/CHANGELOG.md +440 -0
  2. package/README.md +38 -68
  3. package/adapters/onebot12.ts +50 -0
  4. package/lib/index.d.ts +6 -19
  5. package/lib/index.js +5 -27
  6. package/lib/protocol.d.ts +151 -0
  7. package/lib/protocol.js +242 -0
  8. package/lib/webhook.d.ts +25 -0
  9. package/lib/webhook.js +140 -0
  10. package/lib/ws-endpoint.d.ts +25 -0
  11. package/lib/ws-endpoint.js +240 -0
  12. package/lib/ws-types.d.ts +16 -0
  13. package/lib/ws-types.js +1 -0
  14. package/lib/wss-auth.d.ts +2 -0
  15. package/lib/wss-auth.js +16 -0
  16. package/lib/wss-endpoint.d.ts +24 -0
  17. package/lib/wss-endpoint.js +193 -0
  18. package/package.json +42 -16
  19. package/plugin.ts +8 -0
  20. package/schema.json +40 -0
  21. package/src/index.ts +46 -37
  22. package/src/protocol.ts +387 -0
  23. package/src/webhook.ts +180 -0
  24. package/src/ws-endpoint.ts +287 -0
  25. package/src/ws-types.ts +19 -0
  26. package/src/wss-auth.ts +17 -0
  27. package/src/wss-endpoint.ts +226 -0
  28. package/lib/adapter.d.ts +0 -20
  29. package/lib/adapter.d.ts.map +0 -1
  30. package/lib/adapter.js +0 -43
  31. package/lib/adapter.js.map +0 -1
  32. package/lib/api.d.ts +0 -14
  33. package/lib/api.d.ts.map +0 -1
  34. package/lib/api.js +0 -34
  35. package/lib/api.js.map +0 -1
  36. package/lib/endpoint-webhook.d.ts +0 -25
  37. package/lib/endpoint-webhook.d.ts.map +0 -1
  38. package/lib/endpoint-webhook.js +0 -122
  39. package/lib/endpoint-webhook.js.map +0 -1
  40. package/lib/endpoint-ws.d.ts +0 -26
  41. package/lib/endpoint-ws.d.ts.map +0 -1
  42. package/lib/endpoint-ws.js +0 -214
  43. package/lib/endpoint-ws.js.map +0 -1
  44. package/lib/endpoint-wss.d.ts +0 -26
  45. package/lib/endpoint-wss.d.ts.map +0 -1
  46. package/lib/endpoint-wss.js +0 -191
  47. package/lib/endpoint-wss.js.map +0 -1
  48. package/lib/index.d.ts.map +0 -1
  49. package/lib/index.js.map +0 -1
  50. package/lib/segment-mapper.d.ts +0 -2
  51. package/lib/segment-mapper.d.ts.map +0 -1
  52. package/lib/segment-mapper.js +0 -2
  53. package/lib/segment-mapper.js.map +0 -1
  54. package/lib/types.d.ts +0 -77
  55. package/lib/types.d.ts.map +0 -1
  56. package/lib/types.js +0 -5
  57. package/lib/types.js.map +0 -1
  58. package/lib/utils.d.ts +0 -45
  59. package/lib/utils.d.ts.map +0 -1
  60. package/lib/utils.js +0 -62
  61. package/lib/utils.js.map +0 -1
  62. package/src/adapter.ts +0 -56
  63. package/src/api.ts +0 -48
  64. package/src/endpoint-webhook.ts +0 -136
  65. package/src/endpoint-ws.ts +0 -228
  66. package/src/endpoint-wss.ts +0 -208
  67. package/src/segment-mapper.ts +0 -1
  68. package/src/types.ts +0 -86
  69. package/src/utils.ts +0 -89
  70. /package/{skills/onebot12/SKILL.md → agent/skills/onebot12.md} +0 -0
package/src/index.ts CHANGED
@@ -1,40 +1,49 @@
1
- /**
2
- * OneBot 12 适配器入口:单一适配器,支持正向 WS / Webhook / 反向 WS
3
- * 协议文档 https://12.onebot.dev/
4
- */
5
- import { usePlugin, type Plugin, type Context } from 'zhin.js';
6
- import type { Router } from '@zhin.js/host-router';
7
- import { OneBot12Adapter } from './adapter.js';
1
+ export {
2
+ buildSendMessageParams,
3
+ buildWsConnectOptions,
4
+ callOneBot12Action,
5
+ formatInboundContent,
6
+ formatInboundTarget,
7
+ formatOutboundSegments,
8
+ getChannelId,
9
+ isBotMentioned,
10
+ isMessageEvent,
11
+ parseSendTarget,
12
+ resolveOneBot12Config,
13
+ senderNickname,
14
+ senderUserId,
15
+ type OneBot12ActionRequest,
16
+ type OneBot12ActionResponse,
17
+ type OneBot12AdapterConfig,
18
+ type OneBot12ConfigBase,
19
+ type OneBot12EndpointConfig,
20
+ type OneBot12Event,
21
+ type OneBot12HttpOptions,
22
+ type OneBot12Segment,
23
+ type OneBot12Self,
24
+ type OneBot12WebhookConfig,
25
+ type OneBot12WireSegment,
26
+ type OneBot12WsConfig,
27
+ type OneBot12WssConfig,
28
+ type ParsedSendTarget,
29
+ type ResolvedOneBot12Config,
30
+ } from './protocol.js';
8
31
 
9
- export * from './types.js';
10
- export { callOneBot12Action } from './api.js';
11
- export * from './utils.js';
12
- export { OneBot12WsClient } from './endpoint-ws.js';
13
- export { OneBot12WebhookEndpoint } from './endpoint-webhook.js';
14
- export { OneBot12WssServer } from './endpoint-wss.js';
15
- export { OneBot12Adapter, type OneBot12Bot } from './adapter.js';
32
+ export {
33
+ OneBot12WebhookEndpoint,
34
+ type OneBot12WebhookEndpointOptions,
35
+ } from './webhook.js';
16
36
 
17
- declare module 'zhin.js' {
18
- namespace Plugin {
19
- interface Contexts {
20
- router: import('@zhin.js/host-router').Router;
21
- }
22
- }
23
- interface Adapters {
24
- onebot12: OneBot12Adapter;
25
- }
26
- }
37
+ export {
38
+ OneBot12WsEndpoint,
39
+ type OneBot12WsEndpointOptions,
40
+ } from './ws-endpoint.js';
27
41
 
28
- const { provide } = usePlugin();
29
- provide({
30
- name: 'onebot12',
31
- description: 'OneBot 12 协议适配器(正向 WS / Webhook / 反向 WS)',
32
- mounted: async (p: Plugin) => {
33
- const adapter = new OneBot12Adapter(p);
34
- await adapter.start();
35
- return adapter;
36
- },
37
- dispose: async (adapter: OneBot12Adapter) => {
38
- await adapter.stop();
39
- },
40
- } as unknown as Context<'onebot12'>);
42
+ export {
43
+ OneBot12WssEndpoint,
44
+ type OneBot12WssEndpointOptions,
45
+ } from './wss-endpoint.js';
46
+
47
+ export type { OneBot12WsSocket, OneBot12WsCreateOptions } from './ws-types.js';
48
+
49
+ export { verifyOneBotAccessToken } from './wss-auth.js';
@@ -0,0 +1,387 @@
1
+ /**
2
+ * OneBot 12 protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ * Spec: https://12.onebot.dev/
5
+ */
6
+
7
+ /** Transitional legacy endpoint row (`endpoints[]` with `context: onebot12`). */
8
+ export interface OneBot12LegacyEndpointRow {
9
+ readonly context?: string;
10
+ readonly connection?: 'ws' | 'webhook' | 'wss';
11
+ readonly name?: string;
12
+ readonly access_token?: string;
13
+ readonly url?: string;
14
+ readonly path?: string;
15
+ readonly api_url?: string;
16
+ readonly reconnect_interval?: number;
17
+ readonly heartbeat_interval?: number;
18
+ }
19
+
20
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
21
+ export interface OneBot12AdapterConfig {
22
+ readonly connection?: 'ws' | 'webhook' | 'wss';
23
+ readonly name?: string;
24
+ readonly access_token?: string;
25
+ readonly url?: string;
26
+ readonly path?: string;
27
+ readonly api_url?: string;
28
+ readonly reconnect_interval?: number;
29
+ readonly heartbeat_interval?: number;
30
+ /** Transitional: legacy root `endpoints[]` with `context: onebot12`. */
31
+ readonly endpoints?: ReadonlyArray<OneBot12LegacyEndpointRow>;
32
+ }
33
+
34
+ export interface OneBot12ConfigBase {
35
+ readonly context: 'onebot12';
36
+ readonly name: string;
37
+ readonly access_token?: string;
38
+ }
39
+
40
+ /** 正向 WebSocket:应用连 OneBot 实现的 WS 服务器 */
41
+ export interface OneBot12WsConfig extends OneBot12ConfigBase {
42
+ readonly connection: 'ws';
43
+ readonly url: string;
44
+ readonly reconnect_interval: number;
45
+ readonly heartbeat_interval: number;
46
+ }
47
+
48
+ /** HTTP Webhook:httpHostToken POST 入站 + api_url HTTP 出站 */
49
+ export interface OneBot12WebhookConfig extends OneBot12ConfigBase {
50
+ readonly connection: 'webhook';
51
+ readonly path: string;
52
+ readonly api_url?: string;
53
+ }
54
+
55
+ /** 反向 WebSocket:httpHostToken WS upgrade 入站/出站 */
56
+ export interface OneBot12WssConfig extends OneBot12ConfigBase {
57
+ readonly connection: 'wss';
58
+ readonly path: string;
59
+ readonly heartbeat_interval: number;
60
+ }
61
+
62
+ export type ResolvedOneBot12Config = OneBot12WsConfig | OneBot12WebhookConfig | OneBot12WssConfig;
63
+ export type OneBot12EndpointConfig = ResolvedOneBot12Config;
64
+
65
+ export interface OneBot12Self {
66
+ readonly platform: string;
67
+ readonly user_id: string;
68
+ }
69
+
70
+ export interface OneBot12Event {
71
+ id: string;
72
+ time: number;
73
+ type: 'meta' | 'message' | 'notice' | 'request';
74
+ detail_type: string;
75
+ sub_type: string;
76
+ self?: OneBot12Self;
77
+ message_id?: string;
78
+ message?: OneBot12Segment[];
79
+ alt_message?: string;
80
+ user_id?: string;
81
+ group_id?: string;
82
+ channel_id?: string;
83
+ guild_id?: string;
84
+ [key: string]: unknown;
85
+ }
86
+
87
+ export interface OneBot12Segment {
88
+ type: string;
89
+ data?: Record<string, unknown>;
90
+ }
91
+
92
+ export interface OneBot12ActionRequest {
93
+ action: string;
94
+ params: Record<string, unknown>;
95
+ echo?: string;
96
+ self?: OneBot12Self;
97
+ }
98
+
99
+ export interface OneBot12ActionResponse {
100
+ status: 'ok' | 'failed';
101
+ retcode: number;
102
+ data?: unknown;
103
+ message: string;
104
+ echo?: string;
105
+ }
106
+
107
+ export interface OneBot12HttpOptions {
108
+ readonly url: string;
109
+ readonly access_token?: string;
110
+ }
111
+
112
+ export interface OneBot12WireSegment {
113
+ readonly type: string;
114
+ readonly data?: Record<string, unknown>;
115
+ }
116
+
117
+ export interface ParsedSendTarget {
118
+ readonly detail_type: 'private' | 'group' | 'channel';
119
+ readonly id: string;
120
+ readonly guild_id?: string;
121
+ }
122
+
123
+ export function resolveOneBot12Config(config: OneBot12AdapterConfig = {}): ResolvedOneBot12Config {
124
+ const entry = config.endpoints?.find((item) => item.context === 'onebot12');
125
+ const connection = config.connection
126
+ ?? entry?.connection
127
+ ?? 'ws';
128
+ const name = (typeof config.name === 'string' && config.name)
129
+ || (typeof entry?.name === 'string' && entry.name)
130
+ || process.env.ONEBOT12_BOT_NAME
131
+ || 'onebot12-bot';
132
+ const access_token = config.access_token ?? entry?.access_token;
133
+
134
+ if (connection === 'ws') {
135
+ const url = config.url ?? entry?.url;
136
+ if (!url) {
137
+ throw new TypeError(
138
+ 'OneBot12 connection:ws requires url (plugins.<key>.url or endpoints with context: onebot12)',
139
+ );
140
+ }
141
+ return {
142
+ context: 'onebot12',
143
+ connection: 'ws',
144
+ name,
145
+ access_token,
146
+ url,
147
+ reconnect_interval: config.reconnect_interval ?? entry?.reconnect_interval ?? 5000,
148
+ heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
149
+ };
150
+ }
151
+
152
+ if (connection === 'webhook') {
153
+ const path = config.path ?? entry?.path;
154
+ if (!path) {
155
+ throw new TypeError('OneBot12 connection:webhook requires path');
156
+ }
157
+ return {
158
+ context: 'onebot12',
159
+ connection: 'webhook',
160
+ name,
161
+ access_token,
162
+ path,
163
+ api_url: config.api_url ?? entry?.api_url,
164
+ };
165
+ }
166
+
167
+ if (connection === 'wss') {
168
+ const path = config.path ?? entry?.path;
169
+ if (!path) {
170
+ throw new TypeError('OneBot12 connection:wss requires path');
171
+ }
172
+ return {
173
+ context: 'onebot12',
174
+ connection: 'wss',
175
+ name,
176
+ access_token,
177
+ path,
178
+ heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
179
+ };
180
+ }
181
+
182
+ throw new TypeError(`Unknown OneBot12 connection: ${String(connection)}`);
183
+ }
184
+
185
+ /** 判断是否为消息事件(type=message) */
186
+ export function isMessageEvent(
187
+ ev: OneBot12Event,
188
+ ): ev is OneBot12Event & { message_id: string; message?: OneBot12Segment[] } {
189
+ return ev.type === 'message' && !!ev.message_id;
190
+ }
191
+
192
+ /** 从事件得到场景 id:私聊 user_id,群 group_id,频道 channel_id 或 guild_id:channel_id */
193
+ export function getChannelId(ev: OneBot12Event): string {
194
+ if (ev.detail_type === 'private' && ev.user_id) return ev.user_id;
195
+ if (ev.detail_type === 'group' && ev.group_id) return ev.group_id;
196
+ if (ev.detail_type === 'channel' && ev.channel_id) {
197
+ return ev.guild_id ? `${ev.guild_id}:${ev.channel_id}` : ev.channel_id;
198
+ }
199
+ return ev.user_id ?? ev.group_id ?? '';
200
+ }
201
+
202
+ /**
203
+ * Gateway reply target:`detail_type:channelId`,便于 send() 还原动作参数。
204
+ */
205
+ export function formatInboundTarget(ev: OneBot12Event): string {
206
+ const detail = ev.detail_type === 'private' || ev.detail_type === 'group' || ev.detail_type === 'channel'
207
+ ? ev.detail_type
208
+ : 'private';
209
+ return `${detail}:${getChannelId(ev)}`;
210
+ }
211
+
212
+ export function parseSendTarget(target: string): ParsedSendTarget {
213
+ const sep = target.indexOf(':');
214
+ if (sep <= 0) {
215
+ return { detail_type: 'private', id: target };
216
+ }
217
+ const head = target.slice(0, sep);
218
+ const rest = target.slice(sep + 1);
219
+ if (head === 'private' || head === 'group') {
220
+ return { detail_type: head, id: rest };
221
+ }
222
+ if (head === 'channel') {
223
+ const guildSep = rest.indexOf(':');
224
+ if (guildSep > 0) {
225
+ return {
226
+ detail_type: 'channel',
227
+ guild_id: rest.slice(0, guildSep),
228
+ id: rest.slice(guildSep + 1),
229
+ };
230
+ }
231
+ return { detail_type: 'channel', id: rest };
232
+ }
233
+ return { detail_type: 'private', id: target };
234
+ }
235
+
236
+ /** Build inbound text for MessageGateway.receive */
237
+ export function formatInboundContent(ev: OneBot12Event): string {
238
+ if (typeof ev.alt_message === 'string' && ev.alt_message) return ev.alt_message;
239
+ if (Array.isArray(ev.message)) {
240
+ return ev.message
241
+ .map((seg) => (seg.type === 'text' ? String(seg.data?.text ?? '') : ''))
242
+ .join('');
243
+ }
244
+ return '';
245
+ }
246
+
247
+ /**
248
+ * Runtime Message sender 必须是用户 ID(agent bridge 以 sender 与 endpointMaster 比对)。
249
+ * 显示名经 {@link senderNickname} 放入 metadata.nickname。
250
+ */
251
+ export function senderUserId(ev: OneBot12Event): string {
252
+ return ev.user_id ?? '';
253
+ }
254
+
255
+ /** 事件携带的发送者显示名(`user.name` / `qq.nickname`),没有则返回 undefined。 */
256
+ export function senderNickname(ev: OneBot12Event): string | undefined {
257
+ const record = ev as Record<string, unknown>;
258
+ const name = record['user.name'] ?? record['qq.nickname'];
259
+ if (typeof name === 'string' && name) return name;
260
+ return undefined;
261
+ }
262
+
263
+ /**
264
+ * 判断入站消息是否 @ 了本机:OneBot12 提及段为 `{type:'mention', data:{user_id}}`,
265
+ * 目标 user_id 等于事件 self.user_id(self 为 {platform, user_id} 对象)时视为提及。
266
+ * 新 Plugin Runtime 的 Message.content 为纯文本,mention 信息只能经 metadata.mentioned 传递。
267
+ */
268
+ export function isBotMentioned(ev: OneBot12Event): boolean {
269
+ const selfId = ev.self?.user_id;
270
+ if (!selfId || !Array.isArray(ev.message)) return false;
271
+ return ev.message.some(
272
+ (seg) => seg.type === 'mention' && String(seg.data?.['user_id'] ?? '') === selfId,
273
+ );
274
+ }
275
+
276
+ /**
277
+ * Wire-encode an already-rendered outbound payload into OneBot 12 message segments.
278
+ * Segment canonicalization is intentionally not done here.
279
+ */
280
+ export function formatOutboundSegments(payload: unknown): OneBot12Segment[] {
281
+ if (typeof payload === 'string') {
282
+ return [{ type: 'text', data: { text: payload } }];
283
+ }
284
+
285
+ const items: Array<string | OneBot12WireSegment> = Array.isArray(payload)
286
+ ? payload as Array<string | OneBot12WireSegment>
287
+ : payload && typeof payload === 'object' && 'type' in (payload as object)
288
+ ? [payload as OneBot12WireSegment]
289
+ : [];
290
+
291
+ if (items.length === 0) {
292
+ const text = payload == null
293
+ ? ''
294
+ : typeof payload === 'object'
295
+ ? JSON.stringify(payload)
296
+ : String(payload);
297
+ return [{ type: 'text', data: { text } }];
298
+ }
299
+
300
+ const segs: OneBot12Segment[] = [];
301
+ for (const item of items) {
302
+ if (typeof item === 'string') {
303
+ segs.push({ type: 'text', data: { text: item } });
304
+ continue;
305
+ }
306
+ segs.push({ type: item.type, data: item.data ?? {} });
307
+ }
308
+ return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
309
+ }
310
+
311
+ export function buildSendMessageParams(
312
+ target: string,
313
+ message: OneBot12Segment[],
314
+ ): Record<string, unknown> {
315
+ const parsed = parseSendTarget(target);
316
+ const params: Record<string, unknown> = {
317
+ message,
318
+ detail_type: parsed.detail_type,
319
+ };
320
+ if (parsed.detail_type === 'private') {
321
+ params.user_id = parsed.id;
322
+ } else if (parsed.detail_type === 'group') {
323
+ params.group_id = parsed.id;
324
+ } else {
325
+ params.channel_id = parsed.id;
326
+ if (parsed.guild_id) params.guild_id = parsed.guild_id;
327
+ }
328
+ return params;
329
+ }
330
+
331
+ /**
332
+ * 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应。
333
+ * 供 webhook 出站与纯协议测试使用;WS 路径走 WebSocket echo 请求。
334
+ */
335
+ export async function callOneBot12Action(
336
+ options: OneBot12HttpOptions,
337
+ action: string,
338
+ params: Record<string, unknown> = {},
339
+ echo?: string,
340
+ ): Promise<OneBot12ActionResponse> {
341
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
342
+ if (options.access_token) {
343
+ headers.Authorization = `Bearer ${options.access_token}`;
344
+ }
345
+ const body: OneBot12ActionRequest = { action, params };
346
+ if (echo) body.echo = echo;
347
+
348
+ const res = await fetch(options.url, {
349
+ method: 'POST',
350
+ headers,
351
+ body: JSON.stringify(body),
352
+ });
353
+
354
+ const text = await res.text();
355
+ if (res.status === 401) throw new Error(`OneBot12 鉴权失败: ${text}`);
356
+ if (res.status !== 200) throw new Error(`OneBot12 HTTP ${res.status}: ${text}`);
357
+
358
+ let data: OneBot12ActionResponse;
359
+ try {
360
+ data = JSON.parse(text) as OneBot12ActionResponse;
361
+ } catch {
362
+ throw new Error(`OneBot12 无效响应: ${text.slice(0, 200)}`);
363
+ }
364
+ if (data.status === 'failed' && data.retcode !== 0) {
365
+ throw new Error(`OneBot12 动作失败 retcode=${data.retcode}: ${data.message}`);
366
+ }
367
+ return data;
368
+ }
369
+
370
+ /** Build WS connect URL + headers (access_token via Bearer + query). */
371
+ export function buildWsConnectOptions(config: OneBot12WsConfig): {
372
+ readonly url: string;
373
+ readonly headers: Record<string, string>;
374
+ readonly safeUrl: string;
375
+ } {
376
+ const headers: Record<string, string> = {};
377
+ let connectUrl = config.url;
378
+ if (config.access_token) {
379
+ headers.Authorization = `Bearer ${config.access_token}`;
380
+ const url = new URL(config.url);
381
+ url.searchParams.set('access_token', config.access_token);
382
+ connectUrl = url.toString();
383
+ }
384
+ const safeUrl = new URL(connectUrl);
385
+ safeUrl.searchParams.delete('access_token');
386
+ return { url: connectUrl, headers, safeUrl: safeUrl.toString() };
387
+ }
package/src/webhook.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * OneBot12 HTTP webhook endpoint — POST inbound + api_url outbound.
3
+ */
4
+ import type { IncomingMessage, ServerResponse } from 'node:http';
5
+ import type { EndpointInstance } from '@zhin.js/adapter';
6
+ import type { MessageGateway } from '@zhin.js/core/runtime';
7
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
8
+ import { formatCompact, getLogger } from '@zhin.js/logger';
9
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
10
+ import {
11
+ buildSendMessageParams,
12
+ callOneBot12Action,
13
+ formatInboundContent,
14
+ formatInboundTarget,
15
+ formatOutboundSegments,
16
+ isBotMentioned,
17
+ isMessageEvent,
18
+ senderNickname,
19
+ senderUserId,
20
+ type OneBot12Event,
21
+ type OneBot12WebhookConfig,
22
+ } from './protocol.js';
23
+ import { verifyOneBotAccessToken } from './wss-auth.js';
24
+
25
+ const logger = getLogger('onebot12');
26
+
27
+ export interface OneBot12WebhookEndpointOptions {
28
+ readonly id: CapabilityId;
29
+ readonly gateway: MessageGateway;
30
+ readonly http: HttpHost;
31
+ readonly config: OneBot12WebhookConfig;
32
+ readonly callAction?: typeof callOneBot12Action;
33
+ }
34
+
35
+ export class OneBot12WebhookEndpoint implements EndpointInstance {
36
+ readonly #options: OneBot12WebhookEndpointOptions;
37
+ readonly #callAction: typeof callOneBot12Action;
38
+ #routeReleases: HttpRouteRegistration[] = [];
39
+ #open = false;
40
+ #started = false;
41
+
42
+ constructor(options: OneBot12WebhookEndpointOptions) {
43
+ this.#options = options;
44
+ this.#callAction = options.callAction ?? callOneBot12Action;
45
+ }
46
+
47
+ async start(): Promise<void> {
48
+ if (this.#started) return;
49
+ this.#started = true;
50
+ this.#setupRoutes();
51
+ logger.info(formatCompact({
52
+ op: 'listen',
53
+ endpoint: this.#options.config.name,
54
+ mode: 'webhook',
55
+ path: this.#options.config.path,
56
+ }));
57
+ }
58
+
59
+ open(): void {
60
+ this.#open = true;
61
+ }
62
+
63
+ close(): void {
64
+ this.#open = false;
65
+ }
66
+
67
+ async stop(): Promise<void> {
68
+ this.#open = false;
69
+ for (const release of this.#routeReleases.splice(0)) release();
70
+ this.#started = false;
71
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
72
+ }
73
+
74
+ async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
75
+ const apiUrl = this.#options.config.api_url;
76
+ if (!apiUrl) {
77
+ throw new Error('OneBot12 connection:webhook requires api_url for outbound send');
78
+ }
79
+ const message = formatOutboundSegments(payload);
80
+ const params = buildSendMessageParams(target, message);
81
+ const resp = await this.#callAction(
82
+ { url: apiUrl, access_token: this.#options.config.access_token },
83
+ 'send_message',
84
+ params,
85
+ );
86
+ const data = resp.data as { message_id?: string } | undefined;
87
+ const messageId = data?.message_id ?? '';
88
+ logger.debug(formatCompact({
89
+ op: 'onebot12_send',
90
+ endpoint: this.#options.config.name,
91
+ target,
92
+ messageId,
93
+ }));
94
+ return messageId;
95
+ }
96
+
97
+ admit(ev: OneBot12Event): void {
98
+ if (!this.#open || !isMessageEvent(ev)) return;
99
+ const target = formatInboundTarget(ev);
100
+ const content = formatInboundContent(ev);
101
+ const nickname = senderNickname(ev);
102
+ const mentioned = isBotMentioned(ev);
103
+ void this.#options.gateway.receive({
104
+ adapter: this.#options.id,
105
+ target,
106
+ content,
107
+ sender: senderUserId(ev),
108
+ id: ev.message_id,
109
+ metadata: Object.freeze({
110
+ detail_type: ev.detail_type,
111
+ user_id: ev.user_id,
112
+ group_id: ev.group_id,
113
+ channel_id: ev.channel_id,
114
+ guild_id: ev.guild_id,
115
+ endpoint: this.#options.config.name,
116
+ time: ev.time,
117
+ ...(nickname ? { nickname } : {}),
118
+ ...(mentioned ? { mentioned: true } : {}),
119
+ }),
120
+ }).catch((err) => {
121
+ logger.warn(formatCompact({
122
+ op: 'onebot12_gateway_receive_failed',
123
+ target,
124
+ error: err instanceof Error ? err.message : String(err),
125
+ }));
126
+ });
127
+ }
128
+
129
+ #setupRoutes(): void {
130
+ const path = this.#options.config.path;
131
+ this.#routeReleases.push(
132
+ this.#options.http.route('POST', path, async (request, response) => {
133
+ await this.#handleWebhook(request, response);
134
+ }, { summary: 'OneBot12 webhook callback', tags: ['onebot12'] }),
135
+ );
136
+ }
137
+
138
+ async #handleWebhook(request: IncomingMessage, response: ServerResponse): Promise<void> {
139
+ try {
140
+ if (!verifyOneBotAccessToken(this.#options.config.access_token, request)) {
141
+ response.writeHead(403, { 'Content-Type': 'application/json' });
142
+ response.end(JSON.stringify({ message: 'Unauthorized' }));
143
+ return;
144
+ }
145
+ const raw = await readRequestBody(request);
146
+ let ev: OneBot12Event;
147
+ try {
148
+ ev = JSON.parse(raw) as OneBot12Event;
149
+ } catch {
150
+ response.writeHead(400, { 'Content-Type': 'application/json' });
151
+ response.end(JSON.stringify({ message: 'Invalid JSON' }));
152
+ return;
153
+ }
154
+ if (this.#open) this.admit(ev);
155
+ response.writeHead(200, { 'Content-Type': 'application/json' });
156
+ response.end(JSON.stringify({ status: 'ok' }));
157
+ } catch (error) {
158
+ logger.error('OneBot12 webhook error:', error);
159
+ if (!response.headersSent) {
160
+ response.writeHead(500, { 'Content-Type': 'application/json' });
161
+ response.end(JSON.stringify({ message: 'Internal Server Error' }));
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ async function readRequestBody(request: IncomingMessage): Promise<string> {
168
+ const chunks: Buffer[] = [];
169
+ let size = 0;
170
+ for await (const chunk of request) {
171
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
172
+ size += buffer.length;
173
+ if (size > 1_048_576) {
174
+ request.destroy();
175
+ throw new Error('Request body exceeds 1MB');
176
+ }
177
+ chunks.push(buffer);
178
+ }
179
+ return Buffer.concat(chunks).toString('utf8');
180
+ }