@zhin.js/adapter-milky 6.0.2 → 6.0.5

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/src/protocol.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { isMediaRef } from '@zhin.js/core';
8
8
  import type { Segment } from '@zhin.js/core/runtime';
9
+ import type { ConversationRef } from '@zhin.js/im-contract';
9
10
  import { formatCompact, getLogger } from '@zhin.js/logger';
10
11
 
11
12
  const logger = getLogger('milky');
@@ -14,7 +15,7 @@ const logger = getLogger('milky');
14
15
  export interface MilkyLegacyEndpointRow {
15
16
  readonly context?: string;
16
17
  readonly connection?: 'ws' | 'sse' | 'webhook' | 'wss';
17
- readonly name?: string;
18
+ readonly id?: string;
18
19
  readonly baseUrl?: string;
19
20
  readonly access_token?: string;
20
21
  readonly path?: string;
@@ -25,7 +26,7 @@ export interface MilkyLegacyEndpointRow {
25
26
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
26
27
  export interface MilkyAdapterConfig {
27
28
  readonly connection?: 'ws' | 'sse' | 'webhook' | 'wss';
28
- readonly name?: string;
29
+ readonly id?: string;
29
30
  readonly baseUrl?: string;
30
31
  readonly access_token?: string;
31
32
  readonly path?: string;
@@ -37,7 +38,7 @@ export interface MilkyAdapterConfig {
37
38
 
38
39
  export interface MilkyConfigBase {
39
40
  readonly context: 'milky';
40
- readonly name: string;
41
+ readonly id: string;
41
42
  readonly baseUrl: string;
42
43
  readonly access_token?: string;
43
44
  }
@@ -116,11 +117,6 @@ export interface MilkyOutgoingSegment {
116
117
  data: Record<string, unknown>;
117
118
  }
118
119
 
119
- export interface ParsedSendTarget {
120
- readonly message_type: 'private' | 'group';
121
- readonly id: string;
122
- }
123
-
124
120
  export interface MilkyApiClientOptions {
125
121
  readonly baseUrl: string;
126
122
  readonly access_token?: string;
@@ -134,8 +130,8 @@ function normalizeConnection(connection: string | undefined): 'ws' | 'sse' | 'we
134
130
  export function resolveMilkyConfig(config: MilkyAdapterConfig = {}): ResolvedMilkyConfig {
135
131
  const entry = config.endpoints?.find((item) => item.context === 'milky');
136
132
  const connection = normalizeConnection(config.connection ?? entry?.connection);
137
- const name = (typeof config.name === 'string' && config.name)
138
- || (typeof entry?.name === 'string' && entry.name)
133
+ const id = (typeof config.id === 'string' && config.id)
134
+ || (typeof entry?.id === 'string' && entry.id)
139
135
  || process.env.MILKY_BOT_NAME
140
136
  || 'milky-bot';
141
137
  const baseUrl = config.baseUrl ?? entry?.baseUrl;
@@ -147,7 +143,7 @@ export function resolveMilkyConfig(config: MilkyAdapterConfig = {}): ResolvedMil
147
143
  const access_token = config.access_token ?? entry?.access_token;
148
144
  const base = {
149
145
  context: 'milky' as const,
150
- name,
146
+ id,
151
147
  baseUrl,
152
148
  access_token,
153
149
  };
@@ -261,10 +257,22 @@ export function isMessageReceiveEvent(
261
257
  return parseMessageReceiveData(event) != null;
262
258
  }
263
259
 
264
- /** Gateway reply target:`private:uid` / `group:gid` */
265
- export function formatInboundTarget(data: MilkyIncomingMessage): string {
266
- const isGroup = data.message_scene === 'group';
267
- return `${isGroup ? 'group' : 'private'}:${data.peer_id}`;
260
+ /**
261
+ * 入站归一化 ConversationRef:`friend`(私聊)→ `private`;`group` → `group`;
262
+ * `temp`(群临时会话)→ 群容器内的 `private` 会话(parent = 来源群)。
263
+ */
264
+ export function milkyInboundConversation(
265
+ endpointKey: string,
266
+ data: MilkyIncomingMessage,
267
+ ): ConversationRef {
268
+ return {
269
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
270
+ kind: data.message_scene === 'group' ? 'group' : 'private',
271
+ id: String(data.peer_id),
272
+ ...(data.message_scene === 'temp' && data.group
273
+ ? { parent: { kind: 'group' as const, id: String(data.group.group_id) } }
274
+ : {}),
275
+ };
268
276
  }
269
277
 
270
278
  export function formatInboundContent(data: MilkyIncomingMessage): string {
@@ -427,18 +435,6 @@ export function formatInboundMessageId(data: MilkyIncomingMessage): string {
427
435
  return `${data.message_scene}:${data.peer_id}:${data.message_seq}`;
428
436
  }
429
437
 
430
- export function parseSendTarget(target: string): ParsedSendTarget {
431
- const sep = target.indexOf(':');
432
- if (sep <= 0) {
433
- return { message_type: 'private', id: target };
434
- }
435
- const head = target.slice(0, sep);
436
- const rest = target.slice(sep + 1);
437
- if (head === 'group') return { message_type: 'group', id: rest };
438
- if (head === 'private') return { message_type: 'private', id: rest };
439
- return { message_type: 'private', id: target };
440
- }
441
-
442
438
  /**
443
439
  * 解析 image/audio/video 段的投递 uri(canonical MediaRef 唯一来源):
444
440
  * - kind=url → http(s):// 直发;
@@ -570,15 +566,14 @@ export function formatOutboundSegments(payload: unknown): MilkyOutgoingSegment[]
570
566
  }
571
567
 
572
568
  export function buildSendAction(
573
- target: string,
569
+ conversation: ConversationRef,
574
570
  message: MilkyOutgoingSegment[],
575
571
  ): { action: string; params: Record<string, unknown> } {
576
- const parsed = parseSendTarget(target);
577
- if (parsed.message_type === 'group') {
572
+ if (conversation.kind === 'group') {
578
573
  return {
579
574
  action: 'send_group_message',
580
575
  params: {
581
- group_id: parseInt(parsed.id, 10),
576
+ group_id: parseInt(conversation.id, 10),
582
577
  message,
583
578
  },
584
579
  };
@@ -586,21 +581,23 @@ export function buildSendAction(
586
581
  return {
587
582
  action: 'send_private_message',
588
583
  params: {
589
- user_id: parseInt(parsed.id, 10),
584
+ user_id: parseInt(conversation.id, 10),
590
585
  message,
591
586
  },
592
587
  };
593
588
  }
594
589
 
595
- /** message_seq result → gateway message id */
590
+ /**
591
+ * message_seq result → gateway message id(`scene:peer:seq` 复合格式,
592
+ * 与 formatInboundMessageId 同源,仅供 recall 链在本端点边界内解析)。
593
+ */
596
594
  export function formatOutboundMessageId(
597
- target: string,
595
+ conversation: ConversationRef,
598
596
  messageSeq: number | undefined,
599
597
  ): string {
600
598
  if (messageSeq == null) return '';
601
- const parsed = parseSendTarget(target);
602
- const scene = parsed.message_type === 'group' ? 'group' : 'friend';
603
- return `${scene}:${parsed.id}:${messageSeq}`;
599
+ const scene = conversation.kind === 'group' ? 'group' : 'friend';
600
+ return `${scene}:${conversation.id}:${messageSeq}`;
604
601
  }
605
602
 
606
603
  export function parseMilkyMessageId(
@@ -7,9 +7,10 @@ import {
7
7
  type EndpointInstance,
8
8
  type EndpointLifecycle,
9
9
  type EndpointManagement,
10
+ type EndpointSendRequest,
10
11
  } from '@zhin.js/adapter';
11
12
  import type { MessageGateway } from '@zhin.js/core/runtime';
12
- import { formatCompact, getLogger } from '@zhin.js/logger';
13
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
13
14
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
14
15
  import { createMilkyEndpointManagement } from './endpoint-management.js';
15
16
  import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
@@ -21,10 +22,10 @@ import {
21
22
  formatInboundContent,
22
23
  formatInboundMessageId,
23
24
  formatInboundSegments,
24
- formatInboundTarget,
25
25
  formatOutboundMessageId,
26
26
  formatOutboundSegments,
27
27
  isMentioned,
28
+ milkyInboundConversation,
28
29
  parseMessageReceiveData,
29
30
  parseMilkyMessageId,
30
31
  senderNickname,
@@ -34,8 +35,6 @@ import {
34
35
  } from './protocol.js';
35
36
  import { openSseStream, type SseClientHandle } from './sse-client.js';
36
37
 
37
- const logger = getLogger('milky');
38
-
39
38
  export type CreateMilkySseStream = (options: {
40
39
  readonly url: string;
41
40
  readonly headers: Record<string, string>;
@@ -53,6 +52,8 @@ export interface MilkySseEndpointOptions {
53
52
  }
54
53
 
55
54
  export class MilkySseEndpoint implements EndpointInstance {
55
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
56
+
56
57
  readonly #options: MilkySseEndpointOptions;
57
58
  readonly #callApi: typeof callApi;
58
59
  readonly management: EndpointManagement = createMilkyEndpointManagement(this);
@@ -62,10 +63,11 @@ export class MilkySseEndpoint implements EndpointInstance {
62
63
  #unregisterAgent?: () => void;
63
64
 
64
65
  constructor(options: MilkySseEndpointOptions) {
66
+ this.#logger = getAdapterLogger('milky', options.config.id);
65
67
  this.#options = options;
66
68
  this.#callApi = options.callApi ?? callApi;
67
69
  this.#lifecycle = createEndpointLifecycle({
68
- name: options.config.name,
70
+ name: options.config.id,
69
71
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
70
72
  reconnect: {
71
73
  initialIntervalMs: options.config.reconnect_interval,
@@ -78,7 +80,7 @@ export class MilkySseEndpoint implements EndpointInstance {
78
80
 
79
81
  async start(): Promise<void> {
80
82
  if (this.#lifecycle.started) return;
81
- this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.name, this);
83
+ this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
82
84
  try {
83
85
  await this.#lifecycle.start((handle) => this.#connect(handle));
84
86
  } catch (err) {
@@ -106,15 +108,15 @@ export class MilkySseEndpoint implements EndpointInstance {
106
108
  this.#stream = undefined;
107
109
  }
108
110
 
109
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
111
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
110
112
  const message = formatOutboundSegments(payload);
111
- const { action, params } = buildSendAction(target, message);
113
+ const { action, params } = buildSendAction(conversation, message);
112
114
  const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
113
- const messageId = formatOutboundMessageId(target, data?.message_seq);
114
- logger.debug(formatCompact({
115
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
116
+ this.#logger.debug(formatCompact({
115
117
  op: 'milky_send',
116
- endpoint: this.#options.config.name,
117
- target,
118
+ endpoint: this.#options.config.id,
119
+ target: `${conversation.kind}:${conversation.id}`,
118
120
  messageId,
119
121
  mode: 'sse',
120
122
  }));
@@ -218,33 +220,32 @@ export class MilkySseEndpoint implements EndpointInstance {
218
220
  }
219
221
 
220
222
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
221
- const target = formatInboundTarget(data);
223
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
224
+ const target = `${conversation.kind}:${conversation.id}`;
222
225
  const content = formatInboundContent(data);
223
226
  const segments = formatInboundSegments(data);
224
227
  const audioUrl = extractInboundAudioUrl(data);
225
228
  const nickname = senderNickname(data);
226
229
  const mentioned = isMentioned(data, event.self_id);
227
230
  void this.#options.gateway.receive({
228
- adapter: this.#options.id,
229
- target,
231
+ conversation,
232
+ message: { conversation, id: formatInboundMessageId(data) },
230
233
  content,
231
234
  segments,
232
- sender: String(data.sender_id),
233
- id: formatInboundMessageId(data),
235
+ sender: { id: String(data.sender_id), name: nickname },
236
+ endpointId: this.#options.config.id,
237
+ ...(mentioned ? { mentioned: true } : {}),
234
238
  metadata: Object.freeze({
235
239
  message_scene: data.message_scene,
236
240
  peer_id: String(data.peer_id),
237
241
  sender_id: String(data.sender_id),
238
242
  message_seq: data.message_seq,
239
- endpoint: this.#options.config.name,
240
243
  time: data.time ?? event.time,
241
244
  self_id: event.self_id != null ? String(event.self_id) : undefined,
242
245
  ...(nickname ? { nickname } : {}),
243
- ...(mentioned ? { mentioned: true } : {}),
244
- ...(audioUrl ? { audio_url: audioUrl } : {}),
245
246
  }),
246
247
  }).catch((err) => {
247
- logger.warn(formatCompact({
248
+ this.#logger.warn(formatCompact({
248
249
  op: 'milky_gateway_receive_failed',
249
250
  target,
250
251
  error: err instanceof Error ? err.message : String(err),
@@ -264,8 +265,8 @@ export class MilkySseEndpoint implements EndpointInstance {
264
265
  onOpen: () => {
265
266
  if (settled) return;
266
267
  settled = true;
267
- logger.debug(formatCompact({
268
- endpoint: this.#options.config.name,
268
+ this.#logger.debug(formatCompact({
269
+ endpoint: this.#options.config.id,
269
270
  mode: 'sse',
270
271
  url: safeUrl,
271
272
  }));
@@ -273,9 +274,9 @@ export class MilkySseEndpoint implements EndpointInstance {
273
274
  },
274
275
  onMessage: (data) => this.#onMessage(data),
275
276
  onError: (error) => {
276
- logger.warn(formatCompact({
277
+ this.#logger.warn(formatCompact({
277
278
  op: 'sse_error',
278
- endpoint: this.#options.config.name,
279
+ endpoint: this.#options.config.id,
279
280
  ok: false,
280
281
  error: error.message,
281
282
  }));
@@ -296,9 +297,8 @@ export class MilkySseEndpoint implements EndpointInstance {
296
297
  void stream.closed.then(() => {
297
298
  // stop-during-connect 竞态由基座静默 settle(主动停止不算失败),此处仅对运行期断开告警
298
299
  if (this.#lifecycle.state !== 'stopped') {
299
- logger.warn(formatCompact({
300
- op: 'disconnect',
301
- endpoint: this.#options.config.name,
300
+ this.#logger.warn(formatCompact({
301
+ op: 'disconnect',
302
302
  mode: 'sse',
303
303
  reconnect_ms: this.#options.config.reconnect_interval,
304
304
  }));
@@ -318,9 +318,9 @@ export class MilkySseEndpoint implements EndpointInstance {
318
318
  const event = JSON.parse(data) as MilkyEvent;
319
319
  this.admit(event);
320
320
  } catch (error) {
321
- logger.warn(formatCompact({
321
+ this.#logger.warn(formatCompact({
322
322
  op: 'milky_parse_failed',
323
- endpoint: this.#options.config.name,
323
+ endpoint: this.#options.config.id,
324
324
  mode: 'sse',
325
325
  error: error instanceof Error ? error.message : String(error),
326
326
  }));
@@ -2,10 +2,14 @@
2
2
  * Milky webhook endpoint — httpHostToken POST inbound + baseUrl HTTP API outbound.
3
3
  */
4
4
  import type { IncomingMessage, ServerResponse } from 'node:http';
5
- import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
5
+ import type {
6
+ EndpointInstance,
7
+ EndpointManagement,
8
+ EndpointSendRequest,
9
+ } from '@zhin.js/adapter';
6
10
  import type { MessageGateway } from '@zhin.js/core/runtime';
7
11
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
8
- import { formatCompact, getLogger } from '@zhin.js/logger';
12
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
9
13
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
10
14
  import { readRequestBody, verifyMilkyAccessToken } from './milky-auth.js';
11
15
  import { createMilkyEndpointManagement } from './endpoint-management.js';
@@ -17,10 +21,10 @@ import {
17
21
  formatInboundContent,
18
22
  formatInboundMessageId,
19
23
  formatInboundSegments,
20
- formatInboundTarget,
21
24
  formatOutboundMessageId,
22
25
  formatOutboundSegments,
23
26
  isMentioned,
27
+ milkyInboundConversation,
24
28
  parseMessageReceiveData,
25
29
  parseMilkyMessageId,
26
30
  senderNickname,
@@ -29,8 +33,6 @@ import {
29
33
  type MilkyWebhookConfig,
30
34
  } from './protocol.js';
31
35
 
32
- const logger = getLogger('milky');
33
-
34
36
  export interface MilkyWebhookEndpointOptions {
35
37
  readonly id: CapabilityId;
36
38
  readonly gateway: MessageGateway;
@@ -40,6 +42,8 @@ export interface MilkyWebhookEndpointOptions {
40
42
  }
41
43
 
42
44
  export class MilkyWebhookEndpoint implements EndpointInstance {
45
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
46
+
43
47
  readonly #options: MilkyWebhookEndpointOptions;
44
48
  readonly #callApi: typeof callApi;
45
49
  readonly management: EndpointManagement = createMilkyEndpointManagement(this);
@@ -49,6 +53,7 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
49
53
  #unregisterAgent?: () => void;
50
54
 
51
55
  constructor(options: MilkyWebhookEndpointOptions) {
56
+ this.#logger = getAdapterLogger('milky', options.config.id);
52
57
  this.#options = options;
53
58
  this.#callApi = options.callApi ?? callApi;
54
59
  }
@@ -56,11 +61,11 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
56
61
  async start(): Promise<void> {
57
62
  if (this.#started) return;
58
63
  this.#started = true;
59
- this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.name, this);
64
+ this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
60
65
  this.#setupRoutes();
61
- logger.info(formatCompact({
66
+ this.#logger.info(formatCompact({
62
67
  op: 'listen',
63
- endpoint: this.#options.config.name,
68
+ endpoint: this.#options.config.id,
64
69
  mode: 'webhook',
65
70
  path: this.#options.config.path,
66
71
  }));
@@ -80,18 +85,18 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
80
85
  this.#unregisterAgent?.();
81
86
  this.#unregisterAgent = undefined;
82
87
  this.#started = false;
83
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
88
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
84
89
  }
85
90
 
86
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
91
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
87
92
  const message = formatOutboundSegments(payload);
88
- const { action, params } = buildSendAction(target, message);
93
+ const { action, params } = buildSendAction(conversation, message);
89
94
  const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
90
- const messageId = formatOutboundMessageId(target, data?.message_seq);
91
- logger.debug(formatCompact({
95
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
96
+ this.#logger.debug(formatCompact({
92
97
  op: 'milky_send',
93
- endpoint: this.#options.config.name,
94
- target,
98
+ endpoint: this.#options.config.id,
99
+ target: `${conversation.kind}:${conversation.id}`,
95
100
  messageId,
96
101
  }));
97
102
  return messageId;
@@ -194,33 +199,32 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
194
199
  }
195
200
 
196
201
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
197
- const target = formatInboundTarget(data);
202
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
203
+ const target = `${conversation.kind}:${conversation.id}`;
198
204
  const content = formatInboundContent(data);
199
205
  const segments = formatInboundSegments(data);
200
206
  const audioUrl = extractInboundAudioUrl(data);
201
207
  const nickname = senderNickname(data);
202
208
  const mentioned = isMentioned(data, event.self_id);
203
209
  void this.#options.gateway.receive({
204
- adapter: this.#options.id,
205
- target,
210
+ conversation,
211
+ message: { conversation, id: formatInboundMessageId(data) },
206
212
  content,
207
213
  segments,
208
- sender: String(data.sender_id),
209
- id: formatInboundMessageId(data),
214
+ sender: { id: String(data.sender_id), name: nickname },
215
+ endpointId: this.#options.config.id,
216
+ ...(mentioned ? { mentioned: true } : {}),
210
217
  metadata: Object.freeze({
211
218
  message_scene: data.message_scene,
212
219
  peer_id: String(data.peer_id),
213
220
  sender_id: String(data.sender_id),
214
221
  message_seq: data.message_seq,
215
- endpoint: this.#options.config.name,
216
222
  time: data.time ?? event.time,
217
223
  self_id: event.self_id != null ? String(event.self_id) : undefined,
218
224
  ...(nickname ? { nickname } : {}),
219
- ...(mentioned ? { mentioned: true } : {}),
220
- ...(audioUrl ? { audio_url: audioUrl } : {}),
221
225
  }),
222
226
  }).catch((err) => {
223
- logger.warn(formatCompact({
227
+ this.#logger.warn(formatCompact({
224
228
  op: 'milky_gateway_receive_failed',
225
229
  target,
226
230
  error: err instanceof Error ? err.message : String(err),
@@ -257,7 +261,7 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
257
261
  response.writeHead(200, { 'Content-Type': 'application/json' });
258
262
  response.end(JSON.stringify({ status: 'ok' }));
259
263
  } catch (error) {
260
- logger.error('Milky webhook error:', error);
264
+ this.#logger.error('Milky webhook error:', error);
261
265
  if (!response.headersSent) {
262
266
  response.writeHead(500, { 'Content-Type': 'application/json' });
263
267
  response.end(JSON.stringify({ message: 'Internal Server Error' }));
@@ -8,9 +8,10 @@ import {
8
8
  type EndpointInstance,
9
9
  type EndpointLifecycle,
10
10
  type EndpointManagement,
11
+ type EndpointSendRequest,
11
12
  } from '@zhin.js/adapter';
12
13
  import type { MessageGateway } from '@zhin.js/core/runtime';
13
- import { formatCompact, getLogger } from '@zhin.js/logger';
14
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
14
15
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
15
16
  import { createMilkyEndpointManagement } from './endpoint-management.js';
16
17
  import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
@@ -22,10 +23,10 @@ import {
22
23
  formatInboundContent,
23
24
  formatInboundMessageId,
24
25
  formatInboundSegments,
25
- formatInboundTarget,
26
26
  formatOutboundMessageId,
27
27
  formatOutboundSegments,
28
28
  isMentioned,
29
+ milkyInboundConversation,
29
30
  parseMessageReceiveData,
30
31
  parseMilkyMessageId,
31
32
  senderNickname,
@@ -35,7 +36,6 @@ import {
35
36
  } from './protocol.js';
36
37
  import type { MilkyWsCreateOptions, MilkyWsSocket } from './ws-types.js';
37
38
 
38
- const logger = getLogger('milky');
39
39
  const WS_OPEN = 1;
40
40
 
41
41
  export interface MilkyWsEndpointOptions {
@@ -50,6 +50,8 @@ export interface MilkyWsEndpointOptions {
50
50
  }
51
51
 
52
52
  export class MilkyWsEndpoint implements EndpointInstance {
53
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
54
+
53
55
  readonly #options: MilkyWsEndpointOptions;
54
56
  readonly #callApi: typeof callApi;
55
57
  readonly management: EndpointManagement = createMilkyEndpointManagement(this);
@@ -59,10 +61,11 @@ export class MilkyWsEndpoint implements EndpointInstance {
59
61
  #unregisterAgent?: () => void;
60
62
 
61
63
  constructor(options: MilkyWsEndpointOptions) {
64
+ this.#logger = getAdapterLogger('milky', options.config.id);
62
65
  this.#options = options;
63
66
  this.#callApi = options.callApi ?? callApi;
64
67
  this.#lifecycle = createEndpointLifecycle({
65
- name: options.config.name,
68
+ name: options.config.id,
66
69
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
67
70
  reconnect: {
68
71
  initialIntervalMs: options.config.reconnect_interval,
@@ -75,7 +78,7 @@ export class MilkyWsEndpoint implements EndpointInstance {
75
78
 
76
79
  async start(): Promise<void> {
77
80
  if (this.#lifecycle.started) return;
78
- this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.name, this);
81
+ this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
79
82
  try {
80
83
  await this.#lifecycle.start((handle) => this.#connect(handle));
81
84
  } catch (err) {
@@ -109,15 +112,15 @@ export class MilkyWsEndpoint implements EndpointInstance {
109
112
  }
110
113
  }
111
114
 
112
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
115
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
113
116
  const message = formatOutboundSegments(payload);
114
- const { action, params } = buildSendAction(target, message);
117
+ const { action, params } = buildSendAction(conversation, message);
115
118
  const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
116
- const messageId = formatOutboundMessageId(target, data?.message_seq);
117
- logger.debug(formatCompact({
119
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
120
+ this.#logger.debug(formatCompact({
118
121
  op: 'milky_send',
119
- endpoint: this.#options.config.name,
120
- target,
122
+ endpoint: this.#options.config.id,
123
+ target: `${conversation.kind}:${conversation.id}`,
121
124
  messageId,
122
125
  }));
123
126
  return messageId;
@@ -222,33 +225,32 @@ export class MilkyWsEndpoint implements EndpointInstance {
222
225
  }
223
226
 
224
227
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
225
- const target = formatInboundTarget(data);
228
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
229
+ const target = `${conversation.kind}:${conversation.id}`;
226
230
  const content = formatInboundContent(data);
227
231
  const segments = formatInboundSegments(data);
228
232
  const audioUrl = extractInboundAudioUrl(data);
229
233
  const nickname = senderNickname(data);
230
234
  const mentioned = isMentioned(data, event.self_id);
231
235
  void this.#options.gateway.receive({
232
- adapter: this.#options.id,
233
- target,
236
+ conversation,
237
+ message: { conversation, id: formatInboundMessageId(data) },
234
238
  content,
235
239
  segments,
236
- sender: String(data.sender_id),
237
- id: formatInboundMessageId(data),
240
+ sender: { id: String(data.sender_id), name: nickname },
241
+ endpointId: this.#options.config.id,
242
+ ...(mentioned ? { mentioned: true } : {}),
238
243
  metadata: Object.freeze({
239
244
  message_scene: data.message_scene,
240
245
  peer_id: String(data.peer_id),
241
246
  sender_id: String(data.sender_id),
242
247
  message_seq: data.message_seq,
243
- endpoint: this.#options.config.name,
244
248
  time: data.time ?? event.time,
245
249
  self_id: event.self_id != null ? String(event.self_id) : undefined,
246
250
  ...(nickname ? { nickname } : {}),
247
- ...(mentioned ? { mentioned: true } : {}),
248
- ...(audioUrl ? { audio_url: audioUrl } : {}),
249
251
  }),
250
252
  }).catch((err) => {
251
- logger.warn(formatCompact({
253
+ this.#logger.warn(formatCompact({
252
254
  op: 'milky_gateway_receive_failed',
253
255
  target,
254
256
  error: err instanceof Error ? err.message : String(err),
@@ -278,14 +280,14 @@ export class MilkyWsEndpoint implements EndpointInstance {
278
280
  if (settled) return;
279
281
  settled = true;
280
282
  if (!this.#options.config.access_token) {
281
- logger.warn(formatCompact({
282
- endpoint: this.#options.config.name,
283
+ this.#logger.warn(formatCompact({
284
+ endpoint: this.#options.config.id,
283
285
  ok: false,
284
286
  error: 'missing access_token',
285
287
  }));
286
288
  }
287
- logger.debug(formatCompact({
288
- endpoint: this.#options.config.name,
289
+ this.#logger.debug(formatCompact({
290
+ endpoint: this.#options.config.id,
289
291
  mode: 'ws',
290
292
  url: safeUrl,
291
293
  }));
@@ -318,9 +320,8 @@ export class MilkyWsEndpoint implements EndpointInstance {
318
320
  : codeNum === 1006
319
321
  ? ' [异常关闭]'
320
322
  : '';
321
- logger.warn(formatCompact({
323
+ this.#logger.warn(formatCompact({
322
324
  op: 'disconnect',
323
- endpoint: this.#options.config.name,
324
325
  code: codeNum,
325
326
  error: `${reasonStr || 'closed'}${codeHint}`,
326
327
  reconnect_ms: this.#options.config.reconnect_interval,
@@ -335,9 +336,9 @@ export class MilkyWsEndpoint implements EndpointInstance {
335
336
 
336
337
  ws.on('error', (err) => {
337
338
  const error = err instanceof Error ? err : new Error(String(err));
338
- logger.warn(formatCompact({
339
+ this.#logger.warn(formatCompact({
339
340
  op: 'ws_error',
340
- endpoint: this.#options.config.name,
341
+ endpoint: this.#options.config.id,
341
342
  ok: false,
342
343
  error: error.message,
343
344
  }));
@@ -361,9 +362,9 @@ export class MilkyWsEndpoint implements EndpointInstance {
361
362
  const event = JSON.parse(raw) as MilkyEvent;
362
363
  this.admit(event);
363
364
  } catch (error) {
364
- logger.warn(formatCompact({
365
+ this.#logger.warn(formatCompact({
365
366
  op: 'milky_parse_failed',
366
- endpoint: this.#options.config.name,
367
+ endpoint: this.#options.config.id,
367
368
  error: error instanceof Error ? error.message : String(error),
368
369
  }));
369
370
  }