@zhin.js/adapter-milky 6.0.14 → 7.0.1

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,18 +2,19 @@
2
2
  * Milky SSE client endpoint — GET text/event-stream on /event.
3
3
  */
4
4
  import {
5
+ ClientEndpoint,
6
+ createRecallEndpointControl,
5
7
  createEndpointLifecycle,
6
8
  type EndpointConnectHandle,
7
- type EndpointInstance,
9
+ type EndpointControl,
8
10
  type EndpointLifecycle,
9
11
  type EndpointManagement,
10
12
  type EndpointSendRequest,
11
13
  } from 'zhin.js/adapter';
12
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
13
14
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
14
15
  import type { CapabilityId } from 'zhin.js';
15
16
  import { createMilkyEndpointManagement } from './endpoint-management.js';
16
- import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
17
+ import { callMilkyClient, createMilkyEndpointClient, forwardMilkyClientEvents, type MilkyClient } from './client.js';
17
18
  import {
18
19
  buildSendAction,
19
20
  buildSseConnectOptions,
@@ -45,28 +46,38 @@ export type CreateMilkySseStream = (options: {
45
46
 
46
47
  export interface MilkySseEndpointOptions {
47
48
  readonly id: CapabilityId;
48
- readonly gateway: MessageGateway;
49
- readonly sideEvents?: SideEventGateway;
50
49
  readonly config: MilkySseConfig;
51
50
  readonly createSseStream?: CreateMilkySseStream;
52
51
  readonly callApi?: typeof callApi;
53
52
  }
54
53
 
55
- export class MilkySseEndpoint implements EndpointInstance {
54
+ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
55
+ readonly client: MilkyClient;
56
56
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
57
57
 
58
58
  readonly #options: MilkySseEndpointOptions;
59
59
  readonly #callApi: typeof callApi;
60
- readonly management: EndpointManagement = createMilkyEndpointManagement(this);
60
+ readonly management: EndpointManagement;
61
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
61
62
  readonly #lifecycle: EndpointLifecycle;
62
63
  #stream?: SseClientHandle;
63
- #open = false;
64
- #unregisterAgent?: () => void;
65
64
 
66
65
  constructor(options: MilkySseEndpointOptions) {
66
+ super();
67
67
  this.#logger = getAdapterLogger('milky', options.config.id);
68
68
  this.#options = options;
69
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
+ });
74
+ this.bindClientEvents(
75
+ (receive) => forwardMilkyClientEvents(this.client, receive),
76
+ (name, payload) => {
77
+ if (name === 'event') this.#admitRaw(payload as MilkyEvent);
78
+ },
79
+ (_name, error) => this.#warnPlatformEvent(error),
80
+ );
70
81
  this.#lifecycle = createEndpointLifecycle({
71
82
  name: options.config.id,
72
83
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
@@ -81,30 +92,17 @@ export class MilkySseEndpoint implements EndpointInstance {
81
92
 
82
93
  async start(): Promise<void> {
83
94
  if (this.#lifecycle.started) return;
84
- this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
85
95
  try {
86
96
  await this.#lifecycle.start((handle) => this.#connect(handle));
87
97
  } catch (err) {
88
98
  // 与 WS 对齐:start 失败复位由基座保证;agent 反注册留在适配器侧,允许重试
89
- this.#unregisterAgent?.();
90
- this.#unregisterAgent = undefined;
91
99
  throw err;
92
100
  }
93
101
  }
94
102
 
95
- open(): void {
96
- this.#open = true;
97
- }
98
-
99
- close(): void {
100
- this.#open = false;
101
- }
102
-
103
103
  async stop(): Promise<void> {
104
- this.#open = false;
104
+ this.close();
105
105
  await this.#lifecycle.stop();
106
- this.#unregisterAgent?.();
107
- this.#unregisterAgent = undefined;
108
106
  this.#stream?.close();
109
107
  this.#stream = undefined;
110
108
  }
@@ -112,7 +110,7 @@ export class MilkySseEndpoint implements EndpointInstance {
112
110
  async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
113
111
  const message = formatOutboundSegments(payload);
114
112
  const { action, params } = buildSendAction(conversation, message);
115
- const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
113
+ const data = await callMilkyClient<{ message_seq?: number }>(this.client, action, params);
116
114
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
117
115
  this.#logger.debug(formatCompact({
118
116
  op: 'milky_send',
@@ -124,100 +122,30 @@ export class MilkySseEndpoint implements EndpointInstance {
124
122
  return messageId;
125
123
  }
126
124
 
127
- callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
128
- return this.#callApi(this.apiOptions(), action, params);
129
- }
130
-
131
125
  async recallMessage(id: string): Promise<void> {
132
126
  const parsed = parseMilkyMessageId(id);
133
127
  if (!parsed) throw new Error(`Invalid message id: ${id}`);
134
128
  if (parsed.message_scene === 'group') {
135
- await this.callApi('recall_group_message', {
129
+ await callMilkyClient(this.client, 'recall_group_message', {
136
130
  group_id: parsed.peer_id,
137
131
  message_seq: parsed.message_seq,
138
132
  });
139
133
  } else {
140
- await this.callApi('recall_private_message', {
134
+ await callMilkyClient(this.client, 'recall_private_message', {
141
135
  user_id: parsed.peer_id,
142
136
  message_seq: parsed.message_seq,
143
137
  });
144
138
  }
145
139
  }
146
140
 
147
- async kickMember(groupId: number, userId: number, rejectAddRequest = false): Promise<boolean> {
148
- await this.callApi('kick_group_member', {
149
- group_id: groupId,
150
- user_id: userId,
151
- reject_add_request: rejectAddRequest,
152
- });
153
- return true;
154
- }
155
-
156
- async muteMember(groupId: number, userId: number, duration = 600): Promise<boolean> {
157
- await this.callApi('set_group_member_mute', {
158
- group_id: groupId,
159
- user_id: userId,
160
- duration,
161
- });
162
- return true;
163
- }
164
-
165
- async muteAll(groupId: number, enable = true): Promise<boolean> {
166
- await this.callApi('set_group_whole_mute', { group_id: groupId, is_mute: enable });
167
- return true;
168
- }
169
-
170
- async setAdmin(groupId: number, userId: number, enable = true): Promise<boolean> {
171
- await this.callApi('set_group_member_admin', {
172
- group_id: groupId,
173
- user_id: userId,
174
- is_set: enable,
175
- });
176
- return true;
177
- }
178
-
179
- async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
180
- await this.callApi('set_group_member_card', {
181
- group_id: groupId,
182
- user_id: userId,
183
- card,
184
- });
185
- return true;
186
- }
187
-
188
- async setTitle(groupId: number, userId: number, title: string): Promise<boolean> {
189
- await this.callApi('set_group_member_special_title', {
190
- group_id: groupId,
191
- user_id: userId,
192
- special_title: title,
193
- });
194
- return true;
195
- }
196
-
197
- async setGroupName(groupId: number, name: string): Promise<boolean> {
198
- await this.callApi('set_group_name', { group_id: groupId, new_group_name: name });
199
- return true;
200
- }
201
-
202
- async getMemberList(groupId: number): Promise<unknown[]> {
203
- return this.callApi('get_group_member_list', { group_id: groupId }) as Promise<unknown[]>;
204
- }
205
-
206
- async getGroupInfo(groupId: number): Promise<unknown> {
207
- return this.callApi('get_group_info', { group_id: groupId });
208
- }
209
-
210
- admit(event: MilkyEvent): void {
141
+ #admitRaw(event: MilkyEvent): void {
211
142
  const data = parseMessageReceiveData(event);
212
- if (!this.#open || !data) return;
143
+ if (!data) return;
213
144
  this.#admitMessage(data, event);
214
145
  }
215
146
 
216
- apiOptions(): { baseUrl: string; access_token?: string } {
217
- return {
218
- baseUrl: this.#options.config.baseUrl,
219
- access_token: this.#options.config.access_token,
220
- };
147
+ #warnPlatformEvent(error: unknown): void {
148
+ this.#logger.warn(formatCompact({ op: 'milky_platform_event_failed', error: error instanceof Error ? error.message : String(error) }));
221
149
  }
222
150
 
223
151
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
@@ -228,7 +156,7 @@ export class MilkySseEndpoint implements EndpointInstance {
228
156
  const audioUrl = extractInboundAudioUrl(data);
229
157
  const nickname = senderNickname(data);
230
158
  const mentioned = isMentioned(data, event.self_id);
231
- void this.#options.gateway.receive({
159
+ void this.emit('message.receive', {
232
160
  conversation,
233
161
  message: { conversation, id: formatInboundMessageId(data) },
234
162
  content,
@@ -317,7 +245,7 @@ export class MilkySseEndpoint implements EndpointInstance {
317
245
  #onMessage(data: string): void {
318
246
  try {
319
247
  const event = JSON.parse(data) as MilkyEvent;
320
- this.admit(event);
248
+ this.client.ingest(event as Parameters<MilkyClient['ingest']>[0]);
321
249
  } catch (error) {
322
250
  this.#logger.warn(formatCompact({
323
251
  op: 'milky_parse_failed',
@@ -2,18 +2,19 @@
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 {
6
- EndpointInstance,
7
- EndpointManagement,
8
- EndpointSendRequest,
5
+ import {
6
+ ClientEndpoint,
7
+ createRecallEndpointControl,
8
+ type EndpointControl,
9
+ type EndpointManagement,
10
+ type EndpointSendRequest,
9
11
  } from 'zhin.js/adapter';
10
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
11
12
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
12
13
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
13
14
  import type { CapabilityId } from 'zhin.js';
14
- import { readRequestBody, verifyMilkyAccessToken } from './milky-auth.js';
15
+ import { verifyMilkyAccessToken } from './milky-auth.js';
15
16
  import { createMilkyEndpointManagement } from './endpoint-management.js';
16
- import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
17
+ import { callMilkyClient, createMilkyEndpointClient, forwardMilkyClientEvents, type MilkyClient } from './client.js';
17
18
  import {
18
19
  buildSendAction,
19
20
  callApi,
@@ -35,34 +36,43 @@ import {
35
36
 
36
37
  export interface MilkyWebhookEndpointOptions {
37
38
  readonly id: CapabilityId;
38
- readonly gateway: MessageGateway;
39
- readonly sideEvents?: SideEventGateway;
40
39
  readonly http: HttpHost;
41
40
  readonly config: MilkyWebhookConfig;
42
41
  readonly callApi?: typeof callApi;
43
42
  }
44
43
 
45
- export class MilkyWebhookEndpoint implements EndpointInstance {
44
+ export class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
45
+ readonly client: MilkyClient;
46
46
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
47
47
 
48
48
  readonly #options: MilkyWebhookEndpointOptions;
49
49
  readonly #callApi: typeof callApi;
50
- readonly management: EndpointManagement = createMilkyEndpointManagement(this);
50
+ readonly management: EndpointManagement;
51
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
51
52
  #routeReleases: HttpRouteRegistration[] = [];
52
- #open = false;
53
53
  #started = false;
54
- #unregisterAgent?: () => void;
55
54
 
56
55
  constructor(options: MilkyWebhookEndpointOptions) {
56
+ super();
57
57
  this.#logger = getAdapterLogger('milky', options.config.id);
58
58
  this.#options = options;
59
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),
63
+ });
64
+ this.bindClientEvents(
65
+ (receive) => forwardMilkyClientEvents(this.client, receive),
66
+ (name, payload) => {
67
+ if (name === 'event') this.#admitRaw(payload as MilkyEvent);
68
+ },
69
+ (_name, error) => this.#warnPlatformEvent(error),
70
+ );
60
71
  }
61
72
 
62
73
  async start(): Promise<void> {
63
74
  if (this.#started) return;
64
75
  this.#started = true;
65
- this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
66
76
  this.#setupRoutes();
67
77
  this.#logger.info(formatCompact({
68
78
  op: 'listen',
@@ -72,19 +82,9 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
72
82
  }));
73
83
  }
74
84
 
75
- open(): void {
76
- this.#open = true;
77
- }
78
-
79
- close(): void {
80
- this.#open = false;
81
- }
82
-
83
85
  async stop(): Promise<void> {
84
- this.#open = false;
86
+ this.close();
85
87
  for (const release of this.#routeReleases.splice(0)) release();
86
- this.#unregisterAgent?.();
87
- this.#unregisterAgent = undefined;
88
88
  this.#started = false;
89
89
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
90
90
  }
@@ -92,7 +92,7 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
92
92
  async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
93
93
  const message = formatOutboundSegments(payload);
94
94
  const { action, params } = buildSendAction(conversation, message);
95
- const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
95
+ const data = await callMilkyClient<{ message_seq?: number }>(this.client, action, params);
96
96
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
97
97
  this.#logger.debug(formatCompact({
98
98
  op: 'milky_send',
@@ -103,100 +103,30 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
103
103
  return messageId;
104
104
  }
105
105
 
106
- callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
107
- return this.#callApi(this.apiOptions(), action, params);
108
- }
109
-
110
106
  async recallMessage(id: string): Promise<void> {
111
107
  const parsed = parseMilkyMessageId(id);
112
108
  if (!parsed) throw new Error(`Invalid message id: ${id}`);
113
109
  if (parsed.message_scene === 'group') {
114
- await this.callApi('recall_group_message', {
110
+ await callMilkyClient(this.client, 'recall_group_message', {
115
111
  group_id: parsed.peer_id,
116
112
  message_seq: parsed.message_seq,
117
113
  });
118
114
  } else {
119
- await this.callApi('recall_private_message', {
115
+ await callMilkyClient(this.client, 'recall_private_message', {
120
116
  user_id: parsed.peer_id,
121
117
  message_seq: parsed.message_seq,
122
118
  });
123
119
  }
124
120
  }
125
121
 
126
- async kickMember(groupId: number, userId: number, rejectAddRequest = false): Promise<boolean> {
127
- await this.callApi('kick_group_member', {
128
- group_id: groupId,
129
- user_id: userId,
130
- reject_add_request: rejectAddRequest,
131
- });
132
- return true;
133
- }
134
-
135
- async muteMember(groupId: number, userId: number, duration = 600): Promise<boolean> {
136
- await this.callApi('set_group_member_mute', {
137
- group_id: groupId,
138
- user_id: userId,
139
- duration,
140
- });
141
- return true;
142
- }
143
-
144
- async muteAll(groupId: number, enable = true): Promise<boolean> {
145
- await this.callApi('set_group_whole_mute', { group_id: groupId, is_mute: enable });
146
- return true;
147
- }
148
-
149
- async setAdmin(groupId: number, userId: number, enable = true): Promise<boolean> {
150
- await this.callApi('set_group_member_admin', {
151
- group_id: groupId,
152
- user_id: userId,
153
- is_set: enable,
154
- });
155
- return true;
156
- }
157
-
158
- async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
159
- await this.callApi('set_group_member_card', {
160
- group_id: groupId,
161
- user_id: userId,
162
- card,
163
- });
164
- return true;
165
- }
166
-
167
- async setTitle(groupId: number, userId: number, title: string): Promise<boolean> {
168
- await this.callApi('set_group_member_special_title', {
169
- group_id: groupId,
170
- user_id: userId,
171
- special_title: title,
172
- });
173
- return true;
174
- }
175
-
176
- async setGroupName(groupId: number, name: string): Promise<boolean> {
177
- await this.callApi('set_group_name', { group_id: groupId, new_group_name: name });
178
- return true;
179
- }
180
-
181
- async getMemberList(groupId: number): Promise<unknown[]> {
182
- return this.callApi('get_group_member_list', { group_id: groupId }) as Promise<unknown[]>;
183
- }
184
-
185
- async getGroupInfo(groupId: number): Promise<unknown> {
186
- return this.callApi('get_group_info', { group_id: groupId });
187
- }
188
-
189
- admit(event: MilkyEvent): void {
122
+ #admitRaw(event: MilkyEvent): void {
190
123
  const data = parseMessageReceiveData(event);
191
- if (!this.#open || !data) return;
124
+ if (!data) return;
192
125
  this.#admitMessage(data, event);
193
126
  }
194
127
 
195
- apiOptions(): { baseUrl: string; access_token?: string } {
196
- return {
197
- baseUrl: this.#options.config.baseUrl,
198
- access_token: this.#options.config.access_token,
199
- };
128
+ #warnPlatformEvent(error: unknown): void {
129
+ this.#logger.warn(formatCompact({ op: 'milky_platform_event_failed', error: error instanceof Error ? error.message : String(error) }));
200
130
  }
201
131
 
202
132
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
@@ -207,7 +137,7 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
207
137
  const audioUrl = extractInboundAudioUrl(data);
208
138
  const nickname = senderNickname(data);
209
139
  const mentioned = isMentioned(data, event.self_id);
210
- void this.#options.gateway.receive({
140
+ void this.emit('message.receive', {
211
141
  conversation,
212
142
  message: { conversation, id: formatInboundMessageId(data) },
213
143
  content,
@@ -249,18 +179,12 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
249
179
  response.end(JSON.stringify({ message: 'Unauthorized' }));
250
180
  return;
251
181
  }
252
- const raw = await readRequestBody(request);
253
- let event: MilkyEvent;
254
- try {
255
- event = JSON.parse(raw) as MilkyEvent;
256
- } catch {
257
- response.writeHead(400, { 'Content-Type': 'application/json' });
258
- response.end(JSON.stringify({ message: 'Invalid JSON' }));
182
+ if (!this.clientEventsOpen) {
183
+ response.writeHead(200, { 'Content-Type': 'application/json' });
184
+ response.end(JSON.stringify({ status: 'ok' }));
259
185
  return;
260
186
  }
261
- if (this.#open) this.admit(event);
262
- response.writeHead(200, { 'Content-Type': 'application/json' });
263
- response.end(JSON.stringify({ status: 'ok' }));
187
+ await this.client.acceptHttp(request, response);
264
188
  } catch (error) {
265
189
  this.#logger.error('Milky webhook error:', error);
266
190
  if (!response.headersSent) {