@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.
@@ -1,37 +1,62 @@
1
1
  /**
2
2
  * Milky reverse WSS endpoint — httpHostToken WS upgrade inbound + baseUrl HTTP API outbound.
3
3
  */
4
- import { clearInterval } from 'node:timers';
4
+ import { ClientEndpoint, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
5
5
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
6
6
  import { verifyMilkyAccessToken } from './milky-auth.js';
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, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
10
10
  const WS_OPEN = 1;
11
- export class MilkyWssEndpoint {
11
+ export class MilkyWssEndpoint extends ClientEndpoint {
12
+ client;
12
13
  #logger;
13
14
  #options;
14
15
  #callApi;
15
- management = createMilkyEndpointManagement(this);
16
+ management;
17
+ control = createRecallEndpointControl((id) => this.recallMessage(id));
16
18
  #ws;
17
19
  #wsRelease;
18
- #heartbeatTimer;
19
- #open = false;
20
- #started = false;
21
- #unregisterAgent;
20
+ #clientSocketRelease;
21
+ #lifecycle;
22
22
  constructor(options) {
23
+ super();
23
24
  this.#logger = getAdapterLogger('milky', options.config.id);
24
25
  this.#options = options;
25
26
  this.#callApi = options.callApi ?? callApi;
27
+ this.#lifecycle = createEndpointLifecycle({
28
+ name: options.config.id,
29
+ reconnect: false,
30
+ heartbeat: { intervalMs: options.config.heartbeat_interval },
31
+ });
32
+ this.client = createMilkyEndpointClient(options.config, this.#callApi);
33
+ this.management = createMilkyEndpointManagement({
34
+ callApi: (action, params) => callMilkyClient(this.client, action, params),
35
+ });
36
+ this.bindClientEvents((receive) => forwardMilkyClientEvents(this.client, receive), (name, payload) => {
37
+ if (name === 'event')
38
+ this.#admitRaw(payload);
39
+ }, (_name, error) => this.#warnPlatformEvent(error));
26
40
  }
27
41
  async start() {
28
- if (this.#started)
29
- return;
30
- this.#started = true;
31
- this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
32
- const handle = this.#options.http.ws(this.#options.config.path);
33
- this.#wsRelease = handle.onConnection((connection) => {
34
- this.#acceptConnection(connection);
42
+ await this.#lifecycle.start(async (lifecycleHandle) => {
43
+ const handle = this.#options.http.ws(this.#options.config.path);
44
+ this.#wsRelease = handle.onConnection((connection) => {
45
+ this.#acceptConnection(connection);
46
+ });
47
+ lifecycleHandle.onForceClose(() => {
48
+ this.#wsRelease?.();
49
+ this.#wsRelease = undefined;
50
+ this.#clientSocketRelease?.();
51
+ this.#clientSocketRelease = undefined;
52
+ try {
53
+ this.#ws?.close();
54
+ }
55
+ catch {
56
+ /* ignore */
57
+ }
58
+ this.#ws = undefined;
59
+ });
35
60
  });
36
61
  this.#logger.info(formatCompact({
37
62
  op: 'listen',
@@ -40,37 +65,14 @@ export class MilkyWssEndpoint {
40
65
  path: this.#options.config.path,
41
66
  }));
42
67
  }
43
- open() {
44
- this.#open = true;
45
- }
46
- close() {
47
- this.#open = false;
48
- }
49
68
  async stop() {
50
- this.#open = false;
51
- this.#unregisterAgent?.();
52
- this.#unregisterAgent = undefined;
53
- this.#wsRelease?.();
54
- this.#wsRelease = undefined;
55
- if (this.#heartbeatTimer) {
56
- clearInterval(this.#heartbeatTimer);
57
- this.#heartbeatTimer = undefined;
58
- }
59
- if (this.#ws) {
60
- try {
61
- this.#ws.close();
62
- }
63
- catch {
64
- /* ignore */
65
- }
66
- this.#ws = undefined;
67
- }
68
- this.#started = false;
69
+ this.close();
70
+ await this.#lifecycle.stop();
69
71
  }
70
72
  async send({ conversation, payload }) {
71
73
  const message = formatOutboundSegments(payload);
72
74
  const { action, params } = buildSendAction(conversation, message);
73
- const data = await this.callApi(action, params);
75
+ const data = await callMilkyClient(this.client, action, params);
74
76
  const messageId = formatOutboundMessageId(conversation, data?.message_seq);
75
77
  this.#logger.debug(formatCompact({
76
78
  op: 'milky_send',
@@ -80,91 +82,31 @@ export class MilkyWssEndpoint {
80
82
  }));
81
83
  return messageId;
82
84
  }
83
- callApi(action, params = {}) {
84
- return this.#callApi(this.apiOptions(), action, params);
85
- }
86
85
  async recallMessage(id) {
87
86
  const parsed = parseMilkyMessageId(id);
88
87
  if (!parsed)
89
88
  throw new Error(`Invalid message id: ${id}`);
90
89
  if (parsed.message_scene === 'group') {
91
- await this.callApi('recall_group_message', {
90
+ await callMilkyClient(this.client, 'recall_group_message', {
92
91
  group_id: parsed.peer_id,
93
92
  message_seq: parsed.message_seq,
94
93
  });
95
94
  }
96
95
  else {
97
- await this.callApi('recall_private_message', {
96
+ await callMilkyClient(this.client, 'recall_private_message', {
98
97
  user_id: parsed.peer_id,
99
98
  message_seq: parsed.message_seq,
100
99
  });
101
100
  }
102
101
  }
103
- async kickMember(groupId, userId, rejectAddRequest = false) {
104
- await this.callApi('kick_group_member', {
105
- group_id: groupId,
106
- user_id: userId,
107
- reject_add_request: rejectAddRequest,
108
- });
109
- return true;
110
- }
111
- async muteMember(groupId, userId, duration = 600) {
112
- await this.callApi('set_group_member_mute', {
113
- group_id: groupId,
114
- user_id: userId,
115
- duration,
116
- });
117
- return true;
118
- }
119
- async muteAll(groupId, enable = true) {
120
- await this.callApi('set_group_whole_mute', { group_id: groupId, is_mute: enable });
121
- return true;
122
- }
123
- async setAdmin(groupId, userId, enable = true) {
124
- await this.callApi('set_group_member_admin', {
125
- group_id: groupId,
126
- user_id: userId,
127
- is_set: enable,
128
- });
129
- return true;
130
- }
131
- async setCard(groupId, userId, card) {
132
- await this.callApi('set_group_member_card', {
133
- group_id: groupId,
134
- user_id: userId,
135
- card,
136
- });
137
- return true;
138
- }
139
- async setTitle(groupId, userId, title) {
140
- await this.callApi('set_group_member_special_title', {
141
- group_id: groupId,
142
- user_id: userId,
143
- special_title: title,
144
- });
145
- return true;
146
- }
147
- async setGroupName(groupId, name) {
148
- await this.callApi('set_group_name', { group_id: groupId, new_group_name: name });
149
- return true;
150
- }
151
- async getMemberList(groupId) {
152
- return this.callApi('get_group_member_list', { group_id: groupId });
153
- }
154
- async getGroupInfo(groupId) {
155
- return this.callApi('get_group_info', { group_id: groupId });
156
- }
157
- admit(event) {
102
+ #admitRaw(event) {
158
103
  const data = parseMessageReceiveData(event);
159
- if (!this.#open || !data)
104
+ if (!data)
160
105
  return;
161
106
  this.#admitMessage(data, event);
162
107
  }
163
- apiOptions() {
164
- return {
165
- baseUrl: this.#options.config.baseUrl,
166
- access_token: this.#options.config.access_token,
167
- };
108
+ #warnPlatformEvent(error) {
109
+ this.#logger.warn(formatCompact({ op: 'milky_platform_event_failed', error: error instanceof Error ? error.message : String(error) }));
168
110
  }
169
111
  #admitMessage(data, event) {
170
112
  const conversation = milkyInboundConversation(String(this.#options.id), data);
@@ -174,7 +116,7 @@ export class MilkyWssEndpoint {
174
116
  const audioUrl = extractInboundAudioUrl(data);
175
117
  const nickname = senderNickname(data);
176
118
  const mentioned = isMentioned(data, event.self_id);
177
- void this.#options.gateway.receive({
119
+ void this.emit('message.receive', {
178
120
  conversation,
179
121
  message: { conversation, id: formatInboundMessageId(data) },
180
122
  content,
@@ -214,13 +156,24 @@ export class MilkyWssEndpoint {
214
156
  }
215
157
  }
216
158
  this.#ws = socket;
217
- this.#startHeartbeat();
218
- socket.on('message', (data) => {
219
- this.#onMessage(data);
159
+ this.#lifecycle.startHeartbeat(() => {
160
+ try {
161
+ if (this.#ws?.readyState === WS_OPEN)
162
+ this.#ws.ping?.();
163
+ }
164
+ catch {
165
+ /* ignore */
166
+ }
220
167
  });
168
+ this.#clientSocketRelease?.();
169
+ this.#clientSocketRelease = this.client.acceptWebSocket(socket);
221
170
  socket.on('close', () => {
222
- if (this.#ws === socket)
171
+ this.#clientSocketRelease?.();
172
+ this.#clientSocketRelease = undefined;
173
+ if (this.#ws === socket) {
223
174
  this.#ws = undefined;
175
+ this.#lifecycle.stopHeartbeat();
176
+ }
224
177
  });
225
178
  this.#logger.debug(formatCompact({
226
179
  endpoint: this.#options.config.id,
@@ -228,40 +181,4 @@ export class MilkyWssEndpoint {
228
181
  peer: connection.request.socket.remoteAddress,
229
182
  }));
230
183
  }
231
- #onMessage(data) {
232
- try {
233
- const raw = typeof data === 'string'
234
- ? data
235
- : Buffer.isBuffer(data)
236
- ? data.toString()
237
- : data instanceof ArrayBuffer
238
- ? new TextDecoder().decode(data)
239
- : String(data ?? '');
240
- const event = JSON.parse(raw);
241
- this.admit(event);
242
- }
243
- catch (error) {
244
- this.#logger.warn(formatCompact({
245
- op: 'milky_parse_failed',
246
- endpoint: this.#options.config.id,
247
- error: error instanceof Error ? error.message : String(error),
248
- }));
249
- }
250
- }
251
- #startHeartbeat() {
252
- if (this.#heartbeatTimer)
253
- clearInterval(this.#heartbeatTimer);
254
- const interval = this.#options.config.heartbeat_interval;
255
- if (interval <= 0)
256
- return;
257
- this.#heartbeatTimer = setInterval(() => {
258
- try {
259
- if (this.#ws?.readyState === WS_OPEN)
260
- this.#ws.ping?.();
261
- }
262
- catch {
263
- /* ignore */
264
- }
265
- }, interval);
266
- }
267
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-milky",
3
- "version": "6.0.14",
3
+ "version": "7.0.1",
4
4
  "description": "Zhin.js Milky adapter for Plugin Runtime (WebSocket client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -39,12 +39,15 @@
39
39
  "directory": "plugins/adapters/milky"
40
40
  },
41
41
  "dependencies": {
42
+ "@imhelper/milky-v1": "1.0.7",
43
+ "imhelper": "1.0.7",
42
44
  "ws": "^8.21.1",
43
- "@zhin.js/adapter": "1.1.11",
44
- "@zhin.js/core": "1.5.12",
45
- "@zhin.js/host-http": "1.0.11",
45
+ "@zhin.js/adapter": "1.2.1",
46
+ "@zhin.js/core": "1.5.14",
47
+ "@zhin.js/feature-kit": "1.0.13",
48
+ "@zhin.js/host-http": "1.0.13",
46
49
  "@zhin.js/im-contract": "1.0.4",
47
- "@zhin.js/logger": "1.0.76"
50
+ "@zhin.js/logger": "1.0.77"
48
51
  },
49
52
  "devDependencies": {
50
53
  "@types/node": "^26.1.2",
@@ -52,17 +55,17 @@
52
55
  "typescript": "^6.0.3",
53
56
  "vitest": "^4.1.10",
54
57
  "zod": "^4.4.3",
55
- "@zhin.js/agent": "1.1.14",
56
- "@zhin.js/host-http": "1.0.11",
57
- "zhin.js": "6.0.12"
58
+ "@zhin.js/agent": "1.1.16",
59
+ "@zhin.js/host-http": "1.0.13",
60
+ "zhin.js": "6.0.14"
58
61
  },
59
62
  "peerDependencies": {
60
63
  "zod": "^4.0.0",
61
- "@zhin.js/adapter": "1.1.11",
62
- "@zhin.js/agent": "1.1.14",
63
- "@zhin.js/command": "1.0.15",
64
- "@zhin.js/core": "1.5.12",
65
- "zhin.js": "6.0.12"
64
+ "@zhin.js/adapter": "1.2.1",
65
+ "@zhin.js/agent": "1.1.16",
66
+ "@zhin.js/command": "1.0.16",
67
+ "@zhin.js/core": "1.5.14",
68
+ "zhin.js": "6.0.14"
66
69
  },
67
70
  "peerDependenciesMeta": {
68
71
  "@zhin.js/agent": {
package/src/client.ts ADDED
@@ -0,0 +1,80 @@
1
+ import {
2
+ MilkyV1Client,
3
+ type MilkyV1Event,
4
+ type MilkyV1Response,
5
+ } from '@imhelper/milky-v1';
6
+ import {
7
+ EventFactory,
8
+ type EventMap,
9
+ type ImHelperEventMap,
10
+ } from 'imhelper';
11
+ import {
12
+ defineEndpointClient,
13
+ forwardEndpointClientEvents,
14
+ type ClientEventPayloads,
15
+ } from 'zhin.js/adapter';
16
+ import type { ResolvedMilkyConfig, callApi } from './protocol.js';
17
+
18
+ export { MilkyV1Client as MilkyClient } from '@imhelper/milky-v1';
19
+
20
+ type MilkyClientEvents = ImHelperEventMap<string, MilkyV1Event, EventMap<string>>;
21
+ export type MilkyClientEventMap = ClientEventPayloads<MilkyClientEvents>;
22
+
23
+ /** Construct the exact public imhelper Client without handing it transport ownership. */
24
+ export function createMilkyEndpointClient(
25
+ config: ResolvedMilkyConfig,
26
+ request: typeof callApi,
27
+ ): MilkyV1Client {
28
+ return new MilkyV1Client({
29
+ baseUrl: config.baseUrl,
30
+ selfId: config.id,
31
+ accessToken: config.access_token,
32
+ receiveMode: 'manual',
33
+ call: (action, params) => request(
34
+ {
35
+ baseUrl: config.baseUrl,
36
+ access_token: config.access_token,
37
+ },
38
+ action,
39
+ params,
40
+ ),
41
+ });
42
+ }
43
+
44
+ /** Use the Client's complete protocol response while keeping Endpoint internals data-oriented. */
45
+ export async function callMilkyClient<T = unknown>(
46
+ client: MilkyV1Client,
47
+ action: string,
48
+ params?: Record<string, unknown>,
49
+ ): Promise<T | undefined> {
50
+ const response: MilkyV1Response<T> = await client.call<T>(action, params);
51
+ if (response.status !== 'ok') {
52
+ throw new Error(
53
+ `Milky API ${action}: retcode=${response.retcode}${response.message ? ` ${response.message}` : ''}`,
54
+ );
55
+ }
56
+ return response.data;
57
+ }
58
+
59
+ const milkyClientEventNames = Object.freeze([
60
+ ...EventFactory.getSupportedEventTypes<string>(),
61
+ 'event',
62
+ ]);
63
+
64
+ export function forwardMilkyClientEvents(
65
+ client: MilkyV1Client,
66
+ receive: (name: string, payload: unknown) => void,
67
+ ): () => void {
68
+ return forwardEndpointClientEvents(client, milkyClientEventNames, receive);
69
+ }
70
+
71
+ declare module '@zhin.js/feature-kit' {
72
+ interface AdapterClientRegistry {
73
+ readonly milky: {
74
+ readonly client: MilkyV1Client;
75
+ readonly events: MilkyClientEventMap;
76
+ };
77
+ }
78
+ }
79
+
80
+ export const milkyClient = defineEndpointClient<MilkyV1Client, MilkyClientEventMap>('milky');
package/src/endpoint.ts CHANGED
@@ -21,6 +21,6 @@ export {
21
21
 
22
22
  export type { MilkyWsSocket, MilkyWsCreateOptions } from './ws-types.js';
23
23
 
24
- export { verifyMilkyAccessToken, readRequestBody } from './milky-auth.js';
24
+ export { verifyMilkyAccessToken } from './milky-auth.js';
25
25
 
26
26
  export { openSseStream, consumeSseBuffer } from './sse-client.js';
package/src/index.ts CHANGED
@@ -36,7 +36,6 @@ export {
36
36
  MilkyWsEndpoint,
37
37
  consumeSseBuffer,
38
38
  openSseStream,
39
- readRequestBody,
40
39
  verifyMilkyAccessToken,
41
40
  type CreateMilkySseStream,
42
41
  type MilkySseEndpointOptions,
@@ -48,9 +47,20 @@ export {
48
47
  } from './endpoint.js';
49
48
 
50
49
  export {
51
- getMilkyAgentDeps,
52
- registerMilkyAgentEndpoint,
53
- setMilkyAgentDeps,
54
- type MilkyAgentDeps,
55
- type MilkyAgentEndpoint,
56
- } from './milky-agent-deps.js';
50
+ MilkyClient,
51
+ milkyClient,
52
+ type MilkyClientEventMap,
53
+ } from './client.js';
54
+
55
+ export type {
56
+ MilkyAdapterConfig as ImHelperMilkyAdapterConfig,
57
+ MilkyActionUrlResolver,
58
+ MilkyCall,
59
+ MilkyMessageReceiveEvent,
60
+ MilkyMessageRecallEvent,
61
+ MilkyV1ClientConfig,
62
+ MilkyV1Event,
63
+ MilkyV1Response,
64
+ } from '@imhelper/milky-v1';
65
+ export { ProtocolError } from '@imhelper/milky-v1';
66
+ export type { ProtocolErrorKind, ProtocolErrorOptions } from 'imhelper';
package/src/milky-auth.ts CHANGED
@@ -12,18 +12,3 @@ export function verifyMilkyAccessToken(accessToken: string | undefined, request:
12
12
  }
13
13
  return false;
14
14
  }
15
-
16
- export async function readRequestBody(request: IncomingMessage): Promise<string> {
17
- const chunks: Buffer[] = [];
18
- let size = 0;
19
- for await (const chunk of request) {
20
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
21
- size += buffer.length;
22
- if (size > 1_048_576) {
23
- request.destroy();
24
- throw new Error('Request body exceeds 1MB');
25
- }
26
- chunks.push(buffer);
27
- }
28
- return Buffer.concat(chunks).toString('utf8');
29
- }
package/src/protocol.ts CHANGED
@@ -77,7 +77,7 @@ export type ResolvedMilkyConfig =
77
77
  export type MilkyEndpointConfig = ResolvedMilkyConfig;
78
78
 
79
79
  export interface MilkyApiResponse<T = unknown> {
80
- status: string;
80
+ status: 'ok' | 'failed';
81
81
  retcode: number;
82
82
  data?: T;
83
83
  message?: string;
@@ -203,13 +203,13 @@ function authQuery(access_token?: string): string {
203
203
 
204
204
  /**
205
205
  * 调用协议端 API:POST {baseUrl}/api/{apiName},Body JSON,鉴权。
206
- * 200 retcode !== 0 时抛错。
206
+ * HTTP 层错误抛出;协议响应原样返回,由 Client 公开 call() 保留完整语义。
207
207
  */
208
208
  export async function callApi<T = unknown>(
209
209
  options: MilkyApiClientOptions,
210
210
  apiName: string,
211
211
  params: Record<string, unknown> = {},
212
- ): Promise<T> {
212
+ ): Promise<MilkyApiResponse<T>> {
213
213
  const { baseUrl, access_token } = options;
214
214
  const url = new URL(`/api/${apiName}`, baseUrl.replace(/\/$/, ''));
215
215
  const q = authQuery(access_token);
@@ -235,10 +235,7 @@ export async function callApi<T = unknown>(
235
235
  if (res.status !== 200) {
236
236
  throw new Error(`Milky API ${apiName}: HTTP ${res.status} ${body.message ?? text}`);
237
237
  }
238
- if (body.retcode !== 0) {
239
- throw new Error(`Milky API ${apiName}: retcode=${body.retcode} ${body.message ?? ''}`);
240
- }
241
- return (body.data ?? {}) as T;
238
+ return body;
242
239
  }
243
240
 
244
241
  /** 根据 event_type 判断是否为 message_receive,并解析 data */