@zhin.js/adapter-satori 1.1.18 → 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 +36 -0
- package/README.md +15 -9
- package/adapters/satori/index.js +3 -23
- package/adapters/satori/index.ts +2 -23
- package/commands/satori/endpoint/definition.js +2 -2
- package/commands/satori/endpoint/definition.ts +2 -2
- package/lib/adapter.d.ts +19 -0
- package/lib/adapter.js +29 -0
- package/lib/client.d.ts +19 -11
- package/lib/client.js +16 -11
- package/lib/console-spec.js +2 -0
- package/lib/console.d.ts +1 -1
- package/lib/console.js +2 -5
- package/lib/endpoint.d.ts +15 -24
- package/lib/endpoint.js +152 -119
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/protocol.d.ts +14 -1
- package/lib/protocol.js +18 -11
- package/package.json +15 -15
- package/plugin.js +0 -4
- package/schema.json +15 -3
- package/src/adapter.ts +32 -0
- package/src/client.ts +30 -25
- package/src/console-spec.ts +2 -0
- package/src/console.ts +2 -5
- package/src/endpoint.ts +142 -131
- package/src/index.ts +2 -1
- package/src/protocol.ts +31 -11
- package/lib/satori-runtime-state.d.ts +0 -1
- package/lib/satori-runtime-state.js +0 -6
- package/src/satori-runtime-state.ts +0 -7
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 {
|
|
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 {
|
|
9
|
+
import { satoriClientOptions, createSatoriActionCaller, forwardSatoriClientEvents, SatoriClient } from './client.js';
|
|
9
10
|
import { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
|
|
10
|
-
export class SatoriWsEndpoint extends
|
|
11
|
-
|
|
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
|
-
|
|
20
|
-
|
|
19
|
+
#management;
|
|
20
|
+
#call;
|
|
21
|
+
#control = Object.freeze({
|
|
22
|
+
recall: request => this.#recall(request),
|
|
21
23
|
});
|
|
22
24
|
constructor(options) {
|
|
23
|
-
|
|
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
|
|
27
|
-
this
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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 (
|
|
90
|
-
|
|
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.#
|
|
111
|
+
const selfId = this.#options.config.userId;
|
|
97
112
|
const mentioned = isSelfMentioned(body, selfId);
|
|
98
|
-
void this.
|
|
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.#
|
|
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(
|
|
120
|
-
const { channelId, messageId } = parseMessageRef(id);
|
|
121
|
-
await this
|
|
122
|
-
|
|
123
|
-
|
|
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.
|
|
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
|
-
|
|
226
|
-
|
|
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 (!
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
#
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
259
|
-
|
|
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
|
-
|
|
267
|
-
|
|
289
|
+
#management;
|
|
290
|
+
#call;
|
|
291
|
+
#control = Object.freeze({
|
|
292
|
+
recall: request => this.#recall(request),
|
|
268
293
|
});
|
|
269
294
|
constructor(options) {
|
|
270
|
-
|
|
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
|
|
274
|
-
this
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
323
|
-
|
|
324
|
-
|
|
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 (
|
|
337
|
-
|
|
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.#
|
|
381
|
+
const selfId = this.#options.config.userId;
|
|
344
382
|
const mentioned = isSelfMentioned(body, selfId);
|
|
345
|
-
void this.
|
|
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.#
|
|
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(
|
|
367
|
-
const { channelId, messageId } = parseMessageRef(id);
|
|
368
|
-
await this
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
#
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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}`);
|