@zhin.js/adapter-satori 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/lib/endpoint.js CHANGED
@@ -1,34 +1,49 @@
1
+ import { acceptHttpIngress } from 'imhelper';
1
2
  /**
2
3
  * SatoriEndpoint — WebSocket and webhook lifecycle, outbound, admit.
3
4
  */
4
- import { ClientEndpoint, createEndpointLifecycle, } from 'zhin.js/adapter';
5
+ import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, } from 'zhin.js/adapter';
5
6
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
6
7
  import { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatMessageId, formatSatoriOutbound, isMessageEvent, isSelfMentioned, parseMessageRef, resolveInboundSender, satoriInboundConversation, } from './protocol.js';
7
8
  import { registerSatoriWebhookRoutes } from './webhook.js';
8
- import { createSatoriEndpointClient, forwardSatoriClientEvents } from './client.js';
9
+ import { satoriClientOptions, createSatoriActionCaller, forwardSatoriClientEvents, SatoriClient } from './client.js';
9
10
  import { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
10
- export class SatoriWsEndpoint extends ClientEndpoint {
11
- client;
11
+ export class SatoriWsEndpoint extends SatoriClient {
12
+ [endpointSpec];
13
+ #open = false;
12
14
  #logger;
13
15
  #options;
14
16
  #lifecycle;
15
17
  #ws = null;
16
- #login;
17
18
  #lastSn;
18
- management;
19
- control = Object.freeze({
20
- recall: (message) => this.recall(message.id, message.conversation.id),
19
+ #management;
20
+ #call;
21
+ #control = Object.freeze({
22
+ recall: request => this.#recall(request),
21
23
  });
22
24
  constructor(options) {
23
- super();
25
+ const call = createSatoriActionCaller({ config: options.config, request: options.callApi ?? callSatoriApi, signal: options.signal });
26
+ super(satoriClientOptions({ config: options.config, call }));
27
+ this.#call = call;
24
28
  this.#logger = getAdapterLogger('satori', options.config.id);
25
29
  this.#options = options;
26
- this.client = createSatoriEndpointClient(options.config, options.callApi ?? callSatoriApi, () => this.#apiOptions());
27
- this.management = createSatoriEndpointManagement((resource, method, params) => this.client.call(resource, method, params));
28
- this.bindClientEvents((receive) => forwardSatoriClientEvents(this.client, receive), (name, payload) => {
29
- if (name === 'event')
30
- this.#admitRaw(payload);
31
- }, (name, error) => this.#warnPlatformEvent(name, error));
30
+ this.#management = createSatoriEndpointManagement(call);
31
+ this[endpointSpec] = createEndpointSpecification(this, {
32
+ name: options.config.id,
33
+ start: () => this.#start(), open: () => { this.#open = true; },
34
+ close: () => { this.#open = false; }, stop: () => this.#stop(),
35
+ send: request => this.#send(request),
36
+ management: this.#management, control: this.#control,
37
+ });
38
+ forwardSatoriClientEvents(this, (name, payload) => {
39
+ void this[endpointSpec].events.run(() => {
40
+ if (!this.#open && !this.#lifecycle.started)
41
+ return;
42
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(name, error));
43
+ if (name === 'event')
44
+ this.#admitRaw(payload);
45
+ }).catch(error => this.#warnPlatformEvent(name, error));
46
+ });
32
47
  const { config } = options;
33
48
  this.#lifecycle = createEndpointLifecycle({
34
49
  name: config.id,
@@ -46,7 +61,7 @@ export class SatoriWsEndpoint extends ClientEndpoint {
46
61
  },
47
62
  });
48
63
  }
49
- async start() {
64
+ async #start() {
50
65
  try {
51
66
  await this.#lifecycle.start((handle) => this.#connect(handle));
52
67
  }
@@ -64,18 +79,18 @@ export class SatoriWsEndpoint extends ClientEndpoint {
64
79
  throw err;
65
80
  }
66
81
  }
67
- async stop() {
68
- this.close();
82
+ async #stop() {
83
+ this.#open = false;
69
84
  // 基座负责:清重连/心跳定时器、强关 ws、唤醒 stop-during-connect 竞态
70
85
  await this.#lifecycle.stop();
71
86
  this.#ws = null;
72
87
  }
73
- async send({ conversation, payload }) {
88
+ async #send({ conversation, payload, signal }) {
74
89
  const content = formatSatoriOutbound(payload);
75
- const result = await this.client.call('message', 'create', {
76
- channel_id: conversation.id,
77
- content,
78
- });
90
+ const result = await this.#call({ resource: 'message', method: 'create', signal, params: {
91
+ channel_id: conversation.id,
92
+ content,
93
+ } });
79
94
  const msgId = extractCreatedMessageId(result);
80
95
  this.#logger.debug(formatCompact({
81
96
  op: 'satori_send',
@@ -86,16 +101,18 @@ export class SatoriWsEndpoint extends ClientEndpoint {
86
101
  return msgId ? formatMessageId(conversation.id, msgId) : '';
87
102
  }
88
103
  #admitRaw(body) {
89
- if (body.login && !this.#login)
90
- this.#login = body.login;
104
+ if (!matchesSatoriLogin(this.#options.config, body.login))
105
+ return;
91
106
  if (!isMessageEvent(body))
92
107
  return;
93
- const conversation = satoriInboundConversation(String(this.#options.id), body);
108
+ const conversation = { ...satoriInboundConversation(String(this.#options.id), body), endpoint: this[endpointSpec].identity };
94
109
  const content = formatInboundContent(body);
95
110
  const sender = resolveInboundSender(body);
96
- const selfId = this.#login?.user?.id ?? body.login?.user?.id;
111
+ const selfId = this.#options.config.userId;
97
112
  const mentioned = isSelfMentioned(body, selfId);
98
- void this.emit('message.receive', {
113
+ void this[endpointSpec].events.message({
114
+ event: body,
115
+ ...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
99
116
  conversation,
100
117
  message: { conversation, id: body.message.id },
101
118
  content,
@@ -106,7 +123,7 @@ export class SatoriWsEndpoint extends ClientEndpoint {
106
123
  type: body.type,
107
124
  channelType: isPrivateChannelType(body) ? 'private' : 'group',
108
125
  sn: body.sn,
109
- platform: this.#login?.platform,
126
+ platform: this.#options.config.platform,
110
127
  }),
111
128
  }).catch((err) => {
112
129
  this.#logger.warn(formatCompact({
@@ -116,12 +133,12 @@ export class SatoriWsEndpoint extends ClientEndpoint {
116
133
  }));
117
134
  });
118
135
  }
119
- async recall(id, fallbackChannelId) {
120
- const { channelId, messageId } = parseMessageRef(id);
121
- await this.client.call('message', 'delete', {
122
- channel_id: channelId || fallbackChannelId || '',
123
- message_id: messageId,
124
- });
136
+ async #recall({ message, signal }) {
137
+ const { channelId, messageId } = parseMessageRef(message.id);
138
+ await this.#call({ resource: 'message', method: 'delete', signal, params: {
139
+ channel_id: channelId || message.conversation.id,
140
+ message_id: messageId,
141
+ } });
125
142
  }
126
143
  #warnPlatformEvent(name, error) {
127
144
  this.#logger.warn(formatCompact({
@@ -149,6 +166,8 @@ export class SatoriWsEndpoint extends ClientEndpoint {
149
166
  }
150
167
  });
151
168
  ws.on('open', () => {
169
+ if (this.#ws !== ws || !this.#lifecycle.started)
170
+ return;
152
171
  this.#logger.debug(formatCompact({ mode: 'ws' }));
153
172
  this.#sendSignal(SatoriOpcode.IDENTIFY, {
154
173
  token: config.token,
@@ -161,6 +180,8 @@ export class SatoriWsEndpoint extends ClientEndpoint {
161
180
  }
162
181
  });
163
182
  ws.on('message', (data) => {
183
+ if (this.#ws !== ws || !this.#lifecycle.started)
184
+ return;
164
185
  try {
165
186
  const raw = typeof data === 'string'
166
187
  ? data
@@ -211,7 +232,7 @@ export class SatoriWsEndpoint extends ClientEndpoint {
211
232
  if (signal.op === SatoriOpcode.PONG) {
212
233
  // 喂狗:复位基座看门狗计数
213
234
  this.#lifecycle.notifyHeartbeatAck();
214
- void this.emitPlatform('pong', signal).catch((error) => {
235
+ void this[endpointSpec].events.platform('pong', signal).catch((error) => {
215
236
  this.#logger.warn(formatCompact({
216
237
  op: 'satori_platform_event_failed',
217
238
  event: 'pong',
@@ -222,23 +243,28 @@ export class SatoriWsEndpoint extends ClientEndpoint {
222
243
  }
223
244
  if (signal.op === SatoriOpcode.READY && signal.body?.logins) {
224
245
  const logins = signal.body.logins;
225
- this.#login = logins[0];
226
- void this.emitPlatform('ready', signal).catch((error) => {
246
+ const login = logins.find(candidate => matchesSatoriLogin(this.#options.config, candidate));
247
+ if (login?.user?.id)
248
+ this[endpointSpec].reportSelfUserId(login.user.id);
249
+ void this[endpointSpec].events.platform('ready', signal).catch((error) => {
227
250
  this.#logger.warn(formatCompact({
228
251
  op: 'satori_platform_event_failed',
229
252
  event: 'ready',
230
253
  error: error instanceof Error ? error.message : String(error),
231
254
  }));
232
255
  });
233
- if (!this.#login?.platform || !this.#login?.user?.id) {
256
+ if (!login) {
234
257
  this.#logger.warn(formatCompact({ op: 'ready', ok: false, error: 'missing platform/user' }));
235
258
  }
236
259
  return;
237
260
  }
238
261
  if (signal.op === SatoriOpcode.EVENT && signal.body) {
239
- if (typeof signal.body.sn === 'number')
240
- this.#lastSn = signal.body.sn;
241
- this.client.ingest(signal.body);
262
+ const body = signal.body;
263
+ void this[endpointSpec].events.run(() => {
264
+ if (typeof body.sn === 'number')
265
+ this.#lastSn = body.sn;
266
+ this.#receive(body);
267
+ }).catch(error => this.#warnPlatformEvent('event', error));
242
268
  }
243
269
  }
244
270
  #sendSignal(op, body) {
@@ -246,48 +272,50 @@ export class SatoriWsEndpoint extends ClientEndpoint {
246
272
  return;
247
273
  this.#ws.send(JSON.stringify({ op, body: body ?? {} }));
248
274
  }
249
- #apiOptions() {
250
- return {
251
- baseUrl: this.#options.config.baseUrl,
252
- platform: this.#login?.platform ?? '',
253
- userId: this.#login?.user?.id ?? '',
254
- token: this.#options.config.token,
255
- };
275
+ #receive(body) {
276
+ if (!matchesSatoriLogin(this.#options.config, body.login))
277
+ return;
278
+ this[endpointSpec].reportSelfUserId(this.#options.config.userId);
279
+ this.ingest(body);
256
280
  }
257
281
  }
258
- export class SatoriWebhookEndpoint extends ClientEndpoint {
259
- client;
282
+ export class SatoriWebhookEndpoint extends SatoriClient {
283
+ [endpointSpec];
284
+ #open = false;
260
285
  #logger;
261
286
  #options;
262
- #login;
263
287
  #routeReleases = [];
264
288
  #started = false;
265
- management;
266
- control = Object.freeze({
267
- recall: (message) => this.recall(message.id, message.conversation.id),
289
+ #management;
290
+ #call;
291
+ #control = Object.freeze({
292
+ recall: request => this.#recall(request),
268
293
  });
269
294
  constructor(options) {
270
- super();
295
+ const call = createSatoriActionCaller({ config: options.config, request: options.callApi ?? callSatoriApi, signal: options.signal });
296
+ super(satoriClientOptions({ config: options.config, call }));
297
+ this.#call = call;
271
298
  this.#logger = getAdapterLogger('satori', options.config.id);
272
299
  this.#options = options;
273
- this.client = createSatoriEndpointClient(options.config, options.callApi ?? callSatoriApi, () => this.#apiOptions());
274
- this.management = createSatoriEndpointManagement((resource, method, params) => this.client.call(resource, method, params));
275
- this.bindClientEvents((receive) => forwardSatoriClientEvents(this.client, receive), (name, payload) => {
276
- if (name === 'event')
277
- this.#admitRaw(payload);
278
- }, (name, error) => this.#warnPlatformEvent(name, error));
279
- }
280
- /** Used by webhook handler. */
281
- get isOpen() {
282
- return this.clientEventsOpen;
283
- }
284
- get config() {
285
- return this.#options.config;
286
- }
287
- async acceptHttp(request, response) {
288
- await this.client.acceptHttp(request, response);
300
+ this.#management = createSatoriEndpointManagement(call);
301
+ this[endpointSpec] = createEndpointSpecification(this, {
302
+ name: options.config.id,
303
+ start: () => this.#start(), open: () => { this.#open = true; },
304
+ close: () => { this.#open = false; }, stop: () => this.#stop(),
305
+ send: request => this.#send(request),
306
+ management: this.#management, control: this.#control,
307
+ });
308
+ forwardSatoriClientEvents(this, (name, payload) => {
309
+ void this[endpointSpec].events.run(() => {
310
+ if (!this.#started)
311
+ return;
312
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(name, error));
313
+ if (name === 'event')
314
+ this.#admitRaw(payload);
315
+ }).catch(error => this.#warnPlatformEvent(name, error));
316
+ });
289
317
  }
290
- async start() {
318
+ async #start() {
291
319
  if (this.#started)
292
320
  return;
293
321
  this.#started = true;
@@ -300,7 +328,17 @@ export class SatoriWebhookEndpoint extends ClientEndpoint {
300
328
  hint: 'set token to authenticate Satori webhook callbacks',
301
329
  }));
302
330
  }
303
- this.#routeReleases.push(...registerSatoriWebhookRoutes(this.#options.http, this));
331
+ const isOpen = () => this.#open;
332
+ this.#routeReleases.push(...registerSatoriWebhookRoutes(this.#options.http, {
333
+ config: this.#options.config,
334
+ get isOpen() { return isOpen(); },
335
+ acceptHttp: async (request, response) => {
336
+ await acceptHttpIngress(request, response, body => {
337
+ void this[endpointSpec].events.run(() => this.#receive(body))
338
+ .catch(error => this.#warnPlatformEvent('event', error));
339
+ });
340
+ },
341
+ }));
304
342
  this.#logger.info(formatCompact({
305
343
  op: 'listen',
306
344
  endpoint: this.#options.config.id,
@@ -308,8 +346,8 @@ export class SatoriWebhookEndpoint extends ClientEndpoint {
308
346
  path: this.#options.config.path,
309
347
  }));
310
348
  }
311
- async stop() {
312
- this.close();
349
+ async #stop() {
350
+ this.#open = false;
313
351
  for (const release of this.#routeReleases.splice(0))
314
352
  release();
315
353
  this.#started = false;
@@ -317,12 +355,12 @@ export class SatoriWebhookEndpoint extends ClientEndpoint {
317
355
  op: 'disconnect',
318
356
  }));
319
357
  }
320
- async send({ conversation, payload }) {
358
+ async #send({ conversation, payload, signal }) {
321
359
  const content = formatSatoriOutbound(payload);
322
- const result = await this.client.call('message', 'create', {
323
- channel_id: conversation.id,
324
- content,
325
- });
360
+ const result = await this.#call({ resource: 'message', method: 'create', signal, params: {
361
+ channel_id: conversation.id,
362
+ content,
363
+ } });
326
364
  const msgId = extractCreatedMessageId(result);
327
365
  this.#logger.debug(formatCompact({
328
366
  op: 'satori_send',
@@ -333,16 +371,18 @@ export class SatoriWebhookEndpoint extends ClientEndpoint {
333
371
  return msgId ? formatMessageId(conversation.id, msgId) : '';
334
372
  }
335
373
  #admitRaw(body) {
336
- if (body.login && !this.#login)
337
- this.#login = body.login;
374
+ if (!matchesSatoriLogin(this.#options.config, body.login))
375
+ return;
338
376
  if (!isMessageEvent(body))
339
377
  return;
340
- const conversation = satoriInboundConversation(String(this.#options.id), body);
378
+ const conversation = { ...satoriInboundConversation(String(this.#options.id), body), endpoint: this[endpointSpec].identity };
341
379
  const content = formatInboundContent(body);
342
380
  const sender = resolveInboundSender(body);
343
- const selfId = this.#login?.user?.id ?? body.login?.user?.id;
381
+ const selfId = this.#options.config.userId;
344
382
  const mentioned = isSelfMentioned(body, selfId);
345
- void this.emit('message.receive', {
383
+ void this[endpointSpec].events.message({
384
+ event: body,
385
+ ...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
346
386
  conversation,
347
387
  message: { conversation, id: body.message.id },
348
388
  content,
@@ -353,7 +393,7 @@ export class SatoriWebhookEndpoint extends ClientEndpoint {
353
393
  type: body.type,
354
394
  channelType: isPrivateChannelType(body) ? 'private' : 'group',
355
395
  sn: body.sn,
356
- platform: this.#login?.platform,
396
+ platform: this.#options.config.platform,
357
397
  }),
358
398
  }).catch((err) => {
359
399
  this.#logger.warn(formatCompact({
@@ -363,12 +403,12 @@ export class SatoriWebhookEndpoint extends ClientEndpoint {
363
403
  }));
364
404
  });
365
405
  }
366
- async recall(id, fallbackChannelId) {
367
- const { channelId, messageId } = parseMessageRef(id);
368
- await this.client.call('message', 'delete', {
369
- channel_id: channelId || fallbackChannelId || '',
370
- message_id: messageId,
371
- });
406
+ async #recall({ message, signal }) {
407
+ const { channelId, messageId } = parseMessageRef(message.id);
408
+ await this.#call({ resource: 'message', method: 'delete', signal, params: {
409
+ channel_id: channelId || message.conversation.id,
410
+ message_id: messageId,
411
+ } });
372
412
  }
373
413
  #warnPlatformEvent(name, error) {
374
414
  this.#logger.warn(formatCompact({
@@ -377,34 +417,23 @@ export class SatoriWebhookEndpoint extends ClientEndpoint {
377
417
  error: error instanceof Error ? error.message : String(error),
378
418
  }));
379
419
  }
380
- #apiOptions() {
381
- return {
382
- baseUrl: this.#options.config.baseUrl,
383
- platform: this.#login?.platform ?? '',
384
- userId: this.#login?.user?.id ?? '',
385
- token: this.#options.config.token,
386
- };
420
+ #receive(body) {
421
+ if (!matchesSatoriLogin(this.#options.config, body.login))
422
+ return;
423
+ this[endpointSpec].reportSelfUserId(this.#options.config.userId);
424
+ this.ingest(body);
387
425
  }
388
426
  }
389
427
  function isPrivateChannelType(body) {
390
428
  const channel = body.channel ?? body.message?.channel;
391
429
  return channel?.type === 1;
392
430
  }
393
- /**
394
- * Satori guild id 是平台相关字符串(多数平台为雪花号,超
395
- * Number.MAX_SAFE_INTEGER)。Console 社交面只把 group_id 当 JSON 值透传、
396
- * 并以字符串回传给 listGroupMembers,因此保留原始字符串(仅按契约类型
397
- * 声明强转)是全链路最不丢信息的方案。
398
- */
399
- function toGroupId(id) {
400
- return id;
401
- }
402
431
  /** Satori 分页列表({data, next?})聚合;兼容直接返回数组的实现。 */
403
- async function listSatoriPages(api, resource, params) {
432
+ async function listSatoriPages(api, { resource, params = {}, signal }) {
404
433
  const items = [];
405
434
  let next;
406
435
  do {
407
- const page = await api(resource, 'list', next ? { ...params, next } : params);
436
+ const page = await api({ resource, method: 'list', params: next ? { ...params, next } : params, signal });
408
437
  if (Array.isArray(page)) {
409
438
  items.push(...page);
410
439
  break;
@@ -427,28 +456,28 @@ function asRecord(value) {
427
456
  */
428
457
  export function createSatoriEndpointManagement(api) {
429
458
  return Object.freeze({
430
- async listGroups() {
459
+ async listGroups(request = {}) {
431
460
  const groups = [];
432
- for (const value of await listSatoriPages(api, 'guild', {})) {
461
+ for (const value of await listSatoriPages(api, { resource: 'guild', signal: request.signal })) {
433
462
  const guild = asRecord(value);
434
463
  if (guild.id == null)
435
464
  continue;
436
465
  groups.push({
437
- group_id: toGroupId(String(guild.id)),
466
+ group_id: String(guild.id),
438
467
  name: String(guild.name ?? guild.id),
439
468
  });
440
469
  }
441
470
  return groups;
442
471
  },
443
- async listChannels() {
472
+ async listChannels(request = {}) {
444
473
  const channels = [];
445
- for (const value of await listSatoriPages(api, 'guild', {})) {
474
+ for (const value of await listSatoriPages(api, { resource: 'guild', signal: request.signal })) {
446
475
  const guild = asRecord(value);
447
476
  if (guild.id == null)
448
477
  continue;
449
478
  const guildId = String(guild.id);
450
479
  const guildName = String(guild.name ?? guildId);
451
- for (const channelValue of await listSatoriPages(api, 'channel', { guild_id: guildId })) {
480
+ for (const channelValue of await listSatoriPages(api, { resource: 'channel', params: { guild_id: guildId }, signal: request.signal })) {
452
481
  const channel = asRecord(channelValue);
453
482
  if (channel.id == null)
454
483
  continue;
@@ -464,9 +493,13 @@ export function createSatoriEndpointManagement(api) {
464
493
  }
465
494
  return channels;
466
495
  },
467
- async listGroupMembers(groupId) {
496
+ async listGroupMembers({ groupId, signal }) {
468
497
  // 平台形状(GuildMember[])原样返回
469
- return listSatoriPages(api, 'guild-member', { guild_id: groupId });
498
+ return listSatoriPages(api, { resource: 'guild-member', params: { guild_id: groupId }, signal });
470
499
  },
471
500
  });
472
501
  }
502
+ /** A transport connection may multiplex logins; this Endpoint admits only its selected account. */
503
+ function matchesSatoriLogin(config, login) {
504
+ return login?.platform === config.platform && login.user?.id === config.userId;
505
+ }
package/lib/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatSatoriOutbound, isMessageEvent, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, type ResolvedSatoriWebhookConfig, type ResolvedSatoriWsConfig, type SatoriEndpointConfig, type SatoriApiOptions, type SatoriChannel, type SatoriEventBody, type SatoriLogin, type SatoriMessage, type SatoriSignal, type SatoriUser, type SatoriWireSegment, } from './protocol.js';
1
+ export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatSatoriOutbound, isMessageEvent, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, type ResolvedSatoriWebhookConfig, type ResolvedSatoriWsConfig, type SatoriEndpointConfig, type SatoriApiOptions, type SatoriApiRequest, type SatoriChannel, type SatoriEventBody, type SatoriLogin, type SatoriMessage, type SatoriSignal, type SatoriUser, type SatoriWireSegment, } from './protocol.js';
2
2
  export { SatoriWebhookEndpoint, SatoriWsEndpoint, type CreateSatoriWebSocket, type SatoriApiCaller, type SatoriWebhookEndpointOptions, type SatoriWsEndpointOptions, type SatoriWsSocket, } from './endpoint.js';
3
3
  export { handleSatoriWebhookRequest, registerSatoriWebhookRoutes, resolveSatoriOpcode, verifySatoriToken, type SatoriWebhookHandler, } from './webhook.js';
4
4
  export { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
5
- export { SatoriClient, satoriClient, type SatoriClientEventMap, } from './client.js';
5
+ export { SatoriClient, type SatoriClientEventMap, } from './client.js';
6
6
  export type { SatoriAdapterConfig as ImHelperSatoriAdapterConfig, SatoriActionUrlResolver, SatoriCall, SatoriV1ClientConfig, SatoriV1Event, SatoriV1Response, } from '@imhelper/satori-v1';
7
7
  export { ProtocolError } from '@imhelper/satori-v1';
8
8
  export type { ProtocolErrorKind, ProtocolErrorOptions } from 'imhelper';
9
+ export { SatoriAdapter } from './adapter.js';
package/lib/index.js CHANGED
@@ -2,5 +2,6 @@ export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, forma
2
2
  export { SatoriWebhookEndpoint, SatoriWsEndpoint, } from './endpoint.js';
3
3
  export { handleSatoriWebhookRequest, registerSatoriWebhookRoutes, resolveSatoriOpcode, verifySatoriToken, } from './webhook.js';
4
4
  export { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
5
- export { SatoriClient, satoriClient, } from './client.js';
5
+ export { SatoriClient, } from './client.js';
6
6
  export { ProtocolError } from '@imhelper/satori-v1';
7
+ export { SatoriAdapter } from './adapter.js';
package/lib/protocol.d.ts CHANGED
@@ -67,6 +67,7 @@ export interface SatoriApiOptions {
67
67
  readonly platform: string;
68
68
  readonly userId: string;
69
69
  readonly token?: string;
70
+ readonly signal?: AbortSignal;
70
71
  }
71
72
  export interface SatoriWireSegment {
72
73
  readonly type: string;
@@ -75,6 +76,9 @@ export interface SatoriWireSegment {
75
76
  /** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
76
77
  export interface SatoriEndpointConfig {
77
78
  readonly id: string;
79
+ /** Exact remote login selected by this configured Endpoint. */
80
+ readonly platform: string;
81
+ readonly userId: string;
78
82
  readonly connection?: 'ws' | 'webhook';
79
83
  readonly baseUrl: string;
80
84
  readonly token?: string;
@@ -86,6 +90,8 @@ export interface ResolvedSatoriWsConfig {
86
90
  readonly context: 'satori';
87
91
  readonly connection: 'ws';
88
92
  readonly id: string;
93
+ readonly platform: string;
94
+ readonly userId: string;
89
95
  readonly baseUrl: string;
90
96
  readonly token?: string;
91
97
  readonly heartbeat_interval: number;
@@ -94,6 +100,8 @@ export interface ResolvedSatoriWebhookConfig {
94
100
  readonly context: 'satori';
95
101
  readonly connection: 'webhook';
96
102
  readonly id: string;
103
+ readonly platform: string;
104
+ readonly userId: string;
97
105
  readonly baseUrl: string;
98
106
  readonly token?: string;
99
107
  readonly path: string;
@@ -110,7 +118,12 @@ export declare function buildWsUrl(baseUrl: string, token?: string): string;
110
118
  * Call Satori HTTP API: POST {baseUrl}/v1/{resource}.{method}
111
119
  * @see https://satori.chat/en-US/protocol/api.html
112
120
  */
113
- export declare function callSatoriApi<T = unknown>(options: SatoriApiOptions, resource: string, method: string, params?: Record<string, unknown>): Promise<T>;
121
+ export interface SatoriApiRequest extends SatoriApiOptions {
122
+ readonly resource: string;
123
+ readonly method: string;
124
+ readonly params?: Record<string, unknown>;
125
+ }
126
+ export declare function callSatoriApi<T = unknown>({ resource, method, params, ...options }: SatoriApiRequest): Promise<T>;
114
127
  /** Build inbound text for OutboundMessageService.receive. */
115
128
  export declare function formatInboundContent(body: SatoriEventBody & {
116
129
  message: SatoriMessage;
package/lib/protocol.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Satori protocol helpers. Spec: https://satori.chat/zh-CN/protocol/overview.html */
2
- import { isMediaRef } from '@zhin.js/im-contract';
2
+ import { EndpointDeliveryError, isMediaRef } from '@zhin.js/im-contract';
3
3
  import { formatCompact, getLogger } from '@zhin.js/logger';
4
4
  const logger = getLogger('satori');
5
5
  /** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
@@ -14,6 +14,8 @@ export const SatoriOpcode = {
14
14
  export function resolveSatoriConfig(config) {
15
15
  const connection = config.connection ?? 'ws';
16
16
  const id = requiredEndpointField(config.id, 'id');
17
+ const platform = requiredEndpointField(config.platform, 'platform');
18
+ const userId = requiredEndpointField(config.userId, 'userId');
17
19
  const baseUrl = requiredEndpointField(config.baseUrl, 'baseUrl');
18
20
  const token = typeof config.token === 'string' && config.token.trim()
19
21
  ? config.token.trim()
@@ -24,6 +26,8 @@ export function resolveSatoriConfig(config) {
24
26
  context: 'satori',
25
27
  connection: 'webhook',
26
28
  id,
29
+ platform,
30
+ userId,
27
31
  baseUrl,
28
32
  token,
29
33
  path,
@@ -34,6 +38,8 @@ export function resolveSatoriConfig(config) {
34
38
  context: 'satori',
35
39
  connection: 'ws',
36
40
  id,
41
+ platform,
42
+ userId,
37
43
  baseUrl,
38
44
  token,
39
45
  heartbeat_interval: heartbeat,
@@ -59,11 +65,7 @@ export function buildWsUrl(baseUrl, token) {
59
65
  url.searchParams.set('access_token', token);
60
66
  return url.toString();
61
67
  }
62
- /**
63
- * Call Satori HTTP API: POST {baseUrl}/v1/{resource}.{method}
64
- * @see https://satori.chat/en-US/protocol/api.html
65
- */
66
- export async function callSatoriApi(options, resource, method, params = {}) {
68
+ export async function callSatoriApi({ resource, method, params = {}, ...options }) {
67
69
  const { baseUrl, platform, userId, token } = options;
68
70
  const url = `${baseUrl.replace(/\/$/, '')}/v1/${resource}.${method}`;
69
71
  const headers = {
@@ -73,11 +75,16 @@ export async function callSatoriApi(options, resource, method, params = {}) {
73
75
  };
74
76
  if (token)
75
77
  headers.Authorization = `Bearer ${token}`;
76
- const res = await fetch(url, {
77
- method: 'POST',
78
- headers,
79
- body: JSON.stringify(params),
80
- });
78
+ if (options.signal?.aborted)
79
+ throw new EndpointDeliveryError('endpoint_inactive', 'Satori request ended before submission', 'not_sent', { cause: options.signal.reason });
80
+ const body = JSON.stringify(params);
81
+ let res;
82
+ try {
83
+ res = await fetch(url, { method: 'POST', headers, body, signal: options.signal });
84
+ }
85
+ catch (cause) {
86
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Satori HTTP request outcome is unknown', 'unknown', { cause });
87
+ }
81
88
  const text = await res.text();
82
89
  if (res.status === 401)
83
90
  throw new Error(`Satori API 认证失败: ${text}`);