@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +33 -4
  3. package/adapters/onebot12/index.js +3 -32
  4. package/adapters/onebot12/index.ts +2 -38
  5. package/commands/onebot12/endpoint/definition.js +2 -2
  6. package/commands/onebot12/endpoint/definition.ts +2 -2
  7. package/lib/adapter.d.ts +21 -0
  8. package/lib/adapter.js +34 -0
  9. package/lib/client.d.ts +9 -11
  10. package/lib/client.js +23 -13
  11. package/lib/console-spec.d.ts +12 -0
  12. package/lib/console-spec.js +15 -0
  13. package/lib/console.d.ts +8 -0
  14. package/lib/console.js +13 -0
  15. package/lib/content-port.d.ts +2 -2
  16. package/lib/content-port.js +1 -1
  17. package/lib/endpoint-management.d.ts +5 -5
  18. package/lib/endpoint-management.js +17 -17
  19. package/lib/identity.d.ts +4 -0
  20. package/lib/identity.js +53 -0
  21. package/lib/index.d.ts +3 -2
  22. package/lib/index.js +2 -1
  23. package/lib/protocol.d.ts +32 -4
  24. package/lib/protocol.js +54 -53
  25. package/lib/response.d.ts +5 -0
  26. package/lib/response.js +17 -0
  27. package/lib/side-event-dispatch.d.ts +8 -1
  28. package/lib/side-event-dispatch.js +2 -1
  29. package/lib/webhook.d.ts +5 -11
  30. package/lib/webhook.js +72 -37
  31. package/lib/ws-endpoint.d.ts +5 -11
  32. package/lib/ws-endpoint.js +79 -45
  33. package/lib/ws-transport.d.ts +18 -10
  34. package/lib/ws-transport.js +26 -45
  35. package/lib/ws-types.d.ts +0 -6
  36. package/lib/wss-endpoint.d.ts +5 -11
  37. package/lib/wss-endpoint.js +82 -48
  38. package/package.json +22 -14
  39. package/pages/index/index.js +15 -0
  40. package/pages/index/index.ts +16 -0
  41. package/plugin.js +3 -5
  42. package/schema.json +15 -3
  43. package/src/adapter.ts +36 -0
  44. package/src/client.ts +26 -29
  45. package/src/console-spec.ts +20 -0
  46. package/src/console.ts +18 -0
  47. package/src/content-port.ts +3 -3
  48. package/src/endpoint-management.ts +25 -24
  49. package/src/identity.ts +52 -0
  50. package/src/index.ts +4 -1
  51. package/src/protocol.ts +71 -62
  52. package/src/response.ts +19 -0
  53. package/src/side-event-dispatch.ts +9 -7
  54. package/src/webhook.ts +70 -55
  55. package/src/ws-endpoint.ts +75 -63
  56. package/src/ws-transport.ts +37 -73
  57. package/src/ws-types.ts +0 -7
  58. package/src/wss-endpoint.ts +80 -66
  59. package/lib/onebot12-runtime-state.d.ts +0 -1
  60. package/lib/onebot12-runtime-state.js +0 -6
  61. 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
- function oneBot12UploadName(
402
- segmentType: string,
403
- data: Record<string, unknown>,
404
- media: MediaRef,
405
- ): string {
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 `${segmentType}.${ext || 'bin'}`;
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
- segmentType: string,
427
- data: Record<string, unknown>,
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
- callAction: OneBot12CallAction,
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 async function uploadOneBot12MediaSegments(
485
- payload: unknown,
486
- callAction: OneBot12CallAction,
487
- onUploadFailed?: (error: unknown) => void,
488
- ): Promise<unknown> {
489
- if (Array.isArray(payload)) {
490
- return Promise.all(payload.map((item) => uploadOneMediaSegment(item, callAction, onUploadFailed)));
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, callAction, onUploadFailed);
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 async function callOneBot12Action(
523
- options: OneBot12HttpOptions,
524
- action: string,
525
- params: Record<string, unknown> = {},
526
- echo?: string,
527
- ): Promise<OneBot12ActionResponse> {
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 (options.access_token) {
530
- headers.Authorization = `Bearer ${options.access_token}`;
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
- data = JSON.parse(text) as OneBot12ActionResponse;
549
- } catch {
550
- throw new Error(`OneBot12 无效响应: ${text.slice(0, 200)}`);
551
- }
552
- if (data.status === 'failed' && data.retcode !== 0) {
553
- throw new Error(`OneBot12 动作失败 retcode=${data.retcode}: ${data.message}`);
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
+ }
@@ -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
- export function receiveOneBot12SideEvent(
11
- emit: EndpointEventEmitter,
12
- endpointKey: string,
13
- caller: OneBot12SideEventCaller,
14
- raw: OneBot12Event,
15
- logger: ReturnType<typeof getAdapterLogger>,
16
- ): void {
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
- ClientEndpoint,
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, createOnebot12EndpointClient, forwardOnebot12ClientEvents, type Onebot12Client } from './client.js';
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 ClientEndpoint<Onebot12Client> {
45
- readonly client: Onebot12Client;
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.recallMessage(id));
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
- this.client = createOnebot12EndpointClient(options.config, (action, params) => this.#callApi(action, params));
59
- const callApi = (action: string, params?: Record<string, unknown>) => callOnebot12Client(this.client, action, params);
60
- this.management = createOneBot12EndpointManagement({ callApi });
61
- this.content = createOneBot12ContentPort(callApi);
62
- this.bindClientEvents(
63
- (receive) => forwardOnebot12ClientEvents(this.client, receive),
64
- (name, payload) => {
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
- (_name, error) => this.#warnPlatformEvent(error),
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.close();
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) => callOnebot12Client(this.client, action, params),
103
- (error) => {
104
- logger.warn(formatCompact({
105
- op: 'onebot12_upload_failed',
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 = await callOnebot12Client<{ message_id?: string }>(this.client, 'send_message', params);
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 callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
138
+ await this.#callApi({ action: 'delete_message', params: { message_id: messageId }, signal }).then(readOnebot12Response);
128
139
  }
129
140
 
130
- async #callApi(
131
- action: string,
132
- params: Record<string, unknown> = {},
133
- ): Promise<import('@imhelper/onebot-v12').OneBotV12Response> {
134
- return this.#callAction(
135
- { url: this.#options.config.api_url, access_token: this.#options.config.access_token },
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.client, action, params) },
147
- ev,
148
- logger,
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.emit('message.receive', {
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.clientEventsOpen) {
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 this.client.acceptHttp(request, response);
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) {