@zhin.js/adapter-milky 1.1.18 → 1.1.20
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.
- package/CHANGELOG.md +43 -0
- package/README.md +9 -6
- package/adapters/milky/index.js +3 -36
- package/adapters/milky/index.ts +2 -41
- package/commands/milky/endpoint/definition.js +0 -2
- package/commands/milky/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +21 -0
- package/lib/adapter.js +36 -0
- package/lib/client.d.ts +20 -15
- package/lib/client.js +28 -21
- package/lib/console.d.ts +1 -1
- package/lib/console.js +2 -5
- package/lib/endpoint-management.d.ts +2 -2
- package/lib/endpoint-management.js +8 -8
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/protocol.d.ts +6 -1
- package/lib/protocol.js +15 -14
- package/lib/sse-endpoint.d.ts +5 -10
- package/lib/sse-endpoint.js +52 -33
- package/lib/webhook-endpoint.d.ts +5 -10
- package/lib/webhook-endpoint.js +55 -34
- package/lib/ws-endpoint.d.ts +5 -11
- package/lib/ws-endpoint.js +54 -35
- package/lib/wss-endpoint.d.ts +5 -10
- package/lib/wss-endpoint.js +52 -33
- package/package.json +17 -17
- package/plugin.js +0 -4
- package/src/adapter.ts +39 -0
- package/src/client.ts +37 -43
- package/src/console.ts +2 -5
- package/src/endpoint-management.ts +15 -12
- package/src/index.ts +2 -1
- package/src/protocol.ts +18 -12
- package/src/sse-endpoint.ts +45 -31
- package/src/webhook-endpoint.ts +48 -32
- package/src/ws-endpoint.ts +49 -35
- package/src/wss-endpoint.ts +45 -31
- package/lib/milky-runtime-state.d.ts +0 -1
- package/lib/milky-runtime-state.js +0 -6
- package/src/milky-runtime-state.ts +0 -7
package/src/sse-endpoint.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Milky SSE client endpoint — GET text/event-stream on /event.
|
|
3
3
|
*/
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
|
|
6
6
|
createRecallEndpointControl,
|
|
7
7
|
createEndpointLifecycle,
|
|
8
8
|
type EndpointConnectHandle,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
15
15
|
import type { CapabilityId } from 'zhin.js';
|
|
16
16
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
17
|
-
import {
|
|
17
|
+
import { createMilkyActionCaller, readMilkyResponse, type MilkyActionCaller, milkyClientOptions, forwardMilkyClientEvents, MilkyClient } from './client.js';
|
|
18
18
|
import {
|
|
19
19
|
buildSendAction,
|
|
20
20
|
buildSseConnectOptions,
|
|
@@ -46,38 +46,49 @@ export type CreateMilkySseStream = (options: {
|
|
|
46
46
|
|
|
47
47
|
export interface MilkySseEndpointOptions {
|
|
48
48
|
readonly id: CapabilityId;
|
|
49
|
+
readonly signal?: AbortSignal;
|
|
49
50
|
readonly config: MilkySseConfig;
|
|
50
51
|
readonly createSseStream?: CreateMilkySseStream;
|
|
51
52
|
readonly callApi?: typeof callApi;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
export class MilkySseEndpoint extends
|
|
55
|
-
readonly
|
|
55
|
+
export class MilkySseEndpoint extends MilkyClient implements Endpoint {
|
|
56
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
57
|
+
#open = false;
|
|
56
58
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
57
59
|
|
|
58
60
|
readonly #options: MilkySseEndpointOptions;
|
|
59
|
-
readonly #
|
|
60
|
-
readonly
|
|
61
|
-
readonly control: EndpointControl = createRecallEndpointControl((id) => this
|
|
61
|
+
readonly #management: EndpointManagement;
|
|
62
|
+
readonly #call: MilkyActionCaller;
|
|
63
|
+
readonly #control: EndpointControl = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
|
|
62
64
|
readonly #lifecycle: EndpointLifecycle;
|
|
63
65
|
#stream?: SseClientHandle;
|
|
64
66
|
|
|
65
67
|
constructor(options: MilkySseEndpointOptions) {
|
|
66
|
-
|
|
68
|
+
const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
|
|
69
|
+
super(milkyClientOptions({ config: options.config, call }));
|
|
70
|
+
this.#call = call;
|
|
67
71
|
this.#logger = getAdapterLogger('milky', options.config.id);
|
|
68
72
|
this.#options = options;
|
|
69
|
-
this.#
|
|
70
|
-
|
|
71
|
-
this.management = createMilkyEndpointManagement({
|
|
72
|
-
callApi: (action, params) => callMilkyClient(this.client, action, params),
|
|
73
|
+
this.#management = createMilkyEndpointManagement({
|
|
74
|
+
callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
|
|
73
75
|
});
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
const readTransportState = () => this.#lifecycle.state;
|
|
77
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
78
|
+
name: options.config.id,
|
|
79
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
80
|
+
close: () => { this.#open = false; }, stop: () => this.#stop(),
|
|
81
|
+
send: request => this.#send(request),
|
|
82
|
+
management: this.#management, control: this.#control,
|
|
83
|
+
get transportState() { return readTransportState(); },
|
|
84
|
+
});
|
|
85
|
+
forwardMilkyClientEvents(this, (name, payload) => {
|
|
86
|
+
void this[endpointSpec].events.run(() => {
|
|
87
|
+
if (!this.#open && !this.#lifecycle.started) return;
|
|
88
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
77
89
|
if (name === 'event') this.#admitRaw(payload as MilkyEvent);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
);
|
|
90
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
91
|
+
});
|
|
81
92
|
this.#lifecycle = createEndpointLifecycle({
|
|
82
93
|
name: options.config.id,
|
|
83
94
|
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
@@ -90,7 +101,7 @@ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
90
101
|
});
|
|
91
102
|
}
|
|
92
103
|
|
|
93
|
-
async start(): Promise<void> {
|
|
104
|
+
async #start(): Promise<void> {
|
|
94
105
|
if (this.#lifecycle.started) return;
|
|
95
106
|
try {
|
|
96
107
|
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
@@ -100,17 +111,17 @@ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
100
111
|
}
|
|
101
112
|
}
|
|
102
113
|
|
|
103
|
-
async stop(): Promise<void> {
|
|
104
|
-
this
|
|
114
|
+
async #stop(): Promise<void> {
|
|
115
|
+
this.#open = false;
|
|
105
116
|
await this.#lifecycle.stop();
|
|
106
117
|
this.#stream?.close();
|
|
107
118
|
this.#stream = undefined;
|
|
108
119
|
}
|
|
109
120
|
|
|
110
|
-
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
121
|
+
async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
111
122
|
const message = formatOutboundSegments(payload);
|
|
112
123
|
const { action, params } = buildSendAction(conversation, message);
|
|
113
|
-
const data =
|
|
124
|
+
const data = readMilkyResponse<{ message_seq?: number }>(await this.#call({ action, params, signal }), action);
|
|
114
125
|
const messageId = formatOutboundMessageId(conversation, data?.message_seq);
|
|
115
126
|
this.#logger.debug(formatCompact({
|
|
116
127
|
op: 'milky_send',
|
|
@@ -122,23 +133,24 @@ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
122
133
|
return messageId;
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
async recallMessage(id: string): Promise<void> {
|
|
136
|
+
async #recallMessage({ id, signal }: { readonly id: string; readonly signal?: AbortSignal }): Promise<void> {
|
|
126
137
|
const parsed = parseMilkyMessageId(id);
|
|
127
138
|
if (!parsed) throw new Error(`Invalid message id: ${id}`);
|
|
128
139
|
if (parsed.message_scene === 'group') {
|
|
129
|
-
await
|
|
140
|
+
readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
|
|
130
141
|
group_id: parsed.peer_id,
|
|
131
142
|
message_seq: parsed.message_seq,
|
|
132
|
-
});
|
|
143
|
+
} }), 'recall_group_message');
|
|
133
144
|
} else {
|
|
134
|
-
await
|
|
145
|
+
readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
|
|
135
146
|
user_id: parsed.peer_id,
|
|
136
147
|
message_seq: parsed.message_seq,
|
|
137
|
-
});
|
|
148
|
+
} }), 'recall_private_message');
|
|
138
149
|
}
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
#admitRaw(event: MilkyEvent): void {
|
|
153
|
+
if (event.self_id != null) this[endpointSpec].reportSelfUserId(String(event.self_id));
|
|
142
154
|
const data = parseMessageReceiveData(event);
|
|
143
155
|
if (!data) return;
|
|
144
156
|
this.#admitMessage(data, event);
|
|
@@ -149,14 +161,16 @@ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
152
|
-
const conversation = milkyInboundConversation(String(this.#options.id), data);
|
|
164
|
+
const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
|
|
153
165
|
const target = `${conversation.kind}:${conversation.id}`;
|
|
154
166
|
const content = formatInboundContent(data);
|
|
155
167
|
const segments = formatInboundSegments(data);
|
|
156
168
|
const audioUrl = extractInboundAudioUrl(data);
|
|
157
169
|
const nickname = senderNickname(data);
|
|
158
170
|
const mentioned = isMentioned(data, event.self_id);
|
|
159
|
-
void this.
|
|
171
|
+
void this[endpointSpec].events.message({
|
|
172
|
+
event,
|
|
173
|
+
...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
|
|
160
174
|
conversation,
|
|
161
175
|
message: { conversation, id: formatInboundMessageId(data) },
|
|
162
176
|
content,
|
|
@@ -245,7 +259,7 @@ export class MilkySseEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
245
259
|
#onMessage(data: string): void {
|
|
246
260
|
try {
|
|
247
261
|
const event = JSON.parse(data) as MilkyEvent;
|
|
248
|
-
this.
|
|
262
|
+
this.ingest(event as Parameters<MilkyClient['ingest']>[0]);
|
|
249
263
|
} catch (error) {
|
|
250
264
|
this.#logger.warn(formatCompact({
|
|
251
265
|
op: 'milky_parse_failed',
|
package/src/webhook-endpoint.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Milky webhook endpoint — httpHostToken POST inbound + baseUrl HTTP API outbound.
|
|
3
3
|
*/
|
|
4
4
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import { acceptHttpIngress } from 'imhelper';
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
+
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
|
|
7
8
|
createRecallEndpointControl,
|
|
8
9
|
type EndpointControl,
|
|
9
10
|
type EndpointManagement,
|
|
@@ -14,7 +15,7 @@ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
|
14
15
|
import type { CapabilityId } from 'zhin.js';
|
|
15
16
|
import { verifyMilkyAccessToken } from './milky-auth.js';
|
|
16
17
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
17
|
-
import {
|
|
18
|
+
import { createMilkyActionCaller, readMilkyResponse, type MilkyActionCaller, milkyClientOptions, forwardMilkyClientEvents, MilkyClient } from './client.js';
|
|
18
19
|
import {
|
|
19
20
|
buildSendAction,
|
|
20
21
|
callApi,
|
|
@@ -36,41 +37,50 @@ import {
|
|
|
36
37
|
|
|
37
38
|
export interface MilkyWebhookEndpointOptions {
|
|
38
39
|
readonly id: CapabilityId;
|
|
40
|
+
readonly signal?: AbortSignal;
|
|
39
41
|
readonly http: HttpHost;
|
|
40
42
|
readonly config: MilkyWebhookConfig;
|
|
41
43
|
readonly callApi?: typeof callApi;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
export class MilkyWebhookEndpoint extends
|
|
45
|
-
readonly
|
|
46
|
+
export class MilkyWebhookEndpoint extends MilkyClient implements Endpoint {
|
|
47
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
48
|
+
#open = false;
|
|
46
49
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
47
50
|
|
|
48
51
|
readonly #options: MilkyWebhookEndpointOptions;
|
|
49
|
-
readonly #
|
|
50
|
-
readonly
|
|
51
|
-
readonly control: EndpointControl = createRecallEndpointControl((id) => this
|
|
52
|
+
readonly #management: EndpointManagement;
|
|
53
|
+
readonly #call: MilkyActionCaller;
|
|
54
|
+
readonly #control: EndpointControl = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
|
|
52
55
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
53
56
|
#started = false;
|
|
54
57
|
|
|
55
58
|
constructor(options: MilkyWebhookEndpointOptions) {
|
|
56
|
-
|
|
59
|
+
const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
|
|
60
|
+
super(milkyClientOptions({ config: options.config, call }));
|
|
61
|
+
this.#call = call;
|
|
57
62
|
this.#logger = getAdapterLogger('milky', options.config.id);
|
|
58
63
|
this.#options = options;
|
|
59
|
-
this.#
|
|
60
|
-
|
|
61
|
-
this.management = createMilkyEndpointManagement({
|
|
62
|
-
callApi: (action, params) => callMilkyClient(this.client, action, params),
|
|
64
|
+
this.#management = createMilkyEndpointManagement({
|
|
65
|
+
callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
|
|
63
66
|
});
|
|
64
|
-
this
|
|
65
|
-
|
|
66
|
-
(
|
|
67
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
68
|
+
name: options.config.id,
|
|
69
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
70
|
+
close: () => { this.#open = false; }, stop: () => this.#stop(),
|
|
71
|
+
send: request => this.#send(request),
|
|
72
|
+
management: this.#management, control: this.#control,
|
|
73
|
+
});
|
|
74
|
+
forwardMilkyClientEvents(this, (name, payload) => {
|
|
75
|
+
void this[endpointSpec].events.run(() => {
|
|
76
|
+
if (!this.#started) return;
|
|
77
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
67
78
|
if (name === 'event') this.#admitRaw(payload as MilkyEvent);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
);
|
|
79
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
80
|
+
});
|
|
71
81
|
}
|
|
72
82
|
|
|
73
|
-
async start(): Promise<void> {
|
|
83
|
+
async #start(): Promise<void> {
|
|
74
84
|
if (this.#started) return;
|
|
75
85
|
this.#started = true;
|
|
76
86
|
this.#setupRoutes();
|
|
@@ -82,17 +92,17 @@ export class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
82
92
|
}));
|
|
83
93
|
}
|
|
84
94
|
|
|
85
|
-
async stop(): Promise<void> {
|
|
86
|
-
this
|
|
95
|
+
async #stop(): Promise<void> {
|
|
96
|
+
this.#open = false;
|
|
87
97
|
for (const release of this.#routeReleases.splice(0)) release();
|
|
88
98
|
this.#started = false;
|
|
89
99
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
102
|
+
async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
93
103
|
const message = formatOutboundSegments(payload);
|
|
94
104
|
const { action, params } = buildSendAction(conversation, message);
|
|
95
|
-
const data =
|
|
105
|
+
const data = readMilkyResponse<{ message_seq?: number }>(await this.#call({ action, params, signal }), action);
|
|
96
106
|
const messageId = formatOutboundMessageId(conversation, data?.message_seq);
|
|
97
107
|
this.#logger.debug(formatCompact({
|
|
98
108
|
op: 'milky_send',
|
|
@@ -103,23 +113,24 @@ export class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
103
113
|
return messageId;
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
async recallMessage(id: string): Promise<void> {
|
|
116
|
+
async #recallMessage({ id, signal }: { readonly id: string; readonly signal?: AbortSignal }): Promise<void> {
|
|
107
117
|
const parsed = parseMilkyMessageId(id);
|
|
108
118
|
if (!parsed) throw new Error(`Invalid message id: ${id}`);
|
|
109
119
|
if (parsed.message_scene === 'group') {
|
|
110
|
-
await
|
|
120
|
+
readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
|
|
111
121
|
group_id: parsed.peer_id,
|
|
112
122
|
message_seq: parsed.message_seq,
|
|
113
|
-
});
|
|
123
|
+
} }), 'recall_group_message');
|
|
114
124
|
} else {
|
|
115
|
-
await
|
|
125
|
+
readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
|
|
116
126
|
user_id: parsed.peer_id,
|
|
117
127
|
message_seq: parsed.message_seq,
|
|
118
|
-
});
|
|
128
|
+
} }), 'recall_private_message');
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
#admitRaw(event: MilkyEvent): void {
|
|
133
|
+
if (event.self_id != null) this[endpointSpec].reportSelfUserId(String(event.self_id));
|
|
123
134
|
const data = parseMessageReceiveData(event);
|
|
124
135
|
if (!data) return;
|
|
125
136
|
this.#admitMessage(data, event);
|
|
@@ -130,14 +141,16 @@ export class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
133
|
-
const conversation = milkyInboundConversation(String(this.#options.id), data);
|
|
144
|
+
const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
|
|
134
145
|
const target = `${conversation.kind}:${conversation.id}`;
|
|
135
146
|
const content = formatInboundContent(data);
|
|
136
147
|
const segments = formatInboundSegments(data);
|
|
137
148
|
const audioUrl = extractInboundAudioUrl(data);
|
|
138
149
|
const nickname = senderNickname(data);
|
|
139
150
|
const mentioned = isMentioned(data, event.self_id);
|
|
140
|
-
void this.
|
|
151
|
+
void this[endpointSpec].events.message({
|
|
152
|
+
event,
|
|
153
|
+
...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
|
|
141
154
|
conversation,
|
|
142
155
|
message: { conversation, id: formatInboundMessageId(data) },
|
|
143
156
|
content,
|
|
@@ -179,12 +192,15 @@ export class MilkyWebhookEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
179
192
|
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
180
193
|
return;
|
|
181
194
|
}
|
|
182
|
-
if (!this
|
|
195
|
+
if (!this.#open) {
|
|
183
196
|
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
184
197
|
response.end(JSON.stringify({ status: 'ok' }));
|
|
185
198
|
return;
|
|
186
199
|
}
|
|
187
|
-
await
|
|
200
|
+
await acceptHttpIngress<MilkyEvent>(request, response, event => {
|
|
201
|
+
void this[endpointSpec].events.run(() => this.ingest(event as Parameters<MilkyClient['ingest']>[0]))
|
|
202
|
+
.catch(error => this.#warnPlatformEvent(error));
|
|
203
|
+
});
|
|
188
204
|
} catch (error) {
|
|
189
205
|
this.#logger.error('Milky webhook error:', error);
|
|
190
206
|
if (!response.headersSent) {
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
|
|
7
7
|
createRecallEndpointControl,
|
|
8
8
|
createEndpointLifecycle,
|
|
9
9
|
type EndpointConnectHandle,
|
|
@@ -16,10 +16,10 @@ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
|
16
16
|
import type { CapabilityId } from 'zhin.js';
|
|
17
17
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
18
18
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
createMilkyActionCaller, readMilkyResponse, type MilkyActionCaller,
|
|
20
|
+
milkyClientOptions,
|
|
21
21
|
forwardMilkyClientEvents,
|
|
22
|
-
|
|
22
|
+
MilkyClient,
|
|
23
23
|
} from './client.js';
|
|
24
24
|
import {
|
|
25
25
|
buildSendAction,
|
|
@@ -46,6 +46,7 @@ const WS_OPEN = 1;
|
|
|
46
46
|
|
|
47
47
|
export interface MilkyWsEndpointOptions {
|
|
48
48
|
readonly id: CapabilityId;
|
|
49
|
+
readonly signal?: AbortSignal;
|
|
49
50
|
readonly config: MilkyWsConfig;
|
|
50
51
|
readonly createWebSocket?: (
|
|
51
52
|
url: string,
|
|
@@ -54,34 +55,44 @@ export interface MilkyWsEndpointOptions {
|
|
|
54
55
|
readonly callApi?: typeof callApi;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
export class MilkyWsEndpoint extends
|
|
58
|
-
readonly
|
|
58
|
+
export class MilkyWsEndpoint extends MilkyClient implements Endpoint {
|
|
59
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
60
|
+
#open = false;
|
|
59
61
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
60
62
|
|
|
61
63
|
readonly #options: MilkyWsEndpointOptions;
|
|
62
|
-
readonly #
|
|
63
|
-
readonly
|
|
64
|
-
readonly control: EndpointControl = createRecallEndpointControl((id) => this
|
|
64
|
+
readonly #management: EndpointManagement;
|
|
65
|
+
readonly #call: MilkyActionCaller;
|
|
66
|
+
readonly #control: EndpointControl = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
|
|
65
67
|
readonly #lifecycle: EndpointLifecycle;
|
|
66
68
|
#ws?: MilkyWsSocket;
|
|
67
69
|
#clientSocketRelease?: () => void;
|
|
68
70
|
|
|
69
71
|
constructor(options: MilkyWsEndpointOptions) {
|
|
70
|
-
|
|
72
|
+
const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
|
|
73
|
+
super(milkyClientOptions({ config: options.config, call }));
|
|
74
|
+
this.#call = call;
|
|
71
75
|
this.#logger = getAdapterLogger('milky', options.config.id);
|
|
72
76
|
this.#options = options;
|
|
73
|
-
this.#
|
|
74
|
-
|
|
75
|
-
this.management = createMilkyEndpointManagement({
|
|
76
|
-
callApi: (action, params) => callMilkyClient(this.client, action, params),
|
|
77
|
+
this.#management = createMilkyEndpointManagement({
|
|
78
|
+
callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
|
|
77
79
|
});
|
|
78
|
-
this.
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
const readTransportState = () => this.#lifecycle.state;
|
|
81
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
82
|
+
name: options.config.id,
|
|
83
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
84
|
+
close: () => { this.#close(); }, stop: () => this.#stop(),
|
|
85
|
+
send: request => this.#send(request),
|
|
86
|
+
management: this.#management, control: this.#control,
|
|
87
|
+
get transportState() { return readTransportState(); },
|
|
88
|
+
});
|
|
89
|
+
forwardMilkyClientEvents(this, (name, payload) => {
|
|
90
|
+
void this[endpointSpec].events.run(() => {
|
|
91
|
+
if (!this.#open && !this.#lifecycle.started) return;
|
|
92
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
81
93
|
if (name === 'event') this.#admitRaw(payload as MilkyEvent);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
);
|
|
94
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
95
|
+
});
|
|
85
96
|
this.#lifecycle = createEndpointLifecycle({
|
|
86
97
|
name: options.config.id,
|
|
87
98
|
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
@@ -94,19 +105,19 @@ export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
94
105
|
});
|
|
95
106
|
}
|
|
96
107
|
|
|
97
|
-
async start(): Promise<void> {
|
|
108
|
+
async #start(): Promise<void> {
|
|
98
109
|
if (this.#lifecycle.started) return;
|
|
99
110
|
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
100
111
|
}
|
|
101
112
|
|
|
102
|
-
close(): void {
|
|
103
|
-
|
|
113
|
+
#close(): void {
|
|
114
|
+
this.#open = false;
|
|
104
115
|
this.#clientSocketRelease?.();
|
|
105
116
|
this.#clientSocketRelease = undefined;
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
async stop(): Promise<void> {
|
|
109
|
-
this
|
|
119
|
+
async #stop(): Promise<void> {
|
|
120
|
+
this.#close();
|
|
110
121
|
await this.#lifecycle.stop();
|
|
111
122
|
if (this.#ws) {
|
|
112
123
|
try {
|
|
@@ -118,10 +129,10 @@ export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
118
129
|
}
|
|
119
130
|
}
|
|
120
131
|
|
|
121
|
-
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
132
|
+
async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
122
133
|
const message = formatOutboundSegments(payload);
|
|
123
134
|
const { action, params } = buildSendAction(conversation, message);
|
|
124
|
-
const data =
|
|
135
|
+
const data = readMilkyResponse<{ message_seq?: number }>(await this.#call({ action, params, signal }), action);
|
|
125
136
|
const messageId = formatOutboundMessageId(conversation, data?.message_seq);
|
|
126
137
|
this.#logger.debug(formatCompact({
|
|
127
138
|
op: 'milky_send',
|
|
@@ -132,23 +143,24 @@ export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
132
143
|
return messageId;
|
|
133
144
|
}
|
|
134
145
|
|
|
135
|
-
async recallMessage(id: string): Promise<void> {
|
|
146
|
+
async #recallMessage({ id, signal }: { readonly id: string; readonly signal?: AbortSignal }): Promise<void> {
|
|
136
147
|
const parsed = parseMilkyMessageId(id);
|
|
137
148
|
if (!parsed) throw new Error(`Invalid message id: ${id}`);
|
|
138
149
|
if (parsed.message_scene === 'group') {
|
|
139
|
-
await
|
|
150
|
+
readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
|
|
140
151
|
group_id: parsed.peer_id,
|
|
141
152
|
message_seq: parsed.message_seq,
|
|
142
|
-
});
|
|
153
|
+
} }), 'recall_group_message');
|
|
143
154
|
} else {
|
|
144
|
-
await
|
|
155
|
+
readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
|
|
145
156
|
user_id: parsed.peer_id,
|
|
146
157
|
message_seq: parsed.message_seq,
|
|
147
|
-
});
|
|
158
|
+
} }), 'recall_private_message');
|
|
148
159
|
}
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
#admitRaw(event: MilkyEvent): void {
|
|
163
|
+
if (event.self_id != null) this[endpointSpec].reportSelfUserId(String(event.self_id));
|
|
152
164
|
const data = parseMessageReceiveData(event);
|
|
153
165
|
if (!data) return;
|
|
154
166
|
this.#admitMessage(data, event);
|
|
@@ -162,14 +174,16 @@ export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
162
174
|
}
|
|
163
175
|
|
|
164
176
|
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
165
|
-
const conversation = milkyInboundConversation(String(this.#options.id), data);
|
|
177
|
+
const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
|
|
166
178
|
const target = `${conversation.kind}:${conversation.id}`;
|
|
167
179
|
const content = formatInboundContent(data);
|
|
168
180
|
const segments = formatInboundSegments(data);
|
|
169
181
|
const audioUrl = extractInboundAudioUrl(data);
|
|
170
182
|
const nickname = senderNickname(data);
|
|
171
183
|
const mentioned = isMentioned(data, event.self_id);
|
|
172
|
-
void this.
|
|
184
|
+
void this[endpointSpec].events.message({
|
|
185
|
+
event,
|
|
186
|
+
...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
|
|
173
187
|
conversation,
|
|
174
188
|
message: { conversation, id: formatInboundMessageId(data) },
|
|
175
189
|
content,
|
|
@@ -242,7 +256,7 @@ export class MilkyWsEndpoint extends ClientEndpoint<MilkyClient> {
|
|
|
242
256
|
});
|
|
243
257
|
|
|
244
258
|
this.#clientSocketRelease?.();
|
|
245
|
-
this.#clientSocketRelease = this.
|
|
259
|
+
this.#clientSocketRelease = this.acceptWebSocket(ws);
|
|
246
260
|
|
|
247
261
|
ws.on('close', (code, reason) => {
|
|
248
262
|
this.#clientSocketRelease?.();
|