@zhin.js/adapter-milky 1.1.18 → 1.1.19

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.
@@ -2,7 +2,7 @@
2
2
  * Milky SSE client endpoint — GET text/event-stream on /event.
3
3
  */
4
4
  import {
5
- ClientEndpoint,
5
+ createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
6
6
  createRecallEndpointControl,
7
7
  createEndpointLifecycle,
8
8
  type EndpointConnectHandle,
@@ -14,7 +14,7 @@ import {
14
14
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
15
15
  import type { CapabilityId } from 'zhin.js';
16
16
  import { createMilkyEndpointManagement } from './endpoint-management.js';
17
- import { callMilkyClient, createMilkyEndpointClient, forwardMilkyClientEvents, type MilkyClient } from './client.js';
17
+ import { createMilkyActionCaller, readMilkyResponse, type MilkyActionCaller, milkyClientOptions, forwardMilkyClientEvents, MilkyClient } from './client.js';
18
18
  import {
19
19
  buildSendAction,
20
20
  buildSseConnectOptions,
@@ -46,38 +46,49 @@ export type CreateMilkySseStream = (options: {
46
46
 
47
47
  export interface MilkySseEndpointOptions {
48
48
  readonly id: CapabilityId;
49
+ readonly signal?: AbortSignal;
49
50
  readonly config: MilkySseConfig;
50
51
  readonly createSseStream?: CreateMilkySseStream;
51
52
  readonly callApi?: typeof callApi;
52
53
  }
53
54
 
54
- export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
55
- readonly client: MilkyClient;
55
+ export class MilkySseEndpoint extends MilkyClient implements Endpoint {
56
+ readonly [endpointSpec]: EndpointSpecification;
57
+ #open = false;
56
58
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
57
59
 
58
60
  readonly #options: MilkySseEndpointOptions;
59
- readonly #callApi: typeof callApi;
60
- readonly management: EndpointManagement;
61
- readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
61
+ readonly #management: EndpointManagement;
62
+ readonly #call: MilkyActionCaller;
63
+ readonly #control: EndpointControl = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
62
64
  readonly #lifecycle: EndpointLifecycle;
63
65
  #stream?: SseClientHandle;
64
66
 
65
67
  constructor(options: MilkySseEndpointOptions) {
66
- super();
68
+ const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
69
+ super(milkyClientOptions({ config: options.config, call }));
70
+ this.#call = call;
67
71
  this.#logger = getAdapterLogger('milky', options.config.id);
68
72
  this.#options = options;
69
- this.#callApi = options.callApi ?? callApi;
70
- this.client = createMilkyEndpointClient(options.config, this.#callApi);
71
- this.management = createMilkyEndpointManagement({
72
- callApi: (action, params) => callMilkyClient(this.client, action, params),
73
+ this.#management = createMilkyEndpointManagement({
74
+ callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
73
75
  });
74
- this.bindClientEvents(
75
- (receive) => forwardMilkyClientEvents(this.client, receive),
76
- (name, payload) => {
76
+ const readTransportState = () => this.#lifecycle.state;
77
+ this[endpointSpec] = createEndpointSpecification(this, {
78
+ name: options.config.id,
79
+ start: () => this.#start(), open: () => { this.#open = true; },
80
+ close: () => { this.#open = false; }, stop: () => this.#stop(),
81
+ send: request => this.#send(request),
82
+ management: this.#management, control: this.#control,
83
+ get transportState() { return readTransportState(); },
84
+ });
85
+ forwardMilkyClientEvents(this, (name, payload) => {
86
+ void this[endpointSpec].events.run(() => {
87
+ if (!this.#open && !this.#lifecycle.started) return;
88
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
77
89
  if (name === 'event') this.#admitRaw(payload as MilkyEvent);
78
- },
79
- (_name, error) => this.#warnPlatformEvent(error),
80
- );
90
+ }).catch(error => this.#warnPlatformEvent(error));
91
+ });
81
92
  this.#lifecycle = createEndpointLifecycle({
82
93
  name: options.config.id,
83
94
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
@@ -90,7 +101,7 @@ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
90
101
  });
91
102
  }
92
103
 
93
- async start(): Promise<void> {
104
+ async #start(): Promise<void> {
94
105
  if (this.#lifecycle.started) return;
95
106
  try {
96
107
  await this.#lifecycle.start((handle) => this.#connect(handle));
@@ -100,17 +111,17 @@ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
100
111
  }
101
112
  }
102
113
 
103
- async stop(): Promise<void> {
104
- this.close();
114
+ async #stop(): Promise<void> {
115
+ this.#open = false;
105
116
  await this.#lifecycle.stop();
106
117
  this.#stream?.close();
107
118
  this.#stream = undefined;
108
119
  }
109
120
 
110
- async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
121
+ async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
111
122
  const message = formatOutboundSegments(payload);
112
123
  const { action, params } = buildSendAction(conversation, message);
113
- const data = await callMilkyClient<{ message_seq?: number }>(this.client, action, params);
124
+ const data = readMilkyResponse<{ message_seq?: number }>(await this.#call({ action, params, signal }), action);
114
125
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
115
126
  this.#logger.debug(formatCompact({
116
127
  op: 'milky_send',
@@ -122,23 +133,24 @@ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
122
133
  return messageId;
123
134
  }
124
135
 
125
- async recallMessage(id: string): Promise<void> {
136
+ async #recallMessage({ id, signal }: { readonly id: string; readonly signal?: AbortSignal }): Promise<void> {
126
137
  const parsed = parseMilkyMessageId(id);
127
138
  if (!parsed) throw new Error(`Invalid message id: ${id}`);
128
139
  if (parsed.message_scene === 'group') {
129
- await callMilkyClient(this.client, 'recall_group_message', {
140
+ readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
130
141
  group_id: parsed.peer_id,
131
142
  message_seq: parsed.message_seq,
132
- });
143
+ } }), 'recall_group_message');
133
144
  } else {
134
- await callMilkyClient(this.client, 'recall_private_message', {
145
+ readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
135
146
  user_id: parsed.peer_id,
136
147
  message_seq: parsed.message_seq,
137
- });
148
+ } }), 'recall_private_message');
138
149
  }
139
150
  }
140
151
 
141
152
  #admitRaw(event: MilkyEvent): void {
153
+ if (event.self_id != null) this[endpointSpec].reportSelfUserId(String(event.self_id));
142
154
  const data = parseMessageReceiveData(event);
143
155
  if (!data) return;
144
156
  this.#admitMessage(data, event);
@@ -149,14 +161,16 @@ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
149
161
  }
150
162
 
151
163
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
152
- const conversation = milkyInboundConversation(String(this.#options.id), data);
164
+ const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
153
165
  const target = `${conversation.kind}:${conversation.id}`;
154
166
  const content = formatInboundContent(data);
155
167
  const segments = formatInboundSegments(data);
156
168
  const audioUrl = extractInboundAudioUrl(data);
157
169
  const nickname = senderNickname(data);
158
170
  const mentioned = isMentioned(data, event.self_id);
159
- void this.emit('message.receive', {
171
+ void this[endpointSpec].events.message({
172
+ event,
173
+ ...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
160
174
  conversation,
161
175
  message: { conversation, id: formatInboundMessageId(data) },
162
176
  content,
@@ -245,7 +259,7 @@ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
245
259
  #onMessage(data: string): void {
246
260
  try {
247
261
  const event = JSON.parse(data) as MilkyEvent;
248
- this.client.ingest(event as Parameters<MilkyClient['ingest']>[0]);
262
+ this.ingest(event as Parameters<MilkyClient['ingest']>[0]);
249
263
  } catch (error) {
250
264
  this.#logger.warn(formatCompact({
251
265
  op: 'milky_parse_failed',
@@ -2,8 +2,9 @@
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 { acceptHttpIngress } from 'imhelper';
5
6
  import {
6
- ClientEndpoint,
7
+ createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
7
8
  createRecallEndpointControl,
8
9
  type EndpointControl,
9
10
  type EndpointManagement,
@@ -14,7 +15,7 @@ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
14
15
  import type { CapabilityId } from 'zhin.js';
15
16
  import { verifyMilkyAccessToken } from './milky-auth.js';
16
17
  import { createMilkyEndpointManagement } from './endpoint-management.js';
17
- import { callMilkyClient, createMilkyEndpointClient, forwardMilkyClientEvents, type MilkyClient } from './client.js';
18
+ import { createMilkyActionCaller, readMilkyResponse, type MilkyActionCaller, milkyClientOptions, forwardMilkyClientEvents, MilkyClient } from './client.js';
18
19
  import {
19
20
  buildSendAction,
20
21
  callApi,
@@ -36,41 +37,50 @@ import {
36
37
 
37
38
  export interface MilkyWebhookEndpointOptions {
38
39
  readonly id: CapabilityId;
40
+ readonly signal?: AbortSignal;
39
41
  readonly http: HttpHost;
40
42
  readonly config: MilkyWebhookConfig;
41
43
  readonly callApi?: typeof callApi;
42
44
  }
43
45
 
44
- export class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
45
- readonly client: MilkyClient;
46
+ export class MilkyWebhookEndpoint extends MilkyClient implements Endpoint {
47
+ readonly [endpointSpec]: EndpointSpecification;
48
+ #open = false;
46
49
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
47
50
 
48
51
  readonly #options: MilkyWebhookEndpointOptions;
49
- readonly #callApi: typeof callApi;
50
- readonly management: EndpointManagement;
51
- readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
52
+ readonly #management: EndpointManagement;
53
+ readonly #call: MilkyActionCaller;
54
+ readonly #control: EndpointControl = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
52
55
  #routeReleases: HttpRouteRegistration[] = [];
53
56
  #started = false;
54
57
 
55
58
  constructor(options: MilkyWebhookEndpointOptions) {
56
- super();
59
+ const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
60
+ super(milkyClientOptions({ config: options.config, call }));
61
+ this.#call = call;
57
62
  this.#logger = getAdapterLogger('milky', options.config.id);
58
63
  this.#options = options;
59
- this.#callApi = options.callApi ?? callApi;
60
- this.client = createMilkyEndpointClient(options.config, this.#callApi);
61
- this.management = createMilkyEndpointManagement({
62
- callApi: (action, params) => callMilkyClient(this.client, action, params),
64
+ this.#management = createMilkyEndpointManagement({
65
+ callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
63
66
  });
64
- this.bindClientEvents(
65
- (receive) => forwardMilkyClientEvents(this.client, receive),
66
- (name, payload) => {
67
+ this[endpointSpec] = createEndpointSpecification(this, {
68
+ name: options.config.id,
69
+ start: () => this.#start(), open: () => { this.#open = true; },
70
+ close: () => { this.#open = false; }, stop: () => this.#stop(),
71
+ send: request => this.#send(request),
72
+ management: this.#management, control: this.#control,
73
+ });
74
+ forwardMilkyClientEvents(this, (name, payload) => {
75
+ void this[endpointSpec].events.run(() => {
76
+ if (!this.#started) return;
77
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
67
78
  if (name === 'event') this.#admitRaw(payload as MilkyEvent);
68
- },
69
- (_name, error) => this.#warnPlatformEvent(error),
70
- );
79
+ }).catch(error => this.#warnPlatformEvent(error));
80
+ });
71
81
  }
72
82
 
73
- async start(): Promise<void> {
83
+ async #start(): Promise<void> {
74
84
  if (this.#started) return;
75
85
  this.#started = true;
76
86
  this.#setupRoutes();
@@ -82,17 +92,17 @@ export class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
82
92
  }));
83
93
  }
84
94
 
85
- async stop(): Promise<void> {
86
- this.close();
95
+ async #stop(): Promise<void> {
96
+ this.#open = false;
87
97
  for (const release of this.#routeReleases.splice(0)) release();
88
98
  this.#started = false;
89
99
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
90
100
  }
91
101
 
92
- async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
102
+ async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
93
103
  const message = formatOutboundSegments(payload);
94
104
  const { action, params } = buildSendAction(conversation, message);
95
- const data = await callMilkyClient<{ message_seq?: number }>(this.client, action, params);
105
+ const data = readMilkyResponse<{ message_seq?: number }>(await this.#call({ action, params, signal }), action);
96
106
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
97
107
  this.#logger.debug(formatCompact({
98
108
  op: 'milky_send',
@@ -103,23 +113,24 @@ export class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
103
113
  return messageId;
104
114
  }
105
115
 
106
- async recallMessage(id: string): Promise<void> {
116
+ async #recallMessage({ id, signal }: { readonly id: string; readonly signal?: AbortSignal }): Promise<void> {
107
117
  const parsed = parseMilkyMessageId(id);
108
118
  if (!parsed) throw new Error(`Invalid message id: ${id}`);
109
119
  if (parsed.message_scene === 'group') {
110
- await callMilkyClient(this.client, 'recall_group_message', {
120
+ readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
111
121
  group_id: parsed.peer_id,
112
122
  message_seq: parsed.message_seq,
113
- });
123
+ } }), 'recall_group_message');
114
124
  } else {
115
- await callMilkyClient(this.client, 'recall_private_message', {
125
+ readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
116
126
  user_id: parsed.peer_id,
117
127
  message_seq: parsed.message_seq,
118
- });
128
+ } }), 'recall_private_message');
119
129
  }
120
130
  }
121
131
 
122
132
  #admitRaw(event: MilkyEvent): void {
133
+ if (event.self_id != null) this[endpointSpec].reportSelfUserId(String(event.self_id));
123
134
  const data = parseMessageReceiveData(event);
124
135
  if (!data) return;
125
136
  this.#admitMessage(data, event);
@@ -130,14 +141,16 @@ export class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
130
141
  }
131
142
 
132
143
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
133
- const conversation = milkyInboundConversation(String(this.#options.id), data);
144
+ const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
134
145
  const target = `${conversation.kind}:${conversation.id}`;
135
146
  const content = formatInboundContent(data);
136
147
  const segments = formatInboundSegments(data);
137
148
  const audioUrl = extractInboundAudioUrl(data);
138
149
  const nickname = senderNickname(data);
139
150
  const mentioned = isMentioned(data, event.self_id);
140
- void this.emit('message.receive', {
151
+ void this[endpointSpec].events.message({
152
+ event,
153
+ ...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
141
154
  conversation,
142
155
  message: { conversation, id: formatInboundMessageId(data) },
143
156
  content,
@@ -179,12 +192,15 @@ export class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
179
192
  response.end(JSON.stringify({ message: 'Unauthorized' }));
180
193
  return;
181
194
  }
182
- if (!this.clientEventsOpen) {
195
+ if (!this.#open) {
183
196
  response.writeHead(200, { 'Content-Type': 'application/json' });
184
197
  response.end(JSON.stringify({ status: 'ok' }));
185
198
  return;
186
199
  }
187
- await this.client.acceptHttp(request, response);
200
+ await acceptHttpIngress<MilkyEvent>(request, response, event => {
201
+ void this[endpointSpec].events.run(() => this.ingest(event as Parameters<MilkyClient['ingest']>[0]))
202
+ .catch(error => this.#warnPlatformEvent(error));
203
+ });
188
204
  } catch (error) {
189
205
  this.#logger.error('Milky webhook error:', error);
190
206
  if (!response.headersSent) {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import WebSocket from 'ws';
5
5
  import {
6
- ClientEndpoint,
6
+ createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
7
7
  createRecallEndpointControl,
8
8
  createEndpointLifecycle,
9
9
  type EndpointConnectHandle,
@@ -16,10 +16,10 @@ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
16
16
  import type { CapabilityId } from 'zhin.js';
17
17
  import { createMilkyEndpointManagement } from './endpoint-management.js';
18
18
  import {
19
- callMilkyClient,
20
- createMilkyEndpointClient,
19
+ createMilkyActionCaller, readMilkyResponse, type MilkyActionCaller,
20
+ milkyClientOptions,
21
21
  forwardMilkyClientEvents,
22
- type MilkyClient,
22
+ MilkyClient,
23
23
  } from './client.js';
24
24
  import {
25
25
  buildSendAction,
@@ -46,6 +46,7 @@ const WS_OPEN = 1;
46
46
 
47
47
  export interface MilkyWsEndpointOptions {
48
48
  readonly id: CapabilityId;
49
+ readonly signal?: AbortSignal;
49
50
  readonly config: MilkyWsConfig;
50
51
  readonly createWebSocket?: (
51
52
  url: string,
@@ -54,34 +55,44 @@ export interface MilkyWsEndpointOptions {
54
55
  readonly callApi?: typeof callApi;
55
56
  }
56
57
 
57
- export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
58
- readonly client: MilkyClient;
58
+ export class MilkyWsEndpoint extends MilkyClient implements Endpoint {
59
+ readonly [endpointSpec]: EndpointSpecification;
60
+ #open = false;
59
61
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
60
62
 
61
63
  readonly #options: MilkyWsEndpointOptions;
62
- readonly #callApi: typeof callApi;
63
- readonly management: EndpointManagement;
64
- readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
64
+ readonly #management: EndpointManagement;
65
+ readonly #call: MilkyActionCaller;
66
+ readonly #control: EndpointControl = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
65
67
  readonly #lifecycle: EndpointLifecycle;
66
68
  #ws?: MilkyWsSocket;
67
69
  #clientSocketRelease?: () => void;
68
70
 
69
71
  constructor(options: MilkyWsEndpointOptions) {
70
- super();
72
+ const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
73
+ super(milkyClientOptions({ config: options.config, call }));
74
+ this.#call = call;
71
75
  this.#logger = getAdapterLogger('milky', options.config.id);
72
76
  this.#options = options;
73
- this.#callApi = options.callApi ?? callApi;
74
- this.client = createMilkyEndpointClient(options.config, this.#callApi);
75
- this.management = createMilkyEndpointManagement({
76
- callApi: (action, params) => callMilkyClient(this.client, action, params),
77
+ this.#management = createMilkyEndpointManagement({
78
+ callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
77
79
  });
78
- this.bindClientEvents(
79
- (receive) => forwardMilkyClientEvents(this.client, receive),
80
- (name, payload) => {
80
+ const readTransportState = () => this.#lifecycle.state;
81
+ this[endpointSpec] = createEndpointSpecification(this, {
82
+ name: options.config.id,
83
+ start: () => this.#start(), open: () => { this.#open = true; },
84
+ close: () => { this.#close(); }, stop: () => this.#stop(),
85
+ send: request => this.#send(request),
86
+ management: this.#management, control: this.#control,
87
+ get transportState() { return readTransportState(); },
88
+ });
89
+ forwardMilkyClientEvents(this, (name, payload) => {
90
+ void this[endpointSpec].events.run(() => {
91
+ if (!this.#open && !this.#lifecycle.started) return;
92
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
81
93
  if (name === 'event') this.#admitRaw(payload as MilkyEvent);
82
- },
83
- (_name, error) => this.#warnPlatformEvent(error),
84
- );
94
+ }).catch(error => this.#warnPlatformEvent(error));
95
+ });
85
96
  this.#lifecycle = createEndpointLifecycle({
86
97
  name: options.config.id,
87
98
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
@@ -94,19 +105,19 @@ export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
94
105
  });
95
106
  }
96
107
 
97
- async start(): Promise<void> {
108
+ async #start(): Promise<void> {
98
109
  if (this.#lifecycle.started) return;
99
110
  await this.#lifecycle.start((handle) => this.#connect(handle));
100
111
  }
101
112
 
102
- close(): void {
103
- super.close();
113
+ #close(): void {
114
+ this.#open = false;
104
115
  this.#clientSocketRelease?.();
105
116
  this.#clientSocketRelease = undefined;
106
117
  }
107
118
 
108
- async stop(): Promise<void> {
109
- this.close();
119
+ async #stop(): Promise<void> {
120
+ this.#close();
110
121
  await this.#lifecycle.stop();
111
122
  if (this.#ws) {
112
123
  try {
@@ -118,10 +129,10 @@ export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
118
129
  }
119
130
  }
120
131
 
121
- async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
132
+ async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
122
133
  const message = formatOutboundSegments(payload);
123
134
  const { action, params } = buildSendAction(conversation, message);
124
- const data = await callMilkyClient<{ message_seq?: number }>(this.client, action, params);
135
+ const data = readMilkyResponse<{ message_seq?: number }>(await this.#call({ action, params, signal }), action);
125
136
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
126
137
  this.#logger.debug(formatCompact({
127
138
  op: 'milky_send',
@@ -132,23 +143,24 @@ export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
132
143
  return messageId;
133
144
  }
134
145
 
135
- async recallMessage(id: string): Promise<void> {
146
+ async #recallMessage({ id, signal }: { readonly id: string; readonly signal?: AbortSignal }): Promise<void> {
136
147
  const parsed = parseMilkyMessageId(id);
137
148
  if (!parsed) throw new Error(`Invalid message id: ${id}`);
138
149
  if (parsed.message_scene === 'group') {
139
- await callMilkyClient(this.client, 'recall_group_message', {
150
+ readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
140
151
  group_id: parsed.peer_id,
141
152
  message_seq: parsed.message_seq,
142
- });
153
+ } }), 'recall_group_message');
143
154
  } else {
144
- await callMilkyClient(this.client, 'recall_private_message', {
155
+ readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
145
156
  user_id: parsed.peer_id,
146
157
  message_seq: parsed.message_seq,
147
- });
158
+ } }), 'recall_private_message');
148
159
  }
149
160
  }
150
161
 
151
162
  #admitRaw(event: MilkyEvent): void {
163
+ if (event.self_id != null) this[endpointSpec].reportSelfUserId(String(event.self_id));
152
164
  const data = parseMessageReceiveData(event);
153
165
  if (!data) return;
154
166
  this.#admitMessage(data, event);
@@ -162,14 +174,16 @@ export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
162
174
  }
163
175
 
164
176
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
165
- const conversation = milkyInboundConversation(String(this.#options.id), data);
177
+ const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
166
178
  const target = `${conversation.kind}:${conversation.id}`;
167
179
  const content = formatInboundContent(data);
168
180
  const segments = formatInboundSegments(data);
169
181
  const audioUrl = extractInboundAudioUrl(data);
170
182
  const nickname = senderNickname(data);
171
183
  const mentioned = isMentioned(data, event.self_id);
172
- void this.emit('message.receive', {
184
+ void this[endpointSpec].events.message({
185
+ event,
186
+ ...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
173
187
  conversation,
174
188
  message: { conversation, id: formatInboundMessageId(data) },
175
189
  content,
@@ -242,7 +256,7 @@ export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
242
256
  });
243
257
 
244
258
  this.#clientSocketRelease?.();
245
- this.#clientSocketRelease = this.client.acceptWebSocket(ws);
259
+ this.#clientSocketRelease = this.acceptWebSocket(ws);
246
260
 
247
261
  ws.on('close', (code, reason) => {
248
262
  this.#clientSocketRelease?.();