@zhin.js/adapter-milky 3.0.1 → 4.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +443 -0
  2. package/README.md +36 -117
  3. package/adapters/milky.ts +68 -0
  4. package/lib/endpoint.d.ts +7 -0
  5. package/lib/endpoint.js +6 -0
  6. package/lib/index.d.ts +3 -20
  7. package/lib/index.js +3 -35
  8. package/lib/milky-agent-deps.d.ts +24 -0
  9. package/lib/milky-agent-deps.js +30 -0
  10. package/lib/milky-auth.d.ts +3 -0
  11. package/lib/milky-auth.js +30 -0
  12. package/lib/protocol.d.ts +162 -0
  13. package/lib/protocol.js +337 -0
  14. package/lib/sse-client.d.ts +20 -0
  15. package/lib/sse-client.js +85 -0
  16. package/lib/sse-endpoint.d.ts +50 -0
  17. package/lib/sse-endpoint.js +261 -0
  18. package/lib/webhook-endpoint.d.ts +40 -0
  19. package/lib/webhook-endpoint.js +212 -0
  20. package/lib/ws-endpoint.d.ts +42 -0
  21. package/lib/ws-endpoint.js +320 -0
  22. package/lib/ws-types.d.ts +11 -0
  23. package/lib/ws-types.js +1 -0
  24. package/lib/wss-endpoint.d.ts +40 -0
  25. package/lib/wss-endpoint.js +263 -0
  26. package/package.json +52 -19
  27. package/plugin.ts +8 -0
  28. package/schema.json +36 -0
  29. package/src/endpoint.ts +26 -0
  30. package/src/index.ts +59 -48
  31. package/src/milky-agent-deps.ts +53 -0
  32. package/src/milky-auth.ts +29 -0
  33. package/src/protocol.ts +491 -0
  34. package/src/sse-client.ts +106 -0
  35. package/src/sse-endpoint.ts +318 -0
  36. package/src/webhook-endpoint.ts +262 -0
  37. package/src/ws-endpoint.ts +373 -0
  38. package/src/ws-types.ts +12 -0
  39. package/src/wss-endpoint.ts +305 -0
  40. package/lib/adapter.d.ts +0 -37
  41. package/lib/adapter.d.ts.map +0 -1
  42. package/lib/adapter.js +0 -99
  43. package/lib/adapter.js.map +0 -1
  44. package/lib/api.d.ts +0 -10
  45. package/lib/api.d.ts.map +0 -1
  46. package/lib/api.js +0 -48
  47. package/lib/api.js.map +0 -1
  48. package/lib/endpoint-sse.d.ts +0 -31
  49. package/lib/endpoint-sse.d.ts.map +0 -1
  50. package/lib/endpoint-sse.js +0 -197
  51. package/lib/endpoint-sse.js.map +0 -1
  52. package/lib/endpoint-webhook.d.ts +0 -34
  53. package/lib/endpoint-webhook.d.ts.map +0 -1
  54. package/lib/endpoint-webhook.js +0 -189
  55. package/lib/endpoint-webhook.js.map +0 -1
  56. package/lib/endpoint-ws.d.ts +0 -35
  57. package/lib/endpoint-ws.d.ts.map +0 -1
  58. package/lib/endpoint-ws.js +0 -264
  59. package/lib/endpoint-ws.js.map +0 -1
  60. package/lib/endpoint-wss.d.ts +0 -34
  61. package/lib/endpoint-wss.d.ts.map +0 -1
  62. package/lib/endpoint-wss.js +0 -227
  63. package/lib/endpoint-wss.js.map +0 -1
  64. package/lib/index.d.ts.map +0 -1
  65. package/lib/index.js.map +0 -1
  66. package/lib/segment-mapper.d.ts +0 -2
  67. package/lib/segment-mapper.d.ts.map +0 -1
  68. package/lib/segment-mapper.js +0 -2
  69. package/lib/segment-mapper.js.map +0 -1
  70. package/lib/types.d.ts +0 -75
  71. package/lib/types.d.ts.map +0 -1
  72. package/lib/types.js +0 -5
  73. package/lib/types.js.map +0 -1
  74. package/lib/utils.d.ts +0 -35
  75. package/lib/utils.d.ts.map +0 -1
  76. package/lib/utils.js +0 -166
  77. package/lib/utils.js.map +0 -1
  78. package/plugin.yml +0 -3
  79. package/src/adapter.ts +0 -111
  80. package/src/api.ts +0 -63
  81. package/src/endpoint-sse.ts +0 -232
  82. package/src/endpoint-webhook.ts +0 -225
  83. package/src/endpoint-ws.ts +0 -300
  84. package/src/endpoint-wss.ts +0 -272
  85. package/src/segment-mapper.ts +0 -1
  86. package/src/types.ts +0 -73
  87. package/src/utils.ts +0 -182
  88. /package/{skills/milky → agent}/PERMITS.md +0 -0
  89. /package/{skills/milky/SKILL.md → agent/skills/milky.md} +0 -0
@@ -0,0 +1,42 @@
1
+ import type { EndpointInstance } from '@zhin.js/adapter';
2
+ import type { MessageGateway } from '@zhin.js/core/runtime';
3
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
4
+ import { callApi, type MilkyEvent, type MilkyWsConfig } from './protocol.js';
5
+ import type { MilkyWsCreateOptions, MilkyWsSocket } from './ws-types.js';
6
+ export interface MilkyWsEndpointOptions {
7
+ readonly id: CapabilityId;
8
+ readonly gateway: MessageGateway;
9
+ readonly config: MilkyWsConfig;
10
+ readonly createWebSocket?: (url: string, options: MilkyWsCreateOptions) => MilkyWsSocket;
11
+ readonly callApi?: typeof callApi;
12
+ }
13
+ export declare class MilkyWsEndpoint implements EndpointInstance {
14
+ #private;
15
+ constructor(options: MilkyWsEndpointOptions);
16
+ start(): Promise<void>;
17
+ open(): void;
18
+ close(): void;
19
+ stop(): Promise<void>;
20
+ send({ target, payload }: {
21
+ readonly target: string;
22
+ readonly payload: unknown;
23
+ }): Promise<string>;
24
+ /** Public API for agent tools / callers. */
25
+ callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
26
+ 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
+ }
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Milky WS client endpoint — outbound connect to Milky protocol server.
3
+ */
4
+ import WebSocket from 'ws';
5
+ import { clearInterval, clearTimeout } from 'node:timers';
6
+ import { formatCompact, getLogger } from '@zhin.js/logger';
7
+ import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
8
+ import { buildSendAction, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
9
+ const logger = getLogger('milky');
10
+ const WS_OPEN = 1;
11
+ export class MilkyWsEndpoint {
12
+ #options;
13
+ #callApi;
14
+ #ws;
15
+ #reconnectTimer;
16
+ #heartbeatTimer;
17
+ #open = false;
18
+ #started = false;
19
+ #stopping = false;
20
+ #unregisterAgent;
21
+ constructor(options) {
22
+ this.#options = options;
23
+ this.#callApi = options.callApi ?? callApi;
24
+ }
25
+ async start() {
26
+ if (this.#started)
27
+ return;
28
+ this.#started = true;
29
+ this.#stopping = false;
30
+ this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.name, this);
31
+ await this.#connect();
32
+ }
33
+ open() {
34
+ this.#open = true;
35
+ }
36
+ close() {
37
+ this.#open = false;
38
+ }
39
+ async stop() {
40
+ this.#open = false;
41
+ this.#stopping = true;
42
+ this.#started = false;
43
+ this.#unregisterAgent?.();
44
+ this.#unregisterAgent = undefined;
45
+ if (this.#reconnectTimer) {
46
+ clearTimeout(this.#reconnectTimer);
47
+ this.#reconnectTimer = undefined;
48
+ }
49
+ if (this.#heartbeatTimer) {
50
+ clearInterval(this.#heartbeatTimer);
51
+ this.#heartbeatTimer = undefined;
52
+ }
53
+ if (this.#ws) {
54
+ try {
55
+ this.#ws.close();
56
+ }
57
+ catch {
58
+ /* ignore */
59
+ }
60
+ this.#ws = undefined;
61
+ }
62
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
63
+ }
64
+ async send({ target, payload }) {
65
+ const message = formatOutboundSegments(payload);
66
+ const { action, params } = buildSendAction(target, message);
67
+ const data = await this.callApi(action, params);
68
+ const messageId = formatOutboundMessageId(target, data?.message_seq);
69
+ logger.debug(formatCompact({
70
+ op: 'milky_send',
71
+ endpoint: this.#options.config.name,
72
+ target,
73
+ messageId,
74
+ }));
75
+ return messageId;
76
+ }
77
+ /** Public API for agent tools / callers. */
78
+ callApi(action, params = {}) {
79
+ return this.#callApi(this.apiOptions(), action, params);
80
+ }
81
+ async recallMessage(id) {
82
+ const parsed = parseMilkyMessageId(id);
83
+ if (!parsed)
84
+ throw new Error(`Invalid message id: ${id}`);
85
+ if (parsed.message_scene === 'group') {
86
+ await this.callApi('recall_group_message', {
87
+ group_id: parsed.peer_id,
88
+ message_seq: parsed.message_seq,
89
+ });
90
+ }
91
+ else {
92
+ await this.callApi('recall_private_message', {
93
+ user_id: parsed.peer_id,
94
+ message_seq: parsed.message_seq,
95
+ });
96
+ }
97
+ }
98
+ async kickMember(groupId, userId, rejectAddRequest = false) {
99
+ await this.callApi('kick_group_member', {
100
+ group_id: groupId,
101
+ user_id: userId,
102
+ reject_add_request: rejectAddRequest,
103
+ });
104
+ return true;
105
+ }
106
+ async muteMember(groupId, userId, duration = 600) {
107
+ await this.callApi('set_group_member_mute', {
108
+ group_id: groupId,
109
+ user_id: userId,
110
+ duration,
111
+ });
112
+ return true;
113
+ }
114
+ async muteAll(groupId, enable = true) {
115
+ await this.callApi('set_group_whole_mute', { group_id: groupId, is_mute: enable });
116
+ return true;
117
+ }
118
+ async setAdmin(groupId, userId, enable = true) {
119
+ await this.callApi('set_group_member_admin', {
120
+ group_id: groupId,
121
+ user_id: userId,
122
+ is_set: enable,
123
+ });
124
+ return true;
125
+ }
126
+ async setCard(groupId, userId, card) {
127
+ await this.callApi('set_group_member_card', {
128
+ group_id: groupId,
129
+ user_id: userId,
130
+ card,
131
+ });
132
+ return true;
133
+ }
134
+ async setTitle(groupId, userId, title) {
135
+ await this.callApi('set_group_member_special_title', {
136
+ group_id: groupId,
137
+ user_id: userId,
138
+ special_title: title,
139
+ });
140
+ return true;
141
+ }
142
+ async setGroupName(groupId, name) {
143
+ await this.callApi('set_group_name', { group_id: groupId, new_group_name: name });
144
+ return true;
145
+ }
146
+ async getMemberList(groupId) {
147
+ return this.callApi('get_group_member_list', { group_id: groupId });
148
+ }
149
+ async getGroupInfo(groupId) {
150
+ return this.callApi('get_group_info', { group_id: groupId });
151
+ }
152
+ /** Test / internal: admit a parsed event when the endpoint is open. */
153
+ admit(event) {
154
+ const data = parseMessageReceiveData(event);
155
+ if (!this.#open || !data)
156
+ return;
157
+ this.#admitMessage(data, event);
158
+ }
159
+ apiOptions() {
160
+ return {
161
+ baseUrl: this.#options.config.baseUrl,
162
+ access_token: this.#options.config.access_token,
163
+ };
164
+ }
165
+ #admitMessage(data, event) {
166
+ const target = formatInboundTarget(data);
167
+ const content = formatInboundContent(data);
168
+ const audioUrl = extractInboundAudioUrl(data);
169
+ const nickname = senderNickname(data);
170
+ const mentioned = isMentioned(data, event.self_id);
171
+ void this.#options.gateway.receive({
172
+ adapter: this.#options.id,
173
+ target,
174
+ content,
175
+ sender: String(data.sender_id),
176
+ id: formatInboundMessageId(data),
177
+ metadata: Object.freeze({
178
+ message_scene: data.message_scene,
179
+ peer_id: String(data.peer_id),
180
+ sender_id: String(data.sender_id),
181
+ message_seq: data.message_seq,
182
+ endpoint: this.#options.config.name,
183
+ time: data.time ?? event.time,
184
+ self_id: event.self_id != null ? String(event.self_id) : undefined,
185
+ ...(nickname ? { nickname } : {}),
186
+ ...(mentioned ? { mentioned: true } : {}),
187
+ ...(audioUrl ? { audio_url: audioUrl } : {}),
188
+ }),
189
+ }).catch((err) => {
190
+ logger.warn(formatCompact({
191
+ op: 'milky_gateway_receive_failed',
192
+ target,
193
+ error: err instanceof Error ? err.message : String(err),
194
+ }));
195
+ });
196
+ }
197
+ async #connect() {
198
+ const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
199
+ const create = this.#options.createWebSocket
200
+ ?? ((connectUrl, options) => new WebSocket(connectUrl, { headers: options.headers }));
201
+ await new Promise((resolve, reject) => {
202
+ let settled = false;
203
+ const ws = create(url, { headers });
204
+ this.#ws = ws;
205
+ ws.on('open', () => {
206
+ if (settled)
207
+ return;
208
+ settled = true;
209
+ if (!this.#options.config.access_token) {
210
+ logger.warn(formatCompact({
211
+ endpoint: this.#options.config.name,
212
+ ok: false,
213
+ error: 'missing access_token',
214
+ }));
215
+ }
216
+ logger.debug(formatCompact({
217
+ endpoint: this.#options.config.name,
218
+ mode: 'ws',
219
+ url: safeUrl,
220
+ }));
221
+ this.#startHeartbeat();
222
+ resolve();
223
+ });
224
+ ws.on('message', (data) => {
225
+ this.#onMessage(data);
226
+ });
227
+ ws.on('close', (code, reason) => {
228
+ const reasonStr = typeof reason === 'string'
229
+ ? reason
230
+ : Buffer.isBuffer(reason)
231
+ ? reason.toString()
232
+ : String(reason ?? '');
233
+ const codeNum = typeof code === 'number' ? code : Number(code ?? 0);
234
+ const codeHint = codeNum === 1005
235
+ ? ' [无状态,多为服务端/代理未发 close 帧即断开]'
236
+ : codeNum === 1006
237
+ ? ' [异常关闭]'
238
+ : '';
239
+ logger.warn(formatCompact({
240
+ op: 'disconnect',
241
+ endpoint: this.#options.config.name,
242
+ code: codeNum,
243
+ error: `${reasonStr || 'closed'}${codeHint}`,
244
+ reconnect_ms: this.#options.config.reconnect_interval,
245
+ }));
246
+ if (!settled) {
247
+ settled = true;
248
+ reject(new Error(`Milky WS 关闭: ${codeNum} ${reasonStr}`));
249
+ }
250
+ this.#scheduleReconnect();
251
+ });
252
+ ws.on('error', (err) => {
253
+ const error = err instanceof Error ? err : new Error(String(err));
254
+ logger.warn(formatCompact({
255
+ op: 'ws_error',
256
+ endpoint: this.#options.config.name,
257
+ ok: false,
258
+ error: error.message,
259
+ }));
260
+ if (!settled) {
261
+ settled = true;
262
+ reject(error);
263
+ }
264
+ });
265
+ });
266
+ }
267
+ #onMessage(data) {
268
+ try {
269
+ const raw = typeof data === 'string'
270
+ ? data
271
+ : Buffer.isBuffer(data)
272
+ ? data.toString()
273
+ : data instanceof ArrayBuffer
274
+ ? new TextDecoder().decode(data)
275
+ : String(data ?? '');
276
+ const event = JSON.parse(raw);
277
+ this.admit(event);
278
+ }
279
+ catch (error) {
280
+ logger.warn(formatCompact({
281
+ op: 'milky_parse_failed',
282
+ endpoint: this.#options.config.name,
283
+ error: error instanceof Error ? error.message : String(error),
284
+ }));
285
+ }
286
+ }
287
+ #startHeartbeat() {
288
+ if (this.#heartbeatTimer) {
289
+ clearInterval(this.#heartbeatTimer);
290
+ }
291
+ const interval = this.#options.config.heartbeat_interval;
292
+ if (interval <= 0)
293
+ return;
294
+ this.#heartbeatTimer = setInterval(() => {
295
+ try {
296
+ if (this.#ws?.readyState === WS_OPEN)
297
+ this.#ws.ping?.();
298
+ }
299
+ catch {
300
+ /* ignore */
301
+ }
302
+ }, interval);
303
+ }
304
+ #scheduleReconnect() {
305
+ if (this.#stopping || !this.#started || this.#reconnectTimer)
306
+ return;
307
+ const delay = this.#options.config.reconnect_interval;
308
+ this.#reconnectTimer = setTimeout(() => {
309
+ this.#reconnectTimer = undefined;
310
+ void this.#connect().catch((err) => {
311
+ logger.warn(formatCompact({
312
+ op: 'reconnect',
313
+ endpoint: this.#options.config.name,
314
+ ok: false,
315
+ error: err instanceof Error ? err.message : String(err),
316
+ }));
317
+ });
318
+ }, delay);
319
+ }
320
+ }
@@ -0,0 +1,11 @@
1
+ /** Minimal WS surface used by the endpoint (real `ws` or test mock). */
2
+ export interface MilkyWsSocket {
3
+ readonly readyState: number;
4
+ send?(data: string): void;
5
+ close(code?: number, reason?: string): void;
6
+ ping?(): void;
7
+ on(event: 'open' | 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
8
+ }
9
+ export interface MilkyWsCreateOptions {
10
+ readonly headers?: Record<string, string>;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import type { EndpointInstance } from '@zhin.js/adapter';
2
+ import type { MessageGateway } from '@zhin.js/core/runtime';
3
+ import type { HttpHost } from '@zhin.js/host-http';
4
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
5
+ import { callApi, type MilkyEvent, type MilkyWssConfig } from './protocol.js';
6
+ export interface MilkyWssEndpointOptions {
7
+ readonly id: CapabilityId;
8
+ readonly gateway: MessageGateway;
9
+ readonly http: HttpHost;
10
+ readonly config: MilkyWssConfig;
11
+ readonly callApi?: typeof callApi;
12
+ }
13
+ export declare class MilkyWssEndpoint implements EndpointInstance {
14
+ #private;
15
+ constructor(options: MilkyWssEndpointOptions);
16
+ start(): Promise<void>;
17
+ open(): void;
18
+ close(): void;
19
+ stop(): Promise<void>;
20
+ send({ target, payload }: {
21
+ readonly target: string;
22
+ readonly payload: unknown;
23
+ }): Promise<string>;
24
+ callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
25
+ 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
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Milky reverse WSS endpoint — httpHostToken WS upgrade inbound + baseUrl HTTP API outbound.
3
+ */
4
+ import { clearInterval } from 'node:timers';
5
+ import { formatCompact, getLogger } from '@zhin.js/logger';
6
+ import { verifyMilkyAccessToken } from './milky-auth.js';
7
+ import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
8
+ import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
9
+ const logger = getLogger('milky');
10
+ const WS_OPEN = 1;
11
+ export class MilkyWssEndpoint {
12
+ #options;
13
+ #callApi;
14
+ #ws;
15
+ #wsRelease;
16
+ #heartbeatTimer;
17
+ #open = false;
18
+ #started = false;
19
+ #unregisterAgent;
20
+ constructor(options) {
21
+ this.#options = options;
22
+ this.#callApi = options.callApi ?? callApi;
23
+ }
24
+ async start() {
25
+ if (this.#started)
26
+ return;
27
+ this.#started = true;
28
+ this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.name, this);
29
+ const handle = this.#options.http.ws(this.#options.config.path);
30
+ this.#wsRelease = handle.onConnection((connection) => {
31
+ this.#acceptConnection(connection);
32
+ });
33
+ logger.info(formatCompact({
34
+ op: 'listen',
35
+ endpoint: this.#options.config.name,
36
+ mode: 'wss',
37
+ path: this.#options.config.path,
38
+ }));
39
+ }
40
+ open() {
41
+ this.#open = true;
42
+ }
43
+ close() {
44
+ this.#open = false;
45
+ }
46
+ async stop() {
47
+ this.#open = false;
48
+ this.#unregisterAgent?.();
49
+ this.#unregisterAgent = undefined;
50
+ this.#wsRelease?.();
51
+ this.#wsRelease = undefined;
52
+ if (this.#heartbeatTimer) {
53
+ clearInterval(this.#heartbeatTimer);
54
+ this.#heartbeatTimer = undefined;
55
+ }
56
+ if (this.#ws) {
57
+ try {
58
+ this.#ws.close();
59
+ }
60
+ catch {
61
+ /* ignore */
62
+ }
63
+ this.#ws = undefined;
64
+ }
65
+ this.#started = false;
66
+ }
67
+ async send({ target, payload }) {
68
+ const message = formatOutboundSegments(payload);
69
+ const { action, params } = buildSendAction(target, message);
70
+ const data = await this.callApi(action, params);
71
+ const messageId = formatOutboundMessageId(target, data?.message_seq);
72
+ logger.debug(formatCompact({
73
+ op: 'milky_send',
74
+ endpoint: this.#options.config.name,
75
+ target,
76
+ messageId,
77
+ }));
78
+ return messageId;
79
+ }
80
+ callApi(action, params = {}) {
81
+ return this.#callApi(this.apiOptions(), action, params);
82
+ }
83
+ async recallMessage(id) {
84
+ const parsed = parseMilkyMessageId(id);
85
+ if (!parsed)
86
+ throw new Error(`Invalid message id: ${id}`);
87
+ if (parsed.message_scene === 'group') {
88
+ await this.callApi('recall_group_message', {
89
+ group_id: parsed.peer_id,
90
+ message_seq: parsed.message_seq,
91
+ });
92
+ }
93
+ else {
94
+ await this.callApi('recall_private_message', {
95
+ user_id: parsed.peer_id,
96
+ message_seq: parsed.message_seq,
97
+ });
98
+ }
99
+ }
100
+ async kickMember(groupId, userId, rejectAddRequest = false) {
101
+ await this.callApi('kick_group_member', {
102
+ group_id: groupId,
103
+ user_id: userId,
104
+ reject_add_request: rejectAddRequest,
105
+ });
106
+ return true;
107
+ }
108
+ async muteMember(groupId, userId, duration = 600) {
109
+ await this.callApi('set_group_member_mute', {
110
+ group_id: groupId,
111
+ user_id: userId,
112
+ duration,
113
+ });
114
+ return true;
115
+ }
116
+ async muteAll(groupId, enable = true) {
117
+ await this.callApi('set_group_whole_mute', { group_id: groupId, is_mute: enable });
118
+ return true;
119
+ }
120
+ async setAdmin(groupId, userId, enable = true) {
121
+ await this.callApi('set_group_member_admin', {
122
+ group_id: groupId,
123
+ user_id: userId,
124
+ is_set: enable,
125
+ });
126
+ return true;
127
+ }
128
+ async setCard(groupId, userId, card) {
129
+ await this.callApi('set_group_member_card', {
130
+ group_id: groupId,
131
+ user_id: userId,
132
+ card,
133
+ });
134
+ return true;
135
+ }
136
+ async setTitle(groupId, userId, title) {
137
+ await this.callApi('set_group_member_special_title', {
138
+ group_id: groupId,
139
+ user_id: userId,
140
+ special_title: title,
141
+ });
142
+ return true;
143
+ }
144
+ async setGroupName(groupId, name) {
145
+ await this.callApi('set_group_name', { group_id: groupId, new_group_name: name });
146
+ return true;
147
+ }
148
+ async getMemberList(groupId) {
149
+ return this.callApi('get_group_member_list', { group_id: groupId });
150
+ }
151
+ async getGroupInfo(groupId) {
152
+ return this.callApi('get_group_info', { group_id: groupId });
153
+ }
154
+ admit(event) {
155
+ const data = parseMessageReceiveData(event);
156
+ if (!this.#open || !data)
157
+ return;
158
+ this.#admitMessage(data, event);
159
+ }
160
+ apiOptions() {
161
+ return {
162
+ baseUrl: this.#options.config.baseUrl,
163
+ access_token: this.#options.config.access_token,
164
+ };
165
+ }
166
+ #admitMessage(data, event) {
167
+ const target = formatInboundTarget(data);
168
+ const content = formatInboundContent(data);
169
+ const audioUrl = extractInboundAudioUrl(data);
170
+ const nickname = senderNickname(data);
171
+ const mentioned = isMentioned(data, event.self_id);
172
+ void this.#options.gateway.receive({
173
+ adapter: this.#options.id,
174
+ target,
175
+ content,
176
+ sender: String(data.sender_id),
177
+ id: formatInboundMessageId(data),
178
+ metadata: Object.freeze({
179
+ message_scene: data.message_scene,
180
+ peer_id: String(data.peer_id),
181
+ sender_id: String(data.sender_id),
182
+ message_seq: data.message_seq,
183
+ endpoint: this.#options.config.name,
184
+ time: data.time ?? event.time,
185
+ self_id: event.self_id != null ? String(event.self_id) : undefined,
186
+ ...(nickname ? { nickname } : {}),
187
+ ...(mentioned ? { mentioned: true } : {}),
188
+ ...(audioUrl ? { audio_url: audioUrl } : {}),
189
+ }),
190
+ }).catch((err) => {
191
+ logger.warn(formatCompact({
192
+ op: 'milky_gateway_receive_failed',
193
+ target,
194
+ error: err instanceof Error ? err.message : String(err),
195
+ }));
196
+ });
197
+ }
198
+ #acceptConnection(connection) {
199
+ if (!verifyMilkyAccessToken(this.#options.config.access_token, connection.request)) {
200
+ connection.socket.close(4003, 'Unauthorized');
201
+ return;
202
+ }
203
+ const socket = connection.socket;
204
+ if (this.#ws) {
205
+ try {
206
+ this.#ws.close();
207
+ }
208
+ catch {
209
+ /* ignore */
210
+ }
211
+ }
212
+ this.#ws = socket;
213
+ this.#startHeartbeat();
214
+ socket.on('message', (data) => {
215
+ this.#onMessage(data);
216
+ });
217
+ socket.on('close', () => {
218
+ if (this.#ws === socket)
219
+ this.#ws = undefined;
220
+ });
221
+ logger.debug(formatCompact({
222
+ endpoint: this.#options.config.name,
223
+ mode: 'wss',
224
+ peer: connection.request.socket.remoteAddress,
225
+ }));
226
+ }
227
+ #onMessage(data) {
228
+ try {
229
+ const raw = typeof data === 'string'
230
+ ? data
231
+ : Buffer.isBuffer(data)
232
+ ? data.toString()
233
+ : data instanceof ArrayBuffer
234
+ ? new TextDecoder().decode(data)
235
+ : String(data ?? '');
236
+ const event = JSON.parse(raw);
237
+ this.admit(event);
238
+ }
239
+ catch (error) {
240
+ logger.warn(formatCompact({
241
+ op: 'milky_parse_failed',
242
+ endpoint: this.#options.config.name,
243
+ error: error instanceof Error ? error.message : String(error),
244
+ }));
245
+ }
246
+ }
247
+ #startHeartbeat() {
248
+ if (this.#heartbeatTimer)
249
+ clearInterval(this.#heartbeatTimer);
250
+ const interval = this.#options.config.heartbeat_interval;
251
+ if (interval <= 0)
252
+ return;
253
+ this.#heartbeatTimer = setInterval(() => {
254
+ try {
255
+ if (this.#ws?.readyState === WS_OPEN)
256
+ this.#ws.ping?.();
257
+ }
258
+ catch {
259
+ /* ignore */
260
+ }
261
+ }, interval);
262
+ }
263
+ }