@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 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({ adapter, target: channelId, content: text, sender, id, metadata })`
31
- 出站:`send({ target: channelId, payload })` → Satori `message.create`(payload 已由 gateway/core 渲染;无 segment-mapper)
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
 
@@ -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.name, {
24
- name: config.name,
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') {
@@ -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.name, {
35
- name: config.name,
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({ target, payload }: {
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({ target, payload }: {
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, getLogger } from '@zhin.js/logger';
6
- import { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatMessageId, formatSatoriOutbound, isMessageEvent, isSelfMentioned, parseMessageRef, resolveInboundSender, resolveInboundTarget, } from './protocol.js';
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.name,
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({ target, payload }) {
68
+ async send({ conversation, payload }) {
68
69
  const content = formatSatoriOutbound(payload);
69
70
  const result = await this.#api('message', 'create', {
70
- channel_id: target,
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.name,
77
- target,
77
+ endpoint: this.#options.config.id,
78
+ channel: conversation.id,
78
79
  messageId: msgId || undefined,
79
80
  }));
80
- return msgId ? formatMessageId(target, msgId) : '';
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 target = resolveInboundTarget(body);
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
- adapter: this.#options.id,
98
- target,
97
+ conversation,
98
+ message: { conversation, id: body.message.id },
99
99
  content,
100
100
  sender,
101
- id: messageId,
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
- target,
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({ endpoint: config.name, mode: 'ws' }));
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.name,
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.name,
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.name,
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.name,
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({ target, payload }) {
298
+ async send({ conversation, payload }) {
299
299
  const content = formatSatoriOutbound(payload);
300
300
  const result = await this.#api('message', 'create', {
301
- channel_id: target,
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.name,
308
- target,
307
+ endpoint: this.#options.config.id,
308
+ channel: conversation.id,
309
309
  messageId: msgId || undefined,
310
310
  }));
311
- return msgId ? formatMessageId(target, msgId) : '';
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 target = resolveInboundTarget(body);
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
- adapter: this.#options.id,
328
- target,
326
+ conversation,
327
+ message: { conversation, id: body.message.id },
329
328
  content,
330
329
  sender,
331
- id: messageId,
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
- target,
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, formatMessageId, formatSatoriOutbound, isMessageEvent, isPrivateChannel, parseMessageRef, resolveInboundSender, resolveInboundTarget, resolveSatoriConfig, 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';
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, formatMessageId, formatSatoriOutbound, isMessageEvent, isPrivateChannel, parseMessageRef, resolveInboundSender, resolveInboundTarget, resolveSatoriConfig, } from './protocol.js';
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 name?: string;
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 name: string;
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 name: string;
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
- export declare function resolveInboundTarget(body: SatoriEventBody & {
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
- }): string;
134
+ }): ConversationRef;
130
135
  export declare function resolveInboundSender(body: SatoriEventBody & {
131
136
  message: SatoriMessage;
132
- }): string;
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 name = (typeof config.name === 'string' && config.name)
27
- || (typeof entry?.name === 'string' && entry.name)
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
- name,
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
- name,
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
- export function resolveInboundTarget(body) {
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
- return channel?.id ?? '';
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
- return user?.name ?? body.message.member?.nick ?? user?.id ?? '';
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.1",
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.4",
43
- "@zhin.js/command": "1.0.6",
44
- "@zhin.js/core": "1.5.1",
45
- "@zhin.js/host-http": "1.0.5",
46
- "@zhin.js/logger": "1.0.75",
47
- "@zhin.js/plugin-runtime": "1.1.2"
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.5"
55
+ "@zhin.js/host-http": "1.0.8"
55
56
  },
56
57
  "peerDependencies": {
57
- "@zhin.js/adapter": "1.1.4",
58
- "@zhin.js/core": "1.5.1",
59
- "@zhin.js/plugin-runtime": "1.1.2",
60
- "zhin.js": "6.0.1"
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。每项与顶层字段同构(name 必填,其余覆盖顶层)",
40
+ "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
23
41
  "items": {
24
42
  "type": "object",
25
43
  "additionalProperties": true,
26
44
  "properties": {
27
- "name": {
28
- "type": "string",
29
- "description": "Satori bot name"
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
- "name",
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, getLogger } from '@zhin.js/logger';
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
- resolveInboundTarget,
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.name,
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({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
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: target,
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.name,
130
- target,
131
+ endpoint: this.#options.config.id,
132
+ channel: conversation.id,
131
133
  messageId: msgId || undefined,
132
134
  }));
133
- return msgId ? formatMessageId(target, msgId) : '';
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 target = resolveInboundTarget(body);
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
- adapter: this.#options.id,
149
- target,
149
+ conversation,
150
+ message: { conversation, id: body.message.id },
150
151
  content,
151
152
  sender,
152
- id: messageId,
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
- target,
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({ endpoint: config.name, mode: 'ws' }));
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.name,
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.name,
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.name,
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.name,
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
- op: 'disconnect',
373
- endpoint: this.#options.config.name,
374
+ this.#logger.debug(formatCompact({
375
+ op: 'disconnect',
374
376
  }));
375
377
  }
376
378
 
377
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
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: target,
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.name,
387
- target,
388
+ endpoint: this.#options.config.id,
389
+ channel: conversation.id,
388
390
  messageId: msgId || undefined,
389
391
  }));
390
- return msgId ? formatMessageId(target, msgId) : '';
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 target = resolveInboundTarget(body);
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
- adapter: this.#options.id,
405
- target,
405
+ conversation,
406
+ message: { conversation, id: body.message.id },
406
407
  content,
407
408
  sender,
408
- id: messageId,
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
- target,
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 name?: string;
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 name: string;
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 name: string;
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 name = (typeof config.name === 'string' && config.name)
131
- || (typeof entry?.name === 'string' && entry.name)
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
- name,
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
- name,
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
- export function resolveInboundTarget(body: SatoriEventBody & { message: SatoriMessage }): string {
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
- return channel?.id ?? '';
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(body: SatoriEventBody & { message: SatoriMessage }): string {
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
- return user?.name ?? body.message.member?.nick ?? user?.id ?? '';
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
  /**