@zhin.js/adapter-satori 1.0.1 → 1.1.2
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 +912 -0
- package/README.md +60 -38
- package/adapters/satori/index.js +38 -0
- package/adapters/satori/index.ts +49 -0
- package/commands/satori/endpoint/add/[id]/index.js +3 -0
- package/commands/satori/endpoint/add/[id]/index.ts +3 -0
- package/commands/satori/endpoint/definition.js +19 -0
- package/commands/satori/endpoint/definition.ts +19 -0
- package/commands/satori/endpoint/list/index.js +3 -0
- package/commands/satori/endpoint/list/index.ts +3 -0
- package/commands/satori/endpoint/remove/[id]/index.js +3 -0
- package/commands/satori/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.d.ts +18 -0
- package/lib/client.js +21 -0
- package/lib/endpoint.d.ts +56 -0
- package/lib/endpoint.js +472 -0
- package/lib/index.d.ts +8 -18
- package/lib/index.js +6 -25
- package/lib/protocol.d.ts +148 -0
- package/lib/protocol.js +230 -0
- package/lib/satori-runtime-state.d.ts +1 -0
- package/lib/satori-runtime-state.js +6 -0
- package/lib/webhook.d.ts +12 -0
- package/lib/webhook.js +60 -0
- package/lib/ws.d.ts +13 -0
- package/lib/ws.js +5 -0
- package/package.json +68 -17
- package/plugin.js +14 -0
- package/schema.json +109 -0
- package/src/client.ts +57 -0
- package/src/endpoint.ts +580 -0
- package/src/index.ts +61 -35
- package/src/protocol.ts +353 -0
- package/src/satori-runtime-state.ts +7 -0
- package/src/webhook.ts +79 -0
- package/src/ws.ts +22 -0
- package/lib/adapter.d.ts +0 -17
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -35
- 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/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 -33
- package/lib/utils.js.map +0 -1
- package/skills/satori/SKILL.md +0 -33
- package/src/adapter.ts +0 -48
- package/src/api.ts +0 -48
- package/src/endpoint-webhook.ts +0 -117
- package/src/endpoint-ws.ts +0 -214
- package/src/types.ts +0 -91
- package/src/utils.ts +0 -64
package/lib/protocol.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/** Satori protocol helpers. Spec: https://satori.chat/zh-CN/protocol/overview.html */
|
|
2
|
+
import { isMediaRef } from '@zhin.js/im-contract';
|
|
3
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
4
|
+
const logger = getLogger('satori');
|
|
5
|
+
/** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
|
|
6
|
+
export const SatoriOpcode = {
|
|
7
|
+
EVENT: 0,
|
|
8
|
+
PING: 1,
|
|
9
|
+
PONG: 2,
|
|
10
|
+
IDENTIFY: 3,
|
|
11
|
+
READY: 4,
|
|
12
|
+
META: 5,
|
|
13
|
+
};
|
|
14
|
+
export function resolveSatoriConfig(config) {
|
|
15
|
+
const connection = config.connection ?? 'ws';
|
|
16
|
+
const id = requiredEndpointField(config.id, 'id');
|
|
17
|
+
const baseUrl = requiredEndpointField(config.baseUrl, 'baseUrl');
|
|
18
|
+
const token = typeof config.token === 'string' && config.token.trim()
|
|
19
|
+
? config.token.trim()
|
|
20
|
+
: undefined;
|
|
21
|
+
if (connection === 'webhook') {
|
|
22
|
+
const path = requiredEndpointField(config.path, 'path');
|
|
23
|
+
return {
|
|
24
|
+
context: 'satori',
|
|
25
|
+
connection: 'webhook',
|
|
26
|
+
id,
|
|
27
|
+
baseUrl,
|
|
28
|
+
token,
|
|
29
|
+
path,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const heartbeat = config.heartbeat_interval ?? 10_000;
|
|
33
|
+
return {
|
|
34
|
+
context: 'satori',
|
|
35
|
+
connection: 'ws',
|
|
36
|
+
id,
|
|
37
|
+
baseUrl,
|
|
38
|
+
token,
|
|
39
|
+
heartbeat_interval: heartbeat,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function requiredEndpointField(value, field) {
|
|
43
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
44
|
+
throw new TypeError(`Satori endpoint requires a non-empty ${field}`);
|
|
45
|
+
}
|
|
46
|
+
return value.trim();
|
|
47
|
+
}
|
|
48
|
+
/** Channel.type 1 = DIRECT (private). */
|
|
49
|
+
export function isPrivateChannel(channel) {
|
|
50
|
+
return channel?.type === 1;
|
|
51
|
+
}
|
|
52
|
+
export function isMessageEvent(body) {
|
|
53
|
+
return (body.type === 'message-created' || body.type === 'message-updated')
|
|
54
|
+
&& !!body.message?.id;
|
|
55
|
+
}
|
|
56
|
+
export function buildWsUrl(baseUrl, token) {
|
|
57
|
+
const url = new URL(baseUrl.replace(/\/$/, ''));
|
|
58
|
+
if (token)
|
|
59
|
+
url.searchParams.set('access_token', token);
|
|
60
|
+
return url.toString();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Call Satori HTTP API: POST {baseUrl}/v1/{resource}.{method}
|
|
64
|
+
* @see https://satori.chat/en-US/protocol/api.html
|
|
65
|
+
*/
|
|
66
|
+
export async function callSatoriApi(options, resource, method, params = {}) {
|
|
67
|
+
const { baseUrl, platform, userId, token } = options;
|
|
68
|
+
const url = `${baseUrl.replace(/\/$/, '')}/v1/${resource}.${method}`;
|
|
69
|
+
const headers = {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
'Satori-Platform': platform,
|
|
72
|
+
'Satori-User-ID': userId,
|
|
73
|
+
};
|
|
74
|
+
if (token)
|
|
75
|
+
headers.Authorization = `Bearer ${token}`;
|
|
76
|
+
const res = await fetch(url, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers,
|
|
79
|
+
body: JSON.stringify(params),
|
|
80
|
+
});
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
if (res.status === 401)
|
|
83
|
+
throw new Error(`Satori API 认证失败: ${text}`);
|
|
84
|
+
if (res.status === 403)
|
|
85
|
+
throw new Error(`Satori API 权限不足: ${text}`);
|
|
86
|
+
if (res.status === 404)
|
|
87
|
+
throw new Error(`Satori API 不存在: ${resource}.${method}`);
|
|
88
|
+
if (res.status >= 400)
|
|
89
|
+
throw new Error(`Satori API 错误 ${res.status}: ${text}`);
|
|
90
|
+
if (!text || text.trim() === '')
|
|
91
|
+
return undefined;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(text);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new Error(`Satori API 无效 JSON: ${text.slice(0, 200)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Build inbound text for OutboundMessageService.receive. */
|
|
100
|
+
export function formatInboundContent(body) {
|
|
101
|
+
const content = body.message.content ?? '';
|
|
102
|
+
return typeof content === 'string' ? content : String(content);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 入站归一化 → ConversationRef:Channel.type 1 (DIRECT) → kind 'private';
|
|
106
|
+
* 其余频道消息 → kind 'group',所属 guild 容器进 `parent`(kind 'channel')。
|
|
107
|
+
*/
|
|
108
|
+
export function satoriInboundConversation(endpointKey, body) {
|
|
109
|
+
const channel = body.channel ?? body.message.channel;
|
|
110
|
+
const kind = isPrivateChannel(channel) ? 'private' : 'group';
|
|
111
|
+
return {
|
|
112
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
113
|
+
kind,
|
|
114
|
+
id: channel?.id ?? '',
|
|
115
|
+
...(kind === 'group' && body.guild?.id
|
|
116
|
+
? { parent: { kind: 'channel', id: body.guild.id } }
|
|
117
|
+
: {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function resolveInboundSender(body) {
|
|
121
|
+
const user = body.user ?? body.message.user ?? body.message.member?.user;
|
|
122
|
+
const name = user?.name ?? body.message.member?.nick;
|
|
123
|
+
const id = user?.id ?? name ?? '';
|
|
124
|
+
return name && name !== id ? { id, name } : { id };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Detect `<at id="…"/>` elements in message content targeting the bot selfId.
|
|
128
|
+
* selfId 来源:READY/事件携带的 `login.user.id`。
|
|
129
|
+
*/
|
|
130
|
+
export function isSelfMentioned(body, selfId) {
|
|
131
|
+
if (!selfId)
|
|
132
|
+
return false;
|
|
133
|
+
const content = body.message.content;
|
|
134
|
+
if (typeof content !== 'string' || !content.includes('<at'))
|
|
135
|
+
return false;
|
|
136
|
+
const tags = content.match(/<at\b[^>]*>/gi) ?? [];
|
|
137
|
+
return tags.some((tag) => /\bid\s*=\s*["']([^"']+)["']/i.exec(tag)?.[1] === selfId);
|
|
138
|
+
}
|
|
139
|
+
export function formatMessageId(channelId, messageId) {
|
|
140
|
+
return `${channelId}:${messageId}`;
|
|
141
|
+
}
|
|
142
|
+
export function parseMessageRef(id) {
|
|
143
|
+
if (id.includes(':')) {
|
|
144
|
+
const [channelId, messageId] = id.split(':');
|
|
145
|
+
return { channelId: channelId ?? '', messageId: messageId ?? '' };
|
|
146
|
+
}
|
|
147
|
+
return { channelId: '', messageId: id };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 解析媒体段(image/audio/video/file)的投递源,canonical MediaRef 为唯一来源:
|
|
151
|
+
* - kind=url → 直发 URL;
|
|
152
|
+
* - kind=base64 → 规范为 `base64://` 前缀内联(Satori img/file 元素消费内联数据)。
|
|
153
|
+
* 无 canonical 媒体引用或来源不可投递(path / file 本端点不物化)时 warn + 丢弃。
|
|
154
|
+
*/
|
|
155
|
+
function resolveMediaSrc(type, data) {
|
|
156
|
+
const media = data.media;
|
|
157
|
+
if (!isMediaRef(media)) {
|
|
158
|
+
logger.warn(formatCompact({
|
|
159
|
+
op: 'satori_outbound_media_dropped',
|
|
160
|
+
type,
|
|
161
|
+
reason: 'missing_media_ref',
|
|
162
|
+
}));
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
if (media.kind === 'url')
|
|
166
|
+
return media.value;
|
|
167
|
+
if (media.kind === 'base64') {
|
|
168
|
+
return media.value.startsWith('base64://') ? media.value : `base64://${media.value}`;
|
|
169
|
+
}
|
|
170
|
+
logger.warn(formatCompact({
|
|
171
|
+
op: 'satori_outbound_media_dropped',
|
|
172
|
+
type,
|
|
173
|
+
reason: `unsupported_media_kind:${media.kind}`,
|
|
174
|
+
}));
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Wire-encode an already-rendered outbound payload into Satori message content.
|
|
179
|
+
* Segment canonicalization is intentionally not done here.
|
|
180
|
+
*/
|
|
181
|
+
export function formatSatoriOutbound(payload) {
|
|
182
|
+
if (typeof payload === 'string')
|
|
183
|
+
return payload;
|
|
184
|
+
if (payload == null)
|
|
185
|
+
return '';
|
|
186
|
+
const segments = Array.isArray(payload)
|
|
187
|
+
? payload
|
|
188
|
+
: payload && typeof payload === 'object' && 'type' in payload
|
|
189
|
+
? [payload]
|
|
190
|
+
: [];
|
|
191
|
+
if (segments.length === 0) {
|
|
192
|
+
return typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
|
|
193
|
+
}
|
|
194
|
+
const parts = [];
|
|
195
|
+
for (const item of segments) {
|
|
196
|
+
if (typeof item === 'string') {
|
|
197
|
+
parts.push(item);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const data = item.data ?? {};
|
|
201
|
+
switch (item.type) {
|
|
202
|
+
case 'text':
|
|
203
|
+
parts.push(String(data.text ?? data.content ?? ''));
|
|
204
|
+
break;
|
|
205
|
+
case 'at':
|
|
206
|
+
case 'mention':
|
|
207
|
+
parts.push(`@${String(data.name ?? data.id ?? data.target ?? '')}`);
|
|
208
|
+
break;
|
|
209
|
+
case 'image':
|
|
210
|
+
case 'audio':
|
|
211
|
+
case 'video':
|
|
212
|
+
case 'file': {
|
|
213
|
+
const src = resolveMediaSrc(item.type, data);
|
|
214
|
+
if (src)
|
|
215
|
+
parts.push(`[${item.type}:${src}]`);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
default:
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return parts.join('');
|
|
223
|
+
}
|
|
224
|
+
export function extractCreatedMessageId(result) {
|
|
225
|
+
const list = Array.isArray(result)
|
|
226
|
+
? result
|
|
227
|
+
: result?.data;
|
|
228
|
+
const msg = list?.[0];
|
|
229
|
+
return msg?.id ?? '';
|
|
230
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const satoriRuntimeStateToken: import("zhin.js").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
|
|
@@ -0,0 +1,6 @@
|
|
|
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
|
+
export const satoriRuntimeStateToken = defineEndpointRuntimeStateToken('satori');
|
package/lib/webhook.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
3
|
+
import { type ResolvedSatoriWebhookConfig } from './protocol.js';
|
|
4
|
+
export interface SatoriWebhookHandler {
|
|
5
|
+
readonly config: ResolvedSatoriWebhookConfig;
|
|
6
|
+
readonly isOpen: boolean;
|
|
7
|
+
acceptHttp(request: IncomingMessage, response: ServerResponse): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function registerSatoriWebhookRoutes(http: HttpHost, handler: SatoriWebhookHandler): HttpRouteRegistration[];
|
|
10
|
+
export declare function handleSatoriWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: SatoriWebhookHandler): Promise<void>;
|
|
11
|
+
export declare function resolveSatoriOpcode(request: IncomingMessage): number | undefined;
|
|
12
|
+
export declare function verifySatoriToken(token: string | undefined, request: IncomingMessage): boolean;
|
package/lib/webhook.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satori webhook HTTP: token → opcode → parse → admit.
|
|
3
|
+
*/
|
|
4
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
5
|
+
import { getLogger } from '@zhin.js/logger';
|
|
6
|
+
import { SatoriOpcode, } from './protocol.js';
|
|
7
|
+
const logger = getLogger('satori');
|
|
8
|
+
export function registerSatoriWebhookRoutes(http, handler) {
|
|
9
|
+
const path = handler.config.path;
|
|
10
|
+
return [
|
|
11
|
+
http.route('POST', path, async (request, response) => {
|
|
12
|
+
await handleSatoriWebhookRequest(request, response, handler);
|
|
13
|
+
}, { summary: 'Satori webhook callback', tags: ['satori'] }),
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
export async function handleSatoriWebhookRequest(request, response, handler) {
|
|
17
|
+
try {
|
|
18
|
+
if (!verifySatoriToken(handler.config.token, request)) {
|
|
19
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
20
|
+
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const opcode = resolveSatoriOpcode(request);
|
|
24
|
+
if (opcode !== SatoriOpcode.EVENT && opcode !== SatoriOpcode.META) {
|
|
25
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
26
|
+
response.end(JSON.stringify({ message: 'OK' }));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (handler.isOpen) {
|
|
30
|
+
await handler.acceptHttp(request, response);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
34
|
+
response.end(JSON.stringify({ status: 'ok' }));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
logger.error('Satori webhook error:', error);
|
|
39
|
+
if (!response.headersSent) {
|
|
40
|
+
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
41
|
+
response.end(JSON.stringify({ message: 'Internal Server Error' }));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function resolveSatoriOpcode(request) {
|
|
46
|
+
const raw = request.headers['satori-opcode'] ?? request.headers['Satori-Opcode'];
|
|
47
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
48
|
+
if (value == null || value === '')
|
|
49
|
+
return undefined;
|
|
50
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
51
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
52
|
+
}
|
|
53
|
+
export function verifySatoriToken(token, request) {
|
|
54
|
+
if (!token)
|
|
55
|
+
return true;
|
|
56
|
+
const auth = request.headers.authorization ?? '';
|
|
57
|
+
const expected = Buffer.from(`Bearer ${token}`, 'utf8');
|
|
58
|
+
const actual = Buffer.from(auth, 'utf8');
|
|
59
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
60
|
+
}
|
package/lib/ws.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const WS_OPEN = 1;
|
|
2
|
+
export interface SatoriWsSocket {
|
|
3
|
+
readonly readyState: number;
|
|
4
|
+
send(data: string): void;
|
|
5
|
+
close(code?: number, reason?: string): void;
|
|
6
|
+
on(event: 'open' | 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
|
|
7
|
+
}
|
|
8
|
+
export type CreateSatoriWebSocket = (url: string, options?: {
|
|
9
|
+
readonly headers?: Record<string, string>;
|
|
10
|
+
}) => SatoriWsSocket;
|
|
11
|
+
export declare function defaultCreateWebSocket(url: string, options?: {
|
|
12
|
+
readonly headers?: Record<string, string>;
|
|
13
|
+
}): SatoriWsSocket;
|
package/lib/ws.js
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-satori",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Zhin.js adapter for
|
|
3
|
+
"version": "1.1.2",
|
|
4
|
+
"description": "Zhin.js Satori adapter for Plugin Runtime (WebSocket client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
+
"adapters",
|
|
17
|
+
"commands",
|
|
18
|
+
"plugin.js",
|
|
19
|
+
"schema.json",
|
|
16
20
|
"src",
|
|
17
21
|
"lib",
|
|
18
|
-
"client",
|
|
19
|
-
"dist",
|
|
20
22
|
"skills",
|
|
21
|
-
"README.md"
|
|
23
|
+
"README.md",
|
|
24
|
+
"CHANGELOG.md"
|
|
22
25
|
],
|
|
23
26
|
"keywords": [
|
|
24
27
|
"zhin",
|
|
@@ -31,32 +34,80 @@
|
|
|
31
34
|
"license": "MIT",
|
|
32
35
|
"repository": {
|
|
33
36
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/zhinjs/zhin",
|
|
37
|
+
"url": "https://github.com/zhinjs/zhin.git",
|
|
35
38
|
"directory": "plugins/adapters/satori"
|
|
36
39
|
},
|
|
37
40
|
"dependencies": {
|
|
38
|
-
"
|
|
41
|
+
"@imhelper/satori-v1": "1.0.7",
|
|
42
|
+
"imhelper": "1.0.7",
|
|
43
|
+
"ws": "^8.21.1",
|
|
44
|
+
"@zhin.js/adapter": "1.1.14",
|
|
45
|
+
"@zhin.js/core": "1.1.37",
|
|
46
|
+
"@zhin.js/feature-kit": "1.1.1",
|
|
47
|
+
"@zhin.js/host-http": "1.1.1",
|
|
48
|
+
"@zhin.js/im-contract": "1.1.1",
|
|
49
|
+
"@zhin.js/logger": "1.1.1",
|
|
50
|
+
"@zhin.js/skill": "1.1.1"
|
|
39
51
|
},
|
|
40
52
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^
|
|
53
|
+
"@types/node": "^26.1.2",
|
|
42
54
|
"@types/ws": "^8.18.1",
|
|
43
55
|
"typescript": "^6.0.3",
|
|
44
|
-
"
|
|
45
|
-
"@zhin.js/
|
|
46
|
-
"
|
|
47
|
-
"zhin.js": "2.0.1"
|
|
56
|
+
"vitest": "^4.1.10",
|
|
57
|
+
"@zhin.js/host-http": "1.1.1",
|
|
58
|
+
"zhin.js": "1.1.2"
|
|
48
59
|
},
|
|
49
60
|
"peerDependencies": {
|
|
50
|
-
"zhin.js": "
|
|
51
|
-
"@zhin.js/
|
|
61
|
+
"@zhin.js/adapter": "^1.1.14",
|
|
62
|
+
"@zhin.js/command": "^1.1.1",
|
|
63
|
+
"@zhin.js/core": "^1.1.37",
|
|
64
|
+
"zhin.js": "^1.1.2"
|
|
52
65
|
},
|
|
53
66
|
"peerDependenciesMeta": {
|
|
54
|
-
"@zhin.js/
|
|
67
|
+
"@zhin.js/command": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"zhin.js": {
|
|
55
71
|
"optional": true
|
|
56
72
|
}
|
|
57
73
|
},
|
|
74
|
+
"author": {
|
|
75
|
+
"name": "lc-cn",
|
|
76
|
+
"email": "admin@liucl.cn",
|
|
77
|
+
"url": "https://github.com/lc-cn"
|
|
78
|
+
},
|
|
79
|
+
"engines": {
|
|
80
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public",
|
|
84
|
+
"registry": "https://registry.npmjs.org"
|
|
85
|
+
},
|
|
86
|
+
"zhin": {
|
|
87
|
+
"protocol": 1,
|
|
88
|
+
"type": "plugin",
|
|
89
|
+
"entry": "./plugin.js",
|
|
90
|
+
"engine": "^1.0.0",
|
|
91
|
+
"runtime": "trusted",
|
|
92
|
+
"features": [
|
|
93
|
+
{
|
|
94
|
+
"package": "@zhin.js/adapter",
|
|
95
|
+
"api": "^1.0.0"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"package": "@zhin.js/command",
|
|
99
|
+
"api": "^1.0.0"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"package": "@zhin.js/skill",
|
|
103
|
+
"api": "^1.0.0"
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"plugins": []
|
|
107
|
+
},
|
|
58
108
|
"scripts": {
|
|
59
|
-
"build": "
|
|
60
|
-
"clean": "rimraf lib"
|
|
109
|
+
"build": "tsc",
|
|
110
|
+
"clean": "rimraf lib",
|
|
111
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/satori/tests"
|
|
61
112
|
}
|
|
62
113
|
}
|
package/plugin.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { createEndpointRuntimeState } from 'zhin.js/adapter';
|
|
3
|
+
import { definePlugin } from 'zhin.js';
|
|
4
|
+
import { satoriRuntimeStateToken } from "./lib/satori-runtime-state.js";
|
|
5
|
+
export default definePlugin({
|
|
6
|
+
name: 'satori',
|
|
7
|
+
metadata: {
|
|
8
|
+
displayName: 'Satori Adapter',
|
|
9
|
+
},
|
|
10
|
+
setup(context) {
|
|
11
|
+
// 运行中 endpoint 注册表(satori endpoint list 的"运行中"数据源)
|
|
12
|
+
context.resources.provide(satoriRuntimeStateToken, createEndpointRuntimeState());
|
|
13
|
+
},
|
|
14
|
+
});
|
package/schema.json
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"connection": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"enum": [
|
|
9
|
+
"ws",
|
|
10
|
+
"webhook"
|
|
11
|
+
],
|
|
12
|
+
"default": "ws",
|
|
13
|
+
"description": "ws (default) or webhook (httpHostToken POST route)"
|
|
14
|
+
},
|
|
15
|
+
"heartbeat_interval": {
|
|
16
|
+
"type": "number",
|
|
17
|
+
"default": 10000,
|
|
18
|
+
"description": "WS PING interval in milliseconds"
|
|
19
|
+
},
|
|
20
|
+
"master": {
|
|
21
|
+
"type": [
|
|
22
|
+
"string",
|
|
23
|
+
"number"
|
|
24
|
+
],
|
|
25
|
+
"description": "框架 master(platform user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
|
|
26
|
+
},
|
|
27
|
+
"trusted": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": {
|
|
30
|
+
"type": [
|
|
31
|
+
"string",
|
|
32
|
+
"number"
|
|
33
|
+
],
|
|
34
|
+
"description": "Trusted platform user id"
|
|
35
|
+
},
|
|
36
|
+
"description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
|
|
37
|
+
},
|
|
38
|
+
"endpoints": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"minItems": 1,
|
|
41
|
+
"description": "多账号:每项定义一个 endpoint,id/baseUrl 必填,其余字段覆盖实例默认值",
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"properties": {
|
|
46
|
+
"master": {
|
|
47
|
+
"type": [
|
|
48
|
+
"string",
|
|
49
|
+
"number"
|
|
50
|
+
],
|
|
51
|
+
"description": "本 endpoint 的框架 master(platform user id);覆盖顶层 master"
|
|
52
|
+
},
|
|
53
|
+
"trusted": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": {
|
|
56
|
+
"type": [
|
|
57
|
+
"string",
|
|
58
|
+
"number"
|
|
59
|
+
],
|
|
60
|
+
"description": "Trusted platform user id"
|
|
61
|
+
},
|
|
62
|
+
"description": "本 endpoint 的 trusted 列表"
|
|
63
|
+
},
|
|
64
|
+
"baseUrl": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Satori SDK HTTP/WS base URL (e.g. http://127.0.0.1:5140)"
|
|
67
|
+
},
|
|
68
|
+
"connection": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"enum": [
|
|
71
|
+
"ws",
|
|
72
|
+
"webhook"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
"token": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Bearer token for API and WS IDENTIFY"
|
|
78
|
+
},
|
|
79
|
+
"path": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Webhook POST path (connection: webhook)"
|
|
82
|
+
},
|
|
83
|
+
"heartbeat_interval": {
|
|
84
|
+
"type": "number"
|
|
85
|
+
},
|
|
86
|
+
"commandPrefix": {
|
|
87
|
+
"type": "string"
|
|
88
|
+
},
|
|
89
|
+
"id": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Satori bot name"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"required": [
|
|
95
|
+
"id",
|
|
96
|
+
"baseUrl"
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"commandPrefix": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"default": "",
|
|
103
|
+
"description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"required": [
|
|
107
|
+
"endpoints"
|
|
108
|
+
]
|
|
109
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SatoriV1Client,
|
|
3
|
+
type SatoriV1Event,
|
|
4
|
+
} from '@imhelper/satori-v1';
|
|
5
|
+
import { EventFactory, type EventMap, type ImHelperEventMap } from 'imhelper';
|
|
6
|
+
import {
|
|
7
|
+
defineEndpointClient,
|
|
8
|
+
forwardEndpointClientEvents,
|
|
9
|
+
type ClientEventPayloads,
|
|
10
|
+
} from 'zhin.js/adapter';
|
|
11
|
+
import type {
|
|
12
|
+
ResolvedSatoriConfig,
|
|
13
|
+
SatoriApiOptions,
|
|
14
|
+
callSatoriApi,
|
|
15
|
+
} from './protocol.js';
|
|
16
|
+
|
|
17
|
+
export { SatoriV1Client as SatoriClient } from '@imhelper/satori-v1';
|
|
18
|
+
|
|
19
|
+
type SatoriClientEvents = ImHelperEventMap<string, SatoriV1Event, EventMap<string>>;
|
|
20
|
+
export type SatoriClientEventMap = ClientEventPayloads<SatoriClientEvents>;
|
|
21
|
+
|
|
22
|
+
export function createSatoriEndpointClient(
|
|
23
|
+
config: ResolvedSatoriConfig,
|
|
24
|
+
request: typeof callSatoriApi,
|
|
25
|
+
apiOptions: () => SatoriApiOptions,
|
|
26
|
+
): SatoriV1Client {
|
|
27
|
+
return new SatoriV1Client({
|
|
28
|
+
baseUrl: config.baseUrl,
|
|
29
|
+
selfId: config.id,
|
|
30
|
+
accessToken: config.token,
|
|
31
|
+
receiveMode: 'manual',
|
|
32
|
+
call: (resource, method, params) => request(apiOptions(), resource, method, params ?? {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const satoriClientEventNames = Object.freeze([
|
|
37
|
+
...EventFactory.getSupportedEventTypes<string>(),
|
|
38
|
+
'event',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export function forwardSatoriClientEvents(
|
|
42
|
+
client: SatoriV1Client,
|
|
43
|
+
receive: (name: string, payload: unknown) => void,
|
|
44
|
+
): () => void {
|
|
45
|
+
return forwardEndpointClientEvents(client, satoriClientEventNames, receive);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
declare module '@zhin.js/feature-kit' {
|
|
49
|
+
interface AdapterClientRegistry {
|
|
50
|
+
readonly satori: {
|
|
51
|
+
readonly client: SatoriV1Client;
|
|
52
|
+
readonly events: SatoriClientEventMap;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const satoriClient = defineEndpointClient<SatoriV1Client, SatoriClientEventMap>('satori');
|