@zhin.js/adapter-satori 5.0.1 → 5.0.4
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 +55 -0
- package/README.md +2 -2
- package/adapters/satori.js +2 -2
- package/adapters/satori.ts +2 -2
- package/lib/endpoint.d.ts +3 -9
- package/lib/endpoint.js +44 -46
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +14 -6
- package/lib/protocol.js +21 -7
- package/package.json +13 -12
- package/schema.json +41 -5
- package/src/endpoint.ts +48 -48
- package/src/index.ts +1 -3
- package/src/protocol.ts +32 -11
- /package/commands/endpoint/add/{[name:string].js → [id].js} +0 -0
- /package/commands/endpoint/add/{[name:string].ts → [id].ts} +0 -0
- /package/commands/endpoint/remove/{[name:string].js → [id].js} +0 -0
- /package/commands/endpoint/remove/{[name:string].ts → [id].ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# @zhin.js/adapter-satori
|
|
2
2
|
|
|
3
|
+
## 5.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [0de46a8]
|
|
8
|
+
- Updated dependencies [c106ecc]
|
|
9
|
+
- Updated dependencies [ca92e03]
|
|
10
|
+
- Updated dependencies [b0f37ae]
|
|
11
|
+
- Updated dependencies [ba08a2f]
|
|
12
|
+
- Updated dependencies [daffd4c]
|
|
13
|
+
- Updated dependencies [36c7400]
|
|
14
|
+
- Updated dependencies [162fa34]
|
|
15
|
+
- Updated dependencies [e40b048]
|
|
16
|
+
- Updated dependencies [f1708c3]
|
|
17
|
+
- Updated dependencies [e53444f]
|
|
18
|
+
- Updated dependencies [92b0dd7]
|
|
19
|
+
- Updated dependencies [a7df753]
|
|
20
|
+
- @zhin.js/host-http@1.0.8
|
|
21
|
+
- @zhin.js/im-contract@1.0.3
|
|
22
|
+
- @zhin.js/adapter@1.1.7
|
|
23
|
+
- @zhin.js/plugin-runtime@1.1.5
|
|
24
|
+
- @zhin.js/command@1.0.9
|
|
25
|
+
- @zhin.js/core@1.5.4
|
|
26
|
+
- zhin.js@6.0.4
|
|
27
|
+
|
|
28
|
+
## 5.0.3
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- f8c7a54: fix: im
|
|
33
|
+
- Updated dependencies [f8c7a54]
|
|
34
|
+
- @zhin.js/logger@1.0.76
|
|
35
|
+
- @zhin.js/host-http@1.0.7
|
|
36
|
+
- @zhin.js/adapter@1.1.6
|
|
37
|
+
- @zhin.js/command@1.0.8
|
|
38
|
+
- @zhin.js/core@1.5.3
|
|
39
|
+
- @zhin.js/im-contract@1.0.2
|
|
40
|
+
- @zhin.js/plugin-runtime@1.1.4
|
|
41
|
+
- zhin.js@6.0.3
|
|
42
|
+
|
|
43
|
+
## 5.0.2
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [afc0e66]
|
|
48
|
+
- Updated dependencies [2e41ad5]
|
|
49
|
+
- Updated dependencies [9f57124]
|
|
50
|
+
- @zhin.js/core@1.5.2
|
|
51
|
+
- @zhin.js/adapter@1.1.5
|
|
52
|
+
- @zhin.js/im-contract@1.0.1
|
|
53
|
+
- @zhin.js/plugin-runtime@1.1.3
|
|
54
|
+
- @zhin.js/command@1.0.7
|
|
55
|
+
- @zhin.js/host-http@1.0.6
|
|
56
|
+
- zhin.js@6.0.2
|
|
57
|
+
|
|
3
58
|
## 5.0.1
|
|
4
59
|
|
|
5
60
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -27,8 +27,8 @@ pnpm add @zhin.js/adapter-satori
|
|
|
27
27
|
- `@zhin.js/host-http` — Webhook 模式需 `httpHostToken` 注册 POST 路由
|
|
28
28
|
- 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`(`baseUrl` / `token` / …)
|
|
29
29
|
|
|
30
|
-
入站:`gateway.receive({
|
|
31
|
-
出站:`send({
|
|
30
|
+
入站:`gateway.receive({ conversation, message, content, sender, metadata })`(`conversation` 为 ConversationRef:DIRECT 频道 → kind `private`,其余 → kind `group`、所属 guild 进 `parent`)
|
|
31
|
+
出站:`send({ conversation, payload })` → Satori `message.create`(`channel_id = conversation.id`;payload 已由 gateway/core 渲染;无 segment-mapper)
|
|
32
32
|
|
|
33
33
|
入站 `metadata.mentioned`:消息 content 中 `<at id="…"/>` 元素的 id 等于登录 selfId(READY/事件 `login.user.id`)时置 `true`。
|
|
34
34
|
|
package/adapters/satori.js
CHANGED
|
@@ -20,8 +20,8 @@ export default defineAdapter({
|
|
|
20
20
|
const config = resolveSatoriConfig(context.config);
|
|
21
21
|
const gateway = context.use(messageGatewayToken);
|
|
22
22
|
// 注册到插件运行时状态(satori.endpoint list 的"运行中"数据源)
|
|
23
|
-
context.use(satoriRuntimeStateToken).endpoints.set(config.
|
|
24
|
-
|
|
23
|
+
context.use(satoriRuntimeStateToken).endpoints.set(config.id, {
|
|
24
|
+
id: config.id,
|
|
25
25
|
mode: config.connection,
|
|
26
26
|
});
|
|
27
27
|
if (config.connection === 'webhook') {
|
package/adapters/satori.ts
CHANGED
|
@@ -31,8 +31,8 @@ export default defineAdapter<SatoriAdapterConfig>({
|
|
|
31
31
|
const config = resolveSatoriConfig(context.config);
|
|
32
32
|
const gateway = context.use(messageGatewayToken);
|
|
33
33
|
// 注册到插件运行时状态(satori.endpoint list 的"运行中"数据源)
|
|
34
|
-
context.use(satoriRuntimeStateToken).endpoints.set(config.
|
|
35
|
-
|
|
34
|
+
context.use(satoriRuntimeStateToken).endpoints.set(config.id, {
|
|
35
|
+
id: config.id,
|
|
36
36
|
mode: config.connection,
|
|
37
37
|
});
|
|
38
38
|
if (config.connection === 'webhook') {
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SatoriEndpoint — WebSocket and webhook lifecycle, outbound, admit.
|
|
3
3
|
*/
|
|
4
|
-
import { type EndpointInstance, type EndpointManagement } from '@zhin.js/adapter';
|
|
4
|
+
import { type EndpointInstance, type EndpointManagement, type EndpointSendRequest } from '@zhin.js/adapter';
|
|
5
5
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
6
6
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
7
7
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
@@ -23,10 +23,7 @@ export declare class SatoriWsEndpoint implements EndpointInstance {
|
|
|
23
23
|
open(): void;
|
|
24
24
|
close(): void;
|
|
25
25
|
stop(): Promise<void>;
|
|
26
|
-
send({
|
|
27
|
-
readonly target: string;
|
|
28
|
-
readonly payload: unknown;
|
|
29
|
-
}): Promise<string>;
|
|
26
|
+
send({ conversation, payload }: EndpointSendRequest): Promise<string>;
|
|
30
27
|
/** Test / internal: admit a gateway event when the endpoint is open. */
|
|
31
28
|
admit(body: SatoriEventBody): void;
|
|
32
29
|
recall(id: string): Promise<void>;
|
|
@@ -51,10 +48,7 @@ export declare class SatoriWebhookEndpoint implements EndpointInstance {
|
|
|
51
48
|
open(): void;
|
|
52
49
|
close(): void;
|
|
53
50
|
stop(): Promise<void>;
|
|
54
|
-
send({
|
|
55
|
-
readonly target: string;
|
|
56
|
-
readonly payload: unknown;
|
|
57
|
-
}): Promise<string>;
|
|
51
|
+
send({ conversation, payload }: EndpointSendRequest): Promise<string>;
|
|
58
52
|
admit(body: SatoriEventBody): void;
|
|
59
53
|
recall(id: string): Promise<void>;
|
|
60
54
|
/** Test helper: inject login without a live webhook push. */
|
package/lib/endpoint.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* SatoriEndpoint — WebSocket and webhook lifecycle, outbound, admit.
|
|
3
3
|
*/
|
|
4
4
|
import { createEndpointLifecycle, } from '@zhin.js/adapter';
|
|
5
|
-
import { formatCompact,
|
|
6
|
-
import { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatMessageId, formatSatoriOutbound, isMessageEvent, isSelfMentioned, parseMessageRef, resolveInboundSender,
|
|
5
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
6
|
+
import { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatMessageId, formatSatoriOutbound, isMessageEvent, isSelfMentioned, parseMessageRef, resolveInboundSender, satoriInboundConversation, } from './protocol.js';
|
|
7
7
|
import { registerSatoriWebhookRoutes } from './webhook.js';
|
|
8
8
|
import { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
|
|
9
|
-
const logger = getLogger('satori');
|
|
10
9
|
export class SatoriWsEndpoint {
|
|
10
|
+
#logger;
|
|
11
11
|
#options;
|
|
12
12
|
#lifecycle;
|
|
13
13
|
#ws = null;
|
|
@@ -16,10 +16,11 @@ export class SatoriWsEndpoint {
|
|
|
16
16
|
#open = false;
|
|
17
17
|
management = createSatoriEndpointManagement((resource, method, params) => this.#api(resource, method, params));
|
|
18
18
|
constructor(options) {
|
|
19
|
+
this.#logger = getAdapterLogger('satori', options.config.id);
|
|
19
20
|
this.#options = options;
|
|
20
21
|
const { config } = options;
|
|
21
22
|
this.#lifecycle = createEndpointLifecycle({
|
|
22
|
-
name: config.
|
|
23
|
+
name: config.id,
|
|
23
24
|
reconnect: {
|
|
24
25
|
initialIntervalMs: 5_000,
|
|
25
26
|
// 固定间隔(multiplier 1、无抖动),对齐旧 5s 固定重连语义
|
|
@@ -64,20 +65,20 @@ export class SatoriWsEndpoint {
|
|
|
64
65
|
await this.#lifecycle.stop();
|
|
65
66
|
this.#ws = null;
|
|
66
67
|
}
|
|
67
|
-
async send({
|
|
68
|
+
async send({ conversation, payload }) {
|
|
68
69
|
const content = formatSatoriOutbound(payload);
|
|
69
70
|
const result = await this.#api('message', 'create', {
|
|
70
|
-
channel_id:
|
|
71
|
+
channel_id: conversation.id,
|
|
71
72
|
content,
|
|
72
73
|
});
|
|
73
74
|
const msgId = extractCreatedMessageId(result);
|
|
74
|
-
logger.debug(formatCompact({
|
|
75
|
+
this.#logger.debug(formatCompact({
|
|
75
76
|
op: 'satori_send',
|
|
76
|
-
endpoint: this.#options.config.
|
|
77
|
-
|
|
77
|
+
endpoint: this.#options.config.id,
|
|
78
|
+
channel: conversation.id,
|
|
78
79
|
messageId: msgId || undefined,
|
|
79
80
|
}));
|
|
80
|
-
return msgId ? formatMessageId(
|
|
81
|
+
return msgId ? formatMessageId(conversation.id, msgId) : '';
|
|
81
82
|
}
|
|
82
83
|
/** Test / internal: admit a gateway event when the endpoint is open. */
|
|
83
84
|
admit(body) {
|
|
@@ -87,30 +88,28 @@ export class SatoriWsEndpoint {
|
|
|
87
88
|
this.#login = body.login;
|
|
88
89
|
if (!isMessageEvent(body))
|
|
89
90
|
return;
|
|
90
|
-
const
|
|
91
|
+
const conversation = satoriInboundConversation(String(this.#options.id), body);
|
|
91
92
|
const content = formatInboundContent(body);
|
|
92
93
|
const sender = resolveInboundSender(body);
|
|
93
|
-
const messageId = formatMessageId(target, body.message.id);
|
|
94
94
|
const selfId = this.#login?.user?.id ?? body.login?.user?.id;
|
|
95
95
|
const mentioned = isSelfMentioned(body, selfId);
|
|
96
96
|
void this.#options.gateway.receive({
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
conversation,
|
|
98
|
+
message: { conversation, id: body.message.id },
|
|
99
99
|
content,
|
|
100
100
|
sender,
|
|
101
|
-
|
|
101
|
+
endpointId: this.#options.config.id,
|
|
102
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
102
103
|
metadata: Object.freeze({
|
|
103
104
|
type: body.type,
|
|
104
105
|
channelType: isPrivateChannelType(body) ? 'private' : 'group',
|
|
105
106
|
sn: body.sn,
|
|
106
107
|
platform: this.#login?.platform,
|
|
107
|
-
endpoint: this.#options.config.name,
|
|
108
|
-
...(mentioned ? { mentioned: true } : {}),
|
|
109
108
|
}),
|
|
110
109
|
}).catch((err) => {
|
|
111
|
-
logger.warn(formatCompact({
|
|
110
|
+
this.#logger.warn(formatCompact({
|
|
112
111
|
op: 'satori_gateway_receive_failed',
|
|
113
|
-
|
|
112
|
+
channel: conversation.id,
|
|
114
113
|
error: err instanceof Error ? err.message : String(err),
|
|
115
114
|
}));
|
|
116
115
|
});
|
|
@@ -145,7 +144,7 @@ export class SatoriWsEndpoint {
|
|
|
145
144
|
}
|
|
146
145
|
});
|
|
147
146
|
ws.on('open', () => {
|
|
148
|
-
logger.debug(formatCompact({
|
|
147
|
+
this.#logger.debug(formatCompact({ mode: 'ws' }));
|
|
149
148
|
this.#sendSignal(SatoriOpcode.IDENTIFY, {
|
|
150
149
|
token: config.token,
|
|
151
150
|
sn: this.#lastSn,
|
|
@@ -167,9 +166,9 @@ export class SatoriWsEndpoint {
|
|
|
167
166
|
this.#handleSignal(signal);
|
|
168
167
|
}
|
|
169
168
|
catch (error) {
|
|
170
|
-
logger.warn(formatCompact({
|
|
169
|
+
this.#logger.warn(formatCompact({
|
|
171
170
|
op: 'ws_parse_error',
|
|
172
|
-
endpoint: config.
|
|
171
|
+
endpoint: config.id,
|
|
173
172
|
error: error instanceof Error ? error.message : String(error),
|
|
174
173
|
}));
|
|
175
174
|
}
|
|
@@ -190,9 +189,9 @@ export class SatoriWsEndpoint {
|
|
|
190
189
|
handle.notifyClosed(`Satori WS closed: ${numericCode} ${reasonStr || 'closed'}`);
|
|
191
190
|
});
|
|
192
191
|
ws.on('error', (error) => {
|
|
193
|
-
logger.warn(formatCompact({
|
|
192
|
+
this.#logger.warn(formatCompact({
|
|
194
193
|
op: 'ws_error',
|
|
195
|
-
endpoint: config.
|
|
194
|
+
endpoint: config.id,
|
|
196
195
|
ok: false,
|
|
197
196
|
error: error instanceof Error ? error.message : String(error),
|
|
198
197
|
}));
|
|
@@ -213,7 +212,7 @@ export class SatoriWsEndpoint {
|
|
|
213
212
|
const logins = signal.body.logins;
|
|
214
213
|
this.#login = logins[0];
|
|
215
214
|
if (!this.#login?.platform || !this.#login?.user?.id) {
|
|
216
|
-
logger.warn(formatCompact({ op: 'ready', ok: false, error: 'missing platform/user' }));
|
|
215
|
+
this.#logger.warn(formatCompact({ op: 'ready', ok: false, error: 'missing platform/user' }));
|
|
217
216
|
}
|
|
218
217
|
return;
|
|
219
218
|
}
|
|
@@ -242,6 +241,7 @@ export class SatoriWsEndpoint {
|
|
|
242
241
|
}
|
|
243
242
|
}
|
|
244
243
|
export class SatoriWebhookEndpoint {
|
|
244
|
+
#logger;
|
|
245
245
|
#options;
|
|
246
246
|
#login;
|
|
247
247
|
#routeReleases = [];
|
|
@@ -249,6 +249,7 @@ export class SatoriWebhookEndpoint {
|
|
|
249
249
|
#started = false;
|
|
250
250
|
management = createSatoriEndpointManagement((resource, method, params) => this.#api(resource, method, params));
|
|
251
251
|
constructor(options) {
|
|
252
|
+
this.#logger = getAdapterLogger('satori', options.config.id);
|
|
252
253
|
this.#options = options;
|
|
253
254
|
}
|
|
254
255
|
/** Used by webhook handler. */
|
|
@@ -264,17 +265,17 @@ export class SatoriWebhookEndpoint {
|
|
|
264
265
|
this.#started = true;
|
|
265
266
|
if (!this.#options.config.token) {
|
|
266
267
|
// 未配 token 时 webhook 无鉴权:任何人知道 path 即可注入假事件。
|
|
267
|
-
logger.warn(formatCompact({
|
|
268
|
+
this.#logger.warn(formatCompact({
|
|
268
269
|
op: 'webhook_no_token',
|
|
269
|
-
endpoint: this.#options.config.
|
|
270
|
+
endpoint: this.#options.config.id,
|
|
270
271
|
path: this.#options.config.path,
|
|
271
272
|
hint: 'set token to authenticate Satori webhook callbacks',
|
|
272
273
|
}));
|
|
273
274
|
}
|
|
274
275
|
this.#routeReleases.push(...registerSatoriWebhookRoutes(this.#options.http, this));
|
|
275
|
-
logger.info(formatCompact({
|
|
276
|
+
this.#logger.info(formatCompact({
|
|
276
277
|
op: 'listen',
|
|
277
|
-
endpoint: this.#options.config.
|
|
278
|
+
endpoint: this.#options.config.id,
|
|
278
279
|
mode: 'webhook',
|
|
279
280
|
path: this.#options.config.path,
|
|
280
281
|
}));
|
|
@@ -290,25 +291,24 @@ export class SatoriWebhookEndpoint {
|
|
|
290
291
|
for (const release of this.#routeReleases.splice(0))
|
|
291
292
|
release();
|
|
292
293
|
this.#started = false;
|
|
293
|
-
logger.debug(formatCompact({
|
|
294
|
+
this.#logger.debug(formatCompact({
|
|
294
295
|
op: 'disconnect',
|
|
295
|
-
endpoint: this.#options.config.name,
|
|
296
296
|
}));
|
|
297
297
|
}
|
|
298
|
-
async send({
|
|
298
|
+
async send({ conversation, payload }) {
|
|
299
299
|
const content = formatSatoriOutbound(payload);
|
|
300
300
|
const result = await this.#api('message', 'create', {
|
|
301
|
-
channel_id:
|
|
301
|
+
channel_id: conversation.id,
|
|
302
302
|
content,
|
|
303
303
|
});
|
|
304
304
|
const msgId = extractCreatedMessageId(result);
|
|
305
|
-
logger.debug(formatCompact({
|
|
305
|
+
this.#logger.debug(formatCompact({
|
|
306
306
|
op: 'satori_send',
|
|
307
|
-
endpoint: this.#options.config.
|
|
308
|
-
|
|
307
|
+
endpoint: this.#options.config.id,
|
|
308
|
+
channel: conversation.id,
|
|
309
309
|
messageId: msgId || undefined,
|
|
310
310
|
}));
|
|
311
|
-
return msgId ? formatMessageId(
|
|
311
|
+
return msgId ? formatMessageId(conversation.id, msgId) : '';
|
|
312
312
|
}
|
|
313
313
|
admit(body) {
|
|
314
314
|
if (!this.#open)
|
|
@@ -317,30 +317,28 @@ export class SatoriWebhookEndpoint {
|
|
|
317
317
|
this.#login = body.login;
|
|
318
318
|
if (!isMessageEvent(body))
|
|
319
319
|
return;
|
|
320
|
-
const
|
|
320
|
+
const conversation = satoriInboundConversation(String(this.#options.id), body);
|
|
321
321
|
const content = formatInboundContent(body);
|
|
322
322
|
const sender = resolveInboundSender(body);
|
|
323
|
-
const messageId = formatMessageId(target, body.message.id);
|
|
324
323
|
const selfId = this.#login?.user?.id ?? body.login?.user?.id;
|
|
325
324
|
const mentioned = isSelfMentioned(body, selfId);
|
|
326
325
|
void this.#options.gateway.receive({
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
conversation,
|
|
327
|
+
message: { conversation, id: body.message.id },
|
|
329
328
|
content,
|
|
330
329
|
sender,
|
|
331
|
-
|
|
330
|
+
endpointId: this.#options.config.id,
|
|
331
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
332
332
|
metadata: Object.freeze({
|
|
333
333
|
type: body.type,
|
|
334
334
|
channelType: isPrivateChannelType(body) ? 'private' : 'group',
|
|
335
335
|
sn: body.sn,
|
|
336
336
|
platform: this.#login?.platform,
|
|
337
|
-
endpoint: this.#options.config.name,
|
|
338
|
-
...(mentioned ? { mentioned: true } : {}),
|
|
339
337
|
}),
|
|
340
338
|
}).catch((err) => {
|
|
341
|
-
logger.warn(formatCompact({
|
|
339
|
+
this.#logger.warn(formatCompact({
|
|
342
340
|
op: 'satori_gateway_receive_failed',
|
|
343
|
-
|
|
341
|
+
channel: conversation.id,
|
|
344
342
|
error: err instanceof Error ? err.message : String(err),
|
|
345
343
|
}));
|
|
346
344
|
});
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent,
|
|
1
|
+
export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatSatoriOutbound, isMessageEvent, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, type ResolvedSatoriWebhookConfig, type ResolvedSatoriWsConfig, type SatoriAdapterConfig, type SatoriApiOptions, type SatoriChannel, type SatoriEventBody, type SatoriLogin, type SatoriMessage, type SatoriSignal, type SatoriUser, type SatoriWireSegment, } from './protocol.js';
|
|
2
2
|
export { SatoriWebhookEndpoint, SatoriWsEndpoint, type CreateSatoriWebSocket, type SatoriApiCaller, type SatoriWebhookEndpointOptions, type SatoriWsEndpointOptions, type SatoriWsSocket, } from './endpoint.js';
|
|
3
3
|
export { handleSatoriWebhookRequest, readRequestBody, registerSatoriWebhookRoutes, resolveSatoriOpcode, verifySatoriToken, type SatoriWebhookHandler, } from './webhook.js';
|
|
4
4
|
export { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent,
|
|
1
|
+
export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatSatoriOutbound, isMessageEvent, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, } from './protocol.js';
|
|
2
2
|
export { SatoriWebhookEndpoint, SatoriWsEndpoint, } from './endpoint.js';
|
|
3
3
|
export { handleSatoriWebhookRequest, readRequestBody, registerSatoriWebhookRoutes, resolveSatoriOpcode, verifySatoriToken, } from './webhook.js';
|
|
4
4
|
export { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
|
package/lib/protocol.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
4
|
* Spec: https://satori.chat/zh-CN/protocol/overview.html
|
|
5
5
|
*/
|
|
6
|
+
import type { ConversationRef } from '@zhin.js/im-contract';
|
|
6
7
|
/** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
|
|
7
8
|
export declare const SatoriOpcode: {
|
|
8
9
|
readonly EVENT: 0;
|
|
@@ -77,7 +78,7 @@ export interface SatoriWireSegment {
|
|
|
77
78
|
}
|
|
78
79
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
79
80
|
export interface SatoriAdapterConfig {
|
|
80
|
-
readonly
|
|
81
|
+
readonly id?: string;
|
|
81
82
|
readonly connection?: 'ws' | 'webhook';
|
|
82
83
|
readonly baseUrl?: string;
|
|
83
84
|
readonly token?: string;
|
|
@@ -94,7 +95,7 @@ export interface SatoriAdapterConfig {
|
|
|
94
95
|
export interface ResolvedSatoriWsConfig {
|
|
95
96
|
readonly context: 'satori';
|
|
96
97
|
readonly connection: 'ws';
|
|
97
|
-
readonly
|
|
98
|
+
readonly id: string;
|
|
98
99
|
readonly baseUrl: string;
|
|
99
100
|
readonly token?: string;
|
|
100
101
|
readonly heartbeat_interval: number;
|
|
@@ -102,7 +103,7 @@ export interface ResolvedSatoriWsConfig {
|
|
|
102
103
|
export interface ResolvedSatoriWebhookConfig {
|
|
103
104
|
readonly context: 'satori';
|
|
104
105
|
readonly connection: 'webhook';
|
|
105
|
-
readonly
|
|
106
|
+
readonly id: string;
|
|
106
107
|
readonly baseUrl: string;
|
|
107
108
|
readonly token?: string;
|
|
108
109
|
readonly path: string;
|
|
@@ -124,12 +125,19 @@ export declare function callSatoriApi<T = unknown>(options: SatoriApiOptions, re
|
|
|
124
125
|
export declare function formatInboundContent(body: SatoriEventBody & {
|
|
125
126
|
message: SatoriMessage;
|
|
126
127
|
}): string;
|
|
127
|
-
|
|
128
|
+
/**
|
|
129
|
+
* 入站归一化 → ConversationRef:Channel.type 1 (DIRECT) → kind 'private';
|
|
130
|
+
* 其余频道消息 → kind 'group',所属 guild 容器进 `parent`(kind 'channel')。
|
|
131
|
+
*/
|
|
132
|
+
export declare function satoriInboundConversation(endpointKey: string, body: SatoriEventBody & {
|
|
128
133
|
message: SatoriMessage;
|
|
129
|
-
}):
|
|
134
|
+
}): ConversationRef;
|
|
130
135
|
export declare function resolveInboundSender(body: SatoriEventBody & {
|
|
131
136
|
message: SatoriMessage;
|
|
132
|
-
}):
|
|
137
|
+
}): {
|
|
138
|
+
id: string;
|
|
139
|
+
name?: string;
|
|
140
|
+
};
|
|
133
141
|
/**
|
|
134
142
|
* Detect `<at id="…"/>` elements in message content targeting the bot selfId.
|
|
135
143
|
* selfId 来源:READY/事件携带的 `login.user.id`。
|
package/lib/protocol.js
CHANGED
|
@@ -23,8 +23,8 @@ export function resolveSatoriConfig(config = {}) {
|
|
|
23
23
|
if (!baseUrl) {
|
|
24
24
|
throw new TypeError('Satori adapter requires baseUrl (plugins.<key>.baseUrl or endpoints with context: satori)');
|
|
25
25
|
}
|
|
26
|
-
const
|
|
27
|
-
|| (typeof entry?.
|
|
26
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
27
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
28
28
|
|| process.env.SATORI_BOT_NAME
|
|
29
29
|
|| 'satori-bot';
|
|
30
30
|
const token = (typeof config.token === 'string' && config.token)
|
|
@@ -39,7 +39,7 @@ export function resolveSatoriConfig(config = {}) {
|
|
|
39
39
|
return {
|
|
40
40
|
context: 'satori',
|
|
41
41
|
connection: 'webhook',
|
|
42
|
-
|
|
42
|
+
id,
|
|
43
43
|
baseUrl,
|
|
44
44
|
token,
|
|
45
45
|
path,
|
|
@@ -51,7 +51,7 @@ export function resolveSatoriConfig(config = {}) {
|
|
|
51
51
|
return {
|
|
52
52
|
context: 'satori',
|
|
53
53
|
connection: 'ws',
|
|
54
|
-
|
|
54
|
+
id,
|
|
55
55
|
baseUrl,
|
|
56
56
|
token,
|
|
57
57
|
heartbeat_interval: heartbeat,
|
|
@@ -113,13 +113,27 @@ export function formatInboundContent(body) {
|
|
|
113
113
|
const content = body.message.content ?? '';
|
|
114
114
|
return typeof content === 'string' ? content : String(content);
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
/**
|
|
117
|
+
* 入站归一化 → ConversationRef:Channel.type 1 (DIRECT) → kind 'private';
|
|
118
|
+
* 其余频道消息 → kind 'group',所属 guild 容器进 `parent`(kind 'channel')。
|
|
119
|
+
*/
|
|
120
|
+
export function satoriInboundConversation(endpointKey, body) {
|
|
117
121
|
const channel = body.channel ?? body.message.channel;
|
|
118
|
-
|
|
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
|
+
};
|
|
119
131
|
}
|
|
120
132
|
export function resolveInboundSender(body) {
|
|
121
133
|
const user = body.user ?? body.message.user ?? body.message.member?.user;
|
|
122
|
-
|
|
134
|
+
const name = user?.name ?? body.message.member?.nick;
|
|
135
|
+
const id = user?.id ?? name ?? '';
|
|
136
|
+
return name && name !== id ? { id, name } : { id };
|
|
123
137
|
}
|
|
124
138
|
/**
|
|
125
139
|
* Detect `<at id="…"/>` elements in message content targeting the bot selfId.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-satori",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
4
4
|
"description": "Zhin.js Satori adapter for Plugin Runtime (WebSocket client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -39,25 +39,26 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"ws": "^8.21.1",
|
|
42
|
-
"@zhin.js/adapter": "1.1.
|
|
43
|
-
"@zhin.js/command": "1.0.
|
|
44
|
-
"@zhin.js/core": "1.5.
|
|
45
|
-
"@zhin.js/host-http": "1.0.
|
|
46
|
-
"@zhin.js/
|
|
47
|
-
"@zhin.js/
|
|
42
|
+
"@zhin.js/adapter": "1.1.7",
|
|
43
|
+
"@zhin.js/command": "1.0.9",
|
|
44
|
+
"@zhin.js/core": "1.5.4",
|
|
45
|
+
"@zhin.js/host-http": "1.0.8",
|
|
46
|
+
"@zhin.js/im-contract": "1.0.3",
|
|
47
|
+
"@zhin.js/logger": "1.0.76",
|
|
48
|
+
"@zhin.js/plugin-runtime": "1.1.5"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@types/node": "^26.1.2",
|
|
51
52
|
"@types/ws": "^8.18.1",
|
|
52
53
|
"typescript": "^6.0.3",
|
|
53
54
|
"vitest": "^4.1.10",
|
|
54
|
-
"@zhin.js/host-http": "1.0.
|
|
55
|
+
"@zhin.js/host-http": "1.0.8"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
|
-
"@zhin.js/adapter": "1.1.
|
|
58
|
-
"@zhin.js/core": "1.5.
|
|
59
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
60
|
-
"zhin.js": "6.0.
|
|
58
|
+
"@zhin.js/adapter": "1.1.7",
|
|
59
|
+
"@zhin.js/core": "1.5.4",
|
|
60
|
+
"@zhin.js/plugin-runtime": "1.1.5",
|
|
61
|
+
"zhin.js": "6.0.4"
|
|
61
62
|
},
|
|
62
63
|
"peerDependenciesMeta": {
|
|
63
64
|
"zhin.js": {
|
package/schema.json
CHANGED
|
@@ -17,16 +17,48 @@
|
|
|
17
17
|
"default": 10000,
|
|
18
18
|
"description": "WS PING interval in milliseconds"
|
|
19
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
|
+
},
|
|
20
38
|
"endpoints": {
|
|
21
39
|
"type": "array",
|
|
22
|
-
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(
|
|
40
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
23
41
|
"items": {
|
|
24
42
|
"type": "object",
|
|
25
43
|
"additionalProperties": true,
|
|
26
44
|
"properties": {
|
|
27
|
-
"
|
|
28
|
-
"type":
|
|
29
|
-
|
|
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 列表"
|
|
30
62
|
},
|
|
31
63
|
"baseUrl": {
|
|
32
64
|
"type": "string",
|
|
@@ -39,10 +71,14 @@
|
|
|
39
71
|
"path": {
|
|
40
72
|
"type": "string",
|
|
41
73
|
"description": "Webhook POST path (connection: webhook)"
|
|
74
|
+
},
|
|
75
|
+
"id": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Satori bot name"
|
|
42
78
|
}
|
|
43
79
|
},
|
|
44
80
|
"required": [
|
|
45
|
-
"
|
|
81
|
+
"id",
|
|
46
82
|
"baseUrl"
|
|
47
83
|
]
|
|
48
84
|
}
|
package/src/endpoint.ts
CHANGED
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
type EndpointInstance,
|
|
10
10
|
type EndpointLifecycle,
|
|
11
11
|
type EndpointManagement,
|
|
12
|
+
type EndpointSendRequest,
|
|
12
13
|
} from '@zhin.js/adapter';
|
|
13
14
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
14
15
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
15
|
-
import { formatCompact,
|
|
16
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
16
17
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
17
18
|
import {
|
|
18
19
|
SatoriOpcode,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
isSelfMentioned,
|
|
27
28
|
parseMessageRef,
|
|
28
29
|
resolveInboundSender,
|
|
29
|
-
|
|
30
|
+
satoriInboundConversation,
|
|
30
31
|
type ResolvedSatoriWebhookConfig,
|
|
31
32
|
type ResolvedSatoriWsConfig,
|
|
32
33
|
type SatoriApiOptions,
|
|
@@ -42,8 +43,6 @@ import {
|
|
|
42
43
|
type SatoriWsSocket,
|
|
43
44
|
} from './ws.js';
|
|
44
45
|
|
|
45
|
-
const logger = getLogger('satori');
|
|
46
|
-
|
|
47
46
|
export type SatoriApiCaller = typeof callSatoriApi;
|
|
48
47
|
|
|
49
48
|
export interface SatoriWsEndpointOptions {
|
|
@@ -55,6 +54,8 @@ export interface SatoriWsEndpointOptions {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
export class SatoriWsEndpoint implements EndpointInstance {
|
|
57
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
58
|
+
|
|
58
59
|
readonly #options: SatoriWsEndpointOptions;
|
|
59
60
|
readonly #lifecycle: EndpointLifecycle;
|
|
60
61
|
#ws: SatoriWsSocket | null = null;
|
|
@@ -66,10 +67,11 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
66
67
|
);
|
|
67
68
|
|
|
68
69
|
constructor(options: SatoriWsEndpointOptions) {
|
|
70
|
+
this.#logger = getAdapterLogger('satori', options.config.id);
|
|
69
71
|
this.#options = options;
|
|
70
72
|
const { config } = options;
|
|
71
73
|
this.#lifecycle = createEndpointLifecycle({
|
|
72
|
-
name: config.
|
|
74
|
+
name: config.id,
|
|
73
75
|
reconnect: {
|
|
74
76
|
initialIntervalMs: 5_000,
|
|
75
77
|
// 固定间隔(multiplier 1、无抖动),对齐旧 5s 固定重连语义
|
|
@@ -117,20 +119,20 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
117
119
|
this.#ws = null;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
async send({
|
|
122
|
+
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
121
123
|
const content = formatSatoriOutbound(payload);
|
|
122
124
|
const result = await this.#api('message', 'create', {
|
|
123
|
-
channel_id:
|
|
125
|
+
channel_id: conversation.id,
|
|
124
126
|
content,
|
|
125
127
|
});
|
|
126
128
|
const msgId = extractCreatedMessageId(result);
|
|
127
|
-
logger.debug(formatCompact({
|
|
129
|
+
this.#logger.debug(formatCompact({
|
|
128
130
|
op: 'satori_send',
|
|
129
|
-
endpoint: this.#options.config.
|
|
130
|
-
|
|
131
|
+
endpoint: this.#options.config.id,
|
|
132
|
+
channel: conversation.id,
|
|
131
133
|
messageId: msgId || undefined,
|
|
132
134
|
}));
|
|
133
|
-
return msgId ? formatMessageId(
|
|
135
|
+
return msgId ? formatMessageId(conversation.id, msgId) : '';
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
/** Test / internal: admit a gateway event when the endpoint is open. */
|
|
@@ -138,30 +140,28 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
138
140
|
if (!this.#open) return;
|
|
139
141
|
if (body.login && !this.#login) this.#login = body.login;
|
|
140
142
|
if (!isMessageEvent(body)) return;
|
|
141
|
-
const
|
|
143
|
+
const conversation = satoriInboundConversation(String(this.#options.id), body);
|
|
142
144
|
const content = formatInboundContent(body);
|
|
143
145
|
const sender = resolveInboundSender(body);
|
|
144
|
-
const messageId = formatMessageId(target, body.message.id);
|
|
145
146
|
const selfId = this.#login?.user?.id ?? body.login?.user?.id;
|
|
146
147
|
const mentioned = isSelfMentioned(body, selfId);
|
|
147
148
|
void this.#options.gateway.receive({
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
conversation,
|
|
150
|
+
message: { conversation, id: body.message.id },
|
|
150
151
|
content,
|
|
151
152
|
sender,
|
|
152
|
-
|
|
153
|
+
endpointId: this.#options.config.id,
|
|
154
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
153
155
|
metadata: Object.freeze({
|
|
154
156
|
type: body.type,
|
|
155
157
|
channelType: isPrivateChannelType(body) ? 'private' : 'group',
|
|
156
158
|
sn: body.sn,
|
|
157
159
|
platform: this.#login?.platform,
|
|
158
|
-
endpoint: this.#options.config.name,
|
|
159
|
-
...(mentioned ? { mentioned: true } : {}),
|
|
160
160
|
}),
|
|
161
161
|
}).catch((err) => {
|
|
162
|
-
logger.warn(formatCompact({
|
|
162
|
+
this.#logger.warn(formatCompact({
|
|
163
163
|
op: 'satori_gateway_receive_failed',
|
|
164
|
-
|
|
164
|
+
channel: conversation.id,
|
|
165
165
|
error: err instanceof Error ? err.message : String(err),
|
|
166
166
|
}));
|
|
167
167
|
});
|
|
@@ -199,7 +199,7 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
199
199
|
});
|
|
200
200
|
|
|
201
201
|
ws.on('open', () => {
|
|
202
|
-
logger.debug(formatCompact({
|
|
202
|
+
this.#logger.debug(formatCompact({ mode: 'ws' }));
|
|
203
203
|
this.#sendSignal(SatoriOpcode.IDENTIFY, {
|
|
204
204
|
token: config.token,
|
|
205
205
|
sn: this.#lastSn,
|
|
@@ -221,9 +221,9 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
221
221
|
const signal = JSON.parse(raw) as SatoriSignal;
|
|
222
222
|
this.#handleSignal(signal);
|
|
223
223
|
} catch (error) {
|
|
224
|
-
logger.warn(formatCompact({
|
|
224
|
+
this.#logger.warn(formatCompact({
|
|
225
225
|
op: 'ws_parse_error',
|
|
226
|
-
endpoint: config.
|
|
226
|
+
endpoint: config.id,
|
|
227
227
|
error: error instanceof Error ? error.message : String(error),
|
|
228
228
|
}));
|
|
229
229
|
}
|
|
@@ -246,9 +246,9 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
246
246
|
});
|
|
247
247
|
|
|
248
248
|
ws.on('error', (error) => {
|
|
249
|
-
logger.warn(formatCompact({
|
|
249
|
+
this.#logger.warn(formatCompact({
|
|
250
250
|
op: 'ws_error',
|
|
251
|
-
endpoint: config.
|
|
251
|
+
endpoint: config.id,
|
|
252
252
|
ok: false,
|
|
253
253
|
error: error instanceof Error ? error.message : String(error),
|
|
254
254
|
}));
|
|
@@ -270,7 +270,7 @@ export class SatoriWsEndpoint implements EndpointInstance {
|
|
|
270
270
|
const logins = signal.body.logins as SatoriLogin[];
|
|
271
271
|
this.#login = logins[0];
|
|
272
272
|
if (!this.#login?.platform || !this.#login?.user?.id) {
|
|
273
|
-
logger.warn(formatCompact({ op: 'ready', ok: false, error: 'missing platform/user' }));
|
|
273
|
+
this.#logger.warn(formatCompact({ op: 'ready', ok: false, error: 'missing platform/user' }));
|
|
274
274
|
}
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
@@ -313,6 +313,8 @@ export interface SatoriWebhookEndpointOptions {
|
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
export class SatoriWebhookEndpoint implements EndpointInstance {
|
|
316
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
317
|
+
|
|
316
318
|
readonly #options: SatoriWebhookEndpointOptions;
|
|
317
319
|
#login: SatoriLogin | undefined;
|
|
318
320
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
@@ -323,6 +325,7 @@ export class SatoriWebhookEndpoint implements EndpointInstance {
|
|
|
323
325
|
);
|
|
324
326
|
|
|
325
327
|
constructor(options: SatoriWebhookEndpointOptions) {
|
|
328
|
+
this.#logger = getAdapterLogger('satori', options.config.id);
|
|
326
329
|
this.#options = options;
|
|
327
330
|
}
|
|
328
331
|
|
|
@@ -340,17 +343,17 @@ export class SatoriWebhookEndpoint implements EndpointInstance {
|
|
|
340
343
|
this.#started = true;
|
|
341
344
|
if (!this.#options.config.token) {
|
|
342
345
|
// 未配 token 时 webhook 无鉴权:任何人知道 path 即可注入假事件。
|
|
343
|
-
logger.warn(formatCompact({
|
|
346
|
+
this.#logger.warn(formatCompact({
|
|
344
347
|
op: 'webhook_no_token',
|
|
345
|
-
endpoint: this.#options.config.
|
|
348
|
+
endpoint: this.#options.config.id,
|
|
346
349
|
path: this.#options.config.path,
|
|
347
350
|
hint: 'set token to authenticate Satori webhook callbacks',
|
|
348
351
|
}));
|
|
349
352
|
}
|
|
350
353
|
this.#routeReleases.push(...registerSatoriWebhookRoutes(this.#options.http, this));
|
|
351
|
-
logger.info(formatCompact({
|
|
354
|
+
this.#logger.info(formatCompact({
|
|
352
355
|
op: 'listen',
|
|
353
|
-
endpoint: this.#options.config.
|
|
356
|
+
endpoint: this.#options.config.id,
|
|
354
357
|
mode: 'webhook',
|
|
355
358
|
path: this.#options.config.path,
|
|
356
359
|
}));
|
|
@@ -368,56 +371,53 @@ export class SatoriWebhookEndpoint implements EndpointInstance {
|
|
|
368
371
|
this.#open = false;
|
|
369
372
|
for (const release of this.#routeReleases.splice(0)) release();
|
|
370
373
|
this.#started = false;
|
|
371
|
-
logger.debug(formatCompact({
|
|
372
|
-
|
|
373
|
-
endpoint: this.#options.config.name,
|
|
374
|
+
this.#logger.debug(formatCompact({
|
|
375
|
+
op: 'disconnect',
|
|
374
376
|
}));
|
|
375
377
|
}
|
|
376
378
|
|
|
377
|
-
async send({
|
|
379
|
+
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
378
380
|
const content = formatSatoriOutbound(payload);
|
|
379
381
|
const result = await this.#api('message', 'create', {
|
|
380
|
-
channel_id:
|
|
382
|
+
channel_id: conversation.id,
|
|
381
383
|
content,
|
|
382
384
|
});
|
|
383
385
|
const msgId = extractCreatedMessageId(result);
|
|
384
|
-
logger.debug(formatCompact({
|
|
386
|
+
this.#logger.debug(formatCompact({
|
|
385
387
|
op: 'satori_send',
|
|
386
|
-
endpoint: this.#options.config.
|
|
387
|
-
|
|
388
|
+
endpoint: this.#options.config.id,
|
|
389
|
+
channel: conversation.id,
|
|
388
390
|
messageId: msgId || undefined,
|
|
389
391
|
}));
|
|
390
|
-
return msgId ? formatMessageId(
|
|
392
|
+
return msgId ? formatMessageId(conversation.id, msgId) : '';
|
|
391
393
|
}
|
|
392
394
|
|
|
393
395
|
admit(body: SatoriEventBody): void {
|
|
394
396
|
if (!this.#open) return;
|
|
395
397
|
if (body.login && !this.#login) this.#login = body.login;
|
|
396
398
|
if (!isMessageEvent(body)) return;
|
|
397
|
-
const
|
|
399
|
+
const conversation = satoriInboundConversation(String(this.#options.id), body);
|
|
398
400
|
const content = formatInboundContent(body);
|
|
399
401
|
const sender = resolveInboundSender(body);
|
|
400
|
-
const messageId = formatMessageId(target, body.message.id);
|
|
401
402
|
const selfId = this.#login?.user?.id ?? body.login?.user?.id;
|
|
402
403
|
const mentioned = isSelfMentioned(body, selfId);
|
|
403
404
|
void this.#options.gateway.receive({
|
|
404
|
-
|
|
405
|
-
|
|
405
|
+
conversation,
|
|
406
|
+
message: { conversation, id: body.message.id },
|
|
406
407
|
content,
|
|
407
408
|
sender,
|
|
408
|
-
|
|
409
|
+
endpointId: this.#options.config.id,
|
|
410
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
409
411
|
metadata: Object.freeze({
|
|
410
412
|
type: body.type,
|
|
411
413
|
channelType: isPrivateChannelType(body) ? 'private' : 'group',
|
|
412
414
|
sn: body.sn,
|
|
413
415
|
platform: this.#login?.platform,
|
|
414
|
-
endpoint: this.#options.config.name,
|
|
415
|
-
...(mentioned ? { mentioned: true } : {}),
|
|
416
416
|
}),
|
|
417
417
|
}).catch((err) => {
|
|
418
|
-
logger.warn(formatCompact({
|
|
418
|
+
this.#logger.warn(formatCompact({
|
|
419
419
|
op: 'satori_gateway_receive_failed',
|
|
420
|
-
|
|
420
|
+
channel: conversation.id,
|
|
421
421
|
error: err instanceof Error ? err.message : String(err),
|
|
422
422
|
}));
|
|
423
423
|
});
|
package/src/index.ts
CHANGED
|
@@ -4,14 +4,12 @@ export {
|
|
|
4
4
|
callSatoriApi,
|
|
5
5
|
extractCreatedMessageId,
|
|
6
6
|
formatInboundContent,
|
|
7
|
-
formatMessageId,
|
|
8
7
|
formatSatoriOutbound,
|
|
9
8
|
isMessageEvent,
|
|
10
9
|
isPrivateChannel,
|
|
11
|
-
parseMessageRef,
|
|
12
10
|
resolveInboundSender,
|
|
13
|
-
resolveInboundTarget,
|
|
14
11
|
resolveSatoriConfig,
|
|
12
|
+
satoriInboundConversation,
|
|
15
13
|
type ResolvedSatoriWebhookConfig,
|
|
16
14
|
type ResolvedSatoriWsConfig,
|
|
17
15
|
type SatoriAdapterConfig,
|
package/src/protocol.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { pickCredential } from '@zhin.js/adapter';
|
|
8
8
|
import { isMediaRef } from '@zhin.js/core';
|
|
9
|
+
import type { ConversationKind, ConversationRef } from '@zhin.js/im-contract';
|
|
9
10
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
10
11
|
|
|
11
12
|
const logger = getLogger('satori');
|
|
@@ -83,7 +84,7 @@ export interface SatoriWireSegment {
|
|
|
83
84
|
|
|
84
85
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
85
86
|
export interface SatoriAdapterConfig {
|
|
86
|
-
readonly
|
|
87
|
+
readonly id?: string;
|
|
87
88
|
readonly connection?: 'ws' | 'webhook';
|
|
88
89
|
readonly baseUrl?: string;
|
|
89
90
|
readonly token?: string;
|
|
@@ -101,7 +102,7 @@ export interface SatoriAdapterConfig {
|
|
|
101
102
|
export interface ResolvedSatoriWsConfig {
|
|
102
103
|
readonly context: 'satori';
|
|
103
104
|
readonly connection: 'ws';
|
|
104
|
-
readonly
|
|
105
|
+
readonly id: string;
|
|
105
106
|
readonly baseUrl: string;
|
|
106
107
|
readonly token?: string;
|
|
107
108
|
readonly heartbeat_interval: number;
|
|
@@ -110,7 +111,7 @@ export interface ResolvedSatoriWsConfig {
|
|
|
110
111
|
export interface ResolvedSatoriWebhookConfig {
|
|
111
112
|
readonly context: 'satori';
|
|
112
113
|
readonly connection: 'webhook';
|
|
113
|
-
readonly
|
|
114
|
+
readonly id: string;
|
|
114
115
|
readonly baseUrl: string;
|
|
115
116
|
readonly token?: string;
|
|
116
117
|
readonly path: string;
|
|
@@ -127,8 +128,8 @@ export function resolveSatoriConfig(config: SatoriAdapterConfig = {}): ResolvedS
|
|
|
127
128
|
'Satori adapter requires baseUrl (plugins.<key>.baseUrl or endpoints with context: satori)',
|
|
128
129
|
);
|
|
129
130
|
}
|
|
130
|
-
const
|
|
131
|
-
|| (typeof entry?.
|
|
131
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
132
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
132
133
|
|| process.env.SATORI_BOT_NAME
|
|
133
134
|
|| 'satori-bot';
|
|
134
135
|
const token = (typeof config.token === 'string' && config.token)
|
|
@@ -144,7 +145,7 @@ export function resolveSatoriConfig(config: SatoriAdapterConfig = {}): ResolvedS
|
|
|
144
145
|
return {
|
|
145
146
|
context: 'satori',
|
|
146
147
|
connection: 'webhook',
|
|
147
|
-
|
|
148
|
+
id,
|
|
148
149
|
baseUrl,
|
|
149
150
|
token,
|
|
150
151
|
path,
|
|
@@ -157,7 +158,7 @@ export function resolveSatoriConfig(config: SatoriAdapterConfig = {}): ResolvedS
|
|
|
157
158
|
return {
|
|
158
159
|
context: 'satori',
|
|
159
160
|
connection: 'ws',
|
|
160
|
-
|
|
161
|
+
id,
|
|
161
162
|
baseUrl,
|
|
162
163
|
token,
|
|
163
164
|
heartbeat_interval: heartbeat,
|
|
@@ -227,14 +228,34 @@ export function formatInboundContent(body: SatoriEventBody & { message: SatoriMe
|
|
|
227
228
|
return typeof content === 'string' ? content : String(content);
|
|
228
229
|
}
|
|
229
230
|
|
|
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 {
|
|
231
240
|
const channel = body.channel ?? body.message.channel;
|
|
232
|
-
|
|
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
|
+
};
|
|
233
250
|
}
|
|
234
251
|
|
|
235
|
-
export function resolveInboundSender(
|
|
252
|
+
export function resolveInboundSender(
|
|
253
|
+
body: SatoriEventBody & { message: SatoriMessage },
|
|
254
|
+
): { id: string; name?: string } {
|
|
236
255
|
const user = body.user ?? body.message.user ?? body.message.member?.user;
|
|
237
|
-
|
|
256
|
+
const name = user?.name ?? body.message.member?.nick;
|
|
257
|
+
const id = user?.id ?? name ?? '';
|
|
258
|
+
return name && name !== id ? { id, name } : { id };
|
|
238
259
|
}
|
|
239
260
|
|
|
240
261
|
/**
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|