@zhin.js/adapter-onebot12 1.1.17 → 1.1.19
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 +62 -0
- package/README.md +33 -4
- package/adapters/onebot12/index.js +3 -32
- package/adapters/onebot12/index.ts +2 -38
- package/commands/onebot12/endpoint/definition.js +2 -2
- package/commands/onebot12/endpoint/definition.ts +2 -2
- package/lib/adapter.d.ts +21 -0
- package/lib/adapter.js +34 -0
- package/lib/client.d.ts +9 -11
- package/lib/client.js +23 -13
- package/lib/console-spec.d.ts +12 -0
- package/lib/console-spec.js +15 -0
- package/lib/console.d.ts +8 -0
- package/lib/console.js +13 -0
- package/lib/content-port.d.ts +2 -2
- package/lib/content-port.js +1 -1
- package/lib/endpoint-management.d.ts +5 -5
- package/lib/endpoint-management.js +17 -17
- package/lib/identity.d.ts +4 -0
- package/lib/identity.js +53 -0
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/protocol.d.ts +32 -4
- package/lib/protocol.js +54 -53
- package/lib/response.d.ts +5 -0
- package/lib/response.js +17 -0
- package/lib/side-event-dispatch.d.ts +8 -1
- package/lib/side-event-dispatch.js +2 -1
- package/lib/webhook.d.ts +5 -11
- package/lib/webhook.js +72 -37
- package/lib/ws-endpoint.d.ts +5 -11
- package/lib/ws-endpoint.js +79 -45
- package/lib/ws-transport.d.ts +18 -10
- package/lib/ws-transport.js +26 -45
- package/lib/ws-types.d.ts +0 -6
- package/lib/wss-endpoint.d.ts +5 -11
- package/lib/wss-endpoint.js +82 -48
- package/package.json +22 -14
- package/pages/index/index.js +15 -0
- package/pages/index/index.ts +16 -0
- package/plugin.js +3 -5
- package/schema.json +15 -3
- package/src/adapter.ts +36 -0
- package/src/client.ts +26 -29
- package/src/console-spec.ts +20 -0
- package/src/console.ts +18 -0
- package/src/content-port.ts +3 -3
- package/src/endpoint-management.ts +25 -24
- package/src/identity.ts +52 -0
- package/src/index.ts +4 -1
- package/src/protocol.ts +71 -62
- package/src/response.ts +19 -0
- package/src/side-event-dispatch.ts +9 -7
- package/src/webhook.ts +70 -55
- package/src/ws-endpoint.ts +75 -63
- package/src/ws-transport.ts +37 -73
- package/src/ws-types.ts +0 -7
- package/src/wss-endpoint.ts +80 -66
- package/lib/onebot12-runtime-state.d.ts +0 -1
- package/lib/onebot12-runtime-state.js +0 -6
- package/src/onebot12-runtime-state.ts +0 -7
package/src/protocol.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { readOnebot12Response } from './response.js';
|
|
1
2
|
/**
|
|
2
3
|
* OneBot 12 protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
|
|
3
4
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
5
|
* Spec: https://12.onebot.dev/
|
|
5
6
|
*/
|
|
6
|
-
import { isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
|
+
import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
8
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
8
9
|
|
|
9
10
|
const logger = getLogger('onebot12');
|
|
10
11
|
|
|
11
12
|
/** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
|
|
12
13
|
export interface OneBot12EndpointConfig {
|
|
14
|
+
readonly platform: string;
|
|
15
|
+
readonly userId: string;
|
|
13
16
|
readonly connection?: 'ws' | 'webhook' | 'wss';
|
|
14
17
|
readonly id: string;
|
|
15
18
|
readonly access_token?: string;
|
|
@@ -21,6 +24,8 @@ export interface OneBot12EndpointConfig {
|
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export interface OneBot12ConfigBase {
|
|
27
|
+
readonly platform: string;
|
|
28
|
+
readonly userId: string;
|
|
24
29
|
readonly context: 'onebot12';
|
|
25
30
|
readonly id: string;
|
|
26
31
|
readonly access_token?: string;
|
|
@@ -78,10 +83,10 @@ export interface OneBot12Segment {
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
export interface OneBot12ActionRequest {
|
|
86
|
+
readonly self: OneBot12Self;
|
|
81
87
|
action: string;
|
|
82
88
|
params: Record<string, unknown>;
|
|
83
89
|
echo?: string;
|
|
84
|
-
self?: OneBot12Self;
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
export interface OneBot12ActionResponse {
|
|
@@ -93,6 +98,8 @@ export interface OneBot12ActionResponse {
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
export interface OneBot12HttpOptions {
|
|
101
|
+
readonly self: OneBot12Self;
|
|
102
|
+
readonly signal?: AbortSignal;
|
|
96
103
|
readonly url: string;
|
|
97
104
|
readonly access_token?: string;
|
|
98
105
|
}
|
|
@@ -105,12 +112,14 @@ export interface OneBot12WireSegment {
|
|
|
105
112
|
export function resolveOneBot12Config(config: OneBot12EndpointConfig): ResolvedOneBot12Config {
|
|
106
113
|
const connection = config.connection ?? 'ws';
|
|
107
114
|
const id = requiredEndpointField(config.id, 'id');
|
|
115
|
+
const platform = requiredEndpointField(config.platform, 'platform');
|
|
116
|
+
const userId = requiredEndpointField(config.userId, 'userId');
|
|
108
117
|
const access_token = optionalEndpointField(config.access_token);
|
|
109
118
|
|
|
110
119
|
if (connection === 'ws') {
|
|
111
120
|
const url = requiredEndpointField(config.url, 'url');
|
|
112
121
|
return {
|
|
113
|
-
context: 'onebot12',
|
|
122
|
+
context: 'onebot12', platform, userId,
|
|
114
123
|
connection: 'ws',
|
|
115
124
|
id,
|
|
116
125
|
access_token,
|
|
@@ -124,7 +133,7 @@ export function resolveOneBot12Config(config: OneBot12EndpointConfig): ResolvedO
|
|
|
124
133
|
const path = requiredEndpointField(config.path, 'path');
|
|
125
134
|
const api_url = requiredEndpointField(config.api_url, 'api_url');
|
|
126
135
|
return {
|
|
127
|
-
context: 'onebot12',
|
|
136
|
+
context: 'onebot12', platform, userId,
|
|
128
137
|
connection: 'webhook',
|
|
129
138
|
id,
|
|
130
139
|
access_token,
|
|
@@ -136,7 +145,7 @@ export function resolveOneBot12Config(config: OneBot12EndpointConfig): ResolvedO
|
|
|
136
145
|
if (connection === 'wss') {
|
|
137
146
|
const path = requiredEndpointField(config.path, 'path');
|
|
138
147
|
return {
|
|
139
|
-
context: 'onebot12',
|
|
148
|
+
context: 'onebot12', platform, userId,
|
|
140
149
|
connection: 'wss',
|
|
141
150
|
id,
|
|
142
151
|
access_token,
|
|
@@ -150,7 +159,7 @@ export function resolveOneBot12Config(config: OneBot12EndpointConfig): ResolvedO
|
|
|
150
159
|
|
|
151
160
|
function requiredEndpointField(
|
|
152
161
|
value: unknown,
|
|
153
|
-
field: 'id' | 'url' | 'path' | 'api_url',
|
|
162
|
+
field: 'id' | 'url' | 'path' | 'api_url' | 'platform' | 'userId',
|
|
154
163
|
): string {
|
|
155
164
|
const resolved = optionalEndpointField(value);
|
|
156
165
|
if (!resolved) {
|
|
@@ -398,11 +407,12 @@ export type OneBot12CallAction = (
|
|
|
398
407
|
) => Promise<unknown>;
|
|
399
408
|
|
|
400
409
|
/** upload_file 文件名:优先段 data.name/filename,URL/路径取 basename,再按 mime 给默认名。 */
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
data: Record<string, unknown
|
|
404
|
-
media: MediaRef
|
|
405
|
-
|
|
410
|
+
export interface OneBot12MediaSource {
|
|
411
|
+
readonly type: string;
|
|
412
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
413
|
+
readonly media: MediaRef;
|
|
414
|
+
}
|
|
415
|
+
function oneBot12UploadName({ type, data, media }: OneBot12MediaSource): string {
|
|
406
416
|
const named = data.name ?? data.filename;
|
|
407
417
|
if (typeof named === 'string' && named) return named;
|
|
408
418
|
if (media.kind === 'url') {
|
|
@@ -418,16 +428,13 @@ function oneBot12UploadName(
|
|
|
418
428
|
if (base) return base;
|
|
419
429
|
}
|
|
420
430
|
const ext = media.mime_type?.split('/')[1]?.split(';')[0];
|
|
421
|
-
return `${
|
|
431
|
+
return `${type}.${ext || 'bin'}`;
|
|
422
432
|
}
|
|
423
433
|
|
|
424
434
|
/** canonical MediaRef → OB12 `upload_file` 动作参数(spec: type url/path/data)。 */
|
|
425
|
-
export function mediaRefToOneBot12UploadParams(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
media: MediaRef,
|
|
429
|
-
): Record<string, unknown> | undefined {
|
|
430
|
-
const name = oneBot12UploadName(segmentType, data, media);
|
|
435
|
+
export function mediaRefToOneBot12UploadParams(source: OneBot12MediaSource): Record<string, unknown> | undefined {
|
|
436
|
+
const { media } = source;
|
|
437
|
+
const name = oneBot12UploadName(source);
|
|
431
438
|
if (media.kind === 'url') return { type: 'url', name, url: media.value };
|
|
432
439
|
if (media.kind === 'path') {
|
|
433
440
|
const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
|
|
@@ -444,8 +451,7 @@ export function mediaRefToOneBot12UploadParams(
|
|
|
444
451
|
|
|
445
452
|
async function uploadOneMediaSegment(
|
|
446
453
|
item: unknown,
|
|
447
|
-
|
|
448
|
-
onUploadFailed?: (error: unknown) => void,
|
|
454
|
+
request: OneBot12MediaUpload,
|
|
449
455
|
): Promise<unknown> {
|
|
450
456
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
|
|
451
457
|
const segment = item as OneBot12WireSegment;
|
|
@@ -460,17 +466,18 @@ async function uploadOneMediaSegment(
|
|
|
460
466
|
if (media.kind === 'file') {
|
|
461
467
|
return { type: segment.type, data: { ...oneBot12MediaExtra(data), file_id: media.value } };
|
|
462
468
|
}
|
|
463
|
-
const params = mediaRefToOneBot12UploadParams(segment.type, data, media);
|
|
469
|
+
const params = mediaRefToOneBot12UploadParams({ type: segment.type, data, media });
|
|
464
470
|
if (!params) return item;
|
|
465
471
|
try {
|
|
466
|
-
const result = await callAction('upload_file', params) as { file_id?: unknown } | undefined;
|
|
472
|
+
const result = await request.callAction('upload_file', params) as { file_id?: unknown } | undefined;
|
|
467
473
|
if (result && typeof result.file_id === 'string' && result.file_id) {
|
|
468
474
|
return { type: segment.type, data: { ...oneBot12MediaExtra(data), file_id: result.file_id } };
|
|
469
475
|
}
|
|
470
476
|
throw new Error('upload_file 响应缺少 file_id');
|
|
471
477
|
} catch (error) {
|
|
478
|
+
if (request.signal?.aborted) throw error;
|
|
472
479
|
// 上传失败降级:保留原段,由 formatOutboundSegments 走扩展字段(url/data/path)
|
|
473
|
-
onUploadFailed?.(error);
|
|
480
|
+
request.onUploadFailed?.(error);
|
|
474
481
|
return item;
|
|
475
482
|
}
|
|
476
483
|
}
|
|
@@ -481,15 +488,17 @@ async function uploadOneMediaSegment(
|
|
|
481
488
|
* {@link formatOutboundSegments} 编码;kind=file 的 MediaRef 直接按 file_id 复投。
|
|
482
489
|
* 上传失败保留原段(降级扩展字段透传)并回调 onUploadFailed。
|
|
483
490
|
*/
|
|
484
|
-
export
|
|
485
|
-
payload: unknown
|
|
486
|
-
callAction: OneBot12CallAction
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
+
export interface OneBot12MediaUpload {
|
|
492
|
+
readonly payload: unknown;
|
|
493
|
+
readonly callAction: OneBot12CallAction;
|
|
494
|
+
readonly signal?: AbortSignal;
|
|
495
|
+
readonly onUploadFailed?: (error: unknown) => void;
|
|
496
|
+
}
|
|
497
|
+
export async function uploadOneBot12MediaSegments(request: OneBot12MediaUpload): Promise<unknown> {
|
|
498
|
+
if (Array.isArray(request.payload)) {
|
|
499
|
+
return Promise.all(request.payload.map(item => uploadOneMediaSegment(item, request)));
|
|
491
500
|
}
|
|
492
|
-
return uploadOneMediaSegment(payload,
|
|
501
|
+
return uploadOneMediaSegment(request.payload, request);
|
|
493
502
|
}
|
|
494
503
|
|
|
495
504
|
/**
|
|
@@ -519,40 +528,34 @@ export function buildSendMessageParams(
|
|
|
519
528
|
* 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应。
|
|
520
529
|
* 供 webhook 出站与纯协议测试使用;WS 路径走 WebSocket echo 请求。
|
|
521
530
|
*/
|
|
522
|
-
export
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
531
|
+
export interface OneBot12HttpRequest extends OneBot12HttpOptions {
|
|
532
|
+
readonly action: string;
|
|
533
|
+
readonly params?: Record<string, unknown>;
|
|
534
|
+
readonly echo?: string;
|
|
535
|
+
}
|
|
536
|
+
/** HTTP owns submission evidence and cancellation for both SDK and framework callers. */
|
|
537
|
+
export async function callOneBot12Action(request: OneBot12HttpRequest): Promise<OneBot12ActionResponse> {
|
|
538
|
+
if (request.signal?.aborted) throw new EndpointDeliveryError('endpoint_inactive', 'Endpoint operation cancelled', 'not_sent');
|
|
528
539
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
529
|
-
if (
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const body: OneBot12ActionRequest = { action, params };
|
|
533
|
-
if (echo) body.echo = echo;
|
|
534
|
-
|
|
535
|
-
const res = await fetch(options.url, {
|
|
536
|
-
method: 'POST',
|
|
537
|
-
headers,
|
|
538
|
-
body: JSON.stringify(body),
|
|
539
|
-
signal: AbortSignal.timeout(30_000),
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
const text = await res.text();
|
|
543
|
-
if (res.status === 401) throw new Error(`OneBot12 鉴权失败: ${text}`);
|
|
544
|
-
if (res.status !== 200) throw new Error(`OneBot12 HTTP ${res.status}: ${text}`);
|
|
545
|
-
|
|
546
|
-
let data: OneBot12ActionResponse;
|
|
540
|
+
if (request.access_token) headers.Authorization = `Bearer ${request.access_token}`;
|
|
541
|
+
const body: OneBot12ActionRequest = { action: request.action, params: request.params ?? {}, self: request.self,
|
|
542
|
+
...(request.echo !== undefined ? { echo: request.echo } : {}) };
|
|
547
543
|
try {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
544
|
+
const response = await fetch(request.url, { method: 'POST', headers, body: JSON.stringify(body),
|
|
545
|
+
signal: request.signal ? AbortSignal.any([request.signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000) });
|
|
546
|
+
const text = await response.text();
|
|
547
|
+
if (response.status === 401) throw new EndpointDeliveryError('platform_rejected', `OneBot12 鉴权失败: ${text}`, 'rejected');
|
|
548
|
+
if (response.status !== 200) throw new EndpointDeliveryError('endpoint_http_failed', `OneBot12 HTTP ${response.status}: ${text}`, 'unknown');
|
|
549
|
+
let data: OneBot12ActionResponse;
|
|
550
|
+
try { data = JSON.parse(text) as OneBot12ActionResponse; }
|
|
551
|
+
catch { throw new EndpointDeliveryError('delivery_unconfirmed', `OneBot12 无效响应: ${text.slice(0, 200)}`, 'unknown'); }
|
|
552
|
+
readOnebot12Response(data);
|
|
553
|
+
return data;
|
|
554
|
+
} catch (cause) {
|
|
555
|
+
if (cause instanceof EndpointDeliveryError) throw cause;
|
|
556
|
+
throw new EndpointDeliveryError(request.signal?.aborted ? 'endpoint_inactive' : 'endpoint_transport_failed',
|
|
557
|
+
cause instanceof Error ? cause.message : String(cause), 'unknown', { cause });
|
|
554
558
|
}
|
|
555
|
-
return data;
|
|
556
559
|
}
|
|
557
560
|
|
|
558
561
|
/** Build WS connect URL + headers (access_token via Bearer + query). */
|
|
@@ -573,3 +576,9 @@ export function buildWsConnectOptions(config: OneBot12WsConfig): {
|
|
|
573
576
|
safeUrl.searchParams.delete('access_token');
|
|
574
577
|
return { url: connectUrl, headers, safeUrl: safeUrl.toString() };
|
|
575
578
|
}
|
|
579
|
+
|
|
580
|
+
/** Route only the explicitly selected bot; service-wide frames carry no account authority. */
|
|
581
|
+
export function matchesOneBot12Self(config: Pick<OneBot12ConfigBase, 'platform' | 'userId'>,
|
|
582
|
+
event: OneBot12Event): event is OneBot12Event & { readonly self: OneBot12Self } {
|
|
583
|
+
return event.self?.platform === config.platform && event.self?.user_id === config.userId;
|
|
584
|
+
}
|
package/src/response.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
2
|
+
import type { OneBotV12Response } from '@imhelper/onebot-v12';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One classifier for native SDK, HTTP and WS results.
|
|
6
|
+
* @see https://12.onebot.dev/connect/data-protocol/action-response/
|
|
7
|
+
*/
|
|
8
|
+
export function readOnebot12Response<T = unknown>(response: unknown): T | undefined {
|
|
9
|
+
if (response && typeof response === 'object') {
|
|
10
|
+
const result = response as Partial<OneBotV12Response>;
|
|
11
|
+
if (result.status === 'ok' && result.retcode === 0) return result.data as T | undefined;
|
|
12
|
+
if (result.status === 'failed' && typeof result.retcode === 'number' && Number.isSafeInteger(result.retcode) && result.retcode >= 10000 && result.retcode <= 99999) {
|
|
13
|
+
throw new EndpointDeliveryError('platform_rejected',
|
|
14
|
+
`OneBot 12 API rejected: retcode=${result.retcode}${result.message ? ` ${result.message}` : ''}`, 'rejected');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// OB12 defines only ok/0 or failed with an error code; inconsistent envelopes prove neither result.
|
|
18
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', 'OneBot 12 response did not confirm the action result', 'unknown');
|
|
19
|
+
}
|
|
@@ -7,13 +7,15 @@ export interface OneBot12SideEventCaller {
|
|
|
7
7
|
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
logger: ReturnType<typeof getAdapterLogger
|
|
16
|
-
|
|
10
|
+
/** Exact instance-owned dependencies for one canonical side-event projection. */
|
|
11
|
+
export interface OneBot12SideEventContext {
|
|
12
|
+
readonly emit: EndpointEventEmitter;
|
|
13
|
+
readonly endpointKey: string;
|
|
14
|
+
readonly caller: OneBot12SideEventCaller;
|
|
15
|
+
readonly logger: ReturnType<typeof getAdapterLogger>;
|
|
16
|
+
}
|
|
17
|
+
export function receiveOneBot12SideEvent(raw: OneBot12Event, context: OneBot12SideEventContext): void {
|
|
18
|
+
const { emit, endpointKey, caller, logger } = context;
|
|
17
19
|
if (!emit) return;
|
|
18
20
|
const record = raw as Record<string, unknown>;
|
|
19
21
|
const eventType = String(record.type ?? record.post_type ?? '');
|
package/src/webhook.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { acceptHttpIngress } from 'imhelper';
|
|
1
2
|
/**
|
|
2
3
|
* OneBot12 HTTP webhook endpoint — POST inbound + api_url outbound.
|
|
3
4
|
*/
|
|
4
5
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
+
createEndpointSpecification, endpointSpec,
|
|
8
|
+
type Endpoint, type EndpointSpecification,
|
|
7
9
|
createRecallEndpointControl,
|
|
8
10
|
type EndpointControl,
|
|
9
11
|
type EndpointManagement,
|
|
10
12
|
type EndpointSendRequest,
|
|
13
|
+
type PendingRpcRequest,
|
|
11
14
|
} from 'zhin.js/adapter';
|
|
12
15
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
13
16
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
@@ -15,6 +18,7 @@ import type { CapabilityId } from 'zhin.js';
|
|
|
15
18
|
import { createOneBot12EndpointManagement } from './endpoint-management.js';
|
|
16
19
|
import {
|
|
17
20
|
buildSendMessageParams,
|
|
21
|
+
matchesOneBot12Self,
|
|
18
22
|
callOneBot12Action,
|
|
19
23
|
formatInboundContent,
|
|
20
24
|
formatOutboundSegments,
|
|
@@ -29,46 +33,55 @@ import {
|
|
|
29
33
|
} from './protocol.js';
|
|
30
34
|
import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
|
|
31
35
|
import { createOneBot12ContentPort } from './content-port.js';
|
|
32
|
-
import { callOnebot12Client,
|
|
36
|
+
import { callOnebot12Client, readOnebot12Response, onebot12ClientOptions, forwardOnebot12ClientEvents, Onebot12Client } from './client.js';
|
|
33
37
|
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
34
38
|
|
|
35
39
|
const logger = getLogger('onebot12');
|
|
36
40
|
|
|
37
41
|
export interface OneBot12WebhookEndpointOptions {
|
|
42
|
+
readonly signal?: AbortSignal;
|
|
38
43
|
readonly id: CapabilityId;
|
|
39
44
|
readonly http: HttpHost;
|
|
40
45
|
readonly config: OneBot12WebhookConfig;
|
|
41
46
|
readonly callAction?: typeof callOneBot12Action;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
export class OneBot12WebhookEndpoint extends
|
|
45
|
-
readonly
|
|
49
|
+
export class OneBot12WebhookEndpoint extends Onebot12Client implements Endpoint {
|
|
50
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
51
|
+
#open = false;
|
|
46
52
|
readonly #options: OneBot12WebhookEndpointOptions;
|
|
47
|
-
readonly management: EndpointManagement;
|
|
48
|
-
readonly control: EndpointControl = createRecallEndpointControl((id) => this
|
|
49
|
-
readonly content;
|
|
53
|
+
readonly #management: EndpointManagement;
|
|
54
|
+
readonly #control: EndpointControl = createRecallEndpointControl((id, signal?: AbortSignal) => this.#recallMessage(id, signal));
|
|
55
|
+
readonly #content;
|
|
56
|
+
readonly #releaseClientEvents: () => void;
|
|
50
57
|
readonly #callAction: typeof callOneBot12Action;
|
|
51
58
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
52
59
|
#started = false;
|
|
53
60
|
|
|
54
61
|
constructor(options: OneBot12WebhookEndpointOptions) {
|
|
55
|
-
super();
|
|
62
|
+
super(onebot12ClientOptions(options.config, (action, params) => this.#callApi({ action, params })));
|
|
56
63
|
this.#options = options;
|
|
57
64
|
this.#callAction = options.callAction ?? callOneBot12Action;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this
|
|
61
|
-
this
|
|
62
|
-
|
|
63
|
-
(
|
|
64
|
-
(
|
|
65
|
+
const callApi = (request: PendingRpcRequest) => this.#callApi(request).then(readOnebot12Response);
|
|
66
|
+
this.#management = createOneBot12EndpointManagement({ callApi });
|
|
67
|
+
this.#content = createOneBot12ContentPort(callApi);
|
|
68
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
69
|
+
name: options.config.id,
|
|
70
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
71
|
+
close: () => { this.#open = false; }, stop: () => this.#stop(),
|
|
72
|
+
send: request => this.#send(request),
|
|
73
|
+
management: this.#management, control: this.#control, content: this.#content,
|
|
74
|
+
});
|
|
75
|
+
this.#releaseClientEvents = forwardOnebot12ClientEvents(this, (name, payload) => {
|
|
76
|
+
void this[endpointSpec].events.run(() => {
|
|
77
|
+
if (!this.#started) return;
|
|
78
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
65
79
|
if (name === 'event') this.#admitRaw(payload as OneBot12Event);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
);
|
|
80
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
81
|
+
});
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
async start(): Promise<void> {
|
|
84
|
+
async #start(): Promise<void> {
|
|
72
85
|
if (this.#started) return;
|
|
73
86
|
this.#started = true;
|
|
74
87
|
if (!this.#options.config.access_token) {
|
|
@@ -89,28 +102,26 @@ export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
89
102
|
}));
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
async stop(): Promise<void> {
|
|
93
|
-
this
|
|
105
|
+
async #stop(): Promise<void> {
|
|
106
|
+
this.#open = false;
|
|
94
107
|
for (const release of this.#routeReleases.splice(0)) release();
|
|
95
108
|
this.#started = false;
|
|
109
|
+
this.#releaseClientEvents();
|
|
96
110
|
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.id }));
|
|
97
111
|
}
|
|
98
112
|
|
|
99
|
-
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
100
|
-
const materialized = await uploadOneBot12MediaSegments(
|
|
101
|
-
payload,
|
|
102
|
-
(action, params) =>
|
|
103
|
-
|
|
104
|
-
logger.warn(formatCompact({
|
|
105
|
-
|
|
106
|
-
endpoint: this.#options.config.id,
|
|
107
|
-
error: error instanceof Error ? error.message : String(error),
|
|
108
|
-
}));
|
|
113
|
+
async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
114
|
+
const materialized = await uploadOneBot12MediaSegments({
|
|
115
|
+
payload, signal,
|
|
116
|
+
callAction: (action, params) => this.#callApi({ action, params, signal }).then(readOnebot12Response),
|
|
117
|
+
onUploadFailed: error => {
|
|
118
|
+
logger.warn(formatCompact({ op: 'onebot12_upload_failed', endpoint: this.#options.config.id,
|
|
119
|
+
error: error instanceof Error ? error.message : String(error) }));
|
|
109
120
|
},
|
|
110
|
-
);
|
|
121
|
+
});
|
|
111
122
|
const message = formatOutboundSegments(materialized);
|
|
112
123
|
const params = buildSendMessageParams(conversation, message);
|
|
113
|
-
const data =
|
|
124
|
+
const data = readOnebot12Response<{ message_id?: string }>(await this.#callApi({ action: 'send_message', params, signal }));
|
|
114
125
|
const messageId = data?.message_id ?? '';
|
|
115
126
|
logger.debug(formatCompact({
|
|
116
127
|
op: 'onebot12_send',
|
|
@@ -122,38 +133,38 @@ export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
122
133
|
return messageId;
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
async recallMessage(messageId: string): Promise<void> {
|
|
136
|
+
async #recallMessage(messageId: string, signal?: AbortSignal): Promise<void> {
|
|
126
137
|
if (!messageId) return;
|
|
127
|
-
await
|
|
138
|
+
await this.#callApi({ action: 'delete_message', params: { message_id: messageId }, signal }).then(readOnebot12Response);
|
|
128
139
|
}
|
|
129
140
|
|
|
130
|
-
async #callApi(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
action,
|
|
137
|
-
params,
|
|
138
|
-
);
|
|
141
|
+
async #callApi(request: PendingRpcRequest): Promise<import('@imhelper/onebot-v12').OneBotV12Response> {
|
|
142
|
+
const epoch = this.#options.signal;
|
|
143
|
+
const signal = epoch && request.signal ? AbortSignal.any([epoch, request.signal]) : epoch ?? request.signal;
|
|
144
|
+
return this.#callAction({ url: this.#options.config.api_url, access_token: this.#options.config.access_token,
|
|
145
|
+
self: { platform: this.#options.config.platform, user_id: this.#options.config.userId },
|
|
146
|
+
action: request.action, params: request.params, signal });
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
#admitRaw(ev: OneBot12Event): void {
|
|
150
|
+
if (!matchesOneBot12Self(this.#options.config, ev)) return;
|
|
151
|
+
this[endpointSpec].reportSelfUserId(ev.self.user_id);
|
|
142
152
|
if (!isMessageEvent(ev)) {
|
|
143
|
-
receiveOneBot12SideEvent(
|
|
144
|
-
(name, payload) => this.emit(name, payload),
|
|
145
|
-
this.#options.config.id,
|
|
146
|
-
{ callApi: (action, params) => callOnebot12Client(this
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
);
|
|
153
|
+
receiveOneBot12SideEvent(ev, {
|
|
154
|
+
emit: (name, payload) => this[endpointSpec].events.emit(name, payload),
|
|
155
|
+
endpointKey: this.#options.config.id,
|
|
156
|
+
caller: { callApi: (action, params) => callOnebot12Client({ client: this, action, params }) },
|
|
157
|
+
logger: logger,
|
|
158
|
+
});
|
|
150
159
|
return;
|
|
151
160
|
}
|
|
152
|
-
const conversation = onebot12InboundConversation(String(this.#options.id), ev);
|
|
161
|
+
const conversation = { ...onebot12InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
153
162
|
const content = formatInboundContent(ev);
|
|
154
163
|
const nickname = senderNickname(ev);
|
|
155
164
|
const mentioned = isBotMentioned(ev);
|
|
156
|
-
void this.
|
|
165
|
+
void this[endpointSpec].events.message({
|
|
166
|
+
event: ev,
|
|
167
|
+
...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
|
|
157
168
|
conversation,
|
|
158
169
|
message: { conversation, id: ev.message_id },
|
|
159
170
|
content,
|
|
@@ -202,12 +213,16 @@ export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
202
213
|
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
203
214
|
return;
|
|
204
215
|
}
|
|
205
|
-
if (!this
|
|
216
|
+
if (!this.#open) {
|
|
206
217
|
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
207
218
|
response.end(JSON.stringify({ status: 'ok' }));
|
|
208
219
|
return;
|
|
209
220
|
}
|
|
210
|
-
await
|
|
221
|
+
await acceptHttpIngress<OneBot12Event>(request, response, event => {
|
|
222
|
+
if (!matchesOneBot12Self(this.#options.config, event)) return;
|
|
223
|
+
void this[endpointSpec].events.run(() => this.ingest(event as Parameters<Onebot12Client['ingest']>[0]))
|
|
224
|
+
.catch(error => this.#warnPlatformEvent(error));
|
|
225
|
+
});
|
|
211
226
|
} catch (error) {
|
|
212
227
|
logger.error('OneBot12 webhook error:', error);
|
|
213
228
|
if (!response.headersSent) {
|