@zhin.js/adapter-milky 1.1.18 → 1.1.20

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.
@@ -1,34 +1,49 @@
1
1
  /**
2
2
  * Milky SSE client endpoint — GET text/event-stream on /event.
3
3
  */
4
- import { ClientEndpoint, createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
4
+ import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
5
5
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
6
6
  import { createMilkyEndpointManagement } from './endpoint-management.js';
7
- import { callMilkyClient, createMilkyEndpointClient, forwardMilkyClientEvents } from './client.js';
7
+ import { createMilkyActionCaller, readMilkyResponse, milkyClientOptions, forwardMilkyClientEvents, MilkyClient } from './client.js';
8
8
  import { buildSendAction, buildSseConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
9
9
  import { openSseStream } from './sse-client.js';
10
- export class MilkySseEndpoint extends ClientEndpoint {
11
- client;
10
+ export class MilkySseEndpoint extends MilkyClient {
11
+ [endpointSpec];
12
+ #open = false;
12
13
  #logger;
13
14
  #options;
14
- #callApi;
15
- management;
16
- control = createRecallEndpointControl((id) => this.recallMessage(id));
15
+ #management;
16
+ #call;
17
+ #control = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
17
18
  #lifecycle;
18
19
  #stream;
19
20
  constructor(options) {
20
- super();
21
+ const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
22
+ super(milkyClientOptions({ config: options.config, call }));
23
+ this.#call = call;
21
24
  this.#logger = getAdapterLogger('milky', options.config.id);
22
25
  this.#options = options;
23
- this.#callApi = options.callApi ?? callApi;
24
- this.client = createMilkyEndpointClient(options.config, this.#callApi);
25
- this.management = createMilkyEndpointManagement({
26
- callApi: (action, params) => callMilkyClient(this.client, action, params),
26
+ this.#management = createMilkyEndpointManagement({
27
+ callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
28
+ });
29
+ const readTransportState = () => this.#lifecycle.state;
30
+ this[endpointSpec] = createEndpointSpecification(this, {
31
+ name: options.config.id,
32
+ start: () => this.#start(), open: () => { this.#open = true; },
33
+ close: () => { this.#open = false; }, stop: () => this.#stop(),
34
+ send: request => this.#send(request),
35
+ management: this.#management, control: this.#control,
36
+ get transportState() { return readTransportState(); },
37
+ });
38
+ forwardMilkyClientEvents(this, (name, payload) => {
39
+ void this[endpointSpec].events.run(() => {
40
+ if (!this.#open && !this.#lifecycle.started)
41
+ return;
42
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
43
+ if (name === 'event')
44
+ this.#admitRaw(payload);
45
+ }).catch(error => this.#warnPlatformEvent(error));
27
46
  });
28
- this.bindClientEvents((receive) => forwardMilkyClientEvents(this.client, receive), (name, payload) => {
29
- if (name === 'event')
30
- this.#admitRaw(payload);
31
- }, (_name, error) => this.#warnPlatformEvent(error));
32
47
  this.#lifecycle = createEndpointLifecycle({
33
48
  name: options.config.id,
34
49
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
@@ -40,7 +55,7 @@ export class MilkySseEndpoint extends ClientEndpoint {
40
55
  },
41
56
  });
42
57
  }
43
- async start() {
58
+ async #start() {
44
59
  if (this.#lifecycle.started)
45
60
  return;
46
61
  try {
@@ -51,16 +66,16 @@ export class MilkySseEndpoint extends ClientEndpoint {
51
66
  throw err;
52
67
  }
53
68
  }
54
- async stop() {
55
- this.close();
69
+ async #stop() {
70
+ this.#open = false;
56
71
  await this.#lifecycle.stop();
57
72
  this.#stream?.close();
58
73
  this.#stream = undefined;
59
74
  }
60
- async send({ conversation, payload }) {
75
+ async #send({ conversation, payload, signal }) {
61
76
  const message = formatOutboundSegments(payload);
62
77
  const { action, params } = buildSendAction(conversation, message);
63
- const data = await callMilkyClient(this.client, action, params);
78
+ const data = readMilkyResponse(await this.#call({ action, params, signal }), action);
64
79
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
65
80
  this.#logger.debug(formatCompact({
66
81
  op: 'milky_send',
@@ -71,24 +86,26 @@ export class MilkySseEndpoint extends ClientEndpoint {
71
86
  }));
72
87
  return messageId;
73
88
  }
74
- async recallMessage(id) {
89
+ async #recallMessage({ id, signal }) {
75
90
  const parsed = parseMilkyMessageId(id);
76
91
  if (!parsed)
77
92
  throw new Error(`Invalid message id: ${id}`);
78
93
  if (parsed.message_scene === 'group') {
79
- await callMilkyClient(this.client, 'recall_group_message', {
80
- group_id: parsed.peer_id,
81
- message_seq: parsed.message_seq,
82
- });
94
+ readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
95
+ group_id: parsed.peer_id,
96
+ message_seq: parsed.message_seq,
97
+ } }), 'recall_group_message');
83
98
  }
84
99
  else {
85
- await callMilkyClient(this.client, 'recall_private_message', {
86
- user_id: parsed.peer_id,
87
- message_seq: parsed.message_seq,
88
- });
100
+ readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
101
+ user_id: parsed.peer_id,
102
+ message_seq: parsed.message_seq,
103
+ } }), 'recall_private_message');
89
104
  }
90
105
  }
91
106
  #admitRaw(event) {
107
+ if (event.self_id != null)
108
+ this[endpointSpec].reportSelfUserId(String(event.self_id));
92
109
  const data = parseMessageReceiveData(event);
93
110
  if (!data)
94
111
  return;
@@ -98,14 +115,16 @@ export class MilkySseEndpoint extends ClientEndpoint {
98
115
  this.#logger.warn(formatCompact({ op: 'milky_platform_event_failed', error: error instanceof Error ? error.message : String(error) }));
99
116
  }
100
117
  #admitMessage(data, event) {
101
- const conversation = milkyInboundConversation(String(this.#options.id), data);
118
+ const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
102
119
  const target = `${conversation.kind}:${conversation.id}`;
103
120
  const content = formatInboundContent(data);
104
121
  const segments = formatInboundSegments(data);
105
122
  const audioUrl = extractInboundAudioUrl(data);
106
123
  const nickname = senderNickname(data);
107
124
  const mentioned = isMentioned(data, event.self_id);
108
- void this.emit('message.receive', {
125
+ void this[endpointSpec].events.message({
126
+ event,
127
+ ...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
109
128
  conversation,
110
129
  message: { conversation, id: formatInboundMessageId(data) },
111
130
  content,
@@ -193,7 +212,7 @@ export class MilkySseEndpoint extends ClientEndpoint {
193
212
  #onMessage(data) {
194
213
  try {
195
214
  const event = JSON.parse(data);
196
- this.client.ingest(event);
215
+ this.ingest(event);
197
216
  }
198
217
  catch (error) {
199
218
  this.#logger.warn(formatCompact({
@@ -1,22 +1,17 @@
1
- import { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
1
+ import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
2
2
  import type { HttpHost } from '@zhin.js/host-http';
3
3
  import type { CapabilityId } from 'zhin.js';
4
- import { type MilkyClient } from './client.js';
4
+ import { MilkyClient } from './client.js';
5
5
  import { callApi, type MilkyWebhookConfig } from './protocol.js';
6
6
  export interface MilkyWebhookEndpointOptions {
7
7
  readonly id: CapabilityId;
8
+ readonly signal?: AbortSignal;
8
9
  readonly http: HttpHost;
9
10
  readonly config: MilkyWebhookConfig;
10
11
  readonly callApi?: typeof callApi;
11
12
  }
12
- export declare class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
13
+ export declare class MilkyWebhookEndpoint extends MilkyClient implements Endpoint {
13
14
  #private;
14
- readonly client: MilkyClient;
15
- readonly management: EndpointManagement;
16
- readonly control: EndpointControl;
15
+ readonly [endpointSpec]: EndpointSpecification;
17
16
  constructor(options: MilkyWebhookEndpointOptions);
18
- start(): Promise<void>;
19
- stop(): Promise<void>;
20
- send({ conversation, payload }: EndpointSendRequest): Promise<string>;
21
- recallMessage(id: string): Promise<void>;
22
17
  }
@@ -1,33 +1,47 @@
1
- import { ClientEndpoint, createRecallEndpointControl, } from 'zhin.js/adapter';
1
+ import { acceptHttpIngress } from 'imhelper';
2
+ import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, } from 'zhin.js/adapter';
2
3
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
3
4
  import { verifyMilkyAccessToken } from './milky-auth.js';
4
5
  import { createMilkyEndpointManagement } from './endpoint-management.js';
5
- import { callMilkyClient, createMilkyEndpointClient, forwardMilkyClientEvents } from './client.js';
6
+ import { createMilkyActionCaller, readMilkyResponse, milkyClientOptions, forwardMilkyClientEvents, MilkyClient } from './client.js';
6
7
  import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
7
- export class MilkyWebhookEndpoint extends ClientEndpoint {
8
- client;
8
+ export class MilkyWebhookEndpoint extends MilkyClient {
9
+ [endpointSpec];
10
+ #open = false;
9
11
  #logger;
10
12
  #options;
11
- #callApi;
12
- management;
13
- control = createRecallEndpointControl((id) => this.recallMessage(id));
13
+ #management;
14
+ #call;
15
+ #control = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
14
16
  #routeReleases = [];
15
17
  #started = false;
16
18
  constructor(options) {
17
- super();
19
+ const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
20
+ super(milkyClientOptions({ config: options.config, call }));
21
+ this.#call = call;
18
22
  this.#logger = getAdapterLogger('milky', options.config.id);
19
23
  this.#options = options;
20
- this.#callApi = options.callApi ?? callApi;
21
- this.client = createMilkyEndpointClient(options.config, this.#callApi);
22
- this.management = createMilkyEndpointManagement({
23
- callApi: (action, params) => callMilkyClient(this.client, action, params),
24
+ this.#management = createMilkyEndpointManagement({
25
+ callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
26
+ });
27
+ this[endpointSpec] = createEndpointSpecification(this, {
28
+ name: options.config.id,
29
+ start: () => this.#start(), open: () => { this.#open = true; },
30
+ close: () => { this.#open = false; }, stop: () => this.#stop(),
31
+ send: request => this.#send(request),
32
+ management: this.#management, control: this.#control,
33
+ });
34
+ forwardMilkyClientEvents(this, (name, payload) => {
35
+ void this[endpointSpec].events.run(() => {
36
+ if (!this.#started)
37
+ return;
38
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
39
+ if (name === 'event')
40
+ this.#admitRaw(payload);
41
+ }).catch(error => this.#warnPlatformEvent(error));
24
42
  });
25
- this.bindClientEvents((receive) => forwardMilkyClientEvents(this.client, receive), (name, payload) => {
26
- if (name === 'event')
27
- this.#admitRaw(payload);
28
- }, (_name, error) => this.#warnPlatformEvent(error));
29
43
  }
30
- async start() {
44
+ async #start() {
31
45
  if (this.#started)
32
46
  return;
33
47
  this.#started = true;
@@ -39,17 +53,17 @@ export class MilkyWebhookEndpoint extends ClientEndpoint {
39
53
  path: this.#options.config.path,
40
54
  }));
41
55
  }
42
- async stop() {
43
- this.close();
56
+ async #stop() {
57
+ this.#open = false;
44
58
  for (const release of this.#routeReleases.splice(0))
45
59
  release();
46
60
  this.#started = false;
47
61
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
48
62
  }
49
- async send({ conversation, payload }) {
63
+ async #send({ conversation, payload, signal }) {
50
64
  const message = formatOutboundSegments(payload);
51
65
  const { action, params } = buildSendAction(conversation, message);
52
- const data = await callMilkyClient(this.client, action, params);
66
+ const data = readMilkyResponse(await this.#call({ action, params, signal }), action);
53
67
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
54
68
  this.#logger.debug(formatCompact({
55
69
  op: 'milky_send',
@@ -59,24 +73,26 @@ export class MilkyWebhookEndpoint extends ClientEndpoint {
59
73
  }));
60
74
  return messageId;
61
75
  }
62
- async recallMessage(id) {
76
+ async #recallMessage({ id, signal }) {
63
77
  const parsed = parseMilkyMessageId(id);
64
78
  if (!parsed)
65
79
  throw new Error(`Invalid message id: ${id}`);
66
80
  if (parsed.message_scene === 'group') {
67
- await callMilkyClient(this.client, 'recall_group_message', {
68
- group_id: parsed.peer_id,
69
- message_seq: parsed.message_seq,
70
- });
81
+ readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
82
+ group_id: parsed.peer_id,
83
+ message_seq: parsed.message_seq,
84
+ } }), 'recall_group_message');
71
85
  }
72
86
  else {
73
- await callMilkyClient(this.client, 'recall_private_message', {
74
- user_id: parsed.peer_id,
75
- message_seq: parsed.message_seq,
76
- });
87
+ readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
88
+ user_id: parsed.peer_id,
89
+ message_seq: parsed.message_seq,
90
+ } }), 'recall_private_message');
77
91
  }
78
92
  }
79
93
  #admitRaw(event) {
94
+ if (event.self_id != null)
95
+ this[endpointSpec].reportSelfUserId(String(event.self_id));
80
96
  const data = parseMessageReceiveData(event);
81
97
  if (!data)
82
98
  return;
@@ -86,14 +102,16 @@ export class MilkyWebhookEndpoint extends ClientEndpoint {
86
102
  this.#logger.warn(formatCompact({ op: 'milky_platform_event_failed', error: error instanceof Error ? error.message : String(error) }));
87
103
  }
88
104
  #admitMessage(data, event) {
89
- const conversation = milkyInboundConversation(String(this.#options.id), data);
105
+ const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
90
106
  const target = `${conversation.kind}:${conversation.id}`;
91
107
  const content = formatInboundContent(data);
92
108
  const segments = formatInboundSegments(data);
93
109
  const audioUrl = extractInboundAudioUrl(data);
94
110
  const nickname = senderNickname(data);
95
111
  const mentioned = isMentioned(data, event.self_id);
96
- void this.emit('message.receive', {
112
+ void this[endpointSpec].events.message({
113
+ event,
114
+ ...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
97
115
  conversation,
98
116
  message: { conversation, id: formatInboundMessageId(data) },
99
117
  content,
@@ -131,12 +149,15 @@ export class MilkyWebhookEndpoint extends ClientEndpoint {
131
149
  response.end(JSON.stringify({ message: 'Unauthorized' }));
132
150
  return;
133
151
  }
134
- if (!this.clientEventsOpen) {
152
+ if (!this.#open) {
135
153
  response.writeHead(200, { 'Content-Type': 'application/json' });
136
154
  response.end(JSON.stringify({ status: 'ok' }));
137
155
  return;
138
156
  }
139
- await this.client.acceptHttp(request, response);
157
+ await acceptHttpIngress(request, response, event => {
158
+ void this[endpointSpec].events.run(() => this.ingest(event))
159
+ .catch(error => this.#warnPlatformEvent(error));
160
+ });
140
161
  }
141
162
  catch (error) {
142
163
  this.#logger.error('Milky webhook error:', error);
@@ -1,23 +1,17 @@
1
- import { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
1
+ import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
2
2
  import type { CapabilityId } from 'zhin.js';
3
- import { type MilkyClient } from './client.js';
3
+ import { MilkyClient } from './client.js';
4
4
  import { callApi, type MilkyWsConfig } from './protocol.js';
5
5
  import type { MilkyWsCreateOptions, MilkyWsSocket } from './ws-types.js';
6
6
  export interface MilkyWsEndpointOptions {
7
7
  readonly id: CapabilityId;
8
+ readonly signal?: AbortSignal;
8
9
  readonly config: MilkyWsConfig;
9
10
  readonly createWebSocket?: (url: string, options: MilkyWsCreateOptions) => MilkyWsSocket;
10
11
  readonly callApi?: typeof callApi;
11
12
  }
12
- export declare class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
13
+ export declare class MilkyWsEndpoint extends MilkyClient implements Endpoint {
13
14
  #private;
14
- readonly client: MilkyClient;
15
- readonly management: EndpointManagement;
16
- readonly control: EndpointControl;
15
+ readonly [endpointSpec]: EndpointSpecification;
17
16
  constructor(options: MilkyWsEndpointOptions);
18
- start(): Promise<void>;
19
- close(): void;
20
- stop(): Promise<void>;
21
- send({ conversation, payload }: EndpointSendRequest): Promise<string>;
22
- recallMessage(id: string): Promise<void>;
23
17
  }
@@ -2,35 +2,50 @@
2
2
  * Milky WS client endpoint — outbound connect to Milky protocol server.
3
3
  */
4
4
  import WebSocket from 'ws';
5
- import { ClientEndpoint, createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
5
+ import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
6
6
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
7
7
  import { createMilkyEndpointManagement } from './endpoint-management.js';
8
- import { callMilkyClient, createMilkyEndpointClient, forwardMilkyClientEvents, } from './client.js';
8
+ import { createMilkyActionCaller, readMilkyResponse, milkyClientOptions, forwardMilkyClientEvents, MilkyClient, } from './client.js';
9
9
  import { buildSendAction, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
10
10
  const WS_OPEN = 1;
11
- export class MilkyWsEndpoint extends ClientEndpoint {
12
- client;
11
+ export class MilkyWsEndpoint extends MilkyClient {
12
+ [endpointSpec];
13
+ #open = false;
13
14
  #logger;
14
15
  #options;
15
- #callApi;
16
- management;
17
- control = createRecallEndpointControl((id) => this.recallMessage(id));
16
+ #management;
17
+ #call;
18
+ #control = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
18
19
  #lifecycle;
19
20
  #ws;
20
21
  #clientSocketRelease;
21
22
  constructor(options) {
22
- super();
23
+ const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
24
+ super(milkyClientOptions({ config: options.config, call }));
25
+ this.#call = call;
23
26
  this.#logger = getAdapterLogger('milky', options.config.id);
24
27
  this.#options = options;
25
- this.#callApi = options.callApi ?? callApi;
26
- this.client = createMilkyEndpointClient(options.config, this.#callApi);
27
- this.management = createMilkyEndpointManagement({
28
- callApi: (action, params) => callMilkyClient(this.client, action, params),
28
+ this.#management = createMilkyEndpointManagement({
29
+ callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
30
+ });
31
+ const readTransportState = () => this.#lifecycle.state;
32
+ this[endpointSpec] = createEndpointSpecification(this, {
33
+ name: options.config.id,
34
+ start: () => this.#start(), open: () => { this.#open = true; },
35
+ close: () => { this.#close(); }, stop: () => this.#stop(),
36
+ send: request => this.#send(request),
37
+ management: this.#management, control: this.#control,
38
+ get transportState() { return readTransportState(); },
39
+ });
40
+ forwardMilkyClientEvents(this, (name, payload) => {
41
+ void this[endpointSpec].events.run(() => {
42
+ if (!this.#open && !this.#lifecycle.started)
43
+ return;
44
+ void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
45
+ if (name === 'event')
46
+ this.#admitRaw(payload);
47
+ }).catch(error => this.#warnPlatformEvent(error));
29
48
  });
30
- this.bindClientEvents((receive) => forwardMilkyClientEvents(this.client, receive), (name, payload) => {
31
- if (name === 'event')
32
- this.#admitRaw(payload);
33
- }, (_name, error) => this.#warnPlatformEvent(error));
34
49
  this.#lifecycle = createEndpointLifecycle({
35
50
  name: options.config.id,
36
51
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
@@ -42,18 +57,18 @@ export class MilkyWsEndpoint extends ClientEndpoint {
42
57
  },
43
58
  });
44
59
  }
45
- async start() {
60
+ async #start() {
46
61
  if (this.#lifecycle.started)
47
62
  return;
48
63
  await this.#lifecycle.start((handle) => this.#connect(handle));
49
64
  }
50
- close() {
51
- super.close();
65
+ #close() {
66
+ this.#open = false;
52
67
  this.#clientSocketRelease?.();
53
68
  this.#clientSocketRelease = undefined;
54
69
  }
55
- async stop() {
56
- this.close();
70
+ async #stop() {
71
+ this.#close();
57
72
  await this.#lifecycle.stop();
58
73
  if (this.#ws) {
59
74
  try {
@@ -65,10 +80,10 @@ export class MilkyWsEndpoint extends ClientEndpoint {
65
80
  this.#ws = undefined;
66
81
  }
67
82
  }
68
- async send({ conversation, payload }) {
83
+ async #send({ conversation, payload, signal }) {
69
84
  const message = formatOutboundSegments(payload);
70
85
  const { action, params } = buildSendAction(conversation, message);
71
- const data = await callMilkyClient(this.client, action, params);
86
+ const data = readMilkyResponse(await this.#call({ action, params, signal }), action);
72
87
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
73
88
  this.#logger.debug(formatCompact({
74
89
  op: 'milky_send',
@@ -78,24 +93,26 @@ export class MilkyWsEndpoint extends ClientEndpoint {
78
93
  }));
79
94
  return messageId;
80
95
  }
81
- async recallMessage(id) {
96
+ async #recallMessage({ id, signal }) {
82
97
  const parsed = parseMilkyMessageId(id);
83
98
  if (!parsed)
84
99
  throw new Error(`Invalid message id: ${id}`);
85
100
  if (parsed.message_scene === 'group') {
86
- await callMilkyClient(this.client, 'recall_group_message', {
87
- group_id: parsed.peer_id,
88
- message_seq: parsed.message_seq,
89
- });
101
+ readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
102
+ group_id: parsed.peer_id,
103
+ message_seq: parsed.message_seq,
104
+ } }), 'recall_group_message');
90
105
  }
91
106
  else {
92
- await callMilkyClient(this.client, 'recall_private_message', {
93
- user_id: parsed.peer_id,
94
- message_seq: parsed.message_seq,
95
- });
107
+ readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
108
+ user_id: parsed.peer_id,
109
+ message_seq: parsed.message_seq,
110
+ } }), 'recall_private_message');
96
111
  }
97
112
  }
98
113
  #admitRaw(event) {
114
+ if (event.self_id != null)
115
+ this[endpointSpec].reportSelfUserId(String(event.self_id));
99
116
  const data = parseMessageReceiveData(event);
100
117
  if (!data)
101
118
  return;
@@ -108,14 +125,16 @@ export class MilkyWsEndpoint extends ClientEndpoint {
108
125
  }));
109
126
  }
110
127
  #admitMessage(data, event) {
111
- const conversation = milkyInboundConversation(String(this.#options.id), data);
128
+ const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
112
129
  const target = `${conversation.kind}:${conversation.id}`;
113
130
  const content = formatInboundContent(data);
114
131
  const segments = formatInboundSegments(data);
115
132
  const audioUrl = extractInboundAudioUrl(data);
116
133
  const nickname = senderNickname(data);
117
134
  const mentioned = isMentioned(data, event.self_id);
118
- void this.emit('message.receive', {
135
+ void this[endpointSpec].events.message({
136
+ event,
137
+ ...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
119
138
  conversation,
120
139
  message: { conversation, id: formatInboundMessageId(data) },
121
140
  content,
@@ -187,7 +206,7 @@ export class MilkyWsEndpoint extends ClientEndpoint {
187
206
  resolve();
188
207
  });
189
208
  this.#clientSocketRelease?.();
190
- this.#clientSocketRelease = this.client.acceptWebSocket(ws);
209
+ this.#clientSocketRelease = this.acceptWebSocket(ws);
191
210
  ws.on('close', (code, reason) => {
192
211
  this.#clientSocketRelease?.();
193
212
  this.#clientSocketRelease = undefined;
@@ -1,25 +1,20 @@
1
1
  /**
2
2
  * Milky reverse WSS endpoint — httpHostToken WS upgrade inbound + baseUrl HTTP API outbound.
3
3
  */
4
- import { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
4
+ import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
5
5
  import type { HttpHost } from '@zhin.js/host-http';
6
6
  import type { CapabilityId } from 'zhin.js';
7
- import { type MilkyClient } from './client.js';
7
+ import { MilkyClient } from './client.js';
8
8
  import { callApi, type MilkyWssConfig } from './protocol.js';
9
9
  export interface MilkyWssEndpointOptions {
10
10
  readonly id: CapabilityId;
11
+ readonly signal?: AbortSignal;
11
12
  readonly http: HttpHost;
12
13
  readonly config: MilkyWssConfig;
13
14
  readonly callApi?: typeof callApi;
14
15
  }
15
- export declare class MilkyWssEndpoint extends ClientEndpoint<MilkyClient> {
16
+ export declare class MilkyWssEndpoint extends MilkyClient implements Endpoint {
16
17
  #private;
17
- readonly client: MilkyClient;
18
- readonly management: EndpointManagement;
19
- readonly control: EndpointControl;
18
+ readonly [endpointSpec]: EndpointSpecification;
20
19
  constructor(options: MilkyWssEndpointOptions);
21
- start(): Promise<void>;
22
- stop(): Promise<void>;
23
- send({ conversation, payload }: EndpointSendRequest): Promise<string>;
24
- recallMessage(id: string): Promise<void>;
25
20
  }