@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.
@@ -1,29 +1,36 @@
1
- import { createRecallEndpointControl, } from 'zhin.js/adapter';
1
+ import { ClientEndpoint, createRecallEndpointControl, } from 'zhin.js/adapter';
2
2
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
3
- import { readRequestBody, verifyMilkyAccessToken } from './milky-auth.js';
3
+ import { verifyMilkyAccessToken } from './milky-auth.js';
4
4
  import { createMilkyEndpointManagement } from './endpoint-management.js';
5
- import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
5
+ import { callMilkyClient, createMilkyEndpointClient, forwardMilkyClientEvents } from './client.js';
6
6
  import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
7
- export class MilkyWebhookEndpoint {
7
+ export class MilkyWebhookEndpoint extends ClientEndpoint {
8
+ client;
8
9
  #logger;
9
10
  #options;
10
11
  #callApi;
11
- management = createMilkyEndpointManagement(this);
12
+ management;
12
13
  control = createRecallEndpointControl((id) => this.recallMessage(id));
13
14
  #routeReleases = [];
14
- #open = false;
15
15
  #started = false;
16
- #unregisterAgent;
17
16
  constructor(options) {
17
+ super();
18
18
  this.#logger = getAdapterLogger('milky', options.config.id);
19
19
  this.#options = options;
20
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
+ });
25
+ this.bindClientEvents((receive) => forwardMilkyClientEvents(this.client, receive), (name, payload) => {
26
+ if (name === 'event')
27
+ this.#admitRaw(payload);
28
+ }, (_name, error) => this.#warnPlatformEvent(error));
21
29
  }
22
30
  async start() {
23
31
  if (this.#started)
24
32
  return;
25
33
  this.#started = true;
26
- this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
27
34
  this.#setupRoutes();
28
35
  this.#logger.info(formatCompact({
29
36
  op: 'listen',
@@ -32,25 +39,17 @@ export class MilkyWebhookEndpoint {
32
39
  path: this.#options.config.path,
33
40
  }));
34
41
  }
35
- open() {
36
- this.#open = true;
37
- }
38
- close() {
39
- this.#open = false;
40
- }
41
42
  async stop() {
42
- this.#open = false;
43
+ this.close();
43
44
  for (const release of this.#routeReleases.splice(0))
44
45
  release();
45
- this.#unregisterAgent?.();
46
- this.#unregisterAgent = undefined;
47
46
  this.#started = false;
48
47
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
49
48
  }
50
49
  async send({ conversation, payload }) {
51
50
  const message = formatOutboundSegments(payload);
52
51
  const { action, params } = buildSendAction(conversation, message);
53
- const data = await this.callApi(action, params);
52
+ const data = await callMilkyClient(this.client, action, params);
54
53
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
55
54
  this.#logger.debug(formatCompact({
56
55
  op: 'milky_send',
@@ -60,91 +59,31 @@ export class MilkyWebhookEndpoint {
60
59
  }));
61
60
  return messageId;
62
61
  }
63
- callApi(action, params = {}) {
64
- return this.#callApi(this.apiOptions(), action, params);
65
- }
66
62
  async recallMessage(id) {
67
63
  const parsed = parseMilkyMessageId(id);
68
64
  if (!parsed)
69
65
  throw new Error(`Invalid message id: ${id}`);
70
66
  if (parsed.message_scene === 'group') {
71
- await this.callApi('recall_group_message', {
67
+ await callMilkyClient(this.client, 'recall_group_message', {
72
68
  group_id: parsed.peer_id,
73
69
  message_seq: parsed.message_seq,
74
70
  });
75
71
  }
76
72
  else {
77
- await this.callApi('recall_private_message', {
73
+ await callMilkyClient(this.client, 'recall_private_message', {
78
74
  user_id: parsed.peer_id,
79
75
  message_seq: parsed.message_seq,
80
76
  });
81
77
  }
82
78
  }
83
- async kickMember(groupId, userId, rejectAddRequest = false) {
84
- await this.callApi('kick_group_member', {
85
- group_id: groupId,
86
- user_id: userId,
87
- reject_add_request: rejectAddRequest,
88
- });
89
- return true;
90
- }
91
- async muteMember(groupId, userId, duration = 600) {
92
- await this.callApi('set_group_member_mute', {
93
- group_id: groupId,
94
- user_id: userId,
95
- duration,
96
- });
97
- return true;
98
- }
99
- async muteAll(groupId, enable = true) {
100
- await this.callApi('set_group_whole_mute', { group_id: groupId, is_mute: enable });
101
- return true;
102
- }
103
- async setAdmin(groupId, userId, enable = true) {
104
- await this.callApi('set_group_member_admin', {
105
- group_id: groupId,
106
- user_id: userId,
107
- is_set: enable,
108
- });
109
- return true;
110
- }
111
- async setCard(groupId, userId, card) {
112
- await this.callApi('set_group_member_card', {
113
- group_id: groupId,
114
- user_id: userId,
115
- card,
116
- });
117
- return true;
118
- }
119
- async setTitle(groupId, userId, title) {
120
- await this.callApi('set_group_member_special_title', {
121
- group_id: groupId,
122
- user_id: userId,
123
- special_title: title,
124
- });
125
- return true;
126
- }
127
- async setGroupName(groupId, name) {
128
- await this.callApi('set_group_name', { group_id: groupId, new_group_name: name });
129
- return true;
130
- }
131
- async getMemberList(groupId) {
132
- return this.callApi('get_group_member_list', { group_id: groupId });
133
- }
134
- async getGroupInfo(groupId) {
135
- return this.callApi('get_group_info', { group_id: groupId });
136
- }
137
- admit(event) {
79
+ #admitRaw(event) {
138
80
  const data = parseMessageReceiveData(event);
139
- if (!this.#open || !data)
81
+ if (!data)
140
82
  return;
141
83
  this.#admitMessage(data, event);
142
84
  }
143
- apiOptions() {
144
- return {
145
- baseUrl: this.#options.config.baseUrl,
146
- access_token: this.#options.config.access_token,
147
- };
85
+ #warnPlatformEvent(error) {
86
+ this.#logger.warn(formatCompact({ op: 'milky_platform_event_failed', error: error instanceof Error ? error.message : String(error) }));
148
87
  }
149
88
  #admitMessage(data, event) {
150
89
  const conversation = milkyInboundConversation(String(this.#options.id), data);
@@ -154,7 +93,7 @@ export class MilkyWebhookEndpoint {
154
93
  const audioUrl = extractInboundAudioUrl(data);
155
94
  const nickname = senderNickname(data);
156
95
  const mentioned = isMentioned(data, event.self_id);
157
- void this.#options.gateway.receive({
96
+ void this.emit('message.receive', {
158
97
  conversation,
159
98
  message: { conversation, id: formatInboundMessageId(data) },
160
99
  content,
@@ -192,20 +131,12 @@ export class MilkyWebhookEndpoint {
192
131
  response.end(JSON.stringify({ message: 'Unauthorized' }));
193
132
  return;
194
133
  }
195
- const raw = await readRequestBody(request);
196
- let event;
197
- try {
198
- event = JSON.parse(raw);
199
- }
200
- catch {
201
- response.writeHead(400, { 'Content-Type': 'application/json' });
202
- response.end(JSON.stringify({ message: 'Invalid JSON' }));
134
+ if (!this.clientEventsOpen) {
135
+ response.writeHead(200, { 'Content-Type': 'application/json' });
136
+ response.end(JSON.stringify({ status: 'ok' }));
203
137
  return;
204
138
  }
205
- if (this.#open)
206
- this.admit(event);
207
- response.writeHead(200, { 'Content-Type': 'application/json' });
208
- response.end(JSON.stringify({ status: 'ok' }));
139
+ await this.client.acceptHttp(request, response);
209
140
  }
210
141
  catch (error) {
211
142
  this.#logger.error('Milky webhook error:', error);
@@ -1,42 +1,23 @@
1
- import { type EndpointControl, type EndpointInstance, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
2
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
1
+ import { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
3
2
  import type { CapabilityId } from 'zhin.js';
4
- import { callApi, type MilkyEvent, type MilkyWsConfig } from './protocol.js';
3
+ import { type MilkyClient } from './client.js';
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 gateway: MessageGateway;
9
- readonly sideEvents?: SideEventGateway;
10
8
  readonly config: MilkyWsConfig;
11
9
  readonly createWebSocket?: (url: string, options: MilkyWsCreateOptions) => MilkyWsSocket;
12
10
  readonly callApi?: typeof callApi;
13
11
  }
14
- export declare class MilkyWsEndpoint implements EndpointInstance {
12
+ export declare class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
15
13
  #private;
14
+ readonly client: MilkyClient;
16
15
  readonly management: EndpointManagement;
17
16
  readonly control: EndpointControl;
18
17
  constructor(options: MilkyWsEndpointOptions);
19
18
  start(): Promise<void>;
20
- open(): void;
21
19
  close(): void;
22
20
  stop(): Promise<void>;
23
21
  send({ conversation, payload }: EndpointSendRequest): Promise<string>;
24
- /** Public API for agent tools / callers. */
25
- callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
26
22
  recallMessage(id: string): Promise<void>;
27
- kickMember(groupId: number, userId: number, rejectAddRequest?: boolean): Promise<boolean>;
28
- muteMember(groupId: number, userId: number, duration?: number): Promise<boolean>;
29
- muteAll(groupId: number, enable?: boolean): Promise<boolean>;
30
- setAdmin(groupId: number, userId: number, enable?: boolean): Promise<boolean>;
31
- setCard(groupId: number, userId: number, card: string): Promise<boolean>;
32
- setTitle(groupId: number, userId: number, title: string): Promise<boolean>;
33
- setGroupName(groupId: number, name: string): Promise<boolean>;
34
- getMemberList(groupId: number): Promise<unknown[]>;
35
- getGroupInfo(groupId: number): Promise<unknown>;
36
- /** Test / internal: admit a parsed event when the endpoint is open. */
37
- admit(event: MilkyEvent): void;
38
- apiOptions(): {
39
- baseUrl: string;
40
- access_token?: string;
41
- };
42
23
  }
@@ -2,26 +2,35 @@
2
2
  * Milky WS client endpoint — outbound connect to Milky protocol server.
3
3
  */
4
4
  import WebSocket from 'ws';
5
- import { createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
5
+ import { ClientEndpoint, 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 { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
8
+ import { callMilkyClient, createMilkyEndpointClient, forwardMilkyClientEvents, } 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 {
11
+ export class MilkyWsEndpoint extends ClientEndpoint {
12
+ client;
12
13
  #logger;
13
14
  #options;
14
15
  #callApi;
15
- management = createMilkyEndpointManagement(this);
16
+ management;
16
17
  control = createRecallEndpointControl((id) => this.recallMessage(id));
17
18
  #lifecycle;
18
19
  #ws;
19
- #open = false;
20
- #unregisterAgent;
20
+ #clientSocketRelease;
21
21
  constructor(options) {
22
+ super();
22
23
  this.#logger = getAdapterLogger('milky', options.config.id);
23
24
  this.#options = options;
24
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),
29
+ });
30
+ this.bindClientEvents((receive) => forwardMilkyClientEvents(this.client, receive), (name, payload) => {
31
+ if (name === 'event')
32
+ this.#admitRaw(payload);
33
+ }, (_name, error) => this.#warnPlatformEvent(error));
25
34
  this.#lifecycle = createEndpointLifecycle({
26
35
  name: options.config.id,
27
36
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
@@ -36,28 +45,16 @@ export class MilkyWsEndpoint {
36
45
  async start() {
37
46
  if (this.#lifecycle.started)
38
47
  return;
39
- this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
40
- try {
41
- await this.#lifecycle.start((handle) => this.#connect(handle));
42
- }
43
- catch (err) {
44
- // start 失败复位由基座保证;agent 注册/反注册是适配器专有依赖,留在适配器侧
45
- this.#unregisterAgent?.();
46
- this.#unregisterAgent = undefined;
47
- throw err;
48
- }
49
- }
50
- open() {
51
- this.#open = true;
48
+ await this.#lifecycle.start((handle) => this.#connect(handle));
52
49
  }
53
50
  close() {
54
- this.#open = false;
51
+ super.close();
52
+ this.#clientSocketRelease?.();
53
+ this.#clientSocketRelease = undefined;
55
54
  }
56
55
  async stop() {
57
- this.#open = false;
56
+ this.close();
58
57
  await this.#lifecycle.stop();
59
- this.#unregisterAgent?.();
60
- this.#unregisterAgent = undefined;
61
58
  if (this.#ws) {
62
59
  try {
63
60
  this.#ws.close();
@@ -71,7 +68,7 @@ export class MilkyWsEndpoint {
71
68
  async send({ conversation, payload }) {
72
69
  const message = formatOutboundSegments(payload);
73
70
  const { action, params } = buildSendAction(conversation, message);
74
- const data = await this.callApi(action, params);
71
+ const data = await callMilkyClient(this.client, action, params);
75
72
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
76
73
  this.#logger.debug(formatCompact({
77
74
  op: 'milky_send',
@@ -81,93 +78,34 @@ export class MilkyWsEndpoint {
81
78
  }));
82
79
  return messageId;
83
80
  }
84
- /** Public API for agent tools / callers. */
85
- callApi(action, params = {}) {
86
- return this.#callApi(this.apiOptions(), action, params);
87
- }
88
81
  async recallMessage(id) {
89
82
  const parsed = parseMilkyMessageId(id);
90
83
  if (!parsed)
91
84
  throw new Error(`Invalid message id: ${id}`);
92
85
  if (parsed.message_scene === 'group') {
93
- await this.callApi('recall_group_message', {
86
+ await callMilkyClient(this.client, 'recall_group_message', {
94
87
  group_id: parsed.peer_id,
95
88
  message_seq: parsed.message_seq,
96
89
  });
97
90
  }
98
91
  else {
99
- await this.callApi('recall_private_message', {
92
+ await callMilkyClient(this.client, 'recall_private_message', {
100
93
  user_id: parsed.peer_id,
101
94
  message_seq: parsed.message_seq,
102
95
  });
103
96
  }
104
97
  }
105
- async kickMember(groupId, userId, rejectAddRequest = false) {
106
- await this.callApi('kick_group_member', {
107
- group_id: groupId,
108
- user_id: userId,
109
- reject_add_request: rejectAddRequest,
110
- });
111
- return true;
112
- }
113
- async muteMember(groupId, userId, duration = 600) {
114
- await this.callApi('set_group_member_mute', {
115
- group_id: groupId,
116
- user_id: userId,
117
- duration,
118
- });
119
- return true;
120
- }
121
- async muteAll(groupId, enable = true) {
122
- await this.callApi('set_group_whole_mute', { group_id: groupId, is_mute: enable });
123
- return true;
124
- }
125
- async setAdmin(groupId, userId, enable = true) {
126
- await this.callApi('set_group_member_admin', {
127
- group_id: groupId,
128
- user_id: userId,
129
- is_set: enable,
130
- });
131
- return true;
132
- }
133
- async setCard(groupId, userId, card) {
134
- await this.callApi('set_group_member_card', {
135
- group_id: groupId,
136
- user_id: userId,
137
- card,
138
- });
139
- return true;
140
- }
141
- async setTitle(groupId, userId, title) {
142
- await this.callApi('set_group_member_special_title', {
143
- group_id: groupId,
144
- user_id: userId,
145
- special_title: title,
146
- });
147
- return true;
148
- }
149
- async setGroupName(groupId, name) {
150
- await this.callApi('set_group_name', { group_id: groupId, new_group_name: name });
151
- return true;
152
- }
153
- async getMemberList(groupId) {
154
- return this.callApi('get_group_member_list', { group_id: groupId });
155
- }
156
- async getGroupInfo(groupId) {
157
- return this.callApi('get_group_info', { group_id: groupId });
158
- }
159
- /** Test / internal: admit a parsed event when the endpoint is open. */
160
- admit(event) {
98
+ #admitRaw(event) {
161
99
  const data = parseMessageReceiveData(event);
162
- if (!this.#open || !data)
100
+ if (!data)
163
101
  return;
164
102
  this.#admitMessage(data, event);
165
103
  }
166
- apiOptions() {
167
- return {
168
- baseUrl: this.#options.config.baseUrl,
169
- access_token: this.#options.config.access_token,
170
- };
104
+ #warnPlatformEvent(error) {
105
+ this.#logger.warn(formatCompact({
106
+ op: 'milky_platform_event_failed',
107
+ error: error instanceof Error ? error.message : String(error),
108
+ }));
171
109
  }
172
110
  #admitMessage(data, event) {
173
111
  const conversation = milkyInboundConversation(String(this.#options.id), data);
@@ -177,7 +115,7 @@ export class MilkyWsEndpoint {
177
115
  const audioUrl = extractInboundAudioUrl(data);
178
116
  const nickname = senderNickname(data);
179
117
  const mentioned = isMentioned(data, event.self_id);
180
- void this.#options.gateway.receive({
118
+ void this.emit('message.receive', {
181
119
  conversation,
182
120
  message: { conversation, id: formatInboundMessageId(data) },
183
121
  content,
@@ -248,10 +186,11 @@ export class MilkyWsEndpoint {
248
186
  }
249
187
  resolve();
250
188
  });
251
- ws.on('message', (data) => {
252
- this.#onMessage(data);
253
- });
189
+ this.#clientSocketRelease?.();
190
+ this.#clientSocketRelease = this.client.acceptWebSocket(ws);
254
191
  ws.on('close', (code, reason) => {
192
+ this.#clientSocketRelease?.();
193
+ this.#clientSocketRelease = undefined;
255
194
  const reasonStr = typeof reason === 'string'
256
195
  ? reason
257
196
  : Buffer.isBuffer(reason)
@@ -291,24 +230,4 @@ export class MilkyWsEndpoint {
291
230
  });
292
231
  });
293
232
  }
294
- #onMessage(data) {
295
- try {
296
- const raw = typeof data === 'string'
297
- ? data
298
- : Buffer.isBuffer(data)
299
- ? data.toString()
300
- : data instanceof ArrayBuffer
301
- ? new TextDecoder().decode(data)
302
- : String(data ?? '');
303
- const event = JSON.parse(raw);
304
- this.admit(event);
305
- }
306
- catch (error) {
307
- this.#logger.warn(formatCompact({
308
- op: 'milky_parse_failed',
309
- endpoint: this.#options.config.id,
310
- error: error instanceof Error ? error.message : String(error),
311
- }));
312
- }
313
- }
314
233
  }
@@ -1,40 +1,25 @@
1
- import { type EndpointControl, type EndpointInstance, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
2
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
1
+ /**
2
+ * Milky reverse WSS endpoint httpHostToken WS upgrade inbound + baseUrl HTTP API outbound.
3
+ */
4
+ import { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
3
5
  import type { HttpHost } from '@zhin.js/host-http';
4
6
  import type { CapabilityId } from 'zhin.js';
5
- import { callApi, type MilkyEvent, type MilkyWssConfig } from './protocol.js';
7
+ import { type MilkyClient } from './client.js';
8
+ import { callApi, type MilkyWssConfig } from './protocol.js';
6
9
  export interface MilkyWssEndpointOptions {
7
10
  readonly id: CapabilityId;
8
- readonly gateway: MessageGateway;
9
- readonly sideEvents?: SideEventGateway;
10
11
  readonly http: HttpHost;
11
12
  readonly config: MilkyWssConfig;
12
13
  readonly callApi?: typeof callApi;
13
14
  }
14
- export declare class MilkyWssEndpoint implements EndpointInstance {
15
+ export declare class MilkyWssEndpoint extends ClientEndpoint<MilkyClient> {
15
16
  #private;
17
+ readonly client: MilkyClient;
16
18
  readonly management: EndpointManagement;
17
19
  readonly control: EndpointControl;
18
20
  constructor(options: MilkyWssEndpointOptions);
19
21
  start(): Promise<void>;
20
- open(): void;
21
- close(): void;
22
22
  stop(): Promise<void>;
23
23
  send({ conversation, payload }: EndpointSendRequest): Promise<string>;
24
- callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
25
24
  recallMessage(id: string): Promise<void>;
26
- kickMember(groupId: number, userId: number, rejectAddRequest?: boolean): Promise<boolean>;
27
- muteMember(groupId: number, userId: number, duration?: number): Promise<boolean>;
28
- muteAll(groupId: number, enable?: boolean): Promise<boolean>;
29
- setAdmin(groupId: number, userId: number, enable?: boolean): Promise<boolean>;
30
- setCard(groupId: number, userId: number, card: string): Promise<boolean>;
31
- setTitle(groupId: number, userId: number, title: string): Promise<boolean>;
32
- setGroupName(groupId: number, name: string): Promise<boolean>;
33
- getMemberList(groupId: number): Promise<unknown[]>;
34
- getGroupInfo(groupId: number): Promise<unknown>;
35
- admit(event: MilkyEvent): void;
36
- apiOptions(): {
37
- baseUrl: string;
38
- access_token?: string;
39
- };
40
25
  }