@zhin.js/adapter-satori 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +828 -0
  2. package/README.md +60 -38
  3. package/adapters/satori.js +38 -0
  4. package/adapters/satori.ts +49 -0
  5. package/commands/endpoint/add/[id].js +3 -0
  6. package/commands/endpoint/add/[id].ts +3 -0
  7. package/commands/endpoint/list.js +3 -0
  8. package/commands/endpoint/list.ts +3 -0
  9. package/commands/endpoint/remove/[id].js +3 -0
  10. package/commands/endpoint/remove/[id].ts +3 -0
  11. package/lib/client.d.ts +18 -0
  12. package/lib/client.js +21 -0
  13. package/lib/endpoint.d.ts +56 -0
  14. package/lib/endpoint.js +472 -0
  15. package/lib/index.d.ts +8 -18
  16. package/lib/index.js +6 -25
  17. package/lib/protocol.d.ts +158 -0
  18. package/lib/protocol.js +242 -0
  19. package/lib/satori-endpoint-commands.d.ts +1 -0
  20. package/lib/satori-endpoint-commands.js +18 -0
  21. package/lib/satori-runtime-state.d.ts +1 -0
  22. package/lib/satori-runtime-state.js +6 -0
  23. package/lib/webhook.d.ts +12 -0
  24. package/lib/webhook.js +60 -0
  25. package/lib/ws.d.ts +13 -0
  26. package/lib/ws.js +5 -0
  27. package/package.json +64 -18
  28. package/plugin.js +14 -0
  29. package/schema.json +95 -0
  30. package/src/client.ts +57 -0
  31. package/src/endpoint.ts +580 -0
  32. package/src/index.ts +61 -35
  33. package/src/protocol.ts +370 -0
  34. package/src/satori-endpoint-commands.ts +19 -0
  35. package/src/satori-runtime-state.ts +7 -0
  36. package/src/webhook.ts +79 -0
  37. package/src/ws.ts +22 -0
  38. package/lib/adapter.d.ts +0 -17
  39. package/lib/adapter.d.ts.map +0 -1
  40. package/lib/adapter.js +0 -35
  41. package/lib/adapter.js.map +0 -1
  42. package/lib/api.d.ts +0 -15
  43. package/lib/api.d.ts.map +0 -1
  44. package/lib/api.js +0 -37
  45. package/lib/api.js.map +0 -1
  46. package/lib/endpoint-webhook.d.ts +0 -27
  47. package/lib/endpoint-webhook.d.ts.map +0 -1
  48. package/lib/endpoint-webhook.js +0 -99
  49. package/lib/endpoint-webhook.js.map +0 -1
  50. package/lib/endpoint-ws.d.ts +0 -30
  51. package/lib/endpoint-ws.d.ts.map +0 -1
  52. package/lib/endpoint-ws.js +0 -195
  53. package/lib/endpoint-ws.js.map +0 -1
  54. package/lib/index.d.ts.map +0 -1
  55. package/lib/index.js.map +0 -1
  56. package/lib/types.d.ts +0 -91
  57. package/lib/types.d.ts.map +0 -1
  58. package/lib/types.js +0 -13
  59. package/lib/types.js.map +0 -1
  60. package/lib/utils.d.ts +0 -40
  61. package/lib/utils.d.ts.map +0 -1
  62. package/lib/utils.js +0 -33
  63. package/lib/utils.js.map +0 -1
  64. package/src/adapter.ts +0 -48
  65. package/src/api.ts +0 -48
  66. package/src/endpoint-webhook.ts +0 -117
  67. package/src/endpoint-ws.ts +0 -214
  68. package/src/types.ts +0 -91
  69. package/src/utils.ts +0 -64
  70. /package/{skills/satori/SKILL.md → agent/skills/satori.md} +0 -0
@@ -0,0 +1,472 @@
1
+ /**
2
+ * SatoriEndpoint — WebSocket and webhook lifecycle, outbound, admit.
3
+ */
4
+ import { ClientEndpoint, createEndpointLifecycle, } from 'zhin.js/adapter';
5
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
6
+ import { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatMessageId, formatSatoriOutbound, isMessageEvent, isSelfMentioned, parseMessageRef, resolveInboundSender, satoriInboundConversation, } from './protocol.js';
7
+ import { registerSatoriWebhookRoutes } from './webhook.js';
8
+ import { createSatoriEndpointClient, forwardSatoriClientEvents } from './client.js';
9
+ import { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
10
+ export class SatoriWsEndpoint extends ClientEndpoint {
11
+ client;
12
+ #logger;
13
+ #options;
14
+ #lifecycle;
15
+ #ws = null;
16
+ #login;
17
+ #lastSn;
18
+ management;
19
+ control = Object.freeze({
20
+ recall: (message) => this.recall(message.id, message.conversation.id),
21
+ });
22
+ constructor(options) {
23
+ super();
24
+ this.#logger = getAdapterLogger('satori', options.config.id);
25
+ 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));
32
+ const { config } = options;
33
+ this.#lifecycle = createEndpointLifecycle({
34
+ name: config.id,
35
+ reconnect: {
36
+ initialIntervalMs: 5_000,
37
+ // 固定间隔(multiplier 1、无抖动),对齐旧 5s 固定重连语义
38
+ multiplier: 1,
39
+ maxIntervalMs: 5_000,
40
+ jitterMs: 0,
41
+ },
42
+ heartbeat: {
43
+ intervalMs: config.heartbeat_interval,
44
+ // PONG 看门狗:连续 2 轮无回包,下一轮心跳由基座强关连接触发重连
45
+ watchdogMisses: 2,
46
+ },
47
+ });
48
+ }
49
+ async start() {
50
+ try {
51
+ await this.#lifecycle.start((handle) => this.#connect(handle));
52
+ }
53
+ catch (err) {
54
+ // start 失败清理现场(状态复位由基座保证)
55
+ if (this.#ws) {
56
+ try {
57
+ this.#ws.close();
58
+ }
59
+ catch {
60
+ /* ignore */
61
+ }
62
+ this.#ws = null;
63
+ }
64
+ throw err;
65
+ }
66
+ }
67
+ async stop() {
68
+ this.close();
69
+ // 基座负责:清重连/心跳定时器、强关 ws、唤醒 stop-during-connect 竞态
70
+ await this.#lifecycle.stop();
71
+ this.#ws = null;
72
+ }
73
+ async send({ conversation, payload }) {
74
+ const content = formatSatoriOutbound(payload);
75
+ const result = await this.client.call('message', 'create', {
76
+ channel_id: conversation.id,
77
+ content,
78
+ });
79
+ const msgId = extractCreatedMessageId(result);
80
+ this.#logger.debug(formatCompact({
81
+ op: 'satori_send',
82
+ endpoint: this.#options.config.id,
83
+ channel: conversation.id,
84
+ messageId: msgId || undefined,
85
+ }));
86
+ return msgId ? formatMessageId(conversation.id, msgId) : '';
87
+ }
88
+ #admitRaw(body) {
89
+ if (body.login && !this.#login)
90
+ this.#login = body.login;
91
+ if (!isMessageEvent(body))
92
+ return;
93
+ const conversation = satoriInboundConversation(String(this.#options.id), body);
94
+ const content = formatInboundContent(body);
95
+ const sender = resolveInboundSender(body);
96
+ const selfId = this.#login?.user?.id ?? body.login?.user?.id;
97
+ const mentioned = isSelfMentioned(body, selfId);
98
+ void this.emit('message.receive', {
99
+ conversation,
100
+ message: { conversation, id: body.message.id },
101
+ content,
102
+ sender,
103
+ endpointId: this.#options.config.id,
104
+ ...(mentioned ? { mentioned: true } : {}),
105
+ metadata: Object.freeze({
106
+ type: body.type,
107
+ channelType: isPrivateChannelType(body) ? 'private' : 'group',
108
+ sn: body.sn,
109
+ platform: this.#login?.platform,
110
+ }),
111
+ }).catch((err) => {
112
+ this.#logger.warn(formatCompact({
113
+ op: 'satori_gateway_receive_failed',
114
+ channel: conversation.id,
115
+ error: err instanceof Error ? err.message : String(err),
116
+ }));
117
+ });
118
+ }
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
+ });
125
+ }
126
+ #warnPlatformEvent(name, error) {
127
+ this.#logger.warn(formatCompact({
128
+ op: 'satori_platform_event_failed',
129
+ event: name,
130
+ error: error instanceof Error ? error.message : String(error),
131
+ }));
132
+ }
133
+ async #connect(handle) {
134
+ const { config } = this.#options;
135
+ const createWs = this.#options.createWebSocket ?? defaultCreateWebSocket;
136
+ const headers = {};
137
+ if (config.token)
138
+ headers.Authorization = `Bearer ${config.token}`;
139
+ return new Promise((resolve, reject) => {
140
+ let settled = false;
141
+ const ws = createWs(buildWsUrl(config.baseUrl, config.token), { headers });
142
+ this.#ws = ws;
143
+ handle.onForceClose(() => {
144
+ try {
145
+ ws.close();
146
+ }
147
+ catch {
148
+ /* already closed */
149
+ }
150
+ });
151
+ ws.on('open', () => {
152
+ this.#logger.debug(formatCompact({ mode: 'ws' }));
153
+ this.#sendSignal(SatoriOpcode.IDENTIFY, {
154
+ token: config.token,
155
+ sn: this.#lastSn,
156
+ });
157
+ this.#lifecycle.startHeartbeat(() => this.#sendSignal(SatoriOpcode.PING));
158
+ if (!settled) {
159
+ settled = true;
160
+ resolve();
161
+ }
162
+ });
163
+ ws.on('message', (data) => {
164
+ try {
165
+ const raw = typeof data === 'string'
166
+ ? data
167
+ : Buffer.isBuffer(data)
168
+ ? data.toString('utf8')
169
+ : String(data ?? '');
170
+ const signal = JSON.parse(raw);
171
+ this.#handleSignal(signal);
172
+ }
173
+ catch (error) {
174
+ this.#logger.warn(formatCompact({
175
+ op: 'ws_parse_error',
176
+ endpoint: config.id,
177
+ error: error instanceof Error ? error.message : String(error),
178
+ }));
179
+ }
180
+ });
181
+ ws.on('close', (code, reason) => {
182
+ const reasonStr = typeof reason === 'string'
183
+ ? reason
184
+ : Buffer.isBuffer(reason)
185
+ ? reason.toString('utf8')
186
+ : String(reason ?? '');
187
+ const numericCode = typeof code === 'number' ? code : 0;
188
+ if (!settled) {
189
+ settled = true;
190
+ reject(new Error(`Satori WS closed: ${numericCode} ${reasonStr}`));
191
+ }
192
+ // 断开日志与重连武装均由基座负责;仅曾 open 的连接才会武装重连,
193
+ // 初始连接失败由 start() 的拒绝路径复位。
194
+ handle.notifyClosed(`Satori WS closed: ${numericCode} ${reasonStr || 'closed'}`);
195
+ });
196
+ ws.on('error', (error) => {
197
+ this.#logger.warn(formatCompact({
198
+ op: 'ws_error',
199
+ endpoint: config.id,
200
+ ok: false,
201
+ error: error instanceof Error ? error.message : String(error),
202
+ }));
203
+ if (!settled) {
204
+ settled = true;
205
+ reject(error instanceof Error ? error : new Error(String(error)));
206
+ }
207
+ });
208
+ });
209
+ }
210
+ #handleSignal(signal) {
211
+ if (signal.op === SatoriOpcode.PONG) {
212
+ // 喂狗:复位基座看门狗计数
213
+ this.#lifecycle.notifyHeartbeatAck();
214
+ void this.emitPlatform('pong', signal).catch((error) => {
215
+ this.#logger.warn(formatCompact({
216
+ op: 'satori_platform_event_failed',
217
+ event: 'pong',
218
+ error: error instanceof Error ? error.message : String(error),
219
+ }));
220
+ });
221
+ return;
222
+ }
223
+ if (signal.op === SatoriOpcode.READY && signal.body?.logins) {
224
+ const logins = signal.body.logins;
225
+ this.#login = logins[0];
226
+ void this.emitPlatform('ready', signal).catch((error) => {
227
+ this.#logger.warn(formatCompact({
228
+ op: 'satori_platform_event_failed',
229
+ event: 'ready',
230
+ error: error instanceof Error ? error.message : String(error),
231
+ }));
232
+ });
233
+ if (!this.#login?.platform || !this.#login?.user?.id) {
234
+ this.#logger.warn(formatCompact({ op: 'ready', ok: false, error: 'missing platform/user' }));
235
+ }
236
+ return;
237
+ }
238
+ 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);
242
+ }
243
+ }
244
+ #sendSignal(op, body) {
245
+ if (!this.#ws || this.#ws.readyState !== WS_OPEN)
246
+ return;
247
+ this.#ws.send(JSON.stringify({ op, body: body ?? {} }));
248
+ }
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
+ };
256
+ }
257
+ }
258
+ export class SatoriWebhookEndpoint extends ClientEndpoint {
259
+ client;
260
+ #logger;
261
+ #options;
262
+ #login;
263
+ #routeReleases = [];
264
+ #started = false;
265
+ management;
266
+ control = Object.freeze({
267
+ recall: (message) => this.recall(message.id, message.conversation.id),
268
+ });
269
+ constructor(options) {
270
+ super();
271
+ this.#logger = getAdapterLogger('satori', options.config.id);
272
+ 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);
289
+ }
290
+ async start() {
291
+ if (this.#started)
292
+ return;
293
+ this.#started = true;
294
+ if (!this.#options.config.token) {
295
+ // 未配 token 时 webhook 无鉴权:任何人知道 path 即可注入假事件。
296
+ this.#logger.warn(formatCompact({
297
+ op: 'webhook_no_token',
298
+ endpoint: this.#options.config.id,
299
+ path: this.#options.config.path,
300
+ hint: 'set token to authenticate Satori webhook callbacks',
301
+ }));
302
+ }
303
+ this.#routeReleases.push(...registerSatoriWebhookRoutes(this.#options.http, this));
304
+ this.#logger.info(formatCompact({
305
+ op: 'listen',
306
+ endpoint: this.#options.config.id,
307
+ mode: 'webhook',
308
+ path: this.#options.config.path,
309
+ }));
310
+ }
311
+ async stop() {
312
+ this.close();
313
+ for (const release of this.#routeReleases.splice(0))
314
+ release();
315
+ this.#started = false;
316
+ this.#logger.debug(formatCompact({
317
+ op: 'disconnect',
318
+ }));
319
+ }
320
+ async send({ conversation, payload }) {
321
+ const content = formatSatoriOutbound(payload);
322
+ const result = await this.client.call('message', 'create', {
323
+ channel_id: conversation.id,
324
+ content,
325
+ });
326
+ const msgId = extractCreatedMessageId(result);
327
+ this.#logger.debug(formatCompact({
328
+ op: 'satori_send',
329
+ endpoint: this.#options.config.id,
330
+ channel: conversation.id,
331
+ messageId: msgId || undefined,
332
+ }));
333
+ return msgId ? formatMessageId(conversation.id, msgId) : '';
334
+ }
335
+ #admitRaw(body) {
336
+ if (body.login && !this.#login)
337
+ this.#login = body.login;
338
+ if (!isMessageEvent(body))
339
+ return;
340
+ const conversation = satoriInboundConversation(String(this.#options.id), body);
341
+ const content = formatInboundContent(body);
342
+ const sender = resolveInboundSender(body);
343
+ const selfId = this.#login?.user?.id ?? body.login?.user?.id;
344
+ const mentioned = isSelfMentioned(body, selfId);
345
+ void this.emit('message.receive', {
346
+ conversation,
347
+ message: { conversation, id: body.message.id },
348
+ content,
349
+ sender,
350
+ endpointId: this.#options.config.id,
351
+ ...(mentioned ? { mentioned: true } : {}),
352
+ metadata: Object.freeze({
353
+ type: body.type,
354
+ channelType: isPrivateChannelType(body) ? 'private' : 'group',
355
+ sn: body.sn,
356
+ platform: this.#login?.platform,
357
+ }),
358
+ }).catch((err) => {
359
+ this.#logger.warn(formatCompact({
360
+ op: 'satori_gateway_receive_failed',
361
+ channel: conversation.id,
362
+ error: err instanceof Error ? err.message : String(err),
363
+ }));
364
+ });
365
+ }
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
+ });
372
+ }
373
+ #warnPlatformEvent(name, error) {
374
+ this.#logger.warn(formatCompact({
375
+ op: 'satori_platform_event_failed',
376
+ event: name,
377
+ error: error instanceof Error ? error.message : String(error),
378
+ }));
379
+ }
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
+ };
387
+ }
388
+ }
389
+ function isPrivateChannelType(body) {
390
+ const channel = body.channel ?? body.message?.channel;
391
+ return channel?.type === 1;
392
+ }
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
+ /** Satori 分页列表({data, next?})聚合;兼容直接返回数组的实现。 */
403
+ async function listSatoriPages(api, resource, params) {
404
+ const items = [];
405
+ let next;
406
+ do {
407
+ const page = await api(resource, 'list', next ? { ...params, next } : params);
408
+ if (Array.isArray(page)) {
409
+ items.push(...page);
410
+ break;
411
+ }
412
+ const typed = (page ?? {});
413
+ if (Array.isArray(typed.data))
414
+ items.push(...typed.data);
415
+ next = typeof typed.next === 'string' && typed.next ? typed.next : undefined;
416
+ } while (next);
417
+ return items;
418
+ }
419
+ function asRecord(value) {
420
+ return value !== null && typeof value === 'object'
421
+ ? value
422
+ : {};
423
+ }
424
+ /**
425
+ * Satori endpoint 的 EndpointManagement 语义端口(ws / webhook 共用),
426
+ * 数据走协议 API:guild.list / channel.list / guild-member.list。
427
+ */
428
+ export function createSatoriEndpointManagement(api) {
429
+ return Object.freeze({
430
+ async listGroups() {
431
+ const groups = [];
432
+ for (const value of await listSatoriPages(api, 'guild', {})) {
433
+ const guild = asRecord(value);
434
+ if (guild.id == null)
435
+ continue;
436
+ groups.push({
437
+ group_id: toGroupId(String(guild.id)),
438
+ name: String(guild.name ?? guild.id),
439
+ });
440
+ }
441
+ return groups;
442
+ },
443
+ async listChannels() {
444
+ const channels = [];
445
+ for (const value of await listSatoriPages(api, 'guild', {})) {
446
+ const guild = asRecord(value);
447
+ if (guild.id == null)
448
+ continue;
449
+ const guildId = String(guild.id);
450
+ const guildName = String(guild.name ?? guildId);
451
+ for (const channelValue of await listSatoriPages(api, 'channel', { guild_id: guildId })) {
452
+ const channel = asRecord(channelValue);
453
+ if (channel.id == null)
454
+ continue;
455
+ // Channel.type: 0=TEXT 1=DIRECT 2=CATEGORY 3=VOICE;缺省按 TEXT 处理
456
+ if (channel.type != null && Number(channel.type) !== 0)
457
+ continue;
458
+ channels.push({
459
+ id: String(channel.id),
460
+ name: channel.name != null ? String(channel.name) : undefined,
461
+ parent: { type: 'guild', id: guildId, name: guildName },
462
+ });
463
+ }
464
+ }
465
+ return channels;
466
+ },
467
+ async listGroupMembers(groupId) {
468
+ // 平台形状(GuildMember[])原样返回
469
+ return listSatoriPages(api, 'guild-member', { guild_id: groupId });
470
+ },
471
+ });
472
+ }
package/lib/index.d.ts CHANGED
@@ -1,18 +1,8 @@
1
- import { SatoriAdapter } from './adapter.js';
2
- export * from './types.js';
3
- export { callSatoriApi } from './api.js';
4
- export * from './utils.js';
5
- export { SatoriWsClient } from './endpoint-ws.js';
6
- export { SatoriWebhookEndpoint } from './endpoint-webhook.js';
7
- export { SatoriAdapter, type SatoriBot } from './adapter.js';
8
- declare module 'zhin.js' {
9
- namespace Plugin {
10
- interface Contexts {
11
- router: import('@zhin.js/host-router').Router;
12
- }
13
- }
14
- interface Adapters {
15
- satori: SatoriAdapter;
16
- }
17
- }
18
- //# sourceMappingURL=index.d.ts.map
1
+ export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatSatoriOutbound, isMessageEvent, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, type ResolvedSatoriWebhookConfig, type ResolvedSatoriWsConfig, type SatoriAdapterConfig, type SatoriApiOptions, type SatoriChannel, type SatoriEventBody, type SatoriLogin, type SatoriMessage, type SatoriSignal, type SatoriUser, type SatoriWireSegment, } from './protocol.js';
2
+ export { SatoriWebhookEndpoint, SatoriWsEndpoint, type CreateSatoriWebSocket, type SatoriApiCaller, type SatoriWebhookEndpointOptions, type SatoriWsEndpointOptions, type SatoriWsSocket, } from './endpoint.js';
3
+ export { handleSatoriWebhookRequest, registerSatoriWebhookRoutes, resolveSatoriOpcode, verifySatoriToken, type SatoriWebhookHandler, } from './webhook.js';
4
+ export { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
5
+ export { SatoriClient, satoriClient, type SatoriClientEventMap, } from './client.js';
6
+ export type { SatoriAdapterConfig as ImHelperSatoriAdapterConfig, SatoriActionUrlResolver, SatoriCall, SatoriV1ClientConfig, SatoriV1Event, SatoriV1Response, } from '@imhelper/satori-v1';
7
+ export { ProtocolError } from '@imhelper/satori-v1';
8
+ export type { ProtocolErrorKind, ProtocolErrorOptions } from 'imhelper';
package/lib/index.js CHANGED
@@ -1,25 +1,6 @@
1
- /**
2
- * Satori 适配器入口:单一适配器,支持 WS / Webhook,协议文档 https://satori.chat/zh-CN/introduction.html
3
- */
4
- import { usePlugin } from 'zhin.js';
5
- import { SatoriAdapter } from './adapter.js';
6
- export * from './types.js';
7
- export { callSatoriApi } from './api.js';
8
- export * from './utils.js';
9
- export { SatoriWsClient } from './endpoint-ws.js';
10
- export { SatoriWebhookEndpoint } from './endpoint-webhook.js';
11
- export { SatoriAdapter } from './adapter.js';
12
- const { provide } = usePlugin();
13
- provide({
14
- name: 'satori',
15
- description: 'Satori 协议适配器(WS 正向 / Webhook)',
16
- mounted: async (p) => {
17
- const adapter = new SatoriAdapter(p);
18
- await adapter.start();
19
- return adapter;
20
- },
21
- dispose: async (adapter) => {
22
- await adapter.stop();
23
- },
24
- });
25
- //# sourceMappingURL=index.js.map
1
+ export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatSatoriOutbound, isMessageEvent, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, } from './protocol.js';
2
+ export { SatoriWebhookEndpoint, SatoriWsEndpoint, } from './endpoint.js';
3
+ export { handleSatoriWebhookRequest, registerSatoriWebhookRoutes, resolveSatoriOpcode, verifySatoriToken, } from './webhook.js';
4
+ export { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
5
+ export { SatoriClient, satoriClient, } from './client.js';
6
+ export { ProtocolError } from '@imhelper/satori-v1';
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Satori protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ * Spec: https://satori.chat/zh-CN/protocol/overview.html
5
+ */
6
+ import type { ConversationRef } from '@zhin.js/im-contract';
7
+ /** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
8
+ export declare const SatoriOpcode: {
9
+ readonly EVENT: 0;
10
+ readonly PING: 1;
11
+ readonly PONG: 2;
12
+ readonly IDENTIFY: 3;
13
+ readonly READY: 4;
14
+ readonly META: 5;
15
+ };
16
+ export interface SatoriSignal {
17
+ readonly op: number;
18
+ readonly body?: Record<string, unknown>;
19
+ }
20
+ export interface SatoriUser {
21
+ readonly id: string;
22
+ readonly name?: string;
23
+ readonly avatar?: string;
24
+ }
25
+ export interface SatoriChannel {
26
+ readonly id: string;
27
+ /** 0=TEXT, 1=DIRECT, 2=CATEGORY, 3=VOICE */
28
+ readonly type?: number;
29
+ readonly name?: string;
30
+ readonly parent_id?: string;
31
+ }
32
+ export interface SatoriMessage {
33
+ readonly id: string;
34
+ readonly content?: string;
35
+ readonly channel?: SatoriChannel;
36
+ readonly user?: SatoriUser;
37
+ readonly member?: {
38
+ readonly user?: SatoriUser;
39
+ readonly nick?: string;
40
+ };
41
+ readonly created_at?: number;
42
+ readonly updated_at?: number;
43
+ }
44
+ export interface SatoriLogin {
45
+ readonly platform?: string;
46
+ readonly user?: SatoriUser;
47
+ readonly status?: number;
48
+ readonly sn?: number;
49
+ }
50
+ export interface SatoriEventBody {
51
+ readonly type?: string;
52
+ readonly sn?: number;
53
+ readonly timestamp?: number;
54
+ readonly login?: SatoriLogin;
55
+ readonly message?: SatoriMessage;
56
+ readonly channel?: SatoriChannel;
57
+ readonly user?: SatoriUser;
58
+ readonly guild?: {
59
+ readonly id: string;
60
+ readonly name?: string;
61
+ };
62
+ readonly member?: {
63
+ readonly user?: SatoriUser;
64
+ readonly nick?: string;
65
+ readonly roles?: string[];
66
+ };
67
+ readonly [key: string]: unknown;
68
+ }
69
+ export interface SatoriApiOptions {
70
+ readonly baseUrl: string;
71
+ readonly platform: string;
72
+ readonly userId: string;
73
+ readonly token?: string;
74
+ }
75
+ export interface SatoriWireSegment {
76
+ readonly type: string;
77
+ readonly data?: Record<string, unknown>;
78
+ }
79
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
80
+ export interface SatoriAdapterConfig {
81
+ readonly id?: string;
82
+ readonly connection?: 'ws' | 'webhook';
83
+ readonly baseUrl?: string;
84
+ readonly token?: string;
85
+ readonly heartbeat_interval?: number;
86
+ /** Webhook POST path (connection: webhook). */
87
+ readonly path?: string;
88
+ /** Transitional: legacy root `endpoints[]` with `context: satori`. */
89
+ readonly endpoints?: ReadonlyArray<Partial<ResolvedSatoriWsConfig> & {
90
+ readonly context?: string;
91
+ readonly connection?: 'ws' | 'webhook';
92
+ readonly path?: string;
93
+ }>;
94
+ }
95
+ export interface ResolvedSatoriWsConfig {
96
+ readonly context: 'satori';
97
+ readonly connection: 'ws';
98
+ readonly id: string;
99
+ readonly baseUrl: string;
100
+ readonly token?: string;
101
+ readonly heartbeat_interval: number;
102
+ }
103
+ export interface ResolvedSatoriWebhookConfig {
104
+ readonly context: 'satori';
105
+ readonly connection: 'webhook';
106
+ readonly id: string;
107
+ readonly baseUrl: string;
108
+ readonly token?: string;
109
+ readonly path: string;
110
+ }
111
+ export type ResolvedSatoriConfig = ResolvedSatoriWsConfig | ResolvedSatoriWebhookConfig;
112
+ export declare function resolveSatoriConfig(config?: SatoriAdapterConfig): ResolvedSatoriConfig;
113
+ /** Channel.type 1 = DIRECT (private). */
114
+ export declare function isPrivateChannel(channel?: SatoriChannel): boolean;
115
+ export declare function isMessageEvent(body: SatoriEventBody): body is SatoriEventBody & {
116
+ message: SatoriMessage;
117
+ };
118
+ export declare function buildWsUrl(baseUrl: string, token?: string): string;
119
+ /**
120
+ * Call Satori HTTP API: POST {baseUrl}/v1/{resource}.{method}
121
+ * @see https://satori.chat/en-US/protocol/api.html
122
+ */
123
+ export declare function callSatoriApi<T = unknown>(options: SatoriApiOptions, resource: string, method: string, params?: Record<string, unknown>): Promise<T>;
124
+ /** Build inbound text for OutboundMessageService.receive. */
125
+ export declare function formatInboundContent(body: SatoriEventBody & {
126
+ message: SatoriMessage;
127
+ }): string;
128
+ /**
129
+ * 入站归一化 → ConversationRef:Channel.type 1 (DIRECT) → kind 'private';
130
+ * 其余频道消息 → kind 'group',所属 guild 容器进 `parent`(kind 'channel')。
131
+ */
132
+ export declare function satoriInboundConversation(endpointKey: string, body: SatoriEventBody & {
133
+ message: SatoriMessage;
134
+ }): ConversationRef;
135
+ export declare function resolveInboundSender(body: SatoriEventBody & {
136
+ message: SatoriMessage;
137
+ }): {
138
+ id: string;
139
+ name?: string;
140
+ };
141
+ /**
142
+ * Detect `<at id="…"/>` elements in message content targeting the bot selfId.
143
+ * selfId 来源:READY/事件携带的 `login.user.id`。
144
+ */
145
+ export declare function isSelfMentioned(body: SatoriEventBody & {
146
+ message: SatoriMessage;
147
+ }, selfId?: string): boolean;
148
+ export declare function formatMessageId(channelId: string, messageId: string): string;
149
+ export declare function parseMessageRef(id: string): {
150
+ channelId: string;
151
+ messageId: string;
152
+ };
153
+ /**
154
+ * Wire-encode an already-rendered outbound payload into Satori message content.
155
+ * Segment canonicalization is intentionally not done here.
156
+ */
157
+ export declare function formatSatoriOutbound(payload: unknown): string;
158
+ export declare function extractCreatedMessageId(result: unknown): string;