@zhin.js/adapter-milky 3.0.2 → 5.0.0

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 (71) hide show
  1. package/CHANGELOG.md +464 -0
  2. package/README.md +39 -124
  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 -0
  7. package/lib/index.js +3 -0
  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 +43 -20
  27. package/plugin.ts +8 -0
  28. package/schema.json +64 -0
  29. package/src/endpoint.ts +26 -0
  30. package/src/index.ts +59 -57
  31. package/src/milky-agent-deps.ts +44 -8
  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/src/adapter.js +0 -99
  41. package/lib/src/adapter.js.map +0 -1
  42. package/lib/src/api.js +0 -48
  43. package/lib/src/api.js.map +0 -1
  44. package/lib/src/endpoint-sse.js +0 -197
  45. package/lib/src/endpoint-sse.js.map +0 -1
  46. package/lib/src/endpoint-webhook.js +0 -189
  47. package/lib/src/endpoint-webhook.js.map +0 -1
  48. package/lib/src/endpoint-ws.js +0 -264
  49. package/lib/src/endpoint-ws.js.map +0 -1
  50. package/lib/src/endpoint-wss.js +0 -227
  51. package/lib/src/endpoint-wss.js.map +0 -1
  52. package/lib/src/index.js +0 -45
  53. package/lib/src/index.js.map +0 -1
  54. package/lib/src/milky-agent-deps.js +0 -10
  55. package/lib/src/milky-agent-deps.js.map +0 -1
  56. package/lib/src/segment-mapper.js +0 -2
  57. package/lib/src/segment-mapper.js.map +0 -1
  58. package/lib/src/types.js +0 -5
  59. package/lib/src/types.js.map +0 -1
  60. package/lib/src/utils.js +0 -169
  61. package/lib/src/utils.js.map +0 -1
  62. package/plugin.yml +0 -3
  63. package/src/adapter.ts +0 -111
  64. package/src/api.ts +0 -63
  65. package/src/endpoint-sse.ts +0 -232
  66. package/src/endpoint-webhook.ts +0 -225
  67. package/src/endpoint-ws.ts +0 -300
  68. package/src/endpoint-wss.ts +0 -272
  69. package/src/segment-mapper.ts +0 -1
  70. package/src/types.ts +0 -73
  71. package/src/utils.ts +0 -182
@@ -1,225 +0,0 @@
1
- /**
2
- * Milky Webhook Bot:无长连接,在 router.post(path) 接收 POST 事件,鉴权后转 Message
3
- */
4
- import { EventEmitter } from 'events';
5
- import { formatCompact, Endpoint, Message, segment, SendOptions, expandInteractiveSegmentsInContent,} from 'zhin.js';
6
- import { firstHeader, firstQuery, registerFetchRoute, type Router, type RouterContext } from '@zhin.js/host-router/router';
7
- import { callApi } from './api.js';
8
- import type { MilkyWebhookConfig, MilkyEvent } from './types.js';
9
- import type { MilkyAdapter } from './adapter.js';
10
- import {
11
- formatMilkyMessagePayload,
12
- parseMessageReceiveData,
13
- toMilkyOutgoingSegments,
14
- parseMilkyMessageId,
15
- } from './utils.js';
16
- import { fromCanonicalSegments } from './segment-mapper.js';
17
-
18
- function getAccessTokenFromRequest(ctx: RouterContext): string | undefined {
19
- const auth = firstHeader(ctx, 'authorization');
20
- if (auth?.startsWith('Bearer ')) return auth.slice(7);
21
- return firstQuery(ctx, 'access_token');
22
- }
23
-
24
- export class MilkyWebhookEndpoint extends EventEmitter implements Endpoint<MilkyWebhookConfig, MilkyEvent> {
25
- $connected: boolean = true;
26
-
27
- get logger() {
28
- return this.adapter.plugin.logger;
29
- }
30
-
31
- constructor(
32
- public adapter: MilkyAdapter,
33
- public router: Router,
34
- public $config: MilkyWebhookConfig,
35
- ) {
36
- super();
37
- }
38
-
39
- get $id() {
40
- return this.$config.name;
41
- }
42
-
43
- async $connect(): Promise<void> {
44
- const path = this.$config.path.startsWith('/') ? this.$config.path : `/${this.$config.path}`;
45
- const token = this.$config.access_token;
46
- registerFetchRoute(this.router, 'POST', path, async (ctx: RouterContext) => {
47
- const received = getAccessTokenFromRequest(ctx);
48
- if (token && received !== token) {
49
- ctx.status = 401;
50
- ctx.body = { retcode: 401, message: 'Unauthorized' };
51
- return;
52
- }
53
- const body = ctx.request.body;
54
- if (!body || typeof body !== 'object') {
55
- ctx.status = 400;
56
- ctx.body = { retcode: 400, message: 'Invalid JSON' };
57
- return;
58
- }
59
- const event = body as MilkyEvent;
60
- this.handleEvent(event);
61
- ctx.status = 200;
62
- ctx.body = { retcode: 0 };
63
- });
64
- this.logger.debug(formatCompact( { op: 'webhook', path }));
65
- }
66
-
67
- async $disconnect(): Promise<void> {
68
- this.$connected = false;
69
- }
70
-
71
- $formatMessage(event: MilkyEvent): Message<MilkyEvent> {
72
- const data = parseMessageReceiveData(event);
73
- if (!data) {
74
- return Message.from(event, {
75
- $id: '',
76
- $adapter: 'milky',
77
- $endpoint: this.$config.name,
78
- $channel: { id: '', type: 'private' },
79
- $sender: { id: '', name: '' },
80
- $content: [],
81
- $raw: '',
82
- $timestamp: event.time ?? 0,
83
- $recall: async () => {},
84
- $reply: async () => '',
85
- });
86
- }
87
- const payload = formatMilkyMessagePayload(
88
- event,
89
- data,
90
- (id) => this.$recallMessage(id),
91
- (channel, content) =>
92
- this.adapter.sendMessage({
93
- ...channel,
94
- context: 'milky',
95
- endpoint: this.$config.name,
96
- content: content as import('zhin.js').SendContent,
97
- }),
98
- 'milky',
99
- this.$config.name,
100
- );
101
- return Message.from(event, payload);
102
- }
103
-
104
- private handleEvent(event: MilkyEvent): void {
105
- const data = parseMessageReceiveData(event);
106
- if (data) {
107
- const message = this.$formatMessage(event);
108
- this.adapter.emit('message.receive', message);
109
- this.logger.debug(
110
- `${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`,
111
- );
112
- }
113
- }
114
-
115
- private apiOptions() {
116
- return { baseUrl: this.$config.baseUrl, access_token: this.$config.access_token };
117
- }
118
-
119
- async $sendMessage(options: SendOptions): Promise<string> {
120
- const expanded = expandInteractiveSegmentsInContent(options.content);
121
- const arr = Array.isArray(expanded) ? expanded : [expanded];
122
- const wire = fromCanonicalSegments(
123
- arr.map((c) => (typeof c === 'string' ? { type: 'text' as const, data: { text: c } } : c)),
124
- );
125
- const message = toMilkyOutgoingSegments(
126
- wire as (string | { type: string; data?: Record<string, unknown> })[],
127
- );
128
- if (options.type === 'group') {
129
- const result = await callApi(this.apiOptions(), 'send_group_message', {
130
- group_id: parseInt(options.id, 10),
131
- message,
132
- });
133
- const seq = (result as { message_seq?: number }).message_seq;
134
- return seq != null ? `group:${options.id}:${seq}` : '';
135
- }
136
- if (options.type === 'private') {
137
- const result = await callApi(this.apiOptions(), 'send_private_message', {
138
- user_id: parseInt(options.id, 10),
139
- message,
140
- });
141
- const seq = (result as { message_seq?: number }).message_seq;
142
- return seq != null ? `friend:${options.id}:${seq}` : '';
143
- }
144
- throw new Error('Either group or private must be provided');
145
- }
146
-
147
- async $recallMessage(id: string): Promise<void> {
148
- const parsed = parseMilkyMessageId(id);
149
- if (!parsed) throw new Error(`Invalid message id: ${id}`);
150
- if (parsed.message_scene === 'group') {
151
- await callApi(this.apiOptions(), 'recall_group_message', {
152
- group_id: parsed.peer_id,
153
- message_seq: parsed.message_seq,
154
- });
155
- } else {
156
- await callApi(this.apiOptions(), 'recall_private_message', {
157
- user_id: parsed.peer_id,
158
- message_seq: parsed.message_seq,
159
- });
160
- }
161
- }
162
-
163
- async kickMember(groupId: number, userId: number, rejectAddRequest = false): Promise<boolean> {
164
- await callApi(this.apiOptions(), 'kick_group_member', {
165
- group_id: groupId,
166
- user_id: userId,
167
- reject_add_request: rejectAddRequest,
168
- });
169
- return true;
170
- }
171
-
172
- async muteMember(groupId: number, userId: number, duration = 600): Promise<boolean> {
173
- await callApi(this.apiOptions(), 'set_group_member_mute', {
174
- group_id: groupId,
175
- user_id: userId,
176
- duration,
177
- });
178
- return true;
179
- }
180
-
181
- async muteAll(groupId: number, enable = true): Promise<boolean> {
182
- await callApi(this.apiOptions(), 'set_group_whole_mute', { group_id: groupId, is_mute: enable });
183
- return true;
184
- }
185
-
186
- async setAdmin(groupId: number, userId: number, enable = true): Promise<boolean> {
187
- await callApi(this.apiOptions(), 'set_group_member_admin', {
188
- group_id: groupId,
189
- user_id: userId,
190
- is_set: enable,
191
- });
192
- return true;
193
- }
194
-
195
- async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
196
- await callApi(this.apiOptions(), 'set_group_member_card', {
197
- group_id: groupId,
198
- user_id: userId,
199
- card,
200
- });
201
- return true;
202
- }
203
-
204
- async setTitle(groupId: number, userId: number, title: string): Promise<boolean> {
205
- await callApi(this.apiOptions(), 'set_group_member_special_title', {
206
- group_id: groupId,
207
- user_id: userId,
208
- special_title: title,
209
- });
210
- return true;
211
- }
212
-
213
- async setGroupName(groupId: number, name: string): Promise<boolean> {
214
- await callApi(this.apiOptions(), 'set_group_name', { group_id: groupId, new_group_name: name });
215
- return true;
216
- }
217
-
218
- async getMemberList(groupId: number): Promise<unknown[]> {
219
- return callApi(this.apiOptions(), 'get_group_member_list', { group_id: groupId }) as Promise<unknown[]>;
220
- }
221
-
222
- async getGroupInfo(groupId: number): Promise<unknown> {
223
- return callApi(this.apiOptions(), 'get_group_info', { group_id: groupId });
224
- }
225
- }
@@ -1,300 +0,0 @@
1
- /**
2
- * Milky WebSocket 正向连接 Bot(应用连协议端 ws(s)://baseUrl/event)
3
- */
4
- import WebSocket from 'ws';
5
- import { EventEmitter } from 'events';
6
- import { clearInterval } from 'node:timers';
7
- import { formatCompact, Endpoint, Message, segment, SendOptions, expandInteractiveSegmentsInContent,} from 'zhin.js';
8
- import { callApi } from './api.js';
9
- import type { MilkyWsConfig, MilkyEvent } from './types.js';
10
- import type { MilkyAdapter } from './adapter.js';
11
- import {
12
- formatMilkyMessagePayload,
13
- parseMessageReceiveData,
14
- toMilkyOutgoingSegments,
15
- parseMilkyMessageId,
16
- } from './utils.js';
17
- import { fromCanonicalSegments } from './segment-mapper.js';
18
-
19
- export class MilkyWsClient extends EventEmitter implements Endpoint<MilkyWsConfig, MilkyEvent> {
20
- $connected: boolean;
21
- private ws?: WebSocket;
22
- private reconnectTimer?: NodeJS.Timeout;
23
- private heartbeatTimer?: NodeJS.Timeout;
24
-
25
- get logger() {
26
- return this.adapter.plugin.logger;
27
- }
28
-
29
- constructor(public adapter: MilkyAdapter, public $config: MilkyWsConfig) {
30
- super();
31
- this.$connected = false;
32
- }
33
-
34
- get $id() {
35
- return this.$config.name;
36
- }
37
-
38
- private get eventUrl(): string {
39
- const base = this.$config.baseUrl.replace(/\/$/, '');
40
- const url = base.replace(/^http/, 'ws') + '/event';
41
- const token = this.$config.access_token;
42
- if (token) return `${url}${url.includes('?') ? '&' : '?'}access_token=${encodeURIComponent(token)}`;
43
- return url;
44
- }
45
-
46
- private apiOptions() {
47
- return { baseUrl: this.$config.baseUrl, access_token: this.$config.access_token };
48
- }
49
-
50
- async $connect(): Promise<void> {
51
- return new Promise((resolve, reject) => {
52
- const headers: Record<string, string> = {};
53
- if (this.$config.access_token) {
54
- headers['Authorization'] = `Bearer ${this.$config.access_token}`;
55
- }
56
- this.ws = new WebSocket(this.eventUrl, { headers });
57
-
58
- this.ws.on('open', () => {
59
- this.$connected = true;
60
- if (!this.$config.access_token) {
61
- this.logger.warn(formatCompact({ endpoint: this.$id, ok: false, error: 'missing access_token' }));
62
- }
63
- this.logger.debug(formatCompact({ endpoint: this.$id, mode: 'ws' }));
64
- this.startHeartbeat();
65
- resolve();
66
- });
67
-
68
- this.ws.on('message', (data) => {
69
- try {
70
- const event = JSON.parse(data.toString()) as MilkyEvent;
71
- this.handleEvent(event);
72
- } catch (error) {
73
- this.emit('error', error);
74
- }
75
- });
76
-
77
- this.ws.on('close', (code, reason) => {
78
- this.$connected = false;
79
- const reasonStr = reason?.toString?.() || String(reason);
80
- const codeHint = code === 1005 ? ' [无状态,多为服务端/代理未发 close 帧即断开]' : code === 1006 ? ' [异常关闭]' : '';
81
- this.logger.warn(formatCompact( {
82
- op: 'disconnect',
83
- endpoint: this.$config.name,
84
- code,
85
- error: `${reasonStr || 'closed'}${codeHint}`,
86
- reconnect_ms: this.$config.reconnect_interval ?? 5000,
87
- }));
88
- reject(new Error(`WS closed: ${code} ${reasonStr}`));
89
- this.scheduleReconnect();
90
- });
91
-
92
- this.ws.on('error', (error) => {
93
- this.logger.warn(formatCompact( {
94
- op: 'ws_error',
95
- endpoint: this.$config.name,
96
- ok: false,
97
- error: error instanceof Error ? error.message : String(error),
98
- }));
99
- reject(error);
100
- });
101
- });
102
- }
103
-
104
- async $disconnect(): Promise<void> {
105
- if (this.reconnectTimer) {
106
- clearTimeout(this.reconnectTimer);
107
- this.reconnectTimer = undefined;
108
- }
109
- if (this.heartbeatTimer) {
110
- clearInterval(this.heartbeatTimer);
111
- this.heartbeatTimer = undefined;
112
- }
113
- if (this.ws) {
114
- this.ws.close();
115
- this.ws = undefined;
116
- }
117
- this.$connected = false;
118
- }
119
-
120
- private handleEvent(event: MilkyEvent): void {
121
- const data = parseMessageReceiveData(event);
122
- if (data) {
123
- const message = this.$formatMessage(event);
124
- this.adapter.emit('message.receive', message);
125
- this.logger.debug(
126
- `${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`,
127
- );
128
- }
129
- // 其他 event_type 可在此扩展 Notice / Request
130
- }
131
-
132
- $formatMessage(event: MilkyEvent): Message<MilkyEvent> {
133
- const data = parseMessageReceiveData(event);
134
- if (!data) {
135
- return Message.from(event, {
136
- $id: '',
137
- $adapter: 'milky',
138
- $endpoint: this.$config.name,
139
- $channel: { id: '', type: 'private' },
140
- $sender: { id: '', name: '' },
141
- $content: [],
142
- $raw: '',
143
- $timestamp: event.time ?? 0,
144
- $recall: async () => {},
145
- $reply: async () => '',
146
- });
147
- }
148
- const payload = formatMilkyMessagePayload(
149
- event,
150
- data,
151
- (id) => this.$recallMessage(id),
152
- (channel, content, _quote) =>
153
- this.adapter.sendMessage({
154
- ...channel,
155
- context: 'milky',
156
- endpoint: this.$config.name,
157
- content: content as import('zhin.js').SendContent,
158
- }),
159
- 'milky',
160
- this.$config.name,
161
- );
162
- return Message.from(event, payload);
163
- }
164
-
165
- async $sendMessage(options: SendOptions): Promise<string> {
166
- const expanded = expandInteractiveSegmentsInContent(options.content);
167
- const arr = Array.isArray(expanded) ? expanded : [expanded];
168
- const wire = fromCanonicalSegments(
169
- arr.map((c) => (typeof c === 'string' ? { type: 'text' as const, data: { text: c } } : c)),
170
- );
171
- const message = toMilkyOutgoingSegments(
172
- wire as (string | { type: string; data?: Record<string, unknown> })[],
173
- );
174
- if (options.type === 'group') {
175
- const result = await callApi(this.apiOptions(), 'send_group_message', {
176
- group_id: parseInt(options.id, 10),
177
- message,
178
- });
179
- const seq = (result as { message_seq?: number }).message_seq;
180
- this.logger.debug(`${this.$config.name} send group(${options.id}):${segment.raw(options.content)}`);
181
- return seq != null ? `group:${options.id}:${seq}` : '';
182
- }
183
- if (options.type === 'private') {
184
- const result = await callApi(this.apiOptions(), 'send_private_message', {
185
- user_id: parseInt(options.id, 10),
186
- message,
187
- });
188
- const seq = (result as { message_seq?: number }).message_seq;
189
- this.logger.debug(`${this.$config.name} send private(${options.id}):${segment.raw(options.content)}`);
190
- return seq != null ? `friend:${options.id}:${seq}` : '';
191
- }
192
- throw new Error('Either group or private must be provided');
193
- }
194
-
195
- async $recallMessage(id: string): Promise<void> {
196
- const parsed = parseMilkyMessageId(id);
197
- if (!parsed) throw new Error(`Invalid message id: ${id}`);
198
- if (parsed.message_scene === 'group') {
199
- await callApi(this.apiOptions(), 'recall_group_message', {
200
- group_id: parsed.peer_id,
201
- message_seq: parsed.message_seq,
202
- });
203
- } else {
204
- await callApi(this.apiOptions(), 'recall_private_message', {
205
- user_id: parsed.peer_id,
206
- message_seq: parsed.message_seq,
207
- });
208
- }
209
- }
210
-
211
- async kickMember(groupId: number, userId: number, rejectAddRequest = false): Promise<boolean> {
212
- await callApi(this.apiOptions(), 'kick_group_member', {
213
- group_id: groupId,
214
- user_id: userId,
215
- reject_add_request: rejectAddRequest,
216
- });
217
- this.logger.debug(formatCompact( { op: 'kick', endpoint: this.$id, group: groupId, user: userId }));
218
- return true;
219
- }
220
-
221
- async muteMember(groupId: number, userId: number, duration = 600): Promise<boolean> {
222
- await callApi(this.apiOptions(), 'set_group_member_mute', {
223
- group_id: groupId,
224
- user_id: userId,
225
- duration,
226
- });
227
- this.logger.debug(formatCompact( { op: duration > 0 ? 'mute' : 'unmute', endpoint: this.$id, group: groupId, user: userId, duration }));
228
- return true;
229
- }
230
-
231
- async muteAll(groupId: number, enable = true): Promise<boolean> {
232
- await callApi(this.apiOptions(), 'set_group_whole_mute', { group_id: groupId, is_mute: enable });
233
- this.logger.debug(formatCompact( { op: 'mute_all', endpoint: this.$id, group: groupId, enable }));
234
- return true;
235
- }
236
-
237
- async setAdmin(groupId: number, userId: number, enable = true): Promise<boolean> {
238
- await callApi(this.apiOptions(), 'set_group_member_admin', {
239
- group_id: groupId,
240
- user_id: userId,
241
- is_set: enable,
242
- });
243
- this.logger.debug(formatCompact( { op: 'set_admin', endpoint: this.$id, group: groupId, user: userId, enable }));
244
- return true;
245
- }
246
-
247
- async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
248
- await callApi(this.apiOptions(), 'set_group_member_card', {
249
- group_id: groupId,
250
- user_id: userId,
251
- card,
252
- });
253
- this.logger.debug(formatCompact( { op: 'set_card', endpoint: this.$id, group: groupId, user: userId }));
254
- return true;
255
- }
256
-
257
- async setTitle(groupId: number, userId: number, title: string): Promise<boolean> {
258
- await callApi(this.apiOptions(), 'set_group_member_special_title', {
259
- group_id: groupId,
260
- user_id: userId,
261
- special_title: title,
262
- });
263
- this.logger.debug(formatCompact( { op: 'set_title', endpoint: this.$id, group: groupId, user: userId }));
264
- return true;
265
- }
266
-
267
- async setGroupName(groupId: number, name: string): Promise<boolean> {
268
- await callApi(this.apiOptions(), 'set_group_name', { group_id: groupId, new_group_name: name });
269
- this.logger.debug(formatCompact( { op: 'set_group_name', endpoint: this.$id, group: groupId }));
270
- return true;
271
- }
272
-
273
- async getMemberList(groupId: number): Promise<unknown[]> {
274
- return callApi(this.apiOptions(), 'get_group_member_list', { group_id: groupId }) as Promise<unknown[]>;
275
- }
276
-
277
- async getGroupInfo(groupId: number): Promise<unknown> {
278
- return callApi(this.apiOptions(), 'get_group_info', { group_id: groupId });
279
- }
280
-
281
- private startHeartbeat(): void {
282
- const interval = this.$config.heartbeat_interval ?? 30000;
283
- if (interval <= 0) return; // 设为 0 可关闭心跳(部分网关如 onebots 对 ping 处理异常时会 1006 断连)
284
- this.heartbeatTimer = setInterval(() => {
285
- if (this.ws?.readyState === WebSocket.OPEN) this.ws.ping();
286
- }, interval);
287
- }
288
-
289
- private scheduleReconnect(): void {
290
- if (this.reconnectTimer) return;
291
- const interval = this.$config.reconnect_interval ?? 5000;
292
- this.reconnectTimer = setTimeout(() => {
293
- this.reconnectTimer = undefined;
294
- this.$connect().catch((err) => {
295
- this.emit('error', err);
296
- this.scheduleReconnect();
297
- });
298
- }, interval);
299
- }
300
- }