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