@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,370 @@
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
+
7
+ import { pickCredential } from 'zhin.js/adapter';
8
+ import { isMediaRef } from '@zhin.js/core';
9
+ import type { ConversationKind, ConversationRef } from '@zhin.js/im-contract';
10
+ import { formatCompact, getLogger } from '@zhin.js/logger';
11
+
12
+ const logger = getLogger('satori');
13
+
14
+ /** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
15
+ export const SatoriOpcode = {
16
+ EVENT: 0,
17
+ PING: 1,
18
+ PONG: 2,
19
+ IDENTIFY: 3,
20
+ READY: 4,
21
+ META: 5,
22
+ } as const;
23
+
24
+ export interface SatoriSignal {
25
+ readonly op: number;
26
+ readonly body?: Record<string, unknown>;
27
+ }
28
+
29
+ export interface SatoriUser {
30
+ readonly id: string;
31
+ readonly name?: string;
32
+ readonly avatar?: string;
33
+ }
34
+
35
+ export interface SatoriChannel {
36
+ readonly id: string;
37
+ /** 0=TEXT, 1=DIRECT, 2=CATEGORY, 3=VOICE */
38
+ readonly type?: number;
39
+ readonly name?: string;
40
+ readonly parent_id?: string;
41
+ }
42
+
43
+ export interface SatoriMessage {
44
+ readonly id: string;
45
+ readonly content?: string;
46
+ readonly channel?: SatoriChannel;
47
+ readonly user?: SatoriUser;
48
+ readonly member?: { readonly user?: SatoriUser; readonly nick?: string };
49
+ readonly created_at?: number;
50
+ readonly updated_at?: number;
51
+ }
52
+
53
+ export interface SatoriLogin {
54
+ readonly platform?: string;
55
+ readonly user?: SatoriUser;
56
+ readonly status?: number;
57
+ readonly sn?: number;
58
+ }
59
+
60
+ export interface SatoriEventBody {
61
+ readonly type?: string;
62
+ readonly sn?: number;
63
+ readonly timestamp?: number;
64
+ readonly login?: SatoriLogin;
65
+ readonly message?: SatoriMessage;
66
+ readonly channel?: SatoriChannel;
67
+ readonly user?: SatoriUser;
68
+ readonly guild?: { readonly id: string; readonly name?: string };
69
+ readonly member?: { readonly user?: SatoriUser; readonly nick?: string; readonly roles?: string[] };
70
+ readonly [key: string]: unknown;
71
+ }
72
+
73
+ export interface SatoriApiOptions {
74
+ readonly baseUrl: string;
75
+ readonly platform: string;
76
+ readonly userId: string;
77
+ readonly token?: string;
78
+ }
79
+
80
+ export interface SatoriWireSegment {
81
+ readonly type: string;
82
+ readonly data?: Record<string, unknown>;
83
+ }
84
+
85
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
86
+ export interface SatoriAdapterConfig {
87
+ readonly id?: string;
88
+ readonly connection?: 'ws' | 'webhook';
89
+ readonly baseUrl?: string;
90
+ readonly token?: string;
91
+ readonly heartbeat_interval?: number;
92
+ /** Webhook POST path (connection: webhook). */
93
+ readonly path?: string;
94
+ /** Transitional: legacy root `endpoints[]` with `context: satori`. */
95
+ readonly endpoints?: ReadonlyArray<Partial<ResolvedSatoriWsConfig> & {
96
+ readonly context?: string;
97
+ readonly connection?: 'ws' | 'webhook';
98
+ readonly path?: string;
99
+ }>;
100
+ }
101
+
102
+ export interface ResolvedSatoriWsConfig {
103
+ readonly context: 'satori';
104
+ readonly connection: 'ws';
105
+ readonly id: string;
106
+ readonly baseUrl: string;
107
+ readonly token?: string;
108
+ readonly heartbeat_interval: number;
109
+ }
110
+
111
+ export interface ResolvedSatoriWebhookConfig {
112
+ readonly context: 'satori';
113
+ readonly connection: 'webhook';
114
+ readonly id: string;
115
+ readonly baseUrl: string;
116
+ readonly token?: string;
117
+ readonly path: string;
118
+ }
119
+
120
+ export type ResolvedSatoriConfig = ResolvedSatoriWsConfig | ResolvedSatoriWebhookConfig;
121
+
122
+ export function resolveSatoriConfig(config: SatoriAdapterConfig = {}): ResolvedSatoriConfig {
123
+ const entry = config.endpoints?.find((item) => item.context === 'satori');
124
+ const connection = config.connection ?? entry?.connection ?? 'ws';
125
+ const baseUrl = pickCredential(config.baseUrl, entry?.baseUrl, process.env.SATORI_BASE_URL);
126
+ if (!baseUrl) {
127
+ throw new TypeError(
128
+ 'Satori adapter requires baseUrl (plugins.<key>.baseUrl or endpoints with context: satori)',
129
+ );
130
+ }
131
+ const id = (typeof config.id === 'string' && config.id)
132
+ || (typeof entry?.id === 'string' && entry.id)
133
+ || process.env.SATORI_BOT_NAME
134
+ || 'satori-bot';
135
+ const token = (typeof config.token === 'string' && config.token)
136
+ || (typeof entry?.token === 'string' && entry.token)
137
+ || process.env.SATORI_TOKEN
138
+ || undefined;
139
+
140
+ if (connection === 'webhook') {
141
+ const path = config.path ?? entry?.path;
142
+ if (!path) {
143
+ throw new TypeError('Satori connection:webhook requires path');
144
+ }
145
+ return {
146
+ context: 'satori',
147
+ connection: 'webhook',
148
+ id,
149
+ baseUrl,
150
+ token,
151
+ path,
152
+ };
153
+ }
154
+
155
+ const heartbeat = config.heartbeat_interval
156
+ ?? entry?.heartbeat_interval
157
+ ?? 10_000;
158
+ return {
159
+ context: 'satori',
160
+ connection: 'ws',
161
+ id,
162
+ baseUrl,
163
+ token,
164
+ heartbeat_interval: heartbeat,
165
+ };
166
+ }
167
+
168
+ /** Channel.type 1 = DIRECT (private). */
169
+ export function isPrivateChannel(channel?: SatoriChannel): boolean {
170
+ return channel?.type === 1;
171
+ }
172
+
173
+ export function isMessageEvent(
174
+ body: SatoriEventBody,
175
+ ): body is SatoriEventBody & { message: SatoriMessage } {
176
+ return (body.type === 'message-created' || body.type === 'message-updated')
177
+ && !!body.message?.id;
178
+ }
179
+
180
+ export function buildWsUrl(baseUrl: string, token?: string): string {
181
+ const url = new URL(baseUrl.replace(/\/$/, ''));
182
+ if (token) url.searchParams.set('access_token', token);
183
+ return url.toString();
184
+ }
185
+
186
+ /**
187
+ * Call Satori HTTP API: POST {baseUrl}/v1/{resource}.{method}
188
+ * @see https://satori.chat/en-US/protocol/api.html
189
+ */
190
+ export async function callSatoriApi<T = unknown>(
191
+ options: SatoriApiOptions,
192
+ resource: string,
193
+ method: string,
194
+ params: Record<string, unknown> = {},
195
+ ): Promise<T> {
196
+ const { baseUrl, platform, userId, token } = options;
197
+ const url = `${baseUrl.replace(/\/$/, '')}/v1/${resource}.${method}`;
198
+ const headers: Record<string, string> = {
199
+ 'Content-Type': 'application/json',
200
+ 'Satori-Platform': platform,
201
+ 'Satori-User-ID': userId,
202
+ };
203
+ if (token) headers.Authorization = `Bearer ${token}`;
204
+
205
+ const res = await fetch(url, {
206
+ method: 'POST',
207
+ headers,
208
+ body: JSON.stringify(params),
209
+ });
210
+
211
+ const text = await res.text();
212
+ if (res.status === 401) throw new Error(`Satori API 认证失败: ${text}`);
213
+ if (res.status === 403) throw new Error(`Satori API 权限不足: ${text}`);
214
+ if (res.status === 404) throw new Error(`Satori API 不存在: ${resource}.${method}`);
215
+ if (res.status >= 400) throw new Error(`Satori API 错误 ${res.status}: ${text}`);
216
+
217
+ if (!text || text.trim() === '') return undefined as T;
218
+ try {
219
+ return JSON.parse(text) as T;
220
+ } catch {
221
+ throw new Error(`Satori API 无效 JSON: ${text.slice(0, 200)}`);
222
+ }
223
+ }
224
+
225
+ /** Build inbound text for OutboundMessageService.receive. */
226
+ export function formatInboundContent(body: SatoriEventBody & { message: SatoriMessage }): string {
227
+ const content = body.message.content ?? '';
228
+ return typeof content === 'string' ? content : String(content);
229
+ }
230
+
231
+
232
+ /**
233
+ * 入站归一化 → ConversationRef:Channel.type 1 (DIRECT) → kind 'private';
234
+ * 其余频道消息 → kind 'group',所属 guild 容器进 `parent`(kind 'channel')。
235
+ */
236
+ export function satoriInboundConversation(
237
+ endpointKey: string,
238
+ body: SatoriEventBody & { message: SatoriMessage },
239
+ ): ConversationRef {
240
+ const channel = body.channel ?? body.message.channel;
241
+ const kind: ConversationKind = isPrivateChannel(channel) ? 'private' : 'group';
242
+ return {
243
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
244
+ kind,
245
+ id: channel?.id ?? '',
246
+ ...(kind === 'group' && body.guild?.id
247
+ ? { parent: { kind: 'channel' as const, id: body.guild.id } }
248
+ : {}),
249
+ };
250
+ }
251
+
252
+ export function resolveInboundSender(
253
+ body: SatoriEventBody & { message: SatoriMessage },
254
+ ): { id: string; name?: string } {
255
+ const user = body.user ?? body.message.user ?? body.message.member?.user;
256
+ const name = user?.name ?? body.message.member?.nick;
257
+ const id = user?.id ?? name ?? '';
258
+ return name && name !== id ? { id, name } : { id };
259
+ }
260
+
261
+ /**
262
+ * Detect `<at id="…"/>` elements in message content targeting the bot selfId.
263
+ * selfId 来源:READY/事件携带的 `login.user.id`。
264
+ */
265
+ export function isSelfMentioned(
266
+ body: SatoriEventBody & { message: SatoriMessage },
267
+ selfId?: string,
268
+ ): boolean {
269
+ if (!selfId) return false;
270
+ const content = body.message.content;
271
+ if (typeof content !== 'string' || !content.includes('<at')) return false;
272
+ const tags = content.match(/<at\b[^>]*>/gi) ?? [];
273
+ return tags.some((tag) => /\bid\s*=\s*["']([^"']+)["']/i.exec(tag)?.[1] === selfId);
274
+ }
275
+
276
+ export function formatMessageId(channelId: string, messageId: string): string {
277
+ return `${channelId}:${messageId}`;
278
+ }
279
+
280
+ export function parseMessageRef(id: string): { channelId: string; messageId: string } {
281
+ if (id.includes(':')) {
282
+ const [channelId, messageId] = id.split(':');
283
+ return { channelId: channelId ?? '', messageId: messageId ?? '' };
284
+ }
285
+ return { channelId: '', messageId: id };
286
+ }
287
+
288
+ /**
289
+ * 解析媒体段(image/audio/video/file)的投递源,canonical MediaRef 为唯一来源:
290
+ * - kind=url → 直发 URL;
291
+ * - kind=base64 → 规范为 `base64://` 前缀内联(Satori img/file 元素消费内联数据)。
292
+ * 无 canonical 媒体引用或来源不可投递(path / file 本端点不物化)时 warn + 丢弃。
293
+ */
294
+ function resolveMediaSrc(type: string, data: Record<string, unknown>): string | undefined {
295
+ const media = data.media;
296
+ if (!isMediaRef(media)) {
297
+ logger.warn(formatCompact({
298
+ op: 'satori_outbound_media_dropped',
299
+ type,
300
+ reason: 'missing_media_ref',
301
+ }));
302
+ return undefined;
303
+ }
304
+ if (media.kind === 'url') return media.value;
305
+ if (media.kind === 'base64') {
306
+ return media.value.startsWith('base64://') ? media.value : `base64://${media.value}`;
307
+ }
308
+ logger.warn(formatCompact({
309
+ op: 'satori_outbound_media_dropped',
310
+ type,
311
+ reason: `unsupported_media_kind:${media.kind}`,
312
+ }));
313
+ return undefined;
314
+ }
315
+
316
+ /**
317
+ * Wire-encode an already-rendered outbound payload into Satori message content.
318
+ * Segment canonicalization is intentionally not done here.
319
+ */
320
+ export function formatSatoriOutbound(payload: unknown): string {
321
+ if (typeof payload === 'string') return payload;
322
+ if (payload == null) return '';
323
+
324
+ const segments: Array<string | SatoriWireSegment> = Array.isArray(payload)
325
+ ? payload as Array<string | SatoriWireSegment>
326
+ : payload && typeof payload === 'object' && 'type' in (payload as object)
327
+ ? [payload as SatoriWireSegment]
328
+ : [];
329
+
330
+ if (segments.length === 0) {
331
+ return typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
332
+ }
333
+
334
+ const parts: string[] = [];
335
+ for (const item of segments) {
336
+ if (typeof item === 'string') {
337
+ parts.push(item);
338
+ continue;
339
+ }
340
+ const data = item.data ?? {};
341
+ switch (item.type) {
342
+ case 'text':
343
+ parts.push(String(data.text ?? data.content ?? ''));
344
+ break;
345
+ case 'at':
346
+ case 'mention':
347
+ parts.push(`@${String(data.name ?? data.id ?? data.target ?? '')}`);
348
+ break;
349
+ case 'image':
350
+ case 'audio':
351
+ case 'video':
352
+ case 'file': {
353
+ const src = resolveMediaSrc(item.type, data);
354
+ if (src) parts.push(`[${item.type}:${src}]`);
355
+ break;
356
+ }
357
+ default:
358
+ break;
359
+ }
360
+ }
361
+ return parts.join('');
362
+ }
363
+
364
+ export function extractCreatedMessageId(result: unknown): string {
365
+ const list = Array.isArray(result)
366
+ ? result
367
+ : (result as { data?: unknown[] } | null)?.data;
368
+ const msg = list?.[0] as { id?: string } | undefined;
369
+ return msg?.id ?? '';
370
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `satori.endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
+ * commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
4
+ */
5
+ import { createEndpointCommands } from 'zhin.js/adapter';
6
+ import { defineCommand } from 'zhin.js/command';
7
+ import { satoriRuntimeStateToken } from './satori-runtime-state.js';
8
+
9
+ export const satoriEndpointCommands = createEndpointCommands({
10
+ adapterKey: 'satori',
11
+ adapterDisplayName: 'Satori',
12
+ fields: [
13
+ { key: 'baseUrl', required: true, description: 'Satori 服务 base URL' },
14
+ { key: 'path', description: 'webhook 路径(connection: webhook)' },
15
+ { key: 'token', env: true, description: 'Satori access token' },
16
+ ],
17
+ running: (use) => use(satoriRuntimeStateToken).endpoints.values(),
18
+ describeEntry: (entry) => `baseUrl: ${String(entry.baseUrl)}`,
19
+ }, defineCommand);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Satori 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `satori.endpoint` 命令共享(同一 owner generation)。
4
+ */
5
+ import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
+
7
+ export const satoriRuntimeStateToken = defineEndpointRuntimeStateToken('satori');
package/src/webhook.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Satori webhook HTTP: token → opcode → parse → admit.
3
+ */
4
+ import { timingSafeEqual } from 'node:crypto';
5
+ import type { IncomingMessage, ServerResponse } from 'node:http';
6
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
7
+ import { getLogger } from '@zhin.js/logger';
8
+ import {
9
+ SatoriOpcode,
10
+ type ResolvedSatoriWebhookConfig,
11
+ } from './protocol.js';
12
+
13
+ const logger = getLogger('satori');
14
+
15
+ export interface SatoriWebhookHandler {
16
+ readonly config: ResolvedSatoriWebhookConfig;
17
+ readonly isOpen: boolean;
18
+ acceptHttp(request: IncomingMessage, response: ServerResponse): Promise<void>;
19
+ }
20
+
21
+ export function registerSatoriWebhookRoutes(
22
+ http: HttpHost,
23
+ handler: SatoriWebhookHandler,
24
+ ): HttpRouteRegistration[] {
25
+ const path = handler.config.path;
26
+ return [
27
+ http.route('POST', path, async (request, response) => {
28
+ await handleSatoriWebhookRequest(request, response, handler);
29
+ }, { summary: 'Satori webhook callback', tags: ['satori'] }),
30
+ ];
31
+ }
32
+
33
+ export async function handleSatoriWebhookRequest(
34
+ request: IncomingMessage,
35
+ response: ServerResponse,
36
+ handler: SatoriWebhookHandler,
37
+ ): Promise<void> {
38
+ try {
39
+ if (!verifySatoriToken(handler.config.token, request)) {
40
+ response.writeHead(403, { 'Content-Type': 'application/json' });
41
+ response.end(JSON.stringify({ message: 'Unauthorized' }));
42
+ return;
43
+ }
44
+ const opcode = resolveSatoriOpcode(request);
45
+ if (opcode !== SatoriOpcode.EVENT && opcode !== SatoriOpcode.META) {
46
+ response.writeHead(200, { 'Content-Type': 'application/json' });
47
+ response.end(JSON.stringify({ message: 'OK' }));
48
+ return;
49
+ }
50
+ if (handler.isOpen) {
51
+ await handler.acceptHttp(request, response);
52
+ } else {
53
+ response.writeHead(200, { 'Content-Type': 'application/json' });
54
+ response.end(JSON.stringify({ status: 'ok' }));
55
+ }
56
+ } catch (error) {
57
+ logger.error('Satori webhook error:', error);
58
+ if (!response.headersSent) {
59
+ response.writeHead(500, { 'Content-Type': 'application/json' });
60
+ response.end(JSON.stringify({ message: 'Internal Server Error' }));
61
+ }
62
+ }
63
+ }
64
+
65
+ export function resolveSatoriOpcode(request: IncomingMessage): number | undefined {
66
+ const raw = request.headers['satori-opcode'] ?? request.headers['Satori-Opcode'];
67
+ const value = Array.isArray(raw) ? raw[0] : raw;
68
+ if (value == null || value === '') return undefined;
69
+ const parsed = Number.parseInt(String(value), 10);
70
+ return Number.isNaN(parsed) ? undefined : parsed;
71
+ }
72
+
73
+ export function verifySatoriToken(token: string | undefined, request: IncomingMessage): boolean {
74
+ if (!token) return true;
75
+ const auth = request.headers.authorization ?? '';
76
+ const expected = Buffer.from(`Bearer ${token}`, 'utf8');
77
+ const actual = Buffer.from(auth, 'utf8');
78
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
79
+ }
package/src/ws.ts ADDED
@@ -0,0 +1,22 @@
1
+ import WebSocket from 'ws';
2
+
3
+ export const WS_OPEN = 1;
4
+
5
+ export interface SatoriWsSocket {
6
+ readonly readyState: number;
7
+ send(data: string): void;
8
+ close(code?: number, reason?: string): void;
9
+ on(event: 'open' | 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
10
+ }
11
+
12
+ export type CreateSatoriWebSocket = (
13
+ url: string,
14
+ options?: { readonly headers?: Record<string, string> },
15
+ ) => SatoriWsSocket;
16
+
17
+ export function defaultCreateWebSocket(
18
+ url: string,
19
+ options?: { readonly headers?: Record<string, string> },
20
+ ): SatoriWsSocket {
21
+ return new WebSocket(url, { headers: options?.headers }) as unknown as SatoriWsSocket;
22
+ }
package/lib/adapter.d.ts DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * Satori 适配器:单一适配器支持 WS 正向 / Webhook,由 config.connection 区分
3
- * 协议文档:https://satori.chat/zh-CN/introduction.html
4
- */
5
- import { Adapter, Plugin } from 'zhin.js';
6
- import { SatoriWsClient } from './endpoint-ws.js';
7
- import { SatoriWebhookEndpoint } from './endpoint-webhook.js';
8
- import type { SatoriEndpointConfig } from './types.js';
9
- export type SatoriBot = SatoriWsClient | SatoriWebhookEndpoint;
10
- export declare class SatoriAdapter extends Adapter<SatoriBot> {
11
- #private;
12
- static readonly capabilities: readonly ["inbound", "outbound"];
13
- constructor(plugin: Plugin);
14
- createEndpoint(config: SatoriEndpointConfig): SatoriBot;
15
- start(): Promise<void>;
16
- }
17
- //# sourceMappingURL=adapter.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAiB,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EACV,oBAAoB,EAGrB,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,qBAAqB,CAAC;AAE/D,qBAAa,aAAc,SAAQ,OAAO,CAAC,SAAS,CAAC;;IACnD,gBAAyB,YAAY,mCAAoC;gBAI7D,MAAM,EAAE,MAAM;IAI1B,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS;IAcjD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAO7B"}
package/lib/adapter.js DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * Satori 适配器:单一适配器支持 WS 正向 / Webhook,由 config.connection 区分
3
- * 协议文档:https://satori.chat/zh-CN/introduction.html
4
- */
5
- import { Adapter } from 'zhin.js';
6
- import { SatoriWsClient } from './endpoint-ws.js';
7
- import { SatoriWebhookEndpoint } from './endpoint-webhook.js';
8
- export class SatoriAdapter extends Adapter {
9
- static capabilities = ['inbound', 'outbound'];
10
- #router;
11
- constructor(plugin) {
12
- super(plugin, 'satori', []);
13
- }
14
- createEndpoint(config) {
15
- switch (config.connection) {
16
- case 'ws':
17
- return new SatoriWsClient(this, config);
18
- case 'webhook':
19
- if (!this.#router) {
20
- throw new Error('Satori connection: webhook 需要 router,请安装并在配置中启用 @zhin.js/host-router');
21
- }
22
- return new SatoriWebhookEndpoint(this, this.#router, config);
23
- default:
24
- throw new Error(`Unknown Satori connection: ${config.connection}`);
25
- }
26
- }
27
- async start() {
28
- this.#router = this.plugin.inject('router');
29
- this.plugin.useContext('router', (router) => {
30
- this.#router = router;
31
- });
32
- await super.start();
33
- }
34
- }
35
- //# sourceMappingURL=adapter.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAiB,OAAO,EAAU,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAU9D,MAAM,OAAO,aAAc,SAAQ,OAAkB;IACnD,MAAM,CAAmB,YAAY,GAAG,CAAC,SAAS,EAAE,UAAU,CAAU,CAAC;IAEzE,OAAO,CAAU;IAEjB,YAAY,MAAc;QACxB,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED,cAAc,CAAC,MAA4B;QACzC,QAAQ,MAAM,CAAC,UAAU,EAAE,CAAC;YAC1B,KAAK,IAAI;gBACP,OAAO,IAAI,cAAc,CAAC,IAAI,EAAE,MAAwB,CAAC,CAAC;YAC5D,KAAK,SAAS;gBACZ,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;oBAClB,MAAM,IAAI,KAAK,CAAC,sEAAsE,CAAC,CAAC;gBAC1F,CAAC;gBACD,OAAO,IAAI,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,MAA6B,CAAC,CAAC;YACtF;gBACE,MAAM,IAAI,KAAK,CAAC,8BAA+B,MAA+B,CAAC,UAAU,EAAE,CAAC,CAAC;QACjG,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,OAAO,GAAI,IAAI,CAAC,MAAM,CAAC,MAA8C,CAAC,QAAQ,CAAC,CAAC;QACpF,IAAI,CAAC,MAAM,CAAC,UAAkE,CAAC,QAAQ,EAAE,CAAC,MAAc,EAAE,EAAE;YAC3G,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACxB,CAAC,CAAC,CAAC;QACH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC"}
package/lib/api.d.ts DELETED
@@ -1,15 +0,0 @@
1
- /**
2
- * Satori HTTP API 封装:POST /v1/{resource}.{method},头 Satori-Platform、Satori-User-ID、Authorization
3
- * 参考 https://satori.chat/en-US/protocol/api.html
4
- */
5
- export interface SatoriApiOptions {
6
- baseUrl: string;
7
- platform: string;
8
- userId: string;
9
- token?: string;
10
- }
11
- /**
12
- * 调用 Satori API:POST {baseUrl}/v1/{resource}.{method},JSON body
13
- */
14
- export declare function callSatoriApi<T = unknown>(options: SatoriApiOptions, resource: string, method: string, params?: Record<string, unknown>): Promise<T>;
15
- //# sourceMappingURL=api.d.ts.map
package/lib/api.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,CAAC,GAAG,OAAO,EAC7C,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,CAAC,CAAC,CA4BZ"}
package/lib/api.js DELETED
@@ -1,37 +0,0 @@
1
- /**
2
- * 调用 Satori API:POST {baseUrl}/v1/{resource}.{method},JSON body
3
- */
4
- export async function callSatoriApi(options, resource, method, params = {}) {
5
- const { baseUrl, platform, userId, token } = options;
6
- const url = `${baseUrl.replace(/\/$/, '')}/v1/${resource}.${method}`;
7
- const headers = {
8
- 'Content-Type': 'application/json',
9
- 'Satori-Platform': platform,
10
- 'Satori-User-ID': userId,
11
- };
12
- if (token)
13
- headers['Authorization'] = `Bearer ${token}`;
14
- const res = await fetch(url, {
15
- method: 'POST',
16
- headers,
17
- body: JSON.stringify(params),
18
- });
19
- const text = await res.text();
20
- if (res.status === 401)
21
- throw new Error(`Satori API 认证失败: ${text}`);
22
- if (res.status === 403)
23
- throw new Error(`Satori API 权限不足: ${text}`);
24
- if (res.status === 404)
25
- throw new Error(`Satori API 不存在: ${resource}.${method}`);
26
- if (res.status >= 400)
27
- throw new Error(`Satori API 错误 ${res.status}: ${text}`);
28
- if (!text || text.trim() === '')
29
- return undefined;
30
- try {
31
- return JSON.parse(text);
32
- }
33
- catch {
34
- throw new Error(`Satori API 无效 JSON: ${text.slice(0, 200)}`);
35
- }
36
- }
37
- //# sourceMappingURL=api.js.map
package/lib/api.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAWA;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAyB,EACzB,QAAgB,EAChB,MAAc,EACd,SAAkC,EAAE;IAEpC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IACrD,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,QAAQ,IAAI,MAAM,EAAE,CAAC;IACrE,MAAM,OAAO,GAA2B;QACtC,cAAc,EAAE,kBAAkB;QAClC,iBAAiB,EAAE,QAAQ;QAC3B,gBAAgB,EAAE,MAAM;KACzB,CAAC;IACF,IAAI,KAAK;QAAE,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAC;IAExD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC3B,MAAM,EAAE,MAAM;QACd,OAAO;QACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;KAC7B,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC;IACpE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC;IACpE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,QAAQ,IAAI,MAAM,EAAE,CAAC,CAAC;IACjF,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC;IAE/E,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,SAAc,CAAC;IACvD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC"}
@@ -1,27 +0,0 @@
1
- /**
2
- * Satori WebHook Bot:应用提供 POST path,SDK 推送 EVENT(Satori-Opcode: 0)
3
- */
4
- import { EventEmitter } from 'events';
5
- import { Endpoint, Message, SendOptions } from 'zhin.js';
6
- import { type Router } from '@zhin.js/host-router/router';
7
- import type { SatoriWebhookConfig, SatoriEventBody } from './types.js';
8
- import type { SatoriAdapter } from './adapter.js';
9
- export declare class SatoriWebhookEndpoint extends EventEmitter implements Endpoint<SatoriWebhookConfig, SatoriEventBody> {
10
- adapter: SatoriAdapter;
11
- router: Router;
12
- $config: SatoriWebhookConfig;
13
- $connected: boolean;
14
- /** 从首个事件的 login 得到,用于 API 的 platform / userId */
15
- private login?;
16
- get logger(): import("zhin.js").Logger;
17
- constructor(adapter: SatoriAdapter, router: Router, $config: SatoriWebhookConfig);
18
- get $id(): string;
19
- private apiOptions;
20
- $connect(): Promise<void>;
21
- $disconnect(): Promise<void>;
22
- private handleEvent;
23
- $formatMessage(body: SatoriEventBody): Message<SatoriEventBody>;
24
- $sendMessage(options: SendOptions): Promise<string>;
25
- $recallMessage(id: string): Promise<void>;
26
- }
27
- //# sourceMappingURL=endpoint-webhook.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"endpoint-webhook.d.ts","sourceRoot":"","sources":["../src/endpoint-webhook.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAiB,QAAQ,EAAE,OAAO,EAAW,WAAW,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EAAsB,KAAK,MAAM,EAAsB,MAAM,6BAA6B,CAAC;AAElG,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAe,MAAM,YAAY,CAAC;AAEpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,qBAAa,qBAAsB,SAAQ,YAAa,YAAW,QAAQ,CAAC,mBAAmB,EAAE,eAAe,CAAC;IAUtG,OAAO,EAAE,aAAa;IACtB,MAAM,EAAE,MAAM;IACd,OAAO,EAAE,mBAAmB;IAXrC,UAAU,EAAE,OAAO,CAAQ;IAC3B,iDAAiD;IACjD,OAAO,CAAC,KAAK,CAAC,CAAc;IAE5B,IAAI,MAAM,6BAET;gBAGQ,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,mBAAmB;IAKrC,IAAI,GAAG,WAEN;IAED,OAAO,CAAC,UAAU;IAMZ,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAezB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAIlC,OAAO,CAAC,WAAW;IAUnB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IA+BzD,YAAY,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAanD,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAIhD"}