@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/src/endpoint.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { acceptHttpIngress } from 'imhelper';
|
|
2
2
|
/**
|
|
3
3
|
* SatoriEndpoint — WebSocket and webhook lifecycle, outbound, admit.
|
|
4
4
|
*/
|
|
5
5
|
import {
|
|
6
|
-
|
|
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 {
|
|
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
|
|
58
|
-
readonly
|
|
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
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
options.
|
|
78
|
-
() => this.#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
this
|
|
84
|
-
(
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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.#
|
|
163
|
+
const selfId = this.#options.config.userId;
|
|
155
164
|
const mentioned = isSelfMentioned(body, selfId);
|
|
156
|
-
void this.
|
|
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.#
|
|
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(
|
|
179
|
-
const { channelId, messageId } = parseMessageRef(id);
|
|
180
|
-
await this
|
|
181
|
-
channel_id: channelId ||
|
|
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.
|
|
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
|
-
|
|
290
|
-
|
|
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 (!
|
|
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
|
-
|
|
304
|
-
this.
|
|
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
|
-
#
|
|
314
|
-
return
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
332
|
-
readonly
|
|
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
|
|
341
|
-
|
|
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
|
-
|
|
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
|
|
349
|
-
|
|
350
|
-
options.
|
|
351
|
-
() => this.#
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
);
|
|
356
|
-
this
|
|
357
|
-
(
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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.#
|
|
445
|
+
const selfId = this.#options.config.userId;
|
|
431
446
|
const mentioned = isSelfMentioned(body, selfId);
|
|
432
|
-
void this.
|
|
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.#
|
|
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(
|
|
455
|
-
const { channelId, messageId } = parseMessageRef(id);
|
|
456
|
-
await this
|
|
457
|
-
channel_id: channelId ||
|
|
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
|
-
#
|
|
471
|
-
return
|
|
472
|
-
|
|
473
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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';
|