@zhin.js/adapter-satori 3.0.2 → 3.0.3
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 +440 -0
- package/README.md +31 -36
- package/adapters/satori.ts +41 -0
- package/lib/endpoint.d.ts +61 -0
- package/lib/endpoint.js +379 -0
- package/lib/index.d.ts +4 -18
- package/lib/index.js +4 -25
- package/lib/protocol.d.ts +150 -0
- package/lib/protocol.js +196 -0
- package/lib/webhook.d.ts +17 -0
- package/lib/webhook.js +79 -0
- package/lib/ws.d.ts +13 -0
- package/lib/ws.js +5 -0
- package/package.json +41 -15
- package/plugin.ts +8 -0
- package/schema.json +35 -0
- package/src/endpoint.ts +462 -0
- package/src/index.ts +47 -35
- package/src/protocol.ts +315 -0
- package/src/webhook.ts +104 -0
- package/src/ws.ts +22 -0
- package/lib/adapter.d.ts +0 -19
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -37
- package/lib/adapter.js.map +0 -1
- package/lib/api.d.ts +0 -15
- package/lib/api.d.ts.map +0 -1
- package/lib/api.js +0 -37
- package/lib/api.js.map +0 -1
- package/lib/endpoint-webhook.d.ts +0 -27
- package/lib/endpoint-webhook.d.ts.map +0 -1
- package/lib/endpoint-webhook.js +0 -99
- package/lib/endpoint-webhook.js.map +0 -1
- package/lib/endpoint-ws.d.ts +0 -30
- package/lib/endpoint-ws.d.ts.map +0 -1
- package/lib/endpoint-ws.js +0 -195
- package/lib/endpoint-ws.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts +0 -91
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -13
- package/lib/types.js.map +0 -1
- package/lib/utils.d.ts +0 -40
- package/lib/utils.d.ts.map +0 -1
- package/lib/utils.js +0 -34
- package/lib/utils.js.map +0 -1
- package/src/adapter.ts +0 -45
- package/src/api.ts +0 -48
- package/src/endpoint-webhook.ts +0 -117
- package/src/endpoint-ws.ts +0 -214
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -91
- package/src/utils.ts +0 -65
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
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
|
+
|
|
9
|
+
/** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
|
|
10
|
+
export const SatoriOpcode = {
|
|
11
|
+
EVENT: 0,
|
|
12
|
+
PING: 1,
|
|
13
|
+
PONG: 2,
|
|
14
|
+
IDENTIFY: 3,
|
|
15
|
+
READY: 4,
|
|
16
|
+
META: 5,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export interface SatoriSignal {
|
|
20
|
+
readonly op: number;
|
|
21
|
+
readonly body?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SatoriUser {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly name?: string;
|
|
27
|
+
readonly avatar?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SatoriChannel {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
/** 0=TEXT, 1=DIRECT, 2=CATEGORY, 3=VOICE */
|
|
33
|
+
readonly type?: number;
|
|
34
|
+
readonly name?: string;
|
|
35
|
+
readonly parent_id?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SatoriMessage {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly content?: string;
|
|
41
|
+
readonly channel?: SatoriChannel;
|
|
42
|
+
readonly user?: SatoriUser;
|
|
43
|
+
readonly member?: { readonly user?: SatoriUser; readonly nick?: string };
|
|
44
|
+
readonly created_at?: number;
|
|
45
|
+
readonly updated_at?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SatoriLogin {
|
|
49
|
+
readonly platform?: string;
|
|
50
|
+
readonly user?: SatoriUser;
|
|
51
|
+
readonly status?: number;
|
|
52
|
+
readonly sn?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SatoriEventBody {
|
|
56
|
+
readonly type?: string;
|
|
57
|
+
readonly sn?: number;
|
|
58
|
+
readonly timestamp?: number;
|
|
59
|
+
readonly login?: SatoriLogin;
|
|
60
|
+
readonly message?: SatoriMessage;
|
|
61
|
+
readonly channel?: SatoriChannel;
|
|
62
|
+
readonly user?: SatoriUser;
|
|
63
|
+
readonly guild?: { readonly id: string; readonly name?: string };
|
|
64
|
+
readonly member?: { readonly user?: SatoriUser; readonly nick?: string; readonly roles?: string[] };
|
|
65
|
+
readonly [key: string]: unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SatoriApiOptions {
|
|
69
|
+
readonly baseUrl: string;
|
|
70
|
+
readonly platform: string;
|
|
71
|
+
readonly userId: string;
|
|
72
|
+
readonly token?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SatoriWireSegment {
|
|
76
|
+
readonly type: string;
|
|
77
|
+
readonly data?: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
81
|
+
export interface SatoriAdapterConfig {
|
|
82
|
+
readonly name?: string;
|
|
83
|
+
readonly connection?: 'ws' | 'webhook';
|
|
84
|
+
readonly baseUrl?: string;
|
|
85
|
+
readonly token?: string;
|
|
86
|
+
readonly heartbeat_interval?: number;
|
|
87
|
+
/** Webhook POST path (connection: webhook). */
|
|
88
|
+
readonly path?: string;
|
|
89
|
+
/** Transitional: legacy root `endpoints[]` with `context: satori`. */
|
|
90
|
+
readonly endpoints?: ReadonlyArray<Partial<ResolvedSatoriWsConfig> & {
|
|
91
|
+
readonly context?: string;
|
|
92
|
+
readonly connection?: 'ws' | 'webhook';
|
|
93
|
+
readonly path?: string;
|
|
94
|
+
}>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ResolvedSatoriWsConfig {
|
|
98
|
+
readonly context: 'satori';
|
|
99
|
+
readonly connection: 'ws';
|
|
100
|
+
readonly name: string;
|
|
101
|
+
readonly baseUrl: string;
|
|
102
|
+
readonly token?: string;
|
|
103
|
+
readonly heartbeat_interval: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ResolvedSatoriWebhookConfig {
|
|
107
|
+
readonly context: 'satori';
|
|
108
|
+
readonly connection: 'webhook';
|
|
109
|
+
readonly name: string;
|
|
110
|
+
readonly baseUrl: string;
|
|
111
|
+
readonly token?: string;
|
|
112
|
+
readonly path: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ResolvedSatoriConfig = ResolvedSatoriWsConfig | ResolvedSatoriWebhookConfig;
|
|
116
|
+
|
|
117
|
+
export function resolveSatoriConfig(config: SatoriAdapterConfig = {}): ResolvedSatoriConfig {
|
|
118
|
+
const entry = config.endpoints?.find((item) => item.context === 'satori');
|
|
119
|
+
const connection = config.connection ?? entry?.connection ?? 'ws';
|
|
120
|
+
const baseUrl = pickCredential(config.baseUrl, entry?.baseUrl, process.env.SATORI_BASE_URL);
|
|
121
|
+
if (!baseUrl) {
|
|
122
|
+
throw new TypeError(
|
|
123
|
+
'Satori adapter requires baseUrl (plugins.<key>.baseUrl or endpoints with context: satori)',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const name = (typeof config.name === 'string' && config.name)
|
|
127
|
+
|| (typeof entry?.name === 'string' && entry.name)
|
|
128
|
+
|| process.env.SATORI_BOT_NAME
|
|
129
|
+
|| 'satori-bot';
|
|
130
|
+
const token = (typeof config.token === 'string' && config.token)
|
|
131
|
+
|| (typeof entry?.token === 'string' && entry.token)
|
|
132
|
+
|| process.env.SATORI_TOKEN
|
|
133
|
+
|| undefined;
|
|
134
|
+
|
|
135
|
+
if (connection === 'webhook') {
|
|
136
|
+
const path = config.path ?? entry?.path;
|
|
137
|
+
if (!path) {
|
|
138
|
+
throw new TypeError('Satori connection:webhook requires path');
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
context: 'satori',
|
|
142
|
+
connection: 'webhook',
|
|
143
|
+
name,
|
|
144
|
+
baseUrl,
|
|
145
|
+
token,
|
|
146
|
+
path,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const heartbeat = config.heartbeat_interval
|
|
151
|
+
?? entry?.heartbeat_interval
|
|
152
|
+
?? 10_000;
|
|
153
|
+
return {
|
|
154
|
+
context: 'satori',
|
|
155
|
+
connection: 'ws',
|
|
156
|
+
name,
|
|
157
|
+
baseUrl,
|
|
158
|
+
token,
|
|
159
|
+
heartbeat_interval: heartbeat,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Channel.type 1 = DIRECT (private). */
|
|
164
|
+
export function isPrivateChannel(channel?: SatoriChannel): boolean {
|
|
165
|
+
return channel?.type === 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function isMessageEvent(
|
|
169
|
+
body: SatoriEventBody,
|
|
170
|
+
): body is SatoriEventBody & { message: SatoriMessage } {
|
|
171
|
+
return (body.type === 'message-created' || body.type === 'message-updated')
|
|
172
|
+
&& !!body.message?.id;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function buildWsUrl(baseUrl: string, token?: string): string {
|
|
176
|
+
const url = new URL(baseUrl.replace(/\/$/, ''));
|
|
177
|
+
if (token) url.searchParams.set('access_token', token);
|
|
178
|
+
return url.toString();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Call Satori HTTP API: POST {baseUrl}/v1/{resource}.{method}
|
|
183
|
+
* @see https://satori.chat/en-US/protocol/api.html
|
|
184
|
+
*/
|
|
185
|
+
export async function callSatoriApi<T = unknown>(
|
|
186
|
+
options: SatoriApiOptions,
|
|
187
|
+
resource: string,
|
|
188
|
+
method: string,
|
|
189
|
+
params: Record<string, unknown> = {},
|
|
190
|
+
): Promise<T> {
|
|
191
|
+
const { baseUrl, platform, userId, token } = options;
|
|
192
|
+
const url = `${baseUrl.replace(/\/$/, '')}/v1/${resource}.${method}`;
|
|
193
|
+
const headers: Record<string, string> = {
|
|
194
|
+
'Content-Type': 'application/json',
|
|
195
|
+
'Satori-Platform': platform,
|
|
196
|
+
'Satori-User-ID': userId,
|
|
197
|
+
};
|
|
198
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
199
|
+
|
|
200
|
+
const res = await fetch(url, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers,
|
|
203
|
+
body: JSON.stringify(params),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const text = await res.text();
|
|
207
|
+
if (res.status === 401) throw new Error(`Satori API 认证失败: ${text}`);
|
|
208
|
+
if (res.status === 403) throw new Error(`Satori API 权限不足: ${text}`);
|
|
209
|
+
if (res.status === 404) throw new Error(`Satori API 不存在: ${resource}.${method}`);
|
|
210
|
+
if (res.status >= 400) throw new Error(`Satori API 错误 ${res.status}: ${text}`);
|
|
211
|
+
|
|
212
|
+
if (!text || text.trim() === '') return undefined as T;
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(text) as T;
|
|
215
|
+
} catch {
|
|
216
|
+
throw new Error(`Satori API 无效 JSON: ${text.slice(0, 200)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Build inbound text for MessageGateway.receive. */
|
|
221
|
+
export function formatInboundContent(body: SatoriEventBody & { message: SatoriMessage }): string {
|
|
222
|
+
const content = body.message.content ?? '';
|
|
223
|
+
return typeof content === 'string' ? content : String(content);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function resolveInboundTarget(body: SatoriEventBody & { message: SatoriMessage }): string {
|
|
227
|
+
const channel = body.channel ?? body.message.channel;
|
|
228
|
+
return channel?.id ?? '';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function resolveInboundSender(body: SatoriEventBody & { message: SatoriMessage }): string {
|
|
232
|
+
const user = body.user ?? body.message.user ?? body.message.member?.user;
|
|
233
|
+
return user?.name ?? body.message.member?.nick ?? user?.id ?? '';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Detect `<at id="…"/>` elements in message content targeting the bot selfId.
|
|
238
|
+
* selfId 来源:READY/事件携带的 `login.user.id`。
|
|
239
|
+
*/
|
|
240
|
+
export function isSelfMentioned(
|
|
241
|
+
body: SatoriEventBody & { message: SatoriMessage },
|
|
242
|
+
selfId?: string,
|
|
243
|
+
): boolean {
|
|
244
|
+
if (!selfId) return false;
|
|
245
|
+
const content = body.message.content;
|
|
246
|
+
if (typeof content !== 'string' || !content.includes('<at')) return false;
|
|
247
|
+
const tags = content.match(/<at\b[^>]*>/gi) ?? [];
|
|
248
|
+
return tags.some((tag) => /\bid\s*=\s*["']([^"']+)["']/i.exec(tag)?.[1] === selfId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function formatMessageId(channelId: string, messageId: string): string {
|
|
252
|
+
return `${channelId}:${messageId}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function parseMessageRef(id: string): { channelId: string; messageId: string } {
|
|
256
|
+
if (id.includes(':')) {
|
|
257
|
+
const [channelId, messageId] = id.split(':');
|
|
258
|
+
return { channelId: channelId ?? '', messageId: messageId ?? '' };
|
|
259
|
+
}
|
|
260
|
+
return { channelId: '', messageId: id };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Wire-encode an already-rendered outbound payload into Satori message content.
|
|
265
|
+
* Segment canonicalization is intentionally not done here.
|
|
266
|
+
*/
|
|
267
|
+
export function formatSatoriOutbound(payload: unknown): string {
|
|
268
|
+
if (typeof payload === 'string') return payload;
|
|
269
|
+
if (payload == null) return '';
|
|
270
|
+
|
|
271
|
+
const segments: Array<string | SatoriWireSegment> = Array.isArray(payload)
|
|
272
|
+
? payload as Array<string | SatoriWireSegment>
|
|
273
|
+
: payload && typeof payload === 'object' && 'type' in (payload as object)
|
|
274
|
+
? [payload as SatoriWireSegment]
|
|
275
|
+
: [];
|
|
276
|
+
|
|
277
|
+
if (segments.length === 0) {
|
|
278
|
+
return typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const parts: string[] = [];
|
|
282
|
+
for (const item of segments) {
|
|
283
|
+
if (typeof item === 'string') {
|
|
284
|
+
parts.push(item);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const data = item.data ?? {};
|
|
288
|
+
switch (item.type) {
|
|
289
|
+
case 'text':
|
|
290
|
+
parts.push(String(data.text ?? data.content ?? ''));
|
|
291
|
+
break;
|
|
292
|
+
case 'at':
|
|
293
|
+
case 'mention':
|
|
294
|
+
parts.push(`@${String(data.name ?? data.id ?? data.target ?? '')}`);
|
|
295
|
+
break;
|
|
296
|
+
case 'image':
|
|
297
|
+
if (typeof data.url === 'string') parts.push(`[image:${data.url}]`);
|
|
298
|
+
break;
|
|
299
|
+
case 'file':
|
|
300
|
+
if (typeof data.url === 'string') parts.push(`[file:${data.url}]`);
|
|
301
|
+
break;
|
|
302
|
+
default:
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return parts.join('');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function extractCreatedMessageId(result: unknown): string {
|
|
310
|
+
const list = Array.isArray(result)
|
|
311
|
+
? result
|
|
312
|
+
: (result as { data?: unknown[] } | null)?.data;
|
|
313
|
+
const msg = list?.[0] as { id?: string } | undefined;
|
|
314
|
+
return msg?.id ?? '';
|
|
315
|
+
}
|
package/src/webhook.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satori webhook HTTP: token → opcode → parse → admit.
|
|
3
|
+
*/
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
6
|
+
import { getLogger } from '@zhin.js/logger';
|
|
7
|
+
import {
|
|
8
|
+
SatoriOpcode,
|
|
9
|
+
type ResolvedSatoriWebhookConfig,
|
|
10
|
+
type SatoriEventBody,
|
|
11
|
+
type SatoriLogin,
|
|
12
|
+
} from './protocol.js';
|
|
13
|
+
|
|
14
|
+
const logger = getLogger('satori');
|
|
15
|
+
|
|
16
|
+
export interface SatoriWebhookHandler {
|
|
17
|
+
readonly config: ResolvedSatoriWebhookConfig;
|
|
18
|
+
readonly isOpen: boolean;
|
|
19
|
+
admit(body: SatoriEventBody): void;
|
|
20
|
+
setLogin(login: SatoriLogin): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerSatoriWebhookRoutes(
|
|
24
|
+
http: HttpHost,
|
|
25
|
+
handler: SatoriWebhookHandler,
|
|
26
|
+
): HttpRouteRegistration[] {
|
|
27
|
+
const path = handler.config.path;
|
|
28
|
+
return [
|
|
29
|
+
http.route('POST', path, async (request, response) => {
|
|
30
|
+
await handleSatoriWebhookRequest(request, response, handler);
|
|
31
|
+
}, { summary: 'Satori webhook callback', tags: ['satori'] }),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function handleSatoriWebhookRequest(
|
|
36
|
+
request: IncomingMessage,
|
|
37
|
+
response: ServerResponse,
|
|
38
|
+
handler: SatoriWebhookHandler,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
if (!verifySatoriToken(handler.config.token, request)) {
|
|
42
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
43
|
+
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const opcode = resolveSatoriOpcode(request);
|
|
47
|
+
if (opcode !== SatoriOpcode.EVENT && opcode !== SatoriOpcode.META) {
|
|
48
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
49
|
+
response.end(JSON.stringify({ message: 'OK' }));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const raw = await readRequestBody(request);
|
|
53
|
+
let body: SatoriEventBody;
|
|
54
|
+
try {
|
|
55
|
+
body = JSON.parse(raw) as SatoriEventBody;
|
|
56
|
+
} catch {
|
|
57
|
+
response.writeHead(400, { 'Content-Type': 'application/json' });
|
|
58
|
+
response.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (opcode === SatoriOpcode.EVENT && handler.isOpen) {
|
|
62
|
+
handler.admit(body);
|
|
63
|
+
} else if (opcode === SatoriOpcode.META && body.login && handler.isOpen) {
|
|
64
|
+
handler.setLogin(body.login);
|
|
65
|
+
}
|
|
66
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
67
|
+
response.end(JSON.stringify({ message: 'OK' }));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
logger.error('Satori webhook error:', error);
|
|
70
|
+
if (!response.headersSent) {
|
|
71
|
+
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
72
|
+
response.end(JSON.stringify({ message: 'Internal Server Error' }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveSatoriOpcode(request: IncomingMessage): number | undefined {
|
|
78
|
+
const raw = request.headers['satori-opcode'] ?? request.headers['Satori-Opcode'];
|
|
79
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
80
|
+
if (value == null || value === '') return undefined;
|
|
81
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
82
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function verifySatoriToken(token: string | undefined, request: IncomingMessage): boolean {
|
|
86
|
+
if (!token) return true;
|
|
87
|
+
const auth = request.headers.authorization ?? '';
|
|
88
|
+
return auth === `Bearer ${token}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function readRequestBody(request: IncomingMessage): Promise<string> {
|
|
92
|
+
const chunks: Buffer[] = [];
|
|
93
|
+
let size = 0;
|
|
94
|
+
for await (const chunk of request) {
|
|
95
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
96
|
+
size += buffer.length;
|
|
97
|
+
if (size > 1_048_576) {
|
|
98
|
+
request.destroy();
|
|
99
|
+
throw new Error('Request body exceeds 1MB');
|
|
100
|
+
}
|
|
101
|
+
chunks.push(buffer);
|
|
102
|
+
}
|
|
103
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
104
|
+
}
|
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,19 +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
|
-
static outboundRichSegmentPolicy: import("zhin.js").OutboundRichSegmentPolicy;
|
|
14
|
-
static interactivePolicy: "text";
|
|
15
|
-
constructor(plugin: Plugin);
|
|
16
|
-
createEndpoint(config: SatoriEndpointConfig): SatoriBot;
|
|
17
|
-
start(): Promise<void>;
|
|
18
|
-
}
|
|
19
|
-
//# sourceMappingURL=adapter.d.ts.map
|
package/lib/adapter.d.ts.map
DELETED
|
@@ -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,EAAwC,MAAM,SAAS,CAAC;AAE/F,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,KAAK,oBAAoB,EAAuE,MAAM,YAAY,CAAC;AAE5H,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,qBAAqB,CAAC;AAE/D,qBAAa,aAAc,SAAQ,OAAO,CAAC,SAAS,CAAC;;IACnD,gBAAyB,YAAY,mCAAoC;IACzE,OAAgB,yBAAyB,8CAAwC;IACjF,OAAgB,iBAAiB,EAAG,MAAM,CAAU;gBAIxC,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,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Satori 适配器:单一适配器支持 WS 正向 / Webhook,由 config.connection 区分
|
|
3
|
-
* 协议文档:https://satori.chat/zh-CN/introduction.html
|
|
4
|
-
*/
|
|
5
|
-
import { Adapter, OUTBOUND_RICH_SEGMENT_POLICY_IM_FULL } 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
|
-
static outboundRichSegmentPolicy = OUTBOUND_RICH_SEGMENT_POLICY_IM_FULL;
|
|
11
|
-
static interactivePolicy = 'text';
|
|
12
|
-
#router;
|
|
13
|
-
constructor(plugin) {
|
|
14
|
-
super(plugin, 'satori', []);
|
|
15
|
-
}
|
|
16
|
-
createEndpoint(config) {
|
|
17
|
-
switch (config.connection) {
|
|
18
|
-
case 'ws':
|
|
19
|
-
return new SatoriWsClient(this, config);
|
|
20
|
-
case 'webhook':
|
|
21
|
-
if (!this.#router) {
|
|
22
|
-
throw new Error('Satori connection: webhook 需要 router,请安装并在配置中启用 @zhin.js/host-router');
|
|
23
|
-
}
|
|
24
|
-
return new SatoriWebhookEndpoint(this, this.#router, config);
|
|
25
|
-
default:
|
|
26
|
-
throw new Error(`Unknown Satori connection: ${config.connection}`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
async start() {
|
|
30
|
-
this.#router = this.plugin.inject('router');
|
|
31
|
-
this.plugin.useContext('router', (router) => {
|
|
32
|
-
this.#router = router;
|
|
33
|
-
});
|
|
34
|
-
await super.start();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
//# sourceMappingURL=adapter.js.map
|
package/lib/adapter.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAiB,OAAO,EAAU,oCAAoC,EAAE,MAAM,SAAS,CAAC;AAE/F,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAK9D,MAAM,OAAO,aAAc,SAAQ,OAAkB;IACnD,MAAM,CAAmB,YAAY,GAAG,CAAC,SAAS,EAAE,UAAU,CAAU,CAAC;IACzE,MAAM,CAAU,yBAAyB,GAAG,oCAAoC,CAAC;IACjF,MAAM,CAAU,iBAAiB,GAAG,MAAe,CAAC;IAEpD,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, type 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,EAAgB,KAAK,mBAAmB,EAAE,KAAK,eAAe,EAAoB,MAAM,YAAY,CAAC;AAE5G,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"}
|