@zhin.js/adapter-satori 1.0.0 → 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 -17
  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 -16
  39. package/lib/adapter.d.ts.map +0 -1
  40. package/lib/adapter.js +0 -34
  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/bot-webhook.d.ts +0 -27
  47. package/lib/bot-webhook.d.ts.map +0 -1
  48. package/lib/bot-webhook.js +0 -99
  49. package/lib/bot-webhook.js.map +0 -1
  50. package/lib/bot-ws.d.ts +0 -30
  51. package/lib/bot-ws.d.ts.map +0 -1
  52. package/lib/bot-ws.js +0 -195
  53. package/lib/bot-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 -46
  65. package/src/api.ts +0 -48
  66. package/src/bot-webhook.ts +0 -117
  67. package/src/bot-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,242 @@
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 { pickCredential } from 'zhin.js/adapter';
7
+ import { isMediaRef } from '@zhin.js/core';
8
+ import { formatCompact, getLogger } from '@zhin.js/logger';
9
+ const logger = getLogger('satori');
10
+ /** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
11
+ export const SatoriOpcode = {
12
+ EVENT: 0,
13
+ PING: 1,
14
+ PONG: 2,
15
+ IDENTIFY: 3,
16
+ READY: 4,
17
+ META: 5,
18
+ };
19
+ export function resolveSatoriConfig(config = {}) {
20
+ const entry = config.endpoints?.find((item) => item.context === 'satori');
21
+ const connection = config.connection ?? entry?.connection ?? 'ws';
22
+ const baseUrl = pickCredential(config.baseUrl, entry?.baseUrl, process.env.SATORI_BASE_URL);
23
+ if (!baseUrl) {
24
+ throw new TypeError('Satori adapter requires baseUrl (plugins.<key>.baseUrl or endpoints with context: satori)');
25
+ }
26
+ const id = (typeof config.id === 'string' && config.id)
27
+ || (typeof entry?.id === 'string' && entry.id)
28
+ || process.env.SATORI_BOT_NAME
29
+ || 'satori-bot';
30
+ const token = (typeof config.token === 'string' && config.token)
31
+ || (typeof entry?.token === 'string' && entry.token)
32
+ || process.env.SATORI_TOKEN
33
+ || undefined;
34
+ if (connection === 'webhook') {
35
+ const path = config.path ?? entry?.path;
36
+ if (!path) {
37
+ throw new TypeError('Satori connection:webhook requires path');
38
+ }
39
+ return {
40
+ context: 'satori',
41
+ connection: 'webhook',
42
+ id,
43
+ baseUrl,
44
+ token,
45
+ path,
46
+ };
47
+ }
48
+ const heartbeat = config.heartbeat_interval
49
+ ?? entry?.heartbeat_interval
50
+ ?? 10_000;
51
+ return {
52
+ context: 'satori',
53
+ connection: 'ws',
54
+ id,
55
+ baseUrl,
56
+ token,
57
+ heartbeat_interval: heartbeat,
58
+ };
59
+ }
60
+ /** Channel.type 1 = DIRECT (private). */
61
+ export function isPrivateChannel(channel) {
62
+ return channel?.type === 1;
63
+ }
64
+ export function isMessageEvent(body) {
65
+ return (body.type === 'message-created' || body.type === 'message-updated')
66
+ && !!body.message?.id;
67
+ }
68
+ export function buildWsUrl(baseUrl, token) {
69
+ const url = new URL(baseUrl.replace(/\/$/, ''));
70
+ if (token)
71
+ url.searchParams.set('access_token', token);
72
+ return url.toString();
73
+ }
74
+ /**
75
+ * Call Satori HTTP API: POST {baseUrl}/v1/{resource}.{method}
76
+ * @see https://satori.chat/en-US/protocol/api.html
77
+ */
78
+ export async function callSatoriApi(options, resource, method, params = {}) {
79
+ const { baseUrl, platform, userId, token } = options;
80
+ const url = `${baseUrl.replace(/\/$/, '')}/v1/${resource}.${method}`;
81
+ const headers = {
82
+ 'Content-Type': 'application/json',
83
+ 'Satori-Platform': platform,
84
+ 'Satori-User-ID': userId,
85
+ };
86
+ if (token)
87
+ headers.Authorization = `Bearer ${token}`;
88
+ const res = await fetch(url, {
89
+ method: 'POST',
90
+ headers,
91
+ body: JSON.stringify(params),
92
+ });
93
+ const text = await res.text();
94
+ if (res.status === 401)
95
+ throw new Error(`Satori API 认证失败: ${text}`);
96
+ if (res.status === 403)
97
+ throw new Error(`Satori API 权限不足: ${text}`);
98
+ if (res.status === 404)
99
+ throw new Error(`Satori API 不存在: ${resource}.${method}`);
100
+ if (res.status >= 400)
101
+ throw new Error(`Satori API 错误 ${res.status}: ${text}`);
102
+ if (!text || text.trim() === '')
103
+ return undefined;
104
+ try {
105
+ return JSON.parse(text);
106
+ }
107
+ catch {
108
+ throw new Error(`Satori API 无效 JSON: ${text.slice(0, 200)}`);
109
+ }
110
+ }
111
+ /** Build inbound text for OutboundMessageService.receive. */
112
+ export function formatInboundContent(body) {
113
+ const content = body.message.content ?? '';
114
+ return typeof content === 'string' ? content : String(content);
115
+ }
116
+ /**
117
+ * 入站归一化 → ConversationRef:Channel.type 1 (DIRECT) → kind 'private';
118
+ * 其余频道消息 → kind 'group',所属 guild 容器进 `parent`(kind 'channel')。
119
+ */
120
+ export function satoriInboundConversation(endpointKey, body) {
121
+ const channel = body.channel ?? body.message.channel;
122
+ const kind = isPrivateChannel(channel) ? 'private' : 'group';
123
+ return {
124
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
125
+ kind,
126
+ id: channel?.id ?? '',
127
+ ...(kind === 'group' && body.guild?.id
128
+ ? { parent: { kind: 'channel', id: body.guild.id } }
129
+ : {}),
130
+ };
131
+ }
132
+ export function resolveInboundSender(body) {
133
+ const user = body.user ?? body.message.user ?? body.message.member?.user;
134
+ const name = user?.name ?? body.message.member?.nick;
135
+ const id = user?.id ?? name ?? '';
136
+ return name && name !== id ? { id, name } : { id };
137
+ }
138
+ /**
139
+ * Detect `<at id="…"/>` elements in message content targeting the bot selfId.
140
+ * selfId 来源:READY/事件携带的 `login.user.id`。
141
+ */
142
+ export function isSelfMentioned(body, selfId) {
143
+ if (!selfId)
144
+ return false;
145
+ const content = body.message.content;
146
+ if (typeof content !== 'string' || !content.includes('<at'))
147
+ return false;
148
+ const tags = content.match(/<at\b[^>]*>/gi) ?? [];
149
+ return tags.some((tag) => /\bid\s*=\s*["']([^"']+)["']/i.exec(tag)?.[1] === selfId);
150
+ }
151
+ export function formatMessageId(channelId, messageId) {
152
+ return `${channelId}:${messageId}`;
153
+ }
154
+ export function parseMessageRef(id) {
155
+ if (id.includes(':')) {
156
+ const [channelId, messageId] = id.split(':');
157
+ return { channelId: channelId ?? '', messageId: messageId ?? '' };
158
+ }
159
+ return { channelId: '', messageId: id };
160
+ }
161
+ /**
162
+ * 解析媒体段(image/audio/video/file)的投递源,canonical MediaRef 为唯一来源:
163
+ * - kind=url → 直发 URL;
164
+ * - kind=base64 → 规范为 `base64://` 前缀内联(Satori img/file 元素消费内联数据)。
165
+ * 无 canonical 媒体引用或来源不可投递(path / file 本端点不物化)时 warn + 丢弃。
166
+ */
167
+ function resolveMediaSrc(type, data) {
168
+ const media = data.media;
169
+ if (!isMediaRef(media)) {
170
+ logger.warn(formatCompact({
171
+ op: 'satori_outbound_media_dropped',
172
+ type,
173
+ reason: 'missing_media_ref',
174
+ }));
175
+ return undefined;
176
+ }
177
+ if (media.kind === 'url')
178
+ return media.value;
179
+ if (media.kind === 'base64') {
180
+ return media.value.startsWith('base64://') ? media.value : `base64://${media.value}`;
181
+ }
182
+ logger.warn(formatCompact({
183
+ op: 'satori_outbound_media_dropped',
184
+ type,
185
+ reason: `unsupported_media_kind:${media.kind}`,
186
+ }));
187
+ return undefined;
188
+ }
189
+ /**
190
+ * Wire-encode an already-rendered outbound payload into Satori message content.
191
+ * Segment canonicalization is intentionally not done here.
192
+ */
193
+ export function formatSatoriOutbound(payload) {
194
+ if (typeof payload === 'string')
195
+ return payload;
196
+ if (payload == null)
197
+ return '';
198
+ const segments = Array.isArray(payload)
199
+ ? payload
200
+ : payload && typeof payload === 'object' && 'type' in payload
201
+ ? [payload]
202
+ : [];
203
+ if (segments.length === 0) {
204
+ return typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
205
+ }
206
+ const parts = [];
207
+ for (const item of segments) {
208
+ if (typeof item === 'string') {
209
+ parts.push(item);
210
+ continue;
211
+ }
212
+ const data = item.data ?? {};
213
+ switch (item.type) {
214
+ case 'text':
215
+ parts.push(String(data.text ?? data.content ?? ''));
216
+ break;
217
+ case 'at':
218
+ case 'mention':
219
+ parts.push(`@${String(data.name ?? data.id ?? data.target ?? '')}`);
220
+ break;
221
+ case 'image':
222
+ case 'audio':
223
+ case 'video':
224
+ case 'file': {
225
+ const src = resolveMediaSrc(item.type, data);
226
+ if (src)
227
+ parts.push(`[${item.type}:${src}]`);
228
+ break;
229
+ }
230
+ default:
231
+ break;
232
+ }
233
+ }
234
+ return parts.join('');
235
+ }
236
+ export function extractCreatedMessageId(result) {
237
+ const list = Array.isArray(result)
238
+ ? result
239
+ : result?.data;
240
+ const msg = list?.[0];
241
+ return msg?.id ?? '';
242
+ }
@@ -0,0 +1 @@
1
+ export declare const satoriEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage, string | undefined>>>;
@@ -0,0 +1,18 @@
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
+ export const satoriEndpointCommands = createEndpointCommands({
9
+ adapterKey: 'satori',
10
+ adapterDisplayName: 'Satori',
11
+ fields: [
12
+ { key: 'baseUrl', required: true, description: 'Satori 服务 base URL' },
13
+ { key: 'path', description: 'webhook 路径(connection: webhook)' },
14
+ { key: 'token', env: true, description: 'Satori access token' },
15
+ ],
16
+ running: (use) => use(satoriRuntimeStateToken).endpoints.values(),
17
+ describeEntry: (entry) => `baseUrl: ${String(entry.baseUrl)}`,
18
+ }, defineCommand);
@@ -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');
@@ -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
@@ -0,0 +1,5 @@
1
+ import WebSocket from 'ws';
2
+ export const WS_OPEN = 1;
3
+ export function defaultCreateWebSocket(url, options) {
4
+ return new WebSocket(url, { headers: options?.headers });
5
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-satori",
3
- "version": "1.0.0",
4
- "description": "Zhin.js adapter for Satori protocol",
3
+ "version": "1.1.0",
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
- "skills",
21
- "README.md"
22
+ "agent",
23
+ "README.md",
24
+ "CHANGELOG.md"
22
25
  ],
23
26
  "keywords": [
24
27
  "zhin",
@@ -31,31 +34,75 @@
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
- "ws": "^8.21.0"
41
+ "@imhelper/satori-v1": "1.0.7",
42
+ "imhelper": "1.0.7",
43
+ "ws": "^8.21.1",
44
+ "@zhin.js/adapter": "1.1.12",
45
+ "@zhin.js/core": "1.1.35",
46
+ "@zhin.js/feature-kit": "1.1.0",
47
+ "@zhin.js/host-http": "1.1.0",
48
+ "@zhin.js/im-contract": "1.1.0",
49
+ "@zhin.js/logger": "1.1.0"
39
50
  },
40
51
  "devDependencies": {
41
- "@types/node": "^25.9.1",
52
+ "@types/node": "^26.1.2",
42
53
  "@types/ws": "^8.18.1",
43
54
  "typescript": "^6.0.3",
44
- "zhin.js": "2.0.0",
45
- "@zhin.js/cli": "1.0.88",
46
- "@zhin.js/client": "2.0.2"
55
+ "vitest": "^4.1.10",
56
+ "@zhin.js/host-http": "1.1.0",
57
+ "zhin.js": "1.1.0"
47
58
  },
48
59
  "peerDependencies": {
49
- "zhin.js": "2.0.0",
50
- "@zhin.js/host-router": "1.0.0"
60
+ "@zhin.js/adapter": "^1.1.12",
61
+ "@zhin.js/command": "^1.1.0",
62
+ "@zhin.js/core": "^1.1.35",
63
+ "zhin.js": "^1.1.0"
51
64
  },
52
65
  "peerDependenciesMeta": {
53
- "@zhin.js/host-router": {
66
+ "@zhin.js/command": {
67
+ "optional": true
68
+ },
69
+ "zhin.js": {
54
70
  "optional": true
55
71
  }
56
72
  },
73
+ "author": {
74
+ "name": "lc-cn",
75
+ "email": "admin@liucl.cn",
76
+ "url": "https://github.com/lc-cn"
77
+ },
78
+ "engines": {
79
+ "node": "^20.19.0 || >=22.12.0"
80
+ },
81
+ "publishConfig": {
82
+ "access": "public",
83
+ "registry": "https://registry.npmjs.org"
84
+ },
85
+ "zhin": {
86
+ "protocol": 1,
87
+ "type": "plugin",
88
+ "entry": "./plugin.js",
89
+ "engine": "^1.0.0",
90
+ "runtime": "trusted",
91
+ "features": [
92
+ {
93
+ "package": "@zhin.js/adapter",
94
+ "api": "^1.0.0"
95
+ },
96
+ {
97
+ "package": "@zhin.js/command",
98
+ "api": "^1.0.0"
99
+ }
100
+ ],
101
+ "plugins": []
102
+ },
57
103
  "scripts": {
58
- "build": "zhin build",
59
- "clean": "rimraf lib"
104
+ "build": "tsc",
105
+ "clean": "rimraf lib",
106
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/satori/tests"
60
107
  }
61
108
  }
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,95 @@
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
+ "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
41
+ "items": {
42
+ "type": "object",
43
+ "additionalProperties": true,
44
+ "properties": {
45
+ "master": {
46
+ "type": [
47
+ "string",
48
+ "number"
49
+ ],
50
+ "description": "本 endpoint 的框架 master(platform user id);覆盖顶层 master"
51
+ },
52
+ "trusted": {
53
+ "type": "array",
54
+ "items": {
55
+ "type": [
56
+ "string",
57
+ "number"
58
+ ],
59
+ "description": "Trusted platform user id"
60
+ },
61
+ "description": "本 endpoint 的 trusted 列表"
62
+ },
63
+ "baseUrl": {
64
+ "type": "string",
65
+ "description": "Satori SDK HTTP/WS base URL (e.g. http://127.0.0.1:5140)"
66
+ },
67
+ "token": {
68
+ "type": "string",
69
+ "description": "Bearer token for API and WS IDENTIFY"
70
+ },
71
+ "path": {
72
+ "type": "string",
73
+ "description": "Webhook POST path (connection: webhook)"
74
+ },
75
+ "id": {
76
+ "type": "string",
77
+ "description": "Satori bot name"
78
+ }
79
+ },
80
+ "required": [
81
+ "id",
82
+ "baseUrl"
83
+ ]
84
+ }
85
+ },
86
+ "commandPrefix": {
87
+ "type": "string",
88
+ "default": "",
89
+ "description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
90
+ }
91
+ },
92
+ "required": [
93
+ "endpoints"
94
+ ]
95
+ }
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');