@zhin.js/adapter-satori 1.1.18 → 1.1.20

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/src/endpoint.ts CHANGED
@@ -1,17 +1,23 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
1
+ import { acceptHttpIngress } from 'imhelper';
2
2
  /**
3
3
  * SatoriEndpoint — WebSocket and webhook lifecycle, outbound, admit.
4
4
  */
5
5
  import {
6
- ClientEndpoint,
6
+ createEndpointSpecification,
7
+ endpointSpec,
8
+ type Endpoint,
9
+ type EndpointSpecification,
7
10
  createEndpointLifecycle,
8
11
  type EndpointChannel,
9
12
  type EndpointConnectHandle,
10
13
  type EndpointControl,
14
+ type EndpointRecallRequest,
11
15
  type EndpointGroup,
12
16
  type EndpointLifecycle,
13
17
  type EndpointManagement,
14
18
  type EndpointSendRequest,
19
+ type EndpointManagementRequest,
20
+ type EndpointGroupRequest,
15
21
  } from 'zhin.js/adapter';
16
22
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
17
23
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
@@ -31,13 +37,12 @@ import {
31
37
  satoriInboundConversation,
32
38
  type ResolvedSatoriWebhookConfig,
33
39
  type ResolvedSatoriWsConfig,
34
- type SatoriApiOptions,
35
40
  type SatoriEventBody,
36
41
  type SatoriLogin,
37
42
  type SatoriSignal,
38
43
  } from './protocol.js';
39
44
  import { registerSatoriWebhookRoutes } from './webhook.js';
40
- import { createSatoriEndpointClient, forwardSatoriClientEvents, type SatoriClient } from './client.js';
45
+ import { satoriClientOptions, createSatoriActionCaller, type SatoriActionCaller, type SatoriActionRequest, forwardSatoriClientEvents, SatoriClient } from './client.js';
41
46
  import {
42
47
  WS_OPEN,
43
48
  defaultCreateWebSocket,
@@ -48,45 +53,49 @@ import {
48
53
  export type SatoriApiCaller = typeof callSatoriApi;
49
54
 
50
55
  export interface SatoriWsEndpointOptions {
56
+ readonly signal?: AbortSignal;
51
57
  readonly id: CapabilityId;
52
58
  readonly config: ResolvedSatoriWsConfig;
53
59
  readonly createWebSocket?: CreateSatoriWebSocket;
54
60
  readonly callApi?: SatoriApiCaller;
55
61
  }
56
62
 
57
- export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
58
- readonly client: SatoriClient;
63
+ export class SatoriWsEndpoint extends SatoriClient implements Endpoint {
64
+ readonly [endpointSpec]: EndpointSpecification;
65
+ #open = false;
59
66
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
60
67
 
61
68
  readonly #options: SatoriWsEndpointOptions;
62
69
  readonly #lifecycle: EndpointLifecycle;
63
70
  #ws: SatoriWsSocket | null = null;
64
- #login: SatoriLogin | undefined;
65
71
  #lastSn: number | undefined;
66
- readonly management: EndpointManagement;
67
- readonly control: EndpointControl = Object.freeze<EndpointControl>({
68
- recall: (message) => this.recall(message.id, message.conversation.id),
72
+ readonly #management: EndpointManagement;
73
+ readonly #call: SatoriActionCaller;
74
+ readonly #control: EndpointControl = Object.freeze<EndpointControl>({
75
+ recall: request => this.#recall(request),
69
76
  });
70
77
 
71
78
  constructor(options: SatoriWsEndpointOptions) {
72
- super();
79
+ const call = createSatoriActionCaller({ config: options.config, request: options.callApi ?? callSatoriApi, signal: options.signal });
80
+ super(satoriClientOptions({ config: options.config, call }));
81
+ this.#call = call;
73
82
  this.#logger = getAdapterLogger('satori', options.config.id);
74
83
  this.#options = options;
75
- this.client = createSatoriEndpointClient(
76
- options.config,
77
- options.callApi ?? callSatoriApi,
78
- () => this.#apiOptions(),
79
- );
80
- this.management = createSatoriEndpointManagement(
81
- (resource, method, params) => this.client.call(resource, method, params),
82
- );
83
- this.bindClientEvents(
84
- (receive) => forwardSatoriClientEvents(this.client, receive),
85
- (name, payload) => {
84
+ this.#management = createSatoriEndpointManagement(call);
85
+ this[endpointSpec] = createEndpointSpecification(this, {
86
+ name: options.config.id,
87
+ start: () => this.#start(), open: () => { this.#open = true; },
88
+ close: () => { this.#open = false; }, stop: () => this.#stop(),
89
+ send: request => this.#send(request),
90
+ management: this.#management, control: this.#control,
91
+ });
92
+ forwardSatoriClientEvents(this, (name, payload) => {
93
+ void this[endpointSpec].events.run(() => {
94
+ if (!this.#open && !this.#lifecycle.started) return;
95
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(name, error));
86
96
  if (name === 'event') this.#admitRaw(payload as SatoriEventBody);
87
- },
88
- (name, error) => this.#warnPlatformEvent(name, error),
89
- );
97
+ }).catch(error => this.#warnPlatformEvent(name, error));
98
+ });
90
99
  const { config } = options;
91
100
  this.#lifecycle = createEndpointLifecycle({
92
101
  name: config.id,
@@ -105,7 +114,7 @@ export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
105
114
  });
106
115
  }
107
116
 
108
- async start(): Promise<void> {
117
+ async #start(): Promise<void> {
109
118
  try {
110
119
  await this.#lifecycle.start((handle) => this.#connect(handle));
111
120
  } catch (err) {
@@ -122,19 +131,19 @@ export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
122
131
  }
123
132
  }
124
133
 
125
- async stop(): Promise<void> {
126
- this.close();
134
+ async #stop(): Promise<void> {
135
+ this.#open = false;
127
136
  // 基座负责:清重连/心跳定时器、强关 ws、唤醒 stop-during-connect 竞态
128
137
  await this.#lifecycle.stop();
129
138
  this.#ws = null;
130
139
  }
131
140
 
132
- async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
141
+ async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
133
142
  const content = formatSatoriOutbound(payload);
134
- const result = await this.client.call('message', 'create', {
143
+ const result = await this.#call({ resource: 'message', method: 'create', signal, params: {
135
144
  channel_id: conversation.id,
136
145
  content,
137
- });
146
+ } });
138
147
  const msgId = extractCreatedMessageId(result);
139
148
  this.#logger.debug(formatCompact({
140
149
  op: 'satori_send',
@@ -146,14 +155,16 @@ export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
146
155
  }
147
156
 
148
157
  #admitRaw(body: SatoriEventBody): void {
149
- if (body.login && !this.#login) this.#login = body.login;
158
+ if (!matchesSatoriLogin(this.#options.config, body.login)) return;
150
159
  if (!isMessageEvent(body)) return;
151
- const conversation = satoriInboundConversation(String(this.#options.id), body);
160
+ const conversation = { ...satoriInboundConversation(String(this.#options.id), body), endpoint: this[endpointSpec].identity };
152
161
  const content = formatInboundContent(body);
153
162
  const sender = resolveInboundSender(body);
154
- const selfId = this.#login?.user?.id ?? body.login?.user?.id;
163
+ const selfId = this.#options.config.userId;
155
164
  const mentioned = isSelfMentioned(body, selfId);
156
- void this.emit('message.receive', {
165
+ void this[endpointSpec].events.message({
166
+ event: body,
167
+ ...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
157
168
  conversation,
158
169
  message: { conversation, id: body.message.id },
159
170
  content,
@@ -164,7 +175,7 @@ export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
164
175
  type: body.type,
165
176
  channelType: isPrivateChannelType(body) ? 'private' : 'group',
166
177
  sn: body.sn,
167
- platform: this.#login?.platform,
178
+ platform: this.#options.config.platform,
168
179
  }),
169
180
  }).catch((err) => {
170
181
  this.#logger.warn(formatCompact({
@@ -175,12 +186,12 @@ export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
175
186
  });
176
187
  }
177
188
 
178
- async recall(id: string, fallbackChannelId?: string): Promise<void> {
179
- const { channelId, messageId } = parseMessageRef(id);
180
- await this.client.call('message', 'delete', {
181
- channel_id: channelId || fallbackChannelId || '',
189
+ async #recall({ message, signal }: EndpointRecallRequest): Promise<void> {
190
+ const { channelId, messageId } = parseMessageRef(message.id);
191
+ await this.#call({ resource: 'message', method: 'delete', signal, params: {
192
+ channel_id: channelId || message.conversation.id,
182
193
  message_id: messageId,
183
- });
194
+ } });
184
195
  }
185
196
 
186
197
  #warnPlatformEvent(name: string, error: unknown): void {
@@ -210,6 +221,7 @@ export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
210
221
  });
211
222
 
212
223
  ws.on('open', () => {
224
+ if (this.#ws !== ws || !this.#lifecycle.started) return;
213
225
  this.#logger.debug(formatCompact({ mode: 'ws' }));
214
226
  this.#sendSignal(SatoriOpcode.IDENTIFY, {
215
227
  token: config.token,
@@ -223,6 +235,7 @@ export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
223
235
  });
224
236
 
225
237
  ws.on('message', (data) => {
238
+ if (this.#ws !== ws || !this.#lifecycle.started) return;
226
239
  try {
227
240
  const raw = typeof data === 'string'
228
241
  ? data
@@ -275,7 +288,7 @@ export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
275
288
  if (signal.op === SatoriOpcode.PONG) {
276
289
  // 喂狗:复位基座看门狗计数
277
290
  this.#lifecycle.notifyHeartbeatAck();
278
- void this.emitPlatform('pong', signal).catch((error) => {
291
+ void this[endpointSpec].events.platform('pong', signal).catch((error) => {
279
292
  this.#logger.warn(formatCompact({
280
293
  op: 'satori_platform_event_failed',
281
294
  event: 'pong',
@@ -286,22 +299,26 @@ export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
286
299
  }
287
300
  if (signal.op === SatoriOpcode.READY && signal.body?.logins) {
288
301
  const logins = signal.body.logins as SatoriLogin[];
289
- this.#login = logins[0];
290
- void this.emitPlatform('ready', signal).catch((error) => {
302
+ const login = logins.find(candidate => matchesSatoriLogin(this.#options.config, candidate));
303
+ if (login?.user?.id) this[endpointSpec].reportSelfUserId(login.user.id);
304
+ void this[endpointSpec].events.platform('ready', signal).catch((error) => {
291
305
  this.#logger.warn(formatCompact({
292
306
  op: 'satori_platform_event_failed',
293
307
  event: 'ready',
294
308
  error: error instanceof Error ? error.message : String(error),
295
309
  }));
296
310
  });
297
- if (!this.#login?.platform || !this.#login?.user?.id) {
311
+ if (!login) {
298
312
  this.#logger.warn(formatCompact({ op: 'ready', ok: false, error: 'missing platform/user' }));
299
313
  }
300
314
  return;
301
315
  }
302
316
  if (signal.op === SatoriOpcode.EVENT && signal.body) {
303
- if (typeof signal.body.sn === 'number') this.#lastSn = signal.body.sn;
304
- this.client.ingest(signal.body as Parameters<SatoriClient['ingest']>[0]);
317
+ const body = signal.body;
318
+ void this[endpointSpec].events.run(() => {
319
+ if (typeof body.sn === 'number') this.#lastSn = body.sn;
320
+ this.#receive(body as SatoriEventBody);
321
+ }).catch(error => this.#warnPlatformEvent('event', error));
305
322
  }
306
323
  }
307
324
 
@@ -310,72 +327,60 @@ export class SatoriWsEndpoint extends ClientEndpoint<SatoriClient> {
310
327
  this.#ws.send(JSON.stringify({ op, body: body ?? {} }));
311
328
  }
312
329
 
313
- #apiOptions(): SatoriApiOptions {
314
- return {
315
- baseUrl: this.#options.config.baseUrl,
316
- platform: this.#login?.platform ?? '',
317
- userId: this.#login?.user?.id ?? '',
318
- token: this.#options.config.token,
319
- };
330
+ #receive(body: SatoriEventBody): void {
331
+ if (!matchesSatoriLogin(this.#options.config, body.login)) return;
332
+ this[endpointSpec].reportSelfUserId(this.#options.config.userId);
333
+ this.ingest(body as Parameters<SatoriClient['ingest']>[0]);
320
334
  }
321
335
 
322
336
  }
323
337
 
324
338
  export interface SatoriWebhookEndpointOptions {
339
+ readonly signal?: AbortSignal;
325
340
  readonly id: CapabilityId;
326
341
  readonly http: HttpHost;
327
342
  readonly config: ResolvedSatoriWebhookConfig;
328
343
  readonly callApi?: SatoriApiCaller;
329
344
  }
330
345
 
331
- export class SatoriWebhookEndpoint extends ClientEndpoint<SatoriClient> {
332
- readonly client: SatoriClient;
346
+ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
347
+ readonly [endpointSpec]: EndpointSpecification;
348
+ #open = false;
333
349
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
334
350
 
335
351
  readonly #options: SatoriWebhookEndpointOptions;
336
- #login: SatoriLogin | undefined;
337
352
  #routeReleases: HttpRouteRegistration[] = [];
338
353
  #started = false;
339
- readonly management: EndpointManagement;
340
- readonly control: EndpointControl = Object.freeze<EndpointControl>({
341
- recall: (message) => this.recall(message.id, message.conversation.id),
354
+ readonly #management: EndpointManagement;
355
+ readonly #call: SatoriActionCaller;
356
+ readonly #control: EndpointControl = Object.freeze<EndpointControl>({
357
+ recall: request => this.#recall(request),
342
358
  });
343
359
 
344
360
  constructor(options: SatoriWebhookEndpointOptions) {
345
- super();
361
+ const call = createSatoriActionCaller({ config: options.config, request: options.callApi ?? callSatoriApi, signal: options.signal });
362
+ super(satoriClientOptions({ config: options.config, call }));
363
+ this.#call = call;
346
364
  this.#logger = getAdapterLogger('satori', options.config.id);
347
365
  this.#options = options;
348
- this.client = createSatoriEndpointClient(
349
- options.config,
350
- options.callApi ?? callSatoriApi,
351
- () => this.#apiOptions(),
352
- );
353
- this.management = createSatoriEndpointManagement(
354
- (resource, method, params) => this.client.call(resource, method, params),
355
- );
356
- this.bindClientEvents(
357
- (receive) => forwardSatoriClientEvents(this.client, receive),
358
- (name, payload) => {
366
+ this.#management = createSatoriEndpointManagement(call);
367
+ this[endpointSpec] = createEndpointSpecification(this, {
368
+ name: options.config.id,
369
+ start: () => this.#start(), open: () => { this.#open = true; },
370
+ close: () => { this.#open = false; }, stop: () => this.#stop(),
371
+ send: request => this.#send(request),
372
+ management: this.#management, control: this.#control,
373
+ });
374
+ forwardSatoriClientEvents(this, (name, payload) => {
375
+ void this[endpointSpec].events.run(() => {
376
+ if (!this.#started) return;
377
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(name, error));
359
378
  if (name === 'event') this.#admitRaw(payload as SatoriEventBody);
360
- },
361
- (name, error) => this.#warnPlatformEvent(name, error),
362
- );
363
- }
364
-
365
- /** Used by webhook handler. */
366
- get isOpen(): boolean {
367
- return this.clientEventsOpen;
368
- }
369
-
370
- get config(): ResolvedSatoriWebhookConfig {
371
- return this.#options.config;
372
- }
373
-
374
- async acceptHttp(request: IncomingMessage, response: ServerResponse): Promise<void> {
375
- await this.client.acceptHttp(request, response);
379
+ }).catch(error => this.#warnPlatformEvent(name, error));
380
+ });
376
381
  }
377
382
 
378
- async start(): Promise<void> {
383
+ async #start(): Promise<void> {
379
384
  if (this.#started) return;
380
385
  this.#started = true;
381
386
  if (!this.#options.config.token) {
@@ -387,7 +392,17 @@ export class SatoriWebhookEndpoint extends ClientEndpoint<SatoriClient> {
387
392
  hint: 'set token to authenticate Satori webhook callbacks',
388
393
  }));
389
394
  }
390
- this.#routeReleases.push(...registerSatoriWebhookRoutes(this.#options.http, this));
395
+ const isOpen = () => this.#open;
396
+ this.#routeReleases.push(...registerSatoriWebhookRoutes(this.#options.http, {
397
+ config: this.#options.config,
398
+ get isOpen() { return isOpen(); },
399
+ acceptHttp: async (request, response) => {
400
+ await acceptHttpIngress<SatoriEventBody>(request, response, body => {
401
+ void this[endpointSpec].events.run(() => this.#receive(body))
402
+ .catch(error => this.#warnPlatformEvent('event', error));
403
+ });
404
+ },
405
+ }));
391
406
  this.#logger.info(formatCompact({
392
407
  op: 'listen',
393
408
  endpoint: this.#options.config.id,
@@ -396,8 +411,8 @@ export class SatoriWebhookEndpoint extends ClientEndpoint<SatoriClient> {
396
411
  }));
397
412
  }
398
413
 
399
- async stop(): Promise<void> {
400
- this.close();
414
+ async #stop(): Promise<void> {
415
+ this.#open = false;
401
416
  for (const release of this.#routeReleases.splice(0)) release();
402
417
  this.#started = false;
403
418
  this.#logger.debug(formatCompact({
@@ -405,12 +420,12 @@ export class SatoriWebhookEndpoint extends ClientEndpoint<SatoriClient> {
405
420
  }));
406
421
  }
407
422
 
408
- async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
423
+ async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
409
424
  const content = formatSatoriOutbound(payload);
410
- const result = await this.client.call('message', 'create', {
425
+ const result = await this.#call({ resource: 'message', method: 'create', signal, params: {
411
426
  channel_id: conversation.id,
412
427
  content,
413
- });
428
+ } });
414
429
  const msgId = extractCreatedMessageId(result);
415
430
  this.#logger.debug(formatCompact({
416
431
  op: 'satori_send',
@@ -422,14 +437,16 @@ export class SatoriWebhookEndpoint extends ClientEndpoint<SatoriClient> {
422
437
  }
423
438
 
424
439
  #admitRaw(body: SatoriEventBody): void {
425
- if (body.login && !this.#login) this.#login = body.login;
440
+ if (!matchesSatoriLogin(this.#options.config, body.login)) return;
426
441
  if (!isMessageEvent(body)) return;
427
- const conversation = satoriInboundConversation(String(this.#options.id), body);
442
+ const conversation = { ...satoriInboundConversation(String(this.#options.id), body), endpoint: this[endpointSpec].identity };
428
443
  const content = formatInboundContent(body);
429
444
  const sender = resolveInboundSender(body);
430
- const selfId = this.#login?.user?.id ?? body.login?.user?.id;
445
+ const selfId = this.#options.config.userId;
431
446
  const mentioned = isSelfMentioned(body, selfId);
432
- void this.emit('message.receive', {
447
+ void this[endpointSpec].events.message({
448
+ event: body,
449
+ ...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
433
450
  conversation,
434
451
  message: { conversation, id: body.message.id },
435
452
  content,
@@ -440,7 +457,7 @@ export class SatoriWebhookEndpoint extends ClientEndpoint<SatoriClient> {
440
457
  type: body.type,
441
458
  channelType: isPrivateChannelType(body) ? 'private' : 'group',
442
459
  sn: body.sn,
443
- platform: this.#login?.platform,
460
+ platform: this.#options.config.platform,
444
461
  }),
445
462
  }).catch((err) => {
446
463
  this.#logger.warn(formatCompact({
@@ -451,12 +468,12 @@ export class SatoriWebhookEndpoint extends ClientEndpoint<SatoriClient> {
451
468
  });
452
469
  }
453
470
 
454
- async recall(id: string, fallbackChannelId?: string): Promise<void> {
455
- const { channelId, messageId } = parseMessageRef(id);
456
- await this.client.call('message', 'delete', {
457
- channel_id: channelId || fallbackChannelId || '',
471
+ async #recall({ message, signal }: EndpointRecallRequest): Promise<void> {
472
+ const { channelId, messageId } = parseMessageRef(message.id);
473
+ await this.#call({ resource: 'message', method: 'delete', signal, params: {
474
+ channel_id: channelId || message.conversation.id,
458
475
  message_id: messageId,
459
- });
476
+ } });
460
477
  }
461
478
 
462
479
  #warnPlatformEvent(name: string, error: unknown): void {
@@ -467,13 +484,10 @@ export class SatoriWebhookEndpoint extends ClientEndpoint<SatoriClient> {
467
484
  }));
468
485
  }
469
486
 
470
- #apiOptions(): SatoriApiOptions {
471
- return {
472
- baseUrl: this.#options.config.baseUrl,
473
- platform: this.#login?.platform ?? '',
474
- userId: this.#login?.user?.id ?? '',
475
- token: this.#options.config.token,
476
- };
487
+ #receive(body: SatoriEventBody): void {
488
+ if (!matchesSatoriLogin(this.#options.config, body.login)) return;
489
+ this[endpointSpec].reportSelfUserId(this.#options.config.userId);
490
+ this.ingest(body as Parameters<SatoriClient['ingest']>[0]);
477
491
  }
478
492
 
479
493
  }
@@ -489,15 +503,8 @@ function isPrivateChannelType(body: SatoriEventBody): boolean {
489
503
  * 并以字符串回传给 listGroupMembers,因此保留原始字符串(仅按契约类型
490
504
  * 声明强转)是全链路最不丢信息的方案。
491
505
  */
492
- function toGroupId(id: string): number {
493
- return id as unknown as number;
494
- }
495
506
 
496
- export type SatoriManagementApi = (
497
- resource: string,
498
- method: string,
499
- params: Record<string, unknown>,
500
- ) => Promise<unknown>;
507
+ export type SatoriManagementApi = SatoriActionCaller;
501
508
 
502
509
  interface SatoriListPage {
503
510
  readonly data?: unknown[];
@@ -507,13 +514,12 @@ interface SatoriListPage {
507
514
  /** Satori 分页列表({data, next?})聚合;兼容直接返回数组的实现。 */
508
515
  async function listSatoriPages(
509
516
  api: SatoriManagementApi,
510
- resource: string,
511
- params: Record<string, unknown>,
517
+ { resource, params = {}, signal }: Omit<SatoriActionRequest, 'method'>,
512
518
  ): Promise<unknown[]> {
513
519
  const items: unknown[] = [];
514
520
  let next: string | undefined;
515
521
  do {
516
- const page = await api(resource, 'list', next ? { ...params, next } : params);
522
+ const page = await api({ resource, method: 'list', params: next ? { ...params, next } : params, signal });
517
523
  if (Array.isArray(page)) {
518
524
  items.push(...page);
519
525
  break;
@@ -537,26 +543,26 @@ function asRecord(value: unknown): Record<string, unknown> {
537
543
  */
538
544
  export function createSatoriEndpointManagement(api: SatoriManagementApi): EndpointManagement {
539
545
  return Object.freeze<EndpointManagement>({
540
- async listGroups(): Promise<readonly EndpointGroup[]> {
546
+ async listGroups(request: EndpointManagementRequest = {}): Promise<readonly EndpointGroup[]> {
541
547
  const groups: EndpointGroup[] = [];
542
- for (const value of await listSatoriPages(api, 'guild', {})) {
548
+ for (const value of await listSatoriPages(api, { resource: 'guild', signal: request.signal })) {
543
549
  const guild = asRecord(value);
544
550
  if (guild.id == null) continue;
545
551
  groups.push({
546
- group_id: toGroupId(String(guild.id)),
552
+ group_id: String(guild.id),
547
553
  name: String(guild.name ?? guild.id),
548
554
  });
549
555
  }
550
556
  return groups;
551
557
  },
552
- async listChannels(): Promise<readonly EndpointChannel[]> {
558
+ async listChannels(request: EndpointManagementRequest = {}): Promise<readonly EndpointChannel[]> {
553
559
  const channels: EndpointChannel[] = [];
554
- for (const value of await listSatoriPages(api, 'guild', {})) {
560
+ for (const value of await listSatoriPages(api, { resource: 'guild', signal: request.signal })) {
555
561
  const guild = asRecord(value);
556
562
  if (guild.id == null) continue;
557
563
  const guildId = String(guild.id);
558
564
  const guildName = String(guild.name ?? guildId);
559
- for (const channelValue of await listSatoriPages(api, 'channel', { guild_id: guildId })) {
565
+ for (const channelValue of await listSatoriPages(api, { resource: 'channel', params: { guild_id: guildId }, signal: request.signal })) {
560
566
  const channel = asRecord(channelValue);
561
567
  if (channel.id == null) continue;
562
568
  // Channel.type: 0=TEXT 1=DIRECT 2=CATEGORY 3=VOICE;缺省按 TEXT 处理
@@ -570,11 +576,16 @@ export function createSatoriEndpointManagement(api: SatoriManagementApi): Endpoi
570
576
  }
571
577
  return channels;
572
578
  },
573
- async listGroupMembers(groupId: string): Promise<readonly unknown[]> {
579
+ async listGroupMembers({ groupId, signal }: EndpointGroupRequest): Promise<readonly unknown[]> {
574
580
  // 平台形状(GuildMember[])原样返回
575
- return listSatoriPages(api, 'guild-member', { guild_id: groupId });
581
+ return listSatoriPages(api, { resource: 'guild-member', params: { guild_id: groupId }, signal });
576
582
  },
577
583
  });
578
584
  }
579
585
 
580
586
  export type { CreateSatoriWebSocket, SatoriWsSocket } from './ws.js';
587
+
588
+ /** A transport connection may multiplex logins; this Endpoint admits only its selected account. */
589
+ function matchesSatoriLogin(config: { readonly platform: string; readonly userId: string }, login: SatoriLogin | undefined): boolean {
590
+ return login?.platform === config.platform && login.user?.id === config.userId;
591
+ }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export {
14
14
  type ResolvedSatoriWsConfig,
15
15
  type SatoriEndpointConfig,
16
16
  type SatoriApiOptions,
17
+ type SatoriApiRequest,
17
18
  type SatoriChannel,
18
19
  type SatoriEventBody,
19
20
  type SatoriLogin,
@@ -48,7 +49,6 @@ export {
48
49
 
49
50
  export {
50
51
  SatoriClient,
51
- satoriClient,
52
52
  type SatoriClientEventMap,
53
53
  } from './client.js';
54
54
 
@@ -62,3 +62,4 @@ export type {
62
62
  } from '@imhelper/satori-v1';
63
63
  export { ProtocolError } from '@imhelper/satori-v1';
64
64
  export type { ProtocolErrorKind, ProtocolErrorOptions } from 'imhelper';
65
+ export { SatoriAdapter } from './adapter.js';